docs: complete project research

This commit is contained in:
2026-03-16 11:39:16 +01:00
parent 1324018989
commit 79457053b3
5 changed files with 1273 additions and 889 deletions

View File

@@ -1,191 +1,179 @@
# Stack Research
# Technology Stack -- v1.2 Collection Power-Ups
**Domain:** Single-user gear management and purchase planning web app
**Researched:** 2026-03-14
**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
## Recommended Stack
## Key Finding: Minimal New Dependencies
### Core Technologies
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.
| Technology | Version | Purpose | Why Recommended |
|------------|---------|---------|-----------------|
| Bun | 1.3.x | Runtime, package manager, bundler | User constraint. Built-in SQLite, fast installs, native TS support. Eliminates need for separate runtime/bundler/pkg manager. |
| React | 19.2.x | UI framework | Industry standard, massive ecosystem, stable. Server Components not needed for this SPA -- stick with client-side React. |
| Vite | 8.0.x | Dev server, production builds | Rolldown-based builds (5-30x faster than Vite 7). Zero-config React support. Bun-compatible. HMR out of the box. |
| Hono | 4.12.x | Backend API framework | Built on Web Standards, first-class Bun support, zero dependencies, tiny (~12kB). Perfect for a lightweight REST API. Faster than Express on Bun benchmarks. |
| SQLite (bun:sqlite) | Built-in | Database | Zero-dependency, built into Bun runtime. 3-6x faster than better-sqlite3. Single file database -- perfect for single-user app. No server process to manage. |
| Drizzle ORM | 0.45.x | Database ORM, migrations | Type-safe SQL, ~7.4kB, zero dependencies. Native bun:sqlite driver support. SQL-like query API (not abstracting SQL away). Built-in migration tooling via drizzle-kit. |
| Tailwind CSS | 4.2.x | Styling | CSS-native configuration (no JS config file). Auto content detection. Microsecond incremental builds. Perfect for "light, airy, minimalist" design constraint. |
| TanStack Router | 1.167.x | Client-side routing | Full type-safe routing with typed params and search params. File-based route generation. Better SPA experience than React Router v7 (whose best features require framework mode). |
| TanStack Query | 5.93.x | Server state management | Handles API data fetching, caching, and synchronization. Eliminates manual loading/error state management. Automatic cache invalidation on mutations. |
| Zustand | 5.0.x | Client state management | Minimal boilerplate, ~1kB. For UI state like active filters, modal state, theme. TanStack Query handles server state; Zustand handles the rest. |
| Zod | 4.3.x | Schema validation | Validates API inputs on the server, form data on the client, and shares types between both. Single source of truth for data shapes. |
| TypeScript | 5.x (Bun built-in) | Type safety | Bun transpiles TS natively -- no tsc needed at runtime. Catches bugs at dev time. Required by Drizzle and TanStack Router for type-safe queries and routes. |
## New Dependency
### Supporting Libraries
### Charting: react-minimal-pie-chart
| Library | Version | Purpose | When to Use |
|---------|---------|---------|-------------|
| @tanstack/react-query-devtools | 5.x | Query debugging | Development only. Inspect cache state, refetch timing, query status. |
| drizzle-kit | latest | DB migrations CLI | Run `drizzle-kit generate` and `drizzle-kit migrate` for schema changes. |
| @hono/zod-validator | latest | Request validation middleware | Validate API request bodies/params using Zod schemas in Hono routes. |
| clsx | 2.x | Conditional class names | When building components with variant styles. Pairs with Tailwind. |
| @tanstack/react-router-devtools | latest | Router debugging | Development only. Inspect route matches, params, search params. |
| 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. |
### Development Tools
**Why this over alternatives:**
| Tool | Purpose | Notes |
|------|---------|-------|
| Bun | Test runner | `bun test` -- built-in, Jest-compatible API. No need for Vitest or Jest. |
| Biome | Linter + formatter | Single tool replacing ESLint + Prettier. Fast (Rust-based), minimal config. `biome check --write` does both. |
| Vite React plugin | React HMR/JSX | `@vitejs/plugin-react` for Fast Refresh during development. |
| 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:
```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
# Initialize project
bun init
# Core frontend
bun add react react-dom @tanstack/react-router @tanstack/react-query zustand zod clsx
# Core backend
bun add hono @hono/zod-validator drizzle-orm
# Styling
bun add tailwindcss @tailwindcss/vite
# Build tooling
bun add -d vite @vitejs/plugin-react typescript @types/react @types/react-dom
# Database tooling
bun add -d drizzle-kit
# Linting + formatting
bun add -d @biomejs/biome
# Dev tools (optional but recommended)
bun add -d @tanstack/react-query-devtools @tanstack/react-router-devtools
# Only new dependency for v1.2
bun add react-minimal-pie-chart
```
## Architecture Pattern
That is it. One package, under 2kB gzipped.
**Monorepo-lite (single package, split directories):**
## Schema Changes Summary
```
/src
/client -- React SPA (Vite entry point)
/routes -- TanStack Router file-based routes
/components -- Shared UI components
/stores -- Zustand stores
/api -- TanStack Query hooks (fetch wrappers)
/server -- Hono API server
/routes -- API route handlers
/db -- Drizzle schema, migrations
/shared -- Zod schemas shared between client and server
/public -- Static assets, uploaded images
```
These are the Drizzle schema modifications needed (no new tables, just column additions):
Bun runs the Hono server, which also serves the Vite-built SPA in production. In development, Vite dev server proxies API calls to the Hono backend.
| 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. |
## Alternatives Considered
| Recommended | Alternative | When to Use Alternative |
|-------------|-------------|-------------------------|
| Hono | Elysia | If you want end-to-end type safety with Eden Treaty. Elysia is Bun-native but heavier, more opinionated, and has a smaller ecosystem than Hono. |
| Hono | Express | Never for new Bun projects. Express is Node-centric, not built on Web Standards, slower on Bun. |
| TanStack Router | React Router v7 | If you want the simplest possible routing with minimal type safety. React Router v7's best features (loaders, type safety) require framework mode which adds complexity. |
| Drizzle ORM | Prisma | If you have a complex relational model and want auto-generated migrations. But Prisma is heavy (~8MB), generates a query engine binary, and has weaker SQLite support. |
| Drizzle ORM | Kysely | If you want a pure query builder without ORM features. Kysely is lighter but lacks built-in migration tooling. |
| Zustand | Jotai | If you prefer atomic state (bottom-up). Zustand is simpler for this app's needs -- a few global stores, not many independent atoms. |
| Tailwind CSS | Vanilla CSS / CSS Modules | If you strongly prefer writing plain CSS. But Tailwind accelerates building consistent minimalist UIs and requires less design system setup. |
| bun:sqlite | PostgreSQL | If you later need multi-user with concurrent writes. Overkill for single-user. Adds a database server dependency. |
| Biome | ESLint + Prettier | If you need specific ESLint plugins not yet in Biome. But Biome covers 95% of use cases with zero config. |
| Vite | Bun's built-in bundler | Bun can serve HTML directly as of 1.3, but Vite's ecosystem (plugins, HMR, proxy) is far more mature for SPA development. |
## What NOT to Use
## What NOT to Add
| Avoid | Why | Use Instead |
|-------|-----|-------------|
| Next.js | Server-centric framework. Massive overhead for a single-user SPA. Forces Node.js patterns. No benefit without SSR/SSG needs. | Vite + React + Hono |
| Remix / React Router framework mode | Adds server framework complexity. This is a simple SPA with a separate API -- framework routing is unnecessary overhead. | TanStack Router (SPA mode) |
| better-sqlite3 | Requires native compilation, compatibility issues with Bun. bun:sqlite is built-in and 3-6x faster. | bun:sqlite (built into Bun) |
| Redux / Redux Toolkit | Massive boilerplate for a small app. Actions, reducers, slices -- all unnecessary when Zustand does the same in 10 lines. | Zustand |
| Mongoose / MongoDB | Document DB is wrong fit. Gear items have relational structure (items belong to setups, threads reference items). SQL is the right model. | Drizzle + SQLite |
| Axios | Unnecessary abstraction over fetch. Bun and browsers both have native fetch. TanStack Query wraps fetch already. | Native fetch |
| styled-components / Emotion | CSS-in-JS adds runtime overhead and bundle size. Tailwind is faster (zero runtime) and better for consistent minimalist design. | Tailwind CSS |
| Jest / Vitest | Bun has a built-in test runner with Jest-compatible API. No need for external test frameworks. | bun test |
| ESLint + Prettier | Two tools, complex configuration, slow (JS-based). Biome does both in one tool, faster. | Biome |
| 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 |
## Version Compatibility
## Existing Stack Version Compatibility
| Package A | Compatible With | Notes |
|-----------|-----------------|-------|
| Bun 1.3.x | bun:sqlite (built-in) | SQLite driver is part of the runtime, always compatible. |
| Drizzle ORM 0.45.x | bun:sqlite via `drizzle-orm/bun-sqlite` | Official driver. Import from `drizzle-orm/bun-sqlite`. |
| Drizzle ORM 0.45.x | drizzle-kit (latest) | drizzle-kit handles migration generation/execution. Must match major drizzle-orm version. |
| React 19.2.x | TanStack Router 1.x | TanStack Router 1.x supports React 18+ and 19.x. |
| React 19.2.x | TanStack Query 5.x | TanStack Query 5.x supports React 18+ and 19.x. |
| React 19.2.x | Zustand 5.x | Zustand 5.x supports React 18+ and 19.x. |
| Vite 8.x | @vitejs/plugin-react | Check plugin version matches Vite major. Use latest plugin for Vite 8. |
| Tailwind CSS 4.2.x | @tailwindcss/vite | v4 uses Vite plugin instead of PostCSS. Import as `@tailwindcss/vite` in vite config. |
| Zod 4.x | @hono/zod-validator | Verify @hono/zod-validator supports Zod 4. If not, pin Zod 3.23.x until updated. |
All existing dependencies remain unchanged. The only version consideration:
## Key Configuration Notes
### Bun + Vite Setup
Vite runs as the dev server for the frontend. The Hono API server runs separately. Use Vite's `server.proxy` to forward `/api/*` requests to the Hono backend during development.
### SQLite WAL Mode
Enable WAL mode on database initialization for better performance:
```typescript
import { Database } from "bun:sqlite";
const db = new Database("gearbox.db");
db.run("PRAGMA journal_mode = WAL");
db.run("PRAGMA foreign_keys = ON");
```
### Tailwind v4 (No Config File)
Tailwind v4 uses CSS-native configuration. No `tailwind.config.js` needed:
```css
@import "tailwindcss";
@theme {
--color-primary: #2563eb;
--font-sans: "Inter", sans-serif;
}
```
### Drizzle Schema Example (bun:sqlite)
```typescript
import { sqliteTable, text, integer, real } from "drizzle-orm/sqlite-core";
export const gearItems = sqliteTable("gear_items", {
id: integer("id").primaryKey({ autoIncrement: true }),
name: text("name").notNull(),
category: text("category").notNull(),
weightGrams: real("weight_grams"),
priceCents: integer("price_cents"),
source: text("source"),
notes: text("notes"),
createdAt: integer("created_at", { mode: "timestamp" }).notNull(),
});
```
| 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
- [Bun official docs](https://bun.com/docs) -- bun:sqlite features, runtime capabilities (HIGH confidence)
- [Hono official docs](https://hono.dev/docs) -- Bun integration, static serving (HIGH confidence)
- [Drizzle ORM docs - Bun SQLite](https://orm.drizzle.team/docs/connect-bun-sqlite) -- driver support verified (HIGH confidence)
- [Vite releases](https://vite.dev/releases) -- v8.0 with Rolldown confirmed (HIGH confidence)
- [Tailwind CSS v4.2 blog](https://tailwindcss.com/blog/tailwindcss-v4) -- CSS-native config, Vite plugin (HIGH confidence)
- [TanStack Router docs](https://tanstack.com/router/latest) -- v1.167.x confirmed (HIGH confidence)
- [TanStack Query docs](https://tanstack.com/query/latest) -- v5.93.x for React (HIGH confidence)
- [Zustand npm](https://www.npmjs.com/package/zustand) -- v5.0.x confirmed (HIGH confidence)
- [Zod v4 release notes](https://zod.dev/v4) -- v4.3.x confirmed (MEDIUM confidence -- verify @hono/zod-validator compatibility)
- [React versions](https://react.dev/versions) -- v19.2.x confirmed (HIGH confidence)
- [Bun SQLite vs better-sqlite3 benchmarks](https://bun.com/docs/runtime/sqlite) -- 3-6x performance advantage (HIGH confidence)
- [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 -- gear management and purchase planning web app*
*Researched: 2026-03-14*
*Stack research for: GearBox v1.2 -- Collection Power-Ups*
*Researched: 2026-03-16*