diff --git a/src/App.css b/src/App.css
index 445c6b7..2917c18 100644
--- a/src/App.css
+++ b/src/App.css
@@ -1,9 +1,58 @@
+/* CSS Variables for Light Mode (Default) */
+:root {
+ /* Background colors */
+ --bg-primary: rgb(240, 242, 245);
+ --bg-secondary: #fff;
+
+ /* Text colors */
+ --text-primary: #212529;
+ --text-secondary: #fff;
+
+ /* Border colors */
+ --border-primary: #999;
+ --border-transparent: transparent;
+
+ /* Button colors */
+ --btn-primary-bg: #007bff;
+ --btn-primary-border: #007bff;
+ --btn-secondary-bg: #6c757d;
+ --btn-secondary-border: #6c757d;
+}
+
+/* Dark Mode Variables - Productivity Tool Theme */
+[data-theme='dark'] {
+ /* Background colors - Neutral dark grays (not pure black) */
+ --bg-primary: #1E1E1E;
+ /* Main background - matches VS Code */
+ --bg-secondary: #2D2D2D;
+ /* Cards, panels, elevated surfaces */
+
+ /* Text colors - Clear hierarchy, not harsh white */
+ --text-primary: #E4E4E4;
+ /* Primary text - softer than pure white */
+ --text-secondary: #E4E4E4;
+ /* Secondary text on buttons */
+
+ /* Border colors - Subtle separation */
+ --border-primary: #3E3E3E;
+ /* Borders - subtle but visible */
+ --border-transparent: transparent;
+
+ /* Button colors - Subtle blue accent matching brand */
+ --btn-primary-bg: #2B8CF7;
+ /* Accent blue - matching titlebar */
+ --btn-primary-border: #2B8CF7;
+ --btn-secondary-bg: #3E3E3E;
+ /* Neutral secondary actions */
+ --btn-secondary-border: #3E3E3E;
+}
+
* {
- font-family: 'Helvetica', 'Arial', sans-serif;
+ font-family: 'Helvetica', 'Arial', sans-serif
}
input {
- border: 1px solid #999;
+ border: 1px solid var(--border-primary);
}
.container {
@@ -14,20 +63,20 @@ input {
}
.body {
- background-color: rgb(240, 242, 245);
+ background-color: var(--bg-primary);
height: -webkit-fill-available;
flex: 1;
}
.graph {
padding: 50px;
- background-color: rgb(240, 242, 245);
+ background-color: var(--bg-primary);
height: -webkit-fill-available;
flex: 80;
}
.graph-container {
- background-color: #fff;
+ background-color: var(--bg-secondary);
}
.middle {
@@ -39,29 +88,29 @@ input {
.btn {
display: inline-block;
font-weight: 400;
- color: #212529;
+ color: var(--text-primary);
text-align: center;
vertical-align: middle;
-webkit-user-select: none;
-ms-user-select: none;
user-select: none;
background-color: transparent;
- border: 1px solid transparent;
+ border: 1px solid var(--border-transparent);
padding: .375rem .75rem;
line-height: 1.5;
border-radius: .25rem;
}
.btn-primary {
- color: #fff;
- background-color: #007bff;
- border-color: #007bff;
+ color: var(--text-secondary);
+ background-color: var(--btn-primary-bg);
+ border-color: var(--btn-primary-border);
}
.btn-secondary {
- color: #fff;
- background-color: #6c757d;
- border-color: #6c757d;
+ color: var(--text-secondary);
+ background-color: var(--btn-secondary-bg);
+ border-color: var(--btn-secondary-border);
}
.btn:not(:disabled):not(.disabled) {
diff --git a/src/App.jsx b/src/App.jsx
index d702cc0..8af2ccd 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -31,6 +31,16 @@ const app = () => {
window.onbeforeunload = null;
};
}, []);
+
+ // Update document theme attribute when darkMode changes
+ useEffect(() => {
+ if (superState.darkMode) {
+ document.documentElement.setAttribute('data-theme', 'dark');
+ } else {
+ document.documentElement.removeAttribute('data-theme');
+ }
+ }, [superState.darkMode]);
+
return (
diff --git a/src/GraphArea.jsx b/src/GraphArea.jsx
index 373eb82..2396e84 100644
--- a/src/GraphArea.jsx
+++ b/src/GraphArea.jsx
@@ -18,7 +18,15 @@ function Graph({
const initialiseNewGraph = () => {
const myGraph = new MyGraph(
- graphID, ref.current, dispatcher, superState, projectName, nodeValidator, edgeValidator, authorName,
+ graphID,
+ ref.current,
+ dispatcher,
+ superState,
+ projectName,
+ nodeValidator,
+ edgeValidator,
+ authorName,
+ superState.darkMode,
);
if (graphID) myGraph.loadGraphFromLocalStorage();
if (serverID) {
@@ -55,6 +63,13 @@ function Graph({
}
}, [ref]);
+ // Update theme when darkMode changes
+ useEffect(() => {
+ if (instance && instance.updateTheme) {
+ instance.updateTheme(superState.darkMode);
+ }
+ }, [superState.darkMode, instance]);
+
const { id } = el;
return (
diff --git a/src/GraphWorkspace.jsx b/src/GraphWorkspace.jsx
index b092393..3b5a98e 100644
--- a/src/GraphWorkspace.jsx
+++ b/src/GraphWorkspace.jsx
@@ -59,7 +59,7 @@ const GraphComp = (props) => {
}}
>
-
+
{superState.graphs.map((el, i) => (
{
} - concore Editor` : 'untitled'
}
+ dispatcher({ type: T.TOGGLE_DARK_MODE })}
+ style={{
+ cursor: 'pointer',
+ border: '1px solid #ccc',
+ padding: '0 8px',
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ transition: 'opacity 0.2s',
+ backgroundColor: '#eee',
+ }}
+ onMouseEnter={(e) => { e.currentTarget.style.opacity = '0.7'; }}
+ onMouseLeave={(e) => { e.currentTarget.style.opacity = '1'; }}
+ role="button"
+ tabIndex={0}
+ onKeyDown={(e) => e.key === 'Enter' && dispatcher({ type: T.TOGGLE_DARK_MODE })}
+ aria-label="Toggle dark mode"
+ >
+
+
diff --git a/src/component/HeaderComps.jsx b/src/component/HeaderComps.jsx
index ed2c3ac..3062785 100644
--- a/src/component/HeaderComps.jsx
+++ b/src/component/HeaderComps.jsx
@@ -30,7 +30,7 @@ const FileUploader = ({
);
const Switcher = ({
- text, action, active, tabIndex,
+ text, action, active, tabIndex, Icon,
}) => (
ev.key === ' ' && action()}
>
+ {Icon &&
}
-
diff --git a/src/component/fileBrowser.css b/src/component/fileBrowser.css
index fb56556..5cf30c1 100644
--- a/src/component/fileBrowser.css
+++ b/src/component/fileBrowser.css
@@ -198,4 +198,131 @@ div.rendered-file-browser div.files ul {
opacity: 0.8; }
div.rendered-file-browser p.loading, div.rendered-file-browser p.empty {
- margin: 16px 0; }
\ No newline at end of file
+ margin: 16px 0; }
+
+/* DARK MODE - LEFT SIDEBAR / FILE BROWSER*/
+
+[data-theme='dark'] {
+ /* Main sidebar background */
+ background: #181818;
+}
+
+/* Upload Directory Button */
+[data-theme='dark'] .inputButton {
+ background-color: #2B8CF7; /* Accent Blue */
+ color: #FFFFFF;
+ border: 1px solid #2B8CF7;
+}
+
+[data-theme='dark'] .inputButton:hover {
+ background-color: #1e88e5;
+}
+
+/* Section Header (h4 - Folder Name) */
+[data-theme='dark'] h4 {
+ background-color: #202020;
+ color: #EAEAEA;
+ padding: 10px;
+ margin: 0;
+ border-bottom: 1px solid #2C2C2C;
+}
+
+/* Filter Input Field */
+[data-theme='dark'] div.rendered-react-keyed-file-browser div.action-bar input[type="search"] {
+ background: #242424;
+ border: 1px solid #333333;
+ color: #FFFFFF;
+}
+
+[data-theme='dark'] div.rendered-react-keyed-file-browser div.action-bar input[type="search"]::placeholder {
+ color: #888888;
+}
+
+/* File Browser Container */
+[data-theme='dark'] div.rendered-react-keyed-file-browser div.files {
+ background: #181818;
+ color: #EAEAEA;
+}
+
+/* Table Column Headers */
+[data-theme='dark'] div.rendered-react-keyed-file-browser div.files table thead th {
+ color: #9AA0A6;
+ border-bottom: 1px solid #2C2C2C;
+ background: #181818;
+}
+
+/* Table Rows */
+[data-theme='dark'] div.rendered-react-keyed-file-browser div.files table tr td {
+ color: #EAEAEA;
+ border-bottom: 1px solid #202020;
+}
+
+/* Table Row Hover */
+[data-theme='dark'] div.rendered-react-keyed-file-browser div.files table tr:hover td {
+ background: #252525;
+}
+
+/* Selected Row */
+[data-theme='dark'] div.rendered-react-keyed-file-browser div.files table tr.selected td {
+ background: #2A2A2A;
+ color: #FFFFFF;
+}
+
+[data-theme='dark'] div.rendered-react-keyed-file-browser div.files table tr.selected td.name:after {
+ background: #2B8CF7; /* Accent blue for selection indicator */
+}
+
+/* Dragover State */
+[data-theme='dark'] div.rendered-react-keyed-file-browser div.files table tr.dragover td,
+[data-theme='dark'] div.rendered-react-keyed-file-browser div.files table tr.dragover th {
+ background: #2A2A2A;
+}
+
+/* Empty State ("No files.") */
+[data-theme='dark'] div.rendered-file-browser p.empty {
+ color: #6F6F6F;
+}
+
+[data-theme='dark'] div.rendered-file-browser p.loading {
+ color: #9AA0A6;
+}
+
+/* Folder/File Items in List View */
+[data-theme='dark'] div.rendered-file-browser div.files ul li.folder > div.item {
+ border: 1px solid #2C2C2C;
+ background: #181818;
+}
+
+[data-theme='dark'] div.rendered-file-browser div.files ul li.folder > div.item:hover {
+ background: #252525;
+}
+
+[data-theme='dark'] div.rendered-file-browser div.files li.file.selected > div.item,
+[data-theme='dark'] div.rendered-file-browser div.files li.folder.selected > div.item {
+ background: #2A2A2A;
+ color: #FFFFFF;
+ border: 1px solid #2B8CF7;
+}
+
+[data-theme='dark'] div.rendered-file-browser div.files ul li.file > div.item span.thumb {
+ border: 1px solid #2C2C2C;
+ background: #202020;
+}
+
+[data-theme='dark'] div.rendered-file-browser div.files li.file.dragover,
+[data-theme='dark'] div.rendered-file-browser div.files li.folder.dragover {
+ background: #2A2A2A;
+}
+
+/* Expanded Folder Borders */
+[data-theme='dark'] div.rendered-file-browser div.files ul li.folder.expanded {
+ border-bottom: 1px solid #2C2C2C;
+ border-left: 4px solid #2C2C2C;
+ border-right: 1px solid #2C2C2C;
+}
+
+[data-theme='dark'] div.rendered-file-browser div.files ul li.folder.expanded.selected {
+ border-bottom: 1px solid #2B8CF7;
+ border-left: 4px solid #2B8CF7;
+ border-right: 1px solid #2B8CF7;
+}
\ No newline at end of file
diff --git a/src/component/header.css b/src/component/header.css
index feea4ad..947c48e 100644
--- a/src/component/header.css
+++ b/src/component/header.css
@@ -97,4 +97,56 @@
background: transparent;
margin: 0;
padding: 0;
+}
+
+/* ============================================
+ DARK MODE STYLES
+ ============================================ */
+
+[data-theme='dark'] .header {
+ background: #262626;
+}
+
+[data-theme='dark'] .toolbar {
+ background: #262626;
+}
+
+[data-theme='dark'] .toolbar .tool {
+ color: #6A6A6A;
+ /* Disabled/inactive icon color */
+}
+
+[data-theme='dark'] .toolbar .tool.active {
+ color: #E6E6E6;
+ /* Primary icons/text */
+}
+
+[data-theme='dark'] .toolbar .tool-text-only {
+ color: #B0B0B0;
+ /* Secondary text */
+}
+
+[data-theme='dark'] .toolbar .tool.active:hover {
+ background: #2A2A2A;
+ /* Hover effect */
+}
+
+[data-theme='dark'] .titlebar {
+ background-color: #2B8CF7;
+ /* Accent Blue for app title */
+ color: #fff;
+}
+
+[data-theme='dark'] .sep {
+ background: rgba(255, 255, 255, 0.1);
+}
+
+[data-theme='dark'] .rc-switch-checked {
+ border: 1px solid #2B8CF7;
+ background-color: #2B8CF7;
+}
+
+/* Theme Icon Color */
+.theme-icon {
+ color: black;
}
\ No newline at end of file
diff --git a/src/component/modals/FileEdit.jsx b/src/component/modals/FileEdit.jsx
index c93859f..4789297 100644
--- a/src/component/modals/FileEdit.jsx
+++ b/src/component/modals/FileEdit.jsx
@@ -116,7 +116,7 @@ const FileEditModal = ({ superState, dispatcher }) => {
setCodeStuff(value)}
options={{
diff --git a/src/component/modals/contributeDetails.css b/src/component/modals/contributeDetails.css
index 42b627a..ddca03a 100644
--- a/src/component/modals/contributeDetails.css
+++ b/src/component/modals/contributeDetails.css
@@ -18,6 +18,7 @@
.contribute-details .btn{
width: 100%;
}
+
.contribute-details .btn-secondary{
align-self: flex-end;
}
@@ -33,11 +34,23 @@
/* height: 80px; */
padding: 3px 15px;
display: inline-block;
- }
-
- .Toastify__toast {
+}
+
+.Toastify__toast {
/* width: 350px; */
/* height: 80px; */
width: fit-content;
font-size: 16px;
- }
\ No newline at end of file
+}
+
+/* DARK MODE STYLES */
+[data-theme='dark'] .contribute-details input,
+[data-theme='dark'] .contribute-details textarea {
+ background-color: #1a1a1a;
+ border: 1px solid #444;
+ color: #e5e7eb;
+}
+
+[data-theme='dark'] .contribute-details span {
+ color: #e5e7eb;
+}
\ No newline at end of file
diff --git a/src/component/modals/edgeDetails.css b/src/component/modals/edgeDetails.css
index b46b7b3..6728f57 100644
--- a/src/component/modals/edgeDetails.css
+++ b/src/component/modals/edgeDetails.css
@@ -64,13 +64,15 @@
width: fit-content;
border: 1px solid gray
}
+
.color-picker{
display: inline-block;
margin: 13px 0px;
position: absolute;
background: transparent;
}
-.color-picker .overlay{
+
+.color-picker .overlay {
top: 0;
bottom: 0;
left: 0;
@@ -100,4 +102,66 @@ justify-content: space-between;
.edgeform .span-rest {
grid-column: 2;
}
+}
+
+/* DARK MODE - EDGE DETAILS MODAL */
+
+[data-theme='dark'] .edgeform {
+ color: #EAEAEA;
+}
+
+/* Edge preview container */
+[data-theme='dark'] .par-div {
+ background: #202020;
+ border: 1px solid #2C2C2C;
+}
+
+/* Edge label text */
+[data-theme='dark'] .edgeform .label {
+ color: #FFFFFF;
+}
+
+/* Input field */
+[data-theme='dark'] .edgeform input[type="text"] {
+ background: #242424;
+ border: 1px solid #333333;
+ color: #FFFFFF;
+}
+
+[data-theme='dark'] .edgeform input[type="text"]::placeholder {
+ color: #888888;
+}
+
+[data-theme='dark'] .edgeform input[type="text"]:focus {
+ border-color: #2B8CF7;
+ outline: none;
+}
+
+/* Number inputs */
+[data-theme='dark'] .edgeform input[type="number"] {
+ background: #242424;
+ border: 1px solid #333333;
+ color: #FFFFFF;
+}
+
+[data-theme='dark'] .edgeform input[type="number"]:focus {
+ border-color: #2B8CF7;
+ outline: none;
+}
+
+/* Form container */
+[data-theme='dark'] .edgeform .form {
+ background: transparent;
+ color: #EAEAEA;
+}
+
+/* Color box container */
+[data-theme='dark'] .color-box-par {
+ background: #2A2A2A;
+ border: 1px solid #333333;
+ box-shadow: rgb(255 255 255 / 5%) 0px 0px 0px 1px;
+}
+
+[data-theme='dark'] .edgeform .color-box {
+ border: 1px solid #333333;
}
\ No newline at end of file
diff --git a/src/component/modals/file-edit.css b/src/component/modals/file-edit.css
index 0a79641..08b3ca3 100644
--- a/src/component/modals/file-edit.css
+++ b/src/component/modals/file-edit.css
@@ -24,3 +24,25 @@
grid-template-columns: auto;
}
}
+
+/* DARK MODE - FILE EDIT MODAL */
+
+[data-theme='dark'] .save-bar .btn {
+ background: #2A2A2A;
+ border: 1px solid #333333;
+ color: #FFFFFF;
+}
+
+[data-theme='dark'] .save-bar .btn:hover {
+ background: #333333;
+ border-color: #2B8CF7;
+}
+
+[data-theme='dark'] .save-bar .btn-primary {
+ background: #2B8CF7;
+ border-color: #2B8CF7;
+}
+
+[data-theme='dark'] .save-bar .btn-primary:hover {
+ background: #1e88e5;
+}
\ No newline at end of file
diff --git a/src/component/modals/graph-comp-details.css b/src/component/modals/graph-comp-details.css
index 240fdc7..4d37983 100644
--- a/src/component/modals/graph-comp-details.css
+++ b/src/component/modals/graph-comp-details.css
@@ -18,4 +18,8 @@
color: #f00;
font-family: monospaceoo;
padding-bottom: 20px;
+}
+
+[data-theme='dark'] .ReactModalPortal .modal-footer {
+ border-top: 1px solid #333;
}
\ No newline at end of file
diff --git a/src/component/modals/nodeDetails.css b/src/component/modals/nodeDetails.css
index e61517f..1377bf5 100644
--- a/src/component/modals/nodeDetails.css
+++ b/src/component/modals/nodeDetails.css
@@ -112,4 +112,19 @@
.nodeform .nodeLabelFile {
grid-column: 2 / span 1;
}
+}
+
+/* DARK MODE STYLES */
+[data-theme='dark'] .parent-div {
+ background: #1a1a1a;
+}
+
+[data-theme='dark'] .nodeform input {
+ background-color: #1a1a1a;
+ border: 1px solid #444;
+ color: #e5e7eb;
+}
+
+[data-theme='dark'] .nodeform div {
+ color: #e5e7eb;
}
\ No newline at end of file
diff --git a/src/component/modals/optionsModal.css b/src/component/modals/optionsModal.css
index 9f1d56b..5747e09 100644
--- a/src/component/modals/optionsModal.css
+++ b/src/component/modals/optionsModal.css
@@ -3,4 +3,22 @@
}
.main-div-comp {
margin: 20px;
+}
+
+/* DARK MODE STYLES */
+[data-theme='dark'] .main-div input[type="text"],
+[data-theme='dark'] .main-div textarea {
+ background-color: #1a1a1a;
+ border: 1px solid #444;
+ color: #e5e7eb;
+ padding: 5px;
+}
+
+[data-theme='dark'] .main-div input[type="checkbox"] {
+ accent-color: #3b82f6;
+}
+
+[data-theme='dark'] .main-div label,
+[data-theme='dark'] .main-div span {
+ color: #e5e7eb;
}
\ No newline at end of file
diff --git a/src/component/modals/parent-modal.css b/src/component/modals/parent-modal.css
index 39d876f..220f3ae 100644
--- a/src/component/modals/parent-modal.css
+++ b/src/component/modals/parent-modal.css
@@ -44,7 +44,7 @@
}
.modal-body{
- max-height: calc( 80vh - 70px);
+ max-height: calc(80vh - 70px);
overflow: auto;
}
@@ -128,4 +128,23 @@
.ReactModalPortal .Modal {
min-width: 90%;
}
+}
+
+/* DARK MODE STYLES */
+[data-theme='dark'] .ReactModalPortal .modal-content {
+ background-color: #262626;
+ border: 1px solid #444;
+ color: #e5e7eb;
+}
+
+[data-theme='dark'] .ReactModalPortal .modal-header {
+ border-bottom: 1px solid #333;
+}
+
+[data-theme='dark'] .ReactModalPortal .modal-header .close {
+ color: #e5e7eb;
+}
+
+[data-theme='dark'] .ReactModalPortal .modal-title {
+ color: #e5e7eb;
}
\ No newline at end of file
diff --git a/src/component/modals/project-details.css b/src/component/modals/project-details.css
index 6c2996c..c201e8f 100644
--- a/src/component/modals/project-details.css
+++ b/src/component/modals/project-details.css
@@ -28,4 +28,58 @@
.proj-details .serverIDText{
width: calc( 100% - 20px);
margin-bottom: 20px;
+}
+
+/* DARK MODE */
+
+[data-theme='dark'] .proj-details {
+ color: #EAEAEA;
+}
+
+[data-theme='dark'] .proj-details input {
+ background: #242424;
+ border: 1px solid #333333;
+ color: #FFFFFF;
+}
+
+[data-theme='dark'] .proj-details input::placeholder {
+ color: #888888;
+}
+
+[data-theme='dark'] .proj-details input:focus {
+ border-color: #FFFFFF;
+ outline: none;
+}
+
+[data-theme='dark'] .proj-details .btn {
+ background: #2A2A2A;
+ border: 1px solid #333333;
+ color: #FFFFFF;
+}
+
+[data-theme='dark'] .proj-details .btn:hover {
+ background: #333333;
+ border-color: #2B8CF7;
+}
+
+[data-theme='dark'] .proj-details .btn-primary {
+ background: #2B8CF7;
+ border-color: #2B8CF7;
+}
+
+[data-theme='dark'] .proj-details .btn-primary:hover {
+ background: #1e88e5;
+}
+
+[data-theme='dark'] .proj-details .btn-secondary {
+ background: #2A2A2A;
+ border: 1px solid #333333;
+}
+
+[data-theme='dark'] .proj-details .btn-secondary:hover {
+ background: #333333;
+}
+
+[data-theme='dark'] .proj-details .divider {
+ border-bottom: 1px solid #333333;
}
\ No newline at end of file
diff --git a/src/component/tabBar.css b/src/component/tabBar.css
index 921c805..33e1187 100644
--- a/src/component/tabBar.css
+++ b/src/component/tabBar.css
@@ -57,4 +57,53 @@
.tab-act:hover {
text-shadow: 0px 0px 10px #000;
+}
+
+/* DARK MODE - NODE / FLOW TAB BAR */
+
+/* Tab Bar Container */
+[data-theme='dark'] .tab-par {
+ background: #1B1B1B;
+ border-bottom: 1px solid #2A2A2A;
+}
+
+/* Inactive Tabs */
+[data-theme='dark'] .tab {
+ background-color: #1B1B1B;
+ border: 1px solid #2A2A2A;
+ border-bottom: 1px solid #2A2A2A;
+ color: #A0A0A0;
+ /* Inactive tab text */
+}
+
+[data-theme='dark'] .tab:hover {
+ background-color: #202020;
+}
+
+/* Active/Selected Tab */
+[data-theme='dark'] .tab.selected {
+ background-color: #232323;
+ color: #FFFFFF;
+ /* Active tab text */
+ border: 1px solid #2A2A2A;
+ border-bottom: 0;
+}
+
+/* Tab Action Icons (➕ ✏ ❌) */
+[data-theme='dark'] .tab-act {
+ color: #B5B5B5;
+ /* Default icon color */
+}
+
+[data-theme='dark'] .tab-act:hover {
+ color: #FFFFFF;
+ /* Hover icon color */
+ text-shadow: 0px 0px 5px rgba(255, 255, 255, 0.3);
+}
+
+/* Delete Icon Hover (specific red color) */
+[data-theme='dark'] .tab-act.delete-icon:hover {
+ color: #E5533D;
+ /* Delete hover - red */
+ text-shadow: 0px 0px 5px rgba(229, 83, 61, 0.5);
}
\ No newline at end of file
diff --git a/src/component/zoomSetter.css b/src/component/zoomSetter.css
index e1b8a45..ace4adb 100644
--- a/src/component/zoomSetter.css
+++ b/src/component/zoomSetter.css
@@ -40,4 +40,46 @@
.zoom-comp .zoom-value {
width: 40px;
+}
+
+/* DARK MODE - ZOOM CONTROLS*/
+
+/* Container */
+[data-theme='dark'] .zoom-comp {
+ background: #1E1E1E;
+ border: 1px solid #2A2A2A;
+ border-right: none;
+ border-bottom: none;
+}
+
+/* Text & Icons */
+[data-theme='dark'] .zoom-comp .zoom-box {
+ color: #E0E0E0;
+ /* Text (100%) */
+ border-right: 1px solid #2A2A2A;
+}
+
+/* Button Hover State */
+[data-theme='dark'] .zoom-comp .zoom-btn:hover {
+ background: #2A2A2A;
+}
+
+/* Slider Track (Rail) */
+[data-theme='dark'] .zoom-comp .rc-slider-rail {
+ background-color: #2A2A2A;
+}
+
+/* Slider Thumb (Handle) */
+[data-theme='dark'] .zoom-comp .rc-slider-handle {
+ border-color: #2B8CF7;
+ background-color: #2B8CF7;
+ box-shadow: none;
+ /* Clean look */
+}
+
+/* Handle active/focus state for slider */
+[data-theme='dark'] .zoom-comp .rc-slider-handle:active,
+[data-theme='dark'] .zoom-comp .rc-slider-handle:focus,
+[data-theme='dark'] .zoom-comp .rc-slider-handle-click-focused {
+ box-shadow: 0 0 0 5px rgba(43, 140, 247, 0.2);
}
\ No newline at end of file
diff --git a/src/config/cytoscape-options.js b/src/config/cytoscape-options.js
index d0c7b60..870f68d 100644
--- a/src/config/cytoscape-options.js
+++ b/src/config/cytoscape-options.js
@@ -1,11 +1,11 @@
-import style from './cytoscape-style';
+import getCytoscapeStyle from './cytoscape-style';
-const options = {
- style: [...style],
+const getCytoscapeOptions = (darkMode = false) => ({
+ style: [...getCytoscapeStyle(darkMode)],
zoomingEnabled: true,
userZoomingEnabled: true,
minZoom: 0.25,
maxZoom: 5,
-};
+});
-export default options;
+export default getCytoscapeOptions;
diff --git a/src/config/cytoscape-style.js b/src/config/cytoscape-style.js
index 4bbba03..6e05ca2 100644
--- a/src/config/cytoscape-style.js
+++ b/src/config/cytoscape-style.js
@@ -1,206 +1,225 @@
-const style = [
- {
- selector: '*',
- style: {
- overlayOpacity: '0',
- },
- },
- {
- selector: 'node[type = "ordin"]',
- style: {
- content: 'data(label)',
- zIndex: 100,
- width: 'data(style.width)',
- height: 'data(style.height)',
- shape: 'data(style.shape)',
- opacity: 'data(style.opacity)',
- backgroundColor: 'data(style.backgroundColor)',
- borderColor: 'data(style.borderColor)',
- borderWidth: 'data(style.borderWidth)',
- textValign: 'center',
- textHalign: 'center',
- fontSize: (ele) => {
- const w = ele.data('style').width;
- const h = ele.data('style').height;
- const val = Math.min(w, h);
- if (val < 20) return 5;
- if (val > 500) return 60;
- return 10 + ((val - 20) * 50) / 480;
+const getCytoscapeStyle = (darkMode = false) => {
+ // Theme-aware colors
+ const edgeLabelTextColor = darkMode ? '#ffffff' : '#333';
+ const edgeLabelBgColor = darkMode ? '#2d2d2d' : '#fff';
+ const bendNodeColor = darkMode ? '#7e57c2' : '#9575cd';
+ const handleColor = darkMode ? '#ff1744' : '#f50057';
+ const overlayColor = darkMode ? '#fff' : '#000';
+ const nodeTextColor = darkMode ? '#FFFFFF' : '#000'; // Text color for nodes
+
+ return [
+ {
+ selector: '*',
+ style: {
+ overlayOpacity: '0',
},
- textWrap: 'wrap',
- textMaxWidth: 'data(style.width)',
},
- },
- {
- selector: 'node[type="special"]',
- style: {
- width: 8,
- height: 8,
- backgroundColor: 'data(style.backgroundColor)',
- zIndex: 1000,
+ {
+ selector: 'node[type = "ordin"]',
+ style: {
+ content: 'data(label)',
+ zIndex: 100,
+ width: 'data(style.width)',
+ height: 'data(style.height)',
+ shape: 'data(style.shape)',
+ opacity: 'data(style.opacity)',
+ backgroundColor: 'data(style.backgroundColor)',
+ borderColor: 'data(style.borderColor)',
+ borderWidth: 'data(style.borderWidth)',
+ color: nodeTextColor, // Theme-aware text color
+ textValign: 'center',
+ textHalign: 'center',
+ fontSize: (ele) => {
+ const w = ele.data('style').width;
+ const h = ele.data('style').height;
+ const val = Math.min(w, h);
+ if (val < 20) return 5;
+ if (val > 500) return 60;
+ return 10 + ((val - 20) * 50) / 480;
+ },
+ textWrap: 'wrap',
+ textMaxWidth: 'data(style.width)',
+ },
},
- },
-
- {
- selector: 'edge',
- style: {
- curveStyle: 'bezier',
- targetArrowShape: 'triangle',
- arrowScale: 1.2,
+ {
+ selector: 'node[type="special"]',
+ style: {
+ width: 8,
+ height: 8,
+ backgroundColor: 'data(style.backgroundColor)',
+ zIndex: 1000,
+ },
},
- },
- {
- selector: 'edge[type = "ordin"]',
- style: {
- width: 'data(style.thickness)',
- lineColor: 'data(style.backgroundColor)',
- targetArrowColor: 'data(style.backgroundColor)',
- curveStyle: (ele) => {
- const source = ele.source();
- const target = ele.target();
-
- // Check if there are parallel edges
- const parallelEdges = source.edgesWith(target);
- const hasParallelEdges = parallelEdges.length > 1;
-
- // Get positions
- const p1 = source.position();
- const p2 = target.position();
-
- // Calculate distance between nodes
- const distance = Math.sqrt(
- (p2.x - p1.x) ** 2 + (p2.y - p1.y) ** 2,
- );
-
- // Calculate difference
- const dx = Math.abs(p1.x - p2.x);
- const dy = Math.abs(p1.y - p2.y);
-
- // Define a threshold for what counts as "aligned"
- const threshold = 10;
-
- // Check if edge has custom bend data
- const bendDistance = ele.data('bendData')?.bendDistance || 0;
- const hasBend = Math.abs(bendDistance) > 0;
-
- // When nodes are very close, always use straight style to prevent edge disappearance
- if (distance < 50) {
- return 'straight';
- }
-
- // For parallel edges or edges with bend, use bezier curves
- if (hasParallelEdges || hasBend) {
- return 'unbundled-bezier';
- }
- // If aligned horizontally OR vertically, be straight
- if (dx < threshold || dy < threshold) {
- return 'straight';
- }
-
- // use unbundled-bezier to respect bend points
- return 'unbundled-bezier';
- },
- segmentDistances: (ele) => {
- // When nodes are very close, don't apply bend to prevent edge disappearance
- const source = ele.source();
- const target = ele.target();
- const p1 = source.position();
- const p2 = target.position();
- const distance = Math.sqrt(
- (p2.x - p1.x) ** 2 + (p2.y - p1.y) ** 2,
- );
-
- if (distance < 50) {
- return 0;
- }
-
- return ele.data('bendData.bendDistance');
+ {
+ selector: 'edge',
+ style: {
+ curveStyle: 'bezier',
+ targetArrowShape: 'triangle',
+ arrowScale: 1.2,
},
- segmentWeights: 'data(bendData.bendWeight)',
- edgeDistances: 'node-position',
- lineStyle: 'data(style.shape)',
- controlPointDistances: (ele) => {
- // For parallel edges, ensure adequate control point spacing
- const bendDistance = ele.data('bendData')?.bendDistance || 0;
- return Math.abs(bendDistance) > 0 ? bendDistance : undefined;
+ },
+ {
+ selector: 'edge[type = "ordin"]',
+ style: {
+ width: 'data(style.thickness)',
+ lineColor: darkMode ? '#E0E0E0' : 'data(style.backgroundColor)',
+ targetArrowColor: darkMode ? '#E0E0E0' : 'data(style.backgroundColor)',
+ curveStyle: (ele) => {
+ const source = ele.source();
+ const target = ele.target();
+
+ // Check if there are parallel edges
+ const parallelEdges = source.edgesWith(target);
+ const hasParallelEdges = parallelEdges.length > 1;
+
+ // Get positions
+ const p1 = source.position();
+ const p2 = target.position();
+
+ // Calculate distance between nodes
+ const distance = Math.sqrt(
+ (p2.x - p1.x) ** 2 + (p2.y - p1.y) ** 2,
+ );
+
+ // Calculate difference
+ const dx = Math.abs(p1.x - p2.x);
+ const dy = Math.abs(p1.y - p2.y);
+
+ // Define a threshold for what counts as "aligned"
+ const threshold = 10;
+
+ // Check if edge has custom bend data
+ const bendDistance = ele.data('bendData')?.bendDistance || 0;
+ const hasBend = Math.abs(bendDistance) > 0;
+
+ // When nodes are very close, always use straight style to prevent edge disappearance
+ if (distance < 50) {
+ return 'straight';
+ }
+
+ // For parallel edges or edges with bend, use bezier curves
+ if (hasParallelEdges || hasBend) {
+ return 'unbundled-bezier';
+ }
+
+ // If aligned horizontally OR vertically, be straight
+ if (dx < threshold || dy < threshold) {
+ return 'straight';
+ }
+
+ // use unbundled-bezier to respect bend points
+ return 'unbundled-bezier';
+ },
+ segmentDistances: (ele) => {
+ // When nodes are very close, don't apply bend to prevent edge disappearance
+ const source = ele.source();
+ const target = ele.target();
+ const p1 = source.position();
+ const p2 = target.position();
+ const distance = Math.sqrt(
+ (p2.x - p1.x) ** 2 + (p2.y - p1.y) ** 2,
+ );
+
+ if (distance < 50) {
+ return 0;
+ }
+
+ return ele.data('bendData.bendDistance');
+ },
+ segmentWeights: 'data(bendData.bendWeight)',
+ edgeDistances: 'node-position',
+ lineStyle: 'data(style.shape)',
+ controlPointDistances: (ele) => {
+ // For parallel edges, ensure adequate control point spacing
+ const bendDistance = ele.data('bendData')?.bendDistance || 0;
+ return Math.abs(bendDistance) > 0 ? bendDistance : undefined;
+ },
+ controlPointWeights: (ele) => {
+ const bendWeight = ele.data('bendData')?.bendWeight;
+ return bendWeight !== undefined ? bendWeight : 0.5;
+ },
},
- controlPointWeights: (ele) => {
- const bendWeight = ele.data('bendData')?.bendWeight;
- return bendWeight !== undefined ? bendWeight : 0.5;
+ },
+ {
+ selector: 'edge[label]',
+ style: {
+ label: (ele) => {
+ // Get source and target nodes
+ const source = ele.source();
+ const target = ele.target();
+
+ // Calculate distance between nodes
+ const p1 = source.position();
+ const p2 = target.position();
+ const distance = Math.sqrt(
+ (p2.x - p1.x) ** 2 + (p2.y - p1.y) ** 2,
+ );
+
+ // Define minimum distance threshold (in pixels)
+ // Below this distance, hide the label to prevent visual clutter
+ const minDistanceForLabel = 80;
+
+ // Return label only if nodes are far enough apart
+ return distance >= minDistanceForLabel ? ele.data('label') : '';
+ },
+ edgeTextRotation: 'autorotate',
+ zIndex: 999,
+ fontSize: 12,
+ textBackgroundOpacity: 1,
+ textBackgroundPadding: '3px',
+ textBorderWidth: 0,
+ color: edgeLabelTextColor,
+ textBackgroundColor: edgeLabelBgColor,
+ textBackgroundShape: 'roundrectangle',
},
},
- },
- {
- selector: 'edge[label]',
- style: {
- label: (ele) => {
- // Get source and target nodes
- const source = ele.source();
- const target = ele.target();
-
- // Calculate distance between nodes
- const p1 = source.position();
- const p2 = target.position();
- const distance = Math.sqrt(
- (p2.x - p1.x) ** 2 + (p2.y - p1.y) ** 2,
- );
-
- // Define minimum distance threshold (in pixels)
- // Below this distance, hide the label to prevent visual clutter
- const minDistanceForLabel = 80;
-
- // Return label only if nodes are far enough apart
- return distance >= minDistanceForLabel ? ele.data('label') : '';
+ {
+ selector: '.hidden',
+ style: {
+ display: 'none',
},
- edgeTextRotation: 'autorotate',
- zIndex: 999,
- fontSize: 12,
- textBackgroundOpacity: 1,
- textBackgroundPadding: '3px',
- textBorderWidth: 0,
- color: '#333',
- textBackgroundColor: '#fff',
- textBackgroundShape: 'roundrectangle',
},
- },
- {
- selector: '.hidden',
- style: {
- display: 'none',
+ {
+ selector: '.eh-handle,node[type="bend"]',
+ style: {
+ height: 25,
+ width: 25,
+ opacity: 0.4,
+ borderWidth: 5,
+ borderOpacity: 0.1,
+ },
},
- },
- {
- selector: '.eh-handle,node[type="bend"]',
- style: {
- height: 25,
- width: 25,
- opacity: 0.4,
- borderWidth: 5,
- borderOpacity: 0.1,
+ {
+ selector: 'node[type="bend"]',
+ style: {
+ backgroundColor: bendNodeColor,
+ },
},
- },
- {
- selector: 'node[type="bend"]',
- style: {
- backgroundColor: '#9575cd',
+ {
+ selector: '.eh-handle',
+ style: {
+ backgroundColor: handleColor,
+ },
},
- },
- {
- selector: '.eh-handle',
- style: {
- backgroundColor: '#f50057',
+ {
+ selector: ':selected',
+ style: {
+ overlayColor: darkMode ? '#2B8CF7' : overlayColor, // Accent blue for selected in dark mode
+ overlayOpacity: darkMode ? 0.3 : 0.1,
+ overlayPadding: darkMode ? 3 : 5,
+ },
},
- },
- {
- selector: ':selected',
- style: {
- overlayColor: '#000',
- overlayOpacity: 0.1,
- overlayPadding: 5,
+ // Selected nodes - enhanced border
+ {
+ selector: 'node:selected',
+ style: {
+ 'border-color': darkMode ? '#2B8CF7' : 'data(style.borderColor)',
+ 'border-width': darkMode ? 3 : 'data(style.borderWidth)',
+ },
},
- },
-];
+ ];
+};
-export default style;
+export default getCytoscapeStyle;
diff --git a/src/graph-builder/graph-core/1-core.js b/src/graph-builder/graph-core/1-core.js
index 6d2602e..1ec5f94 100644
--- a/src/graph-builder/graph-core/1-core.js
+++ b/src/graph-builder/graph-core/1-core.js
@@ -5,6 +5,7 @@ import Konva from 'konva';
import nodeEditing from 'cytoscape-node-editing';
import $ from 'jquery';
import cyOptions from '../../config/cytoscape-options';
+import getCytoscapeStyle from '../../config/cytoscape-style';
import BendingDistanceWeight from '../calculations/bending-dist-weight';
import { actionType as T } from '../../reducer';
@@ -23,11 +24,17 @@ class CoreGraph {
bendNode;
+ darkMode = false;
+
gridSize = 20; // Configurable grid size in pixels
- constructor(id, element, dispatcher, superState, projectName, nodeValidator, edgeValidator, authorName) {
+ constructor(
+ id, element, dispatcher, superState, projectName,
+ nodeValidator, edgeValidator, authorName, darkMode = false,
+ ) {
if (dispatcher) this.dispatcher = dispatcher;
if (superState) this.superState = superState;
+ this.darkMode = darkMode;
if (typeof cytoscape('core', 'edgehandles') !== 'function') {
cytoscape.use(edgehandles);
}
@@ -38,7 +45,7 @@ class CoreGraph {
gridGuide(cytoscape);
}
// if (cy) this.cy = cy;
- this.cy = cytoscape({ ...cyOptions, container: element });
+ this.cy = cytoscape({ ...cyOptions(darkMode), container: element });
this.cy.on('position', 'node', () => {
this.cy.edges().updateStyle();
});
@@ -131,6 +138,15 @@ class CoreGraph {
isNoControlsMode(node) { return node.data('type') !== 'ordin'; },
});
+ // Grid colors based on dark mode
+ const gridColors = this.darkMode ? {
+ gridColor: '#606060', // Major grid lines - Light Grey
+ lineColor: 'rgba(96, 96, 96, 0.4)', // Minor grid lines - Light Grey (transparent)
+ } : {
+ gridColor: 'rgba(0, 0, 0, 0.2)', // Light mode major grid
+ lineColor: 'rgba(0, 0, 0, 0.1)', // Light mode minor grid
+ };
+
this.cy.gridGuide({
snapToGridOnRelease: true,
snapToGridDuringDrag: false,
@@ -138,6 +154,8 @@ class CoreGraph {
panGrid: true,
gridSpacing: this.gridSize,
snapToAlignmentLocationOnRelease: true,
+ gridColor: gridColors.gridColor,
+ lineColor: gridColors.lineColor,
});
this.cy.edgehandles({
preview: false,
@@ -332,6 +350,28 @@ class CoreGraph {
this.selectDeselectEventHandler();
}
+ updateTheme(darkMode) {
+ this.darkMode = darkMode;
+ const newStyle = getCytoscapeStyle(darkMode);
+ this.cy.style(newStyle);
+
+ // Update grid colors for dark mode
+ const gridColors = darkMode ? {
+ gridColor: '#606060',
+ lineColor: 'rgba(96, 96, 96, 0.4)',
+ } : {
+ gridColor: 'rgba(0, 0, 0, 0.2)',
+ lineColor: 'rgba(0, 0, 0, 0.1)',
+ };
+
+ if (this.cy.gridGuide) {
+ this.cy.gridGuide({
+ gridColor: gridColors.gridColor,
+ lineColor: gridColors.lineColor,
+ });
+ }
+ }
+
reset() {
this.resetAllComp();
this.resetAllAction();
diff --git a/src/graphWorkspace.css b/src/graphWorkspace.css
index 9287120..addfc8d 100644
--- a/src/graphWorkspace.css
+++ b/src/graphWorkspace.css
@@ -1,4 +1,15 @@
.graph-container {
- flex: 1;
- border: 1px solid #888;
+ flex: 1;
+ border: 1px solid #888;
+}
+
+/* DARK MODE - CANVAS / EDITOR AREA */
+
+[data-theme='dark'] .graph-container {
+ background-color: #262626;
+ border: 1px solid #2C2C2C;
+}
+
+[data-theme='dark'] .graph-element {
+ background-color: #262626 !important;
}
\ No newline at end of file
diff --git a/src/reducer/actionType.js b/src/reducer/actionType.js
index 09f7981..ea540e1 100644
--- a/src/reducer/actionType.js
+++ b/src/reducer/actionType.js
@@ -45,6 +45,7 @@ const actionType = {
SET_LOGS: 'SET_LOGS',
SET_LOGS_MESSAGE: 'SET_LOGS_MESSAGE',
SET_GRAPH_INSTANCE: 'SET_GRAPH_INSTANCE',
+ TOGGLE_DARK_MODE: 'TOGGLE_DARK_MODE',
};
export default zealit(actionType);
diff --git a/src/reducer/initialState.js b/src/reducer/initialState.js
index b1432b5..4fb1993 100644
--- a/src/reducer/initialState.js
+++ b/src/reducer/initialState.js
@@ -38,6 +38,7 @@ const initialState = {
octave: false,
logs: false,
logsmessage: '',
+ darkMode: false,
};
const initialGraphState = {
diff --git a/src/reducer/reducer.js b/src/reducer/reducer.js
index dd3139f..948d347 100644
--- a/src/reducer/reducer.js
+++ b/src/reducer/reducer.js
@@ -251,6 +251,10 @@ const reducer = (state, action) => {
return { ...newState };
}
+ case T.TOGGLE_DARK_MODE: {
+ return { ...state, darkMode: !state.darkMode };
+ }
+
default:
return state;
}