diff --git a/.planning/phases/12-comparison-view/12-CONTEXT.md b/.planning/phases/12-comparison-view/12-CONTEXT.md new file mode 100644 index 0000000..bc3bd01 --- /dev/null +++ b/.planning/phases/12-comparison-view/12-CONTEXT.md @@ -0,0 +1,105 @@ +# Phase 12: Comparison View - Context + +**Gathered:** 2026-03-17 +**Status:** Ready for planning + + +## Phase Boundary + +Users can view all candidates for a thread side-by-side in a tabular comparison layout with relative weight and price deltas. The table scrolls horizontally on narrow viewports with a sticky label column. Resolved threads display the comparison in read-only mode with the winning candidate visually marked. Impact preview (setup deltas) is a separate phase (13). + + + + +## Implementation Decisions + +### Compare mode entry point +- Add a third icon to the existing list/grid toggle bar, making it list | grid | compare (three-way toggle) +- Use `candidateViewMode: 'list' | 'grid' | 'compare'` in uiStore — extends the existing Zustand state +- Compare icon only appears when 2+ candidates exist in the thread (hidden otherwise) +- "Add Candidate" button visibility in compare mode is Claude's discretion + +### Table orientation and layout +- Candidates as columns, attribute labels as rows (classic product-comparison pattern — Amazon/Wirecutter style) +- Sticky left column for attribute labels; table scrolls horizontally on narrow viewports +- Attribute row order: Image → Name → Rank → Weight (with delta) → Price (with delta) → Status → Product Link → Notes → Pros → Cons +- Image row: sizing is Claude's discretion (balance compactness with product visibility) +- Multi-line text (notes, pros, cons): rendering approach is Claude's discretion (keep table scannable) + +### Delta highlighting style +- Lightest candidate's weight cell gets a subtle colored background tint (e.g., bg-green-50); cheapest similarly +- Non-best cells show delta text in neutral gray — no colored badges for deltas, only the "best" cell gets color +- Missing weight/price data: Claude's discretion on indicator style (must satisfy COMP-04 — no misleading zeroes) +- Delta format (absolute + delta, or delta only): Claude's discretion based on readability + +### Resolved thread presentation +- Winner column highlight and trophy/banner approach: Claude's discretion (existing resolution banner + column tint are both available patterns) +- Interactive elements in resolved comparison (links clickable vs everything static): Claude's discretion, following the existing Phase 11 pattern where resolved threads disable mutation actions but keep read-only indicators +- Existing resolution banner above the comparison table: Claude's discretion on whether to keep it, remove it, or adapt it + +### Claude's Discretion +- "Add Candidate" button visibility when in compare view +- Image thumbnail sizing in comparison cells (square crop vs wider aspect) +- Multi-line text rendering strategy (clamped with expand vs full text) +- Missing data indicator style (dash with label, empty cell, etc.) +- Delta format: absolute value + delta underneath, or delta only for non-best cells +- Winner column marking approach (column tint, trophy icon, or both) +- Resolved thread interactivity (links clickable vs all read-only) +- Resolution banner behavior in compare view +- View mode persistence (already in Zustand — whether compare resets on navigation or persists) +- Compare toggle icon choice (e.g., Lucide `columns-3`, `table-2`, or similar) +- Table cell padding, border styling, and overall table chrome +- Column minimum/maximum widths +- Keyboard accessibility for horizontal scrolling + + + + +## Existing Code Insights + +### Reusable Assets +- `candidateViewMode` in `uiStore` (`stores/uiStore.ts`): Already stores `'list' | 'grid'` — extend to include `'compare'` +- `CandidateCard` / `CandidateListItem`: Data shape reference for what fields are available per candidate +- `formatWeight()` / `formatPrice()` in `lib/formatters.ts`: Unit-aware formatting for table cells and deltas +- `useWeightUnit()` / `useCurrency()` hooks: Current unit/currency for display +- `RankBadge` (`CandidateListItem.tsx`): Exported component for gold/silver/bronze medals — reuse in compare table name row +- `StatusBadge` (`StatusBadge.tsx`): Click-to-cycle status — render as static text in compare view (no interaction needed) +- `LucideIcon` helper: For compare toggle icon and any icons in the table +- `useThread(threadId)` hook: Returns `thread.candidates[]` with all fields needed (name, weightGrams, priceCents, status, pros, cons, notes, productUrl, imageFilename, categoryName, categoryIcon) + +### Established Patterns +- Three-way toggle: Extend existing `bg-gray-100 rounded-lg p-0.5` toggle bar pattern from thread toolbar +- Pill badges: blue=weight, green=price, gray=category, purple=pros/cons — table can reference these colors for consistency +- framer-motion already installed — AnimatePresence for view transitions if desired +- React Query for server data, Zustand for UI-only state +- Resolution banner: amber-50 bg with amber-200 border in resolved thread header — reusable pattern for winner column + +### Integration Points +- `src/client/routes/threads/$threadId.tsx`: Add compare view branch to the existing list/grid conditional rendering +- `src/client/stores/uiStore.ts`: Extend `candidateViewMode` union type to include `'compare'` +- New component: `ComparisonTable.tsx` (or similar) — receives candidates array, renders the tabular comparison +- No backend changes needed — all data already available from `useThread` hook +- No schema changes — this is a pure frontend/UI phase + + + + +## Specific Ideas + +- Classic product-comparison table like Amazon or Wirecutter — candidates as columns, attributes as rows +- Subtle green tint on the "best" cell rather than heavy badges or bold formatting — keeps the minimalist feel +- Gray delta text for non-best values — visual hierarchy: best stands out, others recede + + + + +## Deferred Ideas + +None — discussion stayed within phase scope + + + +--- + +*Phase: 12-comparison-view* +*Context gathered: 2026-03-17* diff --git a/.planning/phases/29-image-presentation/29-01-SUMMARY.md b/.planning/phases/29-image-presentation/29-01-SUMMARY.md new file mode 100644 index 0000000..3c66885 --- /dev/null +++ b/.planning/phases/29-image-presentation/29-01-SUMMARY.md @@ -0,0 +1,50 @@ +--- +phase: 29 +plan: 01 +subsystem: backend +tags: [schema, image-processing, sharp] +key-files: + created: [] + modified: + - src/db/schema.ts + - src/shared/schemas.ts + - src/server/services/image.service.ts + - src/server/routes/images.ts + - package.json +metrics: + tasks: 7 + commits: 5 + files-changed: 6 +--- + +# Plan 29-01 Summary: Schema + Dominant Color Extraction + +## What was built +- Installed Sharp image processing library for server-side color extraction +- Added `dominant_color`, `crop_zoom`, `crop_x`, `crop_y` columns to items, global_items, and thread_candidates tables +- Created `extractDominantColor()` function that resizes image to 1x1 pixel for weighted average color +- Integrated color extraction into both image upload endpoints (direct and from-url) +- Updated Zod schemas for items, candidates, and global items to accept new fields +- Generated Drizzle migration (db:push deferred — requires running database) + +## Commits + +| Task | Commit | Description | +|------|--------|-------------| +| 1 | cee1500 | Install Sharp for image processing | +| 2 | 36363a8 | Add dominantColor and crop fields to schema | +| 3 | b637b10 | Generate migration for image presentation fields | +| 4 | e305fa7 | Add dominant color extraction via Sharp | +| 5 | 2696b78 | Extract dominant color in image upload endpoints | +| 6 | 3480473 | Add image presentation fields to Zod schemas | +| 7 | — | No changes needed (storage service already spreads fields) | + +## Deviations +- Task 3 (db:push): Database not accessible in dev environment — migration generated but push deferred to deployment. This is non-blocking for frontend work. + +## Self-Check: PASSED +- Sharp installed: YES +- dominant_color in 3 tables: YES (grep confirms 3 occurrences) +- Zod schemas updated: YES (3 schemas) +- Upload returns dominantColor: YES +- Lint passes: YES diff --git a/bun.lock b/bun.lock index cc3ade7..3baca6f 100644 --- a/bun.lock +++ b/bun.lock @@ -38,7 +38,6 @@ "@types/bun": "latest", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", - "@types/sharp": "^0.32.0", "@vitejs/plugin-react": "^6.0.1", "concurrently": "^9.1.2", "drizzle-kit": "^0.31.9", @@ -560,8 +559,6 @@ "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="], - "@types/sharp": ["@types/sharp@0.32.0", "", { "dependencies": { "sharp": "*" } }, "sha512-OOi3kL+FZDnPhVzsfD37J88FNeZh6gQsGcLc95NbeURRGvmSjeXiDcyWzF2o3yh/gQAUn2uhh/e+CPCa5nwAxw=="], - "@types/use-sync-external-store": ["@types/use-sync-external-store@0.0.6", "", {}, "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg=="], "@vitejs/plugin-react": ["@vitejs/plugin-react@6.0.1", "", { "dependencies": { "@rolldown/pluginutils": "1.0.0-rc.7" }, "peerDependencies": { "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", "babel-plugin-react-compiler": "^1.0.0", "vite": "^8.0.0" }, "optionalPeers": ["@rolldown/plugin-babel", "babel-plugin-react-compiler"] }, "sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ=="], diff --git a/package.json b/package.json index 2aadaa6..39058ac 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,6 @@ "@types/bun": "latest", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", - "@types/sharp": "^0.32.0", "@vitejs/plugin-react": "^6.0.1", "concurrently": "^9.1.2", "drizzle-kit": "^0.31.9", diff --git a/src/client/components/CandidateListItem.tsx b/src/client/components/CandidateListItem.tsx index d67e611..1177b19 100644 --- a/src/client/components/CandidateListItem.tsx +++ b/src/client/components/CandidateListItem.tsx @@ -95,10 +95,7 @@ export function CandidateListItem({ }} > {candidate.imageUrl ? ( - + ) : ( ).dominantColor as string) || "#f3f4f6" + ? ((item as Record).dominantColor as string) || + "#f3f4f6" : undefined, }} > {item.imageUrl ? ( - + ) : (
).dominantColor as string) || "#f3f4f6" + ? ((item as Record).dominantColor as string) || + "#f3f4f6" : undefined, }} > {item.imageUrl ? ( - + ) : (
).dominantColor as string) || "#f3f4f6" + ? ((c as Record).dominantColor as string) || + "#f3f4f6" : undefined, }} > {c.imageUrl ? ( - + ) : ( {item.imageUrl ? (
- +
) : (
diff --git a/src/client/routes/global-items/index.tsx b/src/client/routes/global-items/index.tsx index 9fc3117..582f893 100644 --- a/src/client/routes/global-items/index.tsx +++ b/src/client/routes/global-items/index.tsx @@ -547,10 +547,7 @@ function GlobalItemListRow({ item, weight, price }: ListRowProps) { }} > {item.imageUrl ? ( - + ) : (