@djangocfg/layouts 2.1.355 → 2.1.357

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 (75) hide show
  1. package/package.json +17 -17
  2. package/src/layouts/AdminLayout/AdminLayout.tsx +2 -1
  3. package/src/layouts/AppLayout/AppLayout.tsx +35 -15
  4. package/src/layouts/AppLayout/BaseApp.tsx +2 -2
  5. package/src/layouts/AuthLayout/AuthLayout.tsx +26 -19
  6. package/src/layouts/AuthLayout/components/oauth/OAuthCallback.tsx +10 -4
  7. package/src/layouts/AuthLayout/components/shared/AuthButton.tsx +11 -5
  8. package/src/layouts/AuthLayout/components/shared/AuthContainer.tsx +10 -10
  9. package/src/layouts/AuthLayout/components/shared/AuthDivider.tsx +11 -5
  10. package/src/layouts/AuthLayout/components/shared/AuthError.tsx +10 -5
  11. package/src/layouts/AuthLayout/components/shared/AuthFooter.tsx +11 -5
  12. package/src/layouts/AuthLayout/components/shared/AuthHeader.tsx +10 -10
  13. package/src/layouts/AuthLayout/components/shared/AuthLink.tsx +11 -5
  14. package/src/layouts/AuthLayout/components/shared/AuthOTPInput.tsx +28 -20
  15. package/src/layouts/AuthLayout/components/shared/TermsCheckbox.tsx +11 -5
  16. package/src/layouts/AuthLayout/components/steps/IdentifierStep.tsx +12 -4
  17. package/src/layouts/AuthLayout/components/steps/OTPStep.tsx +9 -4
  18. package/src/layouts/AuthLayout/components/steps/SetupStep/SetupComplete.tsx +12 -5
  19. package/src/layouts/AuthLayout/components/steps/SetupStep/SetupLoading.tsx +9 -4
  20. package/src/layouts/AuthLayout/components/steps/SetupStep/SetupQRCode.tsx +11 -5
  21. package/src/layouts/AuthLayout/components/steps/SetupStep/index.tsx +15 -5
  22. package/src/layouts/AuthLayout/components/steps/TwoFactorStep.tsx +9 -4
  23. package/src/layouts/AuthLayout/context.tsx +35 -13
  24. package/src/layouts/AuthLayout/shells/AuthShell.tsx +11 -4
  25. package/src/layouts/AuthLayout/shells/CenteredShell.tsx +10 -4
  26. package/src/layouts/AuthLayout/shells/SplitShell.tsx +10 -4
  27. package/src/layouts/AuthLayout/shells/context.tsx +16 -5
  28. package/src/layouts/PrivateLayout/PrivateLayout.tsx +32 -247
  29. package/src/layouts/PrivateLayout/components/PrivateSidebar.tsx +115 -426
  30. package/src/layouts/{_components → PrivateLayout/components}/PrivateSidebarAccount.tsx +40 -19
  31. package/src/layouts/PrivateLayout/components/SidebarBrand.tsx +165 -0
  32. package/src/layouts/{_components → PrivateLayout/components}/SidebarFeatured.tsx +2 -2
  33. package/src/layouts/PrivateLayout/components/SidebarNavGroup.tsx +189 -0
  34. package/src/layouts/PrivateLayout/components/SidebarNavItem.tsx +137 -0
  35. package/src/layouts/PrivateLayout/components/SidebarSlots.tsx +71 -0
  36. package/src/layouts/PrivateLayout/components/index.ts +4 -0
  37. package/src/layouts/PrivateLayout/context.tsx +211 -0
  38. package/src/layouts/PrivateLayout/density.ts +48 -0
  39. package/src/layouts/PrivateLayout/hooks/index.ts +13 -0
  40. package/src/layouts/PrivateLayout/hooks/useAuthGuard.ts +54 -0
  41. package/src/layouts/PrivateLayout/hooks/useHoverExpand.ts +103 -0
  42. package/src/layouts/PrivateLayout/hooks/useLayoutVisual.ts +113 -0
  43. package/src/layouts/PrivateLayout/hooks/useShellVisualState.ts +207 -0
  44. package/src/layouts/PrivateLayout/hooks/useSidebarKeyboard.ts +115 -0
  45. package/src/layouts/PrivateLayout/index.ts +2 -2
  46. package/src/layouts/PrivateLayout/types.ts +187 -0
  47. package/src/layouts/ProfileLayout/ProfileLayout.tsx +44 -183
  48. package/src/layouts/ProfileLayout/README.md +58 -0
  49. package/src/layouts/ProfileLayout/components/ApiKeySection/ApiKeySection.tsx +197 -0
  50. package/src/layouts/ProfileLayout/components/ApiKeySection/context.tsx +159 -0
  51. package/src/layouts/ProfileLayout/components/ApiKeySection/index.ts +3 -0
  52. package/src/layouts/ProfileLayout/components/ProfileHeader.tsx +110 -0
  53. package/src/layouts/ProfileLayout/components/ProfileTab.tsx +29 -0
  54. package/src/layouts/ProfileLayout/components/{TwoFactorSection.tsx → TwoFactorSection/TwoFactorSection.tsx} +1 -1
  55. package/src/layouts/ProfileLayout/components/TwoFactorSection/index.ts +1 -0
  56. package/src/layouts/ProfileLayout/components/index.ts +4 -2
  57. package/src/layouts/ProfileLayout/context.tsx +4 -6
  58. package/src/layouts/ProfileLayout/hooks/index.ts +2 -0
  59. package/src/layouts/ProfileLayout/hooks/useProfileTabs.ts +43 -0
  60. package/src/layouts/ProfileLayout/index.ts +6 -3
  61. package/src/layouts/ProfileLayout/types.ts +37 -0
  62. package/src/layouts/{_components → PublicLayout/components}/UserMenu.tsx +3 -3
  63. package/src/layouts/PublicLayout/components/index.ts +4 -0
  64. package/src/layouts/PublicLayout/footers/DefaultFooter/DefaultFooter.tsx +12 -2
  65. package/src/layouts/PublicLayout/navbars/MinimalNavbar/MinimalNavbar.tsx +1 -1
  66. package/src/layouts/PublicLayout/primitives/NavActions.tsx +44 -3
  67. package/src/layouts/PublicLayout/primitives/NavBrand.tsx +4 -2
  68. package/src/layouts/PublicLayout/primitives/NavDesktopItems.tsx +42 -2
  69. package/src/layouts/PublicLayout/shared/MobileDrawerShell.tsx +1 -1
  70. package/src/layouts/PublicLayout/shared/NavbarShell.tsx +60 -1
  71. package/src/layouts/_components/index.ts +2 -7
  72. package/src/layouts/index.ts +9 -4
  73. package/src/layouts/ProfileLayout/__tests__/TwoFactorSection.test.tsx +0 -234
  74. package/src/layouts/ProfileLayout/components/ProfileForm.tsx +0 -198
  75. /package/src/layouts/{_components → PublicLayout/components}/UserAvatar.tsx +0 -0
