Add listing detail desktop design spec
This commit is contained in:
@@ -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.
|
||||
Reference in New Issue
Block a user