Skip to content

feat(theme): reland dark mode and the page redesign on 11.0.x - #280

Merged
Clupai8o0 merged 312 commits into
11.0.xfrom
theme/redesign-reland
Sep 27, 2026
Merged

Clupai8o0 merged 312 commits into
11.0.xfrom
theme/redesign-reland

Conversation

@Clupai8o0

Copy link
Copy Markdown

What this is

This relands #192, dark mode and the redesign of the student and staff pages, on current 11.0.x. #192 closed unmerged on 19 Sep when PR intake closed, so none of it reached 11.0.x or the upstream PRs to thoth-tech. Without it the pages added since then have no dark theme. Course Flow for example shows dark text on white cards in dark mode.

How it was built

Dark mode for the features added after #192

  • Course Flow, previous submissions and the PWA install dialog used var(--mat-sys-*, <light colour>). Nothing in the app defines --mat-sys-*, so the light fallback always won. They use the --ot-color-* tokens now.
  • Also themed: the PPI widget and preview, student onboarding, upload requirements, the target grade chart, the TII action log, Unit Hub and study essentials, and the audio recorder.
  • Dark --ot-color-border is #798698, so borders clear 3:1 on raised cards too (3.01, the old value gave 2.87).

Fixes from the merge

  • Project dashboard: the desktop task card gets its deadline line back.
  • Home: the progress bars keep 11.0.x's accessible names, and the unit links keep them with the visible unit code in front, so the name starts with what's on screen.
  • Comments panel: it closes full screen on Escape, so it's now a labelled region that can hold focus, as the new interactive-supports-focus lint rule wants.
  • Task status card: it no longer throws when a task has no status class.
  • Staff editor: the mentor select can pick No mentor again (canSelectNullableOptions), with a spec that fails without it.
  • Web calendar: the switch listens for toggleChange again, as on 11.0.x, not for any click.
  • Onboarding, Course Flow drag preview and the PPI widget take their shadows from the scrim token, so they show on dark surfaces.

Behaviour changes to know about

  • The comment composer stages files and posts them on Send. That came with Maple's reviewed mobile work (86cf018b2), so the per-file confirm dialog from 11.0.x isn't used any more. docs/safe-upload-and-chat-guide.md and docs/FILE-A01-safe-upload-policy-matrix.md still describe the confirm step and need updating.
  • DemoDataMaskInterceptor is gone. Maple's demo scenario registry (86cf018b2, reconciled in 18b54cdaf) replaces it.

Built against

  • web: theme/redesign-reland @ cab60d1d3, on 11.0.x @ df118f868
  • api: 11.0.x @ 5de649d75
  • deploy: 11.0.x @ 1ca0413b5

Scope

715 files, +54,224 / -17,232 against 11.0.x, and 263 non-merge commits ahead of it. Nearly all of that is #192 as it was reviewed. The merge itself and the fixes after it are the last 7 commits.

Testing

On Node 22 at cab60d1d3:

  • npm run test:ci: exit 0, 272 files, 2125 tests passed
  • npm run lint: exit 0
  • npm run typecheck: exit 0
  • npx ng build: exit 0

I tested every page in light and dark myself before #192 closed. My 14 Sep comment on #192 said the light and dark checks were still to do. That was wrong, sorry for the mix up.

On this head I went through it again on the local demo stack and took screenshots in both themes: 18 student and staff pages at desktop width, 6 at tablet width (820px), 9 at phone width (390px), and home, dashboard, Students and the inbox at 320px. They're in the first comment below. That covers the phone, tablet and desktop light and dark evidence Maple asked for on #192.

Known open items

  • The installed PWA checks (keyboard, audio, resume) and the provider delivery checks from Maple's 14 Sep review still need a real device.
  • The staff Students list shows no names on 11.0.x (N/A there, "Unnamed student" here). It happens without this branch too, so I've left it for its own fix.

Relationship to other PRs

