@djangocfg/layouts 2.1.357 → 2.1.358

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 (26) hide show
  1. package/package.json +21 -19
  2. package/src/configurator/private/schema.ts +12 -0
  3. package/src/layouts/PrivateLayout/PrivateLayout.tsx +13 -1
  4. package/src/layouts/PrivateLayout/components/PrivateSidebar.tsx +9 -15
  5. package/src/layouts/PrivateLayout/components/PrivateSidebarAccount.tsx +46 -90
  6. package/src/layouts/PrivateLayout/components/SidebarBrand.tsx +9 -6
  7. package/src/layouts/PrivateLayout/hooks/index.ts +1 -0
  8. package/src/layouts/PrivateLayout/hooks/useHoverExpand.ts +10 -3
  9. package/src/layouts/PrivateLayout/hooks/useShellVisualState.ts +1 -1
  10. package/src/layouts/PrivateLayout/hooks/useSidebarDefaultOpen.ts +21 -0
  11. package/src/layouts/PrivateLayout/types.ts +6 -0
  12. package/src/layouts/ProfileLayout/ProfileDialog/ProfileDialog.tsx +32 -0
  13. package/src/layouts/ProfileLayout/ProfileDialog/index.ts +2 -0
  14. package/src/layouts/ProfileLayout/ProfileDialog/store.ts +19 -0
  15. package/src/layouts/ProfileLayout/{context.tsx → ProfileForm/context.tsx} +4 -2
  16. package/src/layouts/ProfileLayout/{ProfileLayout.tsx → ProfileForm/index.tsx} +10 -7
  17. package/src/layouts/ProfileLayout/README.md +65 -5
  18. package/src/layouts/ProfileLayout/components/EditableField.tsx +1 -1
  19. package/src/layouts/ProfileLayout/components/PreferencesSection.tsx +56 -0
  20. package/src/layouts/ProfileLayout/components/ProfileHeader.tsx +1 -1
  21. package/src/layouts/ProfileLayout/components/ProfileTab.tsx +17 -11
  22. package/src/layouts/ProfileLayout/components/index.ts +1 -0
  23. package/src/layouts/ProfileLayout/hooks/useProfileTabs.ts +17 -12
  24. package/src/layouts/ProfileLayout/index.ts +5 -4
  25. package/src/layouts/ProfileLayout/types.ts +11 -1
  26. package/src/layouts/_components/index.ts +1 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@djangocfg/layouts",
3
- "version": "2.1.357",
3
+ "version": "2.1.358",
4
4
  "description": "Simple, straightforward layout components for Next.js - import and use with props",
5
5
  "keywords": [
6
6
  "layouts",
@@ -84,13 +84,13 @@
84
84
  "check": "tsc --noEmit"
85
85
  },
86
86
  "peerDependencies": {
87
- "@djangocfg/api": "^2.1.357",
88
- "@djangocfg/centrifugo": "^2.1.357",
89
- "@djangocfg/debuger": "^2.1.357",
90
- "@djangocfg/i18n": "^2.1.357",
91
- "@djangocfg/monitor": "^2.1.357",
92
- "@djangocfg/ui-core": "^2.1.357",
93
- "@djangocfg/ui-nextjs": "^2.1.357",
87
+ "@djangocfg/api": "^2.1.358",
88
+ "@djangocfg/centrifugo": "^2.1.358",
89
+ "@djangocfg/debuger": "^2.1.358",
90
+ "@djangocfg/i18n": "^2.1.358",
91
+ "@djangocfg/monitor": "^2.1.358",
92
+ "@djangocfg/ui-core": "^2.1.358",
93
+ "@djangocfg/ui-nextjs": "^2.1.358",
94
94
  "@hookform/resolvers": "^5.2.2",
95
95
  "consola": "^3.4.2",
96
96
  "lucide-react": "^0.545.0",
@@ -105,7 +105,8 @@
105
105
  "swr": "^2.3.7",
106
106
  "tailwindcss": "^4.1.18",
107
107
  "tailwindcss-animate": "^1.0.7",
108
- "zod": "^4.3.6"
108
+ "zod": "^4.3.6",
109
+ "zustand": "^5.0.0"
109
110
  },
110
111
  "peerDependenciesMeta": {
111
112
  "@djangocfg/monitor": {
@@ -120,21 +121,22 @@
120
121
  "uuid": "^11.1.0"
121
122
  },
122
123
  "devDependencies": {
123
- "@djangocfg/api": "^2.1.357",
124
- "@djangocfg/centrifugo": "^2.1.357",
125
- "@djangocfg/debuger": "^2.1.357",
126
- "@djangocfg/i18n": "^2.1.357",
127
- "@djangocfg/monitor": "^2.1.357",
128
- "@djangocfg/typescript-config": "^2.1.357",
129
- "@djangocfg/ui-core": "^2.1.357",
130
- "@djangocfg/ui-nextjs": "^2.1.357",
131
- "@djangocfg/ui-tools": "^2.1.357",
124
+ "@djangocfg/api": "^2.1.358",
125
+ "@djangocfg/centrifugo": "^2.1.358",
126
+ "@djangocfg/debuger": "^2.1.358",
127
+ "@djangocfg/i18n": "^2.1.358",
128
+ "@djangocfg/monitor": "^2.1.358",
129
+ "@djangocfg/typescript-config": "^2.1.358",
130
+ "@djangocfg/ui-core": "^2.1.358",
131
+ "@djangocfg/ui-nextjs": "^2.1.358",
132
+ "@djangocfg/ui-tools": "^2.1.358",
132
133
  "@types/node": "^24.7.2",
133
134
  "@types/react": "^19.1.0",
134
135
  "@types/react-dom": "^19.1.0",
135
136
  "eslint": "^9.37.0",
136
137
  "next-intl": "^4.9.1",
137
- "typescript": "^5.9.3"
138
+ "typescript": "^5.9.3",
139
+ "zustand": "^5.0.4"
138
140
  },
139
141
  "publishConfig": {
140
142
  "access": "public"
@@ -37,6 +37,7 @@ export interface PrivateLayoutConfiguratorData {
37
37
  header: {
38
38
  userPlan: string;
39
39
  showSecondaryAction: boolean;
40
+ accountAction: 'menu' | 'dialog';
40
41
  };
41
42
  }
42
43
 
@@ -126,6 +127,12 @@ export const privateLayoutConfiguratorSchema: CustomJsonSchema7 = {
126
127
  title: 'Footer secondary action',
127
128
  description: 'Adds a download-style icon button inside the footer trigger with a pulsing accent dot.',
128
129
  },
130
+ accountAction: {
131
+ type: 'string',
132
+ title: 'Account action',
133
+ enum: ['menu', 'dialog'],
134
+ description: "'menu' opens a dropdown; 'dialog' opens the global ProfileDialog.",
135
+ },
129
136
  },
130
137
  },
