diff --git a/docs/superpowers/plans/2026-07-03-app-wide-desktop-design.md b/docs/superpowers/plans/2026-07-03-app-wide-desktop-design.md new file mode 100644 index 0000000..fed6474 --- /dev/null +++ b/docs/superpowers/plans/2026-07-03-app-wide-desktop-design.md @@ -0,0 +1,1556 @@ +# App-wide desktop design pass Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Bring the app's theme, fonts, container/spacing system, and typography conventions up to the HTML prototype's desktop/web design across every screen, and fully redesign the listing detail page to match its prototype counterpart. + +**Architecture:** A small set of app-wide foundations (theme tokens in `global.css`, DM Sans/DM Mono fonts loaded in the root layout, a `PageContainer` max-width wrapper, a `MonoText` component, a shared micro-label class constant) get built first, then applied mechanically to every existing screen (Home, Browse, Orders, Order Detail, CheckoutSheet) without changing their structure, and finally the listing detail screen gets a full page-specific redesign built on top of those foundations. + +**Tech Stack:** Expo Router, React 19, HeroUI Native, Uniwind (Tailwind CSS v4 for React Native), `react-native-svg` (already a dependency, used for the new gradient overlays), `tailwind-merge` (already a dependency), `@expo-google-fonts/dm-sans` + `@expo-google-fonts/dm-mono` (new dependencies). + +**Testing approach:** This repo has no test runner configured (`package.json` has no Jest/Vitest, confirmed in `CLAUDE.md`) — do not add one. Every task's verification is: `npm run typecheck` (must pass with zero errors), `npm run lint` (must pass with zero errors), and a manual check in a running `expo start` session (web at a specified viewport width, and native where noted) confirming the specific visual change described in that task. Run all commands from the `app/` directory. + +**Reference spec:** `docs/superpowers/specs/2026-07-03-app-wide-desktop-design.md` — read it before starting if any task below feels underspecified; it has the full rationale (why 1320px, why the hero gradient is white/black not teal, etc). + +--- + +### Task 1: Theme tokens — `--border` and `--subtle` + +**Files:** +- Modify: `app/src/global.css` + +- [ ] **Step 1: Add the missing `--border` and new `--subtle` tokens** + +Replace the entire `@layer theme { ... }` block in `app/src/global.css` with: + +```css +@layer theme { + @variant light { + --background: #f7f6fb; + --surface: #ffffff; + --surface-secondary: #f0eef8; + --accent: #7a34d4; + --accent-foreground: #ffffff; + --success: #059669; + --warning: #d97706; + --danger: #dc2626; + --border: #e2dff0; + --subtle: #9991b8; + } + + @variant dark { + --background: #0e0e1c; + --surface: #171730; + --surface-secondary: #1e1e3c; + --accent: #b060ff; + --accent-foreground: #ffffff; + --success: #14f195; + --warning: #f5a623; + --danger: #ff4d4f; + --border: #252548; + --subtle: #605c88; + } +} + +@theme inline { + --color-subtle: var(--subtle); +} +``` + +Everything except `--border` and `--subtle` is unchanged from the current file — do not alter those existing values. + +- [ ] **Step 2: Typecheck and lint** + +Run: `cd app && npm run typecheck && npm run lint` +Expected: both pass with no errors (this is a CSS-only change, but confirms nothing else broke). + +- [ ] **Step 3: Manual check** + +Run `npx expo start --web` from `app/`, open the app in a browser. Any existing screen (e.g. Browse) should show a visibly different (correct, subtler) border color on cards than before — confirms `--border` is now applied instead of HeroUI's generic default gray. `text-subtle` isn't used anywhere yet, so there's nothing to see for that token yet — that's expected, it lands in later tasks. + +- [ ] **Step 4: Commit** + +```bash +git add app/src/global.css +git commit -m "Add border and subtle theme tokens" +``` + +--- + +### Task 2: DM Sans / DM Mono fonts + +**Files:** +- Modify: `app/package.json` (via install) +- Modify: `app/src/global.css` +- Modify: `app/src/app/_layout.tsx` + +- [ ] **Step 1: Install the font packages** + +Run: `cd app && npx expo install @expo-google-fonts/dm-sans @expo-google-fonts/dm-mono` +Expected: both packages added to `app/package.json` dependencies, install completes with no errors. + +- [ ] **Step 2: Wire the font family variables into the theme** + +Append to the end of `app/src/global.css` (after the `@theme inline { --color-subtle: ...; }` block added in Task 1): + +```css +@theme { + --font-normal: 'DMSans_400Regular'; + --font-medium: 'DMSans_500Medium'; + --font-semibold: 'DMSans_600SemiBold'; +} +``` + +- [ ] **Step 3: Load the fonts in the root layout** + +Replace the full contents of `app/src/app/_layout.tsx` with: + +```tsx +import type { JSX } from "react"; +import { Stack } from "expo-router"; +import { StatusBar } from "expo-status-bar"; +import { HeroUINativeProvider } from "heroui-native"; +import { GestureHandlerRootView } from "react-native-gesture-handler"; +import { View } from "react-native"; +import { useFonts } from "expo-font"; +import { + DMSans_400Regular, + DMSans_500Medium, + DMSans_600SemiBold, + DMSans_700Bold, +} from "@expo-google-fonts/dm-sans"; +import { DMMono_400Regular, DMMono_500Medium } from "@expo-google-fonts/dm-mono"; + +import "../global.css"; +import { WalletProvider } from "@/state/wallet"; +import { ModalProvider } from "@/state/modals"; +import { TopNav } from "@/components/TopNav"; +import { WalletConnectSheet } from "@/components/WalletConnectSheet"; +import { CheckoutSheet } from "@/components/CheckoutSheet"; + +export default function RootLayout(): JSX.Element | null { + const [fontsLoaded] = useFonts({ + DMSans_400Regular, + DMSans_500Medium, + DMSans_600SemiBold, + DMSans_700Bold, + DMMono_400Regular, + DMMono_500Medium, + }); + + if (!fontsLoaded) { + return null; + } + + return ( + + + + + + + + + + + + + + + + + + ); +} +``` + +- [ ] **Step 4: Typecheck and lint** + +Run: `cd app && npm run typecheck && npm run lint` +Expected: both pass. + +- [ ] **Step 5: Manual check** + +Run `npx expo start --web`, open the app. Body text throughout (headings, buttons, paragraphs) should visibly render in DM Sans (a geometric sans with distinct rounded terminals) instead of the previous system font. If the screen stays blank, check the terminal for a font-loading error — the `fontsLoaded` gate means the app renders nothing until fonts resolve. + +- [ ] **Step 6: Commit** + +```bash +git add app/package.json app/package-lock.json app/yarn.lock app/src/global.css app/src/app/_layout.tsx +git commit -m "Load DM Sans and DM Mono fonts" +``` +(Only add whichever lockfile actually exists/changed — this repo uses Yarn per `CLAUDE.md`, so it'll be `app/yarn.lock`.) + +--- + +### Task 3: Shared typography helpers — `MICRO_LABEL_CLASS` and `MonoText` + +**Files:** +- Create: `app/src/lib/typography.ts` +- Create: `app/src/components/MonoText.tsx` + +- [ ] **Step 1: Create the micro-label class constant** + +Create `app/src/lib/typography.ts`: + +```ts +export const MICRO_LABEL_CLASS = + "text-[11px] font-semibold uppercase tracking-wider text-subtle"; +``` + +- [ ] **Step 2: Create the `MonoText` component** + +Create `app/src/components/MonoText.tsx`: + +```tsx +import type { ComponentProps, JSX } from "react"; +import { Typography } from "heroui-native"; + +type MonoTextProps = ComponentProps; + +export function MonoText({ className, style, ...props }: MonoTextProps): JSX.Element { + return ( + + ); +} +``` + +This wraps HeroUI's `Typography type="code"` (which selects a monospace font) but strips its default chip-style background/padding, and overrides the font family to the now-loaded `DMMono_500Medium` via `style` (HeroUI's code-font selection is hardcoded in its own source and isn't theme-variable-driven, so `style` — which takes precedence over `className` — is the only way to swap it). + +- [ ] **Step 3: Typecheck and lint** + +Run: `cd app && npm run typecheck && npm run lint` +Expected: both pass. Neither file is used anywhere yet, so there's no visual check for this task — later tasks that import `MonoText`/`MICRO_LABEL_CLASS` will verify them visually. + +- [ ] **Step 4: Commit** + +```bash +git add app/src/lib/typography.ts app/src/components/MonoText.tsx +git commit -m "Add MICRO_LABEL_CLASS and MonoText typography helpers" +``` + +--- + +### Task 4: `PageContainer` component + +**Files:** +- Create: `app/src/components/PageContainer.tsx` + +- [ ] **Step 1: Create the component** + +Create `app/src/components/PageContainer.tsx`: + +```tsx +import type { JSX, ReactNode } from "react"; +import { View } from "react-native"; +import { twMerge } from "tailwind-merge"; + +interface PageContainerProps { + children: ReactNode; + className?: string; +} + +export function PageContainer({ children, className }: PageContainerProps): JSX.Element { + return ( + + {children} + + ); +} +``` + +- [ ] **Step 2: Typecheck and lint** + +Run: `cd app && npm run typecheck && npm run lint` +Expected: both pass. Not used anywhere yet — no visual check for this task. + +- [ ] **Step 3: Commit** + +```bash +git add app/src/components/PageContainer.tsx +git commit -m "Add PageContainer component" +``` + +--- + +### Task 5: `StatusChip` outline variant + +**Files:** +- Modify: `app/src/components/StatusChip.tsx` + +- [ ] **Step 1: Add the `variant` prop** + +Replace the full contents of `app/src/components/StatusChip.tsx` with: + +```tsx +import type { JSX } from "react"; +import { Chip } from "heroui-native"; +import type { StatusColor } from "@/lib/status"; + +interface StatusChipProps { + label: string; + color: StatusColor; + variant?: "soft" | "outline"; +} + +const OUTLINE_BORDER_CLASS: Record = { + default: "border border-default", + accent: "border border-accent", + success: "border border-success", + warning: "border border-warning", + danger: "border border-danger", +}; + +export function StatusChip({ label, color, variant = "soft" }: StatusChipProps): JSX.Element { + if (variant === "outline") { + return ( + + {label} + + ); + } + + return ( + + {label} + + ); +} +``` + +The lookup map uses static, fully-written-out class strings (`"border border-success"`, etc.) rather than a template literal like `` `border-${color}` `` — Uniwind/Tailwind need statically analyzable class names present in the source to include them in the build; a dynamically interpolated class name would silently fail to apply the border color at runtime. + +- [ ] **Step 2: Typecheck and lint** + +Run: `cd app && npm run typecheck && npm run lint` +Expected: both pass. `variant` defaults to `"soft"`, so every existing call site (`ListingCard`, `OrderRow`, `orders/[id].tsx`) is unaffected — no visual change yet. + +- [ ] **Step 3: Manual check** + +Run `npx expo start --web`, open Browse or Orders. Stock/status chips should look exactly as before (filled soft background) — confirms the default path is unchanged. The new `"outline"` variant gets exercised visually in Task 9 (listing detail sidebar). + +- [ ] **Step 4: Commit** + +```bash +git add app/src/components/StatusChip.tsx +git commit -m "Add outline variant to StatusChip" +``` + +--- + +### Task 6: `ResolverCard` — hover, elevated background, monospace fee + +**Files:** +- Modify: `app/src/components/ResolverCard.tsx` + +- [ ] **Step 1: Rewrite the component** + +Replace the full contents of `app/src/components/ResolverCard.tsx` with: + +```tsx +import type { JSX } from "react"; +import { useState } from "react"; +import { Pressable, View } from "react-native"; +import { Typography } from "heroui-native"; +import type { Resolver } from "@/data/mock"; +import { MonoText } from "@/components/MonoText"; + +interface ResolverCardProps { + resolver: Resolver; + isSelected: boolean; + onSelect: () => void; +} + +export function ResolverCard({ resolver, isSelected, onSelect }: ResolverCardProps): JSX.Element { + const [isHovered, setIsHovered] = useState(false); + + const borderClass = isSelected ? "border-accent" : isHovered ? "border-border-secondary" : "border-border"; + const bgClass = isSelected ? "bg-accent/10" : "bg-surface-secondary"; + + return ( + setIsHovered(true)} + onHoverOut={() => setIsHovered(false)} + className={`mb-1.5 rounded-xl border p-3 ${borderClass} ${bgClass}`} + > + + + {resolver.name} + + + {resolver.fee} + + + + {resolver.type} · {resolver.wins}% buyer-favorable + + + ); +} +``` + +Changes from the current version: unselected background is `bg-surface-secondary` instead of `bg-transparent`; `onHoverIn`/`onHoverOut` (fire only on web/mouse input, no-op on native touch — no `Platform.OS` check needed) lighten the border on hover using HeroUI's calculated `border-secondary` token; the fee percentage renders via `MonoText`. + +- [ ] **Step 2: Typecheck and lint** + +Run: `cd app && npm run typecheck && npm run lint` +Expected: both pass. + +- [ ] **Step 3: Manual check** + +Run `npx expo start --web`, open the listing detail page (`/listing/0`) — the "Dispute Protection" resolver options should now have a visibly elevated (lighter) background when unselected, the fee percentages (0.5%, 1.0%, 1.5%) should render in a monospace font, and hovering an unselected option with the mouse should lighten its border. Also open `CheckoutSheet` (tap "Buy Now" on any in-stock listing while a wallet is connected, or connect one first) to confirm the same component renders correctly there. + +- [ ] **Step 4: Commit** + +```bash +git add app/src/components/ResolverCard.tsx +git commit -m "Add hover state, elevated background, and monospace fee to ResolverCard" +``` + +--- + +### Task 7: `TopNav` — height, `PageContainer`, wider search + +**Files:** +- Modify: `app/src/components/TopNav.tsx` + +- [ ] **Step 1: Rewrite the component** + +Replace the full contents of `app/src/components/TopNav.tsx` with: + +```tsx +import type { JSX } from "react"; +import { useState } from "react"; +import { Pressable, View } from "react-native"; +import { useRouter } from "expo-router"; +import { Ionicons } from "@expo/vector-icons"; +import { Button, SearchField, Typography, useThemeColor } from "heroui-native"; +import { useWallet } from "@/state/wallet"; +import { useModals } from "@/state/modals"; +import { PageContainer } from "@/components/PageContainer"; + +export function TopNav(): JSX.Element { + const router = useRouter(); + const accentForeground = useThemeColor("accent-foreground"); + const { connected, address, disconnect } = useWallet(); + const { openWalletConnect } = useModals(); + const [search, setSearch] = useState(""); + + return ( + + + router.push("/")} className="flex-row items-center gap-2"> + + + + Volana + + + + + + + router.push("/browse")} + /> + + + + + + + + + + {connected ? ( + + ) : ( + + )} + + + ); +} +``` + +The `border-b`/`bg-background` moved to the outer full-width `View` (the nav bar itself stays edge-to-edge); `PageContainer` now wraps only the row of content inside it. + +- [ ] **Step 2: Typecheck and lint** + +Run: `cd app && npm run typecheck && npm run lint` +Expected: both pass. + +- [ ] **Step 3: Manual check** + +Run `npx expo start --web`, resize the browser window: at ≥1320px width the nav content should stop growing and stay centered with visible side margins (previously it stretched edge-to-edge); at ≥1024px the nav bar should be visibly taller (76px vs 64px) and the search field wider (480px cap vs 320px). + +- [ ] **Step 4: Commit** + +```bash +git add app/src/components/TopNav.tsx +git commit -m "Wrap TopNav in PageContainer, scale height and search width" +``` + +--- + +### Task 8: `ListingCard` — grid variant redesign, hover on both variants + +**Files:** +- Modify: `app/src/components/ListingCard.tsx` + +- [ ] **Step 1: Rewrite the component** + +Replace the full contents of `app/src/components/ListingCard.tsx` with: + +```tsx +import type { GestureResponderEvent } from "react-native"; +import type { JSX } from "react"; +import { useState } from "react"; +import { Pressable, StyleSheet, View } from "react-native"; +import { useRouter } from "expo-router"; +import { Button, Separator, Surface, Typography } from "heroui-native"; +import Svg, { Defs, RadialGradient, Rect, Stop } from "react-native-svg"; +import type { Product } from "@/data/mock"; +import { availabilityInfo } from "@/lib/status"; +import { StatusChip } from "@/components/StatusChip"; +import { MonoText } from "@/components/MonoText"; +import { useModals } from "@/state/modals"; + +interface ListingCardProps { + product: Product; + variant?: "list" | "grid"; +} + +function CardHighlightOverlay({ gradientId }: { gradientId: string }): JSX.Element { + return ( + + + + + + + + + + ); +} + +export function ListingCard({ product, variant = "grid" }: ListingCardProps): JSX.Element { + const router = useRouter(); + const { requestBuy } = useModals(); + const [isHovered, setIsHovered] = useState(false); + const availability = availabilityInfo(product.qty); + const isOut = product.qty === 0; + + const openListing = () => router.push(`/listing/${product.id}`); + + const handleBuyPress = (event: GestureResponderEvent) => { + event.stopPropagation(); + requestBuy(product.id); + }; + + if (variant === "list") { + return ( + setIsHovered(true)} + onHoverOut={() => setIsHovered(false)} + > + + + {product.emoji} + + + + + {product.priceNum} + + + {product.cur} + + + + + + {product.name} + + + {product.desc} + + + + + + + + + + ); + } + + return ( + setIsHovered(true)} + onHoverOut={() => setIsHovered(false)} + className="w-full p-1.5 md:w-1/2 lg:w-1/4" + > + + + + {product.emoji} + + + + + {product.name} + + + + + {product.seller} + + + {product.price} + + + + + + ); +} +``` + +Key changes to the grid variant: image area is now `aspect-[4/3]` (was a fixed `h-32`) with a per-card radial highlight overlay (unique gradient `id` per product, since SVG gradient ids are DOM-scoped on web and multiple cards render simultaneously); emoji is much larger (`text-6xl lg:text-7xl` vs the old flat `text-4xl`); grid width steps `w-full` → `md:w-1/2` → `lg:w-1/4` (1/2/4 columns, was `w-1/2`/`md:w-1/3`/`lg:w-1/4`, i.e. always at least 2 columns even on phones); the stock badge moved next to the title instead of next to the price; seller address uses `MonoText`. Both variants gained a permanent 1px border (`border-border`, brightening to `border-border-secondary` on hover) — `Surface` has no border by default, so this is new, and it's always present (not just added on hover) to avoid layout shift. Both variants also override `Surface`'s default `rounded-3xl` (24px) down to `rounded-xl` (12px) — the prototype's product cards use much tighter ~9-10px corners than `Surface`'s default, and 12px is the closest step on the existing radius scale, consistent with the flatter-cornered look used elsewhere (e.g. the listing detail page's CTA button). + +- [ ] **Step 2: Typecheck and lint** + +Run: `cd app && npm run typecheck && npm run lint` +Expected: both pass. + +- [ ] **Step 3: Manual check** + +Run `npx expo start --web`, open Home — the featured-listings grid should show 1 card per row on a narrow (<768px) window, 2 per row at md, and (once you're on a screen ≥1024px wide) 4 per row, each with a large centered emoji over a subtly gradient-lit background and a visible thin border that brightens on mouse hover. Open Browse in grid view for the same check, and list view to confirm the hover border there too. Also run on a native simulator (iOS or Android) to confirm the grid still renders correctly there (no SVG crashes, no `onHoverIn` issues — these are no-ops on native, which is expected). + +- [ ] **Step 4: Commit** + +```bash +git add app/src/components/ListingCard.tsx +git commit -m "Redesign ListingCard grid variant, add hover to both variants" +``` + +--- + +### Task 9: Listing detail page — full redesign + +**Files:** +- Modify: `app/src/app/listing/[id].tsx` + +- [ ] **Step 1: Rewrite the screen** + +Replace the full contents of `app/src/app/listing/[id].tsx` with: + +```tsx +import type { JSX } from "react"; +import type { ViewStyle } from "react-native"; +import { useMemo, useState } from "react"; +import { Platform, ScrollView, StyleSheet, View, useWindowDimensions } from "react-native"; +import { useLocalSearchParams } from "expo-router"; +import { Button, Chip, Separator, Typography, useThemeColor, useToast } from "heroui-native"; +import { Ionicons } from "@expo/vector-icons"; +import Svg, { Defs, LinearGradient, RadialGradient, Rect, Stop } from "react-native-svg"; +import type { Currency } from "@/data/mock"; +import { PRODUCTS, RESOLVERS } from "@/data/mock"; +import { availabilityInfo } from "@/lib/status"; +import { MICRO_LABEL_CLASS } from "@/lib/typography"; +import { AppHeader } from "@/components/AppHeader"; +import { StatusChip } from "@/components/StatusChip"; +import { ResolverCard } from "@/components/ResolverCard"; +import { ListingCard } from "@/components/ListingCard"; +import { MonoText } from "@/components/MonoText"; +import { PageContainer } from "@/components/PageContainer"; +import { useModals } from "@/state/modals"; + +const STICKY_BREAKPOINT = 1024; + +function HeroGradientOverlay({ gradientId }: { gradientId: string }): JSX.Element { + const highlightId = `${gradientId}-highlight`; + const fadeId = `${gradientId}-fade`; + return ( + + + + + + + + + + + + + + + ); +} + +export default function ListingDetailScreen(): JSX.Element { + const { id } = useLocalSearchParams<{ id: string }>(); + const { toast } = useToast(); + const { requestBuy } = useModals(); + const [resolverId, setResolverId] = useState(0); + const { width } = useWindowDimensions(); + const accentColor = useThemeColor("accent"); + + // react-native-web supports CSS `position: sticky`, but React Native's own + // ViewStyle type doesn't include it — cast is required to use it web-only. + const stickyBoxStyle: ViewStyle | undefined = + Platform.OS === "web" && width >= STICKY_BREAKPOINT + ? ({ position: "sticky", top: 92 } as ViewStyle) + : undefined; + + const listing = useMemo( + () => PRODUCTS.find((product) => product.id === Number(id)) ?? PRODUCTS[0], + [id], + ); + const [currency, setCurrency] = useState(listing.cur); + const availability = availabilityInfo(listing.qty); + const isOut = listing.qty === 0; + const relatedProducts = PRODUCTS.filter((p) => p.id !== listing.id && p.qty > 0).slice(0, 6); + + return ( + + + + + + + {listing.emoji} + + + + + {listing.name} + + + {listing.desc} + + + + + + + + + + Verified on Solana + + + {listing.seller} + + + + + + + About this listing + + + Category + + {listing.cat} + + + + Available + + {listing.qty} units + + + + Currency + + {listing.cur} + + + + Listing ID + + {`#${listing.id}${listing.id}${listing.id}${listing.id}`} + + + + + + {relatedProducts.length > 0 && ( + + Customers also bought + + {relatedProducts.map((product) => ( + + ))} + + + )} + + + + + + {listing.price} + + + ≈ {listing.usd} USD + + + + + + + + + {listing.alt.length > 0 && ( + <> + + Pay with + + setCurrency(listing.cur)} + > + {listing.cur} + + setCurrency(listing.alt[0])} + > + {listing.alt[0]} + + + + + + )} + + + Dispute Protection + {RESOLVERS.map((resolver) => ( + setResolverId(resolver.id)} + /> + ))} + + Fee only charged if dispute is adjudicated. + + + + + + + + + + ✓ Final Payment + + + 🛡 Dispute Cover + + + ⬡ On-chain + + + + + Funds held in secure escrow until you confirm receipt. + + + + + ); +} +``` + +Notes on choices that differ slightly from the spec's illustrative pseudocode: +- The hero's aspect ratio is set via Tailwind's `aspect-[8/5] lg:aspect-[12/5]` classes (1.6 and 2.4 respectively) rather than a JS-computed inline `aspectRatio` style. This is simpler and — important — avoids a cross-platform bug: the two-column layout's `lg:flex-row` breakpoint is evaluated by Uniwind on every platform (a wide native tablet can trigger it), so the hero's sizing must respond to the same CSS breakpoint, not a `Platform.OS === "web"`-gated JS value. +- `position: sticky` genuinely has no native equivalent, so `stickyBoxStyle` stays gated on both `Platform.OS === "web"` and the width check — that's the one piece that's correctly platform-specific. +- The sidebar price uses `font-extrabold` via `className` (Typography's `weight` prop only accepts `'normal' | 'medium' | 'semibold' | 'bold'`, no `extrabold`, so the Tailwind class is required to reach that weight). + +- [ ] **Step 2: Typecheck and lint** + +Run: `cd app && npm run typecheck && npm run lint` +Expected: both pass. + +- [ ] **Step 3: Manual check — desktop** + +Run `npx expo start --web`, open `/listing/0` (MacBook Pro listing, which has an alt currency and 5 in stock — exercises every conditional section) at a browser width ≥1320px. Confirm: page content is centered with margins, not edge-to-edge; hero is a wide (2.4:1) rounded rectangle with a large centered emoji and a visible soft highlight/gradient (not a flat color box); title is large (~38px) and bold; sidebar is a distinct bordered card fixed at 340px on the right, sticky when you scroll; "5 in stock" renders as an outlined pill (colored border + text, transparent background) not a filled badge; "Buy Now →" button is tall (~56px) with a less-rounded corner than other buttons in the app; fee percentages and wallet/listing-ID text render in monospace; "Pay with"/"Dispute Protection"/"About this listing" labels are small, uppercase, letter-spaced. + +- [ ] **Step 4: Manual check — mobile and native** + +Resize the browser to <768px width: layout should stay single-column (hero, then content, then sidebar stacked below, same as before this change — just check nothing looks cramped or oversized). Then run on a native simulator (iOS or Android) and open the same listing: confirm it renders without crashing (this is the first screen using `react-native-svg` gradients directly in a screen file, not just inside `ListingCard`) and that the sidebar is not sticky (expected — native has no sticky positioning) but otherwise looks correct stacked below the hero. + +- [ ] **Step 5: Commit** + +```bash +git add app/src/app/listing/[id].tsx +git commit -m "Redesign listing detail page for desktop" +``` + +--- + +### Task 10: Home — adopt `PageContainer` + +**Files:** +- Modify: `app/src/app/(tabs)/index.tsx` + +- [ ] **Step 1: Rewrite the screen** + +Replace the full contents of `app/src/app/(tabs)/index.tsx` with: + +```tsx +import type { JSX } from "react"; +import { ScrollView, View } from "react-native"; +import { useRouter } from "expo-router"; +import { Button, Typography } from "heroui-native"; +import { PRODUCTS } from "@/data/mock"; +import { ListingCard } from "@/components/ListingCard"; +import { PageContainer } from "@/components/PageContainer"; + +export default function HomeTab(): JSX.Element { + const router = useRouter(); + const featured = PRODUCTS.filter((product) => product.qty > 0).slice(0, 4); + + return ( + + + + + {"The marketplace\n"} + + nobody controls. + + + + Buy and sell freely. Payments are final — no chargebacks, no frozen accounts. + + + + + + Featured listings + + {featured.map((product) => ( + + ))} + + + + + ); +} +``` + +- [ ] **Step 2: Typecheck and lint** + +Run: `cd app && npm run typecheck && npm run lint` +Expected: both pass. + +- [ ] **Step 3: Manual check** + +Run `npx expo start --web`, open Home at ≥1320px width — content should stay centered with margins instead of stretching edge-to-edge, and the featured grid should show 4 columns. At <768px, confirm it still looks like a normal single-column mobile screen (headline, description, button, then a 1-column grid). + +- [ ] **Step 4: Commit** + +```bash +git add "app/src/app/(tabs)/index.tsx" +git commit -m "Wrap Home in PageContainer" +``` + +--- + +### Task 11: Browse — adopt `PageContainer` and micro-labels + +**Files:** +- Modify: `app/src/app/(tabs)/browse.tsx` + +- [ ] **Step 1: Rewrite the screen** + +Replace the full contents of `app/src/app/(tabs)/browse.tsx` with: + +```tsx +import type { JSX } from "react"; +import { useMemo, useState } from "react"; +import { Pressable, ScrollView, View } from "react-native"; +import { Ionicons } from "@expo/vector-icons"; +import { Chip, SearchField, Switch, Typography, useThemeColor } from "heroui-native"; +import type { Currency, Product } from "@/data/mock"; +import { PRODUCTS } from "@/data/mock"; +import { ListingCard } from "@/components/ListingCard"; +import { PageContainer } from "@/components/PageContainer"; +import { MICRO_LABEL_CLASS } from "@/lib/typography"; + +type CurrencyFilter = "all" | Currency; +type SortMode = "newest" | "priceLow" | "priceHigh"; +type ViewMode = "list" | "grid"; + +export default function BrowseTab(): JSX.Element { + const [search, setSearch] = useState(""); + const [currencyFilter, setCurrencyFilter] = useState("all"); + const [sortMode, setSortMode] = useState("newest"); + const [inStockOnly, setInStockOnly] = useState(true); + const [viewMode, setViewMode] = useState("list"); + const mutedColor = useThemeColor("muted"); + const accentForeground = useThemeColor("accent-foreground"); + + const products = useMemo(() => { + let result = [...PRODUCTS]; + if (inStockOnly) result = result.filter((product) => product.qty > 0); + if (currencyFilter !== "all") result = result.filter((product) => product.cur === currencyFilter); + if (search.trim()) { + const query = search.trim().toLowerCase(); + result = result.filter( + (product) => + product.name.toLowerCase().includes(query) || product.desc.toLowerCase().includes(query), + ); + } + if (sortMode === "priceLow") result.sort((a, b) => a.priceNum - b.priceNum); + else if (sortMode === "priceHigh") result.sort((a, b) => b.priceNum - a.priceNum); + return result; + }, [search, currencyFilter, sortMode, inStockOnly]); + + return ( + + + + + + + + + + + + + Currency + setCurrencyFilter("all")} + > + All + + setCurrencyFilter("SOL")} + > + SOL + + setCurrencyFilter("USDC")} + > + USDC + + + Sort + setSortMode("newest")} + > + Newest + + setSortMode("priceLow")} + > + Price ↑ + + setSortMode("priceHigh")} + > + Price ↓ + + + + + + + + In stock only + + + + setViewMode("list")} + className={`h-7 w-8 items-center justify-center rounded-md ${ + viewMode === "list" ? "bg-accent" : "" + }`} + > + + + setViewMode("grid")} + className={`h-7 w-8 items-center justify-center rounded-md ${ + viewMode === "grid" ? "bg-accent" : "" + }`} + > + + + + + + + + + + {viewMode === "list" ? ( + + {products.map((product) => ( + + ))} + + ) : ( + + {products.map((product) => ( + + ))} + + )} + + + + ); +} +``` + +- [ ] **Step 2: Typecheck and lint** + +Run: `cd app && npm run typecheck && npm run lint` +Expected: both pass. + +- [ ] **Step 3: Manual check** + +Run `npx expo start --web`, open Browse. "Currency" and "Sort" captions should render small, uppercase, and letter-spaced (not the previous plain sentence-case muted text). At ≥1320px width, the filter bar and listing grid/list should stay centered with margins instead of stretching edge-to-edge, matching Home's and the listing detail page's container width. Toggle list/grid view and confirm both still work. + +- [ ] **Step 4: Commit** + +```bash +git add "app/src/app/(tabs)/browse.tsx" +git commit -m "Wrap Browse in PageContainer, apply micro-label style to filter captions" +``` + +--- + +### Task 12: Orders list + `OrderRow` — `PageContainer`, monospace, hover + +**Files:** +- Modify: `app/src/app/(tabs)/orders.tsx` +- Modify: `app/src/components/OrderRow.tsx` + +- [ ] **Step 1: Rewrite the Orders list screen** + +Replace the full contents of `app/src/app/(tabs)/orders.tsx` with: + +```tsx +import type { JSX } from "react"; +import { ScrollView } from "react-native"; +import { Typography } from "heroui-native"; +import { ORDERS } from "@/data/mock"; +import { OrderRow } from "@/components/OrderRow"; +import { PageContainer } from "@/components/PageContainer"; + +export default function OrdersTab(): JSX.Element { + return ( + + + + My Orders + + {ORDERS.map((order) => ( + + ))} + + + ); +} +``` + +Note the removed `View` import (no longer needed — `PageContainer` replaces the wrapping `View`) and the removed `md:mx-auto md:w-full md:max-w-2xl` — the order list now uses the same 1320px-capped container as every other screen instead of its own narrower 672px cap. + +- [ ] **Step 2: Rewrite `OrderRow`** + +Replace the full contents of `app/src/components/OrderRow.tsx` with: + +```tsx +import type { JSX } from "react"; +import { useState } from "react"; +import { Pressable, View } from "react-native"; +import { useRouter } from "expo-router"; +import { Typography } from "heroui-native"; +import type { Order } from "@/data/mock"; +import { orderStatusInfo } from "@/lib/status"; +import { StatusChip } from "@/components/StatusChip"; +import { MonoText } from "@/components/MonoText"; + +interface OrderRowProps { + order: Order; +} + +export function OrderRow({ order }: OrderRowProps): JSX.Element { + const router = useRouter(); + const [isHovered, setIsHovered] = useState(false); + const status = orderStatusInfo(order.status); + + return ( + router.push(`/orders/${order.id}`)} + onHoverIn={() => setIsHovered(true)} + onHoverOut={() => setIsHovered(false)} + className={`flex-row items-center gap-3.5 rounded-xl border bg-surface p-3.5 ${ + isHovered ? "border-border-secondary" : "border-border" + }`} + > + {order.emoji} + + + {order.name} + + + {order.seller} + + + + + {order.amt} + + + {order.date} + + + + + ); +} +``` + +- [ ] **Step 3: Typecheck and lint** + +Run: `cd app && npm run typecheck && npm run lint` +Expected: both pass. + +- [ ] **Step 4: Manual check** + +Run `npx expo start --web`, open Orders. Seller addresses should render in monospace; hovering a row with the mouse should brighten its border; the page content should be capped/centered at the same 1320px width as other screens (wider than before, since the old 672px cap is gone). + +- [ ] **Step 5: Commit** + +```bash +git add "app/src/app/(tabs)/orders.tsx" app/src/components/OrderRow.tsx +git commit -m "Wrap Orders list in PageContainer, add monospace and hover to OrderRow" +``` + +--- + +### Task 13: Order detail — `PageContainer`, monospace, micro-label + +**Files:** +- Modify: `app/src/app/orders/[id].tsx` + +- [ ] **Step 1: Rewrite the screen** + +Replace the full contents of `app/src/app/orders/[id].tsx` with: + +```tsx +import type { JSX } from "react"; +import { useMemo, useState } from "react"; +import { ScrollView, View } from "react-native"; +import { useLocalSearchParams, useRouter } from "expo-router"; +import { Button, Typography, useToast } from "heroui-native"; +import { ORDERS } from "@/data/mock"; +import { orderStatusInfo } from "@/lib/status"; +import { AppHeader } from "@/components/AppHeader"; +import { StatusChip } from "@/components/StatusChip"; +import { MonoText } from "@/components/MonoText"; +import { PageContainer } from "@/components/PageContainer"; +import { MICRO_LABEL_CLASS } from "@/lib/typography"; + +const STEP_LABELS = ["Payment\nHeld", "Seller\nConfirms", "You Confirm\nReceipt"] as const; + +export default function OrderDetailScreen(): JSX.Element { + const { id } = useLocalSearchParams<{ id: string }>(); + const router = useRouter(); + const { toast } = useToast(); + + const order = useMemo(() => ORDERS.find((item) => item.id === Number(id)) ?? ORDERS[0], [id]); + const [status, setStatus] = useState(order.status); + const statusInfo = orderStatusInfo(status); + + const step2Done = status !== "AwaitingConfirm"; + const step3Done = status === "Complete"; + const activeStepIndex = step3Done ? 2 : step2Done ? 1 : 0; + + const canCancel = status === "AwaitingConfirm"; + const canConfirm = status === "Active"; + + const handleCancel = () => { + setStatus("Cancelled"); + toast.show({ variant: "success", label: "Order cancelled. Full refund initiated." }); + }; + + const handleConfirmReceipt = () => { + setStatus("Complete"); + toast.show({ variant: "success", label: "Receipt confirmed! Seller has been paid." }); + }; + + return ( + + + + + + + Order placed {order.date} + + {order.name} + + {order.seller} + + + + {order.amt} + + + + + + Order Progress + + {STEP_LABELS.map((label, index) => { + const done = index < activeStepIndex || (index === activeStepIndex && step3Done); + const active = index === activeStepIndex && !step3Done; + return ( + + + + {done ? "✓" : String(index + 1)} + + + + {label} + + + ); + })} + + + + + + + {order.amt} + + {" is locked in a secure on-chain escrow. Funds are released to the seller only after you "} + confirm receipt — no third party can touch them. + + + + + {canCancel && ( + + )} + {canConfirm && ( + + )} + + + + + ); +} +``` + +- [ ] **Step 2: Typecheck and lint** + +Run: `cd app && npm run typecheck && npm run lint` +Expected: both pass. + +- [ ] **Step 3: Manual check** + +Run `npx expo start --web`, open an order detail page from Orders. Seller address should render in monospace; "Order Progress" caption should be small/uppercase/letter-spaced; content should be capped/centered at 1320px on wide viewports instead of the old narrower 672px cap. Click through the Cancel/Confirm buttons (where applicable to the order's status) to confirm the status-stepper interaction still works. + +- [ ] **Step 4: Commit** + +```bash +git add "app/src/app/orders/[id].tsx" +git commit -m "Wrap Order Detail in PageContainer, add monospace and micro-label" +``` + +--- + +### Task 14: `CheckoutSheet` — monospace seller address + +**Files:** +- Modify: `app/src/components/CheckoutSheet.tsx` + +- [ ] **Step 1: Import `MonoText` and swap the seller address** + +In `app/src/components/CheckoutSheet.tsx`, add the import alongside the other `@/components` imports: + +```tsx +import { ResolverCard } from "@/components/ResolverCard"; +import { MonoText } from "@/components/MonoText"; +``` + +Then replace this block (inside the `checkoutStep === "review"` branch): + +```tsx + + {listing.seller} + +``` + +with: + +```tsx + + {listing.seller} + +``` + +- [ ] **Step 2: Typecheck and lint** + +Run: `cd app && npm run typecheck && npm run lint` +Expected: both pass. + +- [ ] **Step 3: Manual check** + +Run `npx expo start --web`. Connect a wallet, tap "Buy Now" on an in-stock listing to open the checkout sheet, and confirm the seller address in the order summary now renders in monospace. + +- [ ] **Step 4: Commit** + +```bash +git add app/src/components/CheckoutSheet.tsx +git commit -m "Render seller address in monospace in CheckoutSheet" +``` + +--- + +### Task 15: Final full-app verification pass + +**Files:** none (verification only) + +- [ ] **Step 1: Full typecheck and lint** + +Run: `cd app && npm run typecheck && npm run lint && npm run format:check` +Expected: all three pass with zero errors. If `format:check` fails, run `npm run format` and re-check the diff is only whitespace/formatting before committing it separately. + +- [ ] **Step 2: Web pass — every screen, three widths** + +Run `npx expo start --web`. At each of three browser widths — **375px** (phone), **900px** (tablet), **1400px** (desktop, wider than the 1320px cap so you can see the cap take effect) — visit and check for layout breakage (overlapping text, content escaping its container, oversized/tiny text) on: Home, Browse (both list and grid view), Orders list, an Order Detail page, and a Listing Detail page (use one with an alt currency and in-stock, e.g. `/listing/0`, and one out-of-stock, e.g. `/listing/5`, to check the disabled "Out of Stock" button state). + +- [ ] **Step 3: Native pass** + +Run `npm run ios` or `npm run android` (whichever simulator/emulator is available). Navigate through Home → Browse → a listing detail page → back → Orders → an order detail page → Checkout flow (connect wallet, buy, confirm). Confirm: no crashes (particularly around the new `react-native-svg` gradients and `onHoverIn`/`onHoverOut` handlers, both of which are new on native even though they're inert there), fonts render as DM Sans/DM Mono (not a fallback system font — if fonts are missing, text will look like the platform default), and nothing web-only (the sticky sidebar, hover borders) leaked into a broken native-only state. + +- [ ] **Step 4: Fix anything found, or confirm clean** + +If Steps 2 or 3 surface an issue, fix it in the relevant file from the task above that owns it, re-run that task's typecheck/lint, and commit the fix with a message describing what was broken (not "fix bug" — name the actual problem, e.g. "Fix hero gradient overflowing rounded corners on Android"). If nothing is found, no commit is needed for this task — it's verification-only. + +--- + +## Self-review notes + +- **Spec coverage:** every numbered section of the spec (theme, fonts, `PageContainer`, app-wide rollout, listing detail layout/hero/typography/sidebar/verified/about, `StatusChip`, `ResolverCard`, `TopNav`, `ListingCard`, cross-cutting hover/spacing) maps to a task above. The spec's "Testing" section maps to Task 15 plus the per-task manual-check steps. +- **No test runner:** every task's verification step uses `npm run typecheck`/`npm run lint`/manual checks instead of a test framework, per `CLAUDE.md` — no task assumes Jest/Vitest/pytest exist. +- **Type/name consistency check:** `MICRO_LABEL_CLASS` (from `@/lib/typography`), `MonoText` (from `@/components/MonoText`), `PageContainer` (from `@/components/PageContainer`), `StatusChip`'s `variant` prop, and `StatusColor` (from `@/lib/status`) are named identically everywhere they're imported/used across Tasks 3–14 — verified by re-reading each usage site against its defining task.