Skip to content
Merged
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
38 changes: 38 additions & 0 deletions src/components/index.ts
Original file line number Diff line number Diff line change
@@ -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 (
* <SorokitProvider>
* <SorobanPanel />
* </SorokitProvider>
* );
* }
* ```
*/

// 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
Expand Down
33 changes: 33 additions & 0 deletions src/components/ui/Skeleton.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,24 @@ describe("Skeleton", () => {
const { container } = render(<Skeleton circle />);
expect(container.firstElementChild).toHaveClass("rounded-full");
});

it("uses animate-pulse by default (no variant prop)", () => {
const { container } = render(<Skeleton />);
expect(container.firstElementChild).toHaveClass("animate-pulse");
});

it("applies skeleton-shimmer class when variant='shimmer'", () => {
const { container } = render(<Skeleton variant="shimmer" />);
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(<Skeleton variant="pulse" />);
expect(container.firstElementChild).toHaveClass("animate-pulse");
expect(container.firstElementChild).not.toHaveClass("skeleton-shimmer");
});
});

describe("SkeletonRow", () => {
Expand All @@ -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(<SkeletonCard rows={3} />);
const before = Array.from(
container.querySelectorAll('[role="presentation"]'),
).map((el) => el.getAttribute("data-key"));

rerender(<SkeletonCard rows={2} />);
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", () => {
Expand Down
13 changes: 10 additions & 3 deletions src/components/ui/Skeleton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,22 @@ import { cn } from "@/lib/utils";
interface SkeletonProps extends React.HTMLAttributes<HTMLDivElement> {
/** 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 (
<div
role="presentation"
className={cn(
"bg-surface-2 animate-pulse shrink-0",
"bg-surface-2 shrink-0",
circle ? "rounded-full" : "rounded-lg",
variant === "pulse" ? "animate-pulse" : "skeleton-shimmer",
className,
)}
{...props}
Expand Down Expand Up @@ -96,7 +103,7 @@ export function SkeletonCard({
</div>
<div className="px-5 py-5 flex flex-col gap-4">
{Array.from({ length: rows }).map((_, i) => (
<Skeleton key={i} className="h-4 w-full" />
<Skeleton key={`skeleton-row-${rows}-${i}`} className="h-4 w-full" />
))}
</div>
</>
Expand Down
25 changes: 25 additions & 0 deletions src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down