From 3616a8e04557a3ec692d3f4470d8e23a0bcc8e65 Mon Sep 17 00:00:00 2001 From: hrshjswniii Date: Mon, 27 Jul 2026 12:10:16 +0530 Subject: [PATCH] feat: add High-Contrast Accessibility Mode Toggle --- client/src/components/ThemeContext.jsx | 33 ++++++++++++++++++- client/src/components/ThemeContext.test.js | 15 +++++++++ client/src/pages/Settings.jsx | 38 +++++++++++++++++++++- client/src/styles.css | 31 ++++++++++++++++++ 4 files changed, 115 insertions(+), 2 deletions(-) create mode 100644 client/src/components/ThemeContext.test.js diff --git a/client/src/components/ThemeContext.jsx b/client/src/components/ThemeContext.jsx index 1d1f20a5..144980aa 100644 --- a/client/src/components/ThemeContext.jsx +++ b/client/src/components/ThemeContext.jsx @@ -18,6 +18,14 @@ function getStoredTheme() { } } +function getStoredHighContrast() { + try { + return localStorage.getItem("voiceforge:highContrast") === "true"; + } catch { + return false; + } +} + function storeTheme(theme) { try { localStorage.setItem("voiceforge:theme", theme); @@ -26,8 +34,17 @@ function storeTheme(theme) { } } +function storeHighContrast(enabled) { + try { + localStorage.setItem("voiceforge:highContrast", String(enabled)); + } catch { + // Storage unavailable + } +} + export function ThemeProvider({ children }) { const [theme, setTheme] = React.useState(getStoredTheme); + const [isHighContrast, setIsHighContrast] = React.useState(getStoredHighContrast); React.useEffect(() => { const root = document.documentElement; @@ -39,12 +56,26 @@ export function ThemeProvider({ children }) { storeTheme(theme); }, [theme]); + React.useEffect(() => { + const root = document.documentElement; + if (isHighContrast) { + root.classList.add("high-contrast"); + } else { + root.classList.remove("high-contrast"); + } + storeHighContrast(isHighContrast); + }, [isHighContrast]); + function toggleTheme() { setTheme((prev) => (prev === "dark" ? "light" : "dark")); } + function toggleHighContrast() { + setIsHighContrast((prev) => !prev); + } + return ( - + {children} ); diff --git a/client/src/components/ThemeContext.test.js b/client/src/components/ThemeContext.test.js new file mode 100644 index 00000000..c5ff69d0 --- /dev/null +++ b/client/src/components/ThemeContext.test.js @@ -0,0 +1,15 @@ +import { describe, it, expect } from "vitest"; + +describe("ThemeContext theme & high-contrast state module", () => { + it("manages high contrast localStorage persistence safely", () => { + if (typeof localStorage !== "undefined") { + localStorage.setItem("voiceforge:highContrast", "true"); + expect(localStorage.getItem("voiceforge:highContrast")).toBe("true"); + + localStorage.setItem("voiceforge:highContrast", "false"); + expect(localStorage.getItem("voiceforge:highContrast")).toBe("false"); + } else { + expect(true).toBe(true); + } + }); +}); diff --git a/client/src/pages/Settings.jsx b/client/src/pages/Settings.jsx index f536348a..770df4d4 100644 --- a/client/src/pages/Settings.jsx +++ b/client/src/pages/Settings.jsx @@ -12,9 +12,10 @@ import { LANGUAGE_STORAGE_KEY, } from "../utils/languages.js"; -import { Trash2, CircleAlert, Download, Upload, Globe } from "lucide-react"; +import { Trash2, CircleAlert, Download, Upload, Globe, Eye } from "lucide-react"; import { useToast, ToastContainer } from "../components/useToast.jsx"; import { LanguageSelector } from "../components/LanguageSelector.jsx"; +import { useTheme } from "../components/ThemeContext.jsx"; import { deleteVoiceProfile, getSavedProfiles, @@ -66,6 +67,7 @@ export default function Settings() { const defaultSettings = DEFAULT_VOICE_SETTINGS; + const { theme, toggleTheme, isHighContrast, toggleHighContrast } = useTheme(); const [voiceSettings, setVoiceSettings] = React.useState(loadVoiceSettings); const [language, setLanguage] = React.useState(loadLanguage); const selectedLangObj = getLanguageByCode(language); @@ -381,6 +383,40 @@ export default function Settings() {

+
+

Appearance & Accessibility

+

+ Customize high-contrast accessibility options, contrast ratios, and visual boundaries. +

+ +
+
+
+ + +
+
+

Backup & Restore

diff --git a/client/src/styles.css b/client/src/styles.css index 2b96bc6c..1aeb68a2 100644 --- a/client/src/styles.css +++ b/client/src/styles.css @@ -134,3 +134,34 @@ textarea { scrollbar-color: #22c55e var(--bg-page); } } + +/* High-Contrast Accessibility Mode */ +.high-contrast { + --bg-page: #000000 !important; + --bg-card: #000000 !important; + --bg-input: #000000 !important; + --text-base: #ffffff !important; + --text-muted: #ffff00 !important; + --border: #ffffff !important; + --ring: #ffff00 !important; + color: #ffffff !important; + background-color: #000000 !important; +} + +.high-contrast * { + border-color: #ffffff !important; + box-shadow: none !important; +} + +.high-contrast button, +.high-contrast input, +.high-contrast select, +.high-contrast textarea, +.high-contrast a { + border: 2px solid #ffffff !important; +} + +.high-contrast :focus-visible { + outline: 3px solid #ffff00 !important; + outline-offset: 3px !important; +}