docs(03-interaction-quality-and-completeness): create phase plan

This commit is contained in:
2026-03-11 22:18:00 +01:00
parent c4ab29cb66
commit 0d40043615
4 changed files with 666 additions and 2 deletions

View File

@@ -59,7 +59,11 @@ Plans:
3. After saving an inline edit, the row briefly flashes a confirmation color, confirming the save completed 3. After saving an inline edit, the row briefly flashes a confirmation color, confirming the save completed
4. Attempting to delete a category triggers a confirmation dialog before the deletion executes 4. Attempting to delete a category triggers a confirmation dialog before the deletion executes
5. A user with no budgets sees a designed empty state with a clear CTA on the dashboard; a user with no categories sees the same on the categories page; loading skeletons use pastel-tinted backgrounds matching their section 5. A user with no budgets sees a designed empty state with a clear CTA on the dashboard; a user with no categories sees the same on the categories page; loading skeletons use pastel-tinted backgrounds matching their section
**Plans**: TBD **Plans:** 3 plans
Plans:
- [ ] 03-01-PLAN.md — InlineEditCell pencil icon + save callbacks, form submit spinners
- [ ] 03-02-PLAN.md — Delete confirmation dialog, empty states for Dashboard and Categories
- [ ] 03-03-PLAN.md — Row flash wiring in trackers, pastel-tinted loading skeletons
### Phase 4: Chart Polish and Bug Fixes ### Phase 4: Chart Polish and Bug Fixes
**Goal**: Charts look polished and informative with semantic category colors, correctly formatted currency tooltips, and the currency locale bug fixed so values display in the user's preferred locale **Goal**: Charts look polished and informative with semantic category colors, correctly formatted currency tooltips, and the currency locale bug fixed so values display in the user's preferred locale
@@ -80,5 +84,5 @@ Phases execute in numeric order: 1 → 2 → 3 → 4
|-------|----------------|--------|-----------| |-------|----------------|--------|-----------|
| 1. Design Token Foundation | 2/2 | Complete | 2026-03-11 | | 1. Design Token Foundation | 2/2 | Complete | 2026-03-11 |
| 2. Layout and Brand Identity | 0/2 | In progress | - | | 2. Layout and Brand Identity | 0/2 | In progress | - |
| 3. Interaction Quality and Completeness | 0/TBD | Not started | - | | 3. Interaction Quality and Completeness | 0/3 | Not started | - |
| 4. Chart Polish and Bug Fixes | 0/TBD | Not started | - | | 4. Chart Polish and Bug Fixes | 0/TBD | Not started | - |

View File

