@djangocfg/layouts 2.1.256 → 2.1.259

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 (36) hide show
  1. package/README.md +101 -203
  2. package/package.json +18 -18
  3. package/src/index.ts +4 -1
  4. package/src/layouts/AppLayout/AppLayout.tsx +97 -8
  5. package/src/layouts/AppLayout/BaseApp.tsx +2 -0
  6. package/src/layouts/AppLayout/index.ts +6 -0
  7. package/src/layouts/PrivateLayout/PrivateLayout.tsx +3 -1
  8. package/src/layouts/PrivateLayout/components/PrivateContent.tsx +15 -4
  9. package/src/layouts/PrivateLayout/components/PrivateSidebar.tsx +3 -3
  10. package/src/layouts/PublicLayout/PublicLayout.tsx +82 -17
  11. package/src/layouts/PublicLayout/components/PublicFooter/FooterProjectInfo.tsx +17 -24
  12. package/src/layouts/PublicLayout/components/PublicFooter/PublicFooter.tsx +79 -95
  13. package/src/layouts/PublicLayout/components/PublicFooter/index.ts +2 -0
  14. package/src/layouts/PublicLayout/components/PublicFooter/types.ts +41 -31
  15. package/src/layouts/PublicLayout/components/PublicMobileDrawer.tsx +84 -40
  16. package/src/layouts/PublicLayout/components/PublicNavbar.tsx +22 -35
  17. package/src/layouts/PublicLayout/components/PublicNavigation.tsx +184 -98
  18. package/src/layouts/PublicLayout/components/ThemeBrandMark.tsx +83 -0
  19. package/src/layouts/PublicLayout/components/index.ts +2 -0
  20. package/src/layouts/PublicLayout/context.tsx +5 -0
  21. package/src/layouts/PublicLayout/hooks/index.ts +1 -1
  22. package/src/layouts/PublicLayout/hooks/useMobileNavPanel.ts +55 -0
  23. package/src/layouts/PublicLayout/index.ts +8 -0
  24. package/src/layouts/PublicLayout/navbarTypes.ts +20 -0
  25. package/src/layouts/PublicLayout/publicShellShadow.ts +12 -0
  26. package/src/layouts/_components/PrivateSidebarAccount.tsx +16 -3
  27. package/src/layouts/_components/UserMenu.tsx +133 -30
  28. package/src/layouts/types/index.ts +10 -1
  29. package/src/layouts/types/providers.types.ts +10 -0
  30. package/src/layouts/types/ui.types.ts +9 -0
  31. package/src/theme/ThemeStyleBridge.tsx +41 -0
  32. package/src/theme/buildThemeStyleSheet.ts +71 -0
  33. package/src/theme/index.ts +16 -0
  34. package/src/theme/themeStyle.types.ts +89 -0
  35. package/src/theme/themeStylePresets.ts +202 -0
  36. package/src/layouts/PublicLayout/hooks/useFloatingPanel.ts +0 -61
@@ -13,11 +13,13 @@ import React, { useMemo } from 'react';
13
13
  import { useAuth } from '@djangocfg/api/auth';
14
14
  import { useAppT } from '@djangocfg/i18n';
15
15
  import { Button } from '@djangocfg/ui-core/components';
16
+ import { cn } from '@djangocfg/ui-core/lib';
16
17
  import { usePathnameWithoutLocale } from '../../../hooks';
17
18
 
18
19
  import { UserMenu } from '../../_components/UserMenu';
20
+ import { publicFloatingChromeClassName } from '../publicShellShadow';
19
21
  import { usePublicLayoutOptional } from '../context';
20
- import { useFloatingPanel } from '../hooks';
22
+ import { useMobileNavPanel } from '../hooks';
21
23
 
22
24
  import type { NavigationItem, UserMenuConfig } from '../../types';
23
25
 
@@ -27,6 +29,11 @@ interface PublicMobileDrawerProps {
27
29
  navigation?: NavigationItem[];
28
30
  userMenu?: UserMenuConfig;
29
31
  containerClassName?: string;
32
+ /**
33
+ * Tailwind rounding for the drawer panel. Defaults to `rounded-2xl`.
34
+ * Match `PublicNavigation` `rounding` so bar and sheet align visually.
35
+ */
36
+ rounding?: string;
30
37
  }
31
38
 
