@teamix-evo/ui 0.1.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (295) hide show
  1. package/README.md +184 -184
  2. package/manifest.json +680 -492
  3. package/package.json +20 -10
  4. package/src/components/accordion/accordion.meta.md +5 -4
  5. package/src/components/accordion/accordion.stories.tsx +14 -9
  6. package/src/components/accordion/accordion.tsx +104 -8
  7. package/src/components/affix/affix.meta.md +20 -2
  8. package/src/components/affix/affix.stories.tsx +102 -25
  9. package/src/components/affix/affix.tsx +79 -9
  10. package/src/components/alert/alert.meta.md +44 -13
  11. package/src/components/alert/alert.stories.tsx +66 -21
  12. package/src/components/alert/alert.tsx +81 -34
  13. package/src/components/alert-dialog/alert-dialog.meta.md +61 -16
  14. package/src/components/alert-dialog/alert-dialog.stories.tsx +145 -3
  15. package/src/components/alert-dialog/alert-dialog.tsx +60 -13
  16. package/src/components/anchor/anchor.meta.md +8 -3
  17. package/src/components/anchor/anchor.stories.tsx +3 -3
  18. package/src/components/anchor/anchor.tsx +2 -2
  19. package/src/components/app/app.meta.md +9 -4
  20. package/src/components/app/app.stories.tsx +9 -7
  21. package/src/components/aspect-ratio/aspect-ratio.meta.md +4 -3
  22. package/src/components/aspect-ratio/aspect-ratio.stories.tsx +3 -3
  23. package/src/components/auto-complete/auto-complete.meta.md +14 -6
  24. package/src/components/auto-complete/auto-complete.stories.tsx +47 -4
  25. package/src/components/auto-complete/auto-complete.tsx +119 -71
  26. package/src/components/avatar/avatar.meta.md +6 -7
  27. package/src/components/avatar/avatar.stories.tsx +21 -3
  28. package/src/components/avatar/avatar.tsx +24 -23
  29. package/src/components/badge/badge.meta.md +10 -9
  30. package/src/components/badge/badge.stories.tsx +2 -2
  31. package/src/components/badge/badge.tsx +9 -15
  32. package/src/components/breadcrumb/breadcrumb.meta.md +27 -7
  33. package/src/components/breadcrumb/breadcrumb.stories.tsx +127 -4
  34. package/src/components/breadcrumb/breadcrumb.tsx +22 -8
  35. package/src/components/button/button.meta.md +258 -21
  36. package/src/components/button/button.stories.tsx +549 -41
  37. package/src/components/button/button.tsx +335 -33
  38. package/src/components/button/demo/as-child.tsx +24 -0
  39. package/src/components/button/demo/basic.tsx +8 -0
  40. package/src/components/button/demo/block.tsx +16 -0
  41. package/src/components/button/demo/loading.tsx +19 -0
  42. package/src/components/button/demo/shapes.tsx +18 -0
  43. package/src/components/button/demo/sizes.tsx +19 -0
  44. package/src/components/button/demo/variants.tsx +19 -0
  45. package/src/components/button/demo/with-icon.tsx +20 -0
  46. package/src/components/calendar/calendar.meta.md +13 -3
  47. package/src/components/calendar/calendar.stories.tsx +6 -6
  48. package/src/components/calendar/calendar.tsx +73 -8
  49. package/src/components/card/card.meta.md +27 -5
  50. package/src/components/card/card.stories.tsx +42 -3
  51. package/src/components/card/card.tsx +146 -63
  52. package/src/components/carousel/carousel.meta.md +4 -3
  53. package/src/components/carousel/carousel.stories.tsx +11 -6
  54. package/src/components/cascader/cascader.meta.md +47 -17
  55. package/src/components/cascader/cascader.stories.tsx +22 -10
  56. package/src/components/cascader/cascader.tsx +428 -85
  57. package/src/components/checkbox/checkbox.meta.md +75 -7
  58. package/src/components/checkbox/checkbox.stories.tsx +161 -3
  59. package/src/components/checkbox/checkbox.tsx +77 -9
  60. package/src/components/collapsible/collapsible.meta.md +14 -6
  61. package/src/components/collapsible/collapsible.stories.tsx +10 -2
  62. package/src/components/collapsible/collapsible.tsx +93 -6
  63. package/src/components/color-picker/color-picker.meta.md +12 -7
  64. package/src/components/color-picker/color-picker.stories.tsx +86 -7
  65. package/src/components/color-picker/color-picker.tsx +20 -9
  66. package/src/components/command/command.meta.md +29 -13
  67. package/src/components/command/command.stories.tsx +4 -4
  68. package/src/components/command/command.tsx +19 -8
  69. package/src/components/context-menu/context-menu.meta.md +11 -8
  70. package/src/components/context-menu/context-menu.stories.tsx +11 -3
  71. package/src/components/context-menu/context-menu.tsx +21 -8
  72. package/src/components/data-table/data-table.meta.md +6 -5
  73. package/src/components/data-table/data-table.stories.tsx +13 -6
  74. package/src/components/data-table/data-table.tsx +2 -2
  75. package/src/components/date-picker/date-picker.meta.md +88 -19
  76. package/src/components/date-picker/date-picker.stories.tsx +55 -5
  77. package/src/components/date-picker/date-picker.tsx +1489 -91
  78. package/src/components/descriptions/descriptions.meta.md +10 -5
  79. package/src/components/descriptions/descriptions.stories.tsx +3 -3
  80. package/src/components/descriptions/descriptions.tsx +22 -14
  81. package/src/components/dialog/dialog.meta.md +76 -13
  82. package/src/components/dialog/dialog.stories.tsx +182 -20
  83. package/src/components/dialog/dialog.tsx +67 -15
  84. package/src/components/dialog/imperative.tsx +252 -0
  85. package/src/components/drawer/drawer.meta.md +33 -34
  86. package/src/components/drawer/drawer.stories.tsx +29 -12
  87. package/src/components/drawer/drawer.tsx +22 -113
  88. package/src/components/dropdown-menu/dropdown-menu.meta.md +78 -10
  89. package/src/components/dropdown-menu/dropdown-menu.stories.tsx +88 -2
  90. package/src/components/dropdown-menu/dropdown-menu.tsx +24 -10
  91. package/src/components/ellipsis/ellipsis.meta.md +87 -0
  92. package/src/components/ellipsis/ellipsis.stories.tsx +72 -0
  93. package/src/components/ellipsis/ellipsis.tsx +153 -0
  94. package/src/components/empty/empty.meta.md +9 -4
  95. package/src/components/empty/empty.stories.tsx +4 -4
  96. package/src/components/empty/empty.tsx +10 -3
  97. package/src/components/field/field.meta.md +47 -9
  98. package/src/components/field/field.stories.tsx +385 -5
  99. package/src/components/field/field.tsx +263 -35
  100. package/src/components/filter-bar/filter-bar.meta.md +92 -0
  101. package/src/components/filter-bar/filter-bar.stories.tsx +1083 -0
  102. package/src/components/filter-bar/filter-bar.tsx +568 -0
  103. package/src/components/flex/flex.meta.md +54 -6
  104. package/src/components/flex/flex.stories.tsx +107 -20
  105. package/src/components/flex/flex.tsx +27 -4
  106. package/src/components/float-button/float-button.meta.md +8 -3
  107. package/src/components/float-button/float-button.stories.tsx +9 -7
  108. package/src/components/float-button/float-button.tsx +1 -1
  109. package/src/components/form/form.meta.md +39 -17
  110. package/src/components/form/form.stories.tsx +350 -3
  111. package/src/components/form/form.tsx +101 -35
  112. package/src/components/grid/grid.meta.md +7 -2
  113. package/src/components/grid/grid.stories.tsx +6 -4
  114. package/src/components/hover-card/hover-card.meta.md +20 -9
  115. package/src/components/hover-card/hover-card.stories.tsx +34 -5
  116. package/src/components/hover-card/hover-card.tsx +51 -13
  117. package/src/components/icon/DEVELOPMENT.md +809 -0
  118. package/src/components/icon/icon.meta.md +170 -0
  119. package/src/components/icon/icon.stories.tsx +344 -0
  120. package/src/components/icon/icon.tsx +248 -0
  121. package/src/components/image/image.meta.md +9 -4
  122. package/src/components/image/image.stories.tsx +3 -3
  123. package/src/components/image/image.tsx +6 -4
  124. package/src/components/input/demo/basic.tsx +12 -0
  125. package/src/components/input/demo/clearable.tsx +21 -0
  126. package/src/components/input/demo/show-count.tsx +18 -0
  127. package/src/components/input/demo/sizes.tsx +15 -0
  128. package/src/components/input/input.meta.md +39 -33
  129. package/src/components/input/input.stories.tsx +62 -35
  130. package/src/components/input/input.tsx +97 -98
  131. package/src/components/input-group/input-group.meta.md +54 -22
  132. package/src/components/input-group/input-group.stories.tsx +49 -16
  133. package/src/components/input-group/input-group.tsx +44 -8
  134. package/src/components/input-number/input-number.meta.md +64 -7
  135. package/src/components/input-number/input-number.stories.tsx +46 -8
  136. package/src/components/input-number/input-number.tsx +99 -26
  137. package/src/components/input-otp/input-otp.meta.md +4 -3
  138. package/src/components/input-otp/input-otp.stories.tsx +3 -3
  139. package/src/components/input-otp/input-otp.tsx +1 -1
  140. package/src/components/item/item.meta.md +8 -3
  141. package/src/components/item/item.stories.tsx +8 -5
  142. package/src/components/item/item.tsx +7 -6
  143. package/src/components/kbd/kbd.meta.md +13 -4
  144. package/src/components/kbd/kbd.stories.tsx +4 -4
  145. package/src/components/kbd/kbd.tsx +10 -5
  146. package/src/components/label/label.meta.md +18 -10
  147. package/src/components/label/label.stories.tsx +64 -6
  148. package/src/components/label/label.tsx +91 -19
  149. package/src/components/masonry/masonry.meta.md +8 -3
  150. package/src/components/masonry/masonry.stories.tsx +7 -5
  151. package/src/components/masonry/masonry.tsx +1 -0
  152. package/src/components/mentions/mentions.meta.md +36 -6
  153. package/src/components/mentions/mentions.stories.tsx +120 -6
  154. package/src/components/mentions/mentions.tsx +11 -5
  155. package/src/components/menubar/menubar.meta.md +30 -12
  156. package/src/components/menubar/menubar.stories.tsx +62 -2
  157. package/src/components/menubar/menubar.tsx +9 -9
  158. package/src/components/native-select/native-select.meta.md +8 -3
  159. package/src/components/native-select/native-select.stories.tsx +8 -5
  160. package/src/components/native-select/native-select.tsx +1 -1
  161. package/src/components/navigation-menu/navigation-menu.meta.md +19 -9
  162. package/src/components/navigation-menu/navigation-menu.stories.tsx +112 -9
  163. package/src/components/navigation-menu/navigation-menu.tsx +8 -4
  164. package/src/components/notification/notification.meta.md +52 -10
  165. package/src/components/notification/notification.stories.tsx +11 -9
  166. package/src/components/notification/notification.tsx +36 -21
  167. package/src/components/page-header/DEVELOPMENT.md +842 -0
  168. package/src/components/page-header/page-header.meta.md +208 -0
  169. package/src/components/page-header/page-header.stories.tsx +421 -0
  170. package/src/components/page-header/page-header.tsx +281 -0
  171. package/src/components/pagination/pagination.meta.md +140 -37
  172. package/src/components/pagination/pagination.stories.tsx +232 -10
  173. package/src/components/pagination/pagination.tsx +355 -63
  174. package/src/components/popconfirm/popconfirm.meta.md +9 -4
  175. package/src/components/popconfirm/popconfirm.stories.tsx +3 -4
  176. package/src/components/popconfirm/popconfirm.tsx +2 -2
  177. package/src/components/popover/popover.meta.md +62 -5
  178. package/src/components/popover/popover.stories.tsx +83 -7
  179. package/src/components/popover/popover.tsx +77 -28
  180. package/src/components/progress/progress.meta.md +38 -6
  181. package/src/components/progress/progress.stories.tsx +3 -3
  182. package/src/components/progress/progress.tsx +24 -16
  183. package/src/components/radio-group/radio-group.meta.md +79 -7
  184. package/src/components/radio-group/radio-group.stories.tsx +39 -3
  185. package/src/components/radio-group/radio-group.tsx +149 -18
  186. package/src/components/rate/rate.meta.md +35 -4
  187. package/src/components/rate/rate.stories.tsx +13 -5
  188. package/src/components/rate/rate.tsx +37 -10
  189. package/src/components/resizable/resizable.meta.md +7 -4
  190. package/src/components/resizable/resizable.stories.tsx +6 -6
  191. package/src/components/resizable/resizable.tsx +1 -1
  192. package/src/components/result/result.meta.md +7 -2
  193. package/src/components/result/result.stories.tsx +4 -8
  194. package/src/components/result/result.tsx +24 -15
  195. package/src/components/scroll-area/scroll-area.meta.md +4 -3
  196. package/src/components/scroll-area/scroll-area.stories.tsx +12 -4
  197. package/src/components/scroll-area/scroll-area.tsx +3 -3
  198. package/src/components/segmented/segmented.meta.md +7 -4
  199. package/src/components/segmented/segmented.stories.tsx +37 -8
  200. package/src/components/segmented/segmented.tsx +15 -7
  201. package/src/components/select/select.meta.md +197 -52
  202. package/src/components/select/select.stories.tsx +238 -63
  203. package/src/components/select/select.tsx +718 -171
  204. package/src/components/separator/separator.meta.md +4 -3
  205. package/src/components/separator/separator.stories.tsx +3 -3
  206. package/src/components/separator/separator.tsx +3 -7
  207. package/src/components/sheet/sheet.meta.md +32 -16
  208. package/src/components/sheet/sheet.stories.tsx +116 -10
  209. package/src/components/sheet/sheet.tsx +116 -29
  210. package/src/components/sidebar/sidebar.meta.md +37 -18
  211. package/src/components/sidebar/sidebar.stories.tsx +701 -29
  212. package/src/components/sidebar/sidebar.tsx +615 -142
  213. package/src/components/skeleton/skeleton.meta.md +4 -5
  214. package/src/components/skeleton/skeleton.stories.tsx +4 -4
  215. package/src/components/skeleton/skeleton.tsx +7 -7
  216. package/src/components/slider/slider.meta.md +57 -5
  217. package/src/components/slider/slider.stories.tsx +58 -6
  218. package/src/components/slider/slider.tsx +154 -13
  219. package/src/components/sonner/sonner.meta.md +58 -7
  220. package/src/components/sonner/sonner.stories.tsx +78 -5
  221. package/src/components/sonner/sonner.tsx +137 -8
  222. package/src/components/spinner/spinner.meta.md +62 -13
  223. package/src/components/spinner/spinner.stories.tsx +66 -14
  224. package/src/components/spinner/spinner.tsx +111 -9
  225. package/src/components/statistic/statistic.meta.md +7 -2
  226. package/src/components/statistic/statistic.stories.tsx +3 -7
  227. package/src/components/statistic/statistic.tsx +5 -6
  228. package/src/components/steps/steps.meta.md +18 -4
  229. package/src/components/steps/steps.stories.tsx +43 -3
  230. package/src/components/steps/steps.tsx +15 -12
  231. package/src/components/switch/switch.meta.md +51 -5
  232. package/src/components/switch/switch.stories.tsx +6 -6
  233. package/src/components/switch/switch.tsx +109 -41
  234. package/src/components/table/table.meta.md +17 -6
  235. package/src/components/table/table.stories.tsx +10 -5
  236. package/src/components/table/table.tsx +4 -4
  237. package/src/components/tabs/tabs.meta.md +38 -25
  238. package/src/components/tabs/tabs.stories.tsx +111 -25
  239. package/src/components/tabs/tabs.tsx +125 -54
  240. package/src/components/tag/tag.meta.md +105 -40
  241. package/src/components/tag/tag.stories.tsx +189 -16
  242. package/src/components/tag/tag.tsx +222 -21
  243. package/src/components/textarea/textarea.meta.md +35 -19
  244. package/src/components/textarea/textarea.stories.tsx +32 -6
  245. package/src/components/textarea/textarea.tsx +33 -9
  246. package/src/components/time-picker/time-picker.meta.md +124 -32
  247. package/src/components/time-picker/time-picker.stories.tsx +85 -15
  248. package/src/components/time-picker/time-picker.tsx +913 -61
  249. package/src/components/timeline/timeline.meta.md +14 -6
  250. package/src/components/timeline/timeline.stories.tsx +37 -7
  251. package/src/components/timeline/timeline.tsx +35 -14
  252. package/src/components/toggle/toggle.meta.md +5 -4
  253. package/src/components/toggle/toggle.stories.tsx +4 -4
  254. package/src/components/toggle/toggle.tsx +4 -3
  255. package/src/components/toggle-group/toggle-group.meta.md +5 -4
  256. package/src/components/toggle-group/toggle-group.stories.tsx +3 -3
  257. package/src/components/toggle-group/toggle-group.tsx +2 -2
  258. package/src/components/tooltip/tooltip.meta.md +55 -5
  259. package/src/components/tooltip/tooltip.stories.tsx +42 -5
  260. package/src/components/tooltip/tooltip.tsx +81 -21
  261. package/src/components/tour/tour.meta.md +9 -4
  262. package/src/components/tour/tour.stories.tsx +3 -3
  263. package/src/components/tour/tour.tsx +4 -4
  264. package/src/components/transfer/transfer.meta.md +11 -6
  265. package/src/components/transfer/transfer.stories.tsx +4 -8
  266. package/src/components/transfer/transfer.tsx +28 -21
  267. package/src/components/tree/tree.meta.md +63 -5
  268. package/src/components/tree/tree.stories.tsx +31 -12
  269. package/src/components/tree/tree.tsx +9 -8
  270. package/src/components/tree-select/tree-select.meta.md +59 -8
  271. package/src/components/tree-select/tree-select.stories.tsx +3 -3
  272. package/src/components/tree-select/tree-select.tsx +42 -7
  273. package/src/components/typography/typography.meta.md +61 -14
  274. package/src/components/typography/typography.stories.tsx +12 -11
  275. package/src/components/typography/typography.tsx +43 -28
  276. package/src/components/upload/upload.meta.md +49 -4
  277. package/src/components/upload/upload.stories.tsx +72 -12
  278. package/src/components/upload/upload.tsx +170 -37
  279. package/src/components/watermark/watermark.meta.md +7 -2
  280. package/src/components/watermark/watermark.stories.tsx +101 -9
  281. package/src/components/watermark/watermark.tsx +1 -0
  282. package/src/hooks/use-breakpoint.ts +117 -0
  283. package/src/hooks/use-debounce-callback.ts +52 -0
  284. package/src/hooks/use-mobile.ts +23 -0
  285. package/src/stories/theme-tokens.stories.tsx +747 -0
  286. package/src/utils/trigger-input.ts +53 -0
  287. package/src/components/button-group/button-group.meta.md +0 -92
  288. package/src/components/button-group/button-group.stories.tsx +0 -90
  289. package/src/components/button-group/button-group.tsx +0 -75
  290. package/src/components/combobox/combobox.meta.md +0 -93
  291. package/src/components/combobox/combobox.stories.tsx +0 -55
  292. package/src/components/combobox/combobox.tsx +0 -130
  293. package/src/components/space/space.meta.md +0 -94
  294. package/src/components/space/space.stories.tsx +0 -94
  295. package/src/components/space/space.tsx +0 -106
