flet-box 0.0.0-stage → 1.0.2

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 (178) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +190 -2
  3. package/bin/cli.js +394 -0
  4. package/bin/commands/buildAndroid.js +200 -0
  5. package/bin/commands/buildIOS.js +237 -0
  6. package/bin/commands/create.js +406 -0
  7. package/bin/commands/createBundle.js +375 -0
  8. package/bin/commands/createComponent.js +115 -0
  9. package/bin/commands/createScreen.js +161 -0
  10. package/bin/commands/killServer.js +107 -0
  11. package/bin/commands/package.js +131 -0
  12. package/bin/commands/release.js +189 -0
  13. package/bin/commands/runBundle.js +62 -0
  14. package/bin/commands/runCloudflare.js +89 -0
  15. package/bin/commands/runNative.js +109 -0
  16. package/bin/commands/runServer.js +390 -0
  17. package/bin/ota/loader.js +361 -0
  18. package/bin/utils/bundleConfig.js +178 -0
  19. package/bin/utils/capacitor.js +137 -0
  20. package/bin/utils/colors.js +215 -0
  21. package/bin/utils/helpers.js +80 -0
  22. package/bin/utils/spinner.js +96 -0
  23. package/bin/utils/templates.js +1824 -0
  24. package/bin/utils/version.js +108 -0
  25. package/package.json +58 -4
  26. package/src/animations/AnimatedBox.js +607 -0
  27. package/src/animations/AnimatedText.js +105 -0
  28. package/src/animations/MatrixRain.js +220 -0
  29. package/src/animations/ParallaxBox.js +369 -0
  30. package/src/animations/animate.js +332 -0
  31. package/src/animations/index.js +25 -0
  32. package/src/app.js +41 -0
  33. package/src/assets/icon-1024-maskable.png +0 -0
  34. package/src/assets/icon-192-maskable.png +0 -0
  35. package/src/assets/icon-192.png +0 -0
  36. package/src/assets/icon-512-maskable.png +0 -0
  37. package/src/assets/icon-512.png +0 -0
  38. package/src/assets/logo.png +0 -0
  39. package/src/core/App.js +44 -0
  40. package/src/core/hmr-client.js +142 -0
  41. package/src/core/index.js +34 -0
  42. package/src/core/pwa.js +100 -0
  43. package/src/core/runApp.js +430 -0
  44. package/src/create-index.sh +237 -0
  45. package/src/fonts/MaterialIcons-Regular.woff2 +0 -0
  46. package/src/fonts/icons.css +23 -0
  47. package/src/index.d.ts +1930 -0
  48. package/src/index.js +288 -0
  49. package/src/navigations/AdaptiveScaffold.js +149 -0
  50. package/src/navigations/AppBar.js +331 -0
  51. package/src/navigations/BottomNavigation.js +298 -0
  52. package/src/navigations/CollapsibleSideBar.js +139 -0
  53. package/src/navigations/Drawer.js +333 -0
  54. package/src/navigations/DrawerItem.js +260 -0
  55. package/src/navigations/Router.js +475 -0
  56. package/src/navigations/Scaffold.js +537 -0
  57. package/src/navigations/Tabs.js +384 -0
  58. package/src/navigations/index.js +57 -0
  59. package/src/services/RamStore.js +213 -0
  60. package/src/services/Session.js +310 -0
  61. package/src/services/Storage.js +309 -0
  62. package/src/services/http.js +350 -0
  63. package/src/services/index.js +57 -0
  64. package/src/tools/animation.js +42 -0
  65. package/src/tools/array.js +95 -0
  66. package/src/tools/border.js +28 -0
  67. package/src/tools/clipboard.js +104 -0
  68. package/src/tools/color.js +329 -0
  69. package/src/tools/createList.js +215 -0
  70. package/src/tools/delay.js +55 -0
  71. package/src/tools/device.js +138 -0
  72. package/src/tools/dict.js +297 -0
  73. package/src/tools/dimensions.js +118 -0
  74. package/src/tools/filter.js +41 -0
  75. package/src/tools/flex.js +26 -0
  76. package/src/tools/gradient.js +34 -0
  77. package/src/tools/grid.js +57 -0
  78. package/src/tools/index.js +38 -0
  79. package/src/tools/mapList.js +107 -0
  80. package/src/tools/margin.js +93 -0
  81. package/src/tools/memo.js +60 -0
  82. package/src/tools/os.js +104 -0
  83. package/src/tools/padding.js +93 -0
  84. package/src/tools/print.js +69 -0
  85. package/src/tools/random.js +299 -0
  86. package/src/tools/ref.js +70 -0
  87. package/src/tools/rgba.js +4 -0
  88. package/src/tools/shadow.js +23 -0
  89. package/src/tools/string.js +85 -0
  90. package/src/tools/time.js +674 -0
  91. package/src/tools/transform.js +68 -0
  92. package/src/tools/transition.js +48 -0
  93. package/src/tools/useState.js +268 -0
  94. package/src/tools/uuid.js +42 -0
  95. package/src/utils/TextInputValidator.js +256 -0
  96. package/src/utils/composeUpdate.js +65 -0
  97. package/src/utils/getWidgetProps.js +35 -0
  98. package/src/utils/index.js +76 -0
  99. package/src/utils/markdownParser.js +368 -0
  100. package/src/utils/mediaTime.js +79 -0
  101. package/src/utils/navigation.js +263 -0
  102. package/src/utils/scriptInjector.js +73 -0
  103. package/src/utils/stackPosition.js +31 -0
  104. package/src/utils/stopWebRefresh.js +77 -0
  105. package/src/utils/styleInjector.js +38 -0
  106. package/src/utils/syntaxHighlight.js +339 -0
  107. package/src/utils/themes.js +347 -0
  108. package/src/utils/units.js +87 -0
  109. package/src/utils/visualEffects.js +169 -0
  110. package/src/widget-factory/addChildren.js +114 -0
  111. package/src/widget-factory/assignProps.js +256 -0
  112. package/src/widget-factory/createWidget.js +28 -0
  113. package/src/widget-factory/effects.js +132 -0
  114. package/src/widget-factory/index.js +7 -0
  115. package/src/widget-factory/lifecycle.js +210 -0
  116. package/src/widget-factory/makeParentable.js +89 -0
  117. package/src/widget-factory/pixTool.js +40 -0
  118. package/src/widget-factory/processProps.js +122 -0
  119. package/src/widget-factory/reactivity.js +98 -0
  120. package/src/widget-factory/remTool.js +34 -0
  121. package/src/widget-factory/stackPosition.js +87 -0
  122. package/src/widget-factory/stringTool.js +26 -0
  123. package/src/widget-factory/tools.js +249 -0
  124. package/src/widget-factory/translateProps.js +1049 -0
  125. package/src/widget-factory/warnings.js +284 -0
  126. package/src/widget-factory/widgetFactory.js +200 -0
  127. package/src/widgets/Accordion.js +562 -0
  128. package/src/widgets/AdMob.js +315 -0
  129. package/src/widgets/AdSense.js +268 -0
  130. package/src/widgets/AlertDialog.js +162 -0
  131. package/src/widgets/Audio.js +311 -0
  132. package/src/widgets/Avatar.js +210 -0
  133. package/src/widgets/Badge.js +189 -0
  134. package/src/widgets/BottomSheet.js +287 -0
  135. package/src/widgets/Button.js +247 -0
  136. package/src/widgets/Card.js +33 -0
  137. package/src/widgets/Carousel.js +301 -0
  138. package/src/widgets/Chart.js +632 -0
  139. package/src/widgets/Checkbox.js +106 -0
  140. package/src/widgets/Chip.js +153 -0
  141. package/src/widgets/CircularBar.js +352 -0
  142. package/src/widgets/CircularChart.js +549 -0
  143. package/src/widgets/CodeViewer.js +236 -0
  144. package/src/widgets/Column.js +29 -0
  145. package/src/widgets/Container.js +31 -0
  146. package/src/widgets/DataTable.js +257 -0
  147. package/src/widgets/Divider.js +93 -0
  148. package/src/widgets/DraggBox.js +115 -0
  149. package/src/widgets/Dropdown.js +505 -0
  150. package/src/widgets/DroppBox.js +180 -0
  151. package/src/widgets/FloatingActionButton.js +91 -0
  152. package/src/widgets/GridView.js +20 -0
  153. package/src/widgets/Icon.js +33 -0
  154. package/src/widgets/Image.js +15 -0
  155. package/src/widgets/Input.js +565 -0
  156. package/src/widgets/Inspector.js +254 -0
  157. package/src/widgets/InstallButton.js +110 -0
  158. package/src/widgets/ListTile.js +224 -0
  159. package/src/widgets/ListView.js +503 -0
  160. package/src/widgets/Markdown.js +1236 -0
  161. package/src/widgets/Modal.js +496 -0
  162. package/src/widgets/Pagination.js +195 -0
  163. package/src/widgets/ProgressBar.js +178 -0
  164. package/src/widgets/QRCode.js +467 -0
  165. package/src/widgets/Radio.js +132 -0
  166. package/src/widgets/Rating.js +261 -0
  167. package/src/widgets/Row.js +29 -0
  168. package/src/widgets/Skeleton.js +243 -0
  169. package/src/widgets/Slider.js +527 -0
  170. package/src/widgets/SnackBar.js +227 -0
  171. package/src/widgets/Stack.js +29 -0
  172. package/src/widgets/Stepper.js +296 -0
  173. package/src/widgets/Switch.js +102 -0
  174. package/src/widgets/Text.js +98 -0
  175. package/src/widgets/Tooltip.js +267 -0
  176. package/src/widgets/TreeView.js +370 -0
  177. package/src/widgets/Video.js +92 -0
  178. package/src/widgets/index.js +52 -0
