@hadoku/task-ui-components 4.22.5 → 4.22.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.
@@ -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;AAqI/D,wBAAgB,YAAY,sBAkR3B"}
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"}
@@ -82,8 +82,7 @@ const S = {
82
82
  },
83
83
  line: { display: "flex", alignItems: "center", gap: 6, flexWrap: "wrap" },
84
84
  grow: { flex: 1, minWidth: 0 },
85
- list: { margin: 0, paddingLeft: 18, fontSize: "0.75rem", lineHeight: 1.45 },
86
- fine: { fontSize: "0.7rem", color: "var(--color-text-secondary)", lineHeight: 1.4, margin: 0 }
85
+ list: { margin: 0, paddingLeft: 18, fontSize: "0.75rem", lineHeight: 1.45 }
87
86
  };
88
87
  function portraitUrl(c) {
89
88
  return c.portrait.startsWith("/") ? `${WATCHPARTY_BASE}${c.portrait}` : c.portrait;
@@ -193,8 +192,7 @@ function AvatarPicker() {
193
192
  },
194
193
  src
195
194
  )) }),
196
- /* @__PURE__ */ jsx("span", { className: "settings-popout__label", children: "Character" }),
197
- sets ? /* @__PURE__ */ jsxs(Fragment, { children: [
195
+ shown.source === "sprite" && (sets ? /* @__PURE__ */ jsxs(Fragment, { children: [
198
196
  sets.length > 1 && /* @__PURE__ */ jsx("div", { style: S.sets, children: sets.map((s, i) => /* @__PURE__ */ jsx(
199
197
  "button",
200
198
  {
@@ -225,7 +223,7 @@ function AvatarPicker() {
225
223
  },
226
224
  c.id
227
225
  )) })
228
- ] }) : /* @__PURE__ */ jsx("span", { className: "settings-popout__hint", children: "…" }),
226
+ ] }) : /* @__PURE__ */ jsx("span", { className: "settings-popout__hint", children: "…" })),
229
227
  ["discord", "steam"].map((p) => {
230
228
  const linked = settings.linked[p];
231
229
  return /* @__PURE__ */ jsxs("div", { style: S.line, children: [
@@ -305,12 +303,7 @@ function AvatarPicker() {
305
303
  }
306
304
  )
307
305
  ] }),
308
- error && /* @__PURE__ */ jsx("span", { className: "settings-popout__error", role: "alert", children: error }),
309
- /* @__PURE__ */ jsxs("p", { style: S.fine, children: [
310
- "Steam avatars via the Steam Web API. Steam is a trademark of Valve; hadoku.me is not affiliated with Valve, Steam or Discord.",
311
- " ",
312
- /* @__PURE__ */ jsx("a", { href: "/privacy#avatars", target: "_blank", rel: "noopener", children: "What is kept" })
313
- ] })
306
+ error && /* @__PURE__ */ jsx("span", { className: "settings-popout__error", role: "alert", children: error })
314
307
  ] });
315
308
  }
