dauth-context-react 5.0.0 → 6.1.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.
@@ -0,0 +1,890 @@
1
+ import React, {
2
+ useCallback,
3
+ useEffect,
4
+ useMemo,
5
+ useRef,
6
+ useState,
7
+ } from 'react';
8
+ import { createPortal } from 'react-dom';
9
+ import { useDauth } from './index';
10
+ import type { DauthProfileModalProps } from './interfaces';
11
+
12
+ export { type DauthProfileModalProps };
13
+
14
+ type Phase = 'exited' | 'entering' | 'entered' | 'exiting';
15
+
16
+ const TRANSITION_MS = 200;
17
+ const MOBILE_TRANSITION_MS = 300;
18
+ const SUCCESS_TIMEOUT_MS = 4000;
19
+ const CONFIRM_WORD = 'DELETE';
20
+
21
+ // --- Inline SVG icons ---
22
+
23
+ function IconClose() {
24
+ return (
25
+ <svg
26
+ width="20"
27
+ height="20"
28
+ viewBox="0 0 24 24"
29
+ fill="none"
30
+ stroke="currentColor"
31
+ strokeWidth="2"
32
+ strokeLinecap="round"
33
+ strokeLinejoin="round"
34
+ >
35
+ <line x1="18" y1="6" x2="6" y2="18" />
36
+ <line x1="6" y1="6" x2="18" y2="18" />
37
+ </svg>
38
+ );
39
+ }
40
+
41
+ function IconBack() {
42
+ return (
43
+ <svg
44
+ width="20"
45
+ height="20"
46
+ viewBox="0 0 24 24"
47
+ fill="none"
48
+ stroke="currentColor"
49
+ strokeWidth="2"
50
+ strokeLinecap="round"
51
+ strokeLinejoin="round"
52
+ >
53
+ <line x1="19" y1="12" x2="5" y2="12" />
54
+ <polyline points="12 19 5 12 12 5" />
55
+ </svg>
56
+ );
57
+ }
58
+
59
+ function Spinner() {
60
+ return <span style={spinnerStyle} aria-hidden="true" />;
61
+ }
62
+
63
+ // --- Hooks ---
64
+
65
+ function useMediaQuery(query: string): boolean {
66
+ const [matches, setMatches] = useState(
67
+ () => typeof window !== 'undefined' && window.matchMedia(query).matches
68
+ );
69
+ useEffect(() => {
70
+ const mq = window.matchMedia(query);
71
+ const handler = (e: MediaQueryListEvent) => setMatches(e.matches);
72
+ mq.addEventListener('change', handler);
73
+ return () => mq.removeEventListener('change', handler);
74
+ }, [query]);
75
+ return matches;
76
+ }
77
+
78
+ function useModalAnimation(open: boolean): Phase {
79
+ const [phase, setPhase] = useState<Phase>('exited');
80
+
81
+ useEffect(() => {
82
+ if (open) {
83
+ setPhase('entering');
84
+ const raf = requestAnimationFrame(() => {
85
+ requestAnimationFrame(() => setPhase('entered'));
86
+ });
87
+ return () => cancelAnimationFrame(raf);
88
+ }
89
+ if (phase === 'entered' || phase === 'entering') {
90
+ setPhase('exiting');
91
+ const timer = setTimeout(() => setPhase('exited'), MOBILE_TRANSITION_MS);
92
+ return () => clearTimeout(timer);
93
+ }
94
+ return undefined;
95
+ }, [open]);
96
+
97
+ return phase;
98
+ }
99
+
100
+ function useFocusTrap(
101
+ containerRef: React.RefObject<HTMLDivElement | null>,
102
+ active: boolean,
103
+ onEscape: () => void
104
+ ) {
105
+ const previousFocus = useRef<Element | null>(null);
106
+
107
+ useEffect(() => {
108
+ if (!active) return;
109
+ previousFocus.current = document.activeElement;
110
+
111
+ const container = containerRef.current;
112
+ if (!container) return;
113
+
114
+ const focusFirst = () => {
115
+ const firstInput = container.querySelector<HTMLElement>(
116
+ 'input:not([disabled])'
117
+ );
118
+ (firstInput ?? container).focus();
119
+ };
120
+ // Small delay to let the portal render
121
+ const raf = requestAnimationFrame(focusFirst);
122
+
123
+ const handleKeyDown = (e: KeyboardEvent) => {
124
+ if (e.key === 'Escape') {
125
+ e.preventDefault();
126
+ onEscape();
127
+ return;
128
+ }
129
+ if (e.key !== 'Tab') return;
130
+ const focusable = container.querySelectorAll<HTMLElement>(
131
+ 'button:not([disabled]), input:not([disabled]), [tabindex]:not([tabindex="-1"])'
132
+ );
133
+ if (focusable.length === 0) return;
134
+ const first = focusable[0];
135
+ const last = focusable[focusable.length - 1];
136
+ if (e.shiftKey && document.activeElement === first) {
137
+ e.preventDefault();
138
+ last.focus();
139
+ } else if (!e.shiftKey && document.activeElement === last) {
140
+ e.preventDefault();
141
+ first.focus();
142
+ }
143
+ };
144
+ document.addEventListener('keydown', handleKeyDown);
145
+
146
+ return () => {
147
+ cancelAnimationFrame(raf);
148
+ document.removeEventListener('keydown', handleKeyDown);
149
+ if (previousFocus.current instanceof HTMLElement) {
150
+ previousFocus.current.focus();
151
+ }
152
+ };
153
+ }, [active, containerRef, onEscape]);
154
+ }
155
+
156
+ function useScrollLock(active: boolean) {
157
+ useEffect(() => {
158
+ if (!active) return;
159
+ const scrollbarWidth =
160
+ window.innerWidth - document.documentElement.clientWidth;
161
+ const prevOverflow = document.body.style.overflow;
162
+ const prevPaddingRight = document.body.style.paddingRight;
163
+ document.body.style.overflow = 'hidden';
164
+ if (scrollbarWidth > 0) {
165
+ document.body.style.paddingRight = `${scrollbarWidth}px`;
166
+ }
167
+ return () => {
168
+ document.body.style.overflow = prevOverflow;
169
+ document.body.style.paddingRight = prevPaddingRight;
170
+ };
171
+ }, [active]);
172
+ }
173
+
174
+ // --- Component ---
175
+
176
+ export function DauthProfileModal({ open, onClose }: DauthProfileModalProps) {
177
+ const { user, domain, updateUser, deleteAccount } = useDauth();
178
+ const isDesktop = useMediaQuery('(min-width: 641px)');
179
+ const phase = useModalAnimation(open);
180
+ const modalRef = useRef<HTMLDivElement>(null);
181
+
182
+ // Form state
183
+ const [name, setName] = useState('');
184
+ const [lastname, setLastname] = useState('');
185
+ const [nickname, setNickname] = useState('');
186
+ const [country, setCountry] = useState('');
187
+ const [populated, setPopulated] = useState(false);
188
+
189
+ // Status
190
+ const [saving, setSaving] = useState(false);
191
+ const [status, setStatus] = useState<{
192
+ type: 'success' | 'error';
193
+ message: string;
194
+ } | null>(null);
195
+
196
+ // Delete
197
+ const [showDelete, setShowDelete] = useState(false);
198
+ const [deleteText, setDeleteText] = useState('');
199
+ const [deleting, setDeleting] = useState(false);
200
+
201
+ // Populate form when modal opens
202
+ useEffect(() => {
203
+ if (open && user?._id && !populated) {
204
+ setName(user.name || '');
205
+ setLastname(user.lastname || '');
206
+ setNickname(user.nickname || '');
207
+ setCountry(user.country || '');
208
+ setPopulated(true);
209
+ }
210
+ if (!open) {
211
+ setPopulated(false);
212
+ setStatus(null);
213
+ setShowDelete(false);
214
+ setDeleteText('');
215
+ }
216
+ }, [open, user, populated]);
217
+
218
+ // Auto-clear success message
219
+ useEffect(() => {
220
+ if (status?.type !== 'success') return;
221
+ const timer = setTimeout(() => setStatus(null), SUCCESS_TIMEOUT_MS);
222
+ return () => clearTimeout(timer);
223
+ }, [status]);
224
+
225
+ useFocusTrap(modalRef, phase === 'entered', onClose);
226
+ useScrollLock(phase !== 'exited');
227
+
228
+ const hasField = useCallback(
229
+ (field: string) =>
230
+ domain.formFields?.some((f) => f.field === field) ?? false,
231
+ [domain.formFields]
232
+ );
233
+
234
+ const isRequired = useCallback(
235
+ (field: string) =>
236
+ domain.formFields?.find((f) => f.field === field)?.required ?? false,
237
+ [domain.formFields]
238
+ );
239
+
240
+ const hasChanges = useMemo(() => {
241
+ if (!user?._id) return false;
242
+ return (
243
+ name !== (user.name || '') ||
244
+ lastname !== (user.lastname || '') ||
245
+ nickname !== (user.nickname || '') ||
246
+ country !== (user.country || '')
247
+ );
248
+ }, [name, lastname, nickname, country, user]);
249
+
250
+ const canSave = name.trim().length > 0 && hasChanges && !saving;
251
+
252
+ const handleSave = useCallback(async () => {
253
+ setSaving(true);
254
+ setStatus(null);
255
+ const fields: Record<string, string> = { name };
256
+ if (hasField('lastname')) fields.lastname = lastname;
257
+ if (hasField('nickname')) fields.nickname = nickname;
258
+ if (hasField('country')) fields.country = country;
259
+ const ok = await updateUser(fields);
260
+ setSaving(false);
261
+ if (ok) {
262
+ setStatus({
263
+ type: 'success',
264
+ message: 'Profile updated successfully',
265
+ });
266
+ } else {
267
+ setStatus({
268
+ type: 'error',
269
+ message: 'Something went wrong. Please try again.',
270
+ });
271
+ }
272
+ }, [name, lastname, nickname, country, hasField, updateUser]);
273
+
274
+ const handleDelete = useCallback(async () => {
275
+ setDeleting(true);
276
+ const ok = await deleteAccount();
277
+ setDeleting(false);
278
+ if (ok) {
279
+ onClose();
280
+ } else {
281
+ setStatus({
282
+ type: 'error',
283
+ message: 'Could not delete account. Please try again.',
284
+ });
285
+ setShowDelete(false);
286
+ setDeleteText('');
287
+ }
288
+ }, [deleteAccount, onClose]);
289
+
290
+ // Build CSS custom property overrides from domain.modalTheme
291
+ const themeVars = useMemo(() => {
292
+ const t = domain.modalTheme;
293
+ if (!t) return {};
294
+ const vars: Record<string, string> = {};
295
+ if (t.accent) vars['--dauth-accent'] = t.accent;
296
+ if (t.accentHover) vars['--dauth-accent-hover'] = t.accentHover;
297
+ 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;
301
+ if (t.border) vars['--dauth-border'] = t.border;
302
+ return vars;
303
+ }, [domain.modalTheme]);
304
+
305
+ if (phase === 'exited') return null;
306
+
307
+ const dur = isDesktop ? TRANSITION_MS : MOBILE_TRANSITION_MS;
308
+ const easing = 'cubic-bezier(0.16, 1, 0.3, 1)';
309
+
310
+ const backdrop: React.CSSProperties = {
311
+ position: 'fixed',
312
+ inset: 0,
313
+ zIndex: 2147483647,
314
+ backgroundColor: 'var(--dauth-backdrop, rgba(0, 0, 0, 0.6))',
315
+ backdropFilter: 'blur(4px)',
316
+ WebkitBackdropFilter: 'blur(4px)',
317
+ display: 'flex',
318
+ alignItems: 'center',
319
+ justifyContent: 'center',
320
+ opacity: phase === 'entered' ? 1 : 0,
321
+ transition: `opacity ${dur}ms ease-out`,
322
+ };
323
+
324
+ const modalDesktop: React.CSSProperties = {
325
+ position: 'relative',
326
+ width: '100%',
327
+ maxWidth: 480,
328
+ maxHeight: '90vh',
329
+ margin: 16,
330
+ backgroundColor: 'var(--dauth-surface, #1a1a2e)',
331
+ 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))',
334
+ display: 'flex',
335
+ flexDirection: 'column',
336
+ overflow: 'hidden',
337
+ fontFamily:
338
+ 'var(--dauth-font-family, system-ui, -apple-system, sans-serif)',
339
+ color: 'var(--dauth-text-primary, #e4e4e7)',
340
+ opacity: phase === 'entered' ? 1 : 0,
341
+ transform: phase === 'entered' ? 'translateY(0)' : 'translateY(16px)',
342
+ transition: `opacity ${dur}ms ${easing}, transform ${dur}ms ${easing}`,
343
+ };
344
+
345
+ const modalMobile: React.CSSProperties = {
346
+ position: 'fixed',
347
+ inset: 0,
348
+ backgroundColor: 'var(--dauth-surface, #1a1a2e)',
349
+ display: 'flex',
350
+ flexDirection: 'column',
351
+ fontFamily:
352
+ 'var(--dauth-font-family, system-ui, -apple-system, sans-serif)',
353
+ color: 'var(--dauth-text-primary, #e4e4e7)',
354
+ transform: phase === 'entered' ? 'translateY(0)' : 'translateY(100%)',
355
+ transition: `transform ${dur}ms ${easing}`,
356
+ };
357
+
358
+ const avatarInitial = (user.name || user.email || '?')
359
+ .charAt(0)
360
+ .toUpperCase();
361
+
362
+ return createPortal(
363
+ <>
364
+ <style
365
+ dangerouslySetInnerHTML={{
366
+ __html: '@keyframes dauth-spin{to{transform:rotate(360deg)}}',
367
+ }}
368
+ />
369
+ <div
370
+ style={{ ...backdrop, ...themeVars }}
371
+ onClick={isDesktop ? onClose : undefined}
372
+ data-testid="dauth-profile-backdrop"
373
+ >
374
+ <div
375
+ ref={modalRef}
376
+ role="dialog"
377
+ aria-modal="true"
378
+ aria-labelledby="dauth-profile-title"
379
+ style={isDesktop ? modalDesktop : modalMobile}
380
+ onClick={(e) => e.stopPropagation()}
381
+ tabIndex={-1}
382
+ >
383
+ {/* Header */}
384
+ <div style={headerStyle(isDesktop)}>
385
+ <button
386
+ type="button"
387
+ onClick={onClose}
388
+ style={closeBtn}
389
+ aria-label="Close"
390
+ onMouseEnter={(e) =>
391
+ (e.currentTarget.style.backgroundColor =
392
+ 'var(--dauth-surface-hover, #232340)')
393
+ }
394
+ onMouseLeave={(e) =>
395
+ (e.currentTarget.style.backgroundColor = 'transparent')
396
+ }
397
+ >
398
+ {isDesktop ? <IconClose /> : <IconBack />}
399
+ </button>
400
+ <h2 id="dauth-profile-title" style={titleStyle}>
401
+ Your Profile
402
+ </h2>
403
+ <div style={{ width: 36 }} />
404
+ </div>
405
+
406
+ {/* Scrollable body */}
407
+ <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=""
415
+ style={{
416
+ width: '100%',
417
+ height: '100%',
418
+ objectFit: 'cover',
419
+ }}
420
+ />
421
+ ) : (
422
+ avatarInitial
423
+ )}
424
+ </div>
425
+ <div style={emailText}>{user.email}</div>
426
+ </div>
427
+
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
+ )}
438
+
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
+ />
475
+ </div>
476
+ )}
477
+
478
+ {hasField('nickname') && (
479
+ <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
+ />
495
+ </div>
496
+ )}
497
+
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
+ />
515
+ </div>
516
+ )}
517
+ </div>
518
+
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.
547
+ </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
+ />
558
+ <div
559
+ style={{
560
+ display: 'flex',
561
+ gap: 8,
562
+ marginTop: 12,
563
+ }}
564
+ >
565
+ <button
566
+ type="button"
567
+ style={cancelBtn}
568
+ onClick={() => {
569
+ setShowDelete(false);
570
+ setDeleteText('');
571
+ }}
572
+ onMouseEnter={(e) =>
573
+ (e.currentTarget.style.backgroundColor =
574
+ 'var(--dauth-surface-hover, #232340)')
575
+ }
576
+ onMouseLeave={(e) =>
577
+ (e.currentTarget.style.backgroundColor = 'transparent')
578
+ }
579
+ >
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
598
+ </button>
599
+ </div>
600
+ </div>
601
+ )}
602
+ </div>
603
+ </div>
604
+
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)
619
+ 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>
631
+ </div>
632
+ </div>
633
+ </>,
634
+ document.body
635
+ );
636
+ }
637
+
638
+ // --- Style constants ---
639
+
640
+ const headerStyle = (isDesktop: boolean): React.CSSProperties => ({
641
+ display: 'flex',
642
+ alignItems: 'center',
643
+ justifyContent: 'space-between',
644
+ padding: '16px 24px',
645
+ borderBottom: '1px solid var(--dauth-border, rgba(255, 255, 255, 0.08))',
646
+ flexShrink: 0,
647
+ ...(!isDesktop
648
+ ? {
649
+ position: 'sticky' as const,
650
+ top: 0,
651
+ zIndex: 1,
652
+ backgroundColor: 'var(--dauth-surface, #1a1a2e)',
653
+ }
654
+ : {}),
655
+ });
656
+
657
+ const titleStyle: React.CSSProperties = {
658
+ fontSize: 'var(--dauth-font-size-lg, 1.25rem)',
659
+ fontWeight: 600,
660
+ margin: 0,
661
+ lineHeight: 1.4,
662
+ textAlign: 'center',
663
+ flex: 1,
664
+ };
665
+
666
+ const closeBtn: React.CSSProperties = {
667
+ display: 'flex',
668
+ alignItems: 'center',
669
+ justifyContent: 'center',
670
+ width: 36,
671
+ height: 36,
672
+ borderRadius: 'var(--dauth-radius-sm, 8px)',
673
+ border: 'none',
674
+ backgroundColor: 'transparent',
675
+ color: 'var(--dauth-text-secondary, #a1a1aa)',
676
+ cursor: 'pointer',
677
+ transition: 'background-color 150ms, color 150ms',
678
+ padding: 0,
679
+ };
680
+
681
+ const bodyStyle: React.CSSProperties = {
682
+ flex: 1,
683
+ overflowY: 'auto',
684
+ padding: 24,
685
+ WebkitOverflowScrolling: 'touch',
686
+ };
687
+
688
+ const avatarSection: React.CSSProperties = {
689
+ display: 'flex',
690
+ flexDirection: 'column',
691
+ alignItems: 'center',
692
+ marginBottom: 24,
693
+ gap: 8,
694
+ };
695
+
696
+ const avatarCircle: React.CSSProperties = {
697
+ width: 64,
698
+ height: 64,
699
+ borderRadius: '50%',
700
+ backgroundColor: 'var(--dauth-accent, #6366f1)',
701
+ display: 'flex',
702
+ alignItems: 'center',
703
+ justifyContent: 'center',
704
+ overflow: 'hidden',
705
+ color: '#ffffff',
706
+ fontSize: '1.5rem',
707
+ fontWeight: 600,
708
+ };
709
+
710
+ const emailText: React.CSSProperties = {
711
+ fontSize: 'var(--dauth-font-size-sm, 0.875rem)',
712
+ color: 'var(--dauth-text-secondary, #a1a1aa)',
713
+ };
714
+
715
+ const statusMsg = (type: 'success' | 'error'): React.CSSProperties => ({
716
+ padding: '10px 14px',
717
+ borderRadius: 'var(--dauth-radius-sm, 8px)',
718
+ fontSize: 'var(--dauth-font-size-sm, 0.875rem)',
719
+ marginBottom: 16,
720
+ backgroundColor:
721
+ type === 'success'
722
+ ? 'var(--dauth-success-bg, rgba(34, 197, 94, 0.1))'
723
+ : 'var(--dauth-error-bg, rgba(239, 68, 68, 0.1))',
724
+ color:
725
+ type === 'success'
726
+ ? 'var(--dauth-success, #22c55e)'
727
+ : 'var(--dauth-error, #ef4444)',
728
+ textAlign: 'center',
729
+ lineHeight: 1.5,
730
+ });
731
+
732
+ const fieldGroup: React.CSSProperties = { marginBottom: 16 };
733
+
734
+ const label: React.CSSProperties = {
735
+ display: 'block',
736
+ fontSize: 'var(--dauth-font-size-sm, 0.875rem)',
737
+ fontWeight: 500,
738
+ color: 'var(--dauth-text-primary, #e4e4e7)',
739
+ marginBottom: 6,
740
+ };
741
+
742
+ const input: React.CSSProperties = {
743
+ width: '100%',
744
+ padding: '10px 14px',
745
+ fontSize: 'var(--dauth-font-size-base, 1rem)',
746
+ lineHeight: 1.5,
747
+ 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))',
750
+ borderRadius: 'var(--dauth-radius-input, 8px)',
751
+ outline: 'none',
752
+ transition: 'border-color 150ms, box-shadow 150ms',
753
+ boxSizing: 'border-box' as const,
754
+ fontFamily: 'inherit',
755
+ };
756
+
757
+ const inputFocusHandler = (e: React.FocusEvent<HTMLInputElement>) => {
758
+ e.currentTarget.style.borderColor =
759
+ '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)';
761
+ };
762
+
763
+ const inputBlurHandler = (e: React.FocusEvent<HTMLInputElement>) => {
764
+ e.currentTarget.style.borderColor =
765
+ 'var(--dauth-border, rgba(255, 255, 255, 0.08))';
766
+ e.currentTarget.style.boxShadow = 'none';
767
+ };
768
+
769
+ const separator: React.CSSProperties = {
770
+ height: 1,
771
+ backgroundColor: 'var(--dauth-border, rgba(255, 255, 255, 0.08))',
772
+ margin: '24px 0',
773
+ border: 'none',
774
+ };
775
+
776
+ const dangerTitle: React.CSSProperties = {
777
+ fontSize: 'var(--dauth-font-size-sm, 0.875rem)',
778
+ fontWeight: 600,
779
+ color: 'var(--dauth-error, #ef4444)',
780
+ marginBottom: 4,
781
+ };
782
+
783
+ const dangerDesc: React.CSSProperties = {
784
+ fontSize: 'var(--dauth-font-size-xs, 0.75rem)',
785
+ color: 'var(--dauth-text-muted, #71717a)',
786
+ marginBottom: 12,
787
+ lineHeight: 1.5,
788
+ };
789
+
790
+ const deleteBtn: React.CSSProperties = {
791
+ padding: '8px 16px',
792
+ fontSize: 'var(--dauth-font-size-sm, 0.875rem)',
793
+ fontWeight: 500,
794
+ color: 'var(--dauth-error, #ef4444)',
795
+ backgroundColor: 'var(--dauth-error-bg, rgba(239, 68, 68, 0.1))',
796
+ border: '1px solid rgba(239, 68, 68, 0.2)',
797
+ borderRadius: 'var(--dauth-radius-sm, 8px)',
798
+ cursor: 'pointer',
799
+ transition: 'background-color 150ms, border-color 150ms',
800
+ fontFamily: 'inherit',
801
+ };
802
+
803
+ const deletePanel: React.CSSProperties = {
804
+ marginTop: 12,
805
+ padding: 16,
806
+ borderRadius: 'var(--dauth-radius-sm, 8px)',
807
+ border: '1px solid rgba(239, 68, 68, 0.3)',
808
+ backgroundColor: 'var(--dauth-error-bg, rgba(239, 68, 68, 0.1))',
809
+ };
810
+
811
+ const deletePanelText: React.CSSProperties = {
812
+ fontSize: 'var(--dauth-font-size-sm, 0.875rem)',
813
+ color: 'var(--dauth-text-primary, #e4e4e7)',
814
+ marginBottom: 12,
815
+ lineHeight: 1.5,
816
+ };
817
+
818
+ const cancelBtn: React.CSSProperties = {
819
+ flex: 1,
820
+ padding: '8px 16px',
821
+ fontSize: 'var(--dauth-font-size-sm, 0.875rem)',
822
+ fontWeight: 500,
823
+ color: 'var(--dauth-text-secondary, #a1a1aa)',
824
+ backgroundColor: 'transparent',
825
+ border: '1px solid var(--dauth-border, rgba(255, 255, 255, 0.08))',
826
+ borderRadius: 'var(--dauth-radius-sm, 8px)',
827
+ cursor: 'pointer',
828
+ transition: 'background-color 150ms',
829
+ fontFamily: 'inherit',
830
+ };
831
+
832
+ const deleteConfirmBtn: React.CSSProperties = {
833
+ flex: 1,
834
+ padding: '8px 16px',
835
+ fontSize: 'var(--dauth-font-size-sm, 0.875rem)',
836
+ fontWeight: 500,
837
+ color: '#ffffff',
838
+ backgroundColor: 'var(--dauth-error, #ef4444)',
839
+ border: 'none',
840
+ borderRadius: 'var(--dauth-radius-sm, 8px)',
841
+ cursor: 'pointer',
842
+ transition: 'opacity 150ms',
843
+ fontFamily: 'inherit',
844
+ display: 'flex',
845
+ alignItems: 'center',
846
+ justifyContent: 'center',
847
+ gap: 8,
848
+ };
849
+
850
+ const footerStyle = (isDesktop: boolean): React.CSSProperties => ({
851
+ padding: '16px 24px',
852
+ borderTop: '1px solid var(--dauth-border, rgba(255, 255, 255, 0.08))',
853
+ flexShrink: 0,
854
+ ...(!isDesktop
855
+ ? {
856
+ position: 'sticky' as const,
857
+ bottom: 0,
858
+ zIndex: 1,
859
+ backgroundColor: 'var(--dauth-surface, #1a1a2e)',
860
+ paddingBottom: 'max(16px, env(safe-area-inset-bottom))',
861
+ }
862
+ : {}),
863
+ });
864
+
865
+ const saveBtn: React.CSSProperties = {
866
+ width: '100%',
867
+ padding: '12px 24px',
868
+ fontSize: 'var(--dauth-font-size-base, 1rem)',
869
+ fontWeight: 600,
870
+ color: '#ffffff',
871
+ backgroundColor: 'var(--dauth-accent, #6366f1)',
872
+ border: 'none',
873
+ borderRadius: 'var(--dauth-radius-sm, 8px)',
874
+ transition: 'opacity 150ms, background-color 150ms',
875
+ display: 'flex',
876
+ alignItems: 'center',
877
+ justifyContent: 'center',
878
+ gap: 8,
879
+ fontFamily: 'inherit',
880
+ };
881
+
882
+ const spinnerStyle: React.CSSProperties = {
883
+ display: 'inline-block',
884
+ width: 16,
885
+ height: 16,
886
+ border: '2px solid rgba(255, 255, 255, 0.3)',
887
+ borderTopColor: '#ffffff',
888
+ borderRadius: '50%',
889
+ animation: 'dauth-spin 0.6s linear infinite',
890
+ };