@yewdiel21/motion-kit 0.14.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +174 -155
- package/bin/motion-kit.mjs +3 -3
- package/dist/auto/index.d.ts +6 -3
- package/dist/auto/index.js +8 -2
- package/dist/auto/index.js.map +1 -1
- package/dist/{chunk-EY4BJKC4.js → chunk-6IRJYXJU.js} +2 -2
- package/dist/{chunk-CU6SXW3E.js → chunk-A6QQCF6V.js} +1309 -310
- package/dist/chunk-A6QQCF6V.js.map +1 -0
- package/dist/chunk-JXRS3OEA.js +3638 -0
- package/dist/chunk-JXRS3OEA.js.map +1 -0
- package/dist/css/components/add-to-cart.css +72 -0
- package/dist/css/components/bidirectional-slider.css +168 -0
- package/dist/css/components/data-table.css +121 -26
- package/dist/css/components/filter-builder.css +10 -15
- package/dist/css/components/heatmap-chart.css +391 -0
- package/dist/css/components/hover-card.css +8 -2
- package/dist/css/components/image-preview.css +90 -2
- package/dist/css/components/json-viewer.css +33 -1
- package/dist/css/components/nav.css +102 -102
- package/dist/css/components/orbit-menu.css +130 -0
- package/dist/css/components/pagination.css +10 -5
- package/dist/css/components/pie-chart.css +390 -0
- package/dist/css/components/pill-button.css +135 -0
- package/dist/css/components/radar-chart.css +370 -0
- package/dist/css/components/radial-chart.css +398 -0
- package/dist/css/components/sankey-chart.css +358 -0
- package/dist/css/components/scroll-island.css +111 -0
- package/dist/css/components/select.css +136 -0
- package/dist/css/components/toolbar.css +122 -0
- package/dist/css/components/tree-view.css +79 -0
- package/dist/css/components/voice-dots.css +70 -0
- package/dist/elements/index.d.ts +180 -7
- package/dist/elements/index.js +287 -6
- package/dist/elements/index.js.map +1 -1
- package/dist/{engine-Cn6IhE3z.d.ts → engine-5zRIssTO.d.ts} +11 -1
- package/dist/index.css +1832 -168
- package/dist/index.d.ts +4 -4
- package/dist/index.js +33 -3
- package/dist/index.js.map +1 -1
- package/dist/react/index.d.ts +117 -11
- package/dist/react/index.js +314 -13
- package/dist/react/index.js.map +1 -1
- package/dist/{reduced-BBUusld6.d.ts → reduced-6X_bns60.d.ts} +1 -1
- package/dist/{reveal-DEvp6U6T.d.ts → reveal-BfyKKsw4.d.ts} +97 -4
- package/dist/store-CA2Qcun_.d.ts +453 -0
- package/package.json +9 -16
- package/registry.json +1096 -948
- package/src/auto/auto-init.ts +6 -0
- package/src/components/add-to-cart/README.md +34 -0
- package/src/components/add-to-cart/add-to-cart.css +71 -0
- package/src/components/add-to-cart/element.ts +20 -0
- package/src/components/add-to-cart/engine.ts +206 -0
- package/src/components/add-to-cart/react.tsx +29 -0
- package/src/components/bidirectional-slider/README.md +46 -0
- package/src/components/bidirectional-slider/bidirectional-slider.css +167 -0
- package/src/components/bidirectional-slider/element.ts +42 -0
- package/src/components/bidirectional-slider/engine.ts +289 -0
- package/src/components/bidirectional-slider/react.tsx +45 -0
- package/src/components/chart/README.md +10 -0
- package/src/components/chart/element.ts +2 -0
- package/src/components/chart/engine.ts +50 -23
- package/src/components/chart/parts.ts +237 -0
- package/src/components/chart/react.tsx +2 -2
- package/src/components/chart/use-chart.ts +55 -0
- package/src/components/data-table/README.md +3 -3
- package/src/components/data-table/data-table.css +121 -26
- package/src/components/data-table/engine.ts +81 -29
- package/src/components/editable-table/engine.ts +67 -11
- package/src/components/filter-builder/engine.ts +33 -2
- package/src/components/filter-builder/filter-builder.css +10 -15
- package/src/components/heatmap-chart/README.md +48 -0
- package/src/components/heatmap-chart/element.ts +21 -0
- package/src/components/heatmap-chart/engine.ts +281 -0
- package/src/components/heatmap-chart/heatmap-chart.css +93 -0
- package/src/components/heatmap-chart/react.tsx +14 -0
- package/src/components/hover-card/engine.ts +30 -1
- package/src/components/hover-card/hover-card.css +8 -2
- package/src/components/image-preview/element.ts +1 -1
- package/src/components/image-preview/engine.ts +95 -15
- package/src/components/image-preview/image-preview.css +90 -2
- package/src/components/image-preview/react.tsx +3 -3
- package/src/components/json-viewer/engine.ts +11 -2
- package/src/components/json-viewer/json-viewer.css +33 -1
- package/src/components/nav/README.md +12 -10
- package/src/components/nav/element.ts +5 -5
- package/src/components/nav/engine.ts +21 -14
- package/src/components/nav/nav.css +102 -102
- package/src/components/nav/react.tsx +4 -4
- package/src/components/orbit-menu/README.md +46 -0
- package/src/components/orbit-menu/element.ts +27 -0
- package/src/components/orbit-menu/engine.ts +179 -0
- package/src/components/orbit-menu/orbit-menu.css +129 -0
- package/src/components/orbit-menu/react.tsx +44 -0
- package/src/components/pagination/README.md +1 -1
- package/src/components/pagination/engine.ts +50 -4
- package/src/components/pagination/pagination.css +10 -5
- package/src/components/pie-chart/README.md +48 -0
- package/src/components/pie-chart/element.ts +30 -0
- package/src/components/pie-chart/engine.ts +283 -0
- package/src/components/pie-chart/pie-chart.css +92 -0
- package/src/components/pie-chart/react.tsx +14 -0
- package/src/components/pill-button/README.md +46 -0
- package/src/components/pill-button/element.ts +16 -0
- package/src/components/pill-button/engine.ts +77 -0
- package/src/components/pill-button/pill-button.css +134 -0
- package/src/components/pill-button/react.tsx +32 -0
- package/src/components/radar-chart/README.md +48 -0
- package/src/components/radar-chart/element.ts +30 -0
- package/src/components/radar-chart/engine.ts +327 -0
- package/src/components/radar-chart/radar-chart.css +72 -0
- package/src/components/radar-chart/react.tsx +14 -0
- package/src/components/radial-chart/README.md +48 -0
- package/src/components/radial-chart/element.ts +30 -0
- package/src/components/radial-chart/engine.ts +290 -0
- package/src/components/radial-chart/radial-chart.css +100 -0
- package/src/components/radial-chart/react.tsx +14 -0
- package/src/components/sankey-chart/README.md +48 -0
- package/src/components/sankey-chart/element.ts +31 -0
- package/src/components/sankey-chart/engine.ts +401 -0
- package/src/components/sankey-chart/react.tsx +14 -0
- package/src/components/sankey-chart/sankey-chart.css +60 -0
- package/src/components/scroll-island/README.md +34 -0
- package/src/components/scroll-island/element.ts +16 -0
- package/src/components/scroll-island/engine.ts +252 -0
- package/src/components/scroll-island/react.tsx +26 -0
- package/src/components/scroll-island/scroll-island.css +110 -0
- package/src/components/select/README.md +34 -0
- package/src/components/select/element.ts +26 -0
- package/src/components/select/engine.ts +267 -0
- package/src/components/select/react.tsx +33 -0
- package/src/components/select/select.css +135 -0
- package/src/components/toolbar/README.md +46 -0
- package/src/components/toolbar/element.ts +30 -0
- package/src/components/toolbar/engine.ts +230 -0
- package/src/components/toolbar/react.tsx +34 -0
- package/src/components/toolbar/toolbar.css +121 -0
- package/src/components/tree-view/element.ts +1 -1
- package/src/components/tree-view/engine.ts +174 -0
- package/src/components/tree-view/react.tsx +7 -3
- package/src/components/tree-view/tree-view.css +79 -0
- package/src/components/voice-dots/README.md +48 -0
- package/src/components/voice-dots/element.ts +45 -0
- package/src/components/voice-dots/engine.ts +243 -0
- package/src/components/voice-dots/react.tsx +52 -0
- package/src/components/voice-dots/voice-dots.css +69 -0
- package/src/elements/index.ts +39 -0
- package/src/index.ts +16 -0
- package/src/motion/liquid.ts +14 -4
- package/src/react/index.ts +14 -1
- package/dist/chunk-6P4P7NU6.js +0 -1401
- package/dist/chunk-6P4P7NU6.js.map +0 -1
- package/dist/chunk-CU6SXW3E.js.map +0 -1
- package/dist/store-DjkTBX7f.d.ts +0 -195
- /package/dist/{chunk-EY4BJKC4.js.map → chunk-6IRJYXJU.js.map} +0 -0
|
@@ -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.
|
|
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
|
|
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
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
|
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:
|
|
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]
|
|
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.
|
|
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:
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
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:
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
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:
|
|
508
|
-
background:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|