@flux-ui/components 3.0.0-next.30 → 3.0.0-next.32
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/dist/component/FluxFormTreeViewSelect.vue.d.ts +209 -0
- package/dist/component/FluxOverflowBar.vue.d.ts +2 -0
- package/dist/component/FluxTreeView.vue.d.ts +16 -0
- package/dist/component/index.d.ts +2 -0
- package/dist/composable/private/index.d.ts +2 -0
- package/dist/composable/private/useTreeView.d.ts +28 -0
- package/dist/index.css +258 -0
- package/dist/index.js +717 -185
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/component/FluxCheckbox.vue +2 -2
- package/src/component/FluxFilterBar.vue +1 -0
- package/src/component/FluxFormSlider.vue +1 -1
- package/src/component/FluxFormTreeViewSelect.vue +359 -0
- package/src/component/FluxOverflowBar.vue +2 -2
- package/src/component/FluxPressable.vue +13 -12
- package/src/component/FluxSegmentedControl.vue +35 -8
- package/src/component/FluxTreeView.vue +116 -0
- package/src/component/index.ts +2 -0
- package/src/composable/private/index.ts +2 -0
- package/src/composable/private/useTreeView.ts +186 -0
- package/src/css/component/TreeView.module.scss +34 -0
- package/src/css/component/TreeViewSelect.module.scss +73 -0
- package/src/css/mixin/index.scss +1 -0
- package/src/css/mixin/tree-node.scss +94 -0
|
@@ -1,2 +1,4 @@
|
|
|
1
1
|
export { default as useFormSelect } from './useFormSelect';
|
|
2
2
|
export { default as useTranslate } from './useTranslate';
|
|
3
|
+
export { flattenAll, flattenVisible, getLevelColor, INITIAL_HIGHLIGHTED_INDEX, useTreeView } from './useTreeView';
|
|
4
|
+
export type { TreeBaseOption, TreeFlatNode } from './useTreeView';
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import type { FluxColor, FluxIconName } from '@flux-ui/types';
|
|
2
|
+
import { type ComputedRef, nextTick, ref, type Ref, unref, watch } from 'vue';
|
|
3
|
+
|
|
4
|
+
export type TreeBaseOption = {
|
|
5
|
+
readonly id: string | number;
|
|
6
|
+
readonly label: string;
|
|
7
|
+
readonly icon?: FluxIconName;
|
|
8
|
+
readonly children?: TreeBaseOption[];
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export type TreeFlatNode<TOption extends TreeBaseOption = TreeBaseOption> = TOption & {
|
|
12
|
+
readonly depth: number;
|
|
13
|
+
readonly isLast: boolean;
|
|
14
|
+
readonly lineGuides: boolean[];
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const FLUX_COLORS: FluxColor[] = ['gray', 'primary', 'danger', 'info', 'success', 'warning'];
|
|
18
|
+
export const INITIAL_HIGHLIGHTED_INDEX = -1;
|
|
19
|
+
|
|
20
|
+
export function flattenVisible<TOption extends TreeBaseOption>(
|
|
21
|
+
nodes: TOption[],
|
|
22
|
+
depth: number,
|
|
23
|
+
expanded: Set<string | number>,
|
|
24
|
+
parentGuides: boolean[] = []
|
|
25
|
+
): TreeFlatNode<TOption>[] {
|
|
26
|
+
return nodes.flatMap((node, index) => {
|
|
27
|
+
const isLast = index === nodes.length - 1;
|
|
28
|
+
const flatNode = {...node, depth, isLast, lineGuides: parentGuides} as TreeFlatNode<TOption>;
|
|
29
|
+
|
|
30
|
+
if (node.children?.length && expanded.has(node.id)) {
|
|
31
|
+
const childGuides = [...parentGuides, !isLast];
|
|
32
|
+
return [flatNode, ...flattenVisible(node.children as TOption[], depth + 1, expanded, childGuides)];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return [flatNode];
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function flattenAll<TOption extends TreeBaseOption>(
|
|
40
|
+
nodes: TOption[],
|
|
41
|
+
depth = 0
|
|
42
|
+
): TreeFlatNode<TOption>[] {
|
|
43
|
+
return nodes.flatMap(node => [
|
|
44
|
+
{...node, depth, isLast: false, lineGuides: [] as boolean[]} as TreeFlatNode<TOption>,
|
|
45
|
+
...(node.children ? flattenAll(node.children as TOption[], depth + 1) : [])
|
|
46
|
+
]);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function getLevelColor(depth: number, levelColors?: (FluxColor | string)[]): string | undefined {
|
|
50
|
+
if (!levelColors || depth >= levelColors.length) {
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const color = levelColors[depth];
|
|
55
|
+
|
|
56
|
+
if (FLUX_COLORS.includes(color as FluxColor)) {
|
|
57
|
+
return `var(--${color}-600)`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return color;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function useTreeView<TNode extends TreeFlatNode>(params: {
|
|
64
|
+
readonly expandedIds: Ref<Set<string | number>>;
|
|
65
|
+
readonly nodeElementRefs: Readonly<Ref<HTMLDivElement[] | null | undefined>>;
|
|
66
|
+
readonly visibleNodes: ComputedRef<TNode[]>;
|
|
67
|
+
}): {
|
|
68
|
+
readonly highlightedIndex: Ref<number>;
|
|
69
|
+
toggleExpand: (nodeId: string | number) => void;
|
|
70
|
+
onExpandClick: (node: TNode, evt: MouseEvent) => void;
|
|
71
|
+
onKeyNavigate: (evt: KeyboardEvent, onActivate: (node: TNode) => void) => boolean;
|
|
72
|
+
} {
|
|
73
|
+
const highlightedIndex = ref(INITIAL_HIGHLIGHTED_INDEX);
|
|
74
|
+
|
|
75
|
+
function toggleExpand(nodeId: string | number): void {
|
|
76
|
+
const ids = new Set(unref(params.expandedIds));
|
|
77
|
+
|
|
78
|
+
if (ids.has(nodeId)) {
|
|
79
|
+
ids.delete(nodeId);
|
|
80
|
+
} else {
|
|
81
|
+
ids.add(nodeId);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
params.expandedIds.value = ids;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function onExpandClick(node: TNode, evt: MouseEvent): void {
|
|
88
|
+
if (!node.children?.length) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
evt.stopPropagation();
|
|
93
|
+
toggleExpand(node.id);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function onKeyNavigate(evt: KeyboardEvent, onActivate: (node: TNode) => void): boolean {
|
|
97
|
+
const nodes = unref(params.visibleNodes);
|
|
98
|
+
const current = unref(highlightedIndex);
|
|
99
|
+
|
|
100
|
+
switch (evt.key) {
|
|
101
|
+
case 'ArrowDown':
|
|
102
|
+
evt.preventDefault();
|
|
103
|
+
highlightedIndex.value = current === INITIAL_HIGHLIGHTED_INDEX
|
|
104
|
+
? 0
|
|
105
|
+
: Math.min(nodes.length - 1, current + 1);
|
|
106
|
+
return true;
|
|
107
|
+
|
|
108
|
+
case 'ArrowUp':
|
|
109
|
+
evt.preventDefault();
|
|
110
|
+
highlightedIndex.value = current === INITIAL_HIGHLIGHTED_INDEX
|
|
111
|
+
? nodes.length - 1
|
|
112
|
+
: Math.max(0, current - 1);
|
|
113
|
+
return true;
|
|
114
|
+
|
|
115
|
+
case 'ArrowRight':
|
|
116
|
+
evt.preventDefault();
|
|
117
|
+
if (current >= 0) {
|
|
118
|
+
const node = nodes[current];
|
|
119
|
+
if (node.children?.length) {
|
|
120
|
+
if (!unref(params.expandedIds).has(node.id)) {
|
|
121
|
+
toggleExpand(node.id);
|
|
122
|
+
} else if (current + 1 < nodes.length && nodes[current + 1].depth > node.depth) {
|
|
123
|
+
highlightedIndex.value = current + 1;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return true;
|
|
128
|
+
|
|
129
|
+
case 'ArrowLeft':
|
|
130
|
+
evt.preventDefault();
|
|
131
|
+
if (current >= 0) {
|
|
132
|
+
const node = nodes[current];
|
|
133
|
+
if (node.children?.length && unref(params.expandedIds).has(node.id)) {
|
|
134
|
+
toggleExpand(node.id);
|
|
135
|
+
} else if (node.depth > 0) {
|
|
136
|
+
for (let i = current - 1; i >= 0; i--) {
|
|
137
|
+
if (nodes[i].depth === node.depth - 1) {
|
|
138
|
+
highlightedIndex.value = i;
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return true;
|
|
145
|
+
|
|
146
|
+
case 'Enter':
|
|
147
|
+
case ' ':
|
|
148
|
+
evt.preventDefault();
|
|
149
|
+
if (current >= 0) {
|
|
150
|
+
onActivate(nodes[current]);
|
|
151
|
+
}
|
|
152
|
+
return true;
|
|
153
|
+
|
|
154
|
+
default:
|
|
155
|
+
if (evt.key.length === 1) {
|
|
156
|
+
const lowerKey = evt.key.toLowerCase();
|
|
157
|
+
let matchIndex = nodes.findIndex((n, i) => i > current && n.label.toLowerCase().startsWith(lowerKey));
|
|
158
|
+
if (matchIndex < 0) {
|
|
159
|
+
matchIndex = nodes.findIndex(n => n.label.toLowerCase().startsWith(lowerKey));
|
|
160
|
+
}
|
|
161
|
+
if (matchIndex >= 0) {
|
|
162
|
+
highlightedIndex.value = matchIndex;
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
watch(highlightedIndex, index => {
|
|
171
|
+
if (index < 0) {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
nextTick(() => unref(params.nodeElementRefs)?.[index]?.scrollIntoView({block: 'nearest'}));
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
watch(params.visibleNodes, nodes => {
|
|
179
|
+
const current = unref(highlightedIndex);
|
|
180
|
+
if (current >= nodes.length) {
|
|
181
|
+
highlightedIndex.value = Math.max(INITIAL_HIGHLIGHTED_INDEX, nodes.length - 1);
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
return {highlightedIndex, toggleExpand, onExpandClick, onKeyNavigate};
|
|
186
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
@use '$flux/css/mixin';
|
|
2
|
+
|
|
3
|
+
.treeView {
|
|
4
|
+
display: flex;
|
|
5
|
+
flex-flow: column;
|
|
6
|
+
outline: 0;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.treeNode {
|
|
10
|
+
display: flex;
|
|
11
|
+
width: 100%;
|
|
12
|
+
min-height: 36px;
|
|
13
|
+
padding: 0 8px;
|
|
14
|
+
align-items: center;
|
|
15
|
+
border-radius: var(--radius-half);
|
|
16
|
+
color: var(--foreground);
|
|
17
|
+
cursor: pointer;
|
|
18
|
+
font-size: 14px;
|
|
19
|
+
gap: 8px;
|
|
20
|
+
outline: 0;
|
|
21
|
+
transition: 150ms var(--swift-out);
|
|
22
|
+
transition-property: background, color;
|
|
23
|
+
user-select: none;
|
|
24
|
+
|
|
25
|
+
@include mixin.hover {
|
|
26
|
+
background: var(--surface-hover);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
&.isHighlighted {
|
|
30
|
+
background: var(--surface-hover);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
@include mixin.tree-node-classes();
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
@use '$flux/css/mixin';
|
|
2
|
+
|
|
3
|
+
.treeViewSelectValue {
|
|
4
|
+
margin-left: 6px;
|
|
5
|
+
overflow: hidden;
|
|
6
|
+
text-overflow: ellipsis;
|
|
7
|
+
white-space: nowrap;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.treeViewSelectList {
|
|
11
|
+
display: flex;
|
|
12
|
+
flex-flow: column;
|
|
13
|
+
padding: 9px;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.treeViewSelectEmpty {
|
|
17
|
+
padding: 12px 6px;
|
|
18
|
+
color: var(--foreground-secondary);
|
|
19
|
+
font-size: 14px;
|
|
20
|
+
text-align: center;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.treeNode {
|
|
24
|
+
display: flex;
|
|
25
|
+
width: 100%;
|
|
26
|
+
min-height: 36px;
|
|
27
|
+
padding: 0 8px;
|
|
28
|
+
align-items: center;
|
|
29
|
+
border-radius: var(--radius-half);
|
|
30
|
+
color: var(--foreground);
|
|
31
|
+
font-size: 14px;
|
|
32
|
+
gap: 8px;
|
|
33
|
+
outline: 0;
|
|
34
|
+
transition: 150ms var(--swift-out);
|
|
35
|
+
transition-property: background, color;
|
|
36
|
+
user-select: none;
|
|
37
|
+
|
|
38
|
+
&.isSelectable,
|
|
39
|
+
&.isExpandable {
|
|
40
|
+
cursor: pointer;
|
|
41
|
+
|
|
42
|
+
@include mixin.hover {
|
|
43
|
+
background: var(--surface-hover);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
&.isSelected {
|
|
48
|
+
background: var(--primary-50);
|
|
49
|
+
color: var(--primary-700);
|
|
50
|
+
font-weight: 600;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
&.isSelected.isSelectable {
|
|
54
|
+
@include mixin.hover {
|
|
55
|
+
background: var(--primary-100);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
&.isHighlighted:not(.isSelected) {
|
|
60
|
+
background: var(--surface-hover);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
&.isHighlighted.isSelected {
|
|
64
|
+
background: var(--primary-100);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.treeNodeCheck {
|
|
69
|
+
color: var(--primary-600);
|
|
70
|
+
flex-shrink: 0;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
@include mixin.tree-node-classes();
|
package/src/css/mixin/index.scss
CHANGED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
@mixin tree-node-classes() {
|
|
2
|
+
// Wrapper for guide lines + expand button — no internal gap so lines align perfectly
|
|
3
|
+
.treeNodeLineArea {
|
|
4
|
+
display: flex;
|
|
5
|
+
align-self: stretch;
|
|
6
|
+
flex-shrink: 0;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// Vertical guide line for each ancestor level
|
|
10
|
+
.treeIndent {
|
|
11
|
+
position: relative;
|
|
12
|
+
width: 20px;
|
|
13
|
+
align-self: stretch;
|
|
14
|
+
flex-shrink: 0;
|
|
15
|
+
pointer-events: none;
|
|
16
|
+
|
|
17
|
+
&.hasLine::before {
|
|
18
|
+
content: '';
|
|
19
|
+
position: absolute;
|
|
20
|
+
top: 0;
|
|
21
|
+
bottom: 0;
|
|
22
|
+
left: 9px;
|
|
23
|
+
width: 1px;
|
|
24
|
+
background: var(--surface-stroke);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Connector from parent to current node, with rounded elbow
|
|
29
|
+
.treeConnector {
|
|
30
|
+
position: relative;
|
|
31
|
+
width: 20px;
|
|
32
|
+
align-self: stretch;
|
|
33
|
+
flex-shrink: 0;
|
|
34
|
+
pointer-events: none;
|
|
35
|
+
|
|
36
|
+
// Non-last siblings: full-height vertical continuation line
|
|
37
|
+
&:not(.isLast)::before {
|
|
38
|
+
content: '';
|
|
39
|
+
position: absolute;
|
|
40
|
+
top: 0;
|
|
41
|
+
bottom: 0;
|
|
42
|
+
left: 9px;
|
|
43
|
+
width: 1px;
|
|
44
|
+
background: var(--surface-stroke);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Rounded elbow: vertical segment from top to midpoint, curves right to expand area
|
|
48
|
+
// Overlaps the ::before vertical line for non-last nodes (same position, same color)
|
|
49
|
+
&::after {
|
|
50
|
+
content: '';
|
|
51
|
+
position: absolute;
|
|
52
|
+
top: 0;
|
|
53
|
+
bottom: 50%;
|
|
54
|
+
left: 9px;
|
|
55
|
+
right: 0;
|
|
56
|
+
border-left: 1px solid var(--surface-stroke);
|
|
57
|
+
border-bottom: 1px solid var(--surface-stroke);
|
|
58
|
+
border-bottom-left-radius: 6px;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.treeNodeExpand {
|
|
63
|
+
display: flex;
|
|
64
|
+
width: 16px;
|
|
65
|
+
flex-shrink: 0;
|
|
66
|
+
align-items: center;
|
|
67
|
+
justify-content: center;
|
|
68
|
+
color: var(--foreground-secondary);
|
|
69
|
+
|
|
70
|
+
&:not(:empty) {
|
|
71
|
+
cursor: pointer;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.treeNodeColorDot {
|
|
76
|
+
display: block;
|
|
77
|
+
width: 8px;
|
|
78
|
+
height: 8px;
|
|
79
|
+
border-radius: 50%;
|
|
80
|
+
flex-shrink: 0;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.treeNodeIcon {
|
|
84
|
+
color: var(--foreground-secondary);
|
|
85
|
+
flex-shrink: 0;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.treeNodeLabel {
|
|
89
|
+
flex: 1;
|
|
90
|
+
overflow: hidden;
|
|
91
|
+
text-overflow: ellipsis;
|
|
92
|
+
white-space: nowrap;
|
|
93
|
+
}
|
|
94
|
+
}
|