@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
@@ -2,43 +2,71 @@
2
2
  id: select
3
3
  name: Select
4
4
  type: component
5
- category: form
5
+ category: data-entry
6
6
  since: 0.1.0
7
- package: "@teamix-evo/ui"
7
+ package: '@teamix-evo/ui'
8
+ displayName: 下拉选择
8
9
  ---
9
10
 
10
- # Select
11
+ # Select 下拉选择
11
12
 
12
- 下拉选择 — Radix Select 标准实现。**仅单选**(Radix Select 设计如此),多选 / 搜索请用 `Combobox`(基于 Command + Popover,在 v0.x)。
13
- 对应 antd `Select` 的最常见单选场景;antd 的 `mode="multiple"` / `showSearch` / `tags` 等高级形态由 Combobox 接力。
13
+ 下拉选择 — **单组件覆盖 cd `Select` 全部模式**:单选 / 多选 / 搜索 / 标签创建(creatable)/ 远程异步(onSearch)/ 虚拟滚动(virtual)/ 弹层容器(container)。基于 **Popover + cmdk Command + @tanstack/react-virtual**;触发器 `size` 三档与 Button / Input 同档(24 / 32 / 36)。原独立的 `Combobox` 已合并入此组件(参考 ButtonGroup Button、Textarea → Input 范式)。
14
+
15
+ > **W3 重构**(2026-06):原 `Select`(Radix Select 单选基础版) + `Combobox`(独立多选/搜索) 合并为一个数据驱动的 `Select`,完整对齐 cd hybridcloud `Select` 全部能力。原 JSX-style API(`SelectTrigger` / `SelectContent` / `SelectItem`)已下线,业务侧需迁移到数据驱动 API(`options` / `groups` / `value`)。详见 §[迁移指南](#迁移指南--从旧版-jsx-api--新数据驱动-api)。
14
16
 
15
17
  ## When to use
16
18
 
17
- - 单选枚举(状态 / 类型 / 区域 / 时区等)
18
- - 选项 5 时替代 RadioGroup
19
- - 选项较长 / 需要分组 / 需要 Label 分隔
19
+ - 单选(`<Select options={...} />`)— 单值场景
20
+ - 多选(`<Select multiple options={...} />`)— Tag 形式回显已选项
21
+ - 可搜索过滤(`searchable`)— 弹层顶部搜索框,本地 filter
22
+ - 远程异步(`onSearch`)— 输入触发拉接口刷新 options(自动关闭本地 filter)
23
+ - 自由创建标签(`creatable` + `multiple`)— 等价 cd `mode="tag"`,经典 Tag 输入器
24
+ - 大数据集(`virtual`)— ≥ 100 项时启用虚拟滚动,首次渲染显著优化
25
+ - 内层容器场景(`container`)— Dialog / Drawer / 自定义 overlay 内,Portal 挂到指定节点
20
26
 
21
27
  ## When NOT to use
22
28
 
23
- - 多选 / 可搜索 → `Combobox`(v0.x)
24
- - 选项 ≤ 4 → `RadioGroup`(更直观)
25
- - 自由输入 → `Input`
26
- - 需要异步加载选项 → `Combobox` + 自定义数据源
29
+ - 选项 4 → `RadioGroup`(更直观,无需展开)
30
+ - 自由文本输入 → `Input`
31
+ - 树形选择 → `TreeSelect` / `Cascader`
32
+ - 多列级联选择 → `Cascader`
33
+ - 命令面板 / 全局搜索 → `Command`(直接用底层 cmdk,不要走 Select)
27
34
 
28
35
  ## Props
29
36
 
30
- > 以下表格由 `pnpm --filter @teamix-evo/ui gen:meta` 自动生成。下表是 `SelectTrigger` 的 props;`Select`(Root)透传 Radix `value / defaultValue / onValueChange / open / disabled / required / name`。
37
+ > 以下表格由 `pnpm --filter @teamix-evo/ui gen:meta` 自动生成。
31
38
 
32
39
  <!-- auto:props:begin -->
33
40
  | 名称 | 类型 | 默认值 | 必填 | 说明 |
34
41
  | --- | --- | --- | --- | --- |
35
- | `position` | `React.ComponentPropsWithoutRef< typeof SelectPrimitive.Content >['position']` | `"popper"` | – | 浮层定位策略。`popper` 跟随 trigger 并自动避让边界;`item-aligned` 把当前选中项与 trigger 对齐(类似原生 `<select>`)。默认 `popper` antd Select 行为一致。 |
42
+ | `options` | `SelectOption[]` | | – | 扁平选项列表(与 `groups` 二选一) 等价 antd `<Select options>` 数据驱动模式。 |
43
+ | `groups` | `SelectOptionGroup[]` | – | – | 分组选项列表 — 等价 antd `<Select.OptGroup>`,数据驱动版本。 |
44
+ | `multiple` | `boolean` | `false` | – | 多选模式(antd `mode="multiple"` 并集)。开启后 `value` / `defaultValue` 为 `string[]`,选中项以 Tag 形式回显。 |
45
+ | `searchable` | `boolean` | `false` | – | 启用搜索(antd `showSearch` 并集) — 弹层顶部出现搜索输入框。 |
46
+ | `creatable` | `boolean` | `false` | – | 允许创建新项(antd `mode="tag"` 并集) — 输入未匹配现有选项时,顶部显示 "Create &lt;query&gt;" 项,Enter 或点击即创建并选中。配合 `multiple` 是经典 Tag 模式。 创建时通过 `onCreate(value)` 通知消费方(由消费方决定是否把新项追加到 `options`)。 |
47
+ | `onCreate` | `(value: string) => void` | – | – | 创建新项时的回调(creatable 模式) — 消费方据此追加 options。 |
48
+ | `onSearch` | `(query: string) => void` | – | – | 异步搜索(antd `onSearch` 并集)— 输入变化时触发,消费方据 `query` 拉接口刷新 `options`。 触发后默认**关闭本地 filter**(因为远端已经过滤过);如需保留本地 filter,设 `filter={true}` 显式启用。 |
49
+ | `filter` | `boolean \| ((query: string, option: SelectOption) => boolean)` | `true` | – | 是否启用本地 filter:`true`(默认) / `false` / 自定义函数。 配合 `onSearch` 时通常设 `false` — 服务端已过滤,本地不再筛。 |
50
+ | `value` | `SelectValue` | – | – | 受控 value — 单选 `string`、多选 `string[]`。 |
51
+ | `defaultValue` | `SelectValue` | – | – | uncontrolled 初值。 |
52
+ | `onChange` | `((value: string) => void) \| ((value: string[]) => void)` | – | – | value 变化回调。 |
53
+ | `maxTagCount` | `number` | – | – | 多选模式下展示的最大 Tag 数,超出折叠为 `+N`(antd `maxTagCount` 并集)。 |
54
+ | `placeholder` | `string` | `"请选择..."` | – | 触发器占位文本。 |
55
+ | `searchPlaceholder` | `string` | `"搜索..."` | – | 搜索框占位文本(searchable / creatable 时生效)。 |
56
+ | `emptyText` | `string` | `"无匹配项"` | – | 无匹配项时的提示文本。 |
57
+ | `hideEmptyOptionGroup` | `boolean` | `true` | – | 搜索后无匹配项的分组是否隐藏(antd `hideEmptyOptionGroup` 并集)。 |
58
+ | `clearable` | `boolean` | `false` | – | 显示清除按钮(antd `allowClear` 并集) — 触发器有值时右侧 ✕,点击清空。 |
59
+ | `size` | `'sm' \| 'md' \| 'default' \| 'lg'` | `"md"` | – | 触发器尺寸 — 与 Button / Input 同档:`sm`(24) / `md`(32) / `lg`(36)。 |
60
+ | `error` | `boolean` | `false` | – | 错误态(自动设置 `aria-invalid="true"`,渲染 destructive 边框)。 |
61
+ | `disabled` | `boolean` | – | – | 禁用整个组件。 |
62
+ | `className` | `string` | – | – | 触发器额外类名(可覆盖宽度 / 边距等)。 |
63
+ | `contentClassName` | `string` | – | – | 弹层 className。 |
64
+ | `container` | `HTMLElement \| null` | – | – | 弹层挂载容器(antd `popupContainer` 并集) — 透传给 Popover Portal。 默认挂在 `document.body`;在内层滚动容器 / Dialog 内时常需指定。 |
65
+ | `virtual` | `boolean \| { threshold?: number; itemHeight?: number }` | `false (传 `true` 等价 `{ threshold: 100, itemHeight: 36 }`)` | – | 虚拟滚动(antd `virtual`) — 选项数 ≥ `threshold` 时启用 react-virtual, 大数据集下显著优化首次渲染。注意:启用后本地 filter 仍生效但分组渲染受限 (groups + virtual 同时启用时按 flatten 渲染),业务侧大数据集建议直接走 options。 |
36
66
  <!-- auto:props:end -->
37
67
 
38
68
  ## 依赖
39
69
 
40
- > 以下表格由 `pnpm --filter @teamix-evo/ui gen:meta` 自动生成,数据源是 [`manifest.json`](../../../manifest.json)。**手工编辑 marker 之间的内容会在下次生成时被覆盖**。
41
-
42
70
  <!-- auto:deps:begin -->
43
71
  ### 同库依赖
44
72
 
@@ -47,64 +75,181 @@ package: "@teamix-evo/ui"
47
75
  | Entry | 类型 | 描述 |
48
76
  | --- | --- | --- |
49
77
  | `cn` | util | Tailwind className 合并工具(clsx + tailwind-merge) |
78
+ | `command` | component | 命令面板 / 下拉底座 — cmdk(Linear / Raycast 风格)提供过滤 / 键盘导航 / a11y;同时为 Combobox / Select / AutoComplete 提供同源下拉内核 |
79
+ | `popover` | component | 可交互浮层 — Radix Popover + antd arrow 并集,使用 showArrow 控制尖角(与 Tooltip / HoverCard 命名统一) |
50
80
 
51
81
  ### npm 依赖
52
82
 
53
83
  > 业务侧需要先 `pnpm add` / `npm install` 这些包。CLI 在 `ui add` 完成后会列出此提示。
54
84
 
55
85
  ```bash
56
- pnpm add @radix-ui/react-select@^2.1.0 lucide-react@^0.460.0
86
+ pnpm add @tanstack/react-virtual@^3.0.0 lucide-react@^0.460.0
57
87
  ```
58
88
  <!-- auto:deps:end -->
59
89
 
60
- > 完整子组件:`Select / SelectGroup / SelectValue / SelectTrigger / SelectContent / SelectLabel / SelectItem / SelectSeparator / SelectScrollUpButton / SelectScrollDownButton`。
61
-
62
90
  ## AI 生成纪律
63
91
 
64
- - **`SelectValue` placeholder**:`<SelectValue placeholder="..." />` 提供未选时的占位文字
65
- - **每个 SelectItem 必有 value**:不能空字符串(Radix 拒绝);用稳定 ID
66
- - **大量选项必分组**:超过 8 个用 `SelectGroup` + `SelectLabel`,加 `SelectSeparator` 分段
67
- - **不要嵌套 Select**:嵌套下拉是反模式
68
- - **键盘可用性自带**:Radix 自动支持 Type-ahead(首字母搜索),不需要再加 search
92
+ - **数据驱动 API**:`options: SelectOption[]` `groups: SelectOptionGroup[]` 二选一,**不要**回到 JSX 子组件嵌套(原 `<SelectItem>` 已下线)
93
+ - **multiple 模式 value `string[]`** — 单选是 `string`;TypeScript 上联合类型,业务侧传值时形态要与 `multiple` 匹配
94
+ - **creatable 必配 onCreate**:消费方在 `onCreate(value)` 回调中决定是否把新项追加到 `options`(否则只是临时选中,关闭后丢失)
95
+ - **onSearch 异步搜索 + filter=false 配合用**:`onSearch` 默认会关闭本地 filter(避免"客户端 filter + 服务端 filter 双过滤"导致结果空集);如果想保留本地 filter,显式 `filter={true}`
96
+ - **virtual 启用门槛**:`options.length >= threshold`(默认 100)才启用;小数据集走 cmdk 路径(更轻量、自带 keyboard nav)
97
+ - **container 用法**:仅在 Dialog / Drawer / 自定义 overlay 内才需要;默认挂 `document.body` 即可。Dialog 内必须传 container 否则 Popover 会被 Dialog overlay 遮挡
98
+ - **size 三档**:`sm`(24) / `md`(32) / `lg`(36),与 Button / Input 同档;`"default"` 仅向后兼容,新代码用 `"md"`
99
+ - **error 走 aria-invalid**:`error=true` 自动设 `aria-invalid="true"` + destructive 边框;**不要**手写 `border-destructive`
100
+ - **clearable 与 maxTagCount 组合用**:多选 + 大量已选项时,`maxTagCount` 折叠展示,`clearable` 一键清空,UX 更友好
69
101
 
70
102
  ## Examples
71
103
 
72
104
  ```tsx
73
- import {
74
- Select, SelectTrigger, SelectValue, SelectContent,
75
- SelectGroup, SelectLabel, SelectItem, SelectSeparator,
76
- } from '@/components/ui/select';
77
-
78
- // 基础
79
- <Select>
80
- <SelectTrigger className="w-48">
105
+ import { Select } from '@/components/ui/select';
106
+
107
+ // 单选
108
+ <Select
109
+ options={[
110
+ { value: 'bj', label: '北京' },
111
+ { value: 'sh', label: '上海' },
112
+ ]}
113
+ placeholder="选择城市"
114
+ />
115
+
116
+ // 单选 + 受控
117
+ const [v, setV] = React.useState('bj');
118
+ <Select
119
+ options={cityOptions}
120
+ value={v}
121
+ onChange={(next) => setV(next as string)}
122
+ />
123
+
124
+ // 分组
125
+ <Select
126
+ groups={[
127
+ { label: '亚洲', options: [{ value: 'cn', label: '北京' }] },
128
+ { label: '欧洲', options: [{ value: 'uk', label: '伦敦' }] },
129
+ ]}
130
+ />
131
+
132
+ // 可搜索
133
+ <Select searchable options={cityOptions} />
134
+
135
+ // 多选 + 折叠 +N
136
+ <Select multiple maxTagCount={3} options={cityOptions} defaultValue={['bj','sh','hz','sz']} />
137
+
138
+ // Creatable / Tag 模式(对齐 cd `mode="tag"`)
139
+ const [opts, setOpts] = React.useState([...]);
140
+ <Select
141
+ multiple
142
+ creatable
143
+ searchable
144
+ options={opts}
145
+ onCreate={(val) => setOpts(prev => [...prev, { value: val, label: val }])}
146
+ />
147
+
148
+ // 远程异步搜索
149
+ const [opts, setOpts] = React.useState([]);
150
+ <Select
151
+ searchable
152
+ options={opts}
153
+ onSearch={async (q) => {
154
+ const list = await api.search(q);
155
+ setOpts(list);
156
+ }}
157
+ />
158
+
159
+ // 虚拟滚动(5000 项)
160
+ <Select searchable virtual options={hugeList} />
161
+
162
+ // Dialog 内场景:挂到 Dialog 容器
163
+ const dialogRef = React.useRef<HTMLDivElement>(null);
164
+ <DialogContent ref={dialogRef}>
165
+ <Select options={...} container={dialogRef.current} />
166
+ </DialogContent>
167
+ ```
168
+
169
+ ## Select 形态 — 旧库 API → 新库映射
170
+
171
+ > cd hybridcloud `Select` 单组件多模式;新库 W3 重构后也是单组件多模式 — 模式拆为 prop 而非组件。
172
+
173
+ ### 命名映射
174
+
175
+ | cloud-design / antd 旧库 | `@teamix-evo/ui` Select | 备注 |
176
+ | --- | --- | --- |
177
+ | `<Select mode="single">` | `<Select>` | 默认单选 |
178
+ | `<Select mode="multiple">` | `<Select multiple>` | Tag 回显 |
179
+ | `<Select mode="tag">` | `<Select multiple creatable>` | 拆为两个 prop |
180
+ | `<Select.AutoComplete>` | `<Select searchable creatable>` | 合并到 Select |
181
+ | `dataSource` / `children OptGroup` | `options={...}` / `groups={...}` | 数据驱动 |
182
+ | `showSearch` | `searchable` | ✅ |
183
+ | `showSearch + onSearch` | `searchable` + `onSearch` | ✅ async 远程 |
184
+ | `useVirtual` | `virtual`(默认 false,设 true 启用) | ✅ 阈值 100 项 |
185
+ | `popupContainer` | `container` | ✅ 透传 Popover Portal |
186
+ | `popupClassName` / `popupStyle` | `contentClassName` | API 精简 |
187
+ | `maxTagCount` | `maxTagCount` | ✅ 同名 |
188
+ | `hasClear` | `clearable` | ✅ |
189
+ | `value` / `defaultValue` / `onChange` | 同名;value 类型由 `multiple` 决定 | `string` 或 `string[]` |
190
+ | `visible` / `onVisibleChange` | `open` / `onOpenChange` | (Radix Popover 透传) |
191
+ | `hasArrow` | 不修复 — 走 outline 视觉 | — |
192
+ | `fillProps` / `displayValue` | 不修复 — `SelectOption.label` 直接接 ReactNode | — |
193
+ | `filterTreeNode` | `filter` prop(自定义函数) | 单选 / 多选都可 |
194
+ | `hasBorder={false}` | `className="border-0"` 自定义 | — |
195
+
196
+ ### Breaking Changes(从原 v0.1 Select 迁移时需改写)
197
+
198
+ > 注意:这是**新旧 @teamix-evo/ui Select** 之间的 breaking,不是 cd → @teamix-evo/ui 的迁移。cd → @teamix-evo/ui 见上方命名映射表。
199
+
200
+ 1. **JSX 子组件 API 已下线** — 原 `<SelectTrigger>` / `<SelectContent>` / `<SelectItem>` / `<SelectValue>` / `<SelectGroup>` / `<SelectLabel>` / `<SelectSeparator>` 全部移除,改用数据驱动 API
201
+ 2. **value 类型** — 单选保持 `string`,多选必须是 `string[]`(原 v0.1 仅支持单选)
202
+ 3. **onChange 签名** — 原 Radix `onValueChange` 改为 `onChange`,支持 `(string) => void`(单选)或 `(string[]) => void`(多选)
203
+ 4. **`searchable` 默认 false** — 原 Combobox 默认 true;现合并到 Select 后默认关闭(对齐 cd `showSearch` 默认 false)
204
+ 5. **Combobox 已下线** — 原从 `@/components/ui/combobox` 导入 `Combobox` 的代码,改为从 `@/components/ui/select` 导入 `Select` 即可(props 大致兼容,部分命名优化)
205
+
206
+ ### 迁移指南 — 从旧版 JSX API → 新数据驱动 API
207
+
208
+ ```tsx
209
+ // ❌ Before(旧 v0.1 Select,JSX 子组件)
210
+ <Select onValueChange={setV}>
211
+ <SelectTrigger>
81
212
  <SelectValue placeholder="选择城市" />
82
213
  </SelectTrigger>
83
214
  <SelectContent>
84
215
  <SelectItem value="bj">北京</SelectItem>
85
216
  <SelectItem value="sh">上海</SelectItem>
86
- <SelectItem value="hz">杭州</SelectItem>
87
217
  </SelectContent>
88
218
  </Select>
89
219
 
90
- // 分组
91
- <Select>
92
- <SelectTrigger className="w-48"><SelectValue placeholder="选择时区" /></SelectTrigger>
93
- <SelectContent>
94
- <SelectGroup>
95
- <SelectLabel>亚洲</SelectLabel>
96
- <SelectItem value="cn">北京时间 (UTC+8)</SelectItem>
97
- <SelectItem value="jp">东京时间 (UTC+9)</SelectItem>
98
- </SelectGroup>
99
- <SelectSeparator />
100
- <SelectGroup>
101
- <SelectLabel>欧洲</SelectLabel>
102
- <SelectItem value="uk">伦敦时间 (UTC+0)</SelectItem>
103
- </SelectGroup>
104
- </SelectContent>
105
- </Select>
220
+ // ✅ After(W3+,数据驱动)
221
+ <Select
222
+ onChange={(next) => setV(next as string)}
223
+ placeholder="选择城市"
224
+ options={[
225
+ { value: 'bj', label: '北京' },
226
+ { value: 'sh', label: '上海' },
227
+ ]}
228
+ />
229
+ ```
106
230
 
107
- // 受控
108
- const [v, setV] = React.useState('bj');
109
- <Select value={v} onValueChange={setV}>...</Select>
231
+ ```tsx
232
+ // Before(旧 Combobox)
233
+ import { Combobox } from '@/components/ui/combobox';
234
+ <Combobox multiple searchable options={...} />
235
+
236
+ // ✅ After(W3+,Combobox 合一进 Select)
237
+ import { Select } from '@/components/ui/select';
238
+ <Select multiple searchable options={...} />
110
239
  ```
240
+
241
+ ### 不修复 / 后续工序清单
242
+
243
+ - **`hasArrow` / `hasBorder={false}`**:走 outline trigger 视觉一致;需要无边框时自定义 `className="border-0 shadow-none"`
244
+ - **`fillProps` / `displayValue`**:`SelectOption.label` 已支持 ReactNode,自定义渲染直接传节点
245
+ - **`itemRender` / `valueRender`**:同上,通过 label 节点渲染
246
+ - **`onMouseEnter` / `onMouseLeave` 选项级**:不修复 — 业务低频,需要时用 `label: <span onMouseEnter={...}>` 自管
247
+ - **`isPreview` / `renderPreview`**:不修复 — 走 `disabled` + 自定义渲染替代
248
+
249
+ ### Select 专项 AI 生成纪律
250
+
251
+ - 数据驱动 — `options` 或 `groups` 二选一;`groups[].options[]` 不可空数组
252
+ - `keywords` 用于扩展搜索匹配 — 当 `label` 是富节点(如带图标)无法直接 includes 匹配时,通过 `keywords: ['搜索关键字','别名']` 让 cmdk 命中
253
+ - creatable 时业务侧 `onCreate` 必须更新 `options`,否则关闭弹层后新创建项丢失(只在已选 value 中,但下次打开时无对应 label)
254
+ - 远程搜索时 `emptyText` 建议条件渲染:加载中 → "加载中...",未输入 → "请输入关键字",无结果 → "无匹配项"
255
+ - virtual 启用后 `groups` 渲染受限(强制 flatten),业务大数据集建议直接走 `options` 扁平列表
@@ -1,32 +1,64 @@
1
- import type { Meta, StoryObj } from '@storybook/react';
2
- import {
3
- Select,
4
- SelectTrigger,
5
- SelectValue,
6
- SelectContent,
7
- SelectGroup,
8
- SelectLabel,
9
- SelectItem,
10
- SelectSeparator,
11
- } from './select';
12
-
13
- const meta: Meta<typeof SelectTrigger> = {
14
- title: '表单与输入 · Form/Select',
15
- component: SelectTrigger,
1
+ import * as React from 'react';
2
+ import type { Meta, StoryObj } from '@storybook/react-vite';
3
+ import { Select } from './select';
4
+
5
+ const cityOptions = [
6
+ { value: 'bj', label: '北京' },
7
+ { value: 'sh', label: '上海' },
8
+ { value: 'hz', label: '杭州' },
9
+ { value: 'sz', label: '深圳' },
10
+ ];
11
+
12
+ const tzGroups = [
13
+ {
14
+ label: '亚洲',
15
+ options: [
16
+ { value: 'cn', label: '北京时间 (UTC+8)' },
17
+ { value: 'jp', label: '东京时间 (UTC+9)' },
18
+ { value: 'sg', label: '新加坡时间 (UTC+8)' },
19
+ ],
20
+ },
21
+ {
22
+ label: '欧洲',
23
+ options: [
24
+ { value: 'uk', label: '伦敦时间 (UTC+0)' },
25
+ { value: 'de', label: '柏林时间 (UTC+1)' },
26
+ ],
27
+ },
28
+ ];
29
+
30
+ const meta: Meta<typeof Select> = {
31
+ title: '数据录入 · Data Entry/Select',
32
+ component: Select,
16
33
  tags: ['autodocs'],
17
34
  parameters: {
18
35
  docs: {
19
36
  description: {
20
37
  component:
21
- '下拉选择 — 从预置选项中选中一个值,由按钮触发弹出选项列表。Radix Select 实现 + antd Select 的并集能力:支持 `SelectGroup` / `SelectLabel` / `SelectSeparator` 组合出分组下拉,`size`(sm / default / lg)可调,默认带键盘导航与文本检索。视觉走 OpenTrek semantic tokens,所有样式来自 `@teamix-evo/design`,无 mock。',
38
+ '下拉选择 — 单组件覆盖 cd `Select` 全部模式:**单选 / 多选 / 搜索 / 标签创建(creatable)/ 远程异步 / 虚拟滚动**。基于 Popover + cmdk Command;触发器 `size` 三档与 Button / Input 同档(24/32/36)。原 `Combobox` 已合并入此组件。',
22
39
  },
23
40
  },
24
41
  },
25
42
  argTypes: {
26
- size: { control: 'inline-radio', options: ['sm', 'default', 'lg'] },
43
+ size: { control: 'inline-radio', options: ['sm', 'md', 'lg'] },
44
+ multiple: { control: 'boolean' },
45
+ searchable: { control: 'boolean' },
46
+ creatable: { control: 'boolean' },
47
+ clearable: { control: 'boolean' },
27
48
  disabled: { control: 'boolean' },
49
+ error: { control: 'boolean' },
50
+ placeholder: { control: 'text' },
51
+ },
52
+ args: {
53
+ size: 'md',
54
+ multiple: false,
55
+ searchable: false,
56
+ creatable: false,
57
+ clearable: false,
58
+ disabled: false,
59
+ error: false,
60
+ placeholder: '请选择...',
28
61
  },
29
- args: { size: 'default' },
30
62
  decorators: [
31
63
  (Story) => (
32
64
  <div className="w-64">
@@ -37,64 +69,207 @@ const meta: Meta<typeof SelectTrigger> = {
37
69
  };
38
70
 
39
71
  export default meta;
40
- type Story = StoryObj<typeof SelectTrigger>;
72
+ type Story = StoryObj<typeof Select>;
41
73
 
42
74
  export const Playground: Story = {
43
- render: (args) => (
44
- <Select>
45
- <SelectTrigger {...args}>
46
- <SelectValue placeholder="选择城市" />
47
- </SelectTrigger>
48
- <SelectContent>
49
- <SelectItem value="bj">北京</SelectItem>
50
- <SelectItem value="sh">上海</SelectItem>
51
- <SelectItem value="hz">杭州</SelectItem>
52
- <SelectItem value="sz">深圳</SelectItem>
53
- </SelectContent>
54
- </Select>
75
+ render: (args) => <Select {...args} options={cityOptions} />,
76
+ };
77
+
78
+ export const Sizes: Story = {
79
+ parameters: { controls: { disable: true } },
80
+ render: () => (
81
+ <div className="flex flex-col gap-3">
82
+ {(['sm', 'md', 'lg'] as const).map((s) => (
83
+ <Select
84
+ key={s}
85
+ size={s}
86
+ options={cityOptions}
87
+ placeholder={`size = ${s} (h-${s === 'sm' ? '6' : s === 'md' ? '8' : '9'})`}
88
+ />
89
+ ))}
90
+ </div>
55
91
  ),
56
92
  };
57
93
 
58
94
  export const Grouped: Story = {
95
+ parameters: { controls: { disable: true } },
96
+ render: () => <Select groups={tzGroups} placeholder="选择时区" />,
97
+ };
98
+
99
+ export const Searchable: Story = {
59
100
  parameters: { controls: { disable: true } },
60
101
  render: () => (
61
- <Select>
62
- <SelectTrigger>
63
- <SelectValue placeholder="选择时区" />
64
- </SelectTrigger>
65
- <SelectContent>
66
- <SelectGroup>
67
- <SelectLabel>亚洲</SelectLabel>
68
- <SelectItem value="cn">北京时间 (UTC+8)</SelectItem>
69
- <SelectItem value="jp">东京时间 (UTC+9)</SelectItem>
70
- <SelectItem value="sg">新加坡时间 (UTC+8)</SelectItem>
71
- </SelectGroup>
72
- <SelectSeparator />
73
- <SelectGroup>
74
- <SelectLabel>欧洲</SelectLabel>
75
- <SelectItem value="uk">伦敦时间 (UTC+0)</SelectItem>
76
- <SelectItem value="de">柏林时间 (UTC+1)</SelectItem>
77
- </SelectGroup>
78
- </SelectContent>
79
- </Select>
102
+ <Select
103
+ searchable
104
+ options={cityOptions}
105
+ placeholder="可搜索的城市"
106
+ searchPlaceholder="输入城市名..."
107
+ />
80
108
  ),
81
109
  };
82
110
 
83
- export const Sizes: Story = {
111
+ export const Multiple: Story = {
84
112
  parameters: { controls: { disable: true } },
85
113
  render: () => (
86
- <div className="flex flex-col gap-3">
87
- {(['sm', 'default', 'lg'] as const).map((s) => (
88
- <Select key={s}>
89
- <SelectTrigger size={s}>
90
- <SelectValue placeholder={`size = ${s}`} />
91
- </SelectTrigger>
92
- <SelectContent>
93
- <SelectItem value="a">选项 A</SelectItem>
94
- <SelectItem value="b">选项 B</SelectItem>
95
- </SelectContent>
96
- </Select>
97
- ))}
114
+ <Select
115
+ multiple
116
+ options={cityOptions}
117
+ placeholder="选择多个城市"
118
+ defaultValue={['bj', 'hz']}
119
+ />
120
+ ),
121
+ };
122
+
123
+ export const MultipleWithMaxTagCount: Story = {
124
+ parameters: { controls: { disable: true } },
125
+ render: () => (
126
+ <Select
127
+ multiple
128
+ maxTagCount={2}
129
+ options={cityOptions}
130
+ placeholder="最多展示 2 个 Tag,其余折叠 +N"
131
+ defaultValue={['bj', 'sh', 'hz', 'sz']}
132
+ />
133
+ ),
134
+ };
135
+
136
+ /**
137
+ * Creatable / Tag 模式 — 等价 cd `Select mode="tag"`。
138
+ * 输入未匹配 options 的内容时,顶部显示"创建 'xxx'"项,Enter 即创建并选中。
139
+ * 配合 `multiple` 是经典 Tag 输入器(自由标签创建 + 已选标签 chip 展示)。
140
+ */
141
+ export const CreatableTags: Story = {
142
+ parameters: { controls: { disable: true } },
143
+ render: () => {
144
+ const [opts, setOpts] = React.useState([
145
+ { value: 'react', label: 'React' },
146
+ { value: 'vue', label: 'Vue' },
147
+ { value: 'angular', label: 'Angular' },
148
+ ]);
149
+ return (
150
+ <Select
151
+ multiple
152
+ creatable
153
+ searchable
154
+ options={opts}
155
+ placeholder="选择 / 创建技术栈"
156
+ searchPlaceholder="输入并按 Enter 创建..."
157
+ onCreate={(val) => setOpts((prev) => [...prev, { value: val, label: val }])}
158
+ />
159
+ );
160
+ },
161
+ };
162
+
163
+ /**
164
+ * 远程异步搜索 — 等价 cd `Select showSearch onSearch`。
165
+ * `onSearch` 在输入变化时触发(本 demo 模拟接口延迟),消费方据 `query` 拉接口
166
+ * 刷新 `options`;同时本地 filter 默认关闭(已由服务端过滤)。
167
+ */
168
+ export const AsyncRemoteSearch: Story = {
169
+ parameters: { controls: { disable: true } },
170
+ render: () => {
171
+ const [opts, setOpts] = React.useState<{ value: string; label: string }[]>(
172
+ [],
173
+ );
174
+ const [loading, setLoading] = React.useState(false);
175
+
176
+ const handleSearch = React.useCallback(async (q: string) => {
177
+ if (!q.trim()) {
178
+ setOpts([]);
179
+ return;
180
+ }
181
+ setLoading(true);
182
+ // 模拟接口请求
183
+ await new Promise((r) => setTimeout(r, 300));
184
+ setOpts([
185
+ { value: `${q}-1`, label: `${q} 远程结果 1` },
186
+ { value: `${q}-2`, label: `${q} 远程结果 2` },
187
+ { value: `${q}-3`, label: `${q} 远程结果 3` },
188
+ ]);
189
+ setLoading(false);
190
+ }, []);
191
+
192
+ return (
193
+ <Select
194
+ searchable
195
+ options={opts}
196
+ onSearch={handleSearch}
197
+ placeholder="远程搜索"
198
+ searchPlaceholder="输入查询关键字..."
199
+ emptyText={loading ? '加载中...' : '输入关键字开始搜索'}
200
+ />
201
+ );
202
+ },
203
+ };
204
+
205
+ /**
206
+ * 虚拟滚动 — 选项数 ≥ `threshold`(默认 100)时启用 `@tanstack/react-virtual`,
207
+ * 大数据集首次渲染显著提速。本 demo 渲染 5000 项。
208
+ */
209
+ export const Virtual: Story = {
210
+ parameters: { controls: { disable: true } },
211
+ render: () => {
212
+ const opts = React.useMemo(
213
+ () =>
214
+ Array.from({ length: 5000 }, (_, i) => ({
215
+ value: `item-${i}`,
216
+ label: `选项 #${i.toString().padStart(4, '0')}`,
217
+ })),
218
+ [],
219
+ );
220
+ return (
221
+ <Select
222
+ searchable
223
+ virtual
224
+ options={opts}
225
+ placeholder="5000 项 — 虚拟滚动"
226
+ searchPlaceholder="搜索 5000 个选项..."
227
+ />
228
+ );
229
+ },
230
+ };
231
+
232
+ export const Clearable: Story = {
233
+ parameters: { controls: { disable: true } },
234
+ render: () => (
235
+ <Select
236
+ clearable
237
+ options={cityOptions}
238
+ defaultValue="bj"
239
+ placeholder="可清除"
240
+ />
241
+ ),
242
+ };
243
+
244
+ export const Disabled: Story = {
245
+ parameters: { controls: { disable: true } },
246
+ render: () => <Select disabled options={cityOptions} placeholder="已禁用" />,
247
+ };
248
+
249
+ export const Error: Story = {
250
+ parameters: { controls: { disable: true } },
251
+ render: () => (
252
+ <div className="flex flex-col gap-1">
253
+ <Select error options={cityOptions} placeholder="请选择(必填)" />
254
+ <span className="text-xs text-destructive">该字段为必填项</span>
98
255
  </div>
99
256
  ),
100
257
  };
258
+
259
+ export const Controlled: Story = {
260
+ parameters: { controls: { disable: true } },
261
+ render: () => {
262
+ const [v, setV] = React.useState<string>('');
263
+ return (
264
+ <div className="flex flex-col gap-2">
265
+ <Select
266
+ options={cityOptions}
267
+ value={v}
268
+ onChange={(next: string | string[]) => setV(next as string)}
269
+ placeholder="受控单选"
270
+ />
271
+ <span className="text-xs text-muted-foreground">当前值: {v || '(未选)'}</span>
272
+ </div>
273
+ );
274
+ },
275
+ };