feat(web): community site for deepseek-tui.com (mobile + color refresh) (#1108)

First commit of the Next.js community site that powers
deepseek-tui.com, deployed via Cloudflare Workers / OpenNext.

This commit lands the scaffold and applies the visual + correctness
pass requested by community feedback:

- Palette: drop the cream/Anthropic-feel paper (#F4F1E8) for a
  DeepSeek-aligned cool white + soft gray (#FFFFFF / #F4F6FB), with
  indigo accents kept. Soften default hairlines so a pure-white
  background reads clean instead of harsh.
- Mobile: add a hamburger menu (mobile-menu.tsx) so phones can reach
  Install / Docs / Activity / Roadmap / Contribute — previously the
  link list was hidden on phones with no replacement. Tighter hero,
  flexible button row, viewport-safe code blocks, columnar grids
  collapse cleanly under 768px, and the printed-almanac center rule
  is desktop-only now (it sliced through narrow viewports).
- "How it works" diagram: replace the hand-rolled ASCII art (which
  misaligned under CJK monospace because Han characters take 2
  columns vs Latin's 1, per dhh's note in WeChat) with a real
  mermaid diagram rendered client-side via dynamic import. Uses the
  mermaid.live standard syntax 庄表伟 recommended.
- Issue #1104: the docs listed a `deepseek-cn` provider that the
  v0.8.16 binary doesn't accept (`ProviderArg` in crates/cli only
  has 9 variants; the 10th lives only in the legacy tui/config.rs).
  derive-facts.mjs now omits `deepseek-cn` until that variant is
  wired through the shared ProviderKind, and the install page's
  China-network recipe uses `base_url` / `DEEPSEEK_BASE_URL` (which
  actually works on v0.8.16) instead of the unsupported provider.

Auto-deploys via .github/workflows/deploy-web.yml on push to main.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Hunter Bown
2026-05-07 21:00:06 -05:00
committed by GitHub
parent b3329f69f1
commit 9e45780ba0
61 changed files with 20077 additions and 0 deletions
+59
View File
@@ -0,0 +1,59 @@
import Link from "next/link";
import type { FeedItem } from "@/lib/types";
import { relativeTime } from "@/lib/github";
const KIND_LABEL: Record<FeedItem["kind"], { label: string; cn: string }> = {
issue: { label: "Issue", cn: "议题" },
pull: { label: "Pull", cn: "合并" },
release: { label: "Release", cn: "发布" },
discussion: { label: "Talk", cn: "讨论" },
};
function statePill(state: FeedItem["state"]) {
const map: Record<FeedItem["state"], string> = {
open: "pill pill-jade",
closed: "pill pill-ghost",
merged: "pill pill-hot",
draft: "pill pill-ghost",
published: "pill pill-ochre",
};
return <span className={map[state]}>{state}</span>;
}
export function FeedCard({ item, dense = false }: { item: FeedItem; dense?: boolean }) {
const k = KIND_LABEL[item.kind];
return (
<article className={`hairline-b py-4 ${dense ? "" : "px-1"}`}>
<div className="flex items-baseline gap-3 mb-1.5">
<span className="font-mono text-[0.66rem] uppercase tracking-widest text-indigo">
{k.label} <span className="font-cjk text-ink-mute normal-case tracking-normal ml-1">{k.cn}</span>
</span>
<span className="font-mono text-[0.7rem] text-ink-mute tabular">#{item.number}</span>
<span className="ml-auto font-mono text-[0.7rem] text-ink-mute tabular">{relativeTime(item.updatedAt)}</span>
</div>
<h3 className="font-display text-base leading-snug">
<Link href={item.url} className="hover:text-indigo transition-colors">
{item.title}
</Link>
</h3>
<div className="flex items-center gap-3 mt-2 flex-wrap">
{statePill(item.state)}
{item.labels.slice(0, 3).map((l) => (
<span
key={l.name}
className="pill pill-ghost"
style={{ borderColor: `#${l.color}`, color: `#${l.color}` }}
>
{l.name}
</span>
))}
<span className="ml-auto flex items-center gap-2 font-mono text-[0.7rem] text-ink-mute">
<span>@{item.author}</span>
{item.comments > 0 && <span className="tabular">· {item.comments} reply{item.comments === 1 ? "" : "s"}</span>}
</span>
</div>
</article>
);
}
+143
View File
@@ -0,0 +1,143 @@
import Link from "next/link";
import { GITEE_ENABLED, type Locale } from "@/lib/i18n/config";
import { Seal } from "./seal";
const EN_COLS = [
{
title: "Product",
cn: "产品",
items: [
{ label: "Install", href: "/install" },
{ label: "Documentation", href: "/docs" },
{ label: "Roadmap", href: "/roadmap" },
{ label: "Releases", href: "https://github.com/Hmbown/deepseek-tui/releases" },
],
},
{
title: "Community",
cn: "社区",
items: [
{ label: "Issues", href: "https://github.com/Hmbown/deepseek-tui/issues" },
{ label: "Pull Requests", href: "https://github.com/Hmbown/deepseek-tui/pulls" },
{ label: "Discussions", href: "https://github.com/Hmbown/deepseek-tui/discussions" },
{ label: "Contribute", href: "/contribute" },
{ label: "Support DeepSeek TUI", href: "https://buymeacoffee.com/hmbown" },
],
},
{
title: "Resources",
cn: "资源",
items: [
{ label: "Activity Feed", href: "/feed" },
{ label: "Code of Conduct", href: "https://github.com/Hmbown/deepseek-tui/blob/main/CODE_OF_CONDUCT.md" },
{ label: "Security", href: "https://github.com/Hmbown/deepseek-tui/blob/main/SECURITY.md" },
{ label: "License (MIT)", href: "https://github.com/Hmbown/deepseek-tui/blob/main/LICENSE" },
],
},
];
const ZH_COLS = [
{
title: "产品",
items: [
{ label: "安装指南", href: "/zh/install" },
{ label: "使用文档", href: "/zh/docs" },
{ label: "路线图", href: "/zh/roadmap" },
{ label: "版本发布", href: "https://github.com/Hmbown/deepseek-tui/releases" },
],
},
{
title: "社区",
items: [
{ label: "议题", href: "https://github.com/Hmbown/deepseek-tui/issues" },
{ label: "合并请求", href: "https://github.com/Hmbown/deepseek-tui/pulls" },
{ label: "讨论区", href: "https://github.com/Hmbown/deepseek-tui/discussions" },
{ label: "参与贡献", href: "/zh/contribute" },
{ label: "支持 DeepSeek TUI", href: "https://buymeacoffee.com/hmbown" },
],
},
{
title: "资源",
items: [
{ label: "活动动态", href: "/zh/feed" },
{ label: "行为准则", href: "https://github.com/Hmbown/deepseek-tui/blob/main/CODE_OF_CONDUCT.md" },
{ label: "安全策略", href: "https://github.com/Hmbown/deepseek-tui/blob/main/SECURITY.md" },
{ label: "MIT 许可证", href: "https://github.com/Hmbown/deepseek-tui/blob/main/LICENSE" },
],
},
];
export function Footer({ locale = "en" }: { locale?: Locale }) {
const isZh = locale === "zh";
const cols = isZh ? ZH_COLS : EN_COLS;
return (
<footer className="hairline-t mt-24 bg-paper-deep">
<div className="mx-auto max-w-[1400px] px-6 py-12 grid grid-cols-2 md:grid-cols-5 gap-10">
<div className="col-span-2 md:col-span-2 space-y-4">
<div className="flex items-center gap-3">
<Seal char="深" size="md" />
<div>
<div className="font-display text-xl font-semibold">DeepSeek TUI</div>
<div className="font-cjk text-[0.7rem] text-ink-mute tracking-widest">
{isZh ? "深度求索 · 终端智能体" : "深度求索 · 终端代理"}
</div>
</div>
</div>
<p className="text-sm text-ink-soft max-w-md leading-relaxed">
{isZh
? "基于 DeepSeek V4 的开源终端编程智能体。MIT 许可证。由一位维护者从得克萨斯独立维护。欢迎提交 Pull Request。"
: "Open-source terminal-native coding agent built on DeepSeek V4. MIT licensed. Maintained from a small workshop in Texas. Pull requests welcome."}
</p>
<div className="font-mono text-[0.7rem] text-ink-mute uppercase tracking-widest">
{isZh ? "用心制作 · Made with care" : "Made with care · 用心制作"}
</div>
{/* Mirror sources — prominent on zh */}
{isZh && (
<div className="pt-2 border-t border-paper-line/20">
<div className="eyebrow mb-2 text-ink-mute"> / Mirror</div>
<div className="flex flex-wrap gap-3 text-xs">
{GITEE_ENABLED && <a href="https://gitee.com/Hmbown/deepseek-tui" className="text-indigo hover:underline" target="_blank" rel="noopener">Gitee </a>}
<a href="https://npmmirror.com/package/deepseek-tui" className="text-indigo hover:underline" target="_blank" rel="noopener">npmmirror</a>
<a href="https://mirrors.tuna.tsinghua.edu.cn/help/crates.io-index.html" className="text-indigo hover:underline" target="_blank" rel="noopener">Tuna crates.io</a>
</div>
</div>
)}
</div>
{cols.map((c) => (
<div key={c.title}>
<div className="eyebrow mb-3">
{isZh ? c.title : `${c.title} · `}
{!isZh && "cn" in c && <span className="font-cjk normal-case tracking-normal">{(c as { cn?: string }).cn}</span>}
</div>
<ul className="space-y-2">
{c.items.map((it) => (
<li key={it.href}>
<Link href={it.href} className="text-sm text-ink hover:text-indigo transition-colors">
{it.label}
</Link>
</li>
))}
</ul>
</div>
))}
</div>
<div className="hairline-t">
<div className="mx-auto max-w-[1400px] px-6 py-4 flex flex-col gap-2 text-[0.78rem] text-ink-soft">
<div>
{isZh ? "咨询、投资、研究合作、媒体采访 — " : "For consulting, investors, researchers, or press — "}
<a href="mailto:hunter@shannonlabs.dev" className="font-mono text-ink hover:text-indigo">hunter@shannonlabs.dev</a>
</div>
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-2 font-mono text-[0.7rem] text-ink-mute uppercase tracking-widest">
<span>© {new Date().getFullYear()} · DeepSeek TUI · Hmbown</span>
<span className="font-cjk normal-case tracking-normal">
{isZh ? "本网站由 DeepSeek V4-Flash 协助维护" : "本网站由 DeepSeek V4-Flash 协同维护"}
</span>
</div>
</div>
</div>
</footer>
);
}
+331
View File
@@ -0,0 +1,331 @@
"use client";
import { useEffect, useState } from "react";
type OS = "macos" | "linux" | "windows" | "any";
interface Method {
id: string;
os: OS;
label: string;
cn: string;
recommended?: boolean;
comingSoon?: boolean;
prereq: string;
cmd: string;
}
const METHODS: Method[] = [
// ─── macOS ────────────────────────────────────────────────
{
id: "cargo-mac",
os: "macos",
label: "Cargo (recommended)",
cn: "Cargo · 推荐",
recommended: true,
prereq: "Rust 1.88+ — install via rustup.rs if needed",
cmd: `# Install the dispatcher (provides \`deepseek\`)
cargo install deepseek-tui-cli --locked
# Optional: also install the raw TUI binary (\`deepseek-tui\`)
cargo install deepseek-tui --locked
# Set your API key (one-time)
export DEEPSEEK_API_KEY=sk-...
echo 'export DEEPSEEK_API_KEY=sk-...' >> ~/.zshrc
# Run it
deepseek`,
},
{
id: "npm-mac",
os: "macos",
label: "npm wrapper",
cn: "npm 包",
prereq: "Node.js 18+",
cmd: `npm install -g deepseek-tui
# Provides both binaries on PATH:
deepseek # canonical dispatcher
deepseek-tui # raw TUI binary`,
},
{
id: "binary-mac",
os: "macos",
label: "Pre-built binary",
cn: "二进制",
prereq: "Apple Silicon (arm64) or Intel (x64). Releases ship raw binaries — no archive to extract.",
cmd: `# Apple Silicon
curl -fsSL -o deepseek \\
https://github.com/Hmbown/deepseek-tui/releases/latest/download/deepseek-macos-arm64
chmod +x deepseek
xattr -d com.apple.quarantine deepseek 2>/dev/null || true
sudo mv deepseek /usr/local/bin/
# Intel
curl -fsSL -o deepseek \\
https://github.com/Hmbown/deepseek-tui/releases/latest/download/deepseek-macos-x64
chmod +x deepseek
xattr -d com.apple.quarantine deepseek 2>/dev/null || true
sudo mv deepseek /usr/local/bin/
# Verify checksum (optional but recommended)
curl -fsSL -O https://github.com/Hmbown/deepseek-tui/releases/latest/download/deepseek-artifacts-sha256.txt
shasum -a 256 -c deepseek-artifacts-sha256.txt --ignore-missing
deepseek`,
},
{
id: "brew",
os: "macos",
label: "Homebrew",
cn: "Homebrew",
prereq: "Homebrew on macOS or Linux; installs the dispatcher and companion TUI from the official Hmbown tap.",
cmd: `brew tap Hmbown/deepseek-tui
brew install deepseek-tui
deepseek --version
deepseek`,
},
// ─── Linux ────────────────────────────────────────────────
{
id: "cargo-linux",
os: "linux",
label: "Cargo (recommended)",
cn: "Cargo · 推荐",
recommended: true,
prereq: "Rust 1.88+; on Debian/Ubuntu: apt install build-essential pkg-config libssl-dev",
cmd: `cargo install deepseek-tui-cli --locked
export DEEPSEEK_API_KEY=sk-...
deepseek`,
},
{
id: "npm-linux",
os: "linux",
label: "npm wrapper",
cn: "npm 包",
prereq: "Node.js 18+",
cmd: `npm install -g deepseek-tui
deepseek`,
},
{
id: "binary-linux",
os: "linux",
label: "Pre-built binary",
cn: "二进制",
prereq: "x86_64 or aarch64 glibc. Releases ship raw binaries — no archive to extract.",
cmd: `# x86_64
curl -fsSL -o deepseek \\
https://github.com/Hmbown/deepseek-tui/releases/latest/download/deepseek-linux-x64
chmod +x deepseek
sudo mv deepseek /usr/local/bin/
# arm64
curl -fsSL -o deepseek \\
https://github.com/Hmbown/deepseek-tui/releases/latest/download/deepseek-linux-arm64
chmod +x deepseek
sudo mv deepseek /usr/local/bin/
# Verify checksum (optional but recommended)
curl -fsSL -O https://github.com/Hmbown/deepseek-tui/releases/latest/download/deepseek-artifacts-sha256.txt
sha256sum -c deepseek-artifacts-sha256.txt --ignore-missing
deepseek`,
},
// ─── Windows ──────────────────────────────────────────────
{
id: "cargo-win",
os: "windows",
label: "Cargo (recommended)",
cn: "Cargo · 推荐",
recommended: true,
prereq: "Rust 1.88+ via rustup-init.exe",
cmd: `cargo install deepseek-tui-cli --locked
$env:DEEPSEEK_API_KEY = "sk-..."
deepseek`,
},
{
id: "npm-win",
os: "windows",
label: "npm wrapper",
cn: "npm 包",
prereq: "Node.js 18+",
cmd: `npm install -g deepseek-tui
deepseek`,
},
{
id: "binary-win",
os: "windows",
label: "Pre-built binary",
cn: "二进制",
prereq: "Windows 10+ x64. Releases ship a raw .exe — no archive to extract.",
cmd: `# PowerShell
$ErrorActionPreference = "Stop"
$dest = "$Env:USERPROFILE\\bin"
New-Item -ItemType Directory -Force $dest | Out-Null
Invoke-WebRequest \`
-Uri https://github.com/Hmbown/deepseek-tui/releases/latest/download/deepseek-windows-x64.exe \`
-OutFile "$dest\\deepseek.exe"
# Add to PATH for this session (persist via System Properties → Environment Variables)
$Env:Path = "$dest;$Env:Path"
$Env:DEEPSEEK_API_KEY = "sk-..."
deepseek`,
},
{
id: "scoop",
os: "windows",
label: "Scoop",
cn: "Scoop",
comingSoon: true,
prereq: "Scoop manifest not yet published — use Cargo or the pre-built .exe above.",
cmd: `# Coming soon — no Scoop manifest yet.
# Working alternatives on Windows:
# - Cargo (recommended above)
# - Pre-built deepseek-windows-x64.exe (above)
#
# Track progress:
# https://github.com/Hmbown/deepseek-tui/issues`,
},
// ─── Any (cross-platform) ────────────────────────────────
{
id: "docker",
os: "any",
label: "Docker",
cn: "Docker",
prereq: "Dockerfile ships with the repo (multi-arch buildx). No prebuilt image is published to a registry yet.",
cmd: `git clone https://github.com/Hmbown/deepseek-tui
cd deepseek-tui
# Build for your local arch
docker build -t deepseek-tui .
# Or multi-arch via buildx
docker buildx build --platform linux/amd64,linux/arm64 -t deepseek-tui .
# Run interactively, mounting your config + a project
docker run --rm -it \\
-e DEEPSEEK_API_KEY=$DEEPSEEK_API_KEY \\
-v ~/.deepseek:/home/deepseek/.deepseek \\
-v "$PWD:/work" -w /work \\
deepseek-tui`,
},
{
id: "from-source",
os: "any",
label: "Build from source",
cn: "源码编译",
prereq: "Rust 1.88+ and a git checkout — useful for hacking on the workspace itself.",
cmd: `git clone https://github.com/Hmbown/deepseek-tui
cd deepseek-tui
# Builds both \`deepseek\` and \`deepseek-tui\` into ./target/release/
cargo build --release --locked
# Run without installing
./target/release/deepseek
# Or install both binaries from your local checkout
cargo install --path crates/cli --locked # provides \`deepseek\`
cargo install --path crates/tui --locked # provides \`deepseek-tui\``,
},
];
const OS_LABEL: Record<OS, { en: string; cn: string }> = {
macos: { en: "macOS", cn: "苹果" },
linux: { en: "Linux", cn: "Linux" },
windows: { en: "Windows", cn: "视窗" },
any: { en: "Any platform", cn: "通用" },
};
function detectOS(): OS {
if (typeof navigator === "undefined") return "macos";
const ua = navigator.userAgent.toLowerCase();
if (ua.includes("mac")) return "macos";
if (ua.includes("win")) return "windows";
if (ua.includes("linux")) return "linux";
return "macos";
}
export function InstallTabs() {
const [os, setOS] = useState<OS>("macos");
const [copied, setCopied] = useState<string | null>(null);
useEffect(() => { setOS(detectOS()); }, []);
// Show OS-specific methods + universal ones (Docker status / source build).
// On the "Any" tab, only show universal ones.
const methods = METHODS.filter((m) => (os === "any" ? m.os === "any" : m.os === os || m.os === "any"));
const copy = (id: string, text: string) => {
navigator.clipboard?.writeText(text);
setCopied(id);
setTimeout(() => setCopied(null), 1400);
};
return (
<div>
{/* OS selector */}
<div className="hairline-t hairline-b grid grid-cols-4">
{(["macos", "linux", "windows", "any"] as OS[]).map((o) => {
const active = os === o;
return (
<button
key={o}
onClick={() => setOS(o)}
className={`px-4 py-4 text-left transition-colors hairline-l first:border-l-0 ${
active ? "bg-ink text-paper" : "bg-paper hover:bg-paper-deep"
}`}
>
<div className={`eyebrow mb-1 ${active ? "text-paper-deep/70" : ""}`}>
{active ? "▼ " : ""}Detected · {o === detectOS() ? "auto" : "switch"}
</div>
<div className="font-display text-lg leading-tight">{OS_LABEL[o].en}</div>
<div className={`font-cjk text-xs ${active ? "text-paper-deep/80" : "text-ink-mute"}`}>
{OS_LABEL[o].cn}
</div>
</button>
);
})}
</div>
{/* methods */}
<div className="hairline-b">
{methods.map((m, i) => (
<div key={m.id} className={i > 0 ? "hairline-t" : ""}>
<div className="grid lg:grid-cols-12 gap-0 min-w-0">
<div className={`min-w-0 lg:col-span-4 p-6 hairline-r-0 lg:hairline-r bg-paper ${m.comingSoon ? "opacity-70" : ""}`}>
<div className="flex items-center gap-2 mb-2 flex-wrap">
{m.recommended && <span className="pill pill-hot">Recommended</span>}
{m.comingSoon && <span className="pill pill-ghost">Coming soon</span>}
<span className="eyebrow">Method 0{i + 1}</span>
</div>
<h3 className="font-display text-xl mb-1">{m.label}</h3>
<div className="font-cjk text-sm text-ink-mute mb-3">{m.cn}</div>
<div className="text-xs text-ink-soft leading-relaxed">
<strong className="text-ink">Prereq:</strong> {m.prereq}
</div>
</div>
<div className={`min-w-0 lg:col-span-8 p-6 bg-paper-deep relative ${m.comingSoon ? "opacity-80" : ""}`}>
{!m.comingSoon && (
<button
onClick={() => copy(m.id, m.cmd)}
className="absolute top-7 right-7 z-10 px-3 py-1 bg-paper hairline-t hairline-b hairline-l hairline-r font-mono text-[0.7rem] uppercase tracking-wider hover:bg-indigo hover:text-paper transition-colors"
>
{copied === m.id ? "Copied ✓" : "Copy"}
</button>
)}
<pre className="code-block text-[0.78rem] m-0 max-w-full">{m.cmd}</pre>
</div>
</div>
</div>
))}
</div>
</div>
);
}
+37
View File
@@ -0,0 +1,37 @@
"use client";
import { useRouter, usePathname } from "next/navigation";
import { locales, type Locale } from "@/lib/i18n/config";
const LABELS: Record<Locale, string> = { en: "EN", zh: "中文" };
export function LocaleSwitcher({ current }: { current: string }) {
const router = useRouter();
const pathname = usePathname();
const switchTo = current === "zh" ? "en" : "zh";
const other = LABELS[switchTo as Locale];
const handleClick = () => {
// Replace locale segment in path
const segments = pathname.split("/");
if (locales.includes(segments[1] as Locale)) {
segments[1] = switchTo;
} else {
segments.splice(1, 0, switchTo);
}
const newPath = segments.join("/") || `/${switchTo}`;
document.cookie = `NEXT_LOCALE=${switchTo};path=/;max-age=${60 * 60 * 24 * 365}`;
router.push(newPath);
};
return (
<button
onClick={handleClick}
className="inline-flex items-center gap-1.5 px-2.5 py-1 hairline-t hairline-b hairline-l hairline-r font-mono text-[0.7rem] uppercase tracking-wider hover:bg-paper-deep transition-colors"
title={current === "zh" ? "Switch to English" : "切换到中文"}
>
<span className="font-cjk normal-case tracking-normal">{other}</span>
</button>
);
}
+84
View File
@@ -0,0 +1,84 @@
"use client";
import { useEffect, useRef, useState } from "react";
type Props = {
chart: string;
label?: string;
fallback?: React.ReactNode;
};
export function MermaidDiagram({ chart, label, fallback }: Props) {
const [svg, setSvg] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
const idRef = useRef(`mermaid-${Math.random().toString(36).slice(2, 9)}`);
useEffect(() => {
let cancelled = false;
(async () => {
try {
const mermaid = (await import("mermaid")).default;
mermaid.initialize({
startOnLoad: false,
securityLevel: "strict",
theme: "base",
fontFamily: '"JetBrains Mono", ui-monospace, Menlo, monospace',
flowchart: {
curve: "basis",
padding: 14,
htmlLabels: false,
useMaxWidth: true,
},
themeVariables: {
background: "#ffffff",
primaryColor: "#ffffff",
primaryTextColor: "#0e0e10",
primaryBorderColor: "#0e0e10",
lineColor: "#4d6bfe",
secondaryColor: "#e9eefe",
tertiaryColor: "#f4f6fb",
edgeLabelBackground: "#ffffff",
clusterBkg: "#f4f6fb",
clusterBorder: "#0e0e10",
nodeBorder: "#0e0e10",
mainBkg: "#ffffff",
},
});
const { svg: rendered } = await mermaid.render(idRef.current, chart);
if (!cancelled) setSvg(rendered);
} catch (e) {
if (!cancelled) setError(e instanceof Error ? e.message : String(e));
}
})();
return () => {
cancelled = true;
};
}, [chart]);
if (error) {
return (
<div className="mermaid-frame" role="img" aria-label={label}>
<pre className="code-block text-[0.78rem]">{chart}</pre>
</div>
);
}
if (!svg) {
return (
<div className="mermaid-frame" role="img" aria-label={label} aria-busy="true">
{fallback ?? (
<pre className="code-block text-[0.78rem] opacity-70">{chart}</pre>
)}
</div>
);
}
return (
<div
className="mermaid-frame"
role="img"
aria-label={label}
dangerouslySetInnerHTML={{ __html: svg }}
/>
);
}
+92
View File
@@ -0,0 +1,92 @@
"use client";
import Link from "next/link";
import { useEffect, useState } from "react";
type MobileLink = { href: string; label: string; cn?: string };
export function MobileMenu({
links,
installHref,
installLabel,
}: {
links: MobileLink[];
installHref: string;
installLabel: string;
}) {
const [open, setOpen] = useState(false);
useEffect(() => {
if (!open) return;
const prev = document.body.style.overflow;
document.body.style.overflow = "hidden";
const onKey = (e: KeyboardEvent) => {
if (e.key === "Escape") setOpen(false);
};
window.addEventListener("keydown", onKey);
return () => {
document.body.style.overflow = prev;
window.removeEventListener("keydown", onKey);
};
}, [open]);
return (
<>
<button
type="button"
onClick={() => setOpen((o) => !o)}
className="md:hidden inline-flex items-center justify-center w-9 h-9 hairline-t hairline-b hairline-l hairline-r hover:bg-paper-deep transition-colors"
aria-label={open ? "Close menu" : "Open menu"}
aria-expanded={open}
aria-controls="mobile-menu"
>
{open ? (
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" aria-hidden>
<path d="M2 2L12 12M12 2L2 12" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" />
</svg>
) : (
<svg width="16" height="12" viewBox="0 0 16 12" fill="none" aria-hidden>
<path d="M0 1H16M0 6H16M0 11H16" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" />
</svg>
)}
</button>
{open && (
<div
id="mobile-menu"
className="md:hidden fixed inset-x-0 top-[5.7rem] bottom-0 z-40 bg-paper hairline-t overflow-y-auto"
role="dialog"
aria-modal="true"
>
<nav className="px-6 py-4">
<ul className="divide-y divide-[rgba(14,14,16,0.18)]">
{links.map((l) => (
<li key={l.href}>
<Link
href={l.href}
onClick={() => setOpen(false)}
className="flex items-baseline gap-3 py-4 hover:text-indigo transition-colors"
>
<span className="font-display text-lg">{l.label}</span>
{l.cn && (
<span className="font-cjk text-sm text-ink-mute">{l.cn}</span>
)}
<span className="ml-auto font-mono text-xs text-ink-mute"></span>
</Link>
</li>
))}
</ul>
<Link
href={installHref}
onClick={() => setOpen(false)}
className="mt-6 block w-full text-center px-5 py-3 bg-indigo text-paper font-mono text-sm uppercase tracking-wider hover:bg-indigo-deep transition-colors"
>
{installLabel}
</Link>
</nav>
</div>
)}
</>
);
}
+100
View File
@@ -0,0 +1,100 @@
import Link from "next/link";
import type { Locale } from "@/lib/i18n/config";
import { Seal } from "./seal";
import { Whale } from "./whale";
import { LocaleSwitcher } from "./locale-switcher";
import { MobileMenu } from "./mobile-menu";
const EN_LINKS = [
{ href: "/install", label: "Install", cn: "安装" },
{ href: "/docs", label: "Docs", cn: "文档" },
{ href: "/feed", label: "Activity", cn: "动态" },
{ href: "/roadmap", label: "Roadmap", cn: "路线" },
{ href: "/contribute", label: "Contribute", cn: "参与" },
];
const ZH_LINKS = [
{ href: "/zh/install", label: "安装", cn: "" },
{ href: "/zh/docs", label: "文档", cn: "" },
{ href: "/zh/feed", label: "动态", cn: "" },
{ href: "/zh/roadmap", label: "路线图", cn: "" },
{ href: "/zh/contribute", label: "参与贡献", cn: "" },
];
export function Nav({ locale = "en" }: { locale?: Locale }) {
const isZh = locale === "zh";
const links = isZh ? ZH_LINKS : EN_LINKS;
return (
<header className="hairline-b bg-paper/85 backdrop-blur sticky top-0 z-30">
{/* date / build strip */}
<div className="hairline-b">
<div className="mx-auto max-w-[1400px] px-6 py-1.5 flex items-center justify-between text-[0.66rem] font-mono uppercase tracking-[0.18em] text-ink-mute">
<div className="flex items-center gap-4">
<span>{isZh ? `${new Date().toISOString().slice(0, 10)}` : `${new Date().toISOString().slice(0, 10)}`}</span>
<span className="hidden sm:inline">· {isZh ? new Date().toLocaleDateString("zh-CN", { weekday: "long", month: "long", day: "numeric" }) : new Date().toLocaleDateString("en-US", { weekday: "long", month: "long", day: "numeric" })}</span>
</div>
<div className="flex items-center gap-4">
<span className="hidden md:inline">deepseek-tui.com</span>
<span className="inline-flex items-center gap-1.5">
<span className="w-1.5 h-1.5 bg-jade rounded-full inline-block animate-pulse" />
<span>{isZh ? "API · 在线" : "API · 在线"}</span>
</span>
</div>
</div>
</div>
{/* main nav */}
<div className="mx-auto max-w-[1400px] px-4 sm:px-6 py-3 flex items-center justify-between gap-3 sm:gap-6">
<Link href={isZh ? "/zh" : "/"} className="flex items-center gap-3 group min-w-0">
<Seal char="深" size="md" />
<div className="leading-tight min-w-0">
<div className="font-display text-[1.2rem] sm:text-[1.35rem] font-semibold tracking-crisp flex items-center gap-2 truncate">
DeepSeek TUI
<Whale size={20} className="text-indigo hidden sm:inline-block" />
</div>
<div className="font-cjk text-[0.65rem] sm:text-[0.7rem] text-ink-mute tracking-widest truncate">
{isZh ? "深度求索 · 终端智能体" : "深度求索 · 终端代理"}
</div>
</div>
</Link>
<nav className="hidden md:flex items-center gap-7">
{links.map((l) => (
<Link key={l.href} href={l.href} className="nav-link group">
<span>{l.label}</span>
{!isZh && "cn" in l && l.cn && (
<span className="font-cjk text-[0.66rem] ml-1.5 text-ink-mute">{l.cn}</span>
)}
</Link>
))}
</nav>
<div className="flex items-center gap-2 sm:gap-3">
<LocaleSwitcher current={locale} />
<Link
href="https://github.com/Hmbown/deepseek-tui"
className="hidden sm:inline-flex items-center gap-2 px-3 py-1.5 hairline-t hairline-b hairline-l hairline-r font-mono text-[0.7rem] uppercase tracking-wider hover:bg-paper-deep transition-colors"
>
<span> GitHub</span>
</Link>
<Link
href={isZh ? "/zh/install" : "/install"}
className="hidden md:inline-flex items-center gap-2 px-3 py-1.5 bg-indigo text-paper font-mono text-[0.72rem] uppercase tracking-wider hover:bg-indigo-deep transition-colors"
>
{isZh ? "安装 →" : "Install →"}
</Link>
<MobileMenu
installHref={isZh ? "/zh/install" : "/install"}
installLabel={isZh ? "安装 30 秒搞定 →" : "Install in 30 seconds →"}
links={links.map((l) => ({
href: l.href,
label: l.label,
cn: !isZh && "cn" in l ? l.cn : undefined,
}))}
/>
</div>
</div>
</header>
);
}
+17
View File
@@ -0,0 +1,17 @@
export function Seal({
char = "深",
size = "md",
variant = "ink",
}: {
char?: string;
size?: "sm" | "md" | "lg";
variant?: "ink" | "indigo";
}) {
const dim = size === "sm" ? "w-7 h-7 text-sm" : size === "lg" ? "w-12 h-12 text-2xl" : "w-10 h-10 text-lg";
const cls = variant === "indigo" ? "seal seal-indigo" : "seal";
return (
<span className={`${cls} ${dim}`} aria-hidden>
{char}
</span>
);
}
+33
View File
@@ -0,0 +1,33 @@
import type { RepoStats } from "@/lib/types";
function fmt(n: number): string {
if (n >= 1000) return (n / 1000).toFixed(1) + "k";
return n.toString();
}
export function StatGrid({ stats }: { stats: RepoStats }) {
const cells = [
{ label: "Stars", cn: "星标", value: fmt(stats.stars) },
{ label: "Forks", cn: "复刻", value: fmt(stats.forks) },
{ label: "Contributors", cn: "贡献者", value: fmt(stats.contributors) },
{
label: "Latest",
cn: "版本",
value: stats.latestRelease?.tag ?? "—",
mono: true,
},
];
return (
<div className="hairline-t hairline-b grid grid-cols-2 sm:grid-cols-4 col-rule">
{cells.map((c) => (
<div key={c.label} className="px-5 py-5">
<div className="eyebrow mb-2">
{c.label} · <span className="font-cjk normal-case tracking-normal">{c.cn}</span>
</div>
<div className={c.mono ? "font-mono text-2xl tabular text-ink" : "bignum text-ink"}>{c.value}</div>
</div>
))}
</div>
);
}
+31
View File
@@ -0,0 +1,31 @@
import type { FeedItem } from "@/lib/types";
import { relativeTime } from "@/lib/github";
export function Ticker({ items }: { items: FeedItem[] }) {
if (!items.length) return null;
const doubled = [...items, ...items]; // seamless loop
return (
<div className="hairline-t hairline-b bg-paper-deep overflow-hidden">
<div className="mx-auto max-w-[1400px] flex items-stretch">
<div className="bg-ink text-paper px-4 py-2 flex items-center shrink-0 gap-2">
<span className="w-1.5 h-1.5 bg-indigo rounded-full inline-block animate-pulse" />
<span className="font-cjk text-sm font-semibold tracking-wider"> </span>
<span className="font-mono text-[0.7rem] uppercase tracking-widest text-paper-deep">LIVE</span>
</div>
<div className="flex-1 overflow-hidden relative">
<div className="ticker-track py-2 font-mono text-[0.78rem]">
{doubled.map((item, i) => (
<span key={`${item.url}-${i}`} className="inline-flex items-center gap-2">
<span className="text-indigo uppercase tracking-wider">{item.kind === "pull" ? "PR" : "ISS"}</span>
<span className="tabular text-ink-mute">#{item.number}</span>
<span className="text-ink">{item.title.slice(0, 78)}{item.title.length > 78 ? "…" : ""}</span>
<span className="text-ink-mute tabular">· {relativeTime(item.updatedAt)}</span>
<span className="text-paper-line"></span>
</span>
))}
</div>
</div>
</div>
</div>
);
}
+25
View File
@@ -0,0 +1,25 @@
/**
* Stylized whale mark — a nod to DeepSeek's cetacean motif.
* Kept minimal and geometric so it reads as a wordmark accent, not an illustration.
*/
export function Whale({ size = 22, className = "" }: { size?: number; className?: string }) {
return (
<svg
viewBox="0 0 64 32"
width={size}
height={(size * 32) / 64}
className={className}
aria-hidden
fill="currentColor"
>
{/* body */}
<path d="M2 18 C 2 10, 14 4, 28 4 C 42 4, 50 10, 50 16 C 50 22, 42 28, 28 28 C 18 28, 8 24, 2 18 Z" />
{/* tail flukes */}
<path d="M48 12 L 62 4 L 58 16 L 62 28 L 48 20 Z" />
{/* eye */}
<circle cx="14" cy="14" r="1.4" fill="#FFFFFF" />
{/* spout */}
<path d="M22 4 L 22 0 M 26 4 L 28 0 M 18 4 L 16 0" stroke="currentColor" strokeWidth="1.2" fill="none" />
</svg>
);
}