Fix dashboard hydration and archive theming

This commit is contained in:
2026-03-23 23:08:18 -04:00
parent c852ad0d80
commit feb2f2c37e
25 changed files with 290 additions and 13 deletions

View File

@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-03-23

View File

@@ -0,0 +1,54 @@
## Context
The project uses Next.js with Tailwind CSS v4 and a warm stone/amber palette. All styling is done via Tailwind utility classes directly in JSX. There are no separate CSS modules for components. The root layout owns the `<html>` and `<body>` tags, making it the natural place to manage the theme class and prevent flash-of-unstyled-content.
## Goals / Non-Goals
**Goals:**
- Implement class-based dark mode that works with Tailwind v4's `@custom-variant` API.
- Prevent theme flash on page load with an inline blocking script.
- Persist user preference in `localStorage` under the key `theme`.
- Fall back to system preference (`prefers-color-scheme`) when no preference is saved.
- Keep the dark palette warm and consistent with the existing amber/stone design language.
**Non-Goals:**
- Server-side theme rendering or cookies (local-only app, no SSR theme concerns beyond flash prevention).
- Per-page or per-component theme overrides.
- Animated theme transitions beyond the existing `transition-duration: 180ms` on interactive elements.
## Decisions
### Use Tailwind v4 `@custom-variant` for class-based dark mode
- Rationale: Tailwind v4's default dark mode is media-query-based. Adding `@custom-variant dark (&:where(.dark, .dark *))` in `globals.css` enables the `dark:` prefix to respond to a class on any ancestor, which is the standard pattern for toggle-able dark mode.
- Alternative considered: Continue using media-query dark mode with no toggle. Rejected because users cannot override system preference.
### Store theme preference in `localStorage`
- Rationale: Simple, synchronous, requires no server or cookies. Reads correctly in the blocking inline script.
- Alternative considered: Cookies for SSR. Rejected because this app is local-first and has no meaningful SSR theme benefit.
### Inject an inline blocking script in `<head>` to set the `dark` class before first paint
- Rationale: Prevents the flash where the page briefly renders in light mode before React hydrates and reads `localStorage`. The script is small and runs synchronously.
- Alternative considered: Set the class in a React `useEffect`. Rejected because `useEffect` runs after paint, causing a visible flash.
### Warm deep-stone dark palette
- Rationale: The light theme is built on warm stone and amber tones. The dark theme mirrors this with deep stone backgrounds (`stone-950`, `stone-900`, `stone-800`) and dimmed amber/emerald accents, keeping the visual identity coherent.
- Alternative considered: Neutral dark greys. Rejected because they clash with the warm amber accents and feel disconnected from the brand.
## Risks / Trade-offs
- [Inline script adds a small amount of render-blocking HTML] → Acceptable; the script is under 200 bytes and only runs once per page load.
- [LocalStorage is unavailable in some privacy modes] → The script wraps the read in a try/catch and falls back to system preference.
- [Many components have hardcoded warm background strings like `bg-[#fffaf2]`] → These are replaced with equivalent Tailwind tokens plus `dark:` overrides so the mapping is explicit and maintainable.
## Migration Plan
1. Update `globals.css` with `@custom-variant dark` and dark-mode CSS variable overrides.
2. Create the `ThemeToggle` client component.
3. Update `layout.tsx` to add the blocking script and render `ThemeToggle` in the header.
4. Update `site-nav.tsx` with dark-mode nav styles.
5. Update `home-dashboard.tsx`, `expense-workspace.tsx`, `paycheck-workspace.tsx`, and `recurring-expense-manager.tsx` with `dark:` variants.
6. Update page-level header text that uses hardcoded colour classes.
## Open Questions
- Should the toggle also expose a "System" option (three-way: Light / Dark / System) rather than a binary flip? Deferred to a follow-up; the initialisation script already handles system fallback on first visit.

View File

@@ -0,0 +1,27 @@
## Why
The app currently ships with a warm light theme only. Adding a system-aware dark mode with a manual toggle removes eye strain in low-light conditions and meets baseline accessibility expectations for a personal finance tool used at any hour.
## What Changes
- Add a class-based dark mode variant using Tailwind v4 `@custom-variant` so all `dark:` utilities are controlled by a `dark` class on `<html>`.
- Add a `ThemeToggle` component that persists the user's preference in `localStorage` and initialises the correct class before first paint to prevent flash.
- Update the root layout to inject the inline initialisation script and render the toggle inside the existing header.
- Apply `dark:` variants to all UI components to produce a warm, deep-stone dark palette consistent with the existing amber/stone design language.
## Capabilities
### New Capabilities
- `dark-mode`: Users can switch between light and dark themes with a toggle that persists across sessions. System preference is respected on first visit.
### Modified Capabilities
- `expense-tracking`: Expense workspace and recurring expense manager now render correctly in both themes.
- `paycheck-tracking`: Paycheck workspace now renders correctly in both themes.
- `monthly-dashboard`: Dashboard sections, insight cards, category bars, and stat cards now render correctly in both themes.
## Impact
- Affected code: `globals.css`, `layout.tsx`, all components under `src/components/`, and page header text in `src/app/`.
- APIs: None.
- Dependencies: None — uses Tailwind v4 built-ins and the Web Storage API.
- Systems: No server-side changes required; theme state is fully client-side.

