@teamix-evo/ui 0.2.0 → 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 (282) hide show
  1. package/README.md +184 -184
  2. package/manifest.json +680 -492
  3. package/package.json +15 -9
  4. package/src/components/accordion/accordion.meta.md +5 -9
  5. package/src/components/accordion/accordion.stories.tsx +3 -3
  6. package/src/components/accordion/accordion.tsx +104 -8
  7. package/src/components/affix/affix.meta.md +21 -12
  8. package/src/components/affix/affix.stories.tsx +101 -26
  9. package/src/components/affix/affix.tsx +79 -9
  10. package/src/components/alert/alert.meta.md +52 -26
  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 +48 -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 +10 -14
  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 +10 -14
  20. package/src/components/app/app.stories.tsx +6 -6
  21. package/src/components/aspect-ratio/aspect-ratio.meta.md +4 -8
  22. package/src/components/aspect-ratio/aspect-ratio.stories.tsx +3 -3
  23. package/src/components/auto-complete/auto-complete.meta.md +19 -20
  24. package/src/components/auto-complete/auto-complete.stories.tsx +44 -3
  25. package/src/components/auto-complete/auto-complete.tsx +119 -71
  26. package/src/components/avatar/avatar.meta.md +9 -22
  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 +14 -18
  30. package/src/components/badge/badge.stories.tsx +2 -2
  31. package/src/components/badge/badge.tsx +2 -2
  32. package/src/components/breadcrumb/breadcrumb.meta.md +29 -20
  33. package/src/components/breadcrumb/breadcrumb.stories.tsx +120 -5
  34. package/src/components/breadcrumb/breadcrumb.tsx +22 -8
  35. package/src/components/button/button.meta.md +261 -29
  36. package/src/components/button/button.stories.tsx +549 -41
  37. package/src/components/button/button.tsx +335 -33
  38. package/src/components/calendar/calendar.meta.md +19 -14
  39. package/src/components/calendar/calendar.stories.tsx +5 -5
  40. package/src/components/calendar/calendar.tsx +73 -8
  41. package/src/components/card/card.meta.md +31 -34
  42. package/src/components/card/card.stories.tsx +34 -3
  43. package/src/components/card/card.tsx +146 -63
  44. package/src/components/carousel/carousel.meta.md +10 -14
  45. package/src/components/carousel/carousel.stories.tsx +1 -1
  46. package/src/components/cascader/cascader.meta.md +43 -22
  47. package/src/components/cascader/cascader.stories.tsx +13 -2
  48. package/src/components/cascader/cascader.tsx +427 -84
  49. package/src/components/checkbox/checkbox.meta.md +74 -24
  50. package/src/components/checkbox/checkbox.stories.tsx +160 -2
  51. package/src/components/checkbox/checkbox.tsx +77 -9
  52. package/src/components/collapsible/collapsible.meta.md +7 -6
  53. package/src/components/collapsible/collapsible.stories.tsx +2 -2
  54. package/src/components/collapsible/collapsible.tsx +93 -6
  55. package/src/components/color-picker/color-picker.meta.md +16 -20
  56. package/src/components/color-picker/color-picker.stories.tsx +86 -7
  57. package/src/components/color-picker/color-picker.tsx +19 -9
  58. package/src/components/command/command.meta.md +7 -11
  59. package/src/components/command/command.stories.tsx +4 -4
  60. package/src/components/command/command.tsx +18 -7
  61. package/src/components/context-menu/context-menu.meta.md +5 -25
  62. package/src/components/context-menu/context-menu.stories.tsx +4 -4
  63. package/src/components/context-menu/context-menu.tsx +21 -8
  64. package/src/components/data-table/data-table.meta.md +14 -18
  65. package/src/components/data-table/data-table.stories.tsx +1 -1
  66. package/src/components/data-table/data-table.tsx +2 -2
  67. package/src/components/date-picker/date-picker.meta.md +90 -41
  68. package/src/components/date-picker/date-picker.stories.tsx +55 -5
  69. package/src/components/date-picker/date-picker.tsx +1489 -91
  70. package/src/components/descriptions/descriptions.meta.md +12 -16
  71. package/src/components/descriptions/descriptions.stories.tsx +2 -2
  72. package/src/components/descriptions/descriptions.tsx +22 -14
  73. package/src/components/dialog/dialog.meta.md +67 -17
  74. package/src/components/dialog/dialog.stories.tsx +182 -20
  75. package/src/components/dialog/dialog.tsx +67 -15
  76. package/src/components/dialog/imperative.tsx +252 -0
  77. package/src/components/drawer/drawer.meta.md +27 -39
  78. package/src/components/drawer/drawer.stories.tsx +29 -12
  79. package/src/components/drawer/drawer.tsx +22 -114
  80. package/src/components/dropdown-menu/dropdown-menu.meta.md +64 -24
  81. package/src/components/dropdown-menu/dropdown-menu.stories.tsx +81 -3
  82. package/src/components/dropdown-menu/dropdown-menu.tsx +24 -10
  83. package/src/components/ellipsis/ellipsis.meta.md +87 -0
  84. package/src/components/ellipsis/ellipsis.stories.tsx +72 -0
  85. package/src/components/ellipsis/ellipsis.tsx +153 -0
  86. package/src/components/empty/empty.meta.md +10 -14
  87. package/src/components/empty/empty.stories.tsx +3 -3
  88. package/src/components/empty/empty.tsx +10 -3
  89. package/src/components/field/field.meta.md +46 -25
  90. package/src/components/field/field.stories.tsx +380 -3
  91. package/src/components/field/field.tsx +263 -35
  92. package/src/components/filter-bar/filter-bar.meta.md +92 -0
  93. package/src/components/filter-bar/filter-bar.stories.tsx +1083 -0
  94. package/src/components/filter-bar/filter-bar.tsx +568 -0
  95. package/src/components/flex/flex.meta.md +59 -20
  96. package/src/components/flex/flex.stories.tsx +65 -10
  97. package/src/components/flex/flex.tsx +27 -4
  98. package/src/components/float-button/float-button.meta.md +10 -29
  99. package/src/components/float-button/float-button.stories.tsx +6 -6
  100. package/src/components/form/form.meta.md +31 -52
  101. package/src/components/form/form.stories.tsx +350 -3
  102. package/src/components/form/form.tsx +101 -35
  103. package/src/components/grid/grid.meta.md +4 -24
  104. package/src/components/grid/grid.stories.tsx +2 -2
  105. package/src/components/hover-card/hover-card.meta.md +9 -10
  106. package/src/components/hover-card/hover-card.stories.tsx +29 -4
  107. package/src/components/hover-card/hover-card.tsx +51 -13
  108. package/src/components/icon/DEVELOPMENT.md +809 -0
  109. package/src/components/icon/icon.meta.md +170 -0
  110. package/src/components/icon/icon.stories.tsx +344 -0
  111. package/src/components/icon/icon.tsx +248 -0
  112. package/src/components/image/image.meta.md +14 -18
  113. package/src/components/image/image.stories.tsx +3 -3
  114. package/src/components/image/image.tsx +2 -0
  115. package/src/components/input/demo/sizes.tsx +2 -2
  116. package/src/components/input/input.meta.md +44 -43
  117. package/src/components/input/input.stories.tsx +62 -35
  118. package/src/components/input/input.tsx +96 -101
  119. package/src/components/input-group/input-group.meta.md +53 -39
  120. package/src/components/input-group/input-group.stories.tsx +49 -16
  121. package/src/components/input-group/input-group.tsx +43 -8
  122. package/src/components/input-number/input-number.meta.md +68 -20
  123. package/src/components/input-number/input-number.stories.tsx +33 -6
  124. package/src/components/input-number/input-number.tsx +79 -20
  125. package/src/components/input-otp/input-otp.meta.md +8 -20
  126. package/src/components/input-otp/input-otp.stories.tsx +3 -3
  127. package/src/components/input-otp/input-otp.tsx +1 -1
  128. package/src/components/item/item.meta.md +8 -26
  129. package/src/components/item/item.stories.tsx +3 -3
  130. package/src/components/item/item.tsx +7 -6
  131. package/src/components/kbd/kbd.meta.md +7 -19
  132. package/src/components/kbd/kbd.stories.tsx +4 -4
  133. package/src/components/kbd/kbd.tsx +8 -4
  134. package/src/components/label/label.meta.md +21 -18
  135. package/src/components/label/label.stories.tsx +64 -6
  136. package/src/components/label/label.tsx +91 -19
  137. package/src/components/masonry/masonry.meta.md +8 -12
  138. package/src/components/masonry/masonry.stories.tsx +4 -4
  139. package/src/components/mentions/mentions.meta.md +42 -21
  140. package/src/components/mentions/mentions.stories.tsx +120 -6
  141. package/src/components/mentions/mentions.tsx +10 -5
  142. package/src/components/menubar/menubar.meta.md +4 -8
  143. package/src/components/menubar/menubar.stories.tsx +55 -3
  144. package/src/components/menubar/menubar.tsx +9 -9
  145. package/src/components/native-select/native-select.meta.md +7 -11
  146. package/src/components/native-select/native-select.stories.tsx +4 -4
  147. package/src/components/native-select/native-select.tsx +1 -1
  148. package/src/components/navigation-menu/navigation-menu.meta.md +4 -8
  149. package/src/components/navigation-menu/navigation-menu.stories.tsx +106 -3
  150. package/src/components/navigation-menu/navigation-menu.tsx +6 -3
  151. package/src/components/notification/notification.meta.md +41 -8
  152. package/src/components/notification/notification.stories.tsx +9 -9
  153. package/src/components/notification/notification.tsx +34 -19
  154. package/src/components/page-header/DEVELOPMENT.md +842 -0
  155. package/src/components/page-header/page-header.meta.md +208 -0
  156. package/src/components/page-header/page-header.stories.tsx +421 -0
  157. package/src/components/page-header/page-header.tsx +281 -0
  158. package/src/components/pagination/pagination.meta.md +122 -50
  159. package/src/components/pagination/pagination.stories.tsx +227 -11
  160. package/src/components/pagination/pagination.tsx +355 -63
  161. package/src/components/popconfirm/popconfirm.meta.md +19 -23
  162. package/src/components/popconfirm/popconfirm.stories.tsx +2 -2
  163. package/src/components/popconfirm/popconfirm.tsx +1 -1
  164. package/src/components/popover/popover.meta.md +64 -12
  165. package/src/components/popover/popover.stories.tsx +83 -7
  166. package/src/components/popover/popover.tsx +77 -28
  167. package/src/components/progress/progress.meta.md +43 -26
  168. package/src/components/progress/progress.stories.tsx +2 -2
  169. package/src/components/progress/progress.tsx +19 -11
  170. package/src/components/radio-group/radio-group.meta.md +78 -11
  171. package/src/components/radio-group/radio-group.stories.tsx +38 -2
  172. package/src/components/radio-group/radio-group.tsx +149 -18
  173. package/src/components/rate/rate.meta.md +41 -19
  174. package/src/components/rate/rate.stories.tsx +2 -2
  175. package/src/components/rate/rate.tsx +37 -10
  176. package/src/components/resizable/resizable.meta.md +4 -12
  177. package/src/components/resizable/resizable.stories.tsx +5 -5
  178. package/src/components/resizable/resizable.tsx +1 -1
  179. package/src/components/result/result.meta.md +10 -14
  180. package/src/components/result/result.stories.tsx +2 -2
  181. package/src/components/result/result.tsx +21 -12
  182. package/src/components/scroll-area/scroll-area.meta.md +4 -8
  183. package/src/components/scroll-area/scroll-area.stories.tsx +5 -5
  184. package/src/components/segmented/segmented.meta.md +15 -17
  185. package/src/components/segmented/segmented.stories.tsx +3 -3
  186. package/src/components/segmented/segmented.tsx +15 -7
  187. package/src/components/select/select.meta.md +199 -67
  188. package/src/components/select/select.stories.tsx +238 -63
  189. package/src/components/select/select.tsx +718 -171
  190. package/src/components/separator/separator.meta.md +10 -14
  191. package/src/components/separator/separator.stories.tsx +2 -2
  192. package/src/components/separator/separator.tsx +3 -7
  193. package/src/components/sheet/sheet.meta.md +26 -21
  194. package/src/components/sheet/sheet.stories.tsx +116 -10
  195. package/src/components/sheet/sheet.tsx +116 -29
  196. package/src/components/sidebar/sidebar.meta.md +28 -38
  197. package/src/components/sidebar/sidebar.stories.tsx +696 -29
  198. package/src/components/sidebar/sidebar.tsx +615 -142
  199. package/src/components/skeleton/skeleton.meta.md +7 -31
  200. package/src/components/skeleton/skeleton.stories.tsx +3 -3
  201. package/src/components/skeleton/skeleton.tsx +7 -7
  202. package/src/components/slider/slider.meta.md +60 -13
  203. package/src/components/slider/slider.stories.tsx +58 -6
  204. package/src/components/slider/slider.tsx +154 -13
  205. package/src/components/sonner/sonner.meta.md +54 -8
  206. package/src/components/sonner/sonner.stories.tsx +79 -11
  207. package/src/components/sonner/sonner.tsx +137 -8
  208. package/src/components/spinner/spinner.meta.md +57 -21
  209. package/src/components/spinner/spinner.stories.tsx +66 -14
  210. package/src/components/spinner/spinner.tsx +111 -9
  211. package/src/components/statistic/statistic.meta.md +14 -30
  212. package/src/components/statistic/statistic.stories.tsx +1 -1
  213. package/src/components/statistic/statistic.tsx +4 -5
  214. package/src/components/steps/steps.meta.md +20 -15
  215. package/src/components/steps/steps.stories.tsx +37 -2
  216. package/src/components/steps/steps.tsx +15 -12
  217. package/src/components/switch/switch.meta.md +56 -15
  218. package/src/components/switch/switch.stories.tsx +5 -5
  219. package/src/components/switch/switch.tsx +59 -13
  220. package/src/components/table/table.meta.md +3 -7
  221. package/src/components/table/table.stories.tsx +1 -1
  222. package/src/components/table/table.tsx +4 -4
  223. package/src/components/tabs/tabs.meta.md +40 -32
  224. package/src/components/tabs/tabs.stories.tsx +104 -26
  225. package/src/components/tabs/tabs.tsx +125 -54
  226. package/src/components/tag/tag.meta.md +104 -68
  227. package/src/components/tag/tag.stories.tsx +183 -15
  228. package/src/components/tag/tag.tsx +222 -21
  229. package/src/components/textarea/textarea.meta.md +42 -31
  230. package/src/components/textarea/textarea.stories.tsx +32 -6
  231. package/src/components/textarea/textarea.tsx +32 -8
  232. package/src/components/time-picker/time-picker.meta.md +119 -50
  233. package/src/components/time-picker/time-picker.stories.tsx +65 -33
  234. package/src/components/time-picker/time-picker.tsx +889 -101
  235. package/src/components/timeline/timeline.meta.md +16 -17
  236. package/src/components/timeline/timeline.stories.tsx +24 -4
  237. package/src/components/timeline/timeline.tsx +32 -12
  238. package/src/components/toggle/toggle.meta.md +8 -12
  239. package/src/components/toggle/toggle.stories.tsx +4 -4
  240. package/src/components/toggle/toggle.tsx +4 -3
  241. package/src/components/toggle-group/toggle-group.meta.md +10 -14
  242. package/src/components/toggle-group/toggle-group.stories.tsx +3 -3
  243. package/src/components/toggle-group/toggle-group.tsx +2 -2
  244. package/src/components/tooltip/tooltip.meta.md +63 -18
  245. package/src/components/tooltip/tooltip.stories.tsx +42 -5
  246. package/src/components/tooltip/tooltip.tsx +81 -21
  247. package/src/components/tour/tour.meta.md +16 -20
  248. package/src/components/tour/tour.stories.tsx +3 -3
  249. package/src/components/tour/tour.tsx +3 -3
  250. package/src/components/transfer/transfer.meta.md +18 -22
  251. package/src/components/transfer/transfer.stories.tsx +2 -2
  252. package/src/components/transfer/transfer.tsx +28 -21
  253. package/src/components/tree/tree.meta.md +67 -22
  254. package/src/components/tree/tree.stories.tsx +1 -1
  255. package/src/components/tree/tree.tsx +9 -8
  256. package/src/components/tree-select/tree-select.meta.md +59 -23
  257. package/src/components/tree-select/tree-select.stories.tsx +2 -2
  258. package/src/components/tree-select/tree-select.tsx +42 -7
  259. package/src/components/typography/typography.meta.md +61 -39
  260. package/src/components/typography/typography.stories.tsx +14 -9
  261. package/src/components/typography/typography.tsx +38 -25
  262. package/src/components/upload/upload.meta.md +61 -25
  263. package/src/components/upload/upload.stories.tsx +69 -3
  264. package/src/components/upload/upload.tsx +170 -37
  265. package/src/components/watermark/watermark.meta.md +15 -19
  266. package/src/components/watermark/watermark.stories.tsx +98 -8
  267. package/src/hooks/use-breakpoint.ts +117 -0
  268. package/src/hooks/use-debounce-callback.ts +52 -0
  269. package/src/hooks/use-mobile.ts +23 -0
  270. package/src/stories/theme-tokens.stories.tsx +747 -0
  271. package/src/utils/trigger-input.ts +53 -0
  272. package/src/components/button-group/button-group.meta.md +0 -101
  273. package/src/components/button-group/button-group.stories.tsx +0 -93
  274. package/src/components/button-group/button-group.tsx +0 -75
  275. package/src/components/combobox/combobox.meta.md +0 -102
  276. package/src/components/combobox/combobox.stories.tsx +0 -55
  277. package/src/components/combobox/combobox.tsx +0 -130
  278. package/src/components/input/demo/addon.tsx +0 -15
  279. package/src/components/input/demo/with-prefix-suffix.tsx +0 -19
  280. package/src/components/space/space.meta.md +0 -103
  281. package/src/components/space/space.stories.tsx +0 -108
  282. package/src/components/space/space.tsx +0 -106
