torch-glare 2.1.2 → 2.1.4
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/apps/lib/components/Avatar.tsx +1 -1
- package/apps/lib/components/Card.tsx +68 -54
- package/apps/lib/components/DataViews/DataViewRadio.tsx +47 -0
- package/apps/lib/components/DataViews/DataViewsConfigPanel.tsx +56 -45
- package/apps/lib/components/DataViews/DataViewsHeader.tsx +130 -28
- package/apps/lib/components/DataViews/DataViewsLayout.tsx +32 -2
- package/apps/lib/components/DataViews/FilterPanel.tsx +148 -3
- package/apps/lib/components/DataViews/HeaderSearch.tsx +97 -0
- package/apps/lib/components/DataViews/InboxView.tsx +263 -282
- package/apps/lib/components/DataViews/InboxViewCard.tsx +136 -0
- package/apps/lib/components/DataViews/KanbanView.tsx +264 -153
- package/apps/lib/components/DataViews/PanelControls.tsx +10 -41
- package/apps/lib/components/DataViews/TreeView.tsx +220 -191
- package/apps/lib/components/DataViews/index.ts +6 -0
- package/apps/lib/components/DataViews/types.ts +30 -1
- package/apps/lib/components/Radio.tsx +18 -21
- package/apps/lib/components/Switch.tsx +3 -1
- package/apps/lib/components/Table.tsx +1 -1
- package/apps/lib/components/TreeFolder/TreeFolder.tsx +160 -137
- package/apps/lib/components/TreeFolder/TreeFolderRow.tsx +221 -93
- package/apps/lib/components/TreeFolder/types.ts +9 -0
- package/apps/lib/layouts/DataViewCard.tsx +76 -0
- package/dist/src/shared/copyComponentsRecursively.js +9 -1
- package/dist/src/shared/copyComponentsRecursively.js.map +1 -1
- package/dist/src/shared/getDependenciesAndInstallNestedComponents.d.ts.map +1 -1
- package/dist/src/shared/getDependenciesAndInstallNestedComponents.js +9 -1
- package/dist/src/shared/getDependenciesAndInstallNestedComponents.js.map +1 -1
- package/docs/components/data-views-config-panel.md +204 -0
- package/docs/components/data-views-layout.md +270 -0
- package/package.json +1 -1
|
@@ -9,7 +9,9 @@ export type TreeConfig = {
|
|
|
9
9
|
orderField?: string
|
|
10
10
|
nodeLabel?: string
|
|
11
11
|
defaultExpanded?: "all" | "roots" | "none"
|
|
12
|
-
|
|
12
|
+
/** Right-pane mode for the selected tree node. `"details"` is accepted as a
|
|
13
|
+
* deprecated alias of `"card"` for backward compatibility. */
|
|
14
|
+
defaultRightPane?: "table" | "card" | "details"
|
|
13
15
|
dndEnabled?: boolean
|
|
14
16
|
}
|
|
15
17
|
|
|
@@ -51,6 +53,17 @@ export type FieldPreset =
|
|
|
51
53
|
| { label: string; min?: number; max?: number }
|
|
52
54
|
| { label: string; from?: string; to?: string }
|
|
53
55
|
|
|
56
|
+
// Palette keys for the Kanban column header pill. Kept in lockstep with
|
|
57
|
+
// `COLUMN_PALETTE` in KanbanView.tsx so consumers can pick a color per status
|
|
58
|
+
// via `FieldConfig.kanbanVariants`.
|
|
59
|
+
export type KanbanColumnColor =
|
|
60
|
+
| "gray"
|
|
61
|
+
| "purple"
|
|
62
|
+
| "orange"
|
|
63
|
+
| "blue"
|
|
64
|
+
| "green"
|
|
65
|
+
| "red"
|
|
66
|
+
|
|
54
67
|
export type BadgeVariant =
|
|
55
68
|
| "green"
|
|
56
69
|
| "greenLight"
|
|
@@ -102,6 +115,16 @@ export type FieldConfig = {
|
|
|
102
115
|
variants?: Record<string, BadgeVariant>
|
|
103
116
|
defaultVariant?: BadgeVariant
|
|
104
117
|
|
|
118
|
+
// Per-status overrides for the Kanban board view. Keys must match
|
|
119
|
+
// `variants` keys (or any value present in the data). Lets consumers set a
|
|
120
|
+
// human-friendly column title and pick a column pill color without affecting
|
|
121
|
+
// the badge color used elsewhere.
|
|
122
|
+
kanbanVariants?: Record<
|
|
123
|
+
string,
|
|
124
|
+
{ label?: string; color?: KanbanColumnColor }
|
|
125
|
+
>
|
|
126
|
+
|
|
127
|
+
|
|
105
128
|
variant?: BadgeVariant
|
|
106
129
|
limit?: number
|
|
107
130
|
|
|
@@ -131,6 +154,12 @@ export type FieldConfig = {
|
|
|
131
154
|
filterable?: boolean
|
|
132
155
|
filterLabel?: string
|
|
133
156
|
filterOptions?: string[] | { label: string; value: string }[]
|
|
157
|
+
/**
|
|
158
|
+
* Categorical filter selection mode.
|
|
159
|
+
* - "multi" (default): checkboxes, multi-select. FilterValue is the array of picked options.
|
|
160
|
+
* - "single": radios, single-select. FilterValue is a 1-element array.
|
|
161
|
+
*/
|
|
162
|
+
filterMode?: "single" | "multi"
|
|
134
163
|
presets?: FieldPreset[]
|
|
135
164
|
rangeMin?: number
|
|
136
165
|
rangeMax?: number
|
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
|
|
1
|
+
"use client";
|
|
2
2
|
import { cn } from "../utils/cn";
|
|
3
3
|
import { cva } from "class-variance-authority";
|
|
4
|
-
import * as React from "react"
|
|
5
|
-
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
|
|
6
|
-
|
|
4
|
+
import * as React from "react";
|
|
5
|
+
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group";
|
|
7
6
|
|
|
8
7
|
const RadioGroup = React.forwardRef<
|
|
9
8
|
React.ElementRef<typeof RadioGroupPrimitive.Root>,
|
|
@@ -11,37 +10,35 @@ const RadioGroup = React.forwardRef<
|
|
|
11
10
|
>(({ className, ...props }, ref) => {
|
|
12
11
|
return (
|
|
13
12
|
<RadioGroupPrimitive.Root
|
|
14
|
-
className={cn("grid gap-
|
|
13
|
+
className={cn("grid gap-0.5", className)}
|
|
15
14
|
{...props}
|
|
16
15
|
ref={ref}
|
|
17
16
|
/>
|
|
18
|
-
)
|
|
19
|
-
})
|
|
20
|
-
RadioGroup.displayName = RadioGroupPrimitive.Root.displayName
|
|
17
|
+
);
|
|
18
|
+
});
|
|
19
|
+
RadioGroup.displayName = RadioGroupPrimitive.Root.displayName;
|
|
21
20
|
|
|
22
21
|
const Radio = React.forwardRef<
|
|
23
22
|
React.ElementRef<typeof RadioGroupPrimitive.Item>,
|
|
24
|
-
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item> & {
|
|
23
|
+
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item> & {
|
|
24
|
+
size?: "S" | "M";
|
|
25
|
+
}
|
|
25
26
|
>(({ className, size = "S", ...props }, ref) => {
|
|
26
27
|
return (
|
|
27
28
|
<RadioGroupPrimitive.Item
|
|
28
29
|
ref={ref}
|
|
29
|
-
className={cn(
|
|
30
|
-
glareRadioStyles({ size }),
|
|
31
|
-
className
|
|
32
|
-
)}
|
|
30
|
+
className={cn(glareRadioStyles({ size }), className)}
|
|
33
31
|
{...props}
|
|
34
32
|
>
|
|
35
33
|
<RadioGroupPrimitive.Indicator className="bg-white rounded-full flex items-center justify-center shrink-0">
|
|
36
34
|
<i className="ri-record-circle-fill text-background-presentation-state-information-primary leading-none shrink-0"></i>
|
|
37
35
|
</RadioGroupPrimitive.Indicator>
|
|
38
36
|
</RadioGroupPrimitive.Item>
|
|
39
|
-
)
|
|
40
|
-
})
|
|
41
|
-
Radio.displayName = "Radio"
|
|
42
|
-
|
|
43
|
-
export { RadioGroup, Radio }
|
|
37
|
+
);
|
|
38
|
+
});
|
|
39
|
+
Radio.displayName = "Radio";
|
|
44
40
|
|
|
41
|
+
export { RadioGroup, Radio };
|
|
45
42
|
|
|
46
43
|
const glareRadioStyles = cva(
|
|
47
44
|
[
|
|
@@ -61,6 +58,6 @@ const glareRadioStyles = cva(
|
|
|
61
58
|
S: ["w-[12px]", "h-[12px] [&_i]:text-[10px] [&_i]:scale-[1.5]"],
|
|
62
59
|
M: ["w-[24px]", "h-[24px] [&_i]:text-[20px] [&_i]:scale-[1.47]"],
|
|
63
60
|
},
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
);
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
);
|
|
@@ -17,7 +17,9 @@ const Switch = React.forwardRef<
|
|
|
17
17
|
>
|
|
18
18
|
<SwitchPrimitives.Thumb
|
|
19
19
|
className={cn(
|
|
20
|
-
"pointer-events-none block w-[24px] h-[24px] rounded-full bg-background-presentation-switcher-knob shadow-lg ring-0 transition-transform
|
|
20
|
+
"pointer-events-none block w-[24px] h-[24px] rounded-full bg-background-presentation-switcher-knob shadow-lg ring-0 transition-transform",
|
|
21
|
+
"data-[state=checked]:translate-x-[21px] data-[state=unchecked]:translate-x-0",
|
|
22
|
+
"rtl:data-[state=checked]:-translate-x-[21px] rtl:data-[state=unchecked]:translate-x-0"
|
|
21
23
|
)}
|
|
22
24
|
/>
|
|
23
25
|
</SwitchPrimitives.Root>
|
|
@@ -128,7 +128,7 @@ const TableHead = React.forwardRef<
|
|
|
128
128
|
<th
|
|
129
129
|
ref={headRef}
|
|
130
130
|
className={cn(
|
|
131
|
-
"relative py-[
|
|
131
|
+
"relative py-[6px] px-[4px] border-b-[2px] border-border-presentation-table-header",
|
|
132
132
|
)}
|
|
133
133
|
>
|
|
134
134
|
<div
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"use client"
|
|
1
|
+
"use client";
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
4
|
forwardRef,
|
|
@@ -8,102 +8,121 @@ import {
|
|
|
8
8
|
useRef,
|
|
9
9
|
useState,
|
|
10
10
|
type ReactNode,
|
|
11
|
-
} from "react"
|
|
12
|
-
import { cn } from "../../utils/cn"
|
|
13
|
-
import type { Themes } from "../../utils/types"
|
|
14
|
-
import { TreeFolderBreadcrumb } from "./TreeFolderBreadcrumb"
|
|
15
|
-
import { TreeFolderRow } from "./TreeFolderRow"
|
|
16
|
-
import { TreeFolderStyles } from "./TreeFolderStyles"
|
|
11
|
+
} from "react";
|
|
12
|
+
import { cn } from "../../utils/cn";
|
|
13
|
+
import type { Themes } from "../../utils/types";
|
|
14
|
+
import { TreeFolderBreadcrumb } from "./TreeFolderBreadcrumb";
|
|
15
|
+
import { TreeFolderRow } from "./TreeFolderRow";
|
|
16
|
+
import { TreeFolderStyles } from "./TreeFolderStyles";
|
|
17
17
|
import {
|
|
18
18
|
applyMove,
|
|
19
19
|
descendantIds,
|
|
20
20
|
findNode,
|
|
21
21
|
findPath,
|
|
22
22
|
toBreadcrumb,
|
|
23
|
-
} from "./treeFolderUtils"
|
|
23
|
+
} from "./treeFolderUtils";
|
|
24
24
|
import type {
|
|
25
25
|
TreeFolderIconResolver,
|
|
26
26
|
TreeFolderMoveArgs,
|
|
27
27
|
TreeFolderNode,
|
|
28
28
|
TreeFolderVisibleRow,
|
|
29
|
-
} from "./types"
|
|
30
|
-
import { useTreeFolderDnD } from "./useTreeFolderDnD"
|
|
29
|
+
} from "./types";
|
|
30
|
+
import { useTreeFolderDnD } from "./useTreeFolderDnD";
|
|
31
31
|
|
|
32
32
|
export type TreeFolderProps = {
|
|
33
|
-
data: TreeFolderNode[]
|
|
34
|
-
selectedId?: string | null
|
|
35
|
-
defaultSelectedId?: string | null
|
|
36
|
-
onSelectionChange?: (id: string | null) => void
|
|
37
|
-
|
|
38
|
-
expandedIds?: string[]
|
|
39
|
-
defaultExpanded?: "all" | "roots" | "none" | string[]
|
|
40
|
-
onExpandedChange?: (ids: string[]) => void
|
|
41
|
-
|
|
42
|
-
dndEnabled?: boolean
|
|
43
|
-
onMove?: (args: TreeFolderMoveArgs) => void
|
|
44
|
-
onDataChange?: (next: TreeFolderNode[]) => void
|
|
45
|
-
|
|
46
|
-
iconFor?: TreeFolderIconResolver
|
|
47
|
-
|
|
48
|
-
title?: string
|
|
49
|
-
showBreadcrumb?: boolean
|
|
50
|
-
showHeader?: boolean
|
|
51
|
-
highlightAncestors?: boolean
|
|
52
|
-
highlightSubtree?: boolean
|
|
53
|
-
headerAccessory?: ReactNode
|
|
54
|
-
emptyState?: ReactNode
|
|
55
|
-
|
|
56
|
-
rowHeight?: number
|
|
57
|
-
indent?: number
|
|
33
|
+
data: TreeFolderNode[];
|
|
34
|
+
selectedId?: string | null;
|
|
35
|
+
defaultSelectedId?: string | null;
|
|
36
|
+
onSelectionChange?: (id: string | null) => void;
|
|
37
|
+
|
|
38
|
+
expandedIds?: string[];
|
|
39
|
+
defaultExpanded?: "all" | "roots" | "none" | string[];
|
|
40
|
+
onExpandedChange?: (ids: string[]) => void;
|
|
41
|
+
|
|
42
|
+
dndEnabled?: boolean;
|
|
43
|
+
onMove?: (args: TreeFolderMoveArgs) => void;
|
|
44
|
+
onDataChange?: (next: TreeFolderNode[]) => void;
|
|
45
|
+
|
|
46
|
+
iconFor?: TreeFolderIconResolver;
|
|
47
|
+
|
|
48
|
+
title?: string;
|
|
49
|
+
showBreadcrumb?: boolean;
|
|
50
|
+
showHeader?: boolean;
|
|
51
|
+
highlightAncestors?: boolean;
|
|
52
|
+
highlightSubtree?: boolean;
|
|
53
|
+
headerAccessory?: ReactNode;
|
|
54
|
+
emptyState?: ReactNode;
|
|
55
|
+
|
|
56
|
+
rowHeight?: number;
|
|
57
|
+
indent?: number;
|
|
58
58
|
/** Optional. If set, the row strip will be at least this wide (in px) — useful
|
|
59
59
|
* when you want the band to extend across a wider canvas regardless of content. */
|
|
60
|
-
contentMinWidth?: number
|
|
60
|
+
contentMinWidth?: number;
|
|
61
61
|
|
|
62
|
-
className?: string
|
|
63
|
-
theme?: Themes
|
|
64
|
-
}
|
|
62
|
+
className?: string;
|
|
63
|
+
theme?: Themes;
|
|
64
|
+
};
|
|
65
65
|
|
|
66
66
|
export type TreeFolderHandle = {
|
|
67
|
-
selectId: (id: string | null) => void
|
|
68
|
-
expandAll: () => void
|
|
69
|
-
collapseAll: () => void
|
|
70
|
-
scrollToId: (id: string) => void
|
|
71
|
-
}
|
|
67
|
+
selectId: (id: string | null) => void;
|
|
68
|
+
expandAll: () => void;
|
|
69
|
+
collapseAll: () => void;
|
|
70
|
+
scrollToId: (id: string) => void;
|
|
71
|
+
};
|
|
72
72
|
|
|
73
|
-
const ROW_HEIGHT_DEFAULT = 28
|
|
74
|
-
const INDENT_DEFAULT = 14
|
|
73
|
+
const ROW_HEIGHT_DEFAULT = 28;
|
|
74
|
+
const INDENT_DEFAULT = 14;
|
|
75
75
|
|
|
76
76
|
function collectIds(nodes: TreeFolderNode[], out: string[] = []): string[] {
|
|
77
77
|
for (const n of nodes) {
|
|
78
|
-
out.push(n.id)
|
|
79
|
-
if (n.children) collectIds(n.children, out)
|
|
78
|
+
out.push(n.id);
|
|
79
|
+
if (n.children) collectIds(n.children, out);
|
|
80
80
|
}
|
|
81
|
-
return out
|
|
81
|
+
return out;
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
function rootIds(nodes: TreeFolderNode[]): string[] {
|
|
85
|
-
return nodes.map((n) => n.id)
|
|
85
|
+
return nodes.map((n) => n.id);
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
function flattenVisible(
|
|
89
89
|
data: TreeFolderNode[],
|
|
90
90
|
expanded: Set<string>,
|
|
91
91
|
): TreeFolderVisibleRow[] {
|
|
92
|
-
const out: TreeFolderVisibleRow[] = []
|
|
92
|
+
const out: TreeFolderVisibleRow[] = [];
|
|
93
|
+
// `ancestorHasMore` tracks, for each open ancestor depth on the descent
|
|
94
|
+
// path, whether that ancestor still has more siblings to render. We push
|
|
95
|
+
// before descending and pop on the way back up; each row snapshots the
|
|
96
|
+
// current state so its connector gutters know which verticals to draw.
|
|
97
|
+
const ancestorHasMore: boolean[] = [];
|
|
93
98
|
const walk = (
|
|
94
99
|
nodes: TreeFolderNode[],
|
|
95
100
|
level: number,
|
|
96
101
|
parentId: string | null,
|
|
97
102
|
) => {
|
|
98
103
|
nodes.forEach((node, idx) => {
|
|
99
|
-
const isInternal = !!(node.children && node.children.length > 0)
|
|
100
|
-
const isOpen = isInternal && expanded.has(node.id)
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
104
|
+
const isInternal = !!(node.children && node.children.length > 0);
|
|
105
|
+
const isOpen = isInternal && expanded.has(node.id);
|
|
106
|
+
const isLastChild = idx === nodes.length - 1;
|
|
107
|
+
out.push({
|
|
108
|
+
node,
|
|
109
|
+
level,
|
|
110
|
+
parentId,
|
|
111
|
+
childIndex: idx,
|
|
112
|
+
isOpen,
|
|
113
|
+
isInternal,
|
|
114
|
+
isLastChild,
|
|
115
|
+
ancestorHasMoreSiblings: ancestorHasMore.slice(),
|
|
116
|
+
});
|
|
117
|
+
if (isOpen && node.children) {
|
|
118
|
+
ancestorHasMore.push(!isLastChild);
|
|
119
|
+
walk(node.children, level + 1, node.id);
|
|
120
|
+
ancestorHasMore.pop();
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
};
|
|
124
|
+
walk(data, 0, null);
|
|
125
|
+
return out;
|
|
107
126
|
}
|
|
108
127
|
|
|
109
128
|
export const TreeFolder = forwardRef<TreeFolderHandle, TreeFolderProps>(
|
|
@@ -123,7 +142,7 @@ export const TreeFolder = forwardRef<TreeFolderHandle, TreeFolderProps>(
|
|
|
123
142
|
title = "Layers",
|
|
124
143
|
showBreadcrumb = true,
|
|
125
144
|
showHeader = true,
|
|
126
|
-
highlightAncestors =
|
|
145
|
+
highlightAncestors = false,
|
|
127
146
|
highlightSubtree = true,
|
|
128
147
|
headerAccessory,
|
|
129
148
|
emptyState,
|
|
@@ -132,104 +151,104 @@ export const TreeFolder = forwardRef<TreeFolderHandle, TreeFolderProps>(
|
|
|
132
151
|
contentMinWidth,
|
|
133
152
|
className,
|
|
134
153
|
theme,
|
|
135
|
-
} = props
|
|
154
|
+
} = props;
|
|
136
155
|
|
|
137
|
-
const scrollRef = useRef<HTMLDivElement | null>(null)
|
|
156
|
+
const scrollRef = useRef<HTMLDivElement | null>(null);
|
|
138
157
|
|
|
139
158
|
// ---- Selection state ----
|
|
140
159
|
const [internalSelected, setInternalSelected] = useState<string | null>(
|
|
141
160
|
defaultSelectedId,
|
|
142
|
-
)
|
|
161
|
+
);
|
|
143
162
|
const selectedId =
|
|
144
|
-
controlledSelected !== undefined ? controlledSelected : internalSelected
|
|
145
|
-
const isSelectionControlled = controlledSelected !== undefined
|
|
163
|
+
controlledSelected !== undefined ? controlledSelected : internalSelected;
|
|
164
|
+
const isSelectionControlled = controlledSelected !== undefined;
|
|
146
165
|
|
|
147
166
|
// ---- Expansion state ----
|
|
148
167
|
const [internalExpanded, setInternalExpanded] = useState<string[]>(() => {
|
|
149
|
-
if (Array.isArray(defaultExpanded)) return defaultExpanded
|
|
150
|
-
if (defaultExpanded === "all") return collectIds(data)
|
|
151
|
-
if (defaultExpanded === "none") return []
|
|
152
|
-
return rootIds(data)
|
|
153
|
-
})
|
|
154
|
-
const expandedIds = controlledExpanded ?? internalExpanded
|
|
155
|
-
const isExpansionControlled = controlledExpanded !== undefined
|
|
156
|
-
const expandedSet = useMemo(() => new Set(expandedIds), [expandedIds])
|
|
168
|
+
if (Array.isArray(defaultExpanded)) return defaultExpanded;
|
|
169
|
+
if (defaultExpanded === "all") return collectIds(data);
|
|
170
|
+
if (defaultExpanded === "none") return [];
|
|
171
|
+
return rootIds(data);
|
|
172
|
+
});
|
|
173
|
+
const expandedIds = controlledExpanded ?? internalExpanded;
|
|
174
|
+
const isExpansionControlled = controlledExpanded !== undefined;
|
|
175
|
+
const expandedSet = useMemo(() => new Set(expandedIds), [expandedIds]);
|
|
157
176
|
|
|
158
177
|
// ---- Visible rows ----
|
|
159
178
|
const visibleRows = useMemo(
|
|
160
179
|
() => flattenVisible(data, expandedSet),
|
|
161
180
|
[data, expandedSet],
|
|
162
|
-
)
|
|
181
|
+
);
|
|
163
182
|
const rowsById = useMemo(() => {
|
|
164
|
-
const m = new Map<string, TreeFolderVisibleRow>()
|
|
165
|
-
for (const r of visibleRows) m.set(r.node.id, r)
|
|
166
|
-
return m
|
|
167
|
-
}, [visibleRows])
|
|
183
|
+
const m = new Map<string, TreeFolderVisibleRow>();
|
|
184
|
+
for (const r of visibleRows) m.set(r.node.id, r);
|
|
185
|
+
return m;
|
|
186
|
+
}, [visibleRows]);
|
|
168
187
|
|
|
169
188
|
// ---- Ancestor + subtree highlight ----
|
|
170
189
|
const ancestorPath = useMemo(
|
|
171
190
|
() => (selectedId ? findPath(data, selectedId) : null),
|
|
172
191
|
[data, selectedId],
|
|
173
|
-
)
|
|
192
|
+
);
|
|
174
193
|
const ancestorIdSet = useMemo(
|
|
175
194
|
() => new Set((ancestorPath ?? []).slice(0, -1).map((n) => n.id)),
|
|
176
195
|
[ancestorPath],
|
|
177
|
-
)
|
|
196
|
+
);
|
|
178
197
|
const isAncestorFn = useCallback(
|
|
179
198
|
(id: string) => highlightAncestors && ancestorIdSet.has(id),
|
|
180
199
|
[highlightAncestors, ancestorIdSet],
|
|
181
|
-
)
|
|
200
|
+
);
|
|
182
201
|
|
|
183
202
|
const descendantIdSet = useMemo(() => {
|
|
184
|
-
if (!highlightSubtree || !selectedId) return new Set<string>()
|
|
185
|
-
const node = findNode(data, selectedId)
|
|
186
|
-
if (!node) return new Set<string>()
|
|
187
|
-
return descendantIds(node, false)
|
|
188
|
-
}, [data, selectedId, highlightSubtree])
|
|
203
|
+
if (!highlightSubtree || !selectedId) return new Set<string>();
|
|
204
|
+
const node = findNode(data, selectedId);
|
|
205
|
+
if (!node) return new Set<string>();
|
|
206
|
+
return descendantIds(node, false);
|
|
207
|
+
}, [data, selectedId, highlightSubtree]);
|
|
189
208
|
const isDescendantOfSelected = useCallback(
|
|
190
209
|
(id: string) => descendantIdSet.has(id),
|
|
191
210
|
[descendantIdSet],
|
|
192
|
-
)
|
|
211
|
+
);
|
|
193
212
|
|
|
194
213
|
const breadcrumbItems = useMemo(
|
|
195
214
|
() => (ancestorPath ? toBreadcrumb(ancestorPath) : []),
|
|
196
215
|
[ancestorPath],
|
|
197
|
-
)
|
|
216
|
+
);
|
|
198
217
|
|
|
199
218
|
// ---- Handlers ----
|
|
200
219
|
const handleSelect = useCallback(
|
|
201
220
|
(id: string | null) => {
|
|
202
|
-
if (!isSelectionControlled) setInternalSelected(id)
|
|
203
|
-
onSelectionChange?.(id)
|
|
221
|
+
if (!isSelectionControlled) setInternalSelected(id);
|
|
222
|
+
onSelectionChange?.(id);
|
|
204
223
|
},
|
|
205
224
|
[isSelectionControlled, onSelectionChange],
|
|
206
|
-
)
|
|
225
|
+
);
|
|
207
226
|
|
|
208
227
|
const handleToggle = useCallback(
|
|
209
228
|
(id: string) => {
|
|
210
229
|
const next = expandedIds.includes(id)
|
|
211
230
|
? expandedIds.filter((x) => x !== id)
|
|
212
|
-
: [...expandedIds, id]
|
|
213
|
-
if (!isExpansionControlled) setInternalExpanded(next)
|
|
214
|
-
onExpandedChange?.(next)
|
|
231
|
+
: [...expandedIds, id];
|
|
232
|
+
if (!isExpansionControlled) setInternalExpanded(next);
|
|
233
|
+
onExpandedChange?.(next);
|
|
215
234
|
},
|
|
216
235
|
[expandedIds, isExpansionControlled, onExpandedChange],
|
|
217
|
-
)
|
|
236
|
+
);
|
|
218
237
|
|
|
219
238
|
const handleMoveInternal = useCallback(
|
|
220
239
|
(args: TreeFolderMoveArgs) => {
|
|
221
|
-
onMove?.(args)
|
|
222
|
-
if (onDataChange) onDataChange(applyMove(data, args))
|
|
240
|
+
onMove?.(args);
|
|
241
|
+
if (onDataChange) onDataChange(applyMove(data, args));
|
|
223
242
|
},
|
|
224
243
|
[data, onMove, onDataChange],
|
|
225
|
-
)
|
|
244
|
+
);
|
|
226
245
|
|
|
227
246
|
const scrollToId = useCallback((id: string) => {
|
|
228
247
|
const el = scrollRef.current?.querySelector<HTMLElement>(
|
|
229
248
|
`[data-row-id="${CSS.escape(id)}"]`,
|
|
230
|
-
)
|
|
231
|
-
el?.scrollIntoView({ block: "nearest", behavior: "smooth" })
|
|
232
|
-
}, [])
|
|
249
|
+
);
|
|
250
|
+
el?.scrollIntoView({ block: "nearest", behavior: "smooth" });
|
|
251
|
+
}, []);
|
|
233
252
|
|
|
234
253
|
// ---- DnD ----
|
|
235
254
|
const { dragIds, dropTarget, getRowDragHandlers } = useTreeFolderDnD({
|
|
@@ -240,32 +259,32 @@ export const TreeFolder = forwardRef<TreeFolderHandle, TreeFolderProps>(
|
|
|
240
259
|
onMove: handleMoveInternal,
|
|
241
260
|
canDrop: ({ parentId }) => {
|
|
242
261
|
// Honor per-node `droppable: false` on the resolved parent.
|
|
243
|
-
if (parentId == null) return true
|
|
244
|
-
const target = findNode(data, parentId)
|
|
245
|
-
if (!target) return false
|
|
246
|
-
if (target.disabled || target.droppable === false) return false
|
|
247
|
-
return true
|
|
262
|
+
if (parentId == null) return true;
|
|
263
|
+
const target = findNode(data, parentId);
|
|
264
|
+
if (!target) return false;
|
|
265
|
+
if (target.disabled || target.droppable === false) return false;
|
|
266
|
+
return true;
|
|
248
267
|
},
|
|
249
|
-
})
|
|
250
|
-
const dragIdSet = useMemo(() => new Set(dragIds), [dragIds])
|
|
268
|
+
});
|
|
269
|
+
const dragIdSet = useMemo(() => new Set(dragIds), [dragIds]);
|
|
251
270
|
|
|
252
271
|
// ---- Imperative handle ----
|
|
253
272
|
useImperativeHandle(
|
|
254
273
|
ref,
|
|
255
274
|
() => ({
|
|
256
275
|
selectId: (id: string | null) => {
|
|
257
|
-
if (!isSelectionControlled) setInternalSelected(id)
|
|
258
|
-
onSelectionChange?.(id)
|
|
259
|
-
if (id) scrollToId(id)
|
|
276
|
+
if (!isSelectionControlled) setInternalSelected(id);
|
|
277
|
+
onSelectionChange?.(id);
|
|
278
|
+
if (id) scrollToId(id);
|
|
260
279
|
},
|
|
261
280
|
expandAll: () => {
|
|
262
|
-
const all = collectIds(data)
|
|
263
|
-
if (!isExpansionControlled) setInternalExpanded(all)
|
|
264
|
-
onExpandedChange?.(all)
|
|
281
|
+
const all = collectIds(data);
|
|
282
|
+
if (!isExpansionControlled) setInternalExpanded(all);
|
|
283
|
+
onExpandedChange?.(all);
|
|
265
284
|
},
|
|
266
285
|
collapseAll: () => {
|
|
267
|
-
if (!isExpansionControlled) setInternalExpanded([])
|
|
268
|
-
onExpandedChange?.([])
|
|
286
|
+
if (!isExpansionControlled) setInternalExpanded([]);
|
|
287
|
+
onExpandedChange?.([]);
|
|
269
288
|
},
|
|
270
289
|
scrollToId,
|
|
271
290
|
}),
|
|
@@ -277,12 +296,12 @@ export const TreeFolder = forwardRef<TreeFolderHandle, TreeFolderProps>(
|
|
|
277
296
|
onExpandedChange,
|
|
278
297
|
scrollToId,
|
|
279
298
|
],
|
|
280
|
-
)
|
|
299
|
+
);
|
|
281
300
|
|
|
282
|
-
const isEmpty = data.length === 0
|
|
301
|
+
const isEmpty = data.length === 0;
|
|
283
302
|
const stripStyle = contentMinWidth
|
|
284
303
|
? { minWidth: contentMinWidth }
|
|
285
|
-
: undefined
|
|
304
|
+
: undefined;
|
|
286
305
|
|
|
287
306
|
return (
|
|
288
307
|
<div
|
|
@@ -306,8 +325,8 @@ export const TreeFolder = forwardRef<TreeFolderHandle, TreeFolderProps>(
|
|
|
306
325
|
<TreeFolderBreadcrumb
|
|
307
326
|
items={breadcrumbItems}
|
|
308
327
|
onSelect={(id) => {
|
|
309
|
-
handleSelect(id)
|
|
310
|
-
scrollToId(id)
|
|
328
|
+
handleSelect(id);
|
|
329
|
+
scrollToId(id);
|
|
311
330
|
}}
|
|
312
331
|
/>
|
|
313
332
|
</div>
|
|
@@ -320,19 +339,20 @@ export const TreeFolder = forwardRef<TreeFolderHandle, TreeFolderProps>(
|
|
|
320
339
|
className="tf-scroll flex-1 min-h-0 overflow-auto"
|
|
321
340
|
>
|
|
322
341
|
{isEmpty ? (
|
|
323
|
-
emptyState ?? (
|
|
342
|
+
(emptyState ?? (
|
|
324
343
|
<div className="text-xs text-content-presentation-global-tertiary p-3">
|
|
325
344
|
Nothing here yet.
|
|
326
345
|
</div>
|
|
327
|
-
)
|
|
346
|
+
))
|
|
328
347
|
) : (
|
|
329
348
|
<div className="min-w-max" style={stripStyle}>
|
|
330
349
|
{visibleRows.map((row, idx) => {
|
|
331
|
-
const prevRow = visibleRows[idx - 1]
|
|
332
|
-
const nextRow = visibleRows[idx + 1]
|
|
333
|
-
const isSelected = selectedId === row.node.id
|
|
334
|
-
const isDescendant =
|
|
335
|
-
|
|
350
|
+
const prevRow = visibleRows[idx - 1];
|
|
351
|
+
const nextRow = visibleRows[idx + 1];
|
|
352
|
+
const isSelected = selectedId === row.node.id;
|
|
353
|
+
const isDescendant =
|
|
354
|
+
isDescendantOfSelected(row.node.id) && !isSelected;
|
|
355
|
+
const inBand = isSelected || isDescendant;
|
|
336
356
|
|
|
337
357
|
// Neighbour-aware band rounding: a row is "in-band" if it's the selected
|
|
338
358
|
// node itself or one of its descendants. The Set lookup handles deep nesting.
|
|
@@ -340,20 +360,23 @@ export const TreeFolder = forwardRef<TreeFolderHandle, TreeFolderProps>(
|
|
|
340
360
|
inBand &&
|
|
341
361
|
!!prevRow &&
|
|
342
362
|
(prevRow.node.id === selectedId ||
|
|
343
|
-
descendantIdSet.has(prevRow.node.id))
|
|
363
|
+
descendantIdSet.has(prevRow.node.id));
|
|
344
364
|
const nextInBand =
|
|
345
365
|
inBand &&
|
|
346
366
|
!!nextRow &&
|
|
347
367
|
(nextRow.node.id === selectedId ||
|
|
348
|
-
descendantIdSet.has(nextRow.node.id))
|
|
368
|
+
descendantIdSet.has(nextRow.node.id));
|
|
349
369
|
|
|
350
|
-
const isDragging = dragIdSet.has(row.node.id)
|
|
370
|
+
const isDragging = dragIdSet.has(row.node.id);
|
|
351
371
|
const isDropTargetInside =
|
|
352
|
-
dropTarget?.rowId === row.node.id &&
|
|
372
|
+
dropTarget?.rowId === row.node.id &&
|
|
373
|
+
dropTarget.position === "inside";
|
|
353
374
|
const isDropBefore =
|
|
354
|
-
dropTarget?.rowId === row.node.id &&
|
|
375
|
+
dropTarget?.rowId === row.node.id &&
|
|
376
|
+
dropTarget.position === "before";
|
|
355
377
|
const isDropAfter =
|
|
356
|
-
dropTarget?.rowId === row.node.id &&
|
|
378
|
+
dropTarget?.rowId === row.node.id &&
|
|
379
|
+
dropTarget.position === "after";
|
|
357
380
|
|
|
358
381
|
return (
|
|
359
382
|
<TreeFolderRow
|
|
@@ -376,12 +399,12 @@ export const TreeFolder = forwardRef<TreeFolderHandle, TreeFolderProps>(
|
|
|
376
399
|
onToggle={handleToggle}
|
|
377
400
|
dragHandlers={getRowDragHandlers(row.node.id)}
|
|
378
401
|
/>
|
|
379
|
-
)
|
|
402
|
+
);
|
|
380
403
|
})}
|
|
381
404
|
</div>
|
|
382
405
|
)}
|
|
383
406
|
</div>
|
|
384
407
|
</div>
|
|
385
|
-
)
|
|
408
|
+
);
|
|
386
409
|
},
|
|
387
|
-
)
|
|
410
|
+
);
|