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}