@hadoku/task-ui-components 4.23.5 → 4.23.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/dist/components/AvatarBuilder.d.ts +28 -0
  2. package/dist/components/AvatarBuilder.d.ts.map +1 -0
  3. package/dist/components/AvatarBuilder.js +731 -0
  4. package/dist/components/AvatarBuilder.js.map +1 -0
  5. package/dist/components/AvatarDialog.d.ts +6 -4
  6. package/dist/components/AvatarDialog.d.ts.map +1 -1
  7. package/dist/components/AvatarDialog.js +3 -3
  8. package/dist/components/AvatarDialog.js.map +1 -1
  9. package/dist/components/AvatarPicker.d.ts +21 -18
  10. package/dist/components/AvatarPicker.d.ts.map +1 -1
  11. package/dist/components/AvatarPicker.js +17 -145
  12. package/dist/components/AvatarPicker.js.map +1 -1
  13. package/dist/components/ConnectedSettings.d.ts.map +1 -1
  14. package/dist/components/ConnectedSettings.js +20 -1
  15. package/dist/components/ConnectedSettings.js.map +1 -1
  16. package/dist/components/LinkedAccounts.d.ts +21 -0
  17. package/dist/components/LinkedAccounts.d.ts.map +1 -0
  18. package/dist/components/LinkedAccounts.js +119 -0
  19. package/dist/components/LinkedAccounts.js.map +1 -0
  20. package/dist/lib/avatarData.d.ts +27 -0
  21. package/dist/lib/avatarData.d.ts.map +1 -0
  22. package/dist/lib/avatarData.js +23 -0
  23. package/dist/lib/avatarData.js.map +1 -0
  24. package/dist/vendor/wolffm/prefs-client/avatar.js +7 -1
  25. package/dist/vendor/wolffm/prefs-client/avatar.js.map +1 -1
  26. package/dist/vendor/wolffm/prefs-client/cosmetics.js +491 -0
  27. package/dist/vendor/wolffm/prefs-client/cosmetics.js.map +1 -0
  28. package/package.json +2 -2
  29. package/src/components/AvatarBuilder.tsx +901 -0
  30. package/src/components/AvatarDialog.tsx +8 -6
  31. package/src/components/AvatarPicker.tsx +45 -228
  32. package/src/components/ConnectedSettings.tsx +26 -0
  33. package/src/components/LinkedAccounts.tsx +176 -0
  34. package/src/lib/avatarData.ts +40 -0
