Files
GearBox/.planning/research/STACK.md

199 lines
12 KiB
Markdown

# Stack Research -- v1.3 Research & Decision Tools
**Project:** GearBox
**Researched:** 2026-03-16
**Scope:** Stack additions for side-by-side candidate comparison, setup impact preview, and drag-to-reorder candidate ranking with pros/cons
**Confidence:** HIGH
## Key Finding: Zero New Dependencies
All three v1.3 features are achievable with the existing stack. The drag-to-reorder feature, which would normally require a dedicated DnD library, is covered by `framer-motion`'s built-in `Reorder` component — already installed at v12.37.0 with React 19 support confirmed.
## Recommended Stack: Existing Technologies Only
### No New Dependencies Required
| Feature | Library Needed | Status |
|---------|---------------|--------|
| Side-by-side comparison view | None — pure layout/UI | Existing Tailwind CSS |
| Setup impact preview | None — SQL delta calculation | Existing Drizzle ORM + TanStack Query |
| Drag-to-reorder candidates | `Reorder` component | Already in `framer-motion@12.37.0` |
| Pros/cons text fields | None — schema + form | Existing Drizzle + Zod + React |
### How Each Feature Uses the Existing Stack
#### 1. Side-by-Side Candidate Comparison
**No schema changes. No new dependencies.**
| Existing Tech | How It Is Used |
|---------------|----------------|
| Tailwind CSS v4 | Responsive comparison table layout. Horizontal scroll on mobile with `overflow-x-auto`. Fixed first column (row labels) using `sticky left-0`. |
| TanStack Query (`useThread`) | Thread detail already fetches all candidates in one query. Comparison view reads from the same cached data — no new API endpoint. |
| Lucide React | Comparison row icons (weight, price, status, link). Already in the curated icon set. |
| `formatWeight` / `formatPrice` | Existing formatters handle display with selected unit/currency. No changes needed. |
| `useWeightUnit` / `useCurrency` | Existing hooks provide formatting context. Comparison view uses them identically to `CandidateCard`. |
**Implementation approach:** Add a view toggle (grid vs. comparison table) to the thread detail page. The comparison view is a `<table>` or CSS grid with candidates as columns and attributes as rows. Data already lives in `useThread` response — no API changes.
#### 2. Setup Impact Preview
**No new dependencies. Requires one new API endpoint.**
| Existing Tech | How It Is Used |
|---------------|----------------|
| Drizzle ORM | New query: for a given setup, sum `weight_grams` and `price_cents` of its items. Then compute delta against each candidate's `weight_grams` and `price_cents`. Pure arithmetic in the service layer. |
| TanStack Query | New `useSetupImpact(threadId, setupId)` hook fetching `GET /api/threads/:threadId/impact?setupId=X`. Returns array of `{ candidateId, weightDelta, costDelta }`. |
| Hono + Zod validator | New route validates `setupId` query param. Delegates to service function. |
| `formatWeight` / `formatPrice` | Format deltas with `+` prefix for positive values (candidate adds weight/cost) and `-` for negative (candidate is lighter/cheaper than what's already in setup). |
| `useSetups` hook | Existing `useSetups()` provides the setup list for the picker dropdown. |
**Delta calculation logic (server-side service):**
```typescript
// For each candidate in thread:
// weightDelta = candidate.weightGrams - (matching item in setup).weightGrams
// If no matching item in setup (it would be added, not replaced): delta = candidate.weightGrams
// A "matching item" means: item in setup with same categoryId as the thread.
// This is the intended semantic: "how does picking this candidate affect my setup?"
```
**Key decision:** Impact preview is read-only and derived. It does not mutate any data. It computes what *would* happen if the candidate were picked, without modifying the setup. The delta is displayed inline on each candidate card or in the comparison view.
#### 3. Drag-to-Reorder Candidate Ranking with Pros/Cons
**No new DnD library. Requires schema changes.**
| Existing Tech | How It Is Used |
|---------------|----------------|
| `framer-motion@12.37.0` `Reorder` | `Reorder.Group` wraps the candidate list. `Reorder.Item` wraps each candidate card. `onReorder` updates local order state. `onDragEnd` fires the persist mutation. |
| Drizzle ORM | Two new columns on `thread_candidates`: `sortOrder integer` (default 0, lower = higher rank) and `pros text` / `cons text` (nullable). |
| TanStack Query mutation | `usePatchCandidate` for pros/cons text updates. `useReorderCandidates` for bulk sort order update after drag-end. |
| Hono + Zod validator | `PATCH /api/threads/:threadId/candidates/reorder` accepts `{ candidates: Array<{ id, sortOrder }> }`. `PATCH /api/candidates/:id` accepts `{ pros?, cons? }`. |
| Zod | Extend `updateCandidateSchema` with `pros: z.string().nullable().optional()`, `cons: z.string().nullable().optional()`, `sortOrder: z.number().int().optional()`. |
**Framer Motion `Reorder` API pattern:**
```typescript
import { Reorder } from "framer-motion";
// State holds candidates sorted by sortOrder
const [orderedCandidates, setOrderedCandidates] = useState(
[...candidates].sort((a, b) => a.sortOrder - b.sortOrder)
);
// onReorder fires continuously during drag — update local state only
// onDragEnd fires once on drop — persist to DB
<Reorder.Group
axis="y"
values={orderedCandidates}
onReorder={setOrderedCandidates}
>
{orderedCandidates.map((candidate) => (
<Reorder.Item
key={candidate.id}
value={candidate}
onDragEnd={() => persistOrder(orderedCandidates)}
>
<CandidateCard ... />
</Reorder.Item>
))}
</Reorder.Group>
```
**Schema changes required:**
| Table | Column | Type | Default | Purpose |
|-------|--------|------|---------|---------|
| `thread_candidates` | `sort_order` | `integer NOT NULL` | `0` | Rank position (lower = higher rank) |
| `thread_candidates` | `pros` | `text` | `NULL` | Free-text pros annotation |
| `thread_candidates` | `cons` | `text` | `NULL` | Free-text cons annotation |
**Sort order persistence pattern:** On drag-end, send the full reordered array with new `sortOrder` values (0-based index positions). Backend replaces existing `sort_order` values atomically. This is the same delete-all + re-insert pattern used for `setupItems` but as an UPDATE instead.
## Installation
```bash
# No new packages. Zero.
```
All required capabilities are already installed.
## Alternatives Considered
### Drag and Drop: Why Not Add a Dedicated Library?
| Option | Version | React 19 Status | Verdict |
|--------|---------|-----------------|---------|
| `framer-motion` Reorder (already installed) | 12.37.0 | React 19 explicit peerDep (`^18.0.0 || ^19.0.0`) | USE THIS |
| `@dnd-kit/core` + `@dnd-kit/sortable` | 6.3.1 | No React 19 support (stale ~1yr, open GitHub issue #1511) | AVOID |
| `@dnd-kit/react` (new rewrite) | 0.3.2 | React 19 compatible | Pre-1.0, no maintainer ETA on stable |
| `@hello-pangea/dnd` | 18.0.1 | No React 19 (stale ~1yr, peerDep `^18.0.0` only) | AVOID |
| `pragmatic-drag-and-drop` | latest | Core is React-agnostic but some sub-packages missing React 19 | Overkill for a single sortable list |
| Custom HTML5 DnD | N/A | N/A | 200+ lines of boilerplate, worse accessibility |
**Why framer-motion `Reorder` wins:** Already in the bundle. React 19 peer dep confirmed in lockfile. Handles the single use case (vertical sortable list) with 10 lines of code. Provides smooth layout animations at zero additional cost. The limitation (no cross-container drag, no multi-row grid) does not apply — candidate ranking is a single vertical list.
**Why not `@dnd-kit`:** The legacy `@dnd-kit/core@6.3.1` has no official React 19 support and has been unmaintained for ~1 year. The new `@dnd-kit/react@0.3.2` does support React 19 but is pre-1.0 with zero maintainer response on stability/roadmap questions (GitHub Discussion #1842 has 0 replies). Adding a pre-1.0 library when the project already has a working solution is unjustifiable.
### Setup Impact: Why Not Client-Side Calculation?
Client-side delta calculation (using cached React Query data) is simpler to implement but:
- Requires loading both the full setup items list AND all candidates into the client
- Introduces staleness bugs if setup items change in another tab
- Is harder to test (service test vs. component test)
Server-side calculation in a service function is testable, authoritative, and consistent with the existing architecture (services compute aggregates, not components).
## What NOT to Add
| Avoid | Why | Use Instead |
|-------|-----|-------------|
| `@dnd-kit/core` + `@dnd-kit/sortable` | No React 19 support, stale for ~1 year (latest 6.3.1 from 2024) | `framer-motion` Reorder (already installed) |
| `@hello-pangea/dnd` | No React 19 support, peerDep `react: "^18.0.0"` only, stale | `framer-motion` Reorder |
| `react-comparison-table` or similar component packages | Fragile third-party layouts for a simple table. Custom Tailwind table is trivial and design-consistent. | Custom Tailwind CSS table layout |
| Modal/dialog library (Radix, Headless UI) | The project already has a hand-rolled modal pattern (`SlideOutPanel`, `ConfirmDialog`). Adding a library for one more dialog adds inconsistency. | Extend existing modal patterns |
| Rich text editor for pros/cons | Markdown editors are overkill for a single-line annotation field. Users want a quick note, not a document. | Plain `<textarea>` with Tailwind styling |
## Stack Patterns by Variant
**If the comparison view needs mobile scroll:**
- Wrap comparison table in `overflow-x-auto`
- Freeze the first column (attribute labels) with `sticky left-0 bg-white z-10`
- This is pure CSS, no JavaScript or library needed
**If the setup impact preview needs a setup picker:**
- Use `useSetups()` (already exists) to populate a `<select>` dropdown
- Store selected setup ID in local component state (not URL params — this is transient UI)
- No new state management needed
**If pros/cons fields need to auto-save:**
- Use a debounced mutation (300-500ms) that fires on `onChange`
- Or save on `onBlur` (simpler, adequate for this use case)
- Existing `useUpdateCandidate` hook already handles candidate mutations — extend schema only
## Version Compatibility
| Package | Version in Project | React 19 Compatible | Notes |
|---------|-------------------|---------------------|-------|
| `framer-motion` | 12.37.0 | YES — peerDeps `"^18.0.0 || ^19.0.0"` confirmed in lockfile | `Reorder` component available since v5 |
| `drizzle-orm` | 0.45.1 | N/A (server-side) | ALTER TABLE or migration for new columns |
| `zod` | 4.3.6 | N/A | Extend existing schemas |
| `@tanstack/react-query` | 5.90.21 | YES | New hooks follow existing patterns |
## Sources
- [framer-motion package lockfile entry] — peerDeps `react: "^18.0.0 || ^19.0.0"` confirmed (HIGH confidence, from project's `bun.lock`)
- [Motion Reorder docs](https://motion.dev/docs/react-reorder) — `Reorder.Group`, `Reorder.Item`, `useDragControls` API, `onDragEnd` pattern for persisting order (HIGH confidence)
- [Motion Changelog](https://motion.dev/changelog) — v12.37.0 actively maintained through Feb 2026 (HIGH confidence)
- [@dnd-kit/core npm](https://www.npmjs.com/package/@dnd-kit/core) — v6.3.1, last published ~1 year ago, no React 19 support (HIGH confidence)
- [dnd-kit React 19 issue #1511](https://github.com/clauderic/dnd-kit/issues/1511) — CLOSED but React 19 TypeScript issues confirmed (MEDIUM confidence)
- [@dnd-kit/react roadmap discussion #1842](https://github.com/clauderic/dnd-kit/discussions/1842) — 0 maintainer replies on stability question (HIGH confidence — signals pre-1.0 risk)
- [hello-pangea/dnd React 19 issue #864](https://github.com/hello-pangea/dnd/issues/864) — React 19 support still open as of Jan 2026 (HIGH confidence)
- [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 (MEDIUM confidence)
---
*Stack research for: GearBox v1.3 -- Research & Decision Tools*
*Researched: 2026-03-16*