@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.
Files changed (154) hide show
  1. package/README.md +174 -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-6IRJYXJU.js} +2 -2
  7. package/dist/{chunk-CU6SXW3E.js → chunk-A6QQCF6V.js} +1309 -310
  8. package/dist/chunk-A6QQCF6V.js.map +1 -0
  9. package/dist/chunk-JXRS3OEA.js +3638 -0
  10. package/dist/chunk-JXRS3OEA.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/data-table.css +121 -26
  14. package/dist/css/components/filter-builder.css +10 -15
  15. package/dist/css/components/heatmap-chart.css +391 -0
  16. package/dist/css/components/hover-card.css +8 -2
  17. package/dist/css/components/image-preview.css +90 -2
  18. package/dist/css/components/json-viewer.css +33 -1
  19. package/dist/css/components/nav.css +102 -102
  20. package/dist/css/components/orbit-menu.css +130 -0
  21. package/dist/css/components/pagination.css +10 -5
  22. package/dist/css/components/pie-chart.css +390 -0
  23. package/dist/css/components/pill-button.css +135 -0
  24. package/dist/css/components/radar-chart.css +370 -0
  25. package/dist/css/components/radial-chart.css +398 -0
  26. package/dist/css/components/sankey-chart.css +358 -0
  27. package/dist/css/components/scroll-island.css +111 -0
  28. package/dist/css/components/select.css +136 -0
  29. package/dist/css/components/toolbar.css +122 -0
  30. package/dist/css/components/tree-view.css +79 -0
  31. package/dist/css/components/voice-dots.css +70 -0
  32. package/dist/elements/index.d.ts +180 -7
  33. package/dist/elements/index.js +287 -6
  34. package/dist/elements/index.js.map +1 -1
  35. package/dist/{engine-Cn6IhE3z.d.ts → engine-5zRIssTO.d.ts} +11 -1
  36. package/dist/index.css +1832 -168
  37. package/dist/index.d.ts +4 -4
  38. package/dist/index.js +33 -3
  39. package/dist/index.js.map +1 -1
  40. package/dist/react/index.d.ts +117 -11
  41. package/dist/react/index.js +314 -13
  42. package/dist/react/index.js.map +1 -1
  43. package/dist/{reduced-BBUusld6.d.ts → reduced-6X_bns60.d.ts} +1 -1
  44. package/dist/{reveal-DEvp6U6T.d.ts → reveal-BfyKKsw4.d.ts} +97 -4
  45. package/dist/store-CA2Qcun_.d.ts +453 -0
  46. package/package.json +9 -16
  47. package/registry.json +1096 -948
  48. package/src/auto/auto-init.ts +6 -0
  49. package/src/components/add-to-cart/README.md +34 -0
  50. package/src/components/add-to-cart/add-to-cart.css +71 -0
  51. package/src/components/add-to-cart/element.ts +20 -0
  52. package/src/components/add-to-cart/engine.ts +206 -0
  53. package/src/components/add-to-cart/react.tsx +29 -0
  54. package/src/components/bidirectional-slider/README.md +46 -0
  55. package/src/components/bidirectional-slider/bidirectional-slider.css +167 -0
  56. package/src/components/bidirectional-slider/element.ts +42 -0
  57. package/src/components/bidirectional-slider/engine.ts +289 -0
  58. package/src/components/bidirectional-slider/react.tsx +45 -0
  59. package/src/components/chart/README.md +10 -0
  60. package/src/components/chart/element.ts +2 -0
  61. package/src/components/chart/engine.ts +50 -23
  62. package/src/components/chart/parts.ts +237 -0
  63. package/src/components/chart/react.tsx +2 -2
  64. package/src/components/chart/use-chart.ts +55 -0
  65. package/src/components/data-table/README.md +3 -3
  66. package/src/components/data-table/data-table.css +121 -26
  67. package/src/components/data-table/engine.ts +81 -29
  68. package/src/components/editable-table/engine.ts +67 -11
  69. package/src/components/filter-builder/engine.ts +33 -2
  70. package/src/components/filter-builder/filter-builder.css +10 -15
  71. package/src/components/heatmap-chart/README.md +48 -0
  72. package/src/components/heatmap-chart/element.ts +21 -0
  73. package/src/components/heatmap-chart/engine.ts +281 -0
  74. package/src/components/heatmap-chart/heatmap-chart.css +93 -0
  75. package/src/components/heatmap-chart/react.tsx +14 -0
  76. package/src/components/hover-card/engine.ts +30 -1
  77. package/src/components/hover-card/hover-card.css +8 -2
  78. package/src/components/image-preview/element.ts +1 -1
  79. package/src/components/image-preview/engine.ts +95 -15
  80. package/src/components/image-preview/image-preview.css +90 -2
  81. package/src/components/image-preview/react.tsx +3 -3
  82. package/src/components/json-viewer/engine.ts +11 -2
  83. package/src/components/json-viewer/json-viewer.css +33 -1
  84. package/src/components/nav/README.md +12 -10
  85. package/src/components/nav/element.ts +5 -5
  86. package/src/components/nav/engine.ts +21 -14
  87. package/src/components/nav/nav.css +102 -102
  88. package/src/components/nav/react.tsx +4 -4
  89. package/src/components/orbit-menu/README.md +46 -0
  90. package/src/components/orbit-menu/element.ts +27 -0
  91. package/src/components/orbit-menu/engine.ts +179 -0
  92. package/src/components/orbit-menu/orbit-menu.css +129 -0
  93. package/src/components/orbit-menu/react.tsx +44 -0
  94. package/src/components/pagination/README.md +1 -1
  95. package/src/components/pagination/engine.ts +50 -4
  96. package/src/components/pagination/pagination.css +10 -5
  97. package/src/components/pie-chart/README.md +48 -0
  98. package/src/components/pie-chart/element.ts +30 -0
  99. package/src/components/pie-chart/engine.ts +283 -0
  100. package/src/components/pie-chart/pie-chart.css +92 -0
  101. package/src/components/pie-chart/react.tsx +14 -0
  102. package/src/components/pill-button/README.md +46 -0
  103. package/src/components/pill-button/element.ts +16 -0
  104. package/src/components/pill-button/engine.ts +77 -0
  105. package/src/components/pill-button/pill-button.css +134 -0
  106. package/src/components/pill-button/react.tsx +32 -0
  107. package/src/components/radar-chart/README.md +48 -0
  108. package/src/components/radar-chart/element.ts +30 -0
  109. package/src/components/radar-chart/engine.ts +327 -0
  110. package/src/components/radar-chart/radar-chart.css +72 -0
  111. package/src/components/radar-chart/react.tsx +14 -0
  112. package/src/components/radial-chart/README.md +48 -0
  113. package/src/components/radial-chart/element.ts +30 -0
  114. package/src/components/radial-chart/engine.ts +290 -0
  115. package/src/components/radial-chart/radial-chart.css +100 -0
  116. package/src/components/radial-chart/react.tsx +14 -0
  117. package/src/components/sankey-chart/README.md +48 -0
  118. package/src/components/sankey-chart/element.ts +31 -0
  119. package/src/components/sankey-chart/engine.ts +401 -0
  120. package/src/components/sankey-chart/react.tsx +14 -0
  121. package/src/components/sankey-chart/sankey-chart.css +60 -0
  122. package/src/components/scroll-island/README.md +34 -0
  123. package/src/components/scroll-island/element.ts +16 -0
  124. package/src/components/scroll-island/engine.ts +252 -0
  125. package/src/components/scroll-island/react.tsx +26 -0
  126. package/src/components/scroll-island/scroll-island.css +110 -0
  127. package/src/components/select/README.md +34 -0
  128. package/src/components/select/element.ts +26 -0
  129. package/src/components/select/engine.ts +267 -0
  130. package/src/components/select/react.tsx +33 -0
  131. package/src/components/select/select.css +135 -0
  132. package/src/components/toolbar/README.md +46 -0
  133. package/src/components/toolbar/element.ts +30 -0
  134. package/src/components/toolbar/engine.ts +230 -0
  135. package/src/components/toolbar/react.tsx +34 -0
  136. package/src/components/toolbar/toolbar.css +121 -0
  137. package/src/components/tree-view/element.ts +1 -1
  138. package/src/components/tree-view/engine.ts +174 -0
  139. package/src/components/tree-view/react.tsx +7 -3
  140. package/src/components/tree-view/tree-view.css +79 -0
  141. package/src/components/voice-dots/README.md +48 -0
  142. package/src/components/voice-dots/element.ts +45 -0
  143. package/src/components/voice-dots/engine.ts +243 -0
  144. package/src/components/voice-dots/react.tsx +52 -0
  145. package/src/components/voice-dots/voice-dots.css +69 -0
  146. package/src/elements/index.ts +39 -0
  147. package/src/index.ts +16 -0
  148. package/src/motion/liquid.ts +14 -4
  149. package/src/react/index.ts +14 -1
  150. package/dist/chunk-6P4P7NU6.js +0 -1401
  151. package/dist/chunk-6P4P7NU6.js.map +0 -1
  152. package/dist/chunk-CU6SXW3E.js.map +0 -1
  153. package/dist/store-DjkTBX7f.d.ts +0 -195
  154. /package/dist/{chunk-EY4BJKC4.js.map → chunk-6IRJYXJU.js.map} +0 -0
