文档索引

前端样式规范

本站的全站设计标准,以站内博客目录页(/blog)为主要视觉与交互基准。 新页面与既有页面改版均按本文设计;尚未对齐的页面不因此成为新的标准。 本文规定设计目标、组件边界与验收要求,源码提供实现,维护计划登记待办。

1. 定位与使用方式

1.1 适用范围

标准覆盖界面中的布局、颜色、排版、形状、控件、交互状态、动效与响应式行为。 博客目录页提供可复用的设计关系:页面底色上摆放卡片,以胶囊组织操作, 用连续伸展的外壳连接收起态与展开态。其他页面按自己的内容与任务组织这些元素。

阅读页、工作台、编辑器与用户选择的装饰款式有各自的内容约束,范围见 §7。 这些差异不改变公共导航、按钮、字段、主题与可访问性的基本要求。

1.2 如何查阅

  • 确定整体方向先读 §2;选择视觉尺度查 §3。
  • 组织页面查 §4;选择组件与交互语义查 §5。
  • 设计状态变化与过渡查 §6;组合典型页面查 §7。
  • 实现、复用和验收查 §8;源码入口见附录 A。

同一规则只在一个位置定义。组件章节说明如何应用基础规则,场景章节只规定 该场景需要的组合与尺寸。差异必须写明对象和用途,不能靠章节先后判断哪条生效。

1.3 规范的维护

设计改变时直接修改对应规则,并同步受影响的组件、引用与验收依据。 仅改文档不代表已有页面已经完成适配;实现差异与未完成工作登记在 docs/plan/maintenance-plan.md,不得把旧实现写成通用规则来消解差异。

修复过程与已完成记录查 Git 历史。本文不追加按日期排列的补充说明、故障经过、 已修问题清单或逐页样式台账。数值表只保留做设计决策所需的内容; 完整令牌定义、组件落点与业务权限分别由源码、守卫和架构文档维护。

需要一份能整份转发、不依赖本站源码的对外版本时用 docs/design-philosophy.md: 它讲原则与判据,数值与本文一致 —— 改数值要两处一起改。

2. 整体设计原则

  1. 胶囊与大圆角。 单行操作、字段、导航和统计标记采用胶囊; 内容卡片采用固定大圆角。形状由内容和用途决定。
  2. 底色建立层级。 页面、卡片、卡内子区通过主题底色与留白分开。 普通卡片不描边、常态不投影;选中态使用淡底与同色文字,不添加装饰竖条。
  3. 内容有主次。 标题、正文、辅助信息、数据分别承担不同角色。 用字号、字重、间距和颜色共同建立层级,弱化信息仍须可读。
  4. 反馈对应操作。 可以点击、当前选中、键盘聚焦、已经展开是不同状态。 非交互内容不借用按钮或可点击卡片的反馈。
  5. 动效长在组件上。 所有交互状态都要有可见过渡;外壳伸展说明内容来自哪里, 前后两态的部件一一对应,滑块说明选中了哪项,内容入场说明展开的先后关系。 动效不是盖在组件上的一层效果。按钮和卡片的悬停反馈不做垂直位移。
  6. 文案服务使用。 保留功能名称、必要说明、操作提示与状态反馈。 不主动增加抒情、装饰性或凑气氛的副标题;图标按钮仍须提供可访问名称。

3. 基础视觉标准

3.1 颜色与主题

主题令牌定义于 src/styles-scss/base/_root.scss,由 <html data-theme="light|dark"> 切换。界面使用令牌,不在页面里另建明暗色板。 下面列出核心颜色作为设计参照,完整定义以该文件为准。

角色 令牌 浅色 深色
品牌主色 --color-brand-primary #2563EB #23A5FF
品牌淡底 --color-brand-secondary 品牌蓝 10% 品牌蓝 10%
页面底色 --color-background-page #F8FAFC #131517
卡片底色 --color-background-card #fff #181A1D
卡内内容底色 --color-background-content #f4f6f9 #21252A
弱背景 --color-background-subtle #f3f4f6 #1e2024
主要文字 --color-text-primary #0f172a #f1f5f9
辅助文字 --color-text-secondary #64748B #94A3B8

字段一般放在卡片内,使用页面底色;正文中的嵌入块使用内容底色。 同色区域相贴时先调整底色层级或留白,不能直接加描边补救。 未读等特定状态已有专用背景令牌,使用时同时保留文字或图标提示。

功能色与分类强调色分开使用:

用途 令牌 边界
危险、删除;既有点赞状态 --color-warning-primary / secondary 这组是红色,名字不表示黄色
成功;既有评论统计;行情上涨 --color-success-primary / secondary 绿色不代替所有“已选中”状态
投喂交互与鱼干统计 --color-brand-primary / secondary 沿用本站品牌蓝
收藏状态 --color-star-primary / secondary 仅用于收藏图标的交互与已收藏状态
分类与并列内容的区分 --color-accent-blue / amber / cyan / violet 及各自 -soft 不承担成功、失败或危险的含义

收藏未激活时图标跟随灰色 currentColor,不能预先点亮。 分类色可同时驱动某张功能卡的图标、按钮与悬停光晕,三者保持同一色相; 普通可点击卡片默认使用品牌蓝光晕。

令牌使用边界:

  • --color-text-tertiary 实际是浅淡背景与分隔色,不用于弱化文字。
  • 分隔线可使用 --color-border;它不授权给普通卡片增加轮廓。
  • 现有 --fd-* 是兼容别名;新样式优先使用语义明确的 --color-*, 不能据此建立另一套设计体系。
  • 白字配实底按钮、开关的白色圆钮可以使用固定白色。原创插画、画报、 identicon 和图表的数据着色按其内容定义;界面外壳仍使用主题令牌。
  • 使用变量前确认其定义或组件提供方存在。不能假定存在 --color-danger-*、 --color-brand-primary-rgb 或自行拼出变量名。

