diff --git a/apps/www/app/playground/page.tsx b/apps/www/app/playground/page.tsx index 85c2bb0..45f51f2 100644 --- a/apps/www/app/playground/page.tsx +++ b/apps/www/app/playground/page.tsx @@ -1,6 +1,6 @@ "use client"; -import React from "react"; +import React, { useState } from "react"; import Card from "@/components/ui/card"; import { ModeToggle } from "@/components/ui/ModeToggle"; import { @@ -40,8 +40,16 @@ import { selectAnimationVariants, } from "ruru-ui/components/select"; import AnimationToggle from "@/components/animationToggle"; +import Modal, { ModalProvider } from "ruru-ui/components/modal"; const Playground = () => { + const handleSubmit = async () => { + // Your submit logic here + console.log("Submitted"); + // Simulate an API call or any async operation + await new Promise((resolve) => setTimeout(resolve, 1000)); + }; + return (
@@ -580,6 +588,64 @@ const Playground = () => { ))}
+ + {/* */} + + {/* setOpen(false)}> + + + Create Username + + Enter a unique name for your token to differentiate it from + other tokens and then select the scope. + + + + + + + + */} + {/* setOpen(false)} variant="secondary"> + Cancel + + + setOpen(false)}>Submit */} + {/* setOpen(false)} + variant="secondary" + > + Cancel + + + */} + + + Open Modal + + + + Create Username + + Enter a unique name for your token to differentiate it from + other tokens and then select the scope. + + + + + + + + Cancel + Submit + + + + +
); diff --git a/apps/www/components/preview/Modal/customWidth.tsx b/apps/www/components/preview/Modal/customWidth.tsx new file mode 100644 index 0000000..4bfbdc6 --- /dev/null +++ b/apps/www/components/preview/Modal/customWidth.tsx @@ -0,0 +1,56 @@ +"use client"; + +import React from "react"; +import { Button } from "ruru-ui/components/button"; +import { Input } from "ruru-ui/components/input"; +import Modal, { ModalProvider } from "ruru-ui/components/modal"; + +const CustomWidth = () => { + return ( + + + + + + + + + Custom Width + + + This modal opens with a Custom Width button. + + + + + + + + + Cancel + + + + + ); +}; + +export default CustomWidth; diff --git a/apps/www/components/preview/Modal/customWidth2.tsx b/apps/www/components/preview/Modal/customWidth2.tsx new file mode 100644 index 0000000..7a299c2 --- /dev/null +++ b/apps/www/components/preview/Modal/customWidth2.tsx @@ -0,0 +1,57 @@ +"use client"; + +import React from "react"; +import { Button } from "ruru-ui/components/button"; +import { Input } from "ruru-ui/components/input"; +import Modal, { ModalProvider } from "ruru-ui/components/modal"; + +const CustomWidth2 = () => { + return ( + + + + + + + + + Custom Width + + + This modal opens with a Custom Width button. + + + + + + + + + Cancel + + Submit + + + + ); +}; + +export default CustomWidth2; diff --git a/apps/www/components/preview/Modal/disabled.tsx b/apps/www/components/preview/Modal/disabled.tsx new file mode 100644 index 0000000..bd8a2c8 --- /dev/null +++ b/apps/www/components/preview/Modal/disabled.tsx @@ -0,0 +1,55 @@ +"use client"; + +import React from "react"; +import { Button } from "ruru-ui/components/button"; +import { Input } from "ruru-ui/components/input"; +import Modal, { ModalProvider } from "ruru-ui/components/modal"; + +const Disabled = () => { + return ( + + + + + + + + + Disabled + + + This modal opens with a Disabled button. + + + + + + + + Cancel + Submit + + + + ); +}; + +export default Disabled; diff --git a/apps/www/components/preview/Modal/preview.tsx b/apps/www/components/preview/Modal/preview.tsx new file mode 100644 index 0000000..24e46d0 --- /dev/null +++ b/apps/www/components/preview/Modal/preview.tsx @@ -0,0 +1,93 @@ +"use client"; + +import React, { useState } from "react"; +import Modal, { ModalProvider } from "ruru-ui/components/modal"; +import { Input } from "ruru-ui/components/input"; + +const Preview = () => { + const [isLoading, setIsLoading] = useState(false); + const [status, setStatus] = useState(false); + + const handleSubmit = async () => { + setIsLoading(true); + + await new Promise((resolve) => setTimeout(resolve, 2000)); + setStatus(true); + setIsLoading(false); + + await new Promise((resolve) => setTimeout(resolve, 500)); + + setStatus(false); + }; + + return ( +
+ + Deploy + + + + + Deploy to production + + + Deploy your site to production by clicking the button below. + + + + + + + + Cancel + + {status ? ( + + + + ) : ( + "Deploy" + )} + + + + +
+ ); +}; + +export default Preview; diff --git a/apps/www/components/preview/Modal/singleButton.tsx b/apps/www/components/preview/Modal/singleButton.tsx new file mode 100644 index 0000000..15dcbb7 --- /dev/null +++ b/apps/www/components/preview/Modal/singleButton.tsx @@ -0,0 +1,56 @@ +"use client"; + +import React from "react"; +import { Button } from "ruru-ui/components/button"; +import { Input } from "ruru-ui/components/input"; +import Modal, { ModalProvider } from "ruru-ui/components/modal"; + +const SingleButton = () => { + return ( + + + + + + + + + Single Button + + + This modal opens with a Single Button. + + + + + + + + + Cancel + + + + + ); +}; + +export default SingleButton; diff --git a/apps/www/components/preview/Modal/trigger.tsx b/apps/www/components/preview/Modal/trigger.tsx new file mode 100644 index 0000000..e424323 --- /dev/null +++ b/apps/www/components/preview/Modal/trigger.tsx @@ -0,0 +1,55 @@ +"use client"; + +import React from "react"; +import { Button } from "ruru-ui/components/button"; +import { Input } from "ruru-ui/components/input"; +import Modal, { ModalProvider } from "ruru-ui/components/modal"; + +const Trigger = () => { + return ( + + + + + + + + + Custom Trigger + + + This modal opens with a custom trigger button. + + + + + + + + Cancel + Submit + + + + ); +}; + +export default Trigger; diff --git a/apps/www/components/preview/Modal/usage.tsx b/apps/www/components/preview/Modal/usage.tsx new file mode 100644 index 0000000..855c688 --- /dev/null +++ b/apps/www/components/preview/Modal/usage.tsx @@ -0,0 +1,60 @@ +"use client"; + +import React, { useState } from "react"; +import { Input } from "ruru-ui/components/input"; +import Modal, { ModalProvider } from "ruru-ui/components/modal"; + +const Usage = () => { + const [isLoading, setIsLoading] = useState(false); + + const handleSubmit = async () => { + setIsLoading(true); + + await new Promise((resolve) => setTimeout(resolve, 2000)); + setIsLoading(false); + }; + + return ( +
+ + Open Modal + + + + + Create Username + + + Enter a unique name for your token to differentiate it from + other tokens and then select the scope. + + + + + + + + Cancel + + Submit + + + + +
+ ); +}; + +export default Usage; diff --git a/apps/www/components/preview/index.tsx b/apps/www/components/preview/index.tsx index cf81028..ca12b50 100644 --- a/apps/www/components/preview/index.tsx +++ b/apps/www/components/preview/index.tsx @@ -27,6 +27,7 @@ import { import { BadgePreview } from "../badgePreview"; import Tabspreview from "../tabs"; +import { default as ModalPreview } from "./Modal/preview"; export default { button: ( @@ -163,4 +164,9 @@ export default { ), + modal: ( + + + + ), } as Record; diff --git a/apps/www/content/docs/cli.mdx b/apps/www/content/docs/cli.mdx index 7611d31..803e2f2 100644 --- a/apps/www/content/docs/cli.mdx +++ b/apps/www/content/docs/cli.mdx @@ -1,5 +1,5 @@ --- -title: Cli +title: CLI description: The CLI is a command-line interface that allows you to add components to your project. --- @@ -93,7 +93,7 @@ initialize your project and install dependencies Options: -y, --yes skip confirmation prompt. (default: false) -d, --defaults use default configuration. (default: false) - -a, --autodetact autodetact configuration by freamwork. (default: false) + -a, --autodetect autodetect configuration by framework. (default: false) -c, --cwd the working directory. defaults to the current directory. (default: "D:\\GitHub\\ruru-ui\\apps\\sink") -h, --help display help for command diff --git a/apps/www/content/docs/components/modal.mdx b/apps/www/content/docs/components/modal.mdx new file mode 100644 index 0000000..bbabba9 --- /dev/null +++ b/apps/www/content/docs/components/modal.mdx @@ -0,0 +1,550 @@ +--- +title: Modal +description: The Modal component is used to display content in a modal dialog. +preview: modal +--- + + +import Modal, { ModalProvider } from "ruru-ui/components/modal"; +import { Tab, Tabs } from "fumadocs-ui/components/tabs"; +import { Tabs as Rutabs, Tab as Rutab } from "ruru-ui/components/tabs"; +import Usage from "../../../components/preview/Modal/usage.tsx"; +import Trigger from "../../../components/preview/Modal/trigger.tsx"; +import SingleButton from "../../../components/preview/Modal/singleButton.tsx"; +import Disabled from "../../../components/preview/Modal/disabled.tsx"; +import CustomWidth from "../../../components/preview/Modal/customWidth.tsx"; +import CustomWidth2 from "../../../components/preview/Modal/customWidth2.tsx"; +import Preview from "../../../components/preview/Modal/preview.tsx"; + +## Installation + + + + + +```bash +npx ruru-ui-cli@latest add modal +``` + + +```bash +pnpm dlx ruru-ui-cli@latest add modal +``` + + +```bash +npx ruru-ui-cli@latest add modal +``` + + +```bash +bunx --bun ruru-ui-cli@latest add modal +``` + + + + + +```package-install +npm install ruru-ui@latest +``` + + + + +## Usage + +Here is an example of how to use the `Modal` component. + + + + + + +```tsx +"use client"; + +import React, { useState } from "react"; +import { Input } from "ruru-ui/components/input"; +import Modal, { ModalProvider } from "ruru-ui/components/modal"; + +const Usage = () => { + const [isLoading, setIsLoading] = useState(false); + + const handleSubmit = async () => { + setIsLoading(true); + + await new Promise((resolve) => setTimeout(resolve, 2000)); + setIsLoading(false); + }; + + return ( +
+ + Open Modal + + + + + Create Username + + + Enter a unique name for your token to differentiate it from + other tokens and then select the scope. + + + + + + + + Cancel + + Submit + + + + +
+ ); +}; + +export default Usage; +``` +
+
+ + +## Example + +### Modal with Trigger + +The `Modal` component can be used with a custom trigger button. + + + + + + +```tsx +"use client"; + +import React from "react"; +import { Button } from "ruru-ui/components/button"; +import { Input } from "ruru-ui/components/input"; +import Modal, { ModalProvider } from "ruru-ui/components/modal"; + +const TriggerDemo = () => { + return ( + + + + + + + + + Custom Trigger + + + This modal opens with a custom trigger button. + + + + + + + + Cancel + Submit + + + + ); +}; + +export default TriggerDemo; +``` + + + + +### Single button + +The `Modal` component can be used with a single button. + + + + + + +```tsx +"use client"; + +import React from "react"; +import { Button } from "ruru-ui/components/button"; +import { Input } from "ruru-ui/components/input"; +import Modal, { ModalProvider } from "ruru-ui/components/modal"; + +const SingleButton = () => { + return ( + + + + + + + + + Custom Trigger + + + This modal opens with a custom trigger button. + + + + + + + + Cancel + + + + ); +}; + +export default SingleButton; +``` + + + +### Disabled actions + +The `Modal` component can be used with disabled actions. + + + + + + +```tsx +"use client"; + +import React from "react"; +import { Button } from "ruru-ui/components/button"; +import { Input } from "ruru-ui/components/input"; +import Modal, { ModalProvider } from "ruru-ui/components/modal"; + +const Disabled = () => { + return ( + + + + + + + + + Custom Trigger + + + This modal opens with a custom trigger button. + + + + + + + + Cancel + Submit + + + + ); +}; + +export default Disabled; +``` + + + +### Modal with custom width + +The `Modal` component can be used with a custom width. + + + + + + +```tsx +"use client"; + +import React from "react"; +import { Button } from "ruru-ui/components/button"; +import { Input } from "ruru-ui/components/input"; +import Modal, { ModalProvider } from "ruru-ui/components/modal"; + +const CustomWidth = () => { + return ( + + + + + + + + + Custom Width + + + This modal opens with a Custom Width button. + + + + + + + + Cancel + + + + ); +}; + +export default CustomWidth; +``` + + + + + + + + +```tsx +"use client"; + +import React from "react"; +import { Button } from "ruru-ui/components/button"; +import { Input } from "ruru-ui/components/input"; +import Modal, { ModalProvider } from "ruru-ui/components/modal"; + +const CustomWidth2 = () => { + return ( + + + + + + + + + Custom Width + + + This modal opens with a Custom Width button. + + + + + + + + + Cancel + + Submit + + + + ); +}; + +export default CustomWidth2; +``` + + + +### Preview Component + +The component that you see on preview + + + + + + +```tsx +"use client"; + +import React, { useState } from "react"; +import Modal, { ModalProvider } from "ruru-ui/components/modal"; +import { Input } from "ruru-ui/components/input"; + +const Preview = () => { + const [isLoading, setIsLoading] = useState(false); + const [status, setStatus] = useState(false); + + const handleSubmit = async () => { + setIsLoading(true); + + await new Promise((resolve) => setTimeout(resolve, 2000)); + setStatus(true); + setIsLoading(false); + + await new Promise((resolve) => setTimeout(resolve, 500)); + + setStatus(false); + }; + + return ( +
+ + Deploy + + + + + Deploy to production + + + Deploy your site to production by clicking the button below. + + + + + + + + Cancel + + {status ? ( + + + + ) : ( + "Deploy" + )} + + + + +
+ ); +}; + +export default Preview; +``` +
+
+ +## Props + +Here's how the props table would look for the `Modal` component, formatted in the same style: + +## Props + +### Modal + +| Name | Type | Default | Description | +| ----------------- | --------------------------------- | ----------- | -------------------------------------------------------------------------------------- | +| **children** | **ReactNode** | `undefined` | The children of the Modal component. | +| **onClickOutside**| **() => void** | `undefined` | The function to call when the user clicks outside the modal. | + +### ModalAction + +| Name | Type | Default | Description | +| ----------------- | --------------------------------- | ----------- | -------------------------------------------------------------------------------------- | +| **fullWidth** | **boolean** | `false` | Determines if the action button should take the full width. | +| **onClick** | **() =\> void \| Promise\** | `undefined` | The function to call when the user clicks the action. | + +### Trigger + +| Name | Type | Default | Description | +| ----------------- | --------------------------------- | ----------- | -------------------------------------------------------------------------------------- | +| **children** | **ReactNode** | `undefined` | The children of the Modal component. | +| **onClick** | **() => void** | `undefined` | The function to call when the user clicks the trigger. | +| **asChild** | **boolean** | `false` | Render as a child component. | + +### DivProps + +| Name | Type | Default | Description | +| ----------------- | --------------------------------- | ----------- | -------------------------------------------------------------------------------------- | +| **className** | **string** | `""` | Additional class names for the container. | +| **children** | **ReactNode** | `undefined` | The children of the component. | + +### PTagProps + +| Name | Type | Default | Description | +| ----------------- | --------------------------------- | ----------- | -------------------------------------------------------------------------------------- | +| **className** | **string** | `""` | Additional class names for the paragraph element. | +| **children** | **ReactNode** | `undefined` | The children of the paragraph element. | + diff --git a/apps/www/content/docs/components/textarea.mdx b/apps/www/content/docs/components/textarea.mdx index 0c06252..f794464 100644 --- a/apps/www/content/docs/components/textarea.mdx +++ b/apps/www/content/docs/components/textarea.mdx @@ -11,38 +11,38 @@ import { Tabs as Rutabs, Tab as Rutab } from "ruru-ui/components/tabs"; ## Installation - - - - ```bash - npx ruru-ui-cli@latest add textarea - ``` - - - ```bash - pnpm dlx ruru-ui-cli@latest add textarea - ``` - - - ```bash - npx ruru-ui-cli@latest add textarea - ``` - - - ```bash - bunx --bun ruru-ui-cli@latest add textarea - ``` - - + + + +```bash +npx ruru-ui-cli@latest add textarea +``` + + +```bash +pnpm dlx ruru-ui-cli@latest add textarea +``` + + +```bash +npx ruru-ui-cli@latest add textarea +``` + + +```bash +bunx --bun ruru-ui-cli@latest add textarea +``` + + - - + + - ```package-install - npm install ruru-ui@latest - ``` +```package-install +npm install ruru-ui@latest +``` - + ## Usage diff --git a/apps/www/public/registry/components/button.json b/apps/www/public/registry/components/button.json index 6dba07e..06e4d60 100644 --- a/apps/www/public/registry/components/button.json +++ b/apps/www/public/registry/components/button.json @@ -4,7 +4,7 @@ "files": [ { "name": "button.tsx", - "content": "\"use client\";\n\nimport { Slot } from \"@radix-ui/react-slot\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport React from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport { Spinner } from \"./spinner\";\nimport { motion } from \"framer-motion\";\nimport { useRuru } from \"@/provider\";\n\nexport const buttonVariants = cva(\n \"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50\",\n {\n variants: {\n variant: {\n default:\n \"bg-primary text-primary-foreground shadow hover:bg-primary/85 hover:shadow-md\",\n secondary:\n \"border-input border-[1.5px] bg-primary-foreground hover:bg-[#f3f3f3] dark:hover:bg-[#202020]\",\n tertiary: \"text-primary hover:bg-[#f3f3f3] dark:hover:bg-[#202020]\",\n error: \"bg-[#d93036] hover:bg-[#ff6166]\",\n warning: \"bg-[#ff990a] text-primary-foreground hover:bg-[#d27504]\",\n },\n size: {\n default: \"h-9 px-4 py-2\",\n small: \"h-8 rounded-md px-3 text-xs\",\n large: \"h-10 rounded-md px-8\",\n icon: \"size-9\",\n },\n },\n defaultVariants: {\n variant: \"default\",\n size: \"default\",\n },\n },\n);\n\nexport interface ButtonProps\n extends Omit<\n React.ButtonHTMLAttributes,\n \"prefix\" | \"suffix\"\n >,\n VariantProps {\n \n asChild?: boolean;\n \n prefix?: React.ReactNode;\n \n suffix?: React.ReactNode;\n \n disabled?: boolean;\n \n loading?: boolean;\n}\n\nexport const Button = React.forwardRef(\n (\n {\n className,\n variant = \"default\",\n size = \"default\",\n asChild = false,\n prefix,\n suffix,\n disabled = false,\n loading = false,\n ...props\n },\n ref,\n ) => {\n const { animation } = useRuru();\n\n const Comp = asChild ? Slot : \"button\";\n\n const buttonContent = (\n \n {loading ? : null}\n {prefix ? (\n \n {prefix}\n \n ) : null}\n {props.children}\n {suffix ? (\n \n {suffix}\n \n ) : null}\n \n );\n\n return (\n
\n {animation ? (\n {buttonContent}\n ) : (\n buttonContent\n )}\n
\n );\n },\n);\n\nButton.displayName = \"Button\";\n" + "content": "\"use client\";\n\nimport { Slot } from \"@radix-ui/react-slot\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport React from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport { Spinner } from \"./spinner\";\nimport { motion } from \"framer-motion\";\nimport { useRuru } from \"@/provider\";\n\nexport const buttonVariants = cva(\n \"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50\",\n {\n variants: {\n variant: {\n default:\n \"bg-primary text-primary-foreground shadow hover:bg-primary/85 hover:shadow-md\",\n secondary:\n \"border-input border-[1.5px] bg-primary-foreground hover:bg-[#f3f3f3] dark:hover:bg-[#202020]\",\n tertiary: \"text-primary hover:bg-[#f3f3f3] dark:hover:bg-[#202020]\",\n error: \"bg-[#d93036] hover:bg-[#ff6166]\",\n warning: \"bg-[#ff990a] text-primary-foreground hover:bg-[#d27504]\",\n },\n size: {\n default: \"h-9 px-4 py-2\",\n small: \"h-8 rounded-md px-3 text-xs\",\n large: \"h-10 rounded-md px-8\",\n icon: \"size-9\",\n },\n },\n defaultVariants: {\n variant: \"default\",\n size: \"default\",\n },\n },\n);\n\nexport interface ButtonProps\n extends Omit<\n React.ButtonHTMLAttributes,\n \"prefix\" | \"suffix\"\n >,\n VariantProps {\n \n asChild?: boolean;\n \n prefix?: React.ReactNode;\n \n suffix?: React.ReactNode;\n \n disabled?: boolean;\n \n loading?: boolean;\n}\n\nexport const Button = React.forwardRef(\n (\n {\n className,\n variant = \"default\",\n size = \"default\",\n asChild = false,\n prefix,\n suffix,\n disabled = false,\n loading = false,\n ...props\n },\n ref,\n ) => {\n const { animation } = useRuru();\n\n const Comp = asChild ? Slot : \"button\";\n\n const buttonContent = (\n \n {loading ? : null}\n {prefix ? (\n \n {prefix}\n \n ) : null}\n {props.children}\n {suffix ? (\n \n {suffix}\n \n ) : null}\n \n );\n\n return (\n
\n {animation ? (\n {buttonContent}\n ) : (\n buttonContent\n )}\n
\n );\n },\n);\n\nButton.displayName = \"Button\";\n" } ], "type": "components:ui", diff --git a/apps/www/public/registry/components/modal.json b/apps/www/public/registry/components/modal.json new file mode 100644 index 0000000..9389d04 --- /dev/null +++ b/apps/www/public/registry/components/modal.json @@ -0,0 +1,11 @@ +{ + "name": "modal", + "files": [ + { + "name": "modal.tsx", + "content": "\"use client\";\n\nimport React, {\n useState,\n useContext,\n useCallback,\n ReactNode,\n createContext,\n useEffect,\n} from \"react\";\nimport {\n AnimatePresence,\n ForwardRefComponent,\n HTMLMotionProps,\n motion,\n} from \"framer-motion\";\nimport { cn } from \"@/utils/cn\";\nimport { Button, ButtonProps } from \"./button\";\n\n\nexport interface ModalContextProps {\n \n isOpen: boolean;\n \n openModal: () => void;\n \n closeModal: () => void;\n}\n\n\nexport interface ModalProps\n extends Partial>> {\n \n children: ReactNode;\n \n onClickOutside?: () => void;\n}\n\n\nexport interface ModalActionProps extends ButtonProps {\n \n fullWidth?: boolean;\n \n onClick?: () => void | Promise;\n}\n\n\nexport interface TriggerProps extends ButtonProps {\n \n children: ReactNode;\n \n onClick?: () => void;\n \n asChild?: boolean;\n}\n\n\nexport interface DivProps\n extends React.DetailedHTMLProps<\n React.HTMLAttributes,\n HTMLDivElement\n > {}\n\n\nexport interface PTagProps\n extends React.DetailedHTMLProps<\n React.HTMLAttributes,\n HTMLParagraphElement\n > {}\n\n\nconst ModalContext = createContext(undefined);\n\n\nexport const ModalProvider = ({\n children,\n}: {\n children: ReactNode;\n}): React.ReactElement => {\n const [isOpen, setIsOpen] = useState(false);\n\n const openModal = useCallback(() => setIsOpen(true), []);\n const closeModal = useCallback(() => setIsOpen(false), []);\n\n return (\n \n {children}\n \n );\n};\n\n\nexport const useModal = (): ModalContextProps => {\n const context = useContext(ModalContext);\n if (!context) {\n throw new Error(\"useModal must be used within a ModalProvider\");\n }\n return context;\n};\n\n\nconst Modal = ({\n children,\n onClickOutside,\n ...props\n}: ModalProps): React.ReactElement => {\n const { isOpen, closeModal } = useModal();\n\n useEffect(() => {\n const handleEscape = (event: KeyboardEvent) => {\n if (event.key === \"Escape\") {\n closeModal();\n }\n };\n\n if (isOpen) {\n document.addEventListener(\"keydown\", handleEscape);\n } else {\n document.removeEventListener(\"keydown\", handleEscape);\n }\n\n return () => document.removeEventListener(\"keydown\", handleEscape);\n }, [isOpen, closeModal]);\n\n const modalVariants = {\n hidden: {\n opacity: 0,\n y: -50,\n rotateX: \"0deg\",\n transition: { duration: 0.15 },\n },\n visible: {\n opacity: 1,\n y: 0,\n rotateX: \"0deg\",\n transition: { duration: 0.15 },\n },\n exit: {\n opacity: 0,\n y: -50,\n rotateX: \"-10deg\",\n transition: { duration: 0.15 },\n },\n };\n\n return (\n \n {isOpen && (\n \n e.stopPropagation()}\n variants={modalVariants}\n initial=\"hidden\"\n animate=\"visible\"\n exit=\"exit\"\n >\n {children}\n \n \n )}\n \n );\n};\n\n\nModal.Trigger = ({\n children,\n onClick,\n asChild = false,\n}: TriggerProps): React.ReactElement => {\n const { openModal } = useModal();\n\n const handleClick = () => {\n openModal();\n if (onClick) {\n onClick();\n }\n };\n\n if (asChild) {\n return (\n
\n {children}\n
\n );\n }\n\n return ;\n};\n\n\nModal.Body = ({ children, ...props }: DivProps): React.ReactElement => (\n
\n {children}\n
\n);\n\n\nModal.Header = ({\n children,\n className,\n ...props\n}: DivProps): React.ReactElement => (\n
\n {children}\n
\n);\n\n\nModal.Title = ({\n children,\n className,\n ...props\n}: PTagProps): React.ReactElement => (\n

\n {children}\n

\n);\n\n\nModal.Content = ({\n children,\n className,\n ...props\n}: DivProps): React.ReactElement => (\n
\n {children}\n
\n);\n\n\nModal.Subtitle = ({\n children,\n className,\n ...props\n}: DivProps): React.ReactElement => (\n

\n {children}\n

\n);\n\n\nModal.Actions = ({\n children,\n className,\n ...props\n}: DivProps): React.ReactElement => (\n \n {children}\n \n);\n\n\nModal.Action = ({\n fullWidth = false,\n onClick,\n className,\n ...props\n}: ModalActionProps): React.ReactElement => {\n const { closeModal } = useModal();\n\n const handleClick = async () => {\n if (onClick) {\n await onClick();\n }\n closeModal();\n };\n\n return (\n \n {props.children}\n \n );\n};\n\n\nModal.Close = (props: ModalActionProps): React.ReactElement => (\n \n {props.children}\n \n);\n\nexport default Modal;\n" + } + ], + "type": "components:ui", + "subcategory": ["button"] +} diff --git a/apps/www/public/registry/index.json b/apps/www/public/registry/index.json index 859e7dc..64eee53 100644 --- a/apps/www/public/registry/index.json +++ b/apps/www/public/registry/index.json @@ -60,5 +60,11 @@ "dependencies": ["@radix-ui/react-tooltip"], "files": ["tooltip.tsx"], "type": "components:ui" + }, + { + "name": "modal", + "files": ["modal.tsx"], + "type": "components:ui", + "subcategory": ["button"] } ] diff --git a/apps/www/registry/ui.ts b/apps/www/registry/ui.ts index a0a720a..143c2aa 100644 --- a/apps/www/registry/ui.ts +++ b/apps/www/registry/ui.ts @@ -63,4 +63,10 @@ export const ui: Registry = [ dependencies: ["@radix-ui/react-tooltip"], files: ["tooltip.tsx"], }, + { + name: "modal", + type: "components:ui", + files: ["modal.tsx"], + subcategory: ["button"], + }, ]; diff --git a/packages/cli/package.json b/packages/cli/package.json index 31fc2b9..18aa60f 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "ruru-ui-cli", - "version": "0.0.4", + "version": "0.0.5", "description": "Ruru UI - CLI - add components and dependencies to your project", "keywords": [ "cli", diff --git a/packages/cli/src/commands/init.ts b/packages/cli/src/commands/init.ts index b060946..ea32064 100644 --- a/packages/cli/src/commands/init.ts +++ b/packages/cli/src/commands/init.ts @@ -52,7 +52,7 @@ const PROJECT_DEPENDENCIES = [ const initOptionsSchema = z.object({ defaults: z.boolean().default(false), - autodetact: z.boolean().default(false), + autodetect: z.boolean().default(false), yes: z.boolean(), cwd: z.string(), }); @@ -62,7 +62,7 @@ export const init = new Command() .description("initialize your project and install dependencies") .option("-y, --yes", "skip confirmation prompt.", false) .option("-d, --defaults", "use default configuration.", false) - .option("-a, --autodetact", "autodetact configuration by freamwork.", false) + .option("-a, --autodetect ", "autodetect configuration by framework.", false) .option( "-c, --cwd ", "the working directory. defaults to the current directory.", @@ -79,7 +79,7 @@ export const init = new Command() const projectConfig = await getProjectConfig(cwd); - if (options.autodetact && projectConfig) { + if (options.autodetect && projectConfig) { console.log(pc.green(pc.bold("\n config found! \n "))); const config = await promptForMinimalConfig( diff --git a/packages/ui/src/components/button.tsx b/packages/ui/src/components/button.tsx index b5a09f3..fea44b4 100644 --- a/packages/ui/src/components/button.tsx +++ b/packages/ui/src/components/button.tsx @@ -165,7 +165,7 @@ export const Button = React.forwardRef( ); return ( -
+
{animation ? ( {buttonContent} ) : ( diff --git a/packages/ui/src/components/modal.tsx b/packages/ui/src/components/modal.tsx new file mode 100644 index 0000000..017e5d7 --- /dev/null +++ b/packages/ui/src/components/modal.tsx @@ -0,0 +1,421 @@ +"use client"; + +import React, { + useState, + useContext, + useCallback, + ReactNode, + createContext, + useEffect, +} from "react"; +import { + AnimatePresence, + ForwardRefComponent, + HTMLMotionProps, + motion, +} from "framer-motion"; +import { cn } from "@/utils/cn"; +import { Button, ButtonProps } from "./button"; + +/** + * Represents the props for the Modal component. + */ +export interface ModalContextProps { + /** + * The state of the modal. + * + * @type {boolean} + */ + isOpen: boolean; + /** + * + * Open the modal. + * + * @returns {void} + */ + openModal: () => void; + /** + * + * Close the modal. + * + * @returns {void} + */ + closeModal: () => void; +} + +/** + * Represents the props for the Modal component. + */ +export interface ModalProps + extends Partial>> { + /** + * The children of the Modal component. + */ + children: ReactNode; + /** + * + * The function to call when the user clicks outside the modal. + * + * @returns {void} + */ + onClickOutside?: () => void; +} + +/** + * Represents the props for the Modal component. + */ +export interface ModalActionProps extends ButtonProps { + /** + * The children of the Modal component. + */ + fullWidth?: boolean; + /** + * The function to call when the user clicks the action. + * + * @returns {void} | {Promise} + */ + onClick?: () => void | Promise; +} + +/** + * Represents the props for the Modal component. + */ +export interface TriggerProps extends ButtonProps { + /** + * The children of the Modal component. + */ + children: ReactNode; + /** + * + * The function to call when the user clicks the trigger. + * + * @returns {void} + */ + onClick?: () => void; + /** + * + * Render as child component. + * + * @default false + * @type {boolean} + */ + asChild?: boolean; +} + +/** + * Represents the props for the Modal component. + */ +export interface DivProps + extends React.DetailedHTMLProps< + React.HTMLAttributes, + HTMLDivElement + > {} + +/** + * Represents the props for the Modal component. + */ +export interface PTagProps + extends React.DetailedHTMLProps< + React.HTMLAttributes, + HTMLParagraphElement + > {} + +/** + * Represents the Modal component. + * + * @param {ModalProps} props - The props for the Modal component. + * @returns {React.ReactElement} + * + */ +const ModalContext = createContext(undefined); + +/** + * Represents the Modal component. + * + * @param {ModalProps} props - The props for the Modal component. + * @returns {React.ReactElement} + * + */ +export const ModalProvider = ({ + children, +}: { + children: ReactNode; +}): React.ReactElement => { + const [isOpen, setIsOpen] = useState(false); + + const openModal = useCallback(() => setIsOpen(true), []); + const closeModal = useCallback(() => setIsOpen(false), []); + + return ( + + {children} + + ); +}; + +/** + * Represents the Modal component. + * + * @returns {ModalContextProps} + * + */ +export const useModal = (): ModalContextProps => { + const context = useContext(ModalContext); + if (!context) { + throw new Error("useModal must be used within a ModalProvider"); + } + return context; +}; + +/** + * Represents the Modal component. + * + * @param {ModalProps} props - The props for the Modal component. + * @returns {React.ReactElement} + * + */ +const Modal = ({ + children, + onClickOutside, + ...props +}: ModalProps): React.ReactElement => { + const { isOpen, closeModal } = useModal(); + + useEffect(() => { + const handleEscape = (event: KeyboardEvent) => { + if (event.key === "Escape") { + closeModal(); + } + }; + + if (isOpen) { + document.addEventListener("keydown", handleEscape); + } else { + document.removeEventListener("keydown", handleEscape); + } + + return () => document.removeEventListener("keydown", handleEscape); + }, [isOpen, closeModal]); + + const modalVariants = { + hidden: { + opacity: 0, + y: -50, + rotateX: "0deg", + transition: { duration: 0.15 }, + }, + visible: { + opacity: 1, + y: 0, + rotateX: "0deg", + transition: { duration: 0.15 }, + }, + exit: { + opacity: 0, + y: -50, + rotateX: "-10deg", + transition: { duration: 0.15 }, + }, + }; + + return ( + + {isOpen && ( + + e.stopPropagation()} + variants={modalVariants} + initial="hidden" + animate="visible" + exit="exit" + > + {children} + + + )} + + ); +}; + +/** + * Represents the Modal component. + * + * @param {TriggerProps} props - The props for the Modal component. + * @returns {React.ReactElement} + */ +Modal.Trigger = ({ + children, + onClick, + asChild = false, +}: TriggerProps): React.ReactElement => { + const { openModal } = useModal(); + + const handleClick = () => { + openModal(); + if (onClick) { + onClick(); + } + }; + + if (asChild) { + return ( +
+ {children} +
+ ); + } + + return ; +}; + +/** + * Represents the Modal component. + * + * @param {DivProps} props - The props for the Modal component. + * @returns {React.ReactElement} + */ +Modal.Body = ({ children, ...props }: DivProps): React.ReactElement => ( +
+ {children} +
+); + +/** + * Represents the Modal component. + * + * @param {DivProps} props - The props for the Modal component. + * @returns {React.ReactElement} + */ +Modal.Header = ({ + children, + className, + ...props +}: DivProps): React.ReactElement => ( +
+ {children} +
+); + +/** + * Represents the Modal component. + * + * @param {PTagProps} props - The props for the Modal component. + * @returns {React.ReactElement} + */ +Modal.Title = ({ + children, + className, + ...props +}: PTagProps): React.ReactElement => ( +

+ {children} +

+); + +/** + * Represents the Modal component. + * + * @param {DivProps} props - The props for the Modal component. + * @returns {React.ReactElement} + */ +Modal.Content = ({ + children, + className, + ...props +}: DivProps): React.ReactElement => ( +
+ {children} +
+); + +/** + * Represents the Modal component. + * + * @param {DivProps} props - The props for the Modal component. + * @returns {React.ReactElement} + */ +Modal.Subtitle = ({ + children, + className, + ...props +}: DivProps): React.ReactElement => ( +

+ {children} +

+); + +/** + * Represents the Modal component. + * + * @param {DivProps} props - The props for the Modal component. + * @returns {React.ReactElement} + */ +Modal.Actions = ({ + children, + className, + ...props +}: DivProps): React.ReactElement => ( +
+ {children} +
+); + +/** + * Represents the Modal component. + * + * @param {ModalActionProps} props - The props for the Modal component. + * @returns {React.ReactElement} + */ +Modal.Action = ({ + fullWidth = false, + onClick, + className, + ...props +}: ModalActionProps): React.ReactElement => { + const { closeModal } = useModal(); + + const handleClick = async () => { + if (onClick) { + await onClick(); + } + closeModal(); + }; + + return ( + + ); +}; + +/** + * Represents the Modal component. + * + * @param {ModalActionProps} props - The props for the Modal component. + * @returns {React.ReactElement} + */ +Modal.Close = (props: ModalActionProps): React.ReactElement => ( + + {props.children} + +); + +export default Modal; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f1bdaf1..f20cc7c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -371,7 +371,7 @@ importers: version: 1.0.7(tailwindcss@3.4.4) tsup: specifier: ^8.1.1 - version: 8.1.1(@swc/core@1.6.13)(postcss@8.4.39)(tsx@4.17.0)(typescript@5.5.3) + version: 8.1.1(@swc/core@1.6.13)(postcss@8.4.39)(typescript@5.5.3) zod: specifier: ^3.23.8 version: 3.23.8 @@ -402,7 +402,7 @@ importers: version: 8.4.39 postcss-cli: specifier: ^11.0.0 - version: 11.0.0(postcss@8.4.39)(tsx@4.17.0) + version: 11.0.0(postcss@8.4.39) postcss-lightningcss: specifier: ^1.0.0 version: 1.0.0(postcss@8.4.39) @@ -1080,6 +1080,7 @@ packages: cpu: [ppc64] os: [aix] requiresBuild: true + dev: false optional: true /@esbuild/android-arm64@0.23.0: @@ -1088,6 +1089,7 @@ packages: cpu: [arm64] os: [android] requiresBuild: true + dev: false optional: true /@esbuild/android-arm@0.23.0: @@ -1096,6 +1098,7 @@ packages: cpu: [arm] os: [android] requiresBuild: true + dev: false optional: true /@esbuild/android-x64@0.23.0: @@ -1104,6 +1107,7 @@ packages: cpu: [x64] os: [android] requiresBuild: true + dev: false optional: true /@esbuild/darwin-arm64@0.23.0: @@ -1112,6 +1116,7 @@ packages: cpu: [arm64] os: [darwin] requiresBuild: true + dev: false optional: true /@esbuild/darwin-x64@0.23.0: @@ -1120,6 +1125,7 @@ packages: cpu: [x64] os: [darwin] requiresBuild: true + dev: false optional: true /@esbuild/freebsd-arm64@0.23.0: @@ -1128,6 +1134,7 @@ packages: cpu: [arm64] os: [freebsd] requiresBuild: true + dev: false optional: true /@esbuild/freebsd-x64@0.23.0: @@ -1136,6 +1143,7 @@ packages: cpu: [x64] os: [freebsd] requiresBuild: true + dev: false optional: true /@esbuild/linux-arm64@0.23.0: @@ -1144,6 +1152,7 @@ packages: cpu: [arm64] os: [linux] requiresBuild: true + dev: false optional: true /@esbuild/linux-arm@0.23.0: @@ -1152,6 +1161,7 @@ packages: cpu: [arm] os: [linux] requiresBuild: true + dev: false optional: true /@esbuild/linux-ia32@0.23.0: @@ -1160,6 +1170,7 @@ packages: cpu: [ia32] os: [linux] requiresBuild: true + dev: false optional: true /@esbuild/linux-loong64@0.23.0: @@ -1168,6 +1179,7 @@ packages: cpu: [loong64] os: [linux] requiresBuild: true + dev: false optional: true /@esbuild/linux-mips64el@0.23.0: @@ -1176,6 +1188,7 @@ packages: cpu: [mips64el] os: [linux] requiresBuild: true + dev: false optional: true /@esbuild/linux-ppc64@0.23.0: @@ -1184,6 +1197,7 @@ packages: cpu: [ppc64] os: [linux] requiresBuild: true + dev: false optional: true /@esbuild/linux-riscv64@0.23.0: @@ -1192,6 +1206,7 @@ packages: cpu: [riscv64] os: [linux] requiresBuild: true + dev: false optional: true /@esbuild/linux-s390x@0.23.0: @@ -1200,6 +1215,7 @@ packages: cpu: [s390x] os: [linux] requiresBuild: true + dev: false optional: true /@esbuild/linux-x64@0.23.0: @@ -1208,6 +1224,7 @@ packages: cpu: [x64] os: [linux] requiresBuild: true + dev: false optional: true /@esbuild/netbsd-x64@0.23.0: @@ -1216,6 +1233,7 @@ packages: cpu: [x64] os: [netbsd] requiresBuild: true + dev: false optional: true /@esbuild/openbsd-arm64@0.23.0: @@ -1224,6 +1242,7 @@ packages: cpu: [arm64] os: [openbsd] requiresBuild: true + dev: false optional: true /@esbuild/openbsd-x64@0.23.0: @@ -1232,6 +1251,7 @@ packages: cpu: [x64] os: [openbsd] requiresBuild: true + dev: false optional: true /@esbuild/sunos-x64@0.23.0: @@ -1240,6 +1260,7 @@ packages: cpu: [x64] os: [sunos] requiresBuild: true + dev: false optional: true /@esbuild/win32-arm64@0.23.0: @@ -1248,6 +1269,7 @@ packages: cpu: [arm64] os: [win32] requiresBuild: true + dev: false optional: true /@esbuild/win32-ia32@0.23.0: @@ -1256,6 +1278,7 @@ packages: cpu: [ia32] os: [win32] requiresBuild: true + dev: false optional: true /@esbuild/win32-x64@0.23.0: @@ -1264,6 +1287,7 @@ packages: cpu: [x64] os: [win32] requiresBuild: true + dev: false optional: true /@eslint-community/eslint-utils@4.4.0(eslint@8.57.0): @@ -4890,6 +4914,7 @@ packages: '@esbuild/win32-arm64': 0.23.0 '@esbuild/win32-ia32': 0.23.0 '@esbuild/win32-x64': 0.23.0 + dev: false /escalade@3.1.2: resolution: {integrity: sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==} @@ -8386,7 +8411,7 @@ packages: engines: {node: '>= 0.4'} dev: true - /postcss-cli@11.0.0(postcss@8.4.39)(tsx@4.17.0): + /postcss-cli@11.0.0(postcss@8.4.39): resolution: {integrity: sha512-xMITAI7M0u1yolVcXJ9XTZiO9aO49mcoKQy6pCDFdMh9kGqhzLVpWxeD/32M/QBmkhcGypZFFOLNLmIW4Pg4RA==} engines: {node: '>=18'} hasBin: true @@ -8400,7 +8425,7 @@ packages: globby: 14.0.2 picocolors: 1.0.1 postcss: 8.4.39 - postcss-load-config: 5.1.0(postcss@8.4.39)(tsx@4.17.0) + postcss-load-config: 5.1.0(postcss@8.4.39) postcss-reporter: 7.1.0(postcss@8.4.39) pretty-hrtime: 1.0.3 read-cache: 1.0.0 @@ -8458,7 +8483,7 @@ packages: postcss: 8.4.39 yaml: 2.4.5 - /postcss-load-config@5.1.0(postcss@8.4.39)(tsx@4.17.0): + /postcss-load-config@5.1.0(postcss@8.4.39): resolution: {integrity: sha512-G5AJ+IX0aD0dygOE0yFZQ/huFFMSNneyfp0e3/bT05a8OfPC5FUoZRPfGijUdGOJNMewJiwzcHJXFafFzeKFVA==} engines: {node: '>= 18'} peerDependencies: @@ -8475,10 +8500,31 @@ packages: dependencies: lilconfig: 3.1.2 postcss: 8.4.39 - tsx: 4.17.0 yaml: 2.4.5 dev: true + /postcss-load-config@6.0.1(postcss@8.4.39): + resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==} + engines: {node: '>= 18'} + peerDependencies: + jiti: '>=1.21.0' + postcss: '>=8.0.9' + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + jiti: + optional: true + postcss: + optional: true + tsx: + optional: true + yaml: + optional: true + dependencies: + lilconfig: 3.1.2 + postcss: 8.4.39 + dev: false + /postcss-load-config@6.0.1(postcss@8.4.39)(tsx@4.17.0): resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==} engines: {node: '>= 18'} @@ -9896,7 +9942,7 @@ packages: /tslib@2.6.2: resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} - /tsup@8.1.1(@swc/core@1.6.13)(postcss@8.4.39)(tsx@4.17.0)(typescript@5.5.3): + /tsup@8.1.1(@swc/core@1.6.13)(postcss@8.4.39)(typescript@5.5.3): resolution: {integrity: sha512-lLXP3BshJ6y/32b3tPZUB2siD2mkJ6mLzhbPOShfjogSc3aRw8MhbBV4cPKbqkbXuhsJR+c9B0W9RHMbtbXLMQ==} engines: {node: '>=18'} hasBin: true @@ -9926,7 +9972,7 @@ packages: globby: 11.1.0 joycon: 3.1.1 postcss: 8.4.39 - postcss-load-config: 6.0.1(postcss@8.4.39)(tsx@4.17.0) + postcss-load-config: 6.0.1(postcss@8.4.39) resolve-from: 5.0.0 rollup: 4.18.1 source-map: 0.8.0-beta.0 @@ -10001,6 +10047,7 @@ packages: get-tsconfig: 4.7.5 optionalDependencies: fsevents: 2.3.3 + dev: false /turbo-darwin-64@2.0.9: resolution: {integrity: sha512-owlGsOaExuVGBUfrnJwjkL1BWlvefjSKczEAcpLx4BI7Oh6ttakOi+JyomkPkFlYElRpjbvlR2gP8WIn6M/+xQ==}