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
@@ -8,16 +8,24 @@
|
||||
//! - 形状档位见 [`crate::shape::ShapeScale`],高程见 [`crate::elevation::Elevation`];
|
||||
//! - `text` 是主要文字角色,`support` 是辅助文字(没有就 `None`);
|
||||
//! - `motion` + `duration` 是状态过渡(hover/press/展开),不是页面切换;
|
||||
//! - `morph` 是按下/选中时的形状形变(Expressive 特征),不需要就 `None`。
|
||||
//! - `morph` 是按下/选中时的形状形变(Expressive 特征),不需要就 `None`;
|
||||
//! - 圆角分两层(D32):容器级查 [`crate::shape::ShapeScale`],≤24dp 的微控件查
|
||||
//! [`crate::shape::ComponentRadius`](落在 [`ComponentSpec::micro_radius`])。
|
||||
//!
|
||||
//! 档位(形状/高程/字级/动效)之外,**控件内部的微几何**(20×20、描边 2、状态层 Ø40 …)在
|
||||
//! [`ComponentMetrics`] 里,入口是 [`Component::metrics`]——两者分工见 [`metrics`] 模块文档。
|
||||
//!
|
||||
//! **改这里的值 = 改全站观感**,必须同步 `DevDocs/SubAgentsTalk/design-令牌约定.md`;
|
||||
//! 页面线不许在页面里覆盖这些档位。
|
||||
|
||||
mod metrics;
|
||||
mod table;
|
||||
|
||||
pub use metrics::ComponentMetrics;
|
||||
|
||||
use crate::elevation::Elevation;
|
||||
use crate::motion::{DurationToken, Easing};
|
||||
use crate::shape::{ShapeMorph, ShapeScale};
|
||||
use crate::shape::{ComponentRadius, ShapeMorph, ShapeScale};
|
||||
use crate::typography::TypeRole;
|
||||
|
||||
/// 组件的排版 + 形状 + 动效档位。
|
||||
@@ -25,8 +33,14 @@ use crate::typography::TypeRole;
|
||||
pub struct ComponentSpec {
|
||||
/// 该档位属于哪个组件(表自描述,便于测试与文档生成)。
|
||||
pub component: Component,
|
||||
/// 圆角档位。
|
||||
/// 圆角档位(容器级,[`ShapeScale`] 的严格刻度)。
|
||||
pub shape: ShapeScale,
|
||||
/// 微控件圆角(D32 第二层)。
|
||||
///
|
||||
/// `Some` 时**本字段才是半径真源**,此时 [`ComponentSpec::shape`] 必须是
|
||||
/// [`ShapeScale::None`](占位、不参与渲染)——因为 2dp 不在 `ShapeScale` 刻度里,
|
||||
/// 硬塞进 `shape` 会破坏「圆角只取刻度值」。这层配对由测试钉死。
|
||||
pub micro_radius: Option<ComponentRadius>,
|
||||
/// 高程级别(决定 surface container 角色与是否画阴影)。
|
||||
pub elevation: Elevation,
|
||||
/// 主要文字角色。
|
||||
@@ -41,7 +55,10 @@ pub struct ComponentSpec {
|
||||
pub morph: Option<ShapeMorph>,
|
||||
}
|
||||
|
||||
/// 需要固定档位的组件清单(M53 页面骨架里实际用到的那些)。
|
||||
/// 需要固定档位的组件清单(M53 页面骨架 + M69 组件线补的 5 个)。
|
||||
///
|
||||
/// **冻结为只可末尾追加**:判别值当索引用(`table::SPECS[self as usize]`),改名/换位等于
|
||||
/// 让整张表错行。
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum Component {
|
||||
/// 顶部标题栏(第一层 chrome)。
|
||||
@@ -84,11 +101,35 @@ pub enum Component {
|
||||
Tooltip,
|
||||
/// 空态(图标 + 标题 + 说明 + 标准按钮)。
|
||||
EmptyState,
|
||||
/// 复选框(安装选项 / mod 启用 / 日志过滤 / Java 管理勾选)。
|
||||
///
|
||||
/// 圆角 **2dp** 不在 `ShapeScale` 刻度里 → 走 [`ComponentSpec::micro_radius`](D32 第二层);
|
||||
/// 20×20 / 描边 2 / 状态层 Ø40 见 [`ComponentMetrics::CHECKBOX`]。
|
||||
Checkbox,
|
||||
/// 单选框(Java 版本单选 / 安装模式选择)。
|
||||
///
|
||||
/// 真圆(`ShapeScale::Full`);20 / 环线 2 / 内点 10 / 状态层 Ø40 见 [`ComponentMetrics::RADIO`]。
|
||||
Radio,
|
||||
/// 搜索条(实例 / 版本 / mod 搜索,页级入口)。
|
||||
///
|
||||
/// 高 56 是骨架级(`LayoutMetrics::search_bar_height`),圆角 `Full`(56 → 28);
|
||||
/// 图标 24 + 偏移 4 见 [`ComponentMetrics::SEARCH_BAR`]。高程取 **L0**(源码矛盾已登记)。
|
||||
Search,
|
||||
/// 导航抽屉(左侧常驻导航的展开形态,D33)。
|
||||
///
|
||||
/// 形状 `CornerLargeEnd`(只有靠内容那侧两角 16),宽 240 = `LayoutMetrics::nav_drawer_width`;
|
||||
/// 项内几何见 [`ComponentMetrics::NAVIGATION_DRAWER`]。
|
||||
NavigationDrawer,
|
||||
/// 分隔线(列表行分隔 / 区块分隔)。
|
||||
///
|
||||
/// **无高程、无圆角**;厚度是一像素线(`LayoutMetrics::divider_thickness` = `stroke::HAIRLINE`),
|
||||
/// 颜色 `OutlineVariant`(列表内)或 `Outline`(search view 头部分隔,见 §20.8)。
|
||||
Divider,
|
||||
}
|
||||
|
||||
impl Component {
|
||||
/// 全部组件。
|
||||
pub const ALL: [Component; 20] = [
|
||||
pub const ALL: [Component; 25] = [
|
||||
Component::TopAppBar,
|
||||
Component::NavigationItem,
|
||||
Component::PrimaryButton,
|
||||
@@ -109,6 +150,12 @@ impl Component {
|
||||
Component::Snackbar,
|
||||
Component::Tooltip,
|
||||
Component::EmptyState,
|
||||
// M69 G9:末尾追加的 5 个(前 20 个次序不许动)。
|
||||
Component::Checkbox,
|
||||
Component::Radio,
|
||||
Component::Search,
|
||||
Component::NavigationDrawer,
|
||||
Component::Divider,
|
||||
];
|
||||
|
||||
/// 该组件的档位。
|
||||
@@ -116,6 +163,14 @@ impl Component {
|
||||
table::SPECS[self as usize]
|
||||
}
|
||||
|
||||
/// 该组件的**控件内部**微度量;`None` = 只有骨架值(去 `LayoutMetrics` 拿)。
|
||||
///
|
||||
/// 这一条是 G10 的消费入口:组件线拿到 `Component::Checkbox.metrics()` 就有 20×20 / 圆角 2 /
|
||||
/// 描边 2 / 状态层 Ø40,**不许再在实现里写这些字面量**。
|
||||
pub fn metrics(self) -> Option<ComponentMetrics> {
|
||||
metrics::metrics_of(self)
|
||||
}
|
||||
|
||||
/// 稳定字符串名(kebab-case)。
|
||||
pub const fn name(self) -> &'static str {
|
||||
match self {
|
||||
@@ -139,134 +194,15 @@ impl Component {
|
||||
Component::Snackbar => "snackbar",
|
||||
Component::Tooltip => "tooltip",
|
||||
Component::EmptyState => "empty-state",
|
||||
// M69 G9:末尾追加(`name()` 是持久化字符串 id,同样只许末尾追加)。
|
||||
Component::Checkbox => "checkbox",
|
||||
Component::Radio => "radio",
|
||||
Component::Search => "search",
|
||||
Component::NavigationDrawer => "navigation-drawer",
|
||||
Component::Divider => "divider",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn table_matches_component_order() {
|
||||
for (i, spec) in table::SPECS.iter().enumerate() {
|
||||
assert_eq!(
|
||||
spec.component as usize, i,
|
||||
"SPECS 第 {i} 行顺序与枚举不一致"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_component_resolves() {
|
||||
assert_eq!(Component::ALL.len(), table::SPECS.len());
|
||||
for component in Component::ALL {
|
||||
let spec = component.spec();
|
||||
assert_eq!(spec.component, component);
|
||||
assert!(spec.text.style().size_px >= 11.0);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn names_are_unique() {
|
||||
let mut seen = std::collections::BTreeSet::new();
|
||||
for component in Component::ALL {
|
||||
assert!(
|
||||
seen.insert(component.name()),
|
||||
"组件名重复:{}",
|
||||
component.name()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn elevation_matches_official_component_tokens() {
|
||||
// A 级来源(androidx.compose.material3.tokens):
|
||||
// FilledCardTokens/OutlinedCardTokens.ContainerElevation = Level0
|
||||
// MenuTokens.ContainerElevation = Level2
|
||||
// DialogTokens.ContainerElevation = Level3
|
||||
// SnackbarTokens.ContainerElevation = Level3
|
||||
// AppBarTokens = Level0、ListTokens.ItemContainerElevation = Level0
|
||||
// 偏离只允许出现在有登记的地方(Tooltip 是 B 级:token 文件里没有 Elevation 行)。
|
||||
for (component, expect) in [
|
||||
(Component::FilledCard, Elevation::Level0),
|
||||
(Component::OutlinedCard, Elevation::Level0),
|
||||
(Component::Menu, Elevation::Level2),
|
||||
(Component::Dialog, Elevation::Level3),
|
||||
(Component::Snackbar, Elevation::Level3),
|
||||
(Component::TopAppBar, Elevation::Level0),
|
||||
(Component::ListItem, Elevation::Level0),
|
||||
(Component::TextField, Elevation::Level0),
|
||||
] {
|
||||
assert_eq!(
|
||||
component.spec().elevation,
|
||||
expect,
|
||||
"{} 的高程与官方组件令牌不一致",
|
||||
component.name()
|
||||
);
|
||||
}
|
||||
// R4「全屏都在飘」的守门:一屏常见元素里带阴影(>Level0)的只允许是浮层。
|
||||
for component in Component::ALL {
|
||||
if component.spec().elevation > Elevation::Level0 {
|
||||
assert!(
|
||||
matches!(
|
||||
component,
|
||||
Component::Menu
|
||||
| Component::Dialog
|
||||
| Component::Snackbar
|
||||
| Component::Tooltip
|
||||
),
|
||||
"{} 不该有阴影(只有浮层允许)",
|
||||
component.name()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shapes_match_official_component_tokens() {
|
||||
// 官方:卡片 CornerMedium(12)、对话框 CornerExtraLarge(28)、菜单 CornerExtraSmall(4)、
|
||||
// 按钮/导航指示条 CornerFull、chip CornerSmall(8)。
|
||||
assert_eq!(Component::FilledCard.spec().shape, ShapeScale::Medium);
|
||||
assert_eq!(Component::OutlinedCard.spec().shape, ShapeScale::Medium);
|
||||
assert_eq!(Component::Dialog.spec().shape, ShapeScale::ExtraLarge);
|
||||
assert_eq!(Component::Menu.spec().shape, ShapeScale::ExtraSmall);
|
||||
assert_eq!(Component::PrimaryButton.spec().shape, ShapeScale::Full);
|
||||
assert_eq!(Component::Chip.spec().shape, ShapeScale::Small);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn only_primary_actions_use_filled_emphasis_and_one_dialog_layer() {
|
||||
// 契约 §1:状态色不复用 accent、每屏最多一个实心 accent → 只有 primary-button 是
|
||||
// 「实心强调」,其余按钮的形态由页面决定但不能是 Full + Level0 + Emphasized 的组合。
|
||||
let primary = Component::PrimaryButton.spec();
|
||||
assert_eq!(primary.shape, ShapeScale::Full);
|
||||
assert_eq!(primary.text, TypeRole::LabelLarge);
|
||||
// 浮层类必须比卡片层高。
|
||||
assert!(Component::Dialog.spec().elevation > Component::OutlinedCard.spec().elevation);
|
||||
assert!(Component::Menu.spec().elevation > Component::FilledCard.spec().elevation);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn morphs_always_expand_upwards() {
|
||||
for component in Component::ALL {
|
||||
if let Some(morph) = component.spec().morph {
|
||||
assert_ne!(
|
||||
morph.from,
|
||||
morph.to,
|
||||
"{} 的形变起止不能相同",
|
||||
component.name()
|
||||
);
|
||||
// 方向必须「沿阶梯胀开」(morph 对 `Full` 也没问题,rank 把它排在末尾),
|
||||
// 不比较像素跨度:`Full` 的半径依赖边长,用固定边长比会被 `None` 之外的大档拉爆。
|
||||
assert!(
|
||||
morph.from.rank() < morph.to.rank(),
|
||||
"{} 的形变方向反了:{:?} → {:?}",
|
||||
component.name(),
|
||||
morph.from,
|
||||
morph.to
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
mod tests;
|
||||
Reference in new issue
Block a user