cursedbelt-core 2.11.0 → 2.12.0
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.
|
@@ -4,6 +4,12 @@ import { type GateFn, type NavDomain, type NavGroup, type NavItem, type Navigati
|
|
|
4
4
|
* `#tab=…` in-page target), and a trailing slash. `'/'` stays `'/'`. Used so a tab
|
|
5
5
|
* variant (`/tools#tab=curl`) matches the base route (`/tools`) and `/lists/`
|
|
6
6
|
* matches `/lists`.
|
|
7
|
+
*
|
|
8
|
+
* 🔴 A HASH ROUTE is the route, not an in-page target: `#/queue` and `/#/queue/x.md` are
|
|
9
|
+
* matched on `/queue` and `/queue/x.md`. Until 2.12.0 they were cut at the `#` like a tab
|
|
10
|
+
* variant, so every place of a hash-routed app normalized to `/` and `SideNav` lit ALL of them
|
|
11
|
+
* (station's first `AppShell`, 2026-10-02). The fragment is the route only when nothing but `/`
|
|
12
|
+
* stands before it and it starts with `/` — `/tools#tab=curl` is still `/tools`.
|
|
7
13
|
*/
|
|
8
14
|
export declare function normalizeNavPath(path: string): string;
|
|
9
15
|
/** A flattened, gated catalogue entry — a leaf plus where it lives (for grouping). */
|
|
@@ -9,12 +9,22 @@ import { allowAllGate, } from './navigationModel.js';
|
|
|
9
9
|
* `#tab=…` in-page target), and a trailing slash. `'/'` stays `'/'`. Used so a tab
|
|
10
10
|
* variant (`/tools#tab=curl`) matches the base route (`/tools`) and `/lists/`
|
|
11
11
|
* matches `/lists`.
|
|
12
|
+
*
|
|
13
|
+
* 🔴 A HASH ROUTE is the route, not an in-page target: `#/queue` and `/#/queue/x.md` are
|
|
14
|
+
* matched on `/queue` and `/queue/x.md`. Until 2.12.0 they were cut at the `#` like a tab
|
|
15
|
+
* variant, so every place of a hash-routed app normalized to `/` and `SideNav` lit ALL of them
|
|
16
|
+
* (station's first `AppShell`, 2026-10-02). The fragment is the route only when nothing but `/`
|
|
17
|
+
* stands before it and it starts with `/` — `/tools#tab=curl` is still `/tools`.
|
|
12
18
|
*/
|
|
13
19
|
export function normalizeNavPath(path) {
|
|
14
20
|
// Null-safe: a nav selector must never throw (a breadcrumb derivation crashing
|
|
15
21
|
// white-screens the whole page). Coerce a missing/non-string path to ''.
|
|
16
22
|
const s = typeof path === 'string' ? path : '';
|
|
17
|
-
|
|
23
|
+
const hashAt = s.indexOf('#');
|
|
24
|
+
const before = hashAt === -1 ? s : s.slice(0, hashAt);
|
|
25
|
+
const fragment = hashAt === -1 ? '' : s.slice(hashAt + 1);
|
|
26
|
+
const route = (before === '' || before === '/') && fragment.startsWith('/') ? fragment : before;
|
|
27
|
+
let p = route.split('?')[0]?.trim() ?? '';
|
|
18
28
|
if (p.length > 1 && p.endsWith('/'))
|
|
19
29
|
p = p.slice(0, -1);
|
|
20
30
|
return p || '/';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cursedbelt-core",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.12.0",
|
|
4
4
|
"license": "ISC",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Framework-agnostic core of the cursedbelt split — isomorphic domain logic. cwip below it, cursedbelt-server and cursedbelt (react) above it.",
|
|
@@ -131,6 +131,16 @@ describe('normalizeNavPath', () => {
|
|
|
131
131
|
expect(normalizeNavPath('/')).toBe('/');
|
|
132
132
|
expect(normalizeNavPath('/admin/users')).toBe('/admin/users');
|
|
133
133
|
});
|
|
134
|
+
|
|
135
|
+
test('a hash route IS the route — every place of a hash-routed app must not collapse to /', () => {
|
|
136
|
+
expect(normalizeNavPath('#/queue')).toBe('/queue');
|
|
137
|
+
expect(normalizeNavPath('/#/queue/Not_Started/x.md')).toBe('/queue/Not_Started/x.md');
|
|
138
|
+
expect(normalizeNavPath('#/queue?folder=Blocked')).toBe('/queue');
|
|
139
|
+
expect(normalizeNavPath('#/')).toBe('/');
|
|
140
|
+
// An in-page target after a real path is still a tab variant, not a route.
|
|
141
|
+
expect(normalizeNavPath('/tools#/not-a-route')).toBe('/tools');
|
|
142
|
+
expect(normalizeNavPath('#section')).toBe('/');
|
|
143
|
+
});
|
|
134
144
|
});
|
|
135
145
|
|
|
136
146
|
describe('deriveSideNavDomains', () => {
|
|
@@ -18,12 +18,22 @@ import {
|
|
|
18
18
|
* `#tab=…` in-page target), and a trailing slash. `'/'` stays `'/'`. Used so a tab
|
|
19
19
|
* variant (`/tools#tab=curl`) matches the base route (`/tools`) and `/lists/`
|
|
20
20
|
* matches `/lists`.
|
|
21
|
+
*
|
|
22
|
+
* 🔴 A HASH ROUTE is the route, not an in-page target: `#/queue` and `/#/queue/x.md` are
|
|
23
|
+
* matched on `/queue` and `/queue/x.md`. Until 2.12.0 they were cut at the `#` like a tab
|
|
24
|
+
* variant, so every place of a hash-routed app normalized to `/` and `SideNav` lit ALL of them
|
|
25
|
+
* (station's first `AppShell`, 2026-10-02). The fragment is the route only when nothing but `/`
|
|
26
|
+
* stands before it and it starts with `/` — `/tools#tab=curl` is still `/tools`.
|
|
21
27
|
*/
|
|
22
28
|
export function normalizeNavPath(path: string): string {
|
|
23
29
|
// Null-safe: a nav selector must never throw (a breadcrumb derivation crashing
|
|
24
30
|
// white-screens the whole page). Coerce a missing/non-string path to ''.
|
|
25
31
|
const s = typeof path === 'string' ? path : '';
|
|
26
|
-
|
|
32
|
+
const hashAt = s.indexOf('#');
|
|
33
|
+
const before = hashAt === -1 ? s : s.slice(0, hashAt);
|
|
34
|
+
const fragment = hashAt === -1 ? '' : s.slice(hashAt + 1);
|
|
35
|
+
const route = (before === '' || before === '/') && fragment.startsWith('/') ? fragment : before;
|
|
36
|
+
let p = route.split('?')[0]?.trim() ?? '';
|
|
27
37
|
if (p.length > 1 && p.endsWith('/')) p = p.slice(0, -1);
|
|
28
38
|
return p || '/';
|
|
29
39
|
}
|