shelving 1.229.0 → 1.230.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/package.json +1 -1
- package/ui/misc/MetaContext.d.ts +21 -11
- package/ui/misc/MetaContext.js +20 -12
- package/ui/misc/MetaContext.test.tsx +15 -13
- package/ui/misc/MetaContext.tsx +27 -15
- package/ui/misc/README.md +1 -1
- package/ui/page/HTML.js +2 -2
- package/ui/page/HTML.tsx +2 -2
- package/ui/router/Navigation.js +5 -5
- package/ui/router/Navigation.tsx +5 -5
- package/ui/router/Router.d.ts +9 -1
- package/ui/router/Router.js +23 -17
- package/ui/router/Router.tsx +37 -17
- package/ui/tree/TreeApp.d.ts +5 -4
- package/ui/tree/TreeApp.js +3 -10
- package/ui/tree/TreeApp.tsx +10 -15
- package/ui/tree/TreeRouter.d.ts +30 -0
- package/ui/tree/{TreePage.js → TreeRouter.js} +13 -9
- package/ui/tree/{TreePage.test.tsx → TreeRouter.test.tsx} +4 -4
- package/ui/tree/TreeRouter.tsx +55 -0
- package/ui/tree/index.d.ts +1 -1
- package/ui/tree/index.js +1 -1
- package/ui/tree/index.ts +1 -1
- package/ui/tree/TreePage.d.ts +0 -24
- package/ui/tree/TreePage.tsx +0 -41
package/package.json
CHANGED
package/ui/misc/MetaContext.d.ts
CHANGED
|
@@ -1,20 +1,30 @@
|
|
|
1
|
+
import type { AnyCaller } from "../../util/function.js";
|
|
1
2
|
import type { AbsolutePath } from "../../util/path.js";
|
|
2
3
|
import { type URIParams } from "../../util/uri.js";
|
|
4
|
+
import { type ImmutableURL } from "../../util/url.js";
|
|
3
5
|
import { type Meta, type PossibleMeta } from "../util/meta.js";
|
|
4
|
-
import type { ChildProps } from "../util/props.js";
|
|
5
6
|
/** Context to store the `Config` object. */
|
|
6
7
|
export declare const MetaContext: import("react").Context<Meta>;
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
/**
|
|
9
|
+
* Use the current meta context in a component.
|
|
10
|
+
*
|
|
11
|
+
* @param meta A set of new possible meta data to combine into the current meta context.
|
|
12
|
+
*/
|
|
10
13
|
export declare function requireMeta(meta?: PossibleMeta): Meta;
|
|
11
|
-
/**
|
|
12
|
-
export
|
|
14
|
+
/** A `Meta` object with a defined `url` object, and `path` and `params` properties combined in. */
|
|
15
|
+
export interface MetaURL extends Meta {
|
|
16
|
+
url: ImmutableURL;
|
|
17
|
+
/** The path of `url` relative to `meta.root` (i.e. the _site-root-relative_ path). */
|
|
18
|
+
path: AbsolutePath;
|
|
19
|
+
/** The `?query` params of `url` extracted as a flat set of parameters. */
|
|
20
|
+
params: URIParams;
|
|
21
|
+
}
|
|
13
22
|
/**
|
|
14
|
-
*
|
|
15
|
-
* - Strips the meta `root` (site base) prefix from the meta `url`, leaving a site-root-relative `AbsolutePath`.
|
|
16
|
-
* - Returns `undefined` when `url` or `root` is unset, or they sit on different origins — the path is then unknowable.
|
|
23
|
+
* Use the current meta context in a component with some additional URL helpers.
|
|
17
24
|
*
|
|
18
|
-
* @
|
|
25
|
+
* @param meta A set of new possible meta data to combine into the current meta context.
|
|
26
|
+
* @returns A `Meta` object with a defined `url` object, and `path` and `params` properties combined in.
|
|
27
|
+
* @throws {RequiredError} if the current meta has no `url`
|
|
28
|
+
* @throws {RequiredError} if the current meta `url` does not match the origin of the current meta `root`
|
|
19
29
|
*/
|
|
20
|
-
export declare function
|
|
30
|
+
export declare function requireMetaURL(meta?: PossibleMeta, caller?: AnyCaller): MetaURL;
|
package/ui/misc/MetaContext.js
CHANGED
|
@@ -1,27 +1,35 @@
|
|
|
1
1
|
import { createContext, use } from "react";
|
|
2
|
+
import { RequiredError } from "../../error/RequiredError.js";
|
|
2
3
|
import { getURIParams } from "../../util/uri.js";
|
|
3
4
|
import { matchURLPrefix } from "../../util/url.js";
|
|
4
5
|
import { mergeMeta } from "../util/meta.js";
|
|
5
6
|
/** Context to store the `Config` object. */
|
|
6
7
|
export const MetaContext = createContext({});
|
|
7
8
|
MetaContext.displayName = "MetaContext";
|
|
8
|
-
/**
|
|
9
|
+
/**
|
|
10
|
+
* Use the current meta context in a component.
|
|
11
|
+
*
|
|
12
|
+
* @param meta A set of new possible meta data to combine into the current meta context.
|
|
13
|
+
*/
|
|
9
14
|
export function requireMeta(meta) {
|
|
10
15
|
const current = use(MetaContext);
|
|
11
16
|
return meta ? mergeMeta(current, meta) : current;
|
|
12
17
|
}
|
|
13
|
-
/** Get all URI/route params from the current meta context's URL. */
|
|
14
|
-
export function requireMetaParams() {
|
|
15
|
-
return getURIParams(requireMeta().url ?? {}, requireMetaParams);
|
|
16
|
-
}
|
|
17
18
|
/**
|
|
18
|
-
*
|
|
19
|
-
* - Strips the meta `root` (site base) prefix from the meta `url`, leaving a site-root-relative `AbsolutePath`.
|
|
20
|
-
* - Returns `undefined` when `url` or `root` is unset, or they sit on different origins — the path is then unknowable.
|
|
19
|
+
* Use the current meta context in a component with some additional URL helpers.
|
|
21
20
|
*
|
|
22
|
-
* @
|
|
21
|
+
* @param meta A set of new possible meta data to combine into the current meta context.
|
|
22
|
+
* @returns A `Meta` object with a defined `url` object, and `path` and `params` properties combined in.
|
|
23
|
+
* @throws {RequiredError} if the current meta has no `url`
|
|
24
|
+
* @throws {RequiredError} if the current meta `url` does not match the origin of the current meta `root`
|
|
23
25
|
*/
|
|
24
|
-
export function
|
|
25
|
-
const { url, root } = requireMeta();
|
|
26
|
-
|
|
26
|
+
export function requireMetaURL(meta, caller = requireMetaURL) {
|
|
27
|
+
const { url, root, ...combined } = requireMeta(meta);
|
|
28
|
+
if (!url)
|
|
29
|
+
throw new RequiredError("Meta URL is required", { received: url, caller });
|
|
30
|
+
const path = matchURLPrefix(url, root, caller);
|
|
31
|
+
if (!path)
|
|
32
|
+
throw new RequiredError("Meta URL and meta root must share an origin", { url, root, caller });
|
|
33
|
+
const params = getURIParams(url, caller);
|
|
34
|
+
return { ...combined, url, root, path, params };
|
|
27
35
|
}
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
2
|
import type { ReactNode } from "react";
|
|
3
3
|
import { renderToStaticMarkup } from "react-dom/server";
|
|
4
|
+
import { RequiredError } from "../../error/RequiredError.js";
|
|
4
5
|
import { createMeta } from "../util/meta.js";
|
|
5
|
-
import {
|
|
6
|
+
import { MetaContext, requireMetaURL } from "./MetaContext.js";
|
|
6
7
|
|
|
7
|
-
/** Render `
|
|
8
|
+
/** Render `requireMetaURL().path` from inside a component so its `use(MetaContext)` call is valid. */
|
|
8
9
|
function Probe(): ReactNode {
|
|
9
|
-
return
|
|
10
|
+
return requireMetaURL().path;
|
|
10
11
|
}
|
|
11
12
|
|
|
12
|
-
describe("
|
|
13
|
+
describe("requireMetaURL", () => {
|
|
13
14
|
test("returns the page path relative to the site root", () => {
|
|
14
15
|
const html = renderToStaticMarkup(
|
|
15
16
|
<MetaContext value={createMeta({ root: "http://x.com/sub/", url: "./util/array" })}>
|
|
@@ -28,16 +29,17 @@ describe("getMetaPath", () => {
|
|
|
28
29
|
expect(html).toBe("/");
|
|
29
30
|
});
|
|
30
31
|
|
|
31
|
-
test("
|
|
32
|
-
expect(renderToStaticMarkup(<Probe />)).
|
|
32
|
+
test("throws RequiredError when url is unset", () => {
|
|
33
|
+
expect(() => renderToStaticMarkup(<Probe />)).toThrow(RequiredError);
|
|
33
34
|
});
|
|
34
35
|
|
|
35
|
-
test("
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
<
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
36
|
+
test("throws RequiredError when url and root are on different origins", () => {
|
|
37
|
+
expect(() =>
|
|
38
|
+
renderToStaticMarkup(
|
|
39
|
+
<MetaContext value={createMeta({ root: "http://x.com/", url: "http://y.com/foo" })}>
|
|
40
|
+
<Probe />
|
|
41
|
+
</MetaContext>,
|
|
42
|
+
),
|
|
43
|
+
).toThrow(RequiredError);
|
|
42
44
|
});
|
|
43
45
|
});
|
package/ui/misc/MetaContext.tsx
CHANGED
|
@@ -1,35 +1,47 @@
|
|
|
1
1
|
import { createContext, use } from "react";
|
|
2
|
+
import { RequiredError } from "../../error/RequiredError.js";
|
|
3
|
+
import type { AnyCaller } from "../../util/function.js";
|
|
2
4
|
import type { AbsolutePath } from "../../util/path.js";
|
|
3
5
|
import { getURIParams, type URIParams } from "../../util/uri.js";
|
|
4
|
-
import { matchURLPrefix } from "../../util/url.js";
|
|
6
|
+
import { type ImmutableURL, matchURLPrefix } from "../../util/url.js";
|
|
5
7
|
import { type Meta, mergeMeta, type PossibleMeta } from "../util/meta.js";
|
|
6
|
-
import type { ChildProps } from "../util/props.js";
|
|
7
8
|
|
|
8
9
|
/** Context to store the `Config` object. */
|
|
9
10
|
export const MetaContext = createContext<Meta>({});
|
|
10
11
|
MetaContext.displayName = "MetaContext";
|
|
11
12
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
/**
|
|
14
|
+
* Use the current meta context in a component.
|
|
15
|
+
*
|
|
16
|
+
* @param meta A set of new possible meta data to combine into the current meta context.
|
|
17
|
+
*/
|
|
15
18
|
export function requireMeta(meta?: PossibleMeta): Meta {
|
|
16
19
|
const current = use(MetaContext);
|
|
17
20
|
return meta ? mergeMeta(current, meta) : current;
|
|
18
21
|
}
|
|
19
22
|
|
|
20
|
-
/**
|
|
21
|
-
export
|
|
22
|
-
|
|
23
|
+
/** A `Meta` object with a defined `url` object, and `path` and `params` properties combined in. */
|
|
24
|
+
export interface MetaURL extends Meta {
|
|
25
|
+
url: ImmutableURL;
|
|
26
|
+
/** The path of `url` relative to `meta.root` (i.e. the _site-root-relative_ path). */
|
|
27
|
+
path: AbsolutePath;
|
|
28
|
+
/** The `?query` params of `url` extracted as a flat set of parameters. */
|
|
29
|
+
params: URIParams;
|
|
23
30
|
}
|
|
24
31
|
|
|
25
32
|
/**
|
|
26
|
-
*
|
|
27
|
-
* - Strips the meta `root` (site base) prefix from the meta `url`, leaving a site-root-relative `AbsolutePath`.
|
|
28
|
-
* - Returns `undefined` when `url` or `root` is unset, or they sit on different origins — the path is then unknowable.
|
|
33
|
+
* Use the current meta context in a component with some additional URL helpers.
|
|
29
34
|
*
|
|
30
|
-
* @
|
|
35
|
+
* @param meta A set of new possible meta data to combine into the current meta context.
|
|
36
|
+
* @returns A `Meta` object with a defined `url` object, and `path` and `params` properties combined in.
|
|
37
|
+
* @throws {RequiredError} if the current meta has no `url`
|
|
38
|
+
* @throws {RequiredError} if the current meta `url` does not match the origin of the current meta `root`
|
|
31
39
|
*/
|
|
32
|
-
export function
|
|
33
|
-
const { url, root } = requireMeta();
|
|
34
|
-
|
|
40
|
+
export function requireMetaURL(meta?: PossibleMeta, caller: AnyCaller = requireMetaURL): MetaURL {
|
|
41
|
+
const { url, root, ...combined } = requireMeta(meta);
|
|
42
|
+
if (!url) throw new RequiredError("Meta URL is required", { received: url, caller });
|
|
43
|
+
const path = matchURLPrefix(url, root, caller);
|
|
44
|
+
if (!path) throw new RequiredError("Meta URL and meta root must share an origin", { url, root, caller });
|
|
45
|
+
const params = getURIParams(url, caller);
|
|
46
|
+
return { ...combined, url, root, path, params };
|
|
35
47
|
}
|
package/ui/misc/README.md
CHANGED
|
@@ -111,7 +111,7 @@ const [TreeMapping, TreeMapper] = createMapper({
|
|
|
111
111
|
|
|
112
112
|
`requireMeta` reads the context and optionally merges in override props — use this in components that need to resolve URLs against the current page.
|
|
113
113
|
|
|
114
|
-
`
|
|
114
|
+
`requireMetaURL` resolves the current `url` against the meta `root` and returns the meta extended with the site-root-relative `path` and the extracted query `params`. Throws if `url` is unset or sits on a different origin to `root`.
|
|
115
115
|
|
|
116
116
|
## See also
|
|
117
117
|
|
package/ui/page/HTML.js
CHANGED
|
@@ -6,6 +6,6 @@ import { MetaContext, requireMeta } from "../misc/MetaContext.js";
|
|
|
6
6
|
*/
|
|
7
7
|
export function HTML({ children, ...meta }) {
|
|
8
8
|
const merged = requireMeta(meta);
|
|
9
|
-
const { language, root
|
|
10
|
-
return (_jsxs("html", { lang: language, children: [_jsxs("head", { children: [_jsx("meta", { charSet: "utf-8" }),
|
|
9
|
+
const { language, root, app } = merged;
|
|
10
|
+
return (_jsxs("html", { lang: language, children: [_jsxs("head", { children: [_jsx("meta", { charSet: "utf-8" }), root && _jsx("base", { href: root.href }), app && _jsx("title", { children: app })] }), _jsx("body", { children: _jsx(MetaContext, { value: merged, children: children }) })] }));
|
|
11
11
|
}
|
package/ui/page/HTML.tsx
CHANGED
|
@@ -11,12 +11,12 @@ export interface HTMLProps extends PossibleMeta, ChildProps {}
|
|
|
11
11
|
*/
|
|
12
12
|
export function HTML({ children, ...meta }: HTMLProps): ReactElement {
|
|
13
13
|
const merged = requireMeta(meta);
|
|
14
|
-
const { language, root
|
|
14
|
+
const { language, root, app } = merged;
|
|
15
15
|
return (
|
|
16
16
|
<html lang={language}>
|
|
17
17
|
<head>
|
|
18
18
|
<meta charSet="utf-8" />
|
|
19
|
-
{
|
|
19
|
+
{root && <base href={root.href} />}
|
|
20
20
|
{app && <title>{app}</title>}
|
|
21
21
|
</head>
|
|
22
22
|
<body>
|
package/ui/router/Navigation.js
CHANGED
|
@@ -17,8 +17,8 @@ import { NavigationStore } from "./NavigationStore.js";
|
|
|
17
17
|
* TODO: switch click/popstate handling to the browser Navigation API when broadly supported.
|
|
18
18
|
*/
|
|
19
19
|
export function Navigation({ children, ...meta }) {
|
|
20
|
-
const { url, root
|
|
21
|
-
const nav = useInstance(NavigationStore, url,
|
|
20
|
+
const { url, root, ...merged } = requireMeta(meta);
|
|
21
|
+
const nav = useInstance(NavigationStore, url, root);
|
|
22
22
|
useStore(nav);
|
|
23
23
|
useEffect(() => {
|
|
24
24
|
if (typeof document === "undefined" || typeof window === "undefined")
|
|
@@ -30,8 +30,8 @@ export function Navigation({ children, ...meta }) {
|
|
|
30
30
|
if (anchor instanceof HTMLAnchorElement && anchor.origin === window.location.origin && !anchor.hasAttribute("download")) {
|
|
31
31
|
e.preventDefault();
|
|
32
32
|
nav.forward(anchor.href);
|
|
33
|
-
return false;
|
|
34
|
-
}
|
|
33
|
+
return false; // `return false` stops iOS web app opening every link in a new window.
|
|
34
|
+
}
|
|
35
35
|
}
|
|
36
36
|
};
|
|
37
37
|
const onPopState = () => {
|
|
@@ -44,7 +44,7 @@ export function Navigation({ children, ...meta }) {
|
|
|
44
44
|
window.removeEventListener("popstate", onPopState);
|
|
45
45
|
};
|
|
46
46
|
}, [nav]);
|
|
47
|
-
return (_jsx(NavigationContext, { value: nav, children: _jsx(MetaContext, { value: {
|
|
47
|
+
return (_jsx(NavigationContext, { value: nav, children: _jsx(MetaContext, { value: { url: nav.value, root, ...merged }, children: children }) }));
|
|
48
48
|
}
|
|
49
49
|
/**
|
|
50
50
|
* Force a full remount of children whenever the navigation URL changes.
|
package/ui/router/Navigation.tsx
CHANGED
|
@@ -21,8 +21,8 @@ export interface NavigationProps extends PossibleMeta, OptionalChildProps {}
|
|
|
21
21
|
* TODO: switch click/popstate handling to the browser Navigation API when broadly supported.
|
|
22
22
|
*/
|
|
23
23
|
export function Navigation({ children, ...meta }: NavigationProps): ReactElement {
|
|
24
|
-
const { url, root
|
|
25
|
-
const nav = useInstance(NavigationStore, url,
|
|
24
|
+
const { url, root, ...merged } = requireMeta(meta);
|
|
25
|
+
const nav = useInstance(NavigationStore, url, root);
|
|
26
26
|
useStore(nav);
|
|
27
27
|
|
|
28
28
|
useEffect(() => {
|
|
@@ -36,8 +36,8 @@ export function Navigation({ children, ...meta }: NavigationProps): ReactElement
|
|
|
36
36
|
if (anchor instanceof HTMLAnchorElement && anchor.origin === window.location.origin && !anchor.hasAttribute("download")) {
|
|
37
37
|
e.preventDefault();
|
|
38
38
|
nav.forward(anchor.href);
|
|
39
|
-
return false;
|
|
40
|
-
}
|
|
39
|
+
return false; // `return false` stops iOS web app opening every link in a new window.
|
|
40
|
+
}
|
|
41
41
|
}
|
|
42
42
|
};
|
|
43
43
|
const onPopState = () => {
|
|
@@ -55,7 +55,7 @@ export function Navigation({ children, ...meta }: NavigationProps): ReactElement
|
|
|
55
55
|
|
|
56
56
|
return (
|
|
57
57
|
<NavigationContext value={nav}>
|
|
58
|
-
<MetaContext value={{
|
|
58
|
+
<MetaContext value={{ url: nav.value, root, ...merged }}>{children}</MetaContext>
|
|
59
59
|
</NavigationContext>
|
|
60
60
|
);
|
|
61
61
|
}
|
package/ui/router/Router.d.ts
CHANGED
|
@@ -2,7 +2,13 @@ import type { ReactElement } from "react";
|
|
|
2
2
|
import type { PossibleMeta } from "../util/meta.js";
|
|
3
3
|
import type { Routes } from "./Routes.js";
|
|
4
4
|
export interface RouterProps extends PossibleMeta {
|
|
5
|
+
/** List of routes for the router to match against. */
|
|
5
6
|
readonly routes: Routes;
|
|
7
|
+
/**
|
|
8
|
+
* Optional fallback element.
|
|
9
|
+
* - Explicit `null` means fallback to nothing (router will not throw `NotFoundError`).
|
|
10
|
+
*/
|
|
11
|
+
readonly fallback?: ReactElement | undefined | null;
|
|
6
12
|
}
|
|
7
13
|
/**
|
|
8
14
|
* Match the current URL against `routes` and render the matched element.
|
|
@@ -11,5 +17,7 @@ export interface RouterProps extends PossibleMeta {
|
|
|
11
17
|
* - Nest by putting another `<Router>` inside a route's value; pass `base="/section"` (or wrap in `<Meta>`) to scope.
|
|
12
18
|
* - Route `{placeholders}` are passed as props to function/component route values along with merged URL `?query` params. They are not published into context — descendants of a `ReactElement`-valued route can't see them automatically.
|
|
13
19
|
* - Returns `null` when there's no URL in context or the URL is outside the base.
|
|
20
|
+
*
|
|
21
|
+
* @throws {NotFoundError} if no route matches and `fallback` is `undefined`
|
|
14
22
|
*/
|
|
15
|
-
export declare function Router({ routes, ...meta }: RouterProps): ReactElement | null;
|
|
23
|
+
export declare function Router({ routes, fallback, ...meta }: RouterProps): ReactElement | null;
|
package/ui/router/Router.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { NotFoundError } from "../../error/RequestError.js";
|
|
2
3
|
import { UnexpectedError } from "../../error/UnexpectedError.js";
|
|
4
|
+
import { getProps } from "../../util/index.js";
|
|
3
5
|
import { matchPathTemplate, renderPathTemplate } from "../../util/template.js";
|
|
4
|
-
import {
|
|
5
|
-
import { matchURLPrefix } from "../../util/url.js";
|
|
6
|
-
import { requireMeta } from "../misc/MetaContext.js";
|
|
6
|
+
import { MetaContext, requireMetaURL } from "../misc/MetaContext.js";
|
|
7
7
|
/**
|
|
8
8
|
* Match the current URL against `routes` and render the matched element.
|
|
9
9
|
* - Reads `url` and `base` from the surrounding `<Meta>` context (override via props).
|
|
@@ -11,18 +11,20 @@ import { requireMeta } from "../misc/MetaContext.js";
|
|
|
11
11
|
* - Nest by putting another `<Router>` inside a route's value; pass `base="/section"` (or wrap in `<Meta>`) to scope.
|
|
12
12
|
* - Route `{placeholders}` are passed as props to function/component route values along with merged URL `?query` params. They are not published into context — descendants of a `ReactElement`-valued route can't see them automatically.
|
|
13
13
|
* - Returns `null` when there's no URL in context or the URL is outside the base.
|
|
14
|
+
*
|
|
15
|
+
* @throws {NotFoundError} if no route matches and `fallback` is `undefined`
|
|
14
16
|
*/
|
|
15
|
-
export function Router({ routes, ...meta }) {
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
return _matchRoute(routes, path, url);
|
|
17
|
+
export function Router({ routes, fallback, ...meta }) {
|
|
18
|
+
const combined = requireMetaURL(meta);
|
|
19
|
+
const path = combined;
|
|
20
|
+
const route = _matchRoute(routes, fallback, combined);
|
|
21
|
+
if (route)
|
|
22
|
+
return _jsx(MetaContext, { value: combined, children: route });
|
|
23
|
+
throw new NotFoundError("Tree route not found", { received: path });
|
|
23
24
|
}
|
|
24
|
-
function _matchRoute(routes,
|
|
25
|
-
for (const [route, Route] of
|
|
25
|
+
function _matchRoute(routes, fallback, meta, path = meta.path, depth = 0) {
|
|
26
|
+
for (const [route, Route] of getProps(routes)) {
|
|
27
|
+
// Try to match this path.
|
|
26
28
|
const placeholders = matchPathTemplate(route, path);
|
|
27
29
|
if (!placeholders)
|
|
28
30
|
continue;
|
|
@@ -33,13 +35,17 @@ function _matchRoute(routes, path, url, depth = 0) {
|
|
|
33
35
|
if (typeof Route === "string") {
|
|
34
36
|
if (depth > 10)
|
|
35
37
|
throw new UnexpectedError("Infinite redirect loop", { received: route, expected: path, caller: _matchRoute });
|
|
36
|
-
return _matchRoute(routes, renderPathTemplate(Route, placeholders),
|
|
38
|
+
return _matchRoute(routes, fallback, meta, renderPathTemplate(Route, placeholders), depth + 1);
|
|
37
39
|
}
|
|
38
40
|
// React element — render as-is.
|
|
39
41
|
if (typeof Route !== "function")
|
|
40
42
|
return Route;
|
|
41
|
-
//
|
|
42
|
-
|
|
43
|
+
// Component — render with merged URL query params and route placeholders as props (placeholders win on conflict).
|
|
44
|
+
_jsx(Route, { ...meta.params, ...placeholders }, path);
|
|
43
45
|
}
|
|
44
|
-
|
|
46
|
+
// No match, try the fallback.
|
|
47
|
+
if (fallback !== undefined)
|
|
48
|
+
return fallback;
|
|
49
|
+
// Fallback is undefined, throw a `NotFoundError`
|
|
50
|
+
throw new NotFoundError("No matching route found", { received: path, caller: _matchRoute });
|
|
45
51
|
}
|
package/ui/router/Router.tsx
CHANGED
|
@@ -1,15 +1,21 @@
|
|
|
1
1
|
import type { ReactElement } from "react";
|
|
2
|
+
import { NotFoundError } from "../../error/RequestError.js";
|
|
2
3
|
import { UnexpectedError } from "../../error/UnexpectedError.js";
|
|
3
|
-
import
|
|
4
|
+
import { getProps } from "../../util/index.js";
|
|
4
5
|
import { matchPathTemplate, renderPathTemplate } from "../../util/template.js";
|
|
5
|
-
import {
|
|
6
|
-
import { type ImmutableURL, matchURLPrefix } from "../../util/url.js";
|
|
7
|
-
import { requireMeta } from "../misc/MetaContext.js";
|
|
6
|
+
import { MetaContext, type MetaURL, requireMetaURL } from "../misc/MetaContext.js";
|
|
8
7
|
import type { PossibleMeta } from "../util/meta.js";
|
|
9
8
|
import type { Routes } from "./Routes.js";
|
|
10
9
|
|
|
11
10
|
export interface RouterProps extends PossibleMeta {
|
|
11
|
+
/** List of routes for the router to match against. */
|
|
12
12
|
readonly routes: Routes;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Optional fallback element.
|
|
16
|
+
* - Explicit `null` means fallback to nothing (router will not throw `NotFoundError`).
|
|
17
|
+
*/
|
|
18
|
+
readonly fallback?: ReactElement | undefined | null;
|
|
13
19
|
}
|
|
14
20
|
|
|
15
21
|
/**
|
|
@@ -19,18 +25,27 @@ export interface RouterProps extends PossibleMeta {
|
|
|
19
25
|
* - Nest by putting another `<Router>` inside a route's value; pass `base="/section"` (or wrap in `<Meta>`) to scope.
|
|
20
26
|
* - Route `{placeholders}` are passed as props to function/component route values along with merged URL `?query` params. They are not published into context — descendants of a `ReactElement`-valued route can't see them automatically.
|
|
21
27
|
* - Returns `null` when there's no URL in context or the URL is outside the base.
|
|
28
|
+
*
|
|
29
|
+
* @throws {NotFoundError} if no route matches and `fallback` is `undefined`
|
|
22
30
|
*/
|
|
23
|
-
export function Router({ routes, ...meta }: RouterProps): ReactElement | null {
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
const
|
|
27
|
-
if (
|
|
28
|
-
|
|
31
|
+
export function Router({ routes, fallback, ...meta }: RouterProps): ReactElement | null {
|
|
32
|
+
const combined = requireMetaURL(meta);
|
|
33
|
+
const path = combined;
|
|
34
|
+
const route = _matchRoute(routes, fallback, combined);
|
|
35
|
+
if (route) return <MetaContext value={combined}>{route}</MetaContext>;
|
|
36
|
+
throw new NotFoundError("Tree route not found", { received: path });
|
|
29
37
|
}
|
|
30
38
|
|
|
31
|
-
function _matchRoute(
|
|
32
|
-
|
|
33
|
-
|
|
39
|
+
function _matchRoute(
|
|
40
|
+
routes: Routes,
|
|
41
|
+
fallback: ReactElement | null | undefined,
|
|
42
|
+
meta: MetaURL,
|
|
43
|
+
path = meta.path,
|
|
44
|
+
depth = 0,
|
|
45
|
+
): ReactElement | null | undefined {
|
|
46
|
+
for (const [route, Route] of getProps(routes)) {
|
|
47
|
+
// Try to match this path.
|
|
48
|
+
const placeholders = matchPathTemplate(route, path);
|
|
34
49
|
if (!placeholders) continue;
|
|
35
50
|
|
|
36
51
|
// Skip falsy. Allows a route to be conditionally disabled by setting its value to `null` or `false`.
|
|
@@ -39,14 +54,19 @@ function _matchRoute(routes: Routes, path: AbsolutePath, url: ImmutableURL, dept
|
|
|
39
54
|
// String value is a redirect; re-run matching with the new path. Guard against infinite redirect loops by limiting depth.
|
|
40
55
|
if (typeof Route === "string") {
|
|
41
56
|
if (depth > 10) throw new UnexpectedError("Infinite redirect loop", { received: route, expected: path, caller: _matchRoute });
|
|
42
|
-
return _matchRoute(routes, renderPathTemplate(Route, placeholders),
|
|
57
|
+
return _matchRoute(routes, fallback, meta, renderPathTemplate(Route, placeholders), depth + 1);
|
|
43
58
|
}
|
|
44
59
|
|
|
45
60
|
// React element — render as-is.
|
|
46
61
|
if (typeof Route !== "function") return Route;
|
|
47
62
|
|
|
48
|
-
//
|
|
49
|
-
|
|
63
|
+
// Component — render with merged URL query params and route placeholders as props (placeholders win on conflict).
|
|
64
|
+
<Route key={path} {...meta.params} {...placeholders} />;
|
|
50
65
|
}
|
|
51
|
-
|
|
66
|
+
|
|
67
|
+
// No match, try the fallback.
|
|
68
|
+
if (fallback !== undefined) return fallback;
|
|
69
|
+
|
|
70
|
+
// Fallback is undefined, throw a `NotFoundError`
|
|
71
|
+
throw new NotFoundError("No matching route found", { received: path, caller: _matchRoute });
|
|
52
72
|
}
|
package/ui/tree/TreeApp.d.ts
CHANGED
|
@@ -2,11 +2,12 @@ import type { ReactElement } from "react";
|
|
|
2
2
|
import type { TreeElement } from "../../util/index.js";
|
|
3
3
|
import type { Routes } from "../router/Routes.js";
|
|
4
4
|
import type { PossibleMeta } from "../util/index.js";
|
|
5
|
-
|
|
6
|
-
export interface TreeAppProps extends PossibleMeta, OptionalChildProps {
|
|
5
|
+
export interface TreeAppProps extends PossibleMeta {
|
|
7
6
|
/** The tree elements to display. */
|
|
8
7
|
tree: TreeElement;
|
|
9
|
-
/**
|
|
8
|
+
/**
|
|
9
|
+
* Additional routes.
|
|
10
|
+
*/
|
|
10
11
|
routes?: Routes | undefined;
|
|
11
12
|
}
|
|
12
13
|
/**
|
|
@@ -17,4 +18,4 @@ export interface TreeAppProps extends PossibleMeta, OptionalChildProps {
|
|
|
17
18
|
* - Element rendering uses the default mappings on `<TreePage>`, `<TreeMenu>`, `<TreeCards>`.
|
|
18
19
|
* Override by wrapping with `<TreePageMapping>`, `<TreeMenuMapping>`, or `<TreeCardMapping>`.
|
|
19
20
|
*/
|
|
20
|
-
export declare function TreeApp({ tree, routes
|
|
21
|
+
export declare function TreeApp({ tree, routes: extraRoutes, ...meta }: TreeAppProps): ReactElement;
|
package/ui/tree/TreeApp.js
CHANGED
|
@@ -2,8 +2,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
2
2
|
import { App } from "../app/App.js";
|
|
3
3
|
import { SidebarLayout } from "../layout/SidebarLayout.js";
|
|
4
4
|
import { PageCatcher } from "../misc/Catcher.js";
|
|
5
|
-
import {
|
|
6
|
-
import { TreePage } from "./TreePage.js";
|
|
5
|
+
import { TreeRouter } from "./TreeRouter.js";
|
|
7
6
|
import { TreeSidebar } from "./TreeSidebar.js";
|
|
8
7
|
/**
|
|
9
8
|
* Top-level app component for a tree-based documentation site.
|
|
@@ -13,12 +12,6 @@ import { TreeSidebar } from "./TreeSidebar.js";
|
|
|
13
12
|
* - Element rendering uses the default mappings on `<TreePage>`, `<TreeMenu>`, `<TreeCards>`.
|
|
14
13
|
* Override by wrapping with `<TreePageMapping>`, `<TreeMenuMapping>`, or `<TreeCardMapping>`.
|
|
15
14
|
*/
|
|
16
|
-
export function TreeApp({ tree, routes
|
|
17
|
-
|
|
18
|
-
...routes,
|
|
19
|
-
"/": () => _jsx(TreePage, { tree: tree }),
|
|
20
|
-
// `{...path}` is a named catchall — captures any remaining segments (including empty) as `path`.
|
|
21
|
-
"/{...path}": ({ path = "" }) => _jsx(TreePage, { path: `/${path}`, tree: tree }),
|
|
22
|
-
};
|
|
23
|
-
return (_jsx(App, { ...appProps, children: _jsx(PageCatcher, { children: _jsx(SidebarLayout, { sidebar: _jsx(TreeSidebar, { tree: tree }), children: children ?? _jsx(Router, { routes: allRoutes }) }) }) }));
|
|
15
|
+
export function TreeApp({ tree, routes: extraRoutes, ...meta }) {
|
|
16
|
+
return (_jsx(App, { ...meta, children: _jsx(PageCatcher, { children: _jsx(SidebarLayout, { sidebar: _jsx(TreeSidebar, { tree: tree }), children: _jsx(TreeRouter, { tree: tree }) }) }) }));
|
|
24
17
|
}
|
package/ui/tree/TreeApp.tsx
CHANGED
|
@@ -3,17 +3,17 @@ import type { TreeElement } from "../../util/index.js";
|
|
|
3
3
|
import { App } from "../app/App.js";
|
|
4
4
|
import { SidebarLayout } from "../layout/SidebarLayout.js";
|
|
5
5
|
import { PageCatcher } from "../misc/Catcher.js";
|
|
6
|
-
import { Router } from "../router/Router.js";
|
|
7
6
|
import type { Routes } from "../router/Routes.js";
|
|
8
7
|
import type { PossibleMeta } from "../util/index.js";
|
|
9
|
-
import
|
|
10
|
-
import { TreePage } from "./TreePage.js";
|
|
8
|
+
import { TreeRouter } from "./TreeRouter.js";
|
|
11
9
|
import { TreeSidebar } from "./TreeSidebar.js";
|
|
12
10
|
|
|
13
|
-
export interface TreeAppProps extends PossibleMeta
|
|
11
|
+
export interface TreeAppProps extends PossibleMeta {
|
|
14
12
|
/** The tree elements to display. */
|
|
15
13
|
tree: TreeElement;
|
|
16
|
-
/**
|
|
14
|
+
/**
|
|
15
|
+
* Additional routes.
|
|
16
|
+
*/
|
|
17
17
|
routes?: Routes | undefined;
|
|
18
18
|
}
|
|
19
19
|
|
|
@@ -25,18 +25,13 @@ export interface TreeAppProps extends PossibleMeta, OptionalChildProps {
|
|
|
25
25
|
* - Element rendering uses the default mappings on `<TreePage>`, `<TreeMenu>`, `<TreeCards>`.
|
|
26
26
|
* Override by wrapping with `<TreePageMapping>`, `<TreeMenuMapping>`, or `<TreeCardMapping>`.
|
|
27
27
|
*/
|
|
28
|
-
export function TreeApp({ tree, routes
|
|
29
|
-
const allRoutes: Routes = {
|
|
30
|
-
...routes,
|
|
31
|
-
"/": () => <TreePage tree={tree} />,
|
|
32
|
-
// `{...path}` is a named catchall — captures any remaining segments (including empty) as `path`.
|
|
33
|
-
"/{...path}": ({ path = "" }) => <TreePage path={`/${path}`} tree={tree} />,
|
|
34
|
-
};
|
|
35
|
-
|
|
28
|
+
export function TreeApp({ tree, routes: extraRoutes, ...meta }: TreeAppProps): ReactElement {
|
|
36
29
|
return (
|
|
37
|
-
<App {...
|
|
30
|
+
<App {...meta}>
|
|
38
31
|
<PageCatcher>
|
|
39
|
-
<SidebarLayout sidebar={<TreeSidebar tree={tree} />}>
|
|
32
|
+
<SidebarLayout sidebar={<TreeSidebar tree={tree} />}>
|
|
33
|
+
<TreeRouter tree={tree} />
|
|
34
|
+
</SidebarLayout>
|
|
40
35
|
</PageCatcher>
|
|
41
36
|
</App>
|
|
42
37
|
);
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { ReactElement, ReactNode } from "react";
|
|
2
|
+
import { type TreeElement } from "../../util/element.js";
|
|
3
|
+
import { type AbsolutePath } from "../../util/path.js";
|
|
4
|
+
import type { PossibleMeta } from "../util/index.js";
|
|
5
|
+
/** Extras threaded through `TreeRouterMapper` to the page renderer — the site-root-relative path of the page. */
|
|
6
|
+
interface TreeRouterExtras {
|
|
7
|
+
/** Site-root-relative URL path of the page being rendered. Each page forwards it to its child cards. */
|
|
8
|
+
readonly path: AbsolutePath;
|
|
9
|
+
}
|
|
10
|
+
/** Mapping + Mapper pair for tree routers — wrap children in `<TreeRouterMapping>` to override. */
|
|
11
|
+
export declare const TreeRouterMapping: import("react").FunctionComponent<import("../misc/Mapper.js").MappingProps<TreeRouterExtras>>, TreeRouterMapper: import("react").FunctionComponent<import("../misc/Mapper.js").MapperProps<TreeRouterExtras>>;
|
|
12
|
+
export interface TreeRouterProps extends PossibleMeta {
|
|
13
|
+
/** The tree of elements to match routes for. */
|
|
14
|
+
readonly tree: TreeElement;
|
|
15
|
+
/**
|
|
16
|
+
* Optional fallback element.
|
|
17
|
+
* - Explicit `null` means fallback to nothing (router will not throw `NotFoundError`).
|
|
18
|
+
*/
|
|
19
|
+
readonly fallback?: ReactElement | undefined | null;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Resolve a URL path to a tree element and render it as a full page.
|
|
23
|
+
* - Walks the tree by matching each path segment to a descendant's `key` (via `resolveElementPath()`).
|
|
24
|
+
* - `/` renders the root itself; deeper paths render the matching descendant.
|
|
25
|
+
* - `path` is the site-root-relative path (already stripped of any `APP_URL` subfolder by `<Router>`); it is threaded to the page renderer so child cards build correct hrefs.
|
|
26
|
+
* - Throws `NotFoundError` if no element matches at any level.
|
|
27
|
+
* - To override the renderer for a specific element type, wrap in `<TreeRouterMapping mapping={…}>`.
|
|
28
|
+
*/
|
|
29
|
+
export declare function TreeRouter({ tree, fallback, ...meta }: TreeRouterProps): ReactNode;
|
|
30
|
+
export {};
|
|
@@ -6,8 +6,9 @@ import { DirectoryPage } from "../docs/DirectoryPage.js";
|
|
|
6
6
|
import { DocumentationPage } from "../docs/DocumentationPage.js";
|
|
7
7
|
import { FilePage } from "../docs/FilePage.js";
|
|
8
8
|
import { createMapper } from "../misc/Mapper.js";
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
import { MetaContext, requireMetaURL } from "../misc/MetaContext.js";
|
|
10
|
+
/** Mapping + Mapper pair for tree routers — wrap children in `<TreeRouterMapping>` to override. */
|
|
11
|
+
export const [TreeRouterMapping, TreeRouterMapper] = createMapper({
|
|
11
12
|
"tree-directory": DirectoryPage,
|
|
12
13
|
"tree-file": FilePage,
|
|
13
14
|
"tree-documentation": DocumentationPage,
|
|
@@ -18,12 +19,15 @@ export const [TreePageMapping, TreePageMapper] = createMapper({
|
|
|
18
19
|
* - `/` renders the root itself; deeper paths render the matching descendant.
|
|
19
20
|
* - `path` is the site-root-relative path (already stripped of any `APP_URL` subfolder by `<Router>`); it is threaded to the page renderer so child cards build correct hrefs.
|
|
20
21
|
* - Throws `NotFoundError` if no element matches at any level.
|
|
21
|
-
* - To override the renderer for a specific element type, wrap in `<
|
|
22
|
+
* - To override the renderer for a specific element type, wrap in `<TreeRouterMapping mapping={…}>`.
|
|
22
23
|
*/
|
|
23
|
-
export function
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
24
|
+
export function TreeRouter({ tree, fallback, ...meta }) {
|
|
25
|
+
const { path, ...combined } = requireMetaURL(meta);
|
|
26
|
+
// Find a `TreeElement` matching the current URL meta path.
|
|
27
|
+
const element = resolveElementPath(tree, splitPath(path));
|
|
28
|
+
// We render either a mapped version of the tree element, or the fallback element.
|
|
29
|
+
const route = element ? _jsx(TreeRouterMapper, { path: path, children: element }) : fallback;
|
|
30
|
+
if (route !== undefined)
|
|
31
|
+
return _jsx(MetaContext, { value: combined, children: route });
|
|
32
|
+
throw new NotFoundError("Tree route not found", { received: path });
|
|
29
33
|
}
|
|
@@ -3,7 +3,7 @@ import { renderToStaticMarkup } from "react-dom/server";
|
|
|
3
3
|
import type { TreeElement } from "../../util/element.js";
|
|
4
4
|
import { MetaContext } from "../misc/MetaContext.js";
|
|
5
5
|
import { createMeta } from "../util/meta.js";
|
|
6
|
-
import {
|
|
6
|
+
import { TreeRouter } from "./TreeRouter.js";
|
|
7
7
|
|
|
8
8
|
/** Minimal tree: root → `util` directory → `array` file. */
|
|
9
9
|
const tree: TreeElement = {
|
|
@@ -21,12 +21,12 @@ const tree: TreeElement = {
|
|
|
21
21
|
},
|
|
22
22
|
};
|
|
23
23
|
|
|
24
|
-
describe("
|
|
24
|
+
describe("TreeRouter", () => {
|
|
25
25
|
// When `APP_URL` has a subfolder, the page threads its site-root-relative path to child cards so hrefs include the subfolder exactly once.
|
|
26
26
|
test("card links include an APP_URL subfolder exactly once", () => {
|
|
27
27
|
const html = renderToStaticMarkup(
|
|
28
28
|
<MetaContext value={createMeta({ root: "http://x.com/sub/", url: "./util" })}>
|
|
29
|
-
<
|
|
29
|
+
<TreeRouter tree={tree} />
|
|
30
30
|
</MetaContext>,
|
|
31
31
|
);
|
|
32
32
|
expect(html).toContain('href="http://x.com/sub/util/array"');
|
|
@@ -36,7 +36,7 @@ describe("TreePage", () => {
|
|
|
36
36
|
test("root page card links include an APP_URL subfolder exactly once", () => {
|
|
37
37
|
const html = renderToStaticMarkup(
|
|
38
38
|
<MetaContext value={createMeta({ root: "http://x.com/sub/", url: "./" })}>
|
|
39
|
-
<
|
|
39
|
+
<TreeRouter tree={tree} />
|
|
40
40
|
</MetaContext>,
|
|
41
41
|
);
|
|
42
42
|
expect(html).toContain('href="http://x.com/sub/util"');
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { ReactElement, ReactNode } from "react";
|
|
2
|
+
import { NotFoundError } from "../../error/RequestError.js";
|
|
3
|
+
import { resolveElementPath, type TreeElement } from "../../util/element.js";
|
|
4
|
+
import { type AbsolutePath, splitPath } from "../../util/path.js";
|
|
5
|
+
import { DirectoryPage } from "../docs/DirectoryPage.js";
|
|
6
|
+
import { DocumentationPage } from "../docs/DocumentationPage.js";
|
|
7
|
+
import { FilePage } from "../docs/FilePage.js";
|
|
8
|
+
import { createMapper } from "../misc/Mapper.js";
|
|
9
|
+
import { MetaContext, requireMetaURL } from "../misc/MetaContext.js";
|
|
10
|
+
|
|
11
|
+
import type { PossibleMeta } from "../util/index.js";
|
|
12
|
+
|
|
13
|
+
/** Extras threaded through `TreeRouterMapper` to the page renderer — the site-root-relative path of the page. */
|
|
14
|
+
interface TreeRouterExtras {
|
|
15
|
+
/** Site-root-relative URL path of the page being rendered. Each page forwards it to its child cards. */
|
|
16
|
+
readonly path: AbsolutePath;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Mapping + Mapper pair for tree routers — wrap children in `<TreeRouterMapping>` to override. */
|
|
20
|
+
export const [TreeRouterMapping, TreeRouterMapper] = createMapper<TreeRouterExtras>({
|
|
21
|
+
"tree-directory": DirectoryPage,
|
|
22
|
+
"tree-file": FilePage,
|
|
23
|
+
"tree-documentation": DocumentationPage,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
export interface TreeRouterProps extends PossibleMeta {
|
|
27
|
+
/** The tree of elements to match routes for. */
|
|
28
|
+
readonly tree: TreeElement;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Optional fallback element.
|
|
32
|
+
* - Explicit `null` means fallback to nothing (router will not throw `NotFoundError`).
|
|
33
|
+
*/
|
|
34
|
+
readonly fallback?: ReactElement | undefined | null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Resolve a URL path to a tree element and render it as a full page.
|
|
39
|
+
* - Walks the tree by matching each path segment to a descendant's `key` (via `resolveElementPath()`).
|
|
40
|
+
* - `/` renders the root itself; deeper paths render the matching descendant.
|
|
41
|
+
* - `path` is the site-root-relative path (already stripped of any `APP_URL` subfolder by `<Router>`); it is threaded to the page renderer so child cards build correct hrefs.
|
|
42
|
+
* - Throws `NotFoundError` if no element matches at any level.
|
|
43
|
+
* - To override the renderer for a specific element type, wrap in `<TreeRouterMapping mapping={…}>`.
|
|
44
|
+
*/
|
|
45
|
+
export function TreeRouter({ tree, fallback, ...meta }: TreeRouterProps): ReactNode {
|
|
46
|
+
const { path, ...combined } = requireMetaURL(meta);
|
|
47
|
+
|
|
48
|
+
// Find a `TreeElement` matching the current URL meta path.
|
|
49
|
+
const element = resolveElementPath(tree, splitPath(path));
|
|
50
|
+
|
|
51
|
+
// We render either a mapped version of the tree element, or the fallback element.
|
|
52
|
+
const route = element ? <TreeRouterMapper path={path}>{element}</TreeRouterMapper> : fallback;
|
|
53
|
+
if (route !== undefined) return <MetaContext value={combined}>{route}</MetaContext>;
|
|
54
|
+
throw new NotFoundError("Tree route not found", { received: path });
|
|
55
|
+
}
|
package/ui/tree/index.d.ts
CHANGED
package/ui/tree/index.js
CHANGED
package/ui/tree/index.ts
CHANGED
package/ui/tree/TreePage.d.ts
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import type { ReactNode } from "react";
|
|
2
|
-
import { type TreeElement } from "../../util/element.js";
|
|
3
|
-
import { type AbsolutePath } from "../../util/path.js";
|
|
4
|
-
/** Extras threaded through `TreePageMapper` to the page renderer — the site-root-relative path of the page. */
|
|
5
|
-
interface TreePageExtras {
|
|
6
|
-
/** Site-root-relative URL path of the page being rendered. Each page forwards it to its child cards. */
|
|
7
|
-
readonly path: AbsolutePath;
|
|
8
|
-
}
|
|
9
|
-
/** Mapping + Mapper pair for tree pages — wrap children in `<TreePageMapping>` to override. */
|
|
10
|
-
export declare const TreePageMapping: import("react").FunctionComponent<import("../misc/Mapper.js").MappingProps<TreePageExtras>>, TreePageMapper: import("react").FunctionComponent<import("../misc/Mapper.js").MapperProps<TreePageExtras>>;
|
|
11
|
-
export interface TreePageProps {
|
|
12
|
-
readonly path?: AbsolutePath;
|
|
13
|
-
readonly tree: TreeElement;
|
|
14
|
-
}
|
|
15
|
-
/**
|
|
16
|
-
* Resolve a URL path to a tree element and render it as a full page.
|
|
17
|
-
* - Walks the tree by matching each path segment to a descendant's `key` (via `resolveElementPath()`).
|
|
18
|
-
* - `/` renders the root itself; deeper paths render the matching descendant.
|
|
19
|
-
* - `path` is the site-root-relative path (already stripped of any `APP_URL` subfolder by `<Router>`); it is threaded to the page renderer so child cards build correct hrefs.
|
|
20
|
-
* - Throws `NotFoundError` if no element matches at any level.
|
|
21
|
-
* - To override the renderer for a specific element type, wrap in `<TreePageMapping mapping={…}>`.
|
|
22
|
-
*/
|
|
23
|
-
export declare function TreePage({ path, tree }: TreePageProps): ReactNode;
|
|
24
|
-
export {};
|
package/ui/tree/TreePage.tsx
DELETED
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
import type { ReactNode } from "react";
|
|
2
|
-
import { NotFoundError } from "../../error/RequestError.js";
|
|
3
|
-
import { resolveElementPath, type TreeElement } from "../../util/element.js";
|
|
4
|
-
import { type AbsolutePath, splitPath } from "../../util/path.js";
|
|
5
|
-
import { DirectoryPage } from "../docs/DirectoryPage.js";
|
|
6
|
-
import { DocumentationPage } from "../docs/DocumentationPage.js";
|
|
7
|
-
import { FilePage } from "../docs/FilePage.js";
|
|
8
|
-
import { createMapper } from "../misc/Mapper.js";
|
|
9
|
-
|
|
10
|
-
/** Extras threaded through `TreePageMapper` to the page renderer — the site-root-relative path of the page. */
|
|
11
|
-
interface TreePageExtras {
|
|
12
|
-
/** Site-root-relative URL path of the page being rendered. Each page forwards it to its child cards. */
|
|
13
|
-
readonly path: AbsolutePath;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/** Mapping + Mapper pair for tree pages — wrap children in `<TreePageMapping>` to override. */
|
|
17
|
-
export const [TreePageMapping, TreePageMapper] = createMapper<TreePageExtras>({
|
|
18
|
-
"tree-directory": DirectoryPage,
|
|
19
|
-
"tree-file": FilePage,
|
|
20
|
-
"tree-documentation": DocumentationPage,
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
export interface TreePageProps {
|
|
24
|
-
readonly path?: AbsolutePath;
|
|
25
|
-
readonly tree: TreeElement;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Resolve a URL path to a tree element and render it as a full page.
|
|
30
|
-
* - Walks the tree by matching each path segment to a descendant's `key` (via `resolveElementPath()`).
|
|
31
|
-
* - `/` renders the root itself; deeper paths render the matching descendant.
|
|
32
|
-
* - `path` is the site-root-relative path (already stripped of any `APP_URL` subfolder by `<Router>`); it is threaded to the page renderer so child cards build correct hrefs.
|
|
33
|
-
* - Throws `NotFoundError` if no element matches at any level.
|
|
34
|
-
* - To override the renderer for a specific element type, wrap in `<TreePageMapping mapping={…}>`.
|
|
35
|
-
*/
|
|
36
|
-
export function TreePage({ path = "/", tree }: TreePageProps): ReactNode {
|
|
37
|
-
const segments = splitPath(path);
|
|
38
|
-
const element = resolveElementPath(tree, segments);
|
|
39
|
-
if (!element) throw new NotFoundError("Element not found", { received: path });
|
|
40
|
-
return <TreePageMapper path={path}>{element}</TreePageMapper>;
|
|
41
|
-
}
|