@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
@@ -0,0 +1,289 @@
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 } from '../../motion/micro';
6
+
7
+ /*
8
+ * Bidirectional slider: a value either side of an origin (−100…100 around 0 by
9
+ * default), for volume, balance, exposure or a correction. The fill grows from
10
+ * the origin towards the thumb in the "less" or "more" colour; crossing the
11
+ * origin pops its notch; reaching an end bumps that end's icon. Dragging past
12
+ * an end stretches the track like rubber (up to 4 %) and it springs back when
13
+ * let go. The − and + buttons step it (held, they repeat), a double click
14
+ * glides back to the origin, and a bubble shows the signed value while it is
15
+ * held or focused. Keys, the buttons and setValue glide on the swatch curve
16
+ * (.38,.49,0,1.5); dragging follows the pointer. Built on a native
17
+ * <input type="range"> (keyboard, touch, forms and assistive tech).
18
+ */
19
+
20
+ export interface BidirectionalSliderOptions {
21
+ /** Default −100. */
22
+ min?: number;
23
+ /** Default 100. */
24
+ max?: number;
25
+ /** Where the fill starts (default 0, kept inside min…max). */
26
+ origin?: number;
27
+ step?: number;
28
+ value?: number;
29
+ disabled?: boolean;
30
+ name?: string;
31
+ /** Accessible name of the input. */
32
+ label?: string;
33
+ /** The end icons: − and + (default), the speaker, or none. */
34
+ icons?: 'signs' | 'volume' | false;
35
+ /** Text for the bubble and aria-valuetext (default: signed, "+24", "−12"). */
36
+ formatValue?: (value: number) => string;
37
+ onInput?: (value: number) => void;
38
+ onChange?: (value: number) => void;
39
+ }
40
+
41
+ export interface BidirectionalSliderInstance extends Instance {
42
+ setValue(value: number, notify?: boolean): void;
43
+ /** Back to the origin. */
44
+ reset(notify?: boolean): void;
45
+ readonly value: number;
46
+ }
47
+
48
+ const finite = (value: unknown): value is number => typeof value === 'number' && Number.isFinite(value);
49
+ const ICONS = {
50
+ signs: ['<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M6 12h12"/></svg>', '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M6 12h12M12 6v12"/></svg>'],
51
+ volume: [
52
+ '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 9.5v5h3.5L12 18.5v-13L7.5 9.5z"/><path d="M15.5 9.5a3.5 3.5 0 0 1 0 5"/></svg>',
53
+ '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 9.5v5h3.5L12 18.5v-13L7.5 9.5z"/><path d="M15.5 9.5a3.5 3.5 0 0 1 0 5"/><path d="M18 7a7 7 0 0 1 0 10"/></svg>',
54
+ ],
55
+ } as const;
56
+
57
+ export function createBidirectionalSlider(root: HTMLElement | null, options: BidirectionalSliderOptions = {}): BidirectionalSliderInstance {
58
+ if (!root) return inert({ setValue() {}, reset() {}, value: 0 });
59
+ registerMotion();
60
+ const scope = createScope();
61
+ const min = finite(options.min) ? options.min : -100;
62
+ const max = finite(options.max) && options.max > min ? options.max : Math.max(min + 1, 100);
63
+ const origin = Math.min(max, Math.max(min, finite(options.origin) ? options.origin : 0));
64
+ const step = finite(options.step) && options.step > 0 ? options.step : 1;
65
+ const decimals = (String(step).split('.')[1] ?? '').length;
66
+ const snap = (n: number) => Number(Math.min(max, Math.max(min, Math.round((n - min) / step) * step + min)).toFixed(decimals));
67
+ const format =
68
+ options.formatValue ??
69
+ ((n: number) => {
70
+ const delta = Number((n - origin).toFixed(decimals));
71
+ const text = new Intl.NumberFormat(undefined, { maximumFractionDigits: decimals }).format(Math.abs(delta));
72
+ return delta > 0 ? `+${text}` : delta < 0 ? `−${text}` : text;
73
+ });
74
+
75
+ const made = <K extends keyof HTMLElementTagNameMap>(tag: K, parent: Element, data: string) => {
76
+ const element = document.createElement(tag);
77
+ element.setAttribute(`data-mk-bislider-${data}`, '');
78
+ parent.append(element);
79
+ scope.add(() => element.remove());
80
+ return element;
81
+ };
82
+ const icons = options.icons === false ? null : ICONS[options.icons ?? 'signs'];
83
+ const less = icons ? made('button', root, 'less') : null;
84
+ const track = made('span', root, 'track');
85
+ const more = icons ? made('button', root, 'more') : null;
86
+ if (less && more && icons) {
87
+ less.type = more.type = 'button';
88
+ less.tabIndex = more.tabIndex = -1;
89
+ less.innerHTML = icons[0];
90
+ more.innerHTML = icons[1];
91
+ less.setAttribute('aria-label', 'Menos');
92
+ more.setAttribute('aria-label', 'Más');
93
+ root.prepend(less);
94
+ }
95
+ // The fill and notch are clipped to the track's corners; the thumb and bubble are not.
96
+ const clip = made('span', track, 'clip');
97
+ clip.setAttribute('aria-hidden', 'true');
98
+ const fill = made('span', clip, 'fill');
99
+ const notch = made('span', clip, 'notch');
100
+ const thumb = made('span', track, 'thumb');
101
+ const bubble = made('span', track, 'bubble');
102
+ for (const part of [fill, notch, thumb, bubble]) part.setAttribute('aria-hidden', 'true');
103
+
104
+ let input = root.querySelector<HTMLInputElement>('input[type="range"]');
105
+ if (input) {
106
+ // An authored input goes into the track and back to its place (with its attributes) on destroy.
107
+ const kept = input;
108
+ const place = { parent: kept.parentNode, next: kept.nextSibling };
109
+ const saved = ['min', 'max', 'step', 'value', 'data-mk-bislider-input'].map((name) => [name, kept.getAttribute(name)] as const);
110
+ const value = kept.value;
111
+ scope.add(() => {
112
+ for (const [name, was] of saved) {
113
+ if (was === null) kept.removeAttribute(name);
114
+ else kept.setAttribute(name, was);
115
+ }
116
+ kept.value = value;
117
+ place.parent?.insertBefore(kept, place.next);
118
+ });
119
+ } else {
120
+ input = document.createElement('input');
121
+ input.type = 'range';
122
+ scope.add(() => input!.remove());
123
+ }
124
+ const range = input;
125
+ track.append(range);
126
+ range.dataset.mkBisliderInput = '';
127
+ range.min = String(min);
128
+ range.max = String(max);
129
+ range.step = String(step);
130
+ if (options.name) scope.attr(range, 'name', options.name);
131
+ if (options.label && !range.getAttribute('aria-label')) scope.attr(range, 'aria-label', options.label);
132
+ if (options.disabled) scope.attr(range, 'disabled', '');
133
+ const start = snap(finite(options.value) ? options.value : range.getAttribute('value') !== null ? Number(range.getAttribute('value')) : origin);
134
+ range.value = String(start);
135
+ range.defaultValue = String(start);
136
+ scope.attr(root, 'data-mk-ready', '');
137
+
138
+ let value = start;
139
+ let committed = start;
140
+ /** What is drawn (trails the value while gliding). */
141
+ const shown = { v: start };
142
+ const pct = (n: number) => ((n - min) / (max - min)) * 100;
143
+ notch.style.left = `${pct(origin)}%`;
144
+
145
+ const paint = () => {
146
+ const at = pct(shown.v);
147
+ const from = pct(origin);
148
+ fill.style.left = `${Math.min(at, from)}%`;
149
+ fill.style.width = `${Math.abs(at - from)}%`;
150
+ thumb.style.left = `${at}%`;
151
+ bubble.style.left = `${at}%`;
152
+ root.setAttribute('data-mk-side', shown.v > origin ? 'more' : shown.v < origin ? 'less' : 'origin');
153
+ };
154
+ scope.add(() => root.removeAttribute('data-mk-side'));
155
+ const describe = () => {
156
+ const text = format(value);
157
+ bubble.textContent = text;
158
+ range.setAttribute('aria-valuetext', text);
159
+ };
160
+ scope.add(() => range.removeAttribute('aria-valuetext'));
161
+
162
+ const effects = new Set<gsap.core.Animation>();
163
+ const keep = (animation: gsap.core.Animation | null) => {
164
+ if (!animation) return;
165
+ effects.add(animation);
166
+ for (const effect of effects) if (effect.totalProgress() === 1) effects.delete(effect);
167
+ };
168
+ scope.add(() => {
169
+ gsap.killTweensOf(shown);
170
+ for (const effect of effects) effect.kill();
171
+ effects.clear();
172
+ });
173
+ const bump = (element: Element | null, scale = 1.25) => {
174
+ if (!element || reduced()) return;
175
+ keep(gsap.fromTo(element, { scale }, { scale: 1, duration: 0.5, ease: 'elastic.out(1, 0.4)', overwrite: 'auto' }));
176
+ };
177
+
178
+ let held = false;
179
+ /** Set the value; `glide` tweens the drawing, else it follows at once. */
180
+ const set = (next: number, glide: boolean, notify: 'input' | 'change' | false) => {
181
+ const before = value;
182
+ value = snap(next);
183
+ range.value = String(value);
184
+ describe();
185
+ gsap.killTweensOf(shown);
186
+ if (glide && !reduced()) keep(gsap.to(shown, { v: value, duration: 0.45, ease: EASE.swatch, onUpdate: paint }));
187
+ else {
188
+ shown.v = value;
189
+ paint();
190
+ }
191
+ // Crossing the origin pops its notch; arriving at an end bumps that end's icon.
192
+ if ((before - origin) * (value - origin) < 0 || (before !== origin && value === origin)) bump(notch, 1.8);
193
+ if (value !== before && value === max) bump(more);
194
+ if (value !== before && value === min) bump(less);
195
+ if (notify && value !== before) {
196
+ options.onInput?.(value);
197
+ root.dispatchEvent(new CustomEvent('mk:input', { bubbles: true, detail: { value } }));
198
+ }
199
+ if (notify === 'change') commit();
200
+ };
201
+ const commit = () => {
202
+ if (value === committed) return;
203
+ committed = value;
204
+ options.onChange?.(value);
205
+ root.dispatchEvent(new CustomEvent('mk:change', { bubbles: true, detail: { value } }));
206
+ };
207
+
208
+ scope.on(range, 'input', () => {
209
+ const next = Number(range.value);
210
+ const before = value;
211
+ set(next, !held, false);
212
+ if (value !== before) {
213
+ options.onInput?.(value);
214
+ root.dispatchEvent(new CustomEvent('mk:input', { bubbles: true, detail: { value } }));
215
+ }
216
+ });
217
+ scope.on(range, 'change', commit);
218
+ scope.on(range, 'dblclick', () => set(origin, true, 'change'));
219
+
220
+ // Held: the thumb grows and the bubble shows; past an end, the track stretches like rubber.
221
+ let pointer: number | null = null;
222
+ scope.on(range, 'pointerdown', (event) => {
223
+ const down = event as PointerEvent;
224
+ if (range.disabled || down.button !== 0) return;
225
+ held = true;
226
+ pointer = down.pointerId;
227
+ root.setAttribute('data-mk-held', '');
228
+ });
229
+ scope.on(window, 'pointermove', (event) => {
230
+ const move = event as PointerEvent;
231
+ if (!held || move.pointerId !== pointer || reduced()) return;
232
+ const box = track.getBoundingClientRect();
233
+ const over = move.clientX > box.right ? move.clientX - box.right : move.clientX < box.left ? move.clientX - box.left : 0;
234
+ const stretch = Math.min(1, Math.abs(over) / 80) * 0.04;
235
+ gsap.set(track, { scaleX: 1 + stretch, transformOrigin: over > 0 ? '0% 50%' : '100% 50%' });
236
+ });
237
+ const release = (event: Event) => {
238
+ if (!held || (event as PointerEvent).pointerId !== pointer) return;
239
+ held = false;
240
+ pointer = null;
241
+ root.removeAttribute('data-mk-held');
242
+ if (!reduced()) keep(gsap.to(track, { scaleX: 1, duration: 0.6, ease: 'elastic.out(1, 0.35)', clearProps: 'transform,transformOrigin' }));
243
+ };
244
+ scope.on(window, 'pointerup', release);
245
+ scope.on(window, 'pointercancel', release);
246
+ scope.add(() => {
247
+ root.removeAttribute('data-mk-held');
248
+ gsap.set(track, { clearProps: 'transform,transformOrigin' });
249
+ });
250
+
251
+ // − and +: a step per press, repeating while held.
252
+ let repeat: ReturnType<typeof setTimeout> | undefined;
253
+ const stop = () => {
254
+ clearTimeout(repeat);
255
+ if (repeat !== undefined) commit();
256
+ repeat = undefined;
257
+ };
258
+ scope.add(() => clearTimeout(repeat));
259
+ for (const [button, direction] of [[less, -1], [more, 1]] as const) {
260
+ if (!button) continue;
261
+ scope.on(button, 'pointerdown', (event) => {
262
+ if (range.disabled || (event as PointerEvent).button !== 0) return;
263
+ event.preventDefault();
264
+ press(button);
265
+ const tick = (delay: number) => {
266
+ set(value + step * direction, true, 'input');
267
+ repeat = setTimeout(() => tick(Math.max(50, delay * 0.8)), delay);
268
+ };
269
+ tick(400);
270
+ });
271
+ scope.on(button, 'pointerup', stop);
272
+ scope.on(button, 'pointerleave', stop);
273
+ scope.on(button, 'pointercancel', stop);
274
+ }
275
+ const form = range.form;
276
+ if (form) scope.on(form, 'reset', () => setTimeout(() => set(Number(range.defaultValue), true, false)));
277
+
278
+ describe();
279
+ paint();
280
+
281
+ return {
282
+ setValue: (next, notify = false) => set(next, true, notify ? 'change' : false),
283
+ reset: (notify = false) => set(origin, true, notify ? 'change' : false),
284
+ get value() {
285
+ return value;
286
+ },
287
+ destroy: () => scope.dispose(),
288
+ };
289
+ }
@@ -0,0 +1,45 @@
1
+ 'use client';
2
+ import { useRef, type HTMLAttributes, type MutableRefObject } from 'react';
3
+ import { useIsomorphicLayoutEffect, useLatest } from '../../react/use-recipe';
4
+ import { createBidirectionalSlider, type BidirectionalSliderInstance, type BidirectionalSliderOptions } from './engine';
5
+
6
+ export interface BidirectionalSliderProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onChange' | 'onInput'>, BidirectionalSliderOptions {
7
+ instanceRef?: MutableRefObject<BidirectionalSliderInstance | null>;
8
+ }
9
+
10
+ /** <BidirectionalSlider label="Volumen" icons="volume" value={volume} onChange={setVolume} /> */
11
+ export function BidirectionalSlider({ min, max, origin, step, value, disabled, name, label, icons, formatValue, onInput, onChange, instanceRef, className, ...props }: BidirectionalSliderProps) {
12
+ const ref = useRef<HTMLDivElement>(null);
13
+ const input = useLatest(onInput);
14
+ const change = useLatest(onChange);
15
+ const format = useLatest(formatValue);
16
+ const instance = useRef<BidirectionalSliderInstance | null>(null);
17
+ useIsomorphicLayoutEffect(() => {
18
+ const created = createBidirectionalSlider(ref.current, {
19
+ min,
20
+ max,
21
+ origin,
22
+ step,
23
+ value,
24
+ disabled,
25
+ name,
26
+ label,
27
+ icons,
28
+ formatValue: formatValue ? (n) => format.current!(n) : undefined,
29
+ onInput: (n) => input.current?.(n),
30
+ onChange: (n) => change.current?.(n),
31
+ });
32
+ instance.current = created;
33
+ if (instanceRef) instanceRef.current = created;
34
+ return () => {
35
+ created.destroy();
36
+ instance.current = null;
37
+ if (instanceRef) instanceRef.current = null;
38
+ };
39
+ // eslint-disable-next-line react-hooks/exhaustive-deps
40
+ }, [min, max, origin, step, disabled, name, label, icons]);
41
+ useIsomorphicLayoutEffect(() => {
42
+ if (value !== undefined && instance.current && instance.current.value !== value) instance.current.setValue(value);
43
+ }, [value]);
44
+ return <div {...props} ref={ref} className={['mk-bidirectional-slider', className].filter(Boolean).join(' ')} />;
45
+ }
@@ -1,24 +1,49 @@
1
- # chart
2
-
3
- Gráfica SVG reutilizable de línea, área y barras (agrupadas o apiladas) sin motor canvas ni dependencias. Sigue las gráficas de métricas de Melon Mind: base en cero, línea de 2 px, área que se desvanece de .25 a 0, barras de hasta 24 px con extremo redondeado de 4 px y huecos de 2 px, rejilla fina y tooltip con superficie invertida.
4
-
5
- ```html
6
- <mk-chart class="mk-chart" type="bar" stacked label="Sesiones por semana"
7
- labels="S1,S2,S3,S4"
8
- series='[{"name":"Ana","values":[4,6,5,7]},{"name":"Luis","values":[2,3,4,1]}]'></mk-chart>
9
- ```
10
-
11
- ```ts
12
- const chart = createChart(root, { type: 'area', labels: days, series: [{ name: 'Ingresos', values }], formatValue: money, label: 'Ingresos de octubre' });
13
- chart.update({ series: [{ name: 'Ingresos', values: next }] });
14
- ```
15
-
16
- React: `<Chart type="line" labels={days} series={series} formatValue={money} label="Ingresos" onSelect={setDay} />`.
17
-
18
- Datos: `labels` + `series: ChartSeries[]` (`{name, values: (number | null)[], color?, dash?}`; `dash` es un patrón SVG para líneas, p. ej. `'5 5'` para un ritmo o `'1 6'` para una proyección) o `points: ChartPoint[]` (`{label, value}`) para una serie. `null` es un hueco sin dato: la línea se corta y un punto aislado se muestra con marcador. Opciones: `type='line'|'area'|'bar'`, `stacked` (barras), `formatValue(n)` (ejes, tooltip y tabla), `label` (nombre accesible y título de la tabla), `height=240`, `emptyLabel`, `legend` (por defecto con dos o más series), `axis` (valores y líneas guía en el eje y; por defecto sólo la línea base), `endLabel` (líneas: el último valor de la primera serie en una píldora al final; por defecto `true`), `tooltip` (por defecto `true`; con `false` la gráfica deja la guía y los marcadores y la tarjeta la pone `chart-tooltip`), `onSelect(index | null)`. API: `update(options)` (combina y redibuja), `select(index | null)`, `selected`, `destroy()`. Evento `mk:select` con `{index, label, values, x, y, series}`: `x`/`y` son el punto señalado en píxeles del área de la gráfica y `series` trae `{name, color, value, previous}` por serie.
19
-
20
- Puntero y toque eligen el punto más cercano (el toque mantiene la selección; el ratón la quita al salir). Con foco en la gráfica, flechas/Inicio/Fin recorren los valores y Escape los oculta; un estado educado anuncia el valor elegido. La gráfica tiene un resumen como nombre accesible y una tabla para lectores de pantalla con todos los valores (y el total en barras apiladas). Sin datos muestra `emptyLabel`.
21
-
22
- Movimiento: el primer dibujo traza la línea de izquierda a derecha (barras: crecen desde la base) y cada `update` desliza los valores nuevos desde los anteriores; el tooltip aparece con un pop de cristal y se desliza entre puntos. Con movimiento reducido todo cambia sin transición.
23
-
24
- Colores: una sola serie es monocroma (`--mk-chart-mono`, el color del texto, al estilo de los indicadores de Melon Mind); con varias, cada serie usa su `color` o la ranura `--mk-chart-N` en orden fijo (paleta comprobada para daltonismo, con pasos propios en oscuro); a partir de la novena serie todas comparten `--mk-chart-other`: agrúpalas en «Otros». Los textos usan los colores de texto del tema; el color identifica la marca. Se redimensiona con su contenedor (ResizeObserver) y `destroy()` quita todo lo creado. Variables: `--mk-chart-mono`, `--mk-chart-1…8`, `--mk-chart-other`, `--mk-chart-surface` (anillo de marcadores), `--mk-chart-grid`.
1
+ # chart
2
+
3
+ Desde 0.15 también puedes mezclar series con `type="composed"`. Cada serie declara `type: 'line' | 'bar' | 'area'`. Un área acepta `lowerValues` para rellenar el intervalo entre la frontera inferior y `values`; los huecos `null` interrumpen el relleno. La tabla y el tooltip muestran ambos límites.
4
+
5
+ ```tsx
6
+ <Chart type="composed" labels={['Lun', 'Mar', 'Mié']} series={[
7
+ { name: 'Ventas', type: 'bar', values: [20, 32, 28] },
8
+ { name: 'Meta', type: 'line', values: [25, 30, 35] },
9
+ { name: 'Rango', type: 'area', values: [24, 36, 32], lowerValues: [16, 26, 22] },
10
+ ]} axis endLabel={false} />
11
+ ```
12
+
13
+ Gráfica SVG reutilizable de línea, área y barras (agrupadas o apiladas) sin motor canvas ni dependencias. Sigue las gráficas de métricas de Melon Mind: base en cero, línea de 2 px, área que se desvanece de .25 a 0, barras de hasta 24 px con extremo redondeado de 4 px y huecos de 2 px, rejilla fina y tooltip con superficie invertida.
14
+
15
+ ```html
16
+ <mk-chart class="mk-chart" type="bar" stacked label="Sesiones por semana"
17
+ labels="S1,S2,S3,S4"
18
+ series='[{"name":"Ana","values":[4,6,5,7]},{"name":"Luis","values":[2,3,4,1]}]'></mk-chart>
19
+ ```
20
+
21
+ ```ts
22
+ const chart = createChart(root, { type: 'area', labels: days, series: [{ name: 'Ingresos', values }], formatValue: money, label: 'Ingresos de octubre' });
23
+ chart.update({ series: [{ name: 'Ingresos', values: next }] });
24
+ ```
25
+
26
+ React: `<Chart type="line" labels={days} series={series} formatValue={money} label="Ingresos" onSelect={setDay} />`.
27
+
28
+ Datos: `labels` + `series: ChartSeries[]` (`{name, values: (number | null)[], color?, dash?}`; `dash` es un patrón SVG para líneas, p. ej. `'5 5'` para un ritmo o `'1 6'` para una proyección) o `points: ChartPoint[]` (`{label, value}`) para una serie. `null` es un hueco sin dato: la línea se corta y un punto aislado se muestra con marcador. Opciones: `type='line'|'area'|'bar'`, `stacked` (barras), `formatValue(n)` (ejes, tooltip y tabla), `label` (nombre accesible y título de la tabla), `height=240`, `emptyLabel`, `legend` (por defecto con dos o más series), `axis` (valores y líneas guía en el eje y; por defecto sólo la línea base), `endLabel` (líneas: el último valor de la primera serie en una píldora al final; por defecto `true`), `tooltip` (por defecto `true`; con `false` la gráfica deja la guía y los marcadores y la tarjeta la pone `chart-tooltip`), `onSelect(index | null)`. API: `update(options)` (combina y redibuja), `select(index | null)`, `selected`, `destroy()`. Evento `mk:select` con `{index, label, values, x, y, series}`: `x`/`y` son el punto señalado en píxeles del área de la gráfica y `series` trae `{name, color, value, previous}` por serie.
29
+
30
+ Puntero y toque eligen el punto más cercano (el toque mantiene la selección; el ratón la quita al salir). Con foco en la gráfica, flechas/Inicio/Fin recorren los valores y Escape los oculta; un estado educado anuncia el valor elegido. La gráfica tiene un resumen como nombre accesible y una tabla para lectores de pantalla con todos los valores (y el total en barras apiladas). Sin datos muestra `emptyLabel`.
31
+
32
+ Movimiento: el primer dibujo traza la línea de izquierda a derecha (barras: crecen desde la base) y cada `update` desliza los valores nuevos desde los anteriores; el tooltip aparece con un pop de cristal y se desliza entre puntos. Con movimiento reducido todo cambia sin transición.
33
+
34
+ Colores: una sola serie es monocroma (`--mk-chart-mono`, el color del texto, al estilo de los indicadores de Melon Mind); con varias, cada serie usa su `color` o la ranura `--mk-chart-N` en orden fijo (paleta comprobada para daltonismo, con pasos propios en oscuro); a partir de la novena serie todas comparten `--mk-chart-other`: agrúpalas en «Otros». Los textos usan los colores de texto del tema; el color identifica la marca. Se redimensiona con su contenedor (ResizeObserver) y `destroy()` quita todo lo creado. Variables: `--mk-chart-mono`, `--mk-chart-1…8`, `--mk-chart-other`, `--mk-chart-surface` (anillo de marcadores), `--mk-chart-grid`.
35
+
36
+ ## Variantes visuales · 0.16
37
+
38
+ ```tsx
39
+ <Chart type="bar" barStyle="pill" barTrack barFill="spotlight" barWidth={40}
40
+ labels={['Lun', 'Mar', 'Mié']} series={[{ name: 'Enfoque', values: [32, 56, 44] }]} />
41
+ ```
42
+
43
+ - `barStyle`: `rounded` (compatibilidad) o `pill` (ambos extremos redondos). Las pilas comparten un recorte exterior por columna.
44
+ - `barTrack`: fondo de capacidad, false por defecto.
45
+ - `barFill`: `solid`, `gradient` o `spotlight` (selección con degradado sobre columnas rayadas). Sin selección, spotlight destaca la última columna.
46
+ - `barWidth`: ancho máximo, 24 px por defecto; se adapta al espacio disponible.
47
+ - `curve`: `linear` o `smooth`. La interpolación suave conserva los extremos reales y también funciona en áreas por rangos.
48
+
49
+ La selección de una píldora muestra un valor flotante. Pointer, tacto y teclado usan la misma selección. La entrada se escalona por columnas y los cambios de datos continúan desde los valores dibujados, también al interrumpir una transición.
@@ -15,6 +15,7 @@ mk-chart {
15
15
  --mk-chart-other: var(--mk-muted);
16
16
  --mk-chart-surface: var(--mk-surface-1);
17
17
  --mk-chart-grid: color-mix(in srgb, var(--mk-text) 14%, transparent);
18
+ --mk-chart-track: color-mix(in srgb, var(--mk-text) 5%, transparent);
18
19
  position: relative;
19
20
  display: grid;
20
21
  gap: 0.75rem;
@@ -120,6 +121,12 @@ mk-chart {
120
121
  fill: color-mix(in srgb, var(--mk-text) 6%, transparent);
121
122
  }
122
123
 
124
+ :is(.mk-chart, mk-chart) [data-mk-part='bar-track'] { fill: var(--mk-chart-track); }
125
+ :is(.mk-chart, mk-chart) [data-mk-part='bar'] { transition: opacity .22s ease, filter .22s ease; }
126
+ :is(.mk-chart, mk-chart) [data-mk-part='bar'][data-mk-active] { filter: brightness(1.12); }
127
+ :is(.mk-chart, mk-chart) [data-mk-part='bar-value'] rect { fill: var(--mk-text); }
128
+ :is(.mk-chart, mk-chart) [data-mk-part='bar-value'] text { fill: var(--mk-bg); font-size: 11px; font-weight: 600; }
129
+
123
130
  :is(.mk-chart, mk-chart) [data-mk-part='series'] path[data-mk-index] {
124
131
  transition: opacity 0.2s ease;
125
132
  }
@@ -13,11 +13,14 @@ export class MkChartElement extends MkElement<ChartInstance> {
13
13
  series: 'json',
14
14
  points: 'json',
15
15
  stacked: 'boolean',
16
+ barStyle: 'string', barTrack: 'boolean', barFill: 'string', barWidth: 'number', curve: 'string',
16
17
  label: 'string',
17
18
  height: 'number',
18
19
  emptyLabel: 'string',
19
20
  legend: 'boolean',
20
21
  tooltip: 'boolean',
22
+ axis: 'boolean',
23
+ endLabel: 'boolean',
21
24
  };
22
25
 
23
26
  protected create(options: Record<string, unknown>) {
@@ -26,7 +29,7 @@ export class MkChartElement extends MkElement<ChartInstance> {
26
29
 
27
30
  attributeChangedCallback(_name: string, previous: string | null, next: string | null) {
28
31
  if (previous === next || !this.instance) return;
29
- this.instance.update({ stacked: false, legend: undefined, ...(this.options as ChartOptions) });
32
+ this.instance.update({ stacked: false, barTrack: false, barStyle: undefined, barFill: undefined, barWidth: undefined, curve: undefined, legend: undefined, ...(this.options as ChartOptions) });
30
33
  }
31
34
 
32
35
  update(options: ChartOptions) {