@@ -0,0 +1 @@
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): earned items equip on click; items not yet earned show how to earn\n * them and can be tried on in the preview, which saves nothing. An overlay\n * image can be placed: drag it on the big preview, turn it with the top handle,\n * resize it with the corner handle, or use the sliders. Every move goes through\n * `fitPlacement`, the same function prefs-api runs before it stores one, so the\n * preview never shows a spot the server would refuse.\n *\n * Styles are inline for the reason AvatarPicker's are: apps compile this\n * package's CSS at their own build. Tokens only; the cosmetics' own colours come\n * from the person's character colour (and, for overlay images, the art).\n */\nimport React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'\nimport { Icon } from '@wolffm/themes'\nimport type { AvatarSettings, LoadoutUpdate } from '@wolffm/prefs-client/avatar'\nimport {\n COSMETICS,\n COSMETICS_BY_ID,\n COSMETIC_SLOTS,\n PLACEMENT_DEFAULT,\n PLACEMENT_LIMITS,\n avatarHtml,\n cleanLoadout,\n ensureCosmeticsStyles,\n fitPlacement,\n messageHtml,\n nameHtml,\n placementTransform,\n wornCosmetics,\n type AvatarCosmetics,\n type CosmeticSlot,\n type Loadout,\n type OverlayArt,\n type Placement\n} from '@wolffm/prefs-client/cosmetics'\nimport { AvatarPicker } from './AvatarPicker'\nimport { AVATAR_EVENT } from '../lib/avatarEvent'\nimport {\n avatarClient,\n avatarErrorText,\n fetchCharacters,\n portraitUrl,\n type CharacterSet\n} from '../lib/avatarData'\n\ntype Tab = 'picture' | CosmeticSlot\n\nconst SLOT_LABEL: Record<CosmeticSlot, string> = {\n effect: 'Effect',\n gradient: 'Gradient',\n font: 'Font',\n overlay: 'Overlay',\n message: 'Message'\n}\n\nconst SLIDERS = [\n ['x', 'Left/right', '%'],\n ['y', 'Up/down', '%'],\n ['r', 'Rotate', '°'],\n ['s', 'Size', '%']\n] as const\n\nconst S = {\n body: { display: 'grid', gap: 16, alignItems: 'start' },\n side: { display: 'flex', flexDirection: 'column', gap: 12, minWidth: 0 },\n stage: {\n position: 'relative',\n display: 'flex',\n flexDirection: 'column',\n alignItems: 'center',\n gap: 12,\n padding: '48px 12px 18px',\n borderRadius: 10,\n background: 'var(--color-bg-alt)',\n border: '1px solid var(--color-border)',\n touchAction: 'none',\n userSelect: 'none'\n },\n tryOn: {\n position: 'absolute',\n left: 8,\n right: 8,\n top: 8,\n display: 'flex',\n alignItems: 'center',\n gap: 6,\n padding: '4px 8px',\n borderRadius: 6,\n background: 'var(--color-primary-bg)',\n color: 'var(--color-on-primary-bg)',\n fontSize: '0.72rem'\n },\n linkBtn: {\n border: 0,\n background: 'none',\n color: 'inherit',\n textDecoration: 'underline',\n cursor: 'pointer',\n fontSize: 'inherit',\n padding: 0,\n flex: 'none'\n },\n chat: {\n display: 'flex',\n flexDirection: 'column',\n gap: 10,\n padding: 12,\n borderRadius: 10,\n background: 'var(--color-bg)',\n border: '1px solid var(--color-border)'\n },\n chatHead: {\n display: 'flex',\n justifyContent: 'space-between',\n alignItems: 'center',\n fontSize: '0.72rem',\n color: 'var(--color-text-muted)'\n },\n msg: { display: 'flex', gap: 10, alignItems: 'flex-start' },\n who: { minHeight: 22, display: 'flex', alignItems: 'center' },\n line: { fontSize: '0.85rem', marginTop: 2 },\n place: {\n display: 'flex',\n flexDirection: 'column',\n gap: 8,\n padding: 12,\n borderRadius: 10,\n border: '1px solid var(--color-border)'\n },\n placeHead: { display: 'flex', flexDirection: 'column', gap: 2, fontSize: '0.8rem' },\n hint: { fontSize: '0.72rem', color: 'var(--color-text-muted)' },\n slider: {\n display: 'grid',\n gridTemplateColumns: '64px 1fr 40px',\n alignItems: 'center',\n gap: 6,\n fontSize: '0.75rem',\n color: 'var(--color-text-muted)'\n },\n out: { textAlign: 'right', color: 'var(--color-text)', fontVariantNumeric: 'tabular-nums' },\n toggles: {\n display: 'flex',\n flexWrap: 'wrap',\n gap: 10,\n alignItems: 'center',\n fontSize: '0.75rem',\n color: 'var(--color-text-muted)'\n },\n tabs: {\n display: 'flex',\n gap: 2,\n cursor: 'grab',\n touchAction: 'pan-x',\n borderBottom: '1px solid var(--color-border)',\n overflowX: 'auto',\n scrollbarWidth: 'none'\n },\n tab: {\n flex: 'none',\n padding: '7px 11px',\n borderBottomWidth: 2,\n borderBottomStyle: 'solid',\n borderBottomColor: 'transparent',\n borderTop: 0,\n borderLeft: 0,\n borderRight: 0,\n background: 'none',\n cursor: 'pointer',\n fontSize: '0.82rem',\n fontWeight: 500,\n color: 'var(--color-text-muted)'\n },\n tabOn: { color: 'var(--color-text)', borderBottomColor: 'var(--color-primary)' },\n count: { fontSize: '0.68rem', marginLeft: 4, opacity: 0.8, fontVariantNumeric: 'tabular-nums' },\n pane: { display: 'flex', flexDirection: 'column', gap: 10, paddingTop: 10 },\n grid: { display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(112px, 1fr))', gap: 8 },\n tile: {\n position: 'relative',\n display: 'flex',\n flexDirection: 'column',\n gap: 5,\n padding: 6,\n borderRadius: 8,\n border: '1px solid var(--color-border)',\n background: 'var(--color-bg)',\n color: 'var(--color-text)',\n cursor: 'pointer',\n textAlign: 'left',\n font: 'inherit'\n },\n tileOn: {\n borderColor: 'var(--color-primary)',\n boxShadow: 'inset 0 0 0 1px var(--color-primary)'\n },\n tileTrying: { borderStyle: 'dashed', borderColor: 'var(--color-primary)' },\n tilePv: {\n height: 58,\n borderRadius: 6,\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center',\n background: 'var(--color-bg-alt)',\n overflow: 'hidden',\n padding: '0 8px',\n fontSize: '0.8rem'\n },\n tileName: { fontSize: '0.8rem', fontWeight: 500, display: 'flex', alignItems: 'center', gap: 5 },\n tileEarn: { fontSize: '0.7rem', lineHeight: 1.3, color: 'var(--color-text-muted)' },\n foot: {\n display: 'flex',\n justifyContent: 'space-between',\n alignItems: 'center',\n gap: 8,\n flexWrap: 'wrap',\n fontSize: '0.75rem',\n color: 'var(--color-text-muted)'\n }\n} satisfies Record<string, React.CSSProperties>\n\n/**\n * Trusted HTML from the cosmetics renderer, which escapes every string it is\n * given. Memoised on the string and the style: React re-sets innerHTML on every\n * render even when the string is the same, and replacing the nodes under the\n * pointer between mousedown and mouseup swallows the click.\n */\nconst Html = React.memo(\n function Html({ html, style }: { html: string; style?: React.CSSProperties }) {\n return <span style={style} dangerouslySetInnerHTML={{ __html: html }} />\n },\n (a, b) => a.html === b.html && JSON.stringify(a.style) === JSON.stringify(b.style)\n)\n\n/**\n * Play a tile's message reveal again. A finished reveal holds no animation (it\n * fills backwards only), so its markup is re-set. This runs synchronously in\n * mouseenter, before any press, so it cannot split a click; the memo above keeps\n * React from touching it afterwards.\n */\nfunction replayPreview(tile: HTMLElement) {\n const pv = tile.firstElementChild\n if (!pv) return\n const markup = pv.innerHTML\n pv.innerHTML = markup\n}\n\nfunction useNarrow(): boolean {\n const query = '(max-width: 640px)'\n const [narrow, setNarrow] = useState(() => window.matchMedia(query).matches)\n useEffect(() => {\n const mq = window.matchMedia(query)\n const on = () => setNarrow(mq.matches)\n mq.addEventListener('change', on)\n return () => mq.removeEventListener('change', on)\n }, [])\n return narrow\n}\n\n/** Move every drawing of one overlay in `root` to `p`, without re-rendering (smooth while dragging). */\nfunction applyPlacement(root: HTMLElement | null, id: string, p: Placement) {\n root?.querySelectorAll<HTMLElement>(`.hc-art[data-hc-art=\"${id}\"]`).forEach(ol => {\n ol.style.zIndex = p.back ? '0' : '3'\n const adj = ol.firstElementChild as HTMLElement | null\n if (adj) adj.style.transform = placementTransform(p)\n })\n}\n\n/** Settings with a new loadout, and what is worn recomputed from it. */\nfunction withLoadout(s: AvatarSettings, loadout: Loadout): AvatarSettings {\n return {\n ...s,\n cosmetics: { ...s.cosmetics, loadout },\n avatar: { ...s.avatar, cosmetics: wornCosmetics(loadout) }\n }\n}\n\n/**\n * A row that overflows sideways scrolls by dragging it (the operator's ask: the\n * tab strip is wider than its column and hides its scrollbar) and by the\n * mouse wheel. A drag of more than a few pixels is not a click.\n */\nfunction useDragScroll<T extends HTMLElement>() {\n // A callback ref, not useRef: the row appears only once the settings load,\n // after the first render, and the listeners must attach when it does.\n const [el, setEl] = useState<T | null>(null)\n useEffect(() => {\n if (!el) return\n let start: { x: number; left: number } | null = null\n let dragged = false\n const down = (e: PointerEvent) => {\n if (e.button !== 0) return\n start = { x: e.clientX, left: el.scrollLeft }\n dragged = false\n }\n const move = (e: PointerEvent) => {\n if (!start) return\n const dx = e.clientX - start.x\n if (!dragged && Math.abs(dx) > 4) {\n dragged = true\n el.setPointerCapture(e.pointerId)\n el.style.cursor = 'grabbing'\n }\n if (dragged) el.scrollLeft = start.left - dx\n }\n const up = () => {\n start = null\n el.style.cursor = ''\n }\n // The click that ends a drag is not a tab choice.\n const click = (e: MouseEvent) => {\n if (!dragged) return\n e.preventDefault()\n e.stopPropagation()\n dragged = false\n }\n const wheel = (e: globalThis.WheelEvent) => {\n if (el.scrollWidth <= el.clientWidth || Math.abs(e.deltaX) > Math.abs(e.deltaY)) return\n e.preventDefault()\n el.scrollLeft += e.deltaY\n }\n el.addEventListener('pointerdown', down)\n el.addEventListener('pointermove', move)\n el.addEventListener('pointerup', up)\n el.addEventListener('pointercancel', up)\n el.addEventListener('click', click, true)\n el.addEventListener('wheel', wheel, { passive: false })\n return () => {\n el.removeEventListener('pointerdown', down)\n el.removeEventListener('pointermove', move)\n el.removeEventListener('pointerup', up)\n el.removeEventListener('pointercancel', up)\n el.removeEventListener('click', click, true)\n el.removeEventListener('wheel', wheel)\n }\n }, [el])\n return setEl\n}\n\nexport interface AvatarBuilderProps {\n /** The person's display name, for the preview. */\n name: string | null\n}\n\nexport function AvatarBuilder({ name }: AvatarBuilderProps) {\n const [settings, setSettings] = useState<AvatarSettings | null>(null)\n const [sets, setSets] = useState<CharacterSet[] | null>(null)\n const [busy, setBusy] = useState(false)\n const [error, setError] = useState<string | null>(null)\n const [tab, setTab] = useState<Tab>('picture')\n const [tryOn, setTryOn] = useState<{ slot: CosmeticSlot; id: string } | null>(null)\n const [showLocked, setShowLocked] = useState(true)\n const [replay, setReplay] = useState(0)\n const narrow = useNarrow()\n const root = useRef<HTMLDivElement>(null)\n const tabStrip = useDragScroll<HTMLDivElement>()\n // Only the newest cosmetics request may set state: equips sent quickly in a\n // row can answer out of order.\n const seq = useRef(0)\n\n useEffect(() => ensureCosmeticsStyles(), [])\n\n useEffect(() => {\n let live = true\n avatarClient\n .get()\n .then(s => live && setSettings(s))\n .catch((err: unknown) => live && setError(avatarErrorText(err)))\n fetchCharacters()\n .then(s => live && setSets(s))\n .catch((err: unknown) => live && setError(avatarErrorText(err)))\n return () => {\n live = false\n }\n }, [])\n\n const announce = (next: AvatarSettings) =>\n // The page under the popout (a watchparty room, a game) re-reads the avatar\n // from this instead of waiting for its next join.\n window.dispatchEvent(new window.CustomEvent<AvatarSettings>(AVATAR_EVENT, { detail: next }))\n\n const run = useCallback(async (work: () => Promise<AvatarSettings>) => {\n setBusy(true)\n setError(null)\n try {\n const next = await work()\n setSettings(next)\n announce(next)\n } catch (err) {\n setError(avatarErrorText(err))\n } finally {\n setBusy(false)\n }\n }, [])\n\n /** Save a cosmetics change, showing `optimistic` at once. A failure re-reads the truth. */\n const wear = useCallback(\n async (update: LoadoutUpdate, optimistic: (s: AvatarSettings) => AvatarSettings) => {\n const mine = ++seq.current\n setError(null)\n setSettings(s => (s ? optimistic(s) : s))\n try {\n const next = await avatarClient.setCosmetics(update)\n if (mine !== seq.current) return\n setSettings(next)\n announce(next)\n } catch (err) {\n if (mine !== seq.current) return\n setError(avatarErrorText(err))\n avatarClient\n .get()\n .then(s => mine === seq.current && setSettings(s))\n .catch((e: unknown) => setError(avatarErrorText(e)))\n }\n },\n []\n )\n\n const owned = useMemo(() => new Set(settings?.cosmetics.owned ?? []), [settings])\n const loadout = settings?.cosmetics.loadout\n const equippedOverlay = loadout?.equipped.overlay ?? null\n const art: OverlayArt | undefined = equippedOverlay\n ? COSMETICS_BY_ID.get(equippedOverlay)?.art\n : undefined\n const editing = tab === 'overlay' && !tryOn && !!art && !!equippedOverlay\n\n // The placement being edited, live; saved a beat after the last change.\n const [live, setLive] = useState<Placement>(PLACEMENT_DEFAULT)\n const saveTimer = useRef<ReturnType<typeof setTimeout>>(undefined)\n const dragging = useRef(false)\n useEffect(() => {\n if (dragging.current || !equippedOverlay) return\n setLive(loadout?.placement[equippedOverlay] ?? PLACEMENT_DEFAULT)\n }, [equippedOverlay, loadout])\n useEffect(() => () => clearTimeout(saveTimer.current), [])\n\n const place = useCallback(\n (raw: Partial<Placement>, save: 'later' | 'now' | 'no') => {\n if (!art || !equippedOverlay) return\n const p = fitPlacement(art, raw)\n setLive(p)\n applyPlacement(root.current, equippedOverlay, p)\n clearTimeout(saveTimer.current)\n if (save === 'no') return\n const commit = () =>\n void wear({ placement: { [equippedOverlay]: p } }, s =>\n withLoadout(s, {\n ...s.cosmetics.loadout,\n placement: { ...s.cosmetics.loadout.placement, [equippedOverlay]: p }\n })\n )\n if (save === 'now') commit()\n else saveTimer.current = setTimeout(commit, 400)\n },\n [art, equippedOverlay, wear]\n )\n\n if (!settings) {\n return (\n <span className=\"settings-popout__hint\" role={error ? 'alert' : undefined}>\n {error ?? '…'}\n </span>\n )\n }\n\n const characters = new Map((sets ?? []).flatMap(s => s.characters).map(c => [c.id, c]))\n const character = settings.sprite ? characters.get(settings.sprite) : undefined\n const picture = settings.avatar.url ?? (character ? portraitUrl(character) : null)\n // Every name cosmetic derives its colours from the character's.\n const colour: React.CSSProperties =\n character && /^#[0-9a-f]{6}$/i.test(character.color)\n ? ({ '--hc-base': character.color } as React.CSSProperties)\n : {}\n const displayName = name || 'You'\n\n const worn: AvatarCosmetics = { ...(settings.avatar.cosmetics ?? {}) }\n if (tryOn) {\n worn[tryOn.slot] = tryOn.id\n if (tryOn.slot === 'overlay') delete worn.placement\n }\n\n const equip = (slot: CosmeticSlot, id: string | null) => {\n setTryOn(null)\n void wear({ equip: { [slot]: id } }, s => {\n const l = s.cosmetics.loadout\n return withLoadout(s, cleanLoadout({ ...l, equipped: { ...l.equipped, [slot]: id } }, owned))\n })\n }\n\n const stage = (\n <Stage\n html={\n avatarHtml({ src: picture, size: 112, worn }) + nameHtml(displayName, worn, { large: true })\n }\n editing={editing}\n overlayId={equippedOverlay}\n live={live}\n onPlace={place}\n dragging={dragging}\n style={colour}\n banner={\n tryOn && (\n <div style={S.tryOn}>\n <Icon name=\"lock\" size={12} />\n <span style={{ flex: 1, minWidth: 0 }}>\n Trying on {COSMETICS_BY_ID.get(tryOn.id)?.label} ·{' '}\n {COSMETICS_BY_ID.get(tryOn.id)?.earn}\n </span>\n <button type=\"button\" style={S.linkBtn} onClick={() => setTryOn(null)}>\n Take off\n </button>\n </div>\n )\n }\n />\n )\n\n const chat = (\n <div style={S.chat}>\n <div style={S.chatHead}>\n <span>In chat</span>\n <button\n type=\"button\"\n className=\"settings-popout__btn\"\n onClick={() => setReplay(n => n + 1)}\n >\n Replay\n </button>\n </div>\n <div style={S.msg}>\n <Html html={avatarHtml({ src: picture, size: 36, worn })} style={colour} />\n <div style={{ minWidth: 0, ...colour }}>\n <div style={S.who}>\n <Html html={nameHtml(displayName, worn)} />\n </div>\n <div style={S.line}>\n <Html key={replay} html={messageHtml('gg, that last hand was brutal', worn)} />\n </div>\n </div>\n </div>\n </div>\n )\n\n const placement = editing && art && (\n <div style={S.place} role=\"group\" aria-label=\"Placement\">\n <div style={S.placeHead}>\n <strong>Placement</strong>\n <span style={S.hint}>\n Drag it on the avatar. The top handle turns it, the corner resizes it.\n </span>\n </div>\n {SLIDERS.map(([k, label, unit]) => (\n <label key={k} style={S.slider}>\n <span>{label}</span>\n <input\n type=\"range\"\n min={PLACEMENT_LIMITS[k][0]}\n max={PLACEMENT_LIMITS[k][1]}\n value={live[k]}\n onChange={e => place({ ...live, [k]: Number(e.currentTarget.value) }, 'later')}\n style={{ width: '100%', accentColor: 'var(--color-primary)' }}\n />\n <output style={S.out}>\n {live[k]}\n {unit}\n </output>\n </label>\n ))}\n <div style={S.toggles}>\n <label>\n <input\n type=\"checkbox\"\n checked={live.flip}\n onChange={e => place({ ...live, flip: e.currentTarget.checked }, 'now')}\n />{' '}\n Mirror\n </label>\n <label>\n <input\n type=\"checkbox\"\n checked={live.back}\n onChange={e => place({ ...live, back: e.currentTarget.checked }, 'now')}\n />{' '}\n Behind\n </label>\n <button\n type=\"button\"\n className=\"settings-popout__btn\"\n onClick={() => place(PLACEMENT_DEFAULT, 'now')}\n >\n Reset\n </button>\n </div>\n </div>\n )\n\n const tabs: Tab[] = ['picture', ...COSMETIC_SLOTS]\n const tabLabel = (t: Tab) => (t === 'picture' ? 'Picture' : SLOT_LABEL[t])\n\n return (\n <div\n ref={root}\n style={{ ...S.body, gridTemplateColumns: narrow ? 'minmax(0, 1fr)' : '240px minmax(0, 1fr)' }}\n >\n <div style={S.side}>\n {stage}\n {chat}\n {placement}\n </div>\n <div style={{ minWidth: 0 }}>\n <div ref={tabStrip} style={S.tabs} role=\"tablist\" aria-label=\"Avatar\">\n {tabs.map(t => {\n const items = t === 'picture' ? [] : COSMETICS.filter(i => i.slot === 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 e.currentTarget.scrollIntoView({ block: 'nearest', inline: 'nearest' })\n }}\n >\n {tabLabel(t)}\n {t !== 'picture' && (\n <span style={S.count}>\n {items.filter(i => owned.has(i.id)).length}/{items.length}\n </span>\n )}\n </button>\n )\n })}\n </div>\n <div style={S.pane} role=\"tabpanel\" aria-label={tabLabel(tab)}>\n {tab === 'picture' ? (\n <AvatarPicker settings={settings} sets={sets} busy={busy} run={run} />\n ) : (\n <SlotGrid\n slot={tab}\n owned={owned}\n equipped={loadout?.equipped[tab] ?? null}\n tryOn={tryOn}\n showLocked={showLocked}\n onShowLocked={setShowLocked}\n preview={id => {\n const only: AvatarCosmetics = { [tab]: id ?? undefined }\n if (tab === 'overlay') return avatarHtml({ src: picture, size: 40, worn: only })\n if (tab === 'message') return messageHtml('gg, rematch?', only, { keep: true })\n // A name tile shows the item on top of what is worn in the other name slots.\n return nameHtml(displayName, { ...worn, [tab]: id ?? undefined })\n }}\n colour={colour}\n onEquip={id => equip(tab, id)}\n onTry={id =>\n setTryOn(t => (t?.slot === tab && t.id === id ? null : { slot: tab, id }))\n }\n />\n )}\n {error && (\n <span className=\"settings-popout__error\" role=\"alert\">\n {error}\n </span>\n )}\n </div>\n </div>\n </div>\n )\n}\n\n// ---------------------------------------------------------------------------\n\ninterface StageProps {\n html: string\n editing: boolean\n overlayId: string | null\n live: Placement\n onPlace: (p: Partial<Placement>, save: 'later' | 'now' | 'no') => void\n dragging: React.MutableRefObject<boolean>\n style: React.CSSProperties\n banner: React.ReactNode\n}\n\n/** The big preview, and the placement handles drawn on its overlay while editing. */\nfunction Stage({ html, editing, overlayId, live, onPlace, dragging, style, banner }: StageProps) {\n const box = useRef<HTMLDivElement>(null)\n const drag = useRef<{\n mode: string\n p0: Placement\n x0: number\n y0: number\n cx: number\n cy: number\n a0: number\n d0: number\n size: number\n last: Placement\n } | null>(null)\n\n // The handles live inside the overlay's placement box, so they turn and\n // scale with it. Re-added whenever the preview is redrawn.\n useEffect(() => {\n const adj = box.current?.querySelector('.hc-art .hc-adj')\n if (!editing || !adj) return\n const gz = document.createElement('span')\n gz.dataset.g = 'move'\n gz.setAttribute('aria-hidden', 'true')\n gz.style.cssText =\n 'position:absolute;inset:-3px;border:1.5px dashed var(--color-primary);border-radius:6px;pointer-events:auto;cursor:move'\n const handle = (g: string, css: string) => {\n const h = document.createElement('i')\n h.dataset.g = g\n h.style.cssText = `position:absolute;pointer-events:auto;${css}`\n gz.appendChild(h)\n }\n handle(\n 'rot',\n 'left:50%;top:-20px;width:12px;height:12px;margin-left:-6px;border-radius:50%;background:var(--color-primary);cursor:grab'\n )\n handle(\n 'scale',\n 'right:-7px;bottom:-7px;width:12px;height:12px;border-radius:3px;background:var(--color-bg-card);border:2px solid var(--color-primary);cursor:nwse-resize'\n )\n adj.appendChild(gz)\n return () => gz.remove()\n }, [html, editing])\n\n const down = (e: React.PointerEvent) => {\n const g = (e.target as HTMLElement).closest<HTMLElement>('[data-g]')\n const adj = g?.closest('.hc-adj')\n const avatar = box.current?.querySelector('.hc-av')\n if (!g || !adj || !avatar || !editing || !overlayId) return\n e.preventDefault()\n g.setPointerCapture(e.pointerId)\n const r = adj.getBoundingClientRect()\n const cx = r.left + r.width / 2\n const cy = r.top + r.height / 2\n const size = avatar.getBoundingClientRect().width\n dragging.current = true\n drag.current = {\n mode: g.dataset.g ?? 'move',\n p0: live,\n x0: e.clientX,\n y0: e.clientY,\n cx,\n cy,\n a0: Math.atan2(e.clientY - cy, e.clientX - cx),\n d0: Math.hypot(e.clientX - cx, e.clientY - cy) || 1,\n size,\n last: live\n }\n }\n const move = (e: React.PointerEvent) => {\n const d = drag.current\n if (!d) return\n let next: Partial<Placement> = d.p0\n if (d.mode === 'move') {\n next = {\n ...d.p0,\n x: d.p0.x + ((e.clientX - d.x0) / d.size) * 100,\n y: d.p0.y + ((e.clientY - d.y0) / d.size) * 100\n }\n } else if (d.mode === 'rot') {\n const a = Math.atan2(e.clientY - d.cy, e.clientX - d.cx)\n next = { ...d.p0, r: d.p0.r + ((a - d.a0) * 180) / Math.PI }\n } else {\n next = { ...d.p0, s: (d.p0.s * Math.hypot(e.clientX - d.cx, e.clientY - d.cy)) / d.d0 }\n }\n d.last = next as Placement\n onPlace(next, 'no')\n }\n const up = () => {\n const d = drag.current\n if (!d) return\n drag.current = null\n dragging.current = false\n onPlace(d.last, 'now')\n }\n\n return (\n <div\n ref={box}\n role=\"group\"\n aria-label=\"Preview\"\n style={{ ...S.stage, ...style }}\n onPointerDown={down}\n onPointerMove={move}\n onPointerUp={up}\n onPointerCancel={up}\n >\n {banner}\n <Html html={html} style={{ display: 'contents' }} />\n </div>\n )\n}\n\n// ---------------------------------------------------------------------------\n\ninterface SlotGridProps {\n slot: CosmeticSlot\n owned: ReadonlySet<string>\n equipped: string | null\n tryOn: { slot: CosmeticSlot; id: string } | null\n showLocked: boolean\n onShowLocked: (on: boolean) => void\n preview: (id: string | null) => string\n colour: React.CSSProperties\n onEquip: (id: string | null) => void\n onTry: (id: string) => void\n}\n\n/** One slot's items: None, then everything, earned or not. */\nfunction SlotGrid({\n slot,\n owned,\n equipped,\n tryOn,\n showLocked,\n onShowLocked,\n preview,\n colour,\n onEquip,\n onTry\n}: SlotGridProps) {\n const items = COSMETICS.filter(i => i.slot === slot)\n const locked = items.filter(i => !owned.has(i.id))\n const shown = showLocked ? items : items.filter(i => owned.has(i.id))\n\n return (\n <>\n <div style={S.grid}>\n <button\n type=\"button\"\n aria-pressed={equipped === null}\n style={{ ...S.tile, ...(equipped === null ? S.tileOn : {}) }}\n onClick={() => onEquip(null)}\n >\n <Html html={preview(null)} style={{ ...S.tilePv, ...colour }} />\n <span style={S.tileName}>None</span>\n </button>\n {shown.map(item => {\n 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 <div style={S.foot}>\n <span>\n {locked.length\n ? `${locked.length} still to earn. Pick one to try it on.`\n : 'You have earned all of these.'}\n </span>\n {locked.length > 0 && (\n <label>\n <input\n type=\"checkbox\"\n checked={showLocked}\n onChange={e => onShowLocked(e.currentTarget.checked)}\n />{' '}\n Show locked\n </label>\n )}\n </div>\n </>\n )\n}\n"],"names":["Html","live"],"mappings":"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"}
@@ -1,7 +1,7 @@
1
1
  /**
2
- * AvatarDialog — the avatar picker in its own dialog, opened from the settings
3
- * popout's "Change". The popout is 300px wide; the picker (and the cosmetics
4
- * that will join it) needs the room.
2
+ * AvatarDialog — the avatar builder (picture, character and cosmetics, with a
3
+ * live preview) in its own dialog, opened from the settings popout's "Change".
4
+ * The popout is 300px wide; the builder needs the room.
5
5
  *
6
6
  * Rendered into document.body, so no ancestor of the popout (a transform, an
7
7
  * overflow, a stacking context) can clip or trap it. The popout stays open
@@ -17,6 +17,8 @@ export interface AvatarDialogProps {
17
17
  onClose: () => void;
18
18
  /** Where focus goes when the dialog closes (the Change button). */
19
19
  returnFocus?: React.RefObject<HTMLElement | null>;
20
+ /** The person's display name, shown in the preview. */
21
+ name?: string | null;
20
22
  }
21
- export declare function AvatarDialog({ open, onClose, returnFocus }: AvatarDialogProps): React.ReactPortal | null;
23
+ export declare function AvatarDialog({ open, onClose, returnFocus, name }: AvatarDialogProps): React.ReactPortal | null;
22
24
  //# sourceMappingURL=AvatarDialog.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"AvatarDialog.d.ts","sourceRoot":"","sources":["../../src/components/AvatarDialog.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,KAAmC,MAAM,OAAO,CAAA;AAuCvD,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,OAAO,CAAA;IACb,OAAO,EAAE,MAAM,IAAI,CAAA;IACnB,mEAAmE;IACnE,WAAW,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC,WAAW,GAAG,IAAI,CAAC,CAAA;CAClD;AAED,wBAAgB,YAAY,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,WAAW,EAAE,EAAE,iBAAiB,4BAwD7E"}
1
+ {"version":3,"file":"AvatarDialog.d.ts","sourceRoot":"","sources":["../../src/components/AvatarDialog.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,KAAmC,MAAM,OAAO,CAAA;AAuCvD,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,OAAO,CAAA;IACb,OAAO,EAAE,MAAM,IAAI,CAAA;IACnB,mEAAmE;IACnE,WAAW,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC,WAAW,GAAG,IAAI,CAAC,CAAA;IACjD,uDAAuD;IACvD,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CACrB;AAED,wBAAgB,YAAY,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,WAAW,EAAE,IAAW,EAAE,EAAE,iBAAiB,4BAwD1F"}
@@ -1,7 +1,7 @@
1
1
  import { jsx, jsxs } from "react/jsx-runtime";
2
2
  import { useId, useRef, useEffect } from "react";
3
3
  import { createPortal } from "react-dom";
4
- import { AvatarPicker } from "./AvatarPicker.js";
4
+ import { AvatarBuilder } from "./AvatarBuilder.js";
5
5
  const S = {
6
6
  overlay: {
7
7
  position: "fixed",
@@ -36,7 +36,7 @@ const S = {
36
36
  head: { display: "flex", alignItems: "center", gap: 10 },
37
37
  title: { margin: 0, flex: 1, fontSize: "1.05rem", fontWeight: 600 }
38
38
  };
39
- function AvatarDialog({ open, onClose, returnFocus }) {
39
+ function AvatarDialog({ open, onClose, returnFocus, name = null }) {
40
40
  const titleId = useId();
41
41
  const cardRef = useRef(null);
42
42
  const onCloseRef = useRef(onClose);
@@ -82,7 +82,7 @@ function AvatarDialog({ open, onClose, returnFocus }) {
82
82
  }
83
83
  )
84
84
  ] }),
85
- /* @__PURE__ */ jsx(AvatarPicker, {})
85
+ /* @__PURE__ */ jsx(AvatarBuilder, { name })
86
86
  ]
87
87
  }
