sample-ui-component-library 0.0.7-dev → 0.0.8-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.
Files changed (33) hide show
  1. package/dist/cjs/index.js +6 -6
  2. package/dist/cjs/index.js.map +1 -1
  3. package/dist/esm/index.js +7 -7
  4. package/dist/esm/index.js.map +1 -1
  5. package/package.json +3 -1
  6. package/src/components/Editor/Editor.jsx +77 -42
  7. package/src/components/Editor/Editor.scss +1 -0
  8. package/src/components/Editor/EditorContext.js +2 -0
  9. package/src/components/Editor/EditorReducer.js +88 -0
  10. package/src/components/Editor/MonacoInstance/MonacoInstance.jsx +62 -40
  11. package/src/components/Editor/MonacoInstance/MonacoInstance.scss +8 -0
  12. package/src/components/Editor/Tabs/Gutter/Gutter.jsx +35 -0
  13. package/src/components/Editor/Tabs/Gutter/Gutter.scss +5 -0
  14. package/src/components/Editor/Tabs/Tab/Tab.jsx +89 -0
  15. package/src/components/Editor/Tabs/Tab/Tab.scss +28 -0
  16. package/src/components/Editor/Tabs/Tabs.jsx +47 -0
  17. package/src/components/Editor/Tabs/Tabs.scss +15 -0
  18. package/src/components/FileBrowser/FileBrowser.jsx +56 -119
  19. package/src/components/FileBrowser/FileBrowser.scss +0 -33
  20. package/src/components/FileBrowser/FileBrowserContext.js +2 -0
  21. package/src/components/FileBrowser/FileBrowserReducer.js +47 -0
  22. package/src/components/FileBrowser/Tree/Tree.jsx +55 -0
  23. package/src/components/FileBrowser/Tree/Tree.scss +33 -0
  24. package/src/components/FileBrowser/TreeNode/TreeNode.jsx +101 -0
  25. package/src/components/FileBrowser/TreeNode/TreeNode.scss +33 -0
  26. package/src/components/FileBrowser/helper.js +1 -0
  27. package/src/stories/Editor.stories.js +34 -50
  28. package/src/stories/FileBrowser.stories.js +21 -36
  29. package/src/stories/data/FileBrowser/workspace_sample.json +1 -0
  30. package/src/components/Editor/EditorTabs/EditorTabs.jsx +0 -93
  31. package/src/components/Editor/EditorTabs/EditorTabs.scss +0 -33
  32. package/src/stories/data/FileBrowser/Tree1.json +0 -57
  33. 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.7-dev",
3
+ "version": "0.0.8-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 { EditorTabs } from "./EditorTabs/EditorTabs";
6
- import { useEffect, useState } from "react";
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 = ({systemTree, onFileSelect}) => {
14
-
15
- const [editorContent, setEditorContent] = useState("asdf");
16
-
17
- const [activeTab, setActiveTab] = useState();
18
- const [tabs, setTabs] = useState([
19
- { id: "tab1", label: "Tab 1" },
20
- { id: "tab2", label: "Tab 2" },
21
- { id: "tab3", label: "Tab 3" },
22
- ]);
23
-
24
- useEffect(() => {
25
- if (activeTab) {
26
- setEditorContent(activeTab.label);
27
- }
28
- }, [activeTab]);
29
-
30
-
31
- useEffect(() => {
32
- if (tabs) {
33
- if ((activeTab && activeTab > tabs.length) || activeTab == null) {
34
- setActiveTab(tabs[tabs.length - 1]);
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
- }, [tabs]);
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
- <div className="editorContainer">
46
- <div className="tabContainer">
47
- <EditorTabs activeTab={activeTab} tabs={tabs} selectTab={onTabClick} />
48
- </div>
49
- <div className="monacoContainer">
50
- <MonacoInstance editorContent={editorContent}/>
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
- </div>
82
+ </EditorContext.Provider>
53
83
  );
54
- }
84
+ });
85
+
86
+ Editor.displayName = "Editor";
55
87
 
56
- Editor.propTypes = {
57
- systemTree: PropTypes.object,
58
- onFileSelect: PropTypes.func
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
  }
@@ -5,6 +5,7 @@
5
5
  display:flex;
6
6
  flex-direction:column;
7
7
  overflow:hidden;
8
+ font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
8
9
  }
