# Solisting App — Foundation (Part 2 of 4) > **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:** Scaffold the Next.js 16 App Router project with all shared infrastructure: package setup, CSS theming, wallet providers, React Query, Tailwind (for connectorkit components), copied connectorkit `ui-base` and connector components, shared UI primitives, the Nav, and the root layout. **Architecture:** All pages are `'use client'`. CSS custom properties drive the Nebula dark theme; inline styles use these variables. Tailwind v4 (PostCSS plugin only) is included for the copied connectorkit components. The `WalletProviders` component wraps `AppProvider` (from `@solana/connector/react`) and `QueryClientProvider` (React Query). **Tech Stack:** Next.js 16, React 19, `@solana/connector ^0.2.4`, `@solana/kit ^6`, `@tanstack/react-query ^5`, `@base-ui/react ^1`, `class-variance-authority`, `clsx`, `tailwind-merge`, `lucide-react`, Tailwind v4 via `@tailwindcss/postcss`. **Prerequisites:** Part 1 (SDK) complete; `sdk/` package built; `yarn install` run at workspace root. --- ## File Map | Path | Purpose | |---|---| | `app/package.json` | App package with all deps | | `app/tsconfig.json` | TS config with `@/*` path alias | | `app/next.config.ts` | Minimal Next.js config | | `app/postcss.config.mjs` | Tailwind v4 PostCSS plugin | | `app/src/styles/globals.css` | Nebula CSS variables + Tailwind import + fonts | | `app/src/lib/utils.ts` | `cn()` helper (clsx + tailwind-merge) | | `app/src/lib/format.ts` | `abbrev`, `fmtSol`, `fmtTok`, `relTime`, `exact` | | `app/src/lib/queryClient.ts` | React Query `QueryClient` singleton | | `app/src/providers/WalletProviders.tsx` | `AppProvider` + `QueryClientProvider` | | `app/src/components/ui-base/button.tsx` | Copied from connectorkit (ui-base) | | `app/src/components/ui-base/menu.tsx` | Copied from connectorkit (ui-base) | | `app/src/components/ui-base/dialog.tsx` | Copied from connectorkit (ui-base) | | `app/src/components/ui-base/collapsible.tsx` | Copied from connectorkit (ui-base) | | `app/src/components/connector/connect-button.tsx` | Copied from connectorkit (adapted) | | `app/src/components/connector/wallet-modal.tsx` | Copied from connectorkit (no WC/QR) | | `app/src/components/connector/wallet-dropdown-content.tsx` | Simplified dropdown | | `app/src/components/ui/Button.tsx` | App button primitive (inline styles) | | `app/src/components/ui/Badge.tsx` | Status pill | | `app/src/components/ui/FieldRow.tsx` | Label + value row | | `app/src/components/ui/Toast.tsx` | Toast context + fixed notification | | `app/src/components/Nav.tsx` | Sticky navbar | | `app/src/app/layout.tsx` | Root layout (fonts, providers, toast) | | `app/src/app/page.tsx` | Redirect to `/listings` | --- ### Task 6: App Package Scaffolding **Files:** `app/package.json`, `app/tsconfig.json`, `app/next.config.ts`, `app/postcss.config.mjs` - [ ] **Step 1: Create `app/package.json`** ```json { "name": "solisting-app", "private": true, "scripts": { "dev": "next dev --turbopack", "build": "next build", "start": "next start", "typecheck": "tsc --noEmit" }, "dependencies": { "@base-ui/react": "^1.0.0", "@descro/sdk": "file:../../descro/sdk", "@solana/connector": "^0.2.4", "@solana/connector-debugger": "^0.1.1", "@solana/kit": "^6.0.0", "@solisting/sdk": "workspace:*", "@tanstack/react-query": "^5.90.5", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^0.546.0", "motion": "^12.23.24", "next": "^16.2.6", "react": "^19.2.6", "react-dom": "^19.2.6", "tailwind-merge": "^3.3.1" }, "devDependencies": { "@tailwindcss/postcss": "^4.1.14", "@types/node": "^25.9.0", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", "tailwindcss": "^4.1.14", "typescript": "^6.0.3" } } ``` - [ ] **Step 2: Create `app/tsconfig.json`** ```json { "compilerOptions": { "target": "ES2022", "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, "strict": true, "noEmit": true, "esModuleInterop": true, "module": "esnext", "moduleResolution": "bundler", "resolveJsonModule": true, "isolatedModules": true, "jsx": "preserve", "incremental": true, "plugins": [{ "name": "next" }], "paths": { "@/*": ["./src/*"] } }, "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], "exclude": ["node_modules"] } ``` - [ ] **Step 3: Create `app/next.config.ts`** ```ts import type { NextConfig } from 'next' const config: NextConfig = { reactStrictMode: true, } export default config ``` - [ ] **Step 4: Create `app/postcss.config.mjs`** ```js export default { plugins: { '@tailwindcss/postcss': {}, }, } ``` - [ ] **Step 5: Install dependencies** ```bash yarn install ``` Expected: `app/node_modules/` populated with all deps including `@solisting/sdk` resolved from the local workspace. - [ ] **Step 6: Commit** ```bash git add app/package.json app/tsconfig.json app/next.config.ts app/postcss.config.mjs git commit -m "feat: scaffold solisting-app Next.js package" ``` --- ### Task 7: CSS Globals + format.ts + utils.ts + queryClient.ts **Files:** `app/src/styles/globals.css`, `app/src/lib/utils.ts`, `app/src/lib/format.ts`, `app/src/lib/queryClient.ts` - [ ] **Step 1: Create `app/src/styles/globals.css`** ```css @import "tailwindcss"; @import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap'); :root { --bg: oklch(0.165 0.022 282); --bg2: oklch(0.205 0.026 282); --bg3: oklch(0.25 0.03 283); --bd: oklch(0.33 0.032 286); --bdSoft: oklch(0.27 0.028 284); --navbg: oklch(0.18 0.024 282 / .82); --tx: oklch(0.96 0.008 285); --mut: oklch(0.68 0.022 284); --radius: 13px; --acc: #9945FF; --acc2: #14F195; --acc2light: #7df5c4; --accGlow: rgba(153,69,255,.55); --accSoft: rgba(153,69,255,.13); --accBd: rgba(153,69,255,.4); --danger: #FF7A59; --dangerSoft: rgba(255,122,89,.12); --font-body: 'Space Grotesk', system-ui, sans-serif; --font-mono: 'JetBrains Mono', monospace; } /* Tailwind theme tokens for connectorkit components */ @theme { --color-primary: var(--acc); --color-primary-foreground: #0b0613; --color-background: var(--bg); --color-foreground: var(--tx); --color-popover: var(--bg2); --color-popover-foreground: var(--tx); --color-border: var(--bd); --color-input: var(--bd); --color-muted: var(--bg3); --color-accent: var(--bg3); --color-accent-foreground: var(--tx); --color-destructive: var(--danger); } * { box-sizing: border-box; } body { margin: 0; background: var(--bg); color: var(--tx); font-family: var(--font-body); } input, button, select, textarea { font-family: inherit; font-size: inherit; color: inherit; } button { cursor: pointer; border: none; background: none; } ::selection { background: rgba(153,69,255,.35); } ::-webkit-scrollbar { width: 10px; height: 10px; } ::-webkit-scrollbar-thumb { background: rgba(255,255,255,.13); border-radius: 8px; } ::-webkit-scrollbar-track { background: transparent; } @keyframes scToast { from { opacity: 0; transform: translate(-50%,10px); } to { opacity: 1; transform: translate(-50%,0); } } @keyframes scSpin { to { transform: rotate(360deg); } } @keyframes scPulse { 0%,100% { opacity: 1; } 50% { opacity: .45; } } ``` - [ ] **Step 2: Create `app/src/lib/utils.ts`** ```ts import { clsx, type ClassValue } from 'clsx' import { twMerge } from 'tailwind-merge' export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)) } ``` - [ ] **Step 3: Create `app/src/lib/format.ts`** ```ts export function abbrev(pk: string | null | undefined): string { if (!pk) return '—' return `${pk.slice(0, 4)}…${pk.slice(-4)}` } export function fmtSol(lamports: bigint | number): string { const v = Number(lamports) / 1e9 return v.toLocaleString(undefined, { maximumFractionDigits: 4 }) + ' SOL' } export function fmtTok(raw: bigint | number, decimals: number, symbol: string): string { const v = Number(raw) / Math.pow(10, decimals) return v.toLocaleString(undefined, { maximumFractionDigits: Math.min(decimals, 2) }) + ' ' + symbol } export function relTime(ts: number): string { const d = Math.floor(Date.now() / 1000) - ts if (d < 60) return `${d}s ago` if (d < 3600) return `${Math.floor(d / 60)}m ago` if (d < 86400) return `${Math.floor(d / 3600)}h ago` return `${Math.floor(d / 86400)}d ago` } export function exact(ts: number): string { return new Date(ts * 1000).toUTCString() } export function priceStr( currency: { __kind: 'Sol' } | { __kind: 'Spl'; decimals: number; symbol?: string }, price: bigint | number, ): string { if (currency.__kind === 'Sol') return fmtSol(price) return fmtTok(price, currency.decimals, (currency as { symbol?: string }).symbol ?? 'SPL') } ``` Note: The `__kind` field is how codama renders Rust enums in TypeScript. After running Codama (Task 3), inspect `src/generated/solisting/types/currency.ts` to confirm the exact shape and adjust `priceStr` if needed. - [ ] **Step 4: Create `app/src/lib/queryClient.ts`** ```ts import { QueryClient } from '@tanstack/react-query' export const queryClient = new QueryClient({ defaultOptions: { queries: { staleTime: 10_000, gcTime: 5 * 60 * 1000, retry: 2, }, }, }) ``` - [ ] **Step 5: Type-check** ```bash cd app && yarn typecheck ``` Expected: No errors (or only missing Next.js module errors before `next-env.d.ts` is generated — run `next dev` once if needed). - [ ] **Step 6: Commit** ```bash git add app/src/ git commit -m "feat: add CSS globals, format utils, and queryClient" ``` --- ### Task 8: WalletProviders **Files:** `app/src/providers/WalletProviders.tsx` - [ ] **Step 1: Create `app/src/providers/WalletProviders.tsx`** ```tsx 'use client' import type { ReactNode } from 'react' import { useMemo } from 'react' import { AppProvider, getDefaultConfig, getDefaultMobileConfig } from '@solana/connector/react' import { QueryClientProvider } from '@tanstack/react-query' import { queryClient } from '@/lib/queryClient' function getOrigin() { if (typeof window !== 'undefined') return window.location.origin return 'http://localhost:3000' } export function WalletProviders({ children }: { children: ReactNode }) { const connectorConfig = useMemo( () => getDefaultConfig({ appName: 'Solisting Explorer', appUrl: getOrigin(), autoConnect: true, clusters: [ { id: 'solana:localnet' as const, label: 'Localnet', url: 'http://localhost:8899' }, { id: 'solana:devnet' as const, label: 'Devnet', url: 'https://api.devnet.solana.com' }, { id: 'solana:mainnet' as const, label: 'Mainnet', url: 'https://api.mainnet-beta.solana.com', }, ], }), [], ) const mobile = useMemo( () => getDefaultMobileConfig({ appName: 'Solisting Explorer', appUrl: getOrigin() }), [], ) return ( {children} ) } ``` - [ ] **Step 2: Type-check** ```bash cd app && yarn typecheck ``` - [ ] **Step 3: Commit** ```bash git add app/src/providers/ git commit -m "feat: add WalletProviders with connector + react-query" ``` --- ### Task 9: Copy Connectorkit ui-base Components Fetch the four `ui-base` wrapper components directly from the connectorkit GitHub repo and write them into `app/src/components/ui-base/`. - [ ] **Step 1: Fetch and write `button.tsx`** ```bash gh api repos/solana-foundation/connectorkit/contents/examples/next-js/components/ui-base/button.tsx \ --jq '.content' | base64 -d > app/src/components/ui-base/button.tsx ``` - [ ] **Step 2: Fetch and write `menu.tsx`** ```bash gh api repos/solana-foundation/connectorkit/contents/examples/next-js/components/ui-base/menu.tsx \ --jq '.content' | base64 -d > app/src/components/ui-base/menu.tsx ``` - [ ] **Step 3: Fetch and write `dialog.tsx`** ```bash gh api repos/solana-foundation/connectorkit/contents/examples/next-js/components/ui-base/dialog.tsx \ --jq '.content' | base64 -d > app/src/components/ui-base/dialog.tsx ``` - [ ] **Step 4: Fetch and write `collapsible.tsx`** ```bash gh api repos/solana-foundation/connectorkit/contents/examples/next-js/components/ui-base/collapsible.tsx \ --jq '.content' | base64 -d > app/src/components/ui-base/collapsible.tsx ``` - [ ] **Step 5: Fix imports in all four files** All four files import from `@/lib/utils`. That path is already correct since we have `app/src/lib/utils.ts`. No change needed. Run a quick check: ```bash grep -r "from '@/" app/src/components/ui-base/ ``` Expected: Only `@/lib/utils` imports. If you see other `@/components/...` imports, those files also need to be copied. - [ ] **Step 6: Type-check** ```bash cd app && yarn typecheck ``` Fix any import errors by checking if additional dependencies are missing from `package.json`. - [ ] **Step 7: Commit** ```bash git add app/src/components/ui-base/ git commit -m "feat: copy connectorkit ui-base components" ``` --- ### Task 10: Copy and Adapt Connector Components Fetch `connect-button.tsx` and `wallet-modal.tsx` from connectorkit. Adapt `wallet-modal.tsx` to remove WalletConnect/QR code (heavy dep, not needed). Write a simplified `wallet-dropdown-content.tsx` matching the prototype (address + copy + disconnect). - [ ] **Step 1: Fetch `connect-button.tsx`** ```bash mkdir -p app/src/components/connector gh api repos/solana-foundation/connectorkit/contents/examples/next-js/components/connector/base-ui/connect-button.tsx \ --jq '.content' | base64 -d > app/src/components/connector/connect-button.tsx ``` - [ ] **Step 2: Fetch `wallet-modal.tsx` and strip WalletConnect** ```bash gh api repos/solana-foundation/connectorkit/contents/examples/next-js/components/connector/base-ui/wallet-modal.tsx \ --jq '.content' | base64 -d > app/src/components/connector/wallet-modal.tsx ``` Then open `wallet-modal.tsx` and: - Remove `import { CustomQRCode } from '@/components/ui/custom-qr-code'` - Remove `walletConnectUri` and `onClearWalletConnectUri` from the props interface and destructuring - Remove the entire QR code section (the `{walletConnectUri && ...}` block) - Remove the `walletConnectUri?: string | null` and `onClearWalletConnectUri?: () => void` from `WalletModalProps` - [ ] **Step 3: Update `connect-button.tsx` to remove WalletConnect props** In `connect-button.tsx`, the `WalletModal` is called with `walletConnectUri` and `onClearWalletConnectUri` props. Remove these: ```tsx // Before: { setIsModalOpen(open); if (!open) { clearWalletConnectUri(); } }} walletConnectUri={walletConnectUri} onClearWalletConnectUri={clearWalletConnectUri} /> // After: ``` Also remove `walletConnectUri` and `clearWalletConnectUri` from the `useConnector()` destructure. - [ ] **Step 4: Write simplified `wallet-dropdown-content.tsx`** Replace whatever was fetched with this version matching the prototype's dropdown: ```tsx 'use client' import { useConnector } from '@solana/connector/react' import { useState } from 'react' interface Props { selectedAccount: string walletIcon?: string walletName: string } export function WalletDropdownContent({ selectedAccount, walletIcon, walletName }: Props) { const { disconnectWallet } = useConnector() const [copied, setCopied] = useState(false) function copyAddress() { navigator.clipboard.writeText(selectedAccount) setCopied(true) setTimeout(() => setCopied(false), 1500) } return (
{walletIcon && (
{walletName} {walletName}
)}
CONNECTED WALLET
{selectedAccount}
) } ``` - [ ] **Step 5: Type-check** ```bash cd app && yarn typecheck ``` Fix any remaining import issues (e.g. if `connect-button.tsx` imports from `@/components/connector/shared/hidden-wallet-icons`, fetch that file too or stub it). - [ ] **Step 6: Commit** ```bash git add app/src/components/connector/ git commit -m "feat: add connector wallet components (ConnectButton, WalletModal)" ``` --- ### Task 11: App UI Primitives **Files:** `app/src/components/ui/Button.tsx`, `Badge.tsx`, `FieldRow.tsx`, `Toast.tsx` These use inline styles + CSS variables. No Tailwind. - [ ] **Step 1: Create `app/src/components/ui/Button.tsx`** ```tsx import type { CSSProperties, ReactNode, ButtonHTMLAttributes } from 'react' export type ButtonVariant = 'primary' | 'outline' | 'danger' | 'ghost' interface ButtonProps extends ButtonHTMLAttributes { variant?: ButtonVariant children: ReactNode } const variantStyles: Record = { primary: { background: 'linear-gradient(135deg, var(--acc), var(--acc2))', color: '#0b0613', border: 'none', }, outline: { background: 'transparent', border: '1px solid var(--bd)', color: 'var(--tx)', }, danger: { background: 'transparent', border: '1px solid var(--danger)', color: 'var(--danger)', }, ghost: { background: 'var(--bg3)', border: '1px solid var(--bd)', color: 'var(--tx)', }, } export function Button({ variant = 'primary', children, style, disabled, ...rest }: ButtonProps) { return ( ) } ``` - [ ] **Step 2: Create `app/src/components/ui/Badge.tsx`** ```tsx import type { ReactNode } from 'react' interface BadgeProps { children: ReactNode color: string bg: string } export function Badge({ children, color, bg }: BadgeProps) { return ( {children} ) } export const STATUS_COLORS = { active: { color: '#14F195', bg: 'rgba(20,241,149,.15)' }, inactive: { color: '#8A8FA3', bg: 'rgba(138,143,163,.16)' }, awaitingConfirm: { color: '#F5B23E', bg: 'rgba(245,178,62,.15)' }, disputed: { color: '#FF7A59', bg: 'rgba(255,122,89,.15)' }, complete: { color: '#4FD1E0', bg: 'rgba(79,209,224,.15)' }, cancelled: { color: '#8A8FA3', bg: 'rgba(138,143,163,.16)' }, } ``` - [ ] **Step 3: Create `app/src/components/ui/FieldRow.tsx`** ```tsx import type { ReactNode } from 'react' interface FieldRowProps { label: string children: ReactNode last?: boolean } export function FieldRow({ label, children, last }: FieldRowProps) { return (
{label}
{children}
) } export function MonoChip({ value, onCopy, onClick, sub, }: { value: string onCopy?: () => void onClick?: () => void sub?: string }) { function copy() { navigator.clipboard.writeText(value) onCopy?.() } return (
{value}
{sub &&
{sub}
}
) } ``` - [ ] **Step 4: Create `app/src/components/ui/Toast.tsx`** ```tsx 'use client' import { createContext, useCallback, useContext, useState, type ReactNode } from 'react' const ToastCtx = createContext<(msg: string) => void>(() => {}) export function useToast() { return useContext(ToastCtx) } export function ToastProvider({ children }: { children: ReactNode }) { const [msg, setMsg] = useState('') const show = useCallback((m: string) => { setMsg(m) setTimeout(() => setMsg(''), 2800) }, []) return ( {children} {msg && (
{msg}
)}
) } ``` - [ ] **Step 5: Type-check** ```bash cd app && yarn typecheck ``` - [ ] **Step 6: Commit** ```bash git add app/src/components/ui/ git commit -m "feat: add app UI primitives (Button, Badge, FieldRow, Toast)" ``` --- ### Task 12: Nav Component **Files:** `app/src/components/Nav.tsx` - [ ] **Step 1: Create `app/src/components/Nav.tsx`** ```tsx 'use client' import { usePathname, useRouter } from 'next/navigation' import { useState, useRef, useEffect } from 'react' import { useCluster } from '@solana/connector/react' import { ConnectButton } from '@/components/connector/connect-button' const NAV_LINKS = [ { href: '/listings', label: 'Listings' }, { href: '/escrows', label: 'Escrows' }, { href: '/resolvers', label: 'Resolvers' }, { href: '/dashboard', label: 'Dashboard' }, ] const NET_DOTS: Record = { 'solana:localnet': '#FF7A59', 'solana:devnet': '#14F195', 'solana:mainnet': '#9945FF', } export function Nav() { const pathname = usePathname() const router = useRouter() const { cluster, clusters, setCluster } = useCluster() const [netOpen, setNetOpen] = useState(false) const [search, setSearch] = useState('') const netRef = useRef(null) useEffect(() => { function close(e: MouseEvent) { if (netRef.current && !netRef.current.contains(e.target as Node)) setNetOpen(false) } document.addEventListener('mousedown', close) return () => document.removeEventListener('mousedown', close) }, []) function handleSearch(e: React.KeyboardEvent) { if (e.key === 'Enter' && search.trim()) { router.push(`/search?q=${encodeURIComponent(search.trim())}`) setSearch('') } } const dot = NET_DOTS[cluster?.id ?? ''] ?? '#8A8FA3' return ( ) } ``` - [ ] **Step 2: Type-check** ```bash cd app && yarn typecheck ``` - [ ] **Step 3: Commit** ```bash git add app/src/components/Nav.tsx git commit -m "feat: add Nav component" ``` --- ### Task 13: Root Layout + Redirect Page **Files:** `app/src/app/layout.tsx`, `app/src/app/page.tsx` - [ ] **Step 1: Create `app/src/app/layout.tsx`** ```tsx import type { ReactNode } from 'react' import { WalletProviders } from '@/providers/WalletProviders' import { ToastProvider } from '@/components/ui/Toast' import { Nav } from '@/components/Nav' import '@/styles/globals.css' export const metadata = { title: 'Solisting Explorer', description: 'Explorer for the Solisting protocol on Solana', } export default function RootLayout({ children }: { children: ReactNode }) { return (