@hadoku/task-ui-components 4.23.13 → 4.23.14

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.
@@ -1 +1 @@
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
+ {"version":3,"file":"AvatarBuilder.d.ts","sourceRoot":"","sources":["../../src/components/AvatarBuilder.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,OAAO,KAON,MAAM,OAAO,CAAA;AAmVd,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,4 +1,4 @@
1
- import { jsx, jsxs, Fragment } from "react/jsx-runtime";
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
2
  import React, { useState, useRef, useEffect, useCallback, useMemo, useSyncExternalStore } from "react";
3
3
  import { Icon } from "@wolffm/themes";
4
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";
@@ -138,7 +138,12 @@ const S = {
138
138
  gap: 5,
139
139
  padding: 6,
140
140
  borderRadius: 8,
141
- border: "1px solid var(--color-border)",
141
+ // Longhands, not the `border` shorthand: the selected styles override
142
+ // borderColor, and when one comes off React clears borderColor to '', which
143
+ // would reset it to currentColor and leave a text-coloured ring behind.
144
+ borderWidth: 1,
145
+ borderStyle: "solid",
146
+ borderColor: "var(--color-border)",
142
147
  background: "var(--color-bg)",
143
148
  color: "var(--color-text)",
144
149
  cursor: "pointer",
@@ -162,16 +167,7 @@ const S = {
162
167
  fontSize: "0.8rem"
163
168
  },
164
169
  tileName: { fontSize: "0.8rem", fontWeight: 500, display: "flex", alignItems: "center", gap: 5 },
165
- tileEarn: { fontSize: "0.7rem", lineHeight: 1.3, color: "var(--color-text-muted)" },
166
- foot: {
167
- display: "flex",
168
- justifyContent: "space-between",
169
- alignItems: "center",
170
- gap: 8,
171
- flexWrap: "wrap",
172
- fontSize: "0.75rem",
173
- color: "var(--color-text-muted)"
174
- }
170
+ tileEarn: { fontSize: "0.7rem", lineHeight: 1.3, color: "var(--color-text-muted)" }
175
171
  };
176
172
  const Html = React.memo(
177
173
  function Html2({ html, style }) {
@@ -662,79 +658,72 @@ function SlotGrid({
662
658
  onTry
663
659
  }) {
664
660
  const items = COSMETICS.filter((i) => i.slot === slot && (!i.hidden || isAdmin));
665
- const locked = items.filter((i) => !i.hidden && !owned.has(i.id));
666
- return /* @__PURE__ */ jsxs(Fragment, { children: [
667
- /* @__PURE__ */ jsxs("div", { style: S.grid, children: [
668
- /* @__PURE__ */ jsxs(
669
- "button",
670
- {
671
- type: "button",
672
- "aria-pressed": equipped === null,
673
- style: { ...S.tile, ...equipped === null ? S.tileOn : {} },
674
- onClick: () => onEquip(null),
675
- children: [
676
- /* @__PURE__ */ jsx(Html, { html: preview(null), style: { ...S.tilePv, ...colour } }),
677
- /* @__PURE__ */ jsx("span", { style: S.tileName, children: "None" })
678
- ]
679
- }
680
- ),
681
- items.map((item) => {
682
- if (item.hidden) {
683
- return /* @__PURE__ */ jsxs(
684
- "button",
685
- {
686
- type: "button",
687
- disabled: true,
688
- "aria-label": `${item.label}, archived`,
689
- style: { ...S.tile, cursor: "not-allowed", opacity: 0.45 },
690
- children: [
691
- /* @__PURE__ */ jsx(Html, { html: preview(item.id), style: { ...S.tilePv, ...colour } }),
692
- /* @__PURE__ */ jsx("span", { style: S.tileName, children: item.label }),
693
- /* @__PURE__ */ jsx("span", { style: S.tileEarn, children: "Archived" })
694
- ]
695
- },
696
- item.id
697
- );
698
- }
699
- const own = owned.has(item.id);
700
- const on = equipped === item.id;
701
- const trying = tryOn?.slot === slot && tryOn.id === item.id;
661
+ return /* @__PURE__ */ jsxs("div", { style: S.grid, children: [
662
+ /* @__PURE__ */ jsxs(
663
+ "button",
664
+ {
665
+ type: "button",
666
+ "aria-pressed": equipped === null,
667
+ style: { ...S.tile, ...equipped === null ? S.tileOn : {} },
668
+ onClick: () => onEquip(null),
669
+ children: [
670
+ /* @__PURE__ */ jsx(Html, { html: preview(null), style: { ...S.tilePv, ...colour } }),
671
+ /* @__PURE__ */ jsx("span", { style: S.tileName, children: "None" })
672
+ ]
673
+ }
674
+ ),
675
+ items.map((item) => {
676
+ if (item.hidden) {
702
677
  return /* @__PURE__ */ jsxs(
703
678
  "button",
704
679
  {
705
680
  type: "button",
706
- "aria-pressed": own ? on : trying,
707
- "aria-label": own ? item.label : `${item.label}, locked. ${item.earn ?? ""}. Try it on.`,
708
- style: {
709
- ...S.tile,
710
- ...on ? S.tileOn : {},
711
- ...trying ? S.tileTrying : {}
712
- },
713
- onClick: () => own ? onEquip(item.id) : onTry(item.id),
714
- onMouseEnter: (e) => slot === "message" && replayPreview(e.currentTarget),
681
+ disabled: true,
682
+ "aria-label": `${item.label}, archived`,
683
+ style: { ...S.tile, cursor: "not-allowed", opacity: 0.45 },
715
684
  children: [
716
- /* @__PURE__ */ jsx(
717
- Html,
718
- {
719
- html: preview(item.id),
720
- style: { ...S.tilePv, ...colour, opacity: own ? 1 : 0.55 }
721
- }
722
- ),
723
- /* @__PURE__ */ jsxs("span", { style: { ...S.tileName, color: own ? void 0 : "var(--color-text-muted)" }, children: [
724
- !own && /* @__PURE__ */ jsx(Icon, { name: "lock", size: 12 }),
725
- item.label
726
- ] }),
727
- !own && /* @__PURE__ */ jsx("span", { style: S.tileEarn, children: item.earn })
685
+ /* @__PURE__ */ jsx(Html, { html: preview(item.id), style: { ...S.tilePv, ...colour } }),
686
+ /* @__PURE__ */ jsx("span", { style: S.tileName, children: item.label }),
687
+ /* @__PURE__ */ jsx("span", { style: S.tileEarn, children: "Archived" })
728
688
  ]
729
689
  },
730
690
  item.id
731
691
  );
732
- })
733
- ] }),
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
- ] }) })
692
+ }
693
+ const own = owned.has(item.id);
694
+ const on = equipped === item.id;
695
+ const trying = tryOn?.slot === slot && tryOn.id === item.id;
696
+ return /* @__PURE__ */ jsxs(
697
+ "button",
698
+ {
699
+ type: "button",
700
+ "aria-pressed": own ? on : trying,
701
+ "aria-label": own ? item.label : `${item.label}, locked. ${item.earn ?? ""}. Try it on.`,
702
+ style: {
703
+ ...S.tile,
704
+ ...on ? S.tileOn : {},
705
+ ...trying ? S.tileTrying : {}
706
+ },
707
+ onClick: () => own ? onEquip(item.id) : onTry(item.id),
708
+ onMouseEnter: (e) => slot === "message" && replayPreview(e.currentTarget),
709
+ children: [
710
+ /* @__PURE__ */ jsx(
711
+ Html,
712
+ {
713
+ html: preview(item.id),
714
+ style: { ...S.tilePv, ...colour, opacity: own ? 1 : 0.55 }
715
+ }
716
+ ),
717
+ /* @__PURE__ */ jsxs("span", { style: { ...S.tileName, color: own ? void 0 : "var(--color-text-muted)" }, children: [
718
+ !own && /* @__PURE__ */ jsx(Icon, { name: "lock", size: 12 }),
719
+ item.label
720
+ ] }),
721
+ !own && /* @__PURE__ */ jsx("span", { style: S.tileEarn, children: item.earn })
722
+ ]
723
+ },
724
+ item.id
725
+ );
726
+ })
738
727
  ] });
739
728
  }
740
729
  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 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
+ {"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 // Longhands, not the `border` shorthand: the selected styles override\n // borderColor, and when one comes off React clears borderColor to '', which\n // would reset it to currentColor and leave a text-coloured ring behind.\n borderWidth: 1,\n borderStyle: 'solid',\n borderColor: '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} 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\n return (\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={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 )\n}\n"],"names":["Html","live"],"mappings":"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"}
@@ -7,8 +7,9 @@
7
7
  * protocol cannot drift from the callback page).
