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.
@@ -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 { DauthProfileModalProps } from './interfaces';
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
- () => typeof window !== 'undefined' && window.matchMedia(query).matches
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) => setMatches(e.matches);
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(() => setPhase('exited'), MOBILE_TRANSITION_MS);
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 = container.querySelectorAll<HTMLElement>(
131
- 'button:not([disabled]), input:not([disabled]), [tabindex]:not([tabindex="-1"])'
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 (!e.shiftKey && document.activeElement === last) {
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 - document.documentElement.clientWidth;
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({ open, onClose }: DauthProfileModalProps) {
177
- const { user, domain, updateUser, deleteAccount } = useDauth();
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
- // Form state
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(() => setStatus(null), SUCCESS_TIMEOUT_MS);
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) ?? false,
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)?.required ?? false,
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 = name.trim().length > 0 && hasChanges && !saving;
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: 'Could not delete account. Please try again.',
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: 'var(--dauth-backdrop, rgba(0, 0, 0, 0.6))',
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: 'var(--dauth-shadow, 0 25px 50px -12px rgba(0, 0, 0, 0.5))',
318
- border: '1px solid var(--dauth-border, rgba(255, 255, 255, 0.08))',
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: phase === 'entered' ? 'translateY(0)' : 'translateY(16px)',
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: phase === 'entered' ? 'translateY(0)' : 'translateY(100%)',
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: '@keyframes dauth-spin{to{transform:rotate(360deg)}}',
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 = 'transparent')
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
- {/* Avatar */}
394
- <div style={avatarSection}>
395
- <div style={avatarCircle}>
396
- {user.avatar?.url ? (
397
- <img
398
- src={user.avatar.url}
399
- alt=""
708
+ {/* ========== PROFILE TAB ========== */}
709
+ {activeTab === 'profile' && (
710
+ <>
711
+ {/* Avatar */}
712
+ <div style={avatarSection}>
713
+ <div
400
714
  style={{
401
- width: '100%',
402
- height: '100%',
403
- objectFit: 'cover',
715
+ ...avatarCircle,
716
+ cursor: onAvatarUpload
717
+ ? 'pointer'
718
+ : 'default',
719
+ position: 'relative' as const,
404
720
  }}
405
- />
406
- ) : (
407
- avatarInitial
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
- {/* Status */}
414
- {status && (
415
- <div
416
- role="status"
417
- aria-live="polite"
418
- style={statusMsg(status.type)}
419
- >
420
- {status.message}
421
- </div>
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
- {/* Form */}
425
- <div>
426
- <div style={fieldGroup}>
427
- <label htmlFor="dauth-name" style={label}>
428
- Name *
429
- </label>
430
- <input
431
- id="dauth-name"
432
- type="text"
433
- value={name}
434
- onChange={(e) => setName(e.target.value)}
435
- placeholder="Your name"
436
- disabled={saving}
437
- style={input}
438
- onFocus={inputFocusHandler}
439
- onBlur={inputBlurHandler}
440
- />
441
- </div>
442
-
443
- {hasField('lastname') && (
444
- <div style={fieldGroup}>
445
- <label htmlFor="dauth-lastname" style={label}>
446
- Last name
447
- {isRequired('lastname') ? ' *' : ''}
448
- </label>
449
- <input
450
- id="dauth-lastname"
451
- type="text"
452
- value={lastname}
453
- onChange={(e) => setLastname(e.target.value)}
454
- placeholder="Your last name"
455
- disabled={saving}
456
- style={input}
457
- onFocus={inputFocusHandler}
458
- onBlur={inputBlurHandler}
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
- {hasField('nickname') && (
867
+ {/* Language selector */}
868
+ <hr style={separator} />
464
869
  <div style={fieldGroup}>
465
- <label htmlFor="dauth-nickname" style={label}>
466
- Nickname
467
- {isRequired('nickname') ? ' *' : ''}
468
- </label>
469
- <input
470
- id="dauth-nickname"
471
- type="text"
472
- value={nickname}
473
- onChange={(e) => setNickname(e.target.value)}
474
- placeholder="Choose a nickname"
475
- disabled={saving}
476
- style={input}
477
- onFocus={inputFocusHandler}
478
- onBlur={inputBlurHandler}
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
- {hasField('country') && (
484
- <div style={fieldGroup}>
485
- <label htmlFor="dauth-country" style={label}>
486
- Country
487
- {isRequired('country') ? ' *' : ''}
488
- </label>
489
- <input
490
- id="dauth-country"
491
- type="text"
492
- value={country}
493
- onChange={(e) => setCountry(e.target.value)}
494
- placeholder="Your country"
495
- disabled={saving}
496
- style={input}
497
- onFocus={inputFocusHandler}
498
- onBlur={inputBlurHandler}
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
- {/* Danger zone */}
505
- <hr style={separator} />
506
- <div>
507
- <div style={dangerTitle}>Delete account</div>
508
- <div style={dangerDesc}>
509
- Permanently delete your account and all associated data.
510
- </div>
511
- {!showDelete ? (
512
- <button
513
- type="button"
514
- style={deleteBtn}
515
- onClick={() => setShowDelete(true)}
516
- onMouseEnter={(e) =>
517
- (e.currentTarget.style.backgroundColor =
518
- 'rgba(239, 68, 68, 0.2)')
519
- }
520
- onMouseLeave={(e) =>
521
- (e.currentTarget.style.backgroundColor =
522
- 'var(--dauth-error-bg, rgba(239, 68, 68, 0.1))')
523
- }
524
- >
525
- Delete account
526
- </button>
527
- ) : (
528
- <div style={deletePanel}>
529
- <div style={deletePanelText}>
530
- This action is permanent and cannot be undone. Type{' '}
531
- <strong>{CONFIRM_WORD}</strong> to confirm.
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
- <input
534
- type="text"
535
- value={deleteText}
536
- onChange={(e) => setDeleteText(e.target.value)}
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
- display: 'flex',
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={cancelBtn}
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
- 'var(--dauth-surface-hover, #232340)')
1184
+ 'rgba(239, 68, 68, 0.2)')
560
1185
  }
561
1186
  onMouseLeave={(e) =>
562
- (e.currentTarget.style.backgroundColor = 'transparent')
1187
+ (e.currentTarget.style.backgroundColor =
1188
+ 'var(--dauth-error-bg, rgba(239, 68, 68, 0.1))')
563
1189
  }
564
1190
  >
565
- Cancel
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
- </div>
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
- </div>
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
- <div style={footerStyle(isDesktop)}>
592
- <button
593
- type="button"
594
- style={{
595
- ...saveBtn,
596
- opacity: canSave ? 1 : 0.5,
597
- cursor: canSave ? 'pointer' : 'not-allowed',
598
- }}
599
- disabled={!canSave}
600
- onClick={handleSave}
601
- aria-busy={saving}
602
- onMouseEnter={(e) => {
603
- if (canSave)
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-hover, #818cf8)';
606
- }}
607
- onMouseLeave={(e) => {
608
- e.currentTarget.style.backgroundColor =
609
- 'var(--dauth-accent, #6366f1)';
610
- }}
611
- >
612
- {saving && <Spinner />}
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 = (isDesktop: boolean): React.CSSProperties => ({
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: 'var(--dauth-surface, #1a1a2e)',
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 = (type: 'success' | 'error'): React.CSSProperties => ({
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 = { marginBottom: 16 };
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: 'var(--dauth-surface-secondary, rgba(255, 255, 255, 0.04))',
734
- border: '1px solid var(--dauth-border, rgba(255, 255, 255, 0.08))',
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 = (e: React.FocusEvent<HTMLInputElement>) => {
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 = '0 0 0 3px rgba(99, 102, 241, 0.15)';
1491
+ e.currentTarget.style.boxShadow =
1492
+ '0 0 0 3px rgba(99, 102, 241, 0.15)';
746
1493
  };
747
1494
 
748
- const inputBlurHandler = (e: React.FocusEvent<HTMLInputElement>) => {
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: 'var(--dauth-border, rgba(255, 255, 255, 0.08))',
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: 'var(--dauth-error-bg, rgba(239, 68, 68, 0.1))',
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: 'var(--dauth-error-bg, rgba(239, 68, 68, 0.1))',
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: '1px solid var(--dauth-border, rgba(255, 255, 255, 0.08))',
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 footerStyle = (isDesktop: boolean): React.CSSProperties => ({
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: '1px solid var(--dauth-border, rgba(255, 255, 255, 0.08))',
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: 'var(--dauth-surface, #1a1a2e)',
845
- paddingBottom: 'max(16px, env(safe-area-inset-bottom))',
1706
+ backgroundColor:
1707
+ 'var(--dauth-surface, #1a1a2e)',
1708
+ paddingBottom:
1709
+ 'max(16px, env(safe-area-inset-bottom))',
846
1710
  }
847
1711
  : {}),
848
1712
  });