diff --git a/.ua/backlog.json b/.ua/backlog.json index 7c0dff6..71c841c 100644 --- a/.ua/backlog.json +++ b/.ua/backlog.json @@ -14,8 +14,8 @@ "title": "新增 toast 消息提示组件", "motivation": "alert 是常驻块级元素,缺少轻量转瞬即逝的全局反馈;操作成功/失败场景刚需", "acceptance": "新增 toast.ts:toast.success/error/warning/info 四个工厂函数,入参 {message, duration?},默认 3s 自动消失,右上角堆叠,语义色复用四档 variant,出入场微动画 + prefers-reduced-motion 兜底;导出、manifest、文档、llms-full.txt 同步;npm run check 全绿", - "status": "open", - "notes": "" + "status": "done", + "notes": "对外暴露 toast.info/success/warning/error(message, duration?) 四工厂 + showToast(props) 完整入参版;全局堆叠容器懒创建,role=status 无障碍;退场用 transitionend 移除并带 320ms 兜底(覆盖 reduced-motion 下 transition 不触发的情况);样式复用 alert 语义色 color-mix 模式,reduced-motion 列表已补 .ua-toast;demo 新增 sec-toast 区块" }, { "id": "b03", diff --git a/.ua/index.json b/.ua/index.json index 95e9204..95ac11b 100644 --- a/.ua/index.json +++ b/.ua/index.json @@ -1,6 +1,6 @@ { "$schema": "ui-arch-registry-index/0.1", - "generatedAt": "2026-10-23T07:34:01.200Z", + "generatedAt": "2026-10-23T07:49:42.422Z", "repoRoot": ".", "packages": [ { @@ -130,7 +130,7 @@ { "name": "@ui-arch/ui", "type": "skin-dom", - "description": "DOM 皮肤组件库:button / textField / textarea / checkbox / switch / select / radioGroup / card / badge / alert / tooltip / spinner / progress / tabs / modal / form / formItem / dataTable,构建在 @ui-arch/frame 与 @ui-arch/table-core 之上,样式只消费 --ua-* 令牌。", + "description": "DOM 皮肤组件库:button / textField / textarea / checkbox / switch / select / radioGroup / card / badge / alert / toast / tooltip / spinner / progress / tabs / modal / form / formItem / dataTable,构建在 @ui-arch/frame 与 @ui-arch/table-core 之上,样式只消费 --ua-* 令牌。", "tags": [ "components", "dom", @@ -152,6 +152,7 @@ "card(props, ...children):卡片容器", "badge(props):徽标,variant = default | success | warning | danger", "alert(props):提示条,variant = info | success | warning | danger", + "toast.info/success/warning/error(message, duration?):消息提示工厂,右上角堆叠,默认 3s 自动消失,出入场微动画;showToast(props) 为完整入参版本", "tooltip(props, trigger):悬浮提示,包裹触发器,hover / focus 延迟 150ms 出场,移出 / 失焦 / Esc 收起", "spinner(props):加载指示器,size 控制直径", "progress(props):进度条,轨道 + 渐变流动填充,value 信号驱动 0-100,满格切 success 色", @@ -173,6 +174,7 @@ "CardProps", "BadgeProps", "AlertProps", + "ToastProps", "TooltipProps", "SpinnerProps", "ProgressProps", @@ -192,7 +194,7 @@ "语义色 variant 四档:default|info / success / warning / danger,一律来自令牌,禁止硬编码颜色" ], "docs": { - "intro": "17 个 DOM 组件,统一用法:组件是普通函数,传入 props 返回 DOM 节点;需要状态时先创建 signal 再双向绑定。引入 CSS:import '@ui-arch/ui/ui.css'(内部已消费 tokens)。", + "intro": "18 个 DOM 组件,统一用法:组件是普通函数,传入 props 返回 DOM 节点;需要状态时先创建 signal 再双向绑定。引入 CSS:import '@ui-arch/ui/ui.css'(内部已消费 tokens)。", "steps": [ "安装依赖:pnpm add @ui-arch/ui @ui-arch/frame(workspace 内自动链接)", "引入样式:在应用入口 import '@ui-arch/ui/ui.css',暗色主题在根元素设 data-theme='dark'", @@ -487,6 +489,36 @@ "传入 message 即可,入场自带下滑动画" ] }, + { + "id": "toast", + "title": "消息提示 Toast", + "demo": "sec-toast", + "props": [ + { + "n": "message", + "t": "string", + "r": 1, + "d": "提示文案" + }, + { + "n": "duration", + "t": "number", + "d": "自动消失毫秒;缺省 3000" + }, + { + "n": "variant", + "t": "'info' | 'success' | 'warning' | 'danger'", + "d": "语义色;缺省 info(toast.error 对应 danger)" + } + ], + "example": "toast.success('保存成功'); toast.error('网络请求失败', 5000)", + "steps": [ + "直接调用语义工厂:toast.info / success / warning / error(message, duration?),无需先建容器", + "不传 duration 时默认停留 3 秒后自动淡出并移除节点", + "多条消息在右上角自动纵向堆叠,互不遮挡", + "需要完整入参(含 variant)时用 showToast(props);组件挂载容器由内部懒创建,样式随 ui.css 一起引入" + ] + }, { "id": "tooltip", "title": "悬浮提示 Tooltip", diff --git a/.ua/iterations.log b/.ua/iterations.log index 1af65b1..48c58bf 100644 --- a/.ua/iterations.log +++ b/.ua/iterations.log @@ -1 +1,2 @@ {"time":"2026-10-23T15:47:00+08:00","id":"b01","result":"done","summary":"新增 tooltip 悬浮提示组件(包裹式 API / 150ms 延迟 / Esc 收起 / reduced-motion 兜底),同步 manifest + llms-full.txt + demo 区块,并修复 ua style 对 URL 中 // 的误判;npm run check 全绿"} +{"time":"2026-10-23T15:50:03+08:00","id":"b02","result":"done","summary":"新增 toast 消息提示组件:四语义工厂 + showToast,右上角堆叠,默认 3s 自动消失,微动画 + reduced-motion 兜底;manifest/docs/llms 同步,check 全绿"} diff --git a/apps/demo/main.ts b/apps/demo/main.ts index d04e515..0bd291b 100644 --- a/apps/demo/main.ts +++ b/apps/demo/main.ts @@ -17,6 +17,7 @@ import { tabs, textarea, textField, + toast, tooltip, } from '@ui-arch/ui'; @@ -125,6 +126,19 @@ const app = h( tooltip({ content: '聚焦输入框同样会显示' }, textField({ placeholder: '聚焦我…' })), ), ), + section( + 'sec-toast', + '消息提示', + 'toast 右上角堆叠弹出,默认 3s 自动淡出移除;语义色四档,出入场微动画。', + h( + 'div', + { class: 'row' }, + button({ label: '成功', onClick: () => toast.success('保存成功') }), + button({ label: '错误', variant: 'danger', onClick: () => toast.error('网络请求失败') }), + button({ label: '警告', onClick: () => toast.warning('磁盘空间不足') }), + button({ label: '信息(1.5s)', onClick: () => toast.info('已同步云端', 1500) }), + ), + ), section( 'sec-formlab', '表单校验', diff --git a/llms-full.txt b/llms-full.txt index 4d8fd54..b8c3cf6 100644 --- a/llms-full.txt +++ b/llms-full.txt @@ -1,6 +1,6 @@ # @ui-arch 组件库 Agent 文档(无图形版) -> 自动生成于 2026-10-23T07:34:01.200Z,数据源:各包 ua.manifest.json,勿手改。人类版文档站:apps/docs/index.html。 +> 自动生成于 2026-10-23T07:49:42.422Z,数据源:各包 ua.manifest.json,勿手改。人类版文档站:apps/docs/index.html。 > 组件 = 普通函数,传 props 返回 DOM;状态用 signal 双向绑定:读 signal() 写 signal(v)。 ## 快速上手 @@ -40,7 +40,7 @@ ## @ui-arch/ui(skin-dom)v0.1.0 -17 个 DOM 组件,统一用法:组件是普通函数,传入 props 返回 DOM 节点;需要状态时先创建 signal 再双向绑定。引入 CSS:import '@ui-arch/ui/ui.css'(内部已消费 tokens)。 +18 个 DOM 组件,统一用法:组件是普通函数,传入 props 返回 DOM 节点;需要状态时先创建 signal 再双向绑定。引入 CSS:import '@ui-arch/ui/ui.css'(内部已消费 tokens)。 ### button — 按钮 Button @@ -260,6 +260,29 @@ alert({ message: '操作可能存在风险', variant: 'warning' }) 现场演示:apps/docs/demo.html#sec-feedback +### toast — 消息提示 Toast + +参数: + +- message(必填):string —— 提示文案 +- duration:number —— 自动消失毫秒;缺省 3000 +- variant:'info' | 'success' | 'warning' | 'danger' —— 语义色;缺省 info(toast.error 对应 danger) + +示例: + +``` +toast.success('保存成功'); toast.error('网络请求失败', 5000) +``` + +手把手: + +1. 直接调用语义工厂:toast.info / success / warning / error(message, duration?),无需先建容器 +2. 不传 duration 时默认停留 3 秒后自动淡出并移除节点 +3. 多条消息在右上角自动纵向堆叠,互不遮挡 +4. 需要完整入参(含 variant)时用 showToast(props);组件挂载容器由内部懒创建,样式随 ui.css 一起引入 + +现场演示:apps/docs/demo.html#sec-toast + ### tooltip — 悬浮提示 Tooltip 参数: @@ -444,6 +467,7 @@ API 速览: - card(props, ...children):卡片容器 - badge(props):徽标,variant = default | success | warning | danger - alert(props):提示条,variant = info | success | warning | danger +- toast.info/success/warning/error(message, duration?):消息提示工厂,右上角堆叠,默认 3s 自动消失,出入场微动画;showToast(props) 为完整入参版本 - tooltip(props, trigger):悬浮提示,包裹触发器,hover / focus 延迟 150ms 出场,移出 / 失焦 / Esc 收起 - spinner(props):加载指示器,size 控制直径 - progress(props):进度条,轨道 + 渐变流动填充,value 信号驱动 0-100,满格切 success 色 diff --git a/llms.txt b/llms.txt index 0bc9adf..24c870b 100644 --- a/llms.txt +++ b/llms.txt @@ -45,7 +45,7 @@ ## @ui-arch/ui (v0.1.0) - 类型: skin-dom · 目录: packages/skin/ui -- 描述: DOM 皮肤组件库:button / textField / textarea / checkbox / switch / select / radioGroup / card / badge / alert / tooltip / spinner / progress / tabs / modal / form / formItem / dataTable,构建在 @ui-arch/frame 与 @ui-arch/table-core 之上,样式只消费 --ua-* 令牌。 +- 描述: DOM 皮肤组件库:button / textField / textarea / checkbox / switch / select / radioGroup / card / badge / alert / toast / tooltip / spinner / progress / tabs / modal / form / formItem / dataTable,构建在 @ui-arch/frame 与 @ui-arch/table-core 之上,样式只消费 --ua-* 令牌。 - API: - button(props):按钮,variant = primary | ghost | danger - textField(props):文本输入,value 信号双向绑定 @@ -57,6 +57,7 @@ - card(props, ...children):卡片容器 - badge(props):徽标,variant = default | success | warning | danger - alert(props):提示条,variant = info | success | warning | danger + - toast.info/success/warning/error(message, duration?):消息提示工厂,右上角堆叠,默认 3s 自动消失,出入场微动画;showToast(props) 为完整入参版本 - tooltip(props, trigger):悬浮提示,包裹触发器,hover / focus 延迟 150ms 出场,移出 / 失焦 / Esc 收起 - spinner(props):加载指示器,size 控制直径 - progress(props):进度条,轨道 + 渐变流动填充,value 信号驱动 0-100,满格切 success 色 diff --git a/packages/skin/ui/src/index.ts b/packages/skin/ui/src/index.ts index 3a30be4..8a9243a 100644 --- a/packages/skin/ui/src/index.ts +++ b/packages/skin/ui/src/index.ts @@ -9,6 +9,7 @@ export { radioGroup } from './radioGroup.js'; export { card } from './card.js'; export { badge } from './badge.js'; export { alert } from './alert.js'; +export { toast, showToast } from './toast.js'; export { tooltip } from './tooltip.js'; export { spinner } from './spinner.js'; export { progress } from './progress.js'; diff --git a/packages/skin/ui/src/toast.ts b/packages/skin/ui/src/toast.ts new file mode 100644 index 0000000..d960559 --- /dev/null +++ b/packages/skin/ui/src/toast.ts @@ -0,0 +1,37 @@ +import { h } from '@ui-arch/frame'; +import type { ToastProps } from './types.js'; + +let host: HTMLElement | null = null; // 全局右上角堆叠容器:首条消息出现时懒创建 +let seq = 0; // 自增序号:为每条 toast 生成唯一 id,辅助无障碍定位 + +/*** 确保堆叠容器存在:fixed 挂在 body 末尾,后续消息依次向上堆叠 ***/ +function ensureHost(): HTMLElement { + if (host) return host; + host = h('div', { class: 'ua-toast-host' }); + document.body.appendChild(host); + return host; +} + +/*** 弹出一条 toast:语义色由 variant 决定,duration 毫秒后自动淡出并移除节点 ***/ +export function showToast(props: ToastProps): HTMLElement { + // —— 状态集中声明 —— + const item = h('div', { class: `ua-toast ua-toast-${props.variant ?? 'info'}`, role: 'status', id: `ua-toast-${++seq}` }, props.message); + const duration = props.duration ?? 3000; // 停留时长毫秒 + + ensureHost().appendChild(item); + requestAnimationFrame(() => item.classList.add('ua-open')); // 下一帧入场,保证过渡生效 + window.setTimeout(() => { + item.classList.remove('ua-open'); // 先退场再摘除,退场动画与 reduced-motion 兜底都安全 + item.addEventListener('transitionend', () => item.remove(), { once: true }); + window.setTimeout(() => item.isConnected && item.remove(), 320); // transition 不触发时的兜底移除 + }, duration); + return item; +} + +/*** toast 语义快捷工厂:toast.info / success / warning / error(message, duration?) ***/ +export const toast = { + info: (message: string, duration?: number): HTMLElement => showToast({ message, duration, variant: 'info' }), // 中性信息 + success: (message: string, duration?: number): HTMLElement => showToast({ message, duration, variant: 'success' }), // 成功反馈 + warning: (message: string, duration?: number): HTMLElement => showToast({ message, duration, variant: 'warning' }), // 警告 + error: (message: string, duration?: number): HTMLElement => showToast({ message, duration, variant: 'danger' }), // 错误 +}; diff --git a/packages/skin/ui/src/types.ts b/packages/skin/ui/src/types.ts index a16174c..9e8ad53 100644 --- a/packages/skin/ui/src/types.ts +++ b/packages/skin/ui/src/types.ts @@ -92,6 +92,13 @@ export interface AlertProps { variant?: 'info' | 'success' | 'warning' | 'danger'; // 语义色;缺省 info } +/*** toast 消息提示入参 ***/ +export interface ToastProps { + message: string; // 提示文案 + duration?: number; // 自动消失毫秒;缺省 3000 + variant?: 'info' | 'success' | 'warning' | 'danger'; // 语义色;缺省 info +} + /*** 加载指示器入参 ***/ export interface SpinnerProps { size?: number; // 直径像素;缺省 18 diff --git a/packages/skin/ui/ua.manifest.json b/packages/skin/ui/ua.manifest.json index 99f160a..7b3a2e1 100644 --- a/packages/skin/ui/ua.manifest.json +++ b/packages/skin/ui/ua.manifest.json @@ -1,7 +1,7 @@ { "name": "@ui-arch/ui", "type": "skin-dom", - "description": "DOM 皮肤组件库:button / textField / textarea / checkbox / switch / select / radioGroup / card / badge / alert / tooltip / spinner / progress / tabs / modal / form / formItem / dataTable,构建在 @ui-arch/frame 与 @ui-arch/table-core 之上,样式只消费 --ua-* 令牌。", + "description": "DOM 皮肤组件库:button / textField / textarea / checkbox / switch / select / radioGroup / card / badge / alert / toast / tooltip / spinner / progress / tabs / modal / form / formItem / dataTable,构建在 @ui-arch/frame 与 @ui-arch/table-core 之上,样式只消费 --ua-* 令牌。", "tags": [ "components", "dom", @@ -23,6 +23,7 @@ "card(props, ...children):卡片容器", "badge(props):徽标,variant = default | success | warning | danger", "alert(props):提示条,variant = info | success | warning | danger", + "toast.info/success/warning/error(message, duration?):消息提示工厂,右上角堆叠,默认 3s 自动消失,出入场微动画;showToast(props) 为完整入参版本", "tooltip(props, trigger):悬浮提示,包裹触发器,hover / focus 延迟 150ms 出场,移出 / 失焦 / Esc 收起", "spinner(props):加载指示器,size 控制直径", "progress(props):进度条,轨道 + 渐变流动填充,value 信号驱动 0-100,满格切 success 色", @@ -44,6 +45,7 @@ "CardProps", "BadgeProps", "AlertProps", + "ToastProps", "TooltipProps", "SpinnerProps", "ProgressProps", @@ -63,7 +65,7 @@ "语义色 variant 四档:default|info / success / warning / danger,一律来自令牌,禁止硬编码颜色" ], "docs": { - "intro": "17 个 DOM 组件,统一用法:组件是普通函数,传入 props 返回 DOM 节点;需要状态时先创建 signal 再双向绑定。引入 CSS:import '@ui-arch/ui/ui.css'(内部已消费 tokens)。", + "intro": "18 个 DOM 组件,统一用法:组件是普通函数,传入 props 返回 DOM 节点;需要状态时先创建 signal 再双向绑定。引入 CSS:import '@ui-arch/ui/ui.css'(内部已消费 tokens)。", "steps": [ "安装依赖:pnpm add @ui-arch/ui @ui-arch/frame(workspace 内自动链接)", "引入样式:在应用入口 import '@ui-arch/ui/ui.css',暗色主题在根元素设 data-theme='dark'", @@ -358,6 +360,36 @@ "传入 message 即可,入场自带下滑动画" ] }, + { + "id": "toast", + "title": "消息提示 Toast", + "demo": "sec-toast", + "props": [ + { + "n": "message", + "t": "string", + "r": 1, + "d": "提示文案" + }, + { + "n": "duration", + "t": "number", + "d": "自动消失毫秒;缺省 3000" + }, + { + "n": "variant", + "t": "'info' | 'success' | 'warning' | 'danger'", + "d": "语义色;缺省 info(toast.error 对应 danger)" + } + ], + "example": "toast.success('保存成功'); toast.error('网络请求失败', 5000)", + "steps": [ + "直接调用语义工厂:toast.info / success / warning / error(message, duration?),无需先建容器", + "不传 duration 时默认停留 3 秒后自动淡出并移除节点", + "多条消息在右上角自动纵向堆叠,互不遮挡", + "需要完整入参(含 variant)时用 showToast(props);组件挂载容器由内部懒创建,样式随 ui.css 一起引入" + ] + }, { "id": "tooltip", "title": "悬浮提示 Tooltip", diff --git a/packages/skin/ui/ui.css b/packages/skin/ui/ui.css index eebc5a1..9eb7d96 100644 --- a/packages/skin/ui/ui.css +++ b/packages/skin/ui/ui.css @@ -379,6 +379,48 @@ background: color-mix(in srgb, var(--ua-color-danger) 12%, transparent); } +/* —— toast 消息提示:右上角堆叠,转瞬即逝 —— */ +.ua-toast-host { + position: fixed; + top: var(--ua-space-4); + right: var(--ua-space-4); + z-index: 50; + display: flex; + flex-direction: column; + align-items: flex-end; + gap: var(--ua-space-2); + pointer-events: none; +} +.ua-toast { + padding: var(--ua-space-2) var(--ua-space-3); + border-radius: var(--ua-radius-md); + border: 1px solid; + font-size: var(--ua-font-size-md); + opacity: 0; + transform: translateX(12px); + transition: opacity 0.22s ease-out, transform 0.22s ease-out; +} +.ua-toast.ua-open { + opacity: 1; + transform: translateX(0); +} +.ua-toast-info { + border-color: var(--ua-color-accent); + background: color-mix(in srgb, var(--ua-color-accent) 8%, var(--ua-color-bg)); +} +.ua-toast-success { + border-color: var(--ua-color-success); + background: color-mix(in srgb, var(--ua-color-success) 10%, var(--ua-color-bg)); +} +.ua-toast-warning { + border-color: var(--ua-color-warning); + background: color-mix(in srgb, var(--ua-color-warning) 10%, var(--ua-color-bg)); +} +.ua-toast-danger { + border-color: var(--ua-color-danger); + background: color-mix(in srgb, var(--ua-color-danger) 10%, var(--ua-color-bg)); +} + /* —— 加载指示器 —— */ .ua-spinner { display: inline-block; @@ -664,6 +706,7 @@ .ua-progress-fill::after, .ua-alert, .ua-tip, + .ua-toast, .ua-form-error, .ua-modal, .ua-modal-overlay {