# Phase 11: Candidate Ranking - Context

**Gathered:** 2026-03-16
**Status:** Ready for planning

<domain>
## Phase Boundary

Users can drag candidates into a priority order within a thread. The rank persists across sessions and is visually communicated with medal badges on the top 3. Drag handles and reordering are disabled on resolved threads. Comparison view, impact preview, and pros/cons editing are separate phases.

</domain>

<decisions>
## Implementation Decisions

### Card layout and view toggle
- Add a grid/list view toggle in the thread header (list view is default)
- List view: vertical stack of horizontal cards (image thumbnail on left, name + badges on right) — enables drag-to-reorder
- Grid view: current 3-column responsive card layout preserved
- Both views render candidates in rank order (sort_order ascending)
- Rank badges visible in both views

### Drag handle design
- Always-visible GripVertical icon (Lucide) on the left side of each list-view card
- Grip icon color: muted gray (text-gray-300), darkens to text-gray-500 on hover
- Cursor changes to 'grab' on hover, 'grabbing' during drag
- Drag feedback: elevated card with shadow + scale-up effect; other cards animate to show drop target gap (standard framer-motion Reorder behavior)
- On resolved threads: grip icon disappears entirely (not disabled/grayed)
- Drag only available in list view (grid view has no drag handles)

### Rank badge style
- Medal icons (Lucide 'medal' or 'trophy') in gold (#D4AF37), silver (#C0C0C0), bronze (#CD7F32) for top 3 candidates
- Positioned inline before the candidate name text
- Candidates ranked 4th and below show no rank indicator — position implied by list order
- On resolved threads: rank badges remain visible (static, read-only) — **overrides roadmap success criteria #4 which said "rank badges absent on resolved thread"**; user prefers retrospective visibility

### Sort order and persistence
- Schema migration adds `sort_order REAL NOT NULL DEFAULT 0` to `thread_candidates`
- Migration initializes existing candidates with spaced values (1000, 2000, 3000...) ordered by `created_at` — ensures immediate correct ordering
- Fractional indexing: only the moved item gets a single UPDATE (midpoint between neighbors)
- New candidates added to a thread get the highest sort_order (appended to bottom of rank)
- Auto-save on drop — no "Save order" button; reorder persists immediately via `PATCH /api/threads/:id/candidates/reorder`
- `tempItems` local state pattern: render from `tempItems ?? queryData.candidates`; clear on mutation `onSettled` — prevents React Query flicker

### Claude's Discretion
- Exact horizontal card dimensions and spacing in list view
- Grid/list toggle icon style and placement
- Drag animation timing and spring config
- Image thumbnail size in list view cards
- How action buttons (Winner, Delete, Link) adapt to horizontal card layout
- Keyboard accessibility for reordering (arrow keys to move)

</decisions>

<code_context>
## Existing Code Insights

### Reusable Assets
- `CandidateCard` (`src/client/components/CandidateCard.tsx`): Current card component — needs horizontal variant for list view, or a new `CandidateListItem` component
- `StatusBadge` (`src/client/components/StatusBadge.tsx`): Click-to-cycle pattern reusable in list view
- `useCandidates.ts` hooks: `useCreateCandidate`, `useUpdateCandidate`, `useDeleteCandidate` — need new `useReorderCandidates` mutation
- `useThread` hook: Returns thread with `candidates[]` array — already has all data needed, just needs sort_order ordering
- `formatWeight`/`formatPrice` formatters: Reuse in list view card badges
- `useWeightUnit`/`useCurrency` hooks: Already used by CandidateCard
- `LucideIcon` helper: For GripVertical drag handle and medal rank badges
- `uiStore` (Zustand): Add `candidateViewMode: 'list' | 'grid'` for view toggle persistence

### Established Patterns
- framer-motion@12.37.0 already installed — `Reorder.Group`/`Reorder.Item` for drag ordering
- React Query for server data, Zustand for UI-only state
- Pill badges: blue for weight, green for price, gray for category, purple for pros/cons
- Services accept db as first param (DI pattern for testability)
- API validation via `@hono/zod-validator` with Zod schemas
- Hover-reveal action buttons on CandidateCard (Winner, Delete, External link)

### Integration Points
- `src/db/schema.ts`: Add `sortOrder: real("sort_order").notNull().default(0)` to `threadCandidates`
- `src/server/services/thread.service.ts`: New `reorderCandidates()` function (transactional); update `getCandidates` to ORDER BY sort_order
- `src/server/routes/threads.ts`: New `PATCH /:id/candidates/reorder` endpoint; reject if thread is resolved
- `src/shared/schemas.ts`: New `reorderCandidatesSchema` (z.object with orderedIds array)
- `src/client/routes/threads/$threadId.tsx`: Wrap candidates in `Reorder.Group`, add view toggle, use tempItems pattern
- `src/client/hooks/useCandidates.ts`: New `useReorderCandidates` mutation hook
- `tests/helpers/db.ts`: Update CREATE TABLE for thread_candidates to include sort_order column
- Drizzle migration: `sort_order REAL NOT NULL DEFAULT 0` + data migration to space existing rows

</code_context>

<specifics>
## Specific Ideas

- List view cards should feel like Trello or Linear cards — horizontal layout, grip handle on the left, compact but informative
- Drag feedback should use standard framer-motion spring animations (elevated + gap), not custom physics
- Medal badges should use actual metallic-feeling colors (gold #D4AF37, silver #C0C0C0, bronze #CD7F32), not generic highlight colors

</specifics>

<deferred>
## Deferred Ideas

None — discussion stayed within phase scope

</deferred>

---

*Phase: 11-candidate-ranking*
*Context gathered: 2026-03-16*
