@hadoku/task-ui-components 4.22.0 → 4.22.3
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.
- package/dist/app-header.css +5 -0
- package/dist/components/AvatarPicker.d.ts +29 -0
- package/dist/components/AvatarPicker.d.ts.map +1 -0
- package/dist/components/AvatarPicker.js +326 -0
- package/dist/components/AvatarPicker.js.map +1 -0
- package/dist/components/ConnectedSettings.d.ts +1 -1
- package/dist/components/ConnectedSettings.d.ts.map +1 -1
- package/dist/components/ConnectedSettings.js +42 -9
- package/dist/components/ConnectedSettings.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/avatarEvent.d.ts +12 -0
- package/dist/lib/avatarEvent.d.ts.map +1 -0
- package/dist/lib/avatarEvent.js +5 -0
- package/dist/lib/avatarEvent.js.map +1 -0
- package/dist/lib/settingsClient.d.ts +14 -3
- package/dist/lib/settingsClient.d.ts.map +1 -1
- package/dist/lib/settingsClient.js +7 -3
- package/dist/lib/settingsClient.js.map +1 -1
- package/dist/styles.css +5 -0
- package/dist/task-ui-components.css +5 -0
- package/dist/vendor/wolffm/prefs-client/avatar.js +112 -0
- package/dist/vendor/wolffm/prefs-client/avatar.js.map +1 -0
- package/package.json +1 -1
- package/src/app-header.css +5 -0
- package/src/components/AvatarPicker.tsx +443 -0
- package/src/components/ConnectedSettings.tsx +48 -17
- package/src/index.ts +1 -0
- package/src/lib/avatarEvent.ts +11 -0
- package/src/lib/settingsClient.ts +16 -6
|
@@ -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
|
|
7
|
+
* - avatar: picture and character, chosen in place (AvatarPicker)
|
|
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
|
*
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
*/
|
|
28
28
|
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
|
29
29
|
import { SettingsIcon } from './ThemeIcons'
|
|
30
|
+
import { AvatarPicker } from './AvatarPicker'
|
|
30
31
|
import {
|
|
31
32
|
deleteOwnAccount,
|
|
32
33
|
prefetchSettings,
|
|
@@ -154,6 +155,7 @@ export function ConnectedSettings({
|
|
|
154
155
|
const [nameDraft, setNameDraft] = useState(resolvedName ?? '')
|
|
155
156
|
const [editingName, setEditingName] = useState(false)
|
|
156
157
|
const [nameSaving, setNameSaving] = useState(false)
|
|
158
|
+
const [nameError, setNameError] = useState<string | null>(null)
|
|
157
159
|
const [displayName, setLocalName] = useState<string | null>(resolvedName)
|
|
158
160
|
|
|
159
161
|
// Key swap. The draft lives only in this state and the input: it is never
|
|
@@ -179,6 +181,9 @@ export function ConnectedSettings({
|
|
|
179
181
|
const passkeyCtl = useRef<AbortController | null>(null)
|
|
180
182
|
const cancelPasskey = useCallback((reason?: string) => passkeyCtl.current?.abort(reason), [])
|
|
181
183
|
|
|
184
|
+
// Avatar: the picker loads only when opened, never at mount (AvatarPicker).
|
|
185
|
+
const [avatarOpen, setAvatarOpen] = useState(false)
|
|
186
|
+
|
|
182
187
|
// Content level
|
|
183
188
|
const [content, setContent] = useState<ContentLevelState | null>(null)
|
|
184
189
|
const [levelSaving, setLevelSaving] = useState(false)
|
|
@@ -279,13 +284,17 @@ export function ConnectedSettings({
|
|
|
279
284
|
return
|
|
280
285
|
}
|
|
281
286
|
setNameSaving(true)
|
|
282
|
-
const
|
|
287
|
+
const result = await setDisplayName(trimmed)
|
|
283
288
|
setNameSaving(false)
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
289
|
+
// A refusal keeps the editor open on the draft, saying why.
|
|
290
|
+
if (!result.ok) {
|
|
291
|
+
setNameError(result.error)
|
|
292
|
+
return
|
|
288
293
|
}
|
|
294
|
+
setNameError(null)
|
|
295
|
+
setLocalName(result.name)
|
|
296
|
+
setNameDraft(result.name ?? '')
|
|
297
|
+
if (result.name !== null) onNameChange?.(result.name)
|
|
289
298
|
setEditingName(false)
|
|
290
299
|
}, [nameDraft, displayName, onNameChange])
|
|
291
300
|
|
|
@@ -461,7 +470,12 @@ export function ConnectedSettings({
|
|
|
461
470
|
maxLength={64}
|
|
462
471
|
autoFocus
|
|
463
472
|
disabled={nameSaving}
|
|
464
|
-
|
|
473
|
+
aria-invalid={nameError !== null}
|
|
474
|
+
aria-describedby={nameError ? 'settings-popout-name-error' : undefined}
|
|
475
|
+
onChange={e => {
|
|
476
|
+
setNameDraft(e.target.value)
|
|
477
|
+
setNameError(null)
|
|
478
|
+
}}
|
|
465
479
|
onKeyDown={e => {
|
|
466
480
|
if (e.key === 'Enter') {
|
|
467
481
|
saveName().catch(reportErr)
|
|
@@ -470,6 +484,7 @@ export function ConnectedSettings({
|
|
|
470
484
|
// The edit's Escape, not the popout's (window listener).
|
|
471
485
|
e.stopPropagation()
|
|
472
486
|
setEditingName(false)
|
|
487
|
+
setNameError(null)
|
|
473
488
|
setNameDraft(displayName ?? '')
|
|
474
489
|
}
|
|
475
490
|
}}
|
|
@@ -484,6 +499,15 @@ export function ConnectedSettings({
|
|
|
484
499
|
>
|
|
485
500
|
{nameSaving ? '…' : 'Save'}
|
|
486
501
|
</button>
|
|
502
|
+
{nameError && (
|
|
503
|
+
<span
|
|
504
|
+
id="settings-popout-name-error"
|
|
505
|
+
className="settings-popout__error"
|
|
506
|
+
role="alert"
|
|
507
|
+
>
|
|
508
|
+
{nameError}
|
|
509
|
+
</span>
|
|
510
|
+
)}
|
|
487
511
|
</span>
|
|
488
512
|
) : (
|
|
489
513
|
<button
|
|
@@ -500,17 +524,24 @@ export function ConnectedSettings({
|
|
|
500
524
|
)}
|
|
501
525
|
</section>
|
|
502
526
|
|
|
503
|
-
{/* Avatar
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
callers, like the content pill: /account needs a session. */}
|
|
527
|
+
{/* Avatar: picture and character, chosen in place (AvatarPicker).
|
|
528
|
+
Collapsed until Change, so opening the gear costs no request and no
|
|
529
|
+
page load reads the avatar. Hidden for public callers, like the
|
|
530
|
+
content pill: an avatar needs a session. */}
|
|
508
531
|
{!resolving && userType !== 'public' && (
|
|
509
|
-
<section className="settings-popout__row">
|
|
510
|
-
<span className="settings-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
532
|
+
<section className="settings-popout__row settings-popout__row--stack">
|
|
533
|
+
<span className="settings-popout__inline">
|
|
534
|
+
<span className="settings-popout__label settings-popout__label--grow">Avatar</span>
|
|
535
|
+
<button
|
|
536
|
+
type="button"
|
|
537
|
+
className="settings-popout__btn"
|
|
538
|
+
aria-expanded={avatarOpen}
|
|
539
|
+
onClick={() => setAvatarOpen(v => !v)}
|
|
540
|
+
>
|
|
541
|
+
{avatarOpen ? 'Done' : 'Change'}
|
|
542
|
+
</button>
|
|
543
|
+
</span>
|
|
544
|
+
{avatarOpen && <AvatarPicker />}
|
|
514
545
|
</section>
|
|
515
546
|
)}
|
|
516
547
|
|
package/src/index.ts
CHANGED
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
export { HadokuThemePicker } from './components/HadokuThemePicker'
|
|
21
21
|
export { AppHeader } from './components/AppHeader'
|
|
22
22
|
export { ConnectedSettings } from './components/ConnectedSettings'
|
|
23
|
+
export { AVATAR_EVENT } from './lib/avatarEvent'
|
|
23
24
|
// Re-exported from @wolffm/themes, which now owns the context AND the provider
|
|
24
25
|
// that fills it. These are RE-EXPORTS, not re-declarations: every consumer gets
|
|
25
26
|
// the one context object from themes, which is what makes a duplicated copy of
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fired on `window` after every avatar change made in the settings popout
|
|
3
|
+
* (AvatarPicker), with the new avatar settings as `detail` (hadoku_site's
|
|
4
|
+
* `AvatarSettings`, @wolffm/prefs-client/avatar). A host page listens to update
|
|
5
|
+
* what it shows (watchparty sends the new character to its room) without
|
|
6
|
+
* polling prefs-api.
|
|
7
|
+
*
|
|
8
|
+
* Its own module, free of imports, so exporting it from the package entry adds
|
|
9
|
+
* no type dependency on prefs-client for consumers.
|
|
10
|
+
*/
|
|
11
|
+
export const AVATAR_EVENT = 'hadoku:avatar'
|
|
@@ -140,9 +140,15 @@ export async function deleteOwnAccount(): Promise<boolean> {
|
|
|
140
140
|
}
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
-
/**
|
|
144
|
-
*
|
|
145
|
-
|
|
143
|
+
/**
|
|
144
|
+
* POST a new display name for the signed-in user. Answers the stored name (may
|
|
145
|
+
* differ from the request for wp-* protected keys), or why it was refused: the
|
|
146
|
+
* server's own words where it gave any ("That display name is already taken"),
|
|
147
|
+
* so the editor can say them.
|
|
148
|
+
*/
|
|
149
|
+
export type RenameResult = { ok: true; name: string | null } | { ok: false; error: string }
|
|
150
|
+
|
|
151
|
+
export async function setDisplayName(name: string): Promise<RenameResult> {
|
|
146
152
|
try {
|
|
147
153
|
const res = await fetch(SESSION_NAME, {
|
|
148
154
|
method: 'POST',
|
|
@@ -151,12 +157,16 @@ export async function setDisplayName(name: string): Promise<string | null> {
|
|
|
151
157
|
body: JSON.stringify({ name }),
|
|
152
158
|
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
153
159
|
})
|
|
154
|
-
if (!res.ok)
|
|
160
|
+
if (!res.ok) {
|
|
161
|
+
const body = (await res.json().catch(() => null)) as { error?: unknown } | null
|
|
162
|
+
const said = typeof body?.error === 'string' ? body.error : null
|
|
163
|
+
return { ok: false, error: said ?? `Not saved (${res.status})` }
|
|
164
|
+
}
|
|
155
165
|
const body = (await res.json()) as { ok: boolean; name: string | null }
|
|
156
166
|
patchSnapshot(prev => ({ ...prev, identity: { ...prev.identity, name: body.name } }))
|
|
157
|
-
return body.name
|
|
167
|
+
return { ok: true, name: body.name }
|
|
158
168
|
} catch {
|
|
159
|
-
return
|
|
169
|
+
return { ok: false, error: 'Not saved: no connection' }
|
|
160
170
|
}
|
|
161
171
|
}
|
|
162
172
|
|