docs: complete project research
This commit is contained in:
@@ -1,207 +1,181 @@
|
||||
# Project Research Summary
|
||||
|
||||
**Project:** GearBox v1.2 -- Collection Power-Ups
|
||||
**Domain:** Gear management (bikepacking, sim racing, etc.) -- feature enhancement milestone
|
||||
**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.2 adds six features to the existing gear management app: item search/filter, weight classification (base/worn/consumable), weight distribution charts, candidate status tracking, weight unit selection, and a planning category filter upgrade. Research confirms that four of six features require zero new dependencies -- they are pure application logic built on the existing stack (Drizzle ORM, React Query, Zod, Tailwind). The sole new dependency is `react-minimal-pie-chart` (~2kB gzipped) for donut chart visualization. The codebase is well-positioned for these additions: the settings table already supports key-value preferences, the `setup_items` join table is the correct place for weight classification, and the client-side data model is small enough for in-memory filtering.
|
||||
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 approach is to build weight unit selection first because it refactors the `formatWeight` function that every subsequent feature depends on for display. Search/filter and candidate status tracking are independent and low-risk. Weight classification is the most architecturally significant change -- it adds a column to the `setup_items` join table and changes the sync API shape from `{ itemIds: number[] }` to `{ items: Array<{ itemId, weightClass }> }`. Weight distribution charts come last because they depend on both the unit formatter and the classification data. The two schema changes (columns on `setup_items` and `thread_candidates`) should be batched into a single Drizzle migration.
|
||||
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: (1) weight unit conversion rounding drift from bidirectional conversion in edit forms, (2) accidentally placing weight classification on the `items` table instead of the `setup_items` join table, and (3) chart data diverging from displayed totals due to separate computation paths. All three are preventable with clear architectural rules established in the first phase: store grams canonically, convert only at the display boundary, and use a single source of truth for weight computations.
|
||||
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
|
||||
|
||||
The existing stack (React 19, Hono, Drizzle ORM, SQLite, Bun) handles all v1.2 features without modification. One small library addition is needed.
|
||||
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 (all existing, no changes):**
|
||||
- **Drizzle ORM `like()`, `eq()`, `and()`**: Available for server-side filtering if needed in the future, but client-side filtering is preferred at this scale
|
||||
- **Zod `z.enum()`**: Validates weight classification (`"base" | "worn" | "consumable"`) and candidate status (`"researching" | "ordered" | "arrived"`) with compile-time type safety
|
||||
- **React Query `useSetting()`**: Reactive settings caching ensures unit preference changes propagate to all weight displays without page refresh
|
||||
- **Existing `settings` table**: Key-value store supports weight unit preference with no schema change
|
||||
**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()`
|
||||
|
||||
**New dependency:**
|
||||
- **react-minimal-pie-chart ^9.1.2**: Donut/pie charts at ~2kB gzipped. React 19 compatible (explicit in peerDeps). Zero external dependencies. TypeScript native. Chosen over Recharts (~97kB, React 19 rendering issues reported) and Chart.js (~60kB, canvas-based, harder to style with Tailwind).
|
||||
|
||||
**What NOT to add:**
|
||||
- Recharts, Chart.js, or visx (massive overkill for one chart type)
|
||||
- Fuse.js or FTS5 (overkill for name search on sub-1000 item collections)
|
||||
- XState (candidate status is a simple enum, not a complex state machine)
|
||||
- i18n library for unit conversion (four constants and a formatter function)
|
||||
**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):**
|
||||
- **Search items by name** -- every competitor with an inventory has search; LighterPack notably lacks it and users complain
|
||||
- **Filter items by category** -- partially exists in planning view, missing from collection view
|
||||
- **Weight unit selection (g/oz/lb/kg)** -- universal across all competitors; gear specs come in mixed units
|
||||
- **Weight classification (base/worn/consumable)** -- pioneered by LighterPack, now industry standard; "base weight" is the core metric of the ultralight community
|
||||
- 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):**
|
||||
- **Weight distribution donut chart** -- LighterPack's pie chart is cited as its best feature; GearBox can uniquely combine category and classification breakdown
|
||||
- **Candidate status tracking (researching/ordered/arrived)** -- entirely unique to GearBox's planning thread concept; no competitor has purchase lifecycle tracking
|
||||
- **Per-setup classification** -- architecturally superior to competitors; the same item can be classified differently across setups
|
||||
- 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+):**
|
||||
- Per-item weight input in multiple units (parsing complexity)
|
||||
- Interactive chart drill-down (click to zoom into categories)
|
||||
- Weight goals/targets (opinionated norms conflict with hobby-agnostic design)
|
||||
- Custom weight classification labels beyond base/worn/consumable
|
||||
- Server-side full-text search (premature for single-user scale)
|
||||
- Status change timestamps on candidates (useful but not essential now)
|
||||
- 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 v1.2 features integrate into the existing three-layer architecture (client/server/database) with minimal structural changes. The client layer gains 5 new files (SearchBar, WeightChart, UnitSelector components; useFormatWeight hook; migration SQL) and modifies 15 existing files. The server layer changes are limited to the setup service (weight classification PATCH endpoint, updated sync function) and thread service (candidate status field passthrough). No new route registrations are needed in `src/server/index.ts`. The API layer (`lib/api.ts`) and UI state store (`uiStore.ts`) require no changes.
|
||||
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. **`useFormatWeight` hook** -- single source of truth for unit-aware weight formatting; wraps `useSetting("weightUnit")` and `formatWeight(grams, unit)` so all weight displays stay consistent
|
||||
2. **`WeightChart` component** -- reusable donut chart wrapper; used in collection page (weight by category) and setup detail page (weight by classification)
|
||||
3. **`SearchBar` component** -- reusable search input with clear button; collection page filters via `useMemo` over the cached `useItems()` data
|
||||
4. **Updated `syncSetupItems`** -- breaking API change from `{ itemIds: number[] }` to `{ items: Array<{ itemId, weightClass }> }`; single call site (ItemPicker.tsx) makes this safe
|
||||
5. **`PATCH /api/setups/:id/items/:itemId`** -- new endpoint for updating weight classification without triggering full sync (which would destroy classification data)
|
||||
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. **Weight unit conversion rounding drift** -- bidirectional conversion in edit forms causes grams to drift over multiple edit cycles. Always load stored grams from the API, convert for display, and convert user input back to grams once on save. Never re-convert from a previously displayed value.
|
||||
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. **Weight classification at the wrong level** -- placing `classification` on the `items` table instead of `setup_items` prevents per-setup classification. A rain jacket is "worn" in summer but "base weight" in winter. This is the single most important schema decision in v1.2 and is costly to reverse.
|
||||
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. **Chart data diverging from displayed totals** -- the codebase already has two computation paths (SQL aggregates in `totals.service.ts` vs. JavaScript reduce in `$setupId.tsx`). Adding charts creates a third. Use a shared utility for weight summation and convert units only at the final display step.
|
||||
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. **Server-side search for client-side data** -- adding search API parameters creates React Query cache fragmentation and unnecessary latency. Keep filtering client-side with `useMemo` over the cached items array.
|
||||
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 desync with schema** -- the manual `createTestDb()` in `tests/helpers/db.ts` duplicates schema in raw SQL. Every column addition must be mirrored there or tests pass against the wrong schema.
|
||||
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 combined research, a 5-phase structure is recommended:
|
||||
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: Weight Unit Selection
|
||||
### Phase 1: Schema Foundation + Pros/Cons Fields
|
||||
|
||||
**Rationale:** Foundational infrastructure. The `formatWeight` refactor touches every component that displays weight (~8 call sites). All subsequent features depend on this formatter working correctly with unit awareness. Building this first means classification totals, chart labels, and setup breakdowns automatically display in the user's preferred unit.
|
||||
**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)
|
||||
|
||||
**Delivers:** Global weight unit preference (g/oz/lb/kg) stored in settings, `useFormatWeight` hook, updated `formatWeight` function, UnitSelector component in TotalsBar, correct unit display across all existing weight surfaces (ItemCard, CandidateCard, CategoryHeader, TotalsBar, setup detail), correct unit handling in ItemForm and CandidateForm weight inputs.
|
||||
### Phase 2: Drag-to-Reorder Candidate Ranking
|
||||
|
||||
**Addresses:** Weight unit selection (table stakes from FEATURES.md)
|
||||
**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)
|
||||
|
||||
**Avoids:** Rounding drift (Pitfall 1), inconsistent unit application (Pitfall 7), flash of unconverted weights on load
|
||||
### Phase 3: Side-by-Side Comparison View
|
||||
|
||||
**Schema changes:** None (uses existing settings table key-value store)
|
||||
**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 2: Search, Filter, and Planning Category Filter
|
||||
### Phase 4: Setup Impact Preview
|
||||
|
||||
**Rationale:** Pure client-side addition with no schema changes, no API changes, and no dependencies on other v1.2 features. Immediately useful as collections grow. The planning category filter upgrade fits naturally here since both involve filter UX and the icon-aware dropdown is a shared component.
|
||||
|
||||
**Delivers:** Search input in collection view, icon-aware category filter dropdown (reused in gear and planning tabs), filtered item display with count ("showing 12 of 47 items"), URL search param persistence, empty state for no results, result count display.
|
||||
|
||||
**Addresses:** Search items by name (table stakes), filter by category (table stakes), planning category filter upgrade (differentiator)
|
||||
|
||||
**Avoids:** Server-side search anti-pattern (Pitfall 3), search state lost on tab switch (UX pitfall), category groups disappearing incorrectly during filtering
|
||||
|
||||
**Schema changes:** None
|
||||
|
||||
### Phase 3: Candidate Status Tracking
|
||||
|
||||
**Rationale:** Simple schema change on `thread_candidates` with minimal integration surface. Independent of other features. Low complexity but requires awareness of the existing thread resolution flow. Schema change should be batched with Phase 4 into one Drizzle migration.
|
||||
|
||||
**Delivers:** Status column on candidates (researching/ordered/arrived), status badge on CandidateCard with click-to-cycle, status field in CandidateForm, Zod enum validation, status transition validation in service layer (researching -> ordered -> arrived, no backward transitions).
|
||||
|
||||
**Addresses:** Candidate status tracking (differentiator -- unique to GearBox)
|
||||
|
||||
**Avoids:** Status without transition validation (Pitfall 4), test helper desync (Pitfall 6), not handling candidate status during thread resolution
|
||||
|
||||
**Schema changes:** Add `status TEXT NOT NULL DEFAULT 'researching'` to `thread_candidates`
|
||||
|
||||
### Phase 4: Weight Classification
|
||||
|
||||
**Rationale:** Most architecturally significant change in v1.2. Changes the sync API shape (breaking change, single call site). Requires Phase 1 to be complete so classification totals display in the correct unit. Schema migration should be batched with Phase 3.
|
||||
|
||||
**Delivers:** `weightClass` column on `setup_items`, updated sync endpoint accepting `{ items: Array<{ itemId, weightClass }> }`, new `PATCH /api/setups/:id/items/:itemId` endpoint, three-segment classification toggle per item in setup detail view, base/worn/consumable weight subtotals.
|
||||
|
||||
**Addresses:** Weight classification base/worn/consumable (table stakes), per-setup classification (differentiator)
|
||||
|
||||
**Avoids:** Classification on items table (Pitfall 2), test helper desync (Pitfall 6), losing classification data on sync
|
||||
|
||||
**Schema changes:** Add `weight_class TEXT NOT NULL DEFAULT 'base'` to `setup_items`
|
||||
|
||||
### Phase 5: Weight Distribution Charts
|
||||
|
||||
**Rationale:** Depends on Phase 1 (unit-aware labels) and Phase 4 (classification data for setup breakdown). Only phase requiring a new npm dependency. Highest UI complexity but lowest architectural risk -- read-only visualization of existing data.
|
||||
|
||||
**Delivers:** `react-minimal-pie-chart` integration, `WeightChart` component, collection-level donut chart (weight by category from `useTotals()`), setup-level donut chart (weight by classification), chart legend with consistent colors, hover tooltips with formatted weights.
|
||||
|
||||
**Addresses:** Weight distribution visualization (differentiator)
|
||||
|
||||
**Avoids:** Chart/totals divergence (Pitfall 5), chart crashing on null-weight items, unnecessary chart re-renders on unrelated state changes
|
||||
|
||||
**Schema changes:** None (npm dependency: `bun add react-minimal-pie-chart`)
|
||||
**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 first** because `formatWeight` is called by every weight-displaying component. Refactoring it after other features are built means touching the same files twice.
|
||||
- **Phase 2 is independent** and could be built in any order, but sequencing it second allows the team to ship a quick win while Phase 3/4 schema changes are designed.
|
||||
- **Batch Phase 3 + Phase 4 schema migrations** into one `bun run db:generate` run. Both add columns to existing tables; a single migration simplifies deployment.
|
||||
- **Phase 4 after Phase 1** because classification totals need the unit-aware formatter.
|
||||
- **Phase 5 last** because it is pure visualization depending on data from Phases 1 and 4, and introduces the only external dependency.
|
||||
- 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 likely needing deeper research during planning:
|
||||
- **Phase 4 (Weight Classification):** The sync API shape change is breaking. The existing delete-all/re-insert pattern destroys classification data. Needs careful design of the PATCH endpoint and how ItemPicker interacts with classification preservation during item add/remove. Worth a `/gsd:research-phase`.
|
||||
- **Phase 5 (Weight Distribution Charts):** react-minimal-pie-chart API specifics (label rendering, responsive sizing, animation control) should be validated with a quick prototype. Consider a short research spike.
|
||||
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 (skip research-phase):
|
||||
- **Phase 1 (Weight Unit Selection):** Well-documented pattern. Extend `formatWeight`, add a `useSetting` wrapper, propagate through components. No unknowns.
|
||||
- **Phase 2 (Search/Filter):** Textbook client-side filtering with `useMemo`. No API changes. Standard React pattern.
|
||||
- **Phase 3 (Candidate Status):** Simple column addition with Zod enum validation. Existing `useUpdateCandidate` mutation already handles partial updates.
|
||||
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 | Only one new dependency (react-minimal-pie-chart). React 19 compatibility verified via package.json peerDeps. All other features use existing stack with no changes. |
|
||||
| Features | HIGH | Feature set derived from analysis of 8+ competing tools (LighterPack, Hikt, PackLight, Packstack, HikeLite, Packrat, OutPack, BPL Calculator). Clear consensus on table stakes vs. differentiators. |
|
||||
| Architecture | HIGH | Based on direct codebase analysis with integration points mapped to specific files. The 5 new / 15 modified file inventory is concrete and verified against the existing codebase. |
|
||||
| Pitfalls | HIGH | Derived from codebase-specific patterns (test helper duplication, dual computation paths) combined with domain risks (unit conversion rounding, classification scope). Not generic warnings. |
|
||||
| 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
|
||||
|
||||
- **`lb` display format:** FEATURES.md suggests "2 lb 3 oz" (pounds + remainder ounces) while STACK.md suggests simpler decimal format. The traditional "lb + oz" format is more useful to American users but adds formatting complexity. Decide during Phase 1 implementation.
|
||||
- **Status change timestamps:** PITFALLS.md recommends storing `statusChangedAt` alongside `status` for staleness detection ("ordered 30 days ago -- still waiting?"). Low effort to add during the schema migration. Decide during Phase 3 planning.
|
||||
- **Sync API backward compatibility:** The sync endpoint shape changes from `{ itemIds: number[] }` to `{ items: [...] }`. Single call site (ItemPicker.tsx), but verify no external consumers exist before shipping.
|
||||
- **react-minimal-pie-chart responsive behavior:** SVG-based and should handle responsive sizing, but exact approach (CSS width vs. explicit size prop) should be validated in Phase 5. Not a risk, just a detail to confirm.
|
||||
- **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)
|
||||
- [Drizzle ORM Filter Operators](https://orm.drizzle.team/docs/operators) -- like, eq, and operators for search/filter
|
||||
- [Drizzle ORM Conditional Filters Guide](https://orm.drizzle.team/docs/guides/conditional-filters-in-query) -- dynamic filter composition
|
||||
- [react-minimal-pie-chart GitHub](https://github.com/toomuchdesign/react-minimal-pie-chart) -- v9.1.2, React 19 peerDeps verified in package.json
|
||||
- [LighterPack](https://lighterpack.com/) -- base/worn/consumable classification standard, pie chart visualization pattern
|
||||
- [99Boulders LighterPack Tutorial](https://www.99boulders.com/lighterpack-tutorial) -- classification definitions and feature walkthrough
|
||||
- [BackpackPeek Pack Weight Calculator Guide](https://backpackpeek.com/blog/pack-weight-calculator-base-weight-guide) -- weight classification methodology
|
||||
- Direct codebase analysis of GearBox v1.1 -- schema.ts, services, hooks, routes, test helpers
|
||||
- `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)
|
||||
- [Hikt](https://hikt.app/) -- searchable gear closet, base vs worn weight display
|
||||
- [PackLight (iOS)](https://apps.apple.com/us/app/packlight-for-backpacking/id1054845207) -- search, categories, bar graph visualization
|
||||
- [Packstack](https://www.packstack.io/) -- base/worn/consumable weight separation
|
||||
- [Packrat](https://www.packrat.app/) -- flexible weight unit input and display conversion
|
||||
- [Recharts React 19 issue #6857](https://github.com/recharts/recharts/issues/6857) -- rendering issues with React 19.2.3
|
||||
- [TanStack Query filtering discussions](https://github.com/TanStack/query/discussions/1113) -- client-side vs server-side filtering patterns
|
||||
- [LogRocket Best React Chart Libraries 2025](https://blog.logrocket.com/best-react-chart-libraries-2025/) -- chart library comparison
|
||||
- [@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)
|
||||
- [SQLite LIKE case sensitivity](https://github.com/drizzle-team/drizzle-orm-docs/issues/239) -- LIKE is case-insensitive in SQLite (relevant only if search moves server-side)
|
||||
- [Drizzle ORM SQLite migration pitfalls #1313](https://github.com/drizzle-team/drizzle-orm/issues/1313) -- data loss bug with push + add column (monitor during migration)
|
||||
- [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*
|
||||
|
||||
Reference in New Issue
Block a user