From 9cbffc4acb9a81ef7bfcb705d0453d39cdca43e8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 25 Feb 2026 13:47:41 +0000 Subject: [PATCH 1/2] Initial plan From c428a7ec00e3b600cd2ecbb8edfdd9112ebf5d31 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 25 Feb 2026 13:59:49 +0000 Subject: [PATCH 2/2] feat(ui): add dark mode support with system preference detection and localStorage persistence Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com> --- .env.demo | 5 ++ app/config.py | 10 +++ app/utils/settings_service.py | 14 ++++ app/views/base.py | 4 ++ docs/ConfigurationGuide.md | 26 +++++++ frontend/static/js/common.js | 75 ++++++++++++++++++++ frontend/static/styles.css | 128 ++++++++++++++++++++++++++++++++++ frontend/templates/base.html | 38 +++++++++- tests/test_dark_mode.py | 117 +++++++++++++++++++++++++++++++ 9 files changed, 416 insertions(+), 1 deletion(-) create mode 100644 tests/test_dark_mode.py diff --git a/.env.demo b/.env.demo index a54995f2..bdd7ff03 100644 --- a/.env.demo +++ b/.env.demo @@ -6,6 +6,11 @@ EXTERNAL_HOSTNAME=docuelevate.example.com GOTENBERG_URL=http://gotenberg:3000 ALLOW_FILE_DELETE=true # Allow deletion of file records +# **UI / Appearance** +# Default colour scheme: system (follow OS), light, or dark +# Individual users can always override with the navbar dark-mode toggle. +# UI_DEFAULT_COLOR_SCHEME=system + # **Batch Processing Settings** # Control throttling behavior for the /processall endpoint to prevent overwhelming downstream APIs PROCESSALL_THROTTLE_THRESHOLD=20 # Number of files above which throttling is applied (default: 20) diff --git a/app/config.py b/app/config.py index 41398e91..7f5ccbc8 100644 --- a/app/config.py +++ b/app/config.py @@ -382,6 +382,16 @@ class Settings(BaseSettings): ), ) + # UI / Appearance + ui_default_color_scheme: str = Field( + default="system", + description=( + "Default color scheme for the web interface. " + "Options: 'system' (follow OS preference), 'light', 'dark'. " + "Individual users can override this with the in-app toggle; their choice is persisted in localStorage." + ), + ) + # Rate Limiting Configuration (see SECURITY_AUDIT.md and docs/API.md) # Protects against DoS attacks and API abuse rate_limiting_enabled: bool = Field( diff --git a/app/utils/settings_service.py b/app/utils/settings_service.py index 61039b9b..41faacc8 100644 --- a/app/utils/settings_service.py +++ b/app/utils/settings_service.py @@ -1153,6 +1153,20 @@ SETTING_METADATA = { "required": False, "restart_required": False, }, + # UI / Appearance + "ui_default_color_scheme": { + "category": "UI", + "description": ( + "Default color scheme for the web interface. " + "Options: 'system' (follow OS preference), 'light', 'dark'. " + "Individual users can override this with the in-app toggle; their choice is persisted in localStorage." + ), + "type": "string", + "sensitive": False, + "required": False, + "restart_required": False, + "options": ["system", "light", "dark"], + }, } diff --git a/app/views/base.py b/app/views/base.py index c3b338a7..8b10c514 100644 --- a/app/views/base.py +++ b/app/views/base.py @@ -34,11 +34,15 @@ def template_response_with_version(*args, **kwargs): req = args[1].get("request") if req is not None and hasattr(req.state, "csrf_token"): args[1].setdefault("csrf_token", req.state.csrf_token) + # Inject default color scheme for dark-mode initialisation + args[1].setdefault("ui_default_color_scheme", getattr(settings, "ui_default_color_scheme", "system")) elif "context" in kwargs and isinstance(kwargs["context"], dict): kwargs["context"].setdefault("version", settings.version) req = kwargs["context"].get("request") if req is not None and hasattr(req.state, "csrf_token"): kwargs["context"].setdefault("csrf_token", req.state.csrf_token) + # Inject default color scheme for dark-mode initialisation + kwargs["context"].setdefault("ui_default_color_scheme", getattr(settings, "ui_default_color_scheme", "system")) return original_template_response(*args, **kwargs) diff --git a/docs/ConfigurationGuide.md b/docs/ConfigurationGuide.md index 0e2e330c..90c386df 100644 --- a/docs/ConfigurationGuide.md +++ b/docs/ConfigurationGuide.md @@ -759,6 +759,32 @@ For detailed setup instructions, see the [Notifications Setup Guide](Notificatio | `UPTIME_KUMA_URL` | Uptime Kuma push URL for monitoring the application's health. | | `UPTIME_KUMA_PING_INTERVAL` | How often to ping Uptime Kuma in minutes (default: `5`). | +### UI / Appearance + +DocuElevate supports a **dark mode** toggle in the navbar. Users can switch between light and dark themes at any time; their choice is stored in `localStorage` and persists across page reloads in the same browser. + +Administrators can set the **site-wide default** colour scheme that is applied when a user has not yet made a personal choice: + +| **Variable** | **Description** | **Default** | +|----------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------|-------------| +| `UI_DEFAULT_COLOR_SCHEME` | Default colour scheme for all users. Options: `system` (follow OS preference), `light`, `dark`. Users can always override with the navbar toggle. | `system` | + +**How it works:** + +1. On page load an inline script checks the user's `localStorage` preference first. +2. If no stored preference exists, the server-supplied `UI_DEFAULT_COLOR_SCHEME` is used. +3. When the value is `system` (the default), the OS-level `prefers-color-scheme` media query is respected. +4. Clicking the 🌙 / ☀️ toggle in the navbar saves the new preference to `localStorage` immediately. + +**WCAG AA compliance:** All dark-mode colour pairs have been chosen with a minimum 4.5:1 contrast ratio for normal text and 3:1 for large text. + +**Example:** + +```dotenv +# Force dark mode for all users by default +UI_DEFAULT_COLOR_SCHEME=dark +``` + ## Configuration Examples ### Minimal Configuration diff --git a/frontend/static/js/common.js b/frontend/static/js/common.js index 07c15e09..2d9dd486 100644 --- a/frontend/static/js/common.js +++ b/frontend/static/js/common.js @@ -34,6 +34,81 @@ function getCsrfToken() { }; })(); +// --------------------------------------------------------------------------- +// Dark mode +// --------------------------------------------------------------------------- +// Preference is stored in localStorage under the key 'colorScheme'. +// Values: 'dark' | 'light' (absence means "follow server/system default"). +// The anti-flash @@ -93,6 +105,18 @@ About + + +
@@ -152,6 +176,18 @@ About + + +