@teamix-evo/ui 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (270) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +336 -0
  3. package/_data.json +12 -0
  4. package/manifest.json +1688 -0
  5. package/package.json +90 -0
  6. package/src/components/accordion/accordion.meta.md +87 -0
  7. package/src/components/accordion/accordion.stories.tsx +67 -0
  8. package/src/components/accordion/accordion.tsx +58 -0
  9. package/src/components/affix/affix.meta.md +80 -0
  10. package/src/components/affix/affix.stories.tsx +57 -0
  11. package/src/components/affix/affix.tsx +97 -0
  12. package/src/components/alert/alert.meta.md +101 -0
  13. package/src/components/alert/alert.stories.tsx +93 -0
  14. package/src/components/alert/alert.tsx +132 -0
  15. package/src/components/alert-dialog/alert-dialog.meta.md +107 -0
  16. package/src/components/alert-dialog/alert-dialog.stories.tsx +81 -0
  17. package/src/components/alert-dialog/alert-dialog.tsx +136 -0
  18. package/src/components/anchor/anchor.meta.md +87 -0
  19. package/src/components/anchor/anchor.stories.tsx +74 -0
  20. package/src/components/anchor/anchor.tsx +130 -0
  21. package/src/components/app/app.meta.md +86 -0
  22. package/src/components/app/app.stories.tsx +62 -0
  23. package/src/components/app/app.tsx +58 -0
  24. package/src/components/aspect-ratio/aspect-ratio.meta.md +81 -0
  25. package/src/components/aspect-ratio/aspect-ratio.stories.tsx +59 -0
  26. package/src/components/aspect-ratio/aspect-ratio.tsx +22 -0
  27. package/src/components/auto-complete/auto-complete.meta.md +102 -0
  28. package/src/components/auto-complete/auto-complete.stories.tsx +93 -0
  29. package/src/components/auto-complete/auto-complete.tsx +205 -0
  30. package/src/components/avatar/avatar.meta.md +94 -0
  31. package/src/components/avatar/avatar.stories.tsx +80 -0
  32. package/src/components/avatar/avatar.tsx +126 -0
  33. package/src/components/badge/badge.meta.md +119 -0
  34. package/src/components/badge/badge.stories.tsx +153 -0
  35. package/src/components/badge/badge.tsx +210 -0
  36. package/src/components/breadcrumb/breadcrumb.meta.md +107 -0
  37. package/src/components/breadcrumb/breadcrumb.stories.tsx +84 -0
  38. package/src/components/breadcrumb/breadcrumb.tsx +122 -0
  39. package/src/components/button/button.meta.md +98 -0
  40. package/src/components/button/button.stories.tsx +235 -0
  41. package/src/components/button/button.tsx +160 -0
  42. package/src/components/button-group/button-group.meta.md +92 -0
  43. package/src/components/button-group/button-group.stories.tsx +90 -0
  44. package/src/components/button-group/button-group.tsx +75 -0
  45. package/src/components/calendar/calendar.meta.md +118 -0
  46. package/src/components/calendar/calendar.stories.tsx +68 -0
  47. package/src/components/calendar/calendar.tsx +107 -0
  48. package/src/components/card/card.meta.md +117 -0
  49. package/src/components/card/card.stories.tsx +112 -0
  50. package/src/components/card/card.tsx +222 -0
  51. package/src/components/carousel/carousel.meta.md +117 -0
  52. package/src/components/carousel/carousel.stories.tsx +84 -0
  53. package/src/components/carousel/carousel.tsx +224 -0
  54. package/src/components/cascader/cascader.meta.md +110 -0
  55. package/src/components/cascader/cascader.stories.tsx +108 -0
  56. package/src/components/cascader/cascader.tsx +198 -0
  57. package/src/components/checkbox/checkbox.meta.md +99 -0
  58. package/src/components/checkbox/checkbox.stories.tsx +130 -0
  59. package/src/components/checkbox/checkbox.tsx +125 -0
  60. package/src/components/collapsible/collapsible.meta.md +80 -0
  61. package/src/components/collapsible/collapsible.stories.tsx +35 -0
  62. package/src/components/collapsible/collapsible.tsx +18 -0
  63. package/src/components/color-picker/color-picker.meta.md +84 -0
  64. package/src/components/color-picker/color-picker.stories.tsx +80 -0
  65. package/src/components/color-picker/color-picker.tsx +160 -0
  66. package/src/components/combobox/combobox.meta.md +93 -0
  67. package/src/components/combobox/combobox.stories.tsx +55 -0
  68. package/src/components/combobox/combobox.tsx +130 -0
  69. package/src/components/command/command.meta.md +104 -0
  70. package/src/components/command/command.stories.tsx +59 -0
  71. package/src/components/command/command.tsx +147 -0
  72. package/src/components/context-menu/context-menu.meta.md +90 -0
  73. package/src/components/context-menu/context-menu.stories.tsx +46 -0
  74. package/src/components/context-menu/context-menu.tsx +191 -0
  75. package/src/components/data-table/data-table.meta.md +149 -0
  76. package/src/components/data-table/data-table.stories.tsx +125 -0
  77. package/src/components/data-table/data-table.tsx +185 -0
  78. package/src/components/date-picker/date-picker.meta.md +106 -0
  79. package/src/components/date-picker/date-picker.stories.tsx +58 -0
  80. package/src/components/date-picker/date-picker.tsx +156 -0
  81. package/src/components/descriptions/descriptions.meta.md +78 -0
  82. package/src/components/descriptions/descriptions.stories.tsx +60 -0
  83. package/src/components/descriptions/descriptions.tsx +129 -0
  84. package/src/components/dialog/dialog.meta.md +105 -0
  85. package/src/components/dialog/dialog.stories.tsx +93 -0
  86. package/src/components/dialog/dialog.tsx +128 -0
  87. package/src/components/drawer/drawer.meta.md +96 -0
  88. package/src/components/drawer/drawer.stories.tsx +54 -0
  89. package/src/components/drawer/drawer.tsx +114 -0
  90. package/src/components/dropdown-menu/dropdown-menu.meta.md +103 -0
  91. package/src/components/dropdown-menu/dropdown-menu.stories.tsx +112 -0
  92. package/src/components/dropdown-menu/dropdown-menu.tsx +195 -0
  93. package/src/components/empty/empty.meta.md +81 -0
  94. package/src/components/empty/empty.stories.tsx +46 -0
  95. package/src/components/empty/empty.tsx +47 -0
  96. package/src/components/field/field.meta.md +116 -0
  97. package/src/components/field/field.stories.tsx +117 -0
  98. package/src/components/field/field.tsx +164 -0
  99. package/src/components/flex/flex.meta.md +94 -0
  100. package/src/components/flex/flex.stories.tsx +112 -0
  101. package/src/components/flex/flex.tsx +122 -0
  102. package/src/components/float-button/float-button.meta.md +87 -0
  103. package/src/components/float-button/float-button.stories.tsx +78 -0
  104. package/src/components/float-button/float-button.tsx +143 -0
  105. package/src/components/form/form.meta.md +131 -0
  106. package/src/components/form/form.stories.tsx +122 -0
  107. package/src/components/form/form.tsx +194 -0
  108. package/src/components/grid/grid.meta.md +87 -0
  109. package/src/components/grid/grid.stories.tsx +99 -0
  110. package/src/components/grid/grid.tsx +130 -0
  111. package/src/components/hover-card/hover-card.meta.md +92 -0
  112. package/src/components/hover-card/hover-card.stories.tsx +68 -0
  113. package/src/components/hover-card/hover-card.tsx +29 -0
  114. package/src/components/image/image.meta.md +94 -0
  115. package/src/components/image/image.stories.tsx +55 -0
  116. package/src/components/image/image.tsx +138 -0
  117. package/src/components/input/input.meta.md +109 -0
  118. package/src/components/input/input.stories.tsx +117 -0
  119. package/src/components/input/input.tsx +213 -0
  120. package/src/components/input-group/input-group.meta.md +92 -0
  121. package/src/components/input-group/input-group.stories.tsx +88 -0
  122. package/src/components/input-group/input-group.tsx +107 -0
  123. package/src/components/input-number/input-number.meta.md +91 -0
  124. package/src/components/input-number/input-number.stories.tsx +87 -0
  125. package/src/components/input-number/input-number.tsx +210 -0
  126. package/src/components/input-otp/input-otp.meta.md +105 -0
  127. package/src/components/input-otp/input-otp.stories.tsx +65 -0
  128. package/src/components/input-otp/input-otp.tsx +97 -0
  129. package/src/components/item/item.meta.md +116 -0
  130. package/src/components/item/item.stories.tsx +113 -0
  131. package/src/components/item/item.tsx +171 -0
  132. package/src/components/kbd/kbd.meta.md +85 -0
  133. package/src/components/kbd/kbd.stories.tsx +70 -0
  134. package/src/components/kbd/kbd.tsx +81 -0
  135. package/src/components/label/label.meta.md +91 -0
  136. package/src/components/label/label.stories.tsx +87 -0
  137. package/src/components/label/label.tsx +66 -0
  138. package/src/components/masonry/masonry.meta.md +85 -0
  139. package/src/components/masonry/masonry.stories.tsx +66 -0
  140. package/src/components/masonry/masonry.tsx +59 -0
  141. package/src/components/mentions/mentions.meta.md +89 -0
  142. package/src/components/mentions/mentions.stories.tsx +75 -0
  143. package/src/components/mentions/mentions.tsx +237 -0
  144. package/src/components/menubar/menubar.meta.md +100 -0
  145. package/src/components/menubar/menubar.stories.tsx +81 -0
  146. package/src/components/menubar/menubar.tsx +232 -0
  147. package/src/components/native-select/native-select.meta.md +88 -0
  148. package/src/components/native-select/native-select.stories.tsx +80 -0
  149. package/src/components/native-select/native-select.tsx +54 -0
  150. package/src/components/navigation-menu/navigation-menu.meta.md +108 -0
  151. package/src/components/navigation-menu/navigation-menu.stories.tsx +112 -0
  152. package/src/components/navigation-menu/navigation-menu.tsx +125 -0
  153. package/src/components/notification/notification.meta.md +91 -0
  154. package/src/components/notification/notification.stories.tsx +96 -0
  155. package/src/components/notification/notification.tsx +84 -0
  156. package/src/components/pagination/pagination.meta.md +127 -0
  157. package/src/components/pagination/pagination.stories.tsx +62 -0
  158. package/src/components/pagination/pagination.tsx +285 -0
  159. package/src/components/popconfirm/popconfirm.meta.md +109 -0
  160. package/src/components/popconfirm/popconfirm.stories.tsx +76 -0
  161. package/src/components/popconfirm/popconfirm.tsx +134 -0
  162. package/src/components/popover/popover.meta.md +97 -0
  163. package/src/components/popover/popover.stories.tsx +82 -0
  164. package/src/components/popover/popover.tsx +55 -0
  165. package/src/components/progress/progress.meta.md +86 -0
  166. package/src/components/progress/progress.stories.tsx +75 -0
  167. package/src/components/progress/progress.tsx +195 -0
  168. package/src/components/radio-group/radio-group.meta.md +103 -0
  169. package/src/components/radio-group/radio-group.stories.tsx +77 -0
  170. package/src/components/radio-group/radio-group.tsx +78 -0
  171. package/src/components/rate/rate.meta.md +87 -0
  172. package/src/components/rate/rate.stories.tsx +81 -0
  173. package/src/components/rate/rate.tsx +153 -0
  174. package/src/components/resizable/resizable.meta.md +92 -0
  175. package/src/components/resizable/resizable.stories.tsx +104 -0
  176. package/src/components/resizable/resizable.tsx +56 -0
  177. package/src/components/result/result.meta.md +90 -0
  178. package/src/components/result/result.stories.tsx +71 -0
  179. package/src/components/result/result.tsx +91 -0
  180. package/src/components/scroll-area/scroll-area.meta.md +84 -0
  181. package/src/components/scroll-area/scroll-area.stories.tsx +41 -0
  182. package/src/components/scroll-area/scroll-area.tsx +51 -0
  183. package/src/components/segmented/segmented.meta.md +103 -0
  184. package/src/components/segmented/segmented.stories.tsx +101 -0
  185. package/src/components/segmented/segmented.tsx +138 -0
  186. package/src/components/select/select.meta.md +110 -0
  187. package/src/components/select/select.stories.tsx +100 -0
  188. package/src/components/select/select.tsx +188 -0
  189. package/src/components/separator/separator.meta.md +74 -0
  190. package/src/components/separator/separator.stories.tsx +71 -0
  191. package/src/components/separator/separator.tsx +104 -0
  192. package/src/components/sheet/sheet.meta.md +97 -0
  193. package/src/components/sheet/sheet.stories.tsx +82 -0
  194. package/src/components/sheet/sheet.tsx +139 -0
  195. package/src/components/sidebar/sidebar.meta.md +131 -0
  196. package/src/components/sidebar/sidebar.stories.tsx +82 -0
  197. package/src/components/sidebar/sidebar.tsx +351 -0
  198. package/src/components/skeleton/skeleton.meta.md +95 -0
  199. package/src/components/skeleton/skeleton.stories.tsx +79 -0
  200. package/src/components/skeleton/skeleton.tsx +144 -0
  201. package/src/components/slider/slider.meta.md +94 -0
  202. package/src/components/slider/slider.stories.tsx +69 -0
  203. package/src/components/slider/slider.tsx +86 -0
  204. package/src/components/sonner/sonner.meta.md +96 -0
  205. package/src/components/sonner/sonner.stories.tsx +91 -0
  206. package/src/components/sonner/sonner.tsx +40 -0
  207. package/src/components/space/space.meta.md +94 -0
  208. package/src/components/space/space.stories.tsx +94 -0
  209. package/src/components/space/space.tsx +106 -0
  210. package/src/components/spinner/spinner.meta.md +76 -0
  211. package/src/components/spinner/spinner.stories.tsx +71 -0
  212. package/src/components/spinner/spinner.tsx +64 -0
  213. package/src/components/statistic/statistic.meta.md +99 -0
  214. package/src/components/statistic/statistic.stories.tsx +71 -0
  215. package/src/components/statistic/statistic.tsx +197 -0
  216. package/src/components/steps/steps.meta.md +102 -0
  217. package/src/components/steps/steps.stories.tsx +75 -0
  218. package/src/components/steps/steps.tsx +170 -0
  219. package/src/components/switch/switch.meta.md +92 -0
  220. package/src/components/switch/switch.stories.tsx +75 -0
  221. package/src/components/switch/switch.tsx +101 -0
  222. package/src/components/table/table.meta.md +95 -0
  223. package/src/components/table/table.stories.tsx +75 -0
  224. package/src/components/table/table.tsx +122 -0
  225. package/src/components/tabs/tabs.meta.md +98 -0
  226. package/src/components/tabs/tabs.stories.tsx +70 -0
  227. package/src/components/tabs/tabs.tsx +119 -0
  228. package/src/components/tag/tag.meta.md +94 -0
  229. package/src/components/tag/tag.stories.tsx +77 -0
  230. package/src/components/tag/tag.tsx +185 -0
  231. package/src/components/textarea/textarea.meta.md +83 -0
  232. package/src/components/textarea/textarea.stories.tsx +63 -0
  233. package/src/components/textarea/textarea.tsx +113 -0
  234. package/src/components/time-picker/time-picker.meta.md +83 -0
  235. package/src/components/time-picker/time-picker.stories.tsx +59 -0
  236. package/src/components/time-picker/time-picker.tsx +94 -0
  237. package/src/components/timeline/timeline.meta.md +102 -0
  238. package/src/components/timeline/timeline.stories.tsx +104 -0
  239. package/src/components/timeline/timeline.tsx +147 -0
  240. package/src/components/toggle/toggle.meta.md +88 -0
  241. package/src/components/toggle/toggle.stories.tsx +66 -0
  242. package/src/components/toggle/toggle.tsx +53 -0
  243. package/src/components/toggle-group/toggle-group.meta.md +90 -0
  244. package/src/components/toggle-group/toggle-group.stories.tsx +83 -0
  245. package/src/components/toggle-group/toggle-group.tsx +78 -0
  246. package/src/components/tooltip/tooltip.meta.md +99 -0
  247. package/src/components/tooltip/tooltip.stories.tsx +71 -0
  248. package/src/components/tooltip/tooltip.tsx +93 -0
  249. package/src/components/tour/tour.meta.md +116 -0
  250. package/src/components/tour/tour.stories.tsx +66 -0
  251. package/src/components/tour/tour.tsx +242 -0
  252. package/src/components/transfer/transfer.meta.md +90 -0
  253. package/src/components/transfer/transfer.stories.tsx +68 -0
  254. package/src/components/transfer/transfer.tsx +251 -0
  255. package/src/components/tree/tree.meta.md +111 -0
  256. package/src/components/tree/tree.stories.tsx +109 -0
  257. package/src/components/tree/tree.tsx +367 -0
  258. package/src/components/tree-select/tree-select.meta.md +100 -0
  259. package/src/components/tree-select/tree-select.stories.tsx +80 -0
  260. package/src/components/tree-select/tree-select.tsx +171 -0
  261. package/src/components/typography/typography.meta.md +102 -0
  262. package/src/components/typography/typography.stories.tsx +115 -0
  263. package/src/components/typography/typography.tsx +245 -0
  264. package/src/components/upload/upload.meta.md +111 -0
  265. package/src/components/upload/upload.stories.tsx +75 -0
  266. package/src/components/upload/upload.tsx +265 -0
  267. package/src/components/watermark/watermark.meta.md +95 -0
  268. package/src/components/watermark/watermark.stories.tsx +78 -0
  269. package/src/components/watermark/watermark.tsx +165 -0
  270. package/src/utils/cn.ts +6 -0
