@hadoku/task-ui-components 4.23.5 → 4.23.6

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.
@@ -0,0 +1,674 @@
1
+ import { jsx, jsxs, Fragment } from "react/jsx-runtime";
2
+ import React, { useState, useRef, useEffect, useCallback, useMemo } from "react";
3
+ import { Icon } from "@wolffm/themes";
4
+ import { ensureCosmeticsStyles, COSMETICS_BY_ID, PLACEMENT_DEFAULT, fitPlacement, avatarHtml, nameHtml, messageHtml, PLACEMENT_LIMITS, COSMETIC_SLOTS, COSMETICS, placementTransform, cleanLoadout, wornCosmetics } from "../vendor/wolffm/prefs-client/cosmetics.js";
5
+ import { AvatarPicker } from "./AvatarPicker.js";
6
+ import { AVATAR_EVENT } from "../lib/avatarEvent.js";
7
+ import { avatarClient, avatarErrorText, fetchCharacters, portraitUrl } from "../lib/avatarData.js";
8
+ const SLOT_LABEL = {
9
+ effect: "Effect",
10
+ gradient: "Gradient",
11
+ font: "Font",
12
+ overlay: "Overlay",
13
+ message: "Message"
14
+ };
15
+ const SLIDERS = [
16
+ ["x", "Left/right", "%"],
17
+ ["y", "Up/down", "%"],
18
+ ["r", "Rotate", "°"],
19
+ ["s", "Size", "%"]
20
+ ];
21
+ const S = {
22
+ body: { display: "grid", gap: 16, alignItems: "start" },
23
+ side: { display: "flex", flexDirection: "column", gap: 12, minWidth: 0 },
24
+ stage: {
25
+ position: "relative",
26
+ display: "flex",
27
+ flexDirection: "column",
28
+ alignItems: "center",
29
+ gap: 12,
30
+ padding: "48px 12px 18px",
31
+ borderRadius: 10,
32
+ background: "var(--color-bg-alt)",
33
+ border: "1px solid var(--color-border)",
34
+ touchAction: "none",
35
+ userSelect: "none"
36
+ },
37
+ tryOn: {
38
+ position: "absolute",
39
+ left: 8,
40
+ right: 8,
41
+ top: 8,
42
+ display: "flex",
43
+ alignItems: "center",
44
+ gap: 6,
45
+ padding: "4px 8px",
46
+ borderRadius: 6,
47
+ background: "var(--color-primary-bg)",
48
+ color: "var(--color-on-primary-bg)",
49
+ fontSize: "0.72rem"
50
+ },
51
+ linkBtn: {
52
+ border: 0,
53
+ background: "none",
54
+ color: "inherit",
55
+ textDecoration: "underline",
56
+ cursor: "pointer",
57
+ fontSize: "inherit",
58
+ padding: 0,
59
+ flex: "none"
60
+ },
61
+ chat: {
62
+ display: "flex",
63
+ flexDirection: "column",
64
+ gap: 10,
65
+ padding: 12,
66
+ borderRadius: 10,
67
+ background: "var(--color-bg)",
68
+ border: "1px solid var(--color-border)"
69
+ },
70
+ chatHead: {
71
+ display: "flex",
72
+ justifyContent: "space-between",
73
+ alignItems: "center",
74
+ fontSize: "0.72rem",
75
+ color: "var(--color-text-muted)"
76
+ },
77
+ msg: { display: "flex", gap: 10, alignItems: "flex-start" },
78
+ who: { minHeight: 22, display: "flex", alignItems: "center" },
79
+ line: { fontSize: "0.85rem", marginTop: 2 },
80
+ place: {
81
+ display: "flex",
82
+ flexDirection: "column",
83
+ gap: 8,
84
+ padding: 12,
85
+ borderRadius: 10,
86
+ border: "1px solid var(--color-border)"
87
+ },
88
+ placeHead: { display: "flex", flexDirection: "column", gap: 2, fontSize: "0.8rem" },
89
+ hint: { fontSize: "0.72rem", color: "var(--color-text-muted)" },
90
+ slider: {
91
+ display: "grid",
92
+ gridTemplateColumns: "64px 1fr 40px",
93
+ alignItems: "center",
94
+ gap: 6,
95
+ fontSize: "0.75rem",
96
+ color: "var(--color-text-muted)"
97
+ },
98
+ out: { textAlign: "right", color: "var(--color-text)", fontVariantNumeric: "tabular-nums" },
99
+ toggles: {
100
+ display: "flex",
101
+ flexWrap: "wrap",
102
+ gap: 10,
103
+ alignItems: "center",
104
+ fontSize: "0.75rem",
105
+ color: "var(--color-text-muted)"
106
+ },
107
+ tabs: {
108
+ display: "flex",
109
+ gap: 2,
110
+ borderBottom: "1px solid var(--color-border)",
111
+ overflowX: "auto",
112
+ scrollbarWidth: "none"
113
+ },
114
+ tab: {
115
+ flex: "none",
116
+ padding: "7px 11px",
117
+ borderBottomWidth: 2,
118
+ borderBottomStyle: "solid",
119
+ borderBottomColor: "transparent",
120
+ borderTop: 0,
121
+ borderLeft: 0,
122
+ borderRight: 0,
123
+ background: "none",
124
+ cursor: "pointer",
125
+ fontSize: "0.82rem",
126
+ fontWeight: 500,
127
+ color: "var(--color-text-muted)"
128
+ },
129
+ tabOn: { color: "var(--color-text)", borderBottomColor: "var(--color-primary)" },
130
+ count: { fontSize: "0.68rem", marginLeft: 4, opacity: 0.8, fontVariantNumeric: "tabular-nums" },
131
+ pane: { display: "flex", flexDirection: "column", gap: 10, paddingTop: 10 },
132
+ grid: { display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(112px, 1fr))", gap: 8 },
133
+ tile: {
134
+ position: "relative",
135
+ display: "flex",
136
+ flexDirection: "column",
137
+ gap: 5,
138
+ padding: 6,
139
+ borderRadius: 8,
140
+ border: "1px solid var(--color-border)",
141
+ background: "var(--color-bg)",
142
+ color: "var(--color-text)",
143
+ cursor: "pointer",
144
+ textAlign: "left",
145
+ font: "inherit"
146
+ },
147
+ tileOn: {
148
+ borderColor: "var(--color-primary)",
149
+ boxShadow: "inset 0 0 0 1px var(--color-primary)"
150
+ },
151
+ tileTrying: { borderStyle: "dashed", borderColor: "var(--color-primary)" },
152
+ tilePv: {
153
+ height: 58,
154
+ borderRadius: 6,
155
+ display: "flex",
156
+ alignItems: "center",
157
+ justifyContent: "center",
158
+ background: "var(--color-bg-alt)",
159
+ overflow: "hidden",
160
+ padding: "0 8px",
161
+ fontSize: "0.8rem"
162
+ },
163
+ tileName: { fontSize: "0.8rem", fontWeight: 500, display: "flex", alignItems: "center", gap: 5 },
164
+ tileEarn: { fontSize: "0.7rem", lineHeight: 1.3, color: "var(--color-text-muted)" },
165
+ foot: {
166
+ display: "flex",
167
+ justifyContent: "space-between",
168
+ alignItems: "center",
169
+ gap: 8,
170
+ flexWrap: "wrap",
171
+ fontSize: "0.75rem",
172
+ color: "var(--color-text-muted)"
173
+ }
174
+ };
175
+ const Html = React.memo(
176
+ function Html2({ html, style }) {
177
+ return /* @__PURE__ */ jsx("span", { style, dangerouslySetInnerHTML: { __html: html } });
178
+ },
179
+ (a, b) => a.html === b.html && JSON.stringify(a.style) === JSON.stringify(b.style)
180
+ );
181
+ function replayPreview(tile) {
182
+ const pv = tile.firstElementChild;
183
+ if (!pv) return;
184
+ const markup = pv.innerHTML;
185
+ pv.innerHTML = markup;
186
+ }
187
+ function useNarrow() {
188
+ const query = "(max-width: 640px)";
189
+ const [narrow, setNarrow] = useState(() => window.matchMedia(query).matches);
190
+ useEffect(() => {
191
+ const mq = window.matchMedia(query);
192
+ const on = () => setNarrow(mq.matches);
193
+ mq.addEventListener("change", on);
194
+ return () => mq.removeEventListener("change", on);
195
+ }, []);
196
+ return narrow;
197
+ }
198
+ function applyPlacement(root, id, p) {
199
+ root?.querySelectorAll(`.hc-art[data-hc-art="${id}"]`).forEach((ol) => {
200
+ ol.style.zIndex = p.back ? "0" : "3";
201
+ const adj = ol.firstElementChild;
202
+ if (adj) adj.style.transform = placementTransform(p);
203
+ });
204
+ }
205
+ function withLoadout(s, loadout) {
206
+ return {
207
+ ...s,
208
+ cosmetics: { ...s.cosmetics, loadout },
209
+ avatar: { ...s.avatar, cosmetics: wornCosmetics(loadout) }
210
+ };
211
+ }
212
+ function AvatarBuilder({ name }) {
213
+ const [settings, setSettings] = useState(null);
214
+ const [sets, setSets] = useState(null);
215
+ const [busy, setBusy] = useState(false);
216
+ const [error, setError] = useState(null);
217
+ const [tab, setTab] = useState("picture");
218
+ const [tryOn, setTryOn] = useState(null);
219
+ const [showLocked, setShowLocked] = useState(true);
220
+ const [replay, setReplay] = useState(0);
221
+ const narrow = useNarrow();
222
+ const root = useRef(null);
223
+ const seq = useRef(0);
224
+ useEffect(() => ensureCosmeticsStyles(), []);
225
+ useEffect(() => {
226
+ let live2 = true;
227
+ avatarClient.get().then((s) => live2 && setSettings(s)).catch((err) => live2 && setError(avatarErrorText(err)));
228
+ fetchCharacters().then((s) => live2 && setSets(s)).catch((err) => live2 && setError(avatarErrorText(err)));
229
+ return () => {
230
+ live2 = false;
231
+ };
232
+ }, []);
233
+ const announce = (next) => (
234
+ // The page under the popout (a watchparty room, a game) re-reads the avatar
235
+ // from this instead of waiting for its next join.
236
+ window.dispatchEvent(new window.CustomEvent(AVATAR_EVENT, { detail: next }))
237
+ );
238
+ const run = useCallback(async (work) => {
239
+ setBusy(true);
240
+ setError(null);
241
+ try {
242
+ const next = await work();
243
+ setSettings(next);
244
+ announce(next);
245
+ } catch (err) {
246
+ setError(avatarErrorText(err));
247
+ } finally {
248
+ setBusy(false);
249
+ }
250
+ }, []);
251
+ const wear = useCallback(
252
+ async (update, optimistic) => {
253
+ const mine = ++seq.current;
254
+ setError(null);
255
+ setSettings((s) => s ? optimistic(s) : s);
256
+ try {
257
+ const next = await avatarClient.setCosmetics(update);
258
+ if (mine !== seq.current) return;
259
+ setSettings(next);
260
+ announce(next);
261
+ } catch (err) {
262
+ if (mine !== seq.current) return;
263
+ setError(avatarErrorText(err));
264
+ avatarClient.get().then((s) => mine === seq.current && setSettings(s)).catch((e) => setError(avatarErrorText(e)));
265
+ }
266
+ },
267
+ []
268
+ );
269
+ const owned = useMemo(() => new Set(settings?.cosmetics.owned ?? []), [settings]);
270
+ const loadout = settings?.cosmetics.loadout;
271
+ const equippedOverlay = loadout?.equipped.overlay ?? null;
272
+ const art = equippedOverlay ? COSMETICS_BY_ID.get(equippedOverlay)?.art : void 0;
273
+ const editing = tab === "overlay" && !tryOn && !!art && !!equippedOverlay;
274
+ const [live, setLive] = useState(PLACEMENT_DEFAULT);
275
+ const saveTimer = useRef(void 0);
276
+ const dragging = useRef(false);
277
+ useEffect(() => {
278
+ if (dragging.current || !equippedOverlay) return;
279
+ setLive(loadout?.placement[equippedOverlay] ?? PLACEMENT_DEFAULT);
280
+ }, [equippedOverlay, loadout]);
281
+ useEffect(() => () => clearTimeout(saveTimer.current), []);
282
+ const place = useCallback(
283
+ (raw, save) => {
284
+ if (!art || !equippedOverlay) return;
285
+ const p = fitPlacement(art, raw);
286
+ setLive(p);
287
+ applyPlacement(root.current, equippedOverlay, p);
288
+ clearTimeout(saveTimer.current);
289
+ if (save === "no") return;
290
+ const commit = () => void wear(
291
+ { placement: { [equippedOverlay]: p } },
292
+ (s) => withLoadout(s, {
293
+ ...s.cosmetics.loadout,
294
+ placement: { ...s.cosmetics.loadout.placement, [equippedOverlay]: p }
295
+ })
296
+ );
297
+ if (save === "now") commit();
298
+ else saveTimer.current = setTimeout(commit, 400);
299
+ },
300
+ [art, equippedOverlay, wear]
301
+ );
302
+ if (!settings) {
303
+ return /* @__PURE__ */ jsx("span", { className: "settings-popout__hint", role: error ? "alert" : void 0, children: error ?? "…" });
304
+ }
305
+ const characters = new Map((sets ?? []).flatMap((s) => s.characters).map((c) => [c.id, c]));
306
+ const character = settings.sprite ? characters.get(settings.sprite) : void 0;
307
+ const picture = settings.avatar.url ?? (character ? portraitUrl(character) : null);
308
+ const colour = character && /^#[0-9a-f]{6}$/i.test(character.color) ? { "--hc-base": character.color } : {};
309
+ const displayName = name || "You";
310
+ const worn = { ...settings.avatar.cosmetics ?? {} };
311
+ if (tryOn) {
312
+ worn[tryOn.slot] = tryOn.id;
313
+ if (tryOn.slot === "overlay") delete worn.placement;
314
+ }
315
+ const equip = (slot, id) => {
316
+ setTryOn(null);
317
+ void wear({ equip: { [slot]: id } }, (s) => {
318
+ const l = s.cosmetics.loadout;
319
+ return withLoadout(s, cleanLoadout({ ...l, equipped: { ...l.equipped, [slot]: id } }, owned));
320
+ });
321
+ };
322
+ const stage = /* @__PURE__ */ jsx(
323
+ Stage,
324
+ {
325
+ html: avatarHtml({ src: picture, size: 112, worn }) + nameHtml(displayName, worn, { large: true }),
326
+ editing,
327
+ overlayId: equippedOverlay,
328
+ live,
329
+ onPlace: place,
330
+ dragging,
331
+ style: colour,
332
+ banner: tryOn && /* @__PURE__ */ jsxs("div", { style: S.tryOn, children: [
333
+ /* @__PURE__ */ jsx(Icon, { name: "lock", size: 12 }),
334
+ /* @__PURE__ */ jsxs("span", { style: { flex: 1, minWidth: 0 }, children: [
335
+ "Trying on ",
336
+ COSMETICS_BY_ID.get(tryOn.id)?.label,
337
+ " ·",
338
+ " ",
339
+ COSMETICS_BY_ID.get(tryOn.id)?.earn
340
+ ] }),
341
+ /* @__PURE__ */ jsx("button", { type: "button", style: S.linkBtn, onClick: () => setTryOn(null), children: "Take off" })
342
+ ] })
343
+ }
344
+ );
345
+ const chat = /* @__PURE__ */ jsxs("div", { style: S.chat, children: [
346
+ /* @__PURE__ */ jsxs("div", { style: S.chatHead, children: [
347
+ /* @__PURE__ */ jsx("span", { children: "In chat" }),
348
+ /* @__PURE__ */ jsx(
349
+ "button",
350
+ {
351
+ type: "button",
352
+ className: "settings-popout__btn",
353
+ onClick: () => setReplay((n) => n + 1),
354
+ children: "Replay"
355
+ }
356
+ )
357
+ ] }),
358
+ /* @__PURE__ */ jsxs("div", { style: S.msg, children: [
359
+ /* @__PURE__ */ jsx(Html, { html: avatarHtml({ src: picture, size: 36, worn }), style: colour }),
360
+ /* @__PURE__ */ jsxs("div", { style: { minWidth: 0, ...colour }, children: [
361
+ /* @__PURE__ */ jsx("div", { style: S.who, children: /* @__PURE__ */ jsx(Html, { html: nameHtml(displayName, worn) }) }),
362
+ /* @__PURE__ */ jsx("div", { style: S.line, children: /* @__PURE__ */ jsx(Html, { html: messageHtml("gg, that last hand was brutal", worn) }, replay) })
363
+ ] })
364
+ ] })
365
+ ] });
366
+ const placement = editing && art && /* @__PURE__ */ jsxs("div", { style: S.place, role: "group", "aria-label": "Placement", children: [
367
+ /* @__PURE__ */ jsxs("div", { style: S.placeHead, children: [
368
+ /* @__PURE__ */ jsx("strong", { children: "Placement" }),
369
+ /* @__PURE__ */ jsx("span", { style: S.hint, children: "Drag it on the avatar. The top handle turns it, the corner resizes it." })
370
+ ] }),
371
+ SLIDERS.map(([k, label, unit]) => /* @__PURE__ */ jsxs("label", { style: S.slider, children: [
372
+ /* @__PURE__ */ jsx("span", { children: label }),
373
+ /* @__PURE__ */ jsx(
374
+ "input",
375
+ {
376
+ type: "range",
377
+ min: PLACEMENT_LIMITS[k][0],
378
+ max: PLACEMENT_LIMITS[k][1],
379
+ value: live[k],
380
+ onChange: (e) => place({ ...live, [k]: Number(e.currentTarget.value) }, "later"),
381
+ style: { width: "100%", accentColor: "var(--color-primary)" }
382
+ }
383
+ ),
384
+ /* @__PURE__ */ jsxs("output", { style: S.out, children: [
385
+ live[k],
386
+ unit
387
+ ] })
388
+ ] }, k)),
389
+ /* @__PURE__ */ jsxs("div", { style: S.toggles, children: [
390
+ /* @__PURE__ */ jsxs("label", { children: [
391
+ /* @__PURE__ */ jsx(
392
+ "input",
393
+ {
394
+ type: "checkbox",
395
+ checked: live.flip,
396
+ onChange: (e) => place({ ...live, flip: e.currentTarget.checked }, "now")
397
+ }
398
+ ),
399
+ " ",
400
+ "Mirror"
401
+ ] }),
402
+ /* @__PURE__ */ jsxs("label", { children: [
403
+ /* @__PURE__ */ jsx(
404
+ "input",
405
+ {
406
+ type: "checkbox",
407
+ checked: live.back,
408
+ onChange: (e) => place({ ...live, back: e.currentTarget.checked }, "now")
409
+ }
410
+ ),
411
+ " ",
412
+ "Behind"
413
+ ] }),
414
+ /* @__PURE__ */ jsx(
415
+ "button",
416
+ {
417
+ type: "button",
418
+ className: "settings-popout__btn",
419
+ onClick: () => place(PLACEMENT_DEFAULT, "now"),
420
+ children: "Reset"
421
+ }
422
+ )
423
+ ] })
424
+ ] });
425
+ const tabs = ["picture", ...COSMETIC_SLOTS];
426
+ const tabLabel = (t) => t === "picture" ? "Picture" : SLOT_LABEL[t];
427
+ return /* @__PURE__ */ jsxs(
428
+ "div",
429
+ {
430
+ ref: root,
431
+ style: { ...S.body, gridTemplateColumns: narrow ? "minmax(0, 1fr)" : "240px minmax(0, 1fr)" },
432
+ children: [
433
+ /* @__PURE__ */ jsxs("div", { style: S.side, children: [
434
+ stage,
435
+ chat,
436
+ placement
437
+ ] }),
438
+ /* @__PURE__ */ jsxs("div", { style: { minWidth: 0 }, children: [
439
+ /* @__PURE__ */ jsx("div", { style: S.tabs, role: "tablist", "aria-label": "Avatar", children: tabs.map((t) => {
440
+ const items = t === "picture" ? [] : COSMETICS.filter((i) => i.slot === t);
441
+ return /* @__PURE__ */ jsxs(
442
+ "button",
443
+ {
444
+ type: "button",
445
+ role: "tab",
446
+ "aria-selected": tab === t,
447
+ style: { ...S.tab, ...tab === t ? S.tabOn : {} },
448
+ onClick: () => {
449
+ setTab(t);
450
+ setTryOn(null);
451
+ },
452
+ children: [
453
+ tabLabel(t),
454
+ t !== "picture" && /* @__PURE__ */ jsxs("span", { style: S.count, children: [
455
+ items.filter((i) => owned.has(i.id)).length,
456
+ "/",
457
+ items.length
458
+ ] })
459
+ ]
460
+ },
461
+ t
462
+ );
463
+ }) }),
464
+ /* @__PURE__ */ jsxs("div", { style: S.pane, role: "tabpanel", "aria-label": tabLabel(tab), children: [
465
+ tab === "picture" ? /* @__PURE__ */ jsx(AvatarPicker, { settings, sets, busy, run }) : /* @__PURE__ */ jsx(
466
+ SlotGrid,
467
+ {
468
+ slot: tab,
469
+ owned,
470
+ equipped: loadout?.equipped[tab] ?? null,
471
+ tryOn,
472
+ showLocked,
473
+ onShowLocked: setShowLocked,
474
+ preview: (id) => {
475
+ const only = { [tab]: id ?? void 0 };
476
+ if (tab === "overlay") return avatarHtml({ src: picture, size: 40, worn: only });
477
+ if (tab === "message") return messageHtml("gg, rematch?", only, { keep: true });
478
+ return nameHtml(displayName, { ...worn, [tab]: id ?? void 0 });
479
+ },
480
+ colour,
481
+ onEquip: (id) => equip(tab, id),
482
+ onTry: (id) => setTryOn((t) => t?.slot === tab && t.id === id ? null : { slot: tab, id })
483
+ }
484
+ ),
485
+ error && /* @__PURE__ */ jsx("span", { className: "settings-popout__error", role: "alert", children: error })
486
+ ] })
487
+ ] })
488
+ ]
489
+ }
490
+ );
491
+ }
492
+ function Stage({ html, editing, overlayId, live, onPlace, dragging, style, banner }) {
493
+ const box = useRef(null);
494
+ const drag = useRef(null);
495
+ useEffect(() => {
496
+ const adj = box.current?.querySelector(".hc-art .hc-adj");
497
+ if (!editing || !adj) return;
498
+ const gz = document.createElement("span");
499
+ gz.dataset.g = "move";
500
+ gz.setAttribute("aria-hidden", "true");
501
+ gz.style.cssText = "position:absolute;inset:-3px;border:1.5px dashed var(--color-primary);border-radius:6px;pointer-events:auto;cursor:move";
502
+ const handle = (g, css) => {
503
+ const h = document.createElement("i");
504
+ h.dataset.g = g;
505
+ h.style.cssText = `position:absolute;pointer-events:auto;${css}`;
506
+ gz.appendChild(h);
507
+ };
508
+ handle(
509
+ "rot",
510
+ "left:50%;top:-20px;width:12px;height:12px;margin-left:-6px;border-radius:50%;background:var(--color-primary);cursor:grab"
511
+ );
512
+ handle(
513
+ "scale",
514
+ "right:-7px;bottom:-7px;width:12px;height:12px;border-radius:3px;background:var(--color-bg-card);border:2px solid var(--color-primary);cursor:nwse-resize"
515
+ );
516
+ adj.appendChild(gz);
517
+ return () => gz.remove();
518
+ }, [html, editing]);
519
+ const down = (e) => {
520
+ const g = e.target.closest("[data-g]");
521
+ const adj = g?.closest(".hc-adj");
522
+ const avatar = box.current?.querySelector(".hc-av");
523
+ if (!g || !adj || !avatar || !editing || !overlayId) return;
524
+ e.preventDefault();
525
+ g.setPointerCapture(e.pointerId);
526
+ const r = adj.getBoundingClientRect();
527
+ const cx = r.left + r.width / 2;
528
+ const cy = r.top + r.height / 2;
529
+ const size = avatar.getBoundingClientRect().width;
530
+ dragging.current = true;
531
+ drag.current = {
532
+ mode: g.dataset.g ?? "move",
533
+ p0: live,
534
+ x0: e.clientX,
535
+ y0: e.clientY,
536
+ cx,
537
+ cy,
538
+ a0: Math.atan2(e.clientY - cy, e.clientX - cx),
539
+ d0: Math.hypot(e.clientX - cx, e.clientY - cy) || 1,
540
+ size,
541
+ last: live
542
+ };
543
+ };
544
+ const move = (e) => {
545
+ const d = drag.current;
546
+ if (!d) return;
547
+ let next = d.p0;
548
+ if (d.mode === "move") {
549
+ next = {
550
+ ...d.p0,
551
+ x: d.p0.x + (e.clientX - d.x0) / d.size * 100,
552
+ y: d.p0.y + (e.clientY - d.y0) / d.size * 100
553
+ };
554
+ } else if (d.mode === "rot") {
555
+ const a = Math.atan2(e.clientY - d.cy, e.clientX - d.cx);
556
+ next = { ...d.p0, r: d.p0.r + (a - d.a0) * 180 / Math.PI };
557
+ } else {
558
+ next = { ...d.p0, s: d.p0.s * Math.hypot(e.clientX - d.cx, e.clientY - d.cy) / d.d0 };
559
+ }
560
+ d.last = next;
561
+ onPlace(next, "no");
562
+ };
563
+ const up = () => {
564
+ const d = drag.current;
565
+ if (!d) return;
566
+ drag.current = null;
567
+ dragging.current = false;
568
+ onPlace(d.last, "now");
569
+ };
570
+ return /* @__PURE__ */ jsxs(
571
+ "div",
572
+ {
573
+ ref: box,
574
+ role: "group",
575
+ "aria-label": "Preview",
576
+ style: { ...S.stage, ...style },
577
+ onPointerDown: down,
578
+ onPointerMove: move,
579
+ onPointerUp: up,
580
+ onPointerCancel: up,
581
+ children: [
582
+ banner,
583
+ /* @__PURE__ */ jsx(Html, { html, style: { display: "contents" } })
584
+ ]
585
+ }
586
+ );
587
+ }
588
+ function SlotGrid({
589
+ slot,
590
+ owned,
591
+ equipped,
592
+ tryOn,
593
+ showLocked,
594
+ onShowLocked,
595
+ preview,
596
+ colour,
597
+ onEquip,
598
+ onTry
599
+ }) {
600
+ const items = COSMETICS.filter((i) => i.slot === slot);
601
+ const locked = items.filter((i) => !owned.has(i.id));
602
+ const shown = showLocked ? items : items.filter((i) => owned.has(i.id));
603
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
604
+ /* @__PURE__ */ jsxs("div", { style: S.grid, children: [
605
+ /* @__PURE__ */ jsxs(
606
+ "button",
607
+ {
608
+ type: "button",
609
+ "aria-pressed": equipped === null,
610
+ style: { ...S.tile, ...equipped === null ? S.tileOn : {} },
611
+ onClick: () => onEquip(null),
612
+ children: [
613
+ /* @__PURE__ */ jsx(Html, { html: preview(null), style: { ...S.tilePv, ...colour } }),
614
+ /* @__PURE__ */ jsx("span", { style: S.tileName, children: "None" })
615
+ ]
616
+ }
617
+ ),
618
+ shown.map((item) => {
619
+ const own = owned.has(item.id);
620
+ const on = equipped === item.id;
621
+ const trying = tryOn?.slot === slot && tryOn.id === item.id;
622
+ return /* @__PURE__ */ jsxs(
623
+ "button",
624
+ {
625
+ type: "button",
626
+ "aria-pressed": own ? on : trying,
627
+ "aria-label": own ? item.label : `${item.label}, locked. ${item.earn}. Try it on.`,
628
+ style: {
629
+ ...S.tile,
630
+ ...on ? S.tileOn : {},
631
+ ...trying ? S.tileTrying : {}
632
+ },
633
+ onClick: () => own ? onEquip(item.id) : onTry(item.id),
634
+ onMouseEnter: (e) => slot === "message" && replayPreview(e.currentTarget),
635
+ children: [
636
+ /* @__PURE__ */ jsx(
637
+ Html,
638
+ {
639
+ html: preview(item.id),
640
+ style: { ...S.tilePv, ...colour, opacity: own ? 1 : 0.55 }
641
+ }
642
+ ),
643
+ /* @__PURE__ */ jsxs("span", { style: { ...S.tileName, color: own ? void 0 : "var(--color-text-muted)" }, children: [
644
+ !own && /* @__PURE__ */ jsx(Icon, { name: "lock", size: 12 }),
645
+ item.label
646
+ ] }),
647
+ !own && /* @__PURE__ */ jsx("span", { style: S.tileEarn, children: item.earn })
648
+ ]
649
+ },
650
+ item.id
651
+ );
652
+ })
653
+ ] }),
654
+ /* @__PURE__ */ jsxs("div", { style: S.foot, children: [
655
+ /* @__PURE__ */ jsx("span", { children: locked.length ? `${locked.length} still to earn. Pick one to try it on.` : "You have earned all of these." }),
656
+ locked.length > 0 && /* @__PURE__ */ jsxs("label", { children: [
657
+ /* @__PURE__ */ jsx(
658
+ "input",
659
+ {
660
+ type: "checkbox",
661
+ checked: showLocked,
662
+ onChange: (e) => onShowLocked(e.currentTarget.checked)
663
+ }
664
+ ),
665
+ " ",
666
+ "Show locked"
667
+ ] })
668
+ ] })
669
+ ] });
670
+ }
671
+ export {
672
+ AvatarBuilder
673
+ };
674
+ //# sourceMappingURL=AvatarBuilder.js.map