Add listing detail desktop design spec

This commit is contained in:
thesn10
2026-07-03 12:18:07 +02:00
parent fb53341be1
commit 8d4bbaea0a

View File

@@ -0,0 +1,279 @@
# Listing detail page — desktop/web design pass
## Problem
The listing detail screen (`app/src/app/listing/[id].tsx`) looks noticeably worse on
web/desktop than the HTML design prototype (`prototype/Volana.dc.html`, "LISTING
DETAIL" section, lines 423-543): content runs edge-to-edge with no max-width, the
hero is an oversized flat-color box with a tiny emoji, typography doesn't scale up
for desktop, the sidebar isn't visually a distinct card, and several shared
components it depends on (`TopNav`, `ListingCard`) have the same issues since they're
reused across other screens.
A prior request included a written analysis of the gap with concrete numbers
(container width, font sizes, colors). That analysis was produced without access to
the prototype's source and got several concrete details wrong (see "Corrections"
below) — the prototype HTML/CSS and the current app code are the source of truth for
this spec, not that analysis.
## Scope
Primary target: `app/src/app/listing/[id].tsx`. Because two of the fixes (navbar,
"customers also bought" cards) live in shared components also used by
`(tabs)/index.tsx`, `(tabs)/browse.tsx`, and `(tabs)/orders.tsx`, this spec also
covers:
- `app/src/components/TopNav.tsx`
- `app/src/components/ListingCard.tsx`
- `app/src/components/StatusChip.tsx`
- `app/src/components/ResolverCard.tsx`
- A new `app/src/components/PageContainer.tsx`
- `app/src/global.css` (theme)
- `app/src/app/_layout.tsx` (font loading)
Out of scope: on-chain/wallet logic, `(tabs)/index.tsx` and `(tabs)/browse.tsx`
layout beyond what `ListingCard` changes give them for free, `WalletConnectSheet`,
`CheckoutSheet`, the mobile/desktop nav breakpoint (768px) itself, nav
blur/translucency.
## Corrections vs. the original analysis
Verified against `prototype/Volana.dc.html` and the actual HeroUI Native
docs/source in `.heroui-docs/` and `node_modules/heroui-native/src`:
- **Max-width is 1320px, not 1440px.** The prototype uses `max-width:1320px`
consistently for the nav, hero, and listing-detail wrapper.
- **The app's theme colors already match the prototype.** `app/src/global.css`'s
`--background`, `--surface`, `--surface-secondary`, `--accent`, `--success`,
`--warning`, `--danger` were already copied from the prototype's own
`--c-bg`/`--c-surf`/`--c-a`/etc. CSS variables. The hex codes given in the
original analysis prompt do not match the prototype and must not be used.
- **The hero background is not teal.** The prototype layers a
`rgba(255,255,255,.08)` radial highlight (top-left) and a black-to-transparent
linear fade at the bottom over the listing's own per-product `bg` color
(`mock.ts`), not a fixed teal color.
- **Sidebar width is a fixed 340px, not 380-420px.**
- The prototype has **zero responsive/mobile CSS** — it's a desktop-only mockup
(confirmed: no `@media` queries touch layout). All mobile-specific sizing
(hero aspect ratio, typography minimums, breakpoint choice) is this spec's own
design judgment, not derived from the prototype.
## Theme (`app/src/global.css`)
Extend the existing `@layer theme` blocks (do not replace the existing
background/surface/accent/success/warning/danger values — they're already correct):
- Add `--border` overrides (currently unset, silently falling back to HeroUI's
generic default gray instead of the prototype's border color):
- dark: `--border: #252548`
- light: `--border: #e2dff0`
- Add a second muted tier. The prototype uses two distinct de-emphasis levels —
`--c-t2` for body/description text (already mapped to HeroUI's `--muted`) and a
dimmer `--c-t3` for micro-labels, captions, wallet/listing IDs, fee fine-print
(currently has no equivalent). Add a new semantic color following HeroUI's
documented "Adding Custom Colors" pattern:
```css
@layer theme {
@variant dark { --subtle: #605c88; }
@variant light { --subtle: #9991b8; }
}
@theme inline {
--color-subtle: var(--subtle);
}
```
Usable as `className="text-subtle"` (Typography's `color` prop only accepts
`default|muted`, so subtle text is applied via `className`, not the `color` prop).
- No radius changes. Base `--radius` is already `8px` → `rounded-xl` (12px) and
`rounded-2xl` (16px) already match the spec's button/card radius targets via
existing Tailwind utility classes; just use those classes consistently instead of
`<Surface>`'s default `rounded-3xl` (24px).
## Fonts
The prototype loads DM Sans (body) and DM Mono (addresses, fees, IDs) from Google
Fonts; the app currently uses HeroUI's default system font throughout.
- Add `@expo-google-fonts/dm-sans` and `@expo-google-fonts/dm-mono` (both exist on
npm, confirmed).
- Load `DMSans_400Regular`, `DMSans_500Medium`, `DMSans_600SemiBold`,
`DMSans_700Bold`, `DMMono_400Regular`, `DMMono_500Medium` via `useFonts` in
`app/src/app/_layout.tsx`, following the existing app-level loading-gate pattern
(render nothing / keep splash screen until `fontsLoaded`).
- Override HeroUI's font CSS variables per its documented mechanism:
```css
@theme {
--font-normal: 'DMSans_400Regular';
--font-medium: 'DMSans_500Medium';
--font-semibold: 'DMSans_600SemiBold';
}
```
- Monospace elements (resolver fee %, wallet addresses, listing IDs) use
`<Typography type="code" className="bg-transparent px-0 py-0 self-auto">`
(HeroUI's monospace preset, stripped of its default chip-style
background/padding) with an explicit `style={{ fontFamily: 'DMMono_500Medium' }}`
override — HeroUI's `type="code"` font selection is hardcoded to generic
`'monospace'`/`'Menlo'` in its source (`text.constants.ts`) and isn't
theme-variable-driven, so the loaded DM Mono font must be applied directly via
`style` (which takes precedence over `className` per this project's styling
conventions).
## `PageContainer` (new component)
`app/src/components/PageContainer.tsx`:
```tsx
interface PageContainerProps {
children: ReactNode;
className?: string;
}
export function PageContainer({ children, className }: PageContainerProps) {
return (
<View className={cn("w-full self-center px-4 md:px-8 lg:max-w-[1320px] lg:px-12", className)}>
{children}
</View>
);
}
```
(Exact class-merge helper — `cn`/`twMerge`/plain template string — decided at
implementation time to match whatever convention the rest of the codebase uses.)
Used to wrap:
- `TopNav`'s inner content (the nav bar itself stays edge-to-edge with its
`border-b`; only the row of logo/search/links/wallet-button inside it is
constrained)
- The entire `ListingDetailScreen` body (hero, two-column detail layout, and
"Customers also bought" section all inside one container, mirroring the
prototype's single wrapping div)
## Listing detail layout (`app/src/app/listing/[id].tsx`)
**Two-column split:** change the breakpoint from `md:flex-row` (768px) to
`lg:flex-row` (1024px) so there's room for the 340px sidebar before switching to
two columns. Main content column `flex-1`, sidebar fixed `lg:w-[340px]`. Below
`lg`: single column, sidebar stacks under the hero (today's existing behavior,
just at a higher breakpoint).
**Sticky sidebar:** keep the existing web-only `position: sticky` mechanism, but
gate it behind a `useWindowDimensions` width check (same pattern
`app/src/app/(tabs)/_layout.tsx` already uses for its own breakpoint) so it only
activates at `>=1024`. Update `top` offset to match the new desktop nav height
(~76px) plus spacing (~92px total).
**Hero:**
- `style={{ aspectRatio: isWide ? 2.4 : 1.6 }}` (RN's `aspectRatio` style works
cross-platform, no separate mobile/desktop height branches needed) — `isWide`
reuses the same width check as the sticky sidebar.
- `rounded-2xl overflow-hidden`, background color from `listing.bg` (unchanged,
per-listing data).
- Layered on top via `react-native-svg` (already a project dependency, no new
install needed): a `RadialGradient` white-8%-to-transparent highlight
(top-left-biased) and a bottom linear fade to black-transparent — replicating the
prototype's actual two-layer gradient technique exactly.
- Emoji: `text-[96px] lg:text-[160px]`, centered.
**Typography scale** (Tailwind arbitrary values layered on HeroUI's `type` presets,
which supply weight/line-height defaults; size is overridden responsively):
- Title: `text-2xl lg:text-[38px] leading-tight font-bold`
- Description: `text-[15px] lg:text-[17px]`, `color="muted"`
- Sidebar price: `text-[30px] lg:text-[42px] font-extrabold`
- USD subline: `text-[15px] lg:text-[18px] text-subtle`
- Section micro-labels ("Pay with", "Dispute Protection", "About this listing"):
`text-[11px] font-semibold uppercase tracking-wider text-subtle` — defined once
as a local class-string constant in `[id].tsx` and reused at its ~4 call sites
(not a new shared component; not reused outside this screen yet)
**Purchase sidebar card:**
- `rounded-2xl border border-border bg-surface p-6 lg:p-8`, existing `<Separator />`
usage between price / pay-with / dispute-protection / CTA blocks, re-padded to the
new scale.
- Stock badge uses `StatusChip`'s new `variant="outline"` (see below).
- CTA: `<Button size="lg">`, full width, `variant="primary"` (unchanged) — verify at
implementation time that HeroUI's `lg` button size lands in the ~52-56px height
range; adjust with `className="h-14"` only if the built-in size doesn't already
hit it.
- Trust chips ("Final Payment", "Dispute Cover", "On-chain"):
`<Chip size="sm" variant="secondary">` (renders `bg-default`) as the closest
built-in equivalent to a small elevated pill.
**Verified box:** add an icon in a violet circle (`bg-accent-soft` circle,
`text-accent` `Ionicons` checkmark) to the left of the existing two-line
"Verified on Solana" / address text block; "Copy" button stays right-aligned;
address rendered with the new monospace treatment.
**About this listing:** keep the existing 2-column grid structure; apply the new
micro-label style to the small "Category"/"Available"/"Currency"/"Listing ID"
captions; bump row-gap to match the new spacing scale; Listing ID value gets the
monospace treatment.
## `StatusChip` (`app/src/components/StatusChip.tsx`)
Add an optional `variant?: "soft" | "outline"` prop, default `"soft"` (existing
behavior, unchanged call sites in `ListingCard`/`OrderRow` keep working as-is).
New `"outline"` value renders `<Chip variant="tertiary" color={color}>` plus an
explicit border className looked up from a static `Record<StatusColor, string>`
map (e.g. `{ success: "border border-success", warning: "border border-warning",
default: "border border-default", accent: "border border-accent", danger: "border
border-danger" }`) — dynamic template-literal class names
(`` `border-${color}` ``) must not be used since Uniwind/Tailwind need statically
analyzable class strings to include them in the build. Transparent background,
colored border + text, built entirely from existing Chip primitives and theme
color tokens (no new component, no hardcoded colors). Used for the sidebar's
"5 in stock" badge only.
## `ResolverCard` (`app/src/components/ResolverCard.tsx`)
- Unselected background changes from `bg-transparent` to `bg-surface-secondary`
(was: plain transparent; prototype implies an elevated unselected state).
Selected state (`border-accent bg-accent/10`) is unchanged.
- Add web-only hover feedback via local `onHoverIn`/`onHoverOut` state (these
Pressable events only fire on web/mouse input and are no-ops on native touch —
no `Platform.OS` branching required). Hover state lightens the border slightly
using HeroUI's existing calculated `border-secondary`/`border-tertiary` tokens,
not a new custom color.
## `TopNav` (`app/src/components/TopNav.tsx`)
- Height: `h-16 lg:h-[76px]` (was flat `h-16`).
- Content wrapped in `PageContainer` (replacing the current manual `px-6`).
- Search field width bumped: `lg:w-[480px]` (was capped at `max-w-[320px]` with no
larger breakpoint).
- No other structural changes. The `md:flex` visibility breakpoint (mobile tab bar
vs. desktop nav swap) is unchanged — out of scope for this spec. No
blur/translucency backdrop — the prototype's `backdrop-filter: blur(20px)` is a
nice-to-have not required by the original ask's navbar requirements (height,
border, logo, search, links, pill, connect button) and has no native
equivalent, so it's deliberately excluded to keep this scoped.
## `ListingCard` grid variant (`app/src/components/ListingCard.tsx`)
Affects "Customers also bought" on the listing detail page, and (as a side effect
of this being a shared component) the featured-listings grid on the home tab and
the grid view on the browse tab:
- Image area: `aspect-[4/3]` (was fixed `h-32`), same white-highlight radial-overlay
technique as the hero (smaller), emoji `text-6xl lg:text-7xl` (was flat `text-4xl`).
- Grid gap bumped: `gap-5 lg:gap-6` (was tighter implicit padding via `p-1.5`).
- Title semibold (unchanged), address in the new monospace/subtle treatment, price
in `text-accent font-bold` (unchanged), stock badge small, positioned top-right
of the card body (was inline with price).
- Buy Now button stays `size="sm"` — already more subdued than the sidebar's `lg`
CTA; no variant change needed.
## Cross-cutting
- Vertical spacing between major sections on the listing detail screen:
`gap-6 lg:gap-10` (was flat `gap-5`/`gap-6` with no responsive scaling).
- Web-only hover states (resolver cards, related-product cards) via the
`onHoverIn`/`onHoverOut` local-state pattern described above — applied
consistently wherever this spec calls for hover feedback.
- Mobile regression check: since the two-column breakpoint moves from `md` (768px)
to `lg` (1024px), mobile and tablet-portrait layouts stay single-column longer
than today, which is a safe direction (less likely to cramp a mid-size viewport),
not a regression risk in itself. Verify at implementation time on a phone-width
viewport that spacing/typography changes don't look oversized before `lg`.
## Testing
No test runner is configured in this repo (per `CLAUDE.md`). Verification is:
`npm run typecheck`, `npm run lint`, and manual check in a running `expo start`
web session at mobile (<768px), tablet (~900px), and desktop (>=1320px) widths,
plus a native (iOS or Android simulator) pass to confirm nothing web-only leaked
into native rendering.