iter(g48): 新增 splitter 可拖拽分栏容器
This commit is contained in:
+44
-2
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"$schema": "ui-arch-registry-index/0.1",
|
"$schema": "ui-arch-registry-index/0.1",
|
||||||
"generatedAt": "2026-10-26T04:10:00.644Z",
|
"generatedAt": "2026-10-26T04:11:34.965Z",
|
||||||
"repoRoot": ".",
|
"repoRoot": ".",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
@@ -189,7 +189,8 @@
|
|||||||
"notification(props):右下角通知卡片(标题 + 描述 + 操作区),返回 { el, close } 句柄,缺省常驻需手动关闭",
|
"notification(props):右下角通知卡片(标题 + 描述 + 操作区),返回 { el, close } 句柄,缺省常驻需手动关闭",
|
||||||
"result(props):结果页,status 四档语义色 + 标题 + 说明 + 操作区,用于提交完成/校验失败等终态",
|
"result(props):结果页,status 四档语义色 + 标题 + 说明 + 操作区,用于提交完成/校验失败等终态",
|
||||||
"transfer(props):穿梭框,左右两栏勾选后整体搬运,targetKeys 信号双向绑定,两栏各自搜索与全选(含半选),禁用条目不参与搬运也不可勾选",
|
"transfer(props):穿梭框,左右两栏勾选后整体搬运,targetKeys 信号双向绑定,两栏各自搜索与全选(含半选),禁用条目不参与搬运也不可勾选",
|
||||||
"cascader(props):级联选择,多列联动面板(悬停下钻 / 点击提交),value 为路径数组信号,changeOnSelect 允许停在中间层,全键盘可操作"
|
"cascader(props):级联选择,多列联动面板(悬停下钻 / 点击提交),value 为路径数组信号,changeOnSelect 允许停在中间层,全键盘可操作",
|
||||||
|
"splitter(props):可拖拽分栏容器,分隔条支持指针拖拽 / 方向键微调 / 双击复位,sizes 为百分比数组信号"
|
||||||
],
|
],
|
||||||
"keyTypes": [
|
"keyTypes": [
|
||||||
"ButtonProps",
|
"ButtonProps",
|
||||||
@@ -2153,6 +2154,47 @@
|
|||||||
"键盘:↑↓ 同列移动、→ 下钻、← 回退、Enter/空格 提交、Esc 收起(与弹窗共用一个 Esc 栈)",
|
"键盘:↑↓ 同列移动、→ 下钻、← 回退、Enter/空格 提交、Esc 收起(与弹窗共用一个 Esc 栈)",
|
||||||
"面板空间不足时自动向上翻转;disabled 选项不可点也不会展开下一列"
|
"面板空间不足时自动向上翻转;disabled 选项不可点也不会展开下一列"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "splitter",
|
||||||
|
"title": "可拖拽分栏 Splitter",
|
||||||
|
"demo": "sec-splitter",
|
||||||
|
"props": [
|
||||||
|
{
|
||||||
|
"n": "panes",
|
||||||
|
"t": "unknown[]",
|
||||||
|
"r": 1,
|
||||||
|
"d": "各栏内容(节点或纯文本),顺序即排列顺序"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"n": "direction",
|
||||||
|
"t": "'horizontal' | 'vertical'",
|
||||||
|
"d": "分割方向;缺省 horizontal(左右)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"n": "sizes",
|
||||||
|
"t": "Signal<number[]>",
|
||||||
|
"d": "各栏百分比数组(和为 100);不传则内部均分"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"n": "min",
|
||||||
|
"t": "number",
|
||||||
|
"d": "每栏最小百分比;缺省 10,拖到边界即停"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"n": "onChange",
|
||||||
|
"t": "(sizes: number[]) => void",
|
||||||
|
"d": "拖拽 / 键盘调整后回调(传副本)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"example": "splitter({ sizes: ratio, min: 15, panes: [leftPanel, rightPanel], onChange: (s) => ratio([...s]) })",
|
||||||
|
"steps": [
|
||||||
|
"panes 长度决定栏数与分隔条数(n 栏对应 n-1 条),栏宽由 sizes 百分比驱动",
|
||||||
|
"指针按住分隔条拖动即可改比例,拖动过程两栏之和恒定、各自不低于 min",
|
||||||
|
"分隔条可 Tab 聚焦,← → / ↑ ↓ 每次调 2%,按住 Shift 调 10%;双击复位为均分",
|
||||||
|
"direction 传 vertical 变为上下分栏,键盘方向键与光标同步切换",
|
||||||
|
"传入 sizes 信号即受控,外部写入可做「预设布局」切换(如 1:1 / 2:1)"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
|
|
||||||
- `packages/core/table-core`(@ui-arch/table-core):Headless 表格逻辑内核
|
- `packages/core/table-core`(@ui-arch/table-core):Headless 表格逻辑内核
|
||||||
- `packages/core/frame`(@ui-arch/frame):signal + hyperscript 微框架
|
- `packages/core/frame`(@ui-arch/frame):signal + hyperscript 微框架
|
||||||
- `packages/skin/ui`(@ui-arch/ui):46 个 DOM 组件(表单 / 反馈 / 浮层 / 导航 / 表格),样式只消费令牌
|
- `packages/skin/ui`(@ui-arch/ui):47 个 DOM 组件(表单 / 反馈 / 浮层 / 导航 / 表格),样式只消费令牌
|
||||||
- `packages/design/tokens`(@ui-arch/tokens):设计令牌
|
- `packages/design/tokens`(@ui-arch/tokens):设计令牌
|
||||||
- `packages/tools/cli`(@ui-arch/cli):`ua` 命令行 + MCP 注册表服务(private)
|
- `packages/tools/cli`(@ui-arch/cli):`ua` 命令行 + MCP 注册表服务(private)
|
||||||
- `apps/demo`(@ui-arch/demo):交互演示,产物 apps/demo/dist/demo.html
|
- `apps/demo`(@ui-arch/demo):交互演示,产物 apps/demo/dist/demo.html
|
||||||
|
|||||||
+1
-1
@@ -34,7 +34,7 @@ ui-arch-mvp/
|
|||||||
│ │ ├── table-core/ # 表格状态内核
|
│ │ ├── table-core/ # 表格状态内核
|
||||||
│ │ └── frame/ # 信号 + hyperscript 微框架
|
│ │ └── frame/ # 信号 + hyperscript 微框架
|
||||||
│ ├── skin/ # 皮肤层(DOM 组件,可发布)
|
│ ├── skin/ # 皮肤层(DOM 组件,可发布)
|
||||||
│ │ └── ui/ # 46 个组件 + ui.css
|
│ │ └── ui/ # 47 个组件 + ui.css
|
||||||
│ ├── design/ # 资产层(可发布)
|
│ ├── design/ # 资产层(可发布)
|
||||||
│ │ └── tokens/ # --ua-* 设计令牌
|
│ │ └── tokens/ # --ua-* 设计令牌
|
||||||
│ └── tools/ # 工程层(private,不发布)
|
│ └── tools/ # 工程层(private,不发布)
|
||||||
|
|||||||
@@ -11,13 +11,13 @@ npm run demo # 打开演示:apps/demo/dist/demo.html
|
|||||||
npm run docs # 生成人类文档站 apps/docs/index.html + Agent 全量文档 llms-full.txt
|
npm run docs # 生成人类文档站 apps/docs/index.html + Agent 全量文档 llms-full.txt
|
||||||
```
|
```
|
||||||
|
|
||||||
## 组件(共 46 个)
|
## 组件(共 47 个)
|
||||||
|
|
||||||
<!-- ua:components -->
|
<!-- ua:components -->
|
||||||
accordion · alert · avatar · badge · breadcrumb · button · carousel · card · cascader · checkbox · dataTable · descriptions · divider · drawer · empty · form · image · inputNumber · list · menu · modal · notification · pagination · popover · progress · radioGroup · rate · result · segmented · select · skeleton · slider · spinner · statistic · stepper · switchButton · tabs · tag · textField · textarea · timeline · toast · tooltip · transfer · tree · upload
|
accordion · alert · avatar · badge · breadcrumb · button · carousel · card · cascader · checkbox · dataTable · descriptions · divider · drawer · empty · form · image · inputNumber · list · menu · modal · notification · pagination · popover · progress · radioGroup · rate · result · segmented · select · skeleton · slider · spinner · splitter · statistic · stepper · switchButton · tabs · tag · textField · textarea · timeline · toast · tooltip · transfer · tree · upload
|
||||||
<!-- /ua:components -->
|
<!-- /ua:components -->
|
||||||
|
|
||||||
分组:表单(button / textField / inputNumber / textarea / checkbox / switchButton / select / radioGroup / slider / rate / segmented / upload / form + formItem / transfer / cascader)、反馈(alert / toast / notification / result / spinner / progress / skeleton / badge / empty / statistic)、浮层(tooltip / popover / modal / drawer / menu)、导航(tabs / breadcrumb / pagination / accordion / stepper / tree)、数据展示(dataTable / descriptions / card / list / timeline / tag / avatar / image / carousel)、其他(divider)。
|
分组:表单(button / textField / inputNumber / textarea / checkbox / switchButton / select / radioGroup / slider / rate / segmented / upload / form + formItem / transfer / cascader)、反馈(alert / toast / notification / result / spinner / progress / skeleton / badge / empty / statistic)、浮层(tooltip / popover / modal / drawer / menu)、导航(tabs / breadcrumb / pagination / accordion / stepper / tree)、数据展示(dataTable / descriptions / card / list / timeline / tag / avatar / image / carousel)、其他(divider / splitter)。
|
||||||
|
|
||||||
## 可访问性约定
|
## 可访问性约定
|
||||||
|
|
||||||
@@ -31,7 +31,7 @@ accordion · alert · avatar · badge · breadcrumb · button · carousel · car
|
|||||||
|---|---|
|
|---|---|
|
||||||
| `packages/core/frame` | @ui-arch/frame:signal / computed / effect + hyperscript(无虚拟 DOM) |
|
| `packages/core/frame` | @ui-arch/frame:signal / computed / effect + hyperscript(无虚拟 DOM) |
|
||||||
| `packages/core/table-core` | @ui-arch/table-core:headless 表格(filter → sort → paginate 派生管线) |
|
| `packages/core/table-core` | @ui-arch/table-core:headless 表格(filter → sort → paginate 派生管线) |
|
||||||
| `packages/skin/ui` | @ui-arch/ui:46 个 DOM 组件 + ui.css(只消费 `--ua-*` 令牌) |
|
| `packages/skin/ui` | @ui-arch/ui:47 个 DOM 组件 + ui.css(只消费 `--ua-*` 令牌) |
|
||||||
| `packages/design/tokens` | @ui-arch/tokens:设计令牌(暗色只换值不换名) |
|
| `packages/design/tokens` | @ui-arch/tokens:设计令牌(暗色只换值不换名) |
|
||||||
| `packages/tools/cli` | @ui-arch/cli:`ua` 命令行与 stdio MCP server |
|
| `packages/tools/cli` | @ui-arch/cli:`ua` 命令行与 stdio MCP server |
|
||||||
| `apps/demo` | 交互演示(esbuild 打包成单文件) |
|
| `apps/demo` | 交互演示(esbuild 打包成单文件) |
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ import {
|
|||||||
select,
|
select,
|
||||||
showToast,
|
showToast,
|
||||||
skeleton,
|
skeleton,
|
||||||
|
splitter,
|
||||||
slider,
|
slider,
|
||||||
spinner,
|
spinner,
|
||||||
stepper,
|
stepper,
|
||||||
@@ -183,6 +184,7 @@ const menuPick = signal('');
|
|||||||
const listPick = signal(''); // 列表点击选中的条目标题
|
const listPick = signal(''); // 列表点击选中的条目标题
|
||||||
const slideIndex = signal(0); // 轮播当前下标(外部受控演示)
|
const slideIndex = signal(0); // 轮播当前下标(外部受控演示)
|
||||||
const cascaderPath = signal<string[]>(['east', 'sh']); // 级联选择路径(外部受控演示)
|
const cascaderPath = signal<string[]>(['east', 'sh']); // 级联选择路径(外部受控演示)
|
||||||
|
const splitSizes = signal<number[]>([35, 65]); // 分栏比例(外部受控演示)
|
||||||
const treePick = signal('core'); // 树形选中节点 key
|
const treePick = signal('core'); // 树形选中节点 key
|
||||||
const transferKeys = signal<string[]>(['a11y']); // 穿梭框右侧集合(外部受控演示)
|
const transferKeys = signal<string[]>(['a11y']); // 穿梭框右侧集合(外部受控演示)
|
||||||
const cityPick = signal(''); // 可搜索下拉的选中城市
|
const cityPick = signal(''); // 可搜索下拉的选中城市
|
||||||
@@ -679,6 +681,25 @@ const content = h(
|
|||||||
button({ label: '错误(可关)', variant: 'danger', onClick: () => notification({ title: '保存失败', description: '服务端返回 500,请稍后重试', variant: 'danger' }) }),
|
button({ label: '错误(可关)', variant: 'danger', onClick: () => notification({ title: '保存失败', description: '服务端返回 500,请稍后重试', variant: 'danger' }) }),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
section(
|
||||||
|
'sec-splitter',
|
||||||
|
'可拖拽分栏',
|
||||||
|
'splitter:分隔条可拖拽、可聚焦用方向键微调(Shift 加速)、双击复位均分;sizes 传信号即受控,每栏不低于 min。',
|
||||||
|
h(
|
||||||
|
'div',
|
||||||
|
{ class: 'col' },
|
||||||
|
splitter({
|
||||||
|
sizes: splitSizes,
|
||||||
|
onChange: (p) => toast.info(`比例:${p.map((n) => n + '%').join(' / ')}`),
|
||||||
|
panes: [
|
||||||
|
h('div', {}, h('div', { class: 'desc' }, '左侧栏'), list({ items: [{ title: '待办 A' }, { title: '待办 B' }], bordered: true })),
|
||||||
|
h('div', {}, h('div', { class: 'desc' }, '右侧栏:拖动中间分隔条试试'), h('p', { class: 'desc' }, '分隔条支持键盘:Tab 聚焦后 ← → 每次 2%,Shift + ← → 每次 10%,双击复位。')),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
h('span', { class: 'out' }, () => `当前比例:${splitSizes().map((n) => n + '%').join(' / ')}`),
|
||||||
|
splitter({ direction: 'vertical', min: 20, panes: [h('div', { class: 'desc' }, '上栏'), h('div', { class: 'desc' }, '下栏(纵向拖拽)')] }),
|
||||||
|
),
|
||||||
|
),
|
||||||
section(
|
section(
|
||||||
'sec-cascader',
|
'sec-cascader',
|
||||||
'级联选择',
|
'级联选择',
|
||||||
|
|||||||
+28
-1
@@ -1,6 +1,6 @@
|
|||||||
# @ui-arch 组件库 Agent 文档(无图形版)
|
# @ui-arch 组件库 Agent 文档(无图形版)
|
||||||
|
|
||||||
> 自动生成于 2026-10-26T04:10:00.644Z,数据源:各包 ua.manifest.json,勿手改。人类版文档站:apps/docs/index.html。
|
> 自动生成于 2026-10-26T04:11:34.965Z,数据源:各包 ua.manifest.json,勿手改。人类版文档站:apps/docs/index.html。
|
||||||
> 组件 = 普通函数,传 props 返回 DOM;状态用 signal 双向绑定:读 signal() 写 signal(v)。
|
> 组件 = 普通函数,传 props 返回 DOM;状态用 signal 双向绑定:读 signal() 写 signal(v)。
|
||||||
|
|
||||||
## 快速上手
|
## 快速上手
|
||||||
@@ -1229,6 +1229,32 @@ cascader({ value: areaPath, changeOnSelect: true, options: [{ value: 'east', lab
|
|||||||
|
|
||||||
现场演示:apps/docs/demo.html#sec-cascader
|
现场演示:apps/docs/demo.html#sec-cascader
|
||||||
|
|
||||||
|
### splitter — 可拖拽分栏 Splitter
|
||||||
|
|
||||||
|
参数:
|
||||||
|
|
||||||
|
- panes(必填):unknown[] —— 各栏内容(节点或纯文本),顺序即排列顺序
|
||||||
|
- direction:'horizontal' | 'vertical' —— 分割方向;缺省 horizontal(左右)
|
||||||
|
- sizes:Signal<number[]> —— 各栏百分比数组(和为 100);不传则内部均分
|
||||||
|
- min:number —— 每栏最小百分比;缺省 10,拖到边界即停
|
||||||
|
- onChange:(sizes: number[]) => void —— 拖拽 / 键盘调整后回调(传副本)
|
||||||
|
|
||||||
|
示例:
|
||||||
|
|
||||||
|
```
|
||||||
|
splitter({ sizes: ratio, min: 15, panes: [leftPanel, rightPanel], onChange: (s) => ratio([...s]) })
|
||||||
|
```
|
||||||
|
|
||||||
|
手把手:
|
||||||
|
|
||||||
|
1. panes 长度决定栏数与分隔条数(n 栏对应 n-1 条),栏宽由 sizes 百分比驱动
|
||||||
|
2. 指针按住分隔条拖动即可改比例,拖动过程两栏之和恒定、各自不低于 min
|
||||||
|
3. 分隔条可 Tab 聚焦,← → / ↑ ↓ 每次调 2%,按住 Shift 调 10%;双击复位为均分
|
||||||
|
4. direction 传 vertical 变为上下分栏,键盘方向键与光标同步切换
|
||||||
|
5. 传入 sizes 信号即受控,外部写入可做「预设布局」切换(如 1:1 / 2:1)
|
||||||
|
|
||||||
|
现场演示:apps/docs/demo.html#sec-splitter
|
||||||
|
|
||||||
API 速览:
|
API 速览:
|
||||||
|
|
||||||
- button(props):按钮,variant = primary | ghost | danger,disabled 支持布尔或信号禁用;type 缺省 button(表单内不隐式提交)
|
- button(props):按钮,variant = primary | ghost | danger,disabled 支持布尔或信号禁用;type 缺省 button(表单内不隐式提交)
|
||||||
@@ -1279,6 +1305,7 @@ API 速览:
|
|||||||
- result(props):结果页,status 四档语义色 + 标题 + 说明 + 操作区,用于提交完成/校验失败等终态
|
- result(props):结果页,status 四档语义色 + 标题 + 说明 + 操作区,用于提交完成/校验失败等终态
|
||||||
- transfer(props):穿梭框,左右两栏勾选后整体搬运,targetKeys 信号双向绑定,两栏各自搜索与全选(含半选),禁用条目不参与搬运也不可勾选
|
- transfer(props):穿梭框,左右两栏勾选后整体搬运,targetKeys 信号双向绑定,两栏各自搜索与全选(含半选),禁用条目不参与搬运也不可勾选
|
||||||
- cascader(props):级联选择,多列联动面板(悬停下钻 / 点击提交),value 为路径数组信号,changeOnSelect 允许停在中间层,全键盘可操作
|
- cascader(props):级联选择,多列联动面板(悬停下钻 / 点击提交),value 为路径数组信号,changeOnSelect 允许停在中间层,全键盘可操作
|
||||||
|
- splitter(props):可拖拽分栏容器,分隔条支持指针拖拽 / 方向键微调 / 双击复位,sizes 为百分比数组信号
|
||||||
|
|
||||||
一行用法:
|
一行用法:
|
||||||
|
|
||||||
|
|||||||
@@ -95,6 +95,7 @@
|
|||||||
- result(props):结果页,status 四档语义色 + 标题 + 说明 + 操作区,用于提交完成/校验失败等终态
|
- result(props):结果页,status 四档语义色 + 标题 + 说明 + 操作区,用于提交完成/校验失败等终态
|
||||||
- transfer(props):穿梭框,左右两栏勾选后整体搬运,targetKeys 信号双向绑定,两栏各自搜索与全选(含半选),禁用条目不参与搬运也不可勾选
|
- transfer(props):穿梭框,左右两栏勾选后整体搬运,targetKeys 信号双向绑定,两栏各自搜索与全选(含半选),禁用条目不参与搬运也不可勾选
|
||||||
- cascader(props):级联选择,多列联动面板(悬停下钻 / 点击提交),value 为路径数组信号,changeOnSelect 允许停在中间层,全键盘可操作
|
- cascader(props):级联选择,多列联动面板(悬停下钻 / 点击提交),value 为路径数组信号,changeOnSelect 允许停在中间层,全键盘可操作
|
||||||
|
- splitter(props):可拖拽分栏容器,分隔条支持指针拖拽 / 方向键微调 / 双击复位,sizes 为百分比数组信号
|
||||||
- 用法: `const checked = signal(false); checkbox({ label: '同意', checked }); const open = signal(false); modal({ title: '确认', open }, '内容')`
|
- 用法: `const checked = signal(false); checkbox({ label: '同意', checked }); const open = signal(false); modal({ title: '确认', open }, '内容')`
|
||||||
- 约定: 皮肤不持有业务状态,一律委托 headless 内核或调用方信号
|
- 约定: 皮肤不持有业务状态,一律委托 headless 内核或调用方信号
|
||||||
- 约定: 表单类组件统一约定:*Props.value/checked/active 为 Signal 双向绑定,onChange/onInput 为回调
|
- 约定: 表单类组件统一约定:*Props.value/checked/active 为 Signal 双向绑定,onChange/onInput 为回调
|
||||||
|
|||||||
@@ -46,3 +46,4 @@ export { form, formItem } from './form.js';
|
|||||||
export { dataTable } from './dataTable.js';
|
export { dataTable } from './dataTable.js';
|
||||||
export { transfer } from './transfer.js';
|
export { transfer } from './transfer.js';
|
||||||
export { cascader } from './cascader.js';
|
export { cascader } from './cascader.js';
|
||||||
|
export { splitter } from './splitter.js';
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
import { effect, h, signal } from '@ui-arch/frame';
|
||||||
|
import type { Signal } from '@ui-arch/frame';
|
||||||
|
import type { SplitterProps } from './types.js';
|
||||||
|
|
||||||
|
/*** 均分百分比:总数为 n 时每栏等分,末栏吃掉舍入余量保证和为 100 ***/
|
||||||
|
function even(n: number): number[] {
|
||||||
|
const each = Number((100 / n).toFixed(2));
|
||||||
|
const out = Array.from({ length: n }, () => each);
|
||||||
|
out[n - 1] = Number((100 - each * (n - 1)).toFixed(2));
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*** 分栏容器:可拖拽分隔条调整比例,sizes 信号双向绑定,分隔条可聚焦并用方向键微调、双击复位 ***/
|
||||||
|
export function splitter(props: SplitterProps): HTMLElement {
|
||||||
|
// —— 状态集中声明 ——
|
||||||
|
const dir = props.direction ?? 'horizontal'; // 分割方向
|
||||||
|
const min = Math.max(props.min ?? 10, 0); // 每栏最小百分比,负值归零
|
||||||
|
const sizes: Signal<number[]> = props.sizes ?? signal<number[]>(even(props.panes.length)); // 各栏比例
|
||||||
|
const bars: HTMLElement[] = []; // 分隔条,索引 i 位于第 i / i+1 栏之间
|
||||||
|
const cells: HTMLElement[] = []; // 栏容器,与 panes 一一对应
|
||||||
|
const isH = dir === 'horizontal'; // 横向:改 flex-basis(宽度)
|
||||||
|
|
||||||
|
/*** 同步几何:只写 flex-basis,避免触发重排风暴 ***/
|
||||||
|
const sync = (): void => {
|
||||||
|
const pct = sizes();
|
||||||
|
cells.forEach((cell, i) => { cell.style.flexBasis = `${pct[i] ?? 0}%`; });
|
||||||
|
bars.forEach((bar, i) => { bar.setAttribute('aria-valuenow', String(Math.round(pct[i] ?? 0))); }); // 读屏播报当前分界位置
|
||||||
|
};
|
||||||
|
|
||||||
|
/*** 成对调整:把 i 与 i+1 两栏按 delta 对调,各自不低于 min ***/
|
||||||
|
const shift = (i: number, delta: number): void => {
|
||||||
|
const cur = [...sizes()];
|
||||||
|
const pair = (cur[i] ?? 0) + (cur[i + 1] ?? 0); // 两栏总量恒定
|
||||||
|
const next = Math.min(Math.max((cur[i] ?? 0) + delta, min), pair - min); // 下限 min、上限 pair-min
|
||||||
|
if (next === cur[i]) return; // 卫语句:贴边不动
|
||||||
|
cur[i] = Number(next.toFixed(2));
|
||||||
|
cur[i + 1] = Number((pair - next).toFixed(2));
|
||||||
|
sizes(cur);
|
||||||
|
props.onChange?.([...cur]);
|
||||||
|
};
|
||||||
|
|
||||||
|
/*** 拖动:按下分隔条后按指针位移折算百分比,松手即解绑 ***/
|
||||||
|
const startDrag = (i: number, down: PointerEvent): void => {
|
||||||
|
const bar = down.currentTarget as HTMLElement;
|
||||||
|
const box = root.getBoundingClientRect();
|
||||||
|
const total = (isH ? box.width : box.height) || 1; // 容器主轴尺寸,防 0 除
|
||||||
|
const from = isH ? down.clientX : down.clientY; // 按下时指针主轴坐标
|
||||||
|
const startPct = sizes()[i] ?? 0; // 起拖比例,位移全部相对它计算
|
||||||
|
bar.setPointerCapture(down.pointerId); // 捕获指针:拖出分隔条也不丢事件
|
||||||
|
bar.classList.add('is-dragging');
|
||||||
|
const onMove = (e: PointerEvent): void => {
|
||||||
|
const moved = (((isH ? e.clientX : e.clientY) - from) / total) * 100; // 位移折算成百分比
|
||||||
|
shift(i, Number((startPct + moved - (sizes()[i] ?? 0)).toFixed(2))); // 增量驱动,夹逼交给 shift
|
||||||
|
};
|
||||||
|
const onUp = (): void => {
|
||||||
|
bar.classList.remove('is-dragging');
|
||||||
|
bar.removeEventListener('pointermove', onMove);
|
||||||
|
bar.removeEventListener('pointerup', onUp);
|
||||||
|
bar.removeEventListener('pointercancel', onUp);
|
||||||
|
};
|
||||||
|
bar.addEventListener('pointermove', onMove);
|
||||||
|
bar.addEventListener('pointerup', onUp);
|
||||||
|
bar.addEventListener('pointercancel', onUp);
|
||||||
|
};
|
||||||
|
|
||||||
|
const root = h('div', { class: `ua-splitter ua-splitter-${dir}` }) as HTMLElement;
|
||||||
|
|
||||||
|
props.panes.forEach((pane, i) => {
|
||||||
|
const cell = h('div', { class: 'ua-splitter-pane' }, pane) as HTMLElement; // pane 可为节点或纯文本
|
||||||
|
cells.push(cell);
|
||||||
|
root.append(cell);
|
||||||
|
if (i === props.panes.length - 1) return; // 最后一栏后面不再放分隔条
|
||||||
|
const bar = h('div', {
|
||||||
|
class: 'ua-splitter-bar',
|
||||||
|
role: 'separator',
|
||||||
|
tabindex: '0',
|
||||||
|
'aria-orientation': isH ? 'vertical' : 'horizontal', // 左右分栏时分隔条是竖向的
|
||||||
|
'aria-label': `调整第 ${i + 1} 栏宽度`, // 读屏可辨识
|
||||||
|
'aria-valuemin': String(min),
|
||||||
|
'aria-valuemax': String(100 - min),
|
||||||
|
onpointerdown: (e: PointerEvent) => { e.preventDefault(); startDrag(i, e); },
|
||||||
|
ondblclick: () => { sizes(even(props.panes.length)); props.onChange?.([...even(props.panes.length)]); }, // 双击复位均分
|
||||||
|
}) as HTMLElement;
|
||||||
|
bar.addEventListener('keydown', (e: KeyboardEvent) => {
|
||||||
|
const step = e.shiftKey ? 10 : 2; // Shift 加速
|
||||||
|
const back = isH ? 'ArrowLeft' : 'ArrowUp';
|
||||||
|
const fwd = isH ? 'ArrowRight' : 'ArrowDown';
|
||||||
|
if (e.key !== back && e.key !== fwd) return; // 卫语句:非方向键放行
|
||||||
|
e.preventDefault();
|
||||||
|
shift(i, e.key === back ? -step : step);
|
||||||
|
});
|
||||||
|
bars.push(bar);
|
||||||
|
root.append(bar);
|
||||||
|
});
|
||||||
|
|
||||||
|
effect(sync);
|
||||||
|
return root;
|
||||||
|
}
|
||||||
@@ -514,4 +514,13 @@ export interface CascaderProps {
|
|||||||
onChange?: (path: string[]) => void; // 选中回调(叶子或中间层,传副本)
|
onChange?: (path: string[]) => void; // 选中回调(叶子或中间层,传副本)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*** 分栏容器入参 ***/
|
||||||
|
export interface SplitterProps {
|
||||||
|
panes: unknown[]; // 各栏内容,顺序即排列顺序
|
||||||
|
direction?: 'horizontal' | 'vertical'; // 分割方向;缺省 horizontal(左右)
|
||||||
|
sizes?: Signal<number[]>; // 各栏百分比(和为 100);不传则内部均分
|
||||||
|
min?: number; // 每栏最小百分比;缺省 10
|
||||||
|
onChange?: (sizes: number[]) => void; // 拖拽/键盘调整后回调(传副本)
|
||||||
|
}
|
||||||
|
|
||||||
export type { ColumnDef, RowKey }; // 再导出常用内核类型,方便调用方
|
export type { ColumnDef, RowKey }; // 再导出常用内核类型,方便调用方
|
||||||
|
|||||||
@@ -60,7 +60,8 @@
|
|||||||
"notification(props):右下角通知卡片(标题 + 描述 + 操作区),返回 { el, close } 句柄,缺省常驻需手动关闭",
|
"notification(props):右下角通知卡片(标题 + 描述 + 操作区),返回 { el, close } 句柄,缺省常驻需手动关闭",
|
||||||
"result(props):结果页,status 四档语义色 + 标题 + 说明 + 操作区,用于提交完成/校验失败等终态",
|
"result(props):结果页,status 四档语义色 + 标题 + 说明 + 操作区,用于提交完成/校验失败等终态",
|
||||||
"transfer(props):穿梭框,左右两栏勾选后整体搬运,targetKeys 信号双向绑定,两栏各自搜索与全选(含半选),禁用条目不参与搬运也不可勾选",
|
"transfer(props):穿梭框,左右两栏勾选后整体搬运,targetKeys 信号双向绑定,两栏各自搜索与全选(含半选),禁用条目不参与搬运也不可勾选",
|
||||||
"cascader(props):级联选择,多列联动面板(悬停下钻 / 点击提交),value 为路径数组信号,changeOnSelect 允许停在中间层,全键盘可操作"
|
"cascader(props):级联选择,多列联动面板(悬停下钻 / 点击提交),value 为路径数组信号,changeOnSelect 允许停在中间层,全键盘可操作",
|
||||||
|
"splitter(props):可拖拽分栏容器,分隔条支持指针拖拽 / 方向键微调 / 双击复位,sizes 为百分比数组信号"
|
||||||
],
|
],
|
||||||
"keyTypes": [
|
"keyTypes": [
|
||||||
"ButtonProps",
|
"ButtonProps",
|
||||||
@@ -2024,6 +2025,47 @@
|
|||||||
"键盘:↑↓ 同列移动、→ 下钻、← 回退、Enter/空格 提交、Esc 收起(与弹窗共用一个 Esc 栈)",
|
"键盘:↑↓ 同列移动、→ 下钻、← 回退、Enter/空格 提交、Esc 收起(与弹窗共用一个 Esc 栈)",
|
||||||
"面板空间不足时自动向上翻转;disabled 选项不可点也不会展开下一列"
|
"面板空间不足时自动向上翻转;disabled 选项不可点也不会展开下一列"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "splitter",
|
||||||
|
"title": "可拖拽分栏 Splitter",
|
||||||
|
"demo": "sec-splitter",
|
||||||
|
"props": [
|
||||||
|
{
|
||||||
|
"n": "panes",
|
||||||
|
"t": "unknown[]",
|
||||||
|
"r": 1,
|
||||||
|
"d": "各栏内容(节点或纯文本),顺序即排列顺序"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"n": "direction",
|
||||||
|
"t": "'horizontal' | 'vertical'",
|
||||||
|
"d": "分割方向;缺省 horizontal(左右)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"n": "sizes",
|
||||||
|
"t": "Signal<number[]>",
|
||||||
|
"d": "各栏百分比数组(和为 100);不传则内部均分"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"n": "min",
|
||||||
|
"t": "number",
|
||||||
|
"d": "每栏最小百分比;缺省 10,拖到边界即停"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"n": "onChange",
|
||||||
|
"t": "(sizes: number[]) => void",
|
||||||
|
"d": "拖拽 / 键盘调整后回调(传副本)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"example": "splitter({ sizes: ratio, min: 15, panes: [leftPanel, rightPanel], onChange: (s) => ratio([...s]) })",
|
||||||
|
"steps": [
|
||||||
|
"panes 长度决定栏数与分隔条数(n 栏对应 n-1 条),栏宽由 sizes 百分比驱动",
|
||||||
|
"指针按住分隔条拖动即可改比例,拖动过程两栏之和恒定、各自不低于 min",
|
||||||
|
"分隔条可 Tab 聚焦,← → / ↑ ↓ 每次调 2%,按住 Shift 调 10%;双击复位为均分",
|
||||||
|
"direction 传 vertical 变为上下分栏,键盘方向键与光标同步切换",
|
||||||
|
"传入 sizes 信号即受控,外部写入可做「预设布局」切换(如 1:1 / 2:1)"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2671,3 +2671,53 @@ button.ua-step-dot:hover {
|
|||||||
.ua-cascader-caret,
|
.ua-cascader-caret,
|
||||||
.ua-cascader-opt { transition: none; }
|
.ua-cascader-opt { transition: none; }
|
||||||
}
|
}
|
||||||
|
.ua-splitter {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 160px;
|
||||||
|
border: 1px solid var(--ua-color-border);
|
||||||
|
border-radius: var(--ua-radius-md);
|
||||||
|
background: var(--ua-color-surface);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.ua-splitter-vertical {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.ua-splitter-pane {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 0;
|
||||||
|
padding: var(--ua-space-2);
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
.ua-splitter-bar {
|
||||||
|
position: relative;
|
||||||
|
flex: 0 0 6px;
|
||||||
|
background: var(--ua-color-border);
|
||||||
|
cursor: col-resize;
|
||||||
|
transition: background 0.15s ease;
|
||||||
|
touch-action: none;
|
||||||
|
}
|
||||||
|
.ua-splitter-vertical > .ua-splitter-bar {
|
||||||
|
cursor: row-resize;
|
||||||
|
}
|
||||||
|
.ua-splitter-bar::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: -4px;
|
||||||
|
/* 扩大命中区:视觉 6px,实际可抓 14px */
|
||||||
|
}
|
||||||
|
.ua-splitter-bar:hover,
|
||||||
|
.ua-splitter-bar.is-dragging,
|
||||||
|
.ua-splitter-bar:focus-visible {
|
||||||
|
background: var(--ua-color-accent);
|
||||||
|
}
|
||||||
|
.ua-splitter-bar:focus-visible {
|
||||||
|
outline: 2px solid var(--ua-color-accent);
|
||||||
|
outline-offset: 1px;
|
||||||
|
}
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.ua-splitter-bar {
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user