3.2 字体与排版

角色 字体令牌 使用方式
界面与普通文字 --font-body Kumbh Sans,中文走系统回退字体
品牌英文 --font-brand Outfit,仅用于品牌表达
代码与代码编辑 --font-mono JetBrains Mono
界面数据 --font-numeric Raricy Numeric,数字与数值符号等宽
书页阅读模式 --font-reader-serif 使用该阅读模式的衬线回退栈

字体栈集中定义于 src/styles-scss/base/_root.scss,控件继承所在界面的字体。 随站点提供的字体及授权见 public/static/fonts/README.md。 数学内容保留数学排版所需的字体。

顶栏品牌按三级排列:图标 → 英文标准字 → 中文标准字。图标到英文的距离固定为 6px, 必须小于英文到中文的距离(0.5em,约 12px);两段等距会把「图标 + 英文名 + 中文名」 读成三段并列文字,品牌层级随之消失。

界面正文通常为 0.9–1rem,辅助信息为 0.75–0.875rem; 列表标题以博客卡片的桌面 1.3rem / 600、窄屏 1.05rem 为参照。 页级标题使用 600 / 700 字重,按页面体量与视口调整,不能让大标题挤压主要操作。 这些是排版尺度,不代表存在同名 CSS 令牌。

阅读正文与界面文字分开:博客正文采用 1.15rem 字号、2 行高, 段落两端对齐;代码块维持等宽排版并在自身区域内滚动。 链接默认不加下划线,依靠颜色与上下文识别;说明文中的链接可保留下划线, 悬停时不能靠改变字重挤动相邻文字。

点赞数、金额、页码、日期、进度等数据使用 .numeric; 数值与自由文字混排时只包裹数据部分。用户名、标题、文件名与正文里的数字 属于文字内容,不给整块内容套数字字体,也不扫描正文后改写其中的数字。

3.3 圆角与形状

对象 标准 说明
单行按钮、字段、导航、徽章 999px,即 $radius-max 呈胶囊形
普通卡片、主要内容面板、弹窗 30px,即 $radius-card 固定圆角,不随内容高度增长
窄屏紧凑列表卡片 18px 以博客目录 ≤768px 的卡片为基准
多行字段与输入面板 20px,即 $radius-field 不因输入行数变化而变成椭圆
次级内容块、小标记 10px / 5px,即 $radius-large / small 按内容密度选用
圆形图标按钮与角标 50% 正方形盒子形成圆形

窄屏的 18px 用于收紧后的列表条目,不要求阅读卡、大面板和弹窗一律缩成同一尺寸。 输入框的判据是内部是否仅有一行文字:单行输入与选择字段为胶囊, textarea、正文编辑区与多行组合面板为固定圆角。

有淡色底座的小图标容器一律圆形。 小方块 + 淡色底 + 图标的容器取 50%, 不随底座尺寸或所在页面变化。头像仍按 §3.4 的 8% 方角;卡片、按钮、输入框不适用这条, 它们的圆角由上面的表与用途判据决定。非正方形的底座先把盒子改成正方形 (50% 会把长宽不等的盒子拉成椭圆)。

展开外壳保留收起态的曲率。 高 42px 的栏目触发器收起时为胶囊, 同一外壳展开后保持 21px 固定圆角;不能继续用 999px 把长面板变成椭圆。 嵌套胶囊的内外圆角与间距相配,例如高 48px 的外壳内放高 40px 的按钮, 四周留 4px,对应外圆角 24px、内圆角 20px。

3.4 头像

头像统一为 8% 圆角的正方形,使用百分比随尺寸缩放,不改成圆形头像。 这同时是图标底座圆形规则(§3.3)的例外:带淡色底座的小图标一律 50%, 头像始终是 8% 方角。 内联样式同样使用 borderRadius: '8%'。位图按头像盒子裁剪, 头像框贴图铺满同一盒子,不占布局、不接收点击。

尺寸由所在组件决定:目录作者 20px、阅读页作者 32px、 目录个人面板展开头像 48px、个人主页身份头像 120px。 这些是典型用途,不作为所有页面的固定尺寸清单。

界面头像使用 src/app/components/Avatar.tsx;字符串渲染管线按其 DOM 结构输出。 完整渲染落点与结构约束由 tests/unit/avatar-sites-guard.test.ts 维护。 头像框素材的几何与交付要求见 docs/guide/头像框出图规范.md。

3.5 间距与尺寸

优先沿用 4px 基础尺度:4、8、12、16、20、24、32、40、48px, 分别用于贴边间隙、行内间距、控件内距、内容分组与大区间留白。 现有 SCSS 间距变量在 src/styles-scss/abstracts/_variables.scss; 没有对应变量的尺寸不能自行假定已有令牌。

同一行控件保持等高,文字与图标垂直居中;关系相近的内容间距小于区块之间的间距。 卡片内距要让文字避开圆角,窄屏随内容密度收紧。 博客卡片的非对称内距属于该组件的光学调整,不逐项推广为全站间距变量。

按钮尺寸按用途决定,三档按钮统一表面与状态,不强制复制“创建”按钮的字号和宽度。 主要触屏图标入口采用至少 44px 的点击区域; 紧凑工具栏可缩小可见图标,但要保留足够且不重叠的点击区域。

3.6 阴影、轮廓与遮罩

