Files
volana/docs/superpowers/specs/2026-07-03-app-wide-desktop-design.md

18 KiB

Desktop/web design pass — global foundations + listing detail page

Problem

The app looks noticeably worse on web/desktop than the HTML design prototype (prototype/Volana.dc.html): content runs edge-to-edge with no max-width, typography doesn't scale up for desktop, secondary text has no visual hierarchy, addresses/fees aren't distinguished from regular text, and there's no shared, theme-token-driven system for any of this — every screen does its own ad-hoc centering and spacing.

The listing detail screen (app/src/app/listing/[id].tsx) is the worst offender (hero is an oversized flat-color box with a tiny emoji, sidebar isn't visually a distinct card) and gets a full page-specific redesign here. But the underlying problem — no shared container, no responsive typography/spacing scale, no consistent treatment of muted/subtle text and monospace data — is app-wide, so this spec has two layers:

  1. Foundations (theme, fonts, PageContainer, typography/spacing conventions, hover pattern): built once, applied to every screen.
  2. Listing detail page: the one screen that also gets a full page-specific redesign (hero, purchase sidebar, verified/about sections) to close the gap with its prototype counterpart.

Other screens (Home, Browse, Orders, Order Detail) get layer 1 applied to their existing layout and content — this spec does not redesign their structure or add new sections to them, only replaces ad-hoc containers/spacing/text-color choices with the shared foundations.

Scope

Foundations (app-wide):

  • app/src/global.css (theme tokens)
  • app/src/app/_layout.tsx (font loading)
  • A new app/src/components/PageContainer.tsx
  • app/src/components/StatusChip.tsx, app/src/components/ResolverCard.tsx, app/src/components/TopNav.tsx, app/src/components/ListingCard.tsx (shared components, used across screens)
  • app/src/app/(tabs)/index.tsx (Home) — wrap in PageContainer, adopt the spacing scale
  • app/src/app/(tabs)/browse.tsx (Browse) — wrap in PageContainer, filter captions ("Currency", "Sort") adopt the micro-label convention
  • app/src/app/(tabs)/orders.tsx (Orders list) — wrap in PageContainer (replacing its current md:mx-auto md:w-full md:max-w-2xl ad-hoc centering)
  • app/src/app/orders/[id].tsx (Order detail) — wrap in PageContainer (same ad-hoc centering to replace), "Order Progress" caption adopts the micro-label convention
  • app/src/components/OrderRow.tsx — seller address adopts the monospace convention
  • app/src/components/CheckoutSheet.tsxlisting.seller adopts the monospace convention (no PageContainer; it's a bottom sheet, not a page)

Listing detail page (full redesign): app/src/app/listing/[id].tsx, covered in detail below.

Out of scope: on-chain/wallet logic, redesigning the content/structure of Home/Browse/Orders/Order Detail (their current sections, copy, and interactions stay as they are — only containers/spacing/text treatment change), WalletConnectSheet (no addresses or long text needing the new conventions), the mobile/desktop nav breakpoint (768px) itself, nav blur/translucency.

The prototype (prototype/Volana.dc.html) is the visual source of truth; it has zero responsive/mobile CSS — it's a desktop-only mockup, no @media queries touch layout anywhere in it. All mobile-specific sizing in this spec (hero aspect ratio, typography minimums, the two-column breakpoint) is therefore this spec's own design judgment, not something derived from the prototype.

Theme (app/src/global.css)

Theme colors are sourced from the prototype's own CSS variables (--c-bg, --c-surf, --c-surf2, --c-a, --c-g, --c-y, --c-r, defined in prototype/Volana.dc.html's <style> block). app/src/global.css's existing --background, --surface, --surface-secondary, --accent, --success, --warning, --danger already match these values for both light and dark themes — leave them as they are. Extend the existing @layer theme blocks with what's missing:

  • Add --border overrides (currently unset, silently falling back to HeroUI's generic default gray instead of the prototype's --c-bd 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)
  • (tabs)/index.tsx, (tabs)/browse.tsx, (tabs)/orders.tsx, and orders/[id].tsx — see "App-wide rollout" below

App-wide rollout

The foundational pieces above (theme tokens, fonts, PageContainer, the typography/spacing conventions defined per-element in the Listing detail section below, the onHoverIn/onHoverOut web-hover pattern) apply to every screen once built — theme tokens and fonts are global by construction (global.css / root-layout font loading), and shared components (StatusChip, ResolverCard, ListingCard, TopNav) pick up the changes automatically wherever they're used. The remaining screens need explicit, mechanical updates to adopt the rest:

  • Home ((tabs)/index.tsx): replace the manual px-5 / md:items-center centering with PageContainer. Content and copy are unchanged.
  • Browse ((tabs)/browse.tsx): wrap the filter bar and list/grid in PageContainer. The "Currency" and "Sort" filter captions adopt the micro-label convention (text-[11px] font-semibold uppercase tracking-wider text-subtle) in place of their current type="body-xs" color="muted". ListingCard's grid-variant changes apply automatically. Add the onHoverIn/onHoverOut hover pattern to ListingCard's list variant too (currently only specified for the grid variant).
  • Orders list ((tabs)/orders.tsx): replace md:mx-auto md:w-full md:max-w-2xl with PageContainer. OrderRow's order.seller address adopts the monospace convention (Typography type="code" stripped of its background, DMMono_500Medium via style, same as the listing detail page's addresses). Add the hover pattern to OrderRow (it's a pressable row, currently no hover feedback).
  • Order detail (orders/[id].tsx): replace the same md:mx-auto md:w-full md:max-w-2xl pattern with PageContainer. order.seller adopts the monospace convention. The "Order Progress" caption adopts the micro-label convention.
  • CheckoutSheet: listing.seller adopts the monospace convention. No PageContainer — it's a bottom sheet, not a full-width page, so there's no max-width to enforce.

None of these screens get new sections, restructured layout, or copy changes — only their container, spacing, and text-treatment building blocks change to the shared ones.

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 on top of this navbar's core requirements (height, border, logo, search, links, pill, connect button), has no native equivalent, and is 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: gap-6 lg:gap-10 in place of flat, non-responsive gaps — applied on the listing detail screen and, where a screen has analogous major-section breaks, on Home/Browse/Orders/Order Detail too (their existing gap values step up responsively rather than changing which sections exist).
  • Web-only hover states (resolver cards, related-product cards in both ListingCard variants, OrderRow) via the onHoverIn/onHoverOut local-state pattern described above — applied consistently wherever this spec calls for hover feedback, on every screen that uses these shared components.
  • 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 — on the listing detail page and on every other screen touched by the app-wide rollout.

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. The manual pass covers every screen touched by this spec — Home, Browse, Orders list, Order Detail, and Listing Detail — not just the primary target.