@teamix-evo/ui 0.7.0 → 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 (121) 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 +3 -3
  29. package/src/components/data-table/meta.md +419 -0
  30. package/src/components/data-table/stories.tsx +4 -4
  31. package/src/components/date-picker/meta.md +91 -0
  32. package/src/components/descriptions/index.tsx +1 -1
  33. package/src/components/descriptions/meta.md +245 -0
  34. package/src/components/dialog/index.tsx +4 -4
  35. package/src/components/dialog/meta.md +47 -1
  36. package/src/components/dialog/stories.tsx +38 -41
  37. package/src/components/dropdown-menu/index.tsx +5 -5
  38. package/src/components/empty/index.tsx +2 -2
  39. package/src/components/field/index.tsx +4 -4
  40. package/src/components/filter-bar/index.tsx +6 -6
  41. package/src/components/filter-bar/meta.md +323 -0
  42. package/src/components/float-button/index.tsx +2 -2
  43. package/src/components/form/index.tsx +1 -1
  44. package/src/components/form/meta.md +119 -0
  45. package/src/components/hover-card/index.tsx +1 -1
  46. package/src/components/hover-card/meta.md +21 -0
  47. package/src/components/input/meta.md +16 -0
  48. package/src/components/input-group/index.tsx +1 -1
  49. package/src/components/input-group/meta.md +118 -0
  50. package/src/components/input-group/stories.tsx +6 -6
  51. package/src/components/input-ip/index.tsx +2 -2
  52. package/src/components/input-ip/meta.md +30 -0
  53. package/src/components/input-ip/stories.tsx +2 -2
  54. package/src/components/input-number/index.tsx +3 -2
  55. package/src/components/input-number/meta.md +67 -0
  56. package/src/components/input-number/stories.tsx +2 -2
  57. package/src/components/item/index.tsx +4 -4
  58. package/src/components/label/meta.md +8 -0
  59. package/src/components/mentions/meta.md +15 -0
  60. package/src/components/menubar/index.tsx +4 -4
  61. package/src/components/navigation-menu/index.tsx +4 -4
  62. package/src/components/page-header/index.tsx +2 -2
  63. package/src/components/page-header/meta.md +145 -0
  64. package/src/components/page-shell/index.tsx +3 -3
  65. package/src/components/pagination/index.tsx +1 -1
  66. package/src/components/pagination/meta.md +203 -0
  67. package/src/components/popconfirm/meta.md +45 -0
  68. package/src/components/popover/index.tsx +2 -2
  69. package/src/components/popover/meta.md +47 -0
  70. package/src/components/progress/index.tsx +1 -1
  71. package/src/components/progress/meta.md +36 -0
  72. package/src/components/progress/stories.tsx +1 -1
  73. package/src/components/radio-group/meta.md +69 -0
  74. package/src/components/rate/index.tsx +1 -1
  75. package/src/components/rate/meta.md +50 -0
  76. package/src/components/resizable/index.tsx +1 -1
  77. package/src/components/select/index.tsx +2 -2
  78. package/src/components/select/meta.md +20 -0
  79. package/src/components/separator/index.tsx +1 -1
  80. package/src/components/sheet/index.tsx +13 -14
  81. package/src/components/sheet/meta.md +124 -0
  82. package/src/components/sheet/stories.tsx +110 -119
  83. package/src/components/sidebar/index.tsx +5 -5
  84. package/src/components/sidebar/meta.md +383 -0
  85. package/src/components/skeleton/meta.md +13 -0
  86. package/src/components/slider/index.tsx +2 -2
  87. package/src/components/sonner/meta.md +86 -0
  88. package/src/components/spinner/meta.md +46 -0
  89. package/src/components/spinner/stories.tsx +2 -2
  90. package/src/components/steps/meta.md +20 -0
  91. package/src/components/steps/stories.tsx +1 -1
  92. package/src/components/switch/index.tsx +2 -2
  93. package/src/components/switch/meta.md +33 -0
  94. package/src/components/table/index.tsx +4 -4
  95. package/src/components/table/meta.md +11 -0
  96. package/src/components/tabs/index.tsx +7 -7
  97. package/src/components/tabs/meta.md +52 -0
  98. package/src/components/tag/index.tsx +8 -8
  99. package/src/components/tag/meta.md +194 -0
  100. package/src/components/textarea/index.tsx +1 -1
  101. package/src/components/textarea/meta.md +27 -0
  102. package/src/components/textarea/stories.tsx +1 -1
  103. package/src/components/time-picker/index.tsx +3 -3
  104. package/src/components/time-picker/meta.md +76 -0
  105. package/src/components/timeline/index.tsx +1 -0
  106. package/src/components/toggle/index.tsx +1 -1
  107. package/src/components/toggle-group/index.tsx +1 -1
  108. package/src/components/tooltip/index.tsx +1 -1
  109. package/src/components/tooltip/meta.md +23 -0
  110. package/src/components/transfer/index.tsx +2 -2
  111. package/src/components/transfer/meta.md +97 -0
  112. package/src/components/tree/index.tsx +245 -15
  113. package/src/components/tree/meta.md +151 -0
  114. package/src/components/tree-select/index.tsx +16 -2
  115. package/src/components/tree-select/meta.md +150 -0
  116. package/src/components/typography/index.tsx +3 -3
  117. package/src/components/upload/index.tsx +3 -3
  118. package/src/components/upload/meta.md +82 -0
  119. package/src/components/tree/utils.ts +0 -269
  120. package/src/examples/built-in-assets/stories.tsx +0 -572
  121. package/src/examples/evaluators/stories.tsx +0 -502
