@djangocfg/layouts 2.1.264 → 2.1.267

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.
Files changed (38) hide show
  1. package/README.md +113 -4
  2. package/package.json +19 -18
  3. package/src/hooks/index.ts +1 -1
  4. package/src/hooks/usePathnameWithoutLocale.ts +35 -19
  5. package/src/layouts/AppLayout/AppLayout.tsx +15 -4
  6. package/src/layouts/AuthLayout/components/steps/SetupStep/index.tsx +50 -6
  7. package/src/layouts/ProfileLayout/ProfileLayout.tsx +206 -235
  8. package/src/layouts/ProfileLayout/components/AvatarSection.tsx +2 -3
  9. package/src/layouts/ProfileLayout/components/DeleteAccountSection.tsx +22 -106
  10. package/src/layouts/ProfileLayout/components/EditableField.tsx +15 -10
  11. package/src/layouts/ProfileLayout/components/Section.tsx +1 -1
  12. package/src/layouts/ProfileLayout/components/TwoFactorSection.tsx +255 -215
  13. package/src/layouts/ProfileLayout/context.tsx +108 -16
  14. package/src/layouts/ProfileLayout/index.ts +1 -1
  15. package/src/layouts/PublicLayout/PublicLayout.tsx +18 -0
  16. package/src/layouts/PublicLayout/components/NavActions.tsx +50 -0
  17. package/src/layouts/PublicLayout/components/NavBrand.tsx +26 -0
  18. package/src/layouts/PublicLayout/components/NavDesktopItems.tsx +207 -0
  19. package/src/layouts/PublicLayout/components/PublicMobileDrawer.tsx +1 -1
  20. package/src/layouts/PublicLayout/components/PublicNavbar.tsx +44 -6
  21. package/src/layouts/PublicLayout/components/PublicNavigation.tsx +199 -396
  22. package/src/layouts/PublicLayout/hooks/index.ts +5 -1
  23. package/src/layouts/PublicLayout/hooks/useDropdownMenu.ts +58 -0
  24. package/src/layouts/PublicLayout/hooks/useNavbarScroll.ts +61 -0
  25. package/src/layouts/PublicLayout/hooks/useNavbarViewportVars.ts +46 -0
  26. package/src/layouts/PublicLayout/index.ts +4 -0
  27. package/src/layouts/PublicLayout/navbarTypes.ts +17 -0
  28. package/src/utils/pathMatcher.ts +6 -3
  29. package/src/layouts/ProfileLayout/.claude/.sidecar/activity.jsonl +0 -2
  30. package/src/layouts/ProfileLayout/.claude/.sidecar/history/2026-03-15.md +0 -35
  31. package/src/layouts/ProfileLayout/.claude/.sidecar/review.md +0 -35
  32. package/src/layouts/ProfileLayout/.claude/.sidecar/scan.log +0 -3
  33. package/src/layouts/ProfileLayout/.claude/.sidecar/tasks/T-001.md +0 -18
  34. package/src/layouts/ProfileLayout/.claude/.sidecar/tasks/T-002.md +0 -19
  35. package/src/layouts/ProfileLayout/.claude/.sidecar/tasks/T-003.md +0 -18
  36. package/src/layouts/ProfileLayout/.claude/.sidecar/tasks/T-004.md +0 -18
  37. package/src/layouts/ProfileLayout/.claude/.sidecar/tasks/T-005.md +0 -18
  38. package/src/layouts/ProfileLayout/.claude/.sidecar/usage.json +0 -5
@@ -1,31 +1,123 @@
1
1
  'use client';
2
2
 
3
- import React, { createContext, useCallback, useContext, useState } from 'react';
3
+ import React, { createContext, useCallback, useContext, useMemo } from 'react';
4
4
 