用途 令牌或配方 显示时机
可点击卡片的反馈 --shadow-card-brand 整卡可点时悬停显示
浮层与固定顶栏 --shadow-card-brand 保持可见时显示
字段与键盘聚焦 --shadow-focus-brand 聚焦时显示
开启态的控件光晕 --shadow-glow-brand 开关 / 勾选项处于开启时显示
普通模态遮罩 rgba(0,0,0,.5) 模态面板展开时显示

普通卡片常态无阴影,卡内只有局部按钮可点时,不能让整卡悬停发光。 不用黑色 box-shadow;内容分类明确的功能卡可用 §3.1 的同色光晕。 三种光晕的语义各有归属:抬起是“卡片可点 / 浮层”,聚焦只属于字段与键盘焦点, 开启态只属于“这一项现在是开着的”。它们不能互换,也不能拿开启态光晕做装饰性提亮 或替代焦点标识。 右上角 Toast 不投影:它靠状态色淡底与背景模糊浮起来(见 §5.7)。

容器不使用 border、outline 或 inset 阴影模拟装饰轮廓,不加左右竖条表示选中。 结构性边框同样不画:列表与工作台的分栏线、区段横线、输入区顶线、弹窗分页顶线 一律删掉 —— 层级由页面、卡片、内容底色与留白承担,不靠线把区域「框」出来。 通知列表的按日分组也不用贯通横线,分组层级由标题字号、字重与卡片间距建立。 这个限制不取消键盘焦点标识,也不禁止有内容语义的图形: 正文引用块、图表轴线、日期分隔、新消息线、菜单分组线、滚动条结构, 以及 §7.5 的专用内容边界按其用途设计。 聊天气泡(含 data-bubble 用户款式)的边框、底色与圆角属于气泡自己的外观 (§7.4、§7.5),不随这条改变。

遮罩属于背景,不是投影。遮罩的层级由模态范围决定: 博客目录的移动个人面板只遮页面内容,顶栏保持在其上方; 全局弹窗覆盖自己的完整交互范围。不能把某页的 z-index 数字直接当成全站层级。

3.7 图标

优先复用 public/static/img/icons/ 的 SVG mask;已有 React 界面可沿用 已安装的 Lucide 图标与现成内联 SVG,不为单个图标增加另一套图标库。 同一组操作保持相近的视觉重量、尺寸和基线。

图标颜色跟随 currentColor,状态由承载控件提供。箭头随展开方向旋转; 计数图标与数字同行。纯装饰图标退出读屏,单独操作图标的名称由按钮提供。 业务图形(小鱼干、品牌标识等)复用站内素材,不用含义近似的图标替代。

4. 布局与响应式

4.1 容器、页边距与阅读行宽

普通页面的 .container、顶栏 .site-container 与页脚内容共用宽度阶梯:

视口 容器上限 左右内距
<992px 可用宽度的 100% 16px
≥992px 960px 16px
≥1200px 1140px 16px

实现复用 container-padding 与 site-width-ladder。 通栏色带占满外部宽度,其内容另放容器。 页边距由一层外壳负责:组件嵌入页面时不重复提供容器、标题与整页内距。

阅读行宽在容器内部另设上限。现有博客详情外层为 940px, 正文列为 900px;故事普通阅读与互动阅读分别为 820px、720px。 这些上限用于控制阅读密度,不随普通容器扩大。 后台是专用宽工作区:外壳上限 1440px、左右 20px,由 AdminShell 统一承担。

4.2 页面组织

  • 页面主标题只出现一次,标题、工具栏与主体内容有明确的上下关系。
  • 侧栏加主列使用 min-width: 0 或 minmax(0, 1fr) 允许内容列收缩; 侧栏按自身高度吸顶,不被长列表拉伸。
  • 网格与并列卡片按可用空间折列,保持内容顺序和主要操作可达。
  • 筛选无结果时保留筛选控件,分页与页脚之间保留足够空间。
  • 宽内容只在自己的区域滚动,例如代码、表格、图表与密集操作条; 不能把整个页面撑出横向滚动条。

4.3 固定导航、安全区与层级

顶栏内容基高为 62px,总高度由 --navbar-height 表达, 包含 --safe-top。页面补偿、工作台高度与吸顶位置读取这个变量, 不能各自再抄一份顶栏高度。

普通吸顶工具栏在顶栏下额外留 12px,博客目录的筛选条与个人侧栏使用同一基准。 吸顶背景或渐变使用当前主题底色,不遮挡下拉,也不制造横向滚动。 页面内容、局部吸顶、局部浮层、固定导航、全局模态保持清晰的层级关系。

安全区统一读取 --safe-top / bottom / left / right; 父层已经让出的安全区,子层不能重复叠加。 占满可见高度的页面使用 dvh,可先写 vh 兜底; 软键盘引起的可视区域变化由工作台自己的视口适配处理。

4.4 响应式决策

通用断点为 576、768、992、1200px,定义于 src/styles-scss/abstracts/_breakpoints.scss。 断点用于安排布局变化,不等于所有组件都必须在同一点折叠: 顶栏的 808px、后台与讨论侧栏的 900px 属于按内容容量确定的组件断点。

空间不足时依次考虑收紧留白、换行、折列、收进可展开入口。 隐藏文字后必须保留名称与操作能力;长名字在没有悬停提示的触屏上仍要有读全的路径。 局部工作区的折行按实际容器宽度决定,适合时使用容器查询, 不要用视口宽度猜测一张卡片还剩多少空间。

5. 通用组件标准

5.1 按钮与图标按钮

按钮只有以下三档公共表面,定义在 src/styles-scss/abstracts/_mixins.scss:

