diff --git a/docs/UI-SPEC.md b/docs/UI-SPEC.md index 3bde939..8a1574c 100644 --- a/docs/UI-SPEC.md +++ b/docs/UI-SPEC.md @@ -20,6 +20,7 @@ ### Card geometry - `.card{position:absolute;left:14px;right:14px;top:6px;bottom:34px;border-radius:18px;overflow:hidden;background:var(--card);box-shadow:0 0 0 1px rgba(255,255,255,.07),0 18px 40px rgba(0,0,0,.5);will-change:transform;touch-action:none}` - Photo full-bleed (`object-fit:cover`); bottom gradient 52% tall (transparent → rgba(0,0,0,.55) @55% → .88). Info block at bottom:14px: chips row → title (23px/800, clamp 2 lines) → price (20px/600) → sub (13px, var(--mut)). +- Photo fit is **contain** (v1.1): the whole photo is shown uncropped over a soft blurred backdrop of the same image (option C). The backdrop is decorative (blurred, dimmed, overflow-clipped); the sharp photo sits on top with a gentle shadow. The fullscreen viewer already fits; list thumbs stay `cover` (56px). - No photo → placeholder panel: 45° repeating stripes (#141419/#18181f), centered copy: "Photo pending — thumbnail backfill queued". - ⓘ button: 32px circle, top-right of card (rgba(0,0,0,.45), 1px rgba(255,255,255,.25) border) → opens the dossier for that card. - Chips: 11px/600, rgba(255,255,255,.14) bg, radius 999px. Production content: max 3 = seller chip ("Dealer" only when seller_type=dealer) + first 2 extras. @@ -51,6 +52,8 @@ ### Empty state Dashed panel, copy exactly: **"You're all caught up."** / "New bikes arrive nightly — next import 08:30." / "↺ brings the last card back." Undo restores the last card. +§2's quiet "You're all caught up." remains only for legacy (pre-section) mode. In section mode, when the deck can serve nothing at all — no fresh listings, no eligible second looks, no wildcards — the deck shows a prominent **"No more new bikes!"** panel with "New bikes arrive nightly — next import 08:30." and, when true, "↺ brings the last card back." This panel supersedes the report card in that state only; the report card remains the end-of-section screen whenever more cards can still be served (v1.1). + ## 3. Dossier (fullscreen sheet) — one sheet, both doors Opened from deck ⓘ (or tap) AND list row tap. iOS-sheet geometry: top gap 26px; `border-radius:18px 18px 0 0`; bg #0f0f13; slide-in `.3s cubic-bezier(.2,.8,.3,1)` from `translateY(103%)`. Backdrop: rgba(0,0,0,.38), z 69/70. @@ -87,7 +90,7 @@ Opened from deck ⓘ (or tap) AND list row tap. iOS-sheet geometry: top gap 26px 1. **Data wiring.** Deck buffer from `GET /api/deck` (limit 40, opaque cursor); refill when buffer <5 and a cursor exists. List from `GET /api/list`. Dossier from the row/card payload. Header from `GET /api/stats`. 2. **Swipe persistence.** Commits fire `POST /api/swipe` optimistically — the UI never waits. Failures go to a retry queue (attempts at ~2s/5s/10s, then every 15s while the tab lives); toast "Couldn't save — retrying" on first failure. Undo fires `DELETE /api/swipe/{id}` through the same queue. -3. **Badge.** `POST /api/badge/seen` once per session when the deck first renders. Header shows "{live} live" always; "· {new} new" only when new>0. +3. **Badge.** `POST /api/badge/seen` once per session when the deck first renders. Header pill shows `{live} live · {fresh} fresh` always, and `· {new} new` only when new > 0. `fresh` = active listings with no swipe (the deck's new-pool predicate) — v1.1. 4. **List filters + search** per §4 (mockup had placeholder chips only). 5. **Matches real (§5); Trends stays stub.** 6. **Images.** Every card/thumb references `/img/{id}` (server: cached file → 302 CDN → 404 → placeholder). The client never hotlinks directly. @@ -119,7 +122,7 @@ Human (parent-run after build): Glenn swipes a real deck from his phone and edit - The deck serves ONE section at a time: 25 cards (default). A section is the unit of a swipe burst; when it is exhausted, the next one is forged (with an agent pass in between — §10.2). - Header right side, next to the stats: section progress "7/25" (12px/650, muted; turns `--accent1` at 25/25; fits within the existing header, no layout shift). -- Section composition (engine side — RANKING.md): ≤5 SECOND LOOK cards + 2 WILDCARD cards + the remainder new. +- Section composition (engine side — RANKING.md): every batch targets 25 cards = fresh first (up to 23 slots) + ≥2 SECOND LOOK cards whenever eligible stock exists + 2 WILDCARD cards; when fresh < 23, second looks fill the remainder up to 25. Re-nope returns stay ≤2 per batch; fresh shortfalls are never padded (v1.1). - ALL §2 mechanics apply unchanged inside a section: 5-deep stack, `.topcard` invariant, no-lock commit, stamps, undo; the buffer refills from the pinned section order. ### 10.2 Section report + forge @@ -133,13 +136,14 @@ Human (parent-run after build): Glenn swipes a real deck from his phone and edit - **Forge mode `ondemand` (default):** primary button **"Forge next 25"**. Tap → inline status "The agent is reading your strikes…" → new section hydrates. If the pass fails: "Agent offline — forging on the last read." and the forge still lands (fallback profile). - **Forge mode `strict`:** button disabled; caption "Waiting for the nightly read — next section after the agent runs." - Both modes: small toggle row under the actions — "Forge: on demand ⇄ strict" (persists `prefs['forge_mode']`). + - When a forged batch reports `new: 0` the report shows: "Catalog fully reviewed — fresh listings arrive with the next 08:30 import; showing second looks." and the forge reads the same note in its status line (v1.1). - Undo from the report screen re-opens the section with the last card (§2 undo semantics). ### 10.3 Card bands (the ONLY card-face additions) - Position: top-left inset (12px); `pointer-events:none`; no layout shift; no effect on stack metrics or drag. 11px/650, letter-spacing .4px, radius 999px, padding 4px 9px, max-width 78%; may wrap to 2 lines. - **WILDCARD — maybe you'll like this one instead** — color `#ffb43a`; bg `rgba(255,180,58,.14)`; border `1px solid rgba(255,180,58,.35)`. -- **SECOND LOOK — you {liked|maybed|noped} this** — color `rgba(255,255,255,.85)`; bg `rgba(255,255,255,.10)`; border `1px solid rgba(255,255,255,.22)`. +- **SECOND LOOK — you {liked|maybed|noped} this** — color `#c4b5fd`; bg `rgba(124,58,237,.22)`; border `1px solid rgba(167,139,250,.65)`; font-weight 800. Wildcard stays amber (`#ffb43a` family values unchanged) for contrast. The same band renders in the dossier ("info view") when the listing's slot is `return` or `wildcard` (v1.1). - No other text may sit on the card face in v3. ### 10.4 Tank gauge (match points, non-numeric) @@ -151,9 +155,10 @@ Human (parent-run after build): Glenn swipes a real deck from his phone and edit ### 10.5 Dossier — "Why this rank" - New block after chips, before "The machine": - - Gauge + "{points}/100" (17px/700). + - Gauge + "{points}%" (17px/700). - Up to 4 breakdown lines, strongest first, muted: "· {feature} {±n}"; agent contributions render as "· agent: {why} {±n}". - Footer: "from {N} strikes · read {relative}" (N = events count; time = profile version time). +- Hidden-by-default detail: the breakdown lines and the "from N strikes" footer render only when `prefs['rubric_debug'] === 'on'` (default `'off'`). With the pref off the block shows **the tank gauge + "{points}%"** only — the minimal rubric display. The toggle lives in the Trends "Actions" area (why-breakdown on ⇄ off, same control style as the forge-mode toggle) — v1.1. - This is the only numeric display in the app (accepted — it exists for audit). ### 10.6 Trends tab (real) @@ -192,14 +197,15 @@ Top→bottom: ### 11.3 Fullscreen viewer -- Tap the CENTER third → fullscreen viewer opens on the photo currently shown. +- Tap the photo in the dossier hero → fullscreen viewer opens (starts at photo 0). Direct card→viewer is retired (see §11.4); the viewer is reached from the dossier (v1.1). - Viewer: horizontal swipe = prev/next (clamp); pinch-zoom + double-tap = zoom (1×–~4×); segment bars stay visible; close via ✕ (36px, top-left) or pull-down (grabber or photo, `dy > 110` — the §3 idiom). Closing returns to the same card, same photo position; deck state untouched. - Also opens from the dossier hero (starts at photo 0). Same sheet geometry as the dossier (z-band 69/70), photo-first: no body, no actions row. +- Viewer close (✕, pull-down, or Esc) returns to the dossier; the next Esc returns to the deck (v1.1). ### 11.4 Doors (supersedes the old tap→dossier) -- ⓘ (top-right of card) → dossier (unchanged). The card face now owns: **sides = flip, center = fullscreen**; the "tap anywhere → dossier" gesture is RETIRED. -- The dossier stays reachable from ⓘ and from list rows (§3/§4 unchanged). +- ⓘ (top-right of card) → dossier (unchanged). The card face now owns: **sides = flip, center = dossier (bike data first)**; the "tap anywhere → dossier" gesture is RETIRED and the B1 center-tap→fullscreen gesture is RETIRED in favour of it (v1.1). +- Tapping the photo in the dossier hero opens the fullscreen image viewer (§11.3). Viewer close (✕, pull-down, or Esc) returns to the dossier; the next Esc returns to the deck. The dossier stays reachable from ⓘ and from list rows (§3/§4 unchanged). ### 11.5 Repeat rotation