131
138
  },
@@ -165,6 +172,10 @@ export const privateLayoutConfiguratorUiSchema: CustomJsonUiSchema7 = {
165
172
  header: {
166
173
  'ui:collapsible': true,
167
174
  showSecondaryAction: { 'ui:widget': 'switch' },
175
+ accountAction: {
176
+ 'ui:widget': 'radio',
177
+ 'ui:options': { inline: true },
178
+ },
168
179
  },
169
180
  };
170
181
 
@@ -186,5 +197,6 @@ export const defaultPrivateLayoutConfiguratorData: PrivateLayoutConfiguratorData
186
197
  header: {
187
198
  userPlan: 'Pro plan',
188
199
  showSecondaryAction: false,
200
+ accountAction: 'menu',
189
201
  },
190
202
  };
@@ -9,6 +9,7 @@
9
9
  'use client';
10
10
 
11
11
  import React, { ReactNode } from 'react';
12
+ import dynamic from 'next/dynamic';
12
13
 
13
14
  import { Preloader } from '@djangocfg/ui-core/components';
14
15
  import { SidebarInset, SidebarProvider } from '@djangocfg/ui-core/components';
@@ -18,6 +19,7 @@ import type { LayoutVisualConfig } from '../types';
18
19
  import { PrivateContent, PrivateSidebar } from './components';
19
20
  import { useAuthGuard } from './hooks';
20
21
  import { useLayoutVisual } from './hooks';
22
+ import { useSidebarDefaultOpen } from './hooks';
21
23
 