档位 用途 空闲 悬停 按下
btn-primary 当前主要行动 品牌淡底、品牌字 品牌实底、白字 整体压暗
btn-secondary 辅助操作 无底色、辅助文字色 品牌淡底、品牌字 品牌实底、白字
btn-tab 导航、筛选、当前项选择 未选中同次级;选中为品牌淡底、品牌字 当前项保持选中表面 使用淡底,不闪成实底

危险操作沿用对应档位的层次,颜色换成危险色,不另造第四档按钮。 状态按钮的激活态见 §6.1。任何档位都不做垂直位移,也不以新增描边表示按下。

优先复用公共按钮类或 mixin;尺寸、字号和图标间距由所在组件补齐。 当前项使用 .is-active 并提供对应 ARIA 状态;兼容旧 .active 不作为新写法。 禁用按钮降低视觉强度并实际禁止操作,加载时保持外壳尺寸和名称稳定。

图标按钮复用次级档的反馈。主要入口采用圆形或胶囊点击区; 密集桌面列表可使用可见盒子 30px、圆角 8px 的紧凑按钮, 触屏仍按 §3.5 保留点击区域。仅图标的控件必须有 aria-label, title 只作额外提示。

5.2 输入框、搜索框与表单控件

单行输入与选择字段使用胶囊,多行字段使用 20px 固定圆角。 默认表面为页面底色、主要文字色、无边框,复用 field-surface 或公共表单类。 聚焦使用 --shadow-focus-brand,不换底色、不临时增加边框宽度。

搜索框复用 .search-input;内嵌按钮时,按钮距上、右、下各 4px, 弧线与外壳相配,输入文字为按钮保留空间。 只有内嵌按钮的搜索字段需要右侧留白,独立按钮和纯搜索输入不套用这段几何。

字段有持久标签,placeholder 只提供示例;错误与帮助信息靠近字段并关联到控件。 错误使用文字与危险色表达,错误、聚焦和校验状态不能改变字段几何。 触屏单行字段字号至少 1rem;紧凑桌面字号不直接套到触屏输入。

选择字段沿用字段表面,不伪装成主要行动按钮。 原生 select 的选项和分组同时使用主题底色与字色,并配合 color-scheme。 复选框、单选框与开关保留各自语义、标签和键盘操作; 不能因为都是“二选一”就换成胶囊滑块。

5.3 卡片、内容块与统计胶囊

卡片遵循 §3 的底色、圆角和阴影规则。 整卡可点击时使用真实链接或按钮;拉伸链接不能盖住作者链接等局部操作。 卡片中只有一颗按钮可点时,卡片本身不提供点击光晕或手形指针。

卡内的嵌入内容使用内容底色与适当内距,避免同色卡片层层套壳。 渲染器负责正文时,页面必须明确外壳由哪一层承担; 同一内容不能叠加两份卡片底色、内距或页边距。

标签、身份标记与统计胶囊是信息组件。图标和数据靠近排列, 需要时用功能色或分类色区分;没有操作就不显示按钮反馈。 选中、佩戴中或“我”使用淡底与文字信息表示,不用装饰竖条、描边环或 inset 伪边框。 正文引用块的竖线属于内容语义,不套用装饰竖条的限制。

5.4 导航、筛选与分页

同一层导航保持一致的形状、间距与当前项表达,默认使用 btn-tab。 互斥项选择可以使用 aria-pressed;指向当前页面的链接使用 aria-current。 视觉共用不代表 ARIA 角色必须相同。

多选项筛选、栏目层级、可增加的分类列表使用普通导航项、下拉或复选控件。 分页当前页使用品牌淡底与品牌字,前后页使用站内箭头图标; 跳页字段为单行胶囊。切换范围时搜索、筛选和页码的保留规则由业务定义, 必须与当前界面显示一致。

