@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
|
@@ -88,26 +88,24 @@ export interface SidebarConfig {
|
|
|
88
88
|
* (above `footer` + account block).
|
|
89
89
|
*/
|
|
90
90
|
menuEnd?: ReactNode;
|
|
91
|
-
/**
|
|
92
|
-
* Keep `menuStart` visible when the desktop sidebar is collapsed to the
|
|
93
|
-
* icon rail. Default `false` — most slot content is full-width and looks
|
|
94
|
-
* broken at ~56px. Set `true` only when the slot renders well in compact mode.
|
|
95
|
-
*/
|
|
91
|
+
/** @deprecated No effect: a collapsed sidebar is hidden, not an icon rail. */
|
|
96
92
|
menuStartShowOnCollapsed?: boolean;
|
|
97
|
-
/**
|
|
93
|
+
/** @deprecated No effect: a collapsed sidebar is hidden, not an icon rail. */
|
|
98
94
|
menuEndShowOnCollapsed?: boolean;
|
|
95
|
+
/**
|
|
96
|
+
* Let the user drag the sidebar's right edge (14–24rem, remembered; double-click
|
|
97
|
+
* resets). Default `true`.
|
|
98
|
+
*/
|
|
99
|
+
resizable?: boolean;
|
|
99
100
|
/** Custom footer component rendered at the bottom of the sidebar */
|
|
100
101
|
footer?: ReactNode;
|
|
101
|
-
/**
|
|
102
|
+
/** @deprecated No effect: the active item is always a surface. */
|
|
102
103
|
activeIndicator?: SidebarActiveIndicator;
|
|
103
|
-
/**
|
|
104
|
+
/** @deprecated No effect: group labels are always sentence case. */
|
|
104
105
|
groupLabelStyle?: SidebarGroupLabelStyle;
|
|
105
106
|
/** Featured CTA tile rendered below all groups, above `menuEnd`. */
|
|
106
107
|
featured?: SidebarFeaturedConfig;
|
|
107
|
-
/**
|
|
108
|
-
* Override the auto-calculated nav density. When omitted density is derived
|
|
109
|
-
* from total item count (comfortable ≤5, default ≤9, compact >9).
|
|
110
|
-
*/
|
|
108
|
+
/** @deprecated No effect: rows are one desktop size and one 44px touch size. */
|
|
111
109
|
density?: 'comfortable' | 'default' | 'compact';
|
|
112
110
|
}
|
|
113
111
|
|
|
@@ -167,11 +165,7 @@ export interface HeaderConfig {
|
|
|
167
165
|
brandLetter?: string;
|
|
168
166
|
/** User menu groups (account panel in the sidebar footer) */
|
|
169
167
|
groups?: UserMenuConfig['groups'];
|
|
170
|
-
/**
|
|
171
|
-
* Behaviour of the footer account button.
|
|
172
|
-
* - `'menu'` (default) — opens a DropdownMenu with account links, locale/theme controls, and sign-out.
|
|
173
|
-
* - `'dialog'` — opens the global ProfileDialog (managed via Zustand store).
|
|
174
|
-
*/
|
|
168
|
+
/** @deprecated No effect: the footer account button always opens the account menu. */
|
|
175
169
|
accountAction?: 'menu' | 'dialog';
|
|
176
170
|
/** Auth page path (for sign in button) */
|
|
177
171
|
authPath?: string;
|
|
@@ -213,9 +207,8 @@ export interface PrivateLayoutProps {
|
|
|
213
207
|
*/
|
|
214
208
|
contentScroll?: 'auto' | 'hidden';
|
|
215
209
|
/**
|
|
216
|
-
* Visual style of the shell. Defaults to `'
|
|
217
|
-
* sidebar
|
|
218
|
-
* edge-to-edge layout.
|
|
210
|
+
* Visual style of the shell. Defaults to `'full-bleed'` (flat, Claude-style:
|
|
211
|
+
* sidebar + border + page). Pass `{ variant: 'boxed' }` for the inset rounded card.
|
|
219
212
|
*/
|
|
220
213
|
visual?: LayoutVisualConfig;
|
|
221
214
|
/**
|
|
@@ -6,104 +6,18 @@
|
|
|
6
6
|
|
|
7
7
|
'use client';
|
|
8
8
|
|
|
9
|
-
import React, { memo
|
|
10
|
-
import { Laptop, Moon, Sun } from 'lucide-react';
|
|
11
|
-
|
|
12
|
-
import { Button } from '@djangocfg/ui-core/components';
|
|
13
|
-
import { useForcedTheme, useThemeContext } from '@djangocfg/ui-core/theme';
|
|
9
|
+
import React, { memo } from 'react';
|
|
14
10
|
|
|
15
11
|
import { Link } from '@djangocfg/ui-core/components';
|
|
16
12
|
|
|
17
13
|
import { LocaleSwitcher } from '../../../_components/LocaleSwitcher';
|
|
18
|
-
import { useLayoutI18nOptional } from '
|
|
14
|
+
import { useLayoutI18nOptional } from '../../../../shared/LayoutI18nProvider';
|
|
15
|
+
import { ThemeModeControl } from './ThemeModeControl';
|
|
19
16
|
import { FooterMenuSections } from './FooterMenuSections';
|
|
20
17
|
import { FooterProjectInfo } from './FooterProjectInfo';
|
|
21
18
|
|
|
22
19
|
import type { DefaultFooterProps } from './types';
|
|
23
20
|
|
|
24
|
-
interface ThemeModeControlProps {
|
|
25
|
-
/** What to do when the route forces a theme via `ThemeOverride`. */
|
|
26
|
-
lockedBehavior?: 'disable' | 'hide';
|
|
27
|
-
/** Tooltip shown when locked. */
|
|
28
|
-
lockedTitle?: string;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function ThemeModeControl({
|
|
32
|
-
lockedBehavior = 'disable',
|
|
33
|
-
lockedTitle = 'Theme is set for this page',
|
|
34
|
-
}: ThemeModeControlProps) {
|
|
35
|
-
const { theme, setTheme } = useThemeContext();
|
|
36
|
-
const forcedTheme = useForcedTheme();
|
|
37
|
-
const [mounted, setMounted] = useState(false);
|
|
38
|
-
|
|
39
|
-
useEffect(() => {
|
|
40
|
-
setMounted(true);
|
|
41
|
-
}, []);
|
|
42
|
-
|
|
43
|
-
const isLocked = Boolean(forcedTheme);
|
|
44
|
-
// When locked, the active item reflects the forced theme — not the user's pick.
|
|
45
|
-
const currentTheme: 'system' | 'light' | 'dark' = isLocked
|
|
46
|
-
? forcedTheme!
|
|
47
|
-
: mounted
|
|
48
|
-
? ((theme as 'system' | 'light' | 'dark' | undefined) || 'system')
|
|
49
|
-
: 'system';
|
|
50
|
-
|
|
51
|
-
// Prepare item descriptors above JSX — no inline conditionals in the tree.
|
|
52
|
-
const items: Array<{
|
|
53
|
-
key: 'system' | 'light' | 'dark';
|
|
54
|
-
icon: typeof Laptop;
|
|
55
|
-
label: string;
|
|
56
|
-
}> = [
|
|
57
|
-
{ key: 'system', icon: Laptop, label: 'Use system theme' },
|
|
58
|
-
{ key: 'light', icon: Sun, label: 'Use light theme' },
|
|
59
|
-
{ key: 'dark', icon: Moon, label: 'Use dark theme' },
|
|
60
|
-
];
|
|
61
|
-
|
|
62
|
-
const baseItemClass = 'h-8 w-8 rounded-full p-0 text-muted-foreground hover:text-foreground';
|
|
63
|
-
const activeItemClass = 'bg-background/80 text-foreground shadow-sm';
|
|
64
|
-
const lockedItemClass = 'cursor-not-allowed opacity-60 hover:text-muted-foreground';
|
|
65
|
-
|
|
66
|
-
if (isLocked && lockedBehavior === 'hide') {
|
|
67
|
-
return null;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const containerClass = [
|
|
71
|
-
'inline-flex items-center gap-1 rounded-full border border-border/60 bg-muted/30 p-1',
|
|
72
|
-
isLocked ? 'cursor-not-allowed' : '',
|
|
73
|
-
].filter(Boolean).join(' ');
|
|
74
|
-
const containerTitle = isLocked ? lockedTitle : undefined;
|
|
75
|
-
|
|
76
|
-
return (
|
|
77
|
-
<div className={containerClass} title={containerTitle} aria-disabled={isLocked}>
|
|
78
|
-
{items.map((item) => {
|
|
79
|
-
const Icon = item.icon;
|
|
80
|
-
const isActive = currentTheme === item.key;
|
|
81
|
-
const className = [
|
|
82
|
-
baseItemClass,
|
|
83
|
-
isActive ? activeItemClass : '',
|
|
84
|
-
isLocked ? lockedItemClass : '',
|
|
85
|
-
].filter(Boolean).join(' ');
|
|
86
|
-
const handleClick = isLocked ? undefined : () => setTheme(item.key);
|
|
87
|
-
return (
|
|
88
|
-
<Button
|
|
89
|
-
key={item.key}
|
|
90
|
-
type="button"
|
|
91
|
-
variant="ghost"
|
|
92
|
-
size="icon"
|
|
93
|
-
className={className}
|
|
94
|
-
onClick={handleClick}
|
|
95
|
-
disabled={isLocked}
|
|
96
|
-
aria-label={item.label}
|
|
97
|
-
title={isLocked ? lockedTitle : item.label}
|
|
98
|
-
>
|
|
99
|
-
<Icon className="h-4 w-4" />
|
|
100
|
-
</Button>
|
|
101
|
-
);
|
|
102
|
-
})}
|
|
103
|
-
</div>
|
|
104
|
-
);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
21
|
function DefaultFooterRaw({ config }: DefaultFooterProps) {
|
|
108
22
|
const variant = config.variant ?? 'full';
|
|
109
23
|
const shellClass = config.shell?.className;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Laptop, Moon, Sun } from 'lucide-react';
|
|
4
|
+
import React, { useEffect, useState } from 'react';
|
|
5
|
+
|
|
6
|
+
import { Button } from '@djangocfg/ui-core/components';
|
|
7
|
+
import { useForcedTheme, useThemeContext } from '@djangocfg/ui-core/theme';
|
|
8
|
+
|
|
9
|
+
interface ThemeModeControlProps {
|
|
10
|
+
/** What to do when the route forces a theme via `ThemeOverride`. */
|
|
11
|
+
lockedBehavior?: 'disable' | 'hide';
|
|
12
|
+
/** Tooltip shown when locked. */
|
|
13
|
+
lockedTitle?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function ThemeModeControl({
|
|
17
|
+
lockedBehavior = 'disable',
|
|
18
|
+
lockedTitle = 'Theme is set for this page',
|
|
19
|
+
}: ThemeModeControlProps) {
|
|
20
|
+
const { theme, setTheme } = useThemeContext();
|
|
21
|
+
const forcedTheme = useForcedTheme();
|
|
22
|
+
const [mounted, setMounted] = useState(false);
|
|
23
|
+
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
setMounted(true);
|
|
26
|
+
}, []);
|
|
27
|
+
|
|
28
|
+
const isLocked = Boolean(forcedTheme);
|
|
29
|
+
// When locked, the active item reflects the forced theme — not the user's pick.
|
|
30
|
+
const currentTheme: 'system' | 'light' | 'dark' = isLocked
|
|
31
|
+
? forcedTheme!
|
|
32
|
+
: mounted
|
|
33
|
+
? ((theme as 'system' | 'light' | 'dark' | undefined) || 'system')
|
|
34
|
+
: 'system';
|
|
35
|
+
|
|
36
|
+
// Prepare item descriptors above JSX — no inline conditionals in the tree.
|
|
37
|
+
const items: Array<{
|
|
38
|
+
key: 'system' | 'light' | 'dark';
|
|
39
|
+
icon: typeof Laptop;
|
|
40
|
+
label: string;
|
|
41
|
+
}> = [
|
|
42
|
+
{ key: 'system', icon: Laptop, label: 'Use system theme' },
|
|
43
|
+
{ key: 'light', icon: Sun, label: 'Use light theme' },
|
|
44
|
+
{ key: 'dark', icon: Moon, label: 'Use dark theme' },
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
const baseItemClass = 'h-8 w-8 rounded-full p-0 text-muted-foreground hover:text-foreground';
|
|
48
|
+
const activeItemClass = 'bg-background/80 text-foreground shadow-sm';
|
|
49
|
+
const lockedItemClass = 'cursor-not-allowed opacity-60 hover:text-muted-foreground';
|
|
50
|
+
|
|
51
|
+
if (isLocked && lockedBehavior === 'hide') {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const containerClass = [
|
|
56
|
+
'inline-flex items-center gap-1 rounded-full border border-border/60 bg-muted/30 p-1',
|
|
57
|
+
isLocked ? 'cursor-not-allowed' : '',
|
|
58
|
+
].filter(Boolean).join(' ');
|
|
59
|
+
const containerTitle = isLocked ? lockedTitle : undefined;
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<div className={containerClass} title={containerTitle} aria-disabled={isLocked}>
|
|
63
|
+
{items.map((item) => {
|
|
64
|
+
const Icon = item.icon;
|
|
65
|
+
const isActive = currentTheme === item.key;
|
|
66
|
+
const className = [
|
|
67
|
+
baseItemClass,
|
|
68
|
+
isActive ? activeItemClass : '',
|
|
69
|
+
isLocked ? lockedItemClass : '',
|
|
70
|
+
].filter(Boolean).join(' ');
|
|
71
|
+
const handleClick = isLocked ? undefined : () => setTheme(item.key);
|
|
72
|
+
return (
|
|
73
|
+
<Button
|
|
74
|
+
key={item.key}
|
|
75
|
+
type="button"
|
|
76
|
+
variant="ghost"
|
|
77
|
+
size="icon"
|
|
78
|
+
className={className}
|
|
79
|
+
onClick={handleClick}
|
|
80
|
+
disabled={isLocked}
|
|
81
|
+
aria-label={item.label}
|
|
82
|
+
title={isLocked ? lockedTitle : item.label}
|
|
83
|
+
>
|
|
84
|
+
<Icon className="h-4 w-4" />
|
|
85
|
+
</Button>
|
|
86
|
+
);
|
|
87
|
+
})}
|
|
88
|
+
</div>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { createPortal } from 'react-dom';
|
|
5
|
+
|
|
6
|
+
import { portalPopoverCls } from './navDesktopStyles';
|
|
7
|
+
|
|
8
|
+
export function DesktopDropdownPortal({
|
|
9
|
+
anchor,
|
|
10
|
+
children,
|
|
11
|
+
onMouseEnter,
|
|
12
|
+
onMouseLeave,
|
|
13
|
+
}: {
|
|
14
|
+
anchor: HTMLElement;
|
|
15
|
+
children: React.ReactNode;
|
|
16
|
+
onMouseEnter: () => void;
|
|
17
|
+
onMouseLeave: () => void;
|
|
18
|
+
}) {
|
|
19
|
+
const [position, setPosition] = React.useState({ left: 0, top: 0 });
|
|
20
|
+
|
|
21
|
+
React.useLayoutEffect(() => {
|
|
22
|
+
const updatePosition = () => {
|
|
23
|
+
const rect = anchor.getBoundingClientRect();
|
|
24
|
+
setPosition({ left: rect.left, top: rect.bottom + 4 });
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
updatePosition();
|
|
28
|
+
window.addEventListener('resize', updatePosition);
|
|
29
|
+
window.addEventListener('scroll', updatePosition, true);
|
|
30
|
+
return () => {
|
|
31
|
+
window.removeEventListener('resize', updatePosition);
|
|
32
|
+
window.removeEventListener('scroll', updatePosition, true);
|
|
33
|
+
};
|
|
34
|
+
}, [anchor]);
|
|
35
|
+
|
|
36
|
+
return createPortal(
|
|
37
|
+
<div
|
|
38
|
+
className={portalPopoverCls}
|
|
39
|
+
style={position}
|
|
40
|
+
onMouseEnter={onMouseEnter}
|
|
41
|
+
onMouseLeave={onMouseLeave}
|
|
42
|
+
>
|
|
43
|
+
{children}
|
|
44
|
+
</div>,
|
|
45
|
+
document.body,
|
|
46
|
+
);
|
|
47
|
+
}
|
|
@@ -8,7 +8,7 @@ import { cn } from '@djangocfg/ui-core/lib';
|
|
|
8
8
|
import { useThemeContext } from '@djangocfg/ui-core/theme';
|
|
9
9
|
|
|
10
10
|
import { LocaleSwitcher } from '../../_components/LocaleSwitcher';
|
|
11
|
-
import { useLayoutI18nOptional } from '
|
|
11
|
+
import { useLayoutI18nOptional } from '../../../shared/LayoutI18nProvider';
|
|
12
12
|
|
|
13
13
|
export interface NavControlsProps {
|
|
14
14
|
/** Show the theme (light / system / dark) pill. @default false */
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
import { ChevronDown } from 'lucide-react';
|
|
4
4
|
import React, { memo } from 'react';
|
|
5
|
-
import { createPortal } from 'react-dom';
|
|
6
5
|
|
|
7
6
|
import { Button, Link } from '@djangocfg/ui-core/components';
|
|
8
7
|
import { cn } from '@djangocfg/ui-core/lib';
|
|
@@ -12,6 +11,8 @@ import { navItemsShallowEqual } from '../../../utils/navItemsShallowEqual';
|
|
|
12
11
|
import type { UseDropdownMenuReturn } from '../hooks/useDropdownMenu';
|
|
13
12
|
import { useResponsiveOverflow } from '../hooks/useResponsiveOverflow';
|
|
14
13
|
import type { PublicDesktopDropdownRenderer } from '../navbarTypes';
|
|
14
|
+
import { DesktopDropdownPortal } from './DesktopDropdownPortal';
|
|
15
|
+
import { labelCls, navItemActiveCls, navItemCls, popoverCls, subMenuLinkCls } from './navDesktopStyles';
|
|
15
16
|
|
|
16
17
|
interface NavDesktopItemsProps {
|
|
17
18
|
/** Full ordered item list; overflow is computed responsively. */
|
|
@@ -24,88 +25,6 @@ interface NavDesktopItemsProps {
|
|
|
24
25
|
renderDesktopDropdown?: PublicDesktopDropdownRenderer;
|
|
25
26
|
}
|
|
26
27
|
|
|
27
|
-
// Corners come from ui-core's radius tokens, never raw Tailwind steps —
|
|
28
|
-
// presets (`dense`, `windows`) retune those variables, and a hardcoded
|
|
29
|
-
// `rounded-lg` would opt this navbar out of the active theme.
|
|
30
|
-
//
|
|
31
|
-
// Radius is chosen per element KIND, not unified across the bar: nav rows and
|
|
32
|
-
// menu items take `rounded-control` (--radius-control, the token documented for
|
|
33
|
-
// "nav items, inputs, chips"), their popover takes --radius-popover, while
|
|
34
|
-
// BUTTONS stay pills like every other button in the product. Making the rows
|
|
35
|
-
// pills too is what produced capsules floating inside a soft-cornered popover.
|
|
36
|
-
const navItemCls = cn(
|
|
37
|
-
'inline-flex min-h-9 items-center justify-center gap-1 rounded-control border border-transparent px-4 py-1.5 text-sm !font-medium',
|
|
38
|
-
'ring-0 focus-visible:ring-0',
|
|
39
|
-
'text-foreground/90 transition-colors',
|
|
40
|
-
'surface-hover hover:text-foreground',
|
|
41
|
-
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/35',
|
|
42
|
-
);
|
|
43
|
-
|
|
44
|
-
const navItemActiveCls =
|
|
45
|
-
'border-transparent bg-accent text-foreground shadow-sm dark:bg-muted dark:shadow-none';
|
|
46
|
-
|
|
47
|
-
const labelCls = 'min-w-0 max-w-[11rem] truncate sm:max-w-[13rem]';
|
|
48
|
-
|
|
49
|
-
function subMenuLinkCls(active: boolean) {
|
|
50
|
-
return cn(
|
|
51
|
-
'flex min-h-9 min-w-0 max-w-[min(17rem,calc(100vw-5rem))] items-center rounded-control border-0 px-3 py-2 text-sm font-medium transition-colors',
|
|
52
|
-
'surface-hover',
|
|
53
|
-
active
|
|
54
|
-
? 'border-0 bg-accent font-semibold text-foreground shadow-sm dark:bg-muted/90 dark:shadow-none'
|
|
55
|
-
: 'border-0 text-foreground/90',
|
|
56
|
-
);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// Real glass: low-opacity surface so the blur actually shows the page behind
|
|
60
|
-
// it. The package-level nav-dropdown-glass utility keeps the frosted effect
|
|
61
|
-
// present even when the consuming app does not scan layout sources.
|
|
62
|
-
const popoverCls =
|
|
63
|
-
'absolute left-0 top-full mt-1 z-[1200] min-w-[14.5rem] rounded-[var(--radius-popover)] border border-border/70 nav-dropdown-glass p-1.5 shadow-[0_1px_2px_rgba(0,0,0,0.05),0_8px_28px_rgba(0,0,0,0.18)] dark:shadow-[0_8px_30px_rgba(0,0,0,0.45)]';
|
|
64
|
-
|
|
65
|
-
const portalPopoverCls =
|
|
66
|
-
'fixed z-[1200] min-w-[14.5rem] rounded-[var(--radius-popover)] border border-border/70 nav-dropdown-glass backdrop-blur-[5px] backdrop-saturate-150 p-1.5 shadow-[0_1px_2px_rgba(0,0,0,0.05),0_8px_28px_rgba(0,0,0,0.18)] dark:shadow-[0_8px_30px_rgba(0,0,0,0.45)]';
|
|
67
|
-
|
|
68
|
-
function DesktopDropdownPortal({
|
|
69
|
-
anchor,
|
|
70
|
-
children,
|
|
71
|
-
onMouseEnter,
|
|
72
|
-
onMouseLeave,
|
|
73
|
-
}: {
|
|
74
|
-
anchor: HTMLElement;
|
|
75
|
-
children: React.ReactNode;
|
|
76
|
-
onMouseEnter: () => void;
|
|
77
|
-
onMouseLeave: () => void;
|
|
78
|
-
}) {
|
|
79
|
-
const [position, setPosition] = React.useState({ left: 0, top: 0 });
|
|
80
|
-
|
|
81
|
-
React.useLayoutEffect(() => {
|
|
82
|
-
const updatePosition = () => {
|
|
83
|
-
const rect = anchor.getBoundingClientRect();
|
|
84
|
-
setPosition({ left: rect.left, top: rect.bottom + 4 });
|
|
85
|
-
};
|
|
86
|
-
|
|
87
|
-
updatePosition();
|
|
88
|
-
window.addEventListener('resize', updatePosition);
|
|
89
|
-
window.addEventListener('scroll', updatePosition, true);
|
|
90
|
-
return () => {
|
|
91
|
-
window.removeEventListener('resize', updatePosition);
|
|
92
|
-
window.removeEventListener('scroll', updatePosition, true);
|
|
93
|
-
};
|
|
94
|
-
}, [anchor]);
|
|
95
|
-
|
|
96
|
-
return createPortal(
|
|
97
|
-
<div
|
|
98
|
-
className={portalPopoverCls}
|
|
99
|
-
style={position}
|
|
100
|
-
onMouseEnter={onMouseEnter}
|
|
101
|
-
onMouseLeave={onMouseLeave}
|
|
102
|
-
>
|
|
103
|
-
{children}
|
|
104
|
-
</div>,
|
|
105
|
-
document.body,
|
|
106
|
-
);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
28
|
function NavDesktopItemsRaw({
|
|
110
29
|
items,
|
|
111
30
|
maxVisible,
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { cn } from '@djangocfg/ui-core/lib';
|
|
2
|
+
|
|
3
|
+
// Corners come from ui-core's radius tokens, never raw Tailwind steps —
|
|
4
|
+
// presets (`dense`, `windows`) retune those variables, and a hardcoded
|
|
5
|
+
// `rounded-lg` would opt this navbar out of the active theme.
|
|
6
|
+
//
|
|
7
|
+
// Radius is chosen per element KIND, not unified across the bar: nav rows and
|
|
8
|
+
// menu items take `rounded-control` (--radius-control, the token documented for
|
|
9
|
+
// "nav items, inputs, chips"), their popover takes --radius-popover, while
|
|
10
|
+
// BUTTONS stay pills like every other button in the product. Making the rows
|
|
11
|
+
// pills too is what produced capsules floating inside a soft-cornered popover.
|
|
12
|
+
export const navItemCls = cn(
|
|
13
|
+
'inline-flex min-h-9 items-center justify-center gap-1 rounded-control border border-transparent px-4 py-1.5 text-sm !font-medium',
|
|
14
|
+
'ring-0 focus-visible:ring-0',
|
|
15
|
+
'text-foreground/90 transition-colors',
|
|
16
|
+
'surface-hover hover:text-foreground',
|
|
17
|
+
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/35',
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
export const navItemActiveCls =
|
|
21
|
+
'border-transparent bg-accent text-foreground shadow-sm dark:bg-muted dark:shadow-none';
|
|
22
|
+
|
|
23
|
+
export const labelCls = 'min-w-0 max-w-[11rem] truncate sm:max-w-[13rem]';
|
|
24
|
+
|
|
25
|
+
export function subMenuLinkCls(active: boolean) {
|
|
26
|
+
return cn(
|
|
27
|
+
'flex min-h-9 min-w-0 max-w-[min(17rem,calc(100vw-5rem))] items-center rounded-control border-0 px-3 py-2 text-sm font-medium transition-colors',
|
|
28
|
+
'surface-hover',
|
|
29
|
+
active
|
|
30
|
+
? 'border-0 bg-accent font-semibold text-foreground shadow-sm dark:bg-muted/90 dark:shadow-none'
|
|
31
|
+
: 'border-0 text-foreground/90',
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Real glass: low-opacity surface so the blur actually shows the page behind
|
|
36
|
+
// it. The package-level nav-dropdown-glass utility keeps the frosted effect
|
|
37
|
+
// present even when the consuming app does not scan layout sources.
|
|
38
|
+
export const popoverCls =
|
|
39
|
+
'absolute left-0 top-full mt-1 z-[1200] min-w-[14.5rem] rounded-[var(--radius-popover)] border border-border/70 nav-dropdown-glass p-1.5 shadow-[0_1px_2px_rgba(0,0,0,0.05),0_8px_28px_rgba(0,0,0,0.18)] dark:shadow-[0_8px_30px_rgba(0,0,0,0.45)]';
|
|
40
|
+
|
|
41
|
+
export const portalPopoverCls =
|
|
42
|
+
'fixed z-[1200] min-w-[14.5rem] rounded-[var(--radius-popover)] border border-border/70 nav-dropdown-glass backdrop-blur-[5px] backdrop-saturate-150 p-1.5 shadow-[0_1px_2px_rgba(0,0,0,0.05),0_8px_28px_rgba(0,0,0,0.18)] dark:shadow-[0_8px_30px_rgba(0,0,0,0.45)]';
|
|
@@ -16,7 +16,6 @@
|
|
|
16
16
|
'use client';
|
|
17
17
|
|
|
18
18
|
import React, {
|
|
19
|
-
type ReactNode,
|
|
20
19
|
memo,
|
|
21
20
|
useEffect,
|
|
22
21
|
useLayoutEffect,
|
|
@@ -31,190 +30,24 @@ import { cn } from '@djangocfg/ui-core/lib';
|
|
|
31
30
|
import { usePathnameWithoutLocale } from '../../../hooks';
|
|
32
31
|
import { navItemsShallowEqual } from '../../../utils/navItemsShallowEqual';
|
|
33
32
|
import { LocaleSwitcher } from '../../_components/LocaleSwitcher';
|
|
34
|
-
import { useLayoutI18nOptional } from '
|
|
33
|
+
import { useLayoutI18nOptional } from '../../../shared/LayoutI18nProvider';
|
|
35
34
|
import { usePublicLayoutOptional } from '../context';
|
|
36
35
|
import {
|
|
37
36
|
useDropdownMenu,
|
|
38
37
|
useNavbarScroll,
|
|
39
38
|
useNavbarViewportVars,
|
|
40
39
|
} from '../hooks';
|
|
41
|
-
import type {
|
|
42
|
-
PublicDesktopDropdownRenderer,
|
|
43
|
-
PublicNavbarHeight,
|
|
44
|
-
PublicNavbarInsetX,
|
|
45
|
-
PublicNavbarPosition,
|
|
46
|
-
PublicNavbarVariant,
|
|
47
|
-
PublicNavLayout,
|
|
48
|
-
} from '../navbarTypes';
|
|
49
|
-
import type { NavigationItem, UserMenuConfig } from '../../types';
|
|
40
|
+
import type { NavigationItem } from '../../types';
|
|
50
41
|
|
|
51
42
|
import { NavActions } from '../primitives/NavActions';
|
|
52
|
-
import type { NavAction } from '../primitives/NavActionItem';
|
|
53
43
|
import { NavBrand } from '../primitives/NavBrand';
|
|
54
44
|
import { NavControls } from '../primitives/NavControls';
|
|
55
45
|
import { NavDesktopItems } from '../primitives/NavDesktopItems';
|
|
56
46
|
|
|
57
47
|
import { NavbarRowDesktop, NavbarRowMobile } from './navbarRows';
|
|
58
48
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
* `sm:` steps inside it: the mobile row's tallest element is the 44px burger,
|
|
62
|
-
* so padding there is pure added height, while the desktop row needs the extra
|
|
63
|
-
* air to keep the link list off the panel edge.
|
|
64
|
-
*/
|
|
65
|
-
const heightClsDesktop: Record<PublicNavbarHeight, string> = {
|
|
66
|
-
sm: 'py-2',
|
|
67
|
-
md: 'py-3.5',
|
|
68
|
-
lg: 'py-5',
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
const heightClsMobile: Record<PublicNavbarHeight, string> = {
|
|
72
|
-
sm: 'py-1.5',
|
|
73
|
-
md: 'py-2',
|
|
74
|
-
lg: 'py-3',
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Token → Tailwind class for the inner-row horizontal padding.
|
|
79
|
-
*
|
|
80
|
-
* Deliberately ASYMMETRIC at every breakpoint (`pl` > `pr`) to correct an
|
|
81
|
-
* optical imbalance, not a measured one. On the left the gutter meets the
|
|
82
|
-
* brand's glyphs directly; on the right it meets a button — an icon square on
|
|
83
|
-
* phones, the Login pill on desktop — whose box carries its own internal
|
|
84
|
-
* padding around the glyph. Equal `px` therefore *looks* lopsided, with a
|
|
85
|
-
* conspicuous moat to the right of the last control.
|
|
86
|
-
*
|
|
87
|
-
* It also stacks with the navbar shell's own outer gutter (`px-3 sm:px-4
|
|
88
|
-
* lg:px-6`), which is why the right side stays small in absolute terms: at
|
|
89
|
-
* `lg` the symmetric version put ~48px between the Login button and the
|
|
90
|
-
* viewport edge.
|
|
91
|
-
*/
|
|
92
|
-
/** Desktop (`lg`+) horizontal inset. Right stays trimmed — see the note above. */
|
|
93
|
-
const insetXClsDesktop: Record<'none' | 'xs' | 'sm' | 'md' | 'lg', string> = {
|
|
94
|
-
none: '',
|
|
95
|
-
xs: 'lg:pl-4 lg:pr-1.5',
|
|
96
|
-
sm: 'lg:pl-6 lg:pr-2',
|
|
97
|
-
md: 'lg:pl-8 lg:pr-3',
|
|
98
|
-
lg: 'lg:pl-12 lg:pr-5',
|
|
99
|
-
};
|
|
100
|
-
|
|
101
|
-
/** Mobile horizontal inset — tighter, and asymmetric for the same reason. */
|
|
102
|
-
const insetXClsMobile: Record<'none' | 'xs' | 'sm' | 'md' | 'lg', string> = {
|
|
103
|
-
none: '',
|
|
104
|
-
xs: 'pl-3 pr-1',
|
|
105
|
-
sm: 'pl-4 pr-1.5',
|
|
106
|
-
md: 'pl-5 pr-2',
|
|
107
|
-
lg: 'pl-6 pr-3',
|
|
108
|
-
};
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* Resolves the inset token into one class string covering both breakpoints.
|
|
112
|
-
*
|
|
113
|
-
* A caller may also pass raw Tailwind instead of a token; that escape hatch is
|
|
114
|
-
* applied verbatim to both rows, since we cannot know how to split it.
|
|
115
|
-
*/
|
|
116
|
-
function resolveInsetX(value: PublicNavbarInsetX | undefined): string {
|
|
117
|
-
const token = (value ?? 'sm') as keyof typeof insetXClsMobile;
|
|
118
|
-
if (!(token in insetXClsMobile)) return value as string;
|
|
119
|
-
return cn(insetXClsMobile[token], insetXClsDesktop[token]);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
interface ShapeInputs {
|
|
123
|
-
/** true at scrollY >= threshold */
|
|
124
|
-
scrolled: boolean;
|
|
125
|
-
/** Matches `transparent` flag at the source. */
|
|
126
|
-
transparent: boolean;
|
|
127
|
-
/**
|
|
128
|
-
* true while the mobile drawer is open. A `transparent` bar MUST go opaque
|
|
129
|
-
* here regardless of scroll position: an open drawer means the brand + close
|
|
130
|
-
* button sit over page content, and a see-through bar leaves them floating on
|
|
131
|
-
* whatever is scrolled behind — the "navbar lost its background" bug.
|
|
132
|
-
*/
|
|
133
|
-
menuOpen: boolean;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
export interface NavbarShellProps {
|
|
137
|
-
// ── Surface identity (reported to context) ────────────────────────────────
|
|
138
|
-
variant: PublicNavbarVariant;
|
|
139
|
-
position: PublicNavbarPosition;
|
|
140
|
-
|
|
141
|
-
// ── Content ───────────────────────────────────────────────────────────────
|
|
142
|
-
brand?: ReactNode;
|
|
143
|
-
brandHref?: string;
|
|
144
|
-
navigation?: NavigationItem[];
|
|
145
|
-
userMenu?: UserMenuConfig;
|
|
146
|
-
renderDesktopDropdown?: PublicDesktopDropdownRenderer;
|
|
147
|
-
/**
|
|
148
|
-
* Hard cap for how many items to try to fit on desktop. Useful when a
|
|
149
|
-
* consumer wants to force a compact primary row even if more would fit.
|
|
150
|
-
* Defaults to the full list length (let the responsive measurer decide).
|
|
151
|
-
*/
|
|
152
|
-
desktopMaxPrimaryItems?: number;
|
|
153
|
-
|
|
154
|
-
// ── Layout ────────────────────────────────────────────────────────────────
|
|
155
|
-
navLayout?: PublicNavLayout;
|
|
156
|
-
navbarHeight?: PublicNavbarHeight;
|
|
157
|
-
/** Horizontal inset between outer edge and the inner row.
|
|
158
|
-
* @default 'sm' */
|
|
159
|
-
insetX?: PublicNavbarInsetX;
|
|
160
|
-
/** Full override for the row container (escape hatch).
|
|
161
|
-
* When set, wins over `insetX`. */
|
|
162
|
-
innerPadding?: string;
|
|
163
|
-
|
|
164
|
-
// ── Scroll behavior ───────────────────────────────────────────────────────
|
|
165
|
-
hideNavOnScroll?: boolean;
|
|
166
|
-
transparent?: boolean;
|
|
167
|
-
transparentThreshold?: number;
|
|
168
|
-
|
|
169
|
-
// ── Chrome (per-variant) ──────────────────────────────────────────────────
|
|
170
|
-
/** Classes on the outer wrapper (fixed/sticky, top offset, outer x-padding). */
|
|
171
|
-
outerClassName?: string;
|
|
172
|
-
/** Classes for the `<nav>` element defining the shape (rounding, border, shadow). */
|
|
173
|
-
shapeClassName?: string;
|
|
174
|
-
/** Surface classes (bg, backdrop-blur). Driven by `shapeForState`. */
|
|
175
|
-
shapeForState?: (inputs: ShapeInputs) => string;
|
|
176
|
-
|
|
177
|
-
/**
|
|
178
|
-
* Replaces the default actions column (UserMenu + mobile-toggle).
|
|
179
|
-
* Variant owns the mobile toggle too. Receives helpers so it can build the
|
|
180
|
-
* right-hand side however it wants.
|
|
181
|
-
*/
|
|
182
|
-
renderActions?: (ctx: NavbarActionsContext) => ReactNode;
|
|
183
|
-
|
|
184
|
-
// ── Default-actions slots (only used when `renderActions` is NOT provided) ─
|
|
185
|
-
/** Typed CTA pills (Book a demo / Get started / …) before UserMenu. */
|
|
186
|
-
actions?: NavAction[];
|
|
187
|
-
/** Arbitrary ReactNode between actions and UserMenu. */
|
|
188
|
-
actionsLeadingSlot?: ReactNode;
|
|
189
|
-
/** Arbitrary ReactNode after the mobile toggle. */
|
|
190
|
-
actionsTrailingSlot?: ReactNode;
|
|
191
|
-
|
|
192
|
-
// ── Theme + locale controls (rendered next to UserMenu on desktop) ────────
|
|
193
|
-
/** Toggle individual controls. Locale switcher also requires `i18n`. */
|
|
194
|
-
controls?: {
|
|
195
|
-
/** Light / system / dark pill. @default false */
|
|
196
|
-
showThemeSwitcher?: boolean;
|
|
197
|
-
/** Locale dropdown. Requires `i18n`. @default false */
|
|
198
|
-
showLocaleSwitcher?: boolean;
|
|
199
|
-
};
|
|
200
|
-
|
|
201
|
-
// ── Props override (used by variants that proxy ctx differently) ──────────
|
|
202
|
-
mobileMenuOpen?: boolean;
|
|
203
|
-
onMobileMenuToggle?: () => void;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
export interface NavbarActionsContext {
|
|
207
|
-
userMenu?: UserMenuConfig;
|
|
208
|
-
mobileMenuOpen: boolean;
|
|
209
|
-
toggleMobileMenu: () => void;
|
|
210
|
-
toggleMobileLabel: string;
|
|
211
|
-
navLayout: PublicNavLayout;
|
|
212
|
-
/**
|
|
213
|
-
* Pre-built theme/locale controls node. `null` when both toggles are off.
|
|
214
|
-
* Variants with a custom `renderActions` can choose where to place it.
|
|
215
|
-
*/
|
|
216
|
-
controls: ReactNode | null;
|
|
217
|
-
}
|
|
49
|
+
import { heightClsDesktop, heightClsMobile, resolveInsetX } from './navbarShellStyles';
|
|
50
|
+
import type { NavbarShellProps } from './navbarShell.types';
|
|
218
51
|
|
|
219
52
|
function NavbarShellRaw(props: NavbarShellProps) {
|
|
220
53
|
const context = usePublicLayoutOptional();
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { NavbarShell } from './NavbarShell';
|
|
2
|
-
export type { NavbarShellProps, NavbarActionsContext } from './
|
|
2
|
+
export type { NavbarShellProps, NavbarActionsContext } from './navbarShell.types';
|
|
3
3
|
export { MobileDrawerShell } from './MobileDrawerShell';
|
|
4
4
|
export type { MobileDrawerShellProps } from './MobileDrawerShell';
|