refactor(frontend): remove ui.ts/icons.tsx and all rd-* legacy classes (B5)

This commit is contained in:
2026-09-16 17:53:51 +08:00
parent 28d8587e71
commit 395be9688d
10 changed files with 33 additions and 125 deletions
+5
View File
@@ -8,6 +8,11 @@ The format loosely follows Keep a Changelog and can be adapted to the team's hab
## [Unreleased]
### Changed / 变更
- Reader UI cleanup: the legacy style helpers (ui.ts / icons.tsx) and all rd-* classes are gone; every reader surface now uses the shared design tokens and components.
- 阅读器 UI 清理:旧样式助手(ui.ts / icons.tsx)与全部 rd-* 类删除;所有阅读器表面统一使用共享设计 token 与组件。
### Added / 新增
- The shelf page gains a reading stats card (this week's time, streak days, expandable 7-day bar chart); reading time is tracked on-device while a book is open.
+1 -1
View File
@@ -78,7 +78,7 @@ export function RequireAuth({ children }: { children: ReactNode }) {
const { user, ready } = useAuth();
const loc = useLocation();
if (!getToken()) return <Navigate to="/login" replace state={{ from: loc.pathname }} />;
if (!ready || !user) return <div className="grid h-full place-items-center text-stone-500">加载中…</div>;
if (!ready || !user) return <div className="text-muted-foreground grid h-full place-items-center">加载中…</div>;
return <>{children}</>;
}
+10 -4
View File
@@ -1,7 +1,8 @@
import { useEffect, useState } from "react";
import type { Book } from "../api/types";
import { fetchObjectUrl } from "../lib/authImage";
import { btn, formatBadge } from "./ui";
import { FormatBadge } from "./format-badge";
import { Button } from "./ui/button";
import { TriangleAlert } from "lucide-react";
export function useAuthedImage(url: string | undefined): { src: string; failed: boolean; retry: () => void } {
@@ -44,12 +45,17 @@ export function Cover({ book, className }: { book: Book; className?: string }) {
<span className="select-none font-serif text-4xl text-muted-foreground">
{Array.from(book.title)[0] ?? "?"}
</span>
<span className={formatBadge(book.format, "absolute bottom-1.5 right-1.5")}>{book.format}</span>
<FormatBadge fmt={book.format} className="absolute right-1.5 bottom-1.5" />
{failed && (
<button className={btn + " absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2"} onClick={retry}>
<Button
variant="secondary"
size="xs"
className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 gap-1"
onClick={retry}
>
<TriangleAlert className="size-4" />
封面重试
</button>
</Button>
)}
</div>
);
+2 -2
View File
@@ -13,9 +13,9 @@ export class ErrorBoundary extends Component<{ children: ReactNode }, { err: Err
<div className="grid h-full place-items-center p-8 text-center">
<div>
<p className="mb-2 text-lg text-red-400">页面出错了</p>
<p className="mb-4 max-w-md text-sm break-all text-stone-500">{String(this.state.err)}</p>
<p className="mb-4 max-w-md text-sm break-all text-muted-foreground">{String(this.state.err)}</p>
<button
className="rounded-md bg-stone-800 px-3 py-1.5 text-sm"
className="rounded-md bg-secondary px-3 py-1.5 text-sm"
onClick={() => this.setState({ err: null })}
>
重试
-24
View File
@@ -1,24 +0,0 @@
import type { SVGProps } from "react";
function Svg({ children, ...p }: SVGProps<SVGSVGElement>) {
return (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={1.7}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
{...p}
>
{children}
</svg>
);
}
export const IconArrowLeft = (p: SVGProps<SVGSVGElement>) => (
<Svg {...p}>
<path d="M20 12H4m0 0 6-6m-6 6 6 6" />
</Svg>
);
-19
View File
@@ -1,19 +0,0 @@
import type { Format } from "../api/types";
export const btn =
"inline-flex items-center gap-1.5 rounded-lg bg-white/[0.06] px-3 py-1.5 text-sm ring-1 ring-white/10 transition-colors hover:bg-white/[0.11] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-amber-500 active:bg-white/[0.15] disabled:cursor-not-allowed disabled:opacity-50";
export const btnGhost =
"inline-flex items-center gap-1.5 rounded-lg px-2.5 py-1.5 text-sm text-stone-400 transition-colors hover:bg-white/[0.07] hover:text-stone-100 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-amber-500";
// 格式徽章:按类型着色,色彩之外始终伴随文字,不依赖颜色单独传义
export const FORMAT_BADGE: Record<Format, string> = {
cbz: "bg-orange-500/15 text-orange-300 ring-orange-500/25",
pdf: "bg-rose-500/15 text-rose-300 ring-rose-500/25",
epub: "bg-sky-500/15 text-sky-300 ring-sky-500/25",
txt: "bg-emerald-500/15 text-emerald-300 ring-emerald-500/25",
md: "bg-teal-500/15 text-teal-300 ring-teal-500/25",
};
export function formatBadge(fmt: Format, extra = ""): string {
return `rounded px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-wide ring-1 ${FORMAT_BADGE[fmt]} ${extra}`;
}
-66
View File
@@ -149,74 +149,8 @@ body {
.rd-surface ::selection {
background: color-mix(in oklab, var(--rd-link) 30%, transparent);
}
/* 主题内的次级控件(章节条/翻页键),跟随当前阅读主题 */
.rd-btn {
@apply rounded-md px-2.5 py-1 text-sm transition-colors;
background: color-mix(in oklab, currentColor 8%, transparent);
}
.rd-btn:hover {
background: color-mix(in oklab, currentColor 16%, transparent);
}
.rd-btn:disabled {
@apply cursor-not-allowed opacity-40;
}
/* 目录侧边栏行 */
.rd-row {
@apply block w-full truncate rounded-md px-2.5 py-1.5 text-left text-sm transition-colors;
}
.rd-row:hover {
background: color-mix(in oklab, currentColor 12%, transparent);
}
.rd-btn-on {
@apply font-semibold;
background: color-mix(in oklab, currentColor 20%, transparent);
}
/* Radix 菜单项高亮(指针/键盘) */
.rd-highlight {
outline: none;
}
.rd-highlight[data-highlighted] {
background: color-mix(in oklab, currentColor 16%, transparent);
}
.rd-divider {
border-color: color-mix(in oklab, currentColor 14%, transparent);
}
/* 进度/页码滑条:主题色轨道 + 强调色拇指,触摸友好 */
.rd-range {
@apply h-1.5 w-full cursor-pointer appearance-none rounded-full;
background: color-mix(in oklab, currentColor 18%, transparent);
}
.rd-range::-webkit-slider-thumb {
@apply h-4 w-4 appearance-none rounded-full ring-2 ring-black/10;
background: var(--rd-link, #f59e0b);
}
.rd-range::-moz-range-thumb {
@apply h-4 w-4 rounded-full border-0;
background: var(--rd-link, #f59e0b);
}
/* 沉浸态底部工具抽屉的滑入动效 */
@keyframes rd-rise {
from {
transform: translateY(100%);
opacity: 0.4;
}
to {
transform: translateY(0);
opacity: 1;
}
}
.rd-sheet {
animation: rd-rise 0.2s ease-out;
}
@media (prefers-reduced-motion: reduce) {
.rd-sheet {
animation: none;
}
.rd-rise-anim {
transition: none !important;
}
.fx-rise {
animation: none;
}
+2 -2
View File
@@ -18,7 +18,7 @@ import { Cover } from "@/components/Cover";
import { toast } from "@/components/Toaster";
import { UploadButton } from "@/components/upload-button";
import { ReadingStatsCard } from "@/components/reading-stats-card";
import { formatBadge } from "@/components/ui";
import { FormatBadge } from "@/components/format-badge";
import {
AlertDialog,
AlertDialogAction,
@@ -68,7 +68,7 @@ function BookCard({
>
<span className="relative block aspect-[3/4] overflow-hidden rounded-md border bg-muted shadow-sm transition group-hover:shadow-md group-hover:ring-2 group-hover:ring-accent/50">
<Cover book={book} className="size-full" />
<span className={formatBadge(book.format, "absolute left-1.5 top-1.5")}>{book.format}</span>
<FormatBadge fmt={book.format} className="absolute left-1.5 top-1.5" />
{book.state === "error" && (
<span
className="absolute right-1.5 top-1.5 flex items-center gap-1 rounded bg-destructive px-1.5 py-0.5 text-[10px] font-semibold text-destructive-foreground"
+9 -6
View File
@@ -41,14 +41,14 @@ function PageImg({ url, width, onLoaded }: { url: string; width: number; onLoade
}, [url, nonce]);
if (failed)
return (
<div className="grid place-items-center gap-2 bg-stone-900 py-8 text-sm text-stone-500" style={{ width }}>
<div className="bg-muted text-muted-foreground grid place-items-center gap-2 py-8 text-sm" style={{ width }}>
本页加载失败
<Button variant="secondary" onClick={() => setNonce((n) => n + 1)}>
重试
</Button>
</div>
);
if (!src) return <div className="animate-pulse bg-stone-800" style={{ width, height: width * EST_RATIO }} />;
if (!src) return <div className="animate-pulse bg-muted" style={{ width, height: width * EST_RATIO }} />;
return (
<img
src={src}
@@ -286,10 +286,10 @@ export default function CbzReader({ book, initialLocator, initialPercent, chrome
// pages_url 缺失时 query 被 disabled 永久 pending → 只有真正发起了请求才显示加载态
if (countQ.isPending && !!book.pages_url)
return <div className="grid h-full place-items-center text-stone-500">页索引加载中…</div>;
return <div className="text-muted-foreground grid h-full place-items-center">页索引加载中…</div>;
if (countQ.isError || !book.page_url_fmt)
return (
<div className="grid h-full place-items-center p-8 text-center text-stone-500">
<div className="text-muted-foreground grid h-full place-items-center p-8 text-center">
无法解析页索引:{countQ.error instanceof Error ? countQ.error.message : "缺 page_url_fmt"}
</div>
);
@@ -396,7 +396,7 @@ export default function CbzReader({ book, initialLocator, initialPercent, chrome
style={{ top: ph.offset(w.end) - anchor, height: vh }}
>
{w.end >= count ? (
<span className="text-sm text-stone-500">— 全书完 —</span>
<span className="text-muted-foreground text-sm">— 全书完 —</span>
) : (
<Button variant="secondary" onClick={() => goPage(chapters![ci + 1].start)}>
本章完 · 下一章 <ChevronRight size={14} className="inline" />
@@ -544,7 +544,10 @@ export default function CbzReader({ book, initialLocator, initialPercent, chrome
<li key={i}>
<button
ref={i === ci ? activeRow : undefined}
className={"rd-row" + (i === ci ? " rd-btn-on" : "")}
className={
"min-w-0 flex-1 truncate rounded-md px-2.5 py-1.5 text-left text-sm transition-colors hover:bg-accent hover:text-accent-foreground" +
(i === ci ? " bg-accent text-accent-foreground" : "")
}
onClick={() => {
goPage(c.start);
setNav(null);
+4 -1
View File
@@ -411,7 +411,10 @@ function TxtView({ book, initialLocator, initialPercent, chrome }: ReaderProps)
<li key={i}>
<button
ref={i === ci ? activeRow : undefined}
className={"rd-row" + (i === ci ? " rd-btn-on" : "")}
className={
"min-w-0 flex-1 truncate rounded-md px-2.5 py-1.5 text-left text-sm transition-colors hover:bg-accent hover:text-accent-foreground" +
(i === ci ? " bg-accent text-accent-foreground" : "")
}
onClick={() => {
goChapter(i);
setNav(null);