@teamix-evo/ui 0.1.1

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 (270) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +336 -0
  3. package/_data.json +12 -0
  4. package/manifest.json +1688 -0
  5. package/package.json +90 -0
  6. package/src/components/accordion/accordion.meta.md +87 -0
  7. package/src/components/accordion/accordion.stories.tsx +67 -0
  8. package/src/components/accordion/accordion.tsx +58 -0
  9. package/src/components/affix/affix.meta.md +80 -0
  10. package/src/components/affix/affix.stories.tsx +57 -0
  11. package/src/components/affix/affix.tsx +97 -0
  12. package/src/components/alert/alert.meta.md +101 -0
  13. package/src/components/alert/alert.stories.tsx +93 -0
  14. package/src/components/alert/alert.tsx +132 -0
  15. package/src/components/alert-dialog/alert-dialog.meta.md +107 -0
  16. package/src/components/alert-dialog/alert-dialog.stories.tsx +81 -0
  17. package/src/components/alert-dialog/alert-dialog.tsx +136 -0
  18. package/src/components/anchor/anchor.meta.md +87 -0
  19. package/src/components/anchor/anchor.stories.tsx +74 -0
  20. package/src/components/anchor/anchor.tsx +130 -0
  21. package/src/components/app/app.meta.md +86 -0
  22. package/src/components/app/app.stories.tsx +62 -0
  23. package/src/components/app/app.tsx +58 -0
  24. package/src/components/aspect-ratio/aspect-ratio.meta.md +81 -0
  25. package/src/components/aspect-ratio/aspect-ratio.stories.tsx +59 -0
  26. package/src/components/aspect-ratio/aspect-ratio.tsx +22 -0
  27. package/src/components/auto-complete/auto-complete.meta.md +102 -0
  28. package/src/components/auto-complete/auto-complete.stories.tsx +93 -0
  29. package/src/components/auto-complete/auto-complete.tsx +205 -0
  30. package/src/components/avatar/avatar.meta.md +94 -0
  31. package/src/components/avatar/avatar.stories.tsx +80 -0
  32. package/src/components/avatar/avatar.tsx +126 -0
  33. package/src/components/badge/badge.meta.md +119 -0
  34. package/src/components/badge/badge.stories.tsx +153 -0
  35. package/src/components/badge/badge.tsx +210 -0
  36. package/src/components/breadcrumb/breadcrumb.meta.md +107 -0
  37. package/src/components/breadcrumb/breadcrumb.stories.tsx +84 -0
  38. package/src/components/breadcrumb/breadcrumb.tsx +122 -0
  39. package/src/components/button/button.meta.md +98 -0
  40. package/src/components/button/button.stories.tsx +235 -0
  41. package/src/components/button/button.tsx +160 -0
  42. package/src/components/button-group/button-group.meta.md +92 -0
  43. package/src/components/button-group/button-group.stories.tsx +90 -0
  44. package/src/components/button-group/button-group.tsx +75 -0
  45. package/src/components/calendar/calendar.meta.md +118 -0
  46. package/src/components/calendar/calendar.stories.tsx +68 -0
  47. package/src/components/calendar/calendar.tsx +107 -0
  48. package/src/components/card/card.meta.md +117 -0
  49. package/src/components/card/card.stories.tsx +112 -0
  50. package/src/components/card/card.tsx +222 -0
  51. package/src/components/carousel/carousel.meta.md +117 -0
  52. package/src/components/carousel/carousel.stories.tsx +84 -0
  53. package/src/components/carousel/carousel.tsx +224 -0
  54. package/src/components/cascader/cascader.meta.md +110 -0
  55. package/src/components/cascader/cascader.stories.tsx +108 -0
  56. package/src/components/cascader/cascader.tsx +198 -0
  57. package/src/components/checkbox/checkbox.meta.md +99 -0
  58. package/src/components/checkbox/checkbox.stories.tsx +130 -0
  59. package/src/components/checkbox/checkbox.tsx +125 -0
  60. package/src/components/collapsible/collapsible.meta.md +80 -0
  61. package/src/components/collapsible/collapsible.stories.tsx +35 -0
  62. package/src/components/collapsible/collapsible.tsx +18 -0
  63. package/src/components/color-picker/color-picker.meta.md +84 -0
  64. package/src/components/color-picker/color-picker.stories.tsx +80 -0
  65. package/src/components/color-picker/color-picker.tsx +160 -0
  66. package/src/components/combobox/combobox.meta.md +93 -0
  67. package/src/components/combobox/combobox.stories.tsx +55 -0
  68. package/src/components/combobox/combobox.tsx +130 -0
  69. package/src/components/command/command.meta.md +104 -0
  70. package/src/components/command/command.stories.tsx +59 -0
  71. package/src/components/command/command.tsx +147 -0
  72. package/src/components/context-menu/context-menu.meta.md +90 -0
  73. package/src/components/context-menu/context-menu.stories.tsx +46 -0
  74. package/src/components/context-menu/context-menu.tsx +191 -0
  75. package/src/components/data-table/data-table.meta.md +149 -0
  76. package/src/components/data-table/data-table.stories.tsx +125 -0
  77. package/src/components/data-table/data-table.tsx +185 -0
  78. package/src/components/date-picker/date-picker.meta.md +106 -0
  79. package/src/components/date-picker/date-picker.stories.tsx +58 -0
  80. package/src/components/date-picker/date-picker.tsx +156 -0
  81. package/src/components/descriptions/descriptions.meta.md +78 -0
  82. package/src/components/descriptions/descriptions.stories.tsx +60 -0
  83. package/src/components/descriptions/descriptions.tsx +129 -0
  84. package/src/components/dialog/dialog.meta.md +105 -0
  85. package/src/components/dialog/dialog.stories.tsx +93 -0
  86. package/src/components/dialog/dialog.tsx +128 -0
  87. package/src/components/drawer/drawer.meta.md +96 -0
  88. package/src/components/drawer/drawer.stories.tsx +54 -0
  89. package/src/components/drawer/drawer.tsx +114 -0
  90. package/src/components/dropdown-menu/dropdown-menu.meta.md +103 -0
  91. package/src/components/dropdown-menu/dropdown-menu.stories.tsx +112 -0
  92. package/src/components/dropdown-menu/dropdown-menu.tsx +195 -0
  93. package/src/components/empty/empty.meta.md +81 -0
  94. package/src/components/empty/empty.stories.tsx +46 -0
  95. package/src/components/empty/empty.tsx +47 -0
  96. package/src/components/field/field.meta.md +116 -0
  97. package/src/components/field/field.stories.tsx +117 -0
  98. package/src/components/field/field.tsx +164 -0
  99. package/src/components/flex/flex.meta.md +94 -0
  100. package/src/components/flex/flex.stories.tsx +112 -0
  101. package/src/components/flex/flex.tsx +122 -0
  102. package/src/components/float-button/float-button.meta.md +87 -0
  103. package/src/components/float-button/float-button.stories.tsx +78 -0
  104. package/src/components/float-button/float-button.tsx +143 -0
  105. package/src/components/form/form.meta.md +131 -0
  106. package/src/components/form/form.stories.tsx +122 -0
  107. package/src/components/form/form.tsx +194 -0
  108. package/src/components/grid/grid.meta.md +87 -0
  109. package/src/components/grid/grid.stories.tsx +99 -0
  110. package/src/components/grid/grid.tsx +130 -0
  111. package/src/components/hover-card/hover-card.meta.md +92 -0
  112. package/src/components/hover-card/hover-card.stories.tsx +68 -0
  113. package/src/components/hover-card/hover-card.tsx +29 -0
  114. package/src/components/image/image.meta.md +94 -0
  115. package/src/components/image/image.stories.tsx +55 -0
  116. package/src/components/image/image.tsx +138 -0
  117. package/src/components/input/input.meta.md +109 -0
  118. package/src/components/input/input.stories.tsx +117 -0
  119. package/src/components/input/input.tsx +213 -0
  120. package/src/components/input-group/input-group.meta.md +92 -0
  121. package/src/components/input-group/input-group.stories.tsx +88 -0
  122. package/src/components/input-group/input-group.tsx +107 -0
  123. package/src/components/input-number/input-number.meta.md +91 -0
  124. package/src/components/input-number/input-number.stories.tsx +87 -0
  125. package/src/components/input-number/input-number.tsx +210 -0
  126. package/src/components/input-otp/input-otp.meta.md +105 -0
  127. package/src/components/input-otp/input-otp.stories.tsx +65 -0
  128. package/src/components/input-otp/input-otp.tsx +97 -0
  129. package/src/components/item/item.meta.md +116 -0
  130. package/src/components/item/item.stories.tsx +113 -0
  131. package/src/components/item/item.tsx +171 -0
  132. package/src/components/kbd/kbd.meta.md +85 -0
  133. package/src/components/kbd/kbd.stories.tsx +70 -0
  134. package/src/components/kbd/kbd.tsx +81 -0
  135. package/src/components/label/label.meta.md +91 -0
  136. package/src/components/label/label.stories.tsx +87 -0
  137. package/src/components/label/label.tsx +66 -0
  138. package/src/components/masonry/masonry.meta.md +85 -0
  139. package/src/components/masonry/masonry.stories.tsx +66 -0
  140. package/src/components/masonry/masonry.tsx +59 -0
  141. package/src/components/mentions/mentions.meta.md +89 -0
  142. package/src/components/mentions/mentions.stories.tsx +75 -0
  143. package/src/components/mentions/mentions.tsx +237 -0
  144. package/src/components/menubar/menubar.meta.md +100 -0
  145. package/src/components/menubar/menubar.stories.tsx +81 -0
  146. package/src/components/menubar/menubar.tsx +232 -0
  147. package/src/components/native-select/native-select.meta.md +88 -0
  148. package/src/components/native-select/native-select.stories.tsx +80 -0
  149. package/src/components/native-select/native-select.tsx +54 -0
  150. package/src/components/navigation-menu/navigation-menu.meta.md +108 -0
  151. package/src/components/navigation-menu/navigation-menu.stories.tsx +112 -0
  152. package/src/components/navigation-menu/navigation-menu.tsx +125 -0
  153. package/src/components/notification/notification.meta.md +91 -0
  154. package/src/components/notification/notification.stories.tsx +96 -0
  155. package/src/components/notification/notification.tsx +84 -0
  156. package/src/components/pagination/pagination.meta.md +127 -0
  157. package/src/components/pagination/pagination.stories.tsx +62 -0
  158. package/src/components/pagination/pagination.tsx +285 -0
  159. package/src/components/popconfirm/popconfirm.meta.md +109 -0
  160. package/src/components/popconfirm/popconfirm.stories.tsx +76 -0
  161. package/src/components/popconfirm/popconfirm.tsx +134 -0
  162. package/src/components/popover/popover.meta.md +97 -0
  163. package/src/components/popover/popover.stories.tsx +82 -0
  164. package/src/components/popover/popover.tsx +55 -0
  165. package/src/components/progress/progress.meta.md +86 -0
  166. package/src/components/progress/progress.stories.tsx +75 -0
  167. package/src/components/progress/progress.tsx +195 -0
  168. package/src/components/radio-group/radio-group.meta.md +103 -0
  169. package/src/components/radio-group/radio-group.stories.tsx +77 -0
  170. package/src/components/radio-group/radio-group.tsx +78 -0
  171. package/src/components/rate/rate.meta.md +87 -0
  172. package/src/components/rate/rate.stories.tsx +81 -0
  173. package/src/components/rate/rate.tsx +153 -0
  174. package/src/components/resizable/resizable.meta.md +92 -0
  175. package/src/components/resizable/resizable.stories.tsx +104 -0
  176. package/src/components/resizable/resizable.tsx +56 -0
  177. package/src/components/result/result.meta.md +90 -0
  178. package/src/components/result/result.stories.tsx +71 -0
  179. package/src/components/result/result.tsx +91 -0
  180. package/src/components/scroll-area/scroll-area.meta.md +84 -0
  181. package/src/components/scroll-area/scroll-area.stories.tsx +41 -0
  182. package/src/components/scroll-area/scroll-area.tsx +51 -0
  183. package/src/components/segmented/segmented.meta.md +103 -0
  184. package/src/components/segmented/segmented.stories.tsx +101 -0
  185. package/src/components/segmented/segmented.tsx +138 -0
  186. package/src/components/select/select.meta.md +110 -0
  187. package/src/components/select/select.stories.tsx +100 -0
  188. package/src/components/select/select.tsx +188 -0
  189. package/src/components/separator/separator.meta.md +74 -0
  190. package/src/components/separator/separator.stories.tsx +71 -0
  191. package/src/components/separator/separator.tsx +104 -0
  192. package/src/components/sheet/sheet.meta.md +97 -0
  193. package/src/components/sheet/sheet.stories.tsx +82 -0
  194. package/src/components/sheet/sheet.tsx +139 -0
  195. package/src/components/sidebar/sidebar.meta.md +131 -0
  196. package/src/components/sidebar/sidebar.stories.tsx +82 -0
  197. package/src/components/sidebar/sidebar.tsx +351 -0
  198. package/src/components/skeleton/skeleton.meta.md +95 -0
  199. package/src/components/skeleton/skeleton.stories.tsx +79 -0
  200. package/src/components/skeleton/skeleton.tsx +144 -0
  201. package/src/components/slider/slider.meta.md +94 -0
  202. package/src/components/slider/slider.stories.tsx +69 -0
  203. package/src/components/slider/slider.tsx +86 -0
  204. package/src/components/sonner/sonner.meta.md +96 -0
  205. package/src/components/sonner/sonner.stories.tsx +91 -0
  206. package/src/components/sonner/sonner.tsx +40 -0
  207. package/src/components/space/space.meta.md +94 -0
  208. package/src/components/space/space.stories.tsx +94 -0
  209. package/src/components/space/space.tsx +106 -0
  210. package/src/components/spinner/spinner.meta.md +76 -0
  211. package/src/components/spinner/spinner.stories.tsx +71 -0
  212. package/src/components/spinner/spinner.tsx +64 -0
  213. package/src/components/statistic/statistic.meta.md +99 -0
  214. package/src/components/statistic/statistic.stories.tsx +71 -0
  215. package/src/components/statistic/statistic.tsx +197 -0
  216. package/src/components/steps/steps.meta.md +102 -0
  217. package/src/components/steps/steps.stories.tsx +75 -0
  218. package/src/components/steps/steps.tsx +170 -0
  219. package/src/components/switch/switch.meta.md +92 -0
  220. package/src/components/switch/switch.stories.tsx +75 -0
  221. package/src/components/switch/switch.tsx +101 -0
  222. package/src/components/table/table.meta.md +95 -0
  223. package/src/components/table/table.stories.tsx +75 -0
  224. package/src/components/table/table.tsx +122 -0
  225. package/src/components/tabs/tabs.meta.md +98 -0
  226. package/src/components/tabs/tabs.stories.tsx +70 -0
  227. package/src/components/tabs/tabs.tsx +119 -0
  228. package/src/components/tag/tag.meta.md +94 -0
  229. package/src/components/tag/tag.stories.tsx +77 -0
  230. package/src/components/tag/tag.tsx +185 -0
  231. package/src/components/textarea/textarea.meta.md +83 -0
  232. package/src/components/textarea/textarea.stories.tsx +63 -0
  233. package/src/components/textarea/textarea.tsx +113 -0
  234. package/src/components/time-picker/time-picker.meta.md +83 -0
  235. package/src/components/time-picker/time-picker.stories.tsx +59 -0
  236. package/src/components/time-picker/time-picker.tsx +94 -0
  237. package/src/components/timeline/timeline.meta.md +102 -0
  238. package/src/components/timeline/timeline.stories.tsx +104 -0
  239. package/src/components/timeline/timeline.tsx +147 -0
  240. package/src/components/toggle/toggle.meta.md +88 -0
  241. package/src/components/toggle/toggle.stories.tsx +66 -0
  242. package/src/components/toggle/toggle.tsx +53 -0
  243. package/src/components/toggle-group/toggle-group.meta.md +90 -0
  244. package/src/components/toggle-group/toggle-group.stories.tsx +83 -0
  245. package/src/components/toggle-group/toggle-group.tsx +78 -0
  246. package/src/components/tooltip/tooltip.meta.md +99 -0
  247. package/src/components/tooltip/tooltip.stories.tsx +71 -0
  248. package/src/components/tooltip/tooltip.tsx +93 -0
  249. package/src/components/tour/tour.meta.md +116 -0
  250. package/src/components/tour/tour.stories.tsx +66 -0
  251. package/src/components/tour/tour.tsx +242 -0
  252. package/src/components/transfer/transfer.meta.md +90 -0
  253. package/src/components/transfer/transfer.stories.tsx +68 -0
  254. package/src/components/transfer/transfer.tsx +251 -0
  255. package/src/components/tree/tree.meta.md +111 -0
  256. package/src/components/tree/tree.stories.tsx +109 -0
  257. package/src/components/tree/tree.tsx +367 -0
  258. package/src/components/tree-select/tree-select.meta.md +100 -0
  259. package/src/components/tree-select/tree-select.stories.tsx +80 -0
  260. package/src/components/tree-select/tree-select.tsx +171 -0
  261. package/src/components/typography/typography.meta.md +102 -0
  262. package/src/components/typography/typography.stories.tsx +115 -0
  263. package/src/components/typography/typography.tsx +245 -0
  264. package/src/components/upload/upload.meta.md +111 -0
  265. package/src/components/upload/upload.stories.tsx +75 -0
  266. package/src/components/upload/upload.tsx +265 -0
  267. package/src/components/watermark/watermark.meta.md +95 -0
  268. package/src/components/watermark/watermark.stories.tsx +78 -0
  269. package/src/components/watermark/watermark.tsx +165 -0
  270. package/src/utils/cn.ts +6 -0
