@yewdiel21/motion-kit 0.16.0 → 0.18.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 +44 -0
- package/dist/auto/index.d.ts +23 -3
- package/dist/auto/index.js +43 -3
- package/dist/auto/index.js.map +1 -1
- package/dist/{chunk-RBEKR5UZ.js → chunk-AWDEKDEJ.js} +2 -2
- package/dist/{chunk-NFZIX3IA.js → chunk-EDMXWUW3.js} +2 -2
- package/dist/{chunk-SDQ776LG.js → chunk-Q7IW4FV3.js} +3941 -43
- package/dist/chunk-Q7IW4FV3.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/drilldown-menu.css +134 -0
- package/dist/css/components/equalizer-slider.css +26 -0
- package/dist/css/components/handwriting-text.css +15 -0
- 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-preview-dock.css +18 -0
- package/dist/css/components/select.css +16 -0
- package/dist/css/components/signature-pad.css +131 -0
- 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 +67 -0
- package/dist/css/components/waveform-player.css +51 -0
- package/dist/css/components/wheel-picker.css +18 -0
- package/dist/css/tokens.css +4 -0
- package/dist/elements/index.d.ts +292 -4
- package/dist/elements/index.js +856 -4
- package/dist/elements/index.js.map +1 -1
- package/dist/{reveal-DRpNO4KP.d.ts → engine-t8d7brjn.d.ts} +534 -1
- package/dist/index.css +713 -0
- package/dist/index.d.ts +23 -23
- package/dist/index.js +43 -3
- package/dist/index.js.map +1 -1
- package/dist/react/index.d.ts +151 -7
- package/dist/react/index.js +893 -3
- package/dist/react/index.js.map +1 -1
- package/dist/{reduced-DQwVXQYb.d.ts → reduced-dPsZYqtI.d.ts} +1 -1
- package/dist/{store-Da3zSh8Z.d.ts → store-6hZNKiee.d.ts} +1 -1
- package/package.json +110 -110
- package/registry.json +244 -0
- package/src/auto/auto-init.ts +339 -293
- 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/drilldown-menu/README.md +163 -0
- package/src/components/drilldown-menu/drilldown-menu.css +133 -0
- package/src/components/drilldown-menu/element.ts +59 -0
- package/src/components/drilldown-menu/engine.ts +378 -0
- package/src/components/drilldown-menu/react.tsx +76 -0
- 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/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-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/select.css +16 -0
- package/src/components/signature-pad/README.md +97 -0
- package/src/components/signature-pad/element.ts +57 -0
- package/src/components/signature-pad/engine.ts +550 -0
- package/src/components/signature-pad/react.tsx +56 -0
- package/src/components/signature-pad/signature-pad.css +130 -0
- 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/README.md +31 -0
- package/src/components/voice-message-bubble/element.ts +20 -0
- package/src/components/voice-message-bubble/engine.ts +79 -0
- package/src/components/voice-message-bubble/react.tsx +27 -0
- package/src/components/voice-message-bubble/voice-message-bubble.css +14 -0
- package/src/components/waveform-player/README.md +30 -0
- package/src/components/waveform-player/element.ts +21 -0
- package/src/components/waveform-player/engine.ts +344 -0
- package/src/components/waveform-player/react.tsx +27 -0
- package/src/components/waveform-player/waveform-player.css +50 -0
- package/src/components/wheel-picker/README.md +35 -0
- package/src/components/wheel-picker/element.ts +25 -0
- package/src/components/wheel-picker/engine.ts +239 -0
- package/src/components/wheel-picker/react.tsx +32 -0
- package/src/components/wheel-picker/wheel-picker.css +17 -0
- package/src/css/tokens.css +4 -0
- package/src/elements/index.ts +318 -258
- package/src/index.ts +21 -0
- package/src/react/index.ts +192 -171
- package/dist/chunk-SDQ776LG.js.map +0 -1
- /package/dist/{chunk-RBEKR5UZ.js.map → chunk-AWDEKDEJ.js.map} +0 -0
- /package/dist/{chunk-NFZIX3IA.js.map → chunk-EDMXWUW3.js.map} +0 -0
|
@@ -0,0 +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
|
+
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
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useRef, type HTMLAttributes, type MutableRefObject } from 'react';
|
|
3
|
+
import { useIsomorphicLayoutEffect } from '../../react/use-recipe';
|
|
4
|
+
import { createWaveformPlayer, type WaveformPlayerInstance, type WaveformPlayerOptions } from './engine';
|
|
5
|
+
|
|
6
|
+
export interface WaveformPlayerProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onPlay' | 'onPause' | 'onEnded' | 'onError' | 'onTimeUpdate' | 'children' | 'dangerouslySetInnerHTML'>, WaveformPlayerOptions {
|
|
7
|
+
instanceRef?: MutableRefObject<WaveformPlayerInstance | null>;
|
|
8
|
+
}
|
|
9
|
+
export function WaveformPlayer({ src, peaks, label, disabled, playbackRates, onStateChange, onTimeUpdate, onPlay, onPause, onEnded, onError, instanceRef, className, ...props }: WaveformPlayerProps) {
|
|
10
|
+
// The engine owns descendants. Also filter runtime JS props that bypass the public type.
|
|
11
|
+
const { children: _children, dangerouslySetInnerHTML: _html, ...rootProps } = props as HTMLAttributes<HTMLDivElement>;
|
|
12
|
+
const root = useRef<HTMLDivElement>(null);
|
|
13
|
+
const instance = useRef<WaveformPlayerInstance | null>(null);
|
|
14
|
+
useIsomorphicLayoutEffect(() => {
|
|
15
|
+
const created = createWaveformPlayer(root.current);
|
|
16
|
+
instance.current = created;
|
|
17
|
+
return () => { created.destroy(); instance.current = null; };
|
|
18
|
+
}, []);
|
|
19
|
+
useIsomorphicLayoutEffect(() => {
|
|
20
|
+
instance.current?.update({ src, peaks, label, disabled, playbackRates, onStateChange, onTimeUpdate, onPlay, onPause, onEnded, onError });
|
|
21
|
+
});
|
|
22
|
+
useIsomorphicLayoutEffect(() => {
|
|
23
|
+
if (instanceRef) instanceRef.current = instance.current;
|
|
24
|
+
return () => { if (instanceRef) instanceRef.current = null; };
|
|
25
|
+
}, [instanceRef]);
|
|
26
|
+
return <div {...rootProps} ref={root} className={['mk-waveform-player', className].filter(Boolean).join(' ')} />;
|
|
27
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/* Shares the equalizer's vocabulary: tonal track, pill bars, a cut-out head that grows when held. */
|
|
2
|
+
.mk-waveform-player, mk-waveform-player {
|
|
3
|
+
--mk-wave-color: var(--mk-text);
|
|
4
|
+
--mk-wave-track: var(--mk-outline-soft);
|
|
5
|
+
--mk-wave-surface: var(--mk-surface-2);
|
|
6
|
+
--mk-wave-inset: 14px;
|
|
7
|
+
display: block;
|
|
8
|
+
width: 100%;
|
|
9
|
+
min-width: 0;
|
|
10
|
+
color: var(--mk-text);
|
|
11
|
+
font-family: var(--mk-font);
|
|
12
|
+
}
|
|
13
|
+
[data-mk-wave-content] { display: grid; grid-template-columns: auto minmax(0, 1fr); align-items: center; column-gap: .875rem; }
|
|
14
|
+
[data-mk-wave-toggle] { display: grid; place-items: center; align-self: start; width: 3.5rem; height: 3.5rem; margin-top: .25rem; border: 0; border-radius: 50%; background: var(--mk-text); color: var(--mk-bg); cursor: pointer; transition: border-radius .4s var(--mk-ease-playful); }
|
|
15
|
+
[data-mk-wave-toggle]:hover:enabled { border-radius: 22px; }
|
|
16
|
+
[data-mk-wave-icon] { display: grid; place-items: center; width: 24px; height: 24px; }
|
|
17
|
+
[data-mk-wave-state='playing'] [data-mk-wave-toggle] { border-radius: 20px; }
|
|
18
|
+
[data-mk-wave-toggle] svg { width: 1.4rem; height: 1.4rem; fill: currentColor; stroke: currentColor; stroke-width: 2; stroke-linecap: round; }
|
|
19
|
+
[data-mk-wave-body] { display: grid; gap: .5rem; min-width: 0; }
|
|
20
|
+
[data-mk-wave-track] { position: relative; height: 4rem; min-width: 0; border-radius: 24px; background: var(--mk-wave-surface); }
|
|
21
|
+
[data-mk-wave-bars] { position: absolute; inset: 12px var(--mk-wave-inset); display: flex; align-items: center; gap: 3px; pointer-events: none; }
|
|
22
|
+
[data-mk-wave-bar] { display: block; flex: 1 1 0; min-width: 1px; min-height: 4px; border-radius: 64px; background: var(--mk-wave-track); transform-origin: center; transition: background-color .16s; }
|
|
23
|
+
[data-mk-wave-bar][data-mk-played] { background: var(--mk-wave-color); }
|
|
24
|
+
[data-mk-wave-head] { position: absolute; z-index: 2; top: 50%; left: calc(var(--mk-wave-inset) + (100% - 2 * var(--mk-wave-inset)) * var(--mk-wave-progress, 0)); box-sizing: content-box; width: 4px; height: calc(100% - 20px); border: 3px solid var(--mk-wave-surface); border-radius: 64px; background: var(--mk-wave-color); translate: -50% -50%; pointer-events: none; transition: height .35s var(--mk-ease-swatch), border-width .35s var(--mk-ease-swatch); }
|
|
25
|
+
[data-mk-wave-scrubbing] [data-mk-wave-head] { height: calc(100% - 10px); border-width: 5px; }
|
|
26
|
+
@media (hover: hover) { [data-mk-wave-track]:has(input:enabled:hover) [data-mk-wave-head] { height: calc(100% - 14px); } }
|
|
27
|
+
[data-mk-wave-preview] { position: absolute; bottom: calc(100% + 10px); left: calc(var(--mk-wave-inset) + (100% - 2 * var(--mk-wave-inset)) * var(--mk-wave-at, 0)); z-index: 3; padding: .4rem .7rem; border-radius: 99px; background: var(--mk-glass-inverse); color: var(--mk-bg); backdrop-filter: blur(16px); box-shadow: inset 1px 1px 1px color-mix(in srgb, var(--mk-bg) 20%, transparent); font-size: .6875rem; font-variant-numeric: tabular-nums; translate: -50% 0; pointer-events: none; }
|
|
28
|
+
[data-mk-wave-preview][hidden] { display: none; }
|
|
29
|
+
[data-mk-wave-seek] { position: absolute; z-index: 4; inset: 0; box-sizing: border-box; width: 100%; height: 100%; margin: 0; appearance: none; -webkit-appearance: none; background: transparent; opacity: 0; cursor: ew-resize; }
|
|
30
|
+
[data-mk-wave-seek]::-webkit-slider-thumb { appearance: none; -webkit-appearance: none; width: calc(2 * var(--mk-wave-inset)); height: 4rem; background: transparent; }
|
|
31
|
+
[data-mk-wave-seek]::-moz-range-thumb { width: calc(2 * var(--mk-wave-inset)); height: 4rem; border: 0; background: transparent; }
|
|
32
|
+
[data-mk-wave-track]:has(input:focus-visible) { outline: 2px solid var(--mk-accent); outline-offset: 4px; }
|
|
33
|
+
[data-mk-wave-footer] { display: flex; justify-content: space-between; align-items: center; gap: .75rem; min-height: 2rem; }
|
|
34
|
+
[data-mk-wave-time] { color: var(--mk-muted); font-size: .75rem; font-variant-numeric: tabular-nums; }
|
|
35
|
+
[data-mk-wave-rate] { min-width: 44px; min-height: 36px; padding: .375rem .75rem; border: 0; border-radius: 64px; color: var(--mk-text); background: var(--mk-wave-surface); font: 600 .75rem var(--mk-font); font-variant-numeric: tabular-nums; cursor: pointer; transition: padding .35s var(--mk-ease-swatch), background .15s; }
|
|
36
|
+
[data-mk-wave-rate]:hover:enabled { padding-inline: 1rem; background: color-mix(in srgb, var(--mk-text) 8%, var(--mk-wave-surface)); }
|
|
37
|
+
[data-mk-wave-rate][hidden] { display: none; }
|
|
38
|
+
[data-mk-wave-message] { grid-column: 2; color: var(--mk-muted); font-size: .75rem; line-height: 1.5; }
|
|
39
|
+
[data-mk-wave-message]:empty { display: none; }
|
|
40
|
+
[data-mk-wave-toggle]:focus-visible, [data-mk-wave-rate]:focus-visible { outline: 2px solid var(--mk-accent); outline-offset: 3px; }
|
|
41
|
+
[data-mk-wave-toggle]:disabled, [data-mk-wave-rate]:disabled { opacity: .5; cursor: default; }
|
|
42
|
+
[data-mk-wave-track]:has(input:disabled) { opacity: .55; }
|
|
43
|
+
[data-mk-wave-track]:has(input:disabled) [data-mk-wave-seek] { cursor: default; }
|
|
44
|
+
[data-mk-wave-state='error'] [data-mk-wave-message] { color: var(--mk-error, var(--mk-text)); }
|
|
45
|
+
@media (prefers-reduced-motion: reduce) { [data-mk-wave-bar], [data-mk-wave-toggle], [data-mk-wave-head], [data-mk-wave-rate] { transition: none; } }
|
|
46
|
+
@media (forced-colors: active) {
|
|
47
|
+
[data-mk-wave-toggle], [data-mk-wave-rate] { border: 1px solid ButtonText; }
|
|
48
|
+
[data-mk-wave-bar] { background: GrayText; }
|
|
49
|
+
[data-mk-wave-bar][data-mk-played], [data-mk-wave-head] { background: Highlight; }
|
|
50
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# wheel-picker
|
|
2
|
+
|
|
3
|
+
Selector de rueda con desplazamiento nativo e inercia en táctil, arrastre de ratón, ajuste al centro y navegación por teclado. El valor elegido queda en un input oculto para formularios.
|
|
4
|
+
|
|
5
|
+
```tsx
|
|
6
|
+
import '@yewdiel21/motion-kit/css';
|
|
7
|
+
import { WheelPicker } from '@yewdiel21/motion-kit/react';
|
|
8
|
+
|
|
9
|
+
<WheelPicker label="Duración" name="duration" value={duration} onChange={setDuration}
|
|
10
|
+
items={[{ value: '15', label: '15 min' }, { value: '25', label: '25 min' }, { value: '45', label: '45 min' }]} />
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
En React, `defaultValue` inicializa el estado sin controlarlo; `instanceRef` permite llamar al motor. Los cambios de `items` y `value` se aplican sin sustituir el listbox.
|
|
14
|
+
|
|
15
|
+
```html
|
|
16
|
+
<mk-wheel-picker label="Duración" name="duration" value="25"
|
|
17
|
+
items='[{"value":"15","label":"15 min"},{"value":"25","label":"25 min"},{"value":"45","label":"45 min"}]'>
|
|
18
|
+
</mk-wheel-picker>
|
|
19
|
+
<script>
|
|
20
|
+
import { defineElements } from '@yewdiel21/motion-kit/elements';
|
|
21
|
+
defineElements(['mk-wheel-picker']);
|
|
22
|
+
</script>
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
También puedes usar `createWheelPicker(root, options)` en vanilla. El CLI copia las fuentes con `npx @yewdiel21/motion-kit add wheel-picker --react --elements`.
|
|
26
|
+
|
|
27
|
+
Opciones: `items` (strings o `{value,label,disabled?}`), `value`, `name`, `label`, `disabled`, `visibleItems` (impar de 3 a 9; 5 por defecto), `itemHeight` (44…96 px; 44 por defecto), `onInput(value)` durante el desplazamiento y `onChange(value)` al confirmar. Los valores duplicados se omiten y las etiquetas siempre se tratan como texto.
|
|
28
|
+
|
|
29
|
+
La instancia expone `setValue(value, notify=false)`, `setItems(items, notify=false)`, `value` y `destroy()`. Eventos: `mk:input` y `mk:change` con `{value,item}`. Las actualizaciones por código son silenciosas por defecto.
|
|
30
|
+
|
|
31
|
+
Flechas arriba/abajo omiten opciones deshabilitadas; Home/End van a los extremos y PageUp/PageDown saltan varias filas. Escribir busca una etiqueta por prefijo. El listbox tiene un solo foco y `aria-activedescendant`, las opciones exponen selección/estado deshabilitado y los cambios confirmados se anuncian. El reset de formulario recupera el valor inicial o una opción válida si cambió la lista. Sin opciones, el valor enviado es vacío.
|
|
32
|
+
|
|
33
|
+
Las filas se proyectan sobre un cilindro cuyo radio es la mitad del marco: cerca de los bordes se comprimen, se estrechan y se desvanecen como en la rueda de iOS, y las que pasan de 90° se ocultan. `visibleItems` fija la altura del marco en filas planas; con la curvatura se ven una o dos filas más en los bordes. Las opciones deshabilitadas se muestran atenuadas.
|
|
34
|
+
|
|
35
|
+
El movimiento reducido sustituye el cilindro por una lista plana y elimina los desplazamientos programados suaves. Se usan los tokens de superficie, acento, tipografía y forma del kit; `--mk-wheel-row` y `--mk-wheel-rows` reflejan el tamaño configurado. El componente cancela temporizadores y listeners y restaura el contenedor al destruirse.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { MkElement, defineElement, type OptionType } from '../../elements/base';
|
|
2
|
+
import { createWheelPicker, type WheelPickerInstance, type WheelPickerEntry, type WheelPickerOptions } from './engine';
|
|
3
|
+
|
|
4
|
+
export class MkWheelPickerElement extends MkElement<WheelPickerInstance> {
|
|
5
|
+
static options: Record<string, OptionType> = { items: 'json', value: 'string', name: 'string', label: 'string', disabled: 'boolean', visibleItems: 'number', itemHeight: 'number' };
|
|
6
|
+
private savedValue: string | undefined;
|
|
7
|
+
private savedItems: WheelPickerEntry[] | undefined;
|
|
8
|
+
protected create(options: Record<string, unknown>) { return createWheelPicker(this, { ...options, ...(this.savedItems ? { items: this.savedItems } : {}), value: this.savedValue ?? options.value } as WheelPickerOptions); }
|
|
9
|
+
attributeChangedCallback(name: string, previous: string | null, next: string | null) {
|
|
10
|
+
if (previous === next) return;
|
|
11
|
+
if (name === 'items') this.savedItems = undefined;
|
|
12
|
+
if (!this.instance) { if (name === 'value') this.savedValue = next ?? undefined; return; }
|
|
13
|
+
if (name === 'value' && next !== null) { this.instance.setValue(next); return; }
|
|
14
|
+
if (name === 'items') { this.instance.setItems((this.options.items as WheelPickerEntry[]) ?? []); if (this.options.value !== undefined) this.instance.setValue(String(this.options.value)); return; }
|
|
15
|
+
this.savedValue = name === 'value' ? undefined : this.instance.value;
|
|
16
|
+
this.instance.destroy();
|
|
17
|
+
this.instance = this.create(this.options);
|
|
18
|
+
}
|
|
19
|
+
disconnectedCallback() { this.savedValue = this.instance?.value ?? this.savedValue; super.disconnectedCallback(); }
|
|
20
|
+
get value() { return this.instance?.value ?? ''; }
|
|
21
|
+
set value(next: string) { this.setAttribute('value', next); }
|
|
22
|
+
setValue(value: string, notify = false) { this.instance?.setValue(value, notify); }
|
|
23
|
+
setItems(items: WheelPickerEntry[], notify = false) { this.savedItems = items.map(item => typeof item === 'string' ? item : { ...item }); this.instance?.setItems(items, notify); }
|
|
24
|
+
}
|
|
25
|
+
export const defineWheelPicker = () => defineElement('mk-wheel-picker', MkWheelPickerElement);
|