feat(design): M69 令牌补齐 —— 圆角分层 / 组件微度量 / 骨架值与一像素线
G13(D32 分层):新增 shape::ComponentRadius(白名单只有 Micro=2)、 MICRO_CONTROL_MAX_SIZE=24 与纯函数 radius_layer_for();微半径走 radius_px(size) -> Option<f32>,尺寸 >24dp 一律 None,靠签名而不是自觉拦。 ShapeScale 与 rank() 一个字未动,2 不进刻度。 G11:LayoutMetrics 加 search_bar_height=56 / nav_rail_width=88 / nav_drawer_width=240 / divider_thickness=1;删除语义相反的 nav_expanded_width=88 / nav_collapsed_width=48(D33:展开 drawer 240 / 折叠 rail 88;值 88 的后继是新名字 nav_rail_width,48 无后继)。 G10:新增 component_map::ComponentMetrics(12 字段 + 4 个命名常量 + indicator_width),入口 Component::metrics();框内几何与 LayoutMetrics 的 骨架值分工写进模块文档,骨架级尺寸在 CM 里留 0 不许重复。 G9:Component 末尾追加 Checkbox / Radio / Search / NavigationDrawer / Divider(20 -> 25),同步 table.rs 槽位、ALL 与 name();ComponentSpec 新增 micro_radius(Some 时 shape 必须是占位 None,由构造器 + 测试钉死)。 G12:新增 spacing::stroke::HAIRLINE = 1.0(crate 根再导出为 unlucky_design::stroke),divider_thickness 直接引用它。 其余:lib.rs 的「OKLCh 近似」文案改回真实口径(真 HCT / ColorSpec2021); component_map 单测拆到 tests.rs 以免 mod.rs 超 400 行;测试 145 -> 165。
This commit is contained in:
1 parent
b89cc91b78
commit
141e9b12c7
9 files changed
+1066
-145
No files matched your search
@@ -182,7 +182,8 @@ fn spacing_and_layout_metrics_are_shared() {
|
||||
|
||||
#[test]
|
||||
fn component_map_covers_the_page_skeleton() {
|
||||
assert_eq!(Component::ALL.len(), 20);
|
||||
// M69(G9)在末尾追加 Checkbox/Radio/Search/NavigationDrawer/Divider:20 → 25。
|
||||
assert_eq!(Component::ALL.len(), 25);
|
||||
for component in Component::ALL {
|
||||
let spec = component.spec();
|
||||
assert_eq!(spec.component, component);
|
||||
@@ -191,6 +192,9 @@ fn component_map_covers_the_page_skeleton() {
|
||||
assert_eq!(Component::PrimaryButton.spec().shape, ShapeScale::Full);
|
||||
// 浮层必须高于卡片层。
|
||||
assert!(Component::Dialog.spec().elevation > Component::OutlinedCard.spec().elevation);
|
||||
// 末尾追加的 5 个不许改动既有次序。
|
||||
assert_eq!(Component::ALL[0], Component::TopAppBar);
|
||||
assert_eq!(Component::ALL[19], Component::EmptyState);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -0,0 +1,287 @@
|
||||
//! M69(TOKEXT)的数值钉死:G9~G13 五项缺口逐条关门。
|
||||
//!
|
||||
//! 期望值全部**独立于实现重写**(照 `research/15` §18~§23 与 `00-decision-log.md` D32/D33 抄),
|
||||
//! 实现改一个数、这里不改就红。覆盖:
|
||||
//! - **G13** `ComponentRadius` 白名单 + 尺寸闸(2 不在 `ShapeScale` 里,刻度不许被加宽);
|
||||
//! - **G11** `LayoutMetrics` 的 4 个新骨架值 + nav 语义修正;
|
||||
//! - **G10** `ComponentMetrics` 逐字段;
|
||||
//! - **G9** `Component` 25 个变体与档位表一行对一行;
|
||||
//! - **G12** 一像素线单一真源。
|
||||
|
||||
use unlucky_design::component_map::{Component, ComponentMetrics};
|
||||
use unlucky_design::elevation::Elevation;
|
||||
use unlucky_design::icons::IconSize;
|
||||
use unlucky_design::shape::{radius_layer_for, ComponentRadius, RadiusLayer, ShapeScale};
|
||||
use unlucky_design::spacing::{stroke, LayoutMetrics, Space};
|
||||
use unlucky_design::typography::TypeRole;
|
||||
|
||||
// ---------- G13:圆角分层(D32) ----------
|
||||
|
||||
#[test]
|
||||
fn shape_scale_is_unchanged_and_never_contains_two() {
|
||||
// 刻度一个字都不许动(D32 第 1 条:ShapeScale 保持严格)。
|
||||
let ladder: [(ShapeScale, f32); 9] = [
|
||||
(ShapeScale::None, 0.0),
|
||||
(ShapeScale::ExtraSmall, 4.0),
|
||||
(ShapeScale::Small, 8.0),
|
||||
(ShapeScale::Medium, 12.0),
|
||||
(ShapeScale::Large, 16.0),
|
||||
(ShapeScale::LargeIncreased, 20.0),
|
||||
(ShapeScale::ExtraLarge, 28.0),
|
||||
(ShapeScale::ExtraLargeIncreased, 32.0),
|
||||
(ShapeScale::ExtraExtraLarge, 48.0),
|
||||
];
|
||||
for (scale, dp) in ladder {
|
||||
assert_eq!(scale.dp(), dp, "{scale:?} 的刻度被改动了");
|
||||
// 2 是官方 Checkbox 的圆角,但它**不在**刻度里——加进来就等于废掉 K1 守门。
|
||||
assert_ne!(dp, 2.0, "2dp 不许进 ShapeScale(走 ComponentRadius)");
|
||||
}
|
||||
assert_eq!(ShapeScale::Full.radius_px(64.0), 32.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn component_radius_is_a_one_item_whitelist() {
|
||||
assert_eq!(ComponentRadius::ALL, [ComponentRadius::Micro]);
|
||||
assert_eq!(
|
||||
ComponentRadius::Micro.dp(),
|
||||
2.0,
|
||||
"官方 Checkbox RadiusSize = 2.dp"
|
||||
);
|
||||
// 尺寸闸:>24dp 一律拿不到微半径(D32 的「只允许 ≤24dp 微控件」)。
|
||||
assert_eq!(ComponentRadius::Micro.radius_px(20.0), Some(2.0));
|
||||
assert_eq!(ComponentRadius::Micro.radius_px(24.0), Some(2.0));
|
||||
assert_eq!(ComponentRadius::Micro.radius_px(25.0), None);
|
||||
assert_eq!(ComponentRadius::Micro.radius_px(240.0), None);
|
||||
assert_eq!(ComponentRadius::Micro.radius_px(0.0), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn radius_layer_for_routes_by_size() {
|
||||
assert_eq!(radius_layer_for(20.0), RadiusLayer::Component);
|
||||
assert_eq!(radius_layer_for(24.0), RadiusLayer::Component);
|
||||
assert_eq!(radius_layer_for(28.0), RadiusLayer::Scale);
|
||||
assert_eq!(radius_layer_for(360.0), RadiusLayer::Scale);
|
||||
// 抽屉/面板这类容器永远不该被判成微控件。
|
||||
for container in [
|
||||
LayoutMetrics::DESKTOP.nav_drawer_width,
|
||||
LayoutMetrics::DESKTOP.ai_panel_width,
|
||||
] {
|
||||
assert_eq!(radius_layer_for(container), RadiusLayer::Scale);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- G11:骨架值 + nav 语义修正 ----------
|
||||
|
||||
#[test]
|
||||
fn layout_metrics_new_skeleton_values_are_pinned() {
|
||||
let m = LayoutMetrics::DESKTOP;
|
||||
// A 级:SearchBarTokens.ContainerHeight = 56。
|
||||
assert_eq!(m.search_bar_height, 56.0);
|
||||
// A 级:NavigationDrawer 下限 240(官方 ContainerWidth = 360,1180 窗下过宽,D33 取 240)。
|
||||
assert_eq!(m.nav_drawer_width, 240.0);
|
||||
// 12b §5.1 登记偏离:官方 rail 96 → 本项目 88。
|
||||
assert_eq!(m.nav_rail_width, 88.0);
|
||||
// A 级:DividerTokens.Thickness = 1.0dp(一像素线,不受 4px 栅格约束)。
|
||||
assert_eq!(m.divider_thickness, 1.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nav_widths_mean_what_their_names_say() {
|
||||
// D33:展开(drawer)比折叠(rail)宽;旧字段 nav_expanded_width=88 / nav_collapsed_width=48
|
||||
// 的名字与值相反,已在 M69 改名并删除,这里钉住「新名字不会再次反向」。
|
||||
let m = LayoutMetrics::DESKTOP;
|
||||
assert!(
|
||||
m.nav_drawer_width > m.nav_rail_width,
|
||||
"展开态(drawer {})必须比折叠态(rail {})宽",
|
||||
m.nav_drawer_width,
|
||||
m.nav_rail_width
|
||||
);
|
||||
// rail 里 56 宽的选中指示器要放得下(88 − 2×16)。
|
||||
assert_eq!(m.nav_rail_width - Space::X4.dp() * 2.0, 56.0);
|
||||
// drawer 里 216 宽的指示器同样由「宽 − 2×12」推出(见 ComponentMetrics::indicator_width)。
|
||||
assert_eq!(
|
||||
ComponentMetrics::NAVIGATION_DRAWER.indicator_width(m.nav_drawer_width),
|
||||
216.0
|
||||
);
|
||||
}
|
||||
|
||||
// ---------- G10:组件级微度量 ----------
|
||||
|
||||
#[test]
|
||||
fn checkbox_metrics_are_pinned() {
|
||||
let cm = ComponentMetrics::CHECKBOX;
|
||||
// 20×20(不是 token 文件里那个死值 18,见 research/15 §26.2 矛盾 ①)。
|
||||
assert_eq!((cm.width, cm.height), (20.0, 20.0));
|
||||
assert_eq!(
|
||||
cm.radius,
|
||||
Some(ComponentRadius::Micro),
|
||||
"圆角 2dp 走 D32 第二层"
|
||||
);
|
||||
assert_eq!(cm.stroke, 2.0, "CheckboxTokens.UnselectedOutlineWidth = 2");
|
||||
assert_eq!(cm.state_layer, 40.0, "StateLayerSize = 40");
|
||||
assert_eq!(cm.inner_dot, 0.0);
|
||||
assert_eq!(cm.gap, Space::X2.dp(), "框 ↔ 标签 8(团队口径)");
|
||||
assert_eq!(
|
||||
Component::Checkbox.metrics(),
|
||||
Some(ComponentMetrics::CHECKBOX)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn radio_metrics_are_pinned() {
|
||||
let cm = ComponentMetrics::RADIO;
|
||||
assert_eq!(
|
||||
(cm.width, cm.height),
|
||||
(20.0, 20.0),
|
||||
"RadioButtonTokens.IconSize = 20"
|
||||
);
|
||||
assert_eq!(cm.stroke, 2.0, "RadioStrokeWidth = 2.dp");
|
||||
// 内点 10 不是 12:12 是含描边前的名义值(绘制半径 = 6 − 1 = 5)。
|
||||
assert_eq!(cm.inner_dot, 10.0);
|
||||
assert_eq!(cm.state_layer, 40.0);
|
||||
assert_eq!(cm.radius, None, "radio 是真圆,圆角走 ShapeScale::Full");
|
||||
assert_eq!(Component::Radio.spec().shape, ShapeScale::Full);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn search_bar_metrics_are_pinned() {
|
||||
let cm = ComponentMetrics::SEARCH_BAR;
|
||||
// 高 56 属骨架级:CM 里必须留 0,不许重复一份。
|
||||
assert_eq!((cm.width, cm.height), (0.0, 0.0));
|
||||
assert_eq!(cm.icon, IconSize::Toolbar.px(), "leading/trailing 图标 24");
|
||||
assert_eq!(cm.icon_offset, 4.0, "SearchBarIconOffsetX = 4");
|
||||
// 可视左缘 16 = 文本框默认 12 + 偏移 4。
|
||||
assert_eq!(cm.pad_start, Space::X4.dp());
|
||||
assert_eq!(cm.pad_end, Space::X4.dp());
|
||||
// 搜索条圆角 = 高/2 = 28,等于官方 SearchBarCornerRadius。
|
||||
assert_eq!(
|
||||
ShapeScale::Full.radius_px(LayoutMetrics::DESKTOP.search_bar_height),
|
||||
28.0
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn navigation_drawer_metrics_are_pinned() {
|
||||
let cm = ComponentMetrics::NAVIGATION_DRAWER;
|
||||
assert_eq!((cm.width, cm.height), (0.0, 0.0), "宽/项高属骨架级");
|
||||
assert_eq!(
|
||||
cm.inset, 12.0,
|
||||
"NavigationDrawerItemDefaults.ItemPadding = 12"
|
||||
);
|
||||
assert_eq!(cm.pad_start, Space::X4.dp(), "项内 start 16");
|
||||
assert_eq!(cm.pad_end, Space::X6.dp(), "项内 end 24");
|
||||
assert_eq!(cm.icon, IconSize::Toolbar.px());
|
||||
assert_eq!(cm.gap, Space::X3.dp(), "图标 ↔ label 12");
|
||||
// 指示器宽 = 容器宽 − 24;官方 360 − 24 = 336,本项目 240 − 24 = 216。
|
||||
assert_eq!(cm.indicator_width(360.0), 336.0);
|
||||
assert_eq!(cm.indicator_width(240.0), 216.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn component_metrics_never_duplicate_layout_metrics() {
|
||||
// 同一量只许一份:CM 里出现的骨架级尺寸必须留 0。
|
||||
let m = LayoutMetrics::DESKTOP;
|
||||
for component in [Component::Search, Component::NavigationDrawer] {
|
||||
let cm = component.metrics().expect("这两个控件必须有 CM");
|
||||
assert_eq!(cm.width, 0.0, "{} 的宽度属 LM", component.name());
|
||||
assert_eq!(cm.height, 0.0, "{} 的高度属 LM", component.name());
|
||||
assert_ne!(cm.height, m.search_bar_height);
|
||||
assert_ne!(cm.width, m.nav_drawer_width);
|
||||
}
|
||||
// Divider 的厚度也是 LM(一像素线)→ 它没有 CM。
|
||||
assert_eq!(Component::Divider.metrics(), None);
|
||||
assert_eq!(m.divider_thickness, stroke::HAIRLINE);
|
||||
}
|
||||
|
||||
// ---------- G12:一像素线 ----------
|
||||
|
||||
#[test]
|
||||
fn hairline_is_the_single_source_of_one_pixel_lines() {
|
||||
assert_eq!(stroke::HAIRLINE, 1.0);
|
||||
// 分隔线厚度直接引用它,不是另写一个字面量。
|
||||
assert_eq!(LayoutMetrics::DESKTOP.divider_thickness, stroke::HAIRLINE);
|
||||
// 一像素线不受 4px 栅格约束(栅格测试里显式豁免,这里反向钉一次)。
|
||||
assert_ne!(stroke::HAIRLINE % unlucky_design::spacing::GRID, 0.0);
|
||||
}
|
||||
|
||||
// ---------- G9:枚举 25 变体 ↔ 表槽位 ----------
|
||||
|
||||
#[test]
|
||||
fn component_enum_is_the_frozen_twenty_five() {
|
||||
let names = [
|
||||
"top-app-bar",
|
||||
"navigation-item",
|
||||
"primary-button",
|
||||
"secondary-button",
|
||||
"text-button",
|
||||
"icon-button",
|
||||
"outlined-card",
|
||||
"filled-card",
|
||||
"list-item",
|
||||
"text-field",
|
||||
"segmented-button",
|
||||
"chip",
|
||||
"switch",
|
||||
"slider",
|
||||
"progress-indicator",
|
||||
"dialog",
|
||||
"menu",
|
||||
"snackbar",
|
||||
"tooltip",
|
||||
"empty-state",
|
||||
// M69 末尾追加的 5 个
|
||||
"checkbox",
|
||||
"radio",
|
||||
"search",
|
||||
"navigation-drawer",
|
||||
"divider",
|
||||
];
|
||||
assert_eq!(Component::ALL.len(), names.len(), "M66 的 G9:20 → 25");
|
||||
for (i, component) in Component::ALL.iter().enumerate() {
|
||||
assert_eq!(component.name(), names[i], "第 {i} 个组件被换位/改名了");
|
||||
// 表槽位与枚举判别值一一对应(`spec()` 就是按下标取的)。
|
||||
assert_eq!(*component as usize, i);
|
||||
assert_eq!(component.spec().component, *component);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_components_carry_the_official_elevation_and_shape() {
|
||||
// Checkbox / Radio / Search / NavigationDrawer / Divider:全部 L0(都不是浮层)。
|
||||
for component in [
|
||||
Component::Checkbox,
|
||||
Component::Radio,
|
||||
Component::Search,
|
||||
Component::NavigationDrawer,
|
||||
Component::Divider,
|
||||
] {
|
||||
assert_eq!(
|
||||
component.spec().elevation,
|
||||
Elevation::Level0,
|
||||
"{} 不该有阴影(R4:只有浮层允许)",
|
||||
component.name()
|
||||
);
|
||||
}
|
||||
// Checkbox 的圆角真源是 micro_radius,shape 只是占位 None。
|
||||
let checkbox = Component::Checkbox.spec();
|
||||
assert_eq!(checkbox.micro_radius, Some(ComponentRadius::Micro));
|
||||
assert_eq!(checkbox.shape, ShapeScale::None);
|
||||
// 其余 4 个不碰微半径层。
|
||||
for component in [
|
||||
Component::Radio,
|
||||
Component::Search,
|
||||
Component::NavigationDrawer,
|
||||
Component::Divider,
|
||||
] {
|
||||
assert_eq!(component.spec().micro_radius, None, "{}", component.name());
|
||||
}
|
||||
// NavigationDrawer = CornerLargeEnd 的内侧角 16;Divider 无圆角。
|
||||
assert_eq!(Component::NavigationDrawer.spec().shape, ShapeScale::Large);
|
||||
assert_eq!(Component::Divider.spec().shape, ShapeScale::None);
|
||||
// Checkbox / Radio 的标签字级是团队口径(官方无「框 + 标签」token)。
|
||||
assert_eq!(Component::Checkbox.spec().text, TypeRole::BodyLarge);
|
||||
assert_eq!(Component::Radio.spec().text, TypeRole::BodyLarge);
|
||||
// Search 的字级:输入 BodyLarge、占位/辅助 BodySmall。
|
||||
assert_eq!(Component::Search.spec().text, TypeRole::BodyLarge);
|
||||
assert_eq!(Component::Search.spec().support, Some(TypeRole::BodySmall));
|
||||
}
|
||||
Reference in new issue
Block a user