dauth-context-react 6.0.0 → 6.2.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 +27 -2
- package/dist/index.d.ts +27 -2
- package/dist/index.js +1086 -158
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1086 -158
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/DauthProfileModal.tsx +1102 -238
- 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 +31 -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,13 +285,31 @@ 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);
|
|
307
|
+
|
|
308
|
+
// Tab state
|
|
309
|
+
const showSecurity = domain.authMethods?.passkey === true;
|
|
310
|
+
const [activeTab, setActiveTab] = useState<Tab>('profile');
|
|
181
311
|
|
|
182
|
-
//
|
|
312
|
+
// Profile form state
|
|
183
313
|
const [name, setName] = useState('');
|
|
184
314
|
const [lastname, setLastname] = useState('');
|
|
185
315
|
const [nickname, setNickname] = useState('');
|
|
@@ -198,6 +328,23 @@ export function DauthProfileModal({ open, onClose }: DauthProfileModalProps) {
|
|
|
198
328
|
const [deleteText, setDeleteText] = useState('');
|
|
199
329
|
const [deleting, setDeleting] = useState(false);
|
|
200
330
|
|
|
331
|
+
// Passkey state
|
|
332
|
+
const [credentials, setCredentials] = useState<
|
|
333
|
+
IPasskeyCredential[]
|
|
334
|
+
>([]);
|
|
335
|
+
const [loadingCreds, setLoadingCreds] = useState(false);
|
|
336
|
+
const [showRegister, setShowRegister] = useState(false);
|
|
337
|
+
const [passkeyName, setPasskeyName] = useState('');
|
|
338
|
+
const [registering, setRegistering] = useState(false);
|
|
339
|
+
const [passkeyStatus, setPasskeyStatus] = useState<{
|
|
340
|
+
type: 'success' | 'error';
|
|
341
|
+
message: string;
|
|
342
|
+
} | null>(null);
|
|
343
|
+
|
|
344
|
+
// Avatar upload state
|
|
345
|
+
const [uploadingAvatar, setUploadingAvatar] =
|
|
346
|
+
useState(false);
|
|
347
|
+
|
|
201
348
|
// Populate form when modal opens
|
|
202
349
|
useEffect(() => {
|
|
203
350
|
if (open && user?._id && !populated) {
|
|
@@ -212,28 +359,56 @@ export function DauthProfileModal({ open, onClose }: DauthProfileModalProps) {
|
|
|
212
359
|
setStatus(null);
|
|
213
360
|
setShowDelete(false);
|
|
214
361
|
setDeleteText('');
|
|
362
|
+
setActiveTab('profile');
|
|
363
|
+
setPasskeyStatus(null);
|
|
364
|
+
setShowRegister(false);
|
|
365
|
+
setPasskeyName('');
|
|
215
366
|
}
|
|
216
367
|
}, [open, user, populated]);
|
|
217
368
|
|
|
369
|
+
// Fetch passkey credentials when Security tab is active
|
|
370
|
+
useEffect(() => {
|
|
371
|
+
if (activeTab !== 'security' || !showSecurity) return;
|
|
372
|
+
setLoadingCreds(true);
|
|
373
|
+
getPasskeyCredentials().then((creds) => {
|
|
374
|
+
setCredentials(creds);
|
|
375
|
+
setLoadingCreds(false);
|
|
376
|
+
});
|
|
377
|
+
}, [activeTab, showSecurity, getPasskeyCredentials]);
|
|
378
|
+
|
|
218
379
|
// Auto-clear success message
|
|
219
380
|
useEffect(() => {
|
|
220
381
|
if (status?.type !== 'success') return;
|
|
221
|
-
const timer = setTimeout(
|
|
382
|
+
const timer = setTimeout(
|
|
383
|
+
() => setStatus(null),
|
|
384
|
+
SUCCESS_TIMEOUT_MS
|
|
385
|
+
);
|
|
222
386
|
return () => clearTimeout(timer);
|
|
223
387
|
}, [status]);
|
|
224
388
|
|
|
389
|
+
useEffect(() => {
|
|
390
|
+
if (passkeyStatus?.type !== 'success') return;
|
|
391
|
+
const timer = setTimeout(
|
|
392
|
+
() => setPasskeyStatus(null),
|
|
393
|
+
SUCCESS_TIMEOUT_MS
|
|
394
|
+
);
|
|
395
|
+
return () => clearTimeout(timer);
|
|
396
|
+
}, [passkeyStatus]);
|
|
397
|
+
|
|
225
398
|
useFocusTrap(modalRef, phase === 'entered', onClose);
|
|
226
399
|
useScrollLock(phase !== 'exited');
|
|
227
400
|
|
|
228
401
|
const hasField = useCallback(
|
|
229
402
|
(field: string) =>
|
|
230
|
-
domain.formFields?.some((f) => f.field === field) ??
|
|
403
|
+
domain.formFields?.some((f) => f.field === field) ??
|
|
404
|
+
false,
|
|
231
405
|
[domain.formFields]
|
|
232
406
|
);
|
|
233
407
|
|
|
234
408
|
const isRequired = useCallback(
|
|
235
409
|
(field: string) =>
|
|
236
|
-
domain.formFields?.find((f) => f.field === field)
|
|
410
|
+
domain.formFields?.find((f) => f.field === field)
|
|
411
|
+
?.required ?? false,
|
|
237
412
|
[domain.formFields]
|
|
238
413
|
);
|
|
239
414
|
|
|
@@ -247,7 +422,8 @@ export function DauthProfileModal({ open, onClose }: DauthProfileModalProps) {
|
|
|
247
422
|
);
|
|
248
423
|
}, [name, lastname, nickname, country, user]);
|
|
249
424
|
|
|
250
|
-
const canSave =
|
|
425
|
+
const canSave =
|
|
426
|
+
name.trim().length > 0 && hasChanges && !saving;
|
|
251
427
|
|
|
252
428
|
const handleSave = useCallback(async () => {
|
|
253
429
|
setSaving(true);
|
|
@@ -280,13 +456,107 @@ export function DauthProfileModal({ open, onClose }: DauthProfileModalProps) {
|
|
|
280
456
|
} else {
|
|
281
457
|
setStatus({
|
|
282
458
|
type: 'error',
|
|
283
|
-
message:
|
|
459
|
+
message:
|
|
460
|
+
'Could not delete account. Please try again.',
|
|
284
461
|
});
|
|
285
462
|
setShowDelete(false);
|
|
286
463
|
setDeleteText('');
|
|
287
464
|
}
|
|
288
465
|
}, [deleteAccount, onClose]);
|
|
289
466
|
|
|
467
|
+
const handleLanguage = useCallback(
|
|
468
|
+
async (lang: string) => {
|
|
469
|
+
await updateUser({ language: lang } as any);
|
|
470
|
+
},
|
|
471
|
+
[updateUser]
|
|
472
|
+
);
|
|
473
|
+
|
|
474
|
+
const handleRegisterPasskey = useCallback(async () => {
|
|
475
|
+
setRegistering(true);
|
|
476
|
+
setPasskeyStatus(null);
|
|
477
|
+
const cred = await registerPasskey(
|
|
478
|
+
passkeyName || undefined
|
|
479
|
+
);
|
|
480
|
+
setRegistering(false);
|
|
481
|
+
if (cred) {
|
|
482
|
+
setCredentials((prev) => [...prev, cred]);
|
|
483
|
+
setPasskeyName('');
|
|
484
|
+
setShowRegister(false);
|
|
485
|
+
setPasskeyStatus({
|
|
486
|
+
type: 'success',
|
|
487
|
+
message: 'Passkey registered successfully',
|
|
488
|
+
});
|
|
489
|
+
} else {
|
|
490
|
+
setPasskeyStatus({
|
|
491
|
+
type: 'error',
|
|
492
|
+
message: 'Failed to register passkey',
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
}, [passkeyName, registerPasskey]);
|
|
496
|
+
|
|
497
|
+
const handleDeletePasskey = useCallback(
|
|
498
|
+
async (credentialId: string) => {
|
|
499
|
+
const ok = await deletePasskeyCredential(credentialId);
|
|
500
|
+
if (ok) {
|
|
501
|
+
setCredentials((prev) =>
|
|
502
|
+
prev.filter((c) => c._id !== credentialId)
|
|
503
|
+
);
|
|
504
|
+
}
|
|
505
|
+
},
|
|
506
|
+
[deletePasskeyCredential]
|
|
507
|
+
);
|
|
508
|
+
|
|
509
|
+
const handleAvatarClick = useCallback(() => {
|
|
510
|
+
if (onAvatarUpload) {
|
|
511
|
+
avatarInputRef.current?.click();
|
|
512
|
+
}
|
|
513
|
+
}, [onAvatarUpload]);
|
|
514
|
+
|
|
515
|
+
const handleAvatarChange = useCallback(
|
|
516
|
+
async (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
517
|
+
const file = e.target.files?.[0];
|
|
518
|
+
if (!file || !onAvatarUpload) return;
|
|
519
|
+
setUploadingAvatar(true);
|
|
520
|
+
try {
|
|
521
|
+
const url = await onAvatarUpload(file);
|
|
522
|
+
if (url) {
|
|
523
|
+
await updateUser({ avatar: url } as any);
|
|
524
|
+
}
|
|
525
|
+
} catch {
|
|
526
|
+
// Error handled by onError callback
|
|
527
|
+
}
|
|
528
|
+
setUploadingAvatar(false);
|
|
529
|
+
if (avatarInputRef.current) {
|
|
530
|
+
avatarInputRef.current.value = '';
|
|
531
|
+
}
|
|
532
|
+
},
|
|
533
|
+
[onAvatarUpload, updateUser]
|
|
534
|
+
);
|
|
535
|
+
|
|
536
|
+
const handleSignOut = useCallback(() => {
|
|
537
|
+
logout();
|
|
538
|
+
onClose();
|
|
539
|
+
}, [logout, onClose]);
|
|
540
|
+
|
|
541
|
+
// Build CSS custom property overrides from domain.modalTheme
|
|
542
|
+
const themeVars = useMemo(() => {
|
|
543
|
+
const t = domain.modalTheme;
|
|
544
|
+
if (!t) return {};
|
|
545
|
+
const vars: Record<string, string> = {};
|
|
546
|
+
if (t.accent) vars['--dauth-accent'] = t.accent;
|
|
547
|
+
if (t.accentHover)
|
|
548
|
+
vars['--dauth-accent-hover'] = t.accentHover;
|
|
549
|
+
if (t.surface) vars['--dauth-surface'] = t.surface;
|
|
550
|
+
if (t.surfaceHover)
|
|
551
|
+
vars['--dauth-surface-hover'] = t.surfaceHover;
|
|
552
|
+
if (t.textPrimary)
|
|
553
|
+
vars['--dauth-text-primary'] = t.textPrimary;
|
|
554
|
+
if (t.textSecondary)
|
|
555
|
+
vars['--dauth-text-secondary'] = t.textSecondary;
|
|
556
|
+
if (t.border) vars['--dauth-border'] = t.border;
|
|
557
|
+
return vars;
|
|
558
|
+
}, [domain.modalTheme]);
|
|
559
|
+
|
|
290
560
|
if (phase === 'exited') return null;
|
|
291
561
|
|
|
292
562
|
const dur = isDesktop ? TRANSITION_MS : MOBILE_TRANSITION_MS;
|
|
@@ -296,7 +566,8 @@ export function DauthProfileModal({ open, onClose }: DauthProfileModalProps) {
|
|
|
296
566
|
position: 'fixed',
|
|
297
567
|
inset: 0,
|
|
298
568
|
zIndex: 2147483647,
|
|
299
|
-
backgroundColor:
|
|
569
|
+
backgroundColor:
|
|
570
|
+
'var(--dauth-backdrop, rgba(0, 0, 0, 0.6))',
|
|
300
571
|
backdropFilter: 'blur(4px)',
|
|
301
572
|
WebkitBackdropFilter: 'blur(4px)',
|
|
302
573
|
display: 'flex',
|
|
@@ -314,8 +585,10 @@ export function DauthProfileModal({ open, onClose }: DauthProfileModalProps) {
|
|
|
314
585
|
margin: 16,
|
|
315
586
|
backgroundColor: 'var(--dauth-surface, #1a1a2e)',
|
|
316
587
|
borderRadius: 'var(--dauth-radius, 12px)',
|
|
317
|
-
boxShadow:
|
|
318
|
-
|
|
588
|
+
boxShadow:
|
|
589
|
+
'var(--dauth-shadow, 0 25px 50px -12px rgba(0, 0, 0, 0.5))',
|
|
590
|
+
border:
|
|
591
|
+
'1px solid var(--dauth-border, rgba(255, 255, 255, 0.08))',
|
|
319
592
|
display: 'flex',
|
|
320
593
|
flexDirection: 'column',
|
|
321
594
|
overflow: 'hidden',
|
|
@@ -323,7 +596,10 @@ export function DauthProfileModal({ open, onClose }: DauthProfileModalProps) {
|
|
|
323
596
|
'var(--dauth-font-family, system-ui, -apple-system, sans-serif)',
|
|
324
597
|
color: 'var(--dauth-text-primary, #e4e4e7)',
|
|
325
598
|
opacity: phase === 'entered' ? 1 : 0,
|
|
326
|
-
transform:
|
|
599
|
+
transform:
|
|
600
|
+
phase === 'entered'
|
|
601
|
+
? 'translateY(0)'
|
|
602
|
+
: 'translateY(16px)',
|
|
327
603
|
transition: `opacity ${dur}ms ${easing}, transform ${dur}ms ${easing}`,
|
|
328
604
|
};
|
|
329
605
|
|
|
@@ -336,7 +612,10 @@ export function DauthProfileModal({ open, onClose }: DauthProfileModalProps) {
|
|
|
336
612
|
fontFamily:
|
|
337
613
|
'var(--dauth-font-family, system-ui, -apple-system, sans-serif)',
|
|
338
614
|
color: 'var(--dauth-text-primary, #e4e4e7)',
|
|
339
|
-
transform:
|
|
615
|
+
transform:
|
|
616
|
+
phase === 'entered'
|
|
617
|
+
? 'translateY(0)'
|
|
618
|
+
: 'translateY(100%)',
|
|
340
619
|
transition: `transform ${dur}ms ${easing}`,
|
|
341
620
|
};
|
|
342
621
|
|
|
@@ -344,15 +623,24 @@ export function DauthProfileModal({ open, onClose }: DauthProfileModalProps) {
|
|
|
344
623
|
.charAt(0)
|
|
345
624
|
.toUpperCase();
|
|
346
625
|
|
|
626
|
+
const tabs: { key: Tab; label: string }[] = [
|
|
627
|
+
{ key: 'profile', label: 'Profile' },
|
|
628
|
+
...(showSecurity
|
|
629
|
+
? [{ key: 'security' as Tab, label: 'Security' }]
|
|
630
|
+
: []),
|
|
631
|
+
{ key: 'account', label: 'Account' },
|
|
632
|
+
];
|
|
633
|
+
|
|
347
634
|
return createPortal(
|
|
348
635
|
<>
|
|
349
636
|
<style
|
|
350
637
|
dangerouslySetInnerHTML={{
|
|
351
|
-
__html:
|
|
638
|
+
__html:
|
|
639
|
+
'@keyframes dauth-spin{to{transform:rotate(360deg)}}',
|
|
352
640
|
}}
|
|
353
641
|
/>
|
|
354
642
|
<div
|
|
355
|
-
style={backdrop}
|
|
643
|
+
style={{ ...backdrop, ...themeVars }}
|
|
356
644
|
onClick={isDesktop ? onClose : undefined}
|
|
357
645
|
data-testid="dauth-profile-backdrop"
|
|
358
646
|
>
|
|
@@ -377,7 +665,8 @@ export function DauthProfileModal({ open, onClose }: DauthProfileModalProps) {
|
|
|
377
665
|
'var(--dauth-surface-hover, #232340)')
|
|
378
666
|
}
|
|
379
667
|
onMouseLeave={(e) =>
|
|
380
|
-
(e.currentTarget.style.backgroundColor =
|
|
668
|
+
(e.currentTarget.style.backgroundColor =
|
|
669
|
+
'transparent')
|
|
381
670
|
}
|
|
382
671
|
>
|
|
383
672
|
{isDesktop ? <IconClose /> : <IconBack />}
|
|
@@ -388,231 +677,643 @@ export function DauthProfileModal({ open, onClose }: DauthProfileModalProps) {
|
|
|
388
677
|
<div style={{ width: 36 }} />
|
|
389
678
|
</div>
|
|
390
679
|
|
|
680
|
+
{/* Tab bar */}
|
|
681
|
+
<div style={tabBar} role="tablist">
|
|
682
|
+
{tabs.map((t) => (
|
|
683
|
+
<button
|
|
684
|
+
key={t.key}
|
|
685
|
+
role="tab"
|
|
686
|
+
type="button"
|
|
687
|
+
aria-selected={activeTab === t.key}
|
|
688
|
+
style={{
|
|
689
|
+
...tabBtn,
|
|
690
|
+
color:
|
|
691
|
+
activeTab === t.key
|
|
692
|
+
? 'var(--dauth-accent, #6366f1)'
|
|
693
|
+
: 'var(--dauth-text-secondary, #a1a1aa)',
|
|
694
|
+
borderBottomColor:
|
|
695
|
+
activeTab === t.key
|
|
696
|
+
? 'var(--dauth-accent, #6366f1)'
|
|
697
|
+
: 'transparent',
|
|
698
|
+
}}
|
|
699
|
+
onClick={() => setActiveTab(t.key)}
|
|
700
|
+
>
|
|
701
|
+
{t.label}
|
|
702
|
+
</button>
|
|
703
|
+
))}
|
|
704
|
+
</div>
|
|
705
|
+
|
|
391
706
|
{/* Scrollable body */}
|
|
392
707
|
<div style={bodyStyle}>
|
|
393
|
-
{/*
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
{
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
alt=""
|
|
708
|
+
{/* ========== PROFILE TAB ========== */}
|
|
709
|
+
{activeTab === 'profile' && (
|
|
710
|
+
<>
|
|
711
|
+
{/* Avatar */}
|
|
712
|
+
<div style={avatarSection}>
|
|
713
|
+
<div
|
|
400
714
|
style={{
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
715
|
+
...avatarCircle,
|
|
716
|
+
cursor: onAvatarUpload
|
|
717
|
+
? 'pointer'
|
|
718
|
+
: 'default',
|
|
719
|
+
position: 'relative' as const,
|
|
404
720
|
}}
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
721
|
+
onClick={handleAvatarClick}
|
|
722
|
+
>
|
|
723
|
+
{uploadingAvatar ? (
|
|
724
|
+
<Spinner />
|
|
725
|
+
) : user.avatar?.url ? (
|
|
726
|
+
<img
|
|
727
|
+
src={user.avatar.url}
|
|
728
|
+
alt=""
|
|
729
|
+
style={{
|
|
730
|
+
width: '100%',
|
|
731
|
+
height: '100%',
|
|
732
|
+
objectFit: 'cover',
|
|
733
|
+
}}
|
|
734
|
+
/>
|
|
735
|
+
) : (
|
|
736
|
+
avatarInitial
|
|
737
|
+
)}
|
|
738
|
+
{onAvatarUpload && !uploadingAvatar && (
|
|
739
|
+
<div style={avatarOverlay}>
|
|
740
|
+
<IconCamera />
|
|
741
|
+
</div>
|
|
742
|
+
)}
|
|
743
|
+
</div>
|
|
744
|
+
<div style={emailText}>{user.email}</div>
|
|
745
|
+
{onAvatarUpload && (
|
|
746
|
+
<input
|
|
747
|
+
ref={avatarInputRef}
|
|
748
|
+
type="file"
|
|
749
|
+
accept="image/*"
|
|
750
|
+
style={{ display: 'none' }}
|
|
751
|
+
onChange={handleAvatarChange}
|
|
752
|
+
/>
|
|
753
|
+
)}
|
|
754
|
+
</div>
|
|
755
|
+
|
|
756
|
+
{/* Status */}
|
|
757
|
+
{status && (
|
|
758
|
+
<div
|
|
759
|
+
role="status"
|
|
760
|
+
aria-live="polite"
|
|
761
|
+
style={statusMsg(status.type)}
|
|
762
|
+
>
|
|
763
|
+
{status.message}
|
|
764
|
+
</div>
|
|
408
765
|
)}
|
|
409
|
-
</div>
|
|
410
|
-
<div style={emailText}>{user.email}</div>
|
|
411
|
-
</div>
|
|
412
766
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
767
|
+
{/* Form */}
|
|
768
|
+
<div>
|
|
769
|
+
<div style={fieldGroup}>
|
|
770
|
+
<label
|
|
771
|
+
htmlFor="dauth-name"
|
|
772
|
+
style={label}
|
|
773
|
+
>
|
|
774
|
+
Name *
|
|
775
|
+
</label>
|
|
776
|
+
<input
|
|
777
|
+
id="dauth-name"
|
|
778
|
+
type="text"
|
|
779
|
+
value={name}
|
|
780
|
+
onChange={(e) =>
|
|
781
|
+
setName(e.target.value)
|
|
782
|
+
}
|
|
783
|
+
placeholder="Your name"
|
|
784
|
+
disabled={saving}
|
|
785
|
+
style={input}
|
|
786
|
+
onFocus={inputFocusHandler}
|
|
787
|
+
onBlur={inputBlurHandler}
|
|
788
|
+
/>
|
|
789
|
+
</div>
|
|
423
790
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
791
|
+
{hasField('lastname') && (
|
|
792
|
+
<div style={fieldGroup}>
|
|
793
|
+
<label
|
|
794
|
+
htmlFor="dauth-lastname"
|
|
795
|
+
style={label}
|
|
796
|
+
>
|
|
797
|
+
Last name
|
|
798
|
+
{isRequired('lastname') ? ' *' : ''}
|
|
799
|
+
</label>
|
|
800
|
+
<input
|
|
801
|
+
id="dauth-lastname"
|
|
802
|
+
type="text"
|
|
803
|
+
value={lastname}
|
|
804
|
+
onChange={(e) =>
|
|
805
|
+
setLastname(e.target.value)
|
|
806
|
+
}
|
|
807
|
+
placeholder="Your last name"
|
|
808
|
+
disabled={saving}
|
|
809
|
+
style={input}
|
|
810
|
+
onFocus={inputFocusHandler}
|
|
811
|
+
onBlur={inputBlurHandler}
|
|
812
|
+
/>
|
|
813
|
+
</div>
|
|
814
|
+
)}
|
|
815
|
+
|
|
816
|
+
{hasField('nickname') && (
|
|
817
|
+
<div style={fieldGroup}>
|
|
818
|
+
<label
|
|
819
|
+
htmlFor="dauth-nickname"
|
|
820
|
+
style={label}
|
|
821
|
+
>
|
|
822
|
+
Nickname
|
|
823
|
+
{isRequired('nickname') ? ' *' : ''}
|
|
824
|
+
</label>
|
|
825
|
+
<input
|
|
826
|
+
id="dauth-nickname"
|
|
827
|
+
type="text"
|
|
828
|
+
value={nickname}
|
|
829
|
+
onChange={(e) =>
|
|
830
|
+
setNickname(e.target.value)
|
|
831
|
+
}
|
|
832
|
+
placeholder="Choose a nickname"
|
|
833
|
+
disabled={saving}
|
|
834
|
+
style={input}
|
|
835
|
+
onFocus={inputFocusHandler}
|
|
836
|
+
onBlur={inputBlurHandler}
|
|
837
|
+
/>
|
|
838
|
+
</div>
|
|
839
|
+
)}
|
|
840
|
+
|
|
841
|
+
{hasField('country') && (
|
|
842
|
+
<div style={fieldGroup}>
|
|
843
|
+
<label
|
|
844
|
+
htmlFor="dauth-country"
|
|
845
|
+
style={label}
|
|
846
|
+
>
|
|
847
|
+
Country
|
|
848
|
+
{isRequired('country') ? ' *' : ''}
|
|
849
|
+
</label>
|
|
850
|
+
<input
|
|
851
|
+
id="dauth-country"
|
|
852
|
+
type="text"
|
|
853
|
+
value={country}
|
|
854
|
+
onChange={(e) =>
|
|
855
|
+
setCountry(e.target.value)
|
|
856
|
+
}
|
|
857
|
+
placeholder="Your country"
|
|
858
|
+
disabled={saving}
|
|
859
|
+
style={input}
|
|
860
|
+
onFocus={inputFocusHandler}
|
|
861
|
+
onBlur={inputBlurHandler}
|
|
862
|
+
/>
|
|
863
|
+
</div>
|
|
864
|
+
)}
|
|
460
865
|
</div>
|
|
461
|
-
)}
|
|
462
866
|
|
|
463
|
-
|
|
867
|
+
{/* Language selector */}
|
|
868
|
+
<hr style={separator} />
|
|
464
869
|
<div style={fieldGroup}>
|
|
465
|
-
<
|
|
466
|
-
|
|
467
|
-
{
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
870
|
+
<div style={label}>Language</div>
|
|
871
|
+
<div style={{ display: 'flex', gap: 8 }}>
|
|
872
|
+
{(['es', 'en'] as const).map((lang) => (
|
|
873
|
+
<button
|
|
874
|
+
key={lang}
|
|
875
|
+
type="button"
|
|
876
|
+
style={{
|
|
877
|
+
...langBtn,
|
|
878
|
+
backgroundColor:
|
|
879
|
+
user.language === lang
|
|
880
|
+
? 'var(--dauth-accent, #6366f1)'
|
|
881
|
+
: 'var(--dauth-surface-secondary, rgba(255, 255, 255, 0.04))',
|
|
882
|
+
color:
|
|
883
|
+
user.language === lang
|
|
884
|
+
? '#ffffff'
|
|
885
|
+
: 'var(--dauth-text-secondary, #a1a1aa)',
|
|
886
|
+
}}
|
|
887
|
+
onClick={() => handleLanguage(lang)}
|
|
888
|
+
>
|
|
889
|
+
{lang === 'es'
|
|
890
|
+
? 'Espa\u00f1ol'
|
|
891
|
+
: 'English'}
|
|
892
|
+
</button>
|
|
893
|
+
))}
|
|
894
|
+
</div>
|
|
480
895
|
</div>
|
|
481
|
-
|
|
896
|
+
</>
|
|
897
|
+
)}
|
|
482
898
|
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
899
|
+
{/* ========== SECURITY TAB ========== */}
|
|
900
|
+
{activeTab === 'security' && showSecurity && (
|
|
901
|
+
<>
|
|
902
|
+
<div
|
|
903
|
+
style={{
|
|
904
|
+
display: 'flex',
|
|
905
|
+
alignItems: 'center',
|
|
906
|
+
justifyContent: 'space-between',
|
|
907
|
+
marginBottom: 16,
|
|
908
|
+
}}
|
|
909
|
+
>
|
|
910
|
+
<div
|
|
911
|
+
style={{
|
|
912
|
+
...label,
|
|
913
|
+
marginBottom: 0,
|
|
914
|
+
fontWeight: 600,
|
|
915
|
+
}}
|
|
916
|
+
>
|
|
917
|
+
Passkeys
|
|
918
|
+
</div>
|
|
919
|
+
<button
|
|
920
|
+
type="button"
|
|
921
|
+
style={outlineBtn}
|
|
922
|
+
onClick={() =>
|
|
923
|
+
setShowRegister(!showRegister)
|
|
924
|
+
}
|
|
925
|
+
onMouseEnter={(e) =>
|
|
926
|
+
(e.currentTarget.style.backgroundColor =
|
|
927
|
+
'var(--dauth-surface-hover, #232340)')
|
|
928
|
+
}
|
|
929
|
+
onMouseLeave={(e) =>
|
|
930
|
+
(e.currentTarget.style.backgroundColor =
|
|
931
|
+
'transparent')
|
|
932
|
+
}
|
|
933
|
+
>
|
|
934
|
+
+ Add passkey
|
|
935
|
+
</button>
|
|
500
936
|
</div>
|
|
501
|
-
)}
|
|
502
|
-
</div>
|
|
503
937
|
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
938
|
+
{/* Register passkey form */}
|
|
939
|
+
{showRegister && (
|
|
940
|
+
<div style={registerPanel}>
|
|
941
|
+
<div style={fieldGroup}>
|
|
942
|
+
<label
|
|
943
|
+
htmlFor="dauth-passkey-name"
|
|
944
|
+
style={label}
|
|
945
|
+
>
|
|
946
|
+
Passkey name (optional)
|
|
947
|
+
</label>
|
|
948
|
+
<input
|
|
949
|
+
id="dauth-passkey-name"
|
|
950
|
+
type="text"
|
|
951
|
+
value={passkeyName}
|
|
952
|
+
onChange={(e) =>
|
|
953
|
+
setPasskeyName(e.target.value)
|
|
954
|
+
}
|
|
955
|
+
placeholder="e.g. MacBook Touch ID"
|
|
956
|
+
disabled={registering}
|
|
957
|
+
style={input}
|
|
958
|
+
onFocus={inputFocusHandler}
|
|
959
|
+
onBlur={inputBlurHandler}
|
|
960
|
+
/>
|
|
961
|
+
</div>
|
|
962
|
+
<div
|
|
963
|
+
style={{
|
|
964
|
+
display: 'flex',
|
|
965
|
+
gap: 8,
|
|
966
|
+
}}
|
|
967
|
+
>
|
|
968
|
+
<button
|
|
969
|
+
type="button"
|
|
970
|
+
style={{
|
|
971
|
+
...smallAccentBtn,
|
|
972
|
+
opacity: registering ? 0.6 : 1,
|
|
973
|
+
}}
|
|
974
|
+
disabled={registering}
|
|
975
|
+
onClick={handleRegisterPasskey}
|
|
976
|
+
>
|
|
977
|
+
{registering ? (
|
|
978
|
+
<Spinner />
|
|
979
|
+
) : (
|
|
980
|
+
<IconFingerprint />
|
|
981
|
+
)}
|
|
982
|
+
{registering
|
|
983
|
+
? 'Registering...'
|
|
984
|
+
: 'Register'}
|
|
985
|
+
</button>
|
|
986
|
+
<button
|
|
987
|
+
type="button"
|
|
988
|
+
style={cancelBtn}
|
|
989
|
+
onClick={() =>
|
|
990
|
+
setShowRegister(false)
|
|
991
|
+
}
|
|
992
|
+
onMouseEnter={(e) =>
|
|
993
|
+
(e.currentTarget.style.backgroundColor =
|
|
994
|
+
'var(--dauth-surface-hover, #232340)')
|
|
995
|
+
}
|
|
996
|
+
onMouseLeave={(e) =>
|
|
997
|
+
(e.currentTarget.style.backgroundColor =
|
|
998
|
+
'transparent')
|
|
999
|
+
}
|
|
1000
|
+
>
|
|
1001
|
+
Cancel
|
|
1002
|
+
</button>
|
|
1003
|
+
</div>
|
|
532
1004
|
</div>
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
placeholder={`Type ${CONFIRM_WORD}`}
|
|
538
|
-
style={input}
|
|
539
|
-
onFocus={inputFocusHandler}
|
|
540
|
-
onBlur={inputBlurHandler}
|
|
541
|
-
disabled={deleting}
|
|
542
|
-
/>
|
|
1005
|
+
)}
|
|
1006
|
+
|
|
1007
|
+
{/* Passkey status */}
|
|
1008
|
+
{passkeyStatus && (
|
|
543
1009
|
<div
|
|
1010
|
+
role="status"
|
|
1011
|
+
aria-live="polite"
|
|
544
1012
|
style={{
|
|
545
|
-
|
|
546
|
-
gap: 8,
|
|
1013
|
+
...statusMsg(passkeyStatus.type),
|
|
547
1014
|
marginTop: 12,
|
|
548
1015
|
}}
|
|
549
1016
|
>
|
|
1017
|
+
{passkeyStatus.message}
|
|
1018
|
+
</div>
|
|
1019
|
+
)}
|
|
1020
|
+
|
|
1021
|
+
{/* Credentials list */}
|
|
1022
|
+
<div style={{ marginTop: 12 }}>
|
|
1023
|
+
{loadingCreds ? (
|
|
1024
|
+
<div
|
|
1025
|
+
style={{
|
|
1026
|
+
textAlign: 'center',
|
|
1027
|
+
padding: 24,
|
|
1028
|
+
}}
|
|
1029
|
+
>
|
|
1030
|
+
<Spinner />
|
|
1031
|
+
</div>
|
|
1032
|
+
) : credentials.length > 0 ? (
|
|
1033
|
+
credentials.map((cred) => (
|
|
1034
|
+
<div
|
|
1035
|
+
key={cred._id}
|
|
1036
|
+
style={credentialRow}
|
|
1037
|
+
>
|
|
1038
|
+
<div
|
|
1039
|
+
style={{
|
|
1040
|
+
display: 'flex',
|
|
1041
|
+
alignItems: 'center',
|
|
1042
|
+
gap: 12,
|
|
1043
|
+
flex: 1,
|
|
1044
|
+
minWidth: 0,
|
|
1045
|
+
}}
|
|
1046
|
+
>
|
|
1047
|
+
<span
|
|
1048
|
+
style={{
|
|
1049
|
+
color:
|
|
1050
|
+
'var(--dauth-accent, #6366f1)',
|
|
1051
|
+
flexShrink: 0,
|
|
1052
|
+
}}
|
|
1053
|
+
>
|
|
1054
|
+
<IconFingerprint />
|
|
1055
|
+
</span>
|
|
1056
|
+
<div
|
|
1057
|
+
style={{
|
|
1058
|
+
minWidth: 0,
|
|
1059
|
+
flex: 1,
|
|
1060
|
+
}}
|
|
1061
|
+
>
|
|
1062
|
+
<div
|
|
1063
|
+
style={{
|
|
1064
|
+
fontSize:
|
|
1065
|
+
'var(--dauth-font-size-sm, 0.875rem)',
|
|
1066
|
+
fontWeight: 500,
|
|
1067
|
+
color:
|
|
1068
|
+
'var(--dauth-text-primary, #e4e4e7)',
|
|
1069
|
+
overflow: 'hidden',
|
|
1070
|
+
textOverflow: 'ellipsis',
|
|
1071
|
+
whiteSpace:
|
|
1072
|
+
'nowrap' as const,
|
|
1073
|
+
}}
|
|
1074
|
+
>
|
|
1075
|
+
{cred.name || 'Passkey'}
|
|
1076
|
+
</div>
|
|
1077
|
+
<div
|
|
1078
|
+
style={{
|
|
1079
|
+
fontSize:
|
|
1080
|
+
'var(--dauth-font-size-xs, 0.75rem)',
|
|
1081
|
+
color:
|
|
1082
|
+
'var(--dauth-text-muted, #71717a)',
|
|
1083
|
+
}}
|
|
1084
|
+
>
|
|
1085
|
+
{cred.deviceType ===
|
|
1086
|
+
'multiDevice'
|
|
1087
|
+
? 'Synced'
|
|
1088
|
+
: 'Device-bound'}
|
|
1089
|
+
{cred.createdAt &&
|
|
1090
|
+
` \u00b7 Created ${new Date(cred.createdAt).toLocaleDateString()}`}
|
|
1091
|
+
</div>
|
|
1092
|
+
</div>
|
|
1093
|
+
</div>
|
|
1094
|
+
<button
|
|
1095
|
+
type="button"
|
|
1096
|
+
onClick={() =>
|
|
1097
|
+
handleDeletePasskey(cred._id)
|
|
1098
|
+
}
|
|
1099
|
+
style={trashBtn}
|
|
1100
|
+
onMouseEnter={(e) =>
|
|
1101
|
+
(e.currentTarget.style.color =
|
|
1102
|
+
'var(--dauth-error, #ef4444)')
|
|
1103
|
+
}
|
|
1104
|
+
onMouseLeave={(e) =>
|
|
1105
|
+
(e.currentTarget.style.color =
|
|
1106
|
+
'var(--dauth-text-muted, #71717a)')
|
|
1107
|
+
}
|
|
1108
|
+
aria-label={`Delete passkey ${cred.name || ''}`}
|
|
1109
|
+
>
|
|
1110
|
+
<IconTrash />
|
|
1111
|
+
</button>
|
|
1112
|
+
</div>
|
|
1113
|
+
))
|
|
1114
|
+
) : (
|
|
1115
|
+
<div style={emptyState}>
|
|
1116
|
+
<span
|
|
1117
|
+
style={{
|
|
1118
|
+
color:
|
|
1119
|
+
'var(--dauth-accent, #6366f1)',
|
|
1120
|
+
}}
|
|
1121
|
+
>
|
|
1122
|
+
<IconShield />
|
|
1123
|
+
</span>
|
|
1124
|
+
<div>
|
|
1125
|
+
<div
|
|
1126
|
+
style={{
|
|
1127
|
+
fontSize:
|
|
1128
|
+
'var(--dauth-font-size-sm, 0.875rem)',
|
|
1129
|
+
fontWeight: 500,
|
|
1130
|
+
color:
|
|
1131
|
+
'var(--dauth-text-primary, #e4e4e7)',
|
|
1132
|
+
}}
|
|
1133
|
+
>
|
|
1134
|
+
No passkeys registered
|
|
1135
|
+
</div>
|
|
1136
|
+
<div
|
|
1137
|
+
style={{
|
|
1138
|
+
fontSize:
|
|
1139
|
+
'var(--dauth-font-size-xs, 0.75rem)',
|
|
1140
|
+
color:
|
|
1141
|
+
'var(--dauth-text-secondary, #a1a1aa)',
|
|
1142
|
+
}}
|
|
1143
|
+
>
|
|
1144
|
+
Add a passkey for faster, more
|
|
1145
|
+
secure sign-in.
|
|
1146
|
+
</div>
|
|
1147
|
+
</div>
|
|
1148
|
+
</div>
|
|
1149
|
+
)}
|
|
1150
|
+
</div>
|
|
1151
|
+
</>
|
|
1152
|
+
)}
|
|
1153
|
+
|
|
1154
|
+
{/* ========== ACCOUNT TAB ========== */}
|
|
1155
|
+
{activeTab === 'account' && (
|
|
1156
|
+
<>
|
|
1157
|
+
{/* Status (shared) */}
|
|
1158
|
+
{status && (
|
|
1159
|
+
<div
|
|
1160
|
+
role="status"
|
|
1161
|
+
aria-live="polite"
|
|
1162
|
+
style={statusMsg(status.type)}
|
|
1163
|
+
>
|
|
1164
|
+
{status.message}
|
|
1165
|
+
</div>
|
|
1166
|
+
)}
|
|
1167
|
+
|
|
1168
|
+
{/* Delete account */}
|
|
1169
|
+
<div>
|
|
1170
|
+
<div style={dangerTitle}>
|
|
1171
|
+
Delete account
|
|
1172
|
+
</div>
|
|
1173
|
+
<div style={dangerDesc}>
|
|
1174
|
+
Permanently delete your account and all
|
|
1175
|
+
associated data.
|
|
1176
|
+
</div>
|
|
1177
|
+
{!showDelete ? (
|
|
550
1178
|
<button
|
|
551
1179
|
type="button"
|
|
552
|
-
style={
|
|
553
|
-
onClick={() =>
|
|
554
|
-
setShowDelete(false);
|
|
555
|
-
setDeleteText('');
|
|
556
|
-
}}
|
|
1180
|
+
style={deleteBtn}
|
|
1181
|
+
onClick={() => setShowDelete(true)}
|
|
557
1182
|
onMouseEnter={(e) =>
|
|
558
1183
|
(e.currentTarget.style.backgroundColor =
|
|
559
|
-
'
|
|
1184
|
+
'rgba(239, 68, 68, 0.2)')
|
|
560
1185
|
}
|
|
561
1186
|
onMouseLeave={(e) =>
|
|
562
|
-
(e.currentTarget.style.backgroundColor =
|
|
1187
|
+
(e.currentTarget.style.backgroundColor =
|
|
1188
|
+
'var(--dauth-error-bg, rgba(239, 68, 68, 0.1))')
|
|
563
1189
|
}
|
|
564
1190
|
>
|
|
565
|
-
|
|
566
|
-
</button>
|
|
567
|
-
<button
|
|
568
|
-
type="button"
|
|
569
|
-
style={{
|
|
570
|
-
...deleteConfirmBtn,
|
|
571
|
-
opacity:
|
|
572
|
-
deleteText !== CONFIRM_WORD || deleting ? 0.5 : 1,
|
|
573
|
-
cursor:
|
|
574
|
-
deleteText !== CONFIRM_WORD || deleting
|
|
575
|
-
? 'not-allowed'
|
|
576
|
-
: 'pointer',
|
|
577
|
-
}}
|
|
578
|
-
disabled={deleteText !== CONFIRM_WORD || deleting}
|
|
579
|
-
onClick={handleDelete}
|
|
580
|
-
>
|
|
581
|
-
{deleting && <Spinner />}
|
|
582
|
-
Delete my account
|
|
1191
|
+
Delete account
|
|
583
1192
|
</button>
|
|
584
|
-
|
|
1193
|
+
) : (
|
|
1194
|
+
<div style={deletePanel}>
|
|
1195
|
+
<div style={deletePanelText}>
|
|
1196
|
+
This action is permanent and cannot
|
|
1197
|
+
be undone. Type{' '}
|
|
1198
|
+
<strong>{CONFIRM_WORD}</strong> to
|
|
1199
|
+
confirm.
|
|
1200
|
+
</div>
|
|
1201
|
+
<input
|
|
1202
|
+
type="text"
|
|
1203
|
+
value={deleteText}
|
|
1204
|
+
onChange={(e) =>
|
|
1205
|
+
setDeleteText(e.target.value)
|
|
1206
|
+
}
|
|
1207
|
+
placeholder={`Type ${CONFIRM_WORD}`}
|
|
1208
|
+
style={input}
|
|
1209
|
+
onFocus={inputFocusHandler}
|
|
1210
|
+
onBlur={inputBlurHandler}
|
|
1211
|
+
disabled={deleting}
|
|
1212
|
+
/>
|
|
1213
|
+
<div
|
|
1214
|
+
style={{
|
|
1215
|
+
display: 'flex',
|
|
1216
|
+
gap: 8,
|
|
1217
|
+
marginTop: 12,
|
|
1218
|
+
}}
|
|
1219
|
+
>
|
|
1220
|
+
<button
|
|
1221
|
+
type="button"
|
|
1222
|
+
style={cancelBtn}
|
|
1223
|
+
onClick={() => {
|
|
1224
|
+
setShowDelete(false);
|
|
1225
|
+
setDeleteText('');
|
|
1226
|
+
}}
|
|
1227
|
+
onMouseEnter={(e) =>
|
|
1228
|
+
(e.currentTarget.style.backgroundColor =
|
|
1229
|
+
'var(--dauth-surface-hover, #232340)')
|
|
1230
|
+
}
|
|
1231
|
+
onMouseLeave={(e) =>
|
|
1232
|
+
(e.currentTarget.style.backgroundColor =
|
|
1233
|
+
'transparent')
|
|
1234
|
+
}
|
|
1235
|
+
>
|
|
1236
|
+
Cancel
|
|
1237
|
+
</button>
|
|
1238
|
+
<button
|
|
1239
|
+
type="button"
|
|
1240
|
+
style={{
|
|
1241
|
+
...deleteConfirmBtn,
|
|
1242
|
+
opacity:
|
|
1243
|
+
deleteText !== CONFIRM_WORD ||
|
|
1244
|
+
deleting
|
|
1245
|
+
? 0.5
|
|
1246
|
+
: 1,
|
|
1247
|
+
cursor:
|
|
1248
|
+
deleteText !== CONFIRM_WORD ||
|
|
1249
|
+
deleting
|
|
1250
|
+
? 'not-allowed'
|
|
1251
|
+
: 'pointer',
|
|
1252
|
+
}}
|
|
1253
|
+
disabled={
|
|
1254
|
+
deleteText !== CONFIRM_WORD ||
|
|
1255
|
+
deleting
|
|
1256
|
+
}
|
|
1257
|
+
onClick={handleDelete}
|
|
1258
|
+
>
|
|
1259
|
+
{deleting && <Spinner />}
|
|
1260
|
+
Delete my account
|
|
1261
|
+
</button>
|
|
1262
|
+
</div>
|
|
1263
|
+
</div>
|
|
1264
|
+
)}
|
|
585
1265
|
</div>
|
|
586
|
-
|
|
587
|
-
|
|
1266
|
+
|
|
1267
|
+
{/* Sign out */}
|
|
1268
|
+
<hr style={separator} />
|
|
1269
|
+
<button
|
|
1270
|
+
type="button"
|
|
1271
|
+
style={signOutBtn}
|
|
1272
|
+
onClick={handleSignOut}
|
|
1273
|
+
onMouseEnter={(e) =>
|
|
1274
|
+
(e.currentTarget.style.backgroundColor =
|
|
1275
|
+
'rgba(239, 68, 68, 0.2)')
|
|
1276
|
+
}
|
|
1277
|
+
onMouseLeave={(e) =>
|
|
1278
|
+
(e.currentTarget.style.backgroundColor =
|
|
1279
|
+
'var(--dauth-error-bg, rgba(239, 68, 68, 0.1))')
|
|
1280
|
+
}
|
|
1281
|
+
>
|
|
1282
|
+
<IconLogOut />
|
|
1283
|
+
Sign out
|
|
1284
|
+
</button>
|
|
1285
|
+
</>
|
|
1286
|
+
)}
|
|
588
1287
|
</div>
|
|
589
1288
|
|
|
590
|
-
{/* Footer */}
|
|
591
|
-
|
|
592
|
-
<
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
1289
|
+
{/* Footer — only for Profile tab */}
|
|
1290
|
+
{activeTab === 'profile' && (
|
|
1291
|
+
<div style={footerStyle(isDesktop)}>
|
|
1292
|
+
<button
|
|
1293
|
+
type="button"
|
|
1294
|
+
style={{
|
|
1295
|
+
...saveBtn,
|
|
1296
|
+
opacity: canSave ? 1 : 0.5,
|
|
1297
|
+
cursor: canSave ? 'pointer' : 'not-allowed',
|
|
1298
|
+
}}
|
|
1299
|
+
disabled={!canSave}
|
|
1300
|
+
onClick={handleSave}
|
|
1301
|
+
aria-busy={saving}
|
|
1302
|
+
onMouseEnter={(e) => {
|
|
1303
|
+
if (canSave)
|
|
1304
|
+
e.currentTarget.style.backgroundColor =
|
|
1305
|
+
'var(--dauth-accent-hover, #818cf8)';
|
|
1306
|
+
}}
|
|
1307
|
+
onMouseLeave={(e) => {
|
|
604
1308
|
e.currentTarget.style.backgroundColor =
|
|
605
|
-
'var(--dauth-accent
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
>
|
|
612
|
-
|
|
613
|
-
{saving ? 'Saving...' : 'Save changes'}
|
|
614
|
-
</button>
|
|
615
|
-
</div>
|
|
1309
|
+
'var(--dauth-accent, #6366f1)';
|
|
1310
|
+
}}
|
|
1311
|
+
>
|
|
1312
|
+
{saving && <Spinner />}
|
|
1313
|
+
{saving ? 'Saving...' : 'Save changes'}
|
|
1314
|
+
</button>
|
|
1315
|
+
</div>
|
|
1316
|
+
)}
|
|
616
1317
|
</div>
|
|
617
1318
|
</div>
|
|
618
1319
|
</>,
|
|
@@ -622,19 +1323,21 @@ export function DauthProfileModal({ open, onClose }: DauthProfileModalProps) {
|
|
|
622
1323
|
|
|
623
1324
|
// --- Style constants ---
|
|
624
1325
|
|
|
625
|
-
const headerStyle = (
|
|
1326
|
+
const headerStyle = (
|
|
1327
|
+
isDesktop: boolean
|
|
1328
|
+
): React.CSSProperties => ({
|
|
626
1329
|
display: 'flex',
|
|
627
1330
|
alignItems: 'center',
|
|
628
1331
|
justifyContent: 'space-between',
|
|
629
|
-
padding: '16px 24px',
|
|
630
|
-
borderBottom: '1px solid var(--dauth-border, rgba(255, 255, 255, 0.08))',
|
|
1332
|
+
padding: '16px 24px 0',
|
|
631
1333
|
flexShrink: 0,
|
|
632
1334
|
...(!isDesktop
|
|
633
1335
|
? {
|
|
634
1336
|
position: 'sticky' as const,
|
|
635
1337
|
top: 0,
|
|
636
1338
|
zIndex: 1,
|
|
637
|
-
backgroundColor:
|
|
1339
|
+
backgroundColor:
|
|
1340
|
+
'var(--dauth-surface, #1a1a2e)',
|
|
638
1341
|
}
|
|
639
1342
|
: {}),
|
|
640
1343
|
});
|
|
@@ -663,6 +1366,28 @@ const closeBtn: React.CSSProperties = {
|
|
|
663
1366
|
padding: 0,
|
|
664
1367
|
};
|
|
665
1368
|
|
|
1369
|
+
const tabBar: React.CSSProperties = {
|
|
1370
|
+
display: 'flex',
|
|
1371
|
+
padding: '0 24px',
|
|
1372
|
+
borderBottom:
|
|
1373
|
+
'1px solid var(--dauth-border, rgba(255, 255, 255, 0.08))',
|
|
1374
|
+
flexShrink: 0,
|
|
1375
|
+
};
|
|
1376
|
+
|
|
1377
|
+
const tabBtn: React.CSSProperties = {
|
|
1378
|
+
flex: 1,
|
|
1379
|
+
padding: '12px 4px',
|
|
1380
|
+
fontSize: 'var(--dauth-font-size-sm, 0.875rem)',
|
|
1381
|
+
fontWeight: 500,
|
|
1382
|
+
border: 'none',
|
|
1383
|
+
borderBottom: '2px solid transparent',
|
|
1384
|
+
backgroundColor: 'transparent',
|
|
1385
|
+
cursor: 'pointer',
|
|
1386
|
+
transition: 'color 150ms, border-color 150ms',
|
|
1387
|
+
fontFamily: 'inherit',
|
|
1388
|
+
textAlign: 'center',
|
|
1389
|
+
};
|
|
1390
|
+
|
|
666
1391
|
const bodyStyle: React.CSSProperties = {
|
|
667
1392
|
flex: 1,
|
|
668
1393
|
overflowY: 'auto',
|
|
@@ -692,12 +1417,27 @@ const avatarCircle: React.CSSProperties = {
|
|
|
692
1417
|
fontWeight: 600,
|
|
693
1418
|
};
|
|
694
1419
|
|
|
1420
|
+
const avatarOverlay: React.CSSProperties = {
|
|
1421
|
+
position: 'absolute',
|
|
1422
|
+
inset: 0,
|
|
1423
|
+
backgroundColor: 'rgba(0, 0, 0, 0.4)',
|
|
1424
|
+
display: 'flex',
|
|
1425
|
+
alignItems: 'center',
|
|
1426
|
+
justifyContent: 'center',
|
|
1427
|
+
borderRadius: '50%',
|
|
1428
|
+
opacity: 0.7,
|
|
1429
|
+
transition: 'opacity 150ms',
|
|
1430
|
+
color: '#ffffff',
|
|
1431
|
+
};
|
|
1432
|
+
|
|
695
1433
|
const emailText: React.CSSProperties = {
|
|
696
1434
|
fontSize: 'var(--dauth-font-size-sm, 0.875rem)',
|
|
697
1435
|
color: 'var(--dauth-text-secondary, #a1a1aa)',
|
|
698
1436
|
};
|
|
699
1437
|
|
|
700
|
-
const statusMsg = (
|
|
1438
|
+
const statusMsg = (
|
|
1439
|
+
type: 'success' | 'error'
|
|
1440
|
+
): React.CSSProperties => ({
|
|
701
1441
|
padding: '10px 14px',
|
|
702
1442
|
borderRadius: 'var(--dauth-radius-sm, 8px)',
|
|
703
1443
|
fontSize: 'var(--dauth-font-size-sm, 0.875rem)',
|
|
@@ -714,7 +1454,9 @@ const statusMsg = (type: 'success' | 'error'): React.CSSProperties => ({
|
|
|
714
1454
|
lineHeight: 1.5,
|
|
715
1455
|
});
|
|
716
1456
|
|
|
717
|
-
const fieldGroup: React.CSSProperties = {
|
|
1457
|
+
const fieldGroup: React.CSSProperties = {
|
|
1458
|
+
marginBottom: 16,
|
|
1459
|
+
};
|
|
718
1460
|
|
|
719
1461
|
const label: React.CSSProperties = {
|
|
720
1462
|
display: 'block',
|
|
@@ -730,8 +1472,10 @@ const input: React.CSSProperties = {
|
|
|
730
1472
|
fontSize: 'var(--dauth-font-size-base, 1rem)',
|
|
731
1473
|
lineHeight: 1.5,
|
|
732
1474
|
color: 'var(--dauth-text-primary, #e4e4e7)',
|
|
733
|
-
backgroundColor:
|
|
734
|
-
|
|
1475
|
+
backgroundColor:
|
|
1476
|
+
'var(--dauth-surface-secondary, rgba(255, 255, 255, 0.04))',
|
|
1477
|
+
border:
|
|
1478
|
+
'1px solid var(--dauth-border, rgba(255, 255, 255, 0.08))',
|
|
735
1479
|
borderRadius: 'var(--dauth-radius-input, 8px)',
|
|
736
1480
|
outline: 'none',
|
|
737
1481
|
transition: 'border-color 150ms, box-shadow 150ms',
|
|
@@ -739,25 +1483,119 @@ const input: React.CSSProperties = {
|
|
|
739
1483
|
fontFamily: 'inherit',
|
|
740
1484
|
};
|
|
741
1485
|
|
|
742
|
-
const inputFocusHandler = (
|
|
1486
|
+
const inputFocusHandler = (
|
|
1487
|
+
e: React.FocusEvent<HTMLInputElement>
|
|
1488
|
+
) => {
|
|
743
1489
|
e.currentTarget.style.borderColor =
|
|
744
1490
|
'var(--dauth-border-focus, rgba(99, 102, 241, 0.5))';
|
|
745
|
-
e.currentTarget.style.boxShadow =
|
|
1491
|
+
e.currentTarget.style.boxShadow =
|
|
1492
|
+
'0 0 0 3px rgba(99, 102, 241, 0.15)';
|
|
746
1493
|
};
|
|
747
1494
|
|
|
748
|
-
const inputBlurHandler = (
|
|
1495
|
+
const inputBlurHandler = (
|
|
1496
|
+
e: React.FocusEvent<HTMLInputElement>
|
|
1497
|
+
) => {
|
|
749
1498
|
e.currentTarget.style.borderColor =
|
|
750
1499
|
'var(--dauth-border, rgba(255, 255, 255, 0.08))';
|
|
751
1500
|
e.currentTarget.style.boxShadow = 'none';
|
|
752
1501
|
};
|
|
753
1502
|
|
|
1503
|
+
const langBtn: React.CSSProperties = {
|
|
1504
|
+
padding: '8px 16px',
|
|
1505
|
+
fontSize: 'var(--dauth-font-size-sm, 0.875rem)',
|
|
1506
|
+
fontWeight: 500,
|
|
1507
|
+
border: 'none',
|
|
1508
|
+
borderRadius: 'var(--dauth-radius-sm, 8px)',
|
|
1509
|
+
cursor: 'pointer',
|
|
1510
|
+
transition: 'background-color 150ms, color 150ms',
|
|
1511
|
+
fontFamily: 'inherit',
|
|
1512
|
+
};
|
|
1513
|
+
|
|
754
1514
|
const separator: React.CSSProperties = {
|
|
755
1515
|
height: 1,
|
|
756
|
-
backgroundColor:
|
|
1516
|
+
backgroundColor:
|
|
1517
|
+
'var(--dauth-border, rgba(255, 255, 255, 0.08))',
|
|
757
1518
|
margin: '24px 0',
|
|
758
1519
|
border: 'none',
|
|
759
1520
|
};
|
|
760
1521
|
|
|
1522
|
+
const outlineBtn: React.CSSProperties = {
|
|
1523
|
+
padding: '6px 12px',
|
|
1524
|
+
fontSize: 'var(--dauth-font-size-xs, 0.75rem)',
|
|
1525
|
+
fontWeight: 500,
|
|
1526
|
+
color: 'var(--dauth-text-secondary, #a1a1aa)',
|
|
1527
|
+
backgroundColor: 'transparent',
|
|
1528
|
+
border:
|
|
1529
|
+
'1px solid var(--dauth-border, rgba(255, 255, 255, 0.08))',
|
|
1530
|
+
borderRadius: 'var(--dauth-radius-sm, 8px)',
|
|
1531
|
+
cursor: 'pointer',
|
|
1532
|
+
transition: 'background-color 150ms',
|
|
1533
|
+
fontFamily: 'inherit',
|
|
1534
|
+
};
|
|
1535
|
+
|
|
1536
|
+
const registerPanel: React.CSSProperties = {
|
|
1537
|
+
padding: 16,
|
|
1538
|
+
borderRadius: 'var(--dauth-radius-sm, 8px)',
|
|
1539
|
+
border:
|
|
1540
|
+
'1px solid var(--dauth-border, rgba(255, 255, 255, 0.08))',
|
|
1541
|
+
backgroundColor:
|
|
1542
|
+
'var(--dauth-surface-secondary, rgba(255, 255, 255, 0.04))',
|
|
1543
|
+
marginBottom: 12,
|
|
1544
|
+
};
|
|
1545
|
+
|
|
1546
|
+
const smallAccentBtn: React.CSSProperties = {
|
|
1547
|
+
padding: '8px 16px',
|
|
1548
|
+
fontSize: 'var(--dauth-font-size-sm, 0.875rem)',
|
|
1549
|
+
fontWeight: 500,
|
|
1550
|
+
color: '#ffffff',
|
|
1551
|
+
backgroundColor: 'var(--dauth-accent, #6366f1)',
|
|
1552
|
+
border: 'none',
|
|
1553
|
+
borderRadius: 'var(--dauth-radius-sm, 8px)',
|
|
1554
|
+
cursor: 'pointer',
|
|
1555
|
+
transition: 'opacity 150ms',
|
|
1556
|
+
fontFamily: 'inherit',
|
|
1557
|
+
display: 'flex',
|
|
1558
|
+
alignItems: 'center',
|
|
1559
|
+
gap: 6,
|
|
1560
|
+
};
|
|
1561
|
+
|
|
1562
|
+
const credentialRow: React.CSSProperties = {
|
|
1563
|
+
display: 'flex',
|
|
1564
|
+
alignItems: 'center',
|
|
1565
|
+
justifyContent: 'space-between',
|
|
1566
|
+
padding: 12,
|
|
1567
|
+
borderRadius: 'var(--dauth-radius-sm, 8px)',
|
|
1568
|
+
backgroundColor:
|
|
1569
|
+
'var(--dauth-surface-secondary, rgba(255, 255, 255, 0.04))',
|
|
1570
|
+
marginBottom: 8,
|
|
1571
|
+
};
|
|
1572
|
+
|
|
1573
|
+
const trashBtn: React.CSSProperties = {
|
|
1574
|
+
display: 'flex',
|
|
1575
|
+
alignItems: 'center',
|
|
1576
|
+
justifyContent: 'center',
|
|
1577
|
+
width: 28,
|
|
1578
|
+
height: 28,
|
|
1579
|
+
border: 'none',
|
|
1580
|
+
backgroundColor: 'transparent',
|
|
1581
|
+
color: 'var(--dauth-text-muted, #71717a)',
|
|
1582
|
+
cursor: 'pointer',
|
|
1583
|
+
transition: 'color 150ms',
|
|
1584
|
+
padding: 0,
|
|
1585
|
+
borderRadius: 'var(--dauth-radius-sm, 8px)',
|
|
1586
|
+
flexShrink: 0,
|
|
1587
|
+
};
|
|
1588
|
+
|
|
1589
|
+
const emptyState: React.CSSProperties = {
|
|
1590
|
+
display: 'flex',
|
|
1591
|
+
alignItems: 'center',
|
|
1592
|
+
gap: 12,
|
|
1593
|
+
padding: 16,
|
|
1594
|
+
borderRadius: 'var(--dauth-radius-sm, 8px)',
|
|
1595
|
+
backgroundColor:
|
|
1596
|
+
'var(--dauth-surface-secondary, rgba(255, 255, 255, 0.04))',
|
|
1597
|
+
};
|
|
1598
|
+
|
|
761
1599
|
const dangerTitle: React.CSSProperties = {
|
|
762
1600
|
fontSize: 'var(--dauth-font-size-sm, 0.875rem)',
|
|
763
1601
|
fontWeight: 600,
|
|
@@ -777,7 +1615,8 @@ const deleteBtn: React.CSSProperties = {
|
|
|
777
1615
|
fontSize: 'var(--dauth-font-size-sm, 0.875rem)',
|
|
778
1616
|
fontWeight: 500,
|
|
779
1617
|
color: 'var(--dauth-error, #ef4444)',
|
|
780
|
-
backgroundColor:
|
|
1618
|
+
backgroundColor:
|
|
1619
|
+
'var(--dauth-error-bg, rgba(239, 68, 68, 0.1))',
|
|
781
1620
|
border: '1px solid rgba(239, 68, 68, 0.2)',
|
|
782
1621
|
borderRadius: 'var(--dauth-radius-sm, 8px)',
|
|
783
1622
|
cursor: 'pointer',
|
|
@@ -790,7 +1629,8 @@ const deletePanel: React.CSSProperties = {
|
|
|
790
1629
|
padding: 16,
|
|
791
1630
|
borderRadius: 'var(--dauth-radius-sm, 8px)',
|
|
792
1631
|
border: '1px solid rgba(239, 68, 68, 0.3)',
|
|
793
|
-
backgroundColor:
|
|
1632
|
+
backgroundColor:
|
|
1633
|
+
'var(--dauth-error-bg, rgba(239, 68, 68, 0.1))',
|
|
794
1634
|
};
|
|
795
1635
|
|
|
796
1636
|
const deletePanelText: React.CSSProperties = {
|
|
@@ -801,13 +1641,13 @@ const deletePanelText: React.CSSProperties = {
|
|
|
801
1641
|
};
|
|
802
1642
|
|
|
803
1643
|
const cancelBtn: React.CSSProperties = {
|
|
804
|
-
flex: 1,
|
|
805
1644
|
padding: '8px 16px',
|
|
806
1645
|
fontSize: 'var(--dauth-font-size-sm, 0.875rem)',
|
|
807
1646
|
fontWeight: 500,
|
|
808
1647
|
color: 'var(--dauth-text-secondary, #a1a1aa)',
|
|
809
1648
|
backgroundColor: 'transparent',
|
|
810
|
-
border:
|
|
1649
|
+
border:
|
|
1650
|
+
'1px solid var(--dauth-border, rgba(255, 255, 255, 0.08))',
|
|
811
1651
|
borderRadius: 'var(--dauth-radius-sm, 8px)',
|
|
812
1652
|
cursor: 'pointer',
|
|
813
1653
|
transition: 'background-color 150ms',
|
|
@@ -832,17 +1672,41 @@ const deleteConfirmBtn: React.CSSProperties = {
|
|
|
832
1672
|
gap: 8,
|
|
833
1673
|
};
|
|
834
1674
|
|
|
835
|
-
const
|
|
1675
|
+
const signOutBtn: React.CSSProperties = {
|
|
1676
|
+
width: '100%',
|
|
1677
|
+
padding: '12px 24px',
|
|
1678
|
+
fontSize: 'var(--dauth-font-size-base, 1rem)',
|
|
1679
|
+
fontWeight: 500,
|
|
1680
|
+
color: 'var(--dauth-error, #ef4444)',
|
|
1681
|
+
backgroundColor:
|
|
1682
|
+
'var(--dauth-error-bg, rgba(239, 68, 68, 0.1))',
|
|
1683
|
+
border: '1px solid rgba(239, 68, 68, 0.2)',
|
|
1684
|
+
borderRadius: 'var(--dauth-radius-sm, 8px)',
|
|
1685
|
+
cursor: 'pointer',
|
|
1686
|
+
transition: 'background-color 150ms',
|
|
1687
|
+
fontFamily: 'inherit',
|
|
1688
|
+
display: 'flex',
|
|
1689
|
+
alignItems: 'center',
|
|
1690
|
+
justifyContent: 'center',
|
|
1691
|
+
gap: 8,
|
|
1692
|
+
};
|
|
1693
|
+
|
|
1694
|
+
const footerStyle = (
|
|
1695
|
+
isDesktop: boolean
|
|
1696
|
+
): React.CSSProperties => ({
|
|
836
1697
|
padding: '16px 24px',
|
|
837
|
-
borderTop:
|
|
1698
|
+
borderTop:
|
|
1699
|
+
'1px solid var(--dauth-border, rgba(255, 255, 255, 0.08))',
|
|
838
1700
|
flexShrink: 0,
|
|
839
1701
|
...(!isDesktop
|
|
840
1702
|
? {
|
|
841
1703
|
position: 'sticky' as const,
|
|
842
1704
|
bottom: 0,
|
|
843
1705
|
zIndex: 1,
|
|
844
|
-
backgroundColor:
|
|
845
|
-
|
|
1706
|
+
backgroundColor:
|
|
1707
|
+
'var(--dauth-surface, #1a1a2e)',
|
|
1708
|
+
paddingBottom:
|
|
1709
|
+
'max(16px, env(safe-area-inset-bottom))',
|
|
846
1710
|
}
|
|
847
1711
|
: {}),
|
|
848
1712
|
});
|