dauth-context-react 6.1.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 +17 -2
- package/dist/index.d.ts +17 -2
- package/dist/index.js +1076 -161
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1076 -161
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/DauthProfileModal.tsx +1090 -241
- package/src/api/dauth.api.ts +73 -0
- package/src/api/interfaces/dauth.api.responses.ts +36 -0
- package/src/index.tsx +31 -1
- package/src/initialDauthState.ts +3 -0
- package/src/interfaces.ts +20 -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,24 +456,103 @@ 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
|
+
|
|
290
541
|
// Build CSS custom property overrides from domain.modalTheme
|
|
291
542
|
const themeVars = useMemo(() => {
|
|
292
543
|
const t = domain.modalTheme;
|
|
293
544
|
if (!t) return {};
|
|
294
545
|
const vars: Record<string, string> = {};
|
|
295
546
|
if (t.accent) vars['--dauth-accent'] = t.accent;
|
|
296
|
-
if (t.accentHover)
|
|
547
|
+
if (t.accentHover)
|
|
548
|
+
vars['--dauth-accent-hover'] = t.accentHover;
|
|
297
549
|
if (t.surface) vars['--dauth-surface'] = t.surface;
|
|
298
|
-
if (t.surfaceHover)
|
|
299
|
-
|
|
300
|
-
if (t.
|
|
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;
|
|
301
556
|
if (t.border) vars['--dauth-border'] = t.border;
|
|
302
557
|
return vars;
|
|
303
558
|
}, [domain.modalTheme]);
|
|
@@ -311,7 +566,8 @@ export function DauthProfileModal({ open, onClose }: DauthProfileModalProps) {
|
|
|
311
566
|
position: 'fixed',
|
|
312
567
|
inset: 0,
|
|
313
568
|
zIndex: 2147483647,
|
|
314
|
-
backgroundColor:
|
|
569
|
+
backgroundColor:
|
|
570
|
+
'var(--dauth-backdrop, rgba(0, 0, 0, 0.6))',
|
|
315
571
|
backdropFilter: 'blur(4px)',
|
|
316
572
|
WebkitBackdropFilter: 'blur(4px)',
|
|
317
573
|
display: 'flex',
|
|
@@ -329,8 +585,10 @@ export function DauthProfileModal({ open, onClose }: DauthProfileModalProps) {
|
|
|
329
585
|
margin: 16,
|
|
330
586
|
backgroundColor: 'var(--dauth-surface, #1a1a2e)',
|
|
331
587
|
borderRadius: 'var(--dauth-radius, 12px)',
|
|
332
|
-
boxShadow:
|
|
333
|
-
|
|
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))',
|
|
334
592
|
display: 'flex',
|
|
335
593
|
flexDirection: 'column',
|
|
336
594
|
overflow: 'hidden',
|
|
@@ -338,7 +596,10 @@ export function DauthProfileModal({ open, onClose }: DauthProfileModalProps) {
|
|
|
338
596
|
'var(--dauth-font-family, system-ui, -apple-system, sans-serif)',
|
|
339
597
|
color: 'var(--dauth-text-primary, #e4e4e7)',
|
|
340
598
|
opacity: phase === 'entered' ? 1 : 0,
|
|
341
|
-
transform:
|
|
599
|
+
transform:
|
|
600
|
+
phase === 'entered'
|
|
601
|
+
? 'translateY(0)'
|
|
602
|
+
: 'translateY(16px)',
|
|
342
603
|
transition: `opacity ${dur}ms ${easing}, transform ${dur}ms ${easing}`,
|
|
343
604
|
};
|
|
344
605
|
|
|
@@ -351,7 +612,10 @@ export function DauthProfileModal({ open, onClose }: DauthProfileModalProps) {
|
|
|
351
612
|
fontFamily:
|
|
352
613
|
'var(--dauth-font-family, system-ui, -apple-system, sans-serif)',
|
|
353
614
|
color: 'var(--dauth-text-primary, #e4e4e7)',
|
|
354
|
-
transform:
|
|
615
|
+
transform:
|
|
616
|
+
phase === 'entered'
|
|
617
|
+
? 'translateY(0)'
|
|
618
|
+
: 'translateY(100%)',
|
|
355
619
|
transition: `transform ${dur}ms ${easing}`,
|
|
356
620
|
};
|
|
357
621
|
|
|
@@ -359,11 +623,20 @@ export function DauthProfileModal({ open, onClose }: DauthProfileModalProps) {
|
|
|
359
623
|
.charAt(0)
|
|
360
624
|
.toUpperCase();
|
|
361
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
|
+
|
|
362
634
|
return createPortal(
|
|
363
635
|
<>
|
|
364
636
|
<style
|
|
365
637
|
dangerouslySetInnerHTML={{
|
|
366
|
-
__html:
|
|
638
|
+
__html:
|
|
639
|
+
'@keyframes dauth-spin{to{transform:rotate(360deg)}}',
|
|
367
640
|
}}
|
|
368
641
|
/>
|
|
369
642
|
<div
|
|
@@ -392,7 +665,8 @@ export function DauthProfileModal({ open, onClose }: DauthProfileModalProps) {
|
|
|
392
665
|
'var(--dauth-surface-hover, #232340)')
|
|
393
666
|
}
|
|
394
667
|
onMouseLeave={(e) =>
|
|
395
|
-
(e.currentTarget.style.backgroundColor =
|
|
668
|
+
(e.currentTarget.style.backgroundColor =
|
|
669
|
+
'transparent')
|
|
396
670
|
}
|
|
397
671
|
>
|
|
398
672
|
{isDesktop ? <IconClose /> : <IconBack />}
|
|
@@ -403,231 +677,643 @@ export function DauthProfileModal({ open, onClose }: DauthProfileModalProps) {
|
|
|
403
677
|
<div style={{ width: 36 }} />
|
|
404
678
|
</div>
|
|
405
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
|
+
|
|
406
706
|
{/* Scrollable body */}
|
|
407
707
|
<div style={bodyStyle}>
|
|
408
|
-
{/*
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
{
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
alt=""
|
|
708
|
+
{/* ========== PROFILE TAB ========== */}
|
|
709
|
+
{activeTab === 'profile' && (
|
|
710
|
+
<>
|
|
711
|
+
{/* Avatar */}
|
|
712
|
+
<div style={avatarSection}>
|
|
713
|
+
<div
|
|
415
714
|
style={{
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
715
|
+
...avatarCircle,
|
|
716
|
+
cursor: onAvatarUpload
|
|
717
|
+
? 'pointer'
|
|
718
|
+
: 'default',
|
|
719
|
+
position: 'relative' as const,
|
|
419
720
|
}}
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
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>
|
|
423
765
|
)}
|
|
424
|
-
</div>
|
|
425
|
-
<div style={emailText}>{user.email}</div>
|
|
426
|
-
</div>
|
|
427
766
|
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
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>
|
|
438
790
|
|
|
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
|
-
|
|
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
|
+
)}
|
|
475
865
|
</div>
|
|
476
|
-
)}
|
|
477
866
|
|
|
478
|
-
|
|
867
|
+
{/* Language selector */}
|
|
868
|
+
<hr style={separator} />
|
|
479
869
|
<div style={fieldGroup}>
|
|
480
|
-
<
|
|
481
|
-
|
|
482
|
-
{
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
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>
|
|
495
895
|
</div>
|
|
496
|
-
|
|
896
|
+
</>
|
|
897
|
+
)}
|
|
497
898
|
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
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>
|
|
515
936
|
</div>
|
|
516
|
-
)}
|
|
517
|
-
</div>
|
|
518
937
|
|
|
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
|
-
|
|
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>
|
|
547
1004
|
</div>
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
placeholder={`Type ${CONFIRM_WORD}`}
|
|
553
|
-
style={input}
|
|
554
|
-
onFocus={inputFocusHandler}
|
|
555
|
-
onBlur={inputBlurHandler}
|
|
556
|
-
disabled={deleting}
|
|
557
|
-
/>
|
|
1005
|
+
)}
|
|
1006
|
+
|
|
1007
|
+
{/* Passkey status */}
|
|
1008
|
+
{passkeyStatus && (
|
|
558
1009
|
<div
|
|
1010
|
+
role="status"
|
|
1011
|
+
aria-live="polite"
|
|
559
1012
|
style={{
|
|
560
|
-
|
|
561
|
-
gap: 8,
|
|
1013
|
+
...statusMsg(passkeyStatus.type),
|
|
562
1014
|
marginTop: 12,
|
|
563
1015
|
}}
|
|
564
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 ? (
|
|
565
1178
|
<button
|
|
566
1179
|
type="button"
|
|
567
|
-
style={
|
|
568
|
-
onClick={() =>
|
|
569
|
-
setShowDelete(false);
|
|
570
|
-
setDeleteText('');
|
|
571
|
-
}}
|
|
1180
|
+
style={deleteBtn}
|
|
1181
|
+
onClick={() => setShowDelete(true)}
|
|
572
1182
|
onMouseEnter={(e) =>
|
|
573
1183
|
(e.currentTarget.style.backgroundColor =
|
|
574
|
-
'
|
|
1184
|
+
'rgba(239, 68, 68, 0.2)')
|
|
575
1185
|
}
|
|
576
1186
|
onMouseLeave={(e) =>
|
|
577
|
-
(e.currentTarget.style.backgroundColor =
|
|
1187
|
+
(e.currentTarget.style.backgroundColor =
|
|
1188
|
+
'var(--dauth-error-bg, rgba(239, 68, 68, 0.1))')
|
|
578
1189
|
}
|
|
579
1190
|
>
|
|
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
|
|
1191
|
+
Delete account
|
|
598
1192
|
</button>
|
|
599
|
-
|
|
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
|
+
)}
|
|
600
1265
|
</div>
|
|
601
|
-
|
|
602
|
-
|
|
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
|
+
)}
|
|
603
1287
|
</div>
|
|
604
1288
|
|
|
605
|
-
{/* Footer */}
|
|
606
|
-
|
|
607
|
-
<
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
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) => {
|
|
619
1308
|
e.currentTarget.style.backgroundColor =
|
|
620
|
-
'var(--dauth-accent
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
>
|
|
627
|
-
|
|
628
|
-
{saving ? 'Saving...' : 'Save changes'}
|
|
629
|
-
</button>
|
|
630
|
-
</div>
|
|
1309
|
+
'var(--dauth-accent, #6366f1)';
|
|
1310
|
+
}}
|
|
1311
|
+
>
|
|
1312
|
+
{saving && <Spinner />}
|
|
1313
|
+
{saving ? 'Saving...' : 'Save changes'}
|
|
1314
|
+
</button>
|
|
1315
|
+
</div>
|
|
1316
|
+
)}
|
|
631
1317
|
</div>
|
|
632
1318
|
</div>
|
|
633
1319
|
</>,
|
|
@@ -637,19 +1323,21 @@ export function DauthProfileModal({ open, onClose }: DauthProfileModalProps) {
|
|
|
637
1323
|
|
|
638
1324
|
// --- Style constants ---
|
|
639
1325
|
|
|
640
|
-
const headerStyle = (
|
|
1326
|
+
const headerStyle = (
|
|
1327
|
+
isDesktop: boolean
|
|
1328
|
+
): React.CSSProperties => ({
|
|
641
1329
|
display: 'flex',
|
|
642
1330
|
alignItems: 'center',
|
|
643
1331
|
justifyContent: 'space-between',
|
|
644
|
-
padding: '16px 24px',
|
|
645
|
-
borderBottom: '1px solid var(--dauth-border, rgba(255, 255, 255, 0.08))',
|
|
1332
|
+
padding: '16px 24px 0',
|
|
646
1333
|
flexShrink: 0,
|
|
647
1334
|
...(!isDesktop
|
|
648
1335
|
? {
|
|
649
1336
|
position: 'sticky' as const,
|
|
650
1337
|
top: 0,
|
|
651
1338
|
zIndex: 1,
|
|
652
|
-
backgroundColor:
|
|
1339
|
+
backgroundColor:
|
|
1340
|
+
'var(--dauth-surface, #1a1a2e)',
|
|
653
1341
|
}
|
|
654
1342
|
: {}),
|
|
655
1343
|
});
|
|
@@ -678,6 +1366,28 @@ const closeBtn: React.CSSProperties = {
|
|
|
678
1366
|
padding: 0,
|
|
679
1367
|
};
|
|
680
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
|
+
|
|
681
1391
|
const bodyStyle: React.CSSProperties = {
|
|
682
1392
|
flex: 1,
|
|
683
1393
|
overflowY: 'auto',
|
|
@@ -707,12 +1417,27 @@ const avatarCircle: React.CSSProperties = {
|
|
|
707
1417
|
fontWeight: 600,
|
|
708
1418
|
};
|
|
709
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
|
+
|
|
710
1433
|
const emailText: React.CSSProperties = {
|
|
711
1434
|
fontSize: 'var(--dauth-font-size-sm, 0.875rem)',
|
|
712
1435
|
color: 'var(--dauth-text-secondary, #a1a1aa)',
|
|
713
1436
|
};
|
|
714
1437
|
|
|
715
|
-
const statusMsg = (
|
|
1438
|
+
const statusMsg = (
|
|
1439
|
+
type: 'success' | 'error'
|
|
1440
|
+
): React.CSSProperties => ({
|
|
716
1441
|
padding: '10px 14px',
|
|
717
1442
|
borderRadius: 'var(--dauth-radius-sm, 8px)',
|
|
718
1443
|
fontSize: 'var(--dauth-font-size-sm, 0.875rem)',
|
|
@@ -729,7 +1454,9 @@ const statusMsg = (type: 'success' | 'error'): React.CSSProperties => ({
|
|
|
729
1454
|
lineHeight: 1.5,
|
|
730
1455
|
});
|
|
731
1456
|
|
|
732
|
-
const fieldGroup: React.CSSProperties = {
|
|
1457
|
+
const fieldGroup: React.CSSProperties = {
|
|
1458
|
+
marginBottom: 16,
|
|
1459
|
+
};
|
|
733
1460
|
|
|
734
1461
|
const label: React.CSSProperties = {
|
|
735
1462
|
display: 'block',
|
|
@@ -745,8 +1472,10 @@ const input: React.CSSProperties = {
|
|
|
745
1472
|
fontSize: 'var(--dauth-font-size-base, 1rem)',
|
|
746
1473
|
lineHeight: 1.5,
|
|
747
1474
|
color: 'var(--dauth-text-primary, #e4e4e7)',
|
|
748
|
-
backgroundColor:
|
|
749
|
-
|
|
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))',
|
|
750
1479
|
borderRadius: 'var(--dauth-radius-input, 8px)',
|
|
751
1480
|
outline: 'none',
|
|
752
1481
|
transition: 'border-color 150ms, box-shadow 150ms',
|
|
@@ -754,25 +1483,119 @@ const input: React.CSSProperties = {
|
|
|
754
1483
|
fontFamily: 'inherit',
|
|
755
1484
|
};
|
|
756
1485
|
|
|
757
|
-
const inputFocusHandler = (
|
|
1486
|
+
const inputFocusHandler = (
|
|
1487
|
+
e: React.FocusEvent<HTMLInputElement>
|
|
1488
|
+
) => {
|
|
758
1489
|
e.currentTarget.style.borderColor =
|
|
759
1490
|
'var(--dauth-border-focus, rgba(99, 102, 241, 0.5))';
|
|
760
|
-
e.currentTarget.style.boxShadow =
|
|
1491
|
+
e.currentTarget.style.boxShadow =
|
|
1492
|
+
'0 0 0 3px rgba(99, 102, 241, 0.15)';
|
|
761
1493
|
};
|
|
762
1494
|
|
|
763
|
-
const inputBlurHandler = (
|
|
1495
|
+
const inputBlurHandler = (
|
|
1496
|
+
e: React.FocusEvent<HTMLInputElement>
|
|
1497
|
+
) => {
|
|
764
1498
|
e.currentTarget.style.borderColor =
|
|
765
1499
|
'var(--dauth-border, rgba(255, 255, 255, 0.08))';
|
|
766
1500
|
e.currentTarget.style.boxShadow = 'none';
|
|
767
1501
|
};
|
|
768
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
|
+
|
|
769
1514
|
const separator: React.CSSProperties = {
|
|
770
1515
|
height: 1,
|
|
771
|
-
backgroundColor:
|
|
1516
|
+
backgroundColor:
|
|
1517
|
+
'var(--dauth-border, rgba(255, 255, 255, 0.08))',
|
|
772
1518
|
margin: '24px 0',
|
|
773
1519
|
border: 'none',
|
|
774
1520
|
};
|
|
775
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
|
+
|
|
776
1599
|
const dangerTitle: React.CSSProperties = {
|
|
777
1600
|
fontSize: 'var(--dauth-font-size-sm, 0.875rem)',
|
|
778
1601
|
fontWeight: 600,
|
|
@@ -792,7 +1615,8 @@ const deleteBtn: React.CSSProperties = {
|
|
|
792
1615
|
fontSize: 'var(--dauth-font-size-sm, 0.875rem)',
|
|
793
1616
|
fontWeight: 500,
|
|
794
1617
|
color: 'var(--dauth-error, #ef4444)',
|
|
795
|
-
backgroundColor:
|
|
1618
|
+
backgroundColor:
|
|
1619
|
+
'var(--dauth-error-bg, rgba(239, 68, 68, 0.1))',
|
|
796
1620
|
border: '1px solid rgba(239, 68, 68, 0.2)',
|
|
797
1621
|
borderRadius: 'var(--dauth-radius-sm, 8px)',
|
|
798
1622
|
cursor: 'pointer',
|
|
@@ -805,7 +1629,8 @@ const deletePanel: React.CSSProperties = {
|
|
|
805
1629
|
padding: 16,
|
|
806
1630
|
borderRadius: 'var(--dauth-radius-sm, 8px)',
|
|
807
1631
|
border: '1px solid rgba(239, 68, 68, 0.3)',
|
|
808
|
-
backgroundColor:
|
|
1632
|
+
backgroundColor:
|
|
1633
|
+
'var(--dauth-error-bg, rgba(239, 68, 68, 0.1))',
|
|
809
1634
|
};
|
|
810
1635
|
|
|
811
1636
|
const deletePanelText: React.CSSProperties = {
|
|
@@ -816,13 +1641,13 @@ const deletePanelText: React.CSSProperties = {
|
|
|
816
1641
|
};
|
|
817
1642
|
|
|
818
1643
|
const cancelBtn: React.CSSProperties = {
|
|
819
|
-
flex: 1,
|
|
820
1644
|
padding: '8px 16px',
|
|
821
1645
|
fontSize: 'var(--dauth-font-size-sm, 0.875rem)',
|
|
822
1646
|
fontWeight: 500,
|
|
823
1647
|
color: 'var(--dauth-text-secondary, #a1a1aa)',
|
|
824
1648
|
backgroundColor: 'transparent',
|
|
825
|
-
border:
|
|
1649
|
+
border:
|
|
1650
|
+
'1px solid var(--dauth-border, rgba(255, 255, 255, 0.08))',
|
|
826
1651
|
borderRadius: 'var(--dauth-radius-sm, 8px)',
|
|
827
1652
|
cursor: 'pointer',
|
|
828
1653
|
transition: 'background-color 150ms',
|
|
@@ -847,17 +1672,41 @@ const deleteConfirmBtn: React.CSSProperties = {
|
|
|
847
1672
|
gap: 8,
|
|
848
1673
|
};
|
|
849
1674
|
|
|
850
|
-
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 => ({
|
|
851
1697
|
padding: '16px 24px',
|
|
852
|
-
borderTop:
|
|
1698
|
+
borderTop:
|
|
1699
|
+
'1px solid var(--dauth-border, rgba(255, 255, 255, 0.08))',
|
|
853
1700
|
flexShrink: 0,
|
|
854
1701
|
...(!isDesktop
|
|
855
1702
|
? {
|
|
856
1703
|
position: 'sticky' as const,
|
|
857
1704
|
bottom: 0,
|
|
858
1705
|
zIndex: 1,
|
|
859
|
-
backgroundColor:
|
|
860
|
-
|
|
1706
|
+
backgroundColor:
|
|
1707
|
+
'var(--dauth-surface, #1a1a2e)',
|
|
1708
|
+
paddingBottom:
|
|
1709
|
+
'max(16px, env(safe-area-inset-bottom))',
|
|
861
1710
|
}
|
|
862
1711
|
: {}),
|
|
863
1712
|
});
|