iter(g300): 修正 toast 悬停暂停从未生效
This commit is contained in:
@@ -0,0 +1,10 @@
|
|||||||
|
import { readFileSync, writeFileSync } from 'node:fs';
|
||||||
|
|
||||||
|
// 用法:node .ua/add-entry.mjs <条目json文件>:把自补条目追加进 backlog(按 id 升序插到末尾)
|
||||||
|
const [src] = process.argv.slice(2);
|
||||||
|
const b = JSON.parse(readFileSync('.ua/backlog.json', 'utf8'));
|
||||||
|
const entry = JSON.parse(readFileSync(src, 'utf8'));
|
||||||
|
if (b.items.some((i) => i.id === entry.id)) throw new Error(`id 已存在:${entry.id}`);
|
||||||
|
b.items.push(entry);
|
||||||
|
writeFileSync('.ua/backlog.json', JSON.stringify(b, null, 2) + '\n', 'utf8');
|
||||||
|
console.log('added', entry.id, entry.title);
|
||||||
+10
-1
@@ -3086,6 +3086,15 @@
|
|||||||
"motivation": "清单只覆盖到 sparkline,后期新增的命令面板、通知条、环形进度没有抽查位——视觉类条目收尾时「照单过一遍」会直接跳过它们,等于裸奔。",
|
"motivation": "清单只覆盖到 sparkline,后期新增的命令面板、通知条、环形进度没有抽查位——视觉类条目收尾时「照单过一遍」会直接跳过它们,等于裸奔。",
|
||||||
"acceptance": "① ITERATION.md 新增三段共 11 条,维度落在五类内;② ua.js 的 REQUIRED 从 13 扩到 16、下限自动变 48 条、OK 文案同步;③ AGENTS 的清单描述同步为十六段 ≥48 条;④ npm run verify / check 全绿。",
|
"acceptance": "① ITERATION.md 新增三段共 11 条,维度落在五类内;② ua.js 的 REQUIRED 从 13 扩到 16、下限自动变 48 条、OK 文案同步;③ AGENTS 的清单描述同步为十六段 ≥48 条;④ npm run verify / check 全绿。",
|
||||||
"notes": "ITERATION.md 抽查清单加 commandPalette(4) / noticeBar(3) / progressRing(4);REQUIRED 13→16、条目下限自动 39→48、verify OK 文案与 AGENTS 描述同步"
|
"notes": "ITERATION.md 抽查清单加 commandPalette(4) / noticeBar(3) / progressRing(4);REQUIRED 13→16、条目下限自动 39→48、verify OK 文案与 AGENTS 描述同步"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "g300",
|
||||||
|
"domain": "tune",
|
||||||
|
"status": "open",
|
||||||
|
"title": "toast 悬停暂停从未生效:宿主穿透把指针拦在门外",
|
||||||
|
"motivation": "toast.ts 里写了 pointerenter 暂停倒计时,但样式只在 .ua-toast.is-closable / .is-action 时才给 pointer-events: auto,默认 toast 全程穿透——最常见的「toast.success(msg)」根本收不到 hover,暂停逻辑成了死代码;键盘用户 Tab 进关闭按钮时也照样被倒计时收走。",
|
||||||
|
"acceptance": "① .ua-toast 自身接收指针,删掉 is-closable / is-action 两处补丁式 pointer-events(宿主容器仍穿透,只有卡片本体可命中);② 暂停 / 恢复抽成 pause/resume,剩余时长不低于 400ms 且重复进入不二次扣减;③ focusin / focusout 与悬停同口径暂停,焦点在卡片内部移动不恢复;④ manifest 的 closable 说明与手把手步骤同步,npm run check 全绿。",
|
||||||
|
"notes": ""
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-2
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"$schema": "ui-arch-registry-index/0.1",
|
"$schema": "ui-arch-registry-index/0.1",
|
||||||
"generatedAt": "2026-10-26T15:38:27.731Z",
|
"generatedAt": "2026-10-26T16:25:36.184Z",
|
||||||
"repoRoot": ".",
|
"repoRoot": ".",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
@@ -1257,7 +1257,7 @@
|
|||||||
{
|
{
|
||||||
"n": "closable",
|
"n": "closable",
|
||||||
"t": "boolean",
|
"t": "boolean",
|
||||||
"d": "显示关闭按钮,点 × 立即收起;缺省 false(仅可关闭的 toast 接收指针事件,其余保持穿透)",
|
"d": "显示关闭按钮,点 × 立即收起;缺省 false(任意 toast 都接收指针,悬停或聚焦即暂停倒计时)",
|
||||||
"df": "false"
|
"df": "false"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -1282,6 +1282,7 @@
|
|||||||
"steps": [
|
"steps": [
|
||||||
"直接调用语义工厂:toast.info / success / warning / error(message, duration?),无需先建容器",
|
"直接调用语义工厂:toast.info / success / warning / error(message, duration?),无需先建容器",
|
||||||
"不传 duration 时默认停留 3 秒后自动淡出并移除节点",
|
"不传 duration 时默认停留 3 秒后自动淡出并移除节点",
|
||||||
|
"鼠标悬停或键盘 Tab 进卡片时倒计时暂停,移出 / 焦点离开后按剩余时长续走(剩余不足 400ms 时补到 400ms,避免松手即消失);宿主容器仍穿透,只有卡片本体这一小块可命中",
|
||||||
"多条消息在右上角自动纵向堆叠,互不遮挡",
|
"多条消息在右上角自动纵向堆叠,互不遮挡",
|
||||||
"需要完整入参(含 variant)时用 showToast(props);组件挂载容器由内部懒创建,样式随 ui.css 一起引入",
|
"需要完整入参(含 variant)时用 showToast(props);组件挂载容器由内部懒创建,样式随 ui.css 一起引入",
|
||||||
"同屏最多 4 条,超出自动挤掉最早一条;需要用户主动确认的消息用 showToast({ closable: true }) 给个 ×",
|
"同屏最多 4 条,超出自动挤掉最早一条;需要用户主动确认的消息用 showToast({ closable: true }) 给个 ×",
|
||||||
|
|||||||
+8
-7
@@ -1,6 +1,6 @@
|
|||||||
# @ui-arch 组件库 Agent 文档(无图形版)
|
# @ui-arch 组件库 Agent 文档(无图形版)
|
||||||
|
|
||||||
> 自动生成于 2026-10-26T15:38:27.731Z,数据源:各包 ua.manifest.json,勿手改。人类版文档站:apps/docs/index.html。
|
> 自动生成于 2026-10-26T16:25:36.184Z,数据源:各包 ua.manifest.json,勿手改。人类版文档站:apps/docs/index.html。
|
||||||
> 组件 = 普通函数,传 props 返回 DOM;状态用 signal 双向绑定:读 signal() 写 signal(v)。
|
> 组件 = 普通函数,传 props 返回 DOM;状态用 signal 双向绑定:读 signal() 写 signal(v)。
|
||||||
|
|
||||||
## 快速上手
|
## 快速上手
|
||||||
@@ -587,7 +587,7 @@ skeleton({ variant: 'text', rows: 3 }); skeleton({ variant: 'circle' }); skeleto
|
|||||||
|
|
||||||
- message(必填):string —— 提示文案
|
- message(必填):string —— 提示文案
|
||||||
- duration(默认 3000):number —— 自动消失毫秒;缺省 3000
|
- duration(默认 3000):number —— 自动消失毫秒;缺省 3000
|
||||||
- closable(默认 false):boolean —— 显示关闭按钮,点 × 立即收起;缺省 false(仅可关闭的 toast 接收指针事件,其余保持穿透)
|
- closable(默认 false):boolean —— 显示关闭按钮,点 × 立即收起;缺省 false(任意 toast 都接收指针,悬停或聚焦即暂停倒计时)
|
||||||
- variant(默认 info):'info' | 'success' | 'warning' | 'danger' —— 语义色;缺省 info(toast.error 对应 danger)
|
- variant(默认 info):'info' | 'success' | 'warning' | 'danger' —— 语义色;缺省 info(toast.error 对应 danger)
|
||||||
- actionLabel:string —— 操作按钮文案(如「撤销」);传了才渲染,点后 toast 立即收起
|
- actionLabel:string —— 操作按钮文案(如「撤销」);传了才渲染,点后 toast 立即收起
|
||||||
- onAction(回调):() => void —— 操作按钮回调,在 toast 收起之后触发(撤销删除之类)
|
- onAction(回调):() => void —— 操作按钮回调,在 toast 收起之后触发(撤销删除之类)
|
||||||
@@ -602,11 +602,12 @@ toast.success('保存成功'); toast.error('网络请求失败', 5000)
|
|||||||
|
|
||||||
1. 直接调用语义工厂:toast.info / success / warning / error(message, duration?),无需先建容器
|
1. 直接调用语义工厂:toast.info / success / warning / error(message, duration?),无需先建容器
|
||||||
2. 不传 duration 时默认停留 3 秒后自动淡出并移除节点
|
2. 不传 duration 时默认停留 3 秒后自动淡出并移除节点
|
||||||
3. 多条消息在右上角自动纵向堆叠,互不遮挡
|
3. 鼠标悬停或键盘 Tab 进卡片时倒计时暂停,移出 / 焦点离开后按剩余时长续走(剩余不足 400ms 时补到 400ms,避免松手即消失);宿主容器仍穿透,只有卡片本体这一小块可命中
|
||||||
4. 需要完整入参(含 variant)时用 showToast(props);组件挂载容器由内部懒创建,样式随 ui.css 一起引入
|
4. 多条消息在右上角自动纵向堆叠,互不遮挡
|
||||||
5. 同屏最多 4 条,超出自动挤掉最早一条;需要用户主动确认的消息用 showToast({ closable: true }) 给个 ×
|
5. 需要完整入参(含 variant)时用 showToast(props);组件挂载容器由内部懒创建,样式随 ui.css 一起引入
|
||||||
6. 要「撤销」这类操作时给 showToast 传 actionLabel 与 onAction:按钮出现在文案右侧,点完先收起再回调,duration 建议放到 5000 让用户来得及点
|
6. 同屏最多 4 条,超出自动挤掉最早一条;需要用户主动确认的消息用 showToast({ closable: true }) 给个 ×
|
||||||
7. 同屏最多 4 条:第 5 条进来时最早一条走正常退场(淡出后移除)而不是瞬间消失;要不被挤掉就传 duration: 0 让它常驻,靠 closable 手动关
|
7. 要「撤销」这类操作时给 showToast 传 actionLabel 与 onAction:按钮出现在文案右侧,点完先收起再回调,duration 建议放到 5000 让用户来得及点
|
||||||
|
8. 同屏最多 4 条:第 5 条进来时最早一条走正常退场(淡出后移除)而不是瞬间消失;要不被挤掉就传 duration: 0 让它常驻,靠 closable 手动关
|
||||||
|
|
||||||
现场演示:apps/docs/demo.html#sec-toast
|
现场演示:apps/docs/demo.html#sec-toast
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import type { ToastProps } from './types.js';
|
|||||||
let host: HTMLElement | null = null; // 全局右上角堆叠容器:首条消息出现时懒创建
|
let host: HTMLElement | null = null; // 全局右上角堆叠容器:首条消息出现时懒创建
|
||||||
let seq = 0; // 自增序号:为每条 toast 生成唯一 id,辅助无障碍定位
|
let seq = 0; // 自增序号:为每条 toast 生成唯一 id,辅助无障碍定位
|
||||||
const MAX_STACK = 4; // 同屏最多条数:超出即挤掉最早一条,避免刷屏淹页面
|
const MAX_STACK = 4; // 同屏最多条数:超出即挤掉最早一条,避免刷屏淹页面
|
||||||
|
const PAUSE_FLOOR = 400; // 恢复时的最短剩余毫秒:悬停前已近归零也不至于松手即消失
|
||||||
|
|
||||||
/*** 确保堆叠容器存在:fixed 挂在 body 末尾,后续消息依次向上堆叠 ***/
|
/*** 确保堆叠容器存在:fixed 挂在 body 末尾,后续消息依次向上堆叠 ***/
|
||||||
function ensureHost(): HTMLElement {
|
function ensureHost(): HTMLElement {
|
||||||
@@ -43,10 +44,23 @@ export function showToast(props: ToastProps): HTMLElement {
|
|||||||
stack.appendChild(item);
|
stack.appendChild(item);
|
||||||
requestAnimationFrame(() => item.classList.add('ua-open')); // 下一帧入场,保证过渡生效
|
requestAnimationFrame(() => item.classList.add('ua-open')); // 下一帧入场,保证过渡生效
|
||||||
const arm = (ms: number): void => { startedAt = Date.now(); timer = window.setTimeout(() => dismiss(item), ms); }; // 重新武装倒计时
|
const arm = (ms: number): void => { startedAt = Date.now(); timer = window.setTimeout(() => dismiss(item), ms); }; // 重新武装倒计时
|
||||||
|
/*** 暂停倒计时:扣掉本段已走时长,清空句柄(重复进入不会二次扣减) ***/
|
||||||
|
const pause = (): void => {
|
||||||
|
if (!timer) return; // 已暂停 / 常驻(duration 为 0)时无计时器可停
|
||||||
|
window.clearTimeout(timer);
|
||||||
|
timer = 0;
|
||||||
|
remain = Math.max(PAUSE_FLOOR, remain - (Date.now() - startedAt));
|
||||||
|
};
|
||||||
|
/*** 恢复倒计时:按剩余时长重新武装 ***/
|
||||||
|
const resume = (): void => { if (!timer && remain > 0) arm(remain); };
|
||||||
if (duration > 0) {
|
if (duration > 0) {
|
||||||
arm(duration);
|
arm(duration);
|
||||||
item.addEventListener('pointerenter', () => { window.clearTimeout(timer); remain -= Date.now() - startedAt; }); // 悬停暂停:记下剩余时长
|
item.addEventListener('pointerenter', pause); // 悬停停留阅读:倒计时暂停
|
||||||
item.addEventListener('pointerleave', () => arm(remain)); // 移出后按剩余时长继续
|
item.addEventListener('pointerleave', resume); // 移出后按剩余时长继续
|
||||||
|
item.addEventListener('focusin', pause); // 键盘 Tab 进关闭 / 操作按钮时同样暂停
|
||||||
|
item.addEventListener('focusout', (e) => {
|
||||||
|
if (!item.contains(e.relatedTarget as Node | null)) resume(); // 焦点还在卡片内部(按钮间切换)时不恢复
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return item;
|
return item;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1128,7 +1128,7 @@
|
|||||||
{
|
{
|
||||||
"n": "closable",
|
"n": "closable",
|
||||||
"t": "boolean",
|
"t": "boolean",
|
||||||
"d": "显示关闭按钮,点 × 立即收起;缺省 false(仅可关闭的 toast 接收指针事件,其余保持穿透)",
|
"d": "显示关闭按钮,点 × 立即收起;缺省 false(任意 toast 都接收指针,悬停或聚焦即暂停倒计时)",
|
||||||
"df": "false"
|
"df": "false"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -1153,6 +1153,7 @@
|
|||||||
"steps": [
|
"steps": [
|
||||||
"直接调用语义工厂:toast.info / success / warning / error(message, duration?),无需先建容器",
|
"直接调用语义工厂:toast.info / success / warning / error(message, duration?),无需先建容器",
|
||||||
"不传 duration 时默认停留 3 秒后自动淡出并移除节点",
|
"不传 duration 时默认停留 3 秒后自动淡出并移除节点",
|
||||||
|
"鼠标悬停或键盘 Tab 进卡片时倒计时暂停,移出 / 焦点离开后按剩余时长续走(剩余不足 400ms 时补到 400ms,避免松手即消失);宿主容器仍穿透,只有卡片本体这一小块可命中",
|
||||||
"多条消息在右上角自动纵向堆叠,互不遮挡",
|
"多条消息在右上角自动纵向堆叠,互不遮挡",
|
||||||
"需要完整入参(含 variant)时用 showToast(props);组件挂载容器由内部懒创建,样式随 ui.css 一起引入",
|
"需要完整入参(含 variant)时用 showToast(props);组件挂载容器由内部懒创建,样式随 ui.css 一起引入",
|
||||||
"同屏最多 4 条,超出自动挤掉最早一条;需要用户主动确认的消息用 showToast({ closable: true }) 给个 ×",
|
"同屏最多 4 条,超出自动挤掉最早一条;需要用户主动确认的消息用 showToast({ closable: true }) 给个 ×",
|
||||||
|
|||||||
@@ -1543,16 +1543,14 @@ button.ua-step-dot:hover {
|
|||||||
font-size: var(--ua-font-size-md);
|
font-size: var(--ua-font-size-md);
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateX(var(--ua-translate-12));
|
transform: translateX(var(--ua-translate-12));
|
||||||
|
/* 自己接指针:悬停可暂停倒计时(宿主仍穿透,只有卡片本体这一小块可命中) */
|
||||||
|
pointer-events: auto;
|
||||||
transition: opacity var(--ua-duration-base) var(--ua-ease-out), transform var(--ua-duration-base) var(--ua-ease-out);
|
transition: opacity var(--ua-duration-base) var(--ua-ease-out), transform var(--ua-duration-base) var(--ua-ease-out);
|
||||||
}
|
}
|
||||||
.ua-toast.ua-open {
|
.ua-toast.ua-open {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: translateX(0);
|
transform: translateX(0);
|
||||||
}
|
}
|
||||||
/* 仅带关闭按钮的 toast 接收指针,其余保持穿透不挡页面 */
|
|
||||||
.ua-toast.is-closable {
|
|
||||||
pointer-events: auto;
|
|
||||||
}
|
|
||||||
.ua-toast-close {
|
.ua-toast-close {
|
||||||
padding: 0 var(--ua-space-05);
|
padding: 0 var(--ua-space-05);
|
||||||
border: none;
|
border: none;
|
||||||
@@ -4219,9 +4217,6 @@ button.ua-step-dot:hover {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* —— toast 操作按钮(撤销类) —— */
|
/* —— toast 操作按钮(撤销类) —— */
|
||||||
.ua-toast.is-action {
|
|
||||||
pointer-events: auto;
|
|
||||||
}
|
|
||||||
.ua-toast-action {
|
.ua-toast-action {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
border: none;
|
border: none;
|
||||||
|
|||||||
Reference in New Issue
Block a user