@djangocfg/layouts 2.1.426 → 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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@djangocfg/layouts",
3
- "version": "2.1.426",
3
+ "version": "2.1.427",
4
4
  "description": "Simple, straightforward layout components for Next.js - import and use with props",
5
5
  "keywords": [
6
6
  "layouts",
@@ -89,13 +89,12 @@
89
89
  "check": "tsc --noEmit"
90
90
  },
91
91
  "peerDependencies": {
92
- "@djangocfg/api": "^2.1.426",
93
- "@djangocfg/centrifugo": "^2.1.426",
94
- "@djangocfg/debuger": "^2.1.426",
95
- "@djangocfg/i18n": "^2.1.426",
96
- "@djangocfg/monitor": "^2.1.426",
97
- "@djangocfg/ui-core": "^2.1.426",
98
- "@djangocfg/ui-nextjs": "^2.1.426",
92
+ "@djangocfg/api": "^2.1.427",
93
+ "@djangocfg/centrifugo": "^2.1.427",
94
+ "@djangocfg/debuger": "^2.1.427",
95
+ "@djangocfg/i18n": "^2.1.427",
96
+ "@djangocfg/monitor": "^2.1.427",
97
+ "@djangocfg/ui-core": "^2.1.427",
99
98
  "@hookform/resolvers": "^5.2.2",
100
99
  "consola": "^3.4.2",
101
100
  "lucide-react": "^0.545.0",
@@ -126,15 +125,14 @@
126
125
  "uuid": "^11.1.0"
127
126
  },
