From 9080e6fbc63d3f59032ac73e8d7e96e25b2041b6 Mon Sep 17 00:00:00 2001 From: thesn10 <38666407+thesn10@users.noreply.github.com> Date: Wed, 8 Jul 2026 19:18:58 +0200 Subject: [PATCH] gitignore --- .gitignore | 3 +- .../2026-07-02-expo-app-ui-implementation.md | 2110 ----------------- .../2026-07-03-app-wide-desktop-design.md | 1556 ------------ .../specs/2026-07-02-expo-app-ui-design.md | 135 -- .../2026-07-03-app-wide-desktop-design.md | 339 --- 5 files changed, 2 insertions(+), 4141 deletions(-) delete mode 100644 docs/superpowers/plans/2026-07-02-expo-app-ui-implementation.md delete mode 100644 docs/superpowers/plans/2026-07-03-app-wide-desktop-design.md delete mode 100644 docs/superpowers/specs/2026-07-02-expo-app-ui-design.md delete mode 100644 docs/superpowers/specs/2026-07-03-app-wide-desktop-design.md diff --git a/.gitignore b/.gitignore index 6a588e1..cc22889 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ # heroui-agents-md .heroui-docs/ CLAUDE.md -.yarn \ No newline at end of file +.yarn +docs/superpowers \ No newline at end of file diff --git a/docs/superpowers/plans/2026-07-02-expo-app-ui-implementation.md b/docs/superpowers/plans/2026-07-02-expo-app-ui-implementation.md deleted file mode 100644 index d610501..0000000 --- a/docs/superpowers/plans/2026-07-02-expo-app-ui-implementation.md +++ /dev/null @@ -1,2110 +0,0 @@ -# Volana Expo App UI 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:** Translate the HTML prototype (`prototype/Volana.dc.html`) into the Expo app (`app/`) — all pages, mock data, theming and responsive (mobile + web) layout — per the approved spec at `docs/superpowers/specs/2026-07-02-expo-app-ui-design.md`. - -**Architecture:** Bottom-tab + stack navigation (Expo Router) on mobile, with a persistent top nav bar replacing the tab bar on wide/web viewports (`md:` breakpoint, ~768px). Two React Context providers (`WalletProvider`, `ModalProvider`) hold mock wallet state and the globally-triggered Wallet-Connect/Checkout `BottomSheet` overlays. All product/resolver/order data is static, taken verbatim from the prototype's embedded arrays. Styling uses HeroUI Native components with Tailwind/Uniwind `className`, with only color tokens overridden in `global.css` (radius is left at the HeroUI default, which already matches the prototype's 8–16px scale). - -**Tech Stack:** Expo SDK 57, Expo Router 57 (typed routes), React 19, React Native 0.86, HeroUI Native 1.0.5, Uniwind (Tailwind v4 for RN), `@expo/vector-icons` (Ionicons). - -**Note on verification steps:** This project has no test runner configured (`package.json` only has `lint`/`typecheck`/`format` scripts) and this feature is presentational/mock-data-only per the approved spec (no business logic to unit-test). So instead of TDD red/green steps, every task's verification step is `yarn typecheck` (and `yarn lint` at milestones), plus manual visual checks at the point a screen first becomes viewable. All commands below assume the working directory is `app/` (i.e. `/mnt/store/develop/projects/projd/volana/app`). - ---- - -## File Overview - -``` -app/src/global.css — theme color overrides (Task 1) -app/src/data/mock.ts — Product/Resolver/Order types + mock data (Task 2) -app/src/lib/status.ts — status/availability → label+color helpers (Task 3) -app/src/state/wallet.tsx — WalletProvider/useWallet (Task 4) -app/src/state/modals.tsx — ModalProvider/useModals (Task 5) -app/src/app/_layout.tsx — providers, TopNav, global sheets (Tasks 6 & 15) -app/src/components/StatusChip.tsx — Task 7 -app/src/components/ListingCard.tsx — Task 8 -app/src/components/ResolverCard.tsx — Task 9 -app/src/components/OrderRow.tsx — Task 10 -app/src/components/WalletConnectSheet.tsx — Task 11 -app/src/components/CheckoutSheet.tsx — Task 12 -app/src/components/AppHeader.tsx — Task 13 -app/src/components/TopNav.tsx — Task 14 -app/src/app/(tabs)/index.tsx — Home screen (Task 16) -app/src/app/(tabs)/browse.tsx — Browse screen, replaces explore.tsx (Task 17) -app/src/app/(tabs)/orders.tsx — Orders list screen (Task 18) -app/src/app/(tabs)/_layout.tsx — tab bar, responsive hide (Task 19) -app/src/app/listing/[id].tsx — Listing detail screen (Task 20) -app/src/app/orders/[id].tsx — Order detail screen (Task 21) -``` - -Task 22 is a final typecheck/lint/manual-walkthrough pass. - ---- - -### Task 1: Theme color tokens - -**Files:** -- Modify: `app/src/global.css` - -- [ ] **Step 1: Add prototype color overrides** - -Replace the full contents of `app/src/global.css` with: - -```css -@import "tailwindcss"; -@import "uniwind"; - -@import "heroui-native/styles"; - -/* End-user path: heroui-native lives in the project's node_modules, one - level up from this src/global.css file. */ -@source '../node_modules/heroui-native/lib'; - -@layer theme { - @variant light { - --background: #f7f6fb; - --surface: #ffffff; - --surface-secondary: #f0eef8; - --accent: #7a34d4; - --accent-foreground: #ffffff; - --success: #059669; - --warning: #d97706; - --danger: #dc2626; - } - - @variant dark { - --background: #0e0e1c; - --surface: #171730; - --surface-secondary: #1e1e3c; - --accent: #b060ff; - --accent-foreground: #ffffff; - --success: #14f195; - --warning: #f5a623; - --danger: #ff4d4f; - } -} -``` - -These are the exact hex values from `prototype/Volana.dc.html`'s `:root`/`[data-theme="light"]` CSS variables (`--c-bg`, `--c-surf`, `--c-surf2`, `--c-a`, `--c-g`, `--c-y`, `--c-r`). `--radius` is intentionally left untouched — HeroUI's default base radius is 8px, which already produces `radius-lg`=8px/`radius-xl`=12px/`radius-2xl`=16px, matching the prototype's 8–16px card/button radii closely enough that overriding it isn't needed. - -- [ ] **Step 2: Verify** - -Run: `yarn typecheck` -Expected: passes with no errors (CSS changes don't affect TS, this just confirms the baseline is still healthy before further changes). - -- [ ] **Step 3: Commit** - -```bash -git add app/src/global.css -git commit -m "theme: apply Volana prototype accent/status/surface colors" -``` - ---- - -### Task 2: Mock data - -**Files:** -- Create: `app/src/data/mock.ts` - -- [ ] **Step 1: Create the mock data module** - -```ts -export type Currency = "SOL" | "USDC"; - -export interface Product { - id: number; - name: string; - desc: string; - price: string; - priceNum: number; - cur: Currency; - usd: string; - qty: number; - seller: string; - bg: string; - emoji: string; - alt: Currency[]; - cat: string; -} - -export interface Resolver { - id: number; - name: string; - desc: string; - fee: string; - type: "Human" | "DAO" | "Automated"; - wins: number; - total: number; -} - -export type OrderStatus = "AwaitingConfirm" | "Active" | "Complete" | "Cancelled" | "Disputed"; - -export interface Order { - id: number; - lid: number; - name: string; - amt: string; - status: OrderStatus; - date: string; - seller: string; - emoji: string; -} - -export const PRODUCTS: Product[] = [ - { id: 0, name: 'MacBook Pro 14" M3 Pro', desc: "Factory sealed. 18GB Unified Memory, 512GB SSD, Space Black. International keyboard layout.", price: "2.4 SOL", priceNum: 2.4, cur: "SOL", usd: "$288", qty: 5, seller: "Gh9Z…mF8k", bg: "#0a1828", emoji: "💻", alt: ["USDC"], cat: "Electronics" }, - { id: 1, name: "Adobe Photoshop Lifetime Key", desc: "One-time activation code. Non-subscription. Valid for 2 devices on Windows and macOS.", price: "45 USDC", priceNum: 45, cur: "USDC", usd: "$45", qty: 12, seller: "Bx3K…7pRq", bg: "#001230", emoji: "🎨", alt: ["SOL"], cat: "Software" }, - { id: 2, name: 'Air Jordan 1 Retro "Shadow" US 10', desc: "Deadstock. Black/Particle Grey/White. Original box, tissue paper and receipt. Zero wears.", price: "180 USDC", priceNum: 180, cur: "USDC", usd: "$180", qty: 1, seller: "Qm4R…nL2w", bg: "#180800", emoji: "👟", alt: [], cat: "Fashion" }, - { id: 3, name: "Custom 65% Mechanical Keyboard", desc: "Lubed Gateron Yellow Pro V2 switches. POM plate, anodized aluminum case. USB-C cable included.", price: "0.9 SOL", priceNum: 0.9, cur: "SOL", usd: "$108", qty: 3, seller: "Kx7P…2mJc", bg: "#0a1810", emoji: "⌨️", alt: ["USDC"], cat: "Electronics" }, - { id: 4, name: "Midjourney Pro Plan (1 Year)", desc: "Full-year activation code. 30 fast GPU hours/month, unlimited relaxed, private and stealth mode.", price: "120 USDC", priceNum: 120, cur: "USDC", usd: "$120", qty: 8, seller: "Pp1L…sY9v", bg: "#120820", emoji: "🖼️", alt: [], cat: "Software" }, - { id: 5, name: "Sony WH-1000XM5 Headphones", desc: "Brand new, factory sealed. Midnight Black. 30-hour battery. Industry-leading noise cancellation.", price: "1.8 SOL", priceNum: 1.8, cur: "SOL", usd: "$216", qty: 0, seller: "Fx2T…oK5r", bg: "#0a0a14", emoji: "🎧", alt: [], cat: "Electronics" }, - { id: 6, name: 'iPad Pro 12.9" M2 (256GB Wi-Fi)', desc: "Silver. Opened for 2 hours of testing only. AppleCare+ until June 2026. Original box included.", price: "3.2 SOL", priceNum: 3.2, cur: "SOL", usd: "$384", qty: 2, seller: "Rv8W…eC1x", bg: "#081420", emoji: "📱", alt: ["USDC"], cat: "Electronics" }, - { id: 7, name: "Figma Professional (1 Year)", desc: "Team seat. Unlimited projects, 180-day version history, dev mode and advanced prototyping.", price: "60 USDC", priceNum: 60, cur: "USDC", usd: "$60", qty: 20, seller: "Mn6S…hB3z", bg: "#180a00", emoji: "✏️", alt: ["SOL"], cat: "Software" }, - { id: 8, name: "Supreme Box Logo Hoodie — M", desc: "FW23 White. Unworn, original tags attached. Comes in original Supreme bag. Firm price.", price: "220 USDC", priceNum: 220, cur: "USDC", usd: "$220", qty: 1, seller: "Yx5A…dN7u", bg: "#180008", emoji: "🧥", alt: [], cat: "Fashion" }, - { id: 9, name: "Nintendo Switch OLED (White)", desc: "Excellent condition. Original Joy-Con, dock, power adapter, HDMI cable. Screen is pristine.", price: "1.2 SOL", priceNum: 1.2, cur: "SOL", usd: "$144", qty: 4, seller: "Jc0M…pQ4y", bg: "#0e0d18", emoji: "🕹️", alt: ["USDC"], cat: "Electronics" }, - { id: 10, name: "myshop.sol Domain Name", desc: "Premium 3-year .sol domain registration. Currently parked. Instant transfer to your wallet on purchase.", price: "0.5 SOL", priceNum: 0.5, cur: "SOL", usd: "$60", qty: 1, seller: "Zg3F…wR6t", bg: "#0e0820", emoji: "🌐", alt: [], cat: "Crypto" }, - { id: 11, name: "Rolex Submariner 116610LN", desc: "Ref 116610LN, 2019. Full box and papers. Last serviced June 2024 by Rolex-authorized RSC.", price: "8.5 SOL", priceNum: 8.5, cur: "SOL", usd: "$1,020", qty: 1, seller: "Wh7N…iE2s", bg: "#081215", emoji: "⌚", alt: ["USDC"], cat: "Luxury" }, -]; - -export const RESOLVERS: Resolver[] = [ - { id: 0, name: "ShieldArb", desc: "Human arbitrators with 24h response.", fee: "0.5%", type: "Human", wins: 94, total: 1247 }, - { id: 1, name: "FairDAO", desc: "Community-governed DAO for digital goods.", fee: "1.0%", type: "DAO", wins: 89, total: 823 }, - { id: 2, name: "QuickResolve", desc: "AI-assisted, resolves in under 2 hours.", fee: "1.5%", type: "Automated", wins: 96, total: 3102 }, -]; - -export const ORDERS: Order[] = [ - { id: 0, lid: 0, name: 'MacBook Pro 14" M3 Pro', amt: "2.4 SOL", status: "AwaitingConfirm", date: "2 days ago", seller: "Gh9Z…mF8k", emoji: "💻" }, - { id: 1, lid: 1, name: "Adobe Photoshop Lifetime Key", amt: "45 USDC", status: "Active", date: "5 days ago", seller: "Bx3K…7pRq", emoji: "🎨" }, - { id: 2, lid: 7, name: "Figma Professional (1 Year)", amt: "60 USDC", status: "Complete", date: "12 days ago", seller: "Mn6S…hB3z", emoji: "✏️" }, -]; -``` - -This is a verbatim port of the `_L`, `_R`, `_O` arrays from `prototype/Volana.dc.html` (lines 835–859). - -- [ ] **Step 2: Verify** - -Run: `yarn typecheck` -Expected: passes with no errors. - -- [ ] **Step 3: Commit** - -```bash -git add app/src/data/mock.ts -git commit -m "feat: add mock product/resolver/order data from prototype" -``` - ---- - -### Task 3: Status helpers - -**Files:** -- Create: `app/src/lib/status.ts` - -- [ ] **Step 1: Create the status/availability helper module** - -```ts -import type { OrderStatus } from "@/data/mock"; - -export type StatusColor = "default" | "accent" | "success" | "warning" | "danger"; - -export interface StatusInfo { - label: string; - color: StatusColor; -} - -const ORDER_STATUS_INFO: Record = { - AwaitingConfirm: { label: "Awaiting Confirm", color: "warning" }, - Active: { label: "Active", color: "accent" }, - Complete: { label: "Complete", color: "success" }, - Cancelled: { label: "Cancelled", color: "default" }, - Disputed: { label: "Disputed", color: "danger" }, -}; - -export function orderStatusInfo(status: OrderStatus): StatusInfo { - return ORDER_STATUS_INFO[status]; -} - -export function availabilityInfo(qty: number): StatusInfo { - if (qty === 0) return { label: "Out of Stock", color: "default" }; - if (qty <= 3) return { label: `Only ${qty} left!`, color: "warning" }; - return { label: `${qty} in stock`, color: "success" }; -} -``` - -`StatusColor` intentionally matches HeroUI Native's `Chip` `color` prop union (`'default'|'accent'|'success'|'warning'|'danger'`) exactly, so results can be passed straight through to `Chip`/`StatusChip` without conversion. This ports the `_si`/`_av` helper functions from `prototype/Volana.dc.html` (lines 869–884). - -- [ ] **Step 2: Verify** - -Run: `yarn typecheck` -Expected: passes with no errors. - -- [ ] **Step 3: Commit** - -```bash -git add app/src/lib/status.ts -git commit -m "feat: add order status and availability label/color helpers" -``` - ---- - -### Task 4: WalletProvider - -**Files:** -- Create: `app/src/state/wallet.tsx` - -- [ ] **Step 1: Create the mock wallet context** - -```tsx -import type { JSX, PropsWithChildren } from "react"; -import { createContext, useCallback, useContext, useMemo, useState } from "react"; - -export type WalletName = "phantom" | "solflare" | "backpack"; - -const WALLET_ADDRESSES: Record = { - phantom: "Gh9Z…k3mP", - solflare: "Bx3K…7pRq", - backpack: "Kx7P…2mJc", -}; - -interface WalletContextValue { - connected: boolean; - address: string | null; - connect: (wallet: WalletName) => void; - disconnect: () => void; -} - -const WalletContext = createContext(null); - -export function WalletProvider({ children }: PropsWithChildren): JSX.Element { - const [address, setAddress] = useState(null); - - const connect = useCallback((wallet: WalletName) => { - setAddress(WALLET_ADDRESSES[wallet]); - }, []); - - const disconnect = useCallback(() => { - setAddress(null); - }, []); - - const value = useMemo( - () => ({ connected: address !== null, address, connect, disconnect }), - [address, connect, disconnect], - ); - - return {children}; -} - -export function useWallet(): WalletContextValue { - const ctx = useContext(WalletContext); - if (!ctx) throw new Error("useWallet must be used within a WalletProvider"); - return ctx; -} -``` - -Mirrors the prototype's mock `connectPhantom`/`connectSolflare`/`connectBackpack`/`disconnectWallet` handlers (lines 1062–1082), minus the toast side effect (that's wired at the call site in Task 11). - -- [ ] **Step 2: Verify** - -Run: `yarn typecheck` -Expected: passes with no errors. - -- [ ] **Step 3: Commit** - -```bash -git add app/src/state/wallet.tsx -git commit -m "feat: add mock WalletProvider" -``` - ---- - -### Task 5: ModalProvider - -**Files:** -- Create: `app/src/state/modals.tsx` - -- [ ] **Step 1: Create the global modal/checkout context** - -```tsx -import type { JSX, PropsWithChildren } from "react"; -import { createContext, useCallback, useContext, useMemo, useRef, useState } from "react"; -import { useWallet, type WalletName } from "@/state/wallet"; - -export type CheckoutStep = "review" | "pending" | "success"; - -interface ModalContextValue { - isWalletSheetOpen: boolean; - isCheckoutSheetOpen: boolean; - checkoutListingId: number | null; - checkoutStep: CheckoutStep; - requestBuy: (listingId: number) => void; - openWalletConnect: () => void; - connectWallet: (wallet: WalletName) => void; - closeWalletSheet: () => void; - closeCheckoutSheet: () => void; - confirmOrder: () => void; -} - -const ModalContext = createContext(null); - -export function ModalProvider({ children }: PropsWithChildren): JSX.Element { - const { connected, connect } = useWallet(); - const [isWalletSheetOpen, setWalletSheetOpen] = useState(false); - const [isCheckoutSheetOpen, setCheckoutSheetOpen] = useState(false); - const [checkoutListingId, setCheckoutListingId] = useState(null); - const [checkoutStep, setCheckoutStep] = useState("review"); - const pendingListingId = useRef(null); - - const requestBuy = useCallback( - (listingId: number) => { - if (connected) { - setCheckoutListingId(listingId); - setCheckoutStep("review"); - setCheckoutSheetOpen(true); - } else { - pendingListingId.current = listingId; - setWalletSheetOpen(true); - } - }, - [connected], - ); - - const openWalletConnect = useCallback(() => { - pendingListingId.current = null; - setWalletSheetOpen(true); - }, []); - - const connectWallet = useCallback( - (wallet: WalletName) => { - connect(wallet); - setWalletSheetOpen(false); - const listingId = pendingListingId.current; - pendingListingId.current = null; - if (listingId !== null) { - setCheckoutListingId(listingId); - setCheckoutStep("review"); - setCheckoutSheetOpen(true); - } - }, - [connect], - ); - - const closeWalletSheet = useCallback(() => { - setWalletSheetOpen(false); - pendingListingId.current = null; - }, []); - - const closeCheckoutSheet = useCallback(() => { - setCheckoutSheetOpen(false); - }, []); - - const confirmOrder = useCallback(() => { - setCheckoutStep("pending"); - setTimeout(() => setCheckoutStep("success"), 2200); - }, []); - - const value = useMemo( - () => ({ - isWalletSheetOpen, - isCheckoutSheetOpen, - checkoutListingId, - checkoutStep, - requestBuy, - openWalletConnect, - connectWallet, - closeWalletSheet, - closeCheckoutSheet, - confirmOrder, - }), - [ - isWalletSheetOpen, - isCheckoutSheetOpen, - checkoutListingId, - checkoutStep, - requestBuy, - openWalletConnect, - connectWallet, - closeWalletSheet, - closeCheckoutSheet, - confirmOrder, - ], - ); - - return {children}; -} - -export function useModals(): ModalContextValue { - const ctx = useContext(ModalContext); - if (!ctx) throw new Error("useModals must be used within a ModalProvider"); - return ctx; -} -``` - -`requestBuy` mirrors the prototype's `clickBuy`/`onBuyNow` logic (open wallet modal first if not connected, remembering the listing via `pendingId`, else go straight to checkout). `confirmOrder` mirrors `confirmOrder` (lines 1055–1058): flips to `pending`, then `success` after 2.2s. - -- [ ] **Step 2: Verify** - -Run: `yarn typecheck` -Expected: passes with no errors. - -- [ ] **Step 3: Commit** - -```bash -git add app/src/state/modals.tsx -git commit -m "feat: add ModalProvider for wallet-connect and checkout sheets" -``` - ---- - -### Task 6: Wire Wallet and Modal providers into the root layout - -**Files:** -- Modify: `app/src/app/_layout.tsx` - -- [ ] **Step 1: Wrap the app in WalletProvider and ModalProvider** - -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 "../global.css"; -import { WalletProvider } from "@/state/wallet"; -import { ModalProvider } from "@/state/modals"; - -export default function RootLayout(): JSX.Element { - return ( - - - - - - - - - - - - - ); -} -``` - -- [ ] **Step 2: Verify** - -Run: `yarn typecheck` -Expected: passes with no errors. - -Run: `yarn start` (or `yarn start --web`), open the app. -Expected: app boots and shows the existing Home tab exactly as before (providers don't change behavior yet — nothing consumes them until later tasks). Stop the dev server after confirming. - -- [ ] **Step 3: Commit** - -```bash -git add app/src/app/_layout.tsx -git commit -m "feat: wire WalletProvider and ModalProvider into root layout" -``` - ---- - -### Task 7: StatusChip component - -**Files:** -- Create: `app/src/components/StatusChip.tsx` - -- [ ] **Step 1: Create the component** - -```tsx -import type { JSX } from "react"; -import { Chip } from "heroui-native"; -import type { StatusColor } from "@/lib/status"; - -interface StatusChipProps { - label: string; - color: StatusColor; -} - -export function StatusChip({ label, color }: StatusChipProps): JSX.Element { - return ( - - {label} - - ); -} -``` - -`variant="soft"` gives the tinted-background-plus-matching-text look the prototype uses for all its badges (e.g. `background:var(--c-gd);border:1px solid var(--c-g);color:var(--c-g)`). - -- [ ] **Step 2: Verify** - -Run: `yarn typecheck` -Expected: passes with no errors. - -- [ ] **Step 3: Commit** - -```bash -git add app/src/components/StatusChip.tsx -git commit -m "feat: add StatusChip component" -``` - ---- - -### Task 8: ListingCard component - -**Files:** -- Create: `app/src/components/ListingCard.tsx` - -- [ ] **Step 1: Create the component** - -```tsx -import type { GestureResponderEvent } from "react-native"; -import type { JSX } from "react"; -import { Pressable, View } from "react-native"; -import { useRouter } from "expo-router"; -import { Button, Separator, Surface, Typography } from "heroui-native"; -import type { Product } from "@/data/mock"; -import { availabilityInfo } from "@/lib/status"; -import { StatusChip } from "@/components/StatusChip"; -import { useModals } from "@/state/modals"; - -interface ListingCardProps { - product: Product; - variant?: "list" | "grid"; -} - -export function ListingCard({ product, variant = "grid" }: ListingCardProps): JSX.Element { - const router = useRouter(); - const { requestBuy } = useModals(); - 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 ( - - - - {product.emoji} - - - - - {product.priceNum} - - - {product.cur} - - - - - - {product.name} - - - {product.desc} - - - - - - - - - - ); - } - - return ( - - - - {product.emoji} - - - - {product.name} - - - {product.seller} - - - - {product.price} - - - - - - - - ); -} -``` - -`grid` variant carries its own responsive width (`w-1/2 md:w-1/3 lg:w-1/4`) so callers can just render a `flex-row flex-wrap` container and drop cards in — no CSS Grid needed (React Native doesn't support `display: grid`, even on the web build). `list` variant reproduces the prototype's image/price/name+desc/buy row (lines 353–392) using `Separator` for the vertical divider lines instead of manual border classes. - -- [ ] **Step 2: Verify** - -Run: `yarn typecheck` -Expected: passes with no errors. - -- [ ] **Step 3: Commit** - -```bash -git add app/src/components/ListingCard.tsx -git commit -m "feat: add ListingCard component (list and grid variants)" -``` - ---- - -### Task 9: ResolverCard component - -**Files:** -- Create: `app/src/components/ResolverCard.tsx` - -- [ ] **Step 1: Create the component** - -```tsx -import type { JSX } from "react"; -import { Pressable, View } from "react-native"; -import { Typography } from "heroui-native"; -import type { Resolver } from "@/data/mock"; - -interface ResolverCardProps { - resolver: Resolver; - isSelected: boolean; - onSelect: () => void; -} - -export function ResolverCard({ resolver, isSelected, onSelect }: ResolverCardProps): JSX.Element { - return ( - - - - {resolver.name} - - - {resolver.fee} - - - - {resolver.type} · {resolver.wins}% buyer-favorable - - - ); -} -``` - -Ports the prototype's selectable resolver rows (lines 514–522 / `_fb` selected-state styling). - -- [ ] **Step 2: Verify** - -Run: `yarn typecheck` -Expected: passes with no errors. - -- [ ] **Step 3: Commit** - -```bash -git add app/src/components/ResolverCard.tsx -git commit -m "feat: add ResolverCard component" -``` - ---- - -### Task 10: OrderRow component - -**Files:** -- Create: `app/src/components/OrderRow.tsx` - -- [ ] **Step 1: Create the component** - -```tsx -import type { JSX } 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"; - -interface OrderRowProps { - order: Order; -} - -export function OrderRow({ order }: OrderRowProps): JSX.Element { - const router = useRouter(); - const status = orderStatusInfo(order.status); - - return ( - router.push(`/orders/${order.id}`)} - className="flex-row items-center gap-3.5 rounded-xl border border-border bg-surface p-3.5" - > - {order.emoji} - - - {order.name} - - - {order.seller} - - - - - {order.amt} - - - {order.date} - - - - - ); -} -``` - -Ports the prototype's order list row (lines 556–570). - -- [ ] **Step 2: Verify** - -Run: `yarn typecheck` -Expected: passes with no errors. - -- [ ] **Step 3: Commit** - -```bash -git add app/src/components/OrderRow.tsx -git commit -m "feat: add OrderRow component" -``` - ---- - -### Task 11: WalletConnectSheet component - -**Files:** -- Create: `app/src/components/WalletConnectSheet.tsx` - -- [ ] **Step 1: Create the component** - -```tsx -import type { JSX } from "react"; -import { View } from "react-native"; -import { BottomSheet, Button, Typography, useToast } from "heroui-native"; -import { useModals } from "@/state/modals"; -import type { WalletName } from "@/state/wallet"; - -const WALLETS: { id: WalletName; label: string; description: string; emoji: string }[] = [ - { id: "phantom", label: "Phantom", description: "Most popular Solana wallet", emoji: "👻" }, - { id: "solflare", label: "Solflare", description: "Built for power users", emoji: "🌊" }, - { id: "backpack", label: "Backpack", description: "xNFT & multi-chain wallet", emoji: "🎒" }, -]; - -export function WalletConnectSheet(): JSX.Element { - const { isWalletSheetOpen, closeWalletSheet, connectWallet } = useModals(); - const { toast } = useToast(); - - const handleConnect = (wallet: WalletName, label: string) => { - connectWallet(wallet); - toast.show({ variant: "success", label: `${label} connected!` }); - }; - - return ( - { - if (!open) closeWalletSheet(); - }} - > - - - - - Connect Wallet - Choose your Solana wallet - - {WALLETS.map((wallet) => ( - - ))} - - - - - ); -} -``` - -This is rendered once at the root (Task 15) and driven entirely by `ModalProvider`'s `isWalletSheetOpen`/`closeWalletSheet`/`connectWallet` — no `BottomSheet.Trigger` needed, matching the documented fully-controlled `isOpen`/`onOpenChange` pattern. Ports the prototype's wallet modal (lines 656–698) and `connectPhantom`/`connectSolflare`/`connectBackpack` toasts (lines 1062–1082). - -- [ ] **Step 2: Verify** - -Run: `yarn typecheck` -Expected: passes with no errors. - -- [ ] **Step 3: Commit** - -```bash -git add app/src/components/WalletConnectSheet.tsx -git commit -m "feat: add WalletConnectSheet component" -``` - ---- - -### Task 12: CheckoutSheet component - -**Files:** -- Create: `app/src/components/CheckoutSheet.tsx` - -- [ ] **Step 1: Create the component** - -```tsx -import type { JSX } from "react"; -import { useMemo, useState } from "react"; -import { View } from "react-native"; -import { useRouter } from "expo-router"; -import { BottomSheet, Button, Spinner, Typography, useThemeColor } from "heroui-native"; -import { PRODUCTS, RESOLVERS } from "@/data/mock"; -import { useModals } from "@/state/modals"; -import { ResolverCard } from "@/components/ResolverCard"; - -export function CheckoutSheet(): JSX.Element { - const router = useRouter(); - const accentColor = useThemeColor("accent"); - const { isCheckoutSheetOpen, checkoutListingId, checkoutStep, closeCheckoutSheet, confirmOrder } = - useModals(); - const [resolverId, setResolverId] = useState(0); - - const listing = useMemo( - () => PRODUCTS.find((product) => product.id === checkoutListingId) ?? PRODUCTS[0], - [checkoutListingId], - ); - - const handleViewOrders = () => { - closeCheckoutSheet(); - router.push("/orders"); - }; - - const handleContinueShopping = () => { - closeCheckoutSheet(); - router.push("/browse"); - }; - - return ( - { - if (!open) closeCheckoutSheet(); - }} - > - - - - {checkoutStep === "review" && ( - - - Place Order - - - {listing.emoji} - - - - {listing.name} - - - {listing.seller} - - - - - - - Payment - - - {listing.price} - - ≈ {listing.usd} USD - - - - - - - Dispute Protection - - {RESOLVERS.map((item) => ( - setResolverId(item.id)} - /> - ))} - - - - - - - - )} - - {checkoutStep === "pending" && ( - - - Confirming on Solana… - - {"Approve the transaction in your wallet.\nThis takes a few seconds."} - - - )} - - {checkoutStep === "success" && ( - - - - - - Order Placed! - - {`${listing.price} is now held in secure escrow.\nThe seller has been notified.`} - - - - - - - - )} - - - - ); -} -``` - -`confirmOrder` (from `ModalProvider`) drives the `review → pending → success` transition with the same 2.2s delay as the prototype (lines 1055–1058). The resolver picker reuses `ResolverCard` (Task 9). `isCloseOnPress={checkoutStep !== "pending"}` stops the user from dismissing the sheet mid-"transaction". - -- [ ] **Step 2: Verify** - -Run: `yarn typecheck` -Expected: passes with no errors. - -- [ ] **Step 3: Commit** - -```bash -git add app/src/components/CheckoutSheet.tsx -git commit -m "feat: add CheckoutSheet component (review/pending/success)" -``` - ---- - -### Task 13: AppHeader component - -**Files:** -- Create: `app/src/components/AppHeader.tsx` - -- [ ] **Step 1: Create the component** - -```tsx -import type { JSX } from "react"; -import { Pressable, View } from "react-native"; -import { useRouter } from "expo-router"; -import { Ionicons } from "@expo/vector-icons"; -import { Typography, useThemeColor } from "heroui-native"; - -interface AppHeaderProps { - title: string; -} - -export function AppHeader({ title }: AppHeaderProps): JSX.Element { - const router = useRouter(); - const foreground = useThemeColor("foreground"); - - return ( - - router.back()} hitSlop={8} className="p-1"> - - - - {title} - - - ); -} -``` - -Used only on the mobile-pushed Listing Detail and Order Detail screens (Tasks 20–21), which set `headerShown: false` at the root Stack level and render this instead. It's hidden at `md:` because `TopNav` (Task 14) is visible there and provides navigation instead. - -- [ ] **Step 2: Verify** - -Run: `yarn typecheck` -Expected: passes with no errors. - -- [ ] **Step 3: Commit** - -```bash -git add app/src/components/AppHeader.tsx -git commit -m "feat: add AppHeader component for mobile stack screens" -``` - ---- - -### Task 14: TopNav component - -**Files:** -- Create: `app/src/components/TopNav.tsx` - -- [ ] **Step 1: Create the component** - -```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"; - -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 ? ( - - ) : ( - - )} - - ); -} -``` - -`hidden md:flex` keeps this invisible on mobile widths. The search field navigates to `/browse` on submit — it intentionally does not thread the query into Browse's own search state (Browse has its own independent search field, Task 17); wiring cross-screen search state isn't needed for a mock-data app and would add scope without a corresponding prototype requirement beyond "navigate to browse". - -- [ ] **Step 2: Verify** - -Run: `yarn typecheck` -Expected: passes with no errors. - -- [ ] **Step 3: Commit** - -```bash -git add app/src/components/TopNav.tsx -git commit -m "feat: add TopNav component for wide/web layouts" -``` - ---- - -### Task 15: Wire TopNav and global sheets into the root layout - -**Files:** -- Modify: `app/src/app/_layout.tsx` - -- [ ] **Step 1: Render TopNav above the Stack, and the two sheets alongside it** - -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 "../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 { - return ( - - - - - - - - - - - - - - - - - - ); -} -``` - -`TopNav` sits above the `Stack` inside a `flex-1` wrapper, so it persists across every route pushed onto that Stack (tab screens and, later, `listing/[id]`/`orders/[id]`). The two sheets are rendered once, outside the Stack, and are entirely state-driven by `ModalProvider` — any screen can call `useModals().requestBuy(...)` etc. to open them. - -- [ ] **Step 2: Verify** - -Run: `yarn typecheck` -Expected: passes with no errors. - -Run: `yarn start --web`, resize the browser window wide (≥768px) and narrow (<768px). -Expected: at narrow width, no top bar is visible (bottom tab bar still shows, from the existing (tabs) layout). At wide width, the Volana top bar (logo, search field, Browse/My Orders buttons, Connect Wallet button) appears above the content. Clicking "Connect Wallet" opens a bottom sheet listing Phantom/Solflare/Backpack; clicking one closes it and the nav button now shows the mock address. Stop the dev server after confirming. - -- [ ] **Step 3: Commit** - -```bash -git add app/src/app/_layout.tsx -git commit -m "feat: render TopNav and global wallet/checkout sheets in root layout" -``` - ---- - -### Task 16: Home screen - -**Files:** -- Modify: `app/src/app/(tabs)/index.tsx` - -- [ ] **Step 1: Replace the placeholder Home 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"; - -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) => ( - - ))} - - - - ); -} -``` - -This is the "Minimal" home-tab scope agreed in the spec: short hero + featured listings only (no stats/"why Volana"/"how it works" sections, no footer). Ports the prototype's hero headline (lines 125–126) and featured listings grid (lines 262–276), using the same "first 4 in-stock products" selection (prototype `featuredProducts`, lines 1099–1103). - -- [ ] **Step 2: Verify** - -Run: `yarn typecheck` -Expected: passes with no errors. - -Run: `yarn start --web`, open the Home tab. -Expected: purple-accented headline "The marketplace / nobody controls.", subline, "Browse Listings →" button, and a 2-column (narrow) / 3–4-column (wide) grid of 4 featured product cards with emoji thumbnails, names, and prices. Tapping a card navigates to a (still-placeholder-behaving, since Task 20 isn't done yet — it will 404 or show a default Expo Router "unmatched route" screen) listing route — that's expected at this point. Stop the dev server after confirming the Home tab itself renders correctly. - -- [ ] **Step 3: Commit** - -```bash -git add "app/src/app/(tabs)/index.tsx" -git commit -m "feat: implement Home screen (hero + featured listings)" -``` - ---- - -### Task 17: Browse screen - -**Files:** -- Delete: `app/src/app/(tabs)/explore.tsx` -- Create: `app/src/app/(tabs)/browse.tsx` - -- [ ] **Step 1: Remove the placeholder Explore screen** - -```bash -git rm "app/src/app/(tabs)/explore.tsx" -``` - -- [ ] **Step 2: Create the Browse screen** - -```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"; - -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) => ( - - ))} - - )} - - - ); -} -``` - -Ports the prototype's filter bar (lines 322–347: currency chips, sort chips, in-stock switch, list/grid toggle) and its filtering/sorting logic (`renderVals`, lines 898–914). Both view modes render via a plain `ScrollView` + `.map()` rather than `FlatList`, since `ListingCard`'s `grid` variant already carries responsive width classes that are incompatible with `FlatList`'s fixed `numColumns` — and at 12 mock items, `FlatList`'s virtualization has no practical benefit anyway. - -- [ ] **Step 3: Verify** - -Run: `yarn typecheck` -Expected: passes with no errors (this also confirms the `explore.tsx` deletion didn't break any other import — nothing else references it). - -Run: `yarn start --web`, navigate to the Browse tab (bottom tab bar narrow, or "Browse" link in TopNav wide). -Expected: search field, Currency (All/SOL/USDC) and Sort (Newest/Price↑/Price↓) chips that visibly highlight the active one, "In stock only" switch (defaults on — Sony headphones with qty 0 should be hidden), list/grid toggle that switches layout. Typing in the search field filters the list live. Stop the dev server after confirming. - -- [ ] **Step 4: Commit** - -```bash -git add "app/src/app/(tabs)/browse.tsx" -git commit -m "feat: implement Browse screen (search, filters, list/grid view)" -``` - ---- - -### Task 18: Orders screen - -**Files:** -- Create: `app/src/app/(tabs)/orders.tsx` - -- [ ] **Step 1: Create the Orders list screen** - -```tsx -import type { JSX } from "react"; -import { ScrollView, View } from "react-native"; -import { Typography } from "heroui-native"; -import { ORDERS } from "@/data/mock"; -import { OrderRow } from "@/components/OrderRow"; - -export default function OrdersTab(): JSX.Element { - return ( - - - - My Orders - - {ORDERS.map((order) => ( - - ))} - - - ); -} -``` - -Ports the prototype's My Orders page (lines 546–575), using the static `ORDERS` mock array (per the spec's "no dynamically added orders" decision — the prototype itself doesn't add purchased items to this list either). - -- [ ] **Step 2: Verify** - -Run: `yarn typecheck` -Expected: passes with no errors. - -- [ ] **Step 3: Commit** - -```bash -git add "app/src/app/(tabs)/orders.tsx" -git commit -m "feat: implement Orders list screen" -``` - ---- - -### Task 19: Responsive tab bar with Browse and Orders tabs - -**Files:** -- Modify: `app/src/app/(tabs)/_layout.tsx` - -- [ ] **Step 1: Register the Browse and Orders tabs, hide the tab bar at md: width** - -Replace the full contents of `app/src/app/(tabs)/_layout.tsx` with: - -```tsx -import { Ionicons } from "@expo/vector-icons"; -import { Tabs } from "expo-router"; -import type { ComponentProps, JSX } from "react"; -import type { ColorValue } from "react-native"; -import { useWindowDimensions } from "react-native"; - -type IoniconName = ComponentProps["name"]; - -function TabIcon({ name, color }: { name: IoniconName; color: ColorValue }): JSX.Element { - return ; -} - -const WIDE_BREAKPOINT = 768; - -export default function TabsLayout(): JSX.Element { - const { width } = useWindowDimensions(); - const isWide = width >= WIDE_BREAKPOINT; - - return ( - - , - }} - /> - , - }} - /> - , - }} - /> - - ); -} -``` - -`useWindowDimensions` is used (rather than a Tailwind `md:` class) because `tabBarStyle` is a React Navigation option evaluated in JS, not a style on a component we can attach a `className` to directly. It re-renders on resize, so resizing a browser window across the breakpoint live-toggles the tab bar. - -- [ ] **Step 2: Verify** - -Run: `yarn typecheck` -Expected: passes with no errors. - -Run: `yarn start --web`. -Expected: bottom tab bar shows Home/Browse/Orders at narrow width; resizing the window to ≥768px hides the tab bar (TopNav's Browse/My Orders links take over). Stop the dev server after confirming. - -- [ ] **Step 3: Commit** - -```bash -git add "app/src/app/(tabs)/_layout.tsx" -git commit -m "feat: add Browse/Orders tabs, hide tab bar on wide viewports" -``` - ---- - -### Task 20: Listing Detail screen - -**Files:** -- Create: `app/src/app/listing/[id].tsx` - -- [ ] **Step 1: Create the Listing Detail screen** - -```tsx -import type { JSX } from "react"; -import type { ViewStyle } from "react-native"; -import { useMemo, useState } from "react"; -import { Platform, ScrollView, View } from "react-native"; -import { useLocalSearchParams } from "expo-router"; -import { Button, Chip, Separator, Typography, useToast } from "heroui-native"; -import type { Currency } from "@/data/mock"; -import { PRODUCTS, RESOLVERS } from "@/data/mock"; -import { availabilityInfo } from "@/lib/status"; -import { AppHeader } from "@/components/AppHeader"; -import { StatusChip } from "@/components/StatusChip"; -import { ResolverCard } from "@/components/ResolverCard"; -import { ListingCard } from "@/components/ListingCard"; -import { useModals } from "@/state/modals"; - -// 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" ? ({ position: "sticky", top: 84 } as ViewStyle) : undefined; - -export default function ListingDetailScreen(): JSX.Element { - const { id } = useLocalSearchParams<{ id: string }>(); - const { toast } = useToast(); - const { requestBuy } = useModals(); - const [resolverId, setResolverId] = useState(0); - - 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. - - - - - ); -} -``` - -Ports the prototype's listing detail page (lines 424–543): image, title/description, verified-seller row with copy button (shows a toast instead of touching a real clipboard — no on-chain/real wallet functionality exists to copy, per the spec's non-goals), info grid, related products, and the buy panel (price, currency switch, resolver picker, Buy Now, trust badges). `requestBuy` (from `ModalProvider`, Task 5) reproduces the prototype's "open wallet modal if disconnected, else open checkout" branch. - -- [ ] **Step 2: Verify** - -Run: `yarn typecheck` -Expected: passes with no errors. - -Run: `yarn start --web`, go to Browse, tap any product card. -Expected: listing detail renders with image placeholder, title/description, verified-seller row (Copy button shows a toast), info grid, buy panel with price/resolver picker/Buy Now button, trust badge chips. At narrow width the layout is a single stacked column with a back button (`AppHeader`) at the top; at ≥768px it's two columns with the buy panel on the right. Tapping "Buy Now →" while disconnected opens the Wallet Connect sheet; connecting a wallet then opens the Checkout sheet automatically with this listing pre-filled. Stop the dev server after confirming. - -- [ ] **Step 3: Commit** - -```bash -git add "app/src/app/listing/[id].tsx" -git commit -m "feat: implement Listing Detail screen" -``` - ---- - -### Task 21: Order Detail screen - -**Files:** -- Create: `app/src/app/orders/[id].tsx` - -- [ ] **Step 1: Create the Order Detail screen** - -```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"; - -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 && ( - - )} - - - - - ); -} -``` - -Ports the prototype's order detail page (lines 577–651): header, 3-step progress stepper with the same `done`/`active` color logic as `_si`/the inline `od` derivation (lines 918–943), escrow info box, and status-conditional actions (`Cancel Order` only when `AwaitingConfirm`, `Confirm Receipt` only when `Active`, `View Listing` always). Status changes are local `useState` (mirrors the prototype, which also only mutates its in-memory state and never persists — see spec's "orders stay static" decision for the *list*; here on the *detail* screen the single order's status is allowed to change locally so Cancel/Confirm visibly do something, exactly like the prototype's `cancelOrder`/`confirmReceipt` handlers). - -- [ ] **Step 2: Verify** - -Run: `yarn typecheck` -Expected: passes with no errors. - -Run: `yarn start --web`, go to Orders, tap the "MacBook Pro" order (status `AwaitingConfirm`). -Expected: header with name/seller/amount/status chip, 3-step progress bar (step 1 green checkmark, step 2 highlighted, step 3 grey), escrow info text, "Cancel Order (Full Refund)" and "View Listing →" buttons (no "Confirm Receipt" button, since status is `AwaitingConfirm` not `Active`). Tapping "Cancel Order" shows a toast and flips the status chip/stepper to Cancelled. Stop the dev server after confirming. - -- [ ] **Step 3: Commit** - -```bash -git add "app/src/app/orders/[id].tsx" -git commit -m "feat: implement Order Detail screen" -``` - ---- - -### Task 22: Final verification pass - -**Files:** none (verification only) - -- [ ] **Step 1: Full typecheck and lint** - -Run: `yarn typecheck` -Expected: passes with no errors. - -Run: `yarn lint` -Expected: passes with no errors (fix any issues found before proceeding — do not disable rules to silence them). - -- [ ] **Step 2: Manual walkthrough — mobile width** - -Run: `yarn start --web`, keep the browser window under 768px wide (or open the Expo Go / simulator build if available). - -Walk through and confirm: -- Bottom tab bar shows Home / Browse / Orders with icons. -- Home: hero headline in the accent purple, "Browse Listings →" button navigates to Browse, featured grid shows 4 cards. -- Browse: search filters live, currency/sort chips toggle correctly, "In stock only" switch hides the 0-qty Sony headphones when on, list/grid toggle switches layout. -- Tapping a product card opens Listing Detail with a back button (top-left) that returns to Browse. -- "Buy Now →" while disconnected opens Wallet Connect → picking a wallet closes it and opens Checkout automatically on the same listing. -- Checkout: Confirm & Place Order → shows a spinner for ~2s → success screen → "View My Orders" navigates to the Orders tab. -- Orders tab lists 3 mock orders with correct status chips; tapping one opens Order Detail with the correct progress stepper and status-appropriate action buttons. -- Order Detail: Cancel/Confirm actions show a toast and update the status chip/stepper immediately. - -- [ ] **Step 3: Manual walkthrough — wide/web width** - -Resize the browser window to ≥768px (or use a wide simulator/tablet). - -Walk through and confirm: -- Bottom tab bar is gone; a top nav bar (logo, search field, Browse, My Orders, wallet button) is visible and stays visible on every screen, including Listing Detail and Order Detail. -- "Browse" / "My Orders" nav buttons navigate correctly; wallet button opens Wallet Connect / shows the connected mock address once connected. -- Home hero is centered, featured grid shows more columns than at mobile width. -- Browse grid shows more columns; list view rows are wider. -- Listing Detail shows a two-column layout (content left, buy panel right); the buy panel visually stays in place while scrolling the left column (sticky). -- No mobile-only `AppHeader` back bar is visible on Listing/Order Detail (TopNav is used for navigation instead). - -- [ ] **Step 4: Stop the dev server** - -No commit for this task — it's verification-only. If any issue was found and fixed during the walkthrough, amend the relevant earlier task's commit history is *not* required; just create one more commit with the fix: - -```bash -git add -A -git commit -m "fix: address issues found during manual UI walkthrough" -``` - -(Only run this if fixes were actually needed — skip entirely if the walkthrough passed cleanly.) - ---- - -## Spec Coverage Check - -| Spec section | Covered by | -|---|---| -| Theming (accent/status/surface colors, radius left default) | Task 1 | -| Mock data (products/resolvers/orders) | Task 2 | -| State (WalletProvider, ModalProvider incl. pending-listing-id flow, checkout step machine) | Tasks 4–5 | -| Navigation & Screens file layout | Tasks 6, 15–21 | -| Responsive shell (TopNav vs bottom tabs, `md:` breakpoint) | Tasks 14, 15, 19 | -| Grid layouts via flex-wrap, not CSS grid | Task 8 (ListingCard), used in Tasks 16, 17, 20 | -| Listing-detail two-column + sticky buy box on web | Task 20 | -| Home (minimal scope) | Task 16 | -| Browse (search/filter/sort/view toggle) | Task 17 | -| Listing Detail (info grid, related products, resolver/currency selection, trust badges) | Task 20 | -| Orders list | Task 18 | -| Order Detail (stepper, status actions) | Task 21 | -| Shared components (StatusChip, ListingCard, ResolverCard, OrderRow, WalletConnectSheet, CheckoutSheet, AppHeader, TopNav) | Tasks 7–14 | -| Verification approach (no test runner → typecheck/lint + manual walkthrough) | Every task's Verify step + Task 22 | 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 deleted file mode 100644 index fed6474..0000000 --- a/docs/superpowers/plans/2026-07-03-app-wide-desktop-design.md +++ /dev/null @@ -1,1556 +0,0 @@ -# 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. diff --git a/docs/superpowers/specs/2026-07-02-expo-app-ui-design.md b/docs/superpowers/specs/2026-07-02-expo-app-ui-design.md deleted file mode 100644 index a896529..0000000 --- a/docs/superpowers/specs/2026-07-02-expo-app-ui-design.md +++ /dev/null @@ -1,135 +0,0 @@ -# Volana Expo App — UI-Umsetzung des Prototyps - -## Kontext - -`prototype/Volana.dc.html` ist ein interaktiver HTML-Prototyp (DC-Format mit Template-Bindings à la `{{ x }}`, `sc-if`, `sc-for`) der Volana-Konsumenten-Oberfläche: ein dezentraler, zensurresistenter Marktplatz auf Solana (siehe `docs/volana-design-brief.md` für den vollständigen Produkt-Kontext). Der Prototyp enthält Homepage, Browse (Liste/Grid), Listing-Detail, My Orders, Order-Detail, Wallet-Connect-Modal, Checkout-Modal (Review/Pending/Success) und Toasts, inklusive vollständiger Mock-Daten (12 Produkte, 3 Dispute-Resolver, 3 Orders) und Farb-/Theming-Variablen für Light/Dark. - -Die Expo-App (`app/`) ist aktuell ein leeres Grundgerüst: Expo Router mit `(tabs)`-Gruppe (Home/Explore), HeroUI Native, Uniwind (Tailwind v4 für React Native), React 19 / RN 0.86 / Expo SDK 57. - -**Ziel dieser Iteration:** Das Design des Prototyps 1:1 in Seiten/Komponenten der Expo-App übertragen — mobil UND auf Web (`react-native-web`, in den Dependencies vorhanden) nahe am Prototyp. Keine echte Funktionalität (kein echtes Wallet, kein Backend) — Mock-Daten reichen. Styling bleibt so nah wie möglich an den HeroUI-Native-Defaults; nur Akzentfarbe, Statusfarben und Radius werden angepasst (Theming), komplexere Komponenten/Seiten bekommen zusätzlich Custom-Styles via `className` (Uniwind/Tailwind) bzw. `StyleSheet` wo nötig. - -## Nicht-Ziele - -- Keine echte Solana-Wallet-Integration (`@solana/connector` o.ä.) — Connect/Disconnect ist gemockt wie im Prototyp. -- Kein Backend/On-Chain-Datenfetching — statische Mock-Daten aus dem Prototyp übernommen. -- Keine Persistenz von Bestellungen (gekaufte Listings erscheinen nicht dynamisch in "My Orders" — der Prototyp macht das ebenfalls nicht). -- Kein manueller Light/Dark-Toggle — die App folgt dem System-Theme (Uniwind unterstützt das nativ). -- Kein Footer (im Prototyp vorhanden, in der App nicht nötig). -- Keine Pixel-genaue 1:1-Übernahme der Inline-Styles des Prototyps — HeroUI-Native-Komponenten und ihre Standard-Abstände/Formen bleiben so weit wie möglich erhalten. - -## Theming - -Anpassung ausschließlich über CSS-Variablen-Overrides in `src/global.css` (`@layer theme { @variant light {...} @variant dark {...} }`), wie in der HeroUI-Native-Theming-Doku beschrieben. Kein Erstellen neuer Custom-Colors — bestehende semantische Slots werden mit den Prototyp-Werten belegt: - -| Variable | Dark (Prototyp `--c-*`) | Light (Prototyp `--c-*`) | Verwendung | -|---|---|---|---| -| `--accent` / `--accent-foreground` | `#b060ff` / weiß | `#7a34d4` / weiß | Primär-CTA, Preis-Hervorhebung, aktive Filter | -| `--success` | `#14f195` | `#059669` | "In Stock", "Verified", "Complete", "Final Payment" | -| `--warning` | `#f5a623` | `#d97706` | "Low Stock", "Awaiting Confirm" | -| `--danger` | `#ff4d4f` | `#dc2626` | "Out of Stock", "Disputed", "Cancel Order" | -| `--background` / `--surface` / `--surface-secondary` | leicht lila-stichige Dunkeltöne (`#0e0e1c`/`#171730`/`#1e1e3c`) | helle lila-stichige Töne (`#f7f6fb`/`#ffffff`/`#f0eef8`) | Basis-/Karten-Hintergründe | -| `--radius` | Basiswert so gewählt, dass abgeleitete `--radius-lg`/`--radius-xl` in etwa den 8–14px-Radii des Prototyps entsprechen | gleich | Cards, Buttons, Inputs | - -Die im Prototyp verwendete "Solana-Mainnet"-Grün-Punkt/"Verified"-Optik nutzt direkt `success`, keine eigene Farbe nötig. - -## Datenmodell & Mock-Daten - -Neue Datei `src/data/mock.ts` mit TS-Typen und den Werten 1:1 aus dem Prototyp (`_L`, `_R`, `_O` Arrays in `Volana.dc.html`): - -```ts -type Currency = 'SOL' | 'USDC'; - -interface Product { - id: number; name: string; desc: string; - price: string; priceNum: number; cur: Currency; usd: string; - qty: number; seller: string; bg: string; emoji: string; - alt: Currency[]; cat: string; -} - -interface Resolver { - id: number; name: string; desc: string; - fee: string; type: 'Human' | 'DAO' | 'Automated'; - wins: number; total: number; -} - -type OrderStatus = 'AwaitingConfirm' | 'Active' | 'Complete' | 'Cancelled' | 'Disputed'; - -interface Order { - id: number; lid: number; name: string; amt: string; - status: OrderStatus; date: string; seller: string; emoji: string; -} -``` - -Emojis dienen weiterhin als Platzhalter-"Produktbild" (wie im Prototyp) statt echter Bilder — kein Asset-Aufwand nötig. Helper-Funktionen analog zu `_si` (Status → Label/Farbe) und `_av` (Verfügbarkeit → Label/Farbe) werden als reine Funktionen in `src/data/mock.ts` oder `src/lib/status.ts` nachgebaut. - -## State - -- **`WalletProvider`** (`src/state/wallet.tsx`, React Context): `connected: boolean`, `address: string | null`, `connect(wallet: 'phantom' | 'solflare' | 'backpack')` (setzt eine fixe Mock-Adresse je Wallet, identisch zum Prototyp), `disconnect()`. -- **`ModalProvider`** (`src/state/modals.tsx`, React Context): zentraler Zustand für die beiden globalen Overlays: - - Wallet-Connect-Sheet: `open`, `pendingListingId` (falls "Buy Now" ohne verbundenes Wallet ausgelöst wurde → nach Connect automatisch Checkout öffnen, wie im Prototyp) - - Checkout-Sheet: `open`, `listingId`, `step: 'review' | 'pending' | 'success'`, inkl. simuliertem `setTimeout`-Übergang `pending → success` wie im Prototyp (2.2s) -- Bestellungen bleiben statische Mock-Daten (siehe Nicht-Ziele) — kein zusätzlicher Order-State nötig. -- Toasts über HeroUI Natives eingebauten `useToast`/`ToastProvider` (Teil von `HeroUINativeProvider`), verwendet für: Wallet verbunden/getrennt, Adresse kopiert, Order storniert/bestätigt. - -Beide Provider werden in `app/_layout.tsx` um den bestehenden `HeroUINativeProvider` ergänzt. - -## Navigation & Screens - -``` -app/_layout.tsx Root Stack + WalletProvider + ModalProvider; rendert zusätzlich - (nur ab `md:`, s.u.) + globale Sheets (WalletConnectSheet, CheckoutSheet) -app/(tabs)/_layout.tsx Bottom Tabs: Home | Browse | Orders (Tab-Bar ab `md:` ausgeblendet) -app/(tabs)/index.tsx Home -app/(tabs)/browse.tsx Browse (ersetzt bisheriges explore.tsx) -app/(tabs)/orders.tsx My Orders (Liste) -app/listing/[id].tsx Listing-Detail — Stack-Push über den Tabs, eigener Header mit Zurück-Button -app/orders/[id].tsx Order-Detail — Stack-Push über den Tabs, eigener Header mit Zurück-Button -``` - -Wallet-Connect- und Checkout-Overlay sind bewusst **keine Routen**, sondern globale, über `ModalProvider` gesteuerte `BottomSheet`-Komponenten im Root-Layout — "Buy Now" ist von Home, Browse und Listing-Detail auslösbar und muss überall denselben zentralen State treffen (entspricht dem zentralen Component-State im Prototyp). - -### Responsive Shell (Mobile vs. Web/Desktop) - -Umschaltung über Uniwind-Responsive-Utilities (Breakpoint `md:`, ~768px) — funktioniert sowohl nativ (z. B. Tablet-Breite) als auch im Web-Build: - -- **`TopNav`** (`src/components/TopNav.tsx`): Logo, Suchfeld, "Browse"-/"My Orders"-Links, Wallet-Button/-Pill — analog zur Prototyp-Nav. Klasse `hidden md:flex`, liegt im Root-Layout über allen Screens (auch Listing-/Order-Detail), damit Web-Nutzer durchgehend navigieren können. -- **Bottom-Tab-Bar:** in `(tabs)/_layout.tsx` per `useWindowDimensions` (React-Navigation-`tabBarStyle` ist keine Tailwind-Klasse, daher JS-Bedingung statt CSS) ab `md:`-Breite auf `display: 'none'` gesetzt. -- **Ergebnis:** Mobile = Bottom-Tabs + Stack-Pushes für Details. Web/Desktop = Top-Nav wie Prototyp, keine Tab-Bar, Navigation über `router.push`. - -**Grid-Layouts:** React Native kennt kein CSS-Grid (auch nicht im Web-Build, da react-native-web View-Primitives nutzt) — alle "Grid"-Bereiche (Browse-Grid, Featured Listings, Customers-also-bought) werden über `flex-row flex-wrap` + Breiten-Utilities (`w-1/2 md:w-1/3 lg:w-1/4` o. ä.) gebaut, nicht über `display: grid`. - -**Listing-Detail:** `flex-col md:flex-row` (mobil gestapelt, ab `md:` zweispaltig wie im Prototyp). Die rechte Kauf-Box nutzt auf Web `position: 'sticky'` (von react-native-web unterstützt); nativ scrollt sie regulär mit. - -## Screens im Detail - -### Home (`app/(tabs)/index.tsx`) -Kurzer Hero (Headline + Subline + "Browse Listings"-Button, kein Stats-/"Warum Volana"-/"How it works"-Block), darunter Featured-Listings (4 Karten, `ListingCard` Grid-Variante). - -### Browse (`app/(tabs)/browse.tsx`) -`SearchField` (Name/Beschreibung), Filter-Reihe (`Chip`-Toggles für Currency All/SOL/USDC und Sort Newest/Price↑/Price↓), `Switch` "In stock only", List/Grid-View-Toggle (Icon-Buttons). Darunter Produktliste aus den gefilterten/sortierten Mock-Daten: -- List-View: horizontale Card (Bild-Platzhalter · Preis · Name+Beschreibung · Buy-Button), analog Prototyp-Zeilen-Layout. -- Grid-View: 2-spaltige (mobil) / mehr-spaltige (`md:`/`lg:`) vertikale Cards. - -### Listing-Detail (`app/listing/[id].tsx`) -Bild-Platzhalter (Emoji auf Farbfläche), Titel, Beschreibung, "Verified on Solana"-Zeile mit Copy-Button (Toast "Address copied!"), Info-Grid (Kategorie/Verfügbar/Währung/ID), "Customers also bought" (horizontale/Flex-Wrap-Liste verwandter Produkte). Kauf-Sektion (sticky ab `md:`): Preis, Currency-Umschalter (falls `alt.length > 0`), Resolver-Auswahl (3 selektierbare Cards aus Mock-Resolvern), "Buy Now"-Button (öffnet Wallet-Sheet falls nicht verbunden, sonst Checkout-Sheet), Trust-Badges (Final Payment / Dispute Protection / On-chain Verified). - -### Orders (`app/(tabs)/orders.tsx`) -Liste von `OrderRow` (Emoji, Name, Seller-Kürzel, Betrag, Datum, Status-`Chip`), Tap → `app/orders/[id].tsx`. - -### Order-Detail (`app/orders/[id].tsx`) -Header (Datum, Name, Seller, Betrag, Status), 3-Schritt-Fortschrittsanzeige (Payment Held → Seller Confirms → You Confirm Receipt) mit Status-abhängiger Einfärbung wie im Prototyp (`_si`-Logik), Escrow-Info-Box, Aktions-Buttons je Status: `AwaitingConfirm` → Cancel Order (mock, zeigt Toast + navigiert zurück), `Active` → Confirm Receipt (mock, zeigt Toast), immer sichtbar → "View Listing". - -## Gemeinsame Komponenten (`src/components/`) - -- `TopNav` — s. o. -- `AppHeader` — einfacher mobiler Screen-Header für Stack-Screens (Titel + Zurück-Button), auf `md:` ggf. ausgeblendet da `TopNav` übernimmt -- `ListingCard` — Varianten `list` und `grid` -- `StatusChip` — mappt `OrderStatus`/Verfügbarkeit auf Label + Farbe (nutzt `Chip` von HeroUI) -- `ResolverCard` — selektierbare Resolver-Option (Listing-Detail + Checkout-Review) -- `OrderRow` — Zeile in der Orders-Liste -- `WalletConnectSheet` — globales `BottomSheet` mit den drei Mock-Wallets (Phantom/Solflare/Backpack) -- `CheckoutSheet` — globales `BottomSheet`, rendert intern je nach `step` Review-/Pending-(Spinner)-/Success-Inhalt - -## Verifikation - -Da keine echte Funktionalität besteht, erfolgt Verifikation visuell: App per `expo start` (Web + iOS/Android-Simulator soweit verfügbar) starten, jede Seite und beide Breakpoints (schmal/mobil, breit/`md:`+) durchklicken, Kernflows prüfen (Home → Browse → Listing → Buy Now ohne Wallet → Connect → Checkout Review → Pending → Success → My Orders; Order-Detail Cancel/Confirm-Aktionen; Such-/Filter-/Sortier-/List-Grid-Umschaltung in Browse). diff --git a/docs/superpowers/specs/2026-07-03-app-wide-desktop-design.md b/docs/superpowers/specs/2026-07-03-app-wide-desktop-design.md deleted file mode 100644 index adf0028..0000000 --- a/docs/superpowers/specs/2026-07-03-app-wide-desktop-design.md +++ /dev/null @@ -1,339 +0,0 @@ -# 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 `