@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,228 @@
|
|
|
1
|
+
import { gsap, EASE, registerMotion } from '../../core/gsap';
|
|
2
|
+
import { createScope } from '../../core/scope';
|
|
3
|
+
import { reduced } from '../../core/reduced';
|
|
4
|
+
import { inert, type Instance } from '../../core/types';
|
|
5
|
+
import { blurIn, press } from '../../motion/micro';
|
|
6
|
+
|
|
7
|
+
export interface ScrollPreviewDockItem {
|
|
8
|
+
id: string;
|
|
9
|
+
label: string;
|
|
10
|
+
description?: string;
|
|
11
|
+
thumbnail?: string;
|
|
12
|
+
/** A section element or selector. Defaults to the element with this item's id. */
|
|
13
|
+
section?: HTMLElement | string;
|
|
14
|
+
disabled?: boolean;
|
|
15
|
+
}
|
|
16
|
+
export interface ScrollPreviewDockOptions {
|
|
17
|
+
items?: ScrollPreviewDockItem[];
|
|
18
|
+
scroller?: HTMLElement | string | null;
|
|
19
|
+
offset?: number;
|
|
20
|
+
label?: string;
|
|
21
|
+
disabled?: boolean;
|
|
22
|
+
onChange?: (id: string) => void;
|
|
23
|
+
}
|
|
24
|
+
export interface ScrollPreviewDockInstance extends Instance {
|
|
25
|
+
select(id: string): void;
|
|
26
|
+
update(): void;
|
|
27
|
+
setOptions(options: Partial<ScrollPreviewDockOptions>): void;
|
|
28
|
+
readonly value: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Native section links, a travelling selection pill, and a focus/hover preview. */
|
|
32
|
+
export function createScrollPreviewDock(root: HTMLElement | null, options: ScrollPreviewDockOptions = {}): ScrollPreviewDockInstance {
|
|
33
|
+
if (!root) return inert({ select() {}, update() {}, setOptions() {}, value: '' });
|
|
34
|
+
registerMotion();
|
|
35
|
+
const scope = createScope();
|
|
36
|
+
let opts = { ...options };
|
|
37
|
+
let current = '';
|
|
38
|
+
let previewed = '';
|
|
39
|
+
let frame = 0;
|
|
40
|
+
let scrolling: gsap.core.Tween | null = null;
|
|
41
|
+
let unbindScroller = () => {};
|
|
42
|
+
let scroller: HTMLElement | null = null;
|
|
43
|
+
const original = [...root.childNodes];
|
|
44
|
+
const animations = new Set<gsap.core.Animation>();
|
|
45
|
+
const track = (animation: gsap.core.Animation | null) => { if (animation) animations.add(animation); };
|
|
46
|
+
const panel = document.createElement('div');
|
|
47
|
+
panel.className = 'mk-scroll-preview-dock__preview';
|
|
48
|
+
panel.setAttribute('aria-hidden', 'true');
|
|
49
|
+
const image = document.createElement('span');
|
|
50
|
+
image.className = 'mk-scroll-preview-dock__image';
|
|
51
|
+
const copy = document.createElement('span');
|
|
52
|
+
const title = document.createElement('strong');
|
|
53
|
+
const description = document.createElement('span');
|
|
54
|
+
description.className = 'mk-scroll-preview-dock__description';
|
|
55
|
+
copy.append(title, description);
|
|
56
|
+
panel.append(image, copy);
|
|
57
|
+
const nav = document.createElement('nav');
|
|
58
|
+
nav.className = 'mk-scroll-preview-dock__nav';
|
|
59
|
+
const pill = document.createElement('span');
|
|
60
|
+
pill.className = 'mk-scroll-preview-dock__pill';
|
|
61
|
+
pill.setAttribute('aria-hidden', 'true');
|
|
62
|
+
const empty = document.createElement('p');
|
|
63
|
+
empty.className = 'mk-scroll-preview-dock__empty';
|
|
64
|
+
empty.textContent = 'No hay secciones para explorar.';
|
|
65
|
+
root.replaceChildren(panel, nav, empty);
|
|
66
|
+
scope.attr(root, 'class', `${root.className} mk-scroll-preview-dock`.trim());
|
|
67
|
+
scope.attr(root, 'data-mk-ready', '');
|
|
68
|
+
const items = () => opts.items ?? [];
|
|
69
|
+
const enabled = (item: ScrollPreviewDockItem) => !opts.disabled && !item.disabled;
|
|
70
|
+
const section = (item: ScrollPreviewDockItem) => {
|
|
71
|
+
if (item.section instanceof HTMLElement) return item.section;
|
|
72
|
+
if (!item.section) return document.getElementById(item.id);
|
|
73
|
+
try { return (scroller ?? document).querySelector<HTMLElement>(item.section); } catch { return null; }
|
|
74
|
+
};
|
|
75
|
+
const links = () => [...nav.querySelectorAll<HTMLAnchorElement>('a')];
|
|
76
|
+
const thumb = (node: HTMLElement, item: ScrollPreviewDockItem) => {
|
|
77
|
+
node.replaceChildren();
|
|
78
|
+
if (item.thumbnail) { const img = document.createElement('img'); img.src = item.thumbnail; img.alt = ''; img.draggable = false; node.append(img); }
|
|
79
|
+
else { const letter = document.createElement('span'); letter.textContent = item.label.slice(0, 2); node.append(letter); }
|
|
80
|
+
};
|
|
81
|
+
const preview = (id: string, animate = true) => {
|
|
82
|
+
const item = items().find((item) => item.id === id);
|
|
83
|
+
if (!item || previewed === id) return;
|
|
84
|
+
previewed = id;
|
|
85
|
+
thumb(image, item);
|
|
86
|
+
title.textContent = item.label;
|
|
87
|
+
description.textContent = item.description ?? 'Explorar sección';
|
|
88
|
+
for (const link of links()) link.dataset.mkPreview = String(link.dataset.mkSection === id);
|
|
89
|
+
if (animate) { gsap.killTweensOf(panel); track(blurIn(panel, { from: 'soft', duration: .28 })); }
|
|
90
|
+
};
|
|
91
|
+
const place = (animate = true) => {
|
|
92
|
+
const active = links().find((link) => link.dataset.mkSection === current);
|
|
93
|
+
pill.hidden = !active;
|
|
94
|
+
if (!active) return;
|
|
95
|
+
track(gsap.to(pill, { x: active.offsetLeft, y: active.offsetTop, width: active.offsetWidth, height: active.offsetHeight, duration: reduced() || !animate ? 0 : .45, ease: EASE.swatch, overwrite: 'auto' }));
|
|
96
|
+
};
|
|
97
|
+
const activate = (id: string, notify = true) => {
|
|
98
|
+
if (scope.disposed || current === id) return;
|
|
99
|
+
current = id;
|
|
100
|
+
for (const link of links()) {
|
|
101
|
+
if (link.dataset.mkSection === id) link.setAttribute('aria-current', 'location');
|
|
102
|
+
else link.removeAttribute('aria-current');
|
|
103
|
+
}
|
|
104
|
+
place();
|
|
105
|
+
if (!root.contains(document.activeElement) && !root.matches(':hover')) preview(id);
|
|
106
|
+
else if (!previewed) preview(id, false);
|
|
107
|
+
if (notify) {
|
|
108
|
+
opts.onChange?.(id);
|
|
109
|
+
root.dispatchEvent(new CustomEvent('mk:change', { bubbles: true, detail: { value: id } }));
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
const update = () => {
|
|
113
|
+
frame = 0;
|
|
114
|
+
if (scope.disposed) return;
|
|
115
|
+
const top = scroller ? scroller.getBoundingClientRect().top + scroller.clientTop : 0;
|
|
116
|
+
const height = scroller ? scroller.clientHeight : window.innerHeight;
|
|
117
|
+
const line = top + Math.min(120, height * .3) + (opts.offset ?? 0);
|
|
118
|
+
const targets = items().filter(enabled).map((item) => ({ item, node: section(item) })).filter((entry): entry is { item: ScrollPreviewDockItem; node: HTMLElement } => !!entry.node);
|
|
119
|
+
let target: (typeof targets)[number] | undefined = targets[0];
|
|
120
|
+
for (const entry of targets) if (entry.node.getBoundingClientRect().top <= line) target = entry;
|
|
121
|
+
const atEnd = scroller ? scroller.scrollHeight > scroller.clientHeight && scroller.scrollTop + scroller.clientHeight >= scroller.scrollHeight - 2 : document.documentElement.scrollHeight > window.innerHeight && window.scrollY + window.innerHeight >= document.documentElement.scrollHeight - 2;
|
|
122
|
+
if (atEnd) target = targets.at(-1);
|
|
123
|
+
if (target) activate(target.item.id);
|
|
124
|
+
place(false);
|
|
125
|
+
};
|
|
126
|
+
const queue = () => { if (!frame) frame = requestAnimationFrame(update); };
|
|
127
|
+
const bindScroller = () => {
|
|
128
|
+
unbindScroller();
|
|
129
|
+
scroller = typeof opts.scroller === 'string' ? (() => { try { return root.closest<HTMLElement>(opts.scroller) ?? document.querySelector<HTMLElement>(opts.scroller); } catch { return null; } })() : opts.scroller ?? null;
|
|
130
|
+
const target = scroller ?? window;
|
|
131
|
+
target.addEventListener('scroll', queue, { passive: true });
|
|
132
|
+
target.addEventListener('wheel', cancelScroll, { passive: true });
|
|
133
|
+
target.addEventListener('touchstart', cancelScroll, { passive: true });
|
|
134
|
+
unbindScroller = () => { target.removeEventListener('scroll', queue); target.removeEventListener('wheel', cancelScroll); target.removeEventListener('touchstart', cancelScroll); };
|
|
135
|
+
};
|
|
136
|
+
function cancelScroll() { scrolling?.kill(); scrolling = null; }
|
|
137
|
+
const select = (id: string) => {
|
|
138
|
+
if (scope.disposed) return;
|
|
139
|
+
const item = items().find((entry) => entry.id === id);
|
|
140
|
+
if (!item || !enabled(item)) return;
|
|
141
|
+
const target = section(item);
|
|
142
|
+
if (!target) return;
|
|
143
|
+
cancelScroll();
|
|
144
|
+
const top = Math.max(0, (scroller ? scroller.scrollTop + target.getBoundingClientRect().top - scroller.getBoundingClientRect().top - scroller.clientTop : window.scrollY + target.getBoundingClientRect().top) - (opts.offset ?? 0));
|
|
145
|
+
activate(id);
|
|
146
|
+
preview(id);
|
|
147
|
+
if (scroller) {
|
|
148
|
+
if (reduced()) scroller.scrollTop = top;
|
|
149
|
+
else scrolling = gsap.to(scroller, { scrollTop: top, duration: .65, ease: EASE.signature, overwrite: 'auto', onUpdate: queue });
|
|
150
|
+
} else if (reduced()) window.scrollTo({ top, behavior: 'instant' });
|
|
151
|
+
else {
|
|
152
|
+
const position = { top: window.scrollY };
|
|
153
|
+
scrolling = gsap.to(position, { top, duration: .65, ease: EASE.signature, onUpdate: () => { window.scrollTo(0, position.top); queue(); } });
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
const render = () => {
|
|
157
|
+
gsap.killTweensOf(pill);
|
|
158
|
+
nav.replaceChildren(pill);
|
|
159
|
+
nav.setAttribute('aria-label', opts.label ?? 'Explorar secciones');
|
|
160
|
+
const unique = new Set<string>();
|
|
161
|
+
for (const item of items()) {
|
|
162
|
+
if (!item.id || unique.has(item.id)) continue;
|
|
163
|
+
unique.add(item.id);
|
|
164
|
+
const link = document.createElement('a');
|
|
165
|
+
link.href = `#${encodeURIComponent(section(item)?.id || item.id)}`;
|
|
166
|
+
link.dataset.mkSection = item.id;
|
|
167
|
+
link.className = 'mk-scroll-preview-dock__item';
|
|
168
|
+
const cover = document.createElement('span');
|
|
169
|
+
cover.className = 'mk-scroll-preview-dock__thumb';
|
|
170
|
+
cover.setAttribute('aria-hidden', 'true');
|
|
171
|
+
thumb(cover, item);
|
|
172
|
+
const label = document.createElement('span');
|
|
173
|
+
label.textContent = item.label;
|
|
174
|
+
link.append(cover, label);
|
|
175
|
+
if (!enabled(item)) { link.setAttribute('aria-disabled', 'true'); link.tabIndex = -1; }
|
|
176
|
+
nav.append(link);
|
|
177
|
+
}
|
|
178
|
+
empty.hidden = unique.size > 0;
|
|
179
|
+
panel.hidden = unique.size === 0;
|
|
180
|
+
nav.hidden = unique.size === 0;
|
|
181
|
+
scope.attr(root, 'data-mk-disabled', opts.disabled ? '' : null);
|
|
182
|
+
previewed = '';
|
|
183
|
+
const next = items().find((item) => item.id === current && enabled(item)) ?? items().find(enabled);
|
|
184
|
+
current = '';
|
|
185
|
+
if (next) { activate(next.id, false); preview(next.id, false); }
|
|
186
|
+
else pill.hidden = true;
|
|
187
|
+
};
|
|
188
|
+
scope.on(nav, 'click', (event) => {
|
|
189
|
+
const link = (event.target as Element).closest<HTMLAnchorElement>('a[data-mk-section]');
|
|
190
|
+
if (!link) return;
|
|
191
|
+
if (link.getAttribute('aria-disabled') === 'true') { event.preventDefault(); return; }
|
|
192
|
+
if (event.metaKey || event.ctrlKey || event.altKey || event.shiftKey || event.button !== 0) return;
|
|
193
|
+
const item = items().find((item) => item.id === link.dataset.mkSection);
|
|
194
|
+
if (!item || !section(item)) return;
|
|
195
|
+
event.preventDefault(); track(press(link)); select(item.id);
|
|
196
|
+
});
|
|
197
|
+
scope.on(nav, 'pointerover', (event) => { const link = (event.target as Element).closest<HTMLElement>('a[data-mk-section]'); if (link && link.getAttribute('aria-disabled') !== 'true') preview(link.dataset.mkSection!); });
|
|
198
|
+
scope.on(nav, 'focusin', (event) => { const link = (event.target as Element).closest<HTMLElement>('a[data-mk-section]'); if (link) preview(link.dataset.mkSection!); });
|
|
199
|
+
scope.on(root, 'pointerleave', () => { if (!root.contains(document.activeElement)) preview(current); });
|
|
200
|
+
scope.on(root, 'focusout', (event) => { if (!root.contains(event.relatedTarget as Node | null)) preview(current); });
|
|
201
|
+
scope.on(nav, 'keydown', (event) => {
|
|
202
|
+
if (!['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(event.key)) { if (['PageUp', 'PageDown', 'ArrowUp', 'ArrowDown', ' '].includes(event.key)) cancelScroll(); return; }
|
|
203
|
+
const available = links().filter((link) => link.getAttribute('aria-disabled') !== 'true');
|
|
204
|
+
const index = available.indexOf(document.activeElement as HTMLAnchorElement);
|
|
205
|
+
if (index < 0 || !available.length) return;
|
|
206
|
+
event.preventDefault();
|
|
207
|
+
available[event.key === 'Home' ? 0 : event.key === 'End' ? available.length - 1 : (index + (event.key === 'ArrowRight' ? 1 : -1) + available.length) % available.length]?.focus();
|
|
208
|
+
});
|
|
209
|
+
scope.on(window, 'resize', queue, { passive: true });
|
|
210
|
+
if (typeof ResizeObserver === 'function') { const resize = scope.observe(new ResizeObserver(queue)); resize.observe(nav); }
|
|
211
|
+
bindScroller(); render(); update();
|
|
212
|
+
return {
|
|
213
|
+
select, update,
|
|
214
|
+
get value() { return current; },
|
|
215
|
+
setOptions(next) {
|
|
216
|
+
if (scope.disposed) return;
|
|
217
|
+
const focusedId = root.contains(document.activeElement)
|
|
218
|
+
? (document.activeElement as HTMLElement | null)?.closest<HTMLAnchorElement>('a[data-mk-section]')?.dataset.mkSection
|
|
219
|
+
: undefined;
|
|
220
|
+
opts = { ...opts, ...next };
|
|
221
|
+
if ('scroller' in next) { cancelScroll(); bindScroller(); }
|
|
222
|
+
render();
|
|
223
|
+
if (focusedId) { const target = links().find((link) => link.dataset.mkSection === focusedId && link.getAttribute('aria-disabled') !== 'true') ?? links().find((link) => link.getAttribute('aria-disabled') !== 'true'); if (target) target.focus(); else { scope.attr(root, 'tabindex', root.getAttribute('tabindex')); root.tabIndex = -1; root.focus(); } }
|
|
224
|
+
update();
|
|
225
|
+
},
|
|
226
|
+
destroy() { if (scope.disposed) return; cancelAnimationFrame(frame); cancelScroll(); unbindScroller(); animations.forEach((animation) => animation.kill()); gsap.killTweensOf([...root.querySelectorAll('*')]); scope.dispose(); root.replaceChildren(...original); },
|
|
227
|
+
};
|
|
228
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useRef, type HTMLAttributes, type MutableRefObject } from 'react';
|
|
3
|
+
import { useIsomorphicLayoutEffect, useLatest } from '../../react/use-recipe';
|
|
4
|
+
import { createScrollPreviewDock, type ScrollPreviewDockOptions, type ScrollPreviewDockInstance } from './engine';
|
|
5
|
+
|
|
6
|
+
export interface ScrollPreviewDockProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onChange' | 'children' | 'dangerouslySetInnerHTML'>, ScrollPreviewDockOptions {
|
|
7
|
+
instanceRef?: MutableRefObject<ScrollPreviewDockInstance | null>;
|
|
8
|
+
children?: never;
|
|
9
|
+
dangerouslySetInnerHTML?: never;
|
|
10
|
+
}
|
|
11
|
+
export function ScrollPreviewDock({ items, scroller, offset, label, disabled, onChange, instanceRef, className, children, dangerouslySetInnerHTML: _html, ...props }: ScrollPreviewDockProps) {
|
|
12
|
+
const root = useRef<HTMLDivElement>(null);
|
|
13
|
+
const instance = useRef<ScrollPreviewDockInstance | null>(null);
|
|
14
|
+
const onChangeRef = useLatest(onChange);
|
|
15
|
+
useIsomorphicLayoutEffect(() => {
|
|
16
|
+
const created = createScrollPreviewDock(root.current, { items, scroller, offset, label, disabled, onChange: (...args) => onChangeRef.current?.(...args) });
|
|
17
|
+
instance.current = created;
|
|
18
|
+
return () => { created.destroy(); instance.current = null; };
|
|
19
|
+
// Options update the existing engine so timers, selection and scroll state survive.
|
|
20
|
+
}, []);
|
|
21
|
+
useIsomorphicLayoutEffect(() => {
|
|
22
|
+
instance.current?.setOptions({ items, scroller, offset, label, disabled });
|
|
23
|
+
}, [items, scroller, offset, label, disabled]);
|
|
24
|
+
useIsomorphicLayoutEffect(() => { if (instanceRef) instanceRef.current = instance.current; return () => { if (instanceRef) instanceRef.current = null; }; }, [instanceRef]);
|
|
25
|
+
return <div {...props} ref={root} className={['mk-scroll-preview-dock', className].filter(Boolean).join(' ')}></div>;
|
|
26
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
.mk-scroll-preview-dock, mk-scroll-preview-dock { box-sizing: border-box; display: grid; gap: 10px; width: min(100%, 400px); color: var(--mk-text); font-family: var(--mk-font); }
|
|
2
|
+
.mk-scroll-preview-dock__preview { display: flex; align-items: center; gap: 12px; min-height: 60px; box-sizing: border-box; padding: 12px 16px; border-radius: 24px; background: var(--mk-surface-2); box-shadow: 0 1px 0 var(--mk-outline-soft) inset; }
|
|
3
|
+
.mk-scroll-preview-dock__image { display: grid; place-items: center; flex: 0 0 44px; width: 44px; height: 44px; border-radius: 12px; background: var(--mk-surface-3, var(--mk-surface)); overflow: hidden; font-weight: 600; font-size: 14px; }
|
|
4
|
+
.mk-scroll-preview-dock__image img, .mk-scroll-preview-dock__thumb img { display: block; width: 100%; height: 100%; object-fit: cover; }
|
|
5
|
+
.mk-scroll-preview-dock__preview strong { display: block; font-size: 13px; font-weight: 600; line-height: 1.4; }
|
|
6
|
+
.mk-scroll-preview-dock__description { display: block; color: var(--mk-muted); font-size: 11px; line-height: 1.5; }
|
|
7
|
+
.mk-scroll-preview-dock__nav { position: relative; box-sizing: border-box; display: flex; align-items: stretch; justify-content: space-between; gap: 4px; width: 100%; padding: 6px; border-radius: 32px; background: var(--mk-surface); }
|
|
8
|
+
.mk-scroll-preview-dock__pill { position: absolute; top: 0; left: 0; border-radius: 24px; background: var(--mk-surface-3, var(--mk-surface-2)); pointer-events: none; box-shadow: 0 1px 0 var(--mk-outline-soft) inset; }
|
|
9
|
+
.mk-scroll-preview-dock__item { position: relative; z-index: 1; display: flex; flex: 1 1 0; align-items: center; flex-direction: column; justify-content: center; gap: 6px; min-width: 0; min-height: 68px; padding: 6px; border-radius: 24px; text-align: center; color: var(--mk-muted); text-decoration: none; font-size: 10px; line-height: 1.3; -webkit-tap-highlight-color: transparent; transition: color .2s; }
|
|
10
|
+
.mk-scroll-preview-dock__thumb { display: grid; place-items: center; width: 32px; height: 32px; border-radius: 12px; background: var(--mk-surface-2); overflow: hidden; color: var(--mk-text); font-size: 10px; font-weight: 600; transition: transform .4s var(--mk-ease-swatch), border-radius .4s var(--mk-ease-swatch); }
|
|
11
|
+
.mk-scroll-preview-dock__item[aria-current] { color: var(--mk-text); }
|
|
12
|
+
.mk-scroll-preview-dock__item[data-mk-preview=true] .mk-scroll-preview-dock__thumb { transform: translateY(-3px) rotate(-4deg) scale(1.08); border-radius: 8px; }
|
|
13
|
+
.mk-scroll-preview-dock__item:focus-visible { outline: 2px solid var(--mk-accent); outline-offset: 2px; }
|
|
14
|
+
.mk-scroll-preview-dock__item[aria-disabled=true] { opacity: .4; cursor: not-allowed; }
|
|
15
|
+
.mk-scroll-preview-dock [hidden] { display: none !important; }
|
|
16
|
+
.mk-scroll-preview-dock__empty { margin: 0; padding: 20px; text-align: center; background: var(--mk-surface); border-radius: 24px; font-size: 12px; color: var(--mk-muted); }
|
|
17
|
+
@media (prefers-reduced-motion: reduce) { .mk-scroll-preview-dock * { transition: none !important; } }
|
|
@@ -1,34 +1,48 @@
|
|
|
1
1
|
# select
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
Origen: archivo del kit y parámetros (tiempos, curvas).
|
|
3
|
+
Una elección de una lista que se abre como el `menu` del kit (crece desde su botón y las opciones entran escalonadas) en lugar de la lista del sistema. Mejora un `<select>` escrito a mano, o crea uno desde `options`, y lo mantiene sincronizado y oculto: los formularios lo envían y sus escuchas de `change` siguen funcionando. El valor elegido entra con un blur breve y el chevron gira.
|
|
6
4
|
|
|
7
5
|
## HTML / Astro
|
|
8
6
|
|
|
9
7
|
```html
|
|
10
|
-
<mk-select
|
|
8
|
+
<mk-select class="mk-select" label="Responsable">
|
|
9
|
+
<select name="owner">
|
|
10
|
+
<option value="">Elige a alguien</option>
|
|
11
|
+
<optgroup label="Diseño"><option value="ana">Ana Gómez</option></optgroup>
|
|
12
|
+
<optgroup label="Código"><option value="leo">Leo Ruiz</option></optgroup>
|
|
13
|
+
</select>
|
|
14
|
+
</mk-select>
|
|
11
15
|
<script>
|
|
12
16
|
import { defineElements } from '@yewdiel21/motion-kit/elements';
|
|
13
17
|
defineElements(['mk-select']);
|
|
14
18
|
</script>
|
|
15
19
|
```
|
|
16
20
|
|
|
21
|
+
Dentro de un `.mk-field` el botón toma la altura, el radio y el relleno de los demás campos. Para cambiar el valor desde fuera usa el atributo `value` (o `setValue` en la instancia); el `<select>` y el botón se actualizan juntos.
|
|
22
|
+
|
|
17
23
|
## React
|
|
18
24
|
|
|
19
25
|
```tsx
|
|
20
|
-
<Select
|
|
26
|
+
<Select label="Prioridad" name="priority" value={priority} onChange={setPriority}
|
|
27
|
+
options={[{ value: 'low', label: 'Baja' }, { value: 'normal', label: 'Normal' }, { value: 'high', label: 'Alta' }]} />
|
|
21
28
|
```
|
|
22
29
|
|
|
30
|
+
`options` acepta `{ value, label, disabled?, group? }`; `group` crea encabezados como `<optgroup>`.
|
|
31
|
+
|
|
23
32
|
## Opciones
|
|
24
33
|
|
|
25
34
|
| Opción | Atributo | Por defecto | Qué hace |
|
|
26
35
|
|---|---|---|---|
|
|
36
|
+
| `options` | — | las del `<select>` | Opciones; sin ellas se leen del `<select>` interior. |
|
|
37
|
+
| `value` | `value` | el del `<select>` | Valor elegido. |
|
|
38
|
+
| `name` | `name` | — | Nombre del `<select>` que crea (uno escrito conserva el suyo). |
|
|
39
|
+
| `label` | `label` | `'Opciones'` | Nombre accesible; el botón se lee «label: elegido». |
|
|
40
|
+
| `placeholder` | `placeholder` | texto de la opción vacía o `'Elige…'` | Texto mientras no hay elección. |
|
|
41
|
+
| `disabled` | `disabled` | `false` | Desactiva el botón. |
|
|
42
|
+
| `onChange(value)` | evento `change` del `<select>` / `mk:change` | — | Avisa al elegir. |
|
|
27
43
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
`--mk-select-radius`
|
|
44
|
+
Instancia: `setValue(value, notify?)`, `setOptions(options)`, `refresh()`, `open()`, `close()`, `value`, `destroy()`.
|
|
31
45
|
|
|
32
46
|
## Accesibilidad
|
|
33
47
|
|
|
34
|
-
|
|
48
|
+
El botón tiene `aria-haspopup="menu"` y `aria-expanded`; el menú usa `menuitemradio` con `aria-checked` en la opción elegida. ↓ ↑ Intro y Espacio abren el menú; con el botón cerrado, escribir una letra elige la siguiente opción que empieza por ella. Funciona dentro de diálogos, paneles y popovers sin cerrarlos. Con movimiento reducido el menú aparece sin escalonado y el valor cambia sin blur.
|
|
@@ -133,3 +133,19 @@ mk-select {
|
|
|
133
133
|
transition: none;
|
|
134
134
|
}
|
|
135
135
|
}
|
|
136
|
+
|
|
137
|
+
/* Inside a kit field the trigger takes the field's height, radius, fill and type. */
|
|
138
|
+
.mk-field :is(.mk-select, mk-select) {
|
|
139
|
+
display: flex;
|
|
140
|
+
width: 100%;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.mk-field [data-mk-select-trigger] {
|
|
144
|
+
height: auto;
|
|
145
|
+
min-height: 56px;
|
|
146
|
+
padding: 0 14px 0 16px;
|
|
147
|
+
border-radius: var(--mk-field-radius);
|
|
148
|
+
background: var(--mk-field-bg);
|
|
149
|
+
font-family: var(--md-sys-typescale-body-large-font);
|
|
150
|
+
font-size: var(--md-sys-typescale-body-large-size);
|
|
151
|
+
}
|