feat: build an AI agent from 0 to 1 -- 11 progressive sessions

- 11 sessions from basic agent loop to autonomous teams
- Python MVP implementations for each session
- Mental-model-first docs in en/zh/ja
- Interactive web platform with step-through visualizations
- Incremental architecture: each session adds one mechanism
This commit is contained in:
CrazyBoyM
2026-02-21 14:37:42 +08:00
committed by CrazyBoyM
commit c6a27ef1d7
156 changed files with 28059 additions and 0 deletions

View File

@@ -0,0 +1,82 @@
"use client";
import { ArchDiagram } from "@/components/architecture/arch-diagram";
import { WhatsNew } from "@/components/diff/whats-new";
import { DesignDecisions } from "@/components/architecture/design-decisions";
import { DocRenderer } from "@/components/docs/doc-renderer";
import { SourceViewer } from "@/components/code/source-viewer";
import { AgentLoopSimulator } from "@/components/simulator/agent-loop-simulator";
import { ExecutionFlow } from "@/components/architecture/execution-flow";
import { SessionVisualization } from "@/components/visualizations";
import { Tabs } from "@/components/ui/tabs";
import { useTranslations } from "@/lib/i18n";
interface VersionDetailClientProps {
version: string;
diff: {
from: string;
to: string;
newClasses: string[];
newFunctions: string[];
newTools: string[];
locDelta: number;
} | null;
source: string;
filename: string;
}
export function VersionDetailClient({
version,
diff,
source,
filename,
}: VersionDetailClientProps) {
const t = useTranslations("version");
const tabs = [
{ id: "learn", label: t("tab_learn") },
{ id: "simulate", label: t("tab_simulate") },
{ id: "code", label: t("tab_code") },
{ id: "deep-dive", label: t("tab_deep_dive") },
];
return (
<div className="space-y-6">
{/* Hero Visualization */}
<SessionVisualization version={version} />
{/* Tabbed content */}
<Tabs tabs={tabs} defaultTab="learn">
{(activeTab) => (
<>
{activeTab === "learn" && <DocRenderer version={version} />}
{activeTab === "simulate" && (
<AgentLoopSimulator version={version} />
)}
{activeTab === "code" && (
<SourceViewer source={source} filename={filename} />
)}
{activeTab === "deep-dive" && (
<div className="space-y-8">
<section>
<h2 className="mb-4 text-xl font-semibold">
{t("execution_flow")}
</h2>
<ExecutionFlow version={version} />
</section>
<section>
<h2 className="mb-4 text-xl font-semibold">
{t("architecture")}
</h2>
<ArchDiagram version={version} />
</section>
{diff && <WhatsNew diff={diff} />}
<DesignDecisions version={version} />
</div>
)}
</>
)}
</Tabs>
</div>
);
}

View File

@@ -0,0 +1,202 @@
"use client";
import { useMemo } from "react";
import Link from "next/link";
import { useLocale } from "@/lib/i18n";
import { VERSION_META } from "@/lib/constants";
import { Card, CardHeader, CardTitle } from "@/components/ui/card";
import { LayerBadge } from "@/components/ui/badge";
import { CodeDiff } from "@/components/diff/code-diff";
import { ArrowLeft, Plus, Minus, FileCode, Wrench, Box, FunctionSquare } from "lucide-react";
import type { AgentVersion, VersionDiff, VersionIndex } from "@/types/agent-data";
import versionData from "@/data/generated/versions.json";
const data = versionData as VersionIndex;
interface DiffPageContentProps {
version: string;
}
export function DiffPageContent({ version }: DiffPageContentProps) {
const locale = useLocale();
const meta = VERSION_META[version];
const { currentVersion, prevVersion, diff } = useMemo(() => {
const current = data.versions.find((v) => v.id === version);
const prevId = meta?.prevVersion;
const prev = prevId ? data.versions.find((v) => v.id === prevId) : null;
const d = data.diffs.find((d) => d.to === version);
return { currentVersion: current, prevVersion: prev, diff: d };
}, [version, meta]);
if (!meta || !currentVersion) {
return (
<div className="py-12 text-center">
<p className="text-zinc-500">Version not found.</p>
<Link href={`/${locale}/timeline`} className="mt-4 inline-block text-sm text-blue-600 hover:underline">
Back to timeline
</Link>
</div>
);
}
if (!prevVersion || !diff) {
return (
<div className="py-12">
<Link
href={`/${locale}/${version}`}
className="mb-6 inline-flex items-center gap-1 text-sm text-zinc-500 hover:text-zinc-700 dark:hover:text-zinc-300"
>
<ArrowLeft size={14} />
Back to {meta.title}
</Link>
<h1 className="text-3xl font-bold">{meta.title}</h1>
<p className="mt-4 text-zinc-500">
This is the first version -- there is no previous version to compare against.
</p>
</div>
);
}
const prevMeta = VERSION_META[prevVersion.id];
return (
<div className="py-4">
<Link
href={`/${locale}/${version}`}
className="mb-6 inline-flex items-center gap-1 text-sm text-zinc-500 hover:text-zinc-700 dark:hover:text-zinc-300"
>
<ArrowLeft size={14} />
Back to {meta.title}
</Link>
{/* Header */}
<div className="mb-8">
<h1 className="text-3xl font-bold">
{prevMeta?.title || prevVersion.id} {meta.title}
</h1>
<p className="mt-2 text-zinc-500 dark:text-zinc-400">
{prevVersion.id} ({prevVersion.loc} LOC) {version} ({currentVersion.loc} LOC)
</p>
</div>
{/* Structural Diff */}
<div className="mb-8 grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
<Card>
<CardHeader>
<div className="flex items-center gap-2 text-zinc-500 dark:text-zinc-400">
<FileCode size={16} />
<span className="text-sm">LOC Delta</span>
</div>
</CardHeader>
<CardTitle>
<span className={diff.locDelta >= 0 ? "text-green-600 dark:text-green-400" : "text-red-600 dark:text-red-400"}>
{diff.locDelta >= 0 ? "+" : ""}{diff.locDelta}
</span>
<span className="ml-2 text-sm font-normal text-zinc-500">lines</span>
</CardTitle>
</Card>
<Card>
<CardHeader>
<div className="flex items-center gap-2 text-zinc-500 dark:text-zinc-400">
<Wrench size={16} />
<span className="text-sm">New Tools</span>
</div>
</CardHeader>
<CardTitle>
<span className="text-blue-600 dark:text-blue-400">{diff.newTools.length}</span>
</CardTitle>
{diff.newTools.length > 0 && (
<div className="mt-2 flex flex-wrap gap-1">
{diff.newTools.map((tool) => (
<span key={tool} className="rounded bg-blue-100 px-1.5 py-0.5 text-xs text-blue-700 dark:bg-blue-900/30 dark:text-blue-300">
{tool}
</span>
))}
</div>
)}
</Card>
<Card>
<CardHeader>
<div className="flex items-center gap-2 text-zinc-500 dark:text-zinc-400">
<Box size={16} />
<span className="text-sm">New Classes</span>
</div>
</CardHeader>
<CardTitle>
<span className="text-purple-600 dark:text-purple-400">{diff.newClasses.length}</span>
</CardTitle>
{diff.newClasses.length > 0 && (
<div className="mt-2 flex flex-wrap gap-1">
{diff.newClasses.map((cls) => (
<span key={cls} className="rounded bg-purple-100 px-1.5 py-0.5 text-xs text-purple-700 dark:bg-purple-900/30 dark:text-purple-300">
{cls}
</span>
))}
</div>
)}
</Card>
<Card>
<CardHeader>
<div className="flex items-center gap-2 text-zinc-500 dark:text-zinc-400">
<FunctionSquare size={16} />
<span className="text-sm">New Functions</span>
</div>
</CardHeader>
<CardTitle>
<span className="text-amber-600 dark:text-amber-400">{diff.newFunctions.length}</span>
</CardTitle>
{diff.newFunctions.length > 0 && (
<div className="mt-2 flex flex-wrap gap-1">
{diff.newFunctions.map((fn) => (
<span key={fn} className="rounded bg-amber-100 px-1.5 py-0.5 text-xs text-amber-700 dark:bg-amber-900/30 dark:text-amber-300">
{fn}
</span>
))}
</div>
)}
</Card>
</div>
{/* Version Info Comparison */}
<div className="mb-8 grid grid-cols-1 gap-4 sm:grid-cols-2">
<Card className="border-l-4 border-l-red-300 dark:border-l-red-700">
<CardHeader>
<CardTitle>{prevMeta?.title || prevVersion.id}</CardTitle>
<p className="text-sm text-zinc-500">{prevMeta?.subtitle}</p>
</CardHeader>
<div className="space-y-1 text-sm text-zinc-600 dark:text-zinc-400">
<p>{prevVersion.loc} LOC</p>
<p>{prevVersion.tools.length} tools: {prevVersion.tools.join(", ")}</p>
<LayerBadge layer={prevVersion.layer}>{prevVersion.layer}</LayerBadge>
</div>
</Card>
<Card className="border-l-4 border-l-green-300 dark:border-l-green-700">
<CardHeader>
<CardTitle>{meta.title}</CardTitle>
<p className="text-sm text-zinc-500">{meta.subtitle}</p>
</CardHeader>
<div className="space-y-1 text-sm text-zinc-600 dark:text-zinc-400">
<p>{currentVersion.loc} LOC</p>
<p>{currentVersion.tools.length} tools: {currentVersion.tools.join(", ")}</p>
<LayerBadge layer={currentVersion.layer}>{currentVersion.layer}</LayerBadge>
</div>
</Card>
</div>
{/* Code Diff */}
<div>
<h2 className="mb-4 text-xl font-semibold">Source Code Diff</h2>
<CodeDiff
oldSource={prevVersion.source}
newSource={currentVersion.source}
oldLabel={`${prevVersion.id} (${prevVersion.filename})`}
newLabel={`${version} (${currentVersion.filename})`}
/>
</div>
</div>
);
}