32
39
  export function PublicMobileDrawer(props: PublicMobileDrawerProps = {}) {
@@ -36,10 +43,11 @@ export function PublicMobileDrawer(props: PublicMobileDrawerProps = {}) {
36
43
  const navigation = props.navigation ?? [];
37
44
  const userMenu = props.userMenu;
38
45
  const containerClassName = props.containerClassName;
39
- const { isAuthenticated } = useAuth();
46
+ const rounding = props.rounding;
47
+ const { isAuthenticated, user } = useAuth();
40
48
  const pathname = usePathnameWithoutLocale();
41
49
  const t = useAppT();
42
- const { isRendered, isActive, onTransitionEnd } = useFloatingPanel({
50
+ const { mounted, visible } = useMobileNavPanel({
43
51
  isOpen: mobileMenuOpen,
44
52
  onClose: closeMobileMenu,
45
53
  });
@@ -61,7 +69,10 @@ export function PublicMobileDrawer(props: PublicMobileDrawerProps = {}) {
61
69
  return pathname === href || pathname.startsWith(`${href}/`);
62
70
  };
63
71
 
64
- if (!isRendered) return null;
72
+ if (!mounted) return null;
73
+
74
+ const hasSessionUser = Boolean(isAuthenticated && user);
75
+ const showSignInFooter = !hasSessionUser;
65
76
 
66
77
  return (
67
78
  <>
@@ -73,26 +84,32 @@ export function PublicMobileDrawer(props: PublicMobileDrawerProps = {}) {
73
84
  onClick={closeMobileMenu}
74
85
  />
75
86
  )}
87
+ {/* Outer shell must not capture taps when the panel is closed: with pointer-events-none on the
88
+ inner panel, events would otherwise hit this transparent fixed layer (z-1000) and block the page. */}
76
89
  <div
77
- className="fixed inset-x-0 z-1000 lg:hidden px-4 sm:px-6 lg:px-8"
78
- style={{ top: 'var(--public-navbar-mobile-drawer-top, 5rem)' }}
90
+ className="pointer-events-none fixed inset-x-0 z-1000 lg:hidden px-4 pb-3 sm:px-6 sm:pb-3 lg:px-8"
91
+ style={{
92
+ top: 'var(--public-navbar-mobile-drawer-top, 5rem)',
93
+ bottom: 0,
94
+ }}
79
95
  >
80
96
  <div
81
- onTransitionEnd={onTransitionEnd}
82
- className={`mx-auto w-full rounded-2xl border border-border/60 bg-background/95 backdrop-blur-xl shadow-2xl overflow-hidden flex flex-col transform-gpu will-change-transform transition-[transform,opacity] duration-[220ms] ease-out ${containerClassName || ''} ${
83
- isActive
84
- ? 'opacity-100 translate-y-0 scale-100'
85
- : 'opacity-0 -translate-y-2 scale-[0.985] pointer-events-none'
86
- }`}
97
+ className={cn(
98
+ 'mx-auto flex h-full min-h-0 max-h-full w-full flex-col overflow-hidden bg-background/72 backdrop-blur-[10px] dark:bg-card/80 transform-gpu will-change-transform transition-[transform,opacity] duration-[220ms] ease-out',
99
+ publicFloatingChromeClassName,
100
+ rounding ?? 'rounded-2xl',
101
+ containerClassName,
102
+ visible
103
+ ? 'pointer-events-auto opacity-100 translate-y-0 scale-100'
104
+ : 'pointer-events-none opacity-0 -translate-y-2 scale-[0.985]',
105
+ )}
87
106
  style={{
88
107
  maxHeight: 'min(var(--public-navbar-mobile-drawer-max-height, calc(100dvh - 5rem - 12px)), calc(100dvh - 12px))',
89
- backgroundColor: 'hsl(var(--background) / 0.72)',
90
- backdropFilter: 'blur(10px)',
91
108
  }}
92
109
  >
93
110
  {/* Scrollable content */}
94
111
  <div className="flex-1 min-h-0 overflow-y-auto px-4 py-4 space-y-5">
95
- {isAuthenticated && (
112
+ {hasSessionUser && (
96
113
  <div className="px-2">
97
114
  <h3 className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">
98
115
  {labels.quickActions}
@@ -100,8 +117,8 @@ export function PublicMobileDrawer(props: PublicMobileDrawerProps = {}) {
100
117
  </div>
101
118
  )}
102
119
 
103
- {isAuthenticated && (
104
- <UserMenu variant="mobile" groups={userMenu?.groups} authPath={userMenu?.authPath} />
120
+ {hasSessionUser && (
121
+ <UserMenu variant="mobile" groups={userMenu?.groups} authPath={userMenu?.authPath} i18n={userMenu?.i18n} />
105
122
  )}
106
123
 
107
124
  {/* Navigation Items */}
@@ -112,49 +129,76 @@ export function PublicMobileDrawer(props: PublicMobileDrawerProps = {}) {
112
129
  </h3>
113
130
  </div>
114
131
  <div className="space-y-1">
115
- {mobileNavigation.map((item) => (
132
+ {mobileNavigation.map((item) => {
133
+ const childItems = item.items ?? [];
134
+ const hasChildNav = childItems.length > 0;
135
+ const anyChildActive = hasChildNav && childItems.some((sub) => isActivePath(sub.href));
136
+ const parentPageActive = hasChildNav
137
+ ? isActivePath(item.href) && !anyChildActive
138
+ : isActivePath(item.href);
139
+ const parentOnlySectionOpen = hasChildNav && anyChildActive;
140
+ return (
116
141
  <div key={item.href}>
117
142
  <Link
118
143
  href={item.href}
119
144
  onClick={closeMobileMenu}
120
- className={`block px-3 py-2.5 rounded-lg text-[15px] font-medium transition-colors ${
121
- isActivePath(item.href)
122
- ? 'bg-accent/55 text-foreground'
123
- : 'text-foreground hover:bg-accent/70 hover:text-accent-foreground'
124
- }`}
145
+ title={item.label}
146
+ className={cn(
147
+ 'block min-h-11 min-w-0 max-w-full rounded-full border-0 px-5 py-3 text-[15px] font-medium transition-colors ring-0 truncate',
148
+ 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/35',
149
+ parentPageActive
150
+ ? 'border-0 bg-accent font-semibold text-foreground shadow-sm dark:border dark:border-border dark:bg-muted dark:shadow-none'
151
+ : parentOnlySectionOpen
152
+ ? 'font-semibold text-foreground'
153
+ : 'text-foreground hover:bg-accent/60 hover:text-accent-foreground',
154
+ )}
125
155
  >
126
156
  {item.label}
127
157
  </Link>
128
- {item.items && item.items.length > 0 && (
129
- <div className="ml-3 mt-1 space-y-1 border-l border-border/40 pl-3">
130
- {item.items.map((subItem) => (
131
- <Link
132
- key={`${item.href}-${subItem.href}`}
133
- href={subItem.href}
134
- onClick={closeMobileMenu}
135
- className="block px-2 py-2 rounded-md text-sm text-muted-foreground transition-colors hover:text-foreground hover:bg-accent/40"
136
- >
137
- {subItem.label}
138
- </Link>
139
- ))}
158
+ {hasChildNav && (
159
+ <div className="ml-3 mt-1.5 space-y-1 border-l border-border/40 pl-3">
160
+ {childItems.map((subItem) => {
161
+ const subActive = isActivePath(subItem.href);
162
+ return (
163
+ <Link
164
+ key={`${item.href}-${subItem.href}`}
165
+ href={subItem.href}
166
+ onClick={closeMobileMenu}
167
+ title={subItem.label}
168
+ className={cn(
169
+ 'flex min-h-11 min-w-0 max-w-full items-center rounded-full border-0 px-4 py-2.5 text-sm font-medium transition-colors ring-0 truncate',
170
+ 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/35',
171
+ subActive
172
+ ? 'border-0 bg-accent font-semibold text-foreground shadow-sm dark:border dark:border-border dark:bg-muted/90 dark:shadow-none'
173
+ : 'border-0 text-muted-foreground hover:bg-accent/55 hover:text-foreground',
174
+ )}
175
+ >
176
+ {subItem.label}
177
+ </Link>
178
+ );
179
+ })}
140
180
  </div>
141
181
  )}
142
182
  </div>
143
- ))}
183
+ );
184
+ })}
144
185
  </div>
145
186
  </div>
146
187
  </div>
147
188
 
148
- {!isAuthenticated && (
189
+ {showSignInFooter && (
149
190
  <div className="shrink-0 border-t border-border/50 p-4">
150
191
  <Link
151
192
  href={userMenu?.authPath || '/auth'}
152
193
  onClick={closeMobileMenu}
153
194
  className="block"
154
195
  >
155
- <Button className="w-full justify-between rounded-lg h-11">
156
- <span>{labels.signIn}</span>
157
- <ArrowRight className="h-4 w-4" />
196
+ <Button className="relative w-full justify-center rounded-full h-11 px-6 pr-12">
197
+ {labels.signIn}
198
+ <ArrowRight
199
+ className="pointer-events-none absolute right-4 top-1/2 h-4 w-4 -translate-y-1/2 shrink-0"
200
+ aria-hidden
201
+ />
158
202
  </Button>
159
203
  </Link>
160
204
  </div>
@@ -6,69 +6,56 @@ import { PublicMobileDrawer } from './PublicMobileDrawer';
6
6
  import { PublicNavigation } from './PublicNavigation';
7
7
 
8
8
  import type { NavigationItem, UserMenuConfig } from '../../types';
9
+ import type { PublicDesktopDropdownRenderer } from './PublicNavigation';
9
10
  import type {
10
- PublicDesktopDropdownRenderer,
11
+ PublicNavbarShellConfig,
11
12
  PublicNavbarPosition,
12
13
  PublicNavbarVariant,
13
- } from './PublicNavigation';
14
+ } from '../navbarTypes';
14
15
 
15
16
  export interface PublicNavbarConfig {
17
+ shell?: PublicNavbarShellConfig;
18
+ /** Brand: custom React node, or a plain string (wrapped in `<Link href={brandHref}>`). */
16
19
  brand?: React.ReactNode;
20
+ /** @default '/' */
17
21
  brandHref?: string;
18
- logo?: string;
19
- siteName?: string;
20
22
  navigation?: NavigationItem[];
21
23
  userMenu?: UserMenuConfig;
22
- containerClassName?: string;
23
24
  navbarVariant?: PublicNavbarVariant;
24
25
  navbarPosition?: PublicNavbarPosition;
25
26
  renderDesktopDropdown?: PublicDesktopDropdownRenderer;
26
27
  desktopMaxPrimaryItems?: number;
27
28
  }
28
29
 
29
- export interface PublicNavbarProps extends PublicNavbarConfig {
30
- /**
31
- * Preferred API: pass everything as a single config object.
32
- * Flat props are still supported and override `config`.
33
- */
34
- config?: PublicNavbarConfig;
30
+ export interface PublicNavbarProps {
31
+ config: PublicNavbarConfig;
35
32
  }
36
33
 
37
- export function PublicNavbar(props: PublicNavbarProps = {}) {
38
- const cfg = props.config ?? {};
39
- const brand = props.brand ?? cfg.brand;
40
- const brandHref = props.brandHref ?? cfg.brandHref;
41
- const logo = props.logo ?? cfg.logo;
42
- const siteName = props.siteName ?? cfg.siteName;
43
- const navigation = props.navigation ?? cfg.navigation ?? [];
44
- const userMenu = props.userMenu ?? cfg.userMenu;
45
- const containerClassName = props.containerClassName ?? cfg.containerClassName;
46
- const navbarVariant = props.navbarVariant ?? cfg.navbarVariant;
47
- const navbarPosition = props.navbarPosition ?? cfg.navbarPosition;
48
- const renderDesktopDropdown = props.renderDesktopDropdown ?? cfg.renderDesktopDropdown;
49
- const desktopMaxPrimaryItems = props.desktopMaxPrimaryItems ?? cfg.desktopMaxPrimaryItems;
34
+ export function PublicNavbar({ config }: PublicNavbarProps) {
35
+ const rounding = config.shell?.rounding;
36
+ const containerClassName = config.shell?.className;
37
+ const navigation = config.navigation ?? [];
50
38
 
51
39
  return (
52
40
  <>
53
41
  <PublicNavigation
54
- brand={brand}
55
- brandHref={brandHref}
56
- logo={logo}
57
- siteName={siteName}
42
+ brand={config.brand}
43
+ brandHref={config.brandHref}
58
44
  navigation={navigation}
59
- userMenu={userMenu}
45
+ userMenu={config.userMenu}
60
46
  containerClassName={containerClassName}
61
- navbarVariant={navbarVariant}
62
- navbarPosition={navbarPosition}
63
- renderDesktopDropdown={renderDesktopDropdown}
64
- desktopMaxPrimaryItems={desktopMaxPrimaryItems}
47
+ navbarVariant={config.navbarVariant}
48
+ navbarPosition={config.navbarPosition}
49
+ renderDesktopDropdown={config.renderDesktopDropdown}
50
+ desktopMaxPrimaryItems={config.desktopMaxPrimaryItems}
51
+ rounding={rounding}
65
52
  />
66
53
  <PublicMobileDrawer
67
54
  navigation={navigation}
68
- userMenu={userMenu}
55
+ userMenu={config.userMenu}
69
56
  containerClassName={containerClassName}
57
+ rounding={rounding}
70
58
  />
71
59
  </>
72
60
  );
73
61
  }
74
-