5
- export type ProfileStep = 'main' | 'delete-account';
5
+ import { useAppT } from '@djangocfg/i18n';
6
+ import { toast } from '@djangocfg/ui-core/hooks';
7
+ import { useAuth } from '@djangocfg/api/auth';
8
+
9
+ import { profileLogger } from '../../utils/logger';
10
+ import { useLogout } from '../../hooks';
11
+
12
+ // ─────────────────────────────────────────────────────────────────────────────
13
+ // Types
14
+ // ─────────────────────────────────────────────────────────────────────────────
15
+
16
+ export interface ProfileLabels {
17
+ title: string;
18
+ personalInfo: string;
19
+ work: string;
20
+ security: string;
21
+ firstName: string;
22
+ lastName: string;
23
+ phone: string;
24
+ company: string;
25
+ position: string;
26
+ addFirstName: string;
27
+ addLastName: string;
28
+ addPhone: string;
29
+ addCompany: string;
30
+ addPosition: string;
31
+ signOut: string;
32
+ deleteAccount: string;
33
+ profileUpdated: string;
34
+ failedToUpdate: string;
35
+ notAuthenticated: string;
36
+ pleaseLogIn: string;
37
+ loading: string;
38
+ save: string;
39
+ saving: string;
40
+ cancel: string;
41
+ }
6
42
 
7
43
  interface ProfileContextValue {
8
- step: ProfileStep;
9
- setStep: (step: ProfileStep) => void;
10
- back: () => void;
44
+ labels: ProfileLabels;
45
+ onLogout: () => void;
46
+ onFieldSave: (field: string, value: string) => Promise<void>;
47
+ }
48
+
49
+ // ─────────────────────────────────────────────────────────────────────────────
50
+ // Context
51
+ // ─────────────────────────────────────────────────────────────────────────────
52
+
53
+ const ProfileContext = createContext<ProfileContextValue | null>(null);
54
+
55
+ export const useProfileContext = (): ProfileContextValue => {
56
+ const ctx = useContext(ProfileContext);
57
+ if (!ctx) throw new Error('useProfileContext must be used within ProfileProvider');
58
+ return ctx;
59
+ };
60
+
61
+ // ─────────────────────────────────────────────────────────────────────────────
62
+ // Provider
63
+ // ─────────────────────────────────────────────────────────────────────────────
64
+
65
+ interface ProfileProviderProps {
66
+ children: React.ReactNode;
67
+ title?: string;
68
+ onUnauthenticated?: () => void;
11
69
  }
12
70
 
