sample-ui-component-library 0.0.7-dev → 0.0.9-dev
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/cjs/index.js +6 -6
- package/dist/cjs/index.js.map +1 -1
- package/dist/esm/index.js +7 -7
- package/dist/esm/index.js.map +1 -1
- package/package.json +3 -1
- package/src/components/Editor/Editor.jsx +77 -42
- package/src/components/Editor/Editor.scss +1 -0
- package/src/components/Editor/EditorContext.js +2 -0
- package/src/components/Editor/EditorReducer.js +97 -0
- package/src/components/Editor/MonacoInstance/MonacoInstance.jsx +62 -40
- package/src/components/Editor/MonacoInstance/MonacoInstance.scss +8 -0
- package/src/components/Editor/Tabs/Gutter/Gutter.jsx +35 -0
- package/src/components/Editor/Tabs/Gutter/Gutter.scss +5 -0
- package/src/components/Editor/Tabs/Tab/Tab.jsx +89 -0
- package/src/components/Editor/Tabs/Tab/Tab.scss +28 -0
- package/src/components/Editor/Tabs/Tabs.jsx +47 -0
- package/src/components/Editor/Tabs/Tabs.scss +15 -0
- package/src/components/FileBrowser/FileBrowser.jsx +56 -119
- package/src/components/FileBrowser/FileBrowser.scss +0 -33
- package/src/components/FileBrowser/FileBrowserContext.js +2 -0
- package/src/components/FileBrowser/FileBrowserReducer.js +47 -0
- package/src/components/FileBrowser/Tree/Tree.jsx +55 -0
- package/src/components/FileBrowser/Tree/Tree.scss +33 -0
- package/src/components/FileBrowser/TreeNode/TreeNode.jsx +102 -0
- package/src/components/FileBrowser/TreeNode/TreeNode.scss +33 -0
- package/src/components/FileBrowser/helper.js +1 -0
- package/src/stories/Editor.stories.js +34 -50
- package/src/stories/FileBrowser.stories.js +21 -36
- package/src/stories/data/FileBrowser/workspace_sample.json +1 -0
- package/src/components/Editor/EditorTabs/EditorTabs.jsx +0 -93
- package/src/components/Editor/EditorTabs/EditorTabs.scss +0 -33
- package/src/stories/data/FileBrowser/Tree1.json +0 -57
- package/src/stories/data/FileBrowser/Tree2.json +0 -60
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sample-ui-component-library",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.9-dev",
|
|
4
4
|
"description": "A library which contains sample UI elements that can be used for populating layouts.",
|
|
5
5
|
"main": "dist/cjs/index.js",
|
|
6
6
|
"module": "dist/esm/index.js",
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
"@babel/preset-env": "^7.26.9",
|
|
28
28
|
"@babel/preset-react": "^7.26.3",
|
|
29
29
|
"@chromatic-com/storybook": "^3.2.6",
|
|
30
|
+
"@dnd-kit/core": "^6.3.1",
|
|
30
31
|
"@rollup/plugin-babel": "^6.0.4",
|
|
31
32
|
"@rollup/plugin-commonjs": "^28.0.3",
|
|
32
33
|
"@rollup/plugin-json": "^6.1.0",
|
|
@@ -47,6 +48,7 @@
|
|
|
47
48
|
"css-loader": "^7.1.2",
|
|
48
49
|
"gh-pages": "^6.3.0",
|
|
49
50
|
"prop-types": "^15.8.1",
|
|
51
|
+
"raw-loader": "^4.0.2",
|
|
50
52
|
"react": "^18.2.0",
|
|
51
53
|
"react-dom": "^18.2.0",
|
|
52
54
|
"rollup": "^4.37.0",
|
|
@@ -2,58 +2,93 @@ import "./Editor.scss";
|
|
|
2
2
|
import PropTypes from 'prop-types';
|
|
3
3
|
|
|
4
4
|
import { MonacoInstance } from "./MonacoInstance/MonacoInstance";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
5
|
+
import { Tabs } from "./Tabs/Tabs";
|
|
6
|
+
import { TabPreview } from "./Tabs/Tab/Tab";
|
|
7
|
+
import React, {
|
|
8
|
+
forwardRef,
|
|
9
|
+
useCallback,
|
|
10
|
+
useImperativeHandle,
|
|
11
|
+
useMemo,
|
|
12
|
+
useReducer,
|
|
13
|
+
useContext
|
|
14
|
+
} from "react";
|
|
15
|
+
|
|
16
|
+
import { EditorContext } from "./EditorContext";
|
|
17
|
+
|
|
18
|
+
import { editorReducer, initialState } from "./EditorReducer";
|
|
7
19
|
|
|
8
20
|
/**
|
|
9
21
|
* Renders the editor component with support for tabs.
|
|
10
22
|
*
|
|
11
23
|
* @return {JSX}
|
|
12
24
|
*/
|
|
13
|
-
export const Editor = ({
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
{
|
|
22
|
-
]);
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
25
|
+
export const Editor = forwardRef(({ }, ref) => {
|
|
26
|
+
const [state, dispatch] = useReducer(editorReducer, initialState);
|
|
27
|
+
|
|
28
|
+
const selectTab = useCallback((id) => {
|
|
29
|
+
dispatch({ type: "SELECT_TAB", payload: id });
|
|
30
|
+
}, []);
|
|
31
|
+
|
|
32
|
+
const closeTab = useCallback((id) => {
|
|
33
|
+
dispatch({ type: "CLOSE_TAB", payload: id });
|
|
34
|
+
}, []);
|
|
35
|
+
|
|
36
|
+
const moveTab = useCallback((tabId, newIndex) => {
|
|
37
|
+
dispatch({ type: "MOVE_TAB", payload: { tabId, newIndex } });
|
|
38
|
+
}, []);
|
|
39
|
+
|
|
40
|
+
const addTab = useCallback((tab) => {
|
|
41
|
+
dispatch({ type: "ADD_TAB", payload: tab });
|
|
42
|
+
}, []);
|
|
43
|
+
|
|
44
|
+
const setTabGroupId = useCallback((id) => {
|
|
45
|
+
dispatch({ type: "SET_PARENT_TAB_GROUP_ID", payload: id });
|
|
46
|
+
}, []);
|
|
47
|
+
|
|
48
|
+
const getPreviewElement = useCallback((tabId) => {
|
|
49
|
+
// Get the preview element for a tab by its id for use in drag-and-drop operations.
|
|
50
|
+
const tab = state.tabs.find(t => t.uid === tabId);
|
|
51
|
+
if (!tab) {
|
|
52
|
+
console.error(`getPreviewElement: tab with id ${tabId} not found.`);
|
|
53
|
+
return null;
|
|
36
54
|
}
|
|
37
|
-
|
|
55
|
+
return <TabPreview info={{ label: tab.name }} />;
|
|
56
|
+
}, [state]);
|
|
57
|
+
|
|
58
|
+
const api = useMemo(() => {
|
|
59
|
+
return {
|
|
60
|
+
state,
|
|
61
|
+
addTab,
|
|
62
|
+
setTabGroupId,
|
|
63
|
+
selectTab,
|
|
64
|
+
closeTab,
|
|
65
|
+
moveTab,
|
|
66
|
+
getPreviewElement
|
|
67
|
+
};
|
|
68
|
+
}, [state, addTab, selectTab, closeTab, moveTab, getPreviewElement, setTabGroupId]);
|
|
69
|
+
|
|
70
|
+
useImperativeHandle(ref, () => api, [api]);
|
|
38
71
|
|
|
39
|
-
const onTabClick = (event) => {
|
|
40
|
-
const tab = tabs.find(obj => obj.id === event.target.id);
|
|
41
|
-
setActiveTab(tab);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
72
|
return (
|
|
45
|
-
<
|
|
46
|
-
<div className="
|
|
47
|
-
<
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
<
|
|
73
|
+
<EditorContext.Provider value={api}>
|
|
74
|
+
<div className="editorContainer">
|
|
75
|
+
<div className="tabContainer">
|
|
76
|
+
<Tabs />
|
|
77
|
+
</div>
|
|
78
|
+
<div className="monacoContainer">
|
|
79
|
+
<MonacoInstance />
|
|
80
|
+
</div>
|
|
51
81
|
</div>
|
|
52
|
-
</
|
|
82
|
+
</EditorContext.Provider>
|
|
53
83
|
);
|
|
54
|
-
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
Editor.displayName = "Editor";
|
|
55
87
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
88
|
+
export function useEditor() {
|
|
89
|
+
const ctx = useContext(EditorContext);
|
|
90
|
+
if (!ctx) {
|
|
91
|
+
throw new Error("useEditor must be used inside <Editor>");
|
|
92
|
+
}
|
|
93
|
+
return ctx;
|
|
59
94
|
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
export const initialState = {
|
|
2
|
+
tabs: [],
|
|
3
|
+
activeTab: null,
|
|
4
|
+
parentTabGroupId: null
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export const editorReducer = (state, action) => {
|
|
8
|
+
switch (action.type) {
|
|
9
|
+
|
|
10
|
+
case "ADD_TAB": {
|
|
11
|
+
// TODO: Add some validation for the payload here.
|
|
12
|
+
const tab = state.tabs.find(obj => obj.uid === action.payload.uid);
|
|
13
|
+
if (tab) {
|
|
14
|
+
console.warn(`Tab with id ${action.payload.uid} already exists`);
|
|
15
|
+
return {
|
|
16
|
+
...state,
|
|
17
|
+
activeTab: tab
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
...state,
|
|
23
|
+
tabs: [...state.tabs, action.payload],
|
|
24
|
+
activeTab: action.payload
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
case "SET_PARENT_TAB_GROUP_ID": {
|
|
29
|
+
return {
|
|
30
|
+
...state,
|
|
31
|
+
parentTabGroupId: action.payload
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
case "SELECT_TAB": {
|
|
36
|
+
const tab = state.tabs.find(obj => obj.uid === action.payload);
|
|
37
|
+
if (!tab) {
|
|
38
|
+
console.error(`Tab with id ${action.payload} not found.`);
|
|
39
|
+
return state;
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
...state,
|
|
43
|
+
activeTab: tab
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
case "CLOSE_TAB": {
|
|
48
|
+
const ind = state.tabs.findIndex(obj => obj.uid === action.payload);
|
|
49
|
+
if (ind === -1) {
|
|
50
|
+
console.warn(`Tab with id ${action.payload} not found.`);
|
|
51
|
+
return state;
|
|
52
|
+
}
|
|
53
|
+
const newTabs = [...state.tabs];
|
|
54
|
+
newTabs.splice(ind, 1);
|
|
55
|
+
|
|
56
|
+
// If active tab is closed, select the next tab if it exists, otherwise select the previous tab.
|
|
57
|
+
let activeTab = state.activeTab;
|
|
58
|
+
const isActiveTabClosed = state.activeTab && state.activeTab.uid === action.payload;
|
|
59
|
+
if (isActiveTabClosed && ind < newTabs.length) {
|
|
60
|
+
activeTab = newTabs[Math.max(0, ind)];
|
|
61
|
+
} else if (isActiveTabClosed && ind >= newTabs.length) {
|
|
62
|
+
activeTab = newTabs.length > 0 ? newTabs[newTabs.length - 1] : null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
...state,
|
|
67
|
+
tabs: newTabs,
|
|
68
|
+
activeTab: activeTab
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
case "MOVE_TAB": {
|
|
73
|
+
const prevTabs = [...state.tabs];
|
|
74
|
+
const { tabId, newIndex } = action.payload;
|
|
75
|
+
const oldIndex = prevTabs.findIndex(t => t.uid === tabId);
|
|
76
|
+
if (oldIndex === -1) {
|
|
77
|
+
console.warn(`Tab with id ${tabId} not found.`);
|
|
78
|
+
return state;
|
|
79
|
+
}
|
|
80
|
+
if (newIndex - 1 === oldIndex || newIndex === oldIndex) return state;
|
|
81
|
+
|
|
82
|
+
// Remove the tab from its old position and insert it into the new position.
|
|
83
|
+
const [tabToMove] = prevTabs.splice(oldIndex, 1);
|
|
84
|
+
const adjustedNewIndex = (oldIndex < newIndex) ? newIndex - 1 : newIndex;
|
|
85
|
+
prevTabs.splice(adjustedNewIndex, 0, tabToMove);
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
...state,
|
|
89
|
+
tabs: prevTabs
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
default: {
|
|
94
|
+
return state;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -5,60 +5,82 @@ import Editor from '@monaco-editor/react';
|
|
|
5
5
|
|
|
6
6
|
import "./MonacoInstance.scss"
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
const { setNodeRef, isOver } = useDroppable({
|
|
10
|
-
id,
|
|
11
|
-
});
|
|
8
|
+
import { useEditor } from "../Editor";
|
|
12
9
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
height: 40,
|
|
18
|
-
width: 1,
|
|
19
|
-
background: isOver ? "white" : "#4da3ff33",
|
|
20
|
-
display: "flex",
|
|
21
|
-
alignItems: "center",
|
|
22
|
-
justifyContent: "center"
|
|
23
|
-
}}
|
|
24
|
-
></div>
|
|
25
|
-
);
|
|
26
|
-
}
|
|
10
|
+
export const MonacoInstance = ({ }) => {
|
|
11
|
+
const { state } = useEditor();
|
|
12
|
+
const [editorContent, setEditorContent] = useState("Loading content...");
|
|
13
|
+
const [showEditor, setShowEditor] = useState(false);
|
|
27
14
|
|
|
28
|
-
export const MonacoInstance = ({editorContent}) => {
|
|
29
15
|
const editorRef = useRef(null);
|
|
30
|
-
|
|
31
16
|
const content = useRef();
|
|
32
17
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
if (state.activeTab) {
|
|
20
|
+
setEditorContent(state.activeTab.content);
|
|
21
|
+
setShowEditor(true);
|
|
22
|
+
} else {
|
|
23
|
+
setShowEditor(false);
|
|
37
24
|
}
|
|
38
|
-
}
|
|
25
|
+
}, [state.activeTab]);
|
|
39
26
|
|
|
40
27
|
useEffect(() => {
|
|
41
28
|
content.current = editorContent;
|
|
42
|
-
if (editorRef?.current && content.current) {
|
|
29
|
+
if (editorRef?.current && content.current !== undefined && content.current !== null) {
|
|
43
30
|
editorRef.current.setValue(content.current);
|
|
44
31
|
}
|
|
45
32
|
}, [editorContent]);
|
|
46
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Callback for when the Monaco Editor is mounted.
|
|
36
|
+
* @param {Object} editor
|
|
37
|
+
* @param {Object} monaco
|
|
38
|
+
*/
|
|
39
|
+
const handleEditorDidMount = (editor, monaco) => {
|
|
40
|
+
editorRef.current = editor;
|
|
41
|
+
if (content?.current) {
|
|
42
|
+
editorRef.current.setValue(content.current);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Editor options for Monaco Editor.
|
|
47
|
+
const editorOptions = {
|
|
48
|
+
scrollBeyondLastLine: false,
|
|
49
|
+
fontSize: "13px",
|
|
50
|
+
minimap: {
|
|
51
|
+
enabled: false
|
|
52
|
+
},
|
|
53
|
+
padding: {
|
|
54
|
+
top: 20,
|
|
55
|
+
bottom: 10
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Render the editor if there is an active tab, otherwise render a placeholder message.
|
|
61
|
+
* @returns JSX
|
|
62
|
+
*/
|
|
63
|
+
const renderEditor = () => {
|
|
64
|
+
if (showEditor) {
|
|
65
|
+
return (
|
|
66
|
+
<Editor
|
|
67
|
+
defaultLanguage="python"
|
|
68
|
+
defaultValue={editorContent}
|
|
69
|
+
onMount={handleEditorDidMount}
|
|
70
|
+
theme="vs-dark"
|
|
71
|
+
options={editorOptions}
|
|
72
|
+
/>
|
|
73
|
+
);
|
|
74
|
+
} else {
|
|
75
|
+
return <div className="no-tab">Select file or drag and drop to view.</div>;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
47
79
|
return (
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
theme="vs-dark"
|
|
53
|
-
options={{
|
|
54
|
-
scrollBeyondLastLine:false,
|
|
55
|
-
fontSize:"12px",
|
|
56
|
-
minimap: {
|
|
57
|
-
enabled: false
|
|
58
|
-
}
|
|
59
|
-
}}
|
|
60
|
-
/>
|
|
61
|
-
);
|
|
80
|
+
<>
|
|
81
|
+
{renderEditor()}
|
|
82
|
+
</>
|
|
83
|
+
)
|
|
62
84
|
}
|
|
63
85
|
|
|
64
86
|
MonacoInstance.propTypes = {
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { useDroppable } from "@dnd-kit/core";
|
|
2
|
+
|
|
3
|
+
import PropTypes from "prop-types";
|
|
4
|
+
|
|
5
|
+
import "./Gutter.scss";
|
|
6
|
+
|
|
7
|
+
const GUTTER_ACTIVE_COLOR = "#FFFFFF";
|
|
8
|
+
const GUTTER_INACTIVE_COLOR = "#222425";
|
|
9
|
+
|
|
10
|
+
export const Gutter = ({ id, index, parentId }) => {
|
|
11
|
+
|
|
12
|
+
// Droppable area used by dnd kit.
|
|
13
|
+
const { setNodeRef, isOver } = useDroppable({
|
|
14
|
+
id,
|
|
15
|
+
data: {
|
|
16
|
+
type: "EditorTabGutter",
|
|
17
|
+
parentId: parentId,
|
|
18
|
+
index: index,
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const style = {
|
|
23
|
+
backgroundColor: isOver ? GUTTER_ACTIVE_COLOR : GUTTER_INACTIVE_COLOR,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<div className="gutter" ref={setNodeRef} style={style}></div>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
Gutter.propTypes = {
|
|
32
|
+
id: PropTypes.string.isRequired,
|
|
33
|
+
parentId: PropTypes.string.isRequired,
|
|
34
|
+
index: PropTypes.number.isRequired,
|
|
35
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import { FileEarmark, XLg } from "react-bootstrap-icons";
|
|
3
|
+
import { useDraggable } from "@dnd-kit/core";
|
|
4
|
+
import PropTypes from "prop-types";
|
|
5
|
+
|
|
6
|
+
import { useEditor } from "../../Editor";
|
|
7
|
+
|
|
8
|
+
import "./Tab.scss";
|
|
9
|
+
|
|
10
|
+
const ACTIVE_TAB_BG_COLOR = "#1e1e1e";
|
|
11
|
+
const INACTIVE_TAB_BG_COLOR = "#2d2d2d";
|
|
12
|
+
const ACTIVE_TAB_FG_COLOR = "#FFFFFF";
|
|
13
|
+
const INACTIVE_TAB_FG_COLOR = "#969690";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Tab Component. Renders a single tab with the provided id and label.
|
|
17
|
+
* Also handles the click events for selecting and closing the tab.
|
|
18
|
+
* @param {String} id
|
|
19
|
+
* @param {String} label
|
|
20
|
+
* @returns
|
|
21
|
+
*/
|
|
22
|
+
export const Tab = ({ id, parentId, node }) => {
|
|
23
|
+
const [tabStyle, setTabStyle] = useState();
|
|
24
|
+
|
|
25
|
+
const { selectTab, closeTab, state } = useEditor();
|
|
26
|
+
|
|
27
|
+
// Saves ID of tab and parent tab group for drag and drop context in dnd kit.
|
|
28
|
+
const { attributes, listeners, setNodeRef, transform } = useDraggable({
|
|
29
|
+
id,
|
|
30
|
+
data: {
|
|
31
|
+
type: "EditorTab",
|
|
32
|
+
parentId: parentId,
|
|
33
|
+
node: node
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
renderTab(state.activeTab && state.activeTab.uid === id);
|
|
39
|
+
}, [state.activeTab]);
|
|
40
|
+
|
|
41
|
+
const renderTab = (isActive) => {
|
|
42
|
+
const bgColor = isActive ? ACTIVE_TAB_BG_COLOR : INACTIVE_TAB_BG_COLOR;
|
|
43
|
+
const fgColor = isActive ? ACTIVE_TAB_FG_COLOR : INACTIVE_TAB_FG_COLOR;
|
|
44
|
+
setTabStyle({ backgroundColor: bgColor, color: fgColor });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const clickTab = (e) => {
|
|
48
|
+
e.stopPropagation();
|
|
49
|
+
selectTab(id);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const clickClose = (e) => {
|
|
53
|
+
e.stopPropagation();
|
|
54
|
+
closeTab(id);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<div
|
|
59
|
+
ref={setNodeRef}
|
|
60
|
+
style={tabStyle}
|
|
61
|
+
id={id}
|
|
62
|
+
onMouseDown={clickTab}
|
|
63
|
+
className="tab"
|
|
64
|
+
{...listeners}
|
|
65
|
+
{...attributes}
|
|
66
|
+
>
|
|
67
|
+
<FileEarmark className="icon" style={{ pointerEvents: "none" }} />
|
|
68
|
+
<span className="tab-name">{node.name}</span>
|
|
69
|
+
<XLg onMouseDown={clickClose} className="close-icon"/>
|
|
70
|
+
</div>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
Tab.propTypes = {
|
|
75
|
+
id: PropTypes.string.isRequired,
|
|
76
|
+
parentId: PropTypes.string.isRequired,
|
|
77
|
+
node: PropTypes.shape({
|
|
78
|
+
name: PropTypes.string.isRequired,
|
|
79
|
+
}).isRequired
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
export const TabPreview = ({info}) => {
|
|
84
|
+
return (
|
|
85
|
+
<div className="tab" style={{ backgroundColor: ACTIVE_TAB_BG_COLOR, color: ACTIVE_TAB_FG_COLOR, opacity:0.5 }}>
|
|
86
|
+
<FileEarmark className="icon" />{info.label}<XLg className="close-icon"/>
|
|
87
|
+
</div>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
.tab {
|
|
2
|
+
padding: 0px 15px;
|
|
3
|
+
cursor: pointer;
|
|
4
|
+
height: 40px;
|
|
5
|
+
width: auto;
|
|
6
|
+
display:flex;
|
|
7
|
+
align-items: center;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.tab > .icon {
|
|
11
|
+
padding-right: 7px;
|
|
12
|
+
display:flex;
|
|
13
|
+
align-self: center;
|
|
14
|
+
width:20px;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.tab > .tab-name {
|
|
18
|
+
width: auto;
|
|
19
|
+
font-size:13px;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.tab > .close-icon {
|
|
23
|
+
margin-left:7px;
|
|
24
|
+
margin-right:7px;
|
|
25
|
+
display:flex;
|
|
26
|
+
align-self: center;
|
|
27
|
+
width:10px;
|
|
28
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
|
|
3
|
+
import { Gutter } from "./Gutter/Gutter";
|
|
4
|
+
import { Tab } from "./Tab/Tab";
|
|
5
|
+
|
|
6
|
+
import { useEditor } from "../Editor";
|
|
7
|
+
|
|
8
|
+
import "./Tabs.scss";
|
|
9
|
+
|
|
10
|
+
const TABS_CONTAINER_BG_COLOR = "#222425";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Tabs component. Renders the tabs based on the tabs info provided in the context.
|
|
14
|
+
* Also renders gutters between the tabs and on the sides for dropping tabs into empty spaces.
|
|
15
|
+
* @returns
|
|
16
|
+
*/
|
|
17
|
+
export const Tabs = () => {
|
|
18
|
+
const { state } = useEditor();
|
|
19
|
+
const [tabsList, setTabsList] = useState();
|
|
20
|
+
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
if (state.tabs?.length >= 0 && state.parentTabGroupId != null) {
|
|
23
|
+
drawTabs(state.tabs, state.parentTabGroupId);
|
|
24
|
+
}
|
|
25
|
+
}, [state.tabs, state.parentTabGroupId]);
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Draw the tabs provided in the tabs info. This includes the gutters
|
|
29
|
+
* between the tabs and on the sides for dropping tabs into empty spaces.
|
|
30
|
+
* @param {Object} tabs
|
|
31
|
+
* @returns
|
|
32
|
+
*/
|
|
33
|
+
const drawTabs = (tabs, tabGroupId) => {
|
|
34
|
+
const list = [];
|
|
35
|
+
tabs.forEach((tab, index) => {
|
|
36
|
+
list.push(<Gutter key={tab.uid + "-gutter"} id={tabGroupId + "-" + index} index={index} parentId={tabGroupId} />);
|
|
37
|
+
list.push(<Tab key={tab.uid} id={tab.uid} parentId={tabGroupId} node={tab} />);
|
|
38
|
+
});
|
|
39
|
+
list.push(<Gutter key="last-gutter" id={tabGroupId + "-" + tabs.length} index={tabs.length} parentId={tabGroupId} />);
|
|
40
|
+
setTabsList(list);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<div className="tabs" style={{background: TABS_CONTAINER_BG_COLOR }}>{tabsList}</div>
|
|
45
|
+
);
|
|
46
|
+
};
|
|
47
|
+
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
.tabs {
|
|
2
|
+
display: flex;
|
|
3
|
+
flex-direction: row;
|
|
4
|
+
height: 40px;
|
|
5
|
+
width: 100%;
|
|
6
|
+
overflow-x: auto;
|
|
7
|
+
overflow-y: hidden;
|
|
8
|
+
flex-wrap: nowrap;
|
|
9
|
+
scrollbar-width: none;
|
|
10
|
+
-ms-overflow-style: none;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.tabs::-webkit-scrollbar {
|
|
14
|
+
display: none;
|
|
15
|
+
}
|