# 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 `` + `stroke-dasharray` approach works for static charts but breaks interactivity (stacked circles mean only the last segment is clickable). The `` 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: ```typescript 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 ```bash # 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 - [Drizzle ORM Filter Operators](https://orm.drizzle.team/docs/operators) -- `like`, `eq`, `and`, `or` operators for search/filter (HIGH confidence) - [Drizzle ORM Conditional Filters Guide](https://orm.drizzle.team/docs/guides/conditional-filters-in-query) -- dynamic filter composition pattern (HIGH confidence) - [react-minimal-pie-chart GitHub](https://github.com/toomuchdesign/react-minimal-pie-chart) -- version 9.1.2, React 19 peerDeps confirmed (HIGH confidence) - [react-minimal-pie-chart package.json](https://github.com/toomuchdesign/react-minimal-pie-chart/blob/master/package.json) -- React 19 in peerDependencies and devDependencies (HIGH confidence) - [Recharts npm](https://www.npmjs.com/package/recharts) -- v3.8.0, ~97kB bundle (HIGH confidence) - [Recharts React 19 issue #6857](https://github.com/recharts/recharts/issues/6857) -- rendering issues reported with React 19.2.3 (MEDIUM confidence -- may be project-specific) - [LighterPack weight classification model](https://lighterpack.com) -- base/worn/consumable terminology is industry standard for gear management (HIGH confidence) - [Pack Weight Calculator Guide](https://backpackpeek.com/blog/pack-weight-calculator-base-weight-guide) -- weight classification definitions (HIGH confidence) - [SQLite LIKE case sensitivity note](https://github.com/drizzle-team/drizzle-orm-docs/issues/239) -- LIKE is case-insensitive in SQLite, no need for ilike (MEDIUM confidence) --- *Stack research for: GearBox v1.2 -- Collection Power-Ups* *Researched: 2026-03-16*