Files
GearBox/.planning/research/SUMMARY.md

20 KiB

Project Research Summary

Project: GearBox v1.3 — Research & Decision Tools Domain: Gear management — candidate comparison, setup impact preview, drag-to-reorder ranking with pros/cons Researched: 2026-03-16 Confidence: HIGH

Executive Summary

GearBox v1.3 adds three decision-support features to the existing thread detail page: side-by-side candidate comparison, setup impact preview (weight/cost delta), and drag-to-reorder candidate ranking with pros/cons annotation. All four research areas converge on the same conclusion — the existing stack is sufficient and no new dependencies are required. framer-motion@12.37.0 (already installed) provides the Reorder component for drag-to-reorder, eliminating the need for @dnd-kit (which lacks React 19 support) or any other library. Two of the three features (comparison view and impact preview) require zero schema changes and can be built as pure client-side derived views using data already cached by useThread() and useSetup().

The recommended build sequence is dependency-driven: schema migration first (adds sort_order, pros, cons to thread_candidates), then ranking UI (uses the new columns), then comparison view and impact preview in parallel (both are schema-independent client additions). This order eliminates the risk of mid-feature migrations and ensures the comparison table can display rank, pros, and cons from day one rather than being retrofitted. The entire milestone touches 3 new files and 10 modified files — a contained, low-blast-radius changeset.

The primary risks are implementation-level rather than architectural. Three patterns require deliberate design before coding: (1) use tempItems local state alongside React Query for drag reorder to prevent the well-documented flicker bug, (2) use sortOrder REAL (fractional) instead of INTEGER to avoid bulk UPDATE writes on every drag, and (3) treat impact preview as an "add vs replace" decision — not just a pure addition — since users comparing gear are almost always replacing an existing item, not stacking one on top. All three are avoidable with upfront design; recovery cost is low but retrofitting is disruptive.

Key Findings

Zero new dependencies are needed for this milestone. The existing stack handles all three features: Tailwind CSS for the comparison table layout, framer-motion's Reorder component for drag ordering, Drizzle ORM + Hono + Zod for the one new write endpoint (PATCH /api/threads/:id/candidates/reorder), and TanStack Query for the new useReorderCandidates mutation. All other React Query hooks (useThread, useSetup, useSetups) already exist and return the data needed for comparison and impact preview without modification.

Core technologies:

  • framer-motion@12.37.0 (Reorder component): drag-to-reorder — already installed, React 19 peerDeps confirmed in bun.lock, replaces any need for @dnd-kit
  • drizzle-orm@0.45.1: three new columns on thread_candidates (sort_order REAL, pros TEXT, cons TEXT) plus one new service function (reorderCandidates)
  • Tailwind CSS v4: comparison table layout with overflow-x-auto, sticky left-0 for frozen label column, min-w-[200px] per candidate column
  • TanStack Query v5 + existing hooks: impact preview and comparison view derived entirely from cached useThread + useSetup data — no new API endpoints on read paths
  • Zod v4: extend updateCandidateSchema with sortOrder: z.number().finite(), pros: z.string().max(500).optional(), cons: z.string().max(500).optional()

What NOT to use:

  • @dnd-kit/core@6.3.1 — no React 19 support, unmaintained for ~1 year
  • @dnd-kit/react@0.3.2 — pre-1.0, no maintainer response on stability
  • @hello-pangea/dnd@18.0.1peerDep react: "^18.0.0" only, stale
  • Any third-party comparison table component — custom Tailwind table is trivial and design-consistent

Expected Features

All five v1.3 features are confirmed as P1 (must-have for this milestone). No existing gear management tool (LighterPack, GearGrams, OutPack) has comparison view, delta preview, or ranking — these are unmet-need differentiators adapted from e-commerce comparison UX to the gear domain.

Must have (table stakes):

  • Side-by-side comparison view — users juggling 3+ candidates mentally across cards expect tabular layout; NNGroup and Smashing Magazine confirm this is the standard for comparison contexts
  • Weight and cost delta per candidate — gear apps always display weight prominently; delta is more actionable than raw weight
  • Setup selector for impact preview — required to contextualize the delta; useSetups() already exists

