@hadoku/task-ui-components 4.23.3 → 4.23.5

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.
@@ -0,0 +1,22 @@
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.
5
+ *
6
+ * Rendered into document.body, so no ancestor of the popout (a transform, an
7
+ * overflow, a stacking context) can clip or trap it. The popout stays open
8
+ * underneath: closing the dialog returns to it, and focus goes back to Change.
9
+ *
10
+ * Styles are inline for the reason AvatarPicker's are: apps compile this
11
+ * package's CSS at their own build, so a new stylesheet rule reaches an app only
12
+ * when it rebuilds. Tokens only.
13
+ */
14
+ import React from 'react';
15
+ export interface AvatarDialogProps {
16
+ open: boolean;
17
+ onClose: () => void;
18
+ /** Where focus goes when the dialog closes (the Change button). */
19
+ returnFocus?: React.RefObject<HTMLElement | null>;
20
+ }
21
+ export declare function AvatarDialog({ open, onClose, returnFocus }: AvatarDialogProps): React.ReactPortal | null;
22
+ //# sourceMappingURL=AvatarDialog.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,95 @@
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { useId, useRef, useEffect } from "react";
3
+ import { createPortal } from "react-dom";
4
+ import { AvatarPicker } from "./AvatarPicker.js";
5
+ const S = {
6
+ overlay: {
7
+ position: "fixed",
8
+ inset: 0,
9
+ // Above the popout's panel (100) and its click-catcher (99).
10
+ zIndex: 1e3,
11
+ display: "flex",
12
+ alignItems: "flex-start",
13
+ justifyContent: "center",
14
+ boxSizing: "border-box",
15
+ padding: "6vh 16px",
16
+ background: "var(--color-bg-overlay)"
17
+ },
18
+ card: {
19
+ boxSizing: "border-box",
20
+ width: "min(720px, 100%)",
21
+ maxHeight: "88vh",
22
+ overflowY: "auto",
23
+ display: "flex",
24
+ flexDirection: "column",
25
+ gap: 14,
26
+ padding: 20,
27
+ borderRadius: 12,
28
+ background: "var(--color-bg-card)",
29
+ border: "1px solid var(--color-border)",
30
+ boxShadow: "0 8px 28px rgba(0, 0, 0, 0.18)",
31
+ color: "var(--color-text)",
32
+ // Outside the app's root, so nothing passes the app's font down to it.
33
+ fontFamily: "var(--font-family)",
34
+ outline: "none"
35
+ },
36
+ head: { display: "flex", alignItems: "center", gap: 10 },
37
+ title: { margin: 0, flex: 1, fontSize: "1.05rem", fontWeight: 600 }
38
+ };
39
+ function AvatarDialog({ open, onClose, returnFocus }) {
40
+ const titleId = useId();
41
+ const cardRef = useRef(null);
42
+ const onCloseRef = useRef(onClose);
43
+ onCloseRef.current = onClose;
44
+ useEffect(() => {
45
+ if (!open) return;
46
+ cardRef.current?.focus();
47
+ const onKey = (e) => {
48
+ if (e.key !== "Escape") return;
49
+ e.preventDefault();
50
+ e.stopPropagation();
51
+ onCloseRef.current();
52
+ };
53
+ document.addEventListener("keydown", onKey);
54
+ const back = returnFocus?.current;
55
+ return () => {
56
+ document.removeEventListener("keydown", onKey);
57
+ back?.focus();
58
+ };
59
+ }, [open, returnFocus]);
60
+ if (!open || typeof document === "undefined") return null;
61
+ return createPortal(
62
+ /* @__PURE__ */ jsx("div", { style: S.overlay, onClick: onClose, children: /* @__PURE__ */ jsxs(
63
+ "div",
64
+ {
65
+ ref: cardRef,
66
+ role: "dialog",
67
+ "aria-modal": "true",
68
+ "aria-labelledby": titleId,
69
+ tabIndex: -1,
70
+ style: S.card,
71
+ onClick: (e) => e.stopPropagation(),
72
+ children: [
73
+ /* @__PURE__ */ jsxs("div", { style: S.head, children: [
74
+ /* @__PURE__ */ jsx("h2", { id: titleId, style: S.title, children: "Avatar" }),
75
+ /* @__PURE__ */ jsx(
76
+ "button",
77
+ {
78
+ type: "button",
79
+ className: "settings-popout__btn settings-popout__btn--primary",
80
+ onClick: onClose,
81
+ children: "Done"
82
+ }
83
+ )
84
+ ] }),
85
+ /* @__PURE__ */ jsx(AvatarPicker, {})
86
+ ]
87
+ }
88
+ ) }),
89
+ document.body
90
+ );
91
+ }
92
+ export {
93
+ AvatarDialog
94
+ };
95
+ //# sourceMappingURL=AvatarDialog.js.map
@@ -0,0 +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,5 +1,5 @@
1
1
  /**
2
- * AvatarPicker — the avatar section of ConnectedSettings: the picture people
2
+ * AvatarPicker — the body of AvatarDialog (opened from ConnectedSettings): the picture people
3
3
  * see (character, Discord, Steam or an upload) and the character games use as
4
4
  * the piece. One resolved avatar per person, stored in hadoku_site's prefs-api
5
5
  * and rendered by every app (`@wolffm/prefs-client/avatar`, bundled into this
@@ -10,7 +10,7 @@
10
10
  * (`linkInPopup`), so the page under the popout never navigates and a game
11
11
  * keeps running.
12
12
  *
13
- * Loaded only when the person opens it ("Change"), never at mount: the avatar
13
+ * Loaded only when the person opens the dialog ("Change"), never at mount: the avatar
14
14
  * read can refresh a linked picture from Discord or Steam, which no page load
15
15
  * should cost, and opening the gear itself must stay free (settings-prefetch
16
16
  * e2e).
@@ -35,8 +35,8 @@ const S = {
35
35
  stack: { display: "flex", flexDirection: "column", gap: 10, width: "100%" },
36
36
  current: { display: "flex", alignItems: "center", gap: 10 },
37
37
  preview: {
38
- width: 48,
39
- height: 48,
38
+ width: 64,
39
+ height: 64,
40
40
  borderRadius: "50%",
41
41
  overflow: "hidden",
42
42
  flex: "none",
@@ -70,7 +70,7 @@ const S = {
70
70
  background: "var(--color-bg-alt)"
71
71
  },
72
72
  sets: { display: "flex", gap: 4, flexWrap: "wrap" },
73
- grid: { display: "grid", gridTemplateColumns: "repeat(6, 1fr)", gap: 4 },
73
+ grid: { display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(64px, 1fr))", gap: 6 },
74
74
  cell: {
75
75
  padding: 0,
76
76
  aspectRatio: "1",
@@ -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} 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 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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"}
@@ -4,7 +4,7 @@
4
4
  * property of the PERSON (not the app):
5
5
  * - current access tier (read-only, friendly label)
6
6
  * - display name (inline edit → POST /session/name)
7
- * - avatar: picture and character, chosen in place (AvatarPicker)
7
+ * - avatar: picture and character, chosen in a dialog (AvatarDialog)
8
8
  * - global content-visibility level (tiered pill → PUT /prefs/api/v1/content-level)
9
9
  * - switch key (an inline sign-in form, on the site's own sign-in client)
10
10
  *
@@ -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,qBAqyBxB"}
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"}
@@ -2,7 +2,7 @@ import { jsxs, jsx, Fragment } from "react/jsx-runtime";
2
2
  import React, { useState, useRef, useId, useCallback, useEffect } from "react";
3
3
  import { useSettingsTab } from "@wolffm/themes";
4
4
  import { SettingsIcon } from "./ThemeIcons.js";
5
- import { AvatarPicker } from "./AvatarPicker.js";
5
+ import { AvatarDialog } from "./AvatarDialog.js";
6
6
  import { passkeysSupported, prefetchSettings, setDisplayName, loadSignIn, switchKeyHref, loadSignInClient, passkeyHref, setContentLevel, deleteOwnAccount } from "../lib/settingsClient.js";
7
7
  /* empty css */
8
8
  const ServiceAccess = React.lazy(
@@ -145,6 +145,7 @@ function ConnectedSettings({
145
145
  const passkeyCtl = useRef(null);
146
146
  const cancelPasskey = useCallback((reason) => passkeyCtl.current?.abort(reason), []);
147
147
  const [avatarOpen, setAvatarOpen] = useState(false);
148
+ const avatarBtn = useRef(null);
148
149
  const [serviceGrants, setServiceGrants] = useState(0);
149
150
  const [serviceOpen, setServiceOpen] = useState(false);
150
151
  const [content, setContent] = useState(null);
@@ -219,13 +220,13 @@ function ConnectedSettings({
219
220
  };
220
221
  }, []);
221
222
  useEffect(() => {
222
- if (!open) return;
223
+ if (!open || avatarOpen) return;
223
224
  const onKey = (e) => {
224
225
  if (e.key === "Escape") setOpen(false);
225
226
  };
226
227
  window.addEventListener("keydown", onKey);
227
228
  return () => window.removeEventListener("keydown", onKey);
228
- }, [open]);
229
+ }, [open, avatarOpen]);
229
230
  const saveName = useCallback(async () => {
230
231
  const trimmed = nameDraft.trim();
231
232
  if (!trimmed || trimmed === displayName) {
@@ -479,15 +480,24 @@ function ConnectedSettings({
479
480
  /* @__PURE__ */ jsx(
480
481
  "button",
481
482
  {
483
+ ref: avatarBtn,
482
484
  type: "button",
483
485
  className: "settings-popout__btn",
486
+ "aria-haspopup": "dialog",
484
487
  "aria-expanded": avatarOpen,
485
- onClick: () => setAvatarOpen((v) => !v),
486
- children: avatarOpen ? "Done" : "Change"
488
+ onClick: () => setAvatarOpen(true),
489
+ children: "Change"
487
490
  }
488
491
  )
489
492
  ] }),
490
- avatarOpen && /* @__PURE__ */ jsx(AvatarPicker, {})
493
+ /* @__PURE__ */ jsx(
494
+ AvatarDialog,
495
+ {
496
+ open: avatarOpen,
497
+ onClose: () => setAvatarOpen(false),
498
+ returnFocus: avatarBtn
499
+ }
500
+ )
491
501
  ] }),
