diff --git a/.ua/index.json b/.ua/index.json index 694d5c4..a0a8740 100644 --- a/.ua/index.json +++ b/.ua/index.json @@ -1,6 +1,6 @@ { "$schema": "ui-arch-registry-index/0.1", - "generatedAt": "2026-10-26T04:26:29.979Z", + "generatedAt": "2026-10-26T04:28:43.453Z", "repoRoot": ".", "packages": [ { @@ -827,13 +827,36 @@ "n": "...children", "t": "unknown[]", "d": "任意子节点:字符串 / DOM / 响应式函数" + }, + { + "n": "extra", + "t": "unknown", + "d": "标题行右侧操作区(按钮 / 统计文案);缺省不渲染" + }, + { + "n": "footer", + "t": "unknown", + "d": "底部操作区;缺省不渲染" + }, + { + "n": "bordered", + "t": "boolean", + "d": "是否显示边框;缺省 true" + }, + { + "n": "hoverable", + "t": "boolean", + "d": "悬停浮起(可点击卡片);缺省 false" } ], "example": "card({ title: '用户表' }, dataTable({ data, columns }))", "steps": [ "card 是容器组件:第一个参数是 props,其余参数是任意子节点", "传 title 显示标题行,否则只有内容区", - "子节点可以混合字符串、DOM 节点与响应式函数" + "子节点可以混合字符串、DOM 节点与响应式函数", + "title 与 extra 同处标题行:有 extra 时右侧对齐,没标题也能只放操作区", + "footer 渲染在内容之后的底部区,右对齐并带上分隔线,适合放主次按钮", + "bordered:false 去边框(嵌在已有边界的容器里),hoverable 让整卡悬停浮起(自带 reduced-motion 兜底)" ] }, { diff --git a/apps/demo/main.ts b/apps/demo/main.ts index 27e3814..27d4402 100644 --- a/apps/demo/main.ts +++ b/apps/demo/main.ts @@ -1083,8 +1083,17 @@ const content = h( section( 'sec-card', '卡片容器', - 'card 组件:标题 + 任意子内容,样式全部来自设计令牌。', + 'card 组件:标题行(可带右侧操作区)+ 任意子内容 + 可选底部操作区;bordered / hoverable 控制外观,样式全部来自设计令牌。', card({ title: '令牌驱动' }, h('p', { class: 'desc' }, '这里没有硬编码颜色 —— 全部消费 --ua-* CSS 变量,暗色模式自动切换。')), + card( + { + title: '带操作区的卡片', + extra: h('span', { class: 'desc' }, '共 3 项'), + hoverable: true, + footer: h('div', { class: 'row' }, button({ label: '取消', variant: 'ghost', onClick: () => toast.info('已取消') }), button({ label: '保存', onClick: () => toast.success('已保存') })), + }, + h('p', { class: 'desc' }, '标题行右侧放操作或统计,底部 footer 放主次按钮;hoverable 时整卡悬停浮起。'), + ), ), ); diff --git a/llms-full.txt b/llms-full.txt index 4182790..b867e72 100644 --- a/llms-full.txt +++ b/llms-full.txt @@ -1,6 +1,6 @@ # @ui-arch 组件库 Agent 文档(无图形版) -> 自动生成于 2026-10-26T04:26:29.979Z,数据源:各包 ua.manifest.json,勿手改。人类版文档站:apps/docs/index.html。 +> 自动生成于 2026-10-26T04:28:43.453Z,数据源:各包 ua.manifest.json,勿手改。人类版文档站:apps/docs/index.html。 > 组件 = 普通函数,传 props 返回 DOM;状态用 signal 双向绑定:读 signal() 写 signal(v)。 ## 快速上手 @@ -388,6 +388,10 @@ breadcrumb({ items: [{ label: '首页', href: '/' }, { label: '设置' }, { labe - title:string —— 标题;缺省不渲染标题行 - ...children:unknown[] —— 任意子节点:字符串 / DOM / 响应式函数 +- extra:unknown —— 标题行右侧操作区(按钮 / 统计文案);缺省不渲染 +- footer:unknown —— 底部操作区;缺省不渲染 +- bordered:boolean —— 是否显示边框;缺省 true +- hoverable:boolean —— 悬停浮起(可点击卡片);缺省 false 示例: @@ -400,6 +404,9 @@ card({ title: '用户表' }, dataTable({ data, columns })) 1. card 是容器组件:第一个参数是 props,其余参数是任意子节点 2. 传 title 显示标题行,否则只有内容区 3. 子节点可以混合字符串、DOM 节点与响应式函数 +4. title 与 extra 同处标题行:有 extra 时右侧对齐,没标题也能只放操作区 +5. footer 渲染在内容之后的底部区,右对齐并带上分隔线,适合放主次按钮 +6. bordered:false 去边框(嵌在已有边界的容器里),hoverable 让整卡悬停浮起(自带 reduced-motion 兜底) 现场演示:apps/docs/demo.html#sec-card diff --git a/packages/skin/ui/src/card.ts b/packages/skin/ui/src/card.ts index 8b38cda..0af1278 100644 --- a/packages/skin/ui/src/card.ts +++ b/packages/skin/ui/src/card.ts @@ -1,12 +1,22 @@ import { h } from '@ui-arch/frame'; import type { CardProps } from './types.js'; -/*** 卡片容器:可选标题 + 任意子内容 ***/ +/*** 卡片容器:可选标题行(含右侧操作区)+ 任意子内容 + 可选底部操作区,bordered / hoverable 控制外观 ***/ export function card(props: CardProps, ...children: unknown[]): HTMLElement { + const cls = `ua-card${props.bordered === false ? ' ua-card-borderless' : ''}${props.hoverable ? ' ua-card-hoverable' : ''}`; + const head = props.title || props.extra + ? h( + 'div', + { class: 'ua-card-head' }, + props.title ? h('h3', { class: 'ua-card-title' }, props.title) : h('span', {}), // 无标题时占位,保证操作区仍靠右 + props.extra ? h('span', { class: 'ua-card-extra' }, props.extra) : null, + ) + : null; return h( 'div', - { class: 'ua-card' }, - props.title ? h('h3', { class: 'ua-card-title' }, props.title) : null, + { class: cls }, + head, ...children, - ); + props.footer ? h('div', { class: 'ua-card-foot' }, props.footer) : null, // 底部操作区:右对齐,仅传了才渲染 + ) as HTMLElement; } diff --git a/packages/skin/ui/src/types.ts b/packages/skin/ui/src/types.ts index 453ebab..8df3a7d 100644 --- a/packages/skin/ui/src/types.ts +++ b/packages/skin/ui/src/types.ts @@ -40,7 +40,11 @@ export interface TextFieldProps { /*** 卡片入参 ***/ export interface CardProps { - title?: string; // 标题;缺省不渲染标题行 + title?: string; // 标题;缺省不渲染标题行 + extra?: unknown; // 标题行右侧操作区(按钮 / 链接);缺省不渲染 + footer?: unknown; // 底部操作区;缺省不渲染 + bordered?: boolean; // 是否显示边框;缺省 true + hoverable?: boolean; // 悬停浮起(可点击卡片);缺省 false } /*** 皮肤层列定义:内核列定义 + 纯渲染字段(对齐 / 文案格式化),不参与排序筛选语义 ***/ diff --git a/packages/skin/ui/ua.manifest.json b/packages/skin/ui/ua.manifest.json index 152a21b..e57189e 100644 --- a/packages/skin/ui/ua.manifest.json +++ b/packages/skin/ui/ua.manifest.json @@ -698,13 +698,36 @@ "n": "...children", "t": "unknown[]", "d": "任意子节点:字符串 / DOM / 响应式函数" + }, + { + "n": "extra", + "t": "unknown", + "d": "标题行右侧操作区(按钮 / 统计文案);缺省不渲染" + }, + { + "n": "footer", + "t": "unknown", + "d": "底部操作区;缺省不渲染" + }, + { + "n": "bordered", + "t": "boolean", + "d": "是否显示边框;缺省 true" + }, + { + "n": "hoverable", + "t": "boolean", + "d": "悬停浮起(可点击卡片);缺省 false" } ], "example": "card({ title: '用户表' }, dataTable({ data, columns }))", "steps": [ "card 是容器组件:第一个参数是 props,其余参数是任意子节点", "传 title 显示标题行,否则只有内容区", - "子节点可以混合字符串、DOM 节点与响应式函数" + "子节点可以混合字符串、DOM 节点与响应式函数", + "title 与 extra 同处标题行:有 extra 时右侧对齐,没标题也能只放操作区", + "footer 渲染在内容之后的底部区,右对齐并带上分隔线,适合放主次按钮", + "bordered:false 去边框(嵌在已有边界的容器里),hoverable 让整卡悬停浮起(自带 reduced-motion 兜底)" ] }, { diff --git a/packages/skin/ui/ui.css b/packages/skin/ui/ui.css index 0404fc5..da0544e 100644 --- a/packages/skin/ui/ui.css +++ b/packages/skin/ui/ui.css @@ -58,11 +58,41 @@ border-radius: var(--ua-radius-lg); background: var(--ua-color-surface); } +.ua-card-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--ua-space-2); + margin-bottom: var(--ua-space-3); +} .ua-card-title { - margin: 0 0 var(--ua-space-3); + margin: 0; font-size: var(--ua-font-size-lg); font-weight: 500; } +.ua-card-extra { + display: inline-flex; + align-items: center; + gap: var(--ua-space-1); +} +.ua-card-foot { + display: flex; + justify-content: flex-end; + gap: var(--ua-space-1); + margin-top: var(--ua-space-3); + padding-top: var(--ua-space-2); + border-top: 1px solid var(--ua-color-border); +} +.ua-card-borderless { + border: none; +} +.ua-card-hoverable { + transition: transform 0.15s ease, box-shadow 0.15s ease; +} +.ua-card-hoverable:hover { + transform: translateY(-2px); + box-shadow: 0 10px 24px color-mix(in srgb, var(--ua-color-text) 14%, transparent); +} .ua-table table { width: 100%; border-collapse: collapse; @@ -3394,3 +3424,11 @@ button.ua-step-dot:hover { .ua-tree-row .ua-tree-box { margin-right: 2px; } +@media (prefers-reduced-motion: reduce) { + .ua-card-hoverable { + transition: none; + } + .ua-card-hoverable:hover { + transform: none; + } +}