@@ -8,165 +8,34 @@
8
8
 
9
9
  'use client';
10
10
 
11
- import React, { ReactNode, useEffect, useState } from 'react';
12
- import { useRouter } from 'next/navigation';
11
+ import React, { ReactNode } from 'react';
13
12
 
14
- import { useAuth } from '@djangocfg/api/auth';
15
13
  import { Preloader } from '@djangocfg/ui-core/components';
16
14
  import { SidebarInset, SidebarProvider } from '@djangocfg/ui-core/components';
17
15
 
18
16
  import type { AppLayoutPublicChrome } from '../AppLayout/AppLayout';
19
17
  import type { LayoutVisualConfig } from '../types';
20
- import { UserMenuConfig } from '../types';
21
18
  import { PrivateContent, PrivateSidebar } from './components';
22
-
23
- import type { LucideIcon as LucideIconType } from 'lucide-react';
24
-
25
- export interface SidebarItem {
26
- label: string;
27
- href: string;
28
- icon?: string | LucideIconType;
29
- badge?: string | number;
30
- /** Visual style of `badge`: `'count'` (default, neutral pill) or `'pill'` (accent-tinted, e.g. "lite"/"new"). */
31
- badgeVariant?: 'count' | 'pill';
32
- /** Collapsed rail: shown in tooltip; defaults to `label`. */
33
- tooltip?: string;
34
- }
35
-
36
- export interface SidebarGroupConfig {
37
- /** Group label displayed above items */
38
- label: string;
39
- /** Items in this group */
40
- items: SidebarItem[];
41
- /** If true, group is only shown when it has items (for dynamic groups like extensions) */
42
- dynamic?: boolean;
43
- /** Render group as an accordion (Mailersend/Vercel-style). Label becomes a clickable trigger. */
44
- collapsible?: boolean;
45
- /** Initial open state for collapsible groups. Auto-expanded if any child is active. Default `false`. */
46
- defaultOpen?: boolean;
47
- /** Icon for the group trigger (only when `collapsible`). */
48
- icon?: string | LucideIconType;
49
- /**
50
- * Hide per-item icons inside the group. Defaults to `true` when `collapsible`,
51
- * `false` otherwise (Mailersend convention: icons live on the trigger, not on children).
52
- */
53
- hideItemIcons?: boolean;
54
- }
55
-
56
- /** Active-state visual treatment for sidebar nav items. */
57
- export type SidebarActiveIndicator = 'background' | 'rail' | 'both';
58
-
59
- /** Rendering of group labels. `'uppercase'` is the legacy ultra-light caps; `'plain'` is sm bold. */
60
- export type SidebarGroupLabelStyle = 'uppercase' | 'plain';
61
-
62
- /** Featured CTA tile rendered below groups. */
63
- export interface SidebarFeaturedConfig {
64
- icon?: string | LucideIconType;
65
- label: string;
66
- href: string;
67
- badge?: string;
68
- accent?: 'green' | 'blue' | 'amber' | 'primary';
69
- }
70
-
71
- export interface SidebarConfig {
72
- /** Grouped items with labels */
73
- groups: SidebarGroupConfig[];
74
- /** Home link href */
75
- homeHref?: string;
76
- /**
77
- * Custom block inside the scrollable nav column, **above** all `groups`
78
- * (below the brand header, same horizontal padding as nav).
79
- */
80
- menuStart?: ReactNode;
81
- /**
82
- * Custom block inside the scrollable nav column, **below** all `groups`
83
- * (above `footer` + account block).
84
- */
85
- menuEnd?: ReactNode;
86
- /**
87
- * Keep `menuStart` visible when the desktop sidebar is collapsed to the
88
- * icon rail. Default `false` — most slot content is full-width and looks
89
- * broken at ~56px. Set `true` only when the slot renders well in compact mode.
90
- */
91
- menuStartShowOnCollapsed?: boolean;
92
- /** Same as `menuStartShowOnCollapsed`, but for `menuEnd`. Default `false`. */
93
- menuEndShowOnCollapsed?: boolean;
94
- /** Custom footer component rendered at the bottom of the sidebar */
95
- footer?: ReactNode;
96
- /** Active-state visual on nav items. Default `'background'` (legacy). */
97
- activeIndicator?: SidebarActiveIndicator;
98
- /** Style of group labels. Default `'uppercase'` (legacy). Collapsible groups always use `plain`. */
99
- groupLabelStyle?: SidebarGroupLabelStyle;
100
- /** Featured CTA tile rendered below all groups, above `menuEnd`. */
101
- featured?: SidebarFeaturedConfig;
102
- }
103
-
104
- export interface HeaderConfig {
105
- /** Custom header brand node (same idea as PublicNavbar `brand`). */
106
- brand?: ReactNode;
107
- /** Shown next to the logo when the sidebar is expanded */
108
- title?: string;
109
- /**
110
- * Brand mark in the sidebar header (Lucide icon name or component).
111
- * If omitted, a single-letter monogram from `brandLetter` / `title` is shown.
112
- */
113
- brandIcon?: string | LucideIconType;
114
- /**
115
- * Monogram when `brandIcon` is not set (one visible character).
116
- * Defaults to the first letter of `title`, uppercased.
117
- */
118
- brandLetter?: string;
119
- /** User menu groups (account panel in the sidebar footer) */
120
- groups?: UserMenuConfig['groups'];
121
- /** Auth page path (for sign in button) */
122
- authPath?: string;
123
- /** Subtitle under the display name in the sidebar footer (e.g. "Max plan"). */
124
- userPlan?: string;
125
- /** Optional secondary action button rendered inside the footer trigger (e.g. Get apps download button). */
126
- footerSecondaryAction?: {
127
- icon: string | LucideIconType;
128
- href?: string;
129
- onClick?: () => void;
130
- ariaLabel: string;
131
- /** Show pulsing accent dot on the action (Claude-style "new"). */
132
- pulse?: boolean;
133
- };
134
- }
135
-
136
- export interface PrivateLayoutProps {
137
- children: ReactNode;
138
- /** Sidebar configuration */
139
- sidebar?: SidebarConfig;
140
- /** Title + account links (no top navbar — title is used in the sidebar chrome) */
141
- header?: HeaderConfig;
142
- /**
143
- * Path for active nav highlighting. With `@djangocfg/nextjs` i18n routing, pass `usePathname()` from
144
- * `@djangocfg/nextjs/i18n/navigation` (no `/[locale]` segment). If omitted, uses `next/navigation` (includes locale).
145
- */
146
- pathname?: string;
147
- /** Content padding */
148
- contentPadding?: 'none' | 'default';
149
- /**
150
- * Content scroll behaviour.
151
- * - `'auto'` (default) — the shell scroll-area scrolls vertically.
152
- * - `'hidden'` — shell does NOT scroll; use for full-height pages (e.g. Kanban)
153
- * where children manage their own scroll surfaces.
154
- */
155
- contentScroll?: 'auto' | 'hidden';
156
- /**
157
- * Visual style of the shell. Defaults to `'boxed'` (inset rounded card on a
158
- * sidebar-coloured canvas). Pass `{ variant: 'full-bleed' }` for the legacy
159
- * edge-to-edge layout.
160
- */
161
- visual?: LayoutVisualConfig;
162
- /**
163
- * Skip the built-in auth guard. Useful for static showcases / playground
164
- * embeds where there's no real session. Default `true` (guard on).
165
- */
166
- requireAuth?: boolean;
167
- /** Reserved for `AppLayout` passthrough (`publicChrome`); unused in this layout. */
168
- publicChrome?: AppLayoutPublicChrome;
169
- }
19
+ import { useAuthGuard } from './hooks';
20
+ import { useLayoutVisual } from './hooks';
21
+
22
+ import type {
23
+ HeaderConfig,
24
+ PrivateLayoutProps,
25
+ SidebarConfig,
26
+ } from './types';
27
+
28
+ export type {
29
+ SidebarItem,
30
+ SidebarGroupConfig,
31
+ SidebarConfig,
32
+ HeaderConfig,
33
+ SidebarActiveIndicator,
34
+ SidebarGroupLabelStyle,
35
+ SidebarFeaturedConfig,
36
+ } from './types';
37
+
38
+ export { PrivateLayoutProps };
170
39
 
