Files
Jean-Luc Makiola a826381981
Some checks failed
CI / ci (push) Failing after 19s
docs(13): create phase plan
2026-03-17 16:53:47 +01:00

14 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
13-setup-impact-preview 02 execute 2
13-01
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
true
IMPC-01
IMPC-02
IMPC-03
IMPC-04
truths artifacts key_links
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
path provides exports
src/client/components/SetupImpactSelector.tsx Setup dropdown for thread header
SetupImpactSelector
path provides exports
src/client/components/ImpactDeltaBadge.tsx Inline delta indicator component
ImpactDeltaBadge
path provides
src/client/routes/threads/$threadId.tsx Thread detail page wired with impact preview
path provides
src/client/components/CandidateListItem.tsx List item with delta badges
path provides
src/client/components/CandidateCard.tsx Card with delta badges
path provides
src/client/components/ComparisonTable.tsx Comparison table with impact delta rows
from to via pattern
src/client/routes/threads/$threadId.tsx src/client/hooks/useImpactDeltas.ts useImpactDeltas hook call at page level useImpactDeltas
from to via pattern
src/client/routes/threads/$threadId.tsx src/client/hooks/useSetups.ts useSetup(selectedSetupId) for setup item data useSetup(selectedSetupId
from to via pattern
src/client/routes/threads/$threadId.tsx src/client/stores/uiStore.ts selectedSetupId state read useUIStore.*selectedSetupId
from to via pattern
src/client/components/SetupImpactSelector.tsx src/client/stores/uiStore.ts setSelectedSetupId state write setSelectedSetupId
from to via pattern
src/client/components/ImpactDeltaBadge.tsx src/client/lib/impactDeltas.ts CandidateDelta type import import.*CandidateDelta
from to via pattern
src/client/components/ComparisonTable.tsx src/client/lib/impactDeltas.ts ImpactDeltas type for deltas prop import.*ImpactDeltas
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.

<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/13-setup-impact-preview/13-RESEARCH.md @.planning/phases/13-setup-impact-preview/13-01-SUMMARY.md

From src/client/lib/impactDeltas.ts (created in Plan 01):

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):

export function useImpactDeltas(
  candidates: CandidateInput[],
  setupItems: SetupItemWithCategory[] | undefined,
  threadCategoryId: number,
): ImpactDeltas;

From src/client/hooks/useSetups.ts:

export function useSetups(): UseQueryResult<SetupListItem[]>;
export function useSetup(setupId: number | null): UseQueryResult<SetupWithItems>;

From src/client/stores/uiStore.ts (updated in Plan 01):

selectedSetupId: number | null;
setSelectedSetupId: (id: number | null) => void;

From src/client/hooks/useThreads.ts (updated in Plan 01):

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:

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:

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

CandidateCard props:

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

ComparisonTable props:

interface ComparisonTableProps {
  candidates: CandidateWithCategory[];
  resolvedCandidateId: number | null;
  // Will add: deltas?: Record<number, CandidateDelta>;
}
Task 1: Create SetupImpactSelector and ImpactDeltaBadge components src/client/components/SetupImpactSelector.tsx, src/client/components/ImpactDeltaBadge.tsx 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 `` 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. cd /home/jlmak/Projects/jlmak/GearBox && bun run lint 2>&1 | head -20 SetupImpactSelector renders a setup dropdown reading from uiStore. ImpactDeltaBadge renders signed, colored delta indicators with null-data handling. Both components lint-clean. Task 2: Wire impact preview into thread detail page and all candidate views src/client/routes/threads/$threadId.tsx, src/client/components/CandidateListItem.tsx, src/client/components/CandidateCard.tsx, src/client/components/ComparisonTable.tsx 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 `` 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). cd /home/jlmak/Projects/jlmak/GearBox && bun test && bun run lint 2>&1 | head -20 - 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 - `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) <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> After completion, create `.planning/phases/13-setup-impact-preview/13-02-SUMMARY.md`