From 0d40043615eac3e141166848b371438d2848295b Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Wed, 11 Mar 2026 22:18:00 +0100 Subject: [PATCH] docs(03-interaction-quality-and-completeness): create phase plan --- .planning/ROADMAP.md | 8 +- .../03-01-PLAN.md | 179 ++++++++++++ .../03-02-PLAN.md | 219 +++++++++++++++ .../03-03-PLAN.md | 262 ++++++++++++++++++ 4 files changed, 666 insertions(+), 2 deletions(-) create mode 100644 .planning/phases/03-interaction-quality-and-completeness/03-01-PLAN.md create mode 100644 .planning/phases/03-interaction-quality-and-completeness/03-02-PLAN.md create mode 100644 .planning/phases/03-interaction-quality-and-completeness/03-03-PLAN.md diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 716c2f8..b4225e6 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -59,7 +59,11 @@ Plans: 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 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 **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 | | 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 | - | diff --git a/.planning/phases/03-interaction-quality-and-completeness/03-01-PLAN.md b/.planning/phases/03-interaction-quality-and-completeness/03-01-PLAN.md new file mode 100644 index 0000000..6c00641 --- /dev/null +++ b/.planning/phases/03-interaction-quality-and-completeness/03-01-PLAN.md @@ -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" +--- + + +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. + + + +@/home/jean-luc-makiola/.claude/get-shit-done/workflows/execute-plan.md +@/home/jean-luc-makiola/.claude/get-shit-done/templates/summary.md + + + +@.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 + + + +From frontend/src/components/InlineEditCell.tsx: +```typescript +interface InlineEditCellProps { + value: number + currency: string + onSave: (value: number) => Promise + 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 +``` + + + + + + + Task 1: Enhance InlineEditCell with pencil icon, save/error callbacks, and try/catch + frontend/src/components/InlineEditCell.tsx, frontend/src/components/InlineEditCell.test.tsx + + - 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) + + + 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: `` + - 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()`. + + + cd /home/jean-luc-makiola/Development/projects/SimpleFinanceDash/frontend && bun vitest run src/components/InlineEditCell.test.tsx + + InlineEditCell renders pencil icon in display mode, fires onSaveSuccess on successful save, fires onSaveError and reverts value on failed save. All tests pass. + + + + Task 2: Add loading spinners to Login, Register, and BudgetSetup submit buttons + frontend/src/pages/LoginPage.tsx, frontend/src/pages/RegisterPage.tsx, frontend/src/components/BudgetSetup.tsx + + 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 ? : 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 ? : 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 ? : t('budget.create')}` + + Do NOT modify any other logic in these files — only the Button content and className. + + + 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 + + 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. + + + + + +- `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 + + + +- 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 + + + +After completion, create `.planning/phases/03-interaction-quality-and-completeness/03-01-SUMMARY.md` + diff --git a/.planning/phases/03-interaction-quality-and-completeness/03-02-PLAN.md b/.planning/phases/03-interaction-quality-and-completeness/03-02-PLAN.md new file mode 100644 index 0000000..42b876f --- /dev/null +++ b/.planning/phases/03-interaction-quality-and-completeness/03-02-PLAN.md @@ -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" +--- + + +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. + + + +@/home/jean-luc-makiola/.claude/get-shit-done/workflows/execute-plan.md +@/home/jean-luc-makiola/.claude/get-shit-done/templates/summary.md + + + +@.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 + + + +From frontend/src/pages/CategoriesPage.tsx: +```typescript +const handleDelete = async (id: string) => { + await categoriesApi.delete(id) + fetchCategories() +} +``` + + +From frontend/src/pages/CategoriesPage.tsx: +```typescript +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog' +``` + + +From frontend/src/pages/DashboardPage.tsx: +```typescript +// Line 93-99: plain Card with text — replace with EmptyState + + + {t('dashboard.noBudgets')} + + +``` + + +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 } +``` + + + + + + + Task 1: Create shared EmptyState component and wire into Dashboard and Categories pages + frontend/src/components/EmptyState.tsx, frontend/src/pages/DashboardPage.tsx, frontend/src/pages/CategoriesPage.tsx + + **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 ` 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 `` 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 && }` + - Guard the grouped cards render with `{grouped.length > 0 && grouped.map(...)}` so both empty state and cards don't show simultaneously. + + + cd /home/jean-luc-makiola/Development/projects/SimpleFinanceDash/frontend && bun run build + + 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. + + + + Task 2: Add delete confirmation dialog with spinner and error handling to CategoriesPage + frontend/src/pages/CategoriesPage.tsx + + Add new state variables: + ```typescript + const [pendingDelete, setPendingDelete] = useState<{ id: string; name: string } | null>(null) + const [deleting, setDeleting] = useState(false) + const [deleteError, setDeleteError] = useState(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 + { if (!open) { setPendingDelete(null); setDeleteError(null) } }}> + + + Delete {pendingDelete?.name}? + This cannot be undone. + + {deleteError &&

{deleteError}

} + + + + +
+
+ ``` + + 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. +
+ + cd /home/jean-luc-makiola/Development/projects/SimpleFinanceDash/frontend && bun run build + + 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. +
+ +
+ + +- `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 + + + +- 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) + + + +After completion, create `.planning/phases/03-interaction-quality-and-completeness/03-02-SUMMARY.md` + diff --git a/.planning/phases/03-interaction-quality-and-completeness/03-03-PLAN.md b/.planning/phases/03-interaction-quality-and-completeness/03-03-PLAN.md new file mode 100644 index 0000000..389624b --- /dev/null +++ b/.planning/phases/03-interaction-quality-and-completeness/03-03-PLAN.md @@ -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" +--- + + +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. + + + +@/home/jean-luc-makiola/.claude/get-shit-done/workflows/execute-plan.md +@/home/jean-luc-makiola/.claude/get-shit-done/templates/summary.md + + + +@.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 + + + +From frontend/src/components/InlineEditCell.tsx (post Plan 01): +```typescript +interface InlineEditCellProps { + value: number + currency: string + onSave: (value: number) => Promise + onSaveSuccess?: () => void + onSaveError?: () => void + className?: string +} +``` + + +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: '...' }, + // ... +} +``` + + +From frontend/src/components/BillsTracker.tsx: +```typescript +interface Props { + budget: BudgetDetail + onUpdate: (itemId: string, data: { actual_amount?: number; budgeted_amount?: number }) => Promise +} +// Uses: containing +``` + + +From frontend/src/components/ui/skeleton.tsx: +```typescript +// Accepts className and style props. Default bg is bg-muted. +// Override with style={{ backgroundColor: '...' }} to tint. +``` + + + + + + + Task 1: Wire row flash feedback into BillsTracker, VariableExpenses, and DebtTracker + frontend/src/components/BillsTracker.tsx, frontend/src/components/VariableExpenses.tsx, frontend/src/components/DebtTracker.tsx + + 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(null) + const [errorRowId, setErrorRowId] = useState(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 `` (not the totals row), add inline style for the flash: + ```tsx + + ``` + + Use `color-mix()` inline style (not Tailwind `bg-success/20`) per research recommendation — avoids potential Tailwind class generation issues. + + Pass callbacks to each ``: + ```tsx + 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. + + + cd /home/jean-luc-makiola/Development/projects/SimpleFinanceDash/frontend && bun run build + + All three tracker components have flash state, triggerFlash helper, inline style on data rows, and onSaveSuccess/onSaveError wired to InlineEditCell. Build passes. + + + + Task 2: Add pastel-tinted loading skeletons to DashboardPage and tracker sections + frontend/src/pages/DashboardPage.tsx, frontend/src/components/BillsTracker.tsx, frontend/src/components/VariableExpenses.tsx, frontend/src/components/DebtTracker.tsx + + **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 ( +
+ + +
+ + +
+
+ + +
+
+ ) + } + ``` + 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 ( + + + {t('dashboard.billsTracker')} + + + {[1, 2, 3].map((i) => ( + + ))} + + + ) + } + ``` + 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). +
+ + cd /home/jean-luc-makiola/Development/projects/SimpleFinanceDash/frontend && bun vitest run && bun run build + + 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. +
+ +
+ + +- `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 + + + +- 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 + + + +After completion, create `.planning/phases/03-interaction-quality-and-completeness/03-03-SUMMARY.md` +