chore: archive v1.0 phase directories

This commit is contained in:
2026-03-24 09:46:00 +01:00
parent 3a771ba7cd
commit 439d0e950d
35 changed files with 0 additions and 0 deletions

View 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>