Description
Every transaction hash or account address a user successfully looks up should be saved to a browsable history. This makes the app feel personal and lets users return to past lookups without needing to remember long hashes.
This issue also introduces AppShell and AppShellContext — the shared wrapper that all app pages use. Previous issues used a stub version of AppShell. This is where the real implementation lands.
What To Build
Part 1 — AppShell (replaces stub from UI #5)
**`src/components/AppShellContext.ts`**
- Defines the `AppShellContextValue` interface and exports the `useAppShell()` hook
- Context shape includes: history state, address book state (stubs for now), copy utility, and personal mode (stubs for now)
- Any component inside `AppShell` can call `useAppShell()` to access shared state
**`src/components/AppShell.tsx`** — replaces the stub created in UI #5
- Wraps all app pages (`/app`, `/tx/[hash]`, `/account/[address]`)
- Provides the dark background + grid overlay layers
- Renders the persistent app header: logo (links to `/app`), `HistoryButton` trigger, panel buttons area
- Wraps children with `AppShellContext.Provider`
- Renders `HistoryPanel` and `Toast` at root level (outside normal flow)
- Body scroll locked when any panel is open
**Inner component pattern** — each result page must follow this structure to avoid hooks-in-callbacks errors:
```tsx
function TxPageInner() {
const { addEntry } = useAppShell(); // ✅ hook at component top level
...
}
export default function TxPage() {
return <AppShell><TxPageInner /></AppShell>;
}
Part 2 — Search History
src/hooks/useSearchHistory.ts
- Manages history in
localStorage under key stellar-explain-history
- Stores up to 20 entries:
{ id, type: 'transaction' | 'account', identifier, searchedAt: ISO string, summary?: string }
summary populated from the backend response after a successful fetch (truncated to 80 chars)
- Only reads
localStorage after mount to avoid SSR hydration mismatch
- Exports:
addEntry(), removeEntry(), clearAll(), entries, count
src/components/history/HistoryButton.tsx
- Trigger button with a clock icon and a live count badge
- Props:
count: number, onClick: () => void
- Purple/sky-blue pill style consistent with the app header
src/components/history/HistoryItem.tsx
- Single history entry row — pure display component
- Shows: type icon pill (tx = sky blue, account = purple), truncated identifier (first 8…last 8), relative time ("2 hours ago"), summary snippet
- Individual delete (×) button
- Clicking the row calls
onSelect(entry)
src/components/history/HistoryPanel.tsx
- Glassy slide-out panel from the right with blur overlay
- Width:
min(380px, 100vw) — full width on mobile
- Animation:
translateX(100%) → translateX(0) with cubic-bezier(0.32, 0.72, 0, 1)
- Blur overlay:
backdrop-filter: blur(4px) with dark tint — clicking it closes the panel
- Entries grouped by Transactions and Accounts sections
- Empty state with clock illustration when no history
- Clear all history button in the footer (only shown when entries exist)
- Escape key + backdrop click both close the panel
- Custom scrollbar via
.history-panel-scroll CSS class
Key Files
New files to create:
src/components/AppShellContext.ts
src/components/AppShell.tsx ← replaces stub
src/hooks/useSearchHistory.ts
src/components/history/HistoryButton.tsx
src/components/history/HistoryItem.tsx
src/components/history/HistoryPanel.tsx
Files to update:
src/app/app/page.tsx — wrap with <AppShell>, wire history
src/app/tx/[hash]/page.tsx — wrap with <AppShell>, call addEntry on success
src/app/account/[address]/page.tsx — wrap with <AppShell>, call addEntry on success
Acceptance Criteria
Depends on: UI #5, UI #6, UI #7
Description
Every transaction hash or account address a user successfully looks up should be saved to a browsable history. This makes the app feel personal and lets users return to past lookups without needing to remember long hashes.
This issue also introduces
AppShellandAppShellContext— the shared wrapper that all app pages use. Previous issues used a stub version ofAppShell. This is where the real implementation lands.What To Build
Part 1 — AppShell (replaces stub from UI #5)
Part 2 — Search History
src/hooks/useSearchHistory.tslocalStorageunder keystellar-explain-history{ id, type: 'transaction' | 'account', identifier, searchedAt: ISO string, summary?: string }summarypopulated from the backend response after a successful fetch (truncated to 80 chars)localStorageafter mount to avoid SSR hydration mismatchaddEntry(),removeEntry(),clearAll(),entries,countsrc/components/history/HistoryButton.tsxcount: number,onClick: () => voidsrc/components/history/HistoryItem.tsxonSelect(entry)src/components/history/HistoryPanel.tsxmin(380px, 100vw)— full width on mobiletranslateX(100%)→translateX(0)withcubic-bezier(0.32, 0.72, 0, 1)backdrop-filter: blur(4px)with dark tint — clicking it closes the panel.history-panel-scrollCSS classKey Files
New files to create:
src/components/AppShellContext.tssrc/components/AppShell.tsx← replaces stubsrc/hooks/useSearchHistory.tssrc/components/history/HistoryButton.tsxsrc/components/history/HistoryItem.tsxsrc/components/history/HistoryPanel.tsxFiles to update:
src/app/app/page.tsx— wrap with<AppShell>, wire historysrc/app/tx/[hash]/page.tsx— wrap with<AppShell>, calladdEntryon successsrc/app/account/[address]/page.tsx— wrap with<AppShell>, calladdEntryon successAcceptance Criteria
AppShellfrom UI 5 is fully replaced by the real implementationuseAppShell()hook is accessible from any page wrapped inAppShellDepends on: UI #5, UI #6, UI #7