88
88
  ) }),
@@ -1 +1 @@
1
- {"version":3,"file":"AvatarDialog.js","sources":["../../src/components/AvatarDialog.tsx"],"sourcesContent":["/**\n * AvatarDialog — the avatar picker in its own dialog, opened from the settings\n * popout's \"Change\". The popout is 300px wide; the picker (and the cosmetics\n * that will join it) needs the room.\n *\n * Rendered into document.body, so no ancestor of the popout (a transform, an\n * overflow, a stacking context) can clip or trap it. The popout stays open\n * underneath: closing the dialog returns to it, and focus goes back to Change.\n *\n * Styles are inline for the reason AvatarPicker's are: apps compile this\n * package's CSS at their own build, so a new stylesheet rule reaches an app only\n * when it rebuilds. Tokens only.\n */\nimport React, { useEffect, useId, useRef } from 'react'\nimport { createPortal } from 'react-dom'\nimport { AvatarPicker } from './AvatarPicker'\n\nconst S: Record<string, React.CSSProperties> = {\n overlay: {\n position: 'fixed',\n inset: 0,\n // Above the popout's panel (100) and its click-catcher (99).\n zIndex: 1000,\n display: 'flex',\n alignItems: 'flex-start',\n justifyContent: 'center',\n boxSizing: 'border-box',\n padding: '6vh 16px',\n background: 'var(--color-bg-overlay)'\n },\n card: {\n boxSizing: 'border-box',\n width: 'min(720px, 100%)',\n maxHeight: '88vh',\n overflowY: 'auto',\n display: 'flex',\n flexDirection: 'column',\n gap: 14,\n padding: 20,\n borderRadius: 12,\n background: 'var(--color-bg-card)',\n border: '1px solid var(--color-border)',\n boxShadow: '0 8px 28px rgba(0, 0, 0, 0.18)',\n color: 'var(--color-text)',\n // Outside the app's root, so nothing passes the app's font down to it.\n fontFamily: 'var(--font-family)',\n outline: 'none'\n },\n head: { display: 'flex', alignItems: 'center', gap: 10 },\n title: { margin: 0, flex: 1, fontSize: '1.05rem', fontWeight: 600 }\n}\n\nexport interface AvatarDialogProps {\n open: boolean\n onClose: () => void\n /** Where focus goes when the dialog closes (the Change button). */\n returnFocus?: React.RefObject<HTMLElement | null>\n}\n\nexport function AvatarDialog({ open, onClose, returnFocus }: AvatarDialogProps) {\n const titleId = useId()\n const cardRef = useRef<HTMLDivElement>(null)\n const onCloseRef = useRef(onClose)\n onCloseRef.current = onClose\n\n useEffect(() => {\n if (!open) return\n cardRef.current?.focus()\n const onKey = (e: KeyboardEvent) => {\n if (e.key !== 'Escape') return\n e.preventDefault()\n // Stop it here: closing re-arms the popout's own Escape listener on window\n // (React flushes a keypress's effects at once), and this same keypress\n // would reach it next and close the popout too.\n e.stopPropagation()\n onCloseRef.current()\n }\n document.addEventListener('keydown', onKey)\n const back = returnFocus?.current\n return () => {\n document.removeEventListener('keydown', onKey)\n back?.focus()\n }\n }, [open, returnFocus])\n\n if (!open || typeof document === 'undefined') return null\n\n return createPortal(\n <div style={S.overlay} onClick={onClose}>\n <div\n ref={cardRef}\n role=\"dialog\"\n aria-modal=\"true\"\n aria-labelledby={titleId}\n tabIndex={-1}\n style={S.card}\n onClick={e => e.stopPropagation()}\n >\n <div style={S.head}>\n <h2 id={titleId} style={S.title}>\n Avatar\n </h2>\n <button\n type=\"button\"\n className=\"settings-popout__btn settings-popout__btn--primary\"\n onClick={onClose}\n >\n Done\n </button>\n </div>\n <AvatarPicker />\n </div>\n </div>,\n document.body\n )\n}\n"],"names":[],"mappings":";;;;AAiBA,MAAM,IAAyC;AAAA,EAC7C,SAAS;AAAA,IACP,UAAU;AAAA,IACV,OAAO;AAAA;AAAA,IAEP,QAAQ;AAAA,IACR,SAAS;AAAA,IACT,YAAY;AAAA,IACZ,gBAAgB;AAAA,IAChB,WAAW;AAAA,IACX,SAAS;AAAA,IACT,YAAY;AAAA,EAAA;AAAA,EAEd,MAAM;AAAA,IACJ,WAAW;AAAA,IACX,OAAO;AAAA,IACP,WAAW;AAAA,IACX,WAAW;AAAA,IACX,SAAS;AAAA,IACT,eAAe;AAAA,IACf,KAAK;AAAA,IACL,SAAS;AAAA,IACT,cAAc;AAAA,IACd,YAAY;AAAA,IACZ,QAAQ;AAAA,IACR,WAAW;AAAA,IACX,OAAO;AAAA;AAAA,IAEP,YAAY;AAAA,IACZ,SAAS;AAAA,EAAA;AAAA,EAEX,MAAM,EAAE,SAAS,QAAQ,YAAY,UAAU,KAAK,GAAA;AAAA,EACpD,OAAO,EAAE,QAAQ,GAAG,MAAM,GAAG,UAAU,WAAW,YAAY,IAAA;AAChE;AASO,SAAS,aAAa,EAAE,MAAM,SAAS,eAAkC;AAC9E,QAAM,UAAU,MAAA;AAChB,QAAM,UAAU,OAAuB,IAAI;AAC3C,QAAM,aAAa,OAAO,OAAO;AACjC,aAAW,UAAU;AAErB,YAAU,MAAM;AACd,QAAI,CAAC,KAAM;AACX,YAAQ,SAAS,MAAA;AACjB,UAAM,QAAQ,CAAC,MAAqB;AAClC,UAAI,EAAE,QAAQ,SAAU;AACxB,QAAE,eAAA;AAIF,QAAE,gBAAA;AACF,iBAAW,QAAA;AAAA,IACb;AACA,aAAS,iBAAiB,WAAW,KAAK;AAC1C,UAAM,OAAO,aAAa;AAC1B,WAAO,MAAM;AACX,eAAS,oBAAoB,WAAW,KAAK;AAC7C,YAAM,MAAA;AAAA,IACR;AAAA,EACF,GAAG,CAAC,MAAM,WAAW,CAAC;AAEtB,MAAI,CAAC,QAAQ,OAAO,aAAa,YAAa,QAAO;AAErD,SAAO;AAAA,wBACJ,OAAA,EAAI,OAAO,EAAE,SAAS,SAAS,SAC9B,UAAA;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,KAAK;AAAA,QACL,MAAK;AAAA,QACL,cAAW;AAAA,QACX,mBAAiB;AAAA,QACjB,UAAU;AAAA,QACV,OAAO,EAAE;AAAA,QACT,SAAS,CAAA,MAAK,EAAE,gBAAA;AAAA,QAEhB,UAAA;AAAA,UAAA,qBAAC,OAAA,EAAI,OAAO,EAAE,MACZ,UAAA;AAAA,YAAA,oBAAC,QAAG,IAAI,SAAS,OAAO,EAAE,OAAO,UAAA,UAEjC;AAAA,YACA;AAAA,cAAC;AAAA,cAAA;AAAA,gBACC,MAAK;AAAA,gBACL,WAAU;AAAA,gBACV,SAAS;AAAA,gBACV,UAAA;AAAA,cAAA;AAAA,YAAA;AAAA,UAED,GACF;AAAA,8BACC,cAAA,CAAA,CAAa;AAAA,QAAA;AAAA,MAAA;AAAA,IAAA,GAElB;AAAA,IACA,SAAS;AAAA,EAAA;AAEb;"}
1
+ {"version":3,"file":"AvatarDialog.js","sources":["../../src/components/AvatarDialog.tsx"],"sourcesContent":["/**\n * AvatarDialog — the avatar builder (picture, character and cosmetics, with a\n * live preview) in its own dialog, opened from the settings popout's \"Change\".\n * The popout is 300px wide; the builder needs the room.\n *\n * Rendered into document.body, so no ancestor of the popout (a transform, an\n * overflow, a stacking context) can clip or trap it. The popout stays open\n * underneath: closing the dialog returns to it, and focus goes back to Change.\n *\n * Styles are inline for the reason AvatarPicker's are: apps compile this\n * package's CSS at their own build, so a new stylesheet rule reaches an app only\n * when it rebuilds. Tokens only.\n */\nimport React, { useEffect, useId, useRef } from 'react'\nimport { createPortal } from 'react-dom'\nimport { AvatarBuilder } from './AvatarBuilder'\n\nconst S: Record<string, React.CSSProperties> = {\n overlay: {\n position: 'fixed',\n inset: 0,\n // Above the popout's panel (100) and its click-catcher (99).\n zIndex: 1000,\n display: 'flex',\n alignItems: 'flex-start',\n justifyContent: 'center',\n boxSizing: 'border-box',\n padding: '6vh 16px',\n background: 'var(--color-bg-overlay)'\n },\n card: {\n boxSizing: 'border-box',\n width: 'min(720px, 100%)',\n maxHeight: '88vh',\n overflowY: 'auto',\n display: 'flex',\n flexDirection: 'column',\n gap: 14,\n padding: 20,\n borderRadius: 12,\n background: 'var(--color-bg-card)',\n border: '1px solid var(--color-border)',\n boxShadow: '0 8px 28px rgba(0, 0, 0, 0.18)',\n color: 'var(--color-text)',\n // Outside the app's root, so nothing passes the app's font down to it.\n fontFamily: 'var(--font-family)',\n outline: 'none'\n },\n head: { display: 'flex', alignItems: 'center', gap: 10 },\n title: { margin: 0, flex: 1, fontSize: '1.05rem', fontWeight: 600 }\n}\n\nexport interface AvatarDialogProps {\n open: boolean\n onClose: () => void\n /** Where focus goes when the dialog closes (the Change button). */\n returnFocus?: React.RefObject<HTMLElement | null>\n /** The person's display name, shown in the preview. */\n name?: string | null\n}\n\nexport function AvatarDialog({ open, onClose, returnFocus, name = null }: AvatarDialogProps) {\n const titleId = useId()\n const cardRef = useRef<HTMLDivElement>(null)\n const onCloseRef = useRef(onClose)\n onCloseRef.current = onClose\n\n useEffect(() => {\n if (!open) return\n cardRef.current?.focus()\n const onKey = (e: KeyboardEvent) => {\n if (e.key !== 'Escape') return\n e.preventDefault()\n // Stop it here: closing re-arms the popout's own Escape listener on window\n // (React flushes a keypress's effects at once), and this same keypress\n // would reach it next and close the popout too.\n e.stopPropagation()\n onCloseRef.current()\n }\n document.addEventListener('keydown', onKey)\n const back = returnFocus?.current\n return () => {\n document.removeEventListener('keydown', onKey)\n back?.focus()\n }\n }, [open, returnFocus])\n\n if (!open || typeof document === 'undefined') return null\n\n return createPortal(\n <div style={S.overlay} onClick={onClose}>\n <div\n ref={cardRef}\n role=\"dialog\"\n aria-modal=\"true\"\n aria-labelledby={titleId}\n tabIndex={-1}\n style={S.card}\n onClick={e => e.stopPropagation()}\n >\n <div style={S.head}>\n <h2 id={titleId} style={S.title}>\n Avatar\n </h2>\n <button\n type=\"button\"\n className=\"settings-popout__btn settings-popout__btn--primary\"\n onClick={onClose}\n >\n Done\n </button>\n </div>\n <AvatarBuilder name={name} />\n </div>\n </div>,\n document.body\n )\n}\n"],"names":[],"mappings":";;;;AAiBA,MAAM,IAAyC;AAAA,EAC7C,SAAS;AAAA,IACP,UAAU;AAAA,IACV,OAAO;AAAA;AAAA,IAEP,QAAQ;AAAA,IACR,SAAS;AAAA,IACT,YAAY;AAAA,IACZ,gBAAgB;AAAA,IAChB,WAAW;AAAA,IACX,SAAS;AAAA,IACT,YAAY;AAAA,EAAA;AAAA,EAEd,MAAM;AAAA,IACJ,WAAW;AAAA,IACX,OAAO;AAAA,IACP,WAAW;AAAA,IACX,WAAW;AAAA,IACX,SAAS;AAAA,IACT,eAAe;AAAA,IACf,KAAK;AAAA,IACL,SAAS;AAAA,IACT,cAAc;AAAA,IACd,YAAY;AAAA,IACZ,QAAQ;AAAA,IACR,WAAW;AAAA,IACX,OAAO;AAAA;AAAA,IAEP,YAAY;AAAA,IACZ,SAAS;AAAA,EAAA;AAAA,EAEX,MAAM,EAAE,SAAS,QAAQ,YAAY,UAAU,KAAK,GAAA;AAAA,EACpD,OAAO,EAAE,QAAQ,GAAG,MAAM,GAAG,UAAU,WAAW,YAAY,IAAA;AAChE;AAWO,SAAS,aAAa,EAAE,MAAM,SAAS,aAAa,OAAO,QAA2B;AAC3F,QAAM,UAAU,MAAA;AAChB,QAAM,UAAU,OAAuB,IAAI;AAC3C,QAAM,aAAa,OAAO,OAAO;AACjC,aAAW,UAAU;AAErB,YAAU,MAAM;AACd,QAAI,CAAC,KAAM;AACX,YAAQ,SAAS,MAAA;AACjB,UAAM,QAAQ,CAAC,MAAqB;AAClC,UAAI,EAAE,QAAQ,SAAU;AACxB,QAAE,eAAA;AAIF,QAAE,gBAAA;AACF,iBAAW,QAAA;AAAA,IACb;AACA,aAAS,iBAAiB,WAAW,KAAK;AAC1C,UAAM,OAAO,aAAa;AAC1B,WAAO,MAAM;AACX,eAAS,oBAAoB,WAAW,KAAK;AAC7C,YAAM,MAAA;AAAA,IACR;AAAA,EACF,GAAG,CAAC,MAAM,WAAW,CAAC;AAEtB,MAAI,CAAC,QAAQ,OAAO,aAAa,YAAa,QAAO;AAErD,SAAO;AAAA,wBACJ,OAAA,EAAI,OAAO,EAAE,SAAS,SAAS,SAC9B,UAAA;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,KAAK;AAAA,QACL,MAAK;AAAA,QACL,cAAW;AAAA,QACX,mBAAiB;AAAA,QACjB,UAAU;AAAA,QACV,OAAO,EAAE;AAAA,QACT,SAAS,CAAA,MAAK,EAAE,gBAAA;AAAA,QAEhB,UAAA;AAAA,UAAA,qBAAC,OAAA,EAAI,OAAO,EAAE,MACZ,UAAA;AAAA,YAAA,oBAAC,QAAG,IAAI,SAAS,OAAO,EAAE,OAAO,UAAA,UAEjC;AAAA,YACA;AAAA,cAAC;AAAA,cAAA;AAAA,gBACC,MAAK;AAAA,gBACL,WAAU;AAAA,gBACV,SAAS;AAAA,gBACV,UAAA;AAAA,cAAA;AAAA,YAAA;AAAA,UAED,GACF;AAAA,UACA,oBAAC,iBAAc,KAAA,CAAY;AAAA,QAAA;AAAA,MAAA;AAAA,IAAA,GAE/B;AAAA,IACA,SAAS;AAAA,EAAA;AAEb;"}
@@ -1,29 +1,32 @@
1
1
  /**
2
- * AvatarPicker — the body of AvatarDialog (opened from ConnectedSettings): the picture people
3
- * see (character, Discord, Steam or an upload) and the character games use as
4
- * the piece. One resolved avatar per person, stored in hadoku_site's prefs-api
5
- * and rendered by every app (`@wolffm/prefs-client/avatar`, bundled into this
6
- * package at build so the link protocol cannot drift from the callback page).
2
+ * AvatarPicker — the Picture tab of AvatarBuilder (inside AvatarDialog, opened
3
+ * from ConnectedSettings): the picture people see (character, Discord, Steam or
4
+ * an upload) and the character games use as the piece. One resolved avatar per
5
+ * person, stored in hadoku_site's prefs-api and rendered by every app
6
+ * (`@wolffm/prefs-client/avatar`, bundled into this package at build so the link
7
+ * protocol cannot drift from the callback page).
7
8
  *
8
9
  * Everything happens in place: picking, uploading and removing are requests
9
- * from here, and linking Discord or Steam signs in through a popup
10
- * (`linkInPopup`), so the page under the popout never navigates and a game
11
- * keeps running.
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.
12
12
  *
13
- * Loaded only when the person opens the dialog ("Change"), never at mount: the avatar
14
- * read can refresh a linked picture from Discord or Steam, which no page load
15
- * should cost, and opening the gear itself must stay free (settings-prefetch
16
- * e2e).
17
- *
18
- * COMPLIANCE (Steam Web API terms; Discord developer terms), carried over from
19
- * hadoku.me/account: nothing is fetched from a provider until the person reads
20
- * what is read and kept and presses Continue; Steam attribution and a privacy
21
- * link sit under the controls; no provider logos, and no implied endorsement.
13
+ * AvatarBuilder owns the settings and the character table and loads them only
14
+ * when the dialog opens; this component draws them and sends changes through
15
+ * `run`, which applies the answer everywhere in the dialog.
22
16
  *
23
17
  * Styles are inline for the reason PASSKEY_DOT's are (ConnectedSettings.tsx):
24
18
  * apps compile app-header.css at their own build, so a new rule there reaches
25
19
  * an app only when it rebuilds.
26
20
  */