Should have (differentiators):

  • Drag-to-rank ordering — makes priority explicit without numeric input; no competitor has this in the gear domain; requires sort_order schema migration
  • Per-candidate pros/cons fields — structured decision rationale; stored as newline-delimited text (renders as bullets in comparison view); requires pros/cons schema migration

Defer (v2+):

  • Classification-aware impact breakdown (base/worn/consumable) — data available but UI complexity high; flat delta covers 90% of use case
  • Rank badge on card grid — useful but low urgency; add when users express confusion
  • Mobile-optimized comparison view (swipe between candidates) — horizontal scroll works for now
  • Comparison permalink — requires auth/multi-user work not in scope for v1

Anti-features (explicitly rejected):

  • Custom comparison attributes — complexity trap, rejected in PROJECT.md
  • Score/rating calculation — opaque algorithms distrust; manual ranking expresses user preference better
  • Cross-thread comparison — candidates are decision-scoped; different categories are not apples-to-apples

Architecture Approach

All three features integrate on the /threads/$threadId route with no impact on other routes. The comparison view and impact preview are pure client-side derived views using data already in the React Query cache — no new API endpoints on read paths. The only new server-side endpoint is PATCH /:id/candidates/reorder which accepts { orderedIds: number[] } and applies a transactional bulk-update in thread.service.ts. The uiStore (Zustand) gains two new fields: compareMode: boolean and impactSetupId: number | null, consistent with existing UI-state-only patterns.

Major components:

  1. CandidateCompare.tsx (new) — side-by-side table; columns = candidates, rows = attributes; pure presentational, derives deltas from thread.candidates[]; overflow-x-auto for narrow viewports; sticky label column
  2. SetupImpactRow.tsx (new) — delta display (+Xg / +$Y); reads from useSetup(impactSetupId) data passed as props; handles null weight case explicitly
  3. Reorder.Group / Reorder.Item (framer-motion, no new file) — wraps CandidateCard list in $threadId.tsx; onReorder updates local orderedCandidates state; onDragEnd fires useReorderCandidates mutation
  4. CandidateCard.tsx (modified) — gains rank prop (gold/silver/bronze badge for top 3), pros/cons indicator icons; isActive={false} when rendered inside comparison view
  5. CandidateForm.tsx (modified) — gains pros/cons textarea fields below existing Notes field

Key patterns to follow:

  • tempItems local state alongside React Query for drag reorder — prevents the documented flicker bug; do not use setQueryData alone
  • Client-computed derived data from cached queries — no new read endpoints (anti-pattern: building GET /api/threads/:id/compare or GET /api/threads/:id/impact)
  • uiStore for cross-panel persistent UI flags only — no server data in Zustand
  • Resolved-thread guard — thread.status === "resolved" must disable drag handles and block the reorder endpoint (data integrity requirement, not just UX)

Critical Pitfalls

  1. Drag flicker from setQueryData-only optimistic update — use tempItems local state (useState<Candidate[] | null>(null)); render from tempItems ?? queryData.candidates; clear on mutation onSettled. Must be designed before building the drag UI, not retrofitted. (PITFALLS.md Pitfall 1)

  2. Integer sortOrder causes bulk writes — use REAL (float) type for sort_order column with fractional indexing so only the moved item requires a single UPDATE. With 8+ candidates and rapid dragging, integer bulk updates produce visible latency and hold a SQLite write lock. Start values at 1000 with 1000-unit gaps. (PITFALLS.md Pitfall 2)

  3. Impact preview shows wrong delta (add vs replace) — default to "replace" mode when a setup item exists in the same category as the thread; default to "add" mode when no category match. Pure-addition delta misleads users: a 500g candidate replacing an 800g item shows "+500g" instead of "-300g". The distinction must be designed into the service layer, not retrofitted. (PITFALLS.md Pitfall 6)

  4. Comparison/rank on resolved threadsthread.status === "resolved" must hide drag handles, disable rank mutation, and show a read-only summary. The reorder API route must return 400 for resolved threads. This is a data integrity issue, not just UX. (PITFALLS.md Pitfall 8)

  5. Test helper schema drift — every schema change must update tests/helpers/db.ts in the same commit. Run bun test immediately after schema + helper update. Missing this produces SqliteError: no such column failures. (PITFALLS.md Pitfall 7)

