@hadoku/task-ui-components 4.20.7 → 4.20.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"ConnectedSettings.d.ts","sourceRoot":"","sources":["../../src/components/ConnectedSettings.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,OAAO,KAAmD,MAAM,OAAO,CAAA;AAEvE,OAAO,EAUL,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;;;;;;8BAM0B;IAC1B,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;CAC3B;AA6CD,wBAAgB,iBAAiB,CAAC,EAChC,QAAQ,EAAE,YAAY,EACtB,IAAI,EAAE,QAAQ,EACd,YAAY,EACZ,QAAQ,EACT,EAAE,sBAAsB,qBAoiBxB"}
1
+ {"version":3,"file":"ConnectedSettings.d.ts","sourceRoot":"","sources":["../../src/components/ConnectedSettings.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,OAAO,KAAmD,MAAM,OAAO,CAAA;AAEvE,OAAO,EAUL,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;;;;;;8BAM0B;IAC1B,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;CAC3B;AAyED,wBAAgB,iBAAiB,CAAC,EAChC,QAAQ,EAAE,YAAY,EACtB,IAAI,EAAE,QAAQ,EACd,YAAY,EACZ,QAAQ,EACT,EAAE,sBAAsB,qBAqiBxB"}
@@ -24,6 +24,29 @@ const PASSKEY_DOT = {
24
24
  background: "var(--color-primary)",
25
25
  pointerEvents: "none"
26
26
  };
27
+ const PASSKEY_PULSE_CLASS = "settings-popout__btn--pulse";
28
+ const PASSKEY_PULSE_STYLE_ID = "settings-passkey-pulse";
29
+ const PASSKEY_PULSE_CSS = `
30
+ @keyframes settings-passkey-pulse {
31
+ 0% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--color-primary) 70%, transparent); }
32
+ 70% { box-shadow: 0 0 0 8px color-mix(in srgb, var(--color-primary) 0%, transparent); }
33
+ 100% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--color-primary) 0%, transparent); }
34
+ }
35
+ @media (prefers-reduced-motion: no-preference) {
36
+ .${PASSKEY_PULSE_CLASS}:not(:disabled) {
37
+ animation: settings-passkey-pulse 1.6s ease-out infinite;
38
+ }
39
+ }
40
+ `;
41
+ function usePasskeyPulseStyle(active) {
42
+ useEffect(() => {
43
+ if (!active || document.getElementById(PASSKEY_PULSE_STYLE_ID)) return;
44
+ const el = document.createElement("style");
45
+ el.id = PASSKEY_PULSE_STYLE_ID;
46
+ el.textContent = PASSKEY_PULSE_CSS;
47
+ document.head.appendChild(el);
48
+ }, [active]);
49
+ }
27
50
  const TIER_LABEL = {
28
51
  admin: "Administrator",
29
52
  wife: "Wife",
@@ -60,6 +83,7 @@ function ConnectedSettings({
60
83
  const [resolving, setResolving] = useState(true);
61
84
  const showContentPill = userType !== "public";
62
85
  const needsPasskey = !resolving && userType !== "public" && passkeyCount === 0 && !passkeySaved && passkeysSupported();
86
+ usePasskeyPulseStyle(needsPasskey);
63
87
  useEffect(() => {
64
88
  if (userTypeProp !== void 0) setUserType(userTypeProp);
65
89
  }, [userTypeProp]);
@@ -407,7 +431,7 @@ function ConnectedSettings({
407
431
  "button",
408
432
  {
409
433
  type: "button",
410
- className: `settings-popout__btn${needsPasskey ? " settings-popout__btn--primary" : ""}`,
434
+ className: `settings-popout__btn${needsPasskey ? ` settings-popout__btn--primary ${PASSKEY_PULSE_CLASS}` : ""}`,
411
435
  disabled: passkeyMode !== null || passkeySaved,
412
436
  onClick: () => {
413
437
  savePasskey().catch(reportErr);
@@ -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 * - 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 * \"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, useRef, useState } from 'react'\nimport { SettingsIcon } from './ThemeIcons'\nimport {\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 rendered as a final section in the same popout, so\n * an app with its own preferences keeps ONE unified gear instead of a second\n * control. Shown under an \"App preferences\" divider.\n *\n * This slot is deliberately unconstrained — put whatever the app needs in\n * it. The constraint is only that it lands AFTER the four canonical rows and\n * cannot change them. */\n children?: 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\nconst TIER_LABEL: Record<Tier, string> = {\n admin: 'Administrator',\n wife: 'Wife',\n service: 'Service',\n friend: 'Friend',\n public: 'Guest'\n}\n\nexport function ConnectedSettings({\n userType: userTypeProp,\n name: nameProp,\n onNameChange,\n children\n}: ConnectedSettingsProps) {\n const [open, setOpen] = useState(false)\n const rootRef = useRef<HTMLDivElement>(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 [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 const passkeyCtl = useRef<AbortController | null>(null)\n const cancelPasskey = useCallback((reason?: string) => passkeyCtl.current?.abort(reason), [])\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\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 }) => {\n if (!live) return\n setPasskeyCount(passkeys)\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 stored = await setDisplayName(trimmed)\n setNameSaving(false)\n if (stored !== null) {\n setLocalName(stored)\n setNameDraft(stored)\n onNameChange?.(stored)\n }\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`, 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__header\">Settings</div>\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 onChange={e => setNameDraft(e.target.value)}\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 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 </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 {/* 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 {!resolving && showContentPill && (\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 {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' : ''}`}\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 {/* App-specific settings (optional) — same popout, one gear. */}\n {children && (\n <section className=\"settings-popout__row settings-popout__row--stack settings-popout__app\">\n {children}\n </section>\n )}\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 * - 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 * \"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, useRef, useState } from 'react'\nimport { SettingsIcon } from './ThemeIcons'\nimport {\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 rendered as a final section in the same popout, so\n * an app with its own preferences keeps ONE unified gear instead of a second\n * control. Shown under an \"App preferences\" divider.\n *\n * This slot is deliberately unconstrained — put whatever the app needs in\n * it. The constraint is only that it lands AFTER the four canonical rows and\n * cannot change them. */\n children?: 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_STYLE_ID = 'settings-passkey-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\nfunction usePasskeyPulseStyle(active: boolean) {\n useEffect(() => {\n if (!active || document.getElementById(PASSKEY_PULSE_STYLE_ID)) return\n const el = document.createElement('style')\n el.id = PASSKEY_PULSE_STYLE_ID\n el.textContent = PASSKEY_PULSE_CSS\n document.head.appendChild(el)\n }, [active])\n}\n\nconst TIER_LABEL: Record<Tier, string> = {\n admin: 'Administrator',\n wife: 'Wife',\n service: 'Service',\n friend: 'Friend',\n public: 'Guest'\n}\n\nexport function ConnectedSettings({\n userType: userTypeProp,\n name: nameProp,\n onNameChange,\n children\n}: ConnectedSettingsProps) {\n const [open, setOpen] = useState(false)\n const rootRef = useRef<HTMLDivElement>(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 [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 const passkeyCtl = useRef<AbortController | null>(null)\n const cancelPasskey = useCallback((reason?: string) => passkeyCtl.current?.abort(reason), [])\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 usePasskeyPulseStyle(needsPasskey)\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 }) => {\n if (!live) return\n setPasskeyCount(passkeys)\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 stored = await setDisplayName(trimmed)\n setNameSaving(false)\n if (stored !== null) {\n setLocalName(stored)\n setNameDraft(stored)\n onNameChange?.(stored)\n }\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`, 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__header\">Settings</div>\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 onChange={e => setNameDraft(e.target.value)}\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 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 </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 {/* 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 {!resolving && showContentPill && (\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 {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 {/* App-specific settings (optional) — same popout, one gear. */}\n {children && (\n <section className=\"settings-popout__row settings-popout__row--stack settings-popout__app\">\n {children}\n </section>\n )}\n </div>\n )}\n </div>\n )\n}\n"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hadoku/task-ui-components",
3
- "version": "4.20.7",
3
+ "version": "4.20.8",
4
4
  "description": "Reusable UI components with Bento grid and metallic gradient support",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -98,6 +98,34 @@ const PASSKEY_DOT: React.CSSProperties = {
98
98
  pointerEvents: 'none'
99
99
  }
100
100
 
101
+ /** Save passkey pulses while the dot is asking for it. Keyframes cannot be
102
+ * inline, so the rule is injected once into the document instead, for the
103
+ * same reason PASSKEY_DOT is inline. */
104
+ const PASSKEY_PULSE_CLASS = 'settings-popout__btn--pulse'
105
+ const PASSKEY_PULSE_STYLE_ID = 'settings-passkey-pulse'
106
+ const PASSKEY_PULSE_CSS = `
107
+ @keyframes settings-passkey-pulse {
108
+ 0% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--color-primary) 70%, transparent); }
109
+ 70% { box-shadow: 0 0 0 8px color-mix(in srgb, var(--color-primary) 0%, transparent); }
110
+ 100% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--color-primary) 0%, transparent); }
111
+ }
112
+ @media (prefers-reduced-motion: no-preference) {
113
+ .${PASSKEY_PULSE_CLASS}:not(:disabled) {
114
+ animation: settings-passkey-pulse 1.6s ease-out infinite;
115
+ }
116
+ }
117
+ `
118
+
119
+ function usePasskeyPulseStyle(active: boolean) {
120
+ useEffect(() => {
121
+ if (!active || document.getElementById(PASSKEY_PULSE_STYLE_ID)) return
122
+ const el = document.createElement('style')
123
+ el.id = PASSKEY_PULSE_STYLE_ID
124
+ el.textContent = PASSKEY_PULSE_CSS
125
+ document.head.appendChild(el)
126
+ }, [active])
127
+ }
128
+
101
129
  const TIER_LABEL: Record<Tier, string> = {
102
130
  admin: 'Administrator',
103
131
  wife: 'Wife',
@@ -171,6 +199,7 @@ export function ConnectedSettings({
171
199
  passkeyCount === 0 &&
172
200
  !passkeySaved &&
173
201
  passkeysSupported()
202
+ usePasskeyPulseStyle(needsPasskey)
174
203
 
175
204
  // Keep local identity in sync when the host controls it via props.
176
205
  useEffect(() => {
@@ -607,7 +636,7 @@ export function ConnectedSettings({
607
636
  {!resolving && userType !== 'public' && passkeysSupported() && (
608
637
  <button
609
638
  type="button"
610
- className={`settings-popout__btn${needsPasskey ? ' settings-popout__btn--primary' : ''}`}
639
+ className={`settings-popout__btn${needsPasskey ? ` settings-popout__btn--primary ${PASSKEY_PULSE_CLASS}` : ''}`}
611
640
  disabled={passkeyMode !== null || passkeySaved}
612
641
  onClick={() => {
613
642
  savePasskey().catch(reportErr)