feat: add AirShelf core implementation
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user