Skip to content

feat: add dark mode and consolidate theming onto design tokens - #155

Merged
zeemscript merged 5 commits into
Deen-Bridge:mainfrom
cLamberti:feat/126-dark-mode-theming
Jul 29, 2026
Merged

feat: add dark mode and consolidate theming onto design tokens#155
zeemscript merged 5 commits into
Deen-Bridge:mainfrom
cLamberti:feat/126-dark-mode-theming

Conversation

@cLamberti

@cLamberti cLamberti commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Hi @zeemscript, thanks for the opportunity to contribute and the patience, here's what I did:

Closes #126

What

Wires next-themes end-to-end, adds an accessible theme toggle, makes the three
appearance controls in Account → Settings functional, and migrates hardcoded
colors in the authenticated app to semantic tokens.

The repo already had a full light/dark token palette and next-themes installed,
but nothing rendered a ThemeProvider, so every dark token was dead code.

New

  • components/providers/ThemeProvider.jsxnext-themes wrapper
  • components/providers/AppearanceProvider.jsx — accent colour + font size
  • components/ui/theme-toggle.jsx — light / dark / system, hydration-safe
  • lib/config/appearance.config.js — palettes and the boot script

Changed — 41 files, +203 −215. styles/globals.css loses 74 lines of dead code.

Why these decisions

The accent token was doing two contradictory jobs. --color-accent is a fixed
#265902 serving both as a dark surface (bg-accent + text-white, 89 files) and
as a text colour (text-accent, 154 occurrences). In dark mode the text role
measured 2.38:1 — unreadable.

Making --color-accent theme-aware was not an option: --accent in light is a
near-white grey, and 73 files put text-white on top of it. That change would have
turned ~11:1 into ~1.1:1 and broken light mode, which works today.

So the token was split by role. --color-accent stays a fixed surface — the 89
files using bg-accent are untouched — and a new theme-aware --brand-text takes
over the text role.

--brand-text uses two-level indirection, and this is deliberate:

:root { --brand-text-light: #046b30; --brand-text-dark: #4ade80;
        --brand-text: var(--brand-text-light); }
.dark { --brand-text: var(--brand-text-dark); }

The appearance provider writes only the two bottom values, once. The cascade picks
which one applies. The alternative — a single property rewritten by JS on theme
change — runs after next-themes adds .dark, which is a visible flash of the
brand colour on every toggle.

localStorage stores resolved hex, not the palette name. That keeps the
synchronous boot script trivial: it reads and applies, with no need for the palette
table duplicated inside a serialised string.

Contrast — WCAG AA

Every changed token was measured (oklch → sRGB → WCAG). Both primary pairs failed
in light and dark, not only dark as the issue assumed.

Pair Before After
light primary-foreground / primary 2.12:1 8.95:1
dark primary-foreground / primary 1.01:1 9.53:1
light sidebar-primary-foreground / sidebar-primary 2.12:1 8.95:1
dark sidebar-primary-foreground / sidebar-primary 3.68:1 9.53:1
brand-text light on background did not exist 6.66:1
brand-text dark on background did not exist 11.42:1

.dark --primary was oklch(0.23 0.77 120) — chroma far outside sRGB gamut,
clamping to #005400 against a #0d542b foreground.

All five accent palettes pass: white on surface 8.36:1–12.54:1, text-light
6.47:1–8.72:1, text-dark 10.48:1–11.93:1.

Testing

Verified against a local mock API with each screen loaded fresh in each theme,
auditing the effective foreground/background contrast of every text element
(compositing real backgrounds up the ancestor chain).

Route Elements Light Dark
/dashboard 81 0 0
/dashboard/courses 24 0 0
/dashboard/courses/[id] 38 0 0
/dashboard/library 20 0 0
/dashboard/library/[id] 42 0 0
/dashboard/search/[q] 18 0 0
/account/settings 33 0 0

The audit caught four defects that review alone would have missed, including one
introduced by this branch (a badge on bg-accent inheriting the new near-black
primary-foreground, 2.38:1).

