Files
Unlucky-Launcher/crates/design/tests/ink_cat_visibility.rs
T
AskaEth b8003177d7 fix(design): M228 复审 r1 三条(字段底角色对齐、四处小疵、产品默认口径)
P2-1 字段底角色不对称(塔裁决方案 a):Silver 的 #dcddde 从 surface-container-high 改判到
surface-container-highest,与 Ink 的 #262b25 同位——component_map 的 TextField 口径就是最高
容器层,单一角色接法才不会一边掉层。Silver 的 high 改取推导值 #e4e4e5(tone 90),阶梯
94.12 → 90.61 → 88.07 保持单调、步长 ≥1.74 L*。实测:Error on 新 highest = 4.686(原按单一
角色接到 high 时的 4.379 失守已消除)、OnSurfaceVariant = 6.383、OnSurface = 12.576。
测试同步:Silver 三条字段用法 pin 从 high 改 highest(与 Ink 对称),并新增
`field_backdrop_is_the_same_role_on_both_boards` 把这个不对称钉住(顺带断言 high 比 highest
更亮、highest 就是规格卡的 #dcddde)。

P2-2 四处小疵:
① inkcat/size.rs 扩展 FAB 起始内距 16 → **28**(规格卡画的数):框 x=292 → 图标盒 320(`+`
   font-size 24 居中于 x=332),28 才是图上的距离;16 是 M3 官方口径,已在 doc 里登记为偏离,
  并说明「要回 M3 的 16 得同时把图标中心挪回 316,那是要出截图的视觉改动」。测试补一条自洽
   断言:292 + 28 + 24/2 = 332。
② inkcat/metrics.rs 的 SWITCH_THUMB_INSET 注释重写(原句断成碎片且读起来像算错):写明
   轨道 y=1060..1092 / 拇指 y=1064..1088 ⇒ 上下各 4、右端 116−112=4,以及行程
   52−24−2×4 = 20 的推导。
③ Silver「状态容器 / 容器层」数字统一成实测口径:boards/mod.rs 与 palette/visibility.rs 都写
   「最低 1.02」(Ink 1.54),并点名最差的一组是 tertiary-container 压 surface-container-high
   = 1.016;visibility 表的 Silver 阶梯步长改写 −1.7 ~ −4.1。
④ inkcat/type_scale.rs 正文清单只列 11 个**文字**字号(去掉 FAB 字形 20/26/34),并注明那三个
   归 FabSize::icon_px、SpecText 没有 20 这一档。

P2-3 产品默认口径统一:palettes.rs 的「两个入口」表与 Palettes::baseline 文档、color/mod.rs
的「出厂默认色」小节、Scheme::baseline 文档统一写明「M228 起出厂外观入口是
DesignTokens::brand,本入口是 D38 取色链中性锚点」,与 lib.rs 同口径;顺带把 BASELINE_SEED /
DEFAULT_SEED / DEFAULT_NEUTRAL_CHROMA 的「产品默认」措辞改成取色链口径。

另:tests/ink_cat_boards.rs 已 400 行贴线,把 4 条容器层可视性测试拆到新文件
tests/ink_cat_visibility.rs(纯搬移 + 补 import);palettes.rs 因新增口径说明到 403 行,按
「合并相邻段落、不删信息」压回 399 行;size.rs 现在 323 行。

五步(design 口径):fmt --all --check=0;check -p unlucky-design --all-targets=0(零 warning);
test -p unlucky-design --all-targets=0(267 passed);clippy -p unlucky-design --all-targets
-- -D warnings=0;400 行扫描只剩 README.md(豁免)、无 <2 字节 .rs。
2026-11-10 22:34:05 +08:00

134 lines
5.4 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Ink Cat 容器层可视性门禁(M228 债务裁决的可测一半):状态容器 1.20 门禁、阶梯单调、
//! 以及「容器层不能当状态指示」的实测钉子。
//!
//! 与 `ink_cat_boards.rs` / `ink_cat_geometry.rs` 分开是单文件 ≤400 行的硬约束。
use unlucky_design::color::{BrandBoard, Color, Role, SchemeFlavor};
use unlucky_design::palette::visibility::{
check_surface_hierarchy, LAYER_ROLES, STATE_CONTAINERS, STATE_CONTAINER_MIN,
};
use unlucky_design::palette::wcag::AA_TEXT;
#[test]
fn state_containers_clear_the_visibility_floor() {
// 债务台账「SecondaryContainer 对 Surface 仅 1.06」的令牌层裁决:状态容器 ≥1.20:1。
for board in BrandBoard::ALL {
let report = check_surface_hierarchy(&board.scheme());
assert!(
report.state_failures().is_empty(),
"{} 的状态容器未达 {STATE_CONTAINER_MIN}:1:{:?}",
board.name(),
report.state_failures()
);
let (role, ratio) = report.worst_state();
assert!(
ratio >= STATE_CONTAINER_MIN,
"{} 最弱的状态容器 {} 只有 {ratio:.3}",
board.name(),
role.name()
);
// 旧 baseline 的现场值 1.057 必须被显著甩开,否则「修了但没修好」。
assert!(
ratio > 1.20,
"{} 只是刚好卡线({ratio:.3}),等于没修",
board.name()
);
}
// 两套板覆盖 4 个状态容器角色,一个不落。
assert_eq!(STATE_CONTAINERS.len(), 4);
}
#[test]
fn surface_container_ladder_is_monotone_with_visible_steps() {
for board in BrandBoard::ALL {
let report = check_surface_hierarchy(&board.scheme());
assert!(
report.layer_violations().is_empty(),
"{} 的容器阶梯有断层或方向错:{:?}",
board.name(),
report.layer_violations()
);
assert!(report.is_clean(), "{} 的容器层体检没过", board.name());
// 方向:深色板越靠上越亮、浅色板越靠上越暗。
let steps = report.layer_steps();
for step in steps {
let expect_positive = board.flavor() == SchemeFlavor::Dark;
assert_eq!(
step.delta_lstar > 0.0,
expect_positive,
"{} 的 {} 阶梯方向错",
board.name(),
step.role.name()
);
}
assert_eq!(LAYER_ROLES.len(), 5);
}
}
#[test]
fn container_layers_are_not_state_indicators() {
// 裁决第 3 条的可测化一半:把「状态容器压在容器层上」的最差值钉住,提醒后来者
// 别把它当唯一指示(浅色板必然贴到 1.02,深色板 1.54)。
let worst_over_layers = |board: BrandBoard| {
let scheme = board.scheme();
STATE_CONTAINERS
.iter()
.flat_map(|role| {
LAYER_ROLES
.iter()
.map(move |layer| (*role, *layer, scheme.contrast(*role, *layer)))
})
.map(|(_, _, ratio)| ratio)
.fold(f32::INFINITY, f32::min)
};
let silver = worst_over_layers(BrandBoard::Silver);
assert!(
(1.0..1.1).contains(&silver),
"Silver 的状态容器对容器层最低 = {silver:.3},与「浅色 pale-on-pale 必然贴近」的登记(1.02)不符"
);
// Ink 相反:深色板的状态容器与容器层拉得开,所以深色可以靠底色差做弱指示。
let ink = worst_over_layers(BrandBoard::Ink);
assert!(
ink >= 1.5,
"Ink 最弱的状态容器对容器层 = {ink:.3},应明显高于浅色板"
);
// 两条一起说明「为什么这不是调色问题而是构成问题」:同一套算法下深色能做的弱指示,
// 浅色做不到——所以门禁不能按容器层设,只能按 surface 设。
assert!(silver < ink);
}
#[test]
fn field_backdrop_is_the_same_role_on_both_boards() {
// P2-1 的回归钉子:字段底在两套板都是 `surface-container-highest`。
// 若把 Silver 的字段底挪回 `surface-container-high`(#E4E4E5),错误标签会从 4.69 掉到 4.38(AA 失守)。
for board in BrandBoard::ALL {
let scheme = board.scheme();
for (fg, usage) in [
(Role::OnSurfaceVariant, "字段标签"),
(Role::OnSurface, "字段内容"),
(Role::Error, "错误字段标签"),
] {
let ratio = scheme.contrast(fg, Role::SurfaceContainerHighest);
assert!(
ratio >= AA_TEXT,
"{} 的{usage}({} on surface-container-highest)= {ratio:.3}",
board.name(),
fg.name()
);
}
}
// 浅色板的 high 必须比 highest 更亮、且是另一个色(阶梯方向 + 不许两档同值)。
let silver = BrandBoard::Silver.scheme();
let high = silver.get(Role::SurfaceContainerHigh);
let highest = silver.get(Role::SurfaceContainerHighest);
assert_ne!(high, highest);
assert!(
high.lstar() > highest.lstar(),
"Silver 的 surface-container-high 应比 highest 更亮(高 {} vs 最高 {})",
high.lstar(),
highest.lstar()
);
// 规格卡给的字段底就是 highest 那个值。
assert_eq!(highest, Color::from_hex("#DCDDDE").expect("规格卡字段底"));
}