Skip to content

Dark-mode contrast pass + themed monochrome emoji & UI polish - #31

Open
ZubairQazi wants to merge 16 commits into
mainfrom
fix/dark-mode-contrast
Open

ZubairQazi wants to merge 16 commits into
mainfrom
fix/dark-mode-contrast

Conversation

@ZubairQazi

Copy link
Copy Markdown
Collaborator

Summary

Makes the app render correctly in both light and dark themes, plus a round of UI polish. Builds on the earlier AA-contrast palette work and adds a consistent, theme-aware treatment for emoji, banners, logos, and accent fills.

Changes

Contrast / theming foundation (earlier commits)

  • AA-contrast accent palette + tint/text ramps; converted hardcoded palette hex literals to themed var() tokens; lint rule rejecting hardcoded palette colors.

This round of polish

  • Themed monochrome emoji everywhere — self-hosted Noto Emoji, aliased onto every text family so emoji resolve to the monochrome face even under inline font-family; a mono() helper forces text presentation. Glyphs inherit the themed color and flip with light/dark.
  • Brighter dashboard module banners — decoupled --c-*-fill tokens (vivid in light, pastel in dark) so banners aren't muddy, while accent text stays AA-dark.
  • Legible module-card subtitles (Caveat sizing/contrast).
  • Dark-mode logo<Logo> + useResolvedTheme() swap to the white logo when data-theme is dark.
  • Year emojis tinted with the year's accent color.
  • Fix: essay feedback button + AI badge had lost their background/border — ${MC}xx hex-alpha concatenation became invalid once MODULE_COLORS were var(); routed through withAlpha().
  • Brighter non-text accent fills across modulesfillOf() maps an accent to its vivid fill twin, applied to progress bars, the Aid Engine gauge/dots, submission-pipeline bar, stat-tile stripes, and hero boxes. Buttons/text/count-numbers stay AA-dark (white-text contrast).

Accessibility note

Non-text surfaces (bars, dots, gauges, icons ≥3:1) use vivid fills; accent-colored text and white-on-accent buttons keep the darker AA value (≥4.5:1). Verified with measured contrast.

Verification

  • tsc ✓, eslint ✓, 212/212 tests ✓.
  • Verified in both themes via rendered screenshots.

