` wraps the image.
test -f src/client/components/GearImage.tsx && grep "object-contain" src/client/components/GearImage.tsx && echo "PASS" || echo "FAIL"
- `src/client/components/GearImage.tsx` exists
- Exports `GearImage` component
- Default rendering uses `object-contain` (not `object-cover`)
- When `cover` prop is true, uses `object-cover`
- When crop values exist and cropZoom > 1, uses CSS transform with scale and translate
- Accepts `dominantColor`, `cropZoom`, `cropX`, `cropY` props
### Task 2: Update ItemCard to use GearImage
- src/client/components/ItemCard.tsx
- src/client/components/GearImage.tsx
In `src/client/components/ItemCard.tsx`:
1. Add `dominantColor`, `cropZoom`, `cropX`, `cropY` to `ItemCardProps` interface (all `number | null` or `string | null`)
2. Import `GearImage` from `./GearImage`
3. Replace the image div (around line 164-179):
Current:
```tsx
{imageUrl ? (

) : (
)}
```
New:
```tsx
```
grep "GearImage" src/client/components/ItemCard.tsx && ! grep "object-cover" src/client/components/ItemCard.tsx && echo "PASS" || echo "FAIL"
- ItemCard imports and uses GearImage component
- No `object-cover` remains in ItemCard.tsx
- `dominantColor` prop is passed to GearImage
- Parent div uses inline `backgroundColor` style from dominantColor
- Empty state (no image) still shows category icon on gray-50 background
### Task 3: Update GlobalItemCard to use GearImage
- src/client/components/GlobalItemCard.tsx
- src/client/components/GearImage.tsx
In `src/client/components/GlobalItemCard.tsx`:
1. Add `dominantColor?: string | null`, `cropZoom?: number | null`, `cropX?: number | null`, `cropY?: number | null` to `GlobalItemCardProps`
2. Import `GearImage` from `./GearImage`
3. Replace the image rendering (around line 31-54):
Current:
```tsx
{imageUrl ? (

) : (
{/* SVG placeholder */}
)}
```
New:
```tsx
{imageUrl ? (
) : (
{/* Keep existing SVG placeholder */}
)}
```
grep "GearImage" src/client/components/GlobalItemCard.tsx && ! grep "object-cover" src/client/components/GlobalItemCard.tsx && echo "PASS" || echo "FAIL"
- GlobalItemCard imports and uses GearImage
- No `object-cover` in GlobalItemCard.tsx
- Props include dominantColor, cropZoom, cropX, cropY
### Task 4: Update CandidateCard to use GearImage
- src/client/components/CandidateCard.tsx
- src/client/components/GearImage.tsx
Same pattern as Task 2/3:
1. Add `dominantColor`, `cropZoom`, `cropX`, `cropY` to props interface
2. Import `GearImage`
3. Replace `
` with `` inside the existing `aspect-[4/3]` container
4. Update parent div to use inline `backgroundColor` style
grep "GearImage" src/client/components/CandidateCard.tsx && ! grep "object-cover" src/client/components/CandidateCard.tsx && echo "PASS" || echo "FAIL"
- CandidateCard uses GearImage component
- No `object-cover` remaining
- Dominant color props threaded through
### Task 5: Update CandidateListItem to use GearImage
- src/client/components/CandidateListItem.tsx
- src/client/components/GearImage.tsx
Same pattern:
1. Add image presentation props to interface
2. Import GearImage
3. Replace `object-cover` image with GearImage
4. Update parent container background color
grep "GearImage" src/client/components/CandidateListItem.tsx && ! grep "object-cover" src/client/components/CandidateListItem.tsx && echo "PASS" || echo "FAIL"
- CandidateListItem uses GearImage
- No `object-cover` remaining
### Task 6: Update ComparisonTable to use GearImage
- src/client/components/ComparisonTable.tsx
- src/client/components/GearImage.tsx
Same pattern: replace inline `
` with ``. Thread dominantColor and crop props from the data source.
grep "GearImage" src/client/components/ComparisonTable.tsx && ! grep "object-cover" src/client/components/ComparisonTable.tsx && echo "PASS" || echo "FAIL"
- ComparisonTable uses GearImage
- No `object-cover` remaining
### Task 7: Update CatalogSearchOverlay to use GearImage
- src/client/components/CatalogSearchOverlay.tsx
- src/client/components/GearImage.tsx
CatalogSearchOverlay has 2 `object-cover` instances (search results and selected item preview). Replace both with GearImage. Thread dominantColor from global item data.
grep "GearImage" src/client/components/CatalogSearchOverlay.tsx && ! grep "object-cover" src/client/components/CatalogSearchOverlay.tsx && echo "PASS" || echo "FAIL"
- Both image instances in CatalogSearchOverlay use GearImage
- No `object-cover` remaining in the file
### Task 8: Update ImageUpload preview to use GearImage
- src/client/components/ImageUpload.tsx
- src/client/components/GearImage.tsx
In `src/client/components/ImageUpload.tsx`:
1. Add `dominantColor?: string | null` to `ImageUploadProps`
2. Import GearImage
3. Replace the preview image (line 76-79):
Current:
```tsx
```
New:
```tsx
```
4. Update the parent container to use dominant color background:
```tsx
style={{ backgroundColor: displayUrl ? (dominantColor || "#f3f4f6") : undefined }}
```
grep "GearImage" src/client/components/ImageUpload.tsx && ! grep "object-cover" src/client/components/ImageUpload.tsx && echo "PASS" || echo "FAIL"
- ImageUpload uses GearImage for preview
- No `object-cover` remaining
- Accepts dominantColor prop
### Task 9: Update item detail page
- src/client/routes/items/$itemId.tsx
- src/client/components/GearImage.tsx
In `src/client/routes/items/$itemId.tsx`:
1. Import GearImage
2. Replace the `object-cover` image (around line 245-250) with GearImage
3. Update the parent `aspect-[4/3]` div to use dominant color background via inline style
4. Thread dominantColor, cropZoom, cropX, cropY from the item data
grep "GearImage" src/client/routes/items/\$itemId.tsx && ! grep "object-cover" src/client/routes/items/\$itemId.tsx && echo "PASS" || echo "FAIL"
- Item detail page uses GearImage
- No `object-cover` in the file
- Dominant color and crop fields used from item data
### Task 10: Update global item detail page
- src/client/routes/global-items/$globalItemId.tsx
- src/client/components/GearImage.tsx
In `src/client/routes/global-items/$globalItemId.tsx`:
1. Import GearImage
2. Replace the `object-cover` image (around line 65-70) with GearImage
3. This page uses `aspect-[16/9]` — keep that ratio on the parent container
4. Update background color to use dominant color
grep "GearImage" src/client/routes/global-items/\$globalItemId.tsx && ! grep "object-cover" src/client/routes/global-items/\$globalItemId.tsx && echo "PASS" || echo "FAIL"
- Global item detail uses GearImage
- No `object-cover` remaining
- Aspect ratio 16/9 preserved
### Task 11: Update global items index page
- src/client/routes/global-items/index.tsx
- src/client/components/GearImage.tsx
In `src/client/routes/global-items/index.tsx`:
1. Import GearImage
2. Replace `object-cover` image with GearImage
3. Thread dominantColor from global item data
grep "GearImage" src/client/routes/global-items/index.tsx && ! grep "object-cover" src/client/routes/global-items/index.tsx && echo "PASS" || echo "FAIL"
- Global items index uses GearImage
- No `object-cover` remaining
### Task 12: Update candidate detail page
- src/client/routes/threads/$threadId/candidates/$candidateId.tsx
- src/client/components/GearImage.tsx
In `src/client/routes/threads/$threadId/candidates/$candidateId.tsx`:
1. Import GearImage
2. Replace `object-cover` image with GearImage
3. This page uses `aspect-[16/9]` — keep that ratio
4. Thread dominantColor and crop fields from candidate data
grep "GearImage" src/client/routes/threads/\$threadId/candidates/\$candidateId.tsx && ! grep "object-cover" src/client/routes/threads/\$threadId/candidates/\$candidateId.tsx && echo "PASS" || echo "FAIL"
- Candidate detail uses GearImage
- No `object-cover` remaining
- Aspect ratio 16/9 preserved
### Task 13: Update LinkToGlobalItem with cover mode
- src/client/components/LinkToGlobalItem.tsx
- src/client/components/GearImage.tsx
In `src/client/components/LinkToGlobalItem.tsx`:
The 32x32px thumbnail is too small for letterbox treatment. Use GearImage with `cover={true}` prop to keep `object-cover` for this tiny thumbnail:
Replace:
```tsx
```
With:
```tsx
```
grep "GearImage" src/client/components/LinkToGlobalItem.tsx && echo "PASS" || echo "FAIL"
- LinkToGlobalItem uses GearImage with `cover` prop
- Small thumbnail renders with object-cover (intentional exception for tiny images)
1. `bun run lint` passes
2. `bun run build` passes (TypeScript compilation)
3. `grep -r "object-cover" src/client/ --include="*.tsx"` returns ONLY:
- `GearImage.tsx` (internal cover mode)
- `ProfileSection.tsx` (user avatar — out of scope)
- `routes/users/$userId.tsx` (user avatar — out of scope)
4. All 12 surfaces render images with `object-contain` by default
- GearImage component exists and is used by all 12 gear image surfaces
- Default image display uses object-contain (fit-within)
- Dominant color background fills letterbox/pillarbox space
- Cropped images display with CSS transform
- LinkToGlobalItem uses cover mode for 32px thumbnails
- No regression in empty state (placeholder icons still show)
| Threat | Severity | Mitigation |
|--------|----------|------------|
| XSS via dominantColor in style attribute | Low | dominantColor is server-extracted hex string, not user input; React escapes style values |
| Layout shift from object-contain | Low | Container maintains fixed aspect ratio; image loads within same bounds |
- [ ] GearImage component created at src/client/components/GearImage.tsx
- [ ] All 12 image surfaces use GearImage (except ProfileSection/user avatar)
- [ ] Default rendering uses object-contain, not object-cover
- [ ] Dominant color background on image containers
- [ ] LinkToGlobalItem uses cover mode for tiny thumbnails