13
- const ProfileContext = createContext<ProfileContextValue>({
14
- step: 'main',
15
- setStep: () => {},
16
- back: () => {},
17
- });
71
+ export const ProfileProvider: React.FC<ProfileProviderProps> = ({
72
+ children,
73
+ title,
74
+ }) => {
75
+ const { updateProfile } = useAuth();
76
+ const t = useAppT();
77
+ const onLogout = useLogout();
18
78
 
19
- export const ProfileProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
20
- const [step, setStep] = useState<ProfileStep>('main');
79
+ const labels = useMemo<ProfileLabels>(() => ({
80
+ title: title || t('layouts.profilePage.title'),
81
+ personalInfo: t('layouts.profilePage.personalInfo'),
82
+ work: t('layouts.profilePage.work'),
83
+ security: t('layouts.profilePage.security'),
84
+ firstName: t('layouts.profilePage.firstName'),
85
+ lastName: t('layouts.profilePage.lastName'),
86
+ phone: t('layouts.profilePage.phone'),
87
+ company: t('layouts.profilePage.company'),
88
+ position: t('layouts.profilePage.position'),
89
+ addFirstName: t('layouts.profilePage.addFirstName') || 'Add first name',
90
+ addLastName: t('layouts.profilePage.addLastName') || 'Add last name',
91
+ addPhone: t('layouts.profilePage.addPhone') || 'Add phone number',
92
+ addCompany: t('layouts.profilePage.addCompany') || 'Add company',
93
+ addPosition: t('layouts.profilePage.addPosition') || 'Add position',
94
+ signOut: t('layouts.profilePage.signOut'),
95
+ deleteAccount: t('layouts.profilePage.deleteAccount'),
96
+ profileUpdated: t('layouts.profilePage.profileUpdated'),
97
+ failedToUpdate: t('layouts.profilePage.failedToUpdate'),
98
+ notAuthenticated: t('layouts.profilePage.notAuthenticated'),
99
+ pleaseLogIn: t('layouts.profilePage.pleaseLogIn'),
100
+ loading: t('ui.states.loading'),
101
+ save: t('layouts.profilePage.save'),
102
+ saving: t('layouts.profilePage.saving'),
103
+ cancel: t('layouts.profilePage.cancel'),
104
+ }), [t, title]);
21
105
 
22
- const back = useCallback(() => setStep('main'), []);
106
+ const onFieldSave = useCallback(async (field: string, value: string) => {
107
+ try {
108
+ await updateProfile({ [field]: value });
109
+ toast.success(labels.profileUpdated);
110
+ } catch (error: unknown) {
111
+ profileLogger.error('Profile update error:', error);
112
+ const apiErr = error as { response?: Record<string, string[]> };
113
+ toast.error(apiErr?.response?.[field]?.[0] || labels.failedToUpdate);
114
+ throw error;
115
+ }
116
+ }, [updateProfile, labels]);
23
117
 
24
118
  return (
25
- <ProfileContext.Provider value={{ step, setStep, back }}>
119
+ <ProfileContext.Provider value={{ labels, onLogout, onFieldSave }}>
26
120
  {children}
27
121
  </ProfileContext.Provider>
28
122
  );
29
123
  };
30
-
31
- export const useProfileContext = () => useContext(ProfileContext);
@@ -1,3 +1,3 @@
1
1
  export { ProfileLayout } from './ProfileLayout';
2
- export type { } from './ProfileLayout';
2
+ export type { ProfileLayoutProps, ProfileTab, ProfileSlots } from './ProfileLayout';
3
3
 
@@ -53,6 +53,20 @@ export interface PublicLayoutProps {
53
53
  */
54
54
  navbar?: ReactNode;
55
55
  footer?: ReactNode;
56
+ /**
57
+ * Optional background layer rendered behind navbar and content.
58
+ * Use `position: fixed; inset: 0; pointer-events: none` (or similar) on the element
59
+ * so it fills the viewport without affecting layout flow.
60
+ *
61
+ * @example
62
+ * ```tsx
63
+ * backgroundSlot={
64
+ * <div className="fixed inset-0 -z-10 pointer-events-none"
65
+ * style={{ background: 'radial-gradient(ellipse 60% 50% at 10% 0%, violet, transparent)' }} />
66
+ * }
67
+ * ```
68
+ */
69
+ backgroundSlot?: ReactNode;
56
70
  /**
57
71
  * When `auto` (default), `<main>` gets a small top offset from `PublicNavigation` surface
58
72
  * (`floating` vs `flush`). Set `none` if the page controls spacing itself.
@@ -105,6 +119,7 @@ export function PublicLayout({
105
119
  children,
106
120
  navbar,
107
121
  footer,
122
+ backgroundSlot,
108
123
  contentTopSpacing = 'auto',
109
124
  contentBottomSpacing = 'auto',
110
125
  }: PublicLayoutProps) {
@@ -134,6 +149,9 @@ export function PublicLayout({
134
149
 
135
150
  return (
136
151
  <PublicLayoutProvider value={contextValue}>
152
+ {/* Background slot — renders behind everything, including the sticky navbar */}
153
+ {backgroundSlot ?? null}
154
+
137
155
  <div className="min-h-screen flex flex-col">
138
156
  {navbar ?? null}
139
157
 
