iter: d01 新增下拉菜单 menu 组件(弹层/键盘导航/reduced-motion)
This commit is contained in:
+9
-1
@@ -56,6 +56,14 @@
|
|||||||
"acceptance": "TextFieldProps / TextareaProps 新增可选 disabled(boolean | Signal<boolean>),禁用时不响应输入、置灰 + not-allowed 光标;demo 禁用示例区补一个信号绑定输入框;manifest/文档/llms-full.txt 同步;npm run check 全绿",
|
"acceptance": "TextFieldProps / TextareaProps 新增可选 disabled(boolean | Signal<boolean>),禁用时不响应输入、置灰 + not-allowed 光标;demo 禁用示例区补一个信号绑定输入框;manifest/文档/llms-full.txt 同步;npm run check 全绿",
|
||||||
"status": "done",
|
"status": "done",
|
||||||
"notes": "与 c01 同模式:disabled 透传 h() 即得布尔/信号双支持;禁用态 CSS 追加 .ua-input / .ua-textarea;demo 复用 locked 信号联动开关与输入框"
|
"notes": "与 c01 同模式:disabled 透传 h() 即得布尔/信号双支持;禁用态 CSS 追加 .ua-input / .ua-textarea;demo 复用 locked 信号联动开关与输入框"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "d01",
|
||||||
|
"title": "新增下拉菜单 menu 组件",
|
||||||
|
"motivation": "工具栏/行操作场景缺省可折叠的动作集合,select 只能选值不能派发动作;可复用自绘弹层与微动画既有模式",
|
||||||
|
"acceptance": "packages/skin/ui 新增 menu.ts:触发器 + 自绘弹层,items 支持 disabled / danger / divider,点条目 / 点外部 / Esc 关闭,↑↓ 环形导航(禁用项跳过),Enter/Space 选中,Esc 收起后焦点回触发器;只消费 --ua-* 令牌并带 prefers-reduced-motion 兜底;导出 + manifest + 文档 + llms-full.txt + demo 同步;npm run check 全绿",
|
||||||
|
"status": "done",
|
||||||
|
"notes": "API menu({ label, items, variant, align, onSelect });MenuItem = { value, label, disabled?, danger?, divider? };弹层 ua-open 同时挂在根与弹层(根管箭头旋转、弹层管出入场),危险条目取 danger 色;键盘 ↑↓ 环形导航 + Esc 回焦触发器;demo 新增 sec-menu 双示例(含禁用/危险/分隔线与右对齐)"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
+51
-3
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"$schema": "ui-arch-registry-index/0.1",
|
"$schema": "ui-arch-registry-index/0.1",
|
||||||
"generatedAt": "2026-10-23T08:10:07.974Z",
|
"generatedAt": "2026-10-23T08:45:45.013Z",
|
||||||
"repoRoot": ".",
|
"repoRoot": ".",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
@@ -130,7 +130,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@ui-arch/ui",
|
"name": "@ui-arch/ui",
|
||||||
"type": "skin-dom",
|
"type": "skin-dom",
|
||||||
"description": "DOM 皮肤组件库:button / textField / textarea / checkbox / switch / select / radioGroup / card / badge / alert / toast / tooltip / skeleton / spinner / progress / tabs / pagination / modal / form / formItem / dataTable,构建在 @ui-arch/frame 与 @ui-arch/table-core 之上,样式只消费 --ua-* 令牌。",
|
"description": "DOM 皮肤组件库:button / textField / textarea / checkbox / switch / select / menu / radioGroup / card / badge / alert / toast / tooltip / skeleton / spinner / progress / tabs / pagination / modal / form / formItem / dataTable,构建在 @ui-arch/frame 与 @ui-arch/table-core 之上,样式只消费 --ua-* 令牌。",
|
||||||
"tags": [
|
"tags": [
|
||||||
"components",
|
"components",
|
||||||
"dom",
|
"dom",
|
||||||
@@ -148,6 +148,7 @@
|
|||||||
"checkbox(props):复选框,checked 信号双向绑定,disabled 支持布尔或信号禁用",
|
"checkbox(props):复选框,checked 信号双向绑定,disabled 支持布尔或信号禁用",
|
||||||
"switchButton(props):开关,checked 信号双向绑定,disabled 支持布尔或信号禁用",
|
"switchButton(props):开关,checked 信号双向绑定,disabled 支持布尔或信号禁用",
|
||||||
"select(props):自绘下拉选择(非原生 select),value 信号双向绑定,点外部 / Esc 关闭,选项弹层带微动画",
|
"select(props):自绘下拉选择(非原生 select),value 信号双向绑定,点外部 / Esc 关闭,选项弹层带微动画",
|
||||||
|
"menu(props):下拉菜单,触发器 + 自绘弹层,条目支持禁用 / 危险 / 分隔线,↑↓ 环形导航,点外部 / Esc 关闭",
|
||||||
"radioGroup(props):单选组,options + value 信号双向绑定",
|
"radioGroup(props):单选组,options + value 信号双向绑定",
|
||||||
"card(props, ...children):卡片容器",
|
"card(props, ...children):卡片容器",
|
||||||
"badge(props):徽标,variant = default | success | warning | danger",
|
"badge(props):徽标,variant = default | success | warning | danger",
|
||||||
@@ -172,6 +173,8 @@
|
|||||||
"SwitchProps",
|
"SwitchProps",
|
||||||
"SelectProps",
|
"SelectProps",
|
||||||
"RadioGroupProps",
|
"RadioGroupProps",
|
||||||
|
"MenuItem",
|
||||||
|
"MenuProps",
|
||||||
"OptionItem",
|
"OptionItem",
|
||||||
"CardProps",
|
"CardProps",
|
||||||
"BadgeProps",
|
"BadgeProps",
|
||||||
@@ -198,7 +201,7 @@
|
|||||||
"语义色 variant 四档:default|info / success / warning / danger,一律来自令牌,禁止硬编码颜色"
|
"语义色 variant 四档:default|info / success / warning / danger,一律来自令牌,禁止硬编码颜色"
|
||||||
],
|
],
|
||||||
"docs": {
|
"docs": {
|
||||||
"intro": "20 个 DOM 组件,统一用法:组件是普通函数,传入 props 返回 DOM 节点;需要状态时先创建 signal 再双向绑定。引入 CSS:import '@ui-arch/ui/ui.css'(内部已消费 tokens)。",
|
"intro": "21 个 DOM 组件,统一用法:组件是普通函数,传入 props 返回 DOM 节点;需要状态时先创建 signal 再双向绑定。引入 CSS:import '@ui-arch/ui/ui.css'(内部已消费 tokens)。",
|
||||||
"steps": [
|
"steps": [
|
||||||
"安装依赖:pnpm add @ui-arch/ui @ui-arch/frame(workspace 内自动链接)",
|
"安装依赖:pnpm add @ui-arch/ui @ui-arch/frame(workspace 内自动链接)",
|
||||||
"引入样式:在应用入口 import '@ui-arch/ui/ui.css',暗色主题在根元素设 data-theme='dark'",
|
"引入样式:在应用入口 import '@ui-arch/ui/ui.css',暗色主题在根元素设 data-theme='dark'",
|
||||||
@@ -421,6 +424,51 @@
|
|||||||
"选中项在弹层中用淡色弱化标记,不抢视觉焦点"
|
"选中项在弹层中用淡色弱化标记,不抢视觉焦点"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "menu",
|
||||||
|
"title": "下拉菜单 Menu",
|
||||||
|
"demo": "sec-menu",
|
||||||
|
"props": [
|
||||||
|
{
|
||||||
|
"n": "label",
|
||||||
|
"t": "string",
|
||||||
|
"r": 1,
|
||||||
|
"d": "触发器文案"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"n": "items",
|
||||||
|
"t": "MenuItem[]",
|
||||||
|
"r": 1,
|
||||||
|
"d": "菜单条目:{ value, label, disabled?, danger?, divider? }"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"n": "variant",
|
||||||
|
"t": "'primary' | 'ghost'",
|
||||||
|
"d": "触发器视觉强度;缺省 ghost",
|
||||||
|
"df": "ghost"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"n": "align",
|
||||||
|
"t": "'start' | 'end'",
|
||||||
|
"d": "弹层对齐方向;缺省 start(与触发器左对齐)",
|
||||||
|
"df": "start"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"n": "onSelect",
|
||||||
|
"t": "(value: string) => void",
|
||||||
|
"k": 1,
|
||||||
|
"d": "选中回调,禁用项不触发"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"example": "menu({ label: '操作', items: [{ value: 'edit', label: '编辑' }, { value: 'del', label: '删除', danger: true }], onSelect: (v) => console.log(v) })",
|
||||||
|
"steps": [
|
||||||
|
"准备 items 数组:value 是回调标识、label 是展示文案,插一条 { divider: true } 即可分出条目组",
|
||||||
|
"传 label 决定触发器文案;默认 ghost 轻量样式,主操作用 variant: 'primary'",
|
||||||
|
"点触发器展开弹层,点条目收起并触发 onSelect;点外部或按 Esc 也会收起(Esc 后焦点回到触发器)",
|
||||||
|
"键盘:Tab 聚焦触发器,↑↓ 展开并在可用条目间环形移动,Enter / Space 选中;disabled 项自动跳过",
|
||||||
|
"靠右对齐避免溢出时传 align: 'end';危险操作给条目加 danger: true"
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "radioGroup",
|
"id": "radioGroup",
|
||||||
"title": "单选组 RadioGroup",
|
"title": "单选组 RadioGroup",
|
||||||
|
|||||||
@@ -5,3 +5,4 @@
|
|||||||
{"time":"2026-10-23T16:03:46+08:00","id":"b05","result":"done","summary":"文档站核查确认已 manifest 驱动;补齐默认值信息量缺口:CLI propsTable 新增默认值列(df 字段)+ llms-full 默认标注,16 处默认值提取;check 全绿"}
|
{"time":"2026-10-23T16:03:46+08:00","id":"b05","result":"done","summary":"文档站核查确认已 manifest 驱动;补齐默认值信息量缺口:CLI propsTable 新增默认值列(df 字段)+ llms-full 默认标注,16 处默认值提取;check 全绿"}
|
||||||
{"time":"2026-10-23T16:08:17+08:00","id":"c01","result":"done","summary":"button/checkbox/switchButton 补 disabled(boolean|Signal),复用 h() 布尔/信号既有语义零辅助代码;CSS 通用禁用态 + demo 信号联动示例;check 全绿"}
|
{"time":"2026-10-23T16:08:17+08:00","id":"c01","result":"done","summary":"button/checkbox/switchButton 补 disabled(boolean|Signal),复用 h() 布尔/信号既有语义零辅助代码;CSS 通用禁用态 + demo 信号联动示例;check 全绿"}
|
||||||
{"time":"2026-10-23T16:10:31+08:00","id":"c02","result":"done","summary":"textField/textarea 补 disabled(boolean|Signal),与 c01 同模式;禁用态 CSS 覆盖输入类;demo locked 信号联动;check 全绿"}
|
{"time":"2026-10-23T16:10:31+08:00","id":"c02","result":"done","summary":"textField/textarea 补 disabled(boolean|Signal),与 c01 同模式;禁用态 CSS 覆盖输入类;demo locked 信号联动;check 全绿"}
|
||||||
|
{"time": "2026-10-23T16:55:00+08:00", "id": "d01", "result": "done", "summary": "新增下拉菜单 menu:触发器 + 自绘弹层,disabled/danger/divider 条目,↑↓ 环形导航 + Esc 回焦,reduced-motion 兜底;manifest/llms-full/demo 同步,check 全绿"}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
dataTable,
|
dataTable,
|
||||||
form,
|
form,
|
||||||
formItem,
|
formItem,
|
||||||
|
menu,
|
||||||
modal,
|
modal,
|
||||||
pagination,
|
pagination,
|
||||||
progress,
|
progress,
|
||||||
@@ -71,6 +72,9 @@ const formResult = signal('');
|
|||||||
// —— 分页联动状态:dataTable 与独立 pagination 共用同一页码信号 ——
|
// —— 分页联动状态:dataTable 与独立 pagination 共用同一页码信号 ——
|
||||||
const tablePage = signal(1);
|
const tablePage = signal(1);
|
||||||
|
|
||||||
|
// —— 菜单选中结果:演示 onSelect 回调 ——
|
||||||
|
const menuPick = signal('');
|
||||||
|
|
||||||
// —— 禁用态演示:开关信号同时绑定 switch 的 checked 与 checkbox 的 disabled ——
|
// —— 禁用态演示:开关信号同时绑定 switch 的 checked 与 checkbox 的 disabled ——
|
||||||
const locked = signal(true);
|
const locked = signal(true);
|
||||||
|
|
||||||
@@ -191,6 +195,37 @@ const app = h(
|
|||||||
h('div', { class: 'row' }, button({ label: '知道了', onClick: () => modalOpen(false) }), badge({ label: 'modal', variant: 'success' })),
|
h('div', { class: 'row' }, button({ label: '知道了', onClick: () => modalOpen(false) }), badge({ label: 'modal', variant: 'success' })),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
section(
|
||||||
|
'sec-menu',
|
||||||
|
'下拉菜单',
|
||||||
|
'menu 触发器 + 自绘弹层:条目支持禁用 / 危险 / 分隔线,↑↓ 环形导航,点外部或 Esc 收起。',
|
||||||
|
h(
|
||||||
|
'div',
|
||||||
|
{ class: 'row' },
|
||||||
|
menu({
|
||||||
|
label: '文件操作',
|
||||||
|
items: [
|
||||||
|
{ value: 'rename', label: '重命名' },
|
||||||
|
{ value: 'copy', label: '创建副本' },
|
||||||
|
{ value: 'archive', label: '归档', disabled: true },
|
||||||
|
{ divider: true, value: '', label: '' },
|
||||||
|
{ value: 'delete', label: '删除', danger: true },
|
||||||
|
],
|
||||||
|
onSelect: (v) => menuPick(v),
|
||||||
|
}),
|
||||||
|
menu({
|
||||||
|
label: '右对齐',
|
||||||
|
align: 'end',
|
||||||
|
variant: 'primary',
|
||||||
|
items: [
|
||||||
|
{ value: 'profile', label: '个人资料' },
|
||||||
|
{ value: 'settings', label: '偏好设置' },
|
||||||
|
],
|
||||||
|
onSelect: (v) => menuPick(v),
|
||||||
|
}),
|
||||||
|
h('span', { class: 'out' }, () => (menuPick() ? `已选:${menuPick()}` : '未选择')),
|
||||||
|
),
|
||||||
|
),
|
||||||
section(
|
section(
|
||||||
'sec-table',
|
'sec-table',
|
||||||
'数据表格',
|
'数据表格',
|
||||||
|
|||||||
+29
-2
@@ -1,6 +1,6 @@
|
|||||||
# @ui-arch 组件库 Agent 文档(无图形版)
|
# @ui-arch 组件库 Agent 文档(无图形版)
|
||||||
|
|
||||||
> 自动生成于 2026-10-23T08:10:07.974Z,数据源:各包 ua.manifest.json,勿手改。人类版文档站:apps/docs/index.html。
|
> 自动生成于 2026-10-23T08:45:45.013Z,数据源:各包 ua.manifest.json,勿手改。人类版文档站:apps/docs/index.html。
|
||||||
> 组件 = 普通函数,传 props 返回 DOM;状态用 signal 双向绑定:读 signal() 写 signal(v)。
|
> 组件 = 普通函数,传 props 返回 DOM;状态用 signal 双向绑定:读 signal() 写 signal(v)。
|
||||||
|
|
||||||
## 快速上手
|
## 快速上手
|
||||||
@@ -40,7 +40,7 @@
|
|||||||
|
|
||||||
## @ui-arch/ui(skin-dom)v0.1.0
|
## @ui-arch/ui(skin-dom)v0.1.0
|
||||||
|
|
||||||
20 个 DOM 组件,统一用法:组件是普通函数,传入 props 返回 DOM 节点;需要状态时先创建 signal 再双向绑定。引入 CSS:import '@ui-arch/ui/ui.css'(内部已消费 tokens)。
|
21 个 DOM 组件,统一用法:组件是普通函数,传入 props 返回 DOM 节点;需要状态时先创建 signal 再双向绑定。引入 CSS:import '@ui-arch/ui/ui.css'(内部已消费 tokens)。
|
||||||
|
|
||||||
### button — 按钮 Button
|
### button — 按钮 Button
|
||||||
|
|
||||||
@@ -182,6 +182,32 @@ const city = signal(''); select({ options: [{ value: 'bj', label: '北京' }], v
|
|||||||
|
|
||||||
现场演示:apps/docs/demo.html#sec-form
|
现场演示:apps/docs/demo.html#sec-form
|
||||||
|
|
||||||
|
### menu — 下拉菜单 Menu
|
||||||
|
|
||||||
|
参数:
|
||||||
|
|
||||||
|
- label(必填):string —— 触发器文案
|
||||||
|
- items(必填):MenuItem[] —— 菜单条目:{ value, label, disabled?, danger?, divider? }
|
||||||
|
- variant(默认 ghost):'primary' | 'ghost' —— 触发器视觉强度;缺省 ghost
|
||||||
|
- align(默认 start):'start' | 'end' —— 弹层对齐方向;缺省 start(与触发器左对齐)
|
||||||
|
- onSelect(回调):(value: string) => void —— 选中回调,禁用项不触发
|
||||||
|
|
||||||
|
示例:
|
||||||
|
|
||||||
|
```
|
||||||
|
menu({ label: '操作', items: [{ value: 'edit', label: '编辑' }, { value: 'del', label: '删除', danger: true }], onSelect: (v) => console.log(v) })
|
||||||
|
```
|
||||||
|
|
||||||
|
手把手:
|
||||||
|
|
||||||
|
1. 准备 items 数组:value 是回调标识、label 是展示文案,插一条 { divider: true } 即可分出条目组
|
||||||
|
2. 传 label 决定触发器文案;默认 ghost 轻量样式,主操作用 variant: 'primary'
|
||||||
|
3. 点触发器展开弹层,点条目收起并触发 onSelect;点外部或按 Esc 也会收起(Esc 后焦点回到触发器)
|
||||||
|
4. 键盘:Tab 聚焦触发器,↑↓ 展开并在可用条目间环形移动,Enter / Space 选中;disabled 项自动跳过
|
||||||
|
5. 靠右对齐避免溢出时传 align: 'end';危险操作给条目加 danger: true
|
||||||
|
|
||||||
|
现场演示:apps/docs/demo.html#sec-menu
|
||||||
|
|
||||||
### radioGroup — 单选组 RadioGroup
|
### radioGroup — 单选组 RadioGroup
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
@@ -517,6 +543,7 @@ API 速览:
|
|||||||
- checkbox(props):复选框,checked 信号双向绑定,disabled 支持布尔或信号禁用
|
- checkbox(props):复选框,checked 信号双向绑定,disabled 支持布尔或信号禁用
|
||||||
- switchButton(props):开关,checked 信号双向绑定,disabled 支持布尔或信号禁用
|
- switchButton(props):开关,checked 信号双向绑定,disabled 支持布尔或信号禁用
|
||||||
- select(props):自绘下拉选择(非原生 select),value 信号双向绑定,点外部 / Esc 关闭,选项弹层带微动画
|
- select(props):自绘下拉选择(非原生 select),value 信号双向绑定,点外部 / Esc 关闭,选项弹层带微动画
|
||||||
|
- menu(props):下拉菜单,触发器 + 自绘弹层,条目支持禁用 / 危险 / 分隔线,↑↓ 环形导航,点外部 / Esc 关闭
|
||||||
- radioGroup(props):单选组,options + value 信号双向绑定
|
- radioGroup(props):单选组,options + value 信号双向绑定
|
||||||
- card(props, ...children):卡片容器
|
- card(props, ...children):卡片容器
|
||||||
- badge(props):徽标,variant = default | success | warning | danger
|
- badge(props):徽标,variant = default | success | warning | danger
|
||||||
|
|||||||
@@ -45,7 +45,7 @@
|
|||||||
## @ui-arch/ui (v0.1.0)
|
## @ui-arch/ui (v0.1.0)
|
||||||
|
|
||||||
- 类型: skin-dom · 目录: packages/skin/ui
|
- 类型: skin-dom · 目录: packages/skin/ui
|
||||||
- 描述: DOM 皮肤组件库:button / textField / textarea / checkbox / switch / select / radioGroup / card / badge / alert / toast / tooltip / skeleton / spinner / progress / tabs / pagination / modal / form / formItem / dataTable,构建在 @ui-arch/frame 与 @ui-arch/table-core 之上,样式只消费 --ua-* 令牌。
|
- 描述: DOM 皮肤组件库:button / textField / textarea / checkbox / switch / select / menu / radioGroup / card / badge / alert / toast / tooltip / skeleton / spinner / progress / tabs / pagination / modal / form / formItem / dataTable,构建在 @ui-arch/frame 与 @ui-arch/table-core 之上,样式只消费 --ua-* 令牌。
|
||||||
- API:
|
- API:
|
||||||
- button(props):按钮,variant = primary | ghost | danger,disabled 支持布尔或信号禁用
|
- button(props):按钮,variant = primary | ghost | danger,disabled 支持布尔或信号禁用
|
||||||
- textField(props):文本输入,value 信号双向绑定,disabled 支持布尔或信号禁用
|
- textField(props):文本输入,value 信号双向绑定,disabled 支持布尔或信号禁用
|
||||||
@@ -53,6 +53,7 @@
|
|||||||
- checkbox(props):复选框,checked 信号双向绑定,disabled 支持布尔或信号禁用
|
- checkbox(props):复选框,checked 信号双向绑定,disabled 支持布尔或信号禁用
|
||||||
- switchButton(props):开关,checked 信号双向绑定,disabled 支持布尔或信号禁用
|
- switchButton(props):开关,checked 信号双向绑定,disabled 支持布尔或信号禁用
|
||||||
- select(props):自绘下拉选择(非原生 select),value 信号双向绑定,点外部 / Esc 关闭,选项弹层带微动画
|
- select(props):自绘下拉选择(非原生 select),value 信号双向绑定,点外部 / Esc 关闭,选项弹层带微动画
|
||||||
|
- menu(props):下拉菜单,触发器 + 自绘弹层,条目支持禁用 / 危险 / 分隔线,↑↓ 环形导航,点外部 / Esc 关闭
|
||||||
- radioGroup(props):单选组,options + value 信号双向绑定
|
- radioGroup(props):单选组,options + value 信号双向绑定
|
||||||
- card(props, ...children):卡片容器
|
- card(props, ...children):卡片容器
|
||||||
- badge(props):徽标,variant = default | success | warning | danger
|
- badge(props):徽标,variant = default | success | warning | danger
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ export { textarea } from './textarea.js';
|
|||||||
export { checkbox } from './checkbox.js';
|
export { checkbox } from './checkbox.js';
|
||||||
export { switchButton } from './switch.js';
|
export { switchButton } from './switch.js';
|
||||||
export { select } from './select.js';
|
export { select } from './select.js';
|
||||||
|
export { menu } from './menu.js';
|
||||||
export { radioGroup } from './radioGroup.js';
|
export { radioGroup } from './radioGroup.js';
|
||||||
export { card } from './card.js';
|
export { card } from './card.js';
|
||||||
export { badge } from './badge.js';
|
export { badge } from './badge.js';
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
import { effect, h, signal } from '@ui-arch/frame';
|
||||||
|
import type { MenuItem, MenuProps } from './types.js';
|
||||||
|
|
||||||
|
/*** 下拉菜单:触发器 + 自绘弹层,项支持禁用 / 危险 / 分隔线;点项 / 点外部 / Esc 关闭,↑↓ 环形导航 ***/
|
||||||
|
export function menu(props: MenuProps): HTMLElement {
|
||||||
|
// —— 状态集中声明 ——
|
||||||
|
const open = signal(false); // 弹层开合
|
||||||
|
const focusables: HTMLButtonElement[] = []; // 可聚焦项(跳过分隔线与禁用项),供键盘导航
|
||||||
|
|
||||||
|
const pick = (item: MenuItem): void => {
|
||||||
|
open(false); // 选中即收起
|
||||||
|
props.onSelect?.(item.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
// —— 菜单项:分隔线走独立分支,其余为原生 button 保证键盘可达 ——
|
||||||
|
const rows = props.items.map((item) => {
|
||||||
|
if (item.divider) return h('div', { class: 'ua-menu-divider', role: 'separator' });
|
||||||
|
const btn = h(
|
||||||
|
'button',
|
||||||
|
{
|
||||||
|
type: 'button',
|
||||||
|
class: `ua-menu-item${item.danger ? ' is-danger' : ''}`,
|
||||||
|
role: 'menuitem',
|
||||||
|
disabled: item.disabled ? true : undefined,
|
||||||
|
onClick: () => pick(item),
|
||||||
|
},
|
||||||
|
item.label,
|
||||||
|
) as HTMLButtonElement;
|
||||||
|
if (!item.disabled) focusables.push(btn); // 禁用项不参与导航
|
||||||
|
return btn;
|
||||||
|
});
|
||||||
|
|
||||||
|
const trigger = h(
|
||||||
|
'button',
|
||||||
|
{
|
||||||
|
type: 'button',
|
||||||
|
class: `ua-btn ua-btn-${props.variant ?? 'ghost'} ua-menu-trigger`,
|
||||||
|
'aria-haspopup': 'menu',
|
||||||
|
'aria-expanded': () => String(open()),
|
||||||
|
onClick: () => open(!open()),
|
||||||
|
},
|
||||||
|
props.label,
|
||||||
|
h('span', { class: 'ua-menu-caret' }, '▾'),
|
||||||
|
);
|
||||||
|
|
||||||
|
const pop = h(
|
||||||
|
'div',
|
||||||
|
{
|
||||||
|
class: () => `ua-menu-pop${open() ? ' ua-open' : ''}${props.align === 'end' ? ' is-end' : ''}`,
|
||||||
|
role: 'menu',
|
||||||
|
},
|
||||||
|
...rows,
|
||||||
|
);
|
||||||
|
|
||||||
|
const root = h('div', { class: () => `ua-menu${open() ? ' ua-open' : ''}` }, trigger, pop);
|
||||||
|
|
||||||
|
root.addEventListener('keydown', (e: KeyboardEvent) => {
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
open(false);
|
||||||
|
trigger.focus(); // 收起后焦点回到触发器,键盘流不断
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (e.key !== 'ArrowDown' && e.key !== 'ArrowUp') return; // 卫语句:只接管上下键
|
||||||
|
e.preventDefault();
|
||||||
|
if (!open()) {
|
||||||
|
open(true);
|
||||||
|
focusables[0]?.focus(); // 首次按下即展开并进首项
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const at = focusables.indexOf(document.activeElement as HTMLButtonElement);
|
||||||
|
const step = e.key === 'ArrowDown' ? 1 : -1;
|
||||||
|
focusables[(at + step + focusables.length) % focusables.length]?.focus(); // 环形移动
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('click', (e) => {
|
||||||
|
if (root.contains(e.target as Node)) return; // 卫语句:组件内点击不处理
|
||||||
|
open(false); // 点外部收起
|
||||||
|
});
|
||||||
|
|
||||||
|
effect(() => open()); // 建立依赖,保证开合信号生效
|
||||||
|
return root;
|
||||||
|
}
|
||||||
@@ -77,6 +77,24 @@ export interface RadioGroupProps {
|
|||||||
onChange?: (value: string) => void; // 变更回调
|
onChange?: (value: string) => void; // 变更回调
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*** 菜单项:divider 渲染成分隔线,其余为可选条目 ***/
|
||||||
|
export interface MenuItem {
|
||||||
|
value: string; // 条目标识(经 onSelect 透传)
|
||||||
|
label: string; // 条目文案
|
||||||
|
disabled?: boolean; // 禁用该条目:置灰且不参与键盘导航
|
||||||
|
danger?: boolean; // 危险操作条目:文案取 danger 语义色
|
||||||
|
divider?: boolean; // 该条目渲染为分隔线(忽略 value / label)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*** 下拉菜单入参 ***/
|
||||||
|
export interface MenuProps {
|
||||||
|
label: string; // 触发器文案
|
||||||
|
items: MenuItem[]; // 菜单条目列表
|
||||||
|
variant?: 'primary' | 'ghost'; // 触发器视觉强度;缺省 ghost
|
||||||
|
align?: 'start' | 'end'; // 弹层对齐方向;缺省 start(与触发器左对齐)
|
||||||
|
onSelect?: (value: string) => void; // 选中回调(禁用项不触发)
|
||||||
|
}
|
||||||
|
|
||||||
/*** 多行输入入参 ***/
|
/*** 多行输入入参 ***/
|
||||||
export interface TextareaProps {
|
export interface TextareaProps {
|
||||||
placeholder?: string; // 占位文案
|
placeholder?: string; // 占位文案
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@ui-arch/ui",
|
"name": "@ui-arch/ui",
|
||||||
"type": "skin-dom",
|
"type": "skin-dom",
|
||||||
"description": "DOM 皮肤组件库:button / textField / textarea / checkbox / switch / select / radioGroup / card / badge / alert / toast / tooltip / skeleton / spinner / progress / tabs / pagination / modal / form / formItem / dataTable,构建在 @ui-arch/frame 与 @ui-arch/table-core 之上,样式只消费 --ua-* 令牌。",
|
"description": "DOM 皮肤组件库:button / textField / textarea / checkbox / switch / select / menu / radioGroup / card / badge / alert / toast / tooltip / skeleton / spinner / progress / tabs / pagination / modal / form / formItem / dataTable,构建在 @ui-arch/frame 与 @ui-arch/table-core 之上,样式只消费 --ua-* 令牌。",
|
||||||
"tags": [
|
"tags": [
|
||||||
"components",
|
"components",
|
||||||
"dom",
|
"dom",
|
||||||
@@ -19,6 +19,7 @@
|
|||||||
"checkbox(props):复选框,checked 信号双向绑定,disabled 支持布尔或信号禁用",
|
"checkbox(props):复选框,checked 信号双向绑定,disabled 支持布尔或信号禁用",
|
||||||
"switchButton(props):开关,checked 信号双向绑定,disabled 支持布尔或信号禁用",
|
"switchButton(props):开关,checked 信号双向绑定,disabled 支持布尔或信号禁用",
|
||||||
"select(props):自绘下拉选择(非原生 select),value 信号双向绑定,点外部 / Esc 关闭,选项弹层带微动画",
|
"select(props):自绘下拉选择(非原生 select),value 信号双向绑定,点外部 / Esc 关闭,选项弹层带微动画",
|
||||||
|
"menu(props):下拉菜单,触发器 + 自绘弹层,条目支持禁用 / 危险 / 分隔线,↑↓ 环形导航,点外部 / Esc 关闭",
|
||||||
"radioGroup(props):单选组,options + value 信号双向绑定",
|
"radioGroup(props):单选组,options + value 信号双向绑定",
|
||||||
"card(props, ...children):卡片容器",
|
"card(props, ...children):卡片容器",
|
||||||
"badge(props):徽标,variant = default | success | warning | danger",
|
"badge(props):徽标,variant = default | success | warning | danger",
|
||||||
@@ -43,6 +44,8 @@
|
|||||||
"SwitchProps",
|
"SwitchProps",
|
||||||
"SelectProps",
|
"SelectProps",
|
||||||
"RadioGroupProps",
|
"RadioGroupProps",
|
||||||
|
"MenuItem",
|
||||||
|
"MenuProps",
|
||||||
"OptionItem",
|
"OptionItem",
|
||||||
"CardProps",
|
"CardProps",
|
||||||
"BadgeProps",
|
"BadgeProps",
|
||||||
@@ -69,7 +72,7 @@
|
|||||||
"语义色 variant 四档:default|info / success / warning / danger,一律来自令牌,禁止硬编码颜色"
|
"语义色 variant 四档:default|info / success / warning / danger,一律来自令牌,禁止硬编码颜色"
|
||||||
],
|
],
|
||||||
"docs": {
|
"docs": {
|
||||||
"intro": "20 个 DOM 组件,统一用法:组件是普通函数,传入 props 返回 DOM 节点;需要状态时先创建 signal 再双向绑定。引入 CSS:import '@ui-arch/ui/ui.css'(内部已消费 tokens)。",
|
"intro": "21 个 DOM 组件,统一用法:组件是普通函数,传入 props 返回 DOM 节点;需要状态时先创建 signal 再双向绑定。引入 CSS:import '@ui-arch/ui/ui.css'(内部已消费 tokens)。",
|
||||||
"steps": [
|
"steps": [
|
||||||
"安装依赖:pnpm add @ui-arch/ui @ui-arch/frame(workspace 内自动链接)",
|
"安装依赖:pnpm add @ui-arch/ui @ui-arch/frame(workspace 内自动链接)",
|
||||||
"引入样式:在应用入口 import '@ui-arch/ui/ui.css',暗色主题在根元素设 data-theme='dark'",
|
"引入样式:在应用入口 import '@ui-arch/ui/ui.css',暗色主题在根元素设 data-theme='dark'",
|
||||||
@@ -292,6 +295,51 @@
|
|||||||
"选中项在弹层中用淡色弱化标记,不抢视觉焦点"
|
"选中项在弹层中用淡色弱化标记,不抢视觉焦点"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "menu",
|
||||||
|
"title": "下拉菜单 Menu",
|
||||||
|
"demo": "sec-menu",
|
||||||
|
"props": [
|
||||||
|
{
|
||||||
|
"n": "label",
|
||||||
|
"t": "string",
|
||||||
|
"r": 1,
|
||||||
|
"d": "触发器文案"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"n": "items",
|
||||||
|
"t": "MenuItem[]",
|
||||||
|
"r": 1,
|
||||||
|
"d": "菜单条目:{ value, label, disabled?, danger?, divider? }"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"n": "variant",
|
||||||
|
"t": "'primary' | 'ghost'",
|
||||||
|
"d": "触发器视觉强度;缺省 ghost",
|
||||||
|
"df": "ghost"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"n": "align",
|
||||||
|
"t": "'start' | 'end'",
|
||||||
|
"d": "弹层对齐方向;缺省 start(与触发器左对齐)",
|
||||||
|
"df": "start"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"n": "onSelect",
|
||||||
|
"t": "(value: string) => void",
|
||||||
|
"k": 1,
|
||||||
|
"d": "选中回调,禁用项不触发"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"example": "menu({ label: '操作', items: [{ value: 'edit', label: '编辑' }, { value: 'del', label: '删除', danger: true }], onSelect: (v) => console.log(v) })",
|
||||||
|
"steps": [
|
||||||
|
"准备 items 数组:value 是回调标识、label 是展示文案,插一条 { divider: true } 即可分出条目组",
|
||||||
|
"传 label 决定触发器文案;默认 ghost 轻量样式,主操作用 variant: 'primary'",
|
||||||
|
"点触发器展开弹层,点条目收起并触发 onSelect;点外部或按 Esc 也会收起(Esc 后焦点回到触发器)",
|
||||||
|
"键盘:Tab 聚焦触发器,↑↓ 展开并在可用条目间环形移动,Enter / Space 选中;disabled 项自动跳过",
|
||||||
|
"靠右对齐避免溢出时传 align: 'end';危险操作给条目加 danger: true"
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "radioGroup",
|
"id": "radioGroup",
|
||||||
"title": "单选组 RadioGroup",
|
"title": "单选组 RadioGroup",
|
||||||
|
|||||||
@@ -308,6 +308,82 @@
|
|||||||
background: color-mix(in srgb, var(--ua-color-accent) 14%, transparent);
|
background: color-mix(in srgb, var(--ua-color-accent) 14%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* —— 下拉菜单 —— */
|
||||||
|
.ua-menu {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
.ua-menu-trigger {
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
.ua-menu-caret {
|
||||||
|
font-size: 10px;
|
||||||
|
opacity: 0.7;
|
||||||
|
transition: transform 0.2s;
|
||||||
|
}
|
||||||
|
.ua-menu.ua-open .ua-menu-caret {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
.ua-menu-pop {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 20;
|
||||||
|
top: calc(100% + 4px);
|
||||||
|
left: 0;
|
||||||
|
min-width: 160px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
padding: 4px;
|
||||||
|
border: 1px solid var(--ua-color-border);
|
||||||
|
border-radius: var(--ua-radius-md);
|
||||||
|
background: var(--ua-color-surface);
|
||||||
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.14);
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-4px);
|
||||||
|
visibility: hidden;
|
||||||
|
transition: opacity 0.15s ease, transform 0.15s ease, visibility 0.15s;
|
||||||
|
}
|
||||||
|
.ua-menu-pop.is-end {
|
||||||
|
left: auto;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
.ua-menu-pop.ua-open {
|
||||||
|
opacity: 1;
|
||||||
|
transform: none;
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
.ua-menu-item {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
text-align: left;
|
||||||
|
padding: 6px 10px;
|
||||||
|
border: none;
|
||||||
|
border-radius: var(--ua-radius-sm);
|
||||||
|
background: none;
|
||||||
|
color: var(--ua-color-text);
|
||||||
|
font-size: var(--ua-font-size-md);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.12s;
|
||||||
|
}
|
||||||
|
.ua-menu-item:hover:not(:disabled) {
|
||||||
|
background: color-mix(in srgb, var(--ua-color-accent) 8%, transparent);
|
||||||
|
}
|
||||||
|
.ua-menu-item.is-danger {
|
||||||
|
color: var(--ua-color-danger);
|
||||||
|
}
|
||||||
|
.ua-menu-item.is-danger:hover:not(:disabled) {
|
||||||
|
background: color-mix(in srgb, var(--ua-color-danger) 10%, transparent);
|
||||||
|
}
|
||||||
|
.ua-menu-item:disabled {
|
||||||
|
opacity: 0.45;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
.ua-menu-divider {
|
||||||
|
height: 1px;
|
||||||
|
margin: 4px 2px;
|
||||||
|
background: var(--ua-color-border);
|
||||||
|
}
|
||||||
|
|
||||||
/* —— 多行输入 —— */
|
/* —— 多行输入 —— */
|
||||||
.ua-textarea {
|
.ua-textarea {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -795,6 +871,9 @@
|
|||||||
.ua-select-arrow,
|
.ua-select-arrow,
|
||||||
.ua-select-pop,
|
.ua-select-pop,
|
||||||
.ua-option,
|
.ua-option,
|
||||||
|
.ua-menu-pop,
|
||||||
|
.ua-menu-caret,
|
||||||
|
.ua-menu-item,
|
||||||
.ua-switch-track,
|
.ua-switch-track,
|
||||||
.ua-switch-thumb,
|
.ua-switch-thumb,
|
||||||
.ua-tab,
|
.ua-tab,
|
||||||
|
|||||||
Reference in New Issue
Block a user