@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.
Files changed (270) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +336 -0
  3. package/_data.json +12 -0
  4. package/manifest.json +1688 -0
  5. package/package.json +90 -0
  6. package/src/components/accordion/accordion.meta.md +87 -0
  7. package/src/components/accordion/accordion.stories.tsx +67 -0
  8. package/src/components/accordion/accordion.tsx +58 -0
  9. package/src/components/affix/affix.meta.md +80 -0
  10. package/src/components/affix/affix.stories.tsx +57 -0
  11. package/src/components/affix/affix.tsx +97 -0
  12. package/src/components/alert/alert.meta.md +101 -0
  13. package/src/components/alert/alert.stories.tsx +93 -0
  14. package/src/components/alert/alert.tsx +132 -0
  15. package/src/components/alert-dialog/alert-dialog.meta.md +107 -0
  16. package/src/components/alert-dialog/alert-dialog.stories.tsx +81 -0
  17. package/src/components/alert-dialog/alert-dialog.tsx +136 -0
  18. package/src/components/anchor/anchor.meta.md +87 -0
  19. package/src/components/anchor/anchor.stories.tsx +74 -0
  20. package/src/components/anchor/anchor.tsx +130 -0
  21. package/src/components/app/app.meta.md +86 -0
  22. package/src/components/app/app.stories.tsx +62 -0
  23. package/src/components/app/app.tsx +58 -0
  24. package/src/components/aspect-ratio/aspect-ratio.meta.md +81 -0
  25. package/src/components/aspect-ratio/aspect-ratio.stories.tsx +59 -0
  26. package/src/components/aspect-ratio/aspect-ratio.tsx +22 -0
  27. package/src/components/auto-complete/auto-complete.meta.md +102 -0
  28. package/src/components/auto-complete/auto-complete.stories.tsx +93 -0
  29. package/src/components/auto-complete/auto-complete.tsx +205 -0
  30. package/src/components/avatar/avatar.meta.md +94 -0
  31. package/src/components/avatar/avatar.stories.tsx +80 -0
  32. package/src/components/avatar/avatar.tsx +126 -0
  33. package/src/components/badge/badge.meta.md +119 -0
  34. package/src/components/badge/badge.stories.tsx +153 -0
  35. package/src/components/badge/badge.tsx +210 -0
  36. package/src/components/breadcrumb/breadcrumb.meta.md +107 -0
  37. package/src/components/breadcrumb/breadcrumb.stories.tsx +84 -0
  38. package/src/components/breadcrumb/breadcrumb.tsx +122 -0
  39. package/src/components/button/button.meta.md +98 -0
  40. package/src/components/button/button.stories.tsx +235 -0
  41. package/src/components/button/button.tsx +160 -0
  42. package/src/components/button-group/button-group.meta.md +92 -0
  43. package/src/components/button-group/button-group.stories.tsx +90 -0
  44. package/src/components/button-group/button-group.tsx +75 -0
  45. package/src/components/calendar/calendar.meta.md +118 -0
  46. package/src/components/calendar/calendar.stories.tsx +68 -0
  47. package/src/components/calendar/calendar.tsx +107 -0
  48. package/src/components/card/card.meta.md +117 -0
  49. package/src/components/card/card.stories.tsx +112 -0
  50. package/src/components/card/card.tsx +222 -0
  51. package/src/components/carousel/carousel.meta.md +117 -0
  52. package/src/components/carousel/carousel.stories.tsx +84 -0
  53. package/src/components/carousel/carousel.tsx +224 -0
  54. package/src/components/cascader/cascader.meta.md +110 -0
  55. package/src/components/cascader/cascader.stories.tsx +108 -0
  56. package/src/components/cascader/cascader.tsx +198 -0
  57. package/src/components/checkbox/checkbox.meta.md +99 -0
  58. package/src/components/checkbox/checkbox.stories.tsx +130 -0
  59. package/src/components/checkbox/checkbox.tsx +125 -0
  60. package/src/components/collapsible/collapsible.meta.md +80 -0
  61. package/src/components/collapsible/collapsible.stories.tsx +35 -0
  62. package/src/components/collapsible/collapsible.tsx +18 -0
  63. package/src/components/color-picker/color-picker.meta.md +84 -0
  64. package/src/components/color-picker/color-picker.stories.tsx +80 -0
  65. package/src/components/color-picker/color-picker.tsx +160 -0
  66. package/src/components/combobox/combobox.meta.md +93 -0
  67. package/src/components/combobox/combobox.stories.tsx +55 -0
  68. package/src/components/combobox/combobox.tsx +130 -0
  69. package/src/components/command/command.meta.md +104 -0
  70. package/src/components/command/command.stories.tsx +59 -0
  71. package/src/components/command/command.tsx +147 -0
  72. package/src/components/context-menu/context-menu.meta.md +90 -0
  73. package/src/components/context-menu/context-menu.stories.tsx +46 -0
  74. package/src/components/context-menu/context-menu.tsx +191 -0
  75. package/src/components/data-table/data-table.meta.md +149 -0
  76. package/src/components/data-table/data-table.stories.tsx +125 -0
  77. package/src/components/data-table/data-table.tsx +185 -0
  78. package/src/components/date-picker/date-picker.meta.md +106 -0
  79. package/src/components/date-picker/date-picker.stories.tsx +58 -0
  80. package/src/components/date-picker/date-picker.tsx +156 -0
  81. package/src/components/descriptions/descriptions.meta.md +78 -0
  82. package/src/components/descriptions/descriptions.stories.tsx +60 -0
  83. package/src/components/descriptions/descriptions.tsx +129 -0
  84. package/src/components/dialog/dialog.meta.md +105 -0
  85. package/src/components/dialog/dialog.stories.tsx +93 -0
  86. package/src/components/dialog/dialog.tsx +128 -0
  87. package/src/components/drawer/drawer.meta.md +96 -0
  88. package/src/components/drawer/drawer.stories.tsx +54 -0
  89. package/src/components/drawer/drawer.tsx +114 -0
  90. package/src/components/dropdown-menu/dropdown-menu.meta.md +103 -0
  91. package/src/components/dropdown-menu/dropdown-menu.stories.tsx +112 -0
  92. package/src/components/dropdown-menu/dropdown-menu.tsx +195 -0
  93. package/src/components/empty/empty.meta.md +81 -0
  94. package/src/components/empty/empty.stories.tsx +46 -0
  95. package/src/components/empty/empty.tsx +47 -0
  96. package/src/components/field/field.meta.md +116 -0
  97. package/src/components/field/field.stories.tsx +117 -0
  98. package/src/components/field/field.tsx +164 -0
  99. package/src/components/flex/flex.meta.md +94 -0
  100. package/src/components/flex/flex.stories.tsx +112 -0
  101. package/src/components/flex/flex.tsx +122 -0
  102. package/src/components/float-button/float-button.meta.md +87 -0
  103. package/src/components/float-button/float-button.stories.tsx +78 -0
  104. package/src/components/float-button/float-button.tsx +143 -0
  105. package/src/components/form/form.meta.md +131 -0
  106. package/src/components/form/form.stories.tsx +122 -0
  107. package/src/components/form/form.tsx +194 -0
  108. package/src/components/grid/grid.meta.md +87 -0
  109. package/src/components/grid/grid.stories.tsx +99 -0
  110. package/src/components/grid/grid.tsx +130 -0
  111. package/src/components/hover-card/hover-card.meta.md +92 -0
  112. package/src/components/hover-card/hover-card.stories.tsx +68 -0
  113. package/src/components/hover-card/hover-card.tsx +29 -0
  114. package/src/components/image/image.meta.md +94 -0
  115. package/src/components/image/image.stories.tsx +55 -0
  116. package/src/components/image/image.tsx +138 -0
  117. package/src/components/input/input.meta.md +109 -0
  118. package/src/components/input/input.stories.tsx +117 -0
  119. package/src/components/input/input.tsx +213 -0
  120. package/src/components/input-group/input-group.meta.md +92 -0
  121. package/src/components/input-group/input-group.stories.tsx +88 -0
  122. package/src/components/input-group/input-group.tsx +107 -0
  123. package/src/components/input-number/input-number.meta.md +91 -0
  124. package/src/components/input-number/input-number.stories.tsx +87 -0
  125. package/src/components/input-number/input-number.tsx +210 -0
  126. package/src/components/input-otp/input-otp.meta.md +105 -0
  127. package/src/components/input-otp/input-otp.stories.tsx +65 -0
  128. package/src/components/input-otp/input-otp.tsx +97 -0
  129. package/src/components/item/item.meta.md +116 -0
  130. package/src/components/item/item.stories.tsx +113 -0
  131. package/src/components/item/item.tsx +171 -0
  132. package/src/components/kbd/kbd.meta.md +85 -0
  133. package/src/components/kbd/kbd.stories.tsx +70 -0
  134. package/src/components/kbd/kbd.tsx +81 -0
  135. package/src/components/label/label.meta.md +91 -0
  136. package/src/components/label/label.stories.tsx +87 -0
  137. package/src/components/label/label.tsx +66 -0
  138. package/src/components/masonry/masonry.meta.md +85 -0
  139. package/src/components/masonry/masonry.stories.tsx +66 -0
  140. package/src/components/masonry/masonry.tsx +59 -0
  141. package/src/components/mentions/mentions.meta.md +89 -0
  142. package/src/components/mentions/mentions.stories.tsx +75 -0
  143. package/src/components/mentions/mentions.tsx +237 -0
  144. package/src/components/menubar/menubar.meta.md +100 -0
  145. package/src/components/menubar/menubar.stories.tsx +81 -0
  146. package/src/components/menubar/menubar.tsx +232 -0
  147. package/src/components/native-select/native-select.meta.md +88 -0
  148. package/src/components/native-select/native-select.stories.tsx +80 -0
  149. package/src/components/native-select/native-select.tsx +54 -0
  150. package/src/components/navigation-menu/navigation-menu.meta.md +108 -0
  151. package/src/components/navigation-menu/navigation-menu.stories.tsx +112 -0
  152. package/src/components/navigation-menu/navigation-menu.tsx +125 -0
  153. package/src/components/notification/notification.meta.md +91 -0
  154. package/src/components/notification/notification.stories.tsx +96 -0
  155. package/src/components/notification/notification.tsx +84 -0
  156. package/src/components/pagination/pagination.meta.md +127 -0
  157. package/src/components/pagination/pagination.stories.tsx +62 -0
  158. package/src/components/pagination/pagination.tsx +285 -0
  159. package/src/components/popconfirm/popconfirm.meta.md +109 -0
  160. package/src/components/popconfirm/popconfirm.stories.tsx +76 -0
  161. package/src/components/popconfirm/popconfirm.tsx +134 -0
  162. package/src/components/popover/popover.meta.md +97 -0
  163. package/src/components/popover/popover.stories.tsx +82 -0
  164. package/src/components/popover/popover.tsx +55 -0
  165. package/src/components/progress/progress.meta.md +86 -0
  166. package/src/components/progress/progress.stories.tsx +75 -0
  167. package/src/components/progress/progress.tsx +195 -0
  168. package/src/components/radio-group/radio-group.meta.md +103 -0
  169. package/src/components/radio-group/radio-group.stories.tsx +77 -0
  170. package/src/components/radio-group/radio-group.tsx +78 -0
  171. package/src/components/rate/rate.meta.md +87 -0
  172. package/src/components/rate/rate.stories.tsx +81 -0
  173. package/src/components/rate/rate.tsx +153 -0
  174. package/src/components/resizable/resizable.meta.md +92 -0
  175. package/src/components/resizable/resizable.stories.tsx +104 -0
  176. package/src/components/resizable/resizable.tsx +56 -0
  177. package/src/components/result/result.meta.md +90 -0
  178. package/src/components/result/result.stories.tsx +71 -0
  179. package/src/components/result/result.tsx +91 -0
  180. package/src/components/scroll-area/scroll-area.meta.md +84 -0
  181. package/src/components/scroll-area/scroll-area.stories.tsx +41 -0
  182. package/src/components/scroll-area/scroll-area.tsx +51 -0
  183. package/src/components/segmented/segmented.meta.md +103 -0
  184. package/src/components/segmented/segmented.stories.tsx +101 -0
  185. package/src/components/segmented/segmented.tsx +138 -0
  186. package/src/components/select/select.meta.md +110 -0
  187. package/src/components/select/select.stories.tsx +100 -0
  188. package/src/components/select/select.tsx +188 -0
  189. package/src/components/separator/separator.meta.md +74 -0
  190. package/src/components/separator/separator.stories.tsx +71 -0
  191. package/src/components/separator/separator.tsx +104 -0
  192. package/src/components/sheet/sheet.meta.md +97 -0
  193. package/src/components/sheet/sheet.stories.tsx +82 -0
  194. package/src/components/sheet/sheet.tsx +139 -0
  195. package/src/components/sidebar/sidebar.meta.md +131 -0
  196. package/src/components/sidebar/sidebar.stories.tsx +82 -0
  197. package/src/components/sidebar/sidebar.tsx +351 -0
  198. package/src/components/skeleton/skeleton.meta.md +95 -0
  199. package/src/components/skeleton/skeleton.stories.tsx +79 -0
  200. package/src/components/skeleton/skeleton.tsx +144 -0
  201. package/src/components/slider/slider.meta.md +94 -0
  202. package/src/components/slider/slider.stories.tsx +69 -0
  203. package/src/components/slider/slider.tsx +86 -0
  204. package/src/components/sonner/sonner.meta.md +96 -0
  205. package/src/components/sonner/sonner.stories.tsx +91 -0
  206. package/src/components/sonner/sonner.tsx +40 -0
  207. package/src/components/space/space.meta.md +94 -0
  208. package/src/components/space/space.stories.tsx +94 -0
  209. package/src/components/space/space.tsx +106 -0
  210. package/src/components/spinner/spinner.meta.md +76 -0
  211. package/src/components/spinner/spinner.stories.tsx +71 -0
  212. package/src/components/spinner/spinner.tsx +64 -0
  213. package/src/components/statistic/statistic.meta.md +99 -0
  214. package/src/components/statistic/statistic.stories.tsx +71 -0
  215. package/src/components/statistic/statistic.tsx +197 -0
  216. package/src/components/steps/steps.meta.md +102 -0
  217. package/src/components/steps/steps.stories.tsx +75 -0
  218. package/src/components/steps/steps.tsx +170 -0
  219. package/src/components/switch/switch.meta.md +92 -0
  220. package/src/components/switch/switch.stories.tsx +75 -0
  221. package/src/components/switch/switch.tsx +101 -0
  222. package/src/components/table/table.meta.md +95 -0
  223. package/src/components/table/table.stories.tsx +75 -0
  224. package/src/components/table/table.tsx +122 -0
  225. package/src/components/tabs/tabs.meta.md +98 -0
  226. package/src/components/tabs/tabs.stories.tsx +70 -0
  227. package/src/components/tabs/tabs.tsx +119 -0
  228. package/src/components/tag/tag.meta.md +94 -0
  229. package/src/components/tag/tag.stories.tsx +77 -0
  230. package/src/components/tag/tag.tsx +185 -0
  231. package/src/components/textarea/textarea.meta.md +83 -0
  232. package/src/components/textarea/textarea.stories.tsx +63 -0
  233. package/src/components/textarea/textarea.tsx +113 -0
  234. package/src/components/time-picker/time-picker.meta.md +83 -0
  235. package/src/components/time-picker/time-picker.stories.tsx +59 -0
  236. package/src/components/time-picker/time-picker.tsx +94 -0
  237. package/src/components/timeline/timeline.meta.md +102 -0
  238. package/src/components/timeline/timeline.stories.tsx +104 -0
  239. package/src/components/timeline/timeline.tsx +147 -0
  240. package/src/components/toggle/toggle.meta.md +88 -0
  241. package/src/components/toggle/toggle.stories.tsx +66 -0
  242. package/src/components/toggle/toggle.tsx +53 -0
  243. package/src/components/toggle-group/toggle-group.meta.md +90 -0
  244. package/src/components/toggle-group/toggle-group.stories.tsx +83 -0
  245. package/src/components/toggle-group/toggle-group.tsx +78 -0
  246. package/src/components/tooltip/tooltip.meta.md +99 -0
  247. package/src/components/tooltip/tooltip.stories.tsx +71 -0
  248. package/src/components/tooltip/tooltip.tsx +93 -0
  249. package/src/components/tour/tour.meta.md +116 -0
  250. package/src/components/tour/tour.stories.tsx +66 -0
  251. package/src/components/tour/tour.tsx +242 -0
  252. package/src/components/transfer/transfer.meta.md +90 -0
  253. package/src/components/transfer/transfer.stories.tsx +68 -0
  254. package/src/components/transfer/transfer.tsx +251 -0
  255. package/src/components/tree/tree.meta.md +111 -0
  256. package/src/components/tree/tree.stories.tsx +109 -0
  257. package/src/components/tree/tree.tsx +367 -0
  258. package/src/components/tree-select/tree-select.meta.md +100 -0
  259. package/src/components/tree-select/tree-select.stories.tsx +80 -0
  260. package/src/components/tree-select/tree-select.tsx +171 -0
  261. package/src/components/typography/typography.meta.md +102 -0
  262. package/src/components/typography/typography.stories.tsx +115 -0
  263. package/src/components/typography/typography.tsx +245 -0
  264. package/src/components/upload/upload.meta.md +111 -0
  265. package/src/components/upload/upload.stories.tsx +75 -0
  266. package/src/components/upload/upload.tsx +265 -0
  267. package/src/components/watermark/watermark.meta.md +95 -0
  268. package/src/components/watermark/watermark.stories.tsx +78 -0
  269. package/src/components/watermark/watermark.tsx +165 -0
  270. package/src/utils/cn.ts +6 -0
