@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.
- package/manifest.json +16 -7
- package/package.json +4 -4
- package/src/_design-system/theme-tokens/stories.tsx +2 -2
- package/src/components/accordion/index.tsx +1 -1
- package/src/components/affix/meta.md +26 -0
- package/src/components/alert/index.tsx +2 -2
- package/src/components/alert-dialog/index.tsx +3 -3
- package/src/components/alert-dialog/meta.md +52 -0
- package/src/components/alert-dialog/stories.tsx +45 -48
- package/src/components/avatar/index.tsx +1 -1
- package/src/components/badge/index.tsx +2 -2
- package/src/components/badge/meta.md +48 -0
- package/src/components/button/index.tsx +2 -2
- package/src/components/button/meta.md +15 -0
- package/src/components/button/stories.tsx +1 -1
- package/src/components/calendar/index.tsx +2 -2
- package/src/components/card/index.tsx +1 -1
- package/src/components/carousel/index.tsx +2 -2
- package/src/components/carousel/meta.md +34 -2
- package/src/components/carousel/stories.tsx +2 -2
- package/src/components/cascader-select/index.tsx +2 -1
- package/src/components/cascader-select/meta.md +46 -0
- package/src/components/checkbox/meta.md +47 -0
- package/src/components/color-picker/index.tsx +3 -3
- package/src/components/color-picker/meta.md +80 -0
- package/src/components/combobox/index.tsx +2 -2
- package/src/components/combobox/meta.md +130 -0
- package/src/components/data-table/index.tsx +2 -2
- package/src/components/data-table/meta.md +419 -0
- package/src/components/date-picker/meta.md +91 -0
- package/src/components/descriptions/index.tsx +1 -1
- package/src/components/descriptions/meta.md +245 -0
- package/src/components/dialog/index.tsx +4 -4
- package/src/components/dialog/meta.md +47 -1
- package/src/components/dialog/stories.tsx +38 -41
- package/src/components/dropdown-menu/index.tsx +5 -5
- package/src/components/empty/index.tsx +2 -2
- package/src/components/field/index.tsx +4 -4
- package/src/components/filter-bar/index.tsx +6 -6
- package/src/components/filter-bar/meta.md +323 -0
- package/src/components/float-button/index.tsx +2 -2
- package/src/components/form/index.tsx +1 -1
- package/src/components/form/meta.md +119 -0
- package/src/components/hover-card/index.tsx +1 -1
- package/src/components/hover-card/meta.md +21 -0
- package/src/components/input/meta.md +16 -0
- package/src/components/input-group/index.tsx +1 -1
- package/src/components/input-group/meta.md +118 -0
- package/src/components/input-group/stories.tsx +6 -6
- package/src/components/input-ip/index.tsx +2 -2
- package/src/components/input-ip/meta.md +30 -0
- package/src/components/input-ip/stories.tsx +2 -2
- package/src/components/input-number/index.tsx +3 -2
- package/src/components/input-number/meta.md +67 -0
- package/src/components/input-number/stories.tsx +2 -2
- package/src/components/item/index.tsx +4 -4
- package/src/components/label/meta.md +8 -0
- package/src/components/mentions/meta.md +15 -0
- package/src/components/menubar/index.tsx +4 -4
- package/src/components/navigation-menu/index.tsx +4 -4
- package/src/components/page-header/index.tsx +1 -1
- package/src/components/page-header/meta.md +145 -0
- package/src/components/page-shell/index.tsx +2 -2
- package/src/components/pagination/index.tsx +1 -1
- package/src/components/pagination/meta.md +203 -0
- package/src/components/popconfirm/meta.md +45 -0
- package/src/components/popover/index.tsx +2 -2
- package/src/components/popover/meta.md +47 -0
- package/src/components/progress/index.tsx +1 -1
- package/src/components/progress/meta.md +36 -0
- package/src/components/progress/stories.tsx +1 -1
- package/src/components/radio-group/meta.md +69 -0
- package/src/components/rate/index.tsx +1 -1
- package/src/components/rate/meta.md +50 -0
- package/src/components/resizable/index.tsx +1 -1
- package/src/components/select/index.tsx +2 -2
- package/src/components/select/meta.md +20 -0
- package/src/components/separator/index.tsx +1 -1
- package/src/components/sheet/index.tsx +13 -14
- package/src/components/sheet/meta.md +124 -0
- package/src/components/sheet/stories.tsx +110 -119
- package/src/components/sidebar/index.tsx +5 -5
- package/src/components/sidebar/meta.md +383 -0
- package/src/components/skeleton/meta.md +13 -0
- package/src/components/slider/index.tsx +2 -2
- package/src/components/sonner/meta.md +86 -0
- package/src/components/spinner/meta.md +46 -0
- package/src/components/spinner/stories.tsx +2 -2
- package/src/components/steps/meta.md +20 -0
- package/src/components/steps/stories.tsx +1 -1
- package/src/components/switch/index.tsx +2 -2
- package/src/components/switch/meta.md +33 -0
- package/src/components/table/index.tsx +2 -2
- package/src/components/table/meta.md +11 -0
- package/src/components/tabs/index.tsx +7 -7
- package/src/components/tabs/meta.md +52 -0
- package/src/components/tag/index.tsx +8 -8
- package/src/components/tag/meta.md +194 -0
- package/src/components/textarea/index.tsx +1 -1
- package/src/components/textarea/meta.md +27 -0
- package/src/components/textarea/stories.tsx +1 -1
- package/src/components/time-picker/index.tsx +3 -3
- package/src/components/time-picker/meta.md +76 -0
- package/src/components/timeline/index.tsx +1 -0
- package/src/components/toggle/index.tsx +1 -1
- package/src/components/toggle-group/index.tsx +1 -1
- package/src/components/tooltip/index.tsx +1 -1
- package/src/components/tooltip/meta.md +23 -0
- package/src/components/transfer/index.tsx +2 -2
- package/src/components/transfer/meta.md +97 -0
- package/src/components/tree/index.tsx +245 -15
- package/src/components/tree/meta.md +151 -0
- package/src/components/tree-select/index.tsx +16 -2
- package/src/components/tree-select/meta.md +150 -0
- package/src/components/typography/index.tsx +3 -3
- package/src/components/upload/index.tsx +3 -3
- package/src/components/upload/meta.md +82 -0
- package/src/components/tree/utils.ts +0 -269
- package/src/examples/built-in-assets/stories.tsx +0 -572
- 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
|
|
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
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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=
|
|
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
|
>
|