From c5775ebc901e12dcb4fb1e68713e8458b24ddd9b Mon Sep 17 00:00:00 2001 From: Dhaval Vira Date: Sun, 31 Mar 2024 08:50:15 +0530 Subject: [PATCH 1/2] feat(skeleton): added Skeleton Loading with Examples --- apps/web/content/docs/components/skeleton.mdx | 76 +++++++++++++++++++ apps/web/data/components.tsx | 7 ++ apps/web/data/docs-sidebar.ts | 1 + apps/web/examples/index.ts | 1 + apps/web/examples/skeleton/index.ts | 9 +++ apps/web/examples/skeleton/skeleton.card.tsx | 54 +++++++++++++ .../examples/skeleton/skeleton.circular.tsx | 54 +++++++++++++ apps/web/examples/skeleton/skeleton.image.tsx | 54 +++++++++++++ apps/web/examples/skeleton/skeleton.list.tsx | 54 +++++++++++++ .../skeleton/skeleton.rectangular.tsx | 54 +++++++++++++ apps/web/examples/skeleton/skeleton.root.tsx | 54 +++++++++++++ .../examples/skeleton/skeleton.rounded.tsx | 54 +++++++++++++ .../skeleton/skeleton.testimonial.tsx | 54 +++++++++++++ apps/web/examples/skeleton/skeleton.video.tsx | 54 +++++++++++++ .../images/components/skeleton-dark.svg | 11 +++ .../web/public/images/components/skeleton.svg | 11 +++ .../src/components/Flowbite/FlowbiteTheme.ts | 2 + .../src/components/Skeleton/Skeleton.spec.tsx | 57 ++++++++++++++ .../components/Skeleton/Skeleton.stories.tsx | 70 +++++++++++++++++ .../ui/src/components/Skeleton/Skeleton.tsx | 68 +++++++++++++++++ .../src/components/Skeleton/SkeletonCard.tsx | 64 ++++++++++++++++ .../src/components/Skeleton/SkeletonImage.tsx | 55 ++++++++++++++ .../src/components/Skeleton/SkeletonList.tsx | 53 +++++++++++++ .../Skeleton/SkeletonTestimonial.tsx | 49 ++++++++++++ .../src/components/Skeleton/SkeletonVideo.tsx | 35 +++++++++ packages/ui/src/components/Skeleton/index.ts | 17 +++++ packages/ui/src/components/Skeleton/theme.ts | 71 +++++++++++++++++ packages/ui/src/index.ts | 1 + packages/ui/src/theme.ts | 2 + 29 files changed, 1146 insertions(+) create mode 100644 apps/web/content/docs/components/skeleton.mdx create mode 100644 apps/web/examples/skeleton/index.ts create mode 100644 apps/web/examples/skeleton/skeleton.card.tsx create mode 100644 apps/web/examples/skeleton/skeleton.circular.tsx create mode 100644 apps/web/examples/skeleton/skeleton.image.tsx create mode 100644 apps/web/examples/skeleton/skeleton.list.tsx create mode 100644 apps/web/examples/skeleton/skeleton.rectangular.tsx create mode 100644 apps/web/examples/skeleton/skeleton.root.tsx create mode 100644 apps/web/examples/skeleton/skeleton.rounded.tsx create mode 100644 apps/web/examples/skeleton/skeleton.testimonial.tsx create mode 100644 apps/web/examples/skeleton/skeleton.video.tsx create mode 100644 apps/web/public/images/components/skeleton-dark.svg create mode 100644 apps/web/public/images/components/skeleton.svg create mode 100644 packages/ui/src/components/Skeleton/Skeleton.spec.tsx create mode 100644 packages/ui/src/components/Skeleton/Skeleton.stories.tsx create mode 100644 packages/ui/src/components/Skeleton/Skeleton.tsx create mode 100644 packages/ui/src/components/Skeleton/SkeletonCard.tsx create mode 100644 packages/ui/src/components/Skeleton/SkeletonImage.tsx create mode 100644 packages/ui/src/components/Skeleton/SkeletonList.tsx create mode 100644 packages/ui/src/components/Skeleton/SkeletonTestimonial.tsx create mode 100644 packages/ui/src/components/Skeleton/SkeletonVideo.tsx create mode 100644 packages/ui/src/components/Skeleton/index.ts create mode 100644 packages/ui/src/components/Skeleton/theme.ts diff --git a/apps/web/content/docs/components/skeleton.mdx b/apps/web/content/docs/components/skeleton.mdx new file mode 100644 index 000000000..ee1710682 --- /dev/null +++ b/apps/web/content/docs/components/skeleton.mdx @@ -0,0 +1,76 @@ +--- +title: React Skeleton - Flowbite +description: The skeleton component can be used as an alternative loading indicator to the spinner by mimicking the content that will be loaded such as text, images, or video +--- + +Use the skeleton component to indicate a loading status with placeholder elements that look very similar to the type of content that is being loaded such as paragraphs, heading, images, videos, and more. + +You can set the width and height of these skeleton components based on the size of the content and element that it is being loaded in, such as a card or an article page. + +Start using the skeleton component by importing it from the `flowbite-react` library: + +```jsx +import { Skeleton } from "flowbite-react"; +``` + +# Variants + +## Default + +Represents a single line of text. + + + +## Circular + + + +## Rectangular + + + +## Rounded + + + +# Examples + +## Image placeholder + +This example can be used to show a placeholder when loading an image and text content. + + + +## Video placeholder + +Use this example to show a skeleton placeholder when loading video content. + + + +## Card placeholder + +Use this example to show a placeholder when loading content inside a card. + + + +## List placeholder + +Use this example to show a placeholder when loading a list of items. + + + +## Testimonial placeholder + +Use this example to show a placeholder when loading a list of items. + + + +# Theme + +To learn more about how to customize the appearance of components, please see the [Theme docs](/docs/customize/theme). + + + +# References + +- [Flowbite Skeleton](https://flowbite.com/docs/components/skeleton/) diff --git a/apps/web/data/components.tsx b/apps/web/data/components.tsx index 29e7e8619..d73368c65 100644 --- a/apps/web/data/components.tsx +++ b/apps/web/data/components.tsx @@ -140,6 +140,13 @@ export const COMPONENTS_DATA: Component[] = [ link: "/docs/components/sidebar", classes: "w-16", }, + { + name: "Skeleton", + image: "/images/components/skeleton.svg", + imageDark: "/images/components/skeleton-dark.svg", + link: "/docs/components/skeleton", + classes: "w-48", + }, { name: "Pagination", image: "/images/components/pagination.svg", diff --git a/apps/web/data/docs-sidebar.ts b/apps/web/data/docs-sidebar.ts index 60c49560b..6f8670e76 100644 --- a/apps/web/data/docs-sidebar.ts +++ b/apps/web/data/docs-sidebar.ts @@ -76,6 +76,7 @@ export const DOCS_SIDEBAR: DocsSidebarSection[] = [ { title: "Progress bar", href: "/docs/components/progress" }, { title: "Rating", href: "/docs/components/rating" }, { title: "Sidebar", href: "/docs/components/sidebar" }, + { title: "Skeleton", href: "/docs/components/skeleton", isNew: true }, { title: "Spinner", href: "/docs/components/spinner" }, { title: "Table", href: "/docs/components/table" }, { title: "Tabs", href: "/docs/components/tabs" }, diff --git a/apps/web/examples/index.ts b/apps/web/examples/index.ts index 7992d8e29..f7cb3220f 100644 --- a/apps/web/examples/index.ts +++ b/apps/web/examples/index.ts @@ -25,6 +25,7 @@ export * as popover from "./popover"; export * as progress from "./progress"; export * as rating from "./rating"; export * as sidebar from "./sidebar"; +export * as skeleton from "./skeleton"; export * as spinner from "./spinner"; export * as table from "./table"; export * as tabs from "./tabs"; diff --git a/apps/web/examples/skeleton/index.ts b/apps/web/examples/skeleton/index.ts new file mode 100644 index 000000000..2a9ad42c2 --- /dev/null +++ b/apps/web/examples/skeleton/index.ts @@ -0,0 +1,9 @@ +export { card } from "./skeleton.card"; +export { circular } from "./skeleton.circular"; +export { image } from "./skeleton.image"; +export { list } from "./skeleton.list"; +export { rectangular } from "./skeleton.rectangular"; +export { rounded } from "./skeleton.rounded"; +export { root } from "./skeleton.root"; +export { testimonial } from "./skeleton.testimonial"; +export { video } from "./skeleton.video"; diff --git a/apps/web/examples/skeleton/skeleton.card.tsx b/apps/web/examples/skeleton/skeleton.card.tsx new file mode 100644 index 000000000..d264b77c6 --- /dev/null +++ b/apps/web/examples/skeleton/skeleton.card.tsx @@ -0,0 +1,54 @@ +import { SkeletonCard } from "flowbite-react"; +import type { CodeData } from "~/components/code-demo"; + +const code = ` +'use client'; + +import { Skeleton } from "flowbite-react"; + +function Component() { + return ( +
+ +
+ ) +} +`; + +const codeRSC = ` +import { SkeletonCard } from "flowbite-react"; + +function Component() { + return ( +
+ +
+ ) +} +`; + +function Component() { + return ( +
+ +
+ ); +} + +export const card: CodeData = { + type: "single", + code: [ + { + fileName: "client", + language: "tsx", + code, + }, + { + fileName: "server", + language: "tsx", + code: codeRSC, + }, + ], + githubSlug: "skeleton/skeleton.card.tsx", + component: , +}; diff --git a/apps/web/examples/skeleton/skeleton.circular.tsx b/apps/web/examples/skeleton/skeleton.circular.tsx new file mode 100644 index 000000000..fe17b2789 --- /dev/null +++ b/apps/web/examples/skeleton/skeleton.circular.tsx @@ -0,0 +1,54 @@ +import { Skeleton } from "flowbite-react"; +import type { CodeData } from "~/components/code-demo"; + +const code = ` +'use client'; + +import { Skeleton } from "flowbite-react"; + +function Component() { + return ( +
+ +
+ ) +} +`; + +const codeRSC = ` +import { Skeleton } from "flowbite-react"; + +function Component() { + return ( +
+ +
+ ) +} +`; + +function Component() { + return ( +
+ +
+ ); +} + +export const circular: CodeData = { + type: "single", + code: [ + { + fileName: "client", + language: "tsx", + code, + }, + { + fileName: "server", + language: "tsx", + code: codeRSC, + }, + ], + githubSlug: "skeleton/skeleton.circular.tsx", + component: , +}; diff --git a/apps/web/examples/skeleton/skeleton.image.tsx b/apps/web/examples/skeleton/skeleton.image.tsx new file mode 100644 index 000000000..816a9acbd --- /dev/null +++ b/apps/web/examples/skeleton/skeleton.image.tsx @@ -0,0 +1,54 @@ +import { SkeletonImage } from "flowbite-react"; +import type { CodeData } from "~/components/code-demo"; + +const code = ` +'use client'; + +import { Skeleton } from "flowbite-react"; + +function Component() { + return ( +
+ +
+ ) +} +`; + +const codeRSC = ` +import { SkeletonImage } from "flowbite-react"; + +function Component() { + return ( +
+ +
+ ) +} +`; + +function Component() { + return ( +
+ +
+ ); +} + +export const image: CodeData = { + type: "single", + code: [ + { + fileName: "client", + language: "tsx", + code, + }, + { + fileName: "server", + language: "tsx", + code: codeRSC, + }, + ], + githubSlug: "skeleton/skeleton.image.tsx", + component: , +}; diff --git a/apps/web/examples/skeleton/skeleton.list.tsx b/apps/web/examples/skeleton/skeleton.list.tsx new file mode 100644 index 000000000..084b6276e --- /dev/null +++ b/apps/web/examples/skeleton/skeleton.list.tsx @@ -0,0 +1,54 @@ +import { SkeletonList } from "flowbite-react"; +import type { CodeData } from "~/components/code-demo"; + +const code = ` +'use client'; + +import { Skeleton } from "flowbite-react"; + +function Component() { + return ( +
+ +
+ ) +} +`; + +const codeRSC = ` +import { SkeletonList } from "flowbite-react"; + +function Component() { + return ( +
+ +
+ ) +} +`; + +function Component() { + return ( +
+ +
+ ); +} + +export const list: CodeData = { + type: "single", + code: [ + { + fileName: "client", + language: "tsx", + code, + }, + { + fileName: "server", + language: "tsx", + code: codeRSC, + }, + ], + githubSlug: "skeleton/skeleton.list.tsx", + component: , +}; diff --git a/apps/web/examples/skeleton/skeleton.rectangular.tsx b/apps/web/examples/skeleton/skeleton.rectangular.tsx new file mode 100644 index 000000000..bde99a411 --- /dev/null +++ b/apps/web/examples/skeleton/skeleton.rectangular.tsx @@ -0,0 +1,54 @@ +import { Skeleton } from "flowbite-react"; +import type { CodeData } from "~/components/code-demo"; + +const code = ` +'use client'; + +import { Skeleton } from "flowbite-react"; + +function Component() { + return ( +
+ +
+ ) +} +`; + +const codeRSC = ` +import { Skeleton } from "flowbite-react"; + +function Component() { + return ( +
+ +
+ ) +} +`; + +function Component() { + return ( +
+ +
+ ); +} + +export const rectangular: CodeData = { + type: "single", + code: [ + { + fileName: "client", + language: "tsx", + code, + }, + { + fileName: "server", + language: "tsx", + code: codeRSC, + }, + ], + githubSlug: "skeleton/skeleton.rectangular.tsx", + component: , +}; diff --git a/apps/web/examples/skeleton/skeleton.root.tsx b/apps/web/examples/skeleton/skeleton.root.tsx new file mode 100644 index 000000000..ebf6b3729 --- /dev/null +++ b/apps/web/examples/skeleton/skeleton.root.tsx @@ -0,0 +1,54 @@ +import { Skeleton } from "flowbite-react"; +import type { CodeData } from "~/components/code-demo"; + +const code = ` +'use client'; + +import { Skeleton } from "flowbite-react"; + +function Component() { + return ( +
+ +
+ ) +} +`; + +const codeRSC = ` +import { Skeleton } from "flowbite-react"; + +function Component() { + return ( +
+ +
+ ) +} +`; + +function Component() { + return ( +
+ +
+ ); +} + +export const root: CodeData = { + type: "single", + code: [ + { + fileName: "client", + language: "tsx", + code, + }, + { + fileName: "server", + language: "tsx", + code: codeRSC, + }, + ], + githubSlug: "skeleton/skeleton.root.tsx", + component: , +}; diff --git a/apps/web/examples/skeleton/skeleton.rounded.tsx b/apps/web/examples/skeleton/skeleton.rounded.tsx new file mode 100644 index 000000000..48e541eb7 --- /dev/null +++ b/apps/web/examples/skeleton/skeleton.rounded.tsx @@ -0,0 +1,54 @@ +import { Skeleton } from "flowbite-react"; +import type { CodeData } from "~/components/code-demo"; + +const code = ` +'use client'; + +import { Skeleton } from "flowbite-react"; + +function Component() { + return ( +
+ +
+ ) +} +`; + +const codeRSC = ` +import { Skeleton } from "flowbite-react"; + +function Component() { + return ( +
+ +
+ ) +} +`; + +function Component() { + return ( +
+ +
+ ); +} + +export const rounded: CodeData = { + type: "single", + code: [ + { + fileName: "client", + language: "tsx", + code, + }, + { + fileName: "server", + language: "tsx", + code: codeRSC, + }, + ], + githubSlug: "skeleton/skeleton.rounded.tsx", + component: , +}; diff --git a/apps/web/examples/skeleton/skeleton.testimonial.tsx b/apps/web/examples/skeleton/skeleton.testimonial.tsx new file mode 100644 index 000000000..d138018bc --- /dev/null +++ b/apps/web/examples/skeleton/skeleton.testimonial.tsx @@ -0,0 +1,54 @@ +import { SkeletonTestimonial } from "flowbite-react"; +import type { CodeData } from "~/components/code-demo"; + +const code = ` +'use client'; + +import { Skeleton } from "flowbite-react"; + +function Component() { + return ( +
+ +
+ ) +} +`; + +const codeRSC = ` +import { SkeletonTestimonial } from "flowbite-react"; + +function Component() { + return ( +
+ +
+ ) +} +`; + +function Component() { + return ( +
+ +
+ ); +} + +export const testimonial: CodeData = { + type: "single", + code: [ + { + fileName: "client", + language: "tsx", + code, + }, + { + fileName: "server", + language: "tsx", + code: codeRSC, + }, + ], + githubSlug: "skeleton/skeleton.testimonial.tsx", + component: , +}; diff --git a/apps/web/examples/skeleton/skeleton.video.tsx b/apps/web/examples/skeleton/skeleton.video.tsx new file mode 100644 index 000000000..e73aa1f0e --- /dev/null +++ b/apps/web/examples/skeleton/skeleton.video.tsx @@ -0,0 +1,54 @@ +import { SkeletonVideo } from "flowbite-react"; +import type { CodeData } from "~/components/code-demo"; + +const code = ` +'use client'; + +import { Skeleton } from "flowbite-react"; + +function Component() { + return ( +
+ +
+ ) +} +`; + +const codeRSC = ` +import { SkeletonVideo } from "flowbite-react"; + +function Component() { + return ( +
+ +
+ ) +} +`; + +function Component() { + return ( +
+ +
+ ); +} + +export const video: CodeData = { + type: "single", + code: [ + { + fileName: "client", + language: "tsx", + code, + }, + { + fileName: "server", + language: "tsx", + code: codeRSC, + }, + ], + githubSlug: "skeleton/skeleton.video.tsx", + component: , +}; diff --git a/apps/web/public/images/components/skeleton-dark.svg b/apps/web/public/images/components/skeleton-dark.svg new file mode 100644 index 000000000..c8a9d4955 --- /dev/null +++ b/apps/web/public/images/components/skeleton-dark.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/apps/web/public/images/components/skeleton.svg b/apps/web/public/images/components/skeleton.svg new file mode 100644 index 000000000..11977a7a9 --- /dev/null +++ b/apps/web/public/images/components/skeleton.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/packages/ui/src/components/Flowbite/FlowbiteTheme.ts b/packages/ui/src/components/Flowbite/FlowbiteTheme.ts index a0ce75db5..ce6c8a396 100644 --- a/packages/ui/src/components/Flowbite/FlowbiteTheme.ts +++ b/packages/ui/src/components/Flowbite/FlowbiteTheme.ts @@ -30,6 +30,7 @@ import type { FlowbiteRangeSliderTheme } from "../RangeSlider"; import type { FlowbiteRatingAdvancedTheme, FlowbiteRatingTheme } from "../Rating"; import type { FlowbiteSelectTheme } from "../Select"; import type { FlowbiteSidebarTheme } from "../Sidebar"; +import type { FlowbiteSkeletonTheme } from "../Skeleton"; import type { FlowbiteSpinnerTheme } from "../Spinner"; import type { FlowbiteTableTheme } from "../Table"; import type { FlowbiteTabsTheme } from "../Tabs"; @@ -76,6 +77,7 @@ export interface FlowbiteTheme { ratingAdvanced: FlowbiteRatingAdvancedTheme; select: FlowbiteSelectTheme; sidebar: FlowbiteSidebarTheme; + skeleton: FlowbiteSkeletonTheme; spinner: FlowbiteSpinnerTheme; table: FlowbiteTableTheme; tabs: FlowbiteTabsTheme; diff --git a/packages/ui/src/components/Skeleton/Skeleton.spec.tsx b/packages/ui/src/components/Skeleton/Skeleton.spec.tsx new file mode 100644 index 000000000..ce4183500 --- /dev/null +++ b/packages/ui/src/components/Skeleton/Skeleton.spec.tsx @@ -0,0 +1,57 @@ +import { render, screen } from "@testing-library/react"; +import { describe, expect, it } from "vitest"; +import { Skeleton } from "./Skeleton"; + +describe("Components / Skeleton", () => { + it("should have default Skeleton in Document", async () => { + render(); + + expect(defaultSkeleton()).toBeInTheDocument(); + }); + + it("should have Skeleton.Card in the document", async () => { + render(); + + expect(skeletonCard()).toBeInTheDocument(); + }); + + it("should have Skeleton.Image in the document", async () => { + render(); + + expect(skeletonImg()).toBeInTheDocument(); + }); + + it("should have Skeleton.List in the document", async () => { + render(); + + expect(skeletonList()).toBeInTheDocument(); + }); + + it("should have Skeleton.Testimonial in the document", async () => { + render(); + + expect(skeletonTestimonial()).toBeInTheDocument(); + }); + + it("should have Skeleton.Video in the document", async () => { + render(); + + expect(skeletonVideo()).toBeInTheDocument(); + }); + + it('should have role="Status" in the document', async () => { + render(); + + getSkeletonByStatus().forEach((status) => { + expect(status).toBeInTheDocument(); + }); + }); +}); + +const defaultSkeleton = () => screen.getByTestId("flowbite-skeleton"); +const skeletonCard = () => screen.getByTestId("flowbite-skeleton-card"); +const skeletonImg = () => screen.getByTestId("flowbite-skeleton-image"); +const skeletonList = () => screen.getByTestId("flowbite-skeleton-list"); +const skeletonTestimonial = () => screen.getByTestId("flowbite-skeleton-testimonial"); +const skeletonVideo = () => screen.getByTestId("flowbite-skeleton-video"); +const getSkeletonByStatus = () => screen.getAllByRole("status"); diff --git a/packages/ui/src/components/Skeleton/Skeleton.stories.tsx b/packages/ui/src/components/Skeleton/Skeleton.stories.tsx new file mode 100644 index 000000000..de909f0e4 --- /dev/null +++ b/packages/ui/src/components/Skeleton/Skeleton.stories.tsx @@ -0,0 +1,70 @@ +import type { Meta, StoryFn } from "@storybook/react"; +import { Skeleton } from "./Skeleton"; + +export default { + title: "Components/Skeleton", + component: Skeleton, +} as Meta; + +const Template: StoryFn = (args) => { + return ( +
+ +
+ ); +}; + +export const Default = Template.bind({}); +Default.args = { + variant: "default", +}; + +const CardTemplate: StoryFn = (args) => { + return ( +
+ +
+ ); +}; + +export const CardSkeleton = CardTemplate.bind({}); + +const ImageTemplate: StoryFn = (args) => { + return ( +
+ +
+ ); +}; + +export const ImageSkeleton = ImageTemplate.bind({}); + +const ListTemplate: StoryFn = (args) => { + return ( +
+ +
+ ); +}; + +export const ListSkeleton = ListTemplate.bind({}); + +const TestimonialTemplate: StoryFn = (args) => { + return ( +
+ +
+ ); +}; + +export const TestimonialSkeleton = TestimonialTemplate.bind({}); + +const VideoTemplate: StoryFn = (args) => { + return ( +
+ +
+ ); +}; + +export const VideoSkeleton = VideoTemplate.bind({}); diff --git a/packages/ui/src/components/Skeleton/Skeleton.tsx b/packages/ui/src/components/Skeleton/Skeleton.tsx new file mode 100644 index 000000000..db5057ccf --- /dev/null +++ b/packages/ui/src/components/Skeleton/Skeleton.tsx @@ -0,0 +1,68 @@ +import type { ComponentProps, FC } from "react"; +import { twMerge } from "tailwind-merge"; +import { mergeDeep } from "../../helpers/merge-deep"; +import { getTheme } from "../../theme-store"; +import type { DeepPartial } from "../../types"; +import { SkeletonCard, type FlowbiteSkeletonCardTheme } from "./SkeletonCard"; +import { SkeletonImage, type FlowbiteSkeletonImageTheme } from "./SkeletonImage"; +import { SkeletonList, type FlowbiteSkeletonListTheme } from "./SkeletonList"; +import { SkeletonTestimonial, type FlowbiteSkeletonTestimonialTheme } from "./SkeletonTestimonial"; +import { SkeletonVideo, type FlowbiteSkeletonVideoTheme } from "./SkeletonVideo"; + +export interface FlowbiteSkeletonTheme { + root: FlowbiteSkeletonRootTheme; + variant: { + base: string; + type: { + default: string; + rectangular: string; + rounded: string; + circular: string; + }; + }; + image: FlowbiteSkeletonImageTheme; + video: FlowbiteSkeletonVideoTheme; + card: FlowbiteSkeletonCardTheme; + list: FlowbiteSkeletonListTheme; + testimonial: FlowbiteSkeletonTestimonialTheme; +} + +export interface FlowbiteSkeletonRootTheme { + base: string; +} + +export interface SkeletonProps extends ComponentProps<"div"> { + theme?: DeepPartial; + variant?: "default" | "rectangular" | "rounded" | "circular"; +} + +const SkeletonComponent: FC = ({ + className, + theme: customTheme = {}, + variant: skeletonVariant = "default", + ...props +}) => { + const theme = mergeDeep(getTheme().skeleton, customTheme); + + return ( +
+
+ Loading... +
+ ); +}; + +SkeletonComponent.displayName = "Skeleton"; +SkeletonImage.displayName = "Skeleton.Image"; +SkeletonVideo.displayName = "Skeleton.Video"; +SkeletonCard.displayName = "Skeleton.Card"; +SkeletonList.displayName = "Skeleton.List"; +SkeletonTestimonial.displayName = "Skeleton.Testimonial"; + +export const Skeleton = Object.assign(SkeletonComponent, { + Image: SkeletonImage, + Video: SkeletonVideo, + Card: SkeletonCard, + List: SkeletonList, + Testimonial: SkeletonTestimonial, +}); diff --git a/packages/ui/src/components/Skeleton/SkeletonCard.tsx b/packages/ui/src/components/Skeleton/SkeletonCard.tsx new file mode 100644 index 000000000..71a8c5593 --- /dev/null +++ b/packages/ui/src/components/Skeleton/SkeletonCard.tsx @@ -0,0 +1,64 @@ +import type { FC } from "react"; +import { twMerge } from "tailwind-merge"; +import { mergeDeep } from "../../helpers/merge-deep"; +import { getTheme } from "../../theme-store"; +import type { DeepPartial } from "../../types"; + +export interface FlowbiteSkeletonCardTheme { + base: string; + cardImg: { + base: string; + svg: string; + }; + text: string; + userIcon: { + base: string; + icon: string; + text: string; + }; +} + +export interface SkeletonCardProps { + theme?: DeepPartial; + className?: string; +} + +export const SkeletonCard: FC = ({ className, theme: customTheme = {} }) => { + const theme = mergeDeep(getTheme().skeleton.card, customTheme); + + return ( +
+
+ +
+
+
+
+
+ +
+
+
+
+
+ Loading... +
+ ); +}; diff --git a/packages/ui/src/components/Skeleton/SkeletonImage.tsx b/packages/ui/src/components/Skeleton/SkeletonImage.tsx new file mode 100644 index 000000000..cc10483a0 --- /dev/null +++ b/packages/ui/src/components/Skeleton/SkeletonImage.tsx @@ -0,0 +1,55 @@ +import type { FC } from "react"; +import { twMerge } from "tailwind-merge"; +import { mergeDeep } from "../../helpers/merge-deep"; +import { getTheme } from "../../theme-store"; +import type { DeepPartial } from "../../types"; + +export interface FlowbiteSkeletonImageTheme { + base: string; + imgDiv: string; + imgSvg: string; + texts: { + base: string; + lineOne: string; + lineTwo: string; + lineThree: string; + lineFour: string; + lineFive: string; + lineSix: string; + }; +} + +export interface SkeletonImageProps { + theme?: DeepPartial; + className?: string; +} + +export const SkeletonImage: FC = ({ className, theme: customTheme = {} }) => { + const theme = mergeDeep(getTheme().skeleton.image, customTheme); + + return ( +
+
+ +
+
+
+
+
+
+
+
+
+ Loading... +
+
+ ); +}; diff --git a/packages/ui/src/components/Skeleton/SkeletonList.tsx b/packages/ui/src/components/Skeleton/SkeletonList.tsx new file mode 100644 index 000000000..0b3aa4663 --- /dev/null +++ b/packages/ui/src/components/Skeleton/SkeletonList.tsx @@ -0,0 +1,53 @@ +import type { FC } from "react"; +import { twMerge } from "tailwind-merge"; +import { mergeDeep } from "../../helpers/merge-deep"; +import { getTheme } from "../../theme-store"; +import type { DeepPartial } from "../../types"; + +export interface FlowbiteSkeletonListTheme { + base: string; + textList: { + base: string; + list: { + textOne: string; + textTwo: string; + textThree: string; + }; + }; +} + +export interface SkeletonListProps { + theme?: DeepPartial; + className?: string; +} + +export const SkeletonList: FC = ({ className, theme: customTheme = {} }) => { + const theme = mergeDeep(getTheme().skeleton.list, customTheme); + + return ( +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Loading... +
+ ); +}; diff --git a/packages/ui/src/components/Skeleton/SkeletonTestimonial.tsx b/packages/ui/src/components/Skeleton/SkeletonTestimonial.tsx new file mode 100644 index 000000000..8a445a19c --- /dev/null +++ b/packages/ui/src/components/Skeleton/SkeletonTestimonial.tsx @@ -0,0 +1,49 @@ +import type { FC } from "react"; +import { twMerge } from "tailwind-merge"; +import { mergeDeep } from "../../helpers/merge-deep"; +import { getTheme } from "../../theme-store"; +import type { DeepPartial } from "../../types"; + +export interface FlowbiteSkeletonTestimonialTheme { + base: string; + textList: { + firstLine: string; + secondLine: string; + author: { + base: string; + userIcon: string; + authorName: string; + secondText: string; + }; + }; +} + +export interface SkeletonTestimonialProps { + theme?: DeepPartial; + className?: string; +} + +export const SkeletonTestimonial: FC = ({ className, theme: customTheme = {} }) => { + const theme = mergeDeep(getTheme().skeleton.testimonial, customTheme); + + return ( +
+
+
+
+ +
+
+
+ Loading... +
+ ); +}; diff --git a/packages/ui/src/components/Skeleton/SkeletonVideo.tsx b/packages/ui/src/components/Skeleton/SkeletonVideo.tsx new file mode 100644 index 000000000..0da0ee15e --- /dev/null +++ b/packages/ui/src/components/Skeleton/SkeletonVideo.tsx @@ -0,0 +1,35 @@ +import type { FC } from "react"; +import { twMerge } from "tailwind-merge"; +import { mergeDeep } from "../../helpers/merge-deep"; +import { getTheme } from "../../theme-store"; +import type { DeepPartial } from "../../types"; + +export interface FlowbiteSkeletonVideoTheme { + base: string; + svg: string; +} + +export interface SkeletonVideoProps { + theme?: DeepPartial; + className?: string; +} + +export const SkeletonVideo: FC = ({ className, theme: customTheme = {} }) => { + const theme = mergeDeep(getTheme().skeleton.video, customTheme); + + return ( +
+ + Loading... +
+ ); +}; diff --git a/packages/ui/src/components/Skeleton/index.ts b/packages/ui/src/components/Skeleton/index.ts new file mode 100644 index 000000000..910e924d2 --- /dev/null +++ b/packages/ui/src/components/Skeleton/index.ts @@ -0,0 +1,17 @@ +export { Skeleton } from "./Skeleton"; +export type { FlowbiteSkeletonRootTheme, FlowbiteSkeletonTheme, SkeletonProps } from "./Skeleton"; + +export { SkeletonImage } from "./SkeletonImage"; +export type { FlowbiteSkeletonImageTheme, SkeletonImageProps } from "./SkeletonImage"; + +export { SkeletonVideo } from "./SkeletonVideo"; +export type { FlowbiteSkeletonVideoTheme, SkeletonVideoProps } from "./SkeletonVideo"; + +export { SkeletonCard } from "./SkeletonCard"; +export type { FlowbiteSkeletonCardTheme, SkeletonCardProps } from "./SkeletonCard"; + +export { SkeletonList } from "./SkeletonList"; +export type { FlowbiteSkeletonListTheme, SkeletonListProps } from "./SkeletonList"; + +export { SkeletonTestimonial } from "./SkeletonTestimonial"; +export type { FlowbiteSkeletonTestimonialTheme, SkeletonTestimonialProps } from "./SkeletonTestimonial"; diff --git a/packages/ui/src/components/Skeleton/theme.ts b/packages/ui/src/components/Skeleton/theme.ts new file mode 100644 index 000000000..8aa4ded15 --- /dev/null +++ b/packages/ui/src/components/Skeleton/theme.ts @@ -0,0 +1,71 @@ +import type { FlowbiteSkeletonTheme } from "./Skeleton"; + +export const skeletonTheme: FlowbiteSkeletonTheme = { + root: { + base: "max-w-sm animate-pulse", + }, + variant: { + base: "block bg-gray-200 dark:bg-gray-700 h-[1.2em]", + type: { + default: "rounded-sm transform origin-[0_55%] my-0 scale-100 text-[1rem]", + rectangular: "h-[60px]", + rounded: "h-[60px] rounded-sm", + circular: "rounded-[50%] w-10 h-10", + }, + }, + image: { + base: "space-y-8 animate-pulse md:space-y-0 md:space-x-8 rtl:space-x-reverse md:flex md:items-center", + imgDiv: "flex items-center justify-center w-full h-48 bg-gray-300 rounded sm:w-96 dark:bg-gray-700", + imgSvg: "w-10 h-10 text-gray-200 dark:text-gray-600", + texts: { + base: "w-full", + lineOne: "h-2.5 bg-gray-200 rounded-full dark:bg-gray-700 w-48 mb-4", + lineTwo: "h-2 bg-gray-200 rounded-full dark:bg-gray-700 max-w-[480px] mb-2.5", + lineThree: "h-2 bg-gray-200 rounded-full dark:bg-gray-700 mb-2.5", + lineFour: "h-2 bg-gray-200 rounded-full dark:bg-gray-700 max-w-[440px] mb-2.5", + lineFive: "h-2 bg-gray-200 rounded-full dark:bg-gray-700 max-w-[460px] mb-2.5", + lineSix: "h-2 bg-gray-200 rounded-full dark:bg-gray-700 max-w-[360px]", + }, + }, + video: { + base: "flex items-center justify-center h-56 max-w-sm bg-gray-300 rounded-lg animate-pulse dark:bg-gray-700", + svg: "h-10 w-10 text-gray-200 dark:text-gray-600", + }, + card: { + base: "max-w-sm p-4 border border-gray-200 rounded shadow animate-pulse md:p-6 dark:border-gray-700", + cardImg: { + base: "flex items-center justify-center h-48 mb-4 bg-gray-300 rounded dark:bg-gray-700", + svg: "w-10 h-10 text-gray-200 dark:text-gray-600", + }, + text: "h-2 bg-gray-200 rounded-full dark:bg-gray-700 mb-2.5", + userIcon: { + base: "flex items-center mt-4", + icon: "w-10 h-10 me-3 text-gray-200 dark:text-gray-700", + text: "w-48 h-2 bg-gray-200 rounded-full dark:bg-gray-700", + }, + }, + list: { + base: "max-w-md p-4 space-y-4 border border-gray-200 divide-y divide-gray-200 rounded shadow animate-pulse dark:divide-gray-700 md:p-6 dark:border-gray-700", + textList: { + base: "flex items-center justify-between pt-4", + list: { + textOne: "h-2.5 bg-gray-300 rounded-full dark:bg-gray-600 w-24 mb-2.5", + textTwo: "w-32 h-2 bg-gray-200 rounded-full dark:bg-gray-700", + textThree: "h-2.5 bg-gray-300 rounded-full dark:bg-gray-700 w-12", + }, + }, + }, + testimonial: { + base: "animate-pulse", + textList: { + firstLine: "h-2.5 bg-gray-300 rounded-full dark:bg-gray-700 max-w-[640px] mb-2.5 mx-auto", + secondLine: "h-2.5 mx-auto bg-gray-300 rounded-full dark:bg-gray-700 max-w-[540px]", + author: { + base: "flex items-center justify-center mt-4", + userIcon: "w-8 h-8 text-gray-200 dark:text-gray-700 me-4", + authorName: "w-20 h-2.5 bg-gray-200 rounded-full dark:bg-gray-700 me-3", + secondText: "w-24 h-2 bg-gray-200 rounded-full dark:bg-gray-700", + }, + }, + }, +}; diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts index d4ac43525..cc7346fd3 100644 --- a/packages/ui/src/index.ts +++ b/packages/ui/src/index.ts @@ -32,6 +32,7 @@ export * from "./components/RangeSlider"; export * from "./components/Rating"; export * from "./components/Select"; export * from "./components/Sidebar"; +export * from "./components/Skeleton"; export * from "./components/Spinner"; export * from "./components/Table"; export * from "./components/Tabs"; diff --git a/packages/ui/src/theme.ts b/packages/ui/src/theme.ts index a5c64cb0b..170ffa0b8 100644 --- a/packages/ui/src/theme.ts +++ b/packages/ui/src/theme.ts @@ -30,6 +30,7 @@ import { rangeSliderTheme } from "./components/RangeSlider/theme"; import { ratingAdvancedTheme, ratingTheme } from "./components/Rating/theme"; import { selectTheme } from "./components/Select/theme"; import { sidebarTheme } from "./components/Sidebar/theme"; +import { skeletonTheme } from "./components/Skeleton/theme"; import { spinnerTheme } from "./components/Spinner/theme"; import { tableTheme } from "./components/Table/theme"; import { tabTheme } from "./components/Tabs/theme"; @@ -77,6 +78,7 @@ export const theme: FlowbiteTheme = { textarea: textareaTheme, toggleSwitch: toggleSwitchTheme, sidebar: sidebarTheme, + skeleton: skeletonTheme, spinner: spinnerTheme, table: tableTheme, tabs: tabTheme, From c2eff4db6023d5bed7c39413b53eba581c875d6b Mon Sep 17 00:00:00 2001 From: Dhaval Vira Date: Sun, 31 Mar 2024 09:29:37 +0530 Subject: [PATCH 2/2] small fixed based on reviews --- apps/web/content/docs/components/skeleton.mdx | 2 +- apps/web/examples/skeleton/skeleton.card.tsx | 4 ++-- apps/web/examples/skeleton/skeleton.circular.tsx | 2 +- apps/web/examples/skeleton/skeleton.image.tsx | 2 +- apps/web/examples/skeleton/skeleton.list.tsx | 4 ++-- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/apps/web/content/docs/components/skeleton.mdx b/apps/web/content/docs/components/skeleton.mdx index ee1710682..a0de92dc9 100644 --- a/apps/web/content/docs/components/skeleton.mdx +++ b/apps/web/content/docs/components/skeleton.mdx @@ -3,7 +3,7 @@ title: React Skeleton - Flowbite description: The skeleton component can be used as an alternative loading indicator to the spinner by mimicking the content that will be loaded such as text, images, or video --- -Use the skeleton component to indicate a loading status with placeholder elements that look very similar to the type of content that is being loaded such as paragraphs, heading, images, videos, and more. +Use the skeleton component to indicate a loading status with placeholder elements that look very similar to the type of content that is being loaded such as paragraphs, headings, images, videos, and more. You can set the width and height of these skeleton components based on the size of the content and element that it is being loaded in, such as a card or an article page. diff --git a/apps/web/examples/skeleton/skeleton.card.tsx b/apps/web/examples/skeleton/skeleton.card.tsx index d264b77c6..fdc201a7a 100644 --- a/apps/web/examples/skeleton/skeleton.card.tsx +++ b/apps/web/examples/skeleton/skeleton.card.tsx @@ -2,14 +2,14 @@ import { SkeletonCard } from "flowbite-react"; import type { CodeData } from "~/components/code-demo"; const code = ` -'use client'; +"use client"; import { Skeleton } from "flowbite-react"; function Component() { return (
- +
) } diff --git a/apps/web/examples/skeleton/skeleton.circular.tsx b/apps/web/examples/skeleton/skeleton.circular.tsx index fe17b2789..831738930 100644 --- a/apps/web/examples/skeleton/skeleton.circular.tsx +++ b/apps/web/examples/skeleton/skeleton.circular.tsx @@ -2,7 +2,7 @@ import { Skeleton } from "flowbite-react"; import type { CodeData } from "~/components/code-demo"; const code = ` -'use client'; +"use client"; import { Skeleton } from "flowbite-react"; diff --git a/apps/web/examples/skeleton/skeleton.image.tsx b/apps/web/examples/skeleton/skeleton.image.tsx index 816a9acbd..6feeb5e85 100644 --- a/apps/web/examples/skeleton/skeleton.image.tsx +++ b/apps/web/examples/skeleton/skeleton.image.tsx @@ -2,7 +2,7 @@ import { SkeletonImage } from "flowbite-react"; import type { CodeData } from "~/components/code-demo"; const code = ` -'use client'; +"use client"; import { Skeleton } from "flowbite-react"; diff --git a/apps/web/examples/skeleton/skeleton.list.tsx b/apps/web/examples/skeleton/skeleton.list.tsx index 084b6276e..4bc4f12cf 100644 --- a/apps/web/examples/skeleton/skeleton.list.tsx +++ b/apps/web/examples/skeleton/skeleton.list.tsx @@ -2,14 +2,14 @@ import { SkeletonList } from "flowbite-react"; import type { CodeData } from "~/components/code-demo"; const code = ` -'use client'; +"use client"; import { Skeleton } from "flowbite-react"; function Component() { return (
- +
) }