@mpen/rerouter 0.1.7 → 0.3.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 (59) hide show
  1. package/README.md +76 -18
  2. package/dist/bin.d.ts +29 -0
  3. package/dist/bin.js +228 -0
  4. package/dist/hooks-Dlwcb0sV.js +20 -0
  5. package/dist/hooks.d.ts +2 -0
  6. package/dist/hooks.js +2 -0
  7. package/dist/index-BYXpNitc.d.ts +5 -0
  8. package/dist/index.d.ts +265 -0
  9. package/dist/index.js +139 -0
  10. package/dist/routes-Hpf6cwcZ.js +135 -0
  11. package/examples/App.tsx +111 -0
  12. package/examples/index.html +67 -0
  13. package/examples/pages/BlogPost.tsx +17 -0
  14. package/examples/pages/FetchLoading.tsx +53 -0
  15. package/examples/pages/FetchLoadingItem.tsx +45 -0
  16. package/examples/pages/Home.tsx +3 -0
  17. package/examples/pages/KitchenSink.tsx +23 -0
  18. package/examples/pages/Login.tsx +3 -0
  19. package/examples/pages/Match.tsx +5 -0
  20. package/examples/pages/NotFound.tsx +3 -0
  21. package/examples/pages/SlowLoading.tsx +8 -0
  22. package/examples/routes.gen.ts +125 -0
  23. package/examples/routes.ts +40 -0
  24. package/package.json +37 -32
  25. package/src/bin.test.ts +199 -0
  26. package/src/bin.ts +333 -0
  27. package/src/components/Link.test.tsx +139 -0
  28. package/src/components/Link.tsx +87 -0
  29. package/src/components/NavLink.test.tsx +119 -0
  30. package/src/components/NavLink.tsx +71 -0
  31. package/src/components/Router.tsx +75 -0
  32. package/src/fixtures/bin/kitchen-sink.tsx +15 -0
  33. package/src/fixtures/bin/optional.tsx +3 -0
  34. package/src/fixtures/bin/pages/Home.tsx +3 -0
  35. package/src/fixtures/bin/pages/KitchenSink.tsx +3 -0
  36. package/src/fixtures/bin/pages/Login.tsx +3 -0
  37. package/src/fixtures/bin/pages/Match.tsx +3 -0
  38. package/src/fixtures/bin/pages/NotFound.tsx +3 -0
  39. package/src/fixtures/bin/pages/Optional.tsx +3 -0
  40. package/src/fixtures/bin/regexp-groups.tsx +11 -0
  41. package/src/fixtures/bin/simple.tsx +1 -0
  42. package/src/fixtures/bin/unnamed.tsx +4 -0
  43. package/src/hooks/index.ts +1 -0
  44. package/src/hooks/useUrl.ts +22 -0
  45. package/src/index.ts +6 -0
  46. package/src/lib/mergeSearch.test.ts +37 -0
  47. package/src/lib/mergeSearch.ts +21 -0
  48. package/src/lib/routes.test.ts +67 -0
  49. package/src/lib/routes.ts +245 -0
  50. package/src/lib/url.ts +9 -0
  51. package/tsconfig.json +9 -0
  52. package/tsdown.config.ts +22 -0
  53. package/LICENSE +0 -21
  54. package/dist/bundle.cjs +0 -406
  55. package/dist/bundle.d.ts +0 -2
  56. package/dist/bundle.mjs +0 -404
  57. package/dist/dev.d.ts +0 -1
  58. package/dist/log.d.ts +0 -1
  59. package/dist/uri-template.d.ts +0 -50
