@djangocfg/layouts 2.1.454 → 2.1.455

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.455",
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.455",
93
+ "@djangocfg/centrifugo": "^2.1.455",
94
+ "@djangocfg/debuger": "^2.1.455",
95
+ "@djangocfg/i18n": "^2.1.455",
96
+ "@djangocfg/monitor": "^2.1.455",
97
+ "@djangocfg/ui-core": "^2.1.455",
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.455",
129
+ "@djangocfg/centrifugo": "^2.1.455",
130
+ "@djangocfg/debuger": "^2.1.455",
131
+ "@djangocfg/i18n": "^2.1.455",
132
+ "@djangocfg/monitor": "^2.1.455",
133
+ "@djangocfg/typescript-config": "^2.1.455",
134
+ "@djangocfg/ui-core": "^2.1.455",
135
+ "@djangocfg/ui-tools": "^2.1.455",
136
136
  "@types/node": "^25.2.3",
137
137
  "@types/react": "^19.2.15",
138
138
  "@types/react-dom": "^19.2.3",
@@ -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