@yewdiel21/motion-kit 0.17.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 +23 -0
- package/dist/auto/index.d.ts +18 -3
- package/dist/auto/index.js +32 -2
- package/dist/auto/index.js.map +1 -1
- package/dist/{chunk-VJFPTLAK.js → chunk-AWDEKDEJ.js} +2 -2
- package/dist/{chunk-BZLMCHUD.js → chunk-EDMXWUW3.js} +2 -2
- package/dist/{chunk-7IQTMF7Y.js → chunk-Q7IW4FV3.js} +2580 -58
- 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 +11 -7
- 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 +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 +206 -4
- package/dist/elements/index.js +581 -6
- package/dist/elements/index.js.map +1 -1
- package/dist/{engine-Bn6hC87X.d.ts → engine-t8d7brjn.d.ts} +341 -1
- package/dist/index.css +578 -170
- package/dist/index.d.ts +23 -23
- package/dist/index.js +33 -3
- package/dist/index.js.map +1 -1
- package/dist/react/index.d.ts +113 -7
- package/dist/react/index.js +669 -3
- package/dist/react/index.js.map +1 -1
- package/dist/{reduced-YS6TZjpa.d.ts → reduced-dPsZYqtI.d.ts} +1 -1
- package/dist/{store-CZ6vdtIE.d.ts → store-6hZNKiee.d.ts} +1 -1
- package/package.json +1 -1
- package/registry.json +180 -0
- package/src/auto/auto-init.ts +31 -0
- 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/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/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/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/css/tokens.css +4 -0
- package/src/elements/index.ts +45 -0
- package/src/index.ts +311 -295
- package/src/react/index.ts +16 -0
- package/dist/chunk-7IQTMF7Y.js.map +0 -1
- /package/dist/{chunk-VJFPTLAK.js.map → chunk-AWDEKDEJ.js.map} +0 -0
- /package/dist/{chunk-BZLMCHUD.js.map → chunk-EDMXWUW3.js.map} +0 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { gsap, EASE, registerMotion } from '../../core/gsap';
|
|
2
|
+
import { createScope } from '../../core/scope';
|
|
3
|
+
import { reduced } from '../../core/reduced';
|
|
4
|
+
import { inert, warnOnce, type Instance } from '../../core/types';
|
|
5
|
+
import { press } from '../../motion/micro';
|
|
6
|
+
|
|
7
|
+
export interface StickyHeaderOptions {
|
|
8
|
+
/** Own scrolling container; window by default. */
|
|
9
|
+
scroller?: HTMLElement | string | null;
|
|
10
|
+
expandedHeight?: number;
|
|
11
|
+
compactHeight?: number;
|
|
12
|
+
/** Start compacting after this scroll position, in pixels. */
|
|
13
|
+
threshold?: number;
|
|
14
|
+
/** Pixels of scroll over which geometry changes. */
|
|
15
|
+
range?: number;
|
|
16
|
+
top?: number;
|
|
17
|
+
disabled?: boolean;
|
|
18
|
+
onChange?: (compact: boolean) => void;
|
|
19
|
+
}
|
|
20
|
+
export interface StickyHeaderInstance extends Instance {
|
|
21
|
+
update(): void;
|
|
22
|
+
setOptions(options: Partial<StickyHeaderOptions>): void;
|
|
23
|
+
readonly compact: boolean;
|
|
24
|
+
readonly progress: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** The authored surface changes geometry inside a stable, expanded-height sticky slot. */
|
|
28
|
+
export function createStickyHeader(root: HTMLElement | null, options: StickyHeaderOptions = {}): StickyHeaderInstance {
|
|
29
|
+
const surface = root?.querySelector<HTMLElement>('[data-mk-sticky-surface]');
|
|
30
|
+
if (!root || !surface) { if (root) warnOnce('sticky-header: needs an authored [data-mk-sticky-surface]'); return inert({ update() {}, setOptions() {}, compact: false, progress: 0 }); }
|
|
31
|
+
registerMotion();
|
|
32
|
+
const scope = createScope();
|
|
33
|
+
let opts = { ...options };
|
|
34
|
+
let scroller: HTMLElement | null = null;
|
|
35
|
+
let unbind = () => {};
|
|
36
|
+
let frame = 0;
|
|
37
|
+
let progress = -1;
|
|
38
|
+
let compact = false;
|
|
39
|
+
const title = surface.querySelector<HTMLElement>('[data-mk-sticky-title]');
|
|
40
|
+
const subtitle = surface.querySelector<HTMLElement>('[data-mk-sticky-subtitle]');
|
|
41
|
+
const animations = new Set<gsap.core.Animation>();
|
|
42
|
+
const track = (animation: gsap.core.Animation | null) => { if (animation) animations.add(animation); };
|
|
43
|
+
for (const node of [root, surface, title, subtitle]) if (node) scope.attr(node, 'style', node.getAttribute('style'));
|
|
44
|
+
scope.attr(root, 'class', `${root.className} mk-sticky-header`.trim());
|
|
45
|
+
scope.attr(root, 'data-mk-ready', '');
|
|
46
|
+
const dimensions = () => {
|
|
47
|
+
const large = Number.isFinite(opts.expandedHeight) ? Math.max(48, opts.expandedHeight!) : 104;
|
|
48
|
+
const small = Number.isFinite(opts.compactHeight) ? Math.max(40, Math.min(large, opts.compactHeight!)) : Math.min(64, large);
|
|
49
|
+
root.style.setProperty('--mk-sticky-expanded-height', `${large}px`);
|
|
50
|
+
root.style.setProperty('--mk-sticky-top', `${Math.max(0, opts.top ?? 0)}px`);
|
|
51
|
+
return { large, small };
|
|
52
|
+
};
|
|
53
|
+
const update = () => {
|
|
54
|
+
frame = 0;
|
|
55
|
+
if (scope.disposed) return;
|
|
56
|
+
const { large, small } = dimensions();
|
|
57
|
+
const position = scroller ? scroller.scrollTop : window.scrollY;
|
|
58
|
+
const next = opts.disabled ? 0 : Math.max(0, Math.min(1, (position - Math.max(0, opts.threshold ?? 8)) / Math.max(1, opts.range ?? 96)));
|
|
59
|
+
if (next === progress) return;
|
|
60
|
+
const initial = progress < 0;
|
|
61
|
+
progress = next;
|
|
62
|
+
const isCompact = next >= .5;
|
|
63
|
+
scope.attr(root, 'data-mk-compact', String(isCompact));
|
|
64
|
+
scope.attr(root, 'data-mk-disabled', opts.disabled ? '' : null);
|
|
65
|
+
const duration = reduced() || initial ? 0 : .22;
|
|
66
|
+
track(gsap.to(surface, { height: large + (small - large) * next, borderRadius: 24 - 8 * next, paddingLeft: 24 - 8 * next, paddingRight: 24 - 8 * next, duration, ease: EASE.swatch, overwrite: 'auto' }));
|
|
67
|
+
if (title) track(gsap.to(title, { scale: 1 - .16 * next, y: -2 * next, transformOrigin: 'left center', duration, ease: EASE.swatch, overwrite: 'auto' }));
|
|
68
|
+
if (subtitle) track(gsap.to(subtitle, { opacity: 1 - next, y: -8 * next, height: `${(1 - next) * 1.5}em`, filter: `blur(${next * 4}px)`, duration, ease: EASE.signature, overwrite: 'auto' }));
|
|
69
|
+
if (isCompact !== compact) {
|
|
70
|
+
compact = isCompact;
|
|
71
|
+
opts.onChange?.(compact);
|
|
72
|
+
root.dispatchEvent(new CustomEvent('mk:change', { bubbles: true, detail: { compact, progress } }));
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
const queue = () => { if (!frame) frame = requestAnimationFrame(update); };
|
|
76
|
+
const bind = () => {
|
|
77
|
+
unbind();
|
|
78
|
+
scroller = typeof opts.scroller === 'string' ? (() => { try { return root.closest<HTMLElement>(opts.scroller) ?? document.querySelector<HTMLElement>(opts.scroller); } catch { return null; } })() : opts.scroller ?? null;
|
|
79
|
+
const target = scroller ?? window;
|
|
80
|
+
target.addEventListener('scroll', queue, { passive: true });
|
|
81
|
+
unbind = () => target.removeEventListener('scroll', queue);
|
|
82
|
+
};
|
|
83
|
+
scope.on(window, 'resize', queue, { passive: true });
|
|
84
|
+
scope.on(surface, 'click', (event) => {
|
|
85
|
+
const control = (event.target as Element).closest<HTMLElement>('button, a');
|
|
86
|
+
if (control && !control.matches(':disabled, [aria-disabled="true"]')) { scope.attr(control, 'style', control.getAttribute('style')); track(press(control)); }
|
|
87
|
+
});
|
|
88
|
+
if (typeof ResizeObserver === 'function') { const observer = scope.observe(new ResizeObserver(queue)); observer.observe(root); }
|
|
89
|
+
bind(); update();
|
|
90
|
+
return {
|
|
91
|
+
update, get compact() { return compact; }, get progress() { return Math.max(0, progress); },
|
|
92
|
+
setOptions(next) { if (scope.disposed) return; opts = { ...opts, ...next }; if ('scroller' in next) bind(); progress = -1; update(); },
|
|
93
|
+
destroy() { if (scope.disposed) return; cancelAnimationFrame(frame); unbind(); animations.forEach((animation) => animation.kill()); gsap.killTweensOf([surface, title, subtitle].filter((node): node is HTMLElement => !!node)); scope.dispose(); },
|
|
94
|
+
};
|
|
95
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useRef, type HTMLAttributes, type MutableRefObject } from 'react';
|
|
3
|
+
import { useIsomorphicLayoutEffect, useLatest } from '../../react/use-recipe';
|
|
4
|
+
import { createStickyHeader, type StickyHeaderOptions, type StickyHeaderInstance } from './engine';
|
|
5
|
+
|
|
6
|
+
export interface StickyHeaderProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onChange'>, StickyHeaderOptions {
|
|
7
|
+
instanceRef?: MutableRefObject<StickyHeaderInstance | null>;
|
|
8
|
+
|
|
9
|
+
}
|
|
10
|
+
export function StickyHeader({ scroller, expandedHeight, compactHeight, threshold, range, top, disabled, onChange, instanceRef, className, children, ...props }: StickyHeaderProps) {
|
|
11
|
+
const root = useRef<HTMLDivElement>(null);
|
|
12
|
+
const instance = useRef<StickyHeaderInstance | null>(null);
|
|
13
|
+
const onChangeRef = useLatest(onChange);
|
|
14
|
+
useIsomorphicLayoutEffect(() => {
|
|
15
|
+
const created = createStickyHeader(root.current, { scroller, expandedHeight, compactHeight, threshold, range, top, disabled, onChange: (...args) => onChangeRef.current?.(...args) });
|
|
16
|
+
instance.current = created;
|
|
17
|
+
return () => { created.destroy(); instance.current = null; };
|
|
18
|
+
// Options update the existing engine so timers, selection and scroll state survive.
|
|
19
|
+
}, []);
|
|
20
|
+
useIsomorphicLayoutEffect(() => {
|
|
21
|
+
instance.current?.setOptions({ scroller, expandedHeight, compactHeight, threshold, range, top, disabled });
|
|
22
|
+
}, [scroller, expandedHeight, compactHeight, threshold, range, top, disabled]);
|
|
23
|
+
useIsomorphicLayoutEffect(() => { if (instanceRef) instanceRef.current = instance.current; return () => { if (instanceRef) instanceRef.current = null; }; }, [instanceRef]);
|
|
24
|
+
return <div {...props} ref={root} className={['mk-sticky-header', className].filter(Boolean).join(' ')}><div data-mk-sticky-surface="" className="mk-sticky-header__surface">{children}</div></div>;
|
|
25
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
.mk-sticky-header, mk-sticky-header { display: block; position: sticky; top: var(--mk-sticky-top, 0px); z-index: 5; height: var(--mk-sticky-expanded-height, 104px); min-height: var(--mk-sticky-expanded-height, 104px); pointer-events: none; color: var(--mk-text); font-family: var(--mk-font); }
|
|
2
|
+
.mk-sticky-header [data-mk-sticky-surface], mk-sticky-header [data-mk-sticky-surface] { box-sizing: border-box; position: absolute; inset: 0 0 auto; display: flex; align-items: center; justify-content: space-between; gap: 16px; height: 104px; padding: 16px 24px; border-radius: 24px; pointer-events: auto; background: var(--mk-surface); transition: background .3s, box-shadow .3s; }
|
|
3
|
+
:is(.mk-sticky-header,mk-sticky-header)[data-mk-compact=true] [data-mk-sticky-surface] { background: var(--mk-surface-2); box-shadow: 0 10px 24px -20px var(--mk-shadow-color, rgb(0 0 0 / .4)); }
|
|
4
|
+
:is(.mk-sticky-header,mk-sticky-header) [data-mk-sticky-title] { display: block; margin: 0; font-size: clamp(20px, 3.5vw, 28px); font-weight: 600; line-height: 1.2; letter-spacing: -.035em; }
|
|
5
|
+
:is(.mk-sticky-header,mk-sticky-header) [data-mk-sticky-subtitle] { display: block; margin: 4px 0 0; height: 1.5em; overflow: hidden; color: var(--mk-muted); font-size: 12px; line-height: 1.5; }
|
|
6
|
+
:is(.mk-sticky-header,mk-sticky-header) :is(button,a):focus-visible { outline: 2px solid var(--mk-accent); outline-offset: 3px; }
|
|
7
|
+
:is(.mk-sticky-header,mk-sticky-header) button:disabled { opacity: .4; cursor: not-allowed; }
|
|
8
|
+
@media (max-width: 480px) { :is(.mk-sticky-header,mk-sticky-header) [data-mk-sticky-surface] { gap: 12px; } }
|
|
9
|
+
@media (prefers-reduced-motion: reduce) { :is(.mk-sticky-header,mk-sticky-header) * { transition: none !important; } }
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# TemperatureSlider
|
|
2
|
+
|
|
3
|
+
Rango de temperatura con número rodante, pequeñas marcas y un tinte frío/cálido construido con los tokens del tema. La cifra, el relleno y el pulgar responden inmediatamente al arrastrar.
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
import { createTemperatureSlider } from '@yewdiel21/motion-kit';
|
|
7
|
+
import '@yewdiel21/motion-kit/css/temperature-slider';
|
|
8
|
+
|
|
9
|
+
const control = createTemperatureSlider(document.querySelector('.mk-temperature-slider'), {
|
|
10
|
+
value: 22, unit: 'C', label: 'Temperatura deseada', name: 'temperature', onInput: console.log,
|
|
11
|
+
});
|
|
12
|
+
control.setValue(24);
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Opciones y contrato
|
|
16
|
+
|
|
17
|
+
`unit: C | F` (C), `min` (16 / 60), `max` (30 / 86), `step` (1), `value` / `defaultValue` (22 / 72), `label`, `name`, `disabled`, `error`, `formatValue`, `onInput(value)`, `onChange(value)`.
|
|
18
|
+
|
|
19
|
+
unit determina el sufijo y los límites predeterminados. Cambiar el atributo o la opción `unit` al crear no convierte el número; `setUnit('F' | 'C', range?, notify?)` sí lo convierte en el mismo control: la cifra y los extremos ruedan y el pulgar se desliza sin desmontar nada. En React, cambiar la prop `unit` usa `setUnit`. Los cambios por código, presets y teclado deslizan el pulgar con la curva swatch del kit; al arrastrar sigue al puntero sin retraso. --mk-temperature-cool y --mk-temperature-hot permiten adaptar los dos extremos con colores semánticos del tema.
|
|
20
|
+
|
|
21
|
+
La instancia expone `value: number`, `unit`, `setValue(value, notify = false)`, `setUnit(unit, range?, notify = false)`, `reset(notify = false)` y `destroy()`. Las escrituras imperativas actualizan el control incluso estando disabled. `setValue` no cambia el valor inicial al que vuelve reset. Las notificaciones usan callbacks y eventos `mk:input / mk:change` con `detail: { value }`.
|
|
22
|
+
|
|
23
|
+
## React y HTML
|
|
24
|
+
|
|
25
|
+
```tsx
|
|
26
|
+
import { TemperatureSlider } from '@yewdiel21/motion-kit/react';
|
|
27
|
+
|
|
28
|
+
<TemperatureSlider value={temperature} onInput={setTemperature} unit="C" name="temperature" />
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
`value` controla la instancia; `defaultValue` inicia una instancia que conserva sus propios cambios. Las opciones pueden cambiar sin perder la selección no controlada. `instanceRef` recibe la instancia y admite cambiar de objeto ref. Los children y dangerouslySetInnerHTML no se pasan al host porque el motor posee ese DOM.
|
|
32
|
+
|
|
33
|
+
```html
|
|
34
|
+
<mk-temperature-slider class="mk-temperature-slider" value="22" unit="C" label="Temperatura deseada" name="temperature"></mk-temperature-slider>
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Registra con `defineTemperatureSlider()` desde `@yewdiel21/motion-kit/elements` o utiliza el auto registro. Los atributos de opciones se pueden cambiar en vivo; el valor imperativo se conserva al cambiar otra opción. `disabled="false"` se lee como false. El elemento expone `value`, `setValue`, `reset`.
|
|
38
|
+
|
|
39
|
+
## Accesibilidad y lifecycle
|
|
40
|
+
|
|
41
|
+
Se conserva el input nativo, su etiqueta, teclado, tacto, validación y participación en formularios. El reset de formulario es cancelable; el control espera al reset nativo y verifica defaultPrevented. Movimiento reducido conserva toda la funcionalidad. destroy es idempotente y libera animaciones, eventos y timers, elimina solo las partes creadas y restaura los atributos del host. Los temas utilizan DM Sans y tokens --mk-*; el CSS de este componente es independiente de los otros estilos.
|
|
42
|
+
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { MkElement, defineElement, type OptionType } from '../../elements/base';
|
|
2
|
+
import { createTemperatureSlider, type TemperatureSliderInstance, type TemperatureUnit } from './engine';
|
|
3
|
+
|
|
4
|
+
/** Light DOM; attribute changes keep the current imperative value and reset baseline. */
|
|
5
|
+
export class MkTemperatureSliderElement extends MkElement<TemperatureSliderInstance> {
|
|
6
|
+
static options: Record<string, OptionType> = {"min":"number","max":"number","step":"number","value":"number","label":"string","name":"string","disabled":"boolean","error":"string","unit":"string"};
|
|
7
|
+
private baseline: number | undefined;
|
|
8
|
+
private saved: number | undefined;
|
|
9
|
+
private syncing = false;
|
|
10
|
+
protected create(options: Record<string, unknown>) {
|
|
11
|
+
const created = createTemperatureSlider(this, { ...options, value: this.saved !== undefined ? this.saved : options.value as number | undefined, defaultValue: this.baseline });
|
|
12
|
+
if (this.baseline === undefined) this.baseline = created.value;
|
|
13
|
+
this.saved = undefined;
|
|
14
|
+
return created;
|
|
15
|
+
}
|
|
16
|
+
disconnectedCallback() {
|
|
17
|
+
this.saved = this.instance?.value;
|
|
18
|
+
super.disconnectedCallback();
|
|
19
|
+
}
|
|
20
|
+
attributeChangedCallback(name: string, previous: string | null, next: string | null) {
|
|
21
|
+
if (previous === next || this.syncing) return;
|
|
22
|
+
if (name === 'value' && (!this.instance || !this.isConnected)) {
|
|
23
|
+
this.saved = next === null ? this.baseline ?? 22 : Number.isFinite(Number(next)) ? Number(next) : this.baseline ?? 22;
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
if (!this.instance || !this.isConnected) return;
|
|
27
|
+
if (name === 'value') {
|
|
28
|
+
const parsed = next === null ? this.baseline ?? 22 : Number(next);
|
|
29
|
+
this.instance.setValue(parsed, false);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
this.saved = this.instance.value;
|
|
33
|
+
super.attributeChangedCallback(name, previous, next);
|
|
34
|
+
}
|
|
35
|
+
get value(): number { return this.instance?.value ?? this.saved ?? 22; }
|
|
36
|
+
set value(next: number) { this.setValue(next); }
|
|
37
|
+
setValue(value: number, notify = false) { if (this.instance) this.instance.setValue(value, notify); else this.saved = value; }
|
|
38
|
+
reset(notify = false) { this.instance?.reset(notify); }
|
|
39
|
+
get unit(): TemperatureUnit { return this.instance?.unit ?? (this.getAttribute('unit') === 'F' ? 'F' : 'C'); }
|
|
40
|
+
/** Converts in place and glides; the unit, min and max attributes follow without remounting. */
|
|
41
|
+
setUnit(unit: TemperatureUnit, notify = false) {
|
|
42
|
+
if (!this.instance) { this.setAttribute('unit', unit); return; }
|
|
43
|
+
this.instance.setUnit(unit, {}, notify);
|
|
44
|
+
this.syncing = true;
|
|
45
|
+
try {
|
|
46
|
+
this.setAttribute('unit', this.instance.unit);
|
|
47
|
+
for (const name of ['min', 'max']) this.removeAttribute(name);
|
|
48
|
+
} finally { this.syncing = false; }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
}
|
|
52
|
+
export const defineTemperatureSlider = () => defineElement('mk-temperature-slider', MkTemperatureSliderElement);
|
|
53
|
+
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { createNumericRange, type NumericOptions, type NumericInstance } from '../equalizer-slider/shared';
|
|
2
|
+
|
|
3
|
+
export type TemperatureUnit = 'C' | 'F';
|
|
4
|
+
export interface TemperatureSliderOptions extends NumericOptions { unit?: TemperatureUnit }
|
|
5
|
+
export interface TemperatureSliderInstance extends NumericInstance {
|
|
6
|
+
readonly unit: TemperatureUnit;
|
|
7
|
+
/** Converts the value into `unit` and glides to it. `min`/`max` default to the unit's comfort range. */
|
|
8
|
+
setUnit(unit: TemperatureUnit, range?: { min?: number; max?: number; step?: number }, notify?: boolean): void;
|
|
9
|
+
}
|
|
10
|
+
const RANGE = { C: { min: 16, max: 30, value: 22 }, F: { min: 60, max: 86, value: 72 } } as const;
|
|
11
|
+
const convert = (value: number, from: TemperatureUnit, to: TemperatureUnit) => from === to ? value : to === 'F' ? value * 9 / 5 + 32 : (value - 32) * 5 / 9;
|
|
12
|
+
|
|
13
|
+
export function createTemperatureSlider(root: HTMLElement | null, options: TemperatureSliderOptions = {}): TemperatureSliderInstance {
|
|
14
|
+
let unit: TemperatureUnit = options.unit === 'F' ? 'F' : 'C';
|
|
15
|
+
const formatFor = (next: TemperatureUnit) => (n: number) => `${n}°${next}`;
|
|
16
|
+
const range = createNumericRange(root, options, {
|
|
17
|
+
prefix: 'temperature', label: 'Temperatura', ...RANGE[unit],
|
|
18
|
+
format: formatFor(unit),
|
|
19
|
+
decorate({ track, thumb }) {
|
|
20
|
+
const rail = document.createElement('span'); rail.dataset.mkTemperatureRail = ''; rail.setAttribute('aria-hidden', 'true'); track.prepend(rail);
|
|
21
|
+
const fill = document.createElement('span'); fill.dataset.mkTemperatureFill = ''; rail.append(fill);
|
|
22
|
+
const ticks = document.createElement('span'); ticks.dataset.mkTemperatureTicks = ''; ticks.setAttribute('aria-hidden', 'true');
|
|
23
|
+
for (let i = 0; i <= 14; i++) ticks.append(document.createElement('i'));
|
|
24
|
+
track.prepend(ticks); thumb.textContent = '··';
|
|
25
|
+
return (ratio) => {
|
|
26
|
+
root?.style.setProperty('--mk-temperature-warm', `${Math.round(ratio * 100)}%`);
|
|
27
|
+
fill.style.width = `${ratio * 100}%`;
|
|
28
|
+
};
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
return {
|
|
32
|
+
get value() { return range.value; },
|
|
33
|
+
get unit() { return unit; },
|
|
34
|
+
setValue: range.setValue,
|
|
35
|
+
reset: range.reset,
|
|
36
|
+
setUnit(next, limits = {}, notify = false) {
|
|
37
|
+
const target: TemperatureUnit = next === 'F' ? 'F' : 'C';
|
|
38
|
+
const value = Math.round(convert(range.value, unit, target));
|
|
39
|
+
unit = target;
|
|
40
|
+
range.configure({ min: limits.min ?? RANGE[target].min, max: limits.max ?? RANGE[target].max, step: limits.step, format: options.formatValue ?? formatFor(target), value }, notify);
|
|
41
|
+
},
|
|
42
|
+
destroy: range.destroy,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useRef, type HTMLAttributes, type MutableRefObject } from 'react';
|
|
3
|
+
import { useIsomorphicLayoutEffect, useLatest } from '../../react/use-recipe';
|
|
4
|
+
import { createTemperatureSlider, type TemperatureSliderOptions, type TemperatureSliderInstance } from './engine';
|
|
5
|
+
|
|
6
|
+
export interface TemperatureSliderProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onChange' | 'onInput' | 'defaultValue' | 'children' | 'dangerouslySetInnerHTML'>, Omit<TemperatureSliderOptions, 'value' | 'defaultValue'> {
|
|
7
|
+
value?: number;
|
|
8
|
+
defaultValue?: number;
|
|
9
|
+
instanceRef?: MutableRefObject<TemperatureSliderInstance | null>;
|
|
10
|
+
/** The engine owns this component's DOM. */
|
|
11
|
+
children?: never;
|
|
12
|
+
dangerouslySetInnerHTML?: never;
|
|
13
|
+
}
|
|
14
|
+
export function TemperatureSlider({ value, defaultValue, min, max, step, label, name, disabled, error, formatValue, unit, onChange, onInput, instanceRef, className, children: _children, dangerouslySetInnerHTML: _html, ...props }: TemperatureSliderProps) {
|
|
15
|
+
const root = useRef<HTMLDivElement>(null);
|
|
16
|
+
const engine = useRef<TemperatureSliderInstance | null>(null);
|
|
17
|
+
const latestValue = useLatest(value);
|
|
18
|
+
const change = useLatest(onChange);
|
|
19
|
+
const input = useLatest(onInput);
|
|
20
|
+
const baseline = useRef(value !== undefined ? value : defaultValue);
|
|
21
|
+
const current = useRef(baseline.current);
|
|
22
|
+
const reconcile = () => queueMicrotask(() => {
|
|
23
|
+
if (latestValue.current !== undefined) engine.current?.setValue(latestValue.current, false);
|
|
24
|
+
});
|
|
25
|
+
useIsomorphicLayoutEffect(() => {
|
|
26
|
+
const created = createTemperatureSlider(root.current, {
|
|
27
|
+
min, max, step, label, name, disabled, error, formatValue, unit, value: latestValue.current !== undefined ? latestValue.current : current.current, defaultValue: baseline.current,
|
|
28
|
+
onChange: (next) => { change.current?.(next); reconcile(); },
|
|
29
|
+
onInput: (next) => { input.current?.(next); if (input.current || !change.current) reconcile(); },
|
|
30
|
+
});
|
|
31
|
+
engine.current = created;
|
|
32
|
+
const form = root.current?.closest('form');
|
|
33
|
+
const pending = new Set<ReturnType<typeof setTimeout>>();
|
|
34
|
+
const reset = (event: Event) => {
|
|
35
|
+
const timer = setTimeout(() => {
|
|
36
|
+
pending.delete(timer);
|
|
37
|
+
if (!event.defaultPrevented && latestValue.current !== undefined) created.setValue(latestValue.current, false);
|
|
38
|
+
}, 0);
|
|
39
|
+
pending.add(timer);
|
|
40
|
+
};
|
|
41
|
+
form?.addEventListener('reset', reset);
|
|
42
|
+
return () => {
|
|
43
|
+
form?.removeEventListener('reset', reset);
|
|
44
|
+
pending.forEach((timer) => clearTimeout(timer));
|
|
45
|
+
current.current = created.value; created.destroy(); engine.current = null;
|
|
46
|
+
};
|
|
47
|
+
// Callbacks and controlled value are read through refs.
|
|
48
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
49
|
+
}, [min, max, step, label, name, disabled, error, formatValue]);
|
|
50
|
+
// A unit change converts and glides in place instead of remounting the slider.
|
|
51
|
+
useIsomorphicLayoutEffect(() => {
|
|
52
|
+
const next = unit === 'F' ? 'F' : 'C';
|
|
53
|
+
if (engine.current && engine.current.unit !== next) engine.current.setUnit(next, { min, max, step });
|
|
54
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
55
|
+
}, [unit]);
|
|
56
|
+
useIsomorphicLayoutEffect(() => {
|
|
57
|
+
if (value !== undefined) engine.current?.setValue(value, false);
|
|
58
|
+
});
|
|
59
|
+
useIsomorphicLayoutEffect(() => {
|
|
60
|
+
if (!instanceRef) return;
|
|
61
|
+
instanceRef.current = engine.current;
|
|
62
|
+
return () => { instanceRef.current = null; };
|
|
63
|
+
}, [instanceRef, min, max, step, label, name, disabled, error, formatValue]);
|
|
64
|
+
return <div {...props} ref={root} className={['mk-temperature-slider', className].filter(Boolean).join(' ')} />;
|
|
65
|
+
}
|
|
66
|
+
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
:is(.mk-temperature-slider, mk-temperature-slider) { display: block; width: 100%; min-width: 0; color: var(--mk-text); font: 500 .875rem/1.4 var(--mk-font); }
|
|
2
|
+
[data-mk-temperature-body] { display: grid; gap: .875rem; }
|
|
3
|
+
[data-mk-temperature-header] { display: flex; justify-content: space-between; align-items: center; gap: 1rem; }
|
|
4
|
+
[data-mk-temperature-output] { font-variant-numeric: tabular-nums; white-space: nowrap; }
|
|
5
|
+
[data-mk-temperature-track] { position: relative; height: 76px; min-width: 0; border-radius: 24px; background: var(--mk-surface-2); }
|
|
6
|
+
[data-mk-temperature-input] { position: absolute; z-index: 3; inset: 0; box-sizing: border-box; width: 100%; height: 100%; margin: 0; appearance: none; -webkit-appearance: none; background: transparent; cursor: ew-resize; opacity: 0; }
|
|
7
|
+
[data-mk-temperature-input]::-webkit-slider-thumb { appearance: none; -webkit-appearance: none; width: 32px; height: 76px; background: transparent; }
|
|
8
|
+
[data-mk-temperature-input]::-moz-range-thumb { width: 32px; height: 76px; border: 0; background: transparent; }
|
|
9
|
+
[data-mk-temperature-thumb] { position: absolute; z-index: 2; top: 50%; left: calc(16px + (100% - 32px) * var(--mk-temperature-ratio, .5)); translate: -50% -50%; pointer-events: none; }
|
|
10
|
+
[data-mk-temperature-track]:has(:focus-visible) { outline: 2px solid var(--mk-accent); outline-offset: 4px; }
|
|
11
|
+
[data-mk-temperature-ends] { display: flex; justify-content: space-between; margin-top: -.375rem; color: var(--mk-muted); font-size: .75rem; font-variant-numeric: tabular-nums; }
|
|
12
|
+
[data-mk-temperature-message] { color: var(--mk-error); font-size: .8125rem; font-weight: 400; }
|
|
13
|
+
[data-mk-temperature-message]:empty { display: none; }
|
|
14
|
+
:is(.mk-temperature-slider, mk-temperature-slider)[data-mk-disabled] { opacity: .4; }
|
|
15
|
+
:is(.mk-temperature-slider, mk-temperature-slider)[data-mk-disabled] [data-mk-temperature-input] { cursor: not-allowed; }
|
|
16
|
+
:is(.mk-temperature-slider, mk-temperature-slider)[data-mk-error] [data-mk-temperature-track] { outline: 1px solid var(--mk-error); }
|
|
17
|
+
:is(.mk-temperature-slider, mk-temperature-slider) { --mk-temperature-cool: var(--mk-accent); --mk-temperature-hot: var(--md-sys-color-tertiary, var(--mk-text)); --mk-temperature-tint: color-mix(in srgb, var(--mk-temperature-cool), var(--mk-temperature-hot) var(--mk-temperature-warm, 50%)); }
|
|
18
|
+
[data-mk-temperature-output] { font-size: clamp(2rem, 6vw, 2.75rem); font-weight: 500; letter-spacing: -.06em; line-height: 1.2; }
|
|
19
|
+
[data-mk-temperature-track] { height: 64px; background: color-mix(in srgb, var(--mk-temperature-tint) 5%, var(--mk-surface-2)); }
|
|
20
|
+
[data-mk-temperature-rail] { position: absolute; inset: 28px 16px; border-radius: 64px; background: var(--mk-outline-soft); overflow: hidden; }
|
|
21
|
+
[data-mk-temperature-fill] { display: block; height: 100%; border-radius: inherit; background: color-mix(in srgb, var(--mk-temperature-tint) 42%, var(--mk-text)); }
|
|
22
|
+
[data-mk-temperature-ticks] { position: absolute; inset: 0 16px; display: flex; justify-content: space-between; align-items: center; pointer-events: none; }
|
|
23
|
+
[data-mk-temperature-ticks] > i { width: 1px; height: 18px; background: var(--mk-outline-soft); }
|
|
24
|
+
[data-mk-temperature-thumb] { display: grid; place-items: center; width: 30px; height: 40px; border-radius: 12px; background: var(--mk-text); color: var(--mk-bg); font: 700 18px/1 var(--mk-font); box-shadow: 0 0 0 4px var(--mk-surface-2); transition: width .35s var(--mk-ease-swatch), border-radius .35s var(--mk-ease-swatch); }
|
|
25
|
+
:is(.mk-temperature-slider, mk-temperature-slider)[data-mk-held] [data-mk-temperature-thumb] { width: 38px; border-radius: 16px; }
|
|
26
|
+
@media (hover: hover) { [data-mk-temperature-track]:has(:not(:disabled):hover) [data-mk-temperature-thumb] { border-radius: 16px; width: 34px; } }
|
|
27
|
+
@media (prefers-reduced-motion: reduce) { [data-mk-temperature-thumb] { transition: none; } }
|
|
28
|
+
@media (forced-colors: active) { [data-mk-temperature-rail] { background: GrayText; } [data-mk-temperature-fill], [data-mk-temperature-thumb] { background: Highlight; } }
|
|
29
|
+
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { createScope } from '../../core/scope';
|
|
2
2
|
import type { WaveformPlayerInstance, WaveformPlayerOptions } from '../waveform-player/engine';
|
|
3
3
|
import { createWaveformPlayer } from '../waveform-player/engine';
|
|
4
|
+
import { gsap, EASE, registerMotion } from '../../core/gsap';
|
|
5
|
+
import { reduced } from '../../core/reduced';
|
|
4
6
|
|
|
5
7
|
export type VoiceMessageStatus = 'sending' | 'sent' | 'delivered' | 'read' | 'failed';
|
|
6
8
|
export interface VoiceMessageBubbleOptions extends WaveformPlayerOptions {
|
|
@@ -20,6 +22,7 @@ const statusLabels: Record<VoiceMessageStatus, string> = { sending: 'Enviando',
|
|
|
20
22
|
/** A message presentation around the real waveform audio player. Never records audio. */
|
|
21
23
|
export function createVoiceMessageBubble(root: HTMLElement | null, options: VoiceMessageBubbleOptions = {}): VoiceMessageBubbleInstance {
|
|
22
24
|
if (!root) return createWaveformPlayer(null, options);
|
|
25
|
+
registerMotion();
|
|
23
26
|
const scope = createScope();
|
|
24
27
|
let opts = { ...options };
|
|
25
28
|
const content = document.createElement('div');
|
|
@@ -63,6 +66,14 @@ export function createVoiceMessageBubble(root: HTMLElement | null, options: Voic
|
|
|
63
66
|
player.update({ ...audio, label: opts.label ?? (opts.author ? `mensaje de voz de ${opts.author}` : 'mensaje de voz') });
|
|
64
67
|
};
|
|
65
68
|
update(options);
|
|
69
|
+
// The message grows from its conversational edge, matching the shared chat
|
|
70
|
+
// vocabulary. Its real controls remain available throughout the entrance.
|
|
71
|
+
let entrance: gsap.core.Tween | null = null;
|
|
72
|
+
if (!reduced()) {
|
|
73
|
+
content.style.transformOrigin = opts.direction === 'outgoing' ? 'right bottom' : 'left bottom';
|
|
74
|
+
entrance = gsap.fromTo(content, { scale: .88, opacity: 0, filter: 'blur(12px)', y: 10 }, { scale: 1, opacity: 1, filter: 'blur(0px)', y: 0, duration: .65, ease: EASE.signature, clearProps: 'transform,opacity,filter' });
|
|
75
|
+
}
|
|
76
|
+
scope.add(() => entrance?.kill());
|
|
66
77
|
return { play: () => player.play(), pause: () => player.pause(), seek: n => player.seek(n), update,
|
|
67
78
|
get state() { return player.state; }, get audio() { return player.audio; }, destroy: () => scope.dispose() };
|
|
68
79
|
}
|
|
@@ -1,12 +1,14 @@
|
|
|
1
|
-
/* Requires waveform-player.css; the bubble composes its player without duplicating it. */
|
|
2
|
-
.mk-voice-message-bubble, mk-voice-message-bubble { display: block; width: min(100%, 25rem); color: var(--mk-text); font-family: var(--mk-font); }
|
|
3
|
-
[data-mk-voice-message-content] { display: grid; gap: .
|
|
4
|
-
:is(.mk-voice-message-bubble, mk-voice-message-bubble)[data-mk-direction='outgoing'] { margin-inline-start: auto; }
|
|
5
|
-
:is(.mk-voice-message-bubble, mk-voice-message-bubble)[data-mk-direction='outgoing'] [data-mk-voice-message-content] { background:
|
|
6
|
-
[data-mk-voice-message-header], [data-mk-voice-message-footer] { display: flex; flex-wrap: wrap; align-items: center; gap: .375rem .75rem; font-size: .6875rem; color: var(--mk-muted); }
|
|
7
|
-
[data-mk-voice-message-author] { font-weight: 600; color: var(--mk-text); }
|
|
8
|
-
[data-mk-voice-message-caption] { margin-inline-start: auto; }
|
|
9
|
-
[data-mk-voice-message-footer] { justify-content: flex-end; }
|
|
10
|
-
[data-mk-voice-message-status] { color: var(--mk-accent); }
|
|
11
|
-
|
|
12
|
-
|
|
1
|
+
/* Requires waveform-player.css; the bubble composes its player without duplicating it. */
|
|
2
|
+
.mk-voice-message-bubble, mk-voice-message-bubble { display: block; width: min(100%, 25rem); color: var(--mk-text); font-family: var(--mk-font); }
|
|
3
|
+
[data-mk-voice-message-content] { display: grid; gap: .5rem; padding: 1rem 1.25rem .75rem; background: var(--mk-surface-2); border: 0; border-radius: 32px 32px 32px 12px; }
|
|
4
|
+
:is(.mk-voice-message-bubble, mk-voice-message-bubble)[data-mk-direction='outgoing'] { margin-inline-start: auto; }
|
|
5
|
+
:is(.mk-voice-message-bubble, mk-voice-message-bubble)[data-mk-direction='outgoing'] [data-mk-voice-message-content] { background: var(--mk-surface-3); border-radius: 32px 32px 12px 32px; }
|
|
6
|
+
[data-mk-voice-message-header], [data-mk-voice-message-footer] { display: flex; flex-wrap: wrap; align-items: center; gap: .375rem .75rem; font-size: .6875rem; color: var(--mk-muted); }
|
|
7
|
+
[data-mk-voice-message-author] { font-weight: 600; color: var(--mk-text); }
|
|
8
|
+
[data-mk-voice-message-caption] { margin-inline-start: auto; }
|
|
9
|
+
[data-mk-voice-message-footer] { justify-content: flex-end; }
|
|
10
|
+
[data-mk-voice-message-status] { color: var(--mk-accent); }
|
|
11
|
+
/* The track steps one tonal level above its bubble, like the equalizer above the page. */
|
|
12
|
+
[data-mk-voice-message-content] .mk-waveform-player { --mk-wave-surface: var(--mk-surface-3); }
|
|
13
|
+
:is(.mk-voice-message-bubble, mk-voice-message-bubble)[data-mk-direction='outgoing'] .mk-waveform-player { --mk-wave-surface: var(--mk-surface-4); }
|
|
14
|
+
@media (forced-colors: active) { [data-mk-voice-message-content] { border-color: CanvasText; } }
|
|
@@ -25,6 +25,6 @@ Opciones: `src`, `peaks`, `label` (nombre accesible), `disabled`, `playbackRates
|
|
|
25
25
|
|
|
26
26
|
React admite `instanceRef` y actualiza las opciones/callbacks sin recrear el audio. El custom element expone los mismos métodos, `state`, `audio` y `instance`; los atributos son reactivos (`playback-rates` y `peaks` son JSON). Cambiar solamente picos, etiquetas o callbacks conserva fuente, tiempo y reproducción. Cambiar `src` pausa, cancela solicitudes y reinicia tiempo/duración. `destroy()` detiene el audio, vacía la fuente, retira listeners/animaciones y conserva el DOM ajeno.
|
|
27
27
|
|
|
28
|
-
El slider nativo conserva teclado (flechas, Home/End), toque y semántica accesible; su nombre incluye `label` y su valor se anuncia como tiempo. Carga, errores y finalización usan un estado discreto; los tiempos no se anuncian continuamente. El pequeño resorte GSAP del botón se omite con movimiento reducido. Tokens locales: `--mk-wave-color
|
|
28
|
+
El slider nativo conserva teclado (flechas, Home/End), toque y semántica accesible; su nombre incluye `label` y su valor se anuncia como tiempo. Carga, errores y finalización usan un estado discreto; los tiempos no se anuncian continuamente. El pequeño resorte GSAP del botón se omite con movimiento reducido. Comparte el lenguaje del equalizer: pista tonal, barras en píldora remuestreadas al ancho (media de los picos suministrados, sin inventar datos), cabezal recortado que crece al pasar el puntero y al arrastrar, y barras que rebotan al cruzar el cabezal. Tokens locales: `--mk-wave-color` (reproducido y cabezal, texto por defecto), `--mk-wave-track` (pendiente), `--mk-wave-surface` (pista); el resto usa los colores, tipografía y superficies del tema.
|
|
29
29
|
|
|
30
30
|
Tipos: `WaveformPlayerOptions`, `WaveformPlayerInstance`, `WaveformPlayerState`, `WaveformPlayerStatus`, `WaveformPlayerProps`. El componente reproduce contenido provisto por la aplicación: agrega una transcripción cerca del reproductor cuando el audio contenga información hablada.
|