@djangocfg/layouts 2.1.126 → 2.1.129

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@djangocfg/layouts",
3
- "version": "2.1.126",
3
+ "version": "2.1.129",
4
4
  "description": "Simple, straightforward layout components for Next.js - import and use with props",
5
5
  "keywords": [
6
6
  "layouts",
@@ -74,12 +74,12 @@
74
74
  "check": "tsc --noEmit"
75
75
  },
76
76
  "peerDependencies": {
77
- "@djangocfg/api": "^2.1.126",
78
- "@djangocfg/centrifugo": "^2.1.126",
79
- "@djangocfg/i18n": "^2.1.126",
80
- "@djangocfg/ui-core": "^2.1.126",
81
- "@djangocfg/ui-nextjs": "^2.1.126",
82
- "@djangocfg/ui-tools": "^2.1.126",
77
+ "@djangocfg/api": "^2.1.129",
78
+ "@djangocfg/centrifugo": "^2.1.129",
79
+ "@djangocfg/i18n": "^2.1.129",
80
+ "@djangocfg/ui-core": "^2.1.129",
81
+ "@djangocfg/ui-nextjs": "^2.1.129",
82
+ "@djangocfg/ui-tools": "^2.1.129",
83
83
  "@hookform/resolvers": "^5.2.2",
84
84
  "consola": "^3.4.2",
85
85
  "lucide-react": "^0.545.0",
@@ -102,13 +102,13 @@
102
102
  "uuid": "^11.1.0"
103
103
  },
