18 KiB
phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
| phase | plan | type | wave | depends_on | files_modified | autonomous | requirements | must_haves | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 04-full-app-design-consistency | 03 | execute | 3 |
|
|
true |
|
|
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.
<execution_context> @/home/jlmak/.claude/get-shit-done/workflows/execute-plan.md @/home/jlmak/.claude/get-shit-done/templates/summary.md </execution_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 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):
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 budgettext-on-budget-- green, for amounts within budgettext-muted-foreground-- neutral, for zero difference
Group header pattern (established in Plan 02):
<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:
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)
)
}
-
Import PageShell, Skeleton, useMemo: Add:
import { useState, useMemo } from "react" import { PageShell } from "@/components/shared/PageShell" import { Skeleton } from "@/components/ui/skeleton" -
Remove hardcoded MONTHS array: Delete the entire
const MONTHS = [...]constant (lines 36-49). -
Add locale-aware month generation: Inside the component, after the existing hooks and state, add:
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 destructurei18n: changeconst { t } = useTranslation()toconst { t, i18n } = useTranslation().Rules of Hooks: The
useMemomust be declared BEFORE theif (loading)check. SinceuseTranslationis already before it, just placeuseMemoright after the state declarations and beforeif (loading). -
Fix budgetLabel to use locale: Replace the
budgetLabelhelper function to use locale: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)andbudgetLabel(result, locale). -
Replace MONTHS usage in dialog: In the month Select, replace
MONTHS.map((m) =>withmonthItems.map((m) =>. The shape is identical ({ value, label }). -
Replace hardcoded "Month" and "Year" labels: Replace the
<Label>Month</Label>and<Label>Year</Label>in the new budget dialog with:<Label>{t("budgets.month")}</Label> // and <Label>{t("budgets.year")}</Label> -
Replace header with PageShell: Remove the
<div className="mb-6 flex items-center justify-between">header block. Wrap the return in:<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> -
Skeleton loading: Replace
if (loading) return nullwith: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:
"month": "Month",
"year": "Year",
"total": "{{label}} Total"
i18n additions (de.json): Add inside the "budgets" object:
"month": "Monat",
"year": "Jahr",
"total": "{{label}} Gesamt"
-
Import additions: Add:
import { cn } from "@/lib/utils" import { PageShell } from "@/components/shared/PageShell" import { Skeleton } from "@/components/ui/skeleton" -
Add direction-aware diff logic: At module level (above the component), add the same SPENDING_TYPES pattern from CategorySection:
const SPENDING_TYPES: CategoryType[] = ["bill", "variable_expense", "debt"] function isSpendingType(type: CategoryType): boolean { return SPENDING_TYPES.includes(type) } -
Rewrite DifferenceCell: Replace the entire DifferenceCell component. Change its props: remove
isIncome, addtype: CategoryType: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> ) } -
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).
- Remove the
-
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
colSpanaccordingly: the first footer cell changes fromcolSpan={2}to no colSpan (orcolSpan={1}), and the last footer cell changes appropriately. - Remove the
Badgeimport if no longer used elsewhere in this file.
-
Group header upgrade: Replace the dot+h2 pattern in grouped.map with:
<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> -
Fix locale for headingLabel: Update the
headingLabelfunction. Destructurei18nfromuseTranslation: changeconst { t } = useTranslation()toconst { t, i18n } = useTranslation(). Then: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) ) } -
Fix overall totals section: The overall totals box at the bottom uses hardcoded
text-green-600/text-red-600. Replace with semantic tokens:<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. -
Fix group footer "Total" label: The group footer currently has hardcoded English
Total:<TableCell colSpan={2} className="font-medium"> {t(`categories.types.${type}`)} Total </TableCell>Replace with i18n:
<TableCell className="font-medium"> {t("budgets.total", { label: t(`categories.types.${type}`) })} </TableCell>The
budgets.totalkey was added in Task 1's i18n step:"total": "{{label}} Total"/"total": "{{label}} Gesamt". -
Replace header with PageShell: Replace the back link + header section. Keep the back link as a child of PageShell:
<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-4on the back link compensates for PageShell'sgap-6, pulling it closer to the header. -
Skeleton loading: Replace
if (loading) return nullwith: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.
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"
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.
<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 buildpasses </success_criteria>