Files
Unlucky-Launcher/crates/design/tests/ink_cat_boards.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

286 lines
8.5 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):色板完整性、双主题切换、对比度。
//!
//! 只用公开 API(`unlucky_design::...`),所以它同时是「iced 适配层能拿到什么」的样本:
//! 换肤时只需要 `BrandBoard` → `Scheme` → `Role`,一个色值字面量都不用抄。
//! 几何与字级校核在 `ink_cat_geometry.rs`,容器层可视性在 `ink_cat_visibility.rs`
//! (拆开是单文件 ≤400 行的硬约束)。
use unlucky_design::color::BrandBoard::{Ink, Silver};
use unlucky_design::color::{BrandBoard, Color, Role, SchemeFlavor};
use unlucky_design::palette::wcag::{check, AA_TEXT};
use unlucky_design::DesignTokens;
// ---------------------------------------------------------------- 色板完整性
#[test]
fn every_board_covers_all_49_roles_and_reports_its_flavor() {
for board in BrandBoard::ALL {
let scheme = board.scheme();
assert_eq!(scheme.flavor(), board.flavor());
assert_eq!(scheme.iter().count(), Role::ALL.len());
for role in Role::ALL {
let color = scheme.get(role);
assert!(
color.is_opaque(),
"{} 的 {} 不是不透明色",
board.name(),
role.name()
);
assert_eq!(color, board.get(role), "两个入口必须同源");
}
}
}
#[test]
fn spec_anchors_survive_the_public_api() {
// 规格卡上写死的色值(两套板各自的一批),一份都不许漂。
let cases: [(BrandBoard, Role, &str); 12] = [
(BrandBoard::Ink, Role::Primary, "#E8FFEF"),
(BrandBoard::Ink, Role::OnPrimary, "#256B4B"),
(BrandBoard::Ink, Role::Surface, "#0F150F"),
(BrandBoard::Ink, Role::SurfaceContainerHighest, "#262B25"),
(BrandBoard::Ink, Role::Error, "#FF716C"),
(BrandBoard::Ink, Role::Outline, "#7B8A7B"),
(BrandBoard::Silver, Role::Primary, "#006499"),
(BrandBoard::Silver, Role::PrimaryContainer, "#87C7FF"),
(BrandBoard::Silver, Role::Surface, "#F9F9FA"),
(BrandBoard::Silver, Role::SecondaryContainer, "#D4E4F6"),
(BrandBoard::Silver, Role::OnSurface, "#1A1C1D"),
(BrandBoard::Silver, Role::Error, "#AC3434"),
];
for (board, role, hex) in cases {
assert_eq!(
board.scheme().get(role),
Color::from_hex(hex).expect("规格卡色值"),
"{} / {} 与规格卡的 {hex} 不符",
board.name(),
role.name()
);
}
}
// ---------------------------------------------------------------- 双主题切换
#[test]
fn follow_system_resolves_to_the_right_board() {
// 「跟随系统」是设置层的第三种态,令牌层只提供映射。
assert_eq!(
BrandBoard::for_flavor(SchemeFlavor::Dark),
BrandBoard::Ink,
"系统深色 → Ink"
);
assert_eq!(
BrandBoard::for_flavor(SchemeFlavor::Light),
BrandBoard::Silver,
"系统浅色 → Silver"
);
for board in BrandBoard::ALL {
assert_eq!(
BrandBoard::for_flavor(board.flavor()),
board,
"映射必须可逆"
);
}
}
#[test]
fn switching_boards_swaps_all_the_surface_roles() {
let ink = BrandBoard::Ink.scheme();
let silver = BrandBoard::Silver.scheme();
// 窗口底 / 正文 / 字段底 / 分层容器:切板后全都得换。
for role in [
Role::Surface,
Role::OnSurface,
Role::SurfaceContainerLow,
Role::SurfaceContainerHighest,
Role::Primary,
Role::SecondaryContainer,
Role::Error,
Role::InverseSurface,
] {
assert_ne!(
ink.get(role),
silver.get(role),
"{} 在两套板之间没变——换肤会露出旧颜色",
role.name()
);
}
// 深浅判定:Ink 是真的深、Silver 是真的浅。
assert!(ink.get(Role::Surface).relative_luminance() < 0.02);
assert!(silver.get(Role::Surface).relative_luminance() > 0.90);
}
#[test]
fn design_tokens_brand_entry_is_the_product_default() {
let ink = DesignTokens::brand(BrandBoard::Ink);
assert_eq!(ink.flavor, SchemeFlavor::Dark);
assert_eq!(ink.color(Role::Surface), BrandBoard::Ink.get(Role::Surface));
// 品牌板与「取色链」两条路径互不干扰:baseline 仍是 tone 派生(D38 的锚点)。
let baseline = DesignTokens::baseline(SchemeFlavor::Dark);
assert_eq!(baseline.flavor, SchemeFlavor::Dark);
assert_ne!(
baseline.color(Role::Surface),
ink.color(Role::Surface),
"baseline 若已变成品牌板,D38 取色链的表面色锚点就没了"
);
}
// ---------------------------------------------------------------- 对比度
#[test]
fn both_boards_pass_the_full_wcag_table() {
for board in BrandBoard::ALL {
let report = check(&board.scheme());
assert!(
report.is_clean(),
"{} 有 {} 项未达 AA:{:?}",
board.name(),
report.failures.len(),
report.failures
);
assert!(
report.worst >= AA_TEXT,
"{} 最紧的一对只有 {:.2}",
board.name(),
report.worst
);
}
}
/// 规格卡里真实出现的「文字压在什么色上」组合(25 组,含 `on-*-container` 全族)。
///
/// 只列规格卡上画出来的用法——不列「有可能这么用」的组合,否则就是在替页面线编需求。
const SPEC_TEXT_ON_SURFACE: [(BrandBoard, Role, Role, &str); 25] = [
(Ink, Role::OnPrimary, Role::Primary, "Filled 按钮"),
(
Ink,
Role::OnSecondaryContainer,
Role::SecondaryContainer,
"Tonal 按钮",
),
(Ink, Role::OnSurface, Role::Surface, "Outlined/Text 按钮"),
(Ink, Role::Primary, Role::Surface, "Text 按钮标签"),
(
Ink,
Role::Primary,
Role::SurfaceContainerLowest,
"Elevated 标签",
),
(
Ink,
Role::OnPrimaryContainer,
Role::PrimaryContainer,
"FAB 图标",
),
(
Ink,
Role::OnSurfaceVariant,
Role::SurfaceContainerHighest,
"字段标签",
),
(
Ink,
Role::OnSurface,
Role::SurfaceContainerHighest,
"字段内容",
),
(
Ink,
Role::Error,
Role::SurfaceContainerHighest,
"错误字段标签",
),
(Ink, Role::Error, Role::Surface, "错误提示"),
(Ink, Role::OnError, Role::Error, "徽标数字"),
(
Ink,
Role::OnSurfaceVariant,
Role::Surface,
"chip 未选 / 次要图标",
),
(
Ink,
Role::InverseOnSurface,
Role::InverseSurface,
"snackbar 文字",
),
(
Ink,
Role::InversePrimary,
Role::InverseSurface,
"snackbar 动作",
),
(
Ink,
Role::OnSurfaceVariant,
Role::SurfaceContainer,
"卡片副文案",
),
(
Ink,
Role::OnSurfaceVariant,
Role::SurfaceContainerHigh,
"对话框正文",
),
(Silver, Role::OnPrimary, Role::Primary, "Filled 按钮"),
(
Silver,
Role::OnSecondaryContainer,
Role::SecondaryContainer,
"Tonal 按钮",
),
(Silver, Role::OnSurface, Role::Surface, "Outlined 按钮"),
(Silver, Role::Primary, Role::Surface, "Text 按钮标签"),
(
Silver,
Role::OnSurfaceVariant,
Role::SurfaceContainerHighest,
"字段标签",
),
(
Silver,
Role::OnSurface,
Role::SurfaceContainerHighest,
"字段内容",
),
(
Silver,
Role::Error,
Role::SurfaceContainerHighest,
"错误字段标签",
),
(
Silver,
Role::OnSurfaceVariant,
Role::SurfaceContainer,
"卡片副文案",
),
(
Silver,
Role::InverseOnSurface,
Role::InverseSurface,
"snackbar 文字",
),
];
#[test]
fn spec_text_pairs_meet_aa() {
let mut failures = Vec::new();
for (board, fg, bg, usage) in SPEC_TEXT_ON_SURFACE {
let ratio = board.scheme().contrast(fg, bg);
if ratio < AA_TEXT {
failures.push(format!(
"{} / {usage}:{} on {} = {ratio:.2}",
board.name(),
fg.name(),
bg.name()
));
}
}
assert!(
failures.is_empty(),
"规格卡用法未达 AA(4.5:1):\n{}",
failures.join("\n")
);
}