View File

@@ -0,0 +1,15 @@
import { LEARNING_PATH } from "@/lib/constants";
import { DiffPageContent } from "./diff-content";
export function generateStaticParams() {
return LEARNING_PATH.map((version) => ({ version }));
}
export default async function DiffPage({
params,
}: {
params: Promise<{ locale: string; version: string }>;
}) {
const { version } = await params;
return <DiffPageContent version={version} />;
}

View File

@@ -0,0 +1,125 @@
import Link from "next/link";
import { LEARNING_PATH, VERSION_META, LAYERS } from "@/lib/constants";
import { LayerBadge } from "@/components/ui/badge";
import versionsData from "@/data/generated/versions.json";
import { VersionDetailClient } from "./client";
import { getTranslations } from "@/lib/i18n-server";
export function generateStaticParams() {
return LEARNING_PATH.map((version) => ({ version }));
}
export default async function VersionPage({
params,
}: {
params: Promise<{ locale: string; version: string }>;
}) {
const { locale, version } = await params;
const versionData = versionsData.versions.find((v) => v.id === version);
const meta = VERSION_META[version];
const diff = versionsData.diffs.find((d) => d.to === version) ?? null;
if (!versionData || !meta) {
return (
<div className="py-20 text-center">
<h1 className="text-2xl font-bold">Version not found</h1>
<p className="mt-2 text-zinc-500">{version}</p>
</div>
);
}
const t = getTranslations(locale, "version");
const tSession = getTranslations(locale, "sessions");
const tLayer = getTranslations(locale, "layer_labels");
const layer = LAYERS.find((l) => l.id === meta.layer);
const pathIndex = LEARNING_PATH.indexOf(version as typeof LEARNING_PATH[number]);
const prevVersion = pathIndex > 0 ? LEARNING_PATH[pathIndex - 1] : null;
const nextVersion =
pathIndex < LEARNING_PATH.length - 1
? LEARNING_PATH[pathIndex + 1]
: null;
return (
<div className="mx-auto max-w-3xl space-y-10 py-4">
{/* Header */}
<header className="space-y-3">
<div className="flex flex-wrap items-center gap-3">
<span className="rounded-lg bg-zinc-100 px-3 py-1 font-mono text-lg font-bold dark:bg-zinc-800">
{version}
</span>
<h1 className="text-2xl font-bold sm:text-3xl">{tSession(version) || meta.title}</h1>
{layer && (
<LayerBadge layer={meta.layer}>{tLayer(layer.id)}</LayerBadge>
)}
</div>
<p className="text-lg text-zinc-500 dark:text-zinc-400">
{meta.subtitle}
</p>
<div className="flex flex-wrap items-center gap-4 text-sm text-zinc-500 dark:text-zinc-400">
<span className="font-mono">{versionData.loc} LOC</span>
<span>{versionData.tools.length} {t("tools")}</span>
{meta.coreAddition && (
<span className="rounded-full bg-zinc-100 px-2.5 py-0.5 text-xs dark:bg-zinc-800">
{meta.coreAddition}
</span>
)}
</div>
{meta.keyInsight && (
<blockquote className="border-l-4 border-zinc-300 pl-4 text-sm italic text-zinc-500 dark:border-zinc-600 dark:text-zinc-400">
{meta.keyInsight}
</blockquote>
)}
</header>
{/* Client-rendered interactive sections */}
<VersionDetailClient
version={version}
diff={diff}
source={versionData.source}
filename={versionData.filename}
/>
{/* Prev / Next navigation */}
<nav className="flex items-center justify-between border-t border-zinc-200 pt-6 dark:border-zinc-700">
{prevVersion ? (
<Link
href={`/${locale}/${prevVersion}`}
className="group flex items-center gap-2 text-sm text-zinc-500 transition-colors hover:text-zinc-900 dark:hover:text-white"
>
<span className="transition-transform group-hover:-translate-x-1">
&larr;
</span>
<div>
<div className="text-xs text-zinc-400">{t("prev")}</div>
<div className="font-medium">
{prevVersion} - {tSession(prevVersion) || VERSION_META[prevVersion]?.title}
</div>
</div>
</Link>
) : (
<div />
)}
{nextVersion ? (
<Link
href={`/${locale}/${nextVersion}`}
className="group flex items-center gap-2 text-right text-sm text-zinc-500 transition-colors hover:text-zinc-900 dark:hover:text-white"
>
<div>
<div className="text-xs text-zinc-400">{t("next")}</div>
<div className="font-medium">
{tSession(nextVersion) || VERSION_META[nextVersion]?.title} - {nextVersion}
</div>
</div>
<span className="transition-transform group-hover:translate-x-1">
&rarr;
</span>
</Link>
) : (
<div />
)}
</nav>
</div>
);
}

