diff --git a/.changeset/poor-tools-hide.md b/.changeset/poor-tools-hide.md new file mode 100644 index 000000000..ac2d1bf43 --- /dev/null +++ b/.changeset/poor-tools-hide.md @@ -0,0 +1,5 @@ +--- +"flowbite-react": minor +--- + +feat(components): add "Clipboard" diff --git a/apps/web/content/docs/components/clipboard.mdx b/apps/web/content/docs/components/clipboard.mdx new file mode 100644 index 000000000..447ac9b4c --- /dev/null +++ b/apps/web/content/docs/components/clipboard.mdx @@ -0,0 +1,42 @@ +--- +title: React Clipboard - Flowbite +description: Use the clipboard component to copy text, data or lines of code to the clipboard with a single click based on various styles and examples coded with Tailwind CSS and Flowbite +--- + +The copy to clipboard component allows you to copy text, lines of code, contact details or any other data to the clipboard with a single click on a trigger element such as a button. This component can be used to copy text from an input field, textarea, code block or even address fields in a form element. + +These components are built with Tailwind CSS and Flowbite React and can be found on the internet on websites such as Bitly, Cloudflare, Amazon AWS and almost all open-source projects and documentations. + +Import the component from `flowbite-react` to use the clipboard element: + +```jsx +import { Clipboard } from "flowbite-react"; +``` + +## Default copy to clipboard + +Use this example to copy the content of an input text field by clicking on a button and update the button text. + + + +## Input with copy button + +This example can be used to copy the content of an input field by clicking on a button with an icon positioned inside the form element and also show a tooltip with a message when the text has been copied. + + + +## Copy button with text + +Use this example to show a copy button inside the input field with a text label and icon that updates to a success state when the text has been copied. + + + +## Theme + +To learn more about how to customize the appearance of components, please see the [Theme docs](/docs/customize/theme). + + + +## References + +- [Flowbite Datepicker](https://flowbite.com/docs/components/clipboard/) diff --git a/apps/web/data/docs-sidebar.ts b/apps/web/data/docs-sidebar.ts index 73af25d81..933eaf146 100644 --- a/apps/web/data/docs-sidebar.ts +++ b/apps/web/data/docs-sidebar.ts @@ -63,6 +63,7 @@ export const DOCS_SIDEBAR: DocsSidebarSection[] = [ { title: "Button group", href: "/docs/components/button-group" }, { title: "Card", href: "/docs/components/card" }, { title: "Carousel", href: "/docs/components/carousel" }, + { title: "Clipboard", href: "/docs/components/clipboard", isNew: true }, { title: "Datepicker", href: "/docs/components/datepicker", isNew: true }, { title: "Drawer", href: "/docs/components/drawer", isNew: true }, { title: "Dropdown", href: "/docs/components/dropdown" }, diff --git a/apps/web/examples/clipboard/clipboard.root.tsx b/apps/web/examples/clipboard/clipboard.root.tsx new file mode 100644 index 000000000..d48ddc09c --- /dev/null +++ b/apps/web/examples/clipboard/clipboard.root.tsx @@ -0,0 +1,59 @@ +"use client"; + +import { Clipboard } from "flowbite-react"; +import type { CodeData } from "~/components/code-demo"; + +const code = ` +"use client"; + +import { Clipboard } from "flowbite-react" + +export function Component() { + return ( +
+ + + +
+ ) +} +`; + +export function Component() { + return ( +
+ + + +
+ ); +} + +export const root: CodeData = { + type: "single", + code: [ + { + fileName: "client", + language: "tsx", + code, + }, + ], + githubSlug: "clipboard/clipboard.root.tsx", + component: , +}; diff --git a/apps/web/examples/clipboard/clipboard.withIcon.tsx b/apps/web/examples/clipboard/clipboard.withIcon.tsx new file mode 100644 index 000000000..4e7c51d10 --- /dev/null +++ b/apps/web/examples/clipboard/clipboard.withIcon.tsx @@ -0,0 +1,65 @@ +"use client"; + +import { Clipboard } from "flowbite-react"; +import type { CodeData } from "~/components/code-demo"; + +const code = ` +"use client"; + +import { Clipboard } from "flowbite-react" + +export function Component() { + return ( +
+
+ + + +
+
+ ) +} +`; + +export function Component() { + return ( +
+
+ + + +
+
+ ); +} + +export const withIcon: CodeData = { + type: "single", + code: [ + { + fileName: "client", + language: "tsx", + code, + }, + ], + githubSlug: "clipboard/clipboard.withIcon.tsx", + component: , +}; diff --git a/apps/web/examples/clipboard/clipboard.withIconText.tsx b/apps/web/examples/clipboard/clipboard.withIconText.tsx new file mode 100644 index 000000000..e8640398c --- /dev/null +++ b/apps/web/examples/clipboard/clipboard.withIconText.tsx @@ -0,0 +1,65 @@ +"use client"; + +import { Clipboard } from "flowbite-react"; +import type { CodeData } from "~/components/code-demo"; + +const code = ` +"use client"; + +import { Clipboard } from "flowbite-react" + +export function Component() { + return ( +
+
+ + + +
+
+ ) +} +`; + +export function Component() { + return ( +
+
+ + + +
+
+ ); +} + +export const withIconText: CodeData = { + type: "single", + code: [ + { + fileName: "client", + language: "tsx", + code, + }, + ], + githubSlug: "clipboard/clipboard.withIconText.tsx", + component: , +}; diff --git a/apps/web/examples/clipboard/index.ts b/apps/web/examples/clipboard/index.ts new file mode 100644 index 000000000..176b081e7 --- /dev/null +++ b/apps/web/examples/clipboard/index.ts @@ -0,0 +1,3 @@ +export { root } from "./clipboard.root"; +export { withIcon } from "./clipboard.withIcon"; +export { withIconText } from "./clipboard.withIconText"; diff --git a/apps/web/examples/index.ts b/apps/web/examples/index.ts index 8f1c4575d..8491bc69b 100644 --- a/apps/web/examples/index.ts +++ b/apps/web/examples/index.ts @@ -9,6 +9,7 @@ export * as button from "./button"; export * as buttonGroup from "./buttonGroup"; export * as card from "./card"; export * as carousel from "./carousel"; +export * as clipboard from "./clipboard"; export * as datepicker from "./datepicker"; export * as drawer from "./drawer"; export * as dropdown from "./dropdown"; diff --git a/packages/ui/src/components/Clipboard/Clipboard.stories.tsx b/packages/ui/src/components/Clipboard/Clipboard.stories.tsx new file mode 100644 index 000000000..7374f0f54 --- /dev/null +++ b/packages/ui/src/components/Clipboard/Clipboard.stories.tsx @@ -0,0 +1,72 @@ +import type { Meta, StoryFn } from "@storybook/react"; +// import { FaClipboardList } from "react-icons/fa6"; +import type { ClipboardProps } from "./Clipboard"; +import { Clipboard } from "./Clipboard"; +import type { ClipboardWithIconProps } from "./ClipboardWithIcon"; +import type { ClipboardWithIconTextProps } from "./ClipboardWithIconText"; + +export default { + title: "Components/Clipboard", + component: Clipboard, +} as Meta; + +const DefaultTemplate: StoryFn = () => ( +
+ + + +
+); + +export const Default = DefaultTemplate.bind({}); + +const CopyIconTemplate: StoryFn = () => ( +
+
+ + + +
+
+); + +export const CopyIcon = CopyIconTemplate.bind({}); + +const CopyIconTextTemplate: StoryFn = () => ( +
+
+ + + +
+
+); + +export const CopyIconText = CopyIconTextTemplate.bind({}); diff --git a/packages/ui/src/components/Clipboard/Clipboard.tsx b/packages/ui/src/components/Clipboard/Clipboard.tsx new file mode 100644 index 000000000..a32914e83 --- /dev/null +++ b/packages/ui/src/components/Clipboard/Clipboard.tsx @@ -0,0 +1,58 @@ +"use client"; + +import { forwardRef, useState, type ComponentProps, type ReactNode } from "react"; +import { twMerge } from "tailwind-merge"; +import { mergeDeep } from "../../helpers/merge-deep"; +import { getTheme } from "../../theme-store"; +import type { DeepPartial } from "../../types"; +import { Tooltip } from "../Tooltip"; +import { ClipboardWithIcon } from "./ClipboardWithIcon"; +import type { FlowbiteClipboardWithIconTheme } from "./ClipboardWithIcon"; +import { ClipboardWithIconText } from "./ClipboardWithIconText"; +import type { FlowbiteClipboardWithIconTextTheme } from "./ClipboardWithIconText"; +import { copyToClipboard } from "./helpers"; + +export interface FlowbiteClipboardTheme { + button: { + base: string; + label: string; + }; + withIcon: FlowbiteClipboardWithIconTheme; + withIconText: FlowbiteClipboardWithIconTextTheme; +} + +export interface ClipboardProps extends ComponentProps<"button"> { + valueToCopy: string; + label?: ReactNode; + theme?: DeepPartial; +} + +const ClipboardComponent = forwardRef( + ({ className, valueToCopy, label, theme: customTheme = {}, ...rest }, ref) => { + const [isJustCopied, setIsJustCopied] = useState(false); + + const theme = mergeDeep(getTheme().clipboard.button, customTheme); + + return ( + + + + ); + }, +); + +ClipboardComponent.displayName = "Clipboard"; +ClipboardWithIcon.displayName = "Clipboard.WithIcon"; +ClipboardWithIconText.displayName = "Clipboard.WithIconText"; + +export const Clipboard = Object.assign(ClipboardComponent, { + WithIcon: ClipboardWithIcon, + WithIconText: ClipboardWithIconText, +}); diff --git a/packages/ui/src/components/Clipboard/ClipboardWithIcon.tsx b/packages/ui/src/components/Clipboard/ClipboardWithIcon.tsx new file mode 100644 index 000000000..574c8fa66 --- /dev/null +++ b/packages/ui/src/components/Clipboard/ClipboardWithIcon.tsx @@ -0,0 +1,46 @@ +"use client"; + +import { forwardRef, useState, type ComponentProps, type FC } from "react"; +import { FaCheck, FaClipboardList } from "react-icons/fa6"; +import { twMerge } from "tailwind-merge"; +import { mergeDeep } from "../../helpers/merge-deep"; +import { getTheme } from "../../theme-store"; +import type { DeepPartial } from "../../types"; +import { copyToClipboard } from "./helpers"; + +export interface FlowbiteClipboardWithIconTheme { + base: string; + icon: { + defaultIcon: string; + successIcon: string; + }; +} + +export interface ClipboardWithIconProps extends ComponentProps<"button"> { + valueToCopy: string; + icon?: FC>; + theme?: DeepPartial; +} + +export const ClipboardWithIcon = forwardRef( + ({ valueToCopy, icon: Icon = FaClipboardList, theme: customTheme = {}, className, ...rest }, ref) => { + const [isJustCopied, setIsJustCopied] = useState(false); + + const theme = mergeDeep(getTheme().clipboard.withIcon, customTheme); + + return ( + + ); + }, +); diff --git a/packages/ui/src/components/Clipboard/ClipboardWithIconText.tsx b/packages/ui/src/components/Clipboard/ClipboardWithIconText.tsx new file mode 100644 index 000000000..224ef6a89 --- /dev/null +++ b/packages/ui/src/components/Clipboard/ClipboardWithIconText.tsx @@ -0,0 +1,58 @@ +"use client"; + +import { forwardRef, useState, type ComponentProps, type FC } from "react"; +import { FaCheck, FaClipboardList } from "react-icons/fa6"; +import { twMerge } from "tailwind-merge"; +import { mergeDeep } from "../../helpers/merge-deep"; +import { getTheme } from "../../theme-store"; +import type { DeepPartial } from "../../types"; +import { copyToClipboard } from "./helpers"; + +export interface FlowbiteClipboardWithIconTextTheme { + base: string; + label: { + base: string; + defaultText: string; + successText: string; + }; + icon: { + defaultIcon: string; + successIcon: string; + }; +} + +export interface ClipboardWithIconTextProps extends ComponentProps<"button"> { + valueToCopy: string; + label?: string; + icon?: FC>; + theme?: DeepPartial; +} + +export const ClipboardWithIconText = forwardRef( + ({ valueToCopy, icon: Icon = FaClipboardList, label = "Copy", theme: customTheme = {}, className, ...rest }, ref) => { + const [isJustCopied, setIsJustCopied] = useState(false); + + const theme = mergeDeep(getTheme().clipboard.withIconText, customTheme); + + return ( + + ); + }, +); diff --git a/packages/ui/src/components/Clipboard/helpers.ts b/packages/ui/src/components/Clipboard/helpers.ts new file mode 100644 index 000000000..0f2930780 --- /dev/null +++ b/packages/ui/src/components/Clipboard/helpers.ts @@ -0,0 +1,15 @@ +import type { Dispatch, SetStateAction } from "react"; + +export const copyToClipboard = (valueToCopy: string, setIsJustCopied: Dispatch>) => { + setIsJustCopied(true); + navigator?.clipboard + ?.writeText(valueToCopy) + .then(() => { + console.log("Copy Successfull"); + }) + .catch((error) => { + console.error("Failed to Copy text: ", error); + setIsJustCopied(false); + }); + setTimeout(() => setIsJustCopied(false), 4000); +}; diff --git a/packages/ui/src/components/Clipboard/index.ts b/packages/ui/src/components/Clipboard/index.ts new file mode 100644 index 000000000..b87b42b3b --- /dev/null +++ b/packages/ui/src/components/Clipboard/index.ts @@ -0,0 +1,8 @@ +export { Clipboard } from "./Clipboard"; +export type { ClipboardProps, FlowbiteClipboardTheme } from "./Clipboard"; + +export { ClipboardWithIcon } from "./ClipboardWithIcon"; +export type { ClipboardWithIconProps, FlowbiteClipboardWithIconTheme } from "./ClipboardWithIcon"; + +export { ClipboardWithIconText } from "./ClipboardWithIconText"; +export type { ClipboardWithIconTextProps, FlowbiteClipboardWithIconTextTheme } from "./ClipboardWithIconText"; diff --git a/packages/ui/src/components/Clipboard/theme.tsx b/packages/ui/src/components/Clipboard/theme.tsx new file mode 100644 index 000000000..216dfb72f --- /dev/null +++ b/packages/ui/src/components/Clipboard/theme.tsx @@ -0,0 +1,28 @@ +import { createTheme } from "../../helpers/create-theme"; +import type { FlowbiteClipboardTheme } from "./Clipboard"; + +export const clipboardTheme: FlowbiteClipboardTheme = createTheme({ + button: { + base: "inline-flex w-full items-center justify-center rounded-lg bg-blue-700 px-5 py-3 hover:bg-blue-800 focus:outline-none focus:ring-4 focus:ring-blue-300 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800", + label: "text-center text-sm font-medium text-white sm:w-auto", + }, + withIcon: { + base: "absolute end-2 top-1/2 inline-flex -translate-y-1/2 items-center justify-center rounded-lg p-2 text-gray-500 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-800", + icon: { + defaultIcon: "h-4 w-4", + successIcon: "h-4 w-4 text-blue-700 dark:text-blue-500", + }, + }, + withIconText: { + base: "absolute end-2.5 top-1/2 inline-flex -translate-y-1/2 items-center justify-center rounded-lg border border-gray-200 bg-white px-2.5 py-2 text-gray-900 hover:bg-gray-100 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-gray-700", + icon: { + defaultIcon: "me-1.5 h-3 w-3", + successIcon: "me-1.5 h-3 w-3 text-blue-700 dark:text-blue-500", + }, + label: { + base: "inline-flex items-center", + defaultText: "text-xs font-semibold", + successText: "text-xs font-semibold text-blue-700 dark:text-blue-500", + }, + }, +}); diff --git a/packages/ui/src/components/Flowbite/FlowbiteTheme.ts b/packages/ui/src/components/Flowbite/FlowbiteTheme.ts index 3da3a6d32..8489755ce 100644 --- a/packages/ui/src/components/Flowbite/FlowbiteTheme.ts +++ b/packages/ui/src/components/Flowbite/FlowbiteTheme.ts @@ -9,6 +9,7 @@ import type { FlowbiteButtonGroupTheme, FlowbiteButtonTheme } from "../Button"; import type { FlowbiteCardTheme } from "../Card"; import type { FlowbiteCarouselTheme } from "../Carousel"; import type { FlowbiteCheckboxTheme } from "../Checkbox"; +import type { FlowbiteClipboardTheme } from "../Clipboard"; import type { FlowbiteDarkThemeToggleTheme } from "../DarkThemeToggle"; import type { FlowbiteDatepickerTheme } from "../Datepicker"; import type { FlowbiteDrawerTheme } from "../Drawer"; @@ -56,6 +57,7 @@ export interface FlowbiteTheme { card: FlowbiteCardTheme; carousel: FlowbiteCarouselTheme; checkbox: FlowbiteCheckboxTheme; + clipboard: FlowbiteClipboardTheme; darkThemeToggle: FlowbiteDarkThemeToggleTheme; datepicker: FlowbiteDatepickerTheme; drawer: FlowbiteDrawerTheme; diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts index ac9ade2cd..f3a0ec786 100644 --- a/packages/ui/src/index.ts +++ b/packages/ui/src/index.ts @@ -10,6 +10,7 @@ export * from "./components/Button"; export * from "./components/Card"; export * from "./components/Carousel"; export * from "./components/Checkbox"; +export * from "./components/Clipboard"; export * from "./components/DarkThemeToggle"; export * from "./components/Datepicker"; export * from "./components/Drawer"; diff --git a/packages/ui/src/theme.ts b/packages/ui/src/theme.ts index 73d563ae3..1697b4b94 100644 --- a/packages/ui/src/theme.ts +++ b/packages/ui/src/theme.ts @@ -9,6 +9,7 @@ import { buttonGroupTheme, buttonTheme } from "./components/Button/theme"; import { cardTheme } from "./components/Card/theme"; import { carouselTheme } from "./components/Carousel/theme"; import { checkboxTheme } from "./components/Checkbox/theme"; +import { clipboardTheme } from "./components/Clipboard/theme"; import { darkThemeToggleTheme } from "./components/DarkThemeToggle/theme"; import { datePickerTheme } from "./components/Datepicker/theme"; import { drawerTheme } from "./components/Drawer/theme"; @@ -54,6 +55,7 @@ export const theme: FlowbiteTheme = { card: cardTheme, carousel: carouselTheme, checkbox: checkboxTheme, + clipboard: clipboardTheme, datepicker: datePickerTheme, darkThemeToggle: darkThemeToggleTheme, drawer: drawerTheme,