@yewdiel21/motion-kit 0.14.0 → 0.16.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.
Files changed (156) hide show
  1. package/README.md +178 -155
  2. package/bin/motion-kit.mjs +3 -3
  3. package/dist/auto/index.d.ts +6 -3
  4. package/dist/auto/index.js +8 -2
  5. package/dist/auto/index.js.map +1 -1
  6. package/dist/{chunk-EY4BJKC4.js → chunk-NFZIX3IA.js} +2 -2
  7. package/dist/chunk-RBEKR5UZ.js +3660 -0
  8. package/dist/chunk-RBEKR5UZ.js.map +1 -0
  9. package/dist/{chunk-CU6SXW3E.js → chunk-SDQ776LG.js} +1596 -485
  10. package/dist/chunk-SDQ776LG.js.map +1 -0
  11. package/dist/css/components/add-to-cart.css +72 -0
  12. package/dist/css/components/bidirectional-slider.css +168 -0
  13. package/dist/css/components/chart.css +7 -0
  14. package/dist/css/components/data-table.css +121 -26
  15. package/dist/css/components/filter-builder.css +10 -15
  16. package/dist/css/components/heatmap-chart.css +398 -0
  17. package/dist/css/components/hover-card.css +8 -2
  18. package/dist/css/components/image-preview.css +90 -2
  19. package/dist/css/components/json-viewer.css +33 -1
  20. package/dist/css/components/nav.css +102 -102
  21. package/dist/css/components/orbit-menu.css +130 -0
  22. package/dist/css/components/pagination.css +10 -5
  23. package/dist/css/components/pie-chart.css +397 -0
  24. package/dist/css/components/pill-button.css +135 -0
  25. package/dist/css/components/radar-chart.css +377 -0
  26. package/dist/css/components/radial-chart.css +412 -0
  27. package/dist/css/components/sankey-chart.css +365 -0
  28. package/dist/css/components/scroll-island.css +114 -0
  29. package/dist/css/components/select.css +136 -0
  30. package/dist/css/components/toolbar.css +122 -0
  31. package/dist/css/components/tree-view.css +79 -0
  32. package/dist/css/components/voice-dots.css +70 -0
  33. package/dist/elements/index.d.ts +180 -7
  34. package/dist/elements/index.js +293 -7
  35. package/dist/elements/index.js.map +1 -1
  36. package/dist/{engine-Cn6IhE3z.d.ts → engine-5zRIssTO.d.ts} +11 -1
  37. package/dist/index.css +1849 -168
  38. package/dist/index.d.ts +4 -4
  39. package/dist/index.js +33 -3
  40. package/dist/index.js.map +1 -1
  41. package/dist/react/index.d.ts +117 -11
  42. package/dist/react/index.js +320 -13
  43. package/dist/react/index.js.map +1 -1
  44. package/dist/{reduced-BBUusld6.d.ts → reduced-DQwVXQYb.d.ts} +1 -1
  45. package/dist/{reveal-DEvp6U6T.d.ts → reveal-DRpNO4KP.d.ts} +109 -4
  46. package/dist/store-Da3zSh8Z.d.ts +457 -0
  47. package/package.json +9 -16
  48. package/registry.json +1096 -948
  49. package/src/auto/auto-init.ts +6 -0
  50. package/src/components/add-to-cart/README.md +49 -0
  51. package/src/components/add-to-cart/add-to-cart.css +71 -0
  52. package/src/components/add-to-cart/element.ts +20 -0
  53. package/src/components/add-to-cart/engine.ts +206 -0
  54. package/src/components/add-to-cart/react.tsx +33 -0
  55. package/src/components/bidirectional-slider/README.md +46 -0
  56. package/src/components/bidirectional-slider/bidirectional-slider.css +167 -0
  57. package/src/components/bidirectional-slider/element.ts +42 -0
  58. package/src/components/bidirectional-slider/engine.ts +289 -0
  59. package/src/components/bidirectional-slider/react.tsx +45 -0
  60. package/src/components/chart/README.md +49 -24
  61. package/src/components/chart/chart.css +7 -0
  62. package/src/components/chart/element.ts +4 -1
  63. package/src/components/chart/engine.ts +153 -34
  64. package/src/components/chart/parts.ts +237 -0
  65. package/src/components/chart/react.tsx +2 -2
  66. package/src/components/chart/use-chart.ts +55 -0
  67. package/src/components/data-table/README.md +3 -3
  68. package/src/components/data-table/data-table.css +121 -26
  69. package/src/components/data-table/engine.ts +81 -29
  70. package/src/components/editable-table/engine.ts +67 -11
  71. package/src/components/filter-builder/engine.ts +33 -2
  72. package/src/components/filter-builder/filter-builder.css +10 -15
  73. package/src/components/heatmap-chart/README.md +48 -0
  74. package/src/components/heatmap-chart/element.ts +21 -0
  75. package/src/components/heatmap-chart/engine.ts +281 -0
  76. package/src/components/heatmap-chart/heatmap-chart.css +93 -0
  77. package/src/components/heatmap-chart/react.tsx +14 -0
  78. package/src/components/hover-card/engine.ts +30 -1
  79. package/src/components/hover-card/hover-card.css +8 -2
  80. package/src/components/image-preview/element.ts +1 -1
  81. package/src/components/image-preview/engine.ts +95 -15
  82. package/src/components/image-preview/image-preview.css +90 -2
  83. package/src/components/image-preview/react.tsx +3 -3
  84. package/src/components/json-viewer/engine.ts +11 -2
  85. package/src/components/json-viewer/json-viewer.css +33 -1
  86. package/src/components/nav/README.md +12 -10
  87. package/src/components/nav/element.ts +5 -5
  88. package/src/components/nav/engine.ts +21 -14
  89. package/src/components/nav/nav.css +102 -102
  90. package/src/components/nav/react.tsx +4 -4
  91. package/src/components/orbit-menu/README.md +46 -0
  92. package/src/components/orbit-menu/element.ts +27 -0
  93. package/src/components/orbit-menu/engine.ts +179 -0
  94. package/src/components/orbit-menu/orbit-menu.css +129 -0
  95. package/src/components/orbit-menu/react.tsx +44 -0
  96. package/src/components/pagination/README.md +1 -1
  97. package/src/components/pagination/engine.ts +50 -4
  98. package/src/components/pagination/pagination.css +10 -5
  99. package/src/components/pie-chart/README.md +48 -0
  100. package/src/components/pie-chart/element.ts +30 -0
  101. package/src/components/pie-chart/engine.ts +283 -0
  102. package/src/components/pie-chart/pie-chart.css +92 -0
  103. package/src/components/pie-chart/react.tsx +14 -0
  104. package/src/components/pill-button/README.md +46 -0
  105. package/src/components/pill-button/element.ts +16 -0
  106. package/src/components/pill-button/engine.ts +77 -0
  107. package/src/components/pill-button/pill-button.css +134 -0
  108. package/src/components/pill-button/react.tsx +32 -0
  109. package/src/components/radar-chart/README.md +48 -0
  110. package/src/components/radar-chart/element.ts +30 -0
  111. package/src/components/radar-chart/engine.ts +327 -0
  112. package/src/components/radar-chart/radar-chart.css +72 -0
  113. package/src/components/radar-chart/react.tsx +14 -0
  114. package/src/components/radial-chart/README.md +58 -0
  115. package/src/components/radial-chart/element.ts +30 -0
  116. package/src/components/radial-chart/engine.ts +317 -0
  117. package/src/components/radial-chart/radial-chart.css +107 -0
  118. package/src/components/radial-chart/react.tsx +14 -0
  119. package/src/components/sankey-chart/README.md +48 -0
  120. package/src/components/sankey-chart/element.ts +31 -0
  121. package/src/components/sankey-chart/engine.ts +401 -0
  122. package/src/components/sankey-chart/react.tsx +14 -0
  123. package/src/components/sankey-chart/sankey-chart.css +60 -0
  124. package/src/components/scroll-island/README.md +44 -0
  125. package/src/components/scroll-island/element.ts +16 -0
  126. package/src/components/scroll-island/engine.ts +274 -0
  127. package/src/components/scroll-island/react.tsx +26 -0
  128. package/src/components/scroll-island/scroll-island.css +113 -0
  129. package/src/components/select/README.md +34 -0
  130. package/src/components/select/element.ts +26 -0
  131. package/src/components/select/engine.ts +267 -0
  132. package/src/components/select/react.tsx +33 -0
  133. package/src/components/select/select.css +135 -0
  134. package/src/components/toolbar/README.md +46 -0
  135. package/src/components/toolbar/element.ts +30 -0
  136. package/src/components/toolbar/engine.ts +230 -0
  137. package/src/components/toolbar/react.tsx +34 -0
  138. package/src/components/toolbar/toolbar.css +121 -0
  139. package/src/components/tree-view/element.ts +1 -1
  140. package/src/components/tree-view/engine.ts +174 -0
  141. package/src/components/tree-view/react.tsx +7 -3
  142. package/src/components/tree-view/tree-view.css +79 -0
  143. package/src/components/voice-dots/README.md +48 -0
  144. package/src/components/voice-dots/element.ts +45 -0
  145. package/src/components/voice-dots/engine.ts +243 -0
  146. package/src/components/voice-dots/react.tsx +52 -0
  147. package/src/components/voice-dots/voice-dots.css +69 -0
  148. package/src/elements/index.ts +39 -0
  149. package/src/index.ts +16 -0
  150. package/src/motion/liquid.ts +14 -4
  151. package/src/react/index.ts +14 -1
  152. package/dist/chunk-6P4P7NU6.js +0 -1401
  153. package/dist/chunk-6P4P7NU6.js.map +0 -1
  154. package/dist/chunk-CU6SXW3E.js.map +0 -1
  155. package/dist/store-DjkTBX7f.d.ts +0 -195
  156. /package/dist/{chunk-EY4BJKC4.js.map → chunk-NFZIX3IA.js.map} +0 -0
