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:
- Foundations (theme, fonts,
PageContainer, typography/spacing conventions, hover pattern): built once, applied to every screen. - 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 inPageContainer, adopt the spacing scaleapp/src/app/(tabs)/browse.tsx(Browse) — wrap inPageContainer, filter captions ("Currency", "Sort") adopt the micro-label conventionapp/src/app/(tabs)/orders.tsx(Orders list) — wrap inPageContainer(replacing its currentmd:mx-auto md:w-full md:max-w-2xlad-hoc centering)app/src/app/orders/[id].tsx(Order detail) — wrap inPageContainer(same ad-hoc centering to replace), "Order Progress" caption adopts the micro-label conventionapp/src/components/OrderRow.tsx— seller address adopts the monospace conventionapp/src/components/CheckoutSheet.tsx—listing.selleradopts the monospace convention (noPageContainer; 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
--borderoverrides (currently unset, silently falling back to HeroUI's generic default gray instead of the prototype's--c-bdborder color):- dark:
--border: #252548 - light:
--border: #e2dff0
- dark:
- Add a second muted tier. The prototype uses two distinct de-emphasis levels —
--c-t2for body/description text (already mapped to HeroUI's--muted) and a dimmer--c-t3for 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:Usable as@layer theme { @variant dark { --subtle: #605c88; } @variant light { --subtle: #9991b8; } } @theme inline { --color-subtle: var(--subtle); }className="text-subtle"(Typography'scolorprop only acceptsdefault|muted, so subtle text is applied viaclassName, not thecolorprop). - No radius changes. Base
--radiusis already8px→rounded-xl(12px) androunded-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 defaultrounded-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-sansand@expo-google-fonts/dm-mono(both exist on npm, confirmed). - Load
DMSans_400Regular,DMSans_500Medium,DMSans_600SemiBold,DMSans_700Bold,DMMono_400Regular,DMMono_500MediumviauseFontsinapp/src/app/_layout.tsx, following the existing app-level loading-gate pattern (render nothing / keep splash screen untilfontsLoaded). - 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 explicitstyle={{ fontFamily: 'DMMono_500Medium' }}override — HeroUI'stype="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 viastyle(which takes precedence overclassNameper 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 itsborder-b; only the row of logo/search/links/wallet-button inside it is constrained)- The entire
ListingDetailScreenbody (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, andorders/[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 manualpx-5/md:items-centercentering withPageContainer. Content and copy are unchanged. - Browse (
(tabs)/browse.tsx): wrap the filter bar and list/grid inPageContainer. The "Currency" and "Sort" filter captions adopt the micro-label convention (text-[11px] font-semibold uppercase tracking-wider text-subtle) in place of their currenttype="body-xs" color="muted".ListingCard's grid-variant changes apply automatically. Add theonHoverIn/onHoverOuthover pattern toListingCard's list variant too (currently only specified for the grid variant). - Orders list (
(tabs)/orders.tsx): replacemd:mx-auto md:w-full md:max-w-2xlwithPageContainer.OrderRow'sorder.selleraddress adopts the monospace convention (Typography type="code"stripped of its background,DMMono_500Mediumviastyle, same as the listing detail page's addresses). Add the hover pattern toOrderRow(it's a pressable row, currently no hover feedback). - Order detail (
orders/[id].tsx): replace the samemd:mx-auto md:w-full md:max-w-2xlpattern withPageContainer.order.selleradopts the monospace convention. The "Order Progress" caption adopts the micro-label convention. CheckoutSheet:listing.selleradopts the monospace convention. NoPageContainer— 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'saspectRatiostyle works cross-platform, no separate mobile/desktop height branches needed) —isWidereuses the same width check as the sticky sidebar.rounded-2xl overflow-hidden, background color fromlisting.bg(unchanged, per-listing data).- Layered on top via
react-native-svg(already a project dependency, no new install needed): aRadialGradientwhite-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].tsxand 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 newvariant="outline"(see below). - CTA:
<Button size="lg">, full width,variant="primary"(unchanged) — verify at implementation time that HeroUI'slgbutton size lands in the ~52-56px height range; adjust withclassName="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">(rendersbg-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-transparenttobg-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/onHoverOutstate (these Pressable events only fire on web/mouse input and are no-ops on native touch — noPlatform.OSbranching required). Hover state lightens the border slightly using HeroUI's existing calculatedborder-secondary/border-tertiarytokens, not a new custom color.
TopNav (app/src/components/TopNav.tsx)
- Height:
h-16 lg:h-[76px](was flath-16). - Content wrapped in
PageContainer(replacing the current manualpx-6). - Search field width bumped:
lg:w-[480px](was capped atmax-w-[320px]with no larger breakpoint). - No other structural changes. The
md:flexvisibility breakpoint (mobile tab bar vs. desktop nav swap) is unchanged — out of scope for this spec. No blur/translucency backdrop — the prototype'sbackdrop-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 fixedh-32), same white-highlight radial-overlay technique as the hero (smaller), emojitext-6xl lg:text-7xl(was flattext-4xl). - Grid gap bumped:
gap-5 lg:gap-6(was tighter implicit padding viap-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'slgCTA; no variant change needed.
Cross-cutting
- Vertical spacing between major sections:
gap-6 lg:gap-10in 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
ListingCardvariants,OrderRow) via theonHoverIn/onHoverOutlocal-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) tolg(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 beforelg— 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.