@djangocfg/layouts 2.1.454 → 2.1.456

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.454",
3
+ "version": "2.1.456",
4
4
  "description": "Simple, straightforward layout components for Next.js - import and use with props",
5
5
  "keywords": [
6
6
  "layouts",
@@ -89,12 +89,12 @@
89
89
  "check": "tsc --noEmit"
90
90
  },
91
91
  "peerDependencies": {
92
- "@djangocfg/api": "^2.1.454",
93
- "@djangocfg/centrifugo": "^2.1.454",
94
- "@djangocfg/debuger": "^2.1.454",
95
- "@djangocfg/i18n": "^2.1.454",
96
- "@djangocfg/monitor": "^2.1.454",
97
- "@djangocfg/ui-core": "^2.1.454",
92
+ "@djangocfg/api": "^2.1.456",
93
+ "@djangocfg/centrifugo": "^2.1.456",
94
+ "@djangocfg/debuger": "^2.1.456",
95
+ "@djangocfg/i18n": "^2.1.456",
96
+ "@djangocfg/monitor": "^2.1.456",
97
+ "@djangocfg/ui-core": "^2.1.456",
98
98
  "@hookform/resolvers": "^5.2.2",
99
99
  "consola": "^3.4.2",
100
100
  "lucide-react": "^0.545.0",
@@ -125,14 +125,14 @@
125
125
  "uuid": "^11.1.0"
126
126
  },
