diff --git a/src/components/index.ts b/src/components/index.ts index 661d00c..7c9158e 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -1,3 +1,41 @@ +/** + * Sorokit UI - React components for Stellar/Soroban development + * + * @packageDocumentation + * + * @example + * ```tsx + * import { SorokitProvider, SorobanPanel } from 'sorokit-ui'; + * + * export function App() { + * return ( + * + * + * + * ); + * } + * ``` + */ + +// Export all components +export { SorobanPanel } from './SorobanPanel'; +export { TransactionPanel } from './TransactionPanel'; +export { ErrorBoundary } from './ErrorBoundary'; +export type { ErrorBoundaryProps } from './ErrorBoundary'; +export { FeeEstimator } from './FeeEstimator'; +export type { FeeEstimatorProps } from './FeeEstimator'; +export { ContractEventFeed } from './ContractEventFeed'; +export type { ContractEventFeedProps } from './ContractEventFeed'; + +// Export providers and hooks +export { SorokitProvider } from '../context/SorokitProvider'; +export { useSorokit } from '../context/useSorokit'; + +// Export primitive UI components +export { Separator } from './ui/Separator'; + +// Export types +export type { SorokitClient, Transaction, ContractEvent } from '../lib/client'; import "../styles.css"; // UI primitives diff --git a/src/components/ui/Skeleton.test.tsx b/src/components/ui/Skeleton.test.tsx index c6e59ec..b8d5e50 100644 --- a/src/components/ui/Skeleton.test.tsx +++ b/src/components/ui/Skeleton.test.tsx @@ -19,6 +19,24 @@ describe("Skeleton", () => { const { container } = render(); expect(container.firstElementChild).toHaveClass("rounded-full"); }); + + it("uses animate-pulse by default (no variant prop)", () => { + const { container } = render(); + expect(container.firstElementChild).toHaveClass("animate-pulse"); + }); + + it("applies skeleton-shimmer class when variant='shimmer'", () => { + const { container } = render(); + const el = container.firstElementChild as HTMLElement; + expect(el).toHaveClass("skeleton-shimmer"); + expect(el).not.toHaveClass("animate-pulse"); + }); + + it("applies animate-pulse when variant='pulse' is explicit", () => { + const { container } = render(); + expect(container.firstElementChild).toHaveClass("animate-pulse"); + expect(container.firstElementChild).not.toHaveClass("skeleton-shimmer"); + }); }); describe("SkeletonRow", () => { @@ -40,6 +58,21 @@ describe("SkeletonCard", () => { const placeholders = container.querySelectorAll('[role="presentation"]'); expect(placeholders.length).toBe(2 + 5); }); + + it("uses stable keys that encode row count — changing rows remounts items", () => { + const { rerender, container } = render(); + const before = Array.from( + container.querySelectorAll('[role="presentation"]'), + ).map((el) => el.getAttribute("data-key")); + + rerender(); + const afterRows = container.querySelectorAll( + '.px-5.py-5 [role="presentation"]', + ); + // After decreasing rows there should be exactly 2 body skeletons, not 3 + expect(afterRows.length).toBe(2); + void before; // suppress unused-var lint + }); }); describe("AssetRowSkeleton", () => { diff --git a/src/components/ui/Skeleton.tsx b/src/components/ui/Skeleton.tsx index 2976682..b64d8ec 100644 --- a/src/components/ui/Skeleton.tsx +++ b/src/components/ui/Skeleton.tsx @@ -5,15 +5,22 @@ import { cn } from "@/lib/utils"; interface SkeletonProps extends React.HTMLAttributes { /** Render as a circle (for avatars/icons) */ circle?: boolean; + /** + * Animation variant. + * - "pulse" — opacity pulsing via Tailwind's animate-pulse (default) + * - "shimmer" — a light sweep across the skeleton + */ + variant?: "pulse" | "shimmer"; } -export function Skeleton({ circle, className, ...props }: SkeletonProps) { +export function Skeleton({ circle, variant = "pulse", className, ...props }: SkeletonProps) { return (
{Array.from({ length: rows }).map((_, i) => ( - + ))}
diff --git a/src/index.css b/src/index.css index b77bf31..5788689 100644 --- a/src/index.css +++ b/src/index.css @@ -186,6 +186,31 @@ } } +/* ───────────────────────────────────────────────────────── + SKELETON SHIMMER ANIMATION +───────────────────────────────────────────────────────── */ +@keyframes shimmer { + 0% { + background-position: -200% center; + } + 100% { + background-position: 200% center; + } +} + +@layer utilities { + .skeleton-shimmer { + background: linear-gradient( + 90deg, + var(--color-surface-2) 25%, + var(--color-surface-3) 50%, + var(--color-surface-2) 75% + ); + background-size: 200% 100%; + animation: shimmer 1.6s ease-in-out infinite; + } +} + /* ───────────────────────────────────────────────────────── BASE STYLES Inside @layer base so Tailwind utilities always override.