From f6706c16227c39554caf023a1674771a107a7f03 Mon Sep 17 00:00:00 2001 From: HIsheR Date: Mon, 26 Oct 2026 15:28:20 +0800 Subject: [PATCH] =?UTF-8?q?iter(g136):=20=E6=96=B0=E5=A2=9E=E9=93=BE?= =?UTF-8?q?=E6=8E=A5=E7=BB=84=E4=BB=B6=20link?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .ua/backlog.json | 8 ++++ .ua/index.json | 61 ++++++++++++++++++++++++++++++- AGENTS.md | 2 +- ARCHITECTURE.md | 2 +- README.md | 8 ++-- apps/demo/main.ts | 13 +++++++ llms-full.txt | 31 +++++++++++++++- llms.txt | 1 + packages/skin/ui/src/index.ts | 1 + packages/skin/ui/src/link.ts | 25 +++++++++++++ packages/skin/ui/src/types.ts | 11 ++++++ packages/skin/ui/ua.manifest.json | 59 +++++++++++++++++++++++++++++- packages/skin/ui/ui.css | 52 ++++++++++++++++++++++++++ 13 files changed, 264 insertions(+), 10 deletions(-) create mode 100644 packages/skin/ui/src/link.ts diff --git a/.ua/backlog.json b/.ua/backlog.json index e29ec58..e029666 100644 --- a/.ua/backlog.json +++ b/.ua/backlog.json @@ -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" } ] } diff --git a/.ua/index.json b/.ua/index.json index eeadbeb..d2cf815 100644 --- a/.ua/index.json +++ b/.ua/index.json @@ -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 标签" + ] } ] }, diff --git a/AGENTS.md b/AGENTS.md index 834410d..0a444ed 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):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 diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 4e3a43f..4130c89 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -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,不发布) diff --git a/README.md b/README.md index f713728..1333d7e 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 可查漂移) ``` -## 组件(共 65 个) +## 组件(共 66 个) -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 -分组:表单(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 打包成单文件) | diff --git a/apps/demo/main.ts b/apps/demo/main.ts index 550e2d6..b719702 100644 --- a/apps/demo/main.ts +++ b/apps/demo/main.ts @@ -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', '悬浮操作按钮', diff --git a/llms-full.txt b/llms-full.txt index 540b068..ee07b97 100644 --- a/llms-full.txt +++ b/llms-full.txt @@ -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 并给读屏播报 一行用法: diff --git a/llms.txt b/llms.txt index bfb9206..f68c76c 100644 --- a/llms.txt +++ b/llms.txt @@ -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 为回调 diff --git a/packages/skin/ui/src/index.ts b/packages/skin/ui/src/index.ts index 9fb60d6..09ef1bb 100644 --- a/packages/skin/ui/src/index.ts +++ b/packages/skin/ui/src/index.ts @@ -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'; diff --git a/packages/skin/ui/src/link.ts b/packages/skin/ui/src/link.ts new file mode 100644 index 0000000..1fce5fb --- /dev/null +++ b/packages/skin/ui/src/link.ts @@ -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; +} diff --git a/packages/skin/ui/src/types.ts b/packages/skin/ui/src/types.ts index 5ad3a59..97d233c 100644 --- a/packages/skin/ui/src/types.ts +++ b/packages/skin/ui/src/types.ts @@ -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 }; // 再导出常用内核类型,方便调用方 diff --git a/packages/skin/ui/ua.manifest.json b/packages/skin/ui/ua.manifest.json index 362791f..8e29ff7 100644 --- a/packages/skin/ui/ua.manifest.json +++ b/packages/skin/ui/ua.manifest.json @@ -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 标签" + ] } ] } diff --git a/packages/skin/ui/ui.css b/packages/skin/ui/ui.css index c6155f5..fed7d20 100644 --- a/packages/skin/ui/ui.css +++ b/packages/skin/ui/ui.css @@ -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; + } +}