@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
@@ -1,5 +1,12 @@
1
1
  import * as React from 'react';
2
- import { ChevronDown, ChevronRight } from 'lucide-react';
2
+ import {
3
+ Check,
4
+ ChevronDown,
5
+ ChevronRight,
6
+ Loader2,
7
+ Search,
8
+ X,
9
+ } from 'lucide-react';
3
10
 
4
11
  import { cn } from '@/utils/cn';
5
12
  import { Button } from '@/components/button/button';
@@ -9,6 +16,8 @@ import {
9
16
  PopoverTrigger,
10
17
  } from '@/components/popover/popover';
11
18
 
19
+ // ─── Types ─────────────────────────────────────────────────────────────────
20
+
12
21
  export interface CascaderOption {
13
22
  /** 级联节点 value(同级 value 必须唯一)。 */
14
23
  value: string;
@@ -18,45 +27,70 @@ export interface CascaderOption {
18
27
  children?: CascaderOption[];
19
28
  /** 禁用此项。 */
20
29
  disabled?: boolean;
30
+ /**
31
+ * 显式标记为叶子节点(用于 loadData 模式:当 children 为空但 isLeaf=false 时,
32
+ * 点击该节点触发异步加载;若 isLeaf=true 则直接当叶子处理)。
33
+ * @default 自动推断(无 children 即为叶子)
34
+ */
35
+ isLeaf?: boolean;
21
36
  }
22
37
 
23
- export interface CascaderProps {
38
+ interface CascaderBaseProps {
24
39
  /** 级联选项树(antd `options` 并集)。 */
25
40
  options: CascaderOption[];
26
- /** 受控值(每层 value 数组)。 */
27
- value?: string[];
28
- /** uncontrolled 初值。 */
29
- defaultValue?: string[];
30
- /** 值变化回调 — 触发条件:点选叶子节点或显式 changeOnSelect。 */
31
- onChange?: (value: string[], selectedOptions: CascaderOption[]) => void;
32
41
  /**
33
42
  * 每选中一级即触发 onChange(antd `changeOnSelect` 并集) — 默认仅选叶子触发。
34
43
  * @default false
35
44
  */
36
45
  changeOnSelect?: boolean;
37
- /**
38
- * 占位文本。
39
- * @default "请选择"
40
- */
46
+ /** 占位文本。 @default "请选择" */
41
47
  placeholder?: string;
42
- /**
43
- * 路径展示的分隔符。
44
- * @default " / "
45
- */
48
+ /** 路径展示的分隔符。 @default " / " */
46
49
  separator?: string;
47
50
  /** 整体禁用。 */
48
51
  disabled?: boolean;
52
+ /** 触发器 className。 */
53
+ className?: string;
54
+ /** 触发器尺寸。 @default "md" */
55
+ size?: 'sm' | 'md' | 'default' | 'lg';
49
56
  /**
50
- * 触发器 className(给外层 Button)。
57
+ * 启用搜索(面板顶部输入框,过滤匹配路径)。
58
+ * @default false
51
59
  */
52
- className?: string;
60
+ showSearch?: boolean;
53
61
  /**
54
- * 触发器尺寸。
55
- * @default "default"
62
+ * 异步加载子节点 — 当点击非叶子且无 children 时触发。
63
+ * 消费者在回调内 fetch 数据后更新 `options` prop。
56
64
  */
57
- size?: 'sm' | 'default' | 'lg';
65
+ loadData?: (selectedOptions: CascaderOption[]) => void;
58
66
  }
59
67
 
68
+ export interface CascaderSingleProps extends CascaderBaseProps {
69
+ /** 单选模式(默认)。 */
70
+ multiple?: false | undefined;
71
+ /** 受控值(路径 value 数组)。 */
72
+ value?: string[];
73
+ /** 非受控初值。 */
74
+ defaultValue?: string[];
75
+ /** 值变化回调。 */
76
+ onChange?: (value: string[], selectedOptions: CascaderOption[]) => void;
77
+ }
78
+
79
+ export interface CascaderMultipleProps extends CascaderBaseProps {
80
+ /** 多选模式。 */
81
+ multiple: true;
82
+ /** 受控值(多条路径,每条为 value 数组)。 */
83
+ value?: string[][];
84
+ /** 非受控初值。 */
85
+ defaultValue?: string[][];
86
+ /** 值变化回调。 */
87
+ onChange?: (value: string[][], selectedOptions: CascaderOption[][]) => void;
88
+ }
89
+
90
+ export type CascaderProps = CascaderSingleProps | CascaderMultipleProps;
91
+
92
+ // ─── Helpers ───────────────────────────────────────────────────────────────
93
+
60
94
  function findPath(
61
95
  options: CascaderOption[],
62
96
  value: string[],
@@ -64,7 +98,9 @@ function findPath(
64
98
  const path: CascaderOption[] = [];
65
99
  let cursor: CascaderOption[] | undefined = options;
66
100
  for (const v of value) {
67
- const found: CascaderOption | undefined = cursor?.find((o) => o.value === v);
101
+ const found: CascaderOption | undefined = cursor?.find(
102
+ (o) => o.value === v,
103
+ );
68
104
  if (!found) break;
69
105
  path.push(found);
70
106
  cursor = found.children;
@@ -72,68 +108,284 @@ function findPath(
72
108
  return path;
73
109
  }
74
110
 
111
+ function isNodeLeaf(opt: CascaderOption): boolean {
112
+ if (opt.isLeaf !== undefined) return opt.isLeaf;
113
+ return !opt.children || opt.children.length === 0;
114
+ }
115
+
116
+ /** 用于 multiple 去重的 path → key。 */
117
+ const pathKey = (p: string[]): string => p.join('\0');
118
+
119
+ /** 将 options 树展开为 [path, labels] 列表(用于搜索)。 */
120
+ function flattenOptions(
121
+ options: CascaderOption[],
122
+ parentPath: CascaderOption[] = [],
123
+ ): { path: CascaderOption[]; values: string[] }[] {
124
+ const results: { path: CascaderOption[]; values: string[] }[] = [];
125
+ for (const opt of options) {
126
+ const currentPath = [...parentPath, opt];
127
+ const values = currentPath.map((o) => o.value);
128
+ if (opt.children && opt.children.length > 0) {
129
+ // 有子级则递归
130
+ results.push(...flattenOptions(opt.children, currentPath));
131
+ } else {
132
+ // 叶子节点加入结果
133
+ results.push({ path: currentPath, values });
134
+ }
135
+ }
136
+ return results;
137
+ }
138
+
139
+ function getLabel(node: React.ReactNode): string {
140
+ if (typeof node === 'string') return node;
141
+ if (typeof node === 'number') return String(node);
142
+ return '';
143
+ }
144
+
145
+ // ─── Component ─────────────────────────────────────────────────────────────
146
+
75
147
  /**
76
- * 级联选择 — antd 独有补足。**等价 antd `Cascader`**。多级联动选择(地区 / 分类 / 组织架构),
77
- * 选项树由 `options.children` 表达;点击触发器弹出多列面板,**叶子触发 onChange**(可改 `changeOnSelect` 为每级触发)。
148
+ * 级联选择 — antd 独有补足。多级联动选择(地区 / 分类 / 组织架构),
149
+ * 支持单选 / 多选(checkbox) / 搜索过滤 / 异步加载(loadData)。
78
150
  */
79
151
  const Cascader = React.forwardRef<HTMLButtonElement, CascaderProps>(
80
- (
81
- {
152
+ (props, ref) => {
153
+ const {
82
154
  options,
83
- value,
84
- defaultValue,
85
- onChange,
86
155
  changeOnSelect = false,
87
156
  placeholder = '请选择',
88
157
  separator = ' / ',
89
158
  disabled = false,
90
159
  className,
91
- size = 'default',
92
- },
93
- ref,
94
- ) => {
95
- const isControlled = value !== undefined;
96
- const [internal, setInternal] = React.useState<string[]>(defaultValue ?? []);
97
- const current = isControlled ? value! : internal;
160
+ size = 'md',
161
+ showSearch = false,
162
+ loadData,
163
+ multiple = false,
164
+ } = props;
165
+
166
+ // ─── Single mode state ───────────────────────────────
167
+ const singleValue = !multiple
168
+ ? (props as CascaderSingleProps).value
169
+ : undefined;
170
+ const singleDefault = !multiple
171
+ ? (props as CascaderSingleProps).defaultValue
172
+ : undefined;
173
+ const singleOnChange = !multiple
174
+ ? (props as CascaderSingleProps).onChange
175
+ : undefined;
176
+
177
+ const isSingleControlled = !multiple && singleValue !== undefined;
178
+ const [singleInternal, setSingleInternal] = React.useState<string[]>(
179
+ singleDefault ?? [],
180
+ );
181
+ const currentSingle = isSingleControlled ? singleValue! : singleInternal;
98
182
 
183
+ // ─── Multiple mode state ─────────────────────────────
184
+ const multiValue = multiple
185
+ ? (props as CascaderMultipleProps).value
186
+ : undefined;
187
+ const multiDefault = multiple
188
+ ? (props as CascaderMultipleProps).defaultValue
189
+ : undefined;
190
+ const multiOnChange = multiple
191
+ ? (props as CascaderMultipleProps).onChange
192
+ : undefined;
193
+
194
+ const isMultiControlled = multiple && multiValue !== undefined;
195
+ const [multiInternal, setMultiInternal] = React.useState<string[][]>(
196
+ multiDefault ?? [],
197
+ );
198
+ const currentMulti = isMultiControlled ? multiValue! : multiInternal;
199
+
200
+ // ─── Shared state ────────────────────────────────────
99
201
  const [open, setOpen] = React.useState(false);
100
- const [activePath, setActivePath] = React.useState<string[]>(current);
202
+ const [activePath, setActivePath] = React.useState<string[]>(
203
+ multiple ? [] : currentSingle,
204
+ );
205
+ const [searchText, setSearchText] = React.useState('');
206
+ const [loadingKeys, setLoadingKeys] = React.useState<Set<string>>(
207
+ new Set(),
208
+ );
209
+
210
+ React.useEffect(() => {
211
+ if (open) {
212
+ setActivePath(multiple ? [] : currentSingle);
213
+ setSearchText('');
214
+ }
215
+ }, [open]);
101
216
 
217
+ // ─── loadData 响应:当 options 更新时清除对应 loading ──
102
218
  React.useEffect(() => {
103
- if (open) setActivePath(current);
104
- }, [open, current]);
219
+ if (loadingKeys.size === 0) return;
220
+ setLoadingKeys((prev) => {
221
+ const next = new Set(prev);
222
+ for (const key of prev) {
223
+ const parts = key.split('\0');
224
+ const path = findPath(options, parts);
225
+ const last = path[path.length - 1];
226
+ if (last?.children && last.children.length > 0) {
227
+ next.delete(key);
228
+ }
229
+ }
230
+ return next.size === prev.size ? prev : next;
231
+ });
232
+ }, [options, loadingKeys]);
105
233
 
106
- const selectedPath = findPath(options, current);
234
+ // ─── Handlers ────────────────────────────────────────
107
235
 
108
236
  const handleSelect = (level: number, opt: CascaderOption) => {
109
237
  if (opt.disabled) return;
110
238
  const next = [...activePath.slice(0, level), opt.value];
111
239
  setActivePath(next);
112
- const hasChildren = !!opt.children && opt.children.length > 0;
113
- if (!hasChildren || changeOnSelect) {
240
+
241
+ const leaf = isNodeLeaf(opt);
242
+
243
+ // loadData: 非叶子且无子级时触发异步加载
244
+ if (!leaf && (!opt.children || opt.children.length === 0) && loadData) {
114
245
  const path = findPath(options, next);
115
- if (!isControlled) setInternal(next);
116
- onChange?.(next, path);
117
- if (!hasChildren) setOpen(false);
246
+ const key = pathKey(next);
247
+ setLoadingKeys((prev) => new Set(prev).add(key));
248
+ loadData(path);
249
+ return;
118
250
  }
251
+
252
+ if (!leaf && !changeOnSelect) return;
253
+
254
+ if (multiple) {
255
+ // multiple: toggle selection
256
+ const existing = currentMulti.findIndex(
257
+ (p) => pathKey(p) === pathKey(next),
258
+ );
259
+ let nextMulti: string[][];
260
+ if (existing >= 0) {
261
+ nextMulti = currentMulti.filter((_, i) => i !== existing);
262
+ } else {
263
+ nextMulti = [...currentMulti, next];
264
+ }
265
+ if (!isMultiControlled) setMultiInternal(nextMulti);
266
+ const paths = nextMulti.map((v) => findPath(options, v));
267
+ multiOnChange?.(nextMulti, paths);
268
+ // 不自动关闭
269
+ } else {
270
+ // single
271
+ const path = findPath(options, next);
272
+ if (!isSingleControlled) setSingleInternal(next);
273
+ singleOnChange?.(next, path);
274
+ if (leaf) setOpen(false);
275
+ }
276
+ };
277
+
278
+ const handleSearchSelect = (values: string[]) => {
279
+ if (multiple) {
280
+ const existing = currentMulti.findIndex(
281
+ (p) => pathKey(p) === pathKey(values),
282
+ );
283
+ let nextMulti: string[][];
284
+ if (existing >= 0) {
285
+ nextMulti = currentMulti.filter((_, i) => i !== existing);
286
+ } else {
287
+ nextMulti = [...currentMulti, values];
288
+ }
289
+ if (!isMultiControlled) setMultiInternal(nextMulti);
290
+ const paths = nextMulti.map((v) => findPath(options, v));
291
+ multiOnChange?.(nextMulti, paths);
292
+ } else {
293
+ const path = findPath(options, values);
294
+ if (!isSingleControlled) setSingleInternal(values);
295
+ singleOnChange?.(values, path);
296
+ setOpen(false);
297
+ }
298
+ };
299
+
300
+ const handleRemoveTag = (idx: number) => {
301
+ const nextMulti = currentMulti.filter((_, i) => i !== idx);
302
+ if (!isMultiControlled) setMultiInternal(nextMulti);
303
+ const paths = nextMulti.map((v) => findPath(options, v));
304
+ multiOnChange?.(nextMulti, paths);
119
305
  };
120
306
 
121
- // Render columns based on activePath
307
+ // ─── Columns ─────────────────────────────────────────
122
308
  const columns: CascaderOption[][] = [options];
123
309
  let cursor: CascaderOption[] | undefined = options;
124
310
  for (const v of activePath) {
125
- const found: CascaderOption | undefined = cursor?.find((o) => o.value === v);
311
+ const found: CascaderOption | undefined = cursor?.find(
312
+ (o) => o.value === v,
313
+ );
126
314
  if (found?.children && found.children.length > 0) {
127
315
  columns.push(found.children);
128
316
  cursor = found.children;
129
317
  } else break;
130
318
  }
131
319
 
132
- const display =
133
- selectedPath.length > 0
134
- ? selectedPath.map((o) => o.label).join(separator)
135
- : '';
320
+ // ─── Search results ──────────────────────────────────
321
+ const searchResults = React.useMemo(() => {
322
+ if (!showSearch || !searchText.trim()) return [];
323
+ const term = searchText.trim().toLowerCase();
324
+ const all = flattenOptions(options);
325
+ return all.filter((item) =>
326
+ item.path.some((opt) =>
327
+ getLabel(opt.label).toLowerCase().includes(term),
328
+ ),
329
+ );
330
+ }, [showSearch, searchText, options]);
331
+
332
+ // ─── Display ─────────────────────────────────────────
333
+ let triggerContent: React.ReactNode;
334
+ if (multiple) {
335
+ if (currentMulti.length === 0) {
336
+ triggerContent = (
337
+ <span className="text-muted-foreground">{placeholder}</span>
338
+ );
339
+ } else {
340
+ triggerContent = (
341
+ <span className="flex flex-wrap items-center gap-1 overflow-hidden">
342
+ {currentMulti.slice(0, 3).map((path, i) => {
343
+ const opts = findPath(options, path);
344
+ const label = opts.map((o) => getLabel(o.label)).join(separator);
345
+ return (
346
+ <span
347
+ key={pathKey(path)}
348
+ className="inline-flex max-w-32 items-center gap-0.5 truncate rounded bg-muted px-1.5 py-0.5 text-xs"
349
+ >
350
+ <span className="truncate">{label}</span>
351
+ <X
352
+ className="size-3 shrink-0 cursor-pointer opacity-60 hover:opacity-100"
353
+ onClick={(e) => {
354
+ e.stopPropagation();
355
+ handleRemoveTag(i);
356
+ }}
357
+ />
358
+ </span>
359
+ );
360
+ })}
361
+ {currentMulti.length > 3 && (
362
+ <span className="text-xs text-muted-foreground">
363
+ +{currentMulti.length - 3}
364
+ </span>
365
+ )}
366
+ </span>
367
+ );
368
+ }
369
+ } else {
370
+ const selectedPath = findPath(options, currentSingle);
371
+ const display =
372
+ selectedPath.length > 0
373
+ ? selectedPath.map((o) => o.label).join(separator)
374
+ : '';
375
+ triggerContent = (
376
+ <span className={cn('truncate', !display && 'text-muted-foreground')}>
377
+ {display || placeholder}
378
+ </span>
379
+ );
380
+ }
381
+
382
+ // ─── selectedSet (multiple) ──────────────────────────
383
+ const selectedSet = React.useMemo(
384
+ () => new Set(currentMulti.map(pathKey)),
385
+ [currentMulti],
386
+ );
136
387
 
388
+ // ─── Render ──────────────────────────────────────────
137
389
  return (
138
390
  <Popover open={open} onOpenChange={setOpen}>
139
391
  <PopoverTrigger asChild>
@@ -144,50 +396,141 @@ const Cascader = React.forwardRef<HTMLButtonElement, CascaderProps>(
144
396
  size={size}
145
397
  disabled={disabled}
146
398
  className={cn(
147
- 'min-w-[200px] justify-between font-normal',
148
- !display && 'text-muted-foreground',
399
+ 'min-w-panel-sm justify-between font-normal',
149
400
  className,
150
401
  )}
151
402
  >
152
- <span className="truncate">{display || placeholder}</span>
403
+ {triggerContent}
153
404
  <ChevronDown className="ml-2 size-4 shrink-0 opacity-50" />
154
405
  </Button>
155
406
  </PopoverTrigger>
156
- <PopoverContent className="flex w-auto p-0" align="start">
157
- {columns.map((col, level) => (
407
+ <PopoverContent className="w-auto p-0" align="start">
408
+ {/* Search input */}
409
+ {showSearch && (
410
+ <div className="flex items-center gap-2 border-b border-border px-3 py-2">
411
+ <Search className="size-4 text-muted-foreground" />
412
+ <input
413
+ type="text"
414
+ value={searchText}
415
+ onChange={(e) => setSearchText(e.target.value)}
416
+ placeholder="搜索..."
417
+ className="flex-1 bg-transparent text-xs outline-none placeholder:text-muted-foreground"
418
+ autoFocus
419
+ />
420
+ </div>
421
+ )}
422
+
423
+ {/* Search results */}
424
+ {showSearch && searchText.trim() ? (
158
425
  <ul
159
- key={level}
160
426
  role="listbox"
161
- className={cn(
162
- 'min-w-[160px] max-h-72 overflow-auto py-1 text-sm',
163
- level > 0 && 'border-l',
164
- )}
427
+ className="max-h-72 min-w-48 overflow-auto p-1 text-xs"
165
428
  >
166
- {col.map((opt) => {
167
- const isActive = activePath[level] === opt.value;
168
- const hasChildren = !!opt.children && opt.children.length > 0;
169
- return (
170
- <li
171
- key={opt.value}
172
- role="option"
173
- aria-selected={isActive}
174
- onClick={() => handleSelect(level, opt)}
175
- className={cn(
176
- 'flex cursor-pointer items-center justify-between gap-2 px-3 py-1.5',
177
- !opt.disabled && 'hover:bg-accent',
178
- isActive && !opt.disabled && 'bg-accent text-accent-foreground',
179
- opt.disabled && 'cursor-not-allowed opacity-50',
180
- )}
181
- >
182
- <span className="truncate">{opt.label}</span>
183
- {hasChildren ? (
184
- <ChevronRight className="size-3.5 shrink-0 opacity-60" />
185
- ) : null}
186
- </li>
187
- );
188
- })}
429
+ {searchResults.length === 0 ? (
430
+ <li className="px-2 py-4 text-center text-muted-foreground">
431
+ 无匹配结果
432
+ </li>
433
+ ) : (
434
+ searchResults.map((item) => {
435
+ const key = pathKey(item.values);
436
+ const isSelected = multiple
437
+ ? selectedSet.has(key)
438
+ : pathKey(currentSingle) === key;
439
+ return (
440
+ <li
441
+ key={key}
442
+ role="option"
443
+ aria-selected={isSelected}
444
+ onClick={() => handleSearchSelect(item.values)}
445
+ className={cn(
446
+ 'flex cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 transition-colors hover:bg-accent/50',
447
+ isSelected &&
448
+ 'bg-accent font-medium text-accent-foreground',
449
+ )}
450
+ >
451
+ {multiple && (
452
+ <span
453
+ className={cn(
454
+ 'flex size-4 shrink-0 items-center justify-center rounded border border-primary',
455
+ isSelected && 'bg-primary text-primary-foreground',
456
+ )}
457
+ >
458
+ {isSelected && <Check className="size-3" />}
459
+ </span>
460
+ )}
461
+ <span className="truncate">
462
+ {item.path
463
+ .map((o) => getLabel(o.label))
464
+ .join(separator)}
465
+ </span>
466
+ </li>
467
+ );
468
+ })
469
+ )}
189
470
  </ul>
190
- ))}
471
+ ) : (
472
+ /* Column panels */
473
+ <div className="flex">
474
+ {columns.map((col, level) => (
475
+ <ul
476
+ key={level}
477
+ role="listbox"
478
+ className={cn(
479
+ 'min-w-submenu max-h-72 overflow-auto p-1 text-xs',
480
+ level > 0 && 'border-l border-l-border',
481
+ )}
482
+ >
483
+ {col.map((opt) => {
484
+ const isActive = activePath[level] === opt.value;
485
+ const leaf = isNodeLeaf(opt);
486
+ const hasChildren = !leaf;
487
+ const nodeKey = pathKey([
488
+ ...activePath.slice(0, level),
489
+ opt.value,
490
+ ]);
491
+ const isLoading = loadingKeys.has(nodeKey);
492
+ const isChecked = multiple && selectedSet.has(nodeKey);
493
+ return (
494
+ <li
495
+ key={opt.value}
496
+ role="option"
497
+ aria-selected={isActive}
498
+ onClick={() => handleSelect(level, opt)}
499
+ className={cn(
500
+ 'flex cursor-pointer items-center justify-between gap-2 rounded-sm px-2 py-1.5 transition-colors',
501
+ !opt.disabled && 'hover:bg-accent/50',
502
+ isActive &&
503
+ !opt.disabled &&
504
+ 'bg-accent font-medium text-accent-foreground',
505
+ opt.disabled && 'cursor-not-allowed opacity-50',
506
+ )}
507
+ >
508
+ <span className="flex items-center gap-1.5 truncate">
509
+ {multiple && leaf && (
510
+ <span
511
+ className={cn(
512
+ 'flex size-4 shrink-0 items-center justify-center rounded border border-primary',
513
+ isChecked &&
514
+ 'bg-primary text-primary-foreground',
515
+ )}
516
+ >
517
+ {isChecked && <Check className="size-3" />}
518
+ </span>
519
+ )}
520
+ <span className="truncate">{opt.label}</span>
521
+ </span>
522
+ {isLoading ? (
523
+ <Loader2 className="size-3.5 shrink-0 animate-spin text-muted-foreground" />
524
+ ) : hasChildren ? (
525
+ <ChevronRight className="size-3.5 shrink-0 text-muted-foreground" />
526
+ ) : null}
527
+ </li>
528
+ );
529
+ })}
530
+ </ul>
531
+ ))}
532
+ </div>
533
+ )}
191
534
  </PopoverContent>
192
535
  </Popover>
193
536
  );