@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,77 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
3
|
+
import { Tag } from './tag';
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof Tag> = {
|
|
6
|
+
title: '基础原语 · Foundation/Tag',
|
|
7
|
+
component: Tag,
|
|
8
|
+
tags: ['autodocs'],
|
|
9
|
+
parameters: {
|
|
10
|
+
docs: {
|
|
11
|
+
description: {
|
|
12
|
+
component:
|
|
13
|
+
'标签 — 用户可关闭的关键词 / 分类标签(分类、筛选条件、博客标签)。6 种语义色 + closable + onClose。OpenTrek tokens 适配,等价 antd Tag(与 Badge 互补,Badge 偏状态徽标)。',
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
argTypes: {
|
|
18
|
+
color: {
|
|
19
|
+
control: 'select',
|
|
20
|
+
options: ['default', 'primary', 'success', 'warning', 'error', 'info'],
|
|
21
|
+
},
|
|
22
|
+
closable: { control: 'boolean' },
|
|
23
|
+
},
|
|
24
|
+
args: { color: 'default', children: '标签', closable: false },
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export default meta;
|
|
28
|
+
type Story = StoryObj<typeof Tag>;
|
|
29
|
+
|
|
30
|
+
export const Playground: Story = {};
|
|
31
|
+
|
|
32
|
+
export const Colors: Story = {
|
|
33
|
+
parameters: { controls: { disable: true } },
|
|
34
|
+
render: () => (
|
|
35
|
+
<div className="flex flex-wrap gap-2">
|
|
36
|
+
<Tag>Default</Tag>
|
|
37
|
+
<Tag color="primary">Primary</Tag>
|
|
38
|
+
<Tag color="success">Success</Tag>
|
|
39
|
+
<Tag color="warning">Warning</Tag>
|
|
40
|
+
<Tag color="error">Error</Tag>
|
|
41
|
+
<Tag color="info">Info</Tag>
|
|
42
|
+
</div>
|
|
43
|
+
),
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export const Closable: Story = {
|
|
47
|
+
parameters: { controls: { disable: true } },
|
|
48
|
+
render: () => {
|
|
49
|
+
const [tags, setTags] = React.useState(['React', 'Vue', 'Angular', 'Svelte']);
|
|
50
|
+
return (
|
|
51
|
+
<div className="flex flex-wrap gap-2">
|
|
52
|
+
{tags.map((t) => (
|
|
53
|
+
<Tag
|
|
54
|
+
key={t}
|
|
55
|
+
color="primary"
|
|
56
|
+
closable
|
|
57
|
+
onClose={() => setTags(tags.filter((x) => x !== t))}
|
|
58
|
+
>
|
|
59
|
+
{t}
|
|
60
|
+
</Tag>
|
|
61
|
+
))}
|
|
62
|
+
</div>
|
|
63
|
+
);
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export const Filters: Story = {
|
|
68
|
+
parameters: { controls: { disable: true } },
|
|
69
|
+
render: () => (
|
|
70
|
+
<div className="flex flex-wrap gap-2">
|
|
71
|
+
<span className="text-sm text-muted-foreground">已选筛选:</span>
|
|
72
|
+
<Tag closable>状态: 运行中</Tag>
|
|
73
|
+
<Tag closable>类型: 服务</Tag>
|
|
74
|
+
<Tag closable>地区: 杭州</Tag>
|
|
75
|
+
</div>
|
|
76
|
+
),
|
|
77
|
+
};
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { cva, type VariantProps } from 'class-variance-authority';
|
|
3
|
+
import { X } from 'lucide-react';
|
|
4
|
+
|
|
5
|
+
import { cn } from '@/utils/cn';
|
|
6
|
+
|
|
7
|
+
const tagVariants = cva(
|
|
8
|
+
'inline-flex items-center gap-1 rounded-md border px-2 py-0.5 text-xs font-medium transition-colors',
|
|
9
|
+
{
|
|
10
|
+
variants: {
|
|
11
|
+
color: {
|
|
12
|
+
default: 'border-border bg-muted text-foreground',
|
|
13
|
+
primary: 'border-primary/30 bg-primary/10 text-primary',
|
|
14
|
+
success: 'border-emerald-200 bg-emerald-50 text-emerald-700',
|
|
15
|
+
warning: 'border-amber-200 bg-amber-50 text-amber-700',
|
|
16
|
+
error: 'border-destructive/30 bg-destructive/10 text-destructive',
|
|
17
|
+
info: 'border-blue-200 bg-blue-50 text-blue-700',
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
defaultVariants: { color: 'default' },
|
|
21
|
+
},
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
export interface TagProps
|
|
25
|
+
extends Omit<React.HTMLAttributes<HTMLSpanElement>, 'color'>,
|
|
26
|
+
VariantProps<typeof tagVariants> {
|
|
27
|
+
/**
|
|
28
|
+
* 语义颜色(antd `color` 并集) — 与 Badge 不同,Tag 偏向**用户可关闭的标签**(关键词 / 分类),Badge 偏向状态徽标。
|
|
29
|
+
* @default "default"
|
|
30
|
+
*/
|
|
31
|
+
color?: 'default' | 'primary' | 'success' | 'warning' | 'error' | 'info';
|
|
32
|
+
/**
|
|
33
|
+
* 显示关闭按钮(antd `closable` 并集)。
|
|
34
|
+
* @default false
|
|
35
|
+
*/
|
|
36
|
+
closable?: boolean;
|
|
37
|
+
/**
|
|
38
|
+
* 关闭回调。
|
|
39
|
+
*/
|
|
40
|
+
onClose?: () => void;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const Tag = React.forwardRef<HTMLSpanElement, TagProps>(
|
|
44
|
+
(
|
|
45
|
+
{ color, closable = false, onClose, className, children, ...props },
|
|
46
|
+
ref,
|
|
47
|
+
) => {
|
|
48
|
+
const [open, setOpen] = React.useState(true);
|
|
49
|
+
if (!open) return null;
|
|
50
|
+
return (
|
|
51
|
+
<span
|
|
52
|
+
ref={ref}
|
|
53
|
+
className={cn(tagVariants({ color }), className)}
|
|
54
|
+
{...props}
|
|
55
|
+
>
|
|
56
|
+
{children}
|
|
57
|
+
{closable ? (
|
|
58
|
+
<button
|
|
59
|
+
type="button"
|
|
60
|
+
aria-label="Close"
|
|
61
|
+
onClick={() => {
|
|
62
|
+
setOpen(false);
|
|
63
|
+
onClose?.();
|
|
64
|
+
}}
|
|
65
|
+
className="rounded-sm opacity-60 transition-opacity hover:opacity-100 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
|
66
|
+
>
|
|
67
|
+
<X className="size-3" />
|
|
68
|
+
</button>
|
|
69
|
+
) : null}
|
|
70
|
+
</span>
|
|
71
|
+
);
|
|
72
|
+
},
|
|
73
|
+
);
|
|
74
|
+
Tag.displayName = 'Tag';
|
|
75
|
+
|
|
76
|
+
// ─── CheckableTag(antd `Tag.CheckableTag` 并集)──────────────────────────
|
|
77
|
+
|
|
78
|
+
export interface CheckableTagProps
|
|
79
|
+
extends Omit<React.HTMLAttributes<HTMLSpanElement>, 'onChange'> {
|
|
80
|
+
/** 是否选中(受控)。 */
|
|
81
|
+
checked: boolean;
|
|
82
|
+
/** 选中态变化回调。 */
|
|
83
|
+
onChange?: (next: boolean) => void;
|
|
84
|
+
/** 禁用。 */
|
|
85
|
+
disabled?: boolean;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const CheckableTag = React.forwardRef<HTMLSpanElement, CheckableTagProps>(
|
|
89
|
+
({ checked, onChange, disabled, className, children, ...props }, ref) => (
|
|
90
|
+
<span
|
|
91
|
+
ref={ref}
|
|
92
|
+
role="checkbox"
|
|
93
|
+
aria-checked={checked}
|
|
94
|
+
aria-disabled={disabled}
|
|
95
|
+
tabIndex={disabled ? -1 : 0}
|
|
96
|
+
onClick={() => {
|
|
97
|
+
if (!disabled) onChange?.(!checked);
|
|
98
|
+
}}
|
|
99
|
+
onKeyDown={(e) => {
|
|
100
|
+
if (!disabled && (e.key === ' ' || e.key === 'Enter')) {
|
|
101
|
+
e.preventDefault();
|
|
102
|
+
onChange?.(!checked);
|
|
103
|
+
}
|
|
104
|
+
}}
|
|
105
|
+
className={cn(
|
|
106
|
+
'inline-flex cursor-pointer items-center gap-1 rounded-md border px-2 py-0.5 text-xs font-medium transition-colors',
|
|
107
|
+
'focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring',
|
|
108
|
+
checked
|
|
109
|
+
? 'border-primary bg-primary text-primary-foreground hover:bg-primary/90'
|
|
110
|
+
: 'border-border bg-muted text-foreground hover:bg-accent',
|
|
111
|
+
disabled && 'cursor-not-allowed opacity-50',
|
|
112
|
+
className,
|
|
113
|
+
)}
|
|
114
|
+
{...props}
|
|
115
|
+
>
|
|
116
|
+
{children}
|
|
117
|
+
</span>
|
|
118
|
+
),
|
|
119
|
+
);
|
|
120
|
+
CheckableTag.displayName = 'CheckableTag';
|
|
121
|
+
|
|
122
|
+
// ─── CheckableTagGroup(antd v6 新增) ────────────────────────────────────
|
|
123
|
+
|
|
124
|
+
export interface CheckableTagGroupOption {
|
|
125
|
+
/** 显示文本。 */
|
|
126
|
+
label: React.ReactNode;
|
|
127
|
+
/** 真实 value(受控集合的比对依据)。 */
|
|
128
|
+
value: string;
|
|
129
|
+
/** 禁用此项。 */
|
|
130
|
+
disabled?: boolean;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export interface CheckableTagGroupProps
|
|
134
|
+
extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onChange' | 'defaultValue'> {
|
|
135
|
+
/** 候选项数组。 */
|
|
136
|
+
options: CheckableTagGroupOption[];
|
|
137
|
+
/** 受控选中 value 集合。 */
|
|
138
|
+
value?: string[];
|
|
139
|
+
/** uncontrolled 初值。 */
|
|
140
|
+
defaultValue?: string[];
|
|
141
|
+
/** 选中变化回调。 */
|
|
142
|
+
onChange?: (next: string[]) => void;
|
|
143
|
+
/** 整组禁用。 */
|
|
144
|
+
disabled?: boolean;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const CheckableTagGroup = React.forwardRef<HTMLDivElement, CheckableTagGroupProps>(
|
|
148
|
+
(
|
|
149
|
+
{ options, value, defaultValue, onChange, disabled, className, ...props },
|
|
150
|
+
ref,
|
|
151
|
+
) => {
|
|
152
|
+
const isControlled = value !== undefined;
|
|
153
|
+
const [internal, setInternal] = React.useState<string[]>(defaultValue ?? []);
|
|
154
|
+
const current = isControlled ? value! : internal;
|
|
155
|
+
|
|
156
|
+
const toggle = (v: string, next: boolean) => {
|
|
157
|
+
const arr = next ? [...current, v] : current.filter((x) => x !== v);
|
|
158
|
+
if (!isControlled) setInternal(arr);
|
|
159
|
+
onChange?.(arr);
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
return (
|
|
163
|
+
<div
|
|
164
|
+
ref={ref}
|
|
165
|
+
role="group"
|
|
166
|
+
className={cn('flex flex-wrap gap-2', className)}
|
|
167
|
+
{...props}
|
|
168
|
+
>
|
|
169
|
+
{options.map((opt) => (
|
|
170
|
+
<CheckableTag
|
|
171
|
+
key={opt.value}
|
|
172
|
+
checked={current.includes(opt.value)}
|
|
173
|
+
disabled={disabled || opt.disabled}
|
|
174
|
+
onChange={(c) => toggle(opt.value, c)}
|
|
175
|
+
>
|
|
176
|
+
{opt.label}
|
|
177
|
+
</CheckableTag>
|
|
178
|
+
))}
|
|
179
|
+
</div>
|
|
180
|
+
);
|
|
181
|
+
},
|
|
182
|
+
);
|
|
183
|
+
CheckableTagGroup.displayName = 'CheckableTagGroup';
|
|
184
|
+
|
|
185
|
+
export { Tag, CheckableTag, CheckableTagGroup, tagVariants };
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: textarea
|
|
3
|
+
name: Textarea
|
|
4
|
+
type: component
|
|
5
|
+
category: form
|
|
6
|
+
since: 0.1.0
|
|
7
|
+
package: "@teamix-evo/ui"
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Textarea
|
|
11
|
+
|
|
12
|
+
多行文本输入 — shadcn 简洁基底 + antd 的 `autoSize / showCount`。
|
|
13
|
+
|
|
14
|
+
## When to use
|
|
15
|
+
|
|
16
|
+
- 多行内容输入(评论 / 反馈 / 描述 / 备注)
|
|
17
|
+
- 配 `autoSize` 给"动态高度"输入
|
|
18
|
+
- 配 `showCount + maxLength` 控制长度
|
|
19
|
+
|
|
20
|
+
## When NOT to use
|
|
21
|
+
|
|
22
|
+
- 单行 → `Input`
|
|
23
|
+
- 富文本编辑 → 用 Tiptap / Lexical 等编辑器(超出本组件库范围)
|
|
24
|
+
|
|
25
|
+
## Props
|
|
26
|
+
|
|
27
|
+
> 以下表格由 `pnpm --filter @teamix-evo/ui gen:meta` 自动生成。
|
|
28
|
+
|
|
29
|
+
<!-- auto:props:begin -->
|
|
30
|
+
| 名称 | 类型 | 默认值 | 必填 | 说明 |
|
|
31
|
+
| --- | --- | --- | --- | --- |
|
|
32
|
+
| `autoSize` | `boolean \| { minRows?: number; maxRows?: number }` | `false` | – | 自动高度。`true` 时无上下限;传 `{ minRows, maxRows }` 时受限。 实现:监听 input 事件,把 textarea 设为 auto 后再读 scrollHeight。 |
|
|
33
|
+
| `showCount` | `boolean` | `false` | – | 显示字符计数(antd `showCount` 并集);需配合 `maxLength`。 |
|
|
34
|
+
| `value` | `string` | – | – | 受控值。 |
|
|
35
|
+
| `defaultValue` | `string` | – | – | uncontrolled 初始值。 |
|
|
36
|
+
| `rows` | `number` | `3` | – | 行数(autoSize 关闭时生效)。 |
|
|
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 textarea` 时,以下 entry 会被自动连带安装(无需手动 add)。
|
|
47
|
+
|
|
48
|
+
| Entry | 类型 | 描述 |
|
|
49
|
+
| --- | --- | --- |
|
|
50
|
+
| `cn` | util | Tailwind className 合并工具(clsx + tailwind-merge) |
|
|
51
|
+
|
|
52
|
+
### npm 依赖
|
|
53
|
+
|
|
54
|
+
_无 — 本组件不依赖任何 npm 包。_
|
|
55
|
+
<!-- auto:deps:end -->
|
|
56
|
+
|
|
57
|
+
> 透传所有 `<textarea>` 原生属性(`name` / `placeholder` / `disabled` / `onChange` / ...)。
|
|
58
|
+
|
|
59
|
+
## AI 生成纪律
|
|
60
|
+
|
|
61
|
+
- **`autoSize` 替代 `rows`**:autoSize 启用后 rows 被忽略;两者不要同时传
|
|
62
|
+
- **autoSize 配 `maxRows` 防止失控**:不限的 autoSize 在长内容下会撑爆容器,推荐 `{ minRows: 3, maxRows: 8 }`
|
|
63
|
+
- **`showCount` 必须配 `maxLength`**:无 maxLength 时 showCount 不显示
|
|
64
|
+
- **粘贴长内容时 ResizeObserver**:autoSize 用 effect 监听 value 触发,粘贴大文本会自动调整;无需业务侧介入
|
|
65
|
+
|
|
66
|
+
## Examples
|
|
67
|
+
|
|
68
|
+
```tsx
|
|
69
|
+
import { Textarea } from '@/components/ui/textarea';
|
|
70
|
+
|
|
71
|
+
// 基础
|
|
72
|
+
<Textarea placeholder="留下你的反馈..." />
|
|
73
|
+
|
|
74
|
+
// 自动高度
|
|
75
|
+
<Textarea autoSize={{ minRows: 3, maxRows: 8 }} placeholder="..." />
|
|
76
|
+
|
|
77
|
+
// 字数统计
|
|
78
|
+
<Textarea showCount maxLength={500} placeholder="不超过 500 字" />
|
|
79
|
+
|
|
80
|
+
// 受控
|
|
81
|
+
const [v, setV] = React.useState('');
|
|
82
|
+
<Textarea value={v} onChange={(e) => setV(e.target.value)} />
|
|
83
|
+
```
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import { Textarea } from './textarea';
|
|
3
|
+
|
|
4
|
+
const meta: Meta<typeof Textarea> = {
|
|
5
|
+
title: '表单与输入 · Form/Textarea',
|
|
6
|
+
component: Textarea,
|
|
7
|
+
tags: ['autodocs'],
|
|
8
|
+
parameters: {
|
|
9
|
+
docs: {
|
|
10
|
+
description: {
|
|
11
|
+
component:
|
|
12
|
+
'多行文本输入 — 用于输入多行文本信息。shadcn `Textarea` 原子 + antd `Input.TextArea` 的并集能力:支持 `autoSize`(传 `{ minRows, maxRows }` 随内容自适应高度)、`showCount` 发表字数计数、`maxLength` 限制。视觉走 OpenTrek semantic tokens,所有样式来自 `@teamix-evo/design`,无 mock。',
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
argTypes: {
|
|
17
|
+
rows: { control: 'number' },
|
|
18
|
+
maxLength: { control: 'number' },
|
|
19
|
+
showCount: { control: 'boolean' },
|
|
20
|
+
disabled: { control: 'boolean' },
|
|
21
|
+
placeholder: { control: 'text' },
|
|
22
|
+
},
|
|
23
|
+
args: {
|
|
24
|
+
rows: 3,
|
|
25
|
+
placeholder: '请输入...',
|
|
26
|
+
showCount: false,
|
|
27
|
+
disabled: false,
|
|
28
|
+
},
|
|
29
|
+
decorators: [
|
|
30
|
+
(Story) => (
|
|
31
|
+
<div className="w-80">
|
|
32
|
+
<Story />
|
|
33
|
+
</div>
|
|
34
|
+
),
|
|
35
|
+
],
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export default meta;
|
|
39
|
+
type Story = StoryObj<typeof Textarea>;
|
|
40
|
+
|
|
41
|
+
export const Playground: Story = {};
|
|
42
|
+
|
|
43
|
+
export const AutoSize: Story = {
|
|
44
|
+
parameters: { controls: { disable: true } },
|
|
45
|
+
render: () => (
|
|
46
|
+
<Textarea
|
|
47
|
+
autoSize={{ minRows: 3, maxRows: 8 }}
|
|
48
|
+
placeholder="试试粘贴一段长文本看看高度变化..."
|
|
49
|
+
/>
|
|
50
|
+
),
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export const ShowCount: Story = {
|
|
54
|
+
parameters: { controls: { disable: true } },
|
|
55
|
+
render: () => (
|
|
56
|
+
<Textarea showCount maxLength={500} placeholder="不超过 500 字" />
|
|
57
|
+
),
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export const Disabled: Story = {
|
|
61
|
+
parameters: { controls: { disable: true } },
|
|
62
|
+
render: () => <Textarea disabled defaultValue="这是只读的内容,不可编辑。" />,
|
|
63
|
+
};
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
import { cn } from '@/utils/cn';
|
|
4
|
+
|
|
5
|
+
export interface TextareaProps
|
|
6
|
+
extends Omit<React.TextareaHTMLAttributes<HTMLTextAreaElement>, 'rows'> {
|
|
7
|
+
/**
|
|
8
|
+
* 自动高度。`true` 时无上下限;传 `{ minRows, maxRows }` 时受限。
|
|
9
|
+
* 实现:监听 input 事件,把 textarea 设为 auto 后再读 scrollHeight。
|
|
10
|
+
* @default false
|
|
11
|
+
*/
|
|
12
|
+
autoSize?: boolean | { minRows?: number; maxRows?: number };
|
|
13
|
+
/**
|
|
14
|
+
* 显示字符计数(antd `showCount` 并集);需配合 `maxLength`。
|
|
15
|
+
* @default false
|
|
16
|
+
*/
|
|
17
|
+
showCount?: boolean;
|
|
18
|
+
/** 受控值。 */
|
|
19
|
+
value?: string;
|
|
20
|
+
/** uncontrolled 初始值。 */
|
|
21
|
+
defaultValue?: string;
|
|
22
|
+
/** 行数(autoSize 关闭时生效)。 @default 3 */
|
|
23
|
+
rows?: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
|
27
|
+
(
|
|
28
|
+
{
|
|
29
|
+
className,
|
|
30
|
+
autoSize = false,
|
|
31
|
+
showCount = false,
|
|
32
|
+
value,
|
|
33
|
+
defaultValue,
|
|
34
|
+
rows = 3,
|
|
35
|
+
onChange,
|
|
36
|
+
maxLength,
|
|
37
|
+
...props
|
|
38
|
+
},
|
|
39
|
+
ref,
|
|
40
|
+
) => {
|
|
41
|
+
const isControlled = value !== undefined;
|
|
42
|
+
const [internal, setInternal] = React.useState<string>(
|
|
43
|
+
defaultValue ?? '',
|
|
44
|
+
);
|
|
45
|
+
const current = isControlled ? value : internal;
|
|
46
|
+
|
|
47
|
+
const innerRef = React.useRef<HTMLTextAreaElement | null>(null);
|
|
48
|
+
React.useImperativeHandle(
|
|
49
|
+
ref,
|
|
50
|
+
() => innerRef.current as HTMLTextAreaElement,
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
const resize = React.useCallback(() => {
|
|
54
|
+
if (!autoSize) return;
|
|
55
|
+
const el = innerRef.current;
|
|
56
|
+
if (!el) return;
|
|
57
|
+
const cs = window.getComputedStyle(el);
|
|
58
|
+
const lineHeight =
|
|
59
|
+
parseFloat(cs.lineHeight) || parseFloat(cs.fontSize) * 1.5;
|
|
60
|
+
const padTop = parseFloat(cs.paddingTop) || 0;
|
|
61
|
+
const padBottom = parseFloat(cs.paddingBottom) || 0;
|
|
62
|
+
el.style.height = 'auto';
|
|
63
|
+
const next = el.scrollHeight;
|
|
64
|
+
const opts = typeof autoSize === 'object' ? autoSize : {};
|
|
65
|
+
const minH =
|
|
66
|
+
(opts.minRows ?? 0) * lineHeight + padTop + padBottom;
|
|
67
|
+
const maxH = opts.maxRows
|
|
68
|
+
? opts.maxRows * lineHeight + padTop + padBottom
|
|
69
|
+
: Number.POSITIVE_INFINITY;
|
|
70
|
+
el.style.height = `${Math.max(minH, Math.min(next, maxH))}px`;
|
|
71
|
+
el.style.overflowY = next > maxH ? 'auto' : 'hidden';
|
|
72
|
+
}, [autoSize]);
|
|
73
|
+
|
|
74
|
+
React.useEffect(resize, [resize, current]);
|
|
75
|
+
|
|
76
|
+
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
|
77
|
+
if (!isControlled) setInternal(e.target.value);
|
|
78
|
+
onChange?.(e);
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const textarea = (
|
|
82
|
+
<textarea
|
|
83
|
+
ref={innerRef}
|
|
84
|
+
rows={autoSize ? undefined : rows}
|
|
85
|
+
value={isControlled ? value : undefined}
|
|
86
|
+
defaultValue={!isControlled ? defaultValue : undefined}
|
|
87
|
+
maxLength={maxLength}
|
|
88
|
+
onChange={handleChange}
|
|
89
|
+
className={cn(
|
|
90
|
+
'flex w-full rounded-md border border-input bg-background px-3 py-2 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50',
|
|
91
|
+
autoSize ? 'resize-none' : 'min-h-[80px]',
|
|
92
|
+
className,
|
|
93
|
+
)}
|
|
94
|
+
{...props}
|
|
95
|
+
/>
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
if (showCount && maxLength) {
|
|
99
|
+
return (
|
|
100
|
+
<div className="space-y-1">
|
|
101
|
+
{textarea}
|
|
102
|
+
<div className="text-right text-xs text-muted-foreground tabular-nums">
|
|
103
|
+
{current?.length ?? 0} / {maxLength}
|
|
104
|
+
</div>
|
|
105
|
+
</div>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
return textarea;
|
|
109
|
+
},
|
|
110
|
+
);
|
|
111
|
+
Textarea.displayName = 'Textarea';
|
|
112
|
+
|
|
113
|
+
export { Textarea };
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: time-picker
|
|
3
|
+
name: TimePicker
|
|
4
|
+
type: component
|
|
5
|
+
category: form
|
|
6
|
+
since: 0.1.0
|
|
7
|
+
package: "@teamix-evo/ui"
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# TimePicker
|
|
11
|
+
|
|
12
|
+
时间选择 — antd 独有补足。**等价 antd `TimePicker`** 的基础能力,基于原生 `<input type="time">` 样式封装(零 JS、移动端友好、SSR safe)。配 `DatePicker` 完成日期 + 时间填写。
|
|
13
|
+
|
|
14
|
+
## When to use
|
|
15
|
+
|
|
16
|
+
- 表单字段需要时分(可选秒)输入(会议预约、闹钟、营业时间)
|
|
17
|
+
- 配 `DatePicker` 完整时间戳填写(日期 + 时间)
|
|
18
|
+
|
|
19
|
+
## When NOT to use
|
|
20
|
+
|
|
21
|
+
- 时间区间(开始 - 结束)→ 用两个 TimePicker 并排,或后续基于 Popover 自实现 RangeTimePicker
|
|
22
|
+
- 日期 → `DatePicker`(日期 + 时间二合一也可在 DatePicker 上拓展)
|
|
23
|
+
- 复杂禁用规则 / 自定义面板:当前实现走原生,**不支持** `disabledHours` / `disabledMinutes` 等 antd 高级 API — 需要时基于 Popover + ListBox 自实现
|
|
24
|
+
|
|
25
|
+
<!-- auto:props:begin -->
|
|
26
|
+
| 名称 | 类型 | 默认值 | 必填 | 说明 |
|
|
27
|
+
| --- | --- | --- | --- | --- |
|
|
28
|
+
| `value` | `string` | – | – | 受控值(`HH:mm` 或 `HH:mm:ss`)— 空串表示未选。 |
|
|
29
|
+
| `defaultValue` | `string` | – | – | uncontrolled 初值。 |
|
|
30
|
+
| `onChange` | `(value: string) => void` | – | – | 值变化回调(实时,跟随原生 input)。 |
|
|
31
|
+
| `showSeconds` | `boolean` | `false` | – | 是否显示秒(antd `showSeconds` 并集)— 关闭时原生 input 仅显示 HH:mm。 |
|
|
32
|
+
| `step` | `number` | `60` | – | 步长(秒) — 影响原生 picker 的滚动粒度,e.g. 60 = 一分钟一档。 |
|
|
33
|
+
| `size` | `'sm' \| 'default' \| 'lg'` | `"default"` | – | 尺寸。 |
|
|
34
|
+
<!-- auto:props:end -->
|
|
35
|
+
|
|
36
|
+
<!-- auto:deps:begin -->
|
|
37
|
+
### 同库依赖
|
|
38
|
+
|
|
39
|
+
> `teamix-evo ui add time-picker` 时,以下 entry 会被自动连带安装(无需手动 add)。
|
|
40
|
+
|
|
41
|
+
| Entry | 类型 | 描述 |
|
|
42
|
+
| --- | --- | --- |
|
|
43
|
+
| `cn` | util | Tailwind className 合并工具(clsx + tailwind-merge) |
|
|
44
|
+
|
|
45
|
+
### npm 依赖
|
|
46
|
+
|
|
47
|
+
> 业务侧需要先 `pnpm add` / `npm install` 这些包。CLI 在 `ui add` 完成后会列出此提示。
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
pnpm add lucide-react@^0.460.0
|
|
51
|
+
```
|
|
52
|
+
<!-- auto:deps:end -->
|
|
53
|
+
|
|
54
|
+
## AI 生成纪律
|
|
55
|
+
|
|
56
|
+
- **value 格式必为 `HH:mm` 或 `HH:mm:ss`** — 与原生 input[type=time] 一致,**不要**传 ISO 字符串
|
|
57
|
+
- **`showSeconds=true` 才会显示秒** — 默认隐藏,绝大多数场景不需要秒
|
|
58
|
+
- **`step`** 单位是**秒**(原生规范) — `step=900` 表示 15 分钟一档
|
|
59
|
+
- **空状态用空字符串** `""`,不要传 `null` / `undefined`(原生 input 不接受 null)
|
|
60
|
+
- **当前实现不支持 12 小时制 / AM-PM** — 浏览器根据系统 locale 自动决定显示格式;**不要**手动加 AM/PM 字段
|
|
61
|
+
|
|
62
|
+
## Examples
|
|
63
|
+
|
|
64
|
+
```tsx
|
|
65
|
+
import { TimePicker } from '@/components/ui/time-picker';
|
|
66
|
+
import * as React from 'react';
|
|
67
|
+
|
|
68
|
+
// 基础(HH:mm)
|
|
69
|
+
<TimePicker defaultValue="09:30" />
|
|
70
|
+
|
|
71
|
+
// 受控 + 秒
|
|
72
|
+
const [t, setT] = React.useState('09:30:00');
|
|
73
|
+
<TimePicker value={t} onChange={setT} showSeconds />
|
|
74
|
+
|
|
75
|
+
// 15 分钟一档
|
|
76
|
+
<TimePicker step={15 * 60} />
|
|
77
|
+
|
|
78
|
+
// 与 DatePicker 配对
|
|
79
|
+
<div className="flex gap-2">
|
|
80
|
+
<DatePicker />
|
|
81
|
+
<TimePicker />
|
|
82
|
+
</div>
|
|
83
|
+
```
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
3
|
+
import { TimePicker } from './time-picker';
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof TimePicker> = {
|
|
6
|
+
title: '表单与输入 · Form/TimePicker',
|
|
7
|
+
component: TimePicker,
|
|
8
|
+
tags: ['autodocs'],
|
|
9
|
+
parameters: {
|
|
10
|
+
docs: {
|
|
11
|
+
description: {
|
|
12
|
+
component:
|
|
13
|
+
'时间选择 — 基于原生 input[type=time] 样式封装(零 JS、移动端友好、SSR safe)。等价 antd `TimePicker` 基础能力,复杂禁用规则需自实现。视觉走 OpenTrek tokens,所有样式来自 `@teamix-evo/design`,无 mock。',
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
argTypes: {
|
|
18
|
+
size: { control: 'inline-radio', options: ['sm', 'default', 'lg'] },
|
|
19
|
+
showSeconds: { control: 'boolean' },
|
|
20
|
+
disabled: { control: 'boolean' },
|
|
21
|
+
},
|
|
22
|
+
args: { size: 'default', showSeconds: false, disabled: false },
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export default meta;
|
|
26
|
+
type Story = StoryObj<typeof TimePicker>;
|
|
27
|
+
|
|
28
|
+
export const Playground: Story = {
|
|
29
|
+
render: (args) => <TimePicker {...args} defaultValue="09:30" />,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const Controlled: Story = {
|
|
33
|
+
parameters: { controls: { disable: true } },
|
|
34
|
+
render: () => {
|
|
35
|
+
const [t, setT] = React.useState('09:30:00');
|
|
36
|
+
return (
|
|
37
|
+
<div className="flex items-center gap-3">
|
|
38
|
+
<TimePicker value={t} onChange={setT} showSeconds />
|
|
39
|
+
<span className="text-sm text-muted-foreground tabular-nums">{t || '(空)'}</span>
|
|
40
|
+
</div>
|
|
41
|
+
);
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export const QuarterHourStep: Story = {
|
|
46
|
+
parameters: { controls: { disable: true } },
|
|
47
|
+
render: () => <TimePicker showSeconds step={15 * 60} defaultValue="10:00:00" />,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export const Sizes: Story = {
|
|
51
|
+
parameters: { controls: { disable: true } },
|
|
52
|
+
render: () => (
|
|
53
|
+
<div className="flex items-center gap-3">
|
|
54
|
+
<TimePicker size="sm" defaultValue="09:00" />
|
|
55
|
+
<TimePicker size="default" defaultValue="09:00" />
|
|
56
|
+
<TimePicker size="lg" defaultValue="09:00" />
|
|
57
|
+
</div>
|
|
58
|
+
),
|
|
59
|
+
};
|