iter(g136): 新增链接组件 link

This commit is contained in:
HIsheR
2026-10-26 15:28:20 +08:00
parent b1bc8c6ed0
commit f6706c1622
13 changed files with 264 additions and 10 deletions
+8
View File
@@ -1444,6 +1444,14 @@
"acceptance": "form 新增 columns(1/2/3),>1 时字段走 grid 铺列、窄屏自动单列、按钮区始终整行;单向依赖(form 复用 grid);manifest props/steps 与 demo 同步;npm run check 全绿",
"notes": "自补:既有组件可用性细节",
"status": "done"
},
{
"id": "g136",
"title": "新增链接组件 link",
"motivation": "页面里的 a 标签各写各的样式:站外链接普遍漏 rel=noopener、没有 ↗ 提示,读屏也不知道开了新窗口;禁用态要么当纯文本要么还能点",
"acceptance": "link 支持 href/label/target/external/muted/disabled/onClick,站外自动补 ↗ 与 rel 并播报新窗口,禁用去 href 保留 aria-disabled;样式只消费 --ua-*;文档与演示同步;npm run check 全绿",
"notes": "自补:组件补充方向",
"status": "open"
}
]
}
+59 -2
View File
@@ -1,6 +1,6 @@
{
"$schema": "ui-arch-registry-index/0.1",
"generatedAt": "2026-10-26T07:26:53.819Z",
"generatedAt": "2026-10-26T07:28:06.168Z",
"repoRoot": ".",
"packages": [
{
@@ -208,7 +208,8 @@
"grid(props, ...children):栅格容器,columns 固定列数或 min 走 auto-fit 自适应列宽,gutter 统一列间距,窄屏自动降为单列",
"floatButton(props):悬浮操作按钮,固定停靠左下 / 右下角,label 缺省为纯图标圆形,badge 走角标,tooltip 兼作 aria-label",
"mentions(props):@提及输入,textarea 内键入 @ 弹候选(名字 / 关键词过滤),↑↓ 环形移动、Enter/Tab 选中、Esc 只收本层,选中按光标替换并写回 value",
"treeSelect(props):树形选择,按钮触发弹层并复用 tree(搜索 / 多选 / 级联),单选选中即收起、多选保持展开,Esc 与点外收起"
"treeSelect(props):树形选择,按钮触发弹层并复用 tree(搜索 / 多选 / 级联),单选选中即收起、多选保持展开,Esc 与点外收起",
"link(props):链接,站内跳转与站外跳转统一入口,external 自动补 ↗ 与 rel=noopener noreferrer 并给读屏播报"
],
"keyTypes": [
"ButtonProps",
@@ -3389,6 +3390,62 @@
"选中值只存 key(单选 value / 多选 checked 数组),显示名由 nodes 反查——改 label 不需要迁移数据",
"禁用单个节点用 nodes 里的 disabled(该节点不可选,子节点仍可选);禁用整个控件用 disabled"
]
},
{
"id": "link",
"title": "链接 Link",
"demo": "sec-link",
"props": [
{
"n": "href",
"t": "string",
"r": 1,
"d": "目标地址:站内相对路径或绝对 URL"
},
{
"n": "label",
"t": "string",
"r": 1,
"d": "链接文案"
},
{
"n": "target",
"t": "'_self' | '_blank' | '_parent' | '_top'",
"d": "打开方式;缺省同窗口。传 _blank 会自动按站外处理(补 rel 与图标)"
},
{
"n": "external",
"t": "boolean",
"df": "false",
"d": "站外链接:补 ↗ 图标、强制新窗口、加 rel=noopener noreferrer,并给读屏播报「在新窗口打开」"
},
{
"n": "muted",
"t": "boolean",
"df": "false",
"d": "弱化色,用于次要链接与页脚"
},
{
"n": "disabled",
"t": "boolean",
"df": "false",
"d": "禁用:去掉 href、拦点击,保留可聚焦与 aria-disabled(仍能被 Tab 到,读屏能听出不可用)"
},
{
"n": "onClick",
"t": "(e: Event) => void",
"k": 1,
"d": "点击回调(禁用时不触发;可用于埋点,returnValue 不影响默认跳转)"
}
],
"example": "link({ href: '/docs', label: '查看文档' }); link({ href: 'https://example.com', label: '外部站点', external: true }); link({ href: '#', label: '说明页', muted: true })",
"steps": [
"站内跳转用相对路径、站外用 external: true:会自动补 ↗、target=_blank、rel=noopener noreferrer,并给读屏播报开了新窗口",
"页脚 / 次要说明用 muted 弱化;正文里的行内链接保持默认主色,与 typography 的 underline 区分用途(后者不跳转)",
"需要「点了先确认再跳」这类拦截,在 onClick 里做判断后自行处理,组件不接管默认行为(除禁用外都放行)",
"禁用态只是去掉 href 并保持 aria-disabled,不做视觉上的「像普通文字」处理——需要完全不可聚焦请在外层控制渲染",
"纯样式的「看起来像链接」用 typography({ underline: true }),不要为了样式引入带 href 的 a 标签"
]
}
]
},
+1 -1
View File
@@ -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):65 个 DOM 组件(表单 / 反馈 / 浮层 / 导航 / 表格),样式只消费令牌
- `packages/skin/ui`(@ui-arch/ui):66 个 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
+1 -1
View File
@@ -34,7 +34,7 @@ ui-arch-mvp/
│ │ ├── table-core/ # 表格状态内核
│ │ └── frame/ # 信号 + hyperscript 微框架
│ ├── skin/ # 皮肤层(DOM 组件,可发布)
│ │ └── ui/ # 65 个组件 + ui.css
│ │ └── ui/ # 66 个组件 + ui.css
│ ├── design/ # 资产层(可发布)
│ │ └── tokens/ # --ua-* 设计令牌
│ └── tools/ # 工程层(private,不发布)
+4 -4
View File
@@ -12,13 +12,13 @@ npm run docs # 生成人类文档站 apps/docs/index.html + Agent 全
npm run changelog # 依据 git 提交史重写 CHANGELOG.md(发版前跑;changelog:check 可查漂移)
```
## 组件(共 65 个)
## 组件(共 66 个)
<!-- ua:components -->
accordion · affix · alert · anchor · autoComplete · avatar · backTop · badge · breadcrumb · button · calendar · carousel · card · cascader · checkbox · checkboxGroup · colorPicker · dataTable · datePicker · descriptions · divider · drawer · empty · floatButton · form · grid · image · inputNumber · list · menu · modal · mentions · multiSelect · notification · pagination · popover · popconfirm · progress · radioGroup · rate · result · segmented · select · skeleton · slider · space · spinner · splitter · statistic · stepper · switchButton · tabs · tag · textField · textarea · timeline · toast · tooltip · tour · transfer · tree · treeSelect · typography · upload · watermark
accordion · affix · alert · anchor · autoComplete · avatar · backTop · badge · breadcrumb · button · calendar · carousel · card · cascader · checkbox · checkboxGroup · colorPicker · dataTable · datePicker · descriptions · divider · drawer · empty · floatButton · form · grid · image · inputNumber · link · list · menu · modal · mentions · multiSelect · notification · pagination · popover · popconfirm · progress · radioGroup · rate · result · segmented · select · skeleton · slider · space · spinner · splitter · statistic · stepper · switchButton · tabs · tag · textField · textarea · timeline · toast · tooltip · tour · transfer · tree · treeSelect · typography · upload · watermark
<!-- /ua:components -->
分组:表单(button / textField / inputNumber / textarea / checkbox / checkboxGroup / colorPicker / switchButton / select / mentions / multiSelect / radioGroup / slider / rate / autoComplete / datePicker / segmented / upload / form + formItem / transfer / cascader)、反馈(alert / toast / notification / result / spinner / progress / skeleton / badge / empty / statistic)、浮层(tooltip / popover / popconfirm / modal / drawer / menu)、导航(tabs / breadcrumb / pagination / accordion / stepper / tree / treeSelect / anchor)、数据展示(dataTable / descriptions / card / list / timeline / tag / avatar / image / carousel / calendar / typography)、其他(floatButton / grid / space / divider / splitter / backTop / affix / tour / watermark)。
分组:表单(button / textField / inputNumber / textarea / checkbox / checkboxGroup / colorPicker / switchButton / select / mentions / multiSelect / radioGroup / slider / rate / autoComplete / datePicker / segmented / upload / form + formItem / transfer / cascader)、反馈(alert / toast / notification / result / spinner / progress / skeleton / badge / empty / statistic)、浮层(tooltip / popover / popconfirm / modal / drawer / menu)、导航(tabs / breadcrumb / pagination / accordion / stepper / tree / treeSelect / anchor)、数据展示(dataTable / descriptions / card / list / timeline / tag / avatar / image / carousel / calendar / typography)、其他(floatButton / grid / link / space / divider / splitter / backTop / affix / tour / watermark)。
## 可访问性约定
@@ -32,7 +32,7 @@ accordion · affix · alert · anchor · autoComplete · avatar · backTop · ba
|---|---|
| `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:65 个 DOM 组件 + ui.css(只消费 `--ua-*` 令牌) |
| `packages/skin/ui` | @ui-arch/ui:66 个 DOM 组件 + ui.css(只消费 `--ua-*` 令牌) |
| `packages/design/tokens` | @ui-arch/tokens:设计令牌(暗色只换值不换名) |
| `packages/tools/cli` | @ui-arch/cli:`ua` 命令行与 stdio MCP server |
| `apps/demo` | 交互演示(esbuild 打包成单文件) |
+13
View File
@@ -37,6 +37,7 @@ import {
formItem,
inputNumber,
grid,
link,
list,
carousel,
treeSelect,
@@ -1363,6 +1364,18 @@ const content = h(
),
),
),
section(
'sec-link',
'链接',
'link 统一站内 / 站外跳转:external 自动补 ↗、target=_blank 与 rel=noopener noreferrer,并给读屏播报「在新窗口打开」;muted 用于页脚次要链接,disabled 去掉 href 但保留可聚焦与 aria-disabled。',
space({ wrap: true, size: 'md' },
link({ href: '#sec-link', label: '站内锚点' }),
link({ href: 'https://gitea.yinyuanjingling.icu/temp/UI', label: '仓库地址', external: true }),
link({ href: '#sec-link', label: '次要说明', muted: true }),
link({ href: '#sec-link', label: '已停用', disabled: true }),
link({ href: '#sec-link', label: '带埋点的链接', onClick: () => toast.info('点击已上报') }),
),
),
section(
'sec-floatbutton',
'悬浮操作按钮',
+30 -1
View File
@@ -1,6 +1,6 @@
# @ui-arch 组件库 Agent 文档(无图形版)
> 自动生成于 2026-10-26T07:26:53.819Z,数据源:各包 ua.manifest.json,勿手改。人类版文档站:apps/docs/index.html。
> 自动生成于 2026-10-26T07:28:06.168Z,数据源:各包 ua.manifest.json,勿手改。人类版文档站:apps/docs/index.html。
> 组件 = 普通函数,传 props 返回 DOM;状态用 signal 双向绑定:读 signal() 写 signal(v)。
## 快速上手
@@ -1819,6 +1819,34 @@ const dept = signal(''); treeSelect({ nodes: [{ key: 'tech', label: '技术中
现场演示:apps/docs/demo.html#sec-treeselect
### link — 链接 Link
参数:
- href(必填):string —— 目标地址:站内相对路径或绝对 URL
- label(必填):string —— 链接文案
- target:'_self' | '_blank' | '_parent' | '_top' —— 打开方式;缺省同窗口。传 _blank 会自动按站外处理(补 rel 与图标)
- external(默认 false):boolean —— 站外链接:补 ↗ 图标、强制新窗口、加 rel=noopener noreferrer,并给读屏播报「在新窗口打开」
- muted(默认 false):boolean —— 弱化色,用于次要链接与页脚
- disabled(默认 false):boolean —— 禁用:去掉 href、拦点击,保留可聚焦与 aria-disabled(仍能被 Tab 到,读屏能听出不可用)
- onClick(回调):(e: Event) => void —— 点击回调(禁用时不触发;可用于埋点,returnValue 不影响默认跳转)
示例:
```
link({ href: '/docs', label: '查看文档' }); link({ href: 'https://example.com', label: '外部站点', external: true }); link({ href: '#', label: '说明页', muted: true })
```
手把手:
1. 站内跳转用相对路径、站外用 external: true:会自动补 ↗、target=_blank、rel=noopener noreferrer,并给读屏播报开了新窗口
2. 页脚 / 次要说明用 muted 弱化;正文里的行内链接保持默认主色,与 typography 的 underline 区分用途(后者不跳转)
3. 需要「点了先确认再跳」这类拦截,在 onClick 里做判断后自行处理,组件不接管默认行为(除禁用外都放行)
4. 禁用态只是去掉 href 并保持 aria-disabled,不做视觉上的「像普通文字」处理——需要完全不可聚焦请在外层控制渲染
5. 纯样式的「看起来像链接」用 typography({ underline: true }),不要为了样式引入带 href 的 a 标签
现场演示:apps/docs/demo.html#sec-link
API 速览:
- button(props):按钮,variant = primary | ghost | danger,disabled 支持布尔或信号禁用;type 缺省 button(表单内不隐式提交)
@@ -1888,6 +1916,7 @@ API 速览:
- floatButton(props):悬浮操作按钮,固定停靠左下 / 右下角,label 缺省为纯图标圆形,badge 走角标,tooltip 兼作 aria-label
- mentions(props):@提及输入,textarea 内键入 @ 弹候选(名字 / 关键词过滤),↑↓ 环形移动、Enter/Tab 选中、Esc 只收本层,选中按光标替换并写回 value
- treeSelect(props):树形选择,按钮触发弹层并复用 tree(搜索 / 多选 / 级联),单选选中即收起、多选保持展开,Esc 与点外收起
- link(props):链接,站内跳转与站外跳转统一入口,external 自动补 ↗ 与 rel=noopener noreferrer 并给读屏播报
一行用法:
+1
View File
@@ -114,6 +114,7 @@
- floatButton(props):悬浮操作按钮,固定停靠左下 / 右下角,label 缺省为纯图标圆形,badge 走角标,tooltip 兼作 aria-label
- mentions(props):@提及输入,textarea 内键入 @ 弹候选(名字 / 关键词过滤),↑↓ 环形移动、Enter/Tab 选中、Esc 只收本层,选中按光标替换并写回 value
- treeSelect(props):树形选择,按钮触发弹层并复用 tree(搜索 / 多选 / 级联),单选选中即收起、多选保持展开,Esc 与点外收起
- link(props):链接,站内跳转与站外跳转统一入口,external 自动补 ↗ 与 rel=noopener noreferrer 并给读屏播报
- 用法: `const checked = signal(false); checkbox({ label: '同意', checked }); const open = signal(false); modal({ title: '确认', open }, '内容')`
- 约定: 皮肤不持有业务状态,一律委托 headless 内核或调用方信号
- 约定: 表单类组件统一约定:*Props.value/checked/active 为 Signal 双向绑定,onChange/onInput 为回调
+1
View File
@@ -65,3 +65,4 @@ export { grid } from './grid.js';
export { floatButton } from './floatButton.js';
export { mentions } from './mentions.js';
export { treeSelect } from './treeSelect.js';
export { link } from './link.js';
+25
View File
@@ -0,0 +1,25 @@
import { h } from '@ui-arch/frame';
import type { LinkProps } from './types.js';
/*** 链接:站内跳转与站外跳转统一入口;external 自动补 ↗ 与 rel,disabled 去掉 href 并拦点击 ***/
export function link(props: LinkProps): HTMLElement {
const external = Boolean(props.external) || props.target === '_blank'; // 新窗口一律按站外处理
const cls = ['ua-link', props.muted ? 'is-muted' : '', props.disabled ? 'is-disabled' : '', external ? 'is-external' : '']
.filter(Boolean).join(' ');
const a = h('a', {
class: cls,
href: props.disabled ? null : props.href, // 禁用态去掉 href:键盘也不再当链接用
target: external ? '_blank' : props.target ?? null,
rel: external ? 'noopener noreferrer' : null, // 新窗口必须断开 opener,防目标页反向操控
'aria-disabled': props.disabled ? 'true' : null,
onclick: (e: Event) => {
if (props.disabled) {
e.preventDefault(); // 保留可聚焦但不可跳转
return;
}
props.onClick?.(e);
},
}, props.label, external ? h('span', { class: 'ua-link-icon', 'aria-hidden': 'true' }, '↗') : null);
if (external) a.append(h('span', { class: 'ua-visually-hidden' }, '(在新窗口打开)')); // 读屏需知道开了新窗
return a;
}
+11
View File
@@ -809,4 +809,15 @@ export interface TreeSelectProps {
onChange?: (keys: string[]) => void; // 多选勾选变更回调(传副本,父子已级联)
}
/*** 链接入参 ***/
export interface LinkProps {
href: string; // 目标地址;站内相对路径或绝对 URL
label: string; // 链接文案
target?: '_self' | '_blank' | '_parent' | '_top'; // 打开方式;缺省同窗口
external?: boolean; // 站外链接:补 ↗ 图标、强制新窗口并加 rel=noopener noreferrer
muted?: boolean; // 弱化色(次要链接 / 页脚);缺省主色
disabled?: boolean; // 禁用:去掉 href、拦点击,保留可聚焦与 aria-disabled
onClick?: (e: Event) => void; // 点击回调(禁用时不触发)
}
export type { ColumnDef, RowKey }; // 再导出常用内核类型,方便调用方
+58 -1
View File
@@ -79,7 +79,8 @@
"grid(props, ...children):栅格容器,columns 固定列数或 min 走 auto-fit 自适应列宽,gutter 统一列间距,窄屏自动降为单列",
"floatButton(props):悬浮操作按钮,固定停靠左下 / 右下角,label 缺省为纯图标圆形,badge 走角标,tooltip 兼作 aria-label",
"mentions(props):@提及输入,textarea 内键入 @ 弹候选(名字 / 关键词过滤),↑↓ 环形移动、Enter/Tab 选中、Esc 只收本层,选中按光标替换并写回 value",
"treeSelect(props):树形选择,按钮触发弹层并复用 tree(搜索 / 多选 / 级联),单选选中即收起、多选保持展开,Esc 与点外收起"
"treeSelect(props):树形选择,按钮触发弹层并复用 tree(搜索 / 多选 / 级联),单选选中即收起、多选保持展开,Esc 与点外收起",
"link(props):链接,站内跳转与站外跳转统一入口,external 自动补 ↗ 与 rel=noopener noreferrer 并给读屏播报"
],
"keyTypes": [
"ButtonProps",
@@ -3260,6 +3261,62 @@
"选中值只存 key(单选 value / 多选 checked 数组),显示名由 nodes 反查——改 label 不需要迁移数据",
"禁用单个节点用 nodes 里的 disabled(该节点不可选,子节点仍可选);禁用整个控件用 disabled"
]
},
{
"id": "link",
"title": "链接 Link",
"demo": "sec-link",
"props": [
{
"n": "href",
"t": "string",
"r": 1,
"d": "目标地址:站内相对路径或绝对 URL"
},
{
"n": "label",
"t": "string",
"r": 1,
"d": "链接文案"
},
{
"n": "target",
"t": "'_self' | '_blank' | '_parent' | '_top'",
"d": "打开方式;缺省同窗口。传 _blank 会自动按站外处理(补 rel 与图标)"
},
{
"n": "external",
"t": "boolean",
"df": "false",
"d": "站外链接:补 ↗ 图标、强制新窗口、加 rel=noopener noreferrer,并给读屏播报「在新窗口打开」"
},
{
"n": "muted",
"t": "boolean",
"df": "false",
"d": "弱化色,用于次要链接与页脚"
},
{
"n": "disabled",
"t": "boolean",
"df": "false",
"d": "禁用:去掉 href、拦点击,保留可聚焦与 aria-disabled(仍能被 Tab 到,读屏能听出不可用)"
},
{
"n": "onClick",
"t": "(e: Event) => void",
"k": 1,
"d": "点击回调(禁用时不触发;可用于埋点,returnValue 不影响默认跳转)"
}
],
"example": "link({ href: '/docs', label: '查看文档' }); link({ href: 'https://example.com', label: '外部站点', external: true }); link({ href: '#', label: '说明页', muted: true })",
"steps": [
"站内跳转用相对路径、站外用 external: true:会自动补 ↗、target=_blank、rel=noopener noreferrer,并给读屏播报开了新窗口",
"页脚 / 次要说明用 muted 弱化;正文里的行内链接保持默认主色,与 typography 的 underline 区分用途(后者不跳转)",
"需要「点了先确认再跳」这类拦截,在 onClick 里做判断后自行处理,组件不接管默认行为(除禁用外都放行)",
"禁用态只是去掉 href 并保持 aria-disabled,不做视觉上的「像普通文字」处理——需要完全不可聚焦请在外层控制渲染",
"纯样式的「看起来像链接」用 typography({ underline: true }),不要为了样式引入带 href 的 a 标签"
]
}
]
}
+52
View File
@@ -4308,3 +4308,55 @@ h5.ua-type-title { font-size: var(--ua-font-size-md); }
transition: none;
}
}
/* —— 链接:颜色走令牌,悬停下划线渐进显现 —— */
.ua-link {
display: inline-flex;
align-items: baseline;
gap: 2px;
color: var(--ua-color-accent);
text-decoration: none;
text-underline-offset: 2px;
text-decoration-thickness: 1px;
transition: color 0.15s ease, text-decoration-color 0.15s ease;
cursor: pointer;
}
.ua-link:hover {
text-decoration: underline;
}
.ua-link:focus-visible {
outline: 2px solid var(--ua-color-accent);
outline-offset: 2px;
border-radius: var(--ua-radius-sm);
}
.ua-link.is-muted {
color: var(--ua-color-text-muted);
}
.ua-link.is-muted:hover {
color: var(--ua-color-text);
}
.ua-link.is-disabled {
color: var(--ua-color-text-muted);
cursor: not-allowed;
text-decoration: none;
}
.ua-link-icon {
font-size: 0.85em;
line-height: 1;
}
.ua-visually-hidden {
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
padding: 0;
overflow: hidden;
clip: rect(0 0 0 0); /* 只给读屏,视觉不可见 */
white-space: nowrap;
border: 0;
}
@media (prefers-reduced-motion: reduce) {
.ua-link {
transition: none;
}
}