@djangocfg/layouts 2.1.411 → 2.1.413

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@djangocfg/layouts",
3
- "version": "2.1.411",
3
+ "version": "2.1.413",
4
4
  "description": "Simple, straightforward layout components for Next.js - import and use with props",
5
5
  "keywords": [
6
6
  "layouts",
@@ -70,6 +70,11 @@
70
70
  "import": "./src/configurator/private/index.ts",
71
71
  "require": "./src/configurator/private/index.ts"
72
72
  },
73
+ "./testing": {
74
+ "types": "./src/testing/index.ts",
75
+ "import": "./src/testing/index.ts",
76
+ "require": "./src/testing/index.ts"
77
+ },
73
78
  "./styles": "./src/styles/index.css",
74
79
  "./styles/dashboard": "./src/styles/dashboard.css"
75
80
  },
@@ -84,13 +89,13 @@
84
89
  "check": "tsc --noEmit"
85
90
  },
86
91
  "peerDependencies": {
87
- "@djangocfg/api": "^2.1.411",
88
- "@djangocfg/centrifugo": "^2.1.411",
89
- "@djangocfg/debuger": "^2.1.411",
90
- "@djangocfg/i18n": "^2.1.411",
91
- "@djangocfg/monitor": "^2.1.411",
92
- "@djangocfg/ui-core": "^2.1.411",
93
- "@djangocfg/ui-nextjs": "^2.1.411",
92
+ "@djangocfg/api": "^2.1.413",
93
+ "@djangocfg/centrifugo": "^2.1.413",
94
+ "@djangocfg/debuger": "^2.1.413",
95
+ "@djangocfg/i18n": "^2.1.413",
96
+ "@djangocfg/monitor": "^2.1.413",
97
+ "@djangocfg/ui-core": "^2.1.413",
98
+ "@djangocfg/ui-nextjs": "^2.1.413",
94
99
  "@hookform/resolvers": "^5.2.2",
95
100
  "consola": "^3.4.2",
96
101
  "lucide-react": "^0.545.0",
@@ -121,15 +126,15 @@
121
126
  "uuid": "^11.1.0"
122
127
  },
