---
phase: 13-setup-impact-preview
plan: 02
type: execute
wave: 2
depends_on: ["13-01"]
files_modified:
  - src/client/components/SetupImpactSelector.tsx
  - src/client/components/ImpactDeltaBadge.tsx
  - src/client/routes/threads/$threadId.tsx
  - src/client/components/CandidateListItem.tsx
  - src/client/components/CandidateCard.tsx
  - src/client/components/ComparisonTable.tsx
autonomous: true
requirements: [IMPC-01, IMPC-02, IMPC-03, IMPC-04]

must_haves:
  truths:
    - "User can select a setup from a dropdown in the thread header"
    - "Each candidate displays weight and cost delta badges when a setup is selected"
    - "Replace mode shows signed delta with replaced item name context"
    - "Add mode shows positive delta labeled as '(add)'"
    - "Candidate with no weight shows '-- (no weight data)' instead of a zero"
    - "Candidate with no price shows '-- (no price data)' instead of a zero"
    - "Deselecting setup ('None') clears all delta indicators"
    - "Deltas appear in list view, grid view, and comparison table"
  artifacts:
    - path: "src/client/components/SetupImpactSelector.tsx"
      provides: "Setup dropdown for thread header"
      exports: ["SetupImpactSelector"]
    - path: "src/client/components/ImpactDeltaBadge.tsx"
      provides: "Inline delta indicator component"
      exports: ["ImpactDeltaBadge"]
    - path: "src/client/routes/threads/$threadId.tsx"
      provides: "Thread detail page wired with impact preview"
    - path: "src/client/components/CandidateListItem.tsx"
      provides: "List item with delta badges"
    - path: "src/client/components/CandidateCard.tsx"
      provides: "Card with delta badges"
    - path: "src/client/components/ComparisonTable.tsx"
      provides: "Comparison table with impact delta rows"
  key_links:
    - from: "src/client/routes/threads/$threadId.tsx"
      to: "src/client/hooks/useImpactDeltas.ts"
      via: "useImpactDeltas hook call at page level"
      pattern: "useImpactDeltas"
    - from: "src/client/routes/threads/$threadId.tsx"
      to: "src/client/hooks/useSetups.ts"
      via: "useSetup(selectedSetupId) for setup item data"
      pattern: "useSetup\\(selectedSetupId"
    - from: "src/client/routes/threads/$threadId.tsx"
      to: "src/client/stores/uiStore.ts"
      via: "selectedSetupId state read"
      pattern: "useUIStore.*selectedSetupId"
    - from: "src/client/components/SetupImpactSelector.tsx"
      to: "src/client/stores/uiStore.ts"
      via: "setSelectedSetupId state write"
      pattern: "setSelectedSetupId"
    - from: "src/client/components/ImpactDeltaBadge.tsx"
      to: "src/client/lib/impactDeltas.ts"
      via: "CandidateDelta type import"
      pattern: "import.*CandidateDelta"
    - from: "src/client/components/ComparisonTable.tsx"
      to: "src/client/lib/impactDeltas.ts"
      via: "ImpactDeltas type for deltas prop"
      pattern: "import.*ImpactDeltas"
---

<objective>
Build the UI components (setup dropdown + delta badges) and wire them into the thread detail page across all three view modes (list, grid, compare).

Purpose: This is the user-facing delivery of the impact preview feature. Plan 01 built the logic; this plan renders it.
Output: SetupImpactSelector component, ImpactDeltaBadge component, updated CandidateListItem/CandidateCard/ComparisonTable with delta rendering, wired thread detail page.
</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/13-setup-impact-preview/13-RESEARCH.md
@.planning/phases/13-setup-impact-preview/13-01-SUMMARY.md

<interfaces>
<!-- Types created by Plan 01 that this plan consumes -->

From src/client/lib/impactDeltas.ts (created in Plan 01):
```typescript
export interface CandidateInput {
  id: number;
  weightGrams: number | null;
  priceCents: number | null;
}

export type DeltaMode = "replace" | "add" | "none";

export interface CandidateDelta {
  candidateId: number;
  mode: DeltaMode;
  weightDelta: number | null;
  priceDelta: number | null;
  replacedItemName: string | null;
}

export interface ImpactDeltas {
  mode: DeltaMode;
  deltas: Record<number, CandidateDelta>;
}
```

From src/client/hooks/useImpactDeltas.ts (created in Plan 01):
```typescript
export function useImpactDeltas(
  candidates: CandidateInput[],
  setupItems: SetupItemWithCategory[] | undefined,
  threadCategoryId: number,
): ImpactDeltas;
```

From src/client/hooks/useSetups.ts:
```typescript
export function useSetups(): UseQueryResult<SetupListItem[]>;
export function useSetup(setupId: number | null): UseQueryResult<SetupWithItems>;
```

From src/client/stores/uiStore.ts (updated in Plan 01):
```typescript
selectedSetupId: number | null;
setSelectedSetupId: (id: number | null) => void;
```