@@ -0,0 +1,50 @@
1
+ 'use client';
2
+
3
+ import { Menu, X } from 'lucide-react';
4
+ import React from 'react';
5
+
6
+ import { Button } from '@djangocfg/ui-core/components';
7
+
8
+ import { UserMenu } from '../../_components/UserMenu';
9
+ import type { UserMenuConfig } from '../../types';
10
+
11
+ interface NavActionsProps {
12
+ userMenu?: UserMenuConfig;
13
+ mobileMenuOpen: boolean;
14
+ onMobileMenuToggle: () => void;
15
+ toggleMobileLabel: string;
16
+ /** When true, mobile trigger is always visible (not hidden on lg+). Used for `split` layout. */
17
+ forceShowMobileTrigger?: boolean;
18
+ }
19
+
20
+ export function NavActions({
21
+ userMenu,
22
+ mobileMenuOpen,
23
+ onMobileMenuToggle,
24
+ toggleMobileLabel,
25
+ forceShowMobileTrigger = false,
26
+ }: NavActionsProps) {
27
+ return (
28
+ <div className="flex items-center gap-4">
29
+ <div className="hidden lg:flex">
30
+ <UserMenu
31
+ variant="desktop"
32
+ groups={userMenu?.groups}
33
+ authPath={userMenu?.authPath}
34
+ i18n={userMenu?.i18n}
35
+ />
36
+ </div>
37
+
38
+ <Button
39
+ variant="ghost"
40
+ size="icon"
41
+ aria-label={toggleMobileLabel}
42
+ data-mobile-menu-trigger="true"
43
+ className={forceShowMobileTrigger ? 'rounded-full' : 'lg:hidden rounded-full'}
44
+ onClick={onMobileMenuToggle}
45
+ >
46
+ {mobileMenuOpen ? <X className="h-5 w-5" /> : <Menu className="h-5 w-5" />}
47
+ </Button>
48
+ </div>
49
+ );
50
+ }
@@ -0,0 +1,26 @@
1
+ 'use client';
2
+
3
+ import Link from 'next/link';
4
+ import React, { type ReactNode } from 'react';
5
+
6
+ interface NavBrandProps {
7
+ brand?: ReactNode;
8
+ brandHref?: string;
9
+ }
10
+
11
+ export function NavBrand({ brand, brandHref = '/' }: NavBrandProps) {
12
+ if (brand == null || brand === '' || brand === false) return null;
13
+
14
+ if (typeof brand === 'string') {
15
+ return (
16
+ <Link
17
+ href={brandHref}
18
+ className="font-bold text-[15px] text-foreground hover:opacity-90 transition-opacity"
19
+ >
20
+ {brand}
21
+ </Link>
22
+ );
23
+ }
24
+
25
+ return <>{brand}</>;
26
+ }
@@ -0,0 +1,207 @@
1
+ 'use client';
2
+
3
+ import { ChevronDown } from 'lucide-react';
4
+ import Link from 'next/link';
5
+ import React from 'react';
6
+
7
+ import { Button } from '@djangocfg/ui-core/components';
8
+ import { cn } from '@djangocfg/ui-core/lib';
9
+
10
+ import type { NavigationItem } from '../../types';
11
+ import type { UseDropdownMenuReturn } from '../hooks/useDropdownMenu';
12
+ import type { PublicDesktopDropdownRenderer } from './PublicNavigation';
13
+
14
+ interface NavDesktopItemsProps {
15
+ primaryItems: NavigationItem[];
16
+ overflowItems: NavigationItem[];
17
+ isActivePath: (href: string) => boolean;
18
+ isGroupActive: (item: NavigationItem) => boolean;
19
+ dropdown: UseDropdownMenuReturn;
20
+ renderDesktopDropdown?: PublicDesktopDropdownRenderer;
21
+ }
22
+
23
+ const navItemCls = cn(
24
+ 'inline-flex min-h-9 items-center justify-center gap-1 rounded-full border-0 px-4 py-1.5 text-sm font-medium',
25
+ 'ring-0 focus-visible:ring-0',
26
+ 'text-foreground/90 transition-colors',
27
+ 'hover:bg-accent/55 hover:text-foreground',
28
+ 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/35',
29
+ );
30
+
31
+ const navItemActiveCls =
32
+ 'border-0 bg-accent font-semibold text-foreground shadow-sm dark:border dark:border-border dark:bg-muted dark:shadow-none';
33
+
34
+ const labelCls = 'min-w-0 max-w-[11rem] truncate sm:max-w-[13rem]';
35
+
36
+ function subMenuLinkCls(active: boolean) {
37
+ return cn(
38
+ 'flex min-h-9 min-w-0 max-w-[min(17rem,calc(100vw-5rem))] items-center rounded-full border-0 px-4 py-2 text-sm font-medium transition-colors',
39
+ 'hover:bg-accent/55',
40
+ active
41
+ ? 'border-0 bg-accent font-semibold text-foreground shadow-sm dark:border dark:border-border dark:bg-muted/90 dark:shadow-none'
42
+ : 'border-0 text-foreground/90',
43
+ );
44
+ }
45
+
46
+ const popoverCls =
47
+ 'absolute left-0 top-full mt-1 z-[1200] min-w-[14.5rem] rounded-xl border border-border/70 bg-background/95 backdrop-blur-sm p-1.5 shadow-[0_1px_2px_rgba(0,0,0,0.05),0_6px_18px_rgba(0,0,0,0.035)] dark:shadow-[0_6px_20px_rgba(0,0,0,0.12)]';
48
+
49
+ export function NavDesktopItems({
50
+ primaryItems,
51
+ overflowItems,
52
+ isActivePath,
53
+ isGroupActive,
54
+ dropdown,
55
+ renderDesktopDropdown,
56
+ }: NavDesktopItemsProps) {
57
+ const { openDropdownKey, scheduleOpen, scheduleClose, closeDropdown } = dropdown;
58
+
59
+ const renderItem = (item: NavigationItem) => {
60
+ if (item.items && item.items.length > 0) {
61
+ const key = `${item.label}-${item.href}`;
62
+ const isOpen = openDropdownKey === key;
63
+ const isActive = isGroupActive(item);
64
+
65
+ const defaultItems = (
66
+ <>
67
+ {item.items.map((sub) => {
68
+ const subActive = isActivePath(sub.href);
69
+ return (
70
+ <div key={`${item.label}-${sub.href}`} className="rounded-full">
71
+ {sub.external ? (
72
+ <a
73
+ href={sub.href}
74
+ target="_blank"
75
+ rel="noopener noreferrer"
76
+ className={subMenuLinkCls(subActive)}
77
+ >
78
+ <span className="min-w-0 truncate" title={sub.label}>{sub.label}</span>
79
+ </a>
80
+ ) : (
81
+ <Link href={sub.href} className={subMenuLinkCls(subActive)}>
82
+ <span className="min-w-0 truncate" title={sub.label}>{sub.label}</span>
83
+ </Link>
84
+ )}
85
+ </div>
86
+ );
87
+ })}
88
+ </>
89
+ );
90
+
91
+ const defaultPopover = (
92
+ <div
93
+ className={popoverCls}
94
+ onMouseEnter={() => { scheduleOpen(key); }}
95
+ onMouseLeave={() => scheduleClose(key)}
96
+ >
97
+ {defaultItems}
98
+ </div>
99
+ );
100
+
101
+ return (
102
+ <div
103
+ key={key}
104
+ className="relative"
105
+ onMouseEnter={() => scheduleOpen(key)}
106
+ onMouseLeave={() => scheduleClose(key)}
107
+ >
108
+ <Button
109
+ variant="ghost"
110
+ size="sm"
111
+ className={cn(
112
+ 'group h-auto min-h-9 max-w-[15rem] gap-1 shadow-none [&_svg]:size-3.5 [&_svg]:shrink-0',
113
+ navItemCls,
114
+ (isOpen || isActive) && navItemActiveCls,
115
+ isOpen && 'border-0 dark:border dark:border-border',
116
+ )}
117
+ >
118
+ <span className={labelCls} title={item.label}>{item.label}</span>
119
+ <span className="inline-flex size-3.5 shrink-0 items-center justify-center" aria-hidden>
120
+ <ChevronDown
121
+ className={cn(
122
+ 'size-3.5 origin-center text-muted-foreground transition-transform duration-200 ease-out will-change-transform',
123
+ isOpen && 'rotate-180',
124
+ )}
125
+ />
126
+ </span>
127
+ </Button>
128
+
129
+ {isOpen && (
130
+ renderDesktopDropdown
131
+ ? renderDesktopDropdown({ item, isOpen, isActive, close: closeDropdown, defaultPopover, defaultItems })
132
+ : defaultPopover
133
+ )}
134
+ </div>
135
+ );
136
+ }
137
+
138
+ const active = isActivePath(item.href);
139
+ return (
140
+ <Link
141
+ key={item.href}
142
+ href={item.href}
143
+ className={cn(navItemCls, active && navItemActiveCls)}
144
+ >
145
+ <span className={labelCls} title={item.label}>{item.label}</span>
146
+ </Link>
147
+ );
148
+ };
149
+
150
+ const hasOverflow = overflowItems.length > 0;
151
+ const isMoreOpen = openDropdownKey === '__overflow-more';
152
+ const moreActive = isMoreOpen || overflowItems.some((i) => isGroupActive(i));
153
+
154
+ return (
155
+ <>
156
+ {primaryItems.map(renderItem)}
157
+
158
+ {hasOverflow && (
159
+ <div
160
+ className="relative"
161
+ onMouseEnter={() => scheduleOpen('__overflow-more')}
162
+ onMouseLeave={() => scheduleClose('__overflow-more')}
163
+ >
164
+ <Button
165
+ variant="ghost"
166
+ size="sm"
167
+ className={cn(
168
+ 'group h-auto min-h-9 max-w-[15rem] gap-1 shadow-none [&_svg]:size-3.5 [&_svg]:shrink-0',
169
+ navItemCls,
170
+ moreActive && navItemActiveCls,
171
+ isMoreOpen && 'border-0 dark:border dark:border-border',
172
+ )}
173
+ >
174
+ <span className={labelCls}>More</span>
175
+ <span className="inline-flex size-3.5 shrink-0 items-center justify-center" aria-hidden>
176
+ <ChevronDown
177
+ className={cn(
178
+ 'size-3.5 origin-center text-muted-foreground transition-transform duration-200 ease-out will-change-transform',
179
+ isMoreOpen && 'rotate-180',
180
+ )}
181
+ />
182
+ </span>
183
+ </Button>
184
+
185
+ {isMoreOpen && (
186
+ <div
187
+ className={cn(popoverCls, 'left-auto right-0')}
188
+ onMouseEnter={() => { scheduleOpen('__overflow-more'); }}
189
+ onMouseLeave={() => scheduleClose('__overflow-more')}
190
+ >
191
+ {overflowItems.map((item) => {
192
+ const active = isGroupActive(item);
193
+ return (
194
+ <div key={`overflow-${item.href}`} className="rounded-full">
195
+ <Link href={item.href} className={subMenuLinkCls(active)}>
196
+ <span className="min-w-0 truncate" title={item.label}>{item.label}</span>
197
+ </Link>
198
+ </div>
199
+ );
200
+ })}
201
+ </div>
202
+ )}
203
+ </div>
204
+ )}
205
+ </>
206
+ );
207
+ }
@@ -108,7 +108,7 @@ export function PublicMobileDrawer(props: PublicMobileDrawerProps = {}) {
108
108
  }}
