@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,251 @@
1
+ import * as React from 'react';
2
+ import { ChevronLeft, ChevronRight, Search } from 'lucide-react';
3
+
4
+ import { cn } from '@/utils/cn';
5
+ import { Button } from '@/components/button/button';
6
+ import { Checkbox } from '@/components/checkbox/checkbox';
7
+ import { Input } from '@/components/input/input';
8
+
9
+ export interface TransferItem {
10
+ /** 稳定 key — `targetKeys` 比对依据。 */
11
+ key: string;
12
+ /** 显示文本。 */
13
+ title: React.ReactNode;
14
+ /** 是否禁用(两侧均不可勾选 / 移动)。 */
15
+ disabled?: boolean;
16
+ }
17
+
18
+ export interface TransferProps {
19
+ /** 全部数据源(左+右合集)。 */
20
+ dataSource: TransferItem[];
21
+ /** 受控:已"穿梭到右侧"的 key 集合(antd `targetKeys` 并集)。 */
22
+ targetKeys: string[];
23
+ /** 右侧集合变化回调。 */
24
+ onChange?: (next: string[]) => void;
25
+ /**
26
+ * 两侧标题(antd `titles` 并集) — `[左, 右]`。
27
+ * @default ["源", "目标"]
28
+ */
29
+ titles?: [React.ReactNode, React.ReactNode];
30
+ /**
31
+ * 启用搜索(antd `showSearch` 并集) — 两侧各一个输入框。
32
+ * @default false
33
+ */
34
+ showSearch?: boolean;
35
+ /**
36
+ * 自定义渲染单项(antd `render` 并集) — 默认渲染 `item.title`。
37
+ */
38
+ render?: (item: TransferItem) => React.ReactNode;
39
+ /** 整体禁用。 */
40
+ disabled?: boolean;
41
+ /** 单侧高度(像素)。 @default 280 */
42
+ listHeight?: number;
43
+ className?: string;
44
+ }
45
+
46
+ const Transfer = React.forwardRef<HTMLDivElement, TransferProps>(
47
+ (
48
+ {
49
+ dataSource,
50
+ targetKeys,
51
+ onChange,
52
+ titles = ['源', '目标'],
53
+ showSearch = false,
54
+ render,
55
+ disabled = false,
56
+ listHeight = 280,
57
+ className,
58
+ },
59
+ ref,
60
+ ) => {
61
+ const [leftChecked, setLeftChecked] = React.useState<string[]>([]);
62
+ const [rightChecked, setRightChecked] = React.useState<string[]>([]);
63
+ const [leftQuery, setLeftQuery] = React.useState('');
64
+ const [rightQuery, setRightQuery] = React.useState('');
65
+
66
+ const targetSet = React.useMemo(() => new Set(targetKeys), [targetKeys]);
67
+
68
+ const leftItems = React.useMemo(
69
+ () => dataSource.filter((i) => !targetSet.has(i.key)),
70
+ [dataSource, targetSet],
71
+ );
72
+ const rightItems = React.useMemo(
73
+ () => dataSource.filter((i) => targetSet.has(i.key)),
74
+ [dataSource, targetSet],
75
+ );
76
+
77
+ const filterFn = (q: string) => (item: TransferItem) =>
78
+ q === '' ||
79
+ (typeof item.title === 'string'
80
+ ? item.title.toLowerCase().includes(q.toLowerCase())
81
+ : true);
82
+
83
+ const leftVisible = leftItems.filter(filterFn(leftQuery));
84
+ const rightVisible = rightItems.filter(filterFn(rightQuery));
85
+
86
+ // Strip selections that no longer apply when an item moves sides.
87
+ React.useEffect(() => {
88
+ setLeftChecked((prev) => prev.filter((k) => !targetSet.has(k)));
89
+ setRightChecked((prev) => prev.filter((k) => targetSet.has(k)));
90
+ }, [targetSet]);
91
+
92
+ const moveRight = () => {
93
+ if (disabled) return;
94
+ const movable = leftChecked.filter((k) => {
95
+ const it = dataSource.find((i) => i.key === k);
96
+ return it && !it.disabled;
97
+ });
98
+ if (movable.length === 0) return;
99
+ onChange?.([...targetKeys, ...movable]);
100
+ setLeftChecked([]);
101
+ };
102
+
103
+ const moveLeft = () => {
104
+ if (disabled) return;
105
+ const movable = rightChecked.filter((k) => {
106
+ const it = dataSource.find((i) => i.key === k);
107
+ return it && !it.disabled;
108
+ });
109
+ if (movable.length === 0) return;
110
+ onChange?.(targetKeys.filter((k) => !movable.includes(k)));
111
+ setRightChecked([]);
112
+ };
113
+
114
+ const renderPane = (
115
+ title: React.ReactNode,
116
+ items: TransferItem[],
117
+ visible: TransferItem[],
118
+ checked: string[],
119
+ setChecked: React.Dispatch<React.SetStateAction<string[]>>,
120
+ query: string,
121
+ setQuery: (v: string) => void,
122
+ ) => {
123
+ const selectableKeys = visible.filter((i) => !i.disabled).map((i) => i.key);
124
+ const allChecked =
125
+ selectableKeys.length > 0 && selectableKeys.every((k) => checked.includes(k));
126
+ const someChecked =
127
+ !allChecked && selectableKeys.some((k) => checked.includes(k));
128
+ const toggleAll = (value: boolean) => {
129
+ if (value) setChecked(Array.from(new Set([...checked, ...selectableKeys])));
130
+ else setChecked(checked.filter((k) => !selectableKeys.includes(k)));
131
+ };
132
+
133
+ return (
134
+ <div className="flex w-56 flex-col overflow-hidden rounded-md border bg-card text-sm">
135
+ <div className="flex items-center justify-between gap-2 border-b px-3 py-2">
136
+ <label className="flex items-center gap-2">
137
+ <Checkbox
138
+ checked={allChecked ? true : someChecked ? 'indeterminate' : false}
139
+ disabled={disabled || selectableKeys.length === 0}
140
+ onCheckedChange={(c) => toggleAll(c === true)}
141
+ />
142
+ <span className="text-xs text-muted-foreground">
143
+ {checked.length}/{items.length}
144
+ </span>
145
+ </label>
146
+ <span className="text-xs font-medium">{title}</span>
147
+ </div>
148
+ {showSearch ? (
149
+ <div className="border-b p-2">
150
+ <div className="relative">
151
+ <Search className="absolute left-2 top-1/2 size-3.5 -translate-y-1/2 text-muted-foreground" />
152
+ <Input
153
+ value={query}
154
+ onChange={(e) => setQuery(e.target.value)}
155
+ placeholder="搜索..."
156
+ className="h-8 pl-7 text-xs"
157
+ disabled={disabled}
158
+ />
159
+ </div>
160
+ </div>
161
+ ) : null}
162
+ <ul
163
+ className="flex-1 overflow-auto py-1"
164
+ style={{ height: listHeight }}
165
+ >
166
+ {visible.length === 0 ? (
167
+ <li className="px-3 py-6 text-center text-xs text-muted-foreground">
168
+ 暂无数据
169
+ </li>
170
+ ) : (
171
+ visible.map((item) => {
172
+ const isChecked = checked.includes(item.key);
173
+ const itemDisabled = disabled || item.disabled;
174
+ return (
175
+ <li key={item.key}>
176
+ <label
177
+ className={cn(
178
+ 'flex items-center gap-2 px-3 py-1.5',
179
+ !itemDisabled && 'cursor-pointer hover:bg-accent',
180
+ itemDisabled && 'cursor-not-allowed opacity-50',
181
+ )}
182
+ >
183
+ <Checkbox
184
+ checked={isChecked}
185
+ disabled={itemDisabled}
186
+ onCheckedChange={(c) => {
187
+ if (c === true) setChecked([...checked, item.key]);
188
+ else setChecked(checked.filter((k) => k !== item.key));
189
+ }}
190
+ />
191
+ <span className="min-w-0 flex-1 truncate">
192
+ {render ? render(item) : item.title}
193
+ </span>
194
+ </label>
195
+ </li>
196
+ );
197
+ })
198
+ )}
199
+ </ul>
200
+ </div>
201
+ );
202
+ };
203
+
204
+ return (
205
+ <div
206
+ ref={ref}
207
+ className={cn('flex items-center gap-3', className)}
208
+ >
209
+ {renderPane(
210
+ titles[0],
211
+ leftItems,
212
+ leftVisible,
213
+ leftChecked,
214
+ setLeftChecked,
215
+ leftQuery,
216
+ setLeftQuery,
217
+ )}
218
+ <div className="flex flex-col gap-2">
219
+ <Button
220
+ size="icon"
221
+ variant="outline"
222
+ disabled={disabled || leftChecked.length === 0}
223
+ onClick={moveRight}
224
+ aria-label="移动到右侧"
225
+ icon={<ChevronRight />}
226
+ />
227
+ <Button
228
+ size="icon"
229
+ variant="outline"
230
+ disabled={disabled || rightChecked.length === 0}
231
+ onClick={moveLeft}
232
+ aria-label="移动到左侧"
233
+ icon={<ChevronLeft />}
234
+ />
235
+ </div>
236
+ {renderPane(
237
+ titles[1],
238
+ rightItems,
239
+ rightVisible,
240
+ rightChecked,
241
+ setRightChecked,
242
+ rightQuery,
243
+ setRightQuery,
244
+ )}
245
+ </div>
246
+ );
247
+ },
248
+ );
249
+ Transfer.displayName = 'Transfer';
250
+
251
+ export { Transfer };
@@ -0,0 +1,111 @@
1
+ ---
2
+ id: tree
3
+ name: Tree
4
+ type: component
5
+ category: data-display
6
+ since: 0.1.0
7
+ package: "@teamix-evo/ui"
8
+ ---
9
+
10
+ # Tree
11
+
12
+ 树形控件 — antd 独有补足。递归层级展示(组织架构、文件系统、分类目录),支持**可展开 / 可单选 / 可勾选(父子级联 + 半选)**,受控与非受控并存。Radix 未提供 tree primitive — 本组件自实现递归 + 受控逻辑。
13
+
14
+ ## When to use
15
+
16
+ - 层级数据浏览(组织、目录、分类、JSON 结构)
17
+ - 配 `checkable` 让用户多选叶子(权限分配、批量目录选)
18
+ - 配 `selectable` 实现"选中一个节点查看右侧详情"主从布局
19
+
20
+ ## When NOT to use
21
+
22
+ - 一层无层级 → `List` / `Menu`
23
+ - 大数据量(> 1000 节点)— 本组件不做虚拟化,业务侧请自做分页 / 懒加载子节点
24
+ - 拖拽排序场景 → 需配合 `react-dnd` 等库自行扩展,本组件不内置
25
+
26
+ <!-- auto:props:begin -->
27
+ | 名称 | 类型 | 默认值 | 必填 | 说明 |
28
+ | --- | --- | --- | --- | --- |
29
+ | `data` | `TreeNode[]` | – | ✓ | 树数据(antd `treeData` 并集)。 |
30
+ | `checkable` | `boolean` | `false` | – | 是否启用勾选 checkbox(antd `checkable` 并集) — 父子级联,半选状态自动计算。 |
31
+ | `selectable` | `boolean` | `true` | – | 是否启用单击高亮选择(antd `selectable` 并集)。 |
32
+ | `checkedKeys` | `string[]` | – | – | 受控已勾选 key 集合(checkable 时生效)。 |
33
+ | `defaultCheckedKeys` | `string[]` | – | – | uncontrolled 初值。 |
34
+ | `onCheck` | `(next: string[]) => void` | – | – | 勾选变化回调。 |
35
+ | `selectedKeys` | `string[]` | – | – | 受控已选中 key 集合(selectable 时生效;通常单选,数组形式与 antd 对齐)。 |
36
+ | `defaultSelectedKeys` | `string[]` | – | – | uncontrolled 初值。 |
37
+ | `onSelect` | `(next: string[]) => void` | – | – | 选中变化回调。 |
38
+ | `expandedKeys` | `string[]` | – | – | 受控已展开 key 集合。 |
39
+ | `defaultExpandedKeys` | `string[]` | – | – | uncontrolled 初值。 |
40
+ | `defaultExpandAll` | `boolean` | `false` | – | 默认展开全部(antd `defaultExpandAll` 并集) — 仅 uncontrolled 模式生效。 |
41
+ | `onExpand` | `(next: string[]) => void` | – | – | 展开变化回调。 |
42
+ <!-- auto:props:end -->
43
+
44
+ <!-- auto:deps:begin -->
45
+ ### 同库依赖
46
+
47
+ > `teamix-evo ui add tree` 时,以下 entry 会被自动连带安装(无需手动 add)。
48
+
49
+ | Entry | 类型 | 描述 |
50
+ | --- | --- | --- |
51
+ | `cn` | util | Tailwind className 合并工具(clsx + tailwind-merge) |
52
+ | `checkbox` | component | 复选框 — Radix Checkbox(原生 indeterminate)+ antd Checkbox.Group(options 数组驱动) |
53
+
54
+ ### npm 依赖
55
+
56
+ > 业务侧需要先 `pnpm add` / `npm install` 这些包。CLI 在 `ui add` 完成后会列出此提示。
57
+
58
+ ```bash
59
+ pnpm add lucide-react@^0.460.0
60
+ ```
61
+ <!-- auto:deps:end -->
62
+
63
+ ## AI 生成纪律
64
+
65
+ - **`key` 必须 stable**(用数据库 id,不要用 index / 随机串) — `expandedKeys` / `checkedKeys` / `selectedKeys` 比对全靠它
66
+ - **`checkable` 默认级联**:勾选父节点会勾选所有后代,勾选所有后代会自动勾选父节点 — **不要**自己写额外的级联逻辑;`onCheck` 给的是叶子+完全选中的父节点集合(半选不在内)
67
+ - **半选(indeterminate)**自动计算,不需要消费方维护
68
+ - **`selectable` + `checkable` 可共存**,但 UI 上选中高亮与勾选是两套独立状态(对齐 antd)
69
+ - **大数据量警告**:节点数 ≥ 500 时建议拆分为"按需加载子节点"(在 `onExpand` 中拉取),否则首次渲染卡顿
70
+ - **`disabled` 节点**:展开按钮亦禁用,但**仍参与级联计算** — 父被勾选时禁用子也会变为 checked
71
+
72
+ ## Examples
73
+
74
+ ```tsx
75
+ import { Tree, type TreeNode } from '@/components/ui/tree';
76
+ import { Folder, File } from 'lucide-react';
77
+ import * as React from 'react';
78
+
79
+ const data: TreeNode[] = [
80
+ {
81
+ key: 'src',
82
+ title: 'src',
83
+ icon: <Folder className="size-3.5" />,
84
+ children: [
85
+ { key: 'src/index.ts', title: 'index.ts', icon: <File className="size-3.5" /> },
86
+ {
87
+ key: 'src/components',
88
+ title: 'components',
89
+ icon: <Folder className="size-3.5" />,
90
+ children: [
91
+ { key: 'src/components/Button.tsx', title: 'Button.tsx' },
92
+ { key: 'src/components/Input.tsx', title: 'Input.tsx' },
93
+ ],
94
+ },
95
+ ],
96
+ },
97
+ ];
98
+
99
+ // 默认展开 + 选择
100
+ <Tree data={data} defaultExpandAll />
101
+
102
+ // 可勾选(受控)
103
+ const [checked, setChecked] = React.useState<string[]>([]);
104
+ <Tree
105
+ data={data}
106
+ checkable
107
+ defaultExpandAll
108
+ checkedKeys={checked}
109
+ onCheck={setChecked}
110
+ />
111
+ ```
@@ -0,0 +1,109 @@
1
+ import * as React from 'react';
2
+ import type { Meta, StoryObj } from '@storybook/react';
3
+ import { File, Folder } from 'lucide-react';
4
+ import { Tree, type TreeNode } from './tree';
5
+
6
+ const meta: Meta<typeof Tree> = {
7
+ title: '数据展示 · Data Display/Tree',
8
+ component: Tree,
9
+ tags: ['autodocs'],
10
+ parameters: {
11
+ docs: {
12
+ description: {
13
+ component:
14
+ '树形控件 — 递归层级展示(目录、组织、分类)。支持展开 / 单选 / 父子级联勾选(自动半选),受控与非受控并存。Radix 未提供 tree primitive,本组件自实现递归与级联。视觉走 OpenTrek tokens,等价 antd `Tree`,所有样式来自 `@teamix-evo/design`,无 mock。',
15
+ },
16
+ },
17
+ },
18
+ };
19
+
20
+ export default meta;
21
+ type Story = StoryObj<typeof Tree>;
22
+
23
+ const fileTree: TreeNode[] = [
24
+ {
25
+ key: 'src',
26
+ title: 'src',
27
+ icon: <Folder className="size-3.5" />,
28
+ children: [
29
+ { key: 'src/index.ts', title: 'index.ts', icon: <File className="size-3.5" /> },
30
+ {
31
+ key: 'src/components',
32
+ title: 'components',
33
+ icon: <Folder className="size-3.5" />,
34
+ children: [
35
+ { key: 'src/components/Button.tsx', title: 'Button.tsx', icon: <File className="size-3.5" /> },
36
+ { key: 'src/components/Input.tsx', title: 'Input.tsx', icon: <File className="size-3.5" /> },
37
+ { key: 'src/components/Dialog.tsx', title: 'Dialog.tsx', icon: <File className="size-3.5" />, disabled: true },
38
+ ],
39
+ },
40
+ {
41
+ key: 'src/utils',
42
+ title: 'utils',
43
+ icon: <Folder className="size-3.5" />,
44
+ children: [{ key: 'src/utils/cn.ts', title: 'cn.ts', icon: <File className="size-3.5" /> }],
45
+ },
46
+ ],
47
+ },
48
+ { key: 'README.md', title: 'README.md', icon: <File className="size-3.5" /> },
49
+ ];
50
+
51
+ export const Playground: Story = {
52
+ parameters: { controls: { disable: true } },
53
+ render: () => <Tree data={fileTree} defaultExpandAll />,
54
+ };
55
+
56
+ export const Checkable: Story = {
57
+ parameters: { controls: { disable: true } },
58
+ render: () => {
59
+ const [checked, setChecked] = React.useState<string[]>([]);
60
+ return (
61
+ <div className="flex flex-col gap-3">
62
+ <Tree
63
+ data={fileTree}
64
+ checkable
65
+ defaultExpandAll
66
+ checkedKeys={checked}
67
+ onCheck={setChecked}
68
+ />
69
+ <div className="text-xs text-muted-foreground">
70
+ 已勾选: {checked.length === 0 ? '无' : checked.join(', ')}
71
+ </div>
72
+ </div>
73
+ );
74
+ },
75
+ };
76
+
77
+ export const Selectable: Story = {
78
+ parameters: { controls: { disable: true } },
79
+ render: () => {
80
+ const [selected, setSelected] = React.useState<string[]>([]);
81
+ return (
82
+ <div className="flex flex-col gap-3">
83
+ <Tree
84
+ data={fileTree}
85
+ defaultExpandAll
86
+ selectedKeys={selected}
87
+ onSelect={setSelected}
88
+ />
89
+ <div className="text-xs text-muted-foreground">
90
+ 已选中: {selected[0] ?? '无'}
91
+ </div>
92
+ </div>
93
+ );
94
+ },
95
+ };
96
+
97
+ export const ControlledExpand: Story = {
98
+ parameters: { controls: { disable: true } },
99
+ render: () => {
100
+ const [expanded, setExpanded] = React.useState<string[]>(['src']);
101
+ return (
102
+ <Tree
103
+ data={fileTree}
104
+ expandedKeys={expanded}
105
+ onExpand={setExpanded}
106
+ />
107
+ );
108
+ },
109
+ };