Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
231 changes: 231 additions & 0 deletions app/(site)/docs/components/accordion/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
import { ComponentPreview } from "@/components/docs/component-preview";

export default function AccordionPage() {
return (
<ComponentPreview
name="Accordion"
description="An accordion component for React Native applications."
examples={[
{
"title": "Default",
"value": "default",
"content": "import { Accordion } from \"@nativeui/ui\";\n\nexport default function AccordionDemo() {\n return (\n <Accordion>\n Click me\n </Accordion>\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<AccordionContextValue | null>(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<string[]>(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 (
<AccordionContext.Provider value={{ value: accordionValue, onValueChange: handleValueChange, type }}>
<View className={cn("w-full", className)}>
{children}
</View>
</AccordionContext.Provider>
);
};

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 (
<View className={cn("border-b border-border", className)}>
{React.Children.map(children, (child) => {
if (React.isValidElement(child)) {
return React.cloneElement(child as React.ReactElement<any>, {
value,
isExpanded,
});
}
return child;
})}
</View>
);
};

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 (
<Pressable
onPress={handlePress}
className={cn(
"flex-row items-center justify-between py-4",
className
)}
accessibilityRole="button"
accessibilityState={{ expanded: isExpanded }}
accessibilityHint="Toggle accordion section"
>
<View className="flex-1">
{typeof children === 'string' ? (
<Text className="text-base font-medium text-foreground">{children}</Text>
) : (
children
)}
</View>
<View style={{ transform: [{ rotate: \`""deg\` }] }}>
<Feather name="chevron-down" size={20} color="#888" />
</View>
</Pressable>
);
};

interface AccordionContentProps {
className?: string;
children: React.ReactNode;
value?: string;
isExpanded?: boolean;
}

const AccordionContent = ({
className,
children,
value,
isExpanded,
}: AccordionContentProps) => {
if (!isExpanded) {
return null;
}

return (
<View
className={cn("pb-4 pt-0", className)}
>
{typeof children === 'string' ? (
<Text className="text-base text-muted-foreground">{children}</Text>
) : (
children
)}
</View>
);
};

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 (
<div className="flex flex-col gap-4">
<Accordion>Default Accordion</Accordion>
<Accordion variant="destructive">Delete</Accordion>
<Accordion variant="outline">Outline</Accordion>
<Accordion variant="secondary">Secondary</Accordion>
<Accordion variant="ghost">Ghost</Accordion>
<Accordion variant="link">Link</Accordion>
</div>
);
}`}
registryName="accordion"
packageName="@nativeui/ui"
/>
);
}
4 changes: 2 additions & 2 deletions app/(site)/docs/components/button/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ export default function ButtonPage() {
return (
<ComponentPreview
name="Button"
description="A button component for React Native applications."
description="A button component with multiple variants for React Native applications."
examples={[
{
"title": "Default",
Expand All @@ -15,7 +15,7 @@ export default function ButtonPage() {
{
"title": "Variants",
"value": "variants",
"content": "import { Button } from \"@nativeui/ui\";\n\nexport default function ButtonVariants() {\n return (\n <div className=\"flex flex-col gap-4\">\n <Button variant=\"default\">Default</Button>\n <Button variant=\"dark\">Dark</Button>\n <Button variant=\"destructive\">Destructive</Button>\n <Button variant=\"outline\">Outline</Button>\n <Button variant=\"secondary\">Secondary</Button>\n <Button variant=\"ghost\">Ghost</Button>\n <Button variant=\"link\">Link</Button>\n </div>\n );\n}",
"content": "import { Button } from \"@nativeui/ui\";\n\nexport default function ButtonVariants() {\n return (\n <div className=\"flex flex-col gap-4\">\n <Button variant=\"default\">Default</Button>\n <Button variant=\"destructive\">Destructive</Button>\n <Button variant=\"outline\">Outline</Button>\n <Button variant=\"secondary\">Secondary</Button>\n <Button variant=\"ghost\">Ghost</Button>\n <Button variant=\"link\">Link</Button>\n </div>\n );\n}",
"language": "tsx"
},
{
Expand Down
23 changes: 5 additions & 18 deletions app/(site)/docs/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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: [
Expand All @@ -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}`,
})),
},
],
};
Expand Down
19 changes: 19 additions & 0 deletions public/r/accordion.json
Original file line number Diff line number Diff line change
@@ -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<AccordionContextValue | null>(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<string[]>(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 <AccordionContext.Provider value={{ value: accordionValue, onValueChange: handleValueChange, type }}>\n <View className={cn(\"w-full\", className)}>\n {children}\n </View>\n </AccordionContext.Provider>\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 <View className={cn(\"border-b border-border\", className)}>\n {React.Children.map(children, (child) => {\n if (React.isValidElement(child)) {\n return React.cloneElement(child as React.ReactElement<any>, {\n value,\n isExpanded,\n });\n }\n return child;\n })}\n </View>\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 <Pressable\n onPress={handlePress}\n className={cn(\n \"flex-row items-center justify-between py-4\",\n className\n )}\n accessibilityRole=\"button\"\n accessibilityState={{ expanded: isExpanded }}\n accessibilityHint=\"Toggle accordion section\"\n >\n <View className=\"flex-1\">\n {typeof children === 'string' ? (\n <Text className=\"text-base font-medium text-foreground\">{children}</Text>\n ) : (\n children\n )}\n </View>\n <View style={{ transform: [{ rotate: `${iconRotation}deg` }] }}>\n <Feather name=\"chevron-down\" size={20} color=\"#888\" />\n </View>\n </Pressable>\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 <View\n className={cn(\"pb-4 pt-0\", className)}\n >\n {typeof children === 'string' ? (\n <Text className=\"text-base text-muted-foreground\">{children}</Text>\n ) : (\n children\n )}\n </View>\n );\n};\n\nAccordion.displayName = 'Accordion';\nAccordionItem.displayName = 'AccordionItem';\nAccordionTrigger.displayName = 'AccordionTrigger';\nAccordionContent.displayName = 'AccordionContent';\n\nexport { Accordion, AccordionItem, AccordionTrigger, AccordionContent }; ",
"type": "registry:component"
}
]
}
3 changes: 2 additions & 1 deletion public/r/button.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [],
Expand Down
16 changes: 15 additions & 1 deletion registry.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": []
}
]
Expand Down
Loading