chore: scaffold monorepo and migrate uhd_web design system (W0-A)
- 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]>
This commit is contained in:
commit
b22c440139
138 files changed
+14455
No files matched your search
@@ -0,0 +1,25 @@
|
||||
# 设计系统
|
||||
|
||||
控制台(apps/web)沿用 /opt/uhd_web 的设计系统:token 定义在 [app.css](../../apps/web/app/app.css),shadcn 组件在 `apps/web/app/components/ui/`,控制台共享件在 `apps/web/app/components/console/`。
|
||||
|
||||
| 文档 | 内容 |
|
||||
|---|---|
|
||||
| [design-tokens.md](design-tokens.md) | 颜色 token 分层、命名与使用规则,字号/圆角/时长/缓动阶梯 |
|
||||
| [interface-materials.md](interface-materials.md) | 材质:`pg-chrome` 贴边、`pg-slab` 内容卡、`pg-float` 悬浮层 |
|
||||
| [ui-controls.md](ui-controls.md) | 按钮、选中态、状态反馈、动效与圆角角色 |
|
||||
|
||||
## 硬规则
|
||||
|
||||
1. **新增语义先加 token 再用。** 需要新的颜色、字号、圆角、时长或缓动时,先在 `app.css` 定义语义 token(日夜两套值都给),跑 `pnpm --filter @umt/web gen:tokens`,再在组件里引用。
|
||||
2. **禁止原色与十六进制。** 组件里不写 `#xxxxxx`、`text-[#..]`、`bg-red-500` 这类 Tailwind 调色板色、`white/10` 之类的黑白透明度,也不写 `text-[13px]`、`duration-300`、`ease-[cubic..]`、裸 `rounded`。状态只用 `success` / `warning` / `destructive`,并且配文字,不只靠颜色。
|
||||
3. **材质按层级只有三种**,不在玻璃上叠玻璃:已在卡片里的空态用 `<EmptyState plain>`。
|
||||
4. 确需例外(第三方库的颜色参数等),在该行或上一行写 `token-allow: 原因`。
|
||||
|
||||
以上规则由 `pnpm check:tokens`(`apps/web/scripts/check-design-tokens.sh` + 生成物校验)在 `pnpm run ci` 中守住。
|
||||
|
||||
## 控制台共享件
|
||||
|
||||
- `PageHeader`:页面标题 + 一句说明 + 右侧操作区。
|
||||
- `EmptyState`:空态/建设中占位;`plain` 用于卡片内部。
|
||||
- `StatusBadge`:`success | warning | destructive | neutral` 四种语气,必须带文字。
|
||||
- `nav.ts`:12 个工作区的导航表,标题与说明同时供占位页使用。
|
||||
@@ -0,0 +1,95 @@
|
||||
# 设计 token
|
||||
|
||||
> 迁入自 /opt/uhd_web `docs/features/`(2026-09-27)。文中流媒体、播放器、落地页、套餐等段落是 uhd_web 的原始上下文,本项目没有这些页面,只作材质与 token 用法参考;以本目录 README 的约定为准。
|
||||
|
||||
样式入口是 [app.css](../../apps/web/app/app.css)。本说明定义颜色类 token 的分层、命名与使用规则,目的是让主站之外的皮肤(如动漫版)只改 token 值就能换皮,组件源码两边保持一致。材质类的说明见[界面材质](interface-materials.md)。
|
||||
|
||||
## 分层
|
||||
|
||||
| 层 | 在哪 | 谁能引用 |
|
||||
|---|---|---|
|
||||
| 原色 | `@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](../../apps/web/scripts/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 只引用参数,壳与主题只覆盖参数;结构见[界面材质](interface-materials.md)。
|
||||
|
||||
播放器整体压在画面上,默认只有夜间,但白天模式已经留出位置:主题在播放页作用域(`: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`)在迁移时删除,它们原本不产生任何样式。
|
||||
@@ -0,0 +1,104 @@
|
||||
# 界面材质
|
||||
|
||||
> 迁入自 /opt/uhd_web `docs/features/`(2026-09-27)。文中流媒体、播放器、落地页、套餐等段落是 uhd_web 的原始上下文,本项目没有这些页面,只作材质与 token 用法参考;以本目录 README 的约定为准。
|
||||
|
||||
样式入口是 [app.css](../../apps/web/app/app.css),具体色值与透明度只在 CSS 中维护。颜色 token 见[设计 token](design-tokens.md),按钮与控件的用法见[控件使用标准](ui-controls.md),三块壳下的实物陈列在开发态 `/dev/ui`。
|
||||
|
||||
## 设计原则
|
||||
|
||||
全站(用户中心、公共页、测速、流媒体)是同一套审美,改样式先对照这几条:
|
||||
|
||||
1. **主色无彩**。夜间是白、日间是墨。彩色只来自内容(海报、剧照),外加一种很淡的棱镜光。状态色 `success` / `warning` / `destructive` 只表示状态,不做装饰。支付方式保留品牌色。
|
||||
2. **棱镜光只出现在三处**:左上角的漏光 `PrismLeak`、主操作按钮沿边框转动的光谱环、流媒体入口(侧栏的播放键 `prism-ring` 与菜单里的三角 `prism-icon`)。不再往别处加彩虹。
|
||||
3. **材质按层级只有三种**:
|
||||
- 贴边的用 `pg-surface` / `pg-chrome`;
|
||||
- 内容卡用 `pg-slab`,是磨砂面,好读优先;
|
||||
- 悬浮层用 `pg-float`,只有它用较深的投影。
|
||||
|
||||
不做边缘折射,也不在玻璃上再叠玻璃。
|
||||
4. **一个角色一种外观**:
|
||||
- 主操作是半透明胶囊加棱镜环,一屏只放一个;
|
||||
- 次操作是描边胶囊;
|
||||
- 选中态是静态的 `pill-selected`;
|
||||
- 分段控件的滑块是 `seg-thumb`。
|
||||
|
||||
全站没有实心按钮,也没有实心的选中块。
|
||||
5. **日夜两套,画面跟随页面**。大图、横向滚动行两侧的渐隐和压暗层都取页面底色,不硬切成黑色。播放页永远是夜间,落地页暂时固定夜间。
|
||||
6. **同一种信息用同一种结构**:
|
||||
- 套餐卡的零件在公共定价页和订阅服务页共用;
|
||||
- 电影和剧集详情页共用 `DetailExtras`;
|
||||
- 流媒体页面的内容区统一用 `max-w-[1800px]` 加 `px-4 sm:px-6 lg:px-10`,标题、海报行和顶栏对齐。
|
||||
7. **动效只做一次性的**。入场、按压回弹、悬停反馈可以做;持续运动的装饰不做。主按钮的光谱环是唯一的例外,而且在减少动态效果时停转。
|
||||
|
||||
这些规则里能机器检查的,由 [check-design-tokens.sh](../../apps/web/scripts/check-design-tokens.sh) 在 `pnpm run ci` 中守住(见[设计 token](design-tokens.md#规则))。
|
||||
|
||||
## 结构
|
||||
|
||||
一个材质 = 一份固定配方 + 一组参数。配方写在 class 里(哪几层、怎么叠),参数是 `:root` 上的 `--mat-<材质>-<部位>`:
|
||||
|
||||
| 部位 | 含义 |
|
||||
|---|---|
|
||||
| `bg` / `rgb` | 底色(surface 用 rgb 三元组配 `--pg-surface-alpha`,其余是完整颜色或整段 background 值) |
|
||||
| `sheen` / `reflection` / `glow` | 顶部光泽、斜向反光带、鼠标辉光,整段渐变 |
|
||||
| `blur` | backdrop-filter 值 |
|
||||
| `border` / `hairline` | 边 |
|
||||
| `highlight` / `shade` | 上沿内高光、下沿内暗线 |
|
||||
| `shadow` | 外投影(配方里各层形状不同的材质,这一项存整条 box-shadow 列表) |
|
||||
| `solid` | blur 不可用或需要不透明时的实心兜底 |
|
||||
|
||||
材质分三组:
|
||||
|
||||
- 播放器与媒体区:`surface`、`pill`;
|
||||
- 门户:`slab`、`chrome`、`float`,以及按钮用的 `pill-primary` / `pill-secondary` / `pill-selected` 和分段滑块 `seg-thumb`;
|
||||
- 更新提示胶囊:`notice`。
|
||||
|
||||
浅色门户、深色门户与流媒体的差别都只是参数覆盖,不各自重写声明。换皮做平面材质时,把 blur 设成 none、光泽设成透明、投影收掉即可,class 不动。
|
||||
|
||||
| 场景 | 样式 |
|
||||
|---|---|
|
||||
| 贴边的 header、筛选栏、播放器底栏 | `pg-surface`,模糊与底色,不叠加悬浮面板的泛光亮线 |
|
||||
| 悬浮层:下拉菜单、弹出框、下拉选择、对话框、抽屉、错误卡 | `pg-float`,全站唯一。菜单类 `rounded-2xl`,对话框 `rounded-3xl`,调用处不改底色、边框、投影。菜单里再弹出的子菜单、手机底部抽屉加 `pg-float-solid`(不透明),避免透出下面一层 |
|
||||
| 药丸 HUD、徽章等小型悬浮元素 | `pg-pill` |
|
||||
|
||||
新组件先确认它是贴边、内容还是悬浮,再复用对应样式。滚动行卡片本体和高频跟随鼠标的进度气泡不加背景模糊。
|
||||
|
||||
## 门户材质
|
||||
|
||||
`pg-slab` 用于外层内容卡,`pg-chrome` 用于侧栏、顶栏和贴边操作条,`pg-float` 用于悬浮面板;嵌套的子卡保持轻填充,避免多重玻璃。Dialog / Card 原语自带的 utilities 可能覆盖 components 层的材质类,要核对最终的 class 合并和层叠,不要继续叠加透明度补丁。
|
||||
|
||||
内容卡是磨砂面,不追求透明:背景模糊只让底下的明暗和淡彩柔和地渗进来,层次靠亮棱边、顶部内高光和轻投影区分。投影只用贴边一层加很淡的环境一层,日间更轻(`--shadow-card` 按日夜分别给值)。页面底层是 `bg-page-glow` 的无彩明暗光斑,不铺网格。`PrismLeak` 在每个页面外壳各放一个(落地页与播放页除外),浓度与叠加方式见 `--mat-leak-*`。
|
||||
|
||||
主操作 `pill-primary`:
|
||||
|
||||
- 一圈光谱沿边框转动,6 秒一圈,内侧透一层很淡的同色光;
|
||||
- 悬停时变亮,禁用或减少动态效果时停转;
|
||||
- 参数是 `--mat-pill-primary-*` 与 `--prism-*`。
|
||||
|
||||
次操作 `pill-secondary` 与选中态 `pill-selected` 分别由 `--mat-pill-secondary-*`、`--mat-pill-selected-*` 控制。按下回弹 `scale(0.96~0.97)`。
|
||||
|
||||
动效:
|
||||
|
||||
- 页面入场用 `motion-safe:animate-blur-in`(失焦到清晰,填充模式 backwards,播完不留 filter,避免破坏 fixed 定位的子元素)。
|
||||
- 需要鼠标反馈的卡片(套餐卡)挂 `.pg-spotlight`,并用 `rippleMove` 写入 `--mx/--my`。光在卡面底色之上、文字之下,亮度参数是 `--mat-spotlight`,只在能悬停的设备上出现。
|
||||
- `.pg-beam` 巡边光只留给少数常驻小徽标。
|
||||
|
||||
## 流媒体大图
|
||||
|
||||
流媒体首页与详情页的大图通栏显示,跟随日夜:
|
||||
|
||||
- 压暗层(`.hero-vignette` 与各层渐变)取页面底色:夜间压暗配浅色字,日间提亮配深色字。
|
||||
- 整个画面层在下沿渐隐成透明,露出页面的底色与光斑,和下面的内容连成一片。
|
||||
- 渐隐加在不缩放的外层,ken-burns 放大时不会裁出硬边。
|
||||
|
||||
详情页的标题区向上压进渐隐区,海报、规格徽章和按钮在首屏内。
|
||||
|
||||
画面层下面垫一层页面底色,加载占位 `BackdropSkeleton` 用同一个盒子、同样的渐隐。这样图片没到、数据没到的时候,顶栏下面也不会先露出后面的棱镜漏光再被盖掉,切换首页和详情页时顶栏颜色不跳。
|
||||
|
||||
列表页(电影、剧集、收藏、合集、演职人员)不做色带:标题、筛选条和内容坐在同一张页面底上。吸顶筛选条 `StickyFilterBar` 平时透明,滚到顶栏下面吸住之后才出现磨砂底,和顶栏连成一条。
|
||||
|
||||
## 其他
|
||||
|
||||
- 侧栏的选中项由滑块(`seg-thumb`)承担主要形态,导航文字与 hover 保持清晰;分组展开、滚动和路由变化都会影响滑块位置。
|
||||
- 改支付界面的样式时,要覆盖确认、二维码、错误与返回路径。套餐的优惠金额从价格推导,不写死。交易摘要用现有的 RemarkText 展示,不为排版改写后端摘要的含义。
|
||||
|
||||
主动使用 Taste 的触发条件和项目适配见[前端规范](../architecture.md)。
|
||||
@@ -0,0 +1,62 @@
|
||||
# 控件使用标准
|
||||
|
||||
> 迁入自 /opt/uhd_web `docs/features/`(2026-09-27)。文中流媒体、播放器、落地页、套餐等段落是 uhd_web 的原始上下文,本项目没有这些页面,只作材质与 token 用法参考;以本目录 README 的约定为准。
|
||||
|
||||
配合[设计 token](design-tokens.md)与[界面材质](interface-materials.md)。开发态的陈列页 `/dev/ui`(只在开发构建注册,[routes.ts](../../apps/web/app/routes.ts))把三块壳下的 token、材质和控件排在一页,换皮或改 token 先看它。
|
||||
|
||||
## 按钮只有一套
|
||||
|
||||
全站按钮(门户、公共页、测速、流媒体、播放器浮层)是同一套外观,与动漫站同一思路:形状一律胶囊或正圆,颜色与材质只由角色决定。
|
||||
|
||||
| 角色 | 门户写法 | 流媒体写法 | 外观 |
|
||||
|---|---|---|---|
|
||||
| 主操作 | `<Button>` | `btn-glass btn-glass-primary` | 半透明胶囊 + 流转的棱镜光谱边框,不做实心 |
|
||||
| 次操作 | `<Button variant="outline">`(`secondary` 同样子) | `btn-glass` | 描边胶囊 |
|
||||
| 图标钮 | `<Button variant="outline" size="icon*">` | `btn-glass-icon` | 描边正圆 |
|
||||
| 只有文字 / 图标 | `<Button variant="ghost">` | 各自的 ghost 行 | 悬停出淡底 |
|
||||
| 危险操作 | `<Button variant="destructive">` | — | 淡红底红字胶囊 |
|
||||
|
||||
一屏(或一张卡片)只放一个主操作,并列的其余动作退为描边;前置条件没满足时,真正该做的那一步是主操作(例如团队页没有团队套餐时,「创建团队套餐」是主操作,「添加成员」退为描边)。
|
||||
|
||||
两边的底、边、字色、投影都引用 `--mat-pill-*`,改外观只改参数。调用处只给布局(宽度、高度、间距、定位),不改圆角、底色、边框、投影和字色;不是 `<button>` 的元素(`Link`、`a`)用 `buttonVariants()` 拿同一套类名。分段标签、菜单行、列表行不算按钮,按各自控件规范;分页当前页、角标、选中的季 / 集 / 选项、日历选中日用静态的 `pill-selected`,不借主按钮样式;分段控件(侧栏、SegmentedTabs、求片页的分段)的滑块用 `seg-thumb`。放胶囊或分段控件的横向滚动容器加 `scroll-bleed`:`overflow-x-auto` 会连带裁掉纵向溢出,不加的话胶囊的阴影和描边会被切成直边。
|
||||
|
||||
右上角账户模块全站共用 [`account-popover.tsx`](../../apps/web/app/components/account-popover.tsx):用户中心用头像 + 名字的胶囊,流媒体与测速用圆头像;菜单里是另外两块的跳转(当前所在的不列,流媒体与测速需有生效订阅)与退出登录。
|
||||
|
||||
其余控件仍分两处实现:门户原语在 `app/components/ui/`(shadcn 语义 token),流媒体控件在 `app/routes/stream/_sub/`、`app/routes/stream/player/controls.tsx`(浏览页 `content` / `wash`,播放器 `on-media` 族)。需要相同行为时各自实现。
|
||||
|
||||
## 尺寸与圆角
|
||||
|
||||
- 图标钮:桌面 36px,粗指针 40px(`[@media(pointer:coarse)]`);播放键等主钮 44 / 48。
|
||||
- 圆角角色:控件 `rounded-md`、输入与小按钮 `rounded-lg`、卡片 `rounded-xl`、面板 `rounded-2xl`、抽屉与大卡 `rounded-3xl`、药丸 `rounded-full`、徽章与骨架屏 `rounded-tight`。
|
||||
- 字号:控件文字 `text-control`(15),菜单项 `text-body` / `text-caption`,面板小标签 `text-label` 大写 `tracking-[0.18em]`。
|
||||
|
||||
## 状态反馈
|
||||
|
||||
- hover:洗层 `hover:bg-wash/[0.06]`(播放器 `on-media-wash/[0.07]`),文字提一档(secondary → strong)。
|
||||
- 按压:`active:scale-[0.95]`(圆形主钮 0.92),`transition-[transform,background-color,color] duration-micro`。
|
||||
- 选中:`pill-selected`(静态半透明底 + 稍深描边);单选列表项左侧 Check 标记。进度用细线表示,不做实心填充。
|
||||
- 打开中的触发钮:`bg-wash/[0.08]` 保持高亮。
|
||||
- 焦点:门户走 shadcn 的 `focus-visible:ring`,玻璃族 `focus-visible:ring-2 ring-brand/50`。
|
||||
- 禁用:`opacity-50 cursor-not-allowed`,不改颜色。
|
||||
- 危险:移除、删除这类动作平时是中性描边,悬停时换成 `destructive` 的边框与文字。
|
||||
|
||||
## 动效
|
||||
|
||||
`duration-micro` 微反馈、`duration-base` 颜色透明度、`duration-layout` 尺寸位置、`duration-slow` 及以上只给入场与装饰;缓动默认 `ease-standard`,滑块与按压回弹 `ease-spring`,抽屉 `ease-sheet`,弹层 `animate-pg-pop`。尊重 reduced motion。
|
||||
|
||||
## 大写小标签
|
||||
|
||||
`SectionLabel`([section-label.tsx](../../apps/web/app/components/section-label.tsx)),三种口味:`portal`(门户表单字段名与分组,`text-xs font-semibold uppercase text-muted-foreground`)、`page`(浏览页,`text-label uppercase tracking-[0.18em] text-content-muted`)、`media`(播放器弹层,同上换 on-media)。`as` 指定元素(label / span / p)。
|
||||
|
||||
还有几种写法没收进来,值不同、收进来就是像素变化,待设计决定:门户里 `text-xs font-medium uppercase tracking-wider`(团队订单弹窗)、`text-micro … tracking-[0.15em]`(侧栏分组、邀请页)、`tracking-widest` 一处;落地页 hero 的 `tracking-[0.32em] text-white/45` 与分区标题的 `text-meta font-bold tracking-[.22em] text-brand-strong` 是刻意的装饰用法,单列为 hero kicker 与 section kicker。
|
||||
|
||||
## 弹层结构
|
||||
|
||||
触发钮 + 面板,面板由 `usePopover` 管开关(点外面 / Esc 关闭)。面板顶部一行大写小标签说明这是什么,下面是列表;列表项分两种:单选行(左勾选 + 标签 + 右侧提示)与动作行(左图标 + 文字)。四件在 `player/controls.tsx`:`MenuTrigger`、`MenuPanel`、`MenuOption`、`MenuButton`。`MenuPanel` 通吃三处:`placement` 上方(播放器)/ 下方(浏览页 header),`tone` 决定表头口味,`role` 给 listbox,内边距、宽度、z 与偏移经 className 传入,tailwind-merge 让它们覆盖默认值,所以三处的 class 集合与各自收拢前完全一致(用 react-dom/server 渲出来逐项核对过)。账户菜单的头像表头与排序菜单的单选行样式不同,仍各自写在页面里。
|
||||
|
||||
## 收拢原则
|
||||
|
||||
- 同一份 class 串出现在两处以上就抽原语,抽的时候 class 串逐字不改,渲染不变。
|
||||
- 参数只到必要:尺寸档、是否激活、标签、图标;不把整页状态传给控件。
|
||||
- 只有一处用的局部组件留在页面文件里,不为了"以后复用"提前抽。
|
||||
- 两个页面展示同一种信息时共用零件:套餐卡用 [plan-card-parts.tsx](../../apps/web/app/components/plan-card-parts.tsx)(名称价格、规格表、线路提醒、共有权益),电影与剧集详情用 [DetailExtras.tsx](../../apps/web/app/routes/stream/_sub/DetailExtras.tsx)(规格徽章、影片信息、可播放版本、更多推荐)。
|
||||
Reference in new issue
Block a user