27
21
  import React from 'react';
28
- export declare function AvatarPicker(): React.JSX.Element;
22
+ import { type AvatarSettings } from '@wolffm/prefs-client/avatar';
23
+ import { type CharacterSet } from '../lib/avatarData';
24
+ export interface AvatarPickerProps {
25
+ settings: AvatarSettings;
26
+ sets: CharacterSet[] | null;
27
+ busy: boolean;
28
+ /** Send a change; the builder applies the answer and reports failures. */
29
+ run: (work: () => Promise<AvatarSettings>) => Promise<void>;
30
+ }
31
+ export declare function AvatarPicker({ settings, sets, busy, run }: AvatarPickerProps): React.JSX.Element;
29
32
  //# sourceMappingURL=AvatarPicker.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"AvatarPicker.d.ts","sourceRoot":"","sources":["../../src/components/AvatarPicker.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,OAAO,KAA2C,MAAM,OAAO,CAAA;AAoI/D,wBAAgB,YAAY,sBA0Q3B"}
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,qBAkJ5E"}
@@ -1,48 +1,14 @@
1
- import { jsx, jsxs, Fragment } from "react/jsx-runtime";
2
- import { useState, useEffect, useCallback } from "react";
3
- import { createAvatarClient, AvatarError } from "../vendor/wolffm/prefs-client/avatar.js";
4
- import { AVATAR_EVENT } from "../lib/avatarEvent.js";
5
- const client = createAvatarClient({ baseUrl: "/prefs" });
6
- const CHARACTERS_URL = "/watchparty/api/characters";
7
- const WATCHPARTY_BASE = "/watchparty";
1
+ import { jsxs, jsx, Fragment } from "react/jsx-runtime";
2
+ import { useState, useEffect } from "react";
3
+ import { avatarClient, portraitUrl } from "../lib/avatarData.js";
8
4
  const LABEL = {
9
5
  sprite: "Character",
10
6
  discord: "Discord",
11
7
  steam: "Steam",
12
8
  upload: "Upload"
13
9
  };
