decantr 0.9.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 (382) hide show
  1. package/AGENTS.md +868 -0
  2. package/CHANGELOG.md +255 -0
  3. package/CLAUDE.md +178 -0
  4. package/LICENSE +21 -0
  5. package/README.md +229 -0
  6. package/cli/art.js +127 -0
  7. package/cli/commands/a11y.js +61 -0
  8. package/cli/commands/audit.js +225 -0
  9. package/cli/commands/build.js +38 -0
  10. package/cli/commands/dev.js +18 -0
  11. package/cli/commands/doctor.js +197 -0
  12. package/cli/commands/figma-sync.js +48 -0
  13. package/cli/commands/figma-tokens.js +55 -0
  14. package/cli/commands/generate.js +26 -0
  15. package/cli/commands/init.js +116 -0
  16. package/cli/commands/lint.js +209 -0
  17. package/cli/commands/mcp.js +530 -0
  18. package/cli/commands/migrate.js +175 -0
  19. package/cli/commands/test.js +38 -0
  20. package/cli/commands/validate.js +354 -0
  21. package/cli/index.js +113 -0
  22. package/package.json +95 -0
  23. package/reference/atoms.md +517 -0
  24. package/reference/behaviors.md +384 -0
  25. package/reference/build-tooling.md +275 -0
  26. package/reference/color-guidelines.md +965 -0
  27. package/reference/component-lifecycle.md +137 -0
  28. package/reference/compound-spacing.md +95 -0
  29. package/reference/decantation-process.md +499 -0
  30. package/reference/dev-server-routes.md +93 -0
  31. package/reference/form-system.md +253 -0
  32. package/reference/i18n.md +336 -0
  33. package/reference/icons.md +576 -0
  34. package/reference/llm-primer.md +953 -0
  35. package/reference/plugins.md +252 -0
  36. package/reference/registry-consumption.md +76 -0
  37. package/reference/router.md +217 -0
  38. package/reference/shells.md +116 -0
  39. package/reference/spatial-guidelines.md +541 -0
  40. package/reference/ssr.md +234 -0
  41. package/reference/state-data.md +215 -0
  42. package/reference/state-patterns.md +166 -0
  43. package/reference/state.md +194 -0
  44. package/reference/style-system.md +110 -0
  45. package/reference/tokens.md +460 -0
  46. package/src/app.js +19 -0
  47. package/src/chart/_animate.js +266 -0
  48. package/src/chart/_base.js +109 -0
  49. package/src/chart/_data.js +209 -0
  50. package/src/chart/_format.js +106 -0
  51. package/src/chart/_interact.js +364 -0
  52. package/src/chart/_palette.js +105 -0
  53. package/src/chart/_renderer.js +52 -0
  54. package/src/chart/_scene.js +262 -0
  55. package/src/chart/_shared.js +371 -0
  56. package/src/chart/index.js +637 -0
  57. package/src/chart/layouts/_layout-base.js +328 -0
  58. package/src/chart/layouts/cartesian.js +148 -0
  59. package/src/chart/layouts/hierarchy.js +562 -0
  60. package/src/chart/layouts/polar.js +101 -0
  61. package/src/chart/renderers/canvas.js +179 -0
  62. package/src/chart/renderers/svg.js +256 -0
  63. package/src/chart/renderers/webgpu.js +715 -0
  64. package/src/chart/types/_type-base.js +26 -0
  65. package/src/chart/types/area.js +134 -0
  66. package/src/chart/types/bar.js +173 -0
  67. package/src/chart/types/box-plot.js +125 -0
  68. package/src/chart/types/bubble.js +63 -0
  69. package/src/chart/types/candlestick.js +115 -0
  70. package/src/chart/types/chord.js +85 -0
  71. package/src/chart/types/combination.js +108 -0
  72. package/src/chart/types/funnel.js +68 -0
  73. package/src/chart/types/gauge.js +163 -0
  74. package/src/chart/types/heatmap.js +98 -0
  75. package/src/chart/types/histogram.js +71 -0
  76. package/src/chart/types/line.js +111 -0
  77. package/src/chart/types/org-chart.js +93 -0
  78. package/src/chart/types/pie.js +81 -0
  79. package/src/chart/types/radar.js +96 -0
  80. package/src/chart/types/radial.js +68 -0
  81. package/src/chart/types/range-area.js +55 -0
  82. package/src/chart/types/range-bar.js +61 -0
  83. package/src/chart/types/sankey.js +73 -0
  84. package/src/chart/types/scatter.js +66 -0
  85. package/src/chart/types/sparkline.js +81 -0
  86. package/src/chart/types/sunburst.js +69 -0
  87. package/src/chart/types/swimlane.js +88 -0
  88. package/src/chart/types/treemap.js +62 -0
  89. package/src/chart/types/waterfall.js +100 -0
  90. package/src/components/_base.js +1658 -0
  91. package/src/components/_behaviors.js +1140 -0
  92. package/src/components/_primitives.js +534 -0
  93. package/src/components/_qr-encoder.js +539 -0
  94. package/src/components/accordion.js +207 -0
  95. package/src/components/affix.js +62 -0
  96. package/src/components/alert-dialog.js +75 -0
  97. package/src/components/alert.js +47 -0
  98. package/src/components/aspect-ratio.js +24 -0
  99. package/src/components/avatar-group.js +55 -0
  100. package/src/components/avatar.js +38 -0
  101. package/src/components/back-top.js +75 -0
  102. package/src/components/badge.js +74 -0
  103. package/src/components/banner.js +68 -0
  104. package/src/components/breadcrumb.js +162 -0
  105. package/src/components/button.js +115 -0
  106. package/src/components/calendar.js +131 -0
  107. package/src/components/card.js +192 -0
  108. package/src/components/carousel.js +98 -0
  109. package/src/components/cascader.js +261 -0
  110. package/src/components/checkbox.js +80 -0
  111. package/src/components/chip.js +81 -0
  112. package/src/components/code-block.js +82 -0
  113. package/src/components/collapsible.js +50 -0
  114. package/src/components/color-palette.js +438 -0
  115. package/src/components/color-picker.js +314 -0
  116. package/src/components/combobox.js +181 -0
  117. package/src/components/command.js +174 -0
  118. package/src/components/comment.js +206 -0
  119. package/src/components/context-menu.js +76 -0
  120. package/src/components/data-table.js +724 -0
  121. package/src/components/date-picker.js +217 -0
  122. package/src/components/date-range-picker.js +244 -0
  123. package/src/components/datetime-picker.js +271 -0
  124. package/src/components/descriptions.js +68 -0
  125. package/src/components/drawer.js +179 -0
  126. package/src/components/dropdown.js +88 -0
  127. package/src/components/empty.js +41 -0
  128. package/src/components/float-button.js +90 -0
  129. package/src/components/form.js +106 -0
  130. package/src/components/hover-card.js +49 -0
  131. package/src/components/icon.js +87 -0
  132. package/src/components/image.js +97 -0
  133. package/src/components/index.js +117 -0
  134. package/src/components/input-group.js +75 -0
  135. package/src/components/input-number.js +155 -0
  136. package/src/components/input-otp.js +178 -0
  137. package/src/components/input.js +91 -0
  138. package/src/components/kbd.js +36 -0
  139. package/src/components/label.js +25 -0
  140. package/src/components/list.js +118 -0
  141. package/src/components/masked-input.js +236 -0
  142. package/src/components/mentions.js +165 -0
  143. package/src/components/menu.js +259 -0
  144. package/src/components/message.js +80 -0
  145. package/src/components/modal.js +147 -0
  146. package/src/components/navigation-menu.js +166 -0
  147. package/src/components/notification.js +84 -0
  148. package/src/components/pagination.js +104 -0
  149. package/src/components/placeholder.js +132 -0
  150. package/src/components/popconfirm.js +70 -0
  151. package/src/components/popover.js +58 -0
  152. package/src/components/progress.js +61 -0
  153. package/src/components/qrcode.js +251 -0
  154. package/src/components/radiogroup.js +120 -0
  155. package/src/components/range-slider.js +176 -0
  156. package/src/components/rate.js +186 -0
  157. package/src/components/resizable.js +83 -0
  158. package/src/components/result.js +57 -0
  159. package/src/components/scroll-area.js +43 -0
  160. package/src/components/segmented.js +97 -0
  161. package/src/components/select.js +165 -0
  162. package/src/components/separator.js +31 -0
  163. package/src/components/shell.js +407 -0
  164. package/src/components/skeleton.js +39 -0
  165. package/src/components/slider.js +141 -0
  166. package/src/components/sortable-list.js +176 -0
  167. package/src/components/space.js +42 -0
  168. package/src/components/spinner.js +112 -0
  169. package/src/components/splitter.js +147 -0
  170. package/src/components/statistic.js +136 -0
  171. package/src/components/steps.js +99 -0
  172. package/src/components/switch.js +95 -0
  173. package/src/components/table.js +44 -0
  174. package/src/components/tabs.js +216 -0
  175. package/src/components/tag.js +115 -0
  176. package/src/components/textarea.js +82 -0
  177. package/src/components/time-picker.js +153 -0
  178. package/src/components/time-range-picker.js +170 -0
  179. package/src/components/timeline.js +226 -0
  180. package/src/components/toast.js +71 -0
  181. package/src/components/toggle.js +213 -0
  182. package/src/components/tooltip.js +57 -0
  183. package/src/components/tour.js +159 -0
  184. package/src/components/transfer.js +163 -0
  185. package/src/components/tree-select.js +274 -0
  186. package/src/components/tree.js +141 -0
  187. package/src/components/typography.js +136 -0
  188. package/src/components/upload.js +118 -0
  189. package/src/components/visually-hidden.js +20 -0
  190. package/src/components/watermark.js +124 -0
  191. package/src/core/index.js +539 -0
  192. package/src/core/lifecycle.js +69 -0
  193. package/src/css/atoms.js +651 -0
  194. package/src/css/components.js +940 -0
  195. package/src/css/derive.js +1296 -0
  196. package/src/css/index.js +265 -0
  197. package/src/css/runtime.js +268 -0
  198. package/src/css/styles/addons/bioluminescent.js +93 -0
  199. package/src/css/styles/addons/clay.js +70 -0
  200. package/src/css/styles/addons/clean.js +57 -0
  201. package/src/css/styles/addons/command-center.js +143 -0
  202. package/src/css/styles/addons/dopamine.js +83 -0
  203. package/src/css/styles/addons/editorial.js +80 -0
  204. package/src/css/styles/addons/glassmorphism.js +99 -0
  205. package/src/css/styles/addons/liquid-glass.js +105 -0
  206. package/src/css/styles/addons/prismatic.js +100 -0
  207. package/src/css/styles/addons/retro.js +63 -0
  208. package/src/css/styles/auradecantism.js +96 -0
  209. package/src/css/theme-registry.js +444 -0
  210. package/src/data/entity.js +281 -0
  211. package/src/data/index.js +13 -0
  212. package/src/data/persist.js +225 -0
  213. package/src/data/query.js +839 -0
  214. package/src/data/realtime.js +299 -0
  215. package/src/data/url.js +177 -0
  216. package/src/data/worker.js +134 -0
  217. package/src/explorer/archetypes.js +243 -0
  218. package/src/explorer/atoms.js +228 -0
  219. package/src/explorer/charts.js +497 -0
  220. package/src/explorer/components.js +129 -0
  221. package/src/explorer/foundations.js +949 -0
  222. package/src/explorer/icons.js +178 -0
  223. package/src/explorer/patterns.js +247 -0
  224. package/src/explorer/recipes.js +194 -0
  225. package/src/explorer/shared/pattern-examples.js +1337 -0
  226. package/src/explorer/shared/showcase-renderer.js +958 -0
  227. package/src/explorer/shared/spec-table.js +41 -0
  228. package/src/explorer/shared/usage-links.js +87 -0
  229. package/src/explorer/shell-config.js +10 -0
  230. package/src/explorer/shells.js +551 -0
  231. package/src/explorer/styles.js +161 -0
  232. package/src/explorer/tokens.js +262 -0
  233. package/src/explorer/tools.js +525 -0
  234. package/src/form/index.js +804 -0
  235. package/src/i18n/index.js +251 -0
  236. package/src/icons/essential.js +479 -0
  237. package/src/icons/index.js +53 -0
  238. package/src/plugins/index.js +282 -0
  239. package/src/registry/archetypes/content-site.json +71 -0
  240. package/src/registry/archetypes/docs-explorer.json +23 -0
  241. package/src/registry/archetypes/ecommerce.json +104 -0
  242. package/src/registry/archetypes/financial-dashboard.json +77 -0
  243. package/src/registry/archetypes/index.json +41 -0
  244. package/src/registry/archetypes/portfolio.json +82 -0
  245. package/src/registry/archetypes/recipe-community.json +159 -0
  246. package/src/registry/archetypes/saas-dashboard.json +86 -0
  247. package/src/registry/architect/cross-cutting.json +45 -0
  248. package/src/registry/architect/domains/ecommerce.json +294 -0
  249. package/src/registry/architect/domains/financial-services.json +302 -0
  250. package/src/registry/architect/index.json +26 -0
  251. package/src/registry/architect/traits.json +379 -0
  252. package/src/registry/atoms.json +16 -0
  253. package/src/registry/chart-showcase.json +160 -0
  254. package/src/registry/chart.json +136 -0
  255. package/src/registry/components.json +8616 -0
  256. package/src/registry/core.json +216 -0
  257. package/src/registry/css.json +319 -0
  258. package/src/registry/data.json +135 -0
  259. package/src/registry/foundations.json +11 -0
  260. package/src/registry/icons.json +463 -0
  261. package/src/registry/index.json +101 -0
  262. package/src/registry/patterns/activity-feed.json +37 -0
  263. package/src/registry/patterns/article-content.json +27 -0
  264. package/src/registry/patterns/auth-form.json +37 -0
  265. package/src/registry/patterns/author-card.json +20 -0
  266. package/src/registry/patterns/card-grid.json +127 -0
  267. package/src/registry/patterns/category-nav.json +26 -0
  268. package/src/registry/patterns/chart-grid.json +36 -0
  269. package/src/registry/patterns/chat-interface.json +37 -0
  270. package/src/registry/patterns/checklist-card.json +55 -0
  271. package/src/registry/patterns/comparison-panel.json +27 -0
  272. package/src/registry/patterns/component-showcase.json +24 -0
  273. package/src/registry/patterns/contact-form.json +31 -0
  274. package/src/registry/patterns/cta-section.json +20 -0
  275. package/src/registry/patterns/data-table.json +37 -0
  276. package/src/registry/patterns/detail-header.json +83 -0
  277. package/src/registry/patterns/detail-panel.json +27 -0
  278. package/src/registry/patterns/explorer-shell.json +22 -0
  279. package/src/registry/patterns/filter-bar.json +33 -0
  280. package/src/registry/patterns/filter-sidebar.json +27 -0
  281. package/src/registry/patterns/form-sections.json +110 -0
  282. package/src/registry/patterns/goal-tracker.json +27 -0
  283. package/src/registry/patterns/hero.json +107 -0
  284. package/src/registry/patterns/index.json +47 -0
  285. package/src/registry/patterns/kpi-grid.json +36 -0
  286. package/src/registry/patterns/media-gallery.json +20 -0
  287. package/src/registry/patterns/order-history.json +20 -0
  288. package/src/registry/patterns/pagination.json +19 -0
  289. package/src/registry/patterns/photo-to-recipe.json +36 -0
  290. package/src/registry/patterns/pipeline-tracker.json +28 -0
  291. package/src/registry/patterns/post-list.json +27 -0
  292. package/src/registry/patterns/pricing-table.json +32 -0
  293. package/src/registry/patterns/scorecard.json +28 -0
  294. package/src/registry/patterns/search-bar.json +20 -0
  295. package/src/registry/patterns/specimen-grid.json +19 -0
  296. package/src/registry/patterns/stat-card.json +55 -0
  297. package/src/registry/patterns/stats-bar.json +55 -0
  298. package/src/registry/patterns/steps-card.json +55 -0
  299. package/src/registry/patterns/table-of-contents.json +19 -0
  300. package/src/registry/patterns/testimonials.json +21 -0
  301. package/src/registry/patterns/timeline.json +27 -0
  302. package/src/registry/patterns/token-inspector.json +21 -0
  303. package/src/registry/patterns/wizard.json +27 -0
  304. package/src/registry/recipe-auradecantism.json +69 -0
  305. package/src/registry/recipe-clean.json +65 -0
  306. package/src/registry/recipe-command-center.json +78 -0
  307. package/src/registry/router.json +73 -0
  308. package/src/registry/schema/README.md +197 -0
  309. package/src/registry/skeletons.json +259 -0
  310. package/src/registry/state.json +137 -0
  311. package/src/registry/tokens.json +40 -0
  312. package/src/router/hash.js +17 -0
  313. package/src/router/history.js +18 -0
  314. package/src/router/index.js +598 -0
  315. package/src/ssr/index.js +922 -0
  316. package/src/state/arrays.js +181 -0
  317. package/src/state/devtools.js +647 -0
  318. package/src/state/index.js +498 -0
  319. package/src/state/middleware.js +288 -0
  320. package/src/state/scheduler.js +206 -0
  321. package/src/state/store.js +300 -0
  322. package/src/tags/index.js +19 -0
  323. package/src/tannins/auth.js +396 -0
  324. package/src/test/dom.js +352 -0
  325. package/src/test/index.js +62 -0
  326. package/src/test/state.js +306 -0
  327. package/tools/a11y-audit.js +487 -0
  328. package/tools/analyzer.js +315 -0
  329. package/tools/audit.js +706 -0
  330. package/tools/builder.js +1422 -0
  331. package/tools/css-extract.js +188 -0
  332. package/tools/dev-server.js +316 -0
  333. package/tools/dts-gen.js +1260 -0
  334. package/tools/figma-components.js +329 -0
  335. package/tools/figma-patterns.js +516 -0
  336. package/tools/figma-plugin/code.js +453 -0
  337. package/tools/figma-plugin/manifest.json +14 -0
  338. package/tools/figma-plugin/ui.html +268 -0
  339. package/tools/figma-render.js +293 -0
  340. package/tools/figma-tokens.js +712 -0
  341. package/tools/figma-upload.js +318 -0
  342. package/tools/generate.js +738 -0
  343. package/tools/icons.js +133 -0
  344. package/tools/init-templates.js +265 -0
  345. package/tools/install-hooks.sh +5 -0
  346. package/tools/migrations/0.5.0.js +53 -0
  347. package/tools/migrations/0.6.0.js +95 -0
  348. package/tools/minify.js +170 -0
  349. package/tools/pre-commit +4 -0
  350. package/tools/registry.js +662 -0
  351. package/tools/reset-playground.js +61 -0
  352. package/tools/starter-templates/content-site/app.js +49 -0
  353. package/tools/starter-templates/content-site/essence.js +19 -0
  354. package/tools/starter-templates/content-site/pages.js +31 -0
  355. package/tools/starter-templates/ecommerce/app.js +50 -0
  356. package/tools/starter-templates/ecommerce/essence.js +19 -0
  357. package/tools/starter-templates/ecommerce/pages.js +31 -0
  358. package/tools/starter-templates/landing-page/app.js +38 -0
  359. package/tools/starter-templates/landing-page/essence.js +18 -0
  360. package/tools/starter-templates/landing-page/pages.js +21 -0
  361. package/tools/starter-templates/portfolio/app.js +45 -0
  362. package/tools/starter-templates/portfolio/essence.js +19 -0
  363. package/tools/starter-templates/portfolio/pages.js +33 -0
  364. package/tools/starter-templates/saas-dashboard/app.js +70 -0
  365. package/tools/starter-templates/saas-dashboard/essence.js +19 -0
  366. package/tools/starter-templates/saas-dashboard/pages.js +31 -0
  367. package/tools/verify-pack.js +203 -0
  368. package/types/chart.d.ts +77 -0
  369. package/types/components.d.ts +587 -0
  370. package/types/core.d.ts +89 -0
  371. package/types/css.d.ts +149 -0
  372. package/types/data.d.ts +238 -0
  373. package/types/form.d.ts +164 -0
  374. package/types/i18n.d.ts +51 -0
  375. package/types/icons.d.ts +27 -0
  376. package/types/index.d.ts +13 -0
  377. package/types/router.d.ts +116 -0
  378. package/types/ssr.d.ts +102 -0
  379. package/types/state.d.ts +83 -0
  380. package/types/tags.d.ts +62 -0
  381. package/types/tannins.d.ts +63 -0
  382. package/types/test.d.ts +48 -0
