Skip to content

Commit 56c14b5

Browse files
committed
feat(ui): L1 原语 Button(四态 + 按下微陷)
hover/active/focus-visible/disabled 四态首次落地:三档 variant × 三档 size、原生 disabled(离 Tab 序)与 busy(aria-disabled,保留焦点)、全仓首个 :focus-visible 焦点环与首条按钮禁用视觉降级。本批不迁移任何调用点(批 4)。
1 parent e5969f0 commit 56c14b5

4 files changed

Lines changed: 501 additions & 0 deletions

File tree

‎app/src/ui/primitives/Button.css‎

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
/*
2+
* Button.css —— 按钮的**四态契约**(批 0-D Task 5;规格 §8.6.1 接缝表第 1 行,本批最有价值的产出之一)。
3+
*
4+
* Why(为什么存在):本仓至今**没有任何按钮原语** —— `<button>` 510 行/121 文件全是各自内联样式、
5+
* `const *Btn*` 样式常量 86 行/60 文件(recon §3.3);而 CSS `:hover` / `:active` 在**活代码里 0 处**
6+
* (6+2 处全在已删的死文件 App.css)、`:focus*` / `:disabled` **各 0**、`aria-disabled` **0**、
7+
* 禁用视觉降级**全站 1 处**(recon §8.2)。用户对动效纲领的答复是「我希望我在对其进行交互时,
8+
* 它是活的」(规格 §8.6.1)⇒ 本文件是从零建立「悬停有反应、按下有回执、焦点看得见、禁用看得出来」
9+
* 这条契约的落点,也是批 4 迁移 121 个文件里按钮的靶子。
10+
*
11+
* 四态与本文件的对应(验收逐条打勾):
12+
* ① hover → `.ed-btn--{primary,secondary,ghost}:hover:not(...)`(三档各自成立)
13+
* ② active 按下微陷 → `.ed-btn:active:not(...)` 的 `translateY(1px)`
14+
* ③ focus-visible → `.ed-btn:focus-visible` 的 2px `--ed-ink-1` 焦点环(全仓首个 `:focus-visible`)
15+
* ④ disabled/busy → `.ed-btn:disabled, .ed-btn[aria-disabled="true"]` 的视觉降级
16+
*
17+
* 边界:
18+
* ① 颜色 / 圆角 / 间距 / 字阶一律 `var(--ed-*)`,**本文件零颜色字面量**(`var()` 的兜底值也不许是色值);
19+
* ② 不得写 `z-index`(层级走 `ui/zIndex.ts` 六档标尺,规格 §4.2①);
20+
* ③ **不得出现危险色(`stamp` 档)**(规格 §4.1「危险色绝不用于按钮」;危险语义由 `ConfirmDialog`
21+
* 的印章标记 + 级联影响清单承载,按钮保持中性 —— 控制方裁决 3)。本文件里连该 token 的**字面
22+
* 名字都不写**(只写「危险色」):Task 14 的反例守卫按「`Button.css` 内计数为 0」判,注释里
23+
* 出现该名字会制造假红;
24+
* ④ 交互反馈一律**单属性 ≤200ms 的 CSS transition**(§8.2 判据),**不写 `@keyframes`**
25+
* (循环环境动效才用 keyframes;按压不是循环)+ 不写 `transition: all`(会把布局属性也拖进动画);
26+
* ⑤ reduced-motion 由 `motion.css` 的**唯一**媒体查询块统一覆盖(`.ed-btn` 已在名单里),
27+
* 本文件不重复写媒体查询(重复会让「一处改对所有地方」失效)。
28+
*/
29+
30+
.ed-btn {
31+
display: inline-flex;
32+
align-items: center;
33+
justify-content: center;
34+
gap: var(--ed-space-8, 8px);
35+
/* 原生 <button> 不继承字体 ⇒ 字族必须显式声明,否则按钮会退回 UA 默认字体 */
36+
font-family: var(--ed-font-ui, "Inter", "Segoe UI Variable", "Microsoft YaHei UI", system-ui, sans-serif);
37+
border: 1px solid transparent;
38+
border-radius: var(--ed-radius-control, 5px);
39+
cursor: pointer;
40+
background: transparent;
41+
/* 盒模型口径:本仓**没有全局 CSS reset**,原语不声明就没人声明。`--block` 的 `width: 100%`
42+
在 content-box 下会连同 padding(24px)与边框(2px)一起溢出容器 26px —— 与 `Surface`
43+
基类同款口径(控制方 2026-09-11 对 `Surface.css` 的同一条裁决,本原语的 `block` 是同一失效面)。
44+
`width: auto` 的元素上 border-box 与 content-box 等价 ⇒ 对非 block 按钮零观感影响。 */
45+
box-sizing: border-box;
46+
/* 接缝(§8.6.1 第 2/3 条):四个属性各自**单属性、无时序、120ms** —— 回执不排队,下一次
47+
hover / 按下直接接管当前过渡(快速连点不会"播完上一条")。 */
48+
transition: background-color var(--ed-dur-micro, 120ms) var(--ed-ease, cubic-bezier(0.2, 0, 0, 1)),
49+
border-color var(--ed-dur-micro, 120ms) var(--ed-ease, cubic-bezier(0.2, 0, 0, 1)),
50+
color var(--ed-dur-micro, 120ms) var(--ed-ease, cubic-bezier(0.2, 0, 0, 1)),
51+
transform var(--ed-dur-micro, 120ms) var(--ed-ease, cubic-bezier(0.2, 0, 0, 1));
52+
}
53+
54+
/* 三档尺寸:字号/行高/字重走字阶 token(`SCALE_TOKENS.typeScaleVars` 的第 4/5/6 档),
55+
内边距是「按钮自己的呼吸」(与 spacing 标尺同值但语义不同,故写字面量) */
56+
.ed-btn--sm { padding: 2px 8px; font-size: var(--ed-type-6-size, 11.5px); line-height: var(--ed-type-6-line, 16px); font-weight: var(--ed-type-6-weight, 500); }
57+
.ed-btn--md { padding: 4px 12px; font-size: var(--ed-type-5-size, 12px); line-height: var(--ed-type-5-line, 18px); font-weight: var(--ed-type-5-weight, 500); }
58+
.ed-btn--lg { padding: 8px 16px; font-size: var(--ed-type-4-size, 13px); line-height: var(--ed-type-4-line, 20px); font-weight: var(--ed-type-4-weight, 400); }
59+
60+
.ed-btn--block { width: 100%; }
61+
62+
/* 三档 variant:只调三件事(底 / 墨 / 边框色)。`ghost` 不画边框 —— 它靠 hover 的底色出现 */
63+
.ed-btn--primary { background: var(--ed-ink-1); color: var(--ed-bg-surface); border-color: var(--ed-ink-1); }
64+
.ed-btn--secondary { background: var(--ed-bg-surface); color: var(--ed-ink-1); border-color: var(--ed-border-strong); }
65+
.ed-btn--ghost { background: transparent; color: var(--ed-ink-2); }
66+
67+
/* ① hover:三档各自成立,且**一律排除不可用态** —— `busy` 不带原生 `disabled`,若不排除
68+
`[aria-disabled="true"]`,一个"点了不会有事发生"的按钮仍会给出"我可点"的回执(假回执)。
69+
注意:原生 `<button disabled>` **仍会匹配 `:hover`**,所以 `:not(:disabled)` 不是多余的。 */
70+
.ed-btn--primary:hover:not(:disabled):not([aria-disabled="true"]) { background: var(--ed-ink-2); border-color: var(--ed-ink-2); }
71+
.ed-btn--secondary:hover:not(:disabled):not([aria-disabled="true"]) { background: var(--ed-bg-sunken); border-color: var(--ed-ink-3); }
72+
.ed-btn--ghost:hover:not(:disabled):not([aria-disabled="true"]) { background: var(--ed-bg-sunken); color: var(--ed-ink-1); }
73+
74+
/* ② active:按下微陷 1px(≪ 规格 §8.4 的 8px 位移上限)。hover 与 active 是两段回执:
75+
「收到了」= 悬停变色,「正在按」= 下沉 1px;松开即回到 hover 值(transition 自动回程)。 */
76+
.ed-btn:active:not(:disabled):not([aria-disabled="true"]) { transform: translateY(1px); }
77+
78+
/* ③ focus-visible:全仓首个焦点环。`--ed-ink-1` 亮档是墨黑、暗档是纸白 ⇒ 自动反相,无需第二条规则。
79+
用 `:focus-visible` 而非 `:focus`:鼠标点击不该留环,键盘 Tab 必须看得见。
80+
outline 不吃 hover 背景 ⇒ 与 hover 同时成立时**焦点环永远可见**(§8.6.1 第 4 条:无障碍优先于「活」)。 */
81+
.ed-btn:focus-visible { outline: 2px solid var(--ed-ink-1); outline-offset: 2px; }
82+
83+
/* ④ disabled / busy:**本批首次让"不可用"有视觉降级**(改造前全站仅 1 处,recon §8.2)。
84+
写成一条规则是刻意的:两种态的**视觉**相同(都不可用),差别在语义与焦点 ——
85+
`disabled` 走原生属性(移出 Tab 序),`busy` 只设 `aria-disabled`(留在 Tab 序、焦点不丢)。
86+
放最后一条:同特异度时它必须赢过上面的 hover / active。 */
87+
.ed-btn:disabled, .ed-btn[aria-disabled="true"] {
88+
cursor: not-allowed;
89+
opacity: .55;
90+
transform: none;
91+
box-shadow: none;
92+
}

0 commit comments

Comments
 (0)