@teamix-evo/ui 0.7.1 → 0.7.2

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 (120) hide show
  1. package/manifest.json +16 -7
  2. package/package.json +4 -4
  3. package/src/_design-system/theme-tokens/stories.tsx +2 -2
  4. package/src/components/accordion/index.tsx +1 -1
  5. package/src/components/affix/meta.md +26 -0
  6. package/src/components/alert/index.tsx +2 -2
  7. package/src/components/alert-dialog/index.tsx +3 -3
  8. package/src/components/alert-dialog/meta.md +52 -0
  9. package/src/components/alert-dialog/stories.tsx +45 -48
  10. package/src/components/avatar/index.tsx +1 -1
  11. package/src/components/badge/index.tsx +2 -2
  12. package/src/components/badge/meta.md +48 -0
  13. package/src/components/button/index.tsx +2 -2
  14. package/src/components/button/meta.md +15 -0
  15. package/src/components/button/stories.tsx +1 -1
  16. package/src/components/calendar/index.tsx +2 -2
  17. package/src/components/card/index.tsx +1 -1
  18. package/src/components/carousel/index.tsx +2 -2
  19. package/src/components/carousel/meta.md +34 -2
  20. package/src/components/carousel/stories.tsx +2 -2
  21. package/src/components/cascader-select/index.tsx +2 -1
  22. package/src/components/cascader-select/meta.md +46 -0
  23. package/src/components/checkbox/meta.md +47 -0
  24. package/src/components/color-picker/index.tsx +3 -3
  25. package/src/components/color-picker/meta.md +80 -0
  26. package/src/components/combobox/index.tsx +2 -2
  27. package/src/components/combobox/meta.md +130 -0
  28. package/src/components/data-table/index.tsx +2 -2
  29. package/src/components/data-table/meta.md +419 -0
  30. package/src/components/date-picker/meta.md +91 -0
  31. package/src/components/descriptions/index.tsx +1 -1
  32. package/src/components/descriptions/meta.md +245 -0
  33. package/src/components/dialog/index.tsx +4 -4
  34. package/src/components/dialog/meta.md +47 -1
  35. package/src/components/dialog/stories.tsx +38 -41
  36. package/src/components/dropdown-menu/index.tsx +5 -5
  37. package/src/components/empty/index.tsx +2 -2
  38. package/src/components/field/index.tsx +4 -4
  39. package/src/components/filter-bar/index.tsx +6 -6
  40. package/src/components/filter-bar/meta.md +323 -0
  41. package/src/components/float-button/index.tsx +2 -2
  42. package/src/components/form/index.tsx +1 -1
  43. package/src/components/form/meta.md +119 -0
  44. package/src/components/hover-card/index.tsx +1 -1
  45. package/src/components/hover-card/meta.md +21 -0
  46. package/src/components/input/meta.md +16 -0
  47. package/src/components/input-group/index.tsx +1 -1
  48. package/src/components/input-group/meta.md +118 -0
  49. package/src/components/input-group/stories.tsx +6 -6
  50. package/src/components/input-ip/index.tsx +2 -2
  51. package/src/components/input-ip/meta.md +30 -0
  52. package/src/components/input-ip/stories.tsx +2 -2
  53. package/src/components/input-number/index.tsx +3 -2
  54. package/src/components/input-number/meta.md +67 -0
  55. package/src/components/input-number/stories.tsx +2 -2
  56. package/src/components/item/index.tsx +4 -4
  57. package/src/components/label/meta.md +8 -0
  58. package/src/components/mentions/meta.md +15 -0
  59. package/src/components/menubar/index.tsx +4 -4
  60. package/src/components/navigation-menu/index.tsx +4 -4
  61. package/src/components/page-header/index.tsx +1 -1
  62. package/src/components/page-header/meta.md +145 -0
  63. package/src/components/page-shell/index.tsx +2 -2
  64. package/src/components/pagination/index.tsx +1 -1
  65. package/src/components/pagination/meta.md +203 -0
  66. package/src/components/popconfirm/meta.md +45 -0
  67. package/src/components/popover/index.tsx +2 -2
  68. package/src/components/popover/meta.md +47 -0
  69. package/src/components/progress/index.tsx +1 -1
  70. package/src/components/progress/meta.md +36 -0
  71. package/src/components/progress/stories.tsx +1 -1
  72. package/src/components/radio-group/meta.md +69 -0
  73. package/src/components/rate/index.tsx +1 -1
  74. package/src/components/rate/meta.md +50 -0
  75. package/src/components/resizable/index.tsx +1 -1
  76. package/src/components/select/index.tsx +2 -2
  77. package/src/components/select/meta.md +20 -0
  78. package/src/components/separator/index.tsx +1 -1
  79. package/src/components/sheet/index.tsx +13 -14
  80. package/src/components/sheet/meta.md +124 -0
  81. package/src/components/sheet/stories.tsx +110 -119
  82. package/src/components/sidebar/index.tsx +5 -5
  83. package/src/components/sidebar/meta.md +383 -0
  84. package/src/components/skeleton/meta.md +13 -0
  85. package/src/components/slider/index.tsx +2 -2
  86. package/src/components/sonner/meta.md +86 -0
  87. package/src/components/spinner/meta.md +46 -0
  88. package/src/components/spinner/stories.tsx +2 -2
  89. package/src/components/steps/meta.md +20 -0
  90. package/src/components/steps/stories.tsx +1 -1
  91. package/src/components/switch/index.tsx +2 -2
  92. package/src/components/switch/meta.md +33 -0
  93. package/src/components/table/index.tsx +2 -2
  94. package/src/components/table/meta.md +11 -0
  95. package/src/components/tabs/index.tsx +7 -7
  96. package/src/components/tabs/meta.md +52 -0
  97. package/src/components/tag/index.tsx +8 -8
  98. package/src/components/tag/meta.md +194 -0
  99. package/src/components/textarea/index.tsx +1 -1
  100. package/src/components/textarea/meta.md +27 -0
  101. package/src/components/textarea/stories.tsx +1 -1
  102. package/src/components/time-picker/index.tsx +3 -3
  103. package/src/components/time-picker/meta.md +76 -0
  104. package/src/components/timeline/index.tsx +1 -0
  105. package/src/components/toggle/index.tsx +1 -1
  106. package/src/components/toggle-group/index.tsx +1 -1
  107. package/src/components/tooltip/index.tsx +1 -1
  108. package/src/components/tooltip/meta.md +23 -0
  109. package/src/components/transfer/index.tsx +2 -2
  110. package/src/components/transfer/meta.md +97 -0
  111. package/src/components/tree/index.tsx +245 -15
  112. package/src/components/tree/meta.md +151 -0
  113. package/src/components/tree-select/index.tsx +16 -2
  114. package/src/components/tree-select/meta.md +150 -0
  115. package/src/components/typography/index.tsx +3 -3
  116. package/src/components/upload/index.tsx +3 -3
  117. package/src/components/upload/meta.md +82 -0
  118. package/src/components/tree/utils.ts +0 -269
  119. package/src/examples/built-in-assets/stories.tsx +0 -572
  120. package/src/examples/evaluators/stories.tsx +0 -502