View File

@@ -0,0 +1,26 @@
## ADDED Requirements
### Requirement: User can switch between light and dark themes
The system SHALL allow the user to toggle between light and dark themes with a persistent preference.
#### Scenario: User toggles dark mode on
- **WHEN** the user activates the theme toggle while the app is in light mode
- **THEN** the system applies the dark theme and saves the preference for future visits
#### Scenario: User toggles dark mode off
- **WHEN** the user activates the theme toggle while the app is in dark mode
- **THEN** the system applies the light theme and saves the preference for future visits
### Requirement: Theme preference respects system defaults on first visit
The system SHALL use the user's system color scheme preference when no saved preference exists.
#### Scenario: No stored preference uses system theme
- **WHEN** the user opens the app for the first time without a saved theme preference
- **THEN** the system applies dark mode when the operating system prefers dark color schemes and light mode otherwise
### Requirement: Theme selection loads without a flash of the wrong theme
The system SHALL initialize the theme before the first visible paint so the page does not briefly render in the wrong theme.
#### Scenario: Initial paint matches saved theme
- **WHEN** the app loads and a saved theme preference exists
- **THEN** the document theme is applied before the page content is visibly rendered

View File

@@ -0,0 +1,12 @@
## ADDED Requirements
### Requirement: Expense tracking UI renders correctly in both themes
The system SHALL render the expense tracking workspace, history list, forms, and item states with readable contrast in both light and dark themes.
#### Scenario: Expense workspace renders in dark mode
- **WHEN** the user opens the expense tracking view while dark mode is active
- **THEN** the form card, history card, inputs, actions, and expense rows use dark-compatible colors and remain readable
#### Scenario: Expense workspace renders in light mode
- **WHEN** the user opens the expense tracking view while light mode is active
- **THEN** the form card, history card, inputs, actions, and expense rows use light-compatible colors and remain readable

View File

@@ -0,0 +1,12 @@
## ADDED Requirements
### Requirement: Monthly dashboard UI renders correctly in both themes
The system SHALL render dashboard sections, insight cards, category bars, stat tiles, and empty states with readable contrast in both light and dark themes.
#### Scenario: Dashboard renders in dark mode
- **WHEN** the user opens the dashboard while dark mode is active
- **THEN** the summary cards, comparison cards, progress bars, and empty states use dark-compatible colors and remain readable
#### Scenario: Dashboard renders in light mode
- **WHEN** the user opens the dashboard while light mode is active
- **THEN** the summary cards, comparison cards, progress bars, and empty states use light-compatible colors and remain readable

View File

@@ -0,0 +1,12 @@
## ADDED Requirements
### Requirement: Paycheck tracking UI renders correctly in both themes
The system SHALL render the paycheck tracking workspace, schedule panel, form, and list items with readable contrast in both light and dark themes.
#### Scenario: Paycheck workspace renders in dark mode
- **WHEN** the user opens the paycheck tracking view while dark mode is active
- **THEN** the schedule panel, form card, inputs, and paycheck rows use dark-compatible colors and remain readable
#### Scenario: Paycheck workspace renders in light mode
- **WHEN** the user opens the paycheck tracking view while light mode is active
- **THEN** the schedule panel, form card, inputs, and paycheck rows use light-compatible colors and remain readable

View File

@@ -0,0 +1,25 @@
## 1. CSS and variant setup
- [x] 1.1 Add `@custom-variant dark (&:where(.dark, .dark *))` to `globals.css` and update `:root` / `@theme` blocks with dark-mode CSS variable overrides.
## 2. Theme toggle infrastructure
- [x] 2.1 Create `src/components/theme-toggle.tsx` — a client component that reads and writes `localStorage` theme preference, toggles the `dark` class on `<html>`, and renders a sun/moon button.
- [x] 2.2 Update `src/app/layout.tsx` to inject the inline blocking script in `<head>` and render `ThemeToggle` in the header alongside `SiteNav`.
## 3. Component dark-mode styles
- [x] 3.1 Update `src/components/site-nav.tsx` with `dark:` variants for link backgrounds, borders, and text.
- [x] 3.2 Update `src/components/home-dashboard.tsx` with `dark:` variants for all section cards, stat tiles, insight blocks, progress bars, and empty states.
- [x] 3.3 Update `src/components/expense-workspace.tsx` with `dark:` variants for the form card, list card, inputs, and expense articles.
- [x] 3.4 Update `src/components/paycheck-workspace.tsx` with `dark:` variants for the schedule panel, form, and paycheck list.
- [x] 3.5 Update `src/components/recurring-expense-manager.tsx` with `dark:` variants for the panel, inline form, and definition articles.
## 4. Page header text
- [x] 4.1 Update hardcoded text colours in `src/app/add-expense/page.tsx` and `src/app/income/page.tsx` with `dark:` overrides.
## 5. Verification
- [x] 5.1 Visually verify light and dark modes across the dashboard, add-expense, and income pages in the browser.
- [x] 5.2 Verify that theme preference persists across page refreshes and that there is no flash of the wrong theme on load.