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