@djangocfg/layouts 2.1.618 → 2.1.621

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.618",
3
+ "version": "2.1.621",
4
4
  "description": "Simple, straightforward layout components for Next.js - import and use with props",
5
5
  "keywords": [
6
6
  "layouts",
@@ -91,11 +91,11 @@
91
91
  "test": "vitest run"
92
92
  },
93
93
  "peerDependencies": {
94
- "@djangocfg/analytics": "^2.1.618",
95
- "@djangocfg/api": "^2.1.618",
96
- "@djangocfg/devtools": "^2.1.618",
97
- "@djangocfg/i18n": "^2.1.618",
98
- "@djangocfg/ui-core": "^2.1.618",
94
+ "@djangocfg/analytics": "^2.1.621",
95
+ "@djangocfg/api": "^2.1.621",
96
+ "@djangocfg/devtools": "^2.1.621",
97
+ "@djangocfg/i18n": "^2.1.621",
98
+ "@djangocfg/ui-core": "^2.1.621",
99
99
  "@hookform/resolvers": "^5.2.2 || ^7.0.2",
100
100
  "consola": "^3.4.2",
101
101
  "lucide-react": "^1.68.0",
@@ -123,12 +123,12 @@
123
123
  "qrcode.react": "^4.2.0"
124
124
  },