@@ -0,0 +1,568 @@
1
+ import * as React from 'react';
2
+ import { cva, type VariantProps } from 'class-variance-authority';
3
+ import { ChevronDown, ChevronUp, Search, RotateCcw } from 'lucide-react';
4
+ import {
5
+ type UseFormReturn,
6
+ type FieldValues,
7
+ useFormContext,
8
+ } from 'react-hook-form';
9
+
10
+ import { cn } from '@/utils/cn';
11
+ import { Button } from '@/components/button/button';
12
+ import {
13
+ Collapsible,
14
+ CollapsibleContent,
15
+ CollapsibleTrigger,
16
+ } from '@/components/collapsible/collapsible';
17
+ import { Row, Col } from '@/components/grid/grid';
18
+ import { useDebounceCallback } from '@/hooks/use-debounce-callback';
19
+ import {
20
+ useBreakpoint,
21
+ pickByBreakpoint,
22
+ type ResponsiveValue,
23
+ } from '@/hooks/use-breakpoint';
24
+
25
+ // ─── Context ─────────────────────────────────────────────────────────────────
26
+
27
+ interface FilterBarContextValue {
28
+ expanded: boolean;
29
+ setExpanded: React.Dispatch<React.SetStateAction<boolean>>;
30
+ onFilter?: (values: Record<string, any>) => void;
31
+ onReset?: (values: Record<string, any>) => void;
32
+ filterDebounce: number;
33
+ /** Label 对齐模式 — 右对齐仅适用于筛选区短标签。@default 'left' */
34
+ labelAlign: 'left' | 'right';
35
+ /** 输入域最大宽度(px) — 未设时以有效列数推断(≤2 列为 600,否则 400)。@default undefined */
36
+ inputMaxWidth?: number;
37
+ }
38
+
39
+ const FilterBarContext = React.createContext<FilterBarContextValue | null>(
40
+ null,
41
+ );
42
+
43
+ function useFilterBarContext() {
44
+ const ctx = React.useContext(FilterBarContext);
45
+ if (!ctx) {
46
+ throw new Error('FilterBar 子组件必须在 <FilterBar> 内使用。');
47
+ }
48
+ return ctx;
49
+ }
50
+
51
+ // ─── FilterBar (root) ────────────────────────────────────────────────────────
52
+
53
+ const filterBarVariants = cva('flex flex-col gap-3 w-full', {
54
+ variants: {
55
+ /**
56
+ * 容器外观变体。
57
+ * @default "plain"
58
+ */
59
+ variant: {
60
+ plain: '',
61
+ bordered: 'rounded-md border border-border p-4',
62
+ },
63
+ },
64
+ defaultVariants: {
65
+ variant: 'plain',
66
+ },
67
+ });
68
+
69
+ export interface FilterBarProps
70
+ extends React.HTMLAttributes<HTMLDivElement>,
71
+ VariantProps<typeof filterBarVariants> {
72
+ /**
73
+ * react-hook-form 实例 — 由 `useForm()` 创建,管理所有筛选字段的状态。
74
+ */
75
+ form: UseFormReturn<any>;
76
+ /**
77
+ * 面板是否默认展开。
78
+ * @default false
79
+ */
80
+ defaultExpanded?: boolean;
81
+ /**
82
+ * 筛选回调 — inline 模式防抖后触发,或面板模式点"搜索"按钮触发。
83
+ */
84
+ onFilter?: (values: Record<string, any>) => void;
85
+ /**
86
+ * 重置回调 — 点"重置"按钮后触发,参数为重置后的表单值。
87
+ */
88
+ onReset?: (values: Record<string, any>) => void;
89
+ /**
90
+ * inline 区 onChange 事件的防抖时间(ms)。
91
+ * @default 300
92
+ */
93
+ filterDebounce?: number;
94
+ /**
95
+ * Label 对齐模式 — 右对齐仅适用于筛选区短标签。
96
+ * @default 'left'
97
+ */
98
+ labelAlign?: 'left' | 'right';
99
+ /**
100
+ * 输入域最大宽度(px) — 未设时走默认推断: 多栏(≥3 列)为 400,单栏/双栏为 600。
101
+ * @default undefined
102
+ */
103
+ inputMaxWidth?: number;
104
+ }
105
+
106
+ /**
107
+ * `FilterBar` 根容器 — 注入筛选上下文(form / expand 状态 / 回调),
108
+ * 子组件通过 context 读取和操作。
109
+ */
110
+ const FilterBar = React.forwardRef<HTMLDivElement, FilterBarProps>(
111
+ (
112
+ {
113
+ form,
114
+ defaultExpanded = false,
115
+ onFilter,
116
+ onReset,
117
+ filterDebounce = 300,
118
+ labelAlign = 'left',
119
+ inputMaxWidth,
120
+ variant,
121
+ className,
122
+ children,
123
+ ...props
124
+ },
125
+ ref,
126
+ ) => {
127
+ const [expanded, setExpanded] = React.useState(defaultExpanded);
128
+
129
+ const ctxValue = React.useMemo<FilterBarContextValue>(
130
+ () => ({
131
+ expanded,
132
+ setExpanded,
133
+ onFilter,
134
+ onReset,
135
+ filterDebounce,
136
+ labelAlign,
137
+ inputMaxWidth,
138
+ }),
139
+ [expanded, onFilter, onReset, filterDebounce, labelAlign, inputMaxWidth],
140
+ );
141
+
142
+ return (
143
+ <FilterBarContext.Provider value={ctxValue}>
144
+ <Collapsible open={expanded} onOpenChange={setExpanded} asChild>
145
+ <div
146
+ ref={ref}
147
+ className={cn(filterBarVariants({ variant }), className)}
148
+ {...props}
149
+ >
150
+ {children}
151
+ </div>
152
+ </Collapsible>
153
+ </FilterBarContext.Provider>
154
+ );
155
+ },
156
+ );
157
+ FilterBar.displayName = 'FilterBar';
158
+
159
+ // ─── FilterBarSearch ─────────────────────────────────────────────────────────
160
+
161
+ export interface FilterBarSearchProps
162
+ extends React.HTMLAttributes<HTMLDivElement> {
163
+ /**
164
+ * 是否启用 inline 实时筛选 — 开启后子字段 onChange 触发防抖 onFilter。
165
+ * @default true
166
+ */
167
+ autoFilter?: boolean;
168
+ }
169
+
170
+ /**
171
+ * `FilterBarSearch` 行内搜索区 — 放置高频筛选字段(Select / Input 等),
172
+ * 水平排列。`autoFilter=true` 时自动监听表单变更并防抖触发 `onFilter`。
173
+ */
174
+ const FilterBarSearch = React.forwardRef<HTMLDivElement, FilterBarSearchProps>(
175
+ ({ autoFilter = true, className, children, ...props }, ref) => {
176
+ const { onFilter, filterDebounce } = useFilterBarContext();
177
+ const form = useFormContext();
178
+
179
+ const debouncedFilter = useDebounceCallback(
180
+ (values: Record<string, any>) => {
181
+ onFilter?.(values);
182
+ },
183
+ filterDebounce,
184
+ );
185
+
186
+ // 监听表单变更,防抖触发 onFilter
187
+ React.useEffect(() => {
188
+ if (!autoFilter || !form) return;
189
+ const subscription = form.watch((values) => {
190
+ debouncedFilter(values as Record<string, any>);
191
+ });
192
+ return () => subscription.unsubscribe();
193
+ }, [autoFilter, form, debouncedFilter]);
194
+
195
+ return (
196
+ <div
197
+ ref={ref}
198
+ className={cn('flex items-center gap-2 flex-wrap', className)}
199
+ {...props}
200
+ >
201
+ {children}
202
+ </div>
203
+ );
204
+ },
205
+ );
206
+ FilterBarSearch.displayName = 'FilterBarSearch';
207
+
208
+ // ─── FilterBarTrigger ────────────────────────────────────────────────────────
209
+
210
+ export interface FilterBarTriggerProps {
211
+ /**
212
+ * 展开时显示的文案。
213
+ * @default "高级筛选"
214
+ */
215
+ expandedLabel?: string;
216
+ /**
217
+ * 收起时显示的文案。
218
+ * @default "高级筛选"
219
+ */
220
+ collapsedLabel?: string;
221
+ /**
222
+ * 额外的 className。
223
+ */
224
+ className?: string;
225
+ }
226
+
227
+ /**
228
+ * `FilterBarTrigger` 展开/收起触发器 — 自动读取 expand 状态,
229
+ * 切换 ChevronDown / ChevronUp 图标。
230
+ */
231
+ const FilterBarTrigger = React.forwardRef<
232
+ HTMLButtonElement,
233
+ FilterBarTriggerProps
234
+ >(
235
+ (
236
+ { expandedLabel = '高级筛选', collapsedLabel = '高级筛选', className },
237
+ ref,
238
+ ) => {
239
+ const { expanded } = useFilterBarContext();
240
+
241
+ return (
242
+ <CollapsibleTrigger asChild>
243
+ <Button
244
+ ref={ref}
245
+ variant="outline"
246
+ color="default"
247
+ className={cn('gap-1', className)}
248
+ >
249
+ {expanded ? (
250
+ <ChevronUp className="size-4" />
251
+ ) : (
252
+ <ChevronDown className="size-4" />
253
+ )}
254
+ {expanded ? expandedLabel : collapsedLabel}
255
+ </Button>
256
+ </CollapsibleTrigger>
257
+ );
258
+ },
259
+ );
260
+ FilterBarTrigger.displayName = 'FilterBarTrigger';
261
+
262
+ // ─── FilterBarPanel ──────────────────────────────────────────────────────────
263
+
264
+ export interface FilterBarPanelProps
265
+ extends React.HTMLAttributes<HTMLDivElement> {
266
+ /**
267
+ * 静态列数 — 面板内字段按此列数等分排列(走 Row/Col 24 栅格)。
268
+ * 传入 `responsiveColumns` 时被覆盖。
269
+ * @default 3
270
+ */
271
+ columns?: 1 | 2 | 3 | 4;
272
+ /**
273
+ * 响应式列数 — 标量或 5 档断点映射(对齐《Teamix UI 表单设计规范》§4.1):
274
+ * - `base` < 432 / `xs` ≥ 432 / `s` ≥ 672 / `m` ≥ 944 / `l` ≥ 1200 / `xl` ≥ 1600
275
+ *
276
+ * 启用后走 CSS Grid + JS-side useBreakpoint 解析(不依赖客户 Tailwind 配置)。
277
+ * 优先级高于 `columns`。
278
+ *
279
+ * @example responsiveColumns={{ base: 1, xs: 2, m: 3, l: 4 }}
280
+ */
281
+ responsiveColumns?: ResponsiveValue<1 | 2 | 3 | 4>;
282
+ }
283
+
284
+ /**
285
+ * `FilterBarPanel` 可折叠面板 — 展开后显示更多筛选字段。
286
+ * 传入 `responsiveColumns` 时走 CSS Grid 响应式布局,否则走 Row/Col 栅格。
287
+ */
288
+ const FilterBarPanel = React.forwardRef<HTMLDivElement, FilterBarPanelProps>(
289
+ ({ columns = 3, responsiveColumns, className, children, ...props }, ref) => {
290
+ const { labelAlign, inputMaxWidth } = useFilterBarContext();
291
+ const currentBp = useBreakpoint();
292
+
293
+ // 展开 Fragment,收集所有实际子元素
294
+ const flatChildren: React.ReactNode[] = [];
295
+ const flatten = (nodes: React.ReactNode) => {
296
+ React.Children.forEach(nodes, (child) => {
297
+ if (!React.isValidElement(child)) {
298
+ if (child != null) flatChildren.push(child);
299
+ return;
300
+ }
301
+ if (child.type === React.Fragment) {
302
+ flatten(child.props.children);
303
+ } else {
304
+ flatChildren.push(child);
305
+ }
306
+ });
307
+ };
308
+ flatten(children);
309
+
310
+ // 解析响应式列数:有响应式则按当前断点取值;无则用静态 columns
311
+ const resolvedCols =
312
+ pickByBreakpoint(responsiveColumns, currentBp) ?? columns;
313
+
314
+ // 输入域最大宽度推断(响应规范 §4.1:多栏 400 / 单双栏 600)
315
+ const maxW = inputMaxWidth ?? (resolvedCols <= 2 ? 600 : 400);
316
+ const fieldStyle: React.CSSProperties = { maxWidth: `${maxW}px` };
317
+
318
+ // label 对齐 class:右对齐仅适用于横向窗口下的短标签筛选区;
319
+ // viewport < xs(432px,规范 §4.1)时强制左对齐,避免上下结构时 label 右贴反直觉
320
+ const effectiveLabelAlign =
321
+ labelAlign === 'right' && currentBp === null ? 'left' : labelAlign;
322
+ const labelAlignClass =
323
+ effectiveLabelAlign === 'right' ? '[&_label]:text-right' : '';
324
+
325
+ let content: React.ReactNode;
326
+
327
+ if (responsiveColumns) {
328
+ // CSS Grid 响应式模式
329
+ const gridStyle: React.CSSProperties = {
330
+ display: 'grid',
331
+ gridTemplateColumns: `repeat(${resolvedCols}, minmax(0, 1fr))`,
332
+ columnGap: '1rem', // 16px,对齐规范 §3.1 规则 3
333
+ rowGap: '1.25rem', // 20px,对齐规范 §3.1 规则 4
334
+ };
335
+
336
+ const gridChildren = flatChildren.map((child, idx) => {
337
+ if (!React.isValidElement(child)) {
338
+ return <React.Fragment key={idx}>{child}</React.Fragment>;
339
+ }
340
+ if (child.type === FilterBarActions) {
341
+ return (
342
+ <div key={idx} style={{ gridColumn: '1 / -1' }}>
343
+ {child}
344
+ </div>
345
+ );
346
+ }
347
+ return (
348
+ <div key={idx} style={fieldStyle}>
349
+ {child}
350
+ </div>
351
+ );
352
+ });
353
+
354
+ content = <div style={gridStyle}>{gridChildren}</div>;
355
+ } else {
356
+ // Row/Col 栅格模式(静态 columns)
357
+ const colSpan = Math.floor(24 / columns);
358
+
359
+ const wrappedChildren = flatChildren.map((child, idx) => {
360
+ if (!React.isValidElement(child)) return child;
361
+ if (child.type === Col) return child;
362
+ if (child.type === FilterBarActions) {
363
+ return (
364
+ <Col key={idx} span={24}>
365
+ {child}
366
+ </Col>
367
+ );
368
+ }
369
+ return (
370
+ <Col key={idx} span={colSpan}>
371
+ <div style={fieldStyle}>{child}</div>
372
+ </Col>
373
+ );
374
+ });
375
+
376
+ content = <Row gutter={[4, 4]}>{wrappedChildren}</Row>;
377
+ }
378
+
379
+ return (
380
+ <CollapsibleContent>
381
+ <div
382
+ ref={ref}
383
+ data-label-align={effectiveLabelAlign}
384
+ className={cn('pt-4', labelAlignClass, className)}
385
+ {...props}
386
+ >
387
+ {content}
388
+ </div>
389
+ </CollapsibleContent>
390
+ );
391
+ },
392
+ );
393
+ FilterBarPanel.displayName = 'FilterBarPanel';
394
+
395
+ // ─── FilterBarActions ────────────────────────────────────────────────────────
396
+
397
+ export interface FilterBarActionsProps
398
+ extends React.HTMLAttributes<HTMLDivElement> {
399
+ /**
400
+ * 搜索按钮文案。
401
+ * @default "搜索"
402
+ */
403
+ submitText?: string;
404
+ /**
405
+ * 重置按钮文案。
406
+ * @default "重置"
407
+ */
408
+ resetText?: string;
409
+ /**
410
+ * 是否显示搜索按钮。
411
+ * @default true
412
+ */
413
+ showSubmit?: boolean;
414
+ /**
415
+ * 是否显示重置按钮。
416
+ * @default true
417
+ */
418
+ showReset?: boolean;
419
+ }
420
+
421
+ /**
422
+ * `FilterBarActions` 操作按钮区 — 搜索 + 重置按钮。
423
+ * 搜索走 `form.handleSubmit → onFilter`,重置走 `form.reset → onReset`。
424
+ */
425
+ const FilterBarActions = React.forwardRef<
426
+ HTMLDivElement,
427
+ FilterBarActionsProps
428
+ >(
429
+ (
430
+ {
431
+ submitText = '搜索',
432
+ resetText = '重置',
433
+ showSubmit = true,
434
+ showReset = true,
435
+ className,
436
+ children,
437
+ ...props
438
+ },
439
+ ref,
440
+ ) => {
441
+ const { onFilter, onReset } = useFilterBarContext();
442
+ const form = useFormContext();
443
+
444
+ const handleSubmit = React.useCallback(() => {
445
+ if (!form) return;
446
+ form.handleSubmit((values) => {
447
+ onFilter?.(values);
448
+ })();
449
+ }, [form, onFilter]);
450
+
451
+ const handleReset = React.useCallback(() => {
452
+ if (!form) return;
453
+ form.reset();
454
+ const values = form.getValues();
455
+ onReset?.(values);
456
+ }, [form, onReset]);
457
+
458
+ return (
459
+ <div
460
+ ref={ref}
461
+ className={cn('flex items-center justify-end gap-2 pt-3', className)}
462
+ {...props}
463
+ >
464
+ {children}
465
+ {showReset && (
466
+ <Button type="button" variant="outline" onClick={handleReset}>
467
+ <RotateCcw className="size-4" />
468
+ {resetText}
469
+ </Button>
470
+ )}
471
+ {showSubmit && (
472
+ <Button type="button" onClick={handleSubmit}>
473
+ <Search className="size-4" />
474
+ {submitText}
475
+ </Button>
476
+ )}
477
+ </div>
478
+ );
479
+ },
480
+ );
481
+ FilterBarActions.displayName = 'FilterBarActions';
482
+
483
+ // ─── FilterBarPreset ─────────────────────────────────────────────────────────
484
+
485
+ export interface FilterBarPresetProps extends Omit<FilterBarProps, 'children'> {
486
+ /**
487
+ * 行内区字段 — 放置高频筛选 FormField。
488
+ */
489
+ inlineFields: React.ReactNode;
490
+ /**
491
+ * 面板区字段 — 放置更多筛选 FormField,传入即显示展开按钮。
492
+ */
493
+ panelFields?: React.ReactNode;
494
+ /**
495
+ * 面板静态列数(走 Row/Col 24 栅格,被 `responsiveColumns` 覆盖)。
496
+ * @default 3
497
+ */
498
+ columns?: 1 | 2 | 3 | 4;
499
+ /**
500
+ * 面板响应式列数 — 标量或 5 档断点映射(base / xs / s / m / l / xl,
501
+ * 断点 432/672/944/1200/1600,对齐设计规范 §4.1)。优先级高于 `columns`。
502
+ *
503
+ * @example responsiveColumns={{ base: 1, xs: 2, m: 3, l: 4 }}
504
+ */
505
+ responsiveColumns?: ResponsiveValue<1 | 2 | 3 | 4>;
506
+ /**
507
+ * 搜索按钮文案。
508
+ * @default "搜索"
509
+ */
510
+ submitText?: string;
511
+ /**
512
+ * 重置按钮文案。
513
+ * @default "重置"
514
+ */
515
+ resetText?: string;
516
+ }
517
+
518
+ /**
519
+ * `FilterBarPreset` 预设组合 — 一个组件搞定行内搜索 + 可展开面板 + 栅格布局 + 按钮。
520
+ * 适用于标准列表页筛选场景,无需手动组合子组件。
521
+ */
522
+ const FilterBarPreset = React.forwardRef<HTMLDivElement, FilterBarPresetProps>(
523
+ (
524
+ {
525
+ form,
526
+ inlineFields,
527
+ panelFields,
528
+ columns = 3,
529
+ responsiveColumns,
530
+ submitText = '搜索',
531
+ resetText = '重置',
532
+ ...filterBarProps
533
+ },
534
+ ref,
535
+ ) => {
536
+ return (
537
+ <FilterBar ref={ref} form={form} {...filterBarProps}>
538
+ <FilterBarSearch autoFilter={!panelFields}>
539
+ {inlineFields}
540
+ {panelFields && <FilterBarTrigger />}
541
+ </FilterBarSearch>
542
+ {panelFields && (
543
+ <FilterBarPanel
544
+ columns={columns}
545
+ responsiveColumns={responsiveColumns}
546
+ >
547
+ {panelFields}
548
+ <FilterBarActions submitText={submitText} resetText={resetText} />
549
+ </FilterBarPanel>
550
+ )}
551
+ </FilterBar>
552
+ );
553
+ },
554
+ );
555
+ FilterBarPreset.displayName = 'FilterBarPreset';
556
+
557
+ // ─── Exports ─────────────────────────────────────────────────────────────────
558
+
559
+ export {
560
+ FilterBar,
561
+ FilterBarSearch,
562
+ FilterBarTrigger,
563
+ FilterBarPanel,
564
+ FilterBarActions,
565
+ FilterBarPreset,
566
+ filterBarVariants,
567
+ useFilterBarContext,
568
+ };