From 5eb15066504abb214644802eb7a6801c4e0f44aa Mon Sep 17 00:00:00 2001 From: CNWei Date: Sun, 1 Nov 2026 22:07:31 +0800 Subject: [PATCH] =?UTF-8?q?feat(ui):=20=E6=96=B0=E5=A2=9E=20Caret=20/=20So?= =?UTF-8?q?rtableList=EF=BC=8C=E7=AE=AD=E5=A4=B4=E7=94=BB=E6=B3=95?= =?UTF-8?q?=E6=94=B6=E6=88=90=E4=B8=80=E4=BB=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Caret:6px CSS 折角(open 切朝向,size 可调),Menu 与 Select 改为复用它, 两处各自的画法删掉,全库只剩一份 - SortableList:可拖拽排序的条目列表(items / activeKey / onSelect / onReorder), 拖拽位置计算从应用里挪进组件,顺序没变不回调 - 展示页:菜单补「折叠箭头」,新增 #sortable 一节并挂到导航 - COMPONENTS.md 契约 3.48 / 3.49、组件地图、README 清单、变更记录同步 --- README.md | 2 + docs/COMPONENTS.md | 21 +++++ site/App.tsx | 3 + site/sections/MenuSection.tsx | 31 ++++++- site/sections/Sortables.tsx | 88 +++++++++++++++++++ src/components/Caret.module.css | 25 ++++++ src/components/Caret.tsx | 27 ++++++ src/components/Menu.module.css | 16 +--- src/components/Menu.tsx | 6 +- src/components/Select.module.css | 16 +--- src/components/Select.tsx | 3 +- src/components/SortableList.module.css | 82 +++++++++++++++++ src/components/SortableList.tsx | 117 +++++++++++++++++++++++++ src/index.ts | 6 ++ 14 files changed, 407 insertions(+), 36 deletions(-) create mode 100644 site/sections/Sortables.tsx create mode 100644 src/components/Caret.module.css create mode 100644 src/components/Caret.tsx create mode 100644 src/components/SortableList.module.css create mode 100644 src/components/SortableList.tsx diff --git a/README.md b/README.md index 4610889..87ad683 100644 --- a/README.md +++ b/README.md @@ -111,8 +111,10 @@ stash-ui/ | `Segmented` | 视图形态切换 | `options` `value` `onChange` `size` `block` | | `Tabs` | 页内分区切换 | `items` `activeKey` `onChange` `variant` | | `Menu` | 侧栏两级菜单 | `items` `activeKey` `onSelect` | +| `Caret` | 折叠箭头(6px 折角) | `open` `size` | | `Breadcrumb` | 路径导航 | `items` `separator` | | `Table` | 只读数据表格 | `columns` `rows` `rowKey` `size` `emptyText` | +| `SortableList` | 可拖拽排序的条目列表 | `items` `activeKey` `onSelect` `onReorder` `disabled` | | `Tag` | 分类 / 标签 / 状态(浅底 / 实底) | `tone` `variant` `shape` `icon` `dot` | | `ItemBadge` | 状态角标:置顶 / 付费 / HOT | `tone` | | `Badge` | 数字角标 | `count` `tone` `max` | diff --git a/docs/COMPONENTS.md b/docs/COMPONENTS.md index ee0f1d3..e893c50 100644 --- a/docs/COMPONENTS.md +++ b/docs/COMPONENTS.md @@ -31,6 +31,7 @@ | `Segmented` | L2 | ✅ | 视图形态切换 | `options` `value` `onChange` `size` `block` | | `Tabs` | L2 | ✅ | 页内分区切换 | `items` `activeKey` `onChange` `variant` | | `Menu` | L2 | ✅ | 侧栏两级菜单(可折叠动画) | `items` `activeKey` `onSelect` `itemHeight` | +| `Caret` | L1 | ✅ | 折叠箭头(6px 折角,收起/展开) | `open` `size` | | `Breadcrumb` | L3 | ✅ | 路径导航 | `items` `separator` | | `Brand` | L2 | ✅ | 品牌标识:logo / 首字标记 + 名称 | `name` `mark` `logo` `href` `size` `tone` | | `SearchField` | L2 | ✅ | 搜索框(语义版 Input,对外只给字符串) | `value` `onChange` `variant` `size` `hotkey` | @@ -45,6 +46,7 @@ | `Grid` | L3 | ✅ | 卡片列表网格 | `columns` `tabletColumns` `mobileColumns` `gap` `as` | | `Space` | L3 | ✅ | 一维间距容器 | `direction` `gap` `wrap` `align` `justify` | | `Table` | L3 | ✅ | 只读数据表格 | `columns` `rows` `rowKey` `size` `emptyText` | +| `SortableList` | L3 | ✅ | 可拖拽排序的条目列表 | `items` `activeKey` `onSelect` `onReorder` `disabled` | | `Notice` | L4 | ✅ | 说明 / 错误提示条 | `tone` `title` `action` | | `Empty` | L4 | ✅ | 空态 | `text` `action` | | `Result` | L4 | ✅ | 整块结果反馈 | `status` `title` `description` `actions` | @@ -686,6 +688,24 @@ - **规则**:开关的无障碍名称必须有值,label 不是字符串时请显式传 `switchLabel`。 - **展示页**:`#switch` +### 3.48 Caret ✅ + +- **用途**:折叠箭头——一个 6px 的 CSS 折角,表示「可展开 / 已展开」。 +- **不该用在哪**:带文字的完整按钮(用 `Button` / `TextButton`);方向指示(用 `Icon`)。 +- **解剖**:单个 `span`,left + bottom 两道边框旋转成折角,`aria-hidden`。 +- **Props**:`open`(默认 `false`,收起朝下 / 展开朝上)、`size`(折角边长,默认 6)。 +- **规则**:纯装饰,无障碍状态由外层按钮的 `aria-expanded` 承担;`Menu` 与 `Select` 内部就用的它。 +- **展示页**:`#menu` + +### 3.49 SortableList ✅ + +- **用途**:一列可拖拽排序的条目(卡片列表、菜单排序、优先级调整)。 +- **不该用在哪**:需要键盘完成排序的场景(本组件只做鼠标拖拽,需在外配上移 / 下移按钮);只读列表(用 `Menu` 或自己渲染)。 +- **解剖**:容器 + 逐条 `button`(拖拽手柄 / 文案 / 右侧 meta),拖动时 `dragging`、经过时 `over`、选中时 `active`。 +- **Props**:`items: { key, label, meta? }[]`、`activeKey`、`onSelect`、`onReorder(keys)`、`disabled`、`emptyText`。 +- **规则**:顺序没变不触发 `onReorder`;向下拖插到目标之后(保证能拖到末尾);`disabled` 期间禁止拖动,仍可点击。 +- **展示页**:`#sortable` + --- ## 4. 组合模式(不进库,但必须照这个拼) @@ -792,3 +812,4 @@ | 2026-11-01 | 新增 `EntryRow` / `EntryTile`:把应用里两套手写内容卡(`.app-card` / `.ces-card`)收进库。两者共用同一套点击语义(无修饰键接管、按住修饰键保留默认)与悬停反馈;`EntryTile` 由 `Card padding="none" hoverable` + `Cover` 拼成。兼容性:纯新增 | | 2026-11-01 | `Select` 的下拉箭头从 `arrow_down_line` 换成和 `Menu` 一致的 CSS 折角(6px、收起朝下 / 展开朝上)。影响面:所有 `Select` 的外观,交互不变 | | 2026-11-01 | 新增 `TextButton`(文字按钮:`tone` 主色 / 危险色、可选图标)与 `ToggleRow`(左标签 + 右开关的开关行,支持 `hint`);`Table` 新增 `size: 'md' \| 'sm'` 紧凑档。目的是把后台配置页里手写的 `.group-action` / `.sidebar-toggle-row` / `` 收进库。兼容性:纯新增,`Table` 默认行为不变 | +| 2026-11-01 | 新增 `Caret`(6px 折角,`Menu` 与 `Select` 内部改为复用它,画法只剩一份)与 `SortableList`(可拖拽排序列表,`onReorder` 回传新顺序)。兼容性:纯新增;`Menu` / `Select` 的箭头外观不变 | diff --git a/site/App.tsx b/site/App.tsx index 9509497..3f6828f 100644 --- a/site/App.tsx +++ b/site/App.tsx @@ -16,6 +16,7 @@ import { Layouts } from './sections/Layouts' import { SiteChrome } from './sections/SiteChrome' import { Marks } from './sections/Marks' import { MenuSection } from './sections/MenuSection' +import { Sortables } from './sections/Sortables' import { Modals } from './sections/Modals' import { Overview } from './sections/Overview' import { PageLayout } from './sections/PageLayout' @@ -83,6 +84,7 @@ const NAV = [ { id: 'breadcrumb', label: '面包屑 Breadcrumb' }, { id: 'tabs', label: '标签页 Tabs' }, { id: 'menu', label: '菜单 Menu' }, + { id: 'sortable', label: '排序列表 SortableList' }, ], }, { @@ -208,6 +210,7 @@ export function App() { + diff --git a/site/sections/MenuSection.tsx b/site/sections/MenuSection.tsx index 27b3a1f..94b7c46 100644 --- a/site/sections/MenuSection.tsx +++ b/site/sections/MenuSection.tsx @@ -1,5 +1,5 @@ import { useState } from 'react' -import { Card, Icon, Menu, Text } from '../../src' +import { Card, Caret, Icon, Menu, Space, Text } from '../../src' import { Demo } from '../components/Demo' import { PropsTable } from '../components/PropsTable' import { Section } from '../components/Section' @@ -79,6 +79,28 @@ export function MenuSection() { + +`} + > + + + + + 收起 + + + + + + 展开 + + + + + + + ) } diff --git a/site/sections/Sortables.tsx b/site/sections/Sortables.tsx new file mode 100644 index 0000000..d4df9c5 --- /dev/null +++ b/site/sections/Sortables.tsx @@ -0,0 +1,88 @@ +import { useState } from 'react' +import { SortableList, Text } from '../../src' +import { Demo } from '../components/Demo' +import { PropsTable } from '../components/PropsTable' +import { Section } from '../components/Section' + +const INITIAL = [ + { key: 'pin', label: '常用推荐', meta: 12 }, + { key: 'movie', label: '推荐电影', meta: 12 }, + { key: 'ces', label: 'CES 现场报道', meta: 12 }, + { key: 'tools', label: '开发工具', meta: 5 }, +] + +export function Sortables() { + const [items, setItems] = useState(INITIAL) + const [activeKey, setActiveKey] = useState('pin') + + const reorder = (keys: string[]) => { + setItems((current) => + keys + .map((key) => current.find((item) => item.key === key)) + .filter((item): item is (typeof INITIAL)[number] => item !== undefined), + ) + } + + return ( +
+ setItems(reorder(keys))} +/>`} + > +
+ +
+
+ + {}} onReorder={() => {}} emptyText="还没有卡片" />`} + > +
+ undefined} + onReorder={() => undefined} + emptyText="还没有卡片" + /> +
+
+ + void', required: true, desc: '点击选中' }, + { name: 'onReorder', type: '(keys: string[]) => void', required: true, desc: '拖拽后的完整顺序' }, + { name: 'activeKey', type: 'string', desc: '当前选中项' }, + { name: 'disabled', type: 'boolean', defaultValue: 'false', desc: '排序保存中,禁止再次拖动' }, + { name: 'emptyText', type: 'ReactNode', desc: '空态文案,不传则不渲染' }, + ]} + /> + + + 说明:拖拽只在条目之间生效;松手时若顺序没变不会触发 onReorder。 + +
+ ) +} diff --git a/src/components/Caret.module.css b/src/components/Caret.module.css new file mode 100644 index 0000000..2c49e5f --- /dev/null +++ b/src/components/Caret.module.css @@ -0,0 +1,25 @@ +/* 折角箭头:左 + 下两条边旋转而成,收起朝下、展开朝上 */ +.root { + display: inline-block; + flex-shrink: 0; + width: 6px; + height: 6px; + border-left: 1px solid currentColor; + border-bottom: 1px solid currentColor; + opacity: 0.4; + transform: rotate(315deg); + transition: + transform var(--su-duration-base) var(--su-ease-in-out), + opacity var(--su-duration-base) var(--su-ease-in-out); +} + +.open { + opacity: 0.7; + transform: rotate(135deg); +} + +@media (prefers-reduced-motion: reduce) { + .root { + transition: none; + } +} diff --git a/src/components/Caret.tsx b/src/components/Caret.tsx new file mode 100644 index 0000000..bb150e7 --- /dev/null +++ b/src/components/Caret.tsx @@ -0,0 +1,27 @@ +import styles from './Caret.module.css' + +/** + * 折叠箭头:一个 6px 的 CSS 折角,表示「可展开 / 已展开」。 + * + * Menu、Select、可折叠表格行里的箭头都用它,保证画法一致; + * 纯装饰,`aria-hidden`,无障碍状态由外层按钮的 `aria-expanded` 承担。 + */ +export type CaretProps = { + /** 展开状态:收起朝下、展开朝上 */ + open?: boolean + /** 尺寸,默认 6px */ + size?: number + className?: string +} + +export function Caret({ open = false, size, className }: CaretProps) { + return ( + )} {item.label} -