Implications for Roadmap

Based on research, a 4-phase structure is recommended with a clear dependency order: schema foundation first, ranking second (consumes new columns), then comparison view and impact preview as sequential client-only phases.

Phase 1: Schema Foundation + Pros/Cons Fields

Rationale: All ranking and pros/cons work shares a schema migration. Batching sort_order, pros, and cons into a single migration avoids multiple ALTER TABLE runs and ensures the test helper is updated once. Pros/cons field UI is low-complexity (two textareas in CandidateForm) and can be delivered immediately after the migration, making candidates richer before ranking is built. Delivers: sort_order REAL NOT NULL DEFAULT 0, pros TEXT, cons TEXT on thread_candidates; pros/cons visible in candidate edit panel; CandidateCard shows pros/cons indicator icons; tests/helpers/db.ts updated; Zod schemas extended with 500-char length caps Addresses: Side-by-side comparison row data (pros/cons), drag-to-rank prerequisite (sort_order) Avoids: Test helper schema drift (Pitfall 7), pros/cons as unstructured blobs (Pitfall 5 — newline-delimited format chosen at schema time)

Phase 2: Drag-to-Reorder Candidate Ranking

Rationale: Depends on Phase 1 (sort_order column must exist). Schema work is done; this phase is pure service + client. The tempItems pattern must be implemented correctly from the start to prevent the React Query flicker bug. Delivers: reorderCandidates service function (transactional loop); PATCH /api/threads/:id/candidates/reorder endpoint with thread ownership validation; useReorderCandidates mutation hook; Reorder.Group / Reorder.Item in thread detail route; rank badge (gold/silver/bronze) on CandidateCard; resolved-thread guard (no drag handles, API returns 400 for resolved) Uses: framer-motion@12.37.0 Reorder API (already installed), Drizzle ORM transaction, fractional sort_order REAL arithmetic (single UPDATE per drag) Avoids: dnd-kit flicker (Pitfall 1 — tempItems pattern), bulk integer writes (Pitfall 2 — REAL type), resolved-thread corruption (Pitfall 8)

Phase 3: Side-by-Side Comparison View

Rationale: No schema dependency — can technically be built before Phase 2, but is most useful when rank, pros, and cons are already in the data model so the comparison table shows the full picture from day one. Pure client-side presentational component; no API changes. Delivers: CandidateCompare.tsx component; "Compare" toggle button in thread header; compareMode in uiStore; comparison table with sticky label column, horizontal scroll, weight/price relative deltas (lightest/cheapest candidate highlighted); responsive at 768px viewport; read-only summary for resolved threads Implements: Client-computed derived data pattern — data from useThread() cache; Math.min across candidates for relative delta; formatWeight/formatPrice for display Avoids: Comparison breaking at narrow widths (Pitfall 4 — overflow-x-auto + min-w-[200px]), comparison visible on resolved threads (Pitfall 8), server endpoint for comparison deltas (architecture anti-pattern)

Phase 4: Setup Impact Preview

Rationale: No schema dependency. Easiest to build last because the comparison view UI (Phase 3) already establishes the thread header area where the setup selector lives. Both add-mode and replace-mode deltas must be designed here to avoid the misleading pure-addition delta. Delivers: Setup selector dropdown in thread header (useSetups() data); SetupImpactRow.tsx component; impactSetupId in uiStore; add-mode delta and replace-mode delta (auto-defaults to replace when same-category item exists in setup); null weight guard ("-- (no weight data)" not "+0g"); unit-aware display via useWeightUnit() / useCurrency() Uses: Existing useSetup(id) hook (no new API), existing formatWeight / formatPrice formatters, categoryId on thread for replacement item detection Avoids: Stale data in impact preview (Pitfall 3 — reactive useQuery for setup data), wrong delta from add-vs-replace confusion (Pitfall 6), null weight treated as 0 (integration gotcha), server endpoint for delta calculation (architecture anti-pattern)