@@ -87,6 +87,29 @@
87
87
  </TooltipProvider>
88
88
  ```
89
89
 
90
+ ### AllPositions
91
+
92
+ 12 方向矩阵:side × align 组合。
93
+
94
+ ```tsx
95
+ <TooltipProvider>
96
+ <div className="grid grid-cols-3 gap-4">
97
+ {sides.flatMap((side) =>
98
+ aligns.map((align) => (
99
+ <Tooltip key={`${side}-${align}`}>
100
+ <TooltipTrigger asChild>
101
+ <Button>{`${side}-${align}`}</Button>
102
+ </TooltipTrigger>
103
+ <TooltipContent side={side} align={align}>
104
+ {`${side} / ${align}`}
105
+ </TooltipContent>
106
+ </Tooltip>
107
+ )),
108
+ )}
109
+ </div>
110
+ </TooltipProvider>
111
+ ```
112
+
90
113
  ### WithoutArrow
91
114
 
92
115
  隐藏箭头:showArrow={false}。
@@ -304,7 +304,7 @@ function TransferPanel({
304
304
  className={cn(panelVariants({ size }))}
305
305
  >
306
306
  {/* Header */}
307
- <div className="flex items-center gap-2 border-b border-border px-2 py-2">
307
+ <div className="flex items-center gap-2 border-b border-border p-2">
308
308
  {showHeaderCheckbox ? (
309
309
  <Checkbox
310
310
  checked={
@@ -413,7 +413,7 @@ function TransferPanel({
413
413
  e.stopPropagation();
414
414
  onItemRemove?.(item.key);
415
415
  }}
416
- className="inline-flex size-4 shrink-0 cursor-pointer items-center justify-center rounded-sm text-muted-foreground opacity-0 transition-colors hover:text-foreground group-hover/transfer-item:opacity-100"
416
+ className="inline-flex size-4 shrink-0 cursor-pointer items-center justify-center rounded-sm text-muted-foreground opacity-0 transition-colors group-hover/transfer-item:opacity-100 hover:text-foreground"
417
417
  >
418
418
  <X className="size-3.5" />
419
419
  </span>
@@ -50,6 +50,24 @@ Transfer > TransferPanel × 2 + Operations
50
50
 
51
51
  ## 示例
52
52
 
53
+ ### Basic
54
+
55
+ ```tsx
56
+ <Transfer dataSource={dataSource} defaultTargetKeys={['item-1'} 'item-5']={'item-5']} />
57
+ ```
58
+
59
+ ### WithSearch
60
+
61
+ ```tsx
62
+ <Transfer dataSource={dataSource} showSearch defaultTargetKeys={['item-2']} />
63
+ ```
64
+
65
+ ### SearchPerSide
66
+
67
+ ```tsx
68
+ <Transfer dataSource={dataSource} showSearch={[true} false]={false]} boolean]={boolean]} defaultTargetKeys={['item-2']} />
69
+ ```
70
+
53
71
  ### Sizes
54
72
 
55
73
  ```tsx
