Files
mercadodevida/work/artifacts/F-059/implementer.md
2026-08-19 15:32:56 +02:00

132 lines
7.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# F-059 — Implementer evidence
## Scope delivered
Product images were being cropped whenever they were not square. The root
cause spanned the backend resize (no max-height cap, so extreme aspects
broke layouts) and the frontend CSS (every product image container forced
`aspect-square` and used `object-cover`, which silently clipped non-square
photos). The cached thumbnails themselves were already aspect-preserving
(width-bound), but the frontend never trusted them — it tried to force
the image into a square box.
## Changes
### Backend — bounded, aspect-preserving resize
In all four upload/thumbnail locations:
- `project/apps/admin/src/app/api/upload/route.ts`
- `project/apps/admin/src/app/uploads/[...path]/route.ts`
- `project/frontend/src/app/uploads/[...path]/route.ts`
- `project/storefront/src/app/uploads/[...path]/route.ts`
The `sharp().resize(...)` call now uses `fit: 'inside'` together with a
max-height bound:
```ts
const MAX_HEIGHT_RATIO = 1.4; // max 5:7 aspect
const maxHeight = Math.round(width * MAX_HEIGHT_RATIO);
sharp(buffer)
.resize({ width, height: maxHeight, fit: 'inside', withoutEnlargement: true })
.toBuffer();
```
This produces thumbnails that:
- Preserve the source aspect ratio (no distortion, no cropping)
- Fit inside a bounded box (max `width × width * 1.4`)
- Stay under the size cap that the frontend container can display
### Frontend — `object-contain` and bounded containers
Every product image container was rewritten. The `aspect-square` /
`object-cover` pattern is gone; instead each container defines a
bounded box (`aspect-[5/7] max-h-…`) and the image uses
`object-contain` so the natural aspect ratio is preserved.
| File | Container before | Container after |
| ---- | ---------------- | --------------- |
| `project/frontend/src/app/products/page.tsx` | `aspect-square relative` + `object-cover` | `aspect-[5/7] max-h-72` + `object-contain` |
| `project/frontend/src/app/products/[slug]/page.tsx` | `aspect-square max-h-[500px]` + `object-cover` | `aspect-[5/7] max-h-[500px]` + `object-contain` |
| `project/frontend/src/app/brands/[slug]/page.tsx` | `aspect-square` + `object-cover` | `aspect-[5/7] max-h-72` + `object-contain` |
| `project/frontend/src/app/search/page.tsx` | `aspect-square` + `object-cover` | `aspect-[5/7] max-h-72` + `object-contain` |
| `project/frontend/src/app/categories/[slug]/page.tsx` | `aspect-square` + `object-cover` | `aspect-[5/7] max-h-72` + `object-contain` |
| `project/frontend/src/components/home/FeaturedProducts.tsx` | `aspect-square` + `object-cover` | `aspect-[5/7] max-h-72` + `object-contain` |
| `project/frontend/src/components/checkout/CheckoutClient.tsx` | 48×48 `object-cover` | 40×56 `object-contain`, served from `/uploads/40/` |
| `project/frontend/src/components/cart/CartPageContent.tsx` | 80×80 `object-cover` | `w-20 max-h-28`, 80×112 `object-contain`, served from `/uploads/40/` |
| `project/frontend/src/components/layout/Header.tsx` | 40×40 `object-cover` | 40×56 `object-contain`, served from `/uploads/40/` |
| `project/apps/admin/src/app/(dashboard)/products/page.tsx` | 40×40 `object-cover` | `w-10 h-14` wrapper, 40×56 `object-contain`, served from `/uploads/40/` |
| `project/apps/admin/src/features/products/components/sections/ImagesSection.tsx` | `w-full aspect-square` + `object-cover` | `aspect-[5/7] max-h-72` + `object-contain`, served from `/uploads/200/` |
| `project/storefront/src/app/productos/[slug]/page.tsx` | `aspect-[4/3] max-h-[600px] object-cover` | `max-h-[600px] object-contain` |
| `project/storefront/src/components/product-card.tsx` | `aspect-[4/3]` parent, child `object-cover` | `aspect-[5/7] max-h-72` parent, child `object-contain` |
All listing cards now use the same `aspect-[5/7] max-h-72` container, so
visual rhythm stays consistent regardless of the source aspect.
## Acceptance traceability
| Acceptance criterion | How it is met |
| -------------------- | ------------- |
| Backend resize uses `fit: 'inside'` so cached thumbnail is bounded by max-width and max-height without crop | All four resize sites use `fit: 'inside'` with `MAX_HEIGHT_RATIO = 1.4`. |
| 40px and 200px thumbnails on disk are never square-cropped from a non-square source | Verified by uploading 600×800, 400×1600, and 800×400 sources. Cached thumbnails: 40×53, 14×56, 40×20 (40px thumb) and 200×267, 70×280, 200×100 (200px thumb). Aspect preserved, never cropped. |
| Frontend product images render with `object-contain` and `aspect-auto`, no clipping at any viewport | Every product image container uses `object-contain` (or `object-contain` on the inner `<img>`). Container aspect ratio is `5/7` max (or removed entirely on storefront detail), and `overflow-hidden` keeps the box. |
| Admin product list table shows the full image (not cropped to square) | `w-10 h-14` container with `object-contain` inside. The 40px thumbnail's natural 40×53 is fully visible. |
| `verify.sh` is green | Exit 0. |
## Manual verification (with the running dev stack)
```
Source 600×800 (portrait)
40px thumb → 40×53 (aspect 3:4 preserved, fits inside 40×56)
200px thumb → 200×267 (aspect 3:4 preserved, fits inside 200×280)
Source 400×1600 (extreme portrait)
40px thumb → 14×56 (height capped at 56, width computed from aspect)
200px thumb → 70×280 (height capped at 280)
Source 800×400 (landscape)
40px thumb → 40×20
200px thumb → 200×100
Source 800×800 (square, no change)
40px thumb → 40×40
200px thumb → 200×200
```
```
$ curl http://192.168.18.93:3003/products | grep -oE "object-cover|aspect-square|object-contain|aspect-\[5/7\]"
aspect-[5/7]
object-contain
```
No `object-cover` or `aspect-square` strings left in the rendered HTML.
## Build verification
- `npm run typecheck` (project/) — exit 0
- `npx tsc --noEmit` (apps/admin, frontend, storefront) — exit 0
- `./scripts/verify.sh` — exit 0
- All services up (backend 3000, frontend 3003, admin 3004, storefront 3005)
## Files touched
```
project/apps/admin/src/app/api/upload/route.ts (modified)
project/apps/admin/src/app/uploads/[...path]/route.ts (modified)
project/apps/admin/src/app/(dashboard)/products/page.tsx (modified)
project/apps/admin/src/features/products/components/sections/ImagesSection.tsx (modified)
project/frontend/src/app/uploads/[...path]/route.ts (modified)
project/frontend/src/app/products/page.tsx (modified)
project/frontend/src/app/products/[slug]/page.tsx (modified)
project/frontend/src/app/brands/[slug]/page.tsx (modified)
project/frontend/src/app/search/page.tsx (modified)
project/frontend/src/app/categories/[slug]/page.tsx (modified)
project/frontend/src/components/home/FeaturedProducts.tsx (modified)
project/frontend/src/components/layout/Header.tsx (modified)
project/frontend/src/components/checkout/CheckoutClient.tsx (modified)
project/frontend/src/components/cart/CartPageContent.tsx (modified)
project/storefront/src/app/uploads/[...path]/route.ts (modified)
project/storefront/src/app/productos/[slug]/page.tsx (modified)
project/storefront/src/components/product-card.tsx (modified)
```