|
| 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