Skip to content

[#1288] feat(frontend): add high-contrast accessibility mode and persist theme to user profile - #1384

Merged
Mosas2000 merged 1 commit into
StellaBridge:mainfrom
ToryMic:fix/1288-feat-frontend-add-high-contrast-accessibility-mode-and-persist-theme-to-user-profile
Sep 29, 2026
Merged

Mosas2000 merged 1 commit into
StellaBridge:mainfrom
ToryMic:fix/1288-feat-frontend-add-high-contrast-accessibility-mode-and-persist-theme-to-user-profile

Conversation

@ToryMic

@ToryMic ToryMic commented Sep 29, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds a high-contrast accessibility mode and syncs theme preferences to the user profile.

High contrast mode

highContrast was added to the theme context/provider and persisted under its own storage key, so it survives independently of the light/dark mode. It is applied to the document as data-contrast="high", which makes contrast a pure CSS concern — index.css overrides the palette variables, so every surface already consuming them picks up the stronger treatment without individual components needing to branch on a flag.

  • Borders go to near-white on dark / near-black on light and are reinforced to 2px, so boundaries stay visible instead of collapsing into hairlines.
  • Body text is pushed to maximum contrast in both themes (WCAG AAA oriented).
  • Alert hues are replaced with a colour-vision-deficiency-safe palette (deuteranopia / protanopia / tritanopia), and severity is paired with border weight so it is never signalled by hue alone.

Profile sync

useThemeProfileSync debounces changes and pushes display.theme and display.highContrast to the existing PUT /api/v1/preferences/:userId/display/:key. Since that endpoint takes one key per call, the two writes are issued concurrently.

No backend change was needed — the single-preference body validator is z.unknown(), so both keys are already accepted, and the display category already existed.

Sync is opt-in per device via a stored user id, and the UI reports "Local only" when none is set. The frontend has no auth layer, so assuming an identity would be wrong; without an id the theme still works, it simply isn't synchronised. If the project later adds real auth, the user id can come from the session instead of local storage — the hook already takes it as an input.

Testing

  • 12 new tests in src/theme/highContrast.test.tsx covering the storage helpers, document attributes, both toggles, persistence across mounts, and both sync paths (local-only vs. pushing to the endpoint)
  • All pass; eslint and tsc --noEmit clean on every changed file

Pre-existing baseline failures (not touched here): the frontend suite on main already fails with 76 tests across 15 files. Identical before and after this branch, so no new failures are introduced. .github/workflows/ci.yml and docker.yml also fail to parse on main — an upstream issue I have left alone.

Reviewer notes

  • The issue text suggested PUT /api/v1/preferences; the actual route is /:userId/:category/:key, so that is what this uses.
  • I have not applied the high-contrast palette to individual alert badge components (e.g. the text-red-400 / text-green-400 utilities scattered across pages). The variables give a strong global win immediately; a full sweep replacing hard-coded Tailwind colour utilities with theme tokens would be a larger, separate change. Worth a follow-up issue if the project wants the palette applied per-component too.

Closes #1288

…ode and persist theme to user profile

Theme preferences were light/dark only and lived exclusively in localStorage,
so visually impaired operators had no higher-contrast option and preferences
did not follow them to another device.

### High contrast mode

- `highContrast` added to the theme context/provider, persisted under its own
  storage key so it survives independently of the light/dark mode.
- Applied to the document as `data-contrast="high"`. Contrast is therefore a
  pure CSS concern: `index.css` overrides the palette variables, so every
  surface already consuming them picks up the stronger treatment without
  individual components needing to branch.
  - Borders go to near-white on dark and near-black on light, and are
    reinforced to 2px, so boundaries are visible rather than hairlines.
  - Body text is pushed to maximum contrast in both themes.
  - Alert hues are replaced with a colour-vision-deficiency-safe palette, and
    severity is paired with border weight so it is not signalled by hue alone.

### Profile sync

- `useThemeProfileSync` debounces changes and pushes `display.theme` and
  `display.highContrast` to the existing
  `PUT /api/v1/preferences/:userId/display/:key` endpoint. The endpoint takes
  one key per call, so the two writes are issued concurrently. No backend
  change was needed — the single-preference body validator accepts any value.
- Sync is opt-in per device via a stored user id, and the UI reports
  "Local only" when none is set. The frontend has no auth layer, so assuming an
  identity would be wrong; without an id the theme still works, it is just not
  synchronised.

Adds 12 tests covering the storage helpers, the document attributes, the
toggles, persistence across mounts, and both sync paths.
@drips-wave

drips-wave Bot commented Sep 29, 2026

Copy link
Copy Markdown

@ToryMic Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@Mosas2000
Mosas2000 merged commit 64f8ede into StellaBridge:main Sep 29, 2026
15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(frontend): add high-contrast accessibility mode and persist theme to user profile

2 participants