@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.
- package/package.json +15 -17
- package/src/layouts/AppLayout/AppLayout.tsx +0 -7
- package/src/layouts/AppLayout/BaseApp.tsx +29 -52
- package/src/layouts/AuthLayout/components/steps/SetupStep/SetupLoading.tsx +6 -4
- package/src/layouts/PrivateLayout/PrivateLayout.tsx +7 -3
- package/src/layouts/PrivateLayout/components/PrivateContent.tsx +5 -1
- package/src/layouts/PrivateLayout/components/PrivateSidebarAccount.tsx +105 -70
- package/src/layouts/PrivateLayout/types.ts +8 -0
- package/src/layouts/PublicLayout/components/UserMenu.tsx +68 -113
- package/src/layouts/PublicLayout/navbars/MinimalNavbar/MinimalNavbar.tsx +0 -6
- package/src/layouts/PublicLayout/navbars/MinimalNavbar/index.ts +1 -1
- package/src/layouts/SettingsLayout/README.md +258 -0
- package/src/layouts/SettingsLayout/SettingsDialog.tsx +101 -0
- package/src/layouts/SettingsLayout/SettingsForm.tsx +100 -0
- package/src/layouts/SettingsLayout/components/ApiKeySection/ApiKeySection.tsx +189 -0
- package/src/layouts/SettingsLayout/components/SettingsNav.tsx +71 -0
- package/src/layouts/SettingsLayout/components/SettingsNavItem.tsx +57 -0
- package/src/layouts/SettingsLayout/components/SettingsPanel.tsx +48 -0
- package/src/layouts/SettingsLayout/components/SettingsSearch.tsx +50 -0
- package/src/layouts/SettingsLayout/components/SettingsShell.tsx +77 -0
- package/src/layouts/SettingsLayout/components/SettingsTabs.tsx +56 -0
- package/src/layouts/{ProfileLayout → SettingsLayout}/components/TwoFactorSection/TwoFactorSection.tsx +84 -130
- package/src/layouts/SettingsLayout/components/index.ts +6 -0
- package/src/layouts/SettingsLayout/context/SettingsContext.tsx +122 -0
- package/src/layouts/SettingsLayout/context/index.ts +2 -0
- package/src/layouts/SettingsLayout/hooks/index.ts +12 -0
- package/src/layouts/SettingsLayout/hooks/useProfileSave.ts +95 -0
- package/src/layouts/SettingsLayout/hooks/useSettingsDialog.ts +52 -0
- package/src/layouts/SettingsLayout/hooks/useSettingsSections.ts +123 -0
- package/src/layouts/SettingsLayout/hooks/useSettingsUrl.ts +140 -0
- package/src/layouts/SettingsLayout/index.ts +67 -0
- package/src/layouts/SettingsLayout/sections/AccountSection.tsx +100 -0
- package/src/layouts/SettingsLayout/sections/ApiKeysSection.tsx +15 -0
- package/src/layouts/SettingsLayout/sections/DeleteAccountRow.tsx +57 -0
- package/src/layouts/SettingsLayout/sections/PreferencesRows.tsx +43 -0
- package/src/layouts/SettingsLayout/sections/SecuritySection.tsx +15 -0
- package/src/layouts/SettingsLayout/sections/builtins.tsx +77 -0
- package/src/layouts/SettingsLayout/sections/index.ts +8 -0
- package/src/layouts/SettingsLayout/store.ts +47 -0
- package/src/layouts/SettingsLayout/types.ts +107 -0
- package/src/layouts/index.ts +1 -1
- package/src/layouts/types/index.ts +0 -1
- package/src/layouts/types/layout.types.ts +0 -4
- package/src/layouts/ProfileLayout/ProfileDialog/ProfileDialog.tsx +0 -56
- package/src/layouts/ProfileLayout/ProfileDialog/index.ts +0 -4
- package/src/layouts/ProfileLayout/ProfileDialog/store.ts +0 -51
- package/src/layouts/ProfileLayout/ProfileForm/context.tsx +0 -123
- package/src/layouts/ProfileLayout/ProfileForm/index.tsx +0 -147
- package/src/layouts/ProfileLayout/README.md +0 -150
- package/src/layouts/ProfileLayout/components/ActionButton.tsx +0 -38
- package/src/layouts/ProfileLayout/components/ApiKeySection/ApiKeySection.tsx +0 -197
- package/src/layouts/ProfileLayout/components/DeleteAccountSection.tsx +0 -44
- package/src/layouts/ProfileLayout/components/EditableField.tsx +0 -128
- package/src/layouts/ProfileLayout/components/PreferencesSection.tsx +0 -56
- package/src/layouts/ProfileLayout/components/ProfileHeader.tsx +0 -110
- package/src/layouts/ProfileLayout/components/ProfileTab.tsx +0 -35
- package/src/layouts/ProfileLayout/components/Section.tsx +0 -22
- package/src/layouts/ProfileLayout/components/index.ts +0 -11
- package/src/layouts/ProfileLayout/hooks/index.ts +0 -2
- package/src/layouts/ProfileLayout/hooks/useProfileTabs.ts +0 -56
- package/src/layouts/ProfileLayout/index.ts +0 -8
- package/src/layouts/ProfileLayout/types.ts +0 -48
- /package/src/layouts/{ProfileLayout → SettingsLayout}/components/ApiKeySection/context.tsx +0 -0
- /package/src/layouts/{ProfileLayout → SettingsLayout}/components/ApiKeySection/index.ts +0 -0
- /package/src/layouts/{ProfileLayout → SettingsLayout}/components/AvatarSection.tsx +0 -0
- /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.
|
|
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.
|
|
93
|
-
"@djangocfg/centrifugo": "^2.1.
|
|
94
|
-
"@djangocfg/debuger": "^2.1.
|
|
95
|
-
"@djangocfg/i18n": "^2.1.
|
|
96
|
-
"@djangocfg/monitor": "^2.1.
|
|
97
|
-
"@djangocfg/ui-core": "^2.1.
|
|
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.
|
|
130
|
-
"@djangocfg/centrifugo": "^2.1.
|
|
131
|
-
"@djangocfg/debuger": "^2.1.
|
|
132
|
-
"@djangocfg/i18n": "^2.1.
|
|
133
|
-
"@djangocfg/monitor": "^2.1.
|
|
134
|
-
"@djangocfg/typescript-config": "^2.1.
|
|
135
|
-
"@djangocfg/ui-core": "^2.1.
|
|
136
|
-
"@djangocfg/ui-
|
|
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,
|
|
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
|
-
<
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
{i18n
|
|
164
|
-
|
|
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
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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 {
|
|
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
|
-
<
|
|
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
|
|
46
|
-
import('../
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
26
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
188
|
-
|
|
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' ?
|
|
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
|
|
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
|
-
<
|
|
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
|
-
|
|
228
|
-
|
|
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
|
}
|