Clupai8o0 and others added 30 commits September 10, 2026 07:07
…agement passport

Register the theme tokens as Tailwind colours (bg-ot-surface, text-ot-muted,
border-ot-divider, ...) so literal colour utilities can be swapped for token-backed
ones that flip with data-ot-theme, no dark: variant needed. First use: the
engagement passport card that showed as a white slab on the dark dashboard.
…me tokens

Complete the per-screen dark-mode migration (THM-M02 / THM-M04) and the browser
chrome (THM-W01):

- ~600 hard-coded colour swaps across ~90 screen files: student dashboards, task
  and comment views, unit editors, modals, viewers and calendars now use the
  bg-ot-*/text-ot-*/border-ot-* Tailwind utilities and var(--ot-*) SCSS tokens, so
  they flip with data-ot-theme.
- ThemeColorService resolves --ot-* tokens to concrete colours for ngx-charts and
  other JS that cannot consume a CSS var. The status pie, task-status cards and
  burndown series read status/chart tokens; the pie re-renders on a live theme flip.
- THM-W01: the browser/PWA address-bar colour follows the resolved theme.

Web suite 800 pass 0 fail, lint clean, build green on Node 22.
…olish

Re-base dark mode on a GitHub-blue ground and finish dark-mode coverage
and polish across the app:

- Dark tokens re-based on a GitHub-blue ramp; softened light-mode whites
- Vivid, AA-verified status colours; the task-status card wraps in its
  status colour (solid header, tinted body, coloured border); the status
  header icon is enlarged with tighter padding
- Outlined buttons given a clear control fill and border so they read as
  buttons in both themes
- ngx-gantt Task Planner themed dark by overriding --gantt-color-* on the
  <ngx-gantt-root> host (its inline light defaults beat a :root rule)
- Reusable f-empty-state component; empty states added to 21 tables and
  the timeout/unauthorised pages centred and tokenised
- Portfolio wizard fills the viewport with tightened spacing; body fills
  the viewport so short pages no longer show a page-colour seam
- Global keyboard :focus-visible ring and prefers-reduced-motion support
- Even, comfortable header icon-button and avatar hover targets
- Remaining screens/charts migrated to the --ot-* theme tokens
The profile form's main container used justify-center on a full-width flex row.
When the content was wider than a phone, centring pushed the overflow off BOTH
edges, so headings clipped on the left ('otification Settings') and fields bled
off the right. Switch to justify-start and let the First/Second Name pair stack
below the small breakpoint. Verified at 390px in both themes: no clipping, no
horizontal page scroll.
The task Details sub-tabs centred their overflowing strip, which clipped the
active first and last labels at the viewport edges ('ask Details' /
'Submissic'). Left-align the strip and drop the auto margins so the active tab
stays whole and the rest scrolls into view. Verified at 390px in both themes.
The unit switcher menu was pinned to a hard 400px, wider than a phone viewport;
cap it to calc(100vw - 16px). And the global 2px outward focus ring was clipped
by menu/option panel edges into two harsh bars on the focused row; inset it for
menu items and options so it frames the row. Verified at 390px in both themes.
The global keyboard focus ring uses :is(a[href], ...):focus-visible, which lands
at (0,2,1) specificity because of the a[href] arm, so the earlier menu-item inset
rule (0,2,0) never won and the ring stayed a clipped pair of bars. Scope the inset
under the panel ancestors to clear it. Verified under keyboard focus in dark: the
ring now frames the focused row.
…stence

THM-F01. Adds the --ot-* design-token layer and the ThemeService that
resolves and persists the user's theme choice.

- Token layer: src/styles/tokens/_light.scss and _dark.scss declare every
  --ot-* token, light as the bare :root default and dark under
  [data-ot-theme=dark]. These two files are the only place a colour value is
  declared. Wired into styles.scss.