package/dist/index.js ADDED
@@ -0,0 +1,139 @@
1
+ import { n as useUrlSearchParams, t as useUrlPath } from "./hooks-Dlwcb0sV.js";
2
+ import { n as normalizeRoutes, t as normalizeLegacyPathToRegexpSyntax } from "./routes-Hpf6cwcZ.js";
3
+ import { cc } from "@mpen/classcat";
4
+ import { jsx } from "react/jsx-runtime";
5
+ import { Suspense, lazy, useMemo } from "react";
6
+ //#region src/lib/url.ts
7
+ function pushUrl(next, state) {
8
+ window.history.pushState(state, "", next);
9
+ window.dispatchEvent(new PopStateEvent("popstate"));
10
+ }
11
+ function replaceUrl(next, state) {
12
+ window.history.replaceState(state, "", next);
13
+ window.dispatchEvent(new PopStateEvent("popstate"));
14
+ }
15
+ //#endregion
16
+ //#region src/lib/mergeSearch.ts
17
+ function mergeSearch(to, search) {
18
+ const hashIndex = to.indexOf("#");
19
+ const hash = hashIndex !== -1 ? to.slice(hashIndex) : "";
20
+ const pathAndQuery = hashIndex !== -1 ? to.slice(0, hashIndex) : to;
21
+ const queryIndex = pathAndQuery.indexOf("?");
22
+ const path = queryIndex !== -1 ? pathAndQuery.slice(0, queryIndex) : pathAndQuery;
23
+ const existingQuery = queryIndex !== -1 ? pathAndQuery.slice(queryIndex + 1) : "";
24
+ const params = new URLSearchParams(existingQuery);
25
+ const newParams = new URLSearchParams(search);
26
+ for (const [key, value] of newParams) params.set(key, value);
27
+ const queryString = params.toString();
28
+ return `${path}${queryString ? "?" + queryString : ""}${hash}`;
29
+ }
30
+ //#endregion
31
+ //#region src/components/Link.tsx
32
+ /**
33
+ * Renders an anchor that navigates with rerouter history updates on ordinary clicks.
34
+ *
35
+ * @example
36
+ * ```tsx
37
+ * <Link to="/matches/42" search={{ tab: 'details' }}>
38
+ * View match
39
+ * </Link>
40
+ * ```
41
+ *
42
+ * @param props - Anchor props plus rerouter navigation options.
43
+ * @returns An anchor element that pushes or replaces the browser URL.
44
+ */
45
+ function Link({ to, search, children, className, replace, ...rest }) {
46
+ const href = search ? mergeSearch(to, search) : to;
47
+ const linkClassName = cc(className);
48
+ const onClick = (ev) => {
49
+ if (ev.metaKey || ev.ctrlKey || ev.shiftKey || ev.altKey) return;
50
+ if (ev.button !== 0) return;
51
+ ev.preventDefault();
52
+ if (replace) replaceUrl(href);
53
+ else pushUrl(href);
54
+ };
55
+ return /* @__PURE__ */ jsx("a", {
56
+ ...rest,
57
+ className: linkClassName || void 0,
58
+ href,
59
+ onClick,
60
+ children
61
+ });
62
+ }
63
+ //#endregion
64
+ //#region src/components/NavLink.tsx
65
+ function isActivePath(pathname, targetPathname, match) {
66
+ if (pathname === targetPathname) return true;
67
+ if (match === "exact") return false;
68
+ if (targetPathname === "/") return false;
69
+ return pathname.startsWith(`${targetPathname}/`);
70
+ }
71
+ /**
72
+ * Renders a [`Link`]{@link Link} with classes selected from the current route.
73
+ *
74
+ * @example
75
+ * ```tsx
76
+ * <NavLink
77
+ * activeClass={['pill', 'active']}
78
+ * inactiveClass={['pill', { muted: true }]}
79
+ * to="/settings"
80
+ * >
81
+ * Settings
82
+ * </NavLink>
83
+ * ```
84
+ *
85
+ * @param props - Link props plus active and inactive class values.
86
+ * @returns An anchor element that navigates through rerouter.
87
+ */
88
+ function NavLink({ activeClass, className, inactiveClass, match = "exact", to, ...props }) {
89
+ const linkClassName = cc(className, isActivePath(useUrlPath(), new URL(to, window.location.href).pathname, match) ? activeClass : inactiveClass);
90
+ return /* @__PURE__ */ jsx(Link, {
91
+ ...props,
92
+ className: linkClassName || void 0,
93
+ to
94
+ });
95
+ }
96
+ //#endregion
97
+ //#region src/components/Router.tsx
98
+ const lazyRouteComponents = /* @__PURE__ */ new WeakMap();
99
+ function getLazyRouteComponent(component) {
100
+ let LazyComponent = lazyRouteComponents.get(component);
101
+ if (!LazyComponent) {
102
+ LazyComponent = lazy(component);
103
+ lazyRouteComponents.set(component, LazyComponent);
104
+ }
105
+ return LazyComponent;
106
+ }
107
+ /**
108
+ * Renders the first route that matches the current URL pathname.
109
+ *
110
+ * @param props - The router props.
111
+ * @returns The matched lazy route component, the loading fallback, or `null`.
112
+ *
113
+ * @example
114
+ * ```tsx
115
+ * import routes from './routes'
116
+ *
117
+ * function App() {
118
+ * return <Router routes={routes} loading={<div>Loading...</div>} />
119
+ * }
120
+ * ```
121
+ */
122
+ function Router({ routes, loading = null }) {
123
+ const pathname = useUrlPath();
124
+ const normalizedRoutes = useMemo(() => normalizeRoutes(routes).map((route) => ({
125
+ ...route,
126
+ Component: getLazyRouteComponent(route.component)
127
+ })), [routes]);
128
+ for (const { matches, Component } of normalizedRoutes) {
129
+ const params = matches(pathname);
130
+ if (!params) continue;
131
+ return /* @__PURE__ */ jsx(Suspense, {
132
+ fallback: loading,
133
+ children: /* @__PURE__ */ jsx(Component, { ...params })
134
+ });
135
+ }
136
+ return null;
137
+ }
138
+ //#endregion
139
+ export { Link, NavLink, Router, normalizeLegacyPathToRegexpSyntax, normalizeRoutes, pushUrl, replaceUrl, useUrlPath, useUrlSearchParams };
@@ -0,0 +1,135 @@
1
+ import { match } from "path-to-regexp";
2
+ //#region src/lib/routes.ts
3
+ function toUrlPattern(pattern) {
4
+ if (typeof pattern !== "string") return pattern;
5
+ return new URLPattern({ pathname: pattern });
6
+ }
7
+ function decodeRouteParams(groups) {
8
+ const params = {};
9
+ for (const [key, value] of Object.entries(groups)) if (value == null) params[key] = void 0;
10
+ else params[key] = decodeURIComponent(String(value));
11
+ return params;
12
+ }
13
+ function stripLegacyParamPattern(pattern, startIndex) {
14
+ let depth = 1;
15
+ let endIndex = startIndex + 1;
16
+ while (endIndex < pattern.length && depth > 0) {
17
+ const char = pattern[endIndex];
18
+ if (char === "\\") {
19
+ endIndex += 2;
20
+ continue;
21
+ }
22
+ if (char === "(") depth++;
23
+ else if (char === ")") depth--;
24
+ endIndex++;
25
+ }
26
+ return endIndex;
27
+ }
28
+ /**
29
+ * Converts legacy `path-to-regexp` syntax that is ignored by URL generation into syntax accepted
30
+ * by the current parser.
31
+ *
32
+ * @param pattern - The route pattern to normalize.
33
+ * @returns The pattern with custom regexp constraints stripped and optional group suffixes removed.
34
+ *
35
+ * @example
36
+ * ```ts
37
+ * normalizeLegacyPathToRegexpSyntax('/blog/:id(\\d+){-:title}?')
38
+ * // '/blog/:id{-:title}'
39
+ * ```
40
+ *
41
+ * @internal
42
+ */
43
+ function normalizeLegacyPathToRegexpSyntax(pattern) {
44
+ let normalized = "";
45
+ for (let i = 0; i < pattern.length; i++) {
46
+ const char = pattern[i];
47
+ if (char === "\\") {
48
+ normalized += char;
49
+ if (i + 1 < pattern.length) normalized += pattern[++i];
50
+ continue;
51
+ }
52
+ if (char === ":" || char === "*") {
53
+ normalized += char;
54
+ while (i + 1 < pattern.length && /[$_\p{ID_Continue}]/u.test(pattern[i + 1])) normalized += pattern[++i];
55
+ if (pattern[i + 1] === "(") i = stripLegacyParamPattern(pattern, i + 1) - 1;
56
+ continue;
57
+ }
58
+ if (char === "}" && pattern[i + 1] === "?") {
59
+ normalized += char;
60
+ i++;
61
+ continue;
62
+ }
63
+ normalized += char;
64
+ }
65
+ return normalized;
66
+ }
67
+ /**
68
+ * Normalizes routes into objects with a shared matcher implementation.
69
+ *
70
+ * @param routes - The route definitions to normalize.
71
+ * @returns Routes with stable `name`, `pattern`, `component`, and `matches` fields.
72
+ *
73
+ * @example
74
+ * ```tsx
75
+ * const normalized = normalizeRoutes(routes)
76
+ * const match = normalized[0]?.matches('/users/123')
77
+ * ```
78
+ */
79
+ function normalizeRoutes(routes) {
80
+ return routes.map((route) => {
81
+ const { name, pattern, component } = route;
82
+ if (typeof pattern !== "string") {
83
+ const urlPattern = toUrlPattern(pattern);
84
+ return {
85
+ name,
86
+ pattern,
87
+ component,
88
+ matches(pathname) {
89
+ const match = urlPattern.exec({ pathname });
90
+ if (!match) return null;
91
+ return match.pathname?.groups ?? {};
92
+ }
93
+ };
94
+ }
95
+ if (pattern === "*" || pattern === "/*") return {
96
+ name,
97
+ pattern,
98
+ component,
99
+ matches: (_pathname) => ({})
100
+ };
101
+ let matcher;
102
+ let urlPattern;
103
+ try {
104
+ matcher = match(pattern, { decode: decodeURIComponent });
105
+ } catch {
106
+ try {
107
+ urlPattern = toUrlPattern(pattern);
108
+ } catch {
109
+ matcher = match(normalizeLegacyPathToRegexpSyntax(pattern), { decode: decodeURIComponent });
110
+ }
111
+ }
112
+ return {
113
+ name,
114
+ pattern,
115
+ component,
116
+ matches(pathname) {
117
+ if (urlPattern) {
118
+ const match = urlPattern.exec({ pathname });
119
+ if (!match) return null;
120
+ return decodeRouteParams(match.pathname?.groups ?? {});
121
+ }
122
+ if (!matcher) return null;
123
+ const match = matcher(pathname);
124
+ if (!match) return null;
125
+ const params = {};
126
+ for (const [key, value] of Object.entries(match.params)) if (value == null) params[key] = void 0;
127
+ else if (Array.isArray(value)) params[key] = value.join("/");
128
+ else params[key] = String(value);
129
+ return params;
130
+ }
131
+ };
132
+ });
133
+ }
134
+ //#endregion
135
+ export { normalizeRoutes as n, normalizeLegacyPathToRegexpSyntax as t };
@@ -0,0 +1,111 @@
1
+ import { StrictMode } from 'react'
2
+ import { createRoot } from 'react-dom/client'
3
+ import { NavLink, Router, useUrlPath } from '../src'
4
+ import routes from './routes'
5
+ import * as routesGen from './routes.gen'
6
+
7
+ function CurrentPath() {
8
+ const path = useUrlPath()
9
+ return (
10
+ <div className="card">
11
+ <div
12
+ style={{
13
+ display: 'flex',
14
+ justifyContent: 'space-between',
15
+ gap: 12,
16
+ flexWrap: 'wrap',
17
+ }}
18
+ >
19
+ <div>
20
+ <div style={{ fontSize: 12, opacity: 0.8 }}>Current pathname</div>
21
+ <div>
22
+ <code>{path}</code>
23
+ </div>
24
+ </div>
25
+ <div className="nav">
26
+ <NavLink activeClass="active" className="pill" to={routesGen.home()}>
27
+ Home
28
+ </NavLink>
29
+ <NavLink activeClass="active" className="pill" to={routesGen.login()}>
30
+ Login
31
+ </NavLink>
32
+ <NavLink
33
+ activeClass="active"
34
+ className="pill"
35
+ to={routesGen.match({ id: '123' })}
36
+ >
37
+ Match 123
38
+ </NavLink>
39
+ <NavLink
40
+ activeClass="active"
41
+ className="pill"
42
+ to={routesGen.match({ id: 'a/b' })}
43
+ >
44
+ Match a/b (encoded)
45
+ </NavLink>
46
+ <NavLink
47
+ activeClass="active"
48
+ className="pill"
49
+ to={routesGen.blogPost({ id: 123 })}
50
+ >
51
+ Blog 123
52
+ </NavLink>
53
+ <NavLink
54
+ activeClass="active"
55
+ className="pill"
56
+ to={routesGen.blogPost({ id: 123, title: 'hello world' })}
57
+ >
58
+ Blog 123 Title
59
+ </NavLink>
60
+ <NavLink activeClass="active" className="pill" to={routesGen.slowLoading()}>
61
+ Slow Loading
62
+ </NavLink>
63
+ <NavLink
64
+ activeClass="active"
65
+ className="pill"
66
+ match="prefix"
67
+ to={routesGen.fetchLoading()}
68
+ >
69
+ Fetch Loading
70
+ </NavLink>
71
+ <NavLink
72
+ activeClass="active"
73
+ className="pill"
74
+ to={routesGen.kitchenSink({
75
+ foo: 'a/b',
76
+ baz: 'c',
77
+ splat: ['x', 'y'],
78
+ })}
79
+ >
80
+ KitchenSink
81
+ </NavLink>
82
+ </div>
83
+ </div>
84
+ </div>
85
+ )
86
+ }
87
+
88
+ function Layout() {
89
+ return (
90
+ <div className="app">
91
+ <div className="card">
92
+ <h1 style={{ margin: 0 }}>rerouter examples</h1>
93
+ <div style={{ opacity: 0.8, marginTop: 8 }}>
94
+ Client-only dev server using <code>bun --hot examples/index.html</code>.
95
+ </div>
96
+ </div>
97
+
98
+ <CurrentPath />
99
+
100
+ <div className="card">
101
+ <Router routes={routes} loading={<div>Loading route...</div>} />
102
+ </div>
103
+ </div>
104
+ )
105
+ }
106
+
107
+ createRoot(document.getElementById('root')!).render(
108
+ <StrictMode>
109
+ <Layout />
110
+ </StrictMode>,
111
+ )
@@ -0,0 +1,67 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>@mpen/rerouter examples</title>
7
+ <style>
8
+ body {
9
+ margin: 0;
10
+ font-family:
11
+ ui-sans-serif,
12
+ system-ui,
13
+ -apple-system,
14
+ Segoe UI,
15
+ Roboto,
16
+ Helvetica,
17
+ Arial,
18
+ 'Apple Color Emoji',
19
+ 'Segoe UI Emoji';
20
+ background: #0b1020;
21
+ color: #e8eefc;
22
+ }
23
+ a {
24
+ color: inherit;
25
+ }
26
+ .app {
27
+ max-width: 900px;
28
+ margin: 0 auto;
29
+ padding: 24px;
30
+ }
31
+ .card {
32
+ background: rgba(255, 255, 255, 0.06);
33
+ border: 1px solid rgba(255, 255, 255, 0.12);
34
+ border-radius: 12px;
35
+ padding: 16px;
36
+ margin-bottom: 16px;
37
+ }
38
+ .nav {
39
+ display: flex;
40
+ flex-wrap: wrap;
41
+ gap: 10px;
42
+ margin-top: 8px;
43
+ }
44
+ .pill {
45
+ padding: 6px 10px;
46
+ border-radius: 999px;
47
+ border: 1px solid rgba(255, 255, 255, 0.16);
48
+ background: rgba(255, 255, 255, 0.06);
49
+ text-decoration: none;
50
+ }
51
+ .pill.active {
52
+ border-color: rgba(90, 200, 250, 0.7);
53
+ background: rgba(90, 200, 250, 0.18);
54
+ }
55
+ code {
56
+ font-family:
57
+ ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono',
58
+ 'Courier New', monospace;
59
+ font-size: 0.95em;
60
+ }
61
+ </style>
62
+ </head>
63
+ <body>
64
+ <div id="root"></div>
65
+ <script type="module" src="./App.tsx"></script>
66
+ </body>
67
+ </html>
@@ -0,0 +1,17 @@
1
+ import type { RouteComponent } from '../../src'
2
+
3
+ type BlogPostParams = {
4
+ id: string
5
+ title?: string
6
+ }
7
+
8
+ const BlogPost: RouteComponent<BlogPostParams> = ({ id, title }) => {
9
+ return (
10
+ <div>
11
+ <div>Blog post: {id}</div>
12
+ <div>Title: {title}</div>
13
+ </div>
14
+ )
15
+ }
16
+
17
+ export default BlogPost
@@ -0,0 +1,53 @@
1
+ import { use } from 'react'
2
+ import { NavLink, Router, type RouteObject } from '../../src'
3
+ import FetchLoadingItem from './FetchLoadingItem'
4
+ import * as routesGen from '../routes.gen'
5
+
6
+ const itemRoutes: readonly RouteObject[] = [
7
+ {
8
+ pattern: '/fetch-loading/:id',
9
+ component: async () => ({ default: FetchLoadingItem }),
10
+ },
11
+ ]
12
+
13
+ type FetchResult = {
14
+ message: string
15
+ loadedAt: string
16
+ }
17
+
18
+ const fetchResult = new Promise<FetchResult>((resolve) => {
19
+ setTimeout(() => {
20
+ resolve({
21
+ message: 'The route component loaded immediately, then Suspense waited for data.',
22
+ loadedAt: new Date().toLocaleTimeString(),
23
+ })
24
+ }, 2000)
25
+ })
26
+
27
+ export default function FetchLoading() {
28
+ const result = use(fetchResult)
29
+ const itemIds = ['abc-123', 'invoice-456', 'with/slash']
30
+
31
+ return (
32
+ <div>
33
+ <h2 style={{ marginTop: 0 }}>Fetch loading page</h2>
34
+ <div>{result.message}</div>
35
+ <div style={{ marginTop: 8, opacity: 0.8 }}>Loaded at {result.loadedAt}</div>
36
+ <div className="nav" style={{ marginTop: 16 }}>
37
+ {itemIds.map((id) => (
38
+ <NavLink
39
+ activeClass="active"
40
+ className="pill"
41
+ key={id}
42
+ to={routesGen.fetchLoadingItem({ id })}
43
+ >
44
+ Fetch {id}
45
+ </NavLink>
46
+ ))}
47
+ </div>
48
+ <div style={{ marginTop: 16 }}>
49
+ <Router routes={itemRoutes} loading={<div>Loading item...</div>} />
50
+ </div>
51
+ </div>
52
+ )
53
+ }
@@ -0,0 +1,45 @@
1
+ import { use } from 'react'
2
+
3
+ type FetchLoadingItemProps = {
4
+ id: string
5
+ }
6
+
7
+ type FetchResult = {
8
+ id: string
9
+ message: string
10
+ loadedAt: string
11
+ }
12
+
13
+ const fetchResults = new Map<string, Promise<FetchResult>>()
14
+
15
+ function fetchItem(id: string): Promise<FetchResult> {
16
+ let result = fetchResults.get(id)
17
+ if (!result) {
18
+ result = new Promise<FetchResult>((resolve) => {
19
+ setTimeout(() => {
20
+ resolve({
21
+ id,
22
+ message: `Fetched fake data for item ${id}.`,
23
+ loadedAt: new Date().toLocaleTimeString(),
24
+ })
25
+ }, 2000)
26
+ })
27
+ fetchResults.set(id, result)
28
+ }
29
+ return result
30
+ }
31
+
32
+ export default function FetchLoadingItem({ id }: FetchLoadingItemProps) {
33
+ const result = use(fetchItem(id))
34
+
35
+ return (
36
+ <div>
37
+ <h2 style={{ marginTop: 0 }}>Fetch loading item</h2>
38
+ <div>
39
+ URL param: <code>{result.id}</code>
40
+ </div>
41
+ <div style={{ marginTop: 8 }}>{result.message}</div>
42
+ <div style={{ marginTop: 8, opacity: 0.8 }}>Loaded at {result.loadedAt}</div>
43
+ </div>
44
+ )
45
+ }
@@ -0,0 +1,3 @@
1
+ export default function Home() {
2
+ return <div>Home</div>
3
+ }
@@ -0,0 +1,23 @@
1
+ import type { RouteComponent } from '../../src'
2
+
3
+ type KitchenSinkParams = {
4
+ foo: string
5
+ baz: string
6
+ splat: string
7
+ optional?: string
8
+ two?: string
9
+ }
10
+
11
+ const KitchenSink: RouteComponent<KitchenSinkParams> = ({ foo, baz, splat, optional, two }) => {
12
+ return (
13
+ <div>
14
+ <div>foo: {foo}</div>
15
+ <div>baz: {baz}</div>
16
+ <div>splat: {splat}</div>
17
+ <div>optional: {optional}</div>
18
+ <div>two: {two}</div>
19
+ </div>
20
+ )
21
+ }
22
+
23
+ export default KitchenSink
@@ -0,0 +1,3 @@
1
+ export default function Login() {
2
+ return <div>Login</div>
3
+ }
@@ -0,0 +1,5 @@
1
+ import type { RouteParams } from '../../src'
2
+
3
+ export default function Match({ id }: RouteParams) {
4
+ return <div>Match: {id}</div>
5
+ }
@@ -0,0 +1,3 @@
1
+ export default function NotFound() {
2
+ return <div>Not found</div>
3
+ }
@@ -0,0 +1,8 @@
1
+ export default function SlowLoading() {
2
+ return (
3
+ <div>
4
+ <h2 style={{ marginTop: 0 }}>Slow loading page</h2>
5
+ <div>This page waits 2 seconds before its route component finishes loading.</div>
6
+ </div>
7
+ )
8
+ }