iter(g101): 新增锚点导航组件 anchor

This commit is contained in:
HIsheR
2026-10-26 13:50:12 +08:00
parent befed761d9
commit 265842ef63
13 changed files with 303 additions and 10 deletions
+8
View File
@@ -1164,6 +1164,14 @@
"acceptance": "npm run changelog 重新生成(273 提交),changelog:check 一致;npm run check 全绿", "acceptance": "npm run changelog 重新生成(273 提交),changelog:check 一致;npm run check 全绿",
"notes": "自补:发版前刷新步骤;本条为本次唤起第 22 轮收尾", "notes": "自补:发版前刷新步骤;本条为本次唤起第 22 轮收尾",
"status": "done" "status": "done"
},
{
"id": "g101",
"title": "新增锚点导航组件 anchor",
"motivation": "文档页、设置页、长表单都需要「侧边跳转 + 滚动高亮」,现在库里没有任何可复用件:demo 的侧栏目录 (buildToc) 是手写 IntersectionObserver + 类名切换,偏移量、hash 回写、禁用项、减少动效各写各的,调用方要复制一遍才用得上",
"acceptance": "新增 anchor(props)(packages/skin/ui/src/anchor.ts,< 200 行):items/href/title/disabled、offset 偏移量、container 内滚容器、onChange 回调;滚动按目标位置高亮并同步 aria-current,点击平滑定位 + replaceState 回写 hash,prefers-reduced-motion 下瞬移;types/manifest/index/ui.css/demo/README/AGENTS/ARCHITECTURE 同步,npm run check 全绿",
"notes": "自补:组件补充;样式全走令牌,选出态维持低视觉比重(accent 10% 底色 + 左侧 2px 指示条)",
"status": "open"
} }
] ]
} }
+42 -2
View File
@@ -1,6 +1,6 @@
{ {
"$schema": "ui-arch-registry-index/0.1", "$schema": "ui-arch-registry-index/0.1",
"generatedAt": "2026-10-26T05:16:55.417Z", "generatedAt": "2026-10-26T05:49:43.206Z",
"repoRoot": ".", "repoRoot": ".",
"packages": [ "packages": [
{ {
@@ -200,7 +200,8 @@
"watermark(props, ...children):内容容器 + 平铺水印覆盖层,canvas 生成单元、颜色取自令牌,覆盖层不挡交互", "watermark(props, ...children):内容容器 + 平铺水印覆盖层,canvas 生成单元、颜色取自令牌,覆盖层不挡交互",
"checkboxGroup(props):多选组,value 为数组信号,min/max 用禁用边界项实现(不抛错),checkAll 支持半选且只统计未禁用项", "checkboxGroup(props):多选组,value 为数组信号,min/max 用禁用边界项实现(不抛错),checkAll 支持半选且只统计未禁用项",
"affix(props, ...children):固钉,滚过 offset 后内容转 fixed 贴顶、外层保留占位高度,固定后宽度随容器自适应", "affix(props, ...children):固钉,滚过 offset 后内容转 fixed 贴顶、外层保留占位高度,固定后宽度随容器自适应",
"multiSelect(props):多选下拉,标签式回显 + 可搜索弹层,点选不收起便于连续多选,max 到限后未选项禁用" "multiSelect(props):多选下拉,标签式回显 + 可搜索弹层,点选不收起便于连续多选,max 到限后未选项禁用",
"anchor(props):锚点导航,竖排区块链接,滚动时按目标位置高亮当前区块(aria-current 同步),点击按 offset 偏移量平滑定位并回写 hash;container 可指定内滚容器"
], ],
"keyTypes": [ "keyTypes": [
"ButtonProps", "ButtonProps",
@@ -2830,6 +2831,45 @@
"↑↓ 环形移动高亮、Enter 切换当前项、Esc 收起(与其它弹层共用一个 Esc 栈)", "↑↓ 环形移动高亮、Enter 切换当前项、Esc 收起(与其它弹层共用一个 Esc 栈)",
"value 传数组信号即受控,外部写入会同步标签区" "value 传数组信号即受控,外部写入会同步标签区"
] ]
},
{
"id": "anchor",
"title": "锚点导航 Anchor",
"demo": "sec-anchor",
"props": [
{
"n": "items",
"t": "AnchorItem[]",
"d": "锚点项集合,每项含 href(形如 #sec-x)、title、可选 disabled"
},
{
"n": "offset",
"t": "number",
"d": "定位与高亮判定预留的顶部距离,适合有吸顶栏的页面;缺省 0"
},
{
"n": "container",
"t": "HTMLElement | Window",
"d": "监听的滚动容器;缺省 window(整页滚动)"
},
{
"n": "label",
"t": "string",
"d": "nav 的无障碍名称;缺省「锚点导航」"
},
{
"n": "onChange",
"t": "(href: string) => void",
"d": "跳转后的回调,参数为命中的 href(适合埋点或联动)"
}
],
"example": "anchor({ items: [{ href: '#sec-button', title: '按钮' }, { href: '#sec-card', title: '卡片' }], offset: 72 })",
"steps": [
"在页面任意位置放一个容器,调用 anchor 并把返回的 nav 塞进去(固定侧栏或吸顶栏均可)",
"items 里每条 href 指向页面上真实存在的区块 id,标题即链接文案;目标缺失的项在滚动判定时自动跳过",
"页面有吸顶导航时把它的高度传给 offset,跳转与高亮判定都会预留这段距离",
"滚动容器不是整页时用 container 传入该元素;点击走平滑滚动,系统开启「减少动效」时改为瞬移,hash 用 replaceState 回写不堆历史"
]
} }
] ]
}, },
+1 -1
View File
@@ -21,7 +21,7 @@
- `packages/core/table-core`(@ui-arch/table-core):Headless 表格逻辑内核 - `packages/core/table-core`(@ui-arch/table-core):Headless 表格逻辑内核
- `packages/core/frame`(@ui-arch/frame):signal + hyperscript 微框架 - `packages/core/frame`(@ui-arch/frame):signal + hyperscript 微框架
- `packages/skin/ui`(@ui-arch/ui):57 个 DOM 组件(表单 / 反馈 / 浮层 / 导航 / 表格),样式只消费令牌 - `packages/skin/ui`(@ui-arch/ui):58 个 DOM 组件(表单 / 反馈 / 浮层 / 导航 / 表格),样式只消费令牌
- `packages/design/tokens`(@ui-arch/tokens):设计令牌 - `packages/design/tokens`(@ui-arch/tokens):设计令牌
- `packages/tools/cli`(@ui-arch/cli):`ua` 命令行 + MCP 注册表服务(private) - `packages/tools/cli`(@ui-arch/cli):`ua` 命令行 + MCP 注册表服务(private)
- `apps/demo`(@ui-arch/demo):交互演示,产物 apps/demo/dist/demo.html - `apps/demo`(@ui-arch/demo):交互演示,产物 apps/demo/dist/demo.html
+1 -1
View File
@@ -34,7 +34,7 @@ ui-arch-mvp/
│ │ ├── table-core/ # 表格状态内核 │ │ ├── table-core/ # 表格状态内核
│ │ └── frame/ # 信号 + hyperscript 微框架 │ │ └── frame/ # 信号 + hyperscript 微框架
│ ├── skin/ # 皮肤层(DOM 组件,可发布) │ ├── skin/ # 皮肤层(DOM 组件,可发布)
│ │ └── ui/ # 57 个组件 + ui.css │ │ └── ui/ # 58 个组件 + ui.css
│ ├── design/ # 资产层(可发布) │ ├── design/ # 资产层(可发布)
│ │ └── tokens/ # --ua-* 设计令牌 │ │ └── tokens/ # --ua-* 设计令牌
│ └── tools/ # 工程层(private,不发布) │ └── 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 可查漂移) npm run changelog # 依据 git 提交史重写 CHANGELOG.md(发版前跑;changelog:check 可查漂移)
``` ```
## 组件(共 57 个) ## 组件(共 58 个)
<!-- ua:components --> <!-- ua:components -->
accordion · affix · alert · autoComplete · avatar · backTop · badge · breadcrumb · button · carousel · card · cascader · checkbox · checkboxGroup · colorPicker · dataTable · datePicker · descriptions · divider · drawer · empty · form · image · inputNumber · list · menu · modal · multiSelect · notification · pagination · popover · progress · radioGroup · rate · result · segmented · select · skeleton · slider · space · spinner · splitter · statistic · stepper · switchButton · tabs · tag · textField · textarea · timeline · toast · tooltip · tour · transfer · tree · upload · watermark accordion · affix · alert · anchor · autoComplete · avatar · backTop · badge · breadcrumb · button · carousel · card · cascader · checkbox · checkboxGroup · colorPicker · dataTable · datePicker · descriptions · divider · drawer · empty · form · image · inputNumber · list · menu · modal · multiSelect · notification · pagination · popover · progress · radioGroup · rate · result · segmented · select · skeleton · slider · space · spinner · splitter · statistic · stepper · switchButton · tabs · tag · textField · textarea · timeline · toast · tooltip · tour · transfer · tree · upload · watermark
<!-- /ua:components --> <!-- /ua:components -->
分组:表单(button / textField / inputNumber / textarea / checkbox / checkboxGroup / colorPicker / switchButton / select / multiSelect / radioGroup / slider / rate / autoComplete / datePicker / segmented / upload / form + formItem / transfer / cascader)、反馈(alert / toast / notification / result / spinner / progress / skeleton / badge / empty / statistic)、浮层(tooltip / popover / modal / drawer / menu)、导航(tabs / breadcrumb / pagination / accordion / stepper / tree)、数据展示(dataTable / descriptions / card / list / timeline / tag / avatar / image / carousel)、其他(space / divider / splitter / backTop / affix / tour / watermark)。 分组:表单(button / textField / inputNumber / textarea / checkbox / checkboxGroup / colorPicker / switchButton / select / multiSelect / radioGroup / slider / rate / autoComplete / datePicker / segmented / upload / form + formItem / transfer / cascader)、反馈(alert / toast / notification / result / spinner / progress / skeleton / badge / empty / statistic)、浮层(tooltip / popover / modal / drawer / menu)、导航(tabs / breadcrumb / pagination / accordion / stepper / tree / anchor)、数据展示(dataTable / descriptions / card / list / timeline / tag / avatar / image / carousel)、其他(space / divider / splitter / backTop / affix / tour / watermark)。
## 可访问性约定 ## 可访问性约定
@@ -32,7 +32,7 @@ accordion · affix · alert · autoComplete · avatar · backTop · badge · bre
|---|---| |---|---|
| `packages/core/frame` | @ui-arch/frame:signal / computed / effect + hyperscript(无虚拟 DOM) | | `packages/core/frame` | @ui-arch/frame:signal / computed / effect + hyperscript(无虚拟 DOM) |
| `packages/core/table-core` | @ui-arch/table-core:headless 表格(filter → sort → paginate 派生管线) | | `packages/core/table-core` | @ui-arch/table-core:headless 表格(filter → sort → paginate 派生管线) |
| `packages/skin/ui` | @ui-arch/ui:57 个 DOM 组件 + ui.css(只消费 `--ua-*` 令牌) | | `packages/skin/ui` | @ui-arch/ui:58 个 DOM 组件 + ui.css(只消费 `--ua-*` 令牌) |
| `packages/design/tokens` | @ui-arch/tokens:设计令牌(暗色只换值不换名) | | `packages/design/tokens` | @ui-arch/tokens:设计令牌(暗色只换值不换名) |
| `packages/tools/cli` | @ui-arch/cli:`ua` 命令行与 stdio MCP server | | `packages/tools/cli` | @ui-arch/cli:`ua` 命令行与 stdio MCP server |
| `apps/demo` | 交互演示(esbuild 打包成单文件) | | `apps/demo` | 交互演示(esbuild 打包成单文件) |
+20
View File
@@ -11,6 +11,7 @@ import {
accordion, accordion,
affix, affix,
alert, alert,
anchor,
autoComplete, autoComplete,
avatar, avatar,
backTop, backTop,
@@ -1152,6 +1153,25 @@ const content = h(
h('p', { class: 'desc' }, '标题行右侧放操作或统计,底部 footer 放主次按钮;hoverable 时整卡悬停浮起。'), h('p', { class: 'desc' }, '标题行右侧放操作或统计,底部 footer 放主次按钮;hoverable 时整卡悬停浮起。'),
), ),
), ),
section(
'sec-anchor',
'锚点导航',
'竖排区块链接:滚动时按目标位置高亮当前区块并同步 aria-current,点击按 offset 偏移量平滑定位、hash 用 replaceState 回写(不堆历史);禁用项留在列表里不响应,目标缺失的项在判定时自动跳过。',
card(
{ title: '跳到本页区块' },
anchor({
items: [
{ href: '#sec-anchor', title: '锚点导航(本节)' },
{ href: '#sec-table', title: '数据表格' },
{ href: '#sec-card', title: '卡片容器' },
{ href: '#sec-missing-zone', title: '不存在的区块(自动跳过)' },
{ href: '#sec-affix', title: '吸顶容器(禁用示例)', disabled: true },
],
offset: 12,
onChange: (href) => toast.info(`已跳到 ${href}`),
}),
),
),
); );
// 引导漫游实例:根节点需挂到 body(fixed 定位,不参与布局) // 引导漫游实例:根节点需挂到 body(fixed 定位,不参与布局)
+27 -1
View File
@@ -1,6 +1,6 @@
# @ui-arch 组件库 Agent 文档(无图形版) # @ui-arch 组件库 Agent 文档(无图形版)
> 自动生成于 2026-10-26T05:16:55.417Z,数据源:各包 ua.manifest.json,勿手改。人类版文档站:apps/docs/index.html。 > 自动生成于 2026-10-26T05:49:43.206Z,数据源:各包 ua.manifest.json,勿手改。人类版文档站:apps/docs/index.html。
> 组件 = 普通函数,传 props 返回 DOM;状态用 signal 双向绑定:读 signal() 写 signal(v)。 > 组件 = 普通函数,传 props 返回 DOM;状态用 signal 双向绑定:读 signal() 写 signal(v)。
## 快速上手 ## 快速上手
@@ -1562,6 +1562,31 @@ multiSelect({ value: tagPick, max: 3, placeholder: '选择技术栈', options: [
现场演示:apps/docs/demo.html#sec-multiselect 现场演示:apps/docs/demo.html#sec-multiselect
### anchor — 锚点导航 Anchor
参数:
- items:AnchorItem[] —— 锚点项集合,每项含 href(形如 #sec-x)、title、可选 disabled
- offset:number —— 定位与高亮判定预留的顶部距离,适合有吸顶栏的页面;缺省 0
- container:HTMLElement | Window —— 监听的滚动容器;缺省 window(整页滚动)
- label:string —— nav 的无障碍名称;缺省「锚点导航」
- onChange:(href: string) => void —— 跳转后的回调,参数为命中的 href(适合埋点或联动)
示例:
```
anchor({ items: [{ href: '#sec-button', title: '按钮' }, { href: '#sec-card', title: '卡片' }], offset: 72 })
```
手把手:
1. 在页面任意位置放一个容器,调用 anchor 并把返回的 nav 塞进去(固定侧栏或吸顶栏均可)
2. items 里每条 href 指向页面上真实存在的区块 id,标题即链接文案;目标缺失的项在滚动判定时自动跳过
3. 页面有吸顶导航时把它的高度传给 offset,跳转与高亮判定都会预留这段距离
4. 滚动容器不是整页时用 container 传入该元素;点击走平滑滚动,系统开启「减少动效」时改为瞬移,hash 用 replaceState 回写不堆历史
现场演示:apps/docs/demo.html#sec-anchor
API 速览: API 速览:
- button(props):按钮,variant = primary | ghost | danger,disabled 支持布尔或信号禁用;type 缺省 button(表单内不隐式提交) - button(props):按钮,variant = primary | ghost | danger,disabled 支持布尔或信号禁用;type 缺省 button(表单内不隐式提交)
@@ -1623,6 +1648,7 @@ API 速览:
- checkboxGroup(props):多选组,value 为数组信号,min/max 用禁用边界项实现(不抛错),checkAll 支持半选且只统计未禁用项 - checkboxGroup(props):多选组,value 为数组信号,min/max 用禁用边界项实现(不抛错),checkAll 支持半选且只统计未禁用项
- affix(props, ...children):固钉,滚过 offset 后内容转 fixed 贴顶、外层保留占位高度,固定后宽度随容器自适应 - affix(props, ...children):固钉,滚过 offset 后内容转 fixed 贴顶、外层保留占位高度,固定后宽度随容器自适应
- multiSelect(props):多选下拉,标签式回显 + 可搜索弹层,点选不收起便于连续多选,max 到限后未选项禁用 - multiSelect(props):多选下拉,标签式回显 + 可搜索弹层,点选不收起便于连续多选,max 到限后未选项禁用
- anchor(props):锚点导航,竖排区块链接,滚动时按目标位置高亮当前区块(aria-current 同步),点击按 offset 偏移量平滑定位并回写 hash;container 可指定内滚容器
一行用法: 一行用法:
+1
View File
@@ -106,6 +106,7 @@
- checkboxGroup(props):多选组,value 为数组信号,min/max 用禁用边界项实现(不抛错),checkAll 支持半选且只统计未禁用项 - checkboxGroup(props):多选组,value 为数组信号,min/max 用禁用边界项实现(不抛错),checkAll 支持半选且只统计未禁用项
- affix(props, ...children):固钉,滚过 offset 后内容转 fixed 贴顶、外层保留占位高度,固定后宽度随容器自适应 - affix(props, ...children):固钉,滚过 offset 后内容转 fixed 贴顶、外层保留占位高度,固定后宽度随容器自适应
- multiSelect(props):多选下拉,标签式回显 + 可搜索弹层,点选不收起便于连续多选,max 到限后未选项禁用 - multiSelect(props):多选下拉,标签式回显 + 可搜索弹层,点选不收起便于连续多选,max 到限后未选项禁用
- anchor(props):锚点导航,竖排区块链接,滚动时按目标位置高亮当前区块(aria-current 同步),点击按 offset 偏移量平滑定位并回写 hash;container 可指定内滚容器
- 用法: `const checked = signal(false); checkbox({ label: '同意', checked }); const open = signal(false); modal({ title: '确认', open }, '内容')` - 用法: `const checked = signal(false); checkbox({ label: '同意', checked }); const open = signal(false); modal({ title: '确认', open }, '内容')`
- 约定: 皮肤不持有业务状态,一律委托 headless 内核或调用方信号 - 约定: 皮肤不持有业务状态,一律委托 headless 内核或调用方信号
- 约定: 表单类组件统一约定:*Props.value/checked/active 为 Signal 双向绑定,onChange/onInput 为回调 - 约定: 表单类组件统一约定:*Props.value/checked/active 为 Signal 双向绑定,onChange/onInput 为回调
+77
View File
@@ -0,0 +1,77 @@
import { h } from '@ui-arch/frame';
import type { AnchorItem, AnchorProps } from './types.js';
/*** 锚点导航:竖排区块链接,滚动时高亮当前区块,点击平滑跳转(尊重减少动效偏好) ***/
export function anchor(props: AnchorProps): HTMLElement {
// —— 状态集中声明 ——
const items = props.items; // 锚点项,href 形如 #sec-xxx
const offset = props.offset ?? 0; // 目标顶部预留距离(吸顶栏高度)
const scroller = props.container instanceof HTMLElement ? props.container : null; // 内滚容器:null 表示整页滚动
const links: HTMLAnchorElement[] = []; // 与 items 一一对应的链接节点
const root = h('nav', { class: 'ua-anchor', 'aria-label': props.label ?? '锚点导航' });
/*** 目标元素:href 去掉 # 后按 id 查找,找不到返回 null ***/
const targetOf = (href: string): HTMLElement | null => document.getElementById(href.replace(/^#/, ''));
/*** 目标相对判定原点的垂直坐标:整页用视口坐标,容器用容器内偏移 ***/
const topOf = (el: HTMLElement): number => (scroller
? el.getBoundingClientRect().top - scroller.getBoundingClientRect().top
: el.getBoundingClientRect().top);
/*** 当前区块:取最后一个已越过判定线的目标,全未越过时落到首项 ***/
const current = (): AnchorItem | undefined => {
let hit: AnchorItem | undefined = items[0];
for (const item of items) {
const el = targetOf(item.href);
if (!el) continue; // 卫语句:目标不存在就跳过,不打断整表判定
if (topOf(el) - offset <= 1) hit = item;
}
return hit;
};
/*** 同步高亮:类名与 aria-current 一起切换,读屏能念出当前所在区块 ***/
const sync = (): void => {
const hit = current();
links.forEach((link, i) => {
const on = items[i] === hit;
link.classList.toggle('is-active', on);
if (on) link.setAttribute('aria-current', 'true');
else link.removeAttribute('aria-current');
});
};
/*** 跳转:按偏移量滚动到位并回写 hash,减少动效偏好下改瞬移 ***/
const jump = (item: AnchorItem): void => {
const el = targetOf(item.href);
if (!el) return; // 卫语句:目标不在页面上就什么都不做
const smooth = !window.matchMedia('(prefers-reduced-motion: reduce)').matches;
const behavior: ScrollBehavior = smooth ? 'smooth' : 'auto';
const top = topOf(el) + (scroller ? scroller.scrollTop : window.scrollY) - offset;
if (scroller) scroller.scrollTo({ top, behavior });
else window.scrollTo({ top, behavior });
history.replaceState(null, '', item.href); // 用 replaceState:跳转不堆积历史条目
props.onChange?.(item.href);
sync();
};
for (const item of items) {
const link = h('a', {
class: item.disabled ? 'ua-anchor-link is-disabled' : 'ua-anchor-link',
href: item.href,
'aria-disabled': item.disabled ? 'true' : undefined,
onClick: (e: Event) => {
e.preventDefault(); // 一律接管:自绘偏移量,不走浏览器默认锚点定位
if (item.disabled) return; // 卫语句:禁用项只吞掉点击
jump(item);
},
}, item.title) as HTMLAnchorElement;
links.push(link);
root.append(link);
}
const host: HTMLElement | Window = scroller ?? window;
host.addEventListener('scroll', sync, { passive: true }); // 被动监听:不阻塞滚动
window.addEventListener('resize', sync); // 视口变化会改变判定线
sync(); // 首屏同步一次,刷新后停在中间位置也能高亮正确项
return root;
}
+1
View File
@@ -57,3 +57,4 @@ export { colorPicker } from './colorPicker.js';
export { tour } from './tour.js'; export { tour } from './tour.js';
export { watermark } from './watermark.js'; export { watermark } from './watermark.js';
export { affix } from './affix.js'; export { affix } from './affix.js';
export { anchor } from './anchor.js';
+16
View File
@@ -679,4 +679,20 @@ export interface SpaceProps {
block?: boolean; // 是否占满父容器宽度;缺省按内容收缩 block?: boolean; // 是否占满父容器宽度;缺省按内容收缩
} }
/*** 锚点导航项 ***/
export interface AnchorItem {
href: string; // 目标区块地址(#id);对应元素须存在于页面上
title: string; // 链接文案
disabled?: boolean; // 是否禁用该项;缺省可用
}
/*** 锚点导出入参 ***/
export interface AnchorProps {
items: AnchorItem[]; // 锚点项集合,按显示顺序
offset?: number; // 定位时预留的顶部距离(吸顶栏高度);缺省 0
container?: HTMLElement | Window; // 监听的滚动容器;缺省 window(整页)
label?: string; // nav 的无障碍名称;缺省「锚点导航」
onChange?: (href: string) => void; // 跳转后的回调(埋点或联动滚动)
}
export type { ColumnDef, RowKey }; // 再导出常用内核类型,方便调用方 export type { ColumnDef, RowKey }; // 再导出常用内核类型,方便调用方
+41 -1
View File
@@ -71,7 +71,8 @@
"watermark(props, ...children):内容容器 + 平铺水印覆盖层,canvas 生成单元、颜色取自令牌,覆盖层不挡交互", "watermark(props, ...children):内容容器 + 平铺水印覆盖层,canvas 生成单元、颜色取自令牌,覆盖层不挡交互",
"checkboxGroup(props):多选组,value 为数组信号,min/max 用禁用边界项实现(不抛错),checkAll 支持半选且只统计未禁用项", "checkboxGroup(props):多选组,value 为数组信号,min/max 用禁用边界项实现(不抛错),checkAll 支持半选且只统计未禁用项",
"affix(props, ...children):固钉,滚过 offset 后内容转 fixed 贴顶、外层保留占位高度,固定后宽度随容器自适应", "affix(props, ...children):固钉,滚过 offset 后内容转 fixed 贴顶、外层保留占位高度,固定后宽度随容器自适应",
"multiSelect(props):多选下拉,标签式回显 + 可搜索弹层,点选不收起便于连续多选,max 到限后未选项禁用" "multiSelect(props):多选下拉,标签式回显 + 可搜索弹层,点选不收起便于连续多选,max 到限后未选项禁用",
"anchor(props):锚点导航,竖排区块链接,滚动时按目标位置高亮当前区块(aria-current 同步),点击按 offset 偏移量平滑定位并回写 hash;container 可指定内滚容器"
], ],
"keyTypes": [ "keyTypes": [
"ButtonProps", "ButtonProps",
@@ -2701,6 +2702,45 @@
"↑↓ 环形移动高亮、Enter 切换当前项、Esc 收起(与其它弹层共用一个 Esc 栈)", "↑↓ 环形移动高亮、Enter 切换当前项、Esc 收起(与其它弹层共用一个 Esc 栈)",
"value 传数组信号即受控,外部写入会同步标签区" "value 传数组信号即受控,外部写入会同步标签区"
] ]
},
{
"id": "anchor",
"title": "锚点导航 Anchor",
"demo": "sec-anchor",
"props": [
{
"n": "items",
"t": "AnchorItem[]",
"d": "锚点项集合,每项含 href(形如 #sec-x)、title、可选 disabled"
},
{
"n": "offset",
"t": "number",
"d": "定位与高亮判定预留的顶部距离,适合有吸顶栏的页面;缺省 0"
},
{
"n": "container",
"t": "HTMLElement | Window",
"d": "监听的滚动容器;缺省 window(整页滚动)"
},
{
"n": "label",
"t": "string",
"d": "nav 的无障碍名称;缺省「锚点导航」"
},
{
"n": "onChange",
"t": "(href: string) => void",
"d": "跳转后的回调,参数为命中的 href(适合埋点或联动)"
}
],
"example": "anchor({ items: [{ href: '#sec-button', title: '按钮' }, { href: '#sec-card', title: '卡片' }], offset: 72 })",
"steps": [
"在页面任意位置放一个容器,调用 anchor 并把返回的 nav 塞进去(固定侧栏或吸顶栏均可)",
"items 里每条 href 指向页面上真实存在的区块 id,标题即链接文案;目标缺失的项在滚动判定时自动跳过",
"页面有吸顶导航时把它的高度传给 offset,跳转与高亮判定都会预留这段距离",
"滚动容器不是整页时用 container 传入该元素;点击走平滑滚动,系统开启「减少动效」时改为瞬移,hash 用 replaceState 回写不堆历史"
]
} }
] ]
} }
+64
View File
@@ -3611,3 +3611,67 @@ button.ua-step-dot:hover {
.ua-space.is-block { .ua-space.is-block {
width: 100%; width: 100%;
} }
/* —— 锚点导航 —— */
.ua-anchor {
position: relative;
display: flex;
flex-direction: column;
gap: 2px;
padding-left: var(--ua-space-3);
}
.ua-anchor::before {
content: '';
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 2px;
background: var(--ua-color-border);
border-radius: 1px;
}
.ua-anchor-link {
position: relative;
padding: var(--ua-space-1) var(--ua-space-2);
border-radius: var(--ua-radius-sm);
color: var(--ua-color-text-muted);
font-size: var(--ua-font-size-md);
text-decoration: none;
transition: color 0.15s, background-color 0.15s, transform 0.15s;
}
.ua-anchor-link:hover {
color: var(--ua-color-text);
background: color-mix(in srgb, var(--ua-color-accent) 6%, transparent);
transform: translateX(2px);
}
.ua-anchor-link.is-active {
color: var(--ua-color-text);
background: color-mix(in srgb, var(--ua-color-accent) 10%, transparent);
}
.ua-anchor-link.is-active::before {
content: '';
position: absolute;
left: calc(-1 * var(--ua-space-3));
top: 50%;
width: 2px;
height: 70%;
transform: translateY(-50%);
background: var(--ua-color-accent);
border-radius: 1px;
}
.ua-anchor-link.is-disabled {
opacity: 0.45;
pointer-events: none;
}
.ua-anchor-link:focus-visible {
outline: 2px solid var(--ua-color-accent);
outline-offset: 2px;
}
@media (prefers-reduced-motion: reduce) {
.ua-anchor-link {
transition: none;
}
.ua-anchor-link:hover {
transform: none;
}
}