@djangocfg/layouts 2.1.591 → 2.1.593
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 +12 -12
- package/src/components/errors/ErrorLayout.tsx +1 -164
- package/src/components/errors/ErrorsTracker/providers/ErrorTrackingProvider.tsx +1 -100
- package/src/components/errors/ErrorsTracker/providers/errorDedupe.ts +101 -0
- package/src/components/errors/errorGlyphs.tsx +167 -0
- package/src/layouts/AppLayout/BaseApp.tsx +1 -1
- package/src/layouts/AppLayout/index.ts +2 -2
- package/src/layouts/AuthLayout/AuthLayout.tsx +3 -23
- package/src/layouts/AuthLayout/components/shared/AuthTopBar.tsx +1 -1
- package/src/layouts/AuthLayout/components/steps/IdentifierStep.tsx +1 -1
- package/src/layouts/AuthLayout/layoutContext.ts +25 -0
- package/src/layouts/AuthLayout/styles/split-shell.css +0 -1
- package/src/layouts/PrivateLayout/PrivateLayout.tsx +37 -26
- package/src/layouts/PrivateLayout/README.md +20 -12
- package/src/layouts/PrivateLayout/components/PrivateContent.tsx +29 -42
- package/src/layouts/PrivateLayout/components/PrivateSidebar.tsx +23 -92
- package/src/layouts/PrivateLayout/components/PrivateSidebarAccount.tsx +41 -112
- package/src/layouts/PrivateLayout/components/SidebarBody.tsx +45 -0
- package/src/layouts/PrivateLayout/components/SidebarBrand.tsx +64 -152
- package/src/layouts/PrivateLayout/components/SidebarBrandSwitcher.tsx +2 -30
- package/src/layouts/PrivateLayout/components/SidebarNavGroup.tsx +55 -115
- package/src/layouts/PrivateLayout/components/SidebarNavItem.tsx +37 -100
- package/src/layouts/PrivateLayout/components/SidebarPeek.tsx +34 -0
- package/src/layouts/PrivateLayout/components/SidebarResizeHandle.tsx +76 -0
- package/src/layouts/PrivateLayout/components/SidebarSlots.tsx +14 -53
- package/src/layouts/PrivateLayout/components/index.ts +0 -4
- package/src/layouts/PrivateLayout/context.tsx +10 -115
- package/src/layouts/PrivateLayout/geometry.ts +17 -0
- package/src/layouts/PrivateLayout/hooks/index.ts +2 -1
- package/src/layouts/PrivateLayout/hooks/useLayoutVisual.ts +4 -4
- package/src/layouts/PrivateLayout/hooks/useSidebarKeyboard.ts +33 -93
- package/src/layouts/PrivateLayout/hooks/useSidebarPeek.tsx +151 -0
- package/src/layouts/PrivateLayout/hooks/useSidebarWidth.ts +83 -0
- package/src/layouts/PrivateLayout/types.ts +13 -20
- package/src/layouts/PublicLayout/footers/DefaultFooter/DefaultFooter.tsx +3 -89
- package/src/layouts/PublicLayout/footers/DefaultFooter/ThemeModeControl.tsx +90 -0
- package/src/layouts/PublicLayout/primitives/DesktopDropdownPortal.tsx +47 -0
- package/src/layouts/PublicLayout/primitives/NavControls.tsx +1 -1
- package/src/layouts/PublicLayout/primitives/NavDesktopItems.tsx +2 -83
- package/src/layouts/PublicLayout/primitives/navDesktopStyles.ts +42 -0
- package/src/layouts/PublicLayout/shared/NavbarShell.tsx +4 -171
- package/src/layouts/PublicLayout/shared/index.ts +1 -1
- package/src/layouts/PublicLayout/shared/navbarShell.types.ts +109 -0
- package/src/layouts/PublicLayout/shared/navbarShellStyles.ts +69 -0
- package/src/layouts/SettingsLayout/README.md +23 -10
- package/src/layouts/SettingsLayout/SettingsDialog.tsx +27 -11
- package/src/layouts/SettingsLayout/SettingsForm.tsx +13 -9
- package/src/layouts/SettingsLayout/components/AccountMenuButton.tsx +83 -0
- package/src/layouts/SettingsLayout/components/ApiKeySection/ApiKeySection.tsx +9 -7
- package/src/layouts/SettingsLayout/components/ApiKeySection/context.tsx +6 -41
- package/src/layouts/SettingsLayout/components/AvatarSection.tsx +11 -3
- package/src/layouts/SettingsLayout/components/SettingsNav.tsx +5 -9
- package/src/layouts/SettingsLayout/components/SettingsNavItem.tsx +5 -7
- package/src/layouts/SettingsLayout/components/SettingsPanel.tsx +5 -10
- package/src/layouts/SettingsLayout/components/SettingsSearch.tsx +5 -3
- package/src/layouts/SettingsLayout/components/SettingsShell.tsx +14 -21
- package/src/layouts/SettingsLayout/components/TwoFactorSection/TwoFactorSection.tsx +63 -131
- package/src/layouts/SettingsLayout/components/TwoFactorSection/TwoFactorSetupDialog.tsx +35 -0
- package/src/layouts/SettingsLayout/components/index.ts +0 -1
- package/src/layouts/SettingsLayout/context/SettingsContext.tsx +5 -2
- package/src/layouts/SettingsLayout/hooks/index.ts +3 -0
- package/src/layouts/SettingsLayout/hooks/useAccountMenuItems.ts +69 -0
- package/src/layouts/SettingsLayout/hooks/useSettingsDialog.ts +6 -3
- package/src/layouts/SettingsLayout/i18n/index.ts +39 -0
- package/src/layouts/SettingsLayout/i18n/locales/ar.ts +66 -0
- package/src/layouts/SettingsLayout/i18n/locales/da.ts +66 -0
- package/src/layouts/SettingsLayout/i18n/locales/de.ts +66 -0
- package/src/layouts/SettingsLayout/i18n/locales/en.ts +66 -0
- package/src/layouts/SettingsLayout/i18n/locales/es.ts +66 -0
- package/src/layouts/SettingsLayout/i18n/locales/fr.ts +66 -0
- package/src/layouts/SettingsLayout/i18n/locales/it.ts +66 -0
- package/src/layouts/SettingsLayout/i18n/locales/ja.ts +66 -0
- package/src/layouts/SettingsLayout/i18n/locales/ko.ts +66 -0
- package/src/layouts/SettingsLayout/i18n/locales/nl.ts +66 -0
- package/src/layouts/SettingsLayout/i18n/locales/no.ts +66 -0
- package/src/layouts/SettingsLayout/i18n/locales/pl.ts +66 -0
- package/src/layouts/SettingsLayout/i18n/locales/ptBR.ts +66 -0
- package/src/layouts/SettingsLayout/i18n/locales/ru.ts +66 -0
- package/src/layouts/SettingsLayout/i18n/locales/sv.ts +66 -0
- package/src/layouts/SettingsLayout/i18n/locales/tr.ts +66 -0
- package/src/layouts/SettingsLayout/i18n/locales/zh.ts +66 -0
- package/src/layouts/SettingsLayout/i18n/types.ts +65 -0
- package/src/layouts/SettingsLayout/index.ts +3 -1
- package/src/layouts/SettingsLayout/sections/AccountSection.tsx +9 -4
- package/src/layouts/SettingsLayout/sections/DeleteAccountRow.tsx +4 -1
- package/src/layouts/SettingsLayout/sections/GeneralSection.tsx +25 -0
- package/src/layouts/SettingsLayout/sections/PreferencesRows.tsx +13 -5
- package/src/layouts/SettingsLayout/sections/builtins.tsx +34 -29
- package/src/layouts/SettingsLayout/sections/index.ts +2 -2
- package/src/layouts/SettingsLayout/store.ts +12 -0
- package/src/layouts/SettingsLayout/types.ts +12 -4
- package/src/layouts/_components/locale-switcher/LocaleSwitcher.tsx +1 -1
- package/src/layouts/types/layout.types.ts +5 -6
- package/src/{layouts/AppLayout → shared}/LayoutI18nProvider.tsx +1 -1
- package/src/styles/dashboard.css +10 -0
- package/src/layouts/PrivateLayout/density.ts +0 -48
- package/src/layouts/PrivateLayout/hooks/useHoverExpand.ts +0 -110
- package/src/layouts/PrivateLayout/hooks/useShellVisualState.ts +0 -130
- package/src/layouts/SettingsLayout/components/SettingsRailFooter.tsx +0 -74
- package/src/layouts/SettingsLayout/sections/SecuritySection.tsx +0 -15
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { createContext, useContext } from 'react';
|
|
4
|
+
|
|
5
|
+
export interface AuthLayoutContextValue {
|
|
6
|
+
/** True when the consumer passed custom children — step headers should be hidden */
|
|
7
|
+
hideHeader: boolean;
|
|
8
|
+
/**
|
|
9
|
+
* True when the brand is already rendered on the media half, so the form
|
|
10
|
+
* header must not repeat it.
|
|
11
|
+
*
|
|
12
|
+
* A statement about the DESKTOP layout only: `fullsplit` drops the media half
|
|
13
|
+
* below 1024px, where the logo above the form is the only brand left. CSS
|
|
14
|
+
* owns that half of the decision on the same breakpoint — see
|
|
15
|
+
* `.auth-header[data-brand-elsewhere]` in `auth.css`.
|
|
16
|
+
*/
|
|
17
|
+
hasBrandPanel: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const AuthLayoutContext = createContext<AuthLayoutContextValue>({
|
|
21
|
+
hideHeader: false,
|
|
22
|
+
hasBrandPanel: false,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
export const useAuthLayoutContext = (): AuthLayoutContextValue => useContext(AuthLayoutContext);
|
|
@@ -88,7 +88,6 @@
|
|
|
88
88
|
through the panel as frosted glass. color-mix keeps it theme-aware. */
|
|
89
89
|
background: color-mix(in oklab, var(--card) 72%, transparent);
|
|
90
90
|
backdrop-filter: blur(20px) saturate(140%);
|
|
91
|
-
-webkit-backdrop-filter: blur(20px) saturate(140%);
|
|
92
91
|
color: var(--card-foreground);
|
|
93
92
|
outline: 1px solid color-mix(in oklab, var(--border) 80%, transparent);
|
|
94
93
|
box-shadow: 0 24px 64px hsl(0 0% 0% / 0.28);
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Private Layout
|
|
3
3
|
*
|
|
4
|
-
* Authenticated shell:
|
|
5
|
-
*
|
|
6
|
-
* Ctrl/Cmd + B
|
|
4
|
+
* Authenticated shell: a docked, resizable sidebar + scrollable content.
|
|
5
|
+
* Collapsed, the sidebar leaves a trigger whose hover zone peeks it open over the page;
|
|
6
|
+
* on narrow viewports the same trigger opens a drawer. Ctrl/Cmd + B toggles it.
|
|
7
7
|
*
|
|
8
8
|
* Guarded: no session redirects to `header.authPath`. With `requireStaff`, a
|
|
9
9
|
* signed-in non-staff user gets `ForbiddenState` instead — never a redirect,
|
|
@@ -18,9 +18,14 @@ import { Preloader } from '@djangocfg/ui-core/components';
|
|
|
18
18
|
import { SidebarInset, SidebarProvider } from '@djangocfg/ui-core/components';
|
|
19
19
|
|
|
20
20
|
import { ForbiddenState, PrivateContent, PrivateSidebar } from './components';
|
|
21
|
-
import {
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
import {
|
|
22
|
+
SidebarPeekProvider,
|
|
23
|
+
SidebarWidthContext,
|
|
24
|
+
useAuthGuard,
|
|
25
|
+
useLayoutVisual,
|
|
26
|
+
useSidebarDefaultOpen,
|
|
27
|
+
useSidebarWidth,
|
|
28
|
+
} from './hooks';
|
|
24
29
|
|
|
25
30
|
import type { PrivateLayoutProps } from './types';
|
|
26
31
|
|
|
@@ -67,6 +72,7 @@ export function PrivateLayout({
|
|
|
67
72
|
useLayoutVisual(visual);
|
|
68
73
|
|
|
69
74
|
const defaultOpen = useSidebarDefaultOpen();
|
|
75
|
+
const sidebarWidth = useSidebarWidth(Boolean(sidebar) && sidebar?.resizable !== false);
|
|
70
76
|
|
|
71
77
|
if (isLoading) {
|
|
72
78
|
return (
|
|
@@ -90,28 +96,33 @@ export function PrivateLayout({
|
|
|
90
96
|
return (
|
|
91
97
|
<SidebarProvider
|
|
92
98
|
defaultOpen={defaultOpen}
|
|
93
|
-
style={providerStyle}
|
|
99
|
+
style={{ ...providerStyle, ...sidebarWidth.style }}
|
|
94
100
|
className={providerClassName}
|
|
101
|
+
data-sidebar-resizing={sidebarWidth.dragging || undefined}
|
|
95
102
|
>
|
|
96
|
-
{
|
|
97
|
-
<
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
103
|
+
<SidebarWidthContext.Provider value={sidebarWidth}>
|
|
104
|
+
<SidebarPeekProvider pathname={pathname ?? ''}>
|
|
105
|
+
{sidebar && (
|
|
106
|
+
<PrivateSidebar
|
|
107
|
+
sidebar={sidebar}
|
|
108
|
+
header={header}
|
|
109
|
+
pathname={pathname}
|
|
110
|
+
variant={sidebarVariant}
|
|
111
|
+
/>
|
|
112
|
+
)}
|
|
113
|
+
|
|
114
|
+
<SidebarInset className={insetClassName}>
|
|
115
|
+
<PrivateContent
|
|
116
|
+
padding={contentPadding}
|
|
117
|
+
scroll={contentScroll}
|
|
118
|
+
hasSidebar={Boolean(sidebar)}
|
|
119
|
+
visual={visual}
|
|
120
|
+
>
|
|
121
|
+
{children}
|
|
122
|
+
</PrivateContent>
|
|
123
|
+
</SidebarInset>
|
|
124
|
+
</SidebarPeekProvider>
|
|
125
|
+
</SidebarWidthContext.Provider>
|
|
115
126
|
|
|
116
127
|
{/* Settings is the primary account dialog — always mounted so the
|
|
117
128
|
account-menu "Settings" item works out of the box. `settings` is
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# PrivateLayout
|
|
2
2
|
|
|
3
|
-
Authenticated app shell —
|
|
3
|
+
Authenticated app shell — a docked, resizable sidebar + scrollable content area. Collapsed, a trigger remains and hovering it peeks the sidebar open over the page; on mobile it is a drawer.
|
|
4
4
|
|
|
5
5
|
```tsx
|
|
6
6
|
<PrivateLayout
|
|
@@ -81,13 +81,13 @@ thin-wrapper reference, and the **BaseApp README** for the provider root.
|
|
|
81
81
|
|
|
82
82
|
## Visual variants
|
|
83
83
|
|
|
84
|
-
`boxed`
|
|
84
|
+
`boxed` — opt in with `visual={{ variant: 'boxed' }}`. `<SidebarInset>` becomes a rounded card; the wrapper paints `bg-sidebar` so the brand colour bleeds to the viewport edges. Mobile (<md) degrades to full-bleed automatically.
|
|
85
85
|
|
|
86
|
-
`full-bleed` —
|
|
86
|
+
`full-bleed` (default) — flat, Claude-style: sidebar, one 1px border, page.
|
|
87
87
|
|
|
88
88
|
| Field | Type | Default | Notes |
|
|
89
89
|
|---|---|---|---|
|
|
90
|
-
| `variant` | `'full-bleed' \| 'boxed'` | `'
|
|
90
|
+
| `variant` | `'full-bleed' \| 'boxed'` | `'full-bleed'` | Switch between the two shells. |
|
|
91
91
|
| `inset` | `number \| { x?: number; y?: number }` | `12` | Gap (px) between the card and the viewport edges (md+). |
|
|
92
92
|
| `radius` | `'sm' \| 'md' \| 'lg' \| 'xl' \| '2xl' \| '3xl'` | `'2xl'` | Card corner radius. |
|
|
93
93
|
| `background` | `'sidebar' \| 'muted' \| 'card' \| 'background'` | `'sidebar'` | Canvas colour painted *behind* the boxed card. |
|
|
@@ -139,10 +139,11 @@ For pages like Kanban boards where the shell must be exactly one viewport tall a
|
|
|
139
139
|
| `groups` | `SidebarGroupConfig[]` | Nav items grouped under labels. Set `group.label = ''` to render flat (no header). |
|
|
140
140
|
| `homeHref` | `string` | Brand link target. |
|
|
141
141
|
| `menuStart` / `menuEnd` | `ReactNode` | Free slots above / below `groups` in the scrollable column. |
|
|
142
|
-
| `menuStartShowOnCollapsed` / `menuEndShowOnCollapsed` | `boolean` |
|
|
142
|
+
| `menuStartShowOnCollapsed` / `menuEndShowOnCollapsed` | `boolean` | Deprecated, no effect (no icon rail). |
|
|
143
|
+
| `resizable` | `boolean` | Drag the sidebar edge (14–24rem, remembered under `cfg.layouts.sidebar-width`; double-click resets). Default `true`. |
|
|
143
144
|
| `footer` | `ReactNode` | Custom footer node above the account block. |
|
|
144
|
-
| `activeIndicator` |
|
|
145
|
-
| `groupLabelStyle` |
|
|
145
|
+
| `activeIndicator` | — | Deprecated, no effect. |
|
|
146
|
+
| `groupLabelStyle` | — | Deprecated, no effect: labels are sentence case. |
|
|
146
147
|
| `featured` | `SidebarFeaturedConfig` | Accent-tinted CTA tile rendered below groups (Mailersend-style). |
|
|
147
148
|
|
|
148
149
|
`SidebarGroupConfig`:
|
|
@@ -152,7 +153,7 @@ For pages like Kanban boards where the shell must be exactly one viewport tall a
|
|
|
152
153
|
| `label` | `string` | Group title. Empty string → flat group (no header). |
|
|
153
154
|
| `items` | `SidebarItem[]` | Nav links. |
|
|
154
155
|
| `dynamic` | `boolean` | Hide group when `items` is empty (for extension-driven groups). |
|
|
155
|
-
| `collapsible` | `boolean` | Render as accordion. Label becomes a clickable trigger; auto-expanded if a child is active.
|
|
156
|
+
| `collapsible` | `boolean` | Render as accordion. Label becomes a clickable trigger; auto-expanded if a child is active. |
|
|
156
157
|
| `defaultOpen` | `boolean` | Initial open state for collapsible groups. |
|
|
157
158
|
| `icon` | `string \| LucideIcon` | Icon for the trigger (collapsible only). |
|
|
158
159
|
| `hideItemIcons` | `boolean` | Hide per-item icons inside the group. Defaults to `true` when `collapsible`. |
|
|
@@ -186,7 +187,7 @@ For pages like Kanban boards where the shell must be exactly one viewport tall a
|
|
|
186
187
|
| `groups` | `UserMenuGroup[]` | Account links rendered inside the footer dropdown. |
|
|
187
188
|
| `authPath` | `string` | Sign-in redirect target. |
|
|
188
189
|
| `userPlan` | `string` | Subtitle under the display name (e.g. `"Max plan"`). |
|
|
189
|
-
| `accountAction` |
|
|
190
|
+
| `accountAction` | — | Deprecated, no effect: the account footer always opens the menu. |
|
|
190
191
|
| `footerSecondaryAction` | `{ icon, href?, onClick?, ariaLabel, pulse? }` | Optional secondary icon button inside the footer trigger (e.g. "Get apps" with pulsing dot). |
|
|
191
192
|
|
|
192
193
|
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.
|
|
@@ -233,7 +234,7 @@ const switcher: SidebarBrandSwitcherConfig = {
|
|
|
233
234
|
| `onSelect` | `() => void` | Callback on select (alternative to `href`). |
|
|
234
235
|
| `active` | `boolean` | Mark as currently selected (shows checkmark in dropdown). |
|
|
235
236
|
|
|
236
|
-
|
|
237
|
+
While the sidebar is peeked open the switcher menu keeps the panel open until it closes.
|
|
237
238
|
|
|
238
239
|
---
|
|
239
240
|
|
|
@@ -242,8 +243,6 @@ On the collapsed icon-rail the switcher renders only the active item's avatar
|
|
|
242
243
|
```tsx
|
|
243
244
|
const sidebar: SidebarConfig = {
|
|
244
245
|
homeHref: '/',
|
|
245
|
-
activeIndicator: 'rail',
|
|
246
|
-
groupLabelStyle: 'plain',
|
|
247
246
|
groups: [
|
|
248
247
|
{
|
|
249
248
|
label: 'Email',
|
|
@@ -269,3 +268,12 @@ const header: HeaderConfig = {
|
|
|
269
268
|
groups: [{ title: 'Account', items: [{ label: 'Settings', href: '/settings' }] }],
|
|
270
269
|
};
|
|
271
270
|
```
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
## Sidebar states
|
|
274
|
+
|
|
275
|
+
| State | Behaviour |
|
|
276
|
+
|---|---|
|
|
277
|
+
| Docked | Width from `--sidebar-width`; drag the right edge, arrow keys, double-click to reset. |
|
|
278
|
+
| Collapsed (desktop) | Hidden. A trigger stays top-left; hovering it (or the free area around it) floats the sidebar over the page. The panel stays while the pointer is on it or a menu opened from it is open; Esc, navigation and moving away close it. Clicking the trigger docks it. |
|
|
279
|
+
| Mobile (< md) | Drawer up to 22rem: 44px rows, 20px icons, close button, safe-area insets. Trigger is a 44px ghost button, top-left. |
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Private layout main column
|
|
3
|
-
*
|
|
2
|
+
* Private layout main column: the scroll surface, plus the sidebar trigger
|
|
3
|
+
* whenever the sidebar is not docked (collapsed on desktop, always on mobile).
|
|
4
|
+
* On desktop the trigger sits in a hover zone that peeks the sidebar open.
|
|
4
5
|
*/
|
|
5
6
|
|
|
6
7
|
'use client';
|
|
@@ -11,6 +12,7 @@ import { SidebarTrigger, useSidebar } from '@djangocfg/ui-core/components';
|
|
|
11
12
|
import { cn } from '@djangocfg/ui-core/lib';
|
|
12
13
|
|
|
13
14
|
import type { LayoutVisualConfig } from '../../types';
|
|
15
|
+
import { useSidebarPeek } from '../hooks';
|
|
14
16
|
|
|
15
17
|
interface PrivateContentProps {
|
|
16
18
|
children: ReactNode;
|
|
@@ -42,61 +44,46 @@ export function PrivateContent({
|
|
|
42
44
|
hasSidebar = true,
|
|
43
45
|
visual,
|
|
44
46
|
}: PrivateContentProps) {
|
|
45
|
-
const { isMobile,
|
|
47
|
+
const { isMobile, state } = useSidebar();
|
|
48
|
+
const peek = useSidebarPeek();
|
|
46
49
|
|
|
47
|
-
|
|
48
|
-
* Space for fixed FAB + safe area so content does not sit under the button.
|
|
49
|
-
* Tighter when the mobile drawer is closed (FAB only). When the drawer is open, a bit more
|
|
50
|
-
* room avoids the first line sitting under the overlay edge.
|
|
51
|
-
*/
|
|
52
|
-
const mobileFabClearance =
|
|
53
|
-
hasSidebar &&
|
|
54
|
-
padding === 'default' &&
|
|
55
|
-
(isMobile && !openMobile
|
|
56
|
-
? 'max-md:pt-[max(4.25rem,calc(3.5rem+env(safe-area-inset-top,0px)))]'
|
|
57
|
-
: 'max-md:pt-[max(4.5rem,calc(3.75rem+env(safe-area-inset-top,0px)))]');
|
|
50
|
+
const showTrigger = hasSidebar && (isMobile || state === 'collapsed');
|
|
58
51
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
:
|
|
52
|
+
// The trigger floats over the top-left corner; reserve its row.
|
|
53
|
+
const topPadding = showTrigger
|
|
54
|
+
? 'pt-[max(3.5rem,calc(3rem+env(safe-area-inset-top,0px)))]'
|
|
55
|
+
: hasSidebar
|
|
56
|
+
? 'max-md:pt-[max(3.5rem,calc(3rem+env(safe-area-inset-top,0px)))] md:pt-6 lg:pt-8'
|
|
57
|
+
: 'pt-4 sm:pt-6 lg:pt-8';
|
|
65
58
|
|
|
66
59
|
const scrollAreaClass = cn(
|
|
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
60
|
'min-h-0 flex-1 bg-background',
|
|
72
61
|
scroll === 'auto' ? 'overflow-y-auto' : 'flex flex-col overflow-hidden',
|
|
73
62
|
padding === 'default' && scroll === 'auto' && [
|
|
74
63
|
'px-4 sm:px-6 lg:px-8',
|
|
75
64
|
'pb-[calc(1rem+env(safe-area-inset-bottom,0px))] sm:pb-[calc(1.5rem+env(safe-area-inset-bottom,0px))] lg:pb-[calc(2rem+env(safe-area-inset-bottom,0px))]',
|
|
65
|
+
topPadding,
|
|
76
66
|
],
|
|
77
|
-
scroll === 'auto' && contentTopPaddingClass,
|
|
78
|
-
mobileFabClearance,
|
|
79
67
|
);
|
|
80
68
|
|
|
81
|
-
const mobileMenuFab = hasSidebar ? (
|
|
82
|
-
<SidebarTrigger
|
|
83
|
-
variant="secondary"
|
|
84
|
-
className={cn(
|
|
85
|
-
'fixed z-40 md:hidden',
|
|
86
|
-
'left-3 top-[max(0.75rem,env(safe-area-inset-top,0px))]',
|
|
87
|
-
'h-12 w-12 rounded-xl',
|
|
88
|
-
'border border-border shadow-md',
|
|
89
|
-
'[&_svg]:!h-6 [&_svg]:!w-6',
|
|
90
|
-
'touch-manipulation',
|
|
91
|
-
)}
|
|
92
|
-
/>
|
|
93
|
-
) : null;
|
|
94
|
-
|
|
95
69
|
const innerWidthClass = MAX_WIDTH_CLASS[visual?.maxWidth ?? 'none'];
|
|
96
70
|
|
|
97
71
|
return (
|
|
98
|
-
<div className="flex min-h-0 min-w-0 flex-1 flex-col">
|
|
99
|
-
{
|
|
72
|
+
<div className="relative flex min-h-0 min-w-0 flex-1 flex-col">
|
|
73
|
+
{showTrigger ? (
|
|
74
|
+
<div
|
|
75
|
+
data-sidebar="trigger-zone"
|
|
76
|
+
{...(isMobile ? undefined : peek?.zoneProps)}
|
|
77
|
+
className="absolute left-0 top-[env(safe-area-inset-top,0px)] z-20 flex size-14 items-center justify-center"
|
|
78
|
+
>
|
|
79
|
+
<SidebarTrigger
|
|
80
|
+
className={cn(
|
|
81
|
+
'text-muted-foreground hover:text-foreground',
|
|
82
|
+
isMobile ? 'size-11 [&_svg]:!size-5' : 'size-8',
|
|
83
|
+
)}
|
|
84
|
+
/>
|
|
85
|
+
</div>
|
|
86
|
+
) : null}
|
|
100
87
|
<div className={scrollAreaClass}>
|
|
101
88
|
{innerWidthClass ? <div className={innerWidthClass}>{children}</div> : children}
|
|
102
89
|
</div>
|
|
@@ -1,22 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The sidebar: docked on desktop, a peek panel while collapsed, a drawer on mobile.
|
|
3
|
+
*/
|
|
4
|
+
|
|
1
5
|
'use client';
|
|
2
6
|
|
|
3
7
|
import React from 'react';
|
|
4
8
|
|
|
5
|
-
import {
|
|
6
|
-
Sidebar,
|
|
7
|
-
SidebarContent,
|
|
8
|
-
SidebarFooter,
|
|
9
|
-
useSidebar,
|
|
10
|
-
} from '@djangocfg/ui-core/components';
|
|
11
|
-
import { cn } from '@djangocfg/ui-core/lib';
|
|
9
|
+
import { Sidebar, useSidebar } from '@djangocfg/ui-core/components';
|
|
12
10
|
|
|
13
|
-
import { PrivateSidebarAccount } from './PrivateSidebarAccount';
|
|
14
11
|
import { PrivateLayoutProvider } from '../context';
|
|
15
|
-
import {
|
|
12
|
+
import { useSidebarKeyboard } from '../hooks';
|
|
16
13
|
import type { HeaderConfig, SidebarConfig } from '../types';
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
14
|
+
import { SidebarBody } from './SidebarBody';
|
|
15
|
+
import { SidebarPeek } from './SidebarPeek';
|
|
16
|
+
import { SidebarResizeHandle } from './SidebarResizeHandle';
|
|
20
17
|
|
|
21
18
|
interface PrivateSidebarProps {
|
|
22
19
|
sidebar: SidebarConfig;
|
|
@@ -25,94 +22,28 @@ interface PrivateSidebarProps {
|
|
|
25
22
|
variant?: 'sidebar' | 'inset';
|
|
26
23
|
}
|
|
27
24
|
|
|
28
|
-
export function PrivateSidebar({
|
|
29
|
-
sidebar,
|
|
30
|
-
header,
|
|
31
|
-
pathname: pathnameProp,
|
|
32
|
-
variant = 'sidebar',
|
|
33
|
-
}: PrivateSidebarProps) {
|
|
25
|
+
export function PrivateSidebar({ sidebar, header, pathname = '', variant = 'sidebar' }: PrivateSidebarProps) {
|
|
34
26
|
const { state, isMobile, setOpenMobile } = useSidebar();
|
|
35
|
-
const
|
|
27
|
+
const { setSidebarRef, handleSidebarKeyDown } = useSidebarKeyboard();
|
|
36
28
|
|
|
37
29
|
React.useEffect(() => {
|
|
38
30
|
if (isMobile) setOpenMobile(false);
|
|
39
31
|
}, [pathname, isMobile, setOpenMobile]);
|
|
40
32
|
|
|
41
33
|
return (
|
|
42
|
-
<PrivateLayoutProvider
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
isMobile={isMobile}
|
|
47
|
-
state={state}
|
|
48
|
-
>
|
|
49
|
-
<PrivateSidebarInner
|
|
50
|
-
sidebar={sidebar}
|
|
51
|
-
header={header}
|
|
34
|
+
<PrivateLayoutProvider sidebar={sidebar} header={header} pathname={pathname} isMobile={isMobile}>
|
|
35
|
+
<Sidebar
|
|
36
|
+
ref={setSidebarRef}
|
|
37
|
+
collapsible="offcanvas"
|
|
52
38
|
variant={variant}
|
|
53
|
-
|
|
39
|
+
onKeyDown={handleSidebarKeyDown}
|
|
40
|
+
// Collapsed: parked off-canvas. Inert keeps it out of tab order and the a11y tree.
|
|
41
|
+
inert={state === 'collapsed' && !isMobile}
|
|
42
|
+
>
|
|
43
|
+
<SidebarBody />
|
|
44
|
+
<SidebarResizeHandle />
|
|
45
|
+
</Sidebar>
|
|
46
|
+
<SidebarPeek />
|
|
54
47
|
</PrivateLayoutProvider>
|
|
55
48
|
);
|
|
56
49
|
}
|
|
57
|
-
|
|
58
|
-
interface PrivateSidebarInnerProps {
|
|
59
|
-
sidebar: SidebarConfig;
|
|
60
|
-
header?: HeaderConfig;
|
|
61
|
-
variant: 'sidebar' | 'inset';
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function PrivateSidebarInner({ sidebar, header, variant }: PrivateSidebarInnerProps) {
|
|
65
|
-
const layoutVariant = variant === 'inset' ? 'boxed' : 'full-bleed';
|
|
66
|
-
const { modifiers } = useShellVisualState(layoutVariant);
|
|
67
|
-
const { setSidebarRef, handleSidebarKeyDown } = useSidebarKeyboard();
|
|
68
|
-
|
|
69
|
-
const sidebarRootClass = React.useMemo(
|
|
70
|
-
() =>
|
|
71
|
-
cn(
|
|
72
|
-
'[&>[data-sidebar=sidebar]]:bg-gradient-to-t [&>[data-sidebar=sidebar]]:from-sidebar/85 [&>[data-sidebar=sidebar]]:to-sidebar',
|
|
73
|
-
modifiers.sidebarRoot,
|
|
74
|
-
modifiers.sidebarInner.map((m) => `[&>[data-sidebar=sidebar]]:${m}`),
|
|
75
|
-
),
|
|
76
|
-
[modifiers],
|
|
77
|
-
);
|
|
78
|
-
|
|
79
|
-
const sidebarContentClass = React.useMemo(
|
|
80
|
-
() => cn('gap-2', modifiers.sidebarContent),
|
|
81
|
-
[modifiers.sidebarContent],
|
|
82
|
-
);
|
|
83
|
-
|
|
84
|
-
const renderedGroups = React.useMemo(
|
|
85
|
-
() =>
|
|
86
|
-
sidebar.groups.map((group) => {
|
|
87
|
-
if (group.dynamic && group.items.length === 0) return null;
|
|
88
|
-
return (
|
|
89
|
-
<SidebarNavGroup
|
|
90
|
-
key={group.label || `__flat_${group.items.map((i) => i.href).join('|')}`}
|
|
91
|
-
group={group}
|
|
92
|
-
/>
|
|
93
|
-
);
|
|
94
|
-
}),
|
|
95
|
-
[sidebar.groups],
|
|
96
|
-
);
|
|
97
|
-
|
|
98
|
-
return (
|
|
99
|
-
<Sidebar
|
|
100
|
-
ref={setSidebarRef}
|
|
101
|
-
collapsible="icon"
|
|
102
|
-
variant={variant}
|
|
103
|
-
className={sidebarRootClass}
|
|
104
|
-
onKeyDown={handleSidebarKeyDown}
|
|
105
|
-
>
|
|
106
|
-
<SidebarBrand />
|
|
107
|
-
|
|
108
|
-
<SidebarContent className={sidebarContentClass}>
|
|
109
|
-
<SidebarSlots />
|
|
110
|
-
{renderedGroups}
|
|
111
|
-
</SidebarContent>
|
|
112
|
-
|
|
113
|
-
<SidebarFooter className="p-0">
|
|
114
|
-
<PrivateSidebarAccount header={header} />
|
|
115
|
-
</SidebarFooter>
|
|
116
|
-
</Sidebar>
|
|
117
|
-
);
|
|
118
|
-
}
|