refactor(frontend): remove ui.ts/icons.tsx and all rd-* legacy classes (B5)
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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}</>;
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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 })}
|
||||
>
|
||||
重试
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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}`;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user