Skip to content
Open
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
1 change: 1 addition & 0 deletions packages/frappe-ui-react/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export * from "./progress";
export * from "./popover";
export * from "./rating";
export * from "./select";
export * from "./skeleton";
export * from "./sidebar";
export * from "./spinner";
export * from "./switch";
Expand Down
2 changes: 2 additions & 0 deletions packages/frappe-ui-react/src/components/skeleton/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { default as Skeleton } from "./skeleton";
export * from "./skeleton";
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import React from "react";
import type { Meta, StoryObj } from "@storybook/react-vite";
import Skeleton from "./skeleton";

const meta: Meta<typeof Skeleton> = {
title: "Components/Skeleton",
component: Skeleton,
parameters: { docs: { source: { type: "dynamic" } }, layout: "centered" },
decorators: [
(Story) => (
<div
style={{
display: "grid",
gridTemplateColumns: "1fr 1fr",
gap: "2rem",
width: "500px",
}}
>
<Story />
</div>
),
],
argTypes: {
className: {
control: "text",
description: "Custom CSS classes for sizing and styling.",
},
},
tags: ["autodocs"],
};

export default meta;
type Story = StoryObj<typeof Skeleton>;

export const Default: Story = {
render: () => (
<Skeleton className="w-full h-8 shrink-0 rounded-4xl mx-auto" />
),
};

export const Avatar: Story = {
render: () => (
<>
<div className="text-center">
<div className="flex w-fit items-center gap-4">
<Skeleton className="size-10 shrink-0 rounded-full" />
<div className="grid gap-2">
<Skeleton className="h-4 w-[150px]" />
<Skeleton className="h-4 w-[100px]" />
</div>
</div>
</div>
</>
),
};

export const Card: Story = {
render: () => (
<div className="w-[300px] border border-gray-500/10 dark:border-gray-400 gap-4 overflow-hidden rounded-xl py-4 flex flex-col">
<div className="gap-1 px-4 space-y-2 w-full">
<Skeleton className="h-4 w-11/12" />
<Skeleton className="h-4 w-1/3" />
</div>
<div className="px-4">
<Skeleton className="aspect-video w-full" />
</div>
</div>
),
};
13 changes: 13 additions & 0 deletions packages/frappe-ui-react/src/components/skeleton/skeleton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import React from "react";

const Skeleton = ({ className, ...props }: React.ComponentProps<"div">) => {
return (
<div
data-slot="skeleton"
className={`bg-surface-gray-3 dark:bg-surface-gray-4 rounded-md animate-pulse ${className}`}
{...props}
/>
);
};

export default Skeleton;
50 changes: 50 additions & 0 deletions packages/frappe-ui-react/src/components/skeleton/tests/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import React from "react";
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

React is imported but not used in this test file (other component tests rely on the automatic JSX runtime and omit this import). Consider removing it to avoid unused-import lint failures.

Suggested change
import React from "react";

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Without the react import the ts compiler throws this error 'React' refers to a UMD global, but the current file is a module. Consider adding an import instead.ts(2686) even though ts config has jsx property to be react-jsx

import { render } from "@testing-library/react";
import "@testing-library/jest-dom";

import Skeleton from "../skeleton";

describe("Skeleton Component", () => {
it("renders without crashing", () => {
const { container } = render(<Skeleton />);
expect(container.firstChild).toBeInTheDocument();
});

it("renders a div element", () => {
const { container } = render(<Skeleton />);
expect(container.firstChild?.nodeName).toBe("DIV");
});

it("has the data-slot attribute set to skeleton", () => {
const { container } = render(<Skeleton />);
expect(container.firstChild).toHaveAttribute("data-slot", "skeleton");
});

it("applies default classes for animation and background", () => {
const { container } = render(<Skeleton />);
expect(container.firstChild).toHaveClass("animate-pulse");
expect(container.firstChild).toHaveClass("bg-surface-gray-3");
expect(container.firstChild).toHaveClass("dark:bg-surface-gray-4");
expect(container.firstChild).toHaveClass("rounded-md");
});

it("merges custom className with default classes", () => {
const { container } = render(<Skeleton className="h-4 w-32" />);
expect(container.firstChild).toHaveClass("animate-pulse");
expect(container.firstChild).toHaveClass("h-4");
expect(container.firstChild).toHaveClass("w-32");
});

it("forwards additional HTML div props", () => {
const { container } = render(
<Skeleton aria-label="loading" data-testid="skeleton-el" />
);
expect(container.firstChild).toHaveAttribute("aria-label", "loading");
expect(container.firstChild).toHaveAttribute("data-testid", "skeleton-el");
});

it("renders children when provided", () => {
const { getByText } = render(<Skeleton>Loading...</Skeleton>);
expect(getByText("Loading...")).toBeInTheDocument();
});
});