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.
@@ -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,24 +456,103 @@ 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
+
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) vars['--dauth-accent-hover'] = 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) vars['--dauth-surface-hover'] = t.surfaceHover;
299
- if (t.textPrimary) vars['--dauth-text-primary'] = t.textPrimary;
300
- if (t.textSecondary) vars['--dauth-text-secondary'] = t.textSecondary;
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: 'var(--dauth-backdrop, rgba(0, 0, 0, 0.6))',
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: 'var(--dauth-shadow, 0 25px 50px -12px rgba(0, 0, 0, 0.5))',
333
- 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))',
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: phase === 'entered' ? 'translateY(0)' : 'translateY(16px)',
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: phase === 'entered' ? 'translateY(0)' : 'translateY(100%)',
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: '@keyframes dauth-spin{to{transform:rotate(360deg)}}',
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 = 'transparent')
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
- {/* Avatar */}
409
- <div style={avatarSection}>
410
- <div style={avatarCircle}>
411
- {user.avatar?.url ? (
412
- <img
413
- src={user.avatar.url}
414
- alt=""
708
+ {/* ========== PROFILE TAB ========== */}
709
+ {activeTab === 'profile' && (
710
+ <>
711
+ {/* Avatar */}
712
+ <div style={avatarSection}>
713
+ <div
415
714
  style={{
416
- width: '100%',
417
- height: '100%',
418
- objectFit: 'cover',
715
+ ...avatarCircle,
716
+ cursor: onAvatarUpload
717
+ ? 'pointer'
718
+ : 'default',
719
+ position: 'relative' as const,
419
720
  }}
420
- />
421
- ) : (
422
- 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>
423
765
  )}
424
- </div>
425
- <div style={emailText}>{user.email}</div>
426
- </div>
427
766
 
428
- {/* Status */}
429
- {status && (
430
- <div
431
- role="status"
432
- aria-live="polite"
433
- style={statusMsg(status.type)}
434
- >
435
- {status.message}
436
- </div>
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
- {/* Form */}
440
- <div>
441
- <div style={fieldGroup}>
442
- <label htmlFor="dauth-name" style={label}>
443
- Name *
444
- </label>
445
- <input
446
- id="dauth-name"
447
- type="text"
448
- value={name}
449
- onChange={(e) => setName(e.target.value)}
450
- placeholder="Your name"
451
- disabled={saving}
452
- style={input}
453
- onFocus={inputFocusHandler}
454
- onBlur={inputBlurHandler}
455
- />
456
- </div>
457
-
458
- {hasField('lastname') && (
459
- <div style={fieldGroup}>
460
- <label htmlFor="dauth-lastname" style={label}>
461
- Last name
462
- {isRequired('lastname') ? ' *' : ''}
463
- </label>
464
- <input
465
- id="dauth-lastname"
466
- type="text"
467
- value={lastname}
468
- onChange={(e) => setLastname(e.target.value)}
469
- placeholder="Your last name"
470
- disabled={saving}
471
- style={input}
472
- onFocus={inputFocusHandler}
473
- onBlur={inputBlurHandler}
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
- {hasField('nickname') && (
867
+ {/* Language selector */}
868
+ <hr style={separator} />
479
869
  <div style={fieldGroup}>
480
- <label htmlFor="dauth-nickname" style={label}>
481
- Nickname
482
- {isRequired('nickname') ? ' *' : ''}
483
- </label>
484
- <input
485
- id="dauth-nickname"
486
- type="text"
487
- value={nickname}
488
- onChange={(e) => setNickname(e.target.value)}
489
- placeholder="Choose a nickname"
490
- disabled={saving}
491
- style={input}
492
- onFocus={inputFocusHandler}
493
- onBlur={inputBlurHandler}
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
- {hasField('country') && (
499
- <div style={fieldGroup}>
500
- <label htmlFor="dauth-country" style={label}>
501
- Country
502
- {isRequired('country') ? ' *' : ''}
503
- </label>
504
- <input
505
- id="dauth-country"
506
- type="text"
507
- value={country}
508
- onChange={(e) => setCountry(e.target.value)}
509
- placeholder="Your country"
510
- disabled={saving}
511
- style={input}
512
- onFocus={inputFocusHandler}
513
- onBlur={inputBlurHandler}
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
- {/* Danger zone */}
520
- <hr style={separator} />
521
- <div>
522
- <div style={dangerTitle}>Delete account</div>
523
- <div style={dangerDesc}>
524
- Permanently delete your account and all associated data.
525
- </div>
526
- {!showDelete ? (
527
- <button
528
- type="button"
529
- style={deleteBtn}
530
- onClick={() => setShowDelete(true)}
531
- onMouseEnter={(e) =>
532
- (e.currentTarget.style.backgroundColor =
533
- 'rgba(239, 68, 68, 0.2)')
534
- }
535
- onMouseLeave={(e) =>
536
- (e.currentTarget.style.backgroundColor =
537
- 'var(--dauth-error-bg, rgba(239, 68, 68, 0.1))')
538
- }
539
- >
540
- Delete account
541
- </button>
542
- ) : (
543
- <div style={deletePanel}>
544
- <div style={deletePanelText}>
545
- This action is permanent and cannot be undone. Type{' '}
546
- <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>
547
1004
  </div>
548
- <input
549
- type="text"
550
- value={deleteText}
551
- onChange={(e) => setDeleteText(e.target.value)}
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
- display: 'flex',
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={cancelBtn}
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
- 'var(--dauth-surface-hover, #232340)')
1184
+ 'rgba(239, 68, 68, 0.2)')
575
1185
  }
576
1186
  onMouseLeave={(e) =>
577
- (e.currentTarget.style.backgroundColor = 'transparent')
1187
+ (e.currentTarget.style.backgroundColor =
1188
+ 'var(--dauth-error-bg, rgba(239, 68, 68, 0.1))')
578
1189
  }
579
1190
  >
580
- Cancel
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
- </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
+ )}
600
1265
  </div>
601
- )}
602
- </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
+ )}
603
1287
  </div>
604
1288
 
605
- {/* Footer */}
606
- <div style={footerStyle(isDesktop)}>
607
- <button
608
- type="button"
609
- style={{
610
- ...saveBtn,
611
- opacity: canSave ? 1 : 0.5,
612
- cursor: canSave ? 'pointer' : 'not-allowed',
613
- }}
614
- disabled={!canSave}
615
- onClick={handleSave}
616
- aria-busy={saving}
617
- onMouseEnter={(e) => {
618
- 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) => {
619
1308
  e.currentTarget.style.backgroundColor =
620
- 'var(--dauth-accent-hover, #818cf8)';
621
- }}
622
- onMouseLeave={(e) => {
623
- e.currentTarget.style.backgroundColor =
624
- 'var(--dauth-accent, #6366f1)';
625
- }}
626
- >
627
- {saving && <Spinner />}
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 = (isDesktop: boolean): React.CSSProperties => ({
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: 'var(--dauth-surface, #1a1a2e)',
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 = (type: 'success' | 'error'): React.CSSProperties => ({
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 = { marginBottom: 16 };
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: 'var(--dauth-surface-secondary, rgba(255, 255, 255, 0.04))',
749
- 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))',
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 = (e: React.FocusEvent<HTMLInputElement>) => {
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 = '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)';
761
1493
  };
762
1494
 
763
- const inputBlurHandler = (e: React.FocusEvent<HTMLInputElement>) => {
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: 'var(--dauth-border, rgba(255, 255, 255, 0.08))',
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: 'var(--dauth-error-bg, rgba(239, 68, 68, 0.1))',
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: 'var(--dauth-error-bg, rgba(239, 68, 68, 0.1))',
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: '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))',
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 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 => ({
851
1697
  padding: '16px 24px',
852
- 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))',
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: 'var(--dauth-surface, #1a1a2e)',
860
- 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))',
861
1710
  }
862
1711
  : {}),
863
1712
  });