@teamix-evo/ui 0.1.1 → 0.3.0

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 (295) hide show
  1. package/README.md +184 -184
  2. package/manifest.json +680 -492
  3. package/package.json +20 -10
  4. package/src/components/accordion/accordion.meta.md +5 -4
  5. package/src/components/accordion/accordion.stories.tsx +14 -9
  6. package/src/components/accordion/accordion.tsx +104 -8
  7. package/src/components/affix/affix.meta.md +20 -2
  8. package/src/components/affix/affix.stories.tsx +102 -25
  9. package/src/components/affix/affix.tsx +79 -9
  10. package/src/components/alert/alert.meta.md +44 -13
  11. package/src/components/alert/alert.stories.tsx +66 -21
  12. package/src/components/alert/alert.tsx +81 -34
  13. package/src/components/alert-dialog/alert-dialog.meta.md +61 -16
  14. package/src/components/alert-dialog/alert-dialog.stories.tsx +145 -3
  15. package/src/components/alert-dialog/alert-dialog.tsx +60 -13
  16. package/src/components/anchor/anchor.meta.md +8 -3
  17. package/src/components/anchor/anchor.stories.tsx +3 -3
  18. package/src/components/anchor/anchor.tsx +2 -2
  19. package/src/components/app/app.meta.md +9 -4
  20. package/src/components/app/app.stories.tsx +9 -7
  21. package/src/components/aspect-ratio/aspect-ratio.meta.md +4 -3
  22. package/src/components/aspect-ratio/aspect-ratio.stories.tsx +3 -3
  23. package/src/components/auto-complete/auto-complete.meta.md +14 -6
  24. package/src/components/auto-complete/auto-complete.stories.tsx +47 -4
  25. package/src/components/auto-complete/auto-complete.tsx +119 -71
  26. package/src/components/avatar/avatar.meta.md +6 -7
  27. package/src/components/avatar/avatar.stories.tsx +21 -3
  28. package/src/components/avatar/avatar.tsx +24 -23
  29. package/src/components/badge/badge.meta.md +10 -9
  30. package/src/components/badge/badge.stories.tsx +2 -2
  31. package/src/components/badge/badge.tsx +9 -15
  32. package/src/components/breadcrumb/breadcrumb.meta.md +27 -7
  33. package/src/components/breadcrumb/breadcrumb.stories.tsx +127 -4
  34. package/src/components/breadcrumb/breadcrumb.tsx +22 -8
  35. package/src/components/button/button.meta.md +258 -21
  36. package/src/components/button/button.stories.tsx +549 -41
  37. package/src/components/button/button.tsx +335 -33
  38. package/src/components/button/demo/as-child.tsx +24 -0
  39. package/src/components/button/demo/basic.tsx +8 -0
  40. package/src/components/button/demo/block.tsx +16 -0
  41. package/src/components/button/demo/loading.tsx +19 -0
  42. package/src/components/button/demo/shapes.tsx +18 -0
  43. package/src/components/button/demo/sizes.tsx +19 -0
  44. package/src/components/button/demo/variants.tsx +19 -0
  45. package/src/components/button/demo/with-icon.tsx +20 -0
  46. package/src/components/calendar/calendar.meta.md +13 -3
  47. package/src/components/calendar/calendar.stories.tsx +6 -6
  48. package/src/components/calendar/calendar.tsx +73 -8
  49. package/src/components/card/card.meta.md +27 -5
  50. package/src/components/card/card.stories.tsx +42 -3
  51. package/src/components/card/card.tsx +146 -63
  52. package/src/components/carousel/carousel.meta.md +4 -3
  53. package/src/components/carousel/carousel.stories.tsx +11 -6
  54. package/src/components/cascader/cascader.meta.md +47 -17
  55. package/src/components/cascader/cascader.stories.tsx +22 -10
  56. package/src/components/cascader/cascader.tsx +428 -85
  57. package/src/components/checkbox/checkbox.meta.md +75 -7
  58. package/src/components/checkbox/checkbox.stories.tsx +161 -3
  59. package/src/components/checkbox/checkbox.tsx +77 -9
  60. package/src/components/collapsible/collapsible.meta.md +14 -6
  61. package/src/components/collapsible/collapsible.stories.tsx +10 -2
  62. package/src/components/collapsible/collapsible.tsx +93 -6
  63. package/src/components/color-picker/color-picker.meta.md +12 -7
  64. package/src/components/color-picker/color-picker.stories.tsx +86 -7
  65. package/src/components/color-picker/color-picker.tsx +20 -9
  66. package/src/components/command/command.meta.md +29 -13
  67. package/src/components/command/command.stories.tsx +4 -4
  68. package/src/components/command/command.tsx +19 -8
  69. package/src/components/context-menu/context-menu.meta.md +11 -8
  70. package/src/components/context-menu/context-menu.stories.tsx +11 -3
  71. package/src/components/context-menu/context-menu.tsx +21 -8
  72. package/src/components/data-table/data-table.meta.md +6 -5
  73. package/src/components/data-table/data-table.stories.tsx +13 -6
  74. package/src/components/data-table/data-table.tsx +2 -2
  75. package/src/components/date-picker/date-picker.meta.md +88 -19
  76. package/src/components/date-picker/date-picker.stories.tsx +55 -5
  77. package/src/components/date-picker/date-picker.tsx +1489 -91
  78. package/src/components/descriptions/descriptions.meta.md +10 -5
  79. package/src/components/descriptions/descriptions.stories.tsx +3 -3
  80. package/src/components/descriptions/descriptions.tsx +22 -14
  81. package/src/components/dialog/dialog.meta.md +76 -13
  82. package/src/components/dialog/dialog.stories.tsx +182 -20
  83. package/src/components/dialog/dialog.tsx +67 -15
  84. package/src/components/dialog/imperative.tsx +252 -0
  85. package/src/components/drawer/drawer.meta.md +33 -34
  86. package/src/components/drawer/drawer.stories.tsx +29 -12
  87. package/src/components/drawer/drawer.tsx +22 -113
  88. package/src/components/dropdown-menu/dropdown-menu.meta.md +78 -10
  89. package/src/components/dropdown-menu/dropdown-menu.stories.tsx +88 -2
  90. package/src/components/dropdown-menu/dropdown-menu.tsx +24 -10
  91. package/src/components/ellipsis/ellipsis.meta.md +87 -0
  92. package/src/components/ellipsis/ellipsis.stories.tsx +72 -0
  93. package/src/components/ellipsis/ellipsis.tsx +153 -0
  94. package/src/components/empty/empty.meta.md +9 -4
  95. package/src/components/empty/empty.stories.tsx +4 -4
  96. package/src/components/empty/empty.tsx +10 -3
  97. package/src/components/field/field.meta.md +47 -9
  98. package/src/components/field/field.stories.tsx +385 -5
  99. package/src/components/field/field.tsx +263 -35
  100. package/src/components/filter-bar/filter-bar.meta.md +92 -0
  101. package/src/components/filter-bar/filter-bar.stories.tsx +1083 -0
  102. package/src/components/filter-bar/filter-bar.tsx +568 -0
  103. package/src/components/flex/flex.meta.md +54 -6
  104. package/src/components/flex/flex.stories.tsx +107 -20
  105. package/src/components/flex/flex.tsx +27 -4
  106. package/src/components/float-button/float-button.meta.md +8 -3
  107. package/src/components/float-button/float-button.stories.tsx +9 -7
  108. package/src/components/float-button/float-button.tsx +1 -1
  109. package/src/components/form/form.meta.md +39 -17
  110. package/src/components/form/form.stories.tsx +350 -3
  111. package/src/components/form/form.tsx +101 -35
  112. package/src/components/grid/grid.meta.md +7 -2
  113. package/src/components/grid/grid.stories.tsx +6 -4
  114. package/src/components/hover-card/hover-card.meta.md +20 -9
  115. package/src/components/hover-card/hover-card.stories.tsx +34 -5
  116. package/src/components/hover-card/hover-card.tsx +51 -13
  117. package/src/components/icon/DEVELOPMENT.md +809 -0
  118. package/src/components/icon/icon.meta.md +170 -0
  119. package/src/components/icon/icon.stories.tsx +344 -0
  120. package/src/components/icon/icon.tsx +248 -0
  121. package/src/components/image/image.meta.md +9 -4
  122. package/src/components/image/image.stories.tsx +3 -3
  123. package/src/components/image/image.tsx +6 -4
  124. package/src/components/input/demo/basic.tsx +12 -0
  125. package/src/components/input/demo/clearable.tsx +21 -0
  126. package/src/components/input/demo/show-count.tsx +18 -0
  127. package/src/components/input/demo/sizes.tsx +15 -0
  128. package/src/components/input/input.meta.md +39 -33
  129. package/src/components/input/input.stories.tsx +62 -35
  130. package/src/components/input/input.tsx +97 -98
  131. package/src/components/input-group/input-group.meta.md +54 -22
  132. package/src/components/input-group/input-group.stories.tsx +49 -16
  133. package/src/components/input-group/input-group.tsx +44 -8
  134. package/src/components/input-number/input-number.meta.md +64 -7
  135. package/src/components/input-number/input-number.stories.tsx +46 -8
  136. package/src/components/input-number/input-number.tsx +99 -26
  137. package/src/components/input-otp/input-otp.meta.md +4 -3
  138. package/src/components/input-otp/input-otp.stories.tsx +3 -3
  139. package/src/components/input-otp/input-otp.tsx +1 -1
  140. package/src/components/item/item.meta.md +8 -3
  141. package/src/components/item/item.stories.tsx +8 -5
  142. package/src/components/item/item.tsx +7 -6
  143. package/src/components/kbd/kbd.meta.md +13 -4
  144. package/src/components/kbd/kbd.stories.tsx +4 -4
  145. package/src/components/kbd/kbd.tsx +10 -5
  146. package/src/components/label/label.meta.md +18 -10
  147. package/src/components/label/label.stories.tsx +64 -6
  148. package/src/components/label/label.tsx +91 -19
  149. package/src/components/masonry/masonry.meta.md +8 -3
  150. package/src/components/masonry/masonry.stories.tsx +7 -5
  151. package/src/components/masonry/masonry.tsx +1 -0
  152. package/src/components/mentions/mentions.meta.md +36 -6
  153. package/src/components/mentions/mentions.stories.tsx +120 -6
  154. package/src/components/mentions/mentions.tsx +11 -5
  155. package/src/components/menubar/menubar.meta.md +30 -12
  156. package/src/components/menubar/menubar.stories.tsx +62 -2
  157. package/src/components/menubar/menubar.tsx +9 -9
  158. package/src/components/native-select/native-select.meta.md +8 -3
  159. package/src/components/native-select/native-select.stories.tsx +8 -5
  160. package/src/components/native-select/native-select.tsx +1 -1
  161. package/src/components/navigation-menu/navigation-menu.meta.md +19 -9
  162. package/src/components/navigation-menu/navigation-menu.stories.tsx +112 -9
  163. package/src/components/navigation-menu/navigation-menu.tsx +8 -4
  164. package/src/components/notification/notification.meta.md +52 -10
  165. package/src/components/notification/notification.stories.tsx +11 -9
  166. package/src/components/notification/notification.tsx +36 -21
  167. package/src/components/page-header/DEVELOPMENT.md +842 -0
  168. package/src/components/page-header/page-header.meta.md +208 -0
  169. package/src/components/page-header/page-header.stories.tsx +421 -0
  170. package/src/components/page-header/page-header.tsx +281 -0
  171. package/src/components/pagination/pagination.meta.md +140 -37
  172. package/src/components/pagination/pagination.stories.tsx +232 -10
  173. package/src/components/pagination/pagination.tsx +355 -63
  174. package/src/components/popconfirm/popconfirm.meta.md +9 -4
  175. package/src/components/popconfirm/popconfirm.stories.tsx +3 -4
  176. package/src/components/popconfirm/popconfirm.tsx +2 -2
  177. package/src/components/popover/popover.meta.md +62 -5
  178. package/src/components/popover/popover.stories.tsx +83 -7
  179. package/src/components/popover/popover.tsx +77 -28
  180. package/src/components/progress/progress.meta.md +38 -6
  181. package/src/components/progress/progress.stories.tsx +3 -3
  182. package/src/components/progress/progress.tsx +24 -16
  183. package/src/components/radio-group/radio-group.meta.md +79 -7
  184. package/src/components/radio-group/radio-group.stories.tsx +39 -3
  185. package/src/components/radio-group/radio-group.tsx +149 -18
  186. package/src/components/rate/rate.meta.md +35 -4
  187. package/src/components/rate/rate.stories.tsx +13 -5
  188. package/src/components/rate/rate.tsx +37 -10
  189. package/src/components/resizable/resizable.meta.md +7 -4
  190. package/src/components/resizable/resizable.stories.tsx +6 -6
  191. package/src/components/resizable/resizable.tsx +1 -1
  192. package/src/components/result/result.meta.md +7 -2
  193. package/src/components/result/result.stories.tsx +4 -8
  194. package/src/components/result/result.tsx +24 -15
  195. package/src/components/scroll-area/scroll-area.meta.md +4 -3
  196. package/src/components/scroll-area/scroll-area.stories.tsx +12 -4
  197. package/src/components/scroll-area/scroll-area.tsx +3 -3
  198. package/src/components/segmented/segmented.meta.md +7 -4
  199. package/src/components/segmented/segmented.stories.tsx +37 -8
  200. package/src/components/segmented/segmented.tsx +15 -7
  201. package/src/components/select/select.meta.md +197 -52
  202. package/src/components/select/select.stories.tsx +238 -63
  203. package/src/components/select/select.tsx +718 -171
  204. package/src/components/separator/separator.meta.md +4 -3
  205. package/src/components/separator/separator.stories.tsx +3 -3
  206. package/src/components/separator/separator.tsx +3 -7
  207. package/src/components/sheet/sheet.meta.md +32 -16
  208. package/src/components/sheet/sheet.stories.tsx +116 -10
  209. package/src/components/sheet/sheet.tsx +116 -29
  210. package/src/components/sidebar/sidebar.meta.md +37 -18
  211. package/src/components/sidebar/sidebar.stories.tsx +701 -29
  212. package/src/components/sidebar/sidebar.tsx +615 -142
  213. package/src/components/skeleton/skeleton.meta.md +4 -5
  214. package/src/components/skeleton/skeleton.stories.tsx +4 -4
  215. package/src/components/skeleton/skeleton.tsx +7 -7
  216. package/src/components/slider/slider.meta.md +57 -5
  217. package/src/components/slider/slider.stories.tsx +58 -6
  218. package/src/components/slider/slider.tsx +154 -13
  219. package/src/components/sonner/sonner.meta.md +58 -7
  220. package/src/components/sonner/sonner.stories.tsx +78 -5
  221. package/src/components/sonner/sonner.tsx +137 -8
  222. package/src/components/spinner/spinner.meta.md +62 -13
  223. package/src/components/spinner/spinner.stories.tsx +66 -14
  224. package/src/components/spinner/spinner.tsx +111 -9
  225. package/src/components/statistic/statistic.meta.md +7 -2
  226. package/src/components/statistic/statistic.stories.tsx +3 -7
  227. package/src/components/statistic/statistic.tsx +5 -6
  228. package/src/components/steps/steps.meta.md +18 -4
  229. package/src/components/steps/steps.stories.tsx +43 -3
  230. package/src/components/steps/steps.tsx +15 -12
  231. package/src/components/switch/switch.meta.md +51 -5
  232. package/src/components/switch/switch.stories.tsx +6 -6
  233. package/src/components/switch/switch.tsx +109 -41
  234. package/src/components/table/table.meta.md +17 -6
  235. package/src/components/table/table.stories.tsx +10 -5
  236. package/src/components/table/table.tsx +4 -4
  237. package/src/components/tabs/tabs.meta.md +38 -25
  238. package/src/components/tabs/tabs.stories.tsx +111 -25
  239. package/src/components/tabs/tabs.tsx +125 -54
  240. package/src/components/tag/tag.meta.md +105 -40
  241. package/src/components/tag/tag.stories.tsx +189 -16
  242. package/src/components/tag/tag.tsx +222 -21
  243. package/src/components/textarea/textarea.meta.md +35 -19
  244. package/src/components/textarea/textarea.stories.tsx +32 -6
  245. package/src/components/textarea/textarea.tsx +33 -9
  246. package/src/components/time-picker/time-picker.meta.md +124 -32
  247. package/src/components/time-picker/time-picker.stories.tsx +85 -15
  248. package/src/components/time-picker/time-picker.tsx +913 -61
  249. package/src/components/timeline/timeline.meta.md +14 -6
  250. package/src/components/timeline/timeline.stories.tsx +37 -7
  251. package/src/components/timeline/timeline.tsx +35 -14
  252. package/src/components/toggle/toggle.meta.md +5 -4
  253. package/src/components/toggle/toggle.stories.tsx +4 -4
  254. package/src/components/toggle/toggle.tsx +4 -3
  255. package/src/components/toggle-group/toggle-group.meta.md +5 -4
  256. package/src/components/toggle-group/toggle-group.stories.tsx +3 -3
  257. package/src/components/toggle-group/toggle-group.tsx +2 -2
  258. package/src/components/tooltip/tooltip.meta.md +55 -5
  259. package/src/components/tooltip/tooltip.stories.tsx +42 -5
  260. package/src/components/tooltip/tooltip.tsx +81 -21
  261. package/src/components/tour/tour.meta.md +9 -4
  262. package/src/components/tour/tour.stories.tsx +3 -3
  263. package/src/components/tour/tour.tsx +4 -4
  264. package/src/components/transfer/transfer.meta.md +11 -6
  265. package/src/components/transfer/transfer.stories.tsx +4 -8
  266. package/src/components/transfer/transfer.tsx +28 -21
  267. package/src/components/tree/tree.meta.md +63 -5
  268. package/src/components/tree/tree.stories.tsx +31 -12
  269. package/src/components/tree/tree.tsx +9 -8
  270. package/src/components/tree-select/tree-select.meta.md +59 -8
  271. package/src/components/tree-select/tree-select.stories.tsx +3 -3
  272. package/src/components/tree-select/tree-select.tsx +42 -7
  273. package/src/components/typography/typography.meta.md +61 -14
  274. package/src/components/typography/typography.stories.tsx +12 -11
  275. package/src/components/typography/typography.tsx +43 -28
  276. package/src/components/upload/upload.meta.md +49 -4
  277. package/src/components/upload/upload.stories.tsx +72 -12
  278. package/src/components/upload/upload.tsx +170 -37
  279. package/src/components/watermark/watermark.meta.md +7 -2
  280. package/src/components/watermark/watermark.stories.tsx +101 -9
  281. package/src/components/watermark/watermark.tsx +1 -0
  282. package/src/hooks/use-breakpoint.ts +117 -0
  283. package/src/hooks/use-debounce-callback.ts +52 -0
  284. package/src/hooks/use-mobile.ts +23 -0
  285. package/src/stories/theme-tokens.stories.tsx +747 -0
  286. package/src/utils/trigger-input.ts +53 -0
  287. package/src/components/button-group/button-group.meta.md +0 -92
  288. package/src/components/button-group/button-group.stories.tsx +0 -90
  289. package/src/components/button-group/button-group.tsx +0 -75
  290. package/src/components/combobox/combobox.meta.md +0 -93
  291. package/src/components/combobox/combobox.stories.tsx +0 -55
  292. package/src/components/combobox/combobox.tsx +0 -130
  293. package/src/components/space/space.meta.md +0 -94
  294. package/src/components/space/space.stories.tsx +0 -94
  295. package/src/components/space/space.tsx +0 -106
