@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,93 @@
1
+ import * as React from 'react';
2
+ import type { Meta, StoryObj } from '@storybook/react';
3
+ import { AutoComplete } from './auto-complete';
4
+
5
+ const meta: Meta<typeof AutoComplete> = {
6
+ title: '表单与输入 · Form/AutoComplete',
7
+ component: AutoComplete,
8
+ tags: ['autodocs'],
9
+ parameters: {
10
+ docs: {
11
+ description: {
12
+ component:
13
+ '输入即建议 — 键入触发候选下拉,可选可改。最终 value 是自由文本,不强制为选项之一(与 Combobox 必选互补)。配 onSearch 实现异步建议。等价 antd `AutoComplete`。视觉走 OpenTrek tokens,所有样式来自 `@teamix-evo/design`,无 mock。',
14
+ },
15
+ },
16
+ },
17
+ };
18
+
19
+ export default meta;
20
+ type Story = StoryObj<typeof AutoComplete>;
21
+
22
+ const frameworks = [
23
+ { value: 'React' },
24
+ { value: 'Vue' },
25
+ { value: 'Svelte' },
26
+ { value: 'Angular' },
27
+ { value: 'Solid' },
28
+ ];
29
+
30
+ export const Playground: Story = {
31
+ parameters: { controls: { disable: true } },
32
+ render: () => (
33
+ <AutoComplete
34
+ options={frameworks}
35
+ placeholder="输入框架名..."
36
+ className="w-64"
37
+ />
38
+ ),
39
+ };
40
+
41
+ export const OpenOnFocus: Story = {
42
+ parameters: { controls: { disable: true } },
43
+ render: () => (
44
+ <AutoComplete
45
+ openOnFocus
46
+ options={['react', 'vue', 'svelte', 'angular'].map((v) => ({ value: v }))}
47
+ placeholder="聚焦即显示历史..."
48
+ className="w-64"
49
+ />
50
+ ),
51
+ };
52
+
53
+ export const EmailSuggest: Story = {
54
+ parameters: { controls: { disable: true } },
55
+ render: () => {
56
+ const [v, setV] = React.useState('');
57
+ const domains = ['@gmail.com', '@outlook.com', '@qq.com', '@163.com'];
58
+ const localPart = v.includes('@') ? v.slice(0, v.indexOf('@')) : v;
59
+ const opts = !localPart
60
+ ? []
61
+ : domains.map((d) => ({ value: localPart + d }));
62
+ return (
63
+ <AutoComplete
64
+ value={v}
65
+ onChange={setV}
66
+ options={opts}
67
+ placeholder="输入邮箱前缀..."
68
+ className="w-72"
69
+ />
70
+ );
71
+ },
72
+ };
73
+
74
+ export const AsyncSearch: Story = {
75
+ parameters: { controls: { disable: true } },
76
+ render: () => {
77
+ const all = ['北京', '上海', '深圳', '广州', '杭州', '成都', '武汉'];
78
+ const [opts, setOpts] = React.useState(all.map((v) => ({ value: v })));
79
+ return (
80
+ <AutoComplete
81
+ options={opts}
82
+ onSearch={(q) => {
83
+ window.setTimeout(() => {
84
+ setOpts(all.filter((v) => v.includes(q)).map((v) => ({ value: v })));
85
+ }, 80);
86
+ }}
87
+ placeholder="搜索城市(模拟异步)..."
88
+ emptyText="没有匹配的城市"
89
+ className="w-64"
90
+ />
91
+ );
92
+ },
93
+ };
@@ -0,0 +1,205 @@
1
+ import * as React from 'react';
2
+
3
+ import { cn } from '@/utils/cn';
4
+ import { Input } from '@/components/input/input';
5
+
6
+ export interface AutoCompleteOption {
7
+ /** 真实写入到输入框的 value(也作为建议列表 key)。 */
8
+ value: string;
9
+ /** 建议下拉显示文本(不传则用 value)。 */
10
+ label?: React.ReactNode;
11
+ /** 禁用此项(不可被选中)。 */
12
+ disabled?: boolean;
13
+ }
14
+
15
+ export interface AutoCompleteProps
16
+ extends Omit<
17
+ React.InputHTMLAttributes<HTMLInputElement>,
18
+ 'onChange' | 'value' | 'defaultValue' | 'onSelect' | 'size'
19
+ > {
20
+ /** 候选项(antd `options` 并集) — 通常由 onSearch 回调刷新。 */
21
+ options: AutoCompleteOption[];
22
+ /** 受控值。 */
23
+ value?: string;
24
+ /** uncontrolled 初值。 */
25
+ defaultValue?: string;
26
+ /**
27
+ * 值变化回调 — 用户键入 / 选中候选时触发。
28
+ */
29
+ onChange?: (value: string) => void;
30
+ /**
31
+ * 用户输入时回调,用于刷新 `options`(异步建议)。
32
+ */
33
+ onSearch?: (query: string) => void;
34
+ /**
35
+ * 选中候选时回调。
36
+ */
37
+ onSelect?: (value: string, option: AutoCompleteOption) => void;
38
+ /**
39
+ * 是否在输入框聚焦但 query 为空时显示完整 `options`(antd `defaultActiveFirstOption` 行为的近似)。
40
+ * @default false
41
+ */
42
+ openOnFocus?: boolean;
43
+ /**
44
+ * 无匹配时的提示文本。
45
+ * @default "无匹配项"
46
+ */
47
+ emptyText?: string;
48
+ /**
49
+ * 容器尺寸 — 透传 Input.size。
50
+ * @default "default"
51
+ */
52
+ size?: 'sm' | 'default' | 'lg';
53
+ }
54
+
55
+ /**
56
+ * 输入即建议 — antd 独有补足。**等价 antd `AutoComplete`**。
57
+ * 与 `Combobox` 区别:Combobox 是"必选下拉",输入框只接受选项中的值;
58
+ * AutoComplete 是"自由输入 + 选项建议",最终 value 可以是任意字符串。
59
+ */
60
+ const AutoComplete = React.forwardRef<HTMLInputElement, AutoCompleteProps>(
61
+ (
62
+ {
63
+ options,
64
+ value,
65
+ defaultValue,
66
+ onChange,
67
+ onSearch,
68
+ onSelect,
69
+ openOnFocus = false,
70
+ emptyText = '无匹配项',
71
+ size = 'default',
72
+ className,
73
+ onFocus,
74
+ onBlur,
75
+ onKeyDown,
76
+ placeholder,
77
+ disabled,
78
+ ...props
79
+ },
80
+ ref,
81
+ ) => {
82
+ const isControlled = value !== undefined;
83
+ const [internal, setInternal] = React.useState<string>(defaultValue ?? '');
84
+ const current = isControlled ? value! : internal;
85
+
86
+ const [open, setOpen] = React.useState(false);
87
+ const [activeIdx, setActiveIdx] = React.useState(0);
88
+
89
+ const filtered = React.useMemo(() => {
90
+ const q = current.toLowerCase();
91
+ return q === ''
92
+ ? options
93
+ : options.filter((o) => o.value.toLowerCase().includes(q));
94
+ }, [current, options]);
95
+
96
+ React.useEffect(() => {
97
+ setActiveIdx(0);
98
+ }, [current]);
99
+
100
+ const update = (next: string) => {
101
+ if (!isControlled) setInternal(next);
102
+ onChange?.(next);
103
+ };
104
+
105
+ const select = (opt: AutoCompleteOption) => {
106
+ if (opt.disabled) return;
107
+ update(opt.value);
108
+ onSelect?.(opt.value, opt);
109
+ setOpen(false);
110
+ };
111
+
112
+ const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
113
+ if (open && filtered.length > 0) {
114
+ if (e.key === 'ArrowDown') {
115
+ e.preventDefault();
116
+ setActiveIdx((i) => (i + 1) % filtered.length);
117
+ return;
118
+ }
119
+ if (e.key === 'ArrowUp') {
120
+ e.preventDefault();
121
+ setActiveIdx((i) => (i - 1 + filtered.length) % filtered.length);
122
+ return;
123
+ }
124
+ if (e.key === 'Enter') {
125
+ const opt = filtered[activeIdx];
126
+ if (opt && !opt.disabled) {
127
+ e.preventDefault();
128
+ select(opt);
129
+ return;
130
+ }
131
+ }
132
+ if (e.key === 'Escape') {
133
+ e.preventDefault();
134
+ setOpen(false);
135
+ return;
136
+ }
137
+ }
138
+ onKeyDown?.(e);
139
+ };
140
+
141
+ const shouldShow = open && (filtered.length > 0 || current !== '');
142
+
143
+ return (
144
+ <div className={cn('relative', className)}>
145
+ <Input
146
+ ref={ref}
147
+ value={current}
148
+ placeholder={placeholder}
149
+ disabled={disabled}
150
+ size={size}
151
+ onChange={(e) => {
152
+ update(e.target.value);
153
+ onSearch?.(e.target.value);
154
+ setOpen(true);
155
+ }}
156
+ onFocus={(e) => {
157
+ if (openOnFocus) setOpen(true);
158
+ onFocus?.(e);
159
+ }}
160
+ onBlur={(e) => {
161
+ window.setTimeout(() => setOpen(false), 120);
162
+ onBlur?.(e);
163
+ }}
164
+ onKeyDown={handleKeyDown}
165
+ {...props}
166
+ />
167
+ {shouldShow ? (
168
+ <ul
169
+ role="listbox"
170
+ className="absolute left-0 right-0 top-full z-50 mt-1 max-h-60 overflow-auto rounded-md border bg-popover p-1 text-sm text-popover-foreground shadow-md"
171
+ >
172
+ {filtered.length === 0 ? (
173
+ <li className="px-2 py-1.5 text-center text-xs text-muted-foreground">
174
+ {emptyText}
175
+ </li>
176
+ ) : (
177
+ filtered.map((opt, i) => (
178
+ <li
179
+ key={opt.value}
180
+ role="option"
181
+ aria-selected={i === activeIdx}
182
+ onMouseDown={(e) => {
183
+ e.preventDefault();
184
+ select(opt);
185
+ }}
186
+ onMouseEnter={() => setActiveIdx(i)}
187
+ className={cn(
188
+ 'cursor-pointer rounded-sm px-2 py-1.5',
189
+ i === activeIdx && !opt.disabled && 'bg-accent text-accent-foreground',
190
+ opt.disabled && 'cursor-not-allowed opacity-50',
191
+ )}
192
+ >
193
+ {opt.label ?? opt.value}
194
+ </li>
195
+ ))
196
+ )}
197
+ </ul>
198
+ ) : null}
199
+ </div>
200
+ );
201
+ },
202
+ );
203
+ AutoComplete.displayName = 'AutoComplete';
204
+
205
+ export { AutoComplete };
@@ -0,0 +1,94 @@
1
+ ---
2
+ id: avatar
3
+ name: Avatar
4
+ type: component
5
+ category: foundation
6
+ since: 0.1.0
7
+ package: "@teamix-evo/ui"
8
+ ---
9
+
10
+ # Avatar
11
+
12
+ 头像 — Radix Avatar(图片加载失败自动回退到 Fallback)+ antd Avatar 的 `size / shape / Avatar.Group` 并集。
13
+
14
+ ## When to use
15
+
16
+ - 用户身份标识(列表 / 评论 / 顶部导航)
17
+ - 多人协作场景的成员展示(`AvatarGroup max`)
18
+ - 资源所有者标记
19
+
20
+ ## When NOT to use
21
+
22
+ - 装饰性图标 → 用 `lucide-react` 图标
23
+ - 大尺寸内容头图 → 用 `AspectRatio` + `<img>` 自由布局
24
+
25
+ ## Props
26
+
27
+ > 以下表格由 `pnpm --filter @teamix-evo/ui gen:meta` 自动生成。
28
+
29
+ <!-- auto:props:begin -->
30
+ | 名称 | 类型 | 默认值 | 必填 | 说明 |
31
+ | --- | --- | --- | --- | --- |
32
+ | `size` | `'sm' \| 'default' \| 'lg' \| 'xl'` | `"default"` | – | 尺寸(antd 并集:sm 32 / default 40 / lg 48 / xl 64)。 |
33
+ | `shape` | `'circle' \| 'square'` | `"circle"` | – | 形状(antd 并集)。 |
34
+ <!-- auto:props:end -->
35
+
36
+ ## 依赖
37
+
38
+ > 以下表格由 `pnpm --filter @teamix-evo/ui gen:meta` 自动生成,数据源是 [`manifest.json`](../../../manifest.json)。**手工编辑 marker 之间的内容会在下次生成时被覆盖**。
39
+
40
+ <!-- auto:deps:begin -->
41
+ ### 同库依赖
42
+
43
+ > `teamix-evo ui add avatar` 时,以下 entry 会被自动连带安装(无需手动 add)。
44
+
45
+ | Entry | 类型 | 描述 |
46
+ | --- | --- | --- |
47
+ | `cn` | util | Tailwind className 合并工具(clsx + tailwind-merge) |
48
+
49
+ ### npm 依赖
50
+
51
+ > 业务侧需要先 `pnpm add` / `npm install` 这些包。CLI 在 `ui add` 完成后会列出此提示。
52
+
53
+ ```bash
54
+ pnpm add @radix-ui/react-avatar@^1.1.0 class-variance-authority@^0.7.0
55
+ ```
56
+ <!-- auto:deps:end -->
57
+
58
+ > 子组件:`AvatarImage`(图片源)/ `AvatarFallback`(加载失败 / 无图时的占位)/ `AvatarGroup`(多头像折叠组,带 `max` 与 `size`)。
59
+
60
+ ## AI 生成纪律
61
+
62
+ - **必须配 Fallback**:`AvatarImage` 加载失败时无 Fallback 会渲染空白;**永远**写 `<AvatarImage /> + <AvatarFallback>{initials}</AvatarFallback>` 的对子
63
+ - **Fallback 内容用首字母**:取 name 首字母 1~2 个,**不要**塞图标(图标用 `<img>` 直接渲染更准确)
64
+ - **Group 内尺寸统一**:`AvatarGroup` 的 `size` 会覆盖子项 — 不要在 Group 内混用不同尺寸的 Avatar
65
+ - **shape="square" 用于品牌 / 产品 Logo**:用户头像保持 `circle` 默认
66
+
67
+ ## Examples
68
+
69
+ ```tsx
70
+ import {
71
+ Avatar, AvatarImage, AvatarFallback, AvatarGroup,
72
+ } from '@/components/ui/avatar';
73
+
74
+ // 基础
75
+ <Avatar>
76
+ <AvatarImage src="/u/lyca.jpg" alt="lyca" />
77
+ <AvatarFallback>LY</AvatarFallback>
78
+ </Avatar>
79
+
80
+ // 尺寸 + 形状
81
+ <Avatar size="lg" shape="square">
82
+ <AvatarImage src="/p/teamix.svg" alt="Teamix" />
83
+ <AvatarFallback>TE</AvatarFallback>
84
+ </Avatar>
85
+
86
+ // 协作组(超出 3 收起)
87
+ <AvatarGroup max={3} size="sm">
88
+ <Avatar><AvatarImage src="/u/a.jpg" /><AvatarFallback>A</AvatarFallback></Avatar>
89
+ <Avatar><AvatarImage src="/u/b.jpg" /><AvatarFallback>B</AvatarFallback></Avatar>
90
+ <Avatar><AvatarImage src="/u/c.jpg" /><AvatarFallback>C</AvatarFallback></Avatar>
91
+ <Avatar><AvatarImage src="/u/d.jpg" /><AvatarFallback>D</AvatarFallback></Avatar>
92
+ <Avatar><AvatarImage src="/u/e.jpg" /><AvatarFallback>E</AvatarFallback></Avatar>
93
+ </AvatarGroup>
94
+ ```
@@ -0,0 +1,80 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { Avatar, AvatarImage, AvatarFallback, AvatarGroup } from './avatar';
3
+
4
+ const meta: Meta<typeof Avatar> = {
5
+ title: '基础原语 · Foundation/Avatar',
6
+ component: Avatar,
7
+ tags: ['autodocs'],
8
+ parameters: {
9
+ docs: {
10
+ description: {
11
+ component:
12
+ '头像 — 表示用户或对象,支持图片 / 文字 / 图标三种回退源。基于 Radix Avatar(图片加载失败自动回退到 fallback) + antd 增强:尺寸 sm/default/lg、形状 circle/square、`AvatarGroup` 堆叠。Image / Fallback 通过命名子组件组合。视觉走 OpenTrek semantic tokens,所有样式来自 `@teamix-evo/design`,无 mock。',
13
+ },
14
+ },
15
+ },
16
+ argTypes: {
17
+ size: { control: 'select', options: ['sm', 'default', 'lg', 'xl'] },
18
+ shape: { control: 'inline-radio', options: ['circle', 'square'] },
19
+ },
20
+ args: { size: 'default', shape: 'circle' },
21
+ };
22
+
23
+ export default meta;
24
+ type Story = StoryObj<typeof Avatar>;
25
+
26
+ export const Playground: Story = {
27
+ render: (args) => (
28
+ <Avatar {...args}>
29
+ <AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" />
30
+ <AvatarFallback>SH</AvatarFallback>
31
+ </Avatar>
32
+ ),
33
+ };
34
+
35
+ export const SizesAndShapes: Story = {
36
+ parameters: { controls: { disable: true } },
37
+ render: () => (
38
+ <div className="flex items-end gap-3">
39
+ {(['sm', 'default', 'lg', 'xl'] as const).map((s) => (
40
+ <Avatar key={s} size={s}>
41
+ <AvatarImage src="https://github.com/shadcn.png" />
42
+ <AvatarFallback>SH</AvatarFallback>
43
+ </Avatar>
44
+ ))}
45
+ <Avatar shape="square">
46
+ <AvatarFallback>TE</AvatarFallback>
47
+ </Avatar>
48
+ </div>
49
+ ),
50
+ };
51
+
52
+ export const FallbackOnly: Story = {
53
+ parameters: { controls: { disable: true } },
54
+ render: () => (
55
+ <div className="flex items-center gap-3">
56
+ <Avatar>
57
+ <AvatarFallback>JD</AvatarFallback>
58
+ </Avatar>
59
+ <Avatar>
60
+ <AvatarFallback>李</AvatarFallback>
61
+ </Avatar>
62
+ <Avatar>
63
+ <AvatarFallback>?</AvatarFallback>
64
+ </Avatar>
65
+ </div>
66
+ ),
67
+ };
68
+
69
+ export const Group: Story = {
70
+ parameters: { controls: { disable: true } },
71
+ render: () => (
72
+ <AvatarGroup max={3}>
73
+ {['A', 'B', 'C', 'D', 'E'].map((c) => (
74
+ <Avatar key={c}>
75
+ <AvatarFallback>{c}</AvatarFallback>
76
+ </Avatar>
77
+ ))}
78
+ </AvatarGroup>
79
+ ),
80
+ };
@@ -0,0 +1,126 @@
1
+ import * as React from 'react';
2
+ import * as AvatarPrimitive from '@radix-ui/react-avatar';
3
+ import { cva, type VariantProps } from 'class-variance-authority';
4
+
5
+ import { cn } from '@/utils/cn';
6
+
7
+ const avatarVariants = cva(
8
+ 'relative flex shrink-0 overflow-hidden bg-muted',
9
+ {
10
+ variants: {
11
+ size: {
12
+ sm: 'size-8 text-xs',
13
+ default: 'size-10 text-sm',
14
+ lg: 'size-12 text-base',
15
+ xl: 'size-16 text-lg',
16
+ },
17
+ shape: {
18
+ circle: 'rounded-full',
19
+ square: 'rounded-md',
20
+ },
21
+ },
22
+ defaultVariants: { size: 'default', shape: 'circle' },
23
+ },
24
+ );
25
+
26
+ export interface AvatarProps
27
+ extends React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>,
28
+ VariantProps<typeof avatarVariants> {
29
+ /**
30
+ * 尺寸(antd 并集:sm 32 / default 40 / lg 48 / xl 64)。
31
+ * @default "default"
32
+ */
33
+ size?: 'sm' | 'default' | 'lg' | 'xl';
34
+ /**
35
+ * 形状(antd 并集)。
36
+ * @default "circle"
37
+ */
38
+ shape?: 'circle' | 'square';
39
+ }
40
+
41
+ const Avatar = React.forwardRef<
42
+ React.ElementRef<typeof AvatarPrimitive.Root>,
43
+ AvatarProps
44
+ >(({ className, size, shape, ...props }, ref) => (
45
+ <AvatarPrimitive.Root
46
+ ref={ref}
47
+ className={cn(avatarVariants({ size, shape }), className)}
48
+ {...props}
49
+ />
50
+ ));
51
+ Avatar.displayName = AvatarPrimitive.Root.displayName;
52
+
53
+ const AvatarImage = React.forwardRef<
54
+ React.ElementRef<typeof AvatarPrimitive.Image>,
55
+ React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
56
+ >(({ className, ...props }, ref) => (
57
+ <AvatarPrimitive.Image
58
+ ref={ref}
59
+ className={cn('aspect-square size-full object-cover', className)}
60
+ {...props}
61
+ />
62
+ ));
63
+ AvatarImage.displayName = AvatarPrimitive.Image.displayName;
64
+
65
+ const AvatarFallback = React.forwardRef<
66
+ React.ElementRef<typeof AvatarPrimitive.Fallback>,
67
+ React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
68
+ >(({ className, ...props }, ref) => (
69
+ <AvatarPrimitive.Fallback
70
+ ref={ref}
71
+ className={cn(
72
+ 'flex size-full items-center justify-center font-medium text-muted-foreground',
73
+ className,
74
+ )}
75
+ {...props}
76
+ />
77
+ ));
78
+ AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;
79
+
80
+ // ─── AvatarGroup(antd 并集)────────────────────────────────────────────────
81
+
82
+ export interface AvatarGroupProps extends React.HTMLAttributes<HTMLDivElement> {
83
+ /**
84
+ * 显示前 N 个 Avatar,其余收为 `+N` 折叠气泡。
85
+ * @default 3
86
+ */
87
+ max?: number;
88
+ /** Group 内所有 Avatar 共享的尺寸,会覆盖单个 Avatar 的 size。 */
89
+ size?: AvatarProps['size'];
90
+ }
91
+
92
+ const AvatarGroup = React.forwardRef<HTMLDivElement, AvatarGroupProps>(
93
+ ({ className, max = 3, size, children, ...props }, ref) => {
94
+ const items = React.Children.toArray(children);
95
+ const visible = items.slice(0, max);
96
+ const overflow = items.length - max;
97
+
98
+ return (
99
+ <div
100
+ ref={ref}
101
+ className={cn(
102
+ 'flex items-center -space-x-2 [&>*]:ring-2 [&>*]:ring-background',
103
+ className,
104
+ )}
105
+ {...props}
106
+ >
107
+ {visible.map((child, i) =>
108
+ React.isValidElement(child) && size
109
+ ? React.cloneElement(
110
+ child as React.ReactElement<AvatarProps>,
111
+ { size, key: i },
112
+ )
113
+ : child,
114
+ )}
115
+ {overflow > 0 ? (
116
+ <Avatar size={size}>
117
+ <AvatarFallback>+{overflow}</AvatarFallback>
118
+ </Avatar>
119
+ ) : null}
120
+ </div>
121
+ );
122
+ },
123
+ );
124
+ AvatarGroup.displayName = 'AvatarGroup';
125
+
126
+ export { Avatar, AvatarImage, AvatarFallback, AvatarGroup, avatarVariants };