Skip to content

Commit 4a638de

Browse files
committed
docs(web): codify header typography contract
1 parent d2b80ef commit 4a638de

3 files changed

Lines changed: 234 additions & 19 deletions

File tree

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
# Header 设计规范
2+
3+
> **版本:** 1.0
4+
> **日期:** 2026-05-18
5+
> **状态:** Draft
6+
> **作者:** Codex
7+
8+
## 0. 目标
9+
10+
统一当前项目的页面 header、列表/面板 header、弹框 header,收敛到固定共享 contract,禁止页面、功能模块、弹层各自再写一套标题区结构和字号规则。
11+
12+
本规范的核心目标只有两个:
13+
14+
- header 结构统一
15+
- header 排版 token 固定,禁止自定义字号
16+
17+
## 1. 适用范围
18+
19+
本规范适用于普通业务 UI 中承担“标题区 / 顶部信息区”职责的 header:
20+
21+
- 一级页面 header
22+
- 二级页面 header
23+
- panel / 列表 / 侧栏 / 卡片上沿 header
24+
- modal / dialog / sheet 标题区
25+
26+
不纳入本规范的豁免面:
27+
28+
- terminal / code editor / diff / review 等技术面板 header
29+
- 纯品牌展示型 hero
30+
- 不承担页面或容器标题职责的普通工具栏
31+
32+
说明:
33+
34+
- 名字里带 `header` 不等于必须纳入本规范。
35+
- 是否纳入,取决于它是不是“页面或容器的标准标题区”。
36+
37+
## 2. 唯一允许的 Header Contract
38+
39+
业务 UI 只允许使用以下共享 header 入口:
40+
41+
### 2.1 `PageHeader`
42+
43+
用途:
44+
45+
- 一级页面
46+
- 二级页面
47+
- 设置页、内容页、详情页的顶部标题区
48+
49+
当前 contract:
50+
51+
- `level="primary"`:页面级主标题
52+
- `level="secondary"`:二级页 / 常规页标题
53+
- `onBack` / `backLabel`
54+
- `kicker`
55+
- `rightSlot`
56+
57+
排版约束:
58+
59+
- `PageHeader.primary` title 固定使用 `--type-section-title-*`
60+
- `PageHeader.secondary` title 固定使用 `--type-app-title-*`
61+
- `PageHeader` kicker 固定使用 `--type-kicker-*`
62+
- `PageHeader` back 文案固定使用 `--type-label-*`
63+
64+
### 2.2 `MobilePageHeader`
65+
66+
用途:
67+
68+
- 移动端页面 header
69+
- 全屏 mobile sheet 的标准 header
70+
71+
说明:
72+
73+
- `MobilePageHeader``PageHeader` 的移动端包装,不是另一套独立排版系统。
74+
- 它只能调整移动端布局、间距和返回区交互密度,不能自定义 title 字号。
75+
76+
排版约束:
77+
78+
- title 固定使用 `--type-app-title-*`
79+
- back 文案固定使用 `--type-code-inline-*`
80+
81+
### 2.3 `PanelHeader`
82+
83+
用途:
84+
85+
- 列表区块
86+
- workspace panel
87+
- 侧栏 / 卡片 / 面板上沿标题区
88+
89+
当前 contract:
90+
91+
- `title`
92+
- `status`
93+
- `meta`
94+
- `actions`
95+
96+
排版约束:
97+
98+
- title 固定使用 `--type-app-title-*`
99+
- `status` / `meta` 只能使用共享辅助文案语义,不允许局部重写 panel title 字号
100+
101+
### 2.4 `DialogHeader`
102+
103+
用途:
104+
105+
- modal
106+
- dialog
107+
- 标准弹层标题区
108+
109+
说明:
110+
111+
- `DialogHeader` 是弹层 header 的 canonical 入口。
112+
- `ModalHeader` 仅作为兼容命名,不应继续扩展成新规范分支。
113+
114+
推荐结构:
115+
116+
- `dialog-header__leading`
117+
- `dialog-header__icon`
118+
- `dialog-header__copy`
119+
- `dialog-header__description`
120+
- `ModalTitle`
121+
122+
排版约束:
123+
124+
- `ModalTitle` / dialog title 固定使用 `--type-section-title-*`
125+
- `dialog-header__description` 固定使用 `--type-meta-*`
126+
127+
## 3. 允许的差异
128+
129+
允许存在的差异只有以下几类:
130+
131+
- 高度
132+
- padding / gap / 对齐方式
133+
- 是否带返回
134+
- 是否带 kicker
135+
- 是否带 actions / meta / status
136+
- icon 样式和状态色
137+
138+
这些差异必须建立在共享 contract 之上实现,不能通过重新发明一套 header DOM 来实现。
139+
140+
## 4. 明确禁止项
141+
142+
以下做法一律不允许:
143+
144+
- 页面自己新写 `xxx-header` DOM 结构来替代 `PageHeader`
145+
- modal / dialog 自己拼一套标题区而不走 `DialogHeader`
146+
- panel 自己写标题区而不走 `PanelHeader`
147+
- 在页面包装层覆盖 `.page-header__title` / `.panel-header__title` / `.modal-title` 字号
148+
- 在场景样式里新增 header 专属 `font-size` / `line-height` / `font-weight` override
149+
- header 继续直接使用裸写 `px`
150+
- header 继续直接使用旧 `--text-*` 尺度 token
151+
152+
一句话规则:
153+
154+
- 页面可以改 header 的布局,不可以改 header 的排版语义。
155+
156+
## 5. Token 使用规则
157+
158+
header 只允许消费 `--type-*` 语义排版 token,不允许直接消费基础字号 token。
159+
160+
当前批准映射如下:
161+
162+
- `PageHeader.primary` -> `--type-section-title-*`
163+
- `PageHeader.secondary` -> `--type-app-title-*`
164+
- `MobilePageHeader.title` -> `--type-app-title-*`
165+
- `MobilePageHeader.back` -> `--type-code-inline-*`
166+
- `PanelHeader.title` -> `--type-app-title-*`
167+
- `DialogHeader.title` -> `--type-section-title-*`
168+
- `DialogHeader.description` -> `--type-meta-*`
169+
170+
如果未来要调整 header 的视觉层级,只能修改这份映射和共享实现,不能在业务页面就地逃逸。
171+
172+
## 6. 落地原则
173+
174+
后续新增页面或重构旧页面时,按下面规则执行:
175+
176+
1. 先判断这是页面 header、panel header 还是 dialog header。
177+
2. 直接复用共享组件。
178+
3. 如果现有 props 不够,优先扩展共享组件 contract。
179+
4. 只有在确认不属于标准标题区时,才允许不用这套 header contract。
180+
181+
评审口径:
182+
183+
- 看到自定义 header DOM,默认视为问题
184+
- 看到 header 自定义字号,默认视为问题
185+
- 看到 header 使用 `--text-*` 或裸 `px`,默认视为问题
186+
187+
## 7. 当前仓库结论
188+
189+
截至 2026-05-18,这套规范在当前代码中的主路径已经收敛为:
190+
191+
- `PageHeader`
192+
- `MobilePageHeader`
193+
- `PanelHeader`
194+
- `DialogHeader`
195+
196+
同时已确认:
197+
198+
- settings 桌面 header 已回收为共享 `PageHeader`
199+
- dialog description 已回收为 `dialog-header__description`
200+
- header 主路径已与 `develop` 上新的 semantic typography token 对齐
201+
202+
后续如果再出现 header 自定义字号,应视为违反规范而不是可接受的局部例外。

