183 lines
20 KiB
Markdown
183 lines
20 KiB
Markdown
# 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
|
|
|
|
### Recommended Stack
|
|
|
|
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.1` — `peerDep 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 threads** — `thread.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)
|
|
- `bun.lock` (project lockfile) — framer-motion v12.37.0 peerDeps `"react: ^18.0.0 || ^19.0.0"` confirmed
|
|
- [Motion Reorder docs](https://motion.dev/docs/react-reorder) — `Reorder.Group`, `Reorder.Item`, `onDragEnd` API
|
|
- [dnd-kit Discussion #1522](https://github.com/clauderic/dnd-kit/discussions/1522) — `tempItems` solution for React Query cache flicker
|
|
- [dnd-kit Issue #921](https://github.com/clauderic/dnd-kit/issues/921) — root cause of state lifecycle mismatch
|
|
- [Fractional Indexing — steveruiz.me](https://www.steveruiz.me/posts/reordering-fractional-indices) — why float sort keys beat integer reorder for databases
|
|
- [Baymard Institute: Comparison Tool Design](https://baymard.com/blog/user-friendly-comparison-tools) — sticky headers, horizontal scroll, minimum column width
|
|
- [NNGroup: Comparison Tables](https://www.nngroup.com/articles/comparison-tables/) — information architecture, anti-patterns
|
|
- [Smashing Magazine: Feature Comparison Table](https://www.smashingmagazine.com/2017/08/designing-perfect-feature-comparison-table/) — table layout patterns
|
|
- GearBox codebase direct analysis (`src/db/schema.ts`, `src/server/services/`, `src/client/hooks/`, `tests/helpers/db.ts`) — confirmed existing patterns, missing columns, integration points
|
|
|
|
### Secondary (MEDIUM confidence)
|
|
- [@dnd-kit/core npm](https://www.npmjs.com/package/@dnd-kit/core) — v6.3.1 last published ~1 year ago, no React 19
|
|
- [dnd-kit React 19 issue #1511](https://github.com/clauderic/dnd-kit/issues/1511) — CLOSED but React 19 TypeScript issues confirmed
|
|
- [@dnd-kit/react roadmap discussion #1842](https://github.com/clauderic/dnd-kit/discussions/1842) — 0 maintainer replies; pre-1.0 risk signal
|
|
- [hello-pangea/dnd React 19 issue #864](https://github.com/hello-pangea/dnd/issues/864) — still open as of Jan 2026
|
|
- [BrightCoding dnd-kit deep dive (2025)](https://www.blog.brightcoding.dev/2025/08/21/the-ultimate-drag-and-drop-toolkit-for-react-a-deep-dive-into-dnd-kit/) — react-beautiful-dnd abandoned; dnd-kit current standard but React 19 gap confirmed
|
|
- [TrailsMag: Leaving LighterPack](https://trailsmag.net/blogs/hiker-box/ultralight-the-gear-tracking-app-i-m-leaving-lighterpack-for) — LighterPack feature gap analysis
|
|
- [Contentsquare: Comparing products UX](https://contentsquare.com/blog/comparing-products-design-practices-to-help-your-users-avoid-fragmented-comparison-7/) — fragmented comparison pitfalls
|
|
|
|
### Tertiary (LOW confidence)
|
|
- [Fractional Indexing SQLite library](https://github.com/sqliteai/fractional-indexing) — implementation reference for lexicographic sort keys (pattern reference only; direct float arithmetic sufficient for this use case)
|
|
- [Top 5 Drag-and-Drop Libraries for React 2026](https://puckeditor.com/blog/top-5-drag-and-drop-libraries-for-react) — ecosystem overview confirming dnd-kit and hello-pangea/dnd limitations
|
|
|
|
---
|
|
*Research completed: 2026-03-16*
|
|
*Ready for roadmap: yes*
|