View File

@@ -0,0 +1,308 @@
"use client";
import { useState, useMemo } from "react";
import { useLocale, useTranslations } from "@/lib/i18n";
import { LEARNING_PATH, VERSION_META } from "@/lib/constants";
import { Card, CardHeader, CardTitle } from "@/components/ui/card";
import { LayerBadge } from "@/components/ui/badge";
import { CodeDiff } from "@/components/diff/code-diff";
import { ArchDiagram } from "@/components/architecture/arch-diagram";
import { ArrowRight, FileCode, Wrench, Box, FunctionSquare } from "lucide-react";
import type { VersionIndex } from "@/types/agent-data";
import versionData from "@/data/generated/versions.json";
const data = versionData as VersionIndex;
export default function ComparePage() {
const t = useTranslations("compare");
const locale = useLocale();
const [versionA, setVersionA] = useState<string>("");
const [versionB, setVersionB] = useState<string>("");
const infoA = useMemo(() => data.versions.find((v) => v.id === versionA), [versionA]);
const infoB = useMemo(() => data.versions.find((v) => v.id === versionB), [versionB]);
const metaA = versionA ? VERSION_META[versionA] : null;
const metaB = versionB ? VERSION_META[versionB] : null;
const comparison = useMemo(() => {
if (!infoA || !infoB) return null;
const toolsA = new Set(infoA.tools);
const toolsB = new Set(infoB.tools);
const onlyA = infoA.tools.filter((t) => !toolsB.has(t));
const onlyB = infoB.tools.filter((t) => !toolsA.has(t));
const shared = infoA.tools.filter((t) => toolsB.has(t));
const classesA = new Set(infoA.classes.map((c) => c.name));
const classesB = new Set(infoB.classes.map((c) => c.name));
const newClasses = infoB.classes.map((c) => c.name).filter((c) => !classesA.has(c));
const funcsA = new Set(infoA.functions.map((f) => f.name));
const funcsB = new Set(infoB.functions.map((f) => f.name));
const newFunctions = infoB.functions.map((f) => f.name).filter((f) => !funcsA.has(f));
return {
locDelta: infoB.loc - infoA.loc,
toolsOnlyA: onlyA,
toolsOnlyB: onlyB,
toolsShared: shared,
newClasses,
newFunctions,
};
}, [infoA, infoB]);
return (
<div className="py-4">
<div className="mb-8">
<h1 className="text-3xl font-bold">{t("title")}</h1>
<p className="mt-2 text-zinc-500 dark:text-zinc-400">{t("subtitle")}</p>
</div>
{/* Selectors */}
<div className="mb-8 flex flex-col items-start gap-4 sm:flex-row sm:items-center">
<div className="flex-1">
<label className="mb-1 block text-sm font-medium text-zinc-600 dark:text-zinc-400">
{t("select_a")}
</label>
<select
value={versionA}
onChange={(e) => setVersionA(e.target.value)}
className="w-full rounded-lg border border-zinc-300 bg-white px-3 py-2 text-sm dark:border-zinc-600 dark:bg-zinc-800 dark:text-zinc-200"
>
<option value="">-- select --</option>
{LEARNING_PATH.map((v) => (
<option key={v} value={v}>
{v} - {VERSION_META[v]?.title}
</option>
))}
</select>
</div>
<ArrowRight size={20} className="mt-5 hidden text-zinc-400 sm:block" />
<div className="flex-1">
<label className="mb-1 block text-sm font-medium text-zinc-600 dark:text-zinc-400">
{t("select_b")}
</label>
<select
value={versionB}
onChange={(e) => setVersionB(e.target.value)}
className="w-full rounded-lg border border-zinc-300 bg-white px-3 py-2 text-sm dark:border-zinc-600 dark:bg-zinc-800 dark:text-zinc-200"
>
<option value="">-- select --</option>
{LEARNING_PATH.map((v) => (
<option key={v} value={v}>
{v} - {VERSION_META[v]?.title}
</option>
))}
</select>
</div>
</div>
{/* Results */}
{infoA && infoB && comparison && (
<div className="space-y-8">
{/* Side-by-side version info */}
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<Card>
<CardHeader>
<CardTitle>{metaA?.title || versionA}</CardTitle>
<p className="text-sm text-zinc-500">{metaA?.subtitle}</p>
</CardHeader>
<div className="space-y-2 text-sm text-zinc-600 dark:text-zinc-400">
<p>{infoA.loc} LOC</p>
<p>{infoA.tools.length} tools</p>
{metaA && <LayerBadge layer={metaA.layer}>{metaA.layer}</LayerBadge>}
</div>
</Card>
<Card>
<CardHeader>
<CardTitle>{metaB?.title || versionB}</CardTitle>
<p className="text-sm text-zinc-500">{metaB?.subtitle}</p>
</CardHeader>
<div className="space-y-2 text-sm text-zinc-600 dark:text-zinc-400">
<p>{infoB.loc} LOC</p>
<p>{infoB.tools.length} tools</p>
{metaB && <LayerBadge layer={metaB.layer}>{metaB.layer}</LayerBadge>}
</div>
</Card>
</div>
{/* Side-by-side Architecture Diagrams */}
<div>
<h2 className="mb-4 text-xl font-semibold">{t("architecture")}</h2>
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
<div>
<h3 className="mb-3 text-sm font-medium text-zinc-500 dark:text-zinc-400">
{metaA?.title || versionA}
</h3>
<ArchDiagram version={versionA} />
</div>
<div>
<h3 className="mb-3 text-sm font-medium text-zinc-500 dark:text-zinc-400">
{metaB?.title || versionB}
</h3>
<ArchDiagram version={versionB} />
</div>
</div>
</div>
{/* Structural diff */}
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
<Card>
<CardHeader>
<div className="flex items-center gap-2 text-zinc-500 dark:text-zinc-400">
<FileCode size={16} />
<span className="text-sm">{t("loc_delta")}</span>
</div>
</CardHeader>
<CardTitle>
<span className={comparison.locDelta >= 0 ? "text-green-600 dark:text-green-400" : "text-red-600 dark:text-red-400"}>
{comparison.locDelta >= 0 ? "+" : ""}{comparison.locDelta}
</span>
<span className="ml-2 text-sm font-normal text-zinc-500">{t("lines")}</span>
</CardTitle>
</Card>
<Card>
<CardHeader>
<div className="flex items-center gap-2 text-zinc-500 dark:text-zinc-400">
<Wrench size={16} />
<span className="text-sm">{t("new_tools_in_b")}</span>
</div>
</CardHeader>
<CardTitle>
<span className="text-blue-600 dark:text-blue-400">{comparison.toolsOnlyB.length}</span>
</CardTitle>
{comparison.toolsOnlyB.length > 0 && (
<div className="mt-2 flex flex-wrap gap-1">
{comparison.toolsOnlyB.map((tool) => (
<span key={tool} className="rounded bg-blue-100 px-1.5 py-0.5 text-xs text-blue-700 dark:bg-blue-900/30 dark:text-blue-300">
{tool}
</span>
))}
</div>
)}
</Card>
<Card>
<CardHeader>
<div className="flex items-center gap-2 text-zinc-500 dark:text-zinc-400">
<Box size={16} />
<span className="text-sm">{t("new_classes_in_b")}</span>
</div>
</CardHeader>
<CardTitle>
<span className="text-purple-600 dark:text-purple-400">{comparison.newClasses.length}</span>
</CardTitle>
{comparison.newClasses.length > 0 && (
<div className="mt-2 flex flex-wrap gap-1">
{comparison.newClasses.map((cls) => (
<span key={cls} className="rounded bg-purple-100 px-1.5 py-0.5 text-xs text-purple-700 dark:bg-purple-900/30 dark:text-purple-300">
{cls}
</span>
))}
</div>
)}
</Card>
<Card>
<CardHeader>
<div className="flex items-center gap-2 text-zinc-500 dark:text-zinc-400">
<FunctionSquare size={16} />
<span className="text-sm">{t("new_functions_in_b")}</span>
</div>
</CardHeader>
<CardTitle>
<span className="text-amber-600 dark:text-amber-400">{comparison.newFunctions.length}</span>
</CardTitle>
{comparison.newFunctions.length > 0 && (
<div className="mt-2 flex flex-wrap gap-1">
{comparison.newFunctions.map((fn) => (
<span key={fn} className="rounded bg-amber-100 px-1.5 py-0.5 text-xs text-amber-700 dark:bg-amber-900/30 dark:text-amber-300">
{fn}
</span>
))}
</div>
)}
</Card>
</div>
{/* Tool comparison */}
<Card>
<CardHeader>
<CardTitle>{t("tool_comparison")}</CardTitle>
</CardHeader>
<div className="grid grid-cols-1 gap-6 sm:grid-cols-3">
<div>
<h4 className="mb-2 text-sm font-medium text-zinc-600 dark:text-zinc-400">
{t("only_in")} {metaA?.title || versionA}
</h4>
{comparison.toolsOnlyA.length === 0 ? (
<p className="text-xs text-zinc-400">{t("none")}</p>
) : (
<div className="flex flex-wrap gap-1">
{comparison.toolsOnlyA.map((tool) => (
<span key={tool} className="rounded bg-red-100 px-1.5 py-0.5 text-xs text-red-700 dark:bg-red-900/30 dark:text-red-300">
{tool}
</span>
))}
</div>
)}
</div>
<div>
<h4 className="mb-2 text-sm font-medium text-zinc-600 dark:text-zinc-400">
{t("shared")}
</h4>
{comparison.toolsShared.length === 0 ? (
<p className="text-xs text-zinc-400">{t("none")}</p>
) : (
<div className="flex flex-wrap gap-1">
{comparison.toolsShared.map((tool) => (
<span key={tool} className="rounded bg-zinc-100 px-1.5 py-0.5 text-xs text-zinc-700 dark:bg-zinc-800 dark:text-zinc-300">
{tool}
</span>
))}
</div>
)}
</div>
<div>
<h4 className="mb-2 text-sm font-medium text-zinc-600 dark:text-zinc-400">
{t("only_in")} {metaB?.title || versionB}
</h4>
{comparison.toolsOnlyB.length === 0 ? (
<p className="text-xs text-zinc-400">{t("none")}</p>
) : (
<div className="flex flex-wrap gap-1">
{comparison.toolsOnlyB.map((tool) => (
<span key={tool} className="rounded bg-green-100 px-1.5 py-0.5 text-xs text-green-700 dark:bg-green-900/30 dark:text-green-300">
{tool}
</span>
))}
</div>
)}
</div>
</div>
</Card>
{/* Code Diff */}
<div>
<h2 className="mb-4 text-xl font-semibold">{t("source_diff")}</h2>
<CodeDiff
oldSource={infoA.source}
newSource={infoB.source}
oldLabel={`${infoA.id} (${infoA.filename})`}
newLabel={`${infoB.id} (${infoB.filename})`}
/>
</div>
</div>
)}
{/* Empty state */}
{(!versionA || !versionB) && (
<div className="rounded-lg border border-dashed border-zinc-300 p-12 text-center dark:border-zinc-700">
<p className="text-zinc-400">{t("empty_hint")}</p>
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,133 @@
"use client";
import Link from "next/link";
import { useTranslations, useLocale } from "@/lib/i18n";
import { LAYERS, VERSION_META } from "@/lib/constants";
import { Card, CardHeader, CardTitle } from "@/components/ui/card";
import { LayerBadge } from "@/components/ui/badge";
import { cn } from "@/lib/utils";
import { ChevronRight } from "lucide-react";
import type { VersionIndex } from "@/types/agent-data";
import versionData from "@/data/generated/versions.json";
const data = versionData as VersionIndex;
const LAYER_BORDER_CLASSES: Record<string, string> = {
tools: "border-l-blue-500",
planning: "border-l-emerald-500",
memory: "border-l-purple-500",
concurrency: "border-l-amber-500",
collaboration: "border-l-red-500",
};
const LAYER_HEADER_BG: Record<string, string> = {
tools: "bg-blue-500",
planning: "bg-emerald-500",
memory: "bg-purple-500",
concurrency: "bg-amber-500",
collaboration: "bg-red-500",
};
export default function LayersPage() {
const t = useTranslations("layers");
const locale = useLocale();
return (
<div className="py-4">
<div className="mb-10">
<h1 className="text-3xl font-bold">{t("title")}</h1>
<p className="mt-2 text-zinc-500 dark:text-zinc-400">{t("subtitle")}</p>
</div>
<div className="space-y-6">
{LAYERS.map((layer, index) => {
const versionInfos = layer.versions.map((vId) => {
const info = data.versions.find((v) => v.id === vId);
const meta = VERSION_META[vId];
return { id: vId, info, meta };
});
return (
<div
key={layer.id}
className={cn(
"overflow-hidden rounded-xl border border-zinc-200 dark:border-zinc-800",
"border-l-4",
LAYER_BORDER_CLASSES[layer.id]
)}
>
{/* Layer header */}
<div className="flex items-center gap-3 px-6 py-4">
<div className={cn("h-3 w-3 rounded-full", LAYER_HEADER_BG[layer.id])} />
<div>
<h2 className="text-xl font-bold">
<span className="text-zinc-400 dark:text-zinc-600">L{index + 1}</span>
{" "}
{layer.label}
</h2>
<p className="mt-1 text-sm text-zinc-500 dark:text-zinc-400">
{t(layer.id)}
</p>
</div>
</div>
{/* Version cards within this layer */}
<div className="border-t border-zinc-200 bg-zinc-50/50 px-6 py-4 dark:border-zinc-800 dark:bg-zinc-900/50">
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3">
{versionInfos.map(({ id, info, meta }) => (
<Link
key={id}
href={`/${locale}/${id}`}
className="group"
>
<Card className="transition-shadow hover:shadow-md">
<div className="flex items-start justify-between">
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
<span className="text-xs font-mono text-zinc-400">{id}</span>
<LayerBadge layer={layer.id}>{layer.id}</LayerBadge>
</div>
<h3 className="mt-1 font-semibold text-zinc-900 dark:text-zinc-100">
{meta?.title || id}
</h3>
{meta?.subtitle && (
<p className="mt-0.5 text-xs text-zinc-500 dark:text-zinc-400">
{meta.subtitle}
</p>
)}
</div>
<ChevronRight
size={16}
className="mt-1 shrink-0 text-zinc-300 transition-colors group-hover:text-zinc-600 dark:text-zinc-600 dark:group-hover:text-zinc-300"
/>
</div>
<div className="mt-3 flex items-center gap-4 text-xs text-zinc-500 dark:text-zinc-400">
<span>{info?.loc ?? "?"} LOC</span>
<span>{info?.tools.length ?? "?"} tools</span>
</div>
{meta?.keyInsight && (
<p className="mt-2 text-xs leading-relaxed text-zinc-500 dark:text-zinc-400 line-clamp-2">
{meta.keyInsight}
</p>
)}
</Card>
</Link>
))}
</div>
</div>
{/* Composition indicator */}
{index < LAYERS.length - 1 && (
<div className="flex items-center justify-center py-1 text-zinc-300 dark:text-zinc-700">
<svg width="20" height="12" viewBox="0 0 20 12" fill="none" className="text-current">
<path d="M10 0 L10 12 M5 7 L10 12 L15 7" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</div>
)}
</div>
);
})}
</div>
</div>
);
}

View File

@@ -0,0 +1,14 @@
import { Sidebar } from "@/components/layout/sidebar";
export default function LearnLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="flex gap-8">
<Sidebar />
<div className="min-w-0 flex-1">{children}</div>
</div>
);
}

View File

@@ -0,0 +1,20 @@
"use client";
import { useTranslations } from "@/lib/i18n";
import { Timeline } from "@/components/timeline/timeline";
export default function TimelinePage() {
const t = useTranslations("timeline");
return (
<div>
<div className="mb-8">
<h1 className="text-3xl font-bold">{t("title")}</h1>
<p className="mt-2 text-[var(--color-text-secondary)]">
{t("subtitle")}
</p>
</div>
<Timeline />
</div>
);
}

View File

@@ -0,0 +1,60 @@
import type { Metadata } from "next";
import { I18nProvider } from "@/lib/i18n";
import { Header } from "@/components/layout/header";
import en from "@/i18n/messages/en.json";
import zh from "@/i18n/messages/zh.json";
import ja from "@/i18n/messages/ja.json";
import "../globals.css";
const locales = ["en", "zh", "ja"];
const metaMessages: Record<string, typeof en> = { en, zh, ja };
export function generateStaticParams() {
return locales.map((locale) => ({ locale }));
}
export async function generateMetadata({
params,
}: {
params: Promise<{ locale: string }>;
}): Promise<Metadata> {
const { locale } = await params;
const messages = metaMessages[locale] || metaMessages.en;
return {
title: messages.meta?.title || "Learn Claude Code",
description: messages.meta?.description || "Build an AI coding agent from scratch, one concept at a time",
};
}
export default async function RootLayout({
children,
params,
}: {
children: React.ReactNode;
params: Promise<{ locale: string }>;
}) {
const { locale } = await params;
return (
<html lang={locale} suppressHydrationWarning>
<head>
<script dangerouslySetInnerHTML={{ __html: `
(function() {
var theme = localStorage.getItem('theme');
if (theme === 'dark' || (!theme && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.add('dark');
}
})();
`}} />
</head>
<body className="min-h-screen bg-[var(--color-bg)] text-[var(--color-text)] antialiased">
<I18nProvider locale={locale}>
<Header />
<main className="mx-auto max-w-7xl px-4 py-8 sm:px-6 lg:px-8">
{children}
</main>
</I18nProvider>
</body>
</html>
);
}

View File

@@ -0,0 +1,234 @@
"use client";
import Link from "next/link";
import { useTranslations, useLocale } from "@/lib/i18n";
import { LEARNING_PATH, VERSION_META, LAYERS } from "@/lib/constants";
import { LayerBadge } from "@/components/ui/badge";
import { Card } from "@/components/ui/card";
import { cn } from "@/lib/utils";
import versionsData from "@/data/generated/versions.json";
import { MessageFlow } from "@/components/architecture/message-flow";
const LAYER_DOT_COLORS: Record<string, string> = {
tools: "bg-blue-500",
planning: "bg-emerald-500",
memory: "bg-purple-500",
concurrency: "bg-amber-500",
collaboration: "bg-red-500",
};
const LAYER_BORDER_COLORS: Record<string, string> = {
tools: "border-blue-500/30 hover:border-blue-500/60",
planning: "border-emerald-500/30 hover:border-emerald-500/60",
memory: "border-purple-500/30 hover:border-purple-500/60",
concurrency: "border-amber-500/30 hover:border-amber-500/60",
collaboration: "border-red-500/30 hover:border-red-500/60",
};
const LAYER_BAR_COLORS: Record<string, string> = {
tools: "bg-blue-500",
planning: "bg-emerald-500",
memory: "bg-purple-500",
concurrency: "bg-amber-500",
collaboration: "bg-red-500",
};
function getVersionData(id: string) {
return versionsData.versions.find((v) => v.id === id);
}
export default function HomePage() {
const t = useTranslations("home");
const locale = useLocale();
return (
<div className="flex flex-col gap-20 pb-16">
{/* Hero Section */}
<section className="flex flex-col items-center px-2 pt-8 text-center sm:pt-20">
<h1 className="text-3xl font-bold tracking-tight sm:text-5xl lg:text-6xl">
{t("hero_title")}
</h1>
<p className="mt-4 max-w-2xl text-base text-[var(--color-text-secondary)] sm:text-xl">
{t("hero_subtitle")}
</p>
<div className="mt-8">
<Link
href={`/${locale}/timeline`}
className="inline-flex min-h-[44px] items-center gap-2 rounded-lg bg-zinc-900 px-6 py-3 text-sm font-medium text-white transition-colors hover:bg-zinc-700 dark:bg-white dark:text-zinc-900 dark:hover:bg-zinc-200"
>
{t("start")}
<span aria-hidden="true">&rarr;</span>
</Link>
</div>
</section>
{/* Core Pattern Section */}
<section>
<div className="mb-6 text-center">
<h2 className="text-2xl font-bold sm:text-3xl">{t("core_pattern")}</h2>
<p className="mt-2 text-[var(--color-text-secondary)]">
{t("core_pattern_desc")}
</p>
</div>
<div className="mx-auto max-w-2xl overflow-hidden rounded-xl border border-zinc-800 bg-zinc-950">
<div className="flex items-center gap-2 border-b border-zinc-800 px-4 py-2.5">
<span className="h-3 w-3 rounded-full bg-red-500/70" />
<span className="h-3 w-3 rounded-full bg-yellow-500/70" />
<span className="h-3 w-3 rounded-full bg-green-500/70" />
<span className="ml-3 text-xs text-zinc-500">agent_loop.py</span>
</div>
<pre className="overflow-x-auto p-4 text-sm leading-relaxed">
<code>
<span className="text-purple-400">while</span>
<span className="text-zinc-300"> </span>
<span className="text-orange-300">True</span>
<span className="text-zinc-500">:</span>
{"\n"}
<span className="text-zinc-300">{" "}response = client.messages.</span>
<span className="text-blue-400">create</span>
<span className="text-zinc-500">(</span>
<span className="text-zinc-300">messages=</span>
<span className="text-zinc-300">messages</span>
<span className="text-zinc-500">,</span>
<span className="text-zinc-300"> tools=</span>
<span className="text-zinc-300">tools</span>
<span className="text-zinc-500">)</span>
{"\n"}
<span className="text-purple-400">{" "}if</span>
<span className="text-zinc-300"> response.stop_reason != </span>
<span className="text-green-400">&quot;tool_use&quot;</span>
<span className="text-zinc-500">:</span>
{"\n"}
<span className="text-purple-400">{" "}break</span>
{"\n"}
<span className="text-purple-400">{" "}for</span>
<span className="text-zinc-300"> tool_call </span>
<span className="text-purple-400">in</span>
<span className="text-zinc-300"> response.content</span>
<span className="text-zinc-500">:</span>
{"\n"}
<span className="text-zinc-300">{" "}result = </span>
<span className="text-blue-400">execute_tool</span>
<span className="text-zinc-500">(</span>
<span className="text-zinc-300">tool_call.name</span>
<span className="text-zinc-500">,</span>
<span className="text-zinc-300"> tool_call.input</span>
<span className="text-zinc-500">)</span>
{"\n"}
<span className="text-zinc-300">{" "}messages.</span>
<span className="text-blue-400">append</span>
<span className="text-zinc-500">(</span>
<span className="text-zinc-300">result</span>
<span className="text-zinc-500">)</span>
</code>
</pre>
</div>
</section>
{/* Message Flow Visualization */}
<section>
<div className="mb-6 text-center">
<h2 className="text-2xl font-bold sm:text-3xl">{t("message_flow")}</h2>
<p className="mt-2 text-[var(--color-text-secondary)]">
{t("message_flow_desc")}
</p>
</div>
<div className="mx-auto max-w-2xl">
<MessageFlow />
</div>
</section>
{/* Learning Path Preview */}
<section>
<div className="mb-6 text-center">
<h2 className="text-2xl font-bold sm:text-3xl">{t("learning_path")}</h2>
<p className="mt-2 text-[var(--color-text-secondary)]">
{t("learning_path_desc")}
</p>
</div>
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3">
{LEARNING_PATH.map((versionId) => {
const meta = VERSION_META[versionId];
const data = getVersionData(versionId);
if (!meta || !data) return null;
return (
<Link
key={versionId}
href={`/${locale}/${versionId}`}
className="group block"
>
<Card
className={cn(
"h-full border transition-all duration-200",
LAYER_BORDER_COLORS[meta.layer]
)}
>
<div className="flex items-start justify-between gap-2">
<LayerBadge layer={meta.layer}>{versionId}</LayerBadge>
<span className="text-xs tabular-nums text-[var(--color-text-secondary)]">
{data.loc} {t("loc")}
</span>
</div>
<h3 className="mt-3 text-sm font-semibold group-hover:underline">
{meta.title}
</h3>
<p className="mt-1 text-xs text-[var(--color-text-secondary)]">
{meta.keyInsight}
</p>
</Card>
</Link>
);
})}
</div>
</section>
{/* Layer Overview */}
<section>
<div className="mb-6 text-center">
<h2 className="text-2xl font-bold sm:text-3xl">{t("layers_title")}</h2>
<p className="mt-2 text-[var(--color-text-secondary)]">
{t("layers_desc")}
</p>
</div>
<div className="flex flex-col gap-3">
{LAYERS.map((layer) => (
<div
key={layer.id}
className="flex items-center gap-4 rounded-xl border border-[var(--color-border)] bg-[var(--color-bg)] p-4"
>
<div
className={cn(
"h-full w-1.5 self-stretch rounded-full",
LAYER_BAR_COLORS[layer.id]
)}
/>
<div className="flex-1">
<div className="flex items-center gap-2">
<h3 className="text-sm font-semibold">{layer.label}</h3>
<span className="text-xs text-[var(--color-text-secondary)]">
{layer.versions.length} {t("versions_in_layer")}
</span>
</div>
<div className="mt-2 flex flex-wrap gap-1.5">
{layer.versions.map((vid) => {
const meta = VERSION_META[vid];
return (
<Link key={vid} href={`/${locale}/${vid}`}>
<LayerBadge
layer={layer.id}
className="cursor-pointer transition-opacity hover:opacity-80"
>
{vid}: {meta?.title}
</LayerBadge>
</Link>
);
})}
</div>
</div>
</div>
))}
</div>
</section>
</div>
);
}

BIN
web/src/app/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

555
web/src/app/globals.css Normal file
View File

@@ -0,0 +1,555 @@
@import "tailwindcss";
@custom-variant dark (&:where(.dark, .dark *));
:root {
--color-layer-tools: #3B82F6;
--color-layer-planning: #10B981;
--color-layer-memory: #8B5CF6;
--color-layer-concurrency: #F59E0B;
--color-layer-collaboration: #EF4444;
--color-bg: #ffffff;
--color-bg-secondary: #f4f4f5;
--color-text: #09090b;
--color-text-secondary: #71717a;
--color-border: #e4e4e7;
}
.dark {
--color-bg: #09090b;
--color-bg-secondary: #18181b;
--color-text: #fafafa;
--color-text-secondary: #a1a1aa;
--color-border: #27272a;
}
body {
background: var(--color-bg);
color: var(--color-text);
}
@media (max-width: 640px) {
pre, code {
font-size: 11px;
}
}
* {
-webkit-tap-highlight-color: transparent;
}
/* =====================================================
PROSE-CUSTOM: Premium documentation rendering
===================================================== */
/* -- Headings -- */
.prose-custom h1 {
margin-top: 2.5rem;
margin-bottom: 1rem;
font-size: 1.5rem;
line-height: 2rem;
font-weight: 800;
letter-spacing: -0.02em;
color: #09090b;
}
.dark .prose-custom h1 {
color: #fafafa;
}
.prose-custom h2 {
margin-top: 2.5rem;
margin-bottom: 1rem;
padding-bottom: 0.5rem;
font-size: 1.25rem;
line-height: 1.75rem;
font-weight: 700;
letter-spacing: -0.01em;
color: #09090b;
border-bottom: 1px solid #e4e4e7;
}
.dark .prose-custom h2 {
color: #fafafa;
border-bottom-color: #27272a;
}
.prose-custom h3 {
margin-top: 2rem;
margin-bottom: 0.75rem;
font-size: 1.0625rem;
line-height: 1.5rem;
font-weight: 600;
color: #18181b;
}
.dark .prose-custom h3 {
color: #e4e4e7;
}
.prose-custom h4 {
margin-top: 1.5rem;
margin-bottom: 0.5rem;
font-size: 0.9375rem;
line-height: 1.5rem;
font-weight: 600;
color: #27272a;
}
.dark .prose-custom h4 {
color: #d4d4d8;
}
/* -- Paragraphs -- */
.prose-custom p {
margin-top: 0.75rem;
margin-bottom: 0.75rem;
font-size: 0.9rem;
line-height: 1.7;
color: #3f3f46;
}
.dark .prose-custom p {
color: #d4d4d8;
}
/* -- Hero callout (first blockquote) -- */
.prose-custom blockquote.hero-callout {
position: relative;
margin-top: 0;
margin-bottom: 1.5rem;
border-left: none;
border-radius: 0.75rem;
background: linear-gradient(135deg, #eff6ff 0%, #f0fdf4 100%);
padding: 1.25rem 1.5rem 1.25rem 1.75rem;
font-style: normal;
overflow: hidden;
}
.prose-custom blockquote.hero-callout::before {
content: '';
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 4px;
background: linear-gradient(to bottom, #3b82f6, #10b981);
border-radius: 4px 0 0 4px;
}
.prose-custom blockquote.hero-callout p {
font-size: 0.95rem;
line-height: 1.65;
font-weight: 500;
color: #1e40af;
margin: 0;
}
.dark .prose-custom blockquote.hero-callout {
background: linear-gradient(135deg, #172554 0%, #052e16 100%);
}
.dark .prose-custom blockquote.hero-callout p {
color: #93c5fd;
}
/* -- Regular blockquotes -- */
.prose-custom blockquote {
margin-top: 1rem;
margin-bottom: 1rem;
border-left: 3px solid #a5b4fc;
border-radius: 0 0.5rem 0.5rem 0;
background-color: #eef2ff;
padding: 0.75rem 1rem;
font-style: normal;
}
.prose-custom blockquote p {
color: #4338ca;
font-size: 0.875rem;
margin: 0;
}
.dark .prose-custom blockquote {
border-left-color: #6366f1;
background-color: rgba(99, 102, 241, 0.1);
}
.dark .prose-custom blockquote p {
color: #c7d2fe;
}
/* -- Code blocks with language label -- */
.prose-custom pre {
position: relative;
overflow-x: auto;
margin-top: 1rem;
margin-bottom: 1rem;
border-radius: 0.75rem;
border: 1px solid #1e293b;
background-color: #0f172a;
padding: 1.25rem;
font-size: 0.8125rem;
line-height: 1.6;
color: #e2e8f0;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
}
.prose-custom pre.code-block {
padding-top: 2.25rem;
}
.prose-custom pre.code-block::before {
content: attr(data-language);
position: absolute;
top: 0;
right: 0.75rem;
padding: 0.125rem 0.625rem 0.25rem;
background: #3b82f6;
color: #ffffff;
font-size: 0.625rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.08em;
border-radius: 0 0 0.375rem 0.375rem;
font-family: system-ui, -apple-system, sans-serif;
}
.prose-custom pre.code-block[data-language="sh"]::before {
background: #22c55e;
content: "terminal";
}
/* -- ASCII diagram containers -- */
.prose-custom pre.ascii-diagram {
background: linear-gradient(135deg, #f8fafc, #f1f5f9);
border: 1px solid #cbd5e1;
color: #334155;
text-align: center;
font-size: 0.75rem;
line-height: 1.35;
padding: 1.5rem 1rem;
}
.dark .prose-custom pre.ascii-diagram {
background: linear-gradient(135deg, #1e1b4b, #172554);
border-color: #312e81;
color: #c7d2fe;
}
/* -- Inline code -- */
.prose-custom :not(pre) > code {
border-radius: 0.375rem;
background-color: #f1f5f9;
border: 1px solid #e2e8f0;
padding: 0.125rem 0.425rem;
font-size: 0.8125rem;
font-weight: 500;
color: #be185d;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
}
.dark .prose-custom :not(pre) > code {
background-color: #27272a;
border-color: #3f3f46;
color: #f9a8d4;
}
/* -- Links -- */
.prose-custom a {
color: #2563eb;
font-weight: 500;
text-decoration: underline;
text-decoration-color: #93c5fd;
text-underline-offset: 2px;
transition: text-decoration-color 0.15s;
}
.prose-custom a:hover {
text-decoration-color: #2563eb;
}
.dark .prose-custom a {
color: #60a5fa;
text-decoration-color: #1e40af;
}
.dark .prose-custom a:hover {
text-decoration-color: #60a5fa;
}
/* -- Lists -- */
.prose-custom ul {
margin-top: 0.75rem;
margin-bottom: 0.75rem;
padding-left: 1.5rem;
font-size: 0.9rem;
line-height: 1.7;
color: #3f3f46;
}
.dark .prose-custom ul {
color: #d4d4d8;
}
.prose-custom ul > li {
margin-top: 0.375rem;
margin-bottom: 0.375rem;
position: relative;
}
.prose-custom ul > li::marker {
color: #3b82f6;
}
.prose-custom ol {
margin-top: 0.75rem;
margin-bottom: 0.75rem;
padding-left: 0;
list-style: none;
counter-reset: step-counter;
font-size: 0.9rem;
line-height: 1.7;
color: #3f3f46;
}
.dark .prose-custom ol {
color: #d4d4d8;
}
.prose-custom ol > li {
counter-increment: step-counter;
margin-top: 0.75rem;
margin-bottom: 0.75rem;
padding-left: 2.75rem;
position: relative;
}
.prose-custom ol > li::before {
content: counter(step-counter);
position: absolute;
left: 0;
top: 0;
width: 1.75rem;
height: 1.75rem;
display: flex;
align-items: center;
justify-content: center;
border-radius: 0.5rem;
background: linear-gradient(135deg, #3b82f6, #6366f1);
color: #ffffff;
font-size: 0.75rem;
font-weight: 700;
font-family: ui-monospace, SFMono-Regular, monospace;
flex-shrink: 0;
}
/* Reset nested lists inside ol to normal style */
.prose-custom ol > li > ul {
padding-left: 1.25rem;
}
.prose-custom ol > li > ul > li {
padding-left: 0;
}
.prose-custom ol > li > ul > li::before {
display: none;
}
/* -- Tables -- */
.prose-custom table {
width: 100%;
margin-top: 1.25rem;
margin-bottom: 1.25rem;
border-collapse: separate;
border-spacing: 0;
font-size: 0.8125rem;
line-height: 1.5;
border-radius: 0.75rem;
overflow: hidden;
border: 1px solid #e2e8f0;
}
.dark .prose-custom table {
border-color: #27272a;
}
.prose-custom thead {
border-bottom: none;
}
.prose-custom th {
padding: 0.625rem 1rem;
text-align: left;
font-weight: 600;
font-size: 0.6875rem;
text-transform: uppercase;
letter-spacing: 0.05em;
color: #64748b;
background-color: #f8fafc;
border-bottom: 1px solid #e2e8f0;
}
.dark .prose-custom th {
color: #94a3b8;
background-color: #18181b;
border-bottom-color: #27272a;
}
.prose-custom td {
padding: 0.625rem 1rem;
border-bottom: 1px solid #f1f5f9;
color: #475569;
}
.prose-custom td code {
font-size: 0.75rem;
}
.dark .prose-custom td {
border-bottom-color: #1e1e22;
color: #cbd5e1;
}
.prose-custom tbody tr:last-child td {
border-bottom: none;
}
.prose-custom tbody tr:hover {
background-color: #f8fafc;
}
.dark .prose-custom tbody tr:hover {
background-color: #111113;
}
/* -- Horizontal rules -- */
.prose-custom hr {
margin-top: 2rem;
margin-bottom: 2rem;
border: none;
height: 1px;
background: linear-gradient(to right, transparent, #d4d4d8, transparent);
}
.dark .prose-custom hr {
background: linear-gradient(to right, transparent, #3f3f46, transparent);
}
/* -- Strong / Em -- */
.prose-custom strong {
font-weight: 700;
color: #09090b;
}
.dark .prose-custom strong {
color: #fafafa;
}
.prose-custom em {
font-style: italic;
color: #52525b;
}
.dark .prose-custom em {
color: #a1a1aa;
}
/* =====================================================
HIGHLIGHT.JS TOKEN THEME (code syntax highlighting)
===================================================== */
.hljs {
background: transparent !important;
}
.hljs-keyword,
.hljs-selector-tag,
.hljs-type {
color: #c084fc;
font-weight: 500;
}
.hljs-literal,
.hljs-symbol,
.hljs-bullet {
color: #fb923c;
}
.hljs-string,
.hljs-doctag,
.hljs-template-variable,
.hljs-variable {
color: #34d399;
}
.hljs-number {
color: #fb923c;
}
.hljs-comment,
.hljs-quote {
color: #64748b;
font-style: italic;
}
.hljs-title,
.hljs-section {
color: #60a5fa;
font-weight: 600;
}
.hljs-title.function_,
.hljs-title.class_ {
color: #60a5fa;
}
.hljs-built_in {
color: #f472b6;
}
.hljs-attr,
.hljs-attribute {
color: #fbbf24;
}
.hljs-params {
color: #e2e8f0;
}
.hljs-meta {
color: #94a3b8;
}
.hljs-name,
.hljs-tag {
color: #f87171;
}
.hljs-selector-class,
.hljs-selector-id {
color: #a78bfa;
}
.hljs-deletion {
color: #fca5a5;
background-color: rgba(239, 68, 68, 0.15);
}
.hljs-addition {
color: #86efac;
background-color: rgba(34, 197, 94, 0.15);
}