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.
+
+
+
+
+
+
+
+ High-Contrast Accessibility Mode
+
+
+ Enforces maximum WCAG AAA contrast ratios, thick element borders, and bright yellow focus rings.
+
+
+
+
+
+
+
+
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;
+}