@@ -0,0 +1,475 @@
1
+ // navigations/Router.js
2
+ /**
3
+ * Router Module — client-side routing for the flet-box framework.
4
+ *
5
+ * Provides a simple, push-state-based router with support for:
6
+ * - Static routes: `/about`, `/settings/profile`
7
+ * - Dynamic segments: `/user/:id`, `/post/:slug/comments`
8
+ * - Wildcard / 404 routes: `*` or `/404`
9
+ * - Query string parameters
10
+ * - Browser history integration (pushState / popstate)
11
+ * - Observer pattern (subscribe/notify) so layout shells like Scaffold can
12
+ * re-render when the route changes.
13
+ *
14
+ * All state is module-level, making the router a singleton. Initialize it
15
+ * once (typically inside Scaffold) via `initRouter` before using any
16
+ * navigation helpers.
17
+ */
18
+
19
+ /** @type {Object.<string, *>} Registered route map (pattern → component). */
20
+ let routes = {};
21
+
22
+ /** @type {string} The currently active path (without query string). */
23
+ let currentPath = "/";
24
+
25
+ /**
26
+ * Navigation history stack used by goBack().
27
+ * Each entry captures the path, params, and query at the time of navigation.
28
+ *
29
+ * @type {Array<{path: string, params: Object, query: Object}>}
30
+ */
31
+ let history = [];
32
+
33
+ /** @type {Function[]} Registered route-change listener callbacks. */
34
+ let listeners = [];
35
+
36
+ /** @type {Object.<string, string>} Dynamic path params for the current route. */
37
+ let currentParams = {};
38
+
39
+ /** @type {Object.<string, string>} Query string params for the current URL. */
40
+ let currentQuery = {};
41
+
42
+ /** @type {Function|null} Removes the popstate event listener; set during initRouter. */
43
+ let removePopStateListener = null;
44
+
45
+ // ========== UTILITIES ==========
46
+
47
+ /**
48
+ * Match a route pattern containing `:param` segments against a concrete path.
49
+ * Returns the extracted param map, or an empty object if the path does not match.
50
+ *
51
+ * @param {string} pattern - Route pattern, e.g. "/user/:id/post/:slug".
52
+ * @param {string} path - Concrete path, e.g. "/user/42/post/hello-world".
53
+ * @returns {Object.<string, string>} Extracted param values keyed by param name.
54
+ */
55
+ const extractParams = (pattern, path) => {
56
+ const patternParts = pattern.split("/");
57
+ const pathParts = path.split("/");
58
+ const params = {};
59
+
60
+ if (patternParts.length !== pathParts.length) return params;
61
+
62
+ for (let i = 0; i < patternParts.length; i++) {
63
+ if (patternParts[i].startsWith(":")) {
64
+ const paramName = patternParts[i].slice(1);
65
+ params[paramName] = decodeURIComponent(pathParts[i]);
66
+ } else if (patternParts[i] !== pathParts[i]) {
67
+ // Segment mismatch — this pattern does not match the path.
68
+ return {};
69
+ }
70
+ }
71
+ return params;
72
+ };
73
+
74
+ /**
75
+ * Parse query string parameters from a URL or query string.
76
+ *
77
+ * @param {string} url - Full URL or query string (with or without leading "?").
78
+ * @returns {Object.<string, string>} Key/value pairs decoded from the query string.
79
+ */
80
+ const extractQueryParams = (url) => {
81
+ const queryIndex = url.indexOf("?");
82
+ if (queryIndex === -1) return {};
83
+
84
+ const queryString = url.slice(queryIndex + 1);
85
+ const params = {};
86
+
87
+ queryString.split("&").forEach((param) => {
88
+ const [key, value] = param.split("=");
89
+ if (key) {
90
+ params[decodeURIComponent(key)] = value ? decodeURIComponent(value) : "";
91
+ }
92
+ });
93
+ return params;
94
+ };
95
+
96
+ /**
97
+ * Strip the query string from a path, returning only the path component.
98
+ *
99
+ * @param {string} path - Path, possibly with a "?query" suffix.
100
+ * @returns {string} Path without the query string.
101
+ */
102
+ const cleanPath = (path) => {
103
+ const queryIndex = path.indexOf("?");
104
+ return queryIndex === -1 ? path : path.slice(0, queryIndex);
105
+ };
106
+
107
+ /**
108
+ * Find the registered route that matches a given path.
109
+ * Tries an exact match first, then scans for patterns with dynamic segments.
110
+ *
111
+ * @param {string} path - Concrete path to resolve.
112
+ * @returns {{route: *, params: Object, pattern: string}|null}
113
+ * Matched route entry and extracted params, or null if no match found.
114
+ */
115
+ const findMatchingRoute = (path) => {
116
+ const cleanPathStr = cleanPath(path);
117
+
118
+ // Fast path: exact match.
119
+ if (routes[cleanPathStr]) {
120
+ return { route: routes[cleanPathStr], params: {}, pattern: cleanPathStr };
121
+ }
122
+
123
+ // Slow path: scan patterns for dynamic segments.
124
+ for (const [pattern, route] of Object.entries(routes)) {
125
+ if (pattern.includes(":")) {
126
+ const params = extractParams(pattern, cleanPathStr);
127
+ if (Object.keys(params).length > 0) {
128
+ return { route, params, pattern };
129
+ }
130
+ }
131
+ }
132
+ return null;
133
+ };
134
+
135
+ // ========== INITIALIZATION ==========
136
+
137
+ /**
138
+ * Bootstrap the router with a route map and an optional starting path.
139
+ *
140
+ * Reads the current browser URL when no `initialPath` is provided, so
141
+ * deep-links work correctly on first page load. Also attaches a `popstate`
142
+ * listener so the browser's back/forward buttons trigger re-renders.
143
+ *
144
+ * Call this once — typically inside the Scaffold component — before
145
+ * navigating or subscribing.
146
+ *
147
+ * @param {Object.<string, *>} routesConfig - Map of path patterns to components/config.
148
+ * @param {string|null} [initialPath=null] - Override the starting path; defaults to window.location.pathname.
149
+ */
150
+ export const initRouter = (routesConfig, initialPath = null) => {
151
+ routes = routesConfig;
152
+
153
+ // Prefer the explicit initialPath; fall back to the live browser URL so
154
+ // that deep-links resolve to the correct route on first load.
155
+ let urlPath = initialPath;
156
+ if (!urlPath && typeof window !== "undefined") {
157
+ urlPath = window.location.pathname;
158
+ }
159
+
160
+ // Last resort: use the first registered route or the root.
161
+ if (!urlPath) {
162
+ urlPath = Object.keys(routes)[0] || "/";
163
+ }
164
+
165
+ currentPath = urlPath;
166
+
167
+ // Resolve dynamic params for the initial path.
168
+ const match = findMatchingRoute(currentPath);
169
+ if (match) {
170
+ currentParams = match.params;
171
+ } else if (!routes[currentPath]) {
172
+ // Unknown route — redirect to the first registered route rather than
173
+ // showing a blank screen.
174
+ currentPath = Object.keys(routes)[0] || "/";
175
+ currentParams = {};
176
+ } else {
177
+ currentParams = {};
178
+ }
179
+
180
+ // Parse any query params present in the current URL.
181
+ currentQuery =
182
+ typeof window !== "undefined"
183
+ ? extractQueryParams(window.location.search)
184
+ : extractQueryParams(urlPath);
185
+
186
+ // Sync browser history so the address bar reflects the resolved path
187
+ // (handles the case where the user lands on an unknown path that was
188
+ // redirected to the first route above).
189
+ if (
190
+ typeof window !== "undefined" &&
191
+ window.location.pathname !== currentPath
192
+ ) {
193
+ window.history.replaceState({ path: currentPath }, "", currentPath);
194
+ }
195
+
196
+ // Listen for browser-native back/forward navigation so the router state
197
+ // stays in sync with the History API.
198
+ if (typeof window !== "undefined") {
199
+ if (removePopStateListener) removePopStateListener();
200
+ const handlePopState = (event) => {
201
+ const path = event.state?.path || window.location.pathname;
202
+ if (path !== currentPath) {
203
+ currentPath = path;
204
+ const match = findMatchingRoute(currentPath);
205
+ currentParams = match?.params || {};
206
+ currentQuery = extractQueryParams(window.location.search);
207
+ notify();
208
+ }
209
+ };
210
+ window.addEventListener("popstate", handlePopState);
211
+ removePopStateListener = () =>
212
+ window.removeEventListener("popstate", handlePopState);
213
+ }
214
+
215
+ notify();
216
+ };
217
+
218
+ // ========== NAVIGATION ==========
219
+
220
+ /**
221
+ * Navigate to a new route, pushing a new entry onto the history stack.
222
+ *
223
+ * If the target path does not match any registered route, the router tries
224
+ * to render a wildcard (`*`) or `/404` route instead and logs an error.
225
+ *
226
+ * @param {string} path - Target path (may include a query string, e.g. "/users?page=2").
227
+ * @param {Object.<string, string>} [params={}] - Additional query params merged into the URL's own query string.
228
+ */
229
+ export const goTo = (path, params = {}) => {
230
+ const queryParams = extractQueryParams(path);
231
+ const cleanPathStr = cleanPath(path);
232
+ const match = findMatchingRoute(cleanPathStr);
233
+
234
+ if (match || routes[cleanPathStr]) {
235
+ // Push the current location onto the history stack before navigating.
236
+ history.push({
237
+ path: currentPath,
238
+ params: currentParams,
239
+ query: currentQuery,
240
+ });
241
+ currentPath = cleanPathStr;
242
+ currentParams = match?.params || {};
243
+ currentQuery = { ...queryParams, ...params };
244
+
245
+ if (typeof window !== "undefined") {
246
+ window.history.pushState({ path: currentPath }, "", path);
247
+ }
248
+ notify();
249
+ } else {
250
+ console.error(`Route "${cleanPathStr}" does not exist`);
251
+ // Graceful fallback to a wildcard or 404 route when defined.
252
+ if (routes["*"] || routes["/404"]) {
253
+ const notFoundRoute = routes["*"] || routes["/404"];
254
+ currentPath = "*";
255
+ currentParams = {};
256
+ currentQuery = {};
257
+ notify({ notFound: true, originalPath: cleanPathStr });
258
+ }
259
+ }
260
+ };
261
+
262
+ /**
263
+ * Navigate to a path by replacing the current history entry rather than
264
+ * pushing a new one. Useful for redirects that should not appear in the
265
+ * back-button history (e.g. after a login redirect).
266
+ *
267
+ * @param {string} path - Target path (may include a query string).
268
+ * @param {Object.<string, string>} [params={}] - Additional query params.
269
+ */
270
+ export const replace = (path, params = {}) => {
271
+ const queryParams = extractQueryParams(path);
272
+ const cleanPathStr = cleanPath(path);
273
+ const match = findMatchingRoute(cleanPathStr);
274
+
275
+ if (match || routes[cleanPathStr]) {
276
+ currentPath = cleanPathStr;
277
+ currentParams = match?.params || {};
278
+ currentQuery = { ...queryParams, ...params };
279
+
280
+ if (typeof window !== "undefined") {
281
+ window.history.replaceState({ path: currentPath }, "", path);
282
+ }
283
+ notify();
284
+ }
285
+ };
286
+
287
+ /**
288
+ * Navigate back to the previous entry in the router's internal history stack.
289
+ * Also triggers `window.history.back()` to keep the browser history in sync.
290
+ * If the internal stack is empty, this is a no-op.
291
+ */
292
+ export const goBack = () => {
293
+ if (history.length > 0) {
294
+ const previous = history.pop();
295
+ currentPath = previous.path;
296
+ currentParams = previous.params || {};
297
+ currentQuery = previous.query || {};
298
+ if (typeof window !== "undefined") window.history.back();
299
+ notify();
300
+ }
301
+ };
302
+
303
+ /**
304
+ * Navigate forward in the browser's built-in history (mirrors window.history.forward).
305
+ */
306
+ export const goForward = () => {
307
+ if (typeof window !== "undefined") window.history.forward();
308
+ };
309
+
310
+ // ========== STATE READERS ==========
311
+
312
+ /**
313
+ * Returns the current active path (without query string).
314
+ *
315
+ * @returns {string}
316
+ */
317
+ export const getCurrentPath = () => currentPath;
318
+
319
+ /**
320
+ * Returns a shallow copy of the current dynamic route params.
321
+ * Mutations to the returned object do not affect the router state.
322
+ *
323
+ * @returns {Object.<string, string>}
324
+ */
325
+ export const useParams = () => ({ ...currentParams });
326
+
327
+ /**
328
+ * Returns a shallow copy of the current query string params.
329
+ *
330
+ * @returns {Object.<string, string>}
331
+ */
332
+ export const useQueryParams = () => ({ ...currentQuery });
333
+
334
+ /**
335
+ * Returns the component/config registered for the current route.
336
+ *
337
+ * @returns {*} The route value, or undefined when the current path has no match.
338
+ */
339
+ export const getCurrentRoute = () => {
340
+ const match = findMatchingRoute(currentPath);
341
+ return match?.route || routes[currentPath];
342
+ };
343
+
344
+ /**
345
+ * Returns a full snapshot of the current route state.
346
+ *
347
+ * @returns {{route: *, params: Object, query: Object, path: string}}
348
+ */
349
+ export const getCurrentRouteConfig = () => {
350
+ const match = findMatchingRoute(currentPath);
351
+ return {
352
+ route: match?.route || routes[currentPath],
353
+ params: currentParams,
354
+ query: currentQuery,
355
+ path: currentPath,
356
+ };
357
+ };
358
+
359
+ /**
360
+ * Look up the component/config registered for any arbitrary path.
361
+ *
362
+ * @param {string} path - Path to look up (exact or dynamic pattern match).
363
+ * @returns {*} The route value, or undefined when not found.
364
+ */
365
+ export const getRoute = (path) => {
366
+ const cleanPathStr = cleanPath(path);
367
+ const match = findMatchingRoute(cleanPathStr);
368
+ return match?.route || routes[cleanPathStr];
369
+ };
370
+
371
+ /**
372
+ * Check whether a given path is currently active.
373
+ *
374
+ * @param {string} path - Path to compare against the current route.
375
+ * @param {boolean} [exact=true] - When false, matches any path that starts with `path`.
376
+ * @returns {boolean}
377
+ */
378
+ export const isActive = (path, exact = true) => {
379
+ if (exact) return currentPath === path;
380
+ return currentPath.startsWith(path);
381
+ };
382
+
383
+ /**
384
+ * Build a URL from a pattern with dynamic segments, filling in params and
385
+ * appending a query string.
386
+ *
387
+ * @param {string} pattern - Route pattern, e.g. "/user/:id".
388
+ * @param {Object.<string, string>} [params={}] - Values for dynamic segments.
389
+ * @param {Object.<string, string>} [query={}] - Query string params to append.
390
+ * @returns {string} The resolved URL.
391
+ */
392
+ export const buildUrl = (pattern, params = {}, query = {}) => {
393
+ let url = pattern;
394
+ Object.entries(params).forEach(([key, value]) => {
395
+ url = url.replace(`:${key}`, encodeURIComponent(value));
396
+ });
397
+ const queryKeys = Object.keys(query);
398
+ if (queryKeys.length > 0) {
399
+ const queryString = queryKeys
400
+ .map(
401
+ (key) => `${encodeURIComponent(key)}=${encodeURIComponent(query[key])}`,
402
+ )
403
+ .join("&");
404
+ url += `?${queryString}`;
405
+ }
406
+ return url;
407
+ };
408
+
409
+ // ========== SUBSCRIPTIONS ==========
410
+
411
+ /**
412
+ * Register a callback to be called whenever the current route changes.
413
+ * The callback receives the new route value, params, query, and any extra
414
+ * data passed to notify() (e.g. `{ notFound: true }`).
415
+ *
416
+ * @param {Function} callback - Called as `callback(route, params, query, extras)`.
417
+ * @returns {Function} Unsubscribe function — call it to stop receiving updates.
418
+ */
419
+ export const subscribe = (callback) => {
420
+ listeners.push(callback);
421
+ return () => {
422
+ listeners = listeners.filter((l) => l !== callback);
423
+ };
424
+ };
425
+
426
+ /**
427
+ * Dispatch the current route to all registered listeners.
428
+ * Called internally after every navigation.
429
+ *
430
+ * @param {Object} [extraParams={}] - Extra context passed through to listeners.
431
+ */
432
+ const notify = (extraParams = {}) => {
433
+ const route = getCurrentRoute();
434
+ listeners.forEach((cb) =>
435
+ cb(route, currentParams, currentQuery, extraParams),
436
+ );
437
+ };
438
+
439
+ /**
440
+ * Tear down the router: remove the popstate listener, clear all state,
441
+ * and unregister all listeners.
442
+ *
443
+ * Use this during hot-module replacement or when unmounting the root scaffold
444
+ * to prevent stale listeners from persisting across reloads.
445
+ */
446
+ export const clearRouter = () => {
447
+ if (removePopStateListener) {
448
+ removePopStateListener();
449
+ removePopStateListener = null;
450
+ }
451
+ routes = {};
452
+ currentPath = "/";
453
+ history = [];
454
+ listeners = [];
455
+ currentParams = {};
456
+ currentQuery = {};
457
+ };
458
+
459
+ export default {
460
+ initRouter,
461
+ goTo,
462
+ replace,
463
+ goBack,
464
+ goForward,
465
+ getCurrentPath,
466
+ getCurrentRoute,
467
+ getCurrentRouteConfig,
468
+ getRoute,
469
+ isActive,
470
+ subscribe,
471
+ clearRouter,
472
+ useParams,
473
+ useQueryParams,
474
+ buildUrl,
475
+ };