@djangocfg/layouts 2.1.357 → 2.1.359
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 +21 -19
- package/src/configurator/private/schema.ts +20 -0
- package/src/layouts/PrivateLayout/PrivateLayout.tsx +17 -1
- package/src/layouts/PrivateLayout/README.md +47 -1
- package/src/layouts/PrivateLayout/components/PrivateSidebar.tsx +5 -72
- package/src/layouts/PrivateLayout/components/PrivateSidebarAccount.tsx +47 -96
- package/src/layouts/PrivateLayout/components/SidebarBrand.tsx +36 -17
- package/src/layouts/PrivateLayout/components/SidebarBrandSwitcher.tsx +223 -0
- package/src/layouts/PrivateLayout/components/index.ts +1 -0
- package/src/layouts/PrivateLayout/context.tsx +2 -9
- package/src/layouts/PrivateLayout/hooks/index.ts +1 -5
- package/src/layouts/PrivateLayout/hooks/useHoverExpand.ts +10 -3
- package/src/layouts/PrivateLayout/hooks/useShellVisualState.ts +11 -88
- package/src/layouts/PrivateLayout/hooks/useSidebarDefaultOpen.ts +32 -0
- package/src/layouts/PrivateLayout/index.ts +3 -0
- package/src/layouts/PrivateLayout/types.ts +41 -0
- package/src/layouts/ProfileLayout/ProfileDialog/ProfileDialog.tsx +32 -0
- package/src/layouts/ProfileLayout/ProfileDialog/index.ts +2 -0
- package/src/layouts/ProfileLayout/ProfileDialog/store.ts +19 -0
- package/src/layouts/ProfileLayout/{context.tsx → ProfileForm/context.tsx} +4 -2
- package/src/layouts/ProfileLayout/{ProfileLayout.tsx → ProfileForm/index.tsx} +10 -7
- package/src/layouts/ProfileLayout/README.md +65 -5
- package/src/layouts/ProfileLayout/components/EditableField.tsx +1 -1
- package/src/layouts/ProfileLayout/components/PreferencesSection.tsx +56 -0
- package/src/layouts/ProfileLayout/components/ProfileHeader.tsx +1 -1
- package/src/layouts/ProfileLayout/components/ProfileTab.tsx +17 -11
- package/src/layouts/ProfileLayout/components/index.ts +1 -0
- package/src/layouts/ProfileLayout/hooks/useProfileTabs.ts +17 -12
- package/src/layouts/ProfileLayout/index.ts +5 -4
- package/src/layouts/ProfileLayout/types.ts +11 -1
- package/src/layouts/_components/index.ts +1 -0
- package/src/layouts/types/providers.types.ts +2 -2
- package/src/theme/ThemeStyleBridge.tsx +1 -3
- package/src/theme/index.ts +2 -4
- package/src/theme/buildThemeStyleSheet.ts +0 -71
- package/src/theme/themeStyle.types.ts +0 -89
- package/src/theme/themeStylePresets.ts +0 -202
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@djangocfg/layouts",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.359",
|
|
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.
|
|
88
|
-
"@djangocfg/centrifugo": "^2.1.
|
|
89
|
-
"@djangocfg/debuger": "^2.1.
|
|
90
|
-
"@djangocfg/i18n": "^2.1.
|
|
91
|
-
"@djangocfg/monitor": "^2.1.
|
|
92
|
-
"@djangocfg/ui-core": "^2.1.
|
|
93
|
-
"@djangocfg/ui-nextjs": "^2.1.
|
|
87
|
+
"@djangocfg/api": "^2.1.359",
|
|
88
|
+
"@djangocfg/centrifugo": "^2.1.359",
|
|
89
|
+
"@djangocfg/debuger": "^2.1.359",
|
|
90
|
+
"@djangocfg/i18n": "^2.1.359",
|
|
91
|
+
"@djangocfg/monitor": "^2.1.359",
|
|
92
|
+
"@djangocfg/ui-core": "^2.1.359",
|
|
93
|
+
"@djangocfg/ui-nextjs": "^2.1.359",
|
|
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.
|
|
124
|
-
"@djangocfg/centrifugo": "^2.1.
|
|
125
|
-
"@djangocfg/debuger": "^2.1.
|
|
126
|
-
"@djangocfg/i18n": "^2.1.
|
|
127
|
-
"@djangocfg/monitor": "^2.1.
|
|
128
|
-
"@djangocfg/typescript-config": "^2.1.
|
|
129
|
-
"@djangocfg/ui-core": "^2.1.
|
|
130
|
-
"@djangocfg/ui-nextjs": "^2.1.
|
|
131
|
-
"@djangocfg/ui-tools": "^2.1.
|
|
124
|
+
"@djangocfg/api": "^2.1.359",
|
|
125
|
+
"@djangocfg/centrifugo": "^2.1.359",
|
|
126
|
+
"@djangocfg/debuger": "^2.1.359",
|
|
127
|
+
"@djangocfg/i18n": "^2.1.359",
|
|
128
|
+
"@djangocfg/monitor": "^2.1.359",
|
|
129
|
+
"@djangocfg/typescript-config": "^2.1.359",
|
|
130
|
+
"@djangocfg/ui-core": "^2.1.359",
|
|
131
|
+
"@djangocfg/ui-nextjs": "^2.1.359",
|
|
132
|
+
"@djangocfg/ui-tools": "^2.1.359",
|
|
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,8 @@ export interface PrivateLayoutConfiguratorData {
|
|
|
37
37
|
header: {
|
|
38
38
|
userPlan: string;
|
|
39
39
|
showSecondaryAction: boolean;
|
|
40
|
+
accountAction: 'menu' | 'dialog';
|
|
41
|
+
showSwitcher: boolean;
|
|
40
42
|
};
|
|
41
43
|
}
|
|
42
44
|
|
|
@@ -126,6 +128,17 @@ export const privateLayoutConfiguratorSchema: CustomJsonSchema7 = {
|
|
|
126
128
|
title: 'Footer secondary action',
|
|
127
129
|
description: 'Adds a download-style icon button inside the footer trigger with a pulsing accent dot.',
|
|
128
130
|
},
|
|
131
|
+
accountAction: {
|
|
132
|
+
type: 'string',
|
|
133
|
+
title: 'Account action',
|
|
134
|
+
enum: ['menu', 'dialog'],
|
|
135
|
+
description: "'menu' opens a dropdown; 'dialog' opens the global ProfileDialog.",
|
|
136
|
+
},
|
|
137
|
+
showSwitcher: {
|
|
138
|
+
type: 'boolean',
|
|
139
|
+
title: 'Brand switcher',
|
|
140
|
+
description: 'Replace the static brand with a workspace/account dropdown.',
|
|
141
|
+
},
|
|
129
142
|
},
|
|
130
143
|
},
|
|
131
144
|
},
|
|
@@ -165,6 +178,11 @@ export const privateLayoutConfiguratorUiSchema: CustomJsonUiSchema7 = {
|
|
|
165
178
|
header: {
|
|
166
179
|
'ui:collapsible': true,
|
|
167
180
|
showSecondaryAction: { 'ui:widget': 'switch' },
|
|
181
|
+
showSwitcher: { 'ui:widget': 'switch' },
|
|
182
|
+
accountAction: {
|
|
183
|
+
'ui:widget': 'radio',
|
|
184
|
+
'ui:options': { inline: true },
|
|
185
|
+
},
|
|
168
186
|
},
|
|
169
187
|
};
|
|
170
188
|
|
|
@@ -186,5 +204,7 @@ export const defaultPrivateLayoutConfiguratorData: PrivateLayoutConfiguratorData
|
|
|
186
204
|
header: {
|
|
187
205
|
userPlan: 'Pro plan',
|
|
188
206
|
showSecondaryAction: false,
|
|
207
|
+
accountAction: 'menu',
|
|
208
|
+
showSwitcher: false,
|
|
189
209
|
},
|
|
190
210
|
};
|
|
@@ -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,
|
|
@@ -33,10 +35,20 @@ export type {
|
|
|
33
35
|
SidebarActiveIndicator,
|
|
34
36
|
SidebarGroupLabelStyle,
|
|
35
37
|
SidebarFeaturedConfig,
|
|
38
|
+
SidebarBrandSwitcherConfig,
|
|
39
|
+
SidebarBrandSwitcherItem,
|
|
36
40
|
} from './types';
|
|
37
41
|
|
|
42
|
+
export { SidebarBrandSwitcher } from './components';
|
|
43
|
+
|
|
38
44
|
export { PrivateLayoutProps };
|
|
39
45
|
|
|
46
|
+
// Lazy-load ProfileDialog so the profile bundle is only fetched when opened.
|
|
47
|
+
const ProfileDialog = dynamic(
|
|
48
|
+
() => import('../ProfileLayout/ProfileDialog/ProfileDialog').then((m) => m.ProfileDialog),
|
|
49
|
+
{ ssr: false },
|
|
50
|
+
);
|
|
51
|
+
|
|
40
52
|
export function PrivateLayout({
|
|
41
53
|
children,
|
|
42
54
|
sidebar,
|
|
@@ -55,6 +67,8 @@ export function PrivateLayout({
|
|
|
55
67
|
const { providerStyle, providerClassName, insetClassName, sidebarVariant } =
|
|
56
68
|
useLayoutVisual(visual);
|
|
57
69
|
|
|
70
|
+
const defaultOpen = useSidebarDefaultOpen();
|
|
71
|
+
|
|
58
72
|
if (isLoading) {
|
|
59
73
|
return (
|
|
60
74
|
<Preloader
|
|
@@ -69,7 +83,7 @@ export function PrivateLayout({
|
|
|
69
83
|
|
|
70
84
|
return (
|
|
71
85
|
<SidebarProvider
|
|
72
|
-
defaultOpen={
|
|
86
|
+
defaultOpen={defaultOpen}
|
|
73
87
|
style={providerStyle}
|
|
74
88
|
className={providerClassName}
|
|
75
89
|
>
|
|
@@ -92,6 +106,8 @@ export function PrivateLayout({
|
|
|
92
106
|
{children}
|
|
93
107
|
</PrivateContent>
|
|
94
108
|
</SidebarInset>
|
|
109
|
+
|
|
110
|
+
<ProfileDialog />
|
|
95
111
|
</SidebarProvider>
|
|
96
112
|
);
|
|
97
113
|
}
|
|
@@ -119,16 +119,62 @@ For pages like Kanban boards where the shell must be exactly one viewport tall a
|
|
|
119
119
|
|
|
120
120
|
| Field | Type | Notes |
|
|
121
121
|
|---|---|---|
|
|
122
|
-
| `
|
|
122
|
+
| `switcher` | `SidebarBrandSwitcherConfig` | Brand switcher dropdown (replaces `brand`/`title`/`brandIcon` when set). |
|
|
123
|
+
| `brand`, `title`, `brandIcon`, `brandLetter` | — | Static sidebar header brand (used when `switcher` is not set). |
|
|
123
124
|
| `groups` | `UserMenuGroup[]` | Account links rendered inside the footer dropdown. |
|
|
124
125
|
| `authPath` | `string` | Sign-in redirect target. |
|
|
125
126
|
| `userPlan` | `string` | Subtitle under the display name (e.g. `"Max plan"`). |
|
|
127
|
+
| `accountAction` | `'menu' \| 'dialog'` | Footer button behaviour. `'menu'` opens dropdown; `'dialog'` opens global ProfileDialog. Default `'menu'`. |
|
|
126
128
|
| `footerSecondaryAction` | `{ icon, href?, onClick?, ariaLabel, pulse? }` | Optional secondary icon button inside the footer trigger (e.g. "Get apps" with pulsing dot). |
|
|
127
129
|
|
|
128
130
|
The footer button opens a popover (`DropdownMenu`, `side="top"`) with: email, account links, **Language** (opens fullscreen `LocaleSwitcherDialog` if `LayoutI18nProvider` is mounted), **Theme** (cycles `light → dark → system`), and **Log out**. In dev mode the footer renders a `Guest (dev)` placeholder when there's no authenticated user, so debug controls stay reachable; in production the block hides itself.
|
|
129
131
|
|
|
130
132
|
---
|
|
131
133
|
|
|
134
|
+
## Brand switcher
|
|
135
|
+
|
|
136
|
+
Replace the static brand header with a workspace/account/project switcher:
|
|
137
|
+
|
|
138
|
+
```tsx
|
|
139
|
+
import { SidebarBrandSwitcherConfig } from '@djangocfg/layouts';
|
|
140
|
+
|
|
141
|
+
const switcher: SidebarBrandSwitcherConfig = {
|
|
142
|
+
items: [
|
|
143
|
+
{ label: 'My Workspace', description: 'Personal', href: '/', active: true },
|
|
144
|
+
{ label: 'Team Workspace', description: 'Pro plan', href: '/team' },
|
|
145
|
+
{ label: 'Client Project', onSelect: () => switchProject('client') },
|
|
146
|
+
],
|
|
147
|
+
addLabel: 'Add workspace',
|
|
148
|
+
onAdd: () => openCreateDialog(),
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
<PrivateLayout header={{ switcher }} ...>
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
`SidebarBrandSwitcherConfig`:
|
|
155
|
+
|
|
156
|
+
| Field | Type | Notes |
|
|
157
|
+
|---|---|---|
|
|
158
|
+
| `items` | `SidebarBrandSwitcherItem[]` | List of workspaces / accounts / projects. |
|
|
159
|
+
| `addLabel` | `string` | Label for the "add new" action at the bottom. Omit to hide. |
|
|
160
|
+
| `onAdd` | `() => void` | Called when "add new" is clicked. |
|
|
161
|
+
|
|
162
|
+
`SidebarBrandSwitcherItem`:
|
|
163
|
+
|
|
164
|
+
| Field | Type | Notes |
|
|
165
|
+
|---|---|---|
|
|
166
|
+
| `label` | `string` | Display name. |
|
|
167
|
+
| `avatar` | `string` | Image URL. Falls back to `monogram` / first letter of `label`. |
|
|
168
|
+
| `monogram` | `string` | Single letter override for the avatar fallback. |
|
|
169
|
+
| `description` | `string` | Secondary line under the label (plan, role, etc.). |
|
|
170
|
+
| `href` | `string` | Navigate on select. |
|
|
171
|
+
| `onSelect` | `() => void` | Callback on select (alternative to `href`). |
|
|
172
|
+
| `active` | `boolean` | Mark as currently selected (shows checkmark in dropdown). |
|
|
173
|
+
|
|
174
|
+
On the collapsed icon-rail the switcher renders only the active item's avatar — no dropdown trigger.
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
132
178
|
## Sample
|
|
133
179
|
|
|
134
180
|
```tsx
|
|
@@ -1,10 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Private Sidebar
|
|
3
|
-
*
|
|
4
|
-
* Composed from smaller components: SidebarBrand, SidebarNavGroup, SidebarSlots.
|
|
5
|
-
* Uses PrivateLayoutContext for all UI state.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
1
|
'use client';
|
|
9
2
|
|
|
10
3
|
import React from 'react';
|
|
@@ -19,7 +12,7 @@ import { cn } from '@djangocfg/ui-core/lib';
|
|
|
19
12
|
|
|
20
13
|
import { PrivateSidebarAccount } from './PrivateSidebarAccount';
|
|
21
14
|
import { PrivateLayoutProvider } from '../context';
|
|
22
|
-
import {
|
|
15
|
+
import { useShellVisualState, useSidebarKeyboard } from '../hooks';
|
|
23
16
|
import type { HeaderConfig, SidebarConfig } from '../types';
|
|
24
17
|
import { SidebarBrand } from './SidebarBrand';
|
|
25
18
|
import { SidebarNavGroup } from './SidebarNavGroup';
|
|
@@ -29,11 +22,6 @@ interface PrivateSidebarProps {
|
|
|
29
22
|
sidebar: SidebarConfig;
|
|
30
23
|
header?: HeaderConfig;
|
|
31
24
|
pathname?: string;
|
|
32
|
-
/**
|
|
33
|
-
* shadcn-sidebar `variant`. Used to trigger the inset/boxed visual:
|
|
34
|
-
* `'inset'` makes the sidebar wrapper paint `bg-sidebar` and lets `SidebarInset`
|
|
35
|
-
* float as a rounded card. Default `'sidebar'` (full-bleed).
|
|
36
|
-
*/
|
|
37
25
|
variant?: 'sidebar' | 'inset';
|
|
38
26
|
}
|
|
39
27
|
|
|
@@ -43,18 +31,13 @@ export function PrivateSidebar({
|
|
|
43
31
|
pathname: pathnameProp,
|
|
44
32
|
variant = 'sidebar',
|
|
45
33
|
}: PrivateSidebarProps) {
|
|
46
|
-
const { state, isMobile, setOpenMobile
|
|
34
|
+
const { state, isMobile, setOpenMobile } = useSidebar();
|
|
47
35
|
const pathname = pathnameProp ?? '';
|
|
48
36
|
|
|
49
37
|
React.useEffect(() => {
|
|
50
38
|
if (isMobile) setOpenMobile(false);
|
|
51
39
|
}, [pathname, isMobile, setOpenMobile]);
|
|
52
40
|
|
|
53
|
-
const collapsedRail = !isMobile && state === 'collapsed';
|
|
54
|
-
const { isHoverExpanded, onMouseEnter, onMouseLeave } = useHoverExpand({
|
|
55
|
-
enabled: collapsedRail,
|
|
56
|
-
});
|
|
57
|
-
|
|
58
41
|
return (
|
|
59
42
|
<PrivateLayoutProvider
|
|
60
43
|
sidebar={sidebar}
|
|
@@ -62,86 +45,39 @@ export function PrivateSidebar({
|
|
|
62
45
|
pathname={pathname}
|
|
63
46
|
isMobile={isMobile}
|
|
64
47
|
state={state}
|
|
65
|
-
isHoverExpanded={isHoverExpanded}
|
|
66
48
|
>
|
|
67
49
|
<PrivateSidebarInner
|
|
68
50
|
sidebar={sidebar}
|
|
69
51
|
header={header}
|
|
70
52
|
variant={variant}
|
|
71
|
-
collapsedRail={collapsedRail}
|
|
72
|
-
setOpen={setOpen}
|
|
73
|
-
onMouseEnter={onMouseEnter}
|
|
74
|
-
onMouseLeave={onMouseLeave}
|
|
75
53
|
/>
|
|
76
54
|
</PrivateLayoutProvider>
|
|
77
55
|
);
|
|
78
56
|
}
|
|
79
57
|
|
|
80
|
-
// ---------------------------------------------------------------------------
|
|
81
|
-
// Inner component — runs inside PrivateLayoutProvider so useShellVisualState
|
|
82
|
-
// can safely consume the context.
|
|
83
|
-
// ---------------------------------------------------------------------------
|
|
84
|
-
|
|
85
58
|
interface PrivateSidebarInnerProps {
|
|
86
59
|
sidebar: SidebarConfig;
|
|
87
60
|
header?: HeaderConfig;
|
|
88
61
|
variant: 'sidebar' | 'inset';
|
|
89
|
-
collapsedRail: boolean;
|
|
90
|
-
setOpen: (open: boolean) => void;
|
|
91
|
-
onMouseEnter: () => void;
|
|
92
|
-
onMouseLeave: () => void;
|
|
93
62
|
}
|
|
94
63
|
|
|
95
|
-
function PrivateSidebarInner({
|
|
96
|
-
sidebar,
|
|
97
|
-
header,
|
|
98
|
-
variant,
|
|
99
|
-
collapsedRail,
|
|
100
|
-
setOpen,
|
|
101
|
-
onMouseEnter,
|
|
102
|
-
onMouseLeave,
|
|
103
|
-
}: PrivateSidebarInnerProps) {
|
|
64
|
+
function PrivateSidebarInner({ sidebar, header, variant }: PrivateSidebarInnerProps) {
|
|
104
65
|
const layoutVariant = variant === 'inset' ? 'boxed' : 'full-bleed';
|
|
105
66
|
const { modifiers } = useShellVisualState(layoutVariant);
|
|
106
67
|
const { setSidebarRef, handleSidebarKeyDown } = useSidebarKeyboard();
|
|
107
68
|
|
|
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
|
-
*/
|
|
115
|
-
const expandOnRailClick = React.useCallback(
|
|
116
|
-
(event: React.MouseEvent<HTMLDivElement>) => {
|
|
117
|
-
const interactive = (event.target as Element | null)?.closest(
|
|
118
|
-
'a, button, [role="menuitem"], [data-no-expand]',
|
|
119
|
-
);
|
|
120
|
-
if (interactive) return;
|
|
121
|
-
setOpen(true);
|
|
122
|
-
},
|
|
123
|
-
[setOpen],
|
|
124
|
-
);
|
|
125
|
-
|
|
126
|
-
const railExpandHintClass = collapsedRail ? 'cursor-pointer' : undefined;
|
|
127
|
-
|
|
128
69
|
const sidebarRootClass = React.useMemo(
|
|
129
70
|
() =>
|
|
130
71
|
cn(
|
|
131
|
-
railExpandHintClass,
|
|
132
72
|
'[&>[data-sidebar=sidebar]]:bg-gradient-to-t [&>[data-sidebar=sidebar]]:from-sidebar/85 [&>[data-sidebar=sidebar]]:to-sidebar',
|
|
133
73
|
modifiers.sidebarRoot,
|
|
134
74
|
modifiers.sidebarInner.map((m) => `[&>[data-sidebar=sidebar]]:${m}`),
|
|
135
75
|
),
|
|
136
|
-
[
|
|
76
|
+
[modifiers],
|
|
137
77
|
);
|
|
138
78
|
|
|
139
79
|
const sidebarContentClass = React.useMemo(
|
|
140
|
-
() =>
|
|
141
|
-
cn(
|
|
142
|
-
'gap-2',
|
|
143
|
-
modifiers.sidebarContent,
|
|
144
|
-
),
|
|
80
|
+
() => cn('gap-2', modifiers.sidebarContent),
|
|
145
81
|
[modifiers.sidebarContent],
|
|
146
82
|
);
|
|
147
83
|
|
|
@@ -165,9 +101,6 @@ function PrivateSidebarInner({
|
|
|
165
101
|
collapsible="icon"
|
|
166
102
|
variant={variant}
|
|
167
103
|
className={sidebarRootClass}
|
|
168
|
-
onClick={collapsedRail ? expandOnRailClick : undefined}
|
|
169
|
-
onMouseEnter={onMouseEnter}
|
|
170
|
-
onMouseLeave={onMouseLeave}
|
|
171
104
|
onKeyDown={handleSidebarKeyDown}
|
|
172
105
|
>
|
|
173
106
|
<SidebarBrand />
|
|
@@ -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,
|
|
4
|
-
*
|
|
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 {
|
|
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,12 @@ 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
|
-
import {
|
|
39
|
+
import { useProfileDialogStore } from '../../ProfileLayout/ProfileDialog/store';
|
|
42
40
|
|
|
43
41
|
import type { HeaderConfig } from '../types';
|
|
44
42
|
|
|
@@ -62,8 +60,6 @@ function PrivateSidebarAccountRaw({ header }: PrivateSidebarAccountProps) {
|
|
|
62
60
|
const { setOpen: setSidebarOpen } = useSidebar();
|
|
63
61
|
const { content } = useShellVisualState();
|
|
64
62
|
const [isAccountMenuOpen, setIsAccountMenuOpen] = React.useState(false);
|
|
65
|
-
const { theme, setTheme } = useThemeContext();
|
|
66
|
-
const [langDialogOpen, setLangDialogOpen] = React.useState(false);
|
|
67
63
|
|
|
68
64
|
const signOutLabel = t('layouts.profile.signOut');
|
|
69
65
|
|
|
@@ -110,21 +106,6 @@ function PrivateSidebarAccountRaw({ header }: PrivateSidebarAccountProps) {
|
|
|
110
106
|
handleLogout();
|
|
111
107
|
}, [handleLogout]);
|
|
112
108
|
|
|
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
109
|
// Hide entirely in production when there's no user (auth still loading or
|
|
129
110
|
// /me failed and the parent guard hasn't redirected yet). In dev keep a
|
|
130
111
|
// placeholder so the footer + Log out are reachable for debugging.
|
|
@@ -135,7 +116,7 @@ function PrivateSidebarAccountRaw({ header }: PrivateSidebarAccountProps) {
|
|
|
135
116
|
const secondary = header?.footerSecondaryAction;
|
|
136
117
|
|
|
137
118
|
const triggerClassName = cn(
|
|
138
|
-
'group h-auto w-full gap-3 rounded-
|
|
119
|
+
'group h-auto w-full gap-3 rounded-lg px-3 py-3 text-left',
|
|
139
120
|
'hover:bg-sidebar-accent/70 hover:text-sidebar-accent-foreground',
|
|
140
121
|
content.isAccountCompact ? 'justify-center px-0 py-2' : 'min-h-[52px]',
|
|
141
122
|
);
|
|
@@ -182,40 +163,6 @@ function PrivateSidebarAccountRaw({ header }: PrivateSidebarAccountProps) {
|
|
|
182
163
|
</>
|
|
183
164
|
) : null;
|
|
184
165
|
|
|
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
166
|
const expandedMeta = content.isAccountCompact ? null : (
|
|
220
167
|
<>
|
|
221
168
|
<span className="flex min-w-0 flex-1 flex-col text-left">
|
|
@@ -230,35 +177,55 @@ function PrivateSidebarAccountRaw({ header }: PrivateSidebarAccountProps) {
|
|
|
230
177
|
</span>
|
|
231
178
|
<span className="flex shrink-0 items-center gap-1.5">
|
|
232
179
|
{secondaryButton}
|
|
233
|
-
|
|
180
|
+
{header?.accountAction !== 'dialog' ? (
|
|
181
|
+
<ChevronsUpDown className="h-3.5 w-3.5 text-sidebar-foreground/55" aria-hidden />
|
|
182
|
+
) : null}
|
|
234
183
|
</span>
|
|
235
184
|
</>
|
|
236
185
|
);
|
|
237
186
|
|
|
187
|
+
const openProfileDialog = React.useCallback(() => {
|
|
188
|
+
useProfileDialogStore.getState().open();
|
|
189
|
+
}, []);
|
|
190
|
+
|
|
191
|
+
const triggerButton = (
|
|
192
|
+
<Button
|
|
193
|
+
type="button"
|
|
194
|
+
variant="ghost"
|
|
195
|
+
aria-label={content.isAccountCompact ? account.displayName : undefined}
|
|
196
|
+
className={triggerClassName}
|
|
197
|
+
onClick={header?.accountAction === 'dialog' ? openProfileDialog : onTriggerInteract}
|
|
198
|
+
>
|
|
199
|
+
<Avatar className={avatarClass}>
|
|
200
|
+
<AvatarImage src={account.avatarUrl} alt={account.displayName} />
|
|
201
|
+
<AvatarFallback className="text-sm font-semibold">{userInitial}</AvatarFallback>
|
|
202
|
+
</Avatar>
|
|
203
|
+
{expandedMeta}
|
|
204
|
+
</Button>
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
const wrapperClass = cn(
|
|
208
|
+
'w-full min-w-0',
|
|
209
|
+
content.isAccountCompact ? 'px-0 pb-0' : 'px-2 pb-2',
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
// Dialog mode: simple button that opens the global ProfileDialog
|
|
213
|
+
if (header?.accountAction === 'dialog') {
|
|
214
|
+
return (
|
|
215
|
+
<div className={wrapperClass}>
|
|
216
|
+
{triggerButton}
|
|
217
|
+
</div>
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
|
|
238
221
|
return (
|
|
239
|
-
<div className=
|
|
222
|
+
<div className={wrapperClass}>
|
|
240
223
|
<DropdownMenu
|
|
241
224
|
open={isAccountMenuOpen}
|
|
242
|
-
onOpenChange={
|
|
243
|
-
setIsAccountMenuOpen(open);
|
|
244
|
-
if (open) blockSidebarCollapse();
|
|
245
|
-
else allowSidebarCollapse();
|
|
246
|
-
}}
|
|
225
|
+
onOpenChange={setIsAccountMenuOpen}
|
|
247
226
|
>
|
|
248
227
|
<DropdownMenuTrigger asChild>
|
|
249
|
-
|
|
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>
|
|
228
|
+
{triggerButton}
|
|
262
229
|
</DropdownMenuTrigger>
|
|
263
230
|
|
|
264
231
|
<DropdownMenuContent
|
|
@@ -270,10 +237,6 @@ function PrivateSidebarAccountRaw({ header }: PrivateSidebarAccountProps) {
|
|
|
270
237
|
{headerLabel}
|
|
271
238
|
{accountLinksBlock}
|
|
272
239
|
|
|
273
|
-
<DropdownMenuSeparator />
|
|
274
|
-
{languageItem}
|
|
275
|
-
{themeItem}
|
|
276
|
-
|
|
277
240
|
<DropdownMenuSeparator />
|
|
278
241
|
<DropdownMenuItem
|
|
279
242
|
onSelect={onLogoutSelect}
|
|
@@ -284,25 +247,13 @@ function PrivateSidebarAccountRaw({ header }: PrivateSidebarAccountProps) {
|
|
|
284
247
|
</DropdownMenuItem>
|
|
285
248
|
</DropdownMenuContent>
|
|
286
249
|
</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
250
|
</div>
|
|
300
251
|
);
|
|
301
252
|
}
|
|
302
253
|
|
|
303
254
|
/**
|
|
304
255
|
* Memoised account footer. Re-renders only when the `header` prop reference
|
|
305
|
-
* changes. Internal reactive data (user from useAuth,
|
|
256
|
+
* changes. Internal reactive data (user from useAuth, locale) are
|
|
306
257
|
* consumed via hooks and still update independently.
|
|
307
258
|
*/
|
|
308
259
|
export const PrivateSidebarAccount = memo(PrivateSidebarAccountRaw);
|