dauth-context-react 4.0.4 → 6.0.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,875 @@
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
+ if (phase === 'exited') return null;
291
+
292
+ const dur = isDesktop ? TRANSITION_MS : MOBILE_TRANSITION_MS;
293
+ const easing = 'cubic-bezier(0.16, 1, 0.3, 1)';
294
+
295
+ const backdrop: React.CSSProperties = {
296
+ position: 'fixed',
297
+ inset: 0,
298
+ zIndex: 2147483647,
299
+ backgroundColor: 'var(--dauth-backdrop, rgba(0, 0, 0, 0.6))',
300
+ backdropFilter: 'blur(4px)',
301
+ WebkitBackdropFilter: 'blur(4px)',
302
+ display: 'flex',
303
+ alignItems: 'center',
304
+ justifyContent: 'center',
305
+ opacity: phase === 'entered' ? 1 : 0,
306
+ transition: `opacity ${dur}ms ease-out`,
307
+ };
308
+
309
+ const modalDesktop: React.CSSProperties = {
310
+ position: 'relative',
311
+ width: '100%',
312
+ maxWidth: 480,
313
+ maxHeight: '90vh',
314
+ margin: 16,
315
+ backgroundColor: 'var(--dauth-surface, #1a1a2e)',
316
+ 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))',
319
+ display: 'flex',
320
+ flexDirection: 'column',
321
+ overflow: 'hidden',
322
+ fontFamily:
323
+ 'var(--dauth-font-family, system-ui, -apple-system, sans-serif)',
324
+ color: 'var(--dauth-text-primary, #e4e4e7)',
325
+ opacity: phase === 'entered' ? 1 : 0,
326
+ transform: phase === 'entered' ? 'translateY(0)' : 'translateY(16px)',
327
+ transition: `opacity ${dur}ms ${easing}, transform ${dur}ms ${easing}`,
328
+ };
329
+
330
+ const modalMobile: React.CSSProperties = {
331
+ position: 'fixed',
332
+ inset: 0,
333
+ backgroundColor: 'var(--dauth-surface, #1a1a2e)',
334
+ display: 'flex',
335
+ flexDirection: 'column',
336
+ fontFamily:
337
+ 'var(--dauth-font-family, system-ui, -apple-system, sans-serif)',
338
+ color: 'var(--dauth-text-primary, #e4e4e7)',
339
+ transform: phase === 'entered' ? 'translateY(0)' : 'translateY(100%)',
340
+ transition: `transform ${dur}ms ${easing}`,
341
+ };
342
+
343
+ const avatarInitial = (user.name || user.email || '?')
344
+ .charAt(0)
345
+ .toUpperCase();
346
+
347
+ return createPortal(
348
+ <>
349
+ <style
350
+ dangerouslySetInnerHTML={{
351
+ __html: '@keyframes dauth-spin{to{transform:rotate(360deg)}}',
352
+ }}
353
+ />
354
+ <div
355
+ style={backdrop}
356
+ onClick={isDesktop ? onClose : undefined}
357
+ data-testid="dauth-profile-backdrop"
358
+ >
359
+ <div
360
+ ref={modalRef}
361
+ role="dialog"
362
+ aria-modal="true"
363
+ aria-labelledby="dauth-profile-title"
364
+ style={isDesktop ? modalDesktop : modalMobile}
365
+ onClick={(e) => e.stopPropagation()}
366
+ tabIndex={-1}
367
+ >
368
+ {/* Header */}
369
+ <div style={headerStyle(isDesktop)}>
370
+ <button
371
+ type="button"
372
+ onClick={onClose}
373
+ style={closeBtn}
374
+ aria-label="Close"
375
+ onMouseEnter={(e) =>
376
+ (e.currentTarget.style.backgroundColor =
377
+ 'var(--dauth-surface-hover, #232340)')
378
+ }
379
+ onMouseLeave={(e) =>
380
+ (e.currentTarget.style.backgroundColor = 'transparent')
381
+ }
382
+ >
383
+ {isDesktop ? <IconClose /> : <IconBack />}
384
+ </button>
385
+ <h2 id="dauth-profile-title" style={titleStyle}>
386
+ Your Profile
387
+ </h2>
388
+ <div style={{ width: 36 }} />
389
+ </div>
390
+
391
+ {/* Scrollable body */}
392
+ <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=""
400
+ style={{
401
+ width: '100%',
402
+ height: '100%',
403
+ objectFit: 'cover',
404
+ }}
405
+ />
406
+ ) : (
407
+ avatarInitial
408
+ )}
409
+ </div>
410
+ <div style={emailText}>{user.email}</div>
411
+ </div>
412
+
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
+ )}
423
+
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
+ />
460
+ </div>
461
+ )}
462
+
463
+ {hasField('nickname') && (
464
+ <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
+ />
480
+ </div>
481
+ )}
482
+
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
+ />
500
+ </div>
501
+ )}
502
+ </div>
503
+
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.
532
+ </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
+ />
543
+ <div
544
+ style={{
545
+ display: 'flex',
546
+ gap: 8,
547
+ marginTop: 12,
548
+ }}
549
+ >
550
+ <button
551
+ type="button"
552
+ style={cancelBtn}
553
+ onClick={() => {
554
+ setShowDelete(false);
555
+ setDeleteText('');
556
+ }}
557
+ onMouseEnter={(e) =>
558
+ (e.currentTarget.style.backgroundColor =
559
+ 'var(--dauth-surface-hover, #232340)')
560
+ }
561
+ onMouseLeave={(e) =>
562
+ (e.currentTarget.style.backgroundColor = 'transparent')
563
+ }
564
+ >
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
583
+ </button>
584
+ </div>
585
+ </div>
586
+ )}
587
+ </div>
588
+ </div>
589
+
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)
604
+ 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>
616
+ </div>
617
+ </div>
618
+ </>,
619
+ document.body
620
+ );
621
+ }
622
+
623
+ // --- Style constants ---
624
+
625
+ const headerStyle = (isDesktop: boolean): React.CSSProperties => ({
626
+ display: 'flex',
627
+ alignItems: 'center',
628
+ justifyContent: 'space-between',
629
+ padding: '16px 24px',
630
+ borderBottom: '1px solid var(--dauth-border, rgba(255, 255, 255, 0.08))',
631
+ flexShrink: 0,
632
+ ...(!isDesktop
633
+ ? {
634
+ position: 'sticky' as const,
635
+ top: 0,
636
+ zIndex: 1,
637
+ backgroundColor: 'var(--dauth-surface, #1a1a2e)',
638
+ }
639
+ : {}),
640
+ });
641
+
642
+ const titleStyle: React.CSSProperties = {
643
+ fontSize: 'var(--dauth-font-size-lg, 1.25rem)',
644
+ fontWeight: 600,
645
+ margin: 0,
646
+ lineHeight: 1.4,
647
+ textAlign: 'center',
648
+ flex: 1,
649
+ };
650
+
651
+ const closeBtn: React.CSSProperties = {
652
+ display: 'flex',
653
+ alignItems: 'center',
654
+ justifyContent: 'center',
655
+ width: 36,
656
+ height: 36,
657
+ borderRadius: 'var(--dauth-radius-sm, 8px)',
658
+ border: 'none',
659
+ backgroundColor: 'transparent',
660
+ color: 'var(--dauth-text-secondary, #a1a1aa)',
661
+ cursor: 'pointer',
662
+ transition: 'background-color 150ms, color 150ms',
663
+ padding: 0,
664
+ };
665
+
666
+ const bodyStyle: React.CSSProperties = {
667
+ flex: 1,
668
+ overflowY: 'auto',
669
+ padding: 24,
670
+ WebkitOverflowScrolling: 'touch',
671
+ };
672
+
673
+ const avatarSection: React.CSSProperties = {
674
+ display: 'flex',
675
+ flexDirection: 'column',
676
+ alignItems: 'center',
677
+ marginBottom: 24,
678
+ gap: 8,
679
+ };
680
+
681
+ const avatarCircle: React.CSSProperties = {
682
+ width: 64,
683
+ height: 64,
684
+ borderRadius: '50%',
685
+ backgroundColor: 'var(--dauth-accent, #6366f1)',
686
+ display: 'flex',
687
+ alignItems: 'center',
688
+ justifyContent: 'center',
689
+ overflow: 'hidden',
690
+ color: '#ffffff',
691
+ fontSize: '1.5rem',
692
+ fontWeight: 600,
693
+ };
694
+
695
+ const emailText: React.CSSProperties = {
696
+ fontSize: 'var(--dauth-font-size-sm, 0.875rem)',
697
+ color: 'var(--dauth-text-secondary, #a1a1aa)',
698
+ };
699
+
700
+ const statusMsg = (type: 'success' | 'error'): React.CSSProperties => ({
701
+ padding: '10px 14px',
702
+ borderRadius: 'var(--dauth-radius-sm, 8px)',
703
+ fontSize: 'var(--dauth-font-size-sm, 0.875rem)',
704
+ marginBottom: 16,
705
+ backgroundColor:
706
+ type === 'success'
707
+ ? 'var(--dauth-success-bg, rgba(34, 197, 94, 0.1))'
708
+ : 'var(--dauth-error-bg, rgba(239, 68, 68, 0.1))',
709
+ color:
710
+ type === 'success'
711
+ ? 'var(--dauth-success, #22c55e)'
712
+ : 'var(--dauth-error, #ef4444)',
713
+ textAlign: 'center',
714
+ lineHeight: 1.5,
715
+ });
716
+
717
+ const fieldGroup: React.CSSProperties = { marginBottom: 16 };
718
+
719
+ const label: React.CSSProperties = {
720
+ display: 'block',
721
+ fontSize: 'var(--dauth-font-size-sm, 0.875rem)',
722
+ fontWeight: 500,
723
+ color: 'var(--dauth-text-primary, #e4e4e7)',
724
+ marginBottom: 6,
725
+ };
726
+
727
+ const input: React.CSSProperties = {
728
+ width: '100%',
729
+ padding: '10px 14px',
730
+ fontSize: 'var(--dauth-font-size-base, 1rem)',
731
+ lineHeight: 1.5,
732
+ 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))',
735
+ borderRadius: 'var(--dauth-radius-input, 8px)',
736
+ outline: 'none',
737
+ transition: 'border-color 150ms, box-shadow 150ms',
738
+ boxSizing: 'border-box' as const,
739
+ fontFamily: 'inherit',
740
+ };
741
+
742
+ const inputFocusHandler = (e: React.FocusEvent<HTMLInputElement>) => {
743
+ e.currentTarget.style.borderColor =
744
+ '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)';
746
+ };
747
+
748
+ const inputBlurHandler = (e: React.FocusEvent<HTMLInputElement>) => {
749
+ e.currentTarget.style.borderColor =
750
+ 'var(--dauth-border, rgba(255, 255, 255, 0.08))';
751
+ e.currentTarget.style.boxShadow = 'none';
752
+ };
753
+
754
+ const separator: React.CSSProperties = {
755
+ height: 1,
756
+ backgroundColor: 'var(--dauth-border, rgba(255, 255, 255, 0.08))',
757
+ margin: '24px 0',
758
+ border: 'none',
759
+ };
760
+
761
+ const dangerTitle: React.CSSProperties = {
762
+ fontSize: 'var(--dauth-font-size-sm, 0.875rem)',
763
+ fontWeight: 600,
764
+ color: 'var(--dauth-error, #ef4444)',
765
+ marginBottom: 4,
766
+ };
767
+
768
+ const dangerDesc: React.CSSProperties = {
769
+ fontSize: 'var(--dauth-font-size-xs, 0.75rem)',
770
+ color: 'var(--dauth-text-muted, #71717a)',
771
+ marginBottom: 12,
772
+ lineHeight: 1.5,
773
+ };
774
+
775
+ const deleteBtn: React.CSSProperties = {
776
+ padding: '8px 16px',
777
+ fontSize: 'var(--dauth-font-size-sm, 0.875rem)',
778
+ fontWeight: 500,
779
+ color: 'var(--dauth-error, #ef4444)',
780
+ backgroundColor: 'var(--dauth-error-bg, rgba(239, 68, 68, 0.1))',
781
+ border: '1px solid rgba(239, 68, 68, 0.2)',
782
+ borderRadius: 'var(--dauth-radius-sm, 8px)',
783
+ cursor: 'pointer',
784
+ transition: 'background-color 150ms, border-color 150ms',
785
+ fontFamily: 'inherit',
786
+ };
787
+
788
+ const deletePanel: React.CSSProperties = {
789
+ marginTop: 12,
790
+ padding: 16,
791
+ borderRadius: 'var(--dauth-radius-sm, 8px)',
792
+ border: '1px solid rgba(239, 68, 68, 0.3)',
793
+ backgroundColor: 'var(--dauth-error-bg, rgba(239, 68, 68, 0.1))',
794
+ };
795
+
796
+ const deletePanelText: React.CSSProperties = {
797
+ fontSize: 'var(--dauth-font-size-sm, 0.875rem)',
798
+ color: 'var(--dauth-text-primary, #e4e4e7)',
799
+ marginBottom: 12,
800
+ lineHeight: 1.5,
801
+ };
802
+
803
+ const cancelBtn: React.CSSProperties = {
804
+ flex: 1,
805
+ padding: '8px 16px',
806
+ fontSize: 'var(--dauth-font-size-sm, 0.875rem)',
807
+ fontWeight: 500,
808
+ color: 'var(--dauth-text-secondary, #a1a1aa)',
809
+ backgroundColor: 'transparent',
810
+ border: '1px solid var(--dauth-border, rgba(255, 255, 255, 0.08))',
811
+ borderRadius: 'var(--dauth-radius-sm, 8px)',
812
+ cursor: 'pointer',
813
+ transition: 'background-color 150ms',
814
+ fontFamily: 'inherit',
815
+ };
816
+
817
+ const deleteConfirmBtn: React.CSSProperties = {
818
+ flex: 1,
819
+ padding: '8px 16px',
820
+ fontSize: 'var(--dauth-font-size-sm, 0.875rem)',
821
+ fontWeight: 500,
822
+ color: '#ffffff',
823
+ backgroundColor: 'var(--dauth-error, #ef4444)',
824
+ border: 'none',
825
+ borderRadius: 'var(--dauth-radius-sm, 8px)',
826
+ cursor: 'pointer',
827
+ transition: 'opacity 150ms',
828
+ fontFamily: 'inherit',
829
+ display: 'flex',
830
+ alignItems: 'center',
831
+ justifyContent: 'center',
832
+ gap: 8,
833
+ };
834
+
835
+ const footerStyle = (isDesktop: boolean): React.CSSProperties => ({
836
+ padding: '16px 24px',
837
+ borderTop: '1px solid var(--dauth-border, rgba(255, 255, 255, 0.08))',
838
+ flexShrink: 0,
839
+ ...(!isDesktop
840
+ ? {
841
+ position: 'sticky' as const,
842
+ bottom: 0,
843
+ zIndex: 1,
844
+ backgroundColor: 'var(--dauth-surface, #1a1a2e)',
845
+ paddingBottom: 'max(16px, env(safe-area-inset-bottom))',
846
+ }
847
+ : {}),
848
+ });
849
+
850
+ const saveBtn: React.CSSProperties = {
851
+ width: '100%',
852
+ padding: '12px 24px',
853
+ fontSize: 'var(--dauth-font-size-base, 1rem)',
854
+ fontWeight: 600,
855
+ color: '#ffffff',
856
+ backgroundColor: 'var(--dauth-accent, #6366f1)',
857
+ border: 'none',
858
+ borderRadius: 'var(--dauth-radius-sm, 8px)',
859
+ transition: 'opacity 150ms, background-color 150ms',
860
+ display: 'flex',
861
+ alignItems: 'center',
862
+ justifyContent: 'center',
863
+ gap: 8,
864
+ fontFamily: 'inherit',
865
+ };
866
+
867
+ const spinnerStyle: React.CSSProperties = {
868
+ display: 'inline-block',
869
+ width: 16,
870
+ height: 16,
871
+ border: '2px solid rgba(255, 255, 255, 0.3)',
872
+ borderTopColor: '#ffffff',
873
+ borderRadius: '50%',
874
+ animation: 'dauth-spin 0.6s linear infinite',
875
+ };