@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,237 @@
1
+ import { gsap, EASE } from '../../core/gsap';
2
+ import { reduced } from '../../core/reduced';
3
+ import type { Scope } from '../../core/scope';
4
+
5
+ /*
6
+ * The parts the round and flow charts (pie, radar, radial, sankey, heatmap)
7
+ * share with chart: the same plot, inverse-glass tooltip, outlined legend,
8
+ * screen-reader table and colour slots, so they read as one family and use
9
+ * chart.css. Each chart draws its own marks.
10
+ */
11
+
12
+ export const SVG_NS = 'http://www.w3.org/2000/svg';
13
+ export const SLOTS = 8;
14
+
15
+ export const svg = <K extends keyof SVGElementTagNameMap>(tag: K, attrs: Record<string, string | number> = {}) => {
16
+ const element = document.createElementNS(SVG_NS, tag);
17
+ for (const [name, value] of Object.entries(attrs)) element.setAttribute(name, String(value));
18
+ return element;
19
+ };
20
+ export const finite = (value: unknown): value is number => typeof value === 'number' && Number.isFinite(value);
21
+ export const round = (n: number) => Math.round(n * 100) / 100;
22
+
23
+ /** The colour of slot `i`: monochrome alone, --mk-chart-N for up to 8, a neutral past them. */
24
+ export const slotColor = (i: number, total = SLOTS) => (total === 1 ? 'var(--mk-chart-mono)' : i < SLOTS ? `var(--mk-chart-${i + 1})` : 'var(--mk-chart-other)');
25
+
26
+ export const formatNumber = (value: number) => new Intl.NumberFormat(undefined, { maximumFractionDigits: 2 }).format(value);
27
+
28
+ /** A point on a circle: angle 0 is 12 o'clock, clockwise. */
29
+ export const polar = (cx: number, cy: number, r: number, angle: number) => [cx + r * Math.sin(angle), cy - r * Math.cos(angle)] as const;
30
+
31
+ /** An annular sector (a pie slice when `inner` is 0) from angle `a0` to `a1`. */
32
+ export function arcPath(cx: number, cy: number, outer: number, inner: number, a0: number, a1: number) {
33
+ const sweep = Math.min(a1 - a0, Math.PI * 2 - 1e-4);
34
+ const end = a0 + sweep;
35
+ const large = sweep > Math.PI ? 1 : 0;
36
+ const [x0, y0] = polar(cx, cy, outer, a0);
37
+ const [x1, y1] = polar(cx, cy, outer, end);
38
+ if (inner <= 0) return `M${round(cx)},${round(cy)}L${round(x0)},${round(y0)}A${round(outer)},${round(outer)} 0 ${large} 1 ${round(x1)},${round(y1)}Z`;
39
+ const [x2, y2] = polar(cx, cy, inner, end);
40
+ const [x3, y3] = polar(cx, cy, inner, a0);
41
+ return `M${round(x0)},${round(y0)}A${round(outer)},${round(outer)} 0 ${large} 1 ${round(x1)},${round(y1)}L${round(x2)},${round(y2)}A${round(inner)},${round(inner)} 0 ${large} 0 ${round(x3)},${round(y3)}Z`;
42
+ }
43
+
44
+ export interface TipRow {
45
+ name: string;
46
+ color?: string;
47
+ value: string;
48
+ }
49
+
50
+ export interface ChartFrame {
51
+ plot: HTMLDivElement;
52
+ canvas: SVGSVGElement;
53
+ tooltip: HTMLDivElement;
54
+ empty: HTMLParagraphElement;
55
+ legend: HTMLUListElement;
56
+ table: HTMLTableElement;
57
+ live: HTMLSpanElement;
58
+ /** Fill and show the tooltip near (x, y) in plot px; null hides it. */
59
+ tip(at: { x: number; y: number; title: string; rows: TipRow[] } | null): void;
60
+ /** One outlined pill per item; `type` picks the swatch's shape. */
61
+ fillLegend(items: Array<{ name: string; color: string }>, show: boolean, type?: string): HTMLLIElement[];
62
+ /** The values for screen readers: a caption, the column heads and the rows. */
63
+ fillTable(caption: string, head: string[], rows: string[][]): void;
64
+ keep(animation: gsap.core.Animation | null | undefined): void;
65
+ }
66
+
67
+ /** The plot, tooltip, legend and table of a chart, removed by the scope. The root gets .mk-chart for chart.css. */
68
+ export function chartFrame(root: HTMLElement, scope: Scope, kind: string): ChartFrame {
69
+ if (!root.classList.contains('mk-chart')) {
70
+ root.classList.add('mk-chart');
71
+ scope.add(() => {
72
+ root.classList.remove('mk-chart');
73
+ if (!root.classList.length) root.removeAttribute('class');
74
+ });
75
+ }
76
+ scope.attr(root, 'data-mk-chart-type', kind);
77
+ const made = <K extends keyof HTMLElementTagNameMap>(tag: K, parent: Element = root) => {
78
+ const element = document.createElement(tag);
79
+ parent.append(element);
80
+ scope.add(() => element.remove());
81
+ return element;
82
+ };
83
+ const plot = made('div');
84
+ plot.dataset.mkChartPlot = '';
85
+ const canvas = svg('svg');
86
+ canvas.dataset.mkChartSvg = '';
87
+ canvas.setAttribute('role', 'img');
88
+ canvas.setAttribute('tabindex', '0');
89
+ canvas.setAttribute('focusable', 'true');
90
+ plot.append(canvas);
91
+ const tooltip = made('div', plot);
92
+ tooltip.dataset.mkChartTooltip = '';
93
+ tooltip.setAttribute('aria-hidden', 'true');
94
+ tooltip.hidden = true;
95
+ const empty = made('p');
96
+ empty.dataset.mkChartEmpty = '';
97
+ empty.hidden = true;
98
+ const legend = made('ul');
99
+ legend.dataset.mkChartLegend = '';
100
+ const table = made('table');
101
+ table.dataset.mkChartTable = '';
102
+ table.className = 'mk-sr-only';
103
+ const live = made('span');
104
+ live.className = 'mk-sr-only';
105
+ live.setAttribute('role', 'status');
106
+ scope.attr(root, 'data-mk-ready', '');
107
+
108
+ const effects = new Set<gsap.core.Animation>();
109
+ const keep = (animation: gsap.core.Animation | null | undefined) => {
110
+ if (!animation) return;
111
+ effects.add(animation);
112
+ // Finished ones leave as new ones arrive: the animation's own onComplete stays untouched.
113
+ for (const effect of effects) if (effect.totalProgress() === 1) effects.delete(effect);
114
+ };
115
+ scope.add(() => {
116
+ for (const effect of effects) effect.kill();
117
+ effects.clear();
118
+ gsap.killTweensOf(tooltip);
119
+ });
120
+
121
+ const tip: ChartFrame['tip'] = (at) => {
122
+ if (!at) {
123
+ if (!tooltip.hidden && !reduced()) {
124
+ gsap.killTweensOf(tooltip, 'opacity,scale');
125
+ keep(gsap.to(tooltip, { opacity: 0, scale: 0.94, duration: 0.15, ease: 'power2.in', onComplete: () => {
126
+ tooltip.hidden = true;
127
+ } }));
128
+ } else tooltip.hidden = true;
129
+ return;
130
+ }
131
+ tooltip.replaceChildren();
132
+ const title = document.createElement('strong');
133
+ title.textContent = at.title;
134
+ tooltip.append(title);
135
+ for (const each of at.rows) {
136
+ const row = document.createElement('span');
137
+ row.dataset.mkChartRow = '';
138
+ const swatch = document.createElement('i');
139
+ if (!each.color || /--mk-(text|chart-mono|muted)/.test(each.color)) swatch.dataset.mkMono = '';
140
+ else swatch.style.background = each.color;
141
+ const name = document.createElement('span');
142
+ name.textContent = each.name;
143
+ const amount = document.createElement('b');
144
+ amount.textContent = each.value;
145
+ row.append(swatch, name, amount);
146
+ tooltip.append(row);
147
+ }
148
+ const appearing = tooltip.hidden;
149
+ tooltip.hidden = false;
150
+ const width = plot.clientWidth || 300;
151
+ const height = plot.clientHeight || 240;
152
+ const tipW = tooltip.offsetWidth || 160;
153
+ const tipH = tooltip.offsetHeight || 60;
154
+ // Beside the point, towards the middle of the plot, inside it.
155
+ const left = round(Math.max(0, Math.min(width - tipW, at.x > width / 2 ? at.x - 16 - tipW : at.x + 16)));
156
+ const top = round(Math.max(0, Math.min(height - tipH, at.y - tipH - 12)));
157
+ if (appearing || reduced()) {
158
+ gsap.killTweensOf(tooltip);
159
+ gsap.set(tooltip, { x: left, y: top });
160
+ if (!reduced()) keep(gsap.fromTo(tooltip, { opacity: 0, scale: 0.9 }, { opacity: 1, scale: 1, duration: 0.25, ease: EASE.pop }));
161
+ else gsap.set(tooltip, { opacity: 1, scale: 1 });
162
+ } else {
163
+ gsap.killTweensOf(tooltip, 'opacity,scale');
164
+ gsap.set(tooltip, { opacity: 1, scale: 1 });
165
+ keep(gsap.to(tooltip, { x: left, y: top, duration: 0.25, ease: 'power3.out', overwrite: 'auto' }));
166
+ }
167
+ };
168
+
169
+ const fillLegend: ChartFrame['fillLegend'] = (items, show, type = 'bar') => {
170
+ legend.replaceChildren();
171
+ legend.hidden = !show || !items.length;
172
+ if (legend.hidden) return [];
173
+ return items.map((item) => {
174
+ const li = document.createElement('li');
175
+ const swatch = document.createElement('span');
176
+ swatch.dataset.mkChartSwatch = '';
177
+ swatch.dataset.mkType = type;
178
+ swatch.style.background = item.color;
179
+ swatch.style.color = item.color;
180
+ swatch.setAttribute('aria-hidden', 'true');
181
+ const name = document.createElement('span');
182
+ name.textContent = item.name;
183
+ li.append(swatch, name);
184
+ legend.append(li);
185
+ return li;
186
+ });
187
+ };
188
+
189
+ const fillTable: ChartFrame['fillTable'] = (caption, head, rows) => {
190
+ table.replaceChildren();
191
+ const cap = document.createElement('caption');
192
+ cap.textContent = caption;
193
+ const thead = document.createElement('thead');
194
+ const headRow = document.createElement('tr');
195
+ for (const text of head) {
196
+ const th = document.createElement('th');
197
+ th.scope = 'col';
198
+ th.textContent = text;
199
+ headRow.append(th);
200
+ }
201
+ thead.append(headRow);
202
+ const body = document.createElement('tbody');
203
+ for (const cells of rows) {
204
+ const row = document.createElement('tr');
205
+ cells.forEach((text, i) => {
206
+ const cell = document.createElement(i ? 'td' : 'th');
207
+ if (!i) (cell as HTMLTableCellElement).scope = 'row';
208
+ cell.textContent = text;
209
+ row.append(cell);
210
+ });
211
+ body.append(row);
212
+ }
213
+ table.append(cap, thead, body);
214
+ };
215
+
216
+ return { plot, canvas, tooltip, empty, legend, table, live, tip, fillLegend, fillTable, keep };
217
+ }
218
+
219
+ /** Calls `draw` again when the root changes width (on the next frame, once). */
220
+ export function onWidth(root: HTMLElement, scope: Scope, draw: () => void) {
221
+ if (typeof ResizeObserver !== 'function') return;
222
+ let width = 0;
223
+ let frame = 0;
224
+ const observer = scope.observe(
225
+ new ResizeObserver(() => {
226
+ const next = Math.round(root.clientWidth);
227
+ if (!next || next === width) return;
228
+ const first = !width;
229
+ width = next;
230
+ if (first) return;
231
+ cancelAnimationFrame(frame);
232
+ frame = requestAnimationFrame(draw);
233
+ }),
234
+ );
235
+ observer.observe(root);
236
+ scope.add(() => cancelAnimationFrame(frame));
237
+ }
@@ -11,13 +11,13 @@ export interface ChartProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onSele
11
11
  * <Chart type="bar" stacked labels={weeks} series={[{ name: 'Ana', values }]} formatValue={money} label="Sesiones por semana" />.