@@ -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,49 @@
1
+ # add-to-cart
2
+
3
+ El producto se empaca en una caja y vuela hasta un carrito; al llegar, el contador cambia y el carrito responde con un pulso. Cada clic tiene su propio vuelo. Con movimiento reducido, el contador se actualiza al instante.
4
+
5
+ ## React
6
+
7
+ ```tsx
8
+ import '@yewdiel21/motion-kit/css';
9
+ import { AddToCart } from '@yewdiel21/motion-kit/react';
10
+
11
+ <div id="my-cart">Carrito <span data-mk-cart-count>0</span></div>
12
+ <article data-mk-cart-item>
13
+ <img src="/product.webp" alt="Producto" data-mk-cart-image />
14
+ <AddToCart target="#my-cart" onLand={count => console.log(count)}>
15
+ <button type="button">Agregar al carrito</button>
16
+ </AddToCart>
17
+ </article>
18
+ ```
19
+
20
+ **El botón es obligatorio.** Para ver el recorrido necesitas un destino visible y una imagen. Usa un destino único por demo o carrito para no mezclar instancias. El componente anima la interfaz; la aplicación gestiona el carrito y el inventario reales.
21
+
22
+ ## HTML / Astro
23
+
24
+ ```html
25
+ <div id="my-cart">Carrito <span data-mk-cart-count>0</span></div>
26
+ <article data-mk-cart-item>
27
+ <img src="/product.webp" alt="Producto" data-mk-cart-image>
28
+ <mk-add-to-cart target="#my-cart"><button type="button">Agregar</button></mk-add-to-cart>
29
+ </article>
30
+ <script>
31
+ import { defineElements } from '@yewdiel21/motion-kit/elements';
32
+ defineElements(['mk-add-to-cart']);
33
+ </script>
34
+ ```
35
+
36
+ En vanilla puedes llamar a `createAddToCart(root, options)`. `destroy()` limpia los vuelos y listeners.
37
+
38
+ ## Opciones y eventos
39
+
40
+ - `target`: HTMLElement o selector del carrito; por defecto busca `[data-mk-cart-target]` visible.
41
+ - `image`: HTMLElement o selector; por defecto `[data-mk-cart-image]` dentro del producto.
42
+ - `quantity`: unidades por clic, 1 por defecto.
43
+ - `count`: contador inicial; en React también sincroniza cambios posteriores.
44
+ - `label`: anuncio al llegar, por defecto “Añadido al carrito”.
45
+ - `onAdd(count)`: al pulsar; `onLand(count)`: al terminar el vuelo.
46
+ - Eventos DOM: `mk:add` y `mk:land`, con `detail.count`.
47
+ - Instancia: `add()`, `setCount(count)`, `count`, `destroy()`; React expone `instanceRef`.
48
+
49
+ El estado final se anuncia con `role="status"`. Usa un botón nativo con nombre accesible. Los vuelos son decorativos, no capturan el puntero y se eliminan al desmontar.
@@ -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,33 @@
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
+ const instance = useRef<AddToCartInstance | null>(null);
16
+ useIsomorphicLayoutEffect(() => {
17
+ const created = createAddToCart(ref.current, { target, image, quantity, count, label, onAdd: (n) => add.current?.(n), onLand: (n) => landed.current?.(n) });
18
+ instance.current = created;
19
+ if (instanceRef) instanceRef.current = created;
20
+ return () => {
21
+ created.destroy();
22
+ instance.current = null;
23
+ if (instanceRef) instanceRef.current = null;
24
+ };
25
+ // eslint-disable-next-line react-hooks/exhaustive-deps
26
+ }, [typeof target === 'string' ? target : null, typeof image === 'string' ? image : null, quantity, label]);
27
+ useIsomorphicLayoutEffect(() => { if (count !== undefined) instance.current?.setCount(count); }, [count]);
28
+ return (
29
+ <div {...props} ref={ref} className={['mk-add-to-cart', className].filter(Boolean).join(' ')}>
30
+ {children}
31
+ </div>
32
+ );
33
+ }
@@ -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);