From src/client/hooks/useThreads.ts (updated in Plan 01):
```typescript
interface ThreadWithCandidates {
  id: number;
  name: string;
  status: "active" | "resolved";
  resolvedCandidateId: number | null;
  categoryId: number;  // <-- Added in Plan 01
  createdAt: string;
  updatedAt: string;
  candidates: CandidateWithCategory[];
}
```

From src/client/lib/formatters.ts:
```typescript
export function formatWeight(grams: number | null | undefined, unit: WeightUnit = "g"): string;
export function formatPrice(cents: number | null | undefined, currency: Currency = "USD"): string;
```

Existing component props that need delta additions:

CandidateListItem props:
```typescript
interface CandidateListItemProps {
  candidate: CandidateWithCategory;
  rank: number;
  isActive: boolean;
  onStatusChange: (status: "researching" | "ordered" | "arrived") => void;
  // Will add: delta?: CandidateDelta;
}
```

CandidateCard props:
```typescript
interface CandidateCardProps {
  id: number;
  name: string;
  weightGrams: number | null;
  priceCents: number | null;
  // ... other props
  // Will add: delta?: CandidateDelta;
}
```

ComparisonTable props:
```typescript
interface ComparisonTableProps {
  candidates: CandidateWithCategory[];
  resolvedCandidateId: number | null;
  // Will add: deltas?: Record<number, CandidateDelta>;
}
```
</interfaces>
</context>

<tasks>

<task type="auto">
  <name>Task 1: Create SetupImpactSelector and ImpactDeltaBadge components</name>
  <files>src/client/components/SetupImpactSelector.tsx, src/client/components/ImpactDeltaBadge.tsx</files>
  <action>
    1. Create src/client/components/SetupImpactSelector.tsx:
       - Import useSetups from hooks/useSetups
       - Import useUIStore from stores/uiStore
       - Export function SetupImpactSelector()
       - Read selectedSetupId and setSelectedSetupId from uiStore
       - Fetch setups via useSetups()
       - If no setups or loading, return null
       - Render: a flex row with label "Impact on setup:" (text-xs text-gray-500) and a native `<select>` element
       - Select value = selectedSetupId ?? "", onChange parses to number or null
       - Options: "None" (value="") + each setup by name
       - Styling: text-sm border border-gray-200 rounded-lg px-2 py-1 text-gray-700 bg-white focus:outline-none focus:ring-1 focus:ring-gray-300

    2. Create src/client/components/ImpactDeltaBadge.tsx:
       - Import type CandidateDelta from lib/impactDeltas
       - Import formatWeight, formatPrice, WeightUnit, Currency from lib/formatters
       - Import useWeightUnit from hooks/useWeightUnit
       - Import useCurrency from hooks/useCurrency
       - Export function ImpactDeltaBadge({ delta, type }: { delta: CandidateDelta | undefined; type: "weight" | "price" })
       - If !delta or delta.mode === "none", return null
       - Pick value: type === "weight" ? delta.weightDelta : delta.priceDelta
       - If value === null (no data): render `<span className="text-xs text-gray-300">` with "-- (no weight data)" or "-- (no price data)" depending on type
       - If value is a number:
         - formatted = type === "weight" ? formatWeight(Math.abs(value), unit) : formatPrice(Math.abs(value), currency)
         - sign: value > 0 -> "+" , value < 0 -> "-" (use minus sign), value === 0 -> +/-
         - colorClass: value < 0 -> "text-green-600" (lighter/cheaper is good), value > 0 -> "text-red-500", value === 0 -> "text-gray-400"
         - modeLabel: delta.mode === "add" ? " (add)" : ""
         - vsLabel: delta.mode === "replace" && delta.replacedItemName ? ` vs ${delta.replacedItemName}` : "" (only show this as a title attribute on the span, not inline text -- too long)
         - Render: `<span className="text-xs font-medium {colorClass}" title={vsLabel || undefined}>{sign}{formatted}{modeLabel}</span>`
       - The component reads unit/currency internally via hooks so callers don't need to pass them.
  </action>
  <verify>
    <automated>cd /home/jlmak/Projects/jlmak/GearBox && bun run lint 2>&1 | head -20</automated>
  </verify>
  <done>SetupImpactSelector renders a setup dropdown reading from uiStore. ImpactDeltaBadge renders signed, colored delta indicators with null-data handling. Both components lint-clean.</done>
</task>