@@ -0,0 +1,106 @@
1
+ import * as React from 'react';
2
+ import { cva, type VariantProps } from 'class-variance-authority';
3
+
4
+ import { cn } from '@/utils/cn';
5
+
6
+ const spaceVariants = cva('flex', {
7
+ variants: {
8
+ direction: {
9
+ horizontal: 'flex-row',
10
+ vertical: 'flex-col',
11
+ },
12
+ size: {
13
+ sm: 'gap-2',
14
+ default: 'gap-3',
15
+ lg: 'gap-4',
16
+ },
17
+ wrap: {
18
+ true: 'flex-wrap',
19
+ false: 'flex-nowrap',
20
+ },
21
+ align: {
22
+ start: 'items-start',
23
+ center: 'items-center',
24
+ end: 'items-end',
25
+ baseline: 'items-baseline',
26
+ stretch: 'items-stretch',
27
+ },
28
+ justify: {
29
+ start: 'justify-start',
30
+ center: 'justify-center',
31
+ end: 'justify-end',
32
+ between: 'justify-between',
33
+ around: 'justify-around',
34
+ },
35
+ },
36
+ defaultVariants: { direction: 'horizontal', size: 'default', wrap: false, align: 'center' },
37
+ });
38
+
39
+ export interface SpaceProps
40
+ extends React.HTMLAttributes<HTMLDivElement>,
41
+ VariantProps<typeof spaceVariants> {
42
+ /**
43
+ * 排列方向(antd `direction` 并集)。
44
+ * @default "horizontal"
45
+ */
46
+ direction?: 'horizontal' | 'vertical';
47
+ /**
48
+ * 间距档位(antd `size` 并集) — 走 design 间距刻度,不接受 number。
49
+ * @default "default"
50
+ */
51
+ size?: 'sm' | 'default' | 'lg';
52
+ /**
53
+ * 是否自动换行(antd `wrap` 并集) — horizontal 模式生效。
54
+ * @default false
55
+ */
56
+ wrap?: boolean;
57
+ /**
58
+ * 主轴对齐方式(antd `align` 并集)。
59
+ * @default "center"
60
+ */
61
+ align?: 'start' | 'center' | 'end' | 'baseline' | 'stretch';
62
+ /**
63
+ * 副轴对齐(antd `Space` 无,Space.Compact 也无 — 但中后台需求高,补)。
64
+ */
65
+ justify?: 'start' | 'center' | 'end' | 'between' | 'around';
66
+ /**
67
+ * 分隔节点(antd `split` 并集) — 在每两个子项之间插入(常用 `<Separator>`)。
68
+ */
69
+ split?: React.ReactNode;
70
+ }
71
+
72
+ /**
73
+ * 间距容器 — antd 独有补足。**等价 antd `Space`**:把同语义并排子项统一间距、对齐与可选换行。
74
+ * 与 `Flex` 区别:Space 偏向"小集合 inline 间距"(按钮组、tag 组、表单 label-value 对),
75
+ * Flex 偏向"完整布局容器"。
76
+ */
77
+ const Space = React.forwardRef<HTMLDivElement, SpaceProps>(
78
+ (
79
+ { direction, size, wrap, align, justify, split, className, children, ...props },
80
+ ref,
81
+ ) => {
82
+ const items = React.Children.toArray(children).filter(Boolean);
83
+ return (
84
+ <div
85
+ ref={ref}
86
+ className={cn(
87
+ spaceVariants({ direction, size, wrap, align, justify }),
88
+ className,
89
+ )}
90
+ {...props}
91
+ >
92
+ {split
93
+ ? items.map((child, i) => (
94
+ <React.Fragment key={i}>
95
+ {i > 0 ? split : null}
96
+ {child}
97
+ </React.Fragment>
98
+ ))
99
+ : children}
100
+ </div>
101
+ );
102
+ },
103
+ );
104
+ Space.displayName = 'Space';
105
+
106
+ export { Space, spaceVariants };
@@ -0,0 +1,76 @@
1
+ ---
2
+ id: spinner
3
+ name: Spinner
4
+ type: component
5
+ category: foundation
6
+ since: 0.1.0
7
+ package: "@teamix-evo/ui"
8
+ ---
9
+
10
+ # Spinner
11
+
12
+ 旋转加载指示器 — shadcn 2025-10 新增,**等价 antd `Spin`**(无遮罩版)。基于 `lucide-react` 的 `Loader2`,4 档尺寸 + 4 档语义配色,自带 `role="status"` 与读屏文本。
13
+
14
+ ## When to use
15
+
16
+ - 按钮 / 表单内联 loading(`Button loading` 内部已用同一图标,这里独立 spinner 用于自定义触发器)
17
+ - 容器局部加载(请求中)
18
+ - 全屏 loading 占位(`size="xl"` + 居中)
19
+
20
+ ## When NOT to use
21
+
22
+ - 内容结构占位 → `Skeleton`(组件级 / 段落级)
23
+ - 整页"模态阻塞 loading" → `Dialog` + Spinner 自行组合(本组件不内置遮罩)
24
+ - 进度可知场景 → `Progress`(线形)或 `ProgressCircle`(环形)
25
+
26
+ <!-- auto:props:begin -->
27
+ | 名称 | 类型 | 默认值 | 必填 | 说明 |
28
+ | --- | --- | --- | --- | --- |
29
+ | `size` | `'sm' \| 'default' \| 'lg' \| 'xl'` | `"default"` | – | 尺寸 — 与 antd Spin 的 small/middle/large 对齐(加 xl 用于全屏 loading)。 |
30
+ | `tone` | `'default' \| 'muted' \| 'primary' \| 'destructive'` | `"default"` | – | 配色 — `muted` 走次级文本色;`primary` / `destructive` 走语义色。 |
31
+ | `label` | `string` | – | – | 给读屏器的描述(默认 "Loading...")。 |
32
+ <!-- auto:props:end -->
33
+
34
+ <!-- auto:deps:begin -->
35
+ ### 同库依赖
36
+
37
+ > `teamix-evo ui add spinner` 时,以下 entry 会被自动连带安装(无需手动 add)。
38
+
39
+ | Entry | 类型 | 描述 |
40
+ | --- | --- | --- |
41
+ | `cn` | util | Tailwind className 合并工具(clsx + tailwind-merge) |
42
+
43
+ ### npm 依赖
44
+
45
+ > 业务侧需要先 `pnpm add` / `npm install` 这些包。CLI 在 `ui add` 完成后会列出此提示。
46
+
47
+ ```bash
48
+ pnpm add class-variance-authority@^0.7.0 lucide-react@^0.460.0
49
+ ```
50
+ <!-- auto:deps:end -->
51
+
52
+ ## AI 生成纪律
53
+
54
+ - **不要自己写 `<div className="animate-spin">`** — 用本组件,保留读屏可访问性
55
+ - **`tone="primary"` 配主操作 loading**,`tone="destructive"` 仅用于真正失败回退 — 不要乱用
56
+ - **不要给 spinner 加旋转字幕 / 进度数字** — 那是 Progress 的职责,语义不同
57
+ - **包裹遮罩 loading**:外层加 `relative` 容器 + spinner 居中,**不要**包到本组件内部破坏其单一职责
58
+
59
+ ## Examples
60
+
61
+ ```tsx
62
+ import { Spinner } from '@/components/ui/spinner';
63
+
64
+ // 行内
65
+ <span>正在保存 <Spinner size="sm" tone="muted" /></span>
66
+
67
+ // 居中
68
+ <div className="flex h-40 items-center justify-center">
69
+ <Spinner size="lg" tone="primary" label="加载用户信息..." />
70
+ </div>
71
+
72
+ // 全屏
73
+ <div className="fixed inset-0 grid place-items-center bg-background/80 backdrop-blur-sm">
74
+ <Spinner size="xl" tone="primary" />
75
+ </div>
76
+ ```
@@ -0,0 +1,71 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { Spinner } from './spinner';
3
+
4
+ const meta: Meta<typeof Spinner> = {
5
+ title: '基础原语 · Foundation/Spinner',
6
+ component: Spinner,
7
+ tags: ['autodocs'],
8
+ parameters: {
9
+ docs: {
10
+ description: {
11
+ component:
12
+ '旋转加载指示器 — 局部 / 行内 loading(请求中、按钮提交、占位等待)。基于 lucide Loader2,4 档尺寸 + 4 档配色,自带 role="status" 与 sr-only 读屏文案。shadcn 2025-10 新增,对齐 antd `Spin`(无遮罩版本)。视觉走 OpenTrek tokens,所有样式来自 `@teamix-evo/design`,无 mock。',
13
+ },
14
+ },
15
+ },
16
+ argTypes: {
17
+ size: { control: 'inline-radio', options: ['sm', 'default', 'lg', 'xl'] },
18
+ tone: {
19
+ control: 'inline-radio',
20
+ options: ['default', 'muted', 'primary', 'destructive'],
21
+ },
22
+ },
23
+ args: { size: 'default', tone: 'default' },
24
+ };
25
+
26
+ export default meta;
27
+ type Story = StoryObj<typeof Spinner>;
28
+
29
+ export const Playground: Story = {};
30
+
31
+ export const Sizes: Story = {
32
+ parameters: { controls: { disable: true } },
33
+ render: () => (
34
+ <div className="flex items-end gap-6">
35
+ <Spinner size="sm" />
36
+ <Spinner size="default" />
37
+ <Spinner size="lg" />
38
+ <Spinner size="xl" />
39
+ </div>
40
+ ),
41
+ };
42
+
43
+ export const Tones: Story = {
44
+ parameters: { controls: { disable: true } },
45
+ render: () => (
46
+ <div className="flex items-center gap-6">
47
+ <Spinner tone="default" />
48
+ <Spinner tone="muted" />
49
+ <Spinner tone="primary" />
50
+ <Spinner tone="destructive" />
51
+ </div>
52
+ ),
53
+ };
54
+
55
+ export const Inline: Story = {
56
+ parameters: { controls: { disable: true } },
57
+ render: () => (
58
+ <p className="text-sm">
59
+ 正在保存草稿 <Spinner size="sm" tone="muted" />
60
+ </p>
61
+ ),
62
+ };
63
+
64
+ export const Centered: Story = {
65
+ parameters: { controls: { disable: true } },
66
+ render: () => (
67
+ <div className="flex h-40 items-center justify-center rounded-md border">
68
+ <Spinner size="lg" tone="primary" />
69
+ </div>
70
+ ),
71
+ };
@@ -0,0 +1,64 @@
1
+ import * as React from 'react';
2
+ import { cva, type VariantProps } from 'class-variance-authority';
3
+ import { Loader2 } from 'lucide-react';
4
+
5
+ import { cn } from '@/utils/cn';
6
+
7
+ const spinnerVariants = cva('inline-block shrink-0 animate-spin', {
8
+ variants: {
9
+ size: {
10
+ sm: 'size-3',
11
+ default: 'size-4',
12
+ lg: 'size-6',
13
+ xl: 'size-8',
14
+ },
15
+ tone: {
16
+ default: 'text-foreground',
17
+ muted: 'text-muted-foreground',
18
+ primary: 'text-primary',
19
+ destructive: 'text-destructive',
20
+ },
21
+ },
22
+ defaultVariants: { size: 'default', tone: 'default' },
23
+ });
24
+
25
+ export interface SpinnerProps
26
+ extends Omit<React.HTMLAttributes<HTMLSpanElement>, 'children'>,
27
+ VariantProps<typeof spinnerVariants> {
28
+ /**
29
+ * 尺寸 — 与 antd Spin 的 small/middle/large 对齐(加 xl 用于全屏 loading)。
30
+ * @default "default"
31
+ */
32
+ size?: 'sm' | 'default' | 'lg' | 'xl';
33
+ /**
34
+ * 配色 — `muted` 走次级文本色;`primary` / `destructive` 走语义色。
35
+ * @default "default"
36
+ */
37
+ tone?: 'default' | 'muted' | 'primary' | 'destructive';
38
+ /**
39
+ * 给读屏器的描述(默认 "Loading...")。
40
+ */
41
+ label?: string;
42
+ }
43
+
44
+ /**
45
+ * 简单旋转 loading — shadcn 2025-10 新增,等价 antd `Spin`(无遮罩版)。
46
+ * 需要"包裹内容显示遮罩 loading"时配合 `Skeleton` 或自行包一层。
47
+ */
48
+ const Spinner = React.forwardRef<HTMLSpanElement, SpinnerProps>(
49
+ ({ size, tone, label = 'Loading...', className, ...props }, ref) => (
50
+ <span
51
+ ref={ref}
52
+ role="status"
53
+ aria-live="polite"
54
+ className={cn('inline-flex items-center', className)}
55
+ {...props}
56
+ >
57
+ <Loader2 className={cn(spinnerVariants({ size, tone }))} aria-hidden="true" />
58
+ <span className="sr-only">{label}</span>
59
+ </span>
60
+ ),
61
+ );
62
+ Spinner.displayName = 'Spinner';
63
+
64
+ export { Spinner, spinnerVariants };
@@ -0,0 +1,99 @@
1
+ ---
2
+ id: statistic
3
+ name: Statistic
4
+ type: component
5
+ category: data-display
6
+ since: 0.1.0
7
+ package: "@teamix-evo/ui"
8
+ ---
9
+
10
+ # Statistic
11
+
12
+ 数字展示 — antd 独有补足。**仪表板专用**:大字号数值 + 标题 + 前缀(¥/$ / 图标)+ 后缀(单位)+ 趋势(上 / 下,自动配色)+ 千分位 + 精度。
13
+
14
+ ## When to use
15
+
16
+ - 仪表板核心指标(GMV / 在线人数 / 成功率)
17
+ - 卡片内 KPI 数字
18
+ - 报表 / 数据周报顶部摘要
19
+
20
+ ## When NOT to use
21
+
22
+ - 长文本数据 → 普通 `Paragraph`
23
+ - 表格内数字 → 直接 `<td className="tabular-nums">`
24
+ - 图表 → `Chart`(本仓暂未提供)
25
+
26
+ <!-- auto:props:begin -->
27
+ | 名称 | 类型 | 默认值 | 必填 | 说明 |
28
+ | --- | --- | --- | --- | --- |
29
+ | `title` | `React.ReactNode` | – | – | 标题(antd `title` 并集)。 |
30
+ | `value` | `number \| string` | – | ✓ | 数值(antd `value` 并集)。 |
31
+ | `prefix` | `React.ReactNode` | – | – | 前缀(图标 / 货币符号 / 单位)。 |
32
+ | `suffix` | `React.ReactNode` | – | – | 后缀(单位 / 趋势)。 |
33
+ | `precision` | `number` | – | – | 数值精度(antd `precision` 并集),仅 number 生效。 |
34
+ | `groupSeparator` | `boolean` | `true` | – | 千分位分隔符。 |
35
+ | `trend` | `'up' \| 'down' \| 'none'` | `"none"` | – | 趋势 — `up`(绿色 ↑)/ `down`(红色 ↓)/ `none`。前置在数值上方或左侧。 |
36
+ | `trendValue` | `string \| number` | – | – | 趋势百分比文字(配 trend 显示在 suffix 区)。 |
37
+ | `valueClassName` | `string` | – | – | 数值颜色覆盖(默认随 trend / 否则 foreground)。 |
38
+ <!-- auto:props:end -->
39
+
40
+ <!-- auto:deps:begin -->
41
+ ### 同库依赖
42
+
43
+ > `teamix-evo ui add statistic` 时,以下 entry 会被自动连带安装(无需手动 add)。
44
+
45
+ | Entry | 类型 | 描述 |
46
+ | --- | --- | --- |
47
+ | `cn` | util | Tailwind className 合并工具(clsx + tailwind-merge) |
48
+
49
+ ### npm 依赖
50
+
51
+ > 业务侧需要先 `pnpm add` / `npm install` 这些包。CLI 在 `ui add` 完成后会列出此提示。
52
+
53
+ ```bash
54
+ pnpm add lucide-react@^0.460.0
55
+ ```
56
+ <!-- auto:deps:end -->
57
+
58
+ ## AI 生成纪律
59
+
60
+ - **`trend` 颜色优先级最高**:有 trend 时数值 + 箭头自动套色,**不要**额外加 className 颜色
61
+ - **`prefix` 用 ¥ / $ / icon**:**不要**塞文字单位(那是 suffix 的职责)
62
+ - **`precision` 仅 number 生效**:string value(如 `'1,234.56k'`)请自行格式化好再传
63
+ - **`tabular-nums` 已自动**:数字对齐用,不要再覆盖
64
+
65
+ ## Examples
66
+
67
+ ```tsx
68
+ import { Statistic } from '@/components/ui/statistic';
69
+
70
+ // 基础
71
+ <Statistic title="今日 GMV" value={8493521} prefix="¥" />
72
+
73
+ // 精度 + 后缀
74
+ <Statistic title="转化率" value={3.245} precision={2} suffix="%" />
75
+
76
+ // 趋势(自动配色)
77
+ <Statistic
78
+ title="活跃用户"
79
+ value={1234}
80
+ trend="up"
81
+ trendValue="+12.4%"
82
+ />
83
+
84
+ <Statistic
85
+ title="错误率"
86
+ value={0.83}
87
+ precision={2}
88
+ suffix="%"
89
+ trend="down"
90
+ trendValue="-0.5%"
91
+ />
92
+
93
+ // 卡片内
94
+ <Card>
95
+ <CardContent className="pt-6">
96
+ <Statistic title="本月订单" value={2847} prefix={<ShoppingCart />} />
97
+ </CardContent>
98
+ </Card>
99
+ ```
@@ -0,0 +1,71 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { ShoppingCart, Users } from 'lucide-react';
3
+ import { Statistic } from './statistic';
4
+ import { Card, CardContent } from '@/components/card/card';
5
+
6
+ const meta: Meta<typeof Statistic> = {
7
+ title: '数据展示 · Data Display/Statistic',
8
+ component: Statistic,
9
+ tags: ['autodocs'],
10
+ parameters: {
11
+ docs: {
12
+ description: {
13
+ component:
14
+ '数字展示 — 仪表板核心指标。大字号 + 千分位 + 精度 + 前后缀 + 趋势(上 / 下,自动配色)。OpenTrek tokens 适配,等价 antd Statistic。',
15
+ },
16
+ },
17
+ },
18
+ };
19
+
20
+ export default meta;
21
+ type Story = StoryObj<typeof Statistic>;
22
+
23
+ export const Basic: Story = {
24
+ args: { title: '今日 GMV', value: 8493521, prefix: '¥' },
25
+ };
26
+
27
+ export const Trends: Story = {
28
+ parameters: { controls: { disable: true } },
29
+ render: () => (
30
+ <div className="flex flex-wrap gap-8">
31
+ <Statistic title="活跃用户" value={1234} trend="up" trendValue="+12.4%" />
32
+ <Statistic
33
+ title="错误率"
34
+ value={0.83}
35
+ precision={2}
36
+ suffix="%"
37
+ trend="down"
38
+ trendValue="-0.5%"
39
+ />
40
+ <Statistic title="待处理" value={42} />
41
+ </div>
42
+ ),
43
+ };
44
+
45
+ export const InCards: Story = {
46
+ parameters: { controls: { disable: true } },
47
+ render: () => (
48
+ <div className="grid grid-cols-2 gap-4">
49
+ <Card>
50
+ <CardContent className="pt-6">
51
+ <Statistic
52
+ title="本月订单"
53
+ value={2847}
54
+ prefix={<ShoppingCart />}
55
+ trend="up"
56
+ trendValue="+18%"
57
+ />
58
+ </CardContent>
59
+ </Card>
60
+ <Card>
61
+ <CardContent className="pt-6">
62
+ <Statistic
63
+ title="活跃用户"
64
+ value={12480}
65
+ prefix={<Users />}
66
+ />
67
+ </CardContent>
68
+ </Card>
69
+ </div>
70
+ ),
71
+ };
@@ -0,0 +1,197 @@
1
+ import * as React from 'react';
2
+ import { ArrowDown, ArrowUp } from 'lucide-react';
3
+
4
+ import { cn } from '@/utils/cn';
5
+
6
+ export interface StatisticProps
7
+ extends Omit<React.HTMLAttributes<HTMLDivElement>, 'title' | 'prefix'> {
8
+ /** 标题(antd `title` 并集)。 */
9
+ title?: React.ReactNode;
10
+ /** 数值(antd `value` 并集)。 */
11
+ value: number | string;
12
+ /** 前缀(图标 / 货币符号 / 单位)。 */
13
+ prefix?: React.ReactNode;
14
+ /** 后缀(单位 / 趋势)。 */
15
+ suffix?: React.ReactNode;
16
+ /** 数值精度(antd `precision` 并集),仅 number 生效。 */
17
+ precision?: number;
18
+ /** 千分位分隔符。 @default true */
19
+ groupSeparator?: boolean;
20
+ /**
21
+ * 趋势 — `up`(绿色 ↑)/ `down`(红色 ↓)/ `none`。前置在数值上方或左侧。
22
+ * @default "none"
23
+ */
24
+ trend?: 'up' | 'down' | 'none';
25
+ /** 趋势百分比文字(配 trend 显示在 suffix 区)。 */
26
+ trendValue?: string | number;
27
+ /** 数值颜色覆盖(默认随 trend / 否则 foreground)。 */
28
+ valueClassName?: string;
29
+ }
30
+
31
+ function formatNumber(
32
+ v: number,
33
+ precision?: number,
34
+ groupSeparator = true,
35
+ ): string {
36
+ const fixed =
37
+ precision !== undefined ? v.toFixed(precision) : v.toString();
38
+ if (!groupSeparator) return fixed;
39
+ const [intPart, decPart] = fixed.split('.');
40
+ const grouped = intPart!.replace(/\B(?=(\d{3})+(?!\d))/g, ',');
41
+ return decPart !== undefined ? `${grouped}.${decPart}` : grouped;
42
+ }
43
+
44
+ const trendStyles = {
45
+ up: 'text-emerald-600',
46
+ down: 'text-destructive',
47
+ none: '',
48
+ } as const;
49
+
50
+ const Statistic = React.forwardRef<HTMLDivElement, StatisticProps>(
51
+ (
52
+ {
53
+ title,
54
+ value,
55
+ prefix,
56
+ suffix,
57
+ precision,
58
+ groupSeparator = true,
59
+ trend = 'none',
60
+ trendValue,
61
+ valueClassName,
62
+ className,
63
+ ...props
64
+ },
65
+ ref,
66
+ ) => {
67
+ const display =
68
+ typeof value === 'number'
69
+ ? formatNumber(value, precision, groupSeparator)
70
+ : value;
71
+ const TrendIcon =
72
+ trend === 'up' ? ArrowUp : trend === 'down' ? ArrowDown : null;
73
+ return (
74
+ <div
75
+ ref={ref}
76
+ className={cn('flex flex-col gap-1', className)}
77
+ {...props}
78
+ >
79
+ {title ? (
80
+ <div className="text-sm text-muted-foreground">{title}</div>
81
+ ) : null}
82
+ <div
83
+ className={cn(
84
+ 'flex items-baseline gap-2 text-2xl font-semibold tabular-nums',
85
+ trendStyles[trend],
86
+ valueClassName,
87
+ )}
88
+ >
89
+ {prefix ? (
90
+ <span className="text-base font-medium opacity-80">{prefix}</span>
91
+ ) : null}
92
+ <span>{display}</span>
93
+ {suffix ? (
94
+ <span className="text-sm font-normal text-muted-foreground">
95
+ {suffix}
96
+ </span>
97
+ ) : null}
98
+ {TrendIcon ? (
99
+ <span className="inline-flex items-baseline gap-0.5 text-sm font-medium">
100
+ <TrendIcon className="size-3" />
101
+ {trendValue}
102
+ </span>
103
+ ) : null}
104
+ </div>
105
+ </div>
106
+ );
107
+ },
108
+ );
109
+ Statistic.displayName = 'Statistic';
110
+
111
+ // ─── Statistic.Timer(antd 5.25+ 子组件)─────────────────────────────────
112
+
113
+ export interface StatisticTimerProps
114
+ extends Omit<
115
+ StatisticProps,
116
+ 'value' | 'precision' | 'groupSeparator' | 'onChange'
117
+ > {
118
+ /**
119
+ * 计时类型 — `countdown` 倒计时到 `target`;`countup` 从 `value`(默认 0)向上计时。
120
+ * @default "countdown"
121
+ */
122
+ type?: 'countdown' | 'countup';
123
+ /**
124
+ * 目标时间(毫秒时间戳)。`countdown` 必填;`countup` 可选(不传则从挂载时刻起算)。
125
+ */
126
+ target?: number;
127
+ /**
128
+ * 时间格式化字符串 — 支持 `HH` `mm` `ss` `SSS`(毫秒);`D` 天数;`d` 自动跳过为 0 的天数。
129
+ * @default "HH:mm:ss"
130
+ */
131
+ format?: string;
132
+ /** 倒计时到达 0 / 计时到达 target 时回调。 */
133
+ onFinish?: () => void;
134
+ /** 每次 tick 回调。 */
135
+ onChange?: (msLeft: number) => void;
136
+ }
137
+
138
+ function pad(n: number, width = 2): string {
139
+ return String(Math.max(0, Math.floor(n))).padStart(width, '0');
140
+ }
141
+
142
+ function formatDuration(ms: number, fmt: string): string {
143
+ const totalSec = Math.floor(ms / 1000);
144
+ const days = Math.floor(totalSec / 86400);
145
+ const hours = Math.floor((totalSec % 86400) / 3600);
146
+ const minutes = Math.floor((totalSec % 3600) / 60);
147
+ const seconds = totalSec % 60;
148
+ const millis = ms % 1000;
149
+ return fmt
150
+ .replace(/D/g, String(days))
151
+ .replace(/HH/g, pad(hours))
152
+ .replace(/mm/g, pad(minutes))
153
+ .replace(/ss/g, pad(seconds))
154
+ .replace(/SSS/g, pad(millis, 3));
155
+ }
156
+
157
+ const StatisticTimer = React.forwardRef<HTMLDivElement, StatisticTimerProps>(
158
+ (
159
+ {
160
+ type = 'countdown',
161
+ target,
162
+ format = 'HH:mm:ss',
163
+ onFinish,
164
+ onChange,
165
+ ...rest
166
+ },
167
+ ref,
168
+ ) => {
169
+ const start = React.useMemo(() => Date.now(), []);
170
+ const [now, setNow] = React.useState<number>(() => Date.now());
171
+ const finished = React.useRef(false);
172
+
173
+ React.useEffect(() => {
174
+ const tick = () => setNow(Date.now());
175
+ const id = window.setInterval(tick, 1000);
176
+ return () => window.clearInterval(id);
177
+ }, []);
178
+
179
+ const ms =
180
+ type === 'countdown'
181
+ ? Math.max(0, (target ?? start) - now)
182
+ : Math.max(0, now - (target ?? start));
183
+
184
+ React.useEffect(() => {
185
+ onChange?.(ms);
186
+ if (type === 'countdown' && ms === 0 && !finished.current) {
187
+ finished.current = true;
188
+ onFinish?.();
189
+ }
190
+ }, [ms, type, onChange, onFinish]);
191
+
192
+ return <Statistic ref={ref} value={formatDuration(ms, format)} {...rest} />;
193
+ },
194
+ );
195
+ StatisticTimer.displayName = 'StatisticTimer';
196
+
197
+ export { Statistic, StatisticTimer };