iter(g53): 新增 tour 引导漫游组件
This commit is contained in:
+39
-2
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"$schema": "ui-arch-registry-index/0.1",
|
"$schema": "ui-arch-registry-index/0.1",
|
||||||
"generatedAt": "2026-10-26T04:15:46.909Z",
|
"generatedAt": "2026-10-26T04:17:05.094Z",
|
||||||
"repoRoot": ".",
|
"repoRoot": ".",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
@@ -194,7 +194,8 @@
|
|||||||
"autoComplete(props):输入联想,输入即过滤候选、↑↓ 环形高亮 / Enter 选中 / Esc 收起,options 可传函数做远程联想",
|
"autoComplete(props):输入联想,输入即过滤候选、↑↓ 环形高亮 / Enter 选中 / Esc 收起,options 可传函数做远程联想",
|
||||||
"datePicker(props):日期选择,单月网格 + 翻月 / 今天快捷键,value 为 YYYY-MM-DD 信号,min/max 闭区间限制,方向键逐日逐周可导航",
|
"datePicker(props):日期选择,单月网格 + 翻月 / 今天快捷键,value 为 YYYY-MM-DD 信号,min/max 闭区间限制,方向键逐日逐周可导航",
|
||||||
"backTop(props):返回顶部悬浮按钮,越过阈值才浮现,外环 conic-gradient 显示已滚比例,点击平滑回顶",
|
"backTop(props):返回顶部悬浮按钮,越过阈值才浮现,外环 conic-gradient 显示已滚比例,点击平滑回顶",
|
||||||
"colorPicker(props):颜色选择,预设色板(缺省语义令牌色)+ 原生取色器 + 手输 CSS 颜色串,value 为颜色串信号"
|
"colorPicker(props):颜色选择,预设色板(缺省语义令牌色)+ 原生取色器 + 手输 CSS 颜色串,value 为颜色串信号",
|
||||||
|
"tour(props):引导漫游,返回 { el, start, close } 句柄;聚光框按 target 选择器定位,卡片自动选方位,Esc / 跳过走 onClose"
|
||||||
],
|
],
|
||||||
"keyTypes": [
|
"keyTypes": [
|
||||||
"ButtonProps",
|
"ButtonProps",
|
||||||
@@ -2373,6 +2374,42 @@
|
|||||||
"点色块即提交;手输框支持任意 CSS 颜色串(含 var(--ua-color-accent)),回车提交",
|
"点色块即提交;手输框支持任意 CSS 颜色串(含 var(--ua-color-accent)),回车提交",
|
||||||
"触发器左侧色块始终预览当前值,点击外部或 Esc 收起"
|
"触发器左侧色块始终预览当前值,点击外部或 Esc 收起"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "tour",
|
||||||
|
"title": "引导漫游 Tour",
|
||||||
|
"demo": "sec-tour",
|
||||||
|
"props": [
|
||||||
|
{
|
||||||
|
"n": "steps",
|
||||||
|
"t": "TourStep[]",
|
||||||
|
"r": 1,
|
||||||
|
"d": "步骤序列:每步含 target 选择器、title、可选 description"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"n": "padding",
|
||||||
|
"t": "number",
|
||||||
|
"d": "聚光区相对目标的外扩像素;缺省 6"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"n": "onFinish",
|
||||||
|
"t": "() => void",
|
||||||
|
"d": "走完最后一步的回调"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"n": "onClose",
|
||||||
|
"t": "() => void",
|
||||||
|
"d": "中途跳过(× 或 Esc)的回调"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"example": "const t = tour({ steps: [{ target: '#sec-tour', title: '引导漫游', description: '按选择器定位' }], onFinish: () => toast.success('完成') }); document.body.append(t.el); t.start();",
|
||||||
|
"steps": [
|
||||||
|
"tour() 返回 { el, start, close } 句柄:el 必须挂到 body(fixed 定位,不占布局)",
|
||||||
|
"start() 从第一步播放,可重复调用重播;close() 立即退出并按跳过处理",
|
||||||
|
"聚光框按 target 选择器定位并外扩 padding,其余区域由超大 box-shadow 压暗(省掉独立遮罩层)",
|
||||||
|
"卡片优先落在目标下方,空间不足自动翻到上方,左右夹在视口内;滚动 / 缩放会实时重算",
|
||||||
|
"末步按钮文案变「完成」并触发 onFinish;× 或 Esc 触发 onClose,焦点归还开启前的元素"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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):51 个 DOM 组件(表单 / 反馈 / 浮层 / 导航 / 表格),样式只消费令牌
|
- `packages/skin/ui`(@ui-arch/ui):52 个 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/ # 51 个组件 + ui.css
|
│ │ └── ui/ # 52 个组件 + 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
|
||||||
```
|
```
|
||||||
|
|
||||||
## 组件(共 51 个)
|
## 组件(共 52 个)
|
||||||
|
|
||||||
<!-- ua:components -->
|
<!-- ua:components -->
|
||||||
accordion · alert · autoComplete · avatar · backTop · badge · breadcrumb · button · carousel · card · cascader · checkbox · colorPicker · dataTable · datePicker · 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
|
accordion · alert · autoComplete · avatar · backTop · badge · breadcrumb · button · carousel · card · cascader · checkbox · colorPicker · dataTable · datePicker · 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 · tour · transfer · tree · upload
|
||||||
<!-- /ua:components -->
|
<!-- /ua:components -->
|
||||||
|
|
||||||
分组:表单(button / textField / inputNumber / textarea / checkbox / colorPicker / switchButton / select / radioGroup / slider / rate / autoComplete / datePicker / 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 / backTop)。
|
分组:表单(button / textField / inputNumber / textarea / checkbox / colorPicker / switchButton / select / radioGroup / slider / rate / autoComplete / datePicker / 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 / backTop / tour)。
|
||||||
|
|
||||||
## 可访问性约定
|
## 可访问性约定
|
||||||
|
|
||||||
@@ -31,7 +31,7 @@ accordion · alert · autoComplete · avatar · backTop · badge · breadcrumb
|
|||||||
|---|---|
|
|---|---|
|
||||||
| `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:51 个 DOM 组件 + ui.css(只消费 `--ua-*` 令牌) |
|
| `packages/skin/ui` | @ui-arch/ui:52 个 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 打包成单文件) |
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ import {
|
|||||||
textField,
|
textField,
|
||||||
timeline,
|
timeline,
|
||||||
toast,
|
toast,
|
||||||
|
tour,
|
||||||
tooltip,
|
tooltip,
|
||||||
} from '@ui-arch/ui';
|
} from '@ui-arch/ui';
|
||||||
import type { RowKey } from '@ui-arch/ui';
|
import type { RowKey } from '@ui-arch/ui';
|
||||||
@@ -688,6 +689,17 @@ 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-tour',
|
||||||
|
'引导漫游',
|
||||||
|
'tour({ steps }) 返回句柄:start() 从第一步播放,聚光框按目标选择器定位、卡片自动选上下方位,Esc / × 跳过走 onClose。',
|
||||||
|
h(
|
||||||
|
'div',
|
||||||
|
{ class: 'row' },
|
||||||
|
button({ label: '开始引导(3 步)', variant: 'primary', onClick: () => demoTour.start() }),
|
||||||
|
h('span', { class: 'desc' }, '三步分别指向本段、分栏段与返回顶部段,滚动后位置会实时重算。'),
|
||||||
|
),
|
||||||
|
),
|
||||||
section(
|
section(
|
||||||
'sec-colorpicker',
|
'sec-colorpicker',
|
||||||
'颜色选择',
|
'颜色选择',
|
||||||
@@ -967,6 +979,18 @@ const content = h(
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 引导漫游实例:根节点需挂到 body(fixed 定位,不参与布局)
|
||||||
|
const demoTour = tour({
|
||||||
|
steps: [
|
||||||
|
{ target: '#sec-tour', title: '引导漫游', description: '聚光框按 target 选择器定位,卡片会自动挑空间更大的一侧。' },
|
||||||
|
{ target: '#sec-splitter', title: '可拖拽分栏', description: '分隔条支持指针拖拽与方向键微调,双击复位。' },
|
||||||
|
{ target: '#sec-backtop', title: '返回顶部', description: '滚过 300px 后右下角浮现,外环是已滚比例。' },
|
||||||
|
],
|
||||||
|
onFinish: () => toast.success('引导完成'),
|
||||||
|
onClose: () => toast.info('已跳过引导'),
|
||||||
|
});
|
||||||
|
document.body.append(demoTour.el);
|
||||||
|
|
||||||
const toc = h('aside', { class: 'toc' }, h('div', { class: 'toc-title' }, '目录')); // 目录项由 buildToc 填充
|
const toc = h('aside', { class: 'toc' }, h('div', { class: 'toc-title' }, '目录')); // 目录项由 buildToc 填充
|
||||||
const app = h('div', { class: 'shell' }, toc, content);
|
const app = h('div', { class: 'shell' }, toc, content);
|
||||||
|
|
||||||
|
|||||||
+27
-1
@@ -1,6 +1,6 @@
|
|||||||
# @ui-arch 组件库 Agent 文档(无图形版)
|
# @ui-arch 组件库 Agent 文档(无图形版)
|
||||||
|
|
||||||
> 自动生成于 2026-10-26T04:15:46.909Z,数据源:各包 ua.manifest.json,勿手改。人类版文档站:apps/docs/index.html。
|
> 自动生成于 2026-10-26T04:17:05.094Z,数据源:各包 ua.manifest.json,勿手改。人类版文档站:apps/docs/index.html。
|
||||||
> 组件 = 普通函数,传 props 返回 DOM;状态用 signal 双向绑定:读 signal() 写 signal(v)。
|
> 组件 = 普通函数,传 props 返回 DOM;状态用 signal 双向绑定:读 signal() 写 signal(v)。
|
||||||
|
|
||||||
## 快速上手
|
## 快速上手
|
||||||
@@ -1360,6 +1360,31 @@ colorPicker({ value: colorVal, onChange: (c) => toast.success(c) })
|
|||||||
|
|
||||||
现场演示:apps/docs/demo.html#sec-colorpicker
|
现场演示:apps/docs/demo.html#sec-colorpicker
|
||||||
|
|
||||||
|
### tour — 引导漫游 Tour
|
||||||
|
|
||||||
|
参数:
|
||||||
|
|
||||||
|
- steps(必填):TourStep[] —— 步骤序列:每步含 target 选择器、title、可选 description
|
||||||
|
- padding:number —— 聚光区相对目标的外扩像素;缺省 6
|
||||||
|
- onFinish:() => void —— 走完最后一步的回调
|
||||||
|
- onClose:() => void —— 中途跳过(× 或 Esc)的回调
|
||||||
|
|
||||||
|
示例:
|
||||||
|
|
||||||
|
```
|
||||||
|
const t = tour({ steps: [{ target: '#sec-tour', title: '引导漫游', description: '按选择器定位' }], onFinish: () => toast.success('完成') }); document.body.append(t.el); t.start();
|
||||||
|
```
|
||||||
|
|
||||||
|
手把手:
|
||||||
|
|
||||||
|
1. tour() 返回 { el, start, close } 句柄:el 必须挂到 body(fixed 定位,不占布局)
|
||||||
|
2. start() 从第一步播放,可重复调用重播;close() 立即退出并按跳过处理
|
||||||
|
3. 聚光框按 target 选择器定位并外扩 padding,其余区域由超大 box-shadow 压暗(省掉独立遮罩层)
|
||||||
|
4. 卡片优先落在目标下方,空间不足自动翻到上方,左右夹在视口内;滚动 / 缩放会实时重算
|
||||||
|
5. 末步按钮文案变「完成」并触发 onFinish;× 或 Esc 触发 onClose,焦点归还开启前的元素
|
||||||
|
|
||||||
|
现场演示:apps/docs/demo.html#sec-tour
|
||||||
|
|
||||||
API 速览:
|
API 速览:
|
||||||
|
|
||||||
- button(props):按钮,variant = primary | ghost | danger,disabled 支持布尔或信号禁用;type 缺省 button(表单内不隐式提交)
|
- button(props):按钮,variant = primary | ghost | danger,disabled 支持布尔或信号禁用;type 缺省 button(表单内不隐式提交)
|
||||||
@@ -1415,6 +1440,7 @@ API 速览:
|
|||||||
- datePicker(props):日期选择,单月网格 + 翻月 / 今天快捷键,value 为 YYYY-MM-DD 信号,min/max 闭区间限制,方向键逐日逐周可导航
|
- datePicker(props):日期选择,单月网格 + 翻月 / 今天快捷键,value 为 YYYY-MM-DD 信号,min/max 闭区间限制,方向键逐日逐周可导航
|
||||||
- backTop(props):返回顶部悬浮按钮,越过阈值才浮现,外环 conic-gradient 显示已滚比例,点击平滑回顶
|
- backTop(props):返回顶部悬浮按钮,越过阈值才浮现,外环 conic-gradient 显示已滚比例,点击平滑回顶
|
||||||
- colorPicker(props):颜色选择,预设色板(缺省语义令牌色)+ 原生取色器 + 手输 CSS 颜色串,value 为颜色串信号
|
- colorPicker(props):颜色选择,预设色板(缺省语义令牌色)+ 原生取色器 + 手输 CSS 颜色串,value 为颜色串信号
|
||||||
|
- tour(props):引导漫游,返回 { el, start, close } 句柄;聚光框按 target 选择器定位,卡片自动选方位,Esc / 跳过走 onClose
|
||||||
|
|
||||||
一行用法:
|
一行用法:
|
||||||
|
|
||||||
|
|||||||
@@ -100,6 +100,7 @@
|
|||||||
- datePicker(props):日期选择,单月网格 + 翻月 / 今天快捷键,value 为 YYYY-MM-DD 信号,min/max 闭区间限制,方向键逐日逐周可导航
|
- datePicker(props):日期选择,单月网格 + 翻月 / 今天快捷键,value 为 YYYY-MM-DD 信号,min/max 闭区间限制,方向键逐日逐周可导航
|
||||||
- backTop(props):返回顶部悬浮按钮,越过阈值才浮现,外环 conic-gradient 显示已滚比例,点击平滑回顶
|
- backTop(props):返回顶部悬浮按钮,越过阈值才浮现,外环 conic-gradient 显示已滚比例,点击平滑回顶
|
||||||
- colorPicker(props):颜色选择,预设色板(缺省语义令牌色)+ 原生取色器 + 手输 CSS 颜色串,value 为颜色串信号
|
- colorPicker(props):颜色选择,预设色板(缺省语义令牌色)+ 原生取色器 + 手输 CSS 颜色串,value 为颜色串信号
|
||||||
|
- tour(props):引导漫游,返回 { el, start, close } 句柄;聚光框按 target 选择器定位,卡片自动选方位,Esc / 跳过走 onClose
|
||||||
- 用法: `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 为回调
|
||||||
|
|||||||
@@ -51,3 +51,4 @@ export { autoComplete } from './autoComplete.js';
|
|||||||
export { datePicker } from './datePicker.js';
|
export { datePicker } from './datePicker.js';
|
||||||
export { backTop } from './backTop.js';
|
export { backTop } from './backTop.js';
|
||||||
export { colorPicker } from './colorPicker.js';
|
export { colorPicker } from './colorPicker.js';
|
||||||
|
export { tour } from './tour.js';
|
||||||
|
|||||||
@@ -0,0 +1,102 @@
|
|||||||
|
import { h, signal } from '@ui-arch/frame';
|
||||||
|
import type { Signal } from '@ui-arch/frame';
|
||||||
|
import { pushOverlay } from './overlay.js';
|
||||||
|
import { button } from './button.js';
|
||||||
|
import type { TourHandle, TourProps } from './types.js';
|
||||||
|
|
||||||
|
/*** 引导漫游:聚光框 + 说明卡逐步指引,聚光用超大 box-shadow 挖洞省掉遮罩层,Esc / 跳过可退出 ***/
|
||||||
|
export function tour(props: TourProps): TourHandle {
|
||||||
|
// —— 状态集中声明 ——
|
||||||
|
const idx: Signal<number> = signal(-1); // 当前步骤,-1 表示未开启
|
||||||
|
const pad = props.padding ?? 6; // 聚光外扩像素
|
||||||
|
let releaseEsc: (() => void) | null = null; // Esc 栈注销函数
|
||||||
|
let lastFocus: HTMLElement | null = null; // 开启前焦点宿主,关闭时归还
|
||||||
|
|
||||||
|
const spot = h('div', { class: 'ua-tour-spot', 'aria-hidden': 'true' }); // 聚光框:阴影挖洞
|
||||||
|
const title = h('div', { class: 'ua-tour-title' });
|
||||||
|
const desc = h('div', { class: 'ua-tour-desc' });
|
||||||
|
const counter = h('span', { class: 'ua-tour-counter' });
|
||||||
|
const prevBtn = button({ label: '上一步', variant: 'ghost', onClick: () => go(idx() - 1) });
|
||||||
|
const nextBtn = button({ label: '下一步', variant: 'primary', onClick: () => go(idx() + 1) });
|
||||||
|
const card = h(
|
||||||
|
'div',
|
||||||
|
{ class: 'ua-tour-card', role: 'dialog', 'aria-modal': 'false', 'aria-label': '功能引导' },
|
||||||
|
h('div', { class: 'ua-tour-head' }, title, h('button', { type: 'button', class: 'ua-tour-close', 'aria-label': '跳过引导', onClick: () => close(true) }, '×')),
|
||||||
|
desc,
|
||||||
|
h('div', { class: 'ua-tour-foot' }, counter, h('div', { class: 'ua-tour-actions' }, prevBtn, nextBtn)),
|
||||||
|
) as HTMLElement;
|
||||||
|
const root = h('div', { class: 'ua-tour' }, spot, card) as HTMLElement;
|
||||||
|
|
||||||
|
/*** 定位:目标矩形外扩 pad 作为聚光框,卡片优先放下方、空间不足落上方 ***/
|
||||||
|
const place = (): void => {
|
||||||
|
const step = props.steps[idx()];
|
||||||
|
const target = step ? (document.querySelector(step.target) as HTMLElement | null) : null;
|
||||||
|
if (!step || !target) return; // 卫语句:目标不存在时保持原位
|
||||||
|
const r = target.getBoundingClientRect();
|
||||||
|
spot.style.left = `${r.left - pad}px`;
|
||||||
|
spot.style.top = `${r.top - pad}px`;
|
||||||
|
spot.style.width = `${r.width + pad * 2}px`;
|
||||||
|
spot.style.height = `${r.height + pad * 2}px`;
|
||||||
|
const below = r.bottom + 12; // 卡片默认贴目标下沿
|
||||||
|
const fits = below + card.offsetHeight < window.innerHeight; // 下方放得下
|
||||||
|
card.style.top = `${fits ? below : Math.max(r.top - card.offsetHeight - 12, 8)}px`;
|
||||||
|
card.style.left = `${Math.min(Math.max(r.left, 12), window.innerWidth - card.offsetWidth - 12)}px`; // 左右夹在视口内
|
||||||
|
};
|
||||||
|
|
||||||
|
/*** 渲染当前步:标题 / 说明 / 计数 / 按钮可用性,末步按钮文案改为「完成」 ***/
|
||||||
|
const render = (): void => {
|
||||||
|
const i = idx();
|
||||||
|
const step = props.steps[i];
|
||||||
|
if (!step) return; // 卫语句:未开启不渲染
|
||||||
|
title.textContent = step.title;
|
||||||
|
desc.textContent = step.description ?? '';
|
||||||
|
counter.textContent = `${i + 1} / ${props.steps.length}`;
|
||||||
|
const last = i === props.steps.length - 1;
|
||||||
|
nextBtn.textContent = last ? '完成' : '下一步';
|
||||||
|
(prevBtn as HTMLButtonElement).disabled = i === 0; // 首步没有上一步
|
||||||
|
root.classList.add('ua-open');
|
||||||
|
requestAnimationFrame(place); // 等卡片尺寸落定再定位,避免首帧位置偏
|
||||||
|
};
|
||||||
|
|
||||||
|
/*** 跳步:越界即结束,-1 之上为合法区间 ***/
|
||||||
|
const go = (to: number): void => {
|
||||||
|
if (to < 0) return; // 卫语句:越界不起作用
|
||||||
|
if (to >= props.steps.length) { finish(); return; }
|
||||||
|
idx(to);
|
||||||
|
render();
|
||||||
|
};
|
||||||
|
|
||||||
|
/*** 正常走完:收起并回调 onFinish ***/
|
||||||
|
const finish = (): void => {
|
||||||
|
close(false);
|
||||||
|
props.onFinish?.();
|
||||||
|
};
|
||||||
|
|
||||||
|
/*** 关闭:清 Esc 栈、撤销类名、焦点归还,跳过与走完区分回调 ***/
|
||||||
|
const close = (skipped: boolean): void => {
|
||||||
|
if (idx() < 0) return; // 卫语句:未开启不重复关闭
|
||||||
|
idx(-1);
|
||||||
|
root.classList.remove('ua-open');
|
||||||
|
releaseEsc?.();
|
||||||
|
releaseEsc = null;
|
||||||
|
lastFocus?.focus();
|
||||||
|
if (skipped) props.onClose?.();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onViewport = (): void => { if (idx() >= 0) place(); }; // 滚动/缩放后重算位置
|
||||||
|
window.addEventListener('resize', onViewport);
|
||||||
|
window.addEventListener('scroll', onViewport, { passive: true });
|
||||||
|
|
||||||
|
return {
|
||||||
|
el: root,
|
||||||
|
/*** 开启引导:记录焦点宿主、入 Esc 栈、摘掉旧监听后从第一步播放 ***/
|
||||||
|
start: (): void => {
|
||||||
|
if (!props.steps.length) return; // 卫语句:无步骤不启动
|
||||||
|
lastFocus = document.activeElement as HTMLElement | null;
|
||||||
|
releaseEsc?.();
|
||||||
|
releaseEsc = pushOverlay(() => close(true)); // 与弹层共用逐层关闭
|
||||||
|
go(0);
|
||||||
|
},
|
||||||
|
close: () => close(true),
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -568,4 +568,26 @@ export interface ColorPickerProps {
|
|||||||
onChange?: (color: string) => void; // 选定回调(色板或自定义输入)
|
onChange?: (color: string) => void; // 选定回调(色板或自定义输入)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*** 引导步骤 ***/
|
||||||
|
export interface TourStep {
|
||||||
|
target: string; // 目标元素选择器(document.querySelector 语义)
|
||||||
|
title: string; // 步骤标题
|
||||||
|
description?: string; // 补充说明文案
|
||||||
|
}
|
||||||
|
|
||||||
|
/*** 引导漫游入参 ***/
|
||||||
|
export interface TourProps {
|
||||||
|
steps: TourStep[]; // 步骤序列,按数组顺序逐步展示
|
||||||
|
padding?: number; // 聚光区相对目标的外扩像素;缺省 6
|
||||||
|
onFinish?: () => void; // 走完最后一步的回调
|
||||||
|
onClose?: () => void; // 中途跳过的回调
|
||||||
|
}
|
||||||
|
|
||||||
|
/*** 引导漫游句柄 ***/
|
||||||
|
export interface TourHandle {
|
||||||
|
el: HTMLElement; // 根节点:需由调用方挂到 body(fixed 定位,不占布局)
|
||||||
|
start: () => void; // 从第一步开始(可重复调用重播)
|
||||||
|
close: () => void; // 立即关闭
|
||||||
|
}
|
||||||
|
|
||||||
export type { ColumnDef, RowKey }; // 再导出常用内核类型,方便调用方
|
export type { ColumnDef, RowKey }; // 再导出常用内核类型,方便调用方
|
||||||
|
|||||||
@@ -65,7 +65,8 @@
|
|||||||
"autoComplete(props):输入联想,输入即过滤候选、↑↓ 环形高亮 / Enter 选中 / Esc 收起,options 可传函数做远程联想",
|
"autoComplete(props):输入联想,输入即过滤候选、↑↓ 环形高亮 / Enter 选中 / Esc 收起,options 可传函数做远程联想",
|
||||||
"datePicker(props):日期选择,单月网格 + 翻月 / 今天快捷键,value 为 YYYY-MM-DD 信号,min/max 闭区间限制,方向键逐日逐周可导航",
|
"datePicker(props):日期选择,单月网格 + 翻月 / 今天快捷键,value 为 YYYY-MM-DD 信号,min/max 闭区间限制,方向键逐日逐周可导航",
|
||||||
"backTop(props):返回顶部悬浮按钮,越过阈值才浮现,外环 conic-gradient 显示已滚比例,点击平滑回顶",
|
"backTop(props):返回顶部悬浮按钮,越过阈值才浮现,外环 conic-gradient 显示已滚比例,点击平滑回顶",
|
||||||
"colorPicker(props):颜色选择,预设色板(缺省语义令牌色)+ 原生取色器 + 手输 CSS 颜色串,value 为颜色串信号"
|
"colorPicker(props):颜色选择,预设色板(缺省语义令牌色)+ 原生取色器 + 手输 CSS 颜色串,value 为颜色串信号",
|
||||||
|
"tour(props):引导漫游,返回 { el, start, close } 句柄;聚光框按 target 选择器定位,卡片自动选方位,Esc / 跳过走 onClose"
|
||||||
],
|
],
|
||||||
"keyTypes": [
|
"keyTypes": [
|
||||||
"ButtonProps",
|
"ButtonProps",
|
||||||
@@ -2244,6 +2245,42 @@
|
|||||||
"点色块即提交;手输框支持任意 CSS 颜色串(含 var(--ua-color-accent)),回车提交",
|
"点色块即提交;手输框支持任意 CSS 颜色串(含 var(--ua-color-accent)),回车提交",
|
||||||
"触发器左侧色块始终预览当前值,点击外部或 Esc 收起"
|
"触发器左侧色块始终预览当前值,点击外部或 Esc 收起"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "tour",
|
||||||
|
"title": "引导漫游 Tour",
|
||||||
|
"demo": "sec-tour",
|
||||||
|
"props": [
|
||||||
|
{
|
||||||
|
"n": "steps",
|
||||||
|
"t": "TourStep[]",
|
||||||
|
"r": 1,
|
||||||
|
"d": "步骤序列:每步含 target 选择器、title、可选 description"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"n": "padding",
|
||||||
|
"t": "number",
|
||||||
|
"d": "聚光区相对目标的外扩像素;缺省 6"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"n": "onFinish",
|
||||||
|
"t": "() => void",
|
||||||
|
"d": "走完最后一步的回调"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"n": "onClose",
|
||||||
|
"t": "() => void",
|
||||||
|
"d": "中途跳过(× 或 Esc)的回调"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"example": "const t = tour({ steps: [{ target: '#sec-tour', title: '引导漫游', description: '按选择器定位' }], onFinish: () => toast.success('完成') }); document.body.append(t.el); t.start();",
|
||||||
|
"steps": [
|
||||||
|
"tour() 返回 { el, start, close } 句柄:el 必须挂到 body(fixed 定位,不占布局)",
|
||||||
|
"start() 从第一步播放,可重复调用重播;close() 立即退出并按跳过处理",
|
||||||
|
"聚光框按 target 选择器定位并外扩 padding,其余区域由超大 box-shadow 压暗(省掉独立遮罩层)",
|
||||||
|
"卡片优先落在目标下方,空间不足自动翻到上方,左右夹在视口内;滚动 / 缩放会实时重算",
|
||||||
|
"末步按钮文案变「完成」并触发 onFinish;× 或 Esc 触发 onClose,焦点归还开启前的元素"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3143,3 +3143,82 @@ button.ua-step-dot:hover {
|
|||||||
.ua-colorpicker-trigger,
|
.ua-colorpicker-trigger,
|
||||||
.ua-colorpicker-cell { transition: none; }
|
.ua-colorpicker-cell { transition: none; }
|
||||||
}
|
}
|
||||||
|
.ua-tour {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 70;
|
||||||
|
display: none;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.ua-tour.ua-open {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.ua-tour-spot {
|
||||||
|
position: absolute;
|
||||||
|
border-radius: var(--ua-radius-md);
|
||||||
|
box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.45);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
.ua-tour-card {
|
||||||
|
position: absolute;
|
||||||
|
width: 268px;
|
||||||
|
padding: var(--ua-space-2);
|
||||||
|
pointer-events: auto;
|
||||||
|
border: 1px solid var(--ua-color-border);
|
||||||
|
border-radius: var(--ua-radius-md);
|
||||||
|
background: var(--ua-color-surface);
|
||||||
|
box-shadow: 0 16px 32px rgba(0, 0, 0, 0.24);
|
||||||
|
animation: ua-tour-in 0.18s ease both;
|
||||||
|
}
|
||||||
|
.ua-tour-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: var(--ua-space-1);
|
||||||
|
}
|
||||||
|
.ua-tour-title {
|
||||||
|
font-size: var(--ua-font-size-sm);
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--ua-color-text);
|
||||||
|
}
|
||||||
|
.ua-tour-close {
|
||||||
|
padding: 0 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1;
|
||||||
|
color: var(--ua-color-text-muted);
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.ua-tour-close:hover {
|
||||||
|
color: var(--ua-color-text);
|
||||||
|
}
|
||||||
|
.ua-tour-desc {
|
||||||
|
margin-top: 4px;
|
||||||
|
font-size: var(--ua-font-size-sm);
|
||||||
|
line-height: 1.6;
|
||||||
|
color: var(--ua-color-text-muted);
|
||||||
|
}
|
||||||
|
.ua-tour-foot {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: var(--ua-space-1);
|
||||||
|
margin-top: var(--ua-space-2);
|
||||||
|
}
|
||||||
|
.ua-tour-counter {
|
||||||
|
font-size: var(--ua-font-size-sm);
|
||||||
|
color: var(--ua-color-text-muted);
|
||||||
|
}
|
||||||
|
.ua-tour-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--ua-space-1);
|
||||||
|
}
|
||||||
|
@keyframes ua-tour-in {
|
||||||
|
from { opacity: 0; transform: translateY(4px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.ua-tour-card { animation: none; }
|
||||||
|
.ua-tour-spot { transition: none; }
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user