@@ -74,3 +92,82 @@ Transfer > TransferPanel × 2 + Operations
74
92
  />
75
93
  </div>
76
94
  ```
95
+
96
+ ### SimpleMode
97
+
98
+ ```tsx
99
+ <Transfer dataSource={dataSource} mode="simple" defaultTargetKeys={['item-1']} titles={['可选项'} '已选项']={'已选项']} />
100
+ ```
101
+
102
+ ### OneWay
103
+
104
+ ```tsx
105
+ <Transfer dataSource={dataSource} oneWay defaultTargetKeys={['item-2'} 'item-5']={'item-5']} titles={['可选项'} '已选项']={'已选项']} />
106
+ ```
107
+
108
+ ### WithDescription
109
+
110
+ ```tsx
111
+ <Transfer dataSource={dataSourceWithDescription} defaultTargetKeys={['member-1']} titles={['可分配'} '已分配']={'已分配']} />
112
+ ```
113
+
114
+ ### CustomRender
115
+
116
+ ```tsx
117
+ <span className="inline-flex items-center gap-1.5">
118
+ <Folder className="size-3 text-muted-foreground" />
119
+ {item.title}
120
+ </span>
121
+ ```
122
+
123
+ ### CustomOperations
124
+
125
+ ```tsx
126
+ <Transfer dataSource={dataSource} defaultTargetKeys={['item-1']} operations={[<ChevronsRight />} <ChevronsLeft />]
127
+ React.ReactNode={<ChevronsLeft />]
128
+ React.ReactNode} React.ReactNode={React.ReactNode} ]={]} />
129
+ ```
130
+
131
+ ### Disabled
132
+
133
+ ```tsx
134
+ <Transfer dataSource={dataSource} disabled defaultTargetKeys={['item-1'} 'item-4']={'item-4']} />
135
+ ```
136
+
137
+ ### PartialDisabled
138
+
139
+ ```tsx
140
+ <Transfer dataSource={dataSource} leftDisabled defaultTargetKeys={['item-2'} 'item-5']={'item-5']} titles={['左侧禁用'} '右侧可操作']={'右侧可操作']} />
141
+ ```
142
+
143
+ ### WithLocale
144
+
145
+ ```tsx
146
+ <Transfer dataSource={dataSource} showSearch titles={['可分配成员'} '已分配成员']={'已分配成员']} locale={{
147
+ itemUnit: '人'} itemsUnit="人" notFoundContent="没有匹配的成员" searchPlaceholder="按姓名搜索" }={}} />
148
+ ```
149
+
150
+ ### CustomPanel
151
+
152
+ ```tsx
153
+ <li
154
+ key={item.key}
155
+ className={`flex cursor-pointer items-center gap-2 rounded-sm px-1.5 py-1 hover:bg-accent/50 ${
156
+ checked ? 'bg-accent/40' : ''
157
+ } ${
158
+ disabled || item.disabled
159
+ ? 'pointer-events-none opacity-50'
160
+ : ''
161
+ }`}
162
+ onClick={() => onItemSelect(item.key, !checked)}
163
+ >
164
+ <span
165
+ className={`inline-block size-1.5 rounded-full ${
166
+ direction === 'left'
167
+ ? 'bg-primary'
168
+ : 'bg-muted-foreground'
169
+ }`}
170
+ />
171
+ {item.title}
172
+ </li>
173
+ ```
@@ -25,24 +25,254 @@ import {
25
25
  import { cn } from '@/lib/utils';
26
26
  import { Checkbox } from '@/components/checkbox';
27
27
 
28
- import {
29
- applyCheckedStrategy,
30
- collectIndeterminateKeys,
31
- expandWithParents,
32
- findNode,
33
- flattenVisible,
34
- getAllKeys,
35
- isFullyChecked,
36
- isIndeterminate,
37
- toggleWithCascade,
38
- type FlatTreeNode,
39
- type TreeNodeBase,
40
- } from './utils';
28
+ // ─── Tree Utilities ─────────────────────────────────────────────────────────
29
+
30
+ /** 所有树节点共享的可选基础字段。 */
31
+ interface TreeNodeBase {
32
+ /** 整体禁用:不可选 / 不可勾选 / 不可展开收起。 */
33
+ disabled?: boolean;
34
+ /** 仅禁用复选框(多选 / 勾选下生效)。 */
35
+ disableCheckbox?: boolean;
36
+ /** 强制叶子节点:无展开箭头 / 不参与 cascade。 */
37
+ isLeaf?: boolean;
38
+ }
39
+
40
+ /** 在数据树中按 key 查找节点。 */
41
+ function findNode<T extends { children?: T[] }>(
42
+ data: T[],
43
+ key: string,
44
+ getKey: (n: T) => string,
45
+ ): T | undefined {
46
+ for (const node of data) {
47
+ if (getKey(node) === key) return node;
48
+ if (node.children) {
49
+ const found = findNode(node.children, key, getKey);
50
+ if (found) return found;
51
+ }
52
+ }
53
+ return undefined;
54
+ }
55
+
56
+ /** 收集整棵树所有节点 key(含中间节点)。 */
57
+ function getAllKeys<T extends { children?: T[] }>(
58
+ data: T[],
59
+ getKey: (n: T) => string,
60
+ ): string[] {
61
+ const keys: string[] = [];
62
+ const walk = (nodes: T[]) => {
63
+ for (const node of nodes) {
64
+ keys.push(getKey(node));
65
+ if (node.children) walk(node.children);
66
+ }
67
+ };
68
+ walk(data);
69
+ return keys;
70
+ }
71
+
72
+ /** 收集节点下所有未禁用 / 未 disableCheckbox 的后代 key(含自身)。 */
73
+ function collectCheckable<T extends TreeNodeBase & { children?: T[] }>(
74
+ node: T,
75
+ getKey: (n: T) => string,
76
+ ): string[] {
77
+ const out: string[] = [];
78
+ const walk = (n: T) => {
79
+ if (n.disabled || n.disableCheckbox) return;
80
+ out.push(getKey(n));
81
+ n.children?.forEach(walk);
82
+ };
83
+ walk(node);
84
+ return out;
85
+ }
86
+
87
+ /**
88
+ * 节点是否 fully checked —— 对齐 antd `rc-tree` `conductCheck` 语义:
89
+ * - 叶子(或所有子均不可勾选):`checked.has(self)`
90
+ * - 非叶子:所有 checkable 子均 fully checked,不依赖 self 在 set 中
91
+ * (避免受控展开后 parent in set 误判为 fully,导致取消子项被压缩推导反吐)。
92
+ */
93
+ function isFullyChecked<T extends TreeNodeBase & { children?: T[] }>(
94
+ node: T,
95
+ checked: Set<string>,
96
+ getKey: (n: T) => string,
97
+ ): boolean {
98
+ const children = node.children?.filter(
99
+ (c) => !c.disabled && !c.disableCheckbox,
100
+ );
101
+ if (!children || children.length === 0) {
102
+ return checked.has(getKey(node));
103
+ }
104
+ return children.every((c) => isFullyChecked(c, checked, getKey));
105
+ }
106
+
107
+ /** 是否处于半选状态:自身未 fully checked 但有任一后代被选。 */
108
+ function isIndeterminate<T extends TreeNodeBase & { children?: T[] }>(
109
+ node: T,
110
+ checked: Set<string>,
111
+ getKey: (n: T) => string,
112
+ ): boolean {
113
+ if (isFullyChecked(node, checked, getKey)) return false;
114
+ const stack: T[] = node.children ? [...node.children] : [];
115
+ while (stack.length) {
116
+ const cur = stack.pop()!;
117
+ if (checked.has(getKey(cur))) return true;
118
+ if (cur.children) stack.push(...cur.children);
119
+ }
120
+ return false;
121
+ }
122
+
123
+ /** 把内部完整 checkedSet 按策略压缩为出参。 */
124
+ function applyCheckedStrategy<T extends TreeNodeBase & { children?: T[] }>(
125
+ data: T[],
126
+ checked: Set<string>,
127
+ strategy: 'parent' | 'child' | 'all',
128
+ getKey: (n: T) => string,
129
+ ): string[] {
130
+ const out: string[] = [];
131
+ const walk = (nodes: T[]) => {
132
+ for (const node of nodes) {
133
+ const fully = isFullyChecked(node, checked, getKey);
134
+ if (strategy === 'parent') {
135
+ if (fully) {
136
+ out.push(getKey(node));
137
+ continue;
138
+ }
139
+ if (node.children) walk(node.children);
140
+ } else if (strategy === 'child') {
141
+ const isLeaf =
142
+ node.isLeaf ?? (!node.children || node.children.length === 0);
143
+ if (isLeaf && (fully || checked.has(getKey(node)))) {
144
+ out.push(getKey(node));
145
+ }
146
+ if (node.children) walk(node.children);
147
+ } else {
148
+ if (fully || checked.has(getKey(node))) out.push(getKey(node));
149
+ if (node.children) walk(node.children);
150
+ }
151
+ }
152
+ };
153
+ walk(data);
154
+ return out;
155
+ }
156
+
157
+ /** 父子联动模式:勾选 / 取消时同步 ancestor / descendant。 */
158
+ function toggleWithCascade<T extends TreeNodeBase & { children?: T[] }>(
159
+ data: T[],
160
+ current: Set<string>,
161
+ target: T,
162
+ nextChecked: boolean,
163
+ getKey: (n: T) => string,
164
+ ): Set<string> {
165
+ const next = new Set(current);
166
+ // 1. 同步所有可勾选后代(含自身)
167
+ for (const v of collectCheckable(target, getKey)) {
168
+ if (nextChecked) next.add(v);
169
+ else next.delete(v);
170
+ }
171
+ // 2. 找到 target 的祖先链,倒序回算
172
+ const parents: T[] = [];
173
+ const walk = (nodes: T[], chain: T[]): boolean => {
174
+ for (const node of nodes) {
175
+ if (node === target) {
176
+ parents.push(...chain);
177
+ return true;
178
+ }
179
+ if (node.children && walk(node.children, [...chain, node])) return true;
180
+ }
181
+ return false;
182
+ };
183
+ walk(data, []);
184
+ for (let i = parents.length - 1; i >= 0; i--) {
185
+ const p = parents[i]!;
186
+ if (isFullyChecked(p, next, getKey)) next.add(getKey(p));
187
+ else next.delete(getKey(p));
188
+ }
189
+ return next;
190
+ }
191
+
192
+ /** 收集树中所有节点的半选 key 列表(用于 onCheck info)。 */
193
+ function collectIndeterminateKeys<T extends TreeNodeBase & { children?: T[] }>(
194
+ data: T[],
195
+ checked: Set<string>,
196
+ getKey: (n: T) => string,
197
+ ): string[] {
198
+ const out: string[] = [];
199
+ const walk = (nodes: T[]) => {
200
+ for (const node of nodes) {
201
+ if (isIndeterminate(node, checked, getKey)) out.push(getKey(node));
202
+ if (node.children) walk(node.children);
203
+ }
204
+ };
205
+ walk(data);
206
+ return out;
207
+ }
208
+
209
+ // ─── 展平 / 自动展开父级 ────────────────────────────────────────────────────
210
+
211
+ /** 扁平节点(虚拟滚动消费)。 */
212
+ interface FlatTreeNode<T> {
213
+ /** 当前节点。 */
214
+ node: T;
215
+ /** 层级,根 = 0。 */
216
+ level: number;
217
+ /** 父节点 key 链(自顶向下,不含自身)。 */
218
+ parentKeys: string[];
219
+ /** 是否叶子。 */
220
+ isLeaf: boolean;
221
+ }
222
+
223
+ /** 按 expandedKeys 展开当前可见节点列表,root → leaf 顺序。 */
224
+ function flattenVisible<T extends { children?: T[]; isLeaf?: boolean }>(
225
+ data: T[],
226
+ expandedKeys: Set<string>,
227
+ getKey: (n: T) => string,
228
+ ): FlatTreeNode<T>[] {
229
+ const out: FlatTreeNode<T>[] = [];
230
+ const walk = (nodes: T[], level: number, chain: string[]) => {
231
+ for (const node of nodes) {
232
+ const key = getKey(node);
233
+ const hasChildren = !!(node.children && node.children.length > 0);
234
+ const isLeaf = node.isLeaf ?? !hasChildren;
235
+ out.push({ node, level, parentKeys: chain, isLeaf });
236
+ if (hasChildren && expandedKeys.has(key)) {
237
+ walk(node.children!, level + 1, [...chain, key]);
238
+ }
239
+ }
240
+ };
241
+ walk(data, 0, []);
242
+ return out;
243
+ }
244
+
245
+ /** 把命中节点的所有祖先 key 自动并入展开集合(autoExpandParent 用)。 */
246
+ function expandWithParents<T extends { children?: T[] }>(
247
+ data: T[],
248
+ keys: string[],
249
+ getKey: (n: T) => string,
250
+ ): string[] {
251
+ const target = new Set(keys);
252
+ const out = new Set(keys);
253
+ const walk = (nodes: T[], chain: string[]): boolean => {
254
+ let hit = false;
255
+ for (const node of nodes) {
256
+ const key = getKey(node);
257
+ const childHit = node.children
258
+ ? walk(node.children, [...chain, key])
259
+ : false;
260
+ if (target.has(key) || childHit) {
261
+ // 把当前节点的祖先加进来
262
+ for (const k of chain) out.add(k);
263
+ hit = true;
264
+ }
265
+ }
266
+ return hit;
267
+ };
268
+ walk(data, []);
269
+ return Array.from(out);
270
+ }
41
271
 
42
272
  // ─── Variants ────────────────────────────────────────────────────────────────
43
273
 
44
274
  const treeNodeVariants = cva(
45
- 'group/tree-node relative flex items-center gap-1 rounded-sm text-xs select-none transition-colors data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground',
275
+ 'group/tree-node relative flex items-center gap-1 rounded-sm text-xs transition-colors select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground',
46
276
  {
47
277
  variants: {
48
278
  size: {
@@ -546,7 +776,7 @@ function TreeNodeRow({ node, level }: { node: TreeNode; level: number }) {
546
776
  <span
547
777
  aria-hidden
548
778
  data-slot="tree-line"
549
- className="pointer-events-none absolute top-0 bottom-0 w-px bg-border"
779
+ className="pointer-events-none absolute inset-y-0 w-px bg-border"
550
780
  style={{
551
781
  left: `${
552
782
  level * INDENT_PER_LEVEL + NODE_BASE_PADDING_LEFT + 8
@@ -48,6 +48,135 @@
48
48
 
49
49
  ## 示例
50
50
 
51
+ ### Selectable
52
+
53
+ 受控选中 + 节点级 selectable 覆盖 + multiple 多选选中,对应 cd `control-select`。
54
+
55
+ ```tsx
56
+ <div className="flex flex-col gap-2">
57
+ <label className="flex items-center gap-2 text-xs">
58
+ <Checkbox
59
+ checked={multi}
60
+ onCheckedChange={(next) => {
61
+ setMulti(next === true);
62
+ setKeys([]);
63
+ }}
64
+ />
65
+ 支持多选 multiple
66
+ </label>
67
+ <Tree
68
+ treeData={treeData}
69
+ defaultExpandAll
70
+ multiple={multi}
71
+ selectedKeys={keys}
72
+ onSelect={(next) => setKeys(next)}
73
+ />
74
+ <span className="text-xs text-muted-foreground">
75
+ selectedKeys: {JSON.stringify(keys)}
76
+ </span>
77
+ </div>
78
+ ```
79
+
80
+ ### Checkable
81
+
82
+ 勾选:checkable + checkStrictly 切换 + 三档 checkedStrategy 输出对比, 对应 cd `control-check`。
83
+
84
+ ```tsx
85
+ <div className="flex flex-col gap-3">
86
+ <label className="flex items-center gap-2 text-xs">
87
+ <Checkbox
88
+ checked={strictly}
89
+ onCheckedChange={(next) => {
90
+ setStrictly(next === true);
91
+ setKeys([]);
92
+ }}
93
+ />
94
+ 取消父子关联 checkStrictly
95
+ </label>
96
+ <Tree
97
+ treeData={treeData}
98
+ defaultExpandAll
99
+ checkable
100
+ checkStrictly={strictly}
101
+ checkedKeys={keys}
102
+ onCheck={(next) => setKeys(next)}
103
+ />
104
+ <span className="text-xs text-muted-foreground">
105
+ checkedKeys: {JSON.stringify(keys)}
106
+ </span>
107
+ </div>
108
+ ```
109
+
110
+ ### CheckedStrategy
111
+
112
+ 三档回填策略对比:parent / child / all。
113
+
114
+ ```tsx
115
+ <div className="flex flex-col gap-4">
116
+ <Demo strategy="all" />
117
+ <Demo strategy="parent" />
118
+ <Demo strategy="child" />
119
+ </div>
120
+ ```
121
+
122
+ ### WithLine
123
+
124
+ 连接线 showLine:在子树左侧加一根 1px 线条,对应 cd `line`。
125
+
126
+ ```tsx
127
+ <Tree treeData={treeData} defaultExpandAll showLine />
128
+ ```
129
+
130
+ ### WithIcon
131
+
132
+ 自定义图标 + showIcon,对应 cd `icon`。
133
+
134
+ ```tsx
135
+ <Tree treeData={orgTree} defaultExpandAll showIcon />
136
+ ```
137
+
138
+ ### LoadData
139
+
140
+ 异步加载:点击展开非 isLeaf 节点时调用 loadData,期间显示 spinner。 对应 cd `dynamic`。
141
+
142
+ ```tsx
143
+ const [data, setData] = React.useState<TreeNode[]>([
144
+ { key: '0', label: '点击展开加载' },
145
+ { key: '1', label: '点击展开加载' },
146
+ { key: '2', label: '叶子节点', isLeaf: true },
147
+ ]);
148
+ const onLoad = (node: TreeNode) =>
149
+ new Promise<void>((resolve) => {
150
+ setTimeout(() => {
151
+ setData((prev) => addChildren(prev, node.key));
152
+ resolve();
153
+ }, 800);
154
+ });
155
+ return <Tree treeData={data} loadData={onLoad} />;
156
+ ```
157
+
158
+ ### Search
159
+
160
+ 搜索高亮:外部 Input 驱动 expandedKeys + filterTreeNode 高亮命中节点。 搜索时 autoExpandParent=true 自动展开命中节点的祖先链。对应 cd `search-tree`。
161
+
162
+ ```tsx
163
+ <div className="flex w-72 flex-col gap-2">
164
+ <Input
165
+ size="sm"
166
+ placeholder="搜索节点 label"
167
+ value={keyword}
168
+ onChange={(e) => setKeyword(e.target.value)}
169
+ />
170
+ <Tree
171
+ treeData={treeData}
172
+ expandedKeys={expanded}
173
+ onExpand={(next) => setExpanded(next)}
174
+ autoExpandParent={!!keyword}
175
+ filterTreeNode={(n) => matchedKeys.has(n.key)}
176
+ />
177
+ </div>
178
+ ```
179
+
51
180
  ### BlockNode
52
181
 
53
182
  节点占满整行 blockNode:常用于在节点尾部追加操作按钮。配合 titleRender。 对应 cd `node-block`。
@@ -78,6 +207,28 @@
78
207
  />
79
208
  ```
80
209
 
210
+ ### Virtual
211
+
212
+ 虚拟滚动:1000+ 节点开启 virtual,对应 cd `virtual-tree`。
213
+
214
+ ```tsx
215
+ const big = React.useMemo<TreeNode[]>(() => {
216
+ const out: TreeNode[] = [];
217
+ for (let i = 0; i < 50; i++) {
218
+ out.push({
219
+ key: `g-${i}`,
220
+ label: `分组 ${i}`,
221
+ children: Array.from({ length: 30 }).map((_, j) => ({
222
+ key: `g-${i}-${j}`,
223
+ label: `节点 ${i}-${j}`,
224
+ })),
225
+ });
226
+ }
227
+ return out;
228
+ }, []);
229
+ return <Tree treeData={big} defaultExpandAll virtual />;
230
+ ```
231
+
81
232
  ### Sizes
82
233
 
83
234
  尺寸:sm / default 两档行高。
@@ -21,7 +21,21 @@ import { cn } from '@/lib/utils';
21
21
  import { Spinner } from '@/components/spinner';
22
22
  import { Tree } from '@/components/tree';
23
23
  import type { TreeNode } from '@/components/tree';
24
- import { findNode } from '@/components/tree/utils';
24
+ /** 在数据树中按 key 查找节点。 */
25
+ function findNode<T extends { children?: T[] }>(
26
+ data: T[],
27
+ key: string,
28
+ getKey: (n: T) => string,
29
+ ): T | undefined {
30
+ for (const node of data) {
31
+ if (getKey(node) === key) return node;
32
+ if (node.children) {
33
+ const found = findNode(node.children, key, getKey);
34
+ if (found) return found;
35
+ }
36
+ }
37
+ return undefined;
38
+ }
25
39
 
26
40
  // ─── Variants ────────────────────────────────────────────────────────────────
27
41
 
@@ -327,7 +341,7 @@ function TreeSelect({
327
341
  align="start"
328
342
  {...popoverProps}
329
343
  className={cn(
330
- 'z-50 min-w-(--radix-popover-trigger-width) origin-(--radix-popover-content-transform-origin) rounded-md bg-popover p-1 text-popover-foreground shadow-md ring-1 ring-foreground/10 outline-hidden duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95',
344
+ 'z-50 min-w-(--radix-popover-trigger-width) origin-(--radix-popover-content-transform-origin) rounded-md bg-popover p-1 text-popover-foreground shadow-md ring-1 ring-foreground/10 outline-hidden duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95',
331
345
  popoverProps?.className,
332
346
  )}
333
347
  >