From eac7cea0c8c07d0aec9223390f0d24bd9fabef0a Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Sun, 12 Apr 2026 19:47:13 +0200 Subject: [PATCH] docs(29): UI design contract --- .../29-image-presentation/29-UI-SPEC.md | 237 ++++++++++++++++++ 1 file changed, 237 insertions(+) create mode 100644 .planning/phases/29-image-presentation/29-UI-SPEC.md diff --git a/.planning/phases/29-image-presentation/29-UI-SPEC.md b/.planning/phases/29-image-presentation/29-UI-SPEC.md new file mode 100644 index 0000000..9194105 --- /dev/null +++ b/.planning/phases/29-image-presentation/29-UI-SPEC.md @@ -0,0 +1,237 @@ +--- +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 `` 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