diff --git a/apps/web/content/docs/components/progress.mdx b/apps/web/content/docs/components/progress.mdx index e475398a0..12a0c8c3e 100644 --- a/apps/web/content/docs/components/progress.mdx +++ b/apps/web/content/docs/components/progress.mdx @@ -43,6 +43,18 @@ Set your own custom colors for the progress bar component by using the `color` p +## Circular Progress + +Use this Circular progress example to show a progress bar where you can set the progress rate using the `progress` prop from React which should be a number from 1 to 100. + + + +## Circular Progress With Text + +Use this Circular progress example to show a progress bar with a label. You can set the label text using the `textLabel` prop and the progress text using the `labelText` prop. + + + ## Theme To learn more about how to customize the appearance of components, please see the [Theme docs](/docs/customize/theme). diff --git a/apps/web/examples/progress/index.ts b/apps/web/examples/progress/index.ts index c43d89682..b8776e55c 100644 --- a/apps/web/examples/progress/index.ts +++ b/apps/web/examples/progress/index.ts @@ -1,3 +1,5 @@ +export { circularProgress } from "./progress.circular"; +export { circularProgressWithText } from "./progress.circularWithText"; export { colors } from "./progress.colors"; export { positioning } from "./progress.positioning"; export { root } from "./progress.root"; diff --git a/apps/web/examples/progress/progress.circular.tsx b/apps/web/examples/progress/progress.circular.tsx new file mode 100644 index 000000000..54cf1f57a --- /dev/null +++ b/apps/web/examples/progress/progress.circular.tsx @@ -0,0 +1,42 @@ +import { Progress } from "flowbite-react"; +import { type CodeData } from "~/components/code-demo"; + +const code = ` +"use client"; + +import { Progress } from "flowbite-react"; + +export function Component() { + return ; +} +`; + +const codeRSC = ` +import { ProgressCircular } from "flowbite-react"; + +export function Component() { + return ; +} +`; + +export function Component() { + return ; +} + +export const circularProgress: CodeData = { + type: "single", + code: [ + { + fileName: "client", + language: "tsx", + code, + }, + { + fileName: "server", + language: "tsx", + code: codeRSC, + }, + ], + githubSlug: "progress/progress.circular.tsx", + component: , +}; diff --git a/apps/web/examples/progress/progress.circularWithText.tsx b/apps/web/examples/progress/progress.circularWithText.tsx new file mode 100644 index 000000000..91c19e1da --- /dev/null +++ b/apps/web/examples/progress/progress.circularWithText.tsx @@ -0,0 +1,42 @@ +import { Progress } from "flowbite-react"; +import { type CodeData } from "~/components/code-demo"; + +const code = ` +"use client"; + +import { Progress } from "flowbite-react"; + +export function Component() { + return ; +} +`; + +const codeRSC = ` +import { ProgressCircular } from "flowbite-react"; + +export function Component() { + return ; +} +`; + +export function Component() { + return ; +} + +export const circularProgressWithText: CodeData = { + type: "single", + code: [ + { + fileName: "client", + language: "tsx", + code, + }, + { + fileName: "server", + language: "tsx", + code: codeRSC, + }, + ], + githubSlug: "progress/progress.circularWithText.tsx", + component: , +}; diff --git a/packages/ui/src/components/Progress/CircularProgress.stories.tsx b/packages/ui/src/components/Progress/CircularProgress.stories.tsx new file mode 100644 index 000000000..17527da90 --- /dev/null +++ b/packages/ui/src/components/Progress/CircularProgress.stories.tsx @@ -0,0 +1,31 @@ +import type { Meta, StoryFn } from "@storybook/react"; +import type { CircularProgressProps } from "./ProgressCircular"; +import { CircularProgress } from "./ProgressCircular"; + +export default { + title: "Components/Circular Progress", + component: CircularProgress, + decorators: [ + (Story): JSX.Element => ( +
+ +
+ ), + ], +} as Meta; + +const CircularTemplate: StoryFn = (args) => ; + +export const CircularProgressBar = CircularTemplate.bind({}); +CircularProgressBar.storyName = "Circular Progress"; +CircularProgressBar.args = { + progress: 25, +}; + +export const CircularProgressBarWithText = CircularTemplate.bind({}); +CircularProgressBarWithText.storyName = "Circular Progress With Text"; +CircularProgressBarWithText.args = { + progress: 25, + labelText: true, + textLabel: "25%", +}; diff --git a/packages/ui/src/components/Progress/Progress.tsx b/packages/ui/src/components/Progress/Progress.tsx index ead8c1a11..b61513465 100644 --- a/packages/ui/src/components/Progress/Progress.tsx +++ b/packages/ui/src/components/Progress/Progress.tsx @@ -5,6 +5,8 @@ import { mergeDeep } from "../../helpers/merge-deep"; import { getTheme } from "../../theme-store"; import type { DeepPartial, DynamicStringEnumKeysOf } from "../../types"; import type { FlowbiteColors, FlowbiteSizes } from "../Flowbite"; +import type { FlowbiteCircularProgressTheme } from "./ProgressCircular"; +import { CircularProgress } from "./ProgressCircular"; export interface FlowbiteProgressTheme { base: string; @@ -12,6 +14,7 @@ export interface FlowbiteProgressTheme { bar: string; color: ProgressColor; size: ProgressSizes; + circular: FlowbiteCircularProgressTheme; } export interface ProgressColor @@ -37,7 +40,7 @@ export interface ProgressProps extends ComponentProps<"div"> { theme?: DeepPartial; } -export const Progress: FC = ({ +const ProgressComponent: FC = ({ className, color = "cyan", labelProgress = false, @@ -83,4 +86,8 @@ export const Progress: FC = ({ ); }; -Progress.displayName = "Progress"; +ProgressComponent.displayName = "Progress"; + +export const Progress = Object.assign(ProgressComponent, { + Circular: CircularProgress, +}); diff --git a/packages/ui/src/components/Progress/ProgressCircular.tsx b/packages/ui/src/components/Progress/ProgressCircular.tsx new file mode 100644 index 000000000..c83769fbf --- /dev/null +++ b/packages/ui/src/components/Progress/ProgressCircular.tsx @@ -0,0 +1,92 @@ +import type { ComponentProps, FC } from "react"; +import { useId, useMemo } from "react"; +import { twMerge } from "tailwind-merge"; +import { mergeDeep } from "../../helpers/merge-deep"; +import { getTheme } from "../../theme-store"; +import type { DeepPartial } from "../../types"; +import type { FlowbiteColors } from "../Flowbite"; + +export interface FlowbiteCircularProgressTheme { + base: string; + bar: string; + label: { + base: string; + text: string; + textColor: CircularProgressColor; + }; + color: { + bgColor: string; + barColor: CircularProgressColor; + }; +} + +export interface CircularProgressColor + extends Pick< + FlowbiteColors, + "dark" | "blue" | "red" | "green" | "yellow" | "indigo" | "purple" | "cyan" | "gray" | "lime" | "pink" | "teal" + > { + [key: string]: string; +} + +export interface CircularProgressProps extends ComponentProps<"div"> { + labelText?: boolean; + progress: number; + textLabel?: string; + theme?: DeepPartial; + progressColor?: keyof CircularProgressColor; +} + +export const CircularProgress: FC = ({ + className, + progressColor = "cyan", + labelText = false, + progress, + textLabel = "65%", + theme: customTheme = {}, + ...props +}) => { + const id = useId(); + const theme = mergeDeep(getTheme().progress.circular, customTheme); + + // Memoize calculations for the circumference and stroke offset to avoid recalculating on each render + const { offset } = useMemo(() => { + const circumference = 2 * Math.PI * 16; // Fixed radius of 16 + + const offset = circumference * (1 - progress / 100); // Stroke dash offset based on progress + + return { offset }; + }, [progress]); + + return ( +
+
+ + + + + + + {labelText && textLabel ? ( +
+ + {textLabel} + +
+ ) : null} +
+
+ ); +}; diff --git a/packages/ui/src/components/Progress/theme.ts b/packages/ui/src/components/Progress/theme.ts index 2640133d1..451de776d 100644 --- a/packages/ui/src/components/Progress/theme.ts +++ b/packages/ui/src/components/Progress/theme.ts @@ -25,4 +25,43 @@ export const progressTheme: FlowbiteProgressTheme = createTheme({ lg: "h-4", xl: "h-6", }, + circular: { + base: "relative size-40", + bar: "size-full -rotate-90", + color: { + barColor: { + dark: "stroke-current text-gray-600 dark:text-gray-300", + blue: "stroke-current text-blue-600", + red: "stroke-current text-red-600 dark:text-red-500", + green: "stroke-current text-green-600 dark:text-green-500", + yellow: "stroke-current text-yellow-400", + indigo: "stroke-current text-indigo-600 dark:text-indigo-500", + purple: "stroke-current text-purple-600 dark:text-purple-500", + cyan: "stroke-current text-cyan-600", + gray: "stroke-current text-gray-500", + lime: "stroke-current text-lime-600", + pink: "stroke-current text-pink-500", + teal: "stroke-current text-teal-600", + }, + bgColor: "stroke-current text-gray-200 dark:text-neutral-700", + }, + label: { + base: "absolute start-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 transform", + text: "text-center text-2xl font-bold", + textColor: { + dark: "text-gray-600 dark:text-gray-300", + blue: "text-blue-600", + red: "text-red-600 dark:text-red-500", + green: "text-green-600 dark:text-green-500", + yellow: "text-yellow-400", + indigo: "text-indigo-600 dark:text-indigo-500", + purple: "text-purple-600 dark:text-purple-500", + cyan: "text-cyan-600", + gray: "text-gray-500", + lime: "text-lime-600", + pink: "text-pink-500", + teal: "text-teal-600", + }, + }, + }, });