Skip to content

Make panel borders survive dark mode (M-THEME THEME.5) - #38

Open
NCCU-Schultz-Lab wants to merge 2 commits into
mainfrom
theme5-dark-mode-contrast
Open

Make panel borders survive dark mode (M-THEME THEME.5)#38
NCCU-Schultz-Lab wants to merge 2 commits into
mainfrom
theme5-dark-mode-contrast

Conversation

@NCCU-Schultz-Lab

Copy link
Copy Markdown
Collaborator

"Fix dark mode contrasts and add borders (current approach does not work."

The measurement changed the target

Dark mode is not a palette — it's a whole-page CSS filter:

html { filter: invert(1) hue-rotate(180deg) !important; }
canvas, img, iframe, video { filter: invert(1) hue-rotate(180deg) !important; }

Every colour is authored once for light mode; dark mode is its mathematical
inversion. Computing WCAG ratios through that actual chain (invert, then the
W3C hue-rotate matrix — not an HSL rotation, which would predict different
numbers than the browser produces):

pair light dark verdict
body text on panel 7.24:1 8.91:1 text was never broken — it improves
heading on panel 13.98:1 14.12:1 fine
border vs panel 1.18:1 1.14:1 ❌ invisible
panel vs page 1.05:1 1.03:1 ❌ invisible
viewer frame 1.63:1 1.70:1 ❌ barely there

So "contrast" and "borders" were one defect, not two: structural separation,
not legibility. That's why the request asked for both in one breath.

The fix: the mid-tone rule

A property of the filter drives everything: a light grey border inverts to a
near-black border on a near-black panel; a dark border has the mirror problem
in light mode. Only mid-tones survive inversion.

#7d8ea3 is the lightest value clearing WCAG 1.4.11's 3:1 bar for non-text
UI components in both modes:

light dark
border vs panel 1.18 → 3.20 1.14 → 4.30
border vs page 1.23 → 3.35 1.18 → 4.45
viewer frame 1.63 → 4.76 1.70 → 6.38

3:1 is the correct bar here — borders are UI components, not text. Using 4.5:1
would force a heavier border than the light-mode design wants.

New quantui/theme.py holds the tokens plus the measurements and reasoning,
so nobody has to re-derive why a border is that specific grey. 19 in-app sites
migrated.

Deliberately not done

  • Not a full palette migration. ~390 hex literals across 17 files, but most
    are semantic accents (error red, success green) whose hue survives
    hue-rotate(180) and already look right in both modes. Wholesale migration =
    large, visually-unverifiable change for no user-visible gain.
  • analytics.py untouched — it writes a standalone dashboard HTML opened
    directly in a browser, where the invert filter never applies. Its light borders
    are correct; changing them "for consistency" would have been a bug.
  • Plot grid/bond colours untouched — THEME.2/.3, and some are already
    theme-aware.

Trade-off worth reviewing

Under a global invert there is one source value per colour, so light and dark
can't be tuned independently. Light-mode borders are consequently more visible
than before (3.20:1, was 1.18:1). That reads as normal design weight and is the
price of dark mode working at all — but it is a visible change to light mode,
so worth a look.

Decoupling them means removing the invert filter, which is THEME.6's problem. The
tokens are the seam where that lands.

Testing

14 new tests that re-derive the filter maths (including guards on the measuring
instrument itself — white-on-black must be 21:1) and assert both tokens clear
3:1 in both modes. One test asserts the old values would fail, documenting
why the tokens exist. A future "let's soften those borders" tweak now fails
loudly instead of silently restoring the bug.

Suite 1985 passed / 17 skipped. pre-commit run --all-files clean.

⚠️ Needs a Voilà pass — contrast is measured, but whether it looks right is
not something I can verify. Worth checking both themes, and specifically that
light mode's heavier borders read as intentional.

🤖 Generated with Claude Code

NCCU-Schultz-Lab and others added 2 commits July 31, 2026 17:21
The badge rendered as "package or version not found" even after quantui 0.5.1
published successfully. Not a packaging problem — two independent stale
caches, both holding a genuine 404 from before the package existed:

  - shields.io caches badge results for 3h (max-age=10800) behind Cloudflare.
    The badge URL was first requested when the README was rendered during the
    pre-flight metadata work, well before anything was published.
  - GitHub never lets a browser hit shields.io directly; it proxies README
    images through camo.githubusercontent.com, which cached its own copy of
    that stale SVG and doesn't track the origin's cache headers.

Appending ?v=2 makes camo treat it as a new image and fetch fresh. Bump the
number again if this ever recurs.

Verified before changing anything: pypi.org/pypi/quantui/json returns 200 with
version 0.5.1, and img.shields.io/pypi/v/quantui.json already reported
"v0.5.1" — the origin was correct the whole time.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
User report: "Fix dark mode contrasts and add borders (current approach does
not work)."

Diagnosed by measurement rather than assumption, and the measurement moved the
target. Dark mode is not a palette - it is a whole-page CSS
`filter: invert(1) hue-rotate(180deg)`, so every colour is authored once for
light mode and dark mode is its mathematical inversion. Computing WCAG ratios
through that actual filter chain (invert, then the W3C hue-rotate matrix):

  - Text was never broken. Body text on a panel is 7.24:1 in light and
    *improves* to 8.91:1 inverted.
  - Structural separation was. Border-vs-panel measured 1.14:1 in dark and
    panel-vs-page 1.03:1 - panels and their edges were invisible against the
    dark page. That is why the same request asked for contrast AND borders in
    one breath: they were one defect.

The fix follows from a property of the filter: a light grey border inverts to
a near-black border on a near-black panel, and a dark border has the mirror
problem in light mode. Only MID-TONES survive inversion. #7d8ea3 is the
lightest value clearing WCAG 1.4.11's 3:1 bar for non-text UI components in
both modes (3.20:1 light / 4.30:1 dark, from 1.18 / 1.14). The viewer frame,
which stands alone rather than in a card stack, uses a stronger #64748b.

New quantui/theme.py holds the tokens plus the measurements and the reasoning,
so the next person does not have to re-derive why a border is that specific
grey. 19 in-app border sites migrated to it.

Deliberately NOT done, to keep this reviewable and honest about scope:

  - Not a full palette migration. ~390 hex literals exist across 17 files,
    but most are semantic accents (error red, success green, link blue) whose
    hue survives hue-rotate(180) and which already look right in both modes.
    Migrating them wholesale would be a large, visually-unverifiable change
    for no user-visible gain.
  - analytics.py untouched. It writes a STANDALONE dashboard HTML opened
    directly in a browser, where the app's invert filter never applies - its
    light borders are correct as they are. Changing them "for consistency"
    would have been wrong.
  - Plot grid/bond colours untouched; those are THEME.2/.3 and some are
    already theme-aware.

Trade-off worth knowing: under a global invert there is exactly one source
value per colour, so light and dark cannot be tuned independently. Light-mode
borders are therefore now more visible than before (3.20:1, previously
1.18:1). That reads as normal design weight and is the price of dark mode
working at all. Decoupling them requires removing the invert filter, which is
THEME.6's problem - the tokens are the seam where that change will land.

14 new tests re-derive the filter maths and assert both tokens clear 3:1 in
both modes, so a future "soften those borders" tweak fails loudly instead of
silently restoring the bug.

Suite 1985 passed / 17 skipped. pre-commit clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.

1 participant