Files
aurora-admin/.design_library/kole-ui-mobile/spec/移动端规格.md
T
aurora-admin f1fbfc2ddb
Regression / regression (push) Canceled after 0s
feat(品牌标识): 几何 K 图标(favicon/顶栏标记/theme-color) + 并行会话成果入库
## 品牌标识(本次会话)

起因:品牌此前没有任何图形标识 —— 唯一 favicon 是内联 data-URI 里的字母「A」,
那是 v2.0.0「Aurora Admin → Kole UI」改名漏掉的一处(PC 顶栏也是「A」,
移动端站已是「K」;移动端文档站则完全没有 favicon)。

- 几何:24 网格三个互不接触的笔画(竖 + 两斜),圆头描边;
  描边 2.25 → 16px 标签页尺寸下正好 1.5px = 规范原文「描边1.5px」
- 取色分两套(刻意):favicon 硬编码品牌蓝/白(渲染在浏览器标签栏,不继承 kole-dark);
  顶栏标记走 currentColor(实测暗色下自动转 rgb(20,22,28))
- 新增 theme-color 双条(light #FFFFFF / dark #1C1F26,取 --kole-color-card-bg)
- 修 site/app.js hero 标语 KOLE ADMIN → KOLE UI(改名变形残留)
- 移动端 7 个模板补 favicon(此前计数 0)

验收:门禁 9 条全 OK(site-routing/site-routes/mobile-docs/mobile-site/isolation/
theme/nav/i18n/icons);PC 回归 1464/1464 · 移动端 807/807,各连跑 8 次一致;
两端 favicon 405 字节逐字节一致;PC 站控制台错误 1→0。

## 并行会话成果(本次一并入库)

- 图标系统:2576 图标(TDesign/Element Plus,MIT)+ 11 端注入 + 5 个构建门禁工具
  + IconPreview 预览页 + ICON-SPEC.md 冻结规格
- 移动端平台:47 组件 × 6 端 + 文档站 53 页 + 隔离门禁
- PC 组件:103 个大后台组件 / 组件11 批次
- uni-app:PC 端试点 + 移动端端实现 + 真实编译验证

## 工程

- .gitignore 补 .scratch/ 与 .zcode-preexisting-*.txt(会话中间产物,实测 9.1MB,不入库)
- CHANGELOG 补品牌标识条目
- ROADMAP 登 S8-P4(品牌标识任务包 + og:image/apple-touch-icon 未做部分)
2026-09-21 10:05:48 +08:00

107 KiB
Raw Blame History

Kole UI · 移动端规格(Mobile Spec)

文档性质:移动端组件的规格原文,与本仓库 组件1~10.txt(PC 端)同级 —— 契约 JSON 的 usageHints / anatomy / doNotInvent 必须逐字来自本文件,不得发明。 来源声明:PC 端规格来自外部交付的 组件1~10.txt;移动端没有外部交付的规格原文, 本文件由本仓库撰写并作为唯一授权来源(sourceKind: "authored-spec")。 因此移动端契约的 provenance 字段必须写明 authored-in-repo,不得声称来自 组件*.txt。 分支:feat/s6-p21-component-families 之后的 S7 阶段。


〇 · 隔离总则(先读)

移动端与 PC 端原料隔离、令牌同源:

维度 PC 端 移动端 是否共享
实现目录 frameworks/ frameworks-mobile/ 否
契约 .design_library/kole-ui/components/ .design_library/kole-ui-mobile/components/ 否
类名前缀 kole- kole-m- 否
令牌前缀 --kole- --kole-m- 颜色/字体/圆角/阴影共享(@import PC 令牌文件)
导出名 KoleButton KoleMNavBar 否
测试页 tests/<slug>.html tests/mobile/<slug>.html 否
回归报告 tests/report.json tests/mobile-report.json 否
文档站 site/components/、SPA 路由 site/m/(静态站,不进 SPA 路由表) 否

硬规则:

  1. 移动端实现不得出现在 frameworks/;PC 实现不得出现在 frameworks-mobile/。
  2. 移动端样式只能引用 --kole-* 或 --kole-m-* 令牌,不得出现硬编码十六进制颜色。
  3. 移动端类名只能是 kole-m-<name> 或状态类 is-<state>。
  4. PC 侧任何文件不得引用 frameworks-mobile/ 或 site/m/;移动端不得引用 frameworks/。

1 · 顶部导航栏 NavBar

1.1 用途

页面顶部的标题栏,提供返回入口、页面标题与右侧操作区。

1.2 结构(anatomy)

  • bar:导航栏容器,固定于页面顶部,高度 44px + 顶部安全区
  • back:左侧返回入口,可为返回箭头或文字
  • title:中间标题,单行省略
  • actions:右侧操作区,可放 1~2 个图标按钮或一个文字按钮
  • divider:可选底部分隔线

1.3 变体维度

  • titleAlign:center(标题居中,左侧仅图标) / left(标题左对齐,紧跟返回)
  • elevated:false(无阴影) / true(滚动后投影)

1.4 状态

  • default:常态
  • scrolled:页面滚动后出现分隔线或投影
  • disabled:右侧操作不可用(按钮置灰)

1.5 交互与触控

  • 返回入口与右侧操作的点击热区不小于 44×44,视觉图标可小于该尺寸
  • 标题超长时单行省略,不换行、不撑开栏高
  • 顶部内边距包含安全区,横屏与刘海屏不遮挡内容

1.6 无障碍

  • 容器语义 role="banner"(页内使用时也可用 role="navigation" 并配 aria-label)
  • 标题节点具备 aria-label 或可见文本
  • 图标按钮具备 aria-label

1.7 doNotInvent

  • 多行标题的折叠规则
  • 返回行为的栈深度策略(是否回退到首页)

1.8 unknowns

  • 阴影出现的确切滚动阈值
  • 右侧操作超过 2 个时的收敛方式

2 · 底部标签栏 TabBar

2.1 用途

底部主导航,2~5 个标签页之间切换,是移动端一级导航。

2.2 结构(anatomy)

  • bar:标签栏容器,固定于页面底部,高度 50px + 底部安全区
  • item:单个标签,含图标与文字
  • icon:图标,可为内联 SVG
  • label:标签文字,11px
  • badge:角标,可为数字或红点

2.3 变体维度

  • count:2 / 3 / 4 / 5(标签数量,超过 5 项应改用其它导航形态)
  • badge:none / dot / number

2.4 状态

  • active:当前选中项,颜色为品牌色
  • inactive:未选中项,颜色为次要文字色
  • disabled:该项不可点击(置灰且不响应)

2.5 交互与触控

  • 每项点击热区等分整栏宽度,高度不小于 44px
  • 选中项切换后 aria-selected 同步变化
  • 底部内边距包含安全区

2.6 无障碍

  • 容器 role="tablist",单项 role="tab"
  • 选中项 aria-selected="true",未选中 false
  • 每项具备 aria-label(图标 + 文字时可用文字代替)

2.7 doNotInvent

  • 标签项超过 5 个时的滚动或折叠规则
  • 图标资源的成套规则(规格只约定尺寸与语义)

2.8 unknowns

  • 角标超过两位数的收敛(如 99+)
  • 选中态是否带图标填充切换

3 · 动作面板 ActionSheet

3.1 用途

从底部弹出的操作列表,用于在少量互斥操作中做一次选择。

3.2 结构(anatomy)

  • mask:遮罩,点击关闭
  • panel:面板容器,自底部滑出
  • title:可选标题说明
  • action:操作项,高度 56px,可标记危险操作
  • cancel:底部取消按钮,与操作项之间有间隔

3.3 变体维度

  • tone:default / danger(危险项用错误色)
  • cancel:inline(取消作为普通项) / separate(取消独立成块)

3.4 状态

  • closed:面板收起(默认)
  • open:面板展开,遮罩可见
  • disabled:单个操作项不可用(置灰、点击无效)

3.5 交互与触控

  • 点击遮罩关闭;点击操作项或取消后关闭
  • 面板滑出动画 240ms,缓动曲线 cubic-bezier(.32,.72,0,1)
  • 操作项点击热区高度不小于 56px

3.6 无障碍

  • 面板 role="dialog" + aria-modal="true"
  • 遮罩为纯装饰,不参与焦点
  • 操作项为原生 button,危险项带 aria-label 说明

3.7 doNotInvent

  • 多级面板的堆叠规则
  • 手势下滑关闭的触发阈值

3.8 unknowns

  • 操作项超过多少条时需要内部滚动
  • 危险项是否需要二次确认

4 · 下拉刷新 PullRefresh

4.1 用途

列表顶部下拉手势触发刷新,移动端最常见的列表刷新入口。

4.2 结构(anatomy)

  • viewport:包裹滚动内容的容器,负责手势
  • indicator:下拉指示区,含箭头或旋转图标与状态文字
  • content:业务内容

4.3 变体维度

  • state:pull(下拉中) / ready(已达阈值) / refreshing(刷新中) / done(完成提示)
  • threshold:触发阈值,默认 60px

4.4 状态

  • pull:下拉未达阈值,指示器随位移旋转
  • ready:达到阈值,提示「松开立即刷新」
  • refreshing:刷新中,指示器旋转,下拉不回弹
  • disabled:手势失效(如刷新中再次下拉)

4.5 交互与触控

  • 手势使用 Pointer Events,位移以纵向为主;横向位移更大时让位给页面横滑
  • 达到阈值后松手进入 refreshing;未达阈值松手回弹
  • 刷新期间再次下拉不重复触发

4.6 无障碍

  • 指示区 role="status" + aria-live="polite",状态文字变化被读屏播报
  • 需保留一个非手势的等价入口(如列表底部的刷新按钮)

4.7 doNotInvent

  • 惯性与阻尼曲线
  • 与页面整体下拉(浏览器级)的竞争规则

4.8 unknowns

  • 刷新超时的提示形式
  • 完成提示的停留时长

5 · 滑动单元格 SwipeCell

5.1 用途

列表行左滑露出操作按钮,用于删除、标记等单行操作。

5.2 结构(anatomy)

  • cell:可滑动的行容器
  • content:行内容(标题、描述)
  • actions:右侧操作区,随滑动露出
  • action:单个操作按钮,可标记危险

5.3 变体维度

  • direction:left(左滑露出右侧操作,默认) / right(右滑露出左侧操作)
  • actions:1 / 2(操作数量,最多 2 个)

5.4 状态

  • closed:未滑动(默认)
  • open:已滑出,操作区可见
  • dragging:拖拽中

5.5 交互与触控

  • 横向位移超过 10px 判定为滑动,纵向位移更大时取消滑动
  • 松手后按位移是否超过操作区宽度的一半决定展开或回弹
  • 展开状态下点击内容区先收起,不触发内容点击

5.6 无障碍

  • 操作按钮为原生 button,具备 aria-label
  • 滑动不可作为唯一路径:操作按钮在展开后必须可键盘聚焦

5.7 doNotInvent

  • 多行同时展开的互斥规则
  • 滑动与纵向滚动的竞争阈值细节

5.8 unknowns

  • 操作区宽度是否有标准档位
  • 拖拽中的阴影表现

6 · 按钮 Button

6.1 用途

触发一个即时动作。移动端按钮要比桌面端更"敢按":点击热区不小于 44px,主次层级靠颜色与边框区分。

6.2 结构(anatomy)

  • button:根元素,用原生 button,圆角取令牌
  • label:按钮文字,单行不换行
  • icon:可选图标或加载指示器,与文字间距 4px
  • block:可选块级形态,撑满容器宽度

6.3 变体维度

  • type:primary / default / text / danger
  • size:large(44px 高,移动端默认)/ default(36px)/ small(28px)
  • block:false / true

6.4 状态

  • default:常态
  • active:按下时背景加深(移动端没有 hover,反馈靠 :active)
  • disabled:置灰且不响应点击
  • loading:显示加载指示器并阻止重复触发

6.5 交互与触控

  • 高度不小于 44px 的档位用于主操作区;小尺寸档只用于行列内联操作
  • 按下反馈用 :active 背景色变化,不做位移缩放(避免长按抖动)
  • loading 期间重复点击不触发第二次事件

6.6 无障碍

  • 使用原生 button,天然可聚焦、可键盘触发
  • 仅有图标时必须给 aria-label
  • loading 时置 aria-busy="true" 且 disabled(避免重复提交)

6.7 doNotInvent

  • 按钮内多行文字的排版规则
  • 长按(long-press)的附加行为

6.8 unknowns

  • 图标与文字同时存在时的最小宽度
  • 危险按钮是否需要二次确认

7 · 单元格 Cell

7.1 用途

列表的基本单元:一行里承载"标题 + 说明 + 值 + 箭头",可整行点击进入下级。

7.2 结构(anatomy)

  • cell:根元素,一行两类内容(左侧主区 / 右侧值区)
  • title:主标题,单行省略
  • desc:可选副标题,单行省略
  • value:右侧值或状态文字
  • arrow:可选右箭头,表示可进入
  • icon:可选左侧图标

7.3 变体维度

  • arrow:false / true
  • link:false(纯展示)/ true(整行可点)

7.4 状态

  • default:常态
  • active:按下时整行背景变化
  • disabled:置灰且不响应

7.5 交互与触控

  • 可点单元格整行都是热区,高度不小于 56px
  • 按下反馈是整行背景变化,不是只有文字变色
  • 单元格之间默认有 1px 分隔线,可用 borderless 关掉

7.6 无障碍

  • 可点单元格用原生 button;纯展示单元格用 div
  • 值区只作展示时不要放进可聚焦元素

7.7 doNotInvent

  • 多行标题的折叠规则
  • 单元格内多列布局的栅格规则

7.8 unknowns

  • 副标题最多显示几行
  • 右侧值超长时的截断策略

8 · 分割线 Divider

8.1 用途

在内容之间画一条细分隔线;带文字时用于分区标题。

8.2 结构(anatomy)

  • divider:根元素,水平时为 1px 高
  • text:可选文字,居中或左/右对齐
  • line:文字两侧的线

8.3 变体维度

  • orientation:horizontal / vertical
  • align:left / center / right(带文字时生效)
  • dashed:false / true

8.4 状态

  • 静态组件,无交互状态(这是设计如此,不是缺失)

8.5 交互与触控

  • 纯静态,不参与点击与手势
  • 垂直分割线高度跟随父容器(用 align-self: stretch)

8.6 无障碍

  • 纯装饰:role="separator" + aria-hidden="true"(对读屏无信息量)
  • 带文字的分割线不隐藏,文字本身即语义

8.7 doNotInvent

  • 主题化分割线(渐变、图片)的表现

8.8 unknowns

  • 垂直分割线的推荐间距

9 · 徽标 Badge

9.1 用途

在图标或头像右上角标出数量或状态(红点 / 数字 / 短文字)。

9.2 结构(anatomy)

  • badge:根元素,可包裹子元素(角标形态)也可独立使用
  • count:数字或短文字
  • dot:红点形态(无内容)
  • wrap:被包裹的内容(图标 / 头像 / 按钮)

9.3 变体维度

  • shape:dot / number / text
  • standalone:false(包裹在子元素上)/ true(独立使用)

9.4 状态

  • default:常态
  • overflow:数值超过 max 时显示 max+
  • hidden:数值为 0 且未开启 showZero 时隐藏

9.5 交互与触控

  • 角标本身不可点(可点的是被包裹的元素)
  • 角标不改变被包裹元素的布局尺寸(绝对定位)

9.6 无障碍

  • 数字角标配 aria-label(如「12 条未读」),否则读屏只读出数字
  • 纯装饰红点 aria-hidden="true"

9.7 doNotInvent

  • 角标内容的动画(出现 / 消失)

9.8 unknowns

  • max 的默认取值(本实现取 99)
  • 独立使用时是否需要背景色

10 · 标签 Tag

10.1 用途

用简短的文字标注状态、分类或属性;可关闭的标签用于已选项。

10.2 结构(anatomy)

  • tag:根元素,圆角胶囊
  • label:标签文字
  • close:可选关闭按钮

10.3 变体维度

  • tone:default / primary / success / warning / danger
  • size:default / small
  • closable:false / true

10.4 状态

  • default:常态
  • disabled:置灰且不可关闭
  • closed:已关闭(由宿主从列表里移除)

10.5 交互与触控

  • 关闭按钮热区不小于 24×24(小尺寸标签内用负外边距扩展热区)
  • 关闭动作只触发一次事件,是否真的移除由宿主决定

10.6 无障碍

  • 关闭按钮为原生 button 并带 aria-label(如「移除标签:已发货」)
  • 纯展示标签不加交互角色

10.7 doNotInvent

  • 标签的动态增删动画
  • 超长标签的换行规则

10.8 unknowns

  • 同一行最多放几个标签
  • 关闭后是否保留占位

11 · 弹出层 Popup

11.1 用途

从指定方向弹出的浮层基座,承载面板、抽屉、动作列表等内容。

11.2 结构(anatomy)

  • mask:遮罩,点击关闭(可关)
  • popup:浮层容器,按方向定位
  • header:可选标题区
  • body:内容区,可滚动
  • close:可选关闭按钮

11.3 变体维度

  • placement:center / bottom / top / left / right
  • round:false / true(贴边方向在靠内容一侧切圆角)

11.4 状态

  • closed:收起(默认)
  • open:展开,遮罩可见
  • dragging:预留(本实现未做拖拽关闭)

11.5 交互与触控

  • 遮罩点击关闭;closeOnMask=false 时不关闭
  • 滑入动画 240ms,缓动 cubic-bezier(.32,.72,0,1)
  • 内容超出时 body 内部滚动,遮罩不滚动

11.6 无障碍

  • 浮层 role="dialog" + aria-modal="true"
  • 遮罩 aria-hidden="true"(纯装饰)
  • 关闭按钮为原生 button 并带 aria-label

11.7 doNotInvent

  • 多浮层堆叠的层级规则
  • 手势下滑关闭的阈值

11.8 unknowns

  • 各方向的内容最大尺寸
  • 是否需要焦点陷阱(focus trap)

12 · 轻提示 Toast

12.1 用途

在屏幕中央或底部短暂提示一条结果信息(成功 / 失败 / 警告 / 加载中),不打断当前操作。

12.2 结构(anatomy)

  • toast:浮层本体,居中于视口
  • icon:状态图标(可选)
  • text:提示文字
  • mask:可选透明遮罩(mask=true 时阻断下方点击)

12.3 变体维度

  • tone:info / success / warning / danger / loading
  • position:center / bottom / top
  • mask:false / true(阻断交互)

12.4 状态

  • hidden:收起(默认,靠 is-open 切换)
  • open:展开可见
  • loading:tone=loading 时图标持续旋转(prefers-reduced-motion 下降级为静止)

12.5 交互与触控

  • 提示本身不可点(不抢焦点、不阻断),mask=true 时遮罩吸收手势
  • 出现 / 消失动画 240ms;自动关闭时长由宿主控制(本组件只负责显示态)

12.6 无障碍

  • 容器 role="status" + aria-live="polite"(结果朗读一次,不反复打断)
  • tone=loading 时补 aria-busy="true"
  • 图标为装饰(aria-hidden="true"),语义全部由文字承担

12.7 doNotInvent

  • 自动关闭的默认时长
  • 多条提示的排队 / 合并策略

12.8 unknowns

  • 单行文字的最大宽度与换行规则
  • 是否需要点击穿透设置

13 · 对话框 Dialog

13.1 用途

需要用户确认或输入的中断式浮层:标题 + 内容 + 操作按钮组。

13.2 结构(anatomy)

  • mask:遮罩,点击可关(可配置)
  • dialog:对话框本体,居中
  • header:标题区
  • body:内容区
  • footer:操作按钮组(取消 / 确认)

13.3 变体维度

  • variant:confirm(确认框)/ alert(提示框,只有一个按钮)
  • tone:default / danger(确认按钮用错误色)
  • round:false / true

13.4 状态

  • closed:收起(默认)
  • open:展开,遮罩可见
  • loading:确认按钮进入加载态并禁用(由宿主传入)

13.5 交互与触控

  • 遮罩点击关闭;closeOnMask=false 时不关闭
  • Esc 关闭(键盘可达时)
  • 按钮热区不小于 44px;操作按钮等宽排列

13.6 无障碍

  • 对话框 role="dialog" + aria-modal="true" + aria-labelledby 指向标题
  • 打开后焦点落在对话框内(本实现只标记 tabindex="-1" + role,焦点陷阱见 §13.7)
  • 遮罩 aria-hidden="true"

13.7 doNotInvent

  • 焦点陷阱(focus trap)的完整实现
  • 多对话框嵌套时的层级规则

13.8 unknowns

  • 对话框的最大宽度与最大高度
  • 长内容是否在 body 内滚动

14 · 宫格 Grid

14.1 用途

把图标 / 文字入口按等分列排成网格,用于首页功能入口区。

14.2 结构(anatomy)

  • grid:根容器
  • grid__item:单个格子(图标 + 文字)
  • grid__icon:图标区
  • grid__text:文字标签

14.3 变体维度

  • columns:2 / 3 / 4(每行格数)
  • border:false / true(是否画格线)
  • square:false / true(格子是否为正方形)

14.4 状态

  • default:常态
  • active:按下反馈(:active 底色变化)
  • disabled:置灰且不可点

14.5 交互与触控

  • 每个格子整块可点,热区不小于 44×44
  • 按下反馈用 :active(不用 :hover —— 触屏没有悬停)

14.6 无障碍

  • 可点格子用原生 button(整块热区 + 键盘可达)
  • 纯展示格子用 div 且不加交互角色
  • 图标装饰(aria-hidden="true"),文字即语义

14.7 doNotInvent

  • 格子的拖拽排序
  • 超出 4 列的响应式折行列数

14.8 unknowns

  • 图标区的推荐尺寸
  • 一格最多几个字

15 · 步骤条 Steps

15.1 用途

横向展示多步流程的当前进度(如「提交 → 审核 → 完成」)。

15.2 结构(anatomy)

  • steps:根容器,横向排列
  • steps__item:单个步骤
  • steps__dot:序号点 / 勾选标记
  • steps__label:步骤标题
  • steps__line:连接线

15.3 变体维度

  • direction:horizontal / vertical
  • status:wait(未开始)/ process(进行中)/ finish(已完成)/ error

15.4 状态

  • wait:灰色,未开始
  • process:品牌色,进行中
  • finish:品牌色 + 勾选
  • error:错误色

15.5 交互与触控

  • 纯展示组件,步骤本身不可点(这是设计如此,不是缺失)
  • 步骤过多时横向可滚动(容器 overflow-x: auto)

15.6 无障碍

  • 容器 role="list",每步 role="listitem"
  • aria-current="step" 标出当前步
  • 状态不只靠颜色(进行中加粗 + 已完成用勾选字符)

15.7 doNotInvent

  • 步骤之间的动画过渡
  • 点击步骤跳转的规则

15.8 unknowns

  • 纵向步骤条的推荐间距
  • 标题最多几行

16 · 通知栏 NoticeBar

16.1 用途

在页面顶部或内容区之间横向滚动展示一条通告(如「系统维护通知」)。

16.2 结构(anatomy)

  • noticebar:根容器
  • noticebar__icon:左侧喇叭图标
  • noticebar__text:通告文字(可滚动)
  • noticebar__close:可选关闭按钮

16.3 变体维度

  • tone:default / success / warning / danger
  • scrollable:false / true(文字超宽时是否跑马灯)
  • closable:false / true

16.4 状态

  • default:常态
  • scrollable:文字持续横向滚动
  • closed:已关闭(由宿主移除)

16.5 交互与触控

  • 关闭按钮热区不小于 24×24(用负外边距扩展)
  • 跑马灯在 prefers-reduced-motion: reduce 下停止滚动并改为换行显示(无障碍硬要求)
  • 关闭动作只触发一次事件,是否移除由宿主决定

16.6 无障碍

  • 容器 role="status"(读屏会朗读一次通告)
  • 关闭按钮为原生 button 并带 aria-label
  • 跑马灯不得用 aria-live(滚动内容反复朗读会干扰)

16.7 doNotInvent

  • 多条通告的排队 / 轮播
  • 滚动速度的可配置项

16.8 unknowns

  • 通告文字的最大长度
  • 是否支持富文本

17 · 数字键盘 NumberKeyboard

17.1 用途

为金额、验证码等纯数字输入提供自绘键盘(比系统键盘更可控、更安全)。

17.2 结构(anatomy)

  • keyboard:根容器,固定在底部
  • keyboard__key:单个按键
  • keyboard__key--delete:删除键
  • keyboard__confirm:确认键(type=confirm 时)

17.3 变体维度

  • type:number(0-9 + 小数点)/ digit(纯 0-9)
  • showDelete:false / true
  • showConfirm:false / true

17.4 状态

  • default:常态
  • active:按下反馈
  • disabled:禁用(确认键不可用)

17.5 交互与触控

  • 按键热区不小于 44×44,网格等分排列
  • 按下用 :active,不做悬停态
  • 键盘本身不持有输入值 —— 只 emit 按键事件,由宿主决定写入哪个输入框

17.6 无障碍

  • 容器 role="group" + aria-label
  • 每个按键为原生 button,文字即按键名(读屏读「1」「删除」)
  • 删除键用 aria-label="删除"(不读成符号)

17.7 doNotInvent

  • 键盘高度的手势拖拽调整
  • 与系统键盘的互斥逻辑

17.8 unknowns

  • 是否支持自定义按键顺序
  • 长按连续删除的间隔

18 · 日期选择器 DatePicker

18.1 用途

从底部弹出的年 / 月 / 日选择器,用于生日、有效期这类需要日期输入的场景。

18.2 结构(anatomy)

  • mask:遮罩,点击关闭
  • picker:底部浮层
  • picker__header:取消 / 标题 / 确定
  • picker__columns:年 / 月 / 日三列
  • picker__column:单列,可滚动
  • picker__option:单个选项

18.3 变体维度

  • mode:date(年月日)/ month(年月)
  • round:false / true

18.4 状态

  • closed:收起(默认)
  • open:展开,遮罩可见
  • selected:当前选中项(品牌色高亮)

18.5 交互与触控

  • 遮罩点击关闭;closeOnMask=false 时不关闭
  • 选项行高不小于 44px,滚动容器 -webkit-overflow-scrolling: touch
  • 确定 / 取消按钮热区不小于 44px

18.6 无障碍

  • 浮层 role="dialog" + aria-modal="true"
  • 每列 role="listbox",选项 role="option" + aria-selected
  • 遮罩 aria-hidden="true"
  • 选中值以 YYYY-MM-DD 文本呈现(不依赖视觉滚动位置)

18.7 doNotInvent

  • 日期范围的禁用规则(由宿主传入)
  • 滚轮惯性 / 吸附动画的物理参数

18.8 unknowns

  • 可选年份的范围
  • 是否支持「至今」这类特殊选项

19 · 图标 Icon

19.1 用途

用一个字形表达状态或动作(选中、警告、返回、更多)。移动端与桌面端的差别在尺寸基线:手指操作要求图标更大、且图标自身从不承担点击 —— 点击由包裹它的 44px 按钮提供。

19.2 结构(anatomy)

  • icon:根元素,一个固定边长的内联盒子,负责尺寸与颜色
  • glyph:字形节点,纯符号或内联 SVG,不带语义
  • label:可选的无障碍名称,有值时图标成为「有语义的图」,无值时对读屏隐藏
  • size:四档边长(small 16 / default 20 / large 24 / xlarge 32)
  • tone:颜色来源(继承父级或取语义色)

19.3 变体维度

  • size:small / default / large / xlarge
  • tone:default / brand / secondary / danger
  • spin:false / true

19.4 状态

  • default:常态(静态图)
  • spinning:持续旋转,仅表示「进行中」,不表示成功
  • disabled:无独立禁用态 —— 由父级按钮 / 单元格置灰,图标继承其颜色

19.5 交互与触控

  • 图标自身不是热区;可点时由父级按钮提供不小于 44×44 的点击区
  • 图标与相邻文字的间距取 4px(与按钮内图标一致)
  • spin=true 在 prefers-reduced-motion: reduce 下停止旋转(无障碍硬要求)
  • 同一行内图标与文字基线对齐(vertical-align: -0.125em),避免文字被顶高

19.6 无障碍

  • label 有值时 role="img" + aria-label,读屏读出该名称
  • label 为空时 aria-hidden="true",纯装饰不进读屏序列
  • 图标不得作为唯一信息载体:颜色变化必须伴随文字或 label
  • 语义色图标与背景的对比度不低于 3:1(WCAG 1.4.11 非文本对比)

19.7 doNotInvent

  • 图标资源清单(本仓库不引图标字体,字形由各端内置表或内联 SVG 提供)
  • 图标更换 / 过渡动画
  • 点击图标自身触发动作

19.8 unknowns

  • 业务侧自定义字形的注册方式
  • 图标与文字组合时的推荐最小间距(本实现取 4px)

20 · 布局 Layout

20.1 用途

把一行内容按比例切成若干列(等分或按 12 栅格取值)。移动端屏幕窄,绝大多数场景是 2~4 等分;需要主次分栏时才用不等宽 —— 桌面端的「12 栅格 + 响应式断点」在移动端退化为「一档列数 × 可覆盖的列宽」。

20.2 结构(anatomy)

  • layout:行容器,display: flex + 换行,承载列间距变量
  • col:列,默认占满一行(flex-basis: 100%),由行级等分或列级 span 类决定实际宽度
  • panel:列内容的可选包壳,提供内边距与最小高度,业务也可直接放自己的卡片
  • gutter:列间距,四档(0 / 8 / 16 / 24),由行级类写到 CSS 变量上
  • span:列级权重类(12 栅格取值),可覆盖行级等分

20.3 变体维度

  • gutter:0 / 8 / 16 / 24
  • columns:0(不启用等分) / 2 / 3 / 4
  • align:start / center / end / stretch
  • wrap:true(换行) / false(不换行、横向滚动)

20.4 状态

  • default:常态
  • 本组件无交互状态(纯排布,这是设计如此,不是缺失)

20.5 交互与触控

  • 布局容器不绑点击,不设 cursor: pointer,热区始终由列内的业务元素提供
  • 列内若是可点区域,其热区不小于 44×44(见 §19.5 与 §6.5)
  • gutter 由 CSS 变量承载,同一行内所有列的宽度计算共用该变量,避免手工算宽
  • wrap=false 时容器横向滚动,纵向页面滚动不受影响
  • 列宽使用 flex-basis: calc(...),窄屏下不产生横向溢出(列内文本默认 min-width: 0)

20.6 无障碍

  • 布局是纯视觉分组:不添加 role、不添加 aria-*
  • 不要为了布局把语义节点(列表 / 按钮)拆到不相邻的列里,否则读屏顺序会错乱
  • 视觉顺序必须与 DOM 顺序一致(不用 order 重排)

20.7 doNotInvent

  • 响应式断点(移动端只有一档列数,不为大屏定义断点)
  • 列的排序 / 拖拽
  • 栅格嵌套的层级规则

20.8 unknowns

  • 列内推荐的最大列数(本实现提供 2 / 3 / 4 三档)
  • 列高等分(stretch 之外的等高策略)

21.1 用途

一段内联文字承载「跳转 / 打开下一级」或「触发一次轻量动作」。移动端与桌面端的差别在下划线策略:桌面端靠 hover 变色提示可点,触屏没有 hover,因此链接必须靠颜色常驻区分;默认不加下划线(正文里满屏下划线噪声大),正文段落内与条款页再加下划线。

21.2 结构(anatomy)

  • link:根元素,有 href 时是原生 a,无 href 时是原生 button(只回传事件)
  • label:链接文字,单行不换行(超长由宿主截断)
  • icon:可选尾部图标,继承链接颜色,与文字间距 4px
  • href:跳转地址;禁用时不渲染该属性(否则仍可被打开)
  • text:纯文字快捷入口,与默认插槽二选一

21.3 变体维度

  • tone:brand(默认) / default(继承父级文字色) / danger / success
  • underline:false / true
  • block:false / true(撑满容器、整行可点)

21.4 状态

  • default:常态
  • active:按下时颜色加深 + 极浅底色(移动端没有 hover)
  • disabled:置灰且不响应点击,同时移出 tab 序列
  • visited:不区分(业务型链接不标注已访问,避免用户误判状态)

21.5 交互与触控

  • 点击热区高度不小于 44px,文字可短但热区不缩水(内边距撑开)
  • 按下反馈为颜色变化,不做位移缩放(避免长按抖动)
  • 相邻链接之间至少留 8px 间距,防止误触(同时给出 16px 的更稳选择)
  • 禁用链接点击不触发事件,也不跳转

21.6 无障碍

  • 有 href 用原生 a(可聚焦、可长按复制、读屏报「链接」);无 href 用原生 button
  • 禁用链接不能用 href,且要 aria-disabled="true" + tabindex="-1"(移出 tab 序列)
  • 链接文字必须自解释,不要出现孤立的「点击这里」(读屏会脱离上下文朗读)
  • 颜色不是唯一线索:正文段落内的链接必须带下划线或图标,避免色觉障碍用户无法识别

21.7 doNotInvent

  • 外跳协议处理(tel: / mailto: / 唤起 App)
  • 已访问状态的样式
  • 链接的埋点 / 统计

21.8 unknowns

  • 一屏内链接的最大推荐数量
  • 链接与相邻文字的推荐最小间距(同时给出 8px 与 16px 两档)

22 · 加载 Loading

22.1 用途

告诉用户「系统正在处理,请等」并占住当前位置。移动端与桌面端的差别在是否独占屏幕:桌面端加载指示多是区块内的一个转圈,移动端常需要 fullscreen 铺一层遮罩 —— 提交订单、支付这类不可中断的动作期间,必须挡住下方的重复点击。

22.2 结构(anatomy)

  • loading:根元素,内联形态(转圈 + 文案同行或上下排);fullscreen=true 时它本身即遮罩层
  • spinner:转圈,由 CSS 动画驱动(三端一致,不依赖图片或字体)
  • text:可选文案,说明「在等什么」;为空时只有转圈
  • panel:全屏形态下的卡片面板,承载指示器与文案,保证遮罩上的对比度可控
  • mask:全屏形态的遮罩底色(取令牌,语义等同弹窗遮罩)

22.3 变体维度

  • size:small(16px) / default(20px) / large(28px)
  • vertical:false(横行) / true(上下排布)
  • fullscreen:false(区块内) / true(遮罩全屏)

22.4 状态

  • loading:转圈持续旋转,aria-busy="true"(默认语义)
  • open:fullscreen=true 且展开时可见并可截获手势
  • done:加载结束后的过渡态(转圈停止),实际结果提示由宿主替换(本组件不自动消失)
  • disabled:无独立禁用态 —— 加载中「不可操作」由遮罩承担,不是把控件置灰

22.5 交互与触控

  • 加载组件本身不可点,也不抢焦点(不打断读屏正在读的内容)
  • fullscreen=true 且 open 时遮罩截获手势,下方内容不可点;未展开时不截获
  • 转圈动画 800ms/圈(--kole-m-loading-duration 可覆盖),时长恒定不随尺寸变化
  • prefers-reduced-motion: reduce 下停止旋转(改为静态环),避免前庭不适
  • 加载超过一次会话的合理时长时应由宿主提供取消入口,本组件不自己造取消按钮

22.6 无障碍

  • 容器 role="status" + aria-live="polite"(状态变化被播报,且不打断当前朗读)
  • 加载中置 aria-busy="true";全屏遮罩未展开时置 "false"
  • 转圈是装饰(aria-hidden="true"),语义全部由文案与 role="status" 承担
  • 文案要具体(「正在提交订单…」而不是「加载中」),读屏用户与视力用户获得同样的信息量

22.7 doNotInvent

  • 加载耗时的进度百分比(本组件不假装知道进度)
  • 超时后的自动提示 / 自动重试
  • 多条加载的排队与合并

22.8 unknowns

  • 全屏加载持续多久后应提示「可能需要更长时间」
  • 文案的最大长度与换行策略

23 · 头像 Avatar

23.1 用途

用一张图或一两个字符代表一个主体(用户、企业、群组);移动端列表与详情页里大量出现,必须能单手扫读,因此尺寸只有三档、形状只有两种,且图片不可用时必须立刻退回文字,不能出现裂图。

23.2 结构(anatomy)

  • avatar:根元素,正方形圆角容器,尺寸由 size 决定
  • image:图片,铺满容器并按形状裁切
  • text:文字内容(姓名首字或简称),图片缺失时它就是主体
  • fallback:兜底节点,图片加载失败后退回的形状(文字或图标)
  • badge:可选右下角角标位,挂在线状态或未读数

23.3 变体维度

  • size:small(32px)/ default(40px)/ large(56px)
  • shape:circle(圆形,用于人)/ square(圆角方形,用于企业或群组)
  • type:text(文字头像)/ image(图片头像,失败退文字)

23.4 状态

  • default:常态
  • fallback:图片加载失败,退回文字或图标
  • disabled:置灰(如成员已离职)

23.5 交互与触控

  • 头像本身不是按钮;可点时必须由宿主包一层原生 button 或 a,热区不小于 44×44
  • 图片 alt 或根节点 aria-label 必填其一,读屏读「姓名 + 头像」
  • 图片加载失败只触发一次 error 事件,是否替换资源由宿主决定

23.6 无障碍

  • 文字头像根节点 role="img" 并带 aria-label(内容为姓名)
  • 图片头像直接用 <img alt>,alt 为空时视为装饰并对读屏隐藏
  • 角标不改变头像的可访问名

23.7 doNotInvent

  • 图片裁剪的 focal point(人脸居中)算法
  • 角标位置随形状(圆 / 方)的微调规则

23.8 unknowns

  • 文字头像的底色是否按名字散列取多色
  • 姓名超过两个汉字时的截断规则


24 · 列表 List

24.1 用途

把一组同类信息按行排列,用于「设置项 / 订单 / 成员」这类需要扫读的场合;移动端一屏只有 6~8 行,因此每行信息层级必须收敛到「标题 + 可选的说明或值」,分组之间用标题与留白分隔,而不是靠边框。

24.2 结构(anatomy)

  • list:根元素,一个列表区块
  • header:可选分组标题,位于列表之上
  • item:列表项,一行承载「前缀 + 主区 + 后缀」
  • prefix:可选前缀位(头像、图标、序号)
  • body:主区,标题 + 可选副标题,两行都单行省略
  • suffix:可选后缀位(值文字、标签、箭头、开关)
  • footer:可选分组脚注,用于补充说明

24.3 变体维度

  • border:true(行间 1px 分隔线)/ false(无分隔线,靠间距分组)
  • size:default(行高 56px)/ compact(行高 44px)
  • divider:inset(分隔线缩进到行内容起点)/ full(通栏分隔线)

24.4 状态

  • default:常态
  • active:行按下时整行背景变化
  • disabled:置灰且不响应点击
  • empty:列表为空时显示占位文案

24.5 交互与触控

  • 可点行整行都是热区,default 行高不小于 56px、compact 不小于 44px
  • 按下反馈是整行背景变化,不是只有文字变色
  • 前缀位不参与点击判定(点图标等于点整行),后缀位里的独立控件(开关、按钮)要阻止事件冒泡,避免一次点击触发两个动作
  • 列表滚动由宿主容器负责;本组件不接管滚动、不做虚拟列表

24.6 无障碍

  • 容器 role="list",纯展示行 role="listitem";可点行用原生 button(原生语义优先于 listitem)
  • 分组标题用 aria-label 或可见文本,读屏在进入分组时能读到
  • 空列表用 aria-live="polite" 播报占位文案

24.7 doNotInvent

  • 虚拟滚动与无限加载的触发规则
  • 行的拖拽排序与左滑操作(那是 SwipeCell 的职责)

24.8 unknowns

  • 单行最多几列(前缀 + 主区 + 后缀之外的排布)
  • 分组标题是否吸顶


25 · 折叠面板 Collapse

25.1 用途

把长内容按主题收起来,让用户先看到标题、按需展开某一段;移动端屏幕窄,展开后内容会顶走上下文,因此一次只展开一个(手风琴)是默认推荐形态,展开态必须明确到不靠颜色也能看出。

25.2 结构(anatomy)

  • collapse:根元素,一组面板的容器
  • item:单个面板,含标题行与内容区
  • header:标题行,整行可点,高度不小于 44px
  • arrow:标题行右侧箭头,展开时旋转 90°
  • panel:内容区,展开时可见(收起时高度为 0 或 hidden)
  • content:内容区内的正文节点

25.3 变体维度

  • accordion:false(多面板可同时展开)/ true(手风琴,同时只展开一个)
  • bordered:true(面板之间有分隔线与外框)/ false(无边框,靠留白分隔)

25.4 状态

  • collapsed:收起(默认)
  • expanded:展开
  • disabled:标题行置灰且不响应

25.5 交互与触控

  • 标题行整行都是热区,高度不小于 44px
  • 视觉箭头转 90°(180ms 过渡),展开时 aria-expanded 同步为 true
  • 手风琴模式下展开新面板会收起当前展开项;accordion=false 时互不影响
  • 内容区不做高度动画,直接切换 hidden(省电,读屏也不会读到中间态);减少动态偏好下箭头同样瞬时切换

25.6 无障碍

  • 标题行用原生 button 并带 aria-expanded / aria-controls
  • 内容区与标题用 id / aria-controls 建立关联,收起时用 hidden 属性隐藏(而不是只靠 CSS 高度)
  • 禁用项置 aria-disabled="true" 且不可聚焦

25.7 doNotInvent

  • 展开动画的高度换算公式(内容高度由浏览器决定)
  • 嵌套折叠面板的层级样式

25.8 unknowns

  • 默认是否展开第一项
  • 标题行右侧是否允许放额外操作


26 · 进度条 Progress

26.1 用途

把一个过程的完成度可视化(上传、审核、额度耗尽);移动端的进度多数伴随文字出现,因此百分比文案与状态色是标配而非可选,且进度变化要能被读屏播报而不只是画出来。

26.2 结构(anatomy)

  • progress:根元素,承载轨道与文案
  • track:轨道,未完成部分的底色
  • bar:已完成部分,宽度由 percentage 决定
  • ring:环形进度的圆环轨道(type=circle 时替代 track/bar)
  • label:百分比文案,可置于条内、条右侧或环心
  • status:状态图标位(成功 / 失败),非进行中时显示

26.3 变体维度

  • type:line(线形)/ circle(环形)
  • status:normal(进行中,品牌色)/ success(成功)/ error(失败)
  • labelPlacement:inside(文案在条内)/ right(条右侧)/ center(环心,仅 circle)

26.4 状态

  • default:进行中(0 < percentage < 100)
  • complete:percentage = 100,文案显示 100%
  • error:失败,进度停在断点并转为错误色
  • paused:暂停,条体降透明度

26.5 交互与触控

  • 进度条本身不可交互、不接收点击;需要取消时由宿主在旁边放按钮
  • 数值变化用 CSS 宽度过渡(240ms,--kole-m-progress-duration),不做无限循环动画(省电且不干扰读屏)
  • 无动画偏好(prefers-reduced-motion)下直接跳到目标宽度

26.6 无障碍

  • 根节点 role="progressbar" + aria-valuemin="0" / aria-valuemax="100" / aria-valuenow
  • 文案节点 aria-hidden="true",避免与 aria-valuenow 重复播报
  • 不确定进度(无法给出百分比)用 aria-valuetext="进行中" 表达

26.7 doNotInvent

  • 环形进度的线宽与半径的自适应规则(由 size 决定,不做响应式推导)
  • 进度到达 100% 后的自动隐藏时机

26.8 unknowns

  • 百分比是否四舍五入到整数
  • 环形进度是否支持渐变描边


27 · 输入框 Input

27.1 用途

在一行内收集单行文本(姓名、手机号、金额、验证码等);移动端与桌面端的关键差别是热区与字号:输入框整行占满、高度不小于 44px,字号不小于 16px,否则 iOS 聚焦时会自动放大页面;清除动作也必须在框内完成,因为触屏没有悬停的鼠标可以移开。

27.2 结构(anatomy)

  • field:字段容器,包裹输入框与下方错误提示(错误提示在框外,不挤占输入区)
  • prefix:可选前缀,放单位或符号(如「¥」)
  • control:原生 input,占满剩余宽度,字号 16px
  • clear:可选清除按钮,有值且 clearable=true 时出现在右侧
  • suffix:可选后缀,由默认插槽给出的自定义内容(单位、显示密码等)
  • errorText:字段下方的错误提示文字

27.3 变体维度

  • size:default(高度 44px)/ large(高度 52px)
  • clearable:false(无清除按钮)/ true(有值时右侧出现清除按钮)

27.4 状态

  • default:常态
  • focus:聚焦,边框变品牌色并带 2px 聚焦外发光
  • error:错误,边框变错误色且 aria-invalid="true"
  • disabled:置灰且不可聚焦

27.5 交互与触控

  • 输入框高度不小于 44px;整行可点(把 kole-m-input 放在 label 里,点行即聚焦)
  • 清除按钮视觉是 16px 图标,热区外扩到 44px 最小触控边长
  • 聚焦反馈是边框色 + 2px 外发光,150ms 过渡;不改变布局(不撑开高度)
  • 有值时清除按钮才出现;清除后焦点留在输入框
  • 键盘「完成」键触发 confirm 事件,值随 input 事件实时回传

27.6 无障碍

  • 输入框用原生 input,名称由 aria-label 给出(占位文字不算标签)
  • 错误态用 aria-invalid="true",错误文案用 aria-describedby 关联
  • 清除按钮是原生 button 且带 aria-label="清除"
  • 禁用态用原生 disabled,读屏会跳过

27.7 doNotInvent

  • 输入内容的正则与业务校验(合法性判断在宿主)
  • inputmode 之外的自定义软键盘行为(键盘类型由宿主按场景指定)
  • 自动填充与验证码自动读取的策略

27.8 unknowns

  • 密码是否需要内置「显示/隐藏」开关
  • 数字输入的千分位格式化时机
  • 前缀里是否允许放图片/图标


28.1 用途

用关键词从长列表里取回一小段结果;移动端的搜索框几乎总是页面顶部的独立一行,输入即过滤(不等回车),并且要给出一个明确的退出动作 —— 取消,因为触屏没有 Esc 键,用户清空关键词后仍需一键回到列表。

28.2 结构(anatomy)

  • search:根元素,一行里放进「搜索框 + 取消动作」
  • icon:框内左侧放大镜,纯装饰(不承载语义,读屏由 label 承担)
  • control:原生 input type="search",占满剩余宽度
  • clear:可选清除按钮,有值且清除可用时出现在图标与取消之间
  • cancel:可选取消动作,默认文案「取消」,由 showCancel 控制显隐

28.3 变体维度

  • round:true(胶囊形,页面顶部常用)/ false(方角,嵌在卡片或工具栏里)
  • showCancel:false(只有输入框)/ true(右侧出现取消动作)

28.4 状态

  • default:常态
  • filled:有值(清除按钮出现)
  • disabled:置灰且不可聚焦

28.5 交互与触控

  • 搜索框高度不小于 44px,取消动作热区不小于 44px
  • 输入即触发 input 事件(不等回车);键盘「搜索」键触发 search 事件
  • 清除按钮只在有值时出现,点击清空并把焦点留在输入框
  • 清除与取消都是 44px 热区;两者同时出现时先清除、再取消(自右向左层级递进)

28.6 无障碍

  • 输入框用原生 input type="search"(iOS 键盘右下角键位变成「搜索」)
  • 搜索图标是装饰性的,置 aria-hidden="true",名称由 aria-label 给出
  • 清除按钮是原生 button 且带 aria-label="清除";取消动作是原生 button
  • 取消按钮不用图标代替文字(触屏上文字比图标更好点)

28.7 doNotInvent

  • 搜索的防抖时值与接口节流策略(由宿主决定)
  • 搜索历史的存储与展示
  • 搜索结果的高亮规则

28.8 unknowns

  • 取消文案是否允许替换(如「返回」)
  • 是否需要自动聚焦并拉起键盘
  • 语音/扫码等扩展入口是否放进框内


29 · 开关 Switch

29.1 用途

即时启停一项配置或业务状态(启用通知、公开数据、自动同步);移动端与桌面端的关键差别是热区:开关本体视觉只有 48×28,但整行(开关 + 文字)都是可点热区,行高不小于 44px,否则手指点不中;关态与开态也不能只靠颜色区分,必须同时看到滑块位移。

29.2 结构(anatomy)

  • switch:根元素,一行里放进「轨道 + 文字标签」,整行可点
  • track:轨道,承载背景色与滑块位移的边界
  • knob:滑块,关态靠左、开态靠右(位移是开/关的主要视觉信号)
  • text:可选文字标签,说明这项开关控制什么
  • control:可点的整行控件(原生 button + role="switch"),承接键盘与读屏

29.3 变体维度

  • size:default(轨道 48×28)/ small(轨道 40×22,用于紧凑表单)
  • labelPlacement:right(文字在开关右侧,默认)/ left(文字在左侧,值区右对齐时用)

29.4 状态

  • off:关态(默认)
  • on:开态,滑块位移 + 背景变品牌色,aria-checked="true"
  • disabled:置灰且不可聚焦

29.5 交互与触控

  • 整行(开关 + 文字)都是热区,行高不小于 44px;开关本体不可单独缩到 44px 以下
  • 点击切换只需要一次触摸,不要求拖动滑块(拖动是桌面习惯,触屏误触率高)
  • 切换动效是滑块位移 150ms 过渡;减少动态偏好下瞬时切换
  • 关态与开态不能只靠颜色区分:滑块位置 + aria-checked 双通道
  • 切换后立即触发 change 事件,不做二次确认(需要确认的场景由宿主先弹对话框)

29.6 无障碍

  • 用 role="switch" + aria-checked="true|false",而不是 role="checkbox"(读屏会播报「开关」)
  • 承载控件是原生 button,键盘可聚焦、空格/回车可切换,并有可见焦点环
  • 文字标签在控件内部,读屏播报的名称就是标签本身;无标签时用 label 属性补 aria-label
  • 禁用态用原生 disabled,读屏会播报不可用

29.7 doNotInvent

  • 二次确认弹窗与「切换失败回滚」的业务流程
  • 三态开关(关 / 开 / 待定)的视觉表达
  • 与表单一起提交时的隐藏字段(由宿主添加)

29.8 unknowns

  • 开关本体是否允许小于 48×28(紧凑表单的下限)
  • 文案与开关的间距是否跟随字号
  • 加载态(切换请求进行中)如何表达


30 · 步进器 Stepper

30.1 用途

在一段有界区间里连续加减小整数(购买数量、份数、编号);移动端与桌面端的关键差别是加号与减号必须各自独立占一个 44px 见方的热区,且到边界时不是把按钮藏起来,而是置灰 —— 触屏上「按钮消失」会让用户以为界面坏了,而置灰能表达「到头了」。

30.2 结构(anatomy)

  • stepper:根元素,一行里放进「减号 + 值 + 加号」
  • minus:减号按钮,到达 min 时置灰
  • value:值区;editable=true 时是可聚焦的数字输入框,否则是纯文本
  • plus:加号按钮,到达 max 时置灰
  • unit:可选单位文案,跟在值后面(如「件」),由默认插槽给出
  • label:无障碍分组名称(role="group" + aria-label)

30.3 变体维度

  • size:default(控件高 44px)/ small(控件高 36px,视觉更紧凑)
  • round:false(方角)/ true(全圆角,用于购物车等轻量场合)

30.4 状态

  • default:常态
  • disabled:整组置灰且不可聚焦
  • 边界:值等于 min 时减号置灰,等于 max 时加号置灰(按钮仍在原位,不隐藏)

30.5 交互与触控

  • 加号与减号各自是 44px 见方的热区;size=small 视觉 36px 时用 --kole-m-hit-slack 把热区外扩回 44px
  • 单击步进一个 step,不响应长按连击(长按是桌面习惯,触屏上容易多跳)
  • 到达 min / max 时对应按钮置 disabled 且置灰,点击不产生值与事件
  • 值变化后立即触发 change 事件,不做防抖
  • editable=true 时值区接受键盘直接输入;非法输入(非数字、越界)在失焦时回落到边界值

30.6 无障碍

  • 根元素 role="group" + aria-label 说明这组控件在调什么
  • 减号 / 加号是原生 button,各自的 aria-label 是「减少」/「增加」(不用符号代替名称)
  • 值区在只读态置 role="spinbutton" 并写 aria-valuenow / aria-valuemin / aria-valuemax
  • 置灰的按钮用原生 disabled,读屏会播报不可用且键盘会跳过

30.7 doNotInvent

  • 长按连击、惯性加速的时值曲线
  • 小数与浮点精度(本组件只处理整数;金额请用输入框 + 数字键盘)
  • 超出边界的提示文案(由宿主决定是否提示)

30.8 unknowns

  • 值为 0 时是否自动隐藏整个步进器(购物车场景)
  • 是否需要键盘上的上下方向键加减
  • 单位文案是否随语言变化


31 · 多行文本框 Textarea

31.1 用途

收集可能超过一行的自由文本(备注、收货说明、退换原因);移动端与桌面端的关键差别是高度:文本框本身要够高(至少 3 行),因为触屏不能像桌面那样在输入过程中看到上下文;并且要给出实时字数反馈,超限时是「止写 + 报错」而不是静默截断。

31.2 结构(anatomy)

  • field:字段容器,包裹文本框、计数行与错误提示
  • control:原生 textarea,多行输入,行高不小于 1.5 倍字号
  • counter:右下角字数计数(已输入/上限),maxlength>0 时出现
  • errorText:字段下方的错误提示文字(与计数同行时计数不消失)
  • label:无障碍名称(aria-label),textarea 的初始高度由 rows 决定

31.3 变体维度

  • size:default(最小高度 3 行)/ large(最小高度 5 行,长文本场景)
  • showCounter:false(不显示计数)/ true(右下角显示「已输入/上限」)

31.4 状态

  • default:常态
  • focus:聚焦,边框变品牌色并带 2px 聚焦外发光
  • error:错误,边框变错误色且 aria-invalid="true"
  • disabled:置灰且不可聚焦
  • full:已达到 maxlength 上限(计数变错误色,继续输入不再增加)

31.5 交互与触控

  • 文本框最小高度 3 行(约 96px:3 × 24px 行高 + 上下 12px 内边距);只允许纵向伸缩(resize: vertical),不允许横向拉宽破坏 375 宽的布局
  • 计数随输入实时更新;达到上限时计数置错误色并停止接收新字符(原生 maxlength 兜底,超限靠宿主提示)
  • 文本框整体可点即聚焦(外层不出可点装饰);错误提示与计数都在框外,不挤占输入区
  • 聚焦反馈是边框色 + 2px 外发光,150ms 过渡;不改变高度
  • 不使用自动增高(高度随内容跳动会让上下文错位),需要更长文本时用 size=large

31.6 无障碍

  • 控件是原生 textarea,名称由 aria-label 给出(占位文字不算标签)
  • 错误态用 aria-invalid="true" + aria-describedby 关联错误文案
  • 计数是参考信息,用 aria-live="polite" 播报(不要每敲一个字都播报,只在接近上限时提示)
  • 禁用态用原生 disabled,读屏会跳过

31.7 doNotInvent

  • 自动增高(随内容撑高)的实现细节
  • 富文本 / Markdown 的编辑与渲染
  • 内容敏感词过滤与提交前的业务校验

31.8 unknowns

  • maxlength 缺省时上限取多少(本实现默认 200)
  • 是否需要在接近上限时提前变色(本实现只在到达上限时变色)
  • 计数是否包含空格与换行


32 · 表单 Form

32.1 用途

把一组字段(标签 + 控件 + 错误提示)组织成一次可提交的操作(下单、开票、认证);移动端与桌面端的关键差别是标签位置:窄屏放不下左右两列时标签默认置顶,只有需要一组字段纵向对齐时才退回到定宽左标签,且每个字段的错误提示必须贴在它自己的控件下方 —— 触屏上用户看不到「页头汇总错误」。

32.2 结构(anatomy)

  • form:根元素,包住全部字段与提交行
  • item:单个字段,一列一个,字段之间用 1px 分隔线划分
  • label:字段标签;required=true 时前面带一个错误色星号
  • star:必填星号,装饰性(aria-hidden),语义由控件的 aria-required 承担
  • control:字段控件(原生 input / textarea,或包一层的可点区域)
  • errorText:字段级错误提示,落在该字段控件下方

32.3 变体维度

  • labelPosition:top(标签在控件上方,默认)/ left(标签定宽 72px 与控件同行)
  • borderless:false(字段之间有分隔线)/ true(不画线,靠留白分组)

32.4 状态

  • default:常态
  • error:字段级错误,整项加 is-error,控件加 aria-invalid="true",下方出现错误文案
  • disabled:整表置灰且不可聚焦(子控件用原生 disabled)

32.5 交互与触控

  • 控件高度不小于 44px;非输入型控件(选择器一类)整行都是热区,按下反馈是整行背景变化
  • 错误提示出现在控件正下方,不挤占输入区,也不跨到标签列
  • 失焦即校验(触屏上软键盘收起会触发 blur),提交时再全量校验一次
  • 校验失败时不跳转、不滚动到页首,错误就落在出错的字段上
  • 提交成功后写 data-submitted="true" 作为可断言的提交凭证;星号与错误色不单独承载语义

32.6 无障碍

  • 标签用 aria-labelledby 关联到控件(标签不是占位符的替代品)
  • 必填用控件的 aria-required="true" 与原生 required 双写;星号本身 aria-hidden
  • 错误文案用 aria-describedby 关联到控件,并带 role="alert" 播报
  • 禁用态用原生 disabled,读屏会播报不可用且键盘会跳过

32.7 doNotInvent

  • 字段内容的业务校验规则(正则、长度、合法性判断都在宿主,本组件只表达结构)
  • 提交请求、失败重试与「提交中」的按钮态
  • 字段的联动显隐与动态增删(由宿主决定渲染什么)
  • 页首错误汇总条(移动端用字段级提示,不做汇总)

32.8 unknowns

  • 左标签模式下标签列宽是否随字号变化(当前固定 72px)
  • 提交行是否允许放两个并列动作(如「保存草稿 + 提交」)
  • 只读(readonly)字段是否需要独立的视觉层,还是复用 disabled


33 · 单选框 Radio

33.1 用途

在一组互斥选项里选且只选一项(支付方式、配送时效、发票类型);移动端与桌面端的关键差别是选中态的表达:触屏没有鼠标悬停可以预告状态,选中必须同时有形状差异(圆点内圈实心)与 aria-checked,不能只靠颜色;且整行(圆点 + 文字)都是热区,行高不小于 44px。

33.2 结构(anatomy)

  • group:根元素,role="radiogroup" + aria-label 说明这组在选什么
  • item:单个选项,整行都是热区(原生 button + role="radio")
  • icon:圆点,未选中是空心环、选中是实心圆 + 内圈反色点
  • label:选项文字,占满剩余宽度
  • desc:可选说明行,跟在文字下方(如「需先完成企业认证」)
  • state:选中态 is-checked 与 aria-checked 双写

33.3 变体维度

  • orientation:vertical(纵向排列,组内画分隔线)/ horizontal(横向排列,靠间距分组)
  • button:false(圆点 + 文字)/ true(胶囊标签式,无圆点)

33.4 状态

  • default:未选中(空心环)
  • checked:选中(实心圆 + 内圈反色点,aria-checked="true")
  • disabled:置灰且不可聚焦,读屏播报不可用

33.5 交互与触控

  • 整行(圆点 + 文字)都是热区,行高不小于 44px;横向组里每项自身也保持这个边长
  • 一次触摸即选中,不需要二次确认;选中后不能再点回空值(需要清空由宿主提供额外的「清除」动作)
  • 互斥由组件负责:选中一项会立即清掉同组其它项的选中态,不留下两个选中
  • 切换动效是圆点内圈 150ms 缩放;减少动态偏好下瞬时切换
  • 选中后立即触发 change 事件,回传该项的值

33.6 无障碍

  • 组用 role="radiogroup" + aria-label 说明分组名称
  • 每项是原生 button + role="radio" + aria-checked(读屏会播报「单选按钮,已选中/未选中」)
  • 圆点与内圈是纯装饰,对读屏隐藏(aria-hidden),语义全靠 role="radio"
  • 禁用项用原生 disabled 并补 aria-disabled="true"

33.7 doNotInvent

  • 「取消选中」的回到空值交互(单选的语义就是必有一项)
  • 横向组自动换行的列数策略(由宿主按文案长度决定,组件只负责 flex-wrap)
  • 与表单一起提交时的隐藏字段(由宿主添加)
  • 选项内容的异步加载与搜索过滤

33.8 unknowns

  • 横向组超过一行时是否改成纵向(当前包裹后当行处理)
  • 圆点尺寸是否随字号一起放大(当前固定 20px)
  • desc 说明行是否允许两行以上(当前单行省略)


34 · 多选框 Checkbox

34.1 用途

在一组选项里同时选中任意多项(兴趣标签、订阅范围、筛选维度),并支持「全选」;移动端与桌面端的关键差别是半选态的表达:触屏上没有鼠标悬停预告状态,全选行在「部分选中」时必须显示出与「全选 / 全不选」都不同的第三态(横杠),且整行(方框 + 文字)都是热区,行高不小于 44px。

34.2 结构(anatomy)

  • group:根元素,role="group" + aria-label 说明这组在选什么
  • item:单个选项,整行都是热区(原生 button + role="checkbox")
  • icon:方框,未选中是空心框、选中是品牌底 + 反色勾号、半选是品牌底 + 横杠
  • label:选项文字,占满剩余宽度
  • desc:可选说明行,跟在文字下方(如「需先绑定手机号」)
  • all:全选行,不存储自己的状态,由组内各项推导

34.3 变体维度

  • orientation:vertical(纵向排列,组内画分隔线)/ horizontal(横向排列,靠间距分组)
  • button:false(方框 + 文字)/ true(胶囊标签式,无方框)
  • selectAll:false(不显示全选行)/ true(组首显示全选行)

34.4 状态

  • default:未选中(空心框)
  • checked:选中(品牌底 + 反色勾号,aria-checked="true")
  • indeterminate:半选,只在全选行出现(aria-checked="mixed")
  • disabled:置灰且不可聚焦,读屏播报不可用

34.5 交互与触控

  • 整行(方框 + 文字)都是热区,行高不小于 44px;横向组里每项自身也保持这个边长
  • 一次触摸即切换,各项互相独立;不限制同时选中的数量上限
  • 点全选行:只要还有未选中项就全部选中,否则全部清空(不在「半选」上停留)
  • 全选的选中态是推导值,不单独存储 —— 单项变化后立即重算,避免出现「全选已勾上但还有一项没选」
  • 切换动效是勾号 150ms 缩放;减少动态偏好下瞬时切换
  • 每次切换立即触发 change 事件,回传切换后的完整值数组

34.6 无障碍

  • 组用 role="group" + aria-label 说明分组名称
  • 每项是原生 button + role="checkbox" + aria-checked(读屏会播报「复选框,已选中/未选中」)
  • 全选行的半选态用 aria-checked="mixed"(checkbox 角色允许的第三个值)
  • 方框与勾号是纯装饰,对读屏隐藏(aria-hidden),语义全靠 role="checkbox"
  • 禁用项用原生 disabled 并补 aria-disabled="true"

34.7 doNotInvent

  • 「最多选 N 项」的数量上限与超出提示(业务规则在宿主)
  • 分组嵌套(一组里再分组)的层级表达
  • 与表单一起提交时的隐藏字段(由宿主添加)
  • 选项内容的异步加载与「已选 N 项」的汇总条

34.8 unknowns

  • 全选行是否显示「已选 2/5」这类计数
  • 半选态在非全选行上的用例(如父级节点)
  • 胶囊按钮式是否也需要禁用态的视觉层(当前复用同一条置灰规则)


35 · 滑动选择器 Slider

35.1 用途

在一个连续区间里用一次滑动选出数值(预算上限、价格区间、音量、亮度);移动端与桌面端的关键差别是热区:轨道可视高度只有 4px,但整条轨道所在的行必须是 44px 高的热区,且按下任意位置就要跳到该处 —— 触屏上要求用户精确抓住 20px 的滑块是点不中的。

35.2 结构(anatomy)

  • slider:根元素,一行里放进「轨道热区 + 数值」
  • rail:轨道热区,44px 高的可聚焦区域,承载指针事件与锚点
  • track:轨道本体(可视 4px),承载选中段与刻度的边界
  • filled:选中段,从起点铺到滑块位置
  • thumb:滑块,位置由当前值的百分比决定;拖动中放大作为「抓住了」的反馈
  • mark:刻度点,仅 marks=with-marks 时出现(只标位置,不写文案)

35.3 变体维度

  • mode:single(单滑块)/ range(双滑块,左值不越过右值)
  • marks:with-marks(轨道上标出等分点)/ none(不标刻度)
  • showValue:true(右侧显示当前值)/ false(不显示数值)

35.4 状态

  • default:常态
  • dragging:拖动中(滑块 20px → 24px,轨道加一层浅底)
  • disabled:整条置灰且不响应,指针与键盘都不改值

35.5 交互与触控

  • 轨道热区高 44px,按下任意位置即跳到该处,随后横向拖动连续改值;不要求抓住滑块
  • 手势是「按下 + 拖动」一次连续动作,不响应长按或双击;横向分量优先,纵向滚动不被吞掉(touch-action: pan-y)
  • 取值按 step 吸附并夹在 [min, max];mode=range 时左值不越过右值、右值不越过左值
  • 拖动期间滑块放大到 24px,松手后 150ms 回弹;减少动态偏好下瞬时切换
  • 值变化立即触发 change 事件(连续回传),不做防抖;松手时才做业务提交

35.6 无障碍

  • 单滑块:轨道热区是 role="slider" 且可聚焦,写 aria-valuemin / aria-valuemax / aria-valuenow / aria-label
  • 双滑块:外层是 role="group" + aria-label,每个滑块各自持有 aria-valuenow(两条独立数值)
  • 禁用时写 aria-disabled="true",读屏会播报不可用
  • 键盘:聚焦后左右方向键按 step 步进(触屏之外的替代路径)

35.7 doNotInvent

  • 数值输入框与滑块的双向绑定(宿主自行组合 Input,本组件只回传值)
  • 刻度上的文案标注与区间高亮(只标位置,不写文案)
  • 拖动结束后的「撤销 / 重置」流程
  • 纵向滑块与圆形表盘式选择器

35.8 unknowns

  • 双滑块的最小间距是否应当是 1 个 step(当前是),还是用户可配
  • 刻度数量是否应由 step 推导(当前按 5 等分固定标点)
  • 拖动中是否要把当前值放大显示在滑块上方(气泡提示)


36 · 评分 Rate

36.1 用途

让用户对一次体验给出星级评价(商品、物流、服务),或只读展示已有的平均分;移动端与桌面端的关键差别是没有悬停预览:桌面上鼠标划过就能预告「点下去会是几分」,触屏上这个预告不存在,所以填充比例(含半星)必须直接可读,且星形本体只有 24px、热区要补到 44px 高,否则手指点不准相邻的两颗星。

36.2 结构(anatomy)

  • rate:根元素,一行里放进「星组 + 数值文案」
  • stars:星组,横向排列,只负责布局
  • star:单颗星,可点区域(原生 button),自身撑满 44px 高
  • glyph:星形本体(视觉 24px),由灰底星 + 品牌色覆盖层叠成
  • fill:覆盖层,宽度即该颗星的填充比例(100% 整星 / 50% 半星 / 0% 未选)
  • text:数值文案,默认「N 分」,可由默认插槽替换

36.3 变体维度

  • size:default(星形 24px)/ small(星形 18px,行内展示用)
  • allowHalf:false(只能取整星)/ true(允许半星)
  • readonly:false(可评分)/ true(只读展示已有评分)

36.4 状态

  • default:未评分(全部灰底星)
  • selected:已评分(填充比例 = 所选分值与满分的比例)
  • disabled:置灰且不可聚焦,点击不改值

36.5 交互与触控

  • 取值规则:点第 N 颗星取 N 分;allowHalf=true 时点第 N 颗星的左半取 N-0.5 分、右半取 N 分(半星只在开启该项时存在,默认整星)
  • 星形本体视觉 24px(size=small 时 18px),但每颗星的可点区域高度是 44px;横向不做死区,相邻星的边界就是两半分界
  • 一次触摸即取值,不需要二次确认;不响应长按与拖动(拖动选分在触屏上容易滑错,规格未纳入)
  • 已选中的部分用填充比例表达(半星就是左半填充),不能只靠颜色深浅区分
  • 取值后立即触发 change 事件(回传 3.5 这类半星值也是合法输入)
  • readonly=true 时不渲染任何可点区域(只读展示不抢键盘序列)

36.6 无障碍

  • 可评分形态:根是 role="radiogroup" + aria-label,每颗星是原生 button + role="radio" + aria-checked
  • 星形的名称是「N 星」(aria-label),读屏播报的是分值而不是符号
  • 只读形态:整组是一个 role="img" + aria-label(「评分 4.5 分(满分 5 分)」),组内星形 aria-hidden
  • 禁用态用原生 disabled 并补 aria-disabled="true"
  • 键盘:聚焦后左右方向键步进(整星模式步长 1,半星模式步长 0.5)

36.7 doNotInvent

  • 评分的业务含义映射(如「4 分以上算好评」)与统计口径
  • 拖动选分与悬停预览(触屏没有悬停,拖动容易滑错)
  • 评分理由 / 标签的联动采集(由宿主另行组合)
  • 异步提交与失败回滚(宿主负责)

36.8 unknowns

  • 分值是否允许与文案一一对应(如 1 分「很差」、5 分「很好」)
  • 星形换用图标字体或 SVG 后填充比例的表达是否仍然一致
  • 只读形态是否要显示评价条数(当前由宿主用插槽补)


37 · 排版 Typography

37.1 用途

把一段文字按信息层级(标题 / 正文 / 辅助 / 次要)成套地表达,并可选单行省略与一键复制。移动端与桌面端的关键差别是正文基准字号:桌面端正文 14px,移动端 16px(--kole-m-font-size-body),且层级只有四级 —— 手机屏幕上再细分「大标题 / 中标题 / 小标题」会让层级差异小于字号可辨阈值,用户只能看到「一堆差不多大的字」。

37.2 结构(anatomy)

  • typography:根元素,横向承载「文字 + 可选复制按钮」
  • text:文字本体,层级、省略、换行都在它身上生效;默认插槽与 text prop 二者取一(text 优先)
  • copy:复制按钮(原生 button),仅 copyable=true 时出现;热区 44px,负外边距吸收不撑高行

37.3 变体维度

  • level:title(标题 17px / 600)/ body(正文 16px / 400)/ assist(辅助 14px / 次级色)/ secondary(次要 11px / 三级色)
  • ellipsis:false(按容器换行)/ true(单行省略,省略号在行尾)
  • copyable:false(纯展示)/ true(右侧出现复制按钮)

37.4 状态

  • default:常态
  • copied:is-copied —— 复制成功后的反馈(按钮文字换成「已复制」且转成功色),由宿主在写剪贴板后加上

37.5 交互与触控

  • 组件本体是静态的:根与文字节点不带指针语义,也不进键盘序列(读屏只读文字)
  • 唯一的交互元素是复制按钮:热区 44×44px(--kole-m-touch-target),一次轻点触发 copy 事件
  • 复制动作由宿主完成:组件不读系统剪贴板(Web 端 navigator.clipboard 需要用户手势与安全上下文,小程序端完全没有这个 API,跨端无法统一),组件只回传事件并在宿主置 copied=true 后渲染反馈
  • ellipsis=true 时只做单行省略:被截断的内容不提供展开入口,长内容应改用多行展示或详情页
  • 层级只表达信息次序,不做任何按数值/长度自动降级的推断

37.6 无障碍

  • 根是普通容器,文字由读屏按文档流朗读;层级只改视觉,不改语义标签(是否用 h1/p 由宿主决定,组件不替业务决定文档大纲)
  • 省略形态下读屏仍能读到完整文字(text-overflow: ellipsis 只截视觉,不改可访问名)
  • 复制按钮是原生 button 且带 aria-label,名称随状态在「复制」与「已复制」之间切换
  • 复制结果是异步确认的视觉反馈,因此不额外加 aria-live(避免与页面其它播报抢读)

37.7 doNotInvent

  • 富文本与 Markdown 渲染(换行、加粗、链接、代码块一律由宿主负责)
  • 多行省略(line-clamp)与「展开全文」交互
  • 字号缩放 / 用户字号偏好档位
  • 复制失败的兜底提示(宿主自行组合轻提示)

37.8 unknowns

  • secondary 层级是否应使用 11px(当前取 --kole-m-font-size-caption)而不是 12px
  • 复制按钮在「已复制」态停留多久(当前不自动回退,由宿主控制)
  • 省略态是否需要在长按气泡里显示全文


38 · 分段器 Segmented

38.1 用途

在 25 个互斥选项里选一个,并让「当前选的是哪个」一眼可见(订单状态、时间范围、列表/网格视图切换)。移动端与桌面端的关键差别是没有悬停:桌面端用户把鼠标移到未选项上就能预览「这里可以点」,触屏上这个预告不存在 —— 所以选中项必须靠形态(白底滑块 + 阴影)而非色相区分,且每一项都要占满 44px 高,否则 25 个选项挤在一行时手指点不准。

38.2 结构(anatomy)

  • segmented:容器,浅底 + 内边距,横向排列选项;role="radiogroup" 并带 aria-label 说明这组在选什么
  • item:单个选项,原生 button + role="radio"(整项可点、可键盘聚焦),data-value 携带取值
  • label:选项文字(可含装饰图标,图标 aria-hidden 由文字承担语义)

38.3 变体维度

  • size:default(44px 高)/ small(32px 高,卡片内次级筛选用)
  • block:false(宽度随内容)/ true(占满容器且各项等分)

38.4 状态

  • default:未选中(透明底 + 次级字色)
  • selected:选中(is-active —— 卡片底滑块 + 品牌字色 + aria-checked="true")
  • disabled:整段置灰、不响应点击;单个选项也可单独禁用

38.5 交互与触控

  • 选项切换是一次轻点:点击后该项 is-active、同组其它项复位;不响应长按、双击与拖动(横滑切换属于标签栏手势,不在本组件)
  • 选中态是视觉与属性的双向同步:除类名外必须同时更新 aria-checked,只改颜色不改属性会被无障碍判为缺口
  • 每项热区高 ≥ 44px(size=small 时视觉 32px,但触摸命中区仍按 44px 计,纵向不留死区)
  • 选中项再次点击不重复触发 change(值未变不发事件,避免宿主收到同值事件后做无意义的重渲染)
  • 受控:组件不存值,只回传目标值 change;宿主不采纳时视觉不变化
  • 切换动效 120ms(--kole-duration-fast),prefers-reduced-motion 下瞬时切换

38.6 无障碍

  • 容器 role="radiogroup" + aria-label;每个选项原生 button + role="radio" + aria-checked
  • 选项名称由文字承担;装饰性图标必须 aria-hidden="true"
  • 禁用时容器补 aria-disabled="true",选项用原生 disabled(读屏播报不可用,且不进 Tab 序列)
  • 键盘:Tab 进入分组,左右方向键在选项间移动并选中(原生 radio 组的键盘约定)

38.7 doNotInvent

  • 多选(同时选中多个)—— 需要多选时改用标签组或多选框
  • 选项的横向滚动、换行与「更多」折叠(超过 5 个应换组件)
  • 选中项的下划线滑块动画(那是标签栏的视觉语言,不是分段器)
  • 选项禁用条件与业务权限的判断

38.8 unknowns

  • 选项数量上限是否应硬约束在 5 个(当前只写建议,不做运行时拦截)
  • size=small 的命中区是否需要在纵向自动补到 44px(当前靠 --kole-m-hit-slack 思路,未在样式中强制)
  • 是否需要「滑动经过即选中」(当前只认轻点,滑动不选中)


39 · 吸顶容器 Sticky

39.1 用途

让一段内容在滚动时贴住滚动容器的边缘保持可见(列表标题、分组、购物车合计条)。移动端与桌面端的关键差别是滚动惯性:桌面端滚动是离散的滚轮步进,用「监听 scroll + position: fixed」的 JS 方案看不出问题;触屏上滚动是带惯性的连续位移,fixed 方案会在惯性阶段出现一帧的抖动与跳位,且元素脱离文档流后原位置塌陷、必须手动补一个占位元素 —— 所以这里用 CSS 原生的 position: sticky,贴合由浏览器在合成层完成,不监听滚动。

39.2 结构(anatomy)

  • sticky:根元素,就是滚动内容流里的那一行(sticky 不脱离文档流,因此不需要占位元素)
  • title:标题文字,单行省略,占满剩余宽度
  • extra:右侧附加说明(数量、合计等),可选
  • action:右侧动作按钮(原生 button,热区 44px),可选

39.3 变体维度

  • position:top(贴顶,最常用)/ bottom(贴底,合计条一类)
  • safeArea:false(只用偏移量)/ true(偏移量再叠加刘海或底部横条安全区)
  • shadow:false(恒定无投影)/ true(仅吸顶后才有投影,用于区分「贴住了」与「还在流里」)

39.4 状态

  • default:未吸顶,正常参与文档流
  • stuck:is-stuck —— 已贴合边缘(加下边框 / 投影)。判定由宿主负责:贴合时机只有浏览器知道,组件不监听滚动

39.5 交互与触控

  • 贴合位置由组件级变量 --kole-m-sticky-offset 决定,默认等于导航栏高度;业务侧覆盖它即可适配自有导航(贴底用法通常覆盖为 0px)
  • safeArea=true 时偏移叠加 --kole-m-safe-top / --kole-m-safe-bottom:支持 env() 的浏览器拿到真实刘海高度,不支持的拿到 0px(不会因非法值整条声明被丢弃)
  • 组件本身不监听滚动:is-stuck 由宿主按滚动位置切换。原因有两条 —— ① 贴合判定与滚动容器强绑定,组件无法知道自己在哪个容器里;② 逐帧读写滚动位置在移动端会挤占合成线程,原生 sticky 已经把这件事交给浏览器
  • 触屏热区:整条高度 ≥ 44px;右侧动作按钮自身撑满 44px,负外边距吸收不撑高栏体
  • position=bottom 时吸顶后的描边换到上边(视觉上仍是「靠内容那一侧」的分隔)
  • 不响应长按、双击与横滑:吸顶条不是可拖拽元素(拖拽排序不在本组件范围内)

39.6 无障碍

  • 根是普通容器,标题文字正常参与读屏朗读;is-stuck 是纯视觉增强,不添加任何 aria-*(吸顶不是状态变化,播报它只会打断用户)
  • 右侧动作是原生 button,名称由可见文字承担;装饰性图形必须 aria-hidden="true"
  • 吸顶不改变文档顺序:读屏与键盘的遍历顺序与未吸顶时完全一致(这也是不用 fixed 的另一个理由 —— fixed 会它把挪出正常流,键盘聚焦时的滚动定位会跳)
  • prefers-reduced-motion 下不引入任何过渡(组件本身不加动效,此处显式声明避免宿主样式穿透)

39.7 doNotInvent

  • 吸顶触发的位移/缩放动画(如标题从大字缩成小字)—— 规格只定义了贴合,没有定义形态演变
  • 多个吸顶条的层叠顺序与相互推挤(层叠上下文规则由宿主决定)
  • 进入/离开视口时的埋点事件与曝光统计
  • 拖拽排序与吸附

39.8 unknowns

  • 吸顶判定是否应由组件内部提供一个可选的滚动监听辅助(当前完全交给宿主)
  • 是否需要在吸顶时自动隐藏相邻内容(当前不做,靠宿主布局)
  • 贴底形态在内容不足一屏时是否应始终贴底(当前 sticky 语义下会贴着容器底,未做额外分支)


40 · 遮罩层 Overlay

40.1 用途

浮层的基座:在内容之上盖一层半透明遮罩,让下层内容退到背后(对话框、抽屉、图片预览、卡片加载态)。它与弹出层的分工是:弹出层自带方向位移与开合动画、是「能独立使用的完整浮层」;遮罩层只提供「变暗 + 拦手势 + 承载任意内容」三件事,属于被别的浮层复用的底座。移动端与桌面端的关键差别是手势拦截面:触屏上遮罩必须真的吃掉触摸事件(否则惯性滚动会从遮罩底下穿过去,把下层页面滚走),而桌面端只需要处理鼠标点击。

40.2 结构(anatomy)

  • overlay:根元素,定位容器与开合开关;role="presentation",自身不承担语义
  • scrim:遮罩面(原生 button),aria-hidden="true" 且 tabindex="-1" —— 可点但不进键盘序列
  • content:内容容器(默认插槽落点),语义由宿主决定(对话框给 role="dialog"、面板给 role="region")

40.3 变体维度

  • tone:default(普通遮罩)/ strong(浓遮罩,用于需要专注的确认)/ blur(叠加背景模糊,用于图片预览)
  • contained:false(固定全屏)/ true(绝对定位填充最近的定位祖先,做卡内局部遮罩)

40.4 状态

  • closed:收起(opacity: 0 + pointer-events: none,视觉与手势同时让开)
  • open:展开(is-open;pointer-events: auto 拦下所有手势)

40.5 交互与触控

  • 一次轻点遮罩即关闭;closeOnMask=false 时不关闭(填到一半的表单不该因为一次误触丢数据)
  • 遮罩关闭的键盘路径不落在遮罩上:遮罩显式 tabindex="-1",键盘用户靠内容里的关闭按钮(与宿主实现的 Esc)关闭 —— 让 Tab 停在遮罩上会让读屏读到一个没有名称的元素
  • lockScroll=true 时宿主应锁住下层滚动(组件在根上写 data-lock-scroll 作为标记,实际的 overflow: hidden 由宿主执行:组件不该直接改 document.body.style,那会污染宿主状态并在多层浮层叠加时互相踩踏)
  • 开合动效 240ms(--kole-m-duration-slide),prefers-reduced-motion: reduce 下瞬时切换
  • contained=true 时要求宿主祖先链上存在定位元素(position: relative 一类),否则会向上找到视口

40.6 无障碍

  • 遮罩面 aria-hidden="true"(纯装饰)且 tabindex="-1"(不进键盘序列)
  • 根 role="presentation":容器本身不产生语义,避免读屏把它当成一个空的分组
  • 内容语义完全由宿主提供:焦点陷阱、aria-modal、初始焦点与关闭后焦点归还都属于宿主职责(本组件是基座,不替业务决定这些策略)
  • 遮罩关闭不依赖颜色:开合只改透明度且有 240ms 过渡,减少动态偏好下瞬时切换

40.7 doNotInvent

  • 焦点陷阱(focus trap)与初始焦点策略
  • 多层遮罩的层叠顺序管理
  • 手势下滑关闭与拖拽阻尼
  • 滚动锁定的实现细节(组件只写标记,改 DOM 由宿主做)

40.8 unknowns

  • blur 档在低端机上的性能开销是否可接受(未做降级探测)
  • contained=true 时是否应自动为宿主补 position: relative(当前要求宿主自己保证)
  • 是否要支持「点遮罩不关但双击关」这类折中策略


41 · 弹出气泡 Popover

41.1 用途

在某个元素的旁边弹出一小块说明或轻量操作(运费规则、字段解释、更多操作),说完就收。移动端与桌面端的关键差别是触发方式与关闭路径:桌面端靠 hover 弹出、移开即收;触屏没有悬停,只能点击展开,而展开后「怎么收起来」变成了真问题 —— 桌面上移开鼠标就收了,触屏必须显式给一条关闭路径(点击气泡之外)。这也是它与轻提示 / 遮罩层的根本区别:气泡是相对某个触发元素定位的,不是铺满视口的浮层。

41.2 结构(anatomy)

  • popover:根元素,position: relative 的包一层,气泡在其中绝对定位(因此永远贴着自己的触发器)
  • trigger:触发器(原生 button),热区 ≥44px,写 aria-expanded 与 aria-haspopup
  • panel:气泡面板,按 placement 贴着触发器的某一侧;role="dialog" 并带 aria-label
  • title / text:标题与正文,可选
  • 箭头:arrow=true 时由纯 CSS 三角(border 拼出,无图片、无 hex)指向触发器

41.3 变体维度

  • placement:top / bottom / left / right(气泡相对触发器出现在哪一侧)
  • arrow:false(不带三角,小屏空间紧张时用)/ true(三角指向触发器)

(closeOnOutside 是行为开关而不是形态维度,见 §41.5:它只改变关闭路径,不改变任何视觉。)

41.4 状态

  • closed:收起(opacity: 0 + visibility: hidden + pointer-events: none,不可见也不可点)
  • open:展开(is-open;触发器同步 aria-expanded="true" 并转为品牌色描边)

41.5 交互与触控

  • 一次轻点触发器展开 / 收起,同一个按钮负责开与关(触屏没有「移开鼠标」这条隐式关闭路径)
  • 点击外部关闭:落点不在「气泡或触发器」之内时收起。气泡没有遮罩可依赖(有遮罩就成了弹出层),判定靠监听文档级 pointerdown;uni-app 端没有 document,改用铺满视口的透明捕获层(层级低于面板)
  • 不响应长按、双击与拖动;气泡自身不消费纵向滚动 —— 内容超长应改用弹出层,不在气泡里做内部滚动
  • 气泡与触发器之间留 --kole-m-popover-gap(默认 8px)的间隙,避免气泡盖住触发器的按下反馈
  • 开合动效 120ms(--kole-duration-fast),减少动态偏好下瞬时切换
  • 边缘空间不足时的翻转(flip)由宿主决定:把 placement 换成对侧即可,组件不做自动测量

41.6 无障碍

  • 触发器是原生 button,带 aria-expanded(读屏能播报「已展开 / 已折叠」)与 aria-haspopup
  • 面板 role="dialog" + aria-label:名称取 title,为空时退回触发器文字,保证读屏不会读到一个匿名对话框
  • 弹出时不移动焦点(气泡是补充说明而非中断式浮层,抢焦点会让用户丢失当前输入位置);键盘用户用 Tab 进入气泡内容,用 Esc 或再点一次触发器收起
  • 气泡不因展开而隐藏任何内容:关闭后触发器仍在原地可再次打开

41.7 doNotInvent

  • 基于可用空间的自动翻转与自动方位选择(placement 由宿主决定)
  • 悬停触发(触屏没有悬停;桌面端若需要,由宿主包装)
  • 气泡内的表单校验与提交流程
  • 气泡之间的互斥开关(哪个开着由宿主管理)

41.8 unknowns

  • 是否需要在气泡贴近视口边缘时自动夹在边界内(当前只做静态定位)
  • role="dialog" 对纯文字说明类气泡是否过重(当前统一用 dialog + aria-label)
  • 点击外部关闭是否需要区分「点了另一个气泡」的情况(当前点另一个气泡会同时收起前一个)


42 · 消息通知 Message

42.1 用途

在页面顶部给一条不打断操作的结果提示(提交成功、网络异常、库存告警、操作回执)。移动端与桌面端的关键差别是不阻断:桌面端顶部提示常做成整条横幅、推进页面布局;触屏上页面本身就是有限的可视区域,横幅会把内容挤下去并引发重排,所以这条消息浮在内容之上(position: fixed)且不吃手势(层自身 pointer-events: none),用户读完继续操作,页面不跳。它与轻提示 Toast 的分工是:Toast 占据视口中央、用于「结果就是你此刻唯一关心的事」;Message 贴在顶部一条、用于「结果要告诉你,但不该拦着你」。

42.2 结构(anatomy)

  • message:消息层根元素,顶部固定的纵向列表容器;pointer-events: none 让下方页面照常可点
  • item:单条消息,一行「图标 + 文字(+ 可选关闭)」,带 role="status" 与 aria-live="polite"
  • icon:语气图标(装饰),aria-hidden="true",语义由文字承担
  • text:消息文字,超长换行,不截断
  • close:可选的关闭按钮(原生 button,热区 44px)

42.3 变体维度

  • tone:info(信息)/ success(成功)/ warning(警告)/ error(错误)—— 图标字形与侧边描边同族,文字保持正文色
  • closable:false(读完自己消失)/ true(右侧出现关闭按钮)

42.4 状态

  • closed:收起(opacity: 0 + 上移 100%,不可见)
  • open:展开(is-open,滑下淡入 240ms)

42.5 交互与触控

  • 两种用法,API 只暴露组件形态:
    • 组件形态(本组件提供的接口):受控 open + 内容 props;duration 到点回传 close,由宿主决定是否收起
    • 命令式调用(宿主侧组装):宿主维护一个消息数组与定时器,把 open / tone / text 逐个喂给组件实例 —— 命令式 API 需要单例容器、跨端定时器与「销毁后仍在计时的定时器」治理,属宿主或框架层职责,组件不提供全局方法(规格 §42.7)
  • 消息层不吃手势(根 pointer-events: none),只有关闭按钮自己接收手势(pointer-events: auto)——因此顶部有消息时,下方页面仍可正常点击与滚动
  • 自动消失时长由宿主决定:组件没有默认时长之外的隐式行为,duration=0 表示不自动关闭(需要用户读完的长文案)
  • 入场从上滑下并淡入 240ms(--kole-m-duration-slide),减少动态偏好下瞬时切换
  • 多条并列时纵向排列,新的追加在下方;同屏条数上限与超出后的合并策略由宿主决定(规格 §42.7)

42.6 无障碍

  • 每条消息 role="status" + aria-live="polite":读屏朗读一次,不打断用户当前操作(不用 assertive,那会打断朗读)
  • 语气图标 aria-hidden="true",语义全部由文字承担(颜色不是唯一的信息通道:图标字形与描边同时变化)
  • 关闭按钮是原生 button 并带 aria-label(「关闭消息」),可用 Tab 聚焦、Enter 触发
  • 消息不抢焦点:出现时不移动焦点,用户正在输入的内容不受影响

42.7 doNotInvent

  • 自动关闭的默认时长与「超时后是否保留」的策略
  • 多条消息的排队、合并、去重与同屏上限
  • 命令式全局方法(Message.success() 这类)与其单例容器
  • 消息内的操作按钮与跳转链接(需要动作时请用通知栏或对话框)

42.8 unknowns

  • tone=warning 与 error 是否需要不同的停留时长(当前统一由宿主传 duration)
  • 顶部多条同时出现时是否该限制为最多两条(当前不限,由宿主控制)
  • 是否需要在消息层上提供「点整条跳详情」的交互(当前只有可选的关闭按钮可点)


43 · 选择器 Picker

43.1 用途

从一组有限且已知的选项里选出一项或几项(城市、分值、时间),是「表单里那个下拉框」在触屏上的形态;移动端与桌面端的关键差别是没有悬停、也没有空格去展开:下拉框在手机上只能变成从底部升起的滚轮浮层,手指点选即高亮,并且必须有明确的「确定 / 取消」来收口 —— 桌上点一下就走、触屏上误触代价大,用户需要一次反悔的机会。可绑定到 Popup 的底部形态(position: bottom),但本组件自带遮罩与面板,不要在弹出层里再套一层。

43.2 结构(anatomy)

  • mask:遮罩,点击关闭(closeOnMask=false 时不关)
  • picker:底部浮层面板,role="dialog" + aria-modal="true"
  • picker__header:取消 / 标题 / 确定三格,标题元素由 aria-labelledby 指向
  • picker__columns:列容器,mode=single 一列、mode=multiple 多列等分
  • picker__column:单列,role="listbox" + aria-label 说明这列在选什么,可滚动
  • picker__option:单个选项,role="option" + aria-selected,行高不小于 44px

43.3 变体维度

  • mode:single(单列滚轮)/ multiple(多列滚轮,各列独立选中;是否联动由宿主的选项决定)
  • round:false(直角)/ true(靠内容一侧切圆角)

43.4 状态

  • default:常态
  • open:浮层展开(遮罩可点、面板滑入)
  • closed:收起态(遮罩 pointer-events: none,页面可正常滚动与点击)
  • selected:当前选中项,is-selected 与 aria-selected="true" 同步表达
  • disabled:整块或单个选项置灰且不响应,写 aria-disabled="true"(整块禁用用状态类 is-disabled)

43.5 交互与触控

  • 遮罩点击关闭;closeOnMask=false 时不关闭
  • 选项行高不小于 44px,滚动容器 -webkit-overflow-scrolling: touch
  • 点击选项只改本列高亮(同列其余项取消高亮),不改宿主的值;确认时才提交
  • 确定 / 取消按钮热区不小于 44px
  • 取消防返回:点取消或遮罩丢弃本次点选,宿主侧的值回到打开前的状态

43.6 无障碍

  • 浮层 role="dialog" + aria-modal="true",标题元素 id 由 aria-labelledby 指向
  • 每列 role="listbox" + aria-label,选项 role="option" + aria-selected
  • 遮罩 aria-hidden="true"(纯装饰,读屏不播报)
  • 选中值以文本呈现在标题里(不依赖视觉滚动位置),收起态写 aria-hidden="true"
  • 禁用项写 aria-disabled="true",读屏播报不可用

43.7 doNotInvent

  • 选项数据源与联动规则(由宿主传入 columns,本组件不发明城市库或级联关系)
  • 滚轮惯性 / 吸附动画的物理参数
  • 搜索过滤与键盘输入定位(那是 Input / Search 的职责)
  • 多选(一次选多个值)——本组件是「多列各选一项」,不是「一列选多项」

43.8 unknowns

  • 面板最大高度是否应随列数增长(当前固定 max-height 一列 200px)
  • 是否要支持「不选」的空值项
  • 列数上限(当前实现不限制,但三列以上在 375px 宽度下每列会很窄)


44 · 级联选择器 Cascader

44.1 用途

在有层级关系的选项里逐级选到末级(省 → 市 → 区、品类 → 子品类 → SKU),移动端与桌面端的最大差别是没有横向空间:桌面上三列并排一眼就能看到全路径,375px 宽的手机上并排三列每列只剩 100px 出头,文字全部折行不可读。因此移动端一次只展示当前一层,已选路径收进上方路径条,靠路径条回退而不是靠「上一级」按钮 —— 那会多一次点击。

44.2 结构(anatomy)

  • cascader:根元素,mode=panel 时内嵌在页面里,mode=popup 时是底部浮层
  • mask:遮罩(仅 mode=popup),点击关闭
  • cascader__header:取消 / 标题 / 确定三格(仅 mode=popup)
  • cascader__path:路径条,按已选深度渲染;除末位外都可点,点了回退到该级
  • cascader__panel:选项区,role="listbox" + aria-label 说明当前在选第几级、选什么
  • cascader__option:单个选项,role="option" + aria-selected;有下级显示 ›,叶子显示 ✓

44.3 变体维度

  • mode:panel(内嵌在页面里)/ popup(底部浮层,自带遮罩与确定取消)
  • showPath:true(显示路径条,可回退)/ false(不显示,层级少的场景省一行高度)
  • round:false(直角)/ true(浮层靠内容一侧切圆角)

44.4 状态

  • default:常态(停在根级)
  • selected:当前路径上的项,is-selected 与 aria-selected="true" 同步
  • open / closed:仅 mode=popup,浮层展开 / 收起(收起时遮罩 pointer-events: none)
  • disabled:整块或单个结点置灰且不响应,aria-disabled="true"
  • empty:当前级没有可选项时显示占位文案

44.5 交互与触控

  • 点某一级后下一级选项随之变化:面板始终只渲染当前一层,选择即下钻
  • 点路径条里的上级可回退:回退到该级并重新展示它的下一级;末位是「你在这里」的锚点,不可点
  • 级数不写死:路径条按已选深度渲染,两级与四级用同一份实现
  • 点到叶子结点后再点同级另一项,会截断更深的层级(改选不会留下旧的深层残留)
  • 选项行高与路径条各项热区均不小于 44px;选项区滚动容器 -webkit-overflow-scrolling: touch

44.6 无障碍

  • mode=panel 根元素 role="group" + aria-label;mode=popup 根元素 role="dialog" + aria-modal="true"
  • 选项区 role="listbox",aria-label 随层级变化(如「级联选项 · 城市」),读屏能播报当前在第几级
  • 选项 role="option" + aria-selected;禁用项写 aria-disabled="true"
  • 路径条末位写 aria-current="true",读屏播报「当前项」
  • 路径条每一项是原生 button(键盘可达),末位用 disabled 让键盘跳过

44.7 doNotInvent

  • 层级数据源(由宿主传入 options 树,本组件不发明省市区库)
  • 搜索定位某级选项(那需要把整棵树拍平,是独立的检索组件)
  • 异步逐级加载的占位与重试流程
  • 多选(一次选多条路径)

44.8 unknowns

  • 路径很长时路径条是折行还是横向滚动(当前折行,四级以上会占两行)
  • 叶子被选中后是否要自动收起浮层(当前不自动收,等宿主决定)
  • 是否要保留「上一级」按钮作为路径条之外的第二种回退入口


45 · 颜色选择器 ColorPicker

45.1 用途

从一组预设色里挑一个颜色(主题色、标签色、看板分类色),或在预设之外补一个自定义色值;移动端与桌面端的关键差别是没有取色器画布的位置,也没有精确拖拽的精度:375px 宽下色相环只有一百多像素、手指覆盖几十像素,拖出来的色值几乎不可复现。因此移动端默认只给预设色板——色值是离散的、可预期的,选中与否一眼可辨;需要自由取色时退化为「输入色值」而不是「拖色相」。

45.2 结构(anatomy)

  • colorpicker:根元素,承载当前色与色板
  • colorpicker__current:当前色显示,由「圆点 + 色值文字」组成
  • colorpicker__dot:当前色圆点,背景取 --kole-m-colorpicker-current(色值是数据,由宿主以行内自定义属性传入)
  • colorpicker__value:色值文字(等宽字),读屏靠 aria-label 而不是色块本身
  • colorpicker__swatches:色板,role="radiogroup" + aria-label,定列数网格
  • colorpicker__swatch:单个色块,role="radio" + aria-checked,背景取 --kole-m-colorpicker-swatch
  • colorpicker__custom:自定义色值输入区(仅 mode=custom),由宿主以插槽填入

45.3 变体维度

  • mode:swatch(只有预设色板)/ custom(色板之后追加自定义色值输入)
  • round:false(方形色块)/ true(圆形,主题选择器常用)
  • showValue:true(显示色值文字)/ false(只留色块,空间紧张时用)
  • columns:色板每行列数(默认 6,通过 --kole-m-colorpicker-columns 覆盖)

45.4 状态

  • default:常态
  • selected:当前选中色块,is-selected 与 aria-checked="true" 同步;视觉是外环 + 轻微放大,不是换色 —— 浅色块(白、浅黄)上换色根本看不出来
  • disabled:整块置灰且不响应;色块写 aria-disabled="true" 并用原生 disabled 让键盘跳过

45.5 交互与触控

  • 点色块即选中并回传该色;同组互斥(选新的自动取消旧的)
  • 色块视觉 ≥32px、热区按网格列宽铺满,点空白处不响应
  • 选中反馈是外环 + scale(1.08),变换用 --kole-duration-fast;减少动态偏好下不放大
  • mode=custom 时色值输入只在确认(点「应用」或回车)后生效,输入过程中不回传
  • 色值非法时输入框置红并给出错误文案,不自动纠正(不发明「就近取色」这类行为)

45.6 无障碍

  • 色板 role="radiogroup" + aria-label 说明这组在选什么(如「主题色」)
  • 每个色块 role="radio" + aria-checked,aria-label 必须是色值本身(如 #2F54EB)—— 色块没有文字,读屏只能靠 aria-label
  • 当前色以等宽文字重复一遍:颜色不作为唯一信息通道(色觉障碍用户读得到色值)
  • 禁用块写 aria-disabled="true",读屏播报不可用

45.7 doNotInvent

  • 色相环 / 明度滑条的取色画布(移动端精度不足,本组件不提供)
  • 颜色空间转换(RGB / HSL / HSV 互转)与色值自动纠正(只接受 #RRGGBB)
  • 主题派生(由品牌色自动生成 hover / active / 浅底等衍生色阶)
  • 取色器(吸管)与屏幕取色

45.8 unknowns

  • 色板上限(当前不限制数量,但 40 个以上在小屏上会变成一片色噪)
  • 自定义色值的格式是否要支持 rgb() / hsl() 书写
  • 是否需要「最近使用」一行动态色块


46 · 上传 Upload

46.1 用途

把手机里的文件交给服务端(实名认证的身份证照、报销的发票、工单的附件)。移动端与桌面端的关键差别是没有拖拽:桌面上可以「把文件拖进虚线框」,手机上既没有 hover 也没有拖放,所以触发按钮是唯一入口,虚线框只能表达「这里可以放东西」而不能作为交互方式;另一个差别是相机——很多上传场景其实期望的是「拍一张」,宿主可以用同一个触发器同时给出「拍照」与「从相册选择」,但那是宿主的编排,不是本组件的职责。

46.2 结构(anatomy)

  • upload:根元素,纵向排列「标题行 + 触发器 + 文件列表」
  • upload__header:标题与计数(已选 N / 上限 M)
  • upload__trigger:选择触发器,点击调起系统文件选择器;虚线框是 variant=dashed,实心按钮是 variant=button
  • upload__list / upload__item:文件列表与单行,行高不小于 44px
  • upload__thumb:缩略图位(图片用背景图,其他类型放扩展名文字块)
  • upload__body / upload__name / upload__meta:文件名与「进度条 + 状态文案」
  • upload__track / upload__bar:进度条轨道与进度段,宽度取 --kole-m-upload-percent
  • upload__actions:行内动作(重试 / 删除),各自 44px 热区

46.3 变体维度

  • variant:dashed(虚线框,页面级上传区)/ button(实心按钮,列表内嵌的「+ 添加」)
  • maxCount:文件数上限(到达上限后触发器变成提示行,不再可点)

46.4 状态

  • pending:待上传(进度条不占宽,文案「待上传」)
  • uploading:上传中(品牌色进度条,文案给百分比;role="progressbar" + aria-valuenow)
  • success:成功(满格 + 成功色 +「已上传」)
  • error:失败(满格 + 错误色 + 失败原因,行内出现「重试」)
  • empty:还没有任何文件时的说明文案
  • disabled:整块置灰(状态类 is-disabled,触发器与删除按钮都不可用);到 maxCount 后触发器变提示行

46.5 交互与触控

  • 点触发器调起系统文件选择器(本组件不发请求,选完由宿主拿文件并自行上传)
  • 删除按钮自己 44px 热区,点一次移除该项;删除进行中(uploading)时文案是「取消」
  • 失败行提供「重试」,重试由宿主重新发起,组件只回传下标
  • 进度不自己走:没有内置定时器或假进度,百分比全部由宿主回传,避免出现「看起来在上传其实没动」
  • 列表为空时显示空态文案;到 maxCount 后触发器改成不可点的提示行

46.6 无障碍

  • 触发器是原生 button,带 aria-disabled;到上限时用 disabled 让键盘跳过
  • 进度条写 role="progressbar" + aria-valuemin / aria-valuemax / aria-valuenow,aria-label 说明是哪个文件的进度
  • 删除 / 重试按钮各自带 aria-label(含文件名),读屏播报「删除 合同扫描件.pdf」而不是孤零零的「删除」
  • 状态文案是文字而不只是颜色(「已上传」「上传失败」),颜色不作为唯一信息通道

46.7 doNotInvent

  • 真实的传输:请求、分片、断点续传、并发数(本组件只回传事件,不发任何请求)
  • 图片压缩、裁剪、水印与方向纠正
  • 服务端的校验规则(大小上限、类型白名单以文案与 accept 表达,不代为判断)
  • 拍照与相册的原生调起(由宿主在 select 事件里自行调用平台 API)

46.8 unknowns

  • 上传中能否同时继续添加文件(当前允许,列表各自独立)
  • 失败自动重试的次数与退避策略
  • 是否需要在成功行上展示服务端返回的文件 id / URL


47 · 表格 Table

47.1 用途

把多条同构记录按列并排,用于「订单列表 / 库存明细 / 对账单」这类需要逐行横向对比的场合;移动端与桌面端的关键差别是宽度根本不够:桌面上 8 列一眼看完,375px 的手机上同样 8 列每列只剩 47px,文字全部折断。因此移动端表格必须做「在组件自己的容器里横向滚动」——绝不能让页面整体横向溢出,否则整页会跟着左右晃、顶栏与底部栏一起位移;列更多或更该读字段名时改用 mode=card,把每行摊成键值对,彻底不需要横滚。

47.2 结构(anatomy)

  • table:根元素,纵向排列「视口 + 空态」
  • table__viewport:横向滚动的唯一发生点(overflow-x: auto),页面整体不横向溢出
  • table__inner:原生 table,min-width 由 --kole-m-table-min-width 给出;超过容器即滚
  • table__caption:可选表格标题(说明这张表在讲什么)
  • table__head / table__head-cell:表头与表头单元格,position: sticky 固定在滚动视口顶部
  • table__row / table__cell:数据行与单元格,行高不小于 44px;单元格可带 data-label(卡片模式的字段名来源)
  • table__empty:空态占位,role="status" + aria-live="polite"

47.3 变体维度

  • mode:scroll(横向滚动,列多时用)/ card(每行摊成键值对,小屏彻底不横滚)
  • size:default(常规行高)/ compact(收紧内边距与字号;行高仍不小于 44px)
  • stripe:false(纯白底)/ true(偶数行浅底,便于横向扫读时对齐行)
  • bordered:false(只有横线)/ true(单元格之间也有竖线)

47.4 状态

  • default:常态
  • selected:当前选中行,is-selected 与 aria-selected 同步(行选中不是复选框,单行高亮即可)
  • disabled:整行置灰且不响应,写 aria-disabled="true"
  • empty:没有数据时的占位文案(列宽语义不变,不会把容器撑宽)
  • clickable:行可点(clickable=true 时整行都是热区,按下反馈是整行背景变化)

47.5 交互与触控

  • 横向滚动发生在 table__viewport 上:根与视口都写 min-width: 0,否则内部 min-width 会把父级顶宽、页面整体横向溢出
  • 选项行高不小于 44px;触屏上横向滑动不被父级纵向滚动吞掉(touch-action: pan-x pan-y)
  • 表头用 position: sticky 而不是 fixed——fixed 会脱出滚动容器,横向滚动时表头不跟随
  • 长文本列默认单行省略,完整值放 title;数字列用等宽字右对齐,便于按位对比
  • 点整行即选中(同表互斥);行内的独立控件(按钮 / 链接)要阻止冒泡,避免一次点击触发两个动作
  • 行可点时同时提供键盘路径(聚焦后回车 / 空格),不把鼠标点击当成唯一入口

47.6 无障碍

  • 用原生 table / thead / tbody / tr / th / td,表头写 scope="col"(读屏能报出列名)
  • 空态用 role="status" + aria-live="polite" 播报占位文案
  • 禁用行写 aria-disabled="true",读屏播报不可用
  • 可点行必须同时可聚焦:clickable=true 时行写 tabindex="0" + role="row" + aria-selected,聚焦后回车 / 空格即选中 —— 只有鼠标点击而没有键盘路径是缺陷,不是简化
  • 焦点环画在行内第一个单元格上(tr 上的 outline 在部分浏览器不渲染)
  • 行内的独立控件(链接 / 按钮)有可见焦点环,并阻止冒泡
  • mode=card 时字段名由 data-label 生成并参与读屏(键值对在视觉与语义上都成立)
  • 可点行的 cursor: pointer 只给表体行:表头行不承载点击,跟着变手型会把「这里能点」这个信号弄错

47.7 doNotInvent

  • 虚拟滚动与无限加载的触发规则(由宿主容器负责;本组件不接管纵向滚动)
  • 服务端排序 / 筛选 / 分页(排序图标与请求都由宿主发起)
  • 列宽拖拽调整与列显隐设置(移动端没有这个操作面)
  • 冻结列(position: sticky 的横向版本;当前只固定表头的纵向位置)

47.8 unknowns

  • 表头是否需要在纵向滚动时也吸顶(当前只在宿主给固定高度时生效)
  • 行选中是单选还是多选(当前单选;多选应交给 Checkbox 列)
  • 卡片模式下字段名的排列是「左标签右值」还是「上标签下值」