@@ -0,0 +1,160 @@
1
+ import * as React from 'react';
2
+
3
+ import { cn } from '@/utils/cn';
4
+ import { Input } from '@/components/input/input';
5
+ import {
6
+ Popover,
7
+ PopoverContent,
8
+ PopoverTrigger,
9
+ } from '@/components/popover/popover';
10
+
11
+ export interface ColorPickerProps {
12
+ /** 受控值 — `#RRGGBB` 或 `#RRGGBBAA`(开 `allowAlpha` 时)。 */
13
+ value?: string;
14
+ /** uncontrolled 初值。 @default "#000000" */
15
+ defaultValue?: string;
16
+ /** 值变化回调。 */
17
+ onChange?: (value: string) => void;
18
+ /**
19
+ * 是否允许透明度(antd `format=hex8` 类似行为) — 启用后展示 alpha 滑块,value 变为 8 位 hex。
20
+ * @default false
21
+ */
22
+ allowAlpha?: boolean;
23
+ /**
24
+ * 预设色块(antd `presets` 并集) — 一组常用颜色快捷选择。
25
+ */
26
+ presets?: string[];
27
+ /** 整体禁用。 */
28
+ disabled?: boolean;
29
+ /** 触发器尺寸。 @default "default" */
30
+ size?: 'sm' | 'default' | 'lg';
31
+ /** 触发器 className。 */
32
+ className?: string;
33
+ }
34
+
35
+ function clampHex(hex: string, allowAlpha: boolean): string {
36
+ const cleaned = hex.replace(/[^0-9a-fA-F#]/g, '');
37
+ const withHash = cleaned.startsWith('#') ? cleaned : `#${cleaned}`;
38
+ const expected = allowAlpha ? 9 : 7;
39
+ return withHash.slice(0, expected);
40
+ }
41
+
42
+ /**
43
+ * 颜色选择 — antd 独有补足。**等价 antd `ColorPicker`**(v5.5+)。基于原生
44
+ * `<input type="color">` + alpha range 输入 + hex 文本输入 + 预设色块,
45
+ * 提供"触发器(色块)→ 弹出面板"的标准交互。
46
+ */
47
+ const ColorPicker = React.forwardRef<HTMLButtonElement, ColorPickerProps>(
48
+ (
49
+ {
50
+ value,
51
+ defaultValue = '#000000',
52
+ onChange,
53
+ allowAlpha = false,
54
+ presets,
55
+ disabled = false,
56
+ size = 'default',
57
+ className,
58
+ },
59
+ ref,
60
+ ) => {
61
+ const isControlled = value !== undefined;
62
+ const [internal, setInternal] = React.useState<string>(defaultValue);
63
+ const current = isControlled ? value! : internal;
64
+
65
+ const update = (next: string) => {
66
+ const v = clampHex(next, allowAlpha);
67
+ if (!isControlled) setInternal(v);
68
+ onChange?.(v);
69
+ };
70
+
71
+ const baseHex = current.slice(0, 7);
72
+ const alphaHex = allowAlpha && current.length >= 9 ? current.slice(7, 9) : 'ff';
73
+ const alphaInt = parseInt(alphaHex, 16);
74
+
75
+ const swatchSize = size === 'sm' ? 'size-7' : size === 'lg' ? 'size-11' : 'size-9';
76
+
77
+ return (
78
+ <Popover>
79
+ <PopoverTrigger asChild>
80
+ <button
81
+ ref={ref}
82
+ type="button"
83
+ disabled={disabled}
84
+ className={cn(
85
+ 'inline-flex items-center justify-center rounded-md border border-input bg-background shadow-sm transition-colors',
86
+ 'focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring',
87
+ 'disabled:cursor-not-allowed disabled:opacity-50',
88
+ swatchSize,
89
+ className,
90
+ )}
91
+ aria-label="选择颜色"
92
+ style={{ padding: 2 }}
93
+ >
94
+ <span
95
+ className="block size-full rounded-sm"
96
+ style={{ backgroundColor: current }}
97
+ />
98
+ </button>
99
+ </PopoverTrigger>
100
+ <PopoverContent className="w-64 p-3" align="start">
101
+ <div className="flex flex-col gap-3">
102
+ <input
103
+ type="color"
104
+ value={baseHex}
105
+ disabled={disabled}
106
+ onChange={(e) => update(allowAlpha ? `${e.target.value}${alphaHex}` : e.target.value)}
107
+ className="h-10 w-full cursor-pointer rounded-md border border-input bg-transparent"
108
+ />
109
+ {allowAlpha ? (
110
+ <div className="flex items-center gap-2">
111
+ <span className="w-12 text-xs text-muted-foreground">Alpha</span>
112
+ <input
113
+ type="range"
114
+ min={0}
115
+ max={255}
116
+ value={alphaInt}
117
+ disabled={disabled}
118
+ onChange={(e) => {
119
+ const a = Number(e.target.value).toString(16).padStart(2, '0');
120
+ update(`${baseHex}${a}`);
121
+ }}
122
+ className="flex-1"
123
+ />
124
+ <span className="w-10 text-right text-xs tabular-nums">
125
+ {Math.round((alphaInt / 255) * 100)}%
126
+ </span>
127
+ </div>
128
+ ) : null}
129
+ <Input
130
+ value={current}
131
+ disabled={disabled}
132
+ onChange={(e) => update(e.target.value)}
133
+ placeholder={allowAlpha ? '#RRGGBBAA' : '#RRGGBB'}
134
+ />
135
+ {presets && presets.length > 0 ? (
136
+ <div className="flex flex-wrap gap-1.5">
137
+ {presets.map((p) => (
138
+ <button
139
+ key={p}
140
+ type="button"
141
+ aria-label={p}
142
+ onClick={() => update(p)}
143
+ className={cn(
144
+ 'size-6 rounded-sm border border-border ring-offset-background transition-all',
145
+ 'hover:scale-110 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
146
+ )}
147
+ style={{ backgroundColor: p }}
148
+ />
149
+ ))}
150
+ </div>
151
+ ) : null}
152
+ </div>
153
+ </PopoverContent>
154
+ </Popover>
155
+ );
156
+ },
157
+ );
158
+ ColorPicker.displayName = 'ColorPicker';
159
+
160
+ export { ColorPicker };
@@ -0,0 +1,93 @@
1
+ ---
2
+ id: combobox
3
+ name: Combobox
4
+ type: component
5
+ category: form
6
+ since: 0.1.0
7
+ package: "@teamix-evo/ui"
8
+ ---
9
+
10
+ # Combobox
11
+
12
+ 可搜索单选下拉 — `Command + Popover` 复合,等价 antd `AutoComplete` / `Select showSearch` 的核心场景。
13
+ **单选**(刻意不做多选,保持语义简单);多选请直接用 `Command` + 自管 selected 数组。
14
+
15
+ ## When to use
16
+
17
+ - 选项较多需要搜索筛选(国家 / 时区 / 用户列表 / 标签)
18
+ - 选项动态加载(异步 — 在父组件准备数据后传给 `options`)
19
+
20
+ ## When NOT to use
21
+
22
+ - 选项 ≤ 7 个 → `Select`(无需搜索)
23
+ - 多选 → `Command` 自定义,或 `CheckboxGroup`
24
+ - 自由文本输入 → `Input` + 自定义 suggestion(超出本组件范围)
25
+
26
+ <!-- auto:props:begin -->
27
+ | 名称 | 类型 | 默认值 | 必填 | 说明 |
28
+ | --- | --- | --- | --- | --- |
29
+ | `options` | `ComboboxOption[]` | – | ✓ | 选项列表(antd `AutoComplete.options` 并集)。 |
30
+ | `value` | `string` | – | – | 受控 value(单选)。 |
31
+ | `onChange` | `(value: string) => void` | – | – | value 变化回调。 |
32
+ | `placeholder` | `string` | `"请选择..."` | – | 触发器占位文本(未选)。 |
33
+ | `searchPlaceholder` | `string` | `"搜索..."` | – | 搜索框占位文本。 |
34
+ | `emptyText` | `string` | `"无匹配项"` | – | 无匹配时的提示文本。 |
35
+ | `className` | `string` | `"w-[200px]"` | – | 触发器宽度。 |
36
+ | `disabled` | `boolean` | – | – | 是否禁用。 |
37
+ | `size` | `'sm' \| 'default' \| 'lg'` | `"default"` | – | 触发器尺寸。 |
38
+ <!-- auto:props:end -->
39
+
40
+ <!-- auto:deps:begin -->
41
+ ### 同库依赖
42
+
43
+ > `teamix-evo ui add combobox` 时,以下 entry 会被自动连带安装(无需手动 add)。
44
+
45
+ | Entry | 类型 | 描述 |
46
+ | --- | --- | --- |
47
+ | `cn` | util | Tailwind className 合并工具(clsx + tailwind-merge) |
48
+ | `button` | component | 通用按钮 — shadcn 实现 + antd 功能扩展(loading / icon / shape / block / dashed variant) |
49
+ | `command` | component | 命令面板 — cmdk(Linear / Raycast 风格),全局搜索 / 命令执行;Combobox 的底座 |
50
+ | `popover` | component | 可交互浮层 — Radix Popover + antd arrow 并集 |
51
+
52
+ ### npm 依赖
53
+
54
+ > 业务侧需要先 `pnpm add` / `npm install` 这些包。CLI 在 `ui add` 完成后会列出此提示。
55
+
56
+ ```bash
57
+ pnpm add lucide-react@^0.460.0
58
+ ```
59
+ <!-- auto:deps:end -->
60
+
61
+ > 选项类型 `ComboboxOption`:`{ value: string; label: ReactNode; disabled?: boolean }`。
62
+
63
+ ## AI 生成纪律
64
+
65
+ - **`value` 稳定**:用业务 ID,不要用 index
66
+ - **`label` 可富节点**:支持图标 + 文本,不止 string
67
+ - **第二次点击当前 value 会清空**:`onChange('')` 触发,业务侧据此实现"取消选择"
68
+ - **大量选项(>1000) → 自定义虚拟滚动**:本组件未集成 virtualizer
69
+ - **异步加载选项**:父组件 fetch 后传 `options`,加 loading 占位符在 options 列表外
70
+
71
+ ## Examples
72
+
73
+ ```tsx
74
+ import { Combobox, type ComboboxOption } from '@/components/ui/combobox';
75
+ import * as React from 'react';
76
+
77
+ const frameworks: ComboboxOption[] = [
78
+ { value: 'next', label: 'Next.js' },
79
+ { value: 'sveltekit', label: 'SvelteKit' },
80
+ { value: 'nuxt', label: 'Nuxt' },
81
+ { value: 'remix', label: 'Remix' },
82
+ { value: 'astro', label: 'Astro' },
83
+ ];
84
+
85
+ const [v, setV] = React.useState('');
86
+ <Combobox
87
+ options={frameworks}
88
+ value={v}
89
+ onChange={setV}
90
+ placeholder="选择框架"
91
+ searchPlaceholder="搜索框架..."
92
+ />
93
+ ```
@@ -0,0 +1,55 @@
1
+ import * as React from 'react';
2
+ import type { Meta, StoryObj } from '@storybook/react';
3
+ import { Combobox, type ComboboxOption } from './combobox';
4
+
5
+ const meta: Meta<typeof Combobox> = {
6
+ title: '表单与输入 · Form/Combobox',
7
+ component: Combobox,
8
+ tags: ['autodocs'],
9
+ parameters: {
10
+ docs: {
11
+ description: {
12
+ component:
13
+ '可搜索单选下拉 — Command + Popover 复合,对标 antd AutoComplete / Select showSearch。键盘可达、模糊搜索、空态、再次选中清空,适合选项数 > 7 的单选场景。OpenTrek tokens 适配。',
14
+ },
15
+ },
16
+ },
17
+ };
18
+
19
+ export default meta;
20
+ type Story = StoryObj<typeof Combobox>;
21
+
22
+ const frameworks: ComboboxOption[] = [
23
+ { value: 'next', label: 'Next.js' },
24
+ { value: 'sveltekit', label: 'SvelteKit' },
25
+ { value: 'nuxt', label: 'Nuxt' },
26
+ { value: 'remix', label: 'Remix' },
27
+ { value: 'astro', label: 'Astro' },
28
+ { value: 'qwik', label: 'Qwik', disabled: true },
29
+ ];
30
+
31
+ export const Default: Story = {
32
+ render: () => {
33
+ const [v, setV] = React.useState('');
34
+ return (
35
+ <Combobox
36
+ options={frameworks}
37
+ value={v}
38
+ onChange={setV}
39
+ placeholder="选择框架"
40
+ searchPlaceholder="搜索框架..."
41
+ />
42
+ );
43
+ },
44
+ };
45
+
46
+ export const Sizes: Story = {
47
+ parameters: { controls: { disable: true } },
48
+ render: () => (
49
+ <div className="flex flex-col gap-3">
50
+ <Combobox options={frameworks} placeholder="sm" size="sm" />
51
+ <Combobox options={frameworks} placeholder="default" />
52
+ <Combobox options={frameworks} placeholder="lg" size="lg" />
53
+ </div>
54
+ ),
55
+ };
@@ -0,0 +1,130 @@
1
+ import * as React from 'react';
2
+ import { Check, ChevronsUpDown } from 'lucide-react';
3
+
4
+ import { cn } from '@/utils/cn';
5
+ import { Button } from '@/components/button/button';
6
+ import {
7
+ Command,
8
+ CommandEmpty,
9
+ CommandGroup,
10
+ CommandInput,
11
+ CommandItem,
12
+ CommandList,
13
+ } from '@/components/command/command';
14
+ import {
15
+ Popover,
16
+ PopoverContent,
17
+ PopoverTrigger,
18
+ } from '@/components/popover/popover';
19
+
20
+ export interface ComboboxOption {
21
+ /** 选项值(受控 value 比对的依据,稳定 ID)。 */
22
+ value: string;
23
+ /** 显示文本。 */
24
+ label: React.ReactNode;
25
+ /** 禁用此选项。 */
26
+ disabled?: boolean;
27
+ }
28
+
29
+ export interface ComboboxProps {
30
+ /** 选项列表(antd `AutoComplete.options` 并集)。 */
31
+ options: ComboboxOption[];
32
+ /** 受控 value(单选)。 */
33
+ value?: string;
34
+ /** value 变化回调。 */
35
+ onChange?: (value: string) => void;
36
+ /** 触发器占位文本(未选)。 @default "请选择..." */
37
+ placeholder?: string;
38
+ /** 搜索框占位文本。 @default "搜索..." */
39
+ searchPlaceholder?: string;
40
+ /** 无匹配时的提示文本。 @default "无匹配项" */
41
+ emptyText?: string;
42
+ /** 触发器宽度。 @default "w-[200px]" */
43
+ className?: string;
44
+ /** 是否禁用。 */
45
+ disabled?: boolean;
46
+ /** 触发器尺寸。 @default "default" */
47
+ size?: 'sm' | 'default' | 'lg';
48
+ }
49
+
50
+ /**
51
+ * 可搜索单选下拉 — Command + Popover 复合,等价 antd `AutoComplete` /
52
+ * `Select showSearch` 的核心场景。
53
+ *
54
+ * 多选场景请直接用 Command + 自管 selected 数组(本组件刻意不做多选,保持
55
+ * 单选语义清晰)。
56
+ */
57
+ const Combobox = React.forwardRef<HTMLButtonElement, ComboboxProps>(
58
+ (
59
+ {
60
+ options,
61
+ value,
62
+ onChange,
63
+ placeholder = '请选择...',
64
+ searchPlaceholder = '搜索...',
65
+ emptyText = '无匹配项',
66
+ className,
67
+ disabled,
68
+ size = 'default',
69
+ },
70
+ ref,
71
+ ) => {
72
+ const [open, setOpen] = React.useState(false);
73
+ const selected = options.find((o) => o.value === value);
74
+
75
+ return (
76
+ <Popover open={open} onOpenChange={setOpen}>
77
+ <PopoverTrigger asChild>
78
+ <Button
79
+ ref={ref}
80
+ variant="outline"
81
+ size={size}
82
+ disabled={disabled}
83
+ role="combobox"
84
+ aria-expanded={open}
85
+ className={cn(
86
+ 'w-[200px] justify-between font-normal',
87
+ !value && 'text-muted-foreground',
88
+ className,
89
+ )}
90
+ >
91
+ {selected ? selected.label : placeholder}
92
+ <ChevronsUpDown className="ml-2 size-4 shrink-0 opacity-50" />
93
+ </Button>
94
+ </PopoverTrigger>
95
+ <PopoverContent className="w-[200px] p-0">
96
+ <Command>
97
+ <CommandInput placeholder={searchPlaceholder} />
98
+ <CommandList>
99
+ <CommandEmpty>{emptyText}</CommandEmpty>
100
+ <CommandGroup>
101
+ {options.map((opt) => (
102
+ <CommandItem
103
+ key={opt.value}
104
+ value={opt.value}
105
+ disabled={opt.disabled}
106
+ onSelect={(v) => {
107
+ onChange?.(v === value ? '' : v);
108
+ setOpen(false);
109
+ }}
110
+ >
111
+ <Check
112
+ className={cn(
113
+ 'mr-2 size-4',
114
+ opt.value === value ? 'opacity-100' : 'opacity-0',
115
+ )}
116
+ />
117
+ {opt.label}
118
+ </CommandItem>
119
+ ))}
120
+ </CommandGroup>
121
+ </CommandList>
122
+ </Command>
123
+ </PopoverContent>
124
+ </Popover>
125
+ );
126
+ },
127
+ );
128
+ Combobox.displayName = 'Combobox';
129
+
130
+ export { Combobox };
@@ -0,0 +1,104 @@
1
+ ---
2
+ id: command
3
+ name: Command
4
+ type: component
5
+ category: shell
6
+ since: 0.1.0
7
+ package: "@teamix-evo/ui"
8
+ ---
9
+
10
+ # Command
11
+
12
+ 命令面板 — 基于 [`cmdk`](https://cmdk.paco.me/),提供"按 ⌘K 打开"的全局搜索 / 命令执行体验(Linear / Raycast / Vercel 风格)。
13
+ **shadcn-only**(antd 无对标)。是 `Combobox` 的底座(Combobox = Command + Popover)。
14
+
15
+ ## When to use
16
+
17
+ - 全局命令面板(⌘K 搜索 / 跳转 / 执行任务)
18
+ - 复杂筛选 / 多条件搜索
19
+ - 编辑器命令(quick action)
20
+ - 配 `Popover` 做 Combobox(可搜索下拉)
21
+ - 配 `CommandDialog` 做模态命令面板
22
+
23
+ ## When NOT to use
24
+
25
+ - 简单单选下拉 → `Select`
26
+ - 静态菜单 → `DropdownMenu`
27
+ - 需要表单语义 → `Combobox`(基于本组件)
28
+
29
+ <!-- auto:props:begin -->
30
+ _(no props)_
31
+ <!-- auto:props:end -->
32
+
33
+ <!-- auto:deps:begin -->
34
+ ### 同库依赖
35
+
36
+ > `teamix-evo ui add command` 时,以下 entry 会被自动连带安装(无需手动 add)。
37
+
38
+ | Entry | 类型 | 描述 |
39
+ | --- | --- | --- |
40
+ | `cn` | util | Tailwind className 合并工具(clsx + tailwind-merge) |
41
+ | `dialog` | component | 模态对话框 — Radix Dialog + antd Modal 并集(组合式 Header/Footer/Title/Description) |
42
+
43
+ ### npm 依赖
44
+
45
+ > 业务侧需要先 `pnpm add` / `npm install` 这些包。CLI 在 `ui add` 完成后会列出此提示。
46
+
47
+ ```bash
48
+ pnpm add cmdk@^1.0.0 lucide-react@^0.460.0
49
+ ```
50
+ <!-- auto:deps:end -->
51
+
52
+ > 子组件:`Command`(Root 容器)/ `CommandDialog`(模态版,内部组合 Dialog)/ `CommandInput`(自带搜索图标)/ `CommandList`(可滚动列表)/ `CommandEmpty`(无匹配空态)/ `CommandGroup`(分组,带 heading)/ `CommandItem`(选项,支持 `onSelect`)/ `CommandSeparator` / `CommandShortcut`(右对齐快捷键)。
53
+
54
+ ## AI 生成纪律
55
+
56
+ - **`CommandList` 内必有 `CommandEmpty`**:无搜索结果时不渲染会让 UI 空白
57
+ - **`CommandItem` 用 `onSelect` 不是 `onClick`**:cmdk 通过 keyboard 导航触发,需要 `onSelect`
58
+ - **value 必稳定**:`<CommandItem value="...">` 用稳定 ID(决定搜索匹配)
59
+ - **CommandDialog 全局监听 ⌘K**:业务侧加一个 `useEffect` 监听键盘事件 toggle open
60
+ - **不嵌套 Command**:嵌套是反模式
61
+
62
+ ## Examples
63
+
64
+ ```tsx
65
+ import {
66
+ Command, CommandInput, CommandList, CommandEmpty,
67
+ CommandGroup, CommandItem, CommandShortcut,
68
+ } from '@/components/ui/command';
69
+ import { Calendar, Mail, User } from 'lucide-react';
70
+
71
+ // 内联
72
+ <Command className="rounded-lg border">
73
+ <CommandInput placeholder="输入命令或搜索..." />
74
+ <CommandList>
75
+ <CommandEmpty>无结果。</CommandEmpty>
76
+ <CommandGroup heading="建议">
77
+ <CommandItem><Calendar /> 日历 <CommandShortcut>⌘K</CommandShortcut></CommandItem>
78
+ <CommandItem><Mail /> 邮件</CommandItem>
79
+ </CommandGroup>
80
+ <CommandGroup heading="设置">
81
+ <CommandItem><User /> 个人资料</CommandItem>
82
+ </CommandGroup>
83
+ </CommandList>
84
+ </Command>
85
+
86
+ // 模态(全局 ⌘K)
87
+ import { CommandDialog } from '@/components/ui/command';
88
+ const [open, setOpen] = React.useState(false);
89
+ React.useEffect(() => {
90
+ const onKey = (e: KeyboardEvent) => {
91
+ if (e.key === 'k' && (e.metaKey || e.ctrlKey)) {
92
+ e.preventDefault();
93
+ setOpen((o) => !o);
94
+ }
95
+ };
96
+ document.addEventListener('keydown', onKey);
97
+ return () => document.removeEventListener('keydown', onKey);
98
+ }, []);
99
+
100
+ <CommandDialog open={open} onOpenChange={setOpen}>
101
+ <CommandInput placeholder="..." />
102
+ <CommandList>...</CommandList>
103
+ </CommandDialog>
104
+ ```
@@ -0,0 +1,59 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { Calendar, Mail, Settings, User } from 'lucide-react';
3
+ import {
4
+ Command,
5
+ CommandInput,
6
+ CommandList,
7
+ CommandEmpty,
8
+ CommandGroup,
9
+ CommandItem,
10
+ CommandShortcut,
11
+ CommandSeparator,
12
+ } from './command';
13
+
14
+ const meta: Meta<typeof Command> = {
15
+ title: '应用壳 · Shell/Command',
16
+ component: Command,
17
+ tags: ['autodocs'],
18
+ parameters: {
19
+ docs: {
20
+ description: {
21
+ component:
22
+ '命令面板 — 全局搜索 / 命令执行(Linear / Raycast / Vercel 风格)。基于 cmdk,提供键盘导航、模糊搜索、分组、空态、快捷键提示;模态版用 CommandDialog 监听 ⌘K 全局触发。OpenTrek tokens 适配,无 mock。',
23
+ },
24
+ },
25
+ },
26
+ };
27
+
28
+ export default meta;
29
+ type Story = StoryObj<typeof Command>;
30
+
31
+ export const Inline: Story = {
32
+ render: () => (
33
+ <Command className="w-80 rounded-lg border">
34
+ <CommandInput placeholder="输入命令或搜索..." />
35
+ <CommandList>
36
+ <CommandEmpty>无结果。</CommandEmpty>
37
+ <CommandGroup heading="建议">
38
+ <CommandItem>
39
+ <Calendar /> 日历
40
+ <CommandShortcut>⌘K</CommandShortcut>
41
+ </CommandItem>
42
+ <CommandItem>
43
+ <Mail /> 邮件
44
+ <CommandShortcut>⌘E</CommandShortcut>
45
+ </CommandItem>
46
+ </CommandGroup>
47
+ <CommandSeparator />
48
+ <CommandGroup heading="设置">
49
+ <CommandItem>
50
+ <User /> 个人资料
51
+ </CommandItem>
52
+ <CommandItem>
53
+ <Settings /> 偏好设置
54
+ </CommandItem>
55
+ </CommandGroup>
56
+ </CommandList>
57
+ </Command>
58
+ ),
59
+ };