@@ -0,0 +1,179 @@
---
phase: 03-interaction-quality-and-completeness
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
- frontend/src/components/InlineEditCell.tsx
- frontend/src/components/InlineEditCell.test.tsx
- frontend/src/pages/LoginPage.tsx
- frontend/src/pages/RegisterPage.tsx
- frontend/src/components/BudgetSetup.tsx
autonomous: true
requirements: [IXTN-01, IXTN-02, IXTN-03]
must_haves:
truths:
- "Hovering over an inline-editable cell reveals a pencil icon that fades in"
- "After a successful inline save, the onSaveSuccess callback fires so parents can flash the row"
- "After a failed inline save, the value reverts and onSaveError callback fires"
- "Login submit button shows spinner and is disabled while request is in flight"
- "Register submit button shows spinner and is disabled while request is in flight"
- "Budget create button shows spinner and is disabled while saving"
artifacts:
- path: "frontend/src/components/InlineEditCell.tsx"
provides: "Pencil icon hover, onSaveSuccess/onSaveError callbacks, try/catch in handleBlur"
contains: "Pencil"
- path: "frontend/src/components/InlineEditCell.test.tsx"
provides: "Tests for pencil icon presence, save callbacks, error revert"
contains: "onSaveSuccess"
- path: "frontend/src/pages/LoginPage.tsx"
provides: "Spinner in submit button during loading"
contains: "Spinner"
- path: "frontend/src/pages/RegisterPage.tsx"
provides: "Spinner in submit button during loading"
contains: "Spinner"
- path: "frontend/src/components/BudgetSetup.tsx"
provides: "Spinner in create button during saving"
contains: "Spinner"
key_links:
- from: "frontend/src/components/InlineEditCell.tsx"
to: "parent components (BillsTracker, etc.)"
via: "onSaveSuccess/onSaveError callback props"
pattern: "onSaveSuccess\\?\\(\\)"
- from: "frontend/src/pages/LoginPage.tsx"
to: "ui/spinner.tsx"
via: "Spinner import"
pattern: "import.*Spinner"
---
<objective>
Add pencil icon hover affordance and save/error callbacks to InlineEditCell, plus loading spinners to all four form submit buttons.
Purpose: Make inline editing discoverable (pencil icon on hover) and prepare the callback interface for row-level flash feedback in downstream plans. Make form submissions feel responsive with spinner indicators.
Output: Enhanced InlineEditCell with pencil + callbacks, spinner-enabled Login/Register/BudgetSetup forms.
</objective>
<execution_context>
@/home/jean-luc-makiola/.claude/get-shit-done/workflows/execute-plan.md
@/home/jean-luc-makiola/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/03-interaction-quality-and-completeness/03-CONTEXT.md
@.planning/phases/03-interaction-quality-and-completeness/03-RESEARCH.md
<interfaces>
<!-- InlineEditCell current interface (will be extended) -->
From frontend/src/components/InlineEditCell.tsx:
```typescript
interface InlineEditCellProps {
value: number
currency: string
onSave: (value: number) => Promise<void>
className?: string
}
```
From frontend/src/components/ui/spinner.tsx:
```typescript
export { Spinner } // SVG spinner component, accepts className
```
From frontend/src/lib/format.ts:
```typescript
export function formatCurrency(value: number, currency: string): string
```
</interfaces>
</context>
<tasks>
<task type="auto" tdd="true">
<name>Task 1: Enhance InlineEditCell with pencil icon, save/error callbacks, and try/catch</name>
<files>frontend/src/components/InlineEditCell.tsx, frontend/src/components/InlineEditCell.test.tsx</files>
<behavior>
- Test: Pencil icon element exists in display mode DOM (query by lucide test-id or role)
- Test: Pencil icon has opacity-0 class (hidden by default, visible on CSS hover — not testable in jsdom but DOM presence is)
- Test: When onSave resolves successfully, onSaveSuccess callback is called
- Test: When onSave rejects, value reverts to original and onSaveError callback is called
- Test: When parsed value equals current value, onSave is NOT called (existing behavior preserved)
</behavior>
<action>
Extend InlineEditCellProps with two optional callbacks:
```
onSaveSuccess?: () => void
onSaveError?: () => void
```
In the display-mode span:
- Add `group` class to the outer span
- Change span to `flex items-center justify-end gap-1`
- After the formatted value text, add: `<Pencil className="size-3 opacity-0 transition-opacity duration-150 group-hover:opacity-100 text-muted-foreground" />`
- Import `Pencil` from `lucide-react`
In handleBlur:
- Wrap the `await onSave(num)` in try/catch
- On success: call `onSaveSuccess?.()`
- On catch: revert inputValue to `String(value)`, call `onSaveError?.()`
- `setEditing(false)` remains in finally or after try/catch
Extend existing test file (InlineEditCell.test.tsx) with new test cases for the behaviors above. Use `vi.fn()` for the callback mocks. For error test, make onSave return `Promise.reject()`.
</action>
<verify>
<automated>cd /home/jean-luc-makiola/Development/projects/SimpleFinanceDash/frontend && bun vitest run src/components/InlineEditCell.test.tsx</automated>
</verify>
<done>InlineEditCell renders pencil icon in display mode, fires onSaveSuccess on successful save, fires onSaveError and reverts value on failed save. All tests pass.</done>
</task>
<task type="auto">
<name>Task 2: Add loading spinners to Login, Register, and BudgetSetup submit buttons</name>
<files>frontend/src/pages/LoginPage.tsx, frontend/src/pages/RegisterPage.tsx, frontend/src/components/BudgetSetup.tsx</files>
<action>
In each file, import Spinner: `import { Spinner } from '@/components/ui/spinner'`
**LoginPage.tsx** (line ~81):
- The submit Button already has `disabled={loading}`.
- Add `className="w-full min-w-[120px]"` (keep existing w-full, add min-w).
- Replace the button text content with: `{loading ? <Spinner /> : t('auth.login')}`
**RegisterPage.tsx** (line ~89):
- Same pattern as LoginPage. Button already has `disabled={loading}`.
- Add `min-w-[120px]` to className.
- Replace text with: `{loading ? <Spinner /> : t('auth.register')}`
**BudgetSetup.tsx** (line ~92):
- Button already has `disabled={saving || !name || !startDate || !endDate}`.
- Add `className="min-w-[120px]"` to Button.
- Replace button text with: `{saving ? <Spinner /> : t('budget.create')}`
Do NOT modify any other logic in these files — only the Button content and className.
</action>
<verify>
<automated>cd /home/jean-luc-makiola/Development/projects/SimpleFinanceDash/frontend && bun vitest run src/pages/LoginPage.test.tsx src/pages/RegisterPage.test.tsx && bun run build</automated>
</verify>
<done>All three form submit buttons show Spinner component when loading/saving state is true, buttons are disabled during loading, min-width prevents layout shift. Build passes with zero errors.</done>
</task>
</tasks>
<verification>
- `cd frontend && bun vitest run` — full test suite passes
- `cd frontend && bun run build` — production build succeeds with zero TypeScript errors
- InlineEditCell tests cover pencil icon, save success callback, save error + revert
</verification>
<success_criteria>
- Pencil icon renders in InlineEditCell display mode (opacity-0, visible on hover via CSS)
- onSaveSuccess fires after successful save; onSaveError fires and reverts value on failure
- Login, Register, BudgetSetup buttons show Spinner when loading, disabled to prevent double-submit
- All existing tests continue to pass; new tests cover the added behaviors
</success_criteria>
<output>
After completion, create `.planning/phases/03-interaction-quality-and-completeness/03-01-SUMMARY.md`
</output>

