diff --git a/apps/web/content/docs/components/skeleton.mdx b/apps/web/content/docs/components/skeleton.mdx new file mode 100644 index 000000000..a0de92dc9 --- /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, 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. + +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..fdc201a7a --- /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..831738930 --- /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..6feeb5e85 --- /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..4bc4f12cf --- /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,