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 &&
} -
+
{text}
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; }