[#1288] feat(frontend): add high-contrast accessibility mode and persist theme to user profile - #1384
Merged
Conversation
…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.
|
@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! 🚀 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds a high-contrast accessibility mode and syncs theme preferences to the user profile.
High contrast mode
highContrastwas 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 asdata-contrast="high", which makes contrast a pure CSS concern —index.cssoverrides the palette variables, so every surface already consuming them picks up the stronger treatment without individual components needing to branch on a flag.2px, so boundaries stay visible instead of collapsing into hairlines.Profile sync
useThemeProfileSyncdebounces changes and pushesdisplay.themeanddisplay.highContrastto the existingPUT /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 thedisplaycategory 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
src/theme/highContrast.test.tsxcovering the storage helpers, document attributes, both toggles, persistence across mounts, and both sync paths (local-only vs. pushing to the endpoint)eslintandtsc --noEmitclean on every changed filePre-existing baseline failures (not touched here): the frontend suite on
mainalready fails with 76 tests across 15 files. Identical before and after this branch, so no new failures are introduced..github/workflows/ci.ymlanddocker.ymlalso fail to parse onmain— an upstream issue I have left alone.Reviewer notes
PUT /api/v1/preferences; the actual route is/:userId/:category/:key, so that is what this uses.text-red-400/text-green-400utilities 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