docs(04): create phase plan — 3 plans for full-app design consistency
This commit is contained in:
@@ -81,11 +81,12 @@ Plans:
|
|||||||
3. Budget Detail page displays category groups with the same color-accented card style and line-item presentation as the dashboard collapsible sections
|
3. Budget Detail page displays category groups with the same color-accented card style and line-item presentation as the dashboard collapsible sections
|
||||||
4. Navigating between any two pages in the app produces no jarring visual discontinuity in layout, color, or typography
|
4. Navigating between any two pages in the app produces no jarring visual discontinuity in layout, color, or typography
|
||||||
5. Switching the app to German locale shows fully translated text on every page — no raw i18n key strings visible anywhere
|
5. Switching the app to German locale shows fully translated text on every page — no raw i18n key strings visible anywhere
|
||||||
**Plans**: TBD
|
**Plans**: 3 plans
|
||||||
|
|
||||||
Plans:
|
Plans:
|
||||||
- [ ] 04-01: TBD
|
- [ ] 04-01-PLAN.md — Redesign auth pages (Login, Register) with brand presence, muted background, card accent, OAuth SVG icons
|
||||||
- [ ] 04-02: TBD
|
- [ ] 04-02-PLAN.md — Upgrade CRUD pages (Categories, Template, QuickAdd, Settings) with PageShell, skeletons, group header accents
|
||||||
|
- [ ] 04-03-PLAN.md — Upgrade budget pages (BudgetList, BudgetDetail) with semantic tokens, direction-aware diff, locale-aware months, skeletons
|
||||||
|
|
||||||
## Requirements Traceability
|
## Requirements Traceability
|
||||||
|
|
||||||
@@ -137,4 +138,4 @@ Phases execute in numeric order: 1 -> 2 -> 3 -> 4
|
|||||||
| 1. Design Foundation and Primitives | 2/2 | Complete | 2026-03-16 |
|
| 1. Design Foundation and Primitives | 2/2 | Complete | 2026-03-16 |
|
||||||
| 2. Dashboard Charts and Layout | 3/3 | Complete | 2026-03-16 |
|
| 2. Dashboard Charts and Layout | 3/3 | Complete | 2026-03-16 |
|
||||||
| 3. Collapsible Dashboard Sections | 1/2 | In Progress| |
|
| 3. Collapsible Dashboard Sections | 1/2 | In Progress| |
|
||||||
| 4. Full-App Design Consistency | 0/TBD | Not started | - |
|
| 4. Full-App Design Consistency | 0/3 | Not started | - |
|
||||||
|
|||||||
211
.planning/phases/04-full-app-design-consistency/04-01-PLAN.md
Normal file
211
.planning/phases/04-full-app-design-consistency/04-01-PLAN.md
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
---
|
||||||
|
phase: 04-full-app-design-consistency
|
||||||
|
plan: 01
|
||||||
|
type: execute
|
||||||
|
wave: 1
|
||||||
|
depends_on: []
|
||||||
|
files_modified:
|
||||||
|
- src/pages/LoginPage.tsx
|
||||||
|
- src/pages/RegisterPage.tsx
|
||||||
|
- src/i18n/en.json
|
||||||
|
- src/i18n/de.json
|
||||||
|
autonomous: true
|
||||||
|
requirements: [UI-AUTH-01, UI-DESIGN-01]
|
||||||
|
|
||||||
|
must_haves:
|
||||||
|
truths:
|
||||||
|
- "Login page shows muted background with the card floating on top, app logo above title"
|
||||||
|
- "Register page matches Login page design — same background, logo, card accent treatment"
|
||||||
|
- "OAuth buttons (Google, GitHub) display provider SVG icons next to text labels"
|
||||||
|
- "Auth subtitle text appears below the app title inside the card"
|
||||||
|
- "Switching to German locale shows fully translated auth page text"
|
||||||
|
artifacts:
|
||||||
|
- path: "src/pages/LoginPage.tsx"
|
||||||
|
provides: "Redesigned login page with muted bg, logo, card accent, OAuth icons"
|
||||||
|
contains: "bg-muted"
|
||||||
|
- path: "src/pages/RegisterPage.tsx"
|
||||||
|
provides: "Redesigned register page matching login design"
|
||||||
|
contains: "bg-muted"
|
||||||
|
- path: "src/i18n/en.json"
|
||||||
|
provides: "Auth subtitle i18n keys"
|
||||||
|
contains: "auth.loginSubtitle"
|
||||||
|
- path: "src/i18n/de.json"
|
||||||
|
provides: "German auth subtitle translations"
|
||||||
|
contains: "auth.loginSubtitle"
|
||||||
|
key_links:
|
||||||
|
- from: "src/pages/LoginPage.tsx"
|
||||||
|
to: "/favicon.svg"
|
||||||
|
via: "img src for app logo"
|
||||||
|
pattern: 'src="/favicon.svg"'
|
||||||
|
- from: "src/pages/RegisterPage.tsx"
|
||||||
|
to: "/favicon.svg"
|
||||||
|
via: "img src for app logo"
|
||||||
|
pattern: 'src="/favicon.svg"'
|
||||||
|
---
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
Redesign the Login and Register pages with brand presence and visual polish matching the established design system.
|
||||||
|
|
||||||
|
Purpose: Auth pages are the first impression of the app. Currently they use a plain `bg-background` with a bare card. This plan upgrades them to use a muted background, app logo, card accent styling, and provider SVG icons on OAuth buttons -- establishing visual consistency from the very first screen.
|
||||||
|
|
||||||
|
Output: Redesigned LoginPage.tsx, RegisterPage.tsx, and new i18n keys for auth subtitles.
|
||||||
|
</objective>
|
||||||
|
|
||||||
|
<execution_context>
|
||||||
|
@/home/jlmak/.claude/get-shit-done/workflows/execute-plan.md
|
||||||
|
@/home/jlmak/.claude/get-shit-done/templates/summary.md
|
||||||
|
</execution_context>
|
||||||
|
|
||||||
|
<context>
|
||||||
|
@.planning/PROJECT.md
|
||||||
|
@.planning/ROADMAP.md
|
||||||
|
@.planning/STATE.md
|
||||||
|
@.planning/phases/04-full-app-design-consistency/04-CONTEXT.md
|
||||||
|
@.planning/phases/04-full-app-design-consistency/04-RESEARCH.md
|
||||||
|
|
||||||
|
<interfaces>
|
||||||
|
<!-- PageShell is NOT used here -- auth pages are standalone, outside AppLayout -->
|
||||||
|
|
||||||
|
From src/pages/LoginPage.tsx (current structure to modify):
|
||||||
|
- Root: `<div className="flex min-h-screen items-center justify-center bg-background p-4">`
|
||||||
|
- Card: `<Card className="w-full max-w-sm">`
|
||||||
|
- Has OAuth buttons for Google and GitHub (text-only, no icons)
|
||||||
|
- Has Separator with "Or continue with" text
|
||||||
|
|
||||||
|
From src/pages/RegisterPage.tsx (current structure to modify):
|
||||||
|
- Same root div pattern as LoginPage
|
||||||
|
- No OAuth buttons (only email/password form)
|
||||||
|
- No Separator
|
||||||
|
|
||||||
|
From src/i18n/en.json (existing auth keys):
|
||||||
|
```json
|
||||||
|
"auth": {
|
||||||
|
"login": "Login",
|
||||||
|
"register": "Register",
|
||||||
|
"email": "Email",
|
||||||
|
"password": "Password",
|
||||||
|
"displayName": "Display Name",
|
||||||
|
"noAccount": "Don't have an account?",
|
||||||
|
"hasAccount": "Already have an account?",
|
||||||
|
"orContinueWith": "Or continue with"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Logo asset: `/public/favicon.svg` -- stylized lightning-bolt SVG in purple (#863bff). Use via `<img src="/favicon.svg">`.
|
||||||
|
</interfaces>
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<tasks>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 1: Redesign LoginPage with brand presence and OAuth icons</name>
|
||||||
|
<files>src/pages/LoginPage.tsx, src/i18n/en.json, src/i18n/de.json</files>
|
||||||
|
<action>
|
||||||
|
Modify LoginPage.tsx:
|
||||||
|
|
||||||
|
1. **Background:** Change root div className from `bg-background` to `bg-muted/60`.
|
||||||
|
|
||||||
|
2. **Card accent:** Add `border-t-4 border-t-primary shadow-lg` to the Card className.
|
||||||
|
|
||||||
|
3. **App logo:** Inside CardHeader, above the CardTitle, add:
|
||||||
|
```tsx
|
||||||
|
<img src="/favicon.svg" alt="" className="mx-auto mb-3 size-10" aria-hidden="true" />
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Subtitle:** Below the CardTitle, add:
|
||||||
|
```tsx
|
||||||
|
<p className="text-sm text-muted-foreground">{t("auth.loginSubtitle")}</p>
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **OAuth provider SVG icons:** Replace the plain text-only Google and GitHub buttons with inline SVG icons. Add a small (size-4) SVG before the text label in each button:
|
||||||
|
|
||||||
|
For Google button, add before "Google" text:
|
||||||
|
```tsx
|
||||||
|
<svg className="size-4" viewBox="0 0 24 24" aria-hidden="true">
|
||||||
|
<path d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92a5.06 5.06 0 0 1-2.2 3.32v2.77h3.57c2.08-1.92 3.27-4.74 3.27-8.1z" fill="#4285F4"/>
|
||||||
|
<path d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" fill="#34A853"/>
|
||||||
|
<path d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" fill="#FBBC05"/>
|
||||||
|
<path d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" fill="#EA4335"/>
|
||||||
|
</svg>
|
||||||
|
```
|
||||||
|
|
||||||
|
For GitHub button, add before "GitHub" text:
|
||||||
|
```tsx
|
||||||
|
<svg className="size-4" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
||||||
|
<path d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0 1 12 6.844a9.59 9.59 0 0 1 2.504.337c1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.02 10.02 0 0 0 22 12.017C22 6.484 17.522 2 12 2z"/>
|
||||||
|
</svg>
|
||||||
|
```
|
||||||
|
|
||||||
|
Add `gap-2` to each Button's className to space the icon and text. The buttons already have `className="flex-1"` -- add `gap-2` via the className string.
|
||||||
|
|
||||||
|
6. **i18n keys:** Add to en.json inside the "auth" object:
|
||||||
|
- `"loginSubtitle": "Sign in to your account"`
|
||||||
|
- `"registerSubtitle": "Create a new account"`
|
||||||
|
|
||||||
|
Add to de.json inside the "auth" object:
|
||||||
|
- `"loginSubtitle": "Melde dich bei deinem Konto an"`
|
||||||
|
- `"registerSubtitle": "Erstelle ein neues Konto"`
|
||||||
|
|
||||||
|
IMPORTANT: Update both en.json and de.json atomically in this task. Do not leave any raw i18n key strings.
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
<automated>cd /home/jlmak/Projects/jlmak/SimpleFinanceDash && bun run build</automated>
|
||||||
|
</verify>
|
||||||
|
<done>LoginPage shows muted/60 background, primary-colored top border on card, favicon.svg logo above title, "Sign in to your account" subtitle, Google SVG icon + GitHub SVG icon on OAuth buttons. Both en.json and de.json have the new auth subtitle keys.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 2: Redesign RegisterPage to match LoginPage treatment</name>
|
||||||
|
<files>src/pages/RegisterPage.tsx</files>
|
||||||
|
<action>
|
||||||
|
Modify RegisterPage.tsx to match the LoginPage design established in Task 1:
|
||||||
|
|
||||||
|
1. **Background:** Change root div className from `bg-background` to `bg-muted/60`.
|
||||||
|
|
||||||
|
2. **Card accent:** Add `border-t-4 border-t-primary shadow-lg` to the Card className.
|
||||||
|
|
||||||
|
3. **App logo:** Inside CardHeader, above the CardTitle, add:
|
||||||
|
```tsx
|
||||||
|
<img src="/favicon.svg" alt="" className="mx-auto mb-3 size-10" aria-hidden="true" />
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Subtitle:** Below the CardTitle, add:
|
||||||
|
```tsx
|
||||||
|
<p className="text-sm text-muted-foreground">{t("auth.registerSubtitle")}</p>
|
||||||
|
```
|
||||||
|
The i18n key `auth.registerSubtitle` was already added in Task 1.
|
||||||
|
|
||||||
|
5. **CardHeader padding:** Add `pb-4` to CardHeader className to match LoginPage spacing: `className="text-center pb-4"`.
|
||||||
|
|
||||||
|
Also apply `pb-4` to LoginPage's CardHeader if not already done in Task 1 (add `className="text-center pb-4"`).
|
||||||
|
|
||||||
|
Do NOT add OAuth buttons to RegisterPage -- it only has email/password registration. The existing "Already have an account?" link stays as-is.
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
<automated>cd /home/jlmak/Projects/jlmak/SimpleFinanceDash && bun run build</automated>
|
||||||
|
</verify>
|
||||||
|
<done>RegisterPage shows same muted/60 background, same card accent (border-t-4 primary, shadow-lg), same favicon logo, and register-specific subtitle. Visual parity with LoginPage minus OAuth buttons.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
- `bun run build` compiles without TypeScript errors
|
||||||
|
- `grep -c "bg-background" src/pages/LoginPage.tsx src/pages/RegisterPage.tsx` returns 0 for both files (old pattern fully replaced)
|
||||||
|
- `grep -c "bg-muted" src/pages/LoginPage.tsx src/pages/RegisterPage.tsx` returns 1 for each (new pattern applied)
|
||||||
|
- `grep "loginSubtitle" src/i18n/en.json src/i18n/de.json` returns matches in both files
|
||||||
|
- `grep "registerSubtitle" src/i18n/en.json src/i18n/de.json` returns matches in both files
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<success_criteria>
|
||||||
|
- Both auth pages use `bg-muted/60` background instead of `bg-background`
|
||||||
|
- Both auth pages show the app logo (`favicon.svg`) above the title
|
||||||
|
- Both auth pages have a card with `border-t-4 border-t-primary shadow-lg`
|
||||||
|
- LoginPage OAuth buttons show Google and GitHub SVG icons
|
||||||
|
- Both en.json and de.json have `auth.loginSubtitle` and `auth.registerSubtitle` keys
|
||||||
|
- `bun run build` passes
|
||||||
|
</success_criteria>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
After completion, create `.planning/phases/04-full-app-design-consistency/04-01-SUMMARY.md`
|
||||||
|
</output>
|
||||||
405
.planning/phases/04-full-app-design-consistency/04-02-PLAN.md
Normal file
405
.planning/phases/04-full-app-design-consistency/04-02-PLAN.md
Normal file
@@ -0,0 +1,405 @@
|
|||||||
|
---
|
||||||
|
phase: 04-full-app-design-consistency
|
||||||
|
plan: 02
|
||||||
|
type: execute
|
||||||
|
wave: 2
|
||||||
|
depends_on: [04-01]
|
||||||
|
files_modified:
|
||||||
|
- src/pages/CategoriesPage.tsx
|
||||||
|
- src/pages/TemplatePage.tsx
|
||||||
|
- src/pages/QuickAddPage.tsx
|
||||||
|
- src/pages/SettingsPage.tsx
|
||||||
|
- src/i18n/en.json
|
||||||
|
- src/i18n/de.json
|
||||||
|
autonomous: true
|
||||||
|
requirements: [UI-CATEGORIES-01, UI-TEMPLATE-01, UI-QUICKADD-01, UI-SETTINGS-01, UI-DESIGN-01]
|
||||||
|
|
||||||
|
must_haves:
|
||||||
|
truths:
|
||||||
|
- "Categories page uses PageShell for header with title and Add Category button"
|
||||||
|
- "Categories page shows category group headers with left-border accent styling"
|
||||||
|
- "Categories page shows skeleton loading state instead of blank screen"
|
||||||
|
- "Template page uses PageShell with inline-editable name and Add Item button"
|
||||||
|
- "Template page shows category group headers with left-border accent styling"
|
||||||
|
- "QuickAdd page uses PageShell for header"
|
||||||
|
- "QuickAdd page shows skeleton loading state instead of blank screen"
|
||||||
|
- "Settings page uses PageShell with no duplicate heading"
|
||||||
|
- "Settings page shows skeleton loading state instead of blank screen"
|
||||||
|
- "German locale shows all text translated on all four pages"
|
||||||
|
artifacts:
|
||||||
|
- path: "src/pages/CategoriesPage.tsx"
|
||||||
|
provides: "PageShell adoption, skeleton, group header upgrade"
|
||||||
|
contains: "PageShell"
|
||||||
|
- path: "src/pages/TemplatePage.tsx"
|
||||||
|
provides: "PageShell adoption, skeleton, group header upgrade"
|
||||||
|
contains: "PageShell"
|
||||||
|
- path: "src/pages/QuickAddPage.tsx"
|
||||||
|
provides: "PageShell adoption, skeleton"
|
||||||
|
contains: "PageShell"
|
||||||
|
- path: "src/pages/SettingsPage.tsx"
|
||||||
|
provides: "PageShell adoption, skeleton, no double heading"
|
||||||
|
contains: "PageShell"
|
||||||
|
key_links:
|
||||||
|
- from: "src/pages/CategoriesPage.tsx"
|
||||||
|
to: "src/components/shared/PageShell.tsx"
|
||||||
|
via: "import and render"
|
||||||
|
pattern: 'import.*PageShell.*from.*shared/PageShell'
|
||||||
|
- from: "src/pages/SettingsPage.tsx"
|
||||||
|
to: "src/components/shared/PageShell.tsx"
|
||||||
|
via: "import and render — replacing redundant h1"
|
||||||
|
pattern: 'import.*PageShell.*from.*shared/PageShell'
|
||||||
|
---
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
Apply PageShell, skeleton loading states, and group header upgrades to the four CRUD/settings pages (Categories, Template, QuickAdd, Settings).
|
||||||
|
|
||||||
|
Purpose: These four authenticated pages currently use inline `<h1>` + action div headers, return `null` while loading, and use small-dot group headers. This plan upgrades them to match the dashboard's design language -- consistent headers via PageShell, skeleton loading placeholders, and left-border accent group headers.
|
||||||
|
|
||||||
|
Output: Four updated page components with consistent design system application, plus new i18n keys for page descriptions.
|
||||||
|
</objective>
|
||||||
|
|
||||||
|
<execution_context>
|
||||||
|
@/home/jlmak/.claude/get-shit-done/workflows/execute-plan.md
|
||||||
|
@/home/jlmak/.claude/get-shit-done/templates/summary.md
|
||||||
|
</execution_context>
|
||||||
|
|
||||||
|
<context>
|
||||||
|
@.planning/PROJECT.md
|
||||||
|
@.planning/ROADMAP.md
|
||||||
|
@.planning/STATE.md
|
||||||
|
@.planning/phases/04-full-app-design-consistency/04-CONTEXT.md
|
||||||
|
@.planning/phases/04-full-app-design-consistency/04-RESEARCH.md
|
||||||
|
|
||||||
|
<interfaces>
|
||||||
|
From src/components/shared/PageShell.tsx:
|
||||||
|
```tsx
|
||||||
|
interface PageShellProps {
|
||||||
|
title: string
|
||||||
|
description?: string
|
||||||
|
action?: React.ReactNode
|
||||||
|
children: React.ReactNode
|
||||||
|
}
|
||||||
|
export function PageShell({ title, description, action, children }: PageShellProps)
|
||||||
|
```
|
||||||
|
|
||||||
|
From src/components/ui/skeleton.tsx:
|
||||||
|
```tsx
|
||||||
|
// Skeleton primitive -- use for building page-specific loading states
|
||||||
|
import { Skeleton } from "@/components/ui/skeleton"
|
||||||
|
// Usage: <Skeleton className="h-4 w-32" />
|
||||||
|
```
|
||||||
|
|
||||||
|
From src/lib/palette.ts:
|
||||||
|
```tsx
|
||||||
|
export const categoryColors: Record<CategoryType, string>
|
||||||
|
// Maps category type to CSS variable string like "var(--color-income)"
|
||||||
|
```
|
||||||
|
|
||||||
|
Group header upgrade pattern (from RESEARCH.md):
|
||||||
|
```tsx
|
||||||
|
// Replace plain dot headers with left-border accent
|
||||||
|
<div
|
||||||
|
className="mb-2 flex items-center gap-3 rounded-sm border-l-4 bg-muted/30 px-3 py-2"
|
||||||
|
style={{ borderLeftColor: categoryColors[type] }}
|
||||||
|
>
|
||||||
|
<span className="text-sm font-semibold">{t(`categories.types.${type}`)}</span>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
Current pattern in all CRUD pages to replace:
|
||||||
|
```tsx
|
||||||
|
<div className="mb-2 flex items-center gap-2">
|
||||||
|
<div className="size-3 rounded-full" style={{ backgroundColor: categoryColors[type] }} />
|
||||||
|
<h2 className="text-sm font-medium text-muted-foreground">{t(`categories.types.${type}`)}</h2>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
</interfaces>
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<tasks>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 1: Upgrade CategoriesPage and TemplatePage with PageShell, skeletons, and group headers</name>
|
||||||
|
<files>src/pages/CategoriesPage.tsx, src/pages/TemplatePage.tsx, src/i18n/en.json, src/i18n/de.json</files>
|
||||||
|
<action>
|
||||||
|
**CategoriesPage.tsx changes:**
|
||||||
|
|
||||||
|
1. **Import PageShell:** Add `import { PageShell } from "@/components/shared/PageShell"` and `import { Skeleton } from "@/components/ui/skeleton"`.
|
||||||
|
|
||||||
|
2. **Replace header:** Remove the `<div className="mb-6 flex items-center justify-between">` block containing the `<h1>` and `<Button>`. Wrap the entire return content in:
|
||||||
|
```tsx
|
||||||
|
<PageShell
|
||||||
|
title={t("categories.title")}
|
||||||
|
action={
|
||||||
|
<Button onClick={openCreate} size="sm">
|
||||||
|
<Plus className="mr-1 size-4" />
|
||||||
|
{t("categories.add")}
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{/* existing content (empty state check + grouped sections) */}
|
||||||
|
</PageShell>
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Skeleton loading:** Replace `if (loading) return null` with:
|
||||||
|
```tsx
|
||||||
|
if (loading) return (
|
||||||
|
<PageShell title={t("categories.title")}>
|
||||||
|
<div className="space-y-6">
|
||||||
|
{[1, 2, 3].map((i) => (
|
||||||
|
<div key={i} className="space-y-2">
|
||||||
|
<div className="flex items-center gap-3 rounded-sm border-l-4 border-muted bg-muted/30 px-3 py-2">
|
||||||
|
<Skeleton className="h-4 w-28" />
|
||||||
|
</div>
|
||||||
|
{[1, 2].map((j) => (
|
||||||
|
<div key={j} className="flex items-center gap-4 px-4 py-2.5 border-b border-border">
|
||||||
|
<Skeleton className="h-4 w-36" />
|
||||||
|
<Skeleton className="h-5 w-16 rounded-full" />
|
||||||
|
<Skeleton className="ml-auto h-7 w-7 rounded-md" />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</PageShell>
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Group header upgrade:** Replace the plain dot group header pattern in the `grouped.map` with the left-border accent pattern:
|
||||||
|
```tsx
|
||||||
|
<div
|
||||||
|
className="mb-2 flex items-center gap-3 rounded-sm border-l-4 bg-muted/30 px-3 py-2"
|
||||||
|
style={{ borderLeftColor: categoryColors[type] }}
|
||||||
|
>
|
||||||
|
<span className="text-sm font-semibold">{t(`categories.types.${type}`)}</span>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
Remove the old `<div className="mb-2 flex items-center gap-2">` block with the `size-3 rounded-full` dot and `<h2>`.
|
||||||
|
|
||||||
|
**TemplatePage.tsx changes:**
|
||||||
|
|
||||||
|
1. **Import PageShell and Skeleton:** Same imports as CategoriesPage.
|
||||||
|
|
||||||
|
2. **Replace header:** The TemplatePage header has an inline-editable `TemplateName` component. Wrap with PageShell, putting TemplateName as the title area. Since PageShell accepts a `title` string but TemplateName is a component, use PageShell differently here:
|
||||||
|
|
||||||
|
Instead of wrapping with PageShell using `title` prop, replace the header div with PageShell but pass the template name as a plain string title when NOT editing. Actually, the TemplateName component handles its own editing state inline. The cleanest approach: keep the TemplateName component but wrap the page content differently.
|
||||||
|
|
||||||
|
Replace the entire page structure:
|
||||||
|
```tsx
|
||||||
|
<div>
|
||||||
|
{/* Header */}
|
||||||
|
<div className="mb-6 flex items-center justify-between gap-4">
|
||||||
|
<TemplateName ... />
|
||||||
|
<Button ...>...</Button>
|
||||||
|
</div>
|
||||||
|
...
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
With:
|
||||||
|
```tsx
|
||||||
|
<PageShell
|
||||||
|
title={template?.name ?? t("template.title")}
|
||||||
|
action={
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Button onClick={openCreate} size="sm" disabled={isSaving}>
|
||||||
|
<Plus className="mr-1 size-4" />
|
||||||
|
{t("template.addItem")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
...
|
||||||
|
</PageShell>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note:** The TemplateName inline-edit functionality is a nice feature that will be lost if we just use a plain title string. To preserve it while using PageShell: remove the `title` prop from PageShell and instead render TemplateName inside the PageShell children, ABOVE the content. Actually, the simplest correct approach is to NOT use PageShell's title prop for TemplatePage -- instead, pass a custom `action` that includes the Add button, and render TemplateName as the first child inside PageShell with the title styling matching PageShell's own h1 style. But this defeats the purpose.
|
||||||
|
|
||||||
|
Best approach: Use PageShell for the layout but pass the TemplateName component as a React node for the title slot. Since PageShell only accepts `title: string`, we need to slightly modify the approach. Just use PageShell's wrapper layout manually:
|
||||||
|
|
||||||
|
Replace the header with:
|
||||||
|
```tsx
|
||||||
|
<div className="flex flex-col gap-6">
|
||||||
|
<div className="flex items-start justify-between gap-4">
|
||||||
|
<TemplateName
|
||||||
|
name={template?.name ?? t("template.title")}
|
||||||
|
onSave={handleNameSave}
|
||||||
|
/>
|
||||||
|
<div className="shrink-0">
|
||||||
|
<Button onClick={openCreate} size="sm" disabled={isSaving}>
|
||||||
|
<Plus className="mr-1 size-4" />
|
||||||
|
{t("template.addItem")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* rest of content */}
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
This mirrors PageShell's exact DOM structure (flex flex-col gap-6 > flex items-start justify-between gap-4) without importing PageShell, since TemplateName is a custom component that cannot be a plain string. This keeps visual consistency.
|
||||||
|
|
||||||
|
Additionally, update TemplateName's `<h1>` to use `className="text-2xl font-semibold tracking-tight"` (add `tracking-tight` to match PageShell's h1 styling).
|
||||||
|
|
||||||
|
3. **Skeleton loading:** Replace `if (loading) return null` with a skeleton that mirrors the template page layout:
|
||||||
|
```tsx
|
||||||
|
if (loading) return (
|
||||||
|
<div className="flex flex-col gap-6">
|
||||||
|
<div className="flex items-start justify-between gap-4">
|
||||||
|
<Skeleton className="h-8 w-48" />
|
||||||
|
<Skeleton className="h-9 w-24" />
|
||||||
|
</div>
|
||||||
|
<div className="space-y-6">
|
||||||
|
{[1, 2].map((i) => (
|
||||||
|
<div key={i} className="space-y-2">
|
||||||
|
<div className="flex items-center gap-3 rounded-sm border-l-4 border-muted bg-muted/30 px-3 py-2">
|
||||||
|
<Skeleton className="h-4 w-28" />
|
||||||
|
</div>
|
||||||
|
{[1, 2, 3].map((j) => (
|
||||||
|
<div key={j} className="flex items-center gap-4 px-4 py-2.5 border-b border-border">
|
||||||
|
<Skeleton className="h-4 w-36" />
|
||||||
|
<Skeleton className="h-5 w-16 rounded-full" />
|
||||||
|
<Skeleton className="ml-auto h-4 w-20" />
|
||||||
|
<Skeleton className="h-7 w-7 rounded-md" />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Group header upgrade:** Same left-border accent pattern as CategoriesPage. Replace the dot+h2 pattern in grouped.map with:
|
||||||
|
```tsx
|
||||||
|
<div
|
||||||
|
className="mb-2 flex items-center gap-3 rounded-sm border-l-4 bg-muted/30 px-3 py-2"
|
||||||
|
style={{ borderLeftColor: categoryColors[type] }}
|
||||||
|
>
|
||||||
|
<span className="text-sm font-semibold">{t(`categories.types.${type}`)}</span>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
**i18n: No new keys needed for this task.** Categories and Template pages already have all required i18n keys. The page descriptions are optional (Claude's discretion) -- skip them for these two pages since the page purpose is self-evident from the content.
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
<automated>cd /home/jlmak/Projects/jlmak/SimpleFinanceDash && bun run build</automated>
|
||||||
|
</verify>
|
||||||
|
<done>CategoriesPage and TemplatePage both show: consistent header layout matching PageShell spacing (flex-col gap-6), left-border accent group headers replacing dot headers, skeleton loading states replacing `return null`. No inline h1 header pattern remains. Build passes.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 2: Upgrade QuickAddPage and SettingsPage with PageShell and skeletons</name>
|
||||||
|
<files>src/pages/QuickAddPage.tsx, src/pages/SettingsPage.tsx</files>
|
||||||
|
<action>
|
||||||
|
**QuickAddPage.tsx changes:**
|
||||||
|
|
||||||
|
1. **Import PageShell and Skeleton:** Add `import { PageShell } from "@/components/shared/PageShell"` and `import { Skeleton } from "@/components/ui/skeleton"`.
|
||||||
|
|
||||||
|
2. **Replace header:** Remove the `<div className="mb-6 flex items-center justify-between">` header block. Wrap the entire return in:
|
||||||
|
```tsx
|
||||||
|
<PageShell
|
||||||
|
title={t("quickAdd.title")}
|
||||||
|
action={
|
||||||
|
<Button onClick={openCreate} size="sm">
|
||||||
|
<Plus className="mr-1 size-4" />
|
||||||
|
{t("quickAdd.add")}
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{/* empty state + table + dialog */}
|
||||||
|
</PageShell>
|
||||||
|
```
|
||||||
|
Remove the wrapping `<div>` root since PageShell provides the outer container.
|
||||||
|
|
||||||
|
3. **Skeleton loading:** Replace `if (loading) return null` with:
|
||||||
|
```tsx
|
||||||
|
if (loading) return (
|
||||||
|
<PageShell title={t("quickAdd.title")}>
|
||||||
|
<div className="space-y-1">
|
||||||
|
{[1, 2, 3, 4, 5].map((i) => (
|
||||||
|
<div key={i} className="flex items-center gap-4 px-4 py-2.5 border-b border-border">
|
||||||
|
<Skeleton className="h-5 w-10 rounded-full" />
|
||||||
|
<Skeleton className="h-4 w-36" />
|
||||||
|
<Skeleton className="ml-auto h-7 w-7 rounded-md" />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</PageShell>
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
**SettingsPage.tsx changes:**
|
||||||
|
|
||||||
|
1. **Import PageShell and Skeleton:** Add `import { PageShell } from "@/components/shared/PageShell"` and `import { Skeleton } from "@/components/ui/skeleton"`.
|
||||||
|
|
||||||
|
2. **Remove duplicate heading:** Delete the `<h1 className="mb-6 text-2xl font-semibold">{t("settings.title")}</h1>` on line 67. This creates a double heading since the Card below also has a CardTitle with "Settings".
|
||||||
|
|
||||||
|
3. **Wrap with PageShell:** Replace the `<div className="max-w-lg">` root with:
|
||||||
|
```tsx
|
||||||
|
<PageShell title={t("settings.title")}>
|
||||||
|
<div className="max-w-lg">
|
||||||
|
<Card>
|
||||||
|
{/* Remove CardHeader with CardTitle since PageShell provides the title.
|
||||||
|
Keep CardContent as-is. */}
|
||||||
|
<CardContent className="space-y-4 pt-6">
|
||||||
|
{/* existing form fields unchanged */}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</PageShell>
|
||||||
|
```
|
||||||
|
Remove the CardHeader and CardTitle entirely -- PageShell provides the page-level title, and the Card should just contain the form. Add `pt-6` to CardContent's className since without CardHeader the content needs top padding.
|
||||||
|
|
||||||
|
4. **Skeleton loading:** Replace `if (loading) return null` with:
|
||||||
|
```tsx
|
||||||
|
if (loading) return (
|
||||||
|
<PageShell title={t("settings.title")}>
|
||||||
|
<div className="max-w-lg">
|
||||||
|
<Card>
|
||||||
|
<CardContent className="space-y-4 pt-6">
|
||||||
|
{[1, 2, 3].map((i) => (
|
||||||
|
<div key={i} className="space-y-2">
|
||||||
|
<Skeleton className="h-4 w-24" />
|
||||||
|
<Skeleton className="h-10 w-full" />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<Skeleton className="h-10 w-20" />
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</PageShell>
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Clean up unused imports:** After removing CardHeader and CardTitle usage, update the import to: `import { Card, CardContent } from "@/components/ui/card"`. Remove `CardHeader` and `CardTitle` from the import.
|
||||||
|
|
||||||
|
**No i18n changes needed for this task.** QuickAdd and Settings pages already have all required translation keys.
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
<automated>cd /home/jlmak/Projects/jlmak/SimpleFinanceDash && bun run build</automated>
|
||||||
|
</verify>
|
||||||
|
<done>QuickAddPage uses PageShell with title and action button, shows skeleton on load. SettingsPage uses PageShell with no double "Settings" heading, Card contains only the form, shows skeleton on load. No `return null` loading patterns remain in either file. Build passes.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
- `bun run build` compiles without TypeScript errors
|
||||||
|
- `grep -c "return null" src/pages/CategoriesPage.tsx src/pages/TemplatePage.tsx src/pages/QuickAddPage.tsx src/pages/SettingsPage.tsx` returns 0 for all files
|
||||||
|
- `grep -c "size-3 rounded-full" src/pages/CategoriesPage.tsx src/pages/TemplatePage.tsx` returns 0 for both (old dot headers removed)
|
||||||
|
- `grep -c "border-l-4" src/pages/CategoriesPage.tsx src/pages/TemplatePage.tsx` returns at least 1 for each (new accent headers applied)
|
||||||
|
- `grep -c "PageShell" src/pages/CategoriesPage.tsx src/pages/QuickAddPage.tsx src/pages/SettingsPage.tsx` returns at least 1 for each
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<success_criteria>
|
||||||
|
- All four pages (Categories, Template, QuickAdd, Settings) show consistent PageShell-style headers
|
||||||
|
- All four pages show skeleton loading states instead of blank screens
|
||||||
|
- Categories and Template pages show left-border accent group headers
|
||||||
|
- Settings page has exactly ONE "Settings" heading (via PageShell), not two
|
||||||
|
- `bun run build` passes
|
||||||
|
- No `return null` loading patterns remain in any of the four files
|
||||||
|
</success_criteria>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
After completion, create `.planning/phases/04-full-app-design-consistency/04-02-SUMMARY.md`
|
||||||
|
</output>
|
||||||
448
.planning/phases/04-full-app-design-consistency/04-03-PLAN.md
Normal file
448
.planning/phases/04-full-app-design-consistency/04-03-PLAN.md
Normal file
@@ -0,0 +1,448 @@
|
|||||||
|
---
|
||||||
|
phase: 04-full-app-design-consistency
|
||||||
|
plan: 03
|
||||||
|
type: execute
|
||||||
|
wave: 3
|
||||||
|
depends_on: [04-02]
|
||||||
|
files_modified:
|
||||||
|
- src/pages/BudgetListPage.tsx
|
||||||
|
- src/pages/BudgetDetailPage.tsx
|
||||||
|
- src/i18n/en.json
|
||||||
|
- src/i18n/de.json
|
||||||
|
autonomous: true
|
||||||
|
requirements: [UI-BUDGETS-01, UI-RESPONSIVE-01, UI-DESIGN-01]
|
||||||
|
|
||||||
|
must_haves:
|
||||||
|
truths:
|
||||||
|
- "BudgetList page uses PageShell for header with title and New Budget button"
|
||||||
|
- "BudgetList page shows locale-aware month names (German month names when locale is de)"
|
||||||
|
- "BudgetList page shows skeleton loading state instead of blank screen"
|
||||||
|
- "BudgetList dialog month/year labels are translated (not hardcoded English)"
|
||||||
|
- "BudgetDetail page uses PageShell with locale-aware month heading"
|
||||||
|
- "BudgetDetail page shows left-border accent group headers matching dashboard style"
|
||||||
|
- "BudgetDetail page uses semantic color tokens (text-over-budget/text-on-budget) instead of text-green-600/text-red-600"
|
||||||
|
- "BudgetDetail page uses direction-aware diff logic (spending over when actual > budgeted; income/saving/investment over when actual < budgeted)"
|
||||||
|
- "BudgetDetail page shows skeleton loading state instead of blank screen"
|
||||||
|
- "No hardcoded 'en' locale string remains in any budget page"
|
||||||
|
- "Navigating between all pages produces no jarring visual discontinuity"
|
||||||
|
artifacts:
|
||||||
|
- path: "src/pages/BudgetListPage.tsx"
|
||||||
|
provides: "PageShell adoption, locale-aware months, skeleton, i18n labels"
|
||||||
|
contains: "PageShell"
|
||||||
|
- path: "src/pages/BudgetDetailPage.tsx"
|
||||||
|
provides: "PageShell, semantic tokens, direction-aware diff, group headers, skeleton"
|
||||||
|
contains: "text-over-budget"
|
||||||
|
- path: "src/i18n/en.json"
|
||||||
|
provides: "Budget month/year dialog labels and group total i18n key"
|
||||||
|
contains: "budgets.month"
|
||||||
|
- path: "src/i18n/de.json"
|
||||||
|
provides: "German budget translations"
|
||||||
|
contains: "budgets.month"
|
||||||
|
key_links:
|
||||||
|
- from: "src/pages/BudgetDetailPage.tsx"
|
||||||
|
to: "semantic CSS tokens"
|
||||||
|
via: "text-over-budget / text-on-budget classes"
|
||||||
|
pattern: "text-over-budget|text-on-budget"
|
||||||
|
- from: "src/pages/BudgetListPage.tsx"
|
||||||
|
to: "i18n.language"
|
||||||
|
via: "Intl.DateTimeFormat locale parameter"
|
||||||
|
pattern: "Intl\\.DateTimeFormat"
|
||||||
|
- from: "src/pages/BudgetDetailPage.tsx"
|
||||||
|
to: "i18n.language"
|
||||||
|
via: "Intl.DateTimeFormat locale parameter"
|
||||||
|
pattern: "Intl\\.DateTimeFormat"
|
||||||
|
---
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
Upgrade BudgetListPage and BudgetDetailPage with PageShell, semantic color tokens, direction-aware diff logic, locale-aware month formatting, and skeleton loading states.
|
||||||
|
|
||||||
|
Purpose: These are the most complex pages in the app. BudgetDetailPage currently uses hardcoded `text-green-600`/`text-red-600` color classes that bypass the design token system, a simplified `isIncome` boolean that mishandles saving/investment types, and a hardcoded `"en"` locale for month formatting. BudgetListPage has a hardcoded English MONTHS array. This plan migrates both to the established design system patterns from Phases 1-3.
|
||||||
|
|
||||||
|
Output: Two fully upgraded budget pages with consistent visual language, correct semantic tokens, and locale-aware formatting.
|
||||||
|
</objective>
|
||||||
|
|
||||||
|
<execution_context>
|
||||||
|
@/home/jlmak/.claude/get-shit-done/workflows/execute-plan.md
|
||||||
|
@/home/jlmak/.claude/get-shit-done/templates/summary.md
|
||||||
|
</execution_context>
|
||||||
|
|
||||||
|
<context>
|
||||||
|
@.planning/PROJECT.md
|
||||||
|
@.planning/ROADMAP.md
|
||||||
|
@.planning/STATE.md
|
||||||
|
@.planning/phases/04-full-app-design-consistency/04-CONTEXT.md
|
||||||
|
@.planning/phases/04-full-app-design-consistency/04-RESEARCH.md
|
||||||
|
|
||||||
|
<interfaces>
|
||||||
|
From src/components/shared/PageShell.tsx:
|
||||||
|
```tsx
|
||||||
|
interface PageShellProps {
|
||||||
|
title: string
|
||||||
|
description?: string
|
||||||
|
action?: React.ReactNode
|
||||||
|
children: React.ReactNode
|
||||||
|
}
|
||||||
|
export function PageShell({ title, description, action, children }: PageShellProps)
|
||||||
|
```
|
||||||
|
|
||||||
|
From src/components/dashboard/CategorySection.tsx (direction-aware diff logic to replicate):
|
||||||
|
```tsx
|
||||||
|
const SPENDING_TYPES: CategoryType[] = ["bill", "variable_expense", "debt"]
|
||||||
|
|
||||||
|
function isSpendingType(type: CategoryType): boolean {
|
||||||
|
return SPENDING_TYPES.includes(type)
|
||||||
|
}
|
||||||
|
|
||||||
|
function computeDiff(budgeted: number, actual: number, type: CategoryType): { diff: number; isOver: boolean } {
|
||||||
|
if (isSpendingType(type)) {
|
||||||
|
return { diff: budgeted - actual, isOver: actual > budgeted }
|
||||||
|
}
|
||||||
|
return { diff: actual - budgeted, isOver: actual < budgeted }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Semantic color classes (from index.css Phase 1):
|
||||||
|
- `text-over-budget` -- red, for amounts exceeding budget
|
||||||
|
- `text-on-budget` -- green, for amounts within budget
|
||||||
|
- `text-muted-foreground` -- neutral, for zero difference
|
||||||
|
|
||||||
|
Group header pattern (established in Plan 02):
|
||||||
|
```tsx
|
||||||
|
<div
|
||||||
|
className="mb-2 flex items-center gap-3 rounded-sm border-l-4 bg-muted/30 px-3 py-2"
|
||||||
|
style={{ borderLeftColor: categoryColors[type] }}
|
||||||
|
>
|
||||||
|
<span className="text-sm font-semibold">{t(`categories.types.${type}`)}</span>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
Locale-aware month formatting pattern:
|
||||||
|
```tsx
|
||||||
|
const { i18n } = useTranslation()
|
||||||
|
const locale = i18n.language
|
||||||
|
// Replace hardcoded MONTHS array:
|
||||||
|
const monthItems = useMemo(
|
||||||
|
() => Array.from({ length: 12 }, (_, i) => ({
|
||||||
|
value: i + 1,
|
||||||
|
label: new Intl.DateTimeFormat(locale, { month: "long" }).format(new Date(2000, i, 1)),
|
||||||
|
})),
|
||||||
|
[locale]
|
||||||
|
)
|
||||||
|
// Replace hardcoded "en" in toLocaleDateString:
|
||||||
|
function budgetHeading(startDate: string, locale: string): string {
|
||||||
|
const [year, month] = startDate.split("-").map(Number)
|
||||||
|
return new Intl.DateTimeFormat(locale, { month: "long", year: "numeric" }).format(
|
||||||
|
new Date(year ?? 0, (month ?? 1) - 1, 1)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
</interfaces>
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<tasks>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 1: Upgrade BudgetListPage with PageShell, locale-aware months, skeleton, and i18n labels</name>
|
||||||
|
<files>src/pages/BudgetListPage.tsx, src/i18n/en.json, src/i18n/de.json</files>
|
||||||
|
<action>
|
||||||
|
**BudgetListPage.tsx changes:**
|
||||||
|
|
||||||
|
1. **Import PageShell, Skeleton, useMemo:** Add:
|
||||||
|
```tsx
|
||||||
|
import { useState, useMemo } from "react"
|
||||||
|
import { PageShell } from "@/components/shared/PageShell"
|
||||||
|
import { Skeleton } from "@/components/ui/skeleton"
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Remove hardcoded MONTHS array:** Delete the entire `const MONTHS = [...]` constant (lines 36-49).
|
||||||
|
|
||||||
|
3. **Add locale-aware month generation:** Inside the component, after the existing hooks and state, add:
|
||||||
|
```tsx
|
||||||
|
const { t, i18n } = useTranslation()
|
||||||
|
const locale = i18n.language
|
||||||
|
|
||||||
|
const monthItems = useMemo(
|
||||||
|
() =>
|
||||||
|
Array.from({ length: 12 }, (_, i) => ({
|
||||||
|
value: i + 1,
|
||||||
|
label: new Intl.DateTimeFormat(locale, { month: "long" }).format(
|
||||||
|
new Date(2000, i, 1)
|
||||||
|
),
|
||||||
|
})),
|
||||||
|
[locale]
|
||||||
|
)
|
||||||
|
```
|
||||||
|
Update the existing `useTranslation()` call to also destructure `i18n`: change `const { t } = useTranslation()` to `const { t, i18n } = useTranslation()`.
|
||||||
|
|
||||||
|
**Rules of Hooks:** The `useMemo` must be declared BEFORE the `if (loading)` check. Since `useTranslation` is already before it, just place `useMemo` right after the state declarations and before `if (loading)`.
|
||||||
|
|
||||||
|
4. **Fix budgetLabel to use locale:** Replace the `budgetLabel` helper function to use locale:
|
||||||
|
```tsx
|
||||||
|
function budgetLabel(budget: Budget, locale: string): string {
|
||||||
|
const [year, month] = budget.start_date.split("-").map(Number)
|
||||||
|
return new Intl.DateTimeFormat(locale, { month: "long", year: "numeric" }).format(
|
||||||
|
new Date(year ?? 0, (month ?? 1) - 1, 1)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Update all call sites to pass `locale`: `budgetLabel(budget, locale)` and `budgetLabel(result, locale)`.
|
||||||
|
|
||||||
|
5. **Replace MONTHS usage in dialog:** In the month Select, replace `MONTHS.map((m) =>` with `monthItems.map((m) =>`. The shape is identical (`{ value, label }`).
|
||||||
|
|
||||||
|
6. **Replace hardcoded "Month" and "Year" labels:** Replace the `<Label>Month</Label>` and `<Label>Year</Label>` in the new budget dialog with:
|
||||||
|
```tsx
|
||||||
|
<Label>{t("budgets.month")}</Label>
|
||||||
|
// and
|
||||||
|
<Label>{t("budgets.year")}</Label>
|
||||||
|
```
|
||||||
|
|
||||||
|
7. **Replace header with PageShell:** Remove the `<div className="mb-6 flex items-center justify-between">` header block. Wrap the return in:
|
||||||
|
```tsx
|
||||||
|
<PageShell
|
||||||
|
title={t("budgets.title")}
|
||||||
|
action={
|
||||||
|
<Button onClick={openDialog} size="sm">
|
||||||
|
<Plus className="mr-1 size-4" />
|
||||||
|
{t("budgets.newBudget")}
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{/* empty state + table + dialog */}
|
||||||
|
</PageShell>
|
||||||
|
```
|
||||||
|
|
||||||
|
8. **Skeleton loading:** Replace `if (loading) return null` with:
|
||||||
|
```tsx
|
||||||
|
if (loading) return (
|
||||||
|
<PageShell title={t("budgets.title")}>
|
||||||
|
<div className="space-y-1">
|
||||||
|
{[1, 2, 3, 4].map((i) => (
|
||||||
|
<div key={i} className="flex items-center gap-4 px-4 py-3 border-b border-border">
|
||||||
|
<Skeleton className="h-4 w-40" />
|
||||||
|
<Skeleton className="h-4 w-12" />
|
||||||
|
<Skeleton className="ml-auto h-4 w-4" />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</PageShell>
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
**i18n additions (en.json):** Add inside the "budgets" object:
|
||||||
|
```json
|
||||||
|
"month": "Month",
|
||||||
|
"year": "Year",
|
||||||
|
"total": "{{label}} Total"
|
||||||
|
```
|
||||||
|
|
||||||
|
**i18n additions (de.json):** Add inside the "budgets" object:
|
||||||
|
```json
|
||||||
|
"month": "Monat",
|
||||||
|
"year": "Jahr",
|
||||||
|
"total": "{{label}} Gesamt"
|
||||||
|
```
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
<automated>cd /home/jlmak/Projects/jlmak/SimpleFinanceDash && bun run build</automated>
|
||||||
|
</verify>
|
||||||
|
<done>BudgetListPage uses PageShell, shows locale-aware month names via Intl.DateTimeFormat (no hardcoded English MONTHS array), dialog labels use i18n keys, skeleton replaces null loading state, budgetLabel uses i18n.language locale. Both en.json and de.json have month/year/total keys. Build passes.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 2: Upgrade BudgetDetailPage with semantic tokens, direction-aware diff, PageShell, group headers, and skeleton</name>
|
||||||
|
<files>src/pages/BudgetDetailPage.tsx</files>
|
||||||
|
<action>
|
||||||
|
**BudgetDetailPage.tsx changes:**
|
||||||
|
|
||||||
|
1. **Import additions:** Add:
|
||||||
|
```tsx
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import { PageShell } from "@/components/shared/PageShell"
|
||||||
|
import { Skeleton } from "@/components/ui/skeleton"
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Add direction-aware diff logic:** At module level (above the component), add the same SPENDING_TYPES pattern from CategorySection:
|
||||||
|
```tsx
|
||||||
|
const SPENDING_TYPES: CategoryType[] = ["bill", "variable_expense", "debt"]
|
||||||
|
|
||||||
|
function isSpendingType(type: CategoryType): boolean {
|
||||||
|
return SPENDING_TYPES.includes(type)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Rewrite DifferenceCell:** Replace the entire DifferenceCell component. Change its props: remove `isIncome`, add `type: CategoryType`:
|
||||||
|
```tsx
|
||||||
|
function DifferenceCell({
|
||||||
|
budgeted,
|
||||||
|
actual,
|
||||||
|
currency,
|
||||||
|
type,
|
||||||
|
}: {
|
||||||
|
budgeted: number
|
||||||
|
actual: number
|
||||||
|
currency: string
|
||||||
|
type: CategoryType
|
||||||
|
}) {
|
||||||
|
const isOver = isSpendingType(type)
|
||||||
|
? actual > budgeted
|
||||||
|
: actual < budgeted
|
||||||
|
const diff = isSpendingType(type)
|
||||||
|
? budgeted - actual
|
||||||
|
: actual - budgeted
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableCell
|
||||||
|
className={cn(
|
||||||
|
"text-right tabular-nums",
|
||||||
|
isOver ? "text-over-budget" : diff !== 0 ? "text-on-budget" : "text-muted-foreground"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{formatCurrency(Math.abs(diff), currency)}
|
||||||
|
{diff < 0 ? " over" : ""}
|
||||||
|
</TableCell>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Update DifferenceCell call sites:** In the grouped.map render:
|
||||||
|
- Remove the `const isIncome = type === "income"` line.
|
||||||
|
- Change `<DifferenceCell budgeted={...} actual={...} currency={currency} isIncome={isIncome} />` to `<DifferenceCell budgeted={...} actual={...} currency={currency} type={type} />` in BOTH places (per-item row and group footer).
|
||||||
|
|
||||||
|
5. **Remove TierBadge from BudgetDetailPage:** Per research recommendation, remove the tier column from BudgetDetailPage to reduce visual noise and align with CategorySection display. This is Claude's discretion per CONTEXT.md.
|
||||||
|
- Remove the TierBadge component definition from BudgetDetailPage (keep it in TemplatePage where it belongs).
|
||||||
|
- Remove the `<TableHead>{t("categories.type")}</TableHead>` column from the table header.
|
||||||
|
- Remove the `<TableCell><TierBadge tier={item.item_tier} /></TableCell>` from each table row.
|
||||||
|
- Update the TableFooter `colSpan` accordingly: the first footer cell changes from `colSpan={2}` to no colSpan (or `colSpan={1}`), and the last footer cell changes appropriately.
|
||||||
|
- Remove the `Badge` import if no longer used elsewhere in this file.
|
||||||
|
|
||||||
|
6. **Group header upgrade:** Replace the dot+h2 pattern in grouped.map with:
|
||||||
|
```tsx
|
||||||
|
<div
|
||||||
|
className="mb-2 flex items-center gap-3 rounded-sm border-l-4 bg-muted/30 px-3 py-2"
|
||||||
|
style={{ borderLeftColor: categoryColors[type] }}
|
||||||
|
>
|
||||||
|
<span className="text-sm font-semibold">{t(`categories.types.${type}`)}</span>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
7. **Fix locale for headingLabel:** Update the `headingLabel` function. Destructure `i18n` from `useTranslation`: change `const { t } = useTranslation()` to `const { t, i18n } = useTranslation()`. Then:
|
||||||
|
```tsx
|
||||||
|
function headingLabel(): string {
|
||||||
|
if (!budget) return ""
|
||||||
|
const [year, month] = budget.start_date.split("-").map(Number)
|
||||||
|
return new Intl.DateTimeFormat(i18n.language, { month: "long", year: "numeric" }).format(
|
||||||
|
new Date(year ?? 0, (month ?? 1) - 1, 1)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
8. **Fix overall totals section:** The overall totals box at the bottom uses hardcoded `text-green-600`/`text-red-600`. Replace with semantic tokens:
|
||||||
|
```tsx
|
||||||
|
<p
|
||||||
|
className={cn(
|
||||||
|
"text-lg font-semibold tabular-nums",
|
||||||
|
totalBudgeted - totalActual >= 0 ? "text-on-budget" : "text-over-budget"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
```
|
||||||
|
This replaces the inline ternary with `text-green-600 dark:text-green-400` / `text-red-600 dark:text-red-400`.
|
||||||
|
|
||||||
|
9. **Fix group footer "Total" label:** The group footer currently has hardcoded English ` Total`:
|
||||||
|
```tsx
|
||||||
|
<TableCell colSpan={2} className="font-medium">
|
||||||
|
{t(`categories.types.${type}`)} Total
|
||||||
|
</TableCell>
|
||||||
|
```
|
||||||
|
Replace with i18n:
|
||||||
|
```tsx
|
||||||
|
<TableCell className="font-medium">
|
||||||
|
{t("budgets.total", { label: t(`categories.types.${type}`) })}
|
||||||
|
</TableCell>
|
||||||
|
```
|
||||||
|
The `budgets.total` key was added in Task 1's i18n step: `"total": "{{label}} Total"` / `"total": "{{label}} Gesamt"`.
|
||||||
|
|
||||||
|
10. **Replace header with PageShell:** Replace the back link + header section. Keep the back link as a child of PageShell:
|
||||||
|
```tsx
|
||||||
|
<PageShell
|
||||||
|
title={headingLabel()}
|
||||||
|
action={
|
||||||
|
<Button onClick={openAddDialog} size="sm">
|
||||||
|
<Plus className="mr-1 size-4" />
|
||||||
|
{t("budgets.addItem")}
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Link
|
||||||
|
to="/budgets"
|
||||||
|
className="-mt-4 inline-flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground"
|
||||||
|
>
|
||||||
|
<ArrowLeft className="size-4" />
|
||||||
|
{t("budgets.title")}
|
||||||
|
</Link>
|
||||||
|
{/* rest of content */}
|
||||||
|
</PageShell>
|
||||||
|
```
|
||||||
|
The `-mt-4` on the back link compensates for PageShell's `gap-6`, pulling it closer to the header.
|
||||||
|
|
||||||
|
11. **Skeleton loading:** Replace `if (loading) return null` with:
|
||||||
|
```tsx
|
||||||
|
if (loading) return (
|
||||||
|
<PageShell title="">
|
||||||
|
<div className="space-y-6">
|
||||||
|
<Skeleton className="h-4 w-24" />
|
||||||
|
{[1, 2, 3].map((i) => (
|
||||||
|
<div key={i} className="space-y-2">
|
||||||
|
<div className="flex items-center gap-3 rounded-sm border-l-4 border-muted bg-muted/30 px-3 py-2">
|
||||||
|
<Skeleton className="h-4 w-28" />
|
||||||
|
</div>
|
||||||
|
{[1, 2].map((j) => (
|
||||||
|
<div key={j} className="flex items-center gap-4 px-4 py-2.5 border-b border-border">
|
||||||
|
<Skeleton className="h-4 w-32" />
|
||||||
|
<Skeleton className="ml-auto h-4 w-20" />
|
||||||
|
<Skeleton className="h-4 w-20" />
|
||||||
|
<Skeleton className="h-4 w-16" />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<Skeleton className="h-20 w-full rounded-md" />
|
||||||
|
</div>
|
||||||
|
</PageShell>
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
**IMPORTANT VERIFICATION after changes:** Ensure NO instances of `text-green-600`, `text-red-600`, `text-green-400`, or `text-red-400` remain in BudgetDetailPage.tsx. All color coding must use `text-over-budget`, `text-on-budget`, or `text-muted-foreground`.
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
<automated>cd /home/jlmak/Projects/jlmak/SimpleFinanceDash && bun run build && grep -c "text-green-600\|text-red-600\|text-green-400\|text-red-400" src/pages/BudgetDetailPage.tsx || echo "CLEAN: no hardcoded color classes"</automated>
|
||||||
|
</verify>
|
||||||
|
<done>BudgetDetailPage uses semantic color tokens (text-over-budget/text-on-budget) with zero instances of text-green-600 or text-red-600. Direction-aware diff logic handles all 6 category types correctly (spending types over when actual > budgeted, income/saving/investment over when actual < budgeted). Left-border accent group headers replace dot headers. Tier badge column removed for cleaner display. Locale-aware month heading. Skeleton loading state. PageShell wraps the page. Overall totals box uses semantic tokens. Group footer total label uses i18n interpolation. Build passes.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
- `bun run build` compiles without TypeScript errors
|
||||||
|
- `bun run lint` passes (or pre-existing errors only)
|
||||||
|
- `grep -c "text-green-600\|text-red-600" src/pages/BudgetDetailPage.tsx` returns 0 (semantic tokens only)
|
||||||
|
- `grep -c "text-over-budget\|text-on-budget" src/pages/BudgetDetailPage.tsx` returns at least 2
|
||||||
|
- `grep -c "return null" src/pages/BudgetListPage.tsx src/pages/BudgetDetailPage.tsx` returns 0 for both
|
||||||
|
- `grep -c 'toLocaleDateString("en"' src/pages/BudgetDetailPage.tsx src/pages/BudgetListPage.tsx` returns 0 (no hardcoded English locale)
|
||||||
|
- `grep -c "Intl.DateTimeFormat" src/pages/BudgetListPage.tsx src/pages/BudgetDetailPage.tsx` returns at least 1 for each
|
||||||
|
- `grep -c "PageShell" src/pages/BudgetListPage.tsx src/pages/BudgetDetailPage.tsx` returns at least 1 for each
|
||||||
|
- `grep "budgets.month" src/i18n/en.json src/i18n/de.json` returns matches in both
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<success_criteria>
|
||||||
|
- BudgetListPage: PageShell header, locale-aware month names in dialog and table, skeleton loading, i18n month/year labels
|
||||||
|
- BudgetDetailPage: PageShell header, semantic color tokens (no hardcoded green/red), direction-aware diff for all 6 category types, left-border accent group headers, no tier column, locale-aware heading, skeleton loading, i18n group total label
|
||||||
|
- No hardcoded English locale strings ("en") remain in budget page formatting
|
||||||
|
- No hardcoded Tailwind color classes (text-green-600, text-red-600) remain
|
||||||
|
- All 9 app pages now use consistent header layout (PageShell or equivalent)
|
||||||
|
- German locale shows fully translated text on both pages
|
||||||
|
- `bun run build` passes
|
||||||
|
</success_criteria>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
After completion, create `.planning/phases/04-full-app-design-consistency/04-03-SUMMARY.md`
|
||||||
|
</output>
|
||||||
Reference in New Issue
Block a user