9
10
 
10
11
  .tabContainer{
@@ -0,0 +1,2 @@
1
+ import { createContext } from "react";
2
+ export const EditorContext = createContext();
@@ -0,0 +1,88 @@
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
+ return {
13
+ ...state,
14
+ tabs: [...state.tabs, action.payload],
15
+ activeTab: action.payload
16
+ };
17
+ }
18
+
19
+ case "SET_PARENT_TAB_GROUP_ID": {
20
+ return {
21
+ ...state,
22
+ parentTabGroupId: action.payload
23
+ };
24
+ }
25
+
26
+ case "SELECT_TAB": {
27
+ const tab = state.tabs.find(obj => obj.uid === action.payload);
28
+ if (!tab) {
29
+ console.error(`Tab with id ${action.payload} not found.`);
30
+ return state;
31
+ }
32
+ return {
33
+ ...state,
34
+ activeTab: tab
35
+ };
36
+ }
37
+
38
+ case "CLOSE_TAB": {
39
+ const ind = state.tabs.findIndex(obj => obj.uid === action.payload);
40
+ if (ind === -1) {
41
+ console.warn(`Tab with id ${action.payload} not found.`);
42
+ return state;
43
+ }
44
+ const newTabs = [...state.tabs];
45
+ newTabs.splice(ind, 1);
46
+
47
+ // If active tab is closed, select the next tab if it exists, otherwise select the previous tab.
48
+ let activeTab = state.activeTab;
49
+ const isActiveTabClosed = state.activeTab && state.activeTab.uid === action.payload;
50
+ if (isActiveTabClosed && ind < newTabs.length) {
51
+ activeTab = newTabs[Math.max(0, ind)];
52
+ } else if (isActiveTabClosed && ind >= newTabs.length) {
53
+ activeTab = newTabs.length > 0 ? newTabs[newTabs.length - 1] : null;
54
+ }
55
+
56
+ return {
57
+ ...state,
58
+ tabs: newTabs,
59
+ activeTab: activeTab
60
+ };
61
+ }
62
+
63
+ case "MOVE_TAB": {
64
+ const prevTabs = [...state.tabs];
65
+ const { tabId, newIndex } = action.payload;
66
+ const oldIndex = prevTabs.findIndex(t => t.uid === tabId);
67
+ if (oldIndex === -1) {
68
+ console.warn(`Tab with id ${tabId} not found.`);
69
+ return state;
70
+ }
71
+ if (newIndex - 1 === oldIndex || newIndex === oldIndex) return state;
72
+
73
+ // Remove the tab from its old position and insert it into the new position.
74
+ const [tabToMove] = prevTabs.splice(oldIndex, 1);
75
+ const adjustedNewIndex = (oldIndex < newIndex) ? newIndex - 1 : newIndex;
76
+ prevTabs.splice(adjustedNewIndex, 0, tabToMove);
77
+
78
+ return {
79
+ ...state,
80
+ tabs: prevTabs
81
+ };
82
+ }
83
+
84
+ default: {
85
+ return state;
86
+ }
87
+ }
88
+ }
@@ -5,60 +5,82 @@ import Editor from '@monaco-editor/react';
5
5
 
6
6
  import "./MonacoInstance.scss"
7
7
 
8
- function Gutter({ id }) {
9
- const { setNodeRef, isOver } = useDroppable({
10
- id,
11
- });
8
+ import { useEditor } from "../Editor";
12
9
 
13
- return (
14
- <div
15
- ref={setNodeRef}
16
- style={{
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
- const handleEditorDidMount = (editor, monaco) => {
34
- editorRef.current = editor;
35
- if(content?.current) {
36
- editorRef.current.setValue(content.current);
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
- <Editor
49
- defaultLanguage="python"
50
- defaultValue=""
51
- onMount={handleEditorDidMount}
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,8 @@
1
+ .no-tab {
2
+ background-color: #1e1e1e;
3
+ color:#6b6b6b;
4
+ height:100%;
5
+ display:flex;
6
+ align-items:center;
7
+ justify-content:center;
8
+ }
@@ -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: "tab-gutter",
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,5 @@
1
+ .gutter {
2
+ min-width: 2px;
3
+ height: 40px;
4
+ width: 2px;
5
+ }
@@ -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: "tab-draggable",
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
+ }