123
128
  "devDependencies": {
124
- "@djangocfg/api": "^2.1.411",
125
- "@djangocfg/centrifugo": "^2.1.411",
126
- "@djangocfg/debuger": "^2.1.411",
127
- "@djangocfg/i18n": "^2.1.411",
128
- "@djangocfg/monitor": "^2.1.411",
129
- "@djangocfg/typescript-config": "^2.1.411",
130
- "@djangocfg/ui-core": "^2.1.411",
131
- "@djangocfg/ui-nextjs": "^2.1.411",
132
- "@djangocfg/ui-tools": "^2.1.411",
129
+ "@djangocfg/api": "^2.1.413",
130
+ "@djangocfg/centrifugo": "^2.1.413",
131
+ "@djangocfg/debuger": "^2.1.413",
132
+ "@djangocfg/i18n": "^2.1.413",
133
+ "@djangocfg/monitor": "^2.1.413",
134
+ "@djangocfg/typescript-config": "^2.1.413",
135
+ "@djangocfg/ui-core": "^2.1.413",
136
+ "@djangocfg/ui-nextjs": "^2.1.413",
137
+ "@djangocfg/ui-tools": "^2.1.413",
133
138
  "@types/node": "^25.2.3",
134
139
  "@types/react": "^19.2.15",
135
140
  "@types/react-dom": "^19.2.3",
@@ -23,6 +23,8 @@ interface ErrorBoundaryState {
23
23
  }
24
24
 
25
25
  export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
26
+ private handlePopState: (() => void) | null = null;
27
+
26
28
  constructor(props: ErrorBoundaryProps) {
27
29
  super(props);
28
30
  this.state = { hasError: false, error: null };
@@ -33,17 +35,36 @@ export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundarySt
33
35
  }
34
36
 
35
37
  componentDidCatch(error: Error, errorInfo: ErrorInfo) {
36
- // Call custom error handler if provided
37
38
  if (this.props.onError) {
38
39
  this.props.onError(error, errorInfo);
39
40
  }
40
41
 
41
- // Log to console in development
42
42
  if (isDev) {
43
43
  console.error('ErrorBoundary caught an error:', error, errorInfo);
44
44
  }
45
45
  }
46
46
 
47
+ componentDidUpdate(_prevProps: ErrorBoundaryProps, prevState: ErrorBoundaryState) {
48
+ if (this.state.hasError && !prevState.hasError) {
49
+ this.handlePopState = () => {
50
+ window.location.reload();
51
+ };
52
+ window.addEventListener('popstate', this.handlePopState);
53
+ } else if (!this.state.hasError && prevState.hasError) {
54
+ if (this.handlePopState) {
55
+ window.removeEventListener('popstate', this.handlePopState);
56
+ this.handlePopState = null;
57
+ }
58
+ }
59
+ }
60
+
61
+ componentWillUnmount() {
62
+ if (this.handlePopState) {
63
+ window.removeEventListener('popstate', this.handlePopState);
64
+ this.handlePopState = null;
65
+ }
66
+ }
67
+
47
68
  render() {
48
69
  if (this.state.hasError) {
49
70
  const title = getT('layouts.errors.somethingWentWrong');
@@ -258,10 +258,9 @@ export function ErrorLayout({
258
258
  const handleGoBack = () => {
259
259
  if (document.referrer && document.referrer !== window.location.href) {
260
260
  window.location.href = document.referrer;
261
- } else if (window.history.length > 1) {
262
- window.history.back();
263
261
  } else {
264
- window.location.href = '/';
262
+ window.history.back();
263
+ setTimeout(() => window.location.reload(), 100);
265
264
  }
266
265
  };
267
266
 
@@ -6,7 +6,9 @@ import { useAuthForm } from '@djangocfg/api/auth';
6
6
 
7
7
  import type { AuthFormContextType, AuthLayoutProps } from './types';
8
8
 
9
- const AuthFormContext = createContext<AuthFormContextType | undefined>(undefined);
9
+ // Exported so test/mock providers (storybook) can supply their own value
10
+ // without re-implementing AuthFormProvider end-to-end.
11
+ export const AuthFormContext = createContext<AuthFormContextType | undefined>(undefined);
10
12
 
11
13
  /**
12
14
  * AuthFormProvider — wraps useAuthForm and merges UI config into context.
@@ -2,22 +2,14 @@
2
2
 
3
3
  import React, { memo } from 'react';
4
4
 
5
- import { GlowBackground } from '@djangocfg/ui-core/components';
6
-
7
5
  import type { ShellRenderProps } from './types';
8
6
 
9
7
  /**
10
- * CenteredShell — Apple-style frameless auth layout.
11
- *
12
- * Features:
13
- * - Full viewport centering
14
- * - Glow background (default)
15
- * - Optional custom background image/gradient via shell props
16
- * - No visible card chrome
8
+ * CenteredShell — frameless auth layout, full viewport centering.
17
9
  *
18
- * Memoised: re-renders only when bgStyle/overlayStyle references or
19
- * children change. The background layer is cheap to recreate; the main
20
- * win is preserving the content subtree when parent re-renders.
10
+ * Default background is plain `var(--background)` the form chrome
11
+ * carries the design. Callers can still supply a custom
12
+ * image/gradient/overlay via `bgStyle` / `overlayStyle`.
21
13
  */
22
14
  function CenteredShellRaw({
23
15
  children,
@@ -29,19 +21,15 @@ function CenteredShellRaw({
29
21
 
30
22
  return (
31
23
  <div className={`auth-shell-centered ${className || ''}`}>
32
- {/* Background layer */}
33
- {hasCustomBg ? (
24
+ {hasCustomBg && (
34
25
  <>
35
26
  <div className="auth-shell-centered__bg" style={bgStyle} />
36
27
  {overlayStyle && (
37
28
  <div className="auth-shell-centered__overlay" style={overlayStyle} />
38
29
  )}
39
30
  </>
40
- ) : (
41
- <GlowBackground />
42
31
  )}
43
32
 
44
- {/* Content */}
45
33
  <div className="auth-shell-centered__content">
46
34
  {children}
47
35
  </div>
@@ -0,0 +1,185 @@
1
+ 'use client';
2
+
3
+ import React, { useMemo, useRef, useState } from 'react';
4
+
5
+ import type {
6
+ AuthFormReturn,
7
+ AuthStep,
8
+ } from '@djangocfg/api/auth';
9
+
10
+ import { AuthFormContext } from '../layouts/AuthLayout/context';
11
+ import type { AuthFormContextType, AuthLayoutConfig } from '../layouts/AuthLayout/types';
12
+
13
+ // Storybook-only provider. Reproduces the AuthFormContext shape with
14
+ // in-memory state and no-op async handlers, so step components and the
15
+ // full AuthLayout can be exercised without hitting @djangocfg/api/auth.
16
+
17
+ export interface MockAuthFormProviderProps extends Partial<AuthLayoutConfig> {
18
+ children: React.ReactNode;
19
+
20
+ // Initial state — every field has a sensible default. Override per story.
21
+ step?: AuthStep;
22
+ identifier?: string;
23
+ otp?: string;
24
+ isLoading?: boolean;
25
+ acceptedTerms?: boolean;
26
+ error?: string;
27
+ twoFactorSessionId?: string | null;
28
+ shouldPrompt2FA?: boolean;
29
+ twoFactorCode?: string;
30
+ useBackupCode?: boolean;
31
+ rateLimitSeconds?: number;
32
+ is2FALoading?: boolean;
33
+ twoFactorWarning?: string | null;
34
+ twoFactorAttemptsRemaining?: number | null;
35
+ }
36
+
37
+ function formatRateLimit(seconds: number): string {
38
+ if (seconds <= 0) return '';
39
+ if (seconds >= 60) {
40
+ const m = Math.floor(seconds / 60);
41
+ const s = seconds % 60;
42
+ return `${m}:${String(s).padStart(2, '0')}`;
43
+ }
44
+ return `${seconds}s`;
45
+ }
46
+
47
+ export const MockAuthFormProvider: React.FC<MockAuthFormProviderProps> = ({
48
+ children,
49
+
50
+ // Layout config
51
+ sourceUrl = 'https://example.com',
52
+ supportUrl,
53
+ termsUrl,
54
+ privacyUrl,
55
+ enableGithubAuth = false,
56
+ enable2FASetup = true,
57
+ logoUrl,
58
+ redirectUrl = '/dashboard',
59
+
60
+ // Initial state
61
+ step: initialStep = 'identifier',
62
+ identifier: initialIdentifier = '',
63
+ otp: initialOtp = '',
64
+ isLoading: initialLoading = false,
65
+ acceptedTerms: initialAcceptedTerms = false,
66
+ error: initialError = '',
67
+ twoFactorSessionId: initialTwoFactorSessionId = null,
68
+ shouldPrompt2FA: initialShouldPrompt2FA = false,
69
+ twoFactorCode: initialTwoFactorCode = '',
70
+ useBackupCode: initialUseBackupCode = false,
71
+ rateLimitSeconds: initialRateLimitSeconds = 0,
72
+ is2FALoading = false,
73
+ twoFactorWarning = null,
74
+ twoFactorAttemptsRemaining = null,
75
+ }) => {
76
+ const [step, setStep] = useState<AuthStep>(initialStep);
77
+ const [identifier, setIdentifier] = useState(initialIdentifier);
78
+ const [otp, setOtp] = useState(initialOtp);
79
+ const [isLoading, setIsLoading] = useState(initialLoading);
80
+ const [acceptedTerms, setAcceptedTerms] = useState(initialAcceptedTerms);
81
+ const [error, setError] = useState(initialError);
82
+ const [twoFactorSessionId, setTwoFactorSessionId] = useState<string | null>(initialTwoFactorSessionId);
83
+ const [shouldPrompt2FA, setShouldPrompt2FA] = useState(initialShouldPrompt2FA);
84
+ const [twoFactorCode, setTwoFactorCode] = useState(initialTwoFactorCode);
85
+ const [useBackupCode, setUseBackupCode] = useState(initialUseBackupCode);
86
+ const [rateLimitSeconds, setRateLimitSeconds] = useState(initialRateLimitSeconds);
87
+
88
+ const isAutoSubmittingFromUrl = useRef(false);
89
+
90
+ const value: AuthFormContextType = useMemo(() => {
91
+ const stop = (e: React.FormEvent) => {
92
+ e.preventDefault?.();
93
+ };
94
+
95
+ return {
96
+ // State
97
+ identifier,
98
+ otp,
99
+ isLoading,
100
+ acceptedTerms,
101
+ step,
102
+ error,
103
+ twoFactorSessionId,
104
+ shouldPrompt2FA,
105
+ twoFactorCode,
106
+ useBackupCode,
107
+ rateLimitSeconds,
108
+ isRateLimited: rateLimitSeconds > 0,
109
+ rateLimitLabel: formatRateLimit(rateLimitSeconds),
110
+
111
+ // State handlers
112
+ setIdentifier,
113
+ setOtp,
114
+ setAcceptedTerms,
115
+ setError,
116
+ clearError: () => setError(''),
117
+ setStep,
118
+ setIsLoading,
119
+ setTwoFactorSessionId,
120
+ setShouldPrompt2FA,
121
+ setTwoFactorCode,
122
+ setUseBackupCode,
123
+ startRateLimitCountdown: (seconds: number) => setRateLimitSeconds(seconds),
124
+
125
+ // Submit handlers — all no-op (preventDefault only) so the visible
126
+ // step never changes from under the storyteller. To preview a
127
+ // different step, render a new story with `step="..."`.
128
+ handleIdentifierSubmit: async (e) => stop(e),
129
+ handleOTPSubmit: async (e) => stop(e),
130
+ handleResendOTP: async () => {},
131
+ handleBackToIdentifier: () => setStep('identifier'),
132
+ forceOTPStep: () => setStep('otp'),
133
+ handle2FASubmit: async (e) => stop(e),
134
+ handleUseBackupCode: () => setUseBackupCode(true),
135
+ handleUseTOTP: () => setUseBackupCode(false),
136
+
137
+ // Validation — accept anything that looks email-ish or 10+ digits.
138
+ validateIdentifier: (id: string) =>
139
+ /.+@.+\..+/.test(id) || /^\+?\d{10,}$/.test(id),
140
+
141
+ // Auto-submit ref
142
+ isAutoSubmittingFromUrl,
143
+
144
+ // 2FA state (read-only here; tests/stories override via props)
145
+ is2FALoading,
146
+ twoFactorWarning,
147
+ twoFactorAttemptsRemaining,
148
+
149
+ // Layout config
150
+ sourceUrl,
151
+ supportUrl,
152
+ termsUrl,
153
+ privacyUrl,
154
+ enableGithubAuth,
155
+ enable2FASetup,
156
+ logoUrl,
157
+ redirectUrl,
158
+ };
159
+ }, [
160
+ identifier,
161
+ otp,
162
+ isLoading,
163
+ acceptedTerms,
164
+ step,
165
+ error,
166
+ twoFactorSessionId,
167
+ shouldPrompt2FA,
168
+ twoFactorCode,
169
+ useBackupCode,
170
+ rateLimitSeconds,
171
+ is2FALoading,
172
+ twoFactorWarning,
173
+ twoFactorAttemptsRemaining,
174
+ sourceUrl,
175
+ supportUrl,
176
+ termsUrl,
177
+ privacyUrl,
178
+ enableGithubAuth,
179
+ enable2FASetup,
180
+ logoUrl,
181
+ redirectUrl,
182
+ ]);
183
+
184
+ return <AuthFormContext.Provider value={value}>{children}</AuthFormContext.Provider>;
185
+ };
@@ -0,0 +1,97 @@
1
+ 'use client';
2
+
3
+ import React, { useMemo, useState } from 'react';
4
+
5
+ import {
6
+ AuthContext,
7
+ type AuthContextType,
8
+ type UserProfile,
9
+ } from '@djangocfg/api/auth';
10
+
11
+ // Storybook-only auth provider. Reproduces the AuthContext shape with
12
+ // fixed in-memory values so components that call `useAuth()` (Profile,
13
+ // PrivateLayout, etc.) can render without hitting the real auth API.
14
+
15
+ export interface MockAuthProviderProps {
16
+ children: React.ReactNode;
17
+ user?: UserProfile | null;
18
+ isLoading?: boolean;
19
+ /** Override `isAuthenticated`. Defaults to `!!user`. */
20
+ isAuthenticated?: boolean;
21
+ /** Override `isAdminUser`. Defaults to `false`. */
22
+ isAdminUser?: boolean;
23
+ }
24
+
25
+ const noopAsync = async () => {};
26
+
27
+ export function buildMockUser(overrides: Partial<UserProfile> = {}): UserProfile {
28
+ return {
29
+ id: 1,
30
+ email: 'jane.cooper@example.com',
31
+ username: 'jane.cooper',
32
+ first_name: 'Jane',
33
+ last_name: 'Cooper',
34
+ full_name: 'Jane Cooper',
35
+ is_active: true,
36
+ is_staff: false,
37
+ is_superuser: false,
38
+ date_joined: '2025-01-15T10:00:00Z',
39
+ last_login: '2026-05-20T14:30:00Z',
40
+ initials: 'JC',
41
+ display_username: 'jane.cooper',
42
+ company: null,
43
+ phone: null,
44
+ position: null,
45
+ unanswered_messages_count: 0,
46
+ avatar: null,
47
+ ...overrides,
48
+ } as UserProfile;
49
+ }
50
+
51
+ export const MockAuthProvider: React.FC<MockAuthProviderProps> = ({
52
+ children,
53
+ user: initialUser = buildMockUser(),
54
+ isLoading = false,
55
+ isAuthenticated,
56
+ isAdminUser = false,
57
+ }) => {
58
+ const [user, setUser] = useState<UserProfile | null>(initialUser);
59
+
60
+ const value = useMemo<AuthContextType>(() => ({
61
+ user,
62
+ isLoading,
63
+ isAuthenticated: isAuthenticated ?? Boolean(user),
64
+ isAdminUser,
65
+
66
+ loadCurrentProfile: noopAsync,
67
+ checkAuthAndRedirect: noopAsync,
68
+
69
+ getToken: () => 'mock-access-token',
70
+ getRefreshToken: () => 'mock-refresh-token',
71
+
72
+ getSavedEmail: () => user?.email ?? null,
73
+ saveEmail: () => {},
74
+ clearSavedEmail: () => {},
75
+
76
+ requestOTP: async () => ({ success: true, message: 'mock' }),
77
+ verifyOTP: async () => ({ success: true, message: 'mock', user: user ?? undefined }),
78
+ refreshToken: async () => ({ success: true, message: 'mock' }),
79
+ logout: () => {},
80
+
81
+ saveRedirectUrl: () => {},
82
+ getRedirectUrl: () => '/dashboard',
83
+ clearRedirectUrl: () => {},
84
+ hasRedirectUrl: () => false,
85
+
86
+ // Edits update local state so the form reflects the change in
87
+ // Storybook just like a real backend round-trip would.
88
+ updateProfile: async (data) => {
89
+ const next = { ...(user ?? buildMockUser()), ...data } as UserProfile;
90
+ setUser(next);
91
+ return next;
92
+ },
93
+ uploadAvatar: async () => user ?? buildMockUser(),
94
+ }), [user, isLoading, isAuthenticated, isAdminUser]);
95
+
96
+ return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
97
+ };
@@ -0,0 +1,28 @@
1
+ // Storybook / test-only helpers. Public consumers should NOT import
2
+ // from this entrypoint in production code.
3
+
4
+ export {
5
+ MockAuthFormProvider,
6
+ type MockAuthFormProviderProps,
7
+ } from './MockAuthFormProvider';
8
+
9
+ export {
10
+ MockAuthProvider,
11
+ buildMockUser,
12
+ type MockAuthProviderProps,
13
+ } from './MockAuthProvider';
14
+
15
+ // Auth step type, re-exported so storybook stories don't have to depend
16
+ // on @djangocfg/api/auth directly.
17
+ export type { AuthStep } from '@djangocfg/api/auth';
18
+
19
+ // Re-export auth internals so storybook can mount step components
20
+ // directly under MockAuthFormProvider without going through the full
21
+ // AuthLayout (which mounts its own real AuthFormProvider).
22
+ export { AuthShell } from '../layouts/AuthLayout/shells';
23
+ export {
24
+ IdentifierStep,
25
+ OTPStep,
26
+ TwoFactorStep,
27
+ SetupStep,
28
+ } from '../layouts/AuthLayout/components/steps';