feat(design): 加 Ink/Silver 双主题色板 + 规格卡几何与字号令牌(M228)

规格卡 DevDocs/artifacts/components-ink.svg 的令牌化,只动 crates/design,零依赖不变。

- color::boards:BrandBoard(Ink 深 / Silver 浅)→ 49 角色 × 2 套固化色板。
  spec 直写值进 ANCHORS(测试逐条比 hex,规格卡改了代码不改会红);规格卡没给的
  角色按「同色相 + M3 tone 关系」推导后固化,规则写在 boards/ink.rs 的行注释里。
  Scheme::from_board / DesignTokens::brand 是两个新入口;Scheme::baseline 一字未改
  (它是 D38 取色链的中性锚点,改它等于同时改 app-iced 观感 + 取色语义)。
- palette::visibility:容器层可视性裁决,即债务台账「SecondaryContainer 对 Surface
  仅 1.06」的令牌层结论。状态容器(*-container)对 surface ≥1.20:1 是唯一数字门禁;
  容器层之间不设亮度门禁,改为「选中态 = 容器底 + 文字对比 + 形状/图标」三件套的构成
  要求(浅色板实测 1.05~1.24,任何有意义的门禁都过不了,详情见模块文档实测表)。
- inkcat:规格卡的尺寸与形状数据。ControlSize(按钮 XS~XL = 32/40/56/96/136,圆角
  一律半高)、FabSize(40/56/80/160×56,圆角 12/16/20/16)、控件方框(卡片 16、
  对话框 28、snackbar 52@16、列表行 60 药丸、chip 32@8、字段 4/28 + 内距 16/32、
  复选 18@4、单选 18、开关 52×32 + 拇指 24、进度 4/6)、SpecText(规格卡 11 个字号
  → 命中 / 落档 / 低于 11px 下限三类结论)。
  component_map 的表值一处未动:app-iced 的 pages/frame/tests.rs:42 与 shell/tests.rs:30
  按 Medium(12) pin 住卡片,改表等于连带改观感且破坏既有断言;差异在 inkcat 模块文档
  逐条登记,换肤时由 M229 一次切换 + 截图验收。
- 测试:186 lib(+13 条新单测)+ tests/ink_cat_boards.rs(10 条:完整性/锚点/双主题
  切换/26 对 WCAG AA/25 组规格卡用法/状态容器 1.2 门禁/阶梯单调)+ tests/ink_cat_geometry.rs
  (3 条:按钮 FAB 几何、控件圆角、字号校核)。
This commit is contained in:
AskaEth committed 2026-11-10 22:34:05 +08:00
1 parent 7fe38fb240
commit 0169d4c8fb
14 files changed
+2047 -4

No files matched your search

