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
- let p = (s.split('#')[0] ?? '').split('?')[0]?.trim() ?? '';
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.11.0",
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
- let p = (s.split('#')[0] ?? '').split('?')[0]?.trim() ?? '';
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
  }