@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,367 @@
1
+ import * as React from 'react';
2
+ import { ChevronRight } from 'lucide-react';
3
+
4
+ import { cn } from '@/utils/cn';
5
+ import { Checkbox } from '@/components/checkbox/checkbox';
6
+
7
+ export interface TreeNode {
8
+ /** 稳定 key — 受控集合的比对依据。 */
9
+ key: string;
10
+ /** 节点显示内容。 */
11
+ title: React.ReactNode;
12
+ /** 子节点(无 children 视为叶子)。 */
13
+ children?: TreeNode[];
14
+ /** 禁用整行交互(选择 / 勾选 / 展开)。 */
15
+ disabled?: boolean;
16
+ /** 节点左侧图标(可选,叶子节点配 file 图标常用)。 */
17
+ icon?: React.ReactNode;
18
+ }
19
+
20
+ export interface TreeProps
21
+ extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onSelect'> {
22
+ /** 树数据(antd `treeData` 并集)。 */
23
+ data: TreeNode[];
24
+ /**
25
+ * 是否启用勾选 checkbox(antd `checkable` 并集) — 父子级联,半选状态自动计算。
26
+ * @default false
27
+ */
28
+ checkable?: boolean;
29
+ /**
30
+ * 是否启用单击高亮选择(antd `selectable` 并集)。
31
+ * @default true
32
+ */
33
+ selectable?: boolean;
34
+ /** 受控已勾选 key 集合(checkable 时生效)。 */
35
+ checkedKeys?: string[];
36
+ /** uncontrolled 初值。 */
37
+ defaultCheckedKeys?: string[];
38
+ /** 勾选变化回调。 */
39
+ onCheck?: (next: string[]) => void;
40
+ /** 受控已选中 key 集合(selectable 时生效;通常单选,数组形式与 antd 对齐)。 */
41
+ selectedKeys?: string[];
42
+ /** uncontrolled 初值。 */
43
+ defaultSelectedKeys?: string[];
44
+ /** 选中变化回调。 */
45
+ onSelect?: (next: string[]) => void;
46
+ /** 受控已展开 key 集合。 */
47
+ expandedKeys?: string[];
48
+ /** uncontrolled 初值。 */
49
+ defaultExpandedKeys?: string[];
50
+ /**
51
+ * 默认展开全部(antd `defaultExpandAll` 并集) — 仅 uncontrolled 模式生效。
52
+ * @default false
53
+ */
54
+ defaultExpandAll?: boolean;
55
+ /** 展开变化回调。 */
56
+ onExpand?: (next: string[]) => void;
57
+ }
58
+
59
+ // ─── helpers ──────────────────────────────────────────────────────────────
60
+
61
+ function collectAllKeys(nodes: TreeNode[]): string[] {
62
+ const out: string[] = [];
63
+ const walk = (n: TreeNode) => {
64
+ out.push(n.key);
65
+ n.children?.forEach(walk);
66
+ };
67
+ nodes.forEach(walk);
68
+ return out;
69
+ }
70
+
71
+ function collectDescendants(node: TreeNode): string[] {
72
+ const out: string[] = [];
73
+ const walk = (n: TreeNode) => {
74
+ out.push(n.key);
75
+ n.children?.forEach(walk);
76
+ };
77
+ node.children?.forEach(walk);
78
+ return out;
79
+ }
80
+
81
+ function buildParentMap(nodes: TreeNode[]): Map<string, string | null> {
82
+ const map = new Map<string, string | null>();
83
+ const walk = (n: TreeNode, parent: string | null) => {
84
+ map.set(n.key, parent);
85
+ n.children?.forEach((c) => walk(c, n.key));
86
+ };
87
+ nodes.forEach((n) => walk(n, null));
88
+ return map;
89
+ }
90
+
91
+ function buildNodeMap(nodes: TreeNode[]): Map<string, TreeNode> {
92
+ const map = new Map<string, TreeNode>();
93
+ const walk = (n: TreeNode) => {
94
+ map.set(n.key, n);
95
+ n.children?.forEach(walk);
96
+ };
97
+ nodes.forEach(walk);
98
+ return map;
99
+ }
100
+
101
+ /**
102
+ * Given the explicitly-checked leaf-and-internal set, derive the (checked, halfChecked)
103
+ * tuple by propagating up: a parent is checked iff *all* its descendants are checked,
104
+ * halfChecked iff *some* (not all) descendants are checked.
105
+ */
106
+ function propagate(
107
+ nodes: TreeNode[],
108
+ rawChecked: Set<string>,
109
+ ): { checked: Set<string>; half: Set<string> } {
110
+ const checked = new Set<string>();
111
+ const half = new Set<string>();
112
+
113
+ const visit = (n: TreeNode): { full: boolean; partial: boolean } => {
114
+ if (!n.children || n.children.length === 0) {
115
+ const isChecked = rawChecked.has(n.key);
116
+ if (isChecked) checked.add(n.key);
117
+ return { full: isChecked, partial: false };
118
+ }
119
+ let allFull = true;
120
+ let anyTouched = false;
121
+ for (const c of n.children) {
122
+ const r = visit(c);
123
+ if (!r.full) allFull = false;
124
+ if (r.full || r.partial) anyTouched = true;
125
+ }
126
+ if (allFull) {
127
+ checked.add(n.key);
128
+ return { full: true, partial: false };
129
+ }
130
+ if (anyTouched) {
131
+ half.add(n.key);
132
+ return { full: false, partial: true };
133
+ }
134
+ return { full: false, partial: false };
135
+ };
136
+
137
+ nodes.forEach(visit);
138
+ return { checked, half };
139
+ }
140
+
141
+ // ─── Node row ─────────────────────────────────────────────────────────────
142
+
143
+ interface NodeProps {
144
+ node: TreeNode;
145
+ depth: number;
146
+ expanded: Set<string>;
147
+ selected: Set<string>;
148
+ checked: Set<string>;
149
+ half: Set<string>;
150
+ checkable: boolean;
151
+ selectable: boolean;
152
+ onToggleExpand: (key: string) => void;
153
+ onToggleSelect: (key: string) => void;
154
+ onToggleCheck: (node: TreeNode, next: boolean) => void;
155
+ }
156
+
157
+ const TreeRow: React.FC<NodeProps> = ({
158
+ node,
159
+ depth,
160
+ expanded,
161
+ selected,
162
+ checked,
163
+ half,
164
+ checkable,
165
+ selectable,
166
+ onToggleExpand,
167
+ onToggleSelect,
168
+ onToggleCheck,
169
+ }) => {
170
+ const hasChildren = !!node.children && node.children.length > 0;
171
+ const isOpen = expanded.has(node.key);
172
+ const isSelected = selected.has(node.key);
173
+ const isChecked = checked.has(node.key);
174
+ const isHalf = half.has(node.key);
175
+
176
+ return (
177
+ <li role="treeitem" aria-expanded={hasChildren ? isOpen : undefined}>
178
+ <div
179
+ className={cn(
180
+ 'flex items-center gap-2 rounded-md py-1 pr-2 text-sm transition-colors',
181
+ selectable && !node.disabled && 'hover:bg-accent',
182
+ isSelected && 'bg-accent text-accent-foreground',
183
+ node.disabled && 'cursor-not-allowed opacity-50',
184
+ )}
185
+ style={{ paddingLeft: `${depth * 16 + 4}px` }}
186
+ >
187
+ {hasChildren ? (
188
+ <button
189
+ type="button"
190
+ aria-label={isOpen ? '收起' : '展开'}
191
+ onClick={() => onToggleExpand(node.key)}
192
+ disabled={node.disabled}
193
+ className="flex size-5 shrink-0 items-center justify-center rounded-sm transition-colors hover:bg-accent focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
194
+ >
195
+ <ChevronRight
196
+ className={cn(
197
+ 'size-3.5 transition-transform',
198
+ isOpen && 'rotate-90',
199
+ )}
200
+ />
201
+ </button>
202
+ ) : (
203
+ <span aria-hidden="true" className="size-5 shrink-0" />
204
+ )}
205
+ {checkable ? (
206
+ <Checkbox
207
+ checked={isHalf ? 'indeterminate' : isChecked}
208
+ disabled={node.disabled}
209
+ onCheckedChange={(c) => onToggleCheck(node, c === true)}
210
+ />
211
+ ) : null}
212
+ {node.icon ? (
213
+ <span className="flex size-4 shrink-0 items-center justify-center text-muted-foreground">
214
+ {node.icon}
215
+ </span>
216
+ ) : null}
217
+ <span
218
+ className={cn(
219
+ 'min-w-0 flex-1 truncate',
220
+ selectable && !node.disabled && 'cursor-pointer',
221
+ )}
222
+ onClick={() => {
223
+ if (selectable && !node.disabled) onToggleSelect(node.key);
224
+ }}
225
+ >
226
+ {node.title}
227
+ </span>
228
+ </div>
229
+ {hasChildren && isOpen ? (
230
+ <ul role="group">
231
+ {node.children!.map((child) => (
232
+ <TreeRow
233
+ key={child.key}
234
+ node={child}
235
+ depth={depth + 1}
236
+ expanded={expanded}
237
+ selected={selected}
238
+ checked={checked}
239
+ half={half}
240
+ checkable={checkable}
241
+ selectable={selectable}
242
+ onToggleExpand={onToggleExpand}
243
+ onToggleSelect={onToggleSelect}
244
+ onToggleCheck={onToggleCheck}
245
+ />
246
+ ))}
247
+ </ul>
248
+ ) : null}
249
+ </li>
250
+ );
251
+ };
252
+
253
+ // ─── Tree ─────────────────────────────────────────────────────────────────
254
+
255
+ const Tree = React.forwardRef<HTMLDivElement, TreeProps>(
256
+ (
257
+ {
258
+ data,
259
+ checkable = false,
260
+ selectable = true,
261
+ checkedKeys,
262
+ defaultCheckedKeys,
263
+ onCheck,
264
+ selectedKeys,
265
+ defaultSelectedKeys,
266
+ onSelect,
267
+ expandedKeys,
268
+ defaultExpandedKeys,
269
+ defaultExpandAll = false,
270
+ onExpand,
271
+ className,
272
+ ...props
273
+ },
274
+ ref,
275
+ ) => {
276
+ const parentMap = React.useMemo(() => buildParentMap(data), [data]);
277
+ const nodeMap = React.useMemo(() => buildNodeMap(data), [data]);
278
+
279
+ const initialExpanded = React.useMemo(() => {
280
+ if (defaultExpandedKeys) return defaultExpandedKeys;
281
+ if (defaultExpandAll) return collectAllKeys(data);
282
+ return [];
283
+ }, [defaultExpandAll, defaultExpandedKeys, data]);
284
+
285
+ const [internalExpanded, setInternalExpanded] = React.useState<string[]>(initialExpanded);
286
+ const [internalSelected, setInternalSelected] = React.useState<string[]>(
287
+ defaultSelectedKeys ?? [],
288
+ );
289
+ const [internalChecked, setInternalChecked] = React.useState<string[]>(
290
+ defaultCheckedKeys ?? [],
291
+ );
292
+
293
+ const expanded = expandedKeys ?? internalExpanded;
294
+ const selected = selectedKeys ?? internalSelected;
295
+ const rawChecked = checkedKeys ?? internalChecked;
296
+
297
+ const expandedSet = React.useMemo(() => new Set(expanded), [expanded]);
298
+ const selectedSet = React.useMemo(() => new Set(selected), [selected]);
299
+
300
+ const { checked, half } = React.useMemo(
301
+ () => propagate(data, new Set(rawChecked)),
302
+ [data, rawChecked],
303
+ );
304
+
305
+ const toggleExpand = (key: string) => {
306
+ const next = expandedSet.has(key)
307
+ ? expanded.filter((k) => k !== key)
308
+ : [...expanded, key];
309
+ if (expandedKeys === undefined) setInternalExpanded(next);
310
+ onExpand?.(next);
311
+ };
312
+
313
+ const toggleSelect = (key: string) => {
314
+ const next = selectedSet.has(key) ? [] : [key];
315
+ if (selectedKeys === undefined) setInternalSelected(next);
316
+ onSelect?.(next);
317
+ };
318
+
319
+ const toggleCheck = (node: TreeNode, value: boolean) => {
320
+ const next = new Set(rawChecked);
321
+ const subtree = [node.key, ...collectDescendants(node)];
322
+ if (value) subtree.forEach((k) => next.add(k));
323
+ else subtree.forEach((k) => next.delete(k));
324
+
325
+ // Recompute checked-by-leaves to keep parent states consistent.
326
+ let cursor: string | null = parentMap.get(node.key) ?? null;
327
+ while (cursor) {
328
+ const parent = nodeMap.get(cursor);
329
+ if (!parent || !parent.children) break;
330
+ const allKidsChecked = parent.children.every((c) => next.has(c.key));
331
+ if (allKidsChecked) next.add(parent.key);
332
+ else next.delete(parent.key);
333
+ cursor = parentMap.get(cursor) ?? null;
334
+ }
335
+
336
+ const arr = Array.from(next);
337
+ if (checkedKeys === undefined) setInternalChecked(arr);
338
+ onCheck?.(arr);
339
+ };
340
+
341
+ return (
342
+ <div ref={ref} className={cn('text-sm', className)} {...props}>
343
+ <ul role="tree" className="flex flex-col gap-0.5">
344
+ {data.map((n) => (
345
+ <TreeRow
346
+ key={n.key}
347
+ node={n}
348
+ depth={0}
349
+ expanded={expandedSet}
350
+ selected={selectedSet}
351
+ checked={checked}
352
+ half={half}
353
+ checkable={checkable}
354
+ selectable={selectable}
355
+ onToggleExpand={toggleExpand}
356
+ onToggleSelect={toggleSelect}
357
+ onToggleCheck={toggleCheck}
358
+ />
359
+ ))}
360
+ </ul>
361
+ </div>
362
+ );
363
+ },
364
+ );
365
+ Tree.displayName = 'Tree';
366
+
367
+ export { Tree };
@@ -0,0 +1,100 @@
1
+ ---
2
+ id: tree-select
3
+ name: TreeSelect
4
+ type: component
5
+ category: form
6
+ since: 0.1.0
7
+ package: "@teamix-evo/ui"
8
+ ---
9
+
10
+ # TreeSelect
11
+
12
+ 树形下拉选择 — antd 独有补足。**等价 antd `TreeSelect`**。把 `Tree` 嵌入 Popover,提供**单选**(selectable)或**多选**(checkable)两种模式 — 组织架构 / 分类树形选择 / 文件夹路径选择常用。
13
+
14
+ ## When to use
15
+
16
+ - 组织架构选人(部门 - 子部门 - 个人)
17
+ - 分类目录选择(电商类目、知识库目录)
18
+ - 权限模块树选择(多选,父子级联勾选)
19
+
20
+ ## When NOT to use
21
+
22
+ - 平层多选 → `Select multiple` / `Combobox`
23
+ - 严格级联(每层必选一个,不可跨层) → `Cascader`
24
+ - 树形浏览(不需要"选中"语义) → `Tree`
25
+
26
+ <!-- auto:props:begin -->
27
+ | 名称 | 类型 | 默认值 | 必填 | 说明 |
28
+ | --- | --- | --- | --- | --- |
29
+ | `data` | `TreeNode[]` | – | ✓ | 树形数据(antd `treeData` 并集)。 |
30
+ | `multiple` | `boolean` | `false` | – | 多选模式 — 启用后下拉内的 Tree 变为 checkable;value 是 key 数组。 |
31
+ | `value` | `string \| string[]` | – | – | 受控 value: - `multiple=false`:`string`(单 key)/ `undefined`(未选) - `multiple=true`:`string[]` |
32
+ | `defaultValue` | `string \| string[]` | – | – | uncontrolled 初值。 |
33
+ | `onChange` | `(value: string \| string[]) => void` | – | – | value 变化回调。 |
34
+ | `defaultExpandAll` | `boolean` | `false` | – | 默认展开全部节点(antd `treeDefaultExpandAll` 并集)。 |
35
+ | `placeholder` | `string` | `"请选择"` | – | 占位文本。 |
36
+ | `disabled` | `boolean` | – | – | 整体禁用。 |
37
+ | `className` | `string` | – | – | 触发器 className。 |
38
+ | `size` | `'sm' \| 'default' \| 'lg'` | `"default"` | – | 触发器尺寸。 |
39
+ <!-- auto:props:end -->
40
+
41
+ <!-- auto:deps:begin -->
42
+ ### 同库依赖
43
+
44
+ > `teamix-evo ui add tree-select` 时,以下 entry 会被自动连带安装(无需手动 add)。
45
+
46
+ | Entry | 类型 | 描述 |
47
+ | --- | --- | --- |
48
+ | `cn` | util | Tailwind className 合并工具(clsx + tailwind-merge) |
49
+ | `button` | component | 通用按钮 — shadcn 实现 + antd 功能扩展(loading / icon / shape / block / dashed variant) |
50
+ | `popover` | component | 可交互浮层 — Radix Popover + antd arrow 并集 |
51
+ | `tree` | component | 树形控件 — antd 独有补足。递归层级展示(目录、组织、分类),可展开 / 可选 / 可勾选(父子级联 + 半选),受控与非受控并存 |
52
+
53
+ ### npm 依赖
54
+
55
+ > 业务侧需要先 `pnpm add` / `npm install` 这些包。CLI 在 `ui add` 完成后会列出此提示。
56
+
57
+ ```bash
58
+ pnpm add lucide-react@^0.460.0
59
+ ```
60
+ <!-- auto:deps:end -->
61
+
62
+ ## AI 生成纪律
63
+
64
+ - **value 类型由 `multiple` 决定**:single 模式是 `string`,multi 模式是 `string[]` — **不要**让两种模式共用同一 state 类型
65
+ - **multiple 模式下 onCheck 返回的是完全勾选的 key 集合**(包括完全勾选的父级,不包括半选)— 与 antd 行为一致
66
+ - **单选场景** Tree 设置 `selectable=true selectableKeys=[value]`;选中即关闭 Popover(避免用户误操作)
67
+ - **大数据量(> 500 节点)**:Tree 不做虚拟化,深树会卡顿 — 改用 `Cascader` 或按需懒加载子节点
68
+ - **触发器展示**:多选 ≤ 3 项展示 chip 列表,> 3 项展示 `已选 N 项` — 不要在外层再做截断
69
+ - **`disabled` 节点级联仍计算**:父级被勾选时,disabled 子级也会自动 checked(对齐 antd / Tree)
70
+
71
+ ## Examples
72
+
73
+ ```tsx
74
+ import { TreeSelect } from '@/components/ui/tree-select';
75
+ import * as React from 'react';
76
+
77
+ const data = [
78
+ {
79
+ key: 'eng',
80
+ title: '工程团队',
81
+ children: [
82
+ { key: 'fe', title: '前端' },
83
+ { key: 'be', title: '后端' },
84
+ {
85
+ key: 'qa',
86
+ title: '测试',
87
+ children: [{ key: 'sdet', title: 'SDET' }],
88
+ },
89
+ ],
90
+ },
91
+ { key: 'design', title: '设计团队' },
92
+ ];
93
+
94
+ // 单选
95
+ <TreeSelect data={data} defaultExpandAll />
96
+
97
+ // 多选 + 受控
98
+ const [v, setV] = React.useState<string[]>(['fe', 'be']);
99
+ <TreeSelect data={data} multiple defaultExpandAll value={v} onChange={(next) => setV(next as string[])} />
100
+ ```
@@ -0,0 +1,80 @@
1
+ import * as React from 'react';
2
+ import type { Meta, StoryObj } from '@storybook/react';
3
+ import { TreeSelect } from './tree-select';
4
+
5
+ const meta: Meta<typeof TreeSelect> = {
6
+ title: '表单与输入 · Form/TreeSelect',
7
+ component: TreeSelect,
8
+ tags: ['autodocs'],
9
+ parameters: {
10
+ docs: {
11
+ description: {
12
+ component:
13
+ '树形下拉 — Tree 嵌入 Popover,单选(selectable)或多选(checkable)两种模式。组织架构 / 分类树形选择 / 文件夹路径选择常用。等价 antd `TreeSelect`。视觉走 OpenTrek tokens,所有样式来自 `@teamix-evo/design`,无 mock。',
14
+ },
15
+ },
16
+ },
17
+ };
18
+
19
+ export default meta;
20
+ type Story = StoryObj<typeof TreeSelect>;
21
+
22
+ const data = [
23
+ {
24
+ key: 'eng',
25
+ title: '工程团队',
26
+ children: [
27
+ { key: 'fe', title: '前端' },
28
+ { key: 'be', title: '后端' },
29
+ {
30
+ key: 'qa',
31
+ title: '测试',
32
+ children: [
33
+ { key: 'sdet', title: 'SDET' },
34
+ { key: 'qa-manual', title: '手工测试' },
35
+ ],
36
+ },
37
+ ],
38
+ },
39
+ {
40
+ key: 'design',
41
+ title: '设计团队',
42
+ children: [
43
+ { key: 'product-design', title: '产品设计' },
44
+ { key: 'brand', title: '品牌设计' },
45
+ ],
46
+ },
47
+ ];
48
+
49
+ export const Single: Story = {
50
+ parameters: { controls: { disable: true } },
51
+ render: () => <TreeSelect data={data} defaultExpandAll />,
52
+ };
53
+
54
+ export const Multiple: Story = {
55
+ parameters: { controls: { disable: true } },
56
+ render: () => {
57
+ const [v, setV] = React.useState<string[]>(['fe']);
58
+ return (
59
+ <div className="flex flex-col gap-2">
60
+ <TreeSelect
61
+ data={data}
62
+ multiple
63
+ defaultExpandAll
64
+ value={v}
65
+ onChange={(next) => setV(next as string[])}
66
+ />
67
+ <div className="text-xs text-muted-foreground">
68
+ checked: {v.length ? v.join(', ') : '(空)'}
69
+ </div>
70
+ </div>
71
+ );
72
+ },
73
+ };
74
+
75
+ export const Disabled: Story = {
76
+ parameters: { controls: { disable: true } },
77
+ render: () => (
78
+ <TreeSelect data={data} disabled defaultValue="fe" defaultExpandAll />
79
+ ),
80
+ };