@@ -0,0 +1,598 @@
1
+ import { createSignal, createEffect } from '../state/index.js';
2
+ import { getAnimations } from '../css/theme-registry.js';
3
+ import { h } from '../core/index.js';
4
+ import { hashStrategy } from './hash.js';
5
+ import { historyStrategy } from './history.js';
6
+
7
+ /** @type {ReturnType<typeof createRouter>|null} */
8
+ let activeRouter = null;
9
+
10
+ /** @type {RegExp} */
11
+ const UNSAFE_URL = /^(javascript|data):|^https?:\/\//i;
12
+
13
+ /**
14
+ * Validate a navigation path. Only relative paths starting with '/' are allowed.
15
+ * @param {string} path
16
+ * @returns {string} validated path
17
+ */
18
+ function validatePath(path) {
19
+ if (typeof path !== 'string' || UNSAFE_URL.test(path.trim())) {
20
+ throw new Error(`Invalid route path: ${path}`);
21
+ }
22
+ if (path[0] !== '/') throw new Error(`Route path must start with /: ${path}`);
23
+ return path;
24
+ }
25
+
26
+ /**
27
+ * Parse query string from a full path.
28
+ * @param {string} fullPath
29
+ * @returns {{ pathname: string, search: string, query: Object<string, string> }}
30
+ */
31
+ function parsePath(fullPath) {
32
+ const qIdx = fullPath.indexOf('?');
33
+ if (qIdx === -1) return { pathname: fullPath, search: '', query: {} };
34
+ const pathname = fullPath.slice(0, qIdx);
35
+ const search = fullPath.slice(qIdx);
36
+ const query = {};
37
+ const sp = new URLSearchParams(search);
38
+ sp.forEach((v, k) => { query[k] = v; });
39
+ return { pathname, search, query };
40
+ }
41
+
42
+ /**
43
+ * Compile a single route path segment into a regex + param keys.
44
+ * @param {string} path
45
+ * @returns {{ regex: RegExp, keys: string[] }}
46
+ */
47
+ function compilePath(path) {
48
+ const keys = [];
49
+ const pattern = path
50
+ .replace(/:([^/]+)/g, (_, key) => { keys.push(key); return '([^/]+)'; })
51
+ .replace(/\*/g, '(.*)');
52
+ return { regex: new RegExp(`^${pattern}$`), keys };
53
+ }
54
+
55
+ /**
56
+ * Flatten nested route tree into a list of compiled entries.
57
+ * Each entry has: fullPath, regex, keys, components[], name?, meta
58
+ * components[] is the chain from root layout to leaf component.
59
+ * @param {Array} routes
60
+ * @param {string} parentPath
61
+ * @param {Array} parentComponents
62
+ * @param {Object} parentMeta
63
+ * @returns {Array<{ fullPath: string, regex: RegExp, keys: string[], components: Function[], name?: string, meta: Object }>}
64
+ */
65
+ function flattenRoutes(routes, parentPath = '', parentComponents = [], parentMeta = {}) {
66
+ /** @type {Array} */
67
+ const result = [];
68
+ for (const r of routes) {
69
+ const seg = r.path === '*' ? '*' : r.path;
70
+ const full = seg === '*' ? parentPath + '/*' :
71
+ parentPath + (seg.startsWith('/') ? seg : (seg ? '/' + seg : ''));
72
+ const normalized = full.replace(/\/+/g, '/') || '/';
73
+ const chain = r.component ? [...parentComponents, r.component] : [...parentComponents];
74
+ const mergedMeta = { ...parentMeta, ...(r.meta || {}) };
75
+
76
+ if (r.children && r.children.length) {
77
+ result.push(...flattenRoutes(r.children, normalized, chain, mergedMeta));
78
+ } else {
79
+ const { regex, keys } = compilePath(normalized);
80
+ result.push({ fullPath: normalized, regex, keys, components: chain, name: r.name, meta: mergedMeta });
81
+ }
82
+ }
83
+ return result;
84
+ }
85
+
86
+ /**
87
+ * Build name-to-path lookup from route config.
88
+ * @param {Array} routes
89
+ * @param {string} parentPath
90
+ * @returns {Map<string, string>}
91
+ */
92
+ function buildNameMap(routes, parentPath = '') {
93
+ const map = new Map();
94
+ for (const r of routes) {
95
+ const seg = r.path === '*' ? '*' : r.path;
96
+ const full = seg === '*' ? parentPath + '/*' :
97
+ parentPath + (seg.startsWith('/') ? seg : (seg ? '/' + seg : ''));
98
+ const normalized = full.replace(/\/+/g, '/') || '/';
99
+ if (r.name) map.set(r.name, normalized);
100
+ if (r.children) {
101
+ for (const [k, v] of buildNameMap(r.children, normalized)) map.set(k, v);
102
+ }
103
+ }
104
+ return map;
105
+ }
106
+
107
+ /**
108
+ * Resolve a named route to a path string.
109
+ * @param {Map<string, string>} nameMap
110
+ * @param {{ name: string, params?: Object }} to
111
+ * @returns {string}
112
+ */
113
+ function resolveNamedRoute(nameMap, to) {
114
+ const pattern = nameMap.get(to.name);
115
+ if (!pattern) throw new Error(`Unknown route name: ${to.name}`);
116
+ let path = pattern;
117
+ if (to.params) {
118
+ for (const [k, v] of Object.entries(to.params)) {
119
+ path = path.replace(`:${k}`, encodeURIComponent(String(v)));
120
+ }
121
+ }
122
+ return path;
123
+ }
124
+
125
+ /** @type {Map<Function, Function>} Cache for lazy-loaded components */
126
+ const lazyCache = new Map();
127
+
128
+ /**
129
+ * Resolve a component — handles lazy (async) components with caching.
130
+ * @param {Function} component
131
+ * @returns {Promise<Function>|Function}
132
+ */
133
+ function resolveComponent(component) {
134
+ if (lazyCache.has(component)) return lazyCache.get(component);
135
+ let result;
136
+ try {
137
+ result = component.__isLazy ? component() : component;
138
+ } catch (_) {
139
+ return component;
140
+ }
141
+ if (result && typeof result.then === 'function') {
142
+ const promise = result.then(resolved => {
143
+ const comp = typeof resolved === 'function' ? resolved :
144
+ (resolved && resolved.default ? resolved.default : resolved);
145
+ lazyCache.set(component, comp);
146
+ return comp;
147
+ });
148
+ return promise;
149
+ }
150
+ return result;
151
+ }
152
+
153
+ /**
154
+ * Detect if a component is lazy (returns a Promise when called with no args).
155
+ * We mark it during route compilation to avoid false positives.
156
+ * @param {Function} fn
157
+ * @returns {boolean}
158
+ */
159
+ function isLazyComponent(fn) {
160
+ if (!fn || typeof fn !== 'function') return false;
161
+ // Heuristic: function body contains import() or returns Promise
162
+ // More reliable: try calling it and check for .then
163
+ // We use a safe probe — only if the function has 0 declared params
164
+ if (fn.length > 0) return false;
165
+ try {
166
+ const r = fn();
167
+ if (r && typeof r.then === 'function') {
168
+ // It's async — but we need to not lose this promise.
169
+ // Store it immediately in the cache as a pending promise
170
+ const promise = r.then(resolved => {
171
+ const comp = typeof resolved === 'function' ? resolved :
172
+ (resolved && resolved.default ? resolved.default : resolved);
173
+ lazyCache.set(fn, comp);
174
+ return comp;
175
+ });
176
+ lazyCache.set(fn, promise);
177
+ fn.__isLazy = true;
178
+ return true;
179
+ }
180
+ } catch (_) {
181
+ // Not lazy — just a regular component
182
+ }
183
+ return false;
184
+ }
185
+
186
+ /**
187
+ * @typedef {{
188
+ * mode?: 'hash'|'history',
189
+ * base?: string,
190
+ * routes: Array<{path: string, component?: Function, children?: Array, name?: string, meta?: Object}>,
191
+ * transitions?: boolean,
192
+ * scrollBehavior?: 'top'|'restore'|false,
193
+ * beforeEach?: (to: Object, from: Object) => undefined|false|string,
194
+ * afterEach?: (to: Object, from: Object) => void
195
+ * }} RouterConfig
196
+ */
197
+
198
+ /**
199
+ * Create a router instance.
200
+ * @param {RouterConfig} config
201
+ * @returns {{ navigate: Function, outlet: Function, current: Function, path: Function, destroy: Function, onNavigate: Function }}
202
+ */
203
+ export function createRouter(config) {
204
+ const strategy = config.mode === 'history' ? historyStrategy : hashStrategy;
205
+ const base = (config.base || '').replace(/\/+$/, '');
206
+ const useTransitions = !!config.transitions;
207
+ const scrollBehavior = config.scrollBehavior !== undefined ? config.scrollBehavior : 'top';
208
+ const beforeEach = config.beforeEach || null;
209
+ const afterEach = config.afterEach || null;
210
+
211
+ /** @type {Array<(to: Object, from: Object) => void>} */
212
+ const listeners = [];
213
+
214
+ // Compile routes
215
+ const compiled = flattenRoutes(config.routes);
216
+ const nameMap = buildNameMap(config.routes);
217
+
218
+ // Probe for lazy components during init
219
+ for (const entry of compiled) {
220
+ for (let i = 0; i < entry.components.length; i++) {
221
+ isLazyComponent(entry.components[i]);
222
+ }
223
+ }
224
+
225
+ /**
226
+ * Strip base path prefix from a full path.
227
+ * @param {string} path
228
+ * @returns {string}
229
+ */
230
+ function stripBase(path) {
231
+ if (!base) return path;
232
+ if (path.startsWith(base)) return path.slice(base.length) || '/';
233
+ return path;
234
+ }
235
+
236
+ const [navigating, setNavigating] = createSignal(false);
237
+
238
+ /** @type {Map<string, number>} scroll position cache */
239
+ const scrollPositions = new Map();
240
+
241
+ const initialFull = stripBase(strategy.current());
242
+ const { pathname: initPath, query: initQuery } = parsePath(initialFull);
243
+
244
+ // Start with empty components — handleNavigation (async) will resolve lazy components
245
+ // and set the real route. This prevents the outlet from rendering unresolved components.
246
+ const [route, setRoute] = createSignal({ path: initPath, params: {}, query: initQuery, component: null, components: [], matched: false, meta: {} });
247
+
248
+ /**
249
+ * Match a pathname against compiled routes.
250
+ * @param {string} pathname
251
+ * @param {Object} query
252
+ * @returns {{ path: string, params: Object, query: Object, component: Function|null, components: Function[], matched: boolean, name?: string }}
253
+ */
254
+ function matchRoute(pathname, query) {
255
+ for (const entry of compiled) {
256
+ const match = pathname.match(entry.regex);
257
+ if (match) {
258
+ const params = {};
259
+ entry.keys.forEach((key, i) => { params[key] = decodeURIComponent(match[i + 1]); });
260
+ // Leaf component is last in chain
261
+ const component = entry.components[entry.components.length - 1] || null;
262
+ return {
263
+ path: pathname, params, query, component,
264
+ components: entry.components, matched: true, name: entry.name, meta: entry.meta || {}
265
+ };
266
+ }
267
+ }
268
+ return { path: pathname, params: {}, query, component: null, components: [], matched: false, meta: {} };
269
+ }
270
+
271
+ /**
272
+ * Core navigation handler. Applies guards, updates signal, manages scroll.
273
+ * @param {string} newPath
274
+ * @param {{ replace?: boolean, skipGuards?: boolean }} [opts]
275
+ */
276
+ async function handleNavigation(newPath, opts = {}) {
277
+ const { pathname, query } = parsePath(newPath);
278
+ const to = matchRoute(pathname, query);
279
+ const from = route();
280
+
281
+ // Before guard
282
+ if (!opts.skipGuards && beforeEach) {
283
+ const result = beforeEach(to, from);
284
+ if (result === false) return;
285
+ if (typeof result === 'string') {
286
+ // Redirect — validate and navigate
287
+ validatePath(result);
288
+ const redirectPath = base + result;
289
+ if (opts.replace) {
290
+ strategy.replace(redirectPath);
291
+ } else {
292
+ strategy.push(redirectPath);
293
+ }
294
+ return;
295
+ }
296
+ }
297
+
298
+ // Save scroll position for current path
299
+ if (scrollBehavior === 'restore' && from.path) {
300
+ scrollPositions.set(from.path, window.scrollY || 0);
301
+ }
302
+
303
+ // Resolve lazy components
304
+ setNavigating(true);
305
+ try {
306
+ const resolvedComponents = [];
307
+ for (const comp of to.components) {
308
+ const resolved = resolveComponent(comp);
309
+ if (resolved && typeof resolved.then === 'function') {
310
+ resolvedComponents.push(await resolved);
311
+ } else {
312
+ resolvedComponents.push(resolved);
313
+ }
314
+ }
315
+ to.components = resolvedComponents;
316
+ to.component = resolvedComponents[resolvedComponents.length - 1] || null;
317
+ } finally {
318
+ setNavigating(false);
319
+ }
320
+
321
+ setRoute(to);
322
+
323
+ // After guard
324
+ if (afterEach) afterEach(to, from);
325
+
326
+ // Fire navigation listeners
327
+ for (let i = 0; i < listeners.length; i++) listeners[i](to, from);
328
+
329
+ // Scroll handling
330
+ if (scrollBehavior === 'top') {
331
+ if (typeof window !== 'undefined' && window.scrollTo) window.scrollTo(0, 0);
332
+ } else if (scrollBehavior === 'restore') {
333
+ const saved = scrollPositions.get(to.path);
334
+ if (typeof window !== 'undefined' && window.scrollTo) {
335
+ window.scrollTo(0, saved || 0);
336
+ }
337
+ }
338
+ }
339
+
340
+ // Listen to strategy events (back/forward buttons)
341
+ const unlisten = strategy.listen(fullPath => {
342
+ handleNavigation(stripBase(fullPath), { skipGuards: false });
343
+ });
344
+
345
+ /**
346
+ * Programmatic navigation.
347
+ * @param {string|{ name: string, params?: Object }} to
348
+ * @param {{ replace?: boolean }} [opts]
349
+ */
350
+ function nav(to, opts = {}) {
351
+ let path;
352
+ if (typeof to === 'object' && to.name) {
353
+ path = resolveNamedRoute(nameMap, to);
354
+ } else {
355
+ path = /** @type {string} */ (to);
356
+ }
357
+ validatePath(path);
358
+ const fullPath = base + path;
359
+ if (opts.replace) {
360
+ strategy.replace(fullPath);
361
+ } else {
362
+ strategy.push(fullPath);
363
+ }
364
+ // Strategy listener triggers handleNavigation
365
+ }
366
+
367
+ function back() { window.history.back(); }
368
+ function forward() { window.history.forward(); }
369
+
370
+ /**
371
+ * Create an outlet that renders the matched route's component chain.
372
+ * Supports nested layouts via recursive outlet injection.
373
+ * @param {number} [depth=0] — nesting depth (0 = root outlet)
374
+ * @returns {HTMLElement}
375
+ */
376
+ function outlet(depth = 0) {
377
+ const container = document.createElement('d-route');
378
+ let currentNode = null;
379
+ let currentComp = null;
380
+
381
+ createEffect(() => {
382
+ const r = route();
383
+ const components = r.components || [];
384
+ const comp = components[depth];
385
+ const hasChild = depth + 1 < components.length;
386
+
387
+ // Layout components: if the same component is still at this depth,
388
+ // skip re-render — the child outlet handles its own updates
389
+ if (hasChild && comp === currentComp && currentNode) return;
390
+
391
+ const swap = () => {
392
+ if (currentNode) {
393
+ container.removeChild(currentNode);
394
+ currentNode = null;
395
+ }
396
+ currentComp = comp;
397
+ if (!comp) return;
398
+
399
+ // If there's a deeper component, this is a layout — pass child outlet
400
+ let node;
401
+ if (hasChild) {
402
+ node = comp({ ...r.params, outlet: () => outlet(depth + 1) });
403
+ } else {
404
+ node = comp(r.params);
405
+ }
406
+ if (node) {
407
+ currentNode = node;
408
+ container.appendChild(currentNode);
409
+ }
410
+ };
411
+
412
+ if (useTransitions && typeof document !== 'undefined' &&
413
+ document.startViewTransition && getAnimations()()) {
414
+ document.startViewTransition(swap);
415
+ } else {
416
+ swap();
417
+ }
418
+ });
419
+
420
+ return container;
421
+ }
422
+
423
+ /**
424
+ * Subscribe to navigation events. Returns an unsubscribe function.
425
+ * Fires after route change and afterEach guard.
426
+ * @param {(to: Object, from: Object) => void} callback
427
+ * @returns {() => void}
428
+ */
429
+ function onNavigate(callback) {
430
+ listeners.push(callback);
431
+ return () => {
432
+ const idx = listeners.indexOf(callback);
433
+ if (idx !== -1) listeners.splice(idx, 1);
434
+ };
435
+ }
436
+
437
+ function destroy() {
438
+ unlisten();
439
+ if (activeRouter === router) activeRouter = null;
440
+ }
441
+
442
+ const router = { navigate: nav, outlet, current: route, path: () => route().path, destroy, onNavigate, nameMap, back, forward, isNavigating: navigating, _base: base };
443
+ activeRouter = router;
444
+
445
+ // Run initial navigation to apply guards on first load
446
+ handleNavigation(initialFull, { skipGuards: false });
447
+
448
+ return router;
449
+ }
450
+
451
+ /**
452
+ * Navigate programmatically. Delegates to active router.
453
+ * @param {string|{ name: string, params?: Object }} to
454
+ * @param {{ replace?: boolean }} [opts]
455
+ */
456
+ export function navigate(to, opts) {
457
+ if (!activeRouter) throw new Error('No active router. Call createRouter() first.');
458
+ activeRouter.navigate(to, opts);
459
+ }
460
+
461
+ /**
462
+ * Create a router-aware anchor element with active link detection.
463
+ * @param {{ href: string, activeClass?: string, exact?: boolean, class?: string }} props
464
+ * @param {...any} children
465
+ * @returns {HTMLAnchorElement}
466
+ */
467
+ export function link(props, ...children) {
468
+ const { href, activeClass, exact, ...rest } = props;
469
+ validatePath(href);
470
+
471
+ const cls = activeClass || 'd-link-active';
472
+
473
+ const basePrefix = activeRouter ? activeRouter._base : '';
474
+ const el = h('a', {
475
+ ...rest,
476
+ href: basePrefix + href,
477
+ onclick(e) {
478
+ e.preventDefault();
479
+ navigate(href);
480
+ }
481
+ }, ...children);
482
+
483
+ // Reactive active class
484
+ if (activeRouter) {
485
+ createEffect(() => {
486
+ const r = activeRouter.current();
487
+ const currentPath = r.path;
488
+ const isActive = exact ? currentPath === href :
489
+ (currentPath === href || currentPath.startsWith(href === '/' ? '/__never__' : href + '/'));
490
+ // Special case: '/' only matches exactly unless !exact and href is not '/'
491
+ const active = href === '/' ? currentPath === '/' : isActive;
492
+ if (active) {
493
+ el.classList.add(cls);
494
+ } else {
495
+ el.classList.remove(cls);
496
+ }
497
+ });
498
+ }
499
+
500
+ return el;
501
+ }
502
+
503
+ /**
504
+ * Get current route signal.
505
+ * The returned signal includes a `meta` field — an object merged from parent
506
+ * routes down to the matched leaf. Parent meta is applied first, child meta
507
+ * overrides. Example: `{ path: '/admin', meta: { requiresAuth: true, breadcrumb: 'Admin' }, component: AdminPage }`
508
+ * @returns {() => { path: string, params: Object, query: Object, component: Function|null, components: Function[], matched: boolean, name?: string, meta: Object }}
509
+ */
510
+ export function useRoute() {
511
+ if (!activeRouter) throw new Error('No active router. Call createRouter() first.');
512
+ return activeRouter.current;
513
+ }
514
+
515
+ /**
516
+ * Reactive search params. Returns [getter, setter] tuple.
517
+ * Getter returns URLSearchParams from current URL query string.
518
+ * Setter updates query params without triggering full navigation.
519
+ * @returns {[() => URLSearchParams, (params: Object|URLSearchParams) => void]}
520
+ */
521
+ export function useSearchParams() {
522
+ if (!activeRouter) throw new Error('No active router. Call createRouter() first.');
523
+
524
+ const [params, setParams] = createSignal(new URLSearchParams(window.location.search || window.location.hash.split('?')[1] || ''));
525
+
526
+ // Track route changes to update search params
527
+ createEffect(() => {
528
+ const r = activeRouter.current();
529
+ const sp = new URLSearchParams();
530
+ if (r.query) {
531
+ for (const [k, v] of Object.entries(r.query)) sp.set(k, v);
532
+ }
533
+ setParams(sp);
534
+ });
535
+
536
+ /**
537
+ * Update search params in URL without navigation.
538
+ * @param {Object|URLSearchParams} newParams
539
+ */
540
+ function setter(newParams) {
541
+ const sp = newParams instanceof URLSearchParams ? newParams : new URLSearchParams();
542
+ if (!(newParams instanceof URLSearchParams)) {
543
+ for (const [k, v] of Object.entries(newParams)) sp.set(k, String(v));
544
+ }
545
+ const qs = sp.toString();
546
+ const r = activeRouter.current();
547
+ const basePath = activeRouter._base || '';
548
+ const newPath = basePath + r.path + (qs ? '?' + qs : '');
549
+
550
+ // Replace (not push) — query changes shouldn't create history entries
551
+ if (window.location.hash) {
552
+ // Hash mode
553
+ const url = window.location.pathname + window.location.search + '#' + newPath;
554
+ window.history.replaceState(null, '', url);
555
+ } else {
556
+ // History mode
557
+ window.history.replaceState(null, '', newPath);
558
+ }
559
+ setParams(sp);
560
+ }
561
+
562
+ return [params, setter];
563
+ }
564
+
565
+ /**
566
+ * Subscribe to navigation events on the active router. Returns an unsubscribe function.
567
+ * @param {(to: Object, from: Object) => void} callback
568
+ * @returns {() => void}
569
+ */
570
+ export function onNavigate(callback) {
571
+ if (!activeRouter) throw new Error('No active router. Call createRouter() first.');
572
+ return activeRouter.onNavigate(callback);
573
+ }
574
+
575
+ /**
576
+ * Navigate back in history. Delegates to active router.
577
+ */
578
+ export function back() {
579
+ if (!activeRouter) throw new Error('No active router. Call createRouter() first.');
580
+ activeRouter.back();
581
+ }
582
+
583
+ /**
584
+ * Navigate forward in history. Delegates to active router.
585
+ */
586
+ export function forward() {
587
+ if (!activeRouter) throw new Error('No active router. Call createRouter() first.');
588
+ activeRouter.forward();
589
+ }
590
+
591
+ /**
592
+ * Reactive boolean signal — true while lazy routes are resolving.
593
+ * @returns {boolean}
594
+ */
595
+ export function isNavigating() {
596
+ if (!activeRouter) throw new Error('No active router. Call createRouter() first.');
597
+ return activeRouter.isNavigating();
598
+ }