@@ -3,6 +3,16 @@ import { cva, type VariantProps } from 'class-variance-authority';
3
3
 
4
4
  import { cn } from '@/utils/cn';
5
5
  import { Label } from '@/components/label/label';
6
+ import {
7
+ useBreakpoint,
8
+ pickByBreakpoint,
9
+ type ResponsiveValue,
10
+ } from '@/hooks/use-breakpoint';
11
+
12
+ // ─── 类型 ──────────────────────────────────────────────────────────────
13
+
14
+ /** 输入域宽度档位 — 5 档系统,对应 104 / 216 / 328 / 440 / 552 px。 */
15
+ export type InputWidth = 'xs' | 's' | 'm' | 'l' | 'xl';
6
16
 
7
17
  // ─── FieldSet / FieldLegend(分组的语义边框)────────────────────────────
8
18
 
@@ -13,7 +23,7 @@ const FieldSet = React.forwardRef<
13
23
  <fieldset
14
24
  ref={ref}
15
25
  className={cn(
16
- 'flex flex-col gap-4 rounded-md border bg-card p-4 disabled:cursor-not-allowed disabled:opacity-60',
26
+ 'flex flex-col gap-4 rounded-md border border-border bg-card p-4 disabled:cursor-not-allowed disabled:opacity-60',
17
27
  className,
18
28
  )}
