@teamix-evo/ui 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (282) hide show
  1. package/README.md +184 -184
  2. package/manifest.json +680 -492
  3. package/package.json +15 -9
  4. package/src/components/accordion/accordion.meta.md +5 -9
  5. package/src/components/accordion/accordion.stories.tsx +3 -3
  6. package/src/components/accordion/accordion.tsx +104 -8
  7. package/src/components/affix/affix.meta.md +21 -12
  8. package/src/components/affix/affix.stories.tsx +101 -26
  9. package/src/components/affix/affix.tsx +79 -9
  10. package/src/components/alert/alert.meta.md +52 -26
  11. package/src/components/alert/alert.stories.tsx +66 -21
  12. package/src/components/alert/alert.tsx +81 -34
  13. package/src/components/alert-dialog/alert-dialog.meta.md +48 -16
  14. package/src/components/alert-dialog/alert-dialog.stories.tsx +145 -3
  15. package/src/components/alert-dialog/alert-dialog.tsx +60 -13
  16. package/src/components/anchor/anchor.meta.md +10 -14
  17. package/src/components/anchor/anchor.stories.tsx +3 -3
  18. package/src/components/anchor/anchor.tsx +2 -2
  19. package/src/components/app/app.meta.md +10 -14
  20. package/src/components/app/app.stories.tsx +6 -6
  21. package/src/components/aspect-ratio/aspect-ratio.meta.md +4 -8
  22. package/src/components/aspect-ratio/aspect-ratio.stories.tsx +3 -3
  23. package/src/components/auto-complete/auto-complete.meta.md +19 -20
  24. package/src/components/auto-complete/auto-complete.stories.tsx +44 -3
  25. package/src/components/auto-complete/auto-complete.tsx +119 -71
  26. package/src/components/avatar/avatar.meta.md +9 -22
  27. package/src/components/avatar/avatar.stories.tsx +21 -3
  28. package/src/components/avatar/avatar.tsx +24 -23
  29. package/src/components/badge/badge.meta.md +14 -18
  30. package/src/components/badge/badge.stories.tsx +2 -2
  31. package/src/components/badge/badge.tsx +2 -2
  32. package/src/components/breadcrumb/breadcrumb.meta.md +29 -20
  33. package/src/components/breadcrumb/breadcrumb.stories.tsx +120 -5
  34. package/src/components/breadcrumb/breadcrumb.tsx +22 -8
  35. package/src/components/button/button.meta.md +261 -29
  36. package/src/components/button/button.stories.tsx +549 -41
  37. package/src/components/button/button.tsx +335 -33
  38. package/src/components/calendar/calendar.meta.md +19 -14
  39. package/src/components/calendar/calendar.stories.tsx +5 -5
  40. package/src/components/calendar/calendar.tsx +73 -8
  41. package/src/components/card/card.meta.md +31 -34
  42. package/src/components/card/card.stories.tsx +34 -3
  43. package/src/components/card/card.tsx +146 -63
  44. package/src/components/carousel/carousel.meta.md +10 -14
  45. package/src/components/carousel/carousel.stories.tsx +1 -1
  46. package/src/components/cascader/cascader.meta.md +43 -22
  47. package/src/components/cascader/cascader.stories.tsx +13 -2
  48. package/src/components/cascader/cascader.tsx +427 -84
  49. package/src/components/checkbox/checkbox.meta.md +74 -24
  50. package/src/components/checkbox/checkbox.stories.tsx +160 -2
  51. package/src/components/checkbox/checkbox.tsx +77 -9
  52. package/src/components/collapsible/collapsible.meta.md +7 -6
  53. package/src/components/collapsible/collapsible.stories.tsx +2 -2
  54. package/src/components/collapsible/collapsible.tsx +93 -6
  55. package/src/components/color-picker/color-picker.meta.md +16 -20
  56. package/src/components/color-picker/color-picker.stories.tsx +86 -7
  57. package/src/components/color-picker/color-picker.tsx +19 -9
  58. package/src/components/command/command.meta.md +7 -11
  59. package/src/components/command/command.stories.tsx +4 -4
  60. package/src/components/command/command.tsx +18 -7
  61. package/src/components/context-menu/context-menu.meta.md +5 -25
  62. package/src/components/context-menu/context-menu.stories.tsx +4 -4
  63. package/src/components/context-menu/context-menu.tsx +21 -8
  64. package/src/components/data-table/data-table.meta.md +14 -18
  65. package/src/components/data-table/data-table.stories.tsx +1 -1
  66. package/src/components/data-table/data-table.tsx +2 -2
  67. package/src/components/date-picker/date-picker.meta.md +90 -41
  68. package/src/components/date-picker/date-picker.stories.tsx +55 -5
  69. package/src/components/date-picker/date-picker.tsx +1489 -91
  70. package/src/components/descriptions/descriptions.meta.md +12 -16
  71. package/src/components/descriptions/descriptions.stories.tsx +2 -2
  72. package/src/components/descriptions/descriptions.tsx +22 -14
  73. package/src/components/dialog/dialog.meta.md +67 -17
  74. package/src/components/dialog/dialog.stories.tsx +182 -20
  75. package/src/components/dialog/dialog.tsx +67 -15
  76. package/src/components/dialog/imperative.tsx +252 -0
  77. package/src/components/drawer/drawer.meta.md +27 -39
  78. package/src/components/drawer/drawer.stories.tsx +29 -12
  79. package/src/components/drawer/drawer.tsx +22 -114
  80. package/src/components/dropdown-menu/dropdown-menu.meta.md +64 -24
  81. package/src/components/dropdown-menu/dropdown-menu.stories.tsx +81 -3
  82. package/src/components/dropdown-menu/dropdown-menu.tsx +24 -10
  83. package/src/components/ellipsis/ellipsis.meta.md +87 -0
  84. package/src/components/ellipsis/ellipsis.stories.tsx +72 -0
  85. package/src/components/ellipsis/ellipsis.tsx +153 -0
  86. package/src/components/empty/empty.meta.md +10 -14
  87. package/src/components/empty/empty.stories.tsx +3 -3
  88. package/src/components/empty/empty.tsx +10 -3
  89. package/src/components/field/field.meta.md +46 -25
  90. package/src/components/field/field.stories.tsx +380 -3
  91. package/src/components/field/field.tsx +263 -35
  92. package/src/components/filter-bar/filter-bar.meta.md +92 -0
  93. package/src/components/filter-bar/filter-bar.stories.tsx +1083 -0
  94. package/src/components/filter-bar/filter-bar.tsx +568 -0
  95. package/src/components/flex/flex.meta.md +59 -20
  96. package/src/components/flex/flex.stories.tsx +65 -10
  97. package/src/components/flex/flex.tsx +27 -4
  98. package/src/components/float-button/float-button.meta.md +10 -29
  99. package/src/components/float-button/float-button.stories.tsx +6 -6
  100. package/src/components/form/form.meta.md +31 -52
  101. package/src/components/form/form.stories.tsx +350 -3
  102. package/src/components/form/form.tsx +101 -35
  103. package/src/components/grid/grid.meta.md +4 -24
  104. package/src/components/grid/grid.stories.tsx +2 -2
  105. package/src/components/hover-card/hover-card.meta.md +9 -10
  106. package/src/components/hover-card/hover-card.stories.tsx +29 -4
  107. package/src/components/hover-card/hover-card.tsx +51 -13
  108. package/src/components/icon/DEVELOPMENT.md +809 -0
  109. package/src/components/icon/icon.meta.md +170 -0
  110. package/src/components/icon/icon.stories.tsx +344 -0
  111. package/src/components/icon/icon.tsx +248 -0
  112. package/src/components/image/image.meta.md +14 -18
  113. package/src/components/image/image.stories.tsx +3 -3
  114. package/src/components/image/image.tsx +2 -0
  115. package/src/components/input/demo/sizes.tsx +2 -2
  116. package/src/components/input/input.meta.md +44 -43
  117. package/src/components/input/input.stories.tsx +62 -35
  118. package/src/components/input/input.tsx +96 -101
  119. package/src/components/input-group/input-group.meta.md +53 -39
  120. package/src/components/input-group/input-group.stories.tsx +49 -16
  121. package/src/components/input-group/input-group.tsx +43 -8
  122. package/src/components/input-number/input-number.meta.md +68 -20
  123. package/src/components/input-number/input-number.stories.tsx +33 -6
  124. package/src/components/input-number/input-number.tsx +79 -20
  125. package/src/components/input-otp/input-otp.meta.md +8 -20
  126. package/src/components/input-otp/input-otp.stories.tsx +3 -3
  127. package/src/components/input-otp/input-otp.tsx +1 -1
  128. package/src/components/item/item.meta.md +8 -26
  129. package/src/components/item/item.stories.tsx +3 -3
  130. package/src/components/item/item.tsx +7 -6
  131. package/src/components/kbd/kbd.meta.md +7 -19
  132. package/src/components/kbd/kbd.stories.tsx +4 -4
  133. package/src/components/kbd/kbd.tsx +8 -4
  134. package/src/components/label/label.meta.md +21 -18
  135. package/src/components/label/label.stories.tsx +64 -6
  136. package/src/components/label/label.tsx +91 -19
  137. package/src/components/masonry/masonry.meta.md +8 -12
  138. package/src/components/masonry/masonry.stories.tsx +4 -4
  139. package/src/components/mentions/mentions.meta.md +42 -21
  140. package/src/components/mentions/mentions.stories.tsx +120 -6
  141. package/src/components/mentions/mentions.tsx +10 -5
  142. package/src/components/menubar/menubar.meta.md +4 -8
  143. package/src/components/menubar/menubar.stories.tsx +55 -3
  144. package/src/components/menubar/menubar.tsx +9 -9
  145. package/src/components/native-select/native-select.meta.md +7 -11
  146. package/src/components/native-select/native-select.stories.tsx +4 -4
  147. package/src/components/native-select/native-select.tsx +1 -1
  148. package/src/components/navigation-menu/navigation-menu.meta.md +4 -8
  149. package/src/components/navigation-menu/navigation-menu.stories.tsx +106 -3
  150. package/src/components/navigation-menu/navigation-menu.tsx +6 -3
  151. package/src/components/notification/notification.meta.md +41 -8
  152. package/src/components/notification/notification.stories.tsx +9 -9
  153. package/src/components/notification/notification.tsx +34 -19
  154. package/src/components/page-header/DEVELOPMENT.md +842 -0
  155. package/src/components/page-header/page-header.meta.md +208 -0
  156. package/src/components/page-header/page-header.stories.tsx +421 -0
  157. package/src/components/page-header/page-header.tsx +281 -0
  158. package/src/components/pagination/pagination.meta.md +122 -50
  159. package/src/components/pagination/pagination.stories.tsx +227 -11
  160. package/src/components/pagination/pagination.tsx +355 -63
  161. package/src/components/popconfirm/popconfirm.meta.md +19 -23
  162. package/src/components/popconfirm/popconfirm.stories.tsx +2 -2
  163. package/src/components/popconfirm/popconfirm.tsx +1 -1
  164. package/src/components/popover/popover.meta.md +64 -12
  165. package/src/components/popover/popover.stories.tsx +83 -7
  166. package/src/components/popover/popover.tsx +77 -28
  167. package/src/components/progress/progress.meta.md +43 -26
  168. package/src/components/progress/progress.stories.tsx +2 -2
  169. package/src/components/progress/progress.tsx +19 -11
  170. package/src/components/radio-group/radio-group.meta.md +78 -11
  171. package/src/components/radio-group/radio-group.stories.tsx +38 -2
  172. package/src/components/radio-group/radio-group.tsx +149 -18
  173. package/src/components/rate/rate.meta.md +41 -19
  174. package/src/components/rate/rate.stories.tsx +2 -2
  175. package/src/components/rate/rate.tsx +37 -10
  176. package/src/components/resizable/resizable.meta.md +4 -12
  177. package/src/components/resizable/resizable.stories.tsx +5 -5
  178. package/src/components/resizable/resizable.tsx +1 -1
  179. package/src/components/result/result.meta.md +10 -14
  180. package/src/components/result/result.stories.tsx +2 -2
  181. package/src/components/result/result.tsx +21 -12
  182. package/src/components/scroll-area/scroll-area.meta.md +4 -8
  183. package/src/components/scroll-area/scroll-area.stories.tsx +5 -5
  184. package/src/components/segmented/segmented.meta.md +15 -17
  185. package/src/components/segmented/segmented.stories.tsx +3 -3
  186. package/src/components/segmented/segmented.tsx +15 -7
  187. package/src/components/select/select.meta.md +199 -67
  188. package/src/components/select/select.stories.tsx +238 -63
  189. package/src/components/select/select.tsx +718 -171
  190. package/src/components/separator/separator.meta.md +10 -14
  191. package/src/components/separator/separator.stories.tsx +2 -2
  192. package/src/components/separator/separator.tsx +3 -7
  193. package/src/components/sheet/sheet.meta.md +26 -21
  194. package/src/components/sheet/sheet.stories.tsx +116 -10
  195. package/src/components/sheet/sheet.tsx +116 -29
  196. package/src/components/sidebar/sidebar.meta.md +28 -38
  197. package/src/components/sidebar/sidebar.stories.tsx +696 -29
  198. package/src/components/sidebar/sidebar.tsx +615 -142
  199. package/src/components/skeleton/skeleton.meta.md +7 -31
  200. package/src/components/skeleton/skeleton.stories.tsx +3 -3
  201. package/src/components/skeleton/skeleton.tsx +7 -7
  202. package/src/components/slider/slider.meta.md +60 -13
  203. package/src/components/slider/slider.stories.tsx +58 -6
  204. package/src/components/slider/slider.tsx +154 -13
  205. package/src/components/sonner/sonner.meta.md +54 -8
  206. package/src/components/sonner/sonner.stories.tsx +79 -11
  207. package/src/components/sonner/sonner.tsx +137 -8
  208. package/src/components/spinner/spinner.meta.md +57 -21
  209. package/src/components/spinner/spinner.stories.tsx +66 -14
  210. package/src/components/spinner/spinner.tsx +111 -9
  211. package/src/components/statistic/statistic.meta.md +14 -30
  212. package/src/components/statistic/statistic.stories.tsx +1 -1
  213. package/src/components/statistic/statistic.tsx +4 -5
  214. package/src/components/steps/steps.meta.md +20 -15
  215. package/src/components/steps/steps.stories.tsx +37 -2
  216. package/src/components/steps/steps.tsx +15 -12
  217. package/src/components/switch/switch.meta.md +56 -15
  218. package/src/components/switch/switch.stories.tsx +5 -5
  219. package/src/components/switch/switch.tsx +59 -13
  220. package/src/components/table/table.meta.md +3 -7
  221. package/src/components/table/table.stories.tsx +1 -1
  222. package/src/components/table/table.tsx +4 -4
  223. package/src/components/tabs/tabs.meta.md +40 -32
  224. package/src/components/tabs/tabs.stories.tsx +104 -26
  225. package/src/components/tabs/tabs.tsx +125 -54
  226. package/src/components/tag/tag.meta.md +104 -68
  227. package/src/components/tag/tag.stories.tsx +183 -15
  228. package/src/components/tag/tag.tsx +222 -21
  229. package/src/components/textarea/textarea.meta.md +42 -31
  230. package/src/components/textarea/textarea.stories.tsx +32 -6
  231. package/src/components/textarea/textarea.tsx +32 -8
  232. package/src/components/time-picker/time-picker.meta.md +119 -50
  233. package/src/components/time-picker/time-picker.stories.tsx +65 -33
  234. package/src/components/time-picker/time-picker.tsx +889 -101
  235. package/src/components/timeline/timeline.meta.md +16 -17
  236. package/src/components/timeline/timeline.stories.tsx +24 -4
  237. package/src/components/timeline/timeline.tsx +32 -12
  238. package/src/components/toggle/toggle.meta.md +8 -12
  239. package/src/components/toggle/toggle.stories.tsx +4 -4
  240. package/src/components/toggle/toggle.tsx +4 -3
  241. package/src/components/toggle-group/toggle-group.meta.md +10 -14
  242. package/src/components/toggle-group/toggle-group.stories.tsx +3 -3
  243. package/src/components/toggle-group/toggle-group.tsx +2 -2
  244. package/src/components/tooltip/tooltip.meta.md +63 -18
  245. package/src/components/tooltip/tooltip.stories.tsx +42 -5
  246. package/src/components/tooltip/tooltip.tsx +81 -21
  247. package/src/components/tour/tour.meta.md +16 -20
  248. package/src/components/tour/tour.stories.tsx +3 -3
  249. package/src/components/tour/tour.tsx +3 -3
  250. package/src/components/transfer/transfer.meta.md +18 -22
  251. package/src/components/transfer/transfer.stories.tsx +2 -2
  252. package/src/components/transfer/transfer.tsx +28 -21
  253. package/src/components/tree/tree.meta.md +67 -22
  254. package/src/components/tree/tree.stories.tsx +1 -1
  255. package/src/components/tree/tree.tsx +9 -8
  256. package/src/components/tree-select/tree-select.meta.md +59 -23
  257. package/src/components/tree-select/tree-select.stories.tsx +2 -2
  258. package/src/components/tree-select/tree-select.tsx +42 -7
  259. package/src/components/typography/typography.meta.md +61 -39
  260. package/src/components/typography/typography.stories.tsx +14 -9
  261. package/src/components/typography/typography.tsx +38 -25
  262. package/src/components/upload/upload.meta.md +61 -25
  263. package/src/components/upload/upload.stories.tsx +69 -3
  264. package/src/components/upload/upload.tsx +170 -37
  265. package/src/components/watermark/watermark.meta.md +15 -19
  266. package/src/components/watermark/watermark.stories.tsx +98 -8
  267. package/src/hooks/use-breakpoint.ts +117 -0
  268. package/src/hooks/use-debounce-callback.ts +52 -0
  269. package/src/hooks/use-mobile.ts +23 -0
  270. package/src/stories/theme-tokens.stories.tsx +747 -0
  271. package/src/utils/trigger-input.ts +53 -0
  272. package/src/components/button-group/button-group.meta.md +0 -101
  273. package/src/components/button-group/button-group.stories.tsx +0 -93
  274. package/src/components/button-group/button-group.tsx +0 -75
  275. package/src/components/combobox/combobox.meta.md +0 -102
  276. package/src/components/combobox/combobox.stories.tsx +0 -55
  277. package/src/components/combobox/combobox.tsx +0 -130
  278. package/src/components/input/demo/addon.tsx +0 -15
  279. package/src/components/input/demo/with-prefix-suffix.tsx +0 -19
  280. package/src/components/space/space.meta.md +0 -103
  281. package/src/components/space/space.stories.tsx +0 -108
  282. package/src/components/space/space.tsx +0 -106