22
24
  import type {
23
25
  HeaderConfig,
@@ -37,6 +39,12 @@ export type {
37
39
 
38
40
  export { PrivateLayoutProps };
39
41
 
42
+ // Lazy-load ProfileDialog so the profile bundle is only fetched when opened.
43
+ const ProfileDialog = dynamic(
44
+ () => import('../ProfileLayout/ProfileDialog/ProfileDialog').then((m) => m.ProfileDialog),
45
+ { ssr: false },
46
+ );
47
+
40
48
  export function PrivateLayout({
41
49
  children,
42
50
  sidebar,
@@ -55,6 +63,8 @@ export function PrivateLayout({
55
63
  const { providerStyle, providerClassName, insetClassName, sidebarVariant } =
56
64
  useLayoutVisual(visual);
57
65
 
66
+ const defaultOpen = useSidebarDefaultOpen();
67
+
58
68
  if (isLoading) {
59
69
  return (
60
70
  <Preloader
@@ -69,7 +79,7 @@ export function PrivateLayout({
69
79
 
70
80
  return (
71
81
  <SidebarProvider
72
- defaultOpen={true}
82
+ defaultOpen={defaultOpen}
73
83
  style={providerStyle}
74
84
  className={providerClassName}
75
85
  >
@@ -92,6 +102,8 @@ export function PrivateLayout({
92
102
  {children}
93
103
  </PrivateContent>
94
104
  </SidebarInset>
105
+
106
+ <ProfileDialog />
95
107
  </SidebarProvider>
96
108
  );
97
109
  }
@@ -51,7 +51,7 @@ export function PrivateSidebar({
51
51
  }, [pathname, isMobile, setOpenMobile]);
52
52
 
53
53
  const collapsedRail = !isMobile && state === 'collapsed';
54
- const { isHoverExpanded, onMouseEnter, onMouseLeave } = useHoverExpand({
54
+ const { isHoverExpanded, onMouseEnter, onMouseLeave, setHoverExpanded } = useHoverExpand({
55
55
  enabled: collapsedRail,
56
56
  });
57
57
 
@@ -69,7 +69,7 @@ export function PrivateSidebar({
69
69
  header={header}
70
70
  variant={variant}
71
71
  collapsedRail={collapsedRail}
72
- setOpen={setOpen}
72
+ setHoverExpanded={setHoverExpanded}
73
73
  onMouseEnter={onMouseEnter}
74
74
  onMouseLeave={onMouseLeave}
75
75
  />
@@ -87,7 +87,7 @@ interface PrivateSidebarInnerProps {
87
87
  header?: HeaderConfig;
88
88
  variant: 'sidebar' | 'inset';
89
89
  collapsedRail: boolean;
90
- setOpen: (open: boolean) => void;
90
+ setHoverExpanded: (value: boolean) => void;
91
91
  onMouseEnter: () => void;
92
92
  onMouseLeave: () => void;
93
93
  }
@@ -97,7 +97,7 @@ function PrivateSidebarInner({
97
97
  header,
98
98
  variant,
99
99
  collapsedRail,
100
- setOpen,
100
+ setHoverExpanded,
101
101
  onMouseEnter,
102
102
  onMouseLeave,
103
103
  }: PrivateSidebarInnerProps) {
@@ -105,26 +105,20 @@ function PrivateSidebarInner({
105
105
  const { modifiers } = useShellVisualState(layoutVariant);
106
106
  const { setSidebarRef, handleSidebarKeyDown } = useSidebarKeyboard();
107
107
 
108
- /**
109
- * Click on the collapsed icon-rail expands the sidebar — but only on empty
110
- * areas. Native interactive elements (nav links, the trigger, account menu,
111
- * tooltips) keep their original behaviour: we bail out as soon as the click
112
- * target sits inside a `button`, `a`, or anything explicitly marked
113
- * non-expandable via `data-no-expand`.
114
- */
108
+ const railExpandHintClass = collapsedRail ? 'cursor-pointer' : undefined;
109
+
110
+ /** Click on the collapsed rail acts like a hover — temporary expand, not persistent. */
115
111
  const expandOnRailClick = React.useCallback(
116
112
  (event: React.MouseEvent<HTMLDivElement>) => {
117
113
  const interactive = (event.target as Element | null)?.closest(
118
114
  'a, button, [role="menuitem"], [data-no-expand]',
119
115
  );
120
116
  if (interactive) return;
121
- setOpen(true);
117
+ setHoverExpanded(true);
122
118
  },
123
- [setOpen],
119
+ [setHoverExpanded],
124
120
  );
125
121
 
126
- const railExpandHintClass = collapsedRail ? 'cursor-pointer' : undefined;
127
-
128
122
  const sidebarRootClass = React.useMemo(
129
123
  () =>
130
124
  cn(
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * Sidebar account: rich footer trigger (avatar + name + plan + optional secondary
3
- * action) opens a popover (DropdownMenu) upward with email, account links, locale +
4
- * theme controls, and sign-out. Replaces the legacy inline collapsible.
3
+ * action) opens a popover (DropdownMenu) upward with email, account links, and
4
+ * sign-out. Language and theme controls live inside ProfileLayout / PreferencesSection
5
+ * so they are not duplicated here.
5
6
  *
6
7
  * Reads `isAccountMenuOpen` / `setIsAccountMenuOpen` directly from
7
8
  * PrivateLayoutContext so the parent sidebar can block collapse while the menu
@@ -10,7 +11,7 @@
10
11
 
11
12
  'use client';
12
13
 
13
- import { ChevronRight, ChevronsUpDown, Globe, LogOut, Monitor, Moon, Sun } from 'lucide-react';
14
+ import { ChevronsUpDown, LogOut } from 'lucide-react';
14
15
  import { Link } from '@djangocfg/ui-core/components';
15
16
  import React, { memo, useMemo } from 'react';
16
17
 
@@ -30,15 +31,13 @@ import {
30
31
  } from '@djangocfg/ui-core/components';
31
32
  import { cn, isDev } from '@djangocfg/ui-core/lib';
32
33
  import { useSidebar } from '@djangocfg/ui-core/components';
33
- import { useThemeContext } from '@djangocfg/ui-nextjs/theme';
34
34
 
35
35
  import { useLogout } from '../../../hooks';
36
- import { LocaleSwitcherDialog } from '../../_components/locale-switcher';
37
- import { getLocaleMeta } from '../../_components/locale-switcher/localeMeta';
38
36
  import { useLayoutI18nOptional } from '../../AppLayout/LayoutI18nProvider';
39
37
  import { LucideIcon as LucideIconRender } from '../../../components';
40
38
  import { useShellVisualState } from '../hooks';
41
39
  import { blockSidebarCollapse, allowSidebarCollapse } from '../hooks/useHoverExpand';
40
+ import { useProfileDialogStore } from '../../ProfileLayout/ProfileDialog/store';
42
41
 
43
42
  import type { HeaderConfig } from '../types';
44
43
 
@@ -62,8 +61,6 @@ function PrivateSidebarAccountRaw({ header }: PrivateSidebarAccountProps) {
62
61
  const { setOpen: setSidebarOpen } = useSidebar();
63
62
  const { content } = useShellVisualState();
64
63
  const [isAccountMenuOpen, setIsAccountMenuOpen] = React.useState(false);
65
- const { theme, setTheme } = useThemeContext();
66
- const [langDialogOpen, setLangDialogOpen] = React.useState(false);
67
64
 
68
65
  const signOutLabel = t('layouts.profile.signOut');
69
66
 
@@ -110,21 +107,6 @@ function PrivateSidebarAccountRaw({ header }: PrivateSidebarAccountProps) {
110
107
  handleLogout();
111
108
  }, [handleLogout]);
112
109
 
113
- const onLanguageSelect = React.useCallback((e: Event) => {
114
- // Keep the dropdown closed (default behaviour) but defer dialog mount to
115
- // the next tick so Radix has time to unmount the dropdown overlay first
116
- // (avoids the "two open overlays steal focus" bug).
117
- e.preventDefault();
118
- setTimeout(() => setLangDialogOpen(true), 0);
119
- }, []);
120
-
121
- const onThemeSelect = React.useCallback((e: Event) => {
122
- e.preventDefault();
123
- // Cycle: light → dark → system → light
124
- const next = theme === 'light' ? 'dark' : theme === 'dark' ? 'system' : 'light';
125
- setTheme(next);
126
- }, [theme, setTheme]);
127
-
128
110
  // Hide entirely in production when there's no user (auth still loading or
129
111
  // /me failed and the parent guard hasn't redirected yet). In dev keep a
130
112
  // placeholder so the footer + Log out are reachable for debugging.
@@ -135,7 +117,7 @@ function PrivateSidebarAccountRaw({ header }: PrivateSidebarAccountProps) {
135
117
  const secondary = header?.footerSecondaryAction;
136
118
 
137
119
  const triggerClassName = cn(
138
- 'group h-auto w-full gap-3 rounded-none px-3 py-3 text-left',
120
+ 'group h-auto w-full gap-3 rounded-lg px-3 py-3 text-left',
139
121
  'hover:bg-sidebar-accent/70 hover:text-sidebar-accent-foreground',
140
122
  content.isAccountCompact ? 'justify-center px-0 py-2' : 'min-h-[52px]',
141
123
  );
@@ -182,40 +164,6 @@ function PrivateSidebarAccountRaw({ header }: PrivateSidebarAccountProps) {
182
164
  </>
183
165
  ) : null;
184
166
 
185
- const currentLocaleLabel = layoutI18n
186
- ? getLocaleMeta(layoutI18n.locale).native
187
- : null;
188
- const languageItem = layoutI18n ? (
189
- <DropdownMenuItem
190
- onSelect={onLanguageSelect}
191
- className="flex items-center gap-2 rounded-md px-2 py-1.5 text-sm"
192
- >
193
- <Globe className="h-4 w-4 shrink-0 text-muted-foreground" />
194
- <span className="flex-1 truncate">Language</span>
195
- <span className="ml-auto flex shrink-0 items-center gap-1 text-xs text-muted-foreground">
196
- {currentLocaleLabel}
197
- <ChevronRight className="h-3.5 w-3.5" aria-hidden />
198
- </span>
199
- </DropdownMenuItem>
200
- ) : null;
201
-
202
- const themeIcon = theme === 'dark'
203
- ? <Moon className="h-4 w-4 shrink-0 text-muted-foreground" />
204
- : theme === 'light'
205
- ? <Sun className="h-4 w-4 shrink-0 text-muted-foreground" />
206
- : <Monitor className="h-4 w-4 shrink-0 text-muted-foreground" />;
207
- const themeValueLabel = theme === 'dark' ? 'Dark' : theme === 'light' ? 'Light' : 'System';
208
- const themeItem = (
209
- <DropdownMenuItem
210
- onSelect={onThemeSelect}
211
- className="flex items-center gap-2 rounded-md px-2 py-1.5 text-sm"
212
- >
213
- {themeIcon}
214
- <span className="flex-1 truncate">Theme</span>
215
- <span className="ml-auto shrink-0 text-xs text-muted-foreground">{themeValueLabel}</span>
216
- </DropdownMenuItem>
217
- );
218
-
219
167
  const expandedMeta = content.isAccountCompact ? null : (
220
168
  <>
221
169
  <span className="flex min-w-0 flex-1 flex-col text-left">
@@ -230,13 +178,49 @@ function PrivateSidebarAccountRaw({ header }: PrivateSidebarAccountProps) {
230
178
  </span>
231
179
  <span className="flex shrink-0 items-center gap-1.5">
232
180
  {secondaryButton}
233
- <ChevronsUpDown className="h-3.5 w-3.5 text-sidebar-foreground/55" aria-hidden />
181
+ {header?.accountAction !== 'dialog' ? (
182
+ <ChevronsUpDown className="h-3.5 w-3.5 text-sidebar-foreground/55" aria-hidden />
183
+ ) : null}
234
184
  </span>
235
185
  </>
236
186
  );
237
187
 
188
+ const openProfileDialog = React.useCallback(() => {
189
+ useProfileDialogStore.getState().open();
190
+ }, []);
191
+
192
+ const triggerButton = (
193
+ <Button
194
+ type="button"
195
+ variant="ghost"
196
+ aria-label={content.isAccountCompact ? account.displayName : undefined}
197
+ className={triggerClassName}
198
+ onClick={header?.accountAction === 'dialog' ? openProfileDialog : onTriggerInteract}
199
+ >
200
+ <Avatar className={avatarClass}>
201
+ <AvatarImage src={account.avatarUrl} alt={account.displayName} />
202
+ <AvatarFallback className="text-sm font-semibold">{userInitial}</AvatarFallback>
203
+ </Avatar>
204
+ {expandedMeta}
205
+ </Button>
206
+ );
207
+
208
+ const wrapperClass = cn(
209
+ 'w-full min-w-0',
210
+ content.isAccountCompact ? 'px-0 pb-0' : 'px-2 pb-2',
211
+ );
212
+
213
+ // Dialog mode: simple button that opens the global ProfileDialog
214
+ if (header?.accountAction === 'dialog') {
215
+ return (
216
+ <div className={wrapperClass}>
217
+ {triggerButton}
218
+ </div>
219
+ );
220
+ }
221
+
238
222
  return (
239
- <div className="w-full min-w-0 border-t border-sidebar-border/40">
223
+ <div className={wrapperClass}>
240
224
  <DropdownMenu
241
225
  open={isAccountMenuOpen}
242
226
  onOpenChange={(open) => {
@@ -246,19 +230,7 @@ function PrivateSidebarAccountRaw({ header }: PrivateSidebarAccountProps) {
246
230
  }}
247
231
  >
248
232
  <DropdownMenuTrigger asChild>
249
- <Button
250
- type="button"
251
- variant="ghost"
252
- aria-label={content.isAccountCompact ? account.displayName : undefined}
253
- className={triggerClassName}
254
- onClick={onTriggerInteract}
255
- >
256
- <Avatar className={avatarClass}>
257
- <AvatarImage src={account.avatarUrl} alt={account.displayName} />
258
- <AvatarFallback className="text-sm font-semibold">{userInitial}</AvatarFallback>
259
- </Avatar>
260
- {expandedMeta}
261
- </Button>
233
+ {triggerButton}
262
234
  </DropdownMenuTrigger>
263
235
 
264
236
  <DropdownMenuContent
@@ -270,10 +242,6 @@ function PrivateSidebarAccountRaw({ header }: PrivateSidebarAccountProps) {
270
242
  {headerLabel}
271
243
  {accountLinksBlock}
272
244
 
273
- <DropdownMenuSeparator />
274
- {languageItem}
275
- {themeItem}
276
-
277
245
  <DropdownMenuSeparator />
278
246
  <DropdownMenuItem
279
247
  onSelect={onLogoutSelect}
@@ -284,25 +252,13 @@ function PrivateSidebarAccountRaw({ header }: PrivateSidebarAccountProps) {
284
252
  </DropdownMenuItem>
285
253
  </DropdownMenuContent>
286
254
  </DropdownMenu>
287
-
288
- {layoutI18n ? (
289
- <LocaleSwitcherDialog
290
- open={langDialogOpen}
291
- onOpenChange={setLangDialogOpen}
292
- locale={layoutI18n.locale}
293
- locales={layoutI18n.locales}
294
- onChange={layoutI18n.onLocaleChange}
295
- brand={layoutI18n.brand}
296
- i18nLabels={layoutI18n.dialogLabels}
297
- />
298
- ) : null}
299
255
  </div>
300
256
  );
301
257
  }
302
258
 
303
259
  /**
304
260
  * Memoised account footer. Re-renders only when the `header` prop reference
305
- * changes. Internal reactive data (user from useAuth, theme, locale) are
261
+ * changes. Internal reactive data (user from useAuth, locale) are
306
262
  * consumed via hooks and still update independently.
307
263
  */
308
264
  export const PrivateSidebarAccount = memo(PrivateSidebarAccountRaw);
@@ -17,7 +17,7 @@ import { usePrivateLayoutContext } from '../context';
17
17
  import { useShellVisualState } from '../hooks';
18
18
 
19
19
  function SidebarBrandRaw() {
20
- const { header, homeHref, brandTitle, brandMonogram, isMobile } =
20
+ const { header, homeHref, brandTitle, brandMonogram, isMobile, isHoverExpanded } =
21
21
  usePrivateLayoutContext();
22
22
  const { content } = useShellVisualState();
23
23
 
@@ -41,8 +41,8 @@ function SidebarBrandRaw() {
41
41
  const headerRowClass = useMemo(
42
42
  () =>
43
43
  cn(
44
- 'flex items-center gap-2',
45
- content.showLabels ? 'px-2' : 'px-1.5',
44
+ 'flex items-center gap-2 mb-2',
45
+ // content.showLabels ? 'px-2' : 'px-1.5',
46
46
  ),
47
47
  [content.showLabels],
48
48
  );
@@ -147,10 +147,13 @@ function SidebarBrandRaw() {
147
147
  cn(
148
148
  'pb-2',
149
149
  isMobile
150
- ? 'px-4 pb-3 pt-[max(1.25rem,env(safe-area-inset-top,0px))]'
151
- : 'px-2 pt-3.5',
150
+ ? 'pb-3 pt-[max(1.25rem,env(safe-area-inset-top,0px))]'
151
+ : 'pt-3.5',
152
+ // Hover-expanded overlay: SidebarHeader from ui-core forces paddingLeft/Right to 0
153
+ // when state is collapsed. Override it so content has breathing room.
154
+ !isMobile && isHoverExpanded && '!px-2',
152
155
  ),
153
- [isMobile],
156
+ [isMobile, isHoverExpanded],
154
157
  );
155
158
 
156
159
  return <SidebarHeader className={sidebarHeaderClass}>{sidebarHeaderContent}</SidebarHeader>;
@@ -11,3 +11,4 @@ export {
11
11
  } from './useHoverExpand';
12
12
  export { useShellVisualState } from './useShellVisualState';
13
13
  export { useSidebarKeyboard } from './useSidebarKeyboard';
14
+ export { useSidebarDefaultOpen } from './useSidebarDefaultOpen';
@@ -30,11 +30,13 @@ interface UseHoverExpandResult {
30
30
  onMouseEnter: () => void;
31
31
  /** Attach to the sidebar root element */
32
32
  onMouseLeave: () => void;
33
+ /** Programmatically set hover-expanded state (for click-to-expand) */
34
+ setHoverExpanded: (value: boolean) => void;
33
35
  }
34
36
 
35
37
  export function useHoverExpand({
36
- enterDelay = 250,
37
- leaveDelay = 150,
38
+ enterDelay = 2000,
39
+ leaveDelay = 450,
38
40
  enabled = true,
39
41
  }: UseHoverExpandOptions = {}): UseHoverExpandResult {
40
42
  const [isHoverExpanded, setIsHoverExpanded] = useState(false);
@@ -89,7 +91,12 @@ export function useHoverExpand({
89
91
  return () => clearTimers();
90
92
  }, [clearTimers]);
91
93
 
92
- return { isHoverExpanded, onMouseEnter, onMouseLeave };
94
+ const setHoverExpanded = useCallback((value: boolean) => {
95
+ clearTimers();
96
+ setIsHoverExpanded(value);
97
+ }, [clearTimers]);
98
+
99
+ return { isHoverExpanded, onMouseEnter, onMouseLeave, setHoverExpanded };
93
100
  }
94
101
 
95
102
  /** Dispatch from any descendant to block sidebar collapse while e.g. a dropdown is open. */
@@ -142,7 +142,7 @@ export function useShellVisualState(
142
142
  const sidebarContent: string[] = [];
143
143
 
144
144
  if (isHoverOverlay) {
145
- sidebarRoot.push('z-50', '!w-[var(--sidebar-width)]');
145
+ sidebarRoot.push('z-50', '!w-[var(--sidebar-width)]', 'min-w-[var(--sidebar-width)]');
146
146
  // Allow scroll inside hover-expanded overlay — shadcn hardcodes
147
147
  // overflow-hidden on div[data-sidebar=sidebar]; override it here.
148
148
  sidebarInner.push('!overflow-auto');
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Reads the sidebar open/closed state from the cookie written by shadcn-sidebar.
3
+ * Falls back to `true` (expanded) when no cookie exists.
4
+ *
5
+ * Must run on the client — returns `true` during SSR.
6
+ */
7
+
8
+ const SIDEBAR_COOKIE_NAME = 'sidebar_state';
9
+
10
+ export function useSidebarDefaultOpen(): boolean {
11
+ if (typeof document === 'undefined') return true;
12
+
13
+ const match = document.cookie
14
+ .split('; ')
15
+ .find((row) => row.startsWith(`${SIDEBAR_COOKIE_NAME}=`));
16
+
17
+ if (!match) return true;
18
+
19
+ const value = match.split('=')[1];
20
+ return value === 'true';
21
+ }
@@ -132,6 +132,12 @@ export interface HeaderConfig {
132
132
  brandLetter?: string;
133
133
  /** User menu groups (account panel in the sidebar footer) */
134
134
  groups?: UserMenuConfig['groups'];
135
+ /**
136
+ * Behaviour of the footer account button.
137
+ * - `'menu'` (default) — opens a DropdownMenu with account links, locale/theme controls, and sign-out.
138
+ * - `'dialog'` — opens the global ProfileDialog (managed via Zustand store).
139
+ */
140
+ accountAction?: 'menu' | 'dialog';
135
141
  /** Auth page path (for sign in button) */
136
142
  authPath?: string;
137
143
  /** Subtitle under the display name in the sidebar footer (e.g. "Max plan"). */
@@ -0,0 +1,32 @@
1
+ 'use client';
2
+
3
+ import React from 'react';
4
+
5
+ import {
6
+ Dialog,
7
+ DialogContent,
8
+ DialogHeader,
9
+ DialogTitle,
10
+ } from '@djangocfg/ui-core/components';
11
+
12
+ import { ProfileForm } from '../ProfileForm';
13
+ import { useProfileDialogStore } from './store';
14
+
15
+ export interface ProfileDialogProps {
16
+ title?: string;
17
+ }
18
+
19
+ export const ProfileDialog: React.FC<ProfileDialogProps> = ({ title }) => {
20
+ const { isOpen, close, initialTab } = useProfileDialogStore();
21
+
22
+ return (
23
+ <Dialog open={isOpen} onOpenChange={(open) => !open && close()}>
24
+ <DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto p-0">
25
+ <DialogHeader className="sr-only">
26
+ <DialogTitle>Profile</DialogTitle>
27
+ </DialogHeader>
28
+ <ProfileForm title={title} defaultTab={initialTab} />
29
+ </DialogContent>
30
+ </Dialog>
31
+ );
32
+ };
@@ -0,0 +1,2 @@
1
+ export { ProfileDialog } from './ProfileDialog';
2
+ export { useProfileDialogStore } from './store';
@@ -0,0 +1,19 @@
1
+ import { create } from 'zustand';
2
+
3
+ import type { ProfileTabValue } from '../hooks/useProfileTabs';
4
+
5
+ interface ProfileDialogState {
6
+ isOpen: boolean;
7
+ initialTab: ProfileTabValue | undefined;
8
+ open: (options?: { initialTab?: ProfileTabValue }) => void;
9
+ close: () => void;
10
+ toggle: () => void;
11
+ }
12
+
13
+ export const useProfileDialogStore = create<ProfileDialogState>((set) => ({
14
+ isOpen: false,
15
+ initialTab: undefined,
16
+ open: (options) => set({ isOpen: true, initialTab: options?.initialTab }),
17
+ close: () => set({ isOpen: false, initialTab: undefined }),
18
+ toggle: () => set((state) => ({ isOpen: !state.isOpen })),
19
+ }));
@@ -6,8 +6,8 @@ import { useAuth } from '@djangocfg/api/auth';
6
6
  import { useAppT } from '@djangocfg/i18n';
7
7
  import { toast } from '@djangocfg/ui-core/hooks';
8
8
 
9
- import { profileLogger } from '../../utils/logger';
10
- import { useLogout } from '../../hooks';
9
+ import { profileLogger } from '../../../utils/logger';
10
+ import { useLogout } from '../../../hooks';
11
11
 
12
12
  // ─────────────────────────────────────────────────────────────────────────────
13
13
  // Types
@@ -19,6 +19,7 @@ export interface ProfileLabels {
19
19
  work: string;
20
20
  security: string;
21
21
  apiKeys: string;
22
+ preferences: string;
22
23
  firstName: string;
23
24
  lastName: string;
24
25
  phone: string;
@@ -79,6 +80,7 @@ export const ProfileProvider: React.FC<ProfileProviderProps> = ({ children, titl
79
80
  work: t('layouts.profilePage.work'),
80
81
  security: t('layouts.profilePage.security'),
81
82
  apiKeys: 'API Keys',
83
+ preferences: 'Preferences',
82
84
  firstName: t('layouts.profilePage.firstName'),
83
85
  lastName: t('layouts.profilePage.lastName'),
84
86
  phone: t('layouts.profilePage.phone'),
@@ -12,11 +12,11 @@ import {
12
12
  TabsTrigger,
13
13
  } from '@djangocfg/ui-core/components';
14
14
 
15
- import { ApiKeySection, ProfileHeader, ProfileTab, TwoFactorSection } from './components';
15
+ import { ApiKeySection, ProfileHeader, ProfileTab, TwoFactorSection } from '../components';
16
16
  import { ProfileProvider, useProfileContext } from './context';
17
- import { useProfileTabs } from './hooks/useProfileTabs';
18
- import type { ProfileLayoutProps } from './types';
19
- import type { ProfileTabValue } from './hooks/useProfileTabs';
17
+ import { useProfileTabs } from '../hooks/useProfileTabs';
18
+ import type { ProfileFormProps } from '../types';
19
+ import type { ProfileTabValue } from '../hooks/useProfileTabs';
20
20
 
21
21
  // ─────────────────────────────────────────────────────────────────────────────
22
22
  // Built-in tab panels
@@ -49,15 +49,18 @@ function ProfileContent({
49
49
  enableDeleteAccount = true,
50
50
  tabs = [],
51
51
  slots,
52
- }: ProfileLayoutProps) {
52
+ defaultTab,
53
+ }: ProfileFormProps) {
53
54
  const { labels } = useProfileContext();
54
55
  const { user, isLoading } = useAuth();
55
56
 
56
57
  const extraTabValues = React.useMemo(() => tabs.map((t) => t.value), [tabs]);
57
- const { tab, setTab } = useProfileTabs({
58
+
59
+ const { tab, setTab, allowed } = useProfileTabs({
58
60
  enable2FA,
59
61
  enableAPIKeys,
60
62
  extraTabValues,
63
+ defaultTab,
61
64
  });
62
65
 
63
66
  const handleTabChange = React.useCallback(
@@ -136,7 +139,7 @@ function ProfileContent({
136
139
  // Export
137
140
  // ─────────────────────────────────────────────────────────────────────────────
138
141
 
139
- export const ProfileLayout: React.FC<ProfileLayoutProps> = ({ title, ...props }) => (
142
+ export const ProfileForm: React.FC<ProfileFormProps> = ({ title, ...props }) => (
140
143
  <ProfileProvider title={title}>
141
144
  <ProfileContent title={title} {...props} />
142
145
  </ProfileProvider>
@@ -4,10 +4,12 @@ User profile page with tabbed interface: **Profile** | **Security** | **API Keys
4
4
 
5
5
  ## Usage
6
6
 
7
+ ### Standalone page
8
+
7
9
  ```tsx
8
- import { ProfileLayout } from '@djangocfg/layouts';
10
+ import { ProfileForm } from '@djangocfg/layouts';
9
11
 
10
- <ProfileLayout
12
+ <ProfileForm
11
13
  enable2FA
12
14
  enableAPIKeys
13
15
  enableDeleteAccount
@@ -19,6 +21,18 @@ import { ProfileLayout } from '@djangocfg/layouts';
19
21
  />
20
22
  ```
21
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
+
32
+ // In your layout (lazy-loaded)
33
+ <ProfileDialog />
34
+ ```
35
+
22
36
  ## Props
23
37
 
24
38
  | Prop | Default | Description |
@@ -30,14 +44,60 @@ import { ProfileLayout } from '@djangocfg/layouts';
30
44
  | `tabs` | `[]` | Extra `ProfileTab[]` appended after built-in tabs |
31
45
  | `slots` | — | Named slots: `headerMenuItems`, `headerBadge`, `headerAfter`, `footer` |
32
46
  | `title` | i18n | Page title |
47
+ | `defaultTab` | — | Initial active tab. When provided (e.g. by `ProfileDialog`), tabs start at this value. |
48
+
49
+ ## Global Profile Dialog
50
+
51
+ `ProfileDialog` is a Zustand-driven dialog that can be opened from anywhere:
52
+
53
+ ```tsx
54
+ import { useProfileDialogStore } from '@djangocfg/layouts';
55
+
56
+ const { open, close } = useProfileDialogStore();
57
+
58
+ // Open on Profile tab
59
+ open();
60
+
61
+ // Open on Security tab
62
+ open({ initialTab: 'security' });
63
+ ```
64
+
65
+ Wire it into `PrivateLayout` for global access:
66
+
67
+ ```tsx
68
+ import { PrivateLayout } from '@djangocfg/layouts';
69
+
70
+ <PrivateLayout sidebar={...} header={...}>
71
+ {children}
72
+ </PrivateLayout>
73
+ // ProfileDialog is rendered automatically inside PrivateLayout
74
+ ```
75
+
76
+ ## Sidebar Account Button Modes
77
+
78
+ The footer account button in `PrivateLayout` supports two modes via `HeaderConfig`:
79
+
80
+ ```tsx
81
+ const header: HeaderConfig = {
82
+ // ...
83
+ accountAction: 'menu', // Default — opens DropdownMenu with links, theme, logout
84
+ accountAction: 'dialog', // Opens ProfileDialog instead
85
+ };
86
+ ```
33
87
 
34
88
  ## Architecture
35
89
 
36
90
  ```
37
91
  ProfileLayout/
38
- ├── ProfileLayout.tsx Shell: ProfileProvider → header + Tabs
39
- ├── context.tsx Root context (labels, onLogout, onFieldSave)
40
- ├── types.ts ProfileLayoutProps, ProfileTab, ProfileSlots
92
+ ├── ProfileForm/
93
+ ├── index.tsx Shell: ProfileProvider header + Tabs
94
+ │ └── context.tsx Root context (labels, onLogout, onFieldSave)
95
+ ├── ProfileDialog/
96
+ │ ├── ProfileDialog.tsx Dialog wrapper around ProfileForm
97
+ │ └── store.ts Zustand store (isOpen, initialTab, open/close)
98
+ ├── hooks/
99
+ │ └── useProfileTabs.ts Local tab state (useState)
100
+ ├── types.ts ProfileFormProps, ProfileTab, ProfileSlots
41
101
  └── components/
42
102
  ├── ProfileHeader Avatar + name + dropdown menu
43
103
  ├── ProfileTab Editable fields grid (first_name, last_name, phone, company, position)
@@ -6,7 +6,7 @@ import { parsePhoneNumberFromString } from 'libphonenumber-js';
6
6
  import { Button, Input, PhoneInput } from '@djangocfg/ui-core/components';
7
7
  import { cn } from '@djangocfg/ui-core/lib';
8
8
 
9
- import { useProfileContext } from '../context';
9
+ import { useProfileContext } from '../ProfileForm/context';
10
10
 
11
11
  function formatPhone(raw: string): string {
12
12
  if (!raw) return '';
@@ -0,0 +1,56 @@
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-nextjs/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
+ };
@@ -16,7 +16,7 @@ import {
16
16
  } from '@djangocfg/ui-core/components';
17
17
 
18
18
  import { AvatarSection } from './AvatarSection';
19
- import { useProfileContext } from '../context';
19
+ import { useProfileContext } from '../ProfileForm/context';
20
20
  import type { ProfileSlots } from '../types';
21
21
 
22
22
  interface ProfileHeaderProps {
@@ -4,8 +4,8 @@ import React from 'react';
4
4
 
5
5
  import { useAuth } from '@djangocfg/api/auth';
6
6
 
7
- import { EditableField, Section } from '.';
8
- import { useProfileContext } from '../context';
7
+ import { EditableField, PreferencesSection, Section } from '.';
8
+ import { useProfileContext } from '../ProfileForm/context';
9
9
 
10
10
  export const ProfileTab: React.FC = () => {
11
11
  const { labels, onFieldSave } = useProfileContext();
@@ -13,16 +13,22 @@ export const ProfileTab: React.FC = () => {
13
13
  if (!user) return null;
14
14
 
15
15
  return (
16
- <div className="grid grid-cols-1 md:grid-cols-2 gap-3 md:gap-6 pt-4">
17
- <Section title={labels.personalInfo}>
18
- <EditableField label={labels.firstName} value={user.first_name || ''} placeholder={labels.addFirstName} onSave={(v) => onFieldSave('first_name', v)} />
19
- <EditableField label={labels.lastName} value={user.last_name || ''} placeholder={labels.addLastName} onSave={(v) => onFieldSave('last_name', v)} />
20
- <EditableField label={labels.phone} value={user.phone || ''} placeholder={labels.addPhone} onSave={(v) => onFieldSave('phone', v)} type="phone" />
21
- </Section>
16
+ <div className="space-y-6 pt-4">
17
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-3 md:gap-6">
18
+ <Section title={labels.personalInfo}>
19
+ <EditableField label={labels.firstName} value={user.first_name || ''} placeholder={labels.addFirstName} onSave={(v) => onFieldSave('first_name', v)} />
20
+ <EditableField label={labels.lastName} value={user.last_name || ''} placeholder={labels.addLastName} onSave={(v) => onFieldSave('last_name', v)} />
21
+ <EditableField label={labels.phone} value={user.phone || ''} placeholder={labels.addPhone} onSave={(v) => onFieldSave('phone', v)} type="phone" />
22
+ </Section>
23
+
24
+ <Section title={labels.work}>
25
+ <EditableField label={labels.company} value={user.company || ''} placeholder={labels.addCompany} onSave={(v) => onFieldSave('company', v)} />
26
+ <EditableField label={labels.position} value={user.position || ''} placeholder={labels.addPosition} onSave={(v) => onFieldSave('position', v)} />
27
+ </Section>
28
+ </div>
22
29
 
23
- <Section title={labels.work}>
24
- <EditableField label={labels.company} value={user.company || ''} placeholder={labels.addCompany} onSave={(v) => onFieldSave('company', v)} />
25
- <EditableField label={labels.position} value={user.position || ''} placeholder={labels.addPosition} onSave={(v) => onFieldSave('position', v)} />
30
+ <Section title={labels.preferences ?? 'Preferences'}>
31
+ <PreferencesSection />
26
32
  </Section>
27
33
  </div>
28
34
  );
@@ -4,6 +4,7 @@ export type { ApiKeyLabels } from './ApiKeySection';
4
4
  export { AvatarSection } from './AvatarSection';
5
5
  export { DeleteAccountSection, DeleteAccountScreen } from './DeleteAccountSection';
6
6
  export { EditableField } from './EditableField';
7
+ export { PreferencesSection } from './PreferencesSection';
7
8
  export { ProfileHeader } from './ProfileHeader';
8
9
  export { ProfileTab } from './ProfileTab';
9
10
  export { Section } from './Section';
@@ -1,8 +1,6 @@
1
1
  'use client';
2
2
 
3
- import { useMemo } from 'react';
4
-
5
- import { useQueryState, parseAsStringEnum } from '@djangocfg/ui-core/hooks';
3
+ import { useMemo, useState, useCallback } from 'react';
6
4
 
7
5
  export type ProfileTabValue = 'profile' | 'security' | 'api-keys';
8
6
 
@@ -10,20 +8,21 @@ export interface UseProfileTabsOptions {
10
8
  enable2FA?: boolean;
11
9
  enableAPIKeys?: boolean;
12
10
  extraTabValues?: string[];
11
+ /** Initial active tab. Defaults to `'profile'`. */
12
+ defaultTab?: ProfileTabValue;
13
13
  }
14
14
 
15
15
  /**
16
- * Syncs the active profile tab with the URL query param `?tab=`.
16
+ * Manages the active profile tab with local React state.
17
17
  *
18
- * Falls back to `'profile'` when the param is missing or not allowed.
19
- * Setting the default value clears the key from the URL (no `?tab=profile`).
18
+ * Falls back to `'profile'` when `defaultTab` is missing or not allowed.
20
19
  *
21
20
  * @example
22
- * const { tab, setTab, allowed } = useProfileTabs({ enable2FA: true });
21
+ * const { tab, setTab, allowed } = useProfileTabs({ enable2FA: true, defaultTab: 'security' });
23
22
  * <Tabs value={tab} onValueChange={setTab}>...</Tabs>
24
23
  */
25
24
  export function useProfileTabs(options: UseProfileTabsOptions = {}) {
26
- const { enable2FA, enableAPIKeys, extraTabValues = [] } = options;
25
+ const { enable2FA, enableAPIKeys, extraTabValues = [], defaultTab } = options;
27
26
 
28
27
  const allowed = useMemo(() => {
29
28
  const base: ProfileTabValue[] = ['profile'];
@@ -32,12 +31,18 @@ export function useProfileTabs(options: UseProfileTabsOptions = {}) {
32
31
  return [...base, ...extraTabValues] as ProfileTabValue[];
33
32
  }, [enable2FA, enableAPIKeys, extraTabValues]);
34
33
 
35
- const parser = useMemo(
36
- () => parseAsStringEnum(allowed).withDefault('profile'),
37
- [allowed],
34
+ const [tab, setTabState] = useState<ProfileTabValue>(
35
+ defaultTab && allowed.includes(defaultTab) ? defaultTab : 'profile',
38
36
  );
39
37
 
40
- const [tab, setTab] = useQueryState('tab', parser, { replace: true });
38
+ const setTab = useCallback(
39
+ (value: ProfileTabValue) => {
40
+ if (allowed.includes(value)) {
41
+ setTabState(value);
42
+ }
43
+ },
44
+ [allowed],
45
+ );
41
46
 
42
47
  return { tab, setTab, allowed };
43
48
  }
@@ -1,6 +1,7 @@
1
- export { ProfileLayout, useProfileContext } from './ProfileLayout';
2
- export { ProfileProvider } from './context';
1
+ export { ProfileForm, useProfileContext } from './ProfileForm';
2
+ export { ProfileProvider } from './ProfileForm/context';
3
3
  export { useProfileTabs } from './hooks';
4
- export type { ProfileLabels } from './context';
5
- export type { ProfileLayoutProps, ProfileTab, ProfileSlots } from './types';
4
+ export { ProfileDialog, useProfileDialogStore } from './ProfileDialog';
5
+ export type { ProfileLabels } from './ProfileForm/context';
6
+ export type { ProfileFormProps, ProfileLayoutProps, ProfileTab, ProfileSlots } from './types';
6
7
  export type { ProfileTabValue, UseProfileTabsOptions } from './hooks';
@@ -1,5 +1,7 @@
1
1
  import React from 'react';
2
2
 
3
+ import type { ProfileTabValue } from './hooks/useProfileTabs';
4
+
3
5
  // ─────────────────────────────────────────────────────────────────────────────
4
6
  // Slot + Tab types
5
7
  // ─────────────────────────────────────────────────────────────────────────────
@@ -24,7 +26,7 @@ export interface ProfileSlots {
24
26
  footer?: React.ReactNode;
25
27
  }
26
28
 
27
- export interface ProfileLayoutProps {
29
+ export interface ProfileFormProps {
28
30
  onUnauthenticated?: () => void;
29
31
  title?: string;
30
32
  enable2FA?: boolean;
@@ -34,4 +36,12 @@ export interface ProfileLayoutProps {
34
36
  tabs?: ProfileTab[];
35
37
  /** Named slots for additional content */
36
38
  slots?: ProfileSlots;
39
+ /**
40
+ * When provided, the active tab is controlled locally (no URL sync).
41
+ * Useful for dialogs where query-string pollution is undesirable.
42
+ */
43
+ defaultTab?: ProfileTabValue;
37
44
  }
45
+
46
+ /** @deprecated Use ProfileFormProps instead */
47
+ export type ProfileLayoutProps = ProfileFormProps;
@@ -27,3 +27,4 @@ export type {
27
27
  LocaleSwitcherSharedProps,
28
28
  LocaleSwitcherVariant,
29
29
  } from './LocaleSwitcher';
30
+