Phases 28-31 archived to milestones/v2.2-phases/ Requirements and roadmap snapshots archived to milestones/ Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
7.8 KiB
phase, slug, status, shadcn_initialized, preset, created
| phase | slug | status | shadcn_initialized | preset | created |
|---|---|---|---|---|---|
| 29 | image-presentation | draft | false | none | 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/60on mobile,relativeinline 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
- Dimension 1 Copywriting: PASS
- Dimension 2 Visuals: PASS
- Dimension 3 Color: PASS
- Dimension 4 Typography: PASS
- Dimension 5 Spacing: PASS
- Dimension 6 Registry Safety: PASS
Approval: approved 2026-04-12