14
- const CONSENT = {
15
- discord: {
16
- points: [
17
- 'Discord asks you to share your basic profile ("identify").',
18
- "Kept: your Discord user ID and the address of your profile picture, refreshed while linked.",
19
- "Not kept: your username, email, servers or any token.",
20
- "Unlink deletes both."
21
- ],
22
- go: "Continue to Discord"
23
- },
24
- steam: {
25
- points: [
26
- "You sign in on steamcommunity.com; your password never reaches hadoku.me.",
27
- "Your public Steam profile picture is then read through the Steam Web API, now and periodically while linked.",
28
- "Kept: your Steam ID and the address of that picture.",
29
- "Unlink deletes both and stops the reads."
30
- ],
31
- go: "Continue to Steam"
32
- }
33
- };
34
10
  const S = {
35
11
  stack: { display: "flex", flexDirection: "column", gap: 10, width: "100%" },
36
- current: { display: "flex", alignItems: "center", gap: 10 },
37
- preview: {
38
- width: 64,
39
- height: 64,
40
- borderRadius: "50%",
41
- overflow: "hidden",
42
- flex: "none",
43
- background: "var(--color-bg-alt)",
44
- border: "1px solid var(--color-border)"
45
- },
46
12
  img: { width: "100%", height: "100%", objectFit: "cover", display: "block" },
47
13
  sources: { display: "grid", gridTemplateColumns: "repeat(4, 1fr)", gap: 6 },
48
14
  source: {
@@ -81,12 +47,8 @@ const S = {
81
47
  cursor: "pointer"
82
48
  },
83
49
  line: { display: "flex", alignItems: "center", gap: 6, flexWrap: "wrap" },
84
- grow: { flex: 1, minWidth: 0 },
85
- list: { margin: 0, paddingLeft: 18, fontSize: "0.75rem", lineHeight: 1.45 }
50
+ grow: { flex: 1, minWidth: 0 }
86
51
  };
87
- function portraitUrl(c) {
88
- return c.portrait.startsWith("/") ? `${WATCHPARTY_BASE}${c.portrait}` : c.portrait;
89
- }
90
52
  function Picture({ src, style }) {
91
53
  const [failed, setFailed] = useState(false);
92
54
  useEffect(() => setFailed(false), [src]);
@@ -101,45 +63,17 @@ function Picture({ src, style }) {
101
63
  }
102
64
  ) });
