Files
volana/docs/superpowers/specs/2026-07-03-listing-detail-desktop-design.md
2026-07-03 12:18:07 +02:00

14 KiB

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:
    @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 8pxrounded-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:
    @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:

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.