127
127
  "devDependencies": {
128
- "@djangocfg/api": "^2.1.454",
129
- "@djangocfg/centrifugo": "^2.1.454",
130
- "@djangocfg/debuger": "^2.1.454",
131
- "@djangocfg/i18n": "^2.1.454",
132
- "@djangocfg/monitor": "^2.1.454",
133
- "@djangocfg/typescript-config": "^2.1.454",
134
- "@djangocfg/ui-core": "^2.1.454",
135
- "@djangocfg/ui-tools": "^2.1.454",
128
+ "@djangocfg/api": "^2.1.456",
129
+ "@djangocfg/centrifugo": "^2.1.456",
130
+ "@djangocfg/debuger": "^2.1.456",
131
+ "@djangocfg/i18n": "^2.1.456",
132
+ "@djangocfg/monitor": "^2.1.456",
133
+ "@djangocfg/typescript-config": "^2.1.456",
134
+ "@djangocfg/ui-core": "^2.1.456",
135
+ "@djangocfg/ui-tools": "^2.1.456",
136
136
  "@types/node": "^25.2.3",
137
137
  "@types/react": "^19.2.15",
138
138
  "@types/react-dom": "^19.2.3",
@@ -1,4 +1,5 @@
1
1
  'use client';
2
+ import { DEFAULT_AUTH_PATH } from '@djangocfg/api/auth';
2
3
 
3
4
  import { useEffect } from 'react';
4
5
 
@@ -46,7 +47,7 @@ export interface RedirectPageProps {
46
47
  */
47
48
  export function RedirectPage({
48
49
  authenticatedPath = '/private',
49
- unauthenticatedPath = '/auth',
50
+ unauthenticatedPath = DEFAULT_AUTH_PATH,
50
51
  loadingText = 'Loading...',
51
52
  }: RedirectPageProps) {
52
53
  const { isAuthenticated } = useAuth();
@@ -1,4 +1,5 @@
1
1
  'use client';
2
+ import { DEFAULT_AUTH_PATH } from '@djangocfg/api/auth';
2
3
 
3
4
  import { AlertCircle, Loader2 } from 'lucide-react';
4
5
  import { useSearchParams } from 'next/navigation';
@@ -165,7 +166,7 @@ function OAuthCallbackRaw({
165
166
  {status === 'error' && (
166
167
  <CardContent className="text-center">
167
168
  <a
168
- href="/auth"
169
+ href={DEFAULT_AUTH_PATH}
169
170
  className="text-primary hover:underline text-sm"
170
171
  >
171
172
  Try again
@@ -5,6 +5,7 @@
5
5
  */
6
6
 
7
7
  'use client';
8
+ import { DEFAULT_AUTH_PATH } from '@djangocfg/api/auth';
8
9
 
9
10
  import { useEffect, useState } from 'react';
10
11
  import { useRouter } from 'next/navigation';
@@ -39,7 +40,7 @@ interface UseAuthGuardResult {
39
40
 
40
41
  export function useAuthGuard({
41
42
  requireAuth = true,
42
- authPath = '/auth',
43
+ authPath = DEFAULT_AUTH_PATH,
43
44
  loadingTimeoutMs = 8000,
44
45
  }: UseAuthGuardOptions): UseAuthGuardResult {
45
46
  const { isAuthenticated, isLoading: authLoading, saveRedirectUrl } = useAuth();
@@ -29,6 +29,7 @@
29
29
  */
30
30
 
31
31
  'use client';
32
+ import { DEFAULT_AUTH_PATH } from '@djangocfg/api/auth';
32
33
 
33
34
  import { ArrowRight, Languages, LogOut } from 'lucide-react';
34
35
  import { Link } from '@djangocfg/ui-core/components';
@@ -69,7 +70,7 @@ export interface UserMenuProps {
69
70
  export function UserMenu({
70
71
  variant = 'desktop',
71
72
  groups,
72
- authPath = '/auth',
73
+ authPath = DEFAULT_AUTH_PATH,
73
74
  disableAuth = false,
74
75
  i18n,
75
76
  }: UserMenuProps) {
@@ -6,6 +6,7 @@
6
6
  */
7
7
 
8
8
  'use client';
9
+ import { DEFAULT_AUTH_PATH } from '@djangocfg/api/auth';
9
10
 
10
11
  import { ArrowRight } from 'lucide-react';
11
12
  import React, { useMemo } from 'react';
@@ -211,7 +212,7 @@ export function MobileDrawerShell(props: MobileDrawerShellProps) {
211
212
  {showSignInFooter && (
212
213
  <div className="shrink-0 border-t border-border/50 p-4">
213
214
  <Link
214
- href={userMenu?.authPath || '/auth'}
215
+ href={userMenu?.authPath || DEFAULT_AUTH_PATH}
215
216
  onClick={closeMobileMenu}
216
217
  className="block"
217
218
  >
@@ -16,13 +16,15 @@
16
16
  * store → URL : when the user opens/closes/switches section via the UI, we
17
17
  * push or replace the hash so it stays shareable.
18
18
  *
19
- * We lean on ui-core's `useLocation` (a `useSyncExternalStore` over a patched
20
- * history + `hashchange`) so this is framework-agnostic no next/router import.
19
+ * The URL fragment is read/written via ui-core's `useHashState`, which writes
20
+ * straight to the History API (bypassing the router adapter) so the hash is set
21
+ * absolutely — this is what avoids the `#settings#settings/…` doubling under
22
+ * Next.js App Router + next-intl. It's framework-agnostic (Next / Wails / Vite).
21
23
  */
22
24
 
23
25
  import { useEffect, useRef } from 'react';
24
26
 
25
- import { useLocation, useNavigate } from '@djangocfg/ui-core/hooks';
27
+ import { useHashState } from '@djangocfg/ui-core/hooks';
26
28
 
27
29
  import { useSettingsDialogStore } from '../store';
28
30
 
@@ -44,36 +46,30 @@ interface ParsedHash {
44
46
  section: string | null;
45
47
  }
46
48
 
47
- /** Parse `#settings/usage` → { open: true, section: 'usage' }. */
48
- function parseHash(hash: string, urlKey: string): ParsedHash {
49
- // hash includes the leading '#': "#settings/usage"
50
- const raw = hash.startsWith('#') ? hash.slice(1) : hash;
51
- if (!raw) return { open: false, section: null };
49
+ /**
50
+ * Parse a bare fragment (no leading '#', as `useHashState` yields):
51
+ * `"settings/usage"` { open: true, section: 'usage' }.
52
+ */
53
+ function parseHash(fragment: string, urlKey: string): ParsedHash {
54
+ if (!fragment) return { open: false, section: null };
52
55
 
53
- const [key, ...rest] = raw.split('/');
56
+ const [key, ...rest] = fragment.split('/');
54
57
  if (key !== urlKey) return { open: false, section: null };
55
58
 
56
59
  const section = rest.join('/') || null;
57
60
  return { open: true, section };
58
61
  }
59
62
 
60
- /** Build `#settings/usage` (or `#settings` when no section). */
63
+ /** Build the bare fragment `"settings/usage"` (or `"settings"` when no section). */
61
64
  function buildHash(urlKey: string, section: string | null): string {
62
- return section ? `#${urlKey}/${section}` : `#${urlKey}`;
63
- }
64
-
65
- /** Strip a trailing hash, keeping pathname + search intact. */
66
- function urlWithoutHash(): string {
67
- if (typeof window === 'undefined') return '/';
68
- const { pathname, search } = window.location;
69
- return `${pathname}${search}`;
65
+ return section ? `${urlKey}/${section}` : urlKey;
70
66
  }
71
67
 
72
68
  export function useSettingsUrl(options: UseSettingsUrlOptions = {}): void {
73
69
  const { urlKey = 'settings', enabled = true, resolveSection } = options;
74
70
 
75
- const { hash } = useLocation();
76
- const { navigate } = useNavigate();
71
+ // Bare fragment (without '#'); '' when there is no hash.
72
+ const [fragment, setFragment] = useHashState();
77
73
 
78
74
  const isOpen = useSettingsDialogStore((s) => s.isOpen);
79
75
  const activeSection = useSettingsDialogStore((s) => s.activeSection);
@@ -81,21 +77,21 @@ export function useSettingsUrl(options: UseSettingsUrlOptions = {}): void {
81
77
  const close = useSettingsDialogStore((s) => s.close);
82
78
 
83
79
  // Guard against the URL→store and store→URL effects fighting each other:
84
- // when we write the hash ourselves we tag the value we wrote and skip the
80
+ // when we write the fragment ourselves we tag the value we wrote and skip the
85
81
  // next URL→store pass for that exact value.
86
- const selfWrittenHash = useRef<string | null>(null);
82
+ const selfWrittenFragment = useRef<string | null>(null);
87
83
 
88
84
  // ── URL → store ──────────────────────────────────────────────────────────
89
85
  useEffect(() => {
90
86
  if (!enabled) return;
91
87
 
92
- // Ignore the echo of a hash we just wrote ourselves.
93
- if (selfWrittenHash.current === hash) {
94
- selfWrittenHash.current = null;
88
+ // Ignore the echo of a fragment we just wrote ourselves.
89
+ if (selfWrittenFragment.current === fragment) {
90
+ selfWrittenFragment.current = null;
95
91
  return;
96
92
  }
97
93
 
98
- const parsed = parseHash(hash, urlKey);
94
+ const parsed = parseHash(fragment, urlKey);
99
95
 
100
96
  if (parsed.open) {
101
97
  const resolved = resolveSection
@@ -108,31 +104,33 @@ export function useSettingsUrl(options: UseSettingsUrlOptions = {}): void {
108
104
  // Hash no longer points at us (back button, manual edit) → close.
109
105
  close();
110
106
  }
111
- // We intentionally depend on `hash` only; isOpen is read fresh via the
107
+ // We intentionally depend on `fragment` only; isOpen is read fresh via the
112
108
  // store getter semantics above. Re-running on isOpen would double-fire.
113
109
  // eslint-disable-next-line react-hooks/exhaustive-deps
114
- }, [hash, enabled, urlKey]);
110
+ }, [fragment, enabled, urlKey]);
115
111
 
116
112
  // ── store → URL ──────────────────────────────────────────────────────────
117
113
  useEffect(() => {
118
114
  if (!enabled) return;
119
115
  if (typeof window === 'undefined') return;
120
116
 
121
- const current = window.location.hash;
117
+ const current = fragment;
122
118
 
123
119
  if (isOpen) {
124
120
  const next = buildHash(urlKey, activeSection);
125
121
  if (current !== next) {
126
- selfWrittenHash.current = next;
127
- // push so the back button can close; refresh keeps the section.
128
- navigate(`${urlWithoutHash()}${next}`);
122
+ selfWrittenFragment.current = next;
123
+ // Push when we're *opening* (no settings fragment yet) so the back
124
+ // button closes the dialog; replace when only switching sections within
125
+ // an already-open dialog, so section clicks don't stack history entries.
126
+ const alreadyOurs = parseHash(current, urlKey).open;
127
+ setFragment(next, { replace: alreadyOurs });
129
128
  }
130
129
  } else {
131
- // Only strip if the hash is *ours*; never clobber an unrelated hash.
132
- const parsed = parseHash(current, urlKey);
133
- if (parsed.open) {
134
- selfWrittenHash.current = '';
135
- navigate(urlWithoutHash(), { replace: true });
130
+ // Only strip if the fragment is *ours*; never clobber an unrelated hash.
131
+ if (parseHash(current, urlKey).open) {
132
+ selfWrittenFragment.current = '';
133
+ setFragment(null, { replace: true });
136
134
  }
137
135
  }
138
136
  // eslint-disable-next-line react-hooks/exhaustive-deps
@@ -3,7 +3,7 @@
3
3
  import { LogIn } from 'lucide-react';
4
4
  import React, { useMemo } from 'react';
5
5
 
6
- import { useCfgRouter } from '@djangocfg/api/auth';
6
+ import { DEFAULT_AUTH_PATH, useAuth, useCfgRouter } from '@djangocfg/api/auth';
7
7
  import { useAppT } from '@djangocfg/i18n';
8
8
  import {
9
9
  Button, Dialog, DialogContent, DialogHeader, DialogTitle
@@ -17,7 +17,7 @@ interface AuthDialogProps {
17
17
 
18
18
  export const AuthDialog: React.FC<AuthDialogProps> = ({
19
19
  onAuthRequired,
20
- authPath = '/auth'
20
+ authPath = DEFAULT_AUTH_PATH
21
21
  }) => {
22
22
  const open = useDialogStore((s) => s.authOpen);
23
23
  const authOptions = useDialogStore((s) => s.authOptions);
@@ -32,15 +32,18 @@ export const AuthDialog: React.FC<AuthDialogProps> = ({
32
32
 
33
33
  const message = authOptions?.message || labels.pleaseSignIn;
34
34
  const router = useCfgRouter();
35
+ const { saveRedirectUrl } = useAuth();
35
36
 
36
37
  const handleClose = () => {
37
38
  resolveAuth(false);
38
39
  };
39
40
 
40
41
  const handleGoToAuth = () => {
42
+ // Canonical post-login redirect persistence (the same store the guard and
43
+ // verifyOTP read). The old private `redirectAfterAuth` sessionStorage key
44
+ // was write-only — nothing ever read it.
41
45
  if (typeof window !== 'undefined') {
42
- const redirect = authOptions?.redirectUrl || window.location.pathname;
43
- sessionStorage.setItem('redirectAfterAuth', redirect);
46
+ saveRedirectUrl(authOptions?.redirectUrl || window.location.pathname);
44
47
  }
45
48
 
46
49
  if (onAuthRequired) {