packages/web/src/styles/components.css

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3832,15 +3832,6 @@ body.is-resizing-panels * {
38323832
color: var(--text-secondary);
38333833
}
38343834

3835-
/* Intent-coloured accent at the top of the supervisor dialog */
3836-
.supervisor-dialog .modal-header h3 {
3837-
margin: 0;
3838-
font-size: var(--type-section-title-size);
3839-
line-height: var(--type-section-title-line-height);
3840-
font-weight: var(--type-section-title-weight);
3841-
color: var(--text-primary);
3842-
}
3843-
38443835
.supervisor-dialog-header-icon {
38453836
display: inline-flex;
38463837
align-items: center;
@@ -10063,12 +10054,6 @@ textarea.input {
1006310054
letter-spacing: 0.12em;
1006410055
}
1006510056

10066-
.mobile-sheet--fullscreen .page-header__title {
10067-
font-size: var(--type-app-title-size);
10068-
line-height: var(--type-app-title-line-height);
10069-
font-weight: var(--type-app-title-weight);
10070-
}
10071-
1007210057
.mobile-sheet__action {
1007310058
display: inline-flex;
1007410059
align-items: center;

packages/web/src/styles/components.theme.test.ts

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -744,7 +744,6 @@ describe("components.css theme-sensitive surfaces", () => {
744744
const mobilePageHeaderBack = getLastRuleBlock(".mobile-page-header .page-header__back");
745745
const headerLeading = getLastRuleBlock(".page-header__leading");
746746
const backButton = getLastRuleBlock(".mobile-sheet--fullscreen .page-header__back");
747-
const fullscreenTitle = getLastRuleBlock(".mobile-sheet--fullscreen .page-header__title");
748747
const headerActions = getLastRuleBlock(".page-header__actions");
749748

750749
expect(fullscreenHeader).toContain("padding: 0 var(--sp-3)");
@@ -759,9 +758,7 @@ describe("components.css theme-sensitive surfaces", () => {
759758
expect(headerLeading).toContain("flex: 1");
760759
expect(backButton).toContain("background: transparent");
761760
expect(backButton).not.toContain("border-radius: 999px");
762-
expect(fullscreenTitle).toContain("font-size: var(--type-app-title-size)");
763-
expect(fullscreenTitle).toContain("line-height: var(--type-app-title-line-height)");
764-
expect(fullscreenTitle).toContain("font-weight: var(--type-app-title-weight)");
761+
expect(hasRuleBlock(".mobile-sheet--fullscreen .page-header__title")).toBe(false);
765762
expect(headerActions).toContain("margin-left: auto");
766763
});
767764

@@ -824,6 +821,10 @@ describe("components.css theme-sensitive surfaces", () => {
824821
getRuleBlocksFrom(modalStylesheet, ".dialogHeader").find((block) =>
825822
block.includes("align-items: flex-start")
826823
) ?? "";
824+
const dialogDescription = getLastRuleBlockFrom(
825+
modalStylesheet,
826+
":global(.dialog-header__description)"
827+
);
827828
const dialogIcon = getLastRuleBlock(".supervisor-dialog-header-icon");
828829
const editTone = getLastRuleBlock(".supervisor-dialog--edit .supervisor-dialog-header-icon");
829830
const disableTone = getLastRuleBlock(
@@ -833,6 +834,9 @@ describe("components.css theme-sensitive surfaces", () => {
833834
expect(modalTitle).toContain("font-size: var(--type-section-title-size)");
834835
expect(modalTitle).toContain("line-height: var(--type-section-title-line-height)");
835836
expect(modalTitle).toContain("font-weight: var(--type-section-title-weight)");
837+
expect(dialogDescription).toContain("font-size: var(--type-meta-size)");
838+
expect(dialogDescription).toContain("line-height: var(--type-meta-line-height)");
839+
expect(dialogDescription).toContain("font-weight: var(--type-meta-weight)");
836840
expect(dialogHeader).toContain("align-items: flex-start");
837841
expect(dialogIcon).toContain("width: 28px");
838842
expect(dialogIcon).toContain("height: 28px");
@@ -842,6 +846,30 @@ describe("components.css theme-sensitive surfaces", () => {
842846
expect(disableTone).toContain("var(--icon-error)");
843847
expect(hasRuleBlock(".supervisor-dialog-header")).toBe(false);
844848
expect(hasRuleBlock(".supervisor-dialog-subtitle")).toBe(false);
849+
expect(hasRuleBlock(".supervisor-dialog .modal-header h3")).toBe(false);
850+
});
851+
852+
it("does not allow page or modal wrappers to override approved header typography tokens", () => {
853+
const settingsBack = getLastRuleBlock(
854+
".settings-header .mobile-page-header .page-header__back"
855+
);
856+
const mobileSettingsBack = getLastRuleBlock(
857+
".settings-page--mobile > .settings-header .mobile-page-header .page-header__back"
858+
);
859+
860+
expect(hasRuleBlock(".mobile-sheet--fullscreen .page-header__title")).toBe(false);
861+
expect(hasRuleBlock(".settings-header .mobile-page-header .page-header__title")).toBe(false);
862+
expect(
863+
hasRuleBlock(
864+
".settings-page--mobile > .settings-header .mobile-page-header .page-header__title"
865+
)
866+
).toBe(false);
867+
expect(settingsBack).not.toContain("font-size:");
868+
expect(settingsBack).not.toContain("line-height:");
869+
expect(settingsBack).not.toContain("font-weight:");
870+
expect(mobileSettingsBack).not.toContain("font-size:");
871+
expect(mobileSettingsBack).not.toContain("line-height:");
872+
expect(mobileSettingsBack).not.toContain("font-weight:");
845873
});
846874

847875
it("uses a unified inline sheet treatment for mobile selectors and keeps topbar controls height-aligned", () => {

0 commit comments

Comments
 (0)