A light / dark / system switch in the header, instead of a trip to Your details #9

Closed
opened 2026-09-05 12:08:44 +00:00 by tiagoagueda · 0 comments
Owner

Observation

i need to incorporate a set of icons so i can switch the theme dark/light on the top right corner of the dashboard

What exists today: more than it looks

The preference exists, and everything beneath the switch is built:

  • Profile.theme with three values — system, light, dark — default system (accounts/models.py: Theme at line 76, the field at line 125).
  • The context processor (core/context_processors.py) resolves it to ui_theme, and base.html stamps it as data-theme on <html> (line 2); "system" stamps nothing.
  • The stylesheet's dark variant (assets/css/app.css) honours an explicit data-theme first and prefers-color-scheme otherwise, so all three states already behave correctly.

What is missing is the control. Today the only way to change theme is Your details, the Theme select, Save (accounts/forms.py, line 53) — a trip nobody makes to flip a switch. The header's right cluster (base.html, line 57) holds Capture, Record, the display name and Sign out, nothing else.

So this issue is: a switch in that cluster, drawn with #8's icons, writing to the field that already exists.

Shape

  • Where. The header is shared by every page, so the switch sits in the right cluster on every page — the dashboard included — rather than on the dashboard alone. A control that appears on one page and vanishes on the next is a puzzle, not a feature.
  • What it does. Three states, not two, because the field has three and "match the operating system" is the right default for people who never think about it. Proposal: one icon button that cycles light → dark → system, showing sun, moon and monitor in turn, with the current state and the next one in a title and a visually hidden label ("Theme: dark. Switch to match the operating system"). A three-segment control is the alternative — clearer, and wider than a phone header can spare.
  • How it persists. A small POST view in the accounts app that writes Profile.theme, called from the button through htmx, with a plain form as the no-script fallback. The choice then follows the account to every device, and the server renders the right data-theme on the next load — no flash of the wrong theme, which is the classic failure of client-only toggles.
  • How it applies instantly. A delegated handler in static/js/app.js, the same pattern as data-autosubmit, flips data-theme on <html> the moment the button is pressed, before the request returns. No inline script: the CSP is script-src 'self' and the existing file is already the place for this.
  • Signed-out pages. No profile, so no switch; the sign-in page follows the operating system as it does today. A cookie could carry the choice for anonymous visitors; not worth it for one page.
  • Native controls, while in there. app.css never sets color-scheme. Worth checking on the ragnar instance: without color-scheme: dark on the root when the dark theme is active, scrollbars, <select> popups and the date picker most likely keep their light chrome. A one-line rule keyed on the same data-theme and media query as the variant fixes it, and is wanted with or without the switch.

Classification

Enhancement. Not breaking: the field exists, the default is unchanged, and the select on Your details stays for anyone who prefers it.

Depends on

#8 for the sun, moon and monitor icons. It could ship first with three pasted SVGs, but that is precisely the ad-hoc icon use #8 exists to prevent.

Open questions

  1. Cycle button or segmented control? Proposal: the cycle button in the header, and the select on Your details as the explicit version of the same thing.
  2. Persist per browser as well as per account, for someone who wants dark on the laptop and light at the desk? Proposal: no. Per account is what "preference" means, and each device is one click away from what it wants.
## Observation > i need to incorporate a set of icons so i can switch the theme dark/light on the top right corner of the dashboard ## What exists today: more than it looks The preference exists, and everything beneath the switch is built: - `Profile.theme` with three values — system, light, dark — default system (`accounts/models.py`: `Theme` at line 76, the field at line 125). - The context processor (`core/context_processors.py`) resolves it to `ui_theme`, and `base.html` stamps it as `data-theme` on `<html>` (line 2); "system" stamps nothing. - The stylesheet's `dark` variant (`assets/css/app.css`) honours an explicit `data-theme` first and `prefers-color-scheme` otherwise, so all three states already behave correctly. What is missing is the control. Today the only way to change theme is *Your details*, the *Theme* select, *Save* (`accounts/forms.py`, line 53) — a trip nobody makes to flip a switch. The header's right cluster (`base.html`, line 57) holds *Capture*, *Record*, the display name and *Sign out*, nothing else. So this issue is: a switch in that cluster, drawn with #8's icons, writing to the field that already exists. ## Shape - **Where.** The header is shared by every page, so the switch sits in the right cluster on every page — the dashboard included — rather than on the dashboard alone. A control that appears on one page and vanishes on the next is a puzzle, not a feature. - **What it does.** Three states, not two, because the field has three and "match the operating system" is the right default for people who never think about it. Proposal: one icon button that cycles light → dark → system, showing `sun`, `moon` and `monitor` in turn, with the current state and the next one in a `title` and a visually hidden label ("Theme: dark. Switch to match the operating system"). A three-segment control is the alternative — clearer, and wider than a phone header can spare. - **How it persists.** A small POST view in the accounts app that writes `Profile.theme`, called from the button through htmx, with a plain form as the no-script fallback. The choice then follows the account to every device, and the server renders the right `data-theme` on the next load — no flash of the wrong theme, which is the classic failure of client-only toggles. - **How it applies instantly.** A delegated handler in `static/js/app.js`, the same pattern as `data-autosubmit`, flips `data-theme` on `<html>` the moment the button is pressed, before the request returns. No inline script: the CSP is `script-src 'self'` and the existing file is already the place for this. - **Signed-out pages.** No profile, so no switch; the sign-in page follows the operating system as it does today. A cookie could carry the choice for anonymous visitors; not worth it for one page. - **Native controls, while in there.** `app.css` never sets `color-scheme`. Worth checking on the ragnar instance: without `color-scheme: dark` on the root when the dark theme is active, scrollbars, `<select>` popups and the date picker most likely keep their light chrome. A one-line rule keyed on the same `data-theme` and media query as the variant fixes it, and is wanted with or without the switch. ## Classification Enhancement. Not breaking: the field exists, the default is unchanged, and the select on *Your details* stays for anyone who prefers it. ## Depends on #8 for the sun, moon and monitor icons. It could ship first with three pasted SVGs, but that is precisely the ad-hoc icon use #8 exists to prevent. ## Open questions 1. Cycle button or segmented control? Proposal: the cycle button in the header, and the select on *Your details* as the explicit version of the same thing. 2. Persist per browser as well as per account, for someone who wants dark on the laptop and light at the desk? Proposal: no. Per account is what "preference" means, and each device is one click away from what it wants.
tiagoagueda added this to the 0.2.0 milestone 2026-09-05 12:08:44 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Reference
Postulo/postulo#9
No description provided.