From 392b8cd423d065c13ddd74a9c08dfe2e2287c430 Mon Sep 17 00:00:00 2001 From: Theo2903 Date: Mon, 28 Apr 2025 22:57:53 +0200 Subject: [PATCH 1/2] Add Accordion component to registry and documentation - Introduced a new Accordion component for React Native applications, including its implementation in `registry/accordion/accordion.tsx`. - Updated `registry.json` to include the new Accordion component and its dependencies. - Modified layout documentation to dynamically list components from the registry. - Created a dedicated page for the Accordion component in the documentation with usage examples and code snippets. - Added a new JSON file for the Accordion component in the public registry. --- app/(site)/docs/components/accordion/page.tsx | 231 ++++++++++++++++++ app/(site)/docs/layout.tsx | 23 +- public/r/accordion.json | 19 ++ registry.json | 16 +- registry/accordion/accordion.tsx | 197 +++++++++++++++ 5 files changed, 467 insertions(+), 19 deletions(-) create mode 100644 app/(site)/docs/components/accordion/page.tsx create mode 100644 public/r/accordion.json create mode 100644 registry/accordion/accordion.tsx diff --git a/app/(site)/docs/components/accordion/page.tsx b/app/(site)/docs/components/accordion/page.tsx new file mode 100644 index 0000000..b95ec57 --- /dev/null +++ b/app/(site)/docs/components/accordion/page.tsx @@ -0,0 +1,231 @@ +import { ComponentPreview } from "@/components/docs/component-preview"; + +export default function AccordionPage() { + return ( + \n Click me\n \n );\n}", + "language": "tsx" + } +]} + componentCode={`import * as React from 'react'; +import { Pressable, View, Text, LayoutAnimation, Platform, UIManager } from 'react-native'; +import { Feather } from '@expo/vector-icons'; +import { cn } from '@/lib/utils'; + +// Enable layout animation for Android +if (Platform.OS === 'android') { + if (UIManager.setLayoutAnimationEnabledExperimental) { + UIManager.setLayoutAnimationEnabledExperimental(true); + } +} + +interface AccordionContextValue { + value: string[]; + onValueChange: (itemValue: string) => void; + type: 'single' | 'multiple'; +} + +const AccordionContext = React.createContext(null); + +export interface AccordionProps { + type?: 'single' | 'multiple'; + collapsible?: boolean; + value?: string[]; + onValueChange?: (value: string[]) => void; + defaultValue?: string[]; + className?: string; + children: React.ReactNode; +} + +const Accordion = ({ + type = 'single', + collapsible = false, + value, + onValueChange, + defaultValue, + className, + children, +}: AccordionProps) => { + const [state, setState] = React.useState(value || defaultValue || []); + + const isControlled = value !== undefined; + const accordionValue = isControlled ? value : state; + + const handleValueChange = React.useCallback((itemValue: string) => { + const isSelected = accordionValue.includes(itemValue); + + let newValue: string[] = []; + + if (type === 'single') { + if (isSelected) { + newValue = collapsible ? [] : [itemValue]; + } else { + newValue = [itemValue]; + } + } else { + if (isSelected) { + newValue = accordionValue.filter((v) => v !== itemValue); + } else { + newValue = [...accordionValue, itemValue]; + } + } + + if (!isControlled) { + setState(newValue); + } + + onValueChange?.(newValue); + }, [accordionValue, collapsible, isControlled, onValueChange, type]); + + return ( + + + {children} + + + ); +}; + +interface AccordionItemProps { + value: string; + className?: string; + children: React.ReactNode; +} + +const AccordionItem = ({ value, className, children }: AccordionItemProps) => { + const context = React.useContext(AccordionContext); + + if (!context) { + throw new Error('AccordionItem must be used within an Accordion'); + } + + const isExpanded = context.value.includes(value); + + return ( + + {React.Children.map(children, (child) => { + if (React.isValidElement(child)) { + return React.cloneElement(child as React.ReactElement, { + value, + isExpanded, + }); + } + return child; + })} + + ); +}; + +interface AccordionTriggerProps { + className?: string; + children: React.ReactNode; + value?: string; + isExpanded?: boolean; +} + +const AccordionTrigger = ({ + className, + children, + value, + isExpanded, +}: AccordionTriggerProps) => { + const context = React.useContext(AccordionContext); + + if (!context || value === undefined) { + return null; + } + + // Utiliser LayoutAnimation à la place de Reanimated pour l'icône + const iconRotation = isExpanded ? 180 : 0; + + const handlePress = () => { + LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); + context.onValueChange(value); + }; + + return ( + + + {typeof children === 'string' ? ( + {children} + ) : ( + children + )} + + + + + + ); +}; + +interface AccordionContentProps { + className?: string; + children: React.ReactNode; + value?: string; + isExpanded?: boolean; +} + +const AccordionContent = ({ + className, + children, + value, + isExpanded, +}: AccordionContentProps) => { + if (!isExpanded) { + return null; + } + + return ( + + {typeof children === 'string' ? ( + {children} + ) : ( + children + )} + + ); +}; + +Accordion.displayName = 'Accordion'; +AccordionItem.displayName = 'AccordionItem'; +AccordionTrigger.displayName = 'AccordionTrigger'; +AccordionContent.displayName = 'AccordionContent'; + +export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }; `} + previewCode={`import { Accordion } from "@nativeui/ui"; + +export default function AccordionDemo() { + return ( +
+ Default Accordion + Delete + Outline + Secondary + Ghost + Link +
+ ); +}`} + registryName="accordion" + packageName="@nativeui/ui" + /> + ); +} diff --git a/app/(site)/docs/layout.tsx b/app/(site)/docs/layout.tsx index c72db20..2a59d93 100644 --- a/app/(site)/docs/layout.tsx +++ b/app/(site)/docs/layout.tsx @@ -4,6 +4,7 @@ import React from "react"; import Link from "next/link"; import { usePathname } from "next/navigation"; import { cn } from "@/lib/utils"; +import registry from "@/registry.json"; const docsConfig = { sidebarNav: [ @@ -26,24 +27,10 @@ const docsConfig = { }, { title: "Components", - items: [ - { - title: "Button", - href: "/docs/components/button", - }, - { - title: "Card", - href: "/docs/components/card", - }, - { - title: "Tabs", - href: "/docs/components/tabs", - }, - { - title: "Input", - href: "/docs/components/input", - }, - ], + items: registry.items.map((item) => ({ + title: item.title, + href: `/docs/components/${item.name}`, + })), }, ], }; diff --git a/public/r/accordion.json b/public/r/accordion.json new file mode 100644 index 0000000..bd716d1 --- /dev/null +++ b/public/r/accordion.json @@ -0,0 +1,19 @@ +{ + "$schema": "https://ui.shadcn.com/schema/registry-item.json", + "name": "accordion", + "type": "registry:component", + "title": "Accordion", + "description": "An accordion component for React Native applications.", + "dependencies": [ + "react-native", + "@expo/vector-icons" + ], + "registryDependencies": [], + "files": [ + { + "path": "registry/accordion/accordion.tsx", + "content": "import * as React from 'react';\nimport { Pressable, View, Text, LayoutAnimation, Platform, UIManager } from 'react-native';\nimport { Feather } from '@expo/vector-icons';\nimport { cn } from '@/lib/utils';\n\n// Enable layout animation for Android\nif (Platform.OS === 'android') {\n if (UIManager.setLayoutAnimationEnabledExperimental) {\n UIManager.setLayoutAnimationEnabledExperimental(true);\n }\n}\n\ninterface AccordionContextValue {\n value: string[];\n onValueChange: (itemValue: string) => void;\n type: 'single' | 'multiple';\n}\n\nconst AccordionContext = React.createContext(null);\n\nexport interface AccordionProps {\n type?: 'single' | 'multiple';\n collapsible?: boolean;\n value?: string[];\n onValueChange?: (value: string[]) => void;\n defaultValue?: string[];\n className?: string;\n children: React.ReactNode;\n}\n\nconst Accordion = ({\n type = 'single',\n collapsible = false,\n value,\n onValueChange,\n defaultValue,\n className,\n children,\n}: AccordionProps) => {\n const [state, setState] = React.useState(value || defaultValue || []);\n\n const isControlled = value !== undefined;\n const accordionValue = isControlled ? value : state;\n\n const handleValueChange = React.useCallback((itemValue: string) => {\n const isSelected = accordionValue.includes(itemValue);\n\n let newValue: string[] = [];\n\n if (type === 'single') {\n if (isSelected) {\n newValue = collapsible ? [] : [itemValue];\n } else {\n newValue = [itemValue];\n }\n } else {\n if (isSelected) {\n newValue = accordionValue.filter((v) => v !== itemValue);\n } else {\n newValue = [...accordionValue, itemValue];\n }\n }\n\n if (!isControlled) {\n setState(newValue);\n }\n\n onValueChange?.(newValue);\n }, [accordionValue, collapsible, isControlled, onValueChange, type]);\n\n return (\n \n \n {children}\n \n \n );\n};\n\ninterface AccordionItemProps {\n value: string;\n className?: string;\n children: React.ReactNode;\n}\n\nconst AccordionItem = ({ value, className, children }: AccordionItemProps) => {\n const context = React.useContext(AccordionContext);\n\n if (!context) {\n throw new Error('AccordionItem must be used within an Accordion');\n }\n\n const isExpanded = context.value.includes(value);\n\n return (\n \n {React.Children.map(children, (child) => {\n if (React.isValidElement(child)) {\n return React.cloneElement(child as React.ReactElement, {\n value,\n isExpanded,\n });\n }\n return child;\n })}\n \n );\n};\n\ninterface AccordionTriggerProps {\n className?: string;\n children: React.ReactNode;\n value?: string;\n isExpanded?: boolean;\n}\n\nconst AccordionTrigger = ({\n className,\n children,\n value,\n isExpanded,\n}: AccordionTriggerProps) => {\n const context = React.useContext(AccordionContext);\n\n if (!context || value === undefined) {\n return null;\n }\n\n // Utiliser LayoutAnimation à la place de Reanimated pour l'icône\n const iconRotation = isExpanded ? 180 : 0;\n\n const handlePress = () => {\n LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);\n context.onValueChange(value);\n };\n\n return (\n \n \n {typeof children === 'string' ? (\n {children}\n ) : (\n children\n )}\n \n \n \n \n \n );\n};\n\ninterface AccordionContentProps {\n className?: string;\n children: React.ReactNode;\n value?: string;\n isExpanded?: boolean;\n}\n\nconst AccordionContent = ({\n className,\n children,\n value,\n isExpanded,\n}: AccordionContentProps) => {\n if (!isExpanded) {\n return null;\n }\n\n return (\n \n {typeof children === 'string' ? (\n {children}\n ) : (\n children\n )}\n \n );\n};\n\nAccordion.displayName = 'Accordion';\nAccordionItem.displayName = 'AccordionItem';\nAccordionTrigger.displayName = 'AccordionTrigger';\nAccordionContent.displayName = 'AccordionContent';\n\nexport { Accordion, AccordionItem, AccordionTrigger, AccordionContent }; ", + "type": "registry:component" + } + ] +} \ No newline at end of file diff --git a/registry.json b/registry.json index 32f1685..4dae1f3 100644 --- a/registry.json +++ b/registry.json @@ -14,7 +14,21 @@ "type": "registry:component" } ], - "dependencies": ["@radix-ui/react-slot", "class-variance-authority"], + "dependencies": ["react-native", "class-variance-authority"], + "registryDependencies": [] + }, + { + "name": "accordion", + "type": "registry:component", + "title": "Accordion", + "description": "An accordion component for React Native applications.", + "files": [ + { + "path": "registry/accordion/accordion.tsx", + "type": "registry:component" + } + ], + "dependencies": ["react-native", "@expo/vector-icons"], "registryDependencies": [] } ] diff --git a/registry/accordion/accordion.tsx b/registry/accordion/accordion.tsx new file mode 100644 index 0000000..1564b29 --- /dev/null +++ b/registry/accordion/accordion.tsx @@ -0,0 +1,197 @@ +import * as React from 'react'; +import { Pressable, View, Text, LayoutAnimation, Platform, UIManager } from 'react-native'; +import { Feather } from '@expo/vector-icons'; +import { cn } from '@/lib/utils'; + +// Enable layout animation for Android +if (Platform.OS === 'android') { + if (UIManager.setLayoutAnimationEnabledExperimental) { + UIManager.setLayoutAnimationEnabledExperimental(true); + } +} + +interface AccordionContextValue { + value: string[]; + onValueChange: (itemValue: string) => void; + type: 'single' | 'multiple'; +} + +const AccordionContext = React.createContext(null); + +export interface AccordionProps { + type?: 'single' | 'multiple'; + collapsible?: boolean; + value?: string[]; + onValueChange?: (value: string[]) => void; + defaultValue?: string[]; + className?: string; + children: React.ReactNode; +} + +const Accordion = ({ + type = 'single', + collapsible = false, + value, + onValueChange, + defaultValue, + className, + children, +}: AccordionProps) => { + const [state, setState] = React.useState(value || defaultValue || []); + + const isControlled = value !== undefined; + const accordionValue = isControlled ? value : state; + + const handleValueChange = React.useCallback((itemValue: string) => { + const isSelected = accordionValue.includes(itemValue); + + let newValue: string[] = []; + + if (type === 'single') { + if (isSelected) { + newValue = collapsible ? [] : [itemValue]; + } else { + newValue = [itemValue]; + } + } else { + if (isSelected) { + newValue = accordionValue.filter((v) => v !== itemValue); + } else { + newValue = [...accordionValue, itemValue]; + } + } + + if (!isControlled) { + setState(newValue); + } + + onValueChange?.(newValue); + }, [accordionValue, collapsible, isControlled, onValueChange, type]); + + return ( + + + {children} + + + ); +}; + +interface AccordionItemProps { + value: string; + className?: string; + children: React.ReactNode; +} + +const AccordionItem = ({ value, className, children }: AccordionItemProps) => { + const context = React.useContext(AccordionContext); + + if (!context) { + throw new Error('AccordionItem must be used within an Accordion'); + } + + const isExpanded = context.value.includes(value); + + return ( + + {React.Children.map(children, (child) => { + if (React.isValidElement(child)) { + return React.cloneElement(child as React.ReactElement, { + value, + isExpanded, + }); + } + return child; + })} + + ); +}; + +interface AccordionTriggerProps { + className?: string; + children: React.ReactNode; + value?: string; + isExpanded?: boolean; +} + +const AccordionTrigger = ({ + className, + children, + value, + isExpanded, +}: AccordionTriggerProps) => { + const context = React.useContext(AccordionContext); + + if (!context || value === undefined) { + return null; + } + + // Utiliser LayoutAnimation à la place de Reanimated pour l'icône + const iconRotation = isExpanded ? 180 : 0; + + const handlePress = () => { + LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); + context.onValueChange(value); + }; + + return ( + + + {typeof children === 'string' ? ( + {children} + ) : ( + children + )} + + + + + + ); +}; + +interface AccordionContentProps { + className?: string; + children: React.ReactNode; + value?: string; + isExpanded?: boolean; +} + +const AccordionContent = ({ + className, + children, + value, + isExpanded, +}: AccordionContentProps) => { + if (!isExpanded) { + return null; + } + + return ( + + {typeof children === 'string' ? ( + {children} + ) : ( + children + )} + + ); +}; + +Accordion.displayName = 'Accordion'; +AccordionItem.displayName = 'AccordionItem'; +AccordionTrigger.displayName = 'AccordionTrigger'; +AccordionContent.displayName = 'AccordionContent'; + +export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }; \ No newline at end of file From fee7d69ea2d205cdb5e9a92126d3ee3d4d1f962c Mon Sep 17 00:00:00 2001 From: Theo2903 Date: Mon, 28 Apr 2025 22:58:27 +0200 Subject: [PATCH 2/2] Update button component documentation and JSON metadata - Revised the button component description to highlight multiple variants for React Native applications. - Added "react-native" as a dependency in the button JSON registry file. --- app/(site)/docs/components/button/page.tsx | 4 ++-- public/r/button.json | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/(site)/docs/components/button/page.tsx b/app/(site)/docs/components/button/page.tsx index ae30f27..09a21f4 100644 --- a/app/(site)/docs/components/button/page.tsx +++ b/app/(site)/docs/components/button/page.tsx @@ -4,7 +4,7 @@ export default function ButtonPage() { return ( \n \n \n \n \n \n \n \n \n );\n}", + "content": "import { Button } from \"@nativeui/ui\";\n\nexport default function ButtonVariants() {\n return (\n
\n \n \n \n \n \n \n
\n );\n}", "language": "tsx" }, { diff --git a/public/r/button.json b/public/r/button.json index 475467d..5174f77 100644 --- a/public/r/button.json +++ b/public/r/button.json @@ -3,8 +3,9 @@ "name": "button", "type": "registry:component", "title": "Button", - "description": "A button component for React Native applications.", + "description": "A button component with multiple variants for React Native applications.", "dependencies": [ + "react-native", "class-variance-authority" ], "registryDependencies": [],