@@ -0,0 +1,809 @@
1
+ # Icon 组件研发文档
2
+
3
+ > ⚠️ **当前 API 状态**:`children` / `variant` / **`color`** / `size` / `spin` / `rotate` / `asChild` / `decorative`。
4
+ > `tone` 该 prop 名在 v0.1 结束后二次回退为 `color`(详见**附录 C**、[ADR 0021](../../../../../docs/adr/0021-semantic-color-api-unification.md))。本文档为演进档案 —— 实现以 `icon.tsx` / `icon.stories.tsx` / `icon.meta.md` 为准。
5
+
6
+ > 本文档为 `@teamix-evo/ui` Icon 组件的完整研发分析,基于 cloud-design 中 `Icon` 的能力承接(`@alifd/next` Icon + `createFromIconfontCN`),按照 ui 库第零条准则(能力做并集,理念跟 design,工程跟 shadcn)重新设计实现。
7
+
8
+ > ⚠️ **本组件存在的两个核心理由**:① **背景色容器** — 圆形/方形 + 彩色背景的 icon 容器,lucide 直用每处都得自拼 div+bg+radius+居中,极易视觉漂移;② **多 icon 源统一出入口** — 默认 `lucide-react`,预留 iconfont 与未来其他 icon set 的工厂接入点,API 一次稳定,源可换。**仅 size + color 不必包**(shadcn 工程理念就是 lucide + className 直用),不要把本组件当作 lucide 的强制中转。
9
+
10
+ ---
11
+
12
+ ## 一、源头分析 — Icon(cloud-design)
13
+
14
+ ### 1.1 原始组件定位
15
+
16
+ cloud-design 的 Icon 直接转出 `@alifd/next` 的 Icon,在阿里云控制台生态承担两类职责:
17
+
18
+ | 职责 | 内容 | 原始实现位置 |
19
+ | ---------- | -------------------------------------------------------------------------------------- | --------------------------------------- |
20
+ | 内置图标 | 56 个语义化图标(success / warning / error / loading / smile 等) + xconsole 扩展集 | `@alifd/next` Icon 内置 type 字典 |
21
+ | 自定义源 | iconfont CN 脚本注入,通过 `createFromIconfontCN({ scriptUrl })` 拿到一个新的 Icon 组件 | `@alifd/next` Icon.createFromIconfontCN |
22
+ | 尺寸语义化 | 9 档尺寸 + inherit + number(自由像素值) | size prop |
23
+ | 自定义样式 | style.color / className 自由透传(图标本质是字体) | style/className 透传 |
24
+ | 无障碍 | 文档约束:装饰性走 `aria-hidden`,按钮型走 `role="button" + aria-label` | 文档约束(无运行时强制) |
25
+
26
+ ### 1.2 原始 Props 清单
27
+
28
+ ```typescript
29
+ type CloudDesignIconProps = {
30
+ type: string; // 必填:内置图标名或 createFromIconfontCN 返回组件后的扩展 type
31
+ size?:
32
+ | 'xxs' | 'xs' | 'small' | 'medium' | 'large'
33
+ | 'xl' | 'xxl' | 'xxxl' | 'inherit' | number;
34
+ // 其它 HTML 属性透传(style / className / role / aria-* ...)
35
+ };
36
+
37
+ // 工厂方法
38
+ Icon.createFromIconfontCN({ scriptUrl: string }): React.ComponentType<{ type: string; size?: ... }>;
39
+ ```
40
+
41
+ ### 1.3 原始依赖与生态
42
+
43
+ | 依赖 | 来源 | 作用 |
44
+ | -------------------- | ----------------- | ----------------------------------- |
45
+ | `@alifd/next` Icon | next 内置 | 渲染层 + 内置 svg 字典 |
46
+ | iconfont.cn / 主题包 | 远程 / npm 主题包 | 扩展 type 字典(svg symbol 注入 DOM) |
47
+ | Fusion 主题包变量 | 主题 npm | 控制 type 与 svg 之间的映射 |
48
+
49
+ > cloud-design 的 Icon 与 Fusion 主题包深度耦合 —— 每个主题包都内嵌一份 iconfont 地址,变换主题即变换内置 type 字典,这与 teamix-evo "源码注入 + 静态 lucide" 的理念**根本不同**。
50
+
51
+ ---
52
+
53
+ ## 二、第零条准则对照表(三层对齐)
54
+
55
+ ### 2.1 能力对标 — shadcn ∪ antd ∪ cloud-design 并集
56
+
57
+ > shadcn-ui 没有 Icon wrapper(直接 `import { Smile } from 'lucide-react'` 用),`@ant-design/icons` 提供 wrapper(`<SmileOutlined />` + `<Icon component={Svg} />`),cloud-design 提供 Icon + iconfont 工厂。本组件落点:**承接 cloud-design 的"icon 容器 + 多源工厂"理念,采用 antd 的 `spin` / `rotate` 类语义化 prop,工程对齐 shadcn(lucide 单源 + cva + Slot)**。
58
+
59
+ | 能力维度 | shadcn 提供 | antd 提供(@ant-design/icons) | cloud-design 提供 | 本组件实现 |
60
+ | -------------------- | ------------------------------------------------------ | ------------------------------------------------ | ------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- |
61
+ | icon source | 直接 `import { Smile } from 'lucide-react'`,无 wrapper | `<SmileOutlined />` + `<Icon component={Svg} />` | iconfont type + `createFromIconfontCN` | **多源工厂**:默认 lucide(`icon` prop 接 `LucideIcon`)+ `createIconfontIcon({ scriptUrl })` 工厂(v0.2) |
62
+ | **背景色容器**(核心) | ❌ | ❌ | ✅ `iconBackgroundType` + `iconBackgroundColor` | ✅ `withBackground` + `shape: circle/square` + `bgTone: neutral/primary/destructive/success/warning` |
63
+ | size 档位 | className `size-4 / size-5 / size-6` 自定 | `fontSize` 数值 | `xxs / xs / small / medium / large / xl / xxl / xxxl / inherit / number` | 5 档枚举 `xs / sm / md / lg / xl` + `inherit`(对齐 cloud-design 5 档主流值,放弃过度细分的 xxs/xxxl) |
64
+ | 语义配色 | 无统一 wrapper(自由 className) | 无 | `style.color` 自由 | `tone: default / muted / primary / destructive / success / warning`(走 `@teamix-evo/tokens` 语义色) |
65
+ | spin | ❌(自己写 `animate-spin`) | ✅ `spin` | ❌(loading 是单独 type) | ✅ `spin` |
66
+ | rotate | ❌(自己写 `style.transform`) | ✅ `rotate={number}` | ❌ | ✅ `rotate` |
67
+ | asChild (Slot) | ✅ | ❌ | ❌ | ✅ |
68
+ | 无障碍 | 自己写 `aria-hidden` | 自己写 | 文档约束(运行时不强制) | `decorative` 默认 true 自动 `aria-hidden`;非装饰必须 `aria-label` |
69
+ | 自定义 SVG | 自由 import + className | `<Icon component={Svg} />` | createFromIconfontCN | 透传 children(任意 ReactNode 都走 children 通道) |
70
+ | iconfont 扩展 | ❌ | ❌ | ✅ | **v0.2 接入,API 已稳定**(`createIconfontIcon({ scriptUrl })`) |
71
+
72
+ 简记:**核心是"带背景的多源 icon 容器";size/tone/spin/rotate 顺带统一;命名/工程对齐 shadcn;首发源只锁 lucide-react,iconfont 工厂第二批落地,但 API 在 v0.1 就稳定**。
73
+
74
+ ### 2.2 设计理念 — 跟 `@teamix-evo/tokens` OpenTrek 体系
75
+
76
+ - **配色**走 semantic tokens:
77
+ - inner tone:`text-current` / `text-muted-foreground` / `text-primary` / `text-destructive` / `text-success` / `text-warning`
78
+ - container bgTone:`bg-muted text-foreground` / `bg-primary/10 text-primary` / `bg-destructive/10 text-destructive` / `bg-success/10 text-success` / `bg-warning/10 text-warning`
79
+ - **不照搬** cloud-design 的 `iconColor: 'blue' | 'green' | 'orange' | 'red' | 'yellow'` 原色枚举,改走 OpenTrek 语义色
80
+ - **尺寸**走 4px 倍数 Tailwind size 体系:
81
+ - inner:`size-3 / size-3.5 / size-4 / size-5 / size-6` 即 12 / 14 / 16 / 20 / 24px
82
+ - container:`size-5 / size-6 / size-8 / size-10 / size-12` 即 20 / 24 / 32 / 40 / 48px(inner × 1.67~2.0)
83
+ - **圆角**走五档:`rounded-full`(circle) / `rounded-md`(square,对齐 OpenTrek 卡片圆角)
84
+ - **动效**:spin 走 `animate-spin`(Tailwind 1s 线性循环,与 spinner 组件一致),rotate 走 inline `transform`
85
+ - **不引入运行时主题机制**,不参照 antd Icon 的 fontSize 数值制,也不参照 cloud-design 主题包的 iconfont 映射
86
+
87
+ ### 2.3 代码实现 — 跟 shadcn
88
+
89
+ - **文件结构**:`src/components/icon/icon.tsx` + `.meta.md` + `.stories.tsx`
90
+ - **cva 范式**:inner 与 container 各一份 cva(职责分离,不在一份 cva 里塞 9 个 variant)
91
+ - **forwardRef**:`Icon` 用 `React.forwardRef<HTMLSpanElement, IconProps>`
92
+ - **假路径 import**:`@/utils/cn`
93
+ - **TypeScript strict**:每个 prop 有 JSDoc + `@default`,供 `pnpm gen:meta` 自动生成 props 表
94
+ - **Tailwind**:全部 utility,**不写一行 less/scss**
95
+ - **Slot**:`asChild=true` 时容器从 `span` 切到 `@radix-ui/react-slot`
96
+ - **icon 单源**:遵守 ESLint `teamix-evo/icon-from-lucide`(本组件就是该规则的官方收口出口),组件内部仅 import lucide-react 类型;消费方传入的也只接受 `LucideIcon` 与任意 ReactNode
97
+
98
+ ---
99
+
100
+ ## 三、组件结构设计
101
+
102
+ ### 3.1 职责拆分(双层结构,而非复合组件)
103
+
104
+ Icon 不像 PageHeader / Card / Dialog 那样需要复合组件 —— 它本质是一个"内层 svg + 可选外层背景容器"的双层结构,**单一组件 + 两个 cva** 即可表达,**不导出 IconContainer / IconInner 等子组件**(避免 API 表面积膨胀)。
105
+
106
+ ```
107
+ Icon
108
+ ├── (withBackground=false,默认)
109
+ │ └── IconCmp (lucide 组件 / children) ← 应用 iconInnerVariants
110
+
111
+ └── (withBackground=true)
112
+ └── span [iconContainerVariants]
113
+ └── IconCmp ← 应用 iconInnerVariants(尺寸自动缩到合适比例)
114
+ ```
115
+
116
+ ### 3.2 双 cva 职责边界
117
+
118
+ | cva | 应用对象 | 控制维度 | 说明 |
119
+ | ----------------------- | ----------------------- | --------------------------- | -------------------------------------------------------------------------------- |
120
+ | `iconInnerVariants` | inner svg / lucide 组件 | `size` / `tone` / `spin` | 不论是否有背景容器都会应用 |
121
+ | `iconContainerVariants` | 外层背景 span | `size` / `shape` / `bgTone` | 仅 `withBackground=true` 时启用;容器 size 与 inner size 一一映射(xs↔xs,..,xl↔xl) |
122
+
123
+ > **不让消费方分别控 inner size / container size** —— 用户只传一个 `size`,内外尺寸联动,符合 P3 Predictability(同样的 prop 给同样的视觉强度)。
124
+
125
+ ---
126
+
127
+ ## 四、Props 接口设计
128
+
129
+ ```typescript
130
+ import type { LucideIcon } from 'lucide-react';
131
+
132
+ export interface IconProps
133
+ extends Omit<React.HTMLAttributes<HTMLSpanElement>, 'children'>,
134
+ VariantProps<typeof iconInnerVariants> {
135
+ /**
136
+ * 直接传入 lucide 组件(推荐写法)。与 children 互斥;同时存在时 children 优先。
137
+ * @example <Icon icon={Smile} />
138
+ */
139
+ icon?: LucideIcon;
140
+
141
+ /**
142
+ * 任意 ReactNode 透传 — 用于自定义 SVG / iconfont 工厂返回的组件 / asChild 场景。
143
+ */
144
+ children?: React.ReactNode;
145
+
146
+ /**
147
+ * 尺寸 — 5 档枚举 + inherit。inherit 走 `1em`,跟随父级字号。
148
+ * @default "md"
149
+ */
150
+ size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'inherit';
151
+
152
+ /**
153
+ * 内层 icon 配色 — 走 OpenTrek 语义色;`default` = currentColor 跟随父级。
154
+ * @default "default"
155
+ */
156
+ tone?:
157
+ | 'default'
158
+ | 'muted'
159
+ | 'primary'
160
+ | 'destructive'
161
+ | 'success'
162
+ | 'warning';
163
+
164
+ /**
165
+ * 旋转动效 — 与 antd Icon `spin` 对齐,Tailwind `animate-spin` 实现。
166
+ * @default false
167
+ */
168
+ spin?: boolean;
169
+
170
+ /**
171
+ * 静态旋转角度(度数)— 与 antd Icon `rotate` 对齐,通过 inline transform 实现。
172
+ * 与 `spin` 同时使用时 `spin` 的动效优先,但 transform 起点会受影响,**不建议同时使用**。
173
+ * @example rotate={90}
174
+ */
175
+ rotate?: number;
176
+
177
+ /**
178
+ * Slot 模式 — 把渲染目标交给消费方提供的子元素(如 `<a>` / `<button>` / 自定义组件)。
179
+ * 与 `icon` / `withBackground` 互斥(开启时这两个 prop 被忽略)。
180
+ * @default false
181
+ */
182
+ asChild?: boolean;
183
+
184
+ /**
185
+ * 装饰性图标(默认 true,自动 `aria-hidden="true"`)。
186
+ * 当图标承载语义信息时设为 false,**必须**同时提供 `aria-label` 或被 `<button aria-label>` 包裹。
187
+ * @default true
188
+ */
189
+ decorative?: boolean;
190
+
191
+ /**
192
+ * 启用背景容器。开启后外层 span 应用 shape + bgTone,inner svg 自动缩到合适比例。
193
+ * @default false
194
+ */
195
+ withBackground?: boolean;
196
+
197
+ /**
198
+ * 容器形状(仅 `withBackground=true` 生效)。
199
+ * @default "circle"
200
+ */
201
+ shape?: 'circle' | 'square';
202
+
203
+ /**
204
+ * 容器背景配色(仅 `withBackground=true` 生效)。
205
+ * @default "neutral"
206
+ */
207
+ bgTone?: 'neutral' | 'primary' | 'destructive' | 'success' | 'warning';
208
+ }
209
+ ```
210
+
211
+ ### Props 设计说明
212
+
213
+ | Prop | 设计取舍 |
214
+ | ---------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
215
+ | `icon` vs `children` | 推荐 `icon`(类型受 `LucideIcon` 约束,IDE 提示更精准);`children` 兜底自定义 SVG / iconfont 工厂 |
216
+ | `size` 档位 | 放弃 cloud-design 的 9 档(xxs..xxxl),收敛到 5 档 + inherit。理由:OpenTrek 实际只用得到 4~5 档,过细的 xxs/xxxl 反而引发"先选什么"的决策成本(P4 Efficiency) |
217
+ | `tone` 默认值 | `default` = `text-current`,跟随父级 — 90% 场景不需要单独设色,跟父容器的 text 色即可 |
218
+ | `pixelSize` | **不提供**。理由:开了像素自由就破坏 token 纪律(P3),消费方真有特殊像素诉求,直接传 `className="size-[Xpx]"` 走 lint warn 路径 |
219
+ | `decorative` 默认 true | 与 cloud-design 文档约束一致(默认装饰性);非装饰场景必须显式声明,降低无障碍漏检概率 |
220
+ | `asChild` 与 `withBackground` 互斥 | 背景容器需要外层 span 渲染,Slot 会替换该 span,语义冲突 — 强制二选一 |
221
+
222
+ ---
223
+
224
+ ## 五、渲染结构设计
225
+
226
+ ### 5.1 三种渲染分支
227
+
228
+ ```tsx
229
+ // 分支 A:withBackground=false,asChild=false(默认)
230
+ // 直接渲染 IconCmp,应用 inner variants + aria + style
231
+ <IconCmp
232
+ className={cn(iconInnerVariants({ size, tone, spin }), className)}
233
+ style={{ ...(rotate ? { transform: `rotate(${rotate}deg)` } : null), ...style }}
234
+ {...ariaProps}
235
+ {...rest}
236
+ />
237
+
238
+ // 分支 B:withBackground=true,asChild=false
239
+ // 外层 span 提供背景容器,inner 走 svg 默认尺寸缩放
240
+ <span
241
+ className={cn(iconContainerVariants({ size, shape, bgTone }), className)}
242
+ {...ariaProps}
243
+ {...rest}
244
+ >
245
+ <IconCmp
246
+ className={cn(iconInnerVariants({ size, tone: bgToneToInnerTone(bgTone), spin }))}
247
+ style={rotate ? { transform: `rotate(${rotate}deg)` } : undefined}
248
+ />
249
+ </span>
250
+
251
+ // 分支 C:asChild=true(忽略 withBackground)
252
+ <Slot
253
+ className={cn(iconInnerVariants({ size, tone, spin }), className)}
254
+ style={{ ...(rotate ? { transform: `rotate(${rotate}deg)` } : null), ...style }}
255
+ {...ariaProps}
256
+ {...rest}
257
+ />
258
+ ```
259
+
260
+ ### 5.2 inner svg 在背景容器内的尺寸映射
261
+
262
+ 容器尺寸固定后,inner svg 应该缩到容器 50%~60% 视觉舒适。建议 `iconInnerVariants` 的 size 在容器内统一 hard-code 为容器 size **降一档**(如容器 `md`=32px → inner 走 `sm`=14px;容器 `lg`=40px → inner 走 `md`=16px)。
263
+
264
+ 实现细节(组件内部小工具):
265
+
266
+ ```typescript
267
+ const innerSizeInContainer: Record<
268
+ NonNullable<IconProps['size']>,
269
+ NonNullable<IconProps['size']>
270
+ > = {
271
+ xs: 'xs',
272
+ sm: 'xs',
273
+ md: 'sm',
274
+ lg: 'md',
275
+ xl: 'lg',
276
+ inherit: 'inherit',
277
+ };
278
+ ```
279
+
280
+ ### 5.3 bgTone 与 inner tone 的语义联动
281
+
282
+ 开启背景容器后,inner tone 强制由 bgTone 推导(`bg-primary/10` 配 `text-primary`,语义已锁定),**忽略消费方传入的 `tone`** —— 避免出现 `bgTone="primary" tone="destructive"` 这种语义打架。
283
+
284
+ ```typescript
285
+ const bgToneToInnerTone: Record<
286
+ NonNullable<IconProps['bgTone']>,
287
+ NonNullable<IconProps['tone']>
288
+ > = {
289
+ neutral: 'default',
290
+ primary: 'primary',
291
+ destructive: 'destructive',
292
+ success: 'success',
293
+ warning: 'warning',
294
+ };
295
+ ```
296
+
297
+ ---
298
+
299
+ ## 六、cva 配置完整草稿
300
+
301
+ ```typescript
302
+ import { cva, type VariantProps } from 'class-variance-authority';
303
+
304
+ // 内层:实际渲染的 svg/icon 组件本身的尺寸 + 着色 + 旋转
305
+ export const iconInnerVariants = cva('inline-block shrink-0', {
306
+ variants: {
307
+ size: {
308
+ xs: 'size-3', // 12px
309
+ sm: 'size-3.5', // 14px
310
+ md: 'size-4', // 16px(默认)
311
+ lg: 'size-5', // 20px
312
+ xl: 'size-6', // 24px
313
+ inherit: 'size-[1em]', // 跟随父级字号
314
+ },
315
+ tone: {
316
+ default: 'text-current',
317
+ muted: 'text-muted-foreground',
318
+ primary: 'text-primary',
319
+ destructive: 'text-destructive',
320
+ success: 'text-success', // tokens 暂缺时 fallback 到 currentColor,见 ADR 0008
321
+ warning: 'text-warning',
322
+ },
323
+ spin: { true: 'animate-spin' },
324
+ },
325
+ defaultVariants: { size: 'md', tone: 'default' },
326
+ });
327
+
328
+ // 外层:背景容器 — 仅 withBackground=true 时启用
329
+ export const iconContainerVariants = cva(
330
+ 'inline-flex items-center justify-center shrink-0',
331
+ {
332
+ variants: {
333
+ shape: {
334
+ circle: 'rounded-full',
335
+ square: 'rounded-md',
336
+ },
337
+ bgTone: {
338
+ neutral: 'bg-muted text-foreground',
339
+ primary: 'bg-primary/10 text-primary',
340
+ destructive: 'bg-destructive/10 text-destructive',
341
+ success: 'bg-success/10 text-success',
342
+ warning: 'bg-warning/10 text-warning',
343
+ },
344
+ size: {
345
+ xs: 'size-5', // 20px
346
+ sm: 'size-6', // 24px
347
+ md: 'size-8', // 32px(默认)
348
+ lg: 'size-10', // 40px
349
+ xl: 'size-12', // 48px
350
+ inherit: 'size-[1.75em]',
351
+ },
352
+ },
353
+ defaultVariants: { shape: 'circle', bgTone: 'neutral', size: 'md' },
354
+ },
355
+ );
356
+
357
+ export type IconInnerVariants = VariantProps<typeof iconInnerVariants>;
358
+ export type IconContainerVariants = VariantProps<typeof iconContainerVariants>;
359
+ ```
360
+
361
+ > `success` / `warning` 的 token(`--color-success` / `--color-warning`)当前在 `@teamix-evo/tokens` OpenTrek variant 内尚未定义 —— **不阻塞本次交付**,运行时未定义时 Tailwind 解析失败 fallback 到 currentColor;由 [ADR 0008](../../../../../docs/adr/0008-eslint-visual-rules-warn-baseline.md) 标注的 token 补齐路径修复。
362
+
363
+ ---
364
+
365
+ ## 七、组件实现草稿
366
+
367
+ ```tsx
368
+ // packages/ui/src/components/icon/icon.tsx
369
+ import * as React from 'react';
370
+ import { Slot } from '@radix-ui/react-slot';
371
+ import type { LucideIcon } from 'lucide-react';
372
+
373
+ import { cn } from '@/utils/cn';
374
+
375
+ import {
376
+ iconInnerVariants,
377
+ iconContainerVariants,
378
+ type IconInnerVariants,
379
+ } from './icon.variants'; // 或直接放在 icon.tsx 顶部
380
+
381
+ const innerSizeInContainer = {
382
+ xs: 'xs',
383
+ sm: 'xs',
384
+ md: 'sm',
385
+ lg: 'md',
386
+ xl: 'lg',
387
+ inherit: 'inherit',
388
+ } as const;
389
+
390
+ const bgToneToInnerTone = {
391
+ neutral: 'default',
392
+ primary: 'primary',
393
+ destructive: 'destructive',
394
+ success: 'success',
395
+ warning: 'warning',
396
+ } as const;
397
+
398
+ export interface IconProps
399
+ extends Omit<React.HTMLAttributes<HTMLSpanElement>, 'children'>,
400
+ IconInnerVariants {
401
+ icon?: LucideIcon;
402
+ children?: React.ReactNode;
403
+ rotate?: number;
404
+ asChild?: boolean;
405
+ decorative?: boolean;
406
+ withBackground?: boolean;
407
+ shape?: 'circle' | 'square';
408
+ bgTone?: 'neutral' | 'primary' | 'destructive' | 'success' | 'warning';
409
+ }
410
+
411
+ export const Icon = React.forwardRef<HTMLSpanElement, IconProps>(
412
+ (
413
+ {
414
+ icon: IconCmp,
415
+ children,
416
+ size = 'md',
417
+ tone = 'default',
418
+ spin,
419
+ rotate,
420
+ asChild,
421
+ decorative = true,
422
+ withBackground,
423
+ shape = 'circle',
424
+ bgTone = 'neutral',
425
+ className,
426
+ style,
427
+ ...rest
428
+ },
429
+ ref,
430
+ ) => {
431
+ const ariaProps: React.AriaAttributes & { role?: string } = decorative
432
+ ? { 'aria-hidden': true }
433
+ : { role: rest.role ?? 'img' };
434
+
435
+ const rotateStyle = rotate
436
+ ? { transform: `rotate(${rotate}deg)` }
437
+ : undefined;
438
+ const mergedStyle = { ...rotateStyle, ...style };
439
+
440
+ // 分支 C:Slot 模式
441
+ if (asChild) {
442
+ return (
443
+ <Slot
444
+ ref={ref}
445
+ className={cn(iconInnerVariants({ size, tone, spin }), className)}
446
+ style={mergedStyle}
447
+ {...ariaProps}
448
+ {...rest}
449
+ >
450
+ {children}
451
+ </Slot>
452
+ );
453
+ }
454
+
455
+ // 分支 B:背景容器
456
+ if (withBackground) {
457
+ const innerSize = innerSizeInContainer[size];
458
+ const innerTone = bgToneToInnerTone[bgTone];
459
+ return (
460
+ <span
461
+ ref={ref}
462
+ className={cn(
463
+ iconContainerVariants({ size, shape, bgTone }),
464
+ className,
465
+ )}
466
+ {...ariaProps}
467
+ {...rest}
468
+ >
469
+ {children ? (
470
+ <span
471
+ className={cn(
472
+ iconInnerVariants({ size: innerSize, tone: innerTone, spin }),
473
+ )}
474
+ style={rotateStyle}
475
+ >
476
+ {children}
477
+ </span>
478
+ ) : IconCmp ? (
479
+ <IconCmp
480
+ className={cn(
481
+ iconInnerVariants({ size: innerSize, tone: innerTone, spin }),
482
+ )}
483
+ style={rotateStyle}
484
+ />
485
+ ) : null}
486
+ </span>
487
+ );
488
+ }
489
+
490
+ // 分支 A:默认(直接渲染 inner)
491
+ if (children) {
492
+ return (
493
+ <span
494
+ ref={ref}
495
+ className={cn(iconInnerVariants({ size, tone, spin }), className)}
496
+ style={mergedStyle}
497
+ {...ariaProps}
498
+ {...rest}
499
+ >
500
+ {children}
501
+ </span>
502
+ );
503
+ }
504
+
505
+ if (IconCmp) {
506
+ return (
507
+ <IconCmp
508
+ ref={ref as never}
509
+ className={cn(iconInnerVariants({ size, tone, spin }), className)}
510
+ style={mergedStyle}
511
+ {...ariaProps}
512
+ {...rest}
513
+ />
514
+ );
515
+ }
516
+
517
+ return null;
518
+ },
519
+ );
520
+ Icon.displayName = 'Icon';
521
+ ```
522
+
523
+ ### 实现要点
524
+
525
+ - **lucide 组件直接接受 `className` / `style` / `ref`**(它们都是 React.forwardRef SVG),所以默认分支可以零包裹直出,避免无谓 span 嵌套
526
+ - **children 走 children,icon 走 icon**:不再支持"两者都传"(同时存在时 children 优先,但开发时建议 lint 报警)
527
+ - **withBackground + children** 也支持(自定义 SVG 也能套背景容器)
528
+ - **asChild + Slot**:`@radix-ui/react-slot` 会把所有 className/style/事件 merge 到子元素根节点,适合给 `<a><Icon icon={...}/></a>` 这类容器加图标样式
529
+
530
+ ---
531
+
532
+ ## 八、AI 生成纪律
533
+
534
+ ### 8.1 硬约束(写代码时禁止越界)
535
+
536
+ - **禁止内联 `<svg>`** —— 用 lucide 现成图标(`import { Smile } from 'lucide-react'`),特殊场景才走 children 通道传自定义 svg
537
+ - **禁止从 `@ant-design/icons` / `@alifd/next` icon 引入** —— 由 [`@teamix-evo/eslint-config`](../../../../eslint-config/) 的 `teamix-evo/icon-from-lucide` 规则强制
538
+ - **禁止给 Icon 加 `pixelSize` / arbitrary `size-[Xpx]`** —— 用 5 档枚举 size,真有特殊像素诉求走 lint warn 路径用 `className`,且每处需 review
539
+ - **禁止颜色字面量** —— `style={{ color: '#1DC11D' }}` 是 cloud-design 老写法,现在走 `tone` 枚举或父级 currentColor
540
+ - **禁止给 Icon 加点击事件后忘记加 `decorative={false}` + `aria-label`** —— 装饰性图标不应承担交互,要交互必须语义化
541
+
542
+ ### 8.2 推荐范式
543
+
544
+ - **基础用法**:`<Icon icon={Smile} />`(默认装饰性,默认 md size,跟随父色)
545
+ - **状态色**:`<Icon icon={CheckCircle2} tone="success" size="lg" />`
546
+ - **背景容器**:`<Icon icon={AlertTriangle} withBackground bgTone="warning" shape="circle" size="lg" />`
547
+ - **加载**:`<Icon icon={Loader2} spin tone="muted" />`(注意:常规 loading 优先用 [Spinner](../spinner/spinner.tsx) 组件,Spinner 内部已封装好 `role="status"` + `sr-only` 文案)
548
+ - **可点击**:`<button aria-label="删除"><Icon icon={Trash2} decorative /></button>`(图标装饰性,语义靠 button)
549
+ - **Slot 包裹**:`<Icon asChild size="lg" tone="primary"><a href="..."><MyCustomLogo /></a></Icon>`
550
+
551
+ ### 8.3 反例
552
+
553
+ - ❌ `<Icon icon={Smile} pixelSize={17} />` — 不该开像素自由
554
+ - ❌ `<Icon icon={Trash2} onClick={...} />` — 装饰性图标不应承担交互;要交互包 `<button>`
555
+ - ❌ `<Icon icon={Loader2} spin rotate={45} />` — spin 与 rotate 同用语义模糊,选一个
556
+ - ❌ `<Icon withBackground bgTone="primary" tone="destructive" />` — 背景与内层 tone 打架(实现层会忽略 tone,但代码意图就是错的)
557
+ - ❌ `import { Icon } from '@/components/icon'; <Icon icon={SmileOutlined as any} />` — antd icon 不是 LucideIcon
558
+ - ❌ 全屏 loading 用 `<Icon icon={Loader2} spin size="xl" />` — 用 `<Spinner size="xl" />`,后者带读屏文案
559
+
560
+ ---
561
+
562
+ ## 九、registryDependencies 与 npm 依赖
563
+
564
+ ### 9.1 同库依赖
565
+
566
+ | Entry | 类型 | 作用 |
567
+ | ----- | ---- | --------------------------------------- |
568
+ | `cn` | util | className 合并(`clsx + tailwind-merge`) |
569
+
570
+ > `lucide-react` 的具体图标**不**列入 registryDependencies —— 消费方按需 import,本组件不绑定任何具体图标。
571
+
572
+ ### 9.2 npm 依赖
573
+
574
+ ```json
575
+ {
576
+ "@radix-ui/react-slot": "^1.1.0",
577
+ "class-variance-authority": "^0.7.0",
578
+ "lucide-react": "^0.460.0"
579
+ }
580
+ ```
581
+
582
+ ### 9.3 manifest.json entry 草稿
583
+
584
+ ```json
585
+ {
586
+ "id": "icon",
587
+ "name": "Icon",
588
+ "type": "component",
589
+ "description": "图标容器 — 基于 lucide-react 的统一封装。5 档尺寸 + 6 档语义配色 + spin/rotate + Slot + 背景容器(circle/square × 5 档 bgTone) + 装饰/非装饰无障碍。承接 cloud-design Icon 的尺寸语义与 iconBackgroundType/Color 能力,iconfont 工厂 v0.2 接入但 API 已稳定",
590
+ "files": [
591
+ {
592
+ "source": "src/components/icon/icon.tsx",
593
+ "targetAlias": "components",
594
+ "targetName": "icon.tsx"
595
+ }
596
+ ],
597
+ "meta": "src/components/icon/icon.meta.md",
598
+ "registryDependencies": ["cn"],
599
+ "dependencies": {
600
+ "@radix-ui/react-slot": "^1.1.0",
601
+ "class-variance-authority": "^0.7.0",
602
+ "lucide-react": "^0.460.0"
603
+ },
604
+ "updateStrategy": "frozen"
605
+ }
606
+ ```
607
+
608
+ ---
609
+
610
+ ## 十、伴生文件清单
611
+
612
+ 后续真正落地组件时需要产出 3 份文件 + 1 处 manifest 更新:
613
+
614
+ | 文件 | 必需 | 关键内容 |
615
+ | ------------------ | ---- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
616
+ | `icon.tsx` | ✅ | 上文第七节实现草稿;每个 prop 必带 JSDoc + `@default` |
617
+ | `icon.meta.md` | ✅ | 顶部 1~2 行组件描述 + When to use / NOT use + Props marker(`<!-- auto:props:begin -->`)+ Deps marker(`<!-- auto:deps:begin -->`)+ AI 生成纪律段(直接迁本文档第八节)+ 路线图段(第十一节) |
618
+ | `icon.stories.tsx` | ✅ | CSF 3.0,`tags: ['autodocs']`,`meta.parameters.docs.description.component` 写四段式描述,argTypes 覆盖全部 prop;Stories 列表见下表 |
619
+ | `manifest.json` | ✅ | 追加上文第 9.3 节 entry,**注意 `id` 字典序插入位置**(在 `icon-***` 区,目前没有 icon 前缀的 entry,可放在 `image` 之前) |
620
+
621
+ ### 10.1 stories 覆盖清单
622
+
623
+ | Story | 演示重点 |
624
+ | ---------------------- | ----------------------------------------------------- |
625
+ | `Playground` | 全 prop 可控(默认 story,argTypes 暴露全集) |
626
+ | `Sizes` | 5 档 size + inherit 横排对比(同图标 Smile) |
627
+ | `Tones` | 6 档 tone 横排对比(同 size md) |
628
+ | `Spinning` | `spin=true`,搭配 `Loader2` |
629
+ | `Rotated` | rotate 0 / 45 / 90 / 180 度横排 |
630
+ | `WithAriaLabel` | 非装饰场景,`decorative={false}` + `aria-label` |
631
+ | `AsChild` | Slot 模式包裹自定义元素 |
632
+ | `WithChildren` | children 通道传任意自定义 SVG |
633
+ | `WithBackgroundCircle` | `withBackground=true shape="circle"`,5 档 bgTone 横排 |
634
+ | `WithBackgroundSquare` | `withBackground=true shape="square"`,5 档 bgTone 横排 |
635
+ | `BgToneMatrix` | 5 档 size × 5 档 bgTone 矩阵展示(2 行 shape) |
636
+
637
+ ### 10.2 stories 中 icon mapping 范例
638
+
639
+ ```typescript
640
+ import { Smile, Check, AlertTriangle, Loader2, Search, Trash2, Settings, Info } from 'lucide-react';
641
+
642
+ const iconMapping = { Smile, Check, AlertTriangle, Loader2, Search, Trash2, Settings, Info };
643
+
644
+ // argTypes:
645
+ icon: { control: 'select', options: Object.keys(iconMapping), mapping: iconMapping };
646
+ ```
647
+
648
+ ### 10.3 四段式 description 草稿
649
+
650
+ > **Icon 图标容器 —— 在 UI 中承载图形语义、状态指示、装饰点缀。基于 `lucide-react` 默认源 + 自定义 SVG / iconfont 工厂(v0.2)双通道,提供 5 档尺寸、6 档语义 tone、spin/rotate 动效、Slot 包装、装饰/非装饰无障碍开关。承接 cloud-design Icon 的 `iconBackgroundType/Color` 能力,新增 `withBackground` + `shape` + `bgTone`,把"圆形/方形 + 彩色背景的 icon 容器"沉淀为单一 prop;shadcn 无对标,antd `@ant-design/icons` 提供 `spin / rotate`:关键 prop `icon`、`size`、`tone`、`spin`、`rotate`、`withBackground`、`shape`、`bgTone`、`decorative`、`asChild`。视觉走 OpenTrek semantic tokens,所有样式来自 `@teamix-evo/tokens`,无 mock。**
651
+
652
+ ---
653
+
654
+ ## 十一、后续扩展点路线图
655
+
656
+ | 阶段 | 能力 | 落地形式 |
657
+ | ----------- | --------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
658
+ | v0.1 (本期) | 默认 lucide 源 + 背景容器 + 5 档 size + 6 档 tone + spin/rotate/Slot/无障碍 | 见上文 |
659
+ | v0.2 | iconfont 工厂 | `createIconfontIcon({ scriptUrl: string }): React.FC<{ type: string }>` 返回与 IconProps `children` 通道兼容的组件,使用方式 `<Icon icon={undefined}><MyIconfont type="store" /></Icon>` 或更短的 `<MyIconfont type="store" />` 直接当作子节点 |
660
+ | v0.3+ | 多 icon set 工厂 | heroicons / tabler 等同样以工厂模式注入,组件本身仍保持 lucide 默认源,**API 不变** |
661
+ | 可选 | string name 注册表 | 若业务侧确有 `<Icon type="smile" />` 习惯保留诉求,提供 opt-in `iconRegistry` 工厂(不强制全量打包) |
662
+
663
+ > **API 在 v0.1 就稳定** —— 后续加源不破坏现有 prop。这是本组件最大的"为什么要包一层"的依据。
664
+
665
+ ---
666
+
667
+ ## 十二、验证步骤
668
+
669
+ 按 ui 包标准动作执行:
670
+
671
+ ```bash
672
+ # 1. 生成 props 表与依赖表(从 IconProps JSDoc + manifest.json 渲染到 icon.meta.md marker 区)
673
+ pnpm --filter @teamix-evo/ui gen:meta
674
+
675
+ # 2. 校验 manifest schema + 文件路径 + 依赖图无环
676
+ pnpm --filter @teamix-evo/ui validate
677
+
678
+ # 3. 类型检查
679
+ pnpm --filter @teamix-evo/ui typecheck
680
+
681
+ # 4. 启动 storybook,在 autodocs 页验证:
682
+ # - 顶部组件描述渲染正确(四段式)
683
+ # - Props 表完整(由 react-docgen-typescript 从 icon.tsx 抽取)
684
+ # - 11 个 story 都能渲染
685
+ # - 切换主题变体(toolbar)确认 tone / bgTone 跟随 token 变化
686
+ pnpm --filter @teamix-evo/ui storybook
687
+
688
+ # 5. 防漂移检查(meta props 表 + 依赖表与源码 / manifest 一致)
689
+ pnpm --filter @teamix-evo/ui gen:check
690
+
691
+ # 6. 视觉与 a11y 对照 design checklist 8 项
692
+ # skill: teamix-evo-design-opentrek/checklist.md
693
+ ```
694
+
695
+ ---
696
+
697
+ ## 十三、与 cloud-design Icon 的差异总结
698
+
699
+ | 维度 | cloud-design Icon | teamix-evo Icon |
700
+ | --------- | -------------------------------------------------------- | ----------------------------------------------------------------- |
701
+ | icon 源 | `@alifd/next` 内置字典 + iconfont 工厂(运行时主题包驱动) | `lucide-react` 默认 + iconfont 工厂(v0.2) + children 通道 |
702
+ | 选择方式 | `type="smile"` 字符串查表 | `icon={Smile}` 组件直传(tree-shaking 友好) |
703
+ | size 档位 | 9 档 + inherit + number | 5 档 + inherit(收敛) |
704
+ | 配色 | `style.color` 自由 + cloud-design 主题包覆盖默认色 | `tone` 6 档语义色(走 tokens) |
705
+ | 背景容器 | 不在 Icon 内部,需要在 ProPageHeader 等容器里手拼 | **集成到 Icon 内**(`withBackground`),单一 prop 即得视觉一致的容器 |
706
+ | 旋转 | 不支持 | `rotate` + `spin` |
707
+ | 无障碍 | 文档约束 | 运行时默认 `aria-hidden`,非装饰强制 `aria-label` |
708
+ | 主题机制 | 主题包 npm,运行时切换 | 设计 tokens(CSS vars),编译期 + CSS 切换 |
709
+
710
+ ---
711
+
712
+ ## 十四、研发任务拆分
713
+
714
+ | # | 任务 | 输入 | 输出 |
715
+ | --- | ------------------------------------------------------ | ----------------- | -------------------------------------------------------------- |
716
+ | 1 | 建目录 + 三件套空壳 | 本文档 | `src/components/icon/{icon.tsx,icon.meta.md,icon.stories.tsx}` |
717
+ | 2 | 写 cva 与 Props 类型 | 本文档第六/四节 | `iconInnerVariants` / `iconContainerVariants` / `IconProps` |
718
+ | 3 | 写组件实现(三个分支) | 本文档第七节 | `Icon` forwardRef 组件 |
719
+ | 4 | 写 stories(11 个 + autodocs description) | 本文档第十节 | `icon.stories.tsx` |
720
+ | 5 | 写 meta(描述 + 何时用 / 不用 + AI 生成纪律 + 路线图) | 本文档第八/十一节 | `icon.meta.md` |
721
+ | 6 | 加 manifest.json entry | 本文档第 9.3 节 | manifest 字典序插入 |
722
+ | 7 | 跑 `gen:meta` / `validate` / `typecheck` / `gen:check` | 任务 1~6 产物 | 本地校验通过 |
723
+ | 8 | 跑 storybook,过 design checklist 8 项 | 任务 7 后 | 视觉与 a11y 验收通过 |
724
+
725
+ ---
726
+
727
+ ## 十五、注意事项 & AI 生成纪律(交付前自检)
728
+
729
+ - [ ] 每个 prop 都有 JSDoc + `@default`(供 `gen:meta` 读取)
730
+ - [ ] `icon.meta.md` 顶部 1~2 行组件描述已写
731
+ - [ ] `icon.meta.md` 含 "AI 生成纪律" 小节(直接迁本文档第八节)
732
+ - [ ] `icon.meta.md` 含 `<!-- auto:deps:begin --> <!-- auto:deps:end -->` marker
733
+ - [ ] `icon.stories.tsx` 的 `meta.parameters.docs.description.component` 已填写四段式描述
734
+ - [ ] manifest.json 已加 entry,registryDependencies 列入 `cn`,dependencies 列入 `@radix-ui/react-slot` / `cva` / `lucide-react`
735
+ - [ ] `pnpm --filter @teamix-evo/ui validate` 通过
736
+ - [ ] `pnpm --filter @teamix-evo/ui gen:check` 通过
737
+ - [ ] 11 个 story 在 storybook 可见,切换主题 tone / bgTone 颜色随之变化
738
+ - [ ] design checklist 8 项(色彩 / 排版 / 圆角 / 间距 / 阴影 / 动效 / 一致性 / 安全)逐项过关
739
+ - [ ] 不绕过 ESLint `teamix-evo/icon-from-lucide` 规则;本组件作为该规则的官方收口出口
740
+
741
+ ---
742
+
743
+ ## 附录 B · v0.1 内部 API 收敛纪录
744
+
745
+ > 本节记录 Icon 组件在 v0.1 进入 stable 之前一次性发生的 API 收敛。原始设计来自本文档第四/六/七节(`icon` / `tone` / `size` / `withBackground` / `shape` / `bgTone` 6 prop),最终落地为 `children` / `variant` / `tone` / `size` / `spin` / `rotate` / `asChild` / `decorative`。本次收敛**只发生在 v0.1 内部**,后续 stable 版本不再变动。
746
+
747
+ ### 决策摘要
748
+
749
+ | 维度 | 初版(本文档原稿) | 终版(v0.1 落地) | 决策 reference |
750
+ | --------- | --------------------------------------------------------- | ------------------------------------------------------ | ---------------------------------------------------------------------------- |
751
+ | 图标入口 | `icon: LucideIcon` prop | `children: ReactNode` 必填(单一通道) | Mantine ThemeIcon `<ThemeIcon><IconSun /></ThemeIcon>` 范式 |
752
+ | 形态 | `withBackground: boolean` + `shape: round/square` 两 prop | `variant: 'plain' \| 'circle' \| 'square'` 三档枚举 | Radix Themes / Mantine ThemeIcon `variant` 单 prop 表达形态 |
753
+ | 配色 | `tone` + `bgTone` 两 prop(前景/背景独立) | `tone` 单轴(带背景时由同一 token 派生浅底深字) | Polaris `tone` 在 token-based 体系下表意更准;`bgTone` 在视觉上无独立调色需求 |
754
+ | 尺寸 | 5 档(xs/sm/md/lg/xl) + inherit | 7 档(xs/sm/md/lg/xl/2xl/3xl) + inherit | 对齐 Tailwind v4 / Radix Themes 命名;7 档覆盖 12px → 48px 完整业务尺度 |
755
+ | 默认 size | `md = 16px`(size-4) | `md = 20px`(size-5) | 与设计稿(7 档示意图)对齐,避免默认偏小 |
756
+ | 透传 | className 部分透传 | `className` / `style` 在三个分支均完整透传到外层根节点 | 修复 `withBackground` 分支 `style` 丢失 bug |
757
+
758
+ ### 决策依据
759
+
760
+ 1. **业界范式对照**(共调研 6 家):shadcn 不做 / MUI 分离 / **Mantine ThemeIcon variant 合一(范式参考)** / Chakra `as` prop / **Radix Themes variant + color(范式参考)** / Ant Design 配置式。Mantine ThemeIcon 的 children 通道 + variant 单 prop 同时被多家采纳,是当前最稳的折衷。
761
+ 2. **`tone` vs `color` 取舍**:`color` 是绝对主流(Mantine/MUI/Chakra/Radix Themes/AntD),`tone` 是 Polaris 标杆。本仓库 token-based + uni-manager 多变体,`tone` 比 `color` 更能表达"语义"而非"颜色值"。
762
+ ⚠️ **二次回退**:本决策后续被 ADR 0021 推翻 — `tone` 实测在业界仅 Polaris 一家,不在 antd / shadcn 消费方认知词典内。二次回退详见**附录 C**。
763
+ 3. **size 不进 tokens 包**:Tailwind v4 默认 `--spacing: 0.25rem` spacing scale 已覆盖 12/16/20/24/28/36/48px。Icon size 不是品牌差异轴,无需进 tokens。
764
+ 4. **第零条准则**:能力做 shadcn ∪ antd ∪ cloud-design 并集(保留 `spin` / `rotate` / 带背景容器),理念跟 design(token-based 配色,不裸 hex)。
765
+
766
+ ### 影响面
767
+
768
+ - **PageHeader**:从 `<Icon icon={Cloud} withBackground bgTone="primary" size="lg" />` 改为 `<Icon variant="circle" tone="primary" size="lg"><Cloud /></Icon>`。
769
+ - **ESLint `teamix-evo/icon-from-lucide`**:不变。Icon 仍是 lucide 收口出口,只是入口由 `icon` prop 改为 children。
770
+ - **本文档第四/六/七/十节**:历史描述保留以便追溯决策路径,实现以 `icon.tsx` / `icon.stories.tsx` / `icon.meta.md` 为准。
771
+
772
+ ---
773
+
774
+ ## 附录 C · ADR 0021 二次回退(tone → color)
775
+
776
+ > v0.1 发布后一轮全仓 grep 发现:Tag/Timeline/Typography Text 等取 antd `color`/`type`,Icon/Spinner 取 Polaris `tone`,双轨同在 — 起草 ADR 0021 治理,初版(上午)采 `tone` + `status` 三分。同日下午用户质疑 `tone` 业界流行度,复插后发现:`tone` 在 6 家代表体系(shadcn / antd / MUI / Mantine / Chakra / Polaris)中仅 Polaris 一家采用,不在业界认知主流。ADR 0021 正式版(下午)从 `tone` 二次回退到 `color`。
777
+
778
+ ### 差异点
779
+
780
+ | 维度 | 附录 B(v0.1 初调) | 附录 C(ADR 0021 二次回退) |
781
+ | --------- | ----------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
782
+ | prop 名 | `tone` | **`color`** |
783
+ | 枚举字面 | default / muted / primary / destructive / success / warning | default / muted / primary / success / warning / destructive(仅顺序微调) |
784
+ | 形态 prop | `variant: plain/circle/square` 三档枚举 | 不变 |
785
+ | 论据 | Polaris `tone` 在 token-based 体系表意更准;Spinner 已设下 `tone` 可复用 | 业界主流 5/6 家都用 `color`/`variant`,`tone` 仅 Polaris 孤本;antd 消费方零迁移成本 |
786
+
787
+ ### 影响面
788
+
789
+ - **icon.tsx**:`iconInnerVariants.tone` → `.color`;`iconContainerVariants.tone` → `.color`;`IconTone` 类型 → `IconColor`;默认 `defaultVariants` 同改。
790
+ - **icon.stories.tsx**:`TONES` const 改名 `COLORS`;argTypes/args 中 `tone` → `color`;所有 `<Icon tone="...">` → `<Icon color="...">`;描述文案同步。
791
+ - **上游消费方**:PageHeader / Tag / Timeline / Typography Text / Spinner 等同步 `tone` → `color`(本 ADR 一次性迁移)。
792
+ - **附录 B 决策依据第 2 条**(`tone` vs `color` 取舍):原文保留为演进档案,同时在原句下加了"二次回退"提示。
793
+
794
+ ### 决策者提醒
795
+
796
+ - 本文档第一、二、三、四、五、六节(及附录 B)中出现的 `tone` 字样均为演进档案术语,不代表当前实现。
797
+ - 当前实现以 [icon.tsx](./icon.tsx) / [icon.stories.tsx](./icon.stories.tsx) / [icon.meta.md](./icon.meta.md) / [ADR 0021](../../../../../docs/adr/0021-semantic-color-api-unification.md) 为准。
798
+
799
+ ---
800
+
801
+ ## 附录 A · 关键参考链接
802
+
803
+ - 源头组件:[cloud-design Icon](file:///Users/lyca/Documents/workspace/teamix/cloud-design/base-components/src/icon/index.tsx) / [index.md](file:///Users/lyca/Documents/workspace/teamix/cloud-design/base-components/src/icon/index.md)
804
+ - 范本组件:[Spinner](../spinner/spinner.tsx) — lucide 轻封装的最简范本
805
+ - 关联组件:[PageHeader](../page-header/DEVELOPMENT.md) — `withBackground` 能力的首个消费者
806
+ - ui 包入口:[AGENTS.md](../../../AGENTS.md) — 第零条准则与工程约束
807
+ - ESLint 规则:[`@teamix-evo/eslint-config`](../../../../eslint-config/) — `teamix-evo/icon-from-lucide` 等
808
+ - design 哲学:`teamix-evo-design-opentrek/philosophy.md` / `foundations.md` / `boundaries.md` / `checklist.md`
809
+ - 关联 ADR:[ADR 0008](../../../../../docs/adr/0008-eslint-visual-rules-warn-baseline.md) — visual rules warn baseline(success/warning token 缺口处置)