View File

@@ -0,0 +1,219 @@
---
phase: 03-interaction-quality-and-completeness
plan: 02
type: execute
wave: 1
depends_on: []
files_modified:
- frontend/src/pages/CategoriesPage.tsx
- frontend/src/pages/DashboardPage.tsx
- frontend/src/components/EmptyState.tsx
autonomous: true
requirements: [IXTN-05, STATE-01, STATE-02]
must_haves:
truths:
- "Clicking delete on a category opens a confirmation dialog, not an immediate delete"
- "Confirming delete executes the API call with spinner; cancelling closes the dialog"
- "If delete fails (ON DELETE RESTRICT), error message shows inline in the dialog"
- "Dashboard with no budgets shows an empty state with icon, heading, subtext, and Create CTA"
- "Categories page with no categories shows an empty state with Add CTA"
artifacts:
- path: "frontend/src/pages/CategoriesPage.tsx"
provides: "Delete confirmation dialog with spinner and error handling"
contains: "pendingDelete"
- path: "frontend/src/pages/DashboardPage.tsx"
provides: "Empty state when list is empty and not loading"
contains: "EmptyState"
- path: "frontend/src/components/EmptyState.tsx"
provides: "Shared empty state component with icon + heading + subtext + CTA"
exports: ["EmptyState"]
key_links:
- from: "frontend/src/pages/CategoriesPage.tsx"
to: "categories API delete endpoint"
via: "categoriesApi.delete in confirmDelete handler"
pattern: "categoriesApi\\.delete"
- from: "frontend/src/pages/DashboardPage.tsx"
to: "frontend/src/components/EmptyState.tsx"
via: "EmptyState import"
pattern: "import.*EmptyState"
---
<objective>
Add delete confirmation dialog to CategoriesPage and designed empty states to Dashboard and Categories pages.
Purpose: Prevent accidental category deletion with a confirmation step that handles backend constraints gracefully. Replace bare fallback content with designed empty states that guide users toward first actions.
Output: CategoriesPage with delete dialog, EmptyState shared component, empty states on Dashboard and Categories pages.
</objective>
<execution_context>
@/home/jean-luc-makiola/.claude/get-shit-done/workflows/execute-plan.md
@/home/jean-luc-makiola/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/03-interaction-quality-and-completeness/03-CONTEXT.md
@.planning/phases/03-interaction-quality-and-completeness/03-RESEARCH.md
<interfaces>
<!-- CategoriesPage current delete handler (will be replaced) -->
From frontend/src/pages/CategoriesPage.tsx:
```typescript
const handleDelete = async (id: string) => {
await categoriesApi.delete(id)
fetchCategories()
}
```
<!-- Dialog components already imported in CategoriesPage -->
From frontend/src/pages/CategoriesPage.tsx:
```typescript
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog'
```
<!-- DashboardPage current empty fallback (will be replaced) -->
From frontend/src/pages/DashboardPage.tsx:
```typescript
// Line 93-99: plain Card with text — replace with EmptyState
<Card>
<CardContent className="py-12 text-center text-muted-foreground">
{t('dashboard.noBudgets')}
</CardContent>
</Card>
```
<!-- API types -->
From frontend/src/lib/api.ts:
```typescript
export type CategoryType = 'income' | 'bill' | 'variable_expense' | 'debt' | 'saving' | 'investment'
export interface Category { id: string; name: string; type: CategoryType; sort_order: number }
```
From frontend/src/components/ui/spinner.tsx:
```typescript
export { Spinner }
```
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: Create shared EmptyState component and wire into Dashboard and Categories pages</name>
<files>frontend/src/components/EmptyState.tsx, frontend/src/pages/DashboardPage.tsx, frontend/src/pages/CategoriesPage.tsx</files>
<action>
**Create `frontend/src/components/EmptyState.tsx`:**
```typescript
interface EmptyStateProps {
icon: React.ElementType // lucide-react icon component
heading: string
subtext: string
action?: { label: string; onClick: () => void }
}
```
Render: centered flex column with `py-16 text-center`, icon at `size-12 text-muted-foreground`, heading as `font-semibold`, subtext as `text-sm text-muted-foreground`, optional Button with action.label/onClick.
**DashboardPage.tsx:**
- Import `EmptyState` and `FolderOpen` from lucide-react
- Add a new condition: after the loading skeleton block (line 39-47), before the main return, check `list.length === 0 && !loading`. If true, render the budget selector area + an `<EmptyState icon={FolderOpen} heading="No budgets yet" subtext="Create your first budget to start tracking your finances." action={{ label: "Create your first budget", onClick: () => setShowCreate(true) }} />` inside the page layout. Keep the existing Create Budget button in the header area as well.
- Replace the existing plain Card fallback (the `!current` branch, lines 93-99) with an `<EmptyState>` as well — this handles the "budgets exist but none selected" edge case. Use a simpler message: "Select a budget to view your dashboard."
**CategoriesPage.tsx:**
- Import `EmptyState` and `FolderOpen` from lucide-react
- Add `loading` state: `const [loading, setLoading] = useState(true)` — set to `true` initially, set to `false` after `fetchCategories` completes (wrap existing fetch in try/finally with `setLoading(false)`)
- After the header div and before the `grouped.map(...)`, add: `{!loading && list.length === 0 && <EmptyState icon={FolderOpen} heading="No categories yet" subtext="Add a category to organize your budget." action={{ label: "Add a category", onClick: openCreate }} />}`
- Guard the grouped cards render with `{grouped.length > 0 && grouped.map(...)}` so both empty state and cards don't show simultaneously.
</action>
<verify>
<automated>cd /home/jean-luc-makiola/Development/projects/SimpleFinanceDash/frontend && bun run build</automated>
</verify>
<done>EmptyState component exists and is used in DashboardPage (no-budgets case) and CategoriesPage (no-categories case). CategoriesPage has loading state to prevent empty-state flash. Build passes.</done>
</task>
<task type="auto">
<name>Task 2: Add delete confirmation dialog with spinner and error handling to CategoriesPage</name>
<files>frontend/src/pages/CategoriesPage.tsx</files>
<action>
Add new state variables:
```typescript
const [pendingDelete, setPendingDelete] = useState<{ id: string; name: string } | null>(null)
const [deleting, setDeleting] = useState(false)
const [deleteError, setDeleteError] = useState<string | null>(null)
```
Import `Spinner` from `@/components/ui/spinner` and `DialogDescription` from `@/components/ui/dialog`.
Replace `handleDelete`:
```typescript
const confirmDelete = async () => {
if (!pendingDelete) return
setDeleting(true)
setDeleteError(null)
try {
await categoriesApi.delete(pendingDelete.id)
setPendingDelete(null)
fetchCategories()
} catch (err) {
setDeleteError(err instanceof Error ? err.message : 'Failed to delete category')
} finally {
setDeleting(false)
}
}
```
Change the delete button in each category row from `onClick={() => handleDelete(cat.id)}` to `onClick={() => { setDeleteError(null); setPendingDelete({ id: cat.id, name: cat.name }) }}`.
Add a second Dialog (the delete confirmation) after the existing create/edit dialog:
```tsx
<Dialog open={!!pendingDelete} onOpenChange={(open) => { if (!open) { setPendingDelete(null); setDeleteError(null) } }}>
<DialogContent>
<DialogHeader>
<DialogTitle>Delete {pendingDelete?.name}?</DialogTitle>
<DialogDescription>This cannot be undone.</DialogDescription>
</DialogHeader>
{deleteError && <p className="text-sm text-destructive">{deleteError}</p>}
<DialogFooter>
<Button variant="outline" onClick={() => setPendingDelete(null)} disabled={deleting}>Cancel</Button>
<Button variant="destructive" onClick={confirmDelete} disabled={deleting} className="min-w-[80px]">
{deleting ? <Spinner /> : 'Delete'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
```
Remove the old `handleDelete` function entirely. The delete button in the category row now only sets state — no direct API call.
**CRITICAL:** The ON DELETE RESTRICT constraint means deleting a category with budget items returns 500. The catch block handles this — the error message displays inline in the dialog. The dialog does NOT auto-close on error, letting the user read the message and dismiss manually.
</action>
<verify>
<automated>cd /home/jean-luc-makiola/Development/projects/SimpleFinanceDash/frontend && bun run build</automated>
</verify>
<done>Delete button opens confirmation dialog. Confirm executes delete with spinner. Error from ON DELETE RESTRICT shows inline. Cancel closes dialog. Build passes with zero errors.</done>
</task>
</tasks>
<verification>
- `cd frontend && bun run build` — production build succeeds
- `cd frontend && bun vitest run` — full test suite passes
- CategoriesPage delete button opens dialog, not immediate delete
- DashboardPage shows EmptyState when no budgets exist
- CategoriesPage shows EmptyState when no categories exist
</verification>
<success_criteria>
- Delete confirmation dialog prevents accidental deletion
- ON DELETE RESTRICT errors display inline in dialog (not silent failure)
- EmptyState component renders icon + heading + subtext + optional CTA
- Dashboard empty state shows "Create your first budget" CTA
- Categories empty state shows "Add a category" CTA
- No empty-state flash on initial page load (loading guard in CategoriesPage)
</success_criteria>
<output>
After completion, create `.planning/phases/03-interaction-quality-and-completeness/03-02-SUMMARY.md`
</output>

