mirror of
https://github.com/hectorqin/reader.git
synced 2026-11-11 05:20:54 +08:00
refactor(web): ui/icon.tsx owns the icon set — names, sizes and stroke
IconName came from the legacy font's code-point table, so a new icon meant a new font glyph, while 59 files imported lucide-react directly with their own glyphs, 17 sizes and strokes from 1.4 to Lucide's default 2. icon.tsx now defines every name (in the product's words: video, audiobook, lock, folder-browse, …), each drawn by one Lucide glyph; IconName is its keys. Icon gains a six-step size scale (14–32), `filled`, and `strong` for the current navigation item; the stroke is 1.8 everywhere, 1.4 at the 32px empty-state size. ui.md §1 describes the rules. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
4a6b7a3858
commit
97429ddeed
2 files changed
+100
-29
No files matched your search
+12
-3
@@ -12,9 +12,18 @@
|
||||
|
||||
## 1. 图标
|
||||
|
||||
图标统一使用 `lucide-react` 的 SVG,`web/src/ui/icon.tsx` 集中维护名称映射,线宽 1.8,尺寸 1em。
|
||||
界面使用 Icon / IconButton / IconTextButton,不直接嵌入字体码位或 emoji。
|
||||
装饰图标标记 aria-hidden;没有文字的按钮必须有中文 aria-label。原字体文件与生成工具仅供历史兼容,不再加载到阅读界面。
|
||||
图标统一使用 `lucide-react` 的 SVG,`web/src/ui/icon.tsx` 集中维护名称映射,也是**唯一**引入 `lucide-react` 的文件
|
||||
(`test/icons.test.ts` 守住)。界面使用 Icon / IconButton / IconTextButton,不直接嵌入字体码位、emoji 或 ✓ × + 这类字符。
|
||||
|
||||
- **名称**:一个意思一个名字、一个字形,用本项目自己的英文名(`video`、`audiobook`、`lock`、`folder-browse`),
|
||||
不借用其他图标集的名字。新增图标只改 `icon.tsx` 的 `ICONS`。
|
||||
- **尺寸**:不传 `size` 时为 1em(控件里用这个);需要固定尺寸时只取 14 / 16 / 18 / 20 / 24 / 32 六档。
|
||||
- **线宽**:一律 1.8;32px 的空状态大图标用 1.4,否则放大后像粗体图示;导航当前项用 `strong`(2.1)。
|
||||
`filled` 用文字颜色填充(播放三角、已收藏)。
|
||||
- 装饰图标默认 `aria-hidden`;独立表意时传 `label`。没有文字的按钮必须有中文 aria-label。
|
||||
|
||||
原字体文件与生成工具(`tools/icons`、`reader-icons.ttf`、`icon-names.ts`)仅供历史兼容,不再加载到界面,
|
||||
图标名称也不再由它决定。
|
||||
|
||||
## 2. 设计令牌
|
||||
|
||||
|
||||
+88
-26
@@ -1,34 +1,96 @@
|
||||
import {
|
||||
Menu, ChevronLeft, ChevronRight, ArrowLeft, Folder, FileText, SlidersHorizontal,
|
||||
Search, X, Plus, Check, Upload, Trash2, Download, RefreshCw, BookOpen,
|
||||
Library, Play, Pause, SkipBack, SkipForward, Square, Headphones, Moon, Sun,
|
||||
Eye, Info, TriangleAlert, Clock, Ellipsis, Pencil, ArrowDownUp, LogOut,
|
||||
CirclePlus, IndentIncrease, Type, MoveVertical, Settings, SlidersVertical, Bookmark,
|
||||
Menu, ChevronLeft, ChevronRight, ChevronDown, ArrowLeft, ArrowUp, ArrowDown, ArrowUpRight,
|
||||
Folder, FolderOpen, FolderX, File, FileText, SlidersHorizontal, Search, X, Plus, Check,
|
||||
Upload, Trash2, Download, RefreshCw, RotateCw, RotateCcw, BookOpen, Library, LibraryBig,
|
||||
Play, PlayCircle, Pause, SkipBack, SkipForward, Square, Headphones, Volume2, Moon, Sun,
|
||||
SunMoon, Palette, Eye, EyeOff, Info, TriangleAlert, CloudOff, Clock, History, Timer,
|
||||
TimerOff, Ellipsis, Pencil, ArrowDownUp, LogOut, CirclePlus, IndentIncrease, Type,
|
||||
MoveVertical, Settings, SlidersVertical, Bookmark, Music, ListMusic, ListVideo, ListPlus,
|
||||
ListOrdered, Layers, Heart, Star, Clapperboard, Film, UserRound, Lock, LockOpen, Gauge,
|
||||
Subtitles, AudioLines, MicVocal, Shuffle, Repeat, Repeat1, Maximize, Minimize, Expand,
|
||||
Shrink, LocateFixed, LayoutGrid, House, Rss,
|
||||
} from 'lucide-react';
|
||||
import type { IconName } from './icon-names.ts';
|
||||
import type { JSX } from 'react';
|
||||
export type { IconName };
|
||||
import type { CSSProperties, JSX } from 'react';
|
||||
|
||||
/**
|
||||
* The icon set: one name per idea, in the product's own words, each drawn by one
|
||||
* Lucide glyph at one stroke weight (ui.md §1).
|
||||
*
|
||||
* This file is the only place that imports `lucide-react`. Screens used to import
|
||||
* glyphs directly, and the set drifted the way a set without an owner does: settings
|
||||
* drawn by three different gears, lock by two padlocks, the stroke at 1.4, 1.7, 1.8,
|
||||
* 1.9 and Lucide's default 2 on the same screen, and seventeen sizes. A name here is
|
||||
* what a screen asks for; which glyph answers it is decided once.
|
||||
*
|
||||
* The names are the product's, not another icon set's (`test/icons.test.ts` keeps
|
||||
* borrowed names out of the legacy font table; the same rule applies here). The
|
||||
* aliases at the end are older spellings kept so existing call sites compile.
|
||||
*/
|
||||
const ICONS = {
|
||||
menu: Menu, 'chevron-left': ChevronLeft, 'chevron-right': ChevronRight,
|
||||
'arrow-left': ArrowLeft, folder: Folder, 'file-text': FileText,
|
||||
sliders: SlidersHorizontal, search: Search, close: X, plus: Plus, check: Check,
|
||||
upload: Upload, trash: Trash2, download: Download, refresh: RefreshCw,
|
||||
book: BookOpen, shelf: Library, play: Play, pause: Pause,
|
||||
'step-backward': SkipBack, 'step-forward': SkipForward, stop: Square,
|
||||
volume: Headphones, moon: Moon, sun: Sun, eye: Eye, info: Info,
|
||||
warning: TriangleAlert, clock: Clock, more: Ellipsis, edit: Pencil,
|
||||
sort: ArrowDownUp, logout: LogOut, 'sign-out': LogOut, 'add-circle': CirclePlus,
|
||||
indent: IndentIncrease, 'text-size': Type, 'line-height': MoveVertical,
|
||||
settings: Settings, library: Library, tune: SlidersVertical,
|
||||
font: Type, 'volume-high': Headphones, 'backward-step': SkipBack,
|
||||
'forward-step': SkipForward, bars: Menu, gear: Settings, xmark: X,
|
||||
'magnifying-glass': Search, books: Library, bookmark: Bookmark,
|
||||
} satisfies Record<IconName, typeof Menu>;
|
||||
export interface IconProps { name: IconName; label?: string | null; className?: string; }
|
||||
// navigation and structure
|
||||
menu: Menu, 'chevron-left': ChevronLeft, 'chevron-right': ChevronRight, 'chevron-down': ChevronDown,
|
||||
'arrow-left': ArrowLeft, 'arrow-up': ArrowUp, 'arrow-down': ArrowDown, external: ArrowUpRight,
|
||||
home: House, grid: LayoutGrid, locate: LocateFixed,
|
||||
// files and the library
|
||||
folder: Folder, 'folder-browse': FolderOpen, 'folder-missing': FolderX, file: File, 'file-text': FileText,
|
||||
book: BookOpen, shelf: Library, library: LibraryBig, bookmark: Bookmark, feed: Rss,
|
||||
// actions
|
||||
search: Search, close: X, plus: Plus, check: Check, upload: Upload, trash: Trash2,
|
||||
download: Download, refresh: RefreshCw, edit: Pencil, sort: ArrowDownUp, more: Ellipsis,
|
||||
'add-circle': CirclePlus, 'queue-add': ListPlus, logout: LogOut, 'sign-out': LogOut,
|
||||
// playback
|
||||
play: Play, 'play-circle': PlayCircle, pause: Pause, stop: Square,
|
||||
'step-backward': SkipBack, 'step-forward': SkipForward,
|
||||
'rewind': RotateCcw, 'fast-forward': RotateCw, shuffle: Shuffle, repeat: Repeat, 'repeat-one': Repeat1,
|
||||
speed: Gauge, timer: Timer, 'timer-off': TimerOff, fullscreen: Maximize, 'fullscreen-exit': Minimize,
|
||||
'fit-fill': Expand, 'fit-whole': Shrink, lock: Lock, unlock: LockOpen,
|
||||
subtitles: Subtitles, 'audio-track': AudioLines, versions: Layers, episodes: ListVideo,
|
||||
playlist: ListMusic, 'ordered-list': ListOrdered, lyrics: MicVocal, speaker: Volume2,
|
||||
// media kinds
|
||||
video: Clapperboard, film: Film, music: Music, audiobook: Headphones, volume: Headphones,
|
||||
// reading and appearance
|
||||
'text-size': Type, 'line-height': MoveVertical, indent: IndentIncrease, appearance: SunMoon,
|
||||
palette: Palette, moon: Moon, sun: Sun, settings: Settings, sliders: SlidersHorizontal,
|
||||
tune: SlidersVertical,
|
||||
// state and people
|
||||
eye: Eye, 'eye-off': EyeOff, info: Info, warning: TriangleAlert, offline: CloudOff,
|
||||
clock: Clock, history: History, favorite: Heart, star: Star, account: UserRound,
|
||||
// older spellings
|
||||
font: Type, 'volume-high': Headphones, 'backward-step': SkipBack, 'forward-step': SkipForward,
|
||||
bars: Menu, gear: Settings, xmark: X, 'magnifying-glass': Search, books: LibraryBig,
|
||||
} as const;
|
||||
|
||||
export type IconName = keyof typeof ICONS;
|
||||
|
||||
/**
|
||||
* The size scale, in px. Without a size the glyph is 1em of its own box (the
|
||||
* `.icon` rule sets that to the title size), which is what controls use. A size is
|
||||
* for the places that need a fixed one: inline with small text, list rows, empty
|
||||
* states. Six steps rather than whatever number the call site happened to type.
|
||||
*/
|
||||
export type IconSize = 14 | 16 | 18 | 20 | 24 | 32;
|
||||
|
||||
export interface IconProps {
|
||||
name: IconName;
|
||||
/** Only for an icon that carries meaning on its own; decorative icons stay hidden. */
|
||||
label?: string | null;
|
||||
className?: string;
|
||||
size?: IconSize;
|
||||
/** Filled with the text colour (a play triangle, a set bookmark). */
|
||||
filled?: boolean;
|
||||
/** A heavier stroke for the selected item of a navigation (the tab bar's current tab). */
|
||||
strong?: boolean;
|
||||
style?: CSSProperties;
|
||||
}
|
||||
|
||||
/** Shared geometry and stroke weight, independent of font rendering. */
|
||||
export function Icon({ name, label, className }: IconProps): JSX.Element {
|
||||
export function Icon({ name, label, className, size, filled, strong, style }: IconProps): JSX.Element {
|
||||
const Glyph = ICONS[name];
|
||||
// The large empty-state glyphs are drawn lighter: a 1.8 stroke scaled to 32px reads
|
||||
// as a bold pictogram rather than the line icon the rest of the set is.
|
||||
return <Glyph className={className ? 'icon ' + className : 'icon'} size="1em"
|
||||
strokeWidth={1.8} aria-hidden={label ? undefined : true}
|
||||
strokeWidth={strong ? 2.1 : size === 32 ? 1.4 : 1.8} fill={filled ? 'currentColor' : 'none'}
|
||||
style={size ? { ...style, fontSize: `${size}px` } : style}
|
||||
aria-hidden={label ? undefined : true}
|
||||
role={label ? 'img' : undefined} aria-label={label ?? undefined} />;
|
||||
}
|
||||
Reference in new issue
Block a user