@hadoku/task-ui-components 4.23.5 → 4.23.7

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