@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,96 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { Toaster } from '@/components/sonner/sonner';
3
+ import { notification } from './notification';
4
+ import { Button } from '@/components/button/button';
5
+
6
+ const meta: Meta = {
7
+ title: '反馈与浮层 · Feedback/Notification',
8
+ tags: ['autodocs'],
9
+ parameters: {
10
+ docs: {
11
+ description: {
12
+ component:
13
+ '卡片式通知 — 右上角 title + description 卡片(部署成功 / 配额提醒)。基于 sonner toast.custom,业务侧只需挂载一次 `<Toaster />`,后续在任意地方 imperative 触发。等价 antd `Notification`,与 toast(message)互补。视觉走 OpenTrek tokens,所有样式来自 `@teamix-evo/design`,无 mock。',
14
+ },
15
+ },
16
+ },
17
+ };
18
+
19
+ export default meta;
20
+ type Story = StoryObj;
21
+
22
+ export const Playground: Story = {
23
+ render: () => (
24
+ <div className="flex flex-col gap-2">
25
+ <Toaster position="top-right" />
26
+ <div className="flex flex-wrap gap-2">
27
+ <Button
28
+ variant="outline"
29
+ onClick={() =>
30
+ notification.info({ title: '提示', description: '这是一条 info 通知' })
31
+ }
32
+ >
33
+ Info
34
+ </Button>
35
+ <Button
36
+ onClick={() =>
37
+ notification.success({
38
+ title: '部署成功',
39
+ description: '用户可在 1 分钟内访问新版本',
40
+ })
41
+ }
42
+ >
43
+ Success
44
+ </Button>
45
+ <Button
46
+ variant="outline"
47
+ onClick={() =>
48
+ notification.warning({
49
+ title: '配额即将到期',
50
+ description: '请在 3 天内续费,否则服务将暂停',
51
+ })
52
+ }
53
+ >
54
+ Warning
55
+ </Button>
56
+ <Button
57
+ variant="destructive"
58
+ onClick={() =>
59
+ notification.error({
60
+ title: '上传失败',
61
+ description: '请检查网络连接后重试',
62
+ })
63
+ }
64
+ >
65
+ Error
66
+ </Button>
67
+ </div>
68
+ </div>
69
+ ),
70
+ };
71
+
72
+ export const WithAction: Story = {
73
+ render: () => (
74
+ <div className="flex flex-col gap-2">
75
+ <Toaster position="top-right" />
76
+ <Button
77
+ onClick={() =>
78
+ notification.open({
79
+ type: 'info',
80
+ title: '新版本可用',
81
+ description: 'v2.5 已发布,刷新页面以应用更新',
82
+ action: {
83
+ label: '刷新',
84
+ onClick: () => {
85
+ // eslint-disable-next-line no-alert
86
+ alert('刷新');
87
+ },
88
+ },
89
+ })
90
+ }
91
+ >
92
+ 显示带操作的通知
93
+ </Button>
94
+ </div>
95
+ ),
96
+ };
@@ -0,0 +1,84 @@
1
+ import * as React from 'react';
2
+ import { toast, type ExternalToast } from 'sonner';
3
+ import { AlertCircle, CheckCircle2, Info, XCircle } from 'lucide-react';
4
+
5
+ import { cn } from '@/utils/cn';
6
+
7
+ /**
8
+ * 卡片式通知 — antd 独有补足。**等价 antd `Notification`**(右上角卡片,与 sonner 同一栈)。
9
+ * 基于 sonner 二次封装,提供 antd 风格的"标题 + 描述 + 状态图标"卡片化 API,
10
+ * 与 `Toaster`(toast 容器)共用同一渲染层 —— 业务侧只需要挂载一次 `<Toaster />`。
11
+ *
12
+ * 与 `toast()` 区别:
13
+ * - `toast()` 是无标题短文字(对应 antd `message`)
14
+ * - `notification.open()` 是 **title + description 卡片**(对应 antd `notification`)
15
+ */
16
+
17
+ export type NotificationType = 'info' | 'success' | 'warning' | 'error';
18
+
19
+ export interface NotificationOptions extends Omit<ExternalToast, 'description' | 'icon'> {
20
+ /** 标题(antd `message` 并集,但 antd 这里命名为 message,易混淆 — 我们用更准确的 `title`)。 */
21
+ title: React.ReactNode;
22
+ /** 描述(antd `description` 并集)。 */
23
+ description?: React.ReactNode;
24
+ /**
25
+ * 状态 — 决定左侧 icon 颜色;`open()` 默认 `info`。
26
+ * `success` / `warning` / `error` 也可用对应快捷方法。
27
+ */
28
+ type?: NotificationType;
29
+ }
30
+
31
+ const iconMap: Record<NotificationType, React.ReactNode> = {
32
+ info: <Info className="size-5 text-primary" />,
33
+ success: <CheckCircle2 className="size-5 text-emerald-500" />,
34
+ warning: <AlertCircle className="size-5 text-amber-500" />,
35
+ error: <XCircle className="size-5 text-destructive" />,
36
+ };
37
+
38
+ function fire(
39
+ type: NotificationType,
40
+ { title, description, ...rest }: NotificationOptions,
41
+ ) {
42
+ return toast.custom(
43
+ (id) => (
44
+ <div
45
+ role="alert"
46
+ className={cn(
47
+ 'flex w-[360px] gap-3 rounded-md border bg-background p-4 text-sm shadow-lg',
48
+ )}
49
+ >
50
+ <span className="mt-0.5 shrink-0">{iconMap[type]}</span>
51
+ <div className="min-w-0 flex-1">
52
+ <div className="font-semibold leading-tight">{title}</div>
53
+ {description ? (
54
+ <div className="mt-1 text-muted-foreground">{description}</div>
55
+ ) : null}
56
+ </div>
57
+ <button
58
+ type="button"
59
+ aria-label="关闭"
60
+ onClick={() => toast.dismiss(id)}
61
+ className="text-muted-foreground transition-colors hover:text-foreground"
62
+ >
63
+ ×
64
+ </button>
65
+ </div>
66
+ ),
67
+ rest,
68
+ );
69
+ }
70
+
71
+ const notification = {
72
+ /**
73
+ * 通用打开 — 通过 `type` 区分状态;若只传 title,样式同 `info`。
74
+ */
75
+ open: (opts: NotificationOptions) => fire(opts.type ?? 'info', opts),
76
+ info: (opts: Omit<NotificationOptions, 'type'>) => fire('info', opts),
77
+ success: (opts: Omit<NotificationOptions, 'type'>) => fire('success', opts),
78
+ warning: (opts: Omit<NotificationOptions, 'type'>) => fire('warning', opts),
79
+ error: (opts: Omit<NotificationOptions, 'type'>) => fire('error', opts),
80
+ /** 主动关闭某条(传 `id`)/ 全部(不传)。 */
81
+ destroy: (id?: string | number) => toast.dismiss(id),
82
+ };
83
+
84
+ export { notification };
@@ -0,0 +1,127 @@
1
+ ---
2
+ id: pagination
3
+ name: Pagination
4
+ type: component
5
+ category: navigation
6
+ since: 0.1.0
7
+ package: "@teamix-evo/ui"
8
+ ---
9
+
10
+ # Pagination
11
+
12
+ 分页 — 两套 API:
13
+ 1. **shadcn primitives**(`Pagination / Content / Item / Link / Previous / Next / Ellipsis`)— 完全可组合,适合自定义复杂分页布局
14
+ 2. **`SimplePagination`**(antd 风格 props 化高阶包装)— `total / pageSize / current / onChange` 即用,**自动计算页码窗口 + 折叠 ellipsis + 显示总数**
15
+
16
+ ## When to use
17
+
18
+ - 列表 / 表格分页(配 Data Table)
19
+ - 长内容多页阅读
20
+ - 搜索结果分页
21
+
22
+ ## When NOT to use
23
+
24
+ - 无限滚动 → IntersectionObserver + `hasMore` flag
25
+ - 总数小(< 1 页)→ 不显示
26
+
27
+ ## Props
28
+
29
+ > 以下表格由 `pnpm --filter @teamix-evo/ui gen:meta` 自动生成。下表是 `SimplePagination` 的 props(高阶 API);primitives 的 props 详见 [`pagination.tsx`](./pagination.tsx)。
30
+
31
+ <!-- auto:props:begin -->
32
+ | 名称 | 类型 | 默认值 | 必填 | 说明 |
33
+ | --- | --- | --- | --- | --- |
34
+ | `current` | `number` | – | – | 当前页(1-based)。 |
35
+ | `onChange` | `(page: number) => void` | – | – | 受控变化回调。 |
36
+ | `total` | `number` | – | ✓ | 总条数。 |
37
+ | `pageSize` | `number` | `10` | – | 每页大小。 |
38
+ | `showFirstLast` | `boolean` | `false` | – | 是否显示首末页快捷按钮。 |
39
+ | `showTotal` | `boolean` | `true` | – | 是否显示总数文本(antd `showTotal` 并集,简化版)。 |
40
+ | `totalRender` | `(total: number, range: [number, number]) => React.ReactNode` | – | – | 自定义总数渲染。返回字符串或 ReactNode。 |
41
+ | `siblingCount` | `number` | `5` | – | 显示页号窗口大小(中间显示几个页号)。 |
42
+ | `className` | `string` | – | – | 容器 className。 |
43
+ <!-- auto:props:end -->
44
+
45
+ ## 依赖
46
+
47
+ > 以下表格由 `pnpm --filter @teamix-evo/ui gen:meta` 自动生成,数据源是 [`manifest.json`](../../../manifest.json)。**手工编辑 marker 之间的内容会在下次生成时被覆盖**。
48
+
49
+ <!-- auto:deps:begin -->
50
+ ### 同库依赖
51
+
52
+ > `teamix-evo ui add pagination` 时,以下 entry 会被自动连带安装(无需手动 add)。
53
+
54
+ | Entry | 类型 | 描述 |
55
+ | --- | --- | --- |
56
+ | `cn` | util | Tailwind className 合并工具(clsx + tailwind-merge) |
57
+ | `button` | component | 通用按钮 — shadcn 实现 + antd 功能扩展(loading / icon / shape / block / dashed variant) |
58
+
59
+ ### npm 依赖
60
+
61
+ > 业务侧需要先 `pnpm add` / `npm install` 这些包。CLI 在 `ui add` 完成后会列出此提示。
62
+
63
+ ```bash
64
+ pnpm add lucide-react@^0.460.0
65
+ ```
66
+ <!-- auto:deps:end -->
67
+
68
+ > 子组件(primitives):`Pagination`(nav)/ `PaginationContent`(ul)/ `PaginationItem`(li)/ `PaginationLink`(支持 `isActive`)/ `PaginationPrevious` / `PaginationNext` / `PaginationEllipsis`。
69
+
70
+ ## AI 生成纪律
71
+
72
+ - **业务侧用 `SimplePagination`**:除非要完全自定义布局,默认走高阶 API
73
+ - **当前页 1-based**:antd 惯例,与 0-based 数组索引区分清楚
74
+ - **`pageSize` 默认 10**:与 antd / Web 主流一致
75
+ - **`siblingCount` 控制中间窗口**:奇数(5/7)效果好,偶数会产生不对称
76
+ - **`showSizeChanger` / `showQuickJumper` 在 v0.x**:本版不内置(简化 props),需要时业务侧组合 Select + Input
77
+ - **不要嵌套 Pagination**:同一容器只一组分页
78
+
79
+ ## Examples
80
+
81
+ ```tsx
82
+ import { SimplePagination } from '@/components/ui/pagination';
83
+ import * as React from 'react';
84
+
85
+ // 高阶 API(推荐)
86
+ const [page, setPage] = React.useState(1);
87
+ <SimplePagination
88
+ total={245}
89
+ current={page}
90
+ pageSize={10}
91
+ onChange={setPage}
92
+ />
93
+
94
+ // 自定义总数文本
95
+ <SimplePagination
96
+ total={245}
97
+ pageSize={10}
98
+ current={page}
99
+ onChange={setPage}
100
+ totalRender={(t, [s, e]) => `${s}-${e} of ${t}`}
101
+ />
102
+
103
+ // 显示首末快捷
104
+ <SimplePagination total={500} current={page} onChange={setPage} showFirstLast />
105
+
106
+ // 不显示总数
107
+ <SimplePagination total={245} current={page} onChange={setPage} showTotal={false} />
108
+ ```
109
+
110
+ ```tsx
111
+ // 自由组合(primitives)
112
+ import {
113
+ Pagination, PaginationContent, PaginationItem, PaginationLink,
114
+ PaginationPrevious, PaginationNext, PaginationEllipsis,
115
+ } from '@/components/ui/pagination';
116
+
117
+ <Pagination>
118
+ <PaginationContent>
119
+ <PaginationItem><PaginationPrevious href="#" /></PaginationItem>
120
+ <PaginationItem><PaginationLink href="#">1</PaginationLink></PaginationItem>
121
+ <PaginationItem><PaginationLink href="#" isActive>2</PaginationLink></PaginationItem>
122
+ <PaginationItem><PaginationLink href="#">3</PaginationLink></PaginationItem>
123
+ <PaginationItem><PaginationEllipsis /></PaginationItem>
124
+ <PaginationItem><PaginationNext href="#" /></PaginationItem>
125
+ </PaginationContent>
126
+ </Pagination>
127
+ ```
@@ -0,0 +1,62 @@
1
+ import * as React from 'react';
2
+ import type { Meta, StoryObj } from '@storybook/react';
3
+ import { SimplePagination } from './pagination';
4
+
5
+ const meta: Meta<typeof SimplePagination> = {
6
+ title: '导航 · Navigation/Pagination',
7
+ component: SimplePagination,
8
+ tags: ['autodocs'],
9
+ argTypes: {
10
+ total: { control: 'number' },
11
+ pageSize: { control: 'number' },
12
+ siblingCount: { control: 'number' },
13
+ showFirstLast: { control: 'boolean' },
14
+ showTotal: { control: 'boolean' },
15
+ },
16
+ args: { total: 245, pageSize: 10, siblingCount: 5 },
17
+ decorators: [(Story) => <div className="w-[640px]">{Story()}</div>],
18
+ };
19
+
20
+ export default meta;
21
+ type Story = StoryObj<typeof SimplePagination>;
22
+
23
+ export const Playground: Story = {
24
+ render: (args) => {
25
+ const [page, setPage] = React.useState(args.current ?? 1);
26
+ return (
27
+ <SimplePagination {...args} current={page} onChange={setPage} />
28
+ );
29
+ },
30
+ };
31
+
32
+ export const FirstLast: Story = {
33
+ parameters: { controls: { disable: true } },
34
+ render: () => {
35
+ const [page, setPage] = React.useState(15);
36
+ return (
37
+ <SimplePagination
38
+ total={500}
39
+ pageSize={10}
40
+ current={page}
41
+ onChange={setPage}
42
+ showFirstLast
43
+ />
44
+ );
45
+ },
46
+ };
47
+
48
+ export const CustomTotal: Story = {
49
+ parameters: { controls: { disable: true } },
50
+ render: () => {
51
+ const [page, setPage] = React.useState(1);
52
+ return (
53
+ <SimplePagination
54
+ total={245}
55
+ pageSize={10}
56
+ current={page}
57
+ onChange={setPage}
58
+ totalRender={(t, [s, e]) => `${s}–${e} of ${t}`}
59
+ />
60
+ );
61
+ },
62
+ };
@@ -0,0 +1,285 @@
1
+ import * as React from 'react';
2
+ import {
3
+ ChevronLeft,
4
+ ChevronRight,
5
+ ChevronsLeft,
6
+ ChevronsRight,
7
+ MoreHorizontal,
8
+ } from 'lucide-react';
9
+
10
+ import { cn } from '@/utils/cn';
11
+ import { Button, type ButtonProps } from '@/components/button/button';
12
+
13
+ // ─── shadcn-style primitives ──────────────────────────────────────────────────
14
+
15
+ const Pagination = ({
16
+ className,
17
+ ...props
18
+ }: React.ComponentProps<'nav'>) => (
19
+ <nav
20
+ role="navigation"
21
+ aria-label="pagination"
22
+ className={cn('mx-auto flex w-full justify-center', className)}
23
+ {...props}
24
+ />
25
+ );
26
+ Pagination.displayName = 'Pagination';
27
+
28
+ const PaginationContent = React.forwardRef<
29
+ HTMLUListElement,
30
+ React.ComponentProps<'ul'>
31
+ >(({ className, ...props }, ref) => (
32
+ <ul
33
+ ref={ref}
34
+ className={cn('flex flex-row items-center gap-1', className)}
35
+ {...props}
36
+ />
37
+ ));
38
+ PaginationContent.displayName = 'PaginationContent';
39
+
40
+ const PaginationItem = React.forwardRef<
41
+ HTMLLIElement,
42
+ React.ComponentProps<'li'>
43
+ >(({ className, ...props }, ref) => (
44
+ <li ref={ref} className={cn('', className)} {...props} />
45
+ ));
46
+ PaginationItem.displayName = 'PaginationItem';
47
+
48
+ interface PaginationLinkProps
49
+ extends Pick<ButtonProps, 'size'>,
50
+ React.ComponentProps<'button'> {
51
+ isActive?: boolean;
52
+ }
53
+
54
+ const PaginationLink = ({
55
+ className,
56
+ isActive,
57
+ size = 'icon',
58
+ ...props
59
+ }: PaginationLinkProps) => (
60
+ <Button
61
+ aria-current={isActive ? 'page' : undefined}
62
+ variant={isActive ? 'outline' : 'ghost'}
63
+ size={size}
64
+ className={cn(className)}
65
+ {...props}
66
+ />
67
+ );
68
+ PaginationLink.displayName = 'PaginationLink';
69
+
70
+ const PaginationPrevious = ({
71
+ className,
72
+ ...props
73
+ }: React.ComponentProps<typeof PaginationLink>) => (
74
+ <PaginationLink
75
+ aria-label="Go to previous page"
76
+ size="default"
77
+ className={cn('gap-1 pl-2.5', className)}
78
+ {...props}
79
+ >
80
+ <ChevronLeft className="size-4" />
81
+ <span>上一页</span>
82
+ </PaginationLink>
83
+ );
84
+ PaginationPrevious.displayName = 'PaginationPrevious';
85
+
86
+ const PaginationNext = ({
87
+ className,
88
+ ...props
89
+ }: React.ComponentProps<typeof PaginationLink>) => (
90
+ <PaginationLink
91
+ aria-label="Go to next page"
92
+ size="default"
93
+ className={cn('gap-1 pr-2.5', className)}
94
+ {...props}
95
+ >
96
+ <span>下一页</span>
97
+ <ChevronRight className="size-4" />
98
+ </PaginationLink>
99
+ );
100
+ PaginationNext.displayName = 'PaginationNext';
101
+
102
+ const PaginationEllipsis = ({
103
+ className,
104
+ ...props
105
+ }: React.ComponentProps<'span'>) => (
106
+ <span
107
+ aria-hidden="true"
108
+ className={cn('flex size-9 items-center justify-center', className)}
109
+ {...props}
110
+ >
111
+ <MoreHorizontal className="size-4" />
112
+ <span className="sr-only">More pages</span>
113
+ </span>
114
+ );
115
+ PaginationEllipsis.displayName = 'PaginationEllipsis';
116
+
117
+ // ─── Higher-level wrapper(antd 风格 props 化分页器)───────────────────────
118
+
119
+ export interface SimplePaginationProps {
120
+ /** 当前页(1-based)。 */
121
+ current?: number;
122
+ /** 受控变化回调。 */
123
+ onChange?: (page: number) => void;
124
+ /** 总条数。 */
125
+ total: number;
126
+ /** 每页大小。 @default 10 */
127
+ pageSize?: number;
128
+ /** 是否显示首末页快捷按钮。 @default false */
129
+ showFirstLast?: boolean;
130
+ /** 是否显示总数文本(antd `showTotal` 并集,简化版)。 @default true */
131
+ showTotal?: boolean;
132
+ /** 自定义总数渲染。返回字符串或 ReactNode。 */
133
+ totalRender?: (total: number, range: [number, number]) => React.ReactNode;
134
+ /** 显示页号窗口大小(中间显示几个页号)。 @default 5 */
135
+ siblingCount?: number;
136
+ /** 容器 className。 */
137
+ className?: string;
138
+ }
139
+
140
+ /**
141
+ * 高阶 Pagination — antd 风格 `total / pageSize / current / onChange` props 化用法,
142
+ * 自动计算页码窗口 + 折叠 ellipsis。
143
+ *
144
+ * 内部组合 `Pagination / PaginationContent / PaginationItem / PaginationLink /
145
+ * PaginationPrevious / PaginationNext / PaginationEllipsis` 这套 shadcn primitives,
146
+ * 业务方需要更细粒度时直接使用 primitives 自行拼装。
147
+ */
148
+ const SimplePagination = ({
149
+ current = 1,
150
+ onChange,
151
+ total,
152
+ pageSize = 10,
153
+ showFirstLast = false,
154
+ showTotal = true,
155
+ totalRender,
156
+ siblingCount = 5,
157
+ className,
158
+ }: SimplePaginationProps) => {
159
+ const totalPages = Math.max(1, Math.ceil(total / pageSize));
160
+ const page = Math.min(Math.max(1, current), totalPages);
161
+
162
+ const pages = React.useMemo(() => {
163
+ const half = Math.floor(siblingCount / 2);
164
+ let start = Math.max(1, page - half);
165
+ let end = Math.min(totalPages, start + siblingCount - 1);
166
+ if (end - start + 1 < siblingCount) {
167
+ start = Math.max(1, end - siblingCount + 1);
168
+ }
169
+ const arr: number[] = [];
170
+ for (let i = start; i <= end; i++) arr.push(i);
171
+ return arr;
172
+ }, [page, siblingCount, totalPages]);
173
+
174
+ const goto = (n: number) => {
175
+ if (n < 1 || n > totalPages || n === page) return;
176
+ onChange?.(n);
177
+ };
178
+
179
+ const rangeStart = total === 0 ? 0 : (page - 1) * pageSize + 1;
180
+ const rangeEnd = Math.min(page * pageSize, total);
181
+
182
+ const showLeftEllipsis = pages[0]! > 2;
183
+ const showRightEllipsis = pages[pages.length - 1]! < totalPages - 1;
184
+
185
+ return (
186
+ <div
187
+ className={cn(
188
+ 'flex w-full flex-wrap items-center justify-between gap-3',
189
+ className,
190
+ )}
191
+ >
192
+ {showTotal ? (
193
+ <p className="text-sm text-muted-foreground tabular-nums">
194
+ {totalRender
195
+ ? totalRender(total, [rangeStart, rangeEnd])
196
+ : `共 ${total} 条 · 第 ${rangeStart}-${rangeEnd} 条`}
197
+ </p>
198
+ ) : (
199
+ <span />
200
+ )}
201
+ <Pagination className="mx-0 w-auto">
202
+ <PaginationContent>
203
+ {showFirstLast ? (
204
+ <PaginationItem>
205
+ <PaginationLink
206
+ aria-label="First"
207
+ disabled={page === 1}
208
+ onClick={() => goto(1)}
209
+ >
210
+ <ChevronsLeft className="size-4" />
211
+ </PaginationLink>
212
+ </PaginationItem>
213
+ ) : null}
214
+ <PaginationItem>
215
+ <PaginationPrevious
216
+ disabled={page === 1}
217
+ onClick={() => goto(page - 1)}
218
+ />
219
+ </PaginationItem>
220
+ {pages[0]! > 1 ? (
221
+ <PaginationItem>
222
+ <PaginationLink onClick={() => goto(1)}>1</PaginationLink>
223
+ </PaginationItem>
224
+ ) : null}
225
+ {showLeftEllipsis ? (
226
+ <PaginationItem>
227
+ <PaginationEllipsis />
228
+ </PaginationItem>
229
+ ) : null}
230
+ {pages.map((p) => (
231
+ <PaginationItem key={p}>
232
+ <PaginationLink
233
+ isActive={p === page}
234
+ onClick={() => goto(p)}
235
+ >
236
+ {p}
237
+ </PaginationLink>
238
+ </PaginationItem>
239
+ ))}
240
+ {showRightEllipsis ? (
241
+ <PaginationItem>
242
+ <PaginationEllipsis />
243
+ </PaginationItem>
244
+ ) : null}
245
+ {pages[pages.length - 1]! < totalPages ? (
246
+ <PaginationItem>
247
+ <PaginationLink onClick={() => goto(totalPages)}>
248
+ {totalPages}
249
+ </PaginationLink>
250
+ </PaginationItem>
251
+ ) : null}
252
+ <PaginationItem>
253
+ <PaginationNext
254
+ disabled={page === totalPages}
255
+ onClick={() => goto(page + 1)}
256
+ />
257
+ </PaginationItem>
258
+ {showFirstLast ? (
259
+ <PaginationItem>
260
+ <PaginationLink
261
+ aria-label="Last"
262
+ disabled={page === totalPages}
263
+ onClick={() => goto(totalPages)}
264
+ >
265
+ <ChevronsRight className="size-4" />
266
+ </PaginationLink>
267
+ </PaginationItem>
268
+ ) : null}
269
+ </PaginationContent>
270
+ </Pagination>
271
+ </div>
272
+ );
273
+ };
274
+ SimplePagination.displayName = 'SimplePagination';
275
+
276
+ export {
277
+ Pagination,
278
+ PaginationContent,
279
+ PaginationItem,
280
+ PaginationLink,
281
+ PaginationPrevious,
282
+ PaginationNext,
283
+ PaginationEllipsis,
284
+ SimplePagination,
285
+ };