19
29
  {...props}
@@ -27,7 +37,7 @@ const FieldLegend = React.forwardRef<
27
37
  >(({ className, ...props }, ref) => (
28
38
  <legend
29
39
  ref={ref}
30
- className={cn('px-1 text-sm font-semibold leading-none', className)}
40
+ className={cn('px-1 text-sm font-medium leading-none', className)}
31
41
  {...props}
32
42
  />
33
43
  ));
@@ -35,54 +45,121 @@ FieldLegend.displayName = 'FieldLegend';
35
45
 
36
46
  // ─── FieldGroup(纵向排列多个 Field 的容器)──────────────────────────────
37
47
 
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
- ));
48
+ /**
49
+ * `FieldGroup` 行距密度 — 对齐《Teamix UI 表单设计规范》§3.5-3.6:
50
+ * - `normal`(默认)20px 行距 — 标准表单与混合(详情+表单)模式
51
+ * - `detail` 12px 行距 纯详情页(只展示不录入)
52
+ */
53
+ export type FieldGroupDensity = 'normal' | 'detail';
54
+
55
+ export interface FieldGroupProps extends React.HTMLAttributes<HTMLDivElement> {
56
+ /**
57
+ * 行距密度。详情页用 `detail`(12px),其余(可编辑表单 / 部分展示+部分可编辑混合)用 `normal`(20px)。
58
+ * @default "normal"
59
+ */
60
+ density?: FieldGroupDensity;
61
+ }
62
+
63
+ const FieldGroup = React.forwardRef<HTMLDivElement, FieldGroupProps>(
64
+ ({ className, density = 'normal', ...props }, ref) => (
65
+ <div
66
+ ref={ref}
67
+ data-density={density}
68
+ className={cn(
69
+ 'flex flex-col',
70
+ density === 'detail' ? 'gap-3' : 'gap-5',
71
+ className,
72
+ )}
73
+ {...props}
74
+ />
75
+ ),
76
+ );
44
77
  FieldGroup.displayName = 'FieldGroup';
