diff --git a/src/App.tsx b/src/App.tsx index e5f5916..51e55c7 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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 ( @@ -17,12 +29,63 @@ export function App() { - } /> - } /> - } /> - } /> - } /> - } /> + }> + + + } + /> + }> + + + } + /> + }> + + + } + /> + }> + + + } + /> + }> + + + } + /> + }> + + + } + /> + {/* /pay is a payment-link entry point — reuses the Send chunk */} + }> + + + } + /> } /> diff --git a/src/components/Skeletons/SkeletonBlock.tsx b/src/components/Skeletons/SkeletonBlock.tsx new file mode 100644 index 0000000..5887a8a --- /dev/null +++ b/src/components/Skeletons/SkeletonBlock.tsx @@ -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 { + /** Extra Tailwind classes, e.g. "w-32 h-4" */ + className?: string; +} + +export function SkeletonBlock({ className = '', ...rest }: SkeletonBlockProps) { + return ( + + ); +} diff --git a/src/components/Skeletons/StellarActivitySkeleton.tsx b/src/components/Skeletons/StellarActivitySkeleton.tsx new file mode 100644 index 0000000..bc1871c --- /dev/null +++ b/src/components/Skeletons/StellarActivitySkeleton.tsx @@ -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 ( + + {/* ── Header row ── */} + + + + + + {/* Clear history button */} + + + + {/* ── Filter row ── */} + + + + + + + + + + + + {/* ── Activity rows (5 placeholders) ── */} + + {[0, 1, 2, 3, 4].map((i) => ( + + + {/* kind badge */} + + {/* address */} + + {/* timestamp */} + + + + {/* amount */} + + {/* status chip */} + + + + ))} + + + ); +} diff --git a/src/components/Skeletons/StellarReceiveSkeleton.tsx b/src/components/Skeletons/StellarReceiveSkeleton.tsx new file mode 100644 index 0000000..6d149b4 --- /dev/null +++ b/src/components/Skeletons/StellarReceiveSkeleton.tsx @@ -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 ( + + {/* ── Header ── */} + + + + + + + {/* ── Meta-address card ── */} + + + + {/* copy button placeholder */} + + + {/* address value – two lines to match actual wrap */} + + + + + {/* ── Registration card ── */} + + + + + + {/* ── Scan button ── */} + + + {/* ── Search + filter row ── */} + + + + + + {/* ── Match rows (3 placeholders) ── */} + + {[0, 1, 2].map((i) => ( + + + + + + + + + ))} + + + ); +} diff --git a/src/components/Skeletons/StellarSendSkeleton.tsx b/src/components/Skeletons/StellarSendSkeleton.tsx new file mode 100644 index 0000000..3c41185 --- /dev/null +++ b/src/components/Skeletons/StellarSendSkeleton.tsx @@ -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 ( + + {/* ── Header ── */} + + {/* chain badge */} + + {/* h1 */} + + {/* subtitle */} + + + + {/* ── Wallet-connect area ── */} + + + {/* ── Form ── */} + + {/* Recipient field */} + + + + + + {/* Asset selector row */} + + + + + + {/* Amount field */} + + + + {/* balance */} + + + + + + {/* Send button */} + + + + {/* ── Simulation panel ── */} + + + + + + + ); +} diff --git a/src/components/Skeletons/StellarSettingsSkeleton.tsx b/src/components/Skeletons/StellarSettingsSkeleton.tsx new file mode 100644 index 0000000..b427334 --- /dev/null +++ b/src/components/Skeletons/StellarSettingsSkeleton.tsx @@ -0,0 +1,89 @@ +/** + * StellarSettingsSkeleton + * + * Mirrors the Schedule page layout (the closest "Settings"-like page in this + * app — recurring-payment configuration). + * + * Structure mirrors: + * div.flex-col.gap-8 + * ├── header (h1 + description) + * ├── create form + * │ ├── recipient input + * │ ├── amount + asset row + * │ ├── interval select + * │ ├── end-date input + * │ └── submit button + * └── schedule list (3 row placeholders) + */ +import { SkeletonBlock } from './SkeletonBlock'; + +export function StellarSettingsSkeleton() { + return ( + + {/* ── Header ── */} + + + + + + + {/* ── Create form ── */} + + + + {/* Recipient */} + + + + + + {/* Amount + asset row */} + + + + + + + + + + + + {/* Interval */} + + + + + + {/* End date */} + + + + + + {/* Submit */} + + + + {/* ── Schedule list (3 placeholders) ── */} + + {[0, 1, 2].map((i) => ( + + + + + + + + + + + + ))} + + + ); +} diff --git a/src/components/Skeletons/StellarWithdrawSkeleton.tsx b/src/components/Skeletons/StellarWithdrawSkeleton.tsx new file mode 100644 index 0000000..63bf6ae --- /dev/null +++ b/src/components/Skeletons/StellarWithdrawSkeleton.tsx @@ -0,0 +1,60 @@ +/** + * StellarWithdrawSkeleton + * + * Mirrors the connected state of StellarVault (deposit / claim / status tabs). + * + * Structure mirrors: + * section.flex-col.gap-8 + * ├── header (chain badge + h1 + subtitle) + * ├── tab bar (3 tabs) + * └── active tab content (deposit form — the default tab) + * ├── label + input (unlock timestamp) + * ├── label + input (amount) + * ├── label + input (recipient) + * └── submit button + */ +import { SkeletonBlock } from './SkeletonBlock'; + +export function StellarWithdrawSkeleton() { + return ( + + {/* ── Header ── */} + + + + + + + {/* ── Tab bar ── */} + + {['w-28', 'w-16', 'w-20'].map((w, i) => ( + + ))} + + + {/* ── Deposit form (default tab) ── */} + + {/* Unlock timestamp */} + + + + + + {/* Amount */} + + + + + + {/* Recipient */} + + + + + + {/* Submit */} + + + + ); +} diff --git a/src/components/Skeletons/index.ts b/src/components/Skeletons/index.ts new file mode 100644 index 0000000..17e902c --- /dev/null +++ b/src/components/Skeletons/index.ts @@ -0,0 +1,6 @@ +export { SkeletonBlock } from './SkeletonBlock'; +export { StellarSendSkeleton } from './StellarSendSkeleton'; +export { StellarReceiveSkeleton } from './StellarReceiveSkeleton'; +export { StellarWithdrawSkeleton } from './StellarWithdrawSkeleton'; +export { StellarActivitySkeleton } from './StellarActivitySkeleton'; +export { StellarSettingsSkeleton } from './StellarSettingsSkeleton'; diff --git a/src/index.css b/src/index.css index ea4b762..c473fcd 100644 --- a/src/index.css +++ b/src/index.css @@ -77,6 +77,28 @@ input:focus { background: var(--color-scrollbar-thumb); } +/* ── Skeleton shimmer animation ────────────────────────────────────────── */ +@keyframes skeleton-shimmer { + 0% { + opacity: 1; + } + 50% { + opacity: 0.4; + } + 100% { + opacity: 1; + } +} + +.animate-skeleton { + animation: skeleton-shimmer 1.5s ease-in-out infinite; + /* Respect user motion preferences */ + @media (prefers-reduced-motion: reduce) { + animation: none; + } +} +/* ──────────────────────────────────────────────────────────────────────── */ + /* Solana Wallet Adapter overrides */ .wallet-adapter-button { background-color: transparent !important;