Appearance controls were exercised with real clicks: selecting purple + large wrote
the three CSS variables and font-size: 18px, persisted across navigation and
reload, and applied before React on boot. With purple active, switching to dark
resolves --brand-text to #d8b4fe purely through the cascade.

npm run lint clean. npm run build succeeds.

Not verified

  • /account/wallet throws a client-side exception in this environment. Not
    caused by this branch — the file is untouched and the page depends on
    friendbot.stellar.org and stellar.expert, which were unreachable. Needs a
    pass with network access.
  • Screenshots pending.

Notes for the maintainer

The issue lists BookStats&Info.jsx as an offender; it is not. It uses
bg-accent (#265902) with white on top — 8.36:1, and unaffected by theme
because the token is fixed. Its surfaces were left alone.

components/molecules/Modal.js was changed although it is outside the files the
issue names.
It had a fixed bg-white while its text inherits text-foreground,
which turns near-white in dark — white on white, reachable from the nav on every
screen. One-line fix.

Font size has a known limitation, and it is stated in the UI. The control scales
the root font size, but the repo has 47 arbitrary px values that do not scale,
four of them text-[11px]. Converting those to rem is follow-up.

Follow-up, measured: 29 files / 68 occurrences of text-accent remain outside
the seven target screens (landing, about, blog, contact), at 2.38:1 in dark. Also
text-highlight (15), text-basic (1), text-secondary (8), and the ghost /
outline Button variants, whose hover:bg-accent hover:text-accent-foreground
yields ~1.6:1 in light. All are instances of the same structural limit: a fixed
colour cannot be readable text on both a light and a dark background.

Open question. Is the brand green identity or user preference? The accent
selector was already in the UI promising the latter, and this PR makes it work on
that premise. If the intent was the former, the control should be removed rather
than implemented — that is a product call, not a technical one.

**Screenshots:

image image image image image image image image image image

Summary by CodeRabbit

  • New Features

    • Added light, dark, and system theme selection from the main dashboard header.
    • Added appearance settings for accent colors and font sizes, with preferences retained between visits.
    • Improved theme initialization to reduce visual changes during page loading.
  • Style

    • Updated dashboard cards, badges, icons, forms, alerts, and typography to use theme-aware colors.
    • Improved visual consistency across account, course, library, search, and dashboard screens.
    • Enhanced light and dark mode color contrast.
  • Documentation

    • Added contribution guidance for theme-safe styling and semantic design tokens.

Wire next-themes with a ThemeProvider and a hydration-safe theme toggle,
make the three appearance controls in Account > Settings functional, and
migrate hardcoded colors in the authenticated app to semantic tokens.
Split the overloaded accent token: --color-accent stays a fixed dark
surface for bg-accent, and a new theme-aware --brand-text takes over the
text role, which measured 2.38:1 on dark backgrounds. The text token uses
two-level indirection so the cascade resolves it per theme and switching
themes never repaints from JS.
Fix --primary and --sidebar-primary, both of which failed WCAG AA in light
and dark, and drop the dead HSL token blocks that were shadowed by the
unlayered oklch definitions.
Verified by auditing effective foreground/background contrast across every
text element on the seven target screens in both themes.
Closes Deen-Bridge#126
Wire next-themes with a ThemeProvider and a hydration-safe theme toggle,
make the three appearance controls in Account > Settings functional, and
migrate hardcoded colors in the authenticated app to semantic tokens.
Split the overloaded accent token: --color-accent stays a fixed dark
surface for bg-accent, and a new theme-aware --brand-text takes over the
text role, which measured 2.38:1 on dark backgrounds. The text token uses
two-level indirection so the cascade resolves it per theme and switching
themes never repaints from JS.
Fix --primary and --sidebar-primary, both of which failed WCAG AA in light
and dark, and drop the dead HSL token blocks that were shadowed by the
unlayered oklch definitions.
Verified by auditing effective foreground/background contrast across every
text element on the seven target screens in both themes.
Closes Deen-Bridge#126
@vercel

vercel Bot commented Jul 27, 2026

Copy link
Copy Markdown

@cLamberti is attempting to deploy a commit to the Deen Bridge Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Adds configurable light/dark themes, persisted accent and font-size preferences, theme controls, updated CSS variables, and theme-aware styling across account, dashboard, card, modal, widget, and reader interfaces.

Changes

Theme and appearance system

Layer / File(s) Summary
Theme tokens and appearance configuration
lib/config/appearance.config.js, styles/globals.css, CONTRIBUTING.md
Defines appearance presets and CSS variables, updates light/dark theme values, and documents semantic theming guidance.
Provider initialization and persistence
components/providers/*, app/layout.js, public/sw.js
Adds theme and appearance providers, restores persisted settings, initializes variables before paint, wires root providers, and refreshes precache metadata.
Theme controls and settings integration
components/ui/theme-toggle.jsx, components/molecules/dashboard/nav-header.jsx, app/account/settings/page.jsx
Adds a mounted-safe theme dropdown and connects settings controls to next-themes and appearance persistence.
Theme-aware application styling
app/account/*, app/dashboard/*, components/atoms/*, components/molecules/*, components/organisms/*
Replaces fixed backgrounds, accent colors, and hardcoded foreground colors with semantic theme tokens across application interfaces.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related issues

  • Issue 126 — Adds the dark-mode and theming capabilities implemented by this pull request, including theme providers, controls, token migration, and documentation.

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 3.03% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed Title accurately summarizes the main changes: dark mode support plus migration to semantic design tokens.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (6)
app/dashboard/earnings/page.jsx (1)

149-153: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Pass the new color variables directly to the chart.

The theme tokens now contain complete oklch(...) colors, so hsl(var(--primary)) and hsl(var(--muted)) produce invalid CSS. Revenue bars or tooltip cursors can lose their colors after the theme migration. Use var(--primary) and var(--muted) directly, or define separate channel variables.

Also applies to: 301-305

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/dashboard/earnings/page.jsx` around lines 149 - 153, Update the
chartConfig color values for revenue and the corresponding muted tooltip cursor
configuration to use the complete theme tokens directly via var(--primary) and
var(--muted), removing the hsl() wrappers. Preserve the existing chart labels
and configuration behavior.
app/dashboard/sadaqah/page.jsx (1)

77-93: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Handle rejected fund-stat requests.

If getDonationStats() rejects, setStatsLoading(false) is never reached and the page remains stuck in its skeleton state. Wrap the request in try/catch/finally, set statsError, and preserve the retry state.

As per path instructions, app/** data-fetching screens must expose loading and error states.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/dashboard/sadaqah/page.jsx` around lines 77 - 93, Update fetchStats to
handle rejected getDonationStats requests with try/catch/finally. Set statsError
from the caught error while preserving the existing success and unconfigured
handling, and ensure setStatsLoading(false) runs in finally so loading always
ends and retry behavior remains available.

Source: Path instructions

app/dashboard/search/[searchparam]/page.jsx (2)

61-67: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Distinguish failed searches from empty results.

catch(() => setResults([])) makes API/network failures render the “No results found” state, leaving users without an error or retry path. Track an error state and render it separately.

As per path instructions, app/** data-fetching screens must expose loading and error states.

Also applies to: 125-127

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/dashboard/search/`[searchparam]/page.jsx around lines 61 - 67, Update the
search page’s searchQuery effect to track failures in a dedicated error state
instead of converting them to empty results. Clear the error when starting a new
search, set it in the catch handler, and render a distinct error state with an
appropriate retry path separate from the “No results found” state while
preserving loading behavior.

Source: Path instructions


56-57: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Read the route param with useParams() or use(params).
This page is a client component, so params?.searchparam is the old sync contract and can leave the search empty in Next 15. Unwrap params with React use() or read the segment via useParams() instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/dashboard/search/`[searchparam]/page.jsx around lines 56 - 57, Update the
Page component’s route-parameter handling to use useParams() or React
use(params) instead of synchronously reading params?.searchparam, ensuring the
Next 15 route value populates the search term while preserving the existing
empty-string fallback.

Source: Path instructions

app/dashboard/library/read/[bookid]/BookReaderClient.jsx (1)

263-269: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Theme-aware foregrounds are still placed on fixed light surfaces.

The dark-theme text-brand-text value is light green, but several affected surfaces remain white or light-only. Convert the surfaces—not just the foreground token—to semantic theme tokens.

  • app/dashboard/library/read/[bookid]/BookReaderClient.jsx#L263-L269: replace the reader header and related white surfaces with bg-card/bg-background variants; the same applies to the changed loaders and controls.
  • app/dashboard/sadaqah/page.jsx#L167-L170: replace the hero and statistic via-white gradients with theme-aware surfaces; this also covers the changed statistic values at Lines 228-247.
  • components/organisms/dashboard/JaasMeetingClientSection.jsx#L348-L355: make the outlined disabled Button surface theme-aware instead of forcing bg-white.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/dashboard/library/read/`[bookid]/BookReaderClient.jsx around lines 263 -
269, Replace fixed light surfaces with semantic theme-aware tokens across all
affected sites: in BookReaderClient.jsx, update the reader header, loaders, and
controls around the shown Link to use bg-card/bg-background variants; in
app/dashboard/sadaqah/page.jsx lines 167-170, replace hero and statistic
via-white gradients, including the changed statistic values at lines 228-247,
with theme-aware surfaces; in JaasMeetingClientSection.jsx lines 348-355, remove
forced bg-white from the outlined disabled Button and use a theme-aware surface.
app/dashboard/spaces/[spacesid]/page.jsx (1)

10-11: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Await params before destructuring. Next.js 15 App Router treats params as async; use const { spacesid } = await params; to stay on the supported path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/dashboard/spaces/`[spacesid]/page.jsx around lines 10 - 11, Update the
Page function’s parameter handling to await params before destructuring,
assigning spacesid from the resolved params object while preserving the existing
page behavior.

Source: Path instructions

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/dashboard/saved/page.jsx`:
- Line 97: Update the Bookmark icon styling to keep its fill consistent with the
brand stroke: replace fill-accent with fill-brand-text in the Bookmark element,
or remove the fill class if the icon should remain outlined.

---

Outside diff comments:
In `@app/dashboard/earnings/page.jsx`:
- Around line 149-153: Update the chartConfig color values for revenue and the
corresponding muted tooltip cursor configuration to use the complete theme
tokens directly via var(--primary) and var(--muted), removing the hsl()
wrappers. Preserve the existing chart labels and configuration behavior.

In `@app/dashboard/library/read/`[bookid]/BookReaderClient.jsx:
- Around line 263-269: Replace fixed light surfaces with semantic theme-aware
tokens across all affected sites: in BookReaderClient.jsx, update the reader
header, loaders, and controls around the shown Link to use bg-card/bg-background
variants; in app/dashboard/sadaqah/page.jsx lines 167-170, replace hero and
statistic via-white gradients, including the changed statistic values at lines
228-247, with theme-aware surfaces; in JaasMeetingClientSection.jsx lines
348-355, remove forced bg-white from the outlined disabled Button and use a
theme-aware surface.

In `@app/dashboard/sadaqah/page.jsx`:
- Around line 77-93: Update fetchStats to handle rejected getDonationStats
requests with try/catch/finally. Set statsError from the caught error while
preserving the existing success and unconfigured handling, and ensure
setStatsLoading(false) runs in finally so loading always ends and retry behavior
remains available.

In `@app/dashboard/search/`[searchparam]/page.jsx:
- Around line 61-67: Update the search page’s searchQuery effect to track
failures in a dedicated error state instead of converting them to empty results.
Clear the error when starting a new search, set it in the catch handler, and
render a distinct error state with an appropriate retry path separate from the
“No results found” state while preserving loading behavior.
- Around line 56-57: Update the Page component’s route-parameter handling to use
useParams() or React use(params) instead of synchronously reading
params?.searchparam, ensuring the Next 15 route value populates the search term
while preserving the existing empty-string fallback.

In `@app/dashboard/spaces/`[spacesid]/page.jsx:
- Around line 10-11: Update the Page function’s parameter handling to await
params before destructuring, assigning spacesid from the resolved params object
while preserving the existing page behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 0d0f6161-c7cc-4629-a105-8c5a5e5ac846

📥 Commits

Reviewing files that changed from the base of the PR and between 1d5ad8e and 797bcf3.

📒 Files selected for processing (46)
  • CONTRIBUTING.md
  • app/account/notifications/page.jsx
  • app/account/settings/page.jsx
  • app/dashboard/ai/page.jsx
  • app/dashboard/courses/[courseId]/CourseDetailPageClient.jsx
  • app/dashboard/courses/page.jsx
  • app/dashboard/earnings/page.jsx
  • app/dashboard/library/[bookid]/BookDetailPageClient.jsx
  • app/dashboard/library/page.jsx
  • app/dashboard/library/read/[bookid]/BookReaderClient.jsx
  • app/dashboard/purchases/page.jsx
  • app/dashboard/sadaqah/page.jsx
  • app/dashboard/saved/page.jsx
  • app/dashboard/search/[searchparam]/page.jsx
  • app/dashboard/spaces/[spacesid]/page.jsx
  • app/dashboard/spaces/page.jsx
  • app/layout.js
  • components/atoms/dashboard/DashTabs.jsx
  • components/atoms/dashboard/Notybell.jsx
  • components/atoms/dashboard/Searchbox.jsx
  • components/atoms/form/StarRate.jsx
  • components/molecules/Modal.js
  • components/molecules/dashboard/cards/courseCard.jsx
  • components/molecules/dashboard/cards/libraryCard.jsx
  • components/molecules/dashboard/cards/spaceCard.jsx
  • components/molecules/dashboard/nav-header.jsx
  • components/molecules/dashboard/wizard-step-indicator.jsx
  • components/organisms/account/profile/ProfileTabs.jsx
  • components/organisms/account/profile/ProfileUserInfo.jsx
  • components/organisms/dashboard/JaasMeetingClientSection.jsx
  • components/organisms/dashboard/OngoingSessions.jsx
  • components/organisms/dashboard/PrayerTimesWidget.jsx
  • components/organisms/dashboard/R-BooksCard.jsx
  • components/organisms/dashboard/R-CourseCard.jsx
  • components/organisms/dashboard/RecentChats.jsx
  • components/organisms/dashboard/ReviewsSection.jsx
  • components/organisms/dashboard/StatsOverview.jsx
  • components/organisms/dashboard/StreamingAIChat.jsx
  • components/organisms/dashboard/Supports.jsx
  • components/organisms/dashboard/UpcomingSessions.jsx
  • components/providers/AppearanceProvider.jsx
  • components/providers/ThemeProvider.jsx
  • components/ui/theme-toggle.jsx
  • lib/config/appearance.config.js
  • public/sw.js
  • styles/globals.css

<div>
<h1 className="text-2xl font-bold flex items-center gap-2">
<Bookmark className="h-6 w-6 text-accent fill-accent" />
<Bookmark className="h-6 w-6 text-brand-text fill-accent" />

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Keep the bookmark fill on the same theme token.

text-brand-text themes the stroke, but fill-accent remains a surface token. In dark mode the filled portion becomes dark gray while the stroke is bright green. Use fill-brand-text for a filled brand icon, or remove the fill class if an outline icon is intended.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/dashboard/saved/page.jsx` at line 97, Update the Bookmark icon styling to
keep its fill consistent with the brand stroke: replace fill-accent with
fill-brand-text in the Bookmark element, or remove the fill class if the icon
should remain outlined.

@cLamberti

Copy link
Copy Markdown
Contributor Author

CodeRabbit its mentioning some issues; do you want me to resolve them?

@zeemscript
zeemscript merged commit 606fd0e into Deen-Bridge:main Jul 29, 2026
2 of 3 checks passed
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.

[Enhancement] Dark mode and a consolidated theming system: wire next-themes, add a theme toggle, migrate hardcoded colors to tokens

2 participants