--- phase: 07-setup-wizard plan: 01 type: execute wave: 1 depends_on: [] files_modified: - src/pages/SetupPage.tsx - src/hooks/useWizardState.ts - src/components/setup/WizardStepper.tsx - src/components/setup/IncomeStep.tsx - src/components/setup/AllocationBar.tsx - src/components/setup/CategoryGroupHeader.tsx - src/components/setup/PresetItemRow.tsx - src/components/setup/RecurringItemsStep.tsx - src/i18n/en.json - src/i18n/de.json autonomous: true requirements: [SETUP-01, SETUP-02, SETUP-04] must_haves: truths: - "SetupPage renders a centered card with WizardStepper showing 3 steps" - "useWizardState persists wizard data to localStorage keyed by userId" - "IncomeStep shows a number input pre-filled with 3000 and profile currency" - "RecurringItemsStep shows all 19 PRESETS grouped by type with checkboxes" - "AllocationBar shows live remaining = income - sum(checked amounts)" - "Bills and variable_expense items are pre-checked by default" - "All wizard i18n keys exist in both en.json and de.json" artifacts: - path: "src/pages/SetupPage.tsx" provides: "Wizard page orchestrator with step rendering" min_lines: 40 - path: "src/hooks/useWizardState.ts" provides: "localStorage-synced wizard state management" exports: ["useWizardState"] - path: "src/components/setup/WizardStepper.tsx" provides: "Horizontal 1-2-3 stepper bar" exports: ["WizardStepper"] - path: "src/components/setup/IncomeStep.tsx" provides: "Step 1 income input" exports: ["IncomeStep"] - path: "src/components/setup/RecurringItemsStep.tsx" provides: "Step 2 grouped checklist" exports: ["RecurringItemsStep"] - path: "src/components/setup/AllocationBar.tsx" provides: "Sticky remaining balance bar" exports: ["AllocationBar"] - path: "src/components/setup/PresetItemRow.tsx" provides: "Single checkbox row with amount input" exports: ["PresetItemRow"] - path: "src/components/setup/CategoryGroupHeader.tsx" provides: "Section header with colored dot" exports: ["CategoryGroupHeader"] key_links: - from: "src/pages/SetupPage.tsx" to: "src/hooks/useWizardState.ts" via: "useWizardState(userId) hook call" pattern: "useWizardState" - from: "src/components/setup/RecurringItemsStep.tsx" to: "src/data/presets.ts" via: "import PRESETS" pattern: "import.*PRESETS.*from.*presets" --- Build the setup wizard page shell, state management hook, and all UI components for the 3-step wizard (income, recurring items with live allocation, review placeholder). Purpose: Establishes the wizard's visual structure, localStorage persistence, and all interactive components. Plan 02 will add the ReviewStep and completion/redirect logic on top of this foundation. Output: A navigable 3-step wizard at /setup (not yet routed) with working state persistence and live allocation calculation. @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md @.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/07-setup-wizard/07-CONTEXT.md @.planning/phases/07-setup-wizard/07-RESEARCH.md @.planning/phases/07-setup-wizard/07-PATTERNS.md @.planning/phases/07-setup-wizard/07-UI-SPEC.md From src/lib/types.ts: ```typescript export type CategoryType = "income" | "bill" | "variable_expense" | "debt" | "saving" | "investment" export type ItemTier = "fixed" | "variable" | "one_off" export interface Profile { id: string; display_name: string | null; locale: string; currency: string; setup_completed: boolean; ... } ``` From src/data/presets.ts: ```typescript export interface PresetItem { slug: string type: CategoryType defaultAmount: number item_tier: "fixed" | "variable" } export const PRESETS: PresetItem[] = [ /* 19 items: 4 income, 4 bill, 5 variable_expense, 2 debt, 2 saving, 2 investment */ ] ``` From src/hooks/useAuth.ts: ```typescript // Returns { user, profile, loading, signIn, signOut, signUp } // profile.currency = "EUR" | "USD" | "CHF" etc. ``` From src/hooks/useCategories.ts: ```typescript export function useCategories(): { categories: Category[]; loading: boolean; create: UseMutationResult; ... } // create.mutateAsync({ name: string, type: CategoryType, icon?: string }) => Category ``` From src/hooks/useTemplate.ts: ```typescript export function useTemplate(): { template: Template | null; items: TemplateItem[]; loading: boolean; createItem: UseMutationResult; ... } // createItem.mutateAsync({ category_id: string, item_tier: "fixed"|"variable", budgeted_amount: number }) => TemplateItem ``` Task 1: Install shadcn checkbox + create useWizardState hook + add i18n keys - src/i18n/en.json - src/i18n/de.json - src/data/presets.ts - src/hooks/useAuth.ts src/hooks/useWizardState.ts, src/i18n/en.json, src/i18n/de.json 1. Install shadcn checkbox: ```bash npx shadcn@latest add checkbox ``` 2. Create `src/hooks/useWizardState.ts`: ```typescript import { useState, useEffect } from "react" import { PRESETS } from "@/data/presets" export interface WizardState { currentStep: 1 | 2 | 3 income: number selectedItems: Record } const STORAGE_KEY = (userId: string) => `setup-wizard-${userId}` function getDefaultState(): WizardState { const selectedItems: Record = {} for (const preset of PRESETS) { selectedItems[preset.slug] = { checked: preset.type === "bill" || preset.type === "variable_expense", amount: preset.defaultAmount, } } return { currentStep: 1, income: 3000, selectedItems } } export function useWizardState(userId: string) { const [state, setState] = useState(() => { try { const saved = localStorage.getItem(STORAGE_KEY(userId)) if (saved) { const parsed = JSON.parse(saved) // Validate shape if (parsed && typeof parsed.currentStep === "number" && typeof parsed.income === "number" && parsed.selectedItems) { return parsed as WizardState } } } catch { /* ignore corrupt data */ } return getDefaultState() }) useEffect(() => { localStorage.setItem(STORAGE_KEY(userId), JSON.stringify(state)) }, [state, userId]) const setStep = (step: 1 | 2 | 3) => setState(s => ({ ...s, currentStep: step })) const setIncome = (income: number) => setState(s => ({ ...s, income })) const toggleItem = (slug: string) => setState(s => ({ ...s, selectedItems: { ...s.selectedItems, [slug]: { ...s.selectedItems[slug], checked: !s.selectedItems[slug].checked }, }, })) const setItemAmount = (slug: string, amount: number) => setState(s => ({ ...s, selectedItems: { ...s.selectedItems, [slug]: { ...s.selectedItems[slug], amount }, }, })) const clearState = () => { localStorage.removeItem(STORAGE_KEY(userId)) } return { state, setStep, setIncome, toggleItem, setItemAmount, clearState } } ``` 3. Add i18n keys to `src/i18n/en.json` — add a `"setup"` object at the top level: ```json "setup": { "title": "Set up your budget", "step1": { "title": "Monthly Income", "description": "How much do you earn each month?", "incomeLabel": "Monthly net income", "helper": "Enter your total monthly take-home pay", "validation": "Please enter a positive income amount" }, "step2": { "title": "Recurring Items", "description": "Select your regular monthly expenses", "remaining": "Remaining to allocate" }, "step3": { "title": "Review", "description": "Confirm your budget template", "incomeLabel": "Monthly income", "totalLabel": "Total expenses", "remainingLabel": "Remaining", "empty": "No items selected. You can add items to your template later." }, "steps": { "1": "Income", "2": "Items", "3": "Review" }, "next": "Next Step", "back": "Go Back", "skip": "Skip Step", "skipSetup": "Skip setup", "complete": "Complete Setup", "toast": { "success": "Template created! Your first budget will appear automatically.", "error": "Could not save your template. Please try again.", "partialError": "Some items could not be saved. Check your template page." } } ``` 4. Add equivalent German keys to `src/i18n/de.json`: ```json "setup": { "title": "Budget einrichten", "step1": { "title": "Monatliches Einkommen", "description": "Wie viel verdienst du pro Monat?", "incomeLabel": "Monatliches Nettoeinkommen", "helper": "Gib dein monatliches Nettoeinkommen ein", "validation": "Bitte gib einen positiven Betrag ein" }, "step2": { "title": "Wiederkehrende Posten", "description": "Wahle deine regelmaigen monatlichen Ausgaben", "remaining": "Verbleibend zu verteilen" }, "step3": { "title": "Ubersicht", "description": "Bestatige deine Budgetvorlage", "incomeLabel": "Monatliches Einkommen", "totalLabel": "Gesamtausgaben", "remainingLabel": "Verbleibend", "empty": "Keine Posten ausgewahlt. Du kannst spater Posten zu deiner Vorlage hinzufugen." }, "steps": { "1": "Einkommen", "2": "Posten", "3": "Ubersicht" }, "next": "Nachster Schritt", "back": "Zuruck", "skip": "Schritt uberspringen", "skipSetup": "Einrichtung uberspringen", "complete": "Einrichtung abschlieen", "toast": { "success": "Vorlage erstellt! Dein erstes Budget wird automatisch erscheinen.", "error": "Vorlage konnte nicht gespeichert werden. Bitte versuche es erneut.", "partialError": "Einige Posten konnten nicht gespeichert werden. Prufe deine Vorlagenseite." } } ``` grep -q "useWizardState" src/hooks/useWizardState.ts && grep -q '"setup"' src/i18n/en.json && grep -q '"setup"' src/i18n/de.json && ls src/components/ui/checkbox.tsx - src/hooks/useWizardState.ts contains `export function useWizardState(userId: string)` - src/hooks/useWizardState.ts contains `setup-wizard-${userId}` - src/hooks/useWizardState.ts contains `getDefaultState` - src/i18n/en.json contains `"setup.title"` nested under `"setup": { "title":` - src/i18n/en.json contains `"setup.toast.success"` with value "Template created!" - src/i18n/de.json contains `"setup": { "title": "Budget einrichten"` - src/components/ui/checkbox.tsx exists (shadcn installed) useWizardState hook created with localStorage sync, all setup i18n keys in EN+DE, shadcn checkbox component installed Task 2: Create all wizard UI components and SetupPage - src/pages/LoginPage.tsx - src/components/dashboard/SummaryStrip.tsx - src/pages/SettingsPage.tsx - src/data/presets.ts - src/hooks/useWizardState.ts - src/components/ui/checkbox.tsx src/components/setup/WizardStepper.tsx, src/components/setup/IncomeStep.tsx, src/components/setup/AllocationBar.tsx, src/components/setup/CategoryGroupHeader.tsx, src/components/setup/PresetItemRow.tsx, src/components/setup/RecurringItemsStep.tsx, src/pages/SetupPage.tsx Create the following components in `src/components/setup/`: **1. WizardStepper.tsx:** - Props: `currentStep: 1|2|3`, `onStepClick: (step: 1|2|3) => void` - Renders 3 circles in a row connected by lines - Active step: `bg-primary text-primary-foreground` with number (14px/600) - Completed step (step < currentStep): `bg-primary text-primary-foreground` with lucide `Check` icon (16px) - Upcoming step (step > currentStep): `bg-muted text-muted-foreground border border-border` - Connector line between: `h-px w-16` — completed segment `bg-primary`, upcoming `bg-border` - Step labels below circles: text-xs font-semibold, active `text-foreground`, others `text-muted-foreground` - Labels from i18n: `t("setup.steps.1")`, `t("setup.steps.2")`, `t("setup.steps.3")` - Clicking a completed step or current step calls onStepClick. Future steps: no action, cursor-default. - Wrap in `role="navigation" aria-label="Setup progress"` - Circle dimensions: `w-8 h-8` (32px) - Overall layout: `flex items-center justify-center gap-0` with step groups `flex flex-col items-center gap-1` separated by connector lines - Margin below: handled by parent (mb-6) **2. IncomeStep.tsx:** - Props: `income: number`, `onIncomeChange: (val: number) => void`, `currency: string`, `error: string | null` - Renders card content for step 1 - Label: `t("setup.step1.incomeLabel")` (14px/600) - Input row: `flex items-center gap-2` - Input: `type="number"`, `className="w-full text-right text-lg"`, value={income}, onChange converts to number - Currency suffix: `{currency}` - Helper text: `t("setup.step1.helper")` in `text-sm text-muted-foreground` - If `error` is truthy: show `

