shelving 1.222.3 → 1.224.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/block/Card.module.css +11 -0
- package/ui/docs/DirectoryPage.d.ts +7 -1
- package/ui/docs/DirectoryPage.js +1 -4
- package/ui/docs/DirectoryPage.tsx +7 -4
- package/ui/docs/DocumentationPage.d.ts +7 -1
- package/ui/docs/DocumentationPage.js +1 -4
- package/ui/docs/DocumentationPage.test.tsx +2 -2
- package/ui/docs/DocumentationPage.tsx +7 -4
- package/ui/docs/FilePage.d.ts +7 -1
- package/ui/docs/FilePage.js +1 -4
- package/ui/docs/FilePage.tsx +7 -4
- package/ui/layout/SidebarLayout.d.ts +1 -1
- package/ui/layout/SidebarLayout.js +2 -2
- package/ui/layout/SidebarLayout.module.css +1 -1
- package/ui/layout/SidebarLayout.tsx +2 -2
- package/ui/menu/Menu.module.css +6 -4
- package/ui/misc/MetaContext.d.ts +9 -0
- package/ui/misc/MetaContext.js +12 -0
- package/ui/misc/MetaContext.test.tsx +43 -0
- package/ui/misc/MetaContext.tsx +14 -0
- package/ui/misc/StatusIcon.module.css +1 -1
- package/ui/tree/TreePage.d.ts +8 -3
- package/ui/tree/TreePage.js +2 -1
- package/ui/tree/TreePage.test.tsx +45 -0
- package/ui/tree/TreePage.tsx +9 -2
package/package.json
CHANGED
package/ui/block/Card.module.css
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
/* Opaque surface colour for content nested inside a card — slightly darker than `--color-surface`. */
|
|
3
|
+
--card-color-surface: color-mix(in srgb, var(--color-surface) 92%, black);
|
|
4
|
+
}
|
|
5
|
+
|
|
1
6
|
.card {
|
|
2
7
|
/* Box */
|
|
3
8
|
position: relative;
|
|
@@ -47,6 +52,12 @@
|
|
|
47
52
|
}
|
|
48
53
|
}
|
|
49
54
|
|
|
55
|
+
/* Content nested in a card uses the card's opaque surface colour, so translucent surfaces don't double up. */
|
|
56
|
+
/* `:where()` keeps specificity at zero so colour/status variant classes on descendants still win. */
|
|
57
|
+
:where(.card) * {
|
|
58
|
+
--color-surface: var(--card-color-surface);
|
|
59
|
+
}
|
|
60
|
+
|
|
50
61
|
.overlay {
|
|
51
62
|
/* Cover the entire card. */
|
|
52
63
|
position: absolute;
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
import type { ReactNode } from "react";
|
|
2
2
|
import type { DirectoryElementProps } from "../../util/element.js";
|
|
3
|
+
import type { AbsolutePath } from "../../util/path.js";
|
|
4
|
+
interface DirectoryPageProps extends DirectoryElementProps {
|
|
5
|
+
/** Site-root-relative path of this page — threaded down so child cards build correct hrefs. */
|
|
6
|
+
readonly path: AbsolutePath;
|
|
7
|
+
}
|
|
3
8
|
/** Page renderer for a `tree-directory` element — shows title, content, and child cards. */
|
|
4
|
-
export declare function DirectoryPage({ title, name, description, content, children }:
|
|
9
|
+
export declare function DirectoryPage({ path, title, name, description, content, children }: DirectoryPageProps): ReactNode;
|
|
10
|
+
export {};
|
package/ui/docs/DirectoryPage.js
CHANGED
|
@@ -2,12 +2,9 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { Prose } from "../block/Prose.js";
|
|
3
3
|
import { Title } from "../block/Title.js";
|
|
4
4
|
import { Markup } from "../misc/Markup.js";
|
|
5
|
-
import { requireMeta } from "../misc/MetaContext.js";
|
|
6
5
|
import { Page } from "../page/Page.js";
|
|
7
6
|
import { TreeCards } from "../tree/TreeCards.js";
|
|
8
7
|
/** Page renderer for a `tree-directory` element — shows title, content, and child cards. */
|
|
9
|
-
export function DirectoryPage({ title, name, description, content, children }) {
|
|
10
|
-
const { url } = requireMeta();
|
|
11
|
-
const path = url?.pathname ?? "/";
|
|
8
|
+
export function DirectoryPage({ path, title, name, description, content, children }) {
|
|
12
9
|
return (_jsxs(Page, { title: title ?? name, description: description, children: [_jsx(Title, { children: title ?? name }), content && (_jsx(Prose, { children: _jsx(Markup, { children: content }) })), _jsx(TreeCards, { path: path, children: children })] }));
|
|
13
10
|
}
|
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
import type { ReactNode } from "react";
|
|
2
2
|
import type { DirectoryElementProps } from "../../util/element.js";
|
|
3
|
+
import type { AbsolutePath } from "../../util/path.js";
|
|
3
4
|
import { Prose } from "../block/Prose.js";
|
|
4
5
|
import { Title } from "../block/Title.js";
|
|
5
6
|
import { Markup } from "../misc/Markup.js";
|
|
6
|
-
import { requireMeta } from "../misc/MetaContext.js";
|
|
7
7
|
import { Page } from "../page/Page.js";
|
|
8
8
|
import { TreeCards } from "../tree/TreeCards.js";
|
|
9
9
|
|
|
10
|
+
interface DirectoryPageProps extends DirectoryElementProps {
|
|
11
|
+
/** Site-root-relative path of this page — threaded down so child cards build correct hrefs. */
|
|
12
|
+
readonly path: AbsolutePath;
|
|
13
|
+
}
|
|
14
|
+
|
|
10
15
|
/** Page renderer for a `tree-directory` element — shows title, content, and child cards. */
|
|
11
|
-
export function DirectoryPage({ title, name, description, content, children }:
|
|
12
|
-
const { url } = requireMeta();
|
|
13
|
-
const path = url?.pathname ?? "/";
|
|
16
|
+
export function DirectoryPage({ path, title, name, description, content, children }: DirectoryPageProps): ReactNode {
|
|
14
17
|
return (
|
|
15
18
|
<Page title={title ?? name} description={description}>
|
|
16
19
|
<Title>{title ?? name}</Title>
|
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
import type { ReactNode } from "react";
|
|
2
2
|
import { type DocumentationElementProps } from "../../util/element.js";
|
|
3
|
+
import type { AbsolutePath } from "../../util/path.js";
|
|
4
|
+
interface DocumentationPageProps extends DocumentationElementProps {
|
|
5
|
+
/** Site-root-relative path of this page — threaded down so child cards build correct hrefs. */
|
|
6
|
+
readonly path: AbsolutePath;
|
|
7
|
+
}
|
|
3
8
|
/**
|
|
4
9
|
* Page renderer for a `tree-documentation` element (also used for `tree-file` elements, whose props are a compatible subset).
|
|
5
10
|
* - Renders title, signatures (one per overload), content, parameters, returns, throws, and examples.
|
|
6
11
|
* - Child symbols are grouped by `kind` into card sections (Functions, Classes, Methods, Properties, …), each under its own heading.
|
|
7
12
|
* - All sections are conditional — only render when they have entries.
|
|
8
13
|
*/
|
|
9
|
-
export declare function DocumentationPage({ title, name, kind, description, content, signatures, params, returns, throws, examples, children, }:
|
|
14
|
+
export declare function DocumentationPage({ path, title, name, kind, description, content, signatures, params, returns, throws, examples, children, }: DocumentationPageProps): ReactNode;
|
|
15
|
+
export {};
|
|
@@ -9,7 +9,6 @@ import { Section } from "../block/Section.js";
|
|
|
9
9
|
import { Title } from "../block/Title.js";
|
|
10
10
|
import { Code } from "../inline/Code.js";
|
|
11
11
|
import { Markup } from "../misc/Markup.js";
|
|
12
|
-
import { requireMeta } from "../misc/MetaContext.js";
|
|
13
12
|
import { Page } from "../page/Page.js";
|
|
14
13
|
import { TreeCards } from "../tree/TreeCards.js";
|
|
15
14
|
import { DocumentationKind } from "./DocumentationKind.js";
|
|
@@ -30,9 +29,7 @@ const KIND_SECTIONS = [
|
|
|
30
29
|
* - Child symbols are grouped by `kind` into card sections (Functions, Classes, Methods, Properties, …), each under its own heading.
|
|
31
30
|
* - All sections are conditional — only render when they have entries.
|
|
32
31
|
*/
|
|
33
|
-
export function DocumentationPage({ title, name, kind, description, content, signatures, params, returns, throws, examples, children, }) {
|
|
34
|
-
const { url } = requireMeta();
|
|
35
|
-
const path = (url?.pathname ?? "/");
|
|
32
|
+
export function DocumentationPage({ path, title, name, kind, description, content, signatures, params, returns, throws, examples, children, }) {
|
|
36
33
|
return (_jsxs(Page, { title: title ?? name, description: description, children: [_jsx(Title, { children: _jsxs(Flex, { left: true, wrap: true, children: [title ?? name, kind && _jsx(DocumentationKind, { kind: kind })] }) }), signatures?.map(sig => (_jsx(Preformatted, { children: sig }, sig))), content && (_jsx(Prose, { children: _jsx(Markup, { children: content }) })), params?.length && (_jsxs(Section, { children: [_jsx(Heading, { children: "Parameters" }), _jsx(Definitions, { row: true, children: params.map(({ name, type = DEFAULT_TYPE, description = "", optional }) => (_jsx(Definition, { term: _jsxs(_Fragment, { children: [_jsx(Code, { children: name }), ": ", _jsx(Code, { children: type }), optional && _jsx(_Fragment, { children: " (optional)" })] }), children: description }, `${name}-${type}-${description}`))) })] })), returns?.length && (_jsxs(Section, { children: [_jsx(Heading, { children: "Returns" }), _jsx(Definitions, { row: true, children: returns.map(({ type = DEFAULT_TYPE, description = "" }) => (_jsx(Definition, { term: _jsx(Code, { children: type }), children: description }, `${type}-${description}`))) })] })), throws?.length && (_jsxs(Section, { children: [_jsx(Heading, { children: "Throws" }), _jsx(Definitions, { row: true, children: throws.map(({ type = DEFAULT_TYPE, description = "" }) => (_jsx(Definition, { term: _jsx(Code, { children: type }), children: description }, `${type}-${description}`))) })] })), examples?.length && (_jsxs(Section, { children: [_jsx(Heading, { children: "Examples" }), examples.map(({ description }) => (_jsx(Preformatted, { children: description }, description)))] })), KIND_SECTIONS.map(([kind, label]) => {
|
|
37
34
|
// Pre-filter the children for this kind; only render the section when it has cards.
|
|
38
35
|
const group = Array.from(queryElements(children, { "props.kind": kind }));
|
|
@@ -11,7 +11,7 @@ function doc(name: string, kind: string): DocumentationElement {
|
|
|
11
11
|
describe("DocumentationPage", () => {
|
|
12
12
|
test("groups child symbols into kind-based sections, in order", () => {
|
|
13
13
|
const html = renderToStaticMarkup(
|
|
14
|
-
<DocumentationPage name="array">
|
|
14
|
+
<DocumentationPage path="/array" name="array">
|
|
15
15
|
{[doc("getThing", "function"), doc("Widget", "class"), doc("getOther", "function")]}
|
|
16
16
|
</DocumentationPage>,
|
|
17
17
|
);
|
|
@@ -23,7 +23,7 @@ describe("DocumentationPage", () => {
|
|
|
23
23
|
|
|
24
24
|
test("renders a section only for kinds that have children", () => {
|
|
25
25
|
const html = renderToStaticMarkup(
|
|
26
|
-
<DocumentationPage name="Store" kind="class">
|
|
26
|
+
<DocumentationPage path="/Store" name="Store" kind="class">
|
|
27
27
|
{[doc("get", "method"), doc("size", "property")]}
|
|
28
28
|
</DocumentationPage>,
|
|
29
29
|
);
|
|
@@ -11,7 +11,6 @@ import { Section } from "../block/Section.js";
|
|
|
11
11
|
import { Title } from "../block/Title.js";
|
|
12
12
|
import { Code } from "../inline/Code.js";
|
|
13
13
|
import { Markup } from "../misc/Markup.js";
|
|
14
|
-
import { requireMeta } from "../misc/MetaContext.js";
|
|
15
14
|
import { Page } from "../page/Page.js";
|
|
16
15
|
import { TreeCards } from "../tree/TreeCards.js";
|
|
17
16
|
import { DocumentationKind } from "./DocumentationKind.js";
|
|
@@ -29,6 +28,11 @@ const KIND_SECTIONS: ReadonlyArray<readonly [kind: string, label: string]> = [
|
|
|
29
28
|
["property", "Properties"],
|
|
30
29
|
];
|
|
31
30
|
|
|
31
|
+
interface DocumentationPageProps extends DocumentationElementProps {
|
|
32
|
+
/** Site-root-relative path of this page — threaded down so child cards build correct hrefs. */
|
|
33
|
+
readonly path: AbsolutePath;
|
|
34
|
+
}
|
|
35
|
+
|
|
32
36
|
/**
|
|
33
37
|
* Page renderer for a `tree-documentation` element (also used for `tree-file` elements, whose props are a compatible subset).
|
|
34
38
|
* - Renders title, signatures (one per overload), content, parameters, returns, throws, and examples.
|
|
@@ -36,6 +40,7 @@ const KIND_SECTIONS: ReadonlyArray<readonly [kind: string, label: string]> = [
|
|
|
36
40
|
* - All sections are conditional — only render when they have entries.
|
|
37
41
|
*/
|
|
38
42
|
export function DocumentationPage({
|
|
43
|
+
path,
|
|
39
44
|
title,
|
|
40
45
|
name,
|
|
41
46
|
kind,
|
|
@@ -47,9 +52,7 @@ export function DocumentationPage({
|
|
|
47
52
|
throws,
|
|
48
53
|
examples,
|
|
49
54
|
children,
|
|
50
|
-
}:
|
|
51
|
-
const { url } = requireMeta();
|
|
52
|
-
const path = (url?.pathname ?? "/") as AbsolutePath;
|
|
55
|
+
}: DocumentationPageProps): ReactNode {
|
|
53
56
|
return (
|
|
54
57
|
<Page title={title ?? name} description={description}>
|
|
55
58
|
<Title>
|
package/ui/docs/FilePage.d.ts
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
import type { ReactNode } from "react";
|
|
2
2
|
import type { FileElementProps } from "../../util/element.js";
|
|
3
|
+
import type { AbsolutePath } from "../../util/path.js";
|
|
4
|
+
interface FilePageProps extends FileElementProps {
|
|
5
|
+
/** Site-root-relative path of this page — threaded down so child cards build correct hrefs. */
|
|
6
|
+
readonly path: AbsolutePath;
|
|
7
|
+
}
|
|
3
8
|
/** Page renderer for a `tree-file` element — shows title, content, and child code symbols. */
|
|
4
|
-
export declare function FilePage({ title, name, description, content, children }:
|
|
9
|
+
export declare function FilePage({ path, title, name, description, content, children }: FilePageProps): ReactNode;
|
|
10
|
+
export {};
|
package/ui/docs/FilePage.js
CHANGED
|
@@ -2,12 +2,9 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { Prose } from "../block/Prose.js";
|
|
3
3
|
import { Title } from "../block/Title.js";
|
|
4
4
|
import { Markup } from "../misc/Markup.js";
|
|
5
|
-
import { requireMeta } from "../misc/MetaContext.js";
|
|
6
5
|
import { Page } from "../page/Page.js";
|
|
7
6
|
import { TreeCards } from "../tree/TreeCards.js";
|
|
8
7
|
/** Page renderer for a `tree-file` element — shows title, content, and child code symbols. */
|
|
9
|
-
export function FilePage({ title, name, description, content, children }) {
|
|
10
|
-
const { url } = requireMeta();
|
|
11
|
-
const path = url?.pathname ?? "/";
|
|
8
|
+
export function FilePage({ path, title, name, description, content, children }) {
|
|
12
9
|
return (_jsxs(Page, { title: title ?? name, description: description, children: [_jsx(Title, { children: title ?? name }), content && (_jsx(Prose, { children: _jsx(Markup, { children: content }) })), _jsx(TreeCards, { path: path, children: children })] }));
|
|
13
10
|
}
|
package/ui/docs/FilePage.tsx
CHANGED
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
import type { ReactNode } from "react";
|
|
2
2
|
import type { FileElementProps } from "../../util/element.js";
|
|
3
|
+
import type { AbsolutePath } from "../../util/path.js";
|
|
3
4
|
import { Prose } from "../block/Prose.js";
|
|
4
5
|
import { Title } from "../block/Title.js";
|
|
5
6
|
import { Markup } from "../misc/Markup.js";
|
|
6
|
-
import { requireMeta } from "../misc/MetaContext.js";
|
|
7
7
|
import { Page } from "../page/Page.js";
|
|
8
8
|
import { TreeCards } from "../tree/TreeCards.js";
|
|
9
9
|
|
|
10
|
+
interface FilePageProps extends FileElementProps {
|
|
11
|
+
/** Site-root-relative path of this page — threaded down so child cards build correct hrefs. */
|
|
12
|
+
readonly path: AbsolutePath;
|
|
13
|
+
}
|
|
14
|
+
|
|
10
15
|
/** Page renderer for a `tree-file` element — shows title, content, and child code symbols. */
|
|
11
|
-
export function FilePage({ title, name, description, content, children }:
|
|
12
|
-
const { url } = requireMeta();
|
|
13
|
-
const path = url?.pathname ?? "/";
|
|
16
|
+
export function FilePage({ path, title, name, description, content, children }: FilePageProps): ReactNode {
|
|
14
17
|
return (
|
|
15
18
|
<Page title={title ?? name} description={description}>
|
|
16
19
|
<Title>{title ?? name}</Title>
|
|
@@ -13,7 +13,7 @@ export interface SidebarLayoutProps extends OptionalChildProps {
|
|
|
13
13
|
* - On narrow viewports the sidebar becomes an off-canvas drawer toggled by a single menu button that switches between a burger and a close icon.
|
|
14
14
|
* - While the drawer is open an overlay dims the rest of the page; clicking the overlay closes the drawer.
|
|
15
15
|
* - Inside a `<Navigation>` the drawer closes itself whenever the route changes (e.g. tapping a sidebar link).
|
|
16
|
-
* - Use the `--sidebar-layout-width
|
|
16
|
+
* - Use the `--sidebar-layout-width`, `--sidebar-layout-bg`, `--sidebar-layout-border`, and `--sidebar-layout-color-border` custom properties to override defaults.
|
|
17
17
|
*/
|
|
18
18
|
export declare function SidebarLayout({ sidebar, children, right }: SidebarLayoutProps): ReactElement;
|
|
19
19
|
export { SIDEBAR_LAYOUT_CSS };
|
|
@@ -13,7 +13,7 @@ import SIDEBAR_LAYOUT_CSS from "./SidebarLayout.module.css";
|
|
|
13
13
|
* - On narrow viewports the sidebar becomes an off-canvas drawer toggled by a single menu button that switches between a burger and a close icon.
|
|
14
14
|
* - While the drawer is open an overlay dims the rest of the page; clicking the overlay closes the drawer.
|
|
15
15
|
* - Inside a `<Navigation>` the drawer closes itself whenever the route changes (e.g. tapping a sidebar link).
|
|
16
|
-
* - Use the `--sidebar-layout-width
|
|
16
|
+
* - Use the `--sidebar-layout-width`, `--sidebar-layout-bg`, `--sidebar-layout-border`, and `--sidebar-layout-color-border` custom properties to override defaults.
|
|
17
17
|
*/
|
|
18
18
|
export function SidebarLayout({ sidebar, children, right = false }) {
|
|
19
19
|
const [open, setOpen] = useState(false);
|
|
@@ -24,7 +24,7 @@ export function SidebarLayout({ sidebar, children, right = false }) {
|
|
|
24
24
|
setOpen(false);
|
|
25
25
|
}, [href]);
|
|
26
26
|
const sidebarEl = (_jsx("nav", { className: getClass(SIDEBAR_LAYOUT_CSS.sidebar, open && SIDEBAR_LAYOUT_CSS.open), children: sidebar }, "sidebar"));
|
|
27
|
-
const contentEl = (_jsxs("div", { className: getClass(LAYOUT_CSS.layout, SIDEBAR_LAYOUT_CSS.content), children: [_jsx("div", { className: SIDEBAR_LAYOUT_CSS.toggle, children: _jsx(Button, { fit: true,
|
|
27
|
+
const contentEl = (_jsxs("div", { className: getClass(LAYOUT_CSS.layout, SIDEBAR_LAYOUT_CSS.content), children: [_jsx("div", { className: SIDEBAR_LAYOUT_CSS.toggle, children: _jsx(Button, { fit: true, title: open ? "Close menu" : "Show menu", onClick: () => setOpen(o => !o), children: open ? _jsx(XMarkIcon, {}) : _jsx(Bars3Icon, {}) }) }), _jsx("div", { className: SIDEBAR_LAYOUT_CSS.contentInner, children: children })] }, "content"));
|
|
28
28
|
const overlayEl = open && (_jsx("button", { type: "button", className: SIDEBAR_LAYOUT_CSS.overlay, "aria-label": "Close menu", onClick: () => setOpen(false) }, "overlay"));
|
|
29
29
|
return (_jsx("main", { className: getClass(SIDEBAR_LAYOUT_CSS.main, LAYOUT_CSS.layout), children: right ? [contentEl, sidebarEl, overlayEl] : [sidebarEl, contentEl, overlayEl] }));
|
|
30
30
|
}
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
overscroll-behavior: contain;
|
|
25
25
|
padding: var(--space-normal);
|
|
26
26
|
background: var(--sidebar-layout-bg, var(--color-surface));
|
|
27
|
-
border-right: 1px solid var(--color-border);
|
|
27
|
+
border-right: var(--sidebar-layout-border, 1px) solid var(--sidebar-layout-color-border, var(--color-border));
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
.content {
|
|
@@ -21,7 +21,7 @@ export interface SidebarLayoutProps extends OptionalChildProps {
|
|
|
21
21
|
* - On narrow viewports the sidebar becomes an off-canvas drawer toggled by a single menu button that switches between a burger and a close icon.
|
|
22
22
|
* - While the drawer is open an overlay dims the rest of the page; clicking the overlay closes the drawer.
|
|
23
23
|
* - Inside a `<Navigation>` the drawer closes itself whenever the route changes (e.g. tapping a sidebar link).
|
|
24
|
-
* - Use the `--sidebar-layout-width
|
|
24
|
+
* - Use the `--sidebar-layout-width`, `--sidebar-layout-bg`, `--sidebar-layout-border`, and `--sidebar-layout-color-border` custom properties to override defaults.
|
|
25
25
|
*/
|
|
26
26
|
export function SidebarLayout({ sidebar, children, right = false }: SidebarLayoutProps): ReactElement {
|
|
27
27
|
const [open, setOpen] = useState(false);
|
|
@@ -40,7 +40,7 @@ export function SidebarLayout({ sidebar, children, right = false }: SidebarLayou
|
|
|
40
40
|
const contentEl = (
|
|
41
41
|
<div key="content" className={getClass(LAYOUT_CSS.layout, SIDEBAR_LAYOUT_CSS.content)}>
|
|
42
42
|
<div className={SIDEBAR_LAYOUT_CSS.toggle}>
|
|
43
|
-
<Button fit
|
|
43
|
+
<Button fit title={open ? "Close menu" : "Show menu"} onClick={() => setOpen(o => !o)}>
|
|
44
44
|
{open ? <XMarkIcon /> : <Bars3Icon />}
|
|
45
45
|
</Button>
|
|
46
46
|
</div>
|
package/ui/menu/Menu.module.css
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
font-family: var(--menu-font, var(--font-body));
|
|
12
12
|
font-size: var(--menu-size, var(--size-normal));
|
|
13
13
|
line-height: var(--menu-leading, var(--leading-normal));
|
|
14
|
-
color: var(--menu-color-text, var(--color-
|
|
14
|
+
color: var(--menu-color-text, var(--color-text));
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
.item {
|
|
@@ -41,15 +41,17 @@
|
|
|
41
41
|
outline-offset: 1px;
|
|
42
42
|
}
|
|
43
43
|
&[aria-current="page"] {
|
|
44
|
-
background: var(--menu-link-color-active-bg, var(--color-
|
|
44
|
+
background: var(--menu-link-color-active-bg, var(--color-white));
|
|
45
45
|
color: var(--menu-link-color-active-text, var(--color-text));
|
|
46
46
|
font-weight: var(--menu-link-active-weight, var(--weight-strong));
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
/* "Proud" item — an ancestor of the active page; emphasised
|
|
51
|
-
|
|
50
|
+
/* "Proud" item — an ancestor of the active page; emphasised with a strong weight, and reveals its nested children. */
|
|
51
|
+
/* The active page is excluded so its own `[aria-current]` styling wins instead of the proud styling. */
|
|
52
|
+
.proud > .link:not([aria-current="page"]) {
|
|
52
53
|
color: var(--menu-link-color-proud, var(--color-text));
|
|
54
|
+
font-weight: var(--menu-link-proud-weight, var(--weight-strong));
|
|
53
55
|
}
|
|
54
56
|
|
|
55
57
|
/* Submenu — Blockquote-style left border to signal hierarchy. */
|
package/ui/misc/MetaContext.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { AbsolutePath } from "../../util/path.js";
|
|
1
2
|
import { type URIParams } from "../../util/uri.js";
|
|
2
3
|
import { type Meta, type PossibleMeta } from "../util/meta.js";
|
|
3
4
|
import type { ChildProps } from "../util/props.js";
|
|
@@ -9,3 +10,11 @@ export interface MetaProps extends PossibleMeta, ChildProps {
|
|
|
9
10
|
export declare function requireMeta(meta?: PossibleMeta): Meta;
|
|
10
11
|
/** Get all URI/route params from the current meta context's URL. */
|
|
11
12
|
export declare function requireMetaParams(): URIParams;
|
|
13
|
+
/**
|
|
14
|
+
* Get the current page's path relative to the site root.
|
|
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.
|
|
17
|
+
*
|
|
18
|
+
* @returns The site-root-relative path (e.g. `/util/array`), or `undefined` if it can't be determined.
|
|
19
|
+
*/
|
|
20
|
+
export declare function getMetaPath(): AbsolutePath | undefined;
|
package/ui/misc/MetaContext.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createContext, use } from "react";
|
|
2
2
|
import { getURIParams } from "../../util/uri.js";
|
|
3
|
+
import { matchURLPrefix } from "../../util/url.js";
|
|
3
4
|
import { mergeMeta } from "../util/meta.js";
|
|
4
5
|
/** Context to store the `Config` object. */
|
|
5
6
|
export const MetaContext = createContext({});
|
|
@@ -13,3 +14,14 @@ export function requireMeta(meta) {
|
|
|
13
14
|
export function requireMetaParams() {
|
|
14
15
|
return getURIParams(requireMeta().url ?? {}, requireMetaParams);
|
|
15
16
|
}
|
|
17
|
+
/**
|
|
18
|
+
* Get the current page's path relative to the site root.
|
|
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.
|
|
21
|
+
*
|
|
22
|
+
* @returns The site-root-relative path (e.g. `/util/array`), or `undefined` if it can't be determined.
|
|
23
|
+
*/
|
|
24
|
+
export function getMetaPath() {
|
|
25
|
+
const { url, root } = requireMeta();
|
|
26
|
+
return matchURLPrefix(url, root, getMetaPath);
|
|
27
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
import { renderToStaticMarkup } from "react-dom/server";
|
|
4
|
+
import { createMeta } from "../util/meta.js";
|
|
5
|
+
import { getMetaPath, MetaContext } from "./MetaContext.js";
|
|
6
|
+
|
|
7
|
+
/** Render `getMetaPath()` from inside a component so its `use(MetaContext)` call is valid. */
|
|
8
|
+
function Probe(): ReactNode {
|
|
9
|
+
return getMetaPath() ?? "none";
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
describe("getMetaPath", () => {
|
|
13
|
+
test("returns the page path relative to the site root", () => {
|
|
14
|
+
const html = renderToStaticMarkup(
|
|
15
|
+
<MetaContext value={createMeta({ root: "http://x.com/sub/", url: "./util/array" })}>
|
|
16
|
+
<Probe />
|
|
17
|
+
</MetaContext>,
|
|
18
|
+
);
|
|
19
|
+
expect(html).toBe("/util/array");
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("returns `/` when url and root resolve to the same location", () => {
|
|
23
|
+
const html = renderToStaticMarkup(
|
|
24
|
+
<MetaContext value={createMeta({ root: "http://x.com/sub/", url: "./" })}>
|
|
25
|
+
<Probe />
|
|
26
|
+
</MetaContext>,
|
|
27
|
+
);
|
|
28
|
+
expect(html).toBe("/");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("returns undefined when url or root is unset", () => {
|
|
32
|
+
expect(renderToStaticMarkup(<Probe />)).toBe("none");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("returns undefined when url and root are on different origins", () => {
|
|
36
|
+
const html = renderToStaticMarkup(
|
|
37
|
+
<MetaContext value={createMeta({ root: "http://x.com/", url: "http://y.com/foo" })}>
|
|
38
|
+
<Probe />
|
|
39
|
+
</MetaContext>,
|
|
40
|
+
);
|
|
41
|
+
expect(html).toBe("none");
|
|
42
|
+
});
|
|
43
|
+
});
|
package/ui/misc/MetaContext.tsx
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { createContext, use } from "react";
|
|
2
|
+
import type { AbsolutePath } from "../../util/path.js";
|
|
2
3
|
import { getURIParams, type URIParams } from "../../util/uri.js";
|
|
4
|
+
import { matchURLPrefix } from "../../util/url.js";
|
|
3
5
|
import { type Meta, mergeMeta, type PossibleMeta } from "../util/meta.js";
|
|
4
6
|
import type { ChildProps } from "../util/props.js";
|
|
5
7
|
|
|
@@ -19,3 +21,15 @@ export function requireMeta(meta?: PossibleMeta): Meta {
|
|
|
19
21
|
export function requireMetaParams(): URIParams {
|
|
20
22
|
return getURIParams(requireMeta().url ?? {}, requireMetaParams);
|
|
21
23
|
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Get the current page's path relative to the site root.
|
|
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.
|
|
29
|
+
*
|
|
30
|
+
* @returns The site-root-relative path (e.g. `/util/array`), or `undefined` if it can't be determined.
|
|
31
|
+
*/
|
|
32
|
+
export function getMetaPath(): AbsolutePath | undefined {
|
|
33
|
+
const { url, root } = requireMeta();
|
|
34
|
+
return matchURLPrefix(url, root, getMetaPath);
|
|
35
|
+
}
|
package/ui/tree/TreePage.d.ts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import type { ReactNode } from "react";
|
|
2
2
|
import { type TreeElement } from "../../util/element.js";
|
|
3
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
|
+
}
|
|
4
9
|
/** Mapping + Mapper pair for tree pages — wrap children in `<TreePageMapping>` to override. */
|
|
5
|
-
export declare const TreePageMapping: import("react").FunctionComponent<import("../misc/Mapper.js").MappingProps<
|
|
6
|
-
readonly children?: import("../../util/element.js").Elements;
|
|
7
|
-
}>;
|
|
10
|
+
export declare const TreePageMapping: import("react").FunctionComponent<import("../misc/Mapper.js").MappingProps<TreePageExtras>>, TreePageMapper: import("react").FunctionComponent<import("../misc/Mapper.js").MapperProps<TreePageExtras>>;
|
|
8
11
|
export interface TreePageProps {
|
|
9
12
|
readonly path?: AbsolutePath;
|
|
10
13
|
readonly tree: TreeElement;
|
|
@@ -13,7 +16,9 @@ export interface TreePageProps {
|
|
|
13
16
|
* Resolve a URL path to a tree element and render it as a full page.
|
|
14
17
|
* - Walks the tree by matching each path segment to a descendant's `key` (via `resolveElementPath()`).
|
|
15
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.
|
|
16
20
|
* - Throws `NotFoundError` if no element matches at any level.
|
|
17
21
|
* - To override the renderer for a specific element type, wrap in `<TreePageMapping mapping={…}>`.
|
|
18
22
|
*/
|
|
19
23
|
export declare function TreePage({ path, tree }: TreePageProps): ReactNode;
|
|
24
|
+
export {};
|
package/ui/tree/TreePage.js
CHANGED
|
@@ -16,6 +16,7 @@ export const [TreePageMapping, TreePageMapper] = createMapper({
|
|
|
16
16
|
* Resolve a URL path to a tree element and render it as a full page.
|
|
17
17
|
* - Walks the tree by matching each path segment to a descendant's `key` (via `resolveElementPath()`).
|
|
18
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.
|
|
19
20
|
* - Throws `NotFoundError` if no element matches at any level.
|
|
20
21
|
* - To override the renderer for a specific element type, wrap in `<TreePageMapping mapping={…}>`.
|
|
21
22
|
*/
|
|
@@ -24,5 +25,5 @@ export function TreePage({ path = "/", tree }) {
|
|
|
24
25
|
const element = resolveElementPath(tree, segments);
|
|
25
26
|
if (!element)
|
|
26
27
|
throw new NotFoundError("Element not found", { received: path });
|
|
27
|
-
return _jsx(TreePageMapper, { children: element });
|
|
28
|
+
return _jsx(TreePageMapper, { path: path, children: element });
|
|
28
29
|
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { renderToStaticMarkup } from "react-dom/server";
|
|
3
|
+
import type { TreeElement } from "../../util/element.js";
|
|
4
|
+
import { MetaContext } from "../misc/MetaContext.js";
|
|
5
|
+
import { createMeta } from "../util/meta.js";
|
|
6
|
+
import { TreePage } from "./TreePage.js";
|
|
7
|
+
|
|
8
|
+
/** Minimal tree: root → `util` directory → `array` file. */
|
|
9
|
+
const tree: TreeElement = {
|
|
10
|
+
type: "tree-directory",
|
|
11
|
+
key: "root",
|
|
12
|
+
props: {
|
|
13
|
+
name: "shelving",
|
|
14
|
+
children: [
|
|
15
|
+
{
|
|
16
|
+
type: "tree-directory",
|
|
17
|
+
key: "util",
|
|
18
|
+
props: { name: "util", children: [{ type: "tree-file", key: "array", props: { name: "array" } }] },
|
|
19
|
+
},
|
|
20
|
+
],
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
describe("TreePage", () => {
|
|
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
|
+
test("card links include an APP_URL subfolder exactly once", () => {
|
|
27
|
+
const html = renderToStaticMarkup(
|
|
28
|
+
<MetaContext value={createMeta({ root: "http://x.com/sub/", url: "./util" })}>
|
|
29
|
+
<TreePage path="/util" tree={tree} />
|
|
30
|
+
</MetaContext>,
|
|
31
|
+
);
|
|
32
|
+
expect(html).toContain('href="http://x.com/sub/util/array"');
|
|
33
|
+
expect(html).not.toContain("/sub/sub/");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("root page card links include an APP_URL subfolder exactly once", () => {
|
|
37
|
+
const html = renderToStaticMarkup(
|
|
38
|
+
<MetaContext value={createMeta({ root: "http://x.com/sub/", url: "./" })}>
|
|
39
|
+
<TreePage path="/" tree={tree} />
|
|
40
|
+
</MetaContext>,
|
|
41
|
+
);
|
|
42
|
+
expect(html).toContain('href="http://x.com/sub/util"');
|
|
43
|
+
expect(html).not.toContain("/sub/sub/");
|
|
44
|
+
});
|
|
45
|
+
});
|
package/ui/tree/TreePage.tsx
CHANGED
|
@@ -7,8 +7,14 @@ import { DocumentationPage } from "../docs/DocumentationPage.js";
|
|
|
7
7
|
import { FilePage } from "../docs/FilePage.js";
|
|
8
8
|
import { createMapper } from "../misc/Mapper.js";
|
|
9
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
|
+
|
|
10
16
|
/** Mapping + Mapper pair for tree pages — wrap children in `<TreePageMapping>` to override. */
|
|
11
|
-
export const [TreePageMapping, TreePageMapper] = createMapper({
|
|
17
|
+
export const [TreePageMapping, TreePageMapper] = createMapper<TreePageExtras>({
|
|
12
18
|
"tree-directory": DirectoryPage,
|
|
13
19
|
"tree-file": FilePage,
|
|
14
20
|
"tree-documentation": DocumentationPage,
|
|
@@ -23,6 +29,7 @@ export interface TreePageProps {
|
|
|
23
29
|
* Resolve a URL path to a tree element and render it as a full page.
|
|
24
30
|
* - Walks the tree by matching each path segment to a descendant's `key` (via `resolveElementPath()`).
|
|
25
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.
|
|
26
33
|
* - Throws `NotFoundError` if no element matches at any level.
|
|
27
34
|
* - To override the renderer for a specific element type, wrap in `<TreePageMapping mapping={…}>`.
|
|
28
35
|
*/
|
|
@@ -30,5 +37,5 @@ export function TreePage({ path = "/", tree }: TreePageProps): ReactNode {
|
|
|
30
37
|
const segments = splitPath(path);
|
|
31
38
|
const element = resolveElementPath(tree, segments);
|
|
32
39
|
if (!element) throw new NotFoundError("Element not found", { received: path });
|
|
33
|
-
return <TreePageMapper>{element}</TreePageMapper>;
|
|
40
|
+
return <TreePageMapper path={path}>{element}</TreePageMapper>;
|
|
34
41
|
}
|