Update WBBR frontend template collection

This commit is contained in:
Codex
2026-08-27 20:15:24 +08:00
parent bd4d54a110
commit e444605e5d
85 changed files with 6322 additions and 115 deletions

173
app/icons/page.tsx Normal file
View File

@@ -0,0 +1,173 @@
"use client";
import Link from "next/link";
import { CSSProperties, useEffect, useMemo, useState } from "react";
import iconData from "../data/web-icons.json";
import styles from "./IconLibrary.module.css";
type IconItem = (typeof iconData.icons)[number];
type CodeMode = "character" | "html" | "react" | "css";
type PreviewBackground = "light" | "soft" | "dark";
export default function IconLibraryPage() {
const [query, setQuery] = useState("");
const [category, setCategory] = useState("all");
const [selectedId, setSelectedId] = useState("home");
const [favorites, setFavorites] = useState<string[]>([]);
const [view, setView] = useState<"grid" | "list">("grid");
const [size, setSize] = useState(28);
const [color, setColor] = useState("#27324a");
const [background, setBackground] = useState<PreviewBackground>("soft");
const [codeMode, setCodeMode] = useState<CodeMode>("html");
const [onlyFavorites, setOnlyFavorites] = useState(false);
const [toast, setToast] = useState("");
useEffect(() => {
try {
const saved = window.localStorage.getItem("orbit-icon-favorites");
if (saved) setFavorites(JSON.parse(saved));
} catch { /* use an empty favorite list */ }
}, []);
useEffect(() => {
window.localStorage.setItem("orbit-icon-favorites", JSON.stringify(favorites));
}, [favorites]);
const selected = iconData.icons.find((icon) => icon.id === selectedId) ?? iconData.icons[0];
const filteredIcons = useMemo(() => iconData.icons.filter((icon) => {
const text = `${icon.name}${icon.cn}${icon.id}${icon.keywords.join("")}`.toLowerCase();
const matchesQuery = text.includes(query.toLowerCase());
const matchesCategory = category === "all" || icon.category === category;
const matchesFavorite = !onlyFavorites || favorites.includes(icon.id);
return matchesQuery && matchesCategory && matchesFavorite;
}), [query, category, onlyFavorites, favorites]);
const categoryName = iconData.categories.find((item) => item.id === selected.category)?.name ?? "通用";
const code = getIconCode(selected, codeMode, size, color);
const previewStyle = { "--preview-size": `${size}px`, "--preview-color": color } as CSSProperties;
const notify = (message: string) => {
setToast(message);
window.setTimeout(() => setToast(""), 1800);
};
const copy = async (value: string, label: string) => {
try {
await navigator.clipboard.writeText(value);
} catch {
const input = document.createElement("textarea");
input.value = value;
input.style.position = "fixed";
input.style.opacity = "0";
document.body.appendChild(input);
input.select();
document.execCommand("copy");
input.remove();
}
notify(`${label}已复制`);
};
const toggleFavorite = (id: string) => setFavorites((current) => current.includes(id) ? current.filter((item) => item !== id) : [...current, id]);
const exportFavorites = () => {
const items = iconData.icons.filter((icon) => favorites.includes(icon.id));
const blob = new Blob([JSON.stringify(items, null, 2)], { type: "application/json;charset=utf-8" });
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = "web-icons-favorites.json";
link.click();
URL.revokeObjectURL(url);
notify("收藏图标已导出");
};
return (
<div className={styles.page}>
<header className={styles.topbar}>
<Link href="/" className={styles.brand}><span>G</span><strong>GlyphKit</strong><small>WEB ICON LIBRARY</small></Link>
<nav><Link href="/simple-list"></Link><Link href="/repeatable-form"></Link><Link className={styles.activeNav} href="/icons"></Link><Link href="/markdown"></Link></nav>
<div className={styles.headerActions}><button onClick={() => setOnlyFavorites((value) => !value)} className={onlyFavorites ? styles.favoriteFilterActive : ""}> {favorites.length}</button><button onClick={exportFavorites} disabled={favorites.length === 0}></button><span></span></div>
</header>
<div className={styles.workspace}>
<aside className={styles.sidebar}>
<div className={styles.sidebarTitle}><span>ICON SET</span><strong></strong><small>{iconData.icons.length} </small></div>
<nav className={styles.categories} aria-label="图标分类">
{iconData.categories.map((item) => {
const count = item.id === "all" ? iconData.icons.length : iconData.icons.filter((icon) => icon.category === item.id).length;
return <button key={item.id} className={category === item.id ? styles.categoryActive : ""} onClick={() => { setCategory(item.id); setOnlyFavorites(false); }}><span>{categorySymbol(item.id)}</span>{item.name}<b>{count}</b></button>;
})}
</nav>
<div className={styles.sidebarHelp}><span>i</span><p><strong></strong><small> HTMLCSS React </small></p></div>
</aside>
<main className={styles.main}>
<section className={styles.hero}>
<div><p>SYMBOL PICKER</p><h1></h1><span>使</span></div>
<div className={styles.heroSamples}>{["⌂","✓","⌕","✎","▦"].map((symbol,index) => <span key={symbol} style={{transform:`translateY(${index % 2 ? 7 : -2}px)`}}>{symbol}</span>)}</div>
</section>
<div className={styles.toolbar}>
<label className={styles.search}><span></span><input value={query} onChange={(event) => setQuery(event.target.value)} placeholder="搜索图标名称、功能或关键词例如编辑、upload" />{query && <button onClick={() => setQuery("")}>×</button>}<kbd> K</kbd></label>
<div className={styles.sizeControl}><span></span><input type="range" min="18" max="42" value={size} onChange={(event) => setSize(Number(event.target.value))} /><b>{size}px</b></div>
<div className={styles.viewSwitch}><button className={view === "grid" ? styles.viewActive : ""} onClick={() => setView("grid")} aria-label="网格视图"></button><button className={view === "list" ? styles.viewActive : ""} onClick={() => setView("list")} aria-label="列表视图"></button></div>
</div>
<div className={styles.resultHeading}><div><strong>{onlyFavorites ? "我的收藏" : iconData.categories.find((item) => item.id === category)?.name}</strong><span>{filteredIcons.length} </span></div>{(query || onlyFavorites || category !== "all") && <button onClick={() => { setQuery(""); setCategory("all"); setOnlyFavorites(false); }}></button>}</div>
<div className={`${styles.iconCollection} ${view === "list" ? styles.listView : ""}`} style={previewStyle}>
{filteredIcons.map((icon) => {
const favorite = favorites.includes(icon.id);
return <article key={icon.id} className={selected.id === icon.id ? styles.selectedCard : ""}>
<button className={styles.selectIcon} onClick={() => setSelectedId(icon.id)}>
<span className={styles.iconSymbol}>{icon.symbol}</span>
<span className={styles.iconLabel}><strong>{icon.cn}</strong><small>{icon.name}</small></span>
</button>
<button className={`${styles.favoriteButton} ${favorite ? styles.isFavorite : ""}`} onClick={() => toggleFavorite(icon.id)} aria-label={favorite ? `取消收藏 ${icon.cn}` : `收藏 ${icon.cn}`}>{favorite ? "★" : "☆"}</button>
<button className={styles.quickCopy} onClick={() => copy(icon.symbol, "图标字符")} aria-label={`复制 ${icon.cn}`}></button>
</article>;
})}
{filteredIcons.length === 0 && <div className={styles.empty}><span></span><h3></h3><p></p><button onClick={() => { setQuery(""); setCategory("all"); setOnlyFavorites(false); }}></button></div>}
</div>
</main>
<aside className={styles.inspector}>
<div className={styles.inspectorHeader}><div><span>SELECTED ICON</span><strong></strong></div><button className={favorites.includes(selected.id) ? styles.inspectorFavorite : ""} onClick={() => toggleFavorite(selected.id)}>{favorites.includes(selected.id) ? "★" : "☆"}</button></div>
<div className={`${styles.largePreview} ${styles[`preview_${background}`]}`} style={previewStyle}><span>{selected.symbol}</span><small>{size}px</small></div>
<div className={styles.iconMeta}><div><h2>{selected.cn}</h2><p>{selected.name}</p></div><button onClick={() => copy(selected.symbol, "图标字符")}></button></div>
<dl className={styles.details}><div><dt> ID</dt><dd>{selected.id}</dd></div><div><dt></dt><dd>{categoryName}</dd></div><div><dt>Unicode</dt><dd>U+{unicodeValue(selected.symbol)}</dd></div></dl>
<section className={styles.appearance}><h3></h3><label><span></span><input type="range" min="14" max="64" value={size} onChange={(event) => setSize(Number(event.target.value))} /><b>{size}px</b></label><div className={styles.colorRow}><span></span>{["#27324a","#3d6fe8","#20a26a","#e45252","#8a5bd1"].map((value) => <button key={value} className={color === value ? styles.colorActive : ""} style={{background:value}} onClick={() => setColor(value)} aria-label={`颜色 ${value}`} />)}<input type="color" value={color} onChange={(event) => setColor(event.target.value)} aria-label="自定义颜色" /></div><div className={styles.backgroundSwitch}>{(["light","soft","dark"] as PreviewBackground[]).map((value) => <button key={value} className={background === value ? styles.backgroundActive : ""} onClick={() => setBackground(value)}>{value === "light" ? "白色" : value === "soft" ? "浅色" : "深色"}</button>)}</div></section>
<section className={styles.codeSection}>
<h3>使</h3>
<div className={styles.codeTabs}>{(["character","html","react","css"] as CodeMode[]).map((mode) => <button key={mode} className={codeMode === mode ? styles.codeTabActive : ""} onClick={() => setCodeMode(mode)}>{mode === "character" ? "字符" : mode.toUpperCase()}</button>)}</div>
<pre><code>{code}</code></pre>
<button className={styles.copyCode} onClick={() => copy(code, `${codeMode.toUpperCase()} 代码`)}> </button>
</section>
<p className={styles.compatibility}><span></span><strong></strong><small>使 Segoe UI SymbolArial Unicode MS 线</small></p>
</aside>
</div>
{toast && <div className={styles.toast}> {toast}</div>}
</div>
);
}
function categorySymbol(id: string) {
return ({ all: "✣", navigation: "⌖", actions: "✎", status: "◉", media: "▶", files: "▤", business: "▦", communication: "✉", identity: "⚥", biology: "⚛", breeding: "♂♀", pets: "🐾︎" } as Record<string,string>)[id] ?? "◇";
}
function unicodeValue(symbol: string) {
return Array.from(symbol).map((char) => char.codePointAt(0)?.toString(16).toUpperCase().padStart(4, "0")).join(" ");
}
function cssEscaped(symbol: string) {
return Array.from(symbol).map((char) => `\\${char.codePointAt(0)?.toString(16).toUpperCase()} `).join("");
}
function getIconCode(icon: IconItem, mode: CodeMode, size: number, color: string) {
if (mode === "character") return icon.symbol;
if (mode === "html") return `<span class="web-icon" aria-hidden="true">${icon.symbol}</span>`;
if (mode === "react") return `<span className="web-icon" aria-hidden>${icon.symbol}</span>`;
return `.web-icon::before {\n content: "${cssEscaped(icon.symbol)}";\n color: ${color};\n font: ${size}px/1 "Segoe UI Symbol", sans-serif;\n}`;
}