@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,281 @@
|
|
|
1
|
+
import { gsap, EASE, registerMotion } from '../../core/gsap';
|
|
2
|
+
import { addDays, capitalize, dayKey, safeFormat, startOfWeek, toDate, weekdayNames, type DateInput } from '../../core/dates';
|
|
3
|
+
import { reduced } from '../../core/reduced';
|
|
4
|
+
import { createScope } from '../../core/scope';
|
|
5
|
+
import { inert, type Instance } from '../../core/types';
|
|
6
|
+
import { chartFrame, finite, formatNumber, onWidth, round, svg } from '../chart/parts';
|
|
7
|
+
|
|
8
|
+
/*
|
|
9
|
+
* Heatmap chart: daily activity on a calendar, a column per week like
|
|
10
|
+
* GitHub's, in chart's family. Each day is a 3 px rounded cell whose colour
|
|
11
|
+
* deepens in steps (four by default) from the track to the accent. The cells
|
|
12
|
+
* pop in along the diagonal, oldest first (.35 s, 8 ms apart); new data
|
|
13
|
+
* crossfades their colours. Pointing at a day (or the arrows: up and down a
|
|
14
|
+
* day, left and right a week, PageUp/PageDown four) lifts it with a ring and
|
|
15
|
+
* the tooltip says its value and date; the "less … more" scale lights the
|
|
16
|
+
* days of a step. Narrow screens scroll the weeks, ending on the latest.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
export interface HeatmapDatum {
|
|
20
|
+
date: DateInput;
|
|
21
|
+
value: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface HeatmapChartOptions {
|
|
25
|
+
/** Values by day: a list or a { 'YYYY-MM-DD': value } map. */
|
|
26
|
+
data?: HeatmapDatum[] | Record<string, number>;
|
|
27
|
+
/** The last day shown (default: today). */
|
|
28
|
+
end?: DateInput;
|
|
29
|
+
/** Weeks shown (default 53). */
|
|
30
|
+
weeks?: number;
|
|
31
|
+
/** First day of the week, 0 Sunday … 6 Saturday (default 1, Monday). */
|
|
32
|
+
firstDay?: number;
|
|
33
|
+
locale?: string;
|
|
34
|
+
/** Coloured steps above zero (default 4). */
|
|
35
|
+
levels?: number;
|
|
36
|
+
/** The deepest step's colour (default the accent). */
|
|
37
|
+
color?: string;
|
|
38
|
+
/** What a value counts, for the tooltip (default 'actividades'). */
|
|
39
|
+
unit?: string;
|
|
40
|
+
label?: string;
|
|
41
|
+
/** The "less … more" scale (default true). */
|
|
42
|
+
legend?: boolean;
|
|
43
|
+
formatValue?: (value: number) => string;
|
|
44
|
+
emptyLabel?: string;
|
|
45
|
+
onSelect?: (date: string | null, value: number | null) => void;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface HeatmapChartInstance extends Instance {
|
|
49
|
+
update(options: HeatmapChartOptions): void;
|
|
50
|
+
/** Choose a day ('YYYY-MM-DD') or none. */
|
|
51
|
+
select(date: string | null): void;
|
|
52
|
+
readonly selected: string | null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const LEFT = 28;
|
|
56
|
+
const TOP = 18;
|
|
57
|
+
|
|
58
|
+
export function createHeatmapChart(root: HTMLElement | null, initial: HeatmapChartOptions = {}): HeatmapChartInstance {
|
|
59
|
+
if (!root) return inert({ update() {}, select() {}, selected: null });
|
|
60
|
+
registerMotion();
|
|
61
|
+
const scope = createScope();
|
|
62
|
+
const frame = chartFrame(root, scope, 'heatmap');
|
|
63
|
+
const { plot, canvas, keep } = frame;
|
|
64
|
+
let options: HeatmapChartOptions = { ...initial };
|
|
65
|
+
let selected: number | null = null;
|
|
66
|
+
|
|
67
|
+
// The weeks scroll inside the plot; the tooltip stays over it, unclipped.
|
|
68
|
+
const scroller = document.createElement('div');
|
|
69
|
+
scroller.dataset.mkHeatmapScroll = '';
|
|
70
|
+
plot.prepend(scroller);
|
|
71
|
+
scroller.append(canvas);
|
|
72
|
+
scope.add(() => scroller.remove());
|
|
73
|
+
|
|
74
|
+
const scale = document.createElement('div');
|
|
75
|
+
scale.dataset.mkHeatmapScale = '';
|
|
76
|
+
scale.setAttribute('aria-hidden', 'true');
|
|
77
|
+
root.insertBefore(scale, frame.legend);
|
|
78
|
+
scope.add(() => scale.remove());
|
|
79
|
+
|
|
80
|
+
const locale = () => options.locale ?? (document.documentElement.lang || 'es');
|
|
81
|
+
const format = (value: number) => (options.formatValue ? options.formatValue(value) : formatNumber(value));
|
|
82
|
+
const levels = () => Math.max(1, Math.min(8, Math.round(options.levels ?? 4)));
|
|
83
|
+
|
|
84
|
+
const modelOf = () => {
|
|
85
|
+
const values = new Map<string, number>();
|
|
86
|
+
const add = (key: string, value: number) => finite(value) && values.set(key, (values.get(key) ?? 0) + value);
|
|
87
|
+
if (Array.isArray(options.data)) for (const datum of options.data) add(dayKey(toDate(datum.date)), datum.value);
|
|
88
|
+
else if (options.data) for (const [key, value] of Object.entries(options.data)) add(dayKey(toDate(key)), value);
|
|
89
|
+
const weeks = Math.max(1, Math.min(106, Math.round(options.weeks ?? 53)));
|
|
90
|
+
const firstDay = Number.isInteger(options.firstDay) && options.firstDay! >= 0 && options.firstDay! <= 6 ? options.firstDay! : 1;
|
|
91
|
+
const end = toDate(options.end ?? new Date());
|
|
92
|
+
const start = startOfWeek(addDays(end, -(weeks - 1) * 7), firstDay);
|
|
93
|
+
const days: Array<{ key: string; date: Date; value: number; week: number; day: number }> = [];
|
|
94
|
+
for (let date = start, i = 0; date <= end; date = addDays(date, 1), i++) {
|
|
95
|
+
const key = dayKey(date);
|
|
96
|
+
days.push({ key, date, value: values.get(key) ?? 0, week: Math.floor(i / 7), day: i % 7 });
|
|
97
|
+
}
|
|
98
|
+
const max = Math.max(0, ...days.map((each) => each.value));
|
|
99
|
+
return { days, weeks: (days[days.length - 1]?.week ?? 0) + 1, firstDay, max, total: days.reduce((sum, each) => sum + each.value, 0) };
|
|
100
|
+
};
|
|
101
|
+
let model = modelOf();
|
|
102
|
+
const levelOf = (value: number) => (value <= 0 || !model.max ? 0 : Math.max(1, Math.ceil((value / model.max) * levels())));
|
|
103
|
+
const fillOf = (level: number) => (level === 0 ? 'var(--mk-heatmap-empty)' : `color-mix(in srgb, ${options.color ?? 'var(--mk-heatmap-color)'} ${Math.round(22 + (78 * level) / levels())}%, var(--mk-heatmap-empty))`);
|
|
104
|
+
|
|
105
|
+
let cells: SVGRectElement[] = [];
|
|
106
|
+
/** The first day drawn: the same days keep their cells. */
|
|
107
|
+
let builtFrom = '';
|
|
108
|
+
let geometry = { cell: 12, gap: 3, width: 0, height: 0 };
|
|
109
|
+
const longDate = () => safeFormat(locale(), { weekday: 'short', day: 'numeric', month: 'long', year: 'numeric' });
|
|
110
|
+
|
|
111
|
+
const build = (animate: boolean) => {
|
|
112
|
+
canvas.replaceChildren();
|
|
113
|
+
const available = Math.max(0, (plot.clientWidth || root.clientWidth || 0) - LEFT);
|
|
114
|
+
const gap = 3;
|
|
115
|
+
// Cells fill the width between 10 and 14 px; past that the weeks scroll.
|
|
116
|
+
const fit = available ? Math.floor(available / model.weeks - gap) : 12;
|
|
117
|
+
const cell = Math.max(10, Math.min(14, fit));
|
|
118
|
+
const width = LEFT + model.weeks * (cell + gap);
|
|
119
|
+
const height = TOP + 7 * (cell + gap);
|
|
120
|
+
geometry = { cell, gap, width, height };
|
|
121
|
+
canvas.setAttribute('viewBox', `0 0 ${width} ${height}`);
|
|
122
|
+
canvas.setAttribute('width', String(width));
|
|
123
|
+
canvas.setAttribute('height', String(height));
|
|
124
|
+
plot.style.setProperty('--mk-heatmap-width', `${width}px`);
|
|
125
|
+
|
|
126
|
+
// Month names over the week where each month starts; weekday names on every other row.
|
|
127
|
+
const months = svg('g', { 'data-mk-part': 'months' });
|
|
128
|
+
const monthName = safeFormat(locale(), { month: 'short' });
|
|
129
|
+
let last = -1;
|
|
130
|
+
for (const each of model.days) {
|
|
131
|
+
if (each.day !== 0) continue;
|
|
132
|
+
const month = addDays(each.date, 6).getMonth();
|
|
133
|
+
if (month === last) continue;
|
|
134
|
+
last = month;
|
|
135
|
+
if (each.week > model.weeks - 2 && model.weeks > 4) continue;
|
|
136
|
+
const text = svg('text', { x: LEFT + each.week * (cell + gap), y: 10 });
|
|
137
|
+
text.textContent = capitalize(monthName.format(addDays(each.date, 6)).replace('.', ''));
|
|
138
|
+
months.append(text);
|
|
139
|
+
}
|
|
140
|
+
const names = weekdayNames(locale(), model.firstDay, 'short');
|
|
141
|
+
const weekdays = svg('g', { 'data-mk-part': 'weekdays' });
|
|
142
|
+
[1, 3, 5].forEach((row) => {
|
|
143
|
+
const text = svg('text', { x: 0, y: round(TOP + row * (cell + gap) + cell / 2), 'dominant-baseline': 'middle' });
|
|
144
|
+
text.textContent = capitalize(names[row]!.replace('.', '')).slice(0, 3);
|
|
145
|
+
weekdays.append(text);
|
|
146
|
+
});
|
|
147
|
+
const grid = svg('g', { 'data-mk-part': 'cells' });
|
|
148
|
+
cells = model.days.map((each, i) => {
|
|
149
|
+
const rect = svg('rect', { x: LEFT + each.week * (cell + gap), y: TOP + each.day * (cell + gap), width: cell, height: cell, rx: 3, 'data-mk-index': i, 'data-mk-level': levelOf(each.value) });
|
|
150
|
+
rect.style.fill = fillOf(levelOf(each.value));
|
|
151
|
+
grid.append(rect);
|
|
152
|
+
return rect;
|
|
153
|
+
});
|
|
154
|
+
canvas.append(months, weekdays, grid);
|
|
155
|
+
builtFrom = model.days[0]?.key ?? '';
|
|
156
|
+
// The latest weeks in view when they scroll.
|
|
157
|
+
scroller.scrollLeft = scroller.scrollWidth;
|
|
158
|
+
if (animate && !reduced()) keep(gsap.fromTo(cells, { opacity: 0, scale: 0.3 }, { opacity: 1, scale: 1, duration: 0.35, ease: EASE.pop, clearProps: 'opacity,transform', stagger: (i: number) => Math.min(0.7, (model.days[i]!.week + model.days[i]!.day) * 0.008) }));
|
|
159
|
+
};
|
|
160
|
+
scope.add(() => plot.style.removeProperty('--mk-heatmap-width'));
|
|
161
|
+
|
|
162
|
+
const paintScale = () => {
|
|
163
|
+
scale.replaceChildren();
|
|
164
|
+
scale.hidden = options.legend === false || !model.days.length;
|
|
165
|
+
if (scale.hidden) return;
|
|
166
|
+
const less = document.createElement('span');
|
|
167
|
+
less.textContent = 'Menos';
|
|
168
|
+
scale.append(less);
|
|
169
|
+
for (let level = 0; level <= levels(); level++) {
|
|
170
|
+
const swatch = document.createElement('i');
|
|
171
|
+
swatch.style.background = fillOf(level);
|
|
172
|
+
swatch.addEventListener('pointerenter', () => {
|
|
173
|
+
for (const cell of cells) cell.toggleAttribute('data-mk-dim', cell.getAttribute('data-mk-level') !== String(level));
|
|
174
|
+
});
|
|
175
|
+
swatch.addEventListener('pointerleave', () => {
|
|
176
|
+
for (const cell of cells) cell.removeAttribute('data-mk-dim');
|
|
177
|
+
});
|
|
178
|
+
scale.append(swatch);
|
|
179
|
+
}
|
|
180
|
+
const more = document.createElement('span');
|
|
181
|
+
more.textContent = 'Más';
|
|
182
|
+
scale.append(more);
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const unit = () => options.unit ?? 'actividades';
|
|
186
|
+
const paintSelection = (announce: boolean) => {
|
|
187
|
+
cells.forEach((cell, i) => cell.toggleAttribute('data-mk-on', i === selected));
|
|
188
|
+
const each = selected === null ? null : model.days[selected];
|
|
189
|
+
if (!each) {
|
|
190
|
+
frame.tip(null);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
const { cell, gap } = geometry;
|
|
194
|
+
const x = LEFT + each.week * (cell + gap) + cell / 2 - scroller.scrollLeft;
|
|
195
|
+
const y = TOP + each.day * (cell + gap);
|
|
196
|
+
const title = capitalize(longDate().format(each.date));
|
|
197
|
+
frame.tip({ x, y, title, rows: [{ name: unit(), color: fillOf(Math.max(1, levelOf(each.value))), value: format(each.value) }] });
|
|
198
|
+
if (announce) frame.live.textContent = `${title}: ${format(each.value)} ${unit()}`;
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
const draw = (animate: boolean) => {
|
|
202
|
+
const name = options.label ?? 'Actividad diaria';
|
|
203
|
+
canvas.setAttribute('aria-label', model.days.length ? `${name}. ${model.weeks} semanas, ${format(model.total)} ${unit()} en total. Usa las flechas para recorrer los días.` : `${name}: ${options.emptyLabel ?? 'sin datos'}`);
|
|
204
|
+
// The days with something, for screen readers (a year of empty rows says nothing).
|
|
205
|
+
const active = model.days.filter((each) => each.value > 0);
|
|
206
|
+
frame.fillTable(`${name}: ${format(model.total)} ${unit()}`, ['Día', capitalize(unit())], active.map((each) => [capitalize(longDate().format(each.date)), format(each.value)]));
|
|
207
|
+
frame.fillLegend([], false);
|
|
208
|
+
paintScale();
|
|
209
|
+
if (selected !== null && selected >= model.days.length) selected = null;
|
|
210
|
+
const same = cells.length === model.days.length && cells.length > 0 && builtFrom === model.days[0]!.key;
|
|
211
|
+
if (same && animate) {
|
|
212
|
+
// New values over the same days: the colours crossfade (CSS).
|
|
213
|
+
cells.forEach((cell, i) => {
|
|
214
|
+
const level = levelOf(model.days[i]!.value);
|
|
215
|
+
cell.setAttribute('data-mk-level', String(level));
|
|
216
|
+
cell.style.fill = fillOf(level);
|
|
217
|
+
});
|
|
218
|
+
} else build(animate);
|
|
219
|
+
paintSelection(false);
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
const select = (index: number | null, announce = false) => {
|
|
223
|
+
if (scope.disposed) return;
|
|
224
|
+
const next = index === null || !model.days.length ? null : Math.max(0, Math.min(model.days.length - 1, Math.round(index)));
|
|
225
|
+
if (next === selected) return;
|
|
226
|
+
selected = next;
|
|
227
|
+
paintSelection(announce);
|
|
228
|
+
const each = selected === null ? null : model.days[selected]!;
|
|
229
|
+
options.onSelect?.(each?.key ?? null, each?.value ?? null);
|
|
230
|
+
root.dispatchEvent(new CustomEvent('mk:select', { bubbles: true, detail: { date: each?.key ?? null, value: each?.value ?? null } }));
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
scope.on(canvas, 'pointermove', (event) => {
|
|
234
|
+
const cell = (event.target as Element).closest('[data-mk-index]');
|
|
235
|
+
select(cell ? Number(cell.getAttribute('data-mk-index')) : null);
|
|
236
|
+
});
|
|
237
|
+
scope.on(canvas, 'pointerleave', (event) => {
|
|
238
|
+
if ((event as PointerEvent).pointerType === 'mouse') select(null);
|
|
239
|
+
});
|
|
240
|
+
scope.on(canvas, 'focus', () => {
|
|
241
|
+
if (selected === null && model.days.length) select(model.days.length - 1, true);
|
|
242
|
+
});
|
|
243
|
+
scope.on(canvas, 'blur', () => select(null));
|
|
244
|
+
scope.on(canvas, 'keydown', (event) => {
|
|
245
|
+
const key = (event as KeyboardEvent).key;
|
|
246
|
+
const at = selected ?? model.days.length - 1;
|
|
247
|
+
const moves: Record<string, number | null> = { ArrowUp: at - 1, ArrowDown: at + 1, ArrowLeft: at - 7, ArrowRight: at + 7, PageUp: at - 28, PageDown: at + 28, Home: 0, End: model.days.length - 1, Escape: null };
|
|
248
|
+
if (!(key in moves) || !model.days.length) return;
|
|
249
|
+
event.preventDefault();
|
|
250
|
+
select(moves[key]!, true);
|
|
251
|
+
// Keep the chosen day in view when the weeks scroll.
|
|
252
|
+
const cell = selected === null ? null : cells[selected];
|
|
253
|
+
if (cell) {
|
|
254
|
+
const x = Number(cell.getAttribute('x'));
|
|
255
|
+
if (x - scroller.scrollLeft < LEFT || x - scroller.scrollLeft > scroller.clientWidth - geometry.cell * 2) scroller.scrollLeft = x - scroller.clientWidth / 2;
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
onWidth(root, scope, () => {
|
|
259
|
+
build(false);
|
|
260
|
+
paintSelection(false);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
draw(true);
|
|
264
|
+
|
|
265
|
+
return {
|
|
266
|
+
update(next) {
|
|
267
|
+
if (scope.disposed) return;
|
|
268
|
+
options = { ...options, ...next };
|
|
269
|
+
model = modelOf();
|
|
270
|
+
draw(true);
|
|
271
|
+
},
|
|
272
|
+
select(date) {
|
|
273
|
+
const index = date === null ? -1 : model.days.findIndex((each) => each.key === date);
|
|
274
|
+
select(index < 0 ? null : index);
|
|
275
|
+
},
|
|
276
|
+
get selected() {
|
|
277
|
+
return selected === null ? null : (model.days[selected]?.key ?? null);
|
|
278
|
+
},
|
|
279
|
+
destroy: () => scope.dispose(),
|
|
280
|
+
};
|
|
281
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/* heatmap-chart — a week per column of 3 px rounded days, deepening from the track to the accent; needs chart.css.
|
|
2
|
+
The day under the pointer lifts with a ring; the "less … more" scale sits under it, to the right. */
|
|
3
|
+
.mk-heatmap-chart,
|
|
4
|
+
mk-heatmap-chart {
|
|
5
|
+
--mk-heatmap-color: var(--mk-accent);
|
|
6
|
+
--mk-heatmap-empty: var(--mk-surface-2);
|
|
7
|
+
display: grid;
|
|
8
|
+
gap: 0.6rem;
|
|
9
|
+
min-width: 0;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
[data-mk-heatmap-scroll] {
|
|
13
|
+
overflow-x: auto;
|
|
14
|
+
overflow-y: hidden;
|
|
15
|
+
padding-block: 4px;
|
|
16
|
+
scrollbar-width: thin;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
[data-mk-chart-type='heatmap'] [data-mk-chart-svg] {
|
|
20
|
+
width: var(--mk-heatmap-width, auto);
|
|
21
|
+
max-width: none;
|
|
22
|
+
overflow: visible;
|
|
23
|
+
cursor: pointer;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
[data-mk-chart-type='heatmap'] [data-mk-part='cells'] rect {
|
|
27
|
+
transform-box: fill-box;
|
|
28
|
+
transform-origin: center;
|
|
29
|
+
transition:
|
|
30
|
+
fill 0.35s ease,
|
|
31
|
+
opacity 0.2s ease,
|
|
32
|
+
transform 0.3s var(--mk-ease-swatch);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
[data-mk-chart-type='heatmap'] [data-mk-part='cells'] rect[data-mk-on] {
|
|
36
|
+
stroke: var(--mk-text);
|
|
37
|
+
stroke-width: 1.5;
|
|
38
|
+
transform: scale(1.3);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
[data-mk-chart-type='heatmap'] [data-mk-part='cells'] rect[data-mk-dim] {
|
|
42
|
+
opacity: 0.25;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
[data-mk-chart-type='heatmap'] [data-mk-part='months'] text,
|
|
46
|
+
[data-mk-chart-type='heatmap'] [data-mk-part='weekdays'] text {
|
|
47
|
+
font-size: 10px;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
[data-mk-heatmap-scale] {
|
|
51
|
+
display: flex;
|
|
52
|
+
align-items: center;
|
|
53
|
+
justify-content: flex-end;
|
|
54
|
+
gap: 3px;
|
|
55
|
+
color: var(--mk-muted);
|
|
56
|
+
font-size: 0.75rem;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
[data-mk-heatmap-scale][hidden] {
|
|
60
|
+
display: none;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
[data-mk-heatmap-scale] span {
|
|
64
|
+
margin-inline: 0.35rem;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
[data-mk-heatmap-scale] i {
|
|
68
|
+
width: 12px;
|
|
69
|
+
height: 12px;
|
|
70
|
+
border-radius: 3px;
|
|
71
|
+
cursor: pointer;
|
|
72
|
+
transition: transform 0.25s var(--mk-ease-swatch);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
@media (hover: hover) {
|
|
76
|
+
[data-mk-heatmap-scale] i:hover {
|
|
77
|
+
transform: scale(1.25);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
@media (prefers-reduced-motion: reduce) {
|
|
82
|
+
[data-mk-chart-type='heatmap'] [data-mk-part='cells'] rect,
|
|
83
|
+
[data-mk-heatmap-scale] i {
|
|
84
|
+
transition: none;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
@media (forced-colors: active) {
|
|
89
|
+
[data-mk-chart-type='heatmap'] [data-mk-part='cells'] rect {
|
|
90
|
+
stroke: CanvasText;
|
|
91
|
+
stroke-width: 0.5;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import type { HTMLAttributes } from 'react';
|
|
4
|
+
import { useChart } from '../chart/use-chart';
|
|
5
|
+
import { createHeatmapChart, type HeatmapChartInstance, type HeatmapChartOptions } from './engine';
|
|
6
|
+
|
|
7
|
+
export interface HeatmapChartProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onSelect' | 'children'>, HeatmapChartOptions {
|
|
8
|
+
instanceRef?: { current: HeatmapChartInstance | null };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function HeatmapChart({ data, end, weeks, firstDay, locale, levels, color, unit, label, legend, formatValue, emptyLabel, onSelect, instanceRef, className, ...props }: HeatmapChartProps) {
|
|
12
|
+
const ref = useChart(createHeatmapChart, { data, end, weeks, firstDay, locale, levels, color, unit, label, legend, emptyLabel }, { formatValue, onSelect }, instanceRef);
|
|
13
|
+
return <div {...props} ref={ref} className={['mk-heatmap-chart', className].filter(Boolean).join(' ')} />;
|
|
14
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { requestFrame } from '../../core/frame';
|
|
2
|
+
import { reduced } from '../../core/reduced';
|
|
2
3
|
import { createScope } from '../../core/scope';
|
|
3
4
|
import { inert, type Instance } from '../../core/types';
|
|
4
5
|
|
|
@@ -20,12 +21,16 @@ let sequence = 0;
|
|
|
20
21
|
/** Distance kept from the viewport edges. */
|
|
21
22
|
const EDGE = 12;
|
|
22
23
|
const GAP = 12;
|
|
24
|
+
/** Longest fold back into the trigger (the CSS one is .22 s); then it hides anyway. */
|
|
25
|
+
const FOLD_MS = 400;
|
|
23
26
|
|
|
24
27
|
/**
|
|
25
28
|
* Nonmodal preview: pointer, keyboard focus and tap all reveal the same panel.
|
|
26
29
|
* The panel stays inside the viewport: it flips to the other side when its own
|
|
27
30
|
* has no room and shifts sideways (--mk-hover-shift) away from the edges, again
|
|
28
31
|
* on scroll and resize. Opened from the keyboard it appears without the entrance.
|
|
32
|
+
* It grows out of the trigger's centre (--mk-hover-origin-x) and, when the
|
|
33
|
+
* pointer leaves, folds back into it (data-mk-closing, .22 s) before hiding.
|
|
29
34
|
*/
|
|
30
35
|
export function createHoverCard(root: HTMLElement | null, options: HoverCardOptions = {}): HoverCardInstance {
|
|
31
36
|
const trigger = root?.querySelector<HTMLElement>('[data-mk-hover-trigger]');
|
|
@@ -68,6 +73,9 @@ export function createHoverCard(root: HTMLElement | null, options: HoverCardOpti
|
|
|
68
73
|
if (left + box.width > view.width - EDGE) shift = view.width - EDGE - box.width - left;
|
|
69
74
|
if (left + shift < EDGE) shift = EDGE - left;
|
|
70
75
|
if (shift) root.style.setProperty('--mk-hover-shift', `${Math.round(shift)}px`);
|
|
76
|
+
// The panel grows from (and folds back into) the trigger's centre.
|
|
77
|
+
const target = trigger.getBoundingClientRect();
|
|
78
|
+
root.style.setProperty('--mk-hover-origin-x', `${Math.round(target.left + target.width / 2 - (left + shift))}px`);
|
|
71
79
|
};
|
|
72
80
|
let pending: (() => void) | null = null;
|
|
73
81
|
const schedulePlace = () => {
|
|
@@ -80,13 +88,34 @@ export function createHoverCard(root: HTMLElement | null, options: HoverCardOpti
|
|
|
80
88
|
scope.add(() => pending?.());
|
|
81
89
|
|
|
82
90
|
const cancel = () => { clearTimeout(timer); timer = undefined; };
|
|
91
|
+
// The fold back into the trigger: the panel stays until its animation ends (or FOLD_MS passes).
|
|
92
|
+
let folding: ReturnType<typeof setTimeout> | undefined;
|
|
93
|
+
const settleFold = () => {
|
|
94
|
+
clearTimeout(folding);
|
|
95
|
+
folding = undefined;
|
|
96
|
+
panel.removeEventListener('animationend', settleFold);
|
|
97
|
+
if (scope.disposed) return;
|
|
98
|
+
scope.attr(root, 'data-mk-closing', null);
|
|
99
|
+
if (!opened) scope.attr(panel, 'hidden', '');
|
|
100
|
+
};
|
|
101
|
+
scope.add(() => {
|
|
102
|
+
clearTimeout(folding);
|
|
103
|
+
panel.removeEventListener('animationend', settleFold);
|
|
104
|
+
});
|
|
83
105
|
const set = (value: boolean) => {
|
|
84
106
|
cancel();
|
|
85
107
|
if (scope.disposed) return;
|
|
86
108
|
const changed = value !== opened;
|
|
87
109
|
opened = value;
|
|
88
110
|
scope.attr(trigger, 'aria-expanded', String(value));
|
|
89
|
-
|
|
111
|
+
if (value) {
|
|
112
|
+
if (folding !== undefined) settleFold();
|
|
113
|
+
scope.attr(panel, 'hidden', null);
|
|
114
|
+
} else if (changed && !reduced() && root.dataset.mkInput !== 'keyboard' && !panel.hidden) {
|
|
115
|
+
scope.attr(root, 'data-mk-closing', '');
|
|
116
|
+
panel.addEventListener('animationend', settleFold);
|
|
117
|
+
folding = setTimeout(settleFold, FOLD_MS);
|
|
118
|
+
} else if (folding === undefined) scope.attr(panel, 'hidden', '');
|
|
90
119
|
scope.attr(root, 'data-mk-open', value ? '' : null);
|
|
91
120
|
if (value && changed) {
|
|
92
121
|
scope.attr(root, 'data-mk-input', pointerInput ? 'pointer' : 'keyboard');
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
.mk-hover-card, mk-hover-card { position: relative; display: inline-flex; }
|
|
2
2
|
.mk-hover-card [data-mk-hover-panel], mk-hover-card [data-mk-hover-panel] {
|
|
3
|
-
|
|
3
|
+
/* It grows from, and folds back into, the trigger's centre (the engine measures it). */
|
|
4
|
+
transform-origin: var(--mk-hover-origin-x, 50%) 100%;
|
|
4
5
|
position: absolute; z-index: 10; bottom: calc(100% + .75rem); left: var(--mk-hover-shift, 0);
|
|
5
6
|
width: min(19rem, calc(100vw - 1.5rem)); padding: 1.25rem; border-radius: var(--mk-radius-lg);
|
|
6
7
|
color: var(--mk-on-glass-inverse); background: var(--mk-glass-inverse); border: 0; border-radius: var(--mk-radius-lg, 24px);
|
|
@@ -10,10 +11,15 @@
|
|
|
10
11
|
}
|
|
11
12
|
/* Bridge over the gap so the pointer can travel into the panel. */
|
|
12
13
|
.mk-hover-card [data-mk-hover-panel]::after, mk-hover-card [data-mk-hover-panel]::after { content: ''; position: absolute; inset: -1rem 0; z-index: -1; }
|
|
13
|
-
:is(.mk-hover-card, mk-hover-card)[data-mk-side=bottom] [data-mk-hover-panel] { top: calc(100% + .75rem); bottom: auto; transform-origin: 50% 0; animation-name: mk-hover-card-down; }
|
|
14
|
+
:is(.mk-hover-card, mk-hover-card)[data-mk-side=bottom] [data-mk-hover-panel] { top: calc(100% + .75rem); bottom: auto; transform-origin: var(--mk-hover-origin-x, 50%) 0; animation-name: mk-hover-card-down; }
|
|
15
|
+
/* Leaving: it shrinks back into the trigger out of a blur, then the engine hides it. */
|
|
16
|
+
:is(.mk-hover-card, mk-hover-card)[data-mk-closing] [data-mk-hover-panel] { pointer-events: none; animation: mk-hover-card-out .22s cubic-bezier(.4,0,1,1) both; }
|
|
17
|
+
:is(.mk-hover-card, mk-hover-card)[data-mk-closing][data-mk-side=bottom] [data-mk-hover-panel] { animation-name: mk-hover-card-out-up; }
|
|
14
18
|
/* Opened from the keyboard: no entrance. */
|
|
15
19
|
:is(.mk-hover-card, mk-hover-card)[data-mk-input=keyboard] [data-mk-hover-panel] { animation: none; }
|
|
16
20
|
.mk-hover-card [hidden], mk-hover-card [hidden] { display: none !important; }
|
|
17
21
|
@keyframes mk-hover-card-in { from { opacity: 0; filter: blur(32px); transform: translateY(8px) scale(.7); } to { opacity: 1; filter: none; transform: none; } }
|
|
22
|
+
@keyframes mk-hover-card-out { to { opacity: 0; filter: blur(12px); transform: translateY(10px) scale(.3); } }
|
|
23
|
+
@keyframes mk-hover-card-out-up { to { opacity: 0; filter: blur(12px); transform: translateY(-10px) scale(.3); } }
|
|
18
24
|
@keyframes mk-hover-card-down { from { opacity: 0; filter: blur(32px); transform: translateY(-8px) scale(.7); } to { opacity: 1; filter: none; transform: none; } }
|
|
19
25
|
@media (prefers-reduced-motion: reduce) { .mk-hover-card [data-mk-hover-panel], mk-hover-card [data-mk-hover-panel] { animation: none; } }
|
|
@@ -8,7 +8,7 @@ import { createImagePreview, type ImagePreviewInstance } from './engine';
|
|
|
8
8
|
* Emits mk:open, mk:change ({index, previous}) and mk:close.
|
|
9
9
|
*/
|
|
10
10
|
export class MkImagePreviewElement extends MkElement<ImagePreviewInstance> {
|
|
11
|
-
static options: Record<string, OptionType> = { loop: 'boolean', labels: 'json' };
|
|
11
|
+
static options: Record<string, OptionType> = { loop: 'boolean', strip: 'boolean', labels: 'json' };
|
|
12
12
|
|
|
13
13
|
protected create(options: Record<string, unknown>) {
|
|
14
14
|
return createImagePreview(this, options);
|