@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,92 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: button-group
|
|
3
|
+
name: ButtonGroup
|
|
4
|
+
type: component
|
|
5
|
+
category: form
|
|
6
|
+
since: 0.1.0
|
|
7
|
+
package: "@teamix-evo/ui"
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# ButtonGroup
|
|
11
|
+
|
|
12
|
+
按钮组 — shadcn 2025-10 新增,**等价 antd `Space.Compact`**(v6 替代旧 `Button.Group`)。把多个按钮 / 输入框"粘"在一起共享边线,用于 Split Button、Toolbar、Input + Button 拼装等场景。配 `ButtonGroupText` 渲染只读 addon 文本。
|
|
13
|
+
|
|
14
|
+
## When to use
|
|
15
|
+
|
|
16
|
+
- Split Button(主操作 + 右侧下拉箭头)
|
|
17
|
+
- Toolbar(同语义按钮集合,如 "上一页 / 下一页 / 跳转")
|
|
18
|
+
- Input + 按钮拼装(搜索框、单位选择)
|
|
19
|
+
- 多档切换显示(配 `Toggle` 也可,但 ToggleGroup 更专精)
|
|
20
|
+
|
|
21
|
+
## When NOT to use
|
|
22
|
+
|
|
23
|
+
- 互斥单选切换 → `ToggleGroup`(语义更强)
|
|
24
|
+
- 不相干的按钮 → 普通 `Button` + flex gap
|
|
25
|
+
|
|
26
|
+
<!-- auto:props:begin -->
|
|
27
|
+
| 名称 | 类型 | 默认值 | 必填 | 说明 |
|
|
28
|
+
| --- | --- | --- | --- | --- |
|
|
29
|
+
| `orientation` | `'horizontal' \| 'vertical'` | `"horizontal"` | – | 排列方向。 |
|
|
30
|
+
| `attached` | `boolean` | `true` | – | 是否把按钮"粘"在一起去除边距(antd `Space.Compact` 等价行为)— `false` 时按钮间留 8px gap。 |
|
|
31
|
+
<!-- auto:props:end -->
|
|
32
|
+
|
|
33
|
+
<!-- auto:deps:begin -->
|
|
34
|
+
### 同库依赖
|
|
35
|
+
|
|
36
|
+
> `teamix-evo ui add button-group` 时,以下 entry 会被自动连带安装(无需手动 add)。
|
|
37
|
+
|
|
38
|
+
| Entry | 类型 | 描述 |
|
|
39
|
+
| --- | --- | --- |
|
|
40
|
+
| `cn` | util | Tailwind className 合并工具(clsx + tailwind-merge) |
|
|
41
|
+
|
|
42
|
+
### npm 依赖
|
|
43
|
+
|
|
44
|
+
> 业务侧需要先 `pnpm add` / `npm install` 这些包。CLI 在 `ui add` 完成后会列出此提示。
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
pnpm add class-variance-authority@^0.7.0
|
|
48
|
+
```
|
|
49
|
+
<!-- auto:deps:end -->
|
|
50
|
+
|
|
51
|
+
## AI 生成纪律
|
|
52
|
+
|
|
53
|
+
- **`attached=true`(默认)子项之间边线共享**:第一项 / 最后一项保留外侧圆角,中间项去除圆角;**不要**在内部再设 `rounded`
|
|
54
|
+
- **混用 Input + Button**:把 Input 直接放进 ButtonGroup,它的 `rounded-md` 也会被组件圈圆角(无需特殊处理)
|
|
55
|
+
- **`ButtonGroupText`** 用于只读 addon(`https://` 前缀、`/月` 后缀);**不要**把它当按钮点击
|
|
56
|
+
- **vertical 模式** 适合工具栏侧边小集合,horizontal 是默认推荐
|
|
57
|
+
- **不要**在 ButtonGroup 内放 spinner / icon-only 与文字按钮混排时,高度需要靠 button `size` 对齐 — 子项尺寸应一致
|
|
58
|
+
|
|
59
|
+
## Examples
|
|
60
|
+
|
|
61
|
+
```tsx
|
|
62
|
+
import { ButtonGroup, ButtonGroupText } from '@/components/ui/button-group';
|
|
63
|
+
import { Button } from '@/components/ui/button';
|
|
64
|
+
import { Input } from '@/components/ui/input';
|
|
65
|
+
import { ChevronDown } from 'lucide-react';
|
|
66
|
+
|
|
67
|
+
// Split button
|
|
68
|
+
<ButtonGroup>
|
|
69
|
+
<Button>保存</Button>
|
|
70
|
+
<Button variant="default" size="icon" icon={<ChevronDown />} aria-label="更多" />
|
|
71
|
+
</ButtonGroup>
|
|
72
|
+
|
|
73
|
+
// Toolbar
|
|
74
|
+
<ButtonGroup>
|
|
75
|
+
<Button variant="outline">上一页</Button>
|
|
76
|
+
<Button variant="outline">1 / 10</Button>
|
|
77
|
+
<Button variant="outline">下一页</Button>
|
|
78
|
+
</ButtonGroup>
|
|
79
|
+
|
|
80
|
+
// 带 addon 文本
|
|
81
|
+
<ButtonGroup>
|
|
82
|
+
<ButtonGroupText>https://</ButtonGroupText>
|
|
83
|
+
<Input defaultValue="example.com" className="rounded-l-none" />
|
|
84
|
+
<Button>访问</Button>
|
|
85
|
+
</ButtonGroup>
|
|
86
|
+
|
|
87
|
+
// 不粘连(留间距)
|
|
88
|
+
<ButtonGroup attached={false}>
|
|
89
|
+
<Button variant="outline">取消</Button>
|
|
90
|
+
<Button>确定</Button>
|
|
91
|
+
</ButtonGroup>
|
|
92
|
+
```
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import { ChevronDown } from 'lucide-react';
|
|
3
|
+
import { ButtonGroup, ButtonGroupText } from './button-group';
|
|
4
|
+
import { Button } from '@/components/button/button';
|
|
5
|
+
import { Input } from '@/components/input/input';
|
|
6
|
+
|
|
7
|
+
const meta: Meta<typeof ButtonGroup> = {
|
|
8
|
+
title: '表单与输入 · Form/ButtonGroup',
|
|
9
|
+
component: ButtonGroup,
|
|
10
|
+
tags: ['autodocs'],
|
|
11
|
+
parameters: {
|
|
12
|
+
docs: {
|
|
13
|
+
description: {
|
|
14
|
+
component:
|
|
15
|
+
'按钮组 — 把多个按钮 / 输入框粘在一起共享边线(Split Button、Toolbar、Input + Button 拼装)。配 ButtonGroupText 渲染只读 addon。shadcn 2025-10 新增,等价 antd `Space.Compact`(v6 替代旧 `Button.Group`)。视觉走 OpenTrek tokens,所有样式来自 `@teamix-evo/design`,无 mock。',
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
argTypes: {
|
|
20
|
+
orientation: { control: 'inline-radio', options: ['horizontal', 'vertical'] },
|
|
21
|
+
attached: { control: 'boolean' },
|
|
22
|
+
},
|
|
23
|
+
args: { orientation: 'horizontal', attached: true },
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export default meta;
|
|
27
|
+
type Story = StoryObj<typeof ButtonGroup>;
|
|
28
|
+
|
|
29
|
+
export const Playground: Story = {
|
|
30
|
+
render: (args) => (
|
|
31
|
+
<ButtonGroup {...args}>
|
|
32
|
+
<Button variant="outline">左</Button>
|
|
33
|
+
<Button variant="outline">中</Button>
|
|
34
|
+
<Button variant="outline">右</Button>
|
|
35
|
+
</ButtonGroup>
|
|
36
|
+
),
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export const SplitButton: Story = {
|
|
40
|
+
parameters: { controls: { disable: true } },
|
|
41
|
+
render: () => (
|
|
42
|
+
<ButtonGroup>
|
|
43
|
+
<Button>保存</Button>
|
|
44
|
+
<Button size="icon" icon={<ChevronDown />} aria-label="更多保存选项" />
|
|
45
|
+
</ButtonGroup>
|
|
46
|
+
),
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export const Pager: Story = {
|
|
50
|
+
parameters: { controls: { disable: true } },
|
|
51
|
+
render: () => (
|
|
52
|
+
<ButtonGroup>
|
|
53
|
+
<Button variant="outline">上一页</Button>
|
|
54
|
+
<Button variant="outline">1 / 10</Button>
|
|
55
|
+
<Button variant="outline">下一页</Button>
|
|
56
|
+
</ButtonGroup>
|
|
57
|
+
),
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export const InputAddon: Story = {
|
|
61
|
+
parameters: { controls: { disable: true } },
|
|
62
|
+
render: () => (
|
|
63
|
+
<ButtonGroup>
|
|
64
|
+
<ButtonGroupText>https://</ButtonGroupText>
|
|
65
|
+
<Input defaultValue="example.com" className="rounded-l-none" />
|
|
66
|
+
<Button>访问</Button>
|
|
67
|
+
</ButtonGroup>
|
|
68
|
+
),
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export const Vertical: Story = {
|
|
72
|
+
parameters: { controls: { disable: true } },
|
|
73
|
+
render: () => (
|
|
74
|
+
<ButtonGroup orientation="vertical">
|
|
75
|
+
<Button variant="outline">编辑</Button>
|
|
76
|
+
<Button variant="outline">复制</Button>
|
|
77
|
+
<Button variant="outline">删除</Button>
|
|
78
|
+
</ButtonGroup>
|
|
79
|
+
),
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export const NotAttached: Story = {
|
|
83
|
+
parameters: { controls: { disable: true } },
|
|
84
|
+
render: () => (
|
|
85
|
+
<ButtonGroup attached={false}>
|
|
86
|
+
<Button variant="outline">取消</Button>
|
|
87
|
+
<Button>确定</Button>
|
|
88
|
+
</ButtonGroup>
|
|
89
|
+
),
|
|
90
|
+
};
|
|
@@ -0,0 +1,75 @@
|
|
|
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 buttonGroupVariants = cva('inline-flex isolate', {
|
|
7
|
+
variants: {
|
|
8
|
+
orientation: {
|
|
9
|
+
horizontal:
|
|
10
|
+
'flex-row [&>*:not(:first-child)]:rounded-l-none [&>*:not(:last-child)]:rounded-r-none [&>*:not(:first-child)]:-ml-px hover:[&>*]:z-10 focus-visible:[&>*]:z-10',
|
|
11
|
+
vertical:
|
|
12
|
+
'flex-col [&>*:not(:first-child)]:rounded-t-none [&>*:not(:last-child)]:rounded-b-none [&>*:not(:first-child)]:-mt-px hover:[&>*]:z-10 focus-visible:[&>*]:z-10',
|
|
13
|
+
},
|
|
14
|
+
attached: {
|
|
15
|
+
true: '',
|
|
16
|
+
false: 'gap-2',
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
defaultVariants: { orientation: 'horizontal', attached: true },
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
export interface ButtonGroupProps
|
|
23
|
+
extends React.HTMLAttributes<HTMLDivElement>,
|
|
24
|
+
VariantProps<typeof buttonGroupVariants> {
|
|
25
|
+
/**
|
|
26
|
+
* 排列方向。
|
|
27
|
+
* @default "horizontal"
|
|
28
|
+
*/
|
|
29
|
+
orientation?: 'horizontal' | 'vertical';
|
|
30
|
+
/**
|
|
31
|
+
* 是否把按钮"粘"在一起去除边距(antd `Space.Compact` 等价行为)— `false` 时按钮间留 8px gap。
|
|
32
|
+
* @default true
|
|
33
|
+
*/
|
|
34
|
+
attached?: boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* 按钮组容器 — shadcn 2025-10 新增。语义类似 antd `Space.Compact`(v6 替代旧 `Button.Group`):
|
|
39
|
+
* 把多个 `Button` / `Input` 包在一起共享边线,典型场景:
|
|
40
|
+
* - Split button(左主操作 + 右下拉箭头)
|
|
41
|
+
* - Toolbar(同语义按钮集合)
|
|
42
|
+
* - Input + 按钮拼装(`<Input/><Button>搜索</Button>`)
|
|
43
|
+
*
|
|
44
|
+
* 内部不限定子组件类型 — 任何带 `rounded-md` 的元素都会被组件圈圆角。
|
|
45
|
+
*/
|
|
46
|
+
const ButtonGroup = React.forwardRef<HTMLDivElement, ButtonGroupProps>(
|
|
47
|
+
({ orientation, attached, className, ...props }, ref) => (
|
|
48
|
+
<div
|
|
49
|
+
ref={ref}
|
|
50
|
+
role="group"
|
|
51
|
+
className={cn(buttonGroupVariants({ orientation, attached }), className)}
|
|
52
|
+
{...props}
|
|
53
|
+
/>
|
|
54
|
+
),
|
|
55
|
+
);
|
|
56
|
+
ButtonGroup.displayName = 'ButtonGroup';
|
|
57
|
+
|
|
58
|
+
// ─── ButtonGroupText(antd-style addon text)──────────────────────────────
|
|
59
|
+
|
|
60
|
+
const ButtonGroupText = React.forwardRef<
|
|
61
|
+
HTMLSpanElement,
|
|
62
|
+
React.HTMLAttributes<HTMLSpanElement>
|
|
63
|
+
>(({ className, ...props }, ref) => (
|
|
64
|
+
<span
|
|
65
|
+
ref={ref}
|
|
66
|
+
className={cn(
|
|
67
|
+
'inline-flex h-9 select-none items-center border border-input bg-muted px-3 text-sm text-muted-foreground',
|
|
68
|
+
className,
|
|
69
|
+
)}
|
|
70
|
+
{...props}
|
|
71
|
+
/>
|
|
72
|
+
));
|
|
73
|
+
ButtonGroupText.displayName = 'ButtonGroupText';
|
|
74
|
+
|
|
75
|
+
export { ButtonGroup, ButtonGroupText, buttonGroupVariants };
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: calendar
|
|
3
|
+
name: Calendar
|
|
4
|
+
type: component
|
|
5
|
+
category: form
|
|
6
|
+
since: 0.1.0
|
|
7
|
+
package: '@teamix-evo/ui'
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Calendar
|
|
11
|
+
|
|
12
|
+
日期选择面板 — 基于 [`react-day-picker`](https://react-day-picker.js.org/),与 design tokens 对齐。
|
|
13
|
+
**两种用法**:
|
|
14
|
+
|
|
15
|
+
1. **inline**(裸 Calendar):嵌入页面或 Card
|
|
16
|
+
2. **配 Popover** → `DatePicker`(单日)/ `DateRangePicker`(范围)
|
|
17
|
+
|
|
18
|
+
## When to use
|
|
19
|
+
|
|
20
|
+
- 日期 / 范围选择(配 `DatePicker`)
|
|
21
|
+
- 日历视图(展示日程 / 假期 / 数据热力)
|
|
22
|
+
- 月份导航器
|
|
23
|
+
|
|
24
|
+
## When NOT to use
|
|
25
|
+
|
|
26
|
+
- 时间选择 → 配 `Input type="time"` 或自定义 TimePicker
|
|
27
|
+
- 模糊时间("近 7 天") → `Select` + 预设范围
|
|
28
|
+
- 仅显示日期 → `<time>` 配 `date-fns`
|
|
29
|
+
|
|
30
|
+
## Props
|
|
31
|
+
|
|
32
|
+
> 以下表格由 `pnpm --filter @teamix-evo/ui gen:meta` 自动生成。
|
|
33
|
+
|
|
34
|
+
<!-- auto:props:begin -->
|
|
35
|
+
| 名称 | 类型 | 默认值 | 必填 | 说明 |
|
|
36
|
+
| --- | --- | --- | --- | --- |
|
|
37
|
+
| `className` | `string` | – | – | 容器 className。组件内部默认带 `p-3`,会与传入值自动 merge。 |
|
|
38
|
+
| `classNames` | `DayPickerProps['classNames']` | – | – | 覆盖底层 react-day-picker 的 classNames slots(深度自定义日格 / 表头 / 导航等)。 |
|
|
39
|
+
| `showOutsideDays` | `boolean` | `true` | – | 是否显示当月范围外的日期(灰色填充,占位补齐周表格)。 |
|
|
40
|
+
<!-- auto:props:end -->
|
|
41
|
+
|
|
42
|
+
## 依赖
|
|
43
|
+
|
|
44
|
+
> 以下表格由 `pnpm --filter @teamix-evo/ui gen:meta` 自动生成,数据源是 [`manifest.json`](../../../manifest.json)。**手工编辑 marker 之间的内容会在下次生成时被覆盖**。
|
|
45
|
+
|
|
46
|
+
<!-- auto:deps:begin -->
|
|
47
|
+
### 同库依赖
|
|
48
|
+
|
|
49
|
+
> `teamix-evo ui add calendar` 时,以下 entry 会被自动连带安装(无需手动 add)。
|
|
50
|
+
|
|
51
|
+
| Entry | 类型 | 描述 |
|
|
52
|
+
| --- | --- | --- |
|
|
53
|
+
| `cn` | util | Tailwind className 合并工具(clsx + tailwind-merge) |
|
|
54
|
+
| `button` | component | 通用按钮 — shadcn 实现 + antd 功能扩展(loading / icon / shape / block / dashed variant) |
|
|
55
|
+
|
|
56
|
+
### npm 依赖
|
|
57
|
+
|
|
58
|
+
> 业务侧需要先 `pnpm add` / `npm install` 这些包。CLI 在 `ui add` 完成后会列出此提示。
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
pnpm add react-day-picker@^9.0.0 lucide-react@^0.460.0
|
|
62
|
+
```
|
|
63
|
+
<!-- auto:deps:end -->
|
|
64
|
+
|
|
65
|
+
> 透传所有 react-day-picker `<DayPicker>` props(`mode` / `selected` / `onSelect` / `disabled` / `fromYear` / `toYear` / `numberOfMonths` 等)。完整 API:[react-day-picker.js.org](https://react-day-picker.js.org/)。
|
|
66
|
+
|
|
67
|
+
## AI 生成纪律
|
|
68
|
+
|
|
69
|
+
- **`mode` 必传**:`'single'` / `'multiple'` / `'range'` 三选一,API 不同(`selected` 类型不同)
|
|
70
|
+
- **disabled 日期**:用 `disabled={(date) => date < startOfDay(new Date())}` 而非自己过滤
|
|
71
|
+
- **`fromYear` / `toYear` 限制可选范围**:不设置时年份下拉无界
|
|
72
|
+
- **多日期范围**:`mode="range"` 时 `selected` 是 `{ from?: Date, to?: Date }`
|
|
73
|
+
- **无障碍自带**:react-day-picker 完整 ARIA + 键盘导航,不要自行覆盖
|
|
74
|
+
- **`classNames` 必须用 react-day-picker v9 的 slot key**:本组件依赖 `react-day-picker@^9`。v9 重命名了所有 slot,**v8 老 key 完全失效**(默认渲染会塌掉成 `<table>` 原生样式)。常见映射:
|
|
75
|
+
- `caption` → `month_caption`
|
|
76
|
+
- `nav_button` / `nav_button_previous` / `nav_button_next` → `button_previous` / `button_next`(`nav` 为容器)
|
|
77
|
+
- `table` → `month_grid`
|
|
78
|
+
- `head_row` / `head_cell` → `weekdays` / `weekday`
|
|
79
|
+
- `row` → `week`
|
|
80
|
+
- `cell` / `day` → `day`(td 外壳)/ `day_button`(真正的按钮)
|
|
81
|
+
- `day_selected` / `day_today` / `day_outside` / `day_disabled` / `day_hidden` / `day_range_*` → 去 `day_` 前缀:`selected` / `today` / `outside` / `disabled` / `hidden` / `range_*`
|
|
82
|
+
- **nav 是 `<Months>` 直接子元素,与 `<Month>` 平级**(默认 `navLayout`):若 `nav` 用 absolute 定位横跨容器顶部覆盖到 caption 区域,**必须给 nav 加 `z-10`**,否则源码顺序在后的 `month_caption` 会盖住翻月按钮导致点击失效;同时 `month_caption` 不要自行加 `relative`(会创建 stacking context 反而把 nav 压在下面)
|
|
83
|
+
- **状态类(selected/today/range\_\*)作用在 `day_button` 上,不是 `day` 外壳**:视觉态(背景色/圆角)需直接写到对应 slot;`day` 外壳负责 range 段连续背景,用 `[&:has([data-selected])]` / `[data-range-*]` 这种 v9 data attribute 选择器
|
|
84
|
+
|
|
85
|
+
## Examples
|
|
86
|
+
|
|
87
|
+
```tsx
|
|
88
|
+
import { Calendar } from '@/components/ui/calendar';
|
|
89
|
+
import * as React from 'react';
|
|
90
|
+
|
|
91
|
+
// 单日
|
|
92
|
+
const [date, setDate] = React.useState<Date | undefined>();
|
|
93
|
+
<Calendar mode="single" selected={date} onSelect={setDate} />;
|
|
94
|
+
|
|
95
|
+
// 多选
|
|
96
|
+
const [days, setDays] = React.useState<Date[]>([]);
|
|
97
|
+
<Calendar mode="multiple" selected={days} onSelect={setDays} />;
|
|
98
|
+
|
|
99
|
+
// 范围
|
|
100
|
+
const [range, setRange] = React.useState<{ from?: Date; to?: Date }>();
|
|
101
|
+
<Calendar
|
|
102
|
+
mode="range"
|
|
103
|
+
selected={range}
|
|
104
|
+
onSelect={setRange}
|
|
105
|
+
numberOfMonths={2}
|
|
106
|
+
/>;
|
|
107
|
+
|
|
108
|
+
// 限制范围 + 禁用过去
|
|
109
|
+
import { startOfDay } from 'date-fns';
|
|
110
|
+
<Calendar
|
|
111
|
+
mode="single"
|
|
112
|
+
selected={date}
|
|
113
|
+
onSelect={setDate}
|
|
114
|
+
disabled={(d) => d < startOfDay(new Date())}
|
|
115
|
+
fromYear={2020}
|
|
116
|
+
toYear={2030}
|
|
117
|
+
/>;
|
|
118
|
+
```
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
3
|
+
import type { DateRange } from 'react-day-picker';
|
|
4
|
+
import { Calendar } from './calendar';
|
|
5
|
+
|
|
6
|
+
const meta: Meta<typeof Calendar> = {
|
|
7
|
+
title: '表单与输入 · Form/Calendar',
|
|
8
|
+
component: Calendar,
|
|
9
|
+
tags: ['autodocs'],
|
|
10
|
+
parameters: {
|
|
11
|
+
layout: 'centered',
|
|
12
|
+
docs: {
|
|
13
|
+
description: {
|
|
14
|
+
component:
|
|
15
|
+
'日历 — 内联日期选择面板。基于 [`react-day-picker@9`](https://daypicker.dev/) + design tokens,支持 `mode="single" | "multiple" | "range"` 三种选择模式、年份 / 月份导航、`disabled` 函数式禁用、`numberOfMonths` 多月并排;通常配 `Popover` 包装为 `DatePicker`。视觉走 OpenTrek semantic tokens,所有样式来自 `@teamix-evo/design`,无 mock。',
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export default meta;
|
|
22
|
+
type Story = StoryObj<typeof Calendar>;
|
|
23
|
+
|
|
24
|
+
export const Single: Story = {
|
|
25
|
+
render: () => {
|
|
26
|
+
const [date, setDate] = React.useState<Date | undefined>(new Date());
|
|
27
|
+
return (
|
|
28
|
+
<Calendar
|
|
29
|
+
mode="single"
|
|
30
|
+
selected={date}
|
|
31
|
+
onSelect={setDate}
|
|
32
|
+
className="rounded-md border"
|
|
33
|
+
/>
|
|
34
|
+
);
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export const Multiple: Story = {
|
|
39
|
+
render: () => {
|
|
40
|
+
const [days, setDays] = React.useState<Date[] | undefined>([]);
|
|
41
|
+
return (
|
|
42
|
+
<Calendar
|
|
43
|
+
mode="multiple"
|
|
44
|
+
selected={days}
|
|
45
|
+
onSelect={setDays}
|
|
46
|
+
className="rounded-md border"
|
|
47
|
+
/>
|
|
48
|
+
);
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export const Range: Story = {
|
|
53
|
+
render: () => {
|
|
54
|
+
const [range, setRange] = React.useState<DateRange | undefined>({
|
|
55
|
+
from: new Date(),
|
|
56
|
+
to: new Date(Date.now() + 7 * 24 * 3600 * 1000),
|
|
57
|
+
});
|
|
58
|
+
return (
|
|
59
|
+
<Calendar
|
|
60
|
+
mode="range"
|
|
61
|
+
selected={range}
|
|
62
|
+
onSelect={setRange}
|
|
63
|
+
numberOfMonths={2}
|
|
64
|
+
className="rounded-md border"
|
|
65
|
+
/>
|
|
66
|
+
);
|
|
67
|
+
},
|
|
68
|
+
};
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import {
|
|
3
|
+
ChevronDown,
|
|
4
|
+
ChevronLeft,
|
|
5
|
+
ChevronRight,
|
|
6
|
+
ChevronUp,
|
|
7
|
+
} from 'lucide-react';
|
|
8
|
+
import { DayPicker, type DayPickerProps } from 'react-day-picker';
|
|
9
|
+
|
|
10
|
+
import { cn } from '@/utils/cn';
|
|
11
|
+
import { buttonVariants } from '@/components/button/button';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Calendar 自身的可定制属性(ui 层包装)。
|
|
15
|
+
* 底层完整 props 透传至 [`DayPicker`](https://daypicker.dev/),
|
|
16
|
+
* 包括 `mode` / `selected` / `onSelect` / `numberOfMonths` / `disabled` / `initialFocus` 等。
|
|
17
|
+
*/
|
|
18
|
+
export interface CalendarProps {
|
|
19
|
+
/**
|
|
20
|
+
* 容器 className。组件内部默认带 `p-3`,会与传入值自动 merge。
|
|
21
|
+
*/
|
|
22
|
+
className?: string;
|
|
23
|
+
/**
|
|
24
|
+
* 覆盖底层 react-day-picker 的 classNames slots(深度自定义日格 / 表头 / 导航等)。
|
|
25
|
+
*/
|
|
26
|
+
classNames?: DayPickerProps['classNames'];
|
|
27
|
+
/**
|
|
28
|
+
* 是否显示当月范围外的日期(灰色填充,占位补齐周表格)。
|
|
29
|
+
* @default true
|
|
30
|
+
*/
|
|
31
|
+
showOutsideDays?: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function Calendar({
|
|
35
|
+
className,
|
|
36
|
+
classNames,
|
|
37
|
+
showOutsideDays = true,
|
|
38
|
+
...props
|
|
39
|
+
}: CalendarProps & DayPickerProps) {
|
|
40
|
+
return (
|
|
41
|
+
<DayPicker
|
|
42
|
+
showOutsideDays={showOutsideDays}
|
|
43
|
+
className={cn('p-3', className)}
|
|
44
|
+
classNames={{
|
|
45
|
+
root: 'w-fit',
|
|
46
|
+
months: 'flex flex-col sm:flex-row gap-4 relative',
|
|
47
|
+
month: 'flex flex-col gap-4',
|
|
48
|
+
month_caption: 'flex justify-center pt-1 items-center h-7 w-full',
|
|
49
|
+
caption_label: 'text-sm font-medium',
|
|
50
|
+
nav: 'absolute inset-x-1 top-1 z-10 flex items-center justify-between',
|
|
51
|
+
button_previous: cn(
|
|
52
|
+
buttonVariants({ variant: 'outline', size: 'icon' }),
|
|
53
|
+
'size-7 bg-transparent p-0 opacity-50 hover:opacity-100',
|
|
54
|
+
),
|
|
55
|
+
button_next: cn(
|
|
56
|
+
buttonVariants({ variant: 'outline', size: 'icon' }),
|
|
57
|
+
'size-7 bg-transparent p-0 opacity-50 hover:opacity-100',
|
|
58
|
+
),
|
|
59
|
+
month_grid: 'w-full border-collapse',
|
|
60
|
+
weekdays: 'flex',
|
|
61
|
+
weekday:
|
|
62
|
+
'text-muted-foreground rounded-md w-8 font-normal text-[0.8rem]',
|
|
63
|
+
week: 'flex w-full mt-2',
|
|
64
|
+
day: cn(
|
|
65
|
+
'relative p-0 text-center text-sm focus-within:relative focus-within:z-20',
|
|
66
|
+
'[&:has([data-selected])]:bg-accent',
|
|
67
|
+
'[&:has([data-selected])]:first:rounded-l-md [&:has([data-selected])]:last:rounded-r-md',
|
|
68
|
+
'[&:has([data-range-start])]:rounded-l-md [&:has([data-range-end])]:rounded-r-md',
|
|
69
|
+
'[&:not(:has([data-range-middle]))]:[&:has([data-selected])]:rounded-md',
|
|
70
|
+
),
|
|
71
|
+
day_button: cn(
|
|
72
|
+
buttonVariants({ variant: 'ghost' }),
|
|
73
|
+
'size-8 p-0 font-normal aria-selected:opacity-100',
|
|
74
|
+
),
|
|
75
|
+
range_start: 'day-range-start',
|
|
76
|
+
range_end: 'day-range-end',
|
|
77
|
+
range_middle:
|
|
78
|
+
'aria-selected:bg-accent aria-selected:text-accent-foreground',
|
|
79
|
+
selected:
|
|
80
|
+
'bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground rounded-md',
|
|
81
|
+
today: 'bg-accent text-accent-foreground rounded-md',
|
|
82
|
+
outside:
|
|
83
|
+
'day-outside text-muted-foreground aria-selected:text-muted-foreground',
|
|
84
|
+
disabled: 'text-muted-foreground opacity-50',
|
|
85
|
+
hidden: 'invisible',
|
|
86
|
+
...classNames,
|
|
87
|
+
}}
|
|
88
|
+
components={{
|
|
89
|
+
Chevron: ({ orientation, className: chevronClassName, ...rest }) => {
|
|
90
|
+
const Icon =
|
|
91
|
+
orientation === 'left'
|
|
92
|
+
? ChevronLeft
|
|
93
|
+
: orientation === 'up'
|
|
94
|
+
? ChevronUp
|
|
95
|
+
: orientation === 'down'
|
|
96
|
+
? ChevronDown
|
|
97
|
+
: ChevronRight;
|
|
98
|
+
return <Icon {...rest} className={cn('size-4', chevronClassName)} />;
|
|
99
|
+
},
|
|
100
|
+
}}
|
|
101
|
+
{...props}
|
|
102
|
+
/>
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
Calendar.displayName = 'Calendar';
|
|
106
|
+
|
|
107
|
+
export { Calendar };
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: card
|
|
3
|
+
name: Card
|
|
4
|
+
type: component
|
|
5
|
+
category: layout
|
|
6
|
+
since: 0.1.0
|
|
7
|
+
package: "@teamix-evo/ui"
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Card
|
|
11
|
+
|
|
12
|
+
卡片容器 — shadcn 组合式(`Card / Header / Title / Description / Content / Footer`)+ antd 并集(`hoverable / loading / Cover / Actions / Meta / extra`)。
|
|
13
|
+
**单卡覆盖列表项 / 详情卡 / 资源卡 / 媒体卡多种形态**。
|
|
14
|
+
|
|
15
|
+
## When to use
|
|
16
|
+
|
|
17
|
+
- 列表项 / 网格卡片(产品 / 服务 / 用户)
|
|
18
|
+
- 详情页的分块容器
|
|
19
|
+
- 仪表板小部件(配 Header.extra 放刷新按钮)
|
|
20
|
+
- 媒体卡(`<CardCover>` 顶部图片)
|
|
21
|
+
|
|
22
|
+
## When NOT to use
|
|
23
|
+
|
|
24
|
+
- 整屏页面背景 → 用普通 div + spacing
|
|
25
|
+
- 简单文字提示 → `Alert` / `Badge`
|
|
26
|
+
- 浮层 → `Popover` / `HoverCard`
|
|
27
|
+
|
|
28
|
+
## Props
|
|
29
|
+
|
|
30
|
+
> 以下表格由 `pnpm --filter @teamix-evo/ui gen:meta` 自动生成。
|
|
31
|
+
|
|
32
|
+
<!-- auto:props:begin -->
|
|
33
|
+
| 名称 | 类型 | 默认值 | 必填 | 说明 |
|
|
34
|
+
| --- | --- | --- | --- | --- |
|
|
35
|
+
| `hoverable` | `boolean` | `false` | – | 鼠标 hover 时的视觉反馈(antd `hoverable` 并集) — 阴影升级 + 微交互。 |
|
|
36
|
+
| `loading` | `boolean` | `false` | – | 加载态(antd `loading` 并集) — 整卡渲染为骨架屏占位,不展示 children。 |
|
|
37
|
+
<!-- auto:props:end -->
|
|
38
|
+
|
|
39
|
+
## 依赖
|
|
40
|
+
|
|
41
|
+
> 以下表格由 `pnpm --filter @teamix-evo/ui gen:meta` 自动生成,数据源是 [`manifest.json`](../../../manifest.json)。**手工编辑 marker 之间的内容会在下次生成时被覆盖**。
|
|
42
|
+
|
|
43
|
+
<!-- auto:deps:begin -->
|
|
44
|
+
### 同库依赖
|
|
45
|
+
|
|
46
|
+
> `teamix-evo ui add card` 时,以下 entry 会被自动连带安装(无需手动 add)。
|
|
47
|
+
|
|
48
|
+
| Entry | 类型 | 描述 |
|
|
49
|
+
| --- | --- | --- |
|
|
50
|
+
| `cn` | util | Tailwind className 合并工具(clsx + tailwind-merge) |
|
|
51
|
+
| `skeleton` | component | 骨架屏 — shadcn pulse 单元素 + antd Avatar/Image/Button/Input/Paragraph 子家族,自动 motion-reduce |
|
|
52
|
+
|
|
53
|
+
### npm 依赖
|
|
54
|
+
|
|
55
|
+
_无 — 本组件不依赖任何 npm 包。_
|
|
56
|
+
<!-- auto:deps:end -->
|
|
57
|
+
|
|
58
|
+
> 子组件:`Card`(主容器)/ `CardHeader`(支持 `extra`)/ `CardTitle` / `CardDescription` / `CardContent` / `CardFooter` / `CardCover`(顶部封面)/ `CardActions`(底部分隔操作栏)/ `CardMeta`(avatar + title + description 行)。
|
|
59
|
+
|
|
60
|
+
## AI 生成纪律
|
|
61
|
+
|
|
62
|
+
- **`hoverable` 仅用于可点击卡**:配 `onClick` 或包 `<Link>`,不能 hover 起视觉但点了无反应
|
|
63
|
+
- **`loading` 整卡替换为骨架**:`children` 在 loading 时不渲染,**不要**手动嵌套 Skeleton
|
|
64
|
+
- **`extra` 放轻量按钮**:刷新 / 更多 / 切换;**不要**塞主操作(放 CardFooter 更醒目)
|
|
65
|
+
- **`<CardCover>` 顶部对齐**:必须是 Card 的第一个子元素(避免上方圆角穿帮)
|
|
66
|
+
- **`<CardActions items>` 数组用法**:item 可以是文本 / 图标按钮;数组长度不超过 4
|
|
67
|
+
|
|
68
|
+
## Examples
|
|
69
|
+
|
|
70
|
+
```tsx
|
|
71
|
+
import {
|
|
72
|
+
Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter,
|
|
73
|
+
CardCover, CardActions, CardMeta,
|
|
74
|
+
} from '@/components/ui/card';
|
|
75
|
+
import { Button } from '@/components/ui/button';
|
|
76
|
+
import { Heart, Share, MessageSquare, MoreHorizontal } from 'lucide-react';
|
|
77
|
+
|
|
78
|
+
// 基础
|
|
79
|
+
<Card>
|
|
80
|
+
<CardHeader extra={<Button size="icon" variant="ghost"><MoreHorizontal /></Button>}>
|
|
81
|
+
<CardTitle>项目名称</CardTitle>
|
|
82
|
+
<CardDescription>项目简介一行...</CardDescription>
|
|
83
|
+
</CardHeader>
|
|
84
|
+
<CardContent>主体内容</CardContent>
|
|
85
|
+
<CardFooter className="justify-end gap-2">
|
|
86
|
+
<Button variant="outline">取消</Button>
|
|
87
|
+
<Button>确认</Button>
|
|
88
|
+
</CardFooter>
|
|
89
|
+
</Card>
|
|
90
|
+
|
|
91
|
+
// hoverable
|
|
92
|
+
<Card hoverable onClick={() => navigate('/x')}>
|
|
93
|
+
<CardContent>...</CardContent>
|
|
94
|
+
</Card>
|
|
95
|
+
|
|
96
|
+
// loading
|
|
97
|
+
<Card loading />
|
|
98
|
+
|
|
99
|
+
// 媒体卡(antd 风格)
|
|
100
|
+
<Card hoverable>
|
|
101
|
+
<CardCover>
|
|
102
|
+
<img src="/cover.jpg" alt="" />
|
|
103
|
+
</CardCover>
|
|
104
|
+
<CardContent className="pt-4">
|
|
105
|
+
<CardMeta
|
|
106
|
+
avatar={<Avatar><AvatarImage src="..." /><AvatarFallback>A</AvatarFallback></Avatar>}
|
|
107
|
+
title="作者"
|
|
108
|
+
description="发布时间"
|
|
109
|
+
/>
|
|
110
|
+
</CardContent>
|
|
111
|
+
<CardActions items={[
|
|
112
|
+
<span><Heart className="inline size-4" /> 12</span>,
|
|
113
|
+
<span><Share className="inline size-4" /> 4</span>,
|
|
114
|
+
<span><MessageSquare className="inline size-4" /> 8</span>,
|
|
115
|
+
]} />
|
|
116
|
+
</Card>
|
|
117
|
+
```
|