@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,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
- scope.attr(panel, 'hidden', value ? null : '');
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
- transform-origin: 50% 100%;
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);