45
78
 
46
79
  // ─── Field(单个字段容器:Label + Control + Description + Error)────────
47
80
 
81
+ export type FieldOrientation = 'vertical' | 'horizontal';
82
+
48
83
  const fieldVariants = cva('flex', {
49
84
  variants: {
50
85
  orientation: {
51
- vertical: 'flex-col gap-1.5',
52
- horizontal: 'flex-row items-start gap-3',
86
+ vertical: 'flex-col gap-1',
87
+ horizontal: 'flex-row items-start gap-4',
53
88
  },
54
89
  },
55
90
  defaultVariants: { orientation: 'vertical' },
56
91
  });
57
92
 
58
93
  export interface FieldProps
59
- extends React.HTMLAttributes<HTMLDivElement>,
60
- VariantProps<typeof fieldVariants> {
94
+ extends Omit<React.HTMLAttributes<HTMLDivElement>, 'children'>,
95
+ Omit<VariantProps<typeof fieldVariants>, 'orientation'> {
61
96
  /**
62
- * 字段方向 — `vertical`(默认)Label 在控件上;`horizontal` Label 在控件左侧(配长表单)。
97
+ * 字段方向 — 标量或 5 档断点响应式映射:
98
+ * - `vertical`(默认)Label 在控件上;
99
+ * - `horizontal` Label 在控件左侧(配长表单);
100
+ * - 响应式对象 `{ base, xs, s, m, l, xl }` 按视口断点自动切换
101
+ * (对齐设计规范 §4.2:单栏表单 < 432 应改上下结构)。
102
+ *
63
103
  * @default "vertical"
104
+ * @example orientation={{ base: 'vertical', xs: 'horizontal' }} // <432 上下,≥432 左右
64
105
  */
65
- orientation?: 'vertical' | 'horizontal';
106
+ orientation?: ResponsiveValue<FieldOrientation>;
66
107
  /**
67
108
  * 标记字段为 invalid — 子组件的 ring / 文字色会跟随;同时把内部 `<FieldError>` 渲染为可见。
68
109
  * @default false
69
110
  */
70
111
  invalid?: boolean;
112
+ /**
113
+ * 输入域宽度档位 — 5 档系统,对应 104 / 216 / 328 / 440 / 552 px。
114
+ * 通过 Context 下发,并在 Field 容器上注入 CSS 变量 `--field-input-width`,子组件可消费。
115
+ */
116
+ inputWidth?: InputWidth;
117
+ children?: React.ReactNode;
71
118
  }
72
119
 
73
- const FieldContext = React.createContext<{ invalid: boolean }>({ invalid: false });
120
+ const FieldContext = React.createContext<{
121
+ invalid: boolean;
122
+ inputWidth?: InputWidth;
123
+ orientation: FieldOrientation;
124
+ }>({ invalid: false, orientation: 'vertical' });
125
+
126
+ /** 在受控/自定义子组件中读取当前 Field 的状态(invalid / inputWidth / 解析后的 orientation)。 */
127
+ const useField = () => React.useContext(FieldContext);
74
128
 
75
129
  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
- ),
130
+ (
131
+ { orientation, invalid = false, inputWidth, className, style, ...props },
132
+ ref,
133
+ ) => {
134
+ const currentBp = useBreakpoint();
135
+ const resolvedOrientation: FieldOrientation =
136
+ pickByBreakpoint(orientation, currentBp) ?? 'vertical';
137
+
138
+ return (
139
+ <FieldContext.Provider
140
+ value={{ invalid, inputWidth, orientation: resolvedOrientation }}
141
+ >
142
+ <div
143
+ ref={ref}
144
+ data-invalid={invalid ? '' : undefined}
145
+ data-orientation={resolvedOrientation}
146
+ className={cn(
147
+ fieldVariants({ orientation: resolvedOrientation }),
148
+ className,
149
+ )}
150
+ style={
151
+ {
152
+ ...(inputWidth
153
+ ? { '--field-input-width': `var(--input-width-${inputWidth})` }
154
+ : null),
155
+ ...style,
156
+ } as React.CSSProperties
157
+ }
158
+ {...props}
159
+ />
160
+ </FieldContext.Provider>
161
+ );
162
+ },
86
163
  );
