# 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/.html` | `tests/mobile/.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-` 或状态类 `is-`。 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`(内容为姓名) - 图片头像直接用 ``,`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 列) - 卡片模式下字段名的排列是「左标签右值」还是「上标签下值」 ---