@teamix-evo/ui 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +336 -0
- package/_data.json +12 -0
- package/manifest.json +1688 -0
- package/package.json +90 -0
- package/src/components/accordion/accordion.meta.md +87 -0
- package/src/components/accordion/accordion.stories.tsx +67 -0
- package/src/components/accordion/accordion.tsx +58 -0
- package/src/components/affix/affix.meta.md +80 -0
- package/src/components/affix/affix.stories.tsx +57 -0
- package/src/components/affix/affix.tsx +97 -0
- package/src/components/alert/alert.meta.md +101 -0
- package/src/components/alert/alert.stories.tsx +93 -0
- package/src/components/alert/alert.tsx +132 -0
- package/src/components/alert-dialog/alert-dialog.meta.md +107 -0
- package/src/components/alert-dialog/alert-dialog.stories.tsx +81 -0
- package/src/components/alert-dialog/alert-dialog.tsx +136 -0
- package/src/components/anchor/anchor.meta.md +87 -0
- package/src/components/anchor/anchor.stories.tsx +74 -0
- package/src/components/anchor/anchor.tsx +130 -0
- package/src/components/app/app.meta.md +86 -0
- package/src/components/app/app.stories.tsx +62 -0
- package/src/components/app/app.tsx +58 -0
- package/src/components/aspect-ratio/aspect-ratio.meta.md +81 -0
- package/src/components/aspect-ratio/aspect-ratio.stories.tsx +59 -0
- package/src/components/aspect-ratio/aspect-ratio.tsx +22 -0
- package/src/components/auto-complete/auto-complete.meta.md +102 -0
- package/src/components/auto-complete/auto-complete.stories.tsx +93 -0
- package/src/components/auto-complete/auto-complete.tsx +205 -0
- package/src/components/avatar/avatar.meta.md +94 -0
- package/src/components/avatar/avatar.stories.tsx +80 -0
- package/src/components/avatar/avatar.tsx +126 -0
- package/src/components/badge/badge.meta.md +119 -0
- package/src/components/badge/badge.stories.tsx +153 -0
- package/src/components/badge/badge.tsx +210 -0
- package/src/components/breadcrumb/breadcrumb.meta.md +107 -0
- package/src/components/breadcrumb/breadcrumb.stories.tsx +84 -0
- package/src/components/breadcrumb/breadcrumb.tsx +122 -0
- package/src/components/button/button.meta.md +98 -0
- package/src/components/button/button.stories.tsx +235 -0
- package/src/components/button/button.tsx +160 -0
- package/src/components/button-group/button-group.meta.md +92 -0
- package/src/components/button-group/button-group.stories.tsx +90 -0
- package/src/components/button-group/button-group.tsx +75 -0
- package/src/components/calendar/calendar.meta.md +118 -0
- package/src/components/calendar/calendar.stories.tsx +68 -0
- package/src/components/calendar/calendar.tsx +107 -0
- package/src/components/card/card.meta.md +117 -0
- package/src/components/card/card.stories.tsx +112 -0
- package/src/components/card/card.tsx +222 -0
- package/src/components/carousel/carousel.meta.md +117 -0
- package/src/components/carousel/carousel.stories.tsx +84 -0
- package/src/components/carousel/carousel.tsx +224 -0
- package/src/components/cascader/cascader.meta.md +110 -0
- package/src/components/cascader/cascader.stories.tsx +108 -0
- package/src/components/cascader/cascader.tsx +198 -0
- package/src/components/checkbox/checkbox.meta.md +99 -0
- package/src/components/checkbox/checkbox.stories.tsx +130 -0
- package/src/components/checkbox/checkbox.tsx +125 -0
- package/src/components/collapsible/collapsible.meta.md +80 -0
- package/src/components/collapsible/collapsible.stories.tsx +35 -0
- package/src/components/collapsible/collapsible.tsx +18 -0
- package/src/components/color-picker/color-picker.meta.md +84 -0
- package/src/components/color-picker/color-picker.stories.tsx +80 -0
- package/src/components/color-picker/color-picker.tsx +160 -0
- package/src/components/combobox/combobox.meta.md +93 -0
- package/src/components/combobox/combobox.stories.tsx +55 -0
- package/src/components/combobox/combobox.tsx +130 -0
- package/src/components/command/command.meta.md +104 -0
- package/src/components/command/command.stories.tsx +59 -0
- package/src/components/command/command.tsx +147 -0
- package/src/components/context-menu/context-menu.meta.md +90 -0
- package/src/components/context-menu/context-menu.stories.tsx +46 -0
- package/src/components/context-menu/context-menu.tsx +191 -0
- package/src/components/data-table/data-table.meta.md +149 -0
- package/src/components/data-table/data-table.stories.tsx +125 -0
- package/src/components/data-table/data-table.tsx +185 -0
- package/src/components/date-picker/date-picker.meta.md +106 -0
- package/src/components/date-picker/date-picker.stories.tsx +58 -0
- package/src/components/date-picker/date-picker.tsx +156 -0
- package/src/components/descriptions/descriptions.meta.md +78 -0
- package/src/components/descriptions/descriptions.stories.tsx +60 -0
- package/src/components/descriptions/descriptions.tsx +129 -0
- package/src/components/dialog/dialog.meta.md +105 -0
- package/src/components/dialog/dialog.stories.tsx +93 -0
- package/src/components/dialog/dialog.tsx +128 -0
- package/src/components/drawer/drawer.meta.md +96 -0
- package/src/components/drawer/drawer.stories.tsx +54 -0
- package/src/components/drawer/drawer.tsx +114 -0
- package/src/components/dropdown-menu/dropdown-menu.meta.md +103 -0
- package/src/components/dropdown-menu/dropdown-menu.stories.tsx +112 -0
- package/src/components/dropdown-menu/dropdown-menu.tsx +195 -0
- package/src/components/empty/empty.meta.md +81 -0
- package/src/components/empty/empty.stories.tsx +46 -0
- package/src/components/empty/empty.tsx +47 -0
- package/src/components/field/field.meta.md +116 -0
- package/src/components/field/field.stories.tsx +117 -0
- package/src/components/field/field.tsx +164 -0
- package/src/components/flex/flex.meta.md +94 -0
- package/src/components/flex/flex.stories.tsx +112 -0
- package/src/components/flex/flex.tsx +122 -0
- package/src/components/float-button/float-button.meta.md +87 -0
- package/src/components/float-button/float-button.stories.tsx +78 -0
- package/src/components/float-button/float-button.tsx +143 -0
- package/src/components/form/form.meta.md +131 -0
- package/src/components/form/form.stories.tsx +122 -0
- package/src/components/form/form.tsx +194 -0
- package/src/components/grid/grid.meta.md +87 -0
- package/src/components/grid/grid.stories.tsx +99 -0
- package/src/components/grid/grid.tsx +130 -0
- package/src/components/hover-card/hover-card.meta.md +92 -0
- package/src/components/hover-card/hover-card.stories.tsx +68 -0
- package/src/components/hover-card/hover-card.tsx +29 -0
- package/src/components/image/image.meta.md +94 -0
- package/src/components/image/image.stories.tsx +55 -0
- package/src/components/image/image.tsx +138 -0
- package/src/components/input/input.meta.md +109 -0
- package/src/components/input/input.stories.tsx +117 -0
- package/src/components/input/input.tsx +213 -0
- package/src/components/input-group/input-group.meta.md +92 -0
- package/src/components/input-group/input-group.stories.tsx +88 -0
- package/src/components/input-group/input-group.tsx +107 -0
- package/src/components/input-number/input-number.meta.md +91 -0
- package/src/components/input-number/input-number.stories.tsx +87 -0
- package/src/components/input-number/input-number.tsx +210 -0
- package/src/components/input-otp/input-otp.meta.md +105 -0
- package/src/components/input-otp/input-otp.stories.tsx +65 -0
- package/src/components/input-otp/input-otp.tsx +97 -0
- package/src/components/item/item.meta.md +116 -0
- package/src/components/item/item.stories.tsx +113 -0
- package/src/components/item/item.tsx +171 -0
- package/src/components/kbd/kbd.meta.md +85 -0
- package/src/components/kbd/kbd.stories.tsx +70 -0
- package/src/components/kbd/kbd.tsx +81 -0
- package/src/components/label/label.meta.md +91 -0
- package/src/components/label/label.stories.tsx +87 -0
- package/src/components/label/label.tsx +66 -0
- package/src/components/masonry/masonry.meta.md +85 -0
- package/src/components/masonry/masonry.stories.tsx +66 -0
- package/src/components/masonry/masonry.tsx +59 -0
- package/src/components/mentions/mentions.meta.md +89 -0
- package/src/components/mentions/mentions.stories.tsx +75 -0
- package/src/components/mentions/mentions.tsx +237 -0
- package/src/components/menubar/menubar.meta.md +100 -0
- package/src/components/menubar/menubar.stories.tsx +81 -0
- package/src/components/menubar/menubar.tsx +232 -0
- package/src/components/native-select/native-select.meta.md +88 -0
- package/src/components/native-select/native-select.stories.tsx +80 -0
- package/src/components/native-select/native-select.tsx +54 -0
- package/src/components/navigation-menu/navigation-menu.meta.md +108 -0
- package/src/components/navigation-menu/navigation-menu.stories.tsx +112 -0
- package/src/components/navigation-menu/navigation-menu.tsx +125 -0
- package/src/components/notification/notification.meta.md +91 -0
- package/src/components/notification/notification.stories.tsx +96 -0
- package/src/components/notification/notification.tsx +84 -0
- package/src/components/pagination/pagination.meta.md +127 -0
- package/src/components/pagination/pagination.stories.tsx +62 -0
- package/src/components/pagination/pagination.tsx +285 -0
- package/src/components/popconfirm/popconfirm.meta.md +109 -0
- package/src/components/popconfirm/popconfirm.stories.tsx +76 -0
- package/src/components/popconfirm/popconfirm.tsx +134 -0
- package/src/components/popover/popover.meta.md +97 -0
- package/src/components/popover/popover.stories.tsx +82 -0
- package/src/components/popover/popover.tsx +55 -0
- package/src/components/progress/progress.meta.md +86 -0
- package/src/components/progress/progress.stories.tsx +75 -0
- package/src/components/progress/progress.tsx +195 -0
- package/src/components/radio-group/radio-group.meta.md +103 -0
- package/src/components/radio-group/radio-group.stories.tsx +77 -0
- package/src/components/radio-group/radio-group.tsx +78 -0
- package/src/components/rate/rate.meta.md +87 -0
- package/src/components/rate/rate.stories.tsx +81 -0
- package/src/components/rate/rate.tsx +153 -0
- package/src/components/resizable/resizable.meta.md +92 -0
- package/src/components/resizable/resizable.stories.tsx +104 -0
- package/src/components/resizable/resizable.tsx +56 -0
- package/src/components/result/result.meta.md +90 -0
- package/src/components/result/result.stories.tsx +71 -0
- package/src/components/result/result.tsx +91 -0
- package/src/components/scroll-area/scroll-area.meta.md +84 -0
- package/src/components/scroll-area/scroll-area.stories.tsx +41 -0
- package/src/components/scroll-area/scroll-area.tsx +51 -0
- package/src/components/segmented/segmented.meta.md +103 -0
- package/src/components/segmented/segmented.stories.tsx +101 -0
- package/src/components/segmented/segmented.tsx +138 -0
- package/src/components/select/select.meta.md +110 -0
- package/src/components/select/select.stories.tsx +100 -0
- package/src/components/select/select.tsx +188 -0
- package/src/components/separator/separator.meta.md +74 -0
- package/src/components/separator/separator.stories.tsx +71 -0
- package/src/components/separator/separator.tsx +104 -0
- package/src/components/sheet/sheet.meta.md +97 -0
- package/src/components/sheet/sheet.stories.tsx +82 -0
- package/src/components/sheet/sheet.tsx +139 -0
- package/src/components/sidebar/sidebar.meta.md +131 -0
- package/src/components/sidebar/sidebar.stories.tsx +82 -0
- package/src/components/sidebar/sidebar.tsx +351 -0
- package/src/components/skeleton/skeleton.meta.md +95 -0
- package/src/components/skeleton/skeleton.stories.tsx +79 -0
- package/src/components/skeleton/skeleton.tsx +144 -0
- package/src/components/slider/slider.meta.md +94 -0
- package/src/components/slider/slider.stories.tsx +69 -0
- package/src/components/slider/slider.tsx +86 -0
- package/src/components/sonner/sonner.meta.md +96 -0
- package/src/components/sonner/sonner.stories.tsx +91 -0
- package/src/components/sonner/sonner.tsx +40 -0
- package/src/components/space/space.meta.md +94 -0
- package/src/components/space/space.stories.tsx +94 -0
- package/src/components/space/space.tsx +106 -0
- package/src/components/spinner/spinner.meta.md +76 -0
- package/src/components/spinner/spinner.stories.tsx +71 -0
- package/src/components/spinner/spinner.tsx +64 -0
- package/src/components/statistic/statistic.meta.md +99 -0
- package/src/components/statistic/statistic.stories.tsx +71 -0
- package/src/components/statistic/statistic.tsx +197 -0
- package/src/components/steps/steps.meta.md +102 -0
- package/src/components/steps/steps.stories.tsx +75 -0
- package/src/components/steps/steps.tsx +170 -0
- package/src/components/switch/switch.meta.md +92 -0
- package/src/components/switch/switch.stories.tsx +75 -0
- package/src/components/switch/switch.tsx +101 -0
- package/src/components/table/table.meta.md +95 -0
- package/src/components/table/table.stories.tsx +75 -0
- package/src/components/table/table.tsx +122 -0
- package/src/components/tabs/tabs.meta.md +98 -0
- package/src/components/tabs/tabs.stories.tsx +70 -0
- package/src/components/tabs/tabs.tsx +119 -0
- package/src/components/tag/tag.meta.md +94 -0
- package/src/components/tag/tag.stories.tsx +77 -0
- package/src/components/tag/tag.tsx +185 -0
- package/src/components/textarea/textarea.meta.md +83 -0
- package/src/components/textarea/textarea.stories.tsx +63 -0
- package/src/components/textarea/textarea.tsx +113 -0
- package/src/components/time-picker/time-picker.meta.md +83 -0
- package/src/components/time-picker/time-picker.stories.tsx +59 -0
- package/src/components/time-picker/time-picker.tsx +94 -0
- package/src/components/timeline/timeline.meta.md +102 -0
- package/src/components/timeline/timeline.stories.tsx +104 -0
- package/src/components/timeline/timeline.tsx +147 -0
- package/src/components/toggle/toggle.meta.md +88 -0
- package/src/components/toggle/toggle.stories.tsx +66 -0
- package/src/components/toggle/toggle.tsx +53 -0
- package/src/components/toggle-group/toggle-group.meta.md +90 -0
- package/src/components/toggle-group/toggle-group.stories.tsx +83 -0
- package/src/components/toggle-group/toggle-group.tsx +78 -0
- package/src/components/tooltip/tooltip.meta.md +99 -0
- package/src/components/tooltip/tooltip.stories.tsx +71 -0
- package/src/components/tooltip/tooltip.tsx +93 -0
- package/src/components/tour/tour.meta.md +116 -0
- package/src/components/tour/tour.stories.tsx +66 -0
- package/src/components/tour/tour.tsx +242 -0
- package/src/components/transfer/transfer.meta.md +90 -0
- package/src/components/transfer/transfer.stories.tsx +68 -0
- package/src/components/transfer/transfer.tsx +251 -0
- package/src/components/tree/tree.meta.md +111 -0
- package/src/components/tree/tree.stories.tsx +109 -0
- package/src/components/tree/tree.tsx +367 -0
- package/src/components/tree-select/tree-select.meta.md +100 -0
- package/src/components/tree-select/tree-select.stories.tsx +80 -0
- package/src/components/tree-select/tree-select.tsx +171 -0
- package/src/components/typography/typography.meta.md +102 -0
- package/src/components/typography/typography.stories.tsx +115 -0
- package/src/components/typography/typography.tsx +245 -0
- package/src/components/upload/upload.meta.md +111 -0
- package/src/components/upload/upload.stories.tsx +75 -0
- package/src/components/upload/upload.tsx +265 -0
- package/src/components/watermark/watermark.meta.md +95 -0
- package/src/components/watermark/watermark.stories.tsx +78 -0
- package/src/components/watermark/watermark.tsx +165 -0
- package/src/utils/cn.ts +6 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: scroll-area
|
|
3
|
+
name: ScrollArea
|
|
4
|
+
type: component
|
|
5
|
+
category: layout
|
|
6
|
+
since: 0.1.0
|
|
7
|
+
package: "@teamix-evo/ui"
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# ScrollArea
|
|
11
|
+
|
|
12
|
+
自定义滚动容器 — Radix ScrollArea。**shadcn-only**(antd 用原生滚动)。
|
|
13
|
+
**核心价值**:跨浏览器一致的滚动条样式 + 触屏友好 + 自动隐藏 thumb。
|
|
14
|
+
|
|
15
|
+
## When to use
|
|
16
|
+
|
|
17
|
+
- 需要一致滚动条样式的容器(列表 / 侧栏 / 抽屉内长内容)
|
|
18
|
+
- 在 Radix Dialog / Sheet 内的可滚动子区域
|
|
19
|
+
- 需要隐藏原生滚动条但保留滚动能力
|
|
20
|
+
|
|
21
|
+
## When NOT to use
|
|
22
|
+
|
|
23
|
+
- 整页滚动 → 用 `<body>` 默认
|
|
24
|
+
- 短内容(< 视口)→ 不需要包装
|
|
25
|
+
|
|
26
|
+
## Props
|
|
27
|
+
|
|
28
|
+
> 以下表格由 `pnpm --filter @teamix-evo/ui gen:meta` 自动生成。
|
|
29
|
+
|
|
30
|
+
<!-- auto:props:begin -->
|
|
31
|
+
_(no props)_
|
|
32
|
+
<!-- auto:props:end -->
|
|
33
|
+
|
|
34
|
+
## 依赖
|
|
35
|
+
|
|
36
|
+
> 以下表格由 `pnpm --filter @teamix-evo/ui gen:meta` 自动生成,数据源是 [`manifest.json`](../../../manifest.json)。**手工编辑 marker 之间的内容会在下次生成时被覆盖**。
|
|
37
|
+
|
|
38
|
+
<!-- auto:deps:begin -->
|
|
39
|
+
### 同库依赖
|
|
40
|
+
|
|
41
|
+
> `teamix-evo ui add scroll-area` 时,以下 entry 会被自动连带安装(无需手动 add)。
|
|
42
|
+
|
|
43
|
+
| Entry | 类型 | 描述 |
|
|
44
|
+
| --- | --- | --- |
|
|
45
|
+
| `cn` | util | Tailwind className 合并工具(clsx + tailwind-merge) |
|
|
46
|
+
|
|
47
|
+
### npm 依赖
|
|
48
|
+
|
|
49
|
+
> 业务侧需要先 `pnpm add` / `npm install` 这些包。CLI 在 `ui add` 完成后会列出此提示。
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
pnpm add @radix-ui/react-scroll-area@^1.2.0
|
|
53
|
+
```
|
|
54
|
+
<!-- auto:deps:end -->
|
|
55
|
+
|
|
56
|
+
> 子组件:`ScrollArea`(主容器,内置 Viewport + ScrollBar 组合)/ `ScrollBar`(独立 ScrollBar,需要自定义双向滚动时使用)。
|
|
57
|
+
|
|
58
|
+
## AI 生成纪律
|
|
59
|
+
|
|
60
|
+
- **必给容器固定高度**:ScrollArea 不会自动撑满父级,要么 `h-*` / `max-h-*`,要么父级 flex 撑开
|
|
61
|
+
- **`type` 控制行为**:`auto`(默认,内容溢出才显示)/ `always`(常驻)/ `scroll`(交互后显示)/ `hover`(hover 时显示)
|
|
62
|
+
- **横向滚动需独立 ScrollBar**:`<ScrollBar orientation="horizontal" />` 加在 `<ScrollArea>` 内
|
|
63
|
+
- **不嵌套 ScrollArea**:嵌套滚动会让 thumb 行为混乱
|
|
64
|
+
|
|
65
|
+
## Examples
|
|
66
|
+
|
|
67
|
+
```tsx
|
|
68
|
+
import { ScrollArea, ScrollBar } from '@/components/ui/scroll-area';
|
|
69
|
+
|
|
70
|
+
// 纵向(默认)
|
|
71
|
+
<ScrollArea className="h-72 w-48 rounded-md border p-4">
|
|
72
|
+
{Array.from({ length: 50 }).map((_, i) => (
|
|
73
|
+
<div key={i} className="text-sm">第 {i + 1} 行</div>
|
|
74
|
+
))}
|
|
75
|
+
</ScrollArea>
|
|
76
|
+
|
|
77
|
+
// 横向 + 纵向
|
|
78
|
+
<ScrollArea className="h-72 w-96 whitespace-nowrap rounded-md border">
|
|
79
|
+
<div className="flex w-max gap-4 p-4">
|
|
80
|
+
{items.map((it) => <div key={it.id} className="size-32 shrink-0">...</div>)}
|
|
81
|
+
</div>
|
|
82
|
+
<ScrollBar orientation="horizontal" />
|
|
83
|
+
</ScrollArea>
|
|
84
|
+
```
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import { ScrollArea, ScrollBar } from './scroll-area';
|
|
3
|
+
|
|
4
|
+
const meta: Meta<typeof ScrollArea> = {
|
|
5
|
+
title: '布局与容器 · Layout/ScrollArea',
|
|
6
|
+
component: ScrollArea,
|
|
7
|
+
tags: ['autodocs'],
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export default meta;
|
|
11
|
+
type Story = StoryObj<typeof ScrollArea>;
|
|
12
|
+
|
|
13
|
+
export const Vertical: Story = {
|
|
14
|
+
render: () => (
|
|
15
|
+
<ScrollArea className="h-72 w-48 rounded-md border p-4">
|
|
16
|
+
{Array.from({ length: 50 }).map((_, i) => (
|
|
17
|
+
<div key={i} className="py-1 text-sm">
|
|
18
|
+
第 {i + 1} 行
|
|
19
|
+
</div>
|
|
20
|
+
))}
|
|
21
|
+
</ScrollArea>
|
|
22
|
+
),
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const Horizontal: Story = {
|
|
26
|
+
render: () => (
|
|
27
|
+
<ScrollArea className="w-96 whitespace-nowrap rounded-md border">
|
|
28
|
+
<div className="flex w-max gap-4 p-4">
|
|
29
|
+
{Array.from({ length: 12 }).map((_, i) => (
|
|
30
|
+
<div
|
|
31
|
+
key={i}
|
|
32
|
+
className="flex size-32 shrink-0 items-center justify-center rounded-md bg-muted text-sm text-muted-foreground"
|
|
33
|
+
>
|
|
34
|
+
#{i + 1}
|
|
35
|
+
</div>
|
|
36
|
+
))}
|
|
37
|
+
</div>
|
|
38
|
+
<ScrollBar orientation="horizontal" />
|
|
39
|
+
</ScrollArea>
|
|
40
|
+
),
|
|
41
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area';
|
|
3
|
+
|
|
4
|
+
import { cn } from '@/utils/cn';
|
|
5
|
+
|
|
6
|
+
export interface ScrollAreaProps
|
|
7
|
+
extends React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root> {}
|
|
8
|
+
|
|
9
|
+
const ScrollArea = React.forwardRef<
|
|
10
|
+
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
|
|
11
|
+
ScrollAreaProps
|
|
12
|
+
>(({ className, children, ...props }, ref) => (
|
|
13
|
+
<ScrollAreaPrimitive.Root
|
|
14
|
+
ref={ref}
|
|
15
|
+
className={cn('relative overflow-hidden', className)}
|
|
16
|
+
{...props}
|
|
17
|
+
>
|
|
18
|
+
<ScrollAreaPrimitive.Viewport className="size-full rounded-[inherit]">
|
|
19
|
+
{children}
|
|
20
|
+
</ScrollAreaPrimitive.Viewport>
|
|
21
|
+
<ScrollBar />
|
|
22
|
+
<ScrollAreaPrimitive.Corner />
|
|
23
|
+
</ScrollAreaPrimitive.Root>
|
|
24
|
+
));
|
|
25
|
+
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;
|
|
26
|
+
|
|
27
|
+
const ScrollBar = React.forwardRef<
|
|
28
|
+
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
|
|
29
|
+
React.ComponentPropsWithoutRef<
|
|
30
|
+
typeof ScrollAreaPrimitive.ScrollAreaScrollbar
|
|
31
|
+
>
|
|
32
|
+
>(({ className, orientation = 'vertical', ...props }, ref) => (
|
|
33
|
+
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
|
34
|
+
ref={ref}
|
|
35
|
+
orientation={orientation}
|
|
36
|
+
className={cn(
|
|
37
|
+
'flex touch-none select-none transition-colors',
|
|
38
|
+
orientation === 'vertical' &&
|
|
39
|
+
'h-full w-2.5 border-l border-l-transparent p-[1px]',
|
|
40
|
+
orientation === 'horizontal' &&
|
|
41
|
+
'h-2.5 flex-col border-t border-t-transparent p-[1px]',
|
|
42
|
+
className,
|
|
43
|
+
)}
|
|
44
|
+
{...props}
|
|
45
|
+
>
|
|
46
|
+
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
|
|
47
|
+
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
|
48
|
+
));
|
|
49
|
+
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName;
|
|
50
|
+
|
|
51
|
+
export { ScrollArea, ScrollBar };
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: segmented
|
|
3
|
+
name: Segmented
|
|
4
|
+
type: component
|
|
5
|
+
category: form
|
|
6
|
+
since: 0.1.0
|
|
7
|
+
package: "@teamix-evo/ui"
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Segmented
|
|
11
|
+
|
|
12
|
+
分段控制器 — antd 独有补足。**等价 antd `Segmented`**(v5.0+)。视觉类似 iOS/macOS 系统 Segmented Control,强调**互斥单选 + 紧凑紧贴**(日 / 周 / 月、列表 / 网格、明 / 暗主题切换)。
|
|
13
|
+
|
|
14
|
+
## When to use
|
|
15
|
+
|
|
16
|
+
- 视图切换(列表 / 网格 / 时间轴)
|
|
17
|
+
- 时间区间切换(日 / 周 / 月 / 年)
|
|
18
|
+
- 主题 / 设置项的 2~4 档枚举(配 icon 视觉更直观)
|
|
19
|
+
|
|
20
|
+
## When NOT to use
|
|
21
|
+
|
|
22
|
+
- 工具栏按钮组(多选 / 不互斥)→ `ToggleGroup`
|
|
23
|
+
- 数据流单选过滤 → `RadioGroup`
|
|
24
|
+
- 选项超过 6 个 → 改用 `Select` / `Tabs`
|
|
25
|
+
|
|
26
|
+
<!-- auto:props:begin -->
|
|
27
|
+
| 名称 | 类型 | 默认值 | 必填 | 说明 |
|
|
28
|
+
| --- | --- | --- | --- | --- |
|
|
29
|
+
| `options` | `SegmentedOption[]` | – | ✓ | 候选项数组。 |
|
|
30
|
+
| `value` | `string` | – | – | 受控 value。 |
|
|
31
|
+
| `defaultValue` | `string` | – | – | uncontrolled 初值。 |
|
|
32
|
+
| `onChange` | `(value: string) => void` | – | – | value 变化回调。 |
|
|
33
|
+
| `size` | `'sm' \| 'default' \| 'lg'` | `"default"` | – | 尺寸。 |
|
|
34
|
+
| `block` | `boolean` | `false` | – | 是否撑满父容器宽度(antd `block` 并集)。 |
|
|
35
|
+
| `disabled` | `boolean` | – | – | 整组禁用。 |
|
|
36
|
+
<!-- auto:props:end -->
|
|
37
|
+
|
|
38
|
+
<!-- auto:deps:begin -->
|
|
39
|
+
### 同库依赖
|
|
40
|
+
|
|
41
|
+
> `teamix-evo ui add segmented` 时,以下 entry 会被自动连带安装(无需手动 add)。
|
|
42
|
+
|
|
43
|
+
| Entry | 类型 | 描述 |
|
|
44
|
+
| --- | --- | --- |
|
|
45
|
+
| `cn` | util | Tailwind className 合并工具(clsx + tailwind-merge) |
|
|
46
|
+
|
|
47
|
+
### npm 依赖
|
|
48
|
+
|
|
49
|
+
> 业务侧需要先 `pnpm add` / `npm install` 这些包。CLI 在 `ui add` 完成后会列出此提示。
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
pnpm add class-variance-authority@^0.7.0
|
|
53
|
+
```
|
|
54
|
+
<!-- auto:deps:end -->
|
|
55
|
+
|
|
56
|
+
## AI 生成纪律
|
|
57
|
+
|
|
58
|
+
- **选项数控制在 2~5 个**:Segmented 是紧凑控件,超过 5 个会拥挤
|
|
59
|
+
- **同语义同长度文本**:`日 / 周 / 月` 比 `今天的视图 / 本周 / 月` 视觉对称
|
|
60
|
+
- **配 icon 时图标尺寸 ≤ 16px**:Segmented 高度只有 32~40px,icon 大了挤压文字
|
|
61
|
+
- **`block=true`** 用于在容器内撑满(如卡片头切换视图);默认 fit-content 紧凑
|
|
62
|
+
- **不要用 Segmented 触发危险操作**:它是单选切换语义,不是按钮组
|
|
63
|
+
- **disabled 单项**:配 `disabled` 的选项依然显示,但点击 / 键盘均不可达
|
|
64
|
+
|
|
65
|
+
## Examples
|
|
66
|
+
|
|
67
|
+
```tsx
|
|
68
|
+
import { Segmented } from '@/components/ui/segmented';
|
|
69
|
+
import { LayoutGrid, List, Calendar } from 'lucide-react';
|
|
70
|
+
import * as React from 'react';
|
|
71
|
+
|
|
72
|
+
// 基础
|
|
73
|
+
<Segmented
|
|
74
|
+
options={[
|
|
75
|
+
{ value: 'day', label: '日' },
|
|
76
|
+
{ value: 'week', label: '周' },
|
|
77
|
+
{ value: 'month', label: '月' },
|
|
78
|
+
]}
|
|
79
|
+
defaultValue="week"
|
|
80
|
+
/>
|
|
81
|
+
|
|
82
|
+
// 配 icon
|
|
83
|
+
const [view, setView] = React.useState('grid');
|
|
84
|
+
<Segmented
|
|
85
|
+
value={view}
|
|
86
|
+
onChange={setView}
|
|
87
|
+
options={[
|
|
88
|
+
{ value: 'grid', label: '网格', icon: <LayoutGrid className="size-4" /> },
|
|
89
|
+
{ value: 'list', label: '列表', icon: <List className="size-4" /> },
|
|
90
|
+
{ value: 'cal', label: '日历', icon: <Calendar className="size-4" /> },
|
|
91
|
+
]}
|
|
92
|
+
/>
|
|
93
|
+
|
|
94
|
+
// block 撑满
|
|
95
|
+
<Segmented
|
|
96
|
+
block
|
|
97
|
+
options={[
|
|
98
|
+
{ value: 'light', label: '浅色' },
|
|
99
|
+
{ value: 'dark', label: '深色' },
|
|
100
|
+
{ value: 'system', label: '跟随系统' },
|
|
101
|
+
]}
|
|
102
|
+
/>
|
|
103
|
+
```
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
3
|
+
import { Calendar, LayoutGrid, List } from 'lucide-react';
|
|
4
|
+
import { Segmented } from './segmented';
|
|
5
|
+
|
|
6
|
+
const meta: Meta<typeof Segmented> = {
|
|
7
|
+
title: '表单与输入 · Form/Segmented',
|
|
8
|
+
component: Segmented,
|
|
9
|
+
tags: ['autodocs'],
|
|
10
|
+
parameters: {
|
|
11
|
+
docs: {
|
|
12
|
+
description: {
|
|
13
|
+
component:
|
|
14
|
+
'分段控制器 — iOS / macOS 风格的紧凑互斥单选(视图切换、时间区间、主题档)。等价 antd `Segmented`(v5.0+),与 ToggleGroup 互补(后者偏工具栏多选)。视觉走 OpenTrek tokens,所有样式来自 `@teamix-evo/design`,无 mock。',
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
argTypes: {
|
|
19
|
+
size: { control: 'inline-radio', options: ['sm', 'default', 'lg'] },
|
|
20
|
+
block: { control: 'boolean' },
|
|
21
|
+
disabled: { control: 'boolean' },
|
|
22
|
+
},
|
|
23
|
+
args: { size: 'default', block: false, disabled: false },
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export default meta;
|
|
27
|
+
type Story = StoryObj<typeof Segmented>;
|
|
28
|
+
|
|
29
|
+
export const Playground: Story = {
|
|
30
|
+
render: (args) => (
|
|
31
|
+
<Segmented
|
|
32
|
+
{...args}
|
|
33
|
+
defaultValue="week"
|
|
34
|
+
options={[
|
|
35
|
+
{ value: 'day', label: '日' },
|
|
36
|
+
{ value: 'week', label: '周' },
|
|
37
|
+
{ value: 'month', label: '月' },
|
|
38
|
+
]}
|
|
39
|
+
/>
|
|
40
|
+
),
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export const WithIcons: Story = {
|
|
44
|
+
parameters: { controls: { disable: true } },
|
|
45
|
+
render: () => {
|
|
46
|
+
const [v, setV] = React.useState('grid');
|
|
47
|
+
return (
|
|
48
|
+
<Segmented
|
|
49
|
+
value={v}
|
|
50
|
+
onChange={setV}
|
|
51
|
+
options={[
|
|
52
|
+
{ value: 'grid', label: '网格', icon: <LayoutGrid className="size-4" /> },
|
|
53
|
+
{ value: 'list', label: '列表', icon: <List className="size-4" /> },
|
|
54
|
+
{ value: 'cal', label: '日历', icon: <Calendar className="size-4" /> },
|
|
55
|
+
]}
|
|
56
|
+
/>
|
|
57
|
+
);
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export const Block: Story = {
|
|
62
|
+
parameters: { controls: { disable: true } },
|
|
63
|
+
render: () => (
|
|
64
|
+
<div className="w-80">
|
|
65
|
+
<Segmented
|
|
66
|
+
block
|
|
67
|
+
defaultValue="light"
|
|
68
|
+
options={[
|
|
69
|
+
{ value: 'light', label: '浅色' },
|
|
70
|
+
{ value: 'dark', label: '深色' },
|
|
71
|
+
{ value: 'system', label: '跟随系统' },
|
|
72
|
+
]}
|
|
73
|
+
/>
|
|
74
|
+
</div>
|
|
75
|
+
),
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export const Sizes: Story = {
|
|
79
|
+
parameters: { controls: { disable: true } },
|
|
80
|
+
render: () => (
|
|
81
|
+
<div className="flex flex-col items-start gap-3">
|
|
82
|
+
<Segmented size="sm" defaultValue="a" options={[{ value: 'a', label: 'A' }, { value: 'b', label: 'B' }]} />
|
|
83
|
+
<Segmented size="default" defaultValue="a" options={[{ value: 'a', label: 'A' }, { value: 'b', label: 'B' }]} />
|
|
84
|
+
<Segmented size="lg" defaultValue="a" options={[{ value: 'a', label: 'A' }, { value: 'b', label: 'B' }]} />
|
|
85
|
+
</div>
|
|
86
|
+
),
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
export const WithDisabledItem: Story = {
|
|
90
|
+
parameters: { controls: { disable: true } },
|
|
91
|
+
render: () => (
|
|
92
|
+
<Segmented
|
|
93
|
+
defaultValue="open"
|
|
94
|
+
options={[
|
|
95
|
+
{ value: 'open', label: '进行中' },
|
|
96
|
+
{ value: 'closed', label: '已关闭' },
|
|
97
|
+
{ value: 'archived', label: '已归档', disabled: true },
|
|
98
|
+
]}
|
|
99
|
+
/>
|
|
100
|
+
),
|
|
101
|
+
};
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { cva, type VariantProps } from 'class-variance-authority';
|
|
3
|
+
|
|
4
|
+
import { cn } from '@/utils/cn';
|
|
5
|
+
|
|
6
|
+
const segmentedVariants = cva(
|
|
7
|
+
'inline-flex w-fit items-center rounded-md bg-muted p-1',
|
|
8
|
+
{
|
|
9
|
+
variants: {
|
|
10
|
+
size: {
|
|
11
|
+
sm: 'h-8 text-xs',
|
|
12
|
+
default: 'h-9 text-sm',
|
|
13
|
+
lg: 'h-10 text-base',
|
|
14
|
+
},
|
|
15
|
+
block: { true: 'w-full', false: 'w-fit' },
|
|
16
|
+
},
|
|
17
|
+
defaultVariants: { size: 'default', block: false },
|
|
18
|
+
},
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
const itemVariants = cva(
|
|
22
|
+
'flex items-center justify-center gap-1.5 rounded-sm px-3 transition-all cursor-pointer select-none focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring',
|
|
23
|
+
{
|
|
24
|
+
variants: {
|
|
25
|
+
active: {
|
|
26
|
+
true: 'bg-background text-foreground shadow-sm',
|
|
27
|
+
false: 'text-muted-foreground hover:text-foreground',
|
|
28
|
+
},
|
|
29
|
+
disabled: {
|
|
30
|
+
true: 'cursor-not-allowed opacity-50 hover:text-muted-foreground',
|
|
31
|
+
false: '',
|
|
32
|
+
},
|
|
33
|
+
block: { true: 'flex-1', false: '' },
|
|
34
|
+
},
|
|
35
|
+
defaultVariants: { active: false, disabled: false, block: false },
|
|
36
|
+
},
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
export interface SegmentedOption {
|
|
40
|
+
/** 真实 value(受控比对依据)。 */
|
|
41
|
+
value: string;
|
|
42
|
+
/** 显示文本。 */
|
|
43
|
+
label: React.ReactNode;
|
|
44
|
+
/** 左侧图标(可选)。 */
|
|
45
|
+
icon?: React.ReactNode;
|
|
46
|
+
/** 禁用此项。 */
|
|
47
|
+
disabled?: boolean;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface SegmentedProps
|
|
51
|
+
extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onChange' | 'defaultValue'>,
|
|
52
|
+
VariantProps<typeof segmentedVariants> {
|
|
53
|
+
/** 候选项数组。 */
|
|
54
|
+
options: SegmentedOption[];
|
|
55
|
+
/** 受控 value。 */
|
|
56
|
+
value?: string;
|
|
57
|
+
/** uncontrolled 初值。 */
|
|
58
|
+
defaultValue?: string;
|
|
59
|
+
/** value 变化回调。 */
|
|
60
|
+
onChange?: (value: string) => void;
|
|
61
|
+
/**
|
|
62
|
+
* 尺寸。
|
|
63
|
+
* @default "default"
|
|
64
|
+
*/
|
|
65
|
+
size?: 'sm' | 'default' | 'lg';
|
|
66
|
+
/**
|
|
67
|
+
* 是否撑满父容器宽度(antd `block` 并集)。
|
|
68
|
+
* @default false
|
|
69
|
+
*/
|
|
70
|
+
block?: boolean;
|
|
71
|
+
/** 整组禁用。 */
|
|
72
|
+
disabled?: boolean;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* 分段控制器 — antd 独有补足。**等价 antd `Segmented`**(v5.0+)。
|
|
77
|
+
* 与 `ToggleGroup` 区别:Segmented 视觉**类似 iOS / macOS 系统的 Segmented Control**,
|
|
78
|
+
* 强调"互斥单选 + 紧凑紧贴"(同语义的视图切换:日/周/月、列表/网格);
|
|
79
|
+
* ToggleGroup 偏向工具栏按钮(可选 multiple、视觉更"按钮化")。
|
|
80
|
+
*/
|
|
81
|
+
const Segmented = React.forwardRef<HTMLDivElement, SegmentedProps>(
|
|
82
|
+
(
|
|
83
|
+
{
|
|
84
|
+
options,
|
|
85
|
+
value,
|
|
86
|
+
defaultValue,
|
|
87
|
+
onChange,
|
|
88
|
+
size,
|
|
89
|
+
block,
|
|
90
|
+
disabled = false,
|
|
91
|
+
className,
|
|
92
|
+
...props
|
|
93
|
+
},
|
|
94
|
+
ref,
|
|
95
|
+
) => {
|
|
96
|
+
const isControlled = value !== undefined;
|
|
97
|
+
const [internal, setInternal] = React.useState<string>(
|
|
98
|
+
defaultValue ?? options[0]?.value ?? '',
|
|
99
|
+
);
|
|
100
|
+
const current = isControlled ? value! : internal;
|
|
101
|
+
|
|
102
|
+
const select = (next: string) => {
|
|
103
|
+
if (!isControlled) setInternal(next);
|
|
104
|
+
onChange?.(next);
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
return (
|
|
108
|
+
<div
|
|
109
|
+
ref={ref}
|
|
110
|
+
role="radiogroup"
|
|
111
|
+
className={cn(segmentedVariants({ size, block }), className)}
|
|
112
|
+
{...props}
|
|
113
|
+
>
|
|
114
|
+
{options.map((opt) => {
|
|
115
|
+
const itemDisabled = disabled || opt.disabled;
|
|
116
|
+
const active = opt.value === current;
|
|
117
|
+
return (
|
|
118
|
+
<button
|
|
119
|
+
key={opt.value}
|
|
120
|
+
type="button"
|
|
121
|
+
role="radio"
|
|
122
|
+
aria-checked={active}
|
|
123
|
+
disabled={itemDisabled}
|
|
124
|
+
onClick={() => !itemDisabled && select(opt.value)}
|
|
125
|
+
className={cn(itemVariants({ active, disabled: itemDisabled, block }))}
|
|
126
|
+
>
|
|
127
|
+
{opt.icon ? <span className="inline-flex">{opt.icon}</span> : null}
|
|
128
|
+
{opt.label}
|
|
129
|
+
</button>
|
|
130
|
+
);
|
|
131
|
+
})}
|
|
132
|
+
</div>
|
|
133
|
+
);
|
|
134
|
+
},
|
|
135
|
+
);
|
|
136
|
+
Segmented.displayName = 'Segmented';
|
|
137
|
+
|
|
138
|
+
export { Segmented };
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: select
|
|
3
|
+
name: Select
|
|
4
|
+
type: component
|
|
5
|
+
category: form
|
|
6
|
+
since: 0.1.0
|
|
7
|
+
package: "@teamix-evo/ui"
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Select
|
|
11
|
+
|
|
12
|
+
下拉选择 — Radix Select 标准实现。**仅单选**(Radix Select 设计如此),多选 / 搜索请用 `Combobox`(基于 Command + Popover,在 v0.x)。
|
|
13
|
+
对应 antd `Select` 的最常见单选场景;antd 的 `mode="multiple"` / `showSearch` / `tags` 等高级形态由 Combobox 接力。
|
|
14
|
+
|
|
15
|
+
## When to use
|
|
16
|
+
|
|
17
|
+
- 单选枚举(状态 / 类型 / 区域 / 时区等)
|
|
18
|
+
- 选项 ≥ 5 时替代 RadioGroup
|
|
19
|
+
- 选项较长 / 需要分组 / 需要 Label 分隔
|
|
20
|
+
|
|
21
|
+
## When NOT to use
|
|
22
|
+
|
|
23
|
+
- 多选 / 可搜索 → `Combobox`(v0.x)
|
|
24
|
+
- 选项 ≤ 4 → `RadioGroup`(更直观)
|
|
25
|
+
- 自由输入 → `Input`
|
|
26
|
+
- 需要异步加载选项 → `Combobox` + 自定义数据源
|
|
27
|
+
|
|
28
|
+
## Props
|
|
29
|
+
|
|
30
|
+
> 以下表格由 `pnpm --filter @teamix-evo/ui gen:meta` 自动生成。下表是 `SelectTrigger` 的 props;`Select`(Root)透传 Radix `value / defaultValue / onValueChange / open / disabled / required / name`。
|
|
31
|
+
|
|
32
|
+
<!-- auto:props:begin -->
|
|
33
|
+
| 名称 | 类型 | 默认值 | 必填 | 说明 |
|
|
34
|
+
| --- | --- | --- | --- | --- |
|
|
35
|
+
| `position` | `React.ComponentPropsWithoutRef< typeof SelectPrimitive.Content >['position']` | `"popper"` | – | 浮层定位策略。`popper` 跟随 trigger 并自动避让边界;`item-aligned` 把当前选中项与 trigger 对齐(类似原生 `<select>`)。默认 `popper` 与 antd Select 行为一致。 |
|
|
36
|
+
<!-- auto:props:end -->
|
|
37
|
+
|
|
38
|
+
## 依赖
|
|
39
|
+
|
|
40
|
+
> 以下表格由 `pnpm --filter @teamix-evo/ui gen:meta` 自动生成,数据源是 [`manifest.json`](../../../manifest.json)。**手工编辑 marker 之间的内容会在下次生成时被覆盖**。
|
|
41
|
+
|
|
42
|
+
<!-- auto:deps:begin -->
|
|
43
|
+
### 同库依赖
|
|
44
|
+
|
|
45
|
+
> `teamix-evo ui add select` 时,以下 entry 会被自动连带安装(无需手动 add)。
|
|
46
|
+
|
|
47
|
+
| Entry | 类型 | 描述 |
|
|
48
|
+
| --- | --- | --- |
|
|
49
|
+
| `cn` | util | Tailwind className 合并工具(clsx + tailwind-merge) |
|
|
50
|
+
|
|
51
|
+
### npm 依赖
|
|
52
|
+
|
|
53
|
+
> 业务侧需要先 `pnpm add` / `npm install` 这些包。CLI 在 `ui add` 完成后会列出此提示。
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
pnpm add @radix-ui/react-select@^2.1.0 lucide-react@^0.460.0
|
|
57
|
+
```
|
|
58
|
+
<!-- auto:deps:end -->
|
|
59
|
+
|
|
60
|
+
> 完整子组件:`Select / SelectGroup / SelectValue / SelectTrigger / SelectContent / SelectLabel / SelectItem / SelectSeparator / SelectScrollUpButton / SelectScrollDownButton`。
|
|
61
|
+
|
|
62
|
+
## AI 生成纪律
|
|
63
|
+
|
|
64
|
+
- **`SelectValue` 配 placeholder**:`<SelectValue placeholder="..." />` 提供未选时的占位文字
|
|
65
|
+
- **每个 SelectItem 必有 value**:不能空字符串(Radix 拒绝);用稳定 ID
|
|
66
|
+
- **大量选项必分组**:超过 8 个用 `SelectGroup` + `SelectLabel`,加 `SelectSeparator` 分段
|
|
67
|
+
- **不要嵌套 Select**:嵌套下拉是反模式
|
|
68
|
+
- **键盘可用性自带**:Radix 自动支持 Type-ahead(首字母搜索),不需要再加 search
|
|
69
|
+
|
|
70
|
+
## Examples
|
|
71
|
+
|
|
72
|
+
```tsx
|
|
73
|
+
import {
|
|
74
|
+
Select, SelectTrigger, SelectValue, SelectContent,
|
|
75
|
+
SelectGroup, SelectLabel, SelectItem, SelectSeparator,
|
|
76
|
+
} from '@/components/ui/select';
|
|
77
|
+
|
|
78
|
+
// 基础
|
|
79
|
+
<Select>
|
|
80
|
+
<SelectTrigger className="w-48">
|
|
81
|
+
<SelectValue placeholder="选择城市" />
|
|
82
|
+
</SelectTrigger>
|
|
83
|
+
<SelectContent>
|
|
84
|
+
<SelectItem value="bj">北京</SelectItem>
|
|
85
|
+
<SelectItem value="sh">上海</SelectItem>
|
|
86
|
+
<SelectItem value="hz">杭州</SelectItem>
|
|
87
|
+
</SelectContent>
|
|
88
|
+
</Select>
|
|
89
|
+
|
|
90
|
+
// 分组
|
|
91
|
+
<Select>
|
|
92
|
+
<SelectTrigger className="w-48"><SelectValue placeholder="选择时区" /></SelectTrigger>
|
|
93
|
+
<SelectContent>
|
|
94
|
+
<SelectGroup>
|
|
95
|
+
<SelectLabel>亚洲</SelectLabel>
|
|
96
|
+
<SelectItem value="cn">北京时间 (UTC+8)</SelectItem>
|
|
97
|
+
<SelectItem value="jp">东京时间 (UTC+9)</SelectItem>
|
|
98
|
+
</SelectGroup>
|
|
99
|
+
<SelectSeparator />
|
|
100
|
+
<SelectGroup>
|
|
101
|
+
<SelectLabel>欧洲</SelectLabel>
|
|
102
|
+
<SelectItem value="uk">伦敦时间 (UTC+0)</SelectItem>
|
|
103
|
+
</SelectGroup>
|
|
104
|
+
</SelectContent>
|
|
105
|
+
</Select>
|
|
106
|
+
|
|
107
|
+
// 受控
|
|
108
|
+
const [v, setV] = React.useState('bj');
|
|
109
|
+
<Select value={v} onValueChange={setV}>...</Select>
|
|
110
|
+
```
|