@agentvibes/site 0.0.0-stage → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +4 -2
  3. package/bin/agentvibes-site.mjs +7 -0
  4. package/package.json +87 -3
  5. package/src/app-mode.tsx +110 -0
  6. package/src/catalog.ts +80 -0
  7. package/src/cli.ts +51 -0
  8. package/src/client.ts +5 -0
  9. package/src/components/changelog-entry-view.tsx +11 -0
  10. package/src/components/doctor-throw/doctor-throw.tsx +15 -0
  11. package/src/components/errors-init.tsx +14 -0
  12. package/src/components/hero/hero-action-link.tsx +15 -0
  13. package/src/components/hero/hero.tsx +31 -0
  14. package/src/components/home-shell.tsx +19 -0
  15. package/src/components/jsdoc-view/group-view.tsx +14 -0
  16. package/src/components/jsdoc-view/item-view.tsx +24 -0
  17. package/src/components/jsdoc-view/jsdoc-view.tsx +16 -0
  18. package/src/components/jsdoc-view/member-view.tsx +15 -0
  19. package/src/components/module-cards/module-card.tsx +13 -0
  20. package/src/components/module-cards/module-cards.tsx +15 -0
  21. package/src/components/page-actions/copy-button.tsx +40 -0
  22. package/src/components/page-actions/page-actions.tsx +53 -0
  23. package/src/components/page-footer.tsx +20 -0
  24. package/src/components/page-link/page-link.tsx +19 -0
  25. package/src/components/site-error-boundary/error-panel.tsx +16 -0
  26. package/src/components/site-error-boundary/site-error-boundary.tsx +12 -0
  27. package/src/components/site-error-boundary/site-global-error-boundary.tsx +19 -0
  28. package/src/components/site-footer/footer-column-view.tsx +15 -0
  29. package/src/components/site-footer/footer-link.tsx +20 -0
  30. package/src/components/site-footer/site-footer.tsx +32 -0
  31. package/src/components/site-video/site-video.tsx +25 -0
  32. package/src/components/telemetry-scripts.tsx +18 -0
  33. package/src/config.ts +9 -0
  34. package/src/context.ts +68 -0
  35. package/src/data.ts +64 -0
  36. package/src/docs-title.ts +8 -0
  37. package/src/doctor-routes.tsx +44 -0
  38. package/src/index.ts +39 -0
  39. package/src/instrumentation.ts +53 -0
  40. package/src/jsdoc.ts +38 -0
  41. package/src/layout-options.ts +18 -0
  42. package/src/layouts.tsx +63 -0
  43. package/src/mdx-config.d.ts +2 -0
  44. package/src/mdx-config.js +7 -0
  45. package/src/mdx.tsx +18 -0
  46. package/src/nav.ts +97 -0
  47. package/src/og-route.tsx +29 -0
  48. package/src/output.ts +90 -0
  49. package/src/pages.tsx +186 -0
  50. package/src/prepare/assets.ts +98 -0
  51. package/src/prepare/author-next-config.ts +46 -0
  52. package/src/prepare/changelog.ts +17 -0
  53. package/src/prepare/files.ts +95 -0
  54. package/src/prepare/finish-static.ts +122 -0
  55. package/src/prepare/index.ts +291 -0
  56. package/src/prepare/jsdoc-extract.ts +182 -0
  57. package/src/prepare/release.ts +12 -0
  58. package/src/prepare/templates.ts +415 -0
  59. package/src/prepare/unimplemented.ts +32 -0
  60. package/src/proxy.ts +57 -0
  61. package/src/routes.ts +158 -0
  62. package/src/runtime.tsx +165 -0
  63. package/src/theme.ts +59 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Andrey Yatsyk
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,5 @@
1
- # Temporary Holding Version
1
+ # @agentvibes/site
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The Site Factory renderer: a site keeps only `site.config.ts` and `content/**/*.mdx`, and this package supplies the Next.js 16 and Fumadocs routes, layouts, header and footer, search, changelog and API pages. The command `agentvibes-site prepare` writes the generated files; run it before `next build`.
4
+
5
+ MIT licence.
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ // agentvibes-site — `prepare` generates the Next app of a Site Factory site
3
+ // from site.config.ts (run it before `next build` / `next dev`).
4
+ import { tsImport } from "tsx/esm/api";
5
+
6
+ const cli = await tsImport("../src/cli.ts", import.meta.url);
7
+ process.exitCode = await cli.main(process.argv.slice(2));
package/package.json CHANGED
@@ -1,6 +1,90 @@
1
1
  {
2
2
  "name": "@agentvibes/site",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.10.0",
4
+ "description": "Site Factory renderer: a Next 16 + Fumadocs site generated from site.config.ts and MDX (`agentvibes-site prepare`).",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "engines": {
8
+ "node": ">=20"
9
+ },
10
+ "exports": {
11
+ ".": {
12
+ "types": "./src/index.ts",
13
+ "default": "./src/index.ts"
14
+ },
15
+ "./proxy": {
16
+ "types": "./src/proxy.ts",
17
+ "default": "./src/proxy.ts"
18
+ },
19
+ "./client": {
20
+ "types": "./src/client.ts",
21
+ "default": "./src/client.ts"
22
+ },
23
+ "./mdx-config": {
24
+ "types": "./src/mdx-config.d.ts",
25
+ "default": "./src/mdx-config.js"
26
+ },
27
+ "./instrumentation": {
28
+ "types": "./src/instrumentation.ts",
29
+ "default": "./src/instrumentation.ts"
30
+ }
31
+ },
32
+ "bin": {
33
+ "agentvibes-site": "./bin/agentvibes-site.mjs"
34
+ },
35
+ "files": [
36
+ "src",
37
+ "bin"
38
+ ],
39
+ "dependencies": {
40
+ "@agentvibes/kit": "^0.2.0",
41
+ "clsx": "^2.1.1",
42
+ "mobx": "^6.15.0",
43
+ "mobx-react-lite": "^4.1.1",
44
+ "tailwind-merge": "^3.4.0",
45
+ "ts-pattern": "^5.9.0",
46
+ "tsx": "^4.21.0",
47
+ "zod": "^3.25.76",
48
+ "@agentvibes/site-app": "0.3.4",
49
+ "@agentvibes/site-config": "0.5.0",
50
+ "@agentvibes/site-llms": "0.1.2",
51
+ "@agentvibes/theme-public": "0.1.4",
52
+ "@agentvibes/site-telemetry": "0.2.1",
53
+ "@agentvibes/site-mdx": "0.2.2",
54
+ "@agentvibes/site-seo": "0.1.1"
55
+ },
56
+ "peerDependencies": {
57
+ "fumadocs-core": "~16.4.9",
58
+ "fumadocs-mdx": "~14.2.6",
59
+ "fumadocs-ui": "~16.4.9",
60
+ "next": "^16.1.6",
61
+ "react": "^19.0.0",
62
+ "react-dom": "^19.0.0",
63
+ "typescript": "^5.7.3"
64
+ },
65
+ "devDependencies": {
66
+ "@types/mdx": "^2.0.13",
67
+ "@types/node": "^22.10.2",
68
+ "@types/react": "^19.0.0",
69
+ "@types/react-dom": "^19.0.0",
70
+ "fumadocs-core": "^16.4.9",
71
+ "fumadocs-mdx": "^14.2.6",
72
+ "fumadocs-ui": "^16.4.9",
73
+ "next": "^16.1.6",
74
+ "react": "^19.2.4",
75
+ "react-dom": "^19.2.4",
76
+ "typescript": "^5.7.3",
77
+ "vitest": "^3.2.7"
78
+ },
79
+ "publishConfig": {
80
+ "registry": "https://registry.npmjs.org/",
81
+ "access": "public"
82
+ },
83
+ "siteFactory": {
84
+ "tier": "public"
85
+ },
86
+ "scripts": {
87
+ "check": "tsc -p tsconfig.json --noEmit && vitest run",
88
+ "test": "vitest run"
89
+ }
6
90
  }
