@djangocfg/layouts 2.1.606 → 2.1.608

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 (58) hide show
  1. package/README.md +4 -0
  2. package/package.json +12 -12
  3. package/src/components/account/AccountInline.tsx +122 -0
  4. package/src/components/account/AccountMenu.test.tsx +110 -0
  5. package/src/components/account/AccountMenu.tsx +117 -0
  6. package/src/components/account/accountContext.ts +14 -0
  7. package/src/components/account/index.ts +13 -0
  8. package/src/components/account/triggers.tsx +100 -0
  9. package/src/{layouts/SettingsLayout/hooks → components/account}/useAccountMenuItems.tsx +45 -17
  10. package/src/components/account/useAccountModel.tsx +76 -0
  11. package/src/components/errors/AppErrorFallback.test.tsx +33 -0
  12. package/src/components/errors/AppErrorFallback.tsx +42 -0
  13. package/src/components/errors/ErrorBoundary.tsx +4 -41
  14. package/src/components/errors/ErrorLayout.tsx +17 -1
  15. package/src/components/errors/README.md +6 -1
  16. package/src/components/errors/chunkReload.test.ts +89 -0
  17. package/src/components/errors/index.ts +2 -0
  18. package/src/layouts/AppLayout/BaseApp.tsx +10 -55
  19. package/src/layouts/AppLayout/index.ts +1 -0
  20. package/src/layouts/AuthLayout/components/oauth/OAuthSection.tsx +17 -11
  21. package/src/layouts/AuthLayout/components/oauth/errors/errorCopy.ts +1 -1
  22. package/src/layouts/AuthLayout/components/oauth/notice/OAuthNotice.tsx +1 -4
  23. package/src/layouts/AuthLayout/components/oauth/notice/OAuthNoticeView.test.tsx +17 -3
  24. package/src/layouts/AuthLayout/components/oauth/notice/OAuthNoticeView.tsx +21 -25
  25. package/src/layouts/AuthLayout/components/steps/IdentifierStep.tsx +2 -2
  26. package/src/layouts/AuthLayout/styles/oauth.css +45 -0
  27. package/src/layouts/PrivateLayout/PrivateLayout.tsx +32 -1
  28. package/src/layouts/PrivateLayout/README.md +23 -5
  29. package/src/layouts/PrivateLayout/brand.test.tsx +208 -0
  30. package/src/layouts/PrivateLayout/components/PrivateContent.test.tsx +230 -0
  31. package/src/layouts/PrivateLayout/components/PrivateContent.tsx +86 -19
  32. package/src/layouts/PrivateLayout/components/PrivateSidebar.tsx +4 -2
  33. package/src/layouts/PrivateLayout/components/PrivateSidebarAccount.tsx +16 -123
  34. package/src/layouts/PrivateLayout/components/PrivateSidebarTrigger.tsx +3 -3
  35. package/src/layouts/PrivateLayout/components/PrivateTopBarAccount.tsx +27 -0
  36. package/src/layouts/PrivateLayout/components/SidebarBody.test.tsx +73 -0
  37. package/src/layouts/PrivateLayout/components/SidebarBody.tsx +13 -4
  38. package/src/layouts/PrivateLayout/components/SidebarBrand.tsx +25 -14
  39. package/src/layouts/PrivateLayout/components/SidebarNavGroup.tsx +13 -8
  40. package/src/layouts/PrivateLayout/components/SidebarNavItem.tsx +2 -2
  41. package/src/layouts/PrivateLayout/components/SidebarPeek.tsx +1 -1
  42. package/src/layouts/PrivateLayout/context.tsx +15 -3
  43. package/src/layouts/PrivateLayout/geometry.test.ts +44 -0
  44. package/src/layouts/PrivateLayout/geometry.ts +40 -16
  45. package/src/layouts/PrivateLayout/hooks/useLandingRoute.ts +16 -0
  46. package/src/layouts/PrivateLayout/hooks/useLayoutVisual.ts +2 -2
  47. package/src/layouts/PrivateLayout/hooks/useSidebarPeek.tsx +1 -1
  48. package/src/layouts/PrivateLayout/index.ts +2 -0
  49. package/src/layouts/PrivateLayout/topBar.test.tsx +213 -0
  50. package/src/layouts/PrivateLayout/types.ts +27 -2
  51. package/src/layouts/PublicLayout/components/UserMenu.tsx +24 -297
  52. package/src/layouts/PublicLayout/components/index.ts +2 -2
  53. package/src/layouts/PublicLayout/hooks/useNavbarScroll.ts +13 -7
  54. package/src/layouts/SettingsLayout/components/AccountMenuButton.tsx +19 -60
  55. package/src/layouts/SettingsLayout/hooks/index.ts +2 -2
  56. package/src/shared/ProjectContext.tsx +18 -0
  57. package/src/testing/MockAuthProvider.tsx +5 -2
  58. /package/src/{layouts/PublicLayout/components → components/account}/UserAvatar.tsx +0 -0
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
 