@@ -58,25 +58,22 @@ const titleSizes = {
58
58
  3: 'text-xl font-semibold',
59
59
  4: 'text-lg font-medium',
60
60
  5: 'text-base font-medium',
61
+ 6: 'text-sm font-medium',
61
62
  } as const;
62
63
 
63
64
  export interface TitleProps extends React.HTMLAttributes<HTMLHeadingElement> {
64
65
  /**
65
- * 层级(1~5),决定字号 / 粗细;同时映射为 `<h1>~<h5>` 标签。
66
+ * 层级(1~6),决定字号 / 粗细;同时映射为 `<h1>~<h6>` 标签 — 对齐 antd `Typography.Title.level`。
66
67
  * @default 1
67
68
  */
68
- level?: 1 | 2 | 3 | 4 | 5;
69
+ level?: 1 | 2 | 3 | 4 | 5 | 6;
69
70
  }
70
71
 
71
72
  const Title = React.forwardRef<HTMLHeadingElement, TitleProps>(
72
73
  ({ level = 1, className, ...props }, ref) => {
73
- const Comp = `h${level}` as 'h1' | 'h2' | 'h3' | 'h4' | 'h5';
74
+ const Comp = `h${level}` as 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
74
75
  return (
75
- <Comp
76
- ref={ref}
77
- className={cn(titleSizes[level], className)}
78
- {...props}
79
- />
76
+ <Comp ref={ref} className={cn(titleSizes[level], className)} {...props} />
80
77
  );
81
78
  },
82
79
  );
