@djangocfg/layouts 2.1.356 → 2.1.358
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +21 -19
- package/src/configurator/private/schema.ts +12 -0
- package/src/layouts/AdminLayout/AdminLayout.tsx +2 -1
- package/src/layouts/AppLayout/AppLayout.tsx +35 -15
- package/src/layouts/AppLayout/BaseApp.tsx +2 -2
- package/src/layouts/AuthLayout/AuthLayout.tsx +26 -19
- package/src/layouts/AuthLayout/components/oauth/OAuthCallback.tsx +10 -4
- package/src/layouts/AuthLayout/components/shared/AuthButton.tsx +11 -5
- package/src/layouts/AuthLayout/components/shared/AuthContainer.tsx +10 -10
- package/src/layouts/AuthLayout/components/shared/AuthDivider.tsx +11 -5
- package/src/layouts/AuthLayout/components/shared/AuthError.tsx +10 -5
- package/src/layouts/AuthLayout/components/shared/AuthFooter.tsx +11 -5
- package/src/layouts/AuthLayout/components/shared/AuthHeader.tsx +10 -10
- package/src/layouts/AuthLayout/components/shared/AuthLink.tsx +11 -5
- package/src/layouts/AuthLayout/components/shared/AuthOTPInput.tsx +28 -20
- package/src/layouts/AuthLayout/components/shared/TermsCheckbox.tsx +11 -5
- package/src/layouts/AuthLayout/components/steps/IdentifierStep.tsx +12 -4
- package/src/layouts/AuthLayout/components/steps/OTPStep.tsx +9 -4
- package/src/layouts/AuthLayout/components/steps/SetupStep/SetupComplete.tsx +12 -5
- package/src/layouts/AuthLayout/components/steps/SetupStep/SetupLoading.tsx +9 -4
- package/src/layouts/AuthLayout/components/steps/SetupStep/SetupQRCode.tsx +11 -5
- package/src/layouts/AuthLayout/components/steps/SetupStep/index.tsx +15 -5
- package/src/layouts/AuthLayout/components/steps/TwoFactorStep.tsx +9 -4
- package/src/layouts/AuthLayout/context.tsx +35 -13
- package/src/layouts/AuthLayout/shells/AuthShell.tsx +11 -4
- package/src/layouts/AuthLayout/shells/CenteredShell.tsx +10 -4
- package/src/layouts/AuthLayout/shells/SplitShell.tsx +10 -4
- package/src/layouts/AuthLayout/shells/context.tsx +16 -5
- package/src/layouts/PrivateLayout/PrivateLayout.tsx +45 -248
- package/src/layouts/PrivateLayout/components/PrivateSidebar.tsx +113 -430
- package/src/layouts/{_components → PrivateLayout/components}/PrivateSidebarAccount.tsx +82 -105
- package/src/layouts/PrivateLayout/components/SidebarBrand.tsx +168 -0
- package/src/layouts/{_components → PrivateLayout/components}/SidebarFeatured.tsx +2 -2
- package/src/layouts/PrivateLayout/components/SidebarNavGroup.tsx +189 -0
- package/src/layouts/PrivateLayout/components/SidebarNavItem.tsx +137 -0
- package/src/layouts/PrivateLayout/components/SidebarSlots.tsx +71 -0
- package/src/layouts/PrivateLayout/components/index.ts +4 -0
- package/src/layouts/PrivateLayout/context.tsx +211 -0
- package/src/layouts/PrivateLayout/density.ts +48 -0
- package/src/layouts/PrivateLayout/hooks/index.ts +14 -0
- package/src/layouts/PrivateLayout/hooks/useAuthGuard.ts +54 -0
- package/src/layouts/PrivateLayout/hooks/useHoverExpand.ts +110 -0
- package/src/layouts/PrivateLayout/hooks/useLayoutVisual.ts +113 -0
- package/src/layouts/PrivateLayout/hooks/useShellVisualState.ts +207 -0
- package/src/layouts/PrivateLayout/hooks/useSidebarDefaultOpen.ts +21 -0
- package/src/layouts/PrivateLayout/hooks/useSidebarKeyboard.ts +115 -0
- package/src/layouts/PrivateLayout/index.ts +2 -2
- package/src/layouts/PrivateLayout/types.ts +193 -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} +8 -8
- package/src/layouts/ProfileLayout/ProfileForm/index.tsx +148 -0
- package/src/layouts/ProfileLayout/README.md +118 -0
- package/src/layouts/ProfileLayout/components/ApiKeySection/ApiKeySection.tsx +197 -0
- package/src/layouts/ProfileLayout/components/ApiKeySection/context.tsx +159 -0
- package/src/layouts/ProfileLayout/components/ApiKeySection/index.ts +3 -0
- 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 +110 -0
- package/src/layouts/ProfileLayout/components/ProfileTab.tsx +35 -0
- package/src/layouts/ProfileLayout/components/{TwoFactorSection.tsx → TwoFactorSection/TwoFactorSection.tsx} +1 -1
- package/src/layouts/ProfileLayout/components/TwoFactorSection/index.ts +1 -0
- package/src/layouts/ProfileLayout/components/index.ts +5 -2
- package/src/layouts/ProfileLayout/hooks/index.ts +2 -0
- package/src/layouts/ProfileLayout/hooks/useProfileTabs.ts +48 -0
- package/src/layouts/ProfileLayout/index.ts +7 -3
- package/src/layouts/ProfileLayout/types.ts +47 -0
- package/src/layouts/{_components → PublicLayout/components}/UserMenu.tsx +3 -3
- package/src/layouts/PublicLayout/components/index.ts +4 -0
- package/src/layouts/PublicLayout/footers/DefaultFooter/DefaultFooter.tsx +12 -2
- package/src/layouts/PublicLayout/navbars/MinimalNavbar/MinimalNavbar.tsx +1 -1
- package/src/layouts/PublicLayout/primitives/NavActions.tsx +44 -3
- package/src/layouts/PublicLayout/primitives/NavBrand.tsx +4 -2
- package/src/layouts/PublicLayout/primitives/NavDesktopItems.tsx +42 -2
- package/src/layouts/PublicLayout/shared/MobileDrawerShell.tsx +1 -1
- package/src/layouts/PublicLayout/shared/NavbarShell.tsx +60 -1
- package/src/layouts/_components/index.ts +2 -6
- package/src/layouts/index.ts +9 -4
- package/src/layouts/ProfileLayout/ProfileLayout.tsx +0 -284
- package/src/layouts/ProfileLayout/__tests__/TwoFactorSection.test.tsx +0 -234
- package/src/layouts/ProfileLayout/components/ProfileForm.tsx +0 -198
- /package/src/layouts/{_components → PublicLayout/components}/UserAvatar.tsx +0 -0
|
@@ -8,165 +8,42 @@
|
|
|
8
8
|
|
|
9
9
|
'use client';
|
|
10
10
|
|
|
11
|
-
import React, { ReactNode
|
|
12
|
-
import
|
|
11
|
+
import React, { ReactNode } from 'react';
|
|
12
|
+
import dynamic from 'next/dynamic';
|
|
13
13
|
|
|
14
|
-
import { useAuth } from '@djangocfg/api/auth';
|
|
15
14
|
import { Preloader } from '@djangocfg/ui-core/components';
|
|
16
15
|
import { SidebarInset, SidebarProvider } from '@djangocfg/ui-core/components';
|
|
17
16
|
|
|
18
17
|
import type { AppLayoutPublicChrome } from '../AppLayout/AppLayout';
|
|
19
18
|
import type { LayoutVisualConfig } from '../types';
|
|
20
|
-
import { UserMenuConfig } from '../types';
|
|
21
19
|
import { PrivateContent, PrivateSidebar } from './components';
|
|
22
|
-
|
|
23
|
-
import
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Hide per-item icons inside the group. Defaults to `true` when `collapsible`,
|
|
51
|
-
* `false` otherwise (Mailersend convention: icons live on the trigger, not on children).
|
|
52
|
-
*/
|
|
53
|
-
hideItemIcons?: boolean;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/** Active-state visual treatment for sidebar nav items. */
|
|
57
|
-
export type SidebarActiveIndicator = 'background' | 'rail' | 'both';
|
|
58
|
-
|
|
59
|
-
/** Rendering of group labels. `'uppercase'` is the legacy ultra-light caps; `'plain'` is sm bold. */
|
|
60
|
-
export type SidebarGroupLabelStyle = 'uppercase' | 'plain';
|
|
61
|
-
|
|
62
|
-
/** Featured CTA tile rendered below groups. */
|
|
63
|
-
export interface SidebarFeaturedConfig {
|
|
64
|
-
icon?: string | LucideIconType;
|
|
65
|
-
label: string;
|
|
66
|
-
href: string;
|
|
67
|
-
badge?: string;
|
|
68
|
-
accent?: 'green' | 'blue' | 'amber' | 'primary';
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export interface SidebarConfig {
|
|
72
|
-
/** Grouped items with labels */
|
|
73
|
-
groups: SidebarGroupConfig[];
|
|
74
|
-
/** Home link href */
|
|
75
|
-
homeHref?: string;
|
|
76
|
-
/**
|
|
77
|
-
* Custom block inside the scrollable nav column, **above** all `groups`
|
|
78
|
-
* (below the brand header, same horizontal padding as nav).
|
|
79
|
-
*/
|
|
80
|
-
menuStart?: ReactNode;
|
|
81
|
-
/**
|
|
82
|
-
* Custom block inside the scrollable nav column, **below** all `groups`
|
|
83
|
-
* (above `footer` + account block).
|
|
84
|
-
*/
|
|
85
|
-
menuEnd?: ReactNode;
|
|
86
|
-
/**
|
|
87
|
-
* Keep `menuStart` visible when the desktop sidebar is collapsed to the
|
|
88
|
-
* icon rail. Default `false` — most slot content is full-width and looks
|
|
89
|
-
* broken at ~56px. Set `true` only when the slot renders well in compact mode.
|
|
90
|
-
*/
|
|
91
|
-
menuStartShowOnCollapsed?: boolean;
|
|
92
|
-
/** Same as `menuStartShowOnCollapsed`, but for `menuEnd`. Default `false`. */
|
|
93
|
-
menuEndShowOnCollapsed?: boolean;
|
|
94
|
-
/** Custom footer component rendered at the bottom of the sidebar */
|
|
95
|
-
footer?: ReactNode;
|
|
96
|
-
/** Active-state visual on nav items. Default `'background'` (legacy). */
|
|
97
|
-
activeIndicator?: SidebarActiveIndicator;
|
|
98
|
-
/** Style of group labels. Default `'uppercase'` (legacy). Collapsible groups always use `plain`. */
|
|
99
|
-
groupLabelStyle?: SidebarGroupLabelStyle;
|
|
100
|
-
/** Featured CTA tile rendered below all groups, above `menuEnd`. */
|
|
101
|
-
featured?: SidebarFeaturedConfig;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
export interface HeaderConfig {
|
|
105
|
-
/** Custom header brand node (same idea as PublicNavbar `brand`). */
|
|
106
|
-
brand?: ReactNode;
|
|
107
|
-
/** Shown next to the logo when the sidebar is expanded */
|
|
108
|
-
title?: string;
|
|
109
|
-
/**
|
|
110
|
-
* Brand mark in the sidebar header (Lucide icon name or component).
|
|
111
|
-
* If omitted, a single-letter monogram from `brandLetter` / `title` is shown.
|
|
112
|
-
*/
|
|
113
|
-
brandIcon?: string | LucideIconType;
|
|
114
|
-
/**
|
|
115
|
-
* Monogram when `brandIcon` is not set (one visible character).
|
|
116
|
-
* Defaults to the first letter of `title`, uppercased.
|
|
117
|
-
*/
|
|
118
|
-
brandLetter?: string;
|
|
119
|
-
/** User menu groups (account panel in the sidebar footer) */
|
|
120
|
-
groups?: UserMenuConfig['groups'];
|
|
121
|
-
/** Auth page path (for sign in button) */
|
|
122
|
-
authPath?: string;
|
|
123
|
-
/** Subtitle under the display name in the sidebar footer (e.g. "Max plan"). */
|
|
124
|
-
userPlan?: string;
|
|
125
|
-
/** Optional secondary action button rendered inside the footer trigger (e.g. Get apps download button). */
|
|
126
|
-
footerSecondaryAction?: {
|
|
127
|
-
icon: string | LucideIconType;
|
|
128
|
-
href?: string;
|
|
129
|
-
onClick?: () => void;
|
|
130
|
-
ariaLabel: string;
|
|
131
|
-
/** Show pulsing accent dot on the action (Claude-style "new"). */
|
|
132
|
-
pulse?: boolean;
|
|
133
|
-
};
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
export interface PrivateLayoutProps {
|
|
137
|
-
children: ReactNode;
|
|
138
|
-
/** Sidebar configuration */
|
|
139
|
-
sidebar?: SidebarConfig;
|
|
140
|
-
/** Title + account links (no top navbar — title is used in the sidebar chrome) */
|
|
141
|
-
header?: HeaderConfig;
|
|
142
|
-
/**
|
|
143
|
-
* Path for active nav highlighting. With `@djangocfg/nextjs` i18n routing, pass `usePathname()` from
|
|
144
|
-
* `@djangocfg/nextjs/i18n/navigation` (no `/[locale]` segment). If omitted, uses `next/navigation` (includes locale).
|
|
145
|
-
*/
|
|
146
|
-
pathname?: string;
|
|
147
|
-
/** Content padding */
|
|
148
|
-
contentPadding?: 'none' | 'default';
|
|
149
|
-
/**
|
|
150
|
-
* Content scroll behaviour.
|
|
151
|
-
* - `'auto'` (default) — the shell scroll-area scrolls vertically.
|
|
152
|
-
* - `'hidden'` — shell does NOT scroll; use for full-height pages (e.g. Kanban)
|
|
153
|
-
* where children manage their own scroll surfaces.
|
|
154
|
-
*/
|
|
155
|
-
contentScroll?: 'auto' | 'hidden';
|
|
156
|
-
/**
|
|
157
|
-
* Visual style of the shell. Defaults to `'boxed'` (inset rounded card on a
|
|
158
|
-
* sidebar-coloured canvas). Pass `{ variant: 'full-bleed' }` for the legacy
|
|
159
|
-
* edge-to-edge layout.
|
|
160
|
-
*/
|
|
161
|
-
visual?: LayoutVisualConfig;
|
|
162
|
-
/**
|
|
163
|
-
* Skip the built-in auth guard. Useful for static showcases / playground
|
|
164
|
-
* embeds where there's no real session. Default `true` (guard on).
|
|
165
|
-
*/
|
|
166
|
-
requireAuth?: boolean;
|
|
167
|
-
/** Reserved for `AppLayout` passthrough (`publicChrome`); unused in this layout. */
|
|
168
|
-
publicChrome?: AppLayoutPublicChrome;
|
|
169
|
-
}
|
|
20
|
+
import { useAuthGuard } from './hooks';
|
|
21
|
+
import { useLayoutVisual } from './hooks';
|
|
22
|
+
import { useSidebarDefaultOpen } from './hooks';
|
|
23
|
+
|
|
24
|
+
import type {
|
|
25
|
+
HeaderConfig,
|
|
26
|
+
PrivateLayoutProps,
|
|
27
|
+
SidebarConfig,
|
|
28
|
+
} from './types';
|
|
29
|
+
|
|
30
|
+
export type {
|
|
31
|
+
SidebarItem,
|
|
32
|
+
SidebarGroupConfig,
|
|
33
|
+
SidebarConfig,
|
|
34
|
+
HeaderConfig,
|
|
35
|
+
SidebarActiveIndicator,
|
|
36
|
+
SidebarGroupLabelStyle,
|
|
37
|
+
SidebarFeaturedConfig,
|
|
38
|
+
} from './types';
|
|
39
|
+
|
|
40
|
+
export { PrivateLayoutProps };
|
|
41
|
+
|
|
42
|
+
// Lazy-load ProfileDialog so the profile bundle is only fetched when opened.
|
|
43
|
+
const ProfileDialog = dynamic(
|
|
44
|
+
() => import('../ProfileLayout/ProfileDialog/ProfileDialog').then((m) => m.ProfileDialog),
|
|
45
|
+
{ ssr: false },
|
|
46
|
+
);
|
|
170
47
|
|
|
171
48
|
export function PrivateLayout({
|
|
172
49
|
children,
|
|
@@ -178,25 +55,21 @@ export function PrivateLayout({
|
|
|
178
55
|
visual,
|
|
179
56
|
requireAuth = true,
|
|
180
57
|
}: PrivateLayoutProps) {
|
|
181
|
-
const {
|
|
182
|
-
|
|
183
|
-
|
|
58
|
+
const { isLoading, loadingText } = useAuthGuard({
|
|
59
|
+
requireAuth,
|
|
60
|
+
authPath: header?.authPath,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const { providerStyle, providerClassName, insetClassName, sidebarVariant } =
|
|
64
|
+
useLayoutVisual(visual);
|
|
184
65
|
|
|
185
|
-
|
|
186
|
-
if (!requireAuth) return;
|
|
187
|
-
if (!isLoading && !isAuthenticated && !isRedirecting) {
|
|
188
|
-
const currentUrl = window.location.pathname + window.location.search;
|
|
189
|
-
saveRedirectUrl(currentUrl);
|
|
190
|
-
setIsRedirecting(true);
|
|
191
|
-
router.push(header?.authPath || '/auth');
|
|
192
|
-
}
|
|
193
|
-
}, [requireAuth, isAuthenticated, isLoading, isRedirecting, router, saveRedirectUrl, header?.authPath]);
|
|
66
|
+
const defaultOpen = useSidebarDefaultOpen();
|
|
194
67
|
|
|
195
|
-
if (
|
|
68
|
+
if (isLoading) {
|
|
196
69
|
return (
|
|
197
70
|
<Preloader
|
|
198
71
|
variant="fullscreen"
|
|
199
|
-
text={
|
|
72
|
+
text={loadingText}
|
|
200
73
|
size="lg"
|
|
201
74
|
backdrop={true}
|
|
202
75
|
backdropOpacity={80}
|
|
@@ -204,14 +77,11 @@ export function PrivateLayout({
|
|
|
204
77
|
);
|
|
205
78
|
}
|
|
206
79
|
|
|
207
|
-
const variant: LayoutVisualConfig['variant'] = visual?.variant ?? 'boxed';
|
|
208
|
-
const sidebarVariant = variant === 'boxed' ? 'inset' : 'sidebar';
|
|
209
|
-
|
|
210
80
|
return (
|
|
211
81
|
<SidebarProvider
|
|
212
|
-
defaultOpen={
|
|
213
|
-
style={
|
|
214
|
-
className={
|
|
82
|
+
defaultOpen={defaultOpen}
|
|
83
|
+
style={providerStyle}
|
|
84
|
+
className={providerClassName}
|
|
215
85
|
>
|
|
216
86
|
{sidebar && (
|
|
217
87
|
<PrivateSidebar
|
|
@@ -222,7 +92,7 @@ export function PrivateLayout({
|
|
|
222
92
|
/>
|
|
223
93
|
)}
|
|
224
94
|
|
|
225
|
-
<SidebarInset className={
|
|
95
|
+
<SidebarInset className={insetClassName}>
|
|
226
96
|
<PrivateContent
|
|
227
97
|
padding={contentPadding}
|
|
228
98
|
scroll={contentScroll}
|
|
@@ -232,81 +102,8 @@ export function PrivateLayout({
|
|
|
232
102
|
{children}
|
|
233
103
|
</PrivateContent>
|
|
234
104
|
</SidebarInset>
|
|
105
|
+
|
|
106
|
+
<ProfileDialog />
|
|
235
107
|
</SidebarProvider>
|
|
236
108
|
);
|
|
237
109
|
}
|
|
238
|
-
|
|
239
|
-
/** CSS variables consumed by the boxed `SidebarInset` (margin + radius). */
|
|
240
|
-
function resolveProviderStyle(visual: LayoutVisualConfig | undefined): React.CSSProperties | undefined {
|
|
241
|
-
if ((visual?.variant ?? 'boxed') !== 'boxed') return undefined;
|
|
242
|
-
const inset = normaliseInset(visual?.inset);
|
|
243
|
-
return {
|
|
244
|
-
['--app-shell-inset-x' as string]: `${inset.x}px`,
|
|
245
|
-
['--app-shell-inset-y' as string]: `${inset.y}px`,
|
|
246
|
-
['--app-shell-radius' as string]: BOXED_RADIUS_REM[visual?.radius ?? '2xl'],
|
|
247
|
-
};
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
/**
|
|
251
|
-
* Statically-known Tailwind classes for the boxed inset. Margin and radius are
|
|
252
|
-
* driven by the CSS variables set in `resolveProviderStyle`, so JIT can fully
|
|
253
|
-
* extract these classes at build time.
|
|
254
|
-
*/
|
|
255
|
-
const BOXED_INSET_CLASS = [
|
|
256
|
-
'flex flex-col',
|
|
257
|
-
'md:peer-data-[variant=inset]:my-[var(--app-shell-inset-y)]',
|
|
258
|
-
'md:peer-data-[variant=inset]:mr-[var(--app-shell-inset-x)]',
|
|
259
|
-
'md:peer-data-[variant=inset]:rounded-[var(--app-shell-radius)]',
|
|
260
|
-
'md:peer-data-[variant=inset]:overflow-hidden',
|
|
261
|
-
].join(' ');
|
|
262
|
-
|
|
263
|
-
const BOXED_INSET_BORDER_CLASS =
|
|
264
|
-
'md:peer-data-[variant=inset]:border md:peer-data-[variant=inset]:border-border/60';
|
|
265
|
-
|
|
266
|
-
const BOXED_RADIUS_REM: Record<NonNullable<LayoutVisualConfig['radius']>, string> = {
|
|
267
|
-
sm: '0.375rem',
|
|
268
|
-
md: '0.5rem',
|
|
269
|
-
lg: '0.75rem',
|
|
270
|
-
xl: '1rem',
|
|
271
|
-
'2xl': '1.25rem',
|
|
272
|
-
'3xl': '1.75rem',
|
|
273
|
-
};
|
|
274
|
-
|
|
275
|
-
function resolveInsetClassName(visual: LayoutVisualConfig | undefined): string {
|
|
276
|
-
if ((visual?.variant ?? 'boxed') !== 'boxed') return 'flex flex-col';
|
|
277
|
-
const border = visual?.border ?? true;
|
|
278
|
-
return border ? `${BOXED_INSET_CLASS} ${BOXED_INSET_BORDER_CLASS}` : BOXED_INSET_CLASS;
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
/**
|
|
282
|
-
* Background painted *behind* the boxed container on md+. On mobile the
|
|
283
|
-
* canvas tint is dropped because the sidebar is a Drawer — leaking the
|
|
284
|
-
* canvas colour to the whole viewport just makes the page look dim.
|
|
285
|
-
*
|
|
286
|
-
* `bg-sidebar` (the default) overrides shadcn-sidebar's built-in
|
|
287
|
-
* `has-[&_[data-variant=inset]]:bg-sidebar` only at the breakpoint where
|
|
288
|
-
* the inset shape actually exists.
|
|
289
|
-
*/
|
|
290
|
-
const BOXED_BG_CLASS: Record<NonNullable<LayoutVisualConfig['background']>, string> = {
|
|
291
|
-
sidebar: 'md:!bg-sidebar',
|
|
292
|
-
muted: 'md:!bg-muted',
|
|
293
|
-
card: 'md:!bg-card',
|
|
294
|
-
background: 'md:!bg-background',
|
|
295
|
-
};
|
|
296
|
-
|
|
297
|
-
function resolveProviderClassName(visual: LayoutVisualConfig | undefined): string | undefined {
|
|
298
|
-
// h-svh + overflow-hidden: lock the shell to exactly one viewport height so
|
|
299
|
-
// the inner scroll-area (PrivateContent) is the only scroll surface. Without
|
|
300
|
-
// this, SidebarProvider grows via min-h-svh and the whole page scrolls.
|
|
301
|
-
const base = 'h-svh overflow-hidden';
|
|
302
|
-
if ((visual?.variant ?? 'boxed') !== 'boxed') return base;
|
|
303
|
-
// `max-md:!bg-background` neutralises shadcn-sidebar's built-in
|
|
304
|
-
// `has-[[data-variant=inset]]:bg-sidebar` below md so the mobile Drawer shell
|
|
305
|
-
// doesn't paint the whole viewport with the canvas tint.
|
|
306
|
-
return `${base} max-md:!bg-background ${BOXED_BG_CLASS[visual?.background ?? 'sidebar']}`;
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
function normaliseInset(inset: LayoutVisualConfig['inset']): { x: number; y: number } {
|
|
310
|
-
if (typeof inset === 'number') return { x: inset, y: inset };
|
|
311
|
-
return { x: inset?.x ?? 12, y: inset?.y ?? 12 };
|
|
312
|
-
}
|