From eb5142bc213b88e244f17b10d4faa64ae61191c7 Mon Sep 17 00:00:00 2001 From: Damilola Maria Ajibade Date: Tue, 18 Aug 2026 03:46:32 +0000 Subject: [PATCH 1/2] feat: add design-system theme provider with light/dark modes (#30) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Codify the navy/gold/terracotta identity system into a ThemeProvider context with accessible light and dark modes persisted per user. Theme logic lives in src/lib/theme.ts (storage, resolution, system listening, no-FOUC script); ThemeToggle now consumes useTheme. 🤖 Generated with Codebuff Co-Authored-By: Codebuff --- README.md | 7 +- TODO.md | 57 ++++++++-- src/app/layout.tsx | 20 ++-- src/components/ThemeToggle.tsx | 45 +++----- src/components/theme/ThemeProvider.tsx | 104 +++++++++++++++++ src/components/theme/index.ts | 1 + src/lib/test/theme.test.ts | 150 +++++++++++++++++++++++++ src/lib/theme.ts | 98 ++++++++++++++++ 8 files changed, 431 insertions(+), 51 deletions(-) create mode 100644 src/components/theme/ThemeProvider.tsx create mode 100644 src/components/theme/index.ts create mode 100644 src/lib/test/theme.test.ts create mode 100644 src/lib/theme.ts diff --git a/README.md b/README.md index 69f5578..b24af56 100644 --- a/README.md +++ b/README.md @@ -74,8 +74,11 @@ This is **Identity System v1** — see [Brand assets](#brand-assets): `next/font/google`. - **Dark theme** — a token-level flip in `globals.css`. It follows `prefers-color-scheme` by default; a stored preference stamps `data-theme` on - `` and wins in both directions (`ThemeToggle` in the navbar, with a - no-FOUC script in the layout). + `` and wins in both directions. Theme state is owned by + `` (`src/components/theme/`) via the `useTheme` hook — the + per-user choice is persisted to localStorage, the OS preference is followed + live until a choice is made, and a no-FOUC script in the layout paints the + initial theme before React hydrates (`ThemeToggle` in the navbar). ## Brand assets diff --git a/TODO.md b/TODO.md index c82092f..0e5bf56 100644 --- a/TODO.md +++ b/TODO.md @@ -1,16 +1,57 @@ -# Transaction Notification Center — Implementation Checklist ✓ +# Design-System Theme Provider (navy/gold/terracotta) — Implementation Checklist ✓ + +## Objective +Codify the navy/gold/terracotta identity system into a theme provider with +accessible light and dark modes persisted per user (issue #30). ## Files to Create -- [x] `src/lib/notifications.ts` — Types, interfaces, and helper functions -- [x] `src/components/notifications/useNotifications.tsx` — React Context, Provider, and hook -- [x] `src/components/notifications/NotificationToast.tsx` — Floating toast component -- [x] `src/components/notifications/NotificationCenter.tsx` — Dropdown panel with notification list +- [x] `src/lib/theme.ts` — Theme system single source of truth: `Theme` type, + `THEME_STORAGE_KEY`, storage get/set, `getSystemTheme`, `applyTheme`, + `listenForSystemTheme`, `resolveTheme`, and the shared no-FOUC + `themeScript`. +- [x] `src/components/theme/ThemeProvider.tsx` — React Context provider + + `useTheme` hook (stored choice wins, OS preference followed live until + the user picks). +- [x] `src/components/theme/index.ts` — Barrel export (matches + `notifications/index.ts`). +- [x] `src/lib/test/theme.test.ts` — Unit tests (storage, resolution, + application, system listening, script). ## Files to Edit -- [x] `src/app/layout.tsx` — Wrap with NotificationProvider -- [x] `src/components/Navbar.tsx` — Add bell icon with unread count badge +- [x] `src/components/ThemeToggle.tsx` — Consume `useTheme`; added + `aria-pressed`; visuals unchanged. +- [x] `src/app/layout.tsx` — Wrap app in ``; inline no-FOUC + script now imported from `@/lib/theme` (single source of truth). + +## Architectural Decisions +- **No new dependencies.** Uses React Context (existing pattern from + `NotificationProvider`), `react-icons` (already a dep), and the existing + Tailwind v4 token setup. +- **Tokens stay in CSS.** The navy/gold/terracotta tokens and their dark-mode + overrides live in `src/app/globals.css` via `@theme inline` + `data-theme` + (this was already in place). `src/lib/theme.ts` owns the *logic* around + those tokens. +- **Persistence is per-user via localStorage** (key `theme`) — the same + mechanism the previous standalone toggle used, so existing stored choices + keep working. No auth/session exists yet, so browser-local is the right + scope; a per-account key can be layered on later. +- **Dark mode is a first-class, accessible mode:** `color-scheme` is set so + native controls adapt, focus rings use `outline-gold`, and the toggle + exposes `aria-label` + `aria-pressed`. +- **No flash of wrong theme (FOUC):** the root layout's inline + `themeScript` (now imported from `@/lib/theme`) paints the correct theme + before React hydrates; the provider only keeps React state in sync. +- **OS-follow by default:** until the user makes an explicit choice the + provider follows `prefers-color-scheme` live; choosing persists and stops + following. ## Verification - [x] `npm run lint` — No linting errors -- [x] `npm run build` — Production build succeeds (17 routes) +- [x] `npm run typecheck` — No TypeScript errors +- [x] `npm test` — All tests pass (incl. `theme.test.ts`) +- [x] `npm run build` — Production build succeeds +## CI Note +The issue's "add caching for npm dependencies in CI" task was already +satisfied: `.github/workflows/ci.yml` uses `actions/setup-node` with +`cache: npm`. diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 05a0b4e..807420b 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -4,18 +4,16 @@ import "leaflet/dist/leaflet.css"; import "./globals.css"; import Navbar from "@/components/Navbar"; import Footer from "@/components/Footer"; +import { ThemeProvider } from "@/components/theme"; import { NotificationProvider } from "@/components/notifications/useNotifications"; import NotificationToast from "@/components/notifications/NotificationToast"; +import { themeScript } from "@/lib/theme"; const inter = Inter({ variable: "--font-inter", subsets: ["latin"], }); -// Runs before paint so the correct theme is applied with no flash: honour a -// stored preference, otherwise fall back to the OS setting (handled in CSS). -const themeScript = `(function(){try{var t=localStorage.getItem('theme');var r=document.documentElement;if(t==='light'||t==='dark'){r.setAttribute('data-theme',t);r.style.colorScheme=t;}else{r.style.colorScheme=window.matchMedia('(prefers-color-scheme: dark)').matches?'dark':'light';}}catch(e){}})();`; - export const metadata: Metadata = { title: "GuildWorkman — Book trusted local pros", description: @@ -33,12 +31,14 @@ export default function RootLayout({