<task type="auto">
  <name>Task 2: Wire impact preview into thread detail page and all candidate views</name>
  <files>src/client/routes/threads/$threadId.tsx, src/client/components/CandidateListItem.tsx, src/client/components/CandidateCard.tsx, src/client/components/ComparisonTable.tsx</files>
  <action>
    1. In src/client/routes/threads/$threadId.tsx:
       - Add imports: useSetup from hooks/useSetups, useImpactDeltas from hooks/useImpactDeltas, SetupImpactSelector from components/SetupImpactSelector, type CandidateDelta from lib/impactDeltas
       - Read selectedSetupId from useUIStore: `const selectedSetupId = useUIStore((s) => s.selectedSetupId);`
       - Fetch setup data: `const { data: setupData } = useSetup(selectedSetupId ?? null);`
       - Compute deltas: `const impactDeltas = useImpactDeltas(thread.candidates, setupData?.items, thread.categoryId);` (place after thread is loaded, inside the render body after the isLoading/isError guards)
       - Place `<SetupImpactSelector />` in the header section, after the thread name/status row and before the toolbar. Wrap it in a div for spacing if needed.
       - Pass delta to CandidateListItem: add prop `delta={impactDeltas.deltas[candidate.id]}` to each CandidateListItem (both Reorder.Group and static div renderings)
       - Pass delta to CandidateCard: add prop `delta={impactDeltas.deltas[candidate.id]}` (the CandidateCard receives individual props, so pass it as `delta={impactDeltas.deltas[candidate.id]}`)
       - Pass deltas to ComparisonTable: add prop `deltas={impactDeltas.deltas}` alongside existing candidates and resolvedCandidateId

    2. In src/client/components/CandidateListItem.tsx:
       - Import type CandidateDelta from lib/impactDeltas
       - Import ImpactDeltaBadge from ./ImpactDeltaBadge
       - Add `delta?: CandidateDelta;` to CandidateListItemProps
       - Add `delta` to destructured props
       - Render two ImpactDeltaBadge components inside the badges flex-wrap div (after the existing weight and price badges):
         - `{delta && <ImpactDeltaBadge delta={delta} type="weight" />}`
         - `{delta && <ImpactDeltaBadge delta={delta} type="price" />}`
       - Place these AFTER the existing weight/price badges so they appear as secondary indicators

    3. In src/client/components/CandidateCard.tsx:
       - Import type CandidateDelta from lib/impactDeltas
       - Import ImpactDeltaBadge from ./ImpactDeltaBadge
       - Add `delta?: CandidateDelta;` to CandidateCardProps
       - Add `delta` to destructured props
       - Render two ImpactDeltaBadge components inside the badges flex-wrap div (after weight/price badges):
         - `{delta && <ImpactDeltaBadge delta={delta} type="weight" />}`
         - `{delta && <ImpactDeltaBadge delta={delta} type="price" />}`

    4. In src/client/components/ComparisonTable.tsx:
       - Import type CandidateDelta from lib/impactDeltas
       - Import ImpactDeltaBadge from ./ImpactDeltaBadge
       - Add `deltas?: Record<number, CandidateDelta>;` to ComparisonTableProps
       - Add `deltas` to destructured props
       - Add two new rows to ATTRIBUTE_ROWS array, placed right after the "weight" row and "price" row respectively:
         a. After "weight" row, add:
            ```
            {
              key: "impact-weight",
              label: "Impact (wt)",
              render: (c) => deltas?.[c.id] ? <ImpactDeltaBadge delta={deltas[c.id]} type="weight" /> : <span className="text-gray-300">--</span>,
            }
            ```
         b. After "price" row, add:
            ```
            {
              key: "impact-price",
              label: "Impact ($)",
              render: (c) => deltas?.[c.id] ? <ImpactDeltaBadge delta={deltas[c.id]} type="price" /> : <span className="text-gray-300">--</span>,
            }
            ```
       - These are separate rows (per research recommendation) to avoid conflating candidate-relative deltas with setup impact deltas.
       - The impact rows show "--" when no setup is selected (deltas undefined or no entry for candidate).
  </action>
  <verify>
    <automated>cd /home/jlmak/Projects/jlmak/GearBox && bun test && bun run lint 2>&1 | head -20</automated>
  </verify>
  <done>
    - SetupImpactSelector dropdown visible in thread header
    - Selecting a setup shows weight/cost delta badges on each candidate in list, grid, and compare views
    - Replace mode: signed delta with green (lighter/cheaper) or red (heavier/pricier) coloring
    - Add mode: positive delta with "(add)" label
    - Null weight/price: shows "-- (no weight data)" / "-- (no price data)" indicator
    - Deselecting setup clears all delta indicators
    - ComparisonTable has dedicated "Impact (wt)" and "Impact ($)" rows
    - All tests pass, lint clean
  </done>
</task>

</tasks>

<verification>
- `bun test` full suite passes
- `bun run lint` clean
- SetupImpactSelector renders in thread header with all setups as options
- Selecting a setup triggers useSetup fetch and delta computation
- CandidateListItem, CandidateCard, ComparisonTable all render delta badges
- Replace mode detected when setup has item in same category as thread
- Add mode used otherwise
- Null weight/price shows clear indicator
- Deselecting shows no deltas (clean state)
</verification>

<success_criteria>
- All four IMPC requirements visible in the UI
- Delta rendering works across list, grid, and compare views
- No regressions in existing functionality
- Clean lint output
</success_criteria>

<output>
After completion, create `.planning/phases/13-setup-impact-preview/13-02-SUMMARY.md`
</output>
