feat(ui): 新增 Caret / SortableList,箭头画法收成一份

- Caret:6px CSS 折角(open 切朝向,size 可调),Menu 与 Select 改为复用它,
  两处各自的画法删掉,全库只剩一份
- SortableList:可拖拽排序的条目列表(items / activeKey / onSelect / onReorder),
  拖拽位置计算从应用里挪进组件,顺序没变不回调
- 展示页:菜单补「折叠箭头」,新增 #sortable 一节并挂到导航
- COMPONENTS.md 契约 3.48 / 3.49、组件地图、README 清单、变更记录同步
This commit is contained in:
2026-11-01 22:07:31 +08:00
parent cc35400f67
commit 5eb1506650
14 changed files with 407 additions and 36 deletions
+3
View File
@@ -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() {
<Marks />
<Avatars />
<Tables />
<Sortables />
<Results />
<Progresses />
<StepsSection />
+30 -1
View File
@@ -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() {
</Card>
</Demo>
<Demo
title="折叠箭头"
desc="Caret 是菜单、下拉、可折叠表格行共用的 6px 折角:收起朝下、展开朝上,纯装饰,无障碍状态由外层按钮的 aria-expanded 承担。"
code={`<Caret />
<Caret open />`}
>
<Space gap="lg">
<Space gap="sm" align="center">
<Caret />
<Text size="sm" tone="muted">
收起
</Text>
</Space>
<Space gap="sm" align="center">
<Caret open />
<Text size="sm" tone="muted">
展开
</Text>
</Space>
</Space>
</Demo>
<PropsTable
rows={[
{ name: 'items', type: 'MenuItem[]', required: true, desc: '{ key, label, icon?, children? }' },
@@ -88,6 +110,13 @@ export function MenuSection() {
{ name: 'itemHeight', type: 'number', defaultValue: '36', desc: '一级菜单行高(px)' },
]}
/>
<PropsTable
rows={[
{ name: 'open', type: 'boolean', defaultValue: 'false', desc: '展开状态(决定箭头朝向)' },
{ name: 'size', type: 'number', defaultValue: '6', desc: '折角边长(px)' },
]}
/>
</Section>
)
}
+88
View File
@@ -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 (
<Section
id="sortable"
title="排序列表"
api="SortableList"
desc="一列可拖拽排序的条目:拖动改顺序、点击选中,顺序变化通过 onReorder 交回页面。只做鼠标拖拽,需要键盘排序时在外面配上移/下移按钮。"
>
<Demo
title="基础用法"
desc="拖住左侧手柄上下移动,松手后把新的 key 顺序交给 onReorder;点击选中项会高亮。"
layout="column"
code={`const [items, setItems] = useState(INITIAL)
<SortableList
items={items}
activeKey={activeKey}
onSelect={setActiveKey}
onReorder={(keys) => setItems(reorder(keys))}
/>`}
>
<div style={{ width: 280 }}>
<SortableList
items={items}
activeKey={activeKey}
onSelect={setActiveKey}
onReorder={reorder}
/>
</div>
</Demo>
<Demo
title="空态"
desc="items 为空时渲染 emptyText;不传就不显示空态。"
layout="column"
code={`<SortableList items={[]} onSelect={() => {}} onReorder={() => {}} emptyText="还没有卡片" />`}
>
<div style={{ width: 280 }}>
<SortableList
items={[]}
onSelect={() => undefined}
onReorder={() => undefined}
emptyText="还没有卡片"
/>
</div>
</Demo>
<PropsTable
rows={[
{ name: 'items', type: 'SortableItem[]', required: true, desc: '{ key, label, meta? }' },
{ name: 'onSelect', type: '(key: string) => 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: '空态文案,不传则不渲染' },
]}
/>
<Text size="sm" tone="muted">
说明:拖拽只在条目之间生效;松手时若顺序没变不会触发 onReorder。
</Text>
</Section>
)
}