316
309
  function message(err) {
@@ -1 +1 @@
1
- {"version":3,"file":"AvatarPicker.js","sources":["../../src/components/AvatarPicker.tsx"],"sourcesContent":["/**\n * AvatarPicker — the avatar section of 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 it (\"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: 48,\n height: 48,\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(6, 1fr)', gap: 4 },\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 fine: { fontSize: '0.7rem', color: 'var(--color-text-secondary)', lineHeight: 1.4, margin: 0 }\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 <span className=\"settings-popout__label\">Character</span>\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\n <p style={S.fine}>\n Steam avatars via the Steam Web API. Steam is a trademark of Valve; hadoku.me is not\n affiliated with Valve, Steam or Discord.{' '}\n <a href=\"/privacy#avatars\" target=\"_blank\" rel=\"noopener\">\n What is kept\n </a>\n </p>\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 avatar section of 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 it (\"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: 48,\n height: 48,\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(6, 1fr)', gap: 4 },\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 +1 @@
1
- {"version":3,"file":"avatar.js","sources":["../../../../../node_modules/.pnpm/@wolffm+prefs-client@1.1.6_react@19.2.8_zod@4.6.7/node_modules/@wolffm/prefs-client/dist/avatar.js"],"sourcesContent":["// src/constants.ts\nvar PREFS_API_BASE = \"https://hadoku.me/prefs\";\n\n// src/avatar-contract.ts\nvar AVATAR_SOURCES = [\"sprite\", \"discord\", \"steam\", \"upload\"];\nvar LINK_PROVIDERS = [\"discord\", \"steam\"];\nvar LINK_MESSAGE_TYPE = \"hadoku-avatar-link\";\nvar RESOLVE_MAX_IDS = 100;\nvar SPRITE_ID_PATTERN = /^[a-z][a-z0-9-]{0,31}$/;\nvar LINK_CALLBACK_PATH = \"/account/link/callback\";\n\n// src/avatar.ts\nvar LINK_WINDOW_NAME = \"hadoku-avatar-link\";\nvar LINK_POPUP_MARK = \"hadoku-avatar-link-popup\";\nfunction markPopup(on) {\n try {\n if (on) sessionStorage.setItem(LINK_POPUP_MARK, \"1\");\n else sessionStorage.removeItem(LINK_POPUP_MARK);\n } catch {\n }\n}\nvar AvatarError = class extends Error {\n constructor(status, message) {\n super(message);\n this.status = status;\n this.name = \"AvatarError\";\n }\n status;\n};\nasync function readJson(res) {\n if (!res.ok) {\n const body = await res.json().catch(() => null);\n throw new AvatarError(res.status, body?.message ?? `HTTP ${res.status}`);\n }\n return await res.json();\n}\nfunction createAvatarClient(opts = {}) {\n const base = `${opts.baseUrl ?? PREFS_API_BASE}/api/v1/_avatar`;\n const f = opts.fetch ?? fetch;\n const call = (path, init = {}) => f(`${base}${path}`, { credentials: \"include\", ...init }).then(readJson);\n const send = (path, method, body) => call(path, {\n method,\n ...body === void 0 ? {} : { headers: { \"Content-Type\": \"application/json\" }, body: JSON.stringify(body) }\n });\n const getSettings = () => call(\"\");\n async function linkInPopup(provider) {\n markPopup(true);\n const popup = window.open(\"about:blank\", LINK_WINDOW_NAME, \"popup,width=520,height=760\");\n markPopup(false);\n if (!popup)\n throw new AvatarError(0, \"The sign-in window was blocked: allow pop-ups for hadoku.me\");\n let authorizeUrl;\n try {\n ({ authorizeUrl } = await send(`/link/${provider}`, \"POST\", {}));\n } catch (err) {\n popup.close();\n throw err;\n }\n popup.location.href = authorizeUrl;\n return new Promise((resolve, reject) => {\n const channel = new BroadcastChannel(LINK_MESSAGE_TYPE);\n let settled = false;\n const finish = (failure) => {\n if (settled) return;\n settled = true;\n channel.close();\n clearInterval(closedPoll);\n clearTimeout(giveUp);\n if (failure) reject(new AvatarError(400, failure));\n else getSettings().then(resolve, reject);\n };\n channel.onmessage = (e) => {\n if (e.data?.type !== LINK_MESSAGE_TYPE) return;\n finish(e.data.ok ? void 0 : e.data.error);\n };\n const closedPoll = setInterval(() => {\n if (!popup.closed) return;\n clearInterval(closedPoll);\n setTimeout(() => finish(), 1500);\n }, 500);\n const giveUp = setTimeout(() => finish(), 10 * 60 * 1e3);\n });\n }\n return {\n /** The caller's settings, including the resolved avatar. */\n get: getSettings,\n /** Pick a source. 409 when it is not available (not linked, nothing uploaded). */\n choose: (source) => send(\"\", \"PUT\", { source }),\n /** Set the source, the character, or both in one write. */\n update: (u) => send(\"\", \"PUT\", u),\n /**\n * Begin linking. Navigate the browser to the returned URL; the provider\n * returns it to hadoku.me/account/link/callback, which completes the link.\n * Call this only from the user's own click: nothing is fetched from\n * Discord or Steam before they ask.\n */\n startLink: (provider) => send(`/link/${provider}`, \"POST\", {}),\n /**\n * Link in a popup, so the page never navigates. Call it straight from the\n * click handler: the window is opened before the first await, which is\n * what popup blockers allow. Resolves with fresh settings once the popup\n * reports back or is closed (closing is not an error: the settings say\n * whether the link landed). Rejects only when the popup could not open\n * or the link could not start.\n */\n linkInPopup: (provider) => linkInPopup(provider),\n /** Finish a link from the callback page's query string (`location.search`). */\n completeLink: (query) => send(\"/link/complete\", \"POST\", { query: query.replace(/^\\?/, \"\") }),\n /** Delete the linked id and its cached avatar URL. */\n unlink: (provider) => send(`/link/${provider}`, \"DELETE\"),\n /** Upload an image (PNG, JPEG, WebP or GIF). It is re-encoded and becomes the chosen source. */\n upload: (image) => call(\"/upload\", { method: \"POST\", body: image }),\n removeUpload: () => send(\"/upload\", \"DELETE\")\n };\n}\nasync function resolveAvatars(opts, userIds) {\n const f = opts.fetch ?? fetch;\n const url = `${opts.baseUrl ?? PREFS_API_BASE}/api/v1/_avatar/resolve`;\n const unique = [...new Set(userIds)];\n const out = {};\n for (let i = 0; i < unique.length; i += RESOLVE_MAX_IDS) {\n const res = await f(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", \"X-User-Key\": opts.serviceKey },\n body: JSON.stringify({ userIds: unique.slice(i, i + RESOLVE_MAX_IDS) })\n });\n Object.assign(out, (await readJson(res)).avatars);\n }\n return out;\n}\nexport {\n AVATAR_SOURCES,\n AvatarError,\n LINK_CALLBACK_PATH,\n LINK_MESSAGE_TYPE,\n LINK_POPUP_MARK,\n LINK_PROVIDERS,\n RESOLVE_MAX_IDS,\n SPRITE_ID_PATTERN,\n createAvatarClient,\n resolveAvatars\n};\n"],"names":[],"mappings":"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","x_google_ignoreList":[0]}
1
+ {"version":3,"file":"avatar.js","sources":["../../../../../node_modules/.pnpm/@wolffm+prefs-client@1.1.7_react@19.2.8_zod@4.6.7/node_modules/@wolffm/prefs-client/dist/avatar.js"],"sourcesContent":["// src/constants.ts\nvar PREFS_API_BASE = \"https://hadoku.me/prefs\";\n\n// src/avatar-contract.ts\nvar AVATAR_SOURCES = [\"sprite\", \"discord\", \"steam\", \"upload\"];\nvar LINK_PROVIDERS = [\"discord\", \"steam\"];\nvar LINK_MESSAGE_TYPE = \"hadoku-avatar-link\";\nvar RESOLVE_MAX_IDS = 100;\nvar SPRITE_ID_PATTERN = /^[a-z][a-z0-9-]{0,31}$/;\nvar LINK_CALLBACK_PATH = \"/account/link/callback\";\n\n// src/avatar.ts\nvar LINK_WINDOW_NAME = \"hadoku-avatar-link\";\nvar LINK_POPUP_MARK = \"hadoku-avatar-link-popup\";\nfunction markPopup(on) {\n try {\n if (on) sessionStorage.setItem(LINK_POPUP_MARK, \"1\");\n else sessionStorage.removeItem(LINK_POPUP_MARK);\n } catch {\n }\n}\nvar AvatarError = class extends Error {\n constructor(status, message) {\n super(message);\n this.status = status;\n this.name = \"AvatarError\";\n }\n status;\n};\nasync function readJson(res) {\n if (!res.ok) {\n const body = await res.json().catch(() => null);\n throw new AvatarError(res.status, body?.message ?? `HTTP ${res.status}`);\n }\n return await res.json();\n}\nfunction createAvatarClient(opts = {}) {\n const base = `${opts.baseUrl ?? PREFS_API_BASE}/api/v1/_avatar`;\n const f = opts.fetch ?? fetch;\n const call = (path, init = {}) => f(`${base}${path}`, { credentials: \"include\", ...init }).then(readJson);\n const send = (path, method, body) => call(path, {\n method,\n ...body === void 0 ? {} : { headers: { \"Content-Type\": \"application/json\" }, body: JSON.stringify(body) }\n });\n const getSettings = () => call(\"\");\n async function linkInPopup(provider) {\n markPopup(true);\n const popup = window.open(\"about:blank\", LINK_WINDOW_NAME, \"popup,width=520,height=760\");\n markPopup(false);\n if (!popup)\n throw new AvatarError(0, \"The sign-in window was blocked: allow pop-ups for hadoku.me\");\n let authorizeUrl;\n try {\n ({ authorizeUrl } = await send(`/link/${provider}`, \"POST\", {}));\n } catch (err) {\n popup.close();\n throw err;\n }\n popup.location.href = authorizeUrl;\n return new Promise((resolve, reject) => {\n const channel = new BroadcastChannel(LINK_MESSAGE_TYPE);\n let settled = false;\n const finish = (failure) => {\n if (settled) return;\n settled = true;\n channel.close();\n clearInterval(closedPoll);\n clearTimeout(giveUp);\n if (failure) reject(new AvatarError(400, failure));\n else getSettings().then(resolve, reject);\n };\n channel.onmessage = (e) => {\n if (e.data?.type !== LINK_MESSAGE_TYPE) return;\n finish(e.data.ok ? void 0 : e.data.error);\n };\n const closedPoll = setInterval(() => {\n if (!popup.closed) return;\n clearInterval(closedPoll);\n setTimeout(() => finish(), 1500);\n }, 500);\n const giveUp = setTimeout(() => finish(), 10 * 60 * 1e3);\n });\n }\n return {\n /** The caller's settings, including the resolved avatar. */\n get: getSettings,\n /** Pick a source. 409 when it is not available (not linked, nothing uploaded). */\n choose: (source) => send(\"\", \"PUT\", { source }),\n /** Set the source, the character, or both in one write. */\n update: (u) => send(\"\", \"PUT\", u),\n /**\n * Begin linking. Navigate the browser to the returned URL; the provider\n * returns it to hadoku.me/account/link/callback, which completes the link.\n * Call this only from the user's own click: nothing is fetched from\n * Discord or Steam before they ask.\n */\n startLink: (provider) => send(`/link/${provider}`, \"POST\", {}),\n /**\n * Link in a popup, so the page never navigates. Call it straight from the\n * click handler: the window is opened before the first await, which is\n * what popup blockers allow. Resolves with fresh settings once the popup\n * reports back or is closed (closing is not an error: the settings say\n * whether the link landed). Rejects only when the popup could not open\n * or the link could not start.\n */\n linkInPopup: (provider) => linkInPopup(provider),\n /** Finish a link from the callback page's query string (`location.search`). */\n completeLink: (query) => send(\"/link/complete\", \"POST\", { query: query.replace(/^\\?/, \"\") }),\n /** Delete the linked id and its cached avatar URL. */\n unlink: (provider) => send(`/link/${provider}`, \"DELETE\"),\n /** Upload an image (PNG, JPEG, WebP or GIF). It is re-encoded and becomes the chosen source. */\n upload: (image) => call(\"/upload\", { method: \"POST\", body: image }),\n removeUpload: () => send(\"/upload\", \"DELETE\")\n };\n}\nasync function resolveAvatars(opts, userIds) {\n const f = opts.fetch ?? fetch;\n const url = `${opts.baseUrl ?? PREFS_API_BASE}/api/v1/_avatar/resolve`;\n const unique = [...new Set(userIds)];\n const out = {};\n for (let i = 0; i < unique.length; i += RESOLVE_MAX_IDS) {\n const res = await f(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", \"X-User-Key\": opts.serviceKey },\n body: JSON.stringify({ userIds: unique.slice(i, i + RESOLVE_MAX_IDS) })\n });\n Object.assign(out, (await readJson(res)).avatars);\n }\n return out;\n}\nexport {\n AVATAR_SOURCES,\n AvatarError,\n LINK_CALLBACK_PATH,\n LINK_MESSAGE_TYPE,\n LINK_POPUP_MARK,\n LINK_PROVIDERS,\n RESOLVE_MAX_IDS,\n SPRITE_ID_PATTERN,\n createAvatarClient,\n resolveAvatars\n};\n"],"names":[],"mappings":"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","x_google_ignoreList":[0]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hadoku/task-ui-components",
3
- "version": "4.22.5",
3
+ "version": "4.22.7",
4
4
  "description": "Reusable UI components with Bento grid and metallic gradient support",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -131,8 +131,7 @@ const S = {
131
131
  },
132
132
  line: { display: 'flex', alignItems: 'center', gap: 6, flexWrap: 'wrap' },
133
133
  grow: { flex: 1, minWidth: 0 },
134
- list: { margin: 0, paddingLeft: 18, fontSize: '0.75rem', lineHeight: 1.45 },
135
- fine: { fontSize: '0.7rem', color: 'var(--color-text-secondary)', lineHeight: 1.4, margin: 0 }
134
+ list: { margin: 0, paddingLeft: 18, fontSize: '0.75rem', lineHeight: 1.45 }
136
135
  } satisfies Record<string, React.CSSProperties>
137
136
 
138
137
  function portraitUrl(c: Character): string {
@@ -277,49 +276,49 @@ export function AvatarPicker() {
277
276
  ))}
278
277
  </div>
279
278
 
280
- <span className="settings-popout__label">Character</span>
281
- {sets ? (
282
- <>
283
- {sets.length > 1 && (
284
- <div style={S.sets}>
285
- {sets.map((s, i) => (
279
+ {shown.source === 'sprite' &&
280
+ (sets ? (
281
+ <>
282
+ {sets.length > 1 && (
283
+ <div style={S.sets}>
284
+ {sets.map((s, i) => (
285
+ <button
286
+ key={s.id}
287
+ type="button"
288
+ className={`settings-popout__btn${i === setIdx ? ' settings-popout__btn--primary' : ''}`}
289
+ onClick={() => setSetIdx(i)}
290
+ >
291
+ {s.id}
292
+ </button>
293
+ ))}
294
+ </div>
295
+ )}
296
+ <div style={S.grid} role="radiogroup" aria-label="Character">
297
+ {current?.characters.map(c => (
286
298
  <button
287
- key={s.id}
299
+ key={c.id}
288
300
  type="button"
289
- className={`settings-popout__btn${i === setIdx ? ' settings-popout__btn--primary' : ''}`}
290
- onClick={() => setSetIdx(i)}
301
+ role="radio"
302
+ aria-checked={settings.sprite === c.id}
303
+ aria-label={c.name}
304
+ title={c.name}
305
+ disabled={busy}
306
+ style={{
307
+ ...S.cell,
308
+ borderColor: settings.sprite === c.id ? c.color : 'transparent'
309
+ }}
310
+ onClick={() => {
311
+ run(() => client.update({ sprite: c.id })).catch(reportErr)
312
+ }}
291
313
  >
292
- {s.id}
314
+ <img src={portraitUrl(c)} alt="" style={S.img} />
293
315
  </button>
294
316
  ))}
295
317
  </div>
296
- )}
297
- <div style={S.grid} role="radiogroup" aria-label="Character">
298
- {current?.characters.map(c => (
299
- <button
300
- key={c.id}
301
- type="button"
302
- role="radio"
303
- aria-checked={settings.sprite === c.id}
304
- aria-label={c.name}
305
- title={c.name}
306
- disabled={busy}
307
- style={{
308
- ...S.cell,
309
- borderColor: settings.sprite === c.id ? c.color : 'transparent'
310
- }}
311
- onClick={() => {
312
- run(() => client.update({ sprite: c.id })).catch(reportErr)
313
- }}
314
- >
315
- <img src={portraitUrl(c)} alt="" style={S.img} />
316
- </button>
317
- ))}
318
- </div>
319
- </>
320
- ) : (
321
- <span className="settings-popout__hint">…</span>
322
- )}
318
+ </>
319
+ ) : (
320
+ <span className="settings-popout__hint">…</span>
321
+ ))}
323
322
 
324
323
  {(['discord', 'steam'] as const).map(p => {
325
324
  const linked = settings.linked[p]
@@ -421,14 +420,6 @@ export function AvatarPicker() {
421
420
  {error}
422
421
  </span>
423
422
  )}
424
-
425
- <p style={S.fine}>
426
- Steam avatars via the Steam Web API. Steam is a trademark of Valve; hadoku.me is not
427
- affiliated with Valve, Steam or Discord.{' '}
428
- <a href="/privacy#avatars" target="_blank" rel="noopener">
429
- What is kept
430
- </a>
431
- </p>
432
423
  </div>
433
424
  )
434
425
  }