@hadoku/task-ui-components 4.23.13 → 4.23.15
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/components/AvatarBuilder.d.ts.map +1 -1
- package/dist/components/AvatarBuilder.js +65 -76
- package/dist/components/AvatarBuilder.js.map +1 -1
- package/dist/components/AvatarPicker.d.ts +3 -2
- package/dist/components/AvatarPicker.d.ts.map +1 -1
- package/dist/components/AvatarPicker.js +27 -9
- package/dist/components/AvatarPicker.js.map +1 -1
- package/dist/components/LinkedAccounts.d.ts +19 -0
- package/dist/components/LinkedAccounts.d.ts.map +1 -1
- package/dist/components/LinkedAccounts.js +41 -24
- package/dist/components/LinkedAccounts.js.map +1 -1
- package/dist/components/ThemePicker.d.ts.map +1 -1
- package/dist/components/ThemePicker.js +24 -1
- package/dist/components/ThemePicker.js.map +1 -1
- package/package.json +2 -2
- package/src/components/AvatarBuilder.tsx +57 -74
- package/src/components/AvatarPicker.tsx +31 -13
- package/src/components/LinkedAccounts.tsx +63 -31
- package/src/components/ThemePicker.tsx +40 -1
|
@@ -51,6 +51,60 @@ const S = {
|
|
|
51
51
|
list: { margin: 0, paddingLeft: 18, fontSize: '0.75rem', lineHeight: 1.45 }
|
|
52
52
|
} satisfies Record<string, React.CSSProperties>
|
|
53
53
|
|
|
54
|
+
/**
|
|
55
|
+
* Link `provider` in a popup, and wear its picture when it came back with one:
|
|
56
|
+
* linking from either place means "use this". Call it inside the click, with
|
|
57
|
+
* nothing awaited first — the popup opens before its first await, which is
|
|
58
|
+
* what popup blockers allow.
|
|
59
|
+
*/
|
|
60
|
+
export function linkAndWear(provider: LinkProvider): Promise<AvatarSettings> {
|
|
61
|
+
return avatarClient
|
|
62
|
+
.linkInPopup(provider)
|
|
63
|
+
.then(next => (next.linked[provider]?.avatarUrl ? avatarClient.choose(provider) : next))
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* What linking reads and keeps, then Continue. Shared by Account → Linked
|
|
68
|
+
* accounts and the avatar dialog's Picture tab, so the consent is one text.
|
|
69
|
+
*/
|
|
70
|
+
export function LinkConsent({
|
|
71
|
+
provider,
|
|
72
|
+
busy,
|
|
73
|
+
onLink,
|
|
74
|
+
onCancel
|
|
75
|
+
}: {
|
|
76
|
+
provider: LinkProvider
|
|
77
|
+
busy: boolean
|
|
78
|
+
/** Must open the popup synchronously: call {@link linkAndWear} directly. */
|
|
79
|
+
onLink: () => void
|
|
80
|
+
onCancel?: () => void
|
|
81
|
+
}) {
|
|
82
|
+
return (
|
|
83
|
+
<div style={S.stack}>
|
|
84
|
+
<ul style={S.list}>
|
|
85
|
+
{CONSENT[provider].points.map(t => (
|
|
86
|
+
<li key={t}>{t}</li>
|
|
87
|
+
))}
|
|
88
|
+
</ul>
|
|
89
|
+
<span style={S.line}>
|
|
90
|
+
<button
|
|
91
|
+
type="button"
|
|
92
|
+
className="settings-popout__btn settings-popout__btn--primary"
|
|
93
|
+
disabled={busy}
|
|
94
|
+
onClick={onLink}
|
|
95
|
+
>
|
|
96
|
+
{CONSENT[provider].go}
|
|
97
|
+
</button>
|
|
98
|
+
{onCancel && (
|
|
99
|
+
<button type="button" className="settings-popout__btn" onClick={onCancel}>
|
|
100
|
+
Cancel
|
|
101
|
+
</button>
|
|
102
|
+
)}
|
|
103
|
+
</span>
|
|
104
|
+
</div>
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
|
|
54
108
|
export function LinkedAccounts() {
|
|
55
109
|
const [settings, setSettings] = useState<AvatarSettings | null>(null)
|
|
56
110
|
const [busy, setBusy] = useState(false)
|
|
@@ -92,17 +146,6 @@ export function LinkedAccounts() {
|
|
|
92
146
|
)
|
|
93
147
|
}
|
|
94
148
|
|
|
95
|
-
// Must stay synchronous up to linkInPopup: the popup opens before its first
|
|
96
|
-
// await, inside this click, which is what popup blockers allow.
|
|
97
|
-
const link = (provider: LinkProvider) => {
|
|
98
|
-
setConsent(null)
|
|
99
|
-
void run(async () => {
|
|
100
|
-
const next = await avatarClient.linkInPopup(provider)
|
|
101
|
-
// Linking is choosing, when the account came back with a picture.
|
|
102
|
-
return next.linked[provider]?.avatarUrl ? avatarClient.choose(provider) : next
|
|
103
|
-
})
|
|
104
|
-
}
|
|
105
|
-
|
|
106
149
|
return (
|
|
107
150
|
<div style={S.stack}>
|
|
108
151
|
{(['discord', 'steam'] as const).map(p => {
|
|
@@ -144,26 +187,15 @@ export function LinkedAccounts() {
|
|
|
144
187
|
})}
|
|
145
188
|
|
|
146
189
|
{consent && (
|
|
147
|
-
<
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
)
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
className="settings-popout__btn settings-popout__btn--primary"
|
|
157
|
-
disabled={busy}
|
|
158
|
-
onClick={() => link(consent)}
|
|
159
|
-
>
|
|
160
|
-
{CONSENT[consent].go}
|
|
161
|
-
</button>
|
|
162
|
-
<button type="button" className="settings-popout__btn" onClick={() => setConsent(null)}>
|
|
163
|
-
Cancel
|
|
164
|
-
</button>
|
|
165
|
-
</span>
|
|
166
|
-
</div>
|
|
190
|
+
<LinkConsent
|
|
191
|
+
provider={consent}
|
|
192
|
+
busy={busy}
|
|
193
|
+
onLink={() => {
|
|
194
|
+
setConsent(null)
|
|
195
|
+
void run(() => linkAndWear(consent))
|
|
196
|
+
}}
|
|
197
|
+
onCancel={() => setConsent(null)}
|
|
198
|
+
/>
|
|
167
199
|
)}
|
|
168
200
|
|
|
169
201
|
{error && (
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Designed to work with any theme system via configuration
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import React, { useRef, useLayoutEffect, useState } from 'react'
|
|
7
|
+
import React, { useEffect, useRef, useLayoutEffect, useState } from 'react'
|
|
8
8
|
import { Tooltip } from './Tooltip'
|
|
9
9
|
import type { ThemePickerProps } from '../types'
|
|
10
10
|
import { SettingsIcon, getFallbackIcon } from './ThemeIcons'
|
|
@@ -42,6 +42,11 @@ export function ThemePicker({
|
|
|
42
42
|
dropdownPlacement = 'auto'
|
|
43
43
|
}: ThemePickerProps) {
|
|
44
44
|
const dropdownRef = useRef<HTMLDivElement>(null)
|
|
45
|
+
const toggleRef = useRef<HTMLButtonElement>(null)
|
|
46
|
+
// The latest onToggle, so the open/close effect below runs on isOpen alone:
|
|
47
|
+
// callers pass a new arrow every render, and re-running it would steal focus.
|
|
48
|
+
const onToggleRef = useRef(onToggle)
|
|
49
|
+
onToggleRef.current = onToggle
|
|
45
50
|
const [resolvedPlacement, setResolvedPlacement] = useState<'left' | 'right'>('right')
|
|
46
51
|
|
|
47
52
|
// Edge detection: when 'auto', measure the dropdown after render and flip if it overflows
|
|
@@ -69,6 +74,26 @@ export function ThemePicker({
|
|
|
69
74
|
return () => cancelAnimationFrame(frameId)
|
|
70
75
|
}, [isOpen, dropdownPlacement])
|
|
71
76
|
|
|
77
|
+
// Keyboard and TV remote: opening moves focus into the dropdown (onto the
|
|
78
|
+
// theme in use), and Escape or a TV's Back key closes it and hands focus back
|
|
79
|
+
// to the toggle. Without this a remote could open the picker and never leave.
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
if (!isOpen) return
|
|
82
|
+
const dropdown = dropdownRef.current
|
|
83
|
+
const target =
|
|
84
|
+
dropdown?.querySelector<HTMLButtonElement>('.theme-pill__btn.active') ??
|
|
85
|
+
dropdown?.querySelector<HTMLButtonElement>('button')
|
|
86
|
+
target?.focus()
|
|
87
|
+
const onKey = (e: KeyboardEvent) => {
|
|
88
|
+
if (!isCloseKey(e)) return
|
|
89
|
+
e.preventDefault()
|
|
90
|
+
onToggleRef.current()
|
|
91
|
+
toggleRef.current?.focus()
|
|
92
|
+
}
|
|
93
|
+
window.addEventListener('keydown', onKey)
|
|
94
|
+
return () => window.removeEventListener('keydown', onKey)
|
|
95
|
+
}, [isOpen])
|
|
96
|
+
|
|
72
97
|
// Get icon for current theme (use provided function or fallback to MoonIcon)
|
|
73
98
|
const currentIcon = getThemeIcon ? getThemeIcon(currentTheme) : <MoonIcon />
|
|
74
99
|
|
|
@@ -78,9 +103,12 @@ export function ThemePicker({
|
|
|
78
103
|
<div className={`theme-picker ${openLeftClass} ${className}`.trim()}>
|
|
79
104
|
<Tooltip content="Choose theme">
|
|
80
105
|
<button
|
|
106
|
+
ref={toggleRef}
|
|
81
107
|
className="app-header__icon-btn theme-toggle-btn"
|
|
82
108
|
onClick={onToggle}
|
|
83
109
|
aria-label="Choose theme"
|
|
110
|
+
aria-expanded={isOpen}
|
|
111
|
+
aria-haspopup="true"
|
|
84
112
|
>
|
|
85
113
|
{currentIcon}
|
|
86
114
|
</button>
|
|
@@ -147,3 +175,14 @@ export function ThemePicker({
|
|
|
147
175
|
</div>
|
|
148
176
|
)
|
|
149
177
|
}
|
|
178
|
+
|
|
179
|
+
/** Escape, or a TV remote's Back: GoBack/BrowserBack, webOS 461, Tizen 10009. */
|
|
180
|
+
function isCloseKey(e: KeyboardEvent): boolean {
|
|
181
|
+
return (
|
|
182
|
+
e.key === 'Escape' ||
|
|
183
|
+
e.key === 'GoBack' ||
|
|
184
|
+
e.key === 'BrowserBack' ||
|
|
185
|
+
e.keyCode === 461 ||
|
|
186
|
+
e.keyCode === 10009
|
|
187
|
+
)
|
|
188
|
+
}
|