@djangocfg/layouts 2.1.596 → 2.1.597

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.596",
3
+ "version": "2.1.597",
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.596",
95
- "@djangocfg/api": "^2.1.596",
96
- "@djangocfg/devtools": "^2.1.596",
97
- "@djangocfg/i18n": "^2.1.596",
98
- "@djangocfg/ui-core": "^2.1.596",
94
+ "@djangocfg/analytics": "^2.1.597",
95
+ "@djangocfg/api": "^2.1.597",
96
+ "@djangocfg/devtools": "^2.1.597",
97
+ "@djangocfg/i18n": "^2.1.597",
98
+ "@djangocfg/ui-core": "^2.1.597",
99
99
  "@hookform/resolvers": "^5.2.2",
100
100
  "consola": "^3.4.2",
101
101
  "lucide-react": "^0.545.0",
@@ -125,12 +125,12 @@
125
125
  "uuid": "^11.1.1"
126
126
  },
127
127
  "devDependencies": {
128
- "@djangocfg/analytics": "^2.1.596",
129
- "@djangocfg/api": "^2.1.596",
130
- "@djangocfg/devtools": "^2.1.596",
131
- "@djangocfg/i18n": "^2.1.596",
132
- "@djangocfg/typescript-config": "^2.1.596",
133
- "@djangocfg/ui-core": "^2.1.596",
128
+ "@djangocfg/analytics": "^2.1.597",
129
+ "@djangocfg/api": "^2.1.597",
130
+ "@djangocfg/devtools": "^2.1.597",
131
+ "@djangocfg/i18n": "^2.1.597",
132
+ "@djangocfg/typescript-config": "^2.1.597",
133
+ "@djangocfg/ui-core": "^2.1.597",
134
134
  "@storybook/react-vite": "^10.5.0",
135
135
  "@types/node": "^25.9.5",
136
136
  "@types/react": "19.2.15",
@@ -27,7 +27,7 @@ import { useEffect, useRef } from 'react';
27
27
 
28
28
  import { useHashState } from '@djangocfg/ui-core/hooks';
29
29
 
30
- import { buildSettingsHash as buildHash, parseSettingsHash as parseHash } from '../intent/parseSettingsHash';
30
+ import { buildSettingsHash as buildHash, parseSettingsHash as parseHash, sectionToOpen } from '../intent/parseSettingsHash';
31
31
  import { useSettingsDialogStore } from '../store';
32
32
 
33
33
  export interface UseSettingsUrlOptions {
@@ -37,7 +37,8 @@ export interface UseSettingsUrlOptions {
37
37
  enabled?: boolean;
38
38
  /**
39
39
  * Validate / normalize a section id read from the URL. Return null to reject
40
- * an unknown id (dialog still opens, on the fallback section). The shell
40
+ * an unknown id (dialog still opens, on the fallback section; the id stays in
41
+ * the address so a section registered a moment later can claim it). The shell
41
42
  * passes the set of known section ids here.
42
43
  */
43
44
  resolveSection?: (id: string | null) => string | null;
@@ -73,9 +74,8 @@ export function useSettingsUrl(options: UseSettingsUrlOptions = {}): void {
73
74
  const parsed = parseHash(fragment, urlKey);
74
75
 
75
76
  if (parsed.open) {
76
- const resolved = resolveSection
77
- ? resolveSection(parsed.section)
78
- : parsed.section;
77
+ // A section the host does not know *yet* stays in the address (see `sectionToOpen`).
78
+ const resolved = sectionToOpen(parsed.section, resolveSection ? resolveSection(parsed.section) : parsed.section);
79
79
  // open() preserves the current section when called without one, so only
80
80
  // pass a section when the URL actually named a (valid) one.
81
81
  open(resolved ?? undefined);
@@ -1,6 +1,6 @@
1
1
  import { describe, expect, it } from 'vitest';
2
2
 
3
- import { buildSettingsHash, parseSettingsHash } from './parseSettingsHash';
3
+ import { buildSettingsHash, parseSettingsHash, sectionToOpen } from './parseSettingsHash';
4
4
 
5
5
  describe('parseSettingsHash', () => {
6
6
  it('is closed for an empty or foreign fragment', () => {
@@ -38,3 +38,17 @@ describe('buildSettingsHash', () => {
38
38
  expect(buildSettingsHash('settings', null)).toBe('settings');
39
39
  });
40
40
  });
41
+
42
+ describe('sectionToOpen', () => {
43
+ it('uses the host-resolved id when there is one', () => {
44
+ expect(sectionToOpen('mcp', 'mcp')).toBe('mcp');
45
+ });
46
+
47
+ it('keeps the requested id while the section is not registered yet (cold deep link)', () => {
48
+ expect(sectionToOpen('mcp', null)).toBe('mcp');
49
+ });
50
+
51
+ it('is null for a bare #settings', () => {
52
+ expect(sectionToOpen(null, null)).toBeNull();
53
+ });
54
+ });
@@ -42,3 +42,14 @@ export function parseSettingsHash(fragment: string, urlKey: string): ParsedSetti
42
42
  export function buildSettingsHash(urlKey: string, section: string | null): string {
43
43
  return section ? `${urlKey}/${section}` : urlKey;
44
44
  }
45
+
46
+ /**
47
+ * The section to open for a parsed fragment. `resolved` is the host's check against the sections it knows *right now*;
48
+ * it is null both for a typo and for a section that has not registered yet (a host adds its MCP section once a
49
+ * backend answer arrives, after the dialog has already read a deep link on a cold load). Dropping the id in the
50
+ * second case made the dialog write `#settings` over the link and open on the first section, so the id is kept: an
51
+ * unknown one only shows the fallback section, a late one is picked up as soon as it registers.
52
+ */
53
+ export function sectionToOpen(requested: string | null, resolved: string | null): string | null {
54
+ return resolved ?? requested;
55
+ }