@bquery/bquery 1.2.0 → 1.4.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.
Files changed (309) hide show
  1. package/README.md +127 -27
  2. package/dist/batch-x7b2eZST.js +13 -0
  3. package/dist/batch-x7b2eZST.js.map +1 -0
  4. package/dist/component/component.d.ts +69 -0
  5. package/dist/component/component.d.ts.map +1 -0
  6. package/dist/component/html.d.ts +35 -0
  7. package/dist/component/html.d.ts.map +1 -0
  8. package/dist/component/index.d.ts +3 -126
  9. package/dist/component/index.d.ts.map +1 -1
  10. package/dist/component/props.d.ts +18 -0
  11. package/dist/component/props.d.ts.map +1 -0
  12. package/dist/component/types.d.ts +77 -0
  13. package/dist/component/types.d.ts.map +1 -0
  14. package/dist/component.es.mjs +90 -59
  15. package/dist/component.es.mjs.map +1 -1
  16. package/dist/core/collection.d.ts +55 -3
  17. package/dist/core/collection.d.ts.map +1 -1
  18. package/dist/core/dom.d.ts +6 -0
  19. package/dist/core/dom.d.ts.map +1 -0
  20. package/dist/core/element.d.ts +31 -4
  21. package/dist/core/element.d.ts.map +1 -1
  22. package/dist/core/index.d.ts +2 -0
  23. package/dist/core/index.d.ts.map +1 -1
  24. package/dist/core/utils/array.d.ts +74 -0
  25. package/dist/core/utils/array.d.ts.map +1 -0
  26. package/dist/core/utils/function.d.ts +87 -0
  27. package/dist/core/utils/function.d.ts.map +1 -0
  28. package/dist/core/utils/index.d.ts +70 -0
  29. package/dist/core/utils/index.d.ts.map +1 -0
  30. package/dist/core/utils/misc.d.ts +63 -0
  31. package/dist/core/utils/misc.d.ts.map +1 -0
  32. package/dist/core/utils/number.d.ts +65 -0
  33. package/dist/core/utils/number.d.ts.map +1 -0
  34. package/dist/core/utils/object.d.ts +133 -0
  35. package/dist/core/utils/object.d.ts.map +1 -0
  36. package/dist/core/utils/string.d.ts +80 -0
  37. package/dist/core/utils/string.d.ts.map +1 -0
  38. package/dist/core/utils/type-guards.d.ts +79 -0
  39. package/dist/core/utils/type-guards.d.ts.map +1 -0
  40. package/dist/core-BhpuvPhy.js +170 -0
  41. package/dist/core-BhpuvPhy.js.map +1 -0
  42. package/dist/core.es.mjs +495 -489
  43. package/dist/core.es.mjs.map +1 -1
  44. package/dist/full.d.ts +2 -2
  45. package/dist/full.d.ts.map +1 -1
  46. package/dist/full.es.mjs +87 -64
  47. package/dist/full.es.mjs.map +1 -1
  48. package/dist/full.iife.js +2 -2
  49. package/dist/full.iife.js.map +1 -1
  50. package/dist/full.umd.js +2 -2
  51. package/dist/full.umd.js.map +1 -1
  52. package/dist/index.es.mjs +138 -68
  53. package/dist/index.es.mjs.map +1 -1
  54. package/dist/motion/animate.d.ts +25 -0
  55. package/dist/motion/animate.d.ts.map +1 -0
  56. package/dist/motion/easing.d.ts +30 -0
  57. package/dist/motion/easing.d.ts.map +1 -0
  58. package/dist/motion/flip.d.ts +55 -0
  59. package/dist/motion/flip.d.ts.map +1 -0
  60. package/dist/motion/index.d.ts +11 -138
  61. package/dist/motion/index.d.ts.map +1 -1
  62. package/dist/motion/keyframes.d.ts +21 -0
  63. package/dist/motion/keyframes.d.ts.map +1 -0
  64. package/dist/motion/reduced-motion.d.ts +12 -0
  65. package/dist/motion/reduced-motion.d.ts.map +1 -0
  66. package/dist/motion/scroll.d.ts +15 -0
  67. package/dist/motion/scroll.d.ts.map +1 -0
  68. package/dist/motion/spring.d.ts +42 -0
  69. package/dist/motion/spring.d.ts.map +1 -0
  70. package/dist/motion/stagger.d.ts +22 -0
  71. package/dist/motion/stagger.d.ts.map +1 -0
  72. package/dist/motion/timeline.d.ts +21 -0
  73. package/dist/motion/timeline.d.ts.map +1 -0
  74. package/dist/motion/transition.d.ts +22 -0
  75. package/dist/motion/transition.d.ts.map +1 -0
  76. package/dist/motion/types.d.ts +182 -0
  77. package/dist/motion/types.d.ts.map +1 -0
  78. package/dist/motion.es.mjs +320 -61
  79. package/dist/motion.es.mjs.map +1 -1
  80. package/dist/persisted-DHoi3uEs.js +278 -0
  81. package/dist/persisted-DHoi3uEs.js.map +1 -0
  82. package/dist/platform/storage.d.ts.map +1 -1
  83. package/dist/platform.es.mjs +12 -7
  84. package/dist/platform.es.mjs.map +1 -1
  85. package/dist/reactive/batch.d.ts +13 -0
  86. package/dist/reactive/batch.d.ts.map +1 -0
  87. package/dist/reactive/computed.d.ts +50 -0
  88. package/dist/reactive/computed.d.ts.map +1 -0
  89. package/dist/reactive/core.d.ts +72 -0
  90. package/dist/reactive/core.d.ts.map +1 -0
  91. package/dist/reactive/effect.d.ts +15 -0
  92. package/dist/reactive/effect.d.ts.map +1 -0
  93. package/dist/reactive/index.d.ts +2 -2
  94. package/dist/reactive/index.d.ts.map +1 -1
  95. package/dist/reactive/internals.d.ts +42 -0
  96. package/dist/reactive/internals.d.ts.map +1 -0
  97. package/dist/reactive/linked.d.ts +36 -0
  98. package/dist/reactive/linked.d.ts.map +1 -0
  99. package/dist/reactive/persisted.d.ts +14 -0
  100. package/dist/reactive/persisted.d.ts.map +1 -0
  101. package/dist/reactive/readonly.d.ts +26 -0
  102. package/dist/reactive/readonly.d.ts.map +1 -0
  103. package/dist/reactive/signal.d.ts +13 -312
  104. package/dist/reactive/signal.d.ts.map +1 -1
  105. package/dist/reactive/type-guards.d.ts +20 -0
  106. package/dist/reactive/type-guards.d.ts.map +1 -0
  107. package/dist/reactive/untrack.d.ts +29 -0
  108. package/dist/reactive/untrack.d.ts.map +1 -0
  109. package/dist/reactive/watch.d.ts +42 -0
  110. package/dist/reactive/watch.d.ts.map +1 -0
  111. package/dist/reactive.es.mjs +30 -163
  112. package/dist/reactive.es.mjs.map +1 -1
  113. package/dist/router/index.d.ts +6 -252
  114. package/dist/router/index.d.ts.map +1 -1
  115. package/dist/router/links.d.ts +44 -0
  116. package/dist/router/links.d.ts.map +1 -0
  117. package/dist/router/match.d.ts +20 -0
  118. package/dist/router/match.d.ts.map +1 -0
  119. package/dist/router/navigation.d.ts +45 -0
  120. package/dist/router/navigation.d.ts.map +1 -0
  121. package/dist/router/query.d.ts +16 -0
  122. package/dist/router/query.d.ts.map +1 -0
  123. package/dist/router/router.d.ts +34 -0
  124. package/dist/router/router.d.ts.map +1 -0
  125. package/dist/router/state.d.ts +27 -0
  126. package/dist/router/state.d.ts.map +1 -0
  127. package/dist/router/types.d.ts +88 -0
  128. package/dist/router/types.d.ts.map +1 -0
  129. package/dist/router/utils.d.ts +65 -0
  130. package/dist/router/utils.d.ts.map +1 -0
  131. package/dist/router.es.mjs +168 -132
  132. package/dist/router.es.mjs.map +1 -1
  133. package/dist/sanitize-Cxvxa-DX.js +283 -0
  134. package/dist/sanitize-Cxvxa-DX.js.map +1 -0
  135. package/dist/security/constants.d.ts +42 -0
  136. package/dist/security/constants.d.ts.map +1 -0
  137. package/dist/security/csp.d.ts +24 -0
  138. package/dist/security/csp.d.ts.map +1 -0
  139. package/dist/security/index.d.ts +4 -2
  140. package/dist/security/index.d.ts.map +1 -1
  141. package/dist/security/sanitize-core.d.ts +13 -0
  142. package/dist/security/sanitize-core.d.ts.map +1 -0
  143. package/dist/security/sanitize.d.ts +5 -57
  144. package/dist/security/sanitize.d.ts.map +1 -1
  145. package/dist/security/trusted-types.d.ts +25 -0
  146. package/dist/security/trusted-types.d.ts.map +1 -0
  147. package/dist/security/types.d.ts +36 -0
  148. package/dist/security/types.d.ts.map +1 -0
  149. package/dist/security.es.mjs +50 -277
  150. package/dist/security.es.mjs.map +1 -1
  151. package/dist/store/create-store.d.ts +15 -0
  152. package/dist/store/create-store.d.ts.map +1 -0
  153. package/dist/store/define-store.d.ts +28 -0
  154. package/dist/store/define-store.d.ts.map +1 -0
  155. package/dist/store/devtools.d.ts +22 -0
  156. package/dist/store/devtools.d.ts.map +1 -0
  157. package/dist/store/index.d.ts +10 -286
  158. package/dist/store/index.d.ts.map +1 -1
  159. package/dist/store/mapping.d.ts +28 -0
  160. package/dist/store/mapping.d.ts.map +1 -0
  161. package/dist/store/persisted.d.ts +13 -0
  162. package/dist/store/persisted.d.ts.map +1 -0
  163. package/dist/store/plugins.d.ts +13 -0
  164. package/dist/store/plugins.d.ts.map +1 -0
  165. package/dist/store/registry.d.ts +28 -0
  166. package/dist/store/registry.d.ts.map +1 -0
  167. package/dist/store/types.d.ts +71 -0
  168. package/dist/store/types.d.ts.map +1 -0
  169. package/dist/store/utils.d.ts +28 -0
  170. package/dist/store/utils.d.ts.map +1 -0
  171. package/dist/store/watch.d.ts +23 -0
  172. package/dist/store/watch.d.ts.map +1 -0
  173. package/dist/store.es.mjs +22 -224
  174. package/dist/store.es.mjs.map +1 -1
  175. package/dist/type-guards-BdKlYYlS.js +32 -0
  176. package/dist/type-guards-BdKlYYlS.js.map +1 -0
  177. package/dist/untrack-DNnnqdlR.js +6 -0
  178. package/dist/untrack-DNnnqdlR.js.map +1 -0
  179. package/dist/view/directives/bind.d.ts +7 -0
  180. package/dist/view/directives/bind.d.ts.map +1 -0
  181. package/dist/view/directives/class.d.ts +8 -0
  182. package/dist/view/directives/class.d.ts.map +1 -0
  183. package/dist/view/directives/for.d.ts +23 -0
  184. package/dist/view/directives/for.d.ts.map +1 -0
  185. package/dist/view/directives/html.d.ts +7 -0
  186. package/dist/view/directives/html.d.ts.map +1 -0
  187. package/dist/view/directives/if.d.ts +7 -0
  188. package/dist/view/directives/if.d.ts.map +1 -0
  189. package/dist/view/directives/index.d.ts +12 -0
  190. package/dist/view/directives/index.d.ts.map +1 -0
  191. package/dist/view/directives/model.d.ts +7 -0
  192. package/dist/view/directives/model.d.ts.map +1 -0
  193. package/dist/view/directives/on.d.ts +7 -0
  194. package/dist/view/directives/on.d.ts.map +1 -0
  195. package/dist/view/directives/ref.d.ts +7 -0
  196. package/dist/view/directives/ref.d.ts.map +1 -0
  197. package/dist/view/directives/show.d.ts +7 -0
  198. package/dist/view/directives/show.d.ts.map +1 -0
  199. package/dist/view/directives/style.d.ts +7 -0
  200. package/dist/view/directives/style.d.ts.map +1 -0
  201. package/dist/view/directives/text.d.ts +7 -0
  202. package/dist/view/directives/text.d.ts.map +1 -0
  203. package/dist/view/evaluate.d.ts +43 -0
  204. package/dist/view/evaluate.d.ts.map +1 -0
  205. package/dist/view/index.d.ts +3 -93
  206. package/dist/view/index.d.ts.map +1 -1
  207. package/dist/view/mount.d.ts +69 -0
  208. package/dist/view/mount.d.ts.map +1 -0
  209. package/dist/view/process.d.ts +26 -0
  210. package/dist/view/process.d.ts.map +1 -0
  211. package/dist/view/types.d.ts +36 -0
  212. package/dist/view/types.d.ts.map +1 -0
  213. package/dist/view.es.mjs +358 -251
  214. package/dist/view.es.mjs.map +1 -1
  215. package/dist/watch-DXXv3iAI.js +58 -0
  216. package/dist/watch-DXXv3iAI.js.map +1 -0
  217. package/package.json +14 -14
  218. package/src/component/component.ts +289 -0
  219. package/src/component/html.ts +53 -0
  220. package/src/component/index.ts +40 -414
  221. package/src/component/props.ts +116 -0
  222. package/src/component/types.ts +85 -0
  223. package/src/core/collection.ts +181 -7
  224. package/src/core/dom.ts +38 -0
  225. package/src/core/element.ts +59 -25
  226. package/src/core/index.ts +48 -4
  227. package/src/core/utils/array.ts +102 -0
  228. package/src/core/utils/function.ts +151 -0
  229. package/src/core/utils/index.ts +83 -0
  230. package/src/core/utils/misc.ts +82 -0
  231. package/src/core/utils/number.ts +78 -0
  232. package/src/core/utils/object.ts +206 -0
  233. package/src/core/utils/string.ts +112 -0
  234. package/src/core/utils/type-guards.ts +112 -0
  235. package/src/full.ts +187 -150
  236. package/src/index.ts +36 -36
  237. package/src/motion/animate.ts +113 -0
  238. package/src/motion/easing.ts +40 -0
  239. package/src/motion/flip.ts +176 -0
  240. package/src/motion/index.ts +41 -358
  241. package/src/motion/keyframes.ts +46 -0
  242. package/src/motion/reduced-motion.ts +17 -0
  243. package/src/motion/scroll.ts +57 -0
  244. package/src/motion/spring.ts +150 -0
  245. package/src/motion/stagger.ts +43 -0
  246. package/src/motion/timeline.ts +246 -0
  247. package/src/motion/transition.ts +51 -0
  248. package/src/motion/types.ts +198 -0
  249. package/src/platform/storage.ts +215 -208
  250. package/src/reactive/batch.ts +22 -0
  251. package/src/reactive/computed.ts +92 -0
  252. package/src/reactive/core.ts +114 -0
  253. package/src/reactive/effect.ts +54 -0
  254. package/src/reactive/index.ts +23 -22
  255. package/src/reactive/internals.ts +122 -0
  256. package/src/reactive/linked.ts +56 -0
  257. package/src/reactive/persisted.ts +74 -0
  258. package/src/reactive/readonly.ts +35 -0
  259. package/src/reactive/signal.ts +20 -520
  260. package/src/reactive/type-guards.ts +22 -0
  261. package/src/reactive/untrack.ts +31 -0
  262. package/src/reactive/watch.ts +73 -0
  263. package/src/router/index.ts +41 -718
  264. package/src/router/links.ts +130 -0
  265. package/src/router/match.ts +106 -0
  266. package/src/router/navigation.ts +71 -0
  267. package/src/router/query.ts +35 -0
  268. package/src/router/router.ts +211 -0
  269. package/src/router/state.ts +46 -0
  270. package/src/router/types.ts +93 -0
  271. package/src/router/utils.ts +116 -0
  272. package/src/security/constants.ts +209 -0
  273. package/src/security/csp.ts +77 -0
  274. package/src/security/index.ts +4 -12
  275. package/src/security/sanitize-core.ts +364 -0
  276. package/src/security/sanitize.ts +66 -625
  277. package/src/security/trusted-types.ts +69 -0
  278. package/src/security/types.ts +40 -0
  279. package/src/store/create-store.ts +329 -0
  280. package/src/store/define-store.ts +48 -0
  281. package/src/store/devtools.ts +45 -0
  282. package/src/store/index.ts +22 -848
  283. package/src/store/mapping.ts +73 -0
  284. package/src/store/persisted.ts +61 -0
  285. package/src/store/plugins.ts +32 -0
  286. package/src/store/registry.ts +51 -0
  287. package/src/store/types.ts +94 -0
  288. package/src/store/utils.ts +141 -0
  289. package/src/store/watch.ts +52 -0
  290. package/src/view/directives/bind.ts +23 -0
  291. package/src/view/directives/class.ts +70 -0
  292. package/src/view/directives/for.ts +275 -0
  293. package/src/view/directives/html.ts +19 -0
  294. package/src/view/directives/if.ts +30 -0
  295. package/src/view/directives/index.ts +11 -0
  296. package/src/view/directives/model.ts +56 -0
  297. package/src/view/directives/on.ts +41 -0
  298. package/src/view/directives/ref.ts +41 -0
  299. package/src/view/directives/show.ts +26 -0
  300. package/src/view/directives/style.ts +47 -0
  301. package/src/view/directives/text.ts +15 -0
  302. package/src/view/evaluate.ts +290 -0
  303. package/src/view/index.ts +112 -1041
  304. package/src/view/mount.ts +200 -0
  305. package/src/view/process.ts +92 -0
  306. package/src/view/types.ts +44 -0
  307. package/dist/core/utils.d.ts +0 -313
  308. package/dist/core/utils.d.ts.map +0 -1
  309. package/src/core/utils.ts +0 -444