- ThemeService: preference (light/dark/system) vs resolved (light/dark),
  system detection via matchMedia, persistence in localStorage under the
  ontrack.theme.preference key, writes the data-ot-theme marker and
  colorScheme. Junk or blocked storage falls back to system, never a hard
  dark. One matchMedia listener, removed on destroy.
- Proof page at /theme-demo demonstrating the section-13 component-state
  matrix and the token set, repainting in both themes with no reload.

Out of scope by contract: the accessible toggle (THM-F02), the Tailwind dark
variant (THM-F03), the no-flash <head> script and its drift test (THM-F04),
and the theme-color browser chrome (THM-W01).

Built against doubtfire-web 11.0.x @ 0ba9fd7. 17 unit tests pass, ng build
clean, eslint --max-warnings 0 clean.
Build the app shell and shared components on top of the THM-F01 token
foundation so they follow the resolved-theme marker, with no visual change
in light mode.

- Dark Angular Material M2 theme keyed to :root[data-ot-theme='dark'],
  built from a desaturated indigo palette (500 = #5457e5) so filled
  buttons, the fab and progress bars read as one accent with white labels
  (white measures 5.4:1). Repoint card, menu, dialog and select container
  colours onto the raised surface token via the --mat-* variables.
- Rework the dark token ramp to a cohesive cool-slate palette with a single
  indigo accent (was warm greys sitting too close together).
- Bind Tailwind's dark: variant to the marker (THM-F03).
- Add a single-button theme toggle that flips light and dark from the
  resolved theme (THM-F02), registered in the module and placed in the
  header; eagerly inject ThemeService in AppComponent so the marker is
  stamped on every device, not only where the toggle renders.
- Migrate hardcoded colours to tokens in the header, home dashboard, the
  f-chip shared component, the edit-profile form, the hero sidebar, and the
  two places that set the body background inline in JS. Each token resolves
  to the original light value and carries a legacy var() fallback.

Tests: theme service 17, toggle 2, app/f-chip/header/edit-profile updated
and green. Lint clean at --max-warnings 0.
manifest background_color was #ffffff, which the OS paints as the launch
splash of an installed PWA before any app code runs. A dark-mode user got
a full-screen white flash on every launch.

The manifest is static and read at install time, so it cannot follow the
theme. Set background_color to the brand indigo #3939ff, which already
matches theme_color, so the splash is a solid brand panel behind the icon
in both light and dark. theme_color stays #3939ff.
Map the server theme_preference field onto the User model and let the
account value win on sign-in.

- user.ts: add themePreference ('light' | 'dark' | 'system'), registered
  in the UserService mapping so it maps from theme_preference.
- ThemeService.applyServerPreference: the server value wins on sign-in and
  is written straight back to local storage so the no-flash pre-boot script
  agrees on the next load. Gated by isThemePreference, so a missing or junk
  value is ignored and the local choice stands.
- Wired in setupUserFromResponse once currentUser is set, on both fresh
  sign-in and the refresh-token path.

Resolution order: server wins on sign-in and persists locally; sign-out
leaves the stored value in place, not light; and the matchMedia handler
only ever repaints, so an OS appearance flip while System is selected
never writes anywhere. Only setPreference, a user action, writes.
Swap the hard-coded colours the M01 shell work left behind to the --ot-*
tokens so the shell and global list surfaces flip in dark mode:

- notification dropdown message, time, stale and empty text, the unread
  dot, and the bell badge cut-out ring
- task-dropdown breadcrumb icons (were hard black on the dark toolbar)
- footer action bar and its status-chip resting and disabled states
- the global selectable-table row hover (.f-table and .table-selectable)
- the group-selector row hover

Footer bar uses --ot-color-page so it keeps contrast with the white status
buttons in light mode. Tokens verified present in both palettes. Web suite
800 pass 0 fail, lint clean, build green on Node 22.
…tokens

Retarget the runtime-themed surfaces the shell PR did not cover so they flip
with data-ot-theme:

- status-icon chips read var(--ot-status-<key>) and the AA-fixed
  var(--ot-status-<key>-on) instead of the compile-time light-only map, so
  every task-status chip across the app themes correctly
- footer status chips drive --status-chip-bg from the same token
- grade/quality task-list badges invert against the list surface
  (--ot-color-inverse-surface/-inverse-text) instead of a black circle that
  vanished on the dark list
- custom scrollbars and the global progress spinner use border/divider tokens

Note: adopting the contract's -on foregrounds shifts a few status labels in
LIGHT mode too (e.g. working-on-it goes to dark text on amber), which is the
contract's WCAG AA choice, not a regression. The status colour used inside the
d3/canvas charts still reads the light-only TS map and is a separate follow-up.
Build, lint and web suite (800 pass, 0 fail) green on Node 22.
…agement passport

Register the theme tokens as Tailwind colours (bg-ot-surface, text-ot-muted,
border-ot-divider, ...) so literal colour utilities can be swapped for token-backed
ones that flip with data-ot-theme, no dark: variant needed. First use: the
engagement passport card that showed as a white slab on the dark dashboard.
…me tokens

Complete the per-screen dark-mode migration (THM-M02 / THM-M04) and the browser
chrome (THM-W01):

- ~600 hard-coded colour swaps across ~90 screen files: student dashboards, task
  and comment views, unit editors, modals, viewers and calendars now use the
  bg-ot-*/text-ot-*/border-ot-* Tailwind utilities and var(--ot-*) SCSS tokens, so
  they flip with data-ot-theme.
- ThemeColorService resolves --ot-* tokens to concrete colours for ngx-charts and
  other JS that cannot consume a CSS var. The status pie, task-status cards and
  burndown series read status/chart tokens; the pie re-renders on a live theme flip.
- THM-W01: the browser/PWA address-bar colour follows the resolved theme.

Web suite 800 pass 0 fail, lint clean, build green on Node 22.
…olish

Re-base dark mode on a GitHub-blue ground and finish dark-mode coverage
and polish across the app:

- Dark tokens re-based on a GitHub-blue ramp; softened light-mode whites
- Vivid, AA-verified status colours; the task-status card wraps in its
  status colour (solid header, tinted body, coloured border); the status
  header icon is enlarged with tighter padding
- Outlined buttons given a clear control fill and border so they read as
  buttons in both themes
- ngx-gantt Task Planner themed dark by overriding --gantt-color-* on the
  <ngx-gantt-root> host (its inline light defaults beat a :root rule)
- Reusable f-empty-state component; empty states added to 21 tables and
  the timeout/unauthorised pages centred and tokenised
- Portfolio wizard fills the viewport with tightened spacing; body fills
  the viewport so short pages no longer show a page-colour seam
- Global keyboard :focus-visible ring and prefers-reduced-motion support
- Even, comfortable header icon-button and avatar hover targets
- Remaining screens/charts migrated to the --ot-* theme tokens
The GitHub-dark ground (#0d1117 page) read as near-black and too harsh. Lift the
whole neutral ramp to a softer slate — page #21262d, surface #2b313b, raised
#353c47 — with matching border/divider, a softened text #dbe3ea and a lighter
muted #a4afbd and link #6cb2ff. Accents, status fills and their -on inks are
unchanged. Contrast re-checked: body text 8.6-11.7:1, muted and link 5.0:1 on
the lightest surface, primary fill 4.63:1 — all clear WCAG AA. Elevation still
reads by lightness (page->surface->raised).
In dark, --ot-elevation-1 is near invisible on the #21262d ground, so cards,
dialogs, menus and select panels separated only by an ~18-point lightness step
read flat against the page. Add a hairline --ot-color-border (#464f5b) edge to
each so they lift the way a drop shadow does in light. The outlined-card outline
was pointed at --ot-color-divider, which equals the raised surface in dark
(#353c47) and drew nothing; repointed to the border token. The Web calendar
dialog and home unit cards now have a defined boundary.
…urface distinction

Introduces a theme-independent radius scale (styles/tokens/_shape.scss:
--ot-radius-xs/sm/md/lg/pill/circle) and the matching Tailwind rounded-ot-*
utilities, then collapses the ~20 ad-hoc radii that had accreted across the tree
onto it. Cards/panels -> md (12px), controls/inputs -> sm (8px), progress track
-> pill; the intentional asymmetric chat-bubble corners are left as a documented
exception.

  - home: unit cards 15px -> md; footer progress bar rounded to a pill
  - cross-dashboard: unit column 8px -> md, five hand-rolled inputs/buttons -> sm
  - calendar modal: two nested panels -> md
  - header: notifications dropdown 16px -> md; unit switcher pinned to md so the
    two adjacent menus match
  - shared: .f-table 0.8rem -> md; unit-task-list row 6px -> sm; drop-zone -> sm

Dark surface distinction, the plain (non-Material) surfaces the central card rule
in theme.scss does not reach:
  - cross-dashboard unit column gets a dark-only border-ot-border edge
  - calendar tab panel switched from the divider token (equals the raised surface
    in dark, so invisible) to the border token
  - profile settings separator uses the border token in dark, divider in light
  - .f-table gets the dark hairline edge
  - due-date chips gain dark variants so they stop reading as light pastels on the
    dark ground
…contrast

- home: the enrolled-units action buttons were in an h-60 justify-around column
  so View all / View previous floated far apart; stack them top-aligned with a
  gap. Unit-code subtitle pinned to the muted text token (Material's default read
  too faint for AA).
- calendar modal: size the dialog to its content (the open config passed the
  Tailwind class 'h-min' as a CSS height, which Material dropped, leaving the
  disabled state a tall near-empty box). Only fill+scroll when enabled. Close
  button dropped its warn (red) colour — it is a neutral dismiss.
- confirmation modal: Cancel dropped its warn colour (red reads as destructive)
  and is a neutral stroked button beside the primary Confirm.
- dialogs follow the 12px surface radius (Material default 4px was too tight).
- inactive tab labels pinned to the muted text token so the For Google/Apple/
  Outlook strip clears AA in dark.
The unit card clipped its own content: a fixed max-height 200px with overflow
hidden, plus a title that set text-overflow:ellipsis alongside white-space:wrap
(so it never truncated, it wrapped to 3-4 lines). A long unit name pushed the term
chip and progress bar out of the card. Clamp the title to two lines with a real
ellipsis and drop the hard max-height; the title's min-height keeps every card a
uniform height. Also remove the mid-card divider under the unit code, which read as
an empty text input.

calendar modal: the reminder Save action was a bare icon button that did not read
as a button; make it a labelled mat-flat-button (Save) with a stroked Cancel.
The active tab used the mid primary blue, which read dim on the dark panel. Point
the active label and indicator at the brighter link token so the selected tab pops
in both themes.
The Material dark paginator kept a warm-grey container that read as a stray band
against the slate ground (visible under the View-all units list). Point it at the
page token so it matches the rest of the design.
…right

The earlier attempt set --mat-tab-header-* tokens, which this Material build does
not read (verified: active label stayed the mid primary #1f6feb). Switch to the
--mat-tab-{active,inactive}-label-text-color tokens it actually reads, and because
the theme mixin re-sets the active token inside the dark scope at higher
specificity, force the active label and ink bar to the link token directly. Active
tab now computes #6cb2ff.
The unit column header used a full-saturation accent band (bg-ot-primary /
green-600). Against the slate ground in dark it read as a loud block, heavier
than the task content below it. Route the header colour through a class and,
in dark, sit it on a deep low-brightness tint of the accent (mixed 55% toward
the page token) so the header belongs to the slate family instead of shouting
over it. White labels still clear AA (~7.7:1 blue, ~6.5:1 green). Light keeps
the saturated band, which reads fine on the near-white page.
The comments panel closes full screen on Escape, so it is now a labelled
region that can hold focus, as the 11.0.x interactive-supports-focus rule
requires.
The redesigned cards named each title link by its code alone. The link
now reads the code, the unit name and the role, and the progress bars
name the unit again, as the 11.0.x accessibility work had them.
…he scrim token

These shadows were fixed black at 18 to 28%, which barely shows on the dark
surfaces. The scrim token is darker in dark mode, the same way the project
dashboard panels already use it. The onboarding backdrop stays light because
the tour highlights page elements through it.
The merge dropped canSelectNullableOptions from the mentor select, so No
mentor could not be picked and a mentor could not be cleared. It also put
the web calendar switch back on (click), which fires for clicks that do not
toggle. Both now match 11.0.x, and the staff editor spec checks the first.
@Clupai8o0
Clupai8o0 merged commit 192078b into 11.0.x Sep 27, 2026
13 of 14 checks passed
Clupai8o0 pushed a commit that referenced this pull request Sep 27, 2026
Brings the dark mode page redesign (#280, which relands
#192)
to the files this PR already carries, so every file stays in exactly one PR.

Co-authored-by: Maple Fox <s223932052@deakin.edu.au>
Clupai8o0 pushed a commit that referenced this pull request Sep 27, 2026
Brings the dark mode page redesign (#280, which relands
#192)
to the files this PR already carries, so every file stays in exactly one PR.

Co-authored-by: Davie <W196n8y@gmail.com>
Co-authored-by: Maple Fox <s223932052@deakin.edu.au>
Clupai8o0 pushed a commit that referenced this pull request Sep 27, 2026
Brings the dark mode page redesign (#280, which relands
#192)
to the files this PR already carries, so every file stays in exactly one PR.

Co-authored-by: Maple Fox <s223932052@deakin.edu.au>
Clupai8o0 pushed a commit that referenced this pull request Sep 27, 2026
Brings the dark mode page redesign (#280, which relands
#192)
to the files this PR already carries, so every file stays in exactly one PR.

Co-authored-by: Maple Fox <s223932052@deakin.edu.au>
Clupai8o0 pushed a commit that referenced this pull request Sep 27, 2026
Brings the dark mode page redesign (#280, which relands
#192)
to the files this PR already carries, so every file stays in exactly one PR.

Co-authored-by: Maple Fox <s223932052@deakin.edu.au>
Clupai8o0 pushed a commit that referenced this pull request Sep 27, 2026
Brings the dark mode page redesign (#280, which relands
#192)
to the files this PR already carries, so every file stays in exactly one PR.

Co-authored-by: Maple Fox <s223932052@deakin.edu.au>
Clupai8o0 pushed a commit that referenced this pull request Sep 27, 2026
Brings the dark mode page redesign (#280, which relands
#192)
to the files this PR already carries, so every file stays in exactly one PR.

Co-authored-by: Maple Fox <s223932052@deakin.edu.au>
Clupai8o0 pushed a commit that referenced this pull request Sep 27, 2026
Brings the dark mode page redesign (#280, which relands
#192)
to the files this PR already carries, so every file stays in exactly one PR.

Co-authored-by: Maple Fox <s223932052@deakin.edu.au>
Clupai8o0 pushed a commit that referenced this pull request Sep 27, 2026
Brings the dark mode page redesign (#280, which relands
#192)
to the files this PR already carries, so every file stays in exactly one PR.

Co-authored-by: Maple Fox <s223932052@deakin.edu.au>
Clupai8o0 pushed a commit that referenced this pull request Sep 27, 2026
Brings the dark mode page redesign (#280, which relands
#192)
to the files this PR already carries, so every file stays in exactly one PR.
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.

3 participants