iter: b01 新增 tooltip 悬浮提示组件

- packages/skin/ui/src/tooltip.ts:包裹式 API tooltip(props, trigger),hover/focus 150ms 延迟出场,移出/失焦/Esc 收起
- types.ts 增 TooltipProps;index.ts 导出;ui.css 增气泡样式(纯 --ua-* 令牌 + 出入场过渡 + reduced-motion 兜底)
- ua.manifest.json / llms-full.txt / .ua/index.json / llms.txt 同步;demo 增 sec-tooltip 区块
- backlog b01 置 done,iterations.log 留痕
This commit is contained in:
HIsheR
2026-10-23 15:34:23 +08:00
parent e9165ead18
commit a57577a995
13 changed files with 228 additions and 12 deletions
+1
View File
@@ -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 { tooltip } from './tooltip.js';
export { spinner } from './spinner.js';
export { progress } from './progress.js';
export { tabs } from './tabs.js';
+52
View File
@@ -0,0 +1,52 @@
import { h, signal } from '@ui-arch/frame';
import type { TooltipProps } from './types.js';
let seq = 0; // 全局自增序号:保证每个气泡 id 唯一,供 aria-describedby 关联
/*** 悬浮提示:包裹触发器,hover / focus 延迟出场,移出 / 失焦 / Esc 收起,纯展示不拦截指针 ***/
export function tooltip(props: TooltipProps, trigger: HTMLElement): HTMLElement {
// —— 状态集中声明 ——
const open = signal(false); // 气泡显隐
const id = `ua-tip-${++seq}`; // 气泡唯一 id
const delay = props.delay ?? 150; // 出场延迟毫秒;<=0 视为立即显示
let timer: number | null = null; // 出场延迟计时器句柄
const show = () => {
if (timer != null) window.clearTimeout(timer); // 重复进入先清旧计时
if (delay <= 0) return open(true);
timer = window.setTimeout(() => {
timer = null;
open(true);
}, delay);
};
const hide = () => {
if (timer != null) { // 还没出场就离开:直接取消计时,避免闪一下
window.clearTimeout(timer);
timer = null;
}
open(false);
};
// —— 触发器与气泡 ——
trigger.setAttribute('aria-describedby', id); // 无障碍:读屏把提示文案关联到触发器
const bubble = h(
'span',
{
id,
class: () => `ua-tip ua-tip-${props.placement ?? 'top'}${open() ? ' ua-open' : ''}`,
role: 'tooltip',
'aria-hidden': () => String(!open()),
},
props.content,
);
const root = h('span', { class: 'ua-tip-wrap' }, trigger, bubble);
root.addEventListener('pointerenter', show); // 鼠标进入
root.addEventListener('pointerleave', hide); // 鼠标离开
root.addEventListener('focusin', show); // 键盘聚焦(focusin 会冒泡)
root.addEventListener('focusout', hide); // 失焦
root.addEventListener('keydown', (e: KeyboardEvent) => {
if (e.key === 'Escape') hide(); // Esc 立即收起,不必等延迟
});
return root;
}
+7
View File
@@ -1,6 +1,13 @@
import type { ColumnDef, RowKey } from '@ui-arch/table-core';
import type { Signal } from '@ui-arch/frame';
/*** 悬浮提示入参 ***/
export interface TooltipProps {
content: string; // 提示文案(纯文本)
placement?: 'top' | 'bottom'; // 气泡方位;缺省 top(触发器上方)
delay?: number; // 出场延迟毫秒;缺省 150,传 0 立即显示
}
/*** 按钮入参 ***/
export interface ButtonProps {
label: string; // 按钮文案
+42 -2
View File
@@ -1,7 +1,7 @@
{
"name": "@ui-arch/ui",
"type": "skin-dom",
"description": "DOM 皮肤组件库:button / textField / textarea / checkbox / switch / select / radioGroup / card / badge / alert / 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 / 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",
"tooltip(props, trigger):悬浮提示,包裹触发器,hover / focus 延迟 150ms 出场,移出 / 失焦 / Esc 收起",
"spinner(props):加载指示器,size 控制直径",
"progress(props):进度条,轨道 + 渐变流动填充,value 信号驱动 0-100,满格切 success 色",
"tabs(props):标签页,active 信号双向绑定,滑动指示条 + 方向键环形导航,支持禁用页签",
@@ -43,6 +44,7 @@
"CardProps",
"BadgeProps",
"AlertProps",
"TooltipProps",
"SpinnerProps",
"ProgressProps",
"TabsProps",
@@ -61,7 +63,7 @@
"语义色 variant 四档:default|info / success / warning / danger,一律来自令牌,禁止硬编码颜色"
],
"docs": {
"intro": "16 个 DOM 组件,统一用法:组件是普通函数,传入 props 返回 DOM 节点;需要状态时先创建 signal 再双向绑定。引入 CSS:import '@ui-arch/ui/ui.css'(内部已消费 tokens)。",
"intro": "17 个 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'",
@@ -356,6 +358,44 @@
"传入 message 即可,入场自带下滑动画"
]
},
{
"id": "tooltip",
"title": "悬浮提示 Tooltip",
"demo": "sec-tooltip",
"props": [
{
"n": "content",
"t": "string",
"r": 1,
"d": "提示文案(纯文本)"
},
{
"n": "placement",
"t": "'top' | 'bottom'",
"d": "气泡方位;缺省 top(触发器上方)"
},
{
"n": "delay",
"t": "number",
"d": "出场延迟毫秒;缺省 150,传 0 立即显示"
},
{
"n": "trigger",
"t": "HTMLElement",
"r": 1,
"d": "第二个参数:被包裹的触发器元素(按钮 / 输入框等任意 DOM)"
}
],
"example": "tooltip({ content: '保存后立即生效' }, button({ label: '保存', onClick: save }))",
"steps": [
"先创建触发器元素,如 const btn = button({ label: '保存' })",
"用 tooltip(props, trigger) 包一层:返回带气泡的包裹元素,原触发器仍是独立节点",
"鼠标移入或键盘聚焦约 150ms 后出场,移出 / 失焦立即收起;按 Esc 可立刻关闭",
"气泡默认在上方,空间不足或需要避让时传 placement: 'bottom'",
"气泡 pointer-events: none 不抢指针;判读依赖 aria-describedby + role='tooltip',读屏可读",
"只做展示:不要在气泡里放可点击元素,需要交互请用 modal 或自绘弹层"
]
},
{
"id": "spinner",
"title": "加载指示器 Spinner",
+34
View File
@@ -613,6 +613,39 @@
box-shadow: 0 0 0 3px color-mix(in srgb, var(--ua-color-accent) 18%, transparent);
}
/* —— 悬浮提示 —— */
.ua-tip-wrap {
position: relative;
display: inline-flex;
}
.ua-tip {
position: absolute;
left: 50%;
z-index: 30;
max-width: 220px;
padding: var(--ua-space-1) var(--ua-space-2);
border-radius: var(--ua-radius-sm);
background: var(--ua-color-text);
color: var(--ua-color-bg);
font-size: var(--ua-font-size-sm);
line-height: 1.5;
pointer-events: none;
opacity: 0;
transition: opacity 0.16s ease-out, transform 0.16s ease-out;
}
.ua-tip-top {
bottom: calc(100% + 6px);
transform: translateX(-50%) translateY(3px);
}
.ua-tip-bottom {
top: calc(100% + 6px);
transform: translateX(-50%) translateY(-3px);
}
.ua-tip.ua-open {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
/* —— 无障碍:用户偏好减少动效时关闭全部动画 —— */
@media (prefers-reduced-motion: reduce) {
.ua-btn,
@@ -630,6 +663,7 @@
.ua-progress-fill,
.ua-progress-fill::after,
.ua-alert,
.ua-tip,
.ua-form-error,
.ua-modal,
.ua-modal-overlay {