@@ -0,0 +1,30 @@
1
+ import { MkElement, defineElement, type OptionType } from '../../elements/base';
2
+ import { createPieChart, type PieChartInstance, type PieChartOptions } from './engine';
3
+
4
+ /**
5
+ * <mk-pie-chart class="mk-chart mk-pie-chart" label="Ventas por canal"
6
+ * data='[{"label":"Tienda","value":420},{"label":"En línea","value":310}]'></mk-pie-chart>
7
+ * donut="0" draws a pie. Attribute changes glide in place. Emits mk:select ({index, label, value}).
8
+ */
9
+ export class MkPieChartElement extends MkElement<PieChartInstance> {
10
+ static options: Record<string, OptionType> = { data: 'json', donut: 'number', gap: 'number', size: 'number', label: 'string', totalLabel: 'string', legend: 'boolean', emptyLabel: 'string' };
11
+
12
+ protected create(options: Record<string, unknown>) {
13
+ return createPieChart(this, options as PieChartOptions);
14
+ }
15
+
16
+ attributeChangedCallback(_name: string, previous: string | null, next: string | null) {
17
+ if (previous === next || !this.instance) return;
18
+ this.instance.update({ legend: undefined, ...(this.options as PieChartOptions) });
19
+ }
20
+
21
+ update(options: PieChartOptions) {
22
+ this.instance?.update(options);
23
+ }
24
+
25
+ select(index: number | null) {
26
+ this.instance?.select(index);
27
+ }
28
+ }
29
+
30
+ export const definePieChart = () => defineElement('mk-pie-chart', MkPieChartElement);
@@ -0,0 +1,283 @@
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 { blurIn, rollDigits } from '../../motion/micro';
6
+ import { arcPath, chartFrame, finite, formatNumber, onWidth, polar, round, slotColor, svg } from '../chart/parts';
7
+
8
+ /*
9
+ * Pie chart: parts of a whole as a donut (default) or a pie, in chart's family
10
+ * (colour slots, inverse-glass tooltip, outlined legend, a table for screen
11
+ * readers). Slices sweep in clockwise from 12 o'clock on the swatch curve
12
+ * (.38,.49,0,1.5) while the total in the middle rolls up; new data glides each
13
+ * slice from its old angles. Pointing at a slice (or its legend pill, or the
14
+ * arrows) pulls it out along its middle and dims the others; the middle of a
15
+ * donut then says the slice, its value and share, else the tooltip does.
16
+ */
17
+
18
+ export interface PieSlice {
19
+ label: string;
20
+ value: number;
21
+ color?: string;
22
+ }
23
+
24
+ export interface PieChartOptions {
25
+ data?: PieSlice[];
26
+ /** Hole size as a share of the radius: 0 is a pie (default 0.62). */
27
+ donut?: number;
28
+ /** Degrees of surface between slices (default 1.5). */
29
+ gap?: number;
30
+ /** Diameter in px (default 240; it shrinks with its container). */
31
+ size?: number;
32
+ /** Accessible name (and table caption). */
33
+ label?: string;
34
+ /** The donut's middle when nothing is chosen (default 'Total'). */
35
+ totalLabel?: string;
36
+ legend?: boolean;
37
+ formatValue?: (value: number) => string;
38
+ emptyLabel?: string;
39
+ onSelect?: (index: number | null) => void;
40
+ }
41
+
42
+ export interface PieChartInstance extends Instance {
43
+ update(options: PieChartOptions): void;
44
+ select(index: number | null): void;
45
+ readonly selected: number | null;
46
+ }
47
+
48
+ const PULL = 7;
49
+
50
+ export function createPieChart(root: HTMLElement | null, initial: PieChartOptions = {}): PieChartInstance {
51
+ if (!root) return inert({ update() {}, select() {}, selected: null });
52
+ registerMotion();
53
+ const scope = createScope();
54
+ const frame = chartFrame(root, scope, 'pie');
55
+ const { plot, canvas, keep } = frame;
56
+ let options: PieChartOptions = { ...initial };
57
+ let selected: number | null = null;
58
+
59
+ const center = document.createElement('div');
60
+ center.dataset.mkPieCenter = '';
61
+ center.setAttribute('aria-hidden', 'true');
62
+ const centerValue = document.createElement('strong');
63
+ const centerLabel = document.createElement('span');
64
+ center.append(centerValue, centerLabel);
65
+ plot.append(center);
66
+ scope.add(() => center.remove());
67
+
68
+ const format = (value: number) => (options.formatValue ? options.formatValue(value) : formatNumber(value));
69
+ const slices = () =>
70
+ (options.data ?? [])
71
+ .filter((slice) => finite(slice.value) && slice.value > 0)
72
+ .map((slice, i, all) => ({ label: String(slice.label), value: slice.value, color: slice.color ?? slotColor(i, Math.max(2, all.length)) }));
73
+ let model = slices();
74
+ const total = () => model.reduce((sum, slice) => sum + slice.value, 0);
75
+ const share = (value: number) => new Intl.NumberFormat(undefined, { style: 'percent', maximumFractionDigits: 1 }).format(value / (total() || 1));
76
+ const donut = () => Math.min(0.9, Math.max(0, finite(options.donut) ? options.donut : 0.62));
77
+
78
+ /** Each slice's angles for `values` (fractions of a turn, with the gap taken from both sides). */
79
+ const anglesOf = (values: number[]) => {
80
+ const sum = values.reduce((a, b) => a + b, 0) || 1;
81
+ const pad = values.filter((v) => v > 0).length > 1 ? ((finite(options.gap) ? options.gap : 1.5) * Math.PI) / 180 : 0;
82
+ let at = 0;
83
+ return values.map((value) => {
84
+ const sweep = (value / sum) * Math.PI * 2;
85
+ const a0 = at + Math.min(pad / 2, sweep / 4);
86
+ const a1 = at + sweep - Math.min(pad / 2, sweep / 4);
87
+ at += sweep;
88
+ return [a0, Math.max(a0, a1)] as [number, number];
89
+ });
90
+ };
91
+
92
+ let paths: SVGPathElement[] = [];
93
+ let shown: Array<[number, number]> = [];
94
+ let geometry = { size: 240, cx: 120, cy: 120, outer: 112, inner: 70 };
95
+ let glide: gsap.core.Tween | null = null;
96
+ scope.add(() => glide?.kill());
97
+
98
+ const measure = () => {
99
+ const width = Math.round(plot.clientWidth || root.clientWidth || 0);
100
+ const wanted = finite(options.size) ? options.size : 240;
101
+ const size = Math.max(120, width ? Math.min(wanted, width) : wanted);
102
+ const outer = size / 2 - PULL - 2;
103
+ geometry = { size, cx: size / 2, cy: size / 2, outer, inner: outer * donut() };
104
+ canvas.setAttribute('viewBox', `0 0 ${size} ${size}`);
105
+ canvas.setAttribute('width', String(size));
106
+ canvas.setAttribute('height', String(size));
107
+ plot.style.setProperty('--mk-pie-size', `${size}px`);
108
+ };
109
+ scope.add(() => plot.style.removeProperty('--mk-pie-size'));
110
+
111
+ const paint = (angles: Array<[number, number]>) => {
112
+ shown = angles;
113
+ const { cx, cy, outer, inner } = geometry;
114
+ paths.forEach((path, i) => {
115
+ const [a0, a1] = angles[i] ?? [0, 0];
116
+ path.setAttribute('d', a1 - a0 > 1e-4 ? arcPath(cx, cy, outer, inner, a0, a1) : '');
117
+ });
118
+ };
119
+
120
+ const build = () => {
121
+ canvas.replaceChildren();
122
+ const group = svg('g', { 'data-mk-part': 'series' });
123
+ paths = model.map((slice, i) => {
124
+ const path = svg('path', { 'data-mk-index': i });
125
+ path.style.fill = slice.color;
126
+ group.append(path);
127
+ return path;
128
+ });
129
+ canvas.append(group);
130
+ };
131
+
132
+ const paintCenter = (animate: boolean) => {
133
+ center.hidden = donut() < 0.35 || !model.length;
134
+ if (center.hidden) return;
135
+ const slice = selected === null ? null : model[selected];
136
+ const text = slice ? format(slice.value) : format(total());
137
+ const caption = slice ? `${slice.label} · ${share(slice.value)}` : (options.totalLabel ?? 'Total');
138
+ const changed = centerLabel.textContent !== caption;
139
+ if (centerValue.textContent !== text) {
140
+ if (animate && !reduced() && centerValue.textContent) keep(rollDigits(centerValue, text, 1, 0.35));
141
+ else centerValue.textContent = text;
142
+ }
143
+ centerLabel.textContent = caption;
144
+ if (changed && animate && !reduced()) keep(blurIn(centerLabel, { from: 'soft', duration: 0.3 }));
145
+ };
146
+
147
+ const paintSelection = (announce: boolean) => {
148
+ const { outer } = geometry;
149
+ paths.forEach((path, i) => {
150
+ const chosen = i === selected;
151
+ path.toggleAttribute('data-mk-dim', selected !== null && !chosen);
152
+ if (!chosen) {
153
+ path.style.transform = '';
154
+ return;
155
+ }
156
+ const [a0, a1] = shown[i] ?? [0, 0];
157
+ const [dx, dy] = polar(0, 0, Math.min(PULL, outer * 0.06), (a0 + a1) / 2);
158
+ path.style.transform = `translate(${round(dx)}px, ${round(dy)}px)`;
159
+ });
160
+ legendItems.forEach((item, i) => item.toggleAttribute('data-mk-dim', selected !== null && i !== selected));
161
+ paintCenter(true);
162
+ const slice = selected === null ? null : model[selected];
163
+ // The tooltip speaks for a pie (a donut's middle already does).
164
+ if (slice && center.hidden) {
165
+ const [a0, a1] = shown[selected!] ?? [0, 0];
166
+ const [x, y] = polar(geometry.cx, geometry.cy, geometry.outer * 0.7, (a0 + a1) / 2);
167
+ frame.tip({ x, y, title: slice.label, rows: [{ name: share(slice.value), color: slice.color, value: format(slice.value) }] });
168
+ } else frame.tip(null);
169
+ if (announce && slice) frame.live.textContent = `${slice.label}: ${format(slice.value)}, ${share(slice.value)}`;
170
+ };
171
+
172
+ let legendItems: HTMLLIElement[] = [];
173
+ const draw = (animate: boolean) => {
174
+ glide?.kill();
175
+ const before = shown;
176
+ const sameCount = paths.length === model.length;
177
+ const name = options.label ?? 'Gráfica circular';
178
+ canvas.setAttribute('aria-label', model.length ? `${name}. ${model.length} partes, total ${format(total())}. Usa las flechas para recorrerlas.` : `${name}: ${options.emptyLabel ?? 'sin datos'}`);
179
+ frame.fillTable(name, ['Parte', 'Valor', 'Porcentaje'], model.map((slice) => [slice.label, format(slice.value), share(slice.value)]));
180
+ legendItems = frame.fillLegend(model.map((slice) => ({ name: slice.label, color: slice.color })), options.legend ?? true);
181
+ legendItems.forEach((item, i) => {
182
+ item.addEventListener('pointerenter', () => select(i));
183
+ item.addEventListener('pointerleave', () => select(null));
184
+ });
185
+ frame.empty.hidden = model.length > 0;
186
+ frame.empty.textContent = options.emptyLabel ?? 'Sin datos para mostrar';
187
+ plot.hidden = !model.length;
188
+ if (!model.length) {
189
+ canvas.replaceChildren();
190
+ paths = [];
191
+ shown = [];
192
+ selected = null;
193
+ return;
194
+ }
195
+ if (selected !== null && selected >= model.length) selected = null;
196
+ measure();
197
+ if (!sameCount || !before.length) build();
198
+ paths.forEach((path, i) => { path.style.fill = model[i]!.color; });
199
+ const target = anglesOf(model.map((slice) => slice.value));
200
+ const done = () => {
201
+ paint(target);
202
+ paintSelection(false);
203
+ };
204
+ if (!animate || reduced()) return done();
205
+ const state = { t: 0 };
206
+ if (sameCount && before.length) {
207
+ // New data: every slice glides from its old angles.
208
+ glide = gsap.to(state, {
209
+ t: 1,
210
+ duration: 0.6,
211
+ ease: EASE.swatch,
212
+ onUpdate: () => paint(target.map(([a0, a1], i) => [before[i]![0] + (a0 - before[i]![0]) * state.t, before[i]![1] + (a1 - before[i]![1]) * state.t])),
213
+ onComplete: done,
214
+ });
215
+ paintCenter(true);
216
+ return;
217
+ }
218
+ // First appearance: the slices sweep in clockwise from 12 o'clock and the total rolls up.
219
+ paint(target.map(() => [0, 0]));
220
+ centerValue.textContent = format(0);
221
+ glide = gsap.to(state, {
222
+ t: 1,
223
+ duration: 0.9,
224
+ ease: EASE.signature,
225
+ onUpdate: () => paint(target.map(([a0, a1]) => [a0 * state.t, a1 * state.t])),
226
+ onComplete: done,
227
+ });
228
+ paintCenter(true);
229
+ };
230
+
231
+ const select = (index: number | null, announce = false) => {
232
+ if (scope.disposed) return;
233
+ const next = index === null || !model.length ? null : ((Math.round(index) % model.length) + model.length) % model.length;
234
+ if (next === selected) return;
235
+ selected = next;
236
+ paintSelection(announce);
237
+ options.onSelect?.(selected);
238
+ const slice = selected === null ? null : model[selected];
239
+ root.dispatchEvent(new CustomEvent('mk:select', { bubbles: true, detail: { index: selected, label: slice?.label ?? null, value: slice?.value ?? null } }));
240
+ };
241
+
242
+ const indexAt = (event: PointerEvent) => {
243
+ const target = (event.target as Element).closest('[data-mk-index]');
244
+ return target ? Number(target.getAttribute('data-mk-index')) : null;
245
+ };
246
+ scope.on(canvas, 'pointermove', (event) => select(indexAt(event as PointerEvent)));
247
+ scope.on(canvas, 'pointerdown', (event) => select(indexAt(event as PointerEvent)));
248
+ scope.on(canvas, 'pointerleave', (event) => {
249
+ if ((event as PointerEvent).pointerType === 'mouse') select(null);
250
+ });
251
+ scope.on(canvas, 'focus', () => {
252
+ if (selected === null && model.length) select(0, true);
253
+ });
254
+ scope.on(canvas, 'blur', () => select(null));
255
+ scope.on(canvas, 'keydown', (event) => {
256
+ const key = (event as KeyboardEvent).key;
257
+ const moves: Record<string, number | null> = { ArrowRight: (selected ?? -1) + 1, ArrowDown: (selected ?? -1) + 1, ArrowLeft: (selected ?? 1) - 1, ArrowUp: (selected ?? 1) - 1, Home: 0, End: model.length - 1, Escape: null };
258
+ if (!(key in moves) || !model.length) return;
259
+ event.preventDefault();
260
+ select(moves[key]!, true);
261
+ });
262
+ onWidth(root, scope, () => {
263
+ measure();
264
+ paint(shown);
265
+ paintSelection(false);
266
+ });
267
+
268
+ draw(true);
269
+
270
+ return {
271
+ update(next) {
272
+ if (scope.disposed) return;
273
+ options = { ...options, ...next };
274
+ model = slices();
275
+ draw(true);
276
+ },
277
+ select: (index) => select(index),
278
+ get selected() {
279
+ return selected;
280
+ },
281
+ destroy: () => scope.dispose(),
282
+ };
283
+ }
@@ -0,0 +1,92 @@
1
+ /* pie-chart — a donut (or pie) in chart's family: it needs chart.css for the colours, tooltip, legend and table.
2
+ Slices are parted by the surface; the chosen one slides out, the others dim; the donut's middle names it. */
3
+ .mk-pie-chart,
4
+ mk-pie-chart {
5
+ display: grid;
6
+ justify-items: center;
7
+ gap: 1rem;
8
+ }
9
+
10
+ [data-mk-chart-type='pie'] [data-mk-chart-plot] {
11
+ width: 100%;
12
+ }
13
+
14
+ [data-mk-chart-type='pie'] [data-mk-chart-svg] {
15
+ width: var(--mk-pie-size, 240px);
16
+ max-width: 100%;
17
+ margin-inline: auto;
18
+ border-radius: 50%;
19
+ cursor: pointer;
20
+ }
21
+
22
+ [data-mk-chart-type='pie'] [data-mk-part='series'] path {
23
+ stroke: var(--mk-chart-surface);
24
+ stroke-width: 1;
25
+ stroke-linejoin: round;
26
+ transition:
27
+ transform 0.45s var(--mk-ease-swatch),
28
+ opacity 0.2s ease;
29
+ }
30
+
31
+ [data-mk-chart-type='pie'] [data-mk-part='series'] path[data-mk-dim] {
32
+ opacity: 0.4;
33
+ }
34
+
35
+ [data-mk-pie-center] {
36
+ position: absolute;
37
+ top: 50%;
38
+ left: 50%;
39
+ display: grid;
40
+ justify-items: center;
41
+ gap: 0.15rem;
42
+ max-width: calc(var(--mk-pie-size, 240px) * 0.5);
43
+ transform: translate(-50%, -50%);
44
+ text-align: center;
45
+ pointer-events: none;
46
+ }
47
+
48
+ [data-mk-pie-center][hidden] {
49
+ display: none;
50
+ }
51
+
52
+ [data-mk-pie-center] strong {
53
+ color: var(--mk-text);
54
+ font-size: clamp(1.1rem, calc(var(--mk-pie-size, 240px) * 0.11), 2rem);
55
+ font-weight: 650;
56
+ font-variant-numeric: tabular-nums;
57
+ line-height: 1.1;
58
+ letter-spacing: -0.02em;
59
+ }
60
+
61
+ [data-mk-pie-center] span {
62
+ overflow: hidden;
63
+ max-width: 100%;
64
+ color: var(--mk-muted);
65
+ font-size: 0.8125rem;
66
+ white-space: nowrap;
67
+ text-overflow: ellipsis;
68
+ }
69
+
70
+ [data-mk-chart-type='pie'] [data-mk-chart-legend] {
71
+ justify-content: center;
72
+ }
73
+
74
+ [data-mk-chart-type='pie'] [data-mk-chart-legend] li {
75
+ transition: opacity 0.2s ease;
76
+ }
77
+
78
+ [data-mk-chart-type='pie'] [data-mk-chart-legend] li[data-mk-dim] {
79
+ opacity: 0.45;
80
+ }
81
+
82
+ @media (prefers-reduced-motion: reduce) {
83
+ [data-mk-chart-type='pie'] [data-mk-part='series'] path {
84
+ transition: opacity 0.2s ease;
85
+ }
86
+ }
87
+
88
+ @media (forced-colors: active) {
89
+ [data-mk-chart-type='pie'] [data-mk-part='series'] path {
90
+ stroke: Canvas;
91
+ }
92
+ }
@@ -0,0 +1,14 @@
1
+ 'use client';
2
+ import type { HTMLAttributes } from 'react';
3
+ import { useChart } from '../chart/use-chart';
4
+ import { createPieChart, type PieChartInstance, type PieChartOptions } from './engine';
5
+
6
+ export interface PieChartProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onSelect' | 'children'>, PieChartOptions {
7
+ instanceRef?: { current: PieChartInstance | null };
8
+ }
9
+
10
+ /** <PieChart label="Ventas por canal" data={[{ label: 'Tienda', value: 420 }, …]} formatValue={money} /> */
11
+ export function PieChart({ data, donut, gap, size, label, totalLabel, legend, emptyLabel, formatValue, onSelect, instanceRef, className, ...props }: PieChartProps) {
12
+ const ref = useChart(createPieChart, { data, donut, gap, size, label, totalLabel, legend, emptyLabel } as PieChartOptions, { formatValue, onSelect }, instanceRef);
13
+ return <div {...props} ref={ref} className={['mk-chart mk-pie-chart', className].filter(Boolean).join(' ')} />;
14
+ }
@@ -0,0 +1,46 @@
1
+ # pill-button
2
+
3
+ Botón píldora cuyo fondo se reúne alrededor del icono al pasar el puntero, enfocar o tocar.
4
+
5
+ ## Instalación
6
+
7
+ ```tsx
8
+ import '@yewdiel21/motion-kit/css';
9
+ import { PillButton } from '@yewdiel21/motion-kit/react';
10
+ ```
11
+
12
+ ```sh
13
+ npx @yewdiel21/motion-kit add pill-button --react --elements
14
+ ```
15
+
16
+ ## React
17
+
18
+ ```tsx
19
+ <PillButton onClick={ask}>Haz una pregunta</PillButton>
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-pill-button><button type="button">Haz una pregunta</button></mk-pill-button>
28
+ <script type="module">
29
+ import { defineElements } from '@yewdiel21/motion-kit/elements';
30
+ defineElements(['mk-pill-button']);
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 { createPillButton } 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
+ expand: invierte la animación (false). icon: marcado del icono en vanilla o ReactNode en React.
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: --mk-pill-height, --mk-pill-dot, --mk-pill-ink, --mk-pill-on-ink.
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,16 @@
1
+ import { MkElement, defineElement, type OptionType } from '../../elements/base';
2
+ import { createPillButton, type PillButtonInstance } from './engine';
3
+
4
+ /**
5
+ * <mk-pill-button><button type="button">Haz una pregunta</button></mk-pill-button>
6
+ * The dark fill gathers into a circle around the arrow on hover. `expand` starts as the circle.
7
+ */
8
+ export class MkPillButtonElement extends MkElement<PillButtonInstance> {
9
+ static options: Record<string, OptionType> = { expand: 'boolean', icon: 'string' };
10
+
11
+ protected create(options: Record<string, unknown>) {
12
+ return createPillButton(this, options);
13
+ }
14
+ }
15
+
16
+ export const definePillButton = () => defineElement('mk-pill-button', MkPillButtonElement);
@@ -0,0 +1,77 @@
1
+ import { registerMotion } from '../../core/gsap';
2
+ import { createScope } from '../../core/scope';
3
+ import { inert, type Instance } from '../../core/types';
4
+ import { press } from '../../motion/micro';
5
+
6
+ /*
7
+ * Pill button: a dark pill with a label and an arrow. When the pointer comes in
8
+ * (or the keyboard focuses it) the dark fill gathers into a circle around the
9
+ * icon on the swatch curve (.38,.49,0,1.5, a small overshoot), the label slides
10
+ * left and turns to the text colour, and the arrow nudges forward; leaving, the
11
+ * fill spreads back into the pill. `expand` turns it around: a circle by the
12
+ * label that spreads into the pill. The motion is CSS (pill-button.css); the
13
+ * engine builds the label, icon and fill when the markup lacks them, presses on
14
+ * click, and shows the gathered state for a moment after a tap on touch.
15
+ */
16
+
17
+ export interface PillButtonOptions {
18
+ /** Start as the circle and spread into the pill on hover (default false). */
19
+ expand?: boolean;
20
+ /** The icon's markup when the button has no [data-mk-pill-icon] (default: an arrow). */
21
+ icon?: string;
22
+ }
23
+
24
+ // eslint-disable-next-line @typescript-eslint/no-empty-object-type
25
+ export interface PillButtonInstance extends Instance {}
26
+
27
+ export const PILL_ARROW =
28
+ '<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12h14"/><path d="m13 6 6 6-6 6"/></svg>';
29
+
30
+ export function createPillButton(root: HTMLElement | null, options: PillButtonOptions = {}): PillButtonInstance {
31
+ const control = root ? (root.matches('button, a, [role="button"]') ? root : root.querySelector<HTMLElement>('button, a, [role="button"]')) : null;
32
+ if (!root || !control) return inert();
33
+ registerMotion();
34
+ const scope = createScope();
35
+
36
+ // Only what the engine adds is taken back (React keeps its own nodes).
37
+ const icon = control.querySelector<HTMLElement>('[data-mk-pill-icon]');
38
+ if (!control.querySelector('[data-mk-pill-label]')) {
39
+ const label = document.createElement('span');
40
+ label.dataset.mkPillLabel = '';
41
+ for (const node of [...control.childNodes]) if (node !== icon) label.append(node);
42
+ control.prepend(label);
43
+ scope.add(() => label.replaceWith(...label.childNodes));
44
+ }
45
+ if (!icon) {
46
+ const made = document.createElement('span');
47
+ made.dataset.mkPillIcon = '';
48
+ made.setAttribute('aria-hidden', 'true');
49
+ made.innerHTML = options.icon ?? PILL_ARROW;
50
+ control.append(made);
51
+ scope.add(() => made.remove());
52
+ }
53
+ const fill = document.createElement('span');
54
+ fill.dataset.mkPillFill = '';
55
+ fill.setAttribute('aria-hidden', 'true');
56
+ control.prepend(fill);
57
+ scope.add(() => fill.remove());
58
+
59
+ scope.attr(control, 'data-mk-pill', options.expand ? 'expand' : '');
60
+ scope.attr(root, 'data-mk-ready', '');
61
+
62
+ // A tap has no hover: the gathered state shows for a moment.
63
+ let tapped: ReturnType<typeof setTimeout> | undefined;
64
+ scope.on(control, 'pointerdown', (event) => {
65
+ press(control);
66
+ if ((event as PointerEvent).pointerType !== 'touch') return;
67
+ control.setAttribute('data-mk-active', '');
68
+ clearTimeout(tapped);
69
+ tapped = setTimeout(() => control.removeAttribute('data-mk-active'), 650);
70
+ });
71
+ scope.add(() => {
72
+ clearTimeout(tapped);
73
+ control.removeAttribute('data-mk-active');
74
+ });
75
+
76
+ return { destroy: () => scope.dispose() };
77
+ }