@@ -0,0 +1,130 @@
1
+ /**
2
+ * Link helpers for client-side navigation.
3
+ * @module bquery/router
4
+ */
5
+
6
+ import { getActiveRouter } from './state';
7
+ import { navigate } from './navigation';
8
+
9
+ // ============================================================================
10
+ // Router Link Helper
11
+ // ============================================================================
12
+
13
+ /**
14
+ * Creates click handler for router links.
15
+ * Attach to anchor elements to enable client-side navigation.
16
+ *
17
+ * @param path - Target path
18
+ * @param options - Navigation options
19
+ * @returns Click event handler
20
+ *
21
+ * @example
22
+ * ```ts
23
+ * import { link } from 'bquery/router';
24
+ * import { $ } from 'bquery/core';
25
+ *
26
+ * $('#nav-home').on('click', link('/'));
27
+ * $('#nav-about').on('click', link('/about'));
28
+ * ```
29
+ */
30
+ export const link = (path: string, options: { replace?: boolean } = {}): ((e: Event) => void) => {
31
+ return (e: Event) => {
32
+ e.preventDefault();
33
+ void navigate(path, options).catch((err) => {
34
+ console.error('Navigation failed:', err);
35
+ });
36
+ };
37
+ };
38
+
39
+ /**
40
+ * Intercepts all link clicks within a container for client-side routing.
41
+ * Only intercepts links with matching origins and no target attribute.
42
+ *
43
+ * @param container - The container element to intercept links in
44
+ * @returns Cleanup function to remove the listener
45
+ *
46
+ * @example
47
+ * ```ts
48
+ * import { interceptLinks } from 'bquery/router';
49
+ *
50
+ * // Intercept all links in the app
51
+ * const cleanup = interceptLinks(document.body);
52
+ *
53
+ * // Later, remove the interceptor
54
+ * cleanup();
55
+ * ```
56
+ */
57
+ export const interceptLinks = (container?: Element): (() => void) => {
58
+ // Provide safe default in DOM environments only
59
+ const targetContainer = container ?? (typeof document !== 'undefined' ? document.body : null);
60
+ if (!targetContainer) {
61
+ // No container available (SSR or invalid input)
62
+ return () => undefined;
63
+ }
64
+
65
+ const handler = (e: Event) => {
66
+ // Only intercept standard left-clicks without modifier keys
67
+ if (!(e instanceof MouseEvent)) return;
68
+ if (e.defaultPrevented) return; // Already handled
69
+ if (e.button !== 0) return; // Not left-click (middle-click opens new tab, right-click shows context menu)
70
+ if (e.ctrlKey || e.metaKey || e.shiftKey || e.altKey) return; // Modifier keys (Ctrl/Cmd-click = new tab)
71
+
72
+ // Guard against non-Element targets and non-DOM environments
73
+ if (typeof Element === 'undefined' || !(e.target instanceof Element)) return;
74
+ const target = e.target as HTMLElement;
75
+ const anchor = target.closest('a');
76
+
77
+ if (!anchor) return;
78
+
79
+ // Cross-realm compatible anchor check: use owner document's constructor if available
80
+ const anchorWindow = anchor.ownerDocument.defaultView;
81
+ const AnchorConstructor = anchorWindow?.HTMLAnchorElement ?? HTMLAnchorElement;
82
+ if (!(anchor instanceof AnchorConstructor)) return;
83
+
84
+ if (anchor.target) return; // Has target attribute
85
+ if (anchor.hasAttribute('download')) return;
86
+ if (typeof window === 'undefined') return; // Non-window environment
87
+ if (anchor.origin !== window.location.origin) return; // External link
88
+
89
+ // Get active router config to handle base paths correctly.
90
+ // If no router is active, proceed with no base/hash; navigate() will throw a
91
+ // "No router initialized" error, which is caught and logged below.
92
+ const router = getActiveRouter();
93
+ if (!router) {
94
+ // No active router - trigger navigate(), allowing its error to be logged here
95
+ e.preventDefault();
96
+ void navigate(anchor.pathname + anchor.search + anchor.hash).catch((err) => {
97
+ console.error('Navigation failed:', err);
98
+ });
99
+ return;
100
+ }
101
+
102
+ const base = router.base;
103
+ const useHash = router.hash;
104
+
105
+ // Detect hash-routing mode: links written as href="#/page"
106
+ // In this case, anchor.hash contains the route path
107
+ let path: string;
108
+ if (useHash && anchor.hash && anchor.hash.startsWith('#/')) {
109
+ // Hash-routing mode: extract path from the hash
110
+ // e.g., href="#/page?foo=bar" → path = "/page?foo=bar"
111
+ path = anchor.hash.slice(1); // Remove leading #
112
+ } else {
113
+ // History mode: use pathname + search + hash
114
+ // Strip base from pathname to avoid duplication (router.push() re-adds it)
115
+ let pathname = anchor.pathname;
116
+ if (base && base !== '/' && pathname.startsWith(base)) {
117
+ pathname = pathname.slice(base.length) || '/';
118
+ }
119
+ path = pathname + anchor.search + anchor.hash;
120
+ }
121
+
122
+ e.preventDefault();
123
+ void navigate(path).catch((err) => {
124
+ console.error('Navigation failed:', err);
125
+ });
126
+ };
127
+
128
+ targetContainer.addEventListener('click', handler);
129
+ return () => targetContainer.removeEventListener('click', handler);
130
+ };
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Route matching helpers.
3
+ * @module bquery/router
4
+ */
5
+
6
+ import { parseQuery } from './query';
7
+ import type { Route, RouteDefinition } from './types';
8
+
9
+ // ============================================================================
10
+ // Route Matching
11
+ // ============================================================================
12
+
13
+ /**
14
+ * Converts a route path pattern to a RegExp for matching.
15
+ * Uses placeholder approach to preserve :param and * patterns during escaping.
16
+ * Returns positional capture groups for maximum compatibility.
17
+ * @internal
18
+ */
19
+ const pathToRegex = (path: string): RegExp => {
20
+ // Handle wildcard-only route
21
+ if (path === '*') {
22
+ return /^.*$/;
23
+ }
24
+
25
+ // Unique placeholders using null chars (won't appear in normal paths)
26
+ const PARAM_MARKER = '\u0000P\u0000';
27
+ const WILDCARD_MARKER = '\u0000W\u0000';
28
+
29
+ // Step 1: Extract :param patterns before escaping
30
+ let pattern = path.replace(/:([a-zA-Z_][a-zA-Z0-9_]*)/g, () => {
31
+ return PARAM_MARKER;
32
+ });
33
+
34
+ // Step 2: Extract * wildcards before escaping
35
+ pattern = pattern.replace(/\*/g, WILDCARD_MARKER);
36
+
37
+ // Step 3: Escape ALL regex metacharacters: \ ^ $ . * + ? ( ) [ ] { } |
38
+ pattern = pattern.replace(/[\\^$.*+?()[\]{}|]/g, '\\$&');
39
+
40
+ // Step 4: Restore param capture groups (positional, not named)
41
+ pattern = pattern.replace(/\u0000P\u0000/g, () => `([^/]+)`);
42
+
43
+ // Step 5: Restore wildcards as .*
44
+ pattern = pattern.replace(/\u0000W\u0000/g, '.*');
45
+
46
+ return new RegExp(`^${pattern}$`);
47
+ };
48
+
49
+ /**
50
+ * Extracts param names from a route path.
51
+ * @internal
52
+ */
53
+ const extractParamNames = (path: string): string[] => {
54
+ const matches = path.match(/:([a-zA-Z_][a-zA-Z0-9_]*)/g);
55
+ return matches ? matches.map((m) => m.slice(1)) : [];
56
+ };
57
+
58
+ /**
59
+ * Matches a path against route definitions and extracts params.
60
+ * Uses positional captures for maximum compatibility.
61
+ * @internal
62
+ */
63
+ export const matchRoute = (
64
+ path: string,
65
+ routes: RouteDefinition[]
66
+ ): { matched: RouteDefinition; params: Record<string, string> } | null => {
67
+ for (const route of routes) {
68
+ const regex = pathToRegex(route.path);
69
+ const match = path.match(regex);
70
+
71
+ if (match) {
72
+ const paramNames = extractParamNames(route.path);
73
+ const params: Record<string, string> = {};
74
+
75
+ // Map positional captures to param names
76
+ paramNames.forEach((name, index) => {
77
+ params[name] = match[index + 1] || '';
78
+ });
79
+
80
+ return { matched: route, params };
81
+ }
82
+ }
83
+
84
+ return null;
85
+ };
86
+
87
+ /**
88
+ * Creates a Route object from the current URL.
89
+ * @internal
90
+ */
91
+ export const createRoute = (
92
+ pathname: string,
93
+ search: string,
94
+ hash: string,
95
+ routes: RouteDefinition[]
96
+ ): Route => {
97
+ const result = matchRoute(pathname, routes);
98
+
99
+ return {
100
+ path: pathname,
101
+ params: result?.params ?? {},
102
+ query: parseQuery(search),
103
+ matched: result?.matched ?? null,
104
+ hash: hash.replace(/^#/, ''),
105
+ };
106
+ };
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Navigation helpers and global router access.
3
+ * @module bquery/router
4
+ */
5
+
6
+ import { getActiveRouter } from './state';
7
+
8
+ /**
9
+ * Navigates to a new path.
10
+ *
11
+ * @param path - The path to navigate to
12
+ * @param options - Navigation options
13
+ *
14
+ * @example
15
+ * ```ts
16
+ * import { navigate } from 'bquery/router';
17
+ *
18
+ * // Push to history
19
+ * await navigate('/dashboard');
20
+ *
21
+ * // Replace current entry
22
+ * await navigate('/login', { replace: true });
23
+ * ```
24
+ */
25
+ export const navigate = async (
26
+ path: string,
27
+ options: { replace?: boolean } = {}
28
+ ): Promise<void> => {
29
+ const activeRouter = getActiveRouter();
30
+ if (!activeRouter) {
31
+ throw new Error('bQuery router: No router initialized. Call createRouter() first.');
32
+ }
33
+
34
+ await activeRouter[options.replace ? 'replace' : 'push'](path);
35
+ };
36
+
37
+ /**
38
+ * Programmatically go back in history.
39
+ *
40
+ * @example
41
+ * ```ts
42
+ * import { back } from 'bquery/router';
43
+ * back();
44
+ * ```
45
+ */
46
+ export const back = (): void => {
47
+ const activeRouter = getActiveRouter();
48
+ if (activeRouter) {
49
+ activeRouter.back();
50
+ } else {
51
+ history.back();
52
+ }
53
+ };
54
+
55
+ /**
56
+ * Programmatically go forward in history.
57
+ *
58
+ * @example
59
+ * ```ts
60
+ * import { forward } from 'bquery/router';
61
+ * forward();
62
+ * ```
63
+ */
64
+ export const forward = (): void => {
65
+ const activeRouter = getActiveRouter();
66
+ if (activeRouter) {
67
+ activeRouter.forward();
68
+ } else {
69
+ history.forward();
70
+ }
71
+ };
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Query string helpers.
3
+ * @module bquery/router
4
+ */
5
+
6
+ /**
7
+ * Parses query string into an object.
8
+ * Single values are stored as strings, duplicate keys become arrays.
9
+ * @internal
10
+ *
11
+ * @example
12
+ * parseQuery('?foo=1') // { foo: '1' }
13
+ * parseQuery('?tag=a&tag=b') // { tag: ['a', 'b'] }
14
+ * parseQuery('?x=1&y=2&x=3') // { x: ['1', '3'], y: '2' }
15
+ */
16
+ export const parseQuery = (search: string): Record<string, string | string[]> => {
17
+ const query: Record<string, string | string[]> = {};
18
+ const params = new URLSearchParams(search);
19
+
20
+ params.forEach((value, key) => {
21
+ const existing = query[key];
22
+ if (existing === undefined) {
23
+ // First occurrence: store as string
24
+ query[key] = value;
25
+ } else if (Array.isArray(existing)) {
26
+ // Already an array: append
27
+ existing.push(value);
28
+ } else {
29
+ // Second occurrence: convert to array
30
+ query[key] = [existing, value];
31
+ }
32
+ });
33
+
34
+ return query;
35
+ };
@@ -0,0 +1,211 @@
1
+ /**
2
+ * Router creation and lifecycle management.
3
+ * @module bquery/router
4
+ */
5
+
6
+ import { createRoute } from './match';
7
+ import { currentRoute, getActiveRouter, routeSignal, setActiveRouter } from './state';
8
+ import type { NavigationGuard, Route, Router, RouterOptions } from './types';
9
+ import { flattenRoutes } from './utils';
10
+
11
+ // ============================================================================
12
+ // Router Creation
13
+ // ============================================================================
14
+
15
+ /**
16
+ * Creates and initializes a router instance.
17
+ *
18
+ * @param options - Router configuration
19
+ * @returns The router instance
20
+ *
21
+ * @example
22
+ * ```ts
23
+ * import { createRouter } from 'bquery/router';
24
+ *
25
+ * const router = createRouter({
26
+ * routes: [
27
+ * { path: '/', component: () => import('./pages/Home') },
28
+ * { path: '/about', component: () => import('./pages/About') },
29
+ * { path: '/user/:id', component: () => import('./pages/User') },
30
+ * { path: '*', component: () => import('./pages/NotFound') },
31
+ * ],
32
+ * base: '/app',
33
+ * });
34
+ *
35
+ * router.beforeEach((to, from) => {
36
+ * if (to.path === '/admin' && !isAuthenticated()) {
37
+ * return false; // Cancel navigation
38
+ * }
39
+ * });
40
+ * ```
41
+ */
42
+ export const createRouter = (options: RouterOptions): Router => {
43
+ // Clean up any existing router to prevent guard leakage
44
+ const existingRouter = getActiveRouter();
45
+ if (existingRouter) {
46
+ existingRouter.destroy();
47
+ }
48
+
49
+ const { routes, base = '', hash: useHash = false } = options;
50
+
51
+ // Instance-specific guards and hooks (not shared globally)
52
+ const beforeGuards: NavigationGuard[] = [];
53
+ const afterHooks: Array<(to: Route, from: Route) => void> = [];
54
+
55
+ // Flatten nested routes (base-relative, not including the base path)
56
+ const flatRoutes = flattenRoutes(routes);
57
+
58
+ /**
59
+ * Gets the current path from the URL.
60
+ */
61
+ const getCurrentPath = (): { pathname: string; search: string; hash: string } => {
62
+ if (useHash) {
63
+ const hashPath = window.location.hash.slice(1) || '/';
64
+ // In hash routing, URL structure is #/path?query#fragment
65
+ // Extract hash fragment first (after the second #)
66
+ const [pathWithQuery, hashPart = ''] = hashPath.split('#');
67
+ // Then extract query from the path
68
+ const [pathname, search = ''] = pathWithQuery.split('?');
69
+ return {
70
+ pathname,
71
+ search: search ? `?${search}` : '',
72
+ hash: hashPart ? `#${hashPart}` : '',
73
+ };
74
+ }
75
+
76
+ let pathname = window.location.pathname;
77
+ if (base && (pathname === base || pathname.startsWith(base + '/'))) {
78
+ pathname = pathname.slice(base.length) || '/';
79
+ }
80
+
81
+ return {
82
+ pathname,
83
+ search: window.location.search,
84
+ hash: window.location.hash,
85
+ };
86
+ };
87
+
88
+ /**
89
+ * Updates the route signal with current URL state.
90
+ */
91
+ const syncRoute = (): void => {
92
+ const { pathname, search, hash } = getCurrentPath();
93
+ const newRoute = createRoute(pathname, search, hash, flatRoutes);
94
+ routeSignal.value = newRoute;
95
+ };
96
+
97
+ /**
98
+ * Performs navigation with guards.
99
+ */
100
+ const performNavigation = async (
101
+ path: string,
102
+ method: 'pushState' | 'replaceState'
103
+ ): Promise<void> => {
104
+ const { pathname, search, hash } = getCurrentPath();
105
+ const from = createRoute(pathname, search, hash, flatRoutes);
106
+
107
+ // Parse the target path
108
+ const url = new URL(path, window.location.origin);
109
+ const to = createRoute(url.pathname, url.search, url.hash, flatRoutes);
110
+
111
+ // Run beforeEach guards
112
+ for (const guard of beforeGuards) {
113
+ const result = await guard(to, from);
114
+ if (result === false) {
115
+ return; // Cancel navigation
116
+ }
117
+ }
118
+
119
+ // Update browser history
120
+ const fullPath = useHash ? `#${path}` : `${base}${path}`;
121
+ history[method]({}, '', fullPath);
122
+
123
+ // Update route signal
124
+ syncRoute();
125
+
126
+ // Run afterEach hooks
127
+ for (const hook of afterHooks) {
128
+ hook(routeSignal.value, from);
129
+ }
130
+ };
131
+
132
+ /**
133
+ * Handle popstate events (back/forward).
134
+ */
135
+ const handlePopState = async (): Promise<void> => {
136
+ const { pathname, search, hash } = getCurrentPath();
137
+ const from = routeSignal.value;
138
+ const to = createRoute(pathname, search, hash, flatRoutes);
139
+
140
+ // Run beforeEach guards (supports async guards)
141
+ for (const guard of beforeGuards) {
142
+ const result = await guard(to, from);
143
+ if (result === false) {
144
+ // Restore previous state with full URL (including query/hash)
145
+ const queryString = new URLSearchParams(
146
+ Object.entries(from.query).flatMap(([key, value]) =>
147
+ Array.isArray(value) ? value.map((v) => [key, v]) : [[key, value]]
148
+ )
149
+ ).toString();
150
+ const search = queryString ? `?${queryString}` : '';
151
+ const hash = from.hash ? `#${from.hash}` : '';
152
+ const restorePath = useHash
153
+ ? `#${from.path}${search}${hash}`
154
+ : `${base}${from.path}${search}${hash}`;
155
+ history.replaceState({}, '', restorePath);
156
+ return;
157
+ }
158
+ }
159
+
160
+ syncRoute();
161
+
162
+ for (const hook of afterHooks) {
163
+ hook(routeSignal.value, from);
164
+ }
165
+ };
166
+
167
+ // Attach popstate listener
168
+ window.addEventListener('popstate', handlePopState);
169
+
170
+ // Initialize route
171
+ syncRoute();
172
+
173
+ const router: Router = {
174
+ push: (path: string) => performNavigation(path, 'pushState'),
175
+ replace: (path: string) => performNavigation(path, 'replaceState'),
176
+ back: () => history.back(),
177
+ forward: () => history.forward(),
178
+ go: (delta: number) => history.go(delta),
179
+
180
+ beforeEach: (guard: NavigationGuard) => {
181
+ beforeGuards.push(guard);
182
+ return () => {
183
+ const index = beforeGuards.indexOf(guard);
184
+ if (index > -1) beforeGuards.splice(index, 1);
185
+ };
186
+ },
187
+
188
+ afterEach: (hook: (to: Route, from: Route) => void) => {
189
+ afterHooks.push(hook);
190
+ return () => {
191
+ const index = afterHooks.indexOf(hook);
192
+ if (index > -1) afterHooks.splice(index, 1);
193
+ };
194
+ },
195
+
196
+ currentRoute,
197
+ routes: flatRoutes,
198
+ base,
199
+ hash: useHash,
200
+
201
+ destroy: () => {
202
+ window.removeEventListener('popstate', handlePopState);
203
+ beforeGuards.length = 0;
204
+ afterHooks.length = 0;
205
+ setActiveRouter(null);
206
+ },
207
+ };
208
+
209
+ setActiveRouter(router);
210
+ return router;
211
+ };
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Internal router state (active router and current route signal).
3
+ * @module bquery/router
4
+ */
5
+
6
+ import { computed, signal, type ReadonlySignal, type Signal } from '../reactive/index';
7
+ import type { Route, Router } from './types';
8
+
9
+ // ============================================================================
10
+ // Internal State
11
+ // ============================================================================
12
+
13
+ /** @internal */
14
+ let activeRouter: Router | null = null;
15
+
16
+ /** @internal */
17
+ export const routeSignal: Signal<Route> = signal<Route>({
18
+ path: '',
19
+ params: {},
20
+ query: {},
21
+ matched: null,
22
+ hash: '',
23
+ });
24
+
25
+ /**
26
+ * Reactive signal containing the current route.
27
+ *
28
+ * @example
29
+ * ```ts
30
+ * import { currentRoute } from 'bquery/router';
31
+ * import { effect } from 'bquery/reactive';
32
+ *
33
+ * effect(() => {
34
+ * document.title = `Page: ${currentRoute.value.path}`;
35
+ * });
36
+ * ```
37
+ */
38
+ export const currentRoute: ReadonlySignal<Route> = computed(() => routeSignal.value);
39
+
40
+ /** @internal */
41
+ export const getActiveRouter = (): Router | null => activeRouter;
42
+
43
+ /** @internal */
44
+ export const setActiveRouter = (router: Router | null): void => {
45
+ activeRouter = router;
46
+ };
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Router types and public contracts.
3
+ * @module bquery/router
4
+ */
5
+
6
+ import type { ReadonlySignal } from '../reactive/index';
7
+
8
+ /**
9
+ * Represents a parsed route with matched params.
10
+ */
11
+ export type Route = {
12
+ /** The current path (e.g., '/user/42') */
13
+ path: string;
14
+ /** Extracted route params (e.g., { id: '42' }) */
15
+ params: Record<string, string>;
16
+ /**
17
+ * Query string params.
18
+ * Each key maps to a single string value by default.
19
+ * Only keys that appear multiple times in the query string become arrays.
20
+ * @example
21
+ * // ?foo=1 → { foo: '1' }
22
+ * // ?tag=a&tag=b → { tag: ['a', 'b'] }
23
+ * // ?x=1&y=2&x=3 → { x: ['1', '3'], y: '2' }
24
+ */
25
+ query: Record<string, string | string[]>;
26
+ /** The matched route definition */
27
+ matched: RouteDefinition | null;
28
+ /** Hash fragment without # */
29
+ hash: string;
30
+ };
31
+
32
+ /**
33
+ * Route definition for configuration.
34
+ */
35
+ export type RouteDefinition = {
36
+ /** Path pattern (e.g., '/user/:id', '/posts/*') */
37
+ path: string;
38
+ /** Component loader (sync or async) */
39
+ component: () => unknown | Promise<unknown>;
40
+ /** Optional route name for programmatic navigation */
41
+ name?: string;
42
+ /** Optional metadata */
43
+ meta?: Record<string, unknown>;
44
+ /** Nested child routes */
45
+ children?: RouteDefinition[];
46
+ };
47
+
48
+ /**
49
+ * Router configuration options.
50
+ */
51
+ export type RouterOptions = {
52
+ /** Array of route definitions */
53
+ routes: RouteDefinition[];
54
+ /** Base path for all routes (default: '') */
55
+ base?: string;
56
+ /** Use hash-based routing instead of history (default: false) */
57
+ hash?: boolean;
58
+ };
59
+
60
+ /**
61
+ * Navigation guard function type.
62
+ */
63
+ export type NavigationGuard = (to: Route, from: Route) => boolean | void | Promise<boolean | void>;
64
+
65
+ /**
66
+ * Router instance returned by createRouter.
67
+ */
68
+ export type Router = {
69
+ /** Navigate to a path */
70
+ push: (path: string) => Promise<void>;
71
+ /** Replace current history entry */
72
+ replace: (path: string) => Promise<void>;
73
+ /** Go back in history */
74
+ back: () => void;
75
+ /** Go forward in history */
76
+ forward: () => void;
77
+ /** Go to a specific history entry */
78
+ go: (delta: number) => void;
79
+ /** Add a beforeEach guard */
80
+ beforeEach: (guard: NavigationGuard) => () => void;
81
+ /** Add an afterEach hook */
82
+ afterEach: (hook: (to: Route, from: Route) => void) => () => void;
83
+ /** Current route (reactive) */
84
+ currentRoute: ReadonlySignal<Route>;
85
+ /** All route definitions */
86
+ routes: RouteDefinition[];
87
+ /** Base path for all routes */
88
+ base: string;
89
+ /** Whether hash-based routing is enabled */
90
+ hash: boolean;
91
+ /** Destroy the router and cleanup listeners */
92
+ destroy: () => void;
93
+ };