@@ -0,0 +1,110 @@
1
+ /**
2
+ * App mode in the renderer (`app: { fullscreen, pwa }`, spec §5), on
3
+ * `@agentvibes/site-app`: the root viewport and shell, the manifest, the
4
+ * service worker route, the offline page. Without `app` every function here
5
+ * returns the docs behaviour.
6
+ */
7
+ import {
8
+ appColors,
9
+ AppShell,
10
+ DOCS_VIEWPORT,
11
+ ICON_SET,
12
+ iconUrl,
13
+ rootViewport,
14
+ serviceWorkerScript,
15
+ shortName,
16
+ webManifest,
17
+ type AppColors
18
+ } from "@agentvibes/site-app";
19
+ import { OfflinePage, ServiceWorkerUpdatePrompt } from "@agentvibes/site-app/client";
20
+ import type { Metadata, MetadataRoute, Viewport } from "next";
21
+ import { notFound } from "next/navigation";
22
+ import type { ReactNode } from "react";
23
+
24
+ import type { SiteRuntime } from "./runtime";
25
+ import { siteTheme } from "./theme";
26
+
27
+ export class AppModeConfigError extends Error {
28
+ override readonly name = "AppModeConfigError";
29
+ }
30
+
31
+ function colorsOf(site: SiteRuntime): AppColors | undefined {
32
+ const pwa = site.config.app?.pwa;
33
+ return pwa === undefined ? undefined : appColors(pwa, siteTheme(site.preset).light);
34
+ }
35
+
36
+ /** The root layout's viewport: app (no zoom, `viewport-fit=cover`) when `app.fullscreen`. */
37
+ export function siteViewport(site: SiteRuntime): Viewport {
38
+ return rootViewport(site.config.app, colorsOf(site)?.theme);
39
+ }
40
+
41
+ /** Docs, changelog and landing pages keep pinch zoom, also inside an app-mode site. */
42
+ export function documentViewport(site: SiteRuntime): Viewport {
43
+ const theme = colorsOf(site)?.theme;
44
+ return theme === undefined ? DOCS_VIEWPORT : { ...DOCS_VIEWPORT, themeColor: theme };
45
+ }
46
+
47
+ /** Home-screen metadata (iOS reads `apple-mobile-web-app-*` and the touch icon, not the manifest). */
48
+ export function appMetadata(site: SiteRuntime): Metadata {
49
+ const pwa = site.config.app?.pwa;
50
+ if (pwa === undefined) return {};
51
+ const apple = ICON_SET.find((spec) => spec.purpose === "apple-touch");
52
+ return {
53
+ appleWebApp: { capable: true, title: shortName(site.config.name, pwa), statusBarStyle: site.config.app?.fullscreen === true ? "black-translucent" : "default" },
54
+ ...(apple === undefined ? {} : { icons: { apple: iconUrl(apple) } })
55
+ };
56
+ }
57
+
58
+ /** Wraps every page: the fullscreen shell and the service worker prompt, as configured. */
59
+ export function AppChrome(props: { site: SiteRuntime; children: ReactNode }) {
60
+ const app = props.site.config.app;
61
+ const prompt = app?.pwa?.offline === true ? <ServiceWorkerUpdatePrompt /> : null;
62
+ if (app?.fullscreen !== true) {
63
+ return (
64
+ <>
65
+ {props.children}
66
+ {prompt}
67
+ </>
68
+ );
69
+ }
70
+ return (
71
+ <AppShell>
72
+ {props.children}
73
+ {prompt}
74
+ </AppShell>
75
+ );
76
+ }
77
+
78
+ function requirePwa(site: SiteRuntime, what: string) {
79
+ const pwa = site.config.app?.pwa;
80
+ if (pwa === undefined) throw new AppModeConfigError(`${what} needs app.pwa in site.config`);
81
+ return pwa;
82
+ }
83
+
84
+ /** `app/manifest.ts` (Next serves it as `/manifest.webmanifest` and links it from every page). */
85
+ export function createManifest(site: SiteRuntime) {
86
+ return function manifest(): MetadataRoute.Manifest {
87
+ const pwa = requirePwa(site, "the web manifest");
88
+ return webManifest({ name: site.config.name, description: site.config.description, pwa, colors: appColors(pwa, siteTheme(site.preset).light) });
89
+ };
90
+ }
91
+
92
+ /** `/sw.js`: the generated service worker, versioned by the build's release. */
93
+ export function createServiceWorkerRoute(site: SiteRuntime) {
94
+ return function GET(): Response {
95
+ const pwa = site.config.app?.pwa;
96
+ if (pwa?.offline !== true) return new Response("Not found", { status: 404 });
97
+ return new Response(serviceWorkerScript({ release: site.generated.release, precache: pwa.precache, shellRoutes: pwa.shellRoutes }), {
98
+ headers: { "content-type": "text/javascript; charset=utf-8", "cache-control": "no-cache", "service-worker-allowed": "/" }
99
+ });
100
+ };
101
+ }
102
+
103
+ /** `/~offline`: shown by the service worker for pages it never cached. */
104
+ export function createOfflinePage(site: SiteRuntime) {
105
+ function Page() {
106
+ if (site.config.app?.pwa?.offline !== true) notFound();
107
+ return <OfflinePage name={site.config.name} />;
108
+ }
109
+ return { Page };
110
+ }
package/src/catalog.ts ADDED
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Every page the site serves, in one list: content pages, docs pages, the
3
+ * JSDoc page of each npm module, the changelog. Sitemap, llms.txt, search and
4
+ * the Markdown route all read it, so they cannot disagree about what exists.
5
+ */
6
+ import { jsdocMarkdown } from "./jsdoc";
7
+ import { hasChangelog, hasDocs } from "./nav";
8
+ import type { SiteRuntime, SiteSource } from "./runtime";
9
+
10
+ export type CatalogPage = {
11
+ readonly title: string;
12
+ readonly description: string | undefined;
13
+ /** URL path, starting with "/". */
14
+ readonly path: string;
15
+ /** The llms.txt section the page is listed under. */
16
+ readonly section: string;
17
+ /** Markdown body (processed MDX, or generated text). */
18
+ markdown(): Promise<string>;
19
+ };
20
+
21
+ function contentPages(site: SiteRuntime, source: SiteSource, sectionOf: (slugs: readonly string[]) => string): CatalogPage[] {
22
+ return source.getPages().map((page) => ({
23
+ title: page.data.title ?? page.url,
24
+ description: page.data.description,
25
+ path: page.url,
26
+ section: sectionOf(page.slugs),
27
+ markdown: () => page.data.getText("processed")
28
+ }));
29
+ }
30
+
31
+ export function changelogMarkdown(site: SiteRuntime): string {
32
+ return site.generated.changelog.map((entry) => `## ${entry.packageName}\n\n${entry.markdown.trim()}`).join("\n\n");
33
+ }
34
+
35
+ export function catalog(site: SiteRuntime): CatalogPage[] {
36
+ const modules = site.config.sections.npm?.modules ?? [];
37
+ const moduleTitle = new Map(modules.map((mod) => [mod.id, mod.title]));
38
+ const pages = contentPages(site, site.pages, () => "Pages");
39
+ // kind "app": the home is the author's app/page.tsx, which no MDX source lists.
40
+ const appHome: CatalogPage[] =
41
+ site.config.kind === "app" && !pages.some((page) => page.path === "/")
42
+ ? [{ title: site.config.name, description: site.config.description, path: "/", section: "Pages", markdown: async () => site.config.description ?? "" }]
43
+ : [];
44
+ const docs = hasDocs(site.config)
45
+ ? contentPages(site, site.docs, (slugs) => {
46
+ const first = slugs[0];
47
+ return first === undefined ? "Docs" : (moduleTitle.get(first) ?? "Docs");
48
+ })
49
+ : [];
50
+ const api: CatalogPage[] = modules.map((mod) => ({
51
+ title: `${mod.title} API reference`,
52
+ description: `Exports of ${mod.npm.name}.`,
53
+ path: `/docs/${mod.id}/jsdoc`,
54
+ section: mod.title,
55
+ markdown: async () => jsdocMarkdown(site.generated.jsdoc.modules[mod.id]?.items ?? [])
56
+ }));
57
+ const changelog: CatalogPage[] = hasChangelog(site.config)
58
+ ? [{ title: "Changelog", description: `Release notes of ${site.config.name}.`, path: "/changelog", section: "Changelog", markdown: async () => changelogMarkdown(site) }]
59
+ : [];
60
+ return [...appHome, ...pages, ...docs, ...api, ...changelog];
61
+ }
62
+
63
+ export function findPage(site: SiteRuntime, path: string): CatalogPage | undefined {
64
+ const normalized = path === "" ? "/" : path.replace(/\/+$/, "") || "/";
65
+ return catalog(site).find((page) => page.path === normalized);
66
+ }
67
+
68
+ /** Sections in llms.txt order: pages, general docs, one per npm module, changelog. */
69
+ export function catalogSections(site: SiteRuntime): Array<{ title: string; pages: CatalogPage[] }> {
70
+ const order: string[] = [];
71
+ const bySection = new Map<string, CatalogPage[]>();
72
+ for (const page of catalog(site)) {
73
+ if (!bySection.has(page.section)) {
74
+ order.push(page.section);
75
+ bySection.set(page.section, []);
76
+ }
77
+ bySection.get(page.section)?.push(page);
78
+ }
79
+ return order.map((title) => ({ title, pages: bySection.get(title) ?? [] }));
80
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,51 @@
1
+ import { parseArgs } from "node:util";
2
+
3
+ import type { OutputFlags } from "./output";
4
+ import { prepare } from "./prepare";
5
+ import { finishStatic } from "./prepare/finish-static";
6
+
7
+ const usage = `usage:
8
+ agentvibes-site prepare [site-dir] (follows output in site.config)
9
+ agentvibes-site prepare [site-dir] --static [--origin <https://host>] [--base-path </x/name>] (overrides it)
10
+ agentvibes-site finish-static [site-dir] [--base-path </x/name>]`;
11
+
12
+ const OPTIONS = { static: { type: "boolean" }, origin: { type: "string" }, "base-path": { type: "string" } } as const;
13
+
14
+ function outputFrom(flags: { static?: boolean | undefined; origin?: string | undefined; "base-path"?: string | undefined }): OutputFlags {
15
+ return { static: flags.static, origin: flags.origin, basePath: flags["base-path"] };
16
+ }
17
+
18
+ async function runPrepare(dir: string | undefined, output: OutputFlags): Promise<void> {
19
+ const result = await prepare(dir ?? process.cwd(), process.env, output);
20
+ for (const migration of result.migrations) console.log(`site.config migrated ${migration} (run in memory; update the file to drop this step)`);
21
+ const { written, unchanged, removed } = result.report;
22
+ console.log(`agentvibes-site prepare: ${written.length} written, ${unchanged.length} unchanged, ${removed.length} removed in ${result.siteDir}`);
23
+ for (const file of removed) console.log(` removed ${file}`);
24
+ }
25
+
26
+ async function runFinish(dir: string | undefined, basePath: string): Promise<void> {
27
+ const report = await finishStatic(dir ?? process.cwd(), basePath);
28
+ console.log(`agentvibes-site finish-static: ${report.files} files, ${report.bytes} bytes, ${report.markdownTwins} Markdown twins`);
29
+ console.log(` largest: ${report.largest.file} (${report.largest.bytes} bytes)`);
30
+ for (const redirect of report.redirects) console.log(` redirect ${redirect.source} -> ${redirect.destination}`);
31
+ }
32
+
33
+ export async function main(argv: readonly string[]): Promise<number> {
34
+ try {
35
+ const { values, positionals } = parseArgs({ args: [...argv], options: OPTIONS, allowPositionals: true });
36
+ const [command, dir] = positionals;
37
+ if (command === "prepare") {
38
+ await runPrepare(dir, outputFrom(values));
39
+ return 0;
40
+ }
41
+ if (command === "finish-static") {
42
+ await runFinish(dir, values["base-path"] ?? "");
43
+ return 0;
44
+ }
45
+ console.error(usage);
46
+ return 2;
47
+ } catch (error) {
48
+ console.error(error instanceof Error ? error.message : String(error));
49
+ return 1;
50
+ }
51
+ }
package/src/client.ts ADDED
@@ -0,0 +1,5 @@
1
+ "use client";
2
+
3
+ /** Client components the generated `app/error.tsx` and `app/global-error.tsx` bind. */
4
+ export { SiteErrorBoundary } from "./components/site-error-boundary/site-error-boundary";
5
+ export { SiteGlobalErrorBoundary } from "./components/site-error-boundary/site-global-error-boundary";
@@ -0,0 +1,11 @@
1
+ import { Markdown } from "fumadocs-core/content";
2
+
3
+ /** One package's CHANGELOG.md on `/changelog`. */
4
+ export function ChangelogEntryView(props: { packageName: string; markdown: string }) {
5
+ return (
6
+ <section className="mb-12">
7
+ <h2>{props.packageName}</h2>
8
+ <Markdown>{props.markdown}</Markdown>
9
+ </section>
10
+ );
11
+ }
@@ -0,0 +1,15 @@
1
+ "use client";
2
+
3
+ import { useEffect, useState } from "react";
4
+
5
+ /**
6
+ * Throws in the browser after it mounts, so the page's error boundary
7
+ * (`app/error.tsx`) catches it and reports it: site doctor's client-side
8
+ * synthetic error (rule `errors-captured`).
9
+ */
10
+ export function DoctorThrow(props: { id: string }) {
11
+ const [armed, setArmed] = useState(false);
12
+ useEffect(() => setArmed(true), []);
13
+ if (armed) throw new Error(`site-doctor synthetic client error ${props.id}`);
14
+ return <p data-av-doctor="armed">Throwing a synthetic client error…</p>;
15
+ }
@@ -0,0 +1,14 @@
1
+ import { ErrorReporterInit } from "@agentvibes/site-telemetry/client";
2
+ import { match } from "ts-pattern";
3
+
4
+ import type { RequestContext } from "../context";
5
+ import { telemetryTransport, type SiteRuntime } from "../runtime";
6
+
7
+ /** Starts the browser error reporter, when the request's telemetry plan turns errors on. */
8
+ export function ErrorsInit(props: { site: SiteRuntime; ctx: RequestContext }) {
9
+ const reporter = props.site.errors;
10
+ return match(props.ctx.telemetry.errors)
11
+ .with({ kind: "on" }, (plan) => (reporter === undefined ? null : <ErrorReporterInit config={reporter.clientConfig({ ...plan, origin: props.ctx.origin, transport: telemetryTransport(props.site) })} />))
12
+ .with({ kind: "off" }, () => null)
13
+ .exhaustive();
14
+ }
@@ -0,0 +1,15 @@
1
+ import { PageLink as Link } from "../page-link/page-link";
2
+
3
+ export type HeroAction = { readonly label: string; readonly href: string };
4
+
5
+ export function HeroActionLink(props: { action: HeroAction; primary: boolean }) {
6
+ return props.primary ? (
7
+ <Link href={props.action.href} className="rounded-md bg-fd-primary px-4 py-2 text-sm font-medium text-fd-primary-foreground no-underline">
8
+ {props.action.label}
9
+ </Link>
10
+ ) : (
11
+ <Link href={props.action.href} className="rounded-md border border-fd-border px-4 py-2 text-sm font-medium text-fd-foreground no-underline">
12
+ {props.action.label}
13
+ </Link>
14
+ );
15
+ }
@@ -0,0 +1,31 @@
1
+ import { HeroActionLink, type HeroAction } from "./hero-action-link";
2
+
3
+ export type { HeroAction };
4
+
5
+ export type HeroProps = {
6
+ readonly eyebrow?: string;
7
+ readonly title: string;
8
+ readonly tagline?: string;
9
+ /** A shell command shown as the install line. */
10
+ readonly install?: string;
11
+ readonly actions?: readonly HeroAction[];
12
+ };
13
+
14
+ /** The landing hero, written in `content/pages/index.mdx` as `<Hero … />`. */
15
+ export function Hero(props: HeroProps) {
16
+ return (
17
+ <section className="not-prose border-b border-fd-border py-16">
18
+ {props.eyebrow === undefined ? null : <p className="mb-6 text-xs font-semibold uppercase tracking-widest text-fd-muted-foreground">{props.eyebrow}</p>}
19
+ <h1 className="mb-5 max-w-[18ch] text-5xl font-semibold leading-tight text-fd-foreground">{props.title}</h1>
20
+ {props.tagline === undefined ? null : <p className="mb-7 max-w-[60ch] text-lg text-fd-muted-foreground">{props.tagline}</p>}
21
+ {props.install === undefined ? null : (
22
+ <pre className="mb-6 inline-block rounded-md bg-fd-muted px-4 py-2.5 font-mono text-sm text-fd-foreground">{props.install}</pre>
23
+ )}
24
+ <div className="flex flex-wrap gap-3">
25
+ {(props.actions ?? []).map((action, index) => (
26
+ <HeroActionLink key={action.href} action={action} primary={index === 0} />
27
+ ))}
28
+ </div>
29
+ </section>
30
+ );
31
+ }
@@ -0,0 +1,19 @@
1
+ import { ScrollPage } from "@agentvibes/site-app";
2
+ import { HomeLayout } from "fumadocs-ui/layouts/home";
3
+ import type { ReactNode } from "react";
4
+
5
+ import { layoutOptions } from "../layout-options";
6
+ import type { SiteRuntime } from "../runtime";
7
+ import { PageFooter } from "./page-footer";
8
+
9
+ /** Landing, changelog and other top-level pages: Fumadocs' home layout, then the footer (a document page, also in app mode). */
10
+ export function HomeShell(props: { site: SiteRuntime; children: ReactNode }) {
11
+ return (
12
+ <ScrollPage>
13
+ <HomeLayout {...layoutOptions(props.site)}>
14
+ <main className="mx-auto w-full max-w-fd-container flex-1 px-6 pb-16">{props.children}</main>
15
+ </HomeLayout>
16
+ <PageFooter site={props.site} />
17
+ </ScrollPage>
18
+ );
19
+ }
@@ -0,0 +1,14 @@
1
+ import type { ApiItem } from "../../data";
2
+ import { sectionTitle } from "../../jsdoc";
3
+ import { ItemView } from "./item-view";
4
+
5
+ export function GroupView(props: { kind: ApiItem["kind"]; items: readonly ApiItem[] }) {
6
+ return (
7
+ <section className="grid gap-4">
8
+ <h2 className="text-lg font-semibold">{sectionTitle(props.kind)}</h2>
9
+ {props.items.map((item) => (
10
+ <ItemView key={item.name} item={item} />
11
+ ))}
12
+ </section>
13
+ );
14
+ }
@@ -0,0 +1,24 @@
1
+ import type { ApiItem } from "../../data";
2
+ import { MemberView } from "./member-view";
3
+
4
+ export function ItemView(props: { item: ApiItem }) {
5
+ const { item } = props;
6
+ const members = item.members ?? [];
7
+ return (
8
+ <article id={item.name} className="rounded-xl border border-fd-border bg-fd-card p-4">
9
+ <div className="flex flex-wrap items-center justify-between gap-2">
10
+ <h3 className="m-0 text-base font-semibold">{item.name}</h3>
11
+ <span className="rounded-full bg-fd-muted px-2 py-0.5 text-xs uppercase tracking-wide">{item.kind}</span>
12
+ </div>
13
+ {item.description === undefined ? null : <p className="mt-2 text-sm text-fd-muted-foreground">{item.description}</p>}
14
+ {item.signature === undefined ? null : <pre className="mt-3 overflow-x-auto rounded-lg bg-fd-muted p-3 text-sm">{item.signature}</pre>}
15
+ {members.length === 0 ? null : (
16
+ <div className="mt-4 grid gap-3">
17
+ {members.map((member, index) => (
18
+ <MemberView key={`${member.name}-${index}`} member={member} />
19
+ ))}
20
+ </div>
21
+ )}
22
+ </article>
23
+ );
24
+ }
@@ -0,0 +1,16 @@
1
+ import type { ApiItem } from "../../data";
2
+ import { groupByKind } from "../../jsdoc";
3
+ import { GroupView } from "./group-view";
4
+
5
+ /** The API reference of one npm module, from the data `prepare` extracted with the TypeScript compiler. */
6
+ export function JsdocView(props: { items: readonly ApiItem[] }) {
7
+ const groups = groupByKind(props.items);
8
+ if (groups.length === 0) return <p>No exports yet.</p>;
9
+ return (
10
+ <div className="not-prose grid gap-10">
11
+ {groups.map((group) => (
12
+ <GroupView key={group.kind} kind={group.kind} items={group.items} />
13
+ ))}
14
+ </div>
15
+ );
16
+ }
@@ -0,0 +1,15 @@
1
+ import type { ApiMember } from "../../data";
2
+
3
+ export function MemberView(props: { member: ApiMember }) {
4
+ const { member } = props;
5
+ return (
6
+ <div className="rounded-lg border border-fd-border bg-fd-background p-3">
7
+ <div className="flex flex-wrap items-center justify-between gap-2">
8
+ <h4 className="m-0 text-sm font-medium">{member.name}</h4>
9
+ <span className="rounded-full bg-fd-muted px-2 py-0.5 text-[10px] uppercase tracking-wide">{member.kind}</span>
10
+ </div>
11
+ {member.description === undefined ? null : <p className="mt-2 text-xs text-fd-muted-foreground">{member.description}</p>}
12
+ <pre className="mt-2 overflow-x-auto rounded-md bg-fd-muted p-2 text-xs">{member.signature}</pre>
13
+ </div>
14
+ );
15
+ }
@@ -0,0 +1,13 @@
1
+ import type { NpmModule } from "@agentvibes/site-config";
2
+ import { Card } from "fumadocs-ui/components/card";
3
+
4
+ export function ModuleCard(props: { module: NpmModule }) {
5
+ const { module } = props;
6
+ return (
7
+ <Card title={module.title} description={module.description} href={`/docs/${module.id}`}>
8
+ <span className="mt-2 block font-mono text-xs text-fd-muted-foreground">
9
+ {module.npm.name} · {module.kind} · {module.stability}
10
+ </span>
11
+ </Card>
12
+ );
13
+ }
@@ -0,0 +1,15 @@
1
+ import type { NpmModule } from "@agentvibes/site-config";
2
+ import { Cards } from "fumadocs-ui/components/card";
3
+
4
+ import { ModuleCard } from "./module-card";
5
+
6
+ /** Cards for `sections.npm.modules`, for `<ModuleCards />` in MDX. */
7
+ export function ModuleCards(props: { modules: readonly NpmModule[] }) {
8
+ return (
9
+ <Cards>
10
+ {props.modules.map((module) => (
11
+ <ModuleCard key={module.id} module={module} />
12
+ ))}
13
+ </Cards>
14
+ );
15
+ }
@@ -0,0 +1,40 @@
1
+ "use client";
2
+
3
+ import type { Resource } from "@agentvibes/kit/resource";
4
+ import { match } from "ts-pattern";
5
+
6
+ const BUTTON = "inline-flex items-center gap-1.5 rounded-md border border-fd-border bg-fd-secondary px-2.5 py-1 text-xs font-medium text-fd-secondary-foreground hover:bg-fd-accent disabled:opacity-60";
7
+
8
+ /** "Copy as Markdown", rendering each state of the copy: idle, copying, copied, failed with retry. */
9
+ export function CopyButton(props: { state: Resource<true>; onCopy: () => void; onDismiss: () => void }) {
10
+ return match(props.state)
11
+ .with({ status: "idle" }, () => (
12
+ <button type="button" className={BUTTON} onClick={props.onCopy}>
13
+ Copy as Markdown
14
+ </button>
15
+ ))
16
+ .with({ status: "loading" }, () => (
17
+ <button type="button" className={BUTTON} disabled aria-busy="true">
18
+ <span aria-hidden="true" className="size-3 animate-spin rounded-full border-2 border-fd-muted-foreground border-t-transparent" />
19
+ Copying…
20
+ </button>
21
+ ))
22
+ .with({ status: "ready" }, () => (
23
+ <button type="button" className={BUTTON} onClick={props.onCopy} data-state="copied">
24
+ <span aria-hidden="true">✓</span>
25
+ Copied
26
+ </button>
27
+ ))
28
+ .with({ status: "error" }, ({ error }) => (
29
+ <span role="alert" className="inline-flex items-center gap-2 rounded-md border border-fd-error/40 bg-fd-error/10 px-2.5 py-1 text-xs text-fd-error">
30
+ Copy failed: {error.message}
31
+ <button type="button" className="underline" onClick={props.onCopy}>
32
+ Retry
33
+ </button>
34
+ <button type="button" className="underline" onClick={props.onDismiss}>
35
+ Dismiss
36
+ </button>
37
+ </span>
38
+ ))
39
+ .exhaustive();
40
+ }