From 7684ae2b103f64e45d4515c6c1d9f79a3b19b5ca Mon Sep 17 00:00:00 2001 From: HIsheR Date: Mon, 26 Oct 2026 12:23:25 +0800 Subject: [PATCH] =?UTF-8?q?iter(g58):=20=E6=96=B0=E5=A2=9E=20affix=20?= =?UTF-8?q?=E5=9B=BA=E9=92=89=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .ua/index.json | 34 ++++++++++++++++++++++-- AGENTS.md | 2 +- ARCHITECTURE.md | 2 +- README.md | 8 +++--- apps/demo/main.ts | 14 ++++++++++ llms-full.txt | 26 ++++++++++++++++++- llms.txt | 1 + packages/skin/ui/src/affix.ts | 43 +++++++++++++++++++++++++++++++ packages/skin/ui/src/index.ts | 1 + packages/skin/ui/src/types.ts | 6 +++++ packages/skin/ui/ua.manifest.json | 32 ++++++++++++++++++++++- packages/skin/ui/ui.css | 7 +++++ 12 files changed, 166 insertions(+), 10 deletions(-) create mode 100644 packages/skin/ui/src/affix.ts diff --git a/.ua/index.json b/.ua/index.json index 47cd7f3..9451dd4 100644 --- a/.ua/index.json +++ b/.ua/index.json @@ -1,6 +1,6 @@ { "$schema": "ui-arch-registry-index/0.1", - "generatedAt": "2026-10-26T04:21:58.071Z", + "generatedAt": "2026-10-26T04:23:09.567Z", "repoRoot": ".", "packages": [ { @@ -197,7 +197,8 @@ "colorPicker(props):颜色选择,预设色板(缺省语义令牌色)+ 原生取色器 + 手输 CSS 颜色串,value 为颜色串信号", "tour(props):引导漫游,返回 { el, start, close } 句柄;聚光框按 target 选择器定位,卡片自动选方位,Esc / 跳过走 onClose", "watermark(props, ...children):内容容器 + 平铺水印覆盖层,canvas 生成单元、颜色取自令牌,覆盖层不挡交互", - "checkboxGroup(props):多选组,value 为数组信号,min/max 用禁用边界项实现(不抛错),checkAll 支持半选且只统计未禁用项" + "checkboxGroup(props):多选组,value 为数组信号,min/max 用禁用边界项实现(不抛错),checkAll 支持半选且只统计未禁用项", + "affix(props, ...children):固钉,滚过 offset 后内容转 fixed 贴顶、外层保留占位高度,固定后宽度随容器自适应" ], "keyTypes": [ "ButtonProps", @@ -2519,6 +2520,35 @@ "checkAll 行的选中/半选只统计未禁用项,禁用项不计入基数", "direction 传 vertical 改为纵向排列;disabled 支持信号,整组响应式禁用" ] + }, + { + "id": "affix", + "title": "固钉 Affix", + "demo": "sec-affix", + "props": [ + { + "n": "offset", + "t": "number", + "d": "距视口顶部多少像素后转为固定;缺省 0" + }, + { + "n": "target", + "t": "HTMLElement | Window", + "d": "监听的滚动容器;缺省 window(整页)" + }, + { + "n": "children", + "t": "...unknown[]", + "d": "被固定的内容,作为第二个及之后的实参传入" + } + ], + "example": "affix({ offset: 8 }, card({ title: '滚动时我会贴顶' }, h('p', null, '正文')))", + "steps": [ + "把要贴住的内容作为实参传入;组件内部包两层:外层占位、内层负责 fixed", + "滚动到 offset 以内(默认贴视口顶)时内层转 fixed,外层同步撑出等高占位,页面不会塌跳", + "固定期间宽度与横向位置跟随占位容器,窗口缩放或父容器变宽都自动贴合", + "内滚容器场景把该元素传给 target,判定改为基于它的滚动位置" + ] } ] }, diff --git a/AGENTS.md b/AGENTS.md index ca49d61..af830be 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):54 个 DOM 组件(表单 / 反馈 / 浮层 / 导航 / 表格),样式只消费令牌 +- `packages/skin/ui`(@ui-arch/ui):55 个 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 e12061e..6a7501c 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -34,7 +34,7 @@ ui-arch-mvp/ │ │ ├── table-core/ # 表格状态内核 │ │ └── frame/ # 信号 + hyperscript 微框架 │ ├── skin/ # 皮肤层(DOM 组件,可发布) -│ │ └── ui/ # 54 个组件 + ui.css +│ │ └── ui/ # 55 个组件 + ui.css │ ├── design/ # 资产层(可发布) │ │ └── tokens/ # --ua-* 设计令牌 │ └── tools/ # 工程层(private,不发布) diff --git a/README.md b/README.md index 417a678..b06bdda 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 ``` -## 组件(共 54 个) +## 组件(共 55 个) -accordion · 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 · notification · pagination · popover · progress · radioGroup · rate · result · segmented · select · skeleton · slider · spinner · splitter · statistic · stepper · switchButton · tabs · tag · textField · textarea · timeline · toast · tooltip · tour · transfer · tree · upload · watermark +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 · notification · pagination · popover · progress · radioGroup · rate · result · segmented · select · skeleton · slider · spinner · splitter · statistic · stepper · switchButton · tabs · tag · textField · textarea · timeline · toast · tooltip · tour · transfer · tree · upload · watermark -分组:表单(button / textField / inputNumber / textarea / checkbox / checkboxGroup / colorPicker / switchButton / select / 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)、其他(divider / splitter / backTop / tour / watermark)。 +分组:表单(button / textField / inputNumber / textarea / checkbox / checkboxGroup / colorPicker / switchButton / select / 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)、其他(divider / splitter / backTop / affix / tour / watermark)。 ## 可访问性约定 @@ -31,7 +31,7 @@ accordion · alert · autoComplete · avatar · backTop · badge · breadcrumb |---|---| | `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:54 个 DOM 组件 + ui.css(只消费 `--ua-*` 令牌) | +| `packages/skin/ui` | @ui-arch/ui:55 个 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 6597a82..f01d9e4 100644 --- a/apps/demo/main.ts +++ b/apps/demo/main.ts @@ -9,6 +9,7 @@ const IMG_DEMO = 'data:image/svg+xml;utf8,' + encodeURIComponent( ); import { accordion, + affix, alert, autoComplete, avatar, @@ -763,6 +764,19 @@ const content = h( h('span', { class: 'ua-colorpicker-swatch', style: () => `background:${colorVal()}` }), ), ), + section( + 'sec-affix', + '固钉', + 'affix:内容滚过阈值后转 fixed 贴顶,外层保留占位高度不塌陷;固定后宽度随容器自适应。', + h( + 'div', + { class: 'row' }, + h('div', { style: 'flex:1;min-height:320px;display:flex;gap:16px' }, + affix({ offset: 8 }, card({ title: '滚动时我会贴顶' }, h('p', { class: 'desc' }, '向下滚动本页,这块卡片会固定在视口顶部(距顶 8px),左侧占位不会塌。'))), + h('div', { class: 'desc', style: 'flex:1' }, '右侧是普通内容,用来撑高这一行,方便观察固钉效果。'.repeat(6)), + ), + ), + ), section( 'sec-backtop', '返回顶部', diff --git a/llms-full.txt b/llms-full.txt index 5d52a14..b71c302 100644 --- a/llms-full.txt +++ b/llms-full.txt @@ -1,6 +1,6 @@ # @ui-arch 组件库 Agent 文档(无图形版) -> 自动生成于 2026-10-26T04:21:58.071Z,数据源:各包 ua.manifest.json,勿手改。人类版文档站:apps/docs/index.html。 +> 自动生成于 2026-10-26T04:23:09.567Z,数据源:各包 ua.manifest.json,勿手改。人类版文档站:apps/docs/index.html。 > 组件 = 普通函数,传 props 返回 DOM;状态用 signal 双向绑定:读 signal() 写 signal(v)。 ## 快速上手 @@ -1442,6 +1442,29 @@ checkboxGroup({ value: permPick, min: 1, max: 3, checkAll: true, options: [{ val 现场演示:apps/docs/demo.html#sec-checkgroup +### affix — 固钉 Affix + +参数: + +- offset:number —— 距视口顶部多少像素后转为固定;缺省 0 +- target:HTMLElement | Window —— 监听的滚动容器;缺省 window(整页) +- children:...unknown[] —— 被固定的内容,作为第二个及之后的实参传入 + +示例: + +``` +affix({ offset: 8 }, card({ title: '滚动时我会贴顶' }, h('p', null, '正文'))) +``` + +手把手: + +1. 把要贴住的内容作为实参传入;组件内部包两层:外层占位、内层负责 fixed +2. 滚动到 offset 以内(默认贴视口顶)时内层转 fixed,外层同步撑出等高占位,页面不会塌跳 +3. 固定期间宽度与横向位置跟随占位容器,窗口缩放或父容器变宽都自动贴合 +4. 内滚容器场景把该元素传给 target,判定改为基于它的滚动位置 + +现场演示:apps/docs/demo.html#sec-affix + API 速览: - button(props):按钮,variant = primary | ghost | danger,disabled 支持布尔或信号禁用;type 缺省 button(表单内不隐式提交) @@ -1500,6 +1523,7 @@ API 速览: - tour(props):引导漫游,返回 { el, start, close } 句柄;聚光框按 target 选择器定位,卡片自动选方位,Esc / 跳过走 onClose - watermark(props, ...children):内容容器 + 平铺水印覆盖层,canvas 生成单元、颜色取自令牌,覆盖层不挡交互 - checkboxGroup(props):多选组,value 为数组信号,min/max 用禁用边界项实现(不抛错),checkAll 支持半选且只统计未禁用项 +- affix(props, ...children):固钉,滚过 offset 后内容转 fixed 贴顶、外层保留占位高度,固定后宽度随容器自适应 一行用法: diff --git a/llms.txt b/llms.txt index 5bd6c4b..9c5dd1c 100644 --- a/llms.txt +++ b/llms.txt @@ -103,6 +103,7 @@ - tour(props):引导漫游,返回 { el, start, close } 句柄;聚光框按 target 选择器定位,卡片自动选方位,Esc / 跳过走 onClose - watermark(props, ...children):内容容器 + 平铺水印覆盖层,canvas 生成单元、颜色取自令牌,覆盖层不挡交互 - checkboxGroup(props):多选组,value 为数组信号,min/max 用禁用边界项实现(不抛错),checkAll 支持半选且只统计未禁用项 + - affix(props, ...children):固钉,滚过 offset 后内容转 fixed 贴顶、外层保留占位高度,固定后宽度随容器自适应 - 用法: `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/affix.ts b/packages/skin/ui/src/affix.ts new file mode 100644 index 0000000..af4d08b --- /dev/null +++ b/packages/skin/ui/src/affix.ts @@ -0,0 +1,43 @@ +import { h } from '@ui-arch/frame'; +import type { AffixProps } from './types.js'; + +/*** 固钉:内容滚动越过阈值后转 fixed 贴住视口顶部,外层保留占位高度避免布局跳动 ***/ +export function affix(props: AffixProps, ...children: unknown[]): HTMLElement { + // —— 状态集中声明 —— + const offset = props.offset ?? 0; // 触发固定的阈值 + const scroller = props.target ?? window; // 滚动容器 + const inner = h('div', { class: 'ua-affix-inner' }, ...children) as HTMLElement; // 真正会被固定的内容 + const wrap = h('div', { class: 'ua-affix' }, inner) as HTMLElement; // 占位容器,固定时撑住原位 + let fixed = false; // 当前是否已固定,避免每次滚动都重写样式 + + /*** 同步固定态:越过阈值即 fixed + 撑占位高度,回到阈值以上就还原 ***/ + const sync = (): void => { + const box = wrap.getBoundingClientRect(); // 占位容器几何:left / width 决定固定后的横向位置 + const should = box.top <= offset; // 已滚到阈值之上 + if (should === fixed) { + if (fixed) { // 固定中:宽度与横向位置跟随容器(窗口缩放 / 父容器变宽) + inner.style.width = `${box.width}px`; + inner.style.left = `${box.left}px`; + } + return; // 卫语句:状态未变不重写其余样式 + } + fixed = should; + if (fixed) { + wrap.style.height = `${inner.offsetHeight}px`; // 先撑住占位,再让 inner 脱离文档流 + inner.classList.add('is-fixed'); + inner.style.top = `${offset}px`; + inner.style.left = `${box.left}px`; + inner.style.width = `${box.width}px`; + } else { + inner.classList.remove('is-fixed'); + inner.style.top = ''; + inner.style.width = ''; + wrap.style.height = ''; + } + }; + + scroller.addEventListener('scroll', sync, { passive: true }); // 被动监听,不阻塞滚动 + window.addEventListener('resize', sync); // 视口变化会改判定与宽度 + sync(); // 首屏同步一次:刷新时若已滚动到位,直接进入固定态 + return wrap; +} diff --git a/packages/skin/ui/src/index.ts b/packages/skin/ui/src/index.ts index a74a7e4..382d6eb 100644 --- a/packages/skin/ui/src/index.ts +++ b/packages/skin/ui/src/index.ts @@ -54,3 +54,4 @@ export { backTop } from './backTop.js'; export { colorPicker } from './colorPicker.js'; export { tour } from './tour.js'; export { watermark } from './watermark.js'; +export { affix } from './affix.js'; diff --git a/packages/skin/ui/src/types.ts b/packages/skin/ui/src/types.ts index d4f252f..2823348 100644 --- a/packages/skin/ui/src/types.ts +++ b/packages/skin/ui/src/types.ts @@ -620,4 +620,10 @@ export interface CheckboxGroupProps { onChange?: (values: string[]) => void; // 变更回调(传副本) } +/*** 固钉入参 ***/ +export interface AffixProps { + offset?: number; // 距视口顶部多少像素后转为固定;缺省 0 + target?: HTMLElement | Window; // 监听的滚动容器;缺省 window +} + export type { ColumnDef, RowKey }; // 再导出常用内核类型,方便调用方 diff --git a/packages/skin/ui/ua.manifest.json b/packages/skin/ui/ua.manifest.json index 131a365..c4a7f15 100644 --- a/packages/skin/ui/ua.manifest.json +++ b/packages/skin/ui/ua.manifest.json @@ -68,7 +68,8 @@ "colorPicker(props):颜色选择,预设色板(缺省语义令牌色)+ 原生取色器 + 手输 CSS 颜色串,value 为颜色串信号", "tour(props):引导漫游,返回 { el, start, close } 句柄;聚光框按 target 选择器定位,卡片自动选方位,Esc / 跳过走 onClose", "watermark(props, ...children):内容容器 + 平铺水印覆盖层,canvas 生成单元、颜色取自令牌,覆盖层不挡交互", - "checkboxGroup(props):多选组,value 为数组信号,min/max 用禁用边界项实现(不抛错),checkAll 支持半选且只统计未禁用项" + "checkboxGroup(props):多选组,value 为数组信号,min/max 用禁用边界项实现(不抛错),checkAll 支持半选且只统计未禁用项", + "affix(props, ...children):固钉,滚过 offset 后内容转 fixed 贴顶、外层保留占位高度,固定后宽度随容器自适应" ], "keyTypes": [ "ButtonProps", @@ -2390,6 +2391,35 @@ "checkAll 行的选中/半选只统计未禁用项,禁用项不计入基数", "direction 传 vertical 改为纵向排列;disabled 支持信号,整组响应式禁用" ] + }, + { + "id": "affix", + "title": "固钉 Affix", + "demo": "sec-affix", + "props": [ + { + "n": "offset", + "t": "number", + "d": "距视口顶部多少像素后转为固定;缺省 0" + }, + { + "n": "target", + "t": "HTMLElement | Window", + "d": "监听的滚动容器;缺省 window(整页)" + }, + { + "n": "children", + "t": "...unknown[]", + "d": "被固定的内容,作为第二个及之后的实参传入" + } + ], + "example": "affix({ offset: 8 }, card({ title: '滚动时我会贴顶' }, h('p', null, '正文')))", + "steps": [ + "把要贴住的内容作为实参传入;组件内部包两层:外层占位、内层负责 fixed", + "滚动到 offset 以内(默认贴视口顶)时内层转 fixed,外层同步撑出等高占位,页面不会塌跳", + "固定期间宽度与横向位置跟随占位容器,窗口缩放或父容器变宽都自动贴合", + "内滚容器场景把该元素传给 target,判定改为基于它的滚动位置" + ] } ] } diff --git a/packages/skin/ui/ui.css b/packages/skin/ui/ui.css index 8d4b30e..9d2a6a0 100644 --- a/packages/skin/ui/ui.css +++ b/packages/skin/ui/ui.css @@ -3250,3 +3250,10 @@ button.ua-step-dot:hover { align-items: flex-start; gap: var(--ua-space-1); } +.ua-affix { + position: relative; +} +.ua-affix-inner.is-fixed { + position: fixed; + z-index: 50; +}