3
- import { useEffect, useRef, useState } from 'react';
3
+ import { useEffect, useRef, useState, type RefObject } from 'react';
4
4
 
5
5
  import { useScroll } from '@djangocfg/ui-core/hooks';
6
6
 
@@ -11,6 +11,10 @@ export interface UseNavbarScrollOptions {
11
11
  transparent?: boolean;
12
12
  /** scrollY threshold for hide + transparent triggers. @default 40 */
13
13
  transparentThreshold?: number;
14
+ /** Scroll container to follow instead of the window (shells where the document never scrolls). */
15
+ target?: RefObject<HTMLElement | null>;
16
+ /** When false the hide still happens under `prefers-reduced-motion` (the caller drops the animation). @default true */
17
+ respectReducedMotion?: boolean;
14
18
  }
15
19
 
16
20
  export interface UseNavbarScrollReturn {
@@ -51,12 +55,12 @@ const DIRECTION_COMMIT_PX = 8;
51
55
  * renders.
52
56
  */
53
57
  export function useNavbarScroll(options: UseNavbarScrollOptions = {}): UseNavbarScrollReturn {
54
- const { hideNavOnScroll = false, transparent = false, transparentThreshold = 40 } = options;
58
+ const { hideNavOnScroll = false, transparent = false, transparentThreshold = 40, target, respectReducedMotion = true } = options;
55
59
 
56
60
  const active = hideNavOnScroll || transparent;
57
61
  // Subscribe to the shared store only when the feature is on. The `y` this
58
62
  // returns is already rAF-coalesced; we run cheap arithmetic on each change.
59
- const { y } = useScroll(active ? undefined : NEVER_TARGET);
63
+ const { y } = useScroll(active ? target : NEVER_TARGET);
60
64
 
61
65
  const [hidden, setHidden] = useState(false);
62
66
  const [scrolled, setScrolled] = useState(false);
@@ -84,13 +88,15 @@ export function useNavbarScroll(options: UseNavbarScrollOptions = {}): UseNavbar
84
88
  const prefersReducedMotion =
85
89
  typeof window.matchMedia === 'function' &&
86
90
  window.matchMedia('(prefers-reduced-motion: reduce)').matches;
87
- const hideEnabled = hideNavOnScroll && !prefersReducedMotion;
91
+ const hideEnabled = hideNavOnScroll && !(respectReducedMotion && prefersReducedMotion);
88
92
 
89
93
  // iOS Safari rubber-banding reports negative scrollY at the top and values
90
94
  // past maxScroll at the bottom while the page bounces back; feeding those
91
95
  // into the integrator makes the bar twitch on every overscroll pull.
92
- const doc = document.documentElement;
93
- const maxScroll = Math.max(0, doc.scrollHeight - window.innerHeight);
96
+ const el = target?.current;
97
+ const maxScroll = el
98
+ ? Math.max(0, el.scrollHeight - el.clientHeight)
99
+ : Math.max(0, document.documentElement.scrollHeight - window.innerHeight);
94
100
  const clampedY = Math.min(Math.max(0, y), maxScroll);
95
101
 
96
102
  if (transparent) {
@@ -134,7 +140,7 @@ export function useNavbarScroll(options: UseNavbarScrollOptions = {}): UseNavbar
134
140
  }
135
141
 
136
142
  lastY.current = clampedY;
137
- }, [y, active, hideNavOnScroll, transparent, transparentThreshold]);
143
+ }, [y, active, hideNavOnScroll, transparent, transparentThreshold, target, respectReducedMotion]);
138
144
 
