@llui/router 0.0.33 → 0.0.35

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/dist/connect.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import type { Router } from './index.js';
2
2
  export interface RouterEffect {
3
3
  type: '__router';
4
- action: 'push' | 'replace' | 'back' | 'forward' | 'scroll';
4
+ action: 'push' | 'replace' | 'navigate' | 'back' | 'forward' | 'scroll';
5
5
  path?: string;
6
6
  x?: number;
7
7
  y?: number;
@@ -22,10 +22,44 @@ export interface ConnectOptions<R> {
22
22
  beforeLeave?: (from: R, to: R) => boolean;
23
23
  }
24
24
  export interface ConnectedRouter<R> {
25
- /** Effect: push a new route onto history */
25
+ /**
26
+ * Effect: push a new history entry — URL only.
27
+ *
28
+ * Use when the reducer that emitted the effect has already updated
29
+ * `state.route` itself (e.g. a `Router/Navigate` handler that bundles
30
+ * state changes inline before delegating URL work). For
31
+ * navigate-and-let-the-app-react flows from anywhere else, prefer
32
+ * `navigate()` — it dispatches the listener-captured navigate
33
+ * message after pushState so `state.route` and route-side-effects
34
+ * stay in sync without each reducer re-implementing the delegation.
35
+ */
26
36
  push(route: R): RouterEffect;
27
- /** Effect: replace current history entry */
37
+ /**
38
+ * Effect: replace the current history entry — URL only. Same
39
+ * URL-only contract as `push()`. For replace-and-react flows, see
40
+ * `navigate()` (push semantics) — there's no `replaceAndDispatch`
41
+ * variant yet because the use case hasn't surfaced; if it does,
42
+ * model it the same way.
43
+ */
28
44
  replace(route: R): RouterEffect;
45
+ /**
46
+ * Effect: push history AND dispatch the listener-captured navigate
47
+ * message so the reducer can update `state.route` and run any
48
+ * route-side-effects (data fetches, page-meta resets, analytics).
49
+ *
50
+ * Resolves the asymmetry where `link()` did pushState + send while
51
+ * `push()` did pushState only — apps that wanted programmatic
52
+ * navigation from arbitrary reducers had to either re-implement the
53
+ * delegation or live with desynced `state.route`.
54
+ *
55
+ * Requires that the app has mounted `listener()` (typically inside
56
+ * the shell view) — the navigate effect uses the send/factory
57
+ * captured there. If `navigate()` runs before `listener()` mounts,
58
+ * the URL still updates but no message is dispatched and a
59
+ * `console.warn` surfaces the gap. After listener unmount the same
60
+ * fallback applies.
61
+ */
62
+ navigate(route: R): RouterEffect;
29
63
  /** Effect: go back */
30
64
  back(): RouterEffect;
31
65
  /** Effect: go forward */
@@ -1 +1 @@
1
- {"version":3,"file":"connect.d.ts","sourceRoot":"","sources":["../src/connect.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,YAAY,CAAA;AAKxC,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,UAAU,CAAA;IAChB,MAAM,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,GAAG,QAAQ,CAAA;IAC1D,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,CAAC,CAAC,EAAE,MAAM,CAAA;IACV,CAAC,CAAC,EAAE,MAAM,CAAA;CACX;AAED,MAAM,WAAW,cAAc,CAAC,CAAC;IAC/B;;;;;OAKG;IACH,WAAW,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,GAAG,IAAI,KAAK,CAAC,GAAG,KAAK,GAAG,IAAI,CAAA;IACzD;;;;OAIG;IACH,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,KAAK,OAAO,CAAA;CAC1C;AAED,MAAM,WAAW,eAAe,CAAC,CAAC;IAChC,4CAA4C;IAC5C,IAAI,CAAC,KAAK,EAAE,CAAC,GAAG,YAAY,CAAA;IAC5B,4CAA4C;IAC5C,OAAO,CAAC,KAAK,EAAE,CAAC,GAAG,YAAY,CAAA;IAC/B,sBAAsB;IACtB,IAAI,IAAI,YAAY,CAAA;IACpB,yBAAyB;IACzB,OAAO,IAAI,YAAY,CAAA;IACvB,iCAAiC;IACjC,MAAM,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,YAAY,CAAA;IAE1C,8DAA8D;IAC9D,YAAY,EAAE,CAAC,GAAG,EAAE;QAAE,MAAM,EAAE;YAAE,IAAI,EAAE,MAAM,CAAA;SAAE,CAAC;QAAC,IAAI,EAAE,OAAO,CAAC;QAAC,MAAM,EAAE,WAAW,CAAA;KAAE,KAAK,OAAO,CAAA;IAEhG;;;OAGG;IACH,QAAQ,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,GAAG,EAAE,CAAC,KAAK,IAAI,EAAE,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,CAAC,GAAG,IAAI,EAAE,CAAA;IAEzE;;;OAGG;IACH,IAAI,CAAC,CAAC,EACJ,IAAI,EAAE,CAAC,GAAG,EAAE,CAAC,KAAK,IAAI,EACtB,KAAK,EAAE,CAAC,EACR,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,QAAQ,EAAE,IAAI,EAAE,EAChB,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,CAAC,GAC3B,WAAW,CAAA;IAEd;;;OAGG;IACH,aAAa,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,MAAM,EAAE;QAC7B,mDAAmD;QACnD,OAAO,CAAC,EAAE,MAAM,CAAA;QAChB,iCAAiC;QACjC,QAAQ,EAAE,CAAC,GAAG,EAAE,CAAC,KAAK,CAAC,CAAA;QACvB,oCAAoC;QACpC,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,KAAK,CAAC,CAAA;QACjC,8CAA8C;QAC9C,UAAU,EAAE,CAAC,KAAK,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAA;KAC7C,GAAG,CAAC,KAAK,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,GAAG,IAAI,CAAA;CAC1C;AAED,wBAAgB,aAAa,CAAC,CAAC,EAC7B,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,EACjB,OAAO,CAAC,EAAE,cAAc,CAAC,CAAC,CAAC,GAC1B,eAAe,CAAC,CAAC,CAAC,CAyJpB"}
1
+ {"version":3,"file":"connect.d.ts","sourceRoot":"","sources":["../src/connect.ts"],"names":[],"mappings":"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"}
package/dist/connect.js CHANGED
@@ -1,6 +1,14 @@
1
1
  import { a, onMount } from '@llui/dom';
2
2
  export function connectRouter(router, options) {
3
3
  let currentRoute = null;
4
+ // Captured by listener() at mount, cleared at unmount. The
5
+ // navigate() effect reads these to dispatch the navigate message
6
+ // after pushState — they are the bridge between the reducer-side
7
+ // (which produces effects) and the dispatcher-side (which receives
8
+ // messages). Module-scope inside the closure: at most one listener
9
+ // is active per ConnectedRouter (the shell view).
10
+ let listenerSend = null;
11
+ let listenerFactory = null;
4
12
  /**
5
13
  * Run guards for a navigation to `newRoute`. Returns the final route
6
14
  * to navigate to, or `null` if navigation should be blocked.
@@ -52,6 +60,34 @@ export function connectRouter(router, options) {
52
60
  currentRoute = finalRoute;
53
61
  break;
54
62
  }
63
+ case 'navigate': {
64
+ // pushState semantics + dispatch the navigate message so the
65
+ // app reducer sees the route change. This is the asymmetry
66
+ // fix: link() always did push+send (because click handlers run
67
+ // synchronously in view code with send/factory in scope), but
68
+ // push() as an effect could only do push (no access to send).
69
+ // navigate() resolves it by reading the closure variables that
70
+ // listener() sets at mount time.
71
+ const target = router.match(effect.path);
72
+ const finalRoute = runGuards(target);
73
+ if (finalRoute === null)
74
+ return;
75
+ const finalPath = router.href(finalRoute);
76
+ if (router.mode === 'hash') {
77
+ location.hash = finalPath;
78
+ }
79
+ else {
80
+ history.pushState(null, '', finalPath);
81
+ }
82
+ currentRoute = finalRoute;
83
+ if (listenerSend !== null && listenerFactory !== null) {
84
+ listenerSend(listenerFactory(finalRoute));
85
+ }
86
+ else {
87
+ console.warn('@llui/router: navigate() effect dispatched but listener() is not mounted — URL updated, but no navigate message was sent. Mount connectedRouter.listener() in your shell view, or use push() and dispatch the route-changed message yourself.');
88
+ }
89
+ break;
90
+ }
55
91
  case 'back':
56
92
  history.back();
57
93
  break;
@@ -70,6 +106,9 @@ export function connectRouter(router, options) {
70
106
  replace(route) {
71
107
  return { type: '__router', action: 'replace', path: router.href(route) };
72
108
  },
109
+ navigate(route) {
110
+ return { type: '__router', action: 'navigate', path: router.href(route) };
111
+ },
73
112
  back() {
74
113
  return { type: '__router', action: 'back' };
75
114
  },
@@ -88,6 +127,14 @@ export function connectRouter(router, options) {
88
127
  listener(send, msgFactory) {
89
128
  const factory = msgFactory ?? ((r) => ({ type: 'navigate', route: r }));
90
129
  onMount(() => {
130
+ // Capture send/factory so the navigate() effect can dispatch
131
+ // route-changed messages from any reducer, not just from
132
+ // popstate or click handlers. Stored as the generic `unknown`
133
+ // shape so applyEffect doesn't need to know R or M; the only
134
+ // consumer is the navigate case above, which round-trips R
135
+ // through factory back to the user's M.
136
+ listenerSend = send;
137
+ listenerFactory = factory;
91
138
  const event = router.mode === 'hash' ? 'hashchange' : 'popstate';
92
139
  const handler = () => {
93
140
  const input = router.mode === 'hash' ? location.hash : location.pathname + location.search;
@@ -105,7 +152,11 @@ export function connectRouter(router, options) {
105
152
  send(factory(finalRoute));
106
153
  };
107
154
  window.addEventListener(event, handler);
108
- return () => window.removeEventListener(event, handler);
155
+ return () => {
156
+ window.removeEventListener(event, handler);
157
+ listenerSend = null;
158
+ listenerFactory = null;
159
+ };
109
160
  });
110
161
  return [document.createComment('router')];
111
162
  },
@@ -1 +1 @@
1
- {"version":3,"file":"connect.js","sourceRoot":"","sources":["../src/connect.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Router } from './index.js'\nimport { a, onMount } from '@llui/dom'\n\n// ── Router Effects ───────────────────────────────────────────────\n\nexport interface RouterEffect {\n type: '__router'\n action: 'push' | 'replace' | 'back' | 'forward' | 'scroll'\n path?: string\n x?: number\n y?: number\n}\n\nexport interface ConnectOptions<R> {\n /**\n * Called before entering a new route. Return:\n * - `void` / `undefined` → allow navigation\n * - `false` → block navigation (stay on current route)\n * - a different `Route` → redirect to that route\n */\n beforeEnter?: (to: R, from: R | null) => R | false | void\n /**\n * Called before leaving the current route. Return:\n * - `true` → allow navigation\n * - `false` → block (e.g. unsaved changes prompt)\n */\n beforeLeave?: (from: R, to: R) => boolean\n}\n\nexport interface ConnectedRouter<R> {\n /** Effect: push a new route onto history */\n push(route: R): RouterEffect\n /** Effect: replace current history entry */\n replace(route: R): RouterEffect\n /** Effect: go back */\n back(): RouterEffect\n /** Effect: go forward */\n forward(): RouterEffect\n /** Effect: scroll to position */\n scroll(x: number, y: number): RouterEffect\n\n /** Plugin for handleEffects().use() — handles RouterEffect */\n handleEffect: (ctx: { effect: { type: string }; send: unknown; signal: AbortSignal }) => boolean\n\n /**\n * View helper: attach URL change listener via onMount.\n * Returns an empty comment node. Sends { type: 'navigate', route } on URL change.\n */\n listener<M>(send: (msg: M) => void, msgFactory?: (route: R) => M): Node[]\n\n /**\n * View helper: render a navigation link.\n * Generates <a> with proper href and click handler that sends navigate message.\n */\n link<M>(\n send: (msg: M) => void,\n route: R,\n attrs: Record<string, unknown>,\n children: Node[],\n msgFactory?: (route: R) => M,\n ): HTMLElement\n\n /**\n * Create an update handler for mergeHandlers.\n * Returns [newState, Effect[]] for navigate messages, null for others.\n */\n createHandler<S, M, E>(config: {\n /** Message type to handle (default: 'navigate') */\n message?: string\n /** Extract route from message */\n getRoute: (msg: M) => R\n /** Optional guard — can redirect */\n guard?: (route: R, state: S) => R\n /** Build new state + effects for the route */\n onNavigate: (state: S, route: R) => [S, E[]]\n }): (state: S, msg: M) => [S, E[]] | null\n}\n\nexport function connectRouter<R>(\n router: Router<R>,\n options?: ConnectOptions<R>,\n): ConnectedRouter<R> {\n let currentRoute: R | null = null\n /**\n * Run guards for a navigation to `newRoute`. Returns the final route\n * to navigate to, or `null` if navigation should be blocked.\n */\n function runGuards(newRoute: R): R | null {\n if (options?.beforeLeave && currentRoute !== null) {\n if (!options.beforeLeave(currentRoute, newRoute)) return null\n }\n if (options?.beforeEnter) {\n const result = options.beforeEnter(newRoute, currentRoute)\n if (result === false) return null\n if (result !== undefined && result !== null && typeof result === 'object') {\n return result as R\n }\n }\n return newRoute\n }\n\n function applyEffect(effect: RouterEffect): void {\n switch (effect.action) {\n case 'push': {\n const target = router.match(effect.path!)\n const finalRoute = runGuards(target)\n if (finalRoute === null) return\n const finalPath = router.href(finalRoute)\n if (router.mode === 'hash') {\n location.hash = finalPath\n } else {\n history.pushState(null, '', finalPath)\n }\n currentRoute = finalRoute\n break\n }\n case 'replace': {\n const target = router.match(effect.path!)\n const finalRoute = runGuards(target)\n if (finalRoute === null) return\n const finalPath = router.href(finalRoute)\n if (router.mode === 'hash') {\n location.replace(finalPath)\n } else {\n history.replaceState(null, '', finalPath)\n }\n currentRoute = finalRoute\n break\n }\n case 'back':\n history.back()\n break\n case 'forward':\n history.forward()\n break\n case 'scroll':\n window.scrollTo(effect.x!, effect.y!)\n break\n }\n }\n\n return {\n push(route) {\n return { type: '__router', action: 'push', path: router.href(route) }\n },\n replace(route) {\n return { type: '__router', action: 'replace', path: router.href(route) }\n },\n back() {\n return { type: '__router', action: 'back' }\n },\n forward() {\n return { type: '__router', action: 'forward' }\n },\n scroll(x, y) {\n return { type: '__router', action: 'scroll', x, y }\n },\n\n handleEffect({ effect }) {\n if (effect.type !== '__router') return false\n applyEffect(effect as RouterEffect)\n return true\n },\n\n listener<M>(send: (msg: M) => void, msgFactory?: (route: R) => M): Node[] {\n const factory = msgFactory ?? ((r: R) => ({ type: 'navigate', route: r }) as M)\n onMount(() => {\n const event = router.mode === 'hash' ? 'hashchange' : 'popstate'\n const handler = () => {\n const input = router.mode === 'hash' ? location.hash : location.pathname + location.search\n const route = router.match(input)\n const finalRoute = runGuards(route)\n if (finalRoute === null) {\n // Guard blocked — restore previous URL\n if (currentRoute !== null) {\n const restorePath = router.href(currentRoute)\n history.pushState(null, '', restorePath)\n }\n return\n }\n currentRoute = finalRoute\n send(factory(finalRoute))\n }\n window.addEventListener(event, handler)\n return () => window.removeEventListener(event, handler)\n })\n return [document.createComment('router')]\n },\n\n link<M>(\n send: (msg: M) => void,\n route: R,\n attrs: Record<string, unknown>,\n children: Node[],\n msgFactory?: (route: R) => M,\n ): HTMLElement {\n const factory = msgFactory ?? ((r: R) => ({ type: 'navigate', route: r }) as M)\n return a(\n {\n ...attrs,\n href: router.href(route),\n onClick: (e: Event) => {\n const me = e as MouseEvent\n if (me.ctrlKey || me.metaKey || me.shiftKey || me.altKey || me.button !== 0) return\n e.preventDefault()\n // Push history — pushState doesn't fire popstate, so no double-nav\n if (router.mode === 'hash') {\n // hashchange will fire the listener, which sends the navigate message\n location.hash = router.href(route)\n return\n }\n history.pushState(null, '', router.href(route))\n send(factory(route))\n },\n },\n children,\n )\n },\n\n createHandler<S, M, E>(config: {\n message?: string\n getRoute: (msg: M) => R\n guard?: (route: R, state: S) => R\n onNavigate: (state: S, route: R) => [S, E[]]\n }): (state: S, msg: M) => [S, E[]] | null {\n const msgType = config.message ?? 'navigate'\n return (state: S, msg: M) => {\n if ((msg as Record<string, unknown>).type !== msgType) return null\n let route = config.getRoute(msg)\n if (config.guard) route = config.guard(route, state)\n return config.onNavigate(state, route)\n }\n },\n }\n}\n"]}
1
+ {"version":3,"file":"connect.js","sourceRoot":"","sources":["../src/connect.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Router } from './index.js'\nimport { a, onMount } from '@llui/dom'\n\n// ── Router Effects ───────────────────────────────────────────────\n\nexport interface RouterEffect {\n type: '__router'\n action: 'push' | 'replace' | 'navigate' | 'back' | 'forward' | 'scroll'\n path?: string\n x?: number\n y?: number\n}\n\nexport interface ConnectOptions<R> {\n /**\n * Called before entering a new route. Return:\n * - `void` / `undefined` → allow navigation\n * - `false` → block navigation (stay on current route)\n * - a different `Route` → redirect to that route\n */\n beforeEnter?: (to: R, from: R | null) => R | false | void\n /**\n * Called before leaving the current route. Return:\n * - `true` → allow navigation\n * - `false` → block (e.g. unsaved changes prompt)\n */\n beforeLeave?: (from: R, to: R) => boolean\n}\n\nexport interface ConnectedRouter<R> {\n /**\n * Effect: push a new history entry — URL only.\n *\n * Use when the reducer that emitted the effect has already updated\n * `state.route` itself (e.g. a `Router/Navigate` handler that bundles\n * state changes inline before delegating URL work). For\n * navigate-and-let-the-app-react flows from anywhere else, prefer\n * `navigate()` — it dispatches the listener-captured navigate\n * message after pushState so `state.route` and route-side-effects\n * stay in sync without each reducer re-implementing the delegation.\n */\n push(route: R): RouterEffect\n /**\n * Effect: replace the current history entry — URL only. Same\n * URL-only contract as `push()`. For replace-and-react flows, see\n * `navigate()` (push semantics) — there's no `replaceAndDispatch`\n * variant yet because the use case hasn't surfaced; if it does,\n * model it the same way.\n */\n replace(route: R): RouterEffect\n /**\n * Effect: push history AND dispatch the listener-captured navigate\n * message so the reducer can update `state.route` and run any\n * route-side-effects (data fetches, page-meta resets, analytics).\n *\n * Resolves the asymmetry where `link()` did pushState + send while\n * `push()` did pushState only — apps that wanted programmatic\n * navigation from arbitrary reducers had to either re-implement the\n * delegation or live with desynced `state.route`.\n *\n * Requires that the app has mounted `listener()` (typically inside\n * the shell view) — the navigate effect uses the send/factory\n * captured there. If `navigate()` runs before `listener()` mounts,\n * the URL still updates but no message is dispatched and a\n * `console.warn` surfaces the gap. After listener unmount the same\n * fallback applies.\n */\n navigate(route: R): RouterEffect\n /** Effect: go back */\n back(): RouterEffect\n /** Effect: go forward */\n forward(): RouterEffect\n /** Effect: scroll to position */\n scroll(x: number, y: number): RouterEffect\n\n /** Plugin for handleEffects().use() — handles RouterEffect */\n handleEffect: (ctx: { effect: { type: string }; send: unknown; signal: AbortSignal }) => boolean\n\n /**\n * View helper: attach URL change listener via onMount.\n * Returns an empty comment node. Sends { type: 'navigate', route } on URL change.\n */\n listener<M>(send: (msg: M) => void, msgFactory?: (route: R) => M): Node[]\n\n /**\n * View helper: render a navigation link.\n * Generates <a> with proper href and click handler that sends navigate message.\n */\n link<M>(\n send: (msg: M) => void,\n route: R,\n attrs: Record<string, unknown>,\n children: Node[],\n msgFactory?: (route: R) => M,\n ): HTMLElement\n\n /**\n * Create an update handler for mergeHandlers.\n * Returns [newState, Effect[]] for navigate messages, null for others.\n */\n createHandler<S, M, E>(config: {\n /** Message type to handle (default: 'navigate') */\n message?: string\n /** Extract route from message */\n getRoute: (msg: M) => R\n /** Optional guard — can redirect */\n guard?: (route: R, state: S) => R\n /** Build new state + effects for the route */\n onNavigate: (state: S, route: R) => [S, E[]]\n }): (state: S, msg: M) => [S, E[]] | null\n}\n\nexport function connectRouter<R>(\n router: Router<R>,\n options?: ConnectOptions<R>,\n): ConnectedRouter<R> {\n let currentRoute: R | null = null\n // Captured by listener() at mount, cleared at unmount. The\n // navigate() effect reads these to dispatch the navigate message\n // after pushState — they are the bridge between the reducer-side\n // (which produces effects) and the dispatcher-side (which receives\n // messages). Module-scope inside the closure: at most one listener\n // is active per ConnectedRouter (the shell view).\n let listenerSend: ((msg: unknown) => void) | null = null\n let listenerFactory: ((route: R) => unknown) | null = null\n /**\n * Run guards for a navigation to `newRoute`. Returns the final route\n * to navigate to, or `null` if navigation should be blocked.\n */\n function runGuards(newRoute: R): R | null {\n if (options?.beforeLeave && currentRoute !== null) {\n if (!options.beforeLeave(currentRoute, newRoute)) return null\n }\n if (options?.beforeEnter) {\n const result = options.beforeEnter(newRoute, currentRoute)\n if (result === false) return null\n if (result !== undefined && result !== null && typeof result === 'object') {\n return result as R\n }\n }\n return newRoute\n }\n\n function applyEffect(effect: RouterEffect): void {\n switch (effect.action) {\n case 'push': {\n const target = router.match(effect.path!)\n const finalRoute = runGuards(target)\n if (finalRoute === null) return\n const finalPath = router.href(finalRoute)\n if (router.mode === 'hash') {\n location.hash = finalPath\n } else {\n history.pushState(null, '', finalPath)\n }\n currentRoute = finalRoute\n break\n }\n case 'replace': {\n const target = router.match(effect.path!)\n const finalRoute = runGuards(target)\n if (finalRoute === null) return\n const finalPath = router.href(finalRoute)\n if (router.mode === 'hash') {\n location.replace(finalPath)\n } else {\n history.replaceState(null, '', finalPath)\n }\n currentRoute = finalRoute\n break\n }\n case 'navigate': {\n // pushState semantics + dispatch the navigate message so the\n // app reducer sees the route change. This is the asymmetry\n // fix: link() always did push+send (because click handlers run\n // synchronously in view code with send/factory in scope), but\n // push() as an effect could only do push (no access to send).\n // navigate() resolves it by reading the closure variables that\n // listener() sets at mount time.\n const target = router.match(effect.path!)\n const finalRoute = runGuards(target)\n if (finalRoute === null) return\n const finalPath = router.href(finalRoute)\n if (router.mode === 'hash') {\n location.hash = finalPath\n } else {\n history.pushState(null, '', finalPath)\n }\n currentRoute = finalRoute\n if (listenerSend !== null && listenerFactory !== null) {\n listenerSend(listenerFactory(finalRoute))\n } else {\n console.warn(\n '@llui/router: navigate() effect dispatched but listener() is not mounted — URL updated, but no navigate message was sent. Mount connectedRouter.listener() in your shell view, or use push() and dispatch the route-changed message yourself.',\n )\n }\n break\n }\n case 'back':\n history.back()\n break\n case 'forward':\n history.forward()\n break\n case 'scroll':\n window.scrollTo(effect.x!, effect.y!)\n break\n }\n }\n\n return {\n push(route) {\n return { type: '__router', action: 'push', path: router.href(route) }\n },\n replace(route) {\n return { type: '__router', action: 'replace', path: router.href(route) }\n },\n navigate(route) {\n return { type: '__router', action: 'navigate', path: router.href(route) }\n },\n back() {\n return { type: '__router', action: 'back' }\n },\n forward() {\n return { type: '__router', action: 'forward' }\n },\n scroll(x, y) {\n return { type: '__router', action: 'scroll', x, y }\n },\n\n handleEffect({ effect }) {\n if (effect.type !== '__router') return false\n applyEffect(effect as RouterEffect)\n return true\n },\n\n listener<M>(send: (msg: M) => void, msgFactory?: (route: R) => M): Node[] {\n const factory = msgFactory ?? ((r: R) => ({ type: 'navigate', route: r }) as M)\n onMount(() => {\n // Capture send/factory so the navigate() effect can dispatch\n // route-changed messages from any reducer, not just from\n // popstate or click handlers. Stored as the generic `unknown`\n // shape so applyEffect doesn't need to know R or M; the only\n // consumer is the navigate case above, which round-trips R\n // through factory back to the user's M.\n listenerSend = send as (msg: unknown) => void\n listenerFactory = factory as (route: R) => unknown\n\n const event = router.mode === 'hash' ? 'hashchange' : 'popstate'\n const handler = () => {\n const input = router.mode === 'hash' ? location.hash : location.pathname + location.search\n const route = router.match(input)\n const finalRoute = runGuards(route)\n if (finalRoute === null) {\n // Guard blocked — restore previous URL\n if (currentRoute !== null) {\n const restorePath = router.href(currentRoute)\n history.pushState(null, '', restorePath)\n }\n return\n }\n currentRoute = finalRoute\n send(factory(finalRoute))\n }\n window.addEventListener(event, handler)\n return () => {\n window.removeEventListener(event, handler)\n listenerSend = null\n listenerFactory = null\n }\n })\n return [document.createComment('router')]\n },\n\n link<M>(\n send: (msg: M) => void,\n route: R,\n attrs: Record<string, unknown>,\n children: Node[],\n msgFactory?: (route: R) => M,\n ): HTMLElement {\n const factory = msgFactory ?? ((r: R) => ({ type: 'navigate', route: r }) as M)\n return a(\n {\n ...attrs,\n href: router.href(route),\n onClick: (e: Event) => {\n const me = e as MouseEvent\n if (me.ctrlKey || me.metaKey || me.shiftKey || me.altKey || me.button !== 0) return\n e.preventDefault()\n // Push history — pushState doesn't fire popstate, so no double-nav\n if (router.mode === 'hash') {\n // hashchange will fire the listener, which sends the navigate message\n location.hash = router.href(route)\n return\n }\n history.pushState(null, '', router.href(route))\n send(factory(route))\n },\n },\n children,\n )\n },\n\n createHandler<S, M, E>(config: {\n message?: string\n getRoute: (msg: M) => R\n guard?: (route: R, state: S) => R\n onNavigate: (state: S, route: R) => [S, E[]]\n }): (state: S, msg: M) => [S, E[]] | null {\n const msgType = config.message ?? 'navigate'\n return (state: S, msg: M) => {\n if ((msg as Record<string, unknown>).type !== msgType) return null\n let route = config.getRoute(msg)\n if (config.guard) route = config.guard(route, state)\n return config.onNavigate(state, route)\n }\n },\n }\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llui/router",
3
- "version": "0.0.33",
3
+ "version": "0.0.35",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -15,12 +15,12 @@
15
15
  }
16
16
  },
17
17
  "peerDependencies": {
18
- "@llui/dom": "^0.0.33"
18
+ "@llui/dom": "^0.0.34"
19
19
  },
20
20
  "devDependencies": {
21
21
  "typescript": "^6.0.0",
22
22
  "vitest": "^4.1.2",
23
- "@llui/dom": "0.0.33"
23
+ "@llui/dom": "0.0.34"
24
24
  },
25
25
  "sideEffects": false,
26
26
  "description": "LLui router — structured path matching, history/hash mode, link helper",