shelving 1.204.0 → 1.205.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shelving",
3
- "version": "1.204.0",
3
+ "version": "1.205.0",
4
4
  "author": "Dave Houlbrooke <dave@shax.com>",
5
5
  "repository": {
6
6
  "type": "git",
package/ui/page/Head.js CHANGED
@@ -1,34 +1,46 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useEffect } from "react";
3
- import { notNullish } from "../../util/null.js";
3
+ import { isNullish, notNullish } from "../../util/null.js";
4
4
  import { getProps } from "../../util/object.js";
5
- import { isDefined } from "../../util/undefined.js";
6
5
  import { requireMeta } from "../misc/Meta.js";
7
6
  import { joinTitles } from "../util/meta.js";
8
7
  /** Meta tags with a capital first letter and hyphens, e.g. `Content-Security-Policy` or `Accept`, are `http-equiv=""` tags. */
9
8
  const R_HTTP_EQUIV = /^[A-Z][a-zA-Z0-9]*(-[A-Z][a-zA-Z0-9]*)*$/;
10
9
  /** Use the details from the current page data context to set the document `<title>`, meta tags, and history state. */
11
10
  export function Head() {
12
- const { url, title, base, app, links, tags } = requireMeta();
11
+ const { url, title, base, app, links, tags, stylesheets, modules, scripts } = requireMeta();
13
12
  useEffect(() => {
14
13
  if (typeof window === "undefined")
15
14
  return;
16
15
  if (url)
17
16
  window.history.replaceState(null, "", url);
18
17
  }, [url]);
19
- return (_jsxs("head", { children: [_jsx("title", { children: joinTitles(title, app) }), base && _jsx("base", { href: base.href }), tags &&
20
- getProps(tags)
21
- .map(([k, x]) => {
22
- if (notNullish(x)) {
23
- const y = x === true ? "yes" : x === false ? "no" : x;
24
- if (k.startsWith("og:"))
25
- return _jsx("meta", { property: k, content: y }, k); // Tags that start with `og:` use `property=""`
26
- if (k.match(R_HTTP_EQUIV))
27
- return _jsx("meta", { httpEquiv: k, content: y }, k); // Tags that are in `Snake-Case` use `http-equiv=""`
28
- return _jsx("meta", { name: k, content: y }, k); // All other tags use `content=""`
29
- }
30
- return null;
31
- })
32
- .filter(isDefined), links &&
33
- getProps(links).map(([k, v]) => notNullish(v) ? (_jsx("link", { rel: k, href: v, type: v.endsWith(".png") ? "image/png" : v.endsWith(".ico") ? "image/x-icon" : undefined }, k)) : null)] }));
18
+ return (_jsxs("head", { children: [_jsx("title", { children: joinTitles(title, app) }), base && _jsx("base", { href: base.href }), tags && getProps(tags).map(_renderTags), links && getProps(links).map(_renderLinks), stylesheets?.map(_renderStylesheets), modules?.map(_renderModules), scripts?.map(_renderScripts)] }));
19
+ }
20
+ function _renderTags([k, x]) {
21
+ if (notNullish(x)) {
22
+ const y = x === true ? "yes" : x === false ? "no" : x;
23
+ if (k.startsWith("og:"))
24
+ return _jsx("meta", { property: k, content: y }, k); // Tags that start with `og:` use `property=""`
25
+ if (k.match(R_HTTP_EQUIV))
26
+ return _jsx("meta", { httpEquiv: k, content: y }, k); // Tags that are in `Snake-Case` use `http-equiv=""`
27
+ return _jsx("meta", { name: k, content: y }, k); // All other tags use `content=""`
28
+ }
29
+ return null;
30
+ }
31
+ function _renderLinks([k, v]) {
32
+ if (notNullish(v)) {
33
+ const type = k.endsWith("icon") ? "image/x-icon" : "text/css";
34
+ return _jsx("link", { rel: k, href: v, type: type }, k);
35
+ }
36
+ return null;
37
+ }
38
+ function _renderStylesheets(v) {
39
+ return isNullish(v) ? null : _jsx("link", { rel: "stylesheet", type: "text/css", href: v }, v);
40
+ }
41
+ function _renderModules(v) {
42
+ return isNullish(v) ? null : _jsx("script", { type: "module", src: v, defer: true }, v);
43
+ }
44
+ function _renderScripts(v) {
45
+ return isNullish(v) ? null : _jsx("script", { type: "text/javascript", src: v, defer: true }, v);
34
46
  }
package/ui/page/Head.tsx CHANGED
@@ -1,16 +1,16 @@
1
1
  import { type ReactElement, useEffect } from "react";
2
- import { notNullish } from "../../util/null.js";
3
- import { getProps } from "../../util/object.js";
4
- import { isDefined } from "../../util/undefined.js";
2
+ import type { ArrayItem } from "../../util/array.js";
3
+ import { isNullish, notNullish } from "../../util/null.js";
4
+ import { getProps, type Prop } from "../../util/object.js";
5
5
  import { requireMeta } from "../misc/Meta.js";
6
- import { joinTitles } from "../util/meta.js";
6
+ import { joinTitles, type MetaAssets, type MetaLinks, type MetaTags } from "../util/meta.js";
7
7
 
8
8
  /** Meta tags with a capital first letter and hyphens, e.g. `Content-Security-Policy` or `Accept`, are `http-equiv=""` tags. */
9
9
  const R_HTTP_EQUIV = /^[A-Z][a-zA-Z0-9]*(-[A-Z][a-zA-Z0-9]*)*$/;
