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

340 lines
18 KiB
Markdown

# 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.tsx``listing.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:
```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)
- `(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.