View File

@@ -0,0 +1,262 @@
---
phase: 03-interaction-quality-and-completeness
plan: 03
type: execute
wave: 2
depends_on: ["03-01"]
files_modified:
- frontend/src/components/BillsTracker.tsx
- frontend/src/components/VariableExpenses.tsx
- frontend/src/components/DebtTracker.tsx
- frontend/src/pages/DashboardPage.tsx
autonomous: true
requirements: [IXTN-03, STATE-03]
must_haves:
truths:
- "After saving an inline edit in BillsTracker, the entire row briefly flashes green"
- "After a failed inline edit save, the row briefly flashes red"
- "Same flash behavior works in VariableExpenses and DebtTracker"
- "Dashboard loading skeleton uses pastel-tinted backgrounds matching section colors"
- "BillsTracker, VariableExpenses, DebtTracker show tinted skeletons when budget has no items for that section"
artifacts:
- path: "frontend/src/components/BillsTracker.tsx"
provides: "Row flash state + tinted skeleton loading state"
contains: "flashRowId"
- path: "frontend/src/components/VariableExpenses.tsx"
provides: "Row flash state + tinted skeleton loading state"
contains: "flashRowId"
- path: "frontend/src/components/DebtTracker.tsx"
provides: "Row flash state + tinted skeleton loading state"
contains: "flashRowId"
- path: "frontend/src/pages/DashboardPage.tsx"
provides: "Tinted dashboard loading skeletons using palette light shades"
contains: "palette"
key_links:
- from: "frontend/src/components/BillsTracker.tsx"
to: "frontend/src/components/InlineEditCell.tsx"
via: "onSaveSuccess/onSaveError callbacks"
pattern: "onSaveSuccess.*flashRow"
- from: "frontend/src/pages/DashboardPage.tsx"
to: "frontend/src/lib/palette.ts"
via: "palette import for skeleton tinting"
pattern: "palette\\..*\\.light"
---
<objective>
Wire row-level flash feedback into all three tracker components and add pastel-tinted loading skeletons to the dashboard.
Purpose: Complete the inline edit feedback loop — users see green/red row flashes confirming save success/failure. Tinted skeletons make the loading state feel intentional and branded rather than generic.
Output: BillsTracker, VariableExpenses, DebtTracker with flash + skeleton states; DashboardPage with tinted skeletons.
</objective>
<execution_context>
@/home/jean-luc-makiola/.claude/get-shit-done/workflows/execute-plan.md
@/home/jean-luc-makiola/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/03-interaction-quality-and-completeness/03-CONTEXT.md
@.planning/phases/03-interaction-quality-and-completeness/03-RESEARCH.md
@.planning/phases/03-interaction-quality-and-completeness/03-01-SUMMARY.md
<interfaces>
<!-- InlineEditCell interface AFTER Plan 01 (with new callbacks) -->
From frontend/src/components/InlineEditCell.tsx (post Plan 01):
```typescript
interface InlineEditCellProps {
value: number
currency: string
onSave: (value: number) => Promise<void>
onSaveSuccess?: () => void
onSaveError?: () => void
className?: string
}
```
<!-- Palette light shades for skeleton tinting -->
From frontend/src/lib/palette.ts:
```typescript
export const palette = {
bill: { base: '...', light: 'oklch(0.96 0.03 250)', header: '...' },
variable_expense: { base: '...', light: 'oklch(0.97 0.04 85)', header: '...' },
debt: { base: '...', light: 'oklch(0.96 0.04 15)', header: '...' },
saving: { base: '...', light: 'oklch(0.95 0.04 280)', header: '...' },
investment: { base: '...', light: 'oklch(0.96 0.03 320)', header: '...' },
// ...
}
```
<!-- BillsTracker current structure (representative of all 3 trackers) -->
From frontend/src/components/BillsTracker.tsx:
```typescript
interface Props {
budget: BudgetDetail
onUpdate: (itemId: string, data: { actual_amount?: number; budgeted_amount?: number }) => Promise<void>
}
// Uses: <TableRow key={item.id}> containing <InlineEditCell onSave={...} />
```
<!-- Skeleton component -->
From frontend/src/components/ui/skeleton.tsx:
```typescript
// Accepts className and style props. Default bg is bg-muted.
// Override with style={{ backgroundColor: '...' }} to tint.
```
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: Wire row flash feedback into BillsTracker, VariableExpenses, and DebtTracker</name>
<files>frontend/src/components/BillsTracker.tsx, frontend/src/components/VariableExpenses.tsx, frontend/src/components/DebtTracker.tsx</files>
<action>
Apply the same pattern to all three tracker components. Import `useState` (already imported in most) and `cn` from `@/lib/utils`.
Add flash state and helper to each component:
```typescript
const [flashRowId, setFlashRowId] = useState<string | null>(null)
const [errorRowId, setErrorRowId] = useState<string | null>(null)
const triggerFlash = (id: string, type: 'success' | 'error') => {
if (type === 'success') {
setFlashRowId(id)
setTimeout(() => setFlashRowId(null), 600)
} else {
setErrorRowId(id)
setTimeout(() => setErrorRowId(null), 600)
}
}
```
On each data `<TableRow>` (not the totals row), add inline style for the flash:
```tsx
<TableRow
key={item.id}
className="transition-colors duration-500"
style={
flashRowId === item.id
? { backgroundColor: 'color-mix(in oklch, var(--success) 20%, transparent)' }
: errorRowId === item.id
? { backgroundColor: 'color-mix(in oklch, var(--destructive) 20%, transparent)' }
: undefined
}
>
```
Use `color-mix()` inline style (not Tailwind `bg-success/20`) per research recommendation — avoids potential Tailwind class generation issues.
Pass callbacks to each `<InlineEditCell>`:
```tsx
<InlineEditCell
value={item.actual_amount}
currency={budget.currency}
onSave={(actual) => onUpdate(item.id, { actual_amount: actual })}
onSaveSuccess={() => triggerFlash(item.id, 'success')}
onSaveError={() => triggerFlash(item.id, 'error')}
className={amountColorClass({ type: 'bill', actual: item.actual_amount, budgeted: item.budgeted_amount })}
/>
```
Adjust the `type` argument in `amountColorClass` per component:
- BillsTracker: `type: 'bill'`
- VariableExpenses: `type: 'variable_expense'`
- DebtTracker: `type: 'debt'`
(These should already be correct from Phase 1 — just ensure the new onSaveSuccess/onSaveError props are added.)
**Do NOT modify** the totals row or the CardHeader — only add flash state and wire callbacks.
</action>
<verify>
<automated>cd /home/jean-luc-makiola/Development/projects/SimpleFinanceDash/frontend && bun run build</automated>
</verify>
<done>All three tracker components have flash state, triggerFlash helper, inline style on data rows, and onSaveSuccess/onSaveError wired to InlineEditCell. Build passes.</done>
</task>
<task type="auto">
<name>Task 2: Add pastel-tinted loading skeletons to DashboardPage and tracker sections</name>
<files>frontend/src/pages/DashboardPage.tsx, frontend/src/components/BillsTracker.tsx, frontend/src/components/VariableExpenses.tsx, frontend/src/components/DebtTracker.tsx</files>
<action>
**DashboardPage.tsx** — tint existing loading skeleton block (lines 39-47):
- Import `palette` from `@/lib/palette`
- Replace the existing generic Skeleton elements with tinted versions:
```tsx
if (loading && list.length === 0) {
return (
<div className="flex flex-col gap-4 p-6">
<Skeleton className="h-10 w-64" />
<Skeleton className="h-48 w-full rounded-lg" style={{ backgroundColor: palette.saving.light }} />
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
<Skeleton className="h-40 w-full rounded-lg" style={{ backgroundColor: palette.bill.light }} />
<Skeleton className="h-40 w-full rounded-lg" style={{ backgroundColor: palette.variable_expense.light }} />
</div>
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
<Skeleton className="h-40 w-full rounded-lg" style={{ backgroundColor: palette.debt.light }} />
<Skeleton className="h-40 w-full rounded-lg" style={{ backgroundColor: palette.investment.light }} />
</div>
</div>
)
}
```
Use `style` prop to override `bg-muted` without editing `ui/skeleton.tsx`.
**BillsTracker.tsx, VariableExpenses.tsx, DebtTracker.tsx** — add skeleton for empty sections:
- Import `Skeleton` from `@/components/ui/skeleton` and ensure `palette` is imported (already imported for `headerGradient`)
- After the filter (e.g., `const bills = budget.items.filter(...)`) add an early return if no items exist:
```tsx
if (bills.length === 0) {
return (
<Card>
<CardHeader style={headerGradient('bill')}>
<CardTitle>{t('dashboard.billsTracker')}</CardTitle>
</CardHeader>
<CardContent className="flex flex-col gap-2 p-4">
{[1, 2, 3].map((i) => (
<Skeleton
key={i}
className="h-10 w-full rounded-md"
style={{ backgroundColor: palette.bill.light }}
/>
))}
</CardContent>
</Card>
)
}
```
Use the matching palette key per component:
- BillsTracker: `palette.bill.light`
- VariableExpenses: `palette.variable_expense.light`
- DebtTracker: `palette.debt.light`
**Note:** These skeletons show when a budget exists but has no items of that type — they serve as visual placeholders indicating the section exists. This is distinct from the DashboardPage loading skeleton (which shows before any data loads).
</action>
<verify>
<automated>cd /home/jean-luc-makiola/Development/projects/SimpleFinanceDash/frontend && bun vitest run && bun run build</automated>
</verify>
<done>Dashboard loading skeleton uses palette-tinted backgrounds per section. Each tracker shows tinted skeletons when no items of its type exist. All tests pass, build succeeds.</done>
</task>
</tasks>
<verification>
- `cd frontend && bun vitest run` — full test suite passes
- `cd frontend && bun run build` — production build succeeds
- Row flash uses `color-mix(in oklch, var(--success/destructive) 20%, transparent)` inline style
- Dashboard skeleton uses palette.*.light inline styles
- Tracker skeletons use matching palette key for their section
</verification>
<success_criteria>
- Inline edit save success produces visible green row flash (~600ms duration)
- Inline edit save failure produces visible red row flash + value revert
- Dashboard loading state shows pastel-tinted skeletons (not grey)
- Empty tracker sections show tinted skeleton placeholders matching their card header color
- No flash or skeleton interferes with existing functionality
</success_criteria>
<output>
After completion, create `.planning/phases/03-interaction-quality-and-completeness/03-03-SUMMARY.md`
</output>