@@ -44,6 +44,14 @@
44
44
 
45
45
  ## 示例
46
46
 
47
+ ### Multiple
48
+
49
+ 多选模式:父子联动勾选,半选状态由 indeterminate 表达。
50
+
51
+ ```tsx
52
+ <TreeSelect treeData={treeData} multiple defaultExpandAll />
53
+ ```
54
+
47
55
  ### CheckStrictly
48
56
 
49
57
  父子独立勾选:勾选父级不影响子级,反之亦然。
@@ -52,6 +60,103 @@
52
60
  <TreeSelect treeData={treeData} multiple checkStrictly defaultExpandAll />
53
61
  ```
54
62
 
63
+ ### CheckedStrategy
64
+
65
+ 三种回填策略对比:parent / child / all。
66
+
67
+ ```tsx
68
+ <div className="flex flex-col gap-3">
69
+ <div className="flex items-center gap-2">
70
+ <span className="w-16 text-xs text-muted-foreground">parent</span>
71
+ <TreeSelect
72
+ treeData={treeData}
73
+ multiple
74
+ showCheckedStrategy="parent"
75
+ value={parentVal}
76
+ onChange={(v) => setParentVal(v as string[])}
77
+ defaultExpandAll
78
+ />
79
+ <span className="text-xs text-muted-foreground">
80
+ 出参:{JSON.stringify(parentVal)}
81
+ </span>
82
+ </div>
83
+ <div className="flex items-center gap-2">
84
+ <span className="w-16 text-xs text-muted-foreground">child</span>
85
+ <TreeSelect
86
+ treeData={treeData}
87
+ multiple
88
+ showCheckedStrategy="child"
89
+ value={childVal}
90
+ onChange={(v) => setChildVal(v as string[])}
91
+ defaultExpandAll
92
+ />
93
+ <span className="text-xs text-muted-foreground">
94
+ 出参:{JSON.stringify(childVal)}
95
+ </span>
96
+ </div>
97
+ <div className="flex items-center gap-2">
98
+ <span className="w-16 text-xs text-muted-foreground">all</span>
99
+ <TreeSelect
100
+ treeData={treeData}
101
+ multiple
102
+ showCheckedStrategy="all"
103
+ value={allVal}
104
+ onChange={(v) => setAllVal(v as string[])}
105
+ defaultExpandAll
106
+ />
107
+ <span className="text-xs text-muted-foreground">
108
+ 出参:{JSON.stringify(allVal)}
109
+ </span>
110
+ </div>
111
+ </div>
112
+ ```
113
+
114
+ ### WithSearch
115
+
116
+ 内置搜索:按 label 字段本地过滤。
117
+
118
+ ```tsx
119
+ <TreeSelect treeData={treeData} showSearch defaultExpandAll />
120
+ ```
121
+
122
+ ### AsyncSearch
123
+
124
+ 异步搜索:提供 onSearch 由外部接管 treeData,组件不再做本地过滤。
125
+
126
+ ```tsx
127
+ const [data, setData] = React.useState<TreeSelectNode[]>([
128
+ {
129
+ value: 'zhejiang',
130
+ label: '浙江',
131
+ children: [{ value: 'sx', label: '绍兴' }],
132
+ },
133
+ ]);
134
+ const timer = React.useRef<ReturnType<typeof setTimeout>>();
135
+ return (
136
+ <TreeSelect
137
+ treeData={data}
138
+ showSearch
139
+ defaultExpandAll
140
+ onSearch={(kw) => {
141
+ if (timer.current) clearTimeout(timer.current);
142
+ if (!kw) {
143
+ setData([
144
+ {
145
+ value: 'zhejiang',
146
+ label: '浙江',
147
+ children: [{ value: 'sx', label: '绍兴' }],
148
+ },
149
+ ]);
150
+ return;
151
+ }
152
+ timer.current = setTimeout(() => {
153
+ setData([{ value: kw, label: kw }]);
154
+ }, 300);
155
+ }}
156
+ />
157
+ );
158
+ ```
159
+
55
160
  ### Sizes
56
161
 
57
162
  三档尺寸:sm / default / lg,对齐 Button、Input、Select。
@@ -76,3 +181,48 @@
76
181
  allowClear
77
182
  />
78
183
  ```
184
+
185
+ ### Loading
186
+
187
+ 加载态:把右侧图标替换为旋转 spinner,与 Select 行为一致。
188
+
189
+ ```tsx
190
+ <TreeSelect treeData={treeData} loading />
191
+ ```
192
+
193
+ ### Disabled
194
+
195
+ 整体禁用。
196
+
197
+ ```tsx
198
+ <TreeSelect treeData={treeData} disabled defaultValue="fe-1" />
199
+ ```
200
+
201
+ ### Invalid
202
+
203
+ 错误态:通过 aria-invalid 触发 destructive 边框与 ring。
204
+
205
+ ```tsx
206
+ <TreeSelect treeData={treeData} aria-invalid />
207
+ ```
208
+
209
+ ### Controlled
210
+
211
+ 受控 value + 受控 open。
212
+
213
+ ```tsx
214
+ <div className="flex flex-col items-start gap-2">
215
+ <TreeSelect
216
+ treeData={treeData}
217
+ multiple
218
+ value={value}
219
+ onChange={(v) => setValue(v as string[])}
220
+ open={open}
221
+ onOpenChange={setOpen}
222
+ defaultExpandAll
223
+ />
224
+ <span className="text-xs text-muted-foreground">
225
+ value: {JSON.stringify(value)} · open: {String(open)}
226
+ </span>
227
+ </div>
228
+ ```
@@ -72,7 +72,7 @@ function Prose({ className, ...props }: ProseProps) {
72
72
  '[&_pre]:my-4 [&_pre]:overflow-x-auto [&_pre]:rounded-md [&_pre]:bg-muted [&_pre]:p-4',
73
73
  '[&_pre_code]:bg-transparent [&_pre_code]:p-0',
74
74
  // blockquote / hr
75
- '[&_blockquote]:my-4 [&_blockquote]:border-l-4 [&_blockquote]:border-border [&_blockquote]:pl-4 [&_blockquote]:italic [&_blockquote]:text-muted-foreground',
75
+ '[&_blockquote]:my-4 [&_blockquote]:border-l-4 [&_blockquote]:border-border [&_blockquote]:pl-4 [&_blockquote]:text-muted-foreground [&_blockquote]:italic',
76
76
  '[&_hr]:my-6 [&_hr]:border-border',
77
77
  className,
78
78
  )}
@@ -220,7 +220,7 @@ function Text({
220
220
  del && 'line-through',
221
221
  italic && 'italic',
222
222
  underline && 'underline underline-offset-4',
223
- disabled && 'cursor-not-allowed select-none opacity-50',
223
+ disabled && 'cursor-not-allowed opacity-50 select-none',
224
224
  ellipsis && 'inline-block max-w-full truncate align-bottom',
225
225
  code && 'rounded bg-muted px-1.5 py-0.5 font-mono text-[0.875em]',
226
226
  className,
@@ -236,7 +236,7 @@ function Text({
236
236
  type="button"
237
237
  onClick={handleCopy}
238
238
  aria-label={copied ? '已复制' : '复制'}
239
- className="cursor-pointer rounded-sm p-0.5 text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
239
+ className="cursor-pointer rounded-sm p-0.5 text-muted-foreground transition-colors hover:text-foreground focus-visible:ring-1 focus-visible:ring-ring focus-visible:outline-none"
240
240
  >
241
241
  {copied ? (
242
242
  <Check className="size-3.5 text-success" />
@@ -476,7 +476,7 @@ function UploadList({
476
476
  data-status={file.status}
477
477
  className={cn(
478
478
  'group flex items-center gap-2 rounded-sm px-2 py-1 text-xs transition-colors hover:bg-accent/50',
479
- listType === 'image' && 'items-center gap-2 px-1.5 py-1.5',
479
+ listType === 'image' && 'items-center gap-2 p-1.5',
480
480
  isError && 'text-destructive',
481
481
  )}
482
482
  >
@@ -686,7 +686,7 @@ function Upload({
686
686
  <button
687
687
  type="button"
688
688
  tabIndex={-1}
689
- className="inline-flex cursor-pointer items-center gap-1.5 rounded-md border border-input bg-card px-2.5 h-8 text-xs transition-colors hover:bg-muted hover:text-foreground"
689
+ className="inline-flex h-8 cursor-pointer items-center gap-1.5 rounded-md border border-input bg-card px-2.5 text-xs transition-colors hover:bg-muted hover:text-foreground"
690
690
  >
691
691
  <UploadIcon className="size-3.5" aria-hidden />
692
692
  上传文件
@@ -813,7 +813,7 @@ function UploadDragger({
813
813
  handleFiles(e.dataTransfer.files);
814
814
  }}
815
815
  className={cn(
816
- 'flex cursor-pointer flex-col items-center justify-center gap-2 rounded-md border border-dashed border-input bg-card px-6 py-8 text-center transition-colors hover:bg-muted/50 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring',
816
+ 'flex cursor-pointer flex-col items-center justify-center gap-2 rounded-md border border-dashed border-input bg-card px-6 py-8 text-center transition-colors hover:bg-muted/50 focus-visible:ring-1 focus-visible:ring-ring focus-visible:outline-none',
817
817
  dragOver && 'border-primary bg-primary/5',
818
818
  disabled && 'pointer-events-none opacity-50',
819
819
  )}
@@ -44,6 +44,12 @@
44
44
 
45
45
  ## 示例
46
46
 
47
+ ### Basic
48
+
49
+ ```tsx
50
+ <Upload customRequest={mockRequest} defaultFileList={[sampleDoc]} />
51
+ ```
52
+
47
53
  ### Drag
48
54
 
49
55
  ```tsx
@@ -54,6 +60,47 @@
54
60
  />
55
61
  ```
56
62
 
63
+ ### ImageList
64
+
65
+ ```tsx
66
+ <Upload listType="image" multiple accept="image/*" customRequest={mockRequest} defaultFileList={[
67
+ sampleImage} {
68
+ uid: 'sample-3'={{
69
+ uid: 'sample-3'} name="cover.jpg" status="uploading" percent={40} thumbUrl="https://img.alicdn.com/tfs/TB1x4QlgxYaK1RjSZFnXXa80pXa-1400-742.png" }={}} {
70
+ uid: 'sample-4'={{
71
+ uid: 'sample-4'} name="broken.png" status="error" error={new Error('上传失败')} }={}} ]={]} />
72
+ ```
73
+
74
+ ### Multiple
75
+
76
+ ```tsx
77
+ <Upload multiple accept="image/*" customRequest={mockRequest} />
78
+ ```
79
+
80
+ ### MaxCountAndSize
81
+
82
+ ```tsx
83
+ <div className="flex flex-col gap-2">
84
+ <Upload
85
+ multiple
86
+ maxCount={3}
87
+ maxSize={2 * 1024 * 1024}
88
+ customRequest={mockRequest}
89
+ onLimitExceed={(file) =>
90
+ setHint(`已超出最大数量 (3),已忽略 ${file.name}`)
91
+ }
92
+ onSizeExceed={(file) => setHint(`${file.name} 超过 2MB 体积限制`)}
93
+ />
94
+ {hint ? (
95
+ <span className="text-xs text-destructive">{hint}</span>
96
+ ) : (
97
+ <span className="text-xs text-muted-foreground">
98
+ 最多 3 个文件,单文件 ≤ 2MB
99
+ </span>
100
+ )}
101
+ </div>
102
+ ```
103
+
57
104
  ### BeforeUpload
58
105
 
59
106
  ```tsx
@@ -73,6 +120,41 @@
73
120
  </div>
74
121
  ```
75
122
 
123
+ ### CustomRequest
124
+
125
+ ```tsx
126
+ <Upload multiple customRequest={mockRequest} />
127
+ ```
128
+
129
+ ### Controlled
130
+
131
+ ```tsx
132
+ <div className="flex flex-col gap-2">
133
+ <Upload
134
+ fileList={fileList}
135
+ onChange={(next) => setFileList(next)}
136
+ customRequest={mockRequest}
137
+ />
138
+ <div className="flex items-center gap-2">
139
+ <Button variant="outline" onClick={() => setFileList([sampleDoc])}>
140
+ 重置
141
+ </Button>
142
+ <Button variant="outline" onClick={() => setFileList([])}>
143
+ 清空
144
+ </Button>
145
+ <span className="text-xs text-muted-foreground">
146
+ 当前 {fileList.length} 个文件
147
+ </span>
148
+ </div>
149
+ </div>
150
+ ```
151
+
152
+ ### Disabled
153
+
154
+ ```tsx
155
+ <Upload disabled defaultFileList={[sampleDoc]} />
156
+ ```
157
+
76
158
  ### ItemRender
77
159
 
78
160
  ```tsx
@@ -1,269 +0,0 @@
1
- /**
2
- * Tree / TreeSelect 共用算法工具。
3
- *
4
- * - 抽离自原 `tree-select/index.tsx`,泛化为接受 `getKey` 适配 `key` / `value` 两种字段命名。
5
- * - 全为纯函数,无副作用,便于在 Tree 与 TreeSelect 间共享。
6
- */
7
- import * as React from 'react';
8
-
9
- /** 所有树节点共享的可选基础字段。 */
10
- export interface TreeNodeBase {
11
- /** 整体禁用:不可选 / 不可勾选 / 不可展开收起。 */
12
- disabled?: boolean;
13
- /** 仅禁用复选框(多选 / 勾选下生效)。 */
14
- disableCheckbox?: boolean;
15
- /** 强制叶子节点:无展开箭头 / 不参与 cascade。 */
16
- isLeaf?: boolean;
17
- }
18
-
19
- /** 在数据树中按 key 查找节点。 */
20
- export function findNode<T extends { children?: T[] }>(
21
- data: T[],
22
- key: string,
23
- getKey: (n: T) => string,
24
- ): T | undefined {
25
- for (const node of data) {
26
- if (getKey(node) === key) return node;
27
- if (node.children) {
28
- const found = findNode(node.children, key, getKey);
29
- if (found) return found;
30
- }
31
- }
32
- return undefined;
33
- }
34
-
35
- /** 收集整棵树所有节点 key(含中间节点)。 */
36
- export function getAllKeys<T extends { children?: T[] }>(
37
- data: T[],
38
- getKey: (n: T) => string,
39
- ): string[] {
40
- const keys: string[] = [];
41
- const walk = (nodes: T[]) => {
42
- for (const node of nodes) {
43
- keys.push(getKey(node));
44
- if (node.children) walk(node.children);
45
- }
46
- };
47
- walk(data);
48
- return keys;
49
- }
50
-
51
- /** 收集节点下所有未禁用 / 未 disableCheckbox 的后代 key(含自身)。 */
52
- export function collectCheckable<T extends TreeNodeBase & { children?: T[] }>(
53
- node: T,
54
- getKey: (n: T) => string,
55
- ): string[] {
56
- const out: string[] = [];
57
- const walk = (n: T) => {
58
- if (n.disabled || n.disableCheckbox) return;
59
- out.push(getKey(n));
60
- n.children?.forEach(walk);
61
- };
62
- walk(node);
63
- return out;
64
- }
65
-
66
- /**
67
- * 节点是否 fully checked —— 对齐 antd `rc-tree` `conductCheck` 语义:
68
- * - 叶子(或所有子均不可勾选):`checked.has(self)`
69
- * - 非叶子:所有 checkable 子均 fully checked,不依赖 self 在 set 中
70
- * (避免受控展开后 parent in set 误判为 fully,导致取消子项被压缩推导反吐)。
71
- */
72
- export function isFullyChecked<T extends TreeNodeBase & { children?: T[] }>(
73
- node: T,
74
- checked: Set<string>,
75
- getKey: (n: T) => string,
76
- ): boolean {
77
- const children = node.children?.filter(
78
- (c) => !c.disabled && !c.disableCheckbox,
79
- );
80
- if (!children || children.length === 0) {
81
- return checked.has(getKey(node));
82
- }
83
- return children.every((c) => isFullyChecked(c, checked, getKey));
84
- }
85
-
86
- /** 是否处于半选状态:自身未 fully checked 但有任一后代被选。 */
87
- export function isIndeterminate<T extends TreeNodeBase & { children?: T[] }>(
88
- node: T,
89
- checked: Set<string>,
90
- getKey: (n: T) => string,
91
- ): boolean {
92
- if (isFullyChecked(node, checked, getKey)) return false;
93
- const stack: T[] = node.children ? [...node.children] : [];
94
- while (stack.length) {
95
- const cur = stack.pop()!;
96
- if (checked.has(getKey(cur))) return true;
97
- if (cur.children) stack.push(...cur.children);
98
- }
99
- return false;
100
- }
101
-
102
- /** 把内部完整 checkedSet 按策略压缩为出参。 */
103
- export function applyCheckedStrategy<
104
- T extends TreeNodeBase & { children?: T[] },
105
- >(
106
- data: T[],
107
- checked: Set<string>,
108
- strategy: 'parent' | 'child' | 'all',
109
- getKey: (n: T) => string,
110
- ): string[] {
111
- const out: string[] = [];
112
- const walk = (nodes: T[]) => {
113
- for (const node of nodes) {
114
- const fully = isFullyChecked(node, checked, getKey);
115
- if (strategy === 'parent') {
116
- if (fully) {
117
- out.push(getKey(node));
118
- continue;
119
- }
120
- if (node.children) walk(node.children);
121
- } else if (strategy === 'child') {
122
- const isLeaf =
123
- node.isLeaf ?? (!node.children || node.children.length === 0);
124
- if (isLeaf && (fully || checked.has(getKey(node)))) {
125
- out.push(getKey(node));
126
- }
127
- if (node.children) walk(node.children);
128
- } else {
129
- if (fully || checked.has(getKey(node))) out.push(getKey(node));
130
- if (node.children) walk(node.children);
131
- }
132
- }
133
- };
134
- walk(data);
135
- return out;
136
- }
137
-
138
- /** 父子联动模式:勾选 / 取消时同步 ancestor / descendant。 */
139
- export function toggleWithCascade<T extends TreeNodeBase & { children?: T[] }>(
140
- data: T[],
141
- current: Set<string>,
142
- target: T,
143
- nextChecked: boolean,
144
- getKey: (n: T) => string,
145
- ): Set<string> {
146
- const next = new Set(current);
147
- // 1. 同步所有可勾选后代(含自身)
148
- for (const v of collectCheckable(target, getKey)) {
149
- if (nextChecked) next.add(v);
150
- else next.delete(v);
151
- }
152
- // 2. 找到 target 的祖先链,倒序回算
153
- const parents: T[] = [];
154
- const walk = (nodes: T[], chain: T[]): boolean => {
155
- for (const node of nodes) {
156
- if (node === target) {
157
- parents.push(...chain);
158
- return true;
159
- }
160
- if (node.children && walk(node.children, [...chain, node])) return true;
161
- }
162
- return false;
163
- };
164
- walk(data, []);
165
- for (let i = parents.length - 1; i >= 0; i--) {
166
- const p = parents[i]!;
167
- if (isFullyChecked(p, next, getKey)) next.add(getKey(p));
168
- else next.delete(getKey(p));
169
- }
170
- return next;
171
- }
172
-
173
- /** 收集树中所有节点的半选 key 列表(用于 onCheck info)。 */
174
- export function collectIndeterminateKeys<
175
- T extends TreeNodeBase & { children?: T[] },
176
- >(data: T[], checked: Set<string>, getKey: (n: T) => string): string[] {
177
- const out: string[] = [];
178
- const walk = (nodes: T[]) => {
179
- for (const node of nodes) {
180
- if (isIndeterminate(node, checked, getKey)) out.push(getKey(node));
181
- if (node.children) walk(node.children);
182
- }
183
- };
184
- walk(data);
185
- return out;
186
- }
187
-
188
- // ─── 展平 / 自动展开父级 ────────────────────────────────────────────────────
189
-
190
- /** 扁平节点(虚拟滚动消费)。 */
191
- export interface FlatTreeNode<T> {
192
- /** 当前节点。 */
193
- node: T;
194
- /** 层级,根 = 0。 */
195
- level: number;
196
- /** 父节点 key 链(自顶向下,不含自身)。 */
197
- parentKeys: string[];
198
- /** 是否叶子。 */
199
- isLeaf: boolean;
200
- }
201
-
202
- /** 按 expandedKeys 展开当前可见节点列表,root → leaf 顺序。 */
203
- export function flattenVisible<T extends { children?: T[]; isLeaf?: boolean }>(
204
- data: T[],
205
- expandedKeys: Set<string>,
206
- getKey: (n: T) => string,
207
- ): FlatTreeNode<T>[] {
208
- const out: FlatTreeNode<T>[] = [];
209
- const walk = (nodes: T[], level: number, chain: string[]) => {
210
- for (const node of nodes) {
211
- const key = getKey(node);
212
- const hasChildren = !!(node.children && node.children.length > 0);
213
- const isLeaf = node.isLeaf ?? !hasChildren;
214
- out.push({ node, level, parentKeys: chain, isLeaf });
215
- if (hasChildren && expandedKeys.has(key)) {
216
- walk(node.children!, level + 1, [...chain, key]);
217
- }
218
- }
219
- };
220
- walk(data, 0, []);
221
- return out;
222
- }
223
-
224
- /** 把命中节点的所有祖先 key 自动并入展开集合(autoExpandParent 用)。 */
225
- export function expandWithParents<T extends { children?: T[] }>(
226
- data: T[],
227
- keys: string[],
228
- getKey: (n: T) => string,
229
- ): string[] {
230
- const target = new Set(keys);
231
- const out = new Set(keys);
232
- const walk = (nodes: T[], chain: string[]): boolean => {
233
- let hit = false;
234
- for (const node of nodes) {
235
- const key = getKey(node);
236
- const childHit = node.children
237
- ? walk(node.children, [...chain, key])
238
- : false;
239
- if (target.has(key) || childHit) {
240
- // 把当前节点的祖先加进来
241
- for (const k of chain) out.add(k);
242
- hit = true;
243
- }
244
- }
245
- return hit;
246
- };
247
- walk(data, []);
248
- return Array.from(out);
249
- }
250
-
251
- // ─── React 受控/非受控辅助 ──────────────────────────────────────────────────
252
-
253
- /** 用受控值 + 默认值计算最终值,并返回 setter(仅在非受控时生效)。 */
254
- export function useControllableState<T>(
255
- controlled: T | undefined,
256
- defaultValue: T,
257
- ): [T, (next: T) => T] {
258
- const [internal, setInternal] = React.useState<T>(defaultValue);
259
- const isControlled = controlled !== undefined;
260
- const value = isControlled ? (controlled as T) : internal;
261
- const set = React.useCallback(
262
- (next: T) => {
263
- if (!isControlled) setInternal(next);
264
- return next;
265
- },
266
- [isControlled],
267
- );
268
- return [value, set];
269
- }