Files
GearBox/.planning/research/STACK.md

13 KiB

Technology Stack -- v1.2 Collection Power-Ups

Project: GearBox Researched: 2026-03-16 Scope: Stack additions for search/filter, weight classification, weight distribution charts, candidate status tracking, weight unit selection Confidence: HIGH

Key Finding: Minimal New Dependencies

Four of five v1.2 features require zero new libraries. They are pure application logic built on top of the existing stack (Drizzle ORM filters, Zod schema extensions, Zustand state, React Query invalidation). The only decision point is whether to add a charting library for weight distribution visualization.

New Dependency

Charting: react-minimal-pie-chart

Technology Version Purpose Why
react-minimal-pie-chart ^9.1.2 Weight distribution donut/pie charts Under 2kB gzipped. Supports pie, donut, loading, and completion chart types. SVG-based with CSS animations, hover/click interactions, custom label rendering. React 19 compatible (peerDeps explicitly include ^19). Zero external dependencies. TypeScript native.

Why this over alternatives:

Criterion react-minimal-pie-chart Recharts Custom SVG Chart.js
Bundle size ~2kB gzipped ~97kB gzipped 0kB ~60kB gzipped
Chart types needed Pie + donut (exactly what we need) Overkill (line, bar, area, scatter, etc.) Manual math Overkill
React 19 support Explicit in peerDeps Isolated rendering issues reported with 19.2.x N/A Wrapper has open React 19 issues
Interactivity Click, hover, focus, keyboard events per segment Full but heavy Must implement from scratch Canvas-based (harder to style)
Labels Render prop for custom labels (percentage, value, SVG) Built-in Must implement Built-in
Animation CSS-based, configurable duration/easing, reveal effect D3-based, heavier Must implement Canvas animation
Learning curve Minimal -- one component, straightforward props Moderate -- many components High -- SVG arc math Moderate
Maintenance risk Low -- tiny surface area, stable API Low -- large community Zero Medium -- Canvas abstraction

Why not custom SVG: The SVG <circle> + stroke-dasharray approach works for static charts but breaks interactivity (stacked circles mean only the last segment is clickable). The <path> arc approach gives full interactivity but requires implementing arc math, animation, labels, hover states, and accessibility from scratch. At ~2kB, react-minimal-pie-chart costs less than the custom code would and handles all edge cases.

Why not Recharts: GearBox needs exactly one chart type (donut/pie). Recharts adds ~97kB of unused capability. It also had isolated rendering issues reported with React 19.2.x, and pulls in D3 submodules. Significant overkill for this use case.

Existing Stack Usage for Each Feature

1. Search/Filter Items

No new dependencies. Uses existing Drizzle ORM operators and React state.

Existing Tech How It Is Used
Drizzle ORM like() Server-side text search on items.name column. SQLite LIKE is case-insensitive by default, so no need for ilike().
Drizzle ORM eq(), and() Category filter: eq(items.categoryId, selectedId). Combine with search: and(like(...), eq(...)).
TanStack Query New query key pattern: ["items", { search, categoryId }] for filtered results. Server-side filtering preferred over client-side to establish the pattern early (collections grow).
Zustand or URL search params Store active filter state. URL search params preferred (already used for tab state) so filter state is shareable/bookmarkable.
Zod Validate query params on the Hono route: z.object({ search: z.string().optional(), categoryId: z.number().optional() }).

Implementation approach: Add query parameters to GET /api/items rather than client-side filtering. Drizzle's conditional filter pattern handles optional params cleanly:

import { like, eq, and } from "drizzle-orm";

const conditions = [];
if (search) conditions.push(like(items.name, `%${search}%`));
if (categoryId) conditions.push(eq(items.categoryId, categoryId));

db.select().from(items).where(and(...conditions));

2. Weight Classification (Base/Worn/Consumable)

No new dependencies. Schema change + UI state.

Existing Tech How It Is Used
Drizzle ORM Add weightClass column to setup_items table: text("weight_class").notNull().default("base"). Classification is per-setup-item, not per-item globally (a sleeping bag is "base" in a bikepacking setup but might be categorized differently elsewhere).
Zod Extend syncSetupItemsSchema to include classification: z.enum(["base", "worn", "consumable"]).
drizzle-kit Generate migration for the new column: bun run db:generate.
SQL aggregates Compute base/worn/consumable weight subtotals server-side, same pattern as existing category totals in useTotals.

Key design decision: Weight classification belongs on setup_items (the join table), not on items directly. An item's classification depends on context -- hiking poles are "worn" if you always use them, "base" if they pitch your tent. LighterPack follows this same model. This means the syncSetupItemsSchema changes from { itemIds: number[] } to { items: Array<{ itemId: number, weightClass: "base" | "worn" | "consumable" }> }.

3. Weight Distribution Charts

One new dependency: react-minimal-pie-chart (documented above).