171
40
  export function PrivateLayout({
172
41
  children,
@@ -178,25 +47,19 @@ export function PrivateLayout({
178
47
  visual,
179
48
  requireAuth = true,
180
49
  }: PrivateLayoutProps) {
181
- const { isAuthenticated, isLoading, saveRedirectUrl } = useAuth();
182
- const router = useRouter();
183
- const [isRedirecting, setIsRedirecting] = useState(false);
50
+ const { isLoading, loadingText } = useAuthGuard({
51
+ requireAuth,
52
+ authPath: header?.authPath,
53
+ });
184
54
 
185
- useEffect(() => {
186
- if (!requireAuth) return;
187
- if (!isLoading && !isAuthenticated && !isRedirecting) {
188
- const currentUrl = window.location.pathname + window.location.search;
189
- saveRedirectUrl(currentUrl);
190
- setIsRedirecting(true);
191
- router.push(header?.authPath || '/auth');
192
- }
193
- }, [requireAuth, isAuthenticated, isLoading, isRedirecting, router, saveRedirectUrl, header?.authPath]);
55
+ const { providerStyle, providerClassName, insetClassName, sidebarVariant } =
56
+ useLayoutVisual(visual);
194
57
 
195
- if (requireAuth && (isLoading || isRedirecting || !isAuthenticated)) {
58
+ if (isLoading) {
196
59
  return (
197
60
  <Preloader
198
61
  variant="fullscreen"
199
- text={isRedirecting ? 'Redirecting to login...' : 'Authenticating...'}
62
+ text={loadingText}
200
63
  size="lg"
201
64
  backdrop={true}
202
65
  backdropOpacity={80}
@@ -204,14 +67,11 @@ export function PrivateLayout({
204
67
  );
205
68
  }
206
69
 
207
- const variant: LayoutVisualConfig['variant'] = visual?.variant ?? 'boxed';
208
- const sidebarVariant = variant === 'boxed' ? 'inset' : 'sidebar';
209
-
210
70
  return (
211
71
  <SidebarProvider
212
72
  defaultOpen={true}
213
- style={resolveProviderStyle(visual)}
214
- className={resolveProviderClassName(visual)}
73
+ style={providerStyle}
74
+ className={providerClassName}
215
75
  >
216
76
  {sidebar && (
217
77
  <PrivateSidebar
@@ -222,7 +82,7 @@ export function PrivateLayout({
222
82
  />
223
83
  )}
224
84
 
225
- <SidebarInset className={resolveInsetClassName(visual)}>
85
+ <SidebarInset className={insetClassName}>
226
86
  <PrivateContent
227
87
  padding={contentPadding}
228
88
  scroll={contentScroll}
@@ -235,78 +95,3 @@ export function PrivateLayout({
235
95
  </SidebarProvider>
236
96
  );
237
97
  }
238
-
239
- /** CSS variables consumed by the boxed `SidebarInset` (margin + radius). */
240
- function resolveProviderStyle(visual: LayoutVisualConfig | undefined): React.CSSProperties | undefined {
241
- if ((visual?.variant ?? 'boxed') !== 'boxed') return undefined;
242
- const inset = normaliseInset(visual?.inset);
243
- return {
244
- ['--app-shell-inset-x' as string]: `${inset.x}px`,
245
- ['--app-shell-inset-y' as string]: `${inset.y}px`,
246
- ['--app-shell-radius' as string]: BOXED_RADIUS_REM[visual?.radius ?? '2xl'],
247
- };
248
- }
249
-
250
- /**
251
- * Statically-known Tailwind classes for the boxed inset. Margin and radius are
252
- * driven by the CSS variables set in `resolveProviderStyle`, so JIT can fully
253
- * extract these classes at build time.
254
- */
255
- const BOXED_INSET_CLASS = [
256
- 'flex flex-col',
257
- 'md:peer-data-[variant=inset]:my-[var(--app-shell-inset-y)]',
258
- 'md:peer-data-[variant=inset]:mr-[var(--app-shell-inset-x)]',
259
- 'md:peer-data-[variant=inset]:rounded-[var(--app-shell-radius)]',
260
- 'md:peer-data-[variant=inset]:overflow-hidden',
261
- ].join(' ');
262
-
263
- const BOXED_INSET_BORDER_CLASS =
264
- 'md:peer-data-[variant=inset]:border md:peer-data-[variant=inset]:border-border/60';
265
-
266
- const BOXED_RADIUS_REM: Record<NonNullable<LayoutVisualConfig['radius']>, string> = {
267
- sm: '0.375rem',
268
- md: '0.5rem',
269
- lg: '0.75rem',
270
- xl: '1rem',
271
- '2xl': '1.25rem',
272
- '3xl': '1.75rem',
273
- };
274
-
275
- function resolveInsetClassName(visual: LayoutVisualConfig | undefined): string {
276
- if ((visual?.variant ?? 'boxed') !== 'boxed') return 'flex flex-col';
277
- const border = visual?.border ?? true;
278
- return border ? `${BOXED_INSET_CLASS} ${BOXED_INSET_BORDER_CLASS}` : BOXED_INSET_CLASS;
279
- }
280
-
281
- /**
282
- * Background painted *behind* the boxed container on md+. On mobile the
283
- * canvas tint is dropped because the sidebar is a Drawer — leaking the
284
- * canvas colour to the whole viewport just makes the page look dim.
285
- *
286
- * `bg-sidebar` (the default) overrides shadcn-sidebar's built-in
287
- * `has-[&_[data-variant=inset]]:bg-sidebar` only at the breakpoint where
288
- * the inset shape actually exists.
289
- */
290
- const BOXED_BG_CLASS: Record<NonNullable<LayoutVisualConfig['background']>, string> = {
291
- sidebar: 'md:!bg-sidebar',
292
- muted: 'md:!bg-muted',
293
- card: 'md:!bg-card',
294
- background: 'md:!bg-background',
295
- };
296
-
297
- function resolveProviderClassName(visual: LayoutVisualConfig | undefined): string | undefined {
298
- // h-svh + overflow-hidden: lock the shell to exactly one viewport height so
299
- // the inner scroll-area (PrivateContent) is the only scroll surface. Without
300
- // this, SidebarProvider grows via min-h-svh and the whole page scrolls.
301
- const base = 'h-svh overflow-hidden';
302
- if ((visual?.variant ?? 'boxed') !== 'boxed') return base;
303
- // `max-md:!bg-background` neutralises shadcn-sidebar's built-in
304
- // `has-[[data-variant=inset]]:bg-sidebar` below md so the mobile Drawer shell
305
- // doesn't paint the whole viewport with the canvas tint.
306
- return `${base} max-md:!bg-background ${BOXED_BG_CLASS[visual?.background ?? 'sidebar']}`;
307
- }
308
-
309
- function normaliseInset(inset: LayoutVisualConfig['inset']): { x: number; y: number } {
310
- if (typeof inset === 'number') return { x: inset, y: inset };
311
- return { x: inset?.x ?? 12, y: inset?.y ?? 12 };
312
- }