87
164
  Field.displayName = 'Field';
88
165
 
@@ -91,7 +168,8 @@ Field.displayName = 'Field';
91
168
  export interface FieldLabelProps
92
169
  extends React.ComponentPropsWithoutRef<typeof Label> {
93
170
  /**
94
- * 在 label 末尾追加 "*" 强调必填(antd Form `required` 并集) — 仅视觉,业务侧需在校验层确保必填。
171
+ * 在 label 末尾追加 "*" 强调必填(antd Form `required` 并集)— 仅视觉,业务侧需在校验层确保必填。
172
+ * 顺序为 `文字 ⓘ *`,与 Label / FormLabel 保持一致。
95
173
  * @default false
96
174
  */
97
175
  required?: boolean;
@@ -100,20 +178,38 @@ export interface FieldLabelProps
100
178
  const FieldLabel = React.forwardRef<
101
179
  React.ElementRef<typeof Label>,
102
180
  FieldLabelProps
103
- >(({ required = false, className, children, ...props }, ref) => {
104
- const { invalid } = React.useContext(FieldContext);
181
+ >(({ required = false, className, style, maxWidth, children, ...props }, ref) => {
182
+ const { invalid, inputWidth, orientation } = React.useContext(FieldContext);
183
+
184
+ // horizontal 模式下,Label 默认 maxWidth = ⅓ 输入域宽(对齐规范 §七 "Label 最大宽 ⅓");
185
+ // 业务显式传 maxWidth 仍优先。inputWidth 未设时退化为 ⅓ 容器宽(100%/3)。
186
+ const autoMaxWidth =
187
+ orientation === 'horizontal' && maxWidth === undefined
188
+ ? inputWidth
189
+ ? `calc(var(--input-width-${inputWidth}) / 3)`
190
+ : '33.333%'
191
+ : undefined;
192
+
193
+ const mergedStyle: React.CSSProperties = {
194
+ ...(autoMaxWidth && { maxWidth: autoMaxWidth }),
195
+ ...style,
196
+ };
197
+
105
198
  return (
106
199
  <Label
107
200
  ref={ref}
108
- className={cn(invalid && 'text-destructive', className)}
201
+ required={required}
202
+ style={mergedStyle}
203
+ maxWidth={maxWidth}
204
+ className={cn(
205
+ invalid && 'text-destructive',
206
+ // 启用了 maxWidth 时强制截断,保留 hover tooltip 由 Label 内置(若 tooltip prop 传入)
207
+ autoMaxWidth && 'truncate',
208
+ className,
209
+ )}
109
210
  {...props}
110
211
  >
111
212
  {children}
112
- {required ? (
113
- <span aria-hidden="true" className="ml-0.5 text-destructive">
114
- *
115
- </span>
116
- ) : null}
117
213
  </Label>
118
214
  );
119
215
  });
@@ -143,7 +239,7 @@ const FieldError = React.forwardRef<
143
239
  <p
144
240
  ref={ref}
145
241
  role="alert"
146
- className={cn('text-xs font-medium text-destructive', className)}
242
+ className={cn('-mt-1 text-xs font-medium text-destructive', className)}
147
243
  {...props}
148
244
  >
149
245
  {children}
@@ -152,6 +248,135 @@ const FieldError = React.forwardRef<
152
248
  });
153
249
  FieldError.displayName = 'FieldError';
154
250
 
251
+ // ─── FieldSection(带可选标题的字段分组)────────────────────────────────
252
+
253
+ export interface FieldSectionProps
254
+ extends Omit<React.HTMLAttributes<HTMLDivElement>, 'title'> {
255
+ /** 组标题 — 14px medium,有值时渲染标题行;多个 FieldSection 之间应由父容器提供 32px 间距(gap-8)。 */
256
+ title?: React.ReactNode;
257
+ /**
258
+ * 多栏 grid 列数 — 留空走 `flex-col`(单列纵向);设值时切到 CSS Grid,
259
+ * 列内 4px(`gap-x-4`)、行间 20px(`gap-y-5`,对齐规范 §3.1 规则 4)。
260
+ * @default undefined
261
+ */
262
+ columns?: 1 | 2 | 3 | 4;
263
+ }
264
+
265
+ const FieldSection = React.forwardRef<HTMLDivElement, FieldSectionProps>(
266
+ ({ title, columns, className, children, ...props }, ref) => (
267
+ <div ref={ref} className={cn('flex flex-col gap-5', className)} {...props}>
268
+ {title ? (
269
+ <h3 className="text-sm font-medium leading-none">{title}</h3>
270
+ ) : null}
271
+ {columns && columns > 1 ? (
272
+ <div
273
+ className={cn(
274
+ 'grid gap-x-4 gap-y-5',
275
+ columns === 2 && 'grid-cols-2',
276
+ columns === 3 && 'grid-cols-3',
277
+ columns === 4 && 'grid-cols-4',
278
+ )}
279
+ >
280
+ {children}
281
+ </div>
282
+ ) : (
283
+ children
284
+ )}
285
+ </div>
286
+ ),
287
+ );
288
+ FieldSection.displayName = 'FieldSection';
289
+
290
+ // ─── FieldActions(表单底部操作区)──────────────────────────────────────
291
+
292
+ /**
293
+ * 按钮容器场景 — 对齐《Teamix UI 表单设计规范》§六:
294
+ * - `page` 页面基础/分组表单:跟随表单流(`inline`)、左对齐、与输入域基线对齐
295
+ * - `page-stepper` 页面分步表单:吸底(`sticky`)、左对齐、不基线对齐(离侧导 24px 留给业务定 className)
296
+ * - `modal` 弹窗:跟随(`inline`)、右对齐、不基线对齐
297
+ * - `drawer` 抽屉:吸底(`sticky`)、左对齐、不基线对齐
298
+ *
299
+ * 显式 `position` / `align` / `alignWithInput` 优先级高于 `container` 推导。
300
+ */
301
+ export type FieldActionsContainer = 'page' | 'page-stepper' | 'modal' | 'drawer';
302
+
303
+ const CONTAINER_DEFAULTS: Record<
304
+ FieldActionsContainer,
305
+ { position: 'inline' | 'sticky'; align: 'start' | 'end'; alignWithInput: boolean }
306
+ > = {
307
+ page: { position: 'inline', align: 'start', alignWithInput: true },
308
+ 'page-stepper': { position: 'sticky', align: 'start', alignWithInput: false },
309
+ modal: { position: 'inline', align: 'end', alignWithInput: false },
310
+ drawer: { position: 'sticky', align: 'start', alignWithInput: false },
311
+ };
312
+
313
+ export interface FieldActionsProps extends React.HTMLAttributes<HTMLDivElement> {
314
+ /**
315
+ * 容器场景预设 — 一次性配齐 `position` / `align` / `alignWithInput`,
316
+ * 对齐设计规范 §六按容器分类的按钮位置规则。显式传入的同名 prop 优先。
317
+ *
318
+ * @example container="modal" // 弹窗:inline + end + 不基线对齐
319
+ * @example container="drawer" // 抽屉:sticky + start + 不基线对齐
320
+ */
321
+ container?: FieldActionsContainer;
322
+ /**
323
+ * 定位模式 — `inline` 跟随表单流;`sticky` 吸附在容器底部并带顶部分割线与背景。
324
+ * 未设时由 `container` 推导,默认 `inline`。
325
+ */
326
+ position?: 'inline' | 'sticky';
327
+ /**
328
+ * 对齐方向 — `end` 右对齐(主操作在右,弹窗默认);`start` 左对齐(页面/抽屉默认)。
329
+ * 未设时由 `container` 推导,默认 `end`。
330
+ */
331
+ align?: 'start' | 'end';
332
+ /**
333
+ * 与输入域基线对齐 — 水平表单(Field `orientation="horizontal"`)时预留 label 列宽度,
334
+ * 让按钮起点对齐输入框左边界(对齐设计规范 §六 "基础/分组表单按钮跟随基线对齐")。
335
+ * 通过 CSS 变量 `--form-label-width` 读取列宽,默认 `33.333%`(对齐规范 §七 label max ⅓)。
336
+ * 未设时由 `container` 推导,默认 `false`(仅 `container="page"` 时默认 `true`)。
337
+ */
338
+ alignWithInput?: boolean;
339
+ }
340
+
341
+ const FieldActions = React.forwardRef<HTMLDivElement, FieldActionsProps>(
342
+ (
343
+ { container, position, align, alignWithInput, className, style, ...props },
344
+ ref,
345
+ ) => {
346
+ const preset = container ? CONTAINER_DEFAULTS[container] : undefined;
347
+ const resolvedPosition = position ?? preset?.position ?? 'inline';
348
+ const resolvedAlign = align ?? preset?.align ?? 'end';
349
+ const resolvedAlignWithInput =
350
+ alignWithInput ?? preset?.alignWithInput ?? false;
351
+
352
+ const mergedStyle: React.CSSProperties = {
353
+ // 基线对齐用 inline style 表达 calc + CSS var(Tailwind arbitrary value 触 lint
354
+ // 且无法表达 calc 嵌套 var,inline 是规范允许的例外)
355
+ ...(resolvedAlignWithInput && {
356
+ paddingLeft: 'calc(var(--form-label-width, 33.333%) + 0.75rem)',
357
+ }),
358
+ ...style,
359
+ };
360
+
361
+ return (
362
+ <div
363
+ ref={ref}
364
+ data-container={container}
365
+ style={mergedStyle}
366
+ className={cn(
367
+ 'flex gap-2 pt-4',
368
+ resolvedPosition === 'sticky' &&
369
+ 'sticky bottom-0 border-t border-border bg-background py-4',
370
+ resolvedAlign === 'end' ? 'justify-end' : 'justify-start',
371
+ className,
372
+ )}
373
+ {...props}
374
+ />
375
+ );
376
+ },
377
+ );
378
+ FieldActions.displayName = 'FieldActions';
379
+
155
380
  export {
156
381
  Field,
157
382
  FieldLabel,
@@ -160,5 +385,8 @@ export {
160
385
  FieldGroup,
161
386
  FieldSet,
162
387
  FieldLegend,
388
+ FieldSection,
389
+ FieldActions,
163
390
  fieldVariants,
391
+ useField,
164
392
  };
@@ -0,0 +1,92 @@
1
+ ---
2
+ id: filter-bar
3
+ name: FilterBar
4
+ displayName: 筛选栏
5
+ type: component
6
+ category: data-entry
7
+ since: 0.1.0
8
+ package: '@teamix-evo/ui'
9
+ ---
10
+
11
+ # FilterBar 筛选栏
12
+
13
+ 筛选栏 — 面向 Table/List 上方的筛选表单变体,支持行内实时筛选 + 可折叠高级面板栅格布局。混合式架构:复合子组件自由组合 + FilterBarPreset 开箱即用。
14
+
15
+ ## When to use
16
+
17
+ - 列表页/表格上方需要筛选数据
18
+ - 条件较少时行内实时触发(inline 模式)
19
+ - 条件较多时需要展开面板叠加筛选(panel 模式)
20
+ - 需要搜索 + 重置按钮的标准筛选场景
21
+
22
+ ## When NOT to use
23
+
24
+ - 全局搜索框 → 用 `Input` + `Command`
25
+ - 侧栏持久筛选面板(永久可见) → 直接用 `Form` + `Card` 自行布局
26
+ - 单字段快速搜索 → 用 `Input` 配 `onKeyDown`
27
+
28
+ ## Props
29
+
30
+ > 以下表格由 `pnpm --filter @teamix-evo/ui gen:meta` 自动生成。FilterBar 是复合组件,下表按子组件分节列出各自的 Props。
31
+
32
+ <!-- auto:props:begin -->
33
+ | 名称 | 类型 | 默认值 | 必填 | 说明 |
34
+ | --- | --- | --- | --- | --- |
35
+ | `form` | `UseFormReturn<any>` | – | ✓ | react-hook-form 实例 — 由 `useForm()` 创建,管理所有筛选字段的状态。 |
36
+ | `defaultExpanded` | `boolean` | `false` | – | 面板是否默认展开。 |
37
+ | `onFilter` | `(values: Record<string, any>) => void` | – | – | 筛选回调 — inline 模式防抖后触发,或面板模式点"搜索"按钮触发。 |
38
+ | `onReset` | `(values: Record<string, any>) => void` | – | – | 重置回调 — 点"重置"按钮后触发,参数为重置后的表单值。 |
39
+ | `filterDebounce` | `number` | `300` | – | inline 区 onChange 事件的防抖时间(ms)。 |
40
+ | `labelAlign` | `'left' \| 'right'` | `'left'` | – | Label 对齐模式 — 右对齐仅适用于筛选区短标签。 |
41
+ | `inputMaxWidth` | `number` | `undefined` | – | 输入域最大宽度(px) — 未设时走默认推断: 多栏(≥3 列)为 400,单栏/双栏为 600。 |
42
+ <!-- auto:props:end -->
43
+
44
+ ## 依赖
45
+
46
+ > 以下表格由 `pnpm --filter @teamix-evo/ui gen:meta` 自动生成,数据源是 [`manifest.json`](../../../manifest.json)。**手工编辑 marker 之间的内容会在下次生成时被覆盖**。
47
+
48
+ <!-- auto:deps:begin -->
49
+ ### 同库依赖
50
+
51
+ > `teamix-evo ui add filter-bar` 时,以下 entry 会被自动连带安装(无需手动 add)。
52
+
53
+ | Entry | 类型 | 描述 |
54
+ | --- | --- | --- |
55
+ | `cn` | util | Tailwind className 合并工具(clsx + tailwind-merge) |
56
+ | `button` | component | 通用按钮 — shadcn 实现 + cloud-design 能力并集(loading / icon / shape / block / dashed variant / color 语义双 prop / disabledTooltip)。同文件合一导出 ButtonGroup + ButtonGroupText(等价 antd Space.Compact + cd SplitButton)。 |
57
+ | `form` | component | 表单 — react-hook-form + shadcn 7 件套(FormField/FormItem/Label/Control/Description/Message),配 zod 做 schema 校验 |
58
+ | `grid` | component | 24 栅格(Row + Col) — antd 独有补足。等价 antd Row + Col,基于 CSS Grid 的 24 列容器,支持 span / offset / order / flex / gutter / justify / align |
59
+ | `collapsible` | component | 单区域展开收起 — Radix Collapsible 薄包装(shadcn-only) |
60
+ | `use-debounce-callback` | hook | 防抖回调 hook — 返回一个防抖包装后的函数,在指定 delay 毫秒内连续调用只执行最后一次 |
61
+ | `use-breakpoint` | hook | 响应式断点 hook + pickByBreakpoint 工具 — 5 档断点(432/672/944/1200/1600,对齐《Teamix UI 表单设计规范》§4.1),给 Field 响应式 orientation / FilterBarPanel 响应式列数共用 |
62
+
63
+ ### npm 依赖
64
+
65
+ > 业务侧需要先 `pnpm add` / `npm install` 这些包。CLI 在 `ui add` 完成后会列出此提示。
66
+
67
+ ```bash
68
+ pnpm add lucide-react@^0.460.0 class-variance-authority@^0.7.0 @radix-ui/react-collapsible@^1.1.0
69
+ ```
70
+ <!-- auto:deps:end -->
71
+
72
+ ## 新增 Props 说明
73
+
74
+ 除上表中的核心 FilterBar Props 外,本轮补充以下骨架能力:
75
+
76
+ - **`FilterBar.labelAlign`**:`'left' \| 'right'`,label 在 horizontal 模式下的对齐方式。默认 `'left'`;短标签成片场景可用 `'right'` 对齐 input。
77
+ - **`FilterBar.inputMaxWidth`**:`number`,统一限制 FilterBar 内控件的最大宽度(px),避免多栏下某列过宽拉伸。
78
+ - **`FilterBarPanel.responsiveColumns`**:`ResponsiveValue<1 \| 2 \| 3 \| 4>`,5 档断点映射(对齐《Teamix UI 表单设计规范》§4.1):`base` < 432 / `xs` ≥ 432 / `s` ≥ 672 / `m` ≥ 944 / `l` ≥ 1200 / `xl` ≥ 1600。优先级高于 `columns`。底层走 `useBreakpoint` hook + JS 计算 grid-template-columns,不依赖客户 Tailwind 配置。示例:`responsiveColumns={{ base: 1, xs: 2, m: 3, l: 4 }}`。
79
+
80
+ ## AI 生成纪律
81
+
82
+ - **FilterBar 必须接受 form 实例**:`<FilterBar form={form}>...</FilterBar>`,由外部 `useForm()` 创建
83
+ - **筛选字段必须走 FormField**:无论 inline 还是 panel 内的字段,都必须用 `<FormField name="..." control={form.control} render={...} />`
84
+ - **FilterBarPanel 内字段自动栅格**:直接放 FormItem 即可,Panel 会自动用 Row/Col 包裹
85
+ - **不要在 FilterBarSearch 和 FilterBarPanel 放同名字段**:两个区域的字段名必须唯一,否则会冲突
86
+ - **inline 模式不要放 FilterBarActions**:行内区自动防抖触发 onFilter,不需要搜索按钮
87
+ - **面板模式推荐关闭 autoFilter**:有面板时设置 `<FilterBarSearch autoFilter={false}>`,筛选由 FilterBarActions 的搜索按钮统一触发
88
+ - **重置会清空所有字段**:`form.reset()` 清空所有默认值之外的数据,包括 inline 和 panel
89
+ - **FilterBarPreset 是快捷方式**:如果标准布局满足需求,优先用 FilterBarPreset 而非手动组合子组件
90
+ - ✅ 需要响应式时使用 `responsiveColumns`(5 档断点 base/xs/s/m/l/xl,不要用 `columns` + 手写媒体查询或 `lg:grid-cols-*`)
91
+ - ✅ 右对齐 label 仅适用于短标签筛选区(`labelAlign="right"`),长标签不要用;**viewport < 432px 自动改左对齐**(避免上下结构时 label 右贴反直觉,组件已内置响应式联动)
92
+ - ✅ 多栏筛选建议设置 `inputMaxWidth`(避免某个字段被拉伸过宽)