@@ -0,0 +1,122 @@
1
+ import * as React from 'react';
2
+ import type { Meta, StoryObj } from '@storybook/react';
3
+ import { useForm } from 'react-hook-form';
4
+ import { zodResolver } from '@hookform/resolvers/zod';
5
+ import { z } from 'zod';
6
+ import {
7
+ Form,
8
+ FormField,
9
+ FormItem,
10
+ FormLabel,
11
+ FormControl,
12
+ FormDescription,
13
+ FormMessage,
14
+ } from './form';
15
+ import { Input } from '@/components/input/input';
16
+ import { Button } from '@/components/button/button';
17
+
18
+ const meta: Meta<typeof Form> = {
19
+ title: '表单与输入 · Form/Form',
20
+ component: Form,
21
+ tags: ['autodocs'],
22
+ parameters: {
23
+ docs: {
24
+ description: {
25
+ component:
26
+ '表单 — `react-hook-form` + `zod` 校验 + 语义化字段组件(`FormItem` / `FormLabel` / `FormControl` / `FormDescription` / `FormMessage`)。对齐 shadcn Form 的可组合范式;对比 antd Form 用 `Field` 命令式注册的方式,本组件采用 RHF 的声明式 controller,类型安全且性能更好。视觉走 OpenTrek semantic tokens,所有样式来自 `@teamix-evo/design`,无 mock。',
27
+ },
28
+ },
29
+ },
30
+ };
31
+
32
+ export default meta;
33
+ type Story = StoryObj<typeof Form>;
34
+
35
+ const schema = z.object({
36
+ username: z.string().min(2, '至少 2 个字符').max(16, '最多 16 个字符'),
37
+ email: z.string().email('请输入有效邮箱'),
38
+ bio: z.string().max(140, '不超过 140 字').optional(),
39
+ });
40
+
41
+ export const ZodValidated: Story = {
42
+ parameters: { layout: 'centered' },
43
+ render: () => {
44
+ const form = useForm<z.infer<typeof schema>>({
45
+ resolver: zodResolver(schema),
46
+ defaultValues: { username: '', email: '', bio: '' },
47
+ });
48
+ const [submitted, setSubmitted] = React.useState<unknown>(null);
49
+
50
+ return (
51
+ <div className="w-80">
52
+ <Form {...form}>
53
+ <form
54
+ onSubmit={form.handleSubmit((v) => setSubmitted(v))}
55
+ className="space-y-4"
56
+ >
57
+ <FormField
58
+ control={form.control}
59
+ name="username"
60
+ render={({ field }) => (
61
+ <FormItem>
62
+ <FormLabel required>用户名</FormLabel>
63
+ <FormControl>
64
+ <Input placeholder="lyca" {...field} />
65
+ </FormControl>
66
+ <FormDescription>2~16 个字符。</FormDescription>
67
+ <FormMessage />
68
+ </FormItem>
69
+ )}
70
+ />
71
+ <FormField
72
+ control={form.control}
73
+ name="email"
74
+ render={({ field }) => (
75
+ <FormItem>
76
+ <FormLabel required>邮箱</FormLabel>
77
+ <FormControl>
78
+ <Input
79
+ type="email"
80
+ placeholder="you@example.com"
81
+ {...field}
82
+ />
83
+ </FormControl>
84
+ <FormMessage />
85
+ </FormItem>
86
+ )}
87
+ />
88
+ <FormField
89
+ control={form.control}
90
+ name="bio"
91
+ render={({ field }) => (
92
+ <FormItem>
93
+ <FormLabel>个人简介</FormLabel>
94
+ <FormControl>
95
+ <Input placeholder="一句话介绍自己" {...field} />
96
+ </FormControl>
97
+ <FormDescription>不超过 140 字。</FormDescription>
98
+ <FormMessage />
99
+ </FormItem>
100
+ )}
101
+ />
102
+ <div className="flex gap-2">
103
+ <Button type="submit">提交</Button>
104
+ <Button
105
+ type="button"
106
+ variant="outline"
107
+ onClick={() => form.reset()}
108
+ >
109
+ 重置
110
+ </Button>
111
+ </div>
112
+ </form>
113
+ </Form>
114
+ {submitted ? (
115
+ <pre className="mt-4 rounded-md bg-muted p-3 text-xs">
116
+ {JSON.stringify(submitted, null, 2)}
117
+ </pre>
118
+ ) : null}
119
+ </div>
120
+ );
121
+ },
122
+ };
@@ -0,0 +1,194 @@
1
+ import * as React from 'react';
2
+ import * as LabelPrimitive from '@radix-ui/react-label';
3
+ import { Slot } from '@radix-ui/react-slot';
4
+ import {
5
+ Controller,
6
+ FormProvider,
7
+ useFormContext,
8
+ type ControllerProps,
9
+ type FieldPath,
10
+ type FieldValues,
11
+ type UseFormReturn,
12
+ } from 'react-hook-form';
13
+
14
+ import { cn } from '@/utils/cn';
15
+ import { Label } from '@/components/label/label';
16
+
17
+ /**
18
+ * `Form` props — 即 `useForm()` 返回值(`UseFormReturn<TFieldValues>`) 的全部方法,
19
+ * 加上 `children`。`Form` 本质是 `react-hook-form.FormProvider` 的别名;
20
+ * 该 interface 用于 Props 表生成与类型提示。
21
+ */
22
+ export interface FormProps<TFieldValues extends FieldValues = FieldValues>
23
+ extends UseFormReturn<TFieldValues> {
24
+ /**
25
+ * 表单内容 — 通常是 `<form>` + `<FormField>` 列表。
26
+ */
27
+ children?: React.ReactNode;
28
+ }
29
+
30
+ /**
31
+ * `Form` = `react-hook-form` 的 `FormProvider` 别名。
32
+ * 配合 `useForm()` 使用,把表单实例下发给所有 `FormField`。
33
+ */
34
+ const Form = FormProvider;
35
+
36
+ // ─── FormField context ────────────────────────────────────────────────────────
37
+
38
+ interface FormFieldContextValue<
39
+ TFieldValues extends FieldValues = FieldValues,
40
+ TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
41
+ > {
42
+ name: TName;
43
+ }
44
+
45
+ const FormFieldContext = React.createContext<FormFieldContextValue>(
46
+ {} as FormFieldContextValue,
47
+ );
48
+
49
+ /**
50
+ * `FormField` = `Controller` 包装,把 `name` 注入下层的 useFormField hook。
51
+ * 用法:`<FormField name="email" control={form.control} render={({ field }) => <FormItem>...</FormItem>} />`
52
+ */
53
+ function FormField<
54
+ TFieldValues extends FieldValues = FieldValues,
55
+ TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
56
+ >({ ...props }: ControllerProps<TFieldValues, TName>) {
57
+ return (
58
+ <FormFieldContext.Provider value={{ name: props.name }}>
59
+ <Controller {...props} />
60
+ </FormFieldContext.Provider>
61
+ );
62
+ }
63
+
64
+ // ─── FormItem context ─────────────────────────────────────────────────────────
65
+
66
+ interface FormItemContextValue {
67
+ id: string;
68
+ }
69
+
70
+ const FormItemContext = React.createContext<FormItemContextValue>(
71
+ {} as FormItemContextValue,
72
+ );
73
+
74
+ /**
75
+ * `FormItem` 渲染单个表单字段容器,生成稳定 id 给 Label/Description/Message 关联。
76
+ */
77
+ const FormItem = React.forwardRef<
78
+ HTMLDivElement,
79
+ React.HTMLAttributes<HTMLDivElement>
80
+ >(({ className, ...props }, ref) => {
81
+ const id = React.useId();
82
+ return (
83
+ <FormItemContext.Provider value={{ id }}>
84
+ <div ref={ref} className={cn('space-y-2', className)} {...props} />
85
+ </FormItemContext.Provider>
86
+ );
87
+ });
88
+ FormItem.displayName = 'FormItem';
89
+
90
+ /**
91
+ * 组合 FormField + FormItem 的字段元数据,用于 Label / Control / Description / Message。
92
+ */
93
+ const useFormField = () => {
94
+ const fieldContext = React.useContext(FormFieldContext);
95
+ const itemContext = React.useContext(FormItemContext);
96
+ const { getFieldState, formState } = useFormContext();
97
+ const fieldState = getFieldState(fieldContext.name, formState);
98
+
99
+ if (!fieldContext) {
100
+ throw new Error('useFormField should be used within <FormField>');
101
+ }
102
+ const { id } = itemContext;
103
+ return {
104
+ id,
105
+ name: fieldContext.name,
106
+ formItemId: `${id}-form-item`,
107
+ formDescriptionId: `${id}-form-item-description`,
108
+ formMessageId: `${id}-form-item-message`,
109
+ ...fieldState,
110
+ };
111
+ };
112
+
113
+ const FormLabel = React.forwardRef<
114
+ React.ElementRef<typeof LabelPrimitive.Root>,
115
+ React.ComponentPropsWithoutRef<typeof Label>
116
+ >(({ className, ...props }, ref) => {
117
+ const { error, formItemId } = useFormField();
118
+ return (
119
+ <Label
120
+ ref={ref}
121
+ className={cn(error && 'text-destructive', className)}
122
+ htmlFor={formItemId}
123
+ {...props}
124
+ />
125
+ );
126
+ });
127
+ FormLabel.displayName = 'FormLabel';
128
+
129
+ const FormControl = React.forwardRef<
130
+ React.ElementRef<typeof Slot>,
131
+ React.ComponentPropsWithoutRef<typeof Slot>
132
+ >(({ ...props }, ref) => {
133
+ const { error, formItemId, formDescriptionId, formMessageId } =
134
+ useFormField();
135
+ return (
136
+ <Slot
137
+ ref={ref}
138
+ id={formItemId}
139
+ aria-describedby={
140
+ error ? `${formDescriptionId} ${formMessageId}` : `${formDescriptionId}`
141
+ }
142
+ aria-invalid={!!error}
143
+ {...props}
144
+ />
145
+ );
146
+ });
147
+ FormControl.displayName = 'FormControl';
148
+
149
+ const FormDescription = React.forwardRef<
150
+ HTMLParagraphElement,
151
+ React.HTMLAttributes<HTMLParagraphElement>
152
+ >(({ className, ...props }, ref) => {
153
+ const { formDescriptionId } = useFormField();
154
+ return (
155
+ <p
156
+ ref={ref}
157
+ id={formDescriptionId}
158
+ className={cn('text-xs text-muted-foreground', className)}
159
+ {...props}
160
+ />
161
+ );
162
+ });
163
+ FormDescription.displayName = 'FormDescription';
164
+
165
+ const FormMessage = React.forwardRef<
166
+ HTMLParagraphElement,
167
+ React.HTMLAttributes<HTMLParagraphElement>
168
+ >(({ className, children, ...props }, ref) => {
169
+ const { error, formMessageId } = useFormField();
170
+ const body = error ? String(error?.message ?? '') : children;
171
+ if (!body) return null;
172
+ return (
173
+ <p
174
+ ref={ref}
175
+ id={formMessageId}
176
+ className={cn('text-xs font-medium text-destructive', className)}
177
+ {...props}
178
+ >
179
+ {body}
180
+ </p>
181
+ );
182
+ });
183
+ FormMessage.displayName = 'FormMessage';
184
+
185
+ export {
186
+ useFormField,
187
+ Form,
188
+ FormItem,
189
+ FormLabel,
190
+ FormControl,
191
+ FormDescription,
192
+ FormMessage,
193
+ FormField,
194
+ };
@@ -0,0 +1,87 @@
1
+ ---
2
+ id: grid
3
+ name: Grid
4
+ type: component
5
+ category: layout
6
+ since: 0.1.0
7
+ package: "@teamix-evo/ui"
8
+ ---
9
+
10
+ # Grid (Row / Col)
11
+
12
+ 24 栅格系统 — antd 独有补足。**等价 antd `Row` + `Col`**:基于 CSS Grid 的 24 列容器,支持 `span` / `offset` / `order` / `flex` / `gutter` / `justify` / `align` 等核心 API,中后台表单 / 看板布局基石。
13
+
14
+ ## When to use
15
+
16
+ - 中后台多列表单(`<Col span={12}>` 二等分,`<Col span={8}>` 三等分)
17
+ - 数据看板的卡片网格(配 `gutter` 控制水平 / 垂直间距)
18
+ - 响应式布局(配业务侧 Tailwind 断点 className,如 `className="md:col-span-12 col-span-24"`)
19
+
20
+ ## When NOT to use
21
+
22
+ - 简单 flex 容器 → `Flex`
23
+ - inline 间距小集合 → `Space`
24
+ - 不固定列数的卡片墙 → 直接用 Tailwind `grid-cols-N` 或 `Item` 配 `ItemGroup`
25
+
26
+ <!-- auto:props:begin -->
27
+ _(组件无 `<Name>Props` interface — props 详见 [`grid.tsx`](./grid.tsx))_
28
+ <!-- auto:props:end -->
29
+
30
+ <!-- auto:deps:begin -->
31
+ ### 同库依赖
32
+
33
+ > `teamix-evo ui add grid` 时,以下 entry 会被自动连带安装(无需手动 add)。
34
+
35
+ | Entry | 类型 | 描述 |
36
+ | --- | --- | --- |
37
+ | `cn` | util | Tailwind className 合并工具(clsx + tailwind-merge) |
38
+
39
+ ### npm 依赖
40
+
41
+ _无 — 本组件不依赖任何 npm 包。_
42
+ <!-- auto:deps:end -->
43
+
44
+ ## AI 生成纪律
45
+
46
+ - **`span` 默认 24**(占满一行) — 不传等于"独占整行"
47
+ - **`gutter` 走 design 间距刻度**:接受 number(等价 `[n, n]`)或 `[h, v]`,实际值就是 `n * 4px`(Tailwind gap-N 同源)
48
+ - **`offset`** 用百分比定位,不要超过 24 - span(否则越界)
49
+ - **`flex={true}`** 等价 antd `flex="auto"` — 撑满剩余空间;启用后 `span` 失效
50
+ - **`span={0}` 隐藏**(antd 兼容行为) — 用 `0` 隐藏比 `display: none` 语义更明确
51
+ - **响应式**:本组件不内置 antd 的 `xs/sm/md/lg/xl/xxl` 断点 prop — 业务侧请用 Tailwind `md:col-span-12` 等响应式 className(更原生、更可控)
52
+ - **不要嵌套 Row 表达卡片网格** — 那是 `Item` + Tailwind grid 的活
53
+
54
+ ## Examples
55
+
56
+ ```tsx
57
+ import { Row, Col } from '@/components/ui/grid';
58
+
59
+ // 二等分表单
60
+ <Row gutter={4}>
61
+ <Col span={12}>
62
+ <Field><FieldLabel>姓</FieldLabel><Input /></Field>
63
+ </Col>
64
+ <Col span={12}>
65
+ <Field><FieldLabel>名</FieldLabel><Input /></Field>
66
+ </Col>
67
+ </Row>
68
+
69
+ // 8-8-8 三等分
70
+ <Row gutter={[4, 4]}>
71
+ <Col span={8}><Card /></Col>
72
+ <Col span={8}><Card /></Col>
73
+ <Col span={8}><Card /></Col>
74
+ </Row>
75
+
76
+ // 主区 + 侧边(主区 flex 撑满)
77
+ <Row gutter={4}>
78
+ <Col flex><MainContent /></Col>
79
+ <Col span={6}><Sidebar /></Col>
80
+ </Row>
81
+
82
+ // 偏移
83
+ <Row gutter={4}>
84
+ <Col span={8} offset={4}>居中的 8 列</Col>
85
+ <Col span={8} offset={4}>另一个</Col>
86
+ </Row>
87
+ ```
@@ -0,0 +1,99 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { Row, Col } from './grid';
3
+
4
+ const meta: Meta<typeof Row> = {
5
+ title: '布局与容器 · Layout/Grid',
6
+ component: Row,
7
+ tags: ['autodocs'],
8
+ parameters: {
9
+ docs: {
10
+ description: {
11
+ component:
12
+ '24 栅格 — 基于 CSS Grid 的 24 列容器(antd Row + Col)。中后台表单 / 看板布局基石,支持 span / offset / order / flex / gutter / justify / align。响应式建议直接用 Tailwind 断点 className(`md:col-span-12`),更原生可控。视觉走 OpenTrek tokens,所有样式来自 `@teamix-evo/design`,无 mock。',
13
+ },
14
+ },
15
+ },
16
+ };
17
+
18
+ export default meta;
19
+ type Story = StoryObj<typeof Row>;
20
+
21
+ const Box = ({ children }: { children: React.ReactNode }) => (
22
+ <div className="rounded-md bg-muted px-3 py-4 text-center text-sm">{children}</div>
23
+ );
24
+
25
+ export const Playground: Story = {
26
+ parameters: { controls: { disable: true } },
27
+ render: () => (
28
+ <Row gutter={4}>
29
+ <Col span={12}>
30
+ <Box>span = 12</Box>
31
+ </Col>
32
+ <Col span={12}>
33
+ <Box>span = 12</Box>
34
+ </Col>
35
+ </Row>
36
+ ),
37
+ };
38
+
39
+ export const Thirds: Story = {
40
+ parameters: { controls: { disable: true } },
41
+ render: () => (
42
+ <Row gutter={[4, 4]}>
43
+ <Col span={8}>
44
+ <Box>span = 8</Box>
45
+ </Col>
46
+ <Col span={8}>
47
+ <Box>span = 8</Box>
48
+ </Col>
49
+ <Col span={8}>
50
+ <Box>span = 8</Box>
51
+ </Col>
52
+ </Row>
53
+ ),
54
+ };
55
+
56
+ export const WithFlex: Story = {
57
+ parameters: { controls: { disable: true } },
58
+ render: () => (
59
+ <Row gutter={4}>
60
+ <Col flex>
61
+ <Box>flex(撑满)</Box>
62
+ </Col>
63
+ <Col span={6}>
64
+ <Box>span = 6 sidebar</Box>
65
+ </Col>
66
+ </Row>
67
+ ),
68
+ };
69
+
70
+ export const Offset: Story = {
71
+ parameters: { controls: { disable: true } },
72
+ render: () => (
73
+ <Row gutter={4}>
74
+ <Col span={8} offset={4}>
75
+ <Box>span=8 offset=4</Box>
76
+ </Col>
77
+ <Col span={8} offset={4}>
78
+ <Box>span=8 offset=4</Box>
79
+ </Col>
80
+ </Row>
81
+ ),
82
+ };
83
+
84
+ export const JustifyCenter: Story = {
85
+ parameters: { controls: { disable: true } },
86
+ render: () => (
87
+ <Row gutter={4} justify="center">
88
+ <Col span={6}>
89
+ <Box>1</Box>
90
+ </Col>
91
+ <Col span={6}>
92
+ <Box>2</Box>
93
+ </Col>
94
+ <Col span={6}>
95
+ <Box>3</Box>
96
+ </Col>
97
+ </Row>
98
+ ),
99
+ };
@@ -0,0 +1,130 @@
1
+ import * as React from 'react';
2
+
3
+ import { cn } from '@/utils/cn';
4
+
5
+ /**
6
+ * antd 24 栅格的端口实现。底层使用 CSS Grid + 24 column,与 antd `Row` / `Col` 语义对齐。
7
+ * `span` / `offset` 接受 1~24 整数;`gutter` 是 `[水平, 垂直]` 间距(任选传 number 等价 `[n, n]`)。
8
+ */
9
+
10
+ const justifyMap = {
11
+ start: 'justify-start',
12
+ center: 'justify-center',
13
+ end: 'justify-end',
14
+ 'space-between': 'justify-between',
15
+ 'space-around': 'justify-around',
16
+ 'space-evenly': 'justify-evenly',
17
+ } as const;
18
+
19
+ const alignMap = {
20
+ top: 'items-start',
21
+ middle: 'items-center',
22
+ bottom: 'items-end',
23
+ stretch: 'items-stretch',
24
+ } as const;
25
+
26
+ export interface RowProps extends React.HTMLAttributes<HTMLDivElement> {
27
+ /**
28
+ * 子项间距 — number 等价 `[n, n]`;`[h, v]` 分别控制水平 / 垂直。
29
+ * 注意:走 design 间距刻度,实际渲染值就是 `n * 4px`(Tailwind gap-N)。
30
+ * @default 0
31
+ */
32
+ gutter?: number | [number, number];
33
+ /**
34
+ * 水平排列(antd `justify` 并集)。
35
+ * @default "start"
36
+ */
37
+ justify?: keyof typeof justifyMap;
38
+ /**
39
+ * 垂直对齐(antd `align` 并集)。
40
+ * @default "top"
41
+ */
42
+ align?: keyof typeof alignMap;
43
+ /**
44
+ * 是否允许换行(antd `wrap` 并集)。
45
+ * @default true
46
+ */
47
+ wrap?: boolean;
48
+ }
49
+
50
+ const Row = React.forwardRef<HTMLDivElement, RowProps>(
51
+ (
52
+ {
53
+ gutter = 0,
54
+ justify = 'start',
55
+ align = 'top',
56
+ wrap = true,
57
+ className,
58
+ children,
59
+ style,
60
+ ...props
61
+ },
62
+ ref,
63
+ ) => {
64
+ const [hGutter, vGutter] =
65
+ Array.isArray(gutter) ? gutter : [gutter, gutter];
66
+ return (
67
+ <div
68
+ ref={ref}
69
+ className={cn('grid', justifyMap[justify], alignMap[align], className)}
70
+ style={{
71
+ gridTemplateColumns: 'repeat(24, minmax(0, 1fr))',
72
+ columnGap: `${hGutter * 4}px`,
73
+ rowGap: `${vGutter * 4}px`,
74
+ ...style,
75
+ }}
76
+ {...props}
77
+ >
78
+ {children}
79
+ </div>
80
+ );
81
+ },
82
+ );
83
+ Row.displayName = 'Row';
84
+
85
+ export interface ColProps extends React.HTMLAttributes<HTMLDivElement> {
86
+ /**
87
+ * 占用栅格数(1~24,antd `span` 并集) — 0 表示隐藏(对齐 antd 行为)。
88
+ * @default 24
89
+ */
90
+ span?: number;
91
+ /**
92
+ * 左侧偏移栅格数(antd `offset` 并集)。
93
+ * @default 0
94
+ */
95
+ offset?: number;
96
+ /**
97
+ * 顺序(antd `order` 并集) — 用于响应式 / a11y 顺序变更场景。
98
+ */
99
+ order?: number;
100
+ /**
101
+ * 自动撑满剩余空间(antd `flex="auto"` 等价) — 设置后忽略 span。
102
+ * @default false
103
+ */
104
+ flex?: boolean;
105
+ }
106
+
107
+ const Col = React.forwardRef<HTMLDivElement, ColProps>(
108
+ ({ span = 24, offset = 0, order, flex = false, className, style, ...props }, ref) => {
109
+ if (span === 0) return null;
110
+ const computedStyle: React.CSSProperties = { ...style };
111
+ if (flex) {
112
+ computedStyle.flex = '1 1 0%';
113
+ } else {
114
+ computedStyle.gridColumn = `span ${Math.max(1, Math.min(24, span))} / span ${Math.max(1, Math.min(24, span))}`;
115
+ if (offset > 0) computedStyle.marginLeft = `${(offset / 24) * 100}%`;
116
+ }
117
+ if (typeof order === 'number') computedStyle.order = order;
118
+ return (
119
+ <div
120
+ ref={ref}
121
+ className={cn('min-w-0', className)}
122
+ style={computedStyle}
123
+ {...props}
124
+ />
125
+ );
126
+ },
127
+ );
128
+ Col.displayName = 'Col';
129
+
130
+ export { Row, Col };