@yewdiel21/motion-kit 0.17.0 → 0.19.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 +240 -199
- package/dist/auto/index.d.ts +19 -3
- package/dist/auto/index.js +35 -3
- package/dist/auto/index.js.map +1 -1
- package/dist/{chunk-QFBOHPWZ.js → chunk-CQP66AZK.js} +2 -2
- package/dist/{chunk-QP4QJOEN.js → chunk-H4STLMCM.js} +2 -2
- package/dist/{chunk-BZLMCHUD.js → chunk-KDIJX6G4.js} +3 -3
- package/dist/{chunk-DOOMULVV.js → chunk-OLT3CWDJ.js} +4 -2
- package/dist/chunk-OLT3CWDJ.js.map +1 -0
- package/dist/{chunk-VJFPTLAK.js → chunk-PEDYJ2TV.js} +8 -22
- package/dist/chunk-PEDYJ2TV.js.map +1 -0
- package/dist/{chunk-7IQTMF7Y.js → chunk-Y4GIBTRG.js} +5130 -2153
- package/dist/chunk-Y4GIBTRG.js.map +1 -0
- package/dist/css/components/badge-button.css +16 -0
- package/dist/css/components/bento-grid.css +13 -0
- package/dist/css/components/code-block.css +267 -0
- package/dist/css/components/command-search.css +274 -2
- package/dist/css/components/dialog.css +264 -0
- package/dist/css/components/drilldown-menu.css +11 -7
- package/dist/css/components/equalizer-slider.css +26 -0
- package/dist/css/components/handwriting-text.css +15 -0
- package/dist/css/components/heatmap-chart.css +267 -1
- package/dist/css/components/magnetize-button.css +18 -0
- package/dist/css/components/morphing-page-dots.css +13 -0
- package/dist/css/components/not-found.css +22 -0
- package/dist/css/components/notification-stack.css +42 -0
- package/dist/css/components/particle-button.css +14 -0
- package/dist/css/components/radio-group.css +29 -0
- package/dist/css/components/rating-scale.css +23 -0
- package/dist/css/components/scroll-area.css +261 -0
- package/dist/css/components/scroll-preview-dock.css +18 -0
- package/dist/css/components/select.css +300 -4
- package/dist/css/components/signature-pad.css +130 -103
- package/dist/css/components/stepper.css +36 -0
- package/dist/css/components/sticky-header.css +10 -0
- package/dist/css/components/temperature-slider.css +29 -0
- package/dist/css/components/voice-message-bubble.css +62 -45
- package/dist/css/components/waveform-player.css +49 -34
- package/dist/css/components/wheel-picker.css +16 -15
- package/dist/css/tokens.css +4 -0
- package/dist/elements/index.d.ts +217 -4
- package/dist/elements/index.js +614 -9
- package/dist/elements/index.js.map +1 -1
- package/dist/{engine-Bn6hC87X.d.ts → engine-D6XV-WNt.d.ts} +390 -1
- package/dist/gl/index.js +2 -2
- package/dist/index.css +886 -177
- package/dist/index.d.ts +23 -23
- package/dist/index.js +37 -5
- package/dist/index.js.map +1 -1
- package/dist/react/index.d.ts +126 -7
- package/dist/react/index.js +722 -7
- package/dist/react/index.js.map +1 -1
- package/dist/{reduced-YS6TZjpa.d.ts → reduced-4croIXl-.d.ts} +1 -1
- package/dist/{store-CZ6vdtIE.d.ts → store-CAHWkurn.d.ts} +1 -1
- package/package.json +110 -110
- package/registry.json +1357 -1160
- package/src/auto/auto-init.ts +341 -308
- package/src/components/badge-button/README.md +28 -0
- package/src/components/badge-button/badge-button.css +15 -0
- package/src/components/badge-button/element.ts +16 -0
- package/src/components/badge-button/engine.ts +85 -0
- package/src/components/badge-button/react.tsx +31 -0
- package/src/components/bento-grid/README.md +26 -0
- package/src/components/bento-grid/bento-grid.css +12 -0
- package/src/components/bento-grid/element.ts +22 -0
- package/src/components/bento-grid/engine.ts +153 -0
- package/src/components/bento-grid/react.tsx +25 -0
- package/src/components/code-block/README.md +1 -1
- package/src/components/code-block/code-block.css +5 -0
- package/src/components/code-block/engine.ts +5 -0
- package/src/components/command-search/README.md +1 -1
- package/src/components/command-search/command-search.css +12 -2
- package/src/components/command-search/engine.ts +6 -0
- package/src/components/dialog/README.md +104 -103
- package/src/components/dialog/dialog.css +2 -0
- package/src/components/dialog/engine.ts +465 -449
- package/src/components/drilldown-menu/drilldown-menu.css +11 -7
- package/src/components/drilldown-menu/engine.ts +44 -4
- package/src/components/equalizer-slider/README.md +43 -0
- package/src/components/equalizer-slider/element.ts +41 -0
- package/src/components/equalizer-slider/engine.ts +32 -0
- package/src/components/equalizer-slider/equalizer-slider.css +26 -0
- package/src/components/equalizer-slider/react.tsx +60 -0
- package/src/components/equalizer-slider/shared.ts +161 -0
- package/src/components/handwriting-text/README.md +29 -0
- package/src/components/handwriting-text/element.ts +14 -0
- package/src/components/handwriting-text/engine.ts +108 -0
- package/src/components/handwriting-text/glyphs.ts +71 -0
- package/src/components/handwriting-text/handwriting-text.css +14 -0
- package/src/components/handwriting-text/react.tsx +23 -0
- package/src/components/heatmap-chart/README.md +48 -48
- package/src/components/heatmap-chart/engine.ts +6 -1
- package/src/components/heatmap-chart/heatmap-chart.css +5 -1
- package/src/components/icons.ts +1 -0
- package/src/components/magnetize-button/README.md +26 -0
- package/src/components/magnetize-button/element.ts +13 -0
- package/src/components/magnetize-button/engine.ts +84 -0
- package/src/components/magnetize-button/magnetize-button.css +17 -0
- package/src/components/magnetize-button/react.tsx +22 -0
- package/src/components/morphing-page-dots/README.md +28 -0
- package/src/components/morphing-page-dots/element.ts +16 -0
- package/src/components/morphing-page-dots/engine.ts +89 -0
- package/src/components/morphing-page-dots/morphing-page-dots.css +12 -0
- package/src/components/morphing-page-dots/react.tsx +32 -0
- package/src/components/not-found/README.md +26 -0
- package/src/components/not-found/element.ts +15 -0
- package/src/components/not-found/engine.ts +57 -0
- package/src/components/not-found/not-found.css +21 -0
- package/src/components/not-found/react.tsx +23 -0
- package/src/components/notification-stack/README.md +24 -0
- package/src/components/notification-stack/element.ts +26 -0
- package/src/components/notification-stack/engine.ts +329 -0
- package/src/components/notification-stack/notification-stack.css +41 -0
- package/src/components/notification-stack/react.tsx +28 -0
- package/src/components/particle-button/README.md +26 -0
- package/src/components/particle-button/element.ts +13 -0
- package/src/components/particle-button/engine.ts +52 -0
- package/src/components/particle-button/particle-button.css +13 -0
- package/src/components/particle-button/react.tsx +22 -0
- package/src/components/radio-group/README.md +43 -0
- package/src/components/radio-group/element.ts +41 -0
- package/src/components/radio-group/engine.ts +11 -0
- package/src/components/radio-group/radio-group.css +28 -0
- package/src/components/radio-group/react.tsx +60 -0
- package/src/components/radio-group/shared.ts +118 -0
- package/src/components/rating-scale/README.md +43 -0
- package/src/components/rating-scale/element.ts +41 -0
- package/src/components/rating-scale/engine.ts +63 -0
- package/src/components/rating-scale/rating-scale.css +22 -0
- package/src/components/rating-scale/react.tsx +60 -0
- package/src/components/scroll-area/README.md +89 -0
- package/src/components/scroll-area/element.ts +35 -0
- package/src/components/scroll-area/engine.ts +528 -0
- package/src/components/scroll-area/react.tsx +67 -0
- package/src/components/scroll-area/scroll-area.css +260 -0
- package/src/components/scroll-preview-dock/README.md +24 -0
- package/src/components/scroll-preview-dock/element.ts +20 -0
- package/src/components/scroll-preview-dock/engine.ts +228 -0
- package/src/components/scroll-preview-dock/react.tsx +26 -0
- package/src/components/scroll-preview-dock/scroll-preview-dock.css +17 -0
- package/src/components/select/README.md +23 -9
- package/src/components/select/engine.ts +16 -4
- package/src/components/select/select.css +38 -4
- package/src/components/signature-pad/engine.ts +550 -437
- package/src/components/signature-pad/signature-pad.css +130 -103
- package/src/components/stepper/README.md +46 -0
- package/src/components/stepper/element.ts +42 -0
- package/src/components/stepper/engine.ts +103 -0
- package/src/components/stepper/react.tsx +60 -0
- package/src/components/stepper/stepper.css +35 -0
- package/src/components/sticky-header/README.md +30 -0
- package/src/components/sticky-header/element.ts +19 -0
- package/src/components/sticky-header/engine.ts +95 -0
- package/src/components/sticky-header/react.tsx +25 -0
- package/src/components/sticky-header/sticky-header.css +9 -0
- package/src/components/temperature-slider/README.md +42 -0
- package/src/components/temperature-slider/element.ts +53 -0
- package/src/components/temperature-slider/engine.ts +44 -0
- package/src/components/temperature-slider/react.tsx +66 -0
- package/src/components/temperature-slider/temperature-slider.css +29 -0
- package/src/components/voice-message-bubble/engine.ts +11 -0
- package/src/components/voice-message-bubble/voice-message-bubble.css +14 -12
- package/src/components/waveform-player/README.md +1 -1
- package/src/components/waveform-player/engine.ts +344 -261
- package/src/components/waveform-player/waveform-player.css +50 -35
- package/src/components/wheel-picker/README.md +3 -1
- package/src/components/wheel-picker/element.ts +12 -5
- package/src/components/wheel-picker/engine.ts +37 -8
- package/src/components/wheel-picker/wheel-picker.css +17 -16
- package/src/core/timing.ts +2 -0
- package/src/css/tokens.css +4 -0
- package/src/elements/index.ts +321 -273
- package/src/index.ts +312 -295
- package/src/motion/morph.ts +5 -2
- package/src/react/index.ts +17 -0
- package/dist/chunk-7IQTMF7Y.js.map +0 -1
- package/dist/chunk-DOOMULVV.js.map +0 -1
- package/dist/chunk-VJFPTLAK.js.map +0 -1
- /package/dist/{chunk-QFBOHPWZ.js.map → chunk-CQP66AZK.js.map} +0 -0
- /package/dist/{chunk-QP4QJOEN.js.map → chunk-H4STLMCM.js.map} +0 -0
- /package/dist/{chunk-BZLMCHUD.js.map → chunk-KDIJX6G4.js.map} +0 -0
|
@@ -1,261 +1,344 @@
|
|
|
1
|
-
import { gsap, EASE, registerMotion } from '../../core/gsap';
|
|
2
|
-
import { reduced } from '../../core/reduced';
|
|
3
|
-
import { createScope } from '../../core/scope';
|
|
4
|
-
import { inert, type Instance } from '../../core/types';
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
export
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
readonly
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const
|
|
45
|
-
const
|
|
46
|
-
const
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
const
|
|
83
|
-
const
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
const
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
1
|
+
import { gsap, EASE, registerMotion } from '../../core/gsap';
|
|
2
|
+
import { reduced } from '../../core/reduced';
|
|
3
|
+
import { createScope } from '../../core/scope';
|
|
4
|
+
import { inert, type Instance } from '../../core/types';
|
|
5
|
+
import { press, rollDigits } from '../../motion/micro';
|
|
6
|
+
|
|
7
|
+
export type WaveformPlayerStatus = 'idle' | 'loading' | 'ready' | 'playing' | 'paused' | 'ended' | 'error';
|
|
8
|
+
export interface WaveformPlayerState {
|
|
9
|
+
status: WaveformPlayerStatus;
|
|
10
|
+
currentTime: number;
|
|
11
|
+
/** 0 until finite metadata is available. Live streams cannot be sought. */
|
|
12
|
+
duration: number;
|
|
13
|
+
playbackRate: number;
|
|
14
|
+
error: string | null;
|
|
15
|
+
}
|
|
16
|
+
export interface WaveformPlayerOptions {
|
|
17
|
+
src?: string;
|
|
18
|
+
/** Normalized amplitudes (0…1), supplied by your application, not audio analysis. */
|
|
19
|
+
peaks?: readonly number[];
|
|
20
|
+
label?: string;
|
|
21
|
+
disabled?: boolean;
|
|
22
|
+
/** Defaults to [1, 1.5, 2]. false hides the rate button. */
|
|
23
|
+
playbackRates?: readonly number[] | false;
|
|
24
|
+
/** Status transitions (time changes have their own callback). */
|
|
25
|
+
onStateChange?: (state: WaveformPlayerState) => void;
|
|
26
|
+
onTimeUpdate?: (state: WaveformPlayerState) => void;
|
|
27
|
+
onPlay?: (state: WaveformPlayerState) => void;
|
|
28
|
+
onPause?: (state: WaveformPlayerState) => void;
|
|
29
|
+
onEnded?: (state: WaveformPlayerState) => void;
|
|
30
|
+
onError?: (state: WaveformPlayerState) => void;
|
|
31
|
+
}
|
|
32
|
+
export interface WaveformPlayerInstance extends Instance {
|
|
33
|
+
/** Resolves false when unavailable, cancelled, or rejected. Never rejects for media errors. */
|
|
34
|
+
play(): Promise<boolean>;
|
|
35
|
+
pause(): void;
|
|
36
|
+
/** Seconds, clamped to the finite duration; ignored until metadata is available. */
|
|
37
|
+
seek(seconds: number): void;
|
|
38
|
+
/** Updates presentation/callbacks in place. A different src pauses and resets playback. */
|
|
39
|
+
update(options: Partial<WaveformPlayerOptions>): void;
|
|
40
|
+
readonly state: WaveformPlayerState;
|
|
41
|
+
readonly audio: HTMLAudioElement | null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const initialState = (): WaveformPlayerState => ({ status: 'idle', currentTime: 0, duration: 0, playbackRate: 1, error: null });
|
|
45
|
+
const finite = (n: number) => Number.isFinite(n) && n >= 0;
|
|
46
|
+
const clock = (n: number) => `${Math.floor(n / 60)}:${String(Math.floor(n % 60)).padStart(2, '0')}`;
|
|
47
|
+
const playIcon = '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="m9 6 9 6-9 6Z"/></svg>';
|
|
48
|
+
const pauseIcon = '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M8 6v12M16 6v12"/></svg>';
|
|
49
|
+
const level = (n: unknown) => typeof n === 'number' && Number.isFinite(n) ? Math.max(0, Math.min(1, n)) : 0;
|
|
50
|
+
/** Averages (or repeats) the supplied amplitudes into `count` bars; it never invents audio data. */
|
|
51
|
+
const resample = (values: readonly number[], count: number) => count === values.length ? [...values] : Array.from({ length: count }, (_, k) => {
|
|
52
|
+
const from = Math.floor(k * values.length / count);
|
|
53
|
+
const slice = values.slice(from, Math.max(from + 1, Math.floor((k + 1) * values.length / count)));
|
|
54
|
+
return slice.reduce((sum, n) => sum + n, 0) / slice.length;
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
export function createWaveformPlayer(root: HTMLElement | null, options: WaveformPlayerOptions = {}): WaveformPlayerInstance {
|
|
58
|
+
if (!root) return inert({ play: async () => false, pause() {}, seek() {}, update() {}, state: initialState(), audio: null });
|
|
59
|
+
registerMotion();
|
|
60
|
+
const scope = createScope();
|
|
61
|
+
let opts = { ...options };
|
|
62
|
+
let state = initialState();
|
|
63
|
+
let source = '';
|
|
64
|
+
let request = 0;
|
|
65
|
+
let requested = false;
|
|
66
|
+
let pending: Promise<boolean> | null = null;
|
|
67
|
+
const make = <K extends keyof HTMLElementTagNameMap>(tag: K, part: string, parent: HTMLElement = root) => {
|
|
68
|
+
const el = document.createElement(tag);
|
|
69
|
+
el.setAttribute(`data-mk-wave-${part}`, '');
|
|
70
|
+
parent.append(el);
|
|
71
|
+
return el;
|
|
72
|
+
};
|
|
73
|
+
// A single owned container preserves authored siblings and makes destroy reversible.
|
|
74
|
+
const container = make('div', 'content');
|
|
75
|
+
scope.add(() => container.remove());
|
|
76
|
+
const audio = make('audio', 'audio', container);
|
|
77
|
+
audio.preload = 'metadata';
|
|
78
|
+
audio.hidden = true;
|
|
79
|
+
const toggle = make('button', 'toggle', container);
|
|
80
|
+
toggle.type = 'button';
|
|
81
|
+
const icon = make('span', 'icon', toggle);
|
|
82
|
+
const body = make('div', 'body', container);
|
|
83
|
+
const track = make('div', 'track', body);
|
|
84
|
+
const bars = make('div', 'bars', track);
|
|
85
|
+
bars.setAttribute('aria-hidden', 'true');
|
|
86
|
+
const head = make('span', 'head', track); head.setAttribute('aria-hidden', 'true');
|
|
87
|
+
const preview = make('span', 'preview', track); preview.setAttribute('aria-hidden', 'true'); preview.hidden = true;
|
|
88
|
+
const range = make('input', 'seek', track);
|
|
89
|
+
range.type = 'range';
|
|
90
|
+
range.min = '0';
|
|
91
|
+
range.max = '0';
|
|
92
|
+
range.step = '0.1';
|
|
93
|
+
const footer = make('div', 'footer', body);
|
|
94
|
+
const time = make('span', 'time', footer);
|
|
95
|
+
const rate = make('button', 'rate', footer);
|
|
96
|
+
rate.type = 'button';
|
|
97
|
+
const message = make('span', 'message', container);
|
|
98
|
+
message.setAttribute('role', 'status');
|
|
99
|
+
message.setAttribute('aria-live', 'polite');
|
|
100
|
+
scope.attr(root, 'data-mk-ready', '');
|
|
101
|
+
scope.attr(root, 'data-mk-wave-state', 'idle');
|
|
102
|
+
scope.attr(root, 'data-mk-wave-scrubbing', null);
|
|
103
|
+
let barNodes: HTMLElement[] = [];
|
|
104
|
+
const effects = new Set<gsap.core.Animation>();
|
|
105
|
+
const keep = (effect: gsap.core.Animation | null) => {
|
|
106
|
+
for (const animation of effects) if (animation.totalProgress() === 1) effects.delete(animation);
|
|
107
|
+
if (effect) effects.add(effect);
|
|
108
|
+
};
|
|
109
|
+
let iconMotion: gsap.core.Tween | null = null;
|
|
110
|
+
let lastPlaying: boolean | undefined;
|
|
111
|
+
let lastRate = '';
|
|
112
|
+
let following = false;
|
|
113
|
+
const scrubbing = () => root.hasAttribute('data-mk-wave-scrubbing');
|
|
114
|
+
// Same response as the equalizer: bars crossing the head pop back to height,
|
|
115
|
+
// except while the head is held, where the value follows the pointer directly.
|
|
116
|
+
const paintProgress = (seconds: number, animate = true) => {
|
|
117
|
+
const progress = state.duration ? Math.max(0, Math.min(1, seconds / state.duration)) : 0;
|
|
118
|
+
track.style.setProperty('--mk-wave-progress', String(progress));
|
|
119
|
+
barNodes.forEach((bar, i) => {
|
|
120
|
+
const played = (i + .5) / barNodes.length <= progress;
|
|
121
|
+
if (bar.hasAttribute('data-mk-played') === played) return;
|
|
122
|
+
bar.toggleAttribute('data-mk-played', played);
|
|
123
|
+
if (animate && !scrubbing() && !reduced()) keep(gsap.fromTo(bar, { scaleY: .72 }, { scaleY: 1, duration: .35, ease: EASE.swatch, overwrite: 'auto', clearProps: 'transform' }));
|
|
124
|
+
});
|
|
125
|
+
};
|
|
126
|
+
const followPlayback = () => { if (!scope.disposed && state.status === 'playing') paintProgress(audio.currentTime); };
|
|
127
|
+
const showPreview = (seconds: number, ratio: number) => {
|
|
128
|
+
preview.textContent = clock(seconds); preview.style.setProperty('--mk-wave-at', String(Math.max(.04, Math.min(.96, ratio)))); preview.hidden = false;
|
|
129
|
+
};
|
|
130
|
+
const snapshot = () => ({ ...state });
|
|
131
|
+
const rates = () => {
|
|
132
|
+
const values = opts.playbackRates === false ? [] : (opts.playbackRates ?? [1, 1.5, 2]);
|
|
133
|
+
return Array.isArray(values) ? [...new Set(values.filter(n => typeof n === 'number' && Number.isFinite(n) && n >= 0.25 && n <= 4))] : [];
|
|
134
|
+
};
|
|
135
|
+
const paint = () => {
|
|
136
|
+
const playing = state.status === 'playing';
|
|
137
|
+
const nextPlaying = playing || requested;
|
|
138
|
+
if (lastPlaying !== nextPlaying) {
|
|
139
|
+
icon.innerHTML = nextPlaying ? pauseIcon : playIcon;
|
|
140
|
+
iconMotion?.kill();
|
|
141
|
+
if (lastPlaying !== undefined && !reduced()) iconMotion = gsap.fromTo(icon, { scale: .55, opacity: .3, filter: 'blur(5px)' }, { scale: 1, opacity: 1, filter: 'blur(0px)', duration: .3, ease: EASE.pop, clearProps: 'transform,opacity,filter' });
|
|
142
|
+
lastPlaying = nextPlaying;
|
|
143
|
+
}
|
|
144
|
+
toggle.setAttribute('aria-label', `${playing || requested ? 'Pausar' : 'Reproducir'} ${opts.label ?? 'audio'}`);
|
|
145
|
+
toggle.disabled = !!opts.disabled || !source;
|
|
146
|
+
range.disabled = !!opts.disabled || !state.duration;
|
|
147
|
+
range.max = String(state.duration);
|
|
148
|
+
range.value = String(state.currentTime);
|
|
149
|
+
range.setAttribute('aria-label', `Posición de ${opts.label ?? 'audio'}`);
|
|
150
|
+
range.setAttribute('aria-valuetext', `${clock(state.currentTime)} de ${clock(state.duration)}`);
|
|
151
|
+
time.textContent = `${clock(state.currentTime)} / ${clock(state.duration)}`;
|
|
152
|
+
rate.hidden = rates().length < 2;
|
|
153
|
+
rate.disabled = !!opts.disabled;
|
|
154
|
+
const rateLabel = `${state.playbackRate}×`;
|
|
155
|
+
if (lastRate !== rateLabel) {
|
|
156
|
+
if (lastRate) keep(rollDigits(rate, rateLabel, 1, .28));
|
|
157
|
+
else rate.textContent = rateLabel;
|
|
158
|
+
lastRate = rateLabel;
|
|
159
|
+
}
|
|
160
|
+
rate.setAttribute('aria-label', `Velocidad ${state.playbackRate}×. Cambiar velocidad`);
|
|
161
|
+
paintProgress(state.currentTime);
|
|
162
|
+
if (document.activeElement === range) showPreview(state.currentTime, state.duration ? state.currentTime / state.duration : 0);
|
|
163
|
+
if (playing && !following) { following = true; gsap.ticker.add(followPlayback); }
|
|
164
|
+
else if (!playing && following) { following = false; gsap.ticker.remove(followPlayback); }
|
|
165
|
+
root.setAttribute('data-mk-wave-state', state.status);
|
|
166
|
+
message.textContent = state.error ?? (state.status === 'loading' ? 'Cargando audio…' : state.status === 'ended' ? 'Audio finalizado' : '');
|
|
167
|
+
};
|
|
168
|
+
const emit = (patch: Partial<WaveformPlayerState>, event?: 'play' | 'pause' | 'ended' | 'error' | 'timeupdate') => {
|
|
169
|
+
if (scope.disposed) return;
|
|
170
|
+
const previous = state.status;
|
|
171
|
+
state = { ...state, ...patch };
|
|
172
|
+
paint();
|
|
173
|
+
const next = snapshot();
|
|
174
|
+
if (event === 'timeupdate') opts.onTimeUpdate?.(next);
|
|
175
|
+
if (previous !== state.status) opts.onStateChange?.(snapshot());
|
|
176
|
+
if (event === 'play') opts.onPlay?.(snapshot());
|
|
177
|
+
if (event === 'pause') opts.onPause?.(snapshot());
|
|
178
|
+
if (event === 'ended') opts.onEnded?.(snapshot());
|
|
179
|
+
if (event === 'error') opts.onError?.(snapshot());
|
|
180
|
+
if (previous !== state.status) root.dispatchEvent(new CustomEvent('mk:audio-statechange', { bubbles: true, detail: snapshot() }));
|
|
181
|
+
if (event) root.dispatchEvent(new CustomEvent(`mk:audio-${event}`, { bubbles: true, detail: snapshot() }));
|
|
182
|
+
};
|
|
183
|
+
const bounce = (target: HTMLElement) => {
|
|
184
|
+
keep(press(target));
|
|
185
|
+
};
|
|
186
|
+
const fail = (message: string) => { requested = false; emit({ status: 'error', error: message }, 'error'); };
|
|
187
|
+
const pause = () => {
|
|
188
|
+
if (scope.disposed) return;
|
|
189
|
+
request++;
|
|
190
|
+
requested = false;
|
|
191
|
+
pending = null;
|
|
192
|
+
audio.pause();
|
|
193
|
+
if (state.status !== 'idle' && state.status !== 'error' && state.status !== 'ended' && state.status !== 'paused') emit({ status: 'paused' }, 'pause');
|
|
194
|
+
else paint();
|
|
195
|
+
};
|
|
196
|
+
const seek = (seconds: number) => {
|
|
197
|
+
if (scope.disposed || !state.duration || !Number.isFinite(seconds)) return;
|
|
198
|
+
const next = Math.max(0, Math.min(state.duration, seconds));
|
|
199
|
+
try { audio.currentTime = next; }
|
|
200
|
+
catch { return; }
|
|
201
|
+
emit({ currentTime: next, ...(state.status === 'ended' && next < state.duration ? { status: 'paused' as const } : {}) }, 'timeupdate');
|
|
202
|
+
};
|
|
203
|
+
const play = (): Promise<boolean> => {
|
|
204
|
+
if (scope.disposed || opts.disabled || !source) return Promise.resolve(false);
|
|
205
|
+
if (pending) return pending;
|
|
206
|
+
if (state.status === 'playing') return Promise.resolve(true);
|
|
207
|
+
if (state.status === 'ended') seek(0);
|
|
208
|
+
const token = ++request;
|
|
209
|
+
requested = true;
|
|
210
|
+
emit({ status: 'loading', error: null });
|
|
211
|
+
// Promise.resolve also supports older media implementations that return void.
|
|
212
|
+
let started: Promise<void>;
|
|
213
|
+
try { started = Promise.resolve(audio.play()); }
|
|
214
|
+
catch (error) { started = Promise.reject(error); }
|
|
215
|
+
const result = started.then(() => {
|
|
216
|
+
if (scope.disposed || token !== request) return false;
|
|
217
|
+
requested = false;
|
|
218
|
+
if (audio.paused) { emit({ status: 'paused' }); return false; }
|
|
219
|
+
if (state.status !== 'playing') emit({ status: 'playing', error: null }, 'play');
|
|
220
|
+
return true;
|
|
221
|
+
}).catch((error: unknown) => {
|
|
222
|
+
if (!scope.disposed && token === request) {
|
|
223
|
+
requested = false;
|
|
224
|
+
if (error instanceof Error && error.name === 'AbortError') emit({ status: 'paused' });
|
|
225
|
+
else fail('No se pudo reproducir el audio. Vuelve a intentarlo.');
|
|
226
|
+
}
|
|
227
|
+
return false;
|
|
228
|
+
}).finally(() => { if (token === request) pending = null; });
|
|
229
|
+
pending = result;
|
|
230
|
+
return result;
|
|
231
|
+
};
|
|
232
|
+
const peakValues = () => {
|
|
233
|
+
const supplied = Array.isArray(opts.peaks) ? opts.peaks : [];
|
|
234
|
+
// Empty data is an explicit neutral placeholder, never fabricated audio analysis.
|
|
235
|
+
return supplied.length ? supplied.slice(0, 256).map(level) : Array<number>(48).fill(0.24);
|
|
236
|
+
};
|
|
237
|
+
// Bars keep the equalizer's pitch (pill plus 3px gap ≈ 8px) at any width.
|
|
238
|
+
const barCount = (values: readonly number[]) => {
|
|
239
|
+
const width = bars.clientWidth;
|
|
240
|
+
return width > 0 ? Math.max(12, Math.min(72, Math.floor((width + 3) / 8))) : values.length;
|
|
241
|
+
};
|
|
242
|
+
const renderPeaks = () => {
|
|
243
|
+
const values = peakValues();
|
|
244
|
+
bars.replaceChildren();
|
|
245
|
+
barNodes = resample(values, barCount(values)).map(peak => {
|
|
246
|
+
const bar = make('i', 'bar', bars);
|
|
247
|
+
bar.style.height = `${8 + peak * 92}%`;
|
|
248
|
+
return bar;
|
|
249
|
+
});
|
|
250
|
+
paintProgress(state.currentTime, false);
|
|
251
|
+
};
|
|
252
|
+
if (typeof ResizeObserver === 'function') scope.observe(new ResizeObserver(() => {
|
|
253
|
+
if (!scope.disposed && barCount(peakValues()) !== barNodes.length) renderPeaks();
|
|
254
|
+
})).observe(bars);
|
|
255
|
+
const update = (next: Partial<WaveformPlayerOptions>) => {
|
|
256
|
+
if (scope.disposed) return;
|
|
257
|
+
const previousPeaks = opts.peaks;
|
|
258
|
+
opts = { ...opts, ...next };
|
|
259
|
+
const nextSource = typeof opts.src === 'string' ? opts.src : '';
|
|
260
|
+
if (source !== nextSource) {
|
|
261
|
+
request++;
|
|
262
|
+
pending = null;
|
|
263
|
+
requested = false;
|
|
264
|
+
audio.pause();
|
|
265
|
+
source = nextSource;
|
|
266
|
+
if (source) audio.setAttribute('src', source);
|
|
267
|
+
else audio.removeAttribute('src');
|
|
268
|
+
// load() aborts the previous resource and any in-flight play request.
|
|
269
|
+
audio.load();
|
|
270
|
+
audio.playbackRate = state.playbackRate;
|
|
271
|
+
emit({ ...initialState(), playbackRate: state.playbackRate, status: source ? 'loading' : 'idle' });
|
|
272
|
+
}
|
|
273
|
+
if (opts.disabled && (requested || state.status === 'playing')) pause();
|
|
274
|
+
if (previousPeaks !== opts.peaks || !barNodes.length) renderPeaks();
|
|
275
|
+
paint();
|
|
276
|
+
};
|
|
277
|
+
scope.on(toggle, 'click', () => { bounce(toggle); if (requested || state.status === 'playing') pause(); else void play(); });
|
|
278
|
+
scope.on(range, 'input', () => seek(Number(range.value)));
|
|
279
|
+
scope.on(range, 'pointermove', event => {
|
|
280
|
+
if (!state.duration || opts.disabled) return;
|
|
281
|
+
const rect = range.getBoundingClientRect();
|
|
282
|
+
if (!rect.width) return;
|
|
283
|
+
// The native thumb spans both insets, as in the equalizer, so its value maps inside them.
|
|
284
|
+
const inset = parseFloat(getComputedStyle(track).getPropertyValue('--mk-wave-inset')) || 0;
|
|
285
|
+
const ratio = Math.max(0, Math.min(1, ((event as PointerEvent).clientX - rect.left - inset) / Math.max(1, rect.width - inset * 2)));
|
|
286
|
+
showPreview(state.duration * ratio, ratio);
|
|
287
|
+
});
|
|
288
|
+
scope.on(range, 'pointerleave', () => { if (document.activeElement !== range) preview.hidden = true; });
|
|
289
|
+
scope.on(range, 'focus', () => { if (state.duration) showPreview(state.currentTime, state.currentTime / state.duration); });
|
|
290
|
+
scope.on(range, 'blur', () => { preview.hidden = true; });
|
|
291
|
+
scope.on(range, 'pointerdown', () => {
|
|
292
|
+
if (range.disabled) return;
|
|
293
|
+
root.setAttribute('data-mk-wave-scrubbing', ''); keep(press(head));
|
|
294
|
+
});
|
|
295
|
+
const release = () => {
|
|
296
|
+
if (!scrubbing()) return;
|
|
297
|
+
root.removeAttribute('data-mk-wave-scrubbing');
|
|
298
|
+
if (!reduced()) keep(gsap.fromTo(head, { scale: 1.12 }, { scale: 1, duration: .4, ease: EASE.swatch, overwrite: 'auto', clearProps: 'transform' }));
|
|
299
|
+
};
|
|
300
|
+
scope.on(window, 'pointerup', release);
|
|
301
|
+
scope.on(window, 'pointercancel', release);
|
|
302
|
+
scope.on(rate, 'click', () => {
|
|
303
|
+
const available = rates();
|
|
304
|
+
const index = available.indexOf(state.playbackRate);
|
|
305
|
+
const next = available[(index + 1) % available.length];
|
|
306
|
+
if (next === undefined) return;
|
|
307
|
+
audio.playbackRate = next;
|
|
308
|
+
emit({ playbackRate: next });
|
|
309
|
+
bounce(rate);
|
|
310
|
+
});
|
|
311
|
+
const metadata = () => {
|
|
312
|
+
const duration = finite(audio.duration) ? audio.duration : 0;
|
|
313
|
+
emit({ duration, currentTime: duration ? Math.min(duration, Math.max(0, audio.currentTime || 0)) : 0,
|
|
314
|
+
...(state.status === 'loading' && !requested ? { status: 'ready' as const } : {}) });
|
|
315
|
+
};
|
|
316
|
+
scope.on(audio, 'loadedmetadata', metadata);
|
|
317
|
+
scope.on(audio, 'durationchange', metadata);
|
|
318
|
+
scope.on(audio, 'timeupdate', () => emit({ currentTime: finite(audio.currentTime) ? audio.currentTime : 0 }, 'timeupdate'));
|
|
319
|
+
scope.on(audio, 'playing', () => {
|
|
320
|
+
if (!source || opts.disabled || !requested && state.status !== 'playing') { audio.pause(); return; }
|
|
321
|
+
requested = false;
|
|
322
|
+
if (state.status !== 'playing') emit({ status: 'playing', error: null }, 'play');
|
|
323
|
+
});
|
|
324
|
+
scope.on(audio, 'pause', () => {
|
|
325
|
+
if (state.status === 'playing' || requested) { requested = false; emit({ status: 'paused' }, 'pause'); }
|
|
326
|
+
});
|
|
327
|
+
scope.on(audio, 'waiting', () => { if (state.status === 'playing') { requested = true; emit({ status: 'loading' }); } });
|
|
328
|
+
scope.on(audio, 'ended', () => { request++; pending = null; requested = false; emit({ status: 'ended', currentTime: state.duration || state.currentTime }, 'ended'); });
|
|
329
|
+
scope.on(audio, 'error', () => { request++; pending = null; fail('No se pudo cargar el audio. Comprueba el archivo.'); });
|
|
330
|
+
scope.on(audio, 'ratechange', () => { if (audio.playbackRate !== state.playbackRate) emit({ playbackRate: audio.playbackRate }); });
|
|
331
|
+
scope.add(() => {
|
|
332
|
+
request++;
|
|
333
|
+
requested = false;
|
|
334
|
+
pending = null;
|
|
335
|
+
iconMotion?.kill();
|
|
336
|
+
for (const effect of effects) effect.kill();
|
|
337
|
+
gsap.ticker.remove(followPlayback);
|
|
338
|
+
audio.pause();
|
|
339
|
+
audio.removeAttribute('src');
|
|
340
|
+
audio.load();
|
|
341
|
+
});
|
|
342
|
+
update(options);
|
|
343
|
+
return { play, pause, seek, update, get state() { return snapshot(); }, get audio() { return scope.disposed ? null : audio; }, destroy: () => scope.dispose() };
|
|
344
|
+
}
|