iter(g305): 新增瀑布流布局 masonry

This commit is contained in:
HIsheR
2026-10-27 00:34:25 +08:00
parent adcd0f7dc5
commit 08f14ce0db
13 changed files with 182 additions and 10 deletions
+9
View File
@@ -3131,6 +3131,15 @@
"motivation": "sortableList 是句柄式组件(返回 { el, move, order }),与 form / virtualList 同类,但 18 条配方里没有一条演示句柄式拼装——Agent 照配方抄会把返回值直接当节点 append,页面一片空白;「顺序只存一份」「move 与拖拽同一条提交路径」这些约定也没人写下来。",
"acceptance": "① recipes.md 新增配方 19(编号连续、带代码块与要点),代码块里认出 sortableList / card / button / toast;② 要点覆盖句柄要 append(.el)、顺序唯一真相、move 与拖拽同路径、边缘不自动滚动、条目文案唯一;③ ua recipes 19 能取到该条,llms-full.txt 与 llms.txt 重生成;④ npm run verify / check 全绿。",
"notes": "recipes.md 新增配方 19:sortableList + card + button + toast 的可排序检查单,要点写清句柄要 append(.el)、顺序唯一真相、move 与拖拽同路径、边缘不自动滚动、文案唯一;ua recipes 19 可取出,llms.txt / llms-full.txt 重生成"
},
{
"id": "g305",
"domain": "new",
"status": "open",
"title": "新增瀑布流布局 masonry(89 个组件)",
"motivation": "库里有 grid(等宽等高的规则栅格)与 space(线性间距),但图片墙、评论流、看板这类「列高不等」的布局没有落点,业务侧只能自己写 column-width 与 break-inside,还要记住多列容器不能是 flex/grid 这个坑。",
"acceptance": "① masonry({ items, columns?, minColumnWidth?, gap? }) 走 CSS 多列:不传 columns 按 minColumnWidth 自适应,传了即固定列数;② 每项包 .ua-masonry-item 并 break-inside: avoid,卡片不在列边界拆断;③ gap 同时管列间距与行间距;④ 样式只消费 --ua-* 令牌,禁止新增硬编码阴影/时长/字号;⑤ manifest/docs/README/demo/llms-full 同步,组件 88→89,npm run check 全绿。",
"notes": ""
}
]
}
+42 -2
View File
@@ -1,6 +1,6 @@
{
"$schema": "ui-arch-registry-index/0.1",
"generatedAt": "2026-10-26T16:31:56.843Z",
"generatedAt": "2026-10-26T16:34:08.281Z",
"repoRoot": ".",
"packages": [
{
@@ -231,7 +231,8 @@
"heatmap(props):日历热力图,按周列 × 星期行铺格子、四档浓度表示当天数量,悬停看明细",
"progressRing(props):环形进度,SVG 双圆靠 dasharray 画弧 + 中心百分比,value 可传信号自动重绘",
"noticeBar(props):单行通知条,语义色 + 左侧符号 + 可选跑马灯与关闭,适合页顶常驻播报",
"sortableList(props):拖拽排序列表,整行可拖 + Alt+↑↓ 键盘搬位,返回 { el, move, order } 句柄"
"sortableList(props):拖拽排序列表,整行可拖 + Alt+↑↓ 键盘搬位,返回 { el, move, order } 句柄",
"masonry(props):瀑布流多列布局,列宽自适应或固定列数,卡片不被拆到两列"
],
"keyTypes": [
"ButtonProps",
@@ -4696,6 +4697,45 @@
"外部要按代码调整顺序用句柄 move(from, to),下标越界或禁用时静默忽略;order() 以 DOM 为唯一真相回读当前顺序",
"什么时候别用:条目超过一屏(拖到边缘不会自动滚动,改用筛选或上下按钮)、需要跨列表搬运(用两个实例并自己管状态)"
]
},
{
"id": "masonry",
"title": "瀑布流 Masonry",
"demo": "sec-masonry",
"props": [
{
"n": "items",
"t": "unknown[]",
"r": 1,
"d": "每个卡片内容(字符串 / DOM / 响应式值);数组顺序即铺放顺序,列高不等由内容自己决定"
},
{
"n": "columns",
"t": "number",
"d": "固定列数;传了即不随容器宽度变化,与 minColumnWidth 互斥"
},
{
"n": "minColumnWidth",
"t": "number",
"d": "自适应列宽下限(像素);缺省 240,容器越宽列数越多",
"df": "240"
},
{
"n": "gap",
"t": "string",
"d": "列间距与行间距(CSS 长度或令牌值);缺省 var(--ua-space-3)",
"df": "var(--ua-space-3)"
}
],
"example": "masonry({ minColumnWidth: 220, items: shots.map((s) => card({ title: s.title }, image({ src: s.url }))) })",
"steps": [
"items 里直接放 card / image / 任意 DOM:每项由组件包一层 .ua-masonry-item,只有这一层带 break-inside: avoid,卡片才不会在列边界断成两截",
"不传 columns 就按 minColumnWidth 自适应:容器变宽自动加列、窄屏自动降到一列,不需要自己监听 window.resize",
"要固定几列(比如大屏看板恒 4 列)就传 columns,此时 minColumnWidth 不再生效——两者互斥,别同时传指望它俩协商",
"间距传一次就够:gap 同时给 column-gap 与每项的 margin-bottom,列间距和行间距天然一致;改间距只改这一处",
"列高不等才是瀑布流的价值所在(图片墙、评论流、看板):等高内容请用 grid,瀑布流会把等高卡片铺成奇怪的错位",
"什么时候别用:需要严格的行对齐(表格化数据)、需要按列独立滚动(用 splitter / 多个 virtualList),以及首屏内容需要按阅读顺序严格横向(多列布局是竖向填充,读屏顺序与视觉列序一致但不等同于行序)"
]
}
]
},
+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):88 个 DOM 组件(表单 / 反馈 / 浮层 / 导航 / 表格),样式只消费令牌
- `packages/skin/ui`(@ui-arch/ui):89 个 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
@@ -39,7 +39,7 @@ ui-arch-mvp/
│ │ ├── table-core/ # 表格状态内核
│ │ └── frame/ # 信号 + hyperscript 微框架
│ ├── skin/ # 皮肤层(DOM 组件,可发布)
│ │ └── ui/ # 88 个组件 + ui.css
│ │ └── ui/ # 89 个组件 + 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 可查漂移)
```
## 组件(共 88 个)
## 组件(共 89 个)
<!-- ua:components -->
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 · noticeBar · notification · pagination · pinInput · popover · popconfirm · progress · progressRing · radioGroup · rate · relativeTime · result · segmented · select · signaturePad · skeleton · slider · sortableList · 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 · masonry · menu · modal · mentions · multiSelect · noticeBar · notification · pagination · pinInput · popover · popconfirm · progress · progressRing · radioGroup · rate · relativeTime · result · segmented · select · signaturePad · skeleton · slider · sortableList · space · sparkline · spinner · splitter · statistic · stepper · switchButton · tabs · tag · textField · textarea · timeline · timePicker · toast · tooltip · tour · transfer · tree · treeSelect · typography · upload · virtualList · watermark
<!-- /ua:components -->
分组:表单(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 / noticeBar / 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 / sortableList / 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 / noticeBar / 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 / sortableList / kbd / highlight / timeline / tag / avatar / avatarGroup / image / carousel / calendar / typography / virtualList)、其他(floatButton / grid / layout / link / space / divider / splitter / backTop / affix / tour / masonry / 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:88 个 DOM 组件 + ui.css(只消费 `--ua-*` 令牌) |
| `packages/skin/ui` | @ui-arch/ui:89 个 DOM 组件 + ui.css(只消费 `--ua-*` 令牌) |
| `packages/design/tokens` | @ui-arch/tokens:设计令牌(暗色只换值不换名) |
| `packages/tools/cli` | @ui-arch/cli:`ua` 命令行与 stdio MCP server |
| `apps/demo` | 交互演示(esbuild 打包成单文件) |
+16
View File
@@ -62,6 +62,7 @@ import {
upload,
watermark,
menu,
masonry,
modal,
mentions,
multiSelect,
@@ -1930,6 +1931,21 @@ content.append(
h('p', { class: 'desc' }, () => `当前顺序:${sortableOrder().join(' → ')}`),
),
),
section(
'sec-masonry',
'瀑布流',
'masonry:CSS 多列布局,列宽按 minColumnWidth 自适应(窄屏自动降列)、也能固定列数;每项包一层才不被列边界拆断,间距一处传值同时管列间距与行间距。',
masonry({
minColumnWidth: 200,
items: [
card({ title: '图片墙 ①' }, h('p', { class: 'desc' }, '列高不同才是瀑布流的价值:内容自己决定卡片高度,浏览器负责找最短列。')),
card({ title: '图片墙 ②' }, h('p', { class: 'desc' }, '短卡片。')),
card({ title: '图片墙 ③' }, h('p', { class: 'desc' }, '这一张内容偏长,用来制造高度差:文字越多卡片越高,多列布局才会错落起来,而不是排成整齐的行。')),
card({ title: '图片墙 ④' }, h('p', { class: 'desc' }, '中等长度的一段说明,两行左右。')),
card({ title: '图片墙 ⑤' }, h('p', { class: 'desc' }, '再一条短的。')),
],
}),
),
);
// 悬浮按钮:fixed 定位,挂 body 不参与页面布局
+28 -1
View File
@@ -1,6 +1,6 @@
# @ui-arch 组件库 Agent 文档(无图形版)
> 自动生成于 2026-10-26T16:31:56.843Z,数据源:各包 ua.manifest.json,勿手改。人类版文档站:apps/docs/index.html。
> 自动生成于 2026-10-26T16:34:08.281Z,数据源:各包 ua.manifest.json,勿手改。人类版文档站:apps/docs/index.html。
> 组件 = 普通函数,传 props 返回 DOM;状态用 signal 双向绑定:读 signal() 写 signal(v)。
## 快速上手
@@ -2524,6 +2524,32 @@ container.append(sort.el);
现场演示:apps/docs/demo.html#sec-sortable
### masonry — 瀑布流 Masonry
参数:
- items(必填):unknown[] —— 每个卡片内容(字符串 / DOM / 响应式值);数组顺序即铺放顺序,列高不等由内容自己决定
- columns:number —— 固定列数;传了即不随容器宽度变化,与 minColumnWidth 互斥
- minColumnWidth(默认 240):number —— 自适应列宽下限(像素);缺省 240,容器越宽列数越多
- gap(默认 var(--ua-space-3)):string —— 列间距与行间距(CSS 长度或令牌值);缺省 var(--ua-space-3)
示例:
```
masonry({ minColumnWidth: 220, items: shots.map((s) => card({ title: s.title }, image({ src: s.url }))) })
```
手把手:
1. items 里直接放 card / image / 任意 DOM:每项由组件包一层 .ua-masonry-item,只有这一层带 break-inside: avoid,卡片才不会在列边界断成两截
2. 不传 columns 就按 minColumnWidth 自适应:容器变宽自动加列、窄屏自动降到一列,不需要自己监听 window.resize
3. 要固定几列(比如大屏看板恒 4 列)就传 columns,此时 minColumnWidth 不再生效——两者互斥,别同时传指望它俩协商
4. 间距传一次就够:gap 同时给 column-gap 与每项的 margin-bottom,列间距和行间距天然一致;改间距只改这一处
5. 列高不等才是瀑布流的价值所在(图片墙、评论流、看板):等高内容请用 grid,瀑布流会把等高卡片铺成奇怪的错位
6. 什么时候别用:需要严格的行对齐(表格化数据)、需要按列独立滚动(用 splitter / 多个 virtualList),以及首屏内容需要按阅读顺序严格横向(多列布局是竖向填充,读屏顺序与视觉列序一致但不等同于行序)
现场演示:apps/docs/demo.html#sec-masonry
API 速览:
- button(props):按钮,variant 三档视觉强度,disabled / loading 支持布尔或信号动态同步(loading 转圈并禁点),onClick 点击回调
@@ -2616,6 +2642,7 @@ API 速览:
- progressRing(props):环形进度,SVG 双圆靠 dasharray 画弧 + 中心百分比,value 可传信号自动重绘
- noticeBar(props):单行通知条,语义色 + 左侧符号 + 可选跑马灯与关闭,适合页顶常驻播报
- sortableList(props):拖拽排序列表,整行可拖 + Alt+↑↓ 键盘搬位,返回 { el, move, order } 句柄
- masonry(props):瀑布流多列布局,列宽自适应或固定列数,卡片不被拆到两列
一行用法:
+1
View File
@@ -137,6 +137,7 @@
- progressRing(props):环形进度,SVG 双圆靠 dasharray 画弧 + 中心百分比,value 可传信号自动重绘
- noticeBar(props):单行通知条,语义色 + 左侧符号 + 可选跑马灯与关闭,适合页顶常驻播报
- sortableList(props):拖拽排序列表,整行可拖 + Alt+↑↓ 键盘搬位,返回 { el, move, order } 句柄
- masonry(props):瀑布流多列布局,列宽自适应或固定列数,卡片不被拆到两列
- 用法: `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
@@ -88,3 +88,4 @@ export { heatmap } from './heatmap.js';
export { progressRing } from './progressRing.js';
export { noticeBar } from './noticeBar.js';
export { sortableList } from './sortableList.js';
export { masonry } from './masonry.js';
+21
View File
@@ -0,0 +1,21 @@
import { h } from '@ui-arch/frame';
import type { MasonryProps } from './types.js';
/*** 瀑布流:CSS 多列布局,列数按容器宽度自适应或显式指定;每项单独包一层才能 break-inside 不拆断 ***/
export function masonry(props: MasonryProps): HTMLElement {
// —— 状态集中声明 ——
const adaptive = !props.columns; // 未指定列数即自适应(column-width 让浏览器自己算列数)
const width = props.minColumnWidth ?? 240; // 自适应时的最小列宽(像素)
const gap = props.gap ?? 'var(--ua-space-3)'; // 列间距与行间距同源,避免两处各写一份
const root = h('div', { class: 'ua-masonry' }) as HTMLElement;
root.style.columnGap = gap;
if (adaptive) root.style.columnWidth = `${width}px`; // 自适应:容器越宽列数越多,窄屏自动降到一列
else root.style.columnCount = String(props.columns); // 显式列数:与 column-width 互斥,固定几列就几列
props.items.forEach((item) => {
const cell = h('div', { class: 'ua-masonry-item' }, item);
cell.style.marginBottom = gap; // 行间距与列间距同值,视觉上才是均匀网格
root.append(cell);
});
return root;
}
+8
View File
@@ -1146,3 +1146,11 @@ export interface SortableListHandle {
move: (from: number, to: number) => void; // 程序化搬位:下标越界或禁用时静默忽略
order: () => string[]; // 读取当前顺序(以 DOM 为唯一真相)
}
/*** 瀑布流入参 ***/
export interface MasonryProps {
items: unknown[]; // 每个卡片内容(字符串 / DOM / 响应式值),按数组顺序铺进最短列
columns?: number; // 固定列数;传了即不随容器宽度变化,与 minColumnWidth 互斥
minColumnWidth?: number; // 自适应列宽下限(像素);缺省 240,容器越宽列数越多
gap?: string; // 列间距与行间距(CSS 长度或令牌值);缺省 var(--ua-space-3)
}
+41 -1
View File
@@ -102,7 +102,8 @@
"heatmap(props):日历热力图,按周列 × 星期行铺格子、四档浓度表示当天数量,悬停看明细",
"progressRing(props):环形进度,SVG 双圆靠 dasharray 画弧 + 中心百分比,value 可传信号自动重绘",
"noticeBar(props):单行通知条,语义色 + 左侧符号 + 可选跑马灯与关闭,适合页顶常驻播报",
"sortableList(props):拖拽排序列表,整行可拖 + Alt+↑↓ 键盘搬位,返回 { el, move, order } 句柄"
"sortableList(props):拖拽排序列表,整行可拖 + Alt+↑↓ 键盘搬位,返回 { el, move, order } 句柄",
"masonry(props):瀑布流多列布局,列宽自适应或固定列数,卡片不被拆到两列"
],
"keyTypes": [
"ButtonProps",
@@ -4567,6 +4568,45 @@
"外部要按代码调整顺序用句柄 move(from, to),下标越界或禁用时静默忽略;order() 以 DOM 为唯一真相回读当前顺序",
"什么时候别用:条目超过一屏(拖到边缘不会自动滚动,改用筛选或上下按钮)、需要跨列表搬运(用两个实例并自己管状态)"
]
},
{
"id": "masonry",
"title": "瀑布流 Masonry",
"demo": "sec-masonry",
"props": [
{
"n": "items",
"t": "unknown[]",
"r": 1,
"d": "每个卡片内容(字符串 / DOM / 响应式值);数组顺序即铺放顺序,列高不等由内容自己决定"
},
{
"n": "columns",
"t": "number",
"d": "固定列数;传了即不随容器宽度变化,与 minColumnWidth 互斥"
},
{
"n": "minColumnWidth",
"t": "number",
"d": "自适应列宽下限(像素);缺省 240,容器越宽列数越多",
"df": "240"
},
{
"n": "gap",
"t": "string",
"d": "列间距与行间距(CSS 长度或令牌值);缺省 var(--ua-space-3)",
"df": "var(--ua-space-3)"
}
],
"example": "masonry({ minColumnWidth: 220, items: shots.map((s) => card({ title: s.title }, image({ src: s.url }))) })",
"steps": [
"items 里直接放 card / image / 任意 DOM:每项由组件包一层 .ua-masonry-item,只有这一层带 break-inside: avoid,卡片才不会在列边界断成两截",
"不传 columns 就按 minColumnWidth 自适应:容器变宽自动加列、窄屏自动降到一列,不需要自己监听 window.resize",
"要固定几列(比如大屏看板恒 4 列)就传 columns,此时 minColumnWidth 不再生效——两者互斥,别同时传指望它俩协商",
"间距传一次就够:gap 同时给 column-gap 与每项的 margin-bottom,列间距和行间距天然一致;改间距只改这一处",
"列高不等才是瀑布流的价值所在(图片墙、评论流、看板):等高内容请用 grid,瀑布流会把等高卡片铺成奇怪的错位",
"什么时候别用:需要严格的行对齐(表格化数据)、需要按列独立滚动(用 splitter / 多个 virtualList),以及首屏内容需要按阅读顺序严格横向(多列布局是竖向填充,读屏顺序与视觉列序一致但不等同于行序)"
]
}
]
}
+9
View File
@@ -6089,3 +6089,12 @@ h5.ua-type-title { font-size: var(--ua-font-size-md); }
transition-property: color, background-color, border-color, box-shadow, opacity, fill, stroke; /* 只保留不产生空间位移的属性 */
}
}
/* —— 瀑布流:多列布局,列宽自适应或固定列数 —— */
.ua-masonry {
display: block; /* 多列容器不能是 flex/grid,否则 column-width 失效 */
}
.ua-masonry-item {
break-inside: avoid; /* 卡片不被拆到两列:图片墙里断半截最刺眼 */
page-break-inside: avoid; /* 打印时同样不拆断 */
}