@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,117 @@
1
+ import * as React from 'react';
2
+ import type { Meta, StoryObj } from '@storybook/react';
3
+ import { Mail, Search } from 'lucide-react';
4
+ import { Input } from './input';
5
+
6
+ const meta: Meta<typeof Input> = {
7
+ title: '表单与输入 · Form/Input',
8
+ component: Input,
9
+ tags: ['autodocs'],
10
+ parameters: {
11
+ docs: {
12
+ description: {
13
+ component:
14
+ '输入框 — 文本 / 邮箱 / 密码 / 搜索等单行输入。shadcn 原生 `<input>` 风格 + antd 扩展:`size` 三档、`prefix` / `suffix` 前后缀、`addonBefore` / `addonAfter` 外联组、`clearable` 一键清空、`showCount` + `maxLength` 字数统计。视觉走 OpenTrek semantic tokens,所有样式来自 `@teamix-evo/design`,无 mock。',
15
+ },
16
+ },
17
+ },
18
+ argTypes: {
19
+ size: { control: 'inline-radio', options: ['sm', 'default', 'lg'] },
20
+ type: {
21
+ control: 'select',
22
+ options: ['text', 'email', 'password', 'search', 'tel', 'url'],
23
+ },
24
+ clearable: { control: 'boolean' },
25
+ showCount: { control: 'boolean' },
26
+ disabled: { control: 'boolean' },
27
+ placeholder: { control: 'text' },
28
+ maxLength: { control: 'number' },
29
+ },
30
+ args: {
31
+ size: 'default',
32
+ type: 'text',
33
+ clearable: false,
34
+ showCount: false,
35
+ disabled: false,
36
+ placeholder: '请输入...',
37
+ },
38
+ decorators: [
39
+ (Story) => (
40
+ <div className="w-72">
41
+ <Story />
42
+ </div>
43
+ ),
44
+ ],
45
+ };
46
+
47
+ export default meta;
48
+ type Story = StoryObj<typeof Input>;
49
+
50
+ export const Playground: Story = {};
51
+
52
+ export const Sizes: Story = {
53
+ parameters: { controls: { disable: true } },
54
+ render: () => (
55
+ <div className="flex flex-col gap-3">
56
+ <Input size="sm" placeholder="Small" />
57
+ <Input size="default" placeholder="Default" />
58
+ <Input size="lg" placeholder="Large" />
59
+ </div>
60
+ ),
61
+ };
62
+
63
+ export const PrefixSuffix: Story = {
64
+ parameters: { controls: { disable: true } },
65
+ render: () => (
66
+ <div className="flex flex-col gap-3">
67
+ <Input prefix={<Search />} placeholder="搜索..." />
68
+ <Input type="email" suffix={<Mail />} placeholder="you@example.com" />
69
+ <Input prefix="¥" suffix="USD" placeholder="0.00" />
70
+ </div>
71
+ ),
72
+ };
73
+
74
+ export const Addon: Story = {
75
+ parameters: { controls: { disable: true } },
76
+ render: () => (
77
+ <div className="flex flex-col gap-3">
78
+ <Input
79
+ addonBefore="https://"
80
+ defaultValue="teamix-evo"
81
+ addonAfter=".com"
82
+ />
83
+ <Input addonBefore="@" placeholder="username" />
84
+ <Input addonAfter="kg" defaultValue="42" />
85
+ </div>
86
+ ),
87
+ };
88
+
89
+ export const Clearable: Story = {
90
+ parameters: { controls: { disable: true } },
91
+ render: () => {
92
+ const [v, setV] = React.useState('Hello');
93
+ return (
94
+ <Input
95
+ clearable
96
+ value={v}
97
+ onChange={(e) => setV(e.target.value)}
98
+ placeholder="可清除..."
99
+ />
100
+ );
101
+ },
102
+ };
103
+
104
+ export const ShowCount: Story = {
105
+ parameters: { controls: { disable: true } },
106
+ render: () => <Input showCount maxLength={140} placeholder="发布动态" />,
107
+ };
108
+
109
+ export const Disabled: Story = {
110
+ parameters: { controls: { disable: true } },
111
+ render: () => (
112
+ <div className="flex flex-col gap-3">
113
+ <Input disabled placeholder="禁用态" />
114
+ <Input disabled defaultValue="只读内容" />
115
+ </div>
116
+ ),
117
+ };
@@ -0,0 +1,213 @@
1
+ import * as React from 'react';
2
+ import { X } from 'lucide-react';
3
+
4
+ import { cn } from '@/utils/cn';
5
+
6
+ export interface InputProps
7
+ extends Omit<
8
+ React.InputHTMLAttributes<HTMLInputElement>,
9
+ 'size' | 'prefix' | 'suffix'
10
+ > {
11
+ /** 输入框前置图标 / 文本(antd `prefix` 并集)。 */
12
+ prefix?: React.ReactNode;
13
+ /** 输入框后置图标 / 文本(antd `suffix` 并集)。 */
14
+ suffix?: React.ReactNode;
15
+ /**
16
+ * 显示清除按钮,有内容时点击清空(antd `allowClear` 并集)。
17
+ * @default false
18
+ */
19
+ clearable?: boolean;
20
+ /**
21
+ * 显示字符计数(antd `showCount` 并集);需配合 `maxLength` 一起使用。
22
+ * @default false
23
+ */
24
+ showCount?: boolean;
25
+ /** 输入框前置标签(antd `addonBefore` 并集),与外层组合形成 input-group。 */
26
+ addonBefore?: React.ReactNode;
27
+ /** 输入框后置标签(antd `addonAfter` 并集)。 */
28
+ addonAfter?: React.ReactNode;
29
+ /**
30
+ * 尺寸。
31
+ * @default "default"
32
+ */
33
+ size?: 'sm' | 'default' | 'lg';
34
+ /** 受控值,用于 clearable / showCount 的逻辑;未传时回退到 uncontrolled 行为。 */
35
+ value?: string;
36
+ /** uncontrolled 初始值。 */
37
+ defaultValue?: string;
38
+ }
39
+
40
+ const Input = React.forwardRef<HTMLInputElement, InputProps>(
41
+ (
42
+ {
43
+ className,
44
+ type = 'text',
45
+ prefix,
46
+ suffix,
47
+ clearable = false,
48
+ showCount = false,
49
+ addonBefore,
50
+ addonAfter,
51
+ size = 'default',
52
+ value,
53
+ defaultValue,
54
+ onChange,
55
+ maxLength,
56
+ disabled,
57
+ ...props
58
+ },
59
+ ref,
60
+ ) => {
61
+ // Track the current value for clearable / showCount; mirror uncontrolled
62
+ // input via internal state when `value` prop isn't provided.
63
+ const isControlled = value !== undefined;
64
+ const [internal, setInternal] = React.useState<string>(
65
+ defaultValue ?? '',
66
+ );
67
+ const current = isControlled ? value : internal;
68
+
69
+ const innerRef = React.useRef<HTMLInputElement | null>(null);
70
+ React.useImperativeHandle(
71
+ ref,
72
+ () => innerRef.current as HTMLInputElement,
73
+ );
74
+
75
+ const h =
76
+ size === 'sm'
77
+ ? 'h-8 text-xs'
78
+ : size === 'lg'
79
+ ? 'h-10 text-base'
80
+ : 'h-9 text-sm';
81
+
82
+ const hasAffix = prefix != null || suffix != null || clearable;
83
+
84
+ const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
85
+ if (!isControlled) setInternal(e.target.value);
86
+ onChange?.(e);
87
+ };
88
+
89
+ const handleClear = () => {
90
+ if (!isControlled) setInternal('');
91
+ // Synthetic event for callers wiring onChange to a setter
92
+ const target = innerRef.current;
93
+ if (target) {
94
+ const proto = Object.getPrototypeOf(target);
95
+ const desc = Object.getOwnPropertyDescriptor(proto, 'value');
96
+ desc?.set?.call(target, '');
97
+ target.dispatchEvent(new Event('input', { bubbles: true }));
98
+ }
99
+ };
100
+
101
+ // Inner input + (optional) prefix/suffix wrapper
102
+ const innerInput = hasAffix ? (
103
+ <div
104
+ className={cn(
105
+ 'flex items-center gap-2 rounded-md border border-input bg-background px-3 shadow-sm transition-colors focus-within:ring-1 focus-within:ring-ring',
106
+ h,
107
+ disabled && 'cursor-not-allowed opacity-50',
108
+ (addonBefore || addonAfter) && 'rounded-none border-x-0',
109
+ addonBefore && !addonAfter && 'rounded-l-none border-l-0',
110
+ addonAfter && !addonBefore && 'rounded-r-none border-r-0',
111
+ !addonBefore && !addonAfter && '',
112
+ className,
113
+ )}
114
+ >
115
+ {prefix ? (
116
+ <span className="flex shrink-0 items-center text-muted-foreground [&_svg]:size-4">
117
+ {prefix}
118
+ </span>
119
+ ) : null}
120
+ <input
121
+ ref={innerRef}
122
+ type={type}
123
+ value={isControlled ? value : undefined}
124
+ defaultValue={!isControlled ? defaultValue : undefined}
125
+ maxLength={maxLength}
126
+ disabled={disabled}
127
+ onChange={handleChange}
128
+ className="grow bg-transparent outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed"
129
+ {...props}
130
+ />
131
+ {clearable && current && current.length > 0 && !disabled ? (
132
+ <button
133
+ type="button"
134
+ onClick={handleClear}
135
+ aria-label="Clear"
136
+ className="shrink-0 rounded-sm text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
137
+ >
138
+ <X className="size-4" />
139
+ </button>
140
+ ) : null}
141
+ {suffix ? (
142
+ <span className="flex shrink-0 items-center text-muted-foreground [&_svg]:size-4">
143
+ {suffix}
144
+ </span>
145
+ ) : null}
146
+ </div>
147
+ ) : (
148
+ <input
149
+ ref={innerRef}
150
+ type={type}
151
+ value={isControlled ? value : undefined}
152
+ defaultValue={!isControlled ? defaultValue : undefined}
153
+ maxLength={maxLength}
154
+ disabled={disabled}
155
+ onChange={handleChange}
156
+ className={cn(
157
+ 'flex w-full rounded-md border border-input bg-background px-3 py-1 shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50',
158
+ h,
159
+ (addonBefore || addonAfter) && 'rounded-none',
160
+ addonBefore && !addonAfter && 'rounded-l-none',
161
+ addonAfter && !addonBefore && 'rounded-r-none',
162
+ className,
163
+ )}
164
+ {...props}
165
+ />
166
+ );
167
+
168
+ // Wrap with addons if any
169
+ const addonWrapper =
170
+ addonBefore || addonAfter ? (
171
+ <div className="flex">
172
+ {addonBefore ? (
173
+ <span
174
+ className={cn(
175
+ 'inline-flex shrink-0 items-center rounded-l-md border border-r-0 border-input bg-muted px-3 text-muted-foreground',
176
+ h,
177
+ )}
178
+ >
179
+ {addonBefore}
180
+ </span>
181
+ ) : null}
182
+ {innerInput}
183
+ {addonAfter ? (
184
+ <span
185
+ className={cn(
186
+ 'inline-flex shrink-0 items-center rounded-r-md border border-l-0 border-input bg-muted px-3 text-muted-foreground',
187
+ h,
188
+ )}
189
+ >
190
+ {addonAfter}
191
+ </span>
192
+ ) : null}
193
+ </div>
194
+ ) : (
195
+ innerInput
196
+ );
197
+
198
+ if (showCount && maxLength) {
199
+ return (
200
+ <div className="space-y-1">
201
+ {addonWrapper}
202
+ <div className="text-right text-xs text-muted-foreground tabular-nums">
203
+ {current?.length ?? 0} / {maxLength}
204
+ </div>
205
+ </div>
206
+ );
207
+ }
208
+ return addonWrapper;
209
+ },
210
+ );
211
+ Input.displayName = 'Input';
212
+
213
+ export { Input };
@@ -0,0 +1,92 @@
1
+ ---
2
+ id: input-group
3
+ name: InputGroup
4
+ type: component
5
+ category: form
6
+ since: 0.1.0
7
+ package: "@teamix-evo/ui"
8
+ ---
9
+
10
+ # InputGroup
11
+
12
+ 输入框 + addon 统一容器 — shadcn 2025-10 新增。**等价 antd `Input` 的 `prefix` / `suffix` / `addonBefore` / `addonAfter` 集合**,但拆为更通用的复合插槽:`InputGroupAddon`(图标 / 文本 / 按钮)+ `InputGroupInput` / `InputGroupTextarea`(主体)。
13
+
14
+ ## When to use
15
+
16
+ - 输入框需要前 / 后缀(`https://` `.com`、`$`、`/月`)
17
+ - 输入框 + 操作按钮(搜索、清空、复制)
18
+ - 输入框 + 图标(搜索图标、loading spinner)
19
+ - Textarea + 上传 / 录音类辅助按钮
20
+
21
+ ## When NOT to use
22
+
23
+ - 仅需输入框 → `Input`
24
+ - 仅需 textarea → `Textarea`
25
+ - 多个按钮拼接 → `ButtonGroup`
26
+
27
+ <!-- auto:props:begin -->
28
+ | 名称 | 类型 | 默认值 | 必填 | 说明 |
29
+ | --- | --- | --- | --- | --- |
30
+ | `disabled` | `boolean` | – | – | 整组禁用样式(子项请单独传 `disabled`,这里仅影响视觉上的 opacity)。 |
31
+ <!-- auto:props:end -->
32
+
33
+ <!-- auto:deps:begin -->
34
+ ### 同库依赖
35
+
36
+ > `teamix-evo ui add input-group` 时,以下 entry 会被自动连带安装(无需手动 add)。
37
+
38
+ | Entry | 类型 | 描述 |
39
+ | --- | --- | --- |
40
+ | `cn` | util | Tailwind className 合并工具(clsx + tailwind-merge) |
41
+
42
+ ### npm 依赖
43
+
44
+ _无 — 本组件不依赖任何 npm 包。_
45
+ <!-- auto:deps:end -->
46
+
47
+ ## AI 生成纪律
48
+
49
+ - **顺序按 JSX 写**:`<InputGroup>` 内的子节点顺序就是渲染顺序 — `<Addon position="before" />` 必须在 `<Input />` 前面,反之亦然
50
+ - **不要在 InputGroupAddon 里再放 Input** — addon 是辅助槽,主输入必须用 `InputGroupInput` / `InputGroupTextarea`
51
+ - **`variant="icon"`** 给图标用(无背景、紧凑);**`variant="text"`** 给纯文本前后缀(muted 背景);**`variant="button"`** 给嵌入 Button(去内边距 + 去圆角,避免双重边线)
52
+ - **disabled**:整组传 `disabled` 仅影响外观;子项的真实 disabled 需要单独传(防止误整组禁用)
53
+ - **嵌入 `Button`**:用 `<InputGroupAddon variant="button">` 包,Button 自身 `variant="ghost"` 效果最自然
54
+
55
+ ## Examples
56
+
57
+ ```tsx
58
+ import {
59
+ InputGroup,
60
+ InputGroupAddon,
61
+ InputGroupInput,
62
+ InputGroupTextarea,
63
+ } from '@/components/ui/input-group';
64
+ import { Button } from '@/components/ui/button';
65
+ import { Search, Send } from 'lucide-react';
66
+
67
+ // 前缀图标 + 后缀按钮
68
+ <InputGroup>
69
+ <InputGroupAddon position="before" variant="icon">
70
+ <Search className="size-4" />
71
+ </InputGroupAddon>
72
+ <InputGroupInput placeholder="搜索文档..." />
73
+ <InputGroupAddon position="after" variant="button">
74
+ <Button variant="ghost" size="sm">搜索</Button>
75
+ </InputGroupAddon>
76
+ </InputGroup>
77
+
78
+ // 前后缀文本(URL 输入)
79
+ <InputGroup>
80
+ <InputGroupAddon position="before">https://</InputGroupAddon>
81
+ <InputGroupInput defaultValue="example" />
82
+ <InputGroupAddon position="after">.com</InputGroupAddon>
83
+ </InputGroup>
84
+
85
+ // Textarea + 发送按钮
86
+ <InputGroup>
87
+ <InputGroupTextarea rows={3} placeholder="发表评论..." />
88
+ <InputGroupAddon position="after" variant="button">
89
+ <Button size="icon" icon={<Send />} aria-label="发送" />
90
+ </InputGroupAddon>
91
+ </InputGroup>
92
+ ```
@@ -0,0 +1,88 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { Search, Send } from 'lucide-react';
3
+ import {
4
+ InputGroup,
5
+ InputGroupAddon,
6
+ InputGroupInput,
7
+ InputGroupTextarea,
8
+ } from './input-group';
9
+ import { Button } from '@/components/button/button';
10
+
11
+ const meta: Meta<typeof InputGroup> = {
12
+ title: '表单与输入 · Form/InputGroup',
13
+ component: InputGroup,
14
+ tags: ['autodocs'],
15
+ parameters: {
16
+ docs: {
17
+ description: {
18
+ component:
19
+ '输入框 + addon 统一容器 — 拆为通用复合插槽:InputGroupAddon(图标 / 文本 / 按钮)+ InputGroupInput / InputGroupTextarea(主体)。shadcn 2025-10 新增,等价 antd Input 的 prefix / suffix / addonBefore / addonAfter 集合。视觉走 OpenTrek tokens,所有样式来自 `@teamix-evo/design`,无 mock。',
20
+ },
21
+ },
22
+ },
23
+ };
24
+
25
+ export default meta;
26
+ type Story = StoryObj<typeof InputGroup>;
27
+
28
+ export const Playground: Story = {
29
+ parameters: { controls: { disable: true } },
30
+ render: () => (
31
+ <InputGroup className="w-80">
32
+ <InputGroupAddon position="before" variant="icon">
33
+ <Search className="size-4" />
34
+ </InputGroupAddon>
35
+ <InputGroupInput placeholder="搜索文档..." />
36
+ </InputGroup>
37
+ ),
38
+ };
39
+
40
+ export const Url: Story = {
41
+ parameters: { controls: { disable: true } },
42
+ render: () => (
43
+ <InputGroup className="w-80">
44
+ <InputGroupAddon position="before">https://</InputGroupAddon>
45
+ <InputGroupInput defaultValue="example" />
46
+ <InputGroupAddon position="after">.com</InputGroupAddon>
47
+ </InputGroup>
48
+ ),
49
+ };
50
+
51
+ export const SearchWithButton: Story = {
52
+ parameters: { controls: { disable: true } },
53
+ render: () => (
54
+ <InputGroup className="w-96">
55
+ <InputGroupAddon position="before" variant="icon">
56
+ <Search className="size-4" />
57
+ </InputGroupAddon>
58
+ <InputGroupInput placeholder="输入关键词..." />
59
+ <InputGroupAddon position="after" variant="button">
60
+ <Button variant="ghost" size="sm">
61
+ 搜索
62
+ </Button>
63
+ </InputGroupAddon>
64
+ </InputGroup>
65
+ ),
66
+ };
67
+
68
+ export const TextareaWithSend: Story = {
69
+ parameters: { controls: { disable: true } },
70
+ render: () => (
71
+ <InputGroup className="w-96 items-end">
72
+ <InputGroupTextarea rows={3} placeholder="发表评论..." />
73
+ <InputGroupAddon position="after" variant="button">
74
+ <Button size="icon" icon={<Send />} aria-label="发送" />
75
+ </InputGroupAddon>
76
+ </InputGroup>
77
+ ),
78
+ };
79
+
80
+ export const Disabled: Story = {
81
+ parameters: { controls: { disable: true } },
82
+ render: () => (
83
+ <InputGroup className="w-80" disabled>
84
+ <InputGroupAddon position="before">@</InputGroupAddon>
85
+ <InputGroupInput defaultValue="alice" disabled />
86
+ </InputGroup>
87
+ ),
88
+ };
@@ -0,0 +1,107 @@
1
+ import * as React from 'react';
2
+
3
+ import { cn } from '@/utils/cn';
4
+
5
+ export interface InputGroupProps extends React.HTMLAttributes<HTMLDivElement> {
6
+ /**
7
+ * 整组禁用样式(子项请单独传 `disabled`,这里仅影响视觉上的 opacity)。
8
+ */
9
+ disabled?: boolean;
10
+ }
11
+
12
+ /**
13
+ * Input + addon 统一容器 — shadcn 2025-10 新增,等价 antd `Input` 的
14
+ * `prefix` / `suffix` / `addonBefore` / `addonAfter` 集合,但拆为更通用的
15
+ * **复合插槽**:`<InputGroupAddon position="before|after">`(addon)+
16
+ * `<InputGroupInput>` / `<InputGroupTextarea>`(主体),按需嵌套图标 / 按钮 / 文本。
17
+ */
18
+ const InputGroup = React.forwardRef<HTMLDivElement, InputGroupProps>(
19
+ ({ className, disabled, children, ...props }, ref) => (
20
+ <div
21
+ ref={ref}
22
+ data-disabled={disabled ? '' : undefined}
23
+ className={cn(
24
+ 'flex w-full items-stretch overflow-hidden rounded-md border border-input bg-background shadow-sm',
25
+ 'focus-within:ring-1 focus-within:ring-ring',
26
+ 'data-[disabled]:cursor-not-allowed data-[disabled]:opacity-50',
27
+ className,
28
+ )}
29
+ {...props}
30
+ >
31
+ {children}
32
+ </div>
33
+ ),
34
+ );
35
+ InputGroup.displayName = 'InputGroup';
36
+
37
+ export interface InputGroupAddonProps
38
+ extends React.HTMLAttributes<HTMLDivElement> {
39
+ /**
40
+ * 位于输入框的哪一侧。
41
+ * @default "before"
42
+ */
43
+ position?: 'before' | 'after';
44
+ /**
45
+ * `text` = 默认 muted 文本背景 / `icon` = 紧凑、透明背景(图标专用)/
46
+ * `button` = 用于嵌入 `<Button>`(去内边距)。
47
+ * @default "text"
48
+ */
49
+ variant?: 'text' | 'icon' | 'button';
50
+ }
51
+
52
+ const InputGroupAddon = React.forwardRef<HTMLDivElement, InputGroupAddonProps>(
53
+ ({ position = 'before', variant = 'text', className, ...props }, ref) => (
54
+ <div
55
+ ref={ref}
56
+ data-position={position}
57
+ className={cn(
58
+ 'flex shrink-0 items-center',
59
+ variant === 'text' &&
60
+ 'border-input bg-muted px-3 text-sm text-muted-foreground',
61
+ variant === 'icon' && 'px-2 text-muted-foreground',
62
+ variant === 'button' && '[&>*]:rounded-none [&>*]:border-0 [&>*]:shadow-none',
63
+ position === 'before' && variant !== 'icon' && 'border-r',
64
+ position === 'after' && variant !== 'icon' && 'border-l',
65
+ className,
66
+ )}
67
+ {...props}
68
+ />
69
+ ),
70
+ );
71
+ InputGroupAddon.displayName = 'InputGroupAddon';
72
+
73
+ const InputGroupInput = React.forwardRef<
74
+ HTMLInputElement,
75
+ React.InputHTMLAttributes<HTMLInputElement>
76
+ >(({ className, type = 'text', ...props }, ref) => (
77
+ <input
78
+ ref={ref}
79
+ type={type}
80
+ className={cn(
81
+ 'flex h-9 w-full min-w-0 flex-1 bg-transparent px-3 py-1 text-sm placeholder:text-muted-foreground',
82
+ 'focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50',
83
+ className,
84
+ )}
85
+ {...props}
86
+ />
87
+ ));
88
+ InputGroupInput.displayName = 'InputGroupInput';
89
+
90
+ const InputGroupTextarea = React.forwardRef<
91
+ HTMLTextAreaElement,
92
+ React.TextareaHTMLAttributes<HTMLTextAreaElement>
93
+ >(({ className, rows = 3, ...props }, ref) => (
94
+ <textarea
95
+ ref={ref}
96
+ rows={rows}
97
+ className={cn(
98
+ 'flex w-full min-w-0 flex-1 bg-transparent px-3 py-2 text-sm placeholder:text-muted-foreground',
99
+ 'focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50',
100
+ className,
101
+ )}
102
+ {...props}
103
+ />
104
+ ));
105
+ InputGroupTextarea.displayName = 'InputGroupTextarea';
106
+
107
+ export { InputGroup, InputGroupAddon, InputGroupInput, InputGroupTextarea };