+100
View File
@@ -0,0 +1,100 @@
//! Ink Cat 规格卡的**几何与字级**门禁(M228):按钮 XS~XL、FAB 四态、控件方框、字号校核。
//!
//! 与 `ink_cat_boards.rs` 分开是单文件 ≤400 行的硬约束;两侧都只用公开 API。
use unlucky_design::inkcat::metrics;
use unlucky_design::inkcat::size::{ControlSize, FabSize};
use unlucky_design::inkcat::type_scale::{SpecFit, SpecText, UI_TEXT_FLOOR_PX};
use unlucky_design::shape::ShapeScale;
use unlucky_design::typography::{TypeRole, Weight};
#[test]
fn buttons_and_fabs_hit_the_spec_geometry() {
// 按钮五档:32/40/56/96/136,圆角一律半高(药丸)。
let heights: Vec<f32> = ControlSize::ALL.iter().map(|s| s.height_px()).collect();
assert_eq!(heights, vec![32.0, 40.0, 56.0, 96.0, 136.0]);
for size in ControlSize::ALL {
assert_eq!(
size.radius_px(),
size.height_px() / 2.0,
"{size:?} 该是药丸"
);
}
// M 档就是规格卡 `BUTTONS / 5 LEVELS` 那一行的 56 高。
assert_eq!(ControlSize::M.height_px(), 56.0);
assert_eq!(ControlSize::M.label_px(), 16.0);
// FAB 四态圆角 12/16/20/16(都不是半高,所以不用 Full)。
let radii: Vec<f32> = FabSize::ALL.iter().map(|s| s.radius_px()).collect();
assert_eq!(radii, vec![12.0, 16.0, 20.0, 16.0]);
let sizes: Vec<f32> = FabSize::ALL.iter().map(|s| s.height_px()).collect();
assert_eq!(sizes, vec![40.0, 56.0, 80.0, 56.0]);
}
#[test]
fn component_boxes_hit_the_spec_corners() {
assert_eq!(metrics::CARD.radius_px(), 16.0, "ELEVATED CARD rx=16");
assert_eq!(metrics::DIALOG.radius_px(), 28.0, "对话框 rx=28");
assert_eq!(metrics::SNACKBAR.radius_px(), 16.0, "snackbar rx=16");
assert_eq!(metrics::SNACKBAR.height, 52.0);
assert_eq!(metrics::LIST_ROW.radius_px(), 30.0, "列表行药丸");
assert_eq!(metrics::LIST_ROW.height, 60.0);
assert_eq!(metrics::CHIP.height, 32.0);
assert_eq!(metrics::CHIP.radius_px(), 8.0);
// 输入框:默认圆角 4、药丸变体 28,内距 16 / 32(规格卡专门提醒过)。
assert_eq!(metrics::FIELD_SHAPE, ShapeScale::ExtraSmall);
assert_eq!(metrics::FIELD_SHAPE_ROUNDED, ShapeScale::ExtraLarge);
assert_eq!(metrics::FIELD_HEIGHT, 56.0);
assert_eq!(metrics::FIELD_INSET, 16.0);
assert_eq!(metrics::FIELD_PILL_INSET, 32.0);
// 开关 / 勾选 / 进度。
assert_eq!(
(metrics::SWITCH_TRACK.width, metrics::SWITCH_TRACK.height),
(52.0, 32.0)
);
assert_eq!(metrics::SWITCH_THUMB, 24.0);
assert_eq!(metrics::CHECKBOX.radius_px(), 4.0);
assert_eq!(metrics::PROGRESS_THICKNESS, 4.0);
}
#[test]
fn spec_font_sizes_map_onto_the_official_scale() {
// 命中官方档的六档。
for (text, role) in [
(SpecText::ControlLabelSmall, TypeRole::TitleSmall),
(SpecText::DialogTitle, TypeRole::HeadlineSmall),
(SpecText::DisplayLabel, TypeRole::HeadlineLarge),
(SpecText::Caption, TypeRole::BodySmall),
(SpecText::Badge, TypeRole::LabelSmall),
] {
assert_eq!(text.fit(), SpecFit::Exact(role), "{text:?}");
}
// 落在两档之间的三档:取较大那档,登记 1px 级偏离。
for (text, lower, upper) in [
(SpecText::Body, TypeRole::BodySmall, TypeRole::TitleSmall),
(
SpecText::Subtitle,
TypeRole::TitleSmall,
TypeRole::TitleMedium,
),
(
SpecText::PageTitle,
TypeRole::DisplaySmall,
TypeRole::DisplayMedium,
),
] {
assert_eq!(text.fit(), SpecFit::Between(lower, upper), "{text:?}");
assert_eq!(text.fit().recommended(), Some(upper));
}
// 低于 UI 下限的两档:只能是设计稿标注。
for text in [SpecText::SectionCaps, SpecText::SwatchCaption] {
assert!(!text.in_ui(), "{text:?} 不该进 UI");
assert_eq!(text.fit(), SpecFit::BelowFloor);
assert_eq!(text.fit().recommended(), None);
}
assert_eq!(UI_TEXT_FLOOR_PX, 11.0);
// 按钮标签字重:规格卡是 500(官方 body/headline 是 400,这是按钮语义)。
for size in ControlSize::ALL {
assert_eq!(size.label().weight, Weight::Medium);
}
}