分页标记复用 src/app/components/SitePager.tsx(博客 / 通知 / 对外目录 / 小鱼干流水 / 图床与音频床管理都是它),页码窗口与折叠的算术在 src/lib/page-window.ts。 只渲染窗口内的页码、首尾常驻、缺口折成 …,不把全部页码平铺成一行; 「没有第二份手写分页标记」由 tests/unit/site-pager-guard.test.ts 盯着。 管理端 /admin/* 的 AdminPager 是另一套(不带跳页框),两套并存是刻意的。

5.5 胶囊滑块

.segmented 仅用于同一份内容的两种互斥呈现或排序方式, 例如发布时间 / 更新时间、K 线 / 折线。 执行动作、设置开关、买卖方向、金额或杠杆参数选择不属于这种视图切换; 三种及以上、选项数量可能增加的切换采用普通标签或选择控件。

轨道有固定内距,两个按钮等宽,滑块与选中项对齐; 按钮文字长度不同也不改变分段宽度。 选中状态由滑块与文字共同表达,未选中项悬停不变成选中色, 避免与真正选中项同时发亮。

实现复用 src/styles-scss/components/_segmented.scss, 由容器的 --seg-n 与 --seg-i 驱动位置。 这两个组件变量是实现接口,不授权把该控件扩展成任意数量的通用分段选择。

5.6 下拉与展开面板

收起触发器和展开面板优先使用同一个连续外壳; 外壳沿实际内容高度伸展,内部选项随后淡入。 展开态新增的部件必须能对应到收起态的同一颗控件,或由外壳内部让出; 不能旧控件淡出、新控件淡入,也不能让面板从与触发器无关的位置淡入。 圆角按 §3.3 保持曲率,宽度变化不能超出视口或遮住必要的相邻操作。

普通选择使用现有 ExpandSelect;博客栏目包含父子层级,使用 BlogCategorySelect。两者共用 expand-panel 的展开节奏。 父栏目名称负责选择,箭头负责展开子项,两种操作保持独立的点击区域。

外部点击、焦点离开和 ESC 可收起;关闭后内容立即退出交互和焦点序列, 内容退场先于外壳收缩,不能先收壳把内容切断。 隐藏内容不能仅设透明度。首次内容测量不播放宽度伸缩,避免进入页面就发生一次假展开。 菜单或字段保留符合其实际语义的键盘操作,不能只实现鼠标点选。

5.7 弹窗、浮层与提示

普通弹窗采用主题卡片底色、30px 圆角与品牌浮层阴影, 默认内容宽度以 500px 为参照,窄屏限制在视口内并保留边距。 内容过长时在合适的内部区域滚动,标题和关闭操作保持可达。 内容分两段到达(先占位、后正文)的弹窗,窄屏把上沿钉死:高度变化只向下长, 盒子不跟着上下跳半个高度差;其余弹窗仍是 margin: auto 居中。

模态使用 §3.6 的遮罩,阻止背景交互和穿透滚动; 焦点进入弹窗并在关闭后回到触发入口。非模态下拉不额外遮暗整页。 浮层受父级裁剪或层叠上下文限制时,由浮层组件负责定位与挂载, 不能通过抬高内部 z-index 猜测解决。

Toast 用于短暂结果反馈,不承载唯一的错误详情或必须完成的操作。 右上角的 Toast 是长胶囊形毛玻璃:背景只铺该条提示状态色的 secondary 淡底(10% 透明), 用 backdrop-filter 模糊背后的页面,不投影。 淡底、文字与关闭叉都取同一条提示的状态色(成功绿 / 错误红 / 提示蓝 / 警告橙), 文字直接用状态色、不压深;背景是透明的,所以它在暗色主题下落在深色页面上。 告警区通过状态色、图标和文字共同表达含义, 使用固定圆角与淡底,不加装饰性左侧竖条。

5.8 加载、空状态与错误

加载尽量维持布局与按钮宽度;刷新同一份列表时保留筛选和当前操作上下文。 空状态说明当前范围与下一步,例如调整筛选或创建内容。 错误说明失败的操作与可行的恢复方式,关键错误不只靠 Toast 短暂出现。

这些状态与正常内容使用同一套容器和间距,不额外套入整页卡片。 服务端与客户端的首屏状态要一致,不能先显示可操作内容再在水合时隐藏。

6. 交互状态与动效

6.1 状态表达

状态覆盖是基线,不是可选项。悬停、按下、聚焦、选中、展开、加载、报错、禁用、 数值变化与增删排序,只要状态变了,就必须有可见且连续的反馈;没有动画是待补, 不是克制。瞬时跳变只允许出现在减少动态效果里,或首屏的测量 / 水合修正上。

状态 视觉与行为要求
悬停 只反馈当前可操作对象,不改变布局
按下 使用按钮档位的颜色反馈,不做垂直位移
键盘聚焦 保留清晰的焦点标识,不能只移除 outline
选中 使用持续的淡底、文字、滑块或开启态光晕,并提供 ARIA 状态
展开 触发器、箭头、外壳与面板保持同一状态
禁用 弱化显示并实际阻止操作
加载 明确正在处理,防止重复提交并保持几何稳定
错误 用文字、颜色与关联关系说明,不能只变红

点赞、投喂、收藏等状态按钮沿用同一递进: 未激活时灰色无底;未激活悬停或已激活空闲时为功能色淡底与同色图标; 已激活悬停及按下时为功能色实底与白色图标。 开关 / 勾选项也走这条递进(评论输入区「匿名」前面那颗圆形勾选钮即此形态), 但勾选态直接取功能色实底 + 白色图标,并额外常驻一层开启态光晕 (--shadow-glow-brand,见 §3.6)——“打勾 = 已选”要一眼看得出来,淡底留给悬停; 光晕是这条状态唯一多出来的一档。 勾选钮里的 ✔ 是状态图标的承载者:颜色跟随上述递进,但不能代替 ARIA 状态; 原生复选框的语义、标签与键盘操作必须保留。 触屏松手后恢复真实激活状态,悬停配方按需要限定为 (hover: hover) and (pointer: fine),不能残留模拟悬停造成假选中。

6.2 动效节奏

节奏是第二层问题。先确认动效属于这个组件:同壳、同源、对位(§6.3),再谈时长。

按用途定义时长,不使用一条 transition: all 统管所有变化。 形变只保留两条节奏:展开 480ms / cubic-bezier(0.16, 1, 0.3, 1), 收起 220ms / cubic-bezier(0.4, 0, 0.2, 1)。伸缩、宽度、位移、旋转、网格展开、 入场与退场都引用这两条;颜色、底色、阴影、透明度的短反馈不并入,仍走 150–250ms 的 ease。 这两条是同一手势内共用的时间线,不是"全站只允许两个动画";颜色反馈、内容淡入与 错峰仍是受约束的从属节奏。覆盖范围见 §6.1,不因这两条节奏而缩小。 以下节奏来自博客目录与现有共享组件:

变化 时长 缓动
按钮文字、底色 250ms ease
主按钮按下压暗 150ms ease
卡片悬停与字段聚焦光晕 200ms ease
胶囊滑块位置 250ms ease
展开:外壳伸缩、宽度、位移、旋转、网格展开、入场 480ms cubic-bezier(0.16, 1, 0.3, 1)
收起:以上各项反向 220ms cubic-bezier(0.4, 0, 0.2, 1)
展开内容淡入淡出 180ms ease

展开内容可延后 55ms 开始,连续选项按 28ms 错开。 这套延迟只用于需要说明顺序的展开,不给所有列表逐条排队入场。

expand-panel 与顶栏移动导航不分两套:展开 480ms、收起 220ms(内容先退场, 空面板随后收回);具体编排由顶栏组件持有。顶栏主题切换按钮的旋转、首页日月旋转与天体换色 也引用上面两条,不另写时长与曲线。新组件先使用公共节奏, 需要不同时间时按操作体量说明理由,不能另列无用途的动画常量。

6.3 展开与收起的编排

  1. 触发后同一外壳开始伸展,箭头同步改变方向;面板从触发器的边界长出, 不另建独立浮层。
  2. 收起态与展开态都存在的部件一一对应:同一颗按钮换标签、同一颗头像位移、 同一个关闭钮换位置;展开态新增的内容只能由外壳内部让出, 不能从壳外飞入或原地淡入。
  3. 一次手势只有一条主几何时间线;内容跟着外壳走,进入可见区域后淡入, 轻微位移与外壳保持相同方向。内容不能先于壳出现(会溢出),也不能明显滞后(会空壳)。
  4. 收起先停止内部操作并退出焦点序列,内容先退场,外壳随后收缩; 不能先收壳把内容切断。
  5. 收完后再隐藏可视面板;不能第一帧就移除 DOM 或 visibility, 让收起动画消失,也不能为了等待动画而继续接收点击。

整个过程裁剪在外壳圆角内,不叠加回弹、弹簧与和部件无关的并行运动。 凭空淡入的面板、旧控件淡出 / 新控件淡入、壳先长而内容迟到、关闭时内容被壳切断, 都属于把动画盖在组件上,不是本节这套编排。

按实际内容高度伸展可复用 expand-panel 的 0fr → 1fr。 同一时间线里还有按进度线性移动的元素时(例如手机悬浮球的头像),面板高度改用显式 height 过渡:浏览器把 fr 内插成「进度²」,叠上线性位移会让合成轨迹折一下。 组件内部变化不能触发整页位移,内容溢出时的裁剪与滚动在过渡前后保持稳定。 连续操作从当前状态续接,不等待上一段播放完再响应。

6.4 首屏与主题切换

服务端兜底尺寸到客户端测量尺寸的修正不播放用户操作动画。 这条只约束"没有用户操作"的首帧修正,不表示首屏不该有交互反馈:用户一旦操作, 该有的过渡照常播放(§6.1)。收起状态、排序与主题尽量在首帧就正确,避免先展开再收起、 先亮后暗。

手动主题切换由 public/static/js/core/base.js 统一协调文字、底色、 阴影与 SVG 颜色,局部组件不另跑一套主题动画。换色时间线是 480ms、 cubic-bezier(0.16, 1, 0.3, 1),与形变的展开档同一条。 首页日月的旋转、透明度与天体填色走同一条 480ms 时间线,曲线也相同; 主题换色的 WAAPI 动画与日月必须同速同曲线,改一处要改两处。 离开视口的内容直接更新到终态,避免大量不可见元素占用动画开销。 全局颜色过渡只由 src/styles-scss/base/_reset.scss 定义, 形变、位移和缩放限定在对应组件,不能扩大到全局 * 选择器。

6.5 减少动态效果

prefers-reduced-motion: reduce 下关闭或压缩形变、滑移、视差与错开入场, 直接显示最终状态;主题直接换色。减少动态效果不是取消状态反馈,而是把过渡压到 0ms, 终态、内容、焦点、隐藏状态与操作能力都不变。 顶栏展开后长用户名的自动左右往返也属于这一类:减少动态效果时停止移动,但那一格 仍是可手动横向滚动的文本框,读到全名的路径不消失。动画结束事件不能成为完成功能的唯一条件。

7. 典型页面的组合方式

7.1 列表与目录

博客目录是主要参照:页面底色上放列表卡片,桌面配个人侧栏, 上方同一工具条容纳搜索、栏目与排序。 卡片通过标题、摘要、作者、统计和日期表达内容层级,日期保持辅助信息的强度, 详细时间通过可访问的详情入口或提示提供。

目录的基准尺寸是:桌面侧栏 280px,筛选控件统一高 42px; ≤992px 搜索独占首行,栏目和排序放第二行。 移动个人入口为 129×70px 胶囊:左内距 13 + 创建钮 44 + 间距 8 + 头像 44 + 右侧留白 20, 创建钮与头像各为 44px,创建圆钮与胶囊左端圆角同心。 展开后复用同一外壳:头像移到面板左上(left 20 / top 24、48×48),创建钮变成 105px 宽的按钮, 圆图标与自身左端圆角同心,文案右侧保留与图标同档的留白。 创建钮是同一颗按钮展开文字,头像与关闭钮也是同一颗元素位移,不是收起态消失、展开态新建。 收起态到展开态,外壳宽度、面板高度与头像的 top/left/宽高必须共用同一条时间线 (展开 480ms、收起 220ms,见 §6.2),合成轨迹才是直线。 面板高度用显式 height 过渡,不用 grid-template-rows 的 0fr → 1fr: fr 会被内插成进度²,而头像按进度线性移动,两者叠加会让头像走折线。 卡片按 §3.3 从桌面 30px 收紧到 ≤768px 的 18px 圆角。

通知页的按日分组沿用同一套列表观感:同一天的通知收在同一组, 组标题用字号、字重与次要色建立层级,组间用卡片间距分开,不画贯通分割线。

这组具体几何由目录组件持有,其他列表复用其关系, 不要求每页都有个人侧栏或悬浮入口。 公开列表 /explore 与站内目录有不同的信息组织, 不能把其旧栏目侧栏当成新的全站基准。

7.2 阅读与读者操作

标题、作者、时间和正文共用清楚的阅读外壳, 避免标题卡、正文卡与渲染器卡片重复套壳。 评论与嵌入内容沿用相同的底色层级,长引用、代码与媒体不撑宽页面。

评论区的楼中楼按层深交替使用卡片底色与卡内内容底色: 每条评论独立成卡,同级评论之间用卡片间距分隔,不使用分割线; 头像、正文与操作行共用同一条左竖线(正文不让出头像列), 某条评论的子回复由操作行右端的小箭头整棵折叠。 顶部主输入框不另套外壳,多行输入面板本身使用弱背景色,与评论卡的卡片底色区分开; 就地回复只保留输入框本体(不出现回复预览条,也不套外框),右上角有一颗取消叉。 输入区底条的「匿名」文字不变,变的是它前面那颗圆形勾选钮(样式与状态见 §6.1): 空闲为中性圆底 + 灰色 ✔,勾选后是品牌实底 + 白色 ✔ 与开启态光晕;「匿名」两个字 不随状态变色,也不套底色。

文章详情的读者操作条保留以下契约:

  • 返回圆钮、互动胶囊、管理胶囊全视口保持同一行, 整排按实际内容宽度居中,外壳均高 48px。
  • 互动底座内距 4px,内部按钮高 40px,只显示图标与同行数字; 点赞和收藏有 aria-pressed,所有操作有可访问名称。
  • 管理项向右展开。≤768px 展开管理时互动胶囊收缩并退出交互, 收起后恢复;空间不足只允许操作区滚动,不能撑宽正文。
  • 极窄屏仍保证最后一项可达,关闭菜单或取消弹窗后焦点回到相应入口。 数字变化平滑伸缩,不能让操作条突然错位。
  • 收藏选择器中名称独占一行,两种创建操作在下一行等宽并排; <360px 才上下堆叠。收藏状态遵循 §3.1 与 §6.1。

这些契约由 tests/e2e/favorite-layout.spec.ts 覆盖, 改布局前先检查其几何断言,不能只看桌面截图。

7.3 表单与管理页面

表单按任务分组,标签、帮助、错误和提交位置保持一致。 主要行动与辅助行动使用既有按钮档位,密集表单通过列数与间距调整密度。

后台共用 AdminShell 的宽工作区和导航,内容组件只承担内容列。 数据列表、筛选条、分页和批量操作复用公共组件规则; 个人主页等自由组合页面保持自己的网格,但卡片表面、按钮和编辑状态遵循同一标准。

7.4 工作台与图表

工作台按任务分配侧栏、主区与操作区,空间不足时折成横条、抽屉或单列, 尽量复用同一份 DOM 和状态。整屏工作台由外壳承担视口高度, 内部各区明确谁滚动,输入区与必要操作不能被固定栏或软键盘盖住。

讨论消息默认采用卡片底色气泡,内距 8px 16px, 圆角为 calc(0.775em + 8px):单行呈胶囊,多行保持相同曲率。 讨论区的结构性边框一律不画:会话侧栏的右分栏线、侧栏头部与脚部横线、 消息主区头部横线、输入区顶线、弹窗分页顶线都删掉,层级只由页面、卡片与内容底色承担。 保留有内容语义的分隔:日期分隔、新消息线、菜单分组线。 聊天气泡(含 data-bubble 用户款式)的边框、底色与圆角仍是气泡自己的外观(§7.5),不随这条改变; 图标淡色底座的圆形规则同样适用于讨论区(§3.3)。 消息操作使用右键或触屏长按菜单;触屏长按 500ms、 移动超过 8px 取消,长按后的点击不重复触发操作。 菜单夹入视口并复用统一浮层皮肤,分组、回复和消息状态由消息组件决定。 实现入口为 src/app/chat/ChatMessageItem.tsx 与 src/app/chat/ChatMessageMenu.tsx。

行情工作台随容器阶梯从三栏收紧为两栏、单列; 自选列表可以从竖列改为横条,具体列宽由 src/styles-scss/pages/_fish-trade.scss 维护。 图表数据图形与界面标签分层:会非等比拉伸的 SVG 只画图形, 坐标文字用同格 HTML 定位,线条保持稳定视觉宽度。 涨跌颜色配合正负号、方向和文字,不能只依赖红绿。

7.5 专用内容与装饰款式

编辑器、媒体查看、图表、原创插画和用户选择的聊天气泡可按内容需要定义专用外观, 但不能修改公共按钮、主题、焦点和页面外壳的规则。

编辑器的编辑边界可以保留自身描边;直角或描边聊天气泡仅作用于选定款式, 普通卡片仍按 §5.3。款式边框改变时从内距中扣除对应尺寸, 保持正文位置与外部几何稳定。头像框按 §3.4,不能改造成 CSS 外环。

Markdown 正文与自由内容有独立的排版规则。新增表情、用户名片等内联引用时, 检查通用正文图片选择器带入的 margin、border、border-radius、background、cursor, 按内联组件用途重置,避免把正文图片的外观套到头像或透明贴图上。 内容引用、匿名与权限行为由服务与组件契约决定,不能因统一样式改变。

8. 实现约定与验收

8.1 样式入口与令牌来源

样式入口为 src/styles-scss/main.scss,由 src/app/layout.tsx 引入并交给 Next.js 编译。 修改 SCSS 源文件即可,开发使用 HMR,正式构建输出到 Next 的产物目录; 不手工提交编译 CSS,也不把入口改成 CSS Module。

目录 职责
abstracts/ 尺度、断点、mixin 与共享计算
base/ reset、字体、主题变量与基础排版
components/ 跨页面组件表面与交互
layout/ 容器、导航、页脚与布局外壳
pages/ 页面组合与专用组件尺寸
utilities/ 小范围通用工具类

颜色、字体和安全区读取 base/_root.scss;SCSS 尺度读取 abstracts/_variables.scss;断点读取 abstracts/_breakpoints.scss。 使用明暗主题 CSS 变量,不启用已经停用的 theme-map / themeify 方案。 原生控件与滚动条随 color-scheme 切换;自定义滚动条复用根样式, 不在局部页面另建亮暗配方。

8.2 复用与样式边界

优先复用已有组件、公共类和 mixin。 相同视觉用于不同 BEM 前缀时使用参数化 mixin, 例如 rich-composer($p)、rich-markdown($cls)、doc-prose($cls); 按钮、字段与展开面板复用 abstracts/_mixins.scss。

页面拥有宽度、页边距与主标题,组件拥有内部结构和状态。 渲染器、弹窗或子组件嵌入页面时不能再次提供同一外壳。 新页面类名限制在自己的根作用域,不能用同名全局选择器修改另一页。

复用前核对实际 DOM、选择器权重与导入顺序。 带 !important 的文字工具类会覆盖组件颜色,不挂到需要表达交互状态的控件上; 不能用增加权重和 !important 层层覆盖代替修正样式归属。

Bootstrap 风格类由本地样式提供,不代表加载了 Bootstrap。 JS 或 JSX 使用的类必须有样式或明确的行为消费者; 刻意无样式的类由 tests/unit/css-tsx-classes.test.ts 的台账维护, 不在本文重复列名单。已应用数据库迁移中的注释属于历史上下文, 不为调整本文章节编号而改写迁移文件。

8.3 可访问性与体验验收

实现完成后检查:

  • 内容与语义:一个页面主标题,标签和错误关联正确; 图标有名称,选中与展开状态可被读屏识别,颜色不是唯一信息。
  • 键盘与焦点:操作可用键盘到达,焦点始终可见; 隐藏面板不可聚焦,ESC 与关闭后的焦点恢复符合组件用途。
  • 主题与输入方式:明暗主题均可读,原生下拉和浮层颜色配对; 触屏没有悬停残留,长文字与数值输入不会破坏布局。
  • 布局:宽屏、窄屏与极窄屏无整页横向溢出; 长标题、长名字、较大数字、空结果和错误状态仍能完整使用。
  • 动效:所有交互状态都有连续反馈,没有瞬时换脸; 首屏的测量 / 水合修正不播放动画(那是无操作修正,不是交互反馈); 连续点击从当前状态续接;减少动态效果时直接切换,功能与焦点不依赖动画结束。
  • 复用影响:共享组件的各使用场景都要验证,不能只检查当前页面。

8.4 检查方式

文档改动运行登记、引用和渲染守卫:

npm test -- tests/unit/docs-catalog.test.ts tests/unit/docs-xref.test.ts tests/unit/docs-render.test.ts tests/unit/guide-docs.test.ts

样式实现按改动范围选择 CSS 类名守卫、组件测试和现有布局 e2e; 样式检查需要编译后的选择器,不能用扫 SCSS 字面量代替。 scripts/compiled-css.mjs 为守卫即时编译; npm run css:probe 只服务离线探针,不属于正常构建流程。

几何与交互变化必须通过实际页面验证。 已有 tests/e2e/blog-directory.spec.ts、 tests/e2e/favorite-layout.spec.ts、tests/e2e/page-gutter.spec.ts 及主题相关用例分别覆盖目录、读者操作、页边距与主题。 e2e 读取现有构建产物;修改源码后先构建,或使用 npm run e2e:ci, 避免把旧产物的结果当成本次验证。

附录 A:实现索引

本表提供源码入口,不复制页面尺寸和已完成记录。查看组件时一并检查其对应测试。

对象 入口
主题、字体、安全区 src/styles-scss/base/_root.scss、src/styles-scss/base/_fonts.scss
尺度与公共配方 src/styles-scss/abstracts/_variables.scss、src/styles-scss/abstracts/_mixins.scss
博客目录卡片与搜索 src/styles-scss/pages/blog/_menu.scss
目录筛选与个人面板 src/styles-scss/pages/blog/_directory.scss、src/app/blog/BlogCategorySelect.tsx、src/app/blog/BlogPersonalSidebar.tsx
字段、分段切换、展开选择 src/styles-scss/components/_form-controls.scss、src/styles-scss/components/_segmented.scss、src/app/components/ExpandSelect.tsx
头像与头像框 src/app/components/Avatar.tsx、src/styles-scss/components/_avatar.scss
顶栏与主题切换 src/styles-scss/layout/_header.scss、public/static/js/core/base.js
阅读卡与互动操作 src/styles-scss/pages/blog/_blog.scss、src/styles-scss/pages/_favorite.scss
评论与讨论输入、正文 src/styles-scss/components/_composer.scss、src/styles-scss/components/_markdown-body.scss
后台与自由网格 src/app/components/AdminShell.tsx、src/styles-scss/pages/admin/_common.scss、src/styles-scss/pages/_profile.scss
代码编辑与文档排版 src/styles-scss/components/_markdown-editor.scss、src/styles-scss/components/_doc-prose.scss

本页内容来自仓库 docs/frontend-styles.md·在 GitHub 上查看

前端样式规范 · 聪明山