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.
- package/AGENTS.md +868 -0
- package/CHANGELOG.md +255 -0
- package/CLAUDE.md +178 -0
- package/LICENSE +21 -0
- package/README.md +229 -0
- package/cli/art.js +127 -0
- package/cli/commands/a11y.js +61 -0
- package/cli/commands/audit.js +225 -0
- package/cli/commands/build.js +38 -0
- package/cli/commands/dev.js +18 -0
- package/cli/commands/doctor.js +197 -0
- package/cli/commands/figma-sync.js +48 -0
- package/cli/commands/figma-tokens.js +55 -0
- package/cli/commands/generate.js +26 -0
- package/cli/commands/init.js +116 -0
- package/cli/commands/lint.js +209 -0
- package/cli/commands/mcp.js +530 -0
- package/cli/commands/migrate.js +175 -0
- package/cli/commands/test.js +38 -0
- package/cli/commands/validate.js +354 -0
- package/cli/index.js +113 -0
- package/package.json +95 -0
- package/reference/atoms.md +517 -0
- package/reference/behaviors.md +384 -0
- package/reference/build-tooling.md +275 -0
- package/reference/color-guidelines.md +965 -0
- package/reference/component-lifecycle.md +137 -0
- package/reference/compound-spacing.md +95 -0
- package/reference/decantation-process.md +499 -0
- package/reference/dev-server-routes.md +93 -0
- package/reference/form-system.md +253 -0
- package/reference/i18n.md +336 -0
- package/reference/icons.md +576 -0
- package/reference/llm-primer.md +953 -0
- package/reference/plugins.md +252 -0
- package/reference/registry-consumption.md +76 -0
- package/reference/router.md +217 -0
- package/reference/shells.md +116 -0
- package/reference/spatial-guidelines.md +541 -0
- package/reference/ssr.md +234 -0
- package/reference/state-data.md +215 -0
- package/reference/state-patterns.md +166 -0
- package/reference/state.md +194 -0
- package/reference/style-system.md +110 -0
- package/reference/tokens.md +460 -0
- package/src/app.js +19 -0
- package/src/chart/_animate.js +266 -0
- package/src/chart/_base.js +109 -0
- package/src/chart/_data.js +209 -0
- package/src/chart/_format.js +106 -0
- package/src/chart/_interact.js +364 -0
- package/src/chart/_palette.js +105 -0
- package/src/chart/_renderer.js +52 -0
- package/src/chart/_scene.js +262 -0
- package/src/chart/_shared.js +371 -0
- package/src/chart/index.js +637 -0
- package/src/chart/layouts/_layout-base.js +328 -0
- package/src/chart/layouts/cartesian.js +148 -0
- package/src/chart/layouts/hierarchy.js +562 -0
- package/src/chart/layouts/polar.js +101 -0
- package/src/chart/renderers/canvas.js +179 -0
- package/src/chart/renderers/svg.js +256 -0
- package/src/chart/renderers/webgpu.js +715 -0
- package/src/chart/types/_type-base.js +26 -0
- package/src/chart/types/area.js +134 -0
- package/src/chart/types/bar.js +173 -0
- package/src/chart/types/box-plot.js +125 -0
- package/src/chart/types/bubble.js +63 -0
- package/src/chart/types/candlestick.js +115 -0
- package/src/chart/types/chord.js +85 -0
- package/src/chart/types/combination.js +108 -0
- package/src/chart/types/funnel.js +68 -0
- package/src/chart/types/gauge.js +163 -0
- package/src/chart/types/heatmap.js +98 -0
- package/src/chart/types/histogram.js +71 -0
- package/src/chart/types/line.js +111 -0
- package/src/chart/types/org-chart.js +93 -0
- package/src/chart/types/pie.js +81 -0
- package/src/chart/types/radar.js +96 -0
- package/src/chart/types/radial.js +68 -0
- package/src/chart/types/range-area.js +55 -0
- package/src/chart/types/range-bar.js +61 -0
- package/src/chart/types/sankey.js +73 -0
- package/src/chart/types/scatter.js +66 -0
- package/src/chart/types/sparkline.js +81 -0
- package/src/chart/types/sunburst.js +69 -0
- package/src/chart/types/swimlane.js +88 -0
- package/src/chart/types/treemap.js +62 -0
- package/src/chart/types/waterfall.js +100 -0
- package/src/components/_base.js +1658 -0
- package/src/components/_behaviors.js +1140 -0
- package/src/components/_primitives.js +534 -0
- package/src/components/_qr-encoder.js +539 -0
- package/src/components/accordion.js +207 -0
- package/src/components/affix.js +62 -0
- package/src/components/alert-dialog.js +75 -0
- package/src/components/alert.js +47 -0
- package/src/components/aspect-ratio.js +24 -0
- package/src/components/avatar-group.js +55 -0
- package/src/components/avatar.js +38 -0
- package/src/components/back-top.js +75 -0
- package/src/components/badge.js +74 -0
- package/src/components/banner.js +68 -0
- package/src/components/breadcrumb.js +162 -0
- package/src/components/button.js +115 -0
- package/src/components/calendar.js +131 -0
- package/src/components/card.js +192 -0
- package/src/components/carousel.js +98 -0
- package/src/components/cascader.js +261 -0
- package/src/components/checkbox.js +80 -0
- package/src/components/chip.js +81 -0
- package/src/components/code-block.js +82 -0
- package/src/components/collapsible.js +50 -0
- package/src/components/color-palette.js +438 -0
- package/src/components/color-picker.js +314 -0
- package/src/components/combobox.js +181 -0
- package/src/components/command.js +174 -0
- package/src/components/comment.js +206 -0
- package/src/components/context-menu.js +76 -0
- package/src/components/data-table.js +724 -0
- package/src/components/date-picker.js +217 -0
- package/src/components/date-range-picker.js +244 -0
- package/src/components/datetime-picker.js +271 -0
- package/src/components/descriptions.js +68 -0
- package/src/components/drawer.js +179 -0
- package/src/components/dropdown.js +88 -0
- package/src/components/empty.js +41 -0
- package/src/components/float-button.js +90 -0
- package/src/components/form.js +106 -0
- package/src/components/hover-card.js +49 -0
- package/src/components/icon.js +87 -0
- package/src/components/image.js +97 -0
- package/src/components/index.js +117 -0
- package/src/components/input-group.js +75 -0
- package/src/components/input-number.js +155 -0
- package/src/components/input-otp.js +178 -0
- package/src/components/input.js +91 -0
- package/src/components/kbd.js +36 -0
- package/src/components/label.js +25 -0
- package/src/components/list.js +118 -0
- package/src/components/masked-input.js +236 -0
- package/src/components/mentions.js +165 -0
- package/src/components/menu.js +259 -0
- package/src/components/message.js +80 -0
- package/src/components/modal.js +147 -0
- package/src/components/navigation-menu.js +166 -0
- package/src/components/notification.js +84 -0
- package/src/components/pagination.js +104 -0
- package/src/components/placeholder.js +132 -0
- package/src/components/popconfirm.js +70 -0
- package/src/components/popover.js +58 -0
- package/src/components/progress.js +61 -0
- package/src/components/qrcode.js +251 -0
- package/src/components/radiogroup.js +120 -0
- package/src/components/range-slider.js +176 -0
- package/src/components/rate.js +186 -0
- package/src/components/resizable.js +83 -0
- package/src/components/result.js +57 -0
- package/src/components/scroll-area.js +43 -0
- package/src/components/segmented.js +97 -0
- package/src/components/select.js +165 -0
- package/src/components/separator.js +31 -0
- package/src/components/shell.js +407 -0
- package/src/components/skeleton.js +39 -0
- package/src/components/slider.js +141 -0
- package/src/components/sortable-list.js +176 -0
- package/src/components/space.js +42 -0
- package/src/components/spinner.js +112 -0
- package/src/components/splitter.js +147 -0
- package/src/components/statistic.js +136 -0
- package/src/components/steps.js +99 -0
- package/src/components/switch.js +95 -0
- package/src/components/table.js +44 -0
- package/src/components/tabs.js +216 -0
- package/src/components/tag.js +115 -0
- package/src/components/textarea.js +82 -0
- package/src/components/time-picker.js +153 -0
- package/src/components/time-range-picker.js +170 -0
- package/src/components/timeline.js +226 -0
- package/src/components/toast.js +71 -0
- package/src/components/toggle.js +213 -0
- package/src/components/tooltip.js +57 -0
- package/src/components/tour.js +159 -0
- package/src/components/transfer.js +163 -0
- package/src/components/tree-select.js +274 -0
- package/src/components/tree.js +141 -0
- package/src/components/typography.js +136 -0
- package/src/components/upload.js +118 -0
- package/src/components/visually-hidden.js +20 -0
- package/src/components/watermark.js +124 -0
- package/src/core/index.js +539 -0
- package/src/core/lifecycle.js +69 -0
- package/src/css/atoms.js +651 -0
- package/src/css/components.js +940 -0
- package/src/css/derive.js +1296 -0
- package/src/css/index.js +265 -0
- package/src/css/runtime.js +268 -0
- package/src/css/styles/addons/bioluminescent.js +93 -0
- package/src/css/styles/addons/clay.js +70 -0
- package/src/css/styles/addons/clean.js +57 -0
- package/src/css/styles/addons/command-center.js +143 -0
- package/src/css/styles/addons/dopamine.js +83 -0
- package/src/css/styles/addons/editorial.js +80 -0
- package/src/css/styles/addons/glassmorphism.js +99 -0
- package/src/css/styles/addons/liquid-glass.js +105 -0
- package/src/css/styles/addons/prismatic.js +100 -0
- package/src/css/styles/addons/retro.js +63 -0
- package/src/css/styles/auradecantism.js +96 -0
- package/src/css/theme-registry.js +444 -0
- package/src/data/entity.js +281 -0
- package/src/data/index.js +13 -0
- package/src/data/persist.js +225 -0
- package/src/data/query.js +839 -0
- package/src/data/realtime.js +299 -0
- package/src/data/url.js +177 -0
- package/src/data/worker.js +134 -0
- package/src/explorer/archetypes.js +243 -0
- package/src/explorer/atoms.js +228 -0
- package/src/explorer/charts.js +497 -0
- package/src/explorer/components.js +129 -0
- package/src/explorer/foundations.js +949 -0
- package/src/explorer/icons.js +178 -0
- package/src/explorer/patterns.js +247 -0
- package/src/explorer/recipes.js +194 -0
- package/src/explorer/shared/pattern-examples.js +1337 -0
- package/src/explorer/shared/showcase-renderer.js +958 -0
- package/src/explorer/shared/spec-table.js +41 -0
- package/src/explorer/shared/usage-links.js +87 -0
- package/src/explorer/shell-config.js +10 -0
- package/src/explorer/shells.js +551 -0
- package/src/explorer/styles.js +161 -0
- package/src/explorer/tokens.js +262 -0
- package/src/explorer/tools.js +525 -0
- package/src/form/index.js +804 -0
- package/src/i18n/index.js +251 -0
- package/src/icons/essential.js +479 -0
- package/src/icons/index.js +53 -0
- package/src/plugins/index.js +282 -0
- package/src/registry/archetypes/content-site.json +71 -0
- package/src/registry/archetypes/docs-explorer.json +23 -0
- package/src/registry/archetypes/ecommerce.json +104 -0
- package/src/registry/archetypes/financial-dashboard.json +77 -0
- package/src/registry/archetypes/index.json +41 -0
- package/src/registry/archetypes/portfolio.json +82 -0
- package/src/registry/archetypes/recipe-community.json +159 -0
- package/src/registry/archetypes/saas-dashboard.json +86 -0
- package/src/registry/architect/cross-cutting.json +45 -0
- package/src/registry/architect/domains/ecommerce.json +294 -0
- package/src/registry/architect/domains/financial-services.json +302 -0
- package/src/registry/architect/index.json +26 -0
- package/src/registry/architect/traits.json +379 -0
- package/src/registry/atoms.json +16 -0
- package/src/registry/chart-showcase.json +160 -0
- package/src/registry/chart.json +136 -0
- package/src/registry/components.json +8616 -0
- package/src/registry/core.json +216 -0
- package/src/registry/css.json +319 -0
- package/src/registry/data.json +135 -0
- package/src/registry/foundations.json +11 -0
- package/src/registry/icons.json +463 -0
- package/src/registry/index.json +101 -0
- package/src/registry/patterns/activity-feed.json +37 -0
- package/src/registry/patterns/article-content.json +27 -0
- package/src/registry/patterns/auth-form.json +37 -0
- package/src/registry/patterns/author-card.json +20 -0
- package/src/registry/patterns/card-grid.json +127 -0
- package/src/registry/patterns/category-nav.json +26 -0
- package/src/registry/patterns/chart-grid.json +36 -0
- package/src/registry/patterns/chat-interface.json +37 -0
- package/src/registry/patterns/checklist-card.json +55 -0
- package/src/registry/patterns/comparison-panel.json +27 -0
- package/src/registry/patterns/component-showcase.json +24 -0
- package/src/registry/patterns/contact-form.json +31 -0
- package/src/registry/patterns/cta-section.json +20 -0
- package/src/registry/patterns/data-table.json +37 -0
- package/src/registry/patterns/detail-header.json +83 -0
- package/src/registry/patterns/detail-panel.json +27 -0
- package/src/registry/patterns/explorer-shell.json +22 -0
- package/src/registry/patterns/filter-bar.json +33 -0
- package/src/registry/patterns/filter-sidebar.json +27 -0
- package/src/registry/patterns/form-sections.json +110 -0
- package/src/registry/patterns/goal-tracker.json +27 -0
- package/src/registry/patterns/hero.json +107 -0
- package/src/registry/patterns/index.json +47 -0
- package/src/registry/patterns/kpi-grid.json +36 -0
- package/src/registry/patterns/media-gallery.json +20 -0
- package/src/registry/patterns/order-history.json +20 -0
- package/src/registry/patterns/pagination.json +19 -0
- package/src/registry/patterns/photo-to-recipe.json +36 -0
- package/src/registry/patterns/pipeline-tracker.json +28 -0
- package/src/registry/patterns/post-list.json +27 -0
- package/src/registry/patterns/pricing-table.json +32 -0
- package/src/registry/patterns/scorecard.json +28 -0
- package/src/registry/patterns/search-bar.json +20 -0
- package/src/registry/patterns/specimen-grid.json +19 -0
- package/src/registry/patterns/stat-card.json +55 -0
- package/src/registry/patterns/stats-bar.json +55 -0
- package/src/registry/patterns/steps-card.json +55 -0
- package/src/registry/patterns/table-of-contents.json +19 -0
- package/src/registry/patterns/testimonials.json +21 -0
- package/src/registry/patterns/timeline.json +27 -0
- package/src/registry/patterns/token-inspector.json +21 -0
- package/src/registry/patterns/wizard.json +27 -0
- package/src/registry/recipe-auradecantism.json +69 -0
- package/src/registry/recipe-clean.json +65 -0
- package/src/registry/recipe-command-center.json +78 -0
- package/src/registry/router.json +73 -0
- package/src/registry/schema/README.md +197 -0
- package/src/registry/skeletons.json +259 -0
- package/src/registry/state.json +137 -0
- package/src/registry/tokens.json +40 -0
- package/src/router/hash.js +17 -0
- package/src/router/history.js +18 -0
- package/src/router/index.js +598 -0
- package/src/ssr/index.js +922 -0
- package/src/state/arrays.js +181 -0
- package/src/state/devtools.js +647 -0
- package/src/state/index.js +498 -0
- package/src/state/middleware.js +288 -0
- package/src/state/scheduler.js +206 -0
- package/src/state/store.js +300 -0
- package/src/tags/index.js +19 -0
- package/src/tannins/auth.js +396 -0
- package/src/test/dom.js +352 -0
- package/src/test/index.js +62 -0
- package/src/test/state.js +306 -0
- package/tools/a11y-audit.js +487 -0
- package/tools/analyzer.js +315 -0
- package/tools/audit.js +706 -0
- package/tools/builder.js +1422 -0
- package/tools/css-extract.js +188 -0
- package/tools/dev-server.js +316 -0
- package/tools/dts-gen.js +1260 -0
- package/tools/figma-components.js +329 -0
- package/tools/figma-patterns.js +516 -0
- package/tools/figma-plugin/code.js +453 -0
- package/tools/figma-plugin/manifest.json +14 -0
- package/tools/figma-plugin/ui.html +268 -0
- package/tools/figma-render.js +293 -0
- package/tools/figma-tokens.js +712 -0
- package/tools/figma-upload.js +318 -0
- package/tools/generate.js +738 -0
- package/tools/icons.js +133 -0
- package/tools/init-templates.js +265 -0
- package/tools/install-hooks.sh +5 -0
- package/tools/migrations/0.5.0.js +53 -0
- package/tools/migrations/0.6.0.js +95 -0
- package/tools/minify.js +170 -0
- package/tools/pre-commit +4 -0
- package/tools/registry.js +662 -0
- package/tools/reset-playground.js +61 -0
- package/tools/starter-templates/content-site/app.js +49 -0
- package/tools/starter-templates/content-site/essence.js +19 -0
- package/tools/starter-templates/content-site/pages.js +31 -0
- package/tools/starter-templates/ecommerce/app.js +50 -0
- package/tools/starter-templates/ecommerce/essence.js +19 -0
- package/tools/starter-templates/ecommerce/pages.js +31 -0
- package/tools/starter-templates/landing-page/app.js +38 -0
- package/tools/starter-templates/landing-page/essence.js +18 -0
- package/tools/starter-templates/landing-page/pages.js +21 -0
- package/tools/starter-templates/portfolio/app.js +45 -0
- package/tools/starter-templates/portfolio/essence.js +19 -0
- package/tools/starter-templates/portfolio/pages.js +33 -0
- package/tools/starter-templates/saas-dashboard/app.js +70 -0
- package/tools/starter-templates/saas-dashboard/essence.js +19 -0
- package/tools/starter-templates/saas-dashboard/pages.js +31 -0
- package/tools/verify-pack.js +203 -0
- package/types/chart.d.ts +77 -0
- package/types/components.d.ts +587 -0
- package/types/core.d.ts +89 -0
- package/types/css.d.ts +149 -0
- package/types/data.d.ts +238 -0
- package/types/form.d.ts +164 -0
- package/types/i18n.d.ts +51 -0
- package/types/icons.d.ts +27 -0
- package/types/index.d.ts +13 -0
- package/types/router.d.ts +116 -0
- package/types/ssr.d.ts +102 -0
- package/types/state.d.ts +83 -0
- package/types/tags.d.ts +62 -0
- package/types/tannins.d.ts +63 -0
- 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
|
+
}
|