feat: add AirShelf core implementation

This commit is contained in:
zyc
2026-06-05 10:21:40 +08:00
parent 2ba1058329
commit cfdcd84a30
252 changed files with 70828 additions and 0 deletions
@@ -0,0 +1,80 @@
import { Check, Search } from "lucide-react";
import type { Product, User } from "../types";
import type { NavItem, Notice, Page } from "../routes/route-config";
import { mainNav, parentPage, supplementNav } from "../routes/route-config";
type Navigate = (page: Page) => void;
export function Sidebar({ page, navigate, user, products }: { page: Page; navigate: Navigate; user: User; products: Product[] }) {
const activePage = parentPage(page);
return (
<aside className="sidebar">
<div className="brand">
<div className="flame">
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path d="M12 2c1 3 4 5 4 9a4 4 0 0 1-4 4 4 4 0 0 1-4-4 5 5 0 0 1 1.5-3.5C10.5 6 11.5 4 12 2zm-1 13c0 2 1 3 1 5 1-1 3-2 3-5 0-1.5-1-2-2-3-1 1-2 2-2 3z" />
</svg>
</div>
<div><div className="name">·Studio</div></div>
</div>
<div className="search-box">
<Search size={14} />
<input id="global-search" placeholder="搜索" />
<span className="kbd">K</span>
</div>
<div className="nav-section"></div>
<nav>
{mainNav.map((item) => (
<NavButton key={item.page} item={{ ...item, badge: item.page === "products" ? String(products.length) : item.badge }} active={activePage === item.page} navigate={navigate} />
))}
</nav>
<div className="nav-section"></div>
<nav>
{supplementNav.map((item) => <NavButton key={item.page} item={item} active={activePage === item.page} navigate={navigate} />)}
</nav>
<div className="aside-foot">
<button className="user" type="button">
<div className="av">{user.username.slice(0, 1).toUpperCase()}</div>
<div className="em">{user.username}</div>
</button>
</div>
</aside>
);
}
export function NavButton({ item, active, navigate }: { item: NavItem; active: boolean; navigate: Navigate }) {
const Icon = item.icon;
return (
<button className={active ? "active" : ""} type="button" onClick={() => navigate(item.page)}>
<Icon size={14} />
<span>{item.label}</span>
{item.badge && <span className="pill-mini">{item.badge}</span>}
</button>
);
}
export function Decorations() {
// Exact 设计稿只用 grid-bg 做底纹;旧版散点/角标是绝对定位写死坐标,会压在
// topbar 和 page-head 上(重叠 bug),且不在设计稿里。品牌 mono 签名由各页面 inline
// 的 // 注释、[ ALL ] 等承载,这里只保留底纹。
return <div className="grid-bg" />;
}
export function ToastLike({ notice }: { notice: NonNullable<Notice> }) {
return (
<div className={`inline-toast ${notice.type}`}>
<div className="ic-t"><Check size={12} /></div>
<div className="txt">{notice.text}<span className="mono">[ {notice.type.toUpperCase()} ]</span></div>
</div>
);
}
export function CornerMark({ pos }: { pos: "tl" | "tr" | "bl" | "br" }) {
return (
<span className={`corner-mark ${pos}`}>
<svg viewBox="0 0 22 21" fill="none">
<path d="M10.5 4C10.5 7.31371 7.81371 10 4.5 10H0.5V11H4.5C7.81371 11 10.5 13.6863 10.5 17V21H11.5V17C11.5 13.6863 14.1863 11 17.5 11H21.5V10H17.5C14.1863 10 11.5 7.31371 11.5 4V0H10.5V4Z" fill="currentColor" />
</svg>
</span>
);
}