103
65
  }
104
- function AvatarPicker() {
105
- const [settings, setSettings] = useState(null);
106
- const [sets, setSets] = useState(null);
66
+ function AvatarPicker({ settings, sets, busy, run }) {
107
67
  const [setIdx, setSetIdx] = useState(0);
108
- const [busy, setBusy] = useState(false);
109
- const [error, setError] = useState(null);
110
- const [consent, setConsent] = useState(null);
111
68
  useEffect(() => {
112
- let live = true;
113
- client.get().then((s) => live && setSettings(s)).catch((err) => live && setError(message(err)));
114
- fetch(CHARACTERS_URL).then(
115
- (r) => r.ok ? r.json() : Promise.reject(new Error(`characters ${r.status}`))
116
- ).then((d) => live && setSets(d.sets)).catch((err) => live && setError(message(err)));
117
- return () => {
118
- live = false;
119
- };
120
- }, []);
121
- const run = useCallback(async (work) => {
122
- setBusy(true);
123
- setError(null);
124
- try {
125
- const next = await work();
126
- setSettings(next);
127
- window.dispatchEvent(new window.CustomEvent(AVATAR_EVENT, { detail: next }));
128
- } catch (err) {
129
- setError(message(err));
130
- } finally {
131
- setBusy(false);
132
- }
133
- }, []);
69
+ const at = sets?.findIndex((s) => s.characters.some((c) => c.id === settings.sprite)) ?? -1;
70
+ if (at > 0) setSetIdx(at);
71
+ }, [sets]);
134
72
  const characters = new Map((sets ?? []).flatMap((s) => s.characters).map((c) => [c.id, c]));
135
73
  const spriteSrc = (id) => {
136
74
  const c = id ? characters.get(id) : void 0;
137
75
  return c ? portraitUrl(c) : null;
138
76
  };
139
- const srcOf = (a) => a.url ?? spriteSrc(a.sprite);
140
- if (!settings) {
141
- return /* @__PURE__ */ jsx("span", { className: "settings-popout__hint", role: error ? "alert" : void 0, children: error ?? "…" });
142
- }
143
77
  const available = {
144
78
  sprite: true,
145
79
  discord: Boolean(settings.linked.discord?.avatarUrl),
@@ -154,20 +88,10 @@ function AvatarPicker() {
154
88
  };
155
89
  const shown = settings.avatar;
156
90
  const current = sets?.[setIdx];
157
- const link = (provider) => {
158
- setConsent(null);
159
- run(async () => {
160
- const next = await client.linkInPopup(provider);
161
- return next.linked[provider]?.avatarUrl ? client.choose(provider) : next;
162
- }).catch(reportErr);
163
- };
164
91
  return /* @__PURE__ */ jsxs("div", { style: S.stack, children: [
165
- /* @__PURE__ */ jsxs("div", { style: S.current, children: [
166
- /* @__PURE__ */ jsx(Picture, { src: srcOf(shown), style: S.preview }),
167
- /* @__PURE__ */ jsxs("span", { className: "settings-popout__value", children: [
168
- LABEL[shown.source],
169
- settings.choice && settings.choice !== shown.source ? ` (${LABEL[settings.choice]} unavailable)` : ""
170
- ] })
92
+ settings.choice && settings.choice !== shown.source && /* @__PURE__ */ jsxs("span", { className: "settings-popout__hint", children: [
93
+ LABEL[settings.choice],
94
+ " is unavailable, so your character shows instead."
171
95
  ] }),
172
96
  /* @__PURE__ */ jsx("div", { style: S.sources, role: "radiogroup", "aria-label": "Picture", children: Object.keys(LABEL).map((src) => /* @__PURE__ */ jsxs(
173
97
  "button",
@@ -176,14 +100,14 @@ function AvatarPicker() {
176
100
  role: "radio",
177
101
  "aria-checked": shown.source === src,
178
102
  disabled: busy || !available[src],
179
- title: available[src] ? void 0 : "Link or upload one first",
103
+ title: available[src] ? void 0 : src === "upload" ? "Upload a picture below first" : `Link ${LABEL[src]} under Settings › Account first`,
180
104
  style: {
181
105
  ...S.source,
182
106
  ...shown.source === src ? S.sourceOn : {},
183
107
  opacity: available[src] ? 1 : 0.5
184
108
  },
185
109
  onClick: () => {
186
- run(() => client.choose(src)).catch(reportErr);
110
+ run(() => avatarClient.choose(src)).catch(reportErr);
187
111
  },
188
112
  children: [
189
113
  /* @__PURE__ */ jsx(Picture, { src: thumbOf[src], style: S.thumb }),
@@ -217,60 +141,13 @@ function AvatarPicker() {
217
141
  borderColor: settings.sprite === c.id ? c.color : "transparent"
218
142
  },
219
143
  onClick: () => {
220
- run(() => client.update({ sprite: c.id })).catch(reportErr);
144
+ run(() => avatarClient.update({ sprite: c.id })).catch(reportErr);
221
145
  },
222
146
  children: /* @__PURE__ */ jsx("img", { src: portraitUrl(c), alt: "", style: S.img })
223
147
  },
224
148
  c.id
225
149
  )) })
226
150
  ] }) : /* @__PURE__ */ jsx("span", { className: "settings-popout__hint", children: "…" })),
227
- ["discord", "steam"].map((p) => {
228
- const linked = settings.linked[p];
229
- return /* @__PURE__ */ jsxs("div", { style: S.line, children: [
230
- /* @__PURE__ */ jsxs("span", { className: "settings-popout__label", style: S.grow, children: [
231
- LABEL[p],
232
- !settings.providers[p] ? " · not available yet" : linked ? linked.avatarUrl ? " · linked" : " · linked, no picture" : ""
233
- ] }),
234
- settings.providers[p] && !linked && /* @__PURE__ */ jsx(
235
- "button",
236
- {
237
- type: "button",
238
- className: "settings-popout__btn",
239
- disabled: busy,
240
- onClick: () => setConsent(consent === p ? null : p),
241
- children: "Link"
242
- }
243
- ),
244
- linked && /* @__PURE__ */ jsx(
245
- "button",
246
- {
247
- type: "button",
248
- className: "settings-popout__btn",
249
- disabled: busy,
250
- onClick: () => {
251
- run(() => client.unlink(p)).catch(reportErr);
252
- },
253
- children: "Unlink"
254
- }
255
- )
256
- ] }, p);
257
- }),
258
- consent && /* @__PURE__ */ jsxs("div", { style: S.stack, children: [
259
- /* @__PURE__ */ jsx("ul", { style: S.list, children: CONSENT[consent].points.map((t) => /* @__PURE__ */ jsx("li", { children: t }, t)) }),
260
- /* @__PURE__ */ jsxs("span", { style: S.line, children: [
261
- /* @__PURE__ */ jsx(
262
- "button",
263
- {
264
- type: "button",
265
- className: "settings-popout__btn settings-popout__btn--primary",
266
- disabled: busy,
267
- onClick: () => link(consent),
268
- children: CONSENT[consent].go
269
- }
270
- ),
271
- /* @__PURE__ */ jsx("button", { type: "button", className: "settings-popout__btn", onClick: () => setConsent(null), children: "Cancel" })
272
- ] })
273
- ] }),
274
151
  /* @__PURE__ */ jsxs("div", { style: S.line, children: [
275
152
  /* @__PURE__ */ jsx("span", { className: "settings-popout__label", style: S.grow, children: "Upload" }),
276
153
  /* @__PURE__ */ jsxs("label", { className: "settings-popout__btn", style: { opacity: busy ? 0.5 : 1 }, children: [
@@ -285,7 +162,7 @@ function AvatarPicker() {
285
162
  onChange: (e) => {
286
163
  const file = e.currentTarget.files?.[0];
287
164
  e.currentTarget.value = "";
288
- if (file) run(() => client.upload(file)).catch(reportErr);
165
+ if (file) run(() => avatarClient.upload(file)).catch(reportErr);
289
166
  }
290
167
  }
291
168
  )
@@ -297,19 +174,14 @@ function AvatarPicker() {
297
174
  className: "settings-popout__btn",
298
175
  disabled: busy,
299
176
  onClick: () => {
300
- run(() => client.removeUpload()).catch(reportErr);
177
+ run(() => avatarClient.removeUpload()).catch(reportErr);
301
178
  },
302
179
  children: "Remove"
303
180
  }
304
181
  )
305
- ] }),
306
- error && /* @__PURE__ */ jsx("span", { className: "settings-popout__error", role: "alert", children: error })
182
+ ] })
307
183
  ] });