104
104
  "devDependencies": {
105
- "@djangocfg/api": "^2.1.126",
106
- "@djangocfg/i18n": "^2.1.126",
107
- "@djangocfg/centrifugo": "^2.1.126",
108
- "@djangocfg/typescript-config": "^2.1.126",
109
- "@djangocfg/ui-core": "^2.1.126",
110
- "@djangocfg/ui-nextjs": "^2.1.126",
111
- "@djangocfg/ui-tools": "^2.1.126",
105
+ "@djangocfg/api": "^2.1.129",
106
+ "@djangocfg/i18n": "^2.1.129",
107
+ "@djangocfg/centrifugo": "^2.1.129",
108
+ "@djangocfg/typescript-config": "^2.1.129",
109
+ "@djangocfg/ui-core": "^2.1.129",
110
+ "@djangocfg/ui-nextjs": "^2.1.129",
111
+ "@djangocfg/ui-tools": "^2.1.129",
112
112
  "@types/node": "^24.7.2",
113
113
  "@types/react": "^19.1.0",
114
114
  "@types/react-dom": "^19.1.0",
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
 
3
- import { Camera, ChevronRight, LogOut, Mail, Phone, Shield, ShieldCheck, Trash2, User, Building2, Briefcase } from 'lucide-react';
3
+ import { Camera, LogOut, Trash2 } from 'lucide-react';
4
4
  import moment from 'moment';
5
5
  import React, { useEffect, useMemo, useState } from 'react';
6
6
  import { useForm } from 'react-hook-form';
@@ -47,149 +47,148 @@ interface ProfileLayoutProps {
47
47
  type EditingField = 'first_name' | 'last_name' | 'company' | 'position' | 'phone' | null;
48
48
 
49
49
  // ─────────────────────────────────────────────────────────────────────────────
50
- // Reusable Components
50
+ // Editable Field Component
51
51
  // ─────────────────────────────────────────────────────────────────────────────
52
52
 
53
- /** Apple-style settings group container */
54
- const SettingsGroup = ({ children, className }: { children: React.ReactNode; className?: string }) => (
55
- <div className={cn('bg-card rounded-xl overflow-hidden divide-y divide-border', className)}>
56
- {children}
57
- </div>
58
- );
59
-
60
- /** Apple-style settings group header */
61
- const SettingsGroupHeader = ({ children }: { children: React.ReactNode }) => (
62
- <div className="px-4 py-2 text-xs font-medium text-muted-foreground uppercase tracking-wider">
63
- {children}
64
- </div>
65
- );
66
-
67
- /** Apple-style settings row */
68
- interface SettingsRowProps {
69
- icon?: React.ReactNode;
53
+ interface EditableFieldProps {
70
54
  label: string;
71
- value?: string | React.ReactNode;
72
- onClick?: () => void;
73
- destructive?: boolean;
55
+ value: string;
56
+ placeholder: string;
57
+ onSave: (value: string) => Promise<void>;
74
58
  disabled?: boolean;
75
- showChevron?: boolean;
76
- className?: string;
77
59
  }
78
60
 
79
- const SettingsRow = ({
80
- icon,
81
- label,
82
- value,
83
- onClick,
84
- destructive,
85
- disabled,
86
- showChevron = !!onClick,
87
- className,
88
- }: SettingsRowProps) => {
89
- const Wrapper = onClick ? 'button' : 'div';
61
+ const EditableField = ({ label, value, placeholder, onSave, disabled }: EditableFieldProps) => {
62
+ const [isEditing, setIsEditing] = useState(false);
63
+ const [editValue, setEditValue] = useState(value);
64
+ const [isSaving, setIsSaving] = useState(false);
65
+
66
+ useEffect(() => {
67
+ setEditValue(value);
68
+ }, [value]);
69
+
70
+ const handleSave = async () => {
71
+ if (editValue === value) {
72
+ setIsEditing(false);
73
+ return;
74
+ }
75
+ setIsSaving(true);
76
+ try {
77
+ await onSave(editValue);
78
+ setIsEditing(false);
79
+ } finally {
80
+ setIsSaving(false);
81
+ }
82
+ };
83
+
84
+ const handleKeyDown = (e: React.KeyboardEvent) => {
85
+ if (e.key === 'Enter') {
86
+ handleSave();
87
+ } else if (e.key === 'Escape') {
88
+ setEditValue(value);
89
+ setIsEditing(false);
90
+ }
91
+ };
92
+
93
+ if (isEditing) {
94
+ return (
95
+ <div className="py-4 border-b border-border/50 last:border-0">
96
+ <label className="block text-[13px] text-muted-foreground mb-1.5">
97
+ {label}
98
+ </label>
99
+ <div className="flex items-center gap-2">
100
+ <Input
101
+ value={editValue}
102
+ onChange={(e) => setEditValue(e.target.value)}
103
+ onKeyDown={handleKeyDown}
104
+ onBlur={handleSave}
105
+ placeholder={placeholder}
106
+ autoFocus
107
+ disabled={isSaving}
108
+ className="h-9 text-[15px]"
109
+ />
110
+ </div>
111
+ </div>
112
+ );
113
+ }
90
114
 
91
115
  return (
92
- <Wrapper
93
- onClick={disabled ? undefined : onClick}
116
+ <button
117
+ type="button"
118
+ onClick={() => !disabled && setIsEditing(true)}
94
119
  disabled={disabled}
95
120
  className={cn(
96
- 'w-full flex items-center gap-3 px-4 py-3 text-left transition-colors',
97
- onClick && !disabled && 'hover:bg-muted/50 active:bg-muted cursor-pointer',
98
- disabled && 'opacity-50 cursor-not-allowed',
99
- destructive && 'text-destructive',
100
- className
121
+ 'w-full py-4 border-b border-border/50 last:border-0 text-left',
122
+ 'transition-colors hover:bg-muted/30',
123
+ disabled && 'cursor-default hover:bg-transparent'
101
124
  )}
102
125
  >
103
- {icon && (
104
- <span className={cn('shrink-0', destructive ? 'text-destructive' : 'text-muted-foreground')}>
105
- {icon}
106
- </span>
107
- )}
108
- <span className="flex-1 font-medium">{label}</span>
109
- {value && (
110
- <span className="text-muted-foreground text-sm truncate max-w-[180px]">
111
- {value}
112
- </span>
113
- )}
114
- {showChevron && (
115
- <ChevronRight className="w-4 h-4 text-muted-foreground/50 shrink-0" />
116
- )}
117
- </Wrapper>
126
+ <div className="text-[13px] text-muted-foreground mb-0.5">
127
+ {label}
128
+ </div>
129
+ <div className={cn(
130
+ 'text-[15px]',
131
+ value ? 'text-foreground' : 'text-muted-foreground/60'
132
+ )}>
133
+ {value || placeholder}
134
+ </div>
135
+ </button>
118
136
  );
119
137
  };
120
138
 
121
139
  // ─────────────────────────────────────────────────────────────────────────────
122
- // Edit Field Dialog
140
+ // Section Component
123
141
  // ─────────────────────────────────────────────────────────────────────────────
124
142
 
125
- interface EditFieldDialogProps {
126
- open: boolean;
127
- onOpenChange: (open: boolean) => void;
128
- field: EditingField;
129
- currentValue: string;
130
- onSave: (field: string, value: string) => Promise<void>;
131
- isSaving: boolean;
143
+ interface SectionProps {
144
+ title?: string;
145
+ children: React.ReactNode;
146
+ className?: string;
132
147
  }
133
148
 
134
- const EditFieldDialog = ({ open, onOpenChange, field, currentValue, onSave, isSaving }: EditFieldDialogProps) => {
135
- const [value, setValue] = useState(currentValue);
136
- const t = useTypedT<I18nTranslations>();
137
-
138
- const labels = useMemo(() => ({
139
- fieldLabels: {
140
- first_name: t('layouts.profilePage.firstName'),
141
- last_name: t('layouts.profilePage.lastName'),
142
- company: t('layouts.profilePage.company'),
143
- position: t('layouts.profilePage.position'),
144
- phone: t('layouts.profilePage.phone'),
145
- } as Record<string, string>,
146
- save: t('layouts.profilePage.save'),
147
- saving: t('layouts.profilePage.saving'),
148
- cancel: t('layouts.profilePage.cancel'),
149
- enterField: t('layouts.profilePage.enterField'),
150
- }), [t]);
151
-
152
- useEffect(() => {
153
- setValue(currentValue);
154
- }, [currentValue, open]);
155
-
156
- const handleSave = async () => {
157
- if (field) {
158
- await onSave(field, value);
159
- }
160
- };
149
+ const Section = ({ title, children, className }: SectionProps) => (
150
+ <div className={cn('mb-10', className)}>
151
+ {title && (
152
+ <h2 className="text-[11px] font-medium text-muted-foreground uppercase tracking-wider mb-2 px-1">
153
+ {title}
154
+ </h2>
155
+ )}
156
+ <div className="bg-card rounded-xl px-4">
157
+ {children}
158
+ </div>
159
+ </div>
160
+ );
161
161
 
162
- if (!field) return null;
162
+ // ─────────────────────────────────────────────────────────────────────────────
163
+ // Action Button Component
164
+ // ─────────────────────────────────────────────────────────────────────────────
163
165
 
164
- const fieldLabel = labels.fieldLabels[field] || field;
166
+ interface ActionButtonProps {
167
+ icon?: React.ReactNode;
168
+ label: string;
169
+ onClick: () => void;
170
+ variant?: 'default' | 'destructive';
171
+ disabled?: boolean;
172
+ }
165
173
 
166
- return (
167
- <Dialog open={open} onOpenChange={onOpenChange}>
168
- <DialogContent className="sm:max-w-md">
169
- <DialogHeader>
170
- <DialogTitle>{fieldLabel}</DialogTitle>
171
- </DialogHeader>
172
- <div className="py-4">
173
- <Input
174
- value={value}
175
- onChange={(e) => setValue(e.target.value)}
176
- placeholder={labels.enterField.replace('{field}', fieldLabel.toLowerCase())}
177
- autoFocus
178
- onKeyDown={(e) => e.key === 'Enter' && handleSave()}
179
- />
180
- </div>
181
- <DialogFooter>
182
- <Button variant="ghost" onClick={() => onOpenChange(false)} disabled={isSaving}>
183
- {labels.cancel}
184
- </Button>
185
- <Button onClick={handleSave} disabled={isSaving}>
186
- {isSaving ? labels.saving : labels.save}
187
- </Button>
188
- </DialogFooter>
189
- </DialogContent>
190
- </Dialog>
191
- );
192
- };
174
+ const ActionButton = ({ icon, label, onClick, variant = 'default', disabled }: ActionButtonProps) => (
175
+ <button
176
+ type="button"
177
+ onClick={onClick}
178
+ disabled={disabled}
179
+ className={cn(
180
+ 'w-full flex items-center justify-center gap-2 py-3.5 border-b border-border/50 last:border-0',
181
+ 'text-[15px] font-medium transition-colors',
182
+ variant === 'destructive'
183
+ ? 'text-destructive hover:bg-destructive/5'
184
+ : 'text-foreground hover:bg-muted/30',
185
+ disabled && 'opacity-50 cursor-not-allowed'
186
+ )}
187
+ >
188
+ {icon}
189
+ {label}
190
+ </button>
191
+ );
193
192
 
194
193
  // ─────────────────────────────────────────────────────────────────────────────
195
194
  // Main Component
@@ -202,8 +201,6 @@ const ProfileContent = ({
202
201
  enableDeleteAccount = true,
203
202
  }: ProfileLayoutProps) => {
204
203
  const { user, isLoading, logout, uploadAvatar, updateProfile } = useAuth();
205
- const [editingField, setEditingField] = useState<EditingField>(null);
206
- const [isSaving, setIsSaving] = useState(false);
207
204
  const [isUploading, setIsUploading] = useState(false);
208
205
  const t = useTypedT<I18nTranslations>();
209
206
 
@@ -217,39 +214,35 @@ const ProfileContent = ({
217
214
  const labels = useMemo(() => ({
218
215
  title: title || t('layouts.profilePage.title'),
219
216
  // Sections
220
- account: t('layouts.profilePage.account'),
221
217
  personalInfo: t('layouts.profilePage.personalInfo'),
222
218
  work: t('layouts.profilePage.work'),
223
219
  security: t('layouts.profilePage.security'),
224
- actions: t('layouts.profilePage.actions'),
225
220
  // Fields
226
- fieldLabels: {
227
- first_name: t('layouts.profilePage.firstName'),
228
- last_name: t('layouts.profilePage.lastName'),
229
- company: t('layouts.profilePage.company'),
230
- position: t('layouts.profilePage.position'),
231
- phone: t('layouts.profilePage.phone'),
232
- } as Record<string, string>,
221
+ firstName: t('layouts.profilePage.firstName'),
222
+ lastName: t('layouts.profilePage.lastName'),
233
223
  email: t('layouts.profilePage.email'),
234
- // Values
224
+ phone: t('layouts.profilePage.phone'),
225
+ company: t('layouts.profilePage.company'),
226
+ position: t('layouts.profilePage.position'),
227
+ // Placeholders
235
228
  notSet: t('layouts.profilePage.notSet'),
236
- on: t('layouts.profilePage.on'),
237
- off: t('layouts.profilePage.off'),
229
+ addFirstName: t('layouts.profilePage.addFirstName') || 'Add first name',
230
+ addLastName: t('layouts.profilePage.addLastName') || 'Add last name',
231
+ addPhone: t('layouts.profilePage.addPhone') || 'Add phone number',
232
+ addCompany: t('layouts.profilePage.addCompany') || 'Add company',
233
+ addPosition: t('layouts.profilePage.addPosition') || 'Add position',
234
+ // 2FA
235
+ twoFactorAuth: t('layouts.profilePage.twoFactorAuth'),
236
+ twoFactorEnabled: t('layouts.profilePage.twoFactorEnabled') || 'Two-factor authentication is enabled',
237
+ twoFactorDisabled: t('layouts.profilePage.twoFactorDisabled') || 'Add an extra layer of security',
238
+ enable2FA: t('layouts.profilePage.enable2FA') || 'Enable 2FA',
238
239
  // Actions
239
- save: t('layouts.profilePage.save'),
240
- saving: t('layouts.profilePage.saving'),
241
- cancel: t('layouts.profilePage.cancel'),
242
240
  signOut: t('layouts.profilePage.signOut'),
243
241
  deleteAccount: t('layouts.profilePage.deleteAccount'),
244
- // 2FA
245
- twoFactorAuth: t('layouts.profilePage.twoFactorAuth'),
246
242
  // Avatar
247
- avatar: t('layouts.profilePage.avatar'),
248
243
  changeAvatar: t('layouts.profilePage.changeAvatar'),
249
244
  // Membership
250
245
  memberSince: t('layouts.profilePage.memberSince'),
251
- // Placeholder
252
- enterField: t('layouts.profilePage.enterField'),
253
246
  // Messages
254
247
  profileUpdated: t('layouts.profilePage.profileUpdated'),
255
248
  avatarUpdated: t('layouts.profilePage.avatarUpdated'),
@@ -330,16 +323,13 @@ const ProfileContent = ({
330
323
  };
331
324
 
332
325
  const handleFieldSave = async (field: string, value: string) => {
333
- setIsSaving(true);
334
326
  try {
335
327
  await updateProfile({ [field]: value });
336
328
  toast.success(labels.profileUpdated);
337
- setEditingField(null);
338
329
  } catch (error: any) {
339
330
  profileLogger.error('Profile update error:', error);
340
331
  toast.error(error?.response?.data?.[field]?.[0] || labels.failedToUpdate);
341
- } finally {
342
- setIsSaving(false);
332
+ throw error;
343
333
  }
344
334
  };
345
335
 
@@ -388,28 +378,33 @@ const ProfileContent = ({
388
378
  }
389
379
 
390
380
  return (
391
- <div className="container mx-auto px-4 py-8 max-w-lg">
392
- {/* Header */}
393
- <h1 className="text-2xl font-bold text-center mb-8">{labels.title}</h1>
394
-
395
- {/* Avatar & Name */}
396
- <div className="flex flex-col items-center mb-8">
397
- <div className="relative group mb-3">
398
- <Avatar className="w-24 h-24 text-3xl">
381
+ <div className="container mx-auto px-4 py-12 max-w-md">
382
+ {/* Header with Avatar */}
383
+ <div className="flex flex-col items-center mb-12">
384
+ <div className="relative group mb-4">
385
+ <Avatar className="w-28 h-28 text-3xl">
399
386
  {user.avatar ? (
400
- <img src={user.avatar} alt={labels.avatar} className="w-full h-full object-cover" />
387
+ <img src={user.avatar} alt="" className="w-full h-full object-cover" />
401
388
  ) : (
402
- <AvatarFallback className="bg-primary text-primary-foreground">
389
+ <AvatarFallback className="bg-muted text-muted-foreground text-2xl font-medium">
403
390
  {getInitials(user.display_username || user.email || '')}
404
391
  </AvatarFallback>
405
392
  )}
406
393
  </Avatar>
407
- <label className="absolute inset-0 rounded-full bg-black/50 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center cursor-pointer" title={labels.changeAvatar}>
408
- {isUploading ? (
409
- <div className="w-6 h-6 border-2 border-white border-t-transparent rounded-full animate-spin" />
410
- ) : (
411
- <Camera className="w-6 h-6 text-white" />
394
+ <label
395
+ className={cn(
396
+ 'absolute inset-0 rounded-full flex items-center justify-center cursor-pointer',
397
+ 'bg-black/0 group-hover:bg-black/40 transition-colors'
412
398
  )}
399
+ title={labels.changeAvatar}
400
+ >
401
+ <div className="opacity-0 group-hover:opacity-100 transition-opacity">
402
+ {isUploading ? (
403
+ <div className="w-6 h-6 border-2 border-white border-t-transparent rounded-full animate-spin" />
404
+ ) : (
405
+ <Camera className="w-6 h-6 text-white" />
406
+ )}
407
+ </div>
413
408
  <input
414
409
  type="file"
415
410
  accept="image/*"
@@ -419,118 +414,107 @@ const ProfileContent = ({
419
414
  />
420
415
  </label>
421
416
  </div>
422
- <h2 className="text-xl font-semibold">{user.display_username || user.email}</h2>
417
+
418
+ <h1 className="text-2xl font-semibold tracking-tight">
419
+ {user.full_name || user.display_username || user.email}
420
+ </h1>
421
+
422
+ <p className="text-[15px] text-muted-foreground mt-1">
423
+ {user.email}
424
+ </p>
425
+
423
426
  {user.date_joined && (
424
- <p className="text-sm text-muted-foreground">
427
+ <p className="text-[13px] text-muted-foreground/60 mt-2">
425
428
  {labels.memberSince.replace('{date}', moment.utc(user.date_joined).local().format('MMMM YYYY'))}
426
429
  </p>
427
430
  )}
428
431
  </div>
429
432
 
430
- <div className="space-y-6">
431
- {/* Account Info */}
432
- <div>
433
- <SettingsGroupHeader>{labels.account}</SettingsGroupHeader>
434
- <SettingsGroup>
435
- <SettingsRow
436
- icon={<Mail className="w-5 h-5" />}
437
- label={labels.email}
438
- value={user.email}
439
- showChevron={false}
440
- />
441
- </SettingsGroup>
442
- </div>
443
-
444
- {/* Personal Info */}
445
- <div>
446
- <SettingsGroupHeader>{labels.personalInfo}</SettingsGroupHeader>
447
- <SettingsGroup>
448
- <SettingsRow
449
- icon={<User className="w-5 h-5" />}
450
- label={labels.fieldLabels.first_name}
451
- value={user.first_name || labels.notSet}
452
- onClick={() => setEditingField('first_name')}
453
- />
454
- <SettingsRow
455
- icon={<User className="w-5 h-5" />}
456
- label={labels.fieldLabels.last_name}
457
- value={user.last_name || labels.notSet}
458
- onClick={() => setEditingField('last_name')}
459
- />
460
- <SettingsRow
461
- icon={<Phone className="w-5 h-5" />}
462
- label={labels.fieldLabels.phone}
463
- value={user.phone || labels.notSet}
464
- onClick={() => setEditingField('phone')}
465
- />
466
- </SettingsGroup>
467
- </div>
468
-
469
- {/* Work Info */}
470
- <div>
471
- <SettingsGroupHeader>{labels.work}</SettingsGroupHeader>
472
- <SettingsGroup>
473
- <SettingsRow
474
- icon={<Building2 className="w-5 h-5" />}
475
- label={labels.fieldLabels.company}
476
- value={user.company || labels.notSet}
477
- onClick={() => setEditingField('company')}
478
- />
479
- <SettingsRow
480
- icon={<Briefcase className="w-5 h-5" />}
481
- label={labels.fieldLabels.position}
482
- value={user.position || labels.notSet}
483
- onClick={() => setEditingField('position')}
484
- />
485
- </SettingsGroup>
486
- </div>
487
-
488
- {/* Security */}
489
- {enable2FA && (
490
- <div>
491
- <SettingsGroupHeader>{labels.security}</SettingsGroupHeader>
492
- <SettingsGroup>
493
- <SettingsRow
494
- icon={has2FAEnabled ? <ShieldCheck className="w-5 h-5 text-green-500" /> : <Shield className="w-5 h-5" />}
495
- label={labels.twoFactorAuth}
496
- value={has2FAEnabled ? labels.on : labels.off}
497
- onClick={() => !has2FAEnabled && setShow2FASetup(true)}
498
- showChevron={!has2FAEnabled}
499
- />
500
- </SettingsGroup>
433
+ {/* Personal Info */}
434
+ <Section title={labels.personalInfo}>
435
+ <EditableField
436
+ label={labels.firstName}
437
+ value={user.first_name || ''}
438
+ placeholder={labels.addFirstName}
439
+ onSave={(value) => handleFieldSave('first_name', value)}
440
+ />
441
+ <EditableField
442
+ label={labels.lastName}
443
+ value={user.last_name || ''}
444
+ placeholder={labels.addLastName}
445
+ onSave={(value) => handleFieldSave('last_name', value)}
446
+ />
447
+ <EditableField
448
+ label={labels.phone}
449
+ value={user.phone || ''}
450
+ placeholder={labels.addPhone}
451
+ onSave={(value) => handleFieldSave('phone', value)}
452
+ />
453
+ </Section>
454
+
455
+ {/* Work */}
456
+ <Section title={labels.work}>
457
+ <EditableField
458
+ label={labels.company}
459
+ value={user.company || ''}
460
+ placeholder={labels.addCompany}
461
+ onSave={(value) => handleFieldSave('company', value)}
462
+ />
463
+ <EditableField
464
+ label={labels.position}
465
+ value={user.position || ''}
466
+ placeholder={labels.addPosition}
467
+ onSave={(value) => handleFieldSave('position', value)}
468
+ />
469
+ </Section>
470
+
471
+ {/* Security */}
472
+ {enable2FA && (
473
+ <Section title={labels.security}>
474
+ <div className="py-4 border-b border-border/50 last:border-0">
475
+ <div className="flex items-center justify-between">
476
+ <div>
477
+ <div className="text-[15px] font-medium">
478
+ {labels.twoFactorAuth}
479
+ </div>
480
+ <div className="text-[13px] text-muted-foreground mt-0.5">
481
+ {has2FAEnabled ? labels.twoFactorEnabled : labels.twoFactorDisabled}
482
+ </div>
483
+ </div>
484
+ {!has2FAEnabled && (
485
+ <Button
486
+ variant="outline"
487
+ size="sm"
488
+ onClick={() => setShow2FASetup(true)}
489
+ className="text-[13px] h-8"
490
+ >
491
+ {labels.enable2FA}
492
+ </Button>
493
+ )}
494
+ {has2FAEnabled && (
495
+ <div className="w-2 h-2 rounded-full bg-green-500" />
496
+ )}
497
+ </div>
501
498
  </div>
502
- )}
503
-
504
- {/* Actions */}
505
- <div>
506
- <SettingsGroupHeader>{labels.actions}</SettingsGroupHeader>
507
- <SettingsGroup>
508
- <SettingsRow
509
- icon={<LogOut className="w-5 h-5" />}
510
- label={labels.signOut}
511
- onClick={handleLogout}
512
- />
513
- {enableDeleteAccount && (
514
- <SettingsRow
515
- icon={<Trash2 className="w-5 h-5" />}
516
- label={labels.deleteAccount}
517
- onClick={() => setShowDeleteConfirm(true)}
518
- destructive
519
- />
520
- )}
521
- </SettingsGroup>
522
- </div>
523
- </div>
499
+ </Section>
500
+ )}
524
501
 
525
- {/* Edit Field Dialog */}
526
- <EditFieldDialog
527
- open={editingField !== null}
528
- onOpenChange={(open) => !open && setEditingField(null)}
529
- field={editingField}
530
- currentValue={editingField ? (user[editingField] || '') : ''}
531
- onSave={handleFieldSave}
532
- isSaving={isSaving}
533
- />
502
+ {/* Actions */}
503
+ <Section>
504
+ <ActionButton
505
+ icon={<LogOut className="w-4 h-4" />}
506
+ label={labels.signOut}
507
+ onClick={handleLogout}
508
+ />
509
+ {enableDeleteAccount && (
510
+ <ActionButton
511
+ icon={<Trash2 className="w-4 h-4" />}
512
+ label={labels.deleteAccount}
513
+ onClick={() => setShowDeleteConfirm(true)}
514
+ variant="destructive"
515
+ />
516
+ )}
517
+ </Section>
534
518
 
535
519
  {/* Delete Account Dialog */}
536
520
  <DeleteAccountDialog
@@ -582,9 +566,9 @@ const DeleteAccountDialog = ({ open, onOpenChange }: { open: boolean; onOpenChan
582
566
 
583
567
  return (
584
568
  <Dialog open={open} onOpenChange={onOpenChange}>
585
- <DialogContent>
569
+ <DialogContent className="sm:max-w-md">
586
570
  <DialogHeader>
587
- <DialogTitle className="text-destructive">{labels.title}</DialogTitle>
571
+ <DialogTitle>{labels.title}</DialogTitle>
588
572
  <DialogDescription>
589
573
  {labels.description}
590
574
  </DialogDescription>
@@ -595,9 +579,9 @@ const DeleteAccountDialog = ({ open, onOpenChange }: { open: boolean; onOpenChan
595
579
  <p className="text-sm text-destructive">{error}</p>
596
580
  )}
597
581
  <div className="space-y-2">
598
- <p className="text-sm text-muted-foreground">
582
+ <p className="text-[13px] text-muted-foreground">
599
583
  {labels.typeToConfirm.replace('{word}', '')}
600
- <span className="font-mono font-bold">{labels.confirmationWord}</span>
584
+ <span className="font-mono font-semibold text-foreground">{labels.confirmationWord}</span>
601
585
  </p>
602
586
  <Input
603
587
  value={confirmationInput}
@@ -605,6 +589,7 @@ const DeleteAccountDialog = ({ open, onOpenChange }: { open: boolean; onOpenChan
605
589
  placeholder={labels.confirmationWord}
606
590
  disabled={isLoading}
607
591
  autoComplete="off"
592
+ className="font-mono"
608
593
  />
609
594
  </div>
610
595
  </div>