@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,46 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import { Plus } from 'lucide-react';
|
|
3
|
+
import { Empty } from './empty';
|
|
4
|
+
import { Button } from '@/components/button/button';
|
|
5
|
+
|
|
6
|
+
const meta: Meta<typeof Empty> = {
|
|
7
|
+
title: '反馈与浮层 · Feedback/Empty',
|
|
8
|
+
component: Empty,
|
|
9
|
+
tags: ['autodocs'],
|
|
10
|
+
parameters: {
|
|
11
|
+
docs: {
|
|
12
|
+
description: {
|
|
13
|
+
component:
|
|
14
|
+
'空状态 — 列表 / 表格 / 搜索结果为空时的占位提示。提供 description 引导文案 + extra 主操作,鼓励用户创建首条数据。OpenTrek tokens 适配,等价 antd Empty。',
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
argTypes: {
|
|
19
|
+
size: { control: 'inline-radio', options: ['sm', 'default'] },
|
|
20
|
+
},
|
|
21
|
+
args: { size: 'default' },
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export default meta;
|
|
25
|
+
type Story = StoryObj<typeof Empty>;
|
|
26
|
+
|
|
27
|
+
export const Default: Story = {};
|
|
28
|
+
|
|
29
|
+
export const WithAction: Story = {
|
|
30
|
+
parameters: { controls: { disable: true } },
|
|
31
|
+
render: () => (
|
|
32
|
+
<Empty
|
|
33
|
+
description="还没有项目"
|
|
34
|
+
extra={<Button icon={<Plus />}>创建项目</Button>}
|
|
35
|
+
/>
|
|
36
|
+
),
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export const Small: Story = {
|
|
40
|
+
parameters: { controls: { disable: true } },
|
|
41
|
+
render: () => (
|
|
42
|
+
<div className="rounded-md border">
|
|
43
|
+
<Empty size="sm" description="无匹配的搜索结果" />
|
|
44
|
+
</div>
|
|
45
|
+
),
|
|
46
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { Inbox } from 'lucide-react';
|
|
3
|
+
|
|
4
|
+
import { cn } from '@/utils/cn';
|
|
5
|
+
|
|
6
|
+
export interface EmptyProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
7
|
+
/** 自定义图标(覆盖默认 Inbox)。 */
|
|
8
|
+
image?: React.ReactNode;
|
|
9
|
+
/** 描述文本。 @default "暂无数据" */
|
|
10
|
+
description?: React.ReactNode;
|
|
11
|
+
/** 操作区(放 Button 引导用户创建首条数据)。 */
|
|
12
|
+
extra?: React.ReactNode;
|
|
13
|
+
/**
|
|
14
|
+
* 尺寸 — `sm` 用于卡片内 / 表格空态;`default` 用于整区。
|
|
15
|
+
* @default "default"
|
|
16
|
+
*/
|
|
17
|
+
size?: 'sm' | 'default';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const Empty = React.forwardRef<HTMLDivElement, EmptyProps>(
|
|
21
|
+
(
|
|
22
|
+
{ image, description = '暂无数据', extra, size = 'default', className, ...props },
|
|
23
|
+
ref,
|
|
24
|
+
) => {
|
|
25
|
+
const isSm = size === 'sm';
|
|
26
|
+
return (
|
|
27
|
+
<div
|
|
28
|
+
ref={ref}
|
|
29
|
+
className={cn(
|
|
30
|
+
'flex flex-col items-center justify-center gap-3 text-muted-foreground',
|
|
31
|
+
isSm ? 'py-8' : 'py-16',
|
|
32
|
+
className,
|
|
33
|
+
)}
|
|
34
|
+
{...props}
|
|
35
|
+
>
|
|
36
|
+
<div className={cn('opacity-40', isSm ? 'text-3xl' : 'text-5xl')}>
|
|
37
|
+
{image ?? <Inbox className={isSm ? 'size-8' : 'size-16'} />}
|
|
38
|
+
</div>
|
|
39
|
+
<div className={cn(isSm ? 'text-xs' : 'text-sm')}>{description}</div>
|
|
40
|
+
{extra ? <div className="mt-1">{extra}</div> : null}
|
|
41
|
+
</div>
|
|
42
|
+
);
|
|
43
|
+
},
|
|
44
|
+
);
|
|
45
|
+
Empty.displayName = 'Empty';
|
|
46
|
+
|
|
47
|
+
export { Empty };
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: field
|
|
3
|
+
name: Field
|
|
4
|
+
type: component
|
|
5
|
+
category: form
|
|
6
|
+
since: 0.1.0
|
|
7
|
+
package: "@teamix-evo/ui"
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Field
|
|
11
|
+
|
|
12
|
+
通用表单字段抽象 — shadcn 2025-10 新增。**比 `Form` 更通用**:Form 强绑定 RHF + zod;Field 是纯 UI 抽象,跟任何状态管理(Server Actions / RHF / TanStack Form / 原生 form)都能搭。提供 7 个语义槽:`Field` 容器 / `FieldLabel` 标签 / `FieldDescription` 帮助文案 / `FieldError` 错误提示 / `FieldGroup` 多字段纵向容器 / `FieldSet` 带边框分组 / `FieldLegend` 分组标题。
|
|
13
|
+
|
|
14
|
+
## When to use
|
|
15
|
+
|
|
16
|
+
- Server Actions / 原生 form / 任何非 RHF 流派的表单字段封装
|
|
17
|
+
- 与现有 `Form`(RHF)互补:Form 管校验与状态,**控件内部**仍可用 Field 表达视觉结构
|
|
18
|
+
- 大型表单分组(FieldSet + FieldLegend 表达"账号信息" / "联系方式"等小节)
|
|
19
|
+
|
|
20
|
+
## When NOT to use
|
|
21
|
+
|
|
22
|
+
- 已用 `Form`(RHF)并接受其约定 → 优先 `FormItem / FormLabel / FormMessage`(更紧)
|
|
23
|
+
- 控件本身没有 label / description → 直接用 `Input` / `Select` 自由布局
|
|
24
|
+
|
|
25
|
+
<!-- auto:props:begin -->
|
|
26
|
+
| 名称 | 类型 | 默认值 | 必填 | 说明 |
|
|
27
|
+
| --- | --- | --- | --- | --- |
|
|
28
|
+
| `orientation` | `'vertical' \| 'horizontal'` | `"vertical"` | – | 字段方向 — `vertical`(默认)Label 在控件上;`horizontal` Label 在控件左侧(配长表单)。 |
|
|
29
|
+
| `invalid` | `boolean` | `false` | – | 标记字段为 invalid — 子组件的 ring / 文字色会跟随;同时把内部 `<FieldError>` 渲染为可见。 |
|
|
30
|
+
<!-- auto:props:end -->
|
|
31
|
+
|
|
32
|
+
<!-- auto:deps:begin -->
|
|
33
|
+
### 同库依赖
|
|
34
|
+
|
|
35
|
+
> `teamix-evo ui add field` 时,以下 entry 会被自动连带安装(无需手动 add)。
|
|
36
|
+
|
|
37
|
+
| Entry | 类型 | 描述 |
|
|
38
|
+
| --- | --- | --- |
|
|
39
|
+
| `cn` | util | Tailwind className 合并工具(clsx + tailwind-merge) |
|
|
40
|
+
| `label` | component | 表单字段标签 — Radix Label 包装,补 antd Form.Item 风格的 required / disabled 显式视觉 |
|
|
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
|
+
- **`invalid` 是单一真值**:置于 `<Field>`,内部 `FieldLabel` 会变红、`FieldError` 才可见;**不要**同时给控件再传 `aria-invalid`,重复
|
|
54
|
+
- **`FieldError` children 为空时不渲染** — 直接传可空字符串 / `errors?.email` 即可,无需条件渲染
|
|
55
|
+
- **`FieldLabel required`** 仅视觉(红 *);**业务层必填**仍要在 schema / submit handler 校验
|
|
56
|
+
- **`FieldSet disabled`** 利用原生 fieldset 的级联禁用 — 内部所有原生 input 都会失能;**不要**手动给每个控件传 disabled
|
|
57
|
+
- **不要嵌套 `Field` in `Field`** — 那是 FieldGroup 的活
|
|
58
|
+
- **与 `Form` 共存**:在 RHF 流程外的小表单(登录、邮件订阅)直接用 Field;复杂业务表单仍走 Form
|
|
59
|
+
|
|
60
|
+
## Examples
|
|
61
|
+
|
|
62
|
+
```tsx
|
|
63
|
+
import {
|
|
64
|
+
Field,
|
|
65
|
+
FieldLabel,
|
|
66
|
+
FieldDescription,
|
|
67
|
+
FieldError,
|
|
68
|
+
FieldGroup,
|
|
69
|
+
FieldSet,
|
|
70
|
+
FieldLegend,
|
|
71
|
+
} from '@/components/ui/field';
|
|
72
|
+
import { Input } from '@/components/ui/input';
|
|
73
|
+
import { Switch } from '@/components/ui/switch';
|
|
74
|
+
|
|
75
|
+
// 单字段
|
|
76
|
+
<Field invalid={Boolean(errors.email)}>
|
|
77
|
+
<FieldLabel htmlFor="email" required>邮箱</FieldLabel>
|
|
78
|
+
<Input id="email" type="email" name="email" />
|
|
79
|
+
<FieldDescription>用于接收订单通知</FieldDescription>
|
|
80
|
+
<FieldError>{errors.email}</FieldError>
|
|
81
|
+
</Field>
|
|
82
|
+
|
|
83
|
+
// horizontal(长表单)
|
|
84
|
+
<Field orientation="horizontal">
|
|
85
|
+
<FieldLabel htmlFor="notify" className="min-w-32">
|
|
86
|
+
邮件通知
|
|
87
|
+
</FieldLabel>
|
|
88
|
+
<Switch id="notify" defaultChecked />
|
|
89
|
+
</Field>
|
|
90
|
+
|
|
91
|
+
// 分组
|
|
92
|
+
<FieldSet>
|
|
93
|
+
<FieldLegend>账号信息</FieldLegend>
|
|
94
|
+
<FieldGroup>
|
|
95
|
+
<Field>
|
|
96
|
+
<FieldLabel htmlFor="name">姓名</FieldLabel>
|
|
97
|
+
<Input id="name" name="name" />
|
|
98
|
+
</Field>
|
|
99
|
+
<Field>
|
|
100
|
+
<FieldLabel htmlFor="email">邮箱</FieldLabel>
|
|
101
|
+
<Input id="email" name="email" type="email" />
|
|
102
|
+
</Field>
|
|
103
|
+
</FieldGroup>
|
|
104
|
+
</FieldSet>
|
|
105
|
+
|
|
106
|
+
// 配 Server Actions
|
|
107
|
+
<form action={createUser}>
|
|
108
|
+
<FieldGroup>
|
|
109
|
+
<Field>
|
|
110
|
+
<FieldLabel htmlFor="name" required>姓名</FieldLabel>
|
|
111
|
+
<Input id="name" name="name" required />
|
|
112
|
+
</Field>
|
|
113
|
+
</FieldGroup>
|
|
114
|
+
<button type="submit">提交</button>
|
|
115
|
+
</form>
|
|
116
|
+
```
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import {
|
|
3
|
+
Field,
|
|
4
|
+
FieldLabel,
|
|
5
|
+
FieldDescription,
|
|
6
|
+
FieldError,
|
|
7
|
+
FieldGroup,
|
|
8
|
+
FieldSet,
|
|
9
|
+
FieldLegend,
|
|
10
|
+
} from './field';
|
|
11
|
+
import { Input } from '@/components/input/input';
|
|
12
|
+
import { Switch } from '@/components/switch/switch';
|
|
13
|
+
import { Button } from '@/components/button/button';
|
|
14
|
+
|
|
15
|
+
const meta: Meta<typeof Field> = {
|
|
16
|
+
title: '表单与输入 · Form/Field',
|
|
17
|
+
component: Field,
|
|
18
|
+
tags: ['autodocs'],
|
|
19
|
+
parameters: {
|
|
20
|
+
docs: {
|
|
21
|
+
description: {
|
|
22
|
+
component:
|
|
23
|
+
'通用表单字段抽象 — 比 Form 更通用(Form 强绑 RHF;Field 跟任何状态管理都能搭)。7 个语义槽:Field / FieldLabel / FieldDescription / FieldError / FieldGroup / FieldSet / FieldLegend。shadcn 2025-10 新增,与现有 Form 共存。视觉走 OpenTrek tokens,所有样式来自 `@teamix-evo/design`,无 mock。',
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
argTypes: {
|
|
28
|
+
orientation: { control: 'inline-radio', options: ['vertical', 'horizontal'] },
|
|
29
|
+
invalid: { control: 'boolean' },
|
|
30
|
+
},
|
|
31
|
+
args: { orientation: 'vertical', invalid: false },
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export default meta;
|
|
35
|
+
type Story = StoryObj<typeof Field>;
|
|
36
|
+
|
|
37
|
+
export const Playground: Story = {
|
|
38
|
+
render: (args) => (
|
|
39
|
+
<Field {...args} className="w-80">
|
|
40
|
+
<FieldLabel htmlFor="email-pg" required>
|
|
41
|
+
邮箱
|
|
42
|
+
</FieldLabel>
|
|
43
|
+
<Input id="email-pg" type="email" placeholder="you@example.com" />
|
|
44
|
+
<FieldDescription>用于接收订单通知</FieldDescription>
|
|
45
|
+
<FieldError>邮箱格式不正确</FieldError>
|
|
46
|
+
</Field>
|
|
47
|
+
),
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export const Horizontal: Story = {
|
|
51
|
+
parameters: { controls: { disable: true } },
|
|
52
|
+
render: () => (
|
|
53
|
+
<Field orientation="horizontal" className="w-96">
|
|
54
|
+
<FieldLabel htmlFor="notify-h" className="min-w-28 pt-2">
|
|
55
|
+
邮件通知
|
|
56
|
+
</FieldLabel>
|
|
57
|
+
<div className="flex-1">
|
|
58
|
+
<Switch id="notify-h" defaultChecked />
|
|
59
|
+
<FieldDescription className="mt-1">
|
|
60
|
+
有重要事件时给您发邮件
|
|
61
|
+
</FieldDescription>
|
|
62
|
+
</div>
|
|
63
|
+
</Field>
|
|
64
|
+
),
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export const Grouped: Story = {
|
|
68
|
+
parameters: { controls: { disable: true } },
|
|
69
|
+
render: () => (
|
|
70
|
+
<FieldSet className="w-96">
|
|
71
|
+
<FieldLegend>账号信息</FieldLegend>
|
|
72
|
+
<FieldGroup>
|
|
73
|
+
<Field>
|
|
74
|
+
<FieldLabel htmlFor="name-g">姓名</FieldLabel>
|
|
75
|
+
<Input id="name-g" />
|
|
76
|
+
</Field>
|
|
77
|
+
<Field>
|
|
78
|
+
<FieldLabel htmlFor="email-g" required>
|
|
79
|
+
邮箱
|
|
80
|
+
</FieldLabel>
|
|
81
|
+
<Input id="email-g" type="email" />
|
|
82
|
+
</Field>
|
|
83
|
+
</FieldGroup>
|
|
84
|
+
</FieldSet>
|
|
85
|
+
),
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export const ServerActionForm: Story = {
|
|
89
|
+
parameters: { controls: { disable: true } },
|
|
90
|
+
render: () => (
|
|
91
|
+
<form
|
|
92
|
+
onSubmit={(e) => {
|
|
93
|
+
e.preventDefault();
|
|
94
|
+
const fd = new FormData(e.currentTarget);
|
|
95
|
+
// eslint-disable-next-line no-alert
|
|
96
|
+
alert(`提交: ${JSON.stringify(Object.fromEntries(fd.entries()))}`);
|
|
97
|
+
}}
|
|
98
|
+
className="w-96 space-y-4"
|
|
99
|
+
>
|
|
100
|
+
<FieldGroup>
|
|
101
|
+
<Field>
|
|
102
|
+
<FieldLabel htmlFor="name-srv" required>
|
|
103
|
+
姓名
|
|
104
|
+
</FieldLabel>
|
|
105
|
+
<Input id="name-srv" name="name" required />
|
|
106
|
+
</Field>
|
|
107
|
+
<Field>
|
|
108
|
+
<FieldLabel htmlFor="email-srv" required>
|
|
109
|
+
邮箱
|
|
110
|
+
</FieldLabel>
|
|
111
|
+
<Input id="email-srv" name="email" type="email" required />
|
|
112
|
+
</Field>
|
|
113
|
+
</FieldGroup>
|
|
114
|
+
<Button type="submit">提交</Button>
|
|
115
|
+
</form>
|
|
116
|
+
),
|
|
117
|
+
};
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { cva, type VariantProps } from 'class-variance-authority';
|
|
3
|
+
|
|
4
|
+
import { cn } from '@/utils/cn';
|
|
5
|
+
import { Label } from '@/components/label/label';
|
|
6
|
+
|
|
7
|
+
// ─── FieldSet / FieldLegend(分组的语义边框)────────────────────────────
|
|
8
|
+
|
|
9
|
+
const FieldSet = React.forwardRef<
|
|
10
|
+
HTMLFieldSetElement,
|
|
11
|
+
React.FieldsetHTMLAttributes<HTMLFieldSetElement>
|
|
12
|
+
>(({ className, ...props }, ref) => (
|
|
13
|
+
<fieldset
|
|
14
|
+
ref={ref}
|
|
15
|
+
className={cn(
|
|
16
|
+
'flex flex-col gap-4 rounded-md border bg-card p-4 disabled:cursor-not-allowed disabled:opacity-60',
|
|
17
|
+
className,
|
|
18
|
+
)}
|
|
19
|
+
{...props}
|
|
20
|
+
/>
|
|
21
|
+
));
|
|
22
|
+
FieldSet.displayName = 'FieldSet';
|
|
23
|
+
|
|
24
|
+
const FieldLegend = React.forwardRef<
|
|
25
|
+
HTMLLegendElement,
|
|
26
|
+
React.HTMLAttributes<HTMLLegendElement>
|
|
27
|
+
>(({ className, ...props }, ref) => (
|
|
28
|
+
<legend
|
|
29
|
+
ref={ref}
|
|
30
|
+
className={cn('px-1 text-sm font-semibold leading-none', className)}
|
|
31
|
+
{...props}
|
|
32
|
+
/>
|
|
33
|
+
));
|
|
34
|
+
FieldLegend.displayName = 'FieldLegend';
|
|
35
|
+
|
|
36
|
+
// ─── FieldGroup(纵向排列多个 Field 的容器)──────────────────────────────
|
|
37
|
+
|
|
38
|
+
const FieldGroup = React.forwardRef<
|
|
39
|
+
HTMLDivElement,
|
|
40
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
41
|
+
>(({ className, ...props }, ref) => (
|
|
42
|
+
<div ref={ref} className={cn('flex flex-col gap-4', className)} {...props} />
|
|
43
|
+
));
|
|
44
|
+
FieldGroup.displayName = 'FieldGroup';
|
|
45
|
+
|
|
46
|
+
// ─── Field(单个字段容器:Label + Control + Description + Error)────────
|
|
47
|
+
|
|
48
|
+
const fieldVariants = cva('flex', {
|
|
49
|
+
variants: {
|
|
50
|
+
orientation: {
|
|
51
|
+
vertical: 'flex-col gap-1.5',
|
|
52
|
+
horizontal: 'flex-row items-start gap-3',
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
defaultVariants: { orientation: 'vertical' },
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
export interface FieldProps
|
|
59
|
+
extends React.HTMLAttributes<HTMLDivElement>,
|
|
60
|
+
VariantProps<typeof fieldVariants> {
|
|
61
|
+
/**
|
|
62
|
+
* 字段方向 — `vertical`(默认)Label 在控件上;`horizontal` Label 在控件左侧(配长表单)。
|
|
63
|
+
* @default "vertical"
|
|
64
|
+
*/
|
|
65
|
+
orientation?: 'vertical' | 'horizontal';
|
|
66
|
+
/**
|
|
67
|
+
* 标记字段为 invalid — 子组件的 ring / 文字色会跟随;同时把内部 `<FieldError>` 渲染为可见。
|
|
68
|
+
* @default false
|
|
69
|
+
*/
|
|
70
|
+
invalid?: boolean;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const FieldContext = React.createContext<{ invalid: boolean }>({ invalid: false });
|
|
74
|
+
|
|
75
|
+
const Field = React.forwardRef<HTMLDivElement, FieldProps>(
|
|
76
|
+
({ orientation, invalid = false, className, ...props }, ref) => (
|
|
77
|
+
<FieldContext.Provider value={{ invalid }}>
|
|
78
|
+
<div
|
|
79
|
+
ref={ref}
|
|
80
|
+
data-invalid={invalid ? '' : undefined}
|
|
81
|
+
className={cn(fieldVariants({ orientation }), className)}
|
|
82
|
+
{...props}
|
|
83
|
+
/>
|
|
84
|
+
</FieldContext.Provider>
|
|
85
|
+
),
|
|
86
|
+
);
|
|
87
|
+
Field.displayName = 'Field';
|
|
88
|
+
|
|
89
|
+
// ─── FieldLabel(label,继承 invalid 着色)──────────────────────────────
|
|
90
|
+
|
|
91
|
+
export interface FieldLabelProps
|
|
92
|
+
extends React.ComponentPropsWithoutRef<typeof Label> {
|
|
93
|
+
/**
|
|
94
|
+
* 在 label 末尾追加 "*" 强调必填(antd Form `required` 并集) — 仅视觉,业务侧需在校验层确保必填。
|
|
95
|
+
* @default false
|
|
96
|
+
*/
|
|
97
|
+
required?: boolean;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const FieldLabel = React.forwardRef<
|
|
101
|
+
React.ElementRef<typeof Label>,
|
|
102
|
+
FieldLabelProps
|
|
103
|
+
>(({ required = false, className, children, ...props }, ref) => {
|
|
104
|
+
const { invalid } = React.useContext(FieldContext);
|
|
105
|
+
return (
|
|
106
|
+
<Label
|
|
107
|
+
ref={ref}
|
|
108
|
+
className={cn(invalid && 'text-destructive', className)}
|
|
109
|
+
{...props}
|
|
110
|
+
>
|
|
111
|
+
{children}
|
|
112
|
+
{required ? (
|
|
113
|
+
<span aria-hidden="true" className="ml-0.5 text-destructive">
|
|
114
|
+
*
|
|
115
|
+
</span>
|
|
116
|
+
) : null}
|
|
117
|
+
</Label>
|
|
118
|
+
);
|
|
119
|
+
});
|
|
120
|
+
FieldLabel.displayName = 'FieldLabel';
|
|
121
|
+
|
|
122
|
+
// ─── FieldDescription / FieldError ────────────────────────────────────
|
|
123
|
+
|
|
124
|
+
const FieldDescription = React.forwardRef<
|
|
125
|
+
HTMLParagraphElement,
|
|
126
|
+
React.HTMLAttributes<HTMLParagraphElement>
|
|
127
|
+
>(({ className, ...props }, ref) => (
|
|
128
|
+
<p
|
|
129
|
+
ref={ref}
|
|
130
|
+
className={cn('text-xs text-muted-foreground', className)}
|
|
131
|
+
{...props}
|
|
132
|
+
/>
|
|
133
|
+
));
|
|
134
|
+
FieldDescription.displayName = 'FieldDescription';
|
|
135
|
+
|
|
136
|
+
const FieldError = React.forwardRef<
|
|
137
|
+
HTMLParagraphElement,
|
|
138
|
+
React.HTMLAttributes<HTMLParagraphElement>
|
|
139
|
+
>(({ className, children, ...props }, ref) => {
|
|
140
|
+
const { invalid } = React.useContext(FieldContext);
|
|
141
|
+
if (!invalid || !children) return null;
|
|
142
|
+
return (
|
|
143
|
+
<p
|
|
144
|
+
ref={ref}
|
|
145
|
+
role="alert"
|
|
146
|
+
className={cn('text-xs font-medium text-destructive', className)}
|
|
147
|
+
{...props}
|
|
148
|
+
>
|
|
149
|
+
{children}
|
|
150
|
+
</p>
|
|
151
|
+
);
|
|
152
|
+
});
|
|
153
|
+
FieldError.displayName = 'FieldError';
|
|
154
|
+
|
|
155
|
+
export {
|
|
156
|
+
Field,
|
|
157
|
+
FieldLabel,
|
|
158
|
+
FieldDescription,
|
|
159
|
+
FieldError,
|
|
160
|
+
FieldGroup,
|
|
161
|
+
FieldSet,
|
|
162
|
+
FieldLegend,
|
|
163
|
+
fieldVariants,
|
|
164
|
+
};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: flex
|
|
3
|
+
name: Flex
|
|
4
|
+
type: component
|
|
5
|
+
category: layout
|
|
6
|
+
since: 0.1.0
|
|
7
|
+
package: "@teamix-evo/ui"
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Flex
|
|
11
|
+
|
|
12
|
+
Flex 布局容器 — antd 独有补足。**等价 antd `Flex`**(v5.10+),把 Tailwind flex 的常用配置(对齐 / 间距 / 换行 / 方向 / 渲染元素)收敛为枚举,避免散落的 `flex flex-col items-start gap-4 ...` 反复手写。
|
|
13
|
+
|
|
14
|
+
## When to use
|
|
15
|
+
|
|
16
|
+
- 完整页面 / 卡片 / Section 的容器布局
|
|
17
|
+
- 需要语义化 HTML 标签(`<header>` `<aside>` `<main>` `<nav>`)时用 `as`
|
|
18
|
+
- 多次重复的相同对齐组合(可以靠 Flex 收敛模板)
|
|
19
|
+
|
|
20
|
+
## When NOT to use
|
|
21
|
+
|
|
22
|
+
- 小集合 inline 间距 → `Space`
|
|
23
|
+
- 网格 → `Grid`(`Row + Col`)
|
|
24
|
+
- 仅一次性自由布局 → 直接写 className 也行,不强求
|
|
25
|
+
|
|
26
|
+
<!-- auto:props:begin -->
|
|
27
|
+
| 名称 | 类型 | 默认值 | 必填 | 说明 |
|
|
28
|
+
| --- | --- | --- | --- | --- |
|
|
29
|
+
| `direction` | `'row' \| 'column' \| 'row-reverse' \| 'column-reverse'` | `"row"` | – | 方向(antd `vertical` 并集) — `row`(默认)/ `column` 直观可读;antd `vertical` boolean 映射为 `column`。 |
|
|
30
|
+
| `gap` | `'none' \| 'xs' \| 'sm' \| 'default' \| 'lg' \| 'xl'` | `"default"` | – | 子项之间的间距档位(走 design 间距刻度,不接受任意 number)。 |
|
|
31
|
+
| `align` | `keyof typeof alignMap` | `"stretch"` | – | 副轴对齐方式。 |
|
|
32
|
+
| `justify` | `keyof typeof justifyMap` | `"start"` | – | 主轴对齐方式。 |
|
|
33
|
+
| `wrap` | `boolean` | `false` | – | 是否允许换行(antd `wrap` 并集)。 |
|
|
34
|
+
| `inline` | `boolean` | `false` | – | inline-flex 而非 block-flex。 |
|
|
35
|
+
| `as` | `keyof Pick< React.JSX.IntrinsicElements, 'div' \| 'section' \| 'header' \| 'footer' \| 'aside' \| 'main' \| 'nav' \| 'article' >` | `"div"` | – | 渲染元素(antd `component` 并集) — 支持 `section / header / aside / main / nav` 等语义标签。 |
|
|
36
|
+
<!-- auto:props:end -->
|
|
37
|
+
|
|
38
|
+
<!-- auto:deps:begin -->
|
|
39
|
+
### 同库依赖
|
|
40
|
+
|
|
41
|
+
> `teamix-evo ui add flex` 时,以下 entry 会被自动连带安装(无需手动 add)。
|
|
42
|
+
|
|
43
|
+
| Entry | 类型 | 描述 |
|
|
44
|
+
| --- | --- | --- |
|
|
45
|
+
| `cn` | util | Tailwind className 合并工具(clsx + tailwind-merge) |
|
|
46
|
+
|
|
47
|
+
### npm 依赖
|
|
48
|
+
|
|
49
|
+
_无 — 本组件不依赖任何 npm 包。_
|
|
50
|
+
<!-- auto:deps:end -->
|
|
51
|
+
|
|
52
|
+
## AI 生成纪律
|
|
53
|
+
|
|
54
|
+
- **`gap` 是档位枚举**(none / xs / sm / default / lg / xl),**不接受 number** — 走 design 间距刻度
|
|
55
|
+
- **`direction="column"` 等价 antd `vertical`** — antd 的 `vertical={true}` 我们没沿用 boolean,直接走 direction 更直观
|
|
56
|
+
- **`as` 仅限语义化标签集**(`section / header / aside / main / nav / footer / article / div`)— 不要传 `'span' / 'h1'` 等非 block 标签
|
|
57
|
+
- **不要嵌套 Flex 表达栅格** — 用 `Grid (Row/Col)`
|
|
58
|
+
- **align/justify 默认值**:`align="stretch"`(子项填满高度)、`justify="start"`(主轴起始)— 大部分场景符合直觉
|
|
59
|
+
|
|
60
|
+
## Examples
|
|
61
|
+
|
|
62
|
+
```tsx
|
|
63
|
+
import { Flex } from '@/components/ui/flex';
|
|
64
|
+
|
|
65
|
+
// 居中对话框
|
|
66
|
+
<Flex justify="center" align="center" className="min-h-screen">
|
|
67
|
+
<Dialog />
|
|
68
|
+
</Flex>
|
|
69
|
+
|
|
70
|
+
// 页头:logo + 导航 + 头像
|
|
71
|
+
<Flex as="header" justify="between" align="center" className="border-b px-6 py-3">
|
|
72
|
+
<Logo />
|
|
73
|
+
<Nav />
|
|
74
|
+
<Avatar />
|
|
75
|
+
</Flex>
|
|
76
|
+
|
|
77
|
+
// 纵向卡片体
|
|
78
|
+
<Flex direction="column" gap="sm" className="p-6">
|
|
79
|
+
<Title>标题</Title>
|
|
80
|
+
<Paragraph>正文...</Paragraph>
|
|
81
|
+
<Button>操作</Button>
|
|
82
|
+
</Flex>
|
|
83
|
+
|
|
84
|
+
// 工具栏(右对齐)
|
|
85
|
+
<Flex justify="end" gap="sm">
|
|
86
|
+
<Button variant="outline">取消</Button>
|
|
87
|
+
<Button>提交</Button>
|
|
88
|
+
</Flex>
|
|
89
|
+
|
|
90
|
+
// 标签云
|
|
91
|
+
<Flex wrap gap="sm">
|
|
92
|
+
{tags.map((t) => <Tag key={t}>{t}</Tag>)}
|
|
93
|
+
</Flex>
|
|
94
|
+
```
|