8
8
  *
9
9
  * Everything happens in place: picking, uploading and removing are requests
10
- * from here. Linking Discord or Steam lives in the popout's Account tab
11
- * (LinkedAccounts); a linked account's picture is a choice here.
10
+ * from here. Discord and Steam are managed in the popout's Account tab
11
+ * (LinkedAccounts); one not linked yet can also be linked from its tile here,
12
+ * with the same consent step, and its picture is then worn.
12
13
  *
13
14
  * AvatarBuilder owns the settings and the character table and loads them only
14
15
  * when the dialog opens; this component draws them and sends changes through
@@ -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,qBA2J5E"}
1
+ {"version":3,"file":"AvatarPicker.d.ts","sourceRoot":"","sources":["../../src/components/AvatarPicker.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,OAAO,KAA8B,MAAM,OAAO,CAAA;AAClD,OAAO,EAAE,KAAK,cAAc,EAAqB,MAAM,6BAA6B,CAAA;AACpF,OAAO,EAA6B,KAAK,YAAY,EAAE,MAAM,mBAAmB,CAAA;AA4EhF,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,qBAsK5E"}
@@ -1,6 +1,7 @@
1
1
  import { jsxs, jsx, Fragment } from "react/jsx-runtime";
2
2
  import { useState, useEffect } from "react";
3
3
  import { avatarClient, portraitUrl } from "../lib/avatarData.js";
4
+ import { LinkConsent, linkAndWear } from "./LinkedAccounts.js";
4
5
  const LABEL = {
5
6
  sprite: "Character",
6
7
  discord: "Discord",
@@ -18,7 +19,12 @@ const S = {
18
19
  gap: 4,
19
20
  padding: 6,
20
21
  borderRadius: 8,
21
- border: "1px solid var(--color-border)",
22
+ // Longhands, not the `border` shorthand: the selected styles override
23
+ // borderColor, and when one comes off React clears borderColor to '', which
24
+ // would reset it to currentColor and leave a text-coloured ring behind.
25
+ borderWidth: 1,
26
+ borderStyle: "solid",
27
+ borderColor: "var(--color-border)",
22
28
  background: "var(--color-bg)",
23
29
  color: "var(--color-text)",
24
30
  fontSize: "0.72rem",
@@ -90,7 +96,6 @@ function AvatarPicker({ settings, sets, busy, run }) {
90
96
  const current = sets?.[setIdx];
91
97
  const [view, setView] = useState(shown.source);
92
98
  useEffect(() => setView(shown.source), [shown.source]);
93
- const openable = (src) => src === "upload" || available[src];
94
99
  return /* @__PURE__ */ jsxs("div", { style: S.stack, children: [
95
100
  settings.choice && settings.choice !== shown.source && /* @__PURE__ */ jsxs("span", { className: "settings-popout__hint", children: [
96
101
  LABEL[settings.choice],
@@ -102,13 +107,8 @@ function AvatarPicker({ settings, sets, busy, run }) {
102
107
  type: "button",
103
108
  role: "radio",
104
109
  "aria-checked": view === src,
105
- disabled: busy || !openable(src),
106
- title: openable(src) ? void 0 : `Link ${LABEL[src]} under Settings › Account first`,
107
- style: {
108
- ...S.source,
109
- ...view === src ? S.sourceOn : {},
110
- opacity: openable(src) ? 1 : 0.5
111
- },
110
+ disabled: busy,
111
+ style: { ...S.source, ...view === src ? S.sourceOn : {} },
112
112
  onClick: () => {
113
113
  setView(src);
114
114
  if (available[src] && shown.source !== src) {
@@ -154,6 +154,24 @@ function AvatarPicker({ settings, sets, busy, run }) {
154
154
  c.id
155
155
  )) })
156
156
  ] }) : /* @__PURE__ */ jsx("span", { className: "settings-popout__hint", children: "…" })),
157
+ (view === "discord" || view === "steam") && !available[view] && (!settings.providers[view] ? /* @__PURE__ */ jsxs("span", { className: "settings-popout__hint", children: [
158
+ LABEL[view],
159
+ " is not available yet."
160
+ ] }) : settings.linked[view] ? /* @__PURE__ */ jsxs("span", { className: "settings-popout__hint", children: [
161
+ "Your ",
162
+ LABEL[view],
163
+ " account has no picture."
164
+ ] }) : /* @__PURE__ */ jsx(
165
+ LinkConsent,
166
+ {
167
+ provider: view,
168
+ busy,
169
+ onLink: () => {
170
+ run(() => linkAndWear(view)).catch(reportErr);
171
+ }
172
+ },
173
+ view
174
+ )),
157
175
  view === "upload" && /* @__PURE__ */ jsxs("div", { style: S.line, children: [
158
176
  /* @__PURE__ */ jsx("span", { className: "settings-popout__label", style: S.grow, children: settings.upload ? "Your picture" : "Choose a picture to use" }),
159
177
  /* @__PURE__ */ jsxs("label", { className: "settings-popout__btn", style: { opacity: busy ? 0.5 : 1 }, children: [
@@ -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 // 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
+ {"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. Discord and Steam are managed in the popout's Account tab\n * (LinkedAccounts); one not linked yet can also be linked from its tile here,\n * with the same consent step, and its picture is then worn.\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'\nimport { LinkConsent, linkAndWear } from './LinkedAccounts'\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 // Longhands, not the `border` shorthand: the selected styles override\n // borderColor, and when one comes off React clears borderColor to '', which\n // would reset it to currentColor and leave a text-coloured ring behind.\n borderWidth: 1,\n borderStyle: 'solid',\n borderColor: '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; one that is\n // not yet opens to get it: Upload to choose a file, Discord or Steam to link.\n const [view, setView] = useState<AvatarSource>(shown.source)\n useEffect(() => setView(shown.source), [shown.source])\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}\n style={{ ...S.source, ...(view === src ? S.sourceOn : {}) }}\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 === 'discord' || view === 'steam') &&\n !available[view] &&\n (!settings.providers[view] ? (\n <span className=\"settings-popout__hint\">{LABEL[view]} is not available yet.</span>\n ) : settings.linked[view] ? (\n <span className=\"settings-popout__hint\">Your {LABEL[view]} account has no picture.</span>\n ) : (\n <LinkConsent\n key={view}\n provider={view}\n busy={busy}\n onLink={() => {\n run(() => linkAndWear(view)).catch(reportErr)\n }}\n />\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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"}
@@ -17,5 +17,24 @@
17
17
  * endorsement.
18
18
  */
19
19
  import React from 'react';
20
+ import type { AvatarSettings, LinkProvider } from '@wolffm/prefs-client/avatar';
21
+ /**
22
+ * Link `provider` in a popup, and wear its picture when it came back with one:
23
+ * linking from either place means "use this". Call it inside the click, with
24
+ * nothing awaited first — the popup opens before its first await, which is
25
+ * what popup blockers allow.
26
+ */
27
+ export declare function linkAndWear(provider: LinkProvider): Promise<AvatarSettings>;
28
+ /**
29
+ * What linking reads and keeps, then Continue. Shared by Account → Linked
30
+ * accounts and the avatar dialog's Picture tab, so the consent is one text.
31
+ */
32
+ export declare function LinkConsent({ provider, busy, onLink, onCancel }: {
33
+ provider: LinkProvider;
34
+ busy: boolean;
35
+ /** Must open the popup synchronously: call {@link linkAndWear} directly. */
36
+ onLink: () => void;
37
+ onCancel?: () => void;
38
+ }): React.JSX.Element;
20
39
  export declare function LinkedAccounts(): React.JSX.Element;
21
40
  //# sourceMappingURL=LinkedAccounts.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"LinkedAccounts.d.ts","sourceRoot":"","sources":["../../src/components/LinkedAccounts.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH,OAAO,KAA8B,MAAM,OAAO,CAAA;AAmClD,wBAAgB,cAAc,sBA0H7B"}
1
+ {"version":3,"file":"LinkedAccounts.d.ts","sourceRoot":"","sources":["../../src/components/LinkedAccounts.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH,OAAO,KAA8B,MAAM,OAAO,CAAA;AAClD,OAAO,KAAK,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAA;AAkC/E;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,QAAQ,EAAE,YAAY,GAAG,OAAO,CAAC,cAAc,CAAC,CAI3E;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,EAC1B,QAAQ,EACR,IAAI,EACJ,MAAM,EACN,QAAQ,EACT,EAAE;IACD,QAAQ,EAAE,YAAY,CAAA;IACtB,IAAI,EAAE,OAAO,CAAA;IACb,4EAA4E;IAC5E,MAAM,EAAE,MAAM,IAAI,CAAA;IAClB,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;CACtB,qBAyBA;AAED,wBAAgB,cAAc,sBAoG7B"}
@@ -29,6 +29,32 @@ const S = {
29
29
  grow: { flex: 1, minWidth: 0 },
30
30
  list: { margin: 0, paddingLeft: 18, fontSize: "0.75rem", lineHeight: 1.45 }
31
31
  };
32
+ function linkAndWear(provider) {
33
+ return avatarClient.linkInPopup(provider).then((next) => next.linked[provider]?.avatarUrl ? avatarClient.choose(provider) : next);
34
+ }
35
+ function LinkConsent({
36
+ provider,
37
+ busy,
38
+ onLink,
39
+ onCancel
40
+ }) {
41
+ return /* @__PURE__ */ jsxs("div", { style: S.stack, children: [
42
+ /* @__PURE__ */ jsx("ul", { style: S.list, children: CONSENT[provider].points.map((t) => /* @__PURE__ */ jsx("li", { children: t }, t)) }),
43
+ /* @__PURE__ */ jsxs("span", { style: S.line, children: [
44
+ /* @__PURE__ */ jsx(
45
+ "button",
46
+ {
47
+ type: "button",
48
+ className: "settings-popout__btn settings-popout__btn--primary",
49
+ disabled: busy,
50
+ onClick: onLink,
51
+ children: CONSENT[provider].go
52
+ }
53
+ ),
54
+ onCancel && /* @__PURE__ */ jsx("button", { type: "button", className: "settings-popout__btn", onClick: onCancel, children: "Cancel" })
55
+ ] })
56
+ ] });
57
+ }
32
58
  function LinkedAccounts() {
33
59
  const [settings, setSettings] = useState(null);
34
60
  const [busy, setBusy] = useState(false);
@@ -57,13 +83,6 @@ function LinkedAccounts() {
57
83
  if (!settings) {
58
84
  return /* @__PURE__ */ jsx("span", { className: "settings-popout__hint", role: error ? "alert" : void 0, children: error ?? "…" });
59
85
  }
60
- const link = (provider) => {
61
- setConsent(null);
62
- void run(async () => {
63
- const next = await avatarClient.linkInPopup(provider);
64
- return next.linked[provider]?.avatarUrl ? avatarClient.choose(provider) : next;
65
- });
66
- };
67
86
  return /* @__PURE__ */ jsxs("div", { style: S.stack, children: [
68
87
  ["discord", "steam"].map((p) => {
69
88
  const linked = settings.linked[p];
@@ -94,26 +113,24 @@ function LinkedAccounts() {
94
113
  )
95
114
  ] }, p);
96
115
  }),
97
- consent && /* @__PURE__ */ jsxs("div", { style: S.stack, children: [
98
- /* @__PURE__ */ jsx("ul", { style: S.list, children: CONSENT[consent].points.map((t) => /* @__PURE__ */ jsx("li", { children: t }, t)) }),
99
- /* @__PURE__ */ jsxs("span", { style: S.line, children: [
100
- /* @__PURE__ */ jsx(
101
- "button",
102
- {
103
- type: "button",
104
- className: "settings-popout__btn settings-popout__btn--primary",
105
- disabled: busy,
106
- onClick: () => link(consent),
107
- children: CONSENT[consent].go
108
- }
109
- ),
110
- /* @__PURE__ */ jsx("button", { type: "button", className: "settings-popout__btn", onClick: () => setConsent(null), children: "Cancel" })
111
- ] })
112
- ] }),
116
+ consent && /* @__PURE__ */ jsx(
117
+ LinkConsent,
118
+ {
119
+ provider: consent,
120
+ busy,
121
+ onLink: () => {
122
+ setConsent(null);
123
+ void run(() => linkAndWear(consent));
124
+ },
125
+ onCancel: () => setConsent(null)
126
+ }
127
+ ),
113
128
  error && /* @__PURE__ */ jsx("span", { className: "settings-popout__error", role: "alert", children: error })
114
129
  ] });
115
130
  }
116
131
  export {
117
- LinkedAccounts
132
+ LinkConsent,
133
+ LinkedAccounts,
134
+ linkAndWear
118
135
  };
119
136
  //# sourceMappingURL=LinkedAccounts.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"LinkedAccounts.js","sources":["../../src/components/LinkedAccounts.tsx"],"sourcesContent":["/**\n * LinkedAccounts — Discord and Steam, linked and unlinked from the settings\n * popout's Account tab (operator, 2026-11-02: linking is an account matter, not\n * part of changing the avatar). A linked account's picture then shows up as a\n * choice in the avatar dialog's Picture tab.\n *\n * Mounted only with the Account tab, never when the gear opens elsewhere: the\n * read can refresh a linked picture from Discord or Steam, which opening the\n * gear must not cost (settings-prefetch e2e).\n *\n * Linking signs in through a popup (`linkInPopup`), so the page under the\n * popout never navigates and a game keeps running.\n *\n * COMPLIANCE (Steam Web API terms; Discord developer terms), carried over from\n * hadoku.me/account: nothing is fetched from a provider until the person reads\n * what is read and kept and presses Continue; no provider logos, and no implied\n * endorsement.\n */\nimport React, { useEffect, useState } from 'react'\nimport type { AvatarSettings, LinkProvider } from '@wolffm/prefs-client/avatar'\nimport { AVATAR_EVENT } from '../lib/avatarEvent'\nimport { avatarClient, avatarErrorText } from '../lib/avatarData'\n\nconst LABEL: Record<LinkProvider, string> = { discord: 'Discord', steam: 'Steam' }\n\nconst CONSENT: Record<LinkProvider, { points: string[]; go: string }> = {\n discord: {\n points: [\n 'Discord asks you to share your basic profile (\"identify\").',\n 'Kept: your Discord user ID and the address of your profile picture, refreshed while linked.',\n 'Not kept: your username, email, servers or any token.',\n 'Unlink deletes both.'\n ],\n go: 'Continue to Discord'\n },\n steam: {\n points: [\n 'You sign in on steamcommunity.com; your password never reaches hadoku.me.',\n 'Your public Steam profile picture is then read through the Steam Web API, now and periodically while linked.',\n 'Kept: your Steam ID and the address of that picture.',\n 'Unlink deletes both and stops the reads.'\n ],\n go: 'Continue to Steam'\n }\n}\n\nconst S = {\n stack: { display: 'flex', flexDirection: 'column', gap: 8, width: '100%' },\n line: { display: 'flex', alignItems: 'center', gap: 6, flexWrap: 'wrap' },\n grow: { flex: 1, minWidth: 0 },\n list: { margin: 0, paddingLeft: 18, fontSize: '0.75rem', lineHeight: 1.45 }\n} satisfies Record<string, React.CSSProperties>\n\nexport function LinkedAccounts() {\n const [settings, setSettings] = useState<AvatarSettings | null>(null)\n const [busy, setBusy] = useState(false)\n const [error, setError] = useState<string | null>(null)\n const [consent, setConsent] = useState<LinkProvider | null>(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 return () => {\n live = false\n }\n }, [])\n\n const run = async (work: () => Promise<AvatarSettings>) => {\n setBusy(true)\n setError(null)\n try {\n const next = await work()\n setSettings(next)\n // The page under the popout (a watchparty room, a game) re-reads the\n // avatar from this, as it does after a change in the avatar dialog.\n window.dispatchEvent(new window.CustomEvent<AvatarSettings>(AVATAR_EVENT, { detail: next }))\n } catch (err) {\n setError(avatarErrorText(err))\n } finally {\n setBusy(false)\n }\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 // Must stay synchronous up to linkInPopup: the popup opens before its first\n // await, inside this click, which is what popup blockers allow.\n const link = (provider: LinkProvider) => {\n setConsent(null)\n void run(async () => {\n const next = await avatarClient.linkInPopup(provider)\n // Linking is choosing, when the account came back with a picture.\n return next.linked[provider]?.avatarUrl ? avatarClient.choose(provider) : next\n })\n }\n\n return (\n <div style={S.stack}>\n {(['discord', 'steam'] as const).map(p => {\n const linked = settings.linked[p]\n return (\n <div key={p} style={S.line}>\n <span className=\"settings-popout__label\" style={S.grow}>\n {LABEL[p]}\n {!settings.providers[p]\n ? ' · not available yet'\n : linked\n ? linked.avatarUrl\n ? ' · linked'\n : ' · linked, no picture'\n : ''}\n </span>\n {settings.providers[p] && !linked && (\n <button\n type=\"button\"\n className=\"settings-popout__btn\"\n disabled={busy}\n onClick={() => setConsent(consent === p ? null : p)}\n >\n Link\n </button>\n )}\n {linked && (\n <button\n type=\"button\"\n className=\"settings-popout__btn\"\n disabled={busy}\n onClick={() => void run(() => avatarClient.unlink(p))}\n >\n Unlink\n </button>\n )}\n </div>\n )\n })}\n\n {consent && (\n <div style={S.stack}>\n <ul style={S.list}>\n {CONSENT[consent].points.map(t => (\n <li key={t}>{t}</li>\n ))}\n </ul>\n <span style={S.line}>\n <button\n type=\"button\"\n className=\"settings-popout__btn settings-popout__btn--primary\"\n disabled={busy}\n onClick={() => link(consent)}\n >\n {CONSENT[consent].go}\n </button>\n <button type=\"button\" className=\"settings-popout__btn\" onClick={() => setConsent(null)}>\n Cancel\n </button>\n </span>\n </div>\n )}\n\n {error && (\n <span className=\"settings-popout__error\" role=\"alert\">\n {error}\n </span>\n )}\n </div>\n )\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"LinkedAccounts.js","sources":["../../src/components/LinkedAccounts.tsx"],"sourcesContent":["/**\n * LinkedAccounts — Discord and Steam, linked and unlinked from the settings\n * popout's Account tab (operator, 2026-11-02: linking is an account matter, not\n * part of changing the avatar). A linked account's picture then shows up as a\n * choice in the avatar dialog's Picture tab.\n *\n * Mounted only with the Account tab, never when the gear opens elsewhere: the\n * read can refresh a linked picture from Discord or Steam, which opening the\n * gear must not cost (settings-prefetch e2e).\n *\n * Linking signs in through a popup (`linkInPopup`), so the page under the\n * popout never navigates and a game keeps running.\n *\n * COMPLIANCE (Steam Web API terms; Discord developer terms), carried over from\n * hadoku.me/account: nothing is fetched from a provider until the person reads\n * what is read and kept and presses Continue; no provider logos, and no implied\n * endorsement.\n */\nimport React, { useEffect, useState } from 'react'\nimport type { AvatarSettings, LinkProvider } from '@wolffm/prefs-client/avatar'\nimport { AVATAR_EVENT } from '../lib/avatarEvent'\nimport { avatarClient, avatarErrorText } from '../lib/avatarData'\n\nconst LABEL: Record<LinkProvider, string> = { discord: 'Discord', steam: 'Steam' }\n\nconst CONSENT: Record<LinkProvider, { points: string[]; go: string }> = {\n discord: {\n points: [\n 'Discord asks you to share your basic profile (\"identify\").',\n 'Kept: your Discord user ID and the address of your profile picture, refreshed while linked.',\n 'Not kept: your username, email, servers or any token.',\n 'Unlink deletes both.'\n ],\n go: 'Continue to Discord'\n },\n steam: {\n points: [\n 'You sign in on steamcommunity.com; your password never reaches hadoku.me.',\n 'Your public Steam profile picture is then read through the Steam Web API, now and periodically while linked.',\n 'Kept: your Steam ID and the address of that picture.',\n 'Unlink deletes both and stops the reads.'\n ],\n go: 'Continue to Steam'\n }\n}\n\nconst S = {\n stack: { display: 'flex', flexDirection: 'column', gap: 8, width: '100%' },\n line: { display: 'flex', alignItems: 'center', gap: 6, flexWrap: 'wrap' },\n grow: { flex: 1, minWidth: 0 },\n list: { margin: 0, paddingLeft: 18, fontSize: '0.75rem', lineHeight: 1.45 }\n} satisfies Record<string, React.CSSProperties>\n\n/**\n * Link `provider` in a popup, and wear its picture when it came back with one:\n * linking from either place means \"use this\". Call it inside the click, with\n * nothing awaited first — the popup opens before its first await, which is\n * what popup blockers allow.\n */\nexport function linkAndWear(provider: LinkProvider): Promise<AvatarSettings> {\n return avatarClient\n .linkInPopup(provider)\n .then(next => (next.linked[provider]?.avatarUrl ? avatarClient.choose(provider) : next))\n}\n\n/**\n * What linking reads and keeps, then Continue. Shared by Account → Linked\n * accounts and the avatar dialog's Picture tab, so the consent is one text.\n */\nexport function LinkConsent({\n provider,\n busy,\n onLink,\n onCancel\n}: {\n provider: LinkProvider\n busy: boolean\n /** Must open the popup synchronously: call {@link linkAndWear} directly. */\n onLink: () => void\n onCancel?: () => void\n}) {\n return (\n <div style={S.stack}>\n <ul style={S.list}>\n {CONSENT[provider].points.map(t => (\n <li key={t}>{t}</li>\n ))}\n </ul>\n <span style={S.line}>\n <button\n type=\"button\"\n className=\"settings-popout__btn settings-popout__btn--primary\"\n disabled={busy}\n onClick={onLink}\n >\n {CONSENT[provider].go}\n </button>\n {onCancel && (\n <button type=\"button\" className=\"settings-popout__btn\" onClick={onCancel}>\n Cancel\n </button>\n )}\n </span>\n </div>\n )\n}\n\nexport function LinkedAccounts() {\n const [settings, setSettings] = useState<AvatarSettings | null>(null)\n const [busy, setBusy] = useState(false)\n const [error, setError] = useState<string | null>(null)\n const [consent, setConsent] = useState<LinkProvider | null>(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 return () => {\n live = false\n }\n }, [])\n\n const run = async (work: () => Promise<AvatarSettings>) => {\n setBusy(true)\n setError(null)\n try {\n const next = await work()\n setSettings(next)\n // The page under the popout (a watchparty room, a game) re-reads the\n // avatar from this, as it does after a change in the avatar dialog.\n window.dispatchEvent(new window.CustomEvent<AvatarSettings>(AVATAR_EVENT, { detail: next }))\n } catch (err) {\n setError(avatarErrorText(err))\n } finally {\n setBusy(false)\n }\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 return (\n <div style={S.stack}>\n {(['discord', 'steam'] as const).map(p => {\n const linked = settings.linked[p]\n return (\n <div key={p} style={S.line}>\n <span className=\"settings-popout__label\" style={S.grow}>\n {LABEL[p]}\n {!settings.providers[p]\n ? ' · not available yet'\n : linked\n ? linked.avatarUrl\n ? ' · linked'\n : ' · linked, no picture'\n : ''}\n </span>\n {settings.providers[p] && !linked && (\n <button\n type=\"button\"\n className=\"settings-popout__btn\"\n disabled={busy}\n onClick={() => setConsent(consent === p ? null : p)}\n >\n Link\n </button>\n )}\n {linked && (\n <button\n type=\"button\"\n className=\"settings-popout__btn\"\n disabled={busy}\n onClick={() => void run(() => avatarClient.unlink(p))}\n >\n Unlink\n </button>\n )}\n </div>\n )\n })}\n\n {consent && (\n <LinkConsent\n provider={consent}\n busy={busy}\n onLink={() => {\n setConsent(null)\n void run(() => linkAndWear(consent))\n }}\n onCancel={() => setConsent(null)}\n />\n )}\n\n {error && (\n <span className=\"settings-popout__error\" role=\"alert\">\n {error}\n </span>\n )}\n </div>\n )\n}\n"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@hadoku/task-ui-components",
3
- "version": "4.23.13",
4
- "gitHead": "0b6541432e037dd38a49f7563fa240ffb1d97580",
3
+ "version": "4.23.14",
4
+ "gitHead": "3acf394e47261079db647505f45c7de943e00259",
5
5
  "description": "Reusable UI components with Bento grid and metallic gradient support",
6
6
  "type": "module",
7
7
  "main": "dist/index.js",
@@ -201,7 +201,12 @@ const S = {
201
201
  gap: 5,
202
202
  padding: 6,
203
203
  borderRadius: 8,
204
- border: '1px solid var(--color-border)',
204
+ // Longhands, not the `border` shorthand: the selected styles override
205
+ // borderColor, and when one comes off React clears borderColor to '', which
206
+ // would reset it to currentColor and leave a text-coloured ring behind.
207
+ borderWidth: 1,
208
+ borderStyle: 'solid',
209
+ borderColor: 'var(--color-border)',
205
210
  background: 'var(--color-bg)',
206
211
  color: 'var(--color-text)',
207
212
  cursor: 'pointer',
@@ -225,16 +230,7 @@ const S = {
225
230
  fontSize: '0.8rem'
226
231
  },
227
232
  tileName: { fontSize: '0.8rem', fontWeight: 500, display: 'flex', alignItems: 'center', gap: 5 },
228
- tileEarn: { fontSize: '0.7rem', lineHeight: 1.3, color: 'var(--color-text-muted)' },
229
- foot: {
230
- display: 'flex',
231
- justifyContent: 'space-between',
232
- alignItems: 'center',
233
- gap: 8,
234
- flexWrap: 'wrap',
235
- fontSize: '0.75rem',
236
- color: 'var(--color-text-muted)'
237
- }
233
+ tileEarn: { fontSize: '0.7rem', lineHeight: 1.3, color: 'var(--color-text-muted)' }
238
234
  } satisfies Record<string, React.CSSProperties>
239
235
 
240
236
  /**
@@ -867,77 +863,64 @@ function SlotGrid({
867
863
  onTry
868
864
  }: SlotGridProps) {
869
865
  const items = COSMETICS.filter(i => i.slot === slot && (!i.hidden || isAdmin))
870
- const locked = items.filter(i => !i.hidden && !owned.has(i.id))
871
866
 
872
867
  return (
873
- <>
874
- <div style={S.grid}>
875
- <button
876
- type="button"
877
- aria-pressed={equipped === null}
878
- style={{ ...S.tile, ...(equipped === null ? S.tileOn : {}) }}
879
- onClick={() => onEquip(null)}
880
- >
881
- <Html html={preview(null)} style={{ ...S.tilePv, ...colour }} />
882
- <span style={S.tileName}>None</span>
883
- </button>
884
- {items.map(item => {
885
- if (item.hidden) {
886
- return (
887
- <button
888
- key={item.id}
889
- type="button"
890
- disabled
891
- aria-label={`${item.label}, archived`}
892
- style={{ ...S.tile, cursor: 'not-allowed', opacity: 0.45 }}
893
- >
894
- <Html html={preview(item.id)} style={{ ...S.tilePv, ...colour }} />
895
- <span style={S.tileName}>{item.label}</span>
896
- <span style={S.tileEarn}>Archived</span>
897
- </button>
898
- )
899
- }
900
- const own = owned.has(item.id)
901
- const on = equipped === item.id
902
- const trying = tryOn?.slot === slot && tryOn.id === item.id
868
+ <div style={S.grid}>
869
+ <button
870
+ type="button"
871
+ aria-pressed={equipped === null}
872
+ style={{ ...S.tile, ...(equipped === null ? S.tileOn : {}) }}
873
+ onClick={() => onEquip(null)}
874
+ >
875
+ <Html html={preview(null)} style={{ ...S.tilePv, ...colour }} />
876
+ <span style={S.tileName}>None</span>
877
+ </button>
878
+ {items.map(item => {
879
+ if (item.hidden) {
903
880
  return (
904
881
  <button
905
882
  key={item.id}
906
883
  type="button"
907
- aria-pressed={own ? on : trying}
908
- aria-label={
909
- own ? item.label : `${item.label}, locked. ${item.earn ?? ''}. Try it on.`
910
- }
911
- style={{
912
- ...S.tile,
913
- ...(on ? S.tileOn : {}),
914
- ...(trying ? S.tileTrying : {})
915
- }}
916
- onClick={() => (own ? onEquip(item.id) : onTry(item.id))}
917
- // A message reveal plays once; hovering its tile plays it again.
918
- onMouseEnter={e => slot === 'message' && replayPreview(e.currentTarget)}
884
+ disabled
885
+ aria-label={`${item.label}, archived`}
886
+ style={{ ...S.tile, cursor: 'not-allowed', opacity: 0.45 }}
919
887
  >
920
- <Html
921
- html={preview(item.id)}
922
- style={{ ...S.tilePv, ...colour, opacity: own ? 1 : 0.55 }}
923
- />
924
- <span style={{ ...S.tileName, color: own ? undefined : 'var(--color-text-muted)' }}>
925
- {!own && <Icon name="lock" size={12} />}
926
- {item.label}
927
- </span>
928
- {!own && <span style={S.tileEarn}>{item.earn}</span>}
888
+ <Html html={preview(item.id)} style={{ ...S.tilePv, ...colour }} />
889
+ <span style={S.tileName}>{item.label}</span>
890
+ <span style={S.tileEarn}>Archived</span>
929
891
  </button>
930
892
  )
931
- })}
932
- </div>
933
- {locked.length > 0 && (
934
- <div style={S.foot}>
935
- <span>
936
- {locked.length === 1 ? 'One to earn.' : `${locked.length} to earn.`} Pick one to try it
937
- on.
938
- </span>
939
- </div>
940
- )}
941
- </>
893
+ }
894
+ const own = owned.has(item.id)
895
+ const on = equipped === item.id
896
+ const trying = tryOn?.slot === slot && tryOn.id === item.id
897
+ return (
898
+ <button
899
+ key={item.id}
900
+ type="button"
901
+ aria-pressed={own ? on : trying}
902
+ aria-label={own ? item.label : `${item.label}, locked. ${item.earn ?? ''}. Try it on.`}
903
+ style={{
904
+ ...S.tile,
905
+ ...(on ? S.tileOn : {}),
906
+ ...(trying ? S.tileTrying : {})
907
+ }}
908
+ onClick={() => (own ? onEquip(item.id) : onTry(item.id))}
909
+ // A message reveal plays once; hovering its tile plays it again.
910
+ onMouseEnter={e => slot === 'message' && replayPreview(e.currentTarget)}
911
+ >
912
+ <Html
913
+ html={preview(item.id)}
914
+ style={{ ...S.tilePv, ...colour, opacity: own ? 1 : 0.55 }}
915
+ />
916
+ <span style={{ ...S.tileName, color: own ? undefined : 'var(--color-text-muted)' }}>
917
+ {!own && <Icon name="lock" size={12} />}
918
+ {item.label}
919
+ </span>
920
+ {!own && <span style={S.tileEarn}>{item.earn}</span>}
921
+ </button>
922
+ )
923
+ })}
924
+ </div>
942
925
  )
943
926
  }
@@ -7,8 +7,9 @@
7
7
  * protocol cannot drift from the callback page).
8
8
  *
9
9
  * Everything happens in place: picking, uploading and removing are requests
10
- * from here. Linking Discord or Steam lives in the popout's Account tab
11
- * (LinkedAccounts); a linked account's picture is a choice here.
10
+ * from here. Discord and Steam are managed in the popout's Account tab
11
+ * (LinkedAccounts); one not linked yet can also be linked from its tile here,
12
+ * with the same consent step, and its picture is then worn.
12
13
  *
13
14
  * AvatarBuilder owns the settings and the character table and loads them only
14
15
  * when the dialog opens; this component draws them and sends changes through
@@ -21,6 +22,7 @@
21
22
  import React, { useEffect, useState } from 'react'
22
23
  import { type AvatarSettings, type AvatarSource } from '@wolffm/prefs-client/avatar'
23
24
  import { avatarClient, portraitUrl, type CharacterSet } from '../lib/avatarData'
25
+ import { LinkConsent, linkAndWear } from './LinkedAccounts'
24
26
 
25
27
  const LABEL: Record<AvatarSource, string> = {
26
28
  sprite: 'Character',
@@ -40,7 +42,12 @@ const S = {
40
42
  gap: 4,
41
43
  padding: 6,
42
44
  borderRadius: 8,
43
- border: '1px solid var(--color-border)',
45
+ // Longhands, not the `border` shorthand: the selected styles override
46
+ // borderColor, and when one comes off React clears borderColor to '', which
47
+ // would reset it to currentColor and leave a text-coloured ring behind.
48
+ borderWidth: 1,
49
+ borderStyle: 'solid',
50
+ borderColor: 'var(--color-border)',
44
51
  background: 'var(--color-bg)',
45
52
  color: 'var(--color-text)',
46
53
  fontSize: '0.72rem',
@@ -128,11 +135,10 @@ export function AvatarPicker({ settings, sets, busy, run }: AvatarPickerProps) {
128
135
  }
129
136
  const shown = settings.avatar
130
137
  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.
138
+ // The tab on show. Picking an available source also wears it; one that is
139
+ // not yet opens to get it: Upload to choose a file, Discord or Steam to link.
133
140
  const [view, setView] = useState<AvatarSource>(shown.source)
134
141
  useEffect(() => setView(shown.source), [shown.source])
135
- const openable = (src: AvatarSource) => src === 'upload' || available[src]
136
142
 
137
143
  return (
138
144
  <div style={S.stack}>
@@ -149,13 +155,8 @@ export function AvatarPicker({ settings, sets, busy, run }: AvatarPickerProps) {
149
155
  type="button"
150
156
  role="radio"
151
157
  aria-checked={view === src}
152
- disabled={busy || !openable(src)}
153
- title={openable(src) ? undefined : `Link ${LABEL[src]} under Settings › Account first`}
154
- style={{
155
- ...S.source,
156
- ...(view === src ? S.sourceOn : {}),
157
- opacity: openable(src) ? 1 : 0.5
158
- }}
158
+ disabled={busy}
159
+ style={{ ...S.source, ...(view === src ? S.sourceOn : {}) }}
159
160
  onClick={() => {
160
161
  setView(src)
161
162
  if (available[src] && shown.source !== src) {
@@ -213,6 +214,23 @@ export function AvatarPicker({ settings, sets, busy, run }: AvatarPickerProps) {
213
214
  <span className="settings-popout__hint">…</span>
214
215
  ))}
215
216
 
217
+ {(view === 'discord' || view === 'steam') &&
218
+ !available[view] &&
219
+ (!settings.providers[view] ? (
220
+ <span className="settings-popout__hint">{LABEL[view]} is not available yet.</span>
221
+ ) : settings.linked[view] ? (
222
+ <span className="settings-popout__hint">Your {LABEL[view]} account has no picture.</span>
223
+ ) : (
224
+ <LinkConsent
225
+ key={view}
226
+ provider={view}
227
+ busy={busy}
228
+ onLink={() => {
229
+ run(() => linkAndWear(view)).catch(reportErr)
230
+ }}
231
+ />
232
+ ))}
233
+
216
234
  {view === 'upload' && (
217
235
  <div style={S.line}>
218
236
  <span className="settings-popout__label" style={S.grow}>
@@ -51,6 +51,60 @@ const S = {
51
51
  list: { margin: 0, paddingLeft: 18, fontSize: '0.75rem', lineHeight: 1.45 }
52
52
  } satisfies Record<string, React.CSSProperties>
53
53
 
54
+ /**
55
+ * Link `provider` in a popup, and wear its picture when it came back with one:
56
+ * linking from either place means "use this". Call it inside the click, with
57
+ * nothing awaited first — the popup opens before its first await, which is
58
+ * what popup blockers allow.
59
+ */
60
+ export function linkAndWear(provider: LinkProvider): Promise<AvatarSettings> {
61
+ return avatarClient
62
+ .linkInPopup(provider)
63
+ .then(next => (next.linked[provider]?.avatarUrl ? avatarClient.choose(provider) : next))
64
+ }
65
+
66
+ /**
67
+ * What linking reads and keeps, then Continue. Shared by Account → Linked
68
+ * accounts and the avatar dialog's Picture tab, so the consent is one text.
69
+ */
70
+ export function LinkConsent({
71
+ provider,
72
+ busy,
73
+ onLink,
74
+ onCancel
75
+ }: {
76
+ provider: LinkProvider
77
+ busy: boolean
78
+ /** Must open the popup synchronously: call {@link linkAndWear} directly. */
79
+ onLink: () => void
80
+ onCancel?: () => void
81
+ }) {
82
+ return (
83
+ <div style={S.stack}>
84
+ <ul style={S.list}>
85
+ {CONSENT[provider].points.map(t => (
86
+ <li key={t}>{t}</li>
87
+ ))}
88
+ </ul>
89
+ <span style={S.line}>
90
+ <button
91
+ type="button"
92
+ className="settings-popout__btn settings-popout__btn--primary"
93
+ disabled={busy}
94
+ onClick={onLink}
95
+ >
96
+ {CONSENT[provider].go}
97
+ </button>
98
+ {onCancel && (
99
+ <button type="button" className="settings-popout__btn" onClick={onCancel}>
100
+ Cancel
101
+ </button>
102
+ )}
103
+ </span>
104
+ </div>
105
+ )
106
+ }
107
+
54
108
  export function LinkedAccounts() {
55
109
  const [settings, setSettings] = useState<AvatarSettings | null>(null)
56
110
  const [busy, setBusy] = useState(false)
@@ -92,17 +146,6 @@ export function LinkedAccounts() {
92
146
  )
93
147
  }
94
148
 
95
- // Must stay synchronous up to linkInPopup: the popup opens before its first
96
- // await, inside this click, which is what popup blockers allow.
97
- const link = (provider: LinkProvider) => {
98
- setConsent(null)
99
- void run(async () => {
100
- const next = await avatarClient.linkInPopup(provider)
101
- // Linking is choosing, when the account came back with a picture.
102
- return next.linked[provider]?.avatarUrl ? avatarClient.choose(provider) : next
103
- })
104
- }
105
-
106
149
  return (
107
150
  <div style={S.stack}>
108
151
  {(['discord', 'steam'] as const).map(p => {
@@ -144,26 +187,15 @@ export function LinkedAccounts() {
144
187
  })}
145
188
 
146
189
  {consent && (
147
- <div style={S.stack}>
148
- <ul style={S.list}>
149
- {CONSENT[consent].points.map(t => (
150
- <li key={t}>{t}</li>
151
- ))}
152
- </ul>
153
- <span style={S.line}>
154
- <button
155
- type="button"
156
- className="settings-popout__btn settings-popout__btn--primary"
157
- disabled={busy}
158
- onClick={() => link(consent)}
159
- >
160
- {CONSENT[consent].go}
161
- </button>
162
- <button type="button" className="settings-popout__btn" onClick={() => setConsent(null)}>
163
- Cancel
164
- </button>
165
- </span>
166
- </div>
190
+ <LinkConsent
191
+ provider={consent}
192
+ busy={busy}
193
+ onLink={() => {
194
+ setConsent(null)
195
+ void run(() => linkAndWear(consent))
196
+ }}
197
+ onCancel={() => setConsent(null)}
198
+ />
167
199
  )}
168
200
 
169
201
  {error && (