@yewdiel21/motion-kit 0.14.0 → 0.15.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 +174 -155
- package/bin/motion-kit.mjs +3 -3
- package/dist/auto/index.d.ts +6 -3
- package/dist/auto/index.js +8 -2
- package/dist/auto/index.js.map +1 -1
- package/dist/{chunk-EY4BJKC4.js → chunk-6IRJYXJU.js} +2 -2
- package/dist/{chunk-CU6SXW3E.js → chunk-A6QQCF6V.js} +1309 -310
- package/dist/chunk-A6QQCF6V.js.map +1 -0
- package/dist/chunk-JXRS3OEA.js +3638 -0
- package/dist/chunk-JXRS3OEA.js.map +1 -0
- package/dist/css/components/add-to-cart.css +72 -0
- package/dist/css/components/bidirectional-slider.css +168 -0
- package/dist/css/components/data-table.css +121 -26
- package/dist/css/components/filter-builder.css +10 -15
- package/dist/css/components/heatmap-chart.css +391 -0
- package/dist/css/components/hover-card.css +8 -2
- package/dist/css/components/image-preview.css +90 -2
- package/dist/css/components/json-viewer.css +33 -1
- package/dist/css/components/nav.css +102 -102
- package/dist/css/components/orbit-menu.css +130 -0
- package/dist/css/components/pagination.css +10 -5
- package/dist/css/components/pie-chart.css +390 -0
- package/dist/css/components/pill-button.css +135 -0
- package/dist/css/components/radar-chart.css +370 -0
- package/dist/css/components/radial-chart.css +398 -0
- package/dist/css/components/sankey-chart.css +358 -0
- package/dist/css/components/scroll-island.css +111 -0
- package/dist/css/components/select.css +136 -0
- package/dist/css/components/toolbar.css +122 -0
- package/dist/css/components/tree-view.css +79 -0
- package/dist/css/components/voice-dots.css +70 -0
- package/dist/elements/index.d.ts +180 -7
- package/dist/elements/index.js +287 -6
- package/dist/elements/index.js.map +1 -1
- package/dist/{engine-Cn6IhE3z.d.ts → engine-5zRIssTO.d.ts} +11 -1
- package/dist/index.css +1832 -168
- package/dist/index.d.ts +4 -4
- package/dist/index.js +33 -3
- package/dist/index.js.map +1 -1
- package/dist/react/index.d.ts +117 -11
- package/dist/react/index.js +314 -13
- package/dist/react/index.js.map +1 -1
- package/dist/{reduced-BBUusld6.d.ts → reduced-6X_bns60.d.ts} +1 -1
- package/dist/{reveal-DEvp6U6T.d.ts → reveal-BfyKKsw4.d.ts} +97 -4
- package/dist/store-CA2Qcun_.d.ts +453 -0
- package/package.json +9 -16
- package/registry.json +1096 -948
- package/src/auto/auto-init.ts +6 -0
- package/src/components/add-to-cart/README.md +34 -0
- package/src/components/add-to-cart/add-to-cart.css +71 -0
- package/src/components/add-to-cart/element.ts +20 -0
- package/src/components/add-to-cart/engine.ts +206 -0
- package/src/components/add-to-cart/react.tsx +29 -0
- package/src/components/bidirectional-slider/README.md +46 -0
- package/src/components/bidirectional-slider/bidirectional-slider.css +167 -0
- package/src/components/bidirectional-slider/element.ts +42 -0
- package/src/components/bidirectional-slider/engine.ts +289 -0
- package/src/components/bidirectional-slider/react.tsx +45 -0
- package/src/components/chart/README.md +10 -0
- package/src/components/chart/element.ts +2 -0
- package/src/components/chart/engine.ts +50 -23
- package/src/components/chart/parts.ts +237 -0
- package/src/components/chart/react.tsx +2 -2
- package/src/components/chart/use-chart.ts +55 -0
- package/src/components/data-table/README.md +3 -3
- package/src/components/data-table/data-table.css +121 -26
- package/src/components/data-table/engine.ts +81 -29
- package/src/components/editable-table/engine.ts +67 -11
- package/src/components/filter-builder/engine.ts +33 -2
- package/src/components/filter-builder/filter-builder.css +10 -15
- package/src/components/heatmap-chart/README.md +48 -0
- package/src/components/heatmap-chart/element.ts +21 -0
- package/src/components/heatmap-chart/engine.ts +281 -0
- package/src/components/heatmap-chart/heatmap-chart.css +93 -0
- package/src/components/heatmap-chart/react.tsx +14 -0
- package/src/components/hover-card/engine.ts +30 -1
- package/src/components/hover-card/hover-card.css +8 -2
- package/src/components/image-preview/element.ts +1 -1
- package/src/components/image-preview/engine.ts +95 -15
- package/src/components/image-preview/image-preview.css +90 -2
- package/src/components/image-preview/react.tsx +3 -3
- package/src/components/json-viewer/engine.ts +11 -2
- package/src/components/json-viewer/json-viewer.css +33 -1
- package/src/components/nav/README.md +12 -10
- package/src/components/nav/element.ts +5 -5
- package/src/components/nav/engine.ts +21 -14
- package/src/components/nav/nav.css +102 -102
- package/src/components/nav/react.tsx +4 -4
- package/src/components/orbit-menu/README.md +46 -0
- package/src/components/orbit-menu/element.ts +27 -0
- package/src/components/orbit-menu/engine.ts +179 -0
- package/src/components/orbit-menu/orbit-menu.css +129 -0
- package/src/components/orbit-menu/react.tsx +44 -0
- package/src/components/pagination/README.md +1 -1
- package/src/components/pagination/engine.ts +50 -4
- package/src/components/pagination/pagination.css +10 -5
- package/src/components/pie-chart/README.md +48 -0
- package/src/components/pie-chart/element.ts +30 -0
- package/src/components/pie-chart/engine.ts +283 -0
- package/src/components/pie-chart/pie-chart.css +92 -0
- package/src/components/pie-chart/react.tsx +14 -0
- package/src/components/pill-button/README.md +46 -0
- package/src/components/pill-button/element.ts +16 -0
- package/src/components/pill-button/engine.ts +77 -0
- package/src/components/pill-button/pill-button.css +134 -0
- package/src/components/pill-button/react.tsx +32 -0
- package/src/components/radar-chart/README.md +48 -0
- package/src/components/radar-chart/element.ts +30 -0
- package/src/components/radar-chart/engine.ts +327 -0
- package/src/components/radar-chart/radar-chart.css +72 -0
- package/src/components/radar-chart/react.tsx +14 -0
- package/src/components/radial-chart/README.md +48 -0
- package/src/components/radial-chart/element.ts +30 -0
- package/src/components/radial-chart/engine.ts +290 -0
- package/src/components/radial-chart/radial-chart.css +100 -0
- package/src/components/radial-chart/react.tsx +14 -0
- package/src/components/sankey-chart/README.md +48 -0
- package/src/components/sankey-chart/element.ts +31 -0
- package/src/components/sankey-chart/engine.ts +401 -0
- package/src/components/sankey-chart/react.tsx +14 -0
- package/src/components/sankey-chart/sankey-chart.css +60 -0
- package/src/components/scroll-island/README.md +34 -0
- package/src/components/scroll-island/element.ts +16 -0
- package/src/components/scroll-island/engine.ts +252 -0
- package/src/components/scroll-island/react.tsx +26 -0
- package/src/components/scroll-island/scroll-island.css +110 -0
- package/src/components/select/README.md +34 -0
- package/src/components/select/element.ts +26 -0
- package/src/components/select/engine.ts +267 -0
- package/src/components/select/react.tsx +33 -0
- package/src/components/select/select.css +135 -0
- package/src/components/toolbar/README.md +46 -0
- package/src/components/toolbar/element.ts +30 -0
- package/src/components/toolbar/engine.ts +230 -0
- package/src/components/toolbar/react.tsx +34 -0
- package/src/components/toolbar/toolbar.css +121 -0
- package/src/components/tree-view/element.ts +1 -1
- package/src/components/tree-view/engine.ts +174 -0
- package/src/components/tree-view/react.tsx +7 -3
- package/src/components/tree-view/tree-view.css +79 -0
- package/src/components/voice-dots/README.md +48 -0
- package/src/components/voice-dots/element.ts +45 -0
- package/src/components/voice-dots/engine.ts +243 -0
- package/src/components/voice-dots/react.tsx +52 -0
- package/src/components/voice-dots/voice-dots.css +69 -0
- package/src/elements/index.ts +39 -0
- package/src/index.ts +16 -0
- package/src/motion/liquid.ts +14 -4
- package/src/react/index.ts +14 -1
- package/dist/chunk-6P4P7NU6.js +0 -1401
- package/dist/chunk-6P4P7NU6.js.map +0 -1
- package/dist/chunk-CU6SXW3E.js.map +0 -1
- package/dist/store-DjkTBX7f.d.ts +0 -195
- /package/dist/{chunk-EY4BJKC4.js.map → chunk-6IRJYXJU.js.map} +0 -0
package/src/auto/auto-init.ts
CHANGED
|
@@ -66,6 +66,9 @@ import { createSplitView } from '../components/split-view/engine';
|
|
|
66
66
|
import { createDataTable } from '../components/data-table/engine';
|
|
67
67
|
import { createEditableTable } from '../components/editable-table/engine';
|
|
68
68
|
import { createRichTextEditor } from '../components/rich-text-editor/engine';
|
|
69
|
+
import { createSelect } from '../components/select/engine';
|
|
70
|
+
import { createAddToCart } from '../components/add-to-cart/engine';
|
|
71
|
+
import { createScrollIsland } from '../components/scroll-island/engine';
|
|
69
72
|
import { ScrollTrigger } from '../core/gsap';
|
|
70
73
|
import { warnOnce, type Instance } from '../core/types';
|
|
71
74
|
import { createSpringCards } from '../recipes/spring-cards/create-spring-cards';
|
|
@@ -171,6 +174,9 @@ const factories = {
|
|
|
171
174
|
'data-table': createDataTable,
|
|
172
175
|
'editable-table': createEditableTable,
|
|
173
176
|
'rich-text-editor': createRichTextEditor,
|
|
177
|
+
'select': createSelect,
|
|
178
|
+
'add-to-cart': createAddToCart,
|
|
179
|
+
'scroll-island': createScrollIsland,
|
|
174
180
|
'weight-hover': createWeightHover,
|
|
175
181
|
'sticker': createSticker,
|
|
176
182
|
'scroll-dock': createScrollDock,
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# add-to-cart
|
|
2
|
+
|
|
3
|
+
Qué hace, en una frase.
|
|
4
|
+
|
|
5
|
+
Origen: archivo del kit y parámetros (tiempos, curvas).
|
|
6
|
+
|
|
7
|
+
## HTML / Astro
|
|
8
|
+
|
|
9
|
+
```html
|
|
10
|
+
<mk-add-to-cart></mk-add-to-cart>
|
|
11
|
+
<script>
|
|
12
|
+
import { defineElements } from '@yewdiel21/motion-kit/elements';
|
|
13
|
+
defineElements(['mk-add-to-cart']);
|
|
14
|
+
</script>
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## React
|
|
18
|
+
|
|
19
|
+
```tsx
|
|
20
|
+
<AddToCart />
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Opciones
|
|
24
|
+
|
|
25
|
+
| Opción | Atributo | Por defecto | Qué hace |
|
|
26
|
+
|---|---|---|---|
|
|
27
|
+
|
|
28
|
+
## Variables CSS
|
|
29
|
+
|
|
30
|
+
`--mk-add-to-cart-radius`
|
|
31
|
+
|
|
32
|
+
## Accesibilidad
|
|
33
|
+
|
|
34
|
+
Roles, foco y teclado. Con movimiento reducido…
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/* add-to-cart — the little box that carries the product to the cart: a kraft front with a tape stripe in the accent,
|
|
2
|
+
two flaps that close over the picture, a soft shadow; it flies above everything and never takes the pointer. */
|
|
3
|
+
.mk-add-to-cart,
|
|
4
|
+
mk-add-to-cart {
|
|
5
|
+
display: inline-flex;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
[data-mk-cart-box] {
|
|
9
|
+
--mk-cart-box: #c99a62;
|
|
10
|
+
--mk-cart-box-shade: #b0824d;
|
|
11
|
+
--mk-cart-tape: var(--mk-accent);
|
|
12
|
+
position: fixed;
|
|
13
|
+
z-index: 2147483646;
|
|
14
|
+
pointer-events: none;
|
|
15
|
+
filter: drop-shadow(0 10px 14px rgb(0 0 0 / 0.28));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
[data-mk-cart-thumb] {
|
|
19
|
+
position: absolute;
|
|
20
|
+
top: 6px;
|
|
21
|
+
left: 50%;
|
|
22
|
+
width: 40px;
|
|
23
|
+
height: 40px;
|
|
24
|
+
margin-left: -20px;
|
|
25
|
+
border-radius: 8px;
|
|
26
|
+
object-fit: cover;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
[data-mk-cart-front] {
|
|
30
|
+
position: absolute;
|
|
31
|
+
right: 0;
|
|
32
|
+
bottom: 0;
|
|
33
|
+
left: 0;
|
|
34
|
+
height: 70%;
|
|
35
|
+
border-radius: 4px 4px 8px 8px;
|
|
36
|
+
background: linear-gradient(180deg, var(--mk-cart-box), var(--mk-cart-box-shade));
|
|
37
|
+
transform-origin: 50% 100%;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
[data-mk-cart-tape] {
|
|
41
|
+
position: absolute;
|
|
42
|
+
top: 0;
|
|
43
|
+
left: 50%;
|
|
44
|
+
width: 10px;
|
|
45
|
+
height: 100%;
|
|
46
|
+
margin-left: -5px;
|
|
47
|
+
border-radius: 0 0 2px 2px;
|
|
48
|
+
background: var(--mk-cart-tape);
|
|
49
|
+
opacity: 0.9;
|
|
50
|
+
transform-origin: 50% 0;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
[data-mk-cart-flap] {
|
|
54
|
+
position: absolute;
|
|
55
|
+
top: 18%;
|
|
56
|
+
width: 50%;
|
|
57
|
+
height: 14%;
|
|
58
|
+
background: var(--mk-cart-box-shade);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
[data-mk-cart-flap='left'] {
|
|
62
|
+
left: 0;
|
|
63
|
+
border-radius: 4px 0 0 2px;
|
|
64
|
+
transform-origin: 0 100%;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
[data-mk-cart-flap='right'] {
|
|
68
|
+
right: 0;
|
|
69
|
+
border-radius: 0 4px 2px 0;
|
|
70
|
+
transform-origin: 100% 100%;
|
|
71
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { MkElement, defineElement, type OptionType } from '../../elements/base';
|
|
2
|
+
import { createAddToCart, type AddToCartInstance } from './engine';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* <mk-add-to-cart class="mk-add-to-cart" target="#carrito"><button>Agregar</button></mk-add-to-cart>
|
|
6
|
+
* Packs the product's [data-mk-cart-image] and flies it to the cart. Emits mk:add and mk:land ({count}).
|
|
7
|
+
*/
|
|
8
|
+
export class MkAddToCartElement extends MkElement<AddToCartInstance> {
|
|
9
|
+
static options: Record<string, OptionType> = { target: 'string', image: 'string', quantity: 'number', count: 'number', label: 'string' };
|
|
10
|
+
|
|
11
|
+
protected create(options: Record<string, unknown>) {
|
|
12
|
+
return createAddToCart(this, options);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
get count() {
|
|
16
|
+
return this.instance?.count ?? 0;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const defineAddToCart = () => defineElement('mk-add-to-cart', MkAddToCartElement);
|
|
@@ -0,0 +1,206 @@
|
|
|
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, ringPulse, rollDigits } from '../../motion/micro';
|
|
6
|
+
|
|
7
|
+
/*
|
|
8
|
+
* Add to cart: the button packs the product and sends it to the cart. A copy of
|
|
9
|
+
* the product's picture shrinks and drops into a little box whose flaps close
|
|
10
|
+
* with a bounce and whose tape draws itself (.6 s); the box flies to the cart on
|
|
11
|
+
* an arc (x on the signature curve, y up then down, a slight spin, shrinking,
|
|
12
|
+
* .62 s), dives into it, and the cart bumps, pulses a ring and rolls its count.
|
|
13
|
+
* Each click sends its own box; the cart shows the latest count when a box
|
|
14
|
+
* lands. With reduced motion the count changes at once. The flight comes from
|
|
15
|
+
* the mini POS's flyToCart; the packing is new.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
export interface AddToCartOptions {
|
|
19
|
+
/** Where the box goes: an element, a selector, or (default) the first visible [data-mk-cart-target]. */
|
|
20
|
+
target?: HTMLElement | string;
|
|
21
|
+
/** The picture packed: an element, a selector, or (default) the [data-mk-cart-image] of the [data-mk-cart-item] around the button. */
|
|
22
|
+
image?: HTMLElement | string;
|
|
23
|
+
/** Items each click adds (default 1). */
|
|
24
|
+
quantity?: number;
|
|
25
|
+
/** The cart's count to start from (default: the number in the target's [data-mk-cart-count], else 0). */
|
|
26
|
+
count?: number;
|
|
27
|
+
/** Announced after a box lands (default 'Añadido al carrito'). */
|
|
28
|
+
label?: string;
|
|
29
|
+
/** A click: the new count (right away). */
|
|
30
|
+
onAdd?: (count: number) => void;
|
|
31
|
+
/** A box landed in the cart. */
|
|
32
|
+
onLand?: (count: number) => void;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface AddToCartInstance extends Instance {
|
|
36
|
+
/** Send a box, as a click does. */
|
|
37
|
+
add(): void;
|
|
38
|
+
setCount(count: number): void;
|
|
39
|
+
readonly count: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const BOX = 56;
|
|
43
|
+
const FLIGHT = 0.62;
|
|
44
|
+
|
|
45
|
+
/** An element from an option, a selector, or a default lookup. */
|
|
46
|
+
const resolve = (option: HTMLElement | string | undefined, fallback: () => HTMLElement | null) =>
|
|
47
|
+
option instanceof HTMLElement ? option : typeof option === 'string' ? document.querySelector<HTMLElement>(option) : fallback();
|
|
48
|
+
|
|
49
|
+
export function createAddToCart(root: HTMLElement | null, options: AddToCartOptions = {}): AddToCartInstance {
|
|
50
|
+
const button = root ? (root.matches('button') ? root : root.querySelector<HTMLElement>('button')) : null;
|
|
51
|
+
if (!root || !button) return inert({ add() {}, setCount() {}, count: 0 });
|
|
52
|
+
registerMotion();
|
|
53
|
+
const scope = createScope();
|
|
54
|
+
const quantity = Number.isFinite(options.quantity) && options.quantity! > 0 ? Math.round(options.quantity!) : 1;
|
|
55
|
+
|
|
56
|
+
const target = () =>
|
|
57
|
+
resolve(options.target, () => {
|
|
58
|
+
const all = [...document.querySelectorAll<HTMLElement>('[data-mk-cart-target]')];
|
|
59
|
+
return all.find((element) => {
|
|
60
|
+
const box = element.getBoundingClientRect();
|
|
61
|
+
return box.width > 0 && box.bottom > 0 && box.top < innerHeight;
|
|
62
|
+
}) ?? all[0] ?? null;
|
|
63
|
+
});
|
|
64
|
+
const image = () => resolve(options.image, () => button.closest('[data-mk-cart-item]')?.querySelector<HTMLElement>('[data-mk-cart-image]') ?? null);
|
|
65
|
+
const counter = () => target()?.querySelector<HTMLElement>('[data-mk-cart-count]') ?? null;
|
|
66
|
+
const read = Number.parseInt(counter()?.textContent ?? '', 10);
|
|
67
|
+
let count = Number.isFinite(options.count) ? Math.max(0, Math.round(options.count!)) : Number.isFinite(read) ? read : 0;
|
|
68
|
+
|
|
69
|
+
const status = document.createElement('span');
|
|
70
|
+
status.className = 'mk-sr-only';
|
|
71
|
+
status.setAttribute('role', 'status');
|
|
72
|
+
root.append(status);
|
|
73
|
+
scope.add(() => status.remove());
|
|
74
|
+
scope.attr(root, 'data-mk-ready', '');
|
|
75
|
+
|
|
76
|
+
const boxes = new Set<HTMLElement>();
|
|
77
|
+
const effects = new Set<gsap.core.Animation>();
|
|
78
|
+
const keep = (animation: gsap.core.Animation | null) => {
|
|
79
|
+
if (!animation) return;
|
|
80
|
+
effects.add(animation);
|
|
81
|
+
// Finished ones leave as new ones arrive: the animation's own onComplete stays untouched.
|
|
82
|
+
for (const effect of effects) if (effect.totalProgress() === 1) effects.delete(effect);
|
|
83
|
+
};
|
|
84
|
+
scope.add(() => {
|
|
85
|
+
for (const effect of effects) effect.kill();
|
|
86
|
+
effects.clear();
|
|
87
|
+
for (const box of boxes) box.remove();
|
|
88
|
+
boxes.clear();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
/** The cart takes the box: it bumps, pulses and rolls its count to the latest number. */
|
|
92
|
+
const land = (animate: boolean) => {
|
|
93
|
+
const cart = target();
|
|
94
|
+
const number = counter();
|
|
95
|
+
const text = String(count);
|
|
96
|
+
if (number && number.textContent !== text) {
|
|
97
|
+
if (animate) keep(rollDigits(number, text, 1, 0.35));
|
|
98
|
+
else number.textContent = text;
|
|
99
|
+
}
|
|
100
|
+
if (cart && animate) {
|
|
101
|
+
keep(gsap.fromTo(cart, { scale: 1 }, { scale: 1.1, duration: 0.14, ease: 'power2.out', yoyo: true, repeat: 1, clearProps: 'transform' }));
|
|
102
|
+
keep(ringPulse(cart));
|
|
103
|
+
}
|
|
104
|
+
status.textContent = `${options.label ?? 'Añadido al carrito'}: ${count}`;
|
|
105
|
+
options.onLand?.(count);
|
|
106
|
+
root.dispatchEvent(new CustomEvent('mk:land', { bubbles: true, detail: { count } }));
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
/** The box: the picture inside, a front, two flaps and a strip of tape. */
|
|
110
|
+
const makeBox = (picture: HTMLElement | null) => {
|
|
111
|
+
const box = document.createElement('div');
|
|
112
|
+
box.dataset.mkCartBox = '';
|
|
113
|
+
box.setAttribute('aria-hidden', 'true');
|
|
114
|
+
let thumb: HTMLImageElement | null = null;
|
|
115
|
+
const src = picture instanceof HTMLImageElement ? picture.currentSrc || picture.getAttribute('src') : null;
|
|
116
|
+
if (src) {
|
|
117
|
+
thumb = document.createElement('img');
|
|
118
|
+
thumb.src = src;
|
|
119
|
+
thumb.alt = '';
|
|
120
|
+
thumb.dataset.mkCartThumb = '';
|
|
121
|
+
box.append(thumb);
|
|
122
|
+
}
|
|
123
|
+
const front = document.createElement('span');
|
|
124
|
+
front.dataset.mkCartFront = '';
|
|
125
|
+
const tape = document.createElement('span');
|
|
126
|
+
tape.dataset.mkCartTape = '';
|
|
127
|
+
front.append(tape);
|
|
128
|
+
const left = document.createElement('span');
|
|
129
|
+
left.dataset.mkCartFlap = 'left';
|
|
130
|
+
const right = document.createElement('span');
|
|
131
|
+
right.dataset.mkCartFlap = 'right';
|
|
132
|
+
box.append(front, left, right);
|
|
133
|
+
return { box, thumb, front, tape, left, right };
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const add = () => {
|
|
137
|
+
if (scope.disposed) return;
|
|
138
|
+
count += quantity;
|
|
139
|
+
options.onAdd?.(count);
|
|
140
|
+
root.dispatchEvent(new CustomEvent('mk:add', { bubbles: true, detail: { count } }));
|
|
141
|
+
if (reduced()) return land(false);
|
|
142
|
+
keep(press(button));
|
|
143
|
+
const picture = image();
|
|
144
|
+
const from = (picture ?? button).getBoundingClientRect();
|
|
145
|
+
const cart = target();
|
|
146
|
+
const to = cart?.getBoundingClientRect() ?? null;
|
|
147
|
+
const { box, thumb, front, tape, left, right } = makeBox(picture);
|
|
148
|
+
const startX = from.left + from.width / 2 - BOX / 2;
|
|
149
|
+
const startY = from.top + from.height / 2 - BOX / 2;
|
|
150
|
+
Object.assign(box.style, { left: `${startX}px`, top: `${startY}px`, width: `${BOX}px`, height: `${BOX}px` });
|
|
151
|
+
document.body.append(box);
|
|
152
|
+
boxes.add(box);
|
|
153
|
+
const gone = () => {
|
|
154
|
+
box.remove();
|
|
155
|
+
boxes.delete(box);
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const timeline = gsap.timeline({
|
|
159
|
+
onComplete: () => {
|
|
160
|
+
gone();
|
|
161
|
+
land(true);
|
|
162
|
+
},
|
|
163
|
+
});
|
|
164
|
+
// 1 · Packing: the picture shrinks into the box, the box rises around it, the flaps close, the tape draws.
|
|
165
|
+
if (thumb) {
|
|
166
|
+
const scale = Math.max(from.width, from.height) / 40 || 1;
|
|
167
|
+
timeline.fromTo(thumb, { scale, y: 0, opacity: 1 }, { scale: 1, y: -14, duration: 0.32, ease: EASE.signature }, 0);
|
|
168
|
+
timeline.to(thumb, { y: 4, duration: 0.16, ease: 'power2.in' }, 0.3);
|
|
169
|
+
}
|
|
170
|
+
timeline.fromTo(front, { scaleY: 0 }, { scaleY: 1, duration: 0.26, ease: EASE.pop }, 0.08);
|
|
171
|
+
timeline.fromTo([left, right], { opacity: 0 }, { opacity: 1, duration: 0.12 }, 0.12);
|
|
172
|
+
timeline.fromTo(left, { rotation: -70 }, { rotation: 0, duration: 0.3, ease: EASE.click }, 0.38);
|
|
173
|
+
timeline.fromTo(right, { rotation: 70 }, { rotation: 0, duration: 0.3, ease: EASE.click }, 0.43);
|
|
174
|
+
timeline.fromTo(tape, { scaleY: 0 }, { scaleY: 1, duration: 0.16, ease: EASE.wipe }, 0.62);
|
|
175
|
+
// 2 · The flight: an arc to the cart (x on the signature curve, y up then down), a slight spin, smaller.
|
|
176
|
+
const start = 0.74;
|
|
177
|
+
if (to) {
|
|
178
|
+
const dx = to.left + to.width / 2 - (startX + BOX / 2);
|
|
179
|
+
const dy = to.top + to.height / 2 - (startY + BOX / 2);
|
|
180
|
+
const lift = Math.min(dy, 0) - 70;
|
|
181
|
+
timeline.to(box, { x: dx, duration: FLIGHT, ease: EASE.signature }, start);
|
|
182
|
+
timeline.to(box, { y: lift, duration: FLIGHT * 0.42, ease: 'power2.out' }, start);
|
|
183
|
+
timeline.to(box, { y: dy, duration: FLIGHT * 0.58, ease: 'power2.in' }, start + FLIGHT * 0.42);
|
|
184
|
+
timeline.fromTo(box, { rotation: 0 }, { rotation: 10, scale: 0.55, duration: FLIGHT, ease: 'power1.inOut' }, start);
|
|
185
|
+
// 3 · Into the cart.
|
|
186
|
+
timeline.to(box, { scale: 0.15, opacity: 0, duration: 0.14, ease: 'power2.in' }, start + FLIGHT - 0.04);
|
|
187
|
+
} else timeline.to(box, { y: -24, opacity: 0, scale: 0.8, filter: 'blur(6px)', duration: 0.3, ease: 'power2.in' }, start);
|
|
188
|
+
keep(timeline);
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
scope.on(button, 'click', add);
|
|
192
|
+
|
|
193
|
+
return {
|
|
194
|
+
add,
|
|
195
|
+
setCount(next) {
|
|
196
|
+
if (!Number.isFinite(next)) return;
|
|
197
|
+
count = Math.max(0, Math.round(next));
|
|
198
|
+
const number = counter();
|
|
199
|
+
if (number) number.textContent = String(count);
|
|
200
|
+
},
|
|
201
|
+
get count() {
|
|
202
|
+
return count;
|
|
203
|
+
},
|
|
204
|
+
destroy: () => scope.dispose(),
|
|
205
|
+
};
|
|
206
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useRef, type HTMLAttributes, type MutableRefObject } from 'react';
|
|
3
|
+
import { useIsomorphicLayoutEffect, useLatest } from '../../react/use-recipe';
|
|
4
|
+
import { createAddToCart, type AddToCartInstance, type AddToCartOptions } from './engine';
|
|
5
|
+
|
|
6
|
+
export interface AddToCartProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onAdd'>, AddToCartOptions {
|
|
7
|
+
instanceRef?: MutableRefObject<AddToCartInstance | null>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** <AddToCart target="#carrito" onAdd={setCount}><button className="mk-button">Agregar</button></AddToCart>. */
|
|
11
|
+
export function AddToCart({ target, image, quantity, count, label, onAdd, onLand, instanceRef, className, children, ...props }: AddToCartProps) {
|
|
12
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
13
|
+
const add = useLatest(onAdd);
|
|
14
|
+
const landed = useLatest(onLand);
|
|
15
|
+
useIsomorphicLayoutEffect(() => {
|
|
16
|
+
const created = createAddToCart(ref.current, { target, image, quantity, count, label, onAdd: (n) => add.current?.(n), onLand: (n) => landed.current?.(n) });
|
|
17
|
+
if (instanceRef) instanceRef.current = created;
|
|
18
|
+
return () => {
|
|
19
|
+
created.destroy();
|
|
20
|
+
if (instanceRef) instanceRef.current = null;
|
|
21
|
+
};
|
|
22
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
23
|
+
}, [typeof target === 'string' ? target : null, typeof image === 'string' ? image : null, quantity, label]);
|
|
24
|
+
return (
|
|
25
|
+
<div {...props} ref={ref} className={['mk-add-to-cart', className].filter(Boolean).join(' ')}>
|
|
26
|
+
{children}
|
|
27
|
+
</div>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# bidirectional-slider
|
|
2
|
+
|
|
3
|
+
Control de volumen o balance con relleno desde un origen, burbuja animada y teclado nativo.
|
|
4
|
+
|
|
5
|
+
## Instalación
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
import '@yewdiel21/motion-kit/css';
|
|
9
|
+
import { BidirectionalSlider } from '@yewdiel21/motion-kit/react';
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
```sh
|
|
13
|
+
npx @yewdiel21/motion-kit add bidirectional-slider --react --elements
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## React
|
|
17
|
+
|
|
18
|
+
```tsx
|
|
19
|
+
<BidirectionalSlider label="Volumen" min={0} max={100} origin={0} value={volume} icons="volume" onInput={setVolume} formatValue={n => `${n}%`} />
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Los callbacks del ejemplo (`ask`, `run`, `setTool`, `setVolume`) pertenecen a tu aplicación. React acepta `className`, `style` y atributos HTML. Los componentes con estado exponen `instanceRef` (excepto PillButton).
|
|
23
|
+
|
|
24
|
+
## HTML / Astro
|
|
25
|
+
|
|
26
|
+
```html
|
|
27
|
+
<mk-bidirectional-slider label="Balance" min="-100" max="100" value="0"></mk-bidirectional-slider>
|
|
28
|
+
<script type="module">
|
|
29
|
+
import { defineElements } from '@yewdiel21/motion-kit/elements';
|
|
30
|
+
defineElements(['mk-bidirectional-slider']);
|
|
31
|
+
</script>
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
En Astro usa un `<script>` procesado por Astro. En HTML directo el import requiere bundler o import map. También puedes usar `import { createBidirectionalSlider } from '@yewdiel21/motion-kit'` y llamar a la factory con el contenedor y las mismas opciones. Al desmontar, llama a `instance.destroy()`.
|
|
35
|
+
|
|
36
|
+
## Opciones
|
|
37
|
+
|
|
38
|
+
min: -100. max: 100. origin: 0. step: 1. value: origen. icons: signs, volume o false (atributo icons="none"). disabled, name, label. onInput(value): arrastre. onChange(value): confirmación. Instancia: setValue(value, notify?), reset(notify?), value.
|
|
39
|
+
|
|
40
|
+
Los nombres camelCase se escriben como atributos kebab-case. Los arrays y objetos se pasan como JSON. Los booleanos aceptan atributo vacío o `"false"`.
|
|
41
|
+
|
|
42
|
+
## Estilos y accesibilidad
|
|
43
|
+
|
|
44
|
+
Variables: Usa los colores semánticos --mk-text, --mk-accent y --mk-surface-3..
|
|
45
|
+
|
|
46
|
+
Respeta los colores del tema, foco visible y `prefers-reduced-motion`. Los gráficos incluyen una tabla accesible y selección con flechas, Home, End y Escape. Heatmap usa arriba/abajo para un día e izquierda/derecha para una semana. Toolbar usa flechas y Home/End; OrbitMenu cierra con Escape y devuelve el foco. Los controles conservan nombres accesibles; VoiceDots anuncia los estados sin leer cada fotograma.
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/* bidirectional-slider — a chunky 12 px track whose fill grows from the origin notch towards the thumb:
|
|
2
|
+
"more" in the accent, "less" in a tint of it. − and + sit at the ends; the bubble shows the signed value while held. */
|
|
3
|
+
.mk-bidirectional-slider,
|
|
4
|
+
mk-bidirectional-slider {
|
|
5
|
+
--mk-bislider-height: 2.75rem;
|
|
6
|
+
--mk-bislider-track: var(--mk-surface-2);
|
|
7
|
+
--mk-bislider-more: var(--mk-accent);
|
|
8
|
+
--mk-bislider-less: color-mix(in srgb, var(--mk-accent) 45%, var(--mk-surface-2));
|
|
9
|
+
display: grid;
|
|
10
|
+
grid-template-columns: auto minmax(6rem, 1fr) auto;
|
|
11
|
+
align-items: center;
|
|
12
|
+
gap: 0.375rem;
|
|
13
|
+
width: 100%;
|
|
14
|
+
max-width: 26rem;
|
|
15
|
+
color: var(--mk-text);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
[data-mk-bislider-track] {
|
|
19
|
+
position: relative;
|
|
20
|
+
height: var(--mk-bislider-height);
|
|
21
|
+
border-radius: 12px;
|
|
22
|
+
background: var(--mk-bislider-track);
|
|
23
|
+
touch-action: none;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
[data-mk-bislider-track]:has(input:focus-visible) {
|
|
27
|
+
outline: 2px solid var(--mk-accent);
|
|
28
|
+
outline-offset: 3px;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
[data-mk-bislider-clip] {
|
|
32
|
+
position: absolute;
|
|
33
|
+
inset: 0;
|
|
34
|
+
overflow: hidden;
|
|
35
|
+
border-radius: inherit;
|
|
36
|
+
pointer-events: none;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
[data-mk-bislider-fill] {
|
|
40
|
+
position: absolute;
|
|
41
|
+
top: 0;
|
|
42
|
+
bottom: 0;
|
|
43
|
+
background: var(--mk-bislider-more);
|
|
44
|
+
transition: background-color 0.2s ease;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
:is(.mk-bidirectional-slider, mk-bidirectional-slider)[data-mk-side='less'] [data-mk-bislider-fill] {
|
|
48
|
+
background: var(--mk-bislider-less);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
[data-mk-bislider-notch] {
|
|
52
|
+
position: absolute;
|
|
53
|
+
top: 30%;
|
|
54
|
+
bottom: 30%;
|
|
55
|
+
width: 2px;
|
|
56
|
+
margin-left: -1px;
|
|
57
|
+
border-radius: 2px;
|
|
58
|
+
background: color-mix(in srgb, var(--mk-text) 30%, transparent);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
[data-mk-bislider-thumb] {
|
|
62
|
+
position: absolute;
|
|
63
|
+
top: 20%;
|
|
64
|
+
bottom: 20%;
|
|
65
|
+
width: 4px;
|
|
66
|
+
margin-left: -2px;
|
|
67
|
+
border-radius: 4px;
|
|
68
|
+
background: var(--mk-text);
|
|
69
|
+
pointer-events: none;
|
|
70
|
+
transition: transform 0.35s var(--mk-ease-swatch);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
:is(.mk-bidirectional-slider, mk-bidirectional-slider)[data-mk-held] [data-mk-bislider-thumb] {
|
|
74
|
+
transform: scale(1.5, 1.2);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
[data-mk-bislider-bubble] {
|
|
78
|
+
position: absolute;
|
|
79
|
+
bottom: calc(100% + 0.5rem);
|
|
80
|
+
padding: 0.2rem 0.55rem;
|
|
81
|
+
border-radius: var(--mk-radius-full);
|
|
82
|
+
background: var(--mk-glass-inverse);
|
|
83
|
+
box-shadow: var(--mk-glass-bevel);
|
|
84
|
+
color: var(--mk-on-glass-inverse);
|
|
85
|
+
font: 600 0.75rem/1.2 var(--mk-font);
|
|
86
|
+
font-variant-numeric: tabular-nums;
|
|
87
|
+
white-space: nowrap;
|
|
88
|
+
pointer-events: none;
|
|
89
|
+
opacity: 0;
|
|
90
|
+
transform: translate(-50%, 4px) scale(0.85);
|
|
91
|
+
transform-origin: 50% 100%;
|
|
92
|
+
transition:
|
|
93
|
+
opacity 0.15s ease,
|
|
94
|
+
transform 0.3s var(--mk-ease-swatch);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
:is(.mk-bidirectional-slider, mk-bidirectional-slider):is([data-mk-held], :has(input:focus-visible)) [data-mk-bislider-bubble] {
|
|
98
|
+
opacity: 1;
|
|
99
|
+
transform: translate(-50%, 0) scale(1);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
[data-mk-bislider-input] {
|
|
103
|
+
position: absolute;
|
|
104
|
+
inset: 0;
|
|
105
|
+
width: 100%;
|
|
106
|
+
height: 100%;
|
|
107
|
+
margin: 0;
|
|
108
|
+
opacity: 0;
|
|
109
|
+
cursor: grab;
|
|
110
|
+
appearance: none;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
:is(.mk-bidirectional-slider, mk-bidirectional-slider)[data-mk-held] [data-mk-bislider-input] {
|
|
114
|
+
cursor: grabbing;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
:is([data-mk-bislider-less], [data-mk-bislider-more]) {
|
|
118
|
+
display: grid;
|
|
119
|
+
place-items: center;
|
|
120
|
+
width: var(--mk-bislider-height);
|
|
121
|
+
height: var(--mk-bislider-height);
|
|
122
|
+
padding: 0;
|
|
123
|
+
border: 0;
|
|
124
|
+
border-radius: 12px;
|
|
125
|
+
background: transparent;
|
|
126
|
+
color: inherit;
|
|
127
|
+
cursor: pointer;
|
|
128
|
+
transition: background-color 0.15s ease;
|
|
129
|
+
-webkit-tap-highlight-color: transparent;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
:is([data-mk-bislider-less], [data-mk-bislider-more]) svg {
|
|
133
|
+
width: 1.25rem;
|
|
134
|
+
height: 1.25rem;
|
|
135
|
+
fill: none;
|
|
136
|
+
stroke: currentColor;
|
|
137
|
+
stroke-width: 2;
|
|
138
|
+
stroke-linecap: round;
|
|
139
|
+
stroke-linejoin: round;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
@media (hover: hover) {
|
|
143
|
+
:is([data-mk-bislider-less], [data-mk-bislider-more]):hover {
|
|
144
|
+
background: var(--mk-surface-2);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
:is(.mk-bidirectional-slider, mk-bidirectional-slider):has(input:disabled) {
|
|
149
|
+
opacity: 0.5;
|
|
150
|
+
pointer-events: none;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
@media (prefers-reduced-motion: reduce) {
|
|
154
|
+
[data-mk-bislider-thumb],
|
|
155
|
+
[data-mk-bislider-bubble] {
|
|
156
|
+
transition: opacity 0.15s ease;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
@media (forced-colors: active) {
|
|
161
|
+
[data-mk-bislider-fill] {
|
|
162
|
+
background: Highlight !important;
|
|
163
|
+
}
|
|
164
|
+
[data-mk-bislider-track] {
|
|
165
|
+
border: 1px solid CanvasText;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { MkElement, defineElement, type OptionType } from '../../elements/base';
|
|
2
|
+
import { createBidirectionalSlider, type BidirectionalSliderInstance } from './engine';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* <mk-bidirectional-slider class="mk-bidirectional-slider" label="Volumen" icons="volume" value="20"></mk-bidirectional-slider>
|
|
6
|
+
* A value either side of an origin; the fill grows from it. Emits mk:input and mk:change ({value}).
|
|
7
|
+
*/
|
|
8
|
+
export class MkBidirectionalSliderElement extends MkElement<BidirectionalSliderInstance> {
|
|
9
|
+
static options: Record<string, OptionType> = {
|
|
10
|
+
min: 'number',
|
|
11
|
+
max: 'number',
|
|
12
|
+
origin: 'number',
|
|
13
|
+
step: 'number',
|
|
14
|
+
value: 'number',
|
|
15
|
+
disabled: 'boolean',
|
|
16
|
+
name: 'string',
|
|
17
|
+
label: 'string',
|
|
18
|
+
icons: 'string',
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
protected create(options: Record<string, unknown>) {
|
|
22
|
+
return createBidirectionalSlider(this, { ...options, icons: options.icons === 'none' ? false : (options.icons as 'signs' | 'volume' | undefined) });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
get value() {
|
|
26
|
+
return this.instance?.value ?? 0;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
set value(next: number) { this.setValue(next); }
|
|
30
|
+
setValue(value: number, notify = false) { this.instance?.setValue(value, notify); }
|
|
31
|
+
reset(notify = false) { this.instance?.reset(notify); }
|
|
32
|
+
|
|
33
|
+
attributeChangedCallback(name: string, previous: string | null, next: string | null) {
|
|
34
|
+
if (name === 'value' && this.instance) {
|
|
35
|
+
this.instance.setValue(Number(next ?? this.options.origin ?? 0));
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
super.attributeChangedCallback(name, previous, next);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const defineBidirectionalSlider = () => defineElement('mk-bidirectional-slider', MkBidirectionalSliderElement);
|