From f3523ec82131954cb7494b6b128bbbf7bd83cbfb Mon Sep 17 00:00:00 2001 From: Dave Date: Wed, 27 May 2026 13:55:09 -0400 Subject: [PATCH] feature/IO-3702-ESPD-UI-AND-FIXES - Stage 6 (Dark Mode) --- .gitignore | 1 + src/main/store/store.ts | 1 + src/renderer/src/App.tsx | 150 +++++++++++----- .../Settings/Settings.Appearance.tsx | 33 ++++ .../src/components/Settings/Settings.tsx | 35 ++-- src/renderer/src/util/themeModeContext.ts | 18 ++ src/util/translations/en-US/renderer.json | 167 +++++++++--------- 7 files changed, 268 insertions(+), 137 deletions(-) create mode 100644 src/renderer/src/components/Settings/Settings.Appearance.tsx create mode 100644 src/renderer/src/util/themeModeContext.ts diff --git a/.gitignore b/.gitignore index 4750699..96d0573 100644 --- a/.gitignore +++ b/.gitignore @@ -51,3 +51,4 @@ override.tf.json .terraformrc terraform.rc +/.eslintcache diff --git a/src/main/store/store.ts b/src/main/store/store.ts index fbfa18d..f973fda 100644 --- a/src/main/store/store.ts +++ b/src/main/store/store.ts @@ -10,6 +10,7 @@ const store = new Store({ enabled: false, interval: 30000, }, + darkMode: false, esApiKey: "", }, app: { diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index 571f7ca..27f36e5 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -1,5 +1,5 @@ -import { ConfigProvider, Layout } from "antd"; -import { FC, useEffect } from "react"; +import { ConfigProvider, Layout, theme } from "antd"; +import { FC, useCallback, useEffect, useMemo, useState } from "react"; import { ErrorBoundary } from "react-error-boundary"; import { Provider } from "react-redux"; import { HashRouter, Route, Routes, useNavigate } from "react-router"; @@ -9,8 +9,11 @@ import Settings from "./components/Settings/Settings"; import UpdateAvailable from "./components/UpdateAvailable/UpdateAvailable"; import reduxStore from "./redux/redux-store"; import { NotificationProvider } from "./util/notificationContext"; +import { ThemeModeContext } from "./util/themeModeContext"; import ipcTypes from "../../util/ipcTypes.json"; +const { darkAlgorithm, defaultAlgorithm } = theme; + const ScrubHistoryNavigationBridge: FC = () => { const navigate = useNavigate(); @@ -34,53 +37,112 @@ const ScrubHistoryNavigationBridge: FC = () => { return null; }; -const App: FC = () => { +const AppShell: FC = () => { + const { token } = theme.useToken(); + return ( - - - - - - - + + + + + + - - - - } /> - } /> - - - - - - - - + + + } /> + } /> + + + + + + + + ); +}; + +const App: FC = () => { + const [isDarkMode, setIsDarkMode] = useState(false); + + useEffect(() => { + let cancelled = false; + + window.electron.ipcRenderer + .invoke(ipcTypes.toMain.settings.get, "darkMode") + .then((value: unknown) => { + if (!cancelled) { + setIsDarkMode(value === true); + } + }) + .catch((error) => { + console.error("Failed to load dark mode setting", error); + }); + + return () => { + cancelled = true; + }; + }, []); + + useEffect(() => { + document.documentElement.dataset.theme = isDarkMode ? "dark" : "light"; + document.documentElement.style.colorScheme = isDarkMode ? "dark" : "light"; + }, [isDarkMode]); + + const updateDarkMode = useCallback((enabled: boolean): void => { + setIsDarkMode(enabled); + window.electron.ipcRenderer + .invoke(ipcTypes.toMain.settings.set, "darkMode", enabled) + .catch((error) => { + console.error("Failed to save dark mode setting", error); + }); + }, []); + + const themeModeContextValue = useMemo( + () => ({ + isDarkMode, + setDarkMode: updateDarkMode, + }), + [isDarkMode, updateDarkMode], + ); + + const appTheme = useMemo( + () => ({ + algorithm: isDarkMode ? darkAlgorithm : defaultAlgorithm, + token: { + borderRadius: 8, + }, + components: { + Card: { + boxShadow: isDarkMode + ? "0 2px 8px rgba(0, 0, 0, 0.45)" + : "0 2px 8px rgba(0, 0, 0, 0.1)", + }, + }, + }), + [isDarkMode], + ); + + return ( + + + + + ); }; diff --git a/src/renderer/src/components/Settings/Settings.Appearance.tsx b/src/renderer/src/components/Settings/Settings.Appearance.tsx new file mode 100644 index 0000000..c36ab47 --- /dev/null +++ b/src/renderer/src/components/Settings/Settings.Appearance.tsx @@ -0,0 +1,33 @@ +import { Card, Flex, Space, Switch, Typography } from "antd"; +import { FC } from "react"; +import { useTranslation } from "react-i18next"; +import { useThemeMode } from "@renderer/util/themeModeContext"; + +const SettingsAppearance: FC = () => { + const { t } = useTranslation(); + const { isDarkMode, setDarkMode } = useThemeMode(); + + return ( + + + + + {t("settings.labels.darkMode")} + + + {t("settings.labels.darkModeDescription")} + + + + + + + ); +}; + +export default SettingsAppearance; diff --git a/src/renderer/src/components/Settings/Settings.tsx b/src/renderer/src/components/Settings/Settings.tsx index 5938a2d..0b3efd1 100644 --- a/src/renderer/src/components/Settings/Settings.tsx +++ b/src/renderer/src/components/Settings/Settings.tsx @@ -3,6 +3,7 @@ import { Button, Space } from "antd"; import { ArrowLeftOutlined } from "@ant-design/icons"; import { FC } from "react"; import { useNavigate } from "react-router"; +import SettingsAppearance from "./Settings.Appearance"; import SettingsConfig from "./Settings.Config"; import SettingsWatcher from "./Settings.Watcher"; @@ -10,20 +11,30 @@ const Settings: FC = () => { const navigate = useNavigate(); return ( - - +
+ + - + - - + + + + +
); }; diff --git a/src/renderer/src/util/themeModeContext.ts b/src/renderer/src/util/themeModeContext.ts new file mode 100644 index 0000000..3b6e187 --- /dev/null +++ b/src/renderer/src/util/themeModeContext.ts @@ -0,0 +1,18 @@ +import { createContext, useContext } from "react"; + +export type ThemeModeContextValue = { + isDarkMode: boolean; + setDarkMode: (enabled: boolean) => void; +}; + +export const ThemeModeContext = createContext( + null, +); + +export function useThemeMode(): ThemeModeContextValue { + const context = useContext(ThemeModeContext); + if (!context) { + throw new Error("useThemeMode must be used within ThemeModeContext."); + } + return context; +} diff --git a/src/util/translations/en-US/renderer.json b/src/util/translations/en-US/renderer.json index 5701936..80ce4eb 100644 --- a/src/util/translations/en-US/renderer.json +++ b/src/util/translations/en-US/renderer.json @@ -1,83 +1,88 @@ { - "translation": { - "auth": { - "labels": { - "welcome": "Hi {{name}}" - }, - "login": { - "error": "The username and password combination provided is not valid.", - "login": "Log In", - "resetpassword": "Reset Password" - } - }, - "dashboard": { - "actions": { - "clear_all": "Clear All" - }, - "labels": { - "dashboard": "Dashboard", - "estimate_scrub_history": "Scrub History", - "last_processed": "Last Processed at", - "scrub_results": "Scrub Results", - "total_estimates": "Total Estimates" - } - }, - "errors": { - "errorboundary": "Uh oh - we've hit an error.", - "notificationtitle": "Error Encountered" - }, - "navigation": { - "home": "Home", - "settings": "Settings", - "signout": "Sign Out" - }, - "settings": { - "actions": { - "addpath": "Add path", - "startwatcher": "Start Watcher", - "stopwatcher": "Stop Watcher\n" - }, - "errors": { - "duplicatePath": "The selected directory is already used in another configuration." - }, - "labels": { - "actions": "Actions", - "addPaintScalePath": "Add Paint Scale Path", - "config": "Configuration", - "emsOutFilePath": "EMS Out File Path (Parts Order, etc.)", - "esApiKey": "Estimate Scrubber API Key", - "invalidPath": "Path not set or invalid", - "paintScalePath": "Paint Scale Path", - "paintScaleSettingsInput": "BSMS To Paint Scale", - "paintScaleSettingsOutput": "Paint Scale To BSMS", - "paintScaleType": "Paint Scale Type", - "pollingInterval": "Polling Interval (m)", - "pollinginterval": "Polling Interval (ms)", - "ppcfilepath": "Parts Price Change File Path", - "remove": "Remove", - "selectPaintScaleType": "Select Paint Scale Type", - "started": "Started", - "stopped": "Stopped", - "validPath": "Valid path", - "watchedpaths": "Watched Paths", - "watchermodepolling": "Polling", - "watchermoderealtime": "Real Time", - "watcherstatus": "Watcher Status" - }, - "validation": { - "esApiKeyRequired": "Estimate Scrubber API Key is required." - } - }, - "title": { - "imex": "ImEX Online", - "rome": "Rome Online" - }, - "updates": { - "apply": "Apply Update", - "applying": "Applying update", - "available": "An update is available.", - "download": "Download Update", - "downloading": "An update is downloading." - } - } + "translation": { + "auth": { + "labels": { + "welcome": "Hi {{name}}" + }, + "login": { + "error": "The username and password combination provided is not valid.", + "login": "Log In", + "resetpassword": "Reset Password" + } + }, + "dashboard": { + "actions": { + "clear_all": "Clear All" + }, + "labels": { + "dashboard": "Dashboard", + "estimate_scrub_history": "Scrub History", + "last_processed": "Last Processed at", + "scrub_results": "Scrub Results", + "total_estimates": "Total Estimates" + } + }, + "errors": { + "errorboundary": "Uh oh - we've hit an error.", + "notificationtitle": "Error Encountered" + }, + "navigation": { + "home": "Home", + "settings": "Settings", + "signout": "Sign Out" + }, + "settings": { + "actions": { + "addpath": "Add path", + "startwatcher": "Start Watcher", + "stopwatcher": "Stop Watcher\n" + }, + "errors": { + "duplicatePath": "The selected directory is already used in another configuration." + }, + "labels": { + "actions": "Actions", + "addPaintScalePath": "Add Paint Scale Path", + "appearance": "Appearance", + "config": "Configuration", + "dark": "Dark", + "darkMode": "Dark Mode", + "darkModeDescription": "Use the dark application theme.", + "emsOutFilePath": "EMS Out File Path (Parts Order, etc.)", + "esApiKey": "Estimate Scrubber API Key", + "invalidPath": "Path not set or invalid", + "light": "Light", + "paintScalePath": "Paint Scale Path", + "paintScaleSettingsInput": "BSMS To Paint Scale", + "paintScaleSettingsOutput": "Paint Scale To BSMS", + "paintScaleType": "Paint Scale Type", + "pollingInterval": "Polling Interval (m)", + "pollinginterval": "Polling Interval (ms)", + "ppcfilepath": "Parts Price Change File Path", + "remove": "Remove", + "selectPaintScaleType": "Select Paint Scale Type", + "started": "Started", + "stopped": "Stopped", + "validPath": "Valid path", + "watchedpaths": "Watched Paths", + "watchermodepolling": "Polling", + "watchermoderealtime": "Real Time", + "watcherstatus": "Watcher Status" + }, + "validation": { + "esApiKeyRequired": "Estimate Scrubber API Key is required." + } + }, + "title": { + "imex": "ImEX Online", + "rome": "Rome Online" + }, + "updates": { + "apply": "Apply Update", + "applying": "Applying update", + "available": "An update is available.", + "download": "Download Update", + "downloading": "An update is downloading." + } + } }