diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md
index 7bc703b..1c58545 100644
--- a/.planning/ROADMAP.md
+++ b/.planning/ROADMAP.md
@@ -44,11 +44,11 @@
1. User can select a weight unit (g, oz, lb, kg) from a visible control and the selection persists after closing and reopening the app
2. Every weight value in the app (item cards, candidate cards, category headers, totals bar, setup details) displays in the selected unit with appropriate precision
3. Weight input fields accept values and store them correctly regardless of display unit (no rounding drift across edit cycles)
-**Plans**: TBD
+**Plans:** 2 plans
Plans:
-- [ ] 07-01: TBD
-- [ ] 07-02: TBD
+- [ ] 07-01-PLAN.md -- TDD formatWeight unit conversion core + useWeightUnit hook
+- [ ] 07-02-PLAN.md -- Wire unit toggle into TotalsBar and update all 8 call sites
### Phase 8: Search, Filter, and Candidate Status
**Goal**: Users can find items quickly and track candidate purchase progress
@@ -93,6 +93,6 @@ Plans:
| 4. Database & Planning Fixes | v1.1 | 2/2 | Complete | 2026-03-15 |
| 5. Image Handling | v1.1 | 2/2 | Complete | 2026-03-15 |
| 6. Category Icons | v1.1 | 3/3 | Complete | 2026-03-15 |
-| 7. Weight Unit Selection | v1.2 | 0/? | Not started | - |
+| 7. Weight Unit Selection | v1.2 | 0/2 | Not started | - |
| 8. Search, Filter, and Candidate Status | v1.2 | 0/? | Not started | - |
| 9. Weight Classification and Visualization | v1.2 | 0/? | Not started | - |
diff --git a/.planning/phases/07-weight-unit-selection/07-01-PLAN.md b/.planning/phases/07-weight-unit-selection/07-01-PLAN.md
new file mode 100644
index 0000000..5074bff
--- /dev/null
+++ b/.planning/phases/07-weight-unit-selection/07-01-PLAN.md
@@ -0,0 +1,238 @@
+---
+phase: 07-weight-unit-selection
+plan: 01
+type: tdd
+wave: 1
+depends_on: []
+files_modified:
+ - src/client/lib/formatters.ts
+ - src/client/hooks/useWeightUnit.ts
+ - tests/lib/formatters.test.ts
+autonomous: true
+requirements:
+ - UNIT-02
+ - UNIT-03
+
+must_haves:
+ truths:
+ - "formatWeight converts grams to g, oz, lb, kg with correct precision"
+ - "formatWeight defaults to grams when no unit is specified (backward compatible)"
+ - "formatWeight handles null/undefined input for all units"
+ - "useWeightUnit hook returns a valid WeightUnit from settings, defaulting to 'g'"
+ artifacts:
+ - path: "src/client/lib/formatters.ts"
+ provides: "WeightUnit type export and parameterized formatWeight function"
+ exports: ["WeightUnit", "formatWeight", "formatPrice"]
+ contains: "WeightUnit"
+ - path: "src/client/hooks/useWeightUnit.ts"
+ provides: "Convenience hook wrapping useSetting for weight unit"
+ exports: ["useWeightUnit"]
+ - path: "tests/lib/formatters.test.ts"
+ provides: "Unit tests for formatWeight with all 4 units and edge cases"
+ min_lines: 30
+ key_links:
+ - from: "src/client/hooks/useWeightUnit.ts"
+ to: "src/client/hooks/useSettings.ts"
+ via: "useSetting('weightUnit')"
+ pattern: "useSetting.*weightUnit"
+ - from: "src/client/hooks/useWeightUnit.ts"
+ to: "src/client/lib/formatters.ts"
+ via: "imports WeightUnit type"
+ pattern: "import.*WeightUnit.*formatters"
+---
+
+
+Create the weight unit conversion core: a parameterized `formatWeight` function with a `WeightUnit` type and a `useWeightUnit` convenience hook, all backed by tests.
+
+Purpose: Establish the conversion contracts (type, function, hook) that Plan 02 will wire into every component. TDD approach ensures the conversion math is correct before any UI work.
+Output: Working `formatWeight(grams, unit)` with tests green, `useWeightUnit()` hook ready for consumption.
+
+
+
+@/home/jlmak/.claude/get-shit-done/workflows/execute-plan.md
+@/home/jlmak/.claude/get-shit-done/templates/summary.md
+
+
+
+@.planning/PROJECT.md
+@.planning/ROADMAP.md
+@.planning/STATE.md
+@.planning/phases/07-weight-unit-selection/07-RESEARCH.md
+
+@src/client/lib/formatters.ts
+@src/client/hooks/useSettings.ts
+
+
+
+
+
+From src/client/lib/formatters.ts (current):
+```typescript
+export function formatWeight(grams: number | null | undefined): string {
+ if (grams == null) return "--";
+ return `${Math.round(grams)}g`;
+}
+
+export function formatPrice(cents: number | null | undefined): string {
+ if (cents == null) return "--";
+ return `$${(cents / 100).toFixed(2)}`;
+}
+```
+
+From src/client/hooks/useSettings.ts:
+```typescript
+export function useSetting(key: string) {
+ return useQuery({
+ queryKey: ["settings", key],
+ queryFn: async () => {
+ try {
+ const result = await apiGet(`/api/settings/${key}`);
+ return result.value;
+ } catch (err: any) {
+ if (err?.status === 404) return null;
+ throw err;
+ }
+ },
+ });
+}
+
+export function useUpdateSetting() {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: ({ key, value }: { key: string; value: string }) =>
+ apiPut(`/api/settings/${key}`, { value }),
+ onSuccess: (_data, variables) => {
+ queryClient.invalidateQueries({ queryKey: ["settings", variables.key] });
+ },
+ });
+}
+```
+
+
+
+ formatWeight unit conversion
+ src/client/lib/formatters.ts, tests/lib/formatters.test.ts
+
+ Conversion constants: 1 oz = 28.3495g, 1 lb = 453.592g, 1 kg = 1000g
+
+ - formatWeight(100, "g") -> "100g"
+ - formatWeight(100, "oz") -> "3.5 oz"
+ - formatWeight(1000, "lb") -> "2.20 lb"
+ - formatWeight(1500, "kg") -> "1.50 kg"
+ - formatWeight(null, "oz") -> "--"
+ - formatWeight(undefined, "kg") -> "--"
+ - formatWeight(100) -> "100g" (default unit, backward compatible)
+ - formatWeight(0, "oz") -> "0.0 oz"
+ - formatWeight(5, "lb") -> "0.01 lb" (small weight precision)
+ - formatWeight(50000, "kg") -> "50.00 kg" (large weight)
+
+
+ 1. Add `WeightUnit` type export: `"g" | "oz" | "lb" | "kg"`
+ 2. Add conversion constants as module-level consts (not exported)
+ 3. Modify `formatWeight` signature to `(grams: number | null | undefined, unit: WeightUnit = "g"): string`
+ 4. Keep the null guard as-is at the top
+ 5. Add switch statement for unit-specific formatting:
+ - g: `Math.round(grams)` + "g" (0 decimals, current behavior)
+ - oz: `.toFixed(1)` + " oz" (1 decimal)
+ - lb: `.toFixed(2)` + " lb" (2 decimals)
+ - kg: `.toFixed(2)` + " kg" (2 decimals)
+ 6. Do NOT modify `formatPrice` — leave it untouched
+
+
+
+
+
+
+ Task 1: TDD formatWeight with unit parameter
+ src/client/lib/formatters.ts, tests/lib/formatters.test.ts
+
+ - formatWeight(100, "g") returns "100g"
+ - formatWeight(100, "oz") returns "3.5 oz"
+ - formatWeight(1000, "lb") returns "2.20 lb"
+ - formatWeight(1500, "kg") returns "1.50 kg"
+ - formatWeight(null) returns "--" for all units
+ - formatWeight(undefined, "kg") returns "--"
+ - formatWeight(100) returns "100g" (backward compatible, no second arg)
+ - formatWeight(0, "oz") returns "0.0 oz"
+
+
+ RED: Create `tests/lib/formatters.test.ts`. Import `formatWeight` from `@/client/lib/formatters`. Write tests for:
+ - All 4 units with a known gram value (e.g., 1000g = "1000g", "35.3 oz", "2.20 lb", "1.00 kg")
+ - Null and undefined input returning "--" for each unit
+ - Default parameter (no second arg) producing current "g" behavior
+ - Zero grams producing "0g", "0.0 oz", "0.00 lb", "0.00 kg"
+ - Precision edge cases (small values like 5g in lb = "0.01 lb")
+
+ Run tests — they should fail because formatWeight does not accept a unit parameter yet.
+
+ GREEN: Modify `src/client/lib/formatters.ts`:
+ - Export `type WeightUnit = "g" | "oz" | "lb" | "kg"`
+ - Add constants: `GRAMS_PER_OZ = 28.3495`, `GRAMS_PER_LB = 453.592`, `GRAMS_PER_KG = 1000`
+ - Change signature to `formatWeight(grams: number | null | undefined, unit: WeightUnit = "g")`
+ - Add switch statement after the null guard for unit-specific conversion and formatting
+ - Leave `formatPrice` completely untouched
+
+ Run tests — all should pass.
+
+ REFACTOR: None expected — the code is already minimal.
+
+
+ bun test tests/lib/formatters.test.ts
+
+ formatWeight handles all 4 units with correct precision, null handling, and backward-compatible default. WeightUnit type is exported. All tests pass.
+
+
+
+ Task 2: Create useWeightUnit convenience hook
+ src/client/hooks/useWeightUnit.ts
+
+ Create `src/client/hooks/useWeightUnit.ts`:
+
+ ```typescript
+ import { useSetting } from "./useSettings";
+ import type { WeightUnit } from "../lib/formatters";
+
+ const VALID_UNITS: WeightUnit[] = ["g", "oz", "lb", "kg"];
+
+ export function useWeightUnit(): WeightUnit {
+ const { data } = useSetting("weightUnit");
+ if (data && VALID_UNITS.includes(data as WeightUnit)) {
+ return data as WeightUnit;
+ }
+ return "g";
+ }
+ ```
+
+ This hook:
+ - Wraps `useSetting("weightUnit")` for a typed return value
+ - Validates the stored value is a known unit (protects against bad data)
+ - Defaults to "g" when no setting exists (backward compatible — UNIT-03 persistence works via existing settings API)
+ - Returns `WeightUnit` type so components can pass directly to `formatWeight`
+
+
+ bun run lint
+
+ useWeightUnit hook exists, imports from useSettings and formatters, returns typed WeightUnit with "g" default. Lint passes.
+
+
+
+
+
+- `bun test tests/lib/formatters.test.ts` passes with all unit conversion tests green
+- `bun run lint` passes with no errors
+- `src/client/lib/formatters.ts` exports `WeightUnit` type and updated `formatWeight` function
+- `src/client/hooks/useWeightUnit.ts` exists and exports `useWeightUnit`
+- Existing tests still pass: `bun test` (full suite)
+
+
+
+- formatWeight("g") produces identical output to the old function (backward compatible)
+- formatWeight with oz/lb/kg produces correct conversions with appropriate decimal precision
+- WeightUnit type is exported for use by Plan 02 components
+- useWeightUnit hook is ready for components to consume
+- All existing tests remain green (no regressions)
+
+
+
diff --git a/.planning/phases/07-weight-unit-selection/07-02-PLAN.md b/.planning/phases/07-weight-unit-selection/07-02-PLAN.md
new file mode 100644
index 0000000..4a345d1
--- /dev/null
+++ b/.planning/phases/07-weight-unit-selection/07-02-PLAN.md
@@ -0,0 +1,247 @@
+---
+phase: 07-weight-unit-selection
+plan: 02
+type: execute
+wave: 2
+depends_on:
+ - "07-01"
+files_modified:
+ - src/client/components/TotalsBar.tsx
+ - src/client/components/ItemCard.tsx
+ - src/client/components/CandidateCard.tsx
+ - src/client/components/CategoryHeader.tsx
+ - src/client/components/SetupCard.tsx
+ - src/client/components/ItemPicker.tsx
+ - src/client/routes/index.tsx
+ - src/client/routes/setups/$setupId.tsx
+autonomous: false
+requirements:
+ - UNIT-01
+ - UNIT-02
+ - UNIT-03
+
+must_haves:
+ truths:
+ - "User can see a unit toggle (g/oz/lb/kg) in the TotalsBar"
+ - "Clicking a unit in the toggle changes all weight displays across the app"
+ - "Weight unit selection persists after page refresh"
+ - "Every weight display in the app uses the selected unit"
+ artifacts:
+ - path: "src/client/components/TotalsBar.tsx"
+ provides: "Unit toggle UI and unit-aware weight display"
+ contains: "useWeightUnit"
+ - path: "src/client/components/ItemCard.tsx"
+ provides: "Unit-aware item weight display"
+ contains: "useWeightUnit"
+ - path: "src/client/components/CandidateCard.tsx"
+ provides: "Unit-aware candidate weight display"
+ contains: "useWeightUnit"
+ - path: "src/client/components/CategoryHeader.tsx"
+ provides: "Unit-aware category total weight display"
+ contains: "useWeightUnit"
+ - path: "src/client/components/SetupCard.tsx"
+ provides: "Unit-aware setup weight display"
+ contains: "useWeightUnit"
+ - path: "src/client/components/ItemPicker.tsx"
+ provides: "Unit-aware item picker weight display"
+ contains: "useWeightUnit"
+ - path: "src/client/routes/index.tsx"
+ provides: "Unit-aware dashboard weight display"
+ contains: "useWeightUnit"
+ - path: "src/client/routes/setups/$setupId.tsx"
+ provides: "Unit-aware setup detail weight display"
+ contains: "useWeightUnit"
+ key_links:
+ - from: "src/client/components/TotalsBar.tsx"
+ to: "/api/settings/weightUnit"
+ via: "useUpdateSetting mutation"
+ pattern: "useUpdateSetting.*weightUnit"
+ - from: "src/client/components/ItemCard.tsx"
+ to: "src/client/hooks/useWeightUnit.ts"
+ via: "useWeightUnit hook import"
+ pattern: "useWeightUnit"
+ - from: "src/client/components/TotalsBar.tsx"
+ to: "src/client/lib/formatters.ts"
+ via: "formatWeight(grams, unit)"
+ pattern: "formatWeight\\(.*,\\s*unit"
+---
+
+
+Wire weight unit selection through the entire app: add a segmented unit toggle to TotalsBar and update all 8 formatWeight call sites to use the selected unit.
+
+Purpose: Deliver the complete user-facing feature. After this plan, users can select g/oz/lb/kg and see all weights update instantly across collection, planning, setups, and dashboard.
+Output: Fully functional weight unit selection with persistent preference.
+
+
+
+@/home/jlmak/.claude/get-shit-done/workflows/execute-plan.md
+@/home/jlmak/.claude/get-shit-done/templates/summary.md
+
+
+
+@.planning/PROJECT.md
+@.planning/ROADMAP.md
+@.planning/STATE.md
+@.planning/phases/07-weight-unit-selection/07-RESEARCH.md
+@.planning/phases/07-weight-unit-selection/07-01-SUMMARY.md
+
+
+
+
+
+From src/client/lib/formatters.ts (after Plan 01):
+```typescript
+export type WeightUnit = "g" | "oz" | "lb" | "kg";
+export function formatWeight(grams: number | null | undefined, unit?: WeightUnit): string;
+export function formatPrice(cents: number | null | undefined): string;
+```
+
+From src/client/hooks/useWeightUnit.ts (after Plan 01):
+```typescript
+export function useWeightUnit(): WeightUnit;
+```
+
+From src/client/hooks/useSettings.ts (existing):
+```typescript
+export function useUpdateSetting(): UseMutationResult;
+```
+
+Usage pattern for every component:
+```typescript
+import { useWeightUnit } from "../hooks/useWeightUnit";
+// ...
+const unit = useWeightUnit();
+// ...
+{formatWeight(weightGrams, unit)}
+```
+
+
+
+
+
+ Task 1: Add unit toggle to TotalsBar and update all call sites
+
+ src/client/components/TotalsBar.tsx,
+ src/client/components/ItemCard.tsx,
+ src/client/components/CandidateCard.tsx,
+ src/client/components/CategoryHeader.tsx,
+ src/client/components/SetupCard.tsx,
+ src/client/components/ItemPicker.tsx,
+ src/client/routes/index.tsx,
+ src/client/routes/setups/$setupId.tsx
+
+
+ **TotalsBar.tsx** -- Add unit toggle and wire formatWeight:
+
+ 1. Import `useWeightUnit` from `../hooks/useWeightUnit`, `useUpdateSetting` from `../hooks/useSettings`, and `WeightUnit` type from `../lib/formatters`
+ 2. Inside the component function, call `const unit = useWeightUnit()` and `const updateSetting = useUpdateSetting()`
+ 3. Define `const UNITS: WeightUnit[] = ["g", "oz", "lb", "kg"]`
+ 4. Add a segmented pill toggle to the right side of the TotalsBar, between the title and the stats. The toggle should be a `div` with `flex items-center gap-1 bg-gray-100 rounded-full px-1 py-0.5` containing a button per unit:
+ ```
+
+ ```
+ 5. Update the default stats construction (the `data?.global` branch) to pass `unit` to both `formatWeight` calls:
+ - `formatWeight(data.global.totalWeight, unit)` and `formatWeight(null, unit)`
+ 6. Position the toggle: place it in the flex container between the title and stats, using a wrapper div that pushes stats to the right. The toggle should be visible but not dominant -- it's a small utility control.
+
+ **ItemCard.tsx** -- 3-line change:
+ 1. Add import: `import { useWeightUnit } from "../hooks/useWeightUnit";`
+ 2. Inside component: `const unit = useWeightUnit();`
+ 3. Change `{formatWeight(weightGrams)}` to `{formatWeight(weightGrams, unit)}`
+
+ **CandidateCard.tsx** -- Same 3-line pattern as ItemCard:
+ 1. Add import: `import { useWeightUnit } from "../hooks/useWeightUnit";`
+ 2. Inside component: `const unit = useWeightUnit();`
+ 3. Change `{formatWeight(weightGrams)}` to `{formatWeight(weightGrams, unit)}`
+
+ **CategoryHeader.tsx** -- Same 3-line pattern:
+ 1. Add import: `import { useWeightUnit } from "../hooks/useWeightUnit";`
+ 2. Inside component: `const unit = useWeightUnit();`
+ 3. Change `{formatWeight(totalWeight)}` to `{formatWeight(totalWeight, unit)}`
+
+ **SetupCard.tsx** -- Same 3-line pattern:
+ 1. Add import: `import { useWeightUnit } from "../hooks/useWeightUnit";`
+ 2. Inside component: `const unit = useWeightUnit();`
+ 3. Change `{formatWeight(totalWeight)}` to `{formatWeight(totalWeight, unit)}`
+
+ **ItemPicker.tsx** -- Same 3-line pattern:
+ 1. Add import: `import { useWeightUnit } from "../hooks/useWeightUnit";`
+ 2. Inside component: `const unit = useWeightUnit();`
+ 3. Change `formatWeight(item.weightGrams)` to `formatWeight(item.weightGrams, unit)`
+
+ **routes/index.tsx** (Dashboard) -- Same 3-line pattern:
+ 1. Add import: `import { useWeightUnit } from "../hooks/useWeightUnit";`
+ 2. Inside `DashboardPage`: `const unit = useWeightUnit();`
+ 3. Change `formatWeight(global?.totalWeight ?? null)` to `formatWeight(global?.totalWeight ?? null, unit)`
+
+ **routes/setups/$setupId.tsx** (Setup Detail) -- Same 3-line pattern:
+ 1. Add import: `import { useWeightUnit } from "../../hooks/useWeightUnit";`
+ 2. Inside `SetupDetailPage`: `const unit = useWeightUnit();`
+ 3. Change `{formatWeight(totalWeight)}` to `{formatWeight(totalWeight, unit)}`
+
+ **Completeness check:** After all changes, grep for `formatWeight(` across `src/client/` -- every call must have a second `unit` argument EXCEPT the function definition itself in `formatters.ts`.
+
+
+ bun test && bun run lint
+
+
+ - All 8 components pass `unit` to `formatWeight`
+ - TotalsBar renders a g/oz/lb/kg toggle
+ - Clicking a toggle button calls `useUpdateSetting` with key "weightUnit"
+ - No `formatWeight` call site in src/client/ is missing the unit argument (except the definition)
+ - All tests and lint pass
+
+
+
+
+ Task 2: Verify weight unit selection end-to-end
+
+ Human verifies the complete weight unit selection feature works correctly across all pages.
+
+ Start the dev servers: `bun run dev:client` and `bun run dev:server`
+ Open http://localhost:5173 in a browser and walk through the verification steps below.
+
+
+ 1. Navigate to the Collection page -- verify the TotalsBar shows a g/oz/lb/kg toggle
+ 2. The default should be "g" -- weights display as before (e.g., "450g")
+ 3. Click "oz" -- all weight badges on ItemCards, CategoryHeaders, and the TotalsBar total should update to ounces (e.g., "15.9 oz")
+ 4. Click "kg" -- weights should update to kilograms (e.g., "0.45 kg")
+ 5. Click "lb" -- weights should update to pounds (e.g., "0.99 lb")
+ 6. Navigate to the Dashboard (/) -- the Collection card weight should show in the selected unit
+ 7. Navigate to a Setup detail page -- the sticky sub-bar weight total and all ItemCards should show the selected unit
+ 8. Refresh the page -- the selected unit should persist (still showing the last chosen unit)
+ 9. Switch back to "g" -- all weights should return to the original gram display
+
+ User confirms all weight displays update correctly across all pages, unit toggle is visible and functional, and selection persists across refresh. Type "approved" or describe issues.
+
+
+
+
+
+- `bun test` passes (full suite, no regressions)
+- `bun run lint` passes
+- grep `formatWeight(` across `src/client/` shows all call sites have unit parameter
+- Unit toggle is visible in TotalsBar on all pages that show it
+- Selecting a unit updates all weight displays instantly
+- Selected unit persists across page refresh
+
+
+
+- UNIT-01: User can select g/oz/lb/kg from the TotalsBar toggle -- visible and functional
+- UNIT-02: Every weight display (ItemCard, CandidateCard, CategoryHeader, SetupCard, ItemPicker, Dashboard, Setup Detail, TotalsBar) reflects the selected unit
+- UNIT-03: Weight unit persists across sessions via the existing settings API (PUT/GET /api/settings/weightUnit)
+
+
+