feat: add docs source loader and mdx components
This commit is contained in:
parent
9242868b5a
commit
b3ed10a5ae
|
|
@ -0,0 +1,65 @@
|
|||
import Link from "next/link";
|
||||
import { DocsBody, DocsPage } from "fumadocs-ui/page";
|
||||
|
||||
export function DocsMissingPage() {
|
||||
return (
|
||||
<DocsPage
|
||||
toc={[]}
|
||||
tableOfContent={{
|
||||
enabled: false,
|
||||
}}
|
||||
breadcrumb={{
|
||||
enabled: true,
|
||||
includePage: false,
|
||||
}}
|
||||
footer={{
|
||||
enabled: false,
|
||||
}}
|
||||
>
|
||||
<DocsBody>
|
||||
<div className="not-prose docs-missing-wrap">
|
||||
<div className="docs-missing-card">
|
||||
<div className="docs-missing-label">
|
||||
docs / checkout failed
|
||||
</div>
|
||||
<div className="docs-missing-content">
|
||||
<section className="docs-missing-copy">
|
||||
<h2>
|
||||
This page checked out the wrong worktree.
|
||||
</h2>
|
||||
<p>
|
||||
The docs were rebuilt, and this URL did not survive the merge. Start from the docs
|
||||
index, or use search in the sidebar to find where it landed.
|
||||
</p>
|
||||
<div className="docs-missing-actions">
|
||||
<Link
|
||||
href="/docs"
|
||||
className="docs-missing-primary"
|
||||
>
|
||||
Browse docs
|
||||
</Link>
|
||||
<Link
|
||||
href="/"
|
||||
className="docs-missing-secondary"
|
||||
>
|
||||
Home
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
<div className="docs-missing-terminal">
|
||||
<div className="docs-missing-dots">
|
||||
<span className="docs-missing-dot-red" />
|
||||
<span className="docs-missing-dot-yellow" />
|
||||
<span className="docs-missing-dot-green" />
|
||||
</div>
|
||||
<p className="docs-missing-command">$ ao docs resolve</p>
|
||||
<p className="docs-missing-status">status: missing</p>
|
||||
<p>next: /docs</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DocsBody>
|
||||
</DocsPage>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,100 @@
|
|||
/**
|
||||
* Brand logo component for docs pages.
|
||||
*
|
||||
* Renders real brand assets from /public/docs/logos/ when available, with
|
||||
* name-aliasing. Mono brand SVGs are pre-processed to use white fill for the
|
||||
* dark theme. Falls back to a monogram badge for anything not in the set.
|
||||
*/
|
||||
import type { CSSProperties } from "react";
|
||||
|
||||
/** Aliases: request name -> filename under /docs/logos/. */
|
||||
const ALIASES: Record<string, string> = {
|
||||
claude: "claude-code",
|
||||
chatgpt: "openai",
|
||||
};
|
||||
|
||||
/** Extension for each logo file. Defaults to svg; override for raster assets. */
|
||||
const LOGO_EXT: Record<string, string> = {
|
||||
aider: "png",
|
||||
};
|
||||
|
||||
/** Brands with a real asset file under /public/docs/logos/. */
|
||||
const FILE_LOGOS = new Set([
|
||||
"aider",
|
||||
"anthropic",
|
||||
"apple",
|
||||
"claude-code",
|
||||
"codex",
|
||||
"composio",
|
||||
"cursor",
|
||||
"discord",
|
||||
"docker",
|
||||
"github",
|
||||
"gitlab",
|
||||
"iterm2",
|
||||
"linear",
|
||||
"linux",
|
||||
"microsoft",
|
||||
"openai",
|
||||
"openclaw",
|
||||
"opencode",
|
||||
"slack",
|
||||
"tmux",
|
||||
"web",
|
||||
"webhook",
|
||||
"windows",
|
||||
]);
|
||||
|
||||
export interface LogoProps {
|
||||
/** Brand name (case-insensitive). */
|
||||
name: string;
|
||||
/** Size in pixels. Default 20. */
|
||||
size?: number;
|
||||
className?: string;
|
||||
/** Accepted for backwards compat with existing MDX; no-op. */
|
||||
color?: boolean;
|
||||
}
|
||||
|
||||
export function Logo({ name, size = 20, className }: LogoProps) {
|
||||
const key = name.toLowerCase();
|
||||
const resolved = ALIASES[key] ?? key;
|
||||
const baseStyle: CSSProperties = { flexShrink: 0, width: size, height: size };
|
||||
|
||||
if (FILE_LOGOS.has(resolved)) {
|
||||
const ext = LOGO_EXT[resolved] ?? "svg";
|
||||
return (
|
||||
<img
|
||||
src={`/docs/logos/${resolved}.${ext}`}
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
className={className}
|
||||
style={baseStyle}
|
||||
width={size}
|
||||
height={size}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const initial = name.charAt(0).toUpperCase();
|
||||
return (
|
||||
<span
|
||||
className={className}
|
||||
style={{
|
||||
...baseStyle,
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
borderRadius: size * 0.22,
|
||||
backgroundColor: "var(--color-bg-elevated, #2a2827)",
|
||||
color: "#ffffff",
|
||||
fontSize: size * 0.55,
|
||||
fontWeight: 700,
|
||||
lineHeight: 1,
|
||||
fontFamily: "var(--font-geist-sans), ui-sans-serif, system-ui",
|
||||
}}
|
||||
aria-hidden="true"
|
||||
>
|
||||
{initial}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,102 @@
|
|||
/**
|
||||
* Shows a three-column support matrix for macOS / Linux / Windows.
|
||||
* Use at the top of plugin pages to set platform expectations immediately.
|
||||
*/
|
||||
import type { ReactNode } from "react";
|
||||
import { Logo } from "./Logo";
|
||||
|
||||
type Status = "full" | "partial" | "none";
|
||||
|
||||
export interface PlatformSupportProps {
|
||||
macos?: Status;
|
||||
linux?: Status;
|
||||
windows?: Status;
|
||||
note?: ReactNode;
|
||||
}
|
||||
|
||||
const LABEL: Record<Status, string> = {
|
||||
full: "Supported",
|
||||
partial: "In progress",
|
||||
none: "Not supported",
|
||||
};
|
||||
|
||||
const DOT_COLOR: Record<Status, string> = {
|
||||
full: "var(--color-accent-amber, #f97316)",
|
||||
partial: "var(--color-accent-amber-dim, #a3581b)",
|
||||
none: "var(--color-text-muted, #605e5c)",
|
||||
};
|
||||
|
||||
function Cell({ platform, status }: { platform: "macos" | "linux" | "windows"; status: Status }) {
|
||||
const logoName = platform === "macos" ? "apple" : platform;
|
||||
const title = platform === "macos" ? "macOS" : platform === "linux" ? "Linux" : "Windows";
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "0.5rem",
|
||||
padding: "0.5rem 0.75rem",
|
||||
borderRadius: "0.375rem",
|
||||
border: "1px solid var(--color-fd-border)",
|
||||
backgroundColor: "var(--color-fd-card)",
|
||||
flex: "1 1 0",
|
||||
minWidth: 0,
|
||||
}}
|
||||
>
|
||||
<Logo name={logoName} size={18} />
|
||||
<div style={{ display: "flex", flexDirection: "column", minWidth: 0 }}>
|
||||
<span style={{ fontSize: "0.8125rem", fontWeight: 600, color: "var(--color-fd-foreground)" }}>
|
||||
{title}
|
||||
</span>
|
||||
<span
|
||||
style={{
|
||||
fontSize: "0.75rem",
|
||||
color: "var(--color-fd-muted-foreground)",
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
gap: "0.375rem",
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
width: "6px",
|
||||
height: "6px",
|
||||
borderRadius: "999px",
|
||||
backgroundColor: DOT_COLOR[status],
|
||||
display: "inline-block",
|
||||
}}
|
||||
/>
|
||||
{LABEL[status]}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function PlatformSupport({
|
||||
macos = "full",
|
||||
linux = "full",
|
||||
windows = "full",
|
||||
note,
|
||||
}: PlatformSupportProps) {
|
||||
return (
|
||||
<div style={{ margin: "1.25rem 0" }}>
|
||||
<div style={{ display: "flex", gap: "0.5rem", flexWrap: "wrap" }}>
|
||||
<Cell platform="macos" status={macos} />
|
||||
<Cell platform="linux" status={linux} />
|
||||
<Cell platform="windows" status={windows} />
|
||||
</div>
|
||||
{note && (
|
||||
<p
|
||||
style={{
|
||||
margin: "0.5rem 0 0 0",
|
||||
fontSize: "0.8125rem",
|
||||
color: "var(--color-fd-muted-foreground)",
|
||||
}}
|
||||
>
|
||||
{note}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
/**
|
||||
* Catalog-style card for a single plugin.
|
||||
* Used on the plugin overview pages to let users scan by logo + one-liner.
|
||||
*/
|
||||
import Link from "next/link";
|
||||
import type { ReactNode } from "react";
|
||||
import { Logo } from "./Logo";
|
||||
|
||||
export interface PluginCardProps {
|
||||
name: string;
|
||||
logo: string;
|
||||
href: string;
|
||||
description: string;
|
||||
badge?: ReactNode;
|
||||
}
|
||||
|
||||
export function PluginCard({ name, logo, href, description, badge }: PluginCardProps) {
|
||||
return (
|
||||
<Link
|
||||
href={href}
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "flex-start",
|
||||
gap: "0.875rem",
|
||||
padding: "1rem",
|
||||
borderRadius: "0.5rem",
|
||||
border: "1px solid var(--color-fd-border)",
|
||||
backgroundColor: "var(--color-fd-card)",
|
||||
textDecoration: "none",
|
||||
transition: "border-color 150ms, transform 150ms",
|
||||
}}
|
||||
className="ao-plugin-card"
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
width: "36px",
|
||||
height: "36px",
|
||||
borderRadius: "0.375rem",
|
||||
backgroundColor: "var(--color-fd-muted)",
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
color: "var(--color-fd-foreground)",
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
<Logo name={logo} size={22} />
|
||||
</span>
|
||||
<span style={{ display: "flex", flexDirection: "column", gap: "0.25rem", minWidth: 0 }}>
|
||||
<span
|
||||
style={{
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
gap: "0.5rem",
|
||||
fontSize: "0.9375rem",
|
||||
fontWeight: 600,
|
||||
color: "var(--color-fd-foreground)",
|
||||
margin: 0,
|
||||
}}
|
||||
>
|
||||
{name}
|
||||
{badge}
|
||||
</span>
|
||||
<span
|
||||
style={{
|
||||
fontSize: "0.8125rem",
|
||||
color: "var(--color-fd-muted-foreground)",
|
||||
lineHeight: 1.45,
|
||||
}}
|
||||
>
|
||||
{description}
|
||||
</span>
|
||||
</span>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
export function PluginGrid({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: "grid",
|
||||
gap: "0.75rem",
|
||||
gridTemplateColumns: "repeat(auto-fill, minmax(260px, 1fr))",
|
||||
margin: "1.25rem 0",
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
/**
|
||||
* Shared MDX component registry for docs pages.
|
||||
* Exposes docs-specific components and a small Accordion pair used by content.
|
||||
*/
|
||||
import type { ReactNode } from "react";
|
||||
import defaultMdxComponents from "fumadocs-ui/mdx";
|
||||
import type { MDXComponents } from "mdx/types";
|
||||
import { Logo } from "./Logo";
|
||||
import { PlatformSupport } from "./PlatformSupport";
|
||||
import { PluginCard, PluginGrid } from "./PluginCard";
|
||||
|
||||
function Accordions({ children }: { children: ReactNode }) {
|
||||
return <div className="my-6 space-y-3">{children}</div>;
|
||||
}
|
||||
|
||||
function Accordion({ title, children }: { title: ReactNode; children: ReactNode }) {
|
||||
return (
|
||||
<details className="rounded-md border border-[var(--color-border-default)] bg-[var(--color-bg-surface)] px-4 py-3">
|
||||
<summary className="cursor-pointer list-none text-sm font-medium text-[var(--color-text-primary)]">
|
||||
{title}
|
||||
</summary>
|
||||
<div className="mt-3 text-sm text-[var(--color-text-secondary)]">{children}</div>
|
||||
</details>
|
||||
);
|
||||
}
|
||||
|
||||
export function getMDXComponents(): MDXComponents {
|
||||
return {
|
||||
...defaultMdxComponents,
|
||||
Accordion,
|
||||
Accordions,
|
||||
Logo,
|
||||
PlatformSupport,
|
||||
PluginCard,
|
||||
PluginGrid,
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
import { loader } from "fumadocs-core/source";
|
||||
import { docs } from ".source";
|
||||
|
||||
export const source = loader({
|
||||
baseUrl: "/docs",
|
||||
source: docs.toFumadocsSource(),
|
||||
});
|
||||
Loading…
Reference in New Issue