128
127
  "devDependencies": {
129
- "@djangocfg/api": "^2.1.426",
130
- "@djangocfg/centrifugo": "^2.1.426",
131
- "@djangocfg/debuger": "^2.1.426",
132
- "@djangocfg/i18n": "^2.1.426",
133
- "@djangocfg/monitor": "^2.1.426",
134
- "@djangocfg/typescript-config": "^2.1.426",
135
- "@djangocfg/ui-core": "^2.1.426",
136
- "@djangocfg/ui-nextjs": "^2.1.426",
137
- "@djangocfg/ui-tools": "^2.1.426",
128
+ "@djangocfg/api": "^2.1.427",
129
+ "@djangocfg/centrifugo": "^2.1.427",
130
+ "@djangocfg/debuger": "^2.1.427",
131
+ "@djangocfg/i18n": "^2.1.427",
132
+ "@djangocfg/monitor": "^2.1.427",
133
+ "@djangocfg/typescript-config": "^2.1.427",
134
+ "@djangocfg/ui-core": "^2.1.427",
135
+ "@djangocfg/ui-tools": "^2.1.427",
138
136
  "@types/node": "^25.2.3",
139
137
  "@types/react": "^19.2.15",
140
138
  "@types/react-dom": "^19.2.3",
@@ -48,7 +48,6 @@ import type {
48
48
  ErrorTrackingConfig,
49
49
  ErrorBoundaryConfig,
50
50
  SWRConfigOptions,
51
- PwaInstallConfig,
52
51
  DebugConfig,
53
52
  I18nLayoutConfig,
54
53
  } from '../types';
@@ -211,7 +210,6 @@ export interface AppLayoutBaseAppConfig {
211
210
  errorTracking?: ErrorTrackingConfig;
212
211
  swr?: SWRConfigOptions;
213
212
  errorBoundary?: ErrorBoundaryConfig;
214
- pwaInstall?: PwaInstallConfig;
215
213
  monitor?: MonitorConfig;
216
214
  debug?: DebugConfig;
217
215
  }
@@ -273,9 +271,6 @@ export interface AppLayoutProps {
273
271
  /** Error boundary configuration */
274
272
  errorBoundary?: ErrorBoundaryConfig;
275
273
 
276
- /** PWA Install configuration */
277
- pwaInstall?: PwaInstallConfig;
278
-
279
274
  /** i18n configuration for locale switching (applies to all layouts) */
280
275
  i18n?: I18nLayoutConfig;
281
276
 
@@ -486,7 +481,6 @@ export function AppLayout(props: AppLayoutProps) {
486
481
  const errorTracking = baseAppConfig?.errorTracking ?? props.errorTracking;
487
482
  const errorBoundary = baseAppConfig?.errorBoundary ?? props.errorBoundary;
488
483
  const swr = baseAppConfig?.swr ?? props.swr;
489
- const pwaInstall = baseAppConfig?.pwaInstall ?? props.pwaInstall;
490
484
  const monitor = baseAppConfig?.monitor ?? props.monitor;
491
485
  const debug = baseAppConfig?.debug ?? props.debug;
492
486
 
@@ -500,7 +494,6 @@ export function AppLayout(props: AppLayoutProps) {
500
494
  errorTracking={errorTracking}
501
495
  errorBoundary={errorBoundary}
502
496
  swr={swr}
503
- pwaInstall={pwaInstall}
504
497
  monitor={monitor}
505
498
  debug={debug}
506
499
  i18n={i18n}
@@ -54,7 +54,6 @@ import { ErrorBoundary } from '../../components/errors/ErrorBoundary';
54
54
  import { ErrorTrackingProvider } from '../../components/errors/ErrorsTracker';
55
55
  import { AnalyticsProvider } from '../../snippets/Analytics';
56
56
  import { AuthDialog } from '../../snippets/AuthDialog';
57
- import { A2HSHint, PWAPageResumeManager, PwaProvider } from '@djangocfg/ui-nextjs/pwa';
58
57
 
59
58
  import type { BaseLayoutProps } from '../types/layout.types';
60
59
  import { LayoutI18nProvider } from './LayoutI18nProvider';
@@ -71,8 +70,7 @@ export type BaseAppProps = BaseLayoutProps;
71
70
  * BaseApp - Core providers wrapper for any React/Next.js app
72
71
  *
73
72
  * Includes: ThemeProvider, TooltipProvider, SWRConfig, AuthProvider, AnalyticsProvider,
74
- * CentrifugoProvider, PwaProvider, ErrorTrackingProvider,
75
- * ErrorBoundary (optional)
73
+ * CentrifugoProvider, ErrorTrackingProvider, ErrorBoundary (optional)
76
74
  * Also renders global Toaster and PageProgress components
77
75
  */
78
76
  export function BaseApp({
@@ -85,7 +83,6 @@ export function BaseApp({
85
83
  errorTracking,
86
84
  errorBoundary,
87
85
  swr,
88
- pwaInstall,
89
86
  monitor,
90
87
  debug,
91
88
  i18n,
@@ -97,10 +94,6 @@ export function BaseApp({
97
94
  // ErrorBoundary is enabled by default
98
95
  const enableErrorBoundary = errorBoundary?.enabled !== false;
99
96
 
100
- // Check if PWA Install is enabled
101
- const pwaInstallEnabled = pwaInstall?.enabled === true;
102
- const showInstallHint = pwaInstallEnabled && pwaInstall?.showInstallHint !== false;
103
-
104
97
  // Centrifugo configuration
105
98
  const centrifugoUrl = centrifugo?.url || process.env.NEXT_PUBLIC_CENTRIFUGO_URL;
106
99
  const centrifugoEnabled = centrifugo?.enabled !== false;
@@ -150,51 +143,35 @@ export function BaseApp({
150
143
  return result.data.token;
151
144
  }}
152
145
  >
153
- <PwaProvider enabled={pwaInstallEnabled}>
154
- <ErrorTrackingProvider
155
- validation={errorTracking?.validation}
156
- cors={errorTracking?.cors}
157
- network={errorTracking?.network}
158
- onError={errorTracking?.onError}
159
- onMonitorCapture={(d) => FrontendMonitor.capture(errorDetailToMonitorEvent(d))}
160
- >
161
- <MonitorProvider {...monitorConfig} />
162
- <LayoutI18nProvider value={i18n}>
163
- {i18n?.routing ? (
164
- <NextIntlLinkBridge routing={i18n.routing}>{children}</NextIntlLinkBridge>
165
- ) : (
166
- children
167
- )}
168
- </LayoutI18nProvider>
169
- <NextTopLoader
170
- color="var(--primary)"
171
- height={3}
172
- showSpinner={false}
173
- shadow="0 0 10px var(--primary), 0 0 5px var(--primary)"
174
- />
175
-
176
- {/* PWA Install Hint */}
177
- {showInstallHint && (
178
- <A2HSHint
179
- resetAfterDays={pwaInstall?.resetAfterDays}
180
- delayMs={pwaInstall?.delayMs}
181
- logo={pwaInstall?.logo}
182
- />
183
- )}
184
-
185
- {/* PWA Page Resume Manager */}
186
- {pwaInstallEnabled && pwaInstall?.resumeLastPage && (
187
- <PWAPageResumeManager enabled={true} />
146
+ <ErrorTrackingProvider
147
+ validation={errorTracking?.validation}
148
+ cors={errorTracking?.cors}
149
+ network={errorTracking?.network}
150
+ onError={errorTracking?.onError}
151
+ onMonitorCapture={(d) => FrontendMonitor.capture(errorDetailToMonitorEvent(d))}
152
+ >
153
+ <MonitorProvider {...monitorConfig} />
154
+ <LayoutI18nProvider value={i18n}>
155
+ {i18n?.routing ? (
156
+ <NextIntlLinkBridge routing={i18n.routing}>{children}</NextIntlLinkBridge>
157
+ ) : (
158
+ children
188
159
  )}
189
-
190
- {/* Auth Dialog - only when auth is enabled */}
191
- {authEnabled && <AuthDialog authPath={authConfig?.routes?.auth} />}
192
-
193
- {/* Debug Panel — auto in dev, ?debug=1 in prod, disable with debug={{ enabled: false }} */}
194
- {/* eslint-disable-next-line @typescript-eslint/no-explicit-any */}
195
- <DebugButton enabled={debugEnabled} {...(debugProps as any)} />
196
- </ErrorTrackingProvider>
197
- </PwaProvider>
160
+ </LayoutI18nProvider>
161
+ <NextTopLoader
162
+ color="var(--primary)"
163
+ height={3}
164
+ showSpinner={false}
165
+ shadow="0 0 10px var(--primary), 0 0 5px var(--primary)"
166
+ />
167
+
168
+ {/* Auth Dialog - only when auth is enabled */}
169
+ {authEnabled && <AuthDialog authPath={authConfig?.routes?.auth} />}
170
+
171
+ {/* Debug Panel — auto in dev, ?debug=1 in prod, disable with debug={{ enabled: false }} */}
172
+ {/* eslint-disable-next-line @typescript-eslint/no-explicit-any */}
173
+ <DebugButton enabled={debugEnabled} {...(debugProps as any)} />
174
+ </ErrorTrackingProvider>
198
175
  </CentrifugoProvider>
199
176
  </AnalyticsProvider>
200
177
  </AuthProvider>
@@ -3,12 +3,16 @@
3
3
  import React, { memo, useMemo } from 'react';
4
4
 
5
5
  import { useAppT } from '@djangocfg/i18n';
6
+ import { Preloader } from '@djangocfg/ui-core/components';
6
7
 
7
- import { AuthButton, AuthContainer, AuthHeader } from '../../shared';
8
+ import { AuthContainer, AuthHeader } from '../../shared';
8
9
 
9
10
  /**
10
11
  * SetupLoading - Loading state for 2FA setup.
11
12
  *
13
+ * Uses the ui-core Preloader (text-primary spinner) so the "Setting up…"
14
+ * indicator stays clearly visible instead of the near-invisible chip spinner.
15
+ *
12
16
  * Memoised: no props; pure translation-driven component. Prevents
13
17
  * re-renders when parent orchestrators re-render for unrelated reasons.
14
18
  */
@@ -23,9 +27,7 @@ function SetupLoadingRaw() {
23
27
  <AuthContainer step="2fa-setup">
24
28
  <AuthHeader title={content.title} />
25
29
  <div className="auth-form-group">
26
- <AuthButton loading disabled>
27
- {content.button}
28
- </AuthButton>
30
+ <Preloader variant="inline" text={content.button} className="py-4" />
29
31
  </div>
30
32
  </AuthContainer>
31
33
  );
@@ -42,8 +42,8 @@ export { SidebarBrandSwitcher } from './components';
42
42
 
43
43
  export { PrivateLayoutProps };
44
44
 
45
- const ProfileDialog = React.lazy(() =>
46
- import('../ProfileLayout/ProfileDialog/ProfileDialog').then((m) => ({ default: m.ProfileDialog }))
45
+ const SettingsDialog = React.lazy(() =>
46
+ import('../SettingsLayout/SettingsDialog').then((m) => ({ default: m.SettingsDialog }))
47
47
  );
48
48
 
49
49
  export function PrivateLayout({
@@ -55,6 +55,7 @@ export function PrivateLayout({
55
55
  contentScroll = 'auto',
56
56
  visual,
57
57
  requireAuth = true,
58
+ settings,
58
59
  }: PrivateLayoutProps) {
59
60
  const { isLoading, loadingText } = useAuthGuard({
60
61
  requireAuth,
@@ -104,7 +105,10 @@ export function PrivateLayout({
104
105
  </PrivateContent>
105
106
  </SidebarInset>
106
107
 
107
- <ProfileDialog />
108
+ {/* Settings is the primary account dialog — always mounted so the
109
+ account-menu "Settings" item works out of the box. `settings` is
110
+ optional config (extra app sections / flags), not an on/off switch. */}
111
+ <SettingsDialog {...(settings ?? {})} />
108
112
  </SidebarProvider>
109
113
  );
110
114
  }
@@ -64,7 +64,11 @@ export function PrivateContent({
64
64
  : undefined;
65
65
 
66
66
  const scrollAreaClass = cn(
67
- 'min-h-0 flex-1 bg-card',
67
+ // The boxed content is the MAIN surface (Claude bg-100), not a raised card.
68
+ // `bg-background` keeps it the warm page tone; the slightly darker
69
+ // sidebar-canvas behind the inset gives the floating look. (`bg-card`, now
70
+ // lifted to 18.4%, made this big panel read too light.)
71
+ 'min-h-0 flex-1 bg-background',
68
72
  scroll === 'auto' ? 'overflow-y-auto' : 'flex flex-col overflow-hidden',
69
73
  padding === 'default' && scroll === 'auto' && [
70
74
  'px-4 sm:px-6 lg:px-8',
@@ -11,9 +11,9 @@
11
11
 
12
12
  'use client';
13
13
 
14
- import { ChevronsUpDown, LogOut } from 'lucide-react';
14
+ import { ChevronsUpDown, Languages, LogOut, Settings as SettingsIcon } from 'lucide-react';
15
15
  import { Link } from '@djangocfg/ui-core/components';
16
- import React, { memo, useMemo } from 'react';
16
+ import React, { memo } from 'react';
17
17
 
18
18
  import { useAuth } from '@djangocfg/api/auth';
19
19
  import { useAppT } from '@djangocfg/i18n';
@@ -22,21 +22,19 @@ import {
22
22
  AvatarFallback,
23
23
  AvatarImage,
24
24
  Button,
25
- DropdownMenu,
26
- DropdownMenuContent,
27
- DropdownMenuItem,
28
- DropdownMenuLabel,
29
- DropdownMenuSeparator,
30
- DropdownMenuTrigger,
25
+ LanguageFlag,
26
+ MenuBuilder,
31
27
  } from '@djangocfg/ui-core/components';
28
+ import type { MenuItem } from '@djangocfg/ui-core/components';
32
29
  import { cn, isDev } from '@djangocfg/ui-core/lib';
33
30
  import { useSidebar } from '@djangocfg/ui-core/components';
34
31
 
35
32
  import { useLogout } from '../../../hooks';
36
33
  import { useLayoutI18nOptional } from '../../AppLayout/LayoutI18nProvider';
37
34
  import { LucideIcon as LucideIconRender } from '../../../components';
35
+ import { getLocaleMeta } from '../../_components/locale-switcher/localeMeta';
38
36
  import { useShellVisualState } from '../hooks';
39
- import { useProfileDialogStore } from '../../ProfileLayout/ProfileDialog/store';
37
+ import { useSettingsDialogStore } from '../../SettingsLayout/store';
40
38
 
41
39
  import type { HeaderConfig } from '../types';
42
40
 
@@ -101,11 +99,6 @@ function PrivateSidebarAccountRaw({ header }: PrivateSidebarAccountProps) {
101
99
  setSidebarOpen(true);
102
100
  }, [setSidebarOpen]);
103
101
 
104
- const onLogoutSelect = React.useCallback((e: Event) => {
105
- e.preventDefault();
106
- handleLogout();
107
- }, [handleLogout]);
108
-
109
102
  // Hide entirely in production when there's no user (auth still loading or
110
103
  // /me failed and the parent guard hasn't redirected yet). In dev keep a
111
104
  // placeholder so the footer + Log out are reachable for debugging.
@@ -116,8 +109,10 @@ function PrivateSidebarAccountRaw({ header }: PrivateSidebarAccountProps) {
116
109
  const secondary = header?.footerSecondaryAction;
117
110
 
118
111
  const triggerClassName = cn(
119
- 'group h-auto w-full gap-3 rounded-lg px-3 py-3 text-left',
112
+ 'group h-auto w-full gap-3 rounded-lg px-3 py-3 text-left select-none',
120
113
  'hover:bg-sidebar-accent/70 hover:text-sidebar-accent-foreground',
114
+ // No focus ring on the menu trigger — the open menu is the affordance.
115
+ 'focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0',
121
116
  content.isAccountCompact ? 'justify-center px-0 py-2' : 'min-h-[52px]',
122
117
  );
123
118
 
@@ -129,39 +124,16 @@ function PrivateSidebarAccountRaw({ header }: PrivateSidebarAccountProps) {
129
124
  'p-1.5',
130
125
  content.isAccountCompact ? 'min-w-60' : 'w-[var(--radix-dropdown-menu-trigger-width)] min-w-60',
131
126
  );
132
- const dropdownSide: 'top' | 'right' = content.isAccountCompact ? 'right' : 'top';
127
+ // Always open UPWARD over the avatar (Claude pattern). The footer sits at the
128
+ // bottom of the rail in both expanded and collapsed states, so `top` reads
129
+ // correctly either way (vs. `right`, which floated off to the side).
130
+ const dropdownSide: 'top' = 'top';
133
131
  const avatarClass = cn(
134
132
  'h-9 w-9 shrink-0 border border-transparent transition-colors',
135
133
  'group-hover:border-sidebar-border/70',
136
134
  );
137
135
 
138
136
  const headerLabelText = account.email ?? (account.source === 'dev-fallback' ? 'No active session' : null);
139
- const headerLabel = headerLabelText ? (
140
- <DropdownMenuLabel className="truncate px-2 py-1.5 text-xs font-normal text-muted-foreground">
141
- {headerLabelText}
142
- </DropdownMenuLabel>
143
- ) : null;
144
-
145
- const accountLinksItems = accountLinks.map((item) => {
146
- const Icon = item.icon;
147
- return (
148
- <DropdownMenuItem key={item.href} asChild>
149
- <Link
150
- href={item.href!}
151
- className="flex items-center gap-2 rounded-md px-2 py-1.5 text-sm"
152
- >
153
- {Icon ? <Icon className="h-4 w-4 shrink-0 text-muted-foreground" /> : null}
154
- <span className="truncate">{item.label}</span>
155
- </Link>
156
- </DropdownMenuItem>
157
- );
158
- });
159
- const accountLinksBlock = accountLinks.length > 0 ? (
160
- <>
161
- {headerLabel ? <DropdownMenuSeparator /> : null}
162
- {accountLinksItems}
163
- </>
164
- ) : null;
165
137
 
166
138
  const expandedMeta = content.isAccountCompact ? null : (
167
139
  <>
@@ -184,17 +156,96 @@ function PrivateSidebarAccountRaw({ header }: PrivateSidebarAccountProps) {
184
156
  </>
185
157
  );
186
158
 
187
- const openProfileDialog = React.useCallback(() => {
188
- useProfileDialogStore.getState().open();
159
+ // `accountAction: 'dialog'` opens the global SettingsDialog (mounted in
160
+ // PrivateLayout). Requires `settings` to be passed to PrivateLayout.
161
+ const openSettingsDialog = React.useCallback(() => {
162
+ useSettingsDialogStore.getState().open();
189
163
  }, []);
190
164
 
165
+ // ── Declarative account menu (Claude-style) — fed to MenuBuilder ──
166
+ const menuItems = React.useMemo<MenuItem[]>(() => {
167
+ const items: MenuItem[] = [];
168
+
169
+ // Email header.
170
+ if (headerLabelText) {
171
+ items.push({ kind: 'label', id: 'email', label: headerLabelText });
172
+ }
173
+
174
+ // Settings → opens the global SettingsDialog (⌘, like Claude).
175
+ items.push({
176
+ kind: 'item',
177
+ id: 'settings',
178
+ label: t('layouts.profile.settings') || 'Settings',
179
+ icon: SettingsIcon,
180
+ shortcut: '⇧⌘,',
181
+ onSelect: () => useSettingsDialogStore.getState().open(),
182
+ });
183
+
184
+ // Language → submenu with a radio group of locales (flag + native name,
185
+ // checkmark on the active one). Only when an i18n layout context is present.
186
+ if (layoutI18n && layoutI18n.locales.length > 1) {
187
+ items.push({
188
+ kind: 'submenu',
189
+ id: 'language',
190
+ label: t('layouts.profile.language') || 'Language',
191
+ icon: Languages,
192
+ items: [
193
+ {
194
+ kind: 'radio-group',
195
+ id: 'locale',
196
+ value: layoutI18n.locale,
197
+ onValueChange: layoutI18n.onLocaleChange,
198
+ options: layoutI18n.locales.map((code) => {
199
+ const meta = getLocaleMeta(code);
200
+ return {
201
+ id: code,
202
+ value: code,
203
+ label: meta.native,
204
+ icon: <LanguageFlag code={code} rounded className="h-3 w-4 shrink-0" />,
205
+ };
206
+ }),
207
+ },
208
+ ],
209
+ });
210
+ }
211
+
212
+ // App-provided account links (from header.groups).
213
+ if (accountLinks.length > 0) {
214
+ items.push({ kind: 'separator', id: 'sep-links' });
215
+ for (const link of accountLinks) {
216
+ items.push({
217
+ kind: 'item',
218
+ id: link.href!,
219
+ label: link.label,
220
+ icon: link.icon,
221
+ href: link.href,
222
+ });
223
+ }
224
+ }
225
+
226
+ // Log out (destructive). Let the menu close normally (no preventDefault) —
227
+ // the logout confirm dialog should appear over a CLOSED menu, not under a
228
+ // still-open one.
229
+ items.push({ kind: 'separator', id: 'sep-logout' });
230
+ items.push({
231
+ kind: 'item',
232
+ id: 'logout',
233
+ label: signOutLabel,
234
+ icon: LogOut,
235
+ variant: 'destructive',
236
+ onSelect: () => handleLogout(),
237
+ });
238
+
239
+ return items;
240
+ }, [headerLabelText, t, layoutI18n, accountLinks, signOutLabel, handleLogout]);
241
+
191
242
  const triggerButton = (
192
243
  <Button
193
244
  type="button"
194
245
  variant="ghost"
195
246
  aria-label={content.isAccountCompact ? account.displayName : undefined}
196
247
  className={triggerClassName}
197
- onClick={header?.accountAction === 'dialog' ? openProfileDialog : onTriggerInteract}
248
+ onClick={header?.accountAction === 'dialog' ? openSettingsDialog : onTriggerInteract}
198
249
  >
199
250
  <Avatar className={avatarClass}>
200
251
  <AvatarImage src={account.avatarUrl} alt={account.displayName} />
@@ -209,7 +260,7 @@ function PrivateSidebarAccountRaw({ header }: PrivateSidebarAccountProps) {
209
260
  content.isAccountCompact ? 'px-0 pb-0' : 'px-2 pb-2',
210
261
  );
211
262
 
212
- // Dialog mode: simple button that opens the global ProfileDialog
263
+ // Dialog mode: simple button that opens the global SettingsDialog
213
264
  if (header?.accountAction === 'dialog') {
214
265
  return (
215
266
  <div className={wrapperClass}>
@@ -220,33 +271,17 @@ function PrivateSidebarAccountRaw({ header }: PrivateSidebarAccountProps) {
220
271
 
221
272
  return (
222
273
  <div className={wrapperClass}>
223
- <DropdownMenu
274
+ <MenuBuilder
275
+ items={menuItems}
224
276
  open={isAccountMenuOpen}
225
277
  onOpenChange={setIsAccountMenuOpen}
278
+ side={dropdownSide}
279
+ align="start"
280
+ sideOffset={8}
281
+ contentClassName={dropdownContentClass}
226
282
  >
227
- <DropdownMenuTrigger asChild>
228
- {triggerButton}
229
- </DropdownMenuTrigger>
230
-
231
- <DropdownMenuContent
232
- side={dropdownSide}
233
- align="start"
234
- sideOffset={8}
235
- className={dropdownContentClass}
236
- >
237
- {headerLabel}
238
- {accountLinksBlock}
239
-
240
- <DropdownMenuSeparator />
241
- <DropdownMenuItem
242
- onSelect={onLogoutSelect}
243
- className="flex items-center gap-2 rounded-md px-2 py-1.5 text-sm text-destructive focus:bg-destructive/10 focus:text-destructive"
244
- >
245
- <LogOut className="h-4 w-4 shrink-0" />
246
- <span className="truncate">{signOutLabel}</span>
247
- </DropdownMenuItem>
248
- </DropdownMenuContent>
249
- </DropdownMenu>
283
+ {triggerButton}
284
+ </MenuBuilder>
250
285
  </div>
251
286
  );
252
287
  }
@@ -8,6 +8,7 @@ import type { ReactNode } from 'react';
8
8
  import type { LucideIcon } from 'lucide-react';
9
9
 
10
10
  import type { AppLayoutPublicChrome } from '../AppLayout/AppLayout';
11
+ import type { SettingsDialogProps } from '../SettingsLayout/types';
11
12
  import type { LayoutVisualConfig } from '../types';
12
13
  import type { UserMenuConfig } from '../types';
13
14
 
@@ -225,4 +226,11 @@ export interface PrivateLayoutProps {
225
226
  requireAuth?: boolean;
226
227
  /** Reserved for `AppLayout` passthrough (`publicChrome`); unused in this layout. */
227
228
  publicChrome?: AppLayoutPublicChrome;
229
+ /**
230
+ * Mount the global SettingsDialog (Claude-style master/detail settings modal,
231
+ * hash-URL driven, openable via `useSettingsDialog()`). Pass a config object
232
+ * (even `{}`) to enable it with the built-in sections; omit to not mount it.
233
+ * Coexists with the legacy ProfileDialog — they are independent.
234
+ */
235
+ settings?: SettingsDialogProps;
228
236
  }