@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.
- package/README.md +4 -0
- package/package.json +12 -12
- package/src/components/account/AccountInline.tsx +122 -0
- package/src/components/account/AccountMenu.test.tsx +110 -0
- package/src/components/account/AccountMenu.tsx +117 -0
- package/src/components/account/accountContext.ts +14 -0
- package/src/components/account/index.ts +13 -0
- package/src/components/account/triggers.tsx +100 -0
- package/src/{layouts/SettingsLayout/hooks → components/account}/useAccountMenuItems.tsx +45 -17
- package/src/components/account/useAccountModel.tsx +76 -0
- package/src/components/errors/AppErrorFallback.test.tsx +33 -0
- package/src/components/errors/AppErrorFallback.tsx +42 -0
- package/src/components/errors/ErrorBoundary.tsx +4 -41
- package/src/components/errors/ErrorLayout.tsx +17 -1
- package/src/components/errors/README.md +6 -1
- package/src/components/errors/chunkReload.test.ts +89 -0
- package/src/components/errors/index.ts +2 -0
- package/src/layouts/AppLayout/BaseApp.tsx +10 -55
- package/src/layouts/AppLayout/index.ts +1 -0
- package/src/layouts/AuthLayout/components/oauth/OAuthSection.tsx +17 -11
- package/src/layouts/AuthLayout/components/oauth/errors/errorCopy.ts +1 -1
- package/src/layouts/AuthLayout/components/oauth/notice/OAuthNotice.tsx +1 -4
- package/src/layouts/AuthLayout/components/oauth/notice/OAuthNoticeView.test.tsx +17 -3
- package/src/layouts/AuthLayout/components/oauth/notice/OAuthNoticeView.tsx +21 -25
- package/src/layouts/AuthLayout/components/steps/IdentifierStep.tsx +2 -2
- package/src/layouts/AuthLayout/styles/oauth.css +45 -0
- package/src/layouts/PrivateLayout/PrivateLayout.tsx +32 -1
- package/src/layouts/PrivateLayout/README.md +23 -5
- package/src/layouts/PrivateLayout/brand.test.tsx +208 -0
- package/src/layouts/PrivateLayout/components/PrivateContent.test.tsx +230 -0
- package/src/layouts/PrivateLayout/components/PrivateContent.tsx +86 -19
- package/src/layouts/PrivateLayout/components/PrivateSidebar.tsx +4 -2
- package/src/layouts/PrivateLayout/components/PrivateSidebarAccount.tsx +16 -123
- package/src/layouts/PrivateLayout/components/PrivateSidebarTrigger.tsx +3 -3
- package/src/layouts/PrivateLayout/components/PrivateTopBarAccount.tsx +27 -0
- package/src/layouts/PrivateLayout/components/SidebarBody.test.tsx +73 -0
- package/src/layouts/PrivateLayout/components/SidebarBody.tsx +13 -4
- package/src/layouts/PrivateLayout/components/SidebarBrand.tsx +25 -14
- package/src/layouts/PrivateLayout/components/SidebarNavGroup.tsx +13 -8
- package/src/layouts/PrivateLayout/components/SidebarNavItem.tsx +2 -2
- package/src/layouts/PrivateLayout/components/SidebarPeek.tsx +1 -1
- package/src/layouts/PrivateLayout/context.tsx +15 -3
- package/src/layouts/PrivateLayout/geometry.test.ts +44 -0
- package/src/layouts/PrivateLayout/geometry.ts +40 -16
- package/src/layouts/PrivateLayout/hooks/useLandingRoute.ts +16 -0
- package/src/layouts/PrivateLayout/hooks/useLayoutVisual.ts +2 -2
- package/src/layouts/PrivateLayout/hooks/useSidebarPeek.tsx +1 -1
- package/src/layouts/PrivateLayout/index.ts +2 -0
- package/src/layouts/PrivateLayout/topBar.test.tsx +213 -0
- package/src/layouts/PrivateLayout/types.ts +27 -2
- package/src/layouts/PublicLayout/components/UserMenu.tsx +24 -297
- package/src/layouts/PublicLayout/components/index.ts +2 -2
- package/src/layouts/PublicLayout/hooks/useNavbarScroll.ts +13 -7
- package/src/layouts/SettingsLayout/components/AccountMenuButton.tsx +19 -60
- package/src/layouts/SettingsLayout/hooks/index.ts +2 -2
- package/src/shared/ProjectContext.tsx +18 -0
- package/src/testing/MockAuthProvider.tsx +5 -2
- /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 ?
|
|
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
|
|
93
|
-
const maxScroll =
|
|
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 {
|
|
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:
|
|
30
|
-
*
|
|
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
|
-
<
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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 '
|
|
15
|
-
export type { UseAccountMenuItemsOptions } from '
|
|
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
|
};
|
|
File without changes
|