From c4384be959f1f32c6dc6d755b91caada5937a91b Mon Sep 17 00:00:00 2001 From: HIsheR Date: Mon, 26 Oct 2026 23:12:33 +0800 Subject: [PATCH] =?UTF-8?q?iter(g284):=20=E6=96=B0=E5=A2=9E=20progressRing?= =?UTF-8?q?=20=E7=8E=AF=E5=BD=A2=E8=BF=9B=E5=BA=A6=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .ua/index.json | 45 +++++++++++++++++++++- AGENTS.md | 2 +- ARCHITECTURE.md | 2 +- README.md | 8 ++-- apps/demo/main.ts | 23 ++++++++++++ llms-full.txt | 29 +++++++++++++- llms.txt | 1 + packages/skin/ui/src/index.ts | 1 + packages/skin/ui/src/progressRing.ts | 56 ++++++++++++++++++++++++++++ packages/skin/ui/src/types.ts | 9 +++++ packages/skin/ui/ua.manifest.json | 45 +++++++++++++++++++++- packages/skin/ui/ui.css | 29 ++++++++++++++ 12 files changed, 239 insertions(+), 11 deletions(-) create mode 100644 packages/skin/ui/src/progressRing.ts diff --git a/.ua/index.json b/.ua/index.json index 2167367..eaa23e6 100644 --- a/.ua/index.json +++ b/.ua/index.json @@ -1,6 +1,6 @@ { "$schema": "ui-arch-registry-index/0.1", - "generatedAt": "2026-10-26T15:08:10.710Z", + "generatedAt": "2026-10-26T15:11:40.469Z", "repoRoot": ".", "packages": [ { @@ -228,7 +228,8 @@ "loading(props, ...children):区域加载遮罩,内容原地降透明度 + 转圈文案,带延迟出场避免快请求闪烁", "relativeTime(props):把时间戳渲染成「3 分钟前」并自动刷新,超阈值改显示绝对日期,title 恒给精确值", "sparkline(props):迷你趋势线,把数值序列压成折线(可填充 + 末点标记),无坐标轴无交互", - "heatmap(props):日历热力图,按周列 × 星期行铺格子、四档浓度表示当天数量,悬停看明细" + "heatmap(props):日历热力图,按周列 × 星期行铺格子、四档浓度表示当天数量,悬停看明细", + "progressRing(props):环形进度,SVG 双圆靠 dasharray 画弧 + 中心百分比,value 可传信号自动重绘" ], "keyTypes": [ "ButtonProps", @@ -4539,6 +4540,46 @@ "格子颜色是相对深浅:按序列最大值归一成四档——所以它说的是「哪天更密」,不是绝对值高低", "默认 12 周、11px 格子;嵌进窄卡片就调小 weeks,看不出格子边界就调大 cellSize" ] + }, + { + "id": "progressRing", + "title": "环形进度 ProgressRing", + "demo": "sec-ring", + "props": [ + { + "n": "value", + "t": "number | Signal", + "d": "当前进度 0-100;超出范围自动夹取,信号变化时自动重绘" + }, + { + "n": "size", + "t": "number", + "d": "外径(px);缺省 96" + }, + { + "n": "thickness", + "t": "number", + "d": "环宽(px);缺省 8" + }, + { + "n": "showText", + "t": "boolean", + "d": "是否显示中心文字;缺省 true(关掉时请自行保证 aria 可读)" + }, + { + "n": "label", + "t": "string", + "d": "中心文字;缺省「N%」" + } + ], + "example": "progressRing({ value: pct, size: 96 })", + "steps": [ + "value 给数字就是静态环,给信号就是活环:不用自己算弧长,组件按 0-100 换算 dasharray", + "0 点固定在 12 点方向顺时针走,和进度条的心理模型一致,不需要额外传旋转参数", + "要放进表格或卡片角落就用 size: 56 + showText: false 的小环,文字由附近的主体数字承担", + "换色在父级设 color:进度弧读 currentColor,底环固定描边色(--ua-color-border),不引入第二个色源", + "这是个「环形进度条」,不是仪表盘:只表达 0-100 单一进度,多段占比请用饼图类组件" + ] } ] }, diff --git a/AGENTS.md b/AGENTS.md index 3b2c33e..44a949b 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):85 个 DOM 组件(表单 / 反馈 / 浮层 / 导航 / 表格),样式只消费令牌 +- `packages/skin/ui`(@ui-arch/ui):86 个 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 bb1b93c..ec2f79e 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -34,7 +34,7 @@ ui-arch-mvp/ │ │ ├── table-core/ # 表格状态内核 │ │ └── frame/ # 信号 + hyperscript 微框架 │ ├── skin/ # 皮肤层(DOM 组件,可发布) -│ │ └── ui/ # 85 个组件 + ui.css +│ │ └── ui/ # 86 个组件 + ui.css │ ├── design/ # 资产层(可发布) │ │ └── tokens/ # --ua-* 设计令牌 │ └── tools/ # 工程层(private,不发布) diff --git a/README.md b/README.md index ac7afe4..64816f4 100644 --- a/README.md +++ b/README.md @@ -12,13 +12,13 @@ npm run docs # 生成人类文档站 apps/docs/index.html + Agent 全 npm run changelog # 依据 git 提交史重写 CHANGELOG.md(发版前跑;changelog:check 可查漂移) ``` -## 组件(共 85 个) +## 组件(共 86 个) -accordion · affix · alert · anchor · autoComplete · avatar · avatarGroup · backTop · badge · breadcrumb · button · calendar · carousel · card · cascader · checkbox · checkboxGroup · codeBlock · colorPicker · commandPalette · copyButton · countdown · dataTable · datePicker · descriptions · diffView · divider · drawer · empty · floatButton · form · grid · heatmap · highlight · image · inputNumber · inputTag · jsonViewer · kbd · layout · link · list · loading · menu · modal · mentions · multiSelect · notification · pagination · pinInput · popover · popconfirm · progress · radioGroup · rate · relativeTime · result · segmented · select · signaturePad · skeleton · slider · space · sparkline · spinner · splitter · statistic · stepper · switchButton · tabs · tag · textField · textarea · timeline · timePicker · toast · tooltip · tour · transfer · tree · treeSelect · typography · upload · virtualList · watermark +accordion · affix · alert · anchor · autoComplete · avatar · avatarGroup · backTop · badge · breadcrumb · button · calendar · carousel · card · cascader · checkbox · checkboxGroup · codeBlock · colorPicker · commandPalette · copyButton · countdown · dataTable · datePicker · descriptions · diffView · divider · drawer · empty · floatButton · form · grid · heatmap · highlight · image · inputNumber · inputTag · jsonViewer · kbd · layout · link · list · loading · menu · modal · mentions · multiSelect · notification · pagination · pinInput · popover · popconfirm · progress · progressRing · radioGroup · rate · relativeTime · result · segmented · select · signaturePad · skeleton · slider · space · sparkline · spinner · splitter · statistic · stepper · switchButton · tabs · tag · textField · textarea · timeline · timePicker · toast · tooltip · tour · transfer · tree · treeSelect · typography · upload · virtualList · watermark -分组:表单(button / copyButton / signaturePad / textField / inputNumber / inputTag / textarea / checkbox / checkboxGroup / colorPicker / switchButton / select / mentions / multiSelect / radioGroup / slider / rate / autoComplete / datePicker / timePicker / pinInput / segmented / upload / form + formItem / transfer / cascader)、反馈(alert / loading / toast / notification / result / spinner / progress / skeleton / badge / empty / statistic / countdown)、浮层(tooltip / popover / popconfirm / modal / drawer / menu / commandPalette)、导航(tabs / breadcrumb / pagination / accordion / stepper / tree / treeSelect / anchor)、数据展示(dataTable / jsonViewer / diffView / codeBlock / relativeTime / sparkline / heatmap / descriptions / card / list / kbd / highlight / timeline / tag / avatar / avatarGroup / image / carousel / calendar / typography / virtualList)、其他(floatButton / grid / layout / link / space / divider / splitter / backTop / affix / tour / watermark)。 +分组:表单(button / copyButton / signaturePad / textField / inputNumber / inputTag / textarea / checkbox / checkboxGroup / colorPicker / switchButton / select / mentions / multiSelect / radioGroup / slider / rate / autoComplete / datePicker / timePicker / pinInput / segmented / upload / form + formItem / transfer / cascader)、反馈(alert / loading / toast / notification / result / spinner / progress / progressRing / skeleton / badge / empty / statistic / countdown)、浮层(tooltip / popover / popconfirm / modal / drawer / menu / commandPalette)、导航(tabs / breadcrumb / pagination / accordion / stepper / tree / treeSelect / anchor)、数据展示(dataTable / jsonViewer / diffView / codeBlock / relativeTime / sparkline / heatmap / descriptions / card / list / kbd / highlight / timeline / tag / avatar / avatarGroup / image / carousel / calendar / typography / virtualList)、其他(floatButton / grid / layout / link / space / divider / splitter / backTop / affix / tour / watermark)。 ## 可访问性约定 @@ -34,7 +34,7 @@ accordion · affix · alert · anchor · autoComplete · avatar · avatarGroup |---|---| | `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:85 个 DOM 组件 + ui.css(只消费 `--ua-*` 令牌) | +| `packages/skin/ui` | @ui-arch/ui:86 个 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 2208e2d..9329cde 100644 --- a/apps/demo/main.ts +++ b/apps/demo/main.ts @@ -69,6 +69,7 @@ import { popconfirm, popover, progress, + progressRing, radioGroup, relativeTime, rate, @@ -1861,6 +1862,28 @@ const content = h( ), ); +// 环形进度:value 传信号,按钮改信号 → 弧长与中心文字一起重绘 +content.append( + section( + 'sec-ring', + '环形进度', + 'progressRing:SVG 双圆靠 stroke-dasharray 画弧(0 从 12 点方向顺时针长),中心文字等宽;value 可传信号,弧长随之重绘。', + (() => { + const pct = signal(64); + const bump = (delta: number): void => pct(Math.min(100, Math.max(0, pct() + delta))); // 夹在 0-100,越界按键不产生负进度 + return h( + 'div', + { class: 'row' }, + progressRing({ value: pct }), + progressRing({ value: pct, size: 56, thickness: 6, showText: false }), // 无文案小环:适合塞进表格单元格 + button({ label: '+10%', variant: 'ghost', onClick: () => bump(10) }), + button({ label: '-10%', variant: 'ghost', onClick: () => bump(-10) }), + button({ label: '清零', variant: 'ghost', onClick: () => pct(0) }), + ); + })(), + ), +); + // 悬浮按钮:fixed 定位,挂 body 不参与页面布局 document.body.append( floatButton({ icon: '+', label: '新建工单', tooltip: '新建工单', position: 'left-bottom', onClick: () => toast.success('已新建工单') }), diff --git a/llms-full.txt b/llms-full.txt index 5c34ab8..4295cf6 100644 --- a/llms-full.txt +++ b/llms-full.txt @@ -1,6 +1,6 @@ # @ui-arch 组件库 Agent 文档(无图形版) -> 自动生成于 2026-10-26T15:08:10.710Z,数据源:各包 ua.manifest.json,勿手改。人类版文档站:apps/docs/index.html。 +> 自动生成于 2026-10-26T15:11:40.469Z,数据源:各包 ua.manifest.json,勿手改。人类版文档站:apps/docs/index.html。 > 组件 = 普通函数,传 props 返回 DOM;状态用 signal 双向绑定:读 signal() 写 signal(v)。 ## 快速上手 @@ -2427,6 +2427,32 @@ heatmap({ values: activity, weeks: 12, formatValue: (n, d) => d + ' 提交 ' + n 现场演示:apps/docs/demo.html#sec-heat +### progressRing — 环形进度 ProgressRing + +参数: + +- value:number | Signal —— 当前进度 0-100;超出范围自动夹取,信号变化时自动重绘 +- size:number —— 外径(px);缺省 96 +- thickness:number —— 环宽(px);缺省 8 +- showText:boolean —— 是否显示中心文字;缺省 true(关掉时请自行保证 aria 可读) +- label:string —— 中心文字;缺省「N%」 + +示例: + +``` +progressRing({ value: pct, size: 96 }) +``` + +手把手: + +1. value 给数字就是静态环,给信号就是活环:不用自己算弧长,组件按 0-100 换算 dasharray +2. 0 点固定在 12 点方向顺时针走,和进度条的心理模型一致,不需要额外传旋转参数 +3. 要放进表格或卡片角落就用 size: 56 + showText: false 的小环,文字由附近的主体数字承担 +4. 换色在父级设 color:进度弧读 currentColor,底环固定描边色(--ua-color-border),不引入第二个色源 +5. 这是个「环形进度条」,不是仪表盘:只表达 0-100 单一进度,多段占比请用饼图类组件 + +现场演示:apps/docs/demo.html#sec-ring + API 速览: - button(props):按钮,variant 三档视觉强度,disabled / loading 支持布尔或信号动态同步(loading 转圈并禁点),onClick 点击回调 @@ -2516,6 +2542,7 @@ API 速览: - relativeTime(props):把时间戳渲染成「3 分钟前」并自动刷新,超阈值改显示绝对日期,title 恒给精确值 - sparkline(props):迷你趋势线,把数值序列压成折线(可填充 + 末点标记),无坐标轴无交互 - heatmap(props):日历热力图,按周列 × 星期行铺格子、四档浓度表示当天数量,悬停看明细 +- progressRing(props):环形进度,SVG 双圆靠 dasharray 画弧 + 中心百分比,value 可传信号自动重绘 一行用法: diff --git a/llms.txt b/llms.txt index 64138ab..46a95e4 100644 --- a/llms.txt +++ b/llms.txt @@ -134,6 +134,7 @@ - relativeTime(props):把时间戳渲染成「3 分钟前」并自动刷新,超阈值改显示绝对日期,title 恒给精确值 - sparkline(props):迷你趋势线,把数值序列压成折线(可填充 + 末点标记),无坐标轴无交互 - heatmap(props):日历热力图,按周列 × 星期行铺格子、四档浓度表示当天数量,悬停看明细 + - progressRing(props):环形进度,SVG 双圆靠 dasharray 画弧 + 中心百分比,value 可传信号自动重绘 - 用法: `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 b20e80c..a41f363 100644 --- a/packages/skin/ui/src/index.ts +++ b/packages/skin/ui/src/index.ts @@ -85,3 +85,4 @@ export { loading } from './loading.js'; export { relativeTime } from './relativeTime.js'; export { sparkline } from './sparkline.js'; export { heatmap } from './heatmap.js'; +export { progressRing } from './progressRing.js'; diff --git a/packages/skin/ui/src/progressRing.ts b/packages/skin/ui/src/progressRing.ts new file mode 100644 index 0000000..608ed17 --- /dev/null +++ b/packages/skin/ui/src/progressRing.ts @@ -0,0 +1,56 @@ +import { effect, h } from '@ui-arch/frame'; +import type { ProgressRingProps } from './types.js'; + +const NS = 'http://www.w3.org/2000/svg'; +// 缺省尺寸与环宽:96px 装得下百分比数字,8px 环宽在小尺寸下也不显笨 +const SIZE = 96; +const THICK = 8; + +/*** 建 SVG 元素并写属性:svg 必须 createElementNS,属性只能 setAttribute ***/ +function svgEl(tag: string, attrs: Record): SVGElement { + const el = document.createElementNS(NS, tag); + for (const [k, v] of Object.entries(attrs)) el.setAttribute(k, String(v)); + return el; +} + +/*** 环形进度:SVG 双圆(底环 + 进度弧)靠 dasharray 画弧,value 变化时弧长与文字同步 ***/ +export function progressRing(props: ProgressRingProps): HTMLElement { + const size = props.size ?? SIZE; + const thick = props.thickness ?? THICK; + const mid = size / 2; + const r = (size - thick) / 2; // 半径要扣掉环宽,否则弧会超出画布 + const c = 2 * Math.PI * r; // 周长:dasharray 的分母 + const read = (): number => (typeof props.value === 'function' ? props.value() : props.value); // 数值 / 信号同一口径 + const svg = svgEl('svg', { viewBox: `0 0 ${size} ${size}`, width: size, height: size, 'aria-hidden': 'true' }); + const track = svgEl('circle', { cx: mid, cy: mid, r, fill: 'none', 'stroke-width': thick, class: 'ua-ring-track' }); + // 弧从 12 点方向顺时针长:整圆旋转 -90°,否则默认从 3 点位置起步 + const arc = svgEl('circle', { + cx: mid, cy: mid, r, fill: 'none', 'stroke-width': thick, + class: 'ua-ring-arc', 'stroke-dasharray': `${0} ${c}`, 'stroke-linecap': 'round', + transform: `rotate(-90 ${mid} ${mid})`, + }); + const text = h('span', { class: 'ua-ring-text' }); + const wrap = h( + 'div', + { + class: 'ua-ring', + role: 'progressbar', + 'aria-valuemin': '0', + 'aria-valuemax': '100', + style: `width:${size}px;height:${size}px`, + }, + svg, + text, + ); + svg.append(track, arc); // 底环先画,进度弧盖在上面 + + /*** 重绘:把百分比夹到 0-100,弧长按比例裁,文字与 aria-valuenow 同步 ***/ + const paint = (): void => { + const pct = Math.min(100, Math.max(0, Math.round(read()))); + arc.setAttribute('stroke-dasharray', `${(c * pct) / 100} ${c}`); // 只画这一段,剩下留空 + if (props.showText !== false) text.textContent = props.label ?? `${pct}%`; + wrap.setAttribute('aria-valuenow', String(pct)); + }; + effect(paint); + return wrap; +} diff --git a/packages/skin/ui/src/types.ts b/packages/skin/ui/src/types.ts index 859d7cc..f546e0e 100644 --- a/packages/skin/ui/src/types.ts +++ b/packages/skin/ui/src/types.ts @@ -1110,3 +1110,12 @@ export interface HeatmapProps { ariaLabel?: string; // 可访问名;缺省「最近 N 周活跃度」 formatValue?: (count: number, date: string) => string; // 悬停提示文案;缺省「日期 · 数量」 } + +/*** 环形进度入参 ***/ +export interface ProgressRingProps { + value: number | Signal; // 当前进度 0-100;超出范围自动夹取,信号变化时自动重绘 + size?: number; // 外径(px);缺省 96 + thickness?: number; // 环宽(px);缺省 8 + showText?: boolean; // 是否显示中心文字;缺省 true(关掉时请自行保证 aria 可读) + label?: string; // 中心文字;缺省「N%」 +} diff --git a/packages/skin/ui/ua.manifest.json b/packages/skin/ui/ua.manifest.json index 65374a9..6ed8889 100644 --- a/packages/skin/ui/ua.manifest.json +++ b/packages/skin/ui/ua.manifest.json @@ -99,7 +99,8 @@ "loading(props, ...children):区域加载遮罩,内容原地降透明度 + 转圈文案,带延迟出场避免快请求闪烁", "relativeTime(props):把时间戳渲染成「3 分钟前」并自动刷新,超阈值改显示绝对日期,title 恒给精确值", "sparkline(props):迷你趋势线,把数值序列压成折线(可填充 + 末点标记),无坐标轴无交互", - "heatmap(props):日历热力图,按周列 × 星期行铺格子、四档浓度表示当天数量,悬停看明细" + "heatmap(props):日历热力图,按周列 × 星期行铺格子、四档浓度表示当天数量,悬停看明细", + "progressRing(props):环形进度,SVG 双圆靠 dasharray 画弧 + 中心百分比,value 可传信号自动重绘" ], "keyTypes": [ "ButtonProps", @@ -4410,7 +4411,47 @@ "格子颜色是相对深浅:按序列最大值归一成四档——所以它说的是「哪天更密」,不是绝对值高低", "默认 12 周、11px 格子;嵌进窄卡片就调小 weeks,看不出格子边界就调大 cellSize" ] + }, + { + "id": "progressRing", + "title": "环形进度 ProgressRing", + "demo": "sec-ring", + "props": [ + { + "n": "value", + "t": "number | Signal", + "d": "当前进度 0-100;超出范围自动夹取,信号变化时自动重绘" + }, + { + "n": "size", + "t": "number", + "d": "外径(px);缺省 96" + }, + { + "n": "thickness", + "t": "number", + "d": "环宽(px);缺省 8" + }, + { + "n": "showText", + "t": "boolean", + "d": "是否显示中心文字;缺省 true(关掉时请自行保证 aria 可读)" + }, + { + "n": "label", + "t": "string", + "d": "中心文字;缺省「N%」" + } + ], + "example": "progressRing({ value: pct, size: 96 })", + "steps": [ + "value 给数字就是静态环,给信号就是活环:不用自己算弧长,组件按 0-100 换算 dasharray", + "0 点固定在 12 点方向顺时针走,和进度条的心理模型一致,不需要额外传旋转参数", + "要放进表格或卡片角落就用 size: 56 + showText: false 的小环,文字由附近的主体数字承担", + "换色在父级设 color:进度弧读 currentColor,底环固定描边色(--ua-color-border),不引入第二个色源", + "这是个「环形进度条」,不是仪表盘:只表达 0-100 单一进度,多段占比请用饼图类组件" + ] } ] } -} \ No newline at end of file +} diff --git a/packages/skin/ui/ui.css b/packages/skin/ui/ui.css index 9fe1a0e..d1bb334 100644 --- a/packages/skin/ui/ui.css +++ b/packages/skin/ui/ui.css @@ -5872,6 +5872,35 @@ h5.ua-type-title { font-size: var(--ua-font-size-md); } word-break: break-word; } +/* ---- 环形进度:底环走描边色,进度弧跟随 currentColor,中心文字用等宽数字 ---- */ +.ua-ring { + position: relative; + display: inline-flex; + align-items: center; + justify-content: center; + color: var(--ua-color-accent); /* 弧色跟随文字色:父级改 color 即可换色,不引入第二个色源 */ +} + +.ua-ring svg { + display: block; +} + +.ua-ring-track { + stroke: var(--ua-color-border); +} + +.ua-ring-arc { + stroke: currentColor; + transition: stroke-dasharray var(--ua-duration-slower) var(--ua-ease); /* 只动 dasharray:不产生空间位移,减少动效规则下无副作用 */ +} + +.ua-ring-text { + position: absolute; + color: var(--ua-color-text); + font-size: var(--ua-font-size-sm); + font-variant-numeric: tabular-nums; /* 等宽数字:进度跳到 100% 时文字不会左右晃 */ +} + /* ---- 减少动效:旋转 / 缩放一律退化为瞬时 ---- 前两类(无限动画、悬停位移)已有专门兜底,但 rotate / scale 过渡属于第三类空间动效: 折叠箭头旋转、卡片按压缩放都靠它提供手感。减少动效下把过渡属性收窄成「只留颜色类」,