308
184
  }
309
- function message(err) {
310
- if (err instanceof AvatarError && err.status === 401) return "Sign in to set an avatar.";
311
- return err instanceof Error ? err.message : String(err);
312
- }
313
185
  function reportErr(err) {
314
186
  console.error("[AvatarPicker] action failed", err?.message ?? err);
315
187
  }
@@ -1 +1 @@
1
- {"version":3,"file":"AvatarPicker.js","sources":["../../src/components/AvatarPicker.tsx"],"sourcesContent":["/**\n * AvatarPicker — the body of AvatarDialog (opened from ConnectedSettings): the picture people\n * see (character, Discord, Steam or an upload) and the character games use as\n * the piece. One resolved avatar per person, stored in hadoku_site's prefs-api\n * and rendered by every app (`@wolffm/prefs-client/avatar`, bundled into this\n * package at build so the link protocol cannot drift from the callback page).\n *\n * Everything happens in place: picking, uploading and removing are requests\n * from here, and linking Discord or Steam signs in through a popup\n * (`linkInPopup`), so the page under the popout never navigates and a game\n * keeps running.\n *\n * Loaded only when the person opens the dialog (\"Change\"), never at mount: the avatar\n * read can refresh a linked picture from Discord or Steam, which no page load\n * should cost, and opening the gear itself must stay free (settings-prefetch\n * e2e).\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; Steam attribution and a privacy\n * link sit under the controls; no provider logos, and no implied endorsement.\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, { useCallback, useEffect, useState } from 'react'\nimport {\n createAvatarClient,\n AvatarError,\n type AvatarSettings,\n type AvatarSource,\n type LinkProvider,\n type ResolvedAvatar\n} from '@wolffm/prefs-client/avatar'\nimport { AVATAR_EVENT } from '../lib/avatarEvent'\n\nconst client = createAvatarClient({ baseUrl: '/prefs' })\n\n/** watchparty's public character table: GET /watchparty/api/characters. */\nconst CHARACTERS_URL = '/watchparty/api/characters'\nconst WATCHPARTY_BASE = '/watchparty'\n\ninterface Character {\n id: string\n name: string\n color: string\n portrait: string\n}\ninterface CharacterSet {\n id: string\n characters: Character[]\n}\n\nconst LABEL: Record<AvatarSource, string> = {\n sprite: 'Character',\n discord: 'Discord',\n steam: 'Steam',\n upload: 'Upload'\n}\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: 10, width: '100%' },\n current: { display: 'flex', alignItems: 'center', gap: 10 },\n preview: {\n width: 64,\n height: 64,\n borderRadius: '50%',\n overflow: 'hidden',\n flex: 'none',\n background: 'var(--color-bg-alt)',\n border: '1px solid var(--color-border)'\n },\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 list: { margin: 0, paddingLeft: 18, fontSize: '0.75rem', lineHeight: 1.45 }\n} satisfies Record<string, React.CSSProperties>\n\nfunction portraitUrl(c: Character): string {\n return c.portrait.startsWith('/') ? `${WATCHPARTY_BASE}${c.portrait}` : c.portrait\n}\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 function AvatarPicker() {\n const [settings, setSettings] = useState<AvatarSettings | null>(null)\n const [sets, setSets] = useState<CharacterSet[] | null>(null)\n const [setIdx, setSetIdx] = useState(0)\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 client\n .get()\n .then(s => live && setSettings(s))\n .catch((err: unknown) => live && setError(message(err)))\n fetch(CHARACTERS_URL)\n .then(r =>\n r.ok\n ? (r.json() as Promise<{ sets: CharacterSet[] }>)\n : Promise.reject(new Error(`characters ${r.status}`))\n )\n .then(d => live && setSets(d.sets))\n .catch((err: unknown) => live && setError(message(err)))\n return () => {\n live = false\n }\n }, [])\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 // The page under the popout (a watchparty room, a game) re-reads the\n // avatar from this instead of waiting for its next join.\n window.dispatchEvent(new window.CustomEvent<AvatarSettings>(AVATAR_EVENT, { detail: next }))\n } catch (err) {\n setError(message(err))\n } finally {\n setBusy(false)\n }\n }, [])\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 const srcOf = (a: ResolvedAvatar) => a.url ?? spriteSrc(a.sprite)\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 available: Record<AvatarSource, boolean> = {\n sprite: true,\n discord: Boolean(settings.linked.discord?.avatarUrl),\n steam: Boolean(settings.linked.steam?.avatarUrl),\n upload: settings.upload !== null\n }\n const thumbOf: Record<AvatarSource, string | null> = {\n sprite: spriteSrc(settings.sprite),\n discord: settings.linked.discord?.avatarUrl ?? null,\n steam: settings.linked.steam?.avatarUrl ?? null,\n upload: settings.upload?.url ?? null\n }\n const shown = settings.avatar\n const current = sets?.[setIdx]\n\n // 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 run(async () => {\n const next = await client.linkInPopup(provider)\n // Linking is choosing, when the account came back with a picture.\n return next.linked[provider]?.avatarUrl ? client.choose(provider) : next\n }).catch(reportErr)\n }\n\n return (\n <div style={S.stack}>\n <div style={S.current}>\n <Picture src={srcOf(shown)} style={S.preview} />\n <span className=\"settings-popout__value\">\n {LABEL[shown.source]}\n {settings.choice && settings.choice !== shown.source\n ? ` (${LABEL[settings.choice]} unavailable)`\n : ''}\n </span>\n </div>\n\n <div style={S.sources} role=\"radiogroup\" aria-label=\"Picture\">\n {(Object.keys(LABEL) as AvatarSource[]).map(src => (\n <button\n key={src}\n type=\"button\"\n role=\"radio\"\n aria-checked={shown.source === src}\n disabled={busy || !available[src]}\n title={available[src] ? undefined : 'Link or upload one first'}\n style={{\n ...S.source,\n ...(shown.source === src ? S.sourceOn : {}),\n opacity: available[src] ? 1 : 0.5\n }}\n onClick={() => {\n run(() => client.choose(src)).catch(reportErr)\n }}\n >\n <Picture src={thumbOf[src]} style={S.thumb} />\n {LABEL[src]}\n </button>\n ))}\n </div>\n\n {shown.source === 'sprite' &&\n (sets ? (\n <>\n {sets.length > 1 && (\n <div style={S.sets}>\n {sets.map((s, i) => (\n <button\n key={s.id}\n type=\"button\"\n className={`settings-popout__btn${i === setIdx ? ' settings-popout__btn--primary' : ''}`}\n onClick={() => setSetIdx(i)}\n >\n {s.id}\n </button>\n ))}\n </div>\n )}\n <div style={S.grid} role=\"radiogroup\" aria-label=\"Character\">\n {current?.characters.map(c => (\n <button\n key={c.id}\n type=\"button\"\n role=\"radio\"\n aria-checked={settings.sprite === c.id}\n aria-label={c.name}\n title={c.name}\n disabled={busy}\n style={{\n ...S.cell,\n borderColor: settings.sprite === c.id ? c.color : 'transparent'\n }}\n onClick={() => {\n run(() => client.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 {(['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={() => {\n run(() => client.unlink(p)).catch(reportErr)\n }}\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 <div style={S.line}>\n <span className=\"settings-popout__label\" style={S.grow}>\n Upload\n </span>\n <label className=\"settings-popout__btn\" style={{ opacity: busy ? 0.5 : 1 }}>\n {settings.upload ? 'Replace' : 'Choose image'}\n <input\n type=\"file\"\n accept={settings.limits.acceptedTypes.join(',')}\n hidden\n disabled={busy}\n onChange={e => {\n const file = e.currentTarget.files?.[0]\n e.currentTarget.value = ''\n if (file) run(() => client.upload(file)).catch(reportErr)\n }}\n />\n </label>\n {settings.upload && (\n <button\n type=\"button\"\n className=\"settings-popout__btn\"\n disabled={busy}\n onClick={() => {\n run(() => client.removeUpload()).catch(reportErr)\n }}\n >\n Remove\n </button>\n )}\n </div>\n\n {error && (\n <span className=\"settings-popout__error\" role=\"alert\">\n {error}\n </span>\n )}\n </div>\n )\n}\n\nfunction message(err: unknown): string {\n if (err instanceof AvatarError && err.status === 401) return 'Sign in to set an avatar.'\n return err instanceof Error ? err.message : String(err)\n}\n\nfunction reportErr(err: unknown) {\n console.error('[AvatarPicker] action failed', (err as Error)?.message ?? err)\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"AvatarPicker.js","sources":["../../src/components/AvatarPicker.tsx"],"sourcesContent":["/**\n * AvatarPicker — the Picture tab of AvatarBuilder (inside AvatarDialog, opened\n * from ConnectedSettings): the picture people see (character, Discord, Steam or\n * an upload) and the character games use as the piece. One resolved avatar per\n * person, stored in hadoku_site's prefs-api and rendered by every app\n * (`@wolffm/prefs-client/avatar`, bundled into this package at build so the link\n * protocol cannot drift from the callback page).\n *\n * Everything happens in place: picking, uploading and removing are requests\n * from here. Linking Discord or Steam lives in the popout's Account tab\n * (LinkedAccounts); a linked account's picture is a choice here.\n *\n * AvatarBuilder owns the settings and the character table and loads them only\n * when the dialog opens; this component draws them and sends changes through\n * `run`, which applies the answer everywhere in the dialog.\n *\n * Styles are inline for the reason PASSKEY_DOT's are (ConnectedSettings.tsx):\n * apps compile app-header.css at their own build, so a new rule there reaches\n * an app only when it rebuilds.\n */\nimport React, { useEffect, useState } from 'react'\nimport { type AvatarSettings, type AvatarSource } from '@wolffm/prefs-client/avatar'\nimport { avatarClient, portraitUrl, type CharacterSet } from '../lib/avatarData'\n\nconst LABEL: Record<AvatarSource, string> = {\n sprite: 'Character',\n discord: 'Discord',\n steam: 'Steam',\n upload: 'Upload'\n}\n\nconst S = {\n stack: { display: 'flex', flexDirection: 'column', gap: 10, width: '100%' },\n img: { width: '100%', height: '100%', objectFit: 'cover', display: 'block' },\n sources: { display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 6 },\n source: {\n display: 'flex',\n flexDirection: 'column',\n alignItems: 'center',\n gap: 4,\n padding: 6,\n borderRadius: 8,\n border: '1px solid var(--color-border)',\n background: 'var(--color-bg)',\n color: 'var(--color-text)',\n fontSize: '0.72rem',\n cursor: 'pointer'\n },\n sourceOn: {\n borderColor: 'var(--color-primary)',\n boxShadow: 'inset 0 0 0 1px var(--color-primary)'\n },\n thumb: {\n width: 32,\n height: 32,\n borderRadius: '50%',\n overflow: 'hidden',\n background: 'var(--color-bg-alt)'\n },\n sets: { display: 'flex', gap: 4, flexWrap: 'wrap' },\n grid: { display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(64px, 1fr))', gap: 6 },\n cell: {\n padding: 0,\n aspectRatio: '1',\n borderRadius: 6,\n overflow: 'hidden',\n border: '2px solid transparent',\n background: 'var(--color-bg-alt)',\n cursor: 'pointer'\n },\n line: { display: 'flex', alignItems: 'center', gap: 6, flexWrap: 'wrap' },\n grow: { flex: 1, minWidth: 0 }\n} satisfies Record<string, React.CSSProperties>\n\nfunction Picture({ src, style }: { src: string | null; style: React.CSSProperties }) {\n const [failed, setFailed] = useState(false)\n useEffect(() => setFailed(false), [src])\n return (\n <span style={style}>\n {src && !failed && (\n <img\n src={src}\n alt=\"\"\n referrerPolicy=\"no-referrer\"\n style={S.img}\n onError={() => setFailed(true)}\n />\n )}\n </span>\n )\n}\n\nexport interface AvatarPickerProps {\n settings: AvatarSettings\n sets: CharacterSet[] | null\n busy: boolean\n /** Send a change; the builder applies the answer and reports failures. */\n run: (work: () => Promise<AvatarSettings>) => Promise<void>\n}\n\nexport function AvatarPicker({ settings, sets, busy, run }: AvatarPickerProps) {\n const [setIdx, setSetIdx] = useState(0)\n // Open on the set holding the person's character.\n useEffect(() => {\n const at = sets?.findIndex(s => s.characters.some(c => c.id === settings.sprite)) ?? -1\n if (at > 0) setSetIdx(at)\n // Only when the table arrives: afterwards the person moves between sets.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [sets])\n\n const characters = new Map((sets ?? []).flatMap(s => s.characters).map(c => [c.id, c]))\n const spriteSrc = (id: string | null | undefined) => {\n const c = id ? characters.get(id) : undefined\n return c ? portraitUrl(c) : null\n }\n\n const available: Record<AvatarSource, boolean> = {\n sprite: true,\n discord: Boolean(settings.linked.discord?.avatarUrl),\n steam: Boolean(settings.linked.steam?.avatarUrl),\n upload: settings.upload !== null\n }\n const thumbOf: Record<AvatarSource, string | null> = {\n sprite: spriteSrc(settings.sprite),\n discord: settings.linked.discord?.avatarUrl ?? null,\n steam: settings.linked.steam?.avatarUrl ?? null,\n upload: settings.upload?.url ?? null\n }\n const shown = settings.avatar\n const current = sets?.[setIdx]\n\n return (\n <div style={S.stack}>\n {settings.choice && settings.choice !== shown.source && (\n <span className=\"settings-popout__hint\">\n {LABEL[settings.choice]} is unavailable, so your character shows instead.\n </span>\n )}\n\n <div style={S.sources} role=\"radiogroup\" aria-label=\"Picture\">\n {(Object.keys(LABEL) as AvatarSource[]).map(src => (\n <button\n key={src}\n type=\"button\"\n role=\"radio\"\n aria-checked={shown.source === src}\n disabled={busy || !available[src]}\n title={\n available[src]\n ? undefined\n : src === 'upload'\n ? 'Upload a picture below first'\n : `Link ${LABEL[src]} under Settings › Account first`\n }\n style={{\n ...S.source,\n ...(shown.source === src ? S.sourceOn : {}),\n opacity: available[src] ? 1 : 0.5\n }}\n onClick={() => {\n run(() => avatarClient.choose(src)).catch(reportErr)\n }}\n >\n <Picture src={thumbOf[src]} style={S.thumb} />\n {LABEL[src]}\n </button>\n ))}\n </div>\n\n {shown.source === 'sprite' &&\n (sets ? (\n <>\n {sets.length > 1 && (\n <div style={S.sets}>\n {sets.map((s, i) => (\n <button\n key={s.id}\n type=\"button\"\n className={`settings-popout__btn${i === setIdx ? ' settings-popout__btn--primary' : ''}`}\n onClick={() => setSetIdx(i)}\n >\n {s.id}\n </button>\n ))}\n </div>\n )}\n <div style={S.grid} role=\"radiogroup\" aria-label=\"Character\">\n {current?.characters.map(c => (\n <button\n key={c.id}\n type=\"button\"\n role=\"radio\"\n aria-checked={settings.sprite === c.id}\n aria-label={c.name}\n title={c.name}\n disabled={busy}\n style={{\n ...S.cell,\n borderColor: settings.sprite === c.id ? c.color : 'transparent'\n }}\n onClick={() => {\n run(() => avatarClient.update({ sprite: c.id })).catch(reportErr)\n }}\n >\n <img src={portraitUrl(c)} alt=\"\" style={S.img} />\n </button>\n ))}\n </div>\n </>\n ) : (\n <span className=\"settings-popout__hint\">…</span>\n ))}\n\n <div style={S.line}>\n <span className=\"settings-popout__label\" style={S.grow}>\n Upload\n </span>\n <label className=\"settings-popout__btn\" style={{ opacity: busy ? 0.5 : 1 }}>\n {settings.upload ? 'Replace' : 'Choose image'}\n <input\n type=\"file\"\n accept={settings.limits.acceptedTypes.join(',')}\n hidden\n disabled={busy}\n onChange={e => {\n const file = e.currentTarget.files?.[0]\n e.currentTarget.value = ''\n if (file) run(() => avatarClient.upload(file)).catch(reportErr)\n }}\n />\n </label>\n {settings.upload && (\n <button\n type=\"button\"\n className=\"settings-popout__btn\"\n disabled={busy}\n onClick={() => {\n run(() => avatarClient.removeUpload()).catch(reportErr)\n }}\n >\n Remove\n </button>\n )}\n </div>\n </div>\n )\n}\n\nfunction reportErr(err: unknown) {\n console.error('[AvatarPicker] action failed', (err as Error)?.message ?? err)\n}\n"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"ConnectedSettings.d.ts","sourceRoot":"","sources":["../../src/components/ConnectedSettings.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,OAAO,KAA0D,MAAM,OAAO,CAAA;AAU9E,OAAO,EAWL,KAAK,IAAI,EACV,MAAM,uBAAuB,CAAA;AAC9B,OAAO,mBAAmB,CAAA;AAE1B,MAAM,WAAW,sBAAsB;IACrC;;;;;;+EAM2E;IAC3E,QAAQ,CAAC,EAAE,IAAI,CAAA;IACf,uEAAuE;IACvE,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,mEAAmE;IACnE,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAA;IAC5C;;;;;4EAKwE;IACxE,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IAC1B;;6EAEyE;IACzE,OAAO,CAAC,EAAE,cAAc,EAAE,CAAA;CAC3B;AAED,sDAAsD;AACtD,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,KAAK,CAAC,SAAS,CAAA;CAC9B;AAmID,wBAAgB,iBAAiB,CAAC,EAChC,QAAQ,EAAE,YAAY,EACtB,IAAI,EAAE,QAAQ,EACd,YAAY,EACZ,QAAQ,EACR,OAAO,EACR,EAAE,sBAAsB,qBA4yBxB"}
1
+ {"version":3,"file":"ConnectedSettings.d.ts","sourceRoot":"","sources":["../../src/components/ConnectedSettings.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,OAAO,KAA0D,MAAM,OAAO,CAAA;AAW9E,OAAO,EAWL,KAAK,IAAI,EACV,MAAM,uBAAuB,CAAA;AAC9B,OAAO,mBAAmB,CAAA;AAE1B,MAAM,WAAW,sBAAsB;IACrC;;;;;;+EAM2E;IAC3E,QAAQ,CAAC,EAAE,IAAI,CAAA;IACf,uEAAuE;IACvE,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,mEAAmE;IACnE,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAA;IAC5C;;;;;4EAKwE;IACxE,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IAC1B;;6EAEyE;IACzE,OAAO,CAAC,EAAE,cAAc,EAAE,CAAA;CAC3B;AAED,sDAAsD;AACtD,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,KAAK,CAAC,SAAS,CAAA;CAC9B;AAmID,wBAAgB,iBAAiB,CAAC,EAChC,QAAQ,EAAE,YAAY,EACtB,IAAI,EAAE,QAAQ,EACd,YAAY,EACZ,QAAQ,EACR,OAAO,EACR,EAAE,sBAAsB,qBAq0BxB"}