492
502
  !resolving && serviceGrants > 0 && /* @__PURE__ */ jsxs("section", { className: "settings-popout__row settings-popout__row--stack", children: [
493
503
  /* @__PURE__ */ jsxs("span", { className: "settings-popout__inline", children: [
@@ -1 +1 @@
1
- {"version":3,"file":"ConnectedSettings.js","sources":["../../src/components/ConnectedSettings.tsx"],"sourcesContent":["/**\n * ConnectedSettings — the unified per-user settings control for the hadoku\n * ecosystem: a gear button that opens a popout housing everything that is a\n * property of the PERSON (not the app):\n * - current access tier (read-only, friendly label)\n * - display name (inline edit → POST /session/name)\n * - avatar: picture and character, chosen in place (AvatarPicker)\n * - global content-visibility level (tiered pill → PUT /prefs/api/v1/content-level)\n * - switch key (an inline sign-in form, on the site's own sign-in client)\n *\n * Those are grouped into tabs (Profile, Content, Account), followed by the\n * app's own: `children` as one \"App\" tab, `appTabs` as one tab each. The open\n * tab is remembered per user and per app (useSettingsTab, @wolffm/themes).\n *\n * \"Connected\" (parallel to ConnectedThemePicker): it self-wires to the\n * hadoku.me edge-router via same-origin fetches (see lib/settingsClient), so\n * dropping `<ConnectedSettings />` into any app under hadoku.me needs zero\n * props. Identity is resolved via whoami() unless the host passes `userType` /\n * `name` (avoids a redundant fetch when the app already knows).\n *\n * All of that is resolved AT MOUNT via `prefetchSettings()`, never on\n * gear-click — opening the popout must cost zero requests. See the effect\n * below, and `e2e/settings-prefetch.spec.ts` in hadoku-task, which fails if the\n * fetch moves back behind the click.\n *\n * Mirrors ThemePicker's controlled-popout pattern: owns its own `open` state +\n * a full-viewport overlay for click-outside dismiss. The content pill renders\n * `maxLevel` segments (friend ⇒ 3, admin ⇒ 4) and is hidden for public callers.\n * maxLevel comes from the server so the tier ceiling is never trusted from the\n * client.\n */\nimport React, { useCallback, useEffect, useId, useRef, useState } from 'react'\nimport { useSettingsTab } from '@wolffm/themes'\nimport { SettingsIcon } from './ThemeIcons'\nimport { AvatarPicker } from './AvatarPicker'\n\n// A grantee's own service credentials, with a QR encoder for TOTP enrolment:\n// its own chunk, fetched only when a grantee opens it.\nconst ServiceAccess = React.lazy(() =>\n import('./ServiceAccess').then(m => ({ default: m.ServiceAccess }))\n)\nimport {\n deleteOwnAccount,\n prefetchSettings,\n setContentLevel,\n setDisplayName,\n loadSignIn,\n loadSignInClient,\n passkeyHref,\n passkeysSupported,\n switchKeyHref,\n type ContentLevelState,\n type Tier\n} from '../lib/settingsClient'\nimport '../app-header.css'\n\nexport interface ConnectedSettingsProps {\n /** Caller's tier. Omit to self-resolve via whoami().\n *\n * This exists for apps that CANNOT reach `/session/whoami` — conjure sits\n * behind a path-prefixed shim that serves nothing outside `/conjure`, so\n * whoami 404s and the tier would fall back to guest. It is an identity\n * hint, not a way to present a different tier than the edge assigns: every\n * gate that matters is enforced server-side off the edge-stamped tier. */\n userType?: Tier\n /** Display name. Omit to self-resolve via whoami(). Same rationale. */\n name?: string | null\n /** Notify the host (e.g. a \"Signed in as …\" label) of a rename. */\n onNameChange?: (name: string | null) => void\n /** App-specific settings as ONE tab, labelled \"App\", after the site's own\n * tabs — so an app with its own preferences keeps one unified gear instead\n * of a second control. For more than one section, use `appTabs`.\n *\n * This slot is deliberately unconstrained — put whatever the app needs in\n * it. The constraint is only that it cannot change the site's tabs. */\n children?: React.ReactNode\n /** App-specific settings as tabs of their own, after `children`'s \"App\" tab\n * if both are given. `id` is stable and unique within the app: the last-open\n * tab is remembered by it. `render` runs only while its tab is open. */\n appTabs?: AppSettingsTab[]\n}\n\n/** One app-contributed tab in the settings popout. */\nexport interface AppSettingsTab {\n id: string\n label: string\n render: () => React.ReactNode\n}\n\n/** Rejects once `signal` aborts, so a call that ignores the signal (an older\n * site client, a prompt that never settles) still lets the UI go. */\nfunction untilAborted(signal: AbortSignal): Promise<never> {\n return new Promise((_, reject) => {\n const fail = () => reject(new Error('cancelled'))\n if (signal.aborted) fail()\n else signal.addEventListener('abort', fail, { once: true })\n })\n}\n\n/** Abort reason when the popout closes: a dismissal, not an error to show. */\nconst CLOSED = 'closed'\n\n/** Rejection sink for fire-and-forget settings actions. The client helpers\n * already resolve to null on failure (state reflects it), so this only trips\n * on truly unexpected throws — log rather than swallow silently. */\nfunction reportErr(err: unknown) {\n // task-ui-components v2 dropped its logger; keep the package dependency-free.\n console.error('[ConnectedSettings] action failed', (err as Error)?.message ?? err)\n}\n\n/** Inline, not in app-header.css: every app runs this component from the\n * site's shared vendor module, but compiles app-header.css into its own\n * stylesheet at ITS build, so a new rule reaches an app only when it rebuilds. */\nconst PASSKEY_DOT: React.CSSProperties = {\n position: 'absolute',\n top: 2,\n right: 2,\n width: 8,\n height: 8,\n borderRadius: '50%',\n background: 'var(--color-primary)',\n pointerEvents: 'none'\n}\n\n/** Save passkey pulses while the dot is asking for it. Keyframes cannot be\n * inline, so the rule is injected once into the document instead, for the\n * same reason PASSKEY_DOT is inline. */\nconst PASSKEY_PULSE_CLASS = 'settings-popout__btn--pulse'\nconst PASSKEY_PULSE_CSS = `\n@keyframes settings-passkey-pulse {\n 0% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--color-primary) 70%, transparent); }\n 70% { box-shadow: 0 0 0 8px color-mix(in srgb, var(--color-primary) 0%, transparent); }\n 100% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--color-primary) 0%, transparent); }\n}\n@media (prefers-reduced-motion: no-preference) {\n .${PASSKEY_PULSE_CLASS}:not(:disabled) {\n animation: settings-passkey-pulse 1.6s ease-out infinite;\n }\n}\n`\n\n/** The tab strip and its panels. Injected for the same reason as the pulse:\n * an app's copy of app-header.css predates the tabs until it rebuilds. The\n * strip stays pinned while a long tab scrolls, and scrolls sideways itself\n * when the tabs outgrow a phone-width panel. */\nconst TABS_CSS = `\n.settings-popout__top {\n position: sticky;\n top: -16px;\n z-index: 1;\n display: flex;\n flex-direction: column;\n gap: 10px;\n margin: -16px -16px 0;\n padding: 16px 16px 0;\n background: var(--color-bg-card);\n border-bottom: 1px solid var(--color-border);\n}\n.settings-popout__tabs {\n display: flex;\n gap: 2px;\n margin: 0 -16px -1px;\n padding: 0 16px;\n overflow-x: auto;\n scrollbar-width: thin;\n}\n.settings-popout__tab {\n position: relative;\n flex: none;\n padding: 6px 10px;\n border: 0;\n border-bottom: 2px solid transparent;\n background: transparent;\n color: var(--color-text-secondary);\n font: inherit;\n font-size: 0.8rem;\n font-weight: 600;\n white-space: nowrap;\n cursor: pointer;\n}\n.settings-popout__tab:hover {\n color: var(--color-text);\n}\n.settings-popout__tab[aria-selected='true'] {\n color: var(--color-text);\n border-bottom-color: var(--color-primary);\n}\n.settings-popout__tab:focus-visible {\n outline: 2px solid var(--color-primary);\n outline-offset: -2px;\n border-radius: 6px 6px 0 0;\n}\n.settings-popout__tabpanel {\n display: flex;\n flex-direction: column;\n gap: 14px;\n}\n`\n\nfunction useInjectedStyle(id: string, css: string, active: boolean) {\n useEffect(() => {\n if (!active || document.getElementById(id)) return\n const el = document.createElement('style')\n el.id = id\n el.textContent = css\n document.head.appendChild(el)\n }, [id, css, active])\n}\n\nconst TIER_LABEL: Record<Tier, string> = {\n admin: 'Administrator',\n wife: 'Wife',\n service: 'Service',\n friend: 'Friend',\n member: 'Member',\n public: 'Guest'\n}\n\nexport function ConnectedSettings({\n userType: userTypeProp,\n name: nameProp,\n onNameChange,\n children,\n appTabs\n}: ConnectedSettingsProps) {\n const [open, setOpen] = useState(false)\n const rootRef = useRef<HTMLDivElement>(null)\n const tabIdBase = useId()\n const tabRefs = useRef(new Map<string, HTMLButtonElement>())\n const [savedTab, saveTab] = useSettingsTab()\n // This visit's pick; wins over the saved one while it is still offered.\n const [pickedTab, setPickedTab] = useState<string | null>(null)\n\n // Identity — from props if supplied, else resolved lazily via whoami().\n const [userType, setUserType] = useState<Tier>(userTypeProp ?? 'public')\n const [resolvedName, setResolvedName] = useState<string | null>(nameProp ?? null)\n\n // Name editing\n const [nameDraft, setNameDraft] = useState(resolvedName ?? '')\n const [editingName, setEditingName] = useState(false)\n const [nameSaving, setNameSaving] = useState(false)\n const [nameError, setNameError] = useState<string | null>(null)\n const [displayName, setLocalName] = useState<string | null>(resolvedName)\n\n // Key swap. The draft lives only in this state and the input: it is never\n // logged, stored or sent anywhere but the site's sign-in client.\n const [keyFormOpen, setKeyFormOpen] = useState(false)\n const [keyDraft, setKeyDraft] = useState('')\n const [keyBusy, setKeyBusy] = useState(false)\n const [keyError, setKeyError] = useState<string | null>(null)\n\n // Passkeys, through the same site client: save one while signed in, or\n // sign in with one from the key form. One prompt at a time, and always\n // cancellable: a password manager can dismiss the browser's prompt without\n // ever answering it, so Cancel, closing the popout and the site client's own\n // time limit each end the wait.\n const [passkeyMode, setPasskeyMode] = useState<'save' | 'signin' | null>(null)\n const [passkeySaved, setPasskeySaved] = useState(false)\n // How many passkeys sign this person in, from whoami; null = unknown.\n const [passkeyCount, setPasskeyCount] = useState<number | null>(null)\n const [passkeyError, setPasskeyError] = useState<string | null>(null)\n // Delete account: one click arms it, a second, separate button confirms.\n const [deleteStage, setDeleteStage] = useState<'idle' | 'confirm' | 'busy'>('idle')\n const [deleteError, setDeleteError] = useState<string | null>(null)\n const passkeyCtl = useRef<AbortController | null>(null)\n const cancelPasskey = useCallback((reason?: string) => passkeyCtl.current?.abort(reason), [])\n\n // Avatar: the picker loads only when opened, never at mount (AvatarPicker).\n const [avatarOpen, setAvatarOpen] = useState(false)\n // Service identities this person may mint for (whoami.serviceGrants).\n const [serviceGrants, setServiceGrants] = useState(0)\n const [serviceOpen, setServiceOpen] = useState(false)\n\n // Content level\n const [content, setContent] = useState<ContentLevelState | null>(null)\n const [levelSaving, setLevelSaving] = useState(false)\n\n // One flag for the whole prefetch, not one per request — the panel body\n // reveals when everything has settled, so nothing pops in a beat late. In\n // practice the user never sees it: the prefetch starts at mount and is long\n // done by the time the gear is clicked. It only shows if they click within\n // the first moments of the page.\n const [resolving, setResolving] = useState(true)\n\n // Everyone who is SIGNED IN, expressed as \"not public\" rather than as a list\n // of tiers. The list form (`=== 'admin' || === 'friend' || === 'service'`)\n // is an exact-match allowlist: it silently excluded `wife` the moment that\n // tier existed, which is the failure the rank model is meant to prevent.\n // There is no rank helper in this bundle, but \"above public\" needs none.\n const showContentPill = userType !== 'public'\n\n // Signed in on a key with no passkey: the gear carries a dot and Save passkey\n // is the primary action until one is saved. Unknown counts never prompt.\n const needsPasskey =\n !resolving &&\n userType !== 'public' &&\n passkeyCount === 0 &&\n !passkeySaved &&\n passkeysSupported()\n useInjectedStyle('settings-passkey-pulse', PASSKEY_PULSE_CSS, needsPasskey)\n useInjectedStyle('settings-tabs', TABS_CSS, open)\n\n // The site's own groups first, then the app's. App ids are namespaced so an\n // app tab called \"account\" can never collide with the site's.\n const tabs: { id: string; label: string; dot?: boolean }[] = [\n { id: 'profile', label: 'Profile' },\n ...(!resolving && showContentPill ? [{ id: 'content', label: 'Content' }] : []),\n // The gear's dot asks for a passkey; this one says where Save passkey is.\n { id: 'account', label: 'Account', dot: needsPasskey },\n ...(children ? [{ id: 'app:', label: 'App' }] : []),\n ...(appTabs ?? []).map(t => ({ id: `app:${t.id}`, label: t.label }))\n ]\n const tabIds = tabs.map(t => t.id)\n // Nothing remembered: open on the app's first tab, which is usually why the\n // gear was clicked inside an app.\n const defaultTab = tabIds.find(id => id.startsWith('app:')) ?? 'profile'\n const activeTab =\n [pickedTab, savedTab].find((id): id is string => id !== null && tabIds.includes(id)) ??\n defaultTab\n const appTab = appTabs?.find(t => `app:${t.id}` === activeTab)\n const tabDomId = (id: string) => `${tabIdBase}tab-${id}`\n const panelDomId = `${tabIdBase}panel`\n\n const selectTab = useCallback(\n (id: string) => {\n setPickedTab(id)\n saveTab(id)\n },\n [saveTab]\n )\n\n // Arrow keys, Home and End move between tabs and open the one they land on\n // (the ARIA tablist pattern with automatic activation).\n const onTabKey = (e: React.KeyboardEvent) => {\n const i = tabIds.indexOf(activeTab)\n const n = tabIds.length\n const next =\n e.key === 'ArrowRight'\n ? tabIds[(i + 1) % n]\n : e.key === 'ArrowLeft'\n ? tabIds[(i - 1 + n) % n]\n : e.key === 'Home'\n ? tabIds[0]\n : e.key === 'End'\n ? tabIds[n - 1]\n : undefined\n if (next === undefined) return\n e.preventDefault()\n selectTab(next)\n tabRefs.current.get(next)?.focus()\n }\n\n // A remembered tab past the edge of a narrow strip is scrolled into sight.\n // jsdom implements no scrollIntoView, and every consumer's unit test that\n // renders the popout runs there, so the call is optional.\n useEffect(() => {\n if (open)\n tabRefs.current.get(activeTab)?.scrollIntoView?.({ block: 'nearest', inline: 'nearest' })\n }, [open, activeTab])\n\n // Keep local identity in sync when the host controls it via props.\n useEffect(() => {\n if (userTypeProp !== undefined) setUserType(userTypeProp)\n }, [userTypeProp])\n useEffect(() => {\n if (nameProp !== undefined) {\n setResolvedName(nameProp)\n setLocalName(nameProp)\n setNameDraft(nameProp ?? '')\n }\n }, [nameProp])\n\n // Resolve everything the panel shows AT MOUNT — not on open.\n //\n // This used to be gated on `open`, which made the gear the trigger for\n // whoami + content-level (167ms / 186ms on prod, concurrent) — a round trip\n // the user waited out with the panel already on screen and blank. Nothing\n // about that data depends on the click:\n // it's the signed-in user's own identity, settled at boot and unchanged for\n // the life of the page. So it rides along with the app's boot traffic, and\n // clicking the gear costs zero requests. `e2e/settings-prefetch.spec.ts` in\n // hadoku-task pins that.\n //\n // The client memoises across callers, so mounting this in several places (or\n // remounting it) does not multiply requests, and on hadoku.me the identity\n // half is free — it awaits the whoami the shell already had in flight.\n useEffect(() => {\n let live = true\n prefetchSettings({ userType: userTypeProp, name: nameProp })\n .then(({ identity, content: state, passkeys, serviceGrants: grants }) => {\n if (!live) return\n setPasskeyCount(passkeys)\n setServiceGrants(grants)\n // Props stay authoritative: the host passed them because it can see an\n // identity this component can't resolve for itself.\n if (userTypeProp === undefined) {\n setUserType(identity.userType)\n setResolvedName(identity.name)\n setLocalName(identity.name)\n setNameDraft(identity.name ?? '')\n }\n if (state) setContent(state)\n })\n .catch(reportErr)\n .finally(() => {\n if (live) setResolving(false)\n })\n return () => {\n live = false\n }\n // Hints are read once, on the first mount that gets there — the prefetch is\n // page-scoped and a later prop change cannot un-issue it. Identity changes\n // arrive through the sync effects above, and a key swap reloads the page.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [])\n\n // Escape closes.\n useEffect(() => {\n if (!open) return\n const onKey = (e: KeyboardEvent) => {\n if (e.key === 'Escape') setOpen(false)\n }\n window.addEventListener('keydown', onKey)\n return () => window.removeEventListener('keydown', onKey)\n }, [open])\n\n const saveName = useCallback(async () => {\n const trimmed = nameDraft.trim()\n if (!trimmed || trimmed === displayName) {\n setEditingName(false)\n return\n }\n setNameSaving(true)\n const result = await setDisplayName(trimmed)\n setNameSaving(false)\n // A refusal keeps the editor open on the draft, saying why.\n if (!result.ok) {\n setNameError(result.error)\n return\n }\n setNameError(null)\n setLocalName(result.name)\n setNameDraft(result.name ?? '')\n if (result.name !== null) onNameChange?.(result.name)\n setEditingName(false)\n }, [nameDraft, displayName, onNameChange])\n\n // Open the form AND start loading the site's sign-in client, which begins its\n // fingerprint collection as it evaluates — so it is ready by submit. Where the\n // client cannot load at all (not on hadoku.me), the page-level /auth flow is\n // the only way to sign in, so go there.\n const openKeyForm = useCallback(() => {\n setKeyFormOpen(true)\n setKeyError(null)\n loadSignIn()\n .then(signIn => {\n if (!signIn) window.location.assign(switchKeyHref())\n })\n .catch(reportErr)\n }, [])\n\n const submitKey = useCallback(async () => {\n setKeyBusy(true)\n setKeyError(null)\n const signIn = await loadSignIn()\n if (!signIn) {\n window.location.assign(switchKeyHref())\n return\n }\n try {\n await signIn(keyDraft)\n } catch (err) {\n // A refused key is an inline error, never a redirect.\n setKeyError((err as Error)?.message || 'Invalid key')\n setKeyBusy(false)\n return\n }\n // The page now acts for a different identity: everything must re-mount.\n // Busy stays on through the reload so the form can't be resubmitted.\n window.location.reload()\n }, [keyDraft])\n\n const savePasskey = useCallback(async () => {\n const ctl = new AbortController()\n passkeyCtl.current = ctl\n setPasskeyMode('save')\n setPasskeyError(null)\n try {\n const client = await Promise.race([loadSignInClient(), untilAborted(ctl.signal)])\n if (!client?.addPasskey) {\n window.location.assign(passkeyHref('add'))\n return\n }\n await Promise.race([client.addPasskey({ signal: ctl.signal }), untilAborted(ctl.signal)])\n setPasskeySaved(true)\n } catch (err) {\n if (ctl.signal.reason !== CLOSED) {\n setPasskeyError(\n ctl.signal.aborted ? 'Passkey not saved' : (err as Error)?.message || 'Passkey not saved'\n )\n }\n } finally {\n if (passkeyCtl.current === ctl) passkeyCtl.current = null\n setPasskeyMode(null)\n }\n }, [])\n\n const signInWithPasskey = useCallback(async () => {\n const ctl = new AbortController()\n passkeyCtl.current = ctl\n setPasskeyMode('signin')\n setKeyBusy(true)\n setKeyError(null)\n let leaving = false\n try {\n const client = await Promise.race([loadSignInClient(), untilAborted(ctl.signal)])\n if (!client?.signInWithPasskey) {\n leaving = true\n window.location.assign(passkeyHref('signin'))\n return\n }\n await Promise.race([\n client.signInWithPasskey({ signal: ctl.signal }),\n untilAborted(ctl.signal)\n ])\n // The page now acts for a different identity: everything must re-mount.\n // Busy stays on through the reload so nothing can be resubmitted.\n leaving = true\n window.location.reload()\n } catch (err) {\n if (ctl.signal.reason !== CLOSED) {\n setKeyError(\n ctl.signal.aborted\n ? 'Passkey sign-in cancelled'\n : (err as Error)?.message || 'Passkey sign-in cancelled'\n )\n }\n } finally {\n if (passkeyCtl.current === ctl) passkeyCtl.current = null\n if (!leaving) {\n setPasskeyMode(null)\n setKeyBusy(false)\n }\n }\n }, [])\n\n // Closing the popout (or leaving the page) abandons an open prompt.\n useEffect(() => {\n if (!open) cancelPasskey(CLOSED)\n }, [open, cancelPasskey])\n useEffect(() => () => cancelPasskey(CLOSED), [cancelPasskey])\n\n const pickLevel = useCallback(\n async (level: number) => {\n if (!content || level === content.level || levelSaving) return\n const prev = content\n setContent({ ...content, level }) // optimistic\n setLevelSaving(true)\n const result = await setContentLevel(level)\n setLevelSaving(false)\n setContent(result ?? prev) // adopt server truth, or revert\n },\n [content, levelSaving]\n )\n\n // No caller-supplied class on the root. The four rows below are the\n // platform's, not the app's, and a className here is enough to restyle or\n // hide any of them — which is the drift this component exists to prevent.\n // Apps extend via `children` and `appTabs`, never by reaching into this markup.\n return (\n <div className=\"settings-popout\" ref={rootRef}>\n <button\n type=\"button\"\n className=\"app-header__icon-btn settings-toggle-btn\"\n aria-label={needsPasskey ? 'User settings, no passkey saved' : 'User settings'}\n style={needsPasskey ? { position: 'relative' } : undefined}\n aria-expanded={open}\n onClick={() => setOpen(v => !v)}\n >\n <SettingsIcon />\n {needsPasskey && (\n <span className=\"settings-toggle-btn__dot\" style={PASSKEY_DOT} aria-hidden=\"true\" />\n )}\n </button>\n\n {open && <div className=\"settings-popout__overlay\" onClick={() => setOpen(false)} />}\n\n {open && (\n <div className=\"settings-popout__panel\" role=\"dialog\" aria-label=\"User settings\">\n <div className=\"settings-popout__top\">\n <div className=\"settings-popout__header\">Settings</div>\n <div\n className=\"settings-popout__tabs\"\n role=\"tablist\"\n aria-label=\"Settings sections\"\n onKeyDown={onTabKey}\n >\n {tabs.map(t => (\n <button\n key={t.id}\n ref={el => {\n if (el) tabRefs.current.set(t.id, el)\n else tabRefs.current.delete(t.id)\n }}\n type=\"button\"\n role=\"tab\"\n id={tabDomId(t.id)}\n className=\"settings-popout__tab\"\n aria-selected={t.id === activeTab}\n aria-controls={panelDomId}\n tabIndex={t.id === activeTab ? 0 : -1}\n onClick={() => selectTab(t.id)}\n >\n {t.label}\n {t.dot && <span style={PASSKEY_DOT} aria-hidden=\"true\" />}\n </button>\n ))}\n </div>\n </div>\n\n <div\n className=\"settings-popout__tabpanel\"\n role=\"tabpanel\"\n id={panelDomId}\n aria-labelledby={tabDomId(activeTab)}\n >\n {activeTab === 'profile' && (\n <>\n {/* Access tier. While resolving, the state still says 'public' —\n rendering \"Guest\" there would be a wrong answer that corrects\n itself a moment later, which reads exactly like the lag this\n placeholder exists to remove. */}\n <section className=\"settings-popout__row\">\n <span className=\"settings-popout__label\">Access tier</span>\n {resolving ? (\n <span className=\"settings-popout__value\">…</span>\n ) : (\n <span className={`settings-popout__tier settings-popout__tier--${userType}`}>\n {TIER_LABEL[userType]}\n </span>\n )}\n </section>\n\n {/* Display name */}\n <section className=\"settings-popout__row\">\n <span className=\"settings-popout__label\">Display name</span>\n {resolving ? (\n <span className=\"settings-popout__value\">…</span>\n ) : editingName ? (\n <span className=\"settings-popout__inline\">\n <input\n className=\"settings-popout__input\"\n value={nameDraft}\n maxLength={64}\n autoFocus\n disabled={nameSaving}\n aria-invalid={nameError !== null}\n aria-describedby={nameError ? 'settings-popout-name-error' : undefined}\n onChange={e => {\n setNameDraft(e.target.value)\n setNameError(null)\n }}\n onKeyDown={e => {\n if (e.key === 'Enter') {\n saveName().catch(reportErr)\n }\n if (e.key === 'Escape') {\n // The edit's Escape, not the popout's (window listener).\n e.stopPropagation()\n setEditingName(false)\n setNameError(null)\n setNameDraft(displayName ?? '')\n }\n }}\n />\n <button\n type=\"button\"\n className=\"settings-popout__btn settings-popout__btn--primary\"\n disabled={nameSaving}\n onClick={() => {\n saveName().catch(reportErr)\n }}\n >\n {nameSaving ? '…' : 'Save'}\n </button>\n {nameError && (\n <span\n id=\"settings-popout-name-error\"\n className=\"settings-popout__error\"\n role=\"alert\"\n >\n {nameError}\n </span>\n )}\n </span>\n ) : (\n <button\n type=\"button\"\n className=\"settings-popout__value settings-popout__value--editable\"\n aria-label=\"Edit display name\"\n onClick={() => {\n setNameDraft(displayName ?? '')\n setEditingName(true)\n }}\n >\n {displayName || 'Unnamed'}\n </button>\n )}\n </section>\n\n {/* Avatar: picture and character, chosen in place (AvatarPicker).\n Collapsed until Change, so opening the gear costs no request and no\n page load reads the avatar. Hidden for public callers, like the\n content pill: an avatar needs a session. */}\n {!resolving && userType !== 'public' && (\n <section className=\"settings-popout__row settings-popout__row--stack\">\n <span className=\"settings-popout__inline\">\n <span className=\"settings-popout__label settings-popout__label--grow\">\n Avatar\n </span>\n <button\n type=\"button\"\n className=\"settings-popout__btn\"\n aria-expanded={avatarOpen}\n onClick={() => setAvatarOpen(v => !v)}\n >\n {avatarOpen ? 'Done' : 'Change'}\n </button>\n </span>\n {avatarOpen && <AvatarPicker />}\n </section>\n )}\n\n {/* Service access: only for someone the operator granted a\n service identity. Collapsed until Open, like Avatar. */}\n {!resolving && serviceGrants > 0 && (\n <section className=\"settings-popout__row settings-popout__row--stack\">\n <span className=\"settings-popout__inline\">\n <span className=\"settings-popout__label settings-popout__label--grow\">\n Service access\n </span>\n <button\n type=\"button\"\n className=\"settings-popout__btn\"\n aria-expanded={serviceOpen}\n onClick={() => setServiceOpen(v => !v)}\n >\n {serviceOpen ? 'Done' : 'Open'}\n </button>\n </span>\n {serviceOpen && (\n <React.Suspense fallback={<span className=\"settings-popout__hint\">…</span>}>\n <ServiceAccess />\n </React.Suspense>\n )}\n </section>\n )}\n </>\n )}\n\n {/* Content visibility level. Held back until the first-open fetch\n settles, so it lands in the same paint as the tier and name\n rather than a round trip behind them. `content` is fetched\n concurrently with whoami(), so by then it is already here. */}\n {activeTab === 'content' && (\n <section className=\"settings-popout__row settings-popout__row--stack\">\n <span className=\"settings-popout__label\">Content visibility</span>\n {content ? (\n <>\n <div\n className=\"settings-popout__pill\"\n role=\"radiogroup\"\n aria-label=\"Content visibility level\"\n >\n {Array.from({ length: content.maxLevel }, (_, i) => i + 1).map(lvl => (\n <button\n key={lvl}\n type=\"button\"\n role=\"radio\"\n aria-checked={lvl === content.level}\n aria-label={`Level ${lvl}`}\n className={`settings-popout__seg${\n lvl <= content.level ? ' settings-popout__seg--filled' : ''\n }`}\n disabled={levelSaving}\n onClick={() => {\n pickLevel(lvl).catch(reportErr)\n }}\n >\n {lvl}\n </button>\n ))}\n </div>\n <span className=\"settings-popout__hint\">\n Higher levels reveal more mature content across apps. Level 1 is\n safe-for-everyone.\n </span>\n </>\n ) : (\n // Not \"Loading…\" — the fetch has already settled by the time\n // this section renders at all, so a null here means it failed.\n <span className=\"settings-popout__hint\">Currently unavailable.</span>\n )}\n </section>\n )}\n\n {/* Switch key. Signs in inline through the SITE's sign-in client\n (the /auth page's own code path), never a post of ours. A real\n <form> with a username + current-password pair, so password\n managers offer and save the key. */}\n {activeTab === 'account' && (\n <>\n {keyFormOpen ? (\n <section className=\"settings-popout__row settings-popout__row--stack\">\n <form\n className=\"settings-popout__key-form\"\n onSubmit={e => {\n e.preventDefault()\n submitKey().catch(reportErr)\n }}\n >\n <input\n type=\"text\"\n name=\"username\"\n autoComplete=\"username\"\n value=\"hadoku\"\n readOnly\n hidden\n />\n <label className=\"settings-popout__label\" htmlFor=\"settings-popout-key\">\n Access key\n </label>\n <span className=\"settings-popout__inline\">\n <input\n id=\"settings-popout-key\"\n className=\"settings-popout__input\"\n type=\"password\"\n name=\"password\"\n autoComplete=\"current-password\"\n autoCapitalize=\"none\"\n spellCheck={false}\n autoFocus\n value={keyDraft}\n disabled={keyBusy}\n aria-invalid={keyError !== null}\n aria-describedby={keyError ? 'settings-popout-key-error' : undefined}\n onChange={e => setKeyDraft(e.target.value)}\n />\n <button\n type=\"submit\"\n className=\"settings-popout__btn settings-popout__btn--primary\"\n disabled={keyBusy}\n >\n {keyBusy ? 'Signing in…' : 'Sign in'}\n </button>\n </span>\n {passkeysSupported() && (\n <span className=\"settings-popout__inline\">\n <button\n type=\"button\"\n className=\"settings-popout__btn\"\n disabled={keyBusy}\n onClick={() => {\n signInWithPasskey().catch(reportErr)\n }}\n >\n {passkeyMode === 'signin' ? 'Waiting for passkey…' : 'Use a passkey'}\n </button>\n </span>\n )}\n {/* Cancel on a line of its own: in the row it would slide under\n the pointer, and a double click would cancel what it started. */}\n {passkeyMode === 'signin' && (\n <span className=\"settings-popout__inline\">\n <button\n type=\"button\"\n className=\"settings-popout__btn\"\n onClick={() => cancelPasskey()}\n >\n Cancel\n </button>\n </span>\n )}\n {keyError && (\n <span\n id=\"settings-popout-key-error\"\n className=\"settings-popout__error\"\n role=\"alert\"\n >\n {keyError}\n </span>\n )}\n </form>\n </section>\n ) : (\n <section className=\"settings-popout__row settings-popout__row--stack\">\n <span className=\"settings-popout__inline\">\n <span className=\"settings-popout__label settings-popout__label--grow\">\n Access key\n </span>\n {/* Signed-in people only: the site refuses to save one for anyone else. */}\n {!resolving && userType !== 'public' && passkeysSupported() && (\n <button\n type=\"button\"\n className={`settings-popout__btn${needsPasskey ? ` settings-popout__btn--primary ${PASSKEY_PULSE_CLASS}` : ''}`}\n disabled={passkeyMode !== null || passkeySaved}\n onClick={() => {\n savePasskey().catch(reportErr)\n }}\n >\n {passkeySaved\n ? 'Passkey saved'\n : passkeyMode === 'save'\n ? 'Saving…'\n : 'Save passkey'}\n </button>\n )}\n <button\n type=\"button\"\n className=\"settings-popout__btn\"\n disabled={passkeyMode === 'save'}\n onClick={openKeyForm}\n >\n Switch key\n </button>\n </span>\n {/* Its own line, so nothing moves under a double click. */}\n {passkeyMode === 'save' && (\n <span className=\"settings-popout__inline\">\n <button\n type=\"button\"\n className=\"settings-popout__btn\"\n onClick={() => cancelPasskey()}\n >\n Cancel\n </button>\n </span>\n )}\n {passkeyError && (\n <span className=\"settings-popout__error\" role=\"alert\">\n {passkeyError}\n </span>\n )}\n </section>\n )}\n\n {/* A self-made account (member) can delete itself. The confirm is a\n separate button on its own line, so a double click on Delete\n never confirms. */}\n {!resolving && userType === 'member' && (\n <section className=\"settings-popout__row settings-popout__row--stack\">\n <span className=\"settings-popout__inline\">\n <span className=\"settings-popout__label settings-popout__label--grow\">\n Account\n </span>\n {deleteStage === 'idle' ? (\n <button\n type=\"button\"\n className=\"settings-popout__btn\"\n onClick={() => {\n setDeleteError(null)\n setDeleteStage('confirm')\n }}\n >\n Delete account\n </button>\n ) : (\n <button\n type=\"button\"\n className=\"settings-popout__btn\"\n disabled={deleteStage === 'busy'}\n onClick={() => setDeleteStage('idle')}\n >\n Keep\n </button>\n )}\n </span>\n {deleteStage !== 'idle' && (\n <span className=\"settings-popout__inline\">\n <button\n type=\"button\"\n className=\"settings-popout__btn settings-popout__btn--primary\"\n disabled={deleteStage === 'busy'}\n onClick={() => {\n setDeleteStage('busy')\n deleteOwnAccount()\n .then(deleted => {\n if (!deleted) {\n setDeleteError('Account not deleted')\n setDeleteStage('confirm')\n return\n }\n localStorage.removeItem('hadoku_session_id')\n localStorage.removeItem('hadoku_user_type')\n window.location.assign('/')\n })\n .catch(reportErr)\n }}\n >\n {deleteStage === 'busy' ? 'Deleting…' : 'Delete for good'}\n </button>\n </span>\n )}\n {deleteError && (\n <span className=\"settings-popout__error\" role=\"alert\">\n {deleteError}\n </span>\n )}\n </section>\n )}\n </>\n )}\n\n {/* App-specific settings (optional) — same popout, one gear. */}\n {activeTab === 'app:' && (\n <section className=\"settings-popout__row settings-popout__row--stack\">\n {children}\n </section>\n )}\n {appTab && (\n <section className=\"settings-popout__row settings-popout__row--stack\">\n {appTab.render()}\n </section>\n )}\n </div>\n </div>\n )}\n </div>\n )\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"ConnectedSettings.js","sources":["../../src/components/ConnectedSettings.tsx"],"sourcesContent":["/**\n * ConnectedSettings — the unified per-user settings control for the hadoku\n * ecosystem: a gear button that opens a popout housing everything that is a\n * property of the PERSON (not the app):\n * - current access tier (read-only, friendly label)\n * - display name (inline edit → POST /session/name)\n * - avatar: picture and character, chosen in a dialog (AvatarDialog)\n * - global content-visibility level (tiered pill → PUT /prefs/api/v1/content-level)\n * - switch key (an inline sign-in form, on the site's own sign-in client)\n *\n * Those are grouped into tabs (Profile, Content, Account), followed by the\n * app's own: `children` as one \"App\" tab, `appTabs` as one tab each. The open\n * tab is remembered per user and per app (useSettingsTab, @wolffm/themes).\n *\n * \"Connected\" (parallel to ConnectedThemePicker): it self-wires to the\n * hadoku.me edge-router via same-origin fetches (see lib/settingsClient), so\n * dropping `<ConnectedSettings />` into any app under hadoku.me needs zero\n * props. Identity is resolved via whoami() unless the host passes `userType` /\n * `name` (avoids a redundant fetch when the app already knows).\n *\n * All of that is resolved AT MOUNT via `prefetchSettings()`, never on\n * gear-click — opening the popout must cost zero requests. See the effect\n * below, and `e2e/settings-prefetch.spec.ts` in hadoku-task, which fails if the\n * fetch moves back behind the click.\n *\n * Mirrors ThemePicker's controlled-popout pattern: owns its own `open` state +\n * a full-viewport overlay for click-outside dismiss. The content pill renders\n * `maxLevel` segments (friend ⇒ 3, admin ⇒ 4) and is hidden for public callers.\n * maxLevel comes from the server so the tier ceiling is never trusted from the\n * client.\n */\nimport React, { useCallback, useEffect, useId, useRef, useState } from 'react'\nimport { useSettingsTab } from '@wolffm/themes'\nimport { SettingsIcon } from './ThemeIcons'\nimport { AvatarDialog } from './AvatarDialog'\n\n// A grantee's own service credentials, with a QR encoder for TOTP enrolment:\n// its own chunk, fetched only when a grantee opens it.\nconst ServiceAccess = React.lazy(() =>\n import('./ServiceAccess').then(m => ({ default: m.ServiceAccess }))\n)\nimport {\n deleteOwnAccount,\n prefetchSettings,\n setContentLevel,\n setDisplayName,\n loadSignIn,\n loadSignInClient,\n passkeyHref,\n passkeysSupported,\n switchKeyHref,\n type ContentLevelState,\n type Tier\n} from '../lib/settingsClient'\nimport '../app-header.css'\n\nexport interface ConnectedSettingsProps {\n /** Caller's tier. Omit to self-resolve via whoami().\n *\n * This exists for apps that CANNOT reach `/session/whoami` — conjure sits\n * behind a path-prefixed shim that serves nothing outside `/conjure`, so\n * whoami 404s and the tier would fall back to guest. It is an identity\n * hint, not a way to present a different tier than the edge assigns: every\n * gate that matters is enforced server-side off the edge-stamped tier. */\n userType?: Tier\n /** Display name. Omit to self-resolve via whoami(). Same rationale. */\n name?: string | null\n /** Notify the host (e.g. a \"Signed in as …\" label) of a rename. */\n onNameChange?: (name: string | null) => void\n /** App-specific settings as ONE tab, labelled \"App\", after the site's own\n * tabs — so an app with its own preferences keeps one unified gear instead\n * of a second control. For more than one section, use `appTabs`.\n *\n * This slot is deliberately unconstrained — put whatever the app needs in\n * it. The constraint is only that it cannot change the site's tabs. */\n children?: React.ReactNode\n /** App-specific settings as tabs of their own, after `children`'s \"App\" tab\n * if both are given. `id` is stable and unique within the app: the last-open\n * tab is remembered by it. `render` runs only while its tab is open. */\n appTabs?: AppSettingsTab[]\n}\n\n/** One app-contributed tab in the settings popout. */\nexport interface AppSettingsTab {\n id: string\n label: string\n render: () => React.ReactNode\n}\n\n/** Rejects once `signal` aborts, so a call that ignores the signal (an older\n * site client, a prompt that never settles) still lets the UI go. */\nfunction untilAborted(signal: AbortSignal): Promise<never> {\n return new Promise((_, reject) => {\n const fail = () => reject(new Error('cancelled'))\n if (signal.aborted) fail()\n else signal.addEventListener('abort', fail, { once: true })\n })\n}\n\n/** Abort reason when the popout closes: a dismissal, not an error to show. */\nconst CLOSED = 'closed'\n\n/** Rejection sink for fire-and-forget settings actions. The client helpers\n * already resolve to null on failure (state reflects it), so this only trips\n * on truly unexpected throws — log rather than swallow silently. */\nfunction reportErr(err: unknown) {\n // task-ui-components v2 dropped its logger; keep the package dependency-free.\n console.error('[ConnectedSettings] action failed', (err as Error)?.message ?? err)\n}\n\n/** Inline, not in app-header.css: every app runs this component from the\n * site's shared vendor module, but compiles app-header.css into its own\n * stylesheet at ITS build, so a new rule reaches an app only when it rebuilds. */\nconst PASSKEY_DOT: React.CSSProperties = {\n position: 'absolute',\n top: 2,\n right: 2,\n width: 8,\n height: 8,\n borderRadius: '50%',\n background: 'var(--color-primary)',\n pointerEvents: 'none'\n}\n\n/** Save passkey pulses while the dot is asking for it. Keyframes cannot be\n * inline, so the rule is injected once into the document instead, for the\n * same reason PASSKEY_DOT is inline. */\nconst PASSKEY_PULSE_CLASS = 'settings-popout__btn--pulse'\nconst PASSKEY_PULSE_CSS = `\n@keyframes settings-passkey-pulse {\n 0% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--color-primary) 70%, transparent); }\n 70% { box-shadow: 0 0 0 8px color-mix(in srgb, var(--color-primary) 0%, transparent); }\n 100% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--color-primary) 0%, transparent); }\n}\n@media (prefers-reduced-motion: no-preference) {\n .${PASSKEY_PULSE_CLASS}:not(:disabled) {\n animation: settings-passkey-pulse 1.6s ease-out infinite;\n }\n}\n`\n\n/** The tab strip and its panels. Injected for the same reason as the pulse:\n * an app's copy of app-header.css predates the tabs until it rebuilds. The\n * strip stays pinned while a long tab scrolls, and scrolls sideways itself\n * when the tabs outgrow a phone-width panel. */\nconst TABS_CSS = `\n.settings-popout__top {\n position: sticky;\n top: -16px;\n z-index: 1;\n display: flex;\n flex-direction: column;\n gap: 10px;\n margin: -16px -16px 0;\n padding: 16px 16px 0;\n background: var(--color-bg-card);\n border-bottom: 1px solid var(--color-border);\n}\n.settings-popout__tabs {\n display: flex;\n gap: 2px;\n margin: 0 -16px -1px;\n padding: 0 16px;\n overflow-x: auto;\n scrollbar-width: thin;\n}\n.settings-popout__tab {\n position: relative;\n flex: none;\n padding: 6px 10px;\n border: 0;\n border-bottom: 2px solid transparent;\n background: transparent;\n color: var(--color-text-secondary);\n font: inherit;\n font-size: 0.8rem;\n font-weight: 600;\n white-space: nowrap;\n cursor: pointer;\n}\n.settings-popout__tab:hover {\n color: var(--color-text);\n}\n.settings-popout__tab[aria-selected='true'] {\n color: var(--color-text);\n border-bottom-color: var(--color-primary);\n}\n.settings-popout__tab:focus-visible {\n outline: 2px solid var(--color-primary);\n outline-offset: -2px;\n border-radius: 6px 6px 0 0;\n}\n.settings-popout__tabpanel {\n display: flex;\n flex-direction: column;\n gap: 14px;\n}\n`\n\nfunction useInjectedStyle(id: string, css: string, active: boolean) {\n useEffect(() => {\n if (!active || document.getElementById(id)) return\n const el = document.createElement('style')\n el.id = id\n el.textContent = css\n document.head.appendChild(el)\n }, [id, css, active])\n}\n\nconst TIER_LABEL: Record<Tier, string> = {\n admin: 'Administrator',\n wife: 'Wife',\n service: 'Service',\n friend: 'Friend',\n member: 'Member',\n public: 'Guest'\n}\n\nexport function ConnectedSettings({\n userType: userTypeProp,\n name: nameProp,\n onNameChange,\n children,\n appTabs\n}: ConnectedSettingsProps) {\n const [open, setOpen] = useState(false)\n const rootRef = useRef<HTMLDivElement>(null)\n const tabIdBase = useId()\n const tabRefs = useRef(new Map<string, HTMLButtonElement>())\n const [savedTab, saveTab] = useSettingsTab()\n // This visit's pick; wins over the saved one while it is still offered.\n const [pickedTab, setPickedTab] = useState<string | null>(null)\n\n // Identity — from props if supplied, else resolved lazily via whoami().\n const [userType, setUserType] = useState<Tier>(userTypeProp ?? 'public')\n const [resolvedName, setResolvedName] = useState<string | null>(nameProp ?? null)\n\n // Name editing\n const [nameDraft, setNameDraft] = useState(resolvedName ?? '')\n const [editingName, setEditingName] = useState(false)\n const [nameSaving, setNameSaving] = useState(false)\n const [nameError, setNameError] = useState<string | null>(null)\n const [displayName, setLocalName] = useState<string | null>(resolvedName)\n\n // Key swap. The draft lives only in this state and the input: it is never\n // logged, stored or sent anywhere but the site's sign-in client.\n const [keyFormOpen, setKeyFormOpen] = useState(false)\n const [keyDraft, setKeyDraft] = useState('')\n const [keyBusy, setKeyBusy] = useState(false)\n const [keyError, setKeyError] = useState<string | null>(null)\n\n // Passkeys, through the same site client: save one while signed in, or\n // sign in with one from the key form. One prompt at a time, and always\n // cancellable: a password manager can dismiss the browser's prompt without\n // ever answering it, so Cancel, closing the popout and the site client's own\n // time limit each end the wait.\n const [passkeyMode, setPasskeyMode] = useState<'save' | 'signin' | null>(null)\n const [passkeySaved, setPasskeySaved] = useState(false)\n // How many passkeys sign this person in, from whoami; null = unknown.\n const [passkeyCount, setPasskeyCount] = useState<number | null>(null)\n const [passkeyError, setPasskeyError] = useState<string | null>(null)\n // Delete account: one click arms it, a second, separate button confirms.\n const [deleteStage, setDeleteStage] = useState<'idle' | 'confirm' | 'busy'>('idle')\n const [deleteError, setDeleteError] = useState<string | null>(null)\n const passkeyCtl = useRef<AbortController | null>(null)\n const cancelPasskey = useCallback((reason?: string) => passkeyCtl.current?.abort(reason), [])\n\n // Avatar: the dialog's picker loads only when opened, never at mount (AvatarPicker).\n const [avatarOpen, setAvatarOpen] = useState(false)\n const avatarBtn = useRef<HTMLButtonElement>(null)\n // Service identities this person may mint for (whoami.serviceGrants).\n const [serviceGrants, setServiceGrants] = useState(0)\n const [serviceOpen, setServiceOpen] = useState(false)\n\n // Content level\n const [content, setContent] = useState<ContentLevelState | null>(null)\n const [levelSaving, setLevelSaving] = useState(false)\n\n // One flag for the whole prefetch, not one per request — the panel body\n // reveals when everything has settled, so nothing pops in a beat late. In\n // practice the user never sees it: the prefetch starts at mount and is long\n // done by the time the gear is clicked. It only shows if they click within\n // the first moments of the page.\n const [resolving, setResolving] = useState(true)\n\n // Everyone who is SIGNED IN, expressed as \"not public\" rather than as a list\n // of tiers. The list form (`=== 'admin' || === 'friend' || === 'service'`)\n // is an exact-match allowlist: it silently excluded `wife` the moment that\n // tier existed, which is the failure the rank model is meant to prevent.\n // There is no rank helper in this bundle, but \"above public\" needs none.\n const showContentPill = userType !== 'public'\n\n // Signed in on a key with no passkey: the gear carries a dot and Save passkey\n // is the primary action until one is saved. Unknown counts never prompt.\n const needsPasskey =\n !resolving &&\n userType !== 'public' &&\n passkeyCount === 0 &&\n !passkeySaved &&\n passkeysSupported()\n useInjectedStyle('settings-passkey-pulse', PASSKEY_PULSE_CSS, needsPasskey)\n useInjectedStyle('settings-tabs', TABS_CSS, open)\n\n // The site's own groups first, then the app's. App ids are namespaced so an\n // app tab called \"account\" can never collide with the site's.\n const tabs: { id: string; label: string; dot?: boolean }[] = [\n { id: 'profile', label: 'Profile' },\n ...(!resolving && showContentPill ? [{ id: 'content', label: 'Content' }] : []),\n // The gear's dot asks for a passkey; this one says where Save passkey is.\n { id: 'account', label: 'Account', dot: needsPasskey },\n ...(children ? [{ id: 'app:', label: 'App' }] : []),\n ...(appTabs ?? []).map(t => ({ id: `app:${t.id}`, label: t.label }))\n ]\n const tabIds = tabs.map(t => t.id)\n // Nothing remembered: open on the app's first tab, which is usually why the\n // gear was clicked inside an app.\n const defaultTab = tabIds.find(id => id.startsWith('app:')) ?? 'profile'\n const activeTab =\n [pickedTab, savedTab].find((id): id is string => id !== null && tabIds.includes(id)) ??\n defaultTab\n const appTab = appTabs?.find(t => `app:${t.id}` === activeTab)\n const tabDomId = (id: string) => `${tabIdBase}tab-${id}`\n const panelDomId = `${tabIdBase}panel`\n\n const selectTab = useCallback(\n (id: string) => {\n setPickedTab(id)\n saveTab(id)\n },\n [saveTab]\n )\n\n // Arrow keys, Home and End move between tabs and open the one they land on\n // (the ARIA tablist pattern with automatic activation).\n const onTabKey = (e: React.KeyboardEvent) => {\n const i = tabIds.indexOf(activeTab)\n const n = tabIds.length\n const next =\n e.key === 'ArrowRight'\n ? tabIds[(i + 1) % n]\n : e.key === 'ArrowLeft'\n ? tabIds[(i - 1 + n) % n]\n : e.key === 'Home'\n ? tabIds[0]\n : e.key === 'End'\n ? tabIds[n - 1]\n : undefined\n if (next === undefined) return\n e.preventDefault()\n selectTab(next)\n tabRefs.current.get(next)?.focus()\n }\n\n // A remembered tab past the edge of a narrow strip is scrolled into sight.\n // jsdom implements no scrollIntoView, and every consumer's unit test that\n // renders the popout runs there, so the call is optional.\n useEffect(() => {\n if (open)\n tabRefs.current.get(activeTab)?.scrollIntoView?.({ block: 'nearest', inline: 'nearest' })\n }, [open, activeTab])\n\n // Keep local identity in sync when the host controls it via props.\n useEffect(() => {\n if (userTypeProp !== undefined) setUserType(userTypeProp)\n }, [userTypeProp])\n useEffect(() => {\n if (nameProp !== undefined) {\n setResolvedName(nameProp)\n setLocalName(nameProp)\n setNameDraft(nameProp ?? '')\n }\n }, [nameProp])\n\n // Resolve everything the panel shows AT MOUNT — not on open.\n //\n // This used to be gated on `open`, which made the gear the trigger for\n // whoami + content-level (167ms / 186ms on prod, concurrent) — a round trip\n // the user waited out with the panel already on screen and blank. Nothing\n // about that data depends on the click:\n // it's the signed-in user's own identity, settled at boot and unchanged for\n // the life of the page. So it rides along with the app's boot traffic, and\n // clicking the gear costs zero requests. `e2e/settings-prefetch.spec.ts` in\n // hadoku-task pins that.\n //\n // The client memoises across callers, so mounting this in several places (or\n // remounting it) does not multiply requests, and on hadoku.me the identity\n // half is free — it awaits the whoami the shell already had in flight.\n useEffect(() => {\n let live = true\n prefetchSettings({ userType: userTypeProp, name: nameProp })\n .then(({ identity, content: state, passkeys, serviceGrants: grants }) => {\n if (!live) return\n setPasskeyCount(passkeys)\n setServiceGrants(grants)\n // Props stay authoritative: the host passed them because it can see an\n // identity this component can't resolve for itself.\n if (userTypeProp === undefined) {\n setUserType(identity.userType)\n setResolvedName(identity.name)\n setLocalName(identity.name)\n setNameDraft(identity.name ?? '')\n }\n if (state) setContent(state)\n })\n .catch(reportErr)\n .finally(() => {\n if (live) setResolving(false)\n })\n return () => {\n live = false\n }\n // Hints are read once, on the first mount that gets there — the prefetch is\n // page-scoped and a later prop change cannot un-issue it. Identity changes\n // arrive through the sync effects above, and a key swap reloads the page.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [])\n\n // Escape closes — unless the avatar dialog is open: that Escape is the dialog's.\n useEffect(() => {\n if (!open || avatarOpen) return\n const onKey = (e: KeyboardEvent) => {\n if (e.key === 'Escape') setOpen(false)\n }\n window.addEventListener('keydown', onKey)\n return () => window.removeEventListener('keydown', onKey)\n }, [open, avatarOpen])\n\n const saveName = useCallback(async () => {\n const trimmed = nameDraft.trim()\n if (!trimmed || trimmed === displayName) {\n setEditingName(false)\n return\n }\n setNameSaving(true)\n const result = await setDisplayName(trimmed)\n setNameSaving(false)\n // A refusal keeps the editor open on the draft, saying why.\n if (!result.ok) {\n setNameError(result.error)\n return\n }\n setNameError(null)\n setLocalName(result.name)\n setNameDraft(result.name ?? '')\n if (result.name !== null) onNameChange?.(result.name)\n setEditingName(false)\n }, [nameDraft, displayName, onNameChange])\n\n // Open the form AND start loading the site's sign-in client, which begins its\n // fingerprint collection as it evaluates — so it is ready by submit. Where the\n // client cannot load at all (not on hadoku.me), the page-level /auth flow is\n // the only way to sign in, so go there.\n const openKeyForm = useCallback(() => {\n setKeyFormOpen(true)\n setKeyError(null)\n loadSignIn()\n .then(signIn => {\n if (!signIn) window.location.assign(switchKeyHref())\n })\n .catch(reportErr)\n }, [])\n\n const submitKey = useCallback(async () => {\n setKeyBusy(true)\n setKeyError(null)\n const signIn = await loadSignIn()\n if (!signIn) {\n window.location.assign(switchKeyHref())\n return\n }\n try {\n await signIn(keyDraft)\n } catch (err) {\n // A refused key is an inline error, never a redirect.\n setKeyError((err as Error)?.message || 'Invalid key')\n setKeyBusy(false)\n return\n }\n // The page now acts for a different identity: everything must re-mount.\n // Busy stays on through the reload so the form can't be resubmitted.\n window.location.reload()\n }, [keyDraft])\n\n const savePasskey = useCallback(async () => {\n const ctl = new AbortController()\n passkeyCtl.current = ctl\n setPasskeyMode('save')\n setPasskeyError(null)\n try {\n const client = await Promise.race([loadSignInClient(), untilAborted(ctl.signal)])\n if (!client?.addPasskey) {\n window.location.assign(passkeyHref('add'))\n return\n }\n await Promise.race([client.addPasskey({ signal: ctl.signal }), untilAborted(ctl.signal)])\n setPasskeySaved(true)\n } catch (err) {\n if (ctl.signal.reason !== CLOSED) {\n setPasskeyError(\n ctl.signal.aborted ? 'Passkey not saved' : (err as Error)?.message || 'Passkey not saved'\n )\n }\n } finally {\n if (passkeyCtl.current === ctl) passkeyCtl.current = null\n setPasskeyMode(null)\n }\n }, [])\n\n const signInWithPasskey = useCallback(async () => {\n const ctl = new AbortController()\n passkeyCtl.current = ctl\n setPasskeyMode('signin')\n setKeyBusy(true)\n setKeyError(null)\n let leaving = false\n try {\n const client = await Promise.race([loadSignInClient(), untilAborted(ctl.signal)])\n if (!client?.signInWithPasskey) {\n leaving = true\n window.location.assign(passkeyHref('signin'))\n return\n }\n await Promise.race([\n client.signInWithPasskey({ signal: ctl.signal }),\n untilAborted(ctl.signal)\n ])\n // The page now acts for a different identity: everything must re-mount.\n // Busy stays on through the reload so nothing can be resubmitted.\n leaving = true\n window.location.reload()\n } catch (err) {\n if (ctl.signal.reason !== CLOSED) {\n setKeyError(\n ctl.signal.aborted\n ? 'Passkey sign-in cancelled'\n : (err as Error)?.message || 'Passkey sign-in cancelled'\n )\n }\n } finally {\n if (passkeyCtl.current === ctl) passkeyCtl.current = null\n if (!leaving) {\n setPasskeyMode(null)\n setKeyBusy(false)\n }\n }\n }, [])\n\n // Closing the popout (or leaving the page) abandons an open prompt.\n useEffect(() => {\n if (!open) cancelPasskey(CLOSED)\n }, [open, cancelPasskey])\n useEffect(() => () => cancelPasskey(CLOSED), [cancelPasskey])\n\n const pickLevel = useCallback(\n async (level: number) => {\n if (!content || level === content.level || levelSaving) return\n const prev = content\n setContent({ ...content, level }) // optimistic\n setLevelSaving(true)\n const result = await setContentLevel(level)\n setLevelSaving(false)\n setContent(result ?? prev) // adopt server truth, or revert\n },\n [content, levelSaving]\n )\n\n // No caller-supplied class on the root. The four rows below are the\n // platform's, not the app's, and a className here is enough to restyle or\n // hide any of them — which is the drift this component exists to prevent.\n // Apps extend via `children` and `appTabs`, never by reaching into this markup.\n return (\n <div className=\"settings-popout\" ref={rootRef}>\n <button\n type=\"button\"\n className=\"app-header__icon-btn settings-toggle-btn\"\n aria-label={needsPasskey ? 'User settings, no passkey saved' : 'User settings'}\n style={needsPasskey ? { position: 'relative' } : undefined}\n aria-expanded={open}\n onClick={() => setOpen(v => !v)}\n >\n <SettingsIcon />\n {needsPasskey && (\n <span className=\"settings-toggle-btn__dot\" style={PASSKEY_DOT} aria-hidden=\"true\" />\n )}\n </button>\n\n {open && <div className=\"settings-popout__overlay\" onClick={() => setOpen(false)} />}\n\n {open && (\n <div className=\"settings-popout__panel\" role=\"dialog\" aria-label=\"User settings\">\n <div className=\"settings-popout__top\">\n <div className=\"settings-popout__header\">Settings</div>\n <div\n className=\"settings-popout__tabs\"\n role=\"tablist\"\n aria-label=\"Settings sections\"\n onKeyDown={onTabKey}\n >\n {tabs.map(t => (\n <button\n key={t.id}\n ref={el => {\n if (el) tabRefs.current.set(t.id, el)\n else tabRefs.current.delete(t.id)\n }}\n type=\"button\"\n role=\"tab\"\n id={tabDomId(t.id)}\n className=\"settings-popout__tab\"\n aria-selected={t.id === activeTab}\n aria-controls={panelDomId}\n tabIndex={t.id === activeTab ? 0 : -1}\n onClick={() => selectTab(t.id)}\n >\n {t.label}\n {t.dot && <span style={PASSKEY_DOT} aria-hidden=\"true\" />}\n </button>\n ))}\n </div>\n </div>\n\n <div\n className=\"settings-popout__tabpanel\"\n role=\"tabpanel\"\n id={panelDomId}\n aria-labelledby={tabDomId(activeTab)}\n >\n {activeTab === 'profile' && (\n <>\n {/* Access tier. While resolving, the state still says 'public' —\n rendering \"Guest\" there would be a wrong answer that corrects\n itself a moment later, which reads exactly like the lag this\n placeholder exists to remove. */}\n <section className=\"settings-popout__row\">\n <span className=\"settings-popout__label\">Access tier</span>\n {resolving ? (\n <span className=\"settings-popout__value\">…</span>\n ) : (\n <span className={`settings-popout__tier settings-popout__tier--${userType}`}>\n {TIER_LABEL[userType]}\n </span>\n )}\n </section>\n\n {/* Display name */}\n <section className=\"settings-popout__row\">\n <span className=\"settings-popout__label\">Display name</span>\n {resolving ? (\n <span className=\"settings-popout__value\">…</span>\n ) : editingName ? (\n <span className=\"settings-popout__inline\">\n <input\n className=\"settings-popout__input\"\n value={nameDraft}\n maxLength={64}\n autoFocus\n disabled={nameSaving}\n aria-invalid={nameError !== null}\n aria-describedby={nameError ? 'settings-popout-name-error' : undefined}\n onChange={e => {\n setNameDraft(e.target.value)\n setNameError(null)\n }}\n onKeyDown={e => {\n if (e.key === 'Enter') {\n saveName().catch(reportErr)\n }\n if (e.key === 'Escape') {\n // The edit's Escape, not the popout's (window listener).\n e.stopPropagation()\n setEditingName(false)\n setNameError(null)\n setNameDraft(displayName ?? '')\n }\n }}\n />\n <button\n type=\"button\"\n className=\"settings-popout__btn settings-popout__btn--primary\"\n disabled={nameSaving}\n onClick={() => {\n saveName().catch(reportErr)\n }}\n >\n {nameSaving ? '…' : 'Save'}\n </button>\n {nameError && (\n <span\n id=\"settings-popout-name-error\"\n className=\"settings-popout__error\"\n role=\"alert\"\n >\n {nameError}\n </span>\n )}\n </span>\n ) : (\n <button\n type=\"button\"\n className=\"settings-popout__value settings-popout__value--editable\"\n aria-label=\"Edit display name\"\n onClick={() => {\n setNameDraft(displayName ?? '')\n setEditingName(true)\n }}\n >\n {displayName || 'Unnamed'}\n </button>\n )}\n </section>\n\n {/* Avatar: picture and character, chosen in a dialog (AvatarDialog).\n Nothing loads until Change, so opening the gear costs no request and\n no page load reads the avatar. Hidden for public callers, like the\n content pill: an avatar needs a session. */}\n {!resolving && userType !== 'public' && (\n <section className=\"settings-popout__row settings-popout__row--stack\">\n <span className=\"settings-popout__inline\">\n <span className=\"settings-popout__label settings-popout__label--grow\">\n Avatar\n </span>\n <button\n ref={avatarBtn}\n type=\"button\"\n className=\"settings-popout__btn\"\n aria-haspopup=\"dialog\"\n aria-expanded={avatarOpen}\n onClick={() => setAvatarOpen(true)}\n >\n Change\n </button>\n </span>\n <AvatarDialog\n open={avatarOpen}\n onClose={() => setAvatarOpen(false)}\n returnFocus={avatarBtn}\n />\n </section>\n )}\n\n {/* Service access: only for someone the operator granted a\n service identity. Collapsed until Open. */}\n {!resolving && serviceGrants > 0 && (\n <section className=\"settings-popout__row settings-popout__row--stack\">\n <span className=\"settings-popout__inline\">\n <span className=\"settings-popout__label settings-popout__label--grow\">\n Service access\n </span>\n <button\n type=\"button\"\n className=\"settings-popout__btn\"\n aria-expanded={serviceOpen}\n onClick={() => setServiceOpen(v => !v)}\n >\n {serviceOpen ? 'Done' : 'Open'}\n </button>\n </span>\n {serviceOpen && (\n <React.Suspense fallback={<span className=\"settings-popout__hint\">…</span>}>\n <ServiceAccess />\n </React.Suspense>\n )}\n </section>\n )}\n </>\n )}\n\n {/* Content visibility level. Held back until the first-open fetch\n settles, so it lands in the same paint as the tier and name\n rather than a round trip behind them. `content` is fetched\n concurrently with whoami(), so by then it is already here. */}\n {activeTab === 'content' && (\n <section className=\"settings-popout__row settings-popout__row--stack\">\n <span className=\"settings-popout__label\">Content visibility</span>\n {content ? (\n <>\n <div\n className=\"settings-popout__pill\"\n role=\"radiogroup\"\n aria-label=\"Content visibility level\"\n >\n {Array.from({ length: content.maxLevel }, (_, i) => i + 1).map(lvl => (\n <button\n key={lvl}\n type=\"button\"\n role=\"radio\"\n aria-checked={lvl === content.level}\n aria-label={`Level ${lvl}`}\n className={`settings-popout__seg${\n lvl <= content.level ? ' settings-popout__seg--filled' : ''\n }`}\n disabled={levelSaving}\n onClick={() => {\n pickLevel(lvl).catch(reportErr)\n }}\n >\n {lvl}\n </button>\n ))}\n </div>\n <span className=\"settings-popout__hint\">\n Higher levels reveal more mature content across apps. Level 1 is\n safe-for-everyone.\n </span>\n </>\n ) : (\n // Not \"Loading…\" — the fetch has already settled by the time\n // this section renders at all, so a null here means it failed.\n <span className=\"settings-popout__hint\">Currently unavailable.</span>\n )}\n </section>\n )}\n\n {/* Switch key. Signs in inline through the SITE's sign-in client\n (the /auth page's own code path), never a post of ours. A real\n <form> with a username + current-password pair, so password\n managers offer and save the key. */}\n {activeTab === 'account' && (\n <>\n {keyFormOpen ? (\n <section className=\"settings-popout__row settings-popout__row--stack\">\n <form\n className=\"settings-popout__key-form\"\n onSubmit={e => {\n e.preventDefault()\n submitKey().catch(reportErr)\n }}\n >\n <input\n type=\"text\"\n name=\"username\"\n autoComplete=\"username\"\n value=\"hadoku\"\n readOnly\n hidden\n />\n <label className=\"settings-popout__label\" htmlFor=\"settings-popout-key\">\n Access key\n </label>\n <span className=\"settings-popout__inline\">\n <input\n id=\"settings-popout-key\"\n className=\"settings-popout__input\"\n type=\"password\"\n name=\"password\"\n autoComplete=\"current-password\"\n autoCapitalize=\"none\"\n spellCheck={false}\n autoFocus\n value={keyDraft}\n disabled={keyBusy}\n aria-invalid={keyError !== null}\n aria-describedby={keyError ? 'settings-popout-key-error' : undefined}\n onChange={e => setKeyDraft(e.target.value)}\n />\n <button\n type=\"submit\"\n className=\"settings-popout__btn settings-popout__btn--primary\"\n disabled={keyBusy}\n >\n {keyBusy ? 'Signing in…' : 'Sign in'}\n </button>\n </span>\n {passkeysSupported() && (\n <span className=\"settings-popout__inline\">\n <button\n type=\"button\"\n className=\"settings-popout__btn\"\n disabled={keyBusy}\n onClick={() => {\n signInWithPasskey().catch(reportErr)\n }}\n >\n {passkeyMode === 'signin' ? 'Waiting for passkey…' : 'Use a passkey'}\n </button>\n </span>\n )}\n {/* Cancel on a line of its own: in the row it would slide under\n the pointer, and a double click would cancel what it started. */}\n {passkeyMode === 'signin' && (\n <span className=\"settings-popout__inline\">\n <button\n type=\"button\"\n className=\"settings-popout__btn\"\n onClick={() => cancelPasskey()}\n >\n Cancel\n </button>\n </span>\n )}\n {keyError && (\n <span\n id=\"settings-popout-key-error\"\n className=\"settings-popout__error\"\n role=\"alert\"\n >\n {keyError}\n </span>\n )}\n </form>\n </section>\n ) : (\n <section className=\"settings-popout__row settings-popout__row--stack\">\n <span className=\"settings-popout__inline\">\n <span className=\"settings-popout__label settings-popout__label--grow\">\n Access key\n </span>\n {/* Signed-in people only: the site refuses to save one for anyone else. */}\n {!resolving && userType !== 'public' && passkeysSupported() && (\n <button\n type=\"button\"\n className={`settings-popout__btn${needsPasskey ? ` settings-popout__btn--primary ${PASSKEY_PULSE_CLASS}` : ''}`}\n disabled={passkeyMode !== null || passkeySaved}\n onClick={() => {\n savePasskey().catch(reportErr)\n }}\n >\n {passkeySaved\n ? 'Passkey saved'\n : passkeyMode === 'save'\n ? 'Saving…'\n : 'Save passkey'}\n </button>\n )}\n <button\n type=\"button\"\n className=\"settings-popout__btn\"\n disabled={passkeyMode === 'save'}\n onClick={openKeyForm}\n >\n Switch key\n </button>\n </span>\n {/* Its own line, so nothing moves under a double click. */}\n {passkeyMode === 'save' && (\n <span className=\"settings-popout__inline\">\n <button\n type=\"button\"\n className=\"settings-popout__btn\"\n onClick={() => cancelPasskey()}\n >\n Cancel\n </button>\n </span>\n )}\n {passkeyError && (\n <span className=\"settings-popout__error\" role=\"alert\">\n {passkeyError}\n </span>\n )}\n </section>\n )}\n\n {/* A self-made account (member) can delete itself. The confirm is a\n separate button on its own line, so a double click on Delete\n never confirms. */}\n {!resolving && userType === 'member' && (\n <section className=\"settings-popout__row settings-popout__row--stack\">\n <span className=\"settings-popout__inline\">\n <span className=\"settings-popout__label settings-popout__label--grow\">\n Account\n </span>\n {deleteStage === 'idle' ? (\n <button\n type=\"button\"\n className=\"settings-popout__btn\"\n onClick={() => {\n setDeleteError(null)\n setDeleteStage('confirm')\n }}\n >\n Delete account\n </button>\n ) : (\n <button\n type=\"button\"\n className=\"settings-popout__btn\"\n disabled={deleteStage === 'busy'}\n onClick={() => setDeleteStage('idle')}\n >\n Keep\n </button>\n )}\n </span>\n {deleteStage !== 'idle' && (\n <span className=\"settings-popout__inline\">\n <button\n type=\"button\"\n className=\"settings-popout__btn settings-popout__btn--primary\"\n disabled={deleteStage === 'busy'}\n onClick={() => {\n setDeleteStage('busy')\n deleteOwnAccount()\n .then(deleted => {\n if (!deleted) {\n setDeleteError('Account not deleted')\n setDeleteStage('confirm')\n return\n }\n localStorage.removeItem('hadoku_session_id')\n localStorage.removeItem('hadoku_user_type')\n window.location.assign('/')\n })\n .catch(reportErr)\n }}\n >\n {deleteStage === 'busy' ? 'Deleting…' : 'Delete for good'}\n </button>\n </span>\n )}\n {deleteError && (\n <span className=\"settings-popout__error\" role=\"alert\">\n {deleteError}\n </span>\n )}\n </section>\n )}\n </>\n )}\n\n {/* App-specific settings (optional) — same popout, one gear. */}\n {activeTab === 'app:' && (\n <section className=\"settings-popout__row settings-popout__row--stack\">\n {children}\n </section>\n )}\n {appTab && (\n <section className=\"settings-popout__row settings-popout__row--stack\">\n {appTab.render()}\n </section>\n )}\n </div>\n </div>\n )}\n </div>\n )\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.10_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.11_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,7 @@
1
1
  {
2
2
  "name": "@hadoku/task-ui-components",
3
- "version": "4.23.3",
3
+ "version": "4.23.5",
4
+ "gitHead": "04c01e5cf2093ec110663ad47d946518304c4b5b",
4
5
  "description": "Reusable UI components with Bento grid and metallic gradient support",
5
6
  "type": "module",
6
7
  "main": "dist/index.js",
@@ -0,0 +1,116 @@
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.
5
+ *
6
+ * Rendered into document.body, so no ancestor of the popout (a transform, an
7
+ * overflow, a stacking context) can clip or trap it. The popout stays open
8
+ * underneath: closing the dialog returns to it, and focus goes back to Change.
9
+ *
10
+ * Styles are inline for the reason AvatarPicker's are: apps compile this
11
+ * package's CSS at their own build, so a new stylesheet rule reaches an app only
12
+ * when it rebuilds. Tokens only.
13
+ */
14
+ import React, { useEffect, useId, useRef } from 'react'
15
+ import { createPortal } from 'react-dom'
16
+ import { AvatarPicker } from './AvatarPicker'
17
+
18
+ const S: Record<string, React.CSSProperties> = {
19
+ overlay: {
20
+ position: 'fixed',
21
+ inset: 0,
22
+ // Above the popout's panel (100) and its click-catcher (99).
23
+ zIndex: 1000,
24
+ display: 'flex',
25
+ alignItems: 'flex-start',
26
+ justifyContent: 'center',
27
+ boxSizing: 'border-box',
28
+ padding: '6vh 16px',
29
+ background: 'var(--color-bg-overlay)'
30
+ },
31
+ card: {
32
+ boxSizing: 'border-box',
33
+ width: 'min(720px, 100%)',
34
+ maxHeight: '88vh',
35
+ overflowY: 'auto',
36
+ display: 'flex',
37
+ flexDirection: 'column',
38
+ gap: 14,
39
+ padding: 20,
40
+ borderRadius: 12,
41
+ background: 'var(--color-bg-card)',
42
+ border: '1px solid var(--color-border)',
43
+ boxShadow: '0 8px 28px rgba(0, 0, 0, 0.18)',
44
+ color: 'var(--color-text)',
45
+ // Outside the app's root, so nothing passes the app's font down to it.
46
+ fontFamily: 'var(--font-family)',
47
+ outline: 'none'
48
+ },
49
+ head: { display: 'flex', alignItems: 'center', gap: 10 },
50
+ title: { margin: 0, flex: 1, fontSize: '1.05rem', fontWeight: 600 }
51
+ }
52
+
53
+ export interface AvatarDialogProps {
54
+ open: boolean
55
+ onClose: () => void
56
+ /** Where focus goes when the dialog closes (the Change button). */
57
+ returnFocus?: React.RefObject<HTMLElement | null>
58
+ }
59
+
60
+ export function AvatarDialog({ open, onClose, returnFocus }: AvatarDialogProps) {
61
+ const titleId = useId()
62
+ const cardRef = useRef<HTMLDivElement>(null)
63
+ const onCloseRef = useRef(onClose)
64
+ onCloseRef.current = onClose
65
+
66
+ useEffect(() => {
67
+ if (!open) return
68
+ cardRef.current?.focus()
69
+ const onKey = (e: KeyboardEvent) => {
70
+ if (e.key !== 'Escape') return
71
+ e.preventDefault()
72
+ // Stop it here: closing re-arms the popout's own Escape listener on window
73
+ // (React flushes a keypress's effects at once), and this same keypress
74
+ // would reach it next and close the popout too.
75
+ e.stopPropagation()
76
+ onCloseRef.current()
77
+ }
78
+ document.addEventListener('keydown', onKey)
79
+ const back = returnFocus?.current
80
+ return () => {
81
+ document.removeEventListener('keydown', onKey)
82
+ back?.focus()
83
+ }
84
+ }, [open, returnFocus])
85
+
86
+ if (!open || typeof document === 'undefined') return null
87
+
88
+ return createPortal(
89
+ <div style={S.overlay} onClick={onClose}>
90
+ <div
91
+ ref={cardRef}
92
+ role="dialog"
93
+ aria-modal="true"
94
+ aria-labelledby={titleId}
95
+ tabIndex={-1}
96
+ style={S.card}
97
+ onClick={e => e.stopPropagation()}
98
+ >
99
+ <div style={S.head}>
100
+ <h2 id={titleId} style={S.title}>
101
+ Avatar
102
+ </h2>
103
+ <button
104
+ type="button"
105
+ className="settings-popout__btn settings-popout__btn--primary"
106
+ onClick={onClose}
107
+ >
108
+ Done
109
+ </button>
110
+ </div>
111
+ <AvatarPicker />
112
+ </div>
113
+ </div>,
114
+ document.body
115
+ )
116
+ }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * AvatarPicker — the avatar section of ConnectedSettings: the picture people
2
+ * AvatarPicker — the body of AvatarDialog (opened from ConnectedSettings): the picture people
3
3
  * see (character, Discord, Steam or an upload) and the character games use as
4
4
  * the piece. One resolved avatar per person, stored in hadoku_site's prefs-api
5
5
  * and rendered by every app (`@wolffm/prefs-client/avatar`, bundled into this
@@ -10,7 +10,7 @@
10
10
  * (`linkInPopup`), so the page under the popout never navigates and a game
11
11
  * keeps running.
12
12
  *
13
- * Loaded only when the person opens it ("Change"), never at mount: the avatar
13
+ * Loaded only when the person opens the dialog ("Change"), never at mount: the avatar
14
14
  * read can refresh a linked picture from Discord or Steam, which no page load
15
15
  * should cost, and opening the gear itself must stay free (settings-prefetch
16
16
  * e2e).
@@ -84,8 +84,8 @@ const S = {
84
84
  stack: { display: 'flex', flexDirection: 'column', gap: 10, width: '100%' },
85
85
  current: { display: 'flex', alignItems: 'center', gap: 10 },
86
86
  preview: {
87
- width: 48,
88
- height: 48,
87
+ width: 64,
88
+ height: 64,
89
89
  borderRadius: '50%',
90
90
  overflow: 'hidden',
91
91
  flex: 'none',
@@ -119,7 +119,7 @@ const S = {
119
119
  background: 'var(--color-bg-alt)'
120
120
  },
121
121
  sets: { display: 'flex', gap: 4, flexWrap: 'wrap' },
122
- grid: { display: 'grid', gridTemplateColumns: 'repeat(6, 1fr)', gap: 4 },
122
+ grid: { display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(64px, 1fr))', gap: 6 },
123
123
  cell: {
124
124
  padding: 0,
125
125
  aspectRatio: '1',
@@ -4,7 +4,7 @@
4
4
  * property of the PERSON (not the app):
5
5
  * - current access tier (read-only, friendly label)
6
6
  * - display name (inline edit → POST /session/name)
7
- * - avatar: picture and character, chosen in place (AvatarPicker)
7
+ * - avatar: picture and character, chosen in a dialog (AvatarDialog)
8
8
  * - global content-visibility level (tiered pill → PUT /prefs/api/v1/content-level)
9
9
  * - switch key (an inline sign-in form, on the site's own sign-in client)
10
10
  *
@@ -32,7 +32,7 @@
32
32
  import React, { useCallback, useEffect, useId, useRef, useState } from 'react'
33
33
  import { useSettingsTab } from '@wolffm/themes'
34
34
  import { SettingsIcon } from './ThemeIcons'
35
- import { AvatarPicker } from './AvatarPicker'
35
+ import { AvatarDialog } from './AvatarDialog'
36
36
 
37
37
  // A grantee's own service credentials, with a QR encoder for TOTP enrolment:
38
38
  // its own chunk, fetched only when a grantee opens it.
@@ -265,8 +265,9 @@ export function ConnectedSettings({
265
265
  const passkeyCtl = useRef<AbortController | null>(null)
266
266
  const cancelPasskey = useCallback((reason?: string) => passkeyCtl.current?.abort(reason), [])
267
267
 
268
- // Avatar: the picker loads only when opened, never at mount (AvatarPicker).
268
+ // Avatar: the dialog's picker loads only when opened, never at mount (AvatarPicker).
269
269
  const [avatarOpen, setAvatarOpen] = useState(false)
270
+ const avatarBtn = useRef<HTMLButtonElement>(null)
270
271
  // Service identities this person may mint for (whoami.serviceGrants).
271
272
  const [serviceGrants, setServiceGrants] = useState(0)
272
273
  const [serviceOpen, setServiceOpen] = useState(false)
@@ -414,15 +415,15 @@ export function ConnectedSettings({
414
415
  // eslint-disable-next-line react-hooks/exhaustive-deps
415
416
  }, [])
416
417
 
417
- // Escape closes.
418
+ // Escape closes — unless the avatar dialog is open: that Escape is the dialog's.
418
419
  useEffect(() => {
419
- if (!open) return
420
+ if (!open || avatarOpen) return
420
421
  const onKey = (e: KeyboardEvent) => {
421
422
  if (e.key === 'Escape') setOpen(false)
422
423
  }
423
424
  window.addEventListener('keydown', onKey)
424
425
  return () => window.removeEventListener('keydown', onKey)
425
- }, [open])
426
+ }, [open, avatarOpen])
426
427
 
427
428
  const saveName = useCallback(async () => {
428
429
  const trimmed = nameDraft.trim()
@@ -708,9 +709,9 @@ export function ConnectedSettings({
708
709
  )}
709
710
  </section>
710
711
 
711
- {/* Avatar: picture and character, chosen in place (AvatarPicker).
712
- Collapsed until Change, so opening the gear costs no request and no
713
- page load reads the avatar. Hidden for public callers, like the
712
+ {/* Avatar: picture and character, chosen in a dialog (AvatarDialog).
713
+ Nothing loads until Change, so opening the gear costs no request and
714
+ no page load reads the avatar. Hidden for public callers, like the
714
715
  content pill: an avatar needs a session. */}
715
716
  {!resolving && userType !== 'public' && (
716
717
  <section className="settings-popout__row settings-popout__row--stack">
@@ -719,20 +720,26 @@ export function ConnectedSettings({
719
720
  Avatar
720
721
  </span>
721
722
  <button
723
+ ref={avatarBtn}
722
724
  type="button"
723
725
  className="settings-popout__btn"
726
+ aria-haspopup="dialog"
724
727
  aria-expanded={avatarOpen}
725
- onClick={() => setAvatarOpen(v => !v)}
728
+ onClick={() => setAvatarOpen(true)}
726
729
  >
727
- {avatarOpen ? 'Done' : 'Change'}
730
+ Change
728
731
  </button>
729
732
  </span>
730
- {avatarOpen && <AvatarPicker />}
733
+ <AvatarDialog
734
+ open={avatarOpen}
735
+ onClose={() => setAvatarOpen(false)}
736
+ returnFocus={avatarBtn}
737
+ />
731
738
  </section>
732
739
  )}
733
740
 
734
741
  {/* Service access: only for someone the operator granted a
735
- service identity. Collapsed until Open, like Avatar. */}
742
+ service identity. Collapsed until Open. */}
736
743
  {!resolving && serviceGrants > 0 && (
737
744
  <section className="settings-popout__row settings-popout__row--stack">
738
745
  <span className="settings-popout__inline">