@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,3638 @@
|
|
|
1
|
+
import {
|
|
2
|
+
EASE,
|
|
3
|
+
Flip,
|
|
4
|
+
addDays,
|
|
5
|
+
bezier,
|
|
6
|
+
blurIn,
|
|
7
|
+
capitalize,
|
|
8
|
+
channels,
|
|
9
|
+
dayKey,
|
|
10
|
+
easeInCubic,
|
|
11
|
+
easeOut,
|
|
12
|
+
easeOutCubic,
|
|
13
|
+
eased,
|
|
14
|
+
flowIn,
|
|
15
|
+
flowInPalette,
|
|
16
|
+
gsap,
|
|
17
|
+
motionSpring,
|
|
18
|
+
press,
|
|
19
|
+
registerMotion,
|
|
20
|
+
revealFrom,
|
|
21
|
+
rollDigits,
|
|
22
|
+
safeFormat,
|
|
23
|
+
splitGraphemes,
|
|
24
|
+
splitWords,
|
|
25
|
+
spring,
|
|
26
|
+
springEase,
|
|
27
|
+
startOfWeek,
|
|
28
|
+
toDate,
|
|
29
|
+
weekdayNames
|
|
30
|
+
} from "./chunk-A6QQCF6V.js";
|
|
31
|
+
import {
|
|
32
|
+
THEMES,
|
|
33
|
+
THEME_EVENT,
|
|
34
|
+
getTheme,
|
|
35
|
+
setTheme
|
|
36
|
+
} from "./chunk-RVSKEBIC.js";
|
|
37
|
+
import {
|
|
38
|
+
TIMING,
|
|
39
|
+
createScope,
|
|
40
|
+
entrances,
|
|
41
|
+
reduced,
|
|
42
|
+
subscribe,
|
|
43
|
+
whileVisible
|
|
44
|
+
} from "./chunk-DOOMULVV.js";
|
|
45
|
+
import {
|
|
46
|
+
inert,
|
|
47
|
+
warnOnce
|
|
48
|
+
} from "./chunk-TGV27ZWM.js";
|
|
49
|
+
|
|
50
|
+
// src/motion/chat.ts
|
|
51
|
+
var isUser = (bubble) => bubble.dataset.mkRole === "user" || bubble.classList.contains("user");
|
|
52
|
+
function enterMessages(bubbles, { textSelector = "[data-mk-text], .message-text" } = {}) {
|
|
53
|
+
if (!bubbles.length || reduced()) return null;
|
|
54
|
+
registerMotion();
|
|
55
|
+
const reverts = [];
|
|
56
|
+
const cleanup = () => {
|
|
57
|
+
for (const revert of reverts.splice(0)) revert();
|
|
58
|
+
};
|
|
59
|
+
const timeline = gsap.timeline({ onComplete: cleanup, onInterrupt: cleanup });
|
|
60
|
+
bubbles.forEach((bubble, index) => {
|
|
61
|
+
gsap.set(bubble, { transformOrigin: isUser(bubble) ? "100% 50%" : "0% 50%" });
|
|
62
|
+
timeline.fromTo(
|
|
63
|
+
bubble,
|
|
64
|
+
{ scale: 0.5, autoAlpha: 0, filter: "blur(24px)" },
|
|
65
|
+
{ scale: 1, autoAlpha: 1, filter: "blur(0px)", duration: TIMING.bubble, ease: EASE.signature, clearProps: "transform,opacity,visibility,filter" },
|
|
66
|
+
index * TIMING.bubbleStagger
|
|
67
|
+
);
|
|
68
|
+
const text = bubble.querySelector(textSelector);
|
|
69
|
+
if (!text) return;
|
|
70
|
+
const before = text.offsetHeight;
|
|
71
|
+
const split = splitWords(text);
|
|
72
|
+
const lineHeight = parseFloat(getComputedStyle(text).lineHeight) || 0;
|
|
73
|
+
let targets = split.parts;
|
|
74
|
+
if (!split.parts.length || text.offsetHeight - before > lineHeight / 2) {
|
|
75
|
+
split.revert();
|
|
76
|
+
targets = [text];
|
|
77
|
+
} else reverts.push(() => split.revert());
|
|
78
|
+
timeline.fromTo(
|
|
79
|
+
targets,
|
|
80
|
+
{ y: 16, autoAlpha: 0, filter: "blur(6px)" },
|
|
81
|
+
{
|
|
82
|
+
y: 0,
|
|
83
|
+
autoAlpha: 1,
|
|
84
|
+
filter: "blur(0px)",
|
|
85
|
+
duration: TIMING.word,
|
|
86
|
+
ease: "back.out(1)",
|
|
87
|
+
stagger: TIMING.wordStagger,
|
|
88
|
+
clearProps: "transform,opacity,visibility,filter"
|
|
89
|
+
},
|
|
90
|
+
index * TIMING.bubbleStagger
|
|
91
|
+
);
|
|
92
|
+
});
|
|
93
|
+
return timeline;
|
|
94
|
+
}
|
|
95
|
+
function fitComposer(field, min = 64) {
|
|
96
|
+
const from = field.offsetHeight;
|
|
97
|
+
field.style.height = "auto";
|
|
98
|
+
const to = Math.max(min, field.scrollHeight);
|
|
99
|
+
field.style.height = `${from}px`;
|
|
100
|
+
if (from === to || reduced()) {
|
|
101
|
+
field.style.height = `${to}px`;
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
registerMotion();
|
|
105
|
+
return gsap.to(field, { height: to, duration: TIMING.composer, ease: "power2.out", overwrite: "auto" });
|
|
106
|
+
}
|
|
107
|
+
function scrollToLatest(scroller) {
|
|
108
|
+
const top = scroller.scrollHeight - scroller.clientHeight;
|
|
109
|
+
if (reduced()) {
|
|
110
|
+
scroller.scrollTop = top;
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
registerMotion();
|
|
114
|
+
const state = { top: scroller.scrollTop };
|
|
115
|
+
return gsap.to(state, {
|
|
116
|
+
top,
|
|
117
|
+
duration: TIMING.chatScroll,
|
|
118
|
+
ease: EASE.chatScroll,
|
|
119
|
+
overwrite: "auto",
|
|
120
|
+
onUpdate: () => {
|
|
121
|
+
scroller.scrollTop = state.top;
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// src/components/theme-picker/engine.ts
|
|
127
|
+
function createThemePicker(root, options = {}) {
|
|
128
|
+
if (!root) {
|
|
129
|
+
warnOnce("theme-picker: missing root");
|
|
130
|
+
return inert({ select() {
|
|
131
|
+
} });
|
|
132
|
+
}
|
|
133
|
+
registerMotion();
|
|
134
|
+
const scope = createScope();
|
|
135
|
+
const ctx = gsap.context(() => {
|
|
136
|
+
});
|
|
137
|
+
const generated = [];
|
|
138
|
+
let swatches = [...root.querySelectorAll("[data-mk-swatch]")];
|
|
139
|
+
if (!swatches.length) {
|
|
140
|
+
const byId = new Map(THEMES.map((theme) => [theme.id, theme]));
|
|
141
|
+
const ids = options.themes ?? THEMES.filter((theme) => options.showHidden || !theme.hidden).map((theme) => theme.id);
|
|
142
|
+
for (const id of ids) {
|
|
143
|
+
const theme = byId.get(id);
|
|
144
|
+
if (!theme) {
|
|
145
|
+
warnOnce(`theme-picker: unknown theme "${id}"`);
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
const button = document.createElement("button");
|
|
149
|
+
button.type = "button";
|
|
150
|
+
button.className = "mk-swatch";
|
|
151
|
+
button.dataset.mkSwatch = theme.id;
|
|
152
|
+
button.title = theme.name;
|
|
153
|
+
button.setAttribute("aria-label", theme.name);
|
|
154
|
+
button.style.setProperty("--mk-swatch", theme.seed);
|
|
155
|
+
root.append(button);
|
|
156
|
+
generated.push(button);
|
|
157
|
+
}
|
|
158
|
+
swatches = generated;
|
|
159
|
+
}
|
|
160
|
+
scope.attr(root, "role", "group");
|
|
161
|
+
if (!root.hasAttribute("aria-label")) scope.attr(root, "aria-label", options.labels?.group ?? "Tema");
|
|
162
|
+
const sync = () => {
|
|
163
|
+
const current = getTheme().id;
|
|
164
|
+
for (const swatch of swatches) scope.attr(swatch, "aria-pressed", String(swatch.dataset.mkSwatch === current));
|
|
165
|
+
};
|
|
166
|
+
const select = (id) => {
|
|
167
|
+
setTheme(id);
|
|
168
|
+
options.onChange?.(id);
|
|
169
|
+
};
|
|
170
|
+
scope.on(root, "click", (event) => {
|
|
171
|
+
const swatch = event.target?.closest("[data-mk-swatch]");
|
|
172
|
+
if (swatch && root.contains(swatch) && swatch.dataset.mkSwatch) select(swatch.dataset.mkSwatch);
|
|
173
|
+
});
|
|
174
|
+
scope.on(document, THEME_EVENT, sync);
|
|
175
|
+
sync();
|
|
176
|
+
if (entrances()) ctx.add(() => flowInPalette(swatches));
|
|
177
|
+
root.dataset.mkReady = "";
|
|
178
|
+
return {
|
|
179
|
+
select,
|
|
180
|
+
destroy() {
|
|
181
|
+
scope.dispose();
|
|
182
|
+
ctx.revert();
|
|
183
|
+
for (const button of generated.splice(0)) button.remove();
|
|
184
|
+
delete root.dataset.mkReady;
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// src/components/toast/store.ts
|
|
190
|
+
var TOAST_LIMIT = 5;
|
|
191
|
+
var items = [];
|
|
192
|
+
var nextId = 1;
|
|
193
|
+
var listeners = /* @__PURE__ */ new Set();
|
|
194
|
+
var emit = () => {
|
|
195
|
+
for (const listener of listeners) listener();
|
|
196
|
+
};
|
|
197
|
+
var getToasts = () => items;
|
|
198
|
+
function subscribeToasts(listener) {
|
|
199
|
+
listeners.add(listener);
|
|
200
|
+
return () => {
|
|
201
|
+
listeners.delete(listener);
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
function show(message, { origin = null, duration = 4e3, tone = "success", action, icon = true } = {}) {
|
|
205
|
+
const id = nextId++;
|
|
206
|
+
const live = items.filter((item) => !item.closing);
|
|
207
|
+
const extra = new Set(live.slice(0, Math.max(0, live.length - TOAST_LIMIT + 1)).map((item) => item.id));
|
|
208
|
+
items = [
|
|
209
|
+
...items.map((item) => extra.has(item.id) ? { ...item, closing: true } : item),
|
|
210
|
+
{ id, message, origin, duration, tone, action: action ?? null, icon, closing: false }
|
|
211
|
+
];
|
|
212
|
+
emit();
|
|
213
|
+
return id;
|
|
214
|
+
}
|
|
215
|
+
function dismissToast(id) {
|
|
216
|
+
const item = items.find((entry) => entry.id === id);
|
|
217
|
+
if (!item || item.closing) return;
|
|
218
|
+
items = items.map((entry) => entry.id === id ? { ...entry, closing: true } : entry);
|
|
219
|
+
emit();
|
|
220
|
+
}
|
|
221
|
+
function removeToast(id) {
|
|
222
|
+
toastTimers.clear(id);
|
|
223
|
+
items = items.filter((entry) => entry.id !== id);
|
|
224
|
+
emit();
|
|
225
|
+
}
|
|
226
|
+
function clearToasts() {
|
|
227
|
+
for (const item of items) toastTimers.clear(item.id);
|
|
228
|
+
items = [];
|
|
229
|
+
emit();
|
|
230
|
+
}
|
|
231
|
+
var toast = Object.assign(show, { dismiss: dismissToast, clear: clearToasts });
|
|
232
|
+
var running = /* @__PURE__ */ new Map();
|
|
233
|
+
var toastTimers = {
|
|
234
|
+
start(id, duration) {
|
|
235
|
+
if (duration <= 0 || running.has(id)) return;
|
|
236
|
+
running.set(id, { handle: setTimeout(() => dismissToast(id), duration), remaining: duration, started: Date.now() });
|
|
237
|
+
},
|
|
238
|
+
pause(id) {
|
|
239
|
+
const timer = running.get(id);
|
|
240
|
+
if (!timer?.handle) return;
|
|
241
|
+
clearTimeout(timer.handle);
|
|
242
|
+
timer.remaining = Math.max(0, timer.remaining - (Date.now() - timer.started));
|
|
243
|
+
timer.handle = null;
|
|
244
|
+
},
|
|
245
|
+
resume(id) {
|
|
246
|
+
const timer = running.get(id);
|
|
247
|
+
if (!timer || timer.handle) return;
|
|
248
|
+
if (timer.remaining <= 0) {
|
|
249
|
+
dismissToast(id);
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
timer.started = Date.now();
|
|
253
|
+
timer.handle = setTimeout(() => dismissToast(id), timer.remaining);
|
|
254
|
+
},
|
|
255
|
+
clear(id) {
|
|
256
|
+
const timer = running.get(id);
|
|
257
|
+
if (timer?.handle) clearTimeout(timer.handle);
|
|
258
|
+
running.delete(id);
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
// src/components/icons.ts
|
|
263
|
+
var ICON_PATHS = {
|
|
264
|
+
close: "M6 6l12 12M18 6L6 18",
|
|
265
|
+
expand: "M14 4h6v6M10 20H4v-6M20 4l-7 7M4 20l7-7",
|
|
266
|
+
shrink: "M4 14h6v6M20 10h-6V4M10 14l-7 7M14 10l7-7",
|
|
267
|
+
check: "M5 12.5l4.5 4.5L19 7.5",
|
|
268
|
+
alert: "M12 8v5M12 16.5v.01M10.3 3.9L2.6 17.3a2 2 0 001.7 3h15.4a2 2 0 001.7-3L13.7 3.9a2 2 0 00-3.4 0z",
|
|
269
|
+
info: "M12 11v6M12 7.5v.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z",
|
|
270
|
+
send: "M12 19V5M5.5 11.5L12 5l6.5 6.5",
|
|
271
|
+
mic: "M12 3a3 3 0 00-3 3v6a3 3 0 006 0V6a3 3 0 00-3-3zM5.5 11a6.5 6.5 0 0013 0M12 17.5V21",
|
|
272
|
+
sparkle: "M12 3l1.9 5.1L19 10l-5.1 1.9L12 17l-1.9-5.1L5 10l5.1-1.9z",
|
|
273
|
+
grip: "M9 6h.01M15 6h.01M9 12h.01M15 12h.01M9 18h.01M15 18h.01"
|
|
274
|
+
};
|
|
275
|
+
function iconSvg(name, size = 20) {
|
|
276
|
+
return `<svg aria-hidden="true" width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="${ICON_PATHS[name]}"/></svg>`;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// src/components/toast/engine.ts
|
|
280
|
+
var STACK = { peek: 16, scaleStep: 0.05, minScale: 0.7, maxVisible: 3, gap: 8, baseZ: 1e3 };
|
|
281
|
+
var DISTANCE = { enter: 12, enterBlur: 6, exit: 24, exitBlur: 4, fade: 120 };
|
|
282
|
+
var GESTURE = { swipe: 48, wheel: 80, expandDrag: 24, resistance: 0.15, wheelEndMs: 140 };
|
|
283
|
+
var TONE_ICONS = { success: "check", error: "alert", info: "info", neutral: null };
|
|
284
|
+
function layout(count, expanded, heights) {
|
|
285
|
+
let offset = 0;
|
|
286
|
+
return Array.from({ length: count }, (_, index) => {
|
|
287
|
+
if (expanded) {
|
|
288
|
+
const slot = { y: offset, scale: 1, opacity: 1, visible: true, zIndex: STACK.baseZ - index };
|
|
289
|
+
offset += (heights[index] ?? 0) + STACK.gap;
|
|
290
|
+
return slot;
|
|
291
|
+
}
|
|
292
|
+
const visible = index < STACK.maxVisible;
|
|
293
|
+
return {
|
|
294
|
+
y: index * STACK.peek,
|
|
295
|
+
scale: Math.max(STACK.minScale, 1 - index * STACK.scaleStep),
|
|
296
|
+
opacity: visible ? 1 : 0,
|
|
297
|
+
visible,
|
|
298
|
+
zIndex: STACK.baseZ - index
|
|
299
|
+
};
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
function revealLetters(element) {
|
|
303
|
+
const text = element.textContent ?? "";
|
|
304
|
+
const T = TIMING.toast;
|
|
305
|
+
if (!text || text.length > 64 || reduced()) return;
|
|
306
|
+
const letters = splitGraphemes(text, 64);
|
|
307
|
+
const original = element.innerHTML;
|
|
308
|
+
const width = element.getBoundingClientRect().width;
|
|
309
|
+
const fragment = document.createDocumentFragment();
|
|
310
|
+
const chars = [];
|
|
311
|
+
let word = null;
|
|
312
|
+
for (const letter of letters) {
|
|
313
|
+
if (!letter.trim()) {
|
|
314
|
+
word = null;
|
|
315
|
+
fragment.append(document.createTextNode(letter));
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
if (!word) {
|
|
319
|
+
word = document.createElement("span");
|
|
320
|
+
word.style.display = "inline-block";
|
|
321
|
+
word.style.whiteSpace = "nowrap";
|
|
322
|
+
fragment.append(word);
|
|
323
|
+
}
|
|
324
|
+
const char = document.createElement("span");
|
|
325
|
+
char.textContent = letter;
|
|
326
|
+
char.style.display = "inline-block";
|
|
327
|
+
word.append(char);
|
|
328
|
+
chars.push(char);
|
|
329
|
+
}
|
|
330
|
+
if (width) element.style.width = `${width}px`;
|
|
331
|
+
element.textContent = "";
|
|
332
|
+
element.append(fragment);
|
|
333
|
+
gsap.fromTo(
|
|
334
|
+
chars,
|
|
335
|
+
{ opacity: 0, yPercent: 40, filter: "blur(4px)" },
|
|
336
|
+
{
|
|
337
|
+
opacity: 1,
|
|
338
|
+
yPercent: 0,
|
|
339
|
+
filter: "blur(0px)",
|
|
340
|
+
duration: T.chars,
|
|
341
|
+
ease: easeOutCubic,
|
|
342
|
+
delay: (index) => T.charsDelay + index * T.charsStagger,
|
|
343
|
+
onComplete: () => {
|
|
344
|
+
element.innerHTML = original;
|
|
345
|
+
element.style.width = "";
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
function morphFrom(card, shell, body, origin) {
|
|
351
|
+
const T = TIMING.toast;
|
|
352
|
+
const box = shell.getBoundingClientRect();
|
|
353
|
+
const start = origin.getBoundingClientRect();
|
|
354
|
+
const look = getComputedStyle(origin);
|
|
355
|
+
const own = getComputedStyle(shell);
|
|
356
|
+
const endRadius = parseFloat(own.borderTopLeftRadius) || 20;
|
|
357
|
+
const startRadius = Math.min(parseFloat(look.borderTopLeftRadius) || 0, start.height / 2);
|
|
358
|
+
card.style.width = `${box.width}px`;
|
|
359
|
+
card.style.height = `${box.height}px`;
|
|
360
|
+
body.style.width = `${body.getBoundingClientRect().width}px`;
|
|
361
|
+
const towardX = start.left + start.width / 2 < innerWidth / 2 ? 1 : -1;
|
|
362
|
+
const towardY = start.top + start.height / 2 < innerHeight / 2 ? 1 : -1;
|
|
363
|
+
const offset = { x: start.left - box.left, y: start.top - box.top };
|
|
364
|
+
Object.assign(shell.style, {
|
|
365
|
+
position: "absolute",
|
|
366
|
+
top: "0px",
|
|
367
|
+
left: "0px",
|
|
368
|
+
width: `${start.width}px`,
|
|
369
|
+
height: `${start.height}px`,
|
|
370
|
+
borderRadius: `${startRadius}px`,
|
|
371
|
+
background: look.backgroundColor,
|
|
372
|
+
transform: `translate(${offset.x}px, ${offset.y}px)`,
|
|
373
|
+
transition: "none"
|
|
374
|
+
});
|
|
375
|
+
Object.assign(body.style, { transform: "scale(2)", transformOrigin: "center center", filter: "blur(8px)", opacity: "0" });
|
|
376
|
+
origin.style.transition = "opacity 160ms ease";
|
|
377
|
+
origin.style.opacity = "0";
|
|
378
|
+
void shell.offsetWidth;
|
|
379
|
+
shell.style.transition = `background ${T.morphColor}s ease-out ${T.morphColorDelay}s, box-shadow ${T.morphShadow}s ease-out ${T.morphShadowDelay}s`;
|
|
380
|
+
shell.style.background = "";
|
|
381
|
+
body.style.transition = `opacity ${T.morphColor}s ease-out ${T.morphColorDelay}s`;
|
|
382
|
+
body.style.opacity = "1";
|
|
383
|
+
const motion = channels(
|
|
384
|
+
{ x: offset.x, y: offset.y, width: start.width, height: start.height, radius: startRadius, scale: 2, blur: 8 },
|
|
385
|
+
(name, value) => {
|
|
386
|
+
if (name === "x") offset.x = value;
|
|
387
|
+
else if (name === "y") offset.y = value;
|
|
388
|
+
else if (name === "width") shell.style.width = `${value}px`;
|
|
389
|
+
else if (name === "height") shell.style.height = `${value}px`;
|
|
390
|
+
else if (name === "radius") shell.style.borderRadius = `${value}px`;
|
|
391
|
+
else if (name === "scale") body.style.transform = `scale(${value})`;
|
|
392
|
+
else if (name === "blur") body.style.filter = `blur(${value}px)`;
|
|
393
|
+
if (name === "x" || name === "y") shell.style.transform = `translate(${offset.x}px, ${offset.y}px)`;
|
|
394
|
+
}
|
|
395
|
+
);
|
|
396
|
+
void motion.animate(
|
|
397
|
+
{ x: 0, y: 0, width: box.width, height: box.height, radius: endRadius, scale: 1, blur: 0 },
|
|
398
|
+
{
|
|
399
|
+
x: spring(144, 14, { velocity: 2400, direction: towardX }),
|
|
400
|
+
y: spring(144, 14, { velocity: 2400, direction: towardY }),
|
|
401
|
+
width: eased(T.morphSize, bezier(0.8, 0.3, 0.5, 0.8)),
|
|
402
|
+
height: eased(T.morphSize, bezier(0.8, 0.3, 0.5, 0.8)),
|
|
403
|
+
radius: eased(T.morphRadius, easeOut),
|
|
404
|
+
scale: eased(T.morphContent, easeOut),
|
|
405
|
+
blur: eased(T.morphContent, easeOut)
|
|
406
|
+
}
|
|
407
|
+
);
|
|
408
|
+
let settled = false;
|
|
409
|
+
const settle = () => {
|
|
410
|
+
if (settled) return;
|
|
411
|
+
settled = true;
|
|
412
|
+
clearTimeout(timer);
|
|
413
|
+
motion.stop();
|
|
414
|
+
for (const name of ["position", "top", "left", "width", "height", "border-radius", "background", "transform", "transition"]) shell.style.removeProperty(name);
|
|
415
|
+
card.style.removeProperty("width");
|
|
416
|
+
card.style.removeProperty("height");
|
|
417
|
+
for (const name of ["transform", "transform-origin", "filter", "opacity", "width", "transition"]) body.style.removeProperty(name);
|
|
418
|
+
origin.style.transition = "opacity 300ms ease";
|
|
419
|
+
origin.style.opacity = "";
|
|
420
|
+
setTimeout(() => {
|
|
421
|
+
origin.style.transition = "";
|
|
422
|
+
}, 320);
|
|
423
|
+
};
|
|
424
|
+
const timer = setTimeout(settle, T.safetyMs);
|
|
425
|
+
return settle;
|
|
426
|
+
}
|
|
427
|
+
function buildCard(item, dismissLabel) {
|
|
428
|
+
const card = document.createElement("div");
|
|
429
|
+
card.className = "mk-toast";
|
|
430
|
+
card.dataset.mkToast = String(item.id);
|
|
431
|
+
card.dataset.mkTone = item.tone;
|
|
432
|
+
const shell = document.createElement("div");
|
|
433
|
+
shell.className = "mk-toast__shell";
|
|
434
|
+
const body = document.createElement("div");
|
|
435
|
+
body.className = "mk-toast__body";
|
|
436
|
+
const icon = item.icon ? TONE_ICONS[item.tone] : null;
|
|
437
|
+
if (icon) {
|
|
438
|
+
const holder = document.createElement("span");
|
|
439
|
+
holder.className = "mk-toast__icon";
|
|
440
|
+
holder.innerHTML = iconSvg(icon, 18);
|
|
441
|
+
body.append(holder);
|
|
442
|
+
}
|
|
443
|
+
const message = document.createElement("span");
|
|
444
|
+
message.className = "mk-toast__message";
|
|
445
|
+
message.textContent = item.message;
|
|
446
|
+
body.append(message);
|
|
447
|
+
if (item.action) {
|
|
448
|
+
const action = document.createElement("button");
|
|
449
|
+
action.type = "button";
|
|
450
|
+
action.className = "mk-toast__action";
|
|
451
|
+
action.dataset.mkToastAction = "";
|
|
452
|
+
action.textContent = item.action.label;
|
|
453
|
+
body.append(action);
|
|
454
|
+
}
|
|
455
|
+
const dismiss = document.createElement("button");
|
|
456
|
+
dismiss.type = "button";
|
|
457
|
+
dismiss.className = "mk-toast__dismiss";
|
|
458
|
+
dismiss.dataset.mkToastDismiss = "";
|
|
459
|
+
dismiss.setAttribute("aria-label", dismissLabel);
|
|
460
|
+
dismiss.innerHTML = iconSvg("close", 14);
|
|
461
|
+
body.append(dismiss);
|
|
462
|
+
shell.append(body);
|
|
463
|
+
card.append(shell);
|
|
464
|
+
return { card, shell, body, message };
|
|
465
|
+
}
|
|
466
|
+
function createToaster(host, options = {}) {
|
|
467
|
+
if (!host) {
|
|
468
|
+
warnOnce("toaster: missing host");
|
|
469
|
+
return inert();
|
|
470
|
+
}
|
|
471
|
+
registerMotion();
|
|
472
|
+
const dismissLabel = options.labels?.dismiss ?? "Cerrar";
|
|
473
|
+
const scope = createScope();
|
|
474
|
+
const cards = /* @__PURE__ */ new Map();
|
|
475
|
+
const entered = /* @__PURE__ */ new Set();
|
|
476
|
+
const leaving = /* @__PURE__ */ new Set();
|
|
477
|
+
const dragging = /* @__PURE__ */ new Set();
|
|
478
|
+
const settles = /* @__PURE__ */ new Map();
|
|
479
|
+
let expanded = false;
|
|
480
|
+
let collapse;
|
|
481
|
+
const popover = typeof host.showPopover === "function";
|
|
482
|
+
scope.attr(host, "role", "status");
|
|
483
|
+
scope.attr(host, "aria-live", "polite");
|
|
484
|
+
if (popover) scope.attr(host, "popover", "manual");
|
|
485
|
+
const live = () => getToasts().filter((item) => !item.closing).reverse().map((item) => ({ item, entry: cards.get(item.id) })).filter((pair) => pair.entry !== void 0);
|
|
486
|
+
const slotsFor = () => {
|
|
487
|
+
const entries = live();
|
|
488
|
+
return { entries, slots: layout(entries.length, expanded, entries.map(({ entry }) => entry.card.offsetHeight)) };
|
|
489
|
+
};
|
|
490
|
+
const relayout = (duration = TIMING.toast.relayout, ease = easeOutCubic, stagger = 0) => {
|
|
491
|
+
const { entries, slots } = slotsFor();
|
|
492
|
+
entries.forEach(({ item, entry }, index) => {
|
|
493
|
+
const slot = slots[index];
|
|
494
|
+
if (!slot) return;
|
|
495
|
+
entry.card.style.zIndex = String(slot.zIndex);
|
|
496
|
+
entry.card.style.pointerEvents = slot.visible ? "auto" : "none";
|
|
497
|
+
entry.card.setAttribute("aria-hidden", slot.visible ? "false" : "true");
|
|
498
|
+
if (dragging.has(item.id) || !entered.has(item.id)) return;
|
|
499
|
+
gsap.to(entry.card, {
|
|
500
|
+
xPercent: -50,
|
|
501
|
+
y: slot.y,
|
|
502
|
+
scale: slot.scale,
|
|
503
|
+
opacity: slot.opacity,
|
|
504
|
+
duration: reduced() ? 0 : duration,
|
|
505
|
+
ease,
|
|
506
|
+
delay: stagger * index,
|
|
507
|
+
overwrite: "auto"
|
|
508
|
+
});
|
|
509
|
+
});
|
|
510
|
+
};
|
|
511
|
+
const restingY = (id) => {
|
|
512
|
+
const { entries, slots } = slotsFor();
|
|
513
|
+
return slots[entries.findIndex(({ item }) => item.id === id)]?.y ?? 0;
|
|
514
|
+
};
|
|
515
|
+
const openDeck = () => {
|
|
516
|
+
clearTimeout(collapse);
|
|
517
|
+
if (expanded) return;
|
|
518
|
+
expanded = true;
|
|
519
|
+
for (const { item } of live()) toastTimers.pause(item.id);
|
|
520
|
+
relayout(TIMING.toast.expand, springEase(0.55), TIMING.toast.expandStagger);
|
|
521
|
+
};
|
|
522
|
+
const closeDeck = () => {
|
|
523
|
+
clearTimeout(collapse);
|
|
524
|
+
collapse = setTimeout(() => {
|
|
525
|
+
if (!expanded) return;
|
|
526
|
+
expanded = false;
|
|
527
|
+
for (const { item } of live()) toastTimers.resume(item.id);
|
|
528
|
+
relayout(TIMING.toast.expand, springEase(0.55), TIMING.toast.expandStagger);
|
|
529
|
+
}, TIMING.toast.collapseDelayMs);
|
|
530
|
+
};
|
|
531
|
+
const showLayer = (visible) => {
|
|
532
|
+
if (!popover) return;
|
|
533
|
+
try {
|
|
534
|
+
if (host.matches(":popover-open")) host.hidePopover();
|
|
535
|
+
if (visible) host.showPopover();
|
|
536
|
+
} catch {
|
|
537
|
+
}
|
|
538
|
+
};
|
|
539
|
+
const attachGestures = (item, card) => {
|
|
540
|
+
let start = null;
|
|
541
|
+
let wheel = null;
|
|
542
|
+
const move = (offset) => gsap.set(card, { y: restingY(item.id) + offset, opacity: Math.max(0, 1 + Math.min(0, offset) / DISTANCE.fade) });
|
|
543
|
+
const cancel = () => {
|
|
544
|
+
dragging.delete(item.id);
|
|
545
|
+
gsap.to(card, { y: restingY(item.id), opacity: 1, duration: 0.25, ease: easeOutCubic });
|
|
546
|
+
if (!expanded) toastTimers.resume(item.id);
|
|
547
|
+
};
|
|
548
|
+
card.addEventListener("mouseenter", openDeck);
|
|
549
|
+
card.addEventListener("mouseleave", closeDeck);
|
|
550
|
+
card.addEventListener("pointerdown", (event) => {
|
|
551
|
+
if (event.button !== 0 || event.target.closest('button, a, input, textarea, select, [role="button"]')) return;
|
|
552
|
+
start = { y: event.clientY, type: event.pointerType };
|
|
553
|
+
card.setPointerCapture?.(event.pointerId);
|
|
554
|
+
dragging.add(item.id);
|
|
555
|
+
gsap.killTweensOf(card);
|
|
556
|
+
toastTimers.pause(item.id);
|
|
557
|
+
});
|
|
558
|
+
card.addEventListener("pointermove", (event) => {
|
|
559
|
+
if (!start) return;
|
|
560
|
+
const dy = event.clientY - start.y;
|
|
561
|
+
move(dy < 0 ? dy : start.type === "touch" ? dy * GESTURE.resistance : 0);
|
|
562
|
+
});
|
|
563
|
+
const up = (event) => {
|
|
564
|
+
if (!start) return;
|
|
565
|
+
const dy = event.clientY - start.y;
|
|
566
|
+
const type = start.type;
|
|
567
|
+
start = null;
|
|
568
|
+
if (dy < -GESTURE.swipe) {
|
|
569
|
+
dragging.delete(item.id);
|
|
570
|
+
dismissToast(item.id);
|
|
571
|
+
} else if (type === "touch" && dy > GESTURE.expandDrag) {
|
|
572
|
+
cancel();
|
|
573
|
+
if (expanded) closeDeck();
|
|
574
|
+
else openDeck();
|
|
575
|
+
} else cancel();
|
|
576
|
+
};
|
|
577
|
+
card.addEventListener("pointerup", up);
|
|
578
|
+
card.addEventListener("pointercancel", up);
|
|
579
|
+
card.addEventListener(
|
|
580
|
+
"wheel",
|
|
581
|
+
(event) => {
|
|
582
|
+
const delta = event.deltaMode === 1 ? event.deltaY * 16 : event.deltaMode === 2 ? event.deltaY * innerHeight : event.deltaY;
|
|
583
|
+
if (!wheel) {
|
|
584
|
+
wheel = { offset: 0, end: void 0 };
|
|
585
|
+
dragging.add(item.id);
|
|
586
|
+
toastTimers.pause(item.id);
|
|
587
|
+
}
|
|
588
|
+
wheel.offset = Math.min(0, wheel.offset - delta);
|
|
589
|
+
move(wheel.offset);
|
|
590
|
+
clearTimeout(wheel.end);
|
|
591
|
+
wheel.end = setTimeout(() => {
|
|
592
|
+
const offset = wheel?.offset ?? 0;
|
|
593
|
+
wheel = null;
|
|
594
|
+
if (offset < -GESTURE.wheel) {
|
|
595
|
+
dragging.delete(item.id);
|
|
596
|
+
dismissToast(item.id);
|
|
597
|
+
} else cancel();
|
|
598
|
+
}, GESTURE.wheelEndMs);
|
|
599
|
+
},
|
|
600
|
+
{ passive: true }
|
|
601
|
+
);
|
|
602
|
+
card.addEventListener("click", (event) => {
|
|
603
|
+
const target = event.target;
|
|
604
|
+
if (target.closest("[data-mk-toast-dismiss]")) dismissToast(item.id);
|
|
605
|
+
else if (target.closest("[data-mk-toast-action]")) {
|
|
606
|
+
item.action?.onClick();
|
|
607
|
+
dismissToast(item.id);
|
|
608
|
+
}
|
|
609
|
+
});
|
|
610
|
+
};
|
|
611
|
+
const finishCard = (id) => {
|
|
612
|
+
settles.get(id)?.();
|
|
613
|
+
settles.delete(id);
|
|
614
|
+
const entry = cards.get(id);
|
|
615
|
+
if (entry) {
|
|
616
|
+
gsap.killTweensOf(entry.card);
|
|
617
|
+
entry.card.remove();
|
|
618
|
+
}
|
|
619
|
+
cards.delete(id);
|
|
620
|
+
entered.delete(id);
|
|
621
|
+
leaving.delete(id);
|
|
622
|
+
dragging.delete(id);
|
|
623
|
+
};
|
|
624
|
+
const render = () => {
|
|
625
|
+
const list = getToasts();
|
|
626
|
+
const ids = new Set(list.map((item) => item.id));
|
|
627
|
+
for (const id of [...cards.keys()]) if (!ids.has(id)) finishCard(id);
|
|
628
|
+
for (const item of list) {
|
|
629
|
+
if (!cards.has(item.id) && !item.closing) {
|
|
630
|
+
const entry = buildCard(item, dismissLabel);
|
|
631
|
+
cards.set(item.id, entry);
|
|
632
|
+
host.append(entry.card);
|
|
633
|
+
gsap.set(entry.card, { xPercent: -50 });
|
|
634
|
+
attachGestures(item, entry.card);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
showLayer(list.some((item) => !item.closing) || leaving.size > 0);
|
|
638
|
+
for (const item of list) {
|
|
639
|
+
const entry = cards.get(item.id);
|
|
640
|
+
if (!entry) continue;
|
|
641
|
+
if (!entered.has(item.id) && !item.closing) {
|
|
642
|
+
entered.add(item.id);
|
|
643
|
+
toastTimers.start(item.id, item.duration);
|
|
644
|
+
relayout();
|
|
645
|
+
gsap.killTweensOf(entry.card);
|
|
646
|
+
gsap.set(entry.card, { xPercent: -50, y: restingY(item.id), scale: 1, opacity: 1 });
|
|
647
|
+
if (expanded) toastTimers.pause(item.id);
|
|
648
|
+
revealLetters(entry.message);
|
|
649
|
+
if (reduced()) continue;
|
|
650
|
+
if (item.origin?.isConnected) settles.set(item.id, morphFrom(entry.card, entry.shell, entry.body, item.origin));
|
|
651
|
+
else {
|
|
652
|
+
const y = restingY(item.id);
|
|
653
|
+
gsap.fromTo(
|
|
654
|
+
entry.card,
|
|
655
|
+
{ y: y - DISTANCE.enter, opacity: 0, filter: `blur(${DISTANCE.enterBlur}px)` },
|
|
656
|
+
{ y, opacity: 1, filter: "blur(0px)", duration: TIMING.toast.enter, ease: springEase(0.58, -3.5), clearProps: "filter" }
|
|
657
|
+
);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
if (item.closing && !leaving.has(item.id)) {
|
|
661
|
+
leaving.add(item.id);
|
|
662
|
+
toastTimers.clear(item.id);
|
|
663
|
+
entry.card.style.zIndex = String(STACK.baseZ + 1);
|
|
664
|
+
const done = () => {
|
|
665
|
+
finishCard(item.id);
|
|
666
|
+
removeToast(item.id);
|
|
667
|
+
};
|
|
668
|
+
if (reduced()) done();
|
|
669
|
+
else {
|
|
670
|
+
gsap.to(entry.card, {
|
|
671
|
+
y: Number(gsap.getProperty(entry.card, "y")) - DISTANCE.exit,
|
|
672
|
+
opacity: 0,
|
|
673
|
+
filter: `blur(${DISTANCE.exitBlur}px)`,
|
|
674
|
+
duration: TIMING.toast.exit,
|
|
675
|
+
ease: easeInCubic,
|
|
676
|
+
overwrite: "auto",
|
|
677
|
+
onComplete: done
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
relayout();
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
};
|
|
684
|
+
scope.add(subscribeToasts(render));
|
|
685
|
+
render();
|
|
686
|
+
host.dataset.mkReady = "";
|
|
687
|
+
return {
|
|
688
|
+
destroy() {
|
|
689
|
+
clearTimeout(collapse);
|
|
690
|
+
for (const id of [...cards.keys()]) finishCard(id);
|
|
691
|
+
showLayer(false);
|
|
692
|
+
scope.dispose();
|
|
693
|
+
delete host.dataset.mkReady;
|
|
694
|
+
}
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// src/components/chat/engine.ts
|
|
699
|
+
var MESSAGE = "[data-mk-message]";
|
|
700
|
+
var PENDING_ID = "mk-chat-pending";
|
|
701
|
+
var part = (root, name) => root.querySelector(`[data-mk-part="${name}"]`);
|
|
702
|
+
var capturePending = (placeholder) => {
|
|
703
|
+
registerMotion();
|
|
704
|
+
return placeholder ? Flip.getState(placeholder) : null;
|
|
705
|
+
};
|
|
706
|
+
function morphPendingToReply(state, reply, scroller) {
|
|
707
|
+
registerMotion();
|
|
708
|
+
if (!state || reduced()) {
|
|
709
|
+
if (!reduced()) enterMessages([reply]);
|
|
710
|
+
return null;
|
|
711
|
+
}
|
|
712
|
+
const text = reply.querySelector("[data-mk-text], .message-text");
|
|
713
|
+
if (text) text.style.width = `${text.getBoundingClientRect().width}px`;
|
|
714
|
+
reply.setAttribute("data-flip-id", PENDING_ID);
|
|
715
|
+
const words = text && !text.querySelector("table, pre") ? splitWords(text) : null;
|
|
716
|
+
if (words?.parts.length) {
|
|
717
|
+
gsap.fromTo(
|
|
718
|
+
words.parts,
|
|
719
|
+
{ y: 16, autoAlpha: 0, filter: "blur(6px)" },
|
|
720
|
+
{
|
|
721
|
+
y: 0,
|
|
722
|
+
autoAlpha: 1,
|
|
723
|
+
filter: "blur(0px)",
|
|
724
|
+
duration: TIMING.word,
|
|
725
|
+
ease: "back.out(1)",
|
|
726
|
+
stagger: TIMING.wordStagger,
|
|
727
|
+
clearProps: "transform,opacity,visibility,filter",
|
|
728
|
+
onComplete: () => words.revert()
|
|
729
|
+
}
|
|
730
|
+
);
|
|
731
|
+
}
|
|
732
|
+
return Flip.from(state, {
|
|
733
|
+
targets: reply,
|
|
734
|
+
duration: TIMING.bubble,
|
|
735
|
+
scale: false,
|
|
736
|
+
ease: EASE.signature,
|
|
737
|
+
onUpdate: () => {
|
|
738
|
+
if (scroller) scroller.scrollTop = scroller.scrollHeight;
|
|
739
|
+
},
|
|
740
|
+
onComplete: () => {
|
|
741
|
+
reply.removeAttribute("data-flip-id");
|
|
742
|
+
if (text) text.style.width = "";
|
|
743
|
+
}
|
|
744
|
+
});
|
|
745
|
+
}
|
|
746
|
+
function chatMessage(role, text) {
|
|
747
|
+
const message = document.createElement("div");
|
|
748
|
+
message.dataset.mkMessage = "";
|
|
749
|
+
message.dataset.mkRole = role;
|
|
750
|
+
const body = document.createElement("span");
|
|
751
|
+
body.dataset.mkText = "";
|
|
752
|
+
body.textContent = text;
|
|
753
|
+
message.append(body);
|
|
754
|
+
return message;
|
|
755
|
+
}
|
|
756
|
+
function createChat(root, options = {}) {
|
|
757
|
+
const messages = root ? part(root, "messages") : null;
|
|
758
|
+
if (!root || !messages) {
|
|
759
|
+
warnOnce('chat: needs a [data-mk-part="messages"] container');
|
|
760
|
+
return inert({
|
|
761
|
+
add: () => document.createElement("div"),
|
|
762
|
+
pending() {
|
|
763
|
+
},
|
|
764
|
+
reply: () => document.createElement("div"),
|
|
765
|
+
confirm() {
|
|
766
|
+
},
|
|
767
|
+
setRecording() {
|
|
768
|
+
},
|
|
769
|
+
scrollToLatest() {
|
|
770
|
+
},
|
|
771
|
+
adopt() {
|
|
772
|
+
},
|
|
773
|
+
messages: null
|
|
774
|
+
});
|
|
775
|
+
}
|
|
776
|
+
registerMotion();
|
|
777
|
+
const { clearOnSend = true } = options;
|
|
778
|
+
const thinking = options.labels?.thinking ?? "Pensando...";
|
|
779
|
+
const scope = createScope();
|
|
780
|
+
const composer = part(root, "composer");
|
|
781
|
+
const input = composer?.querySelector('[data-mk-part="input"], textarea') ?? null;
|
|
782
|
+
const mic = part(root, "mic");
|
|
783
|
+
const seen = /* @__PURE__ */ new WeakSet();
|
|
784
|
+
let placeholder = null;
|
|
785
|
+
let pendingTimer;
|
|
786
|
+
let waiting = false;
|
|
787
|
+
scope.attr(messages, "role", "log");
|
|
788
|
+
scope.attr(messages, "aria-live", "polite");
|
|
789
|
+
const existing = [...messages.querySelectorAll(MESSAGE)];
|
|
790
|
+
if (options.animateExisting) enterMessages(existing);
|
|
791
|
+
for (const message of existing) seen.add(message);
|
|
792
|
+
const observer = scope.observe(
|
|
793
|
+
new MutationObserver((records) => {
|
|
794
|
+
const fresh = [];
|
|
795
|
+
for (const record of records) {
|
|
796
|
+
for (const node of record.addedNodes) {
|
|
797
|
+
if (!(node instanceof HTMLElement) || !node.matches(MESSAGE) || seen.has(node)) continue;
|
|
798
|
+
seen.add(node);
|
|
799
|
+
fresh.push(node);
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
if (!fresh.length) return;
|
|
803
|
+
enterMessages(fresh);
|
|
804
|
+
scrollToLatest(messages);
|
|
805
|
+
})
|
|
806
|
+
);
|
|
807
|
+
observer.observe(messages, { childList: true });
|
|
808
|
+
const clearPending = () => {
|
|
809
|
+
clearTimeout(pendingTimer);
|
|
810
|
+
pendingTimer = void 0;
|
|
811
|
+
waiting = false;
|
|
812
|
+
};
|
|
813
|
+
const add = (role, text) => {
|
|
814
|
+
const message = chatMessage(role, text);
|
|
815
|
+
messages.append(message);
|
|
816
|
+
return message;
|
|
817
|
+
};
|
|
818
|
+
const pending = () => {
|
|
819
|
+
clearPending();
|
|
820
|
+
waiting = true;
|
|
821
|
+
pendingTimer = setTimeout(() => {
|
|
822
|
+
pendingTimer = void 0;
|
|
823
|
+
if (!waiting || placeholder) return;
|
|
824
|
+
placeholder = document.createElement("div");
|
|
825
|
+
placeholder.dataset.mkMessage = "";
|
|
826
|
+
placeholder.dataset.mkRole = "assistant";
|
|
827
|
+
placeholder.dataset.mkPending = "";
|
|
828
|
+
placeholder.setAttribute("data-flip-id", PENDING_ID);
|
|
829
|
+
placeholder.innerHTML = `<span class="mk-chat__thinking">${iconSvg("sparkle", 18)}<span></span></span>`;
|
|
830
|
+
placeholder.querySelector("span span").textContent = thinking;
|
|
831
|
+
seen.add(placeholder);
|
|
832
|
+
messages.append(placeholder);
|
|
833
|
+
enterMessages([placeholder]);
|
|
834
|
+
scrollToLatest(messages);
|
|
835
|
+
}, TIMING.chat.pendingDelayMs);
|
|
836
|
+
};
|
|
837
|
+
const reply = (content) => {
|
|
838
|
+
const message = typeof content === "string" ? chatMessage("assistant", content) : content;
|
|
839
|
+
clearPending();
|
|
840
|
+
seen.add(message);
|
|
841
|
+
const was = placeholder;
|
|
842
|
+
placeholder = null;
|
|
843
|
+
if (was?.isConnected) {
|
|
844
|
+
const state = capturePending(was);
|
|
845
|
+
was.replaceWith(message);
|
|
846
|
+
morphPendingToReply(state, message, messages);
|
|
847
|
+
} else {
|
|
848
|
+
if (!message.isConnected) messages.append(message);
|
|
849
|
+
enterMessages([message]);
|
|
850
|
+
scrollToLatest(messages);
|
|
851
|
+
}
|
|
852
|
+
return message;
|
|
853
|
+
};
|
|
854
|
+
const confirm = (bar) => {
|
|
855
|
+
if (reduced()) return;
|
|
856
|
+
flowIn(bar);
|
|
857
|
+
const words = splitWords(bar);
|
|
858
|
+
if (!words.parts.length) return;
|
|
859
|
+
gsap.fromTo(
|
|
860
|
+
words.parts,
|
|
861
|
+
{ y: 16, autoAlpha: 0, filter: "blur(6px)" },
|
|
862
|
+
{
|
|
863
|
+
y: 0,
|
|
864
|
+
autoAlpha: 1,
|
|
865
|
+
filter: "blur(0px)",
|
|
866
|
+
duration: TIMING.word,
|
|
867
|
+
ease: "back.out(1)",
|
|
868
|
+
stagger: TIMING.chat.confirmWordStagger,
|
|
869
|
+
clearProps: "transform,opacity,visibility,filter",
|
|
870
|
+
onComplete: () => words.revert()
|
|
871
|
+
}
|
|
872
|
+
);
|
|
873
|
+
};
|
|
874
|
+
const send = () => {
|
|
875
|
+
if (!input) return;
|
|
876
|
+
const text = input.value.trim();
|
|
877
|
+
if (!text) return;
|
|
878
|
+
const allowed = root.dispatchEvent(new CustomEvent("mk:send", { bubbles: true, cancelable: true, detail: { text } }));
|
|
879
|
+
if (!allowed) return;
|
|
880
|
+
options.onSend?.(text);
|
|
881
|
+
if (clearOnSend) {
|
|
882
|
+
input.value = "";
|
|
883
|
+
fitComposer(input);
|
|
884
|
+
}
|
|
885
|
+
};
|
|
886
|
+
if (input) {
|
|
887
|
+
scope.on(input, "input", () => fitComposer(input));
|
|
888
|
+
scope.on(input, "keydown", (event) => {
|
|
889
|
+
const key = event;
|
|
890
|
+
if (key.key !== "Enter" || key.shiftKey || key.isComposing) return;
|
|
891
|
+
event.preventDefault();
|
|
892
|
+
send();
|
|
893
|
+
});
|
|
894
|
+
}
|
|
895
|
+
if (composer) {
|
|
896
|
+
scope.on(composer, "submit", (event) => {
|
|
897
|
+
event.preventDefault();
|
|
898
|
+
send();
|
|
899
|
+
});
|
|
900
|
+
const sendButton = part(root, "send");
|
|
901
|
+
if (sendButton && !composer.matches("form")) scope.on(sendButton, "click", send);
|
|
902
|
+
}
|
|
903
|
+
scope.add(() => {
|
|
904
|
+
clearPending();
|
|
905
|
+
placeholder?.remove();
|
|
906
|
+
placeholder = null;
|
|
907
|
+
});
|
|
908
|
+
if (input) fitComposer(input);
|
|
909
|
+
root.dataset.mkReady = "";
|
|
910
|
+
return {
|
|
911
|
+
add,
|
|
912
|
+
pending,
|
|
913
|
+
reply,
|
|
914
|
+
confirm,
|
|
915
|
+
setRecording(on) {
|
|
916
|
+
if (mic) scope.attr(mic, "data-mk-recording", on ? "" : null);
|
|
917
|
+
},
|
|
918
|
+
scrollToLatest: () => scrollToLatest(messages),
|
|
919
|
+
adopt: (message) => seen.add(message),
|
|
920
|
+
messages,
|
|
921
|
+
destroy() {
|
|
922
|
+
scope.dispose();
|
|
923
|
+
if (input) input.style.removeProperty("height");
|
|
924
|
+
delete root.dataset.mkReady;
|
|
925
|
+
}
|
|
926
|
+
};
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
// src/components/nav/engine.ts
|
|
930
|
+
var NAV_STYLES = ["sidebar", "pills", "dock", "card", "rail", "wash", "magnify"];
|
|
931
|
+
var NAV_STYLE_ALIASES = { clasica: "sidebar", bonita: "pills", guapa: "card", tasks: "rail", melon: "wash", tunnel: "magnify" };
|
|
932
|
+
var toNavStyle = (value) => NAV_STYLES.includes(value) ? value : NAV_STYLE_ALIASES[value];
|
|
933
|
+
var ITEM = ".mk-nav__buttons .mk-nav__button";
|
|
934
|
+
var valueOf = (item) => item.dataset.mkValue ?? item.getAttribute("href") ?? void 0;
|
|
935
|
+
var read = (key) => {
|
|
936
|
+
try {
|
|
937
|
+
return localStorage.getItem(key);
|
|
938
|
+
} catch {
|
|
939
|
+
return null;
|
|
940
|
+
}
|
|
941
|
+
};
|
|
942
|
+
var write = (key, value) => {
|
|
943
|
+
try {
|
|
944
|
+
localStorage.setItem(key, value);
|
|
945
|
+
} catch {
|
|
946
|
+
}
|
|
947
|
+
};
|
|
948
|
+
function revealShell(nav) {
|
|
949
|
+
if (!entrances()) return null;
|
|
950
|
+
const shell = nav.closest(".mk-shell") ?? nav.parentElement;
|
|
951
|
+
if (!shell) return null;
|
|
952
|
+
const origin = nav.querySelector(".mk-nav__button--first") ?? nav.querySelector(ITEM) ?? nav;
|
|
953
|
+
return revealFrom(shell, { origin });
|
|
954
|
+
}
|
|
955
|
+
function createNav(root, options = {}) {
|
|
956
|
+
if (!root) {
|
|
957
|
+
warnOnce("nav: missing root");
|
|
958
|
+
return inert({ select() {
|
|
959
|
+
}, setStyle() {
|
|
960
|
+
}, setAccent() {
|
|
961
|
+
}, index: -1, style: "pills", items: [] });
|
|
962
|
+
}
|
|
963
|
+
const scope = createScope();
|
|
964
|
+
const items2 = () => [...root.querySelectorAll(ITEM)];
|
|
965
|
+
const known = [...NAV_STYLES, ...Object.keys(NAV_STYLE_ALIASES)];
|
|
966
|
+
const fromClass = toNavStyle(known.find((name) => root.classList.contains(`mk-nav--${name}`)));
|
|
967
|
+
const saved = options.storageKey ? read(options.storageKey) : null;
|
|
968
|
+
let style = toNavStyle(options.style) ?? toNavStyle(saved) ?? fromClass ?? "pills";
|
|
969
|
+
const paint = (accent2) => {
|
|
970
|
+
const classes = new Set(root.classList);
|
|
971
|
+
for (const name of known) classes.delete(`mk-nav--${name}`);
|
|
972
|
+
classes.add("mk-nav");
|
|
973
|
+
classes.add(`mk-nav--${style}`);
|
|
974
|
+
if (accent2) classes.add("mk-nav--accent");
|
|
975
|
+
else classes.delete("mk-nav--accent");
|
|
976
|
+
scope.attr(root, "class", [...classes].join(" "));
|
|
977
|
+
};
|
|
978
|
+
let accent = options.accent ?? root.classList.contains("mk-nav--accent");
|
|
979
|
+
paint(accent);
|
|
980
|
+
if (root.localName !== "nav") scope.attr(root, "role", "navigation");
|
|
981
|
+
if (!root.hasAttribute("aria-label") && !root.hasAttribute("aria-labelledby")) scope.attr(root, "aria-label", options.labels?.nav ?? "Principal");
|
|
982
|
+
const mark = (current) => {
|
|
983
|
+
for (const item of items2()) scope.attr(item, "aria-current", item === current ? "page" : null);
|
|
984
|
+
};
|
|
985
|
+
const find = (target) => {
|
|
986
|
+
const list = items2();
|
|
987
|
+
return typeof target === "number" ? list[target] : list.find((item) => valueOf(item) === target);
|
|
988
|
+
};
|
|
989
|
+
scope.on(root, "click", (event) => {
|
|
990
|
+
const item = event.target?.closest(ITEM);
|
|
991
|
+
if (!item || !root.contains(item)) return;
|
|
992
|
+
const index = items2().indexOf(item);
|
|
993
|
+
const value = valueOf(item);
|
|
994
|
+
const detail = { index, value, item, originalEvent: event };
|
|
995
|
+
if (!root.dispatchEvent(new CustomEvent("mk:navigate", { bubbles: true, cancelable: true, detail }))) {
|
|
996
|
+
event.preventDefault();
|
|
997
|
+
return;
|
|
998
|
+
}
|
|
999
|
+
mark(item);
|
|
1000
|
+
options.onSelect?.(index, value, item);
|
|
1001
|
+
});
|
|
1002
|
+
let reveal = null;
|
|
1003
|
+
if (options.reveal) reveal = revealShell(root);
|
|
1004
|
+
root.dataset.mkReady = "";
|
|
1005
|
+
return {
|
|
1006
|
+
select(target) {
|
|
1007
|
+
const item = find(target);
|
|
1008
|
+
if (!item) warnOnce(`nav: no item ${JSON.stringify(target)}`);
|
|
1009
|
+
else mark(item);
|
|
1010
|
+
},
|
|
1011
|
+
setStyle(next) {
|
|
1012
|
+
const resolved = toNavStyle(next);
|
|
1013
|
+
if (!resolved) {
|
|
1014
|
+
warnOnce(`nav: unknown style "${String(next)}" (use ${NAV_STYLES.join(", ")})`);
|
|
1015
|
+
return;
|
|
1016
|
+
}
|
|
1017
|
+
style = resolved;
|
|
1018
|
+
paint(accent);
|
|
1019
|
+
if (options.storageKey) write(options.storageKey, resolved);
|
|
1020
|
+
},
|
|
1021
|
+
setAccent(on) {
|
|
1022
|
+
accent = on;
|
|
1023
|
+
paint(accent);
|
|
1024
|
+
},
|
|
1025
|
+
get index() {
|
|
1026
|
+
return items2().findIndex((item) => item.getAttribute("aria-current") === "page");
|
|
1027
|
+
},
|
|
1028
|
+
get style() {
|
|
1029
|
+
return style;
|
|
1030
|
+
},
|
|
1031
|
+
get items() {
|
|
1032
|
+
return items2();
|
|
1033
|
+
},
|
|
1034
|
+
destroy() {
|
|
1035
|
+
reveal?.cancel();
|
|
1036
|
+
reveal = null;
|
|
1037
|
+
scope.dispose();
|
|
1038
|
+
delete root.dataset.mkReady;
|
|
1039
|
+
}
|
|
1040
|
+
};
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
// src/components/cursor/engine.ts
|
|
1044
|
+
var svg = (extra = "") => `<svg aria-hidden="true" width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"${extra}><path d="${ICON_PATHS.send}"/></svg>`;
|
|
1045
|
+
function backgroundTone(el) {
|
|
1046
|
+
if (!el) return "light";
|
|
1047
|
+
if (el.closest("img, video, picture, canvas")) return "dark";
|
|
1048
|
+
for (let node = el; node; node = node.parentElement) {
|
|
1049
|
+
const match = getComputedStyle(node).backgroundColor.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/);
|
|
1050
|
+
if (!match) continue;
|
|
1051
|
+
const alpha = match[4] === void 0 ? 1 : parseFloat(match[4]);
|
|
1052
|
+
if (alpha <= 0.1) continue;
|
|
1053
|
+
const luminance = (Number(match[1]) * 299 + Number(match[2]) * 587 + Number(match[3]) * 114) / 1e3;
|
|
1054
|
+
return luminance > TIMING.cursor.light ? "light" : "dark";
|
|
1055
|
+
}
|
|
1056
|
+
return "light";
|
|
1057
|
+
}
|
|
1058
|
+
function createCursor(host, options = {}) {
|
|
1059
|
+
const variant = options.variant ?? "chip";
|
|
1060
|
+
const idle = inert({ visible: false, label: "", position: { x: 0, y: 0 }, tone: "light", update() {
|
|
1061
|
+
} });
|
|
1062
|
+
if (!host) {
|
|
1063
|
+
warnOnce("cursor: missing element");
|
|
1064
|
+
return idle;
|
|
1065
|
+
}
|
|
1066
|
+
const coarse = typeof matchMedia === "function" && matchMedia("(pointer: coarse)").matches;
|
|
1067
|
+
if (coarse || reduced()) return idle;
|
|
1068
|
+
registerMotion();
|
|
1069
|
+
const scope = createScope();
|
|
1070
|
+
const t = TIMING.cursor;
|
|
1071
|
+
const zone = options.zone ?? (variant === "dot" ? host.parentElement ?? document.body : document.documentElement);
|
|
1072
|
+
scope.attr(host, "class", [host.getAttribute("class"), "mk-cursor", `mk-cursor--${variant}`].filter(Boolean).join(" "));
|
|
1073
|
+
scope.attr(host, "aria-hidden", "true");
|
|
1074
|
+
const pointer = { x: -100, y: -100, placed: false };
|
|
1075
|
+
const position = { x: -100, y: -100 };
|
|
1076
|
+
const velocity = { x: 0, y: 0 };
|
|
1077
|
+
let visible = false;
|
|
1078
|
+
let label = "";
|
|
1079
|
+
let tone = "light";
|
|
1080
|
+
const made = [];
|
|
1081
|
+
let chip = null;
|
|
1082
|
+
let chipLabel = null;
|
|
1083
|
+
let chipIcon = null;
|
|
1084
|
+
let word = null;
|
|
1085
|
+
const size = { width: 0, height: 0 };
|
|
1086
|
+
if (variant === "chip") {
|
|
1087
|
+
chip = document.createElement("span");
|
|
1088
|
+
chip.className = "mk-cursor__chip mk-glass";
|
|
1089
|
+
chip.innerHTML = '<span class="mk-glass__shadow" aria-hidden="true"></span><span class="mk-cursor__icon mk-icon"></span><span class="mk-cursor__label"></span>';
|
|
1090
|
+
chipIcon = chip.querySelector(".mk-cursor__icon");
|
|
1091
|
+
chipLabel = chip.querySelector(".mk-cursor__label");
|
|
1092
|
+
host.append(chip);
|
|
1093
|
+
made.push(chip);
|
|
1094
|
+
gsap.set(chip, { autoAlpha: 0, scale: 0.8 });
|
|
1095
|
+
} else {
|
|
1096
|
+
const arrows = options.arrows ?? "y";
|
|
1097
|
+
scope.attr(host, "data-mk-arrows", arrows);
|
|
1098
|
+
scope.attr(zone, "data-mk-cursor-zone", "");
|
|
1099
|
+
const dot = document.createElement("span");
|
|
1100
|
+
dot.className = "mk-cursor__dot";
|
|
1101
|
+
dot.innerHTML = arrows === "none" ? '<span class="mk-cursor__circle"></span><span class="mk-cursor__text"><span class="mk-cursor__word"></span></span>' : `<span class="mk-cursor__circle"></span><span class="mk-cursor__arrow mk-cursor__arrow--start">${svg()}</span><span class="mk-cursor__text"><span class="mk-cursor__word"></span></span><span class="mk-cursor__arrow mk-cursor__arrow--end">${svg()}</span>`;
|
|
1102
|
+
word = dot.querySelector(".mk-cursor__word");
|
|
1103
|
+
if (word) word.textContent = options.text ?? "";
|
|
1104
|
+
host.append(dot);
|
|
1105
|
+
made.push(dot);
|
|
1106
|
+
scope.attr(host, "data-mk-hidden", "");
|
|
1107
|
+
}
|
|
1108
|
+
const appear = motionSpring(...TIMING.springs.cursorAppear);
|
|
1109
|
+
const showChip = (next) => {
|
|
1110
|
+
if (!chip || !chipLabel || !chipIcon) return;
|
|
1111
|
+
const text = next?.dataset.mkCursor ?? "";
|
|
1112
|
+
if (text) {
|
|
1113
|
+
label = text;
|
|
1114
|
+
chipLabel.textContent = text;
|
|
1115
|
+
chipIcon.textContent = next?.dataset.mkCursorIcon ?? "";
|
|
1116
|
+
chip.classList.toggle("mk-cursor__chip--circle", next?.dataset.mkCursorShape === "circle");
|
|
1117
|
+
const box = chip.getBoundingClientRect();
|
|
1118
|
+
size.width = box.width;
|
|
1119
|
+
size.height = box.height;
|
|
1120
|
+
}
|
|
1121
|
+
if (Boolean(text) === visible) return;
|
|
1122
|
+
visible = Boolean(text);
|
|
1123
|
+
if (!visible) label = "";
|
|
1124
|
+
gsap.to(chip, {
|
|
1125
|
+
autoAlpha: visible ? 1 : 0,
|
|
1126
|
+
scale: visible ? 1 : 0.8,
|
|
1127
|
+
duration: appear.duration,
|
|
1128
|
+
ease: appear.ease,
|
|
1129
|
+
overwrite: "auto"
|
|
1130
|
+
});
|
|
1131
|
+
};
|
|
1132
|
+
let inZone = false;
|
|
1133
|
+
let overHide = false;
|
|
1134
|
+
const syncDot = () => scope.attr(host, "data-mk-hidden", inZone && !overHide ? null : "");
|
|
1135
|
+
const wordFor = (target) => {
|
|
1136
|
+
if (!word) return;
|
|
1137
|
+
const source = target?.closest("[data-mk-cursor]");
|
|
1138
|
+
word.textContent = source && zone.contains(source) ? source.dataset.mkCursor ?? "" : options.text ?? "";
|
|
1139
|
+
};
|
|
1140
|
+
const place = (event) => {
|
|
1141
|
+
if (event.pointerType === "touch") return;
|
|
1142
|
+
pointer.x = event.clientX;
|
|
1143
|
+
pointer.y = event.clientY;
|
|
1144
|
+
if (!pointer.placed) {
|
|
1145
|
+
pointer.placed = true;
|
|
1146
|
+
if (variant === "dot") {
|
|
1147
|
+
position.x = pointer.x;
|
|
1148
|
+
position.y = pointer.y;
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
};
|
|
1152
|
+
scope.on(window, "pointermove", (event) => place(event), { passive: true });
|
|
1153
|
+
const overTarget = variant === "chip" ? document : zone;
|
|
1154
|
+
scope.on(overTarget, "pointerover", (event) => {
|
|
1155
|
+
const target = event.target;
|
|
1156
|
+
if (variant === "chip") {
|
|
1157
|
+
const source = target?.closest("[data-mk-cursor]") ?? null;
|
|
1158
|
+
showChip(source && zone.contains(source) ? source : null);
|
|
1159
|
+
return;
|
|
1160
|
+
}
|
|
1161
|
+
overHide = Boolean(target?.closest("[data-mk-cursor-hide]"));
|
|
1162
|
+
wordFor(target);
|
|
1163
|
+
syncDot();
|
|
1164
|
+
});
|
|
1165
|
+
if (variant === "dot") {
|
|
1166
|
+
scope.on(zone, "pointerenter", (event) => {
|
|
1167
|
+
place(event);
|
|
1168
|
+
inZone = true;
|
|
1169
|
+
visible = true;
|
|
1170
|
+
syncDot();
|
|
1171
|
+
});
|
|
1172
|
+
scope.on(zone, "pointerleave", () => {
|
|
1173
|
+
inZone = false;
|
|
1174
|
+
visible = false;
|
|
1175
|
+
scope.attr(host, "data-mk-pressed", null);
|
|
1176
|
+
syncDot();
|
|
1177
|
+
});
|
|
1178
|
+
scope.on(zone, "pointerdown", () => scope.attr(host, "data-mk-pressed", ""));
|
|
1179
|
+
scope.on(window, "pointerup", () => scope.attr(host, "data-mk-pressed", null));
|
|
1180
|
+
}
|
|
1181
|
+
const [stiffness, damping] = TIMING.springs.cursorFollow;
|
|
1182
|
+
const update = (dt = 1) => {
|
|
1183
|
+
if (variant === "chip") {
|
|
1184
|
+
const halfW = size.width / 2 + t.gutter;
|
|
1185
|
+
const halfH = size.height / 2 + t.gutter;
|
|
1186
|
+
const tx = size.width ? Math.min(Math.max(pointer.x, halfW), innerWidth - halfW) : pointer.x;
|
|
1187
|
+
const ty = size.height ? Math.min(Math.max(pointer.y, halfH), innerHeight - halfH) : pointer.y;
|
|
1188
|
+
const steps = Math.max(1, Math.ceil(dt * 4));
|
|
1189
|
+
const h = dt / 60 / steps;
|
|
1190
|
+
for (let i = 0; i < steps; i++) {
|
|
1191
|
+
velocity.x += (stiffness * (tx - position.x) - damping * velocity.x) * h;
|
|
1192
|
+
velocity.y += (stiffness * (ty - position.y) - damping * velocity.y) * h;
|
|
1193
|
+
position.x += velocity.x * h;
|
|
1194
|
+
position.y += velocity.y * h;
|
|
1195
|
+
}
|
|
1196
|
+
if (visible && typeof document.elementFromPoint === "function") {
|
|
1197
|
+
tone = backgroundTone(document.elementFromPoint(pointer.x, pointer.y));
|
|
1198
|
+
if (chip && chip.dataset.mkTone !== tone) chip.dataset.mkTone = tone;
|
|
1199
|
+
}
|
|
1200
|
+
} else {
|
|
1201
|
+
const step = 1 - Math.pow(1 - t.dotLerp, dt);
|
|
1202
|
+
position.x += (pointer.x - position.x) * step;
|
|
1203
|
+
position.y += (pointer.y - position.y) * step;
|
|
1204
|
+
}
|
|
1205
|
+
host.style.transform = `translate3d(${position.x.toFixed(2)}px, ${position.y.toFixed(2)}px, 0) translate(-50%, -50%)`;
|
|
1206
|
+
};
|
|
1207
|
+
scope.add(subscribe(update));
|
|
1208
|
+
update(0);
|
|
1209
|
+
host.dataset.mkReady = "";
|
|
1210
|
+
return {
|
|
1211
|
+
get visible() {
|
|
1212
|
+
return visible;
|
|
1213
|
+
},
|
|
1214
|
+
get label() {
|
|
1215
|
+
return label;
|
|
1216
|
+
},
|
|
1217
|
+
get position() {
|
|
1218
|
+
return { ...position };
|
|
1219
|
+
},
|
|
1220
|
+
get tone() {
|
|
1221
|
+
return tone;
|
|
1222
|
+
},
|
|
1223
|
+
update,
|
|
1224
|
+
destroy() {
|
|
1225
|
+
if (chip) gsap.killTweensOf(chip);
|
|
1226
|
+
scope.dispose();
|
|
1227
|
+
for (const node of made.splice(0)) node.remove();
|
|
1228
|
+
host.style.removeProperty("transform");
|
|
1229
|
+
if (host.getAttribute("style") === "") host.removeAttribute("style");
|
|
1230
|
+
delete host.dataset.mkReady;
|
|
1231
|
+
}
|
|
1232
|
+
};
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
// src/components/island/engine.ts
|
|
1236
|
+
var sequence = 0;
|
|
1237
|
+
var FOCUSABLE = 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
|
|
1238
|
+
function createIsland(root, options = {}) {
|
|
1239
|
+
const shell = root?.querySelector(".mk-island__shell") ?? null;
|
|
1240
|
+
const panels = root ? [...root.querySelectorAll("[data-mk-island-panel]")] : [];
|
|
1241
|
+
if (!root || !shell || !panels.length) {
|
|
1242
|
+
warnOnce("island: needs .mk-island__shell and at least one [data-mk-island-panel]");
|
|
1243
|
+
return inert({ open() {
|
|
1244
|
+
}, close() {
|
|
1245
|
+
}, toggle() {
|
|
1246
|
+
}, ready() {
|
|
1247
|
+
}, active: null });
|
|
1248
|
+
}
|
|
1249
|
+
const t = TIMING.island;
|
|
1250
|
+
const { entrance = true, closeOnScroll = true, closeOnNavigate = true, focusOnOpen = true, onChange } = options;
|
|
1251
|
+
registerMotion();
|
|
1252
|
+
const scope = createScope();
|
|
1253
|
+
const toggles = [...root.querySelectorAll("[data-mk-island-toggle]")];
|
|
1254
|
+
const cta = root.querySelector(".mk-island__cta");
|
|
1255
|
+
const backdrops = [...root.querySelectorAll("[data-mk-island-close]")];
|
|
1256
|
+
let active = null;
|
|
1257
|
+
let trigger = null;
|
|
1258
|
+
let scrollStart = 0;
|
|
1259
|
+
let armed = false;
|
|
1260
|
+
let focusTimer;
|
|
1261
|
+
let entered = false;
|
|
1262
|
+
let entry = null;
|
|
1263
|
+
const flag = (el, name, on) => {
|
|
1264
|
+
if (el) scope.attr(el, name, on ? "" : null);
|
|
1265
|
+
};
|
|
1266
|
+
const sync = () => {
|
|
1267
|
+
for (const panel of panels) {
|
|
1268
|
+
const on = panel.dataset.mkIslandPanel === active;
|
|
1269
|
+
if (active) flag(panel, "hidden", !on);
|
|
1270
|
+
scope.attr(panel, "aria-hidden", String(!on));
|
|
1271
|
+
flag(panel, "inert", !on);
|
|
1272
|
+
}
|
|
1273
|
+
for (const button of toggles) scope.attr(button, "aria-expanded", String(button.dataset.mkIslandToggle === active));
|
|
1274
|
+
flag(cta, "inert", Boolean(active));
|
|
1275
|
+
for (const backdrop of backdrops) {
|
|
1276
|
+
flag(backdrop, "inert", !active);
|
|
1277
|
+
scope.attr(backdrop, "aria-hidden", String(!active));
|
|
1278
|
+
}
|
|
1279
|
+
flag(root, "data-mk-open", Boolean(active));
|
|
1280
|
+
scope.attr(root, "data-mk-active", active);
|
|
1281
|
+
};
|
|
1282
|
+
const stagger = (opening) => {
|
|
1283
|
+
root.querySelectorAll("[data-mk-island-item]").forEach((item, index) => {
|
|
1284
|
+
scope.attr(item, "style", item.getAttribute("style"));
|
|
1285
|
+
item.style.setProperty("--mk-island-delay", opening ? `${Number((t.delay + index * t.step).toFixed(3))}s` : "0s");
|
|
1286
|
+
});
|
|
1287
|
+
};
|
|
1288
|
+
const close = (returnFocus = true) => {
|
|
1289
|
+
if (scope.disposed || !active) return;
|
|
1290
|
+
clearTimeout(focusTimer);
|
|
1291
|
+
active = null;
|
|
1292
|
+
stagger(false);
|
|
1293
|
+
sync();
|
|
1294
|
+
onChange?.(null);
|
|
1295
|
+
if (returnFocus && trigger?.isConnected) trigger.focus({ preventScroll: true });
|
|
1296
|
+
};
|
|
1297
|
+
const open = (name = panels[0].dataset.mkIslandPanel, source) => {
|
|
1298
|
+
if (scope.disposed) return;
|
|
1299
|
+
const panel = panels.find((el) => el.dataset.mkIslandPanel === name);
|
|
1300
|
+
if (!panel) return;
|
|
1301
|
+
const wasClosed = !active;
|
|
1302
|
+
clearTimeout(focusTimer);
|
|
1303
|
+
active = name;
|
|
1304
|
+
trigger = source ?? toggles.find((el) => el.dataset.mkIslandToggle === name) ?? trigger;
|
|
1305
|
+
if (wasClosed) {
|
|
1306
|
+
scrollStart = window.scrollY;
|
|
1307
|
+
armed = false;
|
|
1308
|
+
stagger(true);
|
|
1309
|
+
}
|
|
1310
|
+
sync();
|
|
1311
|
+
onChange?.(name);
|
|
1312
|
+
if (focusOnOpen)
|
|
1313
|
+
focusTimer = setTimeout(
|
|
1314
|
+
() => {
|
|
1315
|
+
if (active !== name) return;
|
|
1316
|
+
const first = [...panel.querySelectorAll(FOCUSABLE)].find((el) => !el.closest("[hidden], [inert]"));
|
|
1317
|
+
(first ?? panel).focus({ preventScroll: true });
|
|
1318
|
+
},
|
|
1319
|
+
reduced() ? 0 : t.focus
|
|
1320
|
+
);
|
|
1321
|
+
};
|
|
1322
|
+
const toggle = (name = panels[0].dataset.mkIslandPanel, source) => active === name ? close(false) : open(name, source);
|
|
1323
|
+
for (const panel of panels) {
|
|
1324
|
+
if (!panel.id) scope.attr(panel, "id", `mk-island-panel-${++sequence}`);
|
|
1325
|
+
if (!panel.hasAttribute("tabindex")) scope.attr(panel, "tabindex", "-1");
|
|
1326
|
+
if (!panel.hasAttribute("role")) scope.attr(panel, "role", "region");
|
|
1327
|
+
}
|
|
1328
|
+
for (const button of toggles) {
|
|
1329
|
+
const panel = panels.find((el) => el.dataset.mkIslandPanel === button.dataset.mkIslandToggle);
|
|
1330
|
+
if (panel) scope.attr(button, "aria-controls", panel.id);
|
|
1331
|
+
scope.on(button, "click", () => toggle(button.dataset.mkIslandToggle, button));
|
|
1332
|
+
}
|
|
1333
|
+
for (const backdrop of backdrops) scope.on(backdrop, "click", () => close(true));
|
|
1334
|
+
scope.on(document, "keydown", (event) => {
|
|
1335
|
+
if (active && event.key === "Escape") {
|
|
1336
|
+
event.preventDefault();
|
|
1337
|
+
close(true);
|
|
1338
|
+
}
|
|
1339
|
+
});
|
|
1340
|
+
if (closeOnNavigate)
|
|
1341
|
+
scope.on(root, "click", (event) => {
|
|
1342
|
+
const link = event.target?.closest?.("a[href]");
|
|
1343
|
+
if (active && link?.closest("[data-mk-island-panel]")) close(false);
|
|
1344
|
+
});
|
|
1345
|
+
if (closeOnScroll) {
|
|
1346
|
+
const arm = () => {
|
|
1347
|
+
if (active) armed = true;
|
|
1348
|
+
};
|
|
1349
|
+
scope.on(window, "wheel", arm, { passive: true });
|
|
1350
|
+
scope.on(window, "touchmove", arm, { passive: true });
|
|
1351
|
+
scope.on(
|
|
1352
|
+
window,
|
|
1353
|
+
"scroll",
|
|
1354
|
+
() => {
|
|
1355
|
+
if (active && armed && Math.abs(window.scrollY - scrollStart) > 1) close(false);
|
|
1356
|
+
},
|
|
1357
|
+
{ passive: true }
|
|
1358
|
+
);
|
|
1359
|
+
}
|
|
1360
|
+
scope.add(() => clearTimeout(focusTimer));
|
|
1361
|
+
const moving = entrance !== false && entrances();
|
|
1362
|
+
if (moving) gsap.set(shell, { yPercent: t.offset });
|
|
1363
|
+
const ready = () => {
|
|
1364
|
+
if (scope.disposed || entered || !moving) return;
|
|
1365
|
+
entered = true;
|
|
1366
|
+
entry = gsap.to(shell, { yPercent: 0, duration: t.entrance });
|
|
1367
|
+
};
|
|
1368
|
+
scope.add(() => {
|
|
1369
|
+
entry?.kill();
|
|
1370
|
+
if (moving) gsap.set(shell, { clearProps: "transform,translate" });
|
|
1371
|
+
if (shell.getAttribute("style") === "") shell.removeAttribute("style");
|
|
1372
|
+
});
|
|
1373
|
+
sync();
|
|
1374
|
+
if (entrance === true) ready();
|
|
1375
|
+
scope.attr(root, "data-mk-ready", "");
|
|
1376
|
+
return {
|
|
1377
|
+
open: (name) => open(name),
|
|
1378
|
+
close,
|
|
1379
|
+
toggle: (name) => toggle(name),
|
|
1380
|
+
ready,
|
|
1381
|
+
get active() {
|
|
1382
|
+
return active;
|
|
1383
|
+
},
|
|
1384
|
+
destroy: () => scope.dispose()
|
|
1385
|
+
};
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
// src/components/pill-button/engine.ts
|
|
1389
|
+
var PILL_ARROW = '<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12h14"/><path d="m13 6 6 6-6 6"/></svg>';
|
|
1390
|
+
function createPillButton(root, options = {}) {
|
|
1391
|
+
const control = root ? root.matches('button, a, [role="button"]') ? root : root.querySelector('button, a, [role="button"]') : null;
|
|
1392
|
+
if (!root || !control) return inert();
|
|
1393
|
+
registerMotion();
|
|
1394
|
+
const scope = createScope();
|
|
1395
|
+
const icon = control.querySelector("[data-mk-pill-icon]");
|
|
1396
|
+
if (!control.querySelector("[data-mk-pill-label]")) {
|
|
1397
|
+
const label = document.createElement("span");
|
|
1398
|
+
label.dataset.mkPillLabel = "";
|
|
1399
|
+
for (const node of [...control.childNodes]) if (node !== icon) label.append(node);
|
|
1400
|
+
control.prepend(label);
|
|
1401
|
+
scope.add(() => label.replaceWith(...label.childNodes));
|
|
1402
|
+
}
|
|
1403
|
+
if (!icon) {
|
|
1404
|
+
const made = document.createElement("span");
|
|
1405
|
+
made.dataset.mkPillIcon = "";
|
|
1406
|
+
made.setAttribute("aria-hidden", "true");
|
|
1407
|
+
made.innerHTML = options.icon ?? PILL_ARROW;
|
|
1408
|
+
control.append(made);
|
|
1409
|
+
scope.add(() => made.remove());
|
|
1410
|
+
}
|
|
1411
|
+
const fill = document.createElement("span");
|
|
1412
|
+
fill.dataset.mkPillFill = "";
|
|
1413
|
+
fill.setAttribute("aria-hidden", "true");
|
|
1414
|
+
control.prepend(fill);
|
|
1415
|
+
scope.add(() => fill.remove());
|
|
1416
|
+
scope.attr(control, "data-mk-pill", options.expand ? "expand" : "");
|
|
1417
|
+
scope.attr(root, "data-mk-ready", "");
|
|
1418
|
+
let tapped;
|
|
1419
|
+
scope.on(control, "pointerdown", (event) => {
|
|
1420
|
+
press(control);
|
|
1421
|
+
if (event.pointerType !== "touch") return;
|
|
1422
|
+
control.setAttribute("data-mk-active", "");
|
|
1423
|
+
clearTimeout(tapped);
|
|
1424
|
+
tapped = setTimeout(() => control.removeAttribute("data-mk-active"), 650);
|
|
1425
|
+
});
|
|
1426
|
+
scope.add(() => {
|
|
1427
|
+
clearTimeout(tapped);
|
|
1428
|
+
control.removeAttribute("data-mk-active");
|
|
1429
|
+
});
|
|
1430
|
+
return { destroy: () => scope.dispose() };
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
// src/components/toolbar/engine.ts
|
|
1434
|
+
function createToolbar(root, options = {}) {
|
|
1435
|
+
const tools = root ? [...root.querySelectorAll("button")] : [];
|
|
1436
|
+
if (!root || !tools.length) {
|
|
1437
|
+
if (root) warnOnce("toolbar: needs at least one <button>");
|
|
1438
|
+
return inert({ setValue() {
|
|
1439
|
+
}, value: "" });
|
|
1440
|
+
}
|
|
1441
|
+
registerMotion();
|
|
1442
|
+
const scope = createScope();
|
|
1443
|
+
const vertical = options.orientation !== "horizontal";
|
|
1444
|
+
const valueOf2 = (tool) => tool.dataset.mkTool ?? String(tools.indexOf(tool));
|
|
1445
|
+
const labelOf = (tool) => tool.getAttribute("aria-label") ?? tool.title ?? tool.textContent?.trim() ?? "";
|
|
1446
|
+
scope.attr(root, "role", "toolbar");
|
|
1447
|
+
scope.attr(root, "aria-orientation", vertical ? "vertical" : "horizontal");
|
|
1448
|
+
scope.attr(root, "data-mk-orientation", vertical ? "vertical" : "horizontal");
|
|
1449
|
+
for (const tool of tools) {
|
|
1450
|
+
scope.attr(tool, "type", tool.getAttribute("type") ?? "button");
|
|
1451
|
+
scope.attr(tool, "aria-pressed", "false");
|
|
1452
|
+
scope.attr(tool, "tabindex", "-1");
|
|
1453
|
+
if (options.tips !== false && tool.title) {
|
|
1454
|
+
scope.attr(tool, "aria-label", tool.getAttribute("aria-label") ?? tool.title);
|
|
1455
|
+
scope.attr(tool, "title", null);
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
const blob = document.createElement("span");
|
|
1459
|
+
blob.dataset.mkToolbarBlob = "";
|
|
1460
|
+
blob.setAttribute("aria-hidden", "true");
|
|
1461
|
+
root.prepend(blob);
|
|
1462
|
+
const tip = document.createElement("span");
|
|
1463
|
+
tip.dataset.mkToolbarTip = "";
|
|
1464
|
+
tip.setAttribute("aria-hidden", "true");
|
|
1465
|
+
tip.hidden = true;
|
|
1466
|
+
if (options.tips !== false) root.append(tip);
|
|
1467
|
+
scope.add(() => {
|
|
1468
|
+
gsap.killTweensOf([blob, tip]);
|
|
1469
|
+
blob.remove();
|
|
1470
|
+
tip.remove();
|
|
1471
|
+
});
|
|
1472
|
+
scope.attr(root, "data-mk-ready", "");
|
|
1473
|
+
const find = (value) => tools.find((tool) => valueOf2(tool) === value);
|
|
1474
|
+
let current = find(options.value) ?? tools.find((tool) => !tool.disabled) ?? tools[0];
|
|
1475
|
+
const box = (tool) => ({ start: vertical ? tool.offsetTop : tool.offsetLeft, size: vertical ? tool.offsetHeight : tool.offsetWidth, cross: vertical ? tool.offsetLeft : tool.offsetTop, crossSize: vertical ? tool.offsetWidth : tool.offsetHeight });
|
|
1476
|
+
const along = vertical ? "y" : "x";
|
|
1477
|
+
const across = vertical ? "x" : "y";
|
|
1478
|
+
const length = vertical ? "height" : "width";
|
|
1479
|
+
const thickness = vertical ? "width" : "height";
|
|
1480
|
+
let timeline = null;
|
|
1481
|
+
const place = () => {
|
|
1482
|
+
const at = box(current);
|
|
1483
|
+
timeline?.kill();
|
|
1484
|
+
gsap.set(blob, { [along]: at.start, [across]: at.cross, [length]: at.size, [thickness]: at.crossSize, scaleX: 1, scaleY: 1 });
|
|
1485
|
+
};
|
|
1486
|
+
const slide = (from, to) => {
|
|
1487
|
+
if (reduced() || from === to) return place();
|
|
1488
|
+
const a = box(from);
|
|
1489
|
+
const b = box(to);
|
|
1490
|
+
const forward = b.start > a.start;
|
|
1491
|
+
const reach = forward ? b.start + b.size - a.start : a.start + a.size - b.start;
|
|
1492
|
+
const squeeze = vertical ? { scaleX: 0.84 } : { scaleY: 0.84 };
|
|
1493
|
+
const relax = vertical ? { scaleX: 1 } : { scaleY: 1 };
|
|
1494
|
+
timeline?.kill();
|
|
1495
|
+
timeline = gsap.timeline();
|
|
1496
|
+
timeline.to(blob, { [along]: forward ? a.start : b.start, [length]: reach, ...squeeze, duration: 0.2, ease: "power2.in" }, 0);
|
|
1497
|
+
timeline.to(blob, { [along]: b.start, [length]: b.size, duration: 0.42, ease: EASE.swatch }, 0.16);
|
|
1498
|
+
timeline.to(blob, { ...relax, duration: 0.5, ease: "elastic.out(1, 0.45)" }, 0.18);
|
|
1499
|
+
};
|
|
1500
|
+
const squish = () => {
|
|
1501
|
+
if (reduced()) return;
|
|
1502
|
+
timeline?.kill();
|
|
1503
|
+
place();
|
|
1504
|
+
timeline = gsap.timeline();
|
|
1505
|
+
timeline.to(blob, { scaleX: 0.88, scaleY: 0.88, duration: 0.1, ease: "power2.out" }).to(blob, { scaleX: 1, scaleY: 1, duration: 0.45, ease: "elastic.out(1, 0.4)" });
|
|
1506
|
+
};
|
|
1507
|
+
const paint = () => {
|
|
1508
|
+
for (const tool of tools) {
|
|
1509
|
+
tool.setAttribute("aria-pressed", String(tool === current));
|
|
1510
|
+
tool.setAttribute("tabindex", tool === current ? "0" : "-1");
|
|
1511
|
+
}
|
|
1512
|
+
};
|
|
1513
|
+
let tipFor = null;
|
|
1514
|
+
let tipTimer;
|
|
1515
|
+
scope.add(() => clearTimeout(tipTimer));
|
|
1516
|
+
const showTip = (tool, linger = 0) => {
|
|
1517
|
+
if (options.tips === false) return;
|
|
1518
|
+
clearTimeout(tipTimer);
|
|
1519
|
+
const text = labelOf(tool);
|
|
1520
|
+
if (!text) return hideTip();
|
|
1521
|
+
const at = box(tool);
|
|
1522
|
+
const appearing = tip.hidden;
|
|
1523
|
+
const changed = tipFor !== tool;
|
|
1524
|
+
tipFor = tool;
|
|
1525
|
+
tip.textContent = text;
|
|
1526
|
+
tip.hidden = false;
|
|
1527
|
+
const tipSize = vertical ? tip.offsetHeight : tip.offsetWidth;
|
|
1528
|
+
const spot = at.start + at.size / 2 - tipSize / 2;
|
|
1529
|
+
if (appearing || reduced()) {
|
|
1530
|
+
gsap.killTweensOf(tip);
|
|
1531
|
+
gsap.set(tip, { [along]: spot });
|
|
1532
|
+
if (!reduced()) gsap.fromTo(tip, { opacity: 0, [across]: -8, filter: "blur(4px)", scale: 0.9 }, { opacity: 1, [across]: 0, filter: "blur(0px)", scale: 1, duration: 0.28, ease: EASE.pop });
|
|
1533
|
+
else gsap.set(tip, { opacity: 1, [across]: 0, scale: 1, filter: "none" });
|
|
1534
|
+
} else if (changed) {
|
|
1535
|
+
gsap.to(tip, { [along]: spot, opacity: 1, [across]: 0, scale: 1, filter: "blur(0px)", duration: 0.32, ease: EASE.swatch, overwrite: "auto" });
|
|
1536
|
+
}
|
|
1537
|
+
if (linger) tipTimer = setTimeout(hideTip, linger);
|
|
1538
|
+
};
|
|
1539
|
+
const hideTip = () => {
|
|
1540
|
+
clearTimeout(tipTimer);
|
|
1541
|
+
tipFor = null;
|
|
1542
|
+
if (tip.hidden) return;
|
|
1543
|
+
if (reduced()) {
|
|
1544
|
+
tip.hidden = true;
|
|
1545
|
+
return;
|
|
1546
|
+
}
|
|
1547
|
+
gsap.to(tip, { opacity: 0, [across]: -6, filter: "blur(4px)", scale: 0.92, duration: 0.18, ease: "power2.in", overwrite: "auto", onComplete: () => {
|
|
1548
|
+
tip.hidden = true;
|
|
1549
|
+
} });
|
|
1550
|
+
};
|
|
1551
|
+
const choose = (tool, notify) => {
|
|
1552
|
+
if (tool.disabled) return;
|
|
1553
|
+
const previous = current;
|
|
1554
|
+
current = tool;
|
|
1555
|
+
paint();
|
|
1556
|
+
if (previous === current) squish();
|
|
1557
|
+
else slide(previous, current);
|
|
1558
|
+
if (previous === current || !notify) return;
|
|
1559
|
+
const value = valueOf2(current);
|
|
1560
|
+
options.onChange?.(value);
|
|
1561
|
+
root.dispatchEvent(new CustomEvent("mk:change", { bubbles: true, detail: { value } }));
|
|
1562
|
+
};
|
|
1563
|
+
scope.on(root, "click", (event) => {
|
|
1564
|
+
const tool = event.target.closest("button");
|
|
1565
|
+
if (!tool || !tools.includes(tool)) return;
|
|
1566
|
+
press(tool);
|
|
1567
|
+
choose(tool, true);
|
|
1568
|
+
showTip(tool, 1200);
|
|
1569
|
+
});
|
|
1570
|
+
scope.on(root, "pointerover", (event) => {
|
|
1571
|
+
const tool = event.target.closest("button");
|
|
1572
|
+
if (tool && tools.includes(tool) && event.pointerType !== "touch") showTip(tool);
|
|
1573
|
+
});
|
|
1574
|
+
scope.on(root, "pointerleave", () => {
|
|
1575
|
+
tipTimer = setTimeout(hideTip, 120);
|
|
1576
|
+
});
|
|
1577
|
+
scope.on(root, "focusin", (event) => {
|
|
1578
|
+
const tool = event.target;
|
|
1579
|
+
if (tools.includes(tool) && tool.matches(":focus-visible")) showTip(tool);
|
|
1580
|
+
});
|
|
1581
|
+
scope.on(root, "focusout", (event) => {
|
|
1582
|
+
if (!root.contains(event.relatedTarget)) hideTip();
|
|
1583
|
+
});
|
|
1584
|
+
scope.on(root, "keydown", (event) => {
|
|
1585
|
+
const key = event.key;
|
|
1586
|
+
const enabled = tools.filter((tool) => !tool.disabled);
|
|
1587
|
+
const at = enabled.indexOf(document.activeElement);
|
|
1588
|
+
if (at < 0) return;
|
|
1589
|
+
const next = key === "ArrowDown" || key === "ArrowRight" ? enabled[(at + 1) % enabled.length] : key === "ArrowUp" || key === "ArrowLeft" ? enabled[(at - 1 + enabled.length) % enabled.length] : key === "Home" ? enabled[0] : key === "End" ? enabled[enabled.length - 1] : null;
|
|
1590
|
+
if (!next) return;
|
|
1591
|
+
event.preventDefault();
|
|
1592
|
+
choose(next, true);
|
|
1593
|
+
next.focus();
|
|
1594
|
+
showTip(next, 1200);
|
|
1595
|
+
});
|
|
1596
|
+
if (typeof ResizeObserver === "function") scope.observe(new ResizeObserver(() => place())).observe(root);
|
|
1597
|
+
paint();
|
|
1598
|
+
place();
|
|
1599
|
+
return {
|
|
1600
|
+
setValue(value, notify = false) {
|
|
1601
|
+
const tool = find(value);
|
|
1602
|
+
if (tool) choose(tool, notify);
|
|
1603
|
+
},
|
|
1604
|
+
get value() {
|
|
1605
|
+
return valueOf2(current);
|
|
1606
|
+
},
|
|
1607
|
+
destroy: () => scope.dispose()
|
|
1608
|
+
};
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
// src/components/bidirectional-slider/engine.ts
|
|
1612
|
+
var finite = (value) => typeof value === "number" && Number.isFinite(value);
|
|
1613
|
+
var ICONS = {
|
|
1614
|
+
signs: ['<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M6 12h12"/></svg>', '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M6 12h12M12 6v12"/></svg>'],
|
|
1615
|
+
volume: [
|
|
1616
|
+
'<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 9.5v5h3.5L12 18.5v-13L7.5 9.5z"/><path d="M15.5 9.5a3.5 3.5 0 0 1 0 5"/></svg>',
|
|
1617
|
+
'<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 9.5v5h3.5L12 18.5v-13L7.5 9.5z"/><path d="M15.5 9.5a3.5 3.5 0 0 1 0 5"/><path d="M18 7a7 7 0 0 1 0 10"/></svg>'
|
|
1618
|
+
]
|
|
1619
|
+
};
|
|
1620
|
+
function createBidirectionalSlider(root, options = {}) {
|
|
1621
|
+
if (!root) return inert({ setValue() {
|
|
1622
|
+
}, reset() {
|
|
1623
|
+
}, value: 0 });
|
|
1624
|
+
registerMotion();
|
|
1625
|
+
const scope = createScope();
|
|
1626
|
+
const min = finite(options.min) ? options.min : -100;
|
|
1627
|
+
const max = finite(options.max) && options.max > min ? options.max : Math.max(min + 1, 100);
|
|
1628
|
+
const origin = Math.min(max, Math.max(min, finite(options.origin) ? options.origin : 0));
|
|
1629
|
+
const step = finite(options.step) && options.step > 0 ? options.step : 1;
|
|
1630
|
+
const decimals = (String(step).split(".")[1] ?? "").length;
|
|
1631
|
+
const snap = (n) => Number(Math.min(max, Math.max(min, Math.round((n - min) / step) * step + min)).toFixed(decimals));
|
|
1632
|
+
const format = options.formatValue ?? ((n) => {
|
|
1633
|
+
const delta = Number((n - origin).toFixed(decimals));
|
|
1634
|
+
const text = new Intl.NumberFormat(void 0, { maximumFractionDigits: decimals }).format(Math.abs(delta));
|
|
1635
|
+
return delta > 0 ? `+${text}` : delta < 0 ? `\u2212${text}` : text;
|
|
1636
|
+
});
|
|
1637
|
+
const made = (tag, parent, data) => {
|
|
1638
|
+
const element = document.createElement(tag);
|
|
1639
|
+
element.setAttribute(`data-mk-bislider-${data}`, "");
|
|
1640
|
+
parent.append(element);
|
|
1641
|
+
scope.add(() => element.remove());
|
|
1642
|
+
return element;
|
|
1643
|
+
};
|
|
1644
|
+
const icons = options.icons === false ? null : ICONS[options.icons ?? "signs"];
|
|
1645
|
+
const less = icons ? made("button", root, "less") : null;
|
|
1646
|
+
const track = made("span", root, "track");
|
|
1647
|
+
const more = icons ? made("button", root, "more") : null;
|
|
1648
|
+
if (less && more && icons) {
|
|
1649
|
+
less.type = more.type = "button";
|
|
1650
|
+
less.tabIndex = more.tabIndex = -1;
|
|
1651
|
+
less.innerHTML = icons[0];
|
|
1652
|
+
more.innerHTML = icons[1];
|
|
1653
|
+
less.setAttribute("aria-label", "Menos");
|
|
1654
|
+
more.setAttribute("aria-label", "M\xE1s");
|
|
1655
|
+
root.prepend(less);
|
|
1656
|
+
}
|
|
1657
|
+
const clip = made("span", track, "clip");
|
|
1658
|
+
clip.setAttribute("aria-hidden", "true");
|
|
1659
|
+
const fill = made("span", clip, "fill");
|
|
1660
|
+
const notch = made("span", clip, "notch");
|
|
1661
|
+
const thumb = made("span", track, "thumb");
|
|
1662
|
+
const bubble = made("span", track, "bubble");
|
|
1663
|
+
for (const part2 of [fill, notch, thumb, bubble]) part2.setAttribute("aria-hidden", "true");
|
|
1664
|
+
let input = root.querySelector('input[type="range"]');
|
|
1665
|
+
if (input) {
|
|
1666
|
+
const kept = input;
|
|
1667
|
+
const place = { parent: kept.parentNode, next: kept.nextSibling };
|
|
1668
|
+
const saved = ["min", "max", "step", "value", "data-mk-bislider-input"].map((name) => [name, kept.getAttribute(name)]);
|
|
1669
|
+
const value2 = kept.value;
|
|
1670
|
+
scope.add(() => {
|
|
1671
|
+
for (const [name, was] of saved) {
|
|
1672
|
+
if (was === null) kept.removeAttribute(name);
|
|
1673
|
+
else kept.setAttribute(name, was);
|
|
1674
|
+
}
|
|
1675
|
+
kept.value = value2;
|
|
1676
|
+
place.parent?.insertBefore(kept, place.next);
|
|
1677
|
+
});
|
|
1678
|
+
} else {
|
|
1679
|
+
input = document.createElement("input");
|
|
1680
|
+
input.type = "range";
|
|
1681
|
+
scope.add(() => input.remove());
|
|
1682
|
+
}
|
|
1683
|
+
const range = input;
|
|
1684
|
+
track.append(range);
|
|
1685
|
+
range.dataset.mkBisliderInput = "";
|
|
1686
|
+
range.min = String(min);
|
|
1687
|
+
range.max = String(max);
|
|
1688
|
+
range.step = String(step);
|
|
1689
|
+
if (options.name) scope.attr(range, "name", options.name);
|
|
1690
|
+
if (options.label && !range.getAttribute("aria-label")) scope.attr(range, "aria-label", options.label);
|
|
1691
|
+
if (options.disabled) scope.attr(range, "disabled", "");
|
|
1692
|
+
const start = snap(finite(options.value) ? options.value : range.getAttribute("value") !== null ? Number(range.getAttribute("value")) : origin);
|
|
1693
|
+
range.value = String(start);
|
|
1694
|
+
range.defaultValue = String(start);
|
|
1695
|
+
scope.attr(root, "data-mk-ready", "");
|
|
1696
|
+
let value = start;
|
|
1697
|
+
let committed = start;
|
|
1698
|
+
const shown = { v: start };
|
|
1699
|
+
const pct = (n) => (n - min) / (max - min) * 100;
|
|
1700
|
+
notch.style.left = `${pct(origin)}%`;
|
|
1701
|
+
const paint = () => {
|
|
1702
|
+
const at = pct(shown.v);
|
|
1703
|
+
const from = pct(origin);
|
|
1704
|
+
fill.style.left = `${Math.min(at, from)}%`;
|
|
1705
|
+
fill.style.width = `${Math.abs(at - from)}%`;
|
|
1706
|
+
thumb.style.left = `${at}%`;
|
|
1707
|
+
bubble.style.left = `${at}%`;
|
|
1708
|
+
root.setAttribute("data-mk-side", shown.v > origin ? "more" : shown.v < origin ? "less" : "origin");
|
|
1709
|
+
};
|
|
1710
|
+
scope.add(() => root.removeAttribute("data-mk-side"));
|
|
1711
|
+
const describe = () => {
|
|
1712
|
+
const text = format(value);
|
|
1713
|
+
bubble.textContent = text;
|
|
1714
|
+
range.setAttribute("aria-valuetext", text);
|
|
1715
|
+
};
|
|
1716
|
+
scope.add(() => range.removeAttribute("aria-valuetext"));
|
|
1717
|
+
const effects = /* @__PURE__ */ new Set();
|
|
1718
|
+
const keep = (animation) => {
|
|
1719
|
+
if (!animation) return;
|
|
1720
|
+
effects.add(animation);
|
|
1721
|
+
for (const effect of effects) if (effect.totalProgress() === 1) effects.delete(effect);
|
|
1722
|
+
};
|
|
1723
|
+
scope.add(() => {
|
|
1724
|
+
gsap.killTweensOf(shown);
|
|
1725
|
+
for (const effect of effects) effect.kill();
|
|
1726
|
+
effects.clear();
|
|
1727
|
+
});
|
|
1728
|
+
const bump = (element, scale = 1.25) => {
|
|
1729
|
+
if (!element || reduced()) return;
|
|
1730
|
+
keep(gsap.fromTo(element, { scale }, { scale: 1, duration: 0.5, ease: "elastic.out(1, 0.4)", overwrite: "auto" }));
|
|
1731
|
+
};
|
|
1732
|
+
let held = false;
|
|
1733
|
+
const set = (next, glide, notify) => {
|
|
1734
|
+
const before = value;
|
|
1735
|
+
value = snap(next);
|
|
1736
|
+
range.value = String(value);
|
|
1737
|
+
describe();
|
|
1738
|
+
gsap.killTweensOf(shown);
|
|
1739
|
+
if (glide && !reduced()) keep(gsap.to(shown, { v: value, duration: 0.45, ease: EASE.swatch, onUpdate: paint }));
|
|
1740
|
+
else {
|
|
1741
|
+
shown.v = value;
|
|
1742
|
+
paint();
|
|
1743
|
+
}
|
|
1744
|
+
if ((before - origin) * (value - origin) < 0 || before !== origin && value === origin) bump(notch, 1.8);
|
|
1745
|
+
if (value !== before && value === max) bump(more);
|
|
1746
|
+
if (value !== before && value === min) bump(less);
|
|
1747
|
+
if (notify && value !== before) {
|
|
1748
|
+
options.onInput?.(value);
|
|
1749
|
+
root.dispatchEvent(new CustomEvent("mk:input", { bubbles: true, detail: { value } }));
|
|
1750
|
+
}
|
|
1751
|
+
if (notify === "change") commit();
|
|
1752
|
+
};
|
|
1753
|
+
const commit = () => {
|
|
1754
|
+
if (value === committed) return;
|
|
1755
|
+
committed = value;
|
|
1756
|
+
options.onChange?.(value);
|
|
1757
|
+
root.dispatchEvent(new CustomEvent("mk:change", { bubbles: true, detail: { value } }));
|
|
1758
|
+
};
|
|
1759
|
+
scope.on(range, "input", () => {
|
|
1760
|
+
const next = Number(range.value);
|
|
1761
|
+
const before = value;
|
|
1762
|
+
set(next, !held, false);
|
|
1763
|
+
if (value !== before) {
|
|
1764
|
+
options.onInput?.(value);
|
|
1765
|
+
root.dispatchEvent(new CustomEvent("mk:input", { bubbles: true, detail: { value } }));
|
|
1766
|
+
}
|
|
1767
|
+
});
|
|
1768
|
+
scope.on(range, "change", commit);
|
|
1769
|
+
scope.on(range, "dblclick", () => set(origin, true, "change"));
|
|
1770
|
+
let pointer = null;
|
|
1771
|
+
scope.on(range, "pointerdown", (event) => {
|
|
1772
|
+
const down = event;
|
|
1773
|
+
if (range.disabled || down.button !== 0) return;
|
|
1774
|
+
held = true;
|
|
1775
|
+
pointer = down.pointerId;
|
|
1776
|
+
root.setAttribute("data-mk-held", "");
|
|
1777
|
+
});
|
|
1778
|
+
scope.on(window, "pointermove", (event) => {
|
|
1779
|
+
const move = event;
|
|
1780
|
+
if (!held || move.pointerId !== pointer || reduced()) return;
|
|
1781
|
+
const box = track.getBoundingClientRect();
|
|
1782
|
+
const over = move.clientX > box.right ? move.clientX - box.right : move.clientX < box.left ? move.clientX - box.left : 0;
|
|
1783
|
+
const stretch = Math.min(1, Math.abs(over) / 80) * 0.04;
|
|
1784
|
+
gsap.set(track, { scaleX: 1 + stretch, transformOrigin: over > 0 ? "0% 50%" : "100% 50%" });
|
|
1785
|
+
});
|
|
1786
|
+
const release = (event) => {
|
|
1787
|
+
if (!held || event.pointerId !== pointer) return;
|
|
1788
|
+
held = false;
|
|
1789
|
+
pointer = null;
|
|
1790
|
+
root.removeAttribute("data-mk-held");
|
|
1791
|
+
if (!reduced()) keep(gsap.to(track, { scaleX: 1, duration: 0.6, ease: "elastic.out(1, 0.35)", clearProps: "transform,transformOrigin" }));
|
|
1792
|
+
};
|
|
1793
|
+
scope.on(window, "pointerup", release);
|
|
1794
|
+
scope.on(window, "pointercancel", release);
|
|
1795
|
+
scope.add(() => {
|
|
1796
|
+
root.removeAttribute("data-mk-held");
|
|
1797
|
+
gsap.set(track, { clearProps: "transform,transformOrigin" });
|
|
1798
|
+
});
|
|
1799
|
+
let repeat;
|
|
1800
|
+
const stop = () => {
|
|
1801
|
+
clearTimeout(repeat);
|
|
1802
|
+
if (repeat !== void 0) commit();
|
|
1803
|
+
repeat = void 0;
|
|
1804
|
+
};
|
|
1805
|
+
scope.add(() => clearTimeout(repeat));
|
|
1806
|
+
for (const [button, direction] of [[less, -1], [more, 1]]) {
|
|
1807
|
+
if (!button) continue;
|
|
1808
|
+
scope.on(button, "pointerdown", (event) => {
|
|
1809
|
+
if (range.disabled || event.button !== 0) return;
|
|
1810
|
+
event.preventDefault();
|
|
1811
|
+
press(button);
|
|
1812
|
+
const tick = (delay) => {
|
|
1813
|
+
set(value + step * direction, true, "input");
|
|
1814
|
+
repeat = setTimeout(() => tick(Math.max(50, delay * 0.8)), delay);
|
|
1815
|
+
};
|
|
1816
|
+
tick(400);
|
|
1817
|
+
});
|
|
1818
|
+
scope.on(button, "pointerup", stop);
|
|
1819
|
+
scope.on(button, "pointerleave", stop);
|
|
1820
|
+
scope.on(button, "pointercancel", stop);
|
|
1821
|
+
}
|
|
1822
|
+
const form = range.form;
|
|
1823
|
+
if (form) scope.on(form, "reset", () => setTimeout(() => set(Number(range.defaultValue), true, false)));
|
|
1824
|
+
describe();
|
|
1825
|
+
paint();
|
|
1826
|
+
return {
|
|
1827
|
+
setValue: (next, notify = false) => set(next, true, notify ? "change" : false),
|
|
1828
|
+
reset: (notify = false) => set(origin, true, notify ? "change" : false),
|
|
1829
|
+
get value() {
|
|
1830
|
+
return value;
|
|
1831
|
+
},
|
|
1832
|
+
destroy: () => scope.dispose()
|
|
1833
|
+
};
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1836
|
+
// src/components/voice-dots/engine.ts
|
|
1837
|
+
var LABELS = { idle: "Asistente en espera", listening: "Escuchando\u2026", thinking: "Pensando\u2026", speaking: "Hablando\u2026" };
|
|
1838
|
+
var STATES = ["idle", "listening", "thinking", "speaking"];
|
|
1839
|
+
var mediaSources = /* @__PURE__ */ new WeakMap();
|
|
1840
|
+
var audio = null;
|
|
1841
|
+
function createVoiceDots(root, options = {}) {
|
|
1842
|
+
if (!root) return inert({ setState() {
|
|
1843
|
+
}, state: "idle", setLevel() {
|
|
1844
|
+
}, listen: async () => () => {
|
|
1845
|
+
}, stop() {
|
|
1846
|
+
} });
|
|
1847
|
+
registerMotion();
|
|
1848
|
+
const scope = createScope();
|
|
1849
|
+
const count = Math.max(3, Math.min(6, Math.round(options.count ?? 4)));
|
|
1850
|
+
const size = Math.max(4, options.size ?? 12);
|
|
1851
|
+
const labels = { ...LABELS, ...options.labels };
|
|
1852
|
+
let state = STATES.includes(options.state) ? options.state : "idle";
|
|
1853
|
+
const row = document.createElement("span");
|
|
1854
|
+
row.dataset.mkVoiceRow = "";
|
|
1855
|
+
row.setAttribute("aria-hidden", "true");
|
|
1856
|
+
const dots = Array.from({ length: count }, (_, i) => {
|
|
1857
|
+
const dot = document.createElement("span");
|
|
1858
|
+
dot.dataset.mkVoiceDot = String(i + 1);
|
|
1859
|
+
row.append(dot);
|
|
1860
|
+
return dot;
|
|
1861
|
+
});
|
|
1862
|
+
const status = document.createElement("span");
|
|
1863
|
+
status.className = "mk-sr-only";
|
|
1864
|
+
status.setAttribute("role", "status");
|
|
1865
|
+
root.append(row, status);
|
|
1866
|
+
scope.add(() => {
|
|
1867
|
+
row.remove();
|
|
1868
|
+
status.remove();
|
|
1869
|
+
});
|
|
1870
|
+
scope.attr(root, "data-mk-ready", "");
|
|
1871
|
+
scope.attr(root, "data-mk-state", state);
|
|
1872
|
+
if (options.colorful) scope.attr(root, "data-mk-colorful", "");
|
|
1873
|
+
root.style.setProperty("--mk-voice-size", `${size}px`);
|
|
1874
|
+
scope.add(() => {
|
|
1875
|
+
root.style.removeProperty("--mk-voice-size");
|
|
1876
|
+
if (!root.getAttribute("style")) root.removeAttribute("style");
|
|
1877
|
+
});
|
|
1878
|
+
const drawn = dots.map(() => ({ h: size, x: 0, y: 0, vh: 0 }));
|
|
1879
|
+
const levels = dots.map(() => 0);
|
|
1880
|
+
let time = 0;
|
|
1881
|
+
const gap = size * 0.9;
|
|
1882
|
+
const home = (i) => (i - (count - 1) / 2) * (size + gap);
|
|
1883
|
+
const draw = () => {
|
|
1884
|
+
dots.forEach((dot, i) => {
|
|
1885
|
+
const d = drawn[i];
|
|
1886
|
+
dot.style.height = `${d.h.toFixed(2)}px`;
|
|
1887
|
+
dot.style.transform = `translate(${d.x.toFixed(2)}px, ${d.y.toFixed(2)}px)`;
|
|
1888
|
+
});
|
|
1889
|
+
};
|
|
1890
|
+
const still = () => {
|
|
1891
|
+
dots.forEach((_, i) => Object.assign(drawn[i], { h: size, x: 0, y: 0, vh: 0 }));
|
|
1892
|
+
draw();
|
|
1893
|
+
};
|
|
1894
|
+
let analyser = null;
|
|
1895
|
+
let bins = null;
|
|
1896
|
+
let release = null;
|
|
1897
|
+
let listening = 0;
|
|
1898
|
+
const sample = () => {
|
|
1899
|
+
if (!analyser || !bins) return;
|
|
1900
|
+
analyser.getByteFrequencyData(bins);
|
|
1901
|
+
const hz = (audio?.sampleRate ?? 48e3) / 2 / bins.length;
|
|
1902
|
+
const low = Math.max(1, Math.floor(120 / hz));
|
|
1903
|
+
const high = Math.min(bins.length - 1, Math.ceil(3e3 / hz));
|
|
1904
|
+
const span = (high - low) / count;
|
|
1905
|
+
for (let i = 0; i < count; i++) {
|
|
1906
|
+
let sum = 0;
|
|
1907
|
+
const from = Math.floor(low + span * i);
|
|
1908
|
+
const to = Math.max(from + 1, Math.floor(low + span * (i + 1)));
|
|
1909
|
+
for (let k = from; k < to; k++) sum += bins[k];
|
|
1910
|
+
levels[i] = Math.min(1, sum / (to - from) / 255 * (1.4 + i * 0.35));
|
|
1911
|
+
}
|
|
1912
|
+
};
|
|
1913
|
+
const tick = (dt) => {
|
|
1914
|
+
if (reduced()) return;
|
|
1915
|
+
time += dt / 60;
|
|
1916
|
+
sample();
|
|
1917
|
+
const k = 1 - Math.exp(-dt * 0.35);
|
|
1918
|
+
dots.forEach((_, i) => {
|
|
1919
|
+
const d = drawn[i];
|
|
1920
|
+
let h = size;
|
|
1921
|
+
let x = 0;
|
|
1922
|
+
let y = 0;
|
|
1923
|
+
if (state === "idle") {
|
|
1924
|
+
h = size * (1 + 0.18 * Math.max(0, Math.sin(time * 3.2 - i * 0.7)));
|
|
1925
|
+
y = -2 * Math.max(0, Math.sin(time * 3.2 - i * 0.7));
|
|
1926
|
+
} else if (state === "listening") {
|
|
1927
|
+
h = size + levels[i] * size * 3.2;
|
|
1928
|
+
} else if (state === "thinking") {
|
|
1929
|
+
const angle = time * 4 + i / count * Math.PI * 2;
|
|
1930
|
+
const ring = size * 1.2;
|
|
1931
|
+
x = Math.cos(angle) * ring - home(i);
|
|
1932
|
+
y = Math.sin(angle) * ring;
|
|
1933
|
+
} else {
|
|
1934
|
+
const wave = 0.5 + 0.5 * Math.sin(time * 9 - i * 0.9);
|
|
1935
|
+
const voice = analyser ? levels[i] : 0.55;
|
|
1936
|
+
h = size + wave * voice * size * 2.4;
|
|
1937
|
+
}
|
|
1938
|
+
const stiffness = h > d.h ? 0.5 : 0.18;
|
|
1939
|
+
d.vh = (d.vh + (h - d.h) * stiffness) * 0.6;
|
|
1940
|
+
d.h = Math.max(size * 0.8, d.h + d.vh * dt);
|
|
1941
|
+
d.x += (x - d.x) * k;
|
|
1942
|
+
d.y += (y - d.y) * k;
|
|
1943
|
+
});
|
|
1944
|
+
draw();
|
|
1945
|
+
};
|
|
1946
|
+
const stopLoop = whileVisible(root, tick);
|
|
1947
|
+
scope.add(stopLoop);
|
|
1948
|
+
const stop = () => {
|
|
1949
|
+
listening++;
|
|
1950
|
+
release?.();
|
|
1951
|
+
release = null;
|
|
1952
|
+
analyser = null;
|
|
1953
|
+
bins = null;
|
|
1954
|
+
levels.fill(0);
|
|
1955
|
+
};
|
|
1956
|
+
scope.add(stop);
|
|
1957
|
+
const setState = (next) => {
|
|
1958
|
+
if (!STATES.includes(next)) return;
|
|
1959
|
+
state = next;
|
|
1960
|
+
root.setAttribute("data-mk-state", next);
|
|
1961
|
+
status.textContent = next === "idle" ? "" : labels[next];
|
|
1962
|
+
if (reduced()) still();
|
|
1963
|
+
root.dispatchEvent(new CustomEvent("mk:state", { bubbles: true, detail: { state: next } }));
|
|
1964
|
+
};
|
|
1965
|
+
draw();
|
|
1966
|
+
if (state !== "idle") status.textContent = labels[state];
|
|
1967
|
+
return {
|
|
1968
|
+
setState,
|
|
1969
|
+
get state() {
|
|
1970
|
+
return state;
|
|
1971
|
+
},
|
|
1972
|
+
setLevel(level) {
|
|
1973
|
+
const list = Array.isArray(level) ? level : dots.map(() => level);
|
|
1974
|
+
dots.forEach((_, i) => {
|
|
1975
|
+
const value = Number(list[i % Math.max(1, list.length)]);
|
|
1976
|
+
levels[i] = Number.isFinite(value) ? Math.min(1, Math.max(0, value)) : 0;
|
|
1977
|
+
});
|
|
1978
|
+
},
|
|
1979
|
+
async listen(source) {
|
|
1980
|
+
stop();
|
|
1981
|
+
const request = listening;
|
|
1982
|
+
const Context = globalThis.AudioContext ?? globalThis.webkitAudioContext;
|
|
1983
|
+
if (!Context || scope.disposed) return () => {
|
|
1984
|
+
};
|
|
1985
|
+
audio ??= new Context();
|
|
1986
|
+
if (audio.state === "suspended") await audio.resume().catch(() => {
|
|
1987
|
+
});
|
|
1988
|
+
if (scope.disposed || request !== listening) return () => {
|
|
1989
|
+
};
|
|
1990
|
+
const node = audio.createAnalyser();
|
|
1991
|
+
node.fftSize = 256;
|
|
1992
|
+
node.smoothingTimeConstant = 0.55;
|
|
1993
|
+
let input;
|
|
1994
|
+
if (source instanceof HTMLMediaElement) {
|
|
1995
|
+
const existing = mediaSources.get(source) ?? audio.createMediaElementSource(source);
|
|
1996
|
+
mediaSources.set(source, existing);
|
|
1997
|
+
existing.disconnect();
|
|
1998
|
+
input = existing;
|
|
1999
|
+
node.connect(audio.destination);
|
|
2000
|
+
} else input = audio.createMediaStreamSource(source);
|
|
2001
|
+
input.connect(node);
|
|
2002
|
+
analyser = node;
|
|
2003
|
+
bins = new Uint8Array(new ArrayBuffer(node.frequencyBinCount));
|
|
2004
|
+
const mine = () => {
|
|
2005
|
+
try {
|
|
2006
|
+
input.disconnect(node);
|
|
2007
|
+
node.disconnect();
|
|
2008
|
+
} catch {
|
|
2009
|
+
}
|
|
2010
|
+
if (source instanceof HTMLMediaElement && audio) input.connect(audio.destination);
|
|
2011
|
+
};
|
|
2012
|
+
release = mine;
|
|
2013
|
+
return () => {
|
|
2014
|
+
if (release === mine) stop();
|
|
2015
|
+
};
|
|
2016
|
+
},
|
|
2017
|
+
stop,
|
|
2018
|
+
destroy: () => scope.dispose()
|
|
2019
|
+
};
|
|
2020
|
+
}
|
|
2021
|
+
|
|
2022
|
+
// src/components/chart/parts.ts
|
|
2023
|
+
var SVG_NS = "http://www.w3.org/2000/svg";
|
|
2024
|
+
var SLOTS = 8;
|
|
2025
|
+
var svg2 = (tag, attrs = {}) => {
|
|
2026
|
+
const element = document.createElementNS(SVG_NS, tag);
|
|
2027
|
+
for (const [name, value] of Object.entries(attrs)) element.setAttribute(name, String(value));
|
|
2028
|
+
return element;
|
|
2029
|
+
};
|
|
2030
|
+
var finite2 = (value) => typeof value === "number" && Number.isFinite(value);
|
|
2031
|
+
var round = (n) => Math.round(n * 100) / 100;
|
|
2032
|
+
var slotColor = (i, total = SLOTS) => total === 1 ? "var(--mk-chart-mono)" : i < SLOTS ? `var(--mk-chart-${i + 1})` : "var(--mk-chart-other)";
|
|
2033
|
+
var formatNumber = (value) => new Intl.NumberFormat(void 0, { maximumFractionDigits: 2 }).format(value);
|
|
2034
|
+
var polar = (cx, cy, r, angle) => [cx + r * Math.sin(angle), cy - r * Math.cos(angle)];
|
|
2035
|
+
function arcPath(cx, cy, outer, inner, a0, a1) {
|
|
2036
|
+
const sweep = Math.min(a1 - a0, Math.PI * 2 - 1e-4);
|
|
2037
|
+
const end = a0 + sweep;
|
|
2038
|
+
const large = sweep > Math.PI ? 1 : 0;
|
|
2039
|
+
const [x0, y0] = polar(cx, cy, outer, a0);
|
|
2040
|
+
const [x1, y1] = polar(cx, cy, outer, end);
|
|
2041
|
+
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`;
|
|
2042
|
+
const [x2, y2] = polar(cx, cy, inner, end);
|
|
2043
|
+
const [x3, y3] = polar(cx, cy, inner, a0);
|
|
2044
|
+
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`;
|
|
2045
|
+
}
|
|
2046
|
+
function chartFrame(root, scope, kind) {
|
|
2047
|
+
if (!root.classList.contains("mk-chart")) {
|
|
2048
|
+
root.classList.add("mk-chart");
|
|
2049
|
+
scope.add(() => {
|
|
2050
|
+
root.classList.remove("mk-chart");
|
|
2051
|
+
if (!root.classList.length) root.removeAttribute("class");
|
|
2052
|
+
});
|
|
2053
|
+
}
|
|
2054
|
+
scope.attr(root, "data-mk-chart-type", kind);
|
|
2055
|
+
const made = (tag, parent = root) => {
|
|
2056
|
+
const element = document.createElement(tag);
|
|
2057
|
+
parent.append(element);
|
|
2058
|
+
scope.add(() => element.remove());
|
|
2059
|
+
return element;
|
|
2060
|
+
};
|
|
2061
|
+
const plot = made("div");
|
|
2062
|
+
plot.dataset.mkChartPlot = "";
|
|
2063
|
+
const canvas = svg2("svg");
|
|
2064
|
+
canvas.dataset.mkChartSvg = "";
|
|
2065
|
+
canvas.setAttribute("role", "img");
|
|
2066
|
+
canvas.setAttribute("tabindex", "0");
|
|
2067
|
+
canvas.setAttribute("focusable", "true");
|
|
2068
|
+
plot.append(canvas);
|
|
2069
|
+
const tooltip = made("div", plot);
|
|
2070
|
+
tooltip.dataset.mkChartTooltip = "";
|
|
2071
|
+
tooltip.setAttribute("aria-hidden", "true");
|
|
2072
|
+
tooltip.hidden = true;
|
|
2073
|
+
const empty = made("p");
|
|
2074
|
+
empty.dataset.mkChartEmpty = "";
|
|
2075
|
+
empty.hidden = true;
|
|
2076
|
+
const legend = made("ul");
|
|
2077
|
+
legend.dataset.mkChartLegend = "";
|
|
2078
|
+
const table = made("table");
|
|
2079
|
+
table.dataset.mkChartTable = "";
|
|
2080
|
+
table.className = "mk-sr-only";
|
|
2081
|
+
const live = made("span");
|
|
2082
|
+
live.className = "mk-sr-only";
|
|
2083
|
+
live.setAttribute("role", "status");
|
|
2084
|
+
scope.attr(root, "data-mk-ready", "");
|
|
2085
|
+
const effects = /* @__PURE__ */ new Set();
|
|
2086
|
+
const keep = (animation) => {
|
|
2087
|
+
if (!animation) return;
|
|
2088
|
+
effects.add(animation);
|
|
2089
|
+
for (const effect of effects) if (effect.totalProgress() === 1) effects.delete(effect);
|
|
2090
|
+
};
|
|
2091
|
+
scope.add(() => {
|
|
2092
|
+
for (const effect of effects) effect.kill();
|
|
2093
|
+
effects.clear();
|
|
2094
|
+
gsap.killTweensOf(tooltip);
|
|
2095
|
+
});
|
|
2096
|
+
const tip = (at) => {
|
|
2097
|
+
if (!at) {
|
|
2098
|
+
if (!tooltip.hidden && !reduced()) {
|
|
2099
|
+
gsap.killTweensOf(tooltip, "opacity,scale");
|
|
2100
|
+
keep(gsap.to(tooltip, { opacity: 0, scale: 0.94, duration: 0.15, ease: "power2.in", onComplete: () => {
|
|
2101
|
+
tooltip.hidden = true;
|
|
2102
|
+
} }));
|
|
2103
|
+
} else tooltip.hidden = true;
|
|
2104
|
+
return;
|
|
2105
|
+
}
|
|
2106
|
+
tooltip.replaceChildren();
|
|
2107
|
+
const title = document.createElement("strong");
|
|
2108
|
+
title.textContent = at.title;
|
|
2109
|
+
tooltip.append(title);
|
|
2110
|
+
for (const each of at.rows) {
|
|
2111
|
+
const row = document.createElement("span");
|
|
2112
|
+
row.dataset.mkChartRow = "";
|
|
2113
|
+
const swatch = document.createElement("i");
|
|
2114
|
+
if (!each.color || /--mk-(text|chart-mono|muted)/.test(each.color)) swatch.dataset.mkMono = "";
|
|
2115
|
+
else swatch.style.background = each.color;
|
|
2116
|
+
const name = document.createElement("span");
|
|
2117
|
+
name.textContent = each.name;
|
|
2118
|
+
const amount = document.createElement("b");
|
|
2119
|
+
amount.textContent = each.value;
|
|
2120
|
+
row.append(swatch, name, amount);
|
|
2121
|
+
tooltip.append(row);
|
|
2122
|
+
}
|
|
2123
|
+
const appearing = tooltip.hidden;
|
|
2124
|
+
tooltip.hidden = false;
|
|
2125
|
+
const width = plot.clientWidth || 300;
|
|
2126
|
+
const height = plot.clientHeight || 240;
|
|
2127
|
+
const tipW = tooltip.offsetWidth || 160;
|
|
2128
|
+
const tipH = tooltip.offsetHeight || 60;
|
|
2129
|
+
const left = round(Math.max(0, Math.min(width - tipW, at.x > width / 2 ? at.x - 16 - tipW : at.x + 16)));
|
|
2130
|
+
const top = round(Math.max(0, Math.min(height - tipH, at.y - tipH - 12)));
|
|
2131
|
+
if (appearing || reduced()) {
|
|
2132
|
+
gsap.killTweensOf(tooltip);
|
|
2133
|
+
gsap.set(tooltip, { x: left, y: top });
|
|
2134
|
+
if (!reduced()) keep(gsap.fromTo(tooltip, { opacity: 0, scale: 0.9 }, { opacity: 1, scale: 1, duration: 0.25, ease: EASE.pop }));
|
|
2135
|
+
else gsap.set(tooltip, { opacity: 1, scale: 1 });
|
|
2136
|
+
} else {
|
|
2137
|
+
gsap.killTweensOf(tooltip, "opacity,scale");
|
|
2138
|
+
gsap.set(tooltip, { opacity: 1, scale: 1 });
|
|
2139
|
+
keep(gsap.to(tooltip, { x: left, y: top, duration: 0.25, ease: "power3.out", overwrite: "auto" }));
|
|
2140
|
+
}
|
|
2141
|
+
};
|
|
2142
|
+
const fillLegend = (items2, show2, type = "bar") => {
|
|
2143
|
+
legend.replaceChildren();
|
|
2144
|
+
legend.hidden = !show2 || !items2.length;
|
|
2145
|
+
if (legend.hidden) return [];
|
|
2146
|
+
return items2.map((item) => {
|
|
2147
|
+
const li = document.createElement("li");
|
|
2148
|
+
const swatch = document.createElement("span");
|
|
2149
|
+
swatch.dataset.mkChartSwatch = "";
|
|
2150
|
+
swatch.dataset.mkType = type;
|
|
2151
|
+
swatch.style.background = item.color;
|
|
2152
|
+
swatch.style.color = item.color;
|
|
2153
|
+
swatch.setAttribute("aria-hidden", "true");
|
|
2154
|
+
const name = document.createElement("span");
|
|
2155
|
+
name.textContent = item.name;
|
|
2156
|
+
li.append(swatch, name);
|
|
2157
|
+
legend.append(li);
|
|
2158
|
+
return li;
|
|
2159
|
+
});
|
|
2160
|
+
};
|
|
2161
|
+
const fillTable = (caption, head, rows) => {
|
|
2162
|
+
table.replaceChildren();
|
|
2163
|
+
const cap = document.createElement("caption");
|
|
2164
|
+
cap.textContent = caption;
|
|
2165
|
+
const thead = document.createElement("thead");
|
|
2166
|
+
const headRow = document.createElement("tr");
|
|
2167
|
+
for (const text of head) {
|
|
2168
|
+
const th = document.createElement("th");
|
|
2169
|
+
th.scope = "col";
|
|
2170
|
+
th.textContent = text;
|
|
2171
|
+
headRow.append(th);
|
|
2172
|
+
}
|
|
2173
|
+
thead.append(headRow);
|
|
2174
|
+
const body = document.createElement("tbody");
|
|
2175
|
+
for (const cells of rows) {
|
|
2176
|
+
const row = document.createElement("tr");
|
|
2177
|
+
cells.forEach((text, i) => {
|
|
2178
|
+
const cell = document.createElement(i ? "td" : "th");
|
|
2179
|
+
if (!i) cell.scope = "row";
|
|
2180
|
+
cell.textContent = text;
|
|
2181
|
+
row.append(cell);
|
|
2182
|
+
});
|
|
2183
|
+
body.append(row);
|
|
2184
|
+
}
|
|
2185
|
+
table.append(cap, thead, body);
|
|
2186
|
+
};
|
|
2187
|
+
return { plot, canvas, tooltip, empty, legend, table, live, tip, fillLegend, fillTable, keep };
|
|
2188
|
+
}
|
|
2189
|
+
function onWidth(root, scope, draw) {
|
|
2190
|
+
if (typeof ResizeObserver !== "function") return;
|
|
2191
|
+
let width = 0;
|
|
2192
|
+
let frame = 0;
|
|
2193
|
+
const observer = scope.observe(
|
|
2194
|
+
new ResizeObserver(() => {
|
|
2195
|
+
const next = Math.round(root.clientWidth);
|
|
2196
|
+
if (!next || next === width) return;
|
|
2197
|
+
const first = !width;
|
|
2198
|
+
width = next;
|
|
2199
|
+
if (first) return;
|
|
2200
|
+
cancelAnimationFrame(frame);
|
|
2201
|
+
frame = requestAnimationFrame(draw);
|
|
2202
|
+
})
|
|
2203
|
+
);
|
|
2204
|
+
observer.observe(root);
|
|
2205
|
+
scope.add(() => cancelAnimationFrame(frame));
|
|
2206
|
+
}
|
|
2207
|
+
|
|
2208
|
+
// src/components/pie-chart/engine.ts
|
|
2209
|
+
var PULL = 7;
|
|
2210
|
+
function createPieChart(root, initial = {}) {
|
|
2211
|
+
if (!root) return inert({ update() {
|
|
2212
|
+
}, select() {
|
|
2213
|
+
}, selected: null });
|
|
2214
|
+
registerMotion();
|
|
2215
|
+
const scope = createScope();
|
|
2216
|
+
const frame = chartFrame(root, scope, "pie");
|
|
2217
|
+
const { plot, canvas, keep } = frame;
|
|
2218
|
+
let options = { ...initial };
|
|
2219
|
+
let selected = null;
|
|
2220
|
+
const center = document.createElement("div");
|
|
2221
|
+
center.dataset.mkPieCenter = "";
|
|
2222
|
+
center.setAttribute("aria-hidden", "true");
|
|
2223
|
+
const centerValue = document.createElement("strong");
|
|
2224
|
+
const centerLabel = document.createElement("span");
|
|
2225
|
+
center.append(centerValue, centerLabel);
|
|
2226
|
+
plot.append(center);
|
|
2227
|
+
scope.add(() => center.remove());
|
|
2228
|
+
const format = (value) => options.formatValue ? options.formatValue(value) : formatNumber(value);
|
|
2229
|
+
const slices = () => (options.data ?? []).filter((slice) => finite2(slice.value) && slice.value > 0).map((slice, i, all) => ({ label: String(slice.label), value: slice.value, color: slice.color ?? slotColor(i, Math.max(2, all.length)) }));
|
|
2230
|
+
let model = slices();
|
|
2231
|
+
const total = () => model.reduce((sum, slice) => sum + slice.value, 0);
|
|
2232
|
+
const share = (value) => new Intl.NumberFormat(void 0, { style: "percent", maximumFractionDigits: 1 }).format(value / (total() || 1));
|
|
2233
|
+
const donut = () => Math.min(0.9, Math.max(0, finite2(options.donut) ? options.donut : 0.62));
|
|
2234
|
+
const anglesOf = (values) => {
|
|
2235
|
+
const sum = values.reduce((a, b) => a + b, 0) || 1;
|
|
2236
|
+
const pad = values.filter((v) => v > 0).length > 1 ? (finite2(options.gap) ? options.gap : 1.5) * Math.PI / 180 : 0;
|
|
2237
|
+
let at = 0;
|
|
2238
|
+
return values.map((value) => {
|
|
2239
|
+
const sweep = value / sum * Math.PI * 2;
|
|
2240
|
+
const a0 = at + Math.min(pad / 2, sweep / 4);
|
|
2241
|
+
const a1 = at + sweep - Math.min(pad / 2, sweep / 4);
|
|
2242
|
+
at += sweep;
|
|
2243
|
+
return [a0, Math.max(a0, a1)];
|
|
2244
|
+
});
|
|
2245
|
+
};
|
|
2246
|
+
let paths = [];
|
|
2247
|
+
let shown = [];
|
|
2248
|
+
let geometry = { size: 240, cx: 120, cy: 120, outer: 112, inner: 70 };
|
|
2249
|
+
let glide = null;
|
|
2250
|
+
scope.add(() => glide?.kill());
|
|
2251
|
+
const measure = () => {
|
|
2252
|
+
const width = Math.round(plot.clientWidth || root.clientWidth || 0);
|
|
2253
|
+
const wanted = finite2(options.size) ? options.size : 240;
|
|
2254
|
+
const size = Math.max(120, width ? Math.min(wanted, width) : wanted);
|
|
2255
|
+
const outer = size / 2 - PULL - 2;
|
|
2256
|
+
geometry = { size, cx: size / 2, cy: size / 2, outer, inner: outer * donut() };
|
|
2257
|
+
canvas.setAttribute("viewBox", `0 0 ${size} ${size}`);
|
|
2258
|
+
canvas.setAttribute("width", String(size));
|
|
2259
|
+
canvas.setAttribute("height", String(size));
|
|
2260
|
+
plot.style.setProperty("--mk-pie-size", `${size}px`);
|
|
2261
|
+
};
|
|
2262
|
+
scope.add(() => plot.style.removeProperty("--mk-pie-size"));
|
|
2263
|
+
const paint = (angles) => {
|
|
2264
|
+
shown = angles;
|
|
2265
|
+
const { cx, cy, outer, inner } = geometry;
|
|
2266
|
+
paths.forEach((path, i) => {
|
|
2267
|
+
const [a0, a1] = angles[i] ?? [0, 0];
|
|
2268
|
+
path.setAttribute("d", a1 - a0 > 1e-4 ? arcPath(cx, cy, outer, inner, a0, a1) : "");
|
|
2269
|
+
});
|
|
2270
|
+
};
|
|
2271
|
+
const build = () => {
|
|
2272
|
+
canvas.replaceChildren();
|
|
2273
|
+
const group = svg2("g", { "data-mk-part": "series" });
|
|
2274
|
+
paths = model.map((slice, i) => {
|
|
2275
|
+
const path = svg2("path", { "data-mk-index": i });
|
|
2276
|
+
path.style.fill = slice.color;
|
|
2277
|
+
group.append(path);
|
|
2278
|
+
return path;
|
|
2279
|
+
});
|
|
2280
|
+
canvas.append(group);
|
|
2281
|
+
};
|
|
2282
|
+
const paintCenter = (animate) => {
|
|
2283
|
+
center.hidden = donut() < 0.35 || !model.length;
|
|
2284
|
+
if (center.hidden) return;
|
|
2285
|
+
const slice = selected === null ? null : model[selected];
|
|
2286
|
+
const text = slice ? format(slice.value) : format(total());
|
|
2287
|
+
const caption = slice ? `${slice.label} \xB7 ${share(slice.value)}` : options.totalLabel ?? "Total";
|
|
2288
|
+
const changed = centerLabel.textContent !== caption;
|
|
2289
|
+
if (centerValue.textContent !== text) {
|
|
2290
|
+
if (animate && !reduced() && centerValue.textContent) keep(rollDigits(centerValue, text, 1, 0.35));
|
|
2291
|
+
else centerValue.textContent = text;
|
|
2292
|
+
}
|
|
2293
|
+
centerLabel.textContent = caption;
|
|
2294
|
+
if (changed && animate && !reduced()) keep(blurIn(centerLabel, { from: "soft", duration: 0.3 }));
|
|
2295
|
+
};
|
|
2296
|
+
const paintSelection = (announce) => {
|
|
2297
|
+
const { outer } = geometry;
|
|
2298
|
+
paths.forEach((path, i) => {
|
|
2299
|
+
const chosen = i === selected;
|
|
2300
|
+
path.toggleAttribute("data-mk-dim", selected !== null && !chosen);
|
|
2301
|
+
if (!chosen) {
|
|
2302
|
+
path.style.transform = "";
|
|
2303
|
+
return;
|
|
2304
|
+
}
|
|
2305
|
+
const [a0, a1] = shown[i] ?? [0, 0];
|
|
2306
|
+
const [dx, dy] = polar(0, 0, Math.min(PULL, outer * 0.06), (a0 + a1) / 2);
|
|
2307
|
+
path.style.transform = `translate(${round(dx)}px, ${round(dy)}px)`;
|
|
2308
|
+
});
|
|
2309
|
+
legendItems.forEach((item, i) => item.toggleAttribute("data-mk-dim", selected !== null && i !== selected));
|
|
2310
|
+
paintCenter(true);
|
|
2311
|
+
const slice = selected === null ? null : model[selected];
|
|
2312
|
+
if (slice && center.hidden) {
|
|
2313
|
+
const [a0, a1] = shown[selected] ?? [0, 0];
|
|
2314
|
+
const [x, y] = polar(geometry.cx, geometry.cy, geometry.outer * 0.7, (a0 + a1) / 2);
|
|
2315
|
+
frame.tip({ x, y, title: slice.label, rows: [{ name: share(slice.value), color: slice.color, value: format(slice.value) }] });
|
|
2316
|
+
} else frame.tip(null);
|
|
2317
|
+
if (announce && slice) frame.live.textContent = `${slice.label}: ${format(slice.value)}, ${share(slice.value)}`;
|
|
2318
|
+
};
|
|
2319
|
+
let legendItems = [];
|
|
2320
|
+
const draw = (animate) => {
|
|
2321
|
+
glide?.kill();
|
|
2322
|
+
const before = shown;
|
|
2323
|
+
const sameCount = paths.length === model.length;
|
|
2324
|
+
const name = options.label ?? "Gr\xE1fica circular";
|
|
2325
|
+
canvas.setAttribute("aria-label", model.length ? `${name}. ${model.length} partes, total ${format(total())}. Usa las flechas para recorrerlas.` : `${name}: ${options.emptyLabel ?? "sin datos"}`);
|
|
2326
|
+
frame.fillTable(name, ["Parte", "Valor", "Porcentaje"], model.map((slice) => [slice.label, format(slice.value), share(slice.value)]));
|
|
2327
|
+
legendItems = frame.fillLegend(model.map((slice) => ({ name: slice.label, color: slice.color })), options.legend ?? true);
|
|
2328
|
+
legendItems.forEach((item, i) => {
|
|
2329
|
+
item.addEventListener("pointerenter", () => select(i));
|
|
2330
|
+
item.addEventListener("pointerleave", () => select(null));
|
|
2331
|
+
});
|
|
2332
|
+
frame.empty.hidden = model.length > 0;
|
|
2333
|
+
frame.empty.textContent = options.emptyLabel ?? "Sin datos para mostrar";
|
|
2334
|
+
plot.hidden = !model.length;
|
|
2335
|
+
if (!model.length) {
|
|
2336
|
+
canvas.replaceChildren();
|
|
2337
|
+
paths = [];
|
|
2338
|
+
shown = [];
|
|
2339
|
+
selected = null;
|
|
2340
|
+
return;
|
|
2341
|
+
}
|
|
2342
|
+
if (selected !== null && selected >= model.length) selected = null;
|
|
2343
|
+
measure();
|
|
2344
|
+
if (!sameCount || !before.length) build();
|
|
2345
|
+
paths.forEach((path, i) => {
|
|
2346
|
+
path.style.fill = model[i].color;
|
|
2347
|
+
});
|
|
2348
|
+
const target = anglesOf(model.map((slice) => slice.value));
|
|
2349
|
+
const done = () => {
|
|
2350
|
+
paint(target);
|
|
2351
|
+
paintSelection(false);
|
|
2352
|
+
};
|
|
2353
|
+
if (!animate || reduced()) return done();
|
|
2354
|
+
const state = { t: 0 };
|
|
2355
|
+
if (sameCount && before.length) {
|
|
2356
|
+
glide = gsap.to(state, {
|
|
2357
|
+
t: 1,
|
|
2358
|
+
duration: 0.6,
|
|
2359
|
+
ease: EASE.swatch,
|
|
2360
|
+
onUpdate: () => paint(target.map(([a0, a1], i) => [before[i][0] + (a0 - before[i][0]) * state.t, before[i][1] + (a1 - before[i][1]) * state.t])),
|
|
2361
|
+
onComplete: done
|
|
2362
|
+
});
|
|
2363
|
+
paintCenter(true);
|
|
2364
|
+
return;
|
|
2365
|
+
}
|
|
2366
|
+
paint(target.map(() => [0, 0]));
|
|
2367
|
+
centerValue.textContent = format(0);
|
|
2368
|
+
glide = gsap.to(state, {
|
|
2369
|
+
t: 1,
|
|
2370
|
+
duration: 0.9,
|
|
2371
|
+
ease: EASE.signature,
|
|
2372
|
+
onUpdate: () => paint(target.map(([a0, a1]) => [a0 * state.t, a1 * state.t])),
|
|
2373
|
+
onComplete: done
|
|
2374
|
+
});
|
|
2375
|
+
paintCenter(true);
|
|
2376
|
+
};
|
|
2377
|
+
const select = (index, announce = false) => {
|
|
2378
|
+
if (scope.disposed) return;
|
|
2379
|
+
const next = index === null || !model.length ? null : (Math.round(index) % model.length + model.length) % model.length;
|
|
2380
|
+
if (next === selected) return;
|
|
2381
|
+
selected = next;
|
|
2382
|
+
paintSelection(announce);
|
|
2383
|
+
options.onSelect?.(selected);
|
|
2384
|
+
const slice = selected === null ? null : model[selected];
|
|
2385
|
+
root.dispatchEvent(new CustomEvent("mk:select", { bubbles: true, detail: { index: selected, label: slice?.label ?? null, value: slice?.value ?? null } }));
|
|
2386
|
+
};
|
|
2387
|
+
const indexAt = (event) => {
|
|
2388
|
+
const target = event.target.closest("[data-mk-index]");
|
|
2389
|
+
return target ? Number(target.getAttribute("data-mk-index")) : null;
|
|
2390
|
+
};
|
|
2391
|
+
scope.on(canvas, "pointermove", (event) => select(indexAt(event)));
|
|
2392
|
+
scope.on(canvas, "pointerdown", (event) => select(indexAt(event)));
|
|
2393
|
+
scope.on(canvas, "pointerleave", (event) => {
|
|
2394
|
+
if (event.pointerType === "mouse") select(null);
|
|
2395
|
+
});
|
|
2396
|
+
scope.on(canvas, "focus", () => {
|
|
2397
|
+
if (selected === null && model.length) select(0, true);
|
|
2398
|
+
});
|
|
2399
|
+
scope.on(canvas, "blur", () => select(null));
|
|
2400
|
+
scope.on(canvas, "keydown", (event) => {
|
|
2401
|
+
const key = event.key;
|
|
2402
|
+
const moves = { ArrowRight: (selected ?? -1) + 1, ArrowDown: (selected ?? -1) + 1, ArrowLeft: (selected ?? 1) - 1, ArrowUp: (selected ?? 1) - 1, Home: 0, End: model.length - 1, Escape: null };
|
|
2403
|
+
if (!(key in moves) || !model.length) return;
|
|
2404
|
+
event.preventDefault();
|
|
2405
|
+
select(moves[key], true);
|
|
2406
|
+
});
|
|
2407
|
+
onWidth(root, scope, () => {
|
|
2408
|
+
measure();
|
|
2409
|
+
paint(shown);
|
|
2410
|
+
paintSelection(false);
|
|
2411
|
+
});
|
|
2412
|
+
draw(true);
|
|
2413
|
+
return {
|
|
2414
|
+
update(next) {
|
|
2415
|
+
if (scope.disposed) return;
|
|
2416
|
+
options = { ...options, ...next };
|
|
2417
|
+
model = slices();
|
|
2418
|
+
draw(true);
|
|
2419
|
+
},
|
|
2420
|
+
select: (index) => select(index),
|
|
2421
|
+
get selected() {
|
|
2422
|
+
return selected;
|
|
2423
|
+
},
|
|
2424
|
+
destroy: () => scope.dispose()
|
|
2425
|
+
};
|
|
2426
|
+
}
|
|
2427
|
+
|
|
2428
|
+
// src/components/radar-chart/engine.ts
|
|
2429
|
+
function niceMax(value) {
|
|
2430
|
+
if (value <= 0) return 1;
|
|
2431
|
+
const power = 10 ** Math.floor(Math.log10(value));
|
|
2432
|
+
const fraction = value / power;
|
|
2433
|
+
return (fraction <= 1 ? 1 : fraction <= 2 ? 2 : fraction <= 2.5 ? 2.5 : fraction <= 5 ? 5 : 10) * power;
|
|
2434
|
+
}
|
|
2435
|
+
function createRadarChart(root, initial = {}) {
|
|
2436
|
+
if (!root) return inert({ update() {
|
|
2437
|
+
}, select() {
|
|
2438
|
+
}, selected: null });
|
|
2439
|
+
registerMotion();
|
|
2440
|
+
const scope = createScope();
|
|
2441
|
+
const frame = chartFrame(root, scope, "radar");
|
|
2442
|
+
const { plot, canvas, keep } = frame;
|
|
2443
|
+
let options = { ...initial };
|
|
2444
|
+
let selected = null;
|
|
2445
|
+
let focusSeries = null;
|
|
2446
|
+
const format = (value) => options.formatValue ? options.formatValue(value) : formatNumber(value);
|
|
2447
|
+
const modelOf = () => {
|
|
2448
|
+
const axes = (options.axes ?? []).map(String);
|
|
2449
|
+
const raw = options.series ?? [];
|
|
2450
|
+
return {
|
|
2451
|
+
axes,
|
|
2452
|
+
series: raw.map((series, i) => ({
|
|
2453
|
+
name: String(series.name ?? ""),
|
|
2454
|
+
values: axes.map((_, k) => finite2(series.values[k]) ? Math.max(0, series.values[k]) : null),
|
|
2455
|
+
color: series.color ?? slotColor(i, raw.length)
|
|
2456
|
+
}))
|
|
2457
|
+
};
|
|
2458
|
+
};
|
|
2459
|
+
let model = modelOf();
|
|
2460
|
+
const hasData = () => model.axes.length >= 3 && model.series.some((series) => series.values.some(finite2));
|
|
2461
|
+
const maxOf = () => finite2(options.max) && options.max > 0 ? options.max : niceMax(Math.max(0, ...model.series.flatMap((series) => series.values.filter(finite2))));
|
|
2462
|
+
let geometry = { size: 300, cx: 150, cy: 150, r: 110 };
|
|
2463
|
+
const angle = (k) => k / model.axes.length * Math.PI * 2;
|
|
2464
|
+
let shapes = [];
|
|
2465
|
+
let guide = null;
|
|
2466
|
+
let labels = [];
|
|
2467
|
+
let shown = [];
|
|
2468
|
+
let glide = null;
|
|
2469
|
+
scope.add(() => glide?.kill());
|
|
2470
|
+
const measure = () => {
|
|
2471
|
+
const width = Math.round(plot.clientWidth || root.clientWidth || 0);
|
|
2472
|
+
const wanted = finite2(options.size) ? options.size : 300;
|
|
2473
|
+
const size = Math.max(180, width ? Math.min(wanted, width) : wanted);
|
|
2474
|
+
const r = size / 2 - Math.min(64, Math.max(36, Math.max(...model.axes.map((axis) => axis.length)) * 3.6 + 14));
|
|
2475
|
+
geometry = { size, cx: size / 2, cy: size / 2, r: Math.max(40, r) };
|
|
2476
|
+
canvas.setAttribute("viewBox", `0 0 ${size} ${size}`);
|
|
2477
|
+
canvas.setAttribute("width", String(size));
|
|
2478
|
+
canvas.setAttribute("height", String(size));
|
|
2479
|
+
plot.style.setProperty("--mk-radar-size", `${size}px`);
|
|
2480
|
+
};
|
|
2481
|
+
scope.add(() => plot.style.removeProperty("--mk-radar-size"));
|
|
2482
|
+
const pointOf = (value, k, max) => polar(geometry.cx, geometry.cy, Math.min(value, max) / max * geometry.r, angle(k));
|
|
2483
|
+
const build = () => {
|
|
2484
|
+
canvas.replaceChildren();
|
|
2485
|
+
const { cx, cy, r } = geometry;
|
|
2486
|
+
const n = model.axes.length;
|
|
2487
|
+
const levels = Math.max(1, Math.round(options.levels ?? 4));
|
|
2488
|
+
const grid = svg2("g", { "data-mk-part": "grid" });
|
|
2489
|
+
for (let level = 1; level <= levels; level++) {
|
|
2490
|
+
const radius = r * level / levels;
|
|
2491
|
+
if (options.grid === "circle") grid.append(svg2("circle", { cx, cy, r: round(radius) }));
|
|
2492
|
+
else {
|
|
2493
|
+
const points = Array.from({ length: n }, (_, k) => polar(cx, cy, radius, angle(k)).map(round).join(",")).join(" ");
|
|
2494
|
+
grid.append(svg2("polygon", { points }));
|
|
2495
|
+
}
|
|
2496
|
+
}
|
|
2497
|
+
for (let k = 0; k < n; k++) {
|
|
2498
|
+
const [x, y] = polar(cx, cy, r, angle(k));
|
|
2499
|
+
grid.append(svg2("line", { x1: cx, y1: cy, x2: round(x), y2: round(y), "data-mk-spoke": k }));
|
|
2500
|
+
}
|
|
2501
|
+
canvas.append(grid);
|
|
2502
|
+
const names = svg2("g", { "data-mk-part": "axes" });
|
|
2503
|
+
labels = model.axes.map((axis, k) => {
|
|
2504
|
+
const a = angle(k);
|
|
2505
|
+
const [x, y] = polar(cx, cy, r + 14, a);
|
|
2506
|
+
const sin = Math.sin(a);
|
|
2507
|
+
const text = svg2("text", { x: round(x), y: round(y), "text-anchor": Math.abs(sin) < 0.2 ? "middle" : sin > 0 ? "start" : "end", "dominant-baseline": "middle" });
|
|
2508
|
+
text.textContent = axis;
|
|
2509
|
+
names.append(text);
|
|
2510
|
+
return text;
|
|
2511
|
+
});
|
|
2512
|
+
canvas.append(names);
|
|
2513
|
+
guide = svg2("line", { "data-mk-part": "guide" });
|
|
2514
|
+
guide.style.display = "none";
|
|
2515
|
+
canvas.append(guide);
|
|
2516
|
+
const series = svg2("g", { "data-mk-part": "series" });
|
|
2517
|
+
shapes = model.series.map((each, s) => {
|
|
2518
|
+
const group = svg2("g", { "data-mk-series": s });
|
|
2519
|
+
const area = svg2("path", { "data-mk-part": "area" });
|
|
2520
|
+
area.style.fill = each.color;
|
|
2521
|
+
const edge = svg2("path", { "data-mk-part": "edge" });
|
|
2522
|
+
edge.style.stroke = each.color;
|
|
2523
|
+
group.append(area, edge);
|
|
2524
|
+
const dots = model.axes.map(() => {
|
|
2525
|
+
const dot = svg2("circle", { r: 3, "data-mk-part": "dot" });
|
|
2526
|
+
dot.style.fill = each.color;
|
|
2527
|
+
group.append(dot);
|
|
2528
|
+
return dot;
|
|
2529
|
+
});
|
|
2530
|
+
series.append(group);
|
|
2531
|
+
return { area, edge, dots };
|
|
2532
|
+
});
|
|
2533
|
+
canvas.append(series);
|
|
2534
|
+
};
|
|
2535
|
+
const paint = (values) => {
|
|
2536
|
+
shown = values;
|
|
2537
|
+
const max = maxOf();
|
|
2538
|
+
shapes.forEach((shape, s) => {
|
|
2539
|
+
const list = values[s] ?? [];
|
|
2540
|
+
const points = model.axes.map((_, k) => pointOf(list[k] ?? 0, k, max));
|
|
2541
|
+
const d = `M${points.map(([x, y]) => `${round(x)},${round(y)}`).join("L")}Z`;
|
|
2542
|
+
shape.area.setAttribute("d", d);
|
|
2543
|
+
shape.edge.setAttribute("d", d);
|
|
2544
|
+
shape.dots.forEach((dot, k) => {
|
|
2545
|
+
dot.setAttribute("cx", String(round(points[k][0])));
|
|
2546
|
+
dot.setAttribute("cy", String(round(points[k][1])));
|
|
2547
|
+
dot.style.display = finite2(model.series[s].values[k]) ? "" : "none";
|
|
2548
|
+
});
|
|
2549
|
+
});
|
|
2550
|
+
};
|
|
2551
|
+
const paintSelection = (announce) => {
|
|
2552
|
+
shapes.forEach((shape, s) => {
|
|
2553
|
+
shape.area.parentElement.toggleAttribute("data-mk-dim", focusSeries !== null && s !== focusSeries);
|
|
2554
|
+
shape.dots.forEach((dot, k2) => {
|
|
2555
|
+
const on = k2 === selected;
|
|
2556
|
+
if (on === dot.hasAttribute("data-mk-on")) return;
|
|
2557
|
+
dot.toggleAttribute("data-mk-on", on);
|
|
2558
|
+
const r = on ? 5 : 3;
|
|
2559
|
+
if (reduced()) dot.setAttribute("r", String(r));
|
|
2560
|
+
else keep(gsap.to(dot, { attr: { r }, duration: on ? 0.3 : 0.2, ease: on ? EASE.pop : "power2.out" }));
|
|
2561
|
+
});
|
|
2562
|
+
});
|
|
2563
|
+
labels.forEach((label, k2) => label.toggleAttribute("data-mk-on", k2 === selected));
|
|
2564
|
+
if (selected === null || !guide) {
|
|
2565
|
+
if (guide) guide.style.display = "none";
|
|
2566
|
+
frame.tip(null);
|
|
2567
|
+
return;
|
|
2568
|
+
}
|
|
2569
|
+
const k = selected;
|
|
2570
|
+
const [x, y] = polar(geometry.cx, geometry.cy, geometry.r, angle(k));
|
|
2571
|
+
Object.entries({ x1: geometry.cx, y1: geometry.cy, x2: round(x), y2: round(y) }).forEach(([name, value]) => guide.setAttribute(name, String(value)));
|
|
2572
|
+
guide.style.display = "";
|
|
2573
|
+
const rows = model.series.map((series) => ({ name: series.name, color: series.color, value: finite2(series.values[k]) ? format(series.values[k]) : "\u2014" }));
|
|
2574
|
+
const max = maxOf();
|
|
2575
|
+
const top = Math.min(...model.series.map((series) => pointOf(series.values[k] ?? 0, k, max)[1]));
|
|
2576
|
+
frame.tip({ x: round(x), y: Math.min(round(y), top), title: model.axes[k], rows });
|
|
2577
|
+
if (announce) frame.live.textContent = `${model.axes[k]}. ${rows.map((row) => `${row.name ? `${row.name}: ` : ""}${row.value}`).join(". ")}`;
|
|
2578
|
+
};
|
|
2579
|
+
let legendItems = [];
|
|
2580
|
+
const draw = (animate) => {
|
|
2581
|
+
glide?.kill();
|
|
2582
|
+
const name = options.label ?? "Gr\xE1fica de radar";
|
|
2583
|
+
canvas.setAttribute("aria-label", hasData() ? `${name}. ${model.axes.length} medidas: ${model.axes.join(", ")}. Series: ${model.series.map((series) => series.name).join(", ")}. Usa las flechas para recorrer las medidas.` : `${name}: ${options.emptyLabel ?? "sin datos"}`);
|
|
2584
|
+
frame.fillTable(name, ["Medida", ...model.series.map((series) => series.name || "Valor")], model.axes.map((axis, k) => [axis, ...model.series.map((series) => finite2(series.values[k]) ? format(series.values[k]) : "\u2014")]));
|
|
2585
|
+
legendItems = frame.fillLegend(model.series, options.legend ?? model.series.length > 1, "area");
|
|
2586
|
+
legendItems.forEach((item, s) => {
|
|
2587
|
+
item.addEventListener("pointerenter", () => {
|
|
2588
|
+
focusSeries = s;
|
|
2589
|
+
paintSelection(false);
|
|
2590
|
+
});
|
|
2591
|
+
item.addEventListener("pointerleave", () => {
|
|
2592
|
+
focusSeries = null;
|
|
2593
|
+
paintSelection(false);
|
|
2594
|
+
});
|
|
2595
|
+
});
|
|
2596
|
+
const ok = hasData();
|
|
2597
|
+
frame.empty.hidden = ok;
|
|
2598
|
+
frame.empty.textContent = options.emptyLabel ?? "Sin datos para mostrar";
|
|
2599
|
+
plot.hidden = !ok;
|
|
2600
|
+
if (!ok) {
|
|
2601
|
+
canvas.replaceChildren();
|
|
2602
|
+
shapes = [];
|
|
2603
|
+
shown = [];
|
|
2604
|
+
selected = null;
|
|
2605
|
+
return;
|
|
2606
|
+
}
|
|
2607
|
+
if (selected !== null && selected >= model.axes.length) selected = null;
|
|
2608
|
+
const before = shown;
|
|
2609
|
+
const comparable = before.length === model.series.length && before.every((list) => list.length === model.axes.length) && shapes.length === model.series.length;
|
|
2610
|
+
measure();
|
|
2611
|
+
build();
|
|
2612
|
+
const target = model.series.map((series) => series.values.map((value) => value ?? 0));
|
|
2613
|
+
const done = () => {
|
|
2614
|
+
paint(target);
|
|
2615
|
+
paintSelection(false);
|
|
2616
|
+
};
|
|
2617
|
+
if (!animate || reduced()) return done();
|
|
2618
|
+
const state = { t: 0 };
|
|
2619
|
+
if (comparable) {
|
|
2620
|
+
glide = gsap.to(state, { t: 1, duration: 0.6, ease: EASE.swatch, onUpdate: () => paint(target.map((list, s) => list.map((value, k) => before[s][k] + (value - before[s][k]) * state.t))), onComplete: done });
|
|
2621
|
+
return;
|
|
2622
|
+
}
|
|
2623
|
+
const stagger = 0.09;
|
|
2624
|
+
const total = 0.7 + stagger * (target.length - 1);
|
|
2625
|
+
paint(target.map((list) => list.map(() => 0)));
|
|
2626
|
+
glide = gsap.to(state, {
|
|
2627
|
+
t: total,
|
|
2628
|
+
duration: total,
|
|
2629
|
+
ease: "none",
|
|
2630
|
+
onUpdate: () => paint(target.map((list, s) => {
|
|
2631
|
+
const local = Math.min(1, Math.max(0, (state.t - s * stagger) / 0.7));
|
|
2632
|
+
const eased2 = gsap.parseEase(EASE.swatch)(local);
|
|
2633
|
+
return list.map((value) => value * eased2);
|
|
2634
|
+
})),
|
|
2635
|
+
onComplete: done
|
|
2636
|
+
});
|
|
2637
|
+
};
|
|
2638
|
+
const select = (axis, announce = false) => {
|
|
2639
|
+
if (scope.disposed) return;
|
|
2640
|
+
const n = model.axes.length;
|
|
2641
|
+
const next = axis === null || !hasData() ? null : (Math.round(axis) % n + n) % n;
|
|
2642
|
+
if (next === selected) return;
|
|
2643
|
+
selected = next;
|
|
2644
|
+
paintSelection(announce);
|
|
2645
|
+
options.onSelect?.(selected);
|
|
2646
|
+
root.dispatchEvent(new CustomEvent("mk:select", { bubbles: true, detail: { index: selected, label: selected === null ? null : model.axes[selected], values: selected === null ? [] : model.series.map((series) => series.values[selected]) } }));
|
|
2647
|
+
};
|
|
2648
|
+
const axisAt = (event) => {
|
|
2649
|
+
const box = canvas.getBoundingClientRect();
|
|
2650
|
+
const scale = box.width ? geometry.size / box.width : 1;
|
|
2651
|
+
const x = (event.clientX - box.left) * scale - geometry.cx;
|
|
2652
|
+
const y = (event.clientY - box.top) * scale - geometry.cy;
|
|
2653
|
+
if (Math.hypot(x, y) < geometry.r * 0.12) return selected;
|
|
2654
|
+
const a = (Math.atan2(x, -y) + Math.PI * 2) % (Math.PI * 2);
|
|
2655
|
+
return Math.round(a / (Math.PI * 2 / model.axes.length)) % model.axes.length;
|
|
2656
|
+
};
|
|
2657
|
+
scope.on(canvas, "pointermove", (event) => select(axisAt(event)));
|
|
2658
|
+
scope.on(canvas, "pointerdown", (event) => select(axisAt(event)));
|
|
2659
|
+
scope.on(canvas, "pointerleave", (event) => {
|
|
2660
|
+
if (event.pointerType === "mouse") select(null);
|
|
2661
|
+
});
|
|
2662
|
+
scope.on(canvas, "focus", () => {
|
|
2663
|
+
if (selected === null && hasData()) select(0, true);
|
|
2664
|
+
});
|
|
2665
|
+
scope.on(canvas, "blur", () => select(null));
|
|
2666
|
+
scope.on(canvas, "keydown", (event) => {
|
|
2667
|
+
const key = event.key;
|
|
2668
|
+
const moves = { ArrowRight: (selected ?? -1) + 1, ArrowDown: (selected ?? -1) + 1, ArrowLeft: (selected ?? 1) - 1, ArrowUp: (selected ?? 1) - 1, Home: 0, End: model.axes.length - 1, Escape: null };
|
|
2669
|
+
if (!(key in moves) || !hasData()) return;
|
|
2670
|
+
event.preventDefault();
|
|
2671
|
+
select(moves[key], true);
|
|
2672
|
+
});
|
|
2673
|
+
onWidth(root, scope, () => {
|
|
2674
|
+
if (!hasData()) return;
|
|
2675
|
+
measure();
|
|
2676
|
+
build();
|
|
2677
|
+
paint(shown);
|
|
2678
|
+
paintSelection(false);
|
|
2679
|
+
});
|
|
2680
|
+
draw(true);
|
|
2681
|
+
return {
|
|
2682
|
+
update(next) {
|
|
2683
|
+
if (scope.disposed) return;
|
|
2684
|
+
options = { ...options, ...next };
|
|
2685
|
+
model = modelOf();
|
|
2686
|
+
draw(true);
|
|
2687
|
+
},
|
|
2688
|
+
select: (axis) => select(axis),
|
|
2689
|
+
get selected() {
|
|
2690
|
+
return selected;
|
|
2691
|
+
},
|
|
2692
|
+
destroy: () => scope.dispose()
|
|
2693
|
+
};
|
|
2694
|
+
}
|
|
2695
|
+
|
|
2696
|
+
// src/components/radial-chart/engine.ts
|
|
2697
|
+
function strokeArc(cx, cy, r, a0, a1) {
|
|
2698
|
+
const end = a0 + Math.min(a1 - a0, Math.PI * 2 - 1e-3);
|
|
2699
|
+
const [x0, y0] = polar(cx, cy, r, a0);
|
|
2700
|
+
const [x1, y1] = polar(cx, cy, r, end);
|
|
2701
|
+
return `M${round(x0)},${round(y0)}A${round(r)},${round(r)} 0 ${end - a0 > Math.PI ? 1 : 0} 1 ${round(x1)},${round(y1)}`;
|
|
2702
|
+
}
|
|
2703
|
+
function createRadialChart(root, initial = {}) {
|
|
2704
|
+
if (!root) return inert({ update() {
|
|
2705
|
+
}, select() {
|
|
2706
|
+
}, selected: null });
|
|
2707
|
+
registerMotion();
|
|
2708
|
+
const scope = createScope();
|
|
2709
|
+
const frame = chartFrame(root, scope, "radial");
|
|
2710
|
+
const { plot, canvas, keep } = frame;
|
|
2711
|
+
let options = { ...initial };
|
|
2712
|
+
let selected = null;
|
|
2713
|
+
const center = document.createElement("div");
|
|
2714
|
+
center.dataset.mkRadialCenter = "";
|
|
2715
|
+
center.setAttribute("aria-hidden", "true");
|
|
2716
|
+
const centerValue = document.createElement("strong");
|
|
2717
|
+
const centerLabel = document.createElement("span");
|
|
2718
|
+
center.append(centerValue, centerLabel);
|
|
2719
|
+
plot.append(center);
|
|
2720
|
+
scope.add(() => center.remove());
|
|
2721
|
+
const format = (value) => options.formatValue ? options.formatValue(value) : formatNumber(value);
|
|
2722
|
+
const modelOf = () => {
|
|
2723
|
+
const data = (options.data ?? []).filter((datum) => finite2(datum.value));
|
|
2724
|
+
const top = Math.max(0, ...data.map((datum) => datum.value));
|
|
2725
|
+
return data.map((datum, i) => ({
|
|
2726
|
+
label: String(datum.label),
|
|
2727
|
+
value: Math.max(0, datum.value),
|
|
2728
|
+
max: finite2(datum.max) && datum.max > 0 ? datum.max : finite2(options.max) && options.max > 0 ? options.max : top || 1,
|
|
2729
|
+
color: datum.color ?? slotColor(i, Math.max(2, data.length))
|
|
2730
|
+
}));
|
|
2731
|
+
};
|
|
2732
|
+
let model = modelOf();
|
|
2733
|
+
const total = () => model.reduce((sum, ring) => sum + ring.value, 0);
|
|
2734
|
+
const percent = (ring) => new Intl.NumberFormat(void 0, { style: "percent", maximumFractionDigits: 0 }).format(ring.value / ring.max);
|
|
2735
|
+
const sweep = () => Math.min(360, Math.max(90, finite2(options.sweep) ? options.sweep : 360)) * Math.PI / 180;
|
|
2736
|
+
const startAngle = () => -sweep() / 2 + (sweep() >= Math.PI * 2 - 1e-6 ? Math.PI : 0);
|
|
2737
|
+
let geometry = { size: 240, cx: 120, cy: 120, radii: [], thickness: 12 };
|
|
2738
|
+
let arcs = [];
|
|
2739
|
+
let tracks = [];
|
|
2740
|
+
let shown = [];
|
|
2741
|
+
let glide = null;
|
|
2742
|
+
scope.add(() => glide?.kill());
|
|
2743
|
+
const measure = () => {
|
|
2744
|
+
const width = Math.round(plot.clientWidth || root.clientWidth || 0);
|
|
2745
|
+
const wanted = finite2(options.size) ? options.size : 240;
|
|
2746
|
+
const size = Math.max(120, width ? Math.min(wanted, width) : wanted);
|
|
2747
|
+
const n = Math.max(1, model.length);
|
|
2748
|
+
const outer = size / 2 - 4;
|
|
2749
|
+
const fit = outer * 0.6 / (n + (n - 1) * 0.35);
|
|
2750
|
+
const thickness = finite2(options.thickness) ? options.thickness : Math.max(4, Math.min(16, fit));
|
|
2751
|
+
const spacing = thickness * 1.35;
|
|
2752
|
+
geometry = { size, cx: size / 2, cy: size / 2, thickness, radii: model.map((_, i) => outer - thickness / 2 - i * spacing) };
|
|
2753
|
+
canvas.setAttribute("viewBox", `0 0 ${size} ${size}`);
|
|
2754
|
+
canvas.setAttribute("width", String(size));
|
|
2755
|
+
canvas.setAttribute("height", String(size));
|
|
2756
|
+
plot.style.setProperty("--mk-radial-size", `${size}px`);
|
|
2757
|
+
plot.style.setProperty("--mk-radial-thickness", `${thickness}px`);
|
|
2758
|
+
};
|
|
2759
|
+
scope.add(() => {
|
|
2760
|
+
plot.style.removeProperty("--mk-radial-size");
|
|
2761
|
+
plot.style.removeProperty("--mk-radial-thickness");
|
|
2762
|
+
});
|
|
2763
|
+
const build = () => {
|
|
2764
|
+
canvas.replaceChildren();
|
|
2765
|
+
const { cx, cy, radii } = geometry;
|
|
2766
|
+
const a0 = startAngle();
|
|
2767
|
+
const back = svg2("g", { "data-mk-part": "tracks" });
|
|
2768
|
+
const front = svg2("g", { "data-mk-part": "series" });
|
|
2769
|
+
tracks = model.map((_, i) => {
|
|
2770
|
+
const track = svg2("path", { d: strokeArc(cx, cy, radii[i], a0, a0 + sweep()), "data-mk-index": i });
|
|
2771
|
+
back.append(track);
|
|
2772
|
+
return track;
|
|
2773
|
+
});
|
|
2774
|
+
arcs = model.map((ring, i) => {
|
|
2775
|
+
const arc = svg2("path", { "data-mk-index": i, "data-mk-part": "ring" });
|
|
2776
|
+
arc.style.stroke = ring.color;
|
|
2777
|
+
front.append(arc);
|
|
2778
|
+
return arc;
|
|
2779
|
+
});
|
|
2780
|
+
canvas.append(back, front);
|
|
2781
|
+
};
|
|
2782
|
+
const paint = (values) => {
|
|
2783
|
+
shown = values;
|
|
2784
|
+
const { cx, cy, radii } = geometry;
|
|
2785
|
+
const a0 = startAngle();
|
|
2786
|
+
arcs.forEach((arc, i) => {
|
|
2787
|
+
const share = Math.min(1, Math.max(0, (values[i] ?? 0) / model[i].max));
|
|
2788
|
+
arc.setAttribute("d", share > 1e-3 ? strokeArc(cx, cy, radii[i], a0, a0 + sweep() * share) : "");
|
|
2789
|
+
});
|
|
2790
|
+
};
|
|
2791
|
+
const paintCenter = (animate) => {
|
|
2792
|
+
center.hidden = !model.length;
|
|
2793
|
+
const ring = selected === null ? null : model[selected];
|
|
2794
|
+
const text = ring ? format(ring.value) : format(total());
|
|
2795
|
+
const caption = ring ? `${ring.label} \xB7 ${percent(ring)}` : options.totalLabel ?? "Total";
|
|
2796
|
+
const changed = centerLabel.textContent !== caption;
|
|
2797
|
+
if (centerValue.textContent !== text) {
|
|
2798
|
+
if (animate && !reduced() && centerValue.textContent) keep(rollDigits(centerValue, text, 1, 0.35));
|
|
2799
|
+
else centerValue.textContent = text;
|
|
2800
|
+
}
|
|
2801
|
+
centerLabel.textContent = caption;
|
|
2802
|
+
if (changed && animate && !reduced()) keep(blurIn(centerLabel, { from: "soft", duration: 0.3 }));
|
|
2803
|
+
};
|
|
2804
|
+
let legendItems = [];
|
|
2805
|
+
const paintSelection = (announce) => {
|
|
2806
|
+
for (const list of [arcs, tracks]) list.forEach((each, i) => {
|
|
2807
|
+
each.toggleAttribute("data-mk-on", i === selected);
|
|
2808
|
+
each.toggleAttribute("data-mk-dim", selected !== null && i !== selected);
|
|
2809
|
+
});
|
|
2810
|
+
legendItems.forEach((item, i) => item.toggleAttribute("data-mk-dim", selected !== null && i !== selected));
|
|
2811
|
+
paintCenter(true);
|
|
2812
|
+
const ring = selected === null ? null : model[selected];
|
|
2813
|
+
if (announce && ring) frame.live.textContent = `${ring.label}: ${format(ring.value)} de ${format(ring.max)}, ${percent(ring)}`;
|
|
2814
|
+
};
|
|
2815
|
+
const draw = (animate) => {
|
|
2816
|
+
glide?.kill();
|
|
2817
|
+
const name = options.label ?? "Gr\xE1fica radial";
|
|
2818
|
+
canvas.setAttribute("aria-label", model.length ? `${name}. ${model.length} anillos, total ${format(total())}. Usa las flechas para recorrerlos.` : `${name}: ${options.emptyLabel ?? "sin datos"}`);
|
|
2819
|
+
frame.fillTable(name, ["Anillo", "Valor", "Meta", "Avance"], model.map((ring) => [ring.label, format(ring.value), format(ring.max), percent(ring)]));
|
|
2820
|
+
legendItems = frame.fillLegend(model.map((datum) => ({ name: datum.label, color: datum.color })), options.legend ?? true);
|
|
2821
|
+
legendItems.forEach((item, i) => {
|
|
2822
|
+
item.addEventListener("pointerenter", () => select(i));
|
|
2823
|
+
item.addEventListener("pointerleave", () => select(null));
|
|
2824
|
+
});
|
|
2825
|
+
frame.empty.hidden = model.length > 0;
|
|
2826
|
+
frame.empty.textContent = options.emptyLabel ?? "Sin datos para mostrar";
|
|
2827
|
+
plot.hidden = !model.length;
|
|
2828
|
+
if (!model.length) {
|
|
2829
|
+
canvas.replaceChildren();
|
|
2830
|
+
arcs = [];
|
|
2831
|
+
shown = [];
|
|
2832
|
+
selected = null;
|
|
2833
|
+
return;
|
|
2834
|
+
}
|
|
2835
|
+
if (selected !== null && selected >= model.length) selected = null;
|
|
2836
|
+
const before = shown;
|
|
2837
|
+
const comparable = before.length === model.length && arcs.length === model.length;
|
|
2838
|
+
measure();
|
|
2839
|
+
build();
|
|
2840
|
+
const target = model.map((ring) => ring.value);
|
|
2841
|
+
const done = () => {
|
|
2842
|
+
paint(target);
|
|
2843
|
+
paintSelection(false);
|
|
2844
|
+
};
|
|
2845
|
+
if (!animate || reduced()) return done();
|
|
2846
|
+
const state = { t: 0 };
|
|
2847
|
+
if (comparable) {
|
|
2848
|
+
glide = gsap.to(state, { t: 1, duration: 0.6, ease: EASE.swatch, onUpdate: () => paint(target.map((value, i) => before[i] + (value - before[i]) * state.t)), onComplete: done });
|
|
2849
|
+
paintCenter(true);
|
|
2850
|
+
return;
|
|
2851
|
+
}
|
|
2852
|
+
const stagger = 0.08;
|
|
2853
|
+
const length = 0.8 + stagger * (target.length - 1);
|
|
2854
|
+
const ease = gsap.parseEase(EASE.swatch);
|
|
2855
|
+
paint(target.map(() => 0));
|
|
2856
|
+
centerValue.textContent = format(0);
|
|
2857
|
+
glide = gsap.to(state, {
|
|
2858
|
+
t: length,
|
|
2859
|
+
duration: length,
|
|
2860
|
+
ease: "none",
|
|
2861
|
+
onUpdate: () => paint(target.map((value, i) => value * ease(Math.min(1, Math.max(0, (state.t - i * stagger) / 0.8))))),
|
|
2862
|
+
onComplete: done
|
|
2863
|
+
});
|
|
2864
|
+
paintCenter(true);
|
|
2865
|
+
};
|
|
2866
|
+
const select = (index, announce = false) => {
|
|
2867
|
+
if (scope.disposed) return;
|
|
2868
|
+
const next = index === null || !model.length ? null : (Math.round(index) % model.length + model.length) % model.length;
|
|
2869
|
+
if (next === selected) return;
|
|
2870
|
+
selected = next;
|
|
2871
|
+
paintSelection(announce);
|
|
2872
|
+
options.onSelect?.(selected);
|
|
2873
|
+
const ring = selected === null ? null : model[selected];
|
|
2874
|
+
root.dispatchEvent(new CustomEvent("mk:select", { bubbles: true, detail: { index: selected, label: ring?.label ?? null, value: ring?.value ?? null } }));
|
|
2875
|
+
};
|
|
2876
|
+
const ringAt = (event) => {
|
|
2877
|
+
const box = canvas.getBoundingClientRect();
|
|
2878
|
+
const scale = box.width ? geometry.size / box.width : 1;
|
|
2879
|
+
const distance = Math.hypot((event.clientX - box.left) * scale - geometry.cx, (event.clientY - box.top) * scale - geometry.cy);
|
|
2880
|
+
const near = geometry.radii.findIndex((r) => Math.abs(distance - r) <= geometry.thickness * 0.7);
|
|
2881
|
+
return near < 0 ? null : near;
|
|
2882
|
+
};
|
|
2883
|
+
scope.on(canvas, "pointermove", (event) => select(ringAt(event)));
|
|
2884
|
+
scope.on(canvas, "pointerdown", (event) => select(ringAt(event)));
|
|
2885
|
+
scope.on(canvas, "pointerleave", (event) => {
|
|
2886
|
+
if (event.pointerType === "mouse") select(null);
|
|
2887
|
+
});
|
|
2888
|
+
scope.on(canvas, "focus", () => {
|
|
2889
|
+
if (selected === null && model.length) select(0, true);
|
|
2890
|
+
});
|
|
2891
|
+
scope.on(canvas, "blur", () => select(null));
|
|
2892
|
+
scope.on(canvas, "keydown", (event) => {
|
|
2893
|
+
const key = event.key;
|
|
2894
|
+
const moves = { ArrowDown: (selected ?? -1) + 1, ArrowRight: (selected ?? -1) + 1, ArrowUp: (selected ?? 1) - 1, ArrowLeft: (selected ?? 1) - 1, Home: 0, End: model.length - 1, Escape: null };
|
|
2895
|
+
if (!(key in moves) || !model.length) return;
|
|
2896
|
+
event.preventDefault();
|
|
2897
|
+
select(moves[key], true);
|
|
2898
|
+
});
|
|
2899
|
+
onWidth(root, scope, () => {
|
|
2900
|
+
if (!model.length) return;
|
|
2901
|
+
measure();
|
|
2902
|
+
build();
|
|
2903
|
+
paint(shown);
|
|
2904
|
+
paintSelection(false);
|
|
2905
|
+
});
|
|
2906
|
+
draw(true);
|
|
2907
|
+
return {
|
|
2908
|
+
update(next) {
|
|
2909
|
+
if (scope.disposed) return;
|
|
2910
|
+
options = { ...options, ...next };
|
|
2911
|
+
model = modelOf();
|
|
2912
|
+
draw(true);
|
|
2913
|
+
},
|
|
2914
|
+
select: (index) => select(index),
|
|
2915
|
+
get selected() {
|
|
2916
|
+
return selected;
|
|
2917
|
+
},
|
|
2918
|
+
destroy: () => scope.dispose()
|
|
2919
|
+
};
|
|
2920
|
+
}
|
|
2921
|
+
|
|
2922
|
+
// src/components/sankey-chart/engine.ts
|
|
2923
|
+
function createSankeyChart(root, initial = {}) {
|
|
2924
|
+
if (!root) return inert({ update() {
|
|
2925
|
+
}, select() {
|
|
2926
|
+
}, selected: null });
|
|
2927
|
+
registerMotion();
|
|
2928
|
+
const scope = createScope();
|
|
2929
|
+
const frame = chartFrame(root, scope, "sankey");
|
|
2930
|
+
const { plot, canvas } = frame;
|
|
2931
|
+
let options = { ...initial };
|
|
2932
|
+
let selected = null;
|
|
2933
|
+
let hoverNode = null;
|
|
2934
|
+
const uid = `mk-sankey-${Math.random().toString(36).slice(2, 8)}`;
|
|
2935
|
+
const format = (value) => options.formatValue ? options.formatValue(value) : formatNumber(value);
|
|
2936
|
+
const modelOf = () => {
|
|
2937
|
+
const nodes = (options.nodes ?? []).map((node, i, all) => ({ id: String(node.id), label: String(node.label ?? node.id), color: node.color ?? slotColor(i, Math.max(2, all.length)) }));
|
|
2938
|
+
const index = new Map(nodes.map((node, i) => [node.id, i]));
|
|
2939
|
+
const links = (options.links ?? []).map((link, i) => ({ source: index.get(String(link.source)) ?? -1, target: index.get(String(link.target)) ?? -1, value: link.value, given: i })).filter((link) => link.source >= 0 && link.target >= 0 && link.source !== link.target && finite2(link.value) && link.value > 0);
|
|
2940
|
+
const depth = nodes.map(() => 0);
|
|
2941
|
+
for (let pass = 0; pass < nodes.length; pass++) {
|
|
2942
|
+
let moved = false;
|
|
2943
|
+
for (const link of links) {
|
|
2944
|
+
if (depth[link.target] < depth[link.source] + 1 && depth[link.source] + 1 < nodes.length) {
|
|
2945
|
+
depth[link.target] = depth[link.source] + 1;
|
|
2946
|
+
moved = true;
|
|
2947
|
+
}
|
|
2948
|
+
}
|
|
2949
|
+
if (!moved) break;
|
|
2950
|
+
}
|
|
2951
|
+
const columns = Math.max(0, ...depth) + 1;
|
|
2952
|
+
nodes.forEach((_, i) => {
|
|
2953
|
+
if (links.some((link) => link.source === i) || !links.some((link) => link.target === i)) return;
|
|
2954
|
+
depth[i] = columns - 1;
|
|
2955
|
+
});
|
|
2956
|
+
const used = nodes.map((_, i) => links.some((link) => link.source === i || link.target === i));
|
|
2957
|
+
const value = nodes.map((_, i) => Math.max(links.filter((link) => link.target === i).reduce((s, l) => s + l.value, 0), links.filter((link) => link.source === i).reduce((s, l) => s + l.value, 0)));
|
|
2958
|
+
return { nodes, links, depth, columns, used, value };
|
|
2959
|
+
};
|
|
2960
|
+
let model = modelOf();
|
|
2961
|
+
const hasData = () => model.links.length > 0;
|
|
2962
|
+
let width = 600;
|
|
2963
|
+
let height = 320;
|
|
2964
|
+
const layoutOf = () => {
|
|
2965
|
+
width = Math.max(240, Math.round(plot.clientWidth || root.clientWidth || 600));
|
|
2966
|
+
height = Math.max(160, finite2(options.height) ? options.height : 320);
|
|
2967
|
+
const nodeWidth2 = Math.max(4, finite2(options.nodeWidth) ? options.nodeWidth : 12);
|
|
2968
|
+
const pad = Math.max(2, finite2(options.nodePadding) ? options.nodePadding : 14);
|
|
2969
|
+
const top = 6;
|
|
2970
|
+
const usable = height - top * 2;
|
|
2971
|
+
const columns = Array.from({ length: model.columns }, (_, c) => model.nodes.map((_2, i) => i).filter((i) => model.used[i] && model.depth[i] === c));
|
|
2972
|
+
const ky = Math.min(...columns.filter((list) => list.length).map((list) => (usable - pad * (list.length - 1)) / list.reduce((sum, i) => sum + model.value[i], 0)));
|
|
2973
|
+
const step = model.columns > 1 ? (width - nodeWidth2) / (model.columns - 1) : 0;
|
|
2974
|
+
const nodes = model.nodes.map(() => ({ x: 0, y0: 0, y1: 0 }));
|
|
2975
|
+
for (const list of columns) {
|
|
2976
|
+
const total = list.reduce((sum, i) => sum + model.value[i] * ky, 0) + pad * Math.max(0, list.length - 1);
|
|
2977
|
+
let y = top + (usable - total) / 2;
|
|
2978
|
+
for (const i of list) {
|
|
2979
|
+
const h = Math.max(1, model.value[i] * ky);
|
|
2980
|
+
nodes[i] = { x: model.depth[i] * step, y0: y, y1: y + h };
|
|
2981
|
+
y += h + pad;
|
|
2982
|
+
}
|
|
2983
|
+
}
|
|
2984
|
+
const out = /* @__PURE__ */ new Map();
|
|
2985
|
+
const into = /* @__PURE__ */ new Map();
|
|
2986
|
+
const centre = (i) => (nodes[i].y0 + nodes[i].y1) / 2;
|
|
2987
|
+
const links = model.links.map(() => ({ sy: 0, ty: 0, width: 0 }));
|
|
2988
|
+
const order = model.links.map((_, k) => k);
|
|
2989
|
+
for (const k of [...order].sort((a, b) => centre(model.links[a].target) - centre(model.links[b].target))) {
|
|
2990
|
+
const link = model.links[k];
|
|
2991
|
+
const w = link.value * ky;
|
|
2992
|
+
const at = out.get(link.source) ?? nodes[link.source].y0;
|
|
2993
|
+
links[k].sy = at + w / 2;
|
|
2994
|
+
links[k].width = w;
|
|
2995
|
+
out.set(link.source, at + w);
|
|
2996
|
+
}
|
|
2997
|
+
for (const k of [...order].sort((a, b) => centre(model.links[a].source) - centre(model.links[b].source))) {
|
|
2998
|
+
const link = model.links[k];
|
|
2999
|
+
const at = into.get(link.target) ?? nodes[link.target].y0;
|
|
3000
|
+
links[k].ty = at + links[k].width / 2;
|
|
3001
|
+
into.set(link.target, at + links[k].width);
|
|
3002
|
+
}
|
|
3003
|
+
return { nodes, links };
|
|
3004
|
+
};
|
|
3005
|
+
let shown = null;
|
|
3006
|
+
let rects = [];
|
|
3007
|
+
let bands = [];
|
|
3008
|
+
let names = [];
|
|
3009
|
+
let glide = null;
|
|
3010
|
+
scope.add(() => glide?.kill());
|
|
3011
|
+
const nodeWidth = () => Math.max(4, finite2(options.nodeWidth) ? options.nodeWidth : 12);
|
|
3012
|
+
const build = () => {
|
|
3013
|
+
canvas.replaceChildren();
|
|
3014
|
+
canvas.setAttribute("viewBox", `0 0 ${width} ${height}`);
|
|
3015
|
+
canvas.setAttribute("width", String(width));
|
|
3016
|
+
canvas.setAttribute("height", String(height));
|
|
3017
|
+
const defs = svg2("defs");
|
|
3018
|
+
const flows = svg2("g", { "data-mk-part": "links" });
|
|
3019
|
+
bands = model.links.map((link, k) => {
|
|
3020
|
+
const gradient = svg2("linearGradient", { id: `${uid}-${k}`, gradientUnits: "userSpaceOnUse" });
|
|
3021
|
+
const from = svg2("stop", { offset: "0" });
|
|
3022
|
+
from.style.stopColor = model.nodes[link.source].color;
|
|
3023
|
+
const to = svg2("stop", { offset: "1" });
|
|
3024
|
+
to.style.stopColor = model.nodes[link.target].color;
|
|
3025
|
+
gradient.append(from, to);
|
|
3026
|
+
defs.append(gradient);
|
|
3027
|
+
const band = svg2("path", { "data-mk-index": k });
|
|
3028
|
+
band.style.fill = `url(#${uid}-${k})`;
|
|
3029
|
+
flows.append(band);
|
|
3030
|
+
return band;
|
|
3031
|
+
});
|
|
3032
|
+
const nodesLayer = svg2("g", { "data-mk-part": "nodes" });
|
|
3033
|
+
rects = model.nodes.map((node, i) => {
|
|
3034
|
+
const rect = svg2("rect", { width: nodeWidth(), rx: 3, "data-mk-node": i });
|
|
3035
|
+
rect.style.fill = node.color;
|
|
3036
|
+
if (!model.used[i]) rect.style.display = "none";
|
|
3037
|
+
nodesLayer.append(rect);
|
|
3038
|
+
return rect;
|
|
3039
|
+
});
|
|
3040
|
+
const labels = svg2("g", { "data-mk-part": "labels" });
|
|
3041
|
+
names = model.nodes.map((node, i) => {
|
|
3042
|
+
const text = svg2("text", { "dominant-baseline": "middle" });
|
|
3043
|
+
text.textContent = node.label;
|
|
3044
|
+
const amount = document.createElementNS(SVG_NS, "tspan");
|
|
3045
|
+
amount.setAttribute("data-mk-part", "amount");
|
|
3046
|
+
amount.textContent = ` ${format(model.value[i])}`;
|
|
3047
|
+
text.append(amount);
|
|
3048
|
+
if (!model.used[i]) text.style.display = "none";
|
|
3049
|
+
labels.append(text);
|
|
3050
|
+
return text;
|
|
3051
|
+
});
|
|
3052
|
+
canvas.append(defs, flows, nodesLayer, labels);
|
|
3053
|
+
};
|
|
3054
|
+
const paint = (layout2) => {
|
|
3055
|
+
shown = layout2;
|
|
3056
|
+
const w = nodeWidth();
|
|
3057
|
+
layout2.nodes.forEach((node, i) => {
|
|
3058
|
+
const rect = rects[i];
|
|
3059
|
+
rect.setAttribute("x", String(round(node.x)));
|
|
3060
|
+
rect.setAttribute("y", String(round(node.y0)));
|
|
3061
|
+
rect.setAttribute("height", String(round(Math.max(1, node.y1 - node.y0))));
|
|
3062
|
+
const last = model.depth[i] === model.columns - 1 && model.columns > 1;
|
|
3063
|
+
const name = names[i];
|
|
3064
|
+
const first = model.depth[i] === 0;
|
|
3065
|
+
const x = round(last ? node.x - 8 : first ? node.x + w + 8 : node.x + w / 2);
|
|
3066
|
+
const available = model.columns > 1 ? (width - w) / (model.columns - 1) - 20 : width - w - 16;
|
|
3067
|
+
const characters = Math.max(3, Math.floor(available / 7));
|
|
3068
|
+
const label = model.nodes[i].label;
|
|
3069
|
+
if (name.firstChild) name.firstChild.textContent = label.length > characters ? `${label.slice(0, characters - 1)}\u2026` : label;
|
|
3070
|
+
name.setAttribute("x", String(x));
|
|
3071
|
+
name.setAttribute("y", String(round((node.y0 + node.y1) / 2 - 7)));
|
|
3072
|
+
name.setAttribute("text-anchor", last ? "end" : first ? "start" : "middle");
|
|
3073
|
+
const amount = name.querySelector("tspan");
|
|
3074
|
+
amount?.setAttribute("x", String(x));
|
|
3075
|
+
amount?.setAttribute("dy", "14");
|
|
3076
|
+
});
|
|
3077
|
+
layout2.links.forEach((link, k) => {
|
|
3078
|
+
const { source, target } = model.links[k];
|
|
3079
|
+
const x0 = layout2.nodes[source].x + w;
|
|
3080
|
+
const x1 = layout2.nodes[target].x;
|
|
3081
|
+
const xm = (x0 + x1) / 2;
|
|
3082
|
+
const h = link.width / 2;
|
|
3083
|
+
const [s0, s1, t0, t1] = [link.sy - h, link.sy + h, link.ty - h, link.ty + h].map(round);
|
|
3084
|
+
bands[k].setAttribute("d", `M${round(x0)},${s0}C${round(xm)},${s0} ${round(xm)},${t0} ${round(x1)},${t0}L${round(x1)},${t1}C${round(xm)},${t1} ${round(xm)},${s1} ${round(x0)},${s1}Z`);
|
|
3085
|
+
const gradient = canvas.querySelector(`#${uid}-${k}`);
|
|
3086
|
+
gradient?.setAttribute("x1", String(round(x0)));
|
|
3087
|
+
gradient?.setAttribute("x2", String(round(x1)));
|
|
3088
|
+
});
|
|
3089
|
+
};
|
|
3090
|
+
const share = (part2, whole) => new Intl.NumberFormat(void 0, { style: "percent", maximumFractionDigits: 1 }).format(part2 / (whole || 1));
|
|
3091
|
+
const paintSelection = (announce) => {
|
|
3092
|
+
const lit = (k) => selected !== null ? k === selected : hoverNode !== null ? model.links[k].source === hoverNode || model.links[k].target === hoverNode : false;
|
|
3093
|
+
const any = selected !== null || hoverNode !== null;
|
|
3094
|
+
bands.forEach((band, k) => {
|
|
3095
|
+
band.toggleAttribute("data-mk-on", lit(k));
|
|
3096
|
+
band.toggleAttribute("data-mk-dim", any && !lit(k));
|
|
3097
|
+
});
|
|
3098
|
+
rects.forEach((rect, i) => rect.toggleAttribute("data-mk-dim", any && !model.links.some((link, k) => lit(k) && (link.source === i || link.target === i))));
|
|
3099
|
+
if (!shown || selected === null && hoverNode === null) {
|
|
3100
|
+
frame.tip(null);
|
|
3101
|
+
return;
|
|
3102
|
+
}
|
|
3103
|
+
if (selected !== null) {
|
|
3104
|
+
const link = model.links[selected];
|
|
3105
|
+
const geometry = shown.links[selected];
|
|
3106
|
+
const x = (shown.nodes[link.source].x + shown.nodes[link.target].x + nodeWidth()) / 2;
|
|
3107
|
+
const y = (geometry.sy + geometry.ty) / 2 - geometry.width / 2;
|
|
3108
|
+
const from = model.nodes[link.source];
|
|
3109
|
+
const to = model.nodes[link.target];
|
|
3110
|
+
const rows = [{ name: `${share(link.value, model.value[link.source])} de ${from.label}`, color: from.color, value: format(link.value) }];
|
|
3111
|
+
frame.tip({ x: round(x), y: round(y), title: `${from.label} \u2192 ${to.label}`, rows });
|
|
3112
|
+
if (announce) frame.live.textContent = `${from.label} a ${to.label}: ${format(link.value)}`;
|
|
3113
|
+
} else {
|
|
3114
|
+
const i = hoverNode;
|
|
3115
|
+
const node = shown.nodes[i];
|
|
3116
|
+
frame.tip({ x: round(node.x + nodeWidth() / 2), y: round(node.y0), title: model.nodes[i].label, rows: [{ name: "Total", color: model.nodes[i].color, value: format(model.value[i]) }] });
|
|
3117
|
+
}
|
|
3118
|
+
};
|
|
3119
|
+
const draw = (animate) => {
|
|
3120
|
+
glide?.kill();
|
|
3121
|
+
const name = options.label ?? "Diagrama de flujo";
|
|
3122
|
+
canvas.setAttribute("aria-label", hasData() ? `${name}. ${model.links.length} flujos entre ${model.used.filter(Boolean).length} etapas. Usa las flechas para recorrer los flujos.` : `${name}: ${options.emptyLabel ?? "sin datos"}`);
|
|
3123
|
+
frame.fillTable(name, ["Flujo", "Valor"], model.links.map((link) => [`${model.nodes[link.source].label} \u2192 ${model.nodes[link.target].label}`, format(link.value)]));
|
|
3124
|
+
frame.fillLegend([], false);
|
|
3125
|
+
const ok = hasData();
|
|
3126
|
+
frame.empty.hidden = ok;
|
|
3127
|
+
frame.empty.textContent = options.emptyLabel ?? "Sin datos para mostrar";
|
|
3128
|
+
plot.hidden = !ok;
|
|
3129
|
+
if (!ok) {
|
|
3130
|
+
canvas.replaceChildren();
|
|
3131
|
+
shown = null;
|
|
3132
|
+
selected = null;
|
|
3133
|
+
return;
|
|
3134
|
+
}
|
|
3135
|
+
if (selected !== null && selected >= model.links.length) selected = null;
|
|
3136
|
+
const before = shown;
|
|
3137
|
+
const target = layoutOf();
|
|
3138
|
+
const comparable = before && before.nodes.length === target.nodes.length && before.links.length === target.links.length;
|
|
3139
|
+
build();
|
|
3140
|
+
const done = () => {
|
|
3141
|
+
paint(target);
|
|
3142
|
+
paintSelection(false);
|
|
3143
|
+
};
|
|
3144
|
+
if (!animate || reduced()) return done();
|
|
3145
|
+
if (comparable) {
|
|
3146
|
+
const state = { t: 0 };
|
|
3147
|
+
const mix = (a, b) => a + (b - a) * state.t;
|
|
3148
|
+
glide = gsap.to(state, {
|
|
3149
|
+
t: 1,
|
|
3150
|
+
duration: 0.6,
|
|
3151
|
+
ease: EASE.swatch,
|
|
3152
|
+
onUpdate: () => paint({
|
|
3153
|
+
nodes: target.nodes.map((node, i) => ({ x: mix(before.nodes[i].x, node.x), y0: mix(before.nodes[i].y0, node.y0), y1: mix(before.nodes[i].y1, node.y1) })),
|
|
3154
|
+
links: target.links.map((link, k) => ({ sy: mix(before.links[k].sy, link.sy), ty: mix(before.links[k].ty, link.ty), width: mix(before.links[k].width, link.width) }))
|
|
3155
|
+
}),
|
|
3156
|
+
onComplete: done
|
|
3157
|
+
});
|
|
3158
|
+
return;
|
|
3159
|
+
}
|
|
3160
|
+
done();
|
|
3161
|
+
const timeline = gsap.timeline();
|
|
3162
|
+
rects.forEach((rect2, i) => {
|
|
3163
|
+
timeline.fromTo(rect2, { scaleY: 0, transformOrigin: "50% 50%" }, { scaleY: 1, duration: 0.5, ease: EASE.swatch, clearProps: "transform" }, (model.depth[i] ?? 0) * 0.12);
|
|
3164
|
+
});
|
|
3165
|
+
timeline.fromTo(names, { opacity: 0 }, { opacity: 1, duration: 0.4, stagger: 0.02, clearProps: "opacity" }, 0.2);
|
|
3166
|
+
const clip = svg2("clipPath", { id: `${uid}-wipe` });
|
|
3167
|
+
const rect = svg2("rect", { x: 0, y: 0, width: 0, height });
|
|
3168
|
+
clip.append(rect);
|
|
3169
|
+
canvas.prepend(clip);
|
|
3170
|
+
const flows = canvas.querySelector('[data-mk-part="links"]');
|
|
3171
|
+
flows.setAttribute("clip-path", `url(#${uid}-wipe)`);
|
|
3172
|
+
timeline.to(rect, { attr: { width }, duration: 0.9, ease: EASE.wipe, onComplete: () => {
|
|
3173
|
+
flows.removeAttribute("clip-path");
|
|
3174
|
+
clip.remove();
|
|
3175
|
+
} }, 0.1);
|
|
3176
|
+
glide = timeline;
|
|
3177
|
+
};
|
|
3178
|
+
const select = (index, announce = false) => {
|
|
3179
|
+
if (scope.disposed) return;
|
|
3180
|
+
const n = model.links.length;
|
|
3181
|
+
const next = index === null || !n ? null : (Math.round(index) % n + n) % n;
|
|
3182
|
+
if (next === selected) return;
|
|
3183
|
+
selected = next;
|
|
3184
|
+
paintSelection(announce);
|
|
3185
|
+
options.onSelect?.(selected === null ? null : model.links[selected].given);
|
|
3186
|
+
const link = selected === null ? null : model.links[selected];
|
|
3187
|
+
root.dispatchEvent(new CustomEvent("mk:select", { bubbles: true, detail: { index: link?.given ?? null, source: link ? model.nodes[link.source].id : null, target: link ? model.nodes[link.target].id : null, value: link?.value ?? null } }));
|
|
3188
|
+
};
|
|
3189
|
+
scope.on(canvas, "pointermove", (event) => {
|
|
3190
|
+
const target = event.target;
|
|
3191
|
+
const band = target.closest("[data-mk-index]");
|
|
3192
|
+
const node = target.closest("[data-mk-node]");
|
|
3193
|
+
if (node) {
|
|
3194
|
+
const i = Number(node.getAttribute("data-mk-node"));
|
|
3195
|
+
if (selected !== null) select(null);
|
|
3196
|
+
if (hoverNode !== i) {
|
|
3197
|
+
hoverNode = i;
|
|
3198
|
+
paintSelection(false);
|
|
3199
|
+
}
|
|
3200
|
+
return;
|
|
3201
|
+
}
|
|
3202
|
+
if (hoverNode !== null) {
|
|
3203
|
+
hoverNode = null;
|
|
3204
|
+
paintSelection(false);
|
|
3205
|
+
}
|
|
3206
|
+
select(band ? Number(band.getAttribute("data-mk-index")) : null);
|
|
3207
|
+
});
|
|
3208
|
+
scope.on(canvas, "pointerleave", (event) => {
|
|
3209
|
+
if (event.pointerType !== "mouse") return;
|
|
3210
|
+
hoverNode = null;
|
|
3211
|
+
select(null);
|
|
3212
|
+
paintSelection(false);
|
|
3213
|
+
});
|
|
3214
|
+
scope.on(canvas, "focus", () => {
|
|
3215
|
+
if (selected === null && hasData()) select(0, true);
|
|
3216
|
+
});
|
|
3217
|
+
scope.on(canvas, "blur", () => select(null));
|
|
3218
|
+
scope.on(canvas, "keydown", (event) => {
|
|
3219
|
+
const key = event.key;
|
|
3220
|
+
const moves = { ArrowDown: (selected ?? -1) + 1, ArrowRight: (selected ?? -1) + 1, ArrowUp: (selected ?? 1) - 1, ArrowLeft: (selected ?? 1) - 1, Home: 0, End: model.links.length - 1, Escape: null };
|
|
3221
|
+
if (!(key in moves) || !hasData()) return;
|
|
3222
|
+
event.preventDefault();
|
|
3223
|
+
select(moves[key], true);
|
|
3224
|
+
});
|
|
3225
|
+
onWidth(root, scope, () => {
|
|
3226
|
+
if (!hasData()) return;
|
|
3227
|
+
const layout2 = layoutOf();
|
|
3228
|
+
build();
|
|
3229
|
+
paint(layout2);
|
|
3230
|
+
paintSelection(false);
|
|
3231
|
+
});
|
|
3232
|
+
draw(true);
|
|
3233
|
+
return {
|
|
3234
|
+
update(next) {
|
|
3235
|
+
if (scope.disposed) return;
|
|
3236
|
+
options = { ...options, ...next };
|
|
3237
|
+
model = modelOf();
|
|
3238
|
+
draw(true);
|
|
3239
|
+
},
|
|
3240
|
+
select: (index) => {
|
|
3241
|
+
const at = index === null ? null : model.links.findIndex((link) => link.given === index);
|
|
3242
|
+
select(at === null || at < 0 ? null : at);
|
|
3243
|
+
},
|
|
3244
|
+
get selected() {
|
|
3245
|
+
return selected === null ? null : model.links[selected].given;
|
|
3246
|
+
},
|
|
3247
|
+
destroy: () => scope.dispose()
|
|
3248
|
+
};
|
|
3249
|
+
}
|
|
3250
|
+
|
|
3251
|
+
// src/components/heatmap-chart/engine.ts
|
|
3252
|
+
var LEFT = 28;
|
|
3253
|
+
var TOP = 18;
|
|
3254
|
+
function createHeatmapChart(root, initial = {}) {
|
|
3255
|
+
if (!root) return inert({ update() {
|
|
3256
|
+
}, select() {
|
|
3257
|
+
}, selected: null });
|
|
3258
|
+
registerMotion();
|
|
3259
|
+
const scope = createScope();
|
|
3260
|
+
const frame = chartFrame(root, scope, "heatmap");
|
|
3261
|
+
const { plot, canvas, keep } = frame;
|
|
3262
|
+
let options = { ...initial };
|
|
3263
|
+
let selected = null;
|
|
3264
|
+
const scroller = document.createElement("div");
|
|
3265
|
+
scroller.dataset.mkHeatmapScroll = "";
|
|
3266
|
+
plot.prepend(scroller);
|
|
3267
|
+
scroller.append(canvas);
|
|
3268
|
+
scope.add(() => scroller.remove());
|
|
3269
|
+
const scale = document.createElement("div");
|
|
3270
|
+
scale.dataset.mkHeatmapScale = "";
|
|
3271
|
+
scale.setAttribute("aria-hidden", "true");
|
|
3272
|
+
root.insertBefore(scale, frame.legend);
|
|
3273
|
+
scope.add(() => scale.remove());
|
|
3274
|
+
const locale = () => options.locale ?? (document.documentElement.lang || "es");
|
|
3275
|
+
const format = (value) => options.formatValue ? options.formatValue(value) : formatNumber(value);
|
|
3276
|
+
const levels = () => Math.max(1, Math.min(8, Math.round(options.levels ?? 4)));
|
|
3277
|
+
const modelOf = () => {
|
|
3278
|
+
const values = /* @__PURE__ */ new Map();
|
|
3279
|
+
const add = (key, value) => finite2(value) && values.set(key, (values.get(key) ?? 0) + value);
|
|
3280
|
+
if (Array.isArray(options.data)) for (const datum of options.data) add(dayKey(toDate(datum.date)), datum.value);
|
|
3281
|
+
else if (options.data) for (const [key, value] of Object.entries(options.data)) add(dayKey(toDate(key)), value);
|
|
3282
|
+
const weeks = Math.max(1, Math.min(106, Math.round(options.weeks ?? 53)));
|
|
3283
|
+
const firstDay = Number.isInteger(options.firstDay) && options.firstDay >= 0 && options.firstDay <= 6 ? options.firstDay : 1;
|
|
3284
|
+
const end = toDate(options.end ?? /* @__PURE__ */ new Date());
|
|
3285
|
+
const start = startOfWeek(addDays(end, -(weeks - 1) * 7), firstDay);
|
|
3286
|
+
const days = [];
|
|
3287
|
+
for (let date = start, i = 0; date <= end; date = addDays(date, 1), i++) {
|
|
3288
|
+
const key = dayKey(date);
|
|
3289
|
+
days.push({ key, date, value: values.get(key) ?? 0, week: Math.floor(i / 7), day: i % 7 });
|
|
3290
|
+
}
|
|
3291
|
+
const max = Math.max(0, ...days.map((each) => each.value));
|
|
3292
|
+
return { days, weeks: (days[days.length - 1]?.week ?? 0) + 1, firstDay, max, total: days.reduce((sum, each) => sum + each.value, 0) };
|
|
3293
|
+
};
|
|
3294
|
+
let model = modelOf();
|
|
3295
|
+
const levelOf = (value) => value <= 0 || !model.max ? 0 : Math.max(1, Math.ceil(value / model.max * levels()));
|
|
3296
|
+
const fillOf = (level) => 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))`;
|
|
3297
|
+
let cells = [];
|
|
3298
|
+
let builtFrom = "";
|
|
3299
|
+
let geometry = { cell: 12, gap: 3, width: 0, height: 0 };
|
|
3300
|
+
const longDate = () => safeFormat(locale(), { weekday: "short", day: "numeric", month: "long", year: "numeric" });
|
|
3301
|
+
const build = (animate) => {
|
|
3302
|
+
canvas.replaceChildren();
|
|
3303
|
+
const available = Math.max(0, (plot.clientWidth || root.clientWidth || 0) - LEFT);
|
|
3304
|
+
const gap = 3;
|
|
3305
|
+
const fit = available ? Math.floor(available / model.weeks - gap) : 12;
|
|
3306
|
+
const cell = Math.max(10, Math.min(14, fit));
|
|
3307
|
+
const width = LEFT + model.weeks * (cell + gap);
|
|
3308
|
+
const height = TOP + 7 * (cell + gap);
|
|
3309
|
+
geometry = { cell, gap, width, height };
|
|
3310
|
+
canvas.setAttribute("viewBox", `0 0 ${width} ${height}`);
|
|
3311
|
+
canvas.setAttribute("width", String(width));
|
|
3312
|
+
canvas.setAttribute("height", String(height));
|
|
3313
|
+
plot.style.setProperty("--mk-heatmap-width", `${width}px`);
|
|
3314
|
+
const months = svg2("g", { "data-mk-part": "months" });
|
|
3315
|
+
const monthName = safeFormat(locale(), { month: "short" });
|
|
3316
|
+
let last = -1;
|
|
3317
|
+
for (const each of model.days) {
|
|
3318
|
+
if (each.day !== 0) continue;
|
|
3319
|
+
const month = addDays(each.date, 6).getMonth();
|
|
3320
|
+
if (month === last) continue;
|
|
3321
|
+
last = month;
|
|
3322
|
+
if (each.week > model.weeks - 2 && model.weeks > 4) continue;
|
|
3323
|
+
const text = svg2("text", { x: LEFT + each.week * (cell + gap), y: 10 });
|
|
3324
|
+
text.textContent = capitalize(monthName.format(addDays(each.date, 6)).replace(".", ""));
|
|
3325
|
+
months.append(text);
|
|
3326
|
+
}
|
|
3327
|
+
const names = weekdayNames(locale(), model.firstDay, "short");
|
|
3328
|
+
const weekdays = svg2("g", { "data-mk-part": "weekdays" });
|
|
3329
|
+
[1, 3, 5].forEach((row) => {
|
|
3330
|
+
const text = svg2("text", { x: 0, y: round(TOP + row * (cell + gap) + cell / 2), "dominant-baseline": "middle" });
|
|
3331
|
+
text.textContent = capitalize(names[row].replace(".", "")).slice(0, 3);
|
|
3332
|
+
weekdays.append(text);
|
|
3333
|
+
});
|
|
3334
|
+
const grid = svg2("g", { "data-mk-part": "cells" });
|
|
3335
|
+
cells = model.days.map((each, i) => {
|
|
3336
|
+
const rect = svg2("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) });
|
|
3337
|
+
rect.style.fill = fillOf(levelOf(each.value));
|
|
3338
|
+
grid.append(rect);
|
|
3339
|
+
return rect;
|
|
3340
|
+
});
|
|
3341
|
+
canvas.append(months, weekdays, grid);
|
|
3342
|
+
builtFrom = model.days[0]?.key ?? "";
|
|
3343
|
+
scroller.scrollLeft = scroller.scrollWidth;
|
|
3344
|
+
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) => Math.min(0.7, (model.days[i].week + model.days[i].day) * 8e-3) }));
|
|
3345
|
+
};
|
|
3346
|
+
scope.add(() => plot.style.removeProperty("--mk-heatmap-width"));
|
|
3347
|
+
const paintScale = () => {
|
|
3348
|
+
scale.replaceChildren();
|
|
3349
|
+
scale.hidden = options.legend === false || !model.days.length;
|
|
3350
|
+
if (scale.hidden) return;
|
|
3351
|
+
const less = document.createElement("span");
|
|
3352
|
+
less.textContent = "Menos";
|
|
3353
|
+
scale.append(less);
|
|
3354
|
+
for (let level = 0; level <= levels(); level++) {
|
|
3355
|
+
const swatch = document.createElement("i");
|
|
3356
|
+
swatch.style.background = fillOf(level);
|
|
3357
|
+
swatch.addEventListener("pointerenter", () => {
|
|
3358
|
+
for (const cell of cells) cell.toggleAttribute("data-mk-dim", cell.getAttribute("data-mk-level") !== String(level));
|
|
3359
|
+
});
|
|
3360
|
+
swatch.addEventListener("pointerleave", () => {
|
|
3361
|
+
for (const cell of cells) cell.removeAttribute("data-mk-dim");
|
|
3362
|
+
});
|
|
3363
|
+
scale.append(swatch);
|
|
3364
|
+
}
|
|
3365
|
+
const more = document.createElement("span");
|
|
3366
|
+
more.textContent = "M\xE1s";
|
|
3367
|
+
scale.append(more);
|
|
3368
|
+
};
|
|
3369
|
+
const unit = () => options.unit ?? "actividades";
|
|
3370
|
+
const paintSelection = (announce) => {
|
|
3371
|
+
cells.forEach((cell2, i) => cell2.toggleAttribute("data-mk-on", i === selected));
|
|
3372
|
+
const each = selected === null ? null : model.days[selected];
|
|
3373
|
+
if (!each) {
|
|
3374
|
+
frame.tip(null);
|
|
3375
|
+
return;
|
|
3376
|
+
}
|
|
3377
|
+
const { cell, gap } = geometry;
|
|
3378
|
+
const x = LEFT + each.week * (cell + gap) + cell / 2 - scroller.scrollLeft;
|
|
3379
|
+
const y = TOP + each.day * (cell + gap);
|
|
3380
|
+
const title = capitalize(longDate().format(each.date));
|
|
3381
|
+
frame.tip({ x, y, title, rows: [{ name: unit(), color: fillOf(Math.max(1, levelOf(each.value))), value: format(each.value) }] });
|
|
3382
|
+
if (announce) frame.live.textContent = `${title}: ${format(each.value)} ${unit()}`;
|
|
3383
|
+
};
|
|
3384
|
+
const draw = (animate) => {
|
|
3385
|
+
const name = options.label ?? "Actividad diaria";
|
|
3386
|
+
canvas.setAttribute("aria-label", model.days.length ? `${name}. ${model.weeks} semanas, ${format(model.total)} ${unit()} en total. Usa las flechas para recorrer los d\xEDas.` : `${name}: ${options.emptyLabel ?? "sin datos"}`);
|
|
3387
|
+
const active = model.days.filter((each) => each.value > 0);
|
|
3388
|
+
frame.fillTable(`${name}: ${format(model.total)} ${unit()}`, ["D\xEDa", capitalize(unit())], active.map((each) => [capitalize(longDate().format(each.date)), format(each.value)]));
|
|
3389
|
+
frame.fillLegend([], false);
|
|
3390
|
+
paintScale();
|
|
3391
|
+
if (selected !== null && selected >= model.days.length) selected = null;
|
|
3392
|
+
const same = cells.length === model.days.length && cells.length > 0 && builtFrom === model.days[0].key;
|
|
3393
|
+
if (same && animate) {
|
|
3394
|
+
cells.forEach((cell, i) => {
|
|
3395
|
+
const level = levelOf(model.days[i].value);
|
|
3396
|
+
cell.setAttribute("data-mk-level", String(level));
|
|
3397
|
+
cell.style.fill = fillOf(level);
|
|
3398
|
+
});
|
|
3399
|
+
} else build(animate);
|
|
3400
|
+
paintSelection(false);
|
|
3401
|
+
};
|
|
3402
|
+
const select = (index, announce = false) => {
|
|
3403
|
+
if (scope.disposed) return;
|
|
3404
|
+
const next = index === null || !model.days.length ? null : Math.max(0, Math.min(model.days.length - 1, Math.round(index)));
|
|
3405
|
+
if (next === selected) return;
|
|
3406
|
+
selected = next;
|
|
3407
|
+
paintSelection(announce);
|
|
3408
|
+
const each = selected === null ? null : model.days[selected];
|
|
3409
|
+
options.onSelect?.(each?.key ?? null, each?.value ?? null);
|
|
3410
|
+
root.dispatchEvent(new CustomEvent("mk:select", { bubbles: true, detail: { date: each?.key ?? null, value: each?.value ?? null } }));
|
|
3411
|
+
};
|
|
3412
|
+
scope.on(canvas, "pointermove", (event) => {
|
|
3413
|
+
const cell = event.target.closest("[data-mk-index]");
|
|
3414
|
+
select(cell ? Number(cell.getAttribute("data-mk-index")) : null);
|
|
3415
|
+
});
|
|
3416
|
+
scope.on(canvas, "pointerleave", (event) => {
|
|
3417
|
+
if (event.pointerType === "mouse") select(null);
|
|
3418
|
+
});
|
|
3419
|
+
scope.on(canvas, "focus", () => {
|
|
3420
|
+
if (selected === null && model.days.length) select(model.days.length - 1, true);
|
|
3421
|
+
});
|
|
3422
|
+
scope.on(canvas, "blur", () => select(null));
|
|
3423
|
+
scope.on(canvas, "keydown", (event) => {
|
|
3424
|
+
const key = event.key;
|
|
3425
|
+
const at = selected ?? model.days.length - 1;
|
|
3426
|
+
const moves = { 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 };
|
|
3427
|
+
if (!(key in moves) || !model.days.length) return;
|
|
3428
|
+
event.preventDefault();
|
|
3429
|
+
select(moves[key], true);
|
|
3430
|
+
const cell = selected === null ? null : cells[selected];
|
|
3431
|
+
if (cell) {
|
|
3432
|
+
const x = Number(cell.getAttribute("x"));
|
|
3433
|
+
if (x - scroller.scrollLeft < LEFT || x - scroller.scrollLeft > scroller.clientWidth - geometry.cell * 2) scroller.scrollLeft = x - scroller.clientWidth / 2;
|
|
3434
|
+
}
|
|
3435
|
+
});
|
|
3436
|
+
onWidth(root, scope, () => {
|
|
3437
|
+
build(false);
|
|
3438
|
+
paintSelection(false);
|
|
3439
|
+
});
|
|
3440
|
+
draw(true);
|
|
3441
|
+
return {
|
|
3442
|
+
update(next) {
|
|
3443
|
+
if (scope.disposed) return;
|
|
3444
|
+
options = { ...options, ...next };
|
|
3445
|
+
model = modelOf();
|
|
3446
|
+
draw(true);
|
|
3447
|
+
},
|
|
3448
|
+
select(date) {
|
|
3449
|
+
const index = date === null ? -1 : model.days.findIndex((each) => each.key === date);
|
|
3450
|
+
select(index < 0 ? null : index);
|
|
3451
|
+
},
|
|
3452
|
+
get selected() {
|
|
3453
|
+
return selected === null ? null : model.days[selected]?.key ?? null;
|
|
3454
|
+
},
|
|
3455
|
+
destroy: () => scope.dispose()
|
|
3456
|
+
};
|
|
3457
|
+
}
|
|
3458
|
+
|
|
3459
|
+
// src/components/orbit-menu/engine.ts
|
|
3460
|
+
function createOrbitMenu(root, options = {}) {
|
|
3461
|
+
const toggle = root?.querySelector("[data-mk-orbit-toggle]") ?? null;
|
|
3462
|
+
const items2 = root ? [...root.querySelectorAll("[data-mk-orbit-item]")] : [];
|
|
3463
|
+
if (!root || !toggle || !items2.length) {
|
|
3464
|
+
if (root) warnOnce("orbit-menu: needs a [data-mk-orbit-toggle] and at least one [data-mk-orbit-item]");
|
|
3465
|
+
return inert({ open() {
|
|
3466
|
+
}, close() {
|
|
3467
|
+
}, toggle() {
|
|
3468
|
+
}, isOpen: false });
|
|
3469
|
+
}
|
|
3470
|
+
registerMotion();
|
|
3471
|
+
const scope = createScope();
|
|
3472
|
+
const radius = Number.isFinite(options.radius) ? Math.max(24, options.radius) : 96;
|
|
3473
|
+
const start = (Number.isFinite(options.start) ? options.start : 0) * Math.PI / 180;
|
|
3474
|
+
const angles = items2.map((_, i) => start + i * Math.PI * 2 / items2.length);
|
|
3475
|
+
const valueOf2 = (item) => item.dataset.mkValue ?? String(items2.indexOf(item));
|
|
3476
|
+
if (!toggle.children.length && !toggle.textContent?.trim()) {
|
|
3477
|
+
const burger = document.createElement("span");
|
|
3478
|
+
burger.dataset.mkOrbitBurger = "";
|
|
3479
|
+
burger.setAttribute("aria-hidden", "true");
|
|
3480
|
+
burger.append(document.createElement("i"), document.createElement("i"), document.createElement("i"));
|
|
3481
|
+
toggle.append(burger);
|
|
3482
|
+
scope.add(() => burger.remove());
|
|
3483
|
+
}
|
|
3484
|
+
scope.attr(toggle, "aria-expanded", "false");
|
|
3485
|
+
scope.attr(toggle, "aria-haspopup", "true");
|
|
3486
|
+
if (!toggle.getAttribute("aria-label") && !toggle.textContent?.trim()) scope.attr(toggle, "aria-label", "Men\xFA");
|
|
3487
|
+
for (const item of items2) {
|
|
3488
|
+
scope.attr(item, "inert", "");
|
|
3489
|
+
if (item instanceof HTMLButtonElement) scope.attr(item, "type", item.getAttribute("type") ?? "button");
|
|
3490
|
+
}
|
|
3491
|
+
scope.attr(root, "data-mk-ready", "");
|
|
3492
|
+
scope.attr(root, "data-mk-state", "closed");
|
|
3493
|
+
const states = items2.map((_, i) => ({ a: angles[i], r: 0 }));
|
|
3494
|
+
const apply = (i) => {
|
|
3495
|
+
const { a, r } = states[i];
|
|
3496
|
+
gsap.set(items2[i], { x: r * Math.sin(a), y: -r * Math.cos(a) });
|
|
3497
|
+
};
|
|
3498
|
+
let timeline = null;
|
|
3499
|
+
scope.add(() => {
|
|
3500
|
+
timeline?.kill();
|
|
3501
|
+
gsap.set(items2, { clearProps: "transform,opacity,filter,visibility" });
|
|
3502
|
+
});
|
|
3503
|
+
gsap.set(items2, { opacity: 0, scale: 0.3, visibility: "hidden" });
|
|
3504
|
+
let open = false;
|
|
3505
|
+
const set = (next, focus = false) => {
|
|
3506
|
+
if (next === open) return;
|
|
3507
|
+
open = next;
|
|
3508
|
+
toggle.setAttribute("aria-expanded", String(open));
|
|
3509
|
+
root.dataset.mkState = open ? "open" : "closed";
|
|
3510
|
+
for (const item of items2) item.toggleAttribute("inert", !open);
|
|
3511
|
+
timeline?.kill();
|
|
3512
|
+
if (reduced()) {
|
|
3513
|
+
items2.forEach((item, i) => {
|
|
3514
|
+
states[i] = { a: angles[i], r: open ? radius : 0 };
|
|
3515
|
+
apply(i);
|
|
3516
|
+
gsap.set(item, { opacity: open ? 1 : 0, scale: open ? 1 : 0.3, filter: "none", visibility: open ? "visible" : "hidden" });
|
|
3517
|
+
});
|
|
3518
|
+
} else if (open) {
|
|
3519
|
+
timeline = gsap.timeline();
|
|
3520
|
+
items2.forEach((item, i) => {
|
|
3521
|
+
const state = states[i];
|
|
3522
|
+
if (state.r < 1) state.a = angles[i] - 0.7;
|
|
3523
|
+
const at = i * 0.045;
|
|
3524
|
+
gsap.set(item, { visibility: "visible" });
|
|
3525
|
+
timeline.to(state, { r: radius, duration: 0.55, ease: EASE.swatch, onUpdate: () => apply(i) }, at);
|
|
3526
|
+
timeline.to(state, { a: angles[i], duration: 0.5, ease: "power3.out", onUpdate: () => apply(i) }, at);
|
|
3527
|
+
timeline.fromTo(item, { filter: "blur(8px)" }, { opacity: 1, scale: 1, filter: "blur(0px)", duration: 0.35, ease: "power2.out", clearProps: "filter" }, at);
|
|
3528
|
+
});
|
|
3529
|
+
} else {
|
|
3530
|
+
timeline = gsap.timeline();
|
|
3531
|
+
[...items2].reverse().forEach((item, k) => {
|
|
3532
|
+
const i = items2.length - 1 - k;
|
|
3533
|
+
const state = states[i];
|
|
3534
|
+
const at = k * 0.03;
|
|
3535
|
+
timeline.to(state, { r: 0, a: state.a + 0.6, duration: 0.3, ease: "power2.in", onUpdate: () => apply(i) }, at);
|
|
3536
|
+
timeline.to(item, { opacity: 0, scale: 0.3, filter: "blur(6px)", duration: 0.28, ease: "power2.in", onComplete: () => {
|
|
3537
|
+
gsap.set(item, { visibility: "hidden", clearProps: "filter" });
|
|
3538
|
+
} }, at);
|
|
3539
|
+
});
|
|
3540
|
+
}
|
|
3541
|
+
if (focus === "first") items2[0].focus();
|
|
3542
|
+
if (focus === "toggle") toggle.focus();
|
|
3543
|
+
options.onToggle?.(open);
|
|
3544
|
+
root.dispatchEvent(new CustomEvent("mk:toggle", { bubbles: true, detail: { open } }));
|
|
3545
|
+
};
|
|
3546
|
+
scope.on(toggle, "click", (event) => {
|
|
3547
|
+
press(toggle);
|
|
3548
|
+
set(!open, !open && event.detail === 0 ? "first" : false);
|
|
3549
|
+
});
|
|
3550
|
+
scope.on(root, "click", (event) => {
|
|
3551
|
+
const item = event.target.closest("[data-mk-orbit-item]");
|
|
3552
|
+
if (!item || !items2.includes(item) || !open) return;
|
|
3553
|
+
press(item);
|
|
3554
|
+
const value = valueOf2(item);
|
|
3555
|
+
options.onSelect?.(value);
|
|
3556
|
+
root.dispatchEvent(new CustomEvent("mk:select", { bubbles: true, detail: { value } }));
|
|
3557
|
+
if (options.closeOnSelect !== false) set(false, "toggle");
|
|
3558
|
+
});
|
|
3559
|
+
const sides = { ArrowUp: 0, ArrowRight: Math.PI / 2, ArrowDown: Math.PI, ArrowLeft: Math.PI * 3 / 2 };
|
|
3560
|
+
scope.on(root, "keydown", (event) => {
|
|
3561
|
+
const key = event.key;
|
|
3562
|
+
if (key === "Escape" && open) {
|
|
3563
|
+
event.preventDefault();
|
|
3564
|
+
set(false, "toggle");
|
|
3565
|
+
return;
|
|
3566
|
+
}
|
|
3567
|
+
if (!(key in sides)) return;
|
|
3568
|
+
if (!open) {
|
|
3569
|
+
if (document.activeElement !== toggle) return;
|
|
3570
|
+
event.preventDefault();
|
|
3571
|
+
set(true);
|
|
3572
|
+
} else event.preventDefault();
|
|
3573
|
+
const want = sides[key];
|
|
3574
|
+
const gap = (a) => {
|
|
3575
|
+
const d = Math.abs(((a - want) % (Math.PI * 2) + Math.PI * 3) % (Math.PI * 2) - Math.PI);
|
|
3576
|
+
return d;
|
|
3577
|
+
};
|
|
3578
|
+
let best = 0;
|
|
3579
|
+
angles.forEach((a, i) => {
|
|
3580
|
+
if (gap(a) < gap(angles[best])) best = i;
|
|
3581
|
+
});
|
|
3582
|
+
items2[best].focus();
|
|
3583
|
+
});
|
|
3584
|
+
scope.on(document, "pointerdown", (event) => {
|
|
3585
|
+
if (open && !root.contains(event.target)) set(false);
|
|
3586
|
+
});
|
|
3587
|
+
return {
|
|
3588
|
+
open: () => set(true),
|
|
3589
|
+
close: () => set(false),
|
|
3590
|
+
toggle: () => set(!open),
|
|
3591
|
+
get isOpen() {
|
|
3592
|
+
return open;
|
|
3593
|
+
},
|
|
3594
|
+
destroy: () => scope.dispose()
|
|
3595
|
+
};
|
|
3596
|
+
}
|
|
3597
|
+
|
|
3598
|
+
export {
|
|
3599
|
+
enterMessages,
|
|
3600
|
+
fitComposer,
|
|
3601
|
+
scrollToLatest,
|
|
3602
|
+
createThemePicker,
|
|
3603
|
+
ICON_PATHS,
|
|
3604
|
+
iconSvg,
|
|
3605
|
+
TOAST_LIMIT,
|
|
3606
|
+
getToasts,
|
|
3607
|
+
subscribeToasts,
|
|
3608
|
+
dismissToast,
|
|
3609
|
+
removeToast,
|
|
3610
|
+
clearToasts,
|
|
3611
|
+
toast,
|
|
3612
|
+
toastTimers,
|
|
3613
|
+
createToaster,
|
|
3614
|
+
PENDING_ID,
|
|
3615
|
+
capturePending,
|
|
3616
|
+
morphPendingToReply,
|
|
3617
|
+
chatMessage,
|
|
3618
|
+
createChat,
|
|
3619
|
+
NAV_STYLES,
|
|
3620
|
+
NAV_STYLE_ALIASES,
|
|
3621
|
+
toNavStyle,
|
|
3622
|
+
revealShell,
|
|
3623
|
+
createNav,
|
|
3624
|
+
createCursor,
|
|
3625
|
+
createIsland,
|
|
3626
|
+
PILL_ARROW,
|
|
3627
|
+
createPillButton,
|
|
3628
|
+
createToolbar,
|
|
3629
|
+
createBidirectionalSlider,
|
|
3630
|
+
createVoiceDots,
|
|
3631
|
+
createPieChart,
|
|
3632
|
+
createRadarChart,
|
|
3633
|
+
createRadialChart,
|
|
3634
|
+
createSankeyChart,
|
|
3635
|
+
createHeatmapChart,
|
|
3636
|
+
createOrbitMenu
|
|
3637
|
+
};
|
|
3638
|
+
//# sourceMappingURL=chunk-JXRS3OEA.js.map
|