12
12
  * Data changes redraw in place; the selection survives when its point still exists.
13
13
  */
14
- export function Chart({ type, labels, series, points, stacked, formatValue, label, height, emptyLabel, legend, tooltip, onSelect, instanceRef, className, ...props }: ChartProps) {
14
+ export function Chart({ type, labels, series, points, stacked, formatValue, label, height, emptyLabel, legend, tooltip, axis, endLabel, onSelect, instanceRef, className, ...props }: ChartProps) {
15
15
  const ref = useRef<HTMLDivElement>(null);
16
16
  const instance = useRef<ChartInstance | null>(null);
17
17
  const format = useLatest(formatValue);
18
18
  const select = useLatest(onSelect);
19
19
  const formatted = formatValue !== undefined;
20
- const data = { type, labels, series, points, stacked, label, height, emptyLabel, legend, tooltip };
20
+ const data = { type, labels, series, points, stacked, label, height, emptyLabel, legend, tooltip, axis, endLabel };
21
21
  const key = JSON.stringify(data);
22
22
  const latest = useLatest(data);
23
23
  const options = (): ChartOptions => ({
@@ -0,0 +1,55 @@
1
+ 'use client';
2
+ import { useRef, type RefObject } from 'react';
3
+ import { useIsomorphicLayoutEffect, useLatest } from '../../react/use-recipe';
4
+ import type { Instance } from '../../core/types';
5
+
6
+ interface Updatable<O> extends Instance {
7
+ update(options: O): void;
8
+ }
9
+
10
+ /**
11
+ * The React side of the round and flow charts: the engine is created once and
12
+ * data changes are pushed with update(), so the chart glides instead of being
13
+ * drawn again. `data` is compared as JSON; callbacks always call the latest.
14
+ */
15
+ export function useChart<O extends object, I extends Updatable<O>>(
16
+ create: (root: HTMLElement | null, options: O) => I,
17
+ data: O,
18
+ callbacks: { formatValue?: (value: number) => string; onSelect?: (...args: never[]) => void },
19
+ instanceRef?: { current: I | null },
20
+ ): RefObject<HTMLDivElement | null> {
21
+ const ref = useRef<HTMLDivElement>(null);
22
+ const instance = useRef<I | null>(null);
23
+ const format = useLatest(callbacks.formatValue);
24
+ const select = useLatest(callbacks.onSelect);
25
+ const formatted = callbacks.formatValue !== undefined;
26
+ const key = JSON.stringify(data);
27
+ const latest = useLatest(data);
28
+ const options = (): O => ({
29
+ ...latest.current,
30
+ formatValue: formatted ? (value: number) => format.current?.(value) ?? String(value) : undefined,
31
+ onSelect: (...args: never[]) => (select.current as ((...a: never[]) => void) | undefined)?.(...args),
32
+ });
33
+ useIsomorphicLayoutEffect(() => {
34
+ const created = create(ref.current, options());
35
+ instance.current = created;
36
+ if (instanceRef) instanceRef.current = created;
37
+ return () => {
38
+ created.destroy();
39
+ instance.current = null;
40
+ if (instanceRef) instanceRef.current = null;
41
+ };
42
+ // Created once; data is pushed below.
43
+ // eslint-disable-next-line react-hooks/exhaustive-deps
44
+ }, []);
45
+ const first = useRef(true);
46
+ useIsomorphicLayoutEffect(() => {
47
+ if (first.current) {
48
+ first.current = false;
49
+ return;
50
+ }
51
+ instance.current?.update(options());
52
+ // eslint-disable-next-line react-hooks/exhaustive-deps
53
+ }, [key, formatted]);
54
+ return ref;
55
+ }
@@ -2,7 +2,7 @@
2
2
 
3
3
  Tabla avanzada con ordenamiento, búsqueda, filtros, selección, paginación, acciones por fila y columnas configurables. Al ordenar, las filas se deslizan a su nuevo lugar (el resorte de layout del kit, .62 s) y la flecha del encabezado gira. La búsqueda resalta lo que coincide (sin importar tildes ni mayúsculas) y las filas que quedan se reacomodan juntas. Los filtros usan `filter-builder` dentro de un panel que crece desde su botón: la tabla responde mientras editas y una frase dice qué está filtrado («Estado es Enviado y Monto mayor que 500»).
4
4
 
5
- Las casillas se dibujan solas y hacen pop; la del encabezado queda a medias cuando solo hay algunas filas elegidas. Al elegir filas, sube desde un blur una barra con el conteo (que rueda) y las acciones masivas, y ofrece elegir todos los resultados cuando la página entera está marcada. Al cambiar de página, las filas entran por el lado hacia el que vas. Las filas eliminadas salen deslizándose y las demás cierran el hueco. Mientras carga hay *skeletons* que brillan, y sin nada que mostrar aparece un estado vacío que explica por qué y ofrece limpiar los filtros.
5
+ Las casillas se dibujan solas y hacen pop; la del encabezado queda a medias cuando solo hay algunas filas elegidas. Una fila elegida se tiñe apenas y lleva un borde de acento a la izquierda, sin tapar sus datos. Al elegir filas, la barra con el conteo (que rueda) y las acciones masivas aparece con un blur en el lugar de la barra de herramientas, así nunca cubre filas, y ofrece elegir todos los resultados cuando la página entera está marcada. Al cambiar de página, las filas entran por el lado hacia el que vas; si la tabla cabe en su panel, el desborde se oculta mientras viajan para que no parpadee una barra de desplazamiento (si ya es más ancha, aparecen con un fundido). Las filas eliminadas salen deslizándose y las demás cierran el hueco. Mientras carga hay *skeletons* que brillan, y sin nada que mostrar aparece un estado vacío que explica por qué y ofrece limpiar los filtros.
6
6
 
7
7
  ## HTML / Astro
8
8
 
@@ -56,7 +56,7 @@ Unas `rows` nuevas entran en su sitio y se conservan el orden, los filtros y la
56
56
  | `rowKey` | `row-key` | `'id'` | Clave (o función) del id de cada fila. |
57
57
  | `caption` | `caption` | `'Tabla'` | Nombre accesible. |
58
58
  | `pageSize` | `page-size` | `10` | Filas por página (`0`: todas). |
59
- | `pageSizes` | `page-sizes` (JSON) | `[10, 25, 50]` | Opciones de filas por página. |
59
+ | `pageSizes` | `page-sizes` (JSON) | `[10, 25, 50]` | Opciones del menú «Filas» (un botón que abre un menú como Filtros y Columnas, con la opción actual marcada). |
60
60
  | `selectable` | `selectable` | `false` | Casillas para elegir filas. |
61
61
  | `search` | `search` | `true` | Campo de búsqueda (un texto es su marcador). |
62
62
  | `filters` | `filters` | `true` | Botón «Filtros» con `filter-builder`. |
@@ -80,4 +80,4 @@ Unas `rows` nuevas entran en su sitio y se conservan el orden, los filtros y la
80
80
 
81
81
  ## Accesibilidad
82
82
 
83
- Una tabla real con `caption`, encabezados `th scope="col"` con `aria-sort` y botones para ordenar; casillas con etiqueta («Seleccionar Ana Gómez»); el botón de acciones dice de qué fila es y abre un menú de WAI-ARIA; la barra de selección es un `toolbar` con nombre. Si la tabla es más ancha que su contenedor, se desplaza de lado dentro de una región enfocable. Un estado discreto anuncia el orden, los resultados, la página y la selección. Con movimiento reducido los cambios son inmediatos.
83
+ Una tabla real con `caption`, encabezados `th scope="col"` con `aria-sort` y botones para ordenar; casillas con etiqueta («Seleccionar Ana Gómez»); el botón de acciones dice de qué fila es y abre un menú de WAI-ARIA; «Filas» es un botón con `aria-haspopup="menu"` y sus opciones son `menuitemradio` con `aria-checked`; la barra de selección es un `toolbar` con nombre. Si la tabla es más ancha que su contenedor, se desplaza de lado dentro de una región enfocable. Un estado discreto anuncia el orden, los resultados, la página y la selección. Con movimiento reducido los cambios son inmediatos.
@@ -1,9 +1,11 @@
1
1
  /* data-table — a toolbar (a 12 px search on surface-3, 12 px tool buttons with an accent count), the filter sentence
2
2
  on accent-soft, then the table on a 20 px panel: a sticky muted header with sort buttons whose arrow turns, 3rem
3
- rows on hairlines, chosen rows tinted, 18 px checks that draw themselves, an 8 px row menu button and shimmering
4
- bones while loading; the foot with the range, rows per page and the pager; the bar of the chosen rows on inverse
5
- glass. Panels (columns, filters) are anchored .mk-dialog (dialog.css) surfaces with 20 px corners. The table
6
- scrolls sideways inside its panel when it is wider; --mk-dt-max-height lets it scroll down too. */
3
+ rows on hairlines, chosen rows softly tinted with an accent edge (their data stays readable), 18 px checks that
4
+ draw themselves, an 8 px row menu button and shimmering bones while loading; the foot with the range, the rows
5
+ per page menu and the pager; the bar of the chosen rows takes the toolbar's place, so it never covers a row.
6
+ Panels and menus (columns, filters, rows per page, row actions) are anchored .mk-dialog (dialog.css) surfaces with
7
+ 20 px corners and 8 px items. The table scrolls sideways inside its panel when it is wider; --mk-dt-max-height
8
+ lets it scroll down too. */
7
9
  .mk-data-table,
8
10
  mk-data-table {
9
11
  --mk-dt-max-height: none;
@@ -17,6 +19,8 @@ mk-data-table {
17
19
 
18
20
  /* ── The toolbar ─────────────────────────────────────────────────────────── */
19
21
  [data-mk-dt-bar] {
22
+ grid-row: 1;
23
+ grid-column: 1;
20
24
  display: flex;
21
25
  flex-wrap: wrap;
22
26
  align-items: center;
@@ -86,6 +90,7 @@ mk-data-table {
86
90
  }
87
91
 
88
92
  [data-mk-dt-tool]:focus-visible,
93
+ [data-mk-dt-size-button]:focus-visible,
89
94
  [data-mk-dt-sort]:focus-visible,
90
95
  [data-mk-dt-row-menu]:focus-visible,
91
96
  [data-mk-dt-pager] button:focus-visible,
@@ -223,8 +228,17 @@ mk-data-table {
223
228
  background: color-mix(in srgb, var(--mk-text) 3%, transparent);
224
229
  }
225
230
 
231
+ /* A chosen row: a light tint and an accent edge, never a fill that hides the data (on OLED the accent is white). */
226
232
  [data-mk-dt-table] tbody tr[data-selected] {
227
- background: color-mix(in srgb, var(--mk-accent-soft) 60%, transparent);
233
+ background: color-mix(in srgb, var(--mk-accent) 7%, transparent);
234
+ }
235
+
236
+ [data-mk-dt-table] tbody tr[data-selected]:hover {
237
+ background: color-mix(in srgb, var(--mk-accent) 11%, transparent);
238
+ }
239
+
240
+ [data-mk-dt-table] tbody tr[data-selected] > :first-child {
241
+ box-shadow: inset 3px 0 0 var(--mk-accent);
228
242
  }
229
243
 
230
244
  [data-mk-dt-table] td mark {
@@ -296,7 +310,7 @@ th[data-direction='desc'] [data-mk-dt-sort] svg {
296
310
  width: 1.125rem;
297
311
  height: 1.125rem;
298
312
  box-sizing: border-box;
299
- border: 1.5px solid var(--mk-outline);
313
+ border: 2px solid var(--mk-outline);
300
314
  border-radius: 5px;
301
315
  color: transparent;
302
316
  vertical-align: middle;
@@ -317,6 +331,9 @@ th[data-direction='desc'] [data-mk-dt-sort] svg {
317
331
  }
318
332
 
319
333
  [data-mk-dt-check] svg {
334
+ display: block;
335
+ width: 100%;
336
+ height: 100%;
320
337
  pointer-events: none;
321
338
  }
322
339
 
@@ -422,14 +439,35 @@ th[data-direction='desc'] [data-mk-dt-sort] svg {
422
439
  margin-left: auto;
423
440
  }
424
441
 
425
- [data-mk-dt-size] select {
442
+ [data-mk-dt-size-button] {
443
+ display: inline-flex;
444
+ align-items: center;
445
+ gap: 0.25rem;
426
446
  height: 2rem;
427
- padding: 0 0.5rem;
447
+ padding: 0 0.375rem 0 0.75rem;
428
448
  border: 1px solid var(--mk-outline-soft);
429
449
  border-radius: 8px;
430
450
  background: var(--mk-surface-1);
431
451
  color: var(--mk-text);
432
452
  font: inherit;
453
+ font-variant-numeric: tabular-nums;
454
+ cursor: pointer;
455
+ transition: background-color 0.15s;
456
+ }
457
+
458
+ [data-mk-dt-size-button]:hover,
459
+ [data-mk-dt-size-button][aria-expanded='true'] {
460
+ background: var(--mk-surface-2);
461
+ }
462
+
463
+ [data-mk-dt-size-button] svg {
464
+ flex: none;
465
+ color: var(--mk-muted);
466
+ transition: transform 0.3s cubic-bezier(0.38, 0.49, 0, 1.5);
467
+ }
468
+
469
+ [data-mk-dt-size-button][aria-expanded='true'] svg {
470
+ transform: rotate(180deg);
433
471
  }
434
472
 
435
473
  [data-mk-dt-pager] {
@@ -463,24 +501,28 @@ th[data-direction='desc'] [data-mk-dt-sort] svg {
463
501
  }
464
502
 
465
503
  /* ── The bar of the chosen rows ──────────────────────────────────────────── */
504
+ /* It shares the toolbar's grid cell and covers it while rows are chosen: the rows stay in view. */
466
505
  [data-mk-dt-bulk] {
467
- position: absolute;
468
- right: 0;
469
- bottom: 3.25rem;
470
- left: 0;
471
- z-index: 3;
506
+ position: relative;
507
+ z-index: 2;
508
+ grid-row: 1;
509
+ grid-column: 1;
472
510
  display: flex;
473
511
  flex-wrap: wrap;
474
512
  align-items: center;
513
+ align-self: stretch;
475
514
  gap: 0.25rem 0.5rem;
476
- width: max-content;
477
- max-width: calc(100% - 1rem);
478
- margin-inline: auto;
479
- padding: 0.375rem 0.375rem 0.375rem 1rem;
480
- border-radius: 20px;
481
- background: var(--mk-glass-inverse, var(--mk-text));
482
- box-shadow: 0 14px 32px -12px color-mix(in srgb, var(--mk-shadow-color, #000) 45%, transparent);
483
- color: var(--mk-on-glass-inverse, var(--mk-surface-1));
515
+ min-width: 0;
516
+ min-height: 2.5rem;
517
+ padding: 0.25rem 0.25rem 0.25rem 1rem;
518
+ border-radius: 12px;
519
+ background: var(--mk-surface-3);
520
+ color: var(--mk-text);
521
+ }
522
+
523
+ .mk-data-table:has(> [data-mk-dt-bulk]:not([hidden])) > [data-mk-dt-bar],
524
+ mk-data-table:has(> [data-mk-dt-bulk]:not([hidden])) > [data-mk-dt-bar] {
525
+ visibility: hidden;
484
526
  }
485
527
 
486
528
  [data-mk-dt-bulk-label] {
@@ -504,12 +546,17 @@ th[data-direction='desc'] [data-mk-dt-sort] svg {
504
546
  height: 2rem;
505
547
  padding: 0 0.75rem;
506
548
  border: 0;
507
- border-radius: 12px;
508
- background: color-mix(in srgb, currentColor 14%, transparent);
549
+ border-radius: 8px;
550
+ background: var(--mk-surface-1);
509
551
  color: inherit;
510
552
  font: inherit;
511
553
  font-weight: 500;
512
554
  cursor: pointer;
555
+ transition: background-color 0.15s;
556
+ }
557
+
558
+ [data-mk-dt-bulk] button:hover {
559
+ background: var(--mk-surface-2);
513
560
  }
514
561
 
515
562
  [data-mk-dt-bulk] button[data-danger] {
@@ -517,23 +564,31 @@ th[data-direction='desc'] [data-mk-dt-sort] svg {
517
564
  color: var(--mk-on-error, #fff);
518
565
  }
519
566
 
520
- [data-mk-dt-select-all] {
567
+ [data-mk-dt-bulk] [data-mk-dt-select-all] {
568
+ background: none;
521
569
  text-decoration: underline;
522
570
  text-underline-offset: 3px;
523
571
  }
524
572
 
525
- [data-mk-dt-bulk-close] {
573
+ /* The close button is a square with its cross in the middle (the bar's button padding must not reach it). */
574
+ [data-mk-dt-bulk] [data-mk-dt-bulk-close] {
526
575
  display: inline-grid;
527
576
  place-items: center;
528
577
  width: 2rem;
578
+ margin-left: auto;
529
579
  padding: 0;
530
580
  }
531
581
 
582
+ [data-mk-dt-bulk-close] svg {
583
+ display: block;
584
+ }
585
+
532
586
  /* ── Panels and the row menu ─────────────────────────────────────────────── */
533
587
  .mk-dialog.mk-dt-panel {
534
588
  --mk-dialog-radius: 20px;
535
589
  --mk-dialog-bg: var(--mk-surface-1);
536
- --mk-dialog-shadow: 0 16px 48px -12px color-mix(in srgb, var(--mk-shadow-color, #000) 35%, transparent);
590
+ /* A hairline keeps the panel apart from the table under it (on OLED both are near black). */
591
+ --mk-dialog-shadow: 0 0 0 1px var(--mk-outline-soft), 0 16px 48px -12px color-mix(in srgb, var(--mk-shadow-color, #000) 35%, transparent);
537
592
  gap: 0.5rem;
538
593
  width: max-content;
539
594
  min-width: min(14rem, calc(100vw - 32px));
@@ -574,6 +629,45 @@ th[data-direction='desc'] [data-mk-dt-sort] svg {
574
629
  background: var(--mk-surface-2);
575
630
  }
576
631
 
632
+ /* The row menu and the rows per page menu look like the panels: 20 px surface, 8 px items, the same hover. */
633
+ .mk-dialog.mk-dt-panel.mk-dt-menu {
634
+ gap: 0.125rem;
635
+ min-width: 9rem;
636
+ padding: 0.375rem;
637
+ -webkit-backdrop-filter: none;
638
+ backdrop-filter: none;
639
+ }
640
+
641
+ .mk-dialog.mk-dt-menu [data-mk-item] {
642
+ gap: 0.625rem;
643
+ min-height: 2.25rem;
644
+ padding: 0 0.625rem;
645
+ border-radius: 8px;
646
+ font-family: inherit;
647
+ font-size: 0.875rem;
648
+ font-weight: 500;
649
+ letter-spacing: normal;
650
+ }
651
+
652
+ .mk-dialog.mk-dt-menu [data-mk-item]:hover,
653
+ .mk-dialog.mk-dt-menu [data-mk-item]:focus-visible {
654
+ background: var(--mk-surface-2);
655
+ }
656
+
657
+ .mk-dialog.mk-dt-menu [data-mk-item] svg {
658
+ flex: none;
659
+ margin-left: auto;
660
+ opacity: 0;
661
+ }
662
+
663
+ .mk-dialog.mk-dt-menu [data-mk-item][aria-checked='true'] {
664
+ font-weight: 600;
665
+ }
666
+
667
+ .mk-dialog.mk-dt-menu [data-mk-item][aria-checked='true'] svg {
668
+ opacity: 1;
669
+ }
670
+
577
671
  .mk-dialog.mk-dt-menu [data-mk-item][data-danger] {
578
672
  color: var(--mk-error);
579
673
  }
@@ -584,6 +678,7 @@ th[data-direction='desc'] [data-mk-dt-sort] svg {
584
678
  }
585
679
 
586
680
  [data-mk-dt-sort] svg,
681
+ [data-mk-dt-size-button] svg,
587
682
  [data-mk-dt-check] {
588
683
  transition: none;
589
684
  }