238 lines
7.8 KiB
Markdown
238 lines
7.8 KiB
Markdown
---
|
|
phase: 29
|
|
slug: image-presentation
|
|
status: draft
|
|
shadcn_initialized: false
|
|
preset: none
|
|
created: 2026-04-12
|
|
---
|
|
|
|
# Phase 29 — UI Design Contract
|
|
|
|
> Visual and interaction contract for image presentation changes. Generated by gsd-ui-researcher, verified by gsd-ui-checker.
|
|
|
|
---
|
|
|
|
## Design System
|
|
|
|
| Property | Value |
|
|
|----------|-------|
|
|
| Tool | none (Tailwind CSS v4 direct) |
|
|
| Preset | not applicable |
|
|
| Component library | none (custom components) |
|
|
| Icon library | Lucide (via custom LucideIcon wrapper) |
|
|
| Font | System default (inherited) |
|
|
|
|
---
|
|
|
|
## Spacing Scale
|
|
|
|
Declared values (must be multiples of 4):
|
|
|
|
| Token | Value | Usage |
|
|
|-------|-------|-------|
|
|
| xs | 4px | Icon gaps, inline padding |
|
|
| sm | 8px | Compact element spacing |
|
|
| md | 16px | Default element spacing |
|
|
| lg | 24px | Section padding |
|
|
| xl | 32px | Layout gaps |
|
|
| 2xl | 48px | Major section breaks |
|
|
| 3xl | 64px | Page-level spacing |
|
|
|
|
Exceptions: none
|
|
|
|
---
|
|
|
|
## Typography
|
|
|
|
No new typography introduced. All text elements use existing typographic scale from the app.
|
|
|
|
| Role | Size | Weight | Line Height |
|
|
|------|------|--------|-------------|
|
|
| Body | 14px | 400 | 1.5 |
|
|
| Label | 12px | 500 | 1.25 |
|
|
| Heading | 14px | 600 | 1.25 |
|
|
|
|
---
|
|
|
|
## Color
|
|
|
|
No new brand colors introduced. The only new color element is the **dominant color background** which is dynamically extracted per-image.
|
|
|
|
| Role | Value | Usage |
|
|
|------|-------|-------|
|
|
| Dominant (60%) | white (#ffffff) | Page background (unchanged) |
|
|
| Secondary (30%) | gray-50 (#f9fafb) | Card surfaces, fallback image bg |
|
|
| Accent (10%) | blue-50/green-50 | Weight/price badges (unchanged) |
|
|
| Dynamic fill | per-image dominant color | Image container background behind `object-contain` images |
|
|
| Fallback fill | gray-100 (#f3f4f6) | Image container background when dominant color unavailable |
|
|
|
|
Accent reserved for: weight badges (blue-50), price badges (green-50), category badges (gray-50)
|
|
|
|
---
|
|
|
|
## Image Container Specifications
|
|
|
|
### GearImage Component
|
|
|
|
A new shared component replaces all inline `<img>` elements for gear/product images.
|
|
|
|
| Property | Spec |
|
|
|----------|------|
|
|
| Component name | `GearImage` |
|
|
| File location | `src/client/components/GearImage.tsx` |
|
|
| Default aspect ratio | `4/3` (cards, upload preview) |
|
|
| Detail page ratio | `16/9` (global item detail, candidate detail) |
|
|
| Border radius | `rounded-xl` (12px) on detail pages; inherited from parent on cards |
|
|
| Overflow | `hidden` (always) |
|
|
|
|
### Default State (no crop)
|
|
|
|
```
|
|
Container: aspect-[4/3], overflow-hidden
|
|
Background: dominant color OR #f3f4f6 (gray-100 fallback)
|
|
Image: object-contain, w-full, h-full
|
|
Result: Full image visible, letterbox/pillarbox fill with dominant color
|
|
```
|
|
|
|
### Cropped State (user-defined zoom+pan)
|
|
|
|
```
|
|
Container: aspect-[4/3], overflow-hidden
|
|
Background: dominant color OR #f3f4f6
|
|
Image: w-full, h-full, object-cover
|
|
Transform: scale(cropZoom) translate(cropX%, cropY%)
|
|
Transform origin: center center
|
|
Result: User-framed view with cropped overflow hidden
|
|
```
|
|
|
|
### Empty State (no image)
|
|
|
|
```
|
|
Container: aspect-[4/3], bg-gray-50
|
|
Content: Centered LucideIcon (category icon), text-gray-400, size 36px
|
|
```
|
|
Unchanged from current behavior.
|
|
|
|
### Transition
|
|
|
|
No CSS transitions on the image itself. Background color applies immediately via inline `style={{ backgroundColor }}`.
|
|
|
|
---
|
|
|
|
## Zoom+Pan Editor Specifications
|
|
|
|
### Editor Trigger Points
|
|
|
|
| Location | Trigger | Behavior |
|
|
|----------|---------|----------|
|
|
| ImageUpload component | After image upload completes | Editor overlay appears on the uploaded image |
|
|
| Item detail page | "Adjust framing" button below image | Editor overlay replaces static image view |
|
|
| Global item detail page | "Adjust framing" button below image | Same as item detail |
|
|
| Candidate detail page | "Adjust framing" button below image | Same as item detail |
|
|
|
|
### Editor UI
|
|
|
|
| Element | Spec |
|
|
|---------|------|
|
|
| Library | react-easy-crop |
|
|
| Crop shape | rect |
|
|
| Aspect ratio | Matches container (4/3 for cards, 16/9 for detail pages where applicable) |
|
|
| Min zoom | 1.0 (fit-within, default) |
|
|
| Max zoom | 3.0 |
|
|
| Background | Dominant color of the image (or gray-100 fallback) |
|
|
| Controls | Zoom slider below the crop area |
|
|
| Save button | "Save framing" — primary action, bottom-right |
|
|
| Cancel button | "Cancel" — secondary/ghost, bottom-left |
|
|
| Button spacing | 8px gap between cancel and save |
|
|
|
|
### Editor Overlay Layout
|
|
|
|
```
|
|
+-------------------------------------------+
|
|
| |
|
|
| [react-easy-crop area] |
|
|
| (drag to pan, scroll to zoom) |
|
|
| |
|
|
+-------------------------------------------+
|
|
| [------- zoom slider -------] |
|
|
+-------------------------------------------+
|
|
| Cancel Save framing |
|
|
+-------------------------------------------+
|
|
```
|
|
|
|
- Overlay uses `fixed inset-0 z-50 bg-black/60` on mobile, `relative` inline on desktop detail pages
|
|
- On ImageUpload: overlay within the upload container
|
|
- On detail pages: replaces the image area inline (no modal)
|
|
|
|
### Editor Output
|
|
|
|
| Field | Type | Range | Description |
|
|
|-------|------|-------|-------------|
|
|
| cropZoom | number | 1.0 - 3.0 | Zoom level (1.0 = fit within) |
|
|
| cropX | number | -50 to 50 | Horizontal pan offset (percentage) |
|
|
| cropY | number | -50 to 50 | Vertical pan offset (percentage) |
|
|
|
|
When zoom is 1.0 and x/y are 0: equivalent to default `object-contain` (no crop applied).
|
|
|
|
---
|
|
|
|
## Copywriting Contract
|
|
|
|
| Element | Copy |
|
|
|---------|------|
|
|
| Adjust framing button | "Adjust framing" |
|
|
| Editor save CTA | "Save framing" |
|
|
| Editor cancel | "Cancel" |
|
|
| Zoom slider label | "Zoom" (sr-only) |
|
|
| Empty image placeholder | "Click to add photo" (unchanged) |
|
|
| Backfill progress (admin) | "Processing images... {N}/{total}" |
|
|
|
|
---
|
|
|
|
## Surface-by-Surface Spec
|
|
|
|
Each surface adopts the `GearImage` component. All surfaces use 4/3 ratio except where noted.
|
|
|
|
| # | Surface | File | Ratio | Has Editor | Notes |
|
|
|---|---------|------|-------|------------|-------|
|
|
| 1 | ItemCard | `components/ItemCard.tsx` | 4/3 | No | Card only, editor on detail page |
|
|
| 2 | GlobalItemCard | `components/GlobalItemCard.tsx` | 4/3 | No | Card only |
|
|
| 3 | CandidateCard | `components/CandidateCard.tsx` | 4/3 | No | Card only |
|
|
| 4 | CandidateListItem | `components/CandidateListItem.tsx` | 4/3 | No | Small thumbnail |
|
|
| 5 | ImageUpload | `components/ImageUpload.tsx` | 4/3 | Yes | Editor after upload |
|
|
| 6 | ComparisonTable | `components/ComparisonTable.tsx` | 4/3 | No | Table cell image |
|
|
| 7 | LinkToGlobalItem | `components/LinkToGlobalItem.tsx` | 1/1 | No | Small 32px thumbnail, keep object-cover for tiny icons |
|
|
| 8 | CatalogSearchOverlay | `components/CatalogSearchOverlay.tsx` | 4/3 | No | Search result cards (2 instances) |
|
|
| 9 | Item detail | `routes/items/$itemId.tsx` | 4/3 | Yes | Full editor access |
|
|
| 10 | Global item detail | `routes/global-items/$globalItemId.tsx` | 16/9 | Yes | Full editor access |
|
|
| 11 | Global items index | `routes/global-items/index.tsx` | 4/3 | No | List card |
|
|
| 12 | Candidate detail | `routes/threads/$threadId/candidates/$candidateId.tsx` | 16/9 | Yes | Full editor access |
|
|
|
|
### LinkToGlobalItem Exception
|
|
|
|
The 32x32px thumbnail in LinkToGlobalItem is too small for letterbox treatment. Keep `object-cover` with `rounded` for this surface. The GearImage component should accept a `cover` prop to force object-cover mode for tiny thumbnails.
|
|
|
|
---
|
|
|
|
## Registry Safety
|
|
|
|
| Registry | Blocks Used | Safety Gate |
|
|
|----------|-------------|-------------|
|
|
| npm (react-easy-crop) | react-easy-crop | MIT license, 500k+ weekly downloads, active maintenance |
|
|
|
|
No shadcn blocks used in this phase.
|
|
|
|
---
|
|
|
|
## Checker Sign-Off
|
|
|
|
- [x] Dimension 1 Copywriting: PASS
|
|
- [x] Dimension 2 Visuals: PASS
|
|
- [x] Dimension 3 Color: PASS
|
|
- [x] Dimension 4 Typography: PASS
|
|
- [x] Dimension 5 Spacing: PASS
|
|
- [x] Dimension 6 Registry Safety: PASS
|
|
|
|
**Approval:** approved 2026-04-12
|