diff --git a/.ua/index.json b/.ua/index.json index 7e22330..fefc562 100644 --- a/.ua/index.json +++ b/.ua/index.json @@ -1,6 +1,6 @@ { "$schema": "ui-arch-registry-index/0.1", - "generatedAt": "2026-10-23T11:03:12.689Z", + "generatedAt": "2026-10-26T02:16:50.161Z", "repoRoot": ".", "packages": [ { @@ -176,7 +176,8 @@ "form(props, items):表单容器,布局 + 聚合校验,全部通过才触发 onSubmit", "formItem(props, control):表单字段,label + 必填红星 + 规则校验 + 错误区,返回 { el, validate } 句柄", "dataTable(props):数据表格,粘性表头 / 行选择(全选三态)/ 排序 / 筛选 / 分页 / 空状态;loading 为真时数据行换骨架行且筛选/翻页禁用,内核为 table-core", - "image(props):图片,缩略图点击 / 回车开遮罩大图预览(Esc 或点击遮罩关闭),width 控制缩略宽" + "image(props):图片,缩略图点击 / 回车开遮罩大图预览(Esc 或点击遮罩关闭),width 控制缩略宽", + "timeline(props):时间线,items 按序渲染纵向事件流(标题 / 时间 / 正文 + 语义色节点),mode 控制轴线左右,pending 追加进行中节点" ], "keyTypes": [ "ButtonProps", @@ -213,7 +214,9 @@ "SliderProps", "TagProps", "DrawerProps", - "AvatarProps" + "AvatarProps", + "TimelineItem", + "TimelineProps" ] }, "usage": "const checked = signal(false); checkbox({ label: '同意', checked }); const open = signal(false); modal({ title: '确认', open }, '内容')", @@ -1579,6 +1582,40 @@ "需要独立分页器联动时传 page 信号:const page = signal(1),dataTable({ page }) 与 pagination({ page }) 共用", "需要批量操作时传 selected 信号:内部勾选实时写回,外部置空或赋值即同步整表(跨页保留,按 rowKey 存)" ] + }, + { + "id": "timeline", + "title": "时间线 Timeline", + "demo": "sec-timeline", + "props": [ + { + "n": "items", + "t": "TimelineItem[]", + "r": 1, + "d": "条目列表,按时间顺序自上而下;每项含 title(必填)、content、time、variant" + }, + { + "n": "mode", + "t": "'left' | 'right'", + "d": "轴线与节点所在侧;缺省 left(内容在右),right 时内容左对齐轴线居右", + "df": "left" + }, + { + "n": "pending", + "t": "boolean", + "d": "末尾追加虚线「进行中…」节点,表示流程尚未结束;缺省 false", + "df": "false" + } + ], + "example": "timeline({ items: [{ title: '提交审批', time: '09:12', variant: 'success' }, { title: '财务复核', time: '10:30', content: '等待张工处理' }], pending: true })", + "steps": [ + "事件流按时间顺序排(组件不做排序):较早的条目放前面,最新的放最后", + "variant 只承载语义:success 完成、warning 待处理、danger 失败、default 普通记录(节点色走 --ua-* 令牌,随暗色主题变化)", + "time 是纯展示文案,格式自由('09:12' / '10-23 18:00'),组件不做日期解析", + "流程未结束时传 pending: true,末尾自动补一个虚线脉冲节点,完成后置回 false 即消失", + "需要轴线居右(如右侧栏时间流)时传 mode: 'right',条目与节点整体镜像", + "content 为纯文本;需要按钮等交互内容时改用 accordion / card 组合" + ] } ] }, diff --git a/AGENTS.md b/AGENTS.md index 402aefb..720697c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -21,7 +21,7 @@ - `packages/core/table-core`(@ui-arch/table-core):Headless 表格逻辑内核 - `packages/core/frame`(@ui-arch/frame):signal + hyperscript 微框架 -- `packages/skin/ui`(@ui-arch/ui):34 个 DOM 组件(表单 / 反馈 / 浮层 / 导航 / 表格),样式只消费令牌 +- `packages/skin/ui`(@ui-arch/ui):35 个 DOM 组件(表单 / 反馈 / 浮层 / 导航 / 表格),样式只消费令牌 - `packages/design/tokens`(@ui-arch/tokens):设计令牌 - `packages/tools/cli`(@ui-arch/cli):`ua` 命令行 + MCP 注册表服务(private) - `apps/demo`(@ui-arch/demo):交互演示,产物 apps/demo/dist/demo.html diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 95d2926..c0156ea 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -34,7 +34,7 @@ ui-arch-mvp/ │ │ ├── table-core/ # 表格状态内核 │ │ └── frame/ # 信号 + hyperscript 微框架 │ ├── skin/ # 皮肤层(DOM 组件,可发布) -│ │ └── ui/ # 34 个组件 + ui.css +│ │ └── ui/ # 35 个组件 + ui.css │ ├── design/ # 资产层(可发布) │ │ └── tokens/ # --ua-* 设计令牌 │ └── tools/ # 工程层(private,不发布) diff --git a/README.md b/README.md index f58e550..0635194 100644 --- a/README.md +++ b/README.md @@ -11,13 +11,13 @@ npm run demo # 打开演示:apps/demo/dist/demo.html npm run docs # 生成人类文档站 apps/docs/index.html + Agent 全量文档 llms-full.txt ``` -## 组件(共 34 个) +## 组件(共 35 个) -accordion · alert · avatar · badge · breadcrumb · button · card · checkbox · dataTable · divider · drawer · empty · form · image · inputNumber · menu · modal · pagination · popover · progress · radioGroup · rate · select · skeleton · slider · spinner · stepper · switchButton · tabs · tag · textField · textarea · toast · tooltip +accordion · alert · avatar · badge · breadcrumb · button · card · checkbox · dataTable · divider · drawer · empty · form · image · inputNumber · menu · modal · pagination · popover · progress · radioGroup · rate · select · skeleton · slider · spinner · stepper · switchButton · tabs · tag · textField · textarea · timeline · toast · tooltip -分组:表单(button / textField / inputNumber / textarea / checkbox / switchButton / select / radioGroup / slider / rate / form / formItem)、反馈(alert / toast / spinner / progress / skeleton / badge / empty)、浮层(tooltip / popover / modal / drawer / menu)、导航(tabs / breadcrumb / pagination / accordion / stepper)、数据(dataTable / tag / avatar / card)、其他(divider)。 +分组:表单(button / textField / inputNumber / textarea / checkbox / switchButton / select / radioGroup / slider / rate / form / formItem)、反馈(alert / toast / spinner / progress / skeleton / badge / empty)、浮层(tooltip / popover / modal / drawer / menu)、导航(tabs / breadcrumb / pagination / accordion / stepper)、数据(dataTable / tag / avatar / card)、其他(divider / timeline)。 ## 目录 @@ -25,7 +25,7 @@ accordion · alert · avatar · badge · breadcrumb · button · card · checkbo |---|---| | `packages/core/frame` | @ui-arch/frame:signal / computed / effect + hyperscript(无虚拟 DOM) | | `packages/core/table-core` | @ui-arch/table-core:headless 表格(filter → sort → paginate 派生管线) | -| `packages/skin/ui` | @ui-arch/ui:34 个 DOM 组件 + ui.css(只消费 `--ua-*` 令牌) | +| `packages/skin/ui` | @ui-arch/ui:35 个 DOM 组件 + ui.css(只消费 `--ua-*` 令牌) | | `packages/design/tokens` | @ui-arch/tokens:设计令牌(暗色只换值不换名) | | `packages/tools/cli` | @ui-arch/cli:`ua` 命令行与 stdio MCP server | | `apps/demo` | 交互演示(esbuild 打包成单文件) | diff --git a/apps/demo/main.ts b/apps/demo/main.ts index f4072ef..8d40c21 100644 --- a/apps/demo/main.ts +++ b/apps/demo/main.ts @@ -42,6 +42,7 @@ import { tag, textarea, textField, + timeline, toast, tooltip, } from '@ui-arch/ui'; @@ -151,6 +152,9 @@ const menuPick = signal(''); // —— 禁用态演示:开关信号同时绑定 switch 的 checked 与 checkbox 的 disabled —— const locked = signal(true); +// —— 时间线:false 时末尾显示「进行中」虚线节点,true 表示流程已结束 —— +const flowDone = signal(false); + const content = h( 'div', { class: 'page' }, @@ -440,6 +444,24 @@ const content = h( button({ label: '导出', variant: 'ghost' }), ), ), + section( + 'sec-timeline', + '时间线', + 'timeline:items 自左向右排事件流,variant 给节点语义色;pending 追加虚线「进行中」节点,点按钮切换。', + h( + 'div', + { class: 'row' }, + button({ label: '流程继续/结束', variant: 'ghost', onClick: () => flowDone(!flowDone()) }), + ), + () => timeline({ + items: [ + { title: '提交审批', time: '09:12', content: '王小明提交了报销单 RB-2048', variant: 'success' }, + { title: '财务复核', time: '10:30', content: '金额与发票一致,待张工确认', variant: 'warning' }, + { title: '打款失败', time: '14:05', content: '收款账户信息有误,已退回修改', variant: 'danger' }, + ], + pending: !flowDone(), + }), + ), section( 'sec-menu', '下拉菜单', diff --git a/llms-full.txt b/llms-full.txt index 422beb4..ed81afa 100644 --- a/llms-full.txt +++ b/llms-full.txt @@ -1,6 +1,6 @@ # @ui-arch 组件库 Agent 文档(无图形版) -> 自动生成于 2026-10-23T11:03:12.689Z,数据源:各包 ua.manifest.json,勿手改。人类版文档站:apps/docs/index.html。 +> 自动生成于 2026-10-26T02:16:50.161Z,数据源:各包 ua.manifest.json,勿手改。人类版文档站:apps/docs/index.html。 > 组件 = 普通函数,传 props 返回 DOM;状态用 signal 双向绑定:读 signal() 写 signal(v)。 ## 快速上手 @@ -898,6 +898,31 @@ dataTable({ data: users, selectable: true, columns: [{ key: 'name', header: '姓 现场演示:apps/docs/demo.html#sec-table +### timeline — 时间线 Timeline + +参数: + +- items(必填):TimelineItem[] —— 条目列表,按时间顺序自上而下;每项含 title(必填)、content、time、variant +- mode(默认 left):'left' | 'right' —— 轴线与节点所在侧;缺省 left(内容在右),right 时内容左对齐轴线居右 +- pending(默认 false):boolean —— 末尾追加虚线「进行中…」节点,表示流程尚未结束;缺省 false + +示例: + +``` +timeline({ items: [{ title: '提交审批', time: '09:12', variant: 'success' }, { title: '财务复核', time: '10:30', content: '等待张工处理' }], pending: true }) +``` + +手把手: + +1. 事件流按时间顺序排(组件不做排序):较早的条目放前面,最新的放最后 +2. variant 只承载语义:success 完成、warning 待处理、danger 失败、default 普通记录(节点色走 --ua-* 令牌,随暗色主题变化) +3. time 是纯展示文案,格式自由('09:12' / '10-23 18:00'),组件不做日期解析 +4. 流程未结束时传 pending: true,末尾自动补一个虚线脉冲节点,完成后置回 false 即消失 +5. 需要轴线居右(如右侧栏时间流)时传 mode: 'right',条目与节点整体镜像 +6. content 为纯文本;需要按钮等交互内容时改用 accordion / card 组合 + +现场演示:apps/docs/demo.html#sec-timeline + API 速览: - button(props):按钮,variant = primary | ghost | danger,disabled 支持布尔或信号禁用 @@ -935,6 +960,7 @@ API 速览: - formItem(props, control):表单字段,label + 必填红星 + 规则校验 + 错误区,返回 { el, validate } 句柄 - dataTable(props):数据表格,粘性表头 / 行选择(全选三态)/ 排序 / 筛选 / 分页 / 空状态;loading 为真时数据行换骨架行且筛选/翻页禁用,内核为 table-core - image(props):图片,缩略图点击 / 回车开遮罩大图预览(Esc 或点击遮罩关闭),width 控制缩略宽 +- timeline(props):时间线,items 按序渲染纵向事件流(标题 / 时间 / 正文 + 语义色节点),mode 控制轴线左右,pending 追加进行中节点 一行用法: diff --git a/llms.txt b/llms.txt index 4cf0c7d..75c1e0b 100644 --- a/llms.txt +++ b/llms.txt @@ -82,6 +82,7 @@ - formItem(props, control):表单字段,label + 必填红星 + 规则校验 + 错误区,返回 { el, validate } 句柄 - dataTable(props):数据表格,粘性表头 / 行选择(全选三态)/ 排序 / 筛选 / 分页 / 空状态;loading 为真时数据行换骨架行且筛选/翻页禁用,内核为 table-core - image(props):图片,缩略图点击 / 回车开遮罩大图预览(Esc 或点击遮罩关闭),width 控制缩略宽 + - timeline(props):时间线,items 按序渲染纵向事件流(标题 / 时间 / 正文 + 语义色节点),mode 控制轴线左右,pending 追加进行中节点 - 用法: `const checked = signal(false); checkbox({ label: '同意', checked }); const open = signal(false); modal({ title: '确认', open }, '内容')` - 约定: 皮肤不持有业务状态,一律委托 headless 内核或调用方信号 - 约定: 表单类组件统一约定:*Props.value/checked/active 为 Signal 双向绑定,onChange/onInput 为回调 diff --git a/packages/skin/ui/src/index.ts b/packages/skin/ui/src/index.ts index ae400b6..46b2d07 100644 --- a/packages/skin/ui/src/index.ts +++ b/packages/skin/ui/src/index.ts @@ -29,6 +29,7 @@ export { stepper } from './stepper.js'; export { rate } from './rate.js'; export { empty } from './empty.js'; export { divider } from './divider.js'; +export { timeline } from './timeline.js'; export { modal } from './modal.js'; export { drawer } from './drawer.js'; export { form, formItem } from './form.js'; diff --git a/packages/skin/ui/src/timeline.ts b/packages/skin/ui/src/timeline.ts new file mode 100644 index 0000000..5c38f6f --- /dev/null +++ b/packages/skin/ui/src/timeline.ts @@ -0,0 +1,20 @@ +import { h } from '@ui-arch/frame'; +import type { TimelineItem, TimelineProps } from './types.js'; + +/*** 单个时间线节点:绝对定位的语义色圆点 + 标题 / 时间 / 正文,pending 节点为虚线空心圈 ***/ +function node(item: TimelineItem, pending: boolean): HTMLElement { + const dot = h('span', { class: `ua-tl-dot ua-tl-dot-${pending ? 'pending' : item.variant ?? 'default'}` }); + const head = h('div', { class: 'ua-tl-head' }, + h('span', { class: 'ua-tl-title' }, item.title), + item.time ? h('span', { class: 'ua-tl-time' }, item.time) : null); + const body = item.content ? h('p', { class: 'ua-tl-content' }, item.content) : null; + return h('li', { class: `ua-tl-item${pending ? ' is-pending' : ''}` }, dot, h('div', { class: 'ua-tl-body' }, head, body)); +} + +/*** 时间线:纵向事件流,mode 决定轴线在左还是在右;pending 为真时末尾补一个进行中节点 ***/ +export function timeline(props: TimelineProps): HTMLElement { + const rows: TimelineItem[] = [...props.items]; // 复制入参,避免改动调用方数组 + if (props.pending) rows.push({ title: '进行中…' }); // 尾部虚节点表示流程尚未结束 + const items = rows.map((row, i) => node(row, !!props.pending && i === rows.length - 1)); + return h('ol', { class: `ua-tl ua-tl-${props.mode ?? 'left'}` }, ...items); +} diff --git a/packages/skin/ui/src/types.ts b/packages/skin/ui/src/types.ts index 2f99caa..2b36b23 100644 --- a/packages/skin/ui/src/types.ts +++ b/packages/skin/ui/src/types.ts @@ -348,4 +348,19 @@ export interface FormProps { onReset?: () => void; // 重置回调 } +/*** 时间线条目 ***/ +export interface TimelineItem { + title: string; // 条目标题(事件名) + content?: string; // 条目正文补充;缺省不渲染 + time?: string; // 时间文案(如 '09:30' / '10-23');缺省不渲染 + variant?: 'default' | 'success' | 'warning' | 'danger'; // 节点语义色;缺省 default +} + +/*** 时间线入参 ***/ +export interface TimelineProps { + items: TimelineItem[]; // 条目列表,按时间顺序自上而下 + mode?: 'left' | 'right'; // 轴线与节点所在侧;缺省 left(内容在右) + pending?: boolean; // 末尾追加「进行中」虚线节点,表示流程未结束;缺省 false +} + export type { ColumnDef, RowKey }; // 再导出常用内核类型,方便调用方 diff --git a/packages/skin/ui/ua.manifest.json b/packages/skin/ui/ua.manifest.json index 749d1d9..a66a7b0 100644 --- a/packages/skin/ui/ua.manifest.json +++ b/packages/skin/ui/ua.manifest.json @@ -47,7 +47,8 @@ "form(props, items):表单容器,布局 + 聚合校验,全部通过才触发 onSubmit", "formItem(props, control):表单字段,label + 必填红星 + 规则校验 + 错误区,返回 { el, validate } 句柄", "dataTable(props):数据表格,粘性表头 / 行选择(全选三态)/ 排序 / 筛选 / 分页 / 空状态;loading 为真时数据行换骨架行且筛选/翻页禁用,内核为 table-core", - "image(props):图片,缩略图点击 / 回车开遮罩大图预览(Esc 或点击遮罩关闭),width 控制缩略宽" + "image(props):图片,缩略图点击 / 回车开遮罩大图预览(Esc 或点击遮罩关闭),width 控制缩略宽", + "timeline(props):时间线,items 按序渲染纵向事件流(标题 / 时间 / 正文 + 语义色节点),mode 控制轴线左右,pending 追加进行中节点" ], "keyTypes": [ "ButtonProps", @@ -84,7 +85,9 @@ "SliderProps", "TagProps", "DrawerProps", - "AvatarProps" + "AvatarProps", + "TimelineItem", + "TimelineProps" ] }, "usage": "const checked = signal(false); checkbox({ label: '同意', checked }); const open = signal(false); modal({ title: '确认', open }, '内容')", @@ -1450,6 +1453,40 @@ "需要独立分页器联动时传 page 信号:const page = signal(1),dataTable({ page }) 与 pagination({ page }) 共用", "需要批量操作时传 selected 信号:内部勾选实时写回,外部置空或赋值即同步整表(跨页保留,按 rowKey 存)" ] + }, + { + "id": "timeline", + "title": "时间线 Timeline", + "demo": "sec-timeline", + "props": [ + { + "n": "items", + "t": "TimelineItem[]", + "r": 1, + "d": "条目列表,按时间顺序自上而下;每项含 title(必填)、content、time、variant" + }, + { + "n": "mode", + "t": "'left' | 'right'", + "d": "轴线与节点所在侧;缺省 left(内容在右),right 时内容左对齐轴线居右", + "df": "left" + }, + { + "n": "pending", + "t": "boolean", + "d": "末尾追加虚线「进行中…」节点,表示流程尚未结束;缺省 false", + "df": "false" + } + ], + "example": "timeline({ items: [{ title: '提交审批', time: '09:12', variant: 'success' }, { title: '财务复核', time: '10:30', content: '等待张工处理' }], pending: true })", + "steps": [ + "事件流按时间顺序排(组件不做排序):较早的条目放前面,最新的放最后", + "variant 只承载语义:success 完成、warning 待处理、danger 失败、default 普通记录(节点色走 --ua-* 令牌,随暗色主题变化)", + "time 是纯展示文案,格式自由('09:12' / '10-23 18:00'),组件不做日期解析", + "流程未结束时传 pending: true,末尾自动补一个虚线脉冲节点,完成后置回 false 即消失", + "需要轴线居右(如右侧栏时间流)时传 mode: 'right',条目与节点整体镜像", + "content 为纯文本;需要按钮等交互内容时改用 accordion / card 组合" + ] } ] } diff --git a/packages/skin/ui/ui.css b/packages/skin/ui/ui.css index 728b48e..002680a 100644 --- a/packages/skin/ui/ui.css +++ b/packages/skin/ui/ui.css @@ -1647,3 +1647,110 @@ button.ua-step-dot:hover { font-size: var(--ua-font-size-md); user-select: none; } + +/* —— 时间线:纵向轴线 + 语义色节点,mode=right 时轴线居右 —— */ +.ua-tl { + position: relative; + margin: 0; + padding: 0; + list-style: none; +} +.ua-tl::before { + content: ''; + position: absolute; + left: 5px; + top: 6px; + bottom: 0; + width: 1px; + background: var(--ua-color-border); +} +.ua-tl-right::before { + left: auto; + right: 5px; +} +.ua-tl-item { + position: relative; + padding: 0 0 var(--ua-space-3) 22px; + animation: ua-tl-in 0.18s ease-out both; +} +.ua-tl-item:last-child { + padding-bottom: 0; +} +.ua-tl-right .ua-tl-item { + padding: 0 22px var(--ua-space-3) 0; + text-align: right; +} +.ua-tl-right .ua-tl-item:last-child { + padding-bottom: 0; +} +.ua-tl-dot { + position: absolute; + left: 0; + top: 4px; + width: 11px; + height: 11px; + border-radius: 50%; + background: var(--ua-color-text-muted); + box-shadow: 0 0 0 3px color-mix(in srgb, var(--ua-color-accent) 10%, transparent); + transition: transform 0.15s; +} +.ua-tl-right .ua-tl-dot { + left: auto; + right: 0; +} +.ua-tl-item:hover .ua-tl-dot { + transform: scale(1.15); +} +.ua-tl-dot-default { background: var(--ua-color-accent); } +.ua-tl-dot-success { background: var(--ua-color-success); } +.ua-tl-dot-warning { background: var(--ua-color-warning); } +.ua-tl-dot-danger { background: var(--ua-color-danger); } +.ua-tl-dot-pending { + background: var(--ua-color-bg); + border: 1px dashed var(--ua-color-accent); + animation: ua-tl-pulse 1.6s ease-in-out infinite; +} +.ua-tl-head { + display: flex; + gap: var(--ua-space-2); + align-items: baseline; +} +.ua-tl-right .ua-tl-head { + justify-content: flex-end; +} +.ua-tl-title { + color: var(--ua-color-text); + font-size: var(--ua-font-size-md); + font-weight: 600; +} +.ua-tl-item.is-pending .ua-tl-title { + color: var(--ua-color-text-muted); + font-weight: 500; +} +.ua-tl-time { + color: var(--ua-color-text-muted); + font-size: var(--ua-font-size-sm); + font-variant-numeric: tabular-nums; +} +.ua-tl-content { + margin: 2px 0 0; + color: var(--ua-color-text-muted); + font-size: var(--ua-font-size-sm); + line-height: 1.6; +} +@keyframes ua-tl-in { + from { opacity: 0; transform: translateY(-4px); } + to { opacity: 1; transform: translateY(0); } +} +@keyframes ua-tl-pulse { + 0%, 100% { box-shadow: 0 0 0 2px color-mix(in srgb, var(--ua-color-accent) 18%, transparent); } + 50% { box-shadow: 0 0 0 5px color-mix(in srgb, var(--ua-color-accent) 4%, transparent); } +} +@media (prefers-reduced-motion: reduce) { + .ua-tl-item, + .ua-tl-dot-pending, + .ua-tl-dot { + animation: none !important; + transition: none !important; + } +}