@djangocfg/layouts 2.1.425 → 2.1.427

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.
Files changed (66) hide show
  1. package/package.json +15 -17
  2. package/src/layouts/AppLayout/AppLayout.tsx +0 -7
  3. package/src/layouts/AppLayout/BaseApp.tsx +29 -52
  4. package/src/layouts/AuthLayout/components/steps/SetupStep/SetupLoading.tsx +6 -4
  5. package/src/layouts/PrivateLayout/PrivateLayout.tsx +7 -3
  6. package/src/layouts/PrivateLayout/components/PrivateContent.tsx +5 -1
  7. package/src/layouts/PrivateLayout/components/PrivateSidebarAccount.tsx +105 -70
  8. package/src/layouts/PrivateLayout/types.ts +8 -0
  9. package/src/layouts/PublicLayout/components/UserMenu.tsx +68 -113
  10. package/src/layouts/PublicLayout/navbars/MinimalNavbar/MinimalNavbar.tsx +0 -6
  11. package/src/layouts/PublicLayout/navbars/MinimalNavbar/index.ts +1 -1
  12. package/src/layouts/SettingsLayout/README.md +258 -0
  13. package/src/layouts/SettingsLayout/SettingsDialog.tsx +101 -0
  14. package/src/layouts/SettingsLayout/SettingsForm.tsx +100 -0
  15. package/src/layouts/SettingsLayout/components/ApiKeySection/ApiKeySection.tsx +189 -0
  16. package/src/layouts/SettingsLayout/components/SettingsNav.tsx +71 -0
  17. package/src/layouts/SettingsLayout/components/SettingsNavItem.tsx +57 -0
  18. package/src/layouts/SettingsLayout/components/SettingsPanel.tsx +48 -0
  19. package/src/layouts/SettingsLayout/components/SettingsSearch.tsx +50 -0
  20. package/src/layouts/SettingsLayout/components/SettingsShell.tsx +77 -0
  21. package/src/layouts/SettingsLayout/components/SettingsTabs.tsx +56 -0
  22. package/src/layouts/{ProfileLayout → SettingsLayout}/components/TwoFactorSection/TwoFactorSection.tsx +84 -130
  23. package/src/layouts/SettingsLayout/components/index.ts +6 -0
  24. package/src/layouts/SettingsLayout/context/SettingsContext.tsx +122 -0
  25. package/src/layouts/SettingsLayout/context/index.ts +2 -0
  26. package/src/layouts/SettingsLayout/hooks/index.ts +12 -0
  27. package/src/layouts/SettingsLayout/hooks/useProfileSave.ts +95 -0
  28. package/src/layouts/SettingsLayout/hooks/useSettingsDialog.ts +52 -0
  29. package/src/layouts/SettingsLayout/hooks/useSettingsSections.ts +123 -0
  30. package/src/layouts/SettingsLayout/hooks/useSettingsUrl.ts +140 -0
  31. package/src/layouts/SettingsLayout/index.ts +67 -0
  32. package/src/layouts/SettingsLayout/sections/AccountSection.tsx +100 -0
  33. package/src/layouts/SettingsLayout/sections/ApiKeysSection.tsx +15 -0
  34. package/src/layouts/SettingsLayout/sections/DeleteAccountRow.tsx +57 -0
  35. package/src/layouts/SettingsLayout/sections/PreferencesRows.tsx +43 -0
  36. package/src/layouts/SettingsLayout/sections/SecuritySection.tsx +15 -0
  37. package/src/layouts/SettingsLayout/sections/builtins.tsx +77 -0
  38. package/src/layouts/SettingsLayout/sections/index.ts +8 -0
  39. package/src/layouts/SettingsLayout/store.ts +47 -0
  40. package/src/layouts/SettingsLayout/types.ts +107 -0
  41. package/src/layouts/index.ts +1 -1
  42. package/src/layouts/types/index.ts +0 -1
  43. package/src/layouts/types/layout.types.ts +0 -4
  44. package/src/layouts/ProfileLayout/ProfileDialog/ProfileDialog.tsx +0 -56
  45. package/src/layouts/ProfileLayout/ProfileDialog/index.ts +0 -4
  46. package/src/layouts/ProfileLayout/ProfileDialog/store.ts +0 -51
  47. package/src/layouts/ProfileLayout/ProfileForm/context.tsx +0 -123
  48. package/src/layouts/ProfileLayout/ProfileForm/index.tsx +0 -147
  49. package/src/layouts/ProfileLayout/README.md +0 -150
  50. package/src/layouts/ProfileLayout/components/ActionButton.tsx +0 -38
  51. package/src/layouts/ProfileLayout/components/ApiKeySection/ApiKeySection.tsx +0 -197
  52. package/src/layouts/ProfileLayout/components/DeleteAccountSection.tsx +0 -44
  53. package/src/layouts/ProfileLayout/components/EditableField.tsx +0 -128
  54. package/src/layouts/ProfileLayout/components/PreferencesSection.tsx +0 -56
  55. package/src/layouts/ProfileLayout/components/ProfileHeader.tsx +0 -110
  56. package/src/layouts/ProfileLayout/components/ProfileTab.tsx +0 -35
  57. package/src/layouts/ProfileLayout/components/Section.tsx +0 -22
  58. package/src/layouts/ProfileLayout/components/index.ts +0 -11
  59. package/src/layouts/ProfileLayout/hooks/index.ts +0 -2
  60. package/src/layouts/ProfileLayout/hooks/useProfileTabs.ts +0 -56
  61. package/src/layouts/ProfileLayout/index.ts +0 -8
  62. package/src/layouts/ProfileLayout/types.ts +0 -48
  63. /package/src/layouts/{ProfileLayout → SettingsLayout}/components/ApiKeySection/context.tsx +0 -0
  64. /package/src/layouts/{ProfileLayout → SettingsLayout}/components/ApiKeySection/index.ts +0 -0
  65. /package/src/layouts/{ProfileLayout → SettingsLayout}/components/AvatarSection.tsx +0 -0
  66. /package/src/layouts/{ProfileLayout → SettingsLayout}/components/TwoFactorSection/index.ts +0 -0
