- pnpm workspace: apps/{web,server}, packages/{contracts,db,domain,adapters,tasks,agent,testkit}
- server: Hono + zod-openapi, API Key auth with scopes, redacting pino logger, SSE event bus with replay, worker heartbeat
- db: Kysely + pg, file migrator, 0001_init (settings/api_key/audit_event/outbox_event)
- web: React Router 7 SPA console shell with 12 workspace placeholders, uhd_web tokens/ui/guard scripts
- docs: ADR-001 (Node over Deno), dev runbook, design system rules
Co-Authored-By: Claude Fable 5.1 <[email protected]>
8.4 KiB
设计 token
迁入自 /opt/uhd_web
docs/features/(2026-09-27)。文中流媒体、播放器、落地页、套餐等段落是 uhd_web 的原始上下文,本项目没有这些页面,只作材质与 token 用法参考;以本目录 README 的约定为准。
样式入口是 app.css。本说明定义颜色类 token 的分层、命名与使用规则,目的是让主站之外的皮肤(如动漫版)只改 token 值就能换皮,组件源码两边保持一致。材质类的说明见界面材质。
分层
| 层 | 在哪 | 谁能引用 |
|---|---|---|
| 原色 | @theme 里的 --color-ink-*、--color-bone-*、--color-gold-* 等色阶 |
只有下一层的 token 定义 |
| 语义 token | :root(浅色门户)、.dark(深色门户)、:root:has([data-stream-shell])(流媒体壳)三块里的 --content-*、--canvas 等 |
材质类与组件 |
| 材质 | pg-surface / pg-float / pg-pill 等 class 与 --pg-* 参数 |
组件 |
| 组件 | app/routes、app/components 里的 class |
不引用原色、不写字面量 |
主题包只覆盖语义 token 的值(以及材质参数),不改组件选择器。流媒体壳的 token 块必须放在无层的语义 token 区,放进 @layer base 会被 .dark 的无层规则盖掉。
词汇
| token | 含义 | 夜间值 |
|---|---|---|
content-strong / content / content-secondary / content-tertiary / content-muted / content-faint |
文字层级,随日夜翻转 | 中性白到灰,逐档变淡 |
canvas / surface / surface-raised / surface-high |
页面底、普通面、抬起的卡、更高一层 | 中性近黑,逐档抬亮 |
brand-soft / brand-light / brand / brand-strong / brand-deep |
品牌色族。主站无彩:夜间是白、日间是墨(2026-09-26);动漫版换成它的品牌色 | 白到浅灰 |
success / warning / destructive(及 -foreground) |
状态色,低饱和,只用于状态提示;实底上的文字用对应的 -foreground |
绿 / 琥珀 / 红 |
wash |
半透明"洗"层的底色:hover、按压、分隔线。透明度留在调用处,写成 bg-wash/[0.07]、border-wash/10;暗底是白,亮底是黑 |
白 |
on-media-*、on-media-wash |
压在视频 / 海报上的文字与洗层,不随日夜翻转,画面永远是暗的。播放器目录一律用这一族 | 与 content 族同值,白 |
scrim |
压暗画面的遮罩底色,透明度留在调用处(bg-scrim/60、from-scrim/75) |
黑 |
bg-wash/[0.07] 经 Tailwind 生成 color-mix(in oklab, var(--wash) 7%, transparent),与原来的 rgba(255,255,255,0.07) 渲染结果一致;去字面量迁移时 143 对旧类到新类在同一元素上原地换类截图,字节级一致(渐变分开两个元素比对会有 Skia 抖动带来的 ±1 噪声,不是颜色差)。
字号
@theme 里的 --text-* 只定字号不定行高,行为等同原来的 text-[Npx];自带行高的 text-xs / text-sm 等 Tailwind 档继续用,两套不混。组件按角色引用,不写像素。
| 阶 | token | 值 |
|---|---|---|
| 正文 | micro / label / meta / caption / body / control / lead / lead-lg |
10 / 11 / 12 / 13 / 14 / 15 / 16 / 17 px |
| 标题 | title-xs / title-sm / title / title-lg / headline-xs / headline-sm / headline / headline-lg / display-xs / display-sm / display / display-lg / display-xl |
18 / 20 / 22 / 24 / 26 / 28 / 30 / 32 / 34 / 42 / 44 / 52 / 56 px |
| 流式 | hero-stream(-sm/-md)、detail-title、notfound-code、pricing-hero、pricing-title、auth-title、landing-* |
各处 clamp 原值,按用途命名 |
2026-09-12 迁移时零散字号并入最近一档(10.5 → 11、13.5 → 14、0.72rem → 11、1.05rem → 17 等,最大偏差 0.6px,约 120 处),经页面级截图复验;标题阶逐档保留现状值,流式标题各处原值保留,收敛是设计决定另议。
tailwind-merge 不认识的 text-* 会被当成文字颜色,text-control text-muted-foreground 合并时字号会被丢掉。登记表不再手写:gen-design-tokens.mjs 从 app.css 抽出字号、缓动、圆角、时长 token 生成 app/lib/design-tokens.generated.ts,cn() 读它;改了 app.css 跑 pnpm gen:tokens,pnpm run ci 校验生成物是最新的。
圆角
控件 rounded-md、输入与小按钮 rounded-lg、卡片 rounded-xl、面板 rounded-2xl、抽屉与大卡 rounded-3xl、药丸 rounded-full。这些是 Tailwind 名,值由 --radius 派生,主题改一个变量整体缩放。角色型的三个:rounded-tight(徽章、骨架屏等小件,原来的裸 rounded)、rounded-t-drawer(手机端底部抽屉顶角)、rounded-slab(门户大卡)。不写 rounded-[..]。
动效
时长四档加两档特例,缓动五种,组件只用这些名字:
| 类 | 值 | 用途 |
|---|---|---|
duration-micro |
150ms | 按压、hover 这类微反馈 |
duration-base |
200ms | 绝大多数颜色 / 透明度过渡 |
duration-layout |
300ms | 尺寸、位置变化 |
duration-slow / duration-hero / duration-cinematic |
500 / 700 / 1000ms | 入场、hero、装饰性动画 |
ease-standard |
cubic-bezier(0.4, 0, 0.2, 1) | 默认 |
ease-spring / ease-spring-soft |
回弹 | 滑块、按压 |
ease-sheet |
抽屉 | 底部抽屉进出 |
ease-pop |
弹层出现 | pg-pop |
duration-* 是 @utility,写法与内置的 duration-N 一致(同时设 --tw-duration,tw-animate 的 animate-in 也读它)。app.css 里材质与关键帧动画的时长是逐个校准过的值,不套四档;缓动已统一引用 --ease-*。
材质参数与播放器白天模式
所有材质(播放器的 surface / pill,按钮的 pill-primary / pill-secondary / pill-selected,分段滑块 seg-thumb,门户的 slab / chrome / float,更新提示 notice)都是"固定配方 + --mat-<材质>-<部位> 参数",class 只引用参数,壳与主题只覆盖参数;结构见界面材质。
播放器整体压在画面上,默认只有夜间,但白天模式已经留出位置:主题在播放页作用域(:root:has([data-stream-shell="play"]))覆盖两组东西即可,不需要改播放器组件。
on-media-*与on-media-wash:控件文字与洗层。做浅色玻璃控件时改成深色文字 + 黑色洗层。--mat-surface-*/--mat-pill-*:材质参数。做浅色玻璃或平面 pop 材质时换底色与线色,blur 设 none 就是平面。
视频面本身永远是黑的,不在主题范围内。
规则
- 组件里不出现
bone/ink/gold原色类、white/black透明度写法和类名里的十六进制颜色。流媒体与播放器连 JS 里的十六进制也不写;测速图表、二维码库这类只接受颜色值的地方用组件内常量表。 - 组件里不出现 Tailwind 调色板颜色(
emerald-*、amber-*、rose-*、blue-*等):状态用success/warning/destructive,装饰性图标与强调用foreground的透明度。 - 组件里不出现
text-[Npx]/text-[Nrem]/text-[clamp(...)],字号只从阶梯取。 - 组件里不出现
duration-N、ease-[...]、裸rounded、rounded-[...]。 - 改了 app.css 里的 token 就跑
pnpm gen:tokens,让 tailwind-merge 的登记表跟上;CI 会校验。 - 播放器(
app/routes/stream/player/)里压在画面上的东西用on-media族;流媒体浏览页用content/wash族。分不清就问一句:主题切到浅色时它应不应该变色。 - 新的语义需求先加 token 再用,不在组件里临时拼色。
- 按钮 hover 不填实心
primary/foreground;浮层不手写shadow-2xl,用pg-float。 scripts/check-design-tokens.sh在pnpm run ci里对整个app/做守卫(落地页组件与/dev/ui除外)。确实需要例外时(二维码必须白底、支付品牌色),在该行或上一行写注释token-allow: 原因,不另开白名单。
迁移状态
- 已迁移:颜色、字号、圆角、时长与缓动全部
app/(2026-09-27 守卫扩到全站)。 - 未迁移:落地页组件(
app/components/landing)仍固定夜间、有装饰性字面量,落地页日间另做;测速页图表配色是组件内的常量表(已改无彩)。 - 材质已全部参数化;
pg-beam、tab-slider(滑块的位移动效,外观是seg-thumb)、spd-bg这类效果不算材质。 - 三个本来就不存在于色阶里的类(
text-bone-600、text-gold-50、to-gold-600)在迁移时删除,它们原本不产生任何样式。