Existing Tech How It Is Used
TanStack Query (useTotals) Already returns per-category weight totals. Extend to also return per-weight-class totals for a given setup.
Tailwind CSS Style chart container, legend, responsive layout. Chart labels use Tailwind color tokens for consistency.
Lucide React Category icons in the chart legend, consistent with existing CategoryHeader component.

Chart data sources:

  • By category: Already available from GET /api/totals response (categories array with totalWeight per category). No new endpoint needed.
  • By weight classification: New endpoint GET /api/setups/:id/breakdown returning { base: number, worn: number, consumable: number } computed from the weight_class column on setup_items.

4. Candidate Status Tracking

No new dependencies. Schema change + UI update.

Existing Tech How It Is Used
Drizzle ORM Add status column to thread_candidates table: text("status").notNull().default("researching"). Values: "researching", "ordered", "arrived".
Zod Add to createCandidateSchema and updateCandidateSchema: status: z.enum(["researching", "ordered", "arrived"]).default("researching").
Tailwind CSS Status badge colors on CandidateCard (gray for researching, amber for ordered, green for arrived). Same badge pattern used for thread status already.
Lucide React Status icons: search for researching, truck for ordered, check-circle for arrived. Already in the curated icon set.

5. Weight Unit Selection

No new dependencies. Settings storage + formatter change.

Existing Tech How It Is Used
SQLite settings table Store preferred unit: { key: "weightUnit", value: "g" }. Same pattern as existing onboarding settings.
React Query (useSettings) Already exists. Fetch and cache the weight unit preference.
formatWeight() in lib/formatters.ts Extend to accept a unit parameter and convert from grams (the canonical storage format).
Zustand (optional) Could cache the unit preference in UI store for synchronous access in formatters. Alternatively, pass it through React context or as a parameter.

Conversion constants (stored weights are always grams):

Unit From Grams Display Format
g (grams) x ${Math.round(x)}g
oz (ounces) x / 28.3495 ${(x / 28.3495).toFixed(1)}oz
lb (pounds) x / 453.592 ${(x / 453.592).toFixed(2)}lb
kg (kilograms) x / 1000 ${(x / 1000).toFixed(2)}kg

Key decision: Store weights in grams always. Convert on display only. This avoids precision loss from repeated conversions and keeps the database canonical. The formatWeight function becomes the single conversion point.

Installation

# Only new dependency for v1.2
bun add react-minimal-pie-chart

That is it. One package, under 2kB gzipped.

Schema Changes Summary

These are the Drizzle schema modifications needed (no new tables, just column additions):

Table Change Migration
setup_items Add weightClass: text("weight_class").notNull().default("base") bun run db:generate && bun run db:push
thread_candidates Add status: text("status").notNull().default("researching") bun run db:generate && bun run db:push
settings No schema change (already key-value). Insert weightUnit row. Seed via service or onboarding.

What NOT to Add

Avoid Why Use Instead
Recharts 97kB for one chart type. React 19 edge-case issues. D3 dependency chain. react-minimal-pie-chart (2kB)
Chart.js / react-chartjs-2 Canvas-based (harder to style with Tailwind). Open React 19 peer dep issues. Overkill. react-minimal-pie-chart
visx Low-level D3 primitives. Steep learning curve. Have to build chart from scratch. Great for custom viz, overkill for a donut chart. react-minimal-pie-chart
Fuse.js or similar search library Client-side fuzzy search adds bundle weight and complexity. SQLite LIKE is sufficient for name search on a single-user collection (hundreds of items, not millions). Drizzle like() operator
Full-text search (FTS5) SQLite FTS5 is powerful but requires virtual tables and different query syntax. Overkill for simple name matching on small collections. Drizzle like() operator
i18n library for unit conversion This is not internationalization. It is four conversion constants and a formatter function. A library would be absurd. Custom formatWeight() function
State machine library (XState) Candidate status is a simple enum, not a complex state machine. Three values with no guards or side effects. Zod enum + Drizzle text column
New Zustand store for filters Filter state should live in URL search params for shareability/bookmarkability. The collection page already uses this pattern for tabs. TanStack Router search params

Existing Stack Version Compatibility

All existing dependencies remain unchanged. The only version consideration:

New Package Compatible With Verified
react-minimal-pie-chart ^9.1.2 React 19 (peerDeps: "^16.8.0 || ^17 || ^18 || ^19") YES -- package.json on GitHub confirms. Dev deps test against React 19.0.0.
react-minimal-pie-chart ^9.1.2 TypeScript 5.x YES -- library is TypeScript native (built with TS 3.8+).
react-minimal-pie-chart ^9.1.2 Bun bundler / Vite YES -- pure ESM, no native dependencies, standard npm package.

Sources


Stack research for: GearBox v1.2 -- Collection Power-Ups Researched: 2026-03-16