The accent palette and the muted/faint text ramps were below WCAG AA in
*both* themes, not just dark. Measured across 8 views: 678 sub-AA text
elements in dark, 569 in light, with near-identical counts for the shared
values (148x alpha 0.5, 95x alpha 0.45, 49x #1D7FC4 ...).

- --text-muted 0.45 -> 0.70 light / 0.66 dark, --text-faint 0.22 -> 0.60 /
  0.55. The old values measured 2.9:1 and 1.7:1; the small uppercase labels
  on --text-faint were the least readable text in the app.
- Accents gain -rgb channel twins and paired --tint-* washes, with
  per-theme values. The required correction differs per hue (purple and red
  needed +33% white in dark, green and amber +5-6%), so one blanket lighten
  would have washed out two of them for nothing.
- Dark tints are the accent hue sunk into --bg. The light pastels were being
  reused unchanged in dark mode, which is why the two Discover banner cards
  rendered cream-on-near-white at 1.01:1 -- fully invisible.
- designTokens.ts referenced its own alphas (0.50/0.25/0.10/0.18) that had
  drifted from index.css (0.45/0.22/0.08/0.12), so one token rendered two
  different colors depending on whether a component used CSS or an inline
  style. It now points at the vars.

Call-site conversion follows; `${MC}12`-style hex-alpha concatenation breaks
against a var() and is handled next.
Transparency was applied by concatenating a hex alpha suffix -- `${color}18`
or `color + '18'`. That only works while `color` is a 6-digit hex. Now that
the accents are var() references it yields `var(--c-jun)18`, which is not a
valid color, so the browser drops the whole declaration and the border or
wash silently disappears. Nothing throws and nothing logs, which is what made
this worth centralising rather than fixing case by case.

withAlpha() resolves a themed token through its -rgb channel twin and still
handles the raw hex neutrals that remain (e.g. APP_STATUS_META's #7A6D5C), so
call sites don't need to know which kind of color they were handed.
166 hardcoded accent and tint literals across 23 files become var(--c-*) /
var(--tint-*), so they follow the theme instead of rendering light-mode
colors under [data-theme="dark"].

The pastel tints mattered most: CollegeDiscoverTab's two banner cards passed
#EDEAF7 / #EBF5F0 as a `tint` prop, which stayed near-white in dark mode
while the text went cream -- both cards measured 1.01:1 and were effectively
invisible.

Also fixes the concatenation sites this breaks (`${color}18` against a var()
yields an invalid color the browser drops), and two pre-existing instances of
the same bug in contentBlocks.tsx and ChecklistContentView.tsx, where
SUCCESS_GREEN and MODULE_COLORS were already var() references.

data/timelineData.ts's accentDim held the same palette pre-expanded as decimal
channels, so it had already drifted from its `accent` twin; it now shares the
-rgb vars.

DeadlineEvent gains a colorRgb companion, since consumers (Dashboard, Calendar,
Timeline) need the accent at partial opacity and can no longer get there by
string concatenation.
Kept separate because it is the single biggest offender -- 71 hex literals,
12 eight-digit hex-alpha literals and 43 concatenation sites, more than any
other file -- and it is not otherwise touched by this branch, so a reviewer
can read the other 23 files without it in the way.

The MC pattern (`${MC}40`, `MC + '50'`) appears 43 times here. MC is
MODULE_COLORS.financialAid, now var(--c-fresh), so every one of those was
already emitting an invalid color and dropping its declaration. They go
through withAlpha().

The status/urgency metadata maps pair a `color` with a `bg`; both sides moved
together (accent -> var(--c-x), its pastel -> var(--tint-x)) so the pairings
survive. One-off colors with no mapped token (#B26A00, #8A8F98, #1F7A54,
#FFF3E0) are left as literals.
The migration only holds if a new hardcoded hex fails CI. Otherwise the next
component written in light mode reintroduces the bug and nobody notices until
someone opens dark mode -- which is exactly how this branch's worst case
happened, three files written after dark mode landed that all hardcoded
light-mode literals anyway.

Covers plain literals and template-literal text, since the colors are written
both ways. Verified with a probe file that all three forms error and that
var(--c-jun) does not, then confirmed `eslint src` is clean -- so a green
lint is now evidence the conversion is complete rather than an assumption.

Does not cover `color + '18'` concatenation: the literal '18' is not
distinguishable from ordinary string math, and a false positive in CI is
worse than the grep. withAlpha() is the documented alternative.
Re-running the audit after the conversion surfaced four things the first pass
missed. Dark 110 -> 20 remaining, light 142 -> 20.

- Accents were derived against the bare surfaces, but chips draw them on
  rgba(accent, 0.15) over --elevated, which is lighter. That left them at
  4.47:1 -- passing on paper, failing where they are actually drawn. Re-derived
  against the washed backdrop too.
- --on-accent: text sitting ON an accent has to invert with the theme, because
  the accents do. Hardcoded white measures 6.6:1 in light and 2.4:1 in dark;
  ink is the reverse. Applied to the 7 rules that put white on an accent fill.
- --glass-rgb: frosted surfaces were literal rgba(255,255,255,a) and stayed
  white in dark mode. The "you are here" badge was cream text on near-white at
  1.01:1. The white-on-accent highlights (.tour-intro-btn, .st-toggle-knob)
  are deliberately left as literal white -- they sit on a colored fill.
- index.css still held 18 palette usages the ESLint rule cannot see, since it
  only lints ts/tsx. Those were the light-mode "Open ->" links at 3.0:1.

Also fixes .auth-sso-btn--apple, which set color:#fff on background:var(--text)
-- cream on cream in dark mode. Neither audit could catch it: both start after
login, so the auth screen is never measured.

The 20 remaining flags in each theme are all .dash-module-emoji. Those are
false positives: color emoji paint from the emoji font and ignore the CSS
`color` property, so comparing that color to the background measures nothing.
# Conflicts:
#	web/src/components/CalendarPage.tsx
#	web/src/components/EssaysModule.tsx
#	web/src/components/TimelinePage.tsx
#	web/src/data/applicationDeadlines.ts
Self-host Noto Emoji and alias it onto every text family with an
emoji-only unicode-range, so emoji resolve to the monochrome face even
where a component sets font-family inline. A mono() helper strips U+FE0F
and appends U+FE0E to force text presentation. Every emoji render site is
wrapped in mono(); glyphs now inherit the themed text colour and flip with
light/dark instead of showing the glossy system colour emoji.
Add --c-*-fill tokens (vivid in light, lighter pastels in dark) and point
the module-card banners at them. The accent tokens stay AA-dark for text;
the fills are only used behind the white on-accent glyphs, so the banners
read lively again instead of the muddy dark tone the contrast pass left.
The Caveat subtitle rendered near-illegible at 14px in muted grey (low
x-height). Bump to 18px/600 in a stronger ink — keeps the handwriting
motif while making the tagline readable.
The colour logo's dark quill vanished against the dark background. Add a
<Logo> component + useResolvedTheme() hook that swaps to logo-white.png
when data-theme is dark, and route every logo <img> through it.
The year glyph (Freshman leaf, etc.) rendered as a neutral monochrome
mark. Colour it with the year's themed accent so it flips per theme.
This branch converted MODULE_COLORS from hex to var() references, which
turned the leftover `${MC}40`/`${MC}08`/`${MC}12` hex-alpha concatenations
into `var(--c-sen)40` — invalid CSS the browser silently drops, so the
feedback button's border/background and the AI badge wash vanished. Route
them through withAlpha().
The In-progress/Submitted/Decisions bar filled with the AA-dark accents,
reading muddy in light mode. Add --c-fresh-fill and give the bar segments
and legend dots the vivid fill tokens; the count numbers keep the AA-dark
accent since they are text.
Add fillOf(), mapping a themed accent to its vivid --c-*-fill twin, and use
it on every non-text fill: the shared Bar (so all module progress bars
brighten at once), the Financial Aid gauge + dots, the Application Tracking
stat-tile stripe, and the Knowledge Library hero. Buttons, colored text and
count numbers keep the AA-dark accent for contrast.
@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying timeline-prototype with  Cloudflare Pages  Cloudflare Pages

Latest commit: f4d29d8
Status: ✅  Deploy successful!
Preview URL: https://299f7c16.timeline-prototype.pages.dev
Branch Preview URL: https://fix-dark-mode-contrast.timeline-prototype.pages.dev

View logs

This branch has not been deployed

No deployments
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