Add multi-board selector and scalable firmware catalog UI.

Board chips reserve future models; firmware uses searchable cards with detail pane and optional meta.json media/docs.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-19 19:45:24 +08:00
parent ae63378fdb
commit a59aaf1c8d
11 changed files with 788 additions and 226 deletions

View File

@@ -80,7 +80,7 @@ export default async function LocaleLayout({ children, params }: Props) {
<div className="bg-grid" aria-hidden />
<div className="bg-glow" aria-hidden />
<header className="animate-rise relative z-10 mx-auto flex max-w-[920px] flex-wrap items-center justify-between gap-4 px-[clamp(1.25rem,4vw,3rem)] pt-6">
<header className="animate-rise relative z-10 mx-auto flex max-w-[1180px] flex-wrap items-center justify-between gap-4 px-[clamp(1.25rem,4vw,3rem)] pt-6">
<div>
<div className="flex items-center gap-2.5">
<span
@@ -98,11 +98,11 @@ export default async function LocaleLayout({ children, params }: Props) {
<LanguageSwitcher />
</header>
<main className="relative z-10 mx-auto max-w-[920px] px-[clamp(1.25rem,4vw,3rem)] py-7 pb-12">
<main className="relative z-10 mx-auto max-w-[1180px] px-[clamp(1.25rem,4vw,3rem)] py-7 pb-12">
{children}
</main>
<footer className="relative z-10 mx-auto flex max-w-[920px] flex-wrap gap-x-6 gap-y-2 px-[clamp(1.25rem,4vw,3rem)] pb-10 text-xs text-[var(--muted)] opacity-85">
<footer className="relative z-10 mx-auto flex max-w-[1180px] flex-wrap gap-x-6 gap-y-2 px-[clamp(1.25rem,4vw,3rem)] pb-10 text-xs text-[var(--muted)] opacity-85">
<span>{tf("offsets")}</span>
<span>{tf("stack")}</span>
</footer>

View File

@@ -0,0 +1,43 @@
import fs from "fs";
import path from "path";
import { NextResponse } from "next/server";
import { resolveMediaFile } from "@/lib/firmware";
export const dynamic = "force-dynamic";
const MIME: Record<string, string> = {
".png": "image/png",
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".webp": "image/webp",
".gif": "image/gif",
".mp4": "video/mp4",
".webm": "video/webm",
".md": "text/markdown; charset=utf-8",
".txt": "text/plain; charset=utf-8",
".html": "text/html; charset=utf-8",
".pdf": "application/pdf",
};
type Params = {
params: Promise<{ project: string; variant: string; path: string[] }>;
};
export async function GET(_req: Request, { params }: Params) {
const { project, variant, path: parts } = await params;
const rel = parts.join("/");
const abs = resolveMediaFile(project, variant, rel);
if (!abs) {
return NextResponse.json({ error: "not found" }, { status: 404 });
}
const ext = path.extname(abs).toLowerCase();
const buf = fs.readFileSync(abs);
return new NextResponse(buf, {
headers: {
"Content-Type": MIME[ext] || "application/octet-stream",
"Content-Length": String(buf.length),
"Cache-Control": "public, max-age=3600",
},
});
}