@@ -98,16 +95,30 @@ const Paragraph = React.forwardRef<HTMLParagraphElement, ParagraphProps>(
98
95
  );
99
96
  Paragraph.displayName = 'Paragraph';
100
97
 
101
- // ─── Text(行内文本,支持 antd ellipsis / copyable / type)──────────────────
98
+ // ─── Text(行内文本,支持 antd ellipsis / copyable / color)──────────────────
102
99
 
103
- export type TextType = 'default' | 'secondary' | 'success' | 'warning' | 'danger';
100
+ /**
101
+ * Text 语义色 6 档(见 [ADR 0021](../../../../../docs/adr/0021-semantic-color-api-unification.md))。
102
+ * 字面与 OpenTrek tokens 对齐:
103
+ * - antd `secondary` → `muted`(对齐 `--muted-foreground`)
104
+ * - antd `danger` → `destructive`(对齐 `--destructive`)
105
+ * - **不收 `info`** — 如需信息色,用 `primary`。
106
+ */
107
+ export type TextColor =
108
+ | 'default'
109
+ | 'muted'
110
+ | 'primary'
111
+ | 'success'
112
+ | 'warning'
113
+ | 'destructive';
104
114
 
105
115
  export interface TextProps extends React.HTMLAttributes<HTMLSpanElement> {
106
116
  /**
107
- * 语义色(antd `type` 并集)。
117
+ * 语义色 — 6 档枚举,字面与 OpenTrek tokens 对齐( [ADR 0021](../../../../../docs/adr/0021-semantic-color-api-unification.md))
118
+ * antd `type` 迁移:`secondary`→`muted`、`danger`→`destructive`。
108
119
  * @default "default"
109
120
  */
110
- type?: TextType;
121
+ color?: TextColor;
111
122
  /**
112
123
  * 删除线。
113
124
  * @default false
@@ -141,18 +152,19 @@ export interface TextProps extends React.HTMLAttributes<HTMLSpanElement> {
141
152
  copyable?: boolean | { text: string; tooltips?: [string, string] };
142
153
  }
143
154
 
144
- const typeColor: Record<TextType, string> = {
155
+ const colorClass: Record<TextColor, string> = {
145
156
  default: 'text-foreground',
146
- secondary: 'text-muted-foreground',
157
+ muted: 'text-muted-foreground',
158
+ primary: 'text-primary',
147
159
  success: 'text-success',
148
160
  warning: 'text-warning',
149
- danger: 'text-destructive',
161
+ destructive: 'text-destructive',
150
162
  };
151
163
 
152
164
  const Text = React.forwardRef<HTMLSpanElement, TextProps>(
153
165
  (
154
166
  {
155
- type = 'default',
167
+ color = 'default',
156
168
  delete: del = false,
157
169
  disabled = false,
158
170
  strong = false,
@@ -171,8 +183,8 @@ const Text = React.forwardRef<HTMLSpanElement, TextProps>(
171
183
  typeof copyable === 'object' && copyable.text
172
184
  ? copyable.text
173
185
  : typeof children === 'string'
174
- ? children
175
- : '';
186
+ ? children
187
+ : '';
176
188
  void navigator.clipboard?.writeText(text).then(() => {
177
189
  setCopied(true);
178
190
  window.setTimeout(() => setCopied(false), 1500);
@@ -180,7 +192,7 @@ const Text = React.forwardRef<HTMLSpanElement, TextProps>(
180
192
  };
181
193
 
182
194
  const cls = cn(
183
- typeColor[type],
195
+ colorClass[color],
184
196
  strong && 'font-semibold',
185
197
  del && 'line-through',
186
198
  disabled && 'cursor-not-allowed select-none opacity-50',
@@ -201,7 +213,7 @@ const Text = React.forwardRef<HTMLSpanElement, TextProps>(
201
213
  type="button"
202
214
  onClick={handleCopy}
203
215
  aria-label={copied ? '已复制' : '复制'}
204
- className="rounded-sm p-0.5 text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
216
+ className="cursor-pointer rounded-sm p-0.5 text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
205
217
  >
206
218
  {copied ? (
207
219
  <Check className="size-3.5 text-success" />
@@ -224,18 +236,19 @@ Text.displayName = 'Text';
224
236
 
225
237
  // ─── Link(antd 风格,默认下划线 hover)────────────────────────────────────
226
238
 
227
- export interface LinkProps extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
228
- /** 语义色(同 Text 的 type)。 @default "default" */
229
- type?: TextType;
239
+ export interface LinkProps
240
+ extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
241
+ /** 语义色(同 Text 的 color)。 @default "default" */
242
+ color?: TextColor;
230
243
  }
