@opentf/web-docs 0.1.0 → 0.2.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/build/blog-posts-plugin.js +127 -0
- package/build/docs-nav-plugin.js +17 -10
- package/build/feed.js +78 -0
- package/build/index.js +4 -0
- package/build/last-updated-plugin.js +98 -0
- package/build/last-updated.js +45 -0
- package/build/pagefind.js +58 -0
- package/build/reading-time.js +16 -0
- package/components/BlogLayout.jsx +77 -0
- package/components/Breadcrumbs.jsx +30 -27
- package/components/Callout.jsx +7 -7
- package/components/Card.jsx +21 -0
- package/components/Cards.jsx +11 -0
- package/components/CodeBlock.jsx +39 -0
- package/components/DocsLayout.jsx +66 -6
- package/components/LastUpdated.jsx +22 -0
- package/components/NavIcon.jsx +34 -0
- package/components/Navbar.jsx +34 -22
- package/components/NavbarLink.jsx +33 -0
- package/components/Pagination.jsx +37 -33
- package/components/PostBanner.jsx +25 -0
- package/components/PostCard.jsx +19 -0
- package/components/PostList.jsx +17 -0
- package/components/PostMeta.jsx +28 -0
- package/components/ReadingTime.jsx +6 -0
- package/components/Search.jsx +211 -0
- package/components/SearchTrigger.jsx +14 -3
- package/components/Sidebar.jsx +111 -9
- package/components/SidebarNode.jsx +1 -0
- package/components/SidebarToggle.jsx +47 -0
- package/components/Steps.jsx +14 -0
- package/components/Tabs.jsx +4 -2
- package/components/ThemeToggle.jsx +150 -25
- package/components/Tooltip.jsx +19 -0
- package/components/format.js +11 -0
- package/config.js +25 -1
- package/index.js +29 -2
- package/nav-virtual.js +6 -4
- package/package.json +15 -1
- package/posts-virtual.js +17 -0
- package/theme/index.css +1034 -58
- package/updated-virtual.js +9 -0
- package/components/CodeGroup.jsx +0 -28
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// A single tile in a <Cards> grid: a title, an optional description, and a link. An
|
|
2
|
+
// internal `href` uses SPA navigation (<Link>); an `external` href is a plain anchor.
|
|
3
|
+
// `desc` (prop) or children supply the body.
|
|
4
|
+
|
|
5
|
+
import { Link } from "@opentf/web";
|
|
6
|
+
|
|
7
|
+
export default function Card(props) {
|
|
8
|
+
return props.external ? (
|
|
9
|
+
<a href={props.href} target="_blank" rel="noreferrer" class="otfw-card">
|
|
10
|
+
<span class="otfw-card-title">{props.title}</span>
|
|
11
|
+
{props.desc ? <span class="otfw-card-desc">{props.desc}</span> : null}
|
|
12
|
+
{props.children}
|
|
13
|
+
</a>
|
|
14
|
+
) : (
|
|
15
|
+
<Link href={props.href} class="otfw-card">
|
|
16
|
+
<span class="otfw-card-title">{props.title}</span>
|
|
17
|
+
{props.desc ? <span class="otfw-card-desc">{props.desc}</span> : null}
|
|
18
|
+
{props.children}
|
|
19
|
+
</Link>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// A responsive grid of <Card>s. Used on index/landing docs pages (e.g. the API
|
|
2
|
+
// reference overview) to present a set of links as tiles instead of a bullet list.
|
|
3
|
+
//
|
|
4
|
+
// import { Cards, Card } from "@opentf/web-docs";
|
|
5
|
+
// <Cards>
|
|
6
|
+
// <Card title="Core" href="/docs/api/core" desc="Runtime API." />
|
|
7
|
+
// </Cards>
|
|
8
|
+
|
|
9
|
+
export default function Cards(props) {
|
|
10
|
+
return <div class="otfw-cards">{props.children}</div>;
|
|
11
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// A code block matching the MDX front-end's `.otfw-code` structure (header bar with
|
|
2
|
+
// an optional language/filename label and a copy button, above a `<pre>`). Use it for
|
|
3
|
+
// code that doesn't come from a Markdown fence — e.g. a code panel inside `Tabs`:
|
|
4
|
+
// `{ label, content: <CodeBlock code="…" /> }`. The text is NOT syntax-highlighted —
|
|
5
|
+
// highlighting is a build-time (syntect) pass that only sees literal Markdown fences,
|
|
6
|
+
// not a runtime string prop; runtime highlighting here is deferred future work. The
|
|
7
|
+
// copy button owns its own behavior via `onclick` (see `copyWithFeedback`), so it
|
|
8
|
+
// works in any layout — not just the docs shell. The icon glyphs go through `RawHtml`
|
|
9
|
+
// because inline `<svg>` would be created in the HTML namespace and not render.
|
|
10
|
+
import { RawHtml, copyWithFeedback } from "@opentf/web";
|
|
11
|
+
|
|
12
|
+
const COPY_SVG =
|
|
13
|
+
'<svg class="otfw-copy-icon" viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="11" height="11" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>';
|
|
14
|
+
const CHECK_SVG =
|
|
15
|
+
'<svg class="otfw-check-icon" viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6 9 17l-5-5"/></svg>';
|
|
16
|
+
const ICONS = COPY_SVG + CHECK_SVG;
|
|
17
|
+
|
|
18
|
+
export default function CodeBlock(props) {
|
|
19
|
+
return (
|
|
20
|
+
<div class="otfw-code">
|
|
21
|
+
<div class="otfw-code-head">
|
|
22
|
+
{props.lang ? <span class="otfw-code-lang">{props.lang}</span> : null}
|
|
23
|
+
{props.name ? <span class="otfw-code-name">{props.name}</span> : null}
|
|
24
|
+
<button
|
|
25
|
+
class="otfw-copy"
|
|
26
|
+
type="button"
|
|
27
|
+
aria-label="Copy code"
|
|
28
|
+
onclick={(e) => copyWithFeedback(e.currentTarget, props.code)}
|
|
29
|
+
>
|
|
30
|
+
<RawHtml html={ICONS} />
|
|
31
|
+
<span class="otfw-copy-label">Copy</span>
|
|
32
|
+
</button>
|
|
33
|
+
</div>
|
|
34
|
+
<pre>
|
|
35
|
+
<code>{props.code}</code>
|
|
36
|
+
</pre>
|
|
37
|
+
</div>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
@@ -1,34 +1,78 @@
|
|
|
1
1
|
// Top-level documentation frame: navbar + (sidebar · content · TOC) + footer.
|
|
2
2
|
//
|
|
3
3
|
// import config from "../../otfw.config.js";
|
|
4
|
-
// import nav from "@opentf/web-docs/nav";
|
|
5
4
|
// export default function (props) {
|
|
6
|
-
// return <DocsLayout config={config.docs}
|
|
5
|
+
// return <DocsLayout config={config.docs}>{props.children}</DocsLayout>;
|
|
7
6
|
// }
|
|
8
7
|
//
|
|
8
|
+
// That's the whole thing — any folder becomes a documentation section just by giving it
|
|
9
|
+
// a layout that renders `DocsLayout`. The sidebar/breadcrumbs/prev-next come from the
|
|
10
|
+
// generated nav (`@opentf/web-docs/nav`, imported here), and the layout scopes them to
|
|
11
|
+
// the current section by route. So every section shares the same traits, and the docs
|
|
12
|
+
// and api layout files are identical. (Pass a `nav` prop only to override.)
|
|
13
|
+
//
|
|
9
14
|
// `frame` (default true) renders the full chrome (navbar + footer). Pass
|
|
10
15
|
// `frame={false}` when nesting inside an existing site layout that already provides
|
|
11
16
|
// the navbar/footer — only the sidebar · content · TOC grid is rendered.
|
|
12
17
|
|
|
18
|
+
import { router } from "@opentf/web";
|
|
19
|
+
import navMap from "@opentf/web-docs/nav";
|
|
20
|
+
import updated, { editPaths } from "@opentf/web-docs/updated";
|
|
21
|
+
|
|
13
22
|
import Navbar from "./Navbar.jsx";
|
|
14
23
|
import Sidebar from "./Sidebar.jsx";
|
|
15
24
|
import Toc from "./Toc.jsx";
|
|
16
25
|
import Footer from "./Footer.jsx";
|
|
17
26
|
import Breadcrumbs from "./Breadcrumbs.jsx";
|
|
18
27
|
import Pagination from "./Pagination.jsx";
|
|
28
|
+
import LastUpdated from "./LastUpdated.jsx";
|
|
29
|
+
|
|
30
|
+
// Code blocks own their copy behavior now: MDX fences compile to the
|
|
31
|
+
// `web-internal-code-block` built-in and `<CodeBlock>` wires `onclick` directly, so a
|
|
32
|
+
// copy button works in any layout (docs, blog, …) with no delegated listener here.
|
|
19
33
|
|
|
20
34
|
export default function DocsLayout(props) {
|
|
21
35
|
const config = props.config || {};
|
|
22
|
-
const nav = props.nav ||
|
|
36
|
+
const nav = props.nav || navMap;
|
|
23
37
|
const frame = props.frame !== false;
|
|
24
38
|
|
|
39
|
+
// The sidebar/breadcrumbs/prev-next operate on the *current section's* tree. `nav` is
|
|
40
|
+
// a section map keyed by base path; pick the branch whose base prefixes the route
|
|
41
|
+
// (longest match wins). A bare array means a single, unscoped section. Each section is
|
|
42
|
+
// its own layout instance, so resolving once at mount is correct.
|
|
43
|
+
const sectionNav = pickSection(nav, router.pathname);
|
|
44
|
+
|
|
45
|
+
// Last-updated time for the current page, looked up in the build-time map keyed by
|
|
46
|
+
// route (same exact-path match Pagination uses). `$derived` so it tracks navigation.
|
|
47
|
+
const lastUpdated = $derived(updated[router.pathname]);
|
|
48
|
+
// "Edit this page" URL: <repoUrl>/edit/main/<repo-relative file>, when both the repo
|
|
49
|
+
// is configured and the page's source path is known.
|
|
50
|
+
const editUrl = $derived(
|
|
51
|
+
config.repoUrl && editPaths[router.pathname]
|
|
52
|
+
? `${config.repoUrl.replace(/\/+$/, "")}/edit/main/${editPaths[router.pathname]}`
|
|
53
|
+
: null,
|
|
54
|
+
);
|
|
55
|
+
|
|
25
56
|
const body = (
|
|
26
57
|
<div class="otfw-docs">
|
|
27
|
-
<Sidebar nav={
|
|
58
|
+
<Sidebar nav={sectionNav} config={config} />
|
|
28
59
|
<main id="otfw-content" class="otfw-content" data-pagefind-body>
|
|
29
|
-
<Breadcrumbs nav={
|
|
60
|
+
<Breadcrumbs nav={sectionNav} />
|
|
30
61
|
<article class="otfw-prose">{props.children}</article>
|
|
31
|
-
|
|
62
|
+
{lastUpdated || editUrl ? (
|
|
63
|
+
<div class="otfw-page-meta">
|
|
64
|
+
{lastUpdated ? <LastUpdated date={lastUpdated} /> : null}
|
|
65
|
+
{editUrl ? (
|
|
66
|
+
<a class="otfw-edit-page" href={editUrl} target="_blank" rel="noreferrer">
|
|
67
|
+
Edit this page
|
|
68
|
+
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
69
|
+
<path d="M7 17 17 7M9 7h8v8" />
|
|
70
|
+
</svg>
|
|
71
|
+
</a>
|
|
72
|
+
) : null}
|
|
73
|
+
</div>
|
|
74
|
+
) : null}
|
|
75
|
+
<Pagination nav={sectionNav} />
|
|
32
76
|
</main>
|
|
33
77
|
<Toc />
|
|
34
78
|
</div>
|
|
@@ -44,3 +88,19 @@ export default function DocsLayout(props) {
|
|
|
44
88
|
body
|
|
45
89
|
);
|
|
46
90
|
}
|
|
91
|
+
|
|
92
|
+
// Resolve the section tree for `pathname` from a `{ [base]: tree }` map. Longest base
|
|
93
|
+
// prefix wins ("/" matches anything). Tolerates a bare array (single section).
|
|
94
|
+
function pickSection(nav, pathname) {
|
|
95
|
+
if (Array.isArray(nav)) return nav;
|
|
96
|
+
let best = null;
|
|
97
|
+
let bestLen = -1;
|
|
98
|
+
for (const base in nav) {
|
|
99
|
+
const match = base === "/" ? true : pathname === base || pathname.startsWith(base + "/");
|
|
100
|
+
if (match && base.length > bestLen) {
|
|
101
|
+
best = nav[base];
|
|
102
|
+
bestLen = base.length;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return best || nav[Object.keys(nav)[0]] || [];
|
|
106
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// "Last updated on <date>" line. Presentational — pass an ISO-8601 (or any
|
|
2
|
+
// Date-parseable) `date`; renders nothing when it's absent. The date itself comes from
|
|
3
|
+
// the build-time `@opentf/web-docs/updated` map (git commit time or a frontmatter
|
|
4
|
+
// override); layouts look up the current route there and pass it in.
|
|
5
|
+
//
|
|
6
|
+
// import updated from "@opentf/web-docs/updated";
|
|
7
|
+
// <LastUpdated date={updated[router.pathname]} />
|
|
8
|
+
import { formatDate } from "./format.js";
|
|
9
|
+
|
|
10
|
+
export default function LastUpdated(props) {
|
|
11
|
+
const date = props.date;
|
|
12
|
+
if (!date) return null;
|
|
13
|
+
const label = props.label || "Last updated on";
|
|
14
|
+
return (
|
|
15
|
+
<div class="otfw-last-updated">
|
|
16
|
+
{label}{" "}
|
|
17
|
+
<time class="otfw-last-updated-time" datetime={String(date)}>
|
|
18
|
+
{formatDate(date)}
|
|
19
|
+
</time>
|
|
20
|
+
</div>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// Named SVG icons for navbar links and actions. A nav item references an icon by
|
|
2
|
+
// name — `nav: [{ label, href, icon: "book" }]` — so the markup stays in the theme
|
|
3
|
+
// package rather than in the consumer's plain-JS config (which can't hold JSX). Add
|
|
4
|
+
// a case here to extend the set.
|
|
5
|
+
|
|
6
|
+
export default function NavIcon(props) {
|
|
7
|
+
const name = props.name;
|
|
8
|
+
|
|
9
|
+
return (
|
|
10
|
+
<span class="otfw-nav-icon" aria-hidden="true">
|
|
11
|
+
{name === "github" ? (
|
|
12
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor">
|
|
13
|
+
<path d="M12 .5C5.7.5.5 5.7.5 12c0 5.1 3.3 9.4 7.9 10.9.6.1.8-.2.8-.5v-1.7c-3.2.7-3.9-1.5-3.9-1.5-.5-1.3-1.3-1.7-1.3-1.7-1.1-.7.1-.7.1-.7 1.2.1 1.8 1.2 1.8 1.2 1 1.8 2.7 1.3 3.4 1 .1-.8.4-1.3.7-1.6-2.6-.3-5.3-1.3-5.3-5.7 0-1.3.5-2.3 1.2-3.1-.1-.3-.5-1.5.1-3.1 0 0 1-.3 3.3 1.2a11.5 11.5 0 016 0C17 4.7 18 5 18 5c.6 1.6.2 2.8.1 3.1.8.8 1.2 1.8 1.2 3.1 0 4.4-2.7 5.4-5.3 5.7.4.4.8 1.1.8 2.2v3.3c0 .3.2.6.8.5a11.5 11.5 0 007.9-10.9C23.5 5.7 18.3.5 12 .5z" />
|
|
14
|
+
</svg>
|
|
15
|
+
) : null}
|
|
16
|
+
{name === "discord" ? (
|
|
17
|
+
<svg width="19" height="19" viewBox="0 0 24 24" fill="currentColor">
|
|
18
|
+
<path d="M20.3 4.4A19.8 19.8 0 0 0 15.4 3l-.2.4a18 18 0 0 1 4.3 1.4 16.6 16.6 0 0 0-14.9 0A18 18 0 0 1 8.8 3.4L8.6 3a19.8 19.8 0 0 0-4.9 1.4C.6 9 0 13.5.3 17.9a19.9 19.9 0 0 0 6 3l.7-1.2a13 13 0 0 1-2-1l.5-.4a14.2 14.2 0 0 0 12 0l.5.4a13 13 0 0 1-2 1l.7 1.2a19.9 19.9 0 0 0 6-3c.4-5.1-.6-9.6-2.6-13.5ZM8.3 15.3c-1.2 0-2.2-1.1-2.2-2.4s1-2.5 2.2-2.5 2.2 1.1 2.2 2.5-1 2.4-2.2 2.4Zm7.4 0c-1.2 0-2.2-1.1-2.2-2.4s1-2.5 2.2-2.5 2.2 1.1 2.2 2.5-1 2.4-2.2 2.4Z" />
|
|
19
|
+
</svg>
|
|
20
|
+
) : null}
|
|
21
|
+
{name === "book" ? (
|
|
22
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
23
|
+
<path d="M4 4.5A2.5 2.5 0 0 1 6.5 2H20v18H6.5A2.5 2.5 0 0 0 4 22.5z" />
|
|
24
|
+
<path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20" />
|
|
25
|
+
</svg>
|
|
26
|
+
) : null}
|
|
27
|
+
{name === "external" ? (
|
|
28
|
+
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
|
|
29
|
+
<path d="M7 17 17 7M9 7h8v8" />
|
|
30
|
+
</svg>
|
|
31
|
+
) : null}
|
|
32
|
+
</span>
|
|
33
|
+
);
|
|
34
|
+
}
|
package/components/Navbar.jsx
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
// Top navigation bar: brand, top-level links, search, GitHub, theme toggle. All
|
|
2
2
|
// content is driven by the `docs` config object passed down from DocsLayout.
|
|
3
|
+
// Top-level links support a per-item `icon` (NavIcon name) and `external` flag, and
|
|
4
|
+
// the active route is underlined (see NavbarLink). The search trigger only renders
|
|
5
|
+
// when a search provider is configured.
|
|
3
6
|
|
|
4
|
-
import { Link } from "@opentf/web";
|
|
5
7
|
import ThemeToggle from "./ThemeToggle.jsx";
|
|
6
8
|
import SearchTrigger from "./SearchTrigger.jsx";
|
|
9
|
+
import Search from "./Search.jsx";
|
|
10
|
+
import SidebarToggle from "./SidebarToggle.jsx";
|
|
11
|
+
import NavbarLink from "./NavbarLink.jsx";
|
|
12
|
+
import NavIcon from "./NavIcon.jsx";
|
|
13
|
+
import { Link } from "@opentf/web";
|
|
7
14
|
|
|
8
15
|
export default function Navbar(props) {
|
|
9
16
|
const config = props.config || {};
|
|
@@ -12,29 +19,34 @@ export default function Navbar(props) {
|
|
|
12
19
|
return (
|
|
13
20
|
<header class="otfw-navbar">
|
|
14
21
|
<div class="otfw-navbar-inner">
|
|
15
|
-
<
|
|
16
|
-
|
|
17
|
-
<
|
|
18
|
-
|
|
22
|
+
<div class="otfw-navbar-lead">
|
|
23
|
+
<SidebarToggle />
|
|
24
|
+
<Link href={config.homeUrl || "/"} class="otfw-navbar-brand">
|
|
25
|
+
{config.logo ? <img src={config.logo} alt="" class="otfw-navbar-logo" /> : null}
|
|
26
|
+
<span class="otfw-navbar-title">{config.title || "Docs"}</span>
|
|
27
|
+
{config.version ? <span class="otfw-navbar-version">{config.version}</span> : null}
|
|
28
|
+
</Link>
|
|
29
|
+
</div>
|
|
19
30
|
|
|
20
|
-
<
|
|
21
|
-
{
|
|
22
|
-
|
|
23
|
-
{l.label}
|
|
24
|
-
</Link>
|
|
25
|
-
))}
|
|
26
|
-
</nav>
|
|
31
|
+
<div class="otfw-navbar-search">
|
|
32
|
+
{config.search ? <SearchTrigger /> : null}
|
|
33
|
+
</div>
|
|
27
34
|
|
|
28
|
-
<div class="otfw-navbar-
|
|
29
|
-
<
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
35
|
+
<div class="otfw-navbar-right">
|
|
36
|
+
<nav class="otfw-navbar-nav">
|
|
37
|
+
{links.map((l) => (
|
|
38
|
+
<NavbarLink link={l} />
|
|
39
|
+
))}
|
|
40
|
+
</nav>
|
|
41
|
+
<div class="otfw-navbar-actions">
|
|
42
|
+
{config.github ? (
|
|
43
|
+
<a href={config.github} target="_blank" rel="noreferrer" class="otfw-navbar-icon" aria-label="GitHub">
|
|
44
|
+
<NavIcon name="github" />
|
|
45
|
+
</a>
|
|
46
|
+
) : null}
|
|
47
|
+
<ThemeToggle />
|
|
48
|
+
</div>
|
|
49
|
+
{config.search ? <Search /> : null}
|
|
38
50
|
</div>
|
|
39
51
|
</div>
|
|
40
52
|
</header>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// One top-level navbar link. Its own component (like SidebarNode) so the active
|
|
2
|
+
// state is a reactive binding in the element's own build — it updates on SPA
|
|
3
|
+
// navigation without re-rendering the whole navbar. `external` links render as a
|
|
4
|
+
// plain anchor (no SPA nav, no active state); internal links get the accent
|
|
5
|
+
// underline when the current route matches (exactly, or as a section prefix).
|
|
6
|
+
|
|
7
|
+
import { Link, router } from "@opentf/web";
|
|
8
|
+
import NavIcon from "./NavIcon.jsx";
|
|
9
|
+
|
|
10
|
+
export default function NavbarLink(props) {
|
|
11
|
+
const link = props.link || {};
|
|
12
|
+
|
|
13
|
+
return link.external ? (
|
|
14
|
+
<a href={link.href} target="_blank" rel="noreferrer" class="otfw-navbar-link">
|
|
15
|
+
{link.icon ? <NavIcon name={link.icon} /> : null}
|
|
16
|
+
{link.label}
|
|
17
|
+
</a>
|
|
18
|
+
) : (
|
|
19
|
+
<Link
|
|
20
|
+
href={link.href}
|
|
21
|
+
class={
|
|
22
|
+
(link.href === "/"
|
|
23
|
+
? router.pathname === "/"
|
|
24
|
+
: router.pathname === link.href || router.pathname.startsWith(link.href + "/"))
|
|
25
|
+
? "otfw-navbar-link otfw-active"
|
|
26
|
+
: "otfw-navbar-link"
|
|
27
|
+
}
|
|
28
|
+
>
|
|
29
|
+
{link.icon ? <NavIcon name={link.icon} /> : null}
|
|
30
|
+
{link.label}
|
|
31
|
+
</Link>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
@@ -5,41 +5,45 @@ import { Link, router } from "@opentf/web";
|
|
|
5
5
|
export default function Pagination(props) {
|
|
6
6
|
const nav = props.nav || [];
|
|
7
7
|
|
|
8
|
+
const flat = [];
|
|
9
|
+
const walk = (items) => {
|
|
10
|
+
for (const it of items) {
|
|
11
|
+
if (it.path) flat.push(it);
|
|
12
|
+
if (it.items) walk(it.items);
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
walk(nav);
|
|
16
|
+
|
|
17
|
+
const adjacent = (path) => {
|
|
18
|
+
const i = flat.findIndex((x) => x.path === path);
|
|
19
|
+
return {
|
|
20
|
+
prev: i > 0 ? flat[i - 1] : null,
|
|
21
|
+
next: i >= 0 && i < flat.length - 1 ? flat[i + 1] : null,
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// A single reactive signal (not a `{() => …}` thunk): `pos` is in scope for the
|
|
26
|
+
// hoisted branch builders, and recomputes on `router.pathname`.
|
|
27
|
+
const pos = $derived(adjacent(router.pathname));
|
|
28
|
+
|
|
8
29
|
return (
|
|
9
30
|
<nav class="otfw-pagination" aria-label="Pagination">
|
|
10
|
-
{
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
<span class="otfw-page-dir">← Previous</span>
|
|
27
|
-
<span class="otfw-page-title">{prev.title}</span>
|
|
28
|
-
</Link>
|
|
29
|
-
) : (
|
|
30
|
-
<span class="otfw-page-spacer" />
|
|
31
|
-
)}
|
|
32
|
-
{next ? (
|
|
33
|
-
<Link href={next.path} class="otfw-page-link otfw-page-next">
|
|
34
|
-
<span class="otfw-page-dir">Next →</span>
|
|
35
|
-
<span class="otfw-page-title">{next.title}</span>
|
|
36
|
-
</Link>
|
|
37
|
-
) : (
|
|
38
|
-
<span class="otfw-page-spacer" />
|
|
39
|
-
)}
|
|
40
|
-
</>
|
|
41
|
-
);
|
|
42
|
-
}}
|
|
31
|
+
{pos.prev ? (
|
|
32
|
+
<Link href={pos.prev?.path} class="otfw-page-link otfw-page-prev">
|
|
33
|
+
<span class="otfw-page-dir">← Previous</span>
|
|
34
|
+
<span class="otfw-page-title">{pos.prev?.title}</span>
|
|
35
|
+
</Link>
|
|
36
|
+
) : (
|
|
37
|
+
<span class="otfw-page-spacer" />
|
|
38
|
+
)}
|
|
39
|
+
{pos.next ? (
|
|
40
|
+
<Link href={pos.next?.path} class="otfw-page-link otfw-page-next">
|
|
41
|
+
<span class="otfw-page-dir">Next →</span>
|
|
42
|
+
<span class="otfw-page-title">{pos.next?.title}</span>
|
|
43
|
+
</Link>
|
|
44
|
+
) : (
|
|
45
|
+
<span class="otfw-page-spacer" />
|
|
46
|
+
)}
|
|
43
47
|
</nav>
|
|
44
48
|
);
|
|
45
49
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// A blog post's header: title, optional description, and the metadata row. Rendered
|
|
2
|
+
// by BlogLayout above the post's prose body, driven by the post record so the MDX
|
|
3
|
+
// file stays pure content (no hand-written title/date in the body).
|
|
4
|
+
import PostMeta from "./PostMeta.jsx";
|
|
5
|
+
|
|
6
|
+
export default function PostBanner(props) {
|
|
7
|
+
const post = props.post || props;
|
|
8
|
+
return (
|
|
9
|
+
<header class="otfw-post-banner">
|
|
10
|
+
{post.cover ? (
|
|
11
|
+
<img class="otfw-post-cover" src={post.cover} alt={post.title} />
|
|
12
|
+
) : null}
|
|
13
|
+
<h1 class="otfw-post-title">{post.title}</h1>
|
|
14
|
+
{post.description ? <p class="otfw-post-desc">{post.description}</p> : null}
|
|
15
|
+
<PostMeta post={post} />
|
|
16
|
+
{post.tags && post.tags.length ? (
|
|
17
|
+
<div class="otfw-post-tags">
|
|
18
|
+
{post.tags.map((tag) => (
|
|
19
|
+
<span class="otfw-post-tag">{tag}</span>
|
|
20
|
+
))}
|
|
21
|
+
</div>
|
|
22
|
+
) : null}
|
|
23
|
+
</header>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// One post in the blog index: a link to the post with its meta + title + excerpt.
|
|
2
|
+
import { Link } from "@opentf/web";
|
|
3
|
+
import PostMeta from "./PostMeta.jsx";
|
|
4
|
+
|
|
5
|
+
export default function PostCard(props) {
|
|
6
|
+
const post = props.post;
|
|
7
|
+
return (
|
|
8
|
+
<Link href={post.path} class={post.cover ? "otfw-post-card otfw-has-cover" : "otfw-post-card"}>
|
|
9
|
+
{post.cover ? (
|
|
10
|
+
<img class="otfw-post-card-cover" src={post.cover} alt={post.title} loading="lazy" />
|
|
11
|
+
) : null}
|
|
12
|
+
<div class="otfw-post-card-body">
|
|
13
|
+
<PostMeta post={post} />
|
|
14
|
+
<div class="otfw-post-card-title">{post.title}</div>
|
|
15
|
+
{post.description ? <p class="otfw-post-card-desc">{post.description}</p> : null}
|
|
16
|
+
</div>
|
|
17
|
+
</Link>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// The blog index listing: renders a PostCard per post. Pass the list from
|
|
2
|
+
// `@opentf/web-docs/posts`:
|
|
3
|
+
//
|
|
4
|
+
// import { posts } from "@opentf/web-docs/posts";
|
|
5
|
+
// <PostList posts={posts} />
|
|
6
|
+
import PostCard from "./PostCard.jsx";
|
|
7
|
+
|
|
8
|
+
export default function PostList(props) {
|
|
9
|
+
const posts = props.posts || [];
|
|
10
|
+
return (
|
|
11
|
+
<div class="otfw-post-list">
|
|
12
|
+
{posts.length
|
|
13
|
+
? posts.map((post) => <PostCard post={post} />)
|
|
14
|
+
: <p class="otfw-post-empty">No posts yet.</p>}
|
|
15
|
+
</div>
|
|
16
|
+
);
|
|
17
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// A post's metadata row: date · author · reading time. Pass a post record (from
|
|
2
|
+
// `@opentf/web-docs/posts`) as `post`, or the fields directly. Missing fields are
|
|
3
|
+
// omitted. Used in both the post banner and the index cards.
|
|
4
|
+
import ReadingTime from "./ReadingTime.jsx";
|
|
5
|
+
import { formatDate } from "./format.js";
|
|
6
|
+
|
|
7
|
+
export default function PostMeta(props) {
|
|
8
|
+
const post = props.post || props;
|
|
9
|
+
return (
|
|
10
|
+
<div class="otfw-post-meta">
|
|
11
|
+
{post.author ? (
|
|
12
|
+
<span class="otfw-post-author">
|
|
13
|
+
{post.authorAvatar ? (
|
|
14
|
+
<img class="otfw-post-avatar" src={post.authorAvatar} alt={post.author} loading="lazy" />
|
|
15
|
+
) : null}
|
|
16
|
+
<span class="otfw-post-author-name">{post.author}</span>
|
|
17
|
+
{post.authorRole ? <span class="otfw-post-author-role">{post.authorRole}</span> : null}
|
|
18
|
+
</span>
|
|
19
|
+
) : null}
|
|
20
|
+
{post.date ? (
|
|
21
|
+
<time class="otfw-post-date" datetime={String(post.date)}>
|
|
22
|
+
{formatDate(post.date)}
|
|
23
|
+
</time>
|
|
24
|
+
) : null}
|
|
25
|
+
{post.readingTime ? <ReadingTime minutes={post.readingTime} /> : null}
|
|
26
|
+
</div>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// Reading-time label, e.g. "4 min read". `minutes` is precomputed at build time by
|
|
2
|
+
// the blog posts plugin (see build/reading-time.js).
|
|
3
|
+
export default function ReadingTime(props) {
|
|
4
|
+
const minutes = props.minutes ?? 1;
|
|
5
|
+
return <span class="otfw-reading-time">{minutes} min read</span>;
|
|
6
|
+
}
|