{error}

` below input - `aria-describedby` on input pointing to helper and error elements **3. AllocationBar.tsx:** - Props: `remaining: number`, `currency: string` - Layout: `sticky top-0 z-10 flex justify-between items-center py-3 px-4 bg-muted border-b border-border rounded-none` - Left: `{t("setup.step2.remaining")}` - Right: formatted amount using `Intl.NumberFormat` with currency - `remaining >= 0`: `className="text-base font-semibold text-on-budget"` - `remaining < 0`: `className="text-base font-semibold text-destructive"` - Add `aria-live="polite"` on the amount element **4. CategoryGroupHeader.tsx:** - Props: `type: CategoryType`, `label: string`, `count: number` - Layout: `flex items-center gap-2 pt-4 pb-2` - Colored dot: `w-2.5 h-2.5 rounded-full` with inline style `backgroundColor: var(--color-${type.replace('_', '-')})` - Map types to CSS vars: income -> `--color-income`, bill -> `--color-bill`, variable_expense -> `--color-variable-expense`, debt -> `--color-debt`, saving -> `--color-saving`, investment -> `--color-investment` - Label: `{label}` - Count: `({count} items)` - Below: `` from shadcn **5. PresetItemRow.tsx:** - Props: `slug: string`, `type: CategoryType`, `checked: boolean`, `amount: number`, `onToggle: () => void`, `onAmountChange: (val: number) => void` - Layout: `flex items-center gap-3 py-2.5 px-1` - Checkbox: shadcn `` - Item name: `{t(\`presets.${type}.${slug}\`)}` - Category badge: `{t(\`categories.types.${type}\`)}` - Amount input: `` - When disabled: add `bg-muted text-muted-foreground opacity-50` classes **6. RecurringItemsStep.tsx:** - Props: `selectedItems: Record`, `income: number`, `currency: string`, `onToggle: (slug: string) => void`, `onAmountChange: (slug: string, amount: number) => void` - Groups PRESETS by type using: `Object.groupBy(PRESETS, (p) => p.type)` — if Object.groupBy not available, use manual reduce - Order groups: `["income", "bill", "variable_expense", "debt", "saving", "investment"]` - Renders AllocationBar at top (sticky) - For each group: CategoryGroupHeader + list of PresetItemRow - Wrap each group in `
` with `{groupLabel}` - Compute remaining: `income - sum of all checked items' amounts` **7. SetupPage.tsx** (page orchestrator): - Imports useAuth, useWizardState, useTranslation, WizardStepper, IncomeStep, RecurringItemsStep - Gets userId from `useAuth()` -> `user.id` - Gets currency from profile: `profile?.currency ?? "EUR"` - Uses `useWizardState(userId)` for state management - Layout: `
` - Renders WizardStepper above the card - Card: `` - CardHeader: step title (heading 20px/600) + step description (body 14px/400 text-muted-foreground) - CardContent: renders active step component based on `state.currentStep` - Bottom nav: `flex justify-between items-center pt-4 border-t border-border` - Left side: Go Back button (variant="ghost", hidden on step 1) + Skip Step button (variant="ghost" text-muted-foreground) - Right side: Next Step button (variant="default") - Step 1 Next: validates income > 0, shows error if invalid, otherwise advances to step 2 - Step 2 Next: no validation, advances to step 3 - Step 3: shows placeholder text "Review step coming in next plan" (will be replaced in Plan 02) - Below card: "Skip setup" link: `` - Skip step on step 1: advance to step 2. Skip step on step 2: advance to step 3. - Clicking completed stepper steps calls `setStep(step)` ls src/components/setup/WizardStepper.tsx src/components/setup/IncomeStep.tsx src/components/setup/AllocationBar.tsx src/components/setup/CategoryGroupHeader.tsx src/components/setup/PresetItemRow.tsx src/components/setup/RecurringItemsStep.tsx src/pages/SetupPage.tsx && npx tsc --noEmit 2>&1 | head -30 - src/pages/SetupPage.tsx contains `useWizardState` and `max-w-2xl` - src/pages/SetupPage.tsx contains `currentStep` conditional rendering - src/components/setup/WizardStepper.tsx contains `role="navigation"` and `aria-label` - src/components/setup/WizardStepper.tsx contains `w-8 h-8` - src/components/setup/IncomeStep.tsx contains `type="number"` and `text-lg` - src/components/setup/AllocationBar.tsx contains `aria-live="polite"` and `sticky top-0` - src/components/setup/CategoryGroupHeader.tsx contains `w-2.5 h-2.5 rounded-full` - src/components/setup/PresetItemRow.tsx contains `Checkbox` import and `w-24` - src/components/setup/RecurringItemsStep.tsx imports `PRESETS` from `@/data/presets` - TypeScript compiles without errors (`npx tsc --noEmit` exits 0) All 7 wizard UI components render correctly, TypeScript compiles without errors, wizard navigates between steps 1 and 2 with working state persistence ## Trust Boundaries | Boundary | Description | |----------|-------------| | localStorage -> component | Persisted wizard data read back into React state | | client -> Supabase (future Plan 02) | Category/template item creation on completion | ## STRIDE Threat Register | Threat ID | Category | Component | Disposition | Mitigation Plan | |-----------|----------|-----------|-------------|-----------------| | T-07-01 | Tampering | useWizardState localStorage read | mitigate | Validate JSON shape on load: check currentStep is 1-3, income is number, selectedItems is object with expected structure. Fall back to defaults on invalid data. | | T-07-02 | Information Disclosure | localStorage key | accept | Key includes userId, preventing cross-user data leakage. Data is non-sensitive (income amount, item selections). | | T-07-03 | Spoofing | Preset data | accept | PRESETS are hardcoded source constants, not user input. i18n keys resolve from bundled JSON, no injection vector. | - `npx tsc --noEmit` passes (no type errors) - All 7 component files exist in src/components/setup/ and src/pages/ - useWizardState stores and retrieves state from localStorage - i18n keys resolve for both "en" and "de" locales - SetupPage renders with WizardStepper showing 3 steps - Step 1 shows income input pre-filled with 3000 and currency suffix - Step 2 shows all 19 PRESETS grouped by type with checkboxes and editable amounts - Bills (4) and variable_expense (5) items are checked by default - AllocationBar shows remaining = 3000 - sum(checked) and turns red when negative - Navigating between steps preserves state - Refreshing the page restores wizard at correct step (localStorage) After completion, create `.planning/phases/07-setup-wizard/07-01-SUMMARY.md`