231
244
 
232
245
  const Link = React.forwardRef<HTMLAnchorElement, LinkProps>(
233
- ({ className, type = 'default', ...props }, ref) => (
246
+ ({ className, color = 'default', ...props }, ref) => (
234
247
  <a
235
248
  ref={ref}
236
249
  className={cn(
237
250
  'underline-offset-4 transition-colors hover:underline',
238
- type === 'default' ? 'text-primary' : typeColor[type],
251
+ color === 'default' ? 'text-primary' : colorClass[color],
239
252
  className,
240
253
  )}
241
254
  {...props}
@@ -3,7 +3,7 @@ id: upload
3
3
  name: Upload
4
4
  displayName: 上传
5
5
  type: component
6
- category: form
6
+ category: data-entry
7
7
  since: 0.1.0
8
8
  package: '@teamix-evo/ui'
9
9
  ---
@@ -26,38 +26,37 @@ package: '@teamix-evo/ui'
26
26
  ## Props
27
27
 
28
28
  <!-- auto:props:begin -->
29
-
30
- | 名称 | 类型 | 默认值 | 必填 | 说明 |
31
- | -------------- | ------------------------------------------------------- | ------- | ---- | -------------------------------------------------------------------------- |
32
- | `fileList` | `UploadFileItem[]` | – | – | 已上传文件列表(受控)— 不传则组件内部维护本地列表(限制少,通常配 onChange)。 |
33
- | `accept` | `string` | | – | 文件接受类型(等价 input.accept) — e.g. `image/*`, `.pdf,.docx`。 |
34
- | `multiple` | `boolean` | `false` | – | 是否支持多文件选择(antd `multiple` 并集)。 |
35
- | `maxCount` | `number` | – | – | 最大文件数(超出则忽略,触发 `onExceed`)。 |
36
- | `maxSize` | `number` | – | – | 单文件最大字节数(超出则忽略,触发 `onExceed`)。 |
37
- | `dragger` | `boolean` | `false` | – | 启用拖拽上传(antd `Dragger` 并集) — 显示大块虚线区,可点击亦可拖拽。 |
38
- | `disabled` | `boolean` | – | – | 禁用上传(已选文件不可删除)。 |
39
- | `showFileList` | `boolean` | `true` | – | 是否显示文件列表(antd `showUploadList` 并集)。 |
40
- | `onChange` | `(next: UploadFileItem[]) => void` | | – | 文件被添加(用户选择 / 拖入)时的回调返回更新后的列表。 |
41
- | `onRemove` | `(file: UploadFileItem) => void` | – | – | 文件被移除时的回调。 |
42
- | `onExceed` | `(rejected: File[], reason: 'count' \| 'size') => void` | – | – | 超出 maxCount / maxSize 时的回调。 |
43
- | `children` | `React.ReactNode` | – | – | 触发器自定义内容(非 dragger 模式下渲染于 Button 内)。 |
44
- | `className` | `string` | – | – | |
45
-
29
+ | 名称 | 类型 | 默认值 | 必填 | 说明 |
30
+ | --- | --- | --- | --- | --- |
31
+ | `fileList` | `UploadFileItem[]` | | | 已上传文件列表(受控)— 不传则组件内部维护本地列表(限制少,通常配 onChange)。 |
32
+ | `accept` | `string` | – | – | 文件接受类型(等价 input.accept) e.g. `image/*`, `.pdf,.docx`。 |
33
+ | `multiple` | `boolean` | `false` | – | 是否支持多文件选择(antd `multiple` 并集) |
34
+ | `maxCount` | `number` | | – | 最大文件数(超出则忽略,触发 `onExceed`)。 |
35
+ | `maxSize` | `number` | – | – | 单文件最大字节数(超出则忽略,触发 `onExceed`)。 |
36
+ | `beforeUpload` | `( file: File, fileList: File[], ) => boolean \| File \| void \| Promise<boolean \| File \| void>` | – | – | 文件添加前的校验钩子(antd `beforeUpload` 并集)。 - 返回 `false` 拒绝当前文件 - 返回 `true` / `undefined` 允许 - 返回 新 `File` 对象可替换原文件(重命名 / 压缩等) - 支持异步 `Promise` 注意: 此钩子在 `maxCount` / `maxSize` 校验**之后**调用。 |
37
+ | `dragger` | `boolean` | `false` | – | 启用拖拽上传(antd `Dragger` 并集) — 显示大块虚线区,可点击亦可拖拽。 |
38
+ | `disabled` | `boolean` | – | – | 禁用上传(已选文件不可删除)。 |
39
+ | `showFileList` | `boolean` | `true` | – | 是否显示文件列表(antd `showUploadList` 并集)。 |
40
+ | `listType` | `'text' \| 'image' \| 'card'` | `"text"` | – | 文件列表的展示形态(antd `listType` 并集) — - `text`(默认):文件名 + 进度条,适用文档上传 - `image`:小缩略图 + 文件名,适用头像 / 附件预览 - `card`:网格大缩略图卡,适用多图上传 / 产品图库 |
41
+ | `onChange` | `(next: UploadFileItem[]) => void` | – | – | 文件被添加(用户选择 / 拖入)时的回调 — 返回更新后的列表。 |
42
+ | `onRemove` | `(file: UploadFileItem) => void` | – | – | 文件被移除时的回调。 |
43
+ | `onExceed` | `(rejected: File[], reason: 'count' \| 'size') => void` | – | – | 超出 maxCount / maxSize 时的回调。 |
44
+ | `children` | `React.ReactNode` | – | – | 触发器自定义内容(非 dragger 模式下渲染于 Button 内)。 |
45
+ | `className` | `string` | – | – | – |
46
46
  <!-- auto:props:end -->
47
47
 
48
48
  ## 依赖
49
49
 
50
50
  <!-- auto:deps:begin -->
51
-
52
51
  ### 同库依赖
53
52
 
54
53
  > `teamix-evo ui add upload` 时,以下 entry 会被自动连带安装(无需手动 add)。
55
54
 
56
- | Entry | 类型 | 描述 |
57
- | ---------- | --------- | --------------------------------------------------------------------------------------- |
58
- | `cn` | util | Tailwind className 合并工具(clsx + tailwind-merge) |
59
- | `button` | component | 通用按钮 — shadcn 实现 + antd 功能扩展(loading / icon / shape / block / dashed variant) |
60
- | `progress` | component | 进度条 — Radix 线性 + antd 的 status / showInfo / size 并集 + 配套 ProgressCircle 环形 |
55
+ | Entry | 类型 | 描述 |
56
+ | --- | --- | --- |
57
+ | `cn` | util | Tailwind className 合并工具(clsx + tailwind-merge) |
58
+ | `button` | component | 通用按钮 — shadcn 实现 + cloud-design 能力并集(loading / icon / shape / block / dashed variant / color 语义双 prop / disabledTooltip)。同文件合一导出 ButtonGroup + ButtonGroupText(等价 antd Space.Compact + cd SplitButton)。 |
59
+ | `progress` | component | 进度条 — Radix 线性 + antd 的 status / showInfo / size 并集 + 配套 ProgressCircle 环形 |
61
60
 
62
61
  ### npm 依赖
63
62
 
@@ -66,7 +65,6 @@ package: '@teamix-evo/ui'
66
65
  ```bash
67
66
  pnpm add lucide-react@^0.460.0
68
67
  ```
69
-
70
68
  <!-- auto:deps:end -->
71
69
 
72
70
  ## AI 生成纪律
@@ -118,3 +116,41 @@ const [files, setFiles] = React.useState<UploadFileItem[]>([]);
118
116
  }}
119
117
  />
120
118
  ```
119
+
120
+ ---
121
+
122
+ ## Upload 形态 — 旧库 API → 新映射
123
+
124
+ > 旧库 `Upload`(hybridcloud,含 Upload.Card / Upload.Dragger 子组件) → 新库 `Upload`(单组件 prop 切换)。
125
+ > 新库**不内置网络层**(XHR/fetch),由消费方在 `onChange` 中自行上传。
126
+
127
+ ### 命名 & 结构映射
128
+
129
+ | 旧库 | 新库 | 说明 |
130
+ | ---------------------------------- | -------------------------------- | -------------------------------- |
131
+ | `Upload` | `Upload` | 组件名相同 |
132
+ | `Upload.Card` | `<Upload listType="card" />` | 子组件变 prop |
133
+ | `Upload.Dragger` | `<Upload dragger />` | 子组件变 prop |
134
+ | `value / defaultValue` | `fileList` | 文件列表(受控) |
135
+ | `limit` | `maxCount` | 最大文件数 |
136
+ | `dragable` | `dragger` | 拖拽上传 prop |
137
+ | `action / headers / method` | 无(消费方自行请求) | 架构差异:无内置 XHR |
138
+ | `onSuccess / onError / onProgress` | 无(通过 fileList.status/percent) | 状态驱动而非回调 |
139
+ | `beforeUpload` | _(未来 P1)_ | MVP 用 accept + maxSize 前置过滤 |
140
+ | `autoUpload` | _(未来 P1)_ | MVP 仅选件,上传由消费方控制 |
141
+ | `itemRender` | _(未来 P2)_ | MVP 使用内置渲染 |
142
+ | `shape='card'` | `listType='card'` + dragger | 组合代替 |
143
+ | `useDataURL` | 不复刻 | 消费方 FileReader |
144
+ | `progressProps` | 不复刻 | 内置简化 Progress |
145
+ | `isPreview / renderPreview` | 不复刻 | 无预览态 |
146
+ | `fileKeyName` | 不复刻 | 固定用 uid |
147
+
148
+ ### 迁移 FAQ
149
+
150
+ | 问题 | 回答 |
151
+ | ------------------- | ------------------------------------------------------------------------------------ |
152
+ | 无 action 怎么上传? | 在 `onChange` 中拿到 `file` 对象,自行 fetch/XHR;通过 `setFiles` 更新 status/percent |
153
+ | 怎么实现拖拽? | `<Upload dragger />` 即可;虚线区域点击/拖放均可触发 |
154
+ | 怎么限制文件类型? | `accept="image/*"` 或 `accept=".pdf,.docx"` |
155
+ | 怎么显示上传进度? | fileList item 设 `status:'uploading'` + `percent:50` |
156
+ | Card 模式怎么用? | `<Upload listType="card" dragger />` — 网格缩略图 + 拖拽区 |
@@ -1,9 +1,9 @@
1
1
  import * as React from 'react';
2
- import type { Meta, StoryObj } from '@storybook/react';
2
+ import type { Meta, StoryObj } from '@storybook/react-vite';
3
3
  import { Upload, type UploadFileItem } from './upload';
4
4
 
5
5
  const meta: Meta<typeof Upload> = {
6
- title: '表单与输入 · Form/Upload',
6
+ title: '数据录入 · Data Entry/Upload',
7
7
  component: Upload,
8
8
  tags: ['autodocs'],
9
9
  parameters: {
@@ -19,8 +19,12 @@ const meta: Meta<typeof Upload> = {
19
19
  dragger: { control: 'boolean' },
20
20
  disabled: { control: 'boolean' },
21
21
  accept: { control: 'text' },
22
+ listType: {
23
+ control: 'inline-radio',
24
+ options: ['text', 'image', 'card'],
25
+ },
22
26
  },
23
- args: { multiple: false, dragger: false, disabled: false },
27
+ args: { multiple: false, dragger: false, disabled: false, listType: 'text' },
24
28
  };
25
29
 
26
30
  export default meta;
@@ -67,3 +71,65 @@ export const Disabled: Story = {
67
71
  parameters: { controls: { disable: true } },
68
72
  render: () => <Upload dragger disabled />,
69
73
  };
74
+
75
+ // ─── listType · 三种列表形态 ────────────────────────────────────
76
+
77
+ const imageDemoFiles: UploadFileItem[] = [
78
+ {
79
+ uid: 'img-1',
80
+ name: 'avatar.png',
81
+ status: 'done',
82
+ thumbUrl: 'https://github.com/shadcn.png',
83
+ },
84
+ {
85
+ uid: 'img-2',
86
+ name: 'cover.jpg',
87
+ status: 'done',
88
+ thumbUrl: 'https://placehold.co/200x200/e5e7eb/6b7280?text=Cover',
89
+ },
90
+ { uid: 'img-3', name: 'broken.png', status: 'error' },
91
+ ];
92
+
93
+ export const ListTypeImage: Story = {
94
+ name: 'listType="image" · 缩略图列表',
95
+ parameters: { controls: { disable: true } },
96
+ render: () => {
97
+ const [files, setFiles] = React.useState<UploadFileItem[]>(imageDemoFiles);
98
+ return (
99
+ <Upload
100
+ listType="image"
101
+ accept="image/*"
102
+ multiple
103
+ fileList={files}
104
+ onChange={setFiles}
105
+ />
106
+ );
107
+ },
108
+ };
109
+
110
+ export const ListTypeCard: Story = {
111
+ name: 'listType="card" · 网格卡',
112
+ parameters: { controls: { disable: true } },
113
+ render: () => {
114
+ const [files, setFiles] = React.useState<UploadFileItem[]>([
115
+ ...imageDemoFiles,
116
+ {
117
+ uid: 'img-4',
118
+ name: 'uploading.png',
119
+ status: 'uploading',
120
+ percent: 65,
121
+ thumbUrl: 'https://placehold.co/200x200/dbeafe/1d4ed8?text=65%25',
122
+ },
123
+ ]);
124
+ return (
125
+ <Upload
126
+ dragger
127
+ listType="card"
128
+ accept="image/*"
129
+ multiple
130
+ fileList={files}
131
+ onChange={setFiles}
132
+ />
133
+ );
134
+ },
135
+ };
@@ -1,5 +1,10 @@
1
1
  import * as React from 'react';
2
- import { File, Upload as UploadIcon, X } from 'lucide-react';
2
+ import {
3
+ File,
4
+ Image as ImageIcon,
5
+ Upload as UploadIcon,
6
+ X,
7
+ } from 'lucide-react';
3
8
 
4
9
  import { cn } from '@/utils/cn';
5
10
  import { Button } from '@/components/button/button';
@@ -20,6 +25,11 @@ export interface UploadFileItem {
20
25
  percent?: number;
21
26
  /** 上传完成后的远程 URL(可选)。 */
22
27
  url?: string;
28
+ /**
29
+ * 缩略图 URL(可选) — `listType="image"|"card"` 时以此为展示源;
30
+ * 未传时回退为 `url` 或默认文件图标。
31
+ */
32
+ thumbUrl?: string;
23
33
  }
24
34
 
25
35
  export interface UploadProps {
@@ -44,6 +54,19 @@ export interface UploadProps {
44
54
  * 单文件最大字节数(超出则忽略,触发 `onExceed`)。
45
55
  */
46
56
  maxSize?: number;
57
+ /**
58
+ * 文件添加前的校验钩子(antd `beforeUpload` 并集)。
59
+ * - 返回 `false` 拒绝当前文件
60
+ * - 返回 `true` / `undefined` 允许
61
+ * - 返回 新 `File` 对象可替换原文件(重命名 / 压缩等)
62
+ * - 支持异步 `Promise`
63
+ *
64
+ * 注意: 此钩子在 `maxCount` / `maxSize` 校验**之后**调用。
65
+ */
66
+ beforeUpload?: (
67
+ file: File,
68
+ fileList: File[],
69
+ ) => boolean | File | void | Promise<boolean | File | void>;
47
70
  /**
48
71
  * 启用拖拽上传(antd `Dragger` 并集) — 显示大块虚线区,可点击亦可拖拽。
49
72
  * @default false
@@ -56,6 +79,14 @@ export interface UploadProps {
56
79
  * @default true
57
80
  */
58
81
  showFileList?: boolean;
82
+ /**
83
+ * 文件列表的展示形态(antd `listType` 并集) —
84
+ * - `text`(默认):文件名 + 进度条,适用文档上传
85
+ * - `image`:小缩略图 + 文件名,适用头像 / 附件预览
86
+ * - `card`:网格大缩略图卡,适用多图上传 / 产品图库
87
+ * @default "text"
88
+ */
89
+ listType?: 'text' | 'image' | 'card';
59
90
  /** 文件被添加(用户选择 / 拖入)时的回调 — 返回更新后的列表。 */
60
91
  onChange?: (next: UploadFileItem[]) => void;
61
92
  /** 文件被移除时的回调。 */
@@ -82,9 +113,11 @@ const Upload = React.forwardRef<HTMLDivElement, UploadProps>(
82
113
  dragger = false,
83
114
  disabled = false,
84
115
  showFileList = true,
116
+ listType = 'text',
85
117
  onChange,
86
118
  onRemove,
87
119
  onExceed,
120
+ beforeUpload,
88
121
  children,
89
122
  className,
90
123
  },
@@ -102,7 +135,7 @@ const Upload = React.forwardRef<HTMLDivElement, UploadProps>(
102
135
  onChange?.(next);
103
136
  };
104
137
 
105
- const addFiles = (files: File[]) => {
138
+ const addFiles = async (files: File[]) => {
106
139
  if (disabled || files.length === 0) return;
107
140
  let candidate = files;
108
141
 
@@ -121,6 +154,28 @@ const Upload = React.forwardRef<HTMLDivElement, UploadProps>(
121
154
  candidate = candidate.slice(0, remaining);
122
155
  }
123
156
 
157
+ // beforeUpload 钩子 — 逐文件校验
158
+ if (beforeUpload) {
159
+ const accepted: File[] = [];
160
+ for (const f of candidate) {
161
+ try {
162
+ const result = await beforeUpload(f, candidate);
163
+ if (result === false) continue; // 拒绝
164
+ if (result instanceof globalThis.File) {
165
+ accepted.push(result); // 替换文件
166
+ } else {
167
+ accepted.push(f); // 允许原文件
168
+ }
169
+ } catch {
170
+ // Promise reject = 拒绝
171
+ continue;
172
+ }
173
+ }
174
+ candidate = accepted;
175
+ }
176
+
177
+ if (candidate.length === 0) return;
178
+
124
179
  const items: UploadFileItem[] = candidate.map((f) => ({
125
180
  uid: makeUid(),
126
181
  name: f.name,
@@ -175,7 +230,7 @@ const Upload = React.forwardRef<HTMLDivElement, UploadProps>(
175
230
  onDragLeave={() => setDragOver(false)}
176
231
  onDrop={onDrop}
177
232
  className={cn(
178
- 'flex cursor-pointer flex-col items-center justify-center gap-2 rounded-md border border-dashed bg-muted/30 px-6 py-10 text-center text-sm text-muted-foreground transition-colors',
233
+ 'flex cursor-pointer flex-col items-center justify-center gap-2 rounded-md border border-border border-dashed bg-muted/30 px-6 py-10 text-center text-xs text-muted-foreground transition-colors',
179
234
  'hover:border-primary/50 hover:bg-muted/50',
180
235
  'focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring',
181
236
  dragOver && 'border-primary bg-primary/5',
@@ -185,7 +240,9 @@ const Upload = React.forwardRef<HTMLDivElement, UploadProps>(
185
240
  {children ?? (
186
241
  <>
187
242
  <UploadIcon className="size-8 text-primary/70" />
188
- <div className="font-medium text-foreground">点击或拖拽文件到此处上传</div>
243
+ <div className="font-medium text-foreground">
244
+ 点击或拖拽文件到此处上传
245
+ </div>
189
246
  <div className="text-xs">
190
247
  {multiple ? '可选择多个文件' : '仅支持单个文件'}
191
248
  {accept ? ` · 限 ${accept}` : null}
@@ -218,43 +275,119 @@ const Upload = React.forwardRef<HTMLDivElement, UploadProps>(
218
275
  />
219
276
  {trigger}
220
277
  {showFileList && current.length > 0 ? (
221
- <ul className="flex flex-col gap-2">
222
- {current.map((f) => (
223
- <li
224
- key={f.uid}
225
- className={cn(
226
- 'flex items-center gap-3 rounded-md border bg-card px-3 py-2 text-sm',
227
- f.status === 'error' && 'border-destructive/40',
228
- )}
229
- >
230
- <File className="size-4 shrink-0 text-muted-foreground" />
231
- <div className="flex min-w-0 flex-1 flex-col gap-1">
232
- <span
278
+ listType === 'card' ? (
279
+ <ul
280
+ className="grid grid-cols-[repeat(auto-fill,minmax(7rem,1fr))] gap-3"
281
+ role="list"
282
+ >
283
+ {current.map((f) => {
284
+ const thumb = f.thumbUrl ?? f.url;
285
+ return (
286
+ <li
287
+ key={f.uid}
233
288
  className={cn(
234
- 'truncate',
235
- f.status === 'error' && 'text-destructive',
289
+ 'group/uc relative flex aspect-square flex-col items-center justify-center overflow-hidden rounded-md border border-border bg-card text-center text-xs',
290
+ f.status === 'error' && 'border-destructive/40',
236
291
  )}
237
- title={f.name}
238
292
  >
239
- {f.name}
240
- </span>
241
- {f.status === 'uploading' ? (
242
- <Progress value={f.percent ?? 0} size="sm" />
243
- ) : null}
244
- </div>
245
- {!disabled ? (
246
- <button
247
- type="button"
248
- aria-label={`移除 ${f.name}`}
249
- onClick={() => remove(f.uid)}
250
- className="rounded-sm p-1 text-muted-foreground transition-colors hover:bg-accent hover:text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
293
+ {thumb ? (
294
+ <img
295
+ src={thumb}
296
+ alt={f.name}
297
+ className="size-full object-cover"
298
+ />
299
+ ) : (
300
+ <div className="flex flex-col items-center gap-1 px-2 text-muted-foreground">
301
+ <ImageIcon className="size-6" />
302
+ <span
303
+ className={cn(
304
+ 'line-clamp-2 break-all',
305
+ f.status === 'error' && 'text-destructive',
306
+ )}
307
+ title={f.name}
308
+ >
309
+ {f.name}
310
+ </span>
311
+ </div>
312
+ )}
313
+ {f.status === 'uploading' ? (
314
+ <div className="absolute inset-x-0 bottom-0 bg-background/85 px-2 py-1">
315
+ <Progress value={f.percent ?? 0} size="sm" />
316
+ </div>
317
+ ) : null}
318
+ {!disabled ? (
319
+ <button
320
+ type="button"
321
+ aria-label={`移除 ${f.name}`}
322
+ onClick={() => remove(f.uid)}
323
+ className="absolute right-1 top-1 cursor-pointer rounded-sm bg-background/80 p-1 text-muted-foreground opacity-0 transition-opacity hover:text-foreground focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring group-hover/uc:opacity-100"
324
+ >
325
+ <X className="size-3.5" />
326
+ </button>
327
+ ) : null}
328
+ </li>
329
+ );
330
+ })}
331
+ </ul>
332
+ ) : (
333
+ <ul className="flex flex-col gap-2">
334
+ {current.map((f) => {
335
+ const isImage = listType === 'image';
336
+ const thumb = f.thumbUrl ?? f.url;
337
+ return (
338
+ <li
339
+ key={f.uid}
340
+ className={cn(
341
+ 'flex items-center gap-3 rounded-md border border-border bg-card px-3 py-2 text-xs',
342
+ f.status === 'error' && 'border-destructive/40',
343
+ )}
251
344
  >
252
- <X className="size-4" />
253
- </button>
254
- ) : null}
255
- </li>
256
- ))}
257
- </ul>
345
+ {isImage ? (
346
+ <div className="size-10 shrink-0 overflow-hidden rounded-sm border border-border bg-muted">
347
+ {thumb ? (
348
+ <img
349
+ src={thumb}
350
+ alt={f.name}
351
+ className="size-full object-cover"
352
+ />
353
+ ) : (
354
+ <div className="flex size-full items-center justify-center text-muted-foreground">
355
+ <ImageIcon className="size-4" />
356
+ </div>
357
+ )}
358
+ </div>
359
+ ) : (
360
+ <File className="size-4 shrink-0 text-muted-foreground" />
361
+ )}
362
+ <div className="flex min-w-0 flex-1 flex-col gap-1">
363
+ <span
364
+ className={cn(
365
+ 'truncate',
366
+ f.status === 'error' && 'text-destructive',
367
+ )}
368
+ title={f.name}
369
+ >
370
+ {f.name}
371
+ </span>
372
+ {f.status === 'uploading' ? (
373
+ <Progress value={f.percent ?? 0} size="sm" />
374
+ ) : null}
375
+ </div>
376
+ {!disabled ? (
377
+ <button
378
+ type="button"
379
+ aria-label={`移除 ${f.name}`}
380
+ onClick={() => remove(f.uid)}
381
+ className="cursor-pointer rounded-sm p-1 text-muted-foreground transition-colors hover:bg-accent hover:text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
382
+ >
383
+ <X className="size-4" />
384
+ </button>
385
+ ) : null}
386
+ </li>
387
+ );
388
+ })}
389
+ </ul>
390
+ )
258
391
  ) : null}
259
392
  </div>
260
393
  );