@vuu-ui/grid-layout 4.0.0-alpha.1
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/LICENSE +201 -0
- package/README.md +141 -0
- package/package.json +40 -0
- package/src/GridCommand.js +395 -0
- package/src/GridComponentSettings.js +163 -0
- package/src/GridController.js +359 -0
- package/src/GridDragCoordinator.js +293 -0
- package/src/GridGeometry.js +1298 -0
- package/src/GridLayout.css.js +119 -0
- package/src/GridLayout.js +120 -0
- package/src/GridLayoutContext.js +58 -0
- package/src/GridLayoutDocument.js +307 -0
- package/src/GridLayoutDocumentIds.js +110 -0
- package/src/GridLayoutItem.css.js +46 -0
- package/src/GridLayoutItem.js +147 -0
- package/src/GridLayoutLegacyCompatibility.js +66 -0
- package/src/GridLayoutModel.js +138 -0
- package/src/GridLayoutProvider.js +263 -0
- package/src/GridLayoutStackedItem.css.js +26 -0
- package/src/GridLayoutStackedtem.js +140 -0
- package/src/GridModel.js +1289 -0
- package/src/GridPlaceholder.css.js +9 -0
- package/src/GridPlaceholder.js +24 -0
- package/src/GridSnapshot.js +11 -0
- package/src/GridSplitter.css.js +62 -0
- package/src/GridSplitter.js +26 -0
- package/src/GridStack.js +277 -0
- package/src/Icon.css.js +8 -0
- package/src/Icon.js +26 -0
- package/src/IconButton.css.js +9 -0
- package/src/IconButton.js +28 -0
- package/src/TabDialog.css.js +10 -0
- package/src/TabDialog.js +77 -0
- package/src/TabMenu.css.js +8 -0
- package/src/TabMenu.js +78 -0
- package/src/componentToJson.js +41 -0
- package/src/drag-drop-next/DragContextNext.js +173 -0
- package/src/drag-drop-next/DragDropProviderNext.css.js +72 -0
- package/src/drag-drop-next/DragDropProviderNext.js +91 -0
- package/src/drag-drop-next/SpaceMan.js +256 -0
- package/src/drag-drop-next/TemplateDragSession.js +36 -0
- package/src/drag-drop-next/tabstrip-drag-drop.js +251 -0
- package/src/grid-dom-utils.js +57 -0
- package/src/grid-layout-utils.js +39 -0
- package/src/grid-snapshot-adapters.js +435 -0
- package/src/index.js +24 -0
- package/src/json-value.js +56 -0
- package/src/layoutFromJson.js +15 -0
- package/src/layoutToJson.js +5 -0
- package/src/propUtils.js +15 -0
- package/src/react-element-utils.js +15 -0
- package/src/typeOf.js +15 -0
- package/src/useAsDropTarget.js +229 -0
- package/src/useDraggable.js +46 -0
- package/src/useEditTabName.js +70 -0
- package/src/useGridChildProps.js +47 -0
- package/src/useGridControllerSnapshot.js +21 -0
- package/src/useGridLayout.js +727 -0
- package/src/useGridSplitterResizing.js +262 -0
- package/src/useNotDropTarget.js +2 -0
- package/types/AddTabDialog.d.ts +5 -0
- package/types/GridCommand.d.ts +146 -0
- package/types/GridComponentSettings.d.ts +71 -0
- package/types/GridController.d.ts +50 -0
- package/types/GridDragCoordinator.d.ts +80 -0
- package/types/GridGeometry.d.ts +331 -0
- package/types/GridLayout.d.ts +20 -0
- package/types/GridLayoutContext.d.ts +141 -0
- package/types/GridLayoutDocument.d.ts +61 -0
- package/types/GridLayoutDocumentIds.d.ts +15 -0
- package/types/GridLayoutItem.d.ts +28 -0
- package/types/GridLayoutLegacyCompatibility.d.ts +57 -0
- package/types/GridLayoutModel.d.ts +72 -0
- package/types/GridLayoutProvider.d.ts +59 -0
- package/types/GridLayoutStackedtem.d.ts +10 -0
- package/types/GridModel.d.ts +504 -0
- package/types/GridPlaceholder.d.ts +5 -0
- package/types/GridSnapshot.d.ts +49 -0
- package/types/GridSplitter.d.ts +10 -0
- package/types/GridStack.d.ts +160 -0
- package/types/Icon.d.ts +6 -0
- package/types/IconButton.d.ts +6 -0
- package/types/TabDialog.d.ts +9 -0
- package/types/TabMenu.d.ts +7 -0
- package/types/componentToJson.d.ts +38 -0
- package/types/drag-drop-next/DragContextNext.d.ts +92 -0
- package/types/drag-drop-next/DragDropProviderNext.d.ts +23 -0
- package/types/drag-drop-next/SpaceMan.d.ts +35 -0
- package/types/drag-drop-next/TemplateDragSession.d.ts +21 -0
- package/types/drag-drop-next/tabstrip-drag-drop.d.ts +3 -0
- package/types/grid-dom-utils.d.ts +17 -0
- package/types/grid-layout-utils.d.ts +18 -0
- package/types/grid-matrix.d.ts +5 -0
- package/types/grid-snapshot-adapters.d.ts +22 -0
- package/types/index.d.ts +24 -0
- package/types/json-value.d.ts +23 -0
- package/types/layoutFromJson.d.ts +4 -0
- package/types/layoutToJson.d.ts +3 -0
- package/types/propUtils.d.ts +5 -0
- package/types/react-element-utils.d.ts +3 -0
- package/types/src/AddTabDialog.d.ts +5 -0
- package/types/src/GridLayout.d.ts +14 -0
- package/types/src/GridLayoutContext.d.ts +88 -0
- package/types/src/GridLayoutItem.d.ts +28 -0
- package/types/src/GridLayoutModel.d.ts +59 -0
- package/types/src/GridLayoutProvider.d.ts +37 -0
- package/types/src/GridLayoutStackedtem.d.ts +10 -0
- package/types/src/GridModel.d.ts +350 -0
- package/types/src/GridPlaceholder.d.ts +5 -0
- package/types/src/GridSplitter.d.ts +9 -0
- package/types/src/Icon.d.ts +6 -0
- package/types/src/IconButton.d.ts +6 -0
- package/types/src/TabMenu.d.ts +6 -0
- package/types/src/componentToJson.d.ts +32 -0
- package/types/src/drag-drop-next/DragContextNext.d.ts +73 -0
- package/types/src/drag-drop-next/DragDropProviderNext.d.ts +20 -0
- package/types/src/drag-drop-next/SpaceMan.d.ts +35 -0
- package/types/src/drag-drop-next/tabstrip-drag-drop.d.ts +3 -0
- package/types/src/grid-dom-utils.d.ts +17 -0
- package/types/src/grid-layout-utils.d.ts +80 -0
- package/types/src/grid-matrix.d.ts +5 -0
- package/types/src/index.d.ts +10 -0
- package/types/src/layoutFromJson.d.ts +3 -0
- package/types/src/layoutToJson.d.ts +2 -0
- package/types/src/propUtils.d.ts +5 -0
- package/types/src/react-element-utils.d.ts +3 -0
- package/types/src/typeOf.d.ts +6 -0
- package/types/src/useAsDropTarget.d.ts +7 -0
- package/types/src/useDraggable.d.ts +23 -0
- package/types/src/useGridChildProps.d.ts +13 -0
- package/types/src/useGridLayout.d.ts +43 -0
- package/types/src/useGridSplitterResizing.d.ts +12 -0
- package/types/src/useNotDropTarget.d.ts +1 -0
- package/types/typeOf.d.ts +6 -0
- package/types/useAsDropTarget.d.ts +7 -0
- package/types/useDraggable.d.ts +24 -0
- package/types/useEditTabName.d.ts +11 -0
- package/types/useGridChildProps.d.ts +13 -0
- package/types/useGridControllerSnapshot.d.ts +7 -0
- package/types/useGridLayout.d.ts +53 -0
- package/types/useGridSplitterResizing.d.ts +14 -0
- package/types/useNotDropTarget.d.ts +1 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useComponentCssInjection } from "@salt-ds/styles";
|
|
3
|
+
import { useWindow } from "@salt-ds/window";
|
|
4
|
+
import { useAsDropTarget } from "./useAsDropTarget.js";
|
|
5
|
+
import GridPlaceholder from "./GridPlaceholder.css";
|
|
6
|
+
import { registerComponent } from "@vuu-ui/vuu-utils";
|
|
7
|
+
const classBase = "vuuGridPlaceholder";
|
|
8
|
+
const GridPlaceholder_GridPlaceholder = ({ ...htmlAttributes })=>{
|
|
9
|
+
const targetWindow = useWindow();
|
|
10
|
+
useComponentCssInjection({
|
|
11
|
+
testId: "vuu-grid-layout-placeholder",
|
|
12
|
+
css: GridPlaceholder,
|
|
13
|
+
window: targetWindow
|
|
14
|
+
});
|
|
15
|
+
const dragDropHandlers = useAsDropTarget();
|
|
16
|
+
return /*#__PURE__*/ jsx("div", {
|
|
17
|
+
...htmlAttributes,
|
|
18
|
+
...dragDropHandlers,
|
|
19
|
+
className: `${classBase} vuuGridLayoutItem`,
|
|
20
|
+
"data-drop-target": true
|
|
21
|
+
});
|
|
22
|
+
};
|
|
23
|
+
registerComponent("GridPlaceholder", GridPlaceholder_GridPlaceholder, "component");
|
|
24
|
+
export { GridPlaceholder_GridPlaceholder as GridPlaceholder };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
class GridSnapshotValidationError extends Error {
|
|
2
|
+
issues;
|
|
3
|
+
constructor(issues){
|
|
4
|
+
super(issues.map(({ message, path })=>`${path}: ${message}`).join("; "));
|
|
5
|
+
this.name = "GridSnapshotValidationError";
|
|
6
|
+
this.issues = [
|
|
7
|
+
...issues
|
|
8
|
+
];
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export { GridSnapshotValidationError };
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
const css = `
|
|
2
|
+
.vuuGridSplitter {
|
|
3
|
+
--splitter-background: var(--salt-separable-secondary-borderColor);
|
|
4
|
+
--splitter-borderColor: white;
|
|
5
|
+
--splitter-borderStyle: none;
|
|
6
|
+
--splitter-borderWidth: 3px;
|
|
7
|
+
--splitter-size: 7px;
|
|
8
|
+
background-color: var(--splitter-background);
|
|
9
|
+
border-color: var(--splitter-borderColor);
|
|
10
|
+
border-style: var(--splitter-borderStyle);
|
|
11
|
+
border-width: var(--splitter-borderWidth);
|
|
12
|
+
z-index: 1;
|
|
13
|
+
position: relative;
|
|
14
|
+
|
|
15
|
+
& .vuu-grab-zone {
|
|
16
|
+
cursor: inherit;
|
|
17
|
+
background-color: #ffffff03;
|
|
18
|
+
position: absolute;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.vuuGridSplitter:hover, .vuuGridSplitter-active {
|
|
23
|
+
--splitter-background: var(--salt-separable-primary-borderColor);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.vuuGridSplitter[aria-orientation="vertical"] {
|
|
27
|
+
--splitter-borderStyle: none solid;
|
|
28
|
+
cursor: ew-resize;
|
|
29
|
+
width: var(--splitter-size);
|
|
30
|
+
|
|
31
|
+
& .vuu-grab-zone {
|
|
32
|
+
inset: -5px 0;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.vuuGridSplitter-offset-start[aria-orientation="vertical"] {
|
|
37
|
+
margin-block-start: var(--splitter-size);
|
|
38
|
+
|
|
39
|
+
& .vuu-grab-zone {
|
|
40
|
+
top: 0;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.vuuGridSplitter[aria-orientation="horizontal"] {
|
|
45
|
+
--splitter-borderStyle: solid none;
|
|
46
|
+
cursor: ns-resize;
|
|
47
|
+
height: var(--splitter-size);
|
|
48
|
+
|
|
49
|
+
& .vuu-grab-zone {
|
|
50
|
+
inset: 0 -5px;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.vuuGridSplitter-active[aria-orientation="vertical"] {
|
|
55
|
+
& .vuu-grab-zone {
|
|
56
|
+
inset: 0 -150px;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
`;
|
|
62
|
+
export default css;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useComponentCssInjection } from "@salt-ds/styles";
|
|
3
|
+
import { useWindow } from "@salt-ds/window";
|
|
4
|
+
import GridSplitter from "./GridSplitter.css";
|
|
5
|
+
const GridSplitter_GridSplitter = ({ "aria-controls": ariaControls, ariaOrientation, offsetStart, orientation, ...htmlAttributes })=>{
|
|
6
|
+
const targetWindow = useWindow();
|
|
7
|
+
useComponentCssInjection({
|
|
8
|
+
testId: "vuu-grid-splitter",
|
|
9
|
+
css: GridSplitter,
|
|
10
|
+
window: targetWindow
|
|
11
|
+
});
|
|
12
|
+
const id = `${ariaControls}-splitter-${orientation[0]}`;
|
|
13
|
+
return /*#__PURE__*/ jsx("div", {
|
|
14
|
+
...htmlAttributes,
|
|
15
|
+
"aria-controls": ariaControls,
|
|
16
|
+
"aria-orientation": ariaOrientation,
|
|
17
|
+
className: `vuuGridSplitter${offsetStart ? " vuuGridSplitter-offset-start" : ""}`,
|
|
18
|
+
draggable: true,
|
|
19
|
+
id: id,
|
|
20
|
+
role: "separator",
|
|
21
|
+
children: /*#__PURE__*/ jsx("div", {
|
|
22
|
+
className: "vuu-grab-zone"
|
|
23
|
+
})
|
|
24
|
+
});
|
|
25
|
+
};
|
|
26
|
+
export { GridSplitter_GridSplitter as GridSplitter };
|
package/src/GridStack.js
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import { GridSnapshotValidationError } from "./GridSnapshot.js";
|
|
2
|
+
const MIN_GRID_STACK_MEMBERS = 2;
|
|
3
|
+
const ok = (value)=>({
|
|
4
|
+
ok: true,
|
|
5
|
+
value
|
|
6
|
+
});
|
|
7
|
+
const failure = (code, message)=>({
|
|
8
|
+
error: {
|
|
9
|
+
code,
|
|
10
|
+
message
|
|
11
|
+
},
|
|
12
|
+
ok: false
|
|
13
|
+
});
|
|
14
|
+
const cloneSpan = ({ span, start })=>({
|
|
15
|
+
span,
|
|
16
|
+
start
|
|
17
|
+
});
|
|
18
|
+
const cloneArea = ({ column, row })=>({
|
|
19
|
+
column: cloneSpan(column),
|
|
20
|
+
row: cloneSpan(row)
|
|
21
|
+
});
|
|
22
|
+
const cloneMember = ({ id, label, title })=>({
|
|
23
|
+
id,
|
|
24
|
+
label,
|
|
25
|
+
title
|
|
26
|
+
});
|
|
27
|
+
const cloneMetadata = ({ minHeight, minWidth, resizeable })=>({
|
|
28
|
+
minHeight,
|
|
29
|
+
minWidth,
|
|
30
|
+
resizeable
|
|
31
|
+
});
|
|
32
|
+
const cloneGridStackState = (state)=>({
|
|
33
|
+
area: cloneArea(state.area),
|
|
34
|
+
id: state.id,
|
|
35
|
+
members: state.members.map(cloneMember),
|
|
36
|
+
metadata: cloneMetadata(state.metadata),
|
|
37
|
+
selectedItemId: state.selectedItemId
|
|
38
|
+
});
|
|
39
|
+
const gridStackItemIds = (state)=>state.members.map(({ id })=>id);
|
|
40
|
+
const findGridStackMember = (state, itemId)=>state.members.find(({ id })=>id === itemId);
|
|
41
|
+
const gridStackMemberIndex = (state, itemId)=>state.members.findIndex(({ id })=>id === itemId);
|
|
42
|
+
const gridStackSelectedIndex = (state)=>gridStackMemberIndex(state, state.selectedItemId);
|
|
43
|
+
const toGridStackSnapshot = (state)=>({
|
|
44
|
+
id: state.id,
|
|
45
|
+
itemIds: [
|
|
46
|
+
...gridStackItemIds(state)
|
|
47
|
+
],
|
|
48
|
+
selectedItemId: state.selectedItemId
|
|
49
|
+
});
|
|
50
|
+
const sameMembers = (left, right)=>left.length === right.length && left.every((member, index)=>member.id === right[index].id && member.label === right[index].label && member.title === right[index].title);
|
|
51
|
+
const sameOrder = (left, right)=>left.length === right.length && left.every((member, index)=>member.id === right[index].id);
|
|
52
|
+
const sameSpan = (left, right)=>left.span === right.span && left.start === right.start;
|
|
53
|
+
const sameArea = (left, right)=>sameSpan(left.column, right.column) && sameSpan(left.row, right.row);
|
|
54
|
+
const sameMetadata = (left, right)=>left.minHeight === right.minHeight && left.minWidth === right.minWidth && left.resizeable === right.resizeable;
|
|
55
|
+
const isSameGridStackState = (left, right)=>left.id === right.id && left.selectedItemId === right.selectedItemId && sameArea(left.area, right.area) && sameMetadata(left.metadata, right.metadata) && sameMembers(left.members, right.members);
|
|
56
|
+
const normalizeGridStackState = (state)=>{
|
|
57
|
+
const members = [];
|
|
58
|
+
const seen = new Set();
|
|
59
|
+
for (const member of state.members)if (member.id && !seen.has(member.id)) {
|
|
60
|
+
seen.add(member.id);
|
|
61
|
+
members.push(cloneMember(member));
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
area: cloneArea(state.area),
|
|
65
|
+
id: state.id,
|
|
66
|
+
members,
|
|
67
|
+
metadata: cloneMetadata(state.metadata),
|
|
68
|
+
selectedItemId: seen.has(state.selectedItemId) ? state.selectedItemId : members[0]?.id ?? ""
|
|
69
|
+
};
|
|
70
|
+
};
|
|
71
|
+
const validateSpan = (span, path, issues)=>{
|
|
72
|
+
if (!span || !Number.isInteger(span.start) || !Number.isInteger(span.span) || span.start < 1 || span.span < 1) issues.push({
|
|
73
|
+
code: "INVALID_SPAN",
|
|
74
|
+
message: "stack area must use positive integer grid lines",
|
|
75
|
+
path
|
|
76
|
+
});
|
|
77
|
+
};
|
|
78
|
+
const validateGridStackState = (state, { itemAreas, path = "$" } = {})=>{
|
|
79
|
+
const issues = [];
|
|
80
|
+
if (!state.id) issues.push({
|
|
81
|
+
code: "EMPTY_ID",
|
|
82
|
+
message: "stack id must not be empty",
|
|
83
|
+
path: `${path}.id`
|
|
84
|
+
});
|
|
85
|
+
validateSpan(state.area?.column, `${path}.area.column`, issues);
|
|
86
|
+
validateSpan(state.area?.row, `${path}.area.row`, issues);
|
|
87
|
+
if (state.members.length < MIN_GRID_STACK_MEMBERS) issues.push({
|
|
88
|
+
code: "INVALID_STACK_MEMBERSHIP",
|
|
89
|
+
message: `a stack must contain at least ${MIN_GRID_STACK_MEMBERS} items`,
|
|
90
|
+
path: `${path}.members`
|
|
91
|
+
});
|
|
92
|
+
const seen = new Set();
|
|
93
|
+
state.members.forEach((member, index)=>{
|
|
94
|
+
const memberPath = `${path}.members[${index}]`;
|
|
95
|
+
if (!member.id) return void issues.push({
|
|
96
|
+
code: "EMPTY_ID",
|
|
97
|
+
message: "stack member id must not be empty",
|
|
98
|
+
path: `${memberPath}.id`
|
|
99
|
+
});
|
|
100
|
+
if (seen.has(member.id)) issues.push({
|
|
101
|
+
code: "DUPLICATE_ID",
|
|
102
|
+
message: `item "${member.id}" appears more than once in stack "${state.id}"`,
|
|
103
|
+
path: `${memberPath}.id`
|
|
104
|
+
});
|
|
105
|
+
seen.add(member.id);
|
|
106
|
+
if ("string" != typeof member.label) issues.push({
|
|
107
|
+
code: "INVALID_FIELD",
|
|
108
|
+
message: "stack member label must be a string",
|
|
109
|
+
path: `${memberPath}.label`
|
|
110
|
+
});
|
|
111
|
+
if (itemAreas) {
|
|
112
|
+
const area = itemAreas.get(member.id);
|
|
113
|
+
if (area) {
|
|
114
|
+
if (state.area && !sameArea(area, state.area)) issues.push({
|
|
115
|
+
code: "INVALID_STACK_POSITION",
|
|
116
|
+
message: `item "${member.id}" does not occupy the stack area`,
|
|
117
|
+
path: `${memberPath}.id`
|
|
118
|
+
});
|
|
119
|
+
} else issues.push({
|
|
120
|
+
code: "INVALID_STACK_MEMBERSHIP",
|
|
121
|
+
message: `item "${member.id}" is not a grid item`,
|
|
122
|
+
path: `${memberPath}.id`
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
if (!state.selectedItemId || !seen.has(state.selectedItemId)) issues.push({
|
|
127
|
+
code: "INVALID_STACK_SELECTION",
|
|
128
|
+
message: `selected item "${state.selectedItemId}" is not a stack member`,
|
|
129
|
+
path: `${path}.selectedItemId`
|
|
130
|
+
});
|
|
131
|
+
return issues;
|
|
132
|
+
};
|
|
133
|
+
const assertValidGridStackState = (state, options)=>{
|
|
134
|
+
const issues = validateGridStackState(state, options);
|
|
135
|
+
if (issues.length > 0) throw new GridSnapshotValidationError(issues);
|
|
136
|
+
return state;
|
|
137
|
+
};
|
|
138
|
+
const buildTransition = ({ added = [], dissolved = false, operation, positioned = false, previous, removed = [], state })=>{
|
|
139
|
+
const orderChanged = void 0 === previous || !sameOrder(previous.members, state.members);
|
|
140
|
+
const selectionChanged = void 0 === previous || previous.selectedItemId !== state.selectedItemId;
|
|
141
|
+
return {
|
|
142
|
+
added: added.map(cloneMember),
|
|
143
|
+
changed: void 0 === previous || dissolved || !isSameGridStackState(previous, state),
|
|
144
|
+
dissolved,
|
|
145
|
+
operation,
|
|
146
|
+
orderChanged,
|
|
147
|
+
positioned,
|
|
148
|
+
previous,
|
|
149
|
+
removed: removed.map(cloneMember),
|
|
150
|
+
selectionChanged,
|
|
151
|
+
stackId: state.id,
|
|
152
|
+
state
|
|
153
|
+
};
|
|
154
|
+
};
|
|
155
|
+
const createGridStack = (request)=>{
|
|
156
|
+
if (!request.id) return failure("INVALID_ITEM", "A grid stack requires a stack id");
|
|
157
|
+
const ids = new Set();
|
|
158
|
+
for (const { id } of request.members){
|
|
159
|
+
if (!id) return failure("INVALID_ITEM", "A stack member requires an item id");
|
|
160
|
+
if (ids.has(id)) return failure("DUPLICATE_ITEM_ID", `Grid item #${id} cannot be stacked twice`);
|
|
161
|
+
ids.add(id);
|
|
162
|
+
}
|
|
163
|
+
if (ids.size < MIN_GRID_STACK_MEMBERS) return failure("INVALID_TARGET", `A new stack requires at least ${MIN_GRID_STACK_MEMBERS} items`);
|
|
164
|
+
if (request.selectedItemId && !ids.has(request.selectedItemId)) return failure("TAB_NOT_FOUND", `Grid tab #${request.selectedItemId} is not a member of stack #${request.id}`);
|
|
165
|
+
const state = normalizeGridStackState({
|
|
166
|
+
area: request.area,
|
|
167
|
+
id: request.id,
|
|
168
|
+
members: request.members,
|
|
169
|
+
metadata: request.metadata ?? {},
|
|
170
|
+
selectedItemId: request.selectedItemId ?? request.members[0].id
|
|
171
|
+
});
|
|
172
|
+
return ok(buildTransition({
|
|
173
|
+
added: state.members,
|
|
174
|
+
operation: "create",
|
|
175
|
+
previous: void 0,
|
|
176
|
+
state
|
|
177
|
+
}));
|
|
178
|
+
};
|
|
179
|
+
const addGridStackItem = (state, { member, position, select })=>{
|
|
180
|
+
if (!member.id) return failure("INVALID_ITEM", "A stack member requires an item id");
|
|
181
|
+
if (findGridStackMember(state, member.id)) return failure("DUPLICATE_ITEM_ID", `Grid item #${member.id} is already a member of stack #${state.id}`);
|
|
182
|
+
const members = state.members.map(cloneMember);
|
|
183
|
+
const added = cloneMember(member);
|
|
184
|
+
let index = members.length;
|
|
185
|
+
if (position) {
|
|
186
|
+
const targetIndex = members.findIndex(({ id })=>id === position.targetItemId);
|
|
187
|
+
if (-1 === targetIndex) return failure("TAB_NOT_FOUND", `Grid tab #${position.targetItemId} not found in stack #${state.id}`);
|
|
188
|
+
index = "after" === position.placement ? targetIndex + 1 : targetIndex;
|
|
189
|
+
}
|
|
190
|
+
members.splice(index, 0, added);
|
|
191
|
+
const selectAdded = select ?? void 0 !== position;
|
|
192
|
+
const next = normalizeGridStackState({
|
|
193
|
+
...state,
|
|
194
|
+
members,
|
|
195
|
+
selectedItemId: selectAdded ? added.id : state.selectedItemId
|
|
196
|
+
});
|
|
197
|
+
return ok(buildTransition({
|
|
198
|
+
added: [
|
|
199
|
+
added
|
|
200
|
+
],
|
|
201
|
+
operation: "add",
|
|
202
|
+
positioned: void 0 !== position,
|
|
203
|
+
previous: state,
|
|
204
|
+
state: next
|
|
205
|
+
}));
|
|
206
|
+
};
|
|
207
|
+
const removeGridStackItem = (state, { itemId })=>{
|
|
208
|
+
const index = gridStackMemberIndex(state, itemId);
|
|
209
|
+
if (-1 === index) return failure("TAB_NOT_FOUND", `Grid tab #${itemId} not found in stack #${state.id}`);
|
|
210
|
+
const removed = state.members[index];
|
|
211
|
+
const members = state.members.filter(({ id })=>id !== itemId).map(cloneMember);
|
|
212
|
+
const selectedItemId = state.selectedItemId === itemId ? members[Math.min(index, members.length - 1)]?.id ?? "" : state.selectedItemId;
|
|
213
|
+
const next = normalizeGridStackState({
|
|
214
|
+
...state,
|
|
215
|
+
members,
|
|
216
|
+
selectedItemId
|
|
217
|
+
});
|
|
218
|
+
return ok(buildTransition({
|
|
219
|
+
dissolved: members.length < MIN_GRID_STACK_MEMBERS,
|
|
220
|
+
operation: "remove",
|
|
221
|
+
previous: state,
|
|
222
|
+
removed: [
|
|
223
|
+
removed
|
|
224
|
+
],
|
|
225
|
+
state: next
|
|
226
|
+
}));
|
|
227
|
+
};
|
|
228
|
+
const selectGridStackItem = (state, { itemId })=>{
|
|
229
|
+
if (!findGridStackMember(state, itemId)) return failure("TAB_NOT_FOUND", `Grid tab #${itemId} not found in stack #${state.id}`);
|
|
230
|
+
const next = state.selectedItemId === itemId ? state : cloneGridStackState({
|
|
231
|
+
...state,
|
|
232
|
+
selectedItemId: itemId
|
|
233
|
+
});
|
|
234
|
+
return ok(buildTransition({
|
|
235
|
+
operation: "select",
|
|
236
|
+
previous: state,
|
|
237
|
+
state: next
|
|
238
|
+
}));
|
|
239
|
+
};
|
|
240
|
+
const reorderGridStackItem = (state, { activate = false, itemId, placement, targetItemId })=>{
|
|
241
|
+
const index = gridStackMemberIndex(state, itemId);
|
|
242
|
+
if (-1 === index) return failure("TAB_NOT_FOUND", `Grid tab #${itemId} not found in stack #${state.id}`);
|
|
243
|
+
if (-1 === gridStackMemberIndex(state, targetItemId)) return failure("TAB_NOT_FOUND", `Grid tab #${targetItemId} not found in stack #${state.id}`);
|
|
244
|
+
if (itemId === targetItemId) return failure("INVALID_TARGET", "A tab cannot be reordered relative to itself");
|
|
245
|
+
const members = state.members.map(cloneMember);
|
|
246
|
+
const [moved] = members.splice(index, 1);
|
|
247
|
+
const targetIndex = members.findIndex(({ id })=>id === targetItemId);
|
|
248
|
+
members.splice("after" === placement ? targetIndex + 1 : targetIndex, 0, moved);
|
|
249
|
+
const next = normalizeGridStackState({
|
|
250
|
+
...state,
|
|
251
|
+
members,
|
|
252
|
+
selectedItemId: activate ? itemId : state.selectedItemId
|
|
253
|
+
});
|
|
254
|
+
return ok(buildTransition({
|
|
255
|
+
operation: "reorder",
|
|
256
|
+
previous: state,
|
|
257
|
+
state: next
|
|
258
|
+
}));
|
|
259
|
+
};
|
|
260
|
+
const renameGridStackItem = (state, { itemId, title })=>{
|
|
261
|
+
const member = findGridStackMember(state, itemId);
|
|
262
|
+
if (!member) return failure("TAB_NOT_FOUND", `Grid tab #${itemId} not found in stack #${state.id}`);
|
|
263
|
+
const next = cloneGridStackState({
|
|
264
|
+
...state,
|
|
265
|
+
members: state.members.map((current)=>current.id === itemId ? {
|
|
266
|
+
...current,
|
|
267
|
+
label: title,
|
|
268
|
+
title
|
|
269
|
+
} : current)
|
|
270
|
+
});
|
|
271
|
+
return ok(buildTransition({
|
|
272
|
+
operation: "rename",
|
|
273
|
+
previous: state,
|
|
274
|
+
state: next
|
|
275
|
+
}));
|
|
276
|
+
};
|
|
277
|
+
export { MIN_GRID_STACK_MEMBERS, addGridStackItem, assertValidGridStackState, cloneGridStackState, createGridStack, findGridStackMember, gridStackItemIds, gridStackMemberIndex, gridStackSelectedIndex, isSameGridStackState, normalizeGridStackState, removeGridStackItem, renameGridStackItem, reorderGridStackItem, selectGridStackItem, toGridStackSnapshot, validateGridStackState };
|
package/src/Icon.css.js
ADDED
package/src/Icon.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { jsx } from "react/jsx-runtime";
|
|
2
|
+
import clsx from "clsx";
|
|
3
|
+
import { useComponentCssInjection } from "@salt-ds/styles";
|
|
4
|
+
import { useWindow } from "@salt-ds/window";
|
|
5
|
+
import Icon from "./Icon.css";
|
|
6
|
+
const classBase = "vuuIcon";
|
|
7
|
+
const Icon_Icon = ({ className, name, size, style: styleProp, ...htmlAttributes })=>{
|
|
8
|
+
const targetWindow = useWindow();
|
|
9
|
+
useComponentCssInjection({
|
|
10
|
+
testId: "vuu-icon",
|
|
11
|
+
css: Icon,
|
|
12
|
+
window: targetWindow
|
|
13
|
+
});
|
|
14
|
+
const style = "number" == typeof size ? {
|
|
15
|
+
...styleProp,
|
|
16
|
+
"--vuu-icon-size": `${size}px`
|
|
17
|
+
} : styleProp;
|
|
18
|
+
return /*#__PURE__*/ jsx("span", {
|
|
19
|
+
...htmlAttributes,
|
|
20
|
+
className: clsx(classBase, className),
|
|
21
|
+
"data-icon": name,
|
|
22
|
+
role: "img",
|
|
23
|
+
style: style
|
|
24
|
+
});
|
|
25
|
+
};
|
|
26
|
+
export { Icon_Icon as Icon };
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { jsx } from "react/jsx-runtime";
|
|
2
|
+
import clsx from "clsx";
|
|
3
|
+
import { Button } from "@salt-ds/core";
|
|
4
|
+
import { useComponentCssInjection } from "@salt-ds/styles";
|
|
5
|
+
import { useWindow } from "@salt-ds/window";
|
|
6
|
+
import { Icon } from "./Icon.js";
|
|
7
|
+
import { forwardRef } from "react";
|
|
8
|
+
import IconButton_0 from "./IconButton.css";
|
|
9
|
+
const classBase = "vuuIconButton";
|
|
10
|
+
const IconButton_IconButton = /*#__PURE__*/ forwardRef(function({ "aria-label": ariaLabel, className, icon, size, ...buttonProps }, ref) {
|
|
11
|
+
const targetWindow = useWindow();
|
|
12
|
+
useComponentCssInjection({
|
|
13
|
+
testId: "vuu-icon-button",
|
|
14
|
+
css: IconButton_0,
|
|
15
|
+
window: targetWindow
|
|
16
|
+
});
|
|
17
|
+
return /*#__PURE__*/ jsx(Button, {
|
|
18
|
+
...buttonProps,
|
|
19
|
+
className: clsx(classBase, className),
|
|
20
|
+
ref: ref,
|
|
21
|
+
children: /*#__PURE__*/ jsx(Icon, {
|
|
22
|
+
"aria-label": ariaLabel,
|
|
23
|
+
name: icon,
|
|
24
|
+
size: size
|
|
25
|
+
})
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
export { IconButton_IconButton as IconButton };
|
package/src/TabDialog.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Button, Dialog, DialogActions, DialogContent, DialogHeader, FormField, FormFieldLabel, Input } from "@salt-ds/core";
|
|
3
|
+
import { useComponentCssInjection } from "@salt-ds/styles";
|
|
4
|
+
import { useWindow } from "@salt-ds/window";
|
|
5
|
+
import { useCallback, useState } from "react";
|
|
6
|
+
import TabDialog from "./TabDialog.css";
|
|
7
|
+
const TabDialog_TabDialog = ({ open, onConfirm, onCancel, style, tabLabel = "", title = "Add new tab" })=>{
|
|
8
|
+
const targetWindow = useWindow();
|
|
9
|
+
useComponentCssInjection({
|
|
10
|
+
testId: "vuu-tab-dialog",
|
|
11
|
+
css: TabDialog,
|
|
12
|
+
window: targetWindow
|
|
13
|
+
});
|
|
14
|
+
const [value, setValue] = useState(tabLabel);
|
|
15
|
+
const inputCallbackRef = useCallback((el)=>{
|
|
16
|
+
if (el) el.select();
|
|
17
|
+
}, []);
|
|
18
|
+
const handleKeyDown = useCallback((evt)=>{
|
|
19
|
+
if ("Enter" === evt.key && value !== tabLabel) onConfirm(value);
|
|
20
|
+
}, [
|
|
21
|
+
onConfirm,
|
|
22
|
+
tabLabel,
|
|
23
|
+
value
|
|
24
|
+
]);
|
|
25
|
+
return /*#__PURE__*/ jsxs(Dialog, {
|
|
26
|
+
open: open,
|
|
27
|
+
className: "TabDialog",
|
|
28
|
+
style: style,
|
|
29
|
+
children: [
|
|
30
|
+
/*#__PURE__*/ jsx(DialogHeader, {
|
|
31
|
+
header: title
|
|
32
|
+
}),
|
|
33
|
+
/*#__PURE__*/ jsx(DialogContent, {
|
|
34
|
+
children: /*#__PURE__*/ jsx("div", {
|
|
35
|
+
className: "TabDialog-content",
|
|
36
|
+
children: /*#__PURE__*/ jsxs(FormField, {
|
|
37
|
+
children: [
|
|
38
|
+
/*#__PURE__*/ jsx(FormFieldLabel, {
|
|
39
|
+
children: "New tab name"
|
|
40
|
+
}),
|
|
41
|
+
/*#__PURE__*/ jsx(Input, {
|
|
42
|
+
inputRef: inputCallbackRef,
|
|
43
|
+
value: value,
|
|
44
|
+
onChange: (event)=>{
|
|
45
|
+
setValue(event.target.value);
|
|
46
|
+
},
|
|
47
|
+
inputProps: {
|
|
48
|
+
onKeyDown: handleKeyDown
|
|
49
|
+
}
|
|
50
|
+
})
|
|
51
|
+
]
|
|
52
|
+
})
|
|
53
|
+
})
|
|
54
|
+
}),
|
|
55
|
+
/*#__PURE__*/ jsxs(DialogActions, {
|
|
56
|
+
children: [
|
|
57
|
+
/*#__PURE__*/ jsx(Button, {
|
|
58
|
+
appearance: "solid",
|
|
59
|
+
sentiment: "negative",
|
|
60
|
+
onClick: onCancel,
|
|
61
|
+
children: "Cancel"
|
|
62
|
+
}),
|
|
63
|
+
/*#__PURE__*/ jsx(Button, {
|
|
64
|
+
disabled: "" === value.trim(),
|
|
65
|
+
appearance: "solid",
|
|
66
|
+
sentiment: "accented",
|
|
67
|
+
onClick: ()=>{
|
|
68
|
+
onConfirm(value);
|
|
69
|
+
},
|
|
70
|
+
children: "Confirm"
|
|
71
|
+
})
|
|
72
|
+
]
|
|
73
|
+
})
|
|
74
|
+
]
|
|
75
|
+
});
|
|
76
|
+
};
|
|
77
|
+
export { TabDialog_TabDialog as TabDialog };
|
package/src/TabMenu.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Icon } from "@vuu-ui/vuu-ui-controls";
|
|
3
|
+
import { Button, Menu, MenuItem, MenuPanel, MenuTrigger } from "@salt-ds/core";
|
|
4
|
+
import { useComponentCssInjection } from "@salt-ds/styles";
|
|
5
|
+
import { useWindow } from "@salt-ds/window";
|
|
6
|
+
import TabMenu from "./TabMenu.css";
|
|
7
|
+
import { useGridLayoutDispatch } from "./GridLayoutContext.js";
|
|
8
|
+
import { useCallback, useMemo } from "react";
|
|
9
|
+
import { useEditTabName } from "./useEditTabName.js";
|
|
10
|
+
const TabMenu_TabMenu = ({ allowClose = true, allowRename = true, layoutItemId, tabLabel })=>{
|
|
11
|
+
const targetWindow = useWindow();
|
|
12
|
+
useComponentCssInjection({
|
|
13
|
+
testId: "vuu-tab-menu",
|
|
14
|
+
css: TabMenu,
|
|
15
|
+
window: targetWindow
|
|
16
|
+
});
|
|
17
|
+
const { dialog, showTabEditDialog } = useEditTabName({
|
|
18
|
+
id: layoutItemId,
|
|
19
|
+
mode: "edit",
|
|
20
|
+
tabLabel
|
|
21
|
+
});
|
|
22
|
+
const dispatch = useGridLayoutDispatch();
|
|
23
|
+
const closeTab = useCallback(()=>{
|
|
24
|
+
dispatch({
|
|
25
|
+
type: "close",
|
|
26
|
+
id: layoutItemId
|
|
27
|
+
});
|
|
28
|
+
}, [
|
|
29
|
+
dispatch,
|
|
30
|
+
layoutItemId
|
|
31
|
+
]);
|
|
32
|
+
const renameTab = useCallback(()=>{
|
|
33
|
+
showTabEditDialog();
|
|
34
|
+
}, [
|
|
35
|
+
showTabEditDialog
|
|
36
|
+
]);
|
|
37
|
+
const menuItems = useMemo(()=>{
|
|
38
|
+
const items = [];
|
|
39
|
+
if (allowClose) items.push(/*#__PURE__*/ jsx(MenuItem, {
|
|
40
|
+
onClick: closeTab,
|
|
41
|
+
children: "Close"
|
|
42
|
+
}, "close"));
|
|
43
|
+
if (allowRename) items.push(/*#__PURE__*/ jsx(MenuItem, {
|
|
44
|
+
onClick: renameTab,
|
|
45
|
+
children: "Rename"
|
|
46
|
+
}, "rename"));
|
|
47
|
+
return items;
|
|
48
|
+
}, [
|
|
49
|
+
allowClose,
|
|
50
|
+
allowRename,
|
|
51
|
+
closeTab,
|
|
52
|
+
renameTab
|
|
53
|
+
]);
|
|
54
|
+
return /*#__PURE__*/ jsxs(Fragment, {
|
|
55
|
+
children: [
|
|
56
|
+
/*#__PURE__*/ jsxs(Menu, {
|
|
57
|
+
children: [
|
|
58
|
+
/*#__PURE__*/ jsx(MenuTrigger, {
|
|
59
|
+
children: /*#__PURE__*/ jsx(Button, {
|
|
60
|
+
"aria-label": `${tabLabel} Settings`,
|
|
61
|
+
className: "TabMenuButton",
|
|
62
|
+
"data-embedded": true,
|
|
63
|
+
children: /*#__PURE__*/ jsx(Icon, {
|
|
64
|
+
"aria-hidden": true,
|
|
65
|
+
name: "more-vert"
|
|
66
|
+
})
|
|
67
|
+
})
|
|
68
|
+
}),
|
|
69
|
+
/*#__PURE__*/ jsx(MenuPanel, {
|
|
70
|
+
children: menuItems
|
|
71
|
+
})
|
|
72
|
+
]
|
|
73
|
+
}),
|
|
74
|
+
dialog
|
|
75
|
+
]
|
|
76
|
+
});
|
|
77
|
+
};
|
|
78
|
+
export { TabMenu_TabMenu as TabMenu };
|