diff --git a/src/components/ui/Skeleton.test.tsx b/src/components/ui/Skeleton.test.tsx index b8d5e50..bb38b50 100644 --- a/src/components/ui/Skeleton.test.tsx +++ b/src/components/ui/Skeleton.test.tsx @@ -20,6 +20,22 @@ describe("Skeleton", () => { expect(container.firstElementChild).toHaveClass("rounded-full"); }); + it("applies rounded-none when shape='square'", () => { + const { container } = render(); + expect(container.firstElementChild).toHaveClass("rounded-none"); + }); + + it("applies rounded-full when shape='circle'", () => { + const { container } = render(); + expect(container.firstElementChild).toHaveClass("rounded-full"); + }); + + it("applies rounded-lg when shape='rounded' or default", () => { + const { container: c1 } = render(); + expect(c1.firstElementChild).toHaveClass("rounded-lg"); + + const { container: c2 } = render(); + expect(c2.firstElementChild).toHaveClass("rounded-lg"); it("uses animate-pulse by default (no variant prop)", () => { const { container } = render(); expect(container.firstElementChild).toHaveClass("animate-pulse"); @@ -44,6 +60,14 @@ describe("SkeletonRow", () => { const { container } = render(); expect(container.firstElementChild).toHaveAttribute("role", "presentation"); }); + + it("renders multiple rows when count prop is provided", () => { + const { container } = render(); + const rows = container.querySelectorAll('[role="presentation"]'); + // Each SkeletonRow contains 1 wrapper div + 3 internal Skeletons = 4 presentational divs per row + // Or querying top-level children / row divs: + expect(container.children.length).toBe(5); + }); }); describe("SkeletonCard", () => { @@ -59,6 +83,15 @@ describe("SkeletonCard", () => { expect(placeholders.length).toBe(2 + 5); }); + it("renders custom header slot when header prop is provided", () => { + const customHeader = ( +
+ + +
+ ); + render(); + expect(screen.getByTestId("custom-card-header")).toBeInTheDocument(); it("uses stable keys that encode row count — changing rows remounts items", () => { const { rerender, container } = render(); const before = Array.from( diff --git a/src/components/ui/Skeleton.tsx b/src/components/ui/Skeleton.tsx index 8c5e53d..f1fb943 100644 --- a/src/components/ui/Skeleton.tsx +++ b/src/components/ui/Skeleton.tsx @@ -2,9 +2,23 @@ import React from "react"; import { cn } from "@/lib/utils"; -interface SkeletonProps extends React.HTMLAttributes { - /** Render as a circle (for avatars/icons) */ +export type SkeletonShape = "rounded" | "circle" | "square"; + +export interface SkeletonProps extends React.HTMLAttributes { + /** Render as a circle (for avatars/icons) - shorthand for shape="circle" */ circle?: boolean; + /** Shape variant: rounded (default), circle, or square */ + shape?: SkeletonShape; +} + +export function Skeleton({ circle, shape, className, ...props }: SkeletonProps) { + const roundedClass = + shape === "circle" || circle + ? "rounded-full" + : shape === "square" + ? "rounded-none" + : "rounded-lg"; + /** Shape variant for non-circular placeholders. */ shape?: "rounded" | "circle" | "square"; /** @@ -21,6 +35,8 @@ export function Skeleton({ circle, shape = "rounded", variant = "pulse", classNa
{ + /** Number of skeleton rows to render */ interface SkeletonRowProps extends React.HTMLAttributes { count?: number; } /** Pre-composed row skeleton: icon + two lines of text */ +export function SkeletonRow({ count, className, ...props }: SkeletonRowProps) { + if (count !== undefined && count > 1) { + return ( + <> + {Array.from({ length: count }).map((_, i) => ( +
+ +
+ + +
+
+ ))} + + ); + } + + return ( +
+ +
+ + +
export function SkeletonRow({ className, count = 1, ...props }: SkeletonRowProps) { return (
{ + /** Number of body rows to render */ + rows?: number; + /** Custom header slot; defaults to standard 2-line header skeleton */ + header?: React.ReactNode; +} + /** Pre-composed card skeleton: header + body lines */ export function SkeletonCard({ rows = 3, @@ -106,6 +160,25 @@ export function SkeletonCard({
+ {header !== undefined ? ( + header + ) : ( +
+ + +
+ )} +
+ {Array.from({ length: rows }).map((_, i) => ( + + ))} +
aria-label="Loading content" className={cn("rounded-xl border border-line bg-surface overflow-hidden", className)} {...props}