Phase Ordering Rationale

  • Phase 1 before all others: SQLite schema changes batched into a single migration; test helper updated once; pros/cons in edit panel adds value immediately without waiting for the comparison view
  • Phase 2 before Phase 3: rank data (sort order, rank badge) is more valuable displayed in the comparison table than in the card grid alone; building the comparison view after ranking ensures the table is complete on first delivery
  • Phase 3 before Phase 4: comparison view establishes the thread header chrome (toggle button area) where the setup selector in Phase 4 will live; building header UI in Phase 3 reduces Phase 4 scope
  • Phases 3 and 4 are technically independent and could parallelize, but sequencing them keeps the thread detail header changes contained to one phase at a time

Research Flags

Phases that need careful plan review before execution (not full research-phase, but plan must address specific design decisions):

  • Phase 2: The tempItems local state pattern and fractional sort_order arithmetic are non-obvious. The PLAN.md must spell these out explicitly before coding. PITFALLS.md Pitfall 1 and Pitfall 2 must be addressed in the plan, not discovered during implementation.
  • Phase 4: The add-vs-replace distinction requires deliberate design (which mode is default, how replacement item is detected by category, how null weight is surfaced). PITFALLS.md Pitfall 6 must be resolved in the plan before the component is built.

Phases with standard patterns (can skip /gsd:research-phase):

  • Phase 1: Standard Drizzle migration + Zod schema extension; established patterns in the codebase; ARCHITECTURE.md provides exact column definitions
  • Phase 3: Pure presentational component; Tailwind comparison table is well-documented; ARCHITECTURE.md provides complete component structure, props interface, and delta calculation code

Confidence Assessment

Area Confidence Notes
Stack HIGH Verified from bun.lock (framer-motion React 19 peerDeps confirmed); dnd-kit abandonment verified via npm + GitHub; Motion Reorder API verified via motion.dev docs
Features HIGH Codebase analysis confirmed no rank/pros/cons columns in existing schema; NNGroup + Smashing Magazine for comparison UX patterns; competitor analysis (LighterPack, GearGrams, OutPack) confirmed feature gap
Architecture HIGH Full integration map derived from direct codebase analysis; build order confirmed by column dependency graph; all changed files enumerated (3 new, 10 modified); complete code patterns provided
Pitfalls HIGH dnd-kit flicker: verified in GitHub Discussion #1522 and Issue #921; fractional indexing: verified via steveruiz.me and fractional-indexing library; comparison UX: Baymard Institute and NNGroup

Overall confidence: HIGH

Gaps to Address

  • Impact preview add-vs-replace UX: Research establishes that both modes are needed and when to default to each (same-category item in setup = replace mode). The exact affordance — dropdown to select which item is replaced vs. automatic category matching — is not fully specified. Recommendation: auto-match by category with a "change" link to override. Decide during Phase 4 planning.
  • Comparison view maximum candidate count: Research recommends 3-4 max for usability. GearBox has no current limit on candidates per thread. Whether to enforce a hard display limit (hide additional candidates behind "show more") or allow unrestricted horizontal scroll should be decided during Phase 3 planning.
  • Sort order initialization for existing candidates: When the migration runs, existing thread_candidates rows get sort_order = 0 (default). Phase 1 plan must specify whether to initialize existing candidates with spaced values (e.g., 1000, 2000, 3000) at migration time or accept that all existing rows start at 0 and rely on first drag to establish order.

Sources

Primary (HIGH confidence)

Secondary (MEDIUM confidence)

Tertiary (LOW confidence)


Research completed: 2026-03-16 Ready for roadmap: yes