diff --git a/apps/www/components/preview/accordion/accordionVariants.tsx b/apps/www/components/preview/accordion/accordionVariants.tsx new file mode 100644 index 0000000..1af4103 --- /dev/null +++ b/apps/www/components/preview/accordion/accordionVariants.tsx @@ -0,0 +1,107 @@ +"use client"; + +import { VariantProps } from "class-variance-authority"; +import React from "react"; +import { + Accordion, + Accordions, + AccordionsVariants, +} from "ruru-ui/components/accordion"; +import { Checkbox } from "ruru-ui/components/checkbox"; +import { Label } from "ruru-ui/components/label"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "ruru-ui/components/select"; + +type Props = {}; + +export default function AccordionVariants({}: Props) { + const [variant, setVariant] = + React.useState["variant"]>( + "default", + ); + + const [theme, setTheme] = + React.useState["theme"]>("default"); + + const [showCopyButton, setShowCopyButton] = React.useState(false); + + return ( +
+
+ + + + +
+ setShowCopyButton(!!c)} + /> + +
+
+ + + {Array.from(new Array(4)).map((_, index) => ( + + Accordion Item {index + 1} + + } + > + This is the content of the accordion item {index + 1} + + ))} + +
+ ); +} diff --git a/apps/www/components/preview/index.tsx b/apps/www/components/preview/index.tsx index 298fa93..c86ed54 100644 --- a/apps/www/components/preview/index.tsx +++ b/apps/www/components/preview/index.tsx @@ -25,6 +25,7 @@ import { SelectValue, } from "ruru-ui/components/select"; import { Label } from "ruru-ui/components/label"; +import { Accordions, Accordion } from "ruru-ui/components/accordion"; import { BadgePreview } from "../badgePreview"; import Tabspreview from "../tabs"; @@ -185,5 +186,24 @@ export default { ), + accordion: ( + + + + Yes. It adheres to the WAI-ARIA design pattern. + + + Yes. It is responsive and mobile-friendly. + + + Yes. It is customizable and supports multiple themes and variants. But + the core components can also be imported for custom styling. + + + Yes. It is animated and supports custom animations through CSS or JS. + + + + ), dropzone: , } as Record; diff --git a/apps/www/content/docs/components/accordion.mdx b/apps/www/content/docs/components/accordion.mdx new file mode 100644 index 0000000..2609af8 --- /dev/null +++ b/apps/www/content/docs/components/accordion.mdx @@ -0,0 +1,330 @@ +--- +title: Accordion +description: The accordion component is a vertically stacked set of interactive headings that each contain a title, content, or both. +preview: accordion +--- + +import { AccordionRoot, AccordionItem, AccordionTrigger, AccordionContent, Accordions, Accordion } from "ruru-ui/components/accordion"; +import { Tab, Tabs } from "fumadocs-ui/components/tabs"; +import { Tabs as Rutabs, Tab as Rutab } from "ruru-ui/components/tabs"; +import AccordionVariants from "@/components/preview/accordion/accordionVariants"; +import { Link } from 'lucide-react'; + +## Installation + + + + + + ```bash + npx ruru-ui-cli@latest add accordion + ``` + + + ```bash + pnpm dlx ruru-ui-cli@latest add accordion + ``` + + + ```bash + npx ruru-ui-cli@latest add accordion + ``` + + + ```bash + bunx --bun ruru-ui-cli@latest add accordion + ``` + + + + + + ```package-install + npm install ruru-ui@latest + ``` + + + +## Usage + +The `Accordion` component is a vertically stacked set of interactive headings that each contain a title, content, or both. + + + +
+ + Yes. It adheres to the WAI-ARIA design pattern. + Yes. It is responsive and mobile-friendly. + Yes. It is customizable and supports theming. + +
+
+ +```tsx +import { Accordions, Accordion } from "ruru-ui/components/accordion"; // [!code highlight] + +export function Demo() { + return ( + // [!code word:Accordions] + // [!code word:trigger] + // [!code word:id] + // [!code word:type] + // [!code word:single] + // [!code word:collapsible] + + + Yes. It adheres to the WAI-ARIA design pattern. + + + Yes. It is responsive and mobile-friendly. + + + Yes. It is customizable and supports theming. + + + ) +} +``` + +
+ +### Core Components + +You can use the core components to build your own custom accordion. + + + +
+ + + Is it accessible? + Yes. It adheres to the WAI-ARIA design pattern. + + + Is it responsive? + Yes. It is responsive and mobile-friendly. + + + Is it customizable? + Yes. It is customizable and supports theming. + + +
+
+ +```tsx +import { + AccordionRoot, // [!code highlight] + AccordionItem, // [!code highlight] + AccordionTrigger, // [!code highlight] + AccordionContent, // [!code highlight] +} from "ruru-ui/components/accordion"; + +// [!code word:AccordionRoot] +// [!code word:type] +// [!code word:single] +// [!code word:collapsible] +export function Demo() { + return ( + + + Is it accessible? // [!code highlight] + // [!code highlight] + Yes. It adheres to the WAI-ARIA design pattern. // [!code highlight] + // [!code highlight] + + + Is it responsive? // [!code highlight] + // [!code highlight] + Yes. It is responsive and mobile-friendly. // [!code highlight] + // [!code highlight] + + + Is it customizable? // [!code highlight] + // [!code highlight] + Yes. It is customizable and supports theming. // [!code highlight] + // [!code highlight] + + + ) +} +``` + + +
+ +## Multiple Variants + +We provide miltiple variants and themes to choose from. You can customize the accordion to fit your design needs. +If you need more customization, you can use the core components to build your own custom accordion. + + + + + + +```tsx +import { + Accordions, + Accordion +} from "ruru-ui/components/accordion"; + +export function Demo() { + return ( + // [!code word:Accordions] + + {Array.from(new Array(4)).map((_, index) => ( + // [!code highlight] + Accordion Item {index + 1} // [!code highlight] + // [!code highlight] + } + > + This is the content of the accordion item {index + 1} + + ))} + + ) +} +``` + + + +## Customizability + +Easily customize the accordion by using the `AccordionRoot`, `AccordionItem`, `AccordionTrigger`, and `AccordionContent` components. +Add custom functionalities and styles with ease. + +> Remember to use `e.stopPropagation()` to prevent the event from bubbling up the DOM tree. + + + +
+ + {Array.from(new Array(4)).map((_, index) => ( + + + + Is this accessible? + + + Yes. It adheres to the WAI-ARIA design pattern. + + + ))} + +
+
+ +```tsx +import { + AccordionRoot, // [!code highlight] + AccordionItem, // [!code highlight] + AccordionTrigger, // [!code highlight] + AccordionContent, // [!code highlight] +} from "ruru-ui/components/accordion"; +import { Link } from 'lucide-react'; // [!code highlight] + +export function Demo() { + return ( + // [!code word:AccordionRoot] + // [!code word:type] + // [!code word:single] + // [!code word:collapsible] + + {Array.from(new Array(4)).map((_, index) => ( + + + // [!code highlight] + Is this accessible? // [!code highlight] + + + Yes. It adheres to the WAI-ARIA design pattern. // [!code highlight] + + + ))} + + ) +} +``` + +
+ +## Imports + +We are exporting all the core components of accordion along with the `Accordions` and `Accordion` components. + +```tsx +import { + AccordionRoot, + AccordionItem, + AccordionTrigger, + AccordionContent, + Accordions, + Accordion, +} from "ruru-ui/components/accordion"; +``` + +## Props + +### Accordions + +| Prop | Description | Type | Default | +| ---- | ----------- | ---- | ------- | +| `type` | The type of accordion. | `"single" \| "multiple"` | `"single"` | +| `collapsible` | Whether the accordion is collapsible. | `boolean` | `false` | +| `variant` | The variant of the accordion. | `"default" \| "primary" \| "none"` | `"default"` | +| `theme` | The theme of the accordion. | `"default" \| "primary" \| "secondary" \| "tertiary"` | `"default"` | +| `showCopyButton` | Weather to show copy button. | `boolean` | `false` | +| `props` | Other props | typeof `AccordionPrimitive.Root` with `ref` | `{}` | + +| Data Attribute | Description | Values | +| -------------- | ----------- | ------ | +| `[data-orientation]` | The orientation of the accordion. | `"vertical" \| "horizontal"` | +| `[data-variant]` | The variant of the accordion. | `"default" \| "primary" \| "none"` | +| `[data-theme]` | The theme of the accordion. | `"default" \| "primary" \| "secondary" \| "tertiary"` | + +### Accordion + +| Prop | Description | Type | Default | +| ---- | ----------- | ---- | ------- | +| `id` | The id of the accordion. | `string` | `undefined` | +| `trigger` | The trigger of the accordion. | `ReactNode` | `undefined` | +| `TClassName` | The class name of the trigger. | `string` | `undefined` | +| `CClassName` | The class name of the content. | `string` | `undefined` | +| `copyText` | Costom text to copy. | `string` | `undefined` | +| `props` | Other props | typeof `AccordionPrimitive.Item` with `ref` | `{}` | + +| Data Attribute | Description | Values | +| -------------- | ----------- | ------ | +| `[data-orientation]` | The orientation of the accordion. | `"vertical" \| "horizontal"` | +| `[data-variant]` | The variant of the accordion. | `"default" \| "primary" \| "none"` | +| `[data-theme]` | The theme of the accordion. | `"default" \| "primary" \| "secondary" \| "tertiary"` | +| `[data-state]` | The state of the accordion. | `"open" \| "closed"` | +| `[data-disabled]` | Whether the accordion is disabled. | `true` \| `false` | diff --git a/apps/www/public/registry/components/accordion.json b/apps/www/public/registry/components/accordion.json new file mode 100644 index 0000000..fb92e8c --- /dev/null +++ b/apps/www/public/registry/components/accordion.json @@ -0,0 +1,12 @@ +{ + "name": "accordion", + "dependencies": ["@radix-ui/react-accordion"], + "files": [ + { + "name": "accordion.tsx", + "content": "\"use client\";\n\nimport * as React from \"react\";\nimport * as AccordionPrimitive from \"@radix-ui/react-accordion\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { CheckIcon, ChevronDownIcon, Link2Icon } from \"@radix-ui/react-icons\";\nimport { cn } from \"@/utils/cn\";\nimport { Button } from \"./button\";\n\nconst AccordionRoot = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef\n>(({ className, orientation, ...props }, ref) => (\n \n));\n\nconst AccordionItem = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef\n>(({ className, ...props }, ref) => (\n \n));\nAccordionItem.displayName = \"AccordionItem\";\n\nconst AccordionTrigger = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef & {\n hideChevron?: boolean;\n chevronPosition?: \"left\" | \"right\";\n chevronRotation?: \"full\" | \"half\";\n before?: React.ReactNode;\n after?: React.ReactNode;\n }\n>(\n (\n {\n className,\n children,\n hideChevron = false,\n chevronPosition,\n chevronRotation = \"full\",\n before = null,\n after = null,\n ...props\n },\n ref,\n ) => (\n \n {before}\n \n {children}\n {!hideChevron && (\n \n )}\n \n {after}\n \n ),\n);\nAccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;\n\nconst AccordionContent = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef\n>(({ className, children, ...props }, ref) => (\n \n
{children}
\n \n));\nAccordionContent.displayName = AccordionPrimitive.Content.displayName;\n\nexport const AccordionsVariants = cva(cn(\"w-full rounded-lg\"), {\n variants: {\n variant: {\n default: cn(),\n primary: cn(),\n none: cn(\"border-none rounded-none\"),\n },\n theme: {\n default: cn(\"border\"),\n primary: cn(\"text-primary-foreground\"),\n secondary: cn(\"border text-foreground\"),\n tertiary: cn(\"border-none text-primary-foreground\"),\n },\n },\n defaultVariants: {\n variant: \"default\",\n theme: \"default\",\n },\n});\n\nexport const AccordionVariants = cva(\n cn(\n \"w-full border-b last-of-type:border-none first-of-type:rounded-t-lg last-of-type:rounded-b-lg transition-colors\",\n ),\n {\n variants: {\n variant: {\n default: cn(),\n primary: cn(),\n none: cn(\n \"border-none rounded-none first-of-type:rounded-none last-of-type:rounded-none\",\n ),\n },\n theme: {\n default: cn(\"bg-none text-foreground\"),\n primary: cn(\"bg-primary hover:bg-primary/85 text-primary-foreground\"),\n secondary: cn(\n \"bg-secondary/55 hover:bg-secondary text-secondary-foreground\",\n ),\n tertiary: cn(\"bg-none hover:bg-accent/75 border-none text-foreground\"),\n },\n },\n defaultVariants: {\n variant: \"default\",\n theme: \"default\",\n },\n },\n);\n\n\nconst Accordions = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef & {\n \n variant?: VariantProps[\"variant\"];\n \n theme?: VariantProps[\"theme\"];\n \n showCopyButton?: boolean;\n }\n>(\n (\n {\n className,\n type = \"single\",\n variant = \"default\",\n theme = \"default\",\n showCopyButton = false,\n children,\n ...props\n },\n ref,\n ) => {\n const newClildren = React.Children.map(children, (child) => {\n if (React.isValidElement(child)) {\n \n return React.cloneElement(child, { variant, theme, showCopyButton });\n }\n return child;\n });\n\n return (\n \n \n {newClildren}\n \n );\n },\n);\nAccordions.displayName = \"Accordions\";\n\n\nconst Accordion = React.forwardRef<\n React.ElementRef,\n Omit<\n React.ComponentPropsWithoutRef,\n \"value\"\n > & {\n \n variant?: VariantProps[\"variant\"];\n theme?: VariantProps[\"theme\"];\n showCopyButton?: boolean;\n \n copyText?: string;\n \n TClassName?: string;\n \n CClassName?: string;\n \n trigger: React.ReactNode;\n \n id: string;\n }\n>(\n (\n {\n className,\n variant = \"default\",\n theme = \"default\",\n showCopyButton = false,\n copyText,\n TClassName,\n CClassName,\n trigger,\n id,\n children,\n ...props\n },\n ref,\n ) => {\n return (\n \n }\n >\n {trigger}\n \n \n \n );\n },\n);\nAccordion.displayName = \"Accordion\";\n\nfunction CopyButton({ id, copyText }: { id: string; copyText?: string }) {\n const [checked, setChecked] = React.useState(false);\n const timeoutRef = React.useRef(null);\n\n const onCopy: React.MouseEventHandler = React.useCallback(\n (e) => {\n e.stopPropagation();\n\n const url = new URL(window.location.href);\n url.hash = id;\n\n navigator.clipboard.writeText(copyText || url.toString());\n },\n [],\n );\n\n const onClick: React.MouseEventHandler = React.useCallback(\n (e) => {\n if (timeoutRef.current) window.clearTimeout(timeoutRef.current);\n timeoutRef.current = window.setTimeout(() => {\n setChecked(false);\n }, 1500);\n onCopy(e);\n setChecked(true);\n },\n [onCopy],\n );\n\n \n React.useEffect(() => {\n return () => {\n if (timeoutRef.current) window.clearTimeout(timeoutRef.current);\n };\n }, []);\n\n return (\n \n {checked ? (\n \n ) : (\n \n )}\n \n );\n}\n\nexport {\n AccordionRoot,\n AccordionItem,\n AccordionTrigger,\n AccordionContent,\n Accordions,\n Accordion,\n};\n" + } + ], + "type": "components:ui", + "subcategory": ["button"] +} diff --git a/apps/www/public/registry/index.json b/apps/www/public/registry/index.json index e38e069..37e425b 100644 --- a/apps/www/public/registry/index.json +++ b/apps/www/public/registry/index.json @@ -84,5 +84,12 @@ "dependencies": ["react-dropzone"], "files": ["dropzone.tsx"], "type": "components:ui" + }, + { + "name": "accordion", + "dependencies": ["@radix-ui/react-accordion"], + "files": ["accordion.tsx"], + "type": "components:ui", + "subcategory": ["button"] } ] diff --git a/apps/www/public/sitemap-0.xml b/apps/www/public/sitemap-0.xml index 6ccf3fb..4979ebc 100644 --- a/apps/www/public/sitemap-0.xml +++ b/apps/www/public/sitemap-0.xml @@ -1,12 +1,15 @@ +https://ruru-ui.vercel.app/playgrounddaily0.7 +https://ruru-ui.vercel.appdaily0.7 https://ruru-ui.vercel.app/theme__daily0.7 https://ruru-ui.vercel.app/colordaily0.7 https://ruru-ui.vercel.app/blocks/login-1daily0.7 https://ruru-ui.vercel.app/blocks/register-1daily0.7 https://ruru-ui.vercel.app/blocks/forgot-1daily0.7 -https://ruru-ui.vercel.appdaily0.7 -https://ruru-ui.vercel.app/playgrounddaily0.7 +https://ruru-ui.vercel.app/sponsorsdaily0.7 +https://ruru-ui.vercel.app/themedaily0.7 +https://ruru-ui.vercel.app/blocksdaily0.7 https://ruru-ui.vercel.app/docs/animationdaily0.7 https://ruru-ui.vercel.app/docs/blockdaily0.7 https://ruru-ui.vercel.app/docs/clidaily0.7 @@ -16,6 +19,7 @@ https://ruru-ui.vercel.app/docs/installationdaily0.7 https://ruru-ui.vercel.app/docs/providerdaily0.7 https://ruru-ui.vercel.app/docs/ruru-jsondaily0.7 +https://ruru-ui.vercel.app/docs/components/accordiondaily0.7 https://ruru-ui.vercel.app/docs/components/avatardaily0.7 https://ruru-ui.vercel.app/docs/components/badgedaily0.7 https://ruru-ui.vercel.app/docs/components/buttondaily0.7 @@ -31,7 +35,4 @@ https://ruru-ui.vercel.app/docs/components/tabsdaily0.7 https://ruru-ui.vercel.app/docs/components/textareadaily0.7 https://ruru-ui.vercel.app/docs/components/tooltipdaily0.7 -https://ruru-ui.vercel.app/sponsorsdaily0.7 -https://ruru-ui.vercel.app/themedaily0.7 -https://ruru-ui.vercel.app/blocksdaily0.7 \ No newline at end of file diff --git a/apps/www/registry/ui.ts b/apps/www/registry/ui.ts index a8ad817..c8bd739 100644 --- a/apps/www/registry/ui.ts +++ b/apps/www/registry/ui.ts @@ -87,4 +87,11 @@ export const ui: Registry = [ files: ["dropzone.tsx"], dependencies: ["react-dropzone"], }, + { + name: "accordion", + type: "components:ui", + dependencies: ["@radix-ui/react-accordion"], + files: ["accordion.tsx"], + subcategory: ["button"], + }, ]; diff --git a/packages/ui/package.json b/packages/ui/package.json index 5c13b0c..23045f8 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "ruru-ui", - "version": "2.2.1", + "version": "2.2.2", "description": "Ruru UI Components", "publishConfig": { "access": "public" @@ -89,6 +89,7 @@ "typescript": "^5.3.3" }, "dependencies": { + "@radix-ui/react-accordion": "^1.2.0", "@radix-ui/react-checkbox": "^1.1.1", "@radix-ui/react-direction": "^1.1.0", "@radix-ui/react-icons": "^1.3.0", diff --git a/packages/ui/src/components/accordion.tsx b/packages/ui/src/components/accordion.tsx new file mode 100644 index 0000000..9f15039 --- /dev/null +++ b/packages/ui/src/components/accordion.tsx @@ -0,0 +1,455 @@ +"use client"; + +import * as React from "react"; +import * as AccordionPrimitive from "@radix-ui/react-accordion"; +import { cva, type VariantProps } from "class-variance-authority"; +import { CheckIcon, ChevronDownIcon, Link2Icon } from "@radix-ui/react-icons"; +import { cn } from "@/utils/cn"; +import { Button } from "./button"; + +const AccordionRoot = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, orientation, ...props }, ref) => ( + +)); + +const AccordionItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +AccordionItem.displayName = "AccordionItem"; + +const AccordionTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + hideChevron?: boolean; + chevronPosition?: "left" | "right"; + chevronRotation?: "full" | "half"; + before?: React.ReactNode; + after?: React.ReactNode; + } +>( + ( + { + className, + children, + hideChevron = false, + chevronPosition, + chevronRotation = "full", + before = null, + after = null, + ...props + }, + ref, + ) => ( + + {before} + + {children} + {!hideChevron && ( + + )} + + {after} + + ), +); +AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName; + +const AccordionContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + +
{children}
+
+)); +AccordionContent.displayName = AccordionPrimitive.Content.displayName; + +export const AccordionsVariants = cva(cn("w-full rounded-lg"), { + variants: { + variant: { + default: cn(), + primary: cn(), + none: cn("border-none rounded-none"), + }, + theme: { + default: cn("border"), + primary: cn("text-primary-foreground"), + secondary: cn("border text-foreground"), + tertiary: cn("border-none text-primary-foreground"), + }, + }, + defaultVariants: { + variant: "default", + theme: "default", + }, +}); + +export const AccordionVariants = cva( + cn( + "w-full border-b last-of-type:border-none first-of-type:rounded-t-lg last-of-type:rounded-b-lg transition-colors", + ), + { + variants: { + variant: { + default: cn(), + primary: cn(), + none: cn( + "border-none rounded-none first-of-type:rounded-none last-of-type:rounded-none", + ), + }, + theme: { + default: cn("bg-none text-foreground"), + primary: cn("bg-primary hover:bg-primary/85 text-primary-foreground"), + secondary: cn( + "bg-secondary/55 hover:bg-secondary text-secondary-foreground", + ), + tertiary: cn("bg-none hover:bg-accent/75 border-none text-foreground"), + }, + }, + defaultVariants: { + variant: "default", + theme: "default", + }, + }, +); + +/** + * Accordions are a collection of vertically stacked sections that expand and collapse on click. + * + * @param {React.ComponentPropsWithoutRef} props + * + * @prop {"default", "primary", "none"} variant - The variant of the accordion. + * @prop {"default", "primary", "secondary", "tertiary"} theme - The theme of the accordion. + * @prop {"single" | "multiple"} type - The type of the accordion. + * @prop {React.ReactNode} children - The content of the accordion. + * @prop {string} className - The class name of the accordion. + * @param {React.Ref} ref - Forwarded ref. + * + * @example + * ```tsx + * + * + * Content 1 + * + * + */ +const Accordions = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + /** + * The variant of the accordion. + * @default "default" + * @type {VariantProps["variant"]} + * + * @example + * ```tsx + * + * + * Content 1 + * + * + * ``` + */ + variant?: VariantProps["variant"]; + /** + * The theme of the accordion. + * @default "default" + * @type {VariantProps["theme"]} + * + * @example + * ```tsx + * + * + * Content 1 + * + * + */ + theme?: VariantProps["theme"]; + /** + * To declare weather to show the copy button or not. + * @default false + * @type {boolean} + * + * @example + * ```tsx + * + * + * Content 1 + * + * + */ + showCopyButton?: boolean; + } +>( + ( + { + className, + type = "single", + variant = "default", + theme = "default", + showCopyButton = false, + children, + ...props + }, + ref, + ) => { + const newClildren = React.Children.map(children, (child) => { + if (React.isValidElement(child)) { + // @ts-expect-error -- Unknown type + return React.cloneElement(child, { variant, theme, showCopyButton }); + } + return child; + }); + + return ( + // @ts-expect-error -- Multiple types + + {newClildren} + + ); + }, +); +Accordions.displayName = "Accordions"; + +/** + * An accordion is a vertically stacked section that expands and collapses on click. + * + * @param {React.ComponentPropsWithoutRef} props + * + * @prop {"default", "primary", "none"} variant - The variant of the accordion. + * @prop {"default", "primary", "secondary", "tertiary"} theme - The theme of the accordion. + * @prop {string} TClassName - The class name of the trigger. + * @prop {string} CClassName - The class name of the content. + * @prop {React.ReactNode} trigger - The trigger of the accordion. + * @prop {string} id - The id of the accordion. + * @prop {React.ReactNode} children - The content of the accordion. + * @prop {string} className - The class name of the accordion. + * @param {React.Ref} ref - Forwarded ref. + * + * @example + * ```tsx + * + * Content 1 + * + */ +const Accordion = React.forwardRef< + React.ElementRef, + Omit< + React.ComponentPropsWithoutRef, + "value" + > & { + // These are the props coming from the parent component + variant?: VariantProps["variant"]; + theme?: VariantProps["theme"]; + showCopyButton?: boolean; + /** + * The text to be copied. `showCopyButton` need to be set true in `Accordions`. + * @type {string} + * + * @example + * ```tsx + * + * Content 1 + * + */ + copyText?: string; + /** + * The class name of the trigger. + * @type {string} + */ + TClassName?: string; + /** + * The class name of the content. + * @type {string} + */ + CClassName?: string; + /** + * The trigger of the accordion. + * @type {React.ReactNode} + * + * @example + * ```tsx + * + * Content 1 + * + */ + trigger: React.ReactNode; + /** + * The id of the accordion. + * @type {string} + * + * @example + * ```tsx + * + * Content 1 + * + */ + id: string; + } +>( + ( + { + className, + variant = "default", + theme = "default", + showCopyButton = false, + copyText, + TClassName, + CClassName, + trigger, + id, + children, + ...props + }, + ref, + ) => { + return ( + + } + > + {trigger} + + + + ); + }, +); +Accordion.displayName = "Accordion"; + +function CopyButton({ id, copyText }: { id: string; copyText?: string }) { + const [checked, setChecked] = React.useState(false); + const timeoutRef = React.useRef(null); + + const onCopy: React.MouseEventHandler = React.useCallback( + (e) => { + e.stopPropagation(); + + const url = new URL(window.location.href); + url.hash = id; + + navigator.clipboard.writeText(copyText || url.toString()); + }, + [], + ); + + const onClick: React.MouseEventHandler = React.useCallback( + (e) => { + if (timeoutRef.current) window.clearTimeout(timeoutRef.current); + timeoutRef.current = window.setTimeout(() => { + setChecked(false); + }, 1500); + onCopy(e); + setChecked(true); + }, + [onCopy], + ); + + // Avoid updates after being unmounted + React.useEffect(() => { + return () => { + if (timeoutRef.current) window.clearTimeout(timeoutRef.current); + }; + }, []); + + return ( + + ); +} + +export { + AccordionRoot, + AccordionItem, + AccordionTrigger, + AccordionContent, + Accordions, + Accordion, +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2542513..91a37e6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -302,6 +302,9 @@ importers: packages/ui: dependencies: + '@radix-ui/react-accordion': + specifier: ^1.2.0 + version: 1.2.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1) '@radix-ui/react-checkbox': specifier: ^1.1.1 version: 1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)