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

2213 lines
107 KiB
Markdown
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.
# 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 · 链接 Link
### 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 · 搜索框 Search
### 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 用途
在 2~5 个**互斥**选项里选一个,并让「当前选的是哪个」一眼可见(订单状态、时间范围、列表/网格视图切换)。移动端与桌面端的关键差别是**没有悬停**:桌面端用户把鼠标移到未选项上就能预览「这里可以点」,触屏上这个预告不存在 —— 所以选中项必须靠**形态**(白底滑块 + 阴影)而非色相区分,且每一项都要占满 44px 高,否则 2~5 个选项挤在一行时手指点不准。
### 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 列)
- 卡片模式下字段名的排列是「左标签右值」还是「上标签下值」
---