@hadoku/task-ui-components 4.23.10 → 4.23.11
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/dist/components/AvatarBuilder.d.ts +2 -1
- package/dist/components/AvatarBuilder.d.ts.map +1 -1
- package/dist/components/AvatarBuilder.js +20 -28
- package/dist/components/AvatarBuilder.js.map +1 -1
- package/dist/components/AvatarPicker.d.ts.map +1 -1
- package/dist/components/AvatarPicker.js +20 -10
- package/dist/components/AvatarPicker.js.map +1 -1
- package/dist/vendor/wolffm/prefs-client/avatar.js.map +1 -1
- package/dist/vendor/wolffm/prefs-client/cosmetics.js +112 -0
- package/dist/vendor/wolffm/prefs-client/cosmetics.js.map +1 -1
- package/package.json +2 -2
- package/src/components/AvatarBuilder.tsx +40 -25
- package/src/components/AvatarPicker.tsx +53 -44
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
*
|
|
19
19
|
* Styles are inline for the reason AvatarPicker's are: apps compile this
|
|
20
20
|
* package's CSS at their own build. Tokens only; the cosmetics' own colours come
|
|
21
|
-
* from the person's
|
|
21
|
+
* from the person's colour, their picture's average or else their character's
|
|
22
|
+
* (and, for overlay images, the art).
|
|
22
23
|
*/
|
|
23
24
|
import React from 'react';
|
|
24
25
|
export interface AvatarBuilderProps {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AvatarBuilder.d.ts","sourceRoot":"","sources":["../../src/components/AvatarBuilder.tsx"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"AvatarBuilder.d.ts","sourceRoot":"","sources":["../../src/components/AvatarBuilder.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,OAAO,KAON,MAAM,OAAO,CAAA;AAuVd,MAAM,WAAW,kBAAkB;IACjC,kDAAkD;IAClD,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;IACnB,sEAAsE;IACtE,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB;AAED,wBAAgB,aAAa,CAAC,EAAE,IAAI,EAAE,OAAe,EAAE,EAAE,kBAAkB,qBA0U1E"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx, jsxs, Fragment } from "react/jsx-runtime";
|
|
2
|
-
import React, { useState, useRef, useEffect, useCallback, useMemo } from "react";
|
|
2
|
+
import React, { useState, useRef, useEffect, useCallback, useMemo, useSyncExternalStore } from "react";
|
|
3
3
|
import { Icon } from "@wolffm/themes";
|
|
4
|
-
import { ensureCosmeticsStyles, COSMETICS_BY_ID, PLACEMENT_DEFAULT, fitPlacement, avatarHtml, nameHtml, messageHtml, PLACEMENT_LIMITS, COSMETIC_SLOTS, placementTransform, COSMETICS, cleanLoadout, wornCosmetics } from "../vendor/wolffm/prefs-client/cosmetics.js";
|
|
4
|
+
import { ensureCosmeticsStyles, COSMETICS_BY_ID, PLACEMENT_DEFAULT, fitPlacement, avatarHtml, nameHtml, messageHtml, PLACEMENT_LIMITS, COSMETIC_SLOTS, readPictureColour, onPictureColour, pictureColour, placementTransform, COSMETICS, cleanLoadout, wornCosmetics } from "../vendor/wolffm/prefs-client/cosmetics.js";
|
|
5
5
|
import { AvatarPicker } from "./AvatarPicker.js";
|
|
6
6
|
import { AVATAR_EVENT } from "../lib/avatarEvent.js";
|
|
7
7
|
import { avatarClient, avatarErrorText, fetchCharacters, portraitUrl } from "../lib/avatarData.js";
|
|
@@ -263,6 +263,16 @@ function useDragScroll() {
|
|
|
263
263
|
}, [el]);
|
|
264
264
|
return setEl;
|
|
265
265
|
}
|
|
266
|
+
function usePictureColour(url) {
|
|
267
|
+
useEffect(() => {
|
|
268
|
+
if (url) void readPictureColour(url);
|
|
269
|
+
}, [url]);
|
|
270
|
+
return useSyncExternalStore(
|
|
271
|
+
onPictureColour,
|
|
272
|
+
() => url ? pictureColour(url) ?? null : null,
|
|
273
|
+
() => null
|
|
274
|
+
);
|
|
275
|
+
}
|
|
266
276
|
function AvatarBuilder({ name, isAdmin = false }) {
|
|
267
277
|
const [settings, setSettings] = useState(null);
|
|
268
278
|
const [sets, setSets] = useState(null);
|
|
@@ -270,13 +280,13 @@ function AvatarBuilder({ name, isAdmin = false }) {
|
|
|
270
280
|
const [error, setError] = useState(null);
|
|
271
281
|
const [tab, setTab] = useState("picture");
|
|
272
282
|
const [tryOn, setTryOn] = useState(null);
|
|
273
|
-
const [showLocked, setShowLocked] = useState(true);
|
|
274
283
|
const [replay, setReplay] = useState(0);
|
|
275
284
|
const narrow = useNarrow();
|
|
276
285
|
const root = useRef(null);
|
|
277
286
|
const tabStrip = useDragScroll();
|
|
278
287
|
const seq = useRef(0);
|
|
279
288
|
useEffect(() => ensureCosmeticsStyles(), []);
|
|
289
|
+
const tint = usePictureColour(settings?.avatar.url ?? null);
|
|
280
290
|
useEffect(() => {
|
|
281
291
|
let live2 = true;
|
|
282
292
|
avatarClient.get().then((s) => live2 && setSettings(s)).catch((err) => live2 && setError(avatarErrorText(err)));
|
|
@@ -360,7 +370,8 @@ function AvatarBuilder({ name, isAdmin = false }) {
|
|
|
360
370
|
const characters = new Map((sets ?? []).flatMap((s) => s.characters).map((c) => [c.id, c]));
|
|
361
371
|
const character = settings.sprite ? characters.get(settings.sprite) : void 0;
|
|
362
372
|
const picture = settings.avatar.url ?? (character ? portraitUrl(character) : null);
|
|
363
|
-
const
|
|
373
|
+
const base = tint ?? (character && /^#[0-9a-f]{6}$/i.test(character.color) ? character.color : null);
|
|
374
|
+
const colour = base ? { "--hc-base": base } : {};
|
|
364
375
|
const displayName = name || "You";
|
|
365
376
|
const worn = { ...settings.avatar.cosmetics ?? {} };
|
|
366
377
|
if (tryOn) {
|
|
@@ -524,8 +535,6 @@ function AvatarBuilder({ name, isAdmin = false }) {
|
|
|
524
535
|
owned,
|
|
525
536
|
equipped: loadout?.equipped[tab] ?? null,
|
|
526
537
|
tryOn,
|
|
527
|
-
showLocked,
|
|
528
|
-
onShowLocked: setShowLocked,
|
|
529
538
|
isAdmin,
|
|
530
539
|
preview: (id) => {
|
|
531
540
|
const only = { [tab]: id ?? void 0 };
|
|
@@ -646,8 +655,6 @@ function SlotGrid({
|
|
|
646
655
|
owned,
|
|
647
656
|
equipped,
|
|
648
657
|
tryOn,
|
|
649
|
-
showLocked,
|
|
650
|
-
onShowLocked,
|
|
651
658
|
isAdmin,
|
|
652
659
|
preview,
|
|
653
660
|
colour,
|
|
@@ -656,7 +663,6 @@ function SlotGrid({
|
|
|
656
663
|
}) {
|
|
657
664
|
const items = COSMETICS.filter((i) => i.slot === slot && (!i.hidden || isAdmin));
|
|
658
665
|
const locked = items.filter((i) => !i.hidden && !owned.has(i.id));
|
|
659
|
-
const shown = showLocked ? items : items.filter((i) => i.hidden || owned.has(i.id));
|
|
660
666
|
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
661
667
|
/* @__PURE__ */ jsxs("div", { style: S.grid, children: [
|
|
662
668
|
/* @__PURE__ */ jsxs(
|
|
@@ -672,7 +678,7 @@ function SlotGrid({
|
|
|
672
678
|
]
|
|
673
679
|
}
|
|
674
680
|
),
|
|
675
|
-
|
|
681
|
+
items.map((item) => {
|
|
676
682
|
if (item.hidden) {
|
|
677
683
|
return /* @__PURE__ */ jsxs(
|
|
678
684
|
"button",
|
|
@@ -725,24 +731,10 @@ function SlotGrid({
|
|
|
725
731
|
);
|
|
726
732
|
})
|
|
727
733
|
] }),
|
|
728
|
-
locked.length > 0 && /* @__PURE__ */
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
] }),
|
|
733
|
-
/* @__PURE__ */ jsxs("label", { children: [
|
|
734
|
-
/* @__PURE__ */ jsx(
|
|
735
|
-
"input",
|
|
736
|
-
{
|
|
737
|
-
type: "checkbox",
|
|
738
|
-
checked: showLocked,
|
|
739
|
-
onChange: (e) => onShowLocked(e.currentTarget.checked)
|
|
740
|
-
}
|
|
741
|
-
),
|
|
742
|
-
" ",
|
|
743
|
-
"Show locked"
|
|
744
|
-
] })
|
|
745
|
-
] })
|
|
734
|
+
locked.length > 0 && /* @__PURE__ */ jsx("div", { style: S.foot, children: /* @__PURE__ */ jsxs("span", { children: [
|
|
735
|
+
locked.length === 1 ? "One to earn." : `${locked.length} to earn.`,
|
|
736
|
+
" Pick one to try it on."
|
|
737
|
+
] }) })
|
|
746
738
|
] });
|
|
747
739
|
}
|
|
748
740
|
export {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AvatarBuilder.js","sources":["../../src/components/AvatarBuilder.tsx"],"sourcesContent":["/**\n * AvatarBuilder — the body of AvatarDialog: the person's avatar and what they\n * wear with it, with a live preview.\n *\n * Layout (operator's pick, 2026-11-01): a preview column on the left (the\n * avatar large, with the name, and a chat line as others see it), and on the\n * right tabs for the Picture (AvatarPicker) and each cosmetic slot. Narrow\n * screens stack the two.\n *\n * Cosmetics (`@wolffm/prefs-client/cosmetics`, bundled at build like the avatar\n * client): what the person owns equips on click (every shown item is free but\n * three earned in a Major); an earned item not yet earned shows how to earn\n * it and can be tried on in the preview, which saves nothing. An overlay\n * image can be placed: drag it on the big preview, turn it with the top handle,\n * resize it with the corner handle, or use the sliders. Every move goes through\n * `fitPlacement`, the same function prefs-api runs before it stores one, so the\n * preview never shows a spot the server would refuse.\n *\n * Styles are inline for the reason AvatarPicker's are: apps compile this\n * package's CSS at their own build. Tokens only; the cosmetics' own colours come\n * from the person's character colour (and, for overlay images, the art).\n */\nimport React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'\nimport { Icon } from '@wolffm/themes'\nimport type { AvatarSettings, LoadoutUpdate } from '@wolffm/prefs-client/avatar'\nimport {\n COSMETICS,\n COSMETICS_BY_ID,\n COSMETIC_SLOTS,\n PLACEMENT_DEFAULT,\n PLACEMENT_LIMITS,\n avatarHtml,\n cleanLoadout,\n ensureCosmeticsStyles,\n fitPlacement,\n messageHtml,\n nameHtml,\n placementTransform,\n wornCosmetics,\n type AvatarCosmetics,\n type CosmeticSlot,\n type Loadout,\n type OverlayArt,\n type Placement\n} from '@wolffm/prefs-client/cosmetics'\nimport { AvatarPicker } from './AvatarPicker'\nimport { AVATAR_EVENT } from '../lib/avatarEvent'\nimport {\n avatarClient,\n avatarErrorText,\n fetchCharacters,\n portraitUrl,\n type CharacterSet\n} from '../lib/avatarData'\n\ntype Tab = 'picture' | CosmeticSlot\n\nconst SLOT_LABEL: Record<CosmeticSlot, string> = {\n effect: 'Effect',\n gradient: 'Gradient',\n font: 'Font',\n overlay: 'Overlay',\n message: 'Message'\n}\n\nconst SLIDERS = [\n ['x', 'Left/right', '%'],\n ['y', 'Up/down', '%'],\n ['r', 'Rotate', '°'],\n ['s', 'Size', '%']\n] as const\n\nconst S = {\n body: { display: 'grid', gap: 16, alignItems: 'start' },\n side: { display: 'flex', flexDirection: 'column', gap: 12, minWidth: 0 },\n stage: {\n position: 'relative',\n display: 'flex',\n flexDirection: 'column',\n alignItems: 'center',\n gap: 12,\n padding: '48px 12px 18px',\n borderRadius: 10,\n background: 'var(--color-bg-alt)',\n border: '1px solid var(--color-border)',\n touchAction: 'none',\n userSelect: 'none'\n },\n tryOn: {\n position: 'absolute',\n left: 8,\n right: 8,\n top: 8,\n display: 'flex',\n alignItems: 'center',\n gap: 6,\n padding: '4px 8px',\n borderRadius: 6,\n background: 'var(--color-primary-bg)',\n color: 'var(--color-on-primary-bg)',\n fontSize: '0.72rem'\n },\n linkBtn: {\n border: 0,\n background: 'none',\n color: 'inherit',\n textDecoration: 'underline',\n cursor: 'pointer',\n fontSize: 'inherit',\n padding: 0,\n flex: 'none'\n },\n chat: {\n display: 'flex',\n flexDirection: 'column',\n gap: 10,\n padding: 12,\n borderRadius: 10,\n background: 'var(--color-bg)',\n border: '1px solid var(--color-border)'\n },\n chatHead: {\n display: 'flex',\n justifyContent: 'space-between',\n alignItems: 'center',\n fontSize: '0.72rem',\n color: 'var(--color-text-muted)'\n },\n msg: { display: 'flex', gap: 10, alignItems: 'flex-start' },\n who: { minHeight: 22, display: 'flex', alignItems: 'center' },\n line: { fontSize: '0.85rem', marginTop: 2 },\n place: {\n display: 'flex',\n flexDirection: 'column',\n gap: 8,\n padding: 12,\n borderRadius: 10,\n border: '1px solid var(--color-border)'\n },\n placeHead: { display: 'flex', flexDirection: 'column', gap: 2, fontSize: '0.8rem' },\n hint: { fontSize: '0.72rem', color: 'var(--color-text-muted)' },\n slider: {\n display: 'grid',\n gridTemplateColumns: '64px 1fr 40px',\n alignItems: 'center',\n gap: 6,\n fontSize: '0.75rem',\n color: 'var(--color-text-muted)'\n },\n out: { textAlign: 'right', color: 'var(--color-text)', fontVariantNumeric: 'tabular-nums' },\n toggles: {\n display: 'flex',\n flexWrap: 'wrap',\n gap: 10,\n alignItems: 'center',\n fontSize: '0.75rem',\n color: 'var(--color-text-muted)'\n },\n tabs: {\n display: 'flex',\n gap: 2,\n cursor: 'grab',\n touchAction: 'pan-x',\n borderBottom: '1px solid var(--color-border)',\n overflowX: 'auto',\n scrollbarWidth: 'none'\n },\n tab: {\n flex: 'none',\n padding: '7px 11px',\n borderBottomWidth: 2,\n borderBottomStyle: 'solid',\n borderBottomColor: 'transparent',\n borderTop: 0,\n borderLeft: 0,\n borderRight: 0,\n background: 'none',\n cursor: 'pointer',\n fontSize: '0.82rem',\n fontWeight: 500,\n color: 'var(--color-text-muted)'\n },\n tabOn: { color: 'var(--color-text)', borderBottomColor: 'var(--color-primary)' },\n pane: { display: 'flex', flexDirection: 'column', gap: 10, paddingTop: 10 },\n grid: { display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(112px, 1fr))', gap: 8 },\n tile: {\n position: 'relative',\n display: 'flex',\n flexDirection: 'column',\n gap: 5,\n padding: 6,\n borderRadius: 8,\n border: '1px solid var(--color-border)',\n background: 'var(--color-bg)',\n color: 'var(--color-text)',\n cursor: 'pointer',\n textAlign: 'left',\n font: 'inherit'\n },\n tileOn: {\n borderColor: 'var(--color-primary)',\n boxShadow: 'inset 0 0 0 1px var(--color-primary)'\n },\n tileTrying: { borderStyle: 'dashed', borderColor: 'var(--color-primary)' },\n tilePv: {\n height: 58,\n borderRadius: 6,\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center',\n background: 'var(--color-bg-alt)',\n overflow: 'hidden',\n padding: '0 8px',\n fontSize: '0.8rem'\n },\n tileName: { fontSize: '0.8rem', fontWeight: 500, display: 'flex', alignItems: 'center', gap: 5 },\n tileEarn: { fontSize: '0.7rem', lineHeight: 1.3, color: 'var(--color-text-muted)' },\n foot: {\n display: 'flex',\n justifyContent: 'space-between',\n alignItems: 'center',\n gap: 8,\n flexWrap: 'wrap',\n fontSize: '0.75rem',\n color: 'var(--color-text-muted)'\n }\n} satisfies Record<string, React.CSSProperties>\n\n/**\n * Trusted HTML from the cosmetics renderer, which escapes every string it is\n * given. Memoised on the string and the style: React re-sets innerHTML on every\n * render even when the string is the same, and replacing the nodes under the\n * pointer between mousedown and mouseup swallows the click.\n */\nconst Html = React.memo(\n function Html({ html, style }: { html: string; style?: React.CSSProperties }) {\n return <span style={style} dangerouslySetInnerHTML={{ __html: html }} />\n },\n (a, b) => a.html === b.html && JSON.stringify(a.style) === JSON.stringify(b.style)\n)\n\n/**\n * Play a tile's message reveal again. A finished reveal holds no animation (it\n * fills backwards only), so its markup is re-set. This runs synchronously in\n * mouseenter, before any press, so it cannot split a click; the memo above keeps\n * React from touching it afterwards.\n */\nfunction replayPreview(tile: HTMLElement) {\n const pv = tile.firstElementChild\n if (!pv) return\n const markup = pv.innerHTML\n pv.innerHTML = markup\n}\n\nfunction useNarrow(): boolean {\n const query = '(max-width: 640px)'\n const [narrow, setNarrow] = useState(() => window.matchMedia(query).matches)\n useEffect(() => {\n const mq = window.matchMedia(query)\n const on = () => setNarrow(mq.matches)\n mq.addEventListener('change', on)\n return () => mq.removeEventListener('change', on)\n }, [])\n return narrow\n}\n\n/** Move every drawing of one overlay in `root` to `p`, without re-rendering (smooth while dragging). */\nfunction applyPlacement(root: HTMLElement | null, id: string, p: Placement) {\n root?.querySelectorAll<HTMLElement>(`.hc-art[data-hc-art=\"${id}\"]`).forEach(ol => {\n ol.style.zIndex = p.back ? '0' : '3'\n const adj = ol.firstElementChild as HTMLElement | null\n if (adj) adj.style.transform = placementTransform(p)\n })\n}\n\n/** Settings with a new loadout, and what is worn recomputed from it. */\nfunction withLoadout(s: AvatarSettings, loadout: Loadout): AvatarSettings {\n return {\n ...s,\n cosmetics: { ...s.cosmetics, loadout },\n avatar: { ...s.avatar, cosmetics: wornCosmetics(loadout) }\n }\n}\n\n/**\n * A row that overflows sideways scrolls by dragging it (the operator's ask: the\n * tab strip is wider than its column and hides its scrollbar) and by the\n * mouse wheel. A drag of more than a few pixels is not a click.\n */\nfunction useDragScroll<T extends HTMLElement>() {\n // A callback ref, not useRef: the row appears only once the settings load,\n // after the first render, and the listeners must attach when it does.\n const [el, setEl] = useState<T | null>(null)\n useEffect(() => {\n if (!el) return\n let start: { x: number; left: number } | null = null\n let dragged = false\n const down = (e: PointerEvent) => {\n if (e.button !== 0) return\n start = { x: e.clientX, left: el.scrollLeft }\n dragged = false\n }\n const move = (e: PointerEvent) => {\n if (!start) return\n const dx = e.clientX - start.x\n if (!dragged && Math.abs(dx) > 4) {\n dragged = true\n el.setPointerCapture(e.pointerId)\n el.style.cursor = 'grabbing'\n }\n if (dragged) el.scrollLeft = start.left - dx\n }\n const up = () => {\n start = null\n el.style.cursor = ''\n }\n // The click that ends a drag is not a tab choice.\n const click = (e: MouseEvent) => {\n if (!dragged) return\n e.preventDefault()\n e.stopPropagation()\n dragged = false\n }\n const wheel = (e: globalThis.WheelEvent) => {\n if (el.scrollWidth <= el.clientWidth || Math.abs(e.deltaX) > Math.abs(e.deltaY)) return\n e.preventDefault()\n el.scrollLeft += e.deltaY\n }\n el.addEventListener('pointerdown', down)\n el.addEventListener('pointermove', move)\n el.addEventListener('pointerup', up)\n el.addEventListener('pointercancel', up)\n el.addEventListener('click', click, true)\n el.addEventListener('wheel', wheel, { passive: false })\n return () => {\n el.removeEventListener('pointerdown', down)\n el.removeEventListener('pointermove', move)\n el.removeEventListener('pointerup', up)\n el.removeEventListener('pointercancel', up)\n el.removeEventListener('click', click, true)\n el.removeEventListener('wheel', wheel)\n }\n }, [el])\n return setEl\n}\n\nexport interface AvatarBuilderProps {\n /** The person's display name, for the preview. */\n name: string | null\n /** Admins also see archived items (greyed; nobody can select one). */\n isAdmin?: boolean\n}\n\nexport function AvatarBuilder({ name, isAdmin = false }: AvatarBuilderProps) {\n const [settings, setSettings] = useState<AvatarSettings | null>(null)\n const [sets, setSets] = useState<CharacterSet[] | null>(null)\n const [busy, setBusy] = useState(false)\n const [error, setError] = useState<string | null>(null)\n const [tab, setTab] = useState<Tab>('picture')\n const [tryOn, setTryOn] = useState<{ slot: CosmeticSlot; id: string } | null>(null)\n const [showLocked, setShowLocked] = useState(true)\n const [replay, setReplay] = useState(0)\n const narrow = useNarrow()\n const root = useRef<HTMLDivElement>(null)\n const tabStrip = useDragScroll<HTMLDivElement>()\n // Only the newest cosmetics request may set state: equips sent quickly in a\n // row can answer out of order.\n const seq = useRef(0)\n\n useEffect(() => ensureCosmeticsStyles(), [])\n\n useEffect(() => {\n let live = true\n avatarClient\n .get()\n .then(s => live && setSettings(s))\n .catch((err: unknown) => live && setError(avatarErrorText(err)))\n fetchCharacters()\n .then(s => live && setSets(s))\n .catch((err: unknown) => live && setError(avatarErrorText(err)))\n return () => {\n live = false\n }\n }, [])\n\n const announce = (next: AvatarSettings) =>\n // The page under the popout (a watchparty room, a game) re-reads the avatar\n // from this instead of waiting for its next join.\n window.dispatchEvent(new window.CustomEvent<AvatarSettings>(AVATAR_EVENT, { detail: next }))\n\n const run = useCallback(async (work: () => Promise<AvatarSettings>) => {\n setBusy(true)\n setError(null)\n try {\n const next = await work()\n setSettings(next)\n announce(next)\n } catch (err) {\n setError(avatarErrorText(err))\n } finally {\n setBusy(false)\n }\n }, [])\n\n /** Save a cosmetics change, showing `optimistic` at once. A failure re-reads the truth. */\n const wear = useCallback(\n async (update: LoadoutUpdate, optimistic: (s: AvatarSettings) => AvatarSettings) => {\n const mine = ++seq.current\n setError(null)\n setSettings(s => (s ? optimistic(s) : s))\n try {\n const next = await avatarClient.setCosmetics(update)\n if (mine !== seq.current) return\n setSettings(next)\n announce(next)\n } catch (err) {\n if (mine !== seq.current) return\n setError(avatarErrorText(err))\n avatarClient\n .get()\n .then(s => mine === seq.current && setSettings(s))\n .catch((e: unknown) => setError(avatarErrorText(e)))\n }\n },\n []\n )\n\n const owned = useMemo(() => new Set(settings?.cosmetics.owned ?? []), [settings])\n const loadout = settings?.cosmetics.loadout\n const equippedOverlay = loadout?.equipped.overlay ?? null\n const art: OverlayArt | undefined = equippedOverlay\n ? COSMETICS_BY_ID.get(equippedOverlay)?.art\n : undefined\n const editing = tab === 'overlay' && !tryOn && !!art && !!equippedOverlay\n\n // The placement being edited, live; saved a beat after the last change.\n const [live, setLive] = useState<Placement>(PLACEMENT_DEFAULT)\n const saveTimer = useRef<ReturnType<typeof setTimeout>>(undefined)\n const dragging = useRef(false)\n useEffect(() => {\n if (dragging.current || !equippedOverlay) return\n setLive(loadout?.placement[equippedOverlay] ?? PLACEMENT_DEFAULT)\n }, [equippedOverlay, loadout])\n useEffect(() => () => clearTimeout(saveTimer.current), [])\n\n const place = useCallback(\n (raw: Partial<Placement>, save: 'later' | 'now' | 'no') => {\n if (!art || !equippedOverlay) return\n const p = fitPlacement(art, raw)\n setLive(p)\n applyPlacement(root.current, equippedOverlay, p)\n clearTimeout(saveTimer.current)\n if (save === 'no') return\n const commit = () =>\n void wear({ placement: { [equippedOverlay]: p } }, s =>\n withLoadout(s, {\n ...s.cosmetics.loadout,\n placement: { ...s.cosmetics.loadout.placement, [equippedOverlay]: p }\n })\n )\n if (save === 'now') commit()\n else saveTimer.current = setTimeout(commit, 400)\n },\n [art, equippedOverlay, wear]\n )\n\n if (!settings) {\n return (\n <span className=\"settings-popout__hint\" role={error ? 'alert' : undefined}>\n {error ?? '…'}\n </span>\n )\n }\n\n const characters = new Map((sets ?? []).flatMap(s => s.characters).map(c => [c.id, c]))\n const character = settings.sprite ? characters.get(settings.sprite) : undefined\n const picture = settings.avatar.url ?? (character ? portraitUrl(character) : null)\n // Every name cosmetic derives its colours from the character's.\n const colour: React.CSSProperties =\n character && /^#[0-9a-f]{6}$/i.test(character.color)\n ? ({ '--hc-base': character.color } as React.CSSProperties)\n : {}\n const displayName = name || 'You'\n\n const worn: AvatarCosmetics = { ...(settings.avatar.cosmetics ?? {}) }\n if (tryOn) {\n worn[tryOn.slot] = tryOn.id\n if (tryOn.slot === 'overlay') delete worn.placement\n }\n\n const equip = (slot: CosmeticSlot, id: string | null) => {\n setTryOn(null)\n void wear({ equip: { [slot]: id } }, s => {\n const l = s.cosmetics.loadout\n return withLoadout(s, cleanLoadout({ ...l, equipped: { ...l.equipped, [slot]: id } }, owned))\n })\n }\n\n const stage = (\n <Stage\n html={\n avatarHtml({ src: picture, size: 112, worn }) + nameHtml(displayName, worn, { large: true })\n }\n editing={editing}\n overlayId={equippedOverlay}\n live={live}\n onPlace={place}\n dragging={dragging}\n style={colour}\n banner={\n tryOn && (\n <div style={S.tryOn}>\n <Icon name=\"lock\" size={12} />\n <span style={{ flex: 1, minWidth: 0 }}>\n Trying on {COSMETICS_BY_ID.get(tryOn.id)?.label} ·{' '}\n {COSMETICS_BY_ID.get(tryOn.id)?.earn}\n </span>\n <button type=\"button\" style={S.linkBtn} onClick={() => setTryOn(null)}>\n Take off\n </button>\n </div>\n )\n }\n />\n )\n\n const chat = (\n <div style={S.chat}>\n <div style={S.chatHead}>\n <span>In chat</span>\n <button\n type=\"button\"\n className=\"settings-popout__btn\"\n onClick={() => setReplay(n => n + 1)}\n >\n Replay\n </button>\n </div>\n <div style={S.msg}>\n <Html html={avatarHtml({ src: picture, size: 36, worn })} style={colour} />\n <div style={{ minWidth: 0, ...colour }}>\n <div style={S.who}>\n <Html html={nameHtml(displayName, worn)} />\n </div>\n <div style={S.line}>\n <Html key={replay} html={messageHtml('gg, that last hand was brutal', worn)} />\n </div>\n </div>\n </div>\n </div>\n )\n\n const placement = editing && art && (\n <div style={S.place} role=\"group\" aria-label=\"Placement\">\n <div style={S.placeHead}>\n <strong>Placement</strong>\n <span style={S.hint}>\n Drag it on the avatar. The top handle turns it, the corner resizes it.\n </span>\n </div>\n {SLIDERS.map(([k, label, unit]) => (\n <label key={k} style={S.slider}>\n <span>{label}</span>\n <input\n type=\"range\"\n min={PLACEMENT_LIMITS[k][0]}\n max={PLACEMENT_LIMITS[k][1]}\n value={live[k]}\n onChange={e => place({ ...live, [k]: Number(e.currentTarget.value) }, 'later')}\n style={{ width: '100%', accentColor: 'var(--color-primary)' }}\n />\n <output style={S.out}>\n {live[k]}\n {unit}\n </output>\n </label>\n ))}\n <div style={S.toggles}>\n <label>\n <input\n type=\"checkbox\"\n checked={live.flip}\n onChange={e => place({ ...live, flip: e.currentTarget.checked }, 'now')}\n />{' '}\n Mirror\n </label>\n <label>\n <input\n type=\"checkbox\"\n checked={live.back}\n onChange={e => place({ ...live, back: e.currentTarget.checked }, 'now')}\n />{' '}\n Behind\n </label>\n <button\n type=\"button\"\n className=\"settings-popout__btn\"\n onClick={() => place(PLACEMENT_DEFAULT, 'now')}\n >\n Reset\n </button>\n </div>\n </div>\n )\n\n const tabs: Tab[] = ['picture', ...COSMETIC_SLOTS]\n const tabLabel = (t: Tab) => (t === 'picture' ? 'Picture' : SLOT_LABEL[t])\n\n return (\n <div\n ref={root}\n style={{ ...S.body, gridTemplateColumns: narrow ? 'minmax(0, 1fr)' : '240px minmax(0, 1fr)' }}\n >\n <div style={S.side}>\n {stage}\n {chat}\n {placement}\n </div>\n <div style={{ minWidth: 0 }}>\n <div ref={tabStrip} style={S.tabs} role=\"tablist\" aria-label=\"Avatar\">\n {tabs.map(t => {\n return (\n <button\n key={t}\n type=\"button\"\n role=\"tab\"\n aria-selected={tab === t}\n style={{ ...S.tab, ...(tab === t ? S.tabOn : {}) }}\n onClick={e => {\n setTab(t)\n setTryOn(null)\n // Where the strip overflows (narrow screens), a tab in its\n // right half shifts the strip to the end and one in its left\n // half to the start, so the tabs past the chosen one show.\n const strip = e.currentTarget.parentElement\n if (strip && strip.scrollWidth > strip.clientWidth) {\n const right = tabs.indexOf(t) >= tabs.length / 2\n strip.scrollTo({\n left: right ? strip.scrollWidth - strip.clientWidth : 0,\n behavior: 'smooth'\n })\n }\n }}\n >\n {tabLabel(t)}\n </button>\n )\n })}\n </div>\n <div style={S.pane} role=\"tabpanel\" aria-label={tabLabel(tab)}>\n {tab === 'picture' ? (\n <AvatarPicker settings={settings} sets={sets} busy={busy} run={run} />\n ) : (\n <SlotGrid\n slot={tab}\n owned={owned}\n equipped={loadout?.equipped[tab] ?? null}\n tryOn={tryOn}\n showLocked={showLocked}\n onShowLocked={setShowLocked}\n isAdmin={isAdmin}\n preview={id => {\n const only: AvatarCosmetics = { [tab]: id ?? undefined }\n if (tab === 'overlay') return avatarHtml({ src: picture, size: 40, worn: only })\n if (tab === 'message') return messageHtml('gg, rematch?', only, { keep: true })\n // A name tile shows the item on top of what is worn in the other name slots.\n return nameHtml(displayName, { ...worn, [tab]: id ?? undefined })\n }}\n colour={colour}\n onEquip={id => equip(tab, id)}\n onTry={id =>\n setTryOn(t => (t?.slot === tab && t.id === id ? null : { slot: tab, id }))\n }\n />\n )}\n {error && (\n <span className=\"settings-popout__error\" role=\"alert\">\n {error}\n </span>\n )}\n </div>\n </div>\n </div>\n )\n}\n\n// ---------------------------------------------------------------------------\n\ninterface StageProps {\n html: string\n editing: boolean\n overlayId: string | null\n live: Placement\n onPlace: (p: Partial<Placement>, save: 'later' | 'now' | 'no') => void\n dragging: React.MutableRefObject<boolean>\n style: React.CSSProperties\n banner: React.ReactNode\n}\n\n/** The big preview, and the placement handles drawn on its overlay while editing. */\nfunction Stage({ html, editing, overlayId, live, onPlace, dragging, style, banner }: StageProps) {\n const box = useRef<HTMLDivElement>(null)\n const drag = useRef<{\n mode: string\n p0: Placement\n x0: number\n y0: number\n cx: number\n cy: number\n a0: number\n d0: number\n size: number\n last: Placement\n } | null>(null)\n\n // The handles live inside the overlay's placement box, so they turn and\n // scale with it. Re-added whenever the preview is redrawn.\n useEffect(() => {\n const adj = box.current?.querySelector('.hc-art .hc-adj')\n if (!editing || !adj) return\n const gz = document.createElement('span')\n gz.dataset.g = 'move'\n gz.setAttribute('aria-hidden', 'true')\n gz.style.cssText =\n 'position:absolute;inset:-3px;border:1.5px dashed var(--color-primary);border-radius:6px;pointer-events:auto;cursor:move'\n const handle = (g: string, css: string) => {\n const h = document.createElement('i')\n h.dataset.g = g\n h.style.cssText = `position:absolute;pointer-events:auto;${css}`\n gz.appendChild(h)\n }\n handle(\n 'rot',\n 'left:50%;top:-20px;width:12px;height:12px;margin-left:-6px;border-radius:50%;background:var(--color-primary);cursor:grab'\n )\n handle(\n 'scale',\n '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'\n )\n adj.appendChild(gz)\n return () => gz.remove()\n }, [html, editing])\n\n const down = (e: React.PointerEvent) => {\n const g = (e.target as HTMLElement).closest<HTMLElement>('[data-g]')\n const adj = g?.closest('.hc-adj')\n const avatar = box.current?.querySelector('.hc-av')\n if (!g || !adj || !avatar || !editing || !overlayId) return\n e.preventDefault()\n g.setPointerCapture(e.pointerId)\n const r = adj.getBoundingClientRect()\n const cx = r.left + r.width / 2\n const cy = r.top + r.height / 2\n const size = avatar.getBoundingClientRect().width\n dragging.current = true\n drag.current = {\n mode: g.dataset.g ?? 'move',\n p0: live,\n x0: e.clientX,\n y0: e.clientY,\n cx,\n cy,\n a0: Math.atan2(e.clientY - cy, e.clientX - cx),\n d0: Math.hypot(e.clientX - cx, e.clientY - cy) || 1,\n size,\n last: live\n }\n }\n const move = (e: React.PointerEvent) => {\n const d = drag.current\n if (!d) return\n let next: Partial<Placement> = d.p0\n if (d.mode === 'move') {\n next = {\n ...d.p0,\n x: d.p0.x + ((e.clientX - d.x0) / d.size) * 100,\n y: d.p0.y + ((e.clientY - d.y0) / d.size) * 100\n }\n } else if (d.mode === 'rot') {\n const a = Math.atan2(e.clientY - d.cy, e.clientX - d.cx)\n next = { ...d.p0, r: d.p0.r + ((a - d.a0) * 180) / Math.PI }\n } else {\n next = { ...d.p0, s: (d.p0.s * Math.hypot(e.clientX - d.cx, e.clientY - d.cy)) / d.d0 }\n }\n d.last = next as Placement\n onPlace(next, 'no')\n }\n const up = () => {\n const d = drag.current\n if (!d) return\n drag.current = null\n dragging.current = false\n onPlace(d.last, 'now')\n }\n\n return (\n <div\n ref={box}\n role=\"group\"\n aria-label=\"Preview\"\n style={{ ...S.stage, ...style }}\n onPointerDown={down}\n onPointerMove={move}\n onPointerUp={up}\n onPointerCancel={up}\n >\n {banner}\n <Html html={html} style={{ display: 'contents' }} />\n </div>\n )\n}\n\n// ---------------------------------------------------------------------------\n\ninterface SlotGridProps {\n slot: CosmeticSlot\n owned: ReadonlySet<string>\n equipped: string | null\n tryOn: { slot: CosmeticSlot; id: string } | null\n showLocked: boolean\n onShowLocked: (on: boolean) => void\n /** Admins see archived items, greyed and not selectable. */\n isAdmin: boolean\n preview: (id: string | null) => string\n colour: React.CSSProperties\n onEquip: (id: string | null) => void\n onTry: (id: string) => void\n}\n\n/**\n * One slot's items: None, then the shown items, free or earned. Archived items\n * show only to admins, greyed and not selectable; nobody can wear one.\n */\nfunction SlotGrid({\n slot,\n owned,\n equipped,\n tryOn,\n showLocked,\n onShowLocked,\n isAdmin,\n preview,\n colour,\n onEquip,\n onTry\n}: SlotGridProps) {\n const items = COSMETICS.filter(i => i.slot === slot && (!i.hidden || isAdmin))\n const locked = items.filter(i => !i.hidden && !owned.has(i.id))\n const shown = showLocked ? items : items.filter(i => i.hidden || owned.has(i.id))\n\n return (\n <>\n <div style={S.grid}>\n <button\n type=\"button\"\n aria-pressed={equipped === null}\n style={{ ...S.tile, ...(equipped === null ? S.tileOn : {}) }}\n onClick={() => onEquip(null)}\n >\n <Html html={preview(null)} style={{ ...S.tilePv, ...colour }} />\n <span style={S.tileName}>None</span>\n </button>\n {shown.map(item => {\n if (item.hidden) {\n return (\n <button\n key={item.id}\n type=\"button\"\n disabled\n aria-label={`${item.label}, archived`}\n style={{ ...S.tile, cursor: 'not-allowed', opacity: 0.45 }}\n >\n <Html html={preview(item.id)} style={{ ...S.tilePv, ...colour }} />\n <span style={S.tileName}>{item.label}</span>\n <span style={S.tileEarn}>Archived</span>\n </button>\n )\n }\n const own = owned.has(item.id)\n const on = equipped === item.id\n const trying = tryOn?.slot === slot && tryOn.id === item.id\n return (\n <button\n key={item.id}\n type=\"button\"\n aria-pressed={own ? on : trying}\n aria-label={own ? item.label : `${item.label}, locked. ${item.earn ?? ''}. Try it on.`}\n style={{\n ...S.tile,\n ...(on ? S.tileOn : {}),\n ...(trying ? S.tileTrying : {})\n }}\n onClick={() => (own ? onEquip(item.id) : onTry(item.id))}\n // A message reveal plays once; hovering its tile plays it again.\n onMouseEnter={e => slot === 'message' && replayPreview(e.currentTarget)}\n >\n <Html\n html={preview(item.id)}\n style={{ ...S.tilePv, ...colour, opacity: own ? 1 : 0.55 }}\n />\n <span style={{ ...S.tileName, color: own ? undefined : 'var(--color-text-muted)' }}>\n {!own && <Icon name=\"lock\" size={12} />}\n {item.label}\n </span>\n {!own && <span style={S.tileEarn}>{item.earn}</span>}\n </button>\n )\n })}\n </div>\n {locked.length > 0 && (\n <div style={S.foot}>\n <span>\n {locked.length === 1 ? 'One to earn.' : `${locked.length} to earn.`} Pick one to try it\n on.\n </span>\n <label>\n <input\n type=\"checkbox\"\n checked={showLocked}\n onChange={e => onShowLocked(e.currentTarget.checked)}\n />{' '}\n Show locked\n </label>\n </div>\n )}\n </>\n )\n}\n"],"names":["Html","live"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"AvatarBuilder.js","sources":["../../src/components/AvatarBuilder.tsx"],"sourcesContent":["/**\n * AvatarBuilder — the body of AvatarDialog: the person's avatar and what they\n * wear with it, with a live preview.\n *\n * Layout (operator's pick, 2026-11-01): a preview column on the left (the\n * avatar large, with the name, and a chat line as others see it), and on the\n * right tabs for the Picture (AvatarPicker) and each cosmetic slot. Narrow\n * screens stack the two.\n *\n * Cosmetics (`@wolffm/prefs-client/cosmetics`, bundled at build like the avatar\n * client): what the person owns equips on click (every shown item is free but\n * three earned in a Major); an earned item not yet earned shows how to earn\n * it and can be tried on in the preview, which saves nothing. An overlay\n * image can be placed: drag it on the big preview, turn it with the top handle,\n * resize it with the corner handle, or use the sliders. Every move goes through\n * `fitPlacement`, the same function prefs-api runs before it stores one, so the\n * preview never shows a spot the server would refuse.\n *\n * Styles are inline for the reason AvatarPicker's are: apps compile this\n * package's CSS at their own build. Tokens only; the cosmetics' own colours come\n * from the person's colour, their picture's average or else their character's\n * (and, for overlay images, the art).\n */\nimport React, {\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n useSyncExternalStore\n} from 'react'\nimport { Icon } from '@wolffm/themes'\nimport type { AvatarSettings, LoadoutUpdate } from '@wolffm/prefs-client/avatar'\nimport {\n COSMETICS,\n COSMETICS_BY_ID,\n COSMETIC_SLOTS,\n PLACEMENT_DEFAULT,\n PLACEMENT_LIMITS,\n avatarHtml,\n cleanLoadout,\n ensureCosmeticsStyles,\n fitPlacement,\n messageHtml,\n nameHtml,\n onPictureColour,\n pictureColour,\n placementTransform,\n readPictureColour,\n wornCosmetics,\n type AvatarCosmetics,\n type CosmeticSlot,\n type Loadout,\n type OverlayArt,\n type Placement\n} from '@wolffm/prefs-client/cosmetics'\nimport { AvatarPicker } from './AvatarPicker'\nimport { AVATAR_EVENT } from '../lib/avatarEvent'\nimport {\n avatarClient,\n avatarErrorText,\n fetchCharacters,\n portraitUrl,\n type CharacterSet\n} from '../lib/avatarData'\n\ntype Tab = 'picture' | CosmeticSlot\n\nconst SLOT_LABEL: Record<CosmeticSlot, string> = {\n effect: 'Effect',\n gradient: 'Gradient',\n font: 'Font',\n overlay: 'Overlay',\n message: 'Message'\n}\n\nconst SLIDERS = [\n ['x', 'Left/right', '%'],\n ['y', 'Up/down', '%'],\n ['r', 'Rotate', '°'],\n ['s', 'Size', '%']\n] as const\n\nconst S = {\n body: { display: 'grid', gap: 16, alignItems: 'start' },\n side: { display: 'flex', flexDirection: 'column', gap: 12, minWidth: 0 },\n stage: {\n position: 'relative',\n display: 'flex',\n flexDirection: 'column',\n alignItems: 'center',\n gap: 12,\n padding: '48px 12px 18px',\n borderRadius: 10,\n background: 'var(--color-bg-alt)',\n border: '1px solid var(--color-border)',\n touchAction: 'none',\n userSelect: 'none'\n },\n tryOn: {\n position: 'absolute',\n left: 8,\n right: 8,\n top: 8,\n display: 'flex',\n alignItems: 'center',\n gap: 6,\n padding: '4px 8px',\n borderRadius: 6,\n background: 'var(--color-primary-bg)',\n color: 'var(--color-on-primary-bg)',\n fontSize: '0.72rem'\n },\n linkBtn: {\n border: 0,\n background: 'none',\n color: 'inherit',\n textDecoration: 'underline',\n cursor: 'pointer',\n fontSize: 'inherit',\n padding: 0,\n flex: 'none'\n },\n chat: {\n display: 'flex',\n flexDirection: 'column',\n gap: 10,\n padding: 12,\n borderRadius: 10,\n background: 'var(--color-bg)',\n border: '1px solid var(--color-border)'\n },\n chatHead: {\n display: 'flex',\n justifyContent: 'space-between',\n alignItems: 'center',\n fontSize: '0.72rem',\n color: 'var(--color-text-muted)'\n },\n msg: { display: 'flex', gap: 10, alignItems: 'flex-start' },\n who: { minHeight: 22, display: 'flex', alignItems: 'center' },\n line: { fontSize: '0.85rem', marginTop: 2 },\n place: {\n display: 'flex',\n flexDirection: 'column',\n gap: 8,\n padding: 12,\n borderRadius: 10,\n border: '1px solid var(--color-border)'\n },\n placeHead: { display: 'flex', flexDirection: 'column', gap: 2, fontSize: '0.8rem' },\n hint: { fontSize: '0.72rem', color: 'var(--color-text-muted)' },\n slider: {\n display: 'grid',\n gridTemplateColumns: '64px 1fr 40px',\n alignItems: 'center',\n gap: 6,\n fontSize: '0.75rem',\n color: 'var(--color-text-muted)'\n },\n out: { textAlign: 'right', color: 'var(--color-text)', fontVariantNumeric: 'tabular-nums' },\n toggles: {\n display: 'flex',\n flexWrap: 'wrap',\n gap: 10,\n alignItems: 'center',\n fontSize: '0.75rem',\n color: 'var(--color-text-muted)'\n },\n tabs: {\n display: 'flex',\n gap: 2,\n cursor: 'grab',\n touchAction: 'pan-x',\n borderBottom: '1px solid var(--color-border)',\n overflowX: 'auto',\n scrollbarWidth: 'none'\n },\n tab: {\n flex: 'none',\n padding: '7px 11px',\n borderBottomWidth: 2,\n borderBottomStyle: 'solid',\n borderBottomColor: 'transparent',\n borderTop: 0,\n borderLeft: 0,\n borderRight: 0,\n background: 'none',\n cursor: 'pointer',\n fontSize: '0.82rem',\n fontWeight: 500,\n color: 'var(--color-text-muted)'\n },\n tabOn: { color: 'var(--color-text)', borderBottomColor: 'var(--color-primary)' },\n pane: { display: 'flex', flexDirection: 'column', gap: 10, paddingTop: 10 },\n grid: { display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(112px, 1fr))', gap: 8 },\n tile: {\n position: 'relative',\n display: 'flex',\n flexDirection: 'column',\n gap: 5,\n padding: 6,\n borderRadius: 8,\n border: '1px solid var(--color-border)',\n background: 'var(--color-bg)',\n color: 'var(--color-text)',\n cursor: 'pointer',\n textAlign: 'left',\n font: 'inherit'\n },\n tileOn: {\n borderColor: 'var(--color-primary)',\n boxShadow: 'inset 0 0 0 1px var(--color-primary)'\n },\n tileTrying: { borderStyle: 'dashed', borderColor: 'var(--color-primary)' },\n tilePv: {\n height: 58,\n borderRadius: 6,\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center',\n background: 'var(--color-bg-alt)',\n overflow: 'hidden',\n padding: '0 8px',\n fontSize: '0.8rem'\n },\n tileName: { fontSize: '0.8rem', fontWeight: 500, display: 'flex', alignItems: 'center', gap: 5 },\n tileEarn: { fontSize: '0.7rem', lineHeight: 1.3, color: 'var(--color-text-muted)' },\n foot: {\n display: 'flex',\n justifyContent: 'space-between',\n alignItems: 'center',\n gap: 8,\n flexWrap: 'wrap',\n fontSize: '0.75rem',\n color: 'var(--color-text-muted)'\n }\n} satisfies Record<string, React.CSSProperties>\n\n/**\n * Trusted HTML from the cosmetics renderer, which escapes every string it is\n * given. Memoised on the string and the style: React re-sets innerHTML on every\n * render even when the string is the same, and replacing the nodes under the\n * pointer between mousedown and mouseup swallows the click.\n */\nconst Html = React.memo(\n function Html({ html, style }: { html: string; style?: React.CSSProperties }) {\n return <span style={style} dangerouslySetInnerHTML={{ __html: html }} />\n },\n (a, b) => a.html === b.html && JSON.stringify(a.style) === JSON.stringify(b.style)\n)\n\n/**\n * Play a tile's message reveal again. A finished reveal holds no animation (it\n * fills backwards only), so its markup is re-set. This runs synchronously in\n * mouseenter, before any press, so it cannot split a click; the memo above keeps\n * React from touching it afterwards.\n */\nfunction replayPreview(tile: HTMLElement) {\n const pv = tile.firstElementChild\n if (!pv) return\n const markup = pv.innerHTML\n pv.innerHTML = markup\n}\n\nfunction useNarrow(): boolean {\n const query = '(max-width: 640px)'\n const [narrow, setNarrow] = useState(() => window.matchMedia(query).matches)\n useEffect(() => {\n const mq = window.matchMedia(query)\n const on = () => setNarrow(mq.matches)\n mq.addEventListener('change', on)\n return () => mq.removeEventListener('change', on)\n }, [])\n return narrow\n}\n\n/** Move every drawing of one overlay in `root` to `p`, without re-rendering (smooth while dragging). */\nfunction applyPlacement(root: HTMLElement | null, id: string, p: Placement) {\n root?.querySelectorAll<HTMLElement>(`.hc-art[data-hc-art=\"${id}\"]`).forEach(ol => {\n ol.style.zIndex = p.back ? '0' : '3'\n const adj = ol.firstElementChild as HTMLElement | null\n if (adj) adj.style.transform = placementTransform(p)\n })\n}\n\n/** Settings with a new loadout, and what is worn recomputed from it. */\nfunction withLoadout(s: AvatarSettings, loadout: Loadout): AvatarSettings {\n return {\n ...s,\n cosmetics: { ...s.cosmetics, loadout },\n avatar: { ...s.avatar, cosmetics: wornCosmetics(loadout) }\n }\n}\n\n/**\n * A row that overflows sideways scrolls by dragging it (the operator's ask: the\n * tab strip is wider than its column and hides its scrollbar) and by the\n * mouse wheel. A drag of more than a few pixels is not a click.\n */\nfunction useDragScroll<T extends HTMLElement>() {\n // A callback ref, not useRef: the row appears only once the settings load,\n // after the first render, and the listeners must attach when it does.\n const [el, setEl] = useState<T | null>(null)\n useEffect(() => {\n if (!el) return\n let start: { x: number; left: number } | null = null\n let dragged = false\n const down = (e: PointerEvent) => {\n if (e.button !== 0) return\n start = { x: e.clientX, left: el.scrollLeft }\n dragged = false\n }\n const move = (e: PointerEvent) => {\n if (!start) return\n const dx = e.clientX - start.x\n if (!dragged && Math.abs(dx) > 4) {\n dragged = true\n el.setPointerCapture(e.pointerId)\n el.style.cursor = 'grabbing'\n }\n if (dragged) el.scrollLeft = start.left - dx\n }\n const up = () => {\n start = null\n el.style.cursor = ''\n }\n // The click that ends a drag is not a tab choice.\n const click = (e: MouseEvent) => {\n if (!dragged) return\n e.preventDefault()\n e.stopPropagation()\n dragged = false\n }\n const wheel = (e: globalThis.WheelEvent) => {\n if (el.scrollWidth <= el.clientWidth || Math.abs(e.deltaX) > Math.abs(e.deltaY)) return\n e.preventDefault()\n el.scrollLeft += e.deltaY\n }\n el.addEventListener('pointerdown', down)\n el.addEventListener('pointermove', move)\n el.addEventListener('pointerup', up)\n el.addEventListener('pointercancel', up)\n el.addEventListener('click', click, true)\n el.addEventListener('wheel', wheel, { passive: false })\n return () => {\n el.removeEventListener('pointerdown', down)\n el.removeEventListener('pointermove', move)\n el.removeEventListener('pointerup', up)\n el.removeEventListener('pointercancel', up)\n el.removeEventListener('click', click, true)\n el.removeEventListener('wheel', wheel)\n }\n }, [el])\n return setEl\n}\n\n/**\n * The colour a picture gives a person's name: its average, read once\n * (@wolffm/prefs-client, the same reader watchparty's chat uses). Null for no\n * picture, or while it is unread or unreadable.\n */\nfunction usePictureColour(url: string | null): string | null {\n useEffect(() => {\n if (url) void readPictureColour(url)\n }, [url])\n return useSyncExternalStore(\n onPictureColour,\n () => (url ? (pictureColour(url) ?? null) : null),\n () => null\n )\n}\n\nexport interface AvatarBuilderProps {\n /** The person's display name, for the preview. */\n name: string | null\n /** Admins also see archived items (greyed; nobody can select one). */\n isAdmin?: boolean\n}\n\nexport function AvatarBuilder({ name, isAdmin = false }: AvatarBuilderProps) {\n const [settings, setSettings] = useState<AvatarSettings | null>(null)\n const [sets, setSets] = useState<CharacterSet[] | null>(null)\n const [busy, setBusy] = useState(false)\n const [error, setError] = useState<string | null>(null)\n const [tab, setTab] = useState<Tab>('picture')\n const [tryOn, setTryOn] = useState<{ slot: CosmeticSlot; id: string } | null>(null)\n const [replay, setReplay] = useState(0)\n const narrow = useNarrow()\n const root = useRef<HTMLDivElement>(null)\n const tabStrip = useDragScroll<HTMLDivElement>()\n // Only the newest cosmetics request may set state: equips sent quickly in a\n // row can answer out of order.\n const seq = useRef(0)\n\n useEffect(() => ensureCosmeticsStyles(), [])\n // A Discord, Steam or uploaded picture colours the name; a character, its own colour.\n const tint = usePictureColour(settings?.avatar.url ?? null)\n\n useEffect(() => {\n let live = true\n avatarClient\n .get()\n .then(s => live && setSettings(s))\n .catch((err: unknown) => live && setError(avatarErrorText(err)))\n fetchCharacters()\n .then(s => live && setSets(s))\n .catch((err: unknown) => live && setError(avatarErrorText(err)))\n return () => {\n live = false\n }\n }, [])\n\n const announce = (next: AvatarSettings) =>\n // The page under the popout (a watchparty room, a game) re-reads the avatar\n // from this instead of waiting for its next join.\n window.dispatchEvent(new window.CustomEvent<AvatarSettings>(AVATAR_EVENT, { detail: next }))\n\n const run = useCallback(async (work: () => Promise<AvatarSettings>) => {\n setBusy(true)\n setError(null)\n try {\n const next = await work()\n setSettings(next)\n announce(next)\n } catch (err) {\n setError(avatarErrorText(err))\n } finally {\n setBusy(false)\n }\n }, [])\n\n /** Save a cosmetics change, showing `optimistic` at once. A failure re-reads the truth. */\n const wear = useCallback(\n async (update: LoadoutUpdate, optimistic: (s: AvatarSettings) => AvatarSettings) => {\n const mine = ++seq.current\n setError(null)\n setSettings(s => (s ? optimistic(s) : s))\n try {\n const next = await avatarClient.setCosmetics(update)\n if (mine !== seq.current) return\n setSettings(next)\n announce(next)\n } catch (err) {\n if (mine !== seq.current) return\n setError(avatarErrorText(err))\n avatarClient\n .get()\n .then(s => mine === seq.current && setSettings(s))\n .catch((e: unknown) => setError(avatarErrorText(e)))\n }\n },\n []\n )\n\n const owned = useMemo(() => new Set(settings?.cosmetics.owned ?? []), [settings])\n const loadout = settings?.cosmetics.loadout\n const equippedOverlay = loadout?.equipped.overlay ?? null\n const art: OverlayArt | undefined = equippedOverlay\n ? COSMETICS_BY_ID.get(equippedOverlay)?.art\n : undefined\n const editing = tab === 'overlay' && !tryOn && !!art && !!equippedOverlay\n\n // The placement being edited, live; saved a beat after the last change.\n const [live, setLive] = useState<Placement>(PLACEMENT_DEFAULT)\n const saveTimer = useRef<ReturnType<typeof setTimeout>>(undefined)\n const dragging = useRef(false)\n useEffect(() => {\n if (dragging.current || !equippedOverlay) return\n setLive(loadout?.placement[equippedOverlay] ?? PLACEMENT_DEFAULT)\n }, [equippedOverlay, loadout])\n useEffect(() => () => clearTimeout(saveTimer.current), [])\n\n const place = useCallback(\n (raw: Partial<Placement>, save: 'later' | 'now' | 'no') => {\n if (!art || !equippedOverlay) return\n const p = fitPlacement(art, raw)\n setLive(p)\n applyPlacement(root.current, equippedOverlay, p)\n clearTimeout(saveTimer.current)\n if (save === 'no') return\n const commit = () =>\n void wear({ placement: { [equippedOverlay]: p } }, s =>\n withLoadout(s, {\n ...s.cosmetics.loadout,\n placement: { ...s.cosmetics.loadout.placement, [equippedOverlay]: p }\n })\n )\n if (save === 'now') commit()\n else saveTimer.current = setTimeout(commit, 400)\n },\n [art, equippedOverlay, wear]\n )\n\n if (!settings) {\n return (\n <span className=\"settings-popout__hint\" role={error ? 'alert' : undefined}>\n {error ?? '…'}\n </span>\n )\n }\n\n const characters = new Map((sets ?? []).flatMap(s => s.characters).map(c => [c.id, c]))\n const character = settings.sprite ? characters.get(settings.sprite) : undefined\n const picture = settings.avatar.url ?? (character ? portraitUrl(character) : null)\n // Every name cosmetic derives its colours from the person's: the picture's\n // average, else the character's, as the chat draws them.\n const base =\n tint ?? (character && /^#[0-9a-f]{6}$/i.test(character.color) ? character.color : null)\n const colour: React.CSSProperties = base ? ({ '--hc-base': base } as React.CSSProperties) : {}\n const displayName = name || 'You'\n\n const worn: AvatarCosmetics = { ...(settings.avatar.cosmetics ?? {}) }\n if (tryOn) {\n worn[tryOn.slot] = tryOn.id\n if (tryOn.slot === 'overlay') delete worn.placement\n }\n\n const equip = (slot: CosmeticSlot, id: string | null) => {\n setTryOn(null)\n void wear({ equip: { [slot]: id } }, s => {\n const l = s.cosmetics.loadout\n return withLoadout(s, cleanLoadout({ ...l, equipped: { ...l.equipped, [slot]: id } }, owned))\n })\n }\n\n const stage = (\n <Stage\n html={\n avatarHtml({ src: picture, size: 112, worn }) + nameHtml(displayName, worn, { large: true })\n }\n editing={editing}\n overlayId={equippedOverlay}\n live={live}\n onPlace={place}\n dragging={dragging}\n style={colour}\n banner={\n tryOn && (\n <div style={S.tryOn}>\n <Icon name=\"lock\" size={12} />\n <span style={{ flex: 1, minWidth: 0 }}>\n Trying on {COSMETICS_BY_ID.get(tryOn.id)?.label} ·{' '}\n {COSMETICS_BY_ID.get(tryOn.id)?.earn}\n </span>\n <button type=\"button\" style={S.linkBtn} onClick={() => setTryOn(null)}>\n Take off\n </button>\n </div>\n )\n }\n />\n )\n\n const chat = (\n <div style={S.chat}>\n <div style={S.chatHead}>\n <span>In chat</span>\n <button\n type=\"button\"\n className=\"settings-popout__btn\"\n onClick={() => setReplay(n => n + 1)}\n >\n Replay\n </button>\n </div>\n <div style={S.msg}>\n <Html html={avatarHtml({ src: picture, size: 36, worn })} style={colour} />\n <div style={{ minWidth: 0, ...colour }}>\n <div style={S.who}>\n <Html html={nameHtml(displayName, worn)} />\n </div>\n <div style={S.line}>\n <Html key={replay} html={messageHtml('gg, that last hand was brutal', worn)} />\n </div>\n </div>\n </div>\n </div>\n )\n\n const placement = editing && art && (\n <div style={S.place} role=\"group\" aria-label=\"Placement\">\n <div style={S.placeHead}>\n <strong>Placement</strong>\n <span style={S.hint}>\n Drag it on the avatar. The top handle turns it, the corner resizes it.\n </span>\n </div>\n {SLIDERS.map(([k, label, unit]) => (\n <label key={k} style={S.slider}>\n <span>{label}</span>\n <input\n type=\"range\"\n min={PLACEMENT_LIMITS[k][0]}\n max={PLACEMENT_LIMITS[k][1]}\n value={live[k]}\n onChange={e => place({ ...live, [k]: Number(e.currentTarget.value) }, 'later')}\n style={{ width: '100%', accentColor: 'var(--color-primary)' }}\n />\n <output style={S.out}>\n {live[k]}\n {unit}\n </output>\n </label>\n ))}\n <div style={S.toggles}>\n <label>\n <input\n type=\"checkbox\"\n checked={live.flip}\n onChange={e => place({ ...live, flip: e.currentTarget.checked }, 'now')}\n />{' '}\n Mirror\n </label>\n <label>\n <input\n type=\"checkbox\"\n checked={live.back}\n onChange={e => place({ ...live, back: e.currentTarget.checked }, 'now')}\n />{' '}\n Behind\n </label>\n <button\n type=\"button\"\n className=\"settings-popout__btn\"\n onClick={() => place(PLACEMENT_DEFAULT, 'now')}\n >\n Reset\n </button>\n </div>\n </div>\n )\n\n const tabs: Tab[] = ['picture', ...COSMETIC_SLOTS]\n const tabLabel = (t: Tab) => (t === 'picture' ? 'Picture' : SLOT_LABEL[t])\n\n return (\n <div\n ref={root}\n style={{ ...S.body, gridTemplateColumns: narrow ? 'minmax(0, 1fr)' : '240px minmax(0, 1fr)' }}\n >\n <div style={S.side}>\n {stage}\n {chat}\n {placement}\n </div>\n <div style={{ minWidth: 0 }}>\n <div ref={tabStrip} style={S.tabs} role=\"tablist\" aria-label=\"Avatar\">\n {tabs.map(t => {\n return (\n <button\n key={t}\n type=\"button\"\n role=\"tab\"\n aria-selected={tab === t}\n style={{ ...S.tab, ...(tab === t ? S.tabOn : {}) }}\n onClick={e => {\n setTab(t)\n setTryOn(null)\n // Where the strip overflows (narrow screens), a tab in its\n // right half shifts the strip to the end and one in its left\n // half to the start, so the tabs past the chosen one show.\n const strip = e.currentTarget.parentElement\n if (strip && strip.scrollWidth > strip.clientWidth) {\n const right = tabs.indexOf(t) >= tabs.length / 2\n strip.scrollTo({\n left: right ? strip.scrollWidth - strip.clientWidth : 0,\n behavior: 'smooth'\n })\n }\n }}\n >\n {tabLabel(t)}\n </button>\n )\n })}\n </div>\n <div style={S.pane} role=\"tabpanel\" aria-label={tabLabel(tab)}>\n {tab === 'picture' ? (\n <AvatarPicker settings={settings} sets={sets} busy={busy} run={run} />\n ) : (\n <SlotGrid\n slot={tab}\n owned={owned}\n equipped={loadout?.equipped[tab] ?? null}\n tryOn={tryOn}\n isAdmin={isAdmin}\n preview={id => {\n const only: AvatarCosmetics = { [tab]: id ?? undefined }\n if (tab === 'overlay') return avatarHtml({ src: picture, size: 40, worn: only })\n if (tab === 'message') return messageHtml('gg, rematch?', only, { keep: true })\n // A name tile shows the item on top of what is worn in the other name slots.\n return nameHtml(displayName, { ...worn, [tab]: id ?? undefined })\n }}\n colour={colour}\n onEquip={id => equip(tab, id)}\n onTry={id =>\n setTryOn(t => (t?.slot === tab && t.id === id ? null : { slot: tab, id }))\n }\n />\n )}\n {error && (\n <span className=\"settings-popout__error\" role=\"alert\">\n {error}\n </span>\n )}\n </div>\n </div>\n </div>\n )\n}\n\n// ---------------------------------------------------------------------------\n\ninterface StageProps {\n html: string\n editing: boolean\n overlayId: string | null\n live: Placement\n onPlace: (p: Partial<Placement>, save: 'later' | 'now' | 'no') => void\n dragging: React.MutableRefObject<boolean>\n style: React.CSSProperties\n banner: React.ReactNode\n}\n\n/** The big preview, and the placement handles drawn on its overlay while editing. */\nfunction Stage({ html, editing, overlayId, live, onPlace, dragging, style, banner }: StageProps) {\n const box = useRef<HTMLDivElement>(null)\n const drag = useRef<{\n mode: string\n p0: Placement\n x0: number\n y0: number\n cx: number\n cy: number\n a0: number\n d0: number\n size: number\n last: Placement\n } | null>(null)\n\n // The handles live inside the overlay's placement box, so they turn and\n // scale with it. Re-added whenever the preview is redrawn.\n useEffect(() => {\n const adj = box.current?.querySelector('.hc-art .hc-adj')\n if (!editing || !adj) return\n const gz = document.createElement('span')\n gz.dataset.g = 'move'\n gz.setAttribute('aria-hidden', 'true')\n gz.style.cssText =\n 'position:absolute;inset:-3px;border:1.5px dashed var(--color-primary);border-radius:6px;pointer-events:auto;cursor:move'\n const handle = (g: string, css: string) => {\n const h = document.createElement('i')\n h.dataset.g = g\n h.style.cssText = `position:absolute;pointer-events:auto;${css}`\n gz.appendChild(h)\n }\n handle(\n 'rot',\n 'left:50%;top:-20px;width:12px;height:12px;margin-left:-6px;border-radius:50%;background:var(--color-primary);cursor:grab'\n )\n handle(\n 'scale',\n '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'\n )\n adj.appendChild(gz)\n return () => gz.remove()\n }, [html, editing])\n\n const down = (e: React.PointerEvent) => {\n const g = (e.target as HTMLElement).closest<HTMLElement>('[data-g]')\n const adj = g?.closest('.hc-adj')\n const avatar = box.current?.querySelector('.hc-av')\n if (!g || !adj || !avatar || !editing || !overlayId) return\n e.preventDefault()\n g.setPointerCapture(e.pointerId)\n const r = adj.getBoundingClientRect()\n const cx = r.left + r.width / 2\n const cy = r.top + r.height / 2\n const size = avatar.getBoundingClientRect().width\n dragging.current = true\n drag.current = {\n mode: g.dataset.g ?? 'move',\n p0: live,\n x0: e.clientX,\n y0: e.clientY,\n cx,\n cy,\n a0: Math.atan2(e.clientY - cy, e.clientX - cx),\n d0: Math.hypot(e.clientX - cx, e.clientY - cy) || 1,\n size,\n last: live\n }\n }\n const move = (e: React.PointerEvent) => {\n const d = drag.current\n if (!d) return\n let next: Partial<Placement> = d.p0\n if (d.mode === 'move') {\n next = {\n ...d.p0,\n x: d.p0.x + ((e.clientX - d.x0) / d.size) * 100,\n y: d.p0.y + ((e.clientY - d.y0) / d.size) * 100\n }\n } else if (d.mode === 'rot') {\n const a = Math.atan2(e.clientY - d.cy, e.clientX - d.cx)\n next = { ...d.p0, r: d.p0.r + ((a - d.a0) * 180) / Math.PI }\n } else {\n next = { ...d.p0, s: (d.p0.s * Math.hypot(e.clientX - d.cx, e.clientY - d.cy)) / d.d0 }\n }\n d.last = next as Placement\n onPlace(next, 'no')\n }\n const up = () => {\n const d = drag.current\n if (!d) return\n drag.current = null\n dragging.current = false\n onPlace(d.last, 'now')\n }\n\n return (\n <div\n ref={box}\n role=\"group\"\n aria-label=\"Preview\"\n style={{ ...S.stage, ...style }}\n onPointerDown={down}\n onPointerMove={move}\n onPointerUp={up}\n onPointerCancel={up}\n >\n {banner}\n <Html html={html} style={{ display: 'contents' }} />\n </div>\n )\n}\n\n// ---------------------------------------------------------------------------\n\ninterface SlotGridProps {\n slot: CosmeticSlot\n owned: ReadonlySet<string>\n equipped: string | null\n tryOn: { slot: CosmeticSlot; id: string } | null\n /** Admins see archived items, greyed and not selectable. */\n isAdmin: boolean\n preview: (id: string | null) => string\n colour: React.CSSProperties\n onEquip: (id: string | null) => void\n onTry: (id: string) => void\n}\n\n/**\n * One slot's items: None, then the shown items, free or earned. Archived items\n * show only to admins, greyed and not selectable; nobody can wear one.\n */\nfunction SlotGrid({\n slot,\n owned,\n equipped,\n tryOn,\n isAdmin,\n preview,\n colour,\n onEquip,\n onTry\n}: SlotGridProps) {\n const items = COSMETICS.filter(i => i.slot === slot && (!i.hidden || isAdmin))\n const locked = items.filter(i => !i.hidden && !owned.has(i.id))\n\n return (\n <>\n <div style={S.grid}>\n <button\n type=\"button\"\n aria-pressed={equipped === null}\n style={{ ...S.tile, ...(equipped === null ? S.tileOn : {}) }}\n onClick={() => onEquip(null)}\n >\n <Html html={preview(null)} style={{ ...S.tilePv, ...colour }} />\n <span style={S.tileName}>None</span>\n </button>\n {items.map(item => {\n if (item.hidden) {\n return (\n <button\n key={item.id}\n type=\"button\"\n disabled\n aria-label={`${item.label}, archived`}\n style={{ ...S.tile, cursor: 'not-allowed', opacity: 0.45 }}\n >\n <Html html={preview(item.id)} style={{ ...S.tilePv, ...colour }} />\n <span style={S.tileName}>{item.label}</span>\n <span style={S.tileEarn}>Archived</span>\n </button>\n )\n }\n const own = owned.has(item.id)\n const on = equipped === item.id\n const trying = tryOn?.slot === slot && tryOn.id === item.id\n return (\n <button\n key={item.id}\n type=\"button\"\n aria-pressed={own ? on : trying}\n aria-label={\n own ? item.label : `${item.label}, locked. ${item.earn ?? ''}. Try it on.`\n }\n style={{\n ...S.tile,\n ...(on ? S.tileOn : {}),\n ...(trying ? S.tileTrying : {})\n }}\n onClick={() => (own ? onEquip(item.id) : onTry(item.id))}\n // A message reveal plays once; hovering its tile plays it again.\n onMouseEnter={e => slot === 'message' && replayPreview(e.currentTarget)}\n >\n <Html\n html={preview(item.id)}\n style={{ ...S.tilePv, ...colour, opacity: own ? 1 : 0.55 }}\n />\n <span style={{ ...S.tileName, color: own ? undefined : 'var(--color-text-muted)' }}>\n {!own && <Icon name=\"lock\" size={12} />}\n {item.label}\n </span>\n {!own && <span style={S.tileEarn}>{item.earn}</span>}\n </button>\n )\n })}\n </div>\n {locked.length > 0 && (\n <div style={S.foot}>\n <span>\n {locked.length === 1 ? 'One to earn.' : `${locked.length} to earn.`} Pick one to try it\n on.\n </span>\n </div>\n )}\n </>\n )\n}\n"],"names":["Html","live"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AvatarPicker.d.ts","sourceRoot":"","sources":["../../src/components/AvatarPicker.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,OAAO,KAA8B,MAAM,OAAO,CAAA;AAClD,OAAO,EAAE,KAAK,cAAc,EAAqB,MAAM,6BAA6B,CAAA;AACpF,OAAO,EAA6B,KAAK,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAsEhF,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,cAAc,CAAA;IACxB,IAAI,EAAE,YAAY,EAAE,GAAG,IAAI,CAAA;IAC3B,IAAI,EAAE,OAAO,CAAA;IACb,0EAA0E;IAC1E,GAAG,EAAE,CAAC,IAAI,EAAE,MAAM,OAAO,CAAC,cAAc,CAAC,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;CAC5D;AAED,wBAAgB,YAAY,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,EAAE,EAAE,iBAAiB,
|
|
1
|
+
{"version":3,"file":"AvatarPicker.d.ts","sourceRoot":"","sources":["../../src/components/AvatarPicker.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,OAAO,KAA8B,MAAM,OAAO,CAAA;AAClD,OAAO,EAAE,KAAK,cAAc,EAAqB,MAAM,6BAA6B,CAAA;AACpF,OAAO,EAA6B,KAAK,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAsEhF,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,cAAc,CAAA;IACxB,IAAI,EAAE,YAAY,EAAE,GAAG,IAAI,CAAA;IAC3B,IAAI,EAAE,OAAO,CAAA;IACb,0EAA0E;IAC1E,GAAG,EAAE,CAAC,IAAI,EAAE,MAAM,OAAO,CAAC,cAAc,CAAC,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;CAC5D;AAED,wBAAgB,YAAY,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,EAAE,EAAE,iBAAiB,qBA2J5E"}
|
|
@@ -88,6 +88,9 @@ function AvatarPicker({ settings, sets, busy, run }) {
|
|
|
88
88
|
};
|
|
89
89
|
const shown = settings.avatar;
|
|
90
90
|
const current = sets?.[setIdx];
|
|
91
|
+
const [view, setView] = useState(shown.source);
|
|
92
|
+
useEffect(() => setView(shown.source), [shown.source]);
|
|
93
|
+
const openable = (src) => src === "upload" || available[src];
|
|
91
94
|
return /* @__PURE__ */ jsxs("div", { style: S.stack, children: [
|
|
92
95
|
settings.choice && settings.choice !== shown.source && /* @__PURE__ */ jsxs("span", { className: "settings-popout__hint", children: [
|
|
93
96
|
LABEL[settings.choice],
|
|
@@ -98,16 +101,19 @@ function AvatarPicker({ settings, sets, busy, run }) {
|
|
|
98
101
|
{
|
|
99
102
|
type: "button",
|
|
100
103
|
role: "radio",
|
|
101
|
-
"aria-checked":
|
|
102
|
-
disabled: busy || !
|
|
103
|
-
title:
|
|
104
|
+
"aria-checked": view === src,
|
|
105
|
+
disabled: busy || !openable(src),
|
|
106
|
+
title: openable(src) ? void 0 : `Link ${LABEL[src]} under Settings › Account first`,
|
|
104
107
|
style: {
|
|
105
108
|
...S.source,
|
|
106
|
-
...
|
|
107
|
-
opacity:
|
|
109
|
+
...view === src ? S.sourceOn : {},
|
|
110
|
+
opacity: openable(src) ? 1 : 0.5
|
|
108
111
|
},
|
|
109
112
|
onClick: () => {
|
|
110
|
-
|
|
113
|
+
setView(src);
|
|
114
|
+
if (available[src] && shown.source !== src) {
|
|
115
|
+
run(() => avatarClient.choose(src)).catch(reportErr);
|
|
116
|
+
}
|
|
111
117
|
},
|
|
112
118
|
children: [
|
|
113
119
|
/* @__PURE__ */ jsx(Picture, { src: thumbOf[src], style: S.thumb }),
|
|
@@ -116,7 +122,7 @@ function AvatarPicker({ settings, sets, busy, run }) {
|
|
|
116
122
|
},
|
|
117
123
|
src
|
|
118
124
|
)) }),
|
|
119
|
-
|
|
125
|
+
view === "sprite" && (sets ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
120
126
|
sets.length > 1 && /* @__PURE__ */ jsx("div", { style: S.sets, children: sets.map((s, i) => /* @__PURE__ */ jsx(
|
|
121
127
|
"button",
|
|
122
128
|
{
|
|
@@ -148,8 +154,8 @@ function AvatarPicker({ settings, sets, busy, run }) {
|
|
|
148
154
|
c.id
|
|
149
155
|
)) })
|
|
150
156
|
] }) : /* @__PURE__ */ jsx("span", { className: "settings-popout__hint", children: "…" })),
|
|
151
|
-
/* @__PURE__ */ jsxs("div", { style: S.line, children: [
|
|
152
|
-
/* @__PURE__ */ jsx("span", { className: "settings-popout__label", style: S.grow, children: "
|
|
157
|
+
view === "upload" && /* @__PURE__ */ jsxs("div", { style: S.line, children: [
|
|
158
|
+
/* @__PURE__ */ jsx("span", { className: "settings-popout__label", style: S.grow, children: settings.upload ? "Your picture" : "Choose a picture to use" }),
|
|
153
159
|
/* @__PURE__ */ jsxs("label", { className: "settings-popout__btn", style: { opacity: busy ? 0.5 : 1 }, children: [
|
|
154
160
|
settings.upload ? "Replace" : "Choose image",
|
|
155
161
|
/* @__PURE__ */ jsx(
|
|
@@ -162,7 +168,11 @@ function AvatarPicker({ settings, sets, busy, run }) {
|
|
|
162
168
|
onChange: (e) => {
|
|
163
169
|
const file = e.currentTarget.files?.[0];
|
|
164
170
|
e.currentTarget.value = "";
|
|
165
|
-
if (file)
|
|
171
|
+
if (file)
|
|
172
|
+
run(async () => {
|
|
173
|
+
await avatarClient.upload(file);
|
|
174
|
+
return avatarClient.choose("upload");
|
|
175
|
+
}).catch(reportErr);
|
|
166
176
|
}
|
|
167
177
|
}
|
|
168
178
|
)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AvatarPicker.js","sources":["../../src/components/AvatarPicker.tsx"],"sourcesContent":["/**\n * AvatarPicker — the Picture tab of AvatarBuilder (inside AvatarDialog, opened\n * from ConnectedSettings): the picture people see (character, Discord, Steam or\n * an upload) and the character games use as the piece. One resolved avatar per\n * person, stored in hadoku_site's prefs-api and rendered by every app\n * (`@wolffm/prefs-client/avatar`, bundled into this package at build so the link\n * protocol cannot drift from the callback page).\n *\n * Everything happens in place: picking, uploading and removing are requests\n * from here. Linking Discord or Steam lives in the popout's Account tab\n * (LinkedAccounts); a linked account's picture is a choice here.\n *\n * AvatarBuilder owns the settings and the character table and loads them only\n * when the dialog opens; this component draws them and sends changes through\n * `run`, which applies the answer everywhere in the dialog.\n *\n * Styles are inline for the reason PASSKEY_DOT's are (ConnectedSettings.tsx):\n * apps compile app-header.css at their own build, so a new rule there reaches\n * an app only when it rebuilds.\n */\nimport React, { useEffect, useState } from 'react'\nimport { type AvatarSettings, type AvatarSource } from '@wolffm/prefs-client/avatar'\nimport { avatarClient, portraitUrl, type CharacterSet } from '../lib/avatarData'\n\nconst LABEL: Record<AvatarSource, string> = {\n sprite: 'Character',\n discord: 'Discord',\n steam: 'Steam',\n upload: 'Upload'\n}\n\nconst S = {\n stack: { display: 'flex', flexDirection: 'column', gap: 10, width: '100%' },\n img: { width: '100%', height: '100%', objectFit: 'cover', display: 'block' },\n sources: { display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 6 },\n source: {\n display: 'flex',\n flexDirection: 'column',\n alignItems: 'center',\n gap: 4,\n padding: 6,\n borderRadius: 8,\n border: '1px solid var(--color-border)',\n background: 'var(--color-bg)',\n color: 'var(--color-text)',\n fontSize: '0.72rem',\n cursor: 'pointer'\n },\n sourceOn: {\n borderColor: 'var(--color-primary)',\n boxShadow: 'inset 0 0 0 1px var(--color-primary)'\n },\n thumb: {\n width: 32,\n height: 32,\n borderRadius: '50%',\n overflow: 'hidden',\n background: 'var(--color-bg-alt)'\n },\n sets: { display: 'flex', gap: 4, flexWrap: 'wrap' },\n grid: { display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(64px, 1fr))', gap: 6 },\n cell: {\n padding: 0,\n aspectRatio: '1',\n borderRadius: 6,\n overflow: 'hidden',\n border: '2px solid transparent',\n background: 'var(--color-bg-alt)',\n cursor: 'pointer'\n },\n line: { display: 'flex', alignItems: 'center', gap: 6, flexWrap: 'wrap' },\n grow: { flex: 1, minWidth: 0 }\n} satisfies Record<string, React.CSSProperties>\n\nfunction Picture({ src, style }: { src: string | null; style: React.CSSProperties }) {\n const [failed, setFailed] = useState(false)\n useEffect(() => setFailed(false), [src])\n return (\n <span style={style}>\n {src && !failed && (\n <img\n src={src}\n alt=\"\"\n referrerPolicy=\"no-referrer\"\n style={S.img}\n onError={() => setFailed(true)}\n />\n )}\n </span>\n )\n}\n\nexport interface AvatarPickerProps {\n settings: AvatarSettings\n sets: CharacterSet[] | null\n busy: boolean\n /** Send a change; the builder applies the answer and reports failures. */\n run: (work: () => Promise<AvatarSettings>) => Promise<void>\n}\n\nexport function AvatarPicker({ settings, sets, busy, run }: AvatarPickerProps) {\n const [setIdx, setSetIdx] = useState(0)\n // Open on the set holding the person's character.\n useEffect(() => {\n const at = sets?.findIndex(s => s.characters.some(c => c.id === settings.sprite)) ?? -1\n if (at > 0) setSetIdx(at)\n // Only when the table arrives: afterwards the person moves between sets.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [sets])\n\n const characters = new Map((sets ?? []).flatMap(s => s.characters).map(c => [c.id, c]))\n const spriteSrc = (id: string | null | undefined) => {\n const c = id ? characters.get(id) : undefined\n return c ? portraitUrl(c) : null\n }\n\n const available: Record<AvatarSource, boolean> = {\n sprite: true,\n discord: Boolean(settings.linked.discord?.avatarUrl),\n steam: Boolean(settings.linked.steam?.avatarUrl),\n upload: settings.upload !== null\n }\n const thumbOf: Record<AvatarSource, string | null> = {\n sprite: spriteSrc(settings.sprite),\n discord: settings.linked.discord?.avatarUrl ?? null,\n steam: settings.linked.steam?.avatarUrl ?? null,\n upload: settings.upload?.url ?? null\n }\n const shown = settings.avatar\n const current = sets?.[setIdx]\n\n return (\n <div style={S.stack}>\n {settings.choice && settings.choice !== shown.source && (\n <span className=\"settings-popout__hint\">\n {LABEL[settings.choice]} is unavailable, so your character shows instead.\n </span>\n )}\n\n <div style={S.sources} role=\"radiogroup\" aria-label=\"Picture\">\n {(Object.keys(LABEL) as AvatarSource[]).map(src => (\n <button\n key={src}\n type=\"button\"\n role=\"radio\"\n aria-checked={shown.source === src}\n disabled={busy || !available[src]}\n title={\n available[src]\n ? undefined\n : src === 'upload'\n ? 'Upload a picture below first'\n : `Link ${LABEL[src]} under Settings › Account first`\n }\n style={{\n ...S.source,\n ...(shown.source === src ? S.sourceOn : {}),\n opacity: available[src] ? 1 : 0.5\n }}\n onClick={() => {\n run(() => avatarClient.choose(src)).catch(reportErr)\n }}\n >\n <Picture src={thumbOf[src]} style={S.thumb} />\n {LABEL[src]}\n </button>\n ))}\n </div>\n\n {shown.source === 'sprite' &&\n (sets ? (\n <>\n {sets.length > 1 && (\n <div style={S.sets}>\n {sets.map((s, i) => (\n <button\n key={s.id}\n type=\"button\"\n className={`settings-popout__btn${i === setIdx ? ' settings-popout__btn--primary' : ''}`}\n onClick={() => setSetIdx(i)}\n >\n {s.id}\n </button>\n ))}\n </div>\n )}\n <div style={S.grid} role=\"radiogroup\" aria-label=\"Character\">\n {current?.characters.map(c => (\n <button\n key={c.id}\n type=\"button\"\n role=\"radio\"\n aria-checked={settings.sprite === c.id}\n aria-label={c.name}\n title={c.name}\n disabled={busy}\n style={{\n ...S.cell,\n borderColor: settings.sprite === c.id ? c.color : 'transparent'\n }}\n onClick={() => {\n run(() => avatarClient.update({ sprite: c.id })).catch(reportErr)\n }}\n >\n <img src={portraitUrl(c)} alt=\"\" style={S.img} />\n </button>\n ))}\n </div>\n </>\n ) : (\n <span className=\"settings-popout__hint\">…</span>\n ))}\n\n <div style={S.line}>\n <span className=\"settings-popout__label\" style={S.grow}>\n Upload\n </span>\n <label className=\"settings-popout__btn\" style={{ opacity: busy ? 0.5 : 1 }}>\n {settings.upload ? 'Replace' : 'Choose image'}\n <input\n type=\"file\"\n accept={settings.limits.acceptedTypes.join(',')}\n hidden\n disabled={busy}\n onChange={e => {\n const file = e.currentTarget.files?.[0]\n e.currentTarget.value = ''\n if (file) run(() => avatarClient.upload(file)).catch(reportErr)\n }}\n />\n </label>\n {settings.upload && (\n <button\n type=\"button\"\n className=\"settings-popout__btn\"\n disabled={busy}\n onClick={() => {\n run(() => avatarClient.removeUpload()).catch(reportErr)\n }}\n >\n Remove\n </button>\n )}\n </div>\n </div>\n )\n}\n\nfunction reportErr(err: unknown) {\n console.error('[AvatarPicker] action failed', (err as Error)?.message ?? err)\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"AvatarPicker.js","sources":["../../src/components/AvatarPicker.tsx"],"sourcesContent":["/**\n * AvatarPicker — the Picture tab of AvatarBuilder (inside AvatarDialog, opened\n * from ConnectedSettings): the picture people see (character, Discord, Steam or\n * an upload) and the character games use as the piece. One resolved avatar per\n * person, stored in hadoku_site's prefs-api and rendered by every app\n * (`@wolffm/prefs-client/avatar`, bundled into this package at build so the link\n * protocol cannot drift from the callback page).\n *\n * Everything happens in place: picking, uploading and removing are requests\n * from here. Linking Discord or Steam lives in the popout's Account tab\n * (LinkedAccounts); a linked account's picture is a choice here.\n *\n * AvatarBuilder owns the settings and the character table and loads them only\n * when the dialog opens; this component draws them and sends changes through\n * `run`, which applies the answer everywhere in the dialog.\n *\n * Styles are inline for the reason PASSKEY_DOT's are (ConnectedSettings.tsx):\n * apps compile app-header.css at their own build, so a new rule there reaches\n * an app only when it rebuilds.\n */\nimport React, { useEffect, useState } from 'react'\nimport { type AvatarSettings, type AvatarSource } from '@wolffm/prefs-client/avatar'\nimport { avatarClient, portraitUrl, type CharacterSet } from '../lib/avatarData'\n\nconst LABEL: Record<AvatarSource, string> = {\n sprite: 'Character',\n discord: 'Discord',\n steam: 'Steam',\n upload: 'Upload'\n}\n\nconst S = {\n stack: { display: 'flex', flexDirection: 'column', gap: 10, width: '100%' },\n img: { width: '100%', height: '100%', objectFit: 'cover', display: 'block' },\n sources: { display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 6 },\n source: {\n display: 'flex',\n flexDirection: 'column',\n alignItems: 'center',\n gap: 4,\n padding: 6,\n borderRadius: 8,\n border: '1px solid var(--color-border)',\n background: 'var(--color-bg)',\n color: 'var(--color-text)',\n fontSize: '0.72rem',\n cursor: 'pointer'\n },\n sourceOn: {\n borderColor: 'var(--color-primary)',\n boxShadow: 'inset 0 0 0 1px var(--color-primary)'\n },\n thumb: {\n width: 32,\n height: 32,\n borderRadius: '50%',\n overflow: 'hidden',\n background: 'var(--color-bg-alt)'\n },\n sets: { display: 'flex', gap: 4, flexWrap: 'wrap' },\n grid: { display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(64px, 1fr))', gap: 6 },\n cell: {\n padding: 0,\n aspectRatio: '1',\n borderRadius: 6,\n overflow: 'hidden',\n border: '2px solid transparent',\n background: 'var(--color-bg-alt)',\n cursor: 'pointer'\n },\n line: { display: 'flex', alignItems: 'center', gap: 6, flexWrap: 'wrap' },\n grow: { flex: 1, minWidth: 0 }\n} satisfies Record<string, React.CSSProperties>\n\nfunction Picture({ src, style }: { src: string | null; style: React.CSSProperties }) {\n const [failed, setFailed] = useState(false)\n useEffect(() => setFailed(false), [src])\n return (\n <span style={style}>\n {src && !failed && (\n <img\n src={src}\n alt=\"\"\n referrerPolicy=\"no-referrer\"\n style={S.img}\n onError={() => setFailed(true)}\n />\n )}\n </span>\n )\n}\n\nexport interface AvatarPickerProps {\n settings: AvatarSettings\n sets: CharacterSet[] | null\n busy: boolean\n /** Send a change; the builder applies the answer and reports failures. */\n run: (work: () => Promise<AvatarSettings>) => Promise<void>\n}\n\nexport function AvatarPicker({ settings, sets, busy, run }: AvatarPickerProps) {\n const [setIdx, setSetIdx] = useState(0)\n // Open on the set holding the person's character.\n useEffect(() => {\n const at = sets?.findIndex(s => s.characters.some(c => c.id === settings.sprite)) ?? -1\n if (at > 0) setSetIdx(at)\n // Only when the table arrives: afterwards the person moves between sets.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [sets])\n\n const characters = new Map((sets ?? []).flatMap(s => s.characters).map(c => [c.id, c]))\n const spriteSrc = (id: string | null | undefined) => {\n const c = id ? characters.get(id) : undefined\n return c ? portraitUrl(c) : null\n }\n\n const available: Record<AvatarSource, boolean> = {\n sprite: true,\n discord: Boolean(settings.linked.discord?.avatarUrl),\n steam: Boolean(settings.linked.steam?.avatarUrl),\n upload: settings.upload !== null\n }\n const thumbOf: Record<AvatarSource, string | null> = {\n sprite: spriteSrc(settings.sprite),\n discord: settings.linked.discord?.avatarUrl ?? null,\n steam: settings.linked.steam?.avatarUrl ?? null,\n upload: settings.upload?.url ?? null\n }\n const shown = settings.avatar\n const current = sets?.[setIdx]\n // The tab on show. Picking an available source also wears it; Upload opens\n // with nothing uploaded yet, to choose a file.\n const [view, setView] = useState<AvatarSource>(shown.source)\n useEffect(() => setView(shown.source), [shown.source])\n const openable = (src: AvatarSource) => src === 'upload' || available[src]\n\n return (\n <div style={S.stack}>\n {settings.choice && settings.choice !== shown.source && (\n <span className=\"settings-popout__hint\">\n {LABEL[settings.choice]} is unavailable, so your character shows instead.\n </span>\n )}\n\n <div style={S.sources} role=\"radiogroup\" aria-label=\"Picture\">\n {(Object.keys(LABEL) as AvatarSource[]).map(src => (\n <button\n key={src}\n type=\"button\"\n role=\"radio\"\n aria-checked={view === src}\n disabled={busy || !openable(src)}\n title={openable(src) ? undefined : `Link ${LABEL[src]} under Settings › Account first`}\n style={{\n ...S.source,\n ...(view === src ? S.sourceOn : {}),\n opacity: openable(src) ? 1 : 0.5\n }}\n onClick={() => {\n setView(src)\n if (available[src] && shown.source !== src) {\n run(() => avatarClient.choose(src)).catch(reportErr)\n }\n }}\n >\n <Picture src={thumbOf[src]} style={S.thumb} />\n {LABEL[src]}\n </button>\n ))}\n </div>\n\n {view === 'sprite' &&\n (sets ? (\n <>\n {sets.length > 1 && (\n <div style={S.sets}>\n {sets.map((s, i) => (\n <button\n key={s.id}\n type=\"button\"\n className={`settings-popout__btn${i === setIdx ? ' settings-popout__btn--primary' : ''}`}\n onClick={() => setSetIdx(i)}\n >\n {s.id}\n </button>\n ))}\n </div>\n )}\n <div style={S.grid} role=\"radiogroup\" aria-label=\"Character\">\n {current?.characters.map(c => (\n <button\n key={c.id}\n type=\"button\"\n role=\"radio\"\n aria-checked={settings.sprite === c.id}\n aria-label={c.name}\n title={c.name}\n disabled={busy}\n style={{\n ...S.cell,\n borderColor: settings.sprite === c.id ? c.color : 'transparent'\n }}\n onClick={() => {\n run(() => avatarClient.update({ sprite: c.id })).catch(reportErr)\n }}\n >\n <img src={portraitUrl(c)} alt=\"\" style={S.img} />\n </button>\n ))}\n </div>\n </>\n ) : (\n <span className=\"settings-popout__hint\">…</span>\n ))}\n\n {view === 'upload' && (\n <div style={S.line}>\n <span className=\"settings-popout__label\" style={S.grow}>\n {settings.upload ? 'Your picture' : 'Choose a picture to use'}\n </span>\n <label className=\"settings-popout__btn\" style={{ opacity: busy ? 0.5 : 1 }}>\n {settings.upload ? 'Replace' : 'Choose image'}\n <input\n type=\"file\"\n accept={settings.limits.acceptedTypes.join(',')}\n hidden\n disabled={busy}\n onChange={e => {\n const file = e.currentTarget.files?.[0]\n e.currentTarget.value = ''\n // An upload is worn at once: picking a file on this tab means \"use this\".\n if (file)\n run(async () => {\n await avatarClient.upload(file)\n return avatarClient.choose('upload')\n }).catch(reportErr)\n }}\n />\n </label>\n {settings.upload && (\n <button\n type=\"button\"\n className=\"settings-popout__btn\"\n disabled={busy}\n onClick={() => {\n run(() => avatarClient.removeUpload()).catch(reportErr)\n }}\n >\n Remove\n </button>\n )}\n </div>\n )}\n </div>\n )\n}\n\nfunction reportErr(err: unknown) {\n console.error('[AvatarPicker] action failed', (err as Error)?.message ?? err)\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"avatar.js","sources":["../../../../../node_modules/.pnpm/@wolffm+prefs-client@1.1.
|
|
1
|
+
{"version":3,"file":"avatar.js","sources":["../../../../../node_modules/.pnpm/@wolffm+prefs-client@1.1.18_react@19.2.8_zod@4.6.7/node_modules/@wolffm/prefs-client/dist/avatar.js"],"sourcesContent":["// src/constants.ts\nvar PREFS_API_BASE = \"https://hadoku.me/prefs\";\n\n// src/avatar-contract.ts\nvar AVATAR_SOURCES = [\"sprite\", \"discord\", \"steam\", \"upload\"];\nvar LINK_PROVIDERS = [\"discord\", \"steam\"];\nvar LINK_MESSAGE_TYPE = \"hadoku-avatar-link\";\nvar RESOLVE_MAX_IDS = 100;\nvar SPRITE_ID_PATTERN = /^[a-z][a-z0-9-]{0,31}$/;\nvar LINK_CALLBACK_PATH = \"/account/link/callback\";\n\n// src/avatar.ts\nvar LINK_WINDOW_NAME = \"hadoku-avatar-link\";\nvar LINK_POPUP_MARK = \"hadoku-avatar-link-popup\";\nfunction markPopup(on) {\n try {\n if (on) sessionStorage.setItem(LINK_POPUP_MARK, \"1\");\n else sessionStorage.removeItem(LINK_POPUP_MARK);\n } catch {\n }\n}\nvar AvatarError = class extends Error {\n constructor(status, message) {\n super(message);\n this.status = status;\n this.name = \"AvatarError\";\n }\n status;\n};\nasync function readJson(res) {\n if (!res.ok) {\n const body = await res.json().catch(() => null);\n throw new AvatarError(res.status, body?.message ?? `HTTP ${res.status}`);\n }\n return await res.json();\n}\nfunction createAvatarClient(opts = {}) {\n const base = `${opts.baseUrl ?? PREFS_API_BASE}/api/v1/_avatar`;\n const f = opts.fetch ?? fetch;\n const call = (path, init = {}) => f(`${base}${path}`, { credentials: \"include\", ...init }).then(readJson);\n const send = (path, method, body) => call(path, {\n method,\n ...body === void 0 ? {} : { headers: { \"Content-Type\": \"application/json\" }, body: JSON.stringify(body) }\n });\n const getSettings = () => call(\"\");\n async function linkInPopup(provider) {\n markPopup(true);\n const popup = window.open(\"about:blank\", LINK_WINDOW_NAME, \"popup,width=520,height=760\");\n markPopup(false);\n if (!popup)\n throw new AvatarError(0, \"The sign-in window was blocked: allow pop-ups for hadoku.me\");\n let authorizeUrl;\n try {\n ({ authorizeUrl } = await send(`/link/${provider}`, \"POST\", {}));\n } catch (err) {\n popup.close();\n throw err;\n }\n popup.location.href = authorizeUrl;\n return new Promise((resolve, reject) => {\n const channel = new BroadcastChannel(LINK_MESSAGE_TYPE);\n let settled = false;\n const finish = (failure) => {\n if (settled) return;\n settled = true;\n channel.close();\n clearInterval(closedPoll);\n clearTimeout(giveUp);\n if (failure) reject(new AvatarError(400, failure));\n else getSettings().then(resolve, reject);\n };\n channel.onmessage = (e) => {\n if (e.data?.type !== LINK_MESSAGE_TYPE) return;\n finish(e.data.ok ? void 0 : e.data.error);\n };\n const closedPoll = setInterval(() => {\n if (!popup.closed) return;\n clearInterval(closedPoll);\n setTimeout(() => finish(), 1500);\n }, 500);\n const giveUp = setTimeout(() => finish(), 10 * 60 * 1e3);\n });\n }\n return {\n /** The caller's settings, including the resolved avatar. */\n get: getSettings,\n /** Pick a source. 409 when it is not available (not linked, nothing uploaded). */\n choose: (source) => send(\"\", \"PUT\", { source }),\n /** Set the source, the character, or both in one write. */\n update: (u) => send(\"\", \"PUT\", u),\n /**\n * Begin linking. Navigate the browser to the returned URL; the provider\n * returns it to hadoku.me/account/link/callback, which completes the link.\n * Call this only from the user's own click: nothing is fetched from\n * Discord or Steam before they ask.\n */\n startLink: (provider) => send(`/link/${provider}`, \"POST\", {}),\n /**\n * Link in a popup, so the page never navigates. Call it straight from the\n * click handler: the window is opened before the first await, which is\n * what popup blockers allow. Resolves with fresh settings once the popup\n * reports back or is closed (closing is not an error: the settings say\n * whether the link landed). Rejects only when the popup could not open\n * or the link could not start.\n */\n linkInPopup: (provider) => linkInPopup(provider),\n /** Finish a link from the callback page's query string (`location.search`). */\n completeLink: (query) => send(\"/link/complete\", \"POST\", { query: query.replace(/^\\?/, \"\") }),\n /** Delete the linked id and its cached avatar URL. */\n unlink: (provider) => send(`/link/${provider}`, \"DELETE\"),\n /** Upload an image (PNG, JPEG, WebP or GIF). It is re-encoded and becomes the chosen source. */\n upload: (image) => call(\"/upload\", { method: \"POST\", body: image }),\n removeUpload: () => send(\"/upload\", \"DELETE\"),\n /**\n * Equip or take off items and place overlays. 409 for an item the person\n * does not own; a placement is fitted to the allowed area before it is\n * stored, so the returned settings show where it really landed.\n */\n setCosmetics: (u) => send(\"/cosmetics\", \"PUT\", u)\n };\n}\nasync function resolveAvatars(opts, userIds) {\n const f = opts.fetch ?? fetch;\n const url = `${opts.baseUrl ?? PREFS_API_BASE}/api/v1/_avatar/resolve`;\n const unique = [...new Set(userIds)];\n const out = {};\n for (let i = 0; i < unique.length; i += RESOLVE_MAX_IDS) {\n const res = await f(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", \"X-User-Key\": opts.serviceKey },\n body: JSON.stringify({ userIds: unique.slice(i, i + RESOLVE_MAX_IDS) })\n });\n Object.assign(out, (await readJson(res)).avatars);\n }\n return out;\n}\nexport {\n AVATAR_SOURCES,\n AvatarError,\n LINK_CALLBACK_PATH,\n LINK_MESSAGE_TYPE,\n LINK_POPUP_MARK,\n LINK_PROVIDERS,\n RESOLVE_MAX_IDS,\n SPRITE_ID_PATTERN,\n createAvatarClient,\n resolveAvatars\n};\n"],"names":[],"mappings":"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","x_google_ignoreList":[0]}
|
|
@@ -200,6 +200,114 @@ function wornCosmetics(loadout) {
|
|
|
200
200
|
}
|
|
201
201
|
return Object.keys(worn).length ? worn : void 0;
|
|
202
202
|
}
|
|
203
|
+
var MIN_LIGHTNESS = 0.3;
|
|
204
|
+
var MAX_LIGHTNESS = 0.82;
|
|
205
|
+
var SAMPLE_SIZE = 24;
|
|
206
|
+
var MIN_ALPHA = 32;
|
|
207
|
+
function averagePictureColour(pixels) {
|
|
208
|
+
let r = 0;
|
|
209
|
+
let g = 0;
|
|
210
|
+
let b = 0;
|
|
211
|
+
let weight = 0;
|
|
212
|
+
for (let i = 0; i + 3 < pixels.length; i += 4) {
|
|
213
|
+
const a = pixels[i + 3];
|
|
214
|
+
if (a < MIN_ALPHA) continue;
|
|
215
|
+
r += pixels[i] * a;
|
|
216
|
+
g += pixels[i + 1] * a;
|
|
217
|
+
b += pixels[i + 2] * a;
|
|
218
|
+
weight += a;
|
|
219
|
+
}
|
|
220
|
+
if (weight === 0) return null;
|
|
221
|
+
return clampLightness(r / weight, g / weight, b / weight);
|
|
222
|
+
}
|
|
223
|
+
function clampLightness(r, g, b) {
|
|
224
|
+
const [h, s, l] = rgbToHsl(r / 255, g / 255, b / 255);
|
|
225
|
+
const clamped = Math.min(MAX_LIGHTNESS, Math.max(MIN_LIGHTNESS, l));
|
|
226
|
+
const [r2, g2, b2] = hslToRgb(h, s, clamped);
|
|
227
|
+
const hex = (v) => Math.round(v * 255).toString(16).padStart(2, "0");
|
|
228
|
+
return `#${hex(r2)}${hex(g2)}${hex(b2)}`;
|
|
229
|
+
}
|
|
230
|
+
function rgbToHsl(r, g, b) {
|
|
231
|
+
const max = Math.max(r, g, b);
|
|
232
|
+
const min = Math.min(r, g, b);
|
|
233
|
+
const l = (max + min) / 2;
|
|
234
|
+
if (max === min) return [0, 0, l];
|
|
235
|
+
const d = max - min;
|
|
236
|
+
const s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
|
237
|
+
let h;
|
|
238
|
+
if (max === r) h = (g - b) / d + (g < b ? 6 : 0);
|
|
239
|
+
else if (max === g) h = (b - r) / d + 2;
|
|
240
|
+
else h = (r - g) / d + 4;
|
|
241
|
+
return [h / 6, s, l];
|
|
242
|
+
}
|
|
243
|
+
function hslToRgb(h, s, l) {
|
|
244
|
+
if (s === 0) return [l, l, l];
|
|
245
|
+
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
|
|
246
|
+
const p = 2 * l - q;
|
|
247
|
+
const channel = (t) => {
|
|
248
|
+
if (t < 0) t += 1;
|
|
249
|
+
if (t > 1) t -= 1;
|
|
250
|
+
if (t < 1 / 6) return p + (q - p) * 6 * t;
|
|
251
|
+
if (t < 1 / 2) return q;
|
|
252
|
+
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
|
|
253
|
+
return p;
|
|
254
|
+
};
|
|
255
|
+
return [channel(h + 1 / 3), channel(h), channel(h - 1 / 3)];
|
|
256
|
+
}
|
|
257
|
+
var colours = /* @__PURE__ */ new Map();
|
|
258
|
+
var pending = /* @__PURE__ */ new Map();
|
|
259
|
+
var listeners = /* @__PURE__ */ new Set();
|
|
260
|
+
function pictureColour(url) {
|
|
261
|
+
return colours.get(url);
|
|
262
|
+
}
|
|
263
|
+
function onPictureColour(listener) {
|
|
264
|
+
listeners.add(listener);
|
|
265
|
+
return () => {
|
|
266
|
+
listeners.delete(listener);
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
function readPictureColour(url) {
|
|
270
|
+
if (colours.has(url)) return Promise.resolve(colours.get(url) ?? null);
|
|
271
|
+
const inFlight = pending.get(url);
|
|
272
|
+
if (inFlight) return inFlight;
|
|
273
|
+
const read = sample(url).then((colour) => {
|
|
274
|
+
pending.delete(url);
|
|
275
|
+
colours.set(url, colour);
|
|
276
|
+
for (const l of listeners) l();
|
|
277
|
+
return colour;
|
|
278
|
+
});
|
|
279
|
+
pending.set(url, read);
|
|
280
|
+
return read;
|
|
281
|
+
}
|
|
282
|
+
function sample(url) {
|
|
283
|
+
if (typeof Image === "undefined" || typeof document === "undefined") {
|
|
284
|
+
return Promise.resolve(null);
|
|
285
|
+
}
|
|
286
|
+
return new Promise((resolve) => {
|
|
287
|
+
const img = new Image();
|
|
288
|
+
img.crossOrigin = "anonymous";
|
|
289
|
+
img.referrerPolicy = "no-referrer";
|
|
290
|
+
img.onload = () => {
|
|
291
|
+
const canvas = document.createElement("canvas");
|
|
292
|
+
canvas.width = SAMPLE_SIZE;
|
|
293
|
+
canvas.height = SAMPLE_SIZE;
|
|
294
|
+
const ctx = canvas.getContext("2d", { willReadFrequently: true });
|
|
295
|
+
if (!ctx) {
|
|
296
|
+
resolve(null);
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
ctx.drawImage(img, 0, 0, SAMPLE_SIZE, SAMPLE_SIZE);
|
|
300
|
+
try {
|
|
301
|
+
resolve(averagePictureColour(ctx.getImageData(0, 0, SAMPLE_SIZE, SAMPLE_SIZE).data));
|
|
302
|
+
} catch (err) {
|
|
303
|
+
console.debug("[prefs-client] picture colour unreadable", url, err);
|
|
304
|
+
resolve(null);
|
|
305
|
+
}
|
|
306
|
+
};
|
|
307
|
+
img.onerror = () => resolve(null);
|
|
308
|
+
img.src = url;
|
|
309
|
+
});
|
|
310
|
+
}
|
|
203
311
|
var COSMETICS_ASSET_BASE = "/cosmetics";
|
|
204
312
|
var STYLE_ID = "hadoku-cosmetics";
|
|
205
313
|
var esc = (s) => s.replace(
|
|
@@ -492,6 +600,7 @@ export {
|
|
|
492
600
|
PLACEMENT_LIMITS,
|
|
493
601
|
PLACEMENT_ZONE,
|
|
494
602
|
avatarHtml,
|
|
603
|
+
averagePictureColour,
|
|
495
604
|
cleanLoadout,
|
|
496
605
|
cosmeticsCss,
|
|
497
606
|
decorHtml,
|
|
@@ -501,8 +610,11 @@ export {
|
|
|
501
610
|
fitPlacement,
|
|
502
611
|
messageHtml,
|
|
503
612
|
nameHtml,
|
|
613
|
+
onPictureColour,
|
|
504
614
|
overlayBounds,
|
|
615
|
+
pictureColour,
|
|
505
616
|
placementTransform,
|
|
617
|
+
readPictureColour,
|
|
506
618
|
wornCosmetics
|
|
507
619
|
};
|
|
508
620
|
//# sourceMappingURL=cosmetics.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cosmetics.js","sources":["../../../../../node_modules/.pnpm/@wolffm+prefs-client@1.1.16_react@19.2.8_zod@4.6.7/node_modules/@wolffm/prefs-client/dist/cosmetics.js"],"sourcesContent":["// src/cosmetics-contract.ts\nvar COSMETIC_SLOTS = [\"effect\", \"gradient\", \"font\", \"overlay\", \"message\"];\nvar COSMETIC_ID_PATTERN = /^(effect|gradient|font|overlay|message)\\.[a-z0-9-]{1,32}$/;\nvar item = (slot, name, label, opts = {}) => ({ id: `${slot}.${name}`, slot, label, ...opts });\nvar archived = { hidden: true };\nvar COSMETICS = [\n item(\"effect\", \"glint\", \"Glint\"),\n item(\"effect\", \"aura\", \"Aura\"),\n item(\"effect\", \"hop\", \"Hop\"),\n item(\"effect\", \"outline\", \"Outline\"),\n item(\"effect\", \"twinkle\", \"Twinkle\", { earn: \"Bronze or better in a Major\" }),\n item(\"gradient\", \"gild\", \"Gilded\"),\n item(\"gradient\", \"gloss\", \"Gloss\"),\n item(\"gradient\", \"split\", \"Split\"),\n item(\"gradient\", \"brushed\", \"Brushed\"),\n item(\"gradient\", \"lumen\", \"Lumen\"),\n item(\"font\", \"script\", \"Script\"),\n item(\"font\", \"surf\", \"Surf\"),\n item(\"font\", \"gothic\", \"Gothic\"),\n item(\"font\", \"runic\", \"Runic\"),\n item(\"font\", \"arcade\", \"Arcade\"),\n item(\"font\", \"caps\", \"Small caps\", archived),\n item(\"font\", \"wide\", \"Wide\", archived),\n item(\"font\", \"corsair\", \"Corsair\", archived),\n item(\"font\", \"metal\", \"Metal\", archived),\n item(\"font\", \"comic\", \"Comic\", archived),\n item(\"font\", \"circuit\", \"Circuit\", archived),\n item(\"font\", \"hover\", \"Hover\", archived),\n item(\"font\", \"retro\", \"Retro\", archived),\n item(\"font\", \"pixel\", \"Pixel\", archived),\n item(\"overlay\", \"crown\", \"Crown\", {\n earn: \"Win a Major\",\n art: {\n file: \"crown.webp\",\n top: -40,\n width: 70,\n tilt: -8,\n pixel: false,\n visible: [0.027, 0.137, 0.973, 0.863]\n }\n }),\n item(\"overlay\", \"halo\", \"Halo\", {\n art: {\n file: \"halo.webp\",\n top: -34,\n width: 76,\n tilt: 0,\n pixel: true,\n visible: [0.033, 0.333, 0.967, 0.667]\n }\n }),\n item(\"overlay\", \"flowers\", \"Flower crown\", {\n art: {\n file: \"flowers.webp\",\n top: -22,\n width: 96,\n tilt: 0,\n pixel: false,\n visible: [0.027, 0.258, 0.969, 0.746]\n }\n }),\n item(\"overlay\", \"petals\", \"Petals\"),\n item(\"overlay\", \"horns\", \"Horns\", {\n ...archived,\n art: {\n file: \"horns.webp\",\n top: -26,\n width: 100,\n tilt: 0,\n pixel: true,\n visible: [0.033, 0.133, 0.967, 0.867]\n }\n }),\n item(\"overlay\", \"runes\", \"Runes\", archived),\n item(\"overlay\", \"comet\", \"Comet\", archived),\n item(\"overlay\", \"shine\", \"Shine\", archived),\n item(\"overlay\", \"glow\", \"Glow\", archived),\n item(\"message\", \"type\", \"Typewriter\"),\n item(\"message\", \"decode\", \"Decode\", { earn: \"Silver or better in a Major\" }),\n item(\"message\", \"cascade\", \"Cascade\"),\n item(\"message\", \"words\", \"Word by word\"),\n item(\"message\", \"bubble\", \"Bubble\", archived),\n item(\"message\", \"drift\", \"Drift\")\n];\nvar COSMETICS_BY_ID = new Map(\n COSMETICS.map((i) => [i.id, i])\n);\nvar EARNED_COSMETICS = COSMETICS.filter(\n (i) => i.earn !== void 0 && !i.hidden\n);\nfunction ownedFrom(granted) {\n const out = new Set(COSMETICS.filter((i) => !i.hidden && i.earn === void 0).map((i) => i.id));\n for (const id of granted) {\n const it = COSMETICS_BY_ID.get(id);\n if (it && !it.hidden) out.add(id);\n }\n return out;\n}\nvar PLACEMENT_DEFAULT = { x: 0, y: 0, r: 0, s: 100, flip: false, back: false };\nvar PLACEMENT_LIMITS = {\n x: [-60, 60],\n y: [-60, 100],\n r: [-45, 45],\n s: [70, 140]\n};\nfunction overlayBounds(art, p) {\n const [a, b, c, d] = art.visible;\n const x0 = p.flip ? 1 - c : a;\n const x1 = p.flip ? 1 - a : c;\n const L = 50 - art.width / 2;\n const T = art.top;\n const W = art.width;\n const ox = L + W * 0.5;\n const oy = T + W * 0.6;\n const cx = L + W / 2;\n const cy = T + W / 2;\n const t = art.tilt * Math.PI / 180;\n const r = p.r * Math.PI / 180;\n const sc = p.s / 100;\n const xs = [];\n const ys = [];\n for (const [fx, fy] of [\n [x0, b],\n [x1, b],\n [x0, d],\n [x1, d]\n ]) {\n const px = L + fx * W;\n const py = T + fy * W;\n const tx = cx + (px - cx) * Math.cos(t) - (py - cy) * Math.sin(t);\n const ty = cy + (px - cx) * Math.sin(t) + (py - cy) * Math.cos(t);\n const dx = (tx - ox) * sc;\n const dy = (ty - oy) * sc;\n xs.push(ox + p.x + dx * Math.cos(r) - dy * Math.sin(r));\n ys.push(oy + p.y + dx * Math.sin(r) + dy * Math.cos(r));\n }\n return {\n left: Math.min(...xs),\n top: Math.min(...ys),\n right: Math.max(...xs),\n bottom: Math.max(...ys)\n };\n}\nvar ART_ITEMS = COSMETICS.filter((i) => !!i.art);\nvar PLACEMENT_ZONE = {\n left: -25,\n top: Math.floor(Math.min(...ART_ITEMS.map((i) => overlayBounds(i.art, PLACEMENT_DEFAULT).top))),\n right: 125,\n bottom: 110\n};\nvar clamp = (v, [lo, hi]) => Math.min(hi, Math.max(lo, v));\nfunction fitPlacement(art, raw) {\n const num = (v, fallback) => typeof v === \"number\" && Number.isFinite(v) ? v : fallback;\n const p = {\n x: Math.round(clamp(num(raw.x, 0), PLACEMENT_LIMITS.x)),\n y: Math.round(clamp(num(raw.y, 0), PLACEMENT_LIMITS.y)),\n r: Math.round(clamp(num(raw.r, 0), PLACEMENT_LIMITS.r)),\n s: Math.round(clamp(num(raw.s, 100), PLACEMENT_LIMITS.s)),\n flip: raw.flip === true,\n back: raw.back === true\n };\n const z = PLACEMENT_ZONE;\n let box = overlayBounds(art, p);\n const room = Math.min(\n (z.right - z.left - 2) / (box.right - box.left),\n (z.bottom - z.top - 2) / (box.bottom - box.top)\n );\n if (room < 1) {\n p.s = Math.max(PLACEMENT_LIMITS.s[0], Math.floor(p.s * room));\n box = overlayBounds(art, p);\n }\n p.x += Math.ceil(Math.max(0, z.left - box.left)) - Math.ceil(Math.max(0, box.right - z.right));\n p.y += Math.ceil(Math.max(0, z.top - box.top)) - Math.ceil(Math.max(0, box.bottom - z.bottom));\n return p;\n}\nfunction emptyLoadout() {\n return {\n equipped: { effect: null, gradient: null, font: null, overlay: null, message: null },\n placement: {}\n };\n}\nfunction cleanWorn(raw) {\n if (!raw || typeof raw !== \"object\") return void 0;\n const src = raw;\n const worn = {};\n for (const slot of COSMETIC_SLOTS) {\n const id = src[slot];\n const it = typeof id === \"string\" ? COSMETICS_BY_ID.get(id) : void 0;\n if (it && !it.hidden && it.slot === slot) worn[slot] = it.id;\n }\n const art = worn.overlay ? COSMETICS_BY_ID.get(worn.overlay)?.art : void 0;\n if (art && src.placement && typeof src.placement === \"object\") {\n worn.placement = fitPlacement(art, src.placement);\n }\n return Object.keys(worn).length ? worn : void 0;\n}\nfunction cleanLoadout(raw, owned) {\n const out = emptyLoadout();\n const src = raw && typeof raw === \"object\" ? raw : {};\n for (const slot of COSMETIC_SLOTS) {\n const id = src.equipped?.[slot];\n const it = typeof id === \"string\" ? COSMETICS_BY_ID.get(id) : void 0;\n if (it && !it.hidden && owned.has(it.id) && it.slot === slot) {\n out.equipped[slot] = it.id;\n }\n }\n for (const [id, p] of Object.entries(src.placement ?? {})) {\n const it = COSMETICS_BY_ID.get(id);\n const art = it?.art;\n if (art && !it.hidden && owned.has(id) && p && typeof p === \"object\") {\n out.placement[id] = fitPlacement(art, p);\n }\n }\n return out;\n}\nfunction wornCosmetics(loadout) {\n const worn = {};\n for (const slot of COSMETIC_SLOTS) {\n const id = loadout.equipped[slot];\n if (id) worn[slot] = id;\n }\n const overlay = loadout.equipped.overlay;\n if (overlay && COSMETICS_BY_ID.get(overlay)?.art && loadout.placement[overlay]) {\n worn.placement = loadout.placement[overlay];\n }\n return Object.keys(worn).length ? worn : void 0;\n}\n\n// src/cosmetics.ts\nvar COSMETICS_ASSET_BASE = \"/cosmetics\";\nvar STYLE_ID = \"hadoku-cosmetics\";\nvar esc = (s) => s.replace(\n /[&<>\"']/g,\n (c) => ({ \"&\": \"&\", \"<\": \"<\", \">\": \">\", '\"': \""\", \"'\": \"'\" })[c]\n);\nvar nameOf = (id) => id ? id.slice(id.indexOf(\".\") + 1) : \"none\";\nfunction colourStyle(hex) {\n return hex && /^#[0-9a-f]{6}$/i.test(hex) ? `--hc-base:${hex}` : \"\";\n}\nfunction placementTransform(p) {\n const sx = p.s / 100 * (p.flip ? -1 : 1);\n return `translate(calc(var(--hc-sz) * ${(p.x / 100).toFixed(3)}),calc(var(--hc-sz) * ${(p.y / 100).toFixed(3)})) rotate(${p.r}deg) scale(${sx.toFixed(3)},${(p.s / 100).toFixed(3)})`;\n}\nvar SPLIT_EFFECTS = /* @__PURE__ */ new Set([\"hop\"]);\nfunction nameHtml(text, worn, opts = {}) {\n const fx = nameOf(worn?.effect);\n const gr = nameOf(worn?.gradient);\n const font = nameOf(worn?.font);\n const t = esc(text);\n const split = SPLIT_EFFECTS.has(fx);\n const tx = split ? `<span class=\"hc-tx\">${[...text].map(\n (c, i) => `<span class=\"hc-ch\" style=\"--hc-i:${i}\"><span class=\"hc-c\">${c === \" \" ? \" \" : esc(c)}</span></span>`\n ).join(\"\")}</span>` : `<span class=\"hc-tx\">${t}</span>`;\n const layer = (cls2) => `<span class=\"hc-lyr${cls2}\" aria-hidden=\"true\">${t}</span>`;\n let back = \"\";\n let front = \"\";\n if (fx === \"aura\" || fx === \"outline\") back += layer(\" hc-g\");\n if (fx === \"glint\") front += `<span class=\"hc-win\" aria-hidden=\"true\">${layer(\"\")}</span>`;\n if (fx === \"twinkle\") {\n front += [\n [-6, -25, 0],\n [52, 55, 0.95],\n [94, -15, 1.9]\n ].map(\n ([x, y, d]) => `<i class=\"hc-tw\" aria-hidden=\"true\" style=\"left:${x}%;top:${y}%;animation-delay:${d}s\"></i>`\n ).join(\"\");\n }\n const cls = `hc-name hc-fx-${fx} hc-gr-${gr} hc-font-${font}${split ? \" hc-split\" : \"\"}${opts.large ? \" hc-large\" : \"\"}`;\n return `<span class=\"${cls}\">${back}${tx}${front}</span>`;\n}\nvar RUNES_SVG = (() => {\n let marks = \"\";\n for (let i = 0; i < 12; i++) {\n marks += `<g transform=\"rotate(${i * 30} 50 50)\"><path d=\"M50 3 l2.5 4 -2.5 4 -2.5 -4z\" fill=\"currentColor\" opacity=\"${i % 3 ? 0.55 : 1}\"/></g>`;\n }\n return `<svg viewBox=\"0 0 100 100\" aria-hidden=\"true\"><circle cx=\"50\" cy=\"50\" r=\"44\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1\" stroke-dasharray=\"3 5\" opacity=\".7\"/>${marks}</svg>`;\n})();\nfunction decorHtml(worn, assetBase = COSMETICS_ASSET_BASE) {\n const id = worn?.overlay;\n if (!id) return \"\";\n const art = COSMETICS_BY_ID.get(id)?.art;\n if (art) {\n const p = worn?.placement ?? PLACEMENT_DEFAULT;\n return `<span class=\"hc-ol hc-art${art.pixel ? \" hc-px\" : \"\"}\" data-hc-art=\"${esc(id)}\" style=\"left:${50 - art.width / 2}%;top:${art.top}%;width:${art.width}%;z-index:${p.back ? 0 : 3}\"><span class=\"hc-adj\" style=\"transform:${placementTransform(p)}\"><img alt=\"\" draggable=\"false\" style=\"--hc-tilt:${art.tilt}deg\" src=\"${esc(`${assetBase}/art/${art.file}`)}\"></span></span>`;\n }\n switch (nameOf(id)) {\n case \"runes\":\n return `<span class=\"hc-ol hc-runes\">${RUNES_SVG}</span>`;\n case \"comet\":\n return `<span class=\"hc-ol hc-comet\"></span>`;\n case \"shine\":\n return `<span class=\"hc-ol hc-clip\"><span class=\"hc-band\"></span></span>`;\n case \"glow\":\n return `<span class=\"hc-ol hc-glow\"></span>`;\n case \"petals\":\n return [15, 40, 62, 85].map(\n (l, i) => `<i class=\"hc-ol hc-petal\" style=\"left:${l}%;animation-delay:${(i * 0.85).toFixed(2)}s\"></i>`\n ).join(\"\");\n default:\n return \"\";\n }\n}\nfunction avatarHtml(o) {\n const pic = o.src ? `<img alt=\"\" src=\"${esc(o.src)}\">` : \"\";\n return `<span class=\"hc-av\" style=\"--hc-sz:${o.size}px\"><span class=\"hc-pic\">${pic}</span>${decorHtml(o.worn, o.assetBase)}</span>`;\n}\nvar GLYPHS = \"!<>-_/[]{}=+*^?#%&01\";\nvar LETTER_MODES = /* @__PURE__ */ new Set([\"type\", \"decode\", \"cascade\"]);\nvar REVEAL_MS = 1400;\nvar LETTER_LIMIT = 160;\nfunction messageHtml(text, worn, opts = {}) {\n let mode = nameOf(worn?.message);\n if (mode === \"none\") return `<span class=\"hc-msg\">${esc(text)}</span>`;\n const words = text.split(/\\s+/).filter(Boolean);\n if (LETTER_MODES.has(mode) && text.length > LETTER_LIMIT) mode = \"words\";\n const keep = opts.keep ? \" data-hc-keep\" : \"\";\n let body;\n let step;\n if (LETTER_MODES.has(mode)) {\n step = Math.min(38, REVEAL_MS / Math.max(1, text.length));\n let n = 0;\n const rnd = () => GLYPHS[Math.floor(Math.random() * GLYPHS.length)];\n body = words.map(\n (w) => `<span class=\"hc-w\">${[...w].map((c) => {\n const i = n++;\n return mode === \"decode\" ? `<span class=\"hc-ch\" style=\"--hc-i:${i}\" data-g=\"${esc(rnd())}\" data-h=\"${esc(rnd())}\"><span class=\"hc-r\">${esc(c)}</span></span>` : `<span class=\"hc-ch\" style=\"--hc-i:${i}\">${esc(c)}</span>`;\n }).join(\"\")}</span>`\n ).join(\" \");\n if (mode === \"type\") body += `<span class=\"hc-caret\" style=\"--hc-i:${n}\"></span>`;\n const lastClass = mode === \"type\" ? \"hc-caret\" : mode === \"decode\" ? \"hc-r\" : \"hc-ch\";\n const at = body.lastIndexOf(`<span class=\"${lastClass}\"`);\n body = `${body.slice(0, at)}<span data-hc-last class=\"${lastClass}\"${body.slice(at + `<span class=\"${lastClass}\"`.length)}`;\n } else {\n step = Math.min(90, REVEAL_MS / Math.max(1, words.length));\n body = words.map(\n (w, i) => `<span class=\"hc-w\" style=\"--hc-i:${i}\"${i === words.length - 1 ? \" data-hc-last\" : \"\"}>${esc(w)}</span>`\n ).join(\" \");\n }\n return `<span class=\"hc-msg hc-mt-${mode}\" style=\"--hc-step:${step.toFixed(1)}ms\" data-text=\"${esc(text)}\"${keep}><span class=\"hc-sr\">${esc(text)}</span><span aria-hidden=\"true\">${body}</span></span>`;\n}\nfunction finishReveal(e) {\n const t = e.target;\n if (e.pseudoElement || !t?.hasAttribute?.(\"data-hc-last\")) return;\n const msg = t.closest(\".hc-msg\");\n if (!msg || msg.hasAttribute(\"data-hc-keep\")) return;\n msg.className = \"hc-msg\";\n msg.removeAttribute(\"style\");\n msg.textContent = msg.getAttribute(\"data-text\") ?? \"\";\n}\nvar FONTS = {\n script: {\n file: \"caveat\",\n family: \"Caveat\",\n generic: \"cursive\",\n css: \"font-weight:700;font-size:1.3em;line-height:1\"\n },\n surf: {\n file: \"pacifico\",\n family: \"Pacifico\",\n generic: \"cursive\",\n css: \"font-weight:400;font-size:1.02em;line-height:1.1\"\n },\n gothic: {\n file: \"gothic\",\n family: \"Grenze Gotisch\",\n generic: \"serif\",\n css: \"font-weight:600;font-size:1.15em\"\n },\n runic: { file: \"uncial\", family: \"Uncial Antiqua\", generic: \"serif\", css: \"font-weight:400\" },\n corsair: {\n file: \"pirata\",\n family: \"Pirata One\",\n generic: \"serif\",\n css: \"font-weight:400;font-size:1.12em;letter-spacing:.02em\"\n },\n metal: {\n file: \"metal\",\n family: \"Metal Mania\",\n generic: \"cursive\",\n css: \"font-weight:400;font-size:1.12em;letter-spacing:.02em\"\n },\n comic: {\n file: \"bangers\",\n family: \"Bangers\",\n generic: \"cursive\",\n css: \"font-weight:400;font-size:1.15em;letter-spacing:.05em\"\n },\n circuit: {\n file: \"orbitron\",\n family: \"Orbitron\",\n generic: \"sans-serif\",\n css: \"font-weight:700;letter-spacing:.03em\"\n },\n hover: {\n file: \"audiowide\",\n family: \"Audiowide\",\n generic: \"sans-serif\",\n css: \"font-weight:400;font-size:.92em\"\n },\n retro: { file: \"righteous\", family: \"Righteous\", generic: \"sans-serif\", css: \"font-weight:400\" },\n arcade: {\n file: \"bungee\",\n family: \"Bungee\",\n generic: \"sans-serif\",\n css: \"font-weight:400;font-size:.9em\"\n },\n pixel: {\n file: \"pixel\",\n family: \"Press Start 2P\",\n generic: \"monospace\",\n css: \"font-weight:400;font-size:.62em;line-height:1.6\"\n }\n};\nfunction cosmeticsCss(assetBase = COSMETICS_ASSET_BASE) {\n const fontFaces = Object.entries(FONTS).map(\n ([k, f]) => `@font-face{font-family:\"hc-${k}\";src:url(\"${assetBase}/fonts/${f.file}.woff2\") format(\"woff2\");font-display:swap}`\n ).join(\"\\n\");\n const fontRules = Object.entries(FONTS).map(([k, f]) => `.hc-font-${k}{font-family:\"hc-${k}\",\"${f.family}\",${f.generic};${f.css}}`).join(\"\\n\");\n return `${fontFaces}\n/* Colours: everything derives from the character colour. */\n.hc-name,.hc-av,.hc-msg{--hc-b:var(--hc-base,currentColor);--hc-hi:color-mix(in oklch,var(--hc-b),white 50%);--hc-lo:color-mix(in oklch,var(--hc-b),black 50%);--hc-glow:color-mix(in oklch,var(--hc-b) 75%,transparent)}\n\n/* --- name ---\n Bleed: a gradient is a background clipped to the letters, and a background covers only the box; script\n swashes reach outside it. So the text box and every layer over it get room on all sides, cancelled by a\n negative margin so nothing moves. */\n.hc-name{--hc-bx:.4em;--hc-by:.3em;--hc-grad:none;position:relative;display:inline-block;white-space:nowrap;color:var(--hc-b);font-weight:700;line-height:1.25}\n.hc-name.hc-large{font-size:1.6em}\n.hc-name .hc-tx{position:relative;z-index:1;display:inline-block;padding:var(--hc-by) var(--hc-bx);margin:calc(-1 * var(--hc-by)) calc(-1 * var(--hc-bx))}\n.hc-name .hc-lyr{position:absolute;left:calc(-1 * var(--hc-bx));top:calc(-1 * var(--hc-by));padding:var(--hc-by) var(--hc-bx);pointer-events:none;white-space:nowrap}\n.hc-name .hc-win{position:absolute;inset:calc(-1 * var(--hc-by)) calc(-1 * var(--hc-bx));overflow:hidden}\n.hc-name .hc-win .hc-lyr{left:0;top:0}\n.hc-name .hc-ch,.hc-name .hc-c{display:inline-block}\n.hc-gr-gild{--hc-grad:linear-gradient(180deg,var(--hc-hi) 0%,var(--hc-b) 55%,var(--hc-lo) 100%)}\n.hc-gr-gloss{--hc-grad:linear-gradient(180deg,color-mix(in oklch,var(--hc-hi),white 35%) 0%,var(--hc-hi) 36%,color-mix(in oklch,var(--hc-hi),var(--hc-b)) 48%,var(--hc-b) 60%,var(--hc-lo) 100%)}\n.hc-gr-split{--hc-grad:linear-gradient(180deg,var(--hc-hi) 0 52%,var(--hc-b) 52% 100%)}\n.hc-gr-brushed{--hc-grad:repeating-linear-gradient(90deg,transparent 0 1px,color-mix(in oklch,var(--hc-hi) 40%,transparent) 1px 2px),linear-gradient(180deg,var(--hc-hi),var(--hc-b) 55%,var(--hc-lo))}\n.hc-gr-lumen{--hc-grad:radial-gradient(ellipse 60% 90% at 50% 35%,var(--hc-hi),var(--hc-b) 55%,var(--hc-lo) 100%)}\n.hc-name:not(.hc-gr-none) .hc-tx{background:var(--hc-grad);-webkit-background-clip:text;background-clip:text;color:transparent}\n.hc-name.hc-split:not(.hc-gr-none) .hc-tx{background:none}\n.hc-name.hc-split:not(.hc-gr-none) .hc-tx .hc-c{padding:var(--hc-by) var(--hc-bx);margin:calc(-1 * var(--hc-by)) calc(-1 * var(--hc-bx));background:var(--hc-grad);-webkit-background-clip:text;background-clip:text;color:transparent}\n.hc-fx-glint .hc-win{z-index:3;animation:hc-win-go 4.5s cubic-bezier(.4,0,.2,1) infinite;-webkit-mask-image:linear-gradient(100deg,transparent 35%,#000 50%,transparent 65%);mask-image:linear-gradient(100deg,transparent 35%,#000 50%,transparent 65%)}\n.hc-fx-glint .hc-win .hc-lyr{color:var(--hc-hi);animation:hc-win-back 4.5s cubic-bezier(.4,0,.2,1) infinite}\n@keyframes hc-win-go{0%{transform:translateX(-100%)}55%,100%{transform:translateX(100%)}}\n@keyframes hc-win-back{0%{transform:translateX(100%)}55%,100%{transform:translateX(-100%)}}\n.hc-fx-aura .hc-lyr.hc-g{color:transparent;text-shadow:0 0 6px var(--hc-glow),0 0 14px var(--hc-glow);animation:hc-breathe 3s ease-in-out infinite}\n.hc-fx-outline .hc-lyr.hc-g{color:transparent;-webkit-text-stroke:3px var(--hc-hi);animation:hc-breathe 3.6s ease-in-out infinite}\n@keyframes hc-breathe{0%,100%{opacity:.25}50%{opacity:1}}\n.hc-fx-hop .hc-c{animation:hc-hop 3.6s ease-in-out calc(var(--hc-i) * 45ms) infinite}\n@keyframes hc-hop{0%,14%,100%{transform:none}5%{transform:translateY(-4px)}10%{transform:translateY(.5px)}}\n.hc-fx-twinkle .hc-tw{position:absolute;width:7px;height:7px;z-index:3;pointer-events:none;background:linear-gradient(var(--hc-hi),var(--hc-hi)) center/1.4px 100% no-repeat,linear-gradient(90deg,var(--hc-hi),var(--hc-hi)) center/100% 1.4px no-repeat;animation:hc-star 2.8s ease-in-out infinite both}\n@keyframes hc-star{0%,100%{opacity:0;transform:scale(.3) rotate(0deg)}25%{opacity:1;transform:scale(1) rotate(45deg)}50%{opacity:0;transform:scale(.3) rotate(90deg)}}\n.hc-font-caps{font-variant:small-caps;letter-spacing:.08em}\n.hc-font-wide{letter-spacing:.22em;font-weight:600;text-transform:uppercase;font-size:.82em}\n${fontRules}\n\n/* --- avatar --- */\n.hc-av{--hc-sz:40px;position:relative;display:inline-block;width:var(--hc-sz);height:var(--hc-sz);flex:none;isolation:isolate;vertical-align:middle}\n.hc-av .hc-pic{position:absolute;inset:0;border-radius:50%;overflow:hidden;z-index:1;background:color-mix(in oklch,var(--hc-b) 30%,transparent)}\n.hc-av .hc-pic img{width:100%;height:100%;display:block;object-fit:cover;image-rendering:pixelated}\n.hc-av .hc-ol{position:absolute;pointer-events:none}\n.hc-ol.hc-art{display:block}\n.hc-ol.hc-art .hc-adj{display:block;position:relative;transform-origin:50% 60%}\n.hc-ol.hc-art img{display:block;width:100%;height:auto;filter:drop-shadow(0 1px 1px rgba(0,0,0,.45));animation:hc-art-bob 3.4s ease-in-out infinite;-webkit-user-drag:none;user-select:none}\n.hc-ol.hc-art.hc-px img{image-rendering:pixelated}\n@keyframes hc-art-bob{0%,100%{transform:rotate(var(--hc-tilt,0deg)) translateY(0)}50%{transform:rotate(var(--hc-tilt,0deg)) translateY(-2px)}}\n.hc-ol.hc-runes{inset:-9px;z-index:2;color:var(--hc-hi);animation:hc-turn 14s linear infinite}\n.hc-ol.hc-runes svg{width:100%;height:100%;display:block}\n@keyframes hc-turn{to{transform:rotate(360deg)}}\n.hc-ol.hc-comet{inset:-6px;z-index:2;border-radius:50%;background:conic-gradient(from 0deg,transparent 0 270deg,color-mix(in oklch,var(--hc-hi) 10%,transparent) 300deg,var(--hc-hi) 358deg,transparent 360deg);-webkit-mask:radial-gradient(farthest-side,transparent calc(100% - 3px),#000 calc(100% - 3px));mask:radial-gradient(farthest-side,transparent calc(100% - 3px),#000 calc(100% - 3px));animation:hc-turn 2.4s linear infinite}\n.hc-ol.hc-clip{inset:0;border-radius:50%;overflow:hidden;z-index:2}\n.hc-ol .hc-band{position:absolute;top:-30%;left:0;width:45%;height:160%;background:linear-gradient(90deg,transparent,color-mix(in oklch,var(--hc-hi) 70%,transparent),transparent);transform:translateX(-130%) rotate(20deg);animation:hc-band 5s ease-in-out infinite}\n@keyframes hc-band{0%{transform:translateX(-130%) rotate(20deg)}45%,100%{transform:translateX(260%) rotate(20deg)}}\n.hc-ol.hc-glow{inset:-30%;border-radius:50%;z-index:0;background:radial-gradient(circle,var(--hc-glow) 0,transparent 65%);animation:hc-glowp 3.2s ease-in-out infinite}\n@keyframes hc-glowp{0%,100%{opacity:.35;transform:scale(.92)}50%{opacity:.9;transform:scale(1.04)}}\n.hc-ol.hc-petal{top:-8%;height:100%;width:5px;z-index:2;animation:hc-fall 3.4s linear infinite both}\n.hc-ol.hc-petal::after{content:\"\";position:absolute;left:0;top:0;width:5px;height:7px;border-radius:5px 0;background:#f7b6c8;transform:rotate(35deg)}\n@keyframes hc-fall{0%{transform:translate(0,0);opacity:0}12%{opacity:.9}100%{transform:translate(-6px,100%);opacity:0}}\n\n/* --- message reveal: fill backwards only; the page swaps back to plain text when it ends --- */\n.hc-sr{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap}\n.hc-msg .hc-w{display:inline-block;white-space:nowrap}\n.hc-msg .hc-ch{display:inline-block}\n.hc-mt-type .hc-ch{animation:hc-in .01s linear calc(var(--hc-i) * var(--hc-step)) backwards}\n.hc-mt-type .hc-caret{display:inline-block;width:2px;height:1em;margin-left:2px;vertical-align:-2px;background:var(--hc-b);opacity:0;animation:hc-caret 1.5s steps(1) calc(var(--hc-i) * var(--hc-step)) backwards}\n@keyframes hc-caret{0%{opacity:0}1%{opacity:1}20%{opacity:0}40%{opacity:1}60%{opacity:0}80%{opacity:1}100%{opacity:0}}\n.hc-mt-cascade .hc-ch{animation:hc-drop .35s cubic-bezier(.3,1.5,.5,1) calc(var(--hc-i) * var(--hc-step)) backwards}\n.hc-mt-decode .hc-ch{position:relative}\n.hc-mt-decode .hc-ch .hc-r{display:inline-block;animation:hc-in .01s linear calc(var(--hc-i) * var(--hc-step) + 260ms) backwards}\n.hc-mt-decode .hc-ch::before,.hc-mt-decode .hc-ch::after{position:absolute;left:0;top:0;color:var(--hc-b);opacity:0;animation:hc-dec-a .26s steps(1) calc(var(--hc-i) * var(--hc-step)) backwards}\n.hc-mt-decode .hc-ch::before{content:attr(data-g)}\n.hc-mt-decode .hc-ch::after{content:attr(data-h);animation-name:hc-dec-b}\n@keyframes hc-dec-a{0%{opacity:.85}50%,100%{opacity:0}}\n@keyframes hc-dec-b{0%{opacity:0}50%{opacity:.85}100%{opacity:0}}\n.hc-mt-words .hc-w{animation:hc-rise .4s ease-out calc(var(--hc-i) * var(--hc-step)) backwards}\n.hc-mt-bubble .hc-w{animation:hc-pop .35s cubic-bezier(.3,1.6,.5,1) calc(var(--hc-i) * var(--hc-step)) backwards}\n.hc-mt-drift .hc-w{animation:hc-slide .45s cubic-bezier(.2,.8,.2,1) calc(var(--hc-i) * var(--hc-step)) backwards}\n@keyframes hc-in{from{opacity:0}to{opacity:1}}\n@keyframes hc-drop{0%{transform:translateY(-10px);opacity:0}100%{transform:none;opacity:1}}\n@keyframes hc-rise{0%{transform:translateY(10px);opacity:0}100%{transform:none;opacity:1}}\n@keyframes hc-pop{0%{transform:scale(.6);opacity:0}100%{transform:scale(1);opacity:1}}\n@keyframes hc-slide{0%{transform:translateX(-18px);opacity:0}100%{transform:none;opacity:1}}\n\n.hc-idle .hc-name,.hc-idle .hc-name *,.hc-idle .hc-av *,.hc-idle .hc-av *::after{animation-play-state:paused!important}\n@media (prefers-reduced-motion:reduce){.hc-name *,.hc-av *,.hc-av *::after,.hc-msg *,.hc-msg *::before,.hc-msg *::after{animation:none!important}}\n`;\n}\nfunction ensureCosmeticsStyles(doc = document, assetBase = COSMETICS_ASSET_BASE) {\n if (doc.getElementById(STYLE_ID)) return;\n const style = doc.createElement(\"style\");\n style.id = STYLE_ID;\n style.textContent = cosmeticsCss(assetBase);\n doc.head.appendChild(style);\n doc.addEventListener(\"animationend\", finishReveal);\n}\nexport {\n COSMETICS,\n COSMETICS_ASSET_BASE,\n COSMETICS_BY_ID,\n COSMETIC_ID_PATTERN,\n COSMETIC_SLOTS,\n EARNED_COSMETICS,\n PLACEMENT_DEFAULT,\n PLACEMENT_LIMITS,\n PLACEMENT_ZONE,\n avatarHtml,\n cleanLoadout,\n cleanWorn,\n colourStyle,\n cosmeticsCss,\n decorHtml,\n emptyLoadout,\n ensureCosmeticsStyles,\n finishReveal,\n fitPlacement,\n messageHtml,\n nameHtml,\n overlayBounds,\n ownedFrom,\n placementTransform,\n wornCosmetics\n};\n"],"names":[],"mappings":"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","x_google_ignoreList":[0]}
|
|
1
|
+
{"version":3,"file":"cosmetics.js","sources":["../../../../../node_modules/.pnpm/@wolffm+prefs-client@1.1.18_react@19.2.8_zod@4.6.7/node_modules/@wolffm/prefs-client/dist/cosmetics.js"],"sourcesContent":["// src/cosmetics-contract.ts\nvar COSMETIC_SLOTS = [\"effect\", \"gradient\", \"font\", \"overlay\", \"message\"];\nvar COSMETIC_ID_PATTERN = /^(effect|gradient|font|overlay|message)\\.[a-z0-9-]{1,32}$/;\nvar item = (slot, name, label, opts = {}) => ({ id: `${slot}.${name}`, slot, label, ...opts });\nvar archived = { hidden: true };\nvar COSMETICS = [\n item(\"effect\", \"glint\", \"Glint\"),\n item(\"effect\", \"aura\", \"Aura\"),\n item(\"effect\", \"hop\", \"Hop\"),\n item(\"effect\", \"outline\", \"Outline\"),\n item(\"effect\", \"twinkle\", \"Twinkle\", { earn: \"Bronze or better in a Major\" }),\n item(\"gradient\", \"gild\", \"Gilded\"),\n item(\"gradient\", \"gloss\", \"Gloss\"),\n item(\"gradient\", \"split\", \"Split\"),\n item(\"gradient\", \"brushed\", \"Brushed\"),\n item(\"gradient\", \"lumen\", \"Lumen\"),\n item(\"font\", \"script\", \"Script\"),\n item(\"font\", \"surf\", \"Surf\"),\n item(\"font\", \"gothic\", \"Gothic\"),\n item(\"font\", \"runic\", \"Runic\"),\n item(\"font\", \"arcade\", \"Arcade\"),\n item(\"font\", \"caps\", \"Small caps\", archived),\n item(\"font\", \"wide\", \"Wide\", archived),\n item(\"font\", \"corsair\", \"Corsair\", archived),\n item(\"font\", \"metal\", \"Metal\", archived),\n item(\"font\", \"comic\", \"Comic\", archived),\n item(\"font\", \"circuit\", \"Circuit\", archived),\n item(\"font\", \"hover\", \"Hover\", archived),\n item(\"font\", \"retro\", \"Retro\", archived),\n item(\"font\", \"pixel\", \"Pixel\", archived),\n item(\"overlay\", \"crown\", \"Crown\", {\n earn: \"Win a Major\",\n art: {\n file: \"crown.webp\",\n top: -40,\n width: 70,\n tilt: -8,\n pixel: false,\n visible: [0.027, 0.137, 0.973, 0.863]\n }\n }),\n item(\"overlay\", \"halo\", \"Halo\", {\n art: {\n file: \"halo.webp\",\n top: -34,\n width: 76,\n tilt: 0,\n pixel: true,\n visible: [0.033, 0.333, 0.967, 0.667]\n }\n }),\n item(\"overlay\", \"flowers\", \"Flower crown\", {\n art: {\n file: \"flowers.webp\",\n top: -22,\n width: 96,\n tilt: 0,\n pixel: false,\n visible: [0.027, 0.258, 0.969, 0.746]\n }\n }),\n item(\"overlay\", \"petals\", \"Petals\"),\n item(\"overlay\", \"horns\", \"Horns\", {\n ...archived,\n art: {\n file: \"horns.webp\",\n top: -26,\n width: 100,\n tilt: 0,\n pixel: true,\n visible: [0.033, 0.133, 0.967, 0.867]\n }\n }),\n item(\"overlay\", \"runes\", \"Runes\", archived),\n item(\"overlay\", \"comet\", \"Comet\", archived),\n item(\"overlay\", \"shine\", \"Shine\", archived),\n item(\"overlay\", \"glow\", \"Glow\", archived),\n item(\"message\", \"type\", \"Typewriter\"),\n item(\"message\", \"decode\", \"Decode\", { earn: \"Silver or better in a Major\" }),\n item(\"message\", \"cascade\", \"Cascade\"),\n item(\"message\", \"words\", \"Word by word\"),\n item(\"message\", \"bubble\", \"Bubble\", archived),\n item(\"message\", \"drift\", \"Drift\")\n];\nvar COSMETICS_BY_ID = new Map(\n COSMETICS.map((i) => [i.id, i])\n);\nvar EARNED_COSMETICS = COSMETICS.filter(\n (i) => i.earn !== void 0 && !i.hidden\n);\nfunction ownedFrom(granted) {\n const out = new Set(COSMETICS.filter((i) => !i.hidden && i.earn === void 0).map((i) => i.id));\n for (const id of granted) {\n const it = COSMETICS_BY_ID.get(id);\n if (it && !it.hidden) out.add(id);\n }\n return out;\n}\nvar PLACEMENT_DEFAULT = { x: 0, y: 0, r: 0, s: 100, flip: false, back: false };\nvar PLACEMENT_LIMITS = {\n x: [-60, 60],\n y: [-60, 100],\n r: [-45, 45],\n s: [70, 140]\n};\nfunction overlayBounds(art, p) {\n const [a, b, c, d] = art.visible;\n const x0 = p.flip ? 1 - c : a;\n const x1 = p.flip ? 1 - a : c;\n const L = 50 - art.width / 2;\n const T = art.top;\n const W = art.width;\n const ox = L + W * 0.5;\n const oy = T + W * 0.6;\n const cx = L + W / 2;\n const cy = T + W / 2;\n const t = art.tilt * Math.PI / 180;\n const r = p.r * Math.PI / 180;\n const sc = p.s / 100;\n const xs = [];\n const ys = [];\n for (const [fx, fy] of [\n [x0, b],\n [x1, b],\n [x0, d],\n [x1, d]\n ]) {\n const px = L + fx * W;\n const py = T + fy * W;\n const tx = cx + (px - cx) * Math.cos(t) - (py - cy) * Math.sin(t);\n const ty = cy + (px - cx) * Math.sin(t) + (py - cy) * Math.cos(t);\n const dx = (tx - ox) * sc;\n const dy = (ty - oy) * sc;\n xs.push(ox + p.x + dx * Math.cos(r) - dy * Math.sin(r));\n ys.push(oy + p.y + dx * Math.sin(r) + dy * Math.cos(r));\n }\n return {\n left: Math.min(...xs),\n top: Math.min(...ys),\n right: Math.max(...xs),\n bottom: Math.max(...ys)\n };\n}\nvar ART_ITEMS = COSMETICS.filter((i) => !!i.art);\nvar PLACEMENT_ZONE = {\n left: -25,\n top: Math.floor(Math.min(...ART_ITEMS.map((i) => overlayBounds(i.art, PLACEMENT_DEFAULT).top))),\n right: 125,\n bottom: 110\n};\nvar clamp = (v, [lo, hi]) => Math.min(hi, Math.max(lo, v));\nfunction fitPlacement(art, raw) {\n const num = (v, fallback) => typeof v === \"number\" && Number.isFinite(v) ? v : fallback;\n const p = {\n x: Math.round(clamp(num(raw.x, 0), PLACEMENT_LIMITS.x)),\n y: Math.round(clamp(num(raw.y, 0), PLACEMENT_LIMITS.y)),\n r: Math.round(clamp(num(raw.r, 0), PLACEMENT_LIMITS.r)),\n s: Math.round(clamp(num(raw.s, 100), PLACEMENT_LIMITS.s)),\n flip: raw.flip === true,\n back: raw.back === true\n };\n const z = PLACEMENT_ZONE;\n let box = overlayBounds(art, p);\n const room = Math.min(\n (z.right - z.left - 2) / (box.right - box.left),\n (z.bottom - z.top - 2) / (box.bottom - box.top)\n );\n if (room < 1) {\n p.s = Math.max(PLACEMENT_LIMITS.s[0], Math.floor(p.s * room));\n box = overlayBounds(art, p);\n }\n p.x += Math.ceil(Math.max(0, z.left - box.left)) - Math.ceil(Math.max(0, box.right - z.right));\n p.y += Math.ceil(Math.max(0, z.top - box.top)) - Math.ceil(Math.max(0, box.bottom - z.bottom));\n return p;\n}\nfunction emptyLoadout() {\n return {\n equipped: { effect: null, gradient: null, font: null, overlay: null, message: null },\n placement: {}\n };\n}\nfunction cleanWorn(raw) {\n if (!raw || typeof raw !== \"object\") return void 0;\n const src = raw;\n const worn = {};\n for (const slot of COSMETIC_SLOTS) {\n const id = src[slot];\n const it = typeof id === \"string\" ? COSMETICS_BY_ID.get(id) : void 0;\n if (it && !it.hidden && it.slot === slot) worn[slot] = it.id;\n }\n const art = worn.overlay ? COSMETICS_BY_ID.get(worn.overlay)?.art : void 0;\n if (art && src.placement && typeof src.placement === \"object\") {\n worn.placement = fitPlacement(art, src.placement);\n }\n return Object.keys(worn).length ? worn : void 0;\n}\nfunction cleanLoadout(raw, owned) {\n const out = emptyLoadout();\n const src = raw && typeof raw === \"object\" ? raw : {};\n for (const slot of COSMETIC_SLOTS) {\n const id = src.equipped?.[slot];\n const it = typeof id === \"string\" ? COSMETICS_BY_ID.get(id) : void 0;\n if (it && !it.hidden && owned.has(it.id) && it.slot === slot) {\n out.equipped[slot] = it.id;\n }\n }\n for (const [id, p] of Object.entries(src.placement ?? {})) {\n const it = COSMETICS_BY_ID.get(id);\n const art = it?.art;\n if (art && !it.hidden && owned.has(id) && p && typeof p === \"object\") {\n out.placement[id] = fitPlacement(art, p);\n }\n }\n return out;\n}\nfunction wornCosmetics(loadout) {\n const worn = {};\n for (const slot of COSMETIC_SLOTS) {\n const id = loadout.equipped[slot];\n if (id) worn[slot] = id;\n }\n const overlay = loadout.equipped.overlay;\n if (overlay && COSMETICS_BY_ID.get(overlay)?.art && loadout.placement[overlay]) {\n worn.placement = loadout.placement[overlay];\n }\n return Object.keys(worn).length ? worn : void 0;\n}\n\n// src/picture-colour.ts\nvar MIN_LIGHTNESS = 0.3;\nvar MAX_LIGHTNESS = 0.82;\nvar SAMPLE_SIZE = 24;\nvar MIN_ALPHA = 32;\nfunction averagePictureColour(pixels) {\n let r = 0;\n let g = 0;\n let b = 0;\n let weight = 0;\n for (let i = 0; i + 3 < pixels.length; i += 4) {\n const a = pixels[i + 3];\n if (a < MIN_ALPHA) continue;\n r += pixels[i] * a;\n g += pixels[i + 1] * a;\n b += pixels[i + 2] * a;\n weight += a;\n }\n if (weight === 0) return null;\n return clampLightness(r / weight, g / weight, b / weight);\n}\nfunction clampLightness(r, g, b) {\n const [h, s, l] = rgbToHsl(r / 255, g / 255, b / 255);\n const clamped = Math.min(MAX_LIGHTNESS, Math.max(MIN_LIGHTNESS, l));\n const [r2, g2, b2] = hslToRgb(h, s, clamped);\n const hex = (v) => Math.round(v * 255).toString(16).padStart(2, \"0\");\n return `#${hex(r2)}${hex(g2)}${hex(b2)}`;\n}\nfunction rgbToHsl(r, g, b) {\n const max = Math.max(r, g, b);\n const min = Math.min(r, g, b);\n const l = (max + min) / 2;\n if (max === min) return [0, 0, l];\n const d = max - min;\n const s = l > 0.5 ? d / (2 - max - min) : d / (max + min);\n let h;\n if (max === r) h = (g - b) / d + (g < b ? 6 : 0);\n else if (max === g) h = (b - r) / d + 2;\n else h = (r - g) / d + 4;\n return [h / 6, s, l];\n}\nfunction hslToRgb(h, s, l) {\n if (s === 0) return [l, l, l];\n const q = l < 0.5 ? l * (1 + s) : l + s - l * s;\n const p = 2 * l - q;\n const channel = (t) => {\n if (t < 0) t += 1;\n if (t > 1) t -= 1;\n if (t < 1 / 6) return p + (q - p) * 6 * t;\n if (t < 1 / 2) return q;\n if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;\n return p;\n };\n return [channel(h + 1 / 3), channel(h), channel(h - 1 / 3)];\n}\nvar colours = /* @__PURE__ */ new Map();\nvar pending = /* @__PURE__ */ new Map();\nvar listeners = /* @__PURE__ */ new Set();\nfunction pictureColour(url) {\n return colours.get(url);\n}\nfunction onPictureColour(listener) {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n}\nfunction readPictureColour(url) {\n if (colours.has(url)) return Promise.resolve(colours.get(url) ?? null);\n const inFlight = pending.get(url);\n if (inFlight) return inFlight;\n const read = sample(url).then((colour) => {\n pending.delete(url);\n colours.set(url, colour);\n for (const l of listeners) l();\n return colour;\n });\n pending.set(url, read);\n return read;\n}\nfunction sample(url) {\n if (typeof Image === \"undefined\" || typeof document === \"undefined\") {\n return Promise.resolve(null);\n }\n return new Promise((resolve) => {\n const img = new Image();\n img.crossOrigin = \"anonymous\";\n img.referrerPolicy = \"no-referrer\";\n img.onload = () => {\n const canvas = document.createElement(\"canvas\");\n canvas.width = SAMPLE_SIZE;\n canvas.height = SAMPLE_SIZE;\n const ctx = canvas.getContext(\"2d\", { willReadFrequently: true });\n if (!ctx) {\n resolve(null);\n return;\n }\n ctx.drawImage(img, 0, 0, SAMPLE_SIZE, SAMPLE_SIZE);\n try {\n resolve(averagePictureColour(ctx.getImageData(0, 0, SAMPLE_SIZE, SAMPLE_SIZE).data));\n } catch (err) {\n console.debug(\"[prefs-client] picture colour unreadable\", url, err);\n resolve(null);\n }\n };\n img.onerror = () => resolve(null);\n img.src = url;\n });\n}\n\n// src/cosmetics.ts\nvar COSMETICS_ASSET_BASE = \"/cosmetics\";\nvar STYLE_ID = \"hadoku-cosmetics\";\nvar esc = (s) => s.replace(\n /[&<>\"']/g,\n (c) => ({ \"&\": \"&\", \"<\": \"<\", \">\": \">\", '\"': \""\", \"'\": \"'\" })[c]\n);\nvar nameOf = (id) => id ? id.slice(id.indexOf(\".\") + 1) : \"none\";\nfunction colourStyle(hex) {\n return hex && /^#[0-9a-f]{6}$/i.test(hex) ? `--hc-base:${hex}` : \"\";\n}\nfunction placementTransform(p) {\n const sx = p.s / 100 * (p.flip ? -1 : 1);\n return `translate(calc(var(--hc-sz) * ${(p.x / 100).toFixed(3)}),calc(var(--hc-sz) * ${(p.y / 100).toFixed(3)})) rotate(${p.r}deg) scale(${sx.toFixed(3)},${(p.s / 100).toFixed(3)})`;\n}\nvar SPLIT_EFFECTS = /* @__PURE__ */ new Set([\"hop\"]);\nfunction nameHtml(text, worn, opts = {}) {\n const fx = nameOf(worn?.effect);\n const gr = nameOf(worn?.gradient);\n const font = nameOf(worn?.font);\n const t = esc(text);\n const split = SPLIT_EFFECTS.has(fx);\n const tx = split ? `<span class=\"hc-tx\">${[...text].map(\n (c, i) => `<span class=\"hc-ch\" style=\"--hc-i:${i}\"><span class=\"hc-c\">${c === \" \" ? \" \" : esc(c)}</span></span>`\n ).join(\"\")}</span>` : `<span class=\"hc-tx\">${t}</span>`;\n const layer = (cls2) => `<span class=\"hc-lyr${cls2}\" aria-hidden=\"true\">${t}</span>`;\n let back = \"\";\n let front = \"\";\n if (fx === \"aura\" || fx === \"outline\") back += layer(\" hc-g\");\n if (fx === \"glint\") front += `<span class=\"hc-win\" aria-hidden=\"true\">${layer(\"\")}</span>`;\n if (fx === \"twinkle\") {\n front += [\n [-6, -25, 0],\n [52, 55, 0.95],\n [94, -15, 1.9]\n ].map(\n ([x, y, d]) => `<i class=\"hc-tw\" aria-hidden=\"true\" style=\"left:${x}%;top:${y}%;animation-delay:${d}s\"></i>`\n ).join(\"\");\n }\n const cls = `hc-name hc-fx-${fx} hc-gr-${gr} hc-font-${font}${split ? \" hc-split\" : \"\"}${opts.large ? \" hc-large\" : \"\"}`;\n return `<span class=\"${cls}\">${back}${tx}${front}</span>`;\n}\nvar RUNES_SVG = (() => {\n let marks = \"\";\n for (let i = 0; i < 12; i++) {\n marks += `<g transform=\"rotate(${i * 30} 50 50)\"><path d=\"M50 3 l2.5 4 -2.5 4 -2.5 -4z\" fill=\"currentColor\" opacity=\"${i % 3 ? 0.55 : 1}\"/></g>`;\n }\n return `<svg viewBox=\"0 0 100 100\" aria-hidden=\"true\"><circle cx=\"50\" cy=\"50\" r=\"44\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1\" stroke-dasharray=\"3 5\" opacity=\".7\"/>${marks}</svg>`;\n})();\nfunction decorHtml(worn, assetBase = COSMETICS_ASSET_BASE) {\n const id = worn?.overlay;\n if (!id) return \"\";\n const art = COSMETICS_BY_ID.get(id)?.art;\n if (art) {\n const p = worn?.placement ?? PLACEMENT_DEFAULT;\n return `<span class=\"hc-ol hc-art${art.pixel ? \" hc-px\" : \"\"}\" data-hc-art=\"${esc(id)}\" style=\"left:${50 - art.width / 2}%;top:${art.top}%;width:${art.width}%;z-index:${p.back ? 0 : 3}\"><span class=\"hc-adj\" style=\"transform:${placementTransform(p)}\"><img alt=\"\" draggable=\"false\" style=\"--hc-tilt:${art.tilt}deg\" src=\"${esc(`${assetBase}/art/${art.file}`)}\"></span></span>`;\n }\n switch (nameOf(id)) {\n case \"runes\":\n return `<span class=\"hc-ol hc-runes\">${RUNES_SVG}</span>`;\n case \"comet\":\n return `<span class=\"hc-ol hc-comet\"></span>`;\n case \"shine\":\n return `<span class=\"hc-ol hc-clip\"><span class=\"hc-band\"></span></span>`;\n case \"glow\":\n return `<span class=\"hc-ol hc-glow\"></span>`;\n case \"petals\":\n return [15, 40, 62, 85].map(\n (l, i) => `<i class=\"hc-ol hc-petal\" style=\"left:${l}%;animation-delay:${(i * 0.85).toFixed(2)}s\"></i>`\n ).join(\"\");\n default:\n return \"\";\n }\n}\nfunction avatarHtml(o) {\n const pic = o.src ? `<img alt=\"\" src=\"${esc(o.src)}\">` : \"\";\n return `<span class=\"hc-av\" style=\"--hc-sz:${o.size}px\"><span class=\"hc-pic\">${pic}</span>${decorHtml(o.worn, o.assetBase)}</span>`;\n}\nvar GLYPHS = \"!<>-_/[]{}=+*^?#%&01\";\nvar LETTER_MODES = /* @__PURE__ */ new Set([\"type\", \"decode\", \"cascade\"]);\nvar REVEAL_MS = 1400;\nvar LETTER_LIMIT = 160;\nfunction messageHtml(text, worn, opts = {}) {\n let mode = nameOf(worn?.message);\n if (mode === \"none\") return `<span class=\"hc-msg\">${esc(text)}</span>`;\n const words = text.split(/\\s+/).filter(Boolean);\n if (LETTER_MODES.has(mode) && text.length > LETTER_LIMIT) mode = \"words\";\n const keep = opts.keep ? \" data-hc-keep\" : \"\";\n let body;\n let step;\n if (LETTER_MODES.has(mode)) {\n step = Math.min(38, REVEAL_MS / Math.max(1, text.length));\n let n = 0;\n const rnd = () => GLYPHS[Math.floor(Math.random() * GLYPHS.length)];\n body = words.map(\n (w) => `<span class=\"hc-w\">${[...w].map((c) => {\n const i = n++;\n return mode === \"decode\" ? `<span class=\"hc-ch\" style=\"--hc-i:${i}\" data-g=\"${esc(rnd())}\" data-h=\"${esc(rnd())}\"><span class=\"hc-r\">${esc(c)}</span></span>` : `<span class=\"hc-ch\" style=\"--hc-i:${i}\">${esc(c)}</span>`;\n }).join(\"\")}</span>`\n ).join(\" \");\n if (mode === \"type\") body += `<span class=\"hc-caret\" style=\"--hc-i:${n}\"></span>`;\n const lastClass = mode === \"type\" ? \"hc-caret\" : mode === \"decode\" ? \"hc-r\" : \"hc-ch\";\n const at = body.lastIndexOf(`<span class=\"${lastClass}\"`);\n body = `${body.slice(0, at)}<span data-hc-last class=\"${lastClass}\"${body.slice(at + `<span class=\"${lastClass}\"`.length)}`;\n } else {\n step = Math.min(90, REVEAL_MS / Math.max(1, words.length));\n body = words.map(\n (w, i) => `<span class=\"hc-w\" style=\"--hc-i:${i}\"${i === words.length - 1 ? \" data-hc-last\" : \"\"}>${esc(w)}</span>`\n ).join(\" \");\n }\n return `<span class=\"hc-msg hc-mt-${mode}\" style=\"--hc-step:${step.toFixed(1)}ms\" data-text=\"${esc(text)}\"${keep}><span class=\"hc-sr\">${esc(text)}</span><span aria-hidden=\"true\">${body}</span></span>`;\n}\nfunction finishReveal(e) {\n const t = e.target;\n if (e.pseudoElement || !t?.hasAttribute?.(\"data-hc-last\")) return;\n const msg = t.closest(\".hc-msg\");\n if (!msg || msg.hasAttribute(\"data-hc-keep\")) return;\n msg.className = \"hc-msg\";\n msg.removeAttribute(\"style\");\n msg.textContent = msg.getAttribute(\"data-text\") ?? \"\";\n}\nvar FONTS = {\n script: {\n file: \"caveat\",\n family: \"Caveat\",\n generic: \"cursive\",\n css: \"font-weight:700;font-size:1.3em;line-height:1\"\n },\n surf: {\n file: \"pacifico\",\n family: \"Pacifico\",\n generic: \"cursive\",\n css: \"font-weight:400;font-size:1.02em;line-height:1.1\"\n },\n gothic: {\n file: \"gothic\",\n family: \"Grenze Gotisch\",\n generic: \"serif\",\n css: \"font-weight:600;font-size:1.15em\"\n },\n runic: { file: \"uncial\", family: \"Uncial Antiqua\", generic: \"serif\", css: \"font-weight:400\" },\n corsair: {\n file: \"pirata\",\n family: \"Pirata One\",\n generic: \"serif\",\n css: \"font-weight:400;font-size:1.12em;letter-spacing:.02em\"\n },\n metal: {\n file: \"metal\",\n family: \"Metal Mania\",\n generic: \"cursive\",\n css: \"font-weight:400;font-size:1.12em;letter-spacing:.02em\"\n },\n comic: {\n file: \"bangers\",\n family: \"Bangers\",\n generic: \"cursive\",\n css: \"font-weight:400;font-size:1.15em;letter-spacing:.05em\"\n },\n circuit: {\n file: \"orbitron\",\n family: \"Orbitron\",\n generic: \"sans-serif\",\n css: \"font-weight:700;letter-spacing:.03em\"\n },\n hover: {\n file: \"audiowide\",\n family: \"Audiowide\",\n generic: \"sans-serif\",\n css: \"font-weight:400;font-size:.92em\"\n },\n retro: { file: \"righteous\", family: \"Righteous\", generic: \"sans-serif\", css: \"font-weight:400\" },\n arcade: {\n file: \"bungee\",\n family: \"Bungee\",\n generic: \"sans-serif\",\n css: \"font-weight:400;font-size:.9em\"\n },\n pixel: {\n file: \"pixel\",\n family: \"Press Start 2P\",\n generic: \"monospace\",\n css: \"font-weight:400;font-size:.62em;line-height:1.6\"\n }\n};\nfunction cosmeticsCss(assetBase = COSMETICS_ASSET_BASE) {\n const fontFaces = Object.entries(FONTS).map(\n ([k, f]) => `@font-face{font-family:\"hc-${k}\";src:url(\"${assetBase}/fonts/${f.file}.woff2\") format(\"woff2\");font-display:swap}`\n ).join(\"\\n\");\n const fontRules = Object.entries(FONTS).map(([k, f]) => `.hc-font-${k}{font-family:\"hc-${k}\",\"${f.family}\",${f.generic};${f.css}}`).join(\"\\n\");\n return `${fontFaces}\n/* Colours: everything derives from the character colour. */\n.hc-name,.hc-av,.hc-msg{--hc-b:var(--hc-base,currentColor);--hc-hi:color-mix(in oklch,var(--hc-b),white 50%);--hc-lo:color-mix(in oklch,var(--hc-b),black 50%);--hc-glow:color-mix(in oklch,var(--hc-b) 75%,transparent)}\n\n/* --- name ---\n Bleed: a gradient is a background clipped to the letters, and a background covers only the box; script\n swashes reach outside it. So the text box and every layer over it get room on all sides, cancelled by a\n negative margin so nothing moves. */\n.hc-name{--hc-bx:.4em;--hc-by:.3em;--hc-grad:none;position:relative;display:inline-block;white-space:nowrap;color:var(--hc-b);font-weight:700;line-height:1.25}\n.hc-name.hc-large{font-size:1.6em}\n.hc-name .hc-tx{position:relative;z-index:1;display:inline-block;padding:var(--hc-by) var(--hc-bx);margin:calc(-1 * var(--hc-by)) calc(-1 * var(--hc-bx))}\n.hc-name .hc-lyr{position:absolute;left:calc(-1 * var(--hc-bx));top:calc(-1 * var(--hc-by));padding:var(--hc-by) var(--hc-bx);pointer-events:none;white-space:nowrap}\n.hc-name .hc-win{position:absolute;inset:calc(-1 * var(--hc-by)) calc(-1 * var(--hc-bx));overflow:hidden}\n.hc-name .hc-win .hc-lyr{left:0;top:0}\n.hc-name .hc-ch,.hc-name .hc-c{display:inline-block}\n.hc-gr-gild{--hc-grad:linear-gradient(180deg,var(--hc-hi) 0%,var(--hc-b) 55%,var(--hc-lo) 100%)}\n.hc-gr-gloss{--hc-grad:linear-gradient(180deg,color-mix(in oklch,var(--hc-hi),white 35%) 0%,var(--hc-hi) 36%,color-mix(in oklch,var(--hc-hi),var(--hc-b)) 48%,var(--hc-b) 60%,var(--hc-lo) 100%)}\n.hc-gr-split{--hc-grad:linear-gradient(180deg,var(--hc-hi) 0 52%,var(--hc-b) 52% 100%)}\n.hc-gr-brushed{--hc-grad:repeating-linear-gradient(90deg,transparent 0 1px,color-mix(in oklch,var(--hc-hi) 40%,transparent) 1px 2px),linear-gradient(180deg,var(--hc-hi),var(--hc-b) 55%,var(--hc-lo))}\n.hc-gr-lumen{--hc-grad:radial-gradient(ellipse 60% 90% at 50% 35%,var(--hc-hi),var(--hc-b) 55%,var(--hc-lo) 100%)}\n.hc-name:not(.hc-gr-none) .hc-tx{background:var(--hc-grad);-webkit-background-clip:text;background-clip:text;color:transparent}\n.hc-name.hc-split:not(.hc-gr-none) .hc-tx{background:none}\n.hc-name.hc-split:not(.hc-gr-none) .hc-tx .hc-c{padding:var(--hc-by) var(--hc-bx);margin:calc(-1 * var(--hc-by)) calc(-1 * var(--hc-bx));background:var(--hc-grad);-webkit-background-clip:text;background-clip:text;color:transparent}\n.hc-fx-glint .hc-win{z-index:3;animation:hc-win-go 4.5s cubic-bezier(.4,0,.2,1) infinite;-webkit-mask-image:linear-gradient(100deg,transparent 35%,#000 50%,transparent 65%);mask-image:linear-gradient(100deg,transparent 35%,#000 50%,transparent 65%)}\n.hc-fx-glint .hc-win .hc-lyr{color:var(--hc-hi);animation:hc-win-back 4.5s cubic-bezier(.4,0,.2,1) infinite}\n@keyframes hc-win-go{0%{transform:translateX(-100%)}55%,100%{transform:translateX(100%)}}\n@keyframes hc-win-back{0%{transform:translateX(100%)}55%,100%{transform:translateX(-100%)}}\n.hc-fx-aura .hc-lyr.hc-g{color:transparent;text-shadow:0 0 6px var(--hc-glow),0 0 14px var(--hc-glow);animation:hc-breathe 3s ease-in-out infinite}\n.hc-fx-outline .hc-lyr.hc-g{color:transparent;-webkit-text-stroke:3px var(--hc-hi);animation:hc-breathe 3.6s ease-in-out infinite}\n@keyframes hc-breathe{0%,100%{opacity:.25}50%{opacity:1}}\n.hc-fx-hop .hc-c{animation:hc-hop 3.6s ease-in-out calc(var(--hc-i) * 45ms) infinite}\n@keyframes hc-hop{0%,14%,100%{transform:none}5%{transform:translateY(-4px)}10%{transform:translateY(.5px)}}\n.hc-fx-twinkle .hc-tw{position:absolute;width:7px;height:7px;z-index:3;pointer-events:none;background:linear-gradient(var(--hc-hi),var(--hc-hi)) center/1.4px 100% no-repeat,linear-gradient(90deg,var(--hc-hi),var(--hc-hi)) center/100% 1.4px no-repeat;animation:hc-star 2.8s ease-in-out infinite both}\n@keyframes hc-star{0%,100%{opacity:0;transform:scale(.3) rotate(0deg)}25%{opacity:1;transform:scale(1) rotate(45deg)}50%{opacity:0;transform:scale(.3) rotate(90deg)}}\n.hc-font-caps{font-variant:small-caps;letter-spacing:.08em}\n.hc-font-wide{letter-spacing:.22em;font-weight:600;text-transform:uppercase;font-size:.82em}\n${fontRules}\n\n/* --- avatar --- */\n.hc-av{--hc-sz:40px;position:relative;display:inline-block;width:var(--hc-sz);height:var(--hc-sz);flex:none;isolation:isolate;vertical-align:middle}\n.hc-av .hc-pic{position:absolute;inset:0;border-radius:50%;overflow:hidden;z-index:1;background:color-mix(in oklch,var(--hc-b) 30%,transparent)}\n.hc-av .hc-pic img{width:100%;height:100%;display:block;object-fit:cover;image-rendering:pixelated}\n.hc-av .hc-ol{position:absolute;pointer-events:none}\n.hc-ol.hc-art{display:block}\n.hc-ol.hc-art .hc-adj{display:block;position:relative;transform-origin:50% 60%}\n.hc-ol.hc-art img{display:block;width:100%;height:auto;filter:drop-shadow(0 1px 1px rgba(0,0,0,.45));animation:hc-art-bob 3.4s ease-in-out infinite;-webkit-user-drag:none;user-select:none}\n.hc-ol.hc-art.hc-px img{image-rendering:pixelated}\n@keyframes hc-art-bob{0%,100%{transform:rotate(var(--hc-tilt,0deg)) translateY(0)}50%{transform:rotate(var(--hc-tilt,0deg)) translateY(-2px)}}\n.hc-ol.hc-runes{inset:-9px;z-index:2;color:var(--hc-hi);animation:hc-turn 14s linear infinite}\n.hc-ol.hc-runes svg{width:100%;height:100%;display:block}\n@keyframes hc-turn{to{transform:rotate(360deg)}}\n.hc-ol.hc-comet{inset:-6px;z-index:2;border-radius:50%;background:conic-gradient(from 0deg,transparent 0 270deg,color-mix(in oklch,var(--hc-hi) 10%,transparent) 300deg,var(--hc-hi) 358deg,transparent 360deg);-webkit-mask:radial-gradient(farthest-side,transparent calc(100% - 3px),#000 calc(100% - 3px));mask:radial-gradient(farthest-side,transparent calc(100% - 3px),#000 calc(100% - 3px));animation:hc-turn 2.4s linear infinite}\n.hc-ol.hc-clip{inset:0;border-radius:50%;overflow:hidden;z-index:2}\n.hc-ol .hc-band{position:absolute;top:-30%;left:0;width:45%;height:160%;background:linear-gradient(90deg,transparent,color-mix(in oklch,var(--hc-hi) 70%,transparent),transparent);transform:translateX(-130%) rotate(20deg);animation:hc-band 5s ease-in-out infinite}\n@keyframes hc-band{0%{transform:translateX(-130%) rotate(20deg)}45%,100%{transform:translateX(260%) rotate(20deg)}}\n.hc-ol.hc-glow{inset:-30%;border-radius:50%;z-index:0;background:radial-gradient(circle,var(--hc-glow) 0,transparent 65%);animation:hc-glowp 3.2s ease-in-out infinite}\n@keyframes hc-glowp{0%,100%{opacity:.35;transform:scale(.92)}50%{opacity:.9;transform:scale(1.04)}}\n.hc-ol.hc-petal{top:-8%;height:100%;width:5px;z-index:2;animation:hc-fall 3.4s linear infinite both}\n.hc-ol.hc-petal::after{content:\"\";position:absolute;left:0;top:0;width:5px;height:7px;border-radius:5px 0;background:#f7b6c8;transform:rotate(35deg)}\n@keyframes hc-fall{0%{transform:translate(0,0);opacity:0}12%{opacity:.9}100%{transform:translate(-6px,100%);opacity:0}}\n\n/* --- message reveal: fill backwards only; the page swaps back to plain text when it ends --- */\n.hc-sr{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap}\n.hc-msg .hc-w{display:inline-block;white-space:nowrap}\n.hc-msg .hc-ch{display:inline-block}\n.hc-mt-type .hc-ch{animation:hc-in .01s linear calc(var(--hc-i) * var(--hc-step)) backwards}\n.hc-mt-type .hc-caret{display:inline-block;width:2px;height:1em;margin-left:2px;vertical-align:-2px;background:var(--hc-b);opacity:0;animation:hc-caret 1.5s steps(1) calc(var(--hc-i) * var(--hc-step)) backwards}\n@keyframes hc-caret{0%{opacity:0}1%{opacity:1}20%{opacity:0}40%{opacity:1}60%{opacity:0}80%{opacity:1}100%{opacity:0}}\n.hc-mt-cascade .hc-ch{animation:hc-drop .35s cubic-bezier(.3,1.5,.5,1) calc(var(--hc-i) * var(--hc-step)) backwards}\n.hc-mt-decode .hc-ch{position:relative}\n.hc-mt-decode .hc-ch .hc-r{display:inline-block;animation:hc-in .01s linear calc(var(--hc-i) * var(--hc-step) + 260ms) backwards}\n.hc-mt-decode .hc-ch::before,.hc-mt-decode .hc-ch::after{position:absolute;left:0;top:0;color:var(--hc-b);opacity:0;animation:hc-dec-a .26s steps(1) calc(var(--hc-i) * var(--hc-step)) backwards}\n.hc-mt-decode .hc-ch::before{content:attr(data-g)}\n.hc-mt-decode .hc-ch::after{content:attr(data-h);animation-name:hc-dec-b}\n@keyframes hc-dec-a{0%{opacity:.85}50%,100%{opacity:0}}\n@keyframes hc-dec-b{0%{opacity:0}50%{opacity:.85}100%{opacity:0}}\n.hc-mt-words .hc-w{animation:hc-rise .4s ease-out calc(var(--hc-i) * var(--hc-step)) backwards}\n.hc-mt-bubble .hc-w{animation:hc-pop .35s cubic-bezier(.3,1.6,.5,1) calc(var(--hc-i) * var(--hc-step)) backwards}\n.hc-mt-drift .hc-w{animation:hc-slide .45s cubic-bezier(.2,.8,.2,1) calc(var(--hc-i) * var(--hc-step)) backwards}\n@keyframes hc-in{from{opacity:0}to{opacity:1}}\n@keyframes hc-drop{0%{transform:translateY(-10px);opacity:0}100%{transform:none;opacity:1}}\n@keyframes hc-rise{0%{transform:translateY(10px);opacity:0}100%{transform:none;opacity:1}}\n@keyframes hc-pop{0%{transform:scale(.6);opacity:0}100%{transform:scale(1);opacity:1}}\n@keyframes hc-slide{0%{transform:translateX(-18px);opacity:0}100%{transform:none;opacity:1}}\n\n.hc-idle .hc-name,.hc-idle .hc-name *,.hc-idle .hc-av *,.hc-idle .hc-av *::after{animation-play-state:paused!important}\n@media (prefers-reduced-motion:reduce){.hc-name *,.hc-av *,.hc-av *::after,.hc-msg *,.hc-msg *::before,.hc-msg *::after{animation:none!important}}\n`;\n}\nfunction ensureCosmeticsStyles(doc = document, assetBase = COSMETICS_ASSET_BASE) {\n if (doc.getElementById(STYLE_ID)) return;\n const style = doc.createElement(\"style\");\n style.id = STYLE_ID;\n style.textContent = cosmeticsCss(assetBase);\n doc.head.appendChild(style);\n doc.addEventListener(\"animationend\", finishReveal);\n}\nexport {\n COSMETICS,\n COSMETICS_ASSET_BASE,\n COSMETICS_BY_ID,\n COSMETIC_ID_PATTERN,\n COSMETIC_SLOTS,\n EARNED_COSMETICS,\n PLACEMENT_DEFAULT,\n PLACEMENT_LIMITS,\n PLACEMENT_ZONE,\n avatarHtml,\n averagePictureColour,\n cleanLoadout,\n cleanWorn,\n colourStyle,\n cosmeticsCss,\n decorHtml,\n emptyLoadout,\n ensureCosmeticsStyles,\n finishReveal,\n fitPlacement,\n messageHtml,\n nameHtml,\n onPictureColour,\n overlayBounds,\n ownedFrom,\n pictureColour,\n placementTransform,\n readPictureColour,\n wornCosmetics\n};\n"],"names":[],"mappings":"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","x_google_ignoreList":[0]}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hadoku/task-ui-components",
|
|
3
|
-
"version": "4.23.
|
|
4
|
-
"gitHead": "
|
|
3
|
+
"version": "4.23.11",
|
|
4
|
+
"gitHead": "ae70b4be99c40129dbdfa61865d3f19c17022103",
|
|
5
5
|
"description": "Reusable UI components with Bento grid and metallic gradient support",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "dist/index.js",
|
|
@@ -18,9 +18,17 @@
|
|
|
18
18
|
*
|
|
19
19
|
* Styles are inline for the reason AvatarPicker's are: apps compile this
|
|
20
20
|
* package's CSS at their own build. Tokens only; the cosmetics' own colours come
|
|
21
|
-
* from the person's
|
|
21
|
+
* from the person's colour, their picture's average or else their character's
|
|
22
|
+
* (and, for overlay images, the art).
|
|
22
23
|
*/
|
|
23
|
-
import React, {
|
|
24
|
+
import React, {
|
|
25
|
+
useCallback,
|
|
26
|
+
useEffect,
|
|
27
|
+
useMemo,
|
|
28
|
+
useRef,
|
|
29
|
+
useState,
|
|
30
|
+
useSyncExternalStore
|
|
31
|
+
} from 'react'
|
|
24
32
|
import { Icon } from '@wolffm/themes'
|
|
25
33
|
import type { AvatarSettings, LoadoutUpdate } from '@wolffm/prefs-client/avatar'
|
|
26
34
|
import {
|
|
@@ -35,7 +43,10 @@ import {
|
|
|
35
43
|
fitPlacement,
|
|
36
44
|
messageHtml,
|
|
37
45
|
nameHtml,
|
|
46
|
+
onPictureColour,
|
|
47
|
+
pictureColour,
|
|
38
48
|
placementTransform,
|
|
49
|
+
readPictureColour,
|
|
39
50
|
wornCosmetics,
|
|
40
51
|
type AvatarCosmetics,
|
|
41
52
|
type CosmeticSlot,
|
|
@@ -344,6 +355,22 @@ function useDragScroll<T extends HTMLElement>() {
|
|
|
344
355
|
return setEl
|
|
345
356
|
}
|
|
346
357
|
|
|
358
|
+
/**
|
|
359
|
+
* The colour a picture gives a person's name: its average, read once
|
|
360
|
+
* (@wolffm/prefs-client, the same reader watchparty's chat uses). Null for no
|
|
361
|
+
* picture, or while it is unread or unreadable.
|
|
362
|
+
*/
|
|
363
|
+
function usePictureColour(url: string | null): string | null {
|
|
364
|
+
useEffect(() => {
|
|
365
|
+
if (url) void readPictureColour(url)
|
|
366
|
+
}, [url])
|
|
367
|
+
return useSyncExternalStore(
|
|
368
|
+
onPictureColour,
|
|
369
|
+
() => (url ? (pictureColour(url) ?? null) : null),
|
|
370
|
+
() => null
|
|
371
|
+
)
|
|
372
|
+
}
|
|
373
|
+
|
|
347
374
|
export interface AvatarBuilderProps {
|
|
348
375
|
/** The person's display name, for the preview. */
|
|
349
376
|
name: string | null
|
|
@@ -358,7 +385,6 @@ export function AvatarBuilder({ name, isAdmin = false }: AvatarBuilderProps) {
|
|
|
358
385
|
const [error, setError] = useState<string | null>(null)
|
|
359
386
|
const [tab, setTab] = useState<Tab>('picture')
|
|
360
387
|
const [tryOn, setTryOn] = useState<{ slot: CosmeticSlot; id: string } | null>(null)
|
|
361
|
-
const [showLocked, setShowLocked] = useState(true)
|
|
362
388
|
const [replay, setReplay] = useState(0)
|
|
363
389
|
const narrow = useNarrow()
|
|
364
390
|
const root = useRef<HTMLDivElement>(null)
|
|
@@ -368,6 +394,8 @@ export function AvatarBuilder({ name, isAdmin = false }: AvatarBuilderProps) {
|
|
|
368
394
|
const seq = useRef(0)
|
|
369
395
|
|
|
370
396
|
useEffect(() => ensureCosmeticsStyles(), [])
|
|
397
|
+
// A Discord, Steam or uploaded picture colours the name; a character, its own colour.
|
|
398
|
+
const tint = usePictureColour(settings?.avatar.url ?? null)
|
|
371
399
|
|
|
372
400
|
useEffect(() => {
|
|
373
401
|
let live = true
|
|
@@ -475,11 +503,11 @@ export function AvatarBuilder({ name, isAdmin = false }: AvatarBuilderProps) {
|
|
|
475
503
|
const characters = new Map((sets ?? []).flatMap(s => s.characters).map(c => [c.id, c]))
|
|
476
504
|
const character = settings.sprite ? characters.get(settings.sprite) : undefined
|
|
477
505
|
const picture = settings.avatar.url ?? (character ? portraitUrl(character) : null)
|
|
478
|
-
// Every name cosmetic derives its colours from the
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
506
|
+
// Every name cosmetic derives its colours from the person's: the picture's
|
|
507
|
+
// average, else the character's, as the chat draws them.
|
|
508
|
+
const base =
|
|
509
|
+
tint ?? (character && /^#[0-9a-f]{6}$/i.test(character.color) ? character.color : null)
|
|
510
|
+
const colour: React.CSSProperties = base ? ({ '--hc-base': base } as React.CSSProperties) : {}
|
|
483
511
|
const displayName = name || 'You'
|
|
484
512
|
|
|
485
513
|
const worn: AvatarCosmetics = { ...(settings.avatar.cosmetics ?? {}) }
|
|
@@ -656,8 +684,6 @@ export function AvatarBuilder({ name, isAdmin = false }: AvatarBuilderProps) {
|
|
|
656
684
|
owned={owned}
|
|
657
685
|
equipped={loadout?.equipped[tab] ?? null}
|
|
658
686
|
tryOn={tryOn}
|
|
659
|
-
showLocked={showLocked}
|
|
660
|
-
onShowLocked={setShowLocked}
|
|
661
687
|
isAdmin={isAdmin}
|
|
662
688
|
preview={id => {
|
|
663
689
|
const only: AvatarCosmetics = { [tab]: id ?? undefined }
|
|
@@ -817,8 +843,6 @@ interface SlotGridProps {
|
|
|
817
843
|
owned: ReadonlySet<string>
|
|
818
844
|
equipped: string | null
|
|
819
845
|
tryOn: { slot: CosmeticSlot; id: string } | null
|
|
820
|
-
showLocked: boolean
|
|
821
|
-
onShowLocked: (on: boolean) => void
|
|
822
846
|
/** Admins see archived items, greyed and not selectable. */
|
|
823
847
|
isAdmin: boolean
|
|
824
848
|
preview: (id: string | null) => string
|
|
@@ -836,8 +860,6 @@ function SlotGrid({
|
|
|
836
860
|
owned,
|
|
837
861
|
equipped,
|
|
838
862
|
tryOn,
|
|
839
|
-
showLocked,
|
|
840
|
-
onShowLocked,
|
|
841
863
|
isAdmin,
|
|
842
864
|
preview,
|
|
843
865
|
colour,
|
|
@@ -846,7 +868,6 @@ function SlotGrid({
|
|
|
846
868
|
}: SlotGridProps) {
|
|
847
869
|
const items = COSMETICS.filter(i => i.slot === slot && (!i.hidden || isAdmin))
|
|
848
870
|
const locked = items.filter(i => !i.hidden && !owned.has(i.id))
|
|
849
|
-
const shown = showLocked ? items : items.filter(i => i.hidden || owned.has(i.id))
|
|
850
871
|
|
|
851
872
|
return (
|
|
852
873
|
<>
|
|
@@ -860,7 +881,7 @@ function SlotGrid({
|
|
|
860
881
|
<Html html={preview(null)} style={{ ...S.tilePv, ...colour }} />
|
|
861
882
|
<span style={S.tileName}>None</span>
|
|
862
883
|
</button>
|
|
863
|
-
{
|
|
884
|
+
{items.map(item => {
|
|
864
885
|
if (item.hidden) {
|
|
865
886
|
return (
|
|
866
887
|
<button
|
|
@@ -884,7 +905,9 @@ function SlotGrid({
|
|
|
884
905
|
key={item.id}
|
|
885
906
|
type="button"
|
|
886
907
|
aria-pressed={own ? on : trying}
|
|
887
|
-
aria-label={
|
|
908
|
+
aria-label={
|
|
909
|
+
own ? item.label : `${item.label}, locked. ${item.earn ?? ''}. Try it on.`
|
|
910
|
+
}
|
|
888
911
|
style={{
|
|
889
912
|
...S.tile,
|
|
890
913
|
...(on ? S.tileOn : {}),
|
|
@@ -913,14 +936,6 @@ function SlotGrid({
|
|
|
913
936
|
{locked.length === 1 ? 'One to earn.' : `${locked.length} to earn.`} Pick one to try it
|
|
914
937
|
on.
|
|
915
938
|
</span>
|
|
916
|
-
<label>
|
|
917
|
-
<input
|
|
918
|
-
type="checkbox"
|
|
919
|
-
checked={showLocked}
|
|
920
|
-
onChange={e => onShowLocked(e.currentTarget.checked)}
|
|
921
|
-
/>{' '}
|
|
922
|
-
Show locked
|
|
923
|
-
</label>
|
|
924
939
|
</div>
|
|
925
940
|
)}
|
|
926
941
|
</>
|
|
@@ -128,6 +128,11 @@ export function AvatarPicker({ settings, sets, busy, run }: AvatarPickerProps) {
|
|
|
128
128
|
}
|
|
129
129
|
const shown = settings.avatar
|
|
130
130
|
const current = sets?.[setIdx]
|
|
131
|
+
// The tab on show. Picking an available source also wears it; Upload opens
|
|
132
|
+
// with nothing uploaded yet, to choose a file.
|
|
133
|
+
const [view, setView] = useState<AvatarSource>(shown.source)
|
|
134
|
+
useEffect(() => setView(shown.source), [shown.source])
|
|
135
|
+
const openable = (src: AvatarSource) => src === 'upload' || available[src]
|
|
131
136
|
|
|
132
137
|
return (
|
|
133
138
|
<div style={S.stack}>
|
|
@@ -143,22 +148,19 @@ export function AvatarPicker({ settings, sets, busy, run }: AvatarPickerProps) {
|
|
|
143
148
|
key={src}
|
|
144
149
|
type="button"
|
|
145
150
|
role="radio"
|
|
146
|
-
aria-checked={
|
|
147
|
-
disabled={busy || !
|
|
148
|
-
title={
|
|
149
|
-
available[src]
|
|
150
|
-
? undefined
|
|
151
|
-
: src === 'upload'
|
|
152
|
-
? 'Upload a picture below first'
|
|
153
|
-
: `Link ${LABEL[src]} under Settings › Account first`
|
|
154
|
-
}
|
|
151
|
+
aria-checked={view === src}
|
|
152
|
+
disabled={busy || !openable(src)}
|
|
153
|
+
title={openable(src) ? undefined : `Link ${LABEL[src]} under Settings › Account first`}
|
|
155
154
|
style={{
|
|
156
155
|
...S.source,
|
|
157
|
-
...(
|
|
158
|
-
opacity:
|
|
156
|
+
...(view === src ? S.sourceOn : {}),
|
|
157
|
+
opacity: openable(src) ? 1 : 0.5
|
|
159
158
|
}}
|
|
160
159
|
onClick={() => {
|
|
161
|
-
|
|
160
|
+
setView(src)
|
|
161
|
+
if (available[src] && shown.source !== src) {
|
|
162
|
+
run(() => avatarClient.choose(src)).catch(reportErr)
|
|
163
|
+
}
|
|
162
164
|
}}
|
|
163
165
|
>
|
|
164
166
|
<Picture src={thumbOf[src]} style={S.thumb} />
|
|
@@ -167,7 +169,7 @@ export function AvatarPicker({ settings, sets, busy, run }: AvatarPickerProps) {
|
|
|
167
169
|
))}
|
|
168
170
|
</div>
|
|
169
171
|
|
|
170
|
-
{
|
|
172
|
+
{view === 'sprite' &&
|
|
171
173
|
(sets ? (
|
|
172
174
|
<>
|
|
173
175
|
{sets.length > 1 && (
|
|
@@ -211,37 +213,44 @@ export function AvatarPicker({ settings, sets, busy, run }: AvatarPickerProps) {
|
|
|
211
213
|
<span className="settings-popout__hint">…</span>
|
|
212
214
|
))}
|
|
213
215
|
|
|
214
|
-
|
|
215
|
-
<
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
216
|
+
{view === 'upload' && (
|
|
217
|
+
<div style={S.line}>
|
|
218
|
+
<span className="settings-popout__label" style={S.grow}>
|
|
219
|
+
{settings.upload ? 'Your picture' : 'Choose a picture to use'}
|
|
220
|
+
</span>
|
|
221
|
+
<label className="settings-popout__btn" style={{ opacity: busy ? 0.5 : 1 }}>
|
|
222
|
+
{settings.upload ? 'Replace' : 'Choose image'}
|
|
223
|
+
<input
|
|
224
|
+
type="file"
|
|
225
|
+
accept={settings.limits.acceptedTypes.join(',')}
|
|
226
|
+
hidden
|
|
227
|
+
disabled={busy}
|
|
228
|
+
onChange={e => {
|
|
229
|
+
const file = e.currentTarget.files?.[0]
|
|
230
|
+
e.currentTarget.value = ''
|
|
231
|
+
// An upload is worn at once: picking a file on this tab means "use this".
|
|
232
|
+
if (file)
|
|
233
|
+
run(async () => {
|
|
234
|
+
await avatarClient.upload(file)
|
|
235
|
+
return avatarClient.choose('upload')
|
|
236
|
+
}).catch(reportErr)
|
|
237
|
+
}}
|
|
238
|
+
/>
|
|
239
|
+
</label>
|
|
240
|
+
{settings.upload && (
|
|
241
|
+
<button
|
|
242
|
+
type="button"
|
|
243
|
+
className="settings-popout__btn"
|
|
244
|
+
disabled={busy}
|
|
245
|
+
onClick={() => {
|
|
246
|
+
run(() => avatarClient.removeUpload()).catch(reportErr)
|
|
247
|
+
}}
|
|
248
|
+
>
|
|
249
|
+
Remove
|
|
250
|
+
</button>
|
|
251
|
+
)}
|
|
252
|
+
</div>
|
|
253
|
+
)}
|
|
245
254
|
</div>
|
|
246
255
|
)
|
|
247
256
|
}
|