139
145
  return { hidden, scrolled };
140
146
  }
@@ -1,14 +1,11 @@
1
1
  'use client';
2
2
 
3
- import { ChevronDown } from 'lucide-react';
4
3
  import React from 'react';
5
4
 
6
- import { useAuth } from '@djangocfg/api/auth';
7
- import { Avatar, AvatarFallback, AvatarImage, Button, MenuBuilder } from '@djangocfg/ui-core/components';
8
5
  import type { MenuItem } from '@djangocfg/ui-core/components';
9
6
  import { cn } from '@djangocfg/ui-core/lib';
10
7
 
11
- import { useAccountMenuItems } from '../hooks/useAccountMenuItems';
8
+ import { AccountAvatarTrigger, AccountFooterTrigger, AccountMenu } from '../../../components/account';
12
9
 
13
10
  export interface AccountMenuButtonProps {
14
11
  /** `row`: avatar, name and chevron (a sidebar footer; the menu opens up). `avatar`: the avatar alone (a header; the menu opens down). */
@@ -26,67 +23,29 @@ export interface AccountMenuButtonProps {
26
23
  }
27
24
 
28
25
  /**
29
- * The ready user menu: trigger button plus the shared menu (email, Settings, a shortcut per
30
- * Settings section, Sign out). Drop it into any chrome; renders nothing for a signed-out visitor.
31
- * `PrivateLayout`'s sidebar footer builds on the same `useAccountMenuItems`.
26
+ * The ready user menu: the shared `AccountMenu` with a row or avatar trigger. Drop it into any chrome; renders
27
+ * nothing for a signed-out visitor.
32
28
  */
33
- export const AccountMenuButton: React.FC<AccountMenuButtonProps> = ({
34
- variant = 'row',
35
- extra,
36
- plan,
37
- className,
38
- classNames,
39
- }) => {
40
- const { user } = useAuth();
41
- const items = useAccountMenuItems({ extra });
42
- const [open, setOpen] = React.useState(false);
43
-
44
- if (!user) return null;
45
-
46
- const name = user.display_username || user.email || 'User';
47
- const initial = (name.charAt(0) || '?').toUpperCase();
29
+ export const AccountMenuButton: React.FC<AccountMenuButtonProps> = ({ variant = 'row', extra, plan, className, classNames }) => {
48
30
  const isRow = variant === 'row';
49
-
50
31
  return (
51
- <MenuBuilder
52
- items={items}
53
- open={open}
54
- onOpenChange={setOpen}
32
+ <AccountMenu
33
+ plan={plan}
34
+ extra={extra}
35
+ signIn="none"
55
36
  side={isRow ? 'top' : 'bottom'}
56
37
  align={isRow ? 'start' : 'end'}
57
- sideOffset={8}
58
- contentClassName="w-60 p-1.5"
59
38
  >
60
- <Button
61
- type="button"
62
- variant="ghost"
63
- aria-label={name}
64
- className={cn(
65
- 'group min-w-0 select-none gap-2 rounded-lg font-normal focus-visible:ring-0',
66
- isRow ? 'h-9 w-full justify-start px-2 text-left' : 'size-9 p-0',
67
- className,
68
- )}
69
- >
70
- <Avatar className={cn('size-6 shrink-0', classNames?.avatar)}>
71
- <AvatarImage src={user.avatar ?? ''} alt={name} />
72
- <AvatarFallback className="text-xs font-medium">{initial}</AvatarFallback>
73
- </Avatar>
74
- {isRow && (
75
- <>
76
- <span className={cn('flex min-w-0 items-baseline gap-1', classNames?.name)}>
77
- <span className="truncate text-sm">{name}</span>
78
- {plan ? <span className="shrink-0 text-xs text-muted-foreground">· {plan}</span> : null}
79
- </span>
80
- <ChevronDown
81
- className={cn(
82
- 'ml-auto size-4 shrink-0 opacity-50 transition-transform duration-150 group-data-[state=open]:rotate-180',
83
- classNames?.chevron,
84
- )}
85
- aria-hidden
86
- />
87
- </>
88
- )}
89
- </Button>
90
- </MenuBuilder>
39
+ {isRow ? (
40
+ <AccountFooterTrigger
41
+ className={cn('h-9', className)}
42
+ avatarClassName={classNames?.avatar}
43
+ nameClassName={classNames?.name}
44
+ chevronClassName={classNames?.chevron}
45
+ />
46
+ ) : (
47
+ <AccountAvatarTrigger className={className} />
48
+ )}
49
+ </AccountMenu>
91
50
  );
92
51
  };
@@ -11,8 +11,8 @@ export type {
11
11
  UseSettingsSectionsResult,
12
12
  } from './useSettingsSections';
13
13
 
14
- export { useAccountMenuItems } from './useAccountMenuItems';
15
- export type { UseAccountMenuItemsOptions } from './useAccountMenuItems';
14
+ export { useAccountMenuItems } from '../../../components/account/useAccountMenuItems';
15
+ export type { UseAccountMenuItemsOptions } from '../../../components/account/useAccountMenuItems';
16
16
 
17
17
  export { useSettingsSectionState } from './useSettingsSectionState';
18
18
  export type { SettingsSectionState } from './useSettingsSectionState';
@@ -0,0 +1,18 @@
1
+ /**
2
+ * The project name `BaseApp` already receives (`project` prop), exposed to descendants so shells can use it as the
3
+ * default product name. Outside `BaseApp` (stories, tests) the hook returns `undefined` and never throws.
4
+ */
5
+
6
+ 'use client';
7
+
8
+ import React from 'react';
9
+
10
+ const ProjectNameContext = React.createContext<string | undefined>(undefined);
11
+
12
+ export function ProjectNameProvider({ value, children }: { value?: string; children: React.ReactNode }) {
13
+ return <ProjectNameContext.Provider value={value?.trim() || undefined}>{children}</ProjectNameContext.Provider>;
14
+ }
15
+
16
+ export function useProjectName(): string | undefined {
17
+ return React.useContext(ProjectNameContext);
18
+ }
@@ -20,6 +20,8 @@ export interface MockAuthProviderProps {
20
20
  isAuthenticated?: boolean;
21
21
  /** Override `isAdminUser`. Defaults to `false`. */
22
22
  isAdminUser?: boolean;
23
+ /** Override resolved auth routes (e.g. `defaultCallback`, the private landing page). */
24
+ routes?: Partial<{ auth: string; defaultCallback: string; defaultAuthCallback: string }>;
23
25
  }
24
26
 
25
27
  const noopAsync = async () => {};
@@ -54,6 +56,7 @@ export const MockAuthProvider: React.FC<MockAuthProviderProps> = ({
54
56
  isLoading = false,
55
57
  isAuthenticated,
56
58
  isAdminUser = false,
59
+ routes,
57
60
  }) => {
58
61
  const [user, setUser] = useState<UserProfile | null>(initialUser);
59
62
 
@@ -62,7 +65,7 @@ export const MockAuthProvider: React.FC<MockAuthProviderProps> = ({
62
65
  isLoading,
63
66
  isAuthenticated: isAuthenticated ?? Boolean(user),
64
67
  isAdminUser,
65
- routes: { auth: '/auth', defaultCallback: '/', defaultAuthCallback: '/auth' },
68
+ routes: { auth: '/auth', defaultCallback: '/', defaultAuthCallback: '/auth', ...routes },
66
69
 
67
70
  loadCurrentProfile: noopAsync,
68
71
  checkAuthAndRedirect: noopAsync,
@@ -93,7 +96,7 @@ export const MockAuthProvider: React.FC<MockAuthProviderProps> = ({
93
96
  return next;
94
97
  },
95
98
  uploadAvatar: async () => user ?? buildMockUser(),
96
- }), [user, isLoading, isAuthenticated, isAdminUser]);
99
+ }), [user, isLoading, isAuthenticated, isAdminUser, routes]);
97
100
 
98
101
  return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
99
102
  };