125
125
  "devDependencies": {
126
- "@djangocfg/analytics": "^2.1.618",
127
- "@djangocfg/api": "^2.1.618",
128
- "@djangocfg/devtools": "^2.1.618",
129
- "@djangocfg/i18n": "^2.1.618",
130
- "@djangocfg/typescript-config": "^2.1.618",
131
- "@djangocfg/ui-core": "^2.1.618",
126
+ "@djangocfg/analytics": "^2.1.621",
127
+ "@djangocfg/api": "^2.1.621",
128
+ "@djangocfg/devtools": "^2.1.621",
129
+ "@djangocfg/i18n": "^2.1.621",
130
+ "@djangocfg/typescript-config": "^2.1.621",
131
+ "@djangocfg/ui-core": "^2.1.621",
132
132
  "@storybook/react-vite": "^10.6.5",
133
133
  "@types/node": "^25.9.5",
134
134
  "@types/react": "19.3.0",
@@ -102,12 +102,14 @@ export function PrivateLayout({
102
102
 
103
103
  if (isLoading) {
104
104
  return (
105
+ // The auth gate covers the whole viewport: solid, above the chat dock (z-100), sheets, dialogs and popovers.
106
+ // Nothing behind it may show or take a click while the session is still being decided.
105
107
  <Preloader
106
108
  variant="fullscreen"
107
109
  text={loadingText}
108
110
  size="lg"
109
- backdrop={true}
110
- backdropOpacity={80}
111
+ backdrop={false}
112
+ className="z-[9996] bg-background"
111
113
  />
112
114
  );
113
115
  }
@@ -15,6 +15,7 @@ import {
15
15
  resolveGuardIsAuthenticated,
16
16
  shouldRedirectToAuth,
17
17
  isForbidden,
18
+ authTrace,
18
19
  } from '@djangocfg/api/auth';
19
20
 
20
21
  interface UseAuthGuardOptions {
@@ -27,10 +28,8 @@ interface UseAuthGuardOptions {
27
28
  /** Explicit override; defaults to the app's resolved `useAuth().routes.auth`. */
28
29
  authPath?: string;
29
30
  /**
30
- * Safety net: if auth is still "loading" this many ms after mount, stop
31
- * waiting and let the guard decide from whatever token state we have. Prevents
32
- * an endless preloader if AuthContext initialization ever wedges (a hung
33
- * request, a CSP-swallowed response, an unresolved promise). Default 8s.
31
+ * @deprecated No effect. The provider owns the single safety timer (8 s), so
32
+ * `status` always leaves 'resolving'. Kept so existing callers still compile.
34
33
  */
35
34
  loadingTimeoutMs?: number;
36
35
  }
@@ -50,9 +49,11 @@ export function useAuthGuard({
50
49
  requireAuth = true,
51
50
  requireStaff = false,
52
51
  authPath,
53
- loadingTimeoutMs = 8000,
54
52
  }: UseAuthGuardOptions): UseAuthGuardResult {
55
- const { isAuthenticated, isLoading: authLoading, isAdminUser, saveRedirectUrl, routes } = useAuth();
53
+ const { status, isAdminUser, saveRedirectUrl, routes } = useAuth();
54
+ // One linear state decides everything: 'resolving' → spinner, 'authenticated' → render, 'anonymous' → redirect.
55
+ const isAuthenticated = status === 'authenticated';
56
+ const authLoading = status === 'resolving';
56
57
  const resolvedAuthPath = authPath ?? routes.auth;
57
58
  const router = useRouter();
58
59
  const [isRedirecting, setIsRedirecting] = useState(false);
@@ -65,26 +66,14 @@ export function useAuthGuard({
65
66
  const [mounted, setMounted] = useState(false);
66
67
  useEffect(() => setMounted(true), []);
67
68
 
68
- // Safety-net timeout: force auth to be considered "resolved" if it hasn't
69
- // settled within loadingTimeoutMs, so the guard can act (render or redirect)
70
- // instead of spinning forever. Only armed while actually loading.
71
- const [authTimedOut, setAuthTimedOut] = useState(false);
72
- useEffect(() => {
73
- if (!mounted || !authLoading || authTimedOut) return;
74
- const t = setTimeout(() => setAuthTimedOut(true), loadingTimeoutMs);
75
- return () => clearTimeout(t);
76
- }, [mounted, authLoading, authTimedOut, loadingTimeoutMs]);
77
-
78
69
  // Guard state derives from a single pure resolver (see @djangocfg/api/auth
79
70
  // utils/guard) so the "spinner vs redirect vs render" decision is unit-tested
80
71
  // in isolation and can't silently regress into the "stuck on Authenticating…"
81
72
  // state where a resolved-but-unauthenticated session never redirects.
82
- // `authLoading` is forced false once the safety-net timeout fires.
83
- const effectiveAuthLoading = authLoading && !authTimedOut;
84
73
  const guardState = {
85
74
  mounted,
86
75
  requireAuth,
87
- authLoading: effectiveAuthLoading,
76
+ authLoading,
88
77
  isRedirecting,
89
78
  isAuthenticated,
90
79
  requireStaff,
@@ -94,12 +83,13 @@ export function useAuthGuard({
94
83
  useEffect(() => {
95
84
  if (shouldRedirectToAuth(guardState)) {
96
85
  const currentUrl = window.location.pathname + window.location.search;
86
+ authTrace('guard.redirect', { from: window.location.pathname, to: resolvedAuthPath });
97
87
  saveRedirectUrl(currentUrl);
98
88
  setIsRedirecting(true);
99
89
  router.push(resolvedAuthPath);
100
90
  }
101
91
  // eslint-disable-next-line react-hooks/exhaustive-deps
102
- }, [mounted, requireAuth, isAuthenticated, effectiveAuthLoading, isRedirecting, router, saveRedirectUrl, resolvedAuthPath]);
92
+ }, [mounted, requireAuth, isAuthenticated, authLoading, isRedirecting, router, saveRedirectUrl, resolvedAuthPath]);
103
93
 
104
94
  const isLoading = resolveGuardIsLoading(guardState);
105
95
  const loadingText = isRedirecting ? 'Redirecting to login...' : 'Authenticating...';
@@ -63,6 +63,7 @@ export const MockAuthProvider: React.FC<MockAuthProviderProps> = ({
63
63
  const value = useMemo<AuthContextType>(() => ({
64
64
  user,
65
65
  isLoading,
66
+ status: isLoading ? 'resolving' : (isAuthenticated ?? Boolean(user)) ? 'authenticated' : 'anonymous',
66
67
  isAuthenticated: isAuthenticated ?? Boolean(user),
67
68
  isAdminUser,
68
69
  routes: { auth: '/auth', defaultCallback: '/', defaultAuthCallback: '/auth', ...routes },