sample-ui-component-library 0.0.51-dev → 0.0.53-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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sample-ui-component-library",
3
- "version": "0.0.51-dev",
3
+ "version": "0.0.53-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",
@@ -10,7 +10,8 @@ import React, {
10
10
  useImperativeHandle,
11
11
  useMemo,
12
12
  useReducer,
13
- useContext
13
+ useContext,
14
+ useRef,
14
15
  } from "react";
15
16
 
16
17
  import { EditorContext } from "./EditorContext";
@@ -27,14 +28,19 @@ const MODES = {
27
28
  *
28
29
  * @return {JSX}
29
30
  */
30
- export const Editor = forwardRef(({ onSelectAbstraction }, ref) => {
31
+ export const Editor = forwardRef(({ onSelectAbstraction, onSelectTab }, ref) => {
31
32
  const [state, dispatch] = useReducer(editorReducer, initialState);
33
+ const editorRef = useRef();
32
34
 
33
35
  const selectTab = useCallback((id) => {
34
36
  dispatch({ type: "SELECT_TAB", payload: id });
35
- }, []);
37
+ if (onSelectTab) {
38
+ onSelectTab(id);
39
+ }
40
+ }, [onSelectTab]);
36
41
 
37
42
  const closeTab = useCallback((id) => {
43
+ editorRef.current && editorRef.current.clearModel(id);
38
44
  dispatch({ type: "CLOSE_TAB", payload: id });
39
45
  }, []);
40
46
 
@@ -63,19 +69,46 @@ export const Editor = forwardRef(({ onSelectAbstraction }, ref) => {
63
69
  dispatch({ type: "SET_MAPPED_IDS", payload: ids });
64
70
  }, []);
65
71
 
72
+ const getTabs = useCallback(() => {
73
+ return state.tabs;
74
+ }, [state.tabs]);
75
+
76
+ const getTab = useCallback((id) => {
77
+ return state.tabs.find((tab) => tab.uid === id);
78
+ }, [state.tabs]);
79
+
80
+ const getActiveTab = useCallback(() => {
81
+ return state.tabs.find((tab) => tab.id === state.activeTabId);
82
+ }, [state.tabs, state.activeTabId]);
83
+
84
+ const setUpdatedContent = useCallback((tab, content) => {
85
+ dispatch({ type: "SET_UPDATED_CONTENT", payload: {tab, content} });
86
+ }, []);
87
+
88
+ const setContent = useCallback((tab, content) => {
89
+ dispatch({ type: "SET_CONTENT", payload: {tab, content} });
90
+ }, []);
91
+
92
+ const api_entries = {
93
+ state,
94
+ addTab,
95
+ setTabGroupId,
96
+ selectTab,
97
+ closeTab,
98
+ moveTab,
99
+ setMapping,
100
+ setMode,
101
+ setMappedIds,
102
+ getTab,
103
+ getTabs,
104
+ getActiveTab,
105
+ setUpdatedContent,
106
+ setContent
107
+ }
108
+
66
109
  const api = useMemo(() => {
67
- return {
68
- state,
69
- addTab,
70
- setTabGroupId,
71
- selectTab,
72
- closeTab,
73
- moveTab,
74
- setMapping,
75
- setMode,
76
- setMappedIds
77
- };
78
- }, [state, addTab, selectTab, closeTab, moveTab, setTabGroupId, setMapping, setMode, setMappedIds]);
110
+ return api_entries;
111
+ }, Object.values(api_entries));
79
112
 
80
113
  useImperativeHandle(ref, () => api, [api]);
81
114
 
@@ -86,7 +119,9 @@ export const Editor = forwardRef(({ onSelectAbstraction }, ref) => {
86
119
  <Tabs />
87
120
  </div>
88
121
  <div className="monacoContainer">
89
- <MonacoInstance onSelectAbstraction={onSelectAbstraction}/>
122
+ <MonacoInstance
123
+ ref={editorRef}
124
+ onSelectAbstraction={onSelectAbstraction}/>
90
125
  </div>
91
126
  </div>
92
127
  </EditorContext.Provider>
@@ -7,7 +7,8 @@ export const initialState = {
7
7
  mode: EDITOR_MODES.DESIGN,
8
8
  mapping: new Map(),
9
9
  parentTabGroupId: null,
10
- mappedIds: []
10
+ mappedIds: [],
11
+ modifiedIndicator: false,
11
12
  };
12
13
 
13
14
  export const editorReducer = (state, action) => {
@@ -24,6 +25,11 @@ export const editorReducer = (state, action) => {
24
25
  activeTab: tabInfo
25
26
  };
26
27
  }
28
+
29
+ // Initialize updatedContent and isDirty properties for the tab.
30
+ // These are used to track whether the content of the tab has been modified since it was opened.
31
+ tab.updatedContent = tab.content;
32
+ tab.isDirty = false;
27
33
 
28
34
  // Insert tab at specific location if it was provided.
29
35
  let tabs = [...state.tabs];
@@ -133,6 +139,30 @@ export const editorReducer = (state, action) => {
133
139
  return initialState;
134
140
  }
135
141
 
142
+ case "SET_UPDATED_CONTENT": {
143
+ const { tab, content } = action.payload;
144
+ tab.updatedContent = content;
145
+ tab.isDirty = (tab.content !== tab.updatedContent);
146
+
147
+ return {
148
+ ...state,
149
+ modifiedIndicator: !state.modifiedIndicator,
150
+ tabs: state.tabs.map(t => t.uid === tab.uid ? tab : t)
151
+ };
152
+ }
153
+
154
+ case "SET_CONTENT": {
155
+ const { tab, content } = action.payload;
156
+ tab.content = content;
157
+ tab.isDirty = (tab.content !== tab.updatedContent);
158
+
159
+ return {
160
+ ...state,
161
+ modifiedIndicator: !state.modifiedIndicator,
162
+ tabs: state.tabs.map(t => t.uid === tab.uid ? tab : t)
163
+ };
164
+ }
165
+
136
166
  default: {
137
167
  return state;
138
168
  }
@@ -0,0 +1,184 @@
1
+ import React, { useCallback, useLayoutEffect, useEffect, useRef, useState } from 'react';
2
+ import PropTypes from 'prop-types';
3
+
4
+ import EDITOR_MODES from '../EDITOR_MODES';
5
+
6
+ import Editor from '@monaco-editor/react';
7
+
8
+ import "./MonacoInstance.scss"
9
+
10
+ import { useEditor } from "../Editor";
11
+
12
+ export const MonacoInstance = ({onSelectAbstraction, updateContent}) => {
13
+ const { state } = useEditor();
14
+ const [editorContent, setEditorContent] = useState("Loading content...");
15
+ const [showEditor, setShowEditor] = useState(false);
16
+ const activeTabRef = useRef(state.activeTab);
17
+
18
+ const editorRef = useRef(null);
19
+ const content = useRef();
20
+ const containerRef = useRef(null);
21
+ const frameRef = useRef(0);
22
+
23
+ const [overlayDivs, setOverlayDivs] = useState();
24
+
25
+ // When active tab changes, update the editor content and add overlays for the new active tab.
26
+ useLayoutEffect(() => {
27
+ if (state.activeTab) {
28
+ console.log(state.activeTab)
29
+ activeTabRef.current = state.activeTab;
30
+ setEditorContent(state.activeTab.updatedContent);
31
+ setShowEditor(true);
32
+ addOverlays();
33
+ } else {
34
+ setShowEditor(false);
35
+ }
36
+ }, [state.activeTab, editorRef]);
37
+
38
+ // When the editor content changes, update the content in the context for the active tab.
39
+ // Setup content change listener for the editor to update the content in the context
40
+ // and track dirty state of the tab.
41
+ useEffect(() => {
42
+ content.current = editorContent;
43
+ if (editorRef?.current && content.current !== undefined && content.current !== null) {
44
+ editorRef.current.setValue(content.current);
45
+ editorRef.current.getModel().onDidChangeContent((content) => {
46
+ updateContent(activeTabRef.current, editorRef.current.getValue());
47
+ });
48
+ }
49
+ }, [editorContent]);
50
+
51
+ /**
52
+ * Callback for when the Monaco Editor is mounted.
53
+ * @param {Object} editor
54
+ * @param {Object} monaco
55
+ */
56
+ const handleEditorDidMount = useCallback((editor, monaco) => {
57
+ editorRef.current = editor;
58
+ if (content?.current) {
59
+ editorRef.current.setValue(content.current);
60
+ }
61
+ editorRef.current.layout();
62
+ addOverlays();
63
+
64
+ }, []);
65
+
66
+ // Add overlays to editor for the given line ranges.
67
+ const addOverlays = useCallback(() => {
68
+ if (!editorRef.current) return;
69
+
70
+ if (state.mode !== EDITOR_MODES.MAPPING || !state.mapping.get(state.activeTab?.name)) {
71
+ setOverlayDivs([]);
72
+ return;
73
+ }
74
+
75
+ const ranges = state.mapping.get(state.activeTab?.name);
76
+ const lineCount = editorRef.current.getModel().getLineCount();
77
+ const lineHeight = editorRef.current.getOption(monaco.editor.EditorOption.lineHeight);
78
+ const divs = [];
79
+
80
+ ranges.forEach((entry) => {
81
+ const top = editorRef.current.getTopForLineNumber(entry.start_line) - editorRef.current.getScrollTop();
82
+ let bottom = editorRef.current.getTopForLineNumber(entry.end_line + 1) - editorRef.current.getScrollTop();
83
+ if (entry.end_line >= lineCount) {
84
+ bottom = bottom + lineHeight;
85
+ }
86
+
87
+ const style= {
88
+ top: top + "px",
89
+ height: (bottom - top) + "px"
90
+ }
91
+ if (state.mappedIds.includes(entry.uid)) {
92
+ style["backgroundColor"] = "rgba(255, 0, 0, 0.3)";
93
+ }
94
+ const overlayDiv = <div
95
+ className="line-block-overlay"
96
+ onClick={(e) => onSelectAbstraction(entry)}
97
+ style={style}>
98
+ </div>;
99
+ divs.push(overlayDiv);
100
+
101
+ });
102
+ setOverlayDivs(divs);
103
+ }, [editorRef?.current, state, onSelectAbstraction]);
104
+
105
+ // Scroll the editor and update overlays on wheel event.
106
+ const handleWheel = useCallback((e) => {
107
+ if (!editorRef.current) return;
108
+ const deltaY = e.deltaY;
109
+ const currentScrollTop = editorRef.current.getScrollTop();
110
+ editorRef.current.setScrollTop(currentScrollTop + deltaY);
111
+ addOverlays();
112
+ }, [state.activeTab, addOverlays]);
113
+
114
+ // Editor options for Monaco Editor.
115
+ const editorOptions = {
116
+ fontSize: "13px",
117
+ minimap: {
118
+ enabled: false
119
+ },
120
+ padding: {
121
+ top: 10
122
+ },
123
+ renderWhitespace: "none",
124
+ wordWrap: "on",
125
+ scrollBeyondLastLine: false,
126
+ automaticLayout: false
127
+ }
128
+
129
+ // Disable automatic layout and Manually layout the editor to avoid resize observer loops
130
+ useEffect(() => {
131
+ if (!containerRef.current) return;
132
+
133
+ const ro = new ResizeObserver(() => {
134
+ cancelAnimationFrame(frameRef.current);
135
+ frameRef.current = requestAnimationFrame(() => {
136
+ editorRef.current?.layout();
137
+ addOverlays();
138
+ });
139
+ });
140
+
141
+ ro.observe(containerRef.current);
142
+
143
+ return () => {
144
+ cancelAnimationFrame(frameRef.current);
145
+ ro.disconnect();
146
+ };
147
+ }, [state.activeTab, addOverlays]);
148
+
149
+ /**
150
+ * Render the editor if there is an active tab, otherwise render a placeholder message.
151
+ * @returns JSX
152
+ */
153
+ const renderEditor = () => {
154
+ if (showEditor) {
155
+ return (
156
+ <Editor
157
+ defaultLanguage="python"
158
+ defaultValue={editorContent}
159
+ onMount={handleEditorDidMount}
160
+ theme="vs-dark"
161
+ options={editorOptions}
162
+ />
163
+ );
164
+ } else {
165
+ return <div className="no-tab">Select file or drag and drop to view.</div>;
166
+ }
167
+ }
168
+
169
+ return (
170
+ <div className="editor-container" ref={containerRef}>
171
+ {renderEditor()}
172
+ {
173
+ state.mode === EDITOR_MODES.MAPPING &&
174
+ <div className="overlay-layer" onWheel={handleWheel}>
175
+ {overlayDivs}
176
+ </div>
177
+ }
178
+ </div>
179
+ )
180
+ }
181
+
182
+ MonacoInstance.propTypes = {
183
+ editorContent: PropTypes.string,
184
+ }
@@ -1,174 +1,146 @@
1
- import React, { useCallback, useLayoutEffect, useEffect, useRef, useState } from 'react';
2
- import PropTypes from 'prop-types';
1
+ import React, {
2
+ useCallback,
3
+ forwardRef,
4
+ useLayoutEffect,
5
+ useEffect,
6
+ useRef,
7
+ useState,
8
+ useMemo,
9
+ useImperativeHandle
10
+ } from "react";
11
+ import PropTypes from "prop-types";
3
12
 
4
- import EDITOR_MODES from '../EDITOR_MODES';
13
+ import { useEditor } from "../Editor";
5
14
 
6
- import Editor from '@monaco-editor/react';
15
+ import EDITOR_MODES from "../EDITOR_MODES";
7
16
 
8
- import "./MonacoInstance.scss"
17
+ import Editor from "@monaco-editor/react";
9
18
 
10
- import { useEditor } from "../Editor";
19
+ import "./MonacoInstance.scss";
11
20
 
12
- export const MonacoInstance = ({onSelectAbstraction}) => {
13
- const { state } = useEditor();
14
- const [editorContent, setEditorContent] = useState("Loading content...");
15
- const [showEditor, setShowEditor] = useState(false);
16
-
17
- const editorRef = useRef(null);
18
- const content = useRef();
19
- const containerRef = useRef(null);
20
- const frameRef = useRef(0);
21
-
22
- const [overlayDivs, setOverlayDivs] = useState();
23
-
24
- useLayoutEffect(() => {
25
- if (state.activeTab) {
26
- setEditorContent(state.activeTab.content);
27
- setShowEditor(true);
28
- addOverlays();
29
- } else {
30
- setShowEditor(false);
31
- }
32
- }, [state, editorRef]);
33
-
34
- useEffect(() => {
35
- content.current = editorContent;
36
- if (editorRef?.current && content.current !== undefined && content.current !== null) {
37
- editorRef.current.setValue(content.current);
38
- }
39
- }, [editorContent]);
40
-
41
- /**
42
- * Callback for when the Monaco Editor is mounted.
43
- * @param {Object} editor
44
- * @param {Object} monaco
45
- */
46
- const handleEditorDidMount = useCallback((editor, monaco) => {
47
- editorRef.current = editor;
48
- if (content?.current) {
49
- editorRef.current.setValue(content.current);
50
- }
51
- editorRef.current.layout();
52
- addOverlays();
53
- }, [state.activeTab]);
54
-
55
- // Add overlays to editor for the given line ranges.
56
- const addOverlays = useCallback(() => {
57
- if (!editorRef.current) return;
58
-
59
- if (state.mode !== EDITOR_MODES.MAPPING || !state.mapping.get(state.activeTab?.name)) {
60
- setOverlayDivs([]);
61
- return;
62
- }
63
-
64
- const ranges = state.mapping.get(state.activeTab?.name);
65
- const lineCount = editorRef.current.getModel().getLineCount();
66
- const lineHeight = editorRef.current.getOption(monaco.editor.EditorOption.lineHeight);
67
- const divs = [];
68
-
69
- ranges.forEach((entry) => {
70
- const top = editorRef.current.getTopForLineNumber(entry.start_line) - editorRef.current.getScrollTop();
71
- let bottom = editorRef.current.getTopForLineNumber(entry.end_line + 1) - editorRef.current.getScrollTop();
72
- if (entry.end_line >= lineCount) {
73
- bottom = bottom + lineHeight;
74
- }
21
+ export const MonacoInstance = forwardRef(({ onSelectAbstraction }, ref) => {
22
+ const { state, setUpdatedContent } = useEditor();
23
+ const [showEditor, setShowEditor] = useState(false);
24
+ const activeTabRef = useRef(state.activeTab);
25
+
26
+ const editorRef = useRef(null);
27
+ const modelsRef = useRef(new Map());
75
28
 
76
- const style= {
77
- top: top + "px",
78
- height: (bottom - top) + "px"
29
+ // When active tab changes, update the editor content and add overlays for the new active tab.
30
+ useEffect(() => {
31
+ if (state.activeTab) {
32
+ activeTabRef.current = state.activeTab;
33
+ editorRef.current && editorRef.current.setModel(getModel(state.activeTab));
34
+ setShowEditor(true);
35
+ } else {
36
+ activeTabRef.current = null;
37
+ setShowEditor(false);
79
38
  }
80
- if (state.mappedIds.includes(entry.uid)) {
81
- style["backgroundColor"] = "rgba(255, 0, 0, 0.3)";
39
+ }, [state.activeTab, editorRef]);
40
+
41
+ useEffect(() => {
42
+ return () => {
43
+ // Dispose all models on unmount to prevent memory leaks.
44
+ modelsRef.current.forEach((model) => model.dispose());
45
+ modelsRef.current.clear();
46
+ editorRef.current = null;
47
+ }
48
+ }, []);
49
+
50
+ // Get the model given the tab.
51
+ const getModel = useCallback((activeTab) => {
52
+ let model;
53
+ if (modelsRef.current.has(activeTab.uid)) {
54
+ model = modelsRef.current.get(activeTab.uid);
55
+ } else {
56
+ const uri = monaco.Uri.parse(`file:///${activeTab.uid}`);
57
+ model = monaco.editor.createModel(activeTab.content, "python", uri);
58
+ modelsRef.current.set(activeTab.uid, model);
59
+ }
60
+ model.onDidChangeContent((content) => {
61
+ setUpdatedContent(activeTabRef.current, editorRef.current.getValue());
62
+ });
63
+ return model;
64
+ }, [editorRef, modelsRef, setUpdatedContent]);
65
+
66
+ // Setup editor ref, and clear existing models on mount to prevent mem leak from old models.
67
+ const handleEditorDidMount = useCallback((editor, monaco) => {
68
+ editorRef.current = editor;
69
+ editor.layout();
70
+ modelsRef.current = new Map();
71
+ monaco.editor.getModels().forEach((model) => model.dispose());
72
+
73
+ if (activeTabRef.current) {
74
+ editorRef.current.setModel(getModel(activeTabRef.current));
75
+ setShowEditor(true);
82
76
  }
83
- const overlayDiv = <div
84
- className="line-block-overlay"
85
- onClick={(e) => onSelectAbstraction(entry)}
86
- style={style}>
87
- </div>;
88
- divs.push(overlayDiv);
89
-
90
- });
91
- setOverlayDivs(divs);
92
- }, [editorRef?.current, state, onSelectAbstraction]);
93
-
94
- // Scroll the editor and update overlays on wheel event.
95
- const handleWheel = useCallback((e) => {
96
- if (!editorRef.current) return;
97
- const deltaY = e.deltaY;
98
- const currentScrollTop = editorRef.current.getScrollTop();
99
- editorRef.current.setScrollTop(currentScrollTop + deltaY);
100
- addOverlays();
101
- }, [state.activeTab, addOverlays]);
102
-
103
- // Editor options for Monaco Editor.
104
- const editorOptions = {
105
- fontSize: "13px",
106
- minimap: {
107
- enabled: false
108
- },
109
- padding: {
110
- top: 10
111
- },
112
- renderWhitespace: "none",
113
- wordWrap: "on",
114
- scrollBeyondLastLine: false,
115
- readOnly: true,
116
- automaticLayout: false
117
- }
118
-
119
- // Disable automatic layout and Manually layout the editor to avoid resize observer loops
120
- useEffect(() => {
121
- if (!containerRef.current) return;
122
-
123
- const ro = new ResizeObserver(() => {
124
- cancelAnimationFrame(frameRef.current);
125
- frameRef.current = requestAnimationFrame(() => {
126
- editorRef.current?.layout();
127
- addOverlays();
77
+ }, []);
78
+
79
+ // Disable automatic layout and Manually layout the editor to avoid resize observer loops
80
+ const containerRef = useRef(null);
81
+ const frameRef = useRef(0);
82
+ useEffect(() => {
83
+ if (!containerRef.current) return;
84
+ const ro = new ResizeObserver(() => {
85
+ cancelAnimationFrame(frameRef.current);
86
+ frameRef.current = requestAnimationFrame(() => {
87
+ editorRef.current?.layout();
88
+ });
128
89
  });
129
- });
130
-
131
- ro.observe(containerRef.current);
132
-
133
- return () => {
134
- cancelAnimationFrame(frameRef.current);
135
- ro.disconnect();
136
- };
137
- }, [state.activeTab, addOverlays]);
138
-
139
- /**
140
- * Render the editor if there is an active tab, otherwise render a placeholder message.
141
- * @returns JSX
142
- */
143
- const renderEditor = () => {
144
- if (showEditor) {
145
- return (
146
- <Editor
147
- defaultLanguage="python"
148
- defaultValue={editorContent}
149
- onMount={handleEditorDidMount}
150
- theme="vs-dark"
151
- options={editorOptions}
152
- />
153
- );
154
- } else {
155
- return <div className="no-tab">Select file or drag and drop to view.</div>;
156
- }
157
- }
158
-
159
- return (
160
- <div className="editor-container" ref={containerRef}>
161
- {renderEditor()}
162
- {
163
- state.mode === EDITOR_MODES.MAPPING &&
164
- <div className="overlay-layer" onWheel={handleWheel}>
165
- {overlayDivs}
166
- </div>
90
+ ro.observe(containerRef.current);
91
+ return () => {
92
+ cancelAnimationFrame(frameRef.current);
93
+ ro.disconnect();
94
+ };
95
+ }, [state.activeTab]);
96
+
97
+ // Setup imperative handle to clear model when tab is closed to prevent mem leak
98
+ const clearModel = useCallback((id) => {
99
+ if (modelsRef.current.has(id)) {
100
+ const model = modelsRef.current.get(id);
101
+ model.dispose();
102
+ modelsRef.current.delete(id);
167
103
  }
168
- </div>
169
- )
170
- }
104
+ }, []);
105
+
106
+ const api = useMemo(() => {
107
+ return {
108
+ clearModel,
109
+ };
110
+ }, [clearModel]);
111
+
112
+ useImperativeHandle(ref, () => api, [api]);
113
+
114
+ return (
115
+ <div className="editor-container" ref={containerRef}>
116
+ {
117
+ showEditor ?
118
+ <Editor
119
+ defaultLanguage="python"
120
+ defaultValue={"Loading content..."}
121
+ onMount={handleEditorDidMount}
122
+ theme="vs-dark"
123
+ options={{
124
+ minimap: { enabled: false },
125
+ padding: { top: 10 },
126
+ renderWhitespace: "none",
127
+ wordWrap: "on",
128
+ scrollBeyondLastLine: false,
129
+ automaticLayout: false,
130
+ }}
131
+ />:
132
+ <div className="no-tab">Select file or drag and drop to view.</div>
133
+ }
134
+ {state.mode === EDITOR_MODES.MAPPING && (
135
+ <div className="overlay-layer" onWheel={handleWheel}>
136
+ {overlayDivs}
137
+ </div>
138
+ )}
139
+ </div>
140
+ );
141
+ },
142
+ );
171
143
 
172
144
  MonacoInstance.propTypes = {
173
145
  editorContent: PropTypes.string,
174
- }
146
+ };
@@ -11,6 +11,7 @@
11
11
  position:relative;
12
12
  width:100%;
13
13
  height: 100%;
14
+ background: #1e1e1e;
14
15
  }
15
16
 
16
17
  .overlay-layer {
@@ -1,5 +1,5 @@
1
1
  import { useEffect, useState } from "react";
2
- import { FileEarmark, XLg } from "react-bootstrap-icons";
2
+ import { FileEarmark, XLg, CircleFill } from "react-bootstrap-icons";
3
3
  import { useDraggable } from "@dnd-kit/core";
4
4
  import PropTypes from "prop-types";
5
5
 
@@ -19,7 +19,7 @@ const INACTIVE_TAB_FG_COLOR = "#969690";
19
19
  * @param {String} label
20
20
  * @returns
21
21
  */
22
- export const Tab = ({ id, parentId, node }) => {
22
+ export const Tab = ({ id, isDirty, parentId, node }) => {
23
23
  const [tabStyle, setTabStyle] = useState();
24
24
 
25
25
  const { selectTab, closeTab, state } = useEditor();
@@ -70,6 +70,11 @@ export const Tab = ({ id, parentId, node }) => {
70
70
  {...attributes}
71
71
  >
72
72
  <div className="tab-content">
73
+ {isDirty &&
74
+ <div className="icon">
75
+ <CircleFill size={6}/>
76
+ </div>
77
+ }
73
78
  <div className="icon">
74
79
  <FileEarmark size={14} style={{ pointerEvents: "none" }} />
75
80
  </div>