@@ -1,150 +0,0 @@
1
- # ProfileLayout
2
-
3
- User profile page with tabbed interface: **Profile** | **Security** | **API Keys**.
4
-
5
- ## Usage
6
-
7
- ### Standalone page
8
-
9
- ```tsx
10
- import { ProfileForm } from '@djangocfg/layouts';
11
-
12
- <ProfileForm
13
- enable2FA
14
- enableAPIKeys
15
- enableDeleteAccount
16
- onUnauthenticated={() => router.push('/login')}
17
- slots={{ headerBadge: <PlanBadge /> }}
18
- tabs={[
19
- { value: 'billing', label: 'Billing', content: <BillingPanel /> },
20
- ]}
21
- />
22
- ```
23
-
24
- ### Dialog
25
-
26
- ```tsx
27
- import { ProfileDialog, useProfileDialogStore } from '@djangocfg/layouts';
28
-
29
- // Open on a specific tab
30
- useProfileDialogStore.getState().open({ initialTab: 'security' });
31
- // …or with custom tabs/slots — see "Opening with custom content" below.
32
-
33
- // In your layout (lazy-loaded)
34
- <ProfileDialog />
35
- ```
36
-
37
- ## Props
38
-
39
- | Prop | Default | Description |
40
- |------|---------|-------------|
41
- | `enable2FA` | `false` | Show Security tab with 2FA management |
42
- | `enableAPIKeys` | `true` | Show API Keys tab |
43
- | `enableDeleteAccount` | `true` | Show Delete Account in header dropdown |
44
- | `onUnauthenticated` | — | Callback when user is not authenticated |
45
- | `tabs` | `[]` | Extra `ProfileTab[]` appended after built-in tabs |
46
- | `slots` | — | Named slots: `headerMenuItems`, `headerBadge`, `headerAfter`, `footer` |
47
- | `title` | i18n | Page title |
48
- | `defaultTab` | — | Initial active tab. Accepts a built-in value or a custom tab's `value`. When provided (e.g. by `ProfileDialog`), tabs start at this value. |
49
-
50
- ## Global Profile Dialog
51
-
52
- `ProfileDialog` is a Zustand-driven dialog that can be opened from anywhere:
53
-
54
- ```tsx
55
- import { useProfileDialogStore } from '@djangocfg/layouts';
56
-
57
- const { open, close } = useProfileDialogStore();
58
-
59
- // Open on Profile tab
60
- open();
61
-
62
- // Open on Security tab
63
- open({ initialTab: 'security' });
64
- ```
65
-
66
- ### Opening with custom content
67
-
68
- `open()` accepts the same content props as `ProfileForm` (all optional), so any
69
- caller can open the dialog with custom tabs / slots / flags — and start on a
70
- custom tab — in one line:
71
-
72
- ```tsx
73
- useProfileDialogStore.getState().open({
74
- initialTab: 'api-keys',
75
- tabs: [{ value: 'api-keys', label: 'API keys', content: <MyApiKeysTab /> }],
76
- enableAPIKeys: false, // hide the built-in API Keys tab
77
- });
78
- ```
79
-
80
- The `open()` payload is typed as `ProfileDialogContent`:
81
-
82
- | Field | Type | Notes |
83
- |-------|------|-------|
84
- | `initialTab` | `string` | Built-in (`'profile' \| 'security' \| 'api-keys'`) **or** a custom tab's `value`. |
85
- | `tabs` | `ProfileTab[]` | Extra tabs appended after the built-in ones. |
86
- | `slots` | `ProfileSlots` | `headerMenuItems`, `headerBadge`, `headerAfter`, `footer`. |
87
- | `enable2FA` | `boolean` | Override the Security tab. |
88
- | `enableAPIKeys` | `boolean` | Override the built-in API Keys tab. |
89
- | `enableDeleteAccount` | `boolean` | Override Delete Account in the header dropdown. |
90
- | `title` | `string` | Dialog title (the `<ProfileDialog title>` prop wins if both set). |
91
-
92
- Each `open()` **replaces** the previous content (it does not merge), and `close()`
93
- clears it — so a bare `open()` always shows the default profile. Omitted fields
94
- fall through to `ProfileForm`'s own defaults, keeping `open()` / `open({ initialTab })`
95
- backward compatible.
96
-
97
- Wire it into `PrivateLayout` for global access:
98
-
99
- ```tsx
100
- import { PrivateLayout } from '@djangocfg/layouts';
101
-
102
- <PrivateLayout sidebar={...} header={...}>
103
- {children}
104
- </PrivateLayout>
105
- // ProfileDialog is rendered automatically inside PrivateLayout
106
- ```
107
-
108
- ## Sidebar Account Button Modes
109
-
110
- The footer account button in `PrivateLayout` supports two modes via `HeaderConfig`:
111
-
112
- ```tsx
113
- const header: HeaderConfig = {
114
- // ...
115
- accountAction: 'menu', // Default — opens DropdownMenu with links, theme, logout
116
- accountAction: 'dialog', // Opens ProfileDialog instead
117
- };
118
- ```
119
-
120
- ## Architecture
121
-
122
- ```
123
- ProfileLayout/
124
- ├── ProfileForm/
125
- │ ├── index.tsx Shell: ProfileProvider → header + Tabs
126
- │ └── context.tsx Root context (labels, onLogout, onFieldSave)
127
- ├── ProfileDialog/
128
- │ ├── ProfileDialog.tsx Dialog wrapper around ProfileForm
129
- │ └── store.ts Zustand store (isOpen + ProfileDialogContent: tabs/slots/flags/initialTab/title)
130
- ├── hooks/
131
- │ └── useProfileTabs.ts Local tab state (useState)
132
- ├── types.ts ProfileFormProps, ProfileTab, ProfileSlots
133
- └── components/
134
- ├── ProfileHeader Avatar + name + dropdown menu
135
- ├── ProfileTab Editable fields grid (first_name, last_name, phone, company, position)
136
- ├── TwoFactorSection/ Own mini-context (2FA status, setup, disable)
137
- ├── ApiKeySection/ Own mini-context (useApiKey, reveal/arm state)
138
- ├── EditableField Inline-editable text/phone field
139
- ├── Section Card-like section wrapper
140
- └── DeleteAccount Confirmation dialog + delete action
141
- ```
142
-
143
- Each section (`TwoFactorSection`, `ApiKeySection`) owns a **mini-context** — isolated state, labels, and API logic. Root context stays minimal.
144
-
145
- ## Dependencies
146
-
147
- - `@djangocfg/api/hooks` — `useApiKey` (retrieve + regenerate)
148
- - `@djangocfg/api/auth` — `useAuth`, `useDeleteAccount`, `useTwoFactorSetup`, `useTwoFactorStatus`
149
- - `@djangocfg/ui-core/components` — UI primitives + `CopyButton`
150
- - `@djangocfg/i18n` — `useAppT`
@@ -1,38 +0,0 @@
1
- 'use client';
2
-
3
- import React from 'react';
4
-
5
- import { cn } from '@djangocfg/ui-core/lib';
6
-
7
- interface ActionButtonProps {
8
- icon?: React.ReactNode;
9
- label: React.ReactNode;
10
- onClick: () => void;
11
- variant?: 'default' | 'destructive';
12
- disabled?: boolean;
13
- }
14
-
15
- export const ActionButton = ({
16
- icon,
17
- label,
18
- onClick,
19
- variant = 'default',
20
- disabled,
21
- }: ActionButtonProps) => (
22
- <button
23
- type="button"
24
- onClick={onClick}
25
- disabled={disabled}
26
- className={cn(
27
- 'w-full flex items-center justify-center gap-2 py-3.5 border-b border-border/50 last:border-0',
28
- 'text-[15px] font-medium transition-colors',
29
- variant === 'destructive'
30
- ? 'text-destructive hover:bg-destructive/5'
31
- : 'text-foreground hover:bg-muted/30',
32
- disabled && 'opacity-50 cursor-not-allowed'
33
- )}
34
- >
35
- {icon}
36
- {label}
37
- </button>
38
- );
@@ -1,197 +0,0 @@
1
- 'use client';
2
-
3
- import { Check, FlaskConical, KeyRound, Loader2, RefreshCw } from 'lucide-react';
4
- import moment from 'moment';
5
- import React from 'react';
6
-
7
- import {
8
- Badge,
9
- Button,
10
- Card,
11
- CardContent,
12
- CardDescription,
13
- CardHeader,
14
- CardTitle,
15
- CopyButton,
16
- } from '@djangocfg/ui-core/components';
17
- import { cn } from '@djangocfg/ui-core/lib';
18
-
19
- import { ApiKeyProvider, useApiKeyContext } from './context';
20
-
21
- // ─────────────────────────────────────────────────────────────────────────────
22
- // Helpers
23
- // ─────────────────────────────────────────────────────────────────────────────
24
-
25
- function formatDate(iso: string | null, fallback: string): string {
26
- if (!iso) return fallback;
27
- return moment.utc(iso).local().format('MMM D, YYYY');
28
- }
29
-
30
- /** Returns true if the backend already masked the key (contains •). */
31
- function isMasked(key: string): boolean {
32
- return key.includes('•');
33
- }
34
-
35
- // ─────────────────────────────────────────────────────────────────────────────
36
- // Inner component (uses context)
37
- // ─────────────────────────────────────────────────────────────────────────────
38
-
39
- function ApiKeyCard() {
40
- const {
41
- labels,
42
- apiKey,
43
- reissuedAt,
44
- createdAt,
45
- isLoading,
46
- error,
47
- isArmed,
48
- arm,
49
- disarm,
50
- regenerate,
51
- isRegenerating,
52
- isFresh,
53
- dismissFresh,
54
- testKey,
55
- isTesting,
56
- testResult,
57
- } = useApiKeyContext();
58
-
59
- if (isLoading) {
60
- return (
61
- <Card>
62
- <CardContent className="flex items-center justify-center py-10">
63
- <Loader2 className="w-5 h-5 animate-spin text-muted-foreground" />
64
- </CardContent>
65
- </Card>
66
- );
67
- }
68
-
69
- const masked = apiKey ? isMasked(apiKey) : false;
70
- const displayKey = apiKey ?? '—';
71
-
72
- return (
73
- <Card>
74
- <CardHeader>
75
- <div className="flex items-center gap-3">
76
- <div className="w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center flex-shrink-0">
77
- <KeyRound className="w-5 h-5 text-primary" />
78
- </div>
79
- <div className="flex-1 min-w-0">
80
- <div className="flex items-center gap-2">
81
- <CardTitle className="text-base">{labels.title}</CardTitle>
82
- {apiKey && <Badge variant="secondary" className="text-xs">Active</Badge>}
83
- </div>
84
- <CardDescription className="mt-0.5">{labels.description}</CardDescription>
85
- </div>
86
- </div>
87
- </CardHeader>
88
-
89
- {apiKey && (
90
- <CardContent className="pt-0 space-y-3">
91
- {/* Fresh-key banner */}
92
- {isFresh && (
93
- <div className="flex items-center gap-2 px-3 py-2 rounded-md bg-amber-50 dark:bg-amber-950/30 border border-amber-200 dark:border-amber-900 text-amber-800 dark:text-amber-200 text-sm">
94
- <KeyRound className="w-4 h-4 flex-shrink-0" />
95
- <span className="flex-1">This is your new API key — copy it now. It will be masked when you leave this page.</span>
96
- <Button variant="ghost" size="sm" className="h-7 px-2" onClick={dismissFresh}>
97
- <Check className="w-4 h-4 mr-1" />
98
- {labels.done}
99
- </Button>
100
- </div>
101
- )}
102
-
103
- {/* Test result banner */}
104
- {testResult !== null && (
105
- <div className={cn(
106
- 'flex items-center gap-2 px-3 py-2 rounded-md text-sm',
107
- testResult
108
- ? 'bg-green-50 dark:bg-green-950/30 border border-green-200 dark:border-green-900 text-green-800 dark:text-green-200'
109
- : 'bg-red-50 dark:bg-red-950/30 border border-red-200 dark:border-red-900 text-red-800 dark:text-red-200',
110
- )}>
111
- <FlaskConical className="w-4 h-4 flex-shrink-0" />
112
- <span>{testResult ? labels.testSuccess : labels.testFailed}</span>
113
- </div>
114
- )}
115
-
116
- <div className="flex items-center gap-2">
117
- <div className="flex-1 min-w-0 px-3 py-2.5 rounded-md bg-muted font-mono text-sm select-all">
118
- <span className={cn(masked && 'tracking-widest')}>
119
- {displayKey}
120
- </span>
121
- </div>
122
-
123
- {/* Copy only when the key is fresh (full key after regenerate) */}
124
- {isFresh && <CopyButton value={apiKey} />}
125
- </div>
126
-
127
- <div className="flex items-center gap-4 text-xs text-muted-foreground">
128
- {createdAt && <span>{labels.created}: {formatDate(createdAt, '—')}</span>}
129
- <span>{labels.reissued}: {formatDate(reissuedAt, labels.neverReissued)}</span>
130
- </div>
131
- </CardContent>
132
- )}
133
-
134
- {error && (
135
- <CardContent className="pt-0">
136
- <p className="text-sm text-destructive">{error}</p>
137
- </CardContent>
138
- )}
139
-
140
- <CardContent className="pt-0">
141
- {isArmed ? (
142
- <div className="flex items-center gap-2">
143
- <Button
144
- variant="destructive"
145
- size="sm"
146
- onClick={regenerate}
147
- disabled={isRegenerating}
148
- >
149
- {isRegenerating
150
- ? <><Loader2 className="mr-2 h-4 w-4 animate-spin" />{labels.regenerating}</>
151
- : labels.confirmRegenerate}
152
- </Button>
153
- <Button variant="ghost" size="sm" onClick={disarm} disabled={isRegenerating}>
154
- Cancel
155
- </Button>
156
- </div>
157
- ) : (
158
- <div className="flex items-center gap-2">
159
- <Button
160
- variant="outline"
161
- size="sm"
162
- onClick={arm}
163
- disabled={!apiKey || isRegenerating}
164
- >
165
- <RefreshCw className="mr-2 h-4 w-4" />
166
- {labels.regenerate}
167
- </Button>
168
-
169
- {/* Test button only when we have a fresh (full) key */}
170
- {isFresh && (
171
- <Button
172
- variant="secondary"
173
- size="sm"
174
- onClick={testKey}
175
- disabled={isTesting}
176
- >
177
- {isTesting
178
- ? <><Loader2 className="mr-2 h-4 w-4 animate-spin" />{labels.testing}</>
179
- : <><FlaskConical className="mr-2 h-4 w-4" />{labels.test}</>}
180
- </Button>
181
- )}
182
- </div>
183
- )}
184
- </CardContent>
185
- </Card>
186
- );
187
- }
188
-
189
- // ─────────────────────────────────────────────────────────────────────────────
190
- // Export (wraps with provider)
191
- // ─────────────────────────────────────────────────────────────────────────────
192
-
193
- export const ApiKeySection: React.FC = () => (
194
- <ApiKeyProvider>
195
- <ApiKeyCard />
196
- </ApiKeyProvider>
197
- );
@@ -1,44 +0,0 @@
1
- 'use client';
2
-
3
- import { Trash2 } from 'lucide-react';
4
- import React from 'react';
5
-
6
- import { useAuth, useDeleteAccount } from '@djangocfg/api/auth';
7
- import { useAppT } from '@djangocfg/i18n';
8
-
9
- import { ActionButton } from './ActionButton';
10
-
11
- export const DeleteAccountSection: React.FC = () => {
12
- const { logout } = useAuth();
13
- const { deleteAccount } = useDeleteAccount();
14
- const t = useAppT();
15
-
16
- const confirmationWord = t('layouts.profilePage.confirmationWord');
17
-
18
- const handleClick = async () => {
19
- const value = await window.dialog.prompt({
20
- title: t('layouts.profilePage.deleteAccountTitle'),
21
- message: t('layouts.profilePage.deleteAccountDesc'),
22
- placeholder: confirmationWord,
23
- confirmText: t('layouts.profilePage.deleteAccount'),
24
- cancelText: t('layouts.profilePage.cancel'),
25
- variant: 'destructive',
26
- });
27
-
28
- if (value?.toUpperCase() !== confirmationWord.toUpperCase()) return;
29
-
30
- const result = await deleteAccount();
31
- if (result.success) logout();
32
- };
33
-
34
- return (
35
- <ActionButton
36
- icon={<Trash2 className="w-4 h-4 text-destructive" />}
37
- label={<span className="text-destructive">{t('layouts.profilePage.deleteAccount')}</span>}
38
- onClick={handleClick}
39
- />
40
- );
41
- };
42
-
43
- // Keep export so nothing breaks — no longer used but exported for backwards compat
44
- export const DeleteAccountScreen: React.FC = () => null;
@@ -1,128 +0,0 @@
1
- 'use client';
2
-
3
- import React, { useEffect, useState } from 'react';
4
- import { parsePhoneNumberFromString } from 'libphonenumber-js';
5
-
6
- import { Button, Input, PhoneInput } from '@djangocfg/ui-core/components';
7
- import { toast } from '@djangocfg/ui-core/hooks';
8
- import { cn } from '@djangocfg/ui-core/lib';
9
-
10
- import { useProfileContext } from '../ProfileForm/context';
11
-
12
- function formatPhone(raw: string): string {
13
- if (!raw) return '';
14
- try {
15
- return parsePhoneNumberFromString(raw)?.formatInternational() ?? raw;
16
- } catch {
17
- return raw;
18
- }
19
- }
20
-
21
- interface EditableFieldProps {
22
- label: string;
23
- value: string;
24
- placeholder: string;
25
- onSave: (value: string) => Promise<void>;
26
- disabled?: boolean;
27
- type?: 'text' | 'phone';
28
- }
29
-
30
- export const EditableField = ({
31
- label,
32
- value,
33
- placeholder,
34
- onSave,
35
- disabled,
36
- type = 'text',
37
- }: EditableFieldProps) => {
38
- const { labels } = useProfileContext();
39
- const [isEditing, setIsEditing] = useState(false);
40
- const [editValue, setEditValue] = useState(value);
41
- const [isSaving, setIsSaving] = useState(false);
42
-
43
- useEffect(() => {
44
- setEditValue(value);
45
- }, [value]);
46
-
47
- const handleSave = async () => {
48
- if (editValue === value) {
49
- setIsEditing(false);
50
- return;
51
- }
52
- setIsSaving(true);
53
- try {
54
- await onSave(editValue);
55
- setIsEditing(false);
56
- } catch {
57
- // Keep editing mode open with the entered value so the user can retry.
58
- toast.error(labels.failedToUpdate);
59
- } finally {
60
- setIsSaving(false);
61
- }
62
- };
63
-
64
- const handleCancel = () => {
65
- setEditValue(value);
66
- setIsEditing(false);
67
- };
68
-
69
- const handleKeyDown = (e: React.KeyboardEvent) => {
70
- if (e.key === 'Enter') handleSave();
71
- else if (e.key === 'Escape') handleCancel();
72
- };
73
-
74
- if (isEditing) {
75
- return (
76
- <div className="py-4 border-b border-border/50 last:border-0">
77
- <label className="block text-[13px] text-muted-foreground mb-1.5">{label}</label>
78
- {type === 'phone' ? (
79
- <div className="space-y-2">
80
- <PhoneInput
81
- value={editValue}
82
- onChange={(val) => setEditValue(val ?? '')}
83
- placeholder={placeholder}
84
- disabled={isSaving}
85
- />
86
- <div className="flex gap-2">
87
- <Button size="sm" onClick={handleSave} disabled={isSaving}>
88
- {isSaving ? labels.saving : labels.save}
89
- </Button>
90
- <Button size="sm" variant="ghost" onClick={handleCancel} disabled={isSaving}>
91
- {labels.cancel}
92
- </Button>
93
- </div>
94
- </div>
95
- ) : (
96
- <Input
97
- value={editValue}
98
- onChange={(e) => setEditValue(e.target.value)}
99
- onKeyDown={handleKeyDown}
100
- onBlur={handleSave}
101
- placeholder={placeholder}
102
- autoFocus
103
- disabled={isSaving}
104
- className="h-9 text-[15px]"
105
- />
106
- )}
107
- </div>
108
- );
109
- }
110
-
111
- return (
112
- <button
113
- type="button"
114
- onClick={() => !disabled && setIsEditing(true)}
115
- disabled={disabled}
116
- className={cn(
117
- 'w-full py-4 border-b border-border/50 last:border-0 text-left',
118
- 'transition-colors hover:bg-muted/30',
119
- disabled && 'cursor-default hover:bg-transparent'
120
- )}
121
- >
122
- <div className="text-[13px] text-muted-foreground mb-0.5">{label}</div>
123
- <div className={cn('text-[15px]', value ? 'text-foreground' : 'text-muted-foreground/60')}>
124
- {value ? (type === 'phone' ? formatPhone(value) : value) : placeholder}
125
- </div>
126
- </button>
127
- );
128
- };
@@ -1,56 +0,0 @@
1
- /**
2
- * Preferences Section
3
- *
4
- * Language + theme controls for ProfileForm.
5
- * Uses ThemeToggle from ui-nextjs and LocaleSwitcherDropdown from _components.
6
- */
7
-
8
- 'use client';
9
-
10
- import React from 'react';
11
-
12
- import { ThemeToggle } from '@djangocfg/ui-core/theme';
13
- import { cn } from '@djangocfg/ui-core/lib';
14
-
15
- import { LocaleSwitcherDropdown } from '../../_components/locale-switcher';
16
- import { useLayoutI18nOptional } from '../../AppLayout/LayoutI18nProvider';
17
-
18
- interface PreferencesSectionProps {
19
- /** Extra className for the root. */
20
- className?: string;
21
- }
22
-
23
- export const PreferencesSection: React.FC<PreferencesSectionProps> = ({
24
- className,
25
- }) => {
26
- const layoutI18n = useLayoutI18nOptional();
27
-
28
- return (
29
- <div className={cn('py-0', className)}>
30
- {layoutI18n && (
31
- <>
32
- <div className="flex items-center justify-between py-3">
33
- <span className="text-sm">Language</span>
34
- <LocaleSwitcherDropdown
35
- locale={layoutI18n.locale}
36
- locales={layoutI18n.locales}
37
- onChange={layoutI18n.onLocaleChange}
38
- variant="outline"
39
- size="sm"
40
- showCode
41
- showIcon={false}
42
- showFlag
43
- showTriggerLabel
44
- />
45
- </div>
46
- <div className="h-px bg-border/60" />
47
- </>
48
- )}
49
-
50
- <div className="flex items-center justify-between py-3">
51
- <span className="text-sm">Theme</span>
52
- <ThemeToggle size="default" />
53
- </div>
54
- </div>
55
- );
56
- };