109
109
  >
110
110
  {/* Scrollable content */}
111
- <div className="flex-1 min-h-0 overflow-y-auto px-4 py-4 space-y-5">
111
+ <div className="flex-1 min-h-0 overflow-y-auto px-4 py-4 pb-10 space-y-5">
112
112
  {hasSessionUser && (
113
113
  <div className="px-2">
114
114
  <h3 className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">
@@ -11,6 +11,8 @@ import type {
11
11
  PublicNavbarShellConfig,
12
12
  PublicNavbarPosition,
13
13
  PublicNavbarVariant,
14
+ PublicNavLayout,
15
+ PublicNavbarHeight,
14
16
  } from '../navbarTypes';
15
17
 
16
18
  export interface PublicNavbarConfig {
@@ -25,6 +27,39 @@ export interface PublicNavbarConfig {
25
27
  navbarPosition?: PublicNavbarPosition;
26
28
  renderDesktopDropdown?: PublicDesktopDropdownRenderer;
27
29
  desktopMaxPrimaryItems?: number;
30
+ /**
31
+ * Desktop nav arrangement.
32
+ * - `default` — brand left | nav centered | actions right
33
+ * - `brand-left` — brand left | nav after brand | actions pushed right
34
+ * - `centered` — all items centered in one row
35
+ * - `split` — brand left | actions right | no desktop nav (drawer only)
36
+ * @default 'default'
37
+ */
38
+ navLayout?: PublicNavLayout;
39
+ /**
40
+ * Navbar vertical padding / height.
41
+ * - `sm` → compact
42
+ * - `md` → default
43
+ * - `lg` → tall
44
+ * @default 'md'
45
+ */
46
+ navbarHeight?: PublicNavbarHeight;
47
+ /**
48
+ * Slide navbar off-screen on scroll-down; restore on scroll-up.
49
+ * @default false
50
+ */
51
+ hideNavOnScroll?: boolean;
52
+ /**
53
+ * Transparent at page top, opaque after scrolling.
54
+ * Pair with `navbarVariant="floating"` for best results.
55
+ * @default false
56
+ */
57
+ transparent?: boolean;
58
+ /**
59
+ * scrollY threshold (px) for transparent → opaque transition.
60
+ * @default 40
61
+ */
62
+ transparentThreshold?: number;
28
63
  }
29
64
 
30
65
  export interface PublicNavbarProps {
@@ -32,8 +67,6 @@ export interface PublicNavbarProps {
32
67
  }
33
68
 
34
69
  export function PublicNavbar({ config }: PublicNavbarProps) {
35
- const rounding = config.shell?.rounding;
36
- const containerClassName = config.shell?.className;
37
70
  const navigation = config.navigation ?? [];
38
71
 
39
72
  return (
@@ -43,18 +76,23 @@ export function PublicNavbar({ config }: PublicNavbarProps) {
43
76
  brandHref={config.brandHref}
44
77
  navigation={navigation}
45
78
  userMenu={config.userMenu}
46
- containerClassName={containerClassName}
79
+ containerClassName={config.shell?.className}
47
80
  navbarVariant={config.navbarVariant}
48
81
  navbarPosition={config.navbarPosition}
49
82
  renderDesktopDropdown={config.renderDesktopDropdown}
50
83
  desktopMaxPrimaryItems={config.desktopMaxPrimaryItems}
51
- rounding={rounding}
84
+ rounding={config.shell?.rounding}
85
+ navLayout={config.navLayout}
86
+ navbarHeight={config.navbarHeight}
87
+ hideNavOnScroll={config.hideNavOnScroll}
88
+ transparent={config.transparent}
89
+ transparentThreshold={config.transparentThreshold}
52
90
  />
53
91
  <PublicMobileDrawer
54
92
  navigation={navigation}
55
93
  userMenu={config.userMenu}
56
- containerClassName={containerClassName}
57
- rounding={rounding}
94
+ containerClassName={config.shell?.className}
95
+ rounding={config.shell?.rounding}
58
96
  />
59
97
  </>
60
98
  );