10
10
 
11
11
  /** Use the details from the current page data context to set the document `<title>`, meta tags, and history state. */
12
12
  export function Head(): ReactElement {
13
- const { url, title, base, app, links, tags } = requireMeta();
13
+ const { url, title, base, app, links, tags, stylesheets, modules, scripts } = requireMeta();
14
14
 
15
15
  useEffect(() => {
16
16
  if (typeof window === "undefined") return;
@@ -21,24 +21,41 @@ export function Head(): ReactElement {
21
21
  <head>
22
22
  <title>{joinTitles(title, app)}</title>
23
23
  {base && <base href={base.href} />}
24
- {tags &&
25
- getProps(tags)
26
- .map(([k, x]) => {
27
- if (notNullish(x)) {
28
- const y = x === true ? "yes" : x === false ? "no" : x;
29
- if (k.startsWith("og:")) return <meta key={k} property={k} content={y} />; // Tags that start with `og:` use `property=""`
30
- if (k.match(R_HTTP_EQUIV)) return <meta key={k} httpEquiv={k} content={y} />; // Tags that are in `Snake-Case` use `http-equiv=""`
31
- return <meta key={k} name={k} content={y} />; // All other tags use `content=""`
32
- }
33
- return null;
34
- })
35
- .filter(isDefined)}
36
- {links &&
37
- getProps(links).map(([k, v]) =>
38
- notNullish(v) ? (
39
- <link key={k} rel={k} href={v} type={v.endsWith(".png") ? "image/png" : v.endsWith(".ico") ? "image/x-icon" : undefined} />
40
- ) : null,
41
- )}
24
+ {tags && getProps(tags).map(_renderTags)}
25
+ {links && getProps(links).map(_renderLinks)}
26
+ {stylesheets?.map(_renderStylesheets)}
27
+ {modules?.map(_renderModules)}
28
+ {scripts?.map(_renderScripts)}
42
29
  </head>
43
30
  );
44
31
  }
32
+
33
+ function _renderTags([k, x]: Prop<MetaTags>): ReactElement | null {
34
+ if (notNullish(x)) {
35
+ const y = x === true ? "yes" : x === false ? "no" : x;
36
+ if (k.startsWith("og:")) return <meta key={k} property={k} content={y} />; // Tags that start with `og:` use `property=""`
37
+ if (k.match(R_HTTP_EQUIV)) return <meta key={k} httpEquiv={k} content={y} />; // Tags that are in `Snake-Case` use `http-equiv=""`
38
+ return <meta key={k} name={k} content={y} />; // All other tags use `content=""`
39
+ }
40
+ return null;
41
+ }
42
+
43
+ function _renderLinks([k, v]: Prop<MetaLinks>): ReactElement | null {
44
+ if (notNullish(v)) {
45
+ const type = k.endsWith("icon") ? "image/x-icon" : "text/css";
46
+ return <link key={k} rel={k} href={v} type={type} />;
47
+ }
48
+ return null;
49
+ }
50
+
51
+ function _renderStylesheets(v: ArrayItem<MetaAssets>): ReactElement | null {
52
+ return isNullish(v) ? null : <link key={v} rel="stylesheet" type="text/css" href={v} />;
53
+ }
54
+
55
+ function _renderModules(v: ArrayItem<MetaAssets>): ReactElement | null {
56
+ return isNullish(v) ? null : <script key={v} type="module" src={v} defer />;
57
+ }
58
+
59
+ function _renderScripts(v: ArrayItem<MetaAssets>): ReactElement | null {
60
+ return isNullish(v) ? null : <script key={v} type="text/javascript" src={v} defer />;
61
+ }
@@ -1,19 +1,21 @@
1
1
  import type { ReactElement } from "react";
2
- import type { Elements } from "../../util/element.js";
3
- import { type AppProps } from "../app/App.js";
2
+ import type { TreeElement } from "../../util/index.js";
4
3
  import type { Routes } from "../router/Routes.js";
5
- export interface TreeAppProps extends AppProps {
4
+ import type { PossibleMeta } from "../util/index.js";
5
+ export interface TreeAppProps extends PossibleMeta {
6
6
  /** The tree elements to display. */
7
- elements: Elements;
7
+ tree: TreeElement;
8
8
  /** Additional routes (merged with the default tree route). */
9
9
  routes?: Routes | undefined;
10
+ /** Children is optional and defaults to `<RouterOutput />` */
11
+ children?: ReactElement | undefined;
10
12
  }
11
13
  /**
12
14
  * Top-level app component for a tree-based documentation site.
13
15
  * - Wraps `<App>` with routing, error catching, and a sidebar layout.
14
16
  * - The sidebar shows a `<TreeMenu>` of top-level elements.
15
- * - A catch-all route renders `<TreePage>` for any path.
17
+ * - `/` renders the root via `<TreePage>`; `/**` catches every deeper path and feeds the full sub-path into `<TreePage>`.
16
18
  * - Element rendering uses the default mappings on `<TreePage>`, `<TreeMenu>`, `<TreeCards>`.
17
19
  * Override by wrapping with `<TreePageMapping>`, `<TreeMenuMapping>`, or `<TreeCardMapping>`.
18
20
  */
19
- export declare function TreeApp({ elements, routes, children, ...appProps }: TreeAppProps): ReactElement;
21
+ export declare function TreeApp({ tree, routes, children, ...appProps }: TreeAppProps): ReactElement;
@@ -2,22 +2,23 @@ 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 { Router } from "../router/Router.js";
5
+ import { Router, RouterOutput } from "../router/Router.js";
6
6
  import { TreeMenu } from "./TreeMenu.js";
7
7
  import { TreePage } from "./TreePage.js";
8
- const TREE_ROUTE = "/{**path}";
9
8
  /**
10
9
  * Top-level app component for a tree-based documentation site.
11
10
  * - Wraps `<App>` with routing, error catching, and a sidebar layout.
12
11
  * - The sidebar shows a `<TreeMenu>` of top-level elements.
13
- * - A catch-all route renders `<TreePage>` for any path.
12
+ * - `/` renders the root via `<TreePage>`; `/**` catches every deeper path and feeds the full sub-path into `<TreePage>`.
14
13
  * - Element rendering uses the default mappings on `<TreePage>`, `<TreeMenu>`, `<TreeCards>`.
15
14
  * Override by wrapping with `<TreePageMapping>`, `<TreeMenuMapping>`, or `<TreeCardMapping>`.
16
15
  */
17
- export function TreeApp({ elements, routes = {}, children, ...appProps }) {
16
+ export function TreeApp({ tree, routes = {}, children = _jsx(RouterOutput, {}), ...appProps }) {
18
17
  const allRoutes = {
19
18
  ...routes,
20
- [TREE_ROUTE]: props => _jsx(TreePage, { ...props, elements: elements }),
19
+ "/": () => _jsx(TreePage, { tree: tree }),
20
+ // `**` captures the multi-segment remainder under index `"0"` (named placeholders are single-segment only).
21
+ "/**": ({ 0: sub }) => _jsx(TreePage, { path: `/${sub ?? ""}`, tree: tree }),
21
22
  };
22
- return (_jsx(App, { ...appProps, children: _jsx(Router, { routes: allRoutes, children: _jsx(PageCatcher, { children: _jsx(SidebarLayout, { sidebar: _jsx(TreeMenu, { children: elements }), children: children }) }) }) }));
23
+ return (_jsx(App, { ...appProps, children: _jsx(Router, { routes: allRoutes, children: _jsx(PageCatcher, { children: _jsx(SidebarLayout, { sidebar: _jsx(TreeMenu, { tree: tree }), children: children }) }) }) }));
23
24
  }
@@ -1,42 +1,45 @@
1
1
  import type { ReactElement } from "react";
2
- import type { Elements } from "../../util/element.js";
2
+ import type { TreeElement } from "../../util/index.js";
3
3
  import type { AbsolutePath } from "../../util/path.js";
4
- import { App, type AppProps } from "../app/App.js";
4
+ import { App } from "../app/App.js";
5
5
  import { SidebarLayout } from "../layout/SidebarLayout.js";
6
6
  import { PageCatcher } from "../misc/Catcher.js";
7
- import { Router } from "../router/Router.js";
7
+ import { Router, RouterOutput } from "../router/Router.js";
8
8
  import type { Routes } from "../router/Routes.js";
9
+ import type { PossibleMeta } from "../util/index.js";
9
10
  import { TreeMenu } from "./TreeMenu.js";
10
11
  import { TreePage } from "./TreePage.js";
11
12
 
12
- export interface TreeAppProps extends AppProps {
13
+ export interface TreeAppProps extends PossibleMeta {
13
14
  /** The tree elements to display. */
14
- elements: Elements;
15
+ tree: TreeElement;
15
16
  /** Additional routes (merged with the default tree route). */
16
17
  routes?: Routes | undefined;
18
+ /** Children is optional and defaults to `<RouterOutput />` */
19
+ children?: ReactElement | undefined;
17
20
  }
18
21
 
19
- const TREE_ROUTE = "/{**path}" as AbsolutePath;
20
-
21
22
  /**
22
23
  * Top-level app component for a tree-based documentation site.
23
24
  * - Wraps `<App>` with routing, error catching, and a sidebar layout.
24
25
  * - The sidebar shows a `<TreeMenu>` of top-level elements.
25
- * - A catch-all route renders `<TreePage>` for any path.
26
+ * - `/` renders the root via `<TreePage>`; `/**` catches every deeper path and feeds the full sub-path into `<TreePage>`.
26
27
  * - Element rendering uses the default mappings on `<TreePage>`, `<TreeMenu>`, `<TreeCards>`.
27
28
  * Override by wrapping with `<TreePageMapping>`, `<TreeMenuMapping>`, or `<TreeCardMapping>`.
28
29
  */
29
- export function TreeApp({ elements, routes = {}, children, ...appProps }: TreeAppProps): ReactElement {
30
+ export function TreeApp({ tree, routes = {}, children = <RouterOutput />, ...appProps }: TreeAppProps): ReactElement {
30
31
  const allRoutes: Routes = {
31
32
  ...routes,
32
- [TREE_ROUTE]: props => <TreePage {...props} elements={elements} />,
33
+ "/": () => <TreePage tree={tree} />,
34
+ // `**` captures the multi-segment remainder under index `"0"` (named placeholders are single-segment only).
35
+ "/**": ({ 0: sub }) => <TreePage path={`/${sub ?? ""}` as AbsolutePath} tree={tree} />,
33
36
  };
34
37
 
35
38
  return (
36
39
  <App {...appProps}>
37
40
  <Router routes={allRoutes}>
38
41
  <PageCatcher>
39
- <SidebarLayout sidebar={<TreeMenu>{elements}</TreeMenu>}>{children}</SidebarLayout>
42
+ <SidebarLayout sidebar={<TreeMenu tree={tree} />}>{children}</SidebarLayout>
40
43
  </PageCatcher>
41
44
  </Router>
42
45
  </App>
@@ -1,5 +1,5 @@
1
1
  import type { ReactNode } from "react";
2
- import type { Elements } from "../../util/element.js";
2
+ import type { TreeElements } from "../../util/element.js";
3
3
  /**
4
4
  * Default mappings for the most common tree element types.
5
5
  * - Consumers can override individual entries via `<TreeCardMapping>`.
@@ -7,7 +7,7 @@ import type { Elements } from "../../util/element.js";
7
7
  export declare const TreeCardMapping: import("react").FunctionComponent<import("../misc/Mapper.js").MappingProps>, TreeCardMapper: import("react").FunctionComponent<import("../misc/Mapper.js").MapperProps>;
8
8
  export interface TreeCardsProps {
9
9
  /** Elements to render as cards. */
10
- children?: Elements;
10
+ children: TreeElements;
11
11
  }
12
- /** Grid of cards built from a tree of elements. */
12
+ /** Grid of cards rendered from a flat collection of tree elements. */
13
13
  export declare function TreeCards({ children }: TreeCardsProps): ReactNode;
@@ -13,7 +13,7 @@ export const [TreeCardMapping, TreeCardMapper] = createMapper({
13
13
  "tree-file": FileCard,
14
14
  "tree-documentation": DocumentationCard,
15
15
  });
16
- /** Grid of cards built from a tree of elements. */
16
+ /** Grid of cards rendered from a flat collection of tree elements. */
17
17
  export function TreeCards({ children }) {
18
18
  return (_jsx("div", { className: TREE_CARDS_CSS.grid, children: _jsx(TreeCardMapper, { children: children }) }));
19
19
  }
@@ -1,5 +1,5 @@
1
1
  import type { ReactNode } from "react";
2
- import type { Elements } from "../../util/element.js";
2
+ import type { TreeElements } from "../../util/element.js";
3
3
  import { DirectoryCard } from "../docs/DirectoryCard.js";
4
4
  import { DocumentationCard } from "../docs/DocumentationCard.js";
5
5
  import { FileCard } from "../docs/FileCard.js";
@@ -18,10 +18,10 @@ export const [TreeCardMapping, TreeCardMapper] = createMapper({
18
18
 
19
19
  export interface TreeCardsProps {
20
20
  /** Elements to render as cards. */
21
- children?: Elements;
21
+ children: TreeElements;
22
22
  }
23
23
 
24
- /** Grid of cards built from a tree of elements. */
24
+ /** Grid of cards rendered from a flat collection of tree elements. */
25
25
  export function TreeCards({ children }: TreeCardsProps): ReactNode {
26
26
  return (
27
27
  <div className={TREE_CARDS_CSS.grid}>
@@ -1,5 +1,5 @@
1
1
  import type { ReactNode } from "react";
2
- import type { Elements } from "../../util/element.js";
2
+ import type { TreeElement } from "../../util/element.js";
3
3
  /**
4
4
  * Default mappings for the most common tree element types.
5
5
  * - Consumers can override individual entries via `<TreeMenuMapping>`.
@@ -7,8 +7,8 @@ import type { Elements } from "../../util/element.js";
7
7
  */
8
8
  export declare const TreeMenuMapping: import("react").FunctionComponent<import("../misc/Mapper.js").MappingProps>, TreeMenuMapper: import("react").FunctionComponent<import("../misc/Mapper.js").MapperProps>;
9
9
  export interface TreeMenuProps {
10
- /** Elements to render as navigation links. */
11
- children?: Elements;
10
+ /** Root element whose children become the navigation links. */
11
+ tree: TreeElement;
12
12
  }
13
- /** Sidebar navigation menu built from a tree of elements. */
14
- export declare function TreeMenu({ children }: TreeMenuProps): ReactNode;
13
+ /** Sidebar navigation menu built from the children of a root tree element. */
14
+ export declare function TreeMenu({ tree }: TreeMenuProps): ReactNode;
@@ -12,7 +12,7 @@ export const [TreeMenuMapping, TreeMenuMapper] = createMapper({
12
12
  "tree-directory": DirectoryMenuItem,
13
13
  "tree-file": FileMenuItem,
14
14
  });
15
- /** Sidebar navigation menu built from a tree of elements. */
16
- export function TreeMenu({ children }) {
17
- return (_jsx("nav", { className: TREE_MENU_CSS.menu, children: _jsx("ul", { className: TREE_MENU_CSS.list, children: _jsx(TreeMenuMapper, { children: children }) }) }));
15
+ /** Sidebar navigation menu built from the children of a root tree element. */
16
+ export function TreeMenu({ tree }) {
17
+ return (_jsx("nav", { className: TREE_MENU_CSS.menu, children: _jsx("ul", { className: TREE_MENU_CSS.list, children: _jsx(TreeMenuMapper, { children: tree.props.children }) }) }));
18
18
  }
@@ -1,5 +1,5 @@
1
1
  import type { ReactNode } from "react";
2
- import type { Elements } from "../../util/element.js";
2
+ import type { TreeElement } from "../../util/element.js";
3
3
  import { DirectoryMenuItem } from "../docs/DirectoryMenuItem.js";
4
4
  import { FileMenuItem } from "../docs/FileMenuItem.js";
5
5
  import { createMapper } from "../misc/Mapper.js";
@@ -16,16 +16,16 @@ export const [TreeMenuMapping, TreeMenuMapper] = createMapper({
16
16
  });
17
17
 
18
18
  export interface TreeMenuProps {
19
- /** Elements to render as navigation links. */
20
- children?: Elements;
19
+ /** Root element whose children become the navigation links. */
20
+ tree: TreeElement;
21
21
  }
22
22
 
23
- /** Sidebar navigation menu built from a tree of elements. */
24
- export function TreeMenu({ children }: TreeMenuProps): ReactNode {
23
+ /** Sidebar navigation menu built from the children of a root tree element. */
24
+ export function TreeMenu({ tree }: TreeMenuProps): ReactNode {
25
25
  return (
26
26
  <nav className={TREE_MENU_CSS.menu}>
27
27
  <ul className={TREE_MENU_CSS.list}>
28
- <TreeMenuMapper>{children}</TreeMenuMapper>
28
+ <TreeMenuMapper>{tree.props.children}</TreeMenuMapper>
29
29
  </ul>
30
30
  </nav>
31
31
  );
@@ -1,5 +1,5 @@
1
1
  import type { ReactNode } from "react";
2
- import { type Elements } from "../../util/element.js";
2
+ import { type TreeElement } from "../../util/element.js";
3
3
  import { type AbsolutePath } from "../../util/path.js";
4
4
  /**
5
5
  * Default mappings for the most common tree element types.
@@ -8,13 +8,13 @@ import { type AbsolutePath } from "../../util/path.js";
8
8
  export declare const TreePageMapping: import("react").FunctionComponent<import("../misc/Mapper.js").MappingProps>, TreePageMapper: import("react").FunctionComponent<import("../misc/Mapper.js").MapperProps>;
9
9
  export interface TreePageProps {
10
10
  path?: AbsolutePath;
11
- /** The root elements to search within. */
12
- elements?: Elements;
11
+ tree: TreeElement;
13
12
  }
14
13
  /**
15
14
  * Resolve a URL path to a tree element and render it.
16
- * - Uses `resolveElement()` to walk the tree matching each path segment to an element's `key`.
15
+ * - Uses `resolveElementPath()` to walk the tree matching each path segment to a descendant's `key`.
16
+ * - `/` renders `tree` itself; deeper paths render the matching descendant.
17
17
  * - Delegates rendering to the component registered in the element mapper.
18
18
  * - Throws `NotFoundError` if no element matches at any level.
19
19
  */
20
- export declare function TreePage({ path, elements }: TreePageProps): ReactNode;
20
+ export declare function TreePage({ path, tree }: TreePageProps): ReactNode;
@@ -17,12 +17,13 @@ export const [TreePageMapping, TreePageMapper] = createMapper({
17
17
  });
18
18
  /**
19
19
  * Resolve a URL path to a tree element and render it.
20
- * - Uses `resolveElement()` to walk the tree matching each path segment to an element's `key`.
20
+ * - Uses `resolveElementPath()` to walk the tree matching each path segment to a descendant's `key`.
21
+ * - `/` renders `tree` itself; deeper paths render the matching descendant.
21
22
  * - Delegates rendering to the component registered in the element mapper.
22
23
  * - Throws `NotFoundError` if no element matches at any level.
23
24
  */
24
- export function TreePage({ path = "/", elements }) {
25
- const element = resolveElementPath(elements, splitAbsolutePath(path));
25
+ export function TreePage({ path = "/", tree }) {
26
+ const element = resolveElementPath(tree, splitAbsolutePath(path));
26
27
  if (!element)
27
28
  throw new NotFoundError("Element not found", { received: path });
28
29
  return _jsx(TreePageMapper, { children: element });
@@ -1,6 +1,6 @@
1
1
  import type { ReactNode } from "react";
2
2
  import { NotFoundError } from "../../error/RequestError.js";
3
- import { type Elements, resolveElementPath } from "../../util/element.js";
3
+ import { resolveElementPath, type TreeElement } from "../../util/element.js";
4
4
  import { type AbsolutePath, splitAbsolutePath } from "../../util/path.js";
5
5
  import { DirectoryPage } from "../docs/DirectoryPage.js";
6
6
  import { DocumentationPage } from "../docs/DocumentationPage.js";
@@ -19,18 +19,18 @@ export const [TreePageMapping, TreePageMapper] = createMapper({
19
19
 
20
20
  export interface TreePageProps {
21
21
  path?: AbsolutePath;
22
- /** The root elements to search within. */
23
- elements?: Elements;
22
+ tree: TreeElement;
24
23
  }
25
24
 
26
25
  /**
27
26
  * Resolve a URL path to a tree element and render it.
28
- * - Uses `resolveElement()` to walk the tree matching each path segment to an element's `key`.
27
+ * - Uses `resolveElementPath()` to walk the tree matching each path segment to a descendant's `key`.
28
+ * - `/` renders `tree` itself; deeper paths render the matching descendant.
29
29
  * - Delegates rendering to the component registered in the element mapper.
30
30
  * - Throws `NotFoundError` if no element matches at any level.
31
31
  */
32
- export function TreePage({ path = "/", elements }: TreePageProps): ReactNode {
33
- const element = resolveElementPath(elements, splitAbsolutePath(path));
32
+ export function TreePage({ path = "/", tree }: TreePageProps): ReactNode {
33
+ const element = resolveElementPath(tree, splitAbsolutePath(path));
34
34
  if (!element) throw new NotFoundError("Element not found", { received: path });
35
35
  return <TreePageMapper>{element}</TreePageMapper>;
36
36
  }
package/ui/util/meta.d.ts CHANGED
@@ -34,7 +34,7 @@ export interface MetaData {
34
34
  readonly links?: MetaLinks | undefined;
35
35
  readonly modules?: MetaAssets | undefined;
36
36
  readonly scripts?: MetaAssets | undefined;
37
- readonly styles?: MetaAssets | undefined;
37
+ readonly stylesheets?: MetaAssets | undefined;
38
38
  }
39
39
  /** Input metadata that can be parsed and converted to proper metadata. */
40
40
  export interface PossibleMeta extends Omit<MetaData, "url"> {
package/ui/util/meta.ts CHANGED
@@ -37,7 +37,7 @@ export interface MetaData {
37
37
  readonly links?: MetaLinks | undefined;
38
38
  readonly modules?: MetaAssets | undefined;
39
39
  readonly scripts?: MetaAssets | undefined;
40
- readonly styles?: MetaAssets | undefined;
40
+ readonly stylesheets?: MetaAssets | undefined;
41
41
  }
42
42
 
43
43
  /** Input metadata that can be parsed and converted to proper metadata. */
package/util/element.d.ts CHANGED
@@ -1,7 +1,6 @@
1
1
  import type { ImmutableArray } from "./array.js";
2
2
  import type { AbsolutePath } from "./path.js";
3
3
  import type { Query } from "./query.js";
4
- import type { Segments } from "./string.js";
5
4
  /** Set of valid props for an element. */
6
5
  export interface ElementProps {
7
6
  readonly [key: string]: unknown;
@@ -151,48 +150,45 @@ export declare function queryElements(elements: Elements, query: Query<Element>,
151
150
  */
152
151
  export declare function filterElements(elements: Elements, match: (element: Element) => boolean, depth?: number): Iterable<Element>;
153
152
  /**
154
- * Resolve an element in a tree by walking a sequence of keys.
155
- * - Accepts a dot-separated string (e.g. `"util.array"`) or an array of path segments (e.g. `["util", "array"]`).
156
- * - Matches each segment to the `key` of an immediate child element.
157
- * - If `keys` is empty, undefined, or an empty string, returns the first keyed element at the root level.
158
- * - Returns `undefined` if no element matches at any level.
153
+ * Resolve an element in a tree by walking a sequence of keys from `root`.
154
+ * - The `root` element's own key is never matched against path segments it's the container, not a step in the path.
155
+ * - Each segment matches the `key` of an immediate child of the current element.
156
+ * - If `path` is empty, returns `root` itself.
157
+ * - Returns `undefined` if no descendant matches at any level.
159
158
  *
160
159
  * Splitting the path:
161
- * - We accept a raw `Segments` array for each element, so they can be joined later however you wish.
160
+ * - We accept a raw `Segments` array, so callers can join paths later however they wish.
162
161
  * - Element paths have no canonical string representation so we use `Segments` instead.
163
- * - To split the keys in `a.b.c` dotted data format use `mapItems(getElementPaths(elements), splitDataPath)`
164
- * - To split the keys in `/a/b/c` absolute path format use `mapItems(getElementPaths(elements), splitAbsolutePath)`
162
+ * - To split the keys in `a.b.c` dotted data format use `mapItems(getElementPaths(root), splitDataPath)`
163
+ * - To split the keys in `/a/b/c` absolute path format use `mapItems(getElementPaths(root), splitAbsolutePath)`
165
164
  *
166
- * @param elements The root elements to search within.
167
- * @param path An array of path segments.
168
- * - Element paths have no canonical string representation so we always us the `Segments` format.
165
+ * @param root The root element to walk from. Its own `key` is treated as a label, not a path segment.
166
+ * @param path An array of path segments naming descendants of `root`.
169
167
  *
170
- * @example resolveElement(elements, "util.array") // Element with key "array" inside element with key "util"
171
- * @example resolveElement(elements, ["util", "array"]) // Same as above
172
- * @example resolveElement(elements, "") // First keyed root element
168
+ * @example resolveElementPath(root, ["util", "array"]) // Element with key "array" inside child with key "util"
169
+ * @example resolveElementPath(root, []) // `root` itself
173
170
  */
174
- export declare function resolveElementPath(elements: Elements, path: Segments): Element | undefined;
171
+ export declare function resolveElementPath(root: TreeElement, path: readonly string[]): Element | undefined;
175
172
  /**
176
- * Deeply iterate a tree of elements and yield an array of path segments for each element that has a string `key:` property.
177
- * - Each yielded value is an array of path segments from root to the element.
178
- * - Only elements with a string `key:` property are included.
179
- * - Elements with `undefined` or `null` key are skipped.
173
+ * Deeply iterate a tree from `root` and yield path segments for each reachable element.
174
+ * - Yields `[]` for `root` itself.
175
+ * - Yields `[key]` for each immediate child, `[key, key]` for grandchildren, etc.
176
+ * - The `root` element's own key never appears in any yielded path — it's the container.
177
+ * - Children with `undefined` or `null` keys are skipped (and their descendants are not yielded).
180
178
  *
181
179
  * Joining the paths:
182
- * - We return a `Segments` array for each element, so they can be joined later however you wish.
180
+ * - We return a `Segments` array for each element, so callers can join paths later however they wish.
183
181
  * - Element paths have no canonical string representation so we use `Segments` instead.
184
- * - To join the keys in `a.b.c` dotted data format use `mapItems(getElementPaths(elements), joinDataPath)`
185
- * - To join the keys in `/a/b/c` absolute path format use `mapItems(getElementPaths(elements), joinAbsolutePath)`
182
+ * - To join the keys in `a.b.c` dotted data format use `mapItems(getElementPaths(root), joinDataPath)`
183
+ * - To join the keys in `/a/b/c` absolute path format use `mapItems(getElementPaths(root), joinAbsolutePath)`
186
184
  *
187
- * @param elements The elements to get keys for.
185
+ * @param root The root element to walk from. Its own `key` is treated as a label, not a path segment.
188
186
  * @param depth Controls how many levels of children to recurse into (defaults to infinite depth).
189
- * - `depth=0` yields matching elements at the current level only (no recursion into children).
187
+ * - `depth=0` yields only `[]` (the root itself).
190
188
  *
191
- * @returns Iterable set of path segment arrays, each representing one component.
192
- * - Element paths have no canonical string representation so we always us the `Segments` format.
189
+ * @returns Iterable set of path segment arrays, each representing one descendant (or the root).
193
190
  */
194
- export declare function getElementPaths(elements: Elements, depth?: number): Iterable<Segments>;
195
- export declare function _getElementPaths(elements: Elements, depth: number, prefix?: Segments): Iterable<Segments>;
191
+ export declare function getElementPaths(root: TreeElement, depth?: number): Iterable<readonly string[]>;
196
192
  /** Combine two `Elements`, preserving both if both are set. */
197
193
  export declare function mergeElements<T extends Elements>(a: T, b: T): T;
198
194
  export declare function mergeElements(a: Elements, b: Elements): Elements;
package/util/element.js CHANGED
@@ -70,32 +70,29 @@ export function* filterElements(elements, match, depth = Infinity) {
70
70
  yield element;
71
71
  }
72
72
  /**
73
- * Resolve an element in a tree by walking a sequence of keys.
74
- * - Accepts a dot-separated string (e.g. `"util.array"`) or an array of path segments (e.g. `["util", "array"]`).
75
- * - Matches each segment to the `key` of an immediate child element.
76
- * - If `keys` is empty, undefined, or an empty string, returns the first keyed element at the root level.
77
- * - Returns `undefined` if no element matches at any level.
73
+ * Resolve an element in a tree by walking a sequence of keys from `root`.
74
+ * - The `root` element's own key is never matched against path segments it's the container, not a step in the path.
75
+ * - Each segment matches the `key` of an immediate child of the current element.
76
+ * - If `path` is empty, returns `root` itself.
77
+ * - Returns `undefined` if no descendant matches at any level.
78
78
  *
79
79
  * Splitting the path:
80
- * - We accept a raw `Segments` array for each element, so they can be joined later however you wish.
80
+ * - We accept a raw `Segments` array, so callers can join paths later however they wish.
81
81
  * - Element paths have no canonical string representation so we use `Segments` instead.
82
- * - To split the keys in `a.b.c` dotted data format use `mapItems(getElementPaths(elements), splitDataPath)`
83
- * - To split the keys in `/a/b/c` absolute path format use `mapItems(getElementPaths(elements), splitAbsolutePath)`
82
+ * - To split the keys in `a.b.c` dotted data format use `mapItems(getElementPaths(root), splitDataPath)`
83
+ * - To split the keys in `/a/b/c` absolute path format use `mapItems(getElementPaths(root), splitAbsolutePath)`
84
84
  *
85
- * @param elements The root elements to search within.
86
- * @param path An array of path segments.
87
- * - Element paths have no canonical string representation so we always us the `Segments` format.
85
+ * @param root The root element to walk from. Its own `key` is treated as a label, not a path segment.
86
+ * @param path An array of path segments naming descendants of `root`.
88
87
  *
89
- * @example resolveElement(elements, "util.array") // Element with key "array" inside element with key "util"
90
- * @example resolveElement(elements, ["util", "array"]) // Same as above
91
- * @example resolveElement(elements, "") // First keyed root element
88
+ * @example resolveElementPath(root, ["util", "array"]) // Element with key "array" inside child with key "util"
89
+ * @example resolveElementPath(root, []) // `root` itself
92
90
  */
93
- export function resolveElementPath(elements, path) {
94
- let current = elements;
95
- let found;
91
+ export function resolveElementPath(root, path) {
92
+ let current = root;
96
93
  for (const segment of path) {
97
- found = undefined;
98
- for (const el of getElements(current, 0)) {
94
+ let found;
95
+ for (const el of getElements(current.props.children, 0)) {
99
96
  if (el.key === segment) {
100
97
  found = el;
101
98
  break;
@@ -103,35 +100,40 @@ export function resolveElementPath(elements, path) {
103
100
  }
104
101
  if (!found)
105
102
  return undefined;
106
- current = found.props.children;
103
+ current = found;
107
104
  }
108
- return found;
105
+ return current;
109
106
  }
110
107
  /**
111
- * Deeply iterate a tree of elements and yield an array of path segments for each element that has a string `key:` property.
112
- * - Each yielded value is an array of path segments from root to the element.
113
- * - Only elements with a string `key:` property are included.
114
- * - Elements with `undefined` or `null` key are skipped.
108
+ * Deeply iterate a tree from `root` and yield path segments for each reachable element.
109
+ * - Yields `[]` for `root` itself.
110
+ * - Yields `[key]` for each immediate child, `[key, key]` for grandchildren, etc.
111
+ * - The `root` element's own key never appears in any yielded path — it's the container.
112
+ * - Children with `undefined` or `null` keys are skipped (and their descendants are not yielded).
115
113
  *
116
114
  * Joining the paths:
117
- * - We return a `Segments` array for each element, so they can be joined later however you wish.
115
+ * - We return a `Segments` array for each element, so callers can join paths later however they wish.
118
116
  * - Element paths have no canonical string representation so we use `Segments` instead.
119
- * - To join the keys in `a.b.c` dotted data format use `mapItems(getElementPaths(elements), joinDataPath)`
120
- * - To join the keys in `/a/b/c` absolute path format use `mapItems(getElementPaths(elements), joinAbsolutePath)`
117
+ * - To join the keys in `a.b.c` dotted data format use `mapItems(getElementPaths(root), joinDataPath)`
118
+ * - To join the keys in `/a/b/c` absolute path format use `mapItems(getElementPaths(root), joinAbsolutePath)`
121
119
  *
122
- * @param elements The elements to get keys for.
120
+ * @param root The root element to walk from. Its own `key` is treated as a label, not a path segment.
123
121
  * @param depth Controls how many levels of children to recurse into (defaults to infinite depth).
124
- * - `depth=0` yields matching elements at the current level only (no recursion into children).
122
+ * - `depth=0` yields only `[]` (the root itself).
125
123
  *
126
- * @returns Iterable set of path segment arrays, each representing one component.
127
- * - Element paths have no canonical string representation so we always us the `Segments` format.
124
+ * @returns Iterable set of path segment arrays, each representing one descendant (or the root).
128
125
  */
129
- export function getElementPaths(elements, depth = Infinity) {
130
- return _getElementPaths(elements, depth);
126
+ export function getElementPaths(root, depth = Infinity) {
127
+ return _getElementPaths(root, depth);
128
+ }
129
+ function* _getElementPaths(root, depth) {
130
+ yield [];
131
+ if (depth > 0)
132
+ yield* _getDescendantPaths(root.props.children, depth - 1);
131
133
  }
132
- export function* _getElementPaths(elements, depth, prefix) {
134
+ function* _getDescendantPaths(elements, depth, prefix) {
133
135
  for (const { key, props } of getElements(elements, 0)) {
134
- // Skip `null` or `undefined` keys.
136
+ // Skip `null` or `undefined` keys (and their descendants).
135
137
  if (isNullish(key))
136
138
  continue;
137
139
  // Make the path and yield it.
@@ -139,7 +141,7 @@ export function* _getElementPaths(elements, depth, prefix) {
139
141
  yield keys;
140
142
  // Recurse into the children.
141
143
  if (depth > 0 && props.children)
142
- yield* _getElementPaths(props.children, depth - 1, keys);
144
+ yield* _getDescendantPaths(props.children, depth - 1, keys);
143
145
  }
144
146
  }
145
147
  export function mergeElements(a, b) {
package/util/path.d.ts CHANGED
@@ -2,7 +2,6 @@
2
2
  * Reuseable utilities for dealing with filesystem paths.
3
3
  */
4
4
  import type { AnyCaller } from "./function.js";
5
- import { type Segments } from "./index.js";
6
5
  import type { Nullish } from "./null.js";
7
6
  /** Absolute path string starts with `/` slash. */
8
7
  export type AbsolutePath = `/` | `/${string}`;
@@ -52,7 +51,13 @@ export declare function matchPathPrefix(target: PossiblePath, base: PossiblePath
52
51
  export declare function isPathActive(target: AbsolutePath, current: AbsolutePath): boolean;
53
52
  /** Is a target path proud (i.e. is the current path, or is a child of the current path)? */
54
53
  export declare function isPathProud(target: AbsolutePath, current: AbsolutePath): boolean;
55
- /** Get the "segments" in an absolute path. */
56
- export declare function splitAbsolutePath(path: AbsolutePath | Segments): Segments;
57
- /** Join a set of path segments to form an absolute path. */
58
- export declare function joinAbsolutePath(path: Segments | AbsolutePath): AbsolutePath;
54
+ /**
55
+ * Get the "segments" in an absolute path.
56
+ * - `splitAbsolutePath("/")` returns `[]` the root has no segments.
57
+ */
58
+ export declare function splitAbsolutePath(path: AbsolutePath | readonly string[]): readonly string[];
59
+ /**
60
+ * Join a set of path segments to form an absolute path.
61
+ * - `joinAbsolutePath([])` returns `"/"` — an empty segment list represents the root.
62
+ */
63
+ export declare function joinAbsolutePath(path: AbsolutePath | readonly string[]): AbsolutePath;
package/util/path.js CHANGED
@@ -74,11 +74,21 @@ export function isPathActive(target, current) {
74
74
  export function isPathProud(target, current) {
75
75
  return target === current || (target !== "/" && target.startsWith(`${current}/`));
76
76
  }
77
- /** Get the "segments" in an absolute path. */
77
+ /**
78
+ * Get the "segments" in an absolute path.
79
+ * - `splitAbsolutePath("/")` returns `[]` — the root has no segments.
80
+ */
78
81
  export function splitAbsolutePath(path) {
79
- return typeof path === "string" ? splitString(path.slice(1), "/", 1, undefined, splitAbsolutePath) : path;
82
+ if (typeof path !== "string")
83
+ return path;
84
+ if (path === "/")
85
+ return [];
86
+ return splitString(path.slice(1), "/", 1, undefined, splitAbsolutePath);
80
87
  }
81
- /** Join a set of path segments to form an absolute path. */
88
+ /**
89
+ * Join a set of path segments to form an absolute path.
90
+ * - `joinAbsolutePath([])` returns `"/"` — an empty segment list represents the root.
91
+ */
82
92
  export function joinAbsolutePath(path) {
83
- return typeof path === "string" ? path : `/${path.join("")}`;
93
+ return typeof path === "string" ? path : `/${path.join("/")}`;
84
94
  }