@zidian-primitive/cascader 0.0.0-next-20260204023823

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/README.md ADDED
@@ -0,0 +1,436 @@
1
+ # @zidian-base/cascader
2
+
3
+ A flexible cascader component library built with component composition pattern, providing complete UI and logic for hierarchical selection.
4
+
5
+ ## Features
6
+
7
+ - 🎯 **Component-Based Architecture**: Modular components for maximum flexibility
8
+ - 🔄 **Flexible Data Sources**: Support both hierarchical and flat data structures
9
+ - 🎨 **Fully Customizable**: Extensive slots for custom rendering
10
+ - ♿ **Accessibility**: Built with accessibility in mind
11
+ - 🔍 **Multiple Selection Modes**: Single and multiple selection support
12
+ - 📦 **TypeScript Support**: Full type safety
13
+ - 🚀 **Performance Optimized**: Efficient state management and rendering
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ npm install @zidian-base/cascader
19
+ ```
20
+
21
+ ## Quick Start
22
+
23
+ ```tsx
24
+ import React, { useState } from 'react'
25
+ import {
26
+ CascaderRoot,
27
+ CascaderPanel,
28
+ CascaderNode,
29
+ CascaderPanelHeader,
30
+ CascaderPanelIndicator,
31
+ useCascader,
32
+ type CascaderItem
33
+ } from '@zidian-base/cascader'
34
+
35
+ const data = [
36
+ { category: 'Electronics', type: 'Phone', brand: 'iPhone' },
37
+ { category: 'Electronics', type: 'Phone', brand: 'Samsung' },
38
+ { category: 'Electronics', type: 'Laptop', brand: 'MacBook' },
39
+ { category: 'Clothing', type: 'Shirt', brand: 'Nike' },
40
+ ]
41
+
42
+ const handleDataConfig = {
43
+ levels: ['category', 'type', 'brand']
44
+ }
45
+
46
+ export default function App() {
47
+ const cascader = useCascader({
48
+ data,
49
+ handleDataConfig,
50
+ selectOption: {
51
+ defaultSelected: [],
52
+ selectType: 'single'
53
+ }
54
+ })
55
+
56
+ const handleSelect = (nodeIdx: number) => {
57
+ cascader.handleChangeCheckStatus(nodeIdx)
58
+ }
59
+
60
+ return (
61
+ <CascaderRoot
62
+ isSelected={true}
63
+ selectType="single"
64
+ onSelectChange={handleSelect}
65
+ >
66
+ <CascaderPanel>
67
+ <CascaderPanelHeader
68
+ column={cascader.columns[0]}
69
+ showNode={null}
70
+ />
71
+ <CascaderPanelIndicator />
72
+
73
+ {cascader.columns.map((column, level) => (
74
+ <div key={level} className="cascader-column">
75
+ {column.map((node) => (
76
+ <CascaderNode
77
+ key={node.index}
78
+ node={node}
79
+ onSelectChange={() => handleSelect(node.indices)}
80
+ />
81
+ ))}
82
+ </div>
83
+ ))}
84
+ </CascaderPanel>
85
+ </CascaderRoot>
86
+ )
87
+ }
88
+ ```
89
+
90
+ ## API Reference
91
+
92
+ ### Hook
93
+
94
+ #### useCascader
95
+
96
+ ```ts
97
+ const cascader = useCascader({
98
+ data: T[],
99
+ handleDataConfig: FlatDataConfig,
100
+ selectOption?: SelectOption
101
+ })
102
+ ```
103
+
104
+ **Hook Configuration**
105
+
106
+ | Property | Type | Default | Description |
107
+ |----------|------|---------|-------------|
108
+ | data | `T[]` | - | Flat data array |
109
+ | handleDataConfig | `FlatDataConfig` | - | Configuration for converting flat data to hierarchical |
110
+ | selectOption | `SelectOption` | - | Selection configuration |
111
+
112
+ **Hook Return Values**
113
+
114
+ | Property | Type | Description |
115
+ |----------|------|-------------|
116
+ | nodes | `CascaderNodeType<T>[]` | All nodes in the cascader |
117
+ | columns | `CascaderItem<T>[][]` | Visible columns based on current selection |
118
+ | showIndices | `number[]` | Indices of currently shown nodes |
119
+ | checkStatusArray | `Uint8Array` | Check status of all nodes |
120
+ | handleShowIndices | `(depth: number, index: number) => void` | Handle column expansion |
121
+ | handleChangeCheckStatus | `(nodeIdx: number) => void` | Handle node selection |
122
+
123
+ ### Components
124
+
125
+ #### CascaderRoot
126
+
127
+ The root component that provides cascader context.
128
+
129
+ ```tsx
130
+ <CascaderRoot
131
+ isSelected={boolean}
132
+ selectType={'single' | 'multiple'}
133
+ onSelectChange={(nodeIdx: number) => void}
134
+ {...divProps}
135
+ >
136
+ {children}
137
+ </CascaderRoot>
138
+ ```
139
+
140
+ **Props**
141
+
142
+ | Property | Type | Default | Description |
143
+ |----------|------|---------|-------------|
144
+ | isSelected | `boolean` | `false` | Whether selection is enabled |
145
+ | selectType | `'single' | 'multiple'` | - | Selection mode |
146
+ | onSelectChange | `(nodeIdx: number) => void` | - | Selection change callback |
147
+
148
+ #### CascaderPanel
149
+
150
+ Container for cascader columns and panels.
151
+
152
+ ```tsx
153
+ <CascaderPanel {...divProps}>
154
+ {children}
155
+ </CascaderPanel>
156
+ ```
157
+
158
+ #### CascaderPanelHeader
159
+
160
+ Header component for displaying column information.
161
+
162
+ ```tsx
163
+ <CascaderPanelHeader
164
+ column={CascaderNodeType<any>[]}
165
+ showNode={CascaderItem<any> | null}
166
+ />
167
+ ```
168
+
169
+ **Props**
170
+
171
+ | Property | Type | Description |
172
+ |----------|------|-------------|
173
+ | column | `CascaderNodeType<any>[]` | Current column nodes |
174
+ | showNode | `CascaderItem<any> | null` | Currently shown node |
175
+
176
+ #### CascaderPanelIndicator
177
+
178
+ Visual indicator for panel navigation.
179
+
180
+ ```tsx
181
+ <CascaderPanelIndicator />
182
+ ```
183
+
184
+ #### CascaderNode
185
+
186
+ Individual node component in the cascader.
187
+
188
+ ```tsx
189
+ <CascaderNode
190
+ node={CascaderItem<any>}
191
+ onSelectChange={(nodeIdx: number) => void}
192
+ {...divProps}
193
+ />
194
+ ```
195
+
196
+ **Props**
197
+
198
+ | Property | Type | Description |
199
+ |----------|------|-------------|
200
+ | node | `CascaderItem<any>` | Node data |
201
+ | onSelectChange | `(nodeIdx: number) => void` | Selection change callback |
202
+
203
+ ### Types
204
+
205
+ #### CascaderNodeType
206
+
207
+ ```ts
208
+ interface CascaderNodeType<T = any> {
209
+ depth: number
210
+ index: number
211
+ parentIndex: number
212
+ range: [number, number]
213
+ childrenIndices: number[]
214
+ pathId: string
215
+ label: string
216
+ value: string | number
217
+ raw: T | null
218
+ }
219
+ ```
220
+
221
+ #### CascaderItem
222
+
223
+ ```ts
224
+ interface CascaderItem<T> {
225
+ indices: number
226
+ level: number
227
+ label: string
228
+ value: string | number
229
+ originData?: T
230
+ checkStatus?: CheckStatus
231
+ }
232
+ ```
233
+
234
+ #### FlatDataConfig
235
+
236
+ ```ts
237
+ interface FlatDataConfig {
238
+ levels: (string | [string, string])[]
239
+ }
240
+ ```
241
+
242
+ #### SelectOption
243
+
244
+ ```ts
245
+ interface SelectOption {
246
+ defaultSelected: string[]
247
+ selectType: 'single' | 'multiple'
248
+ }
249
+ ```
250
+
251
+ #### CheckStatus
252
+
253
+ ```ts
254
+ type CheckStatus = 0 | 1 | 2
255
+ // 0: unchecked, 1: partially checked, 2: fully checked
256
+ ```
257
+
258
+ ## Usage Examples
259
+
260
+ ### Multiple Selection Mode
261
+
262
+ ```tsx
263
+ const cascader = useCascader({
264
+ data,
265
+ handleDataConfig,
266
+ selectOption: {
267
+ defaultSelected: [],
268
+ selectType: 'multiple'
269
+ }
270
+ })
271
+
272
+ <CascaderRoot
273
+ isSelected={true}
274
+ selectType="multiple"
275
+ onSelectChange={cascader.handleChangeCheckStatus}
276
+ >
277
+ {/* Components */}
278
+ </CascaderRoot>
279
+ ```
280
+
281
+ ### Custom Node Rendering
282
+
283
+ ```tsx
284
+ const CustomNode = ({ node, onSelectChange }) => {
285
+ return (
286
+ <CascaderNode
287
+ node={node}
288
+ onSelectChange={onSelectChange}
289
+ className="custom-node"
290
+ >
291
+ <div className="node-content">
292
+ <span className="node-label">{node.label}</span>
293
+ {node.checkStatus === 2 && <span>✓</span>}
294
+ </div>
295
+ </CascaderNode>
296
+ )
297
+ }
298
+ ```
299
+
300
+ ### Custom Header
301
+
302
+ ```tsx
303
+ const CustomHeader = ({ column, showNode }) => {
304
+ return (
305
+ <CascaderPanelHeader column={column} showNode={showNode}>
306
+ <div className="custom-header">
307
+ {showNode ? `Selected: ${showNode.label}` : 'Please select'}
308
+ </div>
309
+ </CascaderPanelHeader>
310
+ )
311
+ }
312
+ ```
313
+
314
+ ### Async Data Loading
315
+
316
+ ```tsx
317
+ const [data, setData] = useState([])
318
+
319
+ useEffect(() => {
320
+ fetchData().then(setData)
321
+ }, [])
322
+
323
+ const cascader = useCascader({
324
+ data,
325
+ handleDataConfig,
326
+ selectOption: {
327
+ defaultSelected: [],
328
+ selectType: 'single'
329
+ }
330
+ })
331
+ ```
332
+
333
+ ### Controlled Selection
334
+
335
+ ```tsx
336
+ const [selectedValues, setSelectedValues] = useState([])
337
+
338
+ const handleSelect = useCallback((nodeIdx: number) => {
339
+ const node = cascader.nodes[nodeIdx]
340
+ const newValue = cascader.selectOption.selectType === 'single'
341
+ ? [node.value]
342
+ : [...selectedValues, node.value]
343
+
344
+ setSelectedValues(newValue)
345
+ cascader.handleChangeCheckStatus(nodeIdx)
346
+ }, [cascader, selectedValues])
347
+ ```
348
+
349
+ ## Advanced Patterns
350
+
351
+ ### Search Functionality
352
+
353
+ ```tsx
354
+ const [searchTerm, setSearchTerm] = useState('')
355
+ const [filteredData, setFilteredData] = useState(data)
356
+
357
+ useEffect(() => {
358
+ const filtered = data.filter(item =>
359
+ Object.values(item).some(value =>
360
+ String(value).toLowerCase().includes(searchTerm.toLowerCase())
361
+ )
362
+ )
363
+ setFilteredData(filtered)
364
+ }, [searchTerm])
365
+
366
+ const cascader = useCascader({
367
+ data: filteredData,
368
+ handleDataConfig
369
+ })
370
+ ```
371
+
372
+ ### Custom Animation
373
+
374
+ ```tsx
375
+ import { motion } from 'framer-motion'
376
+
377
+ const AnimatedNode = ({ node, onSelectChange }) => {
378
+ return (
379
+ <motion.div
380
+ initial={{ opacity: 0, y: -10 }}
381
+ animate={{ opacity: 1, y: 0 }}
382
+ exit={{ opacity: 0, y: -10 }}
383
+ >
384
+ <CascaderNode node={node} onSelectChange={onSelectChange} />
385
+ </motion.div>
386
+ )
387
+ }
388
+ ```
389
+
390
+ ## Data Structure Examples
391
+
392
+ ### Hierarchical Data (Converted from Flat)
393
+
394
+ ```tsx
395
+ const flatData = [
396
+ { level1: 'Electronics', level2: 'Phones', level3: 'iPhone' },
397
+ { level1: 'Electronics', level2: 'Phones', level3: 'Samsung' },
398
+ { level1: 'Electronics', level2: 'Laptops', level3: 'MacBook' },
399
+ ]
400
+
401
+ const config = {
402
+ levels: ['level1', 'level2', 'level3']
403
+ }
404
+ ```
405
+
406
+ ### Complex Field Mapping
407
+
408
+ ```tsx
409
+ const complexData = [
410
+ {
411
+ category: { name: 'Electronics', id: 1 },
412
+ product: { name: 'iPhone', type: 'Phone' },
413
+ brand: 'Apple'
414
+ }
415
+ ]
416
+
417
+ const complexConfig = {
418
+ levels: [
419
+ ['category', 'name'], // Use category.name as label
420
+ ['product', 'name'], // Use product.name as label
421
+ 'brand' // Use brand directly
422
+ ]
423
+ }
424
+ ```
425
+
426
+ ## Best Practices
427
+
428
+ 1. **Performance**: Use `useMemo` for data transformations and filtering
429
+ 2. **Accessibility**: Ensure keyboard navigation and screen reader support
430
+ 3. **State Management**: Keep selection state in parent component for controlled behavior
431
+ 4. **Customization**: Use slots for custom rendering while maintaining core functionality
432
+ 5. **Data Structure**: Keep data flat for better performance and easier manipulation
433
+
434
+ ## License
435
+
436
+ MIT
@@ -0,0 +1,41 @@
1
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
2
+ import { useContext } from 'react';
3
+ import { CascaderContext, CascaderPanelContext } from '../../context/index.js';
4
+
5
+ const CascaderNode = (props) => {
6
+ const {
7
+ node,
8
+ isLeaf,
9
+ onClick,
10
+ onTrigger,
11
+ render,
12
+ ...elementProps
13
+ } = props;
14
+ const { onSelectChange } = useContext(CascaderContext);
15
+ const { showIndicator, registerNode } = useContext(CascaderPanelContext);
16
+ const handleClick = (event) => {
17
+ if (isLeaf) {
18
+ if (onClick) onClick(node);
19
+ } else {
20
+ if (onTrigger) onTrigger(node.level, node.indices);
21
+ }
22
+ };
23
+ return /* @__PURE__ */ jsx("div", { ref: showIndicator ? (el) => registerNode(node.indices, el) : void 0, onClick: handleClick, ...elementProps, children: render ? render({ node, onSelectChange }) : /* @__PURE__ */ jsxs(Fragment, { children: [
24
+ /* @__PURE__ */ jsx("span", { style: { flex: 1 }, children: node.label }),
25
+ !isLeaf && /* @__PURE__ */ jsx(
26
+ "svg",
27
+ {
28
+ xmlns: "http://www.w3.org/2000/svg",
29
+ width: "18",
30
+ height: "18",
31
+ viewBox: "0 0 24 24",
32
+ fill: "none",
33
+ stroke: "currentColor",
34
+ children: /* @__PURE__ */ jsx("path", { d: "m9 18 6-6-6-6" })
35
+ }
36
+ )
37
+ ] }) });
38
+ };
39
+ CascaderNode.displayName = "CascaderNode";
40
+
41
+ export { CascaderNode };
@@ -0,0 +1,7 @@
1
+ import { jsx } from 'react/jsx-runtime';
2
+
3
+ const CascaderNodeCheck = () => {
4
+ return /* @__PURE__ */ jsx("div", {});
5
+ };
6
+
7
+ export { CascaderNodeCheck };
@@ -0,0 +1,40 @@
1
+ import { jsx } from 'react/jsx-runtime';
2
+ import { forwardRef, useRef, useState, useCallback, useLayoutEffect } from 'react';
3
+ import { CascaderPanelContext } from '../../context/index.js';
4
+
5
+ const CascaderPanel = forwardRef((props, ref) => {
6
+ const {
7
+ depth,
8
+ children,
9
+ showIndicator = false,
10
+ activeIndices,
11
+ ...elementProps
12
+ } = props;
13
+ const nodeRefs = useRef(/* @__PURE__ */ new Map());
14
+ const [indicatorStyle, setIndicatorStyle] = useState();
15
+ const registerNode = useCallback((index, el) => {
16
+ if (el) nodeRefs.current.set(index, el);
17
+ else nodeRefs.current.delete(index);
18
+ }, []);
19
+ useLayoutEffect(() => {
20
+ if (activeIndices === void 0) return;
21
+ const activeEl = nodeRefs.current.get(activeIndices);
22
+ if (activeEl) {
23
+ const measure = () => {
24
+ setIndicatorStyle({
25
+ ["--indicator-offset-top"]: `${activeEl.offsetTop}px`,
26
+ ["--indicator-height"]: `${activeEl.offsetHeight}px`,
27
+ transform: `translateY(var(--indicator-offset-top))`,
28
+ height: `${activeEl.offsetHeight}px`
29
+ });
30
+ };
31
+ measure();
32
+ const resizeObserver = new ResizeObserver(measure);
33
+ resizeObserver.observe(activeEl);
34
+ }
35
+ }, [activeIndices]);
36
+ return /* @__PURE__ */ jsx(CascaderPanelContext.Provider, { value: { showIndicator, activeIndices, registerNode, indicatorStyle }, children: /* @__PURE__ */ jsx("div", { ref, "data-depth": depth, "data-active-indices": activeIndices, ...elementProps, children }) });
37
+ });
38
+ CascaderPanel.displayName = "CascaderPanel";
39
+
40
+ export { CascaderPanel };
@@ -0,0 +1,20 @@
1
+ import { jsxs, jsx } from 'react/jsx-runtime';
2
+ import { useContext } from 'react';
3
+ import { CascaderContext } from '../../context/index.js';
4
+
5
+ const CascaderPanelHeader = (props) => {
6
+ const {
7
+ column,
8
+ showNode,
9
+ render,
10
+ ...elementProps
11
+ } = props;
12
+ const { selectType } = useContext(CascaderContext);
13
+ if (render) render({ column, showNode });
14
+ else return /* @__PURE__ */ jsxs("div", { ...elementProps, children: [
15
+ /* @__PURE__ */ jsx("span", { children: showNode?.level }),
16
+ /* @__PURE__ */ jsx("span", { children: column.length })
17
+ ] });
18
+ };
19
+
20
+ export { CascaderPanelHeader };
@@ -0,0 +1,17 @@
1
+ import { jsx } from 'react/jsx-runtime';
2
+ import { useContext } from 'react';
3
+ import { CascaderPanelContext } from '../../context/index.js';
4
+
5
+ const CascaderPanelIndicator = (props) => {
6
+ const {
7
+ render,
8
+ ...elementProps
9
+ } = props;
10
+ const { indicatorStyle, activeIndices } = useContext(CascaderPanelContext);
11
+ if (render) return render();
12
+ if (activeIndices === void 0) return null;
13
+ return /* @__PURE__ */ jsx("div", { style: indicatorStyle, ...elementProps });
14
+ };
15
+ CascaderPanelIndicator.displayName = "CascaderPanelIndicator";
16
+
17
+ export { CascaderPanelIndicator };
@@ -0,0 +1,21 @@
1
+ import { jsx } from 'react/jsx-runtime';
2
+ import { forwardRef } from 'react';
3
+ import { CascaderContext } from '../../context/index.js';
4
+
5
+ const CascaderRoot = forwardRef((props, ref) => {
6
+ const {
7
+ isSelected = false,
8
+ selectType,
9
+ onSelectChange,
10
+ children,
11
+ ...elementProps
12
+ } = props;
13
+ return /* @__PURE__ */ jsx(CascaderContext.Provider, { value: {
14
+ isSelected,
15
+ selectType,
16
+ onSelectChange
17
+ }, children: /* @__PURE__ */ jsx("div", { ref, ...elementProps, children }) });
18
+ });
19
+ CascaderRoot.displayName = "CascaderRoot";
20
+
21
+ export { CascaderRoot };
@@ -0,0 +1,15 @@
1
+ import { createContext } from 'react';
2
+
3
+ const BLANK_FUNCTION = () => {
4
+ };
5
+ const CascaderContext = createContext({
6
+ isSelected: false,
7
+ selectType: "single",
8
+ onSelectChange: void 0
9
+ });
10
+ const CascaderPanelContext = createContext({
11
+ showIndicator: false,
12
+ registerNode: BLANK_FUNCTION
13
+ });
14
+
15
+ export { CascaderContext, CascaderPanelContext };
@@ -0,0 +1,61 @@
1
+ import { useMemo, useState } from 'react';
2
+ import { buildCascaderEngine, getDefaultIndices, getCascadeColumns, getShowIndices } from '../utils/data.js';
3
+ import { setMultipleNodeStatus, setNodeCheckStatus } from '../utils/logic.js';
4
+
5
+ function useCascader(options) {
6
+ const { data, handleDataConfig, selectOption } = options;
7
+ const sortedData = useMemo(() => {
8
+ return data.sort((a, b) => {
9
+ for (const field of handleDataConfig.levels) {
10
+ const f = Array.isArray(field) ? field[0] : field;
11
+ if (a[f] !== b[f]) return String(a[f]).localeCompare(String(b[f]));
12
+ }
13
+ return 0;
14
+ });
15
+ }, [data, handleDataConfig]);
16
+ const defaultNodes = useMemo(() => {
17
+ return buildCascaderEngine(sortedData, handleDataConfig);
18
+ }, [sortedData, handleDataConfig]);
19
+ const defaultStatusArray = useMemo(() => {
20
+ const statusArray = new Uint8Array(defaultNodes.length);
21
+ if (selectOption?.defaultSelected) {
22
+ const defaultIndices = defaultNodes.filter(
23
+ (node) => selectOption.defaultSelected.find(
24
+ (selected) => node.raw?.value === selected
25
+ )
26
+ ).map((node) => node.index);
27
+ return setMultipleNodeStatus(defaultNodes, statusArray, defaultIndices, 1);
28
+ }
29
+ return statusArray;
30
+ }, [defaultNodes, selectOption?.defaultSelected]);
31
+ useMemo(() => Boolean(selectOption), [selectOption]);
32
+ const [checkStatusArray, setCheckStatusArray] = useState(defaultStatusArray);
33
+ const [showIndices, setShowIndices] = useState(getDefaultIndices(defaultNodes));
34
+ const columns = useMemo(() => getCascadeColumns(defaultNodes, showIndices), [defaultNodes, showIndices]);
35
+ const handleShowIndices = (depth, index) => {
36
+ setShowIndices(getShowIndices(
37
+ defaultNodes,
38
+ {
39
+ oldShowIndices: showIndices,
40
+ actionIndices: {
41
+ depth,
42
+ nodeIdx: index
43
+ }
44
+ }
45
+ ));
46
+ };
47
+ const handleChangeCheckStatus = (nodeIdx) => {
48
+ const newStatusArray = setNodeCheckStatus(defaultNodes, checkStatusArray, nodeIdx);
49
+ setCheckStatusArray(newStatusArray);
50
+ };
51
+ return {
52
+ nodes: defaultNodes,
53
+ columns,
54
+ showIndices,
55
+ checkStatusArray,
56
+ handleShowIndices,
57
+ handleChangeCheckStatus
58
+ };
59
+ }
60
+
61
+ export { useCascader };
package/esm/index.js ADDED
@@ -0,0 +1,9 @@
1
+ export { CascaderRoot } from './components/root/CascaderRoot.js';
2
+ export { CascaderNode } from './components/node/CascaderNode.js';
3
+ export { CascaderNodeCheck } from './components/node/CascaderNodeCheck.js';
4
+ export { CascaderPanel } from './components/panel/CascaderPanel.js';
5
+ export { CascaderPanelHeader } from './components/panel/CascaderPanelHeader.js';
6
+ export { CascaderPanelIndicator } from './components/panel/CascaderPanelIndicator.js';
7
+ export { useCascader } from './hooks/useCascader.js';
8
+ export { buildCascaderEngine, getCascadeColumns, getDefaultIndices, getShowIndices } from './utils/data.js';
9
+ export { setMultipleNodeStatus, setNodeCheckStatus } from './utils/logic.js';
@@ -0,0 +1,102 @@
1
+ function createCascaderNode(option) {
2
+ const { depth, currentIndex, parentIndex, row, label, value, pathId } = option;
3
+ return {
4
+ depth,
5
+ index: currentIndex,
6
+ parentIndex,
7
+ range: [currentIndex, currentIndex],
8
+ childrenIndices: [],
9
+ pathId,
10
+ label,
11
+ value,
12
+ raw: row
13
+ };
14
+ }
15
+ function buildCascaderEngine(data, config) {
16
+ let currentIndex = 0;
17
+ const currentConfig = config;
18
+ const nodes = [];
19
+ const nodeMap = /* @__PURE__ */ new Map();
20
+ const levelConfigs = currentConfig.levels;
21
+ data.forEach((item, itemIndex) => {
22
+ let parentIndex = -1;
23
+ let currentPathId = "";
24
+ for (let levelIndex = 0; levelIndex < levelConfigs.length; levelIndex++) {
25
+ const field = levelConfigs[levelIndex];
26
+ let labelField, valueField = "";
27
+ if (Array.isArray(field)) {
28
+ labelField = field[0];
29
+ valueField = field[1];
30
+ } else {
31
+ labelField = valueField = field;
32
+ }
33
+ const label = item[labelField];
34
+ const value = item[valueField];
35
+ if (label === void 0 || label === null) break;
36
+ const labelStr = typeof item[labelField] === "string" ? item[labelField] : `${itemIndex}-${levelIndex}`;
37
+ currentPathId = levelIndex == 0 ? labelStr : `${currentPathId}${labelStr}`;
38
+ let nodeIdx = nodeMap.get(currentPathId);
39
+ if (nodeIdx === void 0) {
40
+ nodeIdx = currentIndex++;
41
+ nodeMap.set(currentPathId, nodeIdx);
42
+ levelIndex === levelConfigs.length - 1;
43
+ nodes[nodeIdx] = createCascaderNode({
44
+ parentIndex,
45
+ currentIndex: nodeIdx,
46
+ pathId: currentPathId,
47
+ depth: levelIndex,
48
+ label,
49
+ value,
50
+ row: item
51
+ });
52
+ if (parentIndex !== -1) {
53
+ nodes[parentIndex].childrenIndices.push(nodeIdx);
54
+ }
55
+ }
56
+ parentIndex = nodeIdx;
57
+ }
58
+ });
59
+ for (let i = nodes.length - 1; i >= 0; i--) {
60
+ const node = nodes[i];
61
+ if (node.childrenIndices.length > 0) {
62
+ const lastChildIdx = node.childrenIndices[node.childrenIndices.length - 1];
63
+ node.range[1] = nodes[lastChildIdx].range[1];
64
+ }
65
+ }
66
+ return nodes;
67
+ }
68
+ function getCascadeColumns(nodes, showIndices) {
69
+ const columns = [];
70
+ const rootColumn = nodes.filter((node) => node.parentIndex === -1);
71
+ columns.push(rootColumn);
72
+ showIndices.forEach((selectedIndex) => {
73
+ const parentNode = nodes[selectedIndex];
74
+ if (parentNode && parentNode.childrenIndices.length > 0) {
75
+ const nextColumn = parentNode.childrenIndices.map((idx) => nodes[idx]);
76
+ columns.push(nextColumn);
77
+ }
78
+ });
79
+ return columns;
80
+ }
81
+ function getShowIndices(nodes, option) {
82
+ const { oldShowIndices, actionIndices } = option;
83
+ let targetPath = [];
84
+ const { depth, nodeIdx } = actionIndices;
85
+ let currentNode = nodes[nodeIdx];
86
+ while (currentNode.childrenIndices.length > 0) {
87
+ targetPath.push(currentNode.index);
88
+ currentNode = nodes[currentNode.childrenIndices[0]];
89
+ }
90
+ return oldShowIndices.slice(0, depth).concat(targetPath);
91
+ }
92
+ function getDefaultIndices(nodes) {
93
+ let defaultIndices = [];
94
+ let currentNode = nodes[0];
95
+ while (currentNode.childrenIndices.length > 0) {
96
+ defaultIndices.push(currentNode.index);
97
+ currentNode = nodes[currentNode.childrenIndices[0]];
98
+ }
99
+ return defaultIndices;
100
+ }
101
+
102
+ export { buildCascaderEngine, getCascadeColumns, getDefaultIndices, getShowIndices };
@@ -0,0 +1,79 @@
1
+ const CHECK_NONE = 0;
2
+ const CHECK_ALL = 1;
3
+ const CHECK_INDETERMINATE = 2;
4
+ function propagateToChildren(statusArray, range, status) {
5
+ const [start, end] = range;
6
+ statusArray.fill(status, start, end + 1);
7
+ }
8
+ function propagateToParents(nodes, statusArray, childIndex) {
9
+ const childNode = nodes[childIndex];
10
+ if (!childNode || childNode.parentIndex === -1) return;
11
+ const parentIdx = childNode.parentIndex;
12
+ const parentNode = nodes[parentIdx];
13
+ const children = parentNode.childrenIndices;
14
+ let checkedCount = 0;
15
+ let hasIndeterminate = false;
16
+ for (const idx of children) {
17
+ const s = statusArray[idx];
18
+ if (s === CHECK_ALL) checkedCount++;
19
+ else if (s === CHECK_INDETERMINATE) hasIndeterminate = true;
20
+ }
21
+ let parentStatus = CHECK_NONE;
22
+ if (checkedCount === children.length) {
23
+ parentStatus = CHECK_ALL;
24
+ } else if (checkedCount > 0 || hasIndeterminate) {
25
+ parentStatus = CHECK_INDETERMINATE;
26
+ }
27
+ if (statusArray[parentIdx] !== parentStatus) {
28
+ statusArray[parentIdx] = parentStatus;
29
+ propagateToParents(nodes, statusArray, parentIdx);
30
+ }
31
+ }
32
+ function setNodeCheckStatus(nodes, currentStatusArray, nodeIndex) {
33
+ const nextStatus = new Uint8Array(currentStatusArray);
34
+ const targetNode = nodes[nodeIndex];
35
+ const targetValue = nextStatus[nodeIndex] === CHECK_ALL ? CHECK_NONE : nextStatus[nodeIndex] === CHECK_INDETERMINATE ? CHECK_NONE : CHECK_ALL;
36
+ propagateToChildren(nextStatus, targetNode.range, targetValue);
37
+ propagateToParents(nodes, nextStatus, nodeIndex);
38
+ return nextStatus;
39
+ }
40
+ function syncUpPath(nodes, statusArray, parentIdx) {
41
+ let currIdx = parentIdx;
42
+ while (currIdx !== -1) {
43
+ const node = nodes[currIdx];
44
+ const children = node.childrenIndices;
45
+ let checkedCount = 0;
46
+ let hasIndeterminate = false;
47
+ for (const childIdx of children) {
48
+ const s = statusArray[childIdx];
49
+ if (s === 1) checkedCount++;
50
+ else if (s === 2) hasIndeterminate = true;
51
+ }
52
+ let nextS = 0;
53
+ if (checkedCount === children.length) nextS = 1;
54
+ else if (checkedCount > 0 || hasIndeterminate) nextS = 2;
55
+ if (statusArray[currIdx] === nextS) break;
56
+ statusArray[currIdx] = nextS;
57
+ currIdx = node.parentIndex;
58
+ }
59
+ }
60
+ function setMultipleNodeStatus(nodes, currentStatusArray, indices, targetStatus) {
61
+ const nextStatus = new Uint8Array(currentStatusArray);
62
+ const parentsToSync = /* @__PURE__ */ new Set();
63
+ indices.forEach((index) => {
64
+ const node = nodes[index];
65
+ if (!node) return;
66
+ const [start, end] = node.range;
67
+ nextStatus.fill(targetStatus, start, end + 1);
68
+ if (node.parentIndex !== -1) {
69
+ parentsToSync.add(node.parentIndex);
70
+ }
71
+ });
72
+ const sortedParents = Array.from(parentsToSync).sort((a, b) => b - a);
73
+ sortedParents.forEach((parentIdx) => {
74
+ syncUpPath(nodes, nextStatus, parentIdx);
75
+ });
76
+ return nextStatus;
77
+ }
78
+
79
+ export { setMultipleNodeStatus, setNodeCheckStatus };
@@ -0,0 +1,6 @@
1
+ export * from "./root/CascaderRoot";
2
+ export * from "./node/CascaderNode";
3
+ export * from "./node/CascaderNodeCheck";
4
+ export * from "./panel/CascaderPanel";
5
+ export * from "./panel/CascaderPanelHeader";
6
+ export * from "./panel/CascaderPanelIndicator";
@@ -0,0 +1,10 @@
1
+ import { ComponentProps } from "react";
2
+ import { CascaderItem, NodeSlotType } from "../../types";
3
+ export interface CascaderNodeProps extends Omit<ComponentProps<'div'>, "onClick"> {
4
+ node: CascaderItem<any>;
5
+ isLeaf: boolean;
6
+ onClick?: (node: CascaderItem<any>) => void;
7
+ onTrigger?: (depth: number, nodeIdx: number) => void;
8
+ render?: NodeSlotType;
9
+ }
10
+ export declare const CascaderNode: React.FC<CascaderNodeProps>;
@@ -0,0 +1 @@
1
+ export declare const CascaderNodeCheck: () => import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,7 @@
1
+ import { ComponentProps } from 'react';
2
+ export interface CascaderPanelProps extends ComponentProps<'div'> {
3
+ depth: number;
4
+ showIndicator?: boolean;
5
+ activeIndices?: number;
6
+ }
7
+ export declare const CascaderPanel: import("react").ForwardRefExoticComponent<Omit<CascaderPanelProps, "ref"> & import("react").RefAttributes<HTMLDivElement>>;
@@ -0,0 +1,8 @@
1
+ import { ComponentProps, FC } from "react";
2
+ import { CascaderItem, CascaderNodeType, PanelHeaderSlot } from "../../types";
3
+ export interface CascaderPanelHeaderProps extends ComponentProps<'div'> {
4
+ column: CascaderNodeType<any>[];
5
+ showNode: CascaderItem<any> | null;
6
+ render?: PanelHeaderSlot;
7
+ }
8
+ export declare const CascaderPanelHeader: FC<CascaderPanelHeaderProps>;
@@ -0,0 +1,6 @@
1
+ import { ComponentProps, FC } from "react";
2
+ import { PanelIndicatorSlot } from "../../types";
3
+ export interface CascaderPanelIndicatorProps extends ComponentProps<'div'> {
4
+ render?: PanelIndicatorSlot;
5
+ }
6
+ export declare const CascaderPanelIndicator: FC<CascaderPanelIndicatorProps>;
@@ -0,0 +1,7 @@
1
+ import { ComponentProps } from "react";
2
+ export interface CascaderRootProps extends ComponentProps<'div'> {
3
+ isSelected?: boolean;
4
+ selectType?: "single" | "multiple";
5
+ onSelectChange?: (nodeIdx: number) => void;
6
+ }
7
+ export declare const CascaderRoot: import("react").ForwardRefExoticComponent<Omit<CascaderRootProps, "ref"> & import("react").RefAttributes<unknown>>;
@@ -0,0 +1,12 @@
1
+ import { CSSProperties } from "react";
2
+ export declare const CascaderContext: import("react").Context<{
3
+ isSelected: boolean;
4
+ selectType: "single" | "multiple" | undefined;
5
+ onSelectChange: ((nodeIdx: number) => void) | undefined;
6
+ }>;
7
+ export declare const CascaderPanelContext: import("react").Context<{
8
+ showIndicator: boolean;
9
+ indicatorStyle?: CSSProperties;
10
+ activeIndices?: number;
11
+ registerNode: (index: number, el: HTMLElement | null) => void;
12
+ }>;
@@ -0,0 +1,14 @@
1
+ import { CascaderNodeType, SelectOption, FlatDataConfig } from "../types";
2
+ export type CascaderHookOption<T> = {
3
+ data: T[];
4
+ handleDataConfig: FlatDataConfig;
5
+ selectOption?: SelectOption;
6
+ };
7
+ export declare function useCascader<T extends Record<string, any>>(options: CascaderHookOption<T>): {
8
+ nodes: CascaderNodeType<T>[];
9
+ columns: CascaderNodeType<T>[][];
10
+ showIndices: number[];
11
+ checkStatusArray: Uint8Array<ArrayBufferLike>;
12
+ handleShowIndices: (depth: number, index: number) => void;
13
+ handleChangeCheckStatus: (nodeIdx: number) => void;
14
+ };
package/lib/index.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ export * from "./components";
2
+ export * from "./hooks/useCascader";
3
+ export * from "./utils/index";
4
+ export * from "./types";
@@ -0,0 +1,59 @@
1
+ import { ReactNode } from "react";
2
+ export type CheckStatus = 0 | 1 | 2;
3
+ /**============================================
4
+ ** Data
5
+ *=============================================**/
6
+ export interface CascaderNodeType<T = any> {
7
+ depth: number;
8
+ index: number;
9
+ parentIndex: number;
10
+ range: [number, number];
11
+ childrenIndices: number[];
12
+ pathId: string;
13
+ label: string;
14
+ value: string | number;
15
+ raw: T | null;
16
+ }
17
+ export interface CascaderItem<T> {
18
+ indices: number;
19
+ level: number;
20
+ label: string;
21
+ value: string | number;
22
+ originData?: T;
23
+ checkStatus?: CheckStatus;
24
+ }
25
+ /**============================================
26
+ ** Utils
27
+ *=============================================**/
28
+ export type FlatDataConfig = {
29
+ levels: (string | [string, string])[];
30
+ };
31
+ export type GetShowIndicesOption = {
32
+ oldShowIndices: number[];
33
+ actionIndices: {
34
+ depth: number;
35
+ nodeIdx: number;
36
+ };
37
+ };
38
+ export type GetCascaderColumnsOption = {
39
+ checkStatusArray?: Uint8Array<ArrayBufferLike>;
40
+ };
41
+ /**============================================
42
+ ** Others
43
+ *=============================================**/
44
+ export type SelectOption = {
45
+ defaultSelected: string[];
46
+ selectType: "single" | "multiple";
47
+ };
48
+ /**============================================
49
+ ** Slot
50
+ *=============================================**/
51
+ export type PanelHeaderSlot = (props: {
52
+ column: CascaderNodeType<any>[];
53
+ showNode: CascaderItem<any> | null;
54
+ }) => ReactNode;
55
+ export type PanelIndicatorSlot = (props?: any) => ReactNode;
56
+ export type NodeSlotType = (props: {
57
+ node: CascaderItem<any>;
58
+ onSelectChange?: (nodeIdx: number) => void;
59
+ }) => ReactNode;
@@ -0,0 +1,5 @@
1
+ import { CascaderNodeType, FlatDataConfig, GetShowIndicesOption } from '../types/index.js';
2
+ export declare function buildCascaderEngine<T extends Record<string, any>>(data: T[], config: FlatDataConfig): CascaderNodeType<T>[];
3
+ export declare function getCascadeColumns<T>(nodes: CascaderNodeType<T>[], showIndices: number[]): CascaderNodeType<T>[][];
4
+ export declare function getShowIndices<T>(nodes: CascaderNodeType<T>[], option: GetShowIndicesOption): number[];
5
+ export declare function getDefaultIndices<T>(nodes: CascaderNodeType<T>[]): number[];
@@ -0,0 +1,2 @@
1
+ export * from "./data";
2
+ export * from "./logic";
@@ -0,0 +1,3 @@
1
+ import { CascaderNodeType } from "src/types";
2
+ export declare function setNodeCheckStatus<T>(nodes: CascaderNodeType<T>[], currentStatusArray: Uint8Array, nodeIndex: number): Uint8Array;
3
+ export declare function setMultipleNodeStatus<T>(nodes: CascaderNodeType<T>[], currentStatusArray: Uint8Array, indices: number[], targetStatus: number): Uint8Array;
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@zidian-primitive/cascader",
3
+ "version": "0.0.0-next-20260204023823",
4
+ "main": "./src/index.ts",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/zidcn/component-lib.git",
10
+ "directory": "packages/@zidian-primitive/cascader"
11
+ },
12
+ "exports": {
13
+ ".": {
14
+ "types": "./lib/index.d.ts",
15
+ "import": "./esm/index.js",
16
+ "default": "./esm/index.js"
17
+ }
18
+ },
19
+ "files": [
20
+ "esm",
21
+ "lib",
22
+ "README.md"
23
+ ],
24
+ "author": "GWR",
25
+ "peerDependencies": {
26
+ "react": "^18.x || ^19.x",
27
+ "react-dom": "^18.x || ^19.x"
28
+ },
29
+ "devDependencies": {
30
+ "react": "19.0.0",
31
+ "react-dom": "19.0.0"
32
+ }
33
+ }