From fb9ab88feb41d5b174518145c84468c49a23c84f Mon Sep 17 00:00:00 2001 From: ui-arch-bot Date: Fri, 23 Oct 2026 16:51:48 +0800 Subject: [PATCH] =?UTF-8?q?iter:=20d07=20=E6=96=B0=E5=A2=9E=E5=8F=AF?= =?UTF-8?q?=E4=BA=A4=E4=BA=92=E6=B0=94=E6=B3=A1=E5=8D=A1=E7=89=87=20popove?= =?UTF-8?q?r?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .ua/backlog.json | 8 ++++ .ua/index.json | 66 +++++++++++++++++++++++++++++-- .ua/iterations.log | 1 + apps/demo/main.ts | 23 +++++++++++ llms-full.txt | 33 +++++++++++++++- llms.txt | 3 +- packages/skin/ui/src/index.ts | 1 + packages/skin/ui/src/popover.ts | 60 ++++++++++++++++++++++++++++ packages/skin/ui/src/types.ts | 10 +++++ packages/skin/ui/ua.manifest.json | 64 ++++++++++++++++++++++++++++-- packages/skin/ui/ui.css | 46 +++++++++++++++++++++ 11 files changed, 305 insertions(+), 10 deletions(-) create mode 100644 packages/skin/ui/src/popover.ts diff --git a/.ua/backlog.json b/.ua/backlog.json index 79871d5..2be99cf 100644 --- a/.ua/backlog.json +++ b/.ua/backlog.json @@ -104,6 +104,14 @@ "acceptance": "CLI 新增 verify 子命令:docs.components 的 id 唯一且 api.exports 有 `${id}(` 或以 `${id}.` 开头条目、props 条目必备 n/t/d、steps ≥3、example 必填、demo 锚点必须存在于 apps/demo/main.ts;纳入 npm run check 链条;AGENTS.md 同步;npm run check 全绿", "status": "done", "notes": "锚点校验改看演示源码而非打包产物(demo.html 是 esbuild 产物,锚点在运行时生成);顺带补齐 badge/alert/spinner 的第 3 条手把手(说明与 button/skeleton 的分工);check 链变为 build→style→typecheck→smoke→index→verify" + }, + { + "id": "d07", + "title": "新增可交互气泡卡片 popover 组件", + "motivation": "tooltip 只做纯展示(pointer-events:none),筛选面板/快捷操作需要可点击的浮层;与 tooltip 形成展示/交互分工", + "acceptance": "新增 popover.ts:popover(props, trigger) 包裹式,默认点击开合,trigger:'hover' 走延迟出场,点外部 / Esc 收起(Esc 回焦触发器),content 支持任意 DOM 且内部点击不误关,onOpenChange 回调;aria-haspopup/aria-expanded 正确;只消费 --ua-* 令牌 + reduced-motion 兜底;导出 / manifest / 文档 / llms-full.txt / demo 同步;npm run check 全绿", + "status": "done", + "notes": "触发器 click 内 stopPropagation,避免刚开就被 document 的外部点击关掉;aria-expanded 用 effect 写回(setAttribute 不吃函数值);弹层点击已阻止冒泡;demo 新增 sec-popover(含 checkbox+button 的筛选面板与 hover 触发示例)" } ] } diff --git a/.ua/index.json b/.ua/index.json index 7f3ec4d..04f2aee 100644 --- a/.ua/index.json +++ b/.ua/index.json @@ -1,6 +1,6 @@ { "$schema": "ui-arch-registry-index/0.1", - "generatedAt": "2026-10-23T08:50:48.366Z", + "generatedAt": "2026-10-23T08:51:47.072Z", "repoRoot": ".", "packages": [ { @@ -130,7 +130,7 @@ { "name": "@ui-arch/ui", "type": "skin-dom", - "description": "DOM 皮肤组件库:button / textField / textarea / checkbox / switch / select / menu / radioGroup / accordion / breadcrumb / card / badge / alert / toast / tooltip / skeleton / spinner / progress / tabs / pagination / modal / form / formItem / dataTable,构建在 @ui-arch/frame 与 @ui-arch/table-core 之上,样式只消费 --ua-* 令牌。", + "description": "DOM 皮肤组件库:button / textField / textarea / checkbox / switch / select / menu / radioGroup / accordion / breadcrumb / card / badge / alert / toast / tooltip / popover / skeleton / spinner / progress / tabs / pagination / modal / form / formItem / dataTable,构建在 @ui-arch/frame 与 @ui-arch/table-core 之上,样式只消费 --ua-* 令牌。", "tags": [ "components", "dom", @@ -157,6 +157,7 @@ "alert(props):提示条,variant = info | success | warning | danger", "toast.info/success/warning/error(message, duration?):消息提示工厂,右上角堆叠,默认 3s 自动消失,出入场微动画;showToast(props) 为完整入参版本", "tooltip(props, trigger):悬浮提示,包裹触发器,hover / focus 延迟 150ms 出场,移出 / 失焦 / Esc 收起", + "popover(props, trigger):可交互气泡卡片,包裹触发器,默认点击开合 + 点外部 / Esc 收起,content 可放按钮等交互元素", "skeleton(props):骨架屏占位,variant = text | rect | circle,text 支持 rows 多行,扫光微动画", "pagination(props):独立分页器,page 信号双向绑定,页码过多折叠省略号,首末页禁用翻页,键盘可达", "spinner(props):加载指示器,size 控制直径", @@ -197,7 +198,8 @@ "FormItemProps", "FormItemHandle", "FormProps", - "DataTableProps" + "DataTableProps", + "PopoverProps" ] }, "usage": "const checked = signal(false); checkbox({ label: '同意', checked }); const open = signal(false); modal({ title: '确认', open }, '内容')", @@ -207,7 +209,7 @@ "语义色 variant 四档:default|info / success / warning / danger,一律来自令牌,禁止硬编码颜色" ], "docs": { - "intro": "23 个 DOM 组件,统一用法:组件是普通函数,传入 props 返回 DOM 节点;需要状态时先创建 signal 再双向绑定。引入 CSS:import '@ui-arch/ui/ui.css'(内部已消费 tokens)。", + "intro": "24 个 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'", @@ -757,6 +759,62 @@ "只做展示:不要在气泡里放可点击元素,需要交互请用 modal 或自绘弹层" ] }, + { + "id": "popover", + "title": "气泡卡片 Popover", + "demo": "sec-popover", + "props": [ + { + "n": "content", + "t": "unknown", + "r": 1, + "d": "弹层内容:字符串 / DOM 节点 / 响应式函数,可放按钮等交互元素" + }, + { + "n": "title", + "t": "string", + "d": "弹层标题;缺省不渲染标题行" + }, + { + "n": "placement", + "t": "'top' | 'bottom'", + "d": "弹层方位;缺省 bottom(触发器下方)", + "df": "bottom" + }, + { + "n": "trigger", + "t": "'click' | 'hover'", + "d": "触发方式;缺省 click", + "df": "click" + }, + { + "n": "delay", + "t": "number", + "d": "hover 模式出场延迟毫秒;缺省 150", + "df": "150" + }, + { + "n": "onOpenChange", + "t": "(open: boolean) => void", + "k": 1, + "d": "开合回调" + }, + { + "n": "triggerEl(第二个参数)", + "t": "HTMLElement", + "r": 1, + "d": "第二个参数:被包裹的触发器元素" + } + ], + "example": "popover({ title: '筛选条件', content: h('div', null, '任意可交互内容') }, button({ label: '筛选' }))", + "steps": [ + "创建触发器元素,再用 popover(props, trigger) 包一层:返回带弹层的包裹元素", + "点击触发器开合;点弹层外部或按 Esc 收起(Esc 后焦点回触发器)", + "content 里可以放 button / checkbox 等交互元素(tooltip 只能纯文本),点击弹层内部不会误关", + "hover 场景传 trigger: 'hover' + delay 控制多快出场;placement: 'top' 让弹层往上翻", + "需要感知开合做副作用(如懒加载数据)时用 onOpenChange" + ] + }, { "id": "spinner", "title": "加载指示器 Spinner", diff --git a/.ua/iterations.log b/.ua/iterations.log index 941a2d5..425294a 100644 --- a/.ua/iterations.log +++ b/.ua/iterations.log @@ -11,3 +11,4 @@ {"time": "2026-10-23T16:48:36+08:00", "id": "d04", "result": "done", "summary": "dataTable 新增 loading(布尔|信号):表体换骨架行、筛选与翻页禁用、aria-busy;demo 加开关,manifest/llms 同步,check 全绿"} {"time": "2026-10-23T16:49:45+08:00", "id": "d05", "result": "done", "summary": "新增面包屑 breadcrumb:nav/ol 语义结构 + 末项 aria-current,href 走 、无 href 走 onNavigate,分隔符可配;manifest/llms/demo 同步,check 全绿(中途被 style 门禁拦下多行注释,已改单行)"} {"time": "2026-10-23T16:50:49+08:00", "id": "d06", "result": "done", "summary": "新增 ua verify 门禁:清单 id/exports/props/steps/example 与 demo 锚点一致性,纳入 npm run check;顺带补齐 badge/alert/spinner 手把手第三条,check 全绿"} +{"time": "2026-10-23T16:51:48+08:00", "id": "d07", "result": "done", "summary": "新增可交互气泡卡片 popover:点击/hover 触发、点外部与 Esc 收起、内容可交互且内部点击不误关;manifest/llms/demo 同步,check 全绿"} diff --git a/apps/demo/main.ts b/apps/demo/main.ts index 3aff419..5b167d1 100644 --- a/apps/demo/main.ts +++ b/apps/demo/main.ts @@ -13,6 +13,7 @@ import { menu, modal, pagination, + popover, progress, radioGroup, select, @@ -238,6 +239,28 @@ const app = h( h('span', { class: 'out' }, () => (menuPick() ? `已选:${menuPick()}` : '未选择')), ), ), + section( + 'sec-popover', + '气泡卡片', + 'popover:可交互弹层(tooltip 只展示,popover 里能点),点外部 / Esc 收起。', + h( + 'div', + { class: 'row' }, + popover( + { + title: '筛选条件', + content: h( + 'div', + null, + h('div', { class: 'row' }, checkbox({ label: '只看我的', checked: agreed })), + h('div', { class: 'row' }, button({ label: '应用', onClick: () => toast.success('已应用筛选') })), + ), + }, + button({ label: '打开气泡卡片' }), + ), + popover({ placement: 'top', trigger: 'hover', content: '悬停出场,移出收起(可放链接)' }, badge({ label: 'hover 触发' })), + ), + ), section( 'sec-breadcrumb', '面包屑', diff --git a/llms-full.txt b/llms-full.txt index 427f5a3..b6611c2 100644 --- a/llms-full.txt +++ b/llms-full.txt @@ -1,6 +1,6 @@ # @ui-arch 组件库 Agent 文档(无图形版) -> 自动生成于 2026-10-23T08:50:48.366Z,数据源:各包 ua.manifest.json,勿手改。人类版文档站:apps/docs/index.html。 +> 自动生成于 2026-10-23T08:51:47.072Z,数据源:各包 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 -23 个 DOM 组件,统一用法:组件是普通函数,传入 props 返回 DOM 节点;需要状态时先创建 signal 再双向绑定。引入 CSS:import '@ui-arch/ui/ui.css'(内部已消费 tokens)。 +24 个 DOM 组件,统一用法:组件是普通函数,传入 props 返回 DOM 节点;需要状态时先创建 signal 再双向绑定。引入 CSS:import '@ui-arch/ui/ui.css'(内部已消费 tokens)。 ### button — 按钮 Button @@ -415,6 +415,34 @@ tooltip({ content: '保存后立即生效' }, button({ label: '保存', onClick: 现场演示:apps/docs/demo.html#sec-tooltip +### popover — 气泡卡片 Popover + +参数: + +- content(必填):unknown —— 弹层内容:字符串 / DOM 节点 / 响应式函数,可放按钮等交互元素 +- title:string —— 弹层标题;缺省不渲染标题行 +- placement(默认 bottom):'top' | 'bottom' —— 弹层方位;缺省 bottom(触发器下方) +- trigger(默认 click):'click' | 'hover' —— 触发方式;缺省 click +- delay(默认 150):number —— hover 模式出场延迟毫秒;缺省 150 +- onOpenChange(回调):(open: boolean) => void —— 开合回调 +- triggerEl(第二个参数)(必填):HTMLElement —— 第二个参数:被包裹的触发器元素 + +示例: + +``` +popover({ title: '筛选条件', content: h('div', null, '任意可交互内容') }, button({ label: '筛选' })) +``` + +手把手: + +1. 创建触发器元素,再用 popover(props, trigger) 包一层:返回带弹层的包裹元素 +2. 点击触发器开合;点弹层外部或按 Esc 收起(Esc 后焦点回触发器) +3. content 里可以放 button / checkbox 等交互元素(tooltip 只能纯文本),点击弹层内部不会误关 +4. hover 场景传 trigger: 'hover' + delay 控制多快出场;placement: 'top' 让弹层往上翻 +5. 需要感知开合做副作用(如懒加载数据)时用 onOpenChange + +现场演示:apps/docs/demo.html#sec-popover + ### spinner — 加载指示器 Spinner 参数: @@ -608,6 +636,7 @@ API 速览: - alert(props):提示条,variant = info | success | warning | danger - toast.info/success/warning/error(message, duration?):消息提示工厂,右上角堆叠,默认 3s 自动消失,出入场微动画;showToast(props) 为完整入参版本 - tooltip(props, trigger):悬浮提示,包裹触发器,hover / focus 延迟 150ms 出场,移出 / 失焦 / Esc 收起 +- popover(props, trigger):可交互气泡卡片,包裹触发器,默认点击开合 + 点外部 / Esc 收起,content 可放按钮等交互元素 - skeleton(props):骨架屏占位,variant = text | rect | circle,text 支持 rows 多行,扫光微动画 - pagination(props):独立分页器,page 信号双向绑定,页码过多折叠省略号,首末页禁用翻页,键盘可达 - spinner(props):加载指示器,size 控制直径 diff --git a/llms.txt b/llms.txt index 0f15986..f3ef621 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 / menu / radioGroup / accordion / breadcrumb / card / badge / alert / toast / tooltip / skeleton / spinner / progress / tabs / pagination / modal / form / formItem / dataTable,构建在 @ui-arch/frame 与 @ui-arch/table-core 之上,样式只消费 --ua-* 令牌。 +- 描述: DOM 皮肤组件库:button / textField / textarea / checkbox / switch / select / menu / radioGroup / accordion / breadcrumb / card / badge / alert / toast / tooltip / popover / skeleton / spinner / progress / tabs / pagination / modal / form / formItem / dataTable,构建在 @ui-arch/frame 与 @ui-arch/table-core 之上,样式只消费 --ua-* 令牌。 - API: - button(props):按钮,variant = primary | ghost | danger,disabled 支持布尔或信号禁用 - textField(props):文本输入,value 信号双向绑定,disabled 支持布尔或信号禁用 @@ -62,6 +62,7 @@ - alert(props):提示条,variant = info | success | warning | danger - toast.info/success/warning/error(message, duration?):消息提示工厂,右上角堆叠,默认 3s 自动消失,出入场微动画;showToast(props) 为完整入参版本 - tooltip(props, trigger):悬浮提示,包裹触发器,hover / focus 延迟 150ms 出场,移出 / 失焦 / Esc 收起 + - popover(props, trigger):可交互气泡卡片,包裹触发器,默认点击开合 + 点外部 / Esc 收起,content 可放按钮等交互元素 - skeleton(props):骨架屏占位,variant = text | rect | circle,text 支持 rows 多行,扫光微动画 - pagination(props):独立分页器,page 信号双向绑定,页码过多折叠省略号,首末页禁用翻页,键盘可达 - spinner(props):加载指示器,size 控制直径 diff --git a/packages/skin/ui/src/index.ts b/packages/skin/ui/src/index.ts index 8b45099..a3cd5c3 100644 --- a/packages/skin/ui/src/index.ts +++ b/packages/skin/ui/src/index.ts @@ -14,6 +14,7 @@ export { badge } from './badge.js'; export { alert } from './alert.js'; export { toast, showToast } from './toast.js'; export { tooltip } from './tooltip.js'; +export { popover } from './popover.js'; export { skeleton } from './skeleton.js'; export { pagination } from './pagination.js'; export { spinner } from './spinner.js'; diff --git a/packages/skin/ui/src/popover.ts b/packages/skin/ui/src/popover.ts new file mode 100644 index 0000000..15035d1 --- /dev/null +++ b/packages/skin/ui/src/popover.ts @@ -0,0 +1,60 @@ +import { effect, h, signal } from '@ui-arch/frame'; +import type { PopoverProps } from './types.js'; + +let seq = 0; // 全局自增序号:保证每个弹层 id 唯一,供 aria-controls 关联 + +/*** 气泡卡片:包裹触发器,默认点击开合;内容可交互,点外部 / Esc / 再次点触发器收起 ***/ +export function popover(props: PopoverProps, trigger: HTMLElement): HTMLElement { + // —— 状态集中声明 —— + const open = signal(false); // 弹层显隐 + const id = `ua-pop-${++seq}`; // 弹层唯一 id + let timer: number | null = null; // hover 模式下的出入场计时器 + + const setOpen = (next: boolean): void => { + if (open() === next) return; // 幂等:状态未变不派发回调 + open(next); + props.onOpenChange?.(next); + }; + const toggle = (): void => setOpen(!open()); + + // —— 触发器与弹层 —— + trigger.setAttribute('aria-haspopup', 'dialog'); + effect(() => trigger.setAttribute('aria-expanded', String(open()))); // 展开态跟随信号写回 + const panel = h( + 'div', + { + id, + class: () => `ua-pop ua-pop-${props.placement ?? 'bottom'}${open() ? ' ua-open' : ''}`, + role: 'dialog', + 'aria-hidden': () => String(!open()), + onClick: (e: Event) => e.stopPropagation(), // 弹层内点击不触发外部关闭 + }, + props.title ? h('div', { class: 'ua-pop-title' }, props.title) : null, + h('div', { class: 'ua-pop-body' }, props.content), + ); + const root = h('span', { class: 'ua-pop-wrap' }, trigger, panel); + + if (props.trigger === 'hover') { + root.addEventListener('pointerenter', () => { + if (timer != null) window.clearTimeout(timer); + timer = window.setTimeout(() => setOpen(true), props.delay ?? 150); + }); + root.addEventListener('pointerleave', () => { + if (timer != null) window.clearTimeout(timer); // 未出场就离开:取消计时 + setOpen(false); + }); + } else { + trigger.addEventListener('click', (e: Event) => { + e.stopPropagation(); // 阻止冒泡到 document,否则刚打开就被外部点击关掉 + toggle(); + }); + } + root.addEventListener('keydown', (e: KeyboardEvent) => { + if (e.key !== 'Escape' || !open()) return; // 卫语句:未展开时 Esc 不处理 + setOpen(false); + trigger.focus(); // 收起后焦点回触发器 + }); + document.addEventListener('click', () => setOpen(false)); // 点外部收起(弹层内已阻止冒泡) + + return root; +} diff --git a/packages/skin/ui/src/types.ts b/packages/skin/ui/src/types.ts index 5a362dc..4064863 100644 --- a/packages/skin/ui/src/types.ts +++ b/packages/skin/ui/src/types.ts @@ -8,6 +8,16 @@ export interface TooltipProps { delay?: number; // 出场延迟毫秒;缺省 150,传 0 立即显示 } +/*** 气泡卡片入参 ***/ +export interface PopoverProps { + content: unknown; // 弹层内容:字符串 / DOM 节点 / 响应式函数(可放交互元素) + title?: string; // 弹层标题;缺省不渲染标题行 + placement?: 'top' | 'bottom'; // 弹层方位;缺省 bottom(触发器下方) + trigger?: 'click' | 'hover'; // 触发方式;缺省 click(hover 模式带 delay 延迟) + delay?: number; // hover 模式出场延迟毫秒;缺省 150 + onOpenChange?: (open: boolean) => void; // 开合回调 +} + /*** 按钮入参 ***/ export interface ButtonProps { label: string; // 按钮文案 diff --git a/packages/skin/ui/ua.manifest.json b/packages/skin/ui/ua.manifest.json index ec0bcfb..0714650 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 / menu / radioGroup / accordion / breadcrumb / card / badge / alert / toast / tooltip / skeleton / spinner / progress / tabs / pagination / modal / form / formItem / dataTable,构建在 @ui-arch/frame 与 @ui-arch/table-core 之上,样式只消费 --ua-* 令牌。", + "description": "DOM 皮肤组件库:button / textField / textarea / checkbox / switch / select / menu / radioGroup / accordion / breadcrumb / card / badge / alert / toast / tooltip / popover / skeleton / spinner / progress / tabs / pagination / modal / form / formItem / dataTable,构建在 @ui-arch/frame 与 @ui-arch/table-core 之上,样式只消费 --ua-* 令牌。", "tags": [ "components", "dom", @@ -28,6 +28,7 @@ "alert(props):提示条,variant = info | success | warning | danger", "toast.info/success/warning/error(message, duration?):消息提示工厂,右上角堆叠,默认 3s 自动消失,出入场微动画;showToast(props) 为完整入参版本", "tooltip(props, trigger):悬浮提示,包裹触发器,hover / focus 延迟 150ms 出场,移出 / 失焦 / Esc 收起", + "popover(props, trigger):可交互气泡卡片,包裹触发器,默认点击开合 + 点外部 / Esc 收起,content 可放按钮等交互元素", "skeleton(props):骨架屏占位,variant = text | rect | circle,text 支持 rows 多行,扫光微动画", "pagination(props):独立分页器,page 信号双向绑定,页码过多折叠省略号,首末页禁用翻页,键盘可达", "spinner(props):加载指示器,size 控制直径", @@ -68,7 +69,8 @@ "FormItemProps", "FormItemHandle", "FormProps", - "DataTableProps" + "DataTableProps", + "PopoverProps" ] }, "usage": "const checked = signal(false); checkbox({ label: '同意', checked }); const open = signal(false); modal({ title: '确认', open }, '内容')", @@ -78,7 +80,7 @@ "语义色 variant 四档:default|info / success / warning / danger,一律来自令牌,禁止硬编码颜色" ], "docs": { - "intro": "23 个 DOM 组件,统一用法:组件是普通函数,传入 props 返回 DOM 节点;需要状态时先创建 signal 再双向绑定。引入 CSS:import '@ui-arch/ui/ui.css'(内部已消费 tokens)。", + "intro": "24 个 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'", @@ -628,6 +630,62 @@ "只做展示:不要在气泡里放可点击元素,需要交互请用 modal 或自绘弹层" ] }, + { + "id": "popover", + "title": "气泡卡片 Popover", + "demo": "sec-popover", + "props": [ + { + "n": "content", + "t": "unknown", + "r": 1, + "d": "弹层内容:字符串 / DOM 节点 / 响应式函数,可放按钮等交互元素" + }, + { + "n": "title", + "t": "string", + "d": "弹层标题;缺省不渲染标题行" + }, + { + "n": "placement", + "t": "'top' | 'bottom'", + "d": "弹层方位;缺省 bottom(触发器下方)", + "df": "bottom" + }, + { + "n": "trigger", + "t": "'click' | 'hover'", + "d": "触发方式;缺省 click", + "df": "click" + }, + { + "n": "delay", + "t": "number", + "d": "hover 模式出场延迟毫秒;缺省 150", + "df": "150" + }, + { + "n": "onOpenChange", + "t": "(open: boolean) => void", + "k": 1, + "d": "开合回调" + }, + { + "n": "triggerEl(第二个参数)", + "t": "HTMLElement", + "r": 1, + "d": "第二个参数:被包裹的触发器元素" + } + ], + "example": "popover({ title: '筛选条件', content: h('div', null, '任意可交互内容') }, button({ label: '筛选' }))", + "steps": [ + "创建触发器元素,再用 popover(props, trigger) 包一层:返回带弹层的包裹元素", + "点击触发器开合;点弹层外部或按 Esc 收起(Esc 后焦点回触发器)", + "content 里可以放 button / checkbox 等交互元素(tooltip 只能纯文本),点击弹层内部不会误关", + "hover 场景传 trigger: 'hover' + delay 控制多快出场;placement: 'top' 让弹层往上翻", + "需要感知开合做副作用(如懒加载数据)时用 onOpenChange" + ] + }, { "id": "spinner", "title": "加载指示器 Spinner", diff --git a/packages/skin/ui/ui.css b/packages/skin/ui/ui.css index b893faf..6b697fd 100644 --- a/packages/skin/ui/ui.css +++ b/packages/skin/ui/ui.css @@ -429,6 +429,51 @@ opacity: 0.6; } +/* —— 气泡卡片(可交互弹层,与 tooltip 的分工是 content 里能点) —— */ +.ua-pop-wrap { + position: relative; + display: inline-flex; +} +.ua-pop { + position: absolute; + left: 0; + z-index: 30; + min-width: 200px; + max-width: 280px; + padding: var(--ua-space-3); + border: 1px solid var(--ua-color-border); + border-radius: var(--ua-radius-md); + background: var(--ua-color-bg); + box-shadow: 0 10px 28px rgba(0, 0, 0, 0.16); + opacity: 0; + visibility: hidden; + transition: opacity 0.16s ease, transform 0.16s ease, visibility 0.16s; +} +.ua-pop-top { + bottom: calc(100% + 8px); + transform: translateY(4px); +} +.ua-pop-bottom { + top: calc(100% + 8px); + transform: translateY(-4px); +} +.ua-pop.ua-open { + opacity: 1; + transform: none; + visibility: visible; +} +.ua-pop-title { + margin-bottom: var(--ua-space-2); + color: var(--ua-color-text); + font-size: var(--ua-font-size-lg); + font-weight: 600; +} +.ua-pop-body { + color: var(--ua-color-text-muted); + font-size: var(--ua-font-size-md); + line-height: 1.6; +} + /* —— 折叠面板 —— */ .ua-accordion { display: flex; @@ -999,6 +1044,7 @@ .ua-acc-caret, .ua-acc-body, .ua-crumb-link, + .ua-pop, .ua-switch-track, .ua-switch-thumb, .ua-tab,