Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 74 additions & 11 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,25 @@
import { lazy, Suspense } from 'react';
import { Routes, Route, Navigate } from 'react-router-dom';
import { Header } from '@/components/Header';
import { AutoSign } from '@/components/AutoSign';
import { TelemetryBanner } from '@/components/TelemetryBanner';
import Send from '@/pages/Send';
import Receive from '@/pages/Receive';
import Privacy from '@/pages/Privacy';
import { HelpButton } from '@/components/HelpButton';
import Vault from '@/pages/Vault';
import Schedule from '@/pages/Schedule';
import {
StellarSendSkeleton,
StellarReceiveSkeleton,
StellarWithdrawSkeleton,
StellarActivitySkeleton,
StellarSettingsSkeleton,
} from '@/components/Skeletons';

// Lazy-load every page so each Route gets its own Suspense boundary and the
// correct skeleton is shown while the chunk is fetched/parsed.
const Send = lazy(() => import('@/pages/Send'));
const Receive = lazy(() => import('@/pages/Receive'));
const Privacy = lazy(() => import('@/pages/Privacy'));
const Vault = lazy(() => import('@/pages/Vault'));
const Schedule = lazy(() => import('@/pages/Schedule'));
const History = lazy(() => import('@/pages/History'));

export function App() {
return (
Expand All @@ -17,12 +29,63 @@ export function App() {
<AutoSign />
<main className="mx-auto w-full max-w-[720px] flex-1 px-4 pb-16 pt-8 sm:px-6 sm:pb-24 sm:pt-10">
<Routes>
<Route path="/send" element={<Send />} />
<Route path="/receive" element={<Receive />} />
<Route path="/privacy" element={<Privacy />} />
<Route path="/vault" element={<Vault />} />
<Route path="/schedule" element={<Schedule />} />
<Route path="/pay" element={<Send />} />
<Route
path="/send"
element={
<Suspense fallback={<StellarSendSkeleton />}>
<Send />
</Suspense>
}
/>
<Route
path="/receive"
element={
<Suspense fallback={<StellarReceiveSkeleton />}>
<Receive />
</Suspense>
}
/>
<Route
path="/vault"
element={
<Suspense fallback={<StellarWithdrawSkeleton />}>
<Vault />
</Suspense>
}
/>
<Route
path="/history"
element={
<Suspense fallback={<StellarActivitySkeleton />}>
<History />
</Suspense>
}
/>
<Route
path="/schedule"
element={
<Suspense fallback={<StellarSettingsSkeleton />}>
<Schedule />
</Suspense>
}
/>
<Route
path="/privacy"
element={
<Suspense fallback={<StellarSendSkeleton />}>
<Privacy />
</Suspense>
}
/>
{/* /pay is a payment-link entry point — reuses the Send chunk */}
<Route
path="/pay"
element={
<Suspense fallback={<StellarSendSkeleton />}>
<Send />
</Suspense>
}
/>
<Route path="*" element={<Navigate to="/send" replace />} />
</Routes>
</main>
Expand Down
25 changes: 25 additions & 0 deletions src/components/Skeletons/SkeletonBlock.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/**
* SkeletonBlock – a single animated placeholder block.
*
* Uses a shimmer animation defined in index.css via the `animate-skeleton`
* Tailwind utility. All dimensions are passed as Tailwind classes so callers
* control width/height while this component owns the animation, colour, and
* aria attributes.
*/
import type { HTMLAttributes } from 'react';

interface SkeletonBlockProps extends HTMLAttributes<HTMLDivElement> {
/** Extra Tailwind classes, e.g. "w-32 h-4" */
className?: string;
}

export function SkeletonBlock({ className = '', ...rest }: SkeletonBlockProps) {
return (
<div
role="presentation"
aria-hidden="true"
className={`animate-skeleton bg-outline-variant/40 ${className}`}
{...rest}
/>
);
}
66 changes: 66 additions & 0 deletions src/components/Skeletons/StellarActivitySkeleton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/**
* StellarActivitySkeleton
*
* Mirrors the connected state of StellarHistory.
*
* Structure mirrors:
* section.flex-col.gap-8
* ├── header row (chain badge + h1 || clear button)
* ├── filter row (Type select + Status select)
* └── activity list (5 row placeholders)
* each row: kind badge | address | amount | status chip
*/
import { SkeletonBlock } from './SkeletonBlock';

export function StellarActivitySkeleton() {
return (
<section aria-label="Loading activity history" className="flex flex-col gap-8">
{/* ── Header row ── */}
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div className="flex flex-col gap-2">
<SkeletonBlock className="h-3 w-40" />
<SkeletonBlock className="h-8 w-40" />
</div>
{/* Clear history button */}
<SkeletonBlock className="h-9 w-32" />
</div>

{/* ── Filter row ── */}
<div className="flex flex-wrap gap-4">
<div className="flex flex-col gap-1">
<SkeletonBlock className="h-3 w-10" />
<SkeletonBlock className="h-9 w-36" />
</div>
<div className="flex flex-col gap-1">
<SkeletonBlock className="h-3 w-14" />
<SkeletonBlock className="h-9 w-36" />
</div>
</div>

{/* ── Activity rows (5 placeholders) ── */}
<div className="flex flex-col gap-3">
{[0, 1, 2, 3, 4].map((i) => (
<div
key={i}
className="flex items-center justify-between border border-outline-variant p-4"
>
<div className="flex flex-col gap-2">
{/* kind badge */}
<SkeletonBlock className="h-4 w-28" />
{/* address */}
<SkeletonBlock className="h-3 w-48" />
{/* timestamp */}
<SkeletonBlock className="h-3 w-24" />
</div>
<div className="flex flex-col items-end gap-2">
{/* amount */}
<SkeletonBlock className="h-4 w-16" />
{/* status chip */}
<SkeletonBlock className="h-5 w-14" />
</div>
</div>
))}
</div>
</section>
);
}
70 changes: 70 additions & 0 deletions src/components/Skeletons/StellarReceiveSkeleton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/**
* StellarReceiveSkeleton
*
* Mirrors the "keys derived + scanning" state of StellarReceive / StellarReceiveView
* which is the most commonly loaded state (wallet already connected).
*
* Structure mirrors:
* section.flex-col.gap-8
* ├── header (chain badge + h1 + subtitle)
* ├── meta-address card (border box with label + truncated address + copy)
* ├── registration card (border box)
* ├── scan button
* ├── search + filter row
* └── match list placeholder (3 rows)
*/
import { SkeletonBlock } from './SkeletonBlock';

export function StellarReceiveSkeleton() {
return (
<section aria-label="Loading receive page" className="flex flex-col gap-8">
{/* ── Header ── */}
<div className="flex flex-col gap-2">
<SkeletonBlock className="h-3 w-40" />
<SkeletonBlock className="h-8 w-28" />
<SkeletonBlock className="h-4 w-80" />
</div>

{/* ── Meta-address card ── */}
<div className="flex flex-col gap-3 border border-outline-variant bg-surface-container p-5">
<div className="flex items-center justify-between">
<SkeletonBlock className="h-3 w-36" />
{/* copy button placeholder */}
<SkeletonBlock className="h-6 w-6" />
</div>
{/* address value – two lines to match actual wrap */}
<SkeletonBlock className="h-4 w-full" />
<SkeletonBlock className="h-4 w-3/4" />
</div>

{/* ── Registration card ── */}
<div className="flex flex-col gap-3 border border-outline-variant bg-surface-container p-5">
<SkeletonBlock className="h-3 w-40" />
<SkeletonBlock className="h-10 w-full" />
</div>

{/* ── Scan button ── */}
<SkeletonBlock className="h-12 w-full" />

{/* ── Search + filter row ── */}
<div className="flex gap-3">
<SkeletonBlock className="h-9 flex-1" />
<SkeletonBlock className="h-9 w-24" />
</div>

{/* ── Match rows (3 placeholders) ── */}
<div className="flex flex-col gap-4">
{[0, 1, 2].map((i) => (
<div key={i} className="flex flex-col gap-2 border border-outline-variant p-4">
<div className="flex items-center justify-between">
<SkeletonBlock className="h-4 w-40" />
<SkeletonBlock className="h-4 w-16" />
</div>
<SkeletonBlock className="h-3 w-full" />
<SkeletonBlock className="h-3 w-2/3" />
</div>
))}
</div>
</section>
);
}
72 changes: 72 additions & 0 deletions src/components/Skeletons/StellarSendSkeleton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/**
* StellarSendSkeleton
*
* Placeholder that mirrors the visible layout of StellarSend / StellarSendView
* so the page never shifts when the real component mounts (CLS ≤ 0.05).
*
* Structure mirrors:
* section.flex-col.gap-8
* ├── header (chain badge + h1)
* ├── wallet-connect button
* ├── form
* │ ├── recipient field (label + input)
* │ ├── asset selector row
* │ ├── amount field (label + input + balance)
* │ └── send button
* └── simulation panel placeholder
*/
import { SkeletonBlock } from './SkeletonBlock';

export function StellarSendSkeleton() {
return (
<section aria-label="Loading send page" className="flex flex-col gap-8">
{/* ── Header ── */}
<div className="flex flex-col gap-2">
{/* chain badge */}
<SkeletonBlock className="h-3 w-40" />
{/* h1 */}
<SkeletonBlock className="h-8 w-24" />
{/* subtitle */}
<SkeletonBlock className="h-4 w-72" />
</div>

{/* ── Wallet-connect area ── */}
<SkeletonBlock className="h-10 w-full" />

{/* ── Form ── */}
<div className="flex flex-col gap-6">
{/* Recipient field */}
<div className="flex flex-col gap-1">
<SkeletonBlock className="h-3 w-28" />
<SkeletonBlock className="h-10 w-full" />
</div>

{/* Asset selector row */}
<div className="flex items-center gap-2">
<SkeletonBlock className="h-10 w-28" />
<SkeletonBlock className="h-3 w-20" />
</div>

{/* Amount field */}
<div className="flex flex-col gap-1">
<div className="flex items-center justify-between">
<SkeletonBlock className="h-3 w-16" />
{/* balance */}
<SkeletonBlock className="h-3 w-24" />
</div>
<SkeletonBlock className="h-10 w-full" />
</div>

{/* Send button */}
<SkeletonBlock className="h-12 w-full" />
</div>

{/* ── Simulation panel ── */}
<div className="flex flex-col gap-2 border border-outline-variant p-4">
<SkeletonBlock className="h-3 w-32" />
<SkeletonBlock className="h-3 w-48" />
<SkeletonBlock className="h-3 w-40" />
</div>
</section>
);
}
Loading
Loading