chore: archive v1.0 phase directories
This commit is contained in:
@@ -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