@djangocfg/layouts 2.1.257 → 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 (29) 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 +4 -1
  9. package/src/layouts/PublicLayout/PublicLayout.tsx +31 -8
  10. package/src/layouts/PublicLayout/components/PublicFooter/FooterProjectInfo.tsx +17 -24
  11. package/src/layouts/PublicLayout/components/PublicFooter/PublicFooter.tsx +79 -95
  12. package/src/layouts/PublicLayout/components/PublicFooter/index.ts +2 -0
  13. package/src/layouts/PublicLayout/components/PublicFooter/types.ts +41 -31
  14. package/src/layouts/PublicLayout/components/PublicMobileDrawer.tsx +69 -30
  15. package/src/layouts/PublicLayout/components/PublicNavbar.tsx +24 -34
  16. package/src/layouts/PublicLayout/components/PublicNavigation.tsx +162 -94
  17. package/src/layouts/PublicLayout/components/ThemeBrandMark.tsx +83 -0
  18. package/src/layouts/PublicLayout/components/index.ts +2 -0
  19. package/src/layouts/PublicLayout/index.ts +5 -0
  20. package/src/layouts/PublicLayout/navbarTypes.ts +8 -0
  21. package/src/layouts/PublicLayout/publicShellShadow.ts +12 -0
  22. package/src/layouts/_components/UserMenu.tsx +2 -2
  23. package/src/layouts/types/index.ts +9 -1
  24. package/src/layouts/types/providers.types.ts +10 -0
  25. package/src/theme/ThemeStyleBridge.tsx +41 -0
  26. package/src/theme/buildThemeStyleSheet.ts +71 -0
  27. package/src/theme/index.ts +16 -0
  28. package/src/theme/themeStyle.types.ts +89 -0
  29. package/src/theme/themeStylePresets.ts +202 -0
@@ -13,9 +13,11 @@ 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
22
  import { useMobileNavPanel } from '../hooks';
21
23
 
@@ -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,7 +43,8 @@ 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
50
  const { mounted, visible } = useMobileNavPanel({
@@ -63,6 +71,9 @@ export function PublicMobileDrawer(props: PublicMobileDrawerProps = {}) {
63
71
 
64
72
  if (!mounted) return null;
65
73
 
74
+ const hasSessionUser = Boolean(isAuthenticated && user);
75
+ const showSignInFooter = !hasSessionUser;
76
+
66
77
  return (
67
78
  <>
68
79
  {mobileMenuOpen && (
@@ -76,24 +87,29 @@ export function PublicMobileDrawer(props: PublicMobileDrawerProps = {}) {
76
87
  {/* Outer shell must not capture taps when the panel is closed: with pointer-events-none on the
77
88
  inner panel, events would otherwise hit this transparent fixed layer (z-1000) and block the page. */}
78
89
  <div
79
- className="pointer-events-none fixed inset-x-0 z-1000 lg:hidden px-4 sm:px-6 lg:px-8"
80
- 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
+ }}
81
95
  >
82
96
  <div
83
- 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 || ''} ${
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,
84
102
  visible
85
103
  ? 'pointer-events-auto opacity-100 translate-y-0 scale-100'
86
- : 'pointer-events-none opacity-0 -translate-y-2 scale-[0.985]'
87
- }`}
104
+ : 'pointer-events-none opacity-0 -translate-y-2 scale-[0.985]',
105
+ )}
88
106
  style={{
89
107
  maxHeight: 'min(var(--public-navbar-mobile-drawer-max-height, calc(100dvh - 5rem - 12px)), calc(100dvh - 12px))',
90
- backgroundColor: 'hsl(var(--background) / 0.72)',
91
- backdropFilter: 'blur(10px)',
92
108
  }}
93
109
  >
94
110
  {/* Scrollable content */}
95
111
  <div className="flex-1 min-h-0 overflow-y-auto px-4 py-4 space-y-5">
96
- {isAuthenticated && (
112
+ {hasSessionUser && (
97
113
  <div className="px-2">
98
114
  <h3 className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">
99
115
  {labels.quickActions}
@@ -101,7 +117,7 @@ export function PublicMobileDrawer(props: PublicMobileDrawerProps = {}) {
101
117
  </div>
102
118
  )}
103
119
 
104
- {isAuthenticated && (
120
+ {hasSessionUser && (
105
121
  <UserMenu variant="mobile" groups={userMenu?.groups} authPath={userMenu?.authPath} i18n={userMenu?.i18n} />
106
122
  )}
107
123
 
@@ -116,31 +132,51 @@ export function PublicMobileDrawer(props: PublicMobileDrawerProps = {}) {
116
132
  {mobileNavigation.map((item) => {
117
133
  const childItems = item.items ?? [];
118
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;
119
140
  return (
120
141
  <div key={item.href}>
121
142
  <Link
122
143
  href={item.href}
123
144
  onClick={closeMobileMenu}
124
- className={`block px-3 py-2.5 rounded-lg text-[15px] font-medium transition-colors ${
125
- isActivePath(item.href)
126
- ? 'bg-accent/55 text-foreground'
127
- : 'text-foreground hover:bg-accent/70 hover:text-accent-foreground'
128
- }`}
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
+ )}
129
155
  >
130
156
  {item.label}
131
157
  </Link>
132
158
  {hasChildNav && (
133
- <div className="ml-3 mt-1 space-y-1 border-l border-border/40 pl-3">
134
- {childItems.map((subItem) => (
135
- <Link
136
- key={`${item.href}-${subItem.href}`}
137
- href={subItem.href}
138
- onClick={closeMobileMenu}
139
- className="block px-2 py-2 rounded-md text-sm text-muted-foreground transition-colors hover:text-foreground hover:bg-accent/40"
140
- >
141
- {subItem.label}
142
- </Link>
143
- ))}
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
+ })}
144
180
  </div>
145
181
  )}
146
182
  </div>
@@ -150,16 +186,19 @@ export function PublicMobileDrawer(props: PublicMobileDrawerProps = {}) {
150
186
  </div>
151
187
  </div>
152
188
 
153
- {!isAuthenticated && (
189
+ {showSignInFooter && (
154
190
  <div className="shrink-0 border-t border-border/50 p-4">
155
191
  <Link
156
192
  href={userMenu?.authPath || '/auth'}
157
193
  onClick={closeMobileMenu}
158
194
  className="block"
159
195
  >
160
- <Button className="w-full justify-between rounded-lg h-11">
161
- <span>{labels.signIn}</span>
162
- <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
+ />
163
202
  </Button>
164
203
  </Link>
165
204
  </div>
@@ -7,65 +7,55 @@ import { PublicNavigation } from './PublicNavigation';
7
7
 
8
8
  import type { NavigationItem, UserMenuConfig } from '../../types';
9
9
  import type { PublicDesktopDropdownRenderer } from './PublicNavigation';
10
- import type { PublicNavbarPosition, PublicNavbarVariant } from '../navbarTypes';
10
+ import type {
11
+ PublicNavbarShellConfig,
12
+ PublicNavbarPosition,
13
+ PublicNavbarVariant,
14
+ } from '../navbarTypes';
11
15
 
12
16
  export interface PublicNavbarConfig {
17
+ shell?: PublicNavbarShellConfig;
18
+ /** Brand: custom React node, or a plain string (wrapped in `<Link href={brandHref}>`). */
13
19
  brand?: React.ReactNode;
20
+ /** @default '/' */
14
21
  brandHref?: string;
15
- logo?: string;
16
- siteName?: string;
17
22
  navigation?: NavigationItem[];
18
23
  userMenu?: UserMenuConfig;
19
- containerClassName?: string;
20
24
  navbarVariant?: PublicNavbarVariant;
21
25
  navbarPosition?: PublicNavbarPosition;
22
26
  renderDesktopDropdown?: PublicDesktopDropdownRenderer;
23
27
  desktopMaxPrimaryItems?: number;
24
28
  }
25
29
 
26
- export interface PublicNavbarProps extends PublicNavbarConfig {
27
- /**
28
- * Preferred API: pass everything as a single config object.
29
- * Flat props are still supported and override `config`.
30
- */
31
- config?: PublicNavbarConfig;
30
+ export interface PublicNavbarProps {
31
+ config: PublicNavbarConfig;
32
32
  }
33
33
 
34
- export function PublicNavbar(props: PublicNavbarProps = {}) {
35
- const cfg = props.config ?? {};
36
- const brand = props.brand ?? cfg.brand;
37
- const brandHref = props.brandHref ?? cfg.brandHref;
38
- const logo = props.logo ?? cfg.logo;
39
- const siteName = props.siteName ?? cfg.siteName;
40
- const navigation = props.navigation ?? cfg.navigation ?? [];
41
- const userMenu = props.userMenu ?? cfg.userMenu;
42
- const containerClassName = props.containerClassName ?? cfg.containerClassName;
43
- const navbarVariant = props.navbarVariant ?? cfg.navbarVariant;
44
- const navbarPosition = props.navbarPosition ?? cfg.navbarPosition;
45
- const renderDesktopDropdown = props.renderDesktopDropdown ?? cfg.renderDesktopDropdown;
46
- 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 ?? [];
47
38
 
48
39
  return (
49
40
  <>
50
41
  <PublicNavigation
51
- brand={brand}
52
- brandHref={brandHref}
53
- logo={logo}
54
- siteName={siteName}
42
+ brand={config.brand}
43
+ brandHref={config.brandHref}
55
44
  navigation={navigation}
56
- userMenu={userMenu}
45
+ userMenu={config.userMenu}
57
46
  containerClassName={containerClassName}
58
- navbarVariant={navbarVariant}
59
- navbarPosition={navbarPosition}
60
- renderDesktopDropdown={renderDesktopDropdown}
61
- desktopMaxPrimaryItems={desktopMaxPrimaryItems}
47
+ navbarVariant={config.navbarVariant}
48
+ navbarPosition={config.navbarPosition}
49
+ renderDesktopDropdown={config.renderDesktopDropdown}
50
+ desktopMaxPrimaryItems={config.desktopMaxPrimaryItems}
51
+ rounding={rounding}
62
52
  />
63
53
  <PublicMobileDrawer
64
54
  navigation={navigation}
65
- userMenu={userMenu}
55
+ userMenu={config.userMenu}
66
56
  containerClassName={containerClassName}
57
+ rounding={rounding}
67
58
  />
68
59
  </>
69
60
  );
70
61
  }
71
-