dauth-context-react 6.1.0 → 6.3.0
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/index.d.mts +24 -2
- package/dist/index.d.ts +24 -2
- package/dist/index.js +1249 -164
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1249 -164
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/DauthProfileModal.tsx +1285 -246
- package/src/api/dauth.api.ts +73 -0
- package/src/api/interfaces/dauth.api.responses.ts +36 -0
- package/src/index.tsx +33 -1
- package/src/initialDauthState.ts +3 -0
- package/src/interfaces.ts +28 -0
- package/src/reducer/dauth.actions.ts +91 -0
- package/src/webauthn.ts +111 -0
|
@@ -7,11 +7,15 @@ import React, {
|
|
|
7
7
|
} from 'react';
|
|
8
8
|
import { createPortal } from 'react-dom';
|
|
9
9
|
import { useDauth } from './index';
|
|
10
|
-
import type {
|
|
10
|
+
import type {
|
|
11
|
+
DauthProfileModalProps,
|
|
12
|
+
IPasskeyCredential,
|
|
13
|
+
} from './interfaces';
|
|
11
14
|
|
|
12
15
|
export { type DauthProfileModalProps };
|
|
13
16
|
|
|
14
17
|
type Phase = 'exited' | 'entering' | 'entered' | 'exiting';
|
|
18
|
+
type Tab = 'profile' | 'security' | 'account';
|
|
15
19
|
|
|
16
20
|
const TRANSITION_MS = 200;
|
|
17
21
|
const MOBILE_TRANSITION_MS = 300;
|
|
@@ -56,6 +60,104 @@ function IconBack() {
|
|
|
56
60
|
);
|
|
57
61
|
}
|
|
58
62
|
|
|
63
|
+
function IconFingerprint() {
|
|
64
|
+
return (
|
|
65
|
+
<svg
|
|
66
|
+
width="16"
|
|
67
|
+
height="16"
|
|
68
|
+
viewBox="0 0 24 24"
|
|
69
|
+
fill="none"
|
|
70
|
+
stroke="currentColor"
|
|
71
|
+
strokeWidth="2"
|
|
72
|
+
strokeLinecap="round"
|
|
73
|
+
strokeLinejoin="round"
|
|
74
|
+
>
|
|
75
|
+
<path d="M12 10a2 2 0 0 0-2 2c0 1.02-.1 2.51-.26 4" />
|
|
76
|
+
<path d="M14 13.12c0 2.38 0 6.38-1 8.88" />
|
|
77
|
+
<path d="M17.29 21.02c.12-.6.43-2.3.5-3.02" />
|
|
78
|
+
<path d="M2 12a10 10 0 0 1 18-6" />
|
|
79
|
+
<path d="M2 16h.01" />
|
|
80
|
+
<path d="M21.8 16c.2-2 .131-5.354 0-6" />
|
|
81
|
+
<path d="M5 19.5C5.5 18 6 15 6 12a6 6 0 0 1 .34-2" />
|
|
82
|
+
<path d="M8.65 22c.21-.66.45-1.32.57-2" />
|
|
83
|
+
<path d="M9 6.8a6 6 0 0 1 9 5.2v2" />
|
|
84
|
+
</svg>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function IconShield() {
|
|
89
|
+
return (
|
|
90
|
+
<svg
|
|
91
|
+
width="20"
|
|
92
|
+
height="20"
|
|
93
|
+
viewBox="0 0 24 24"
|
|
94
|
+
fill="none"
|
|
95
|
+
stroke="currentColor"
|
|
96
|
+
strokeWidth="2"
|
|
97
|
+
strokeLinecap="round"
|
|
98
|
+
strokeLinejoin="round"
|
|
99
|
+
>
|
|
100
|
+
<path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z" />
|
|
101
|
+
</svg>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function IconTrash() {
|
|
106
|
+
return (
|
|
107
|
+
<svg
|
|
108
|
+
width="14"
|
|
109
|
+
height="14"
|
|
110
|
+
viewBox="0 0 24 24"
|
|
111
|
+
fill="none"
|
|
112
|
+
stroke="currentColor"
|
|
113
|
+
strokeWidth="2"
|
|
114
|
+
strokeLinecap="round"
|
|
115
|
+
strokeLinejoin="round"
|
|
116
|
+
>
|
|
117
|
+
<path d="M3 6h18" />
|
|
118
|
+
<path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6" />
|
|
119
|
+
<path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2" />
|
|
120
|
+
</svg>
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function IconLogOut() {
|
|
125
|
+
return (
|
|
126
|
+
<svg
|
|
127
|
+
width="16"
|
|
128
|
+
height="16"
|
|
129
|
+
viewBox="0 0 24 24"
|
|
130
|
+
fill="none"
|
|
131
|
+
stroke="currentColor"
|
|
132
|
+
strokeWidth="2"
|
|
133
|
+
strokeLinecap="round"
|
|
134
|
+
strokeLinejoin="round"
|
|
135
|
+
>
|
|
136
|
+
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" />
|
|
137
|
+
<polyline points="16 17 21 12 16 7" />
|
|
138
|
+
<line x1="21" y1="12" x2="9" y2="12" />
|
|
139
|
+
</svg>
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function IconCamera() {
|
|
144
|
+
return (
|
|
145
|
+
<svg
|
|
146
|
+
width="14"
|
|
147
|
+
height="14"
|
|
148
|
+
viewBox="0 0 24 24"
|
|
149
|
+
fill="none"
|
|
150
|
+
stroke="currentColor"
|
|
151
|
+
strokeWidth="2"
|
|
152
|
+
strokeLinecap="round"
|
|
153
|
+
strokeLinejoin="round"
|
|
154
|
+
>
|
|
155
|
+
<path d="M14.5 4h-5L7 7H4a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2h-3l-2.5-3z" />
|
|
156
|
+
<circle cx="12" cy="13" r="3" />
|
|
157
|
+
</svg>
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
59
161
|
function Spinner() {
|
|
60
162
|
return <span style={spinnerStyle} aria-hidden="true" />;
|
|
61
163
|
}
|
|
@@ -64,11 +166,14 @@ function Spinner() {
|
|
|
64
166
|
|
|
65
167
|
function useMediaQuery(query: string): boolean {
|
|
66
168
|
const [matches, setMatches] = useState(
|
|
67
|
-
() =>
|
|
169
|
+
() =>
|
|
170
|
+
typeof window !== 'undefined' &&
|
|
171
|
+
window.matchMedia(query).matches
|
|
68
172
|
);
|
|
69
173
|
useEffect(() => {
|
|
70
174
|
const mq = window.matchMedia(query);
|
|
71
|
-
const handler = (e: MediaQueryListEvent) =>
|
|
175
|
+
const handler = (e: MediaQueryListEvent) =>
|
|
176
|
+
setMatches(e.matches);
|
|
72
177
|
mq.addEventListener('change', handler);
|
|
73
178
|
return () => mq.removeEventListener('change', handler);
|
|
74
179
|
}, [query]);
|
|
@@ -88,7 +193,10 @@ function useModalAnimation(open: boolean): Phase {
|
|
|
88
193
|
}
|
|
89
194
|
if (phase === 'entered' || phase === 'entering') {
|
|
90
195
|
setPhase('exiting');
|
|
91
|
-
const timer = setTimeout(
|
|
196
|
+
const timer = setTimeout(
|
|
197
|
+
() => setPhase('exited'),
|
|
198
|
+
MOBILE_TRANSITION_MS
|
|
199
|
+
);
|
|
92
200
|
return () => clearTimeout(timer);
|
|
93
201
|
}
|
|
94
202
|
return undefined;
|
|
@@ -117,7 +225,6 @@ function useFocusTrap(
|
|
|
117
225
|
);
|
|
118
226
|
(firstInput ?? container).focus();
|
|
119
227
|
};
|
|
120
|
-
// Small delay to let the portal render
|
|
121
228
|
const raf = requestAnimationFrame(focusFirst);
|
|
122
229
|
|
|
123
230
|
const handleKeyDown = (e: KeyboardEvent) => {
|
|
@@ -127,16 +234,20 @@ function useFocusTrap(
|
|
|
127
234
|
return;
|
|
128
235
|
}
|
|
129
236
|
if (e.key !== 'Tab') return;
|
|
130
|
-
const focusable =
|
|
131
|
-
|
|
132
|
-
|
|
237
|
+
const focusable =
|
|
238
|
+
container.querySelectorAll<HTMLElement>(
|
|
239
|
+
'button:not([disabled]), input:not([disabled]), [tabindex]:not([tabindex="-1"])'
|
|
240
|
+
);
|
|
133
241
|
if (focusable.length === 0) return;
|
|
134
242
|
const first = focusable[0];
|
|
135
243
|
const last = focusable[focusable.length - 1];
|
|
136
244
|
if (e.shiftKey && document.activeElement === first) {
|
|
137
245
|
e.preventDefault();
|
|
138
246
|
last.focus();
|
|
139
|
-
} else if (
|
|
247
|
+
} else if (
|
|
248
|
+
!e.shiftKey &&
|
|
249
|
+
document.activeElement === last
|
|
250
|
+
) {
|
|
140
251
|
e.preventDefault();
|
|
141
252
|
first.focus();
|
|
142
253
|
}
|
|
@@ -157,7 +268,8 @@ function useScrollLock(active: boolean) {
|
|
|
157
268
|
useEffect(() => {
|
|
158
269
|
if (!active) return;
|
|
159
270
|
const scrollbarWidth =
|
|
160
|
-
window.innerWidth -
|
|
271
|
+
window.innerWidth -
|
|
272
|
+
document.documentElement.clientWidth;
|
|
161
273
|
const prevOverflow = document.body.style.overflow;
|
|
162
274
|
const prevPaddingRight = document.body.style.paddingRight;
|
|
163
275
|
document.body.style.overflow = 'hidden';
|
|
@@ -173,17 +285,41 @@ function useScrollLock(active: boolean) {
|
|
|
173
285
|
|
|
174
286
|
// --- Component ---
|
|
175
287
|
|
|
176
|
-
export function DauthProfileModal({
|
|
177
|
-
|
|
288
|
+
export function DauthProfileModal({
|
|
289
|
+
open,
|
|
290
|
+
onClose,
|
|
291
|
+
onAvatarUpload,
|
|
292
|
+
}: DauthProfileModalProps) {
|
|
293
|
+
const {
|
|
294
|
+
user,
|
|
295
|
+
domain,
|
|
296
|
+
updateUser,
|
|
297
|
+
deleteAccount,
|
|
298
|
+
logout,
|
|
299
|
+
getPasskeyCredentials,
|
|
300
|
+
registerPasskey,
|
|
301
|
+
deletePasskeyCredential,
|
|
302
|
+
} = useDauth();
|
|
178
303
|
const isDesktop = useMediaQuery('(min-width: 641px)');
|
|
179
304
|
const phase = useModalAnimation(open);
|
|
180
305
|
const modalRef = useRef<HTMLDivElement>(null);
|
|
306
|
+
const avatarInputRef = useRef<HTMLInputElement>(null);
|
|
181
307
|
|
|
182
|
-
//
|
|
308
|
+
// Tab state
|
|
309
|
+
const showSecurity = domain.authMethods?.passkey === true;
|
|
310
|
+
const [activeTab, setActiveTab] = useState<Tab>('profile');
|
|
311
|
+
|
|
312
|
+
// Profile form state
|
|
183
313
|
const [name, setName] = useState('');
|
|
184
314
|
const [lastname, setLastname] = useState('');
|
|
185
315
|
const [nickname, setNickname] = useState('');
|
|
186
316
|
const [country, setCountry] = useState('');
|
|
317
|
+
const [telPrefix, setTelPrefix] = useState('');
|
|
318
|
+
const [telSuffix, setTelSuffix] = useState('');
|
|
319
|
+
const [birthDate, setBirthDate] = useState('');
|
|
320
|
+
const [customFieldValues, setCustomFieldValues] = useState<
|
|
321
|
+
Record<string, string>
|
|
322
|
+
>({});
|
|
187
323
|
const [populated, setPopulated] = useState(false);
|
|
188
324
|
|
|
189
325
|
// Status
|
|
@@ -198,6 +334,23 @@ export function DauthProfileModal({ open, onClose }: DauthProfileModalProps) {
|
|
|
198
334
|
const [deleteText, setDeleteText] = useState('');
|
|
199
335
|
const [deleting, setDeleting] = useState(false);
|
|
200
336
|
|
|
337
|
+
// Passkey state
|
|
338
|
+
const [credentials, setCredentials] = useState<
|
|
339
|
+
IPasskeyCredential[]
|
|
340
|
+
>([]);
|
|
341
|
+
const [loadingCreds, setLoadingCreds] = useState(false);
|
|
342
|
+
const [showRegister, setShowRegister] = useState(false);
|
|
343
|
+
const [passkeyName, setPasskeyName] = useState('');
|
|
344
|
+
const [registering, setRegistering] = useState(false);
|
|
345
|
+
const [passkeyStatus, setPasskeyStatus] = useState<{
|
|
346
|
+
type: 'success' | 'error';
|
|
347
|
+
message: string;
|
|
348
|
+
} | null>(null);
|
|
349
|
+
|
|
350
|
+
// Avatar upload state
|
|
351
|
+
const [uploadingAvatar, setUploadingAvatar] =
|
|
352
|
+
useState(false);
|
|
353
|
+
|
|
201
354
|
// Populate form when modal opens
|
|
202
355
|
useEffect(() => {
|
|
203
356
|
if (open && user?._id && !populated) {
|
|
@@ -205,6 +358,20 @@ export function DauthProfileModal({ open, onClose }: DauthProfileModalProps) {
|
|
|
205
358
|
setLastname(user.lastname || '');
|
|
206
359
|
setNickname(user.nickname || '');
|
|
207
360
|
setCountry(user.country || '');
|
|
361
|
+
setTelPrefix(user.telPrefix || '');
|
|
362
|
+
setTelSuffix(user.telSuffix || '');
|
|
363
|
+
setBirthDate(
|
|
364
|
+
user.birthDate
|
|
365
|
+
? new Date(user.birthDate)
|
|
366
|
+
.toISOString()
|
|
367
|
+
.split('T')[0]
|
|
368
|
+
: ''
|
|
369
|
+
);
|
|
370
|
+
const cf: Record<string, string> = {};
|
|
371
|
+
for (const f of domain.customFields ?? []) {
|
|
372
|
+
cf[f.key] = user.customFields?.[f.key] ?? '';
|
|
373
|
+
}
|
|
374
|
+
setCustomFieldValues(cf);
|
|
208
375
|
setPopulated(true);
|
|
209
376
|
}
|
|
210
377
|
if (!open) {
|
|
@@ -212,50 +379,110 @@ export function DauthProfileModal({ open, onClose }: DauthProfileModalProps) {
|
|
|
212
379
|
setStatus(null);
|
|
213
380
|
setShowDelete(false);
|
|
214
381
|
setDeleteText('');
|
|
382
|
+
setActiveTab('profile');
|
|
383
|
+
setPasskeyStatus(null);
|
|
384
|
+
setShowRegister(false);
|
|
385
|
+
setPasskeyName('');
|
|
215
386
|
}
|
|
216
387
|
}, [open, user, populated]);
|
|
217
388
|
|
|
389
|
+
// Fetch passkey credentials when Security tab is active
|
|
390
|
+
useEffect(() => {
|
|
391
|
+
if (activeTab !== 'security' || !showSecurity) return;
|
|
392
|
+
setLoadingCreds(true);
|
|
393
|
+
getPasskeyCredentials().then((creds) => {
|
|
394
|
+
setCredentials(creds);
|
|
395
|
+
setLoadingCreds(false);
|
|
396
|
+
});
|
|
397
|
+
}, [activeTab, showSecurity, getPasskeyCredentials]);
|
|
398
|
+
|
|
218
399
|
// Auto-clear success message
|
|
219
400
|
useEffect(() => {
|
|
220
401
|
if (status?.type !== 'success') return;
|
|
221
|
-
const timer = setTimeout(
|
|
402
|
+
const timer = setTimeout(
|
|
403
|
+
() => setStatus(null),
|
|
404
|
+
SUCCESS_TIMEOUT_MS
|
|
405
|
+
);
|
|
222
406
|
return () => clearTimeout(timer);
|
|
223
407
|
}, [status]);
|
|
224
408
|
|
|
409
|
+
useEffect(() => {
|
|
410
|
+
if (passkeyStatus?.type !== 'success') return;
|
|
411
|
+
const timer = setTimeout(
|
|
412
|
+
() => setPasskeyStatus(null),
|
|
413
|
+
SUCCESS_TIMEOUT_MS
|
|
414
|
+
);
|
|
415
|
+
return () => clearTimeout(timer);
|
|
416
|
+
}, [passkeyStatus]);
|
|
417
|
+
|
|
225
418
|
useFocusTrap(modalRef, phase === 'entered', onClose);
|
|
226
419
|
useScrollLock(phase !== 'exited');
|
|
227
420
|
|
|
228
421
|
const hasField = useCallback(
|
|
229
422
|
(field: string) =>
|
|
230
|
-
domain.formFields?.some((f) => f.field === field) ??
|
|
423
|
+
domain.formFields?.some((f) => f.field === field) ??
|
|
424
|
+
false,
|
|
231
425
|
[domain.formFields]
|
|
232
426
|
);
|
|
233
427
|
|
|
234
428
|
const isRequired = useCallback(
|
|
235
429
|
(field: string) =>
|
|
236
|
-
domain.formFields?.find((f) => f.field === field)
|
|
430
|
+
domain.formFields?.find((f) => f.field === field)
|
|
431
|
+
?.required ?? false,
|
|
237
432
|
[domain.formFields]
|
|
238
433
|
);
|
|
239
434
|
|
|
240
435
|
const hasChanges = useMemo(() => {
|
|
241
436
|
if (!user?._id) return false;
|
|
437
|
+
const origBirthDate = user.birthDate
|
|
438
|
+
? new Date(user.birthDate).toISOString().split('T')[0]
|
|
439
|
+
: '';
|
|
440
|
+
const cfChanged = (domain.customFields ?? []).some(
|
|
441
|
+
(f) =>
|
|
442
|
+
(customFieldValues[f.key] ?? '') !==
|
|
443
|
+
(user.customFields?.[f.key] ?? '')
|
|
444
|
+
);
|
|
242
445
|
return (
|
|
243
446
|
name !== (user.name || '') ||
|
|
244
447
|
lastname !== (user.lastname || '') ||
|
|
245
448
|
nickname !== (user.nickname || '') ||
|
|
246
|
-
country !== (user.country || '')
|
|
449
|
+
country !== (user.country || '') ||
|
|
450
|
+
telPrefix !== (user.telPrefix || '') ||
|
|
451
|
+
telSuffix !== (user.telSuffix || '') ||
|
|
452
|
+
birthDate !== origBirthDate ||
|
|
453
|
+
cfChanged
|
|
247
454
|
);
|
|
248
|
-
}, [
|
|
249
|
-
|
|
250
|
-
|
|
455
|
+
}, [
|
|
456
|
+
name,
|
|
457
|
+
lastname,
|
|
458
|
+
nickname,
|
|
459
|
+
country,
|
|
460
|
+
telPrefix,
|
|
461
|
+
telSuffix,
|
|
462
|
+
birthDate,
|
|
463
|
+
customFieldValues,
|
|
464
|
+
user,
|
|
465
|
+
domain.customFields,
|
|
466
|
+
]);
|
|
467
|
+
|
|
468
|
+
const canSave =
|
|
469
|
+
name.trim().length > 0 && hasChanges && !saving;
|
|
251
470
|
|
|
252
471
|
const handleSave = useCallback(async () => {
|
|
253
472
|
setSaving(true);
|
|
254
473
|
setStatus(null);
|
|
255
|
-
const fields: Record<string,
|
|
474
|
+
const fields: Record<string, any> = { name };
|
|
256
475
|
if (hasField('lastname')) fields.lastname = lastname;
|
|
257
476
|
if (hasField('nickname')) fields.nickname = nickname;
|
|
258
477
|
if (hasField('country')) fields.country = country;
|
|
478
|
+
if (hasField('tel_prefix'))
|
|
479
|
+
fields.telPrefix = telPrefix;
|
|
480
|
+
if (hasField('tel_suffix'))
|
|
481
|
+
fields.telSuffix = telSuffix;
|
|
482
|
+
if (hasField('birth_date') && birthDate)
|
|
483
|
+
fields.birthDate = birthDate;
|
|
484
|
+
if ((domain.customFields ?? []).length > 0)
|
|
485
|
+
fields.customFields = customFieldValues;
|
|
259
486
|
const ok = await updateUser(fields);
|
|
260
487
|
setSaving(false);
|
|
261
488
|
if (ok) {
|
|
@@ -269,7 +496,19 @@ export function DauthProfileModal({ open, onClose }: DauthProfileModalProps) {
|
|
|
269
496
|
message: 'Something went wrong. Please try again.',
|
|
270
497
|
});
|
|
271
498
|
}
|
|
272
|
-
}, [
|
|
499
|
+
}, [
|
|
500
|
+
name,
|
|
501
|
+
lastname,
|
|
502
|
+
nickname,
|
|
503
|
+
country,
|
|
504
|
+
telPrefix,
|
|
505
|
+
telSuffix,
|
|
506
|
+
birthDate,
|
|
507
|
+
customFieldValues,
|
|
508
|
+
hasField,
|
|
509
|
+
updateUser,
|
|
510
|
+
domain.customFields,
|
|
511
|
+
]);
|
|
273
512
|
|
|
274
513
|
const handleDelete = useCallback(async () => {
|
|
275
514
|
setDeleting(true);
|
|
@@ -280,24 +519,103 @@ export function DauthProfileModal({ open, onClose }: DauthProfileModalProps) {
|
|
|
280
519
|
} else {
|
|
281
520
|
setStatus({
|
|
282
521
|
type: 'error',
|
|
283
|
-
message:
|
|
522
|
+
message:
|
|
523
|
+
'Could not delete account. Please try again.',
|
|
284
524
|
});
|
|
285
525
|
setShowDelete(false);
|
|
286
526
|
setDeleteText('');
|
|
287
527
|
}
|
|
288
528
|
}, [deleteAccount, onClose]);
|
|
289
529
|
|
|
530
|
+
const handleLanguage = useCallback(
|
|
531
|
+
async (lang: string) => {
|
|
532
|
+
await updateUser({ language: lang } as any);
|
|
533
|
+
},
|
|
534
|
+
[updateUser]
|
|
535
|
+
);
|
|
536
|
+
|
|
537
|
+
const handleRegisterPasskey = useCallback(async () => {
|
|
538
|
+
setRegistering(true);
|
|
539
|
+
setPasskeyStatus(null);
|
|
540
|
+
const cred = await registerPasskey(
|
|
541
|
+
passkeyName || undefined
|
|
542
|
+
);
|
|
543
|
+
setRegistering(false);
|
|
544
|
+
if (cred) {
|
|
545
|
+
setCredentials((prev) => [...prev, cred]);
|
|
546
|
+
setPasskeyName('');
|
|
547
|
+
setShowRegister(false);
|
|
548
|
+
setPasskeyStatus({
|
|
549
|
+
type: 'success',
|
|
550
|
+
message: 'Passkey registered successfully',
|
|
551
|
+
});
|
|
552
|
+
} else {
|
|
553
|
+
setPasskeyStatus({
|
|
554
|
+
type: 'error',
|
|
555
|
+
message: 'Failed to register passkey',
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
}, [passkeyName, registerPasskey]);
|
|
559
|
+
|
|
560
|
+
const handleDeletePasskey = useCallback(
|
|
561
|
+
async (credentialId: string) => {
|
|
562
|
+
const ok = await deletePasskeyCredential(credentialId);
|
|
563
|
+
if (ok) {
|
|
564
|
+
setCredentials((prev) =>
|
|
565
|
+
prev.filter((c) => c._id !== credentialId)
|
|
566
|
+
);
|
|
567
|
+
}
|
|
568
|
+
},
|
|
569
|
+
[deletePasskeyCredential]
|
|
570
|
+
);
|
|
571
|
+
|
|
572
|
+
const handleAvatarClick = useCallback(() => {
|
|
573
|
+
if (onAvatarUpload) {
|
|
574
|
+
avatarInputRef.current?.click();
|
|
575
|
+
}
|
|
576
|
+
}, [onAvatarUpload]);
|
|
577
|
+
|
|
578
|
+
const handleAvatarChange = useCallback(
|
|
579
|
+
async (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
580
|
+
const file = e.target.files?.[0];
|
|
581
|
+
if (!file || !onAvatarUpload) return;
|
|
582
|
+
setUploadingAvatar(true);
|
|
583
|
+
try {
|
|
584
|
+
const url = await onAvatarUpload(file);
|
|
585
|
+
if (url) {
|
|
586
|
+
await updateUser({ avatar: url } as any);
|
|
587
|
+
}
|
|
588
|
+
} catch {
|
|
589
|
+
// Error handled by onError callback
|
|
590
|
+
}
|
|
591
|
+
setUploadingAvatar(false);
|
|
592
|
+
if (avatarInputRef.current) {
|
|
593
|
+
avatarInputRef.current.value = '';
|
|
594
|
+
}
|
|
595
|
+
},
|
|
596
|
+
[onAvatarUpload, updateUser]
|
|
597
|
+
);
|
|
598
|
+
|
|
599
|
+
const handleSignOut = useCallback(() => {
|
|
600
|
+
logout();
|
|
601
|
+
onClose();
|
|
602
|
+
}, [logout, onClose]);
|
|
603
|
+
|
|
290
604
|
// Build CSS custom property overrides from domain.modalTheme
|
|
291
605
|
const themeVars = useMemo(() => {
|
|
292
606
|
const t = domain.modalTheme;
|
|
293
607
|
if (!t) return {};
|
|
294
608
|
const vars: Record<string, string> = {};
|
|
295
609
|
if (t.accent) vars['--dauth-accent'] = t.accent;
|
|
296
|
-
if (t.accentHover)
|
|
610
|
+
if (t.accentHover)
|
|
611
|
+
vars['--dauth-accent-hover'] = t.accentHover;
|
|
297
612
|
if (t.surface) vars['--dauth-surface'] = t.surface;
|
|
298
|
-
if (t.surfaceHover)
|
|
299
|
-
|
|
300
|
-
if (t.
|
|
613
|
+
if (t.surfaceHover)
|
|
614
|
+
vars['--dauth-surface-hover'] = t.surfaceHover;
|
|
615
|
+
if (t.textPrimary)
|
|
616
|
+
vars['--dauth-text-primary'] = t.textPrimary;
|
|
617
|
+
if (t.textSecondary)
|
|
618
|
+
vars['--dauth-text-secondary'] = t.textSecondary;
|
|
301
619
|
if (t.border) vars['--dauth-border'] = t.border;
|
|
302
620
|
return vars;
|
|
303
621
|
}, [domain.modalTheme]);
|
|
@@ -311,7 +629,8 @@ export function DauthProfileModal({ open, onClose }: DauthProfileModalProps) {
|
|
|
311
629
|
position: 'fixed',
|
|
312
630
|
inset: 0,
|
|
313
631
|
zIndex: 2147483647,
|
|
314
|
-
backgroundColor:
|
|
632
|
+
backgroundColor:
|
|
633
|
+
'var(--dauth-backdrop, rgba(0, 0, 0, 0.6))',
|
|
315
634
|
backdropFilter: 'blur(4px)',
|
|
316
635
|
WebkitBackdropFilter: 'blur(4px)',
|
|
317
636
|
display: 'flex',
|
|
@@ -329,8 +648,10 @@ export function DauthProfileModal({ open, onClose }: DauthProfileModalProps) {
|
|
|
329
648
|
margin: 16,
|
|
330
649
|
backgroundColor: 'var(--dauth-surface, #1a1a2e)',
|
|
331
650
|
borderRadius: 'var(--dauth-radius, 12px)',
|
|
332
|
-
boxShadow:
|
|
333
|
-
|
|
651
|
+
boxShadow:
|
|
652
|
+
'var(--dauth-shadow, 0 25px 50px -12px rgba(0, 0, 0, 0.5))',
|
|
653
|
+
border:
|
|
654
|
+
'1px solid var(--dauth-border, rgba(255, 255, 255, 0.08))',
|
|
334
655
|
display: 'flex',
|
|
335
656
|
flexDirection: 'column',
|
|
336
657
|
overflow: 'hidden',
|
|
@@ -338,7 +659,10 @@ export function DauthProfileModal({ open, onClose }: DauthProfileModalProps) {
|
|
|
338
659
|
'var(--dauth-font-family, system-ui, -apple-system, sans-serif)',
|
|
339
660
|
color: 'var(--dauth-text-primary, #e4e4e7)',
|
|
340
661
|
opacity: phase === 'entered' ? 1 : 0,
|
|
341
|
-
transform:
|
|
662
|
+
transform:
|
|
663
|
+
phase === 'entered'
|
|
664
|
+
? 'translateY(0)'
|
|
665
|
+
: 'translateY(16px)',
|
|
342
666
|
transition: `opacity ${dur}ms ${easing}, transform ${dur}ms ${easing}`,
|
|
343
667
|
};
|
|
344
668
|
|
|
@@ -351,7 +675,10 @@ export function DauthProfileModal({ open, onClose }: DauthProfileModalProps) {
|
|
|
351
675
|
fontFamily:
|
|
352
676
|
'var(--dauth-font-family, system-ui, -apple-system, sans-serif)',
|
|
353
677
|
color: 'var(--dauth-text-primary, #e4e4e7)',
|
|
354
|
-
transform:
|
|
678
|
+
transform:
|
|
679
|
+
phase === 'entered'
|
|
680
|
+
? 'translateY(0)'
|
|
681
|
+
: 'translateY(100%)',
|
|
355
682
|
transition: `transform ${dur}ms ${easing}`,
|
|
356
683
|
};
|
|
357
684
|
|
|
@@ -359,11 +686,20 @@ export function DauthProfileModal({ open, onClose }: DauthProfileModalProps) {
|
|
|
359
686
|
.charAt(0)
|
|
360
687
|
.toUpperCase();
|
|
361
688
|
|
|
689
|
+
const tabs: { key: Tab; label: string }[] = [
|
|
690
|
+
{ key: 'profile', label: 'Profile' },
|
|
691
|
+
...(showSecurity
|
|
692
|
+
? [{ key: 'security' as Tab, label: 'Security' }]
|
|
693
|
+
: []),
|
|
694
|
+
{ key: 'account', label: 'Account' },
|
|
695
|
+
];
|
|
696
|
+
|
|
362
697
|
return createPortal(
|
|
363
698
|
<>
|
|
364
699
|
<style
|
|
365
700
|
dangerouslySetInnerHTML={{
|
|
366
|
-
__html:
|
|
701
|
+
__html:
|
|
702
|
+
'@keyframes dauth-spin{to{transform:rotate(360deg)}}',
|
|
367
703
|
}}
|
|
368
704
|
/>
|
|
369
705
|
<div
|
|
@@ -392,7 +728,8 @@ export function DauthProfileModal({ open, onClose }: DauthProfileModalProps) {
|
|
|
392
728
|
'var(--dauth-surface-hover, #232340)')
|
|
393
729
|
}
|
|
394
730
|
onMouseLeave={(e) =>
|
|
395
|
-
(e.currentTarget.style.backgroundColor =
|
|
731
|
+
(e.currentTarget.style.backgroundColor =
|
|
732
|
+
'transparent')
|
|
396
733
|
}
|
|
397
734
|
>
|
|
398
735
|
{isDesktop ? <IconClose /> : <IconBack />}
|
|
@@ -403,231 +740,770 @@ export function DauthProfileModal({ open, onClose }: DauthProfileModalProps) {
|
|
|
403
740
|
<div style={{ width: 36 }} />
|
|
404
741
|
</div>
|
|
405
742
|
|
|
743
|
+
{/* Tab bar */}
|
|
744
|
+
<div style={tabBar} role="tablist">
|
|
745
|
+
{tabs.map((t) => (
|
|
746
|
+
<button
|
|
747
|
+
key={t.key}
|
|
748
|
+
role="tab"
|
|
749
|
+
type="button"
|
|
750
|
+
aria-selected={activeTab === t.key}
|
|
751
|
+
style={{
|
|
752
|
+
...tabBtn,
|
|
753
|
+
color:
|
|
754
|
+
activeTab === t.key
|
|
755
|
+
? 'var(--dauth-accent, #6366f1)'
|
|
756
|
+
: 'var(--dauth-text-secondary, #a1a1aa)',
|
|
757
|
+
borderBottomColor:
|
|
758
|
+
activeTab === t.key
|
|
759
|
+
? 'var(--dauth-accent, #6366f1)'
|
|
760
|
+
: 'transparent',
|
|
761
|
+
}}
|
|
762
|
+
onClick={() => setActiveTab(t.key)}
|
|
763
|
+
>
|
|
764
|
+
{t.label}
|
|
765
|
+
</button>
|
|
766
|
+
))}
|
|
767
|
+
</div>
|
|
768
|
+
|
|
406
769
|
{/* Scrollable body */}
|
|
407
770
|
<div style={bodyStyle}>
|
|
408
|
-
{/*
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
{
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
alt=""
|
|
771
|
+
{/* ========== PROFILE TAB ========== */}
|
|
772
|
+
{activeTab === 'profile' && (
|
|
773
|
+
<>
|
|
774
|
+
{/* Avatar */}
|
|
775
|
+
<div style={avatarSection}>
|
|
776
|
+
<div
|
|
415
777
|
style={{
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
778
|
+
...avatarCircle,
|
|
779
|
+
cursor: onAvatarUpload
|
|
780
|
+
? 'pointer'
|
|
781
|
+
: 'default',
|
|
782
|
+
position: 'relative' as const,
|
|
419
783
|
}}
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
784
|
+
onClick={handleAvatarClick}
|
|
785
|
+
>
|
|
786
|
+
{uploadingAvatar ? (
|
|
787
|
+
<Spinner />
|
|
788
|
+
) : user.avatar?.url ? (
|
|
789
|
+
<img
|
|
790
|
+
src={user.avatar.url}
|
|
791
|
+
alt=""
|
|
792
|
+
style={{
|
|
793
|
+
width: '100%',
|
|
794
|
+
height: '100%',
|
|
795
|
+
objectFit: 'cover',
|
|
796
|
+
}}
|
|
797
|
+
/>
|
|
798
|
+
) : (
|
|
799
|
+
avatarInitial
|
|
800
|
+
)}
|
|
801
|
+
{onAvatarUpload && !uploadingAvatar && (
|
|
802
|
+
<div style={avatarOverlay}>
|
|
803
|
+
<IconCamera />
|
|
804
|
+
</div>
|
|
805
|
+
)}
|
|
806
|
+
</div>
|
|
807
|
+
<div style={emailText}>{user.email}</div>
|
|
808
|
+
{onAvatarUpload && (
|
|
809
|
+
<input
|
|
810
|
+
ref={avatarInputRef}
|
|
811
|
+
type="file"
|
|
812
|
+
accept="image/*"
|
|
813
|
+
style={{ display: 'none' }}
|
|
814
|
+
onChange={handleAvatarChange}
|
|
815
|
+
/>
|
|
816
|
+
)}
|
|
817
|
+
</div>
|
|
818
|
+
|
|
819
|
+
{/* Status */}
|
|
820
|
+
{status && (
|
|
821
|
+
<div
|
|
822
|
+
role="status"
|
|
823
|
+
aria-live="polite"
|
|
824
|
+
style={statusMsg(status.type)}
|
|
825
|
+
>
|
|
826
|
+
{status.message}
|
|
827
|
+
</div>
|
|
423
828
|
)}
|
|
424
|
-
</div>
|
|
425
|
-
<div style={emailText}>{user.email}</div>
|
|
426
|
-
</div>
|
|
427
829
|
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
830
|
+
{/* Form */}
|
|
831
|
+
<div>
|
|
832
|
+
<div style={fieldGroup}>
|
|
833
|
+
<label
|
|
834
|
+
htmlFor="dauth-name"
|
|
835
|
+
style={label}
|
|
836
|
+
>
|
|
837
|
+
Name *
|
|
838
|
+
</label>
|
|
839
|
+
<input
|
|
840
|
+
id="dauth-name"
|
|
841
|
+
type="text"
|
|
842
|
+
value={name}
|
|
843
|
+
onChange={(e) =>
|
|
844
|
+
setName(e.target.value)
|
|
845
|
+
}
|
|
846
|
+
placeholder="Your name"
|
|
847
|
+
disabled={saving}
|
|
848
|
+
style={input}
|
|
849
|
+
onFocus={inputFocusHandler}
|
|
850
|
+
onBlur={inputBlurHandler}
|
|
851
|
+
/>
|
|
852
|
+
</div>
|
|
438
853
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
854
|
+
{hasField('lastname') && (
|
|
855
|
+
<div style={fieldGroup}>
|
|
856
|
+
<label
|
|
857
|
+
htmlFor="dauth-lastname"
|
|
858
|
+
style={label}
|
|
859
|
+
>
|
|
860
|
+
Last name
|
|
861
|
+
{isRequired('lastname') ? ' *' : ''}
|
|
862
|
+
</label>
|
|
863
|
+
<input
|
|
864
|
+
id="dauth-lastname"
|
|
865
|
+
type="text"
|
|
866
|
+
value={lastname}
|
|
867
|
+
onChange={(e) =>
|
|
868
|
+
setLastname(e.target.value)
|
|
869
|
+
}
|
|
870
|
+
placeholder="Your last name"
|
|
871
|
+
disabled={saving}
|
|
872
|
+
style={input}
|
|
873
|
+
onFocus={inputFocusHandler}
|
|
874
|
+
onBlur={inputBlurHandler}
|
|
875
|
+
/>
|
|
876
|
+
</div>
|
|
877
|
+
)}
|
|
878
|
+
|
|
879
|
+
{hasField('nickname') && (
|
|
880
|
+
<div style={fieldGroup}>
|
|
881
|
+
<label
|
|
882
|
+
htmlFor="dauth-nickname"
|
|
883
|
+
style={label}
|
|
884
|
+
>
|
|
885
|
+
Nickname
|
|
886
|
+
{isRequired('nickname') ? ' *' : ''}
|
|
887
|
+
</label>
|
|
888
|
+
<input
|
|
889
|
+
id="dauth-nickname"
|
|
890
|
+
type="text"
|
|
891
|
+
value={nickname}
|
|
892
|
+
onChange={(e) =>
|
|
893
|
+
setNickname(e.target.value)
|
|
894
|
+
}
|
|
895
|
+
placeholder="Choose a nickname"
|
|
896
|
+
disabled={saving}
|
|
897
|
+
style={input}
|
|
898
|
+
onFocus={inputFocusHandler}
|
|
899
|
+
onBlur={inputBlurHandler}
|
|
900
|
+
/>
|
|
901
|
+
</div>
|
|
902
|
+
)}
|
|
903
|
+
|
|
904
|
+
{hasField('country') && (
|
|
905
|
+
<div style={fieldGroup}>
|
|
906
|
+
<label
|
|
907
|
+
htmlFor="dauth-country"
|
|
908
|
+
style={label}
|
|
909
|
+
>
|
|
910
|
+
Country
|
|
911
|
+
{isRequired('country') ? ' *' : ''}
|
|
912
|
+
</label>
|
|
913
|
+
<input
|
|
914
|
+
id="dauth-country"
|
|
915
|
+
type="text"
|
|
916
|
+
value={country}
|
|
917
|
+
onChange={(e) =>
|
|
918
|
+
setCountry(e.target.value)
|
|
919
|
+
}
|
|
920
|
+
placeholder="Your country"
|
|
921
|
+
disabled={saving}
|
|
922
|
+
style={input}
|
|
923
|
+
onFocus={inputFocusHandler}
|
|
924
|
+
onBlur={inputBlurHandler}
|
|
925
|
+
/>
|
|
926
|
+
</div>
|
|
927
|
+
)}
|
|
928
|
+
|
|
929
|
+
{(hasField('tel_prefix') ||
|
|
930
|
+
hasField('tel_suffix')) && (
|
|
931
|
+
<div style={fieldGroup}>
|
|
932
|
+
<div style={label}>
|
|
933
|
+
Phone
|
|
934
|
+
{isRequired('tel_prefix') ||
|
|
935
|
+
isRequired('tel_suffix')
|
|
936
|
+
? ' *'
|
|
937
|
+
: ''}
|
|
938
|
+
</div>
|
|
939
|
+
<div
|
|
940
|
+
style={{
|
|
941
|
+
display: 'flex',
|
|
942
|
+
gap: 8,
|
|
943
|
+
}}
|
|
944
|
+
>
|
|
945
|
+
{hasField('tel_prefix') && (
|
|
946
|
+
<input
|
|
947
|
+
id="dauth-tel-prefix"
|
|
948
|
+
type="text"
|
|
949
|
+
value={telPrefix}
|
|
950
|
+
onChange={(e) =>
|
|
951
|
+
setTelPrefix(e.target.value)
|
|
952
|
+
}
|
|
953
|
+
placeholder="+34"
|
|
954
|
+
disabled={saving}
|
|
955
|
+
style={{
|
|
956
|
+
...input,
|
|
957
|
+
width: 80,
|
|
958
|
+
flexShrink: 0,
|
|
959
|
+
}}
|
|
960
|
+
onFocus={inputFocusHandler}
|
|
961
|
+
onBlur={inputBlurHandler}
|
|
962
|
+
aria-label="Phone prefix"
|
|
963
|
+
/>
|
|
964
|
+
)}
|
|
965
|
+
{hasField('tel_suffix') && (
|
|
966
|
+
<input
|
|
967
|
+
id="dauth-tel-suffix"
|
|
968
|
+
type="tel"
|
|
969
|
+
value={telSuffix}
|
|
970
|
+
onChange={(e) =>
|
|
971
|
+
setTelSuffix(e.target.value)
|
|
972
|
+
}
|
|
973
|
+
placeholder="612 345 678"
|
|
974
|
+
disabled={saving}
|
|
975
|
+
style={{
|
|
976
|
+
...input,
|
|
977
|
+
flex: 1,
|
|
978
|
+
}}
|
|
979
|
+
onFocus={inputFocusHandler}
|
|
980
|
+
onBlur={inputBlurHandler}
|
|
981
|
+
aria-label="Phone number"
|
|
982
|
+
/>
|
|
983
|
+
)}
|
|
984
|
+
</div>
|
|
985
|
+
</div>
|
|
986
|
+
)}
|
|
987
|
+
|
|
988
|
+
{hasField('birth_date') && (
|
|
989
|
+
<div style={fieldGroup}>
|
|
990
|
+
<label
|
|
991
|
+
htmlFor="dauth-birthdate"
|
|
992
|
+
style={label}
|
|
993
|
+
>
|
|
994
|
+
Birth date
|
|
995
|
+
{isRequired('birth_date')
|
|
996
|
+
? ' *'
|
|
997
|
+
: ''}
|
|
998
|
+
</label>
|
|
999
|
+
<input
|
|
1000
|
+
id="dauth-birthdate"
|
|
1001
|
+
type="date"
|
|
1002
|
+
value={birthDate}
|
|
1003
|
+
onChange={(e) =>
|
|
1004
|
+
setBirthDate(e.target.value)
|
|
1005
|
+
}
|
|
1006
|
+
disabled={saving}
|
|
1007
|
+
style={input}
|
|
1008
|
+
onFocus={inputFocusHandler}
|
|
1009
|
+
onBlur={inputBlurHandler}
|
|
1010
|
+
/>
|
|
1011
|
+
</div>
|
|
1012
|
+
)}
|
|
1013
|
+
|
|
1014
|
+
{(domain.customFields ?? []).length >
|
|
1015
|
+
0 && (
|
|
1016
|
+
<>
|
|
1017
|
+
<hr style={separator} />
|
|
1018
|
+
{domain.customFields!.map((cf) => (
|
|
1019
|
+
<div
|
|
1020
|
+
key={cf.key}
|
|
1021
|
+
style={fieldGroup}
|
|
1022
|
+
>
|
|
1023
|
+
<label
|
|
1024
|
+
htmlFor={`dauth-cf-${cf.key}`}
|
|
1025
|
+
style={label}
|
|
1026
|
+
>
|
|
1027
|
+
{cf.label}
|
|
1028
|
+
{cf.required ? ' *' : ''}
|
|
1029
|
+
</label>
|
|
1030
|
+
<input
|
|
1031
|
+
id={`dauth-cf-${cf.key}`}
|
|
1032
|
+
type="text"
|
|
1033
|
+
value={
|
|
1034
|
+
customFieldValues[cf.key] ??
|
|
1035
|
+
''
|
|
1036
|
+
}
|
|
1037
|
+
onChange={(e) =>
|
|
1038
|
+
setCustomFieldValues(
|
|
1039
|
+
(prev) => ({
|
|
1040
|
+
...prev,
|
|
1041
|
+
[cf.key]:
|
|
1042
|
+
e.target.value,
|
|
1043
|
+
})
|
|
1044
|
+
)
|
|
1045
|
+
}
|
|
1046
|
+
disabled={saving}
|
|
1047
|
+
style={input}
|
|
1048
|
+
onFocus={inputFocusHandler}
|
|
1049
|
+
onBlur={inputBlurHandler}
|
|
1050
|
+
/>
|
|
1051
|
+
</div>
|
|
1052
|
+
))}
|
|
1053
|
+
</>
|
|
1054
|
+
)}
|
|
475
1055
|
</div>
|
|
476
|
-
)}
|
|
477
1056
|
|
|
478
|
-
|
|
1057
|
+
{/* Language selector */}
|
|
1058
|
+
<hr style={separator} />
|
|
479
1059
|
<div style={fieldGroup}>
|
|
480
|
-
<
|
|
481
|
-
|
|
482
|
-
{
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
1060
|
+
<div style={label}>Language</div>
|
|
1061
|
+
<div style={{ display: 'flex', gap: 8 }}>
|
|
1062
|
+
{(['es', 'en'] as const).map((lang) => (
|
|
1063
|
+
<button
|
|
1064
|
+
key={lang}
|
|
1065
|
+
type="button"
|
|
1066
|
+
style={{
|
|
1067
|
+
...langBtn,
|
|
1068
|
+
backgroundColor:
|
|
1069
|
+
user.language === lang
|
|
1070
|
+
? 'var(--dauth-accent, #6366f1)'
|
|
1071
|
+
: 'var(--dauth-surface-secondary, rgba(255, 255, 255, 0.04))',
|
|
1072
|
+
color:
|
|
1073
|
+
user.language === lang
|
|
1074
|
+
? '#ffffff'
|
|
1075
|
+
: 'var(--dauth-text-secondary, #a1a1aa)',
|
|
1076
|
+
}}
|
|
1077
|
+
onClick={() => handleLanguage(lang)}
|
|
1078
|
+
>
|
|
1079
|
+
{lang === 'es'
|
|
1080
|
+
? 'Espa\u00f1ol'
|
|
1081
|
+
: 'English'}
|
|
1082
|
+
</button>
|
|
1083
|
+
))}
|
|
1084
|
+
</div>
|
|
495
1085
|
</div>
|
|
496
|
-
|
|
1086
|
+
</>
|
|
1087
|
+
)}
|
|
497
1088
|
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
1089
|
+
{/* ========== SECURITY TAB ========== */}
|
|
1090
|
+
{activeTab === 'security' && showSecurity && (
|
|
1091
|
+
<>
|
|
1092
|
+
<div
|
|
1093
|
+
style={{
|
|
1094
|
+
display: 'flex',
|
|
1095
|
+
alignItems: 'center',
|
|
1096
|
+
justifyContent: 'space-between',
|
|
1097
|
+
marginBottom: 16,
|
|
1098
|
+
}}
|
|
1099
|
+
>
|
|
1100
|
+
<div
|
|
1101
|
+
style={{
|
|
1102
|
+
...label,
|
|
1103
|
+
marginBottom: 0,
|
|
1104
|
+
fontWeight: 600,
|
|
1105
|
+
}}
|
|
1106
|
+
>
|
|
1107
|
+
Passkeys
|
|
1108
|
+
</div>
|
|
1109
|
+
<button
|
|
1110
|
+
type="button"
|
|
1111
|
+
style={outlineBtn}
|
|
1112
|
+
onClick={() =>
|
|
1113
|
+
setShowRegister(!showRegister)
|
|
1114
|
+
}
|
|
1115
|
+
onMouseEnter={(e) =>
|
|
1116
|
+
(e.currentTarget.style.backgroundColor =
|
|
1117
|
+
'var(--dauth-surface-hover, #232340)')
|
|
1118
|
+
}
|
|
1119
|
+
onMouseLeave={(e) =>
|
|
1120
|
+
(e.currentTarget.style.backgroundColor =
|
|
1121
|
+
'transparent')
|
|
1122
|
+
}
|
|
1123
|
+
>
|
|
1124
|
+
+ Add passkey
|
|
1125
|
+
</button>
|
|
515
1126
|
</div>
|
|
516
|
-
)}
|
|
517
|
-
</div>
|
|
518
1127
|
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
1128
|
+
{/* Register passkey form */}
|
|
1129
|
+
{showRegister && (
|
|
1130
|
+
<div style={registerPanel}>
|
|
1131
|
+
<div style={fieldGroup}>
|
|
1132
|
+
<label
|
|
1133
|
+
htmlFor="dauth-passkey-name"
|
|
1134
|
+
style={label}
|
|
1135
|
+
>
|
|
1136
|
+
Passkey name (optional)
|
|
1137
|
+
</label>
|
|
1138
|
+
<input
|
|
1139
|
+
id="dauth-passkey-name"
|
|
1140
|
+
type="text"
|
|
1141
|
+
value={passkeyName}
|
|
1142
|
+
onChange={(e) =>
|
|
1143
|
+
setPasskeyName(e.target.value)
|
|
1144
|
+
}
|
|
1145
|
+
placeholder="e.g. MacBook Touch ID"
|
|
1146
|
+
disabled={registering}
|
|
1147
|
+
style={input}
|
|
1148
|
+
onFocus={inputFocusHandler}
|
|
1149
|
+
onBlur={inputBlurHandler}
|
|
1150
|
+
/>
|
|
1151
|
+
</div>
|
|
1152
|
+
<div
|
|
1153
|
+
style={{
|
|
1154
|
+
display: 'flex',
|
|
1155
|
+
gap: 8,
|
|
1156
|
+
}}
|
|
1157
|
+
>
|
|
1158
|
+
<button
|
|
1159
|
+
type="button"
|
|
1160
|
+
style={{
|
|
1161
|
+
...smallAccentBtn,
|
|
1162
|
+
opacity: registering ? 0.6 : 1,
|
|
1163
|
+
}}
|
|
1164
|
+
disabled={registering}
|
|
1165
|
+
onClick={handleRegisterPasskey}
|
|
1166
|
+
>
|
|
1167
|
+
{registering ? (
|
|
1168
|
+
<Spinner />
|
|
1169
|
+
) : (
|
|
1170
|
+
<IconFingerprint />
|
|
1171
|
+
)}
|
|
1172
|
+
{registering
|
|
1173
|
+
? 'Registering...'
|
|
1174
|
+
: 'Register'}
|
|
1175
|
+
</button>
|
|
1176
|
+
<button
|
|
1177
|
+
type="button"
|
|
1178
|
+
style={cancelBtn}
|
|
1179
|
+
onClick={() =>
|
|
1180
|
+
setShowRegister(false)
|
|
1181
|
+
}
|
|
1182
|
+
onMouseEnter={(e) =>
|
|
1183
|
+
(e.currentTarget.style.backgroundColor =
|
|
1184
|
+
'var(--dauth-surface-hover, #232340)')
|
|
1185
|
+
}
|
|
1186
|
+
onMouseLeave={(e) =>
|
|
1187
|
+
(e.currentTarget.style.backgroundColor =
|
|
1188
|
+
'transparent')
|
|
1189
|
+
}
|
|
1190
|
+
>
|
|
1191
|
+
Cancel
|
|
1192
|
+
</button>
|
|
1193
|
+
</div>
|
|
547
1194
|
</div>
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
placeholder={`Type ${CONFIRM_WORD}`}
|
|
553
|
-
style={input}
|
|
554
|
-
onFocus={inputFocusHandler}
|
|
555
|
-
onBlur={inputBlurHandler}
|
|
556
|
-
disabled={deleting}
|
|
557
|
-
/>
|
|
1195
|
+
)}
|
|
1196
|
+
|
|
1197
|
+
{/* Passkey status */}
|
|
1198
|
+
{passkeyStatus && (
|
|
558
1199
|
<div
|
|
1200
|
+
role="status"
|
|
1201
|
+
aria-live="polite"
|
|
559
1202
|
style={{
|
|
560
|
-
|
|
561
|
-
gap: 8,
|
|
1203
|
+
...statusMsg(passkeyStatus.type),
|
|
562
1204
|
marginTop: 12,
|
|
563
1205
|
}}
|
|
564
1206
|
>
|
|
1207
|
+
{passkeyStatus.message}
|
|
1208
|
+
</div>
|
|
1209
|
+
)}
|
|
1210
|
+
|
|
1211
|
+
{/* Credentials list */}
|
|
1212
|
+
<div style={{ marginTop: 12 }}>
|
|
1213
|
+
{loadingCreds ? (
|
|
1214
|
+
<div
|
|
1215
|
+
style={{
|
|
1216
|
+
textAlign: 'center',
|
|
1217
|
+
padding: 24,
|
|
1218
|
+
}}
|
|
1219
|
+
>
|
|
1220
|
+
<Spinner />
|
|
1221
|
+
</div>
|
|
1222
|
+
) : credentials.length > 0 ? (
|
|
1223
|
+
credentials.map((cred) => (
|
|
1224
|
+
<div
|
|
1225
|
+
key={cred._id}
|
|
1226
|
+
style={credentialRow}
|
|
1227
|
+
>
|
|
1228
|
+
<div
|
|
1229
|
+
style={{
|
|
1230
|
+
display: 'flex',
|
|
1231
|
+
alignItems: 'center',
|
|
1232
|
+
gap: 12,
|
|
1233
|
+
flex: 1,
|
|
1234
|
+
minWidth: 0,
|
|
1235
|
+
}}
|
|
1236
|
+
>
|
|
1237
|
+
<span
|
|
1238
|
+
style={{
|
|
1239
|
+
color:
|
|
1240
|
+
'var(--dauth-accent, #6366f1)',
|
|
1241
|
+
flexShrink: 0,
|
|
1242
|
+
}}
|
|
1243
|
+
>
|
|
1244
|
+
<IconFingerprint />
|
|
1245
|
+
</span>
|
|
1246
|
+
<div
|
|
1247
|
+
style={{
|
|
1248
|
+
minWidth: 0,
|
|
1249
|
+
flex: 1,
|
|
1250
|
+
}}
|
|
1251
|
+
>
|
|
1252
|
+
<div
|
|
1253
|
+
style={{
|
|
1254
|
+
fontSize:
|
|
1255
|
+
'var(--dauth-font-size-sm, 0.875rem)',
|
|
1256
|
+
fontWeight: 500,
|
|
1257
|
+
color:
|
|
1258
|
+
'var(--dauth-text-primary, #e4e4e7)',
|
|
1259
|
+
overflow: 'hidden',
|
|
1260
|
+
textOverflow: 'ellipsis',
|
|
1261
|
+
whiteSpace:
|
|
1262
|
+
'nowrap' as const,
|
|
1263
|
+
}}
|
|
1264
|
+
>
|
|
1265
|
+
{cred.name || 'Passkey'}
|
|
1266
|
+
</div>
|
|
1267
|
+
<div
|
|
1268
|
+
style={{
|
|
1269
|
+
fontSize:
|
|
1270
|
+
'var(--dauth-font-size-xs, 0.75rem)',
|
|
1271
|
+
color:
|
|
1272
|
+
'var(--dauth-text-muted, #71717a)',
|
|
1273
|
+
}}
|
|
1274
|
+
>
|
|
1275
|
+
{cred.deviceType ===
|
|
1276
|
+
'multiDevice'
|
|
1277
|
+
? 'Synced'
|
|
1278
|
+
: 'Device-bound'}
|
|
1279
|
+
{cred.createdAt &&
|
|
1280
|
+
` \u00b7 Created ${new Date(cred.createdAt).toLocaleDateString()}`}
|
|
1281
|
+
</div>
|
|
1282
|
+
</div>
|
|
1283
|
+
</div>
|
|
1284
|
+
<button
|
|
1285
|
+
type="button"
|
|
1286
|
+
onClick={() =>
|
|
1287
|
+
handleDeletePasskey(cred._id)
|
|
1288
|
+
}
|
|
1289
|
+
style={trashBtn}
|
|
1290
|
+
onMouseEnter={(e) =>
|
|
1291
|
+
(e.currentTarget.style.color =
|
|
1292
|
+
'var(--dauth-error, #ef4444)')
|
|
1293
|
+
}
|
|
1294
|
+
onMouseLeave={(e) =>
|
|
1295
|
+
(e.currentTarget.style.color =
|
|
1296
|
+
'var(--dauth-text-muted, #71717a)')
|
|
1297
|
+
}
|
|
1298
|
+
aria-label={`Delete passkey ${cred.name || ''}`}
|
|
1299
|
+
>
|
|
1300
|
+
<IconTrash />
|
|
1301
|
+
</button>
|
|
1302
|
+
</div>
|
|
1303
|
+
))
|
|
1304
|
+
) : (
|
|
1305
|
+
<div style={emptyState}>
|
|
1306
|
+
<span
|
|
1307
|
+
style={{
|
|
1308
|
+
color:
|
|
1309
|
+
'var(--dauth-accent, #6366f1)',
|
|
1310
|
+
}}
|
|
1311
|
+
>
|
|
1312
|
+
<IconShield />
|
|
1313
|
+
</span>
|
|
1314
|
+
<div>
|
|
1315
|
+
<div
|
|
1316
|
+
style={{
|
|
1317
|
+
fontSize:
|
|
1318
|
+
'var(--dauth-font-size-sm, 0.875rem)',
|
|
1319
|
+
fontWeight: 500,
|
|
1320
|
+
color:
|
|
1321
|
+
'var(--dauth-text-primary, #e4e4e7)',
|
|
1322
|
+
}}
|
|
1323
|
+
>
|
|
1324
|
+
No passkeys registered
|
|
1325
|
+
</div>
|
|
1326
|
+
<div
|
|
1327
|
+
style={{
|
|
1328
|
+
fontSize:
|
|
1329
|
+
'var(--dauth-font-size-xs, 0.75rem)',
|
|
1330
|
+
color:
|
|
1331
|
+
'var(--dauth-text-secondary, #a1a1aa)',
|
|
1332
|
+
}}
|
|
1333
|
+
>
|
|
1334
|
+
Add a passkey for faster, more
|
|
1335
|
+
secure sign-in.
|
|
1336
|
+
</div>
|
|
1337
|
+
</div>
|
|
1338
|
+
</div>
|
|
1339
|
+
)}
|
|
1340
|
+
</div>
|
|
1341
|
+
</>
|
|
1342
|
+
)}
|
|
1343
|
+
|
|
1344
|
+
{/* ========== ACCOUNT TAB ========== */}
|
|
1345
|
+
{activeTab === 'account' && (
|
|
1346
|
+
<>
|
|
1347
|
+
{/* Status (shared) */}
|
|
1348
|
+
{status && (
|
|
1349
|
+
<div
|
|
1350
|
+
role="status"
|
|
1351
|
+
aria-live="polite"
|
|
1352
|
+
style={statusMsg(status.type)}
|
|
1353
|
+
>
|
|
1354
|
+
{status.message}
|
|
1355
|
+
</div>
|
|
1356
|
+
)}
|
|
1357
|
+
|
|
1358
|
+
{/* Delete account */}
|
|
1359
|
+
<div>
|
|
1360
|
+
<div style={dangerTitle}>
|
|
1361
|
+
Delete account
|
|
1362
|
+
</div>
|
|
1363
|
+
<div style={dangerDesc}>
|
|
1364
|
+
Permanently delete your account and all
|
|
1365
|
+
associated data.
|
|
1366
|
+
</div>
|
|
1367
|
+
{!showDelete ? (
|
|
565
1368
|
<button
|
|
566
1369
|
type="button"
|
|
567
|
-
style={
|
|
568
|
-
onClick={() =>
|
|
569
|
-
setShowDelete(false);
|
|
570
|
-
setDeleteText('');
|
|
571
|
-
}}
|
|
1370
|
+
style={deleteBtn}
|
|
1371
|
+
onClick={() => setShowDelete(true)}
|
|
572
1372
|
onMouseEnter={(e) =>
|
|
573
1373
|
(e.currentTarget.style.backgroundColor =
|
|
574
|
-
'
|
|
1374
|
+
'rgba(239, 68, 68, 0.2)')
|
|
575
1375
|
}
|
|
576
1376
|
onMouseLeave={(e) =>
|
|
577
|
-
(e.currentTarget.style.backgroundColor =
|
|
1377
|
+
(e.currentTarget.style.backgroundColor =
|
|
1378
|
+
'var(--dauth-error-bg, rgba(239, 68, 68, 0.1))')
|
|
578
1379
|
}
|
|
579
1380
|
>
|
|
580
|
-
|
|
581
|
-
</button>
|
|
582
|
-
<button
|
|
583
|
-
type="button"
|
|
584
|
-
style={{
|
|
585
|
-
...deleteConfirmBtn,
|
|
586
|
-
opacity:
|
|
587
|
-
deleteText !== CONFIRM_WORD || deleting ? 0.5 : 1,
|
|
588
|
-
cursor:
|
|
589
|
-
deleteText !== CONFIRM_WORD || deleting
|
|
590
|
-
? 'not-allowed'
|
|
591
|
-
: 'pointer',
|
|
592
|
-
}}
|
|
593
|
-
disabled={deleteText !== CONFIRM_WORD || deleting}
|
|
594
|
-
onClick={handleDelete}
|
|
595
|
-
>
|
|
596
|
-
{deleting && <Spinner />}
|
|
597
|
-
Delete my account
|
|
1381
|
+
Delete account
|
|
598
1382
|
</button>
|
|
599
|
-
|
|
1383
|
+
) : (
|
|
1384
|
+
<div style={deletePanel}>
|
|
1385
|
+
<div style={deletePanelText}>
|
|
1386
|
+
This action is permanent and cannot
|
|
1387
|
+
be undone. Type{' '}
|
|
1388
|
+
<strong>{CONFIRM_WORD}</strong> to
|
|
1389
|
+
confirm.
|
|
1390
|
+
</div>
|
|
1391
|
+
<input
|
|
1392
|
+
type="text"
|
|
1393
|
+
value={deleteText}
|
|
1394
|
+
onChange={(e) =>
|
|
1395
|
+
setDeleteText(e.target.value)
|
|
1396
|
+
}
|
|
1397
|
+
placeholder={`Type ${CONFIRM_WORD}`}
|
|
1398
|
+
style={input}
|
|
1399
|
+
onFocus={inputFocusHandler}
|
|
1400
|
+
onBlur={inputBlurHandler}
|
|
1401
|
+
disabled={deleting}
|
|
1402
|
+
/>
|
|
1403
|
+
<div
|
|
1404
|
+
style={{
|
|
1405
|
+
display: 'flex',
|
|
1406
|
+
gap: 8,
|
|
1407
|
+
marginTop: 12,
|
|
1408
|
+
}}
|
|
1409
|
+
>
|
|
1410
|
+
<button
|
|
1411
|
+
type="button"
|
|
1412
|
+
style={cancelBtn}
|
|
1413
|
+
onClick={() => {
|
|
1414
|
+
setShowDelete(false);
|
|
1415
|
+
setDeleteText('');
|
|
1416
|
+
}}
|
|
1417
|
+
onMouseEnter={(e) =>
|
|
1418
|
+
(e.currentTarget.style.backgroundColor =
|
|
1419
|
+
'var(--dauth-surface-hover, #232340)')
|
|
1420
|
+
}
|
|
1421
|
+
onMouseLeave={(e) =>
|
|
1422
|
+
(e.currentTarget.style.backgroundColor =
|
|
1423
|
+
'transparent')
|
|
1424
|
+
}
|
|
1425
|
+
>
|
|
1426
|
+
Cancel
|
|
1427
|
+
</button>
|
|
1428
|
+
<button
|
|
1429
|
+
type="button"
|
|
1430
|
+
style={{
|
|
1431
|
+
...deleteConfirmBtn,
|
|
1432
|
+
opacity:
|
|
1433
|
+
deleteText !== CONFIRM_WORD ||
|
|
1434
|
+
deleting
|
|
1435
|
+
? 0.5
|
|
1436
|
+
: 1,
|
|
1437
|
+
cursor:
|
|
1438
|
+
deleteText !== CONFIRM_WORD ||
|
|
1439
|
+
deleting
|
|
1440
|
+
? 'not-allowed'
|
|
1441
|
+
: 'pointer',
|
|
1442
|
+
}}
|
|
1443
|
+
disabled={
|
|
1444
|
+
deleteText !== CONFIRM_WORD ||
|
|
1445
|
+
deleting
|
|
1446
|
+
}
|
|
1447
|
+
onClick={handleDelete}
|
|
1448
|
+
>
|
|
1449
|
+
{deleting && <Spinner />}
|
|
1450
|
+
Delete my account
|
|
1451
|
+
</button>
|
|
1452
|
+
</div>
|
|
1453
|
+
</div>
|
|
1454
|
+
)}
|
|
600
1455
|
</div>
|
|
601
|
-
|
|
602
|
-
|
|
1456
|
+
|
|
1457
|
+
{/* Sign out */}
|
|
1458
|
+
<hr style={separator} />
|
|
1459
|
+
<button
|
|
1460
|
+
type="button"
|
|
1461
|
+
style={signOutBtn}
|
|
1462
|
+
onClick={handleSignOut}
|
|
1463
|
+
onMouseEnter={(e) =>
|
|
1464
|
+
(e.currentTarget.style.backgroundColor =
|
|
1465
|
+
'rgba(239, 68, 68, 0.2)')
|
|
1466
|
+
}
|
|
1467
|
+
onMouseLeave={(e) =>
|
|
1468
|
+
(e.currentTarget.style.backgroundColor =
|
|
1469
|
+
'var(--dauth-error-bg, rgba(239, 68, 68, 0.1))')
|
|
1470
|
+
}
|
|
1471
|
+
>
|
|
1472
|
+
<IconLogOut />
|
|
1473
|
+
Sign out
|
|
1474
|
+
</button>
|
|
1475
|
+
</>
|
|
1476
|
+
)}
|
|
603
1477
|
</div>
|
|
604
1478
|
|
|
605
|
-
{/* Footer */}
|
|
606
|
-
|
|
607
|
-
<
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
1479
|
+
{/* Footer — only for Profile tab */}
|
|
1480
|
+
{activeTab === 'profile' && (
|
|
1481
|
+
<div style={footerStyle(isDesktop)}>
|
|
1482
|
+
<button
|
|
1483
|
+
type="button"
|
|
1484
|
+
style={{
|
|
1485
|
+
...saveBtn,
|
|
1486
|
+
opacity: canSave ? 1 : 0.5,
|
|
1487
|
+
cursor: canSave ? 'pointer' : 'not-allowed',
|
|
1488
|
+
}}
|
|
1489
|
+
disabled={!canSave}
|
|
1490
|
+
onClick={handleSave}
|
|
1491
|
+
aria-busy={saving}
|
|
1492
|
+
onMouseEnter={(e) => {
|
|
1493
|
+
if (canSave)
|
|
1494
|
+
e.currentTarget.style.backgroundColor =
|
|
1495
|
+
'var(--dauth-accent-hover, #818cf8)';
|
|
1496
|
+
}}
|
|
1497
|
+
onMouseLeave={(e) => {
|
|
619
1498
|
e.currentTarget.style.backgroundColor =
|
|
620
|
-
'var(--dauth-accent
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
>
|
|
627
|
-
|
|
628
|
-
{saving ? 'Saving...' : 'Save changes'}
|
|
629
|
-
</button>
|
|
630
|
-
</div>
|
|
1499
|
+
'var(--dauth-accent, #6366f1)';
|
|
1500
|
+
}}
|
|
1501
|
+
>
|
|
1502
|
+
{saving && <Spinner />}
|
|
1503
|
+
{saving ? 'Saving...' : 'Save changes'}
|
|
1504
|
+
</button>
|
|
1505
|
+
</div>
|
|
1506
|
+
)}
|
|
631
1507
|
</div>
|
|
632
1508
|
</div>
|
|
633
1509
|
</>,
|
|
@@ -637,19 +1513,21 @@ export function DauthProfileModal({ open, onClose }: DauthProfileModalProps) {
|
|
|
637
1513
|
|
|
638
1514
|
// --- Style constants ---
|
|
639
1515
|
|
|
640
|
-
const headerStyle = (
|
|
1516
|
+
const headerStyle = (
|
|
1517
|
+
isDesktop: boolean
|
|
1518
|
+
): React.CSSProperties => ({
|
|
641
1519
|
display: 'flex',
|
|
642
1520
|
alignItems: 'center',
|
|
643
1521
|
justifyContent: 'space-between',
|
|
644
|
-
padding: '16px 24px',
|
|
645
|
-
borderBottom: '1px solid var(--dauth-border, rgba(255, 255, 255, 0.08))',
|
|
1522
|
+
padding: '16px 24px 0',
|
|
646
1523
|
flexShrink: 0,
|
|
647
1524
|
...(!isDesktop
|
|
648
1525
|
? {
|
|
649
1526
|
position: 'sticky' as const,
|
|
650
1527
|
top: 0,
|
|
651
1528
|
zIndex: 1,
|
|
652
|
-
backgroundColor:
|
|
1529
|
+
backgroundColor:
|
|
1530
|
+
'var(--dauth-surface, #1a1a2e)',
|
|
653
1531
|
}
|
|
654
1532
|
: {}),
|
|
655
1533
|
});
|
|
@@ -678,6 +1556,28 @@ const closeBtn: React.CSSProperties = {
|
|
|
678
1556
|
padding: 0,
|
|
679
1557
|
};
|
|
680
1558
|
|
|
1559
|
+
const tabBar: React.CSSProperties = {
|
|
1560
|
+
display: 'flex',
|
|
1561
|
+
padding: '0 24px',
|
|
1562
|
+
borderBottom:
|
|
1563
|
+
'1px solid var(--dauth-border, rgba(255, 255, 255, 0.08))',
|
|
1564
|
+
flexShrink: 0,
|
|
1565
|
+
};
|
|
1566
|
+
|
|
1567
|
+
const tabBtn: React.CSSProperties = {
|
|
1568
|
+
flex: 1,
|
|
1569
|
+
padding: '12px 4px',
|
|
1570
|
+
fontSize: 'var(--dauth-font-size-sm, 0.875rem)',
|
|
1571
|
+
fontWeight: 500,
|
|
1572
|
+
border: 'none',
|
|
1573
|
+
borderBottom: '2px solid transparent',
|
|
1574
|
+
backgroundColor: 'transparent',
|
|
1575
|
+
cursor: 'pointer',
|
|
1576
|
+
transition: 'color 150ms, border-color 150ms',
|
|
1577
|
+
fontFamily: 'inherit',
|
|
1578
|
+
textAlign: 'center',
|
|
1579
|
+
};
|
|
1580
|
+
|
|
681
1581
|
const bodyStyle: React.CSSProperties = {
|
|
682
1582
|
flex: 1,
|
|
683
1583
|
overflowY: 'auto',
|
|
@@ -707,12 +1607,27 @@ const avatarCircle: React.CSSProperties = {
|
|
|
707
1607
|
fontWeight: 600,
|
|
708
1608
|
};
|
|
709
1609
|
|
|
1610
|
+
const avatarOverlay: React.CSSProperties = {
|
|
1611
|
+
position: 'absolute',
|
|
1612
|
+
inset: 0,
|
|
1613
|
+
backgroundColor: 'rgba(0, 0, 0, 0.4)',
|
|
1614
|
+
display: 'flex',
|
|
1615
|
+
alignItems: 'center',
|
|
1616
|
+
justifyContent: 'center',
|
|
1617
|
+
borderRadius: '50%',
|
|
1618
|
+
opacity: 0.7,
|
|
1619
|
+
transition: 'opacity 150ms',
|
|
1620
|
+
color: '#ffffff',
|
|
1621
|
+
};
|
|
1622
|
+
|
|
710
1623
|
const emailText: React.CSSProperties = {
|
|
711
1624
|
fontSize: 'var(--dauth-font-size-sm, 0.875rem)',
|
|
712
1625
|
color: 'var(--dauth-text-secondary, #a1a1aa)',
|
|
713
1626
|
};
|
|
714
1627
|
|
|
715
|
-
const statusMsg = (
|
|
1628
|
+
const statusMsg = (
|
|
1629
|
+
type: 'success' | 'error'
|
|
1630
|
+
): React.CSSProperties => ({
|
|
716
1631
|
padding: '10px 14px',
|
|
717
1632
|
borderRadius: 'var(--dauth-radius-sm, 8px)',
|
|
718
1633
|
fontSize: 'var(--dauth-font-size-sm, 0.875rem)',
|
|
@@ -729,7 +1644,9 @@ const statusMsg = (type: 'success' | 'error'): React.CSSProperties => ({
|
|
|
729
1644
|
lineHeight: 1.5,
|
|
730
1645
|
});
|
|
731
1646
|
|
|
732
|
-
const fieldGroup: React.CSSProperties = {
|
|
1647
|
+
const fieldGroup: React.CSSProperties = {
|
|
1648
|
+
marginBottom: 16,
|
|
1649
|
+
};
|
|
733
1650
|
|
|
734
1651
|
const label: React.CSSProperties = {
|
|
735
1652
|
display: 'block',
|
|
@@ -745,8 +1662,10 @@ const input: React.CSSProperties = {
|
|
|
745
1662
|
fontSize: 'var(--dauth-font-size-base, 1rem)',
|
|
746
1663
|
lineHeight: 1.5,
|
|
747
1664
|
color: 'var(--dauth-text-primary, #e4e4e7)',
|
|
748
|
-
backgroundColor:
|
|
749
|
-
|
|
1665
|
+
backgroundColor:
|
|
1666
|
+
'var(--dauth-surface-secondary, rgba(255, 255, 255, 0.04))',
|
|
1667
|
+
border:
|
|
1668
|
+
'1px solid var(--dauth-border, rgba(255, 255, 255, 0.08))',
|
|
750
1669
|
borderRadius: 'var(--dauth-radius-input, 8px)',
|
|
751
1670
|
outline: 'none',
|
|
752
1671
|
transition: 'border-color 150ms, box-shadow 150ms',
|
|
@@ -754,25 +1673,119 @@ const input: React.CSSProperties = {
|
|
|
754
1673
|
fontFamily: 'inherit',
|
|
755
1674
|
};
|
|
756
1675
|
|
|
757
|
-
const inputFocusHandler = (
|
|
1676
|
+
const inputFocusHandler = (
|
|
1677
|
+
e: React.FocusEvent<HTMLInputElement>
|
|
1678
|
+
) => {
|
|
758
1679
|
e.currentTarget.style.borderColor =
|
|
759
1680
|
'var(--dauth-border-focus, rgba(99, 102, 241, 0.5))';
|
|
760
|
-
e.currentTarget.style.boxShadow =
|
|
1681
|
+
e.currentTarget.style.boxShadow =
|
|
1682
|
+
'0 0 0 3px rgba(99, 102, 241, 0.15)';
|
|
761
1683
|
};
|
|
762
1684
|
|
|
763
|
-
const inputBlurHandler = (
|
|
1685
|
+
const inputBlurHandler = (
|
|
1686
|
+
e: React.FocusEvent<HTMLInputElement>
|
|
1687
|
+
) => {
|
|
764
1688
|
e.currentTarget.style.borderColor =
|
|
765
1689
|
'var(--dauth-border, rgba(255, 255, 255, 0.08))';
|
|
766
1690
|
e.currentTarget.style.boxShadow = 'none';
|
|
767
1691
|
};
|
|
768
1692
|
|
|
1693
|
+
const langBtn: React.CSSProperties = {
|
|
1694
|
+
padding: '8px 16px',
|
|
1695
|
+
fontSize: 'var(--dauth-font-size-sm, 0.875rem)',
|
|
1696
|
+
fontWeight: 500,
|
|
1697
|
+
border: 'none',
|
|
1698
|
+
borderRadius: 'var(--dauth-radius-sm, 8px)',
|
|
1699
|
+
cursor: 'pointer',
|
|
1700
|
+
transition: 'background-color 150ms, color 150ms',
|
|
1701
|
+
fontFamily: 'inherit',
|
|
1702
|
+
};
|
|
1703
|
+
|
|
769
1704
|
const separator: React.CSSProperties = {
|
|
770
1705
|
height: 1,
|
|
771
|
-
backgroundColor:
|
|
1706
|
+
backgroundColor:
|
|
1707
|
+
'var(--dauth-border, rgba(255, 255, 255, 0.08))',
|
|
772
1708
|
margin: '24px 0',
|
|
773
1709
|
border: 'none',
|
|
774
1710
|
};
|
|
775
1711
|
|
|
1712
|
+
const outlineBtn: React.CSSProperties = {
|
|
1713
|
+
padding: '6px 12px',
|
|
1714
|
+
fontSize: 'var(--dauth-font-size-xs, 0.75rem)',
|
|
1715
|
+
fontWeight: 500,
|
|
1716
|
+
color: 'var(--dauth-text-secondary, #a1a1aa)',
|
|
1717
|
+
backgroundColor: 'transparent',
|
|
1718
|
+
border:
|
|
1719
|
+
'1px solid var(--dauth-border, rgba(255, 255, 255, 0.08))',
|
|
1720
|
+
borderRadius: 'var(--dauth-radius-sm, 8px)',
|
|
1721
|
+
cursor: 'pointer',
|
|
1722
|
+
transition: 'background-color 150ms',
|
|
1723
|
+
fontFamily: 'inherit',
|
|
1724
|
+
};
|
|
1725
|
+
|
|
1726
|
+
const registerPanel: React.CSSProperties = {
|
|
1727
|
+
padding: 16,
|
|
1728
|
+
borderRadius: 'var(--dauth-radius-sm, 8px)',
|
|
1729
|
+
border:
|
|
1730
|
+
'1px solid var(--dauth-border, rgba(255, 255, 255, 0.08))',
|
|
1731
|
+
backgroundColor:
|
|
1732
|
+
'var(--dauth-surface-secondary, rgba(255, 255, 255, 0.04))',
|
|
1733
|
+
marginBottom: 12,
|
|
1734
|
+
};
|
|
1735
|
+
|
|
1736
|
+
const smallAccentBtn: React.CSSProperties = {
|
|
1737
|
+
padding: '8px 16px',
|
|
1738
|
+
fontSize: 'var(--dauth-font-size-sm, 0.875rem)',
|
|
1739
|
+
fontWeight: 500,
|
|
1740
|
+
color: '#ffffff',
|
|
1741
|
+
backgroundColor: 'var(--dauth-accent, #6366f1)',
|
|
1742
|
+
border: 'none',
|
|
1743
|
+
borderRadius: 'var(--dauth-radius-sm, 8px)',
|
|
1744
|
+
cursor: 'pointer',
|
|
1745
|
+
transition: 'opacity 150ms',
|
|
1746
|
+
fontFamily: 'inherit',
|
|
1747
|
+
display: 'flex',
|
|
1748
|
+
alignItems: 'center',
|
|
1749
|
+
gap: 6,
|
|
1750
|
+
};
|
|
1751
|
+
|
|
1752
|
+
const credentialRow: React.CSSProperties = {
|
|
1753
|
+
display: 'flex',
|
|
1754
|
+
alignItems: 'center',
|
|
1755
|
+
justifyContent: 'space-between',
|
|
1756
|
+
padding: 12,
|
|
1757
|
+
borderRadius: 'var(--dauth-radius-sm, 8px)',
|
|
1758
|
+
backgroundColor:
|
|
1759
|
+
'var(--dauth-surface-secondary, rgba(255, 255, 255, 0.04))',
|
|
1760
|
+
marginBottom: 8,
|
|
1761
|
+
};
|
|
1762
|
+
|
|
1763
|
+
const trashBtn: React.CSSProperties = {
|
|
1764
|
+
display: 'flex',
|
|
1765
|
+
alignItems: 'center',
|
|
1766
|
+
justifyContent: 'center',
|
|
1767
|
+
width: 28,
|
|
1768
|
+
height: 28,
|
|
1769
|
+
border: 'none',
|
|
1770
|
+
backgroundColor: 'transparent',
|
|
1771
|
+
color: 'var(--dauth-text-muted, #71717a)',
|
|
1772
|
+
cursor: 'pointer',
|
|
1773
|
+
transition: 'color 150ms',
|
|
1774
|
+
padding: 0,
|
|
1775
|
+
borderRadius: 'var(--dauth-radius-sm, 8px)',
|
|
1776
|
+
flexShrink: 0,
|
|
1777
|
+
};
|
|
1778
|
+
|
|
1779
|
+
const emptyState: React.CSSProperties = {
|
|
1780
|
+
display: 'flex',
|
|
1781
|
+
alignItems: 'center',
|
|
1782
|
+
gap: 12,
|
|
1783
|
+
padding: 16,
|
|
1784
|
+
borderRadius: 'var(--dauth-radius-sm, 8px)',
|
|
1785
|
+
backgroundColor:
|
|
1786
|
+
'var(--dauth-surface-secondary, rgba(255, 255, 255, 0.04))',
|
|
1787
|
+
};
|
|
1788
|
+
|
|
776
1789
|
const dangerTitle: React.CSSProperties = {
|
|
777
1790
|
fontSize: 'var(--dauth-font-size-sm, 0.875rem)',
|
|
778
1791
|
fontWeight: 600,
|
|
@@ -792,7 +1805,8 @@ const deleteBtn: React.CSSProperties = {
|
|
|
792
1805
|
fontSize: 'var(--dauth-font-size-sm, 0.875rem)',
|
|
793
1806
|
fontWeight: 500,
|
|
794
1807
|
color: 'var(--dauth-error, #ef4444)',
|
|
795
|
-
backgroundColor:
|
|
1808
|
+
backgroundColor:
|
|
1809
|
+
'var(--dauth-error-bg, rgba(239, 68, 68, 0.1))',
|
|
796
1810
|
border: '1px solid rgba(239, 68, 68, 0.2)',
|
|
797
1811
|
borderRadius: 'var(--dauth-radius-sm, 8px)',
|
|
798
1812
|
cursor: 'pointer',
|
|
@@ -805,7 +1819,8 @@ const deletePanel: React.CSSProperties = {
|
|
|
805
1819
|
padding: 16,
|
|
806
1820
|
borderRadius: 'var(--dauth-radius-sm, 8px)',
|
|
807
1821
|
border: '1px solid rgba(239, 68, 68, 0.3)',
|
|
808
|
-
backgroundColor:
|
|
1822
|
+
backgroundColor:
|
|
1823
|
+
'var(--dauth-error-bg, rgba(239, 68, 68, 0.1))',
|
|
809
1824
|
};
|
|
810
1825
|
|
|
811
1826
|
const deletePanelText: React.CSSProperties = {
|
|
@@ -816,13 +1831,13 @@ const deletePanelText: React.CSSProperties = {
|
|
|
816
1831
|
};
|
|
817
1832
|
|
|
818
1833
|
const cancelBtn: React.CSSProperties = {
|
|
819
|
-
flex: 1,
|
|
820
1834
|
padding: '8px 16px',
|
|
821
1835
|
fontSize: 'var(--dauth-font-size-sm, 0.875rem)',
|
|
822
1836
|
fontWeight: 500,
|
|
823
1837
|
color: 'var(--dauth-text-secondary, #a1a1aa)',
|
|
824
1838
|
backgroundColor: 'transparent',
|
|
825
|
-
border:
|
|
1839
|
+
border:
|
|
1840
|
+
'1px solid var(--dauth-border, rgba(255, 255, 255, 0.08))',
|
|
826
1841
|
borderRadius: 'var(--dauth-radius-sm, 8px)',
|
|
827
1842
|
cursor: 'pointer',
|
|
828
1843
|
transition: 'background-color 150ms',
|
|
@@ -847,17 +1862,41 @@ const deleteConfirmBtn: React.CSSProperties = {
|
|
|
847
1862
|
gap: 8,
|
|
848
1863
|
};
|
|
849
1864
|
|
|
850
|
-
const
|
|
1865
|
+
const signOutBtn: React.CSSProperties = {
|
|
1866
|
+
width: '100%',
|
|
1867
|
+
padding: '12px 24px',
|
|
1868
|
+
fontSize: 'var(--dauth-font-size-base, 1rem)',
|
|
1869
|
+
fontWeight: 500,
|
|
1870
|
+
color: 'var(--dauth-error, #ef4444)',
|
|
1871
|
+
backgroundColor:
|
|
1872
|
+
'var(--dauth-error-bg, rgba(239, 68, 68, 0.1))',
|
|
1873
|
+
border: '1px solid rgba(239, 68, 68, 0.2)',
|
|
1874
|
+
borderRadius: 'var(--dauth-radius-sm, 8px)',
|
|
1875
|
+
cursor: 'pointer',
|
|
1876
|
+
transition: 'background-color 150ms',
|
|
1877
|
+
fontFamily: 'inherit',
|
|
1878
|
+
display: 'flex',
|
|
1879
|
+
alignItems: 'center',
|
|
1880
|
+
justifyContent: 'center',
|
|
1881
|
+
gap: 8,
|
|
1882
|
+
};
|
|
1883
|
+
|
|
1884
|
+
const footerStyle = (
|
|
1885
|
+
isDesktop: boolean
|
|
1886
|
+
): React.CSSProperties => ({
|
|
851
1887
|
padding: '16px 24px',
|
|
852
|
-
borderTop:
|
|
1888
|
+
borderTop:
|
|
1889
|
+
'1px solid var(--dauth-border, rgba(255, 255, 255, 0.08))',
|
|
853
1890
|
flexShrink: 0,
|
|
854
1891
|
...(!isDesktop
|
|
855
1892
|
? {
|
|
856
1893
|
position: 'sticky' as const,
|
|
857
1894
|
bottom: 0,
|
|
858
1895
|
zIndex: 1,
|
|
859
|
-
backgroundColor:
|
|
860
|
-
|
|
1896
|
+
backgroundColor:
|
|
1897
|
+
'var(--dauth-surface, #1a1a2e)',
|
|
1898
|
+
paddingBottom:
|
|
1899
|
+
'max(16px, env(safe-area-inset-bottom))',
|
|
861
1900
|
}
|
|
862
1901
|
: {}),
|
|
863
1902
|
});
|