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
194 changes: 194 additions & 0 deletions app/(site)/docs/components/avatar/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
import { ComponentPreview } from "@/components/docs/component-preview";

export default function AvatarPage() {
return (
<ComponentPreview
name="Avatar"
description="An avatar component for React Native applications."
examples={[
{
"title": "Default",
"value": "default",
"content": "import { Avatar } from \"@nativeui/ui\";\n\nexport default function AvatarDemo() {\n return (\n <Avatar>\n Click me\n </Avatar>\n );\n}",
"language": "tsx"
}
]}
componentCode={`import * as React from "react";
import {
Image,
View,
Text,
ImageSourcePropType,
ImageStyle,
} from "react-native";
import { cn } from "@/lib/utils";

interface AvatarRootProps {
className?: string;
children: React.ReactNode;
}

const AvatarRoot = React.forwardRef<View, AvatarRootProps>(
({ className, children, ...props }, ref) => {
return (
<View
ref={ref}
className={cn(
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
className
)}
{...props}
>
{children}
</View>
);
}
);

interface AvatarImageProps {
className?: string;
source: ImageSourcePropType;
alt?: string;
style?: ImageStyle;
onLoad?: () => void;
onError?: () => void;
}

const AvatarImage = React.forwardRef<Image, AvatarImageProps>(
({ className, source, alt, ...props }, ref) => {
const imageContext = React.useContext(AvatarContext);

const handleError = () => {
imageContext?.setHasError(true);
props.onError?.();
};

const handleLoad = () => {
imageContext?.setImageLoaded(true);
props.onLoad?.();
};

return (
<Image
ref={ref}
source={source}
accessibilityLabel={alt}
className={cn("h-full w-full object-cover", className)}
onError={handleError}
onLoad={handleLoad}
{...props}
/>
);
}
);

interface AvatarFallbackProps {
className?: string;
children: React.ReactNode;
delayMs?: number;
}

interface AvatarContextValue {
hasError: boolean;
setHasError: React.Dispatch<React.SetStateAction<boolean>>;
imageLoaded: boolean;
setImageLoaded: React.Dispatch<React.SetStateAction<boolean>>;
}

const AvatarContext = React.createContext<AvatarContextValue | null>(null);

const AvatarFallback = React.forwardRef<View, AvatarFallbackProps>(
({ className, children, delayMs = 600, ...props }, ref) => {
const [isShowing, setIsShowing] = React.useState(delayMs === 0);
const avatarContext = React.useContext(AvatarContext);

React.useEffect(() => {
if (delayMs === 0) return;

// Only show fallback if image has error or hasn't loaded after delay
const timer = setTimeout(() => {
if (!avatarContext?.imageLoaded) {
setIsShowing(true);
}
}, delayMs);

return () => clearTimeout(timer);
}, [delayMs, avatarContext?.imageLoaded]);

// Hide fallback if image loads successfully
React.useEffect(() => {
if (avatarContext?.imageLoaded) {
setIsShowing(false);
}
}, [avatarContext?.imageLoaded]);

if (!isShowing || avatarContext?.imageLoaded) {
return null;
}

return (
<View
ref={ref}
className={cn(
"absolute inset-0 flex h-full w-full items-center justify-center bg-muted",
className
)}
{...props}
>
{typeof children === "string" ? (
<Text className="text-base font-medium text-muted-foreground">
{children}
</Text>
) : (
children
)}
</View>
);
}
);

// Wrap the original AvatarRoot to provide context
const Avatar = React.forwardRef<View, AvatarRootProps>((props, ref) => {
const [hasError, setHasError] = React.useState(false);
const [imageLoaded, setImageLoaded] = React.useState(false);

return (
<AvatarContext.Provider
value={{
hasError,
setHasError,
imageLoaded,
setImageLoaded,
}}
>
<AvatarRoot ref={ref} {...props} />
</AvatarContext.Provider>
);
});

AvatarRoot.displayName = "AvatarRoot";
Avatar.displayName = "Avatar";
AvatarImage.displayName = "AvatarImage";
AvatarFallback.displayName = "AvatarFallback";

export { Avatar, AvatarImage, AvatarFallback };
`}
previewCode={`import { Avatar } from "@nativeui/ui";

export default function AvatarDemo() {
return (
<div className="flex flex-col gap-4">
<Avatar>Default Avatar</Avatar>
<Avatar variant="destructive">Delete</Avatar>
<Avatar variant="outline">Outline</Avatar>
<Avatar variant="secondary">Secondary</Avatar>
<Avatar variant="ghost">Ghost</Avatar>
<Avatar variant="link">Link</Avatar>
</div>
);
}`}
registryName="avatar"
packageName="@nativeui/ui"
/>
);
}
18 changes: 18 additions & 0 deletions public/r/avatar.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "avatar",
"type": "registry:component",
"title": "Avatar",
"description": "An avatar component for React Native applications.",
"dependencies": [
"react-native"
],
"registryDependencies": [],
"files": [
{
"path": "registry/avatar/avatar.tsx",
"content": "import * as React from \"react\";\nimport {\n Image,\n View,\n Text,\n ImageSourcePropType,\n ImageStyle,\n} from \"react-native\";\nimport { cn } from \"@/lib/utils\";\n\ninterface AvatarRootProps {\n className?: string;\n children: React.ReactNode;\n}\n\nconst AvatarRoot = React.forwardRef<View, AvatarRootProps>(\n ({ className, children, ...props }, ref) => {\n return (\n <View\n ref={ref}\n className={cn(\n \"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full\",\n className\n )}\n {...props}\n >\n {children}\n </View>\n );\n }\n);\n\ninterface AvatarImageProps {\n className?: string;\n source: ImageSourcePropType;\n alt?: string;\n style?: ImageStyle;\n onLoad?: () => void;\n onError?: () => void;\n}\n\nconst AvatarImage = React.forwardRef<Image, AvatarImageProps>(\n ({ className, source, alt, ...props }, ref) => {\n const imageContext = React.useContext(AvatarContext);\n\n const handleError = () => {\n imageContext?.setHasError(true);\n props.onError?.();\n };\n\n const handleLoad = () => {\n imageContext?.setImageLoaded(true);\n props.onLoad?.();\n };\n\n return (\n <Image\n ref={ref}\n source={source}\n accessibilityLabel={alt}\n className={cn(\"h-full w-full object-cover\", className)}\n onError={handleError}\n onLoad={handleLoad}\n {...props}\n />\n );\n }\n);\n\ninterface AvatarFallbackProps {\n className?: string;\n children: React.ReactNode;\n delayMs?: number;\n}\n\ninterface AvatarContextValue {\n hasError: boolean;\n setHasError: React.Dispatch<React.SetStateAction<boolean>>;\n imageLoaded: boolean;\n setImageLoaded: React.Dispatch<React.SetStateAction<boolean>>;\n}\n\nconst AvatarContext = React.createContext<AvatarContextValue | null>(null);\n\nconst AvatarFallback = React.forwardRef<View, AvatarFallbackProps>(\n ({ className, children, delayMs = 600, ...props }, ref) => {\n const [isShowing, setIsShowing] = React.useState(delayMs === 0);\n const avatarContext = React.useContext(AvatarContext);\n\n React.useEffect(() => {\n if (delayMs === 0) return;\n\n // Only show fallback if image has error or hasn't loaded after delay\n const timer = setTimeout(() => {\n if (!avatarContext?.imageLoaded) {\n setIsShowing(true);\n }\n }, delayMs);\n\n return () => clearTimeout(timer);\n }, [delayMs, avatarContext?.imageLoaded]);\n\n // Hide fallback if image loads successfully\n React.useEffect(() => {\n if (avatarContext?.imageLoaded) {\n setIsShowing(false);\n }\n }, [avatarContext?.imageLoaded]);\n\n if (!isShowing || avatarContext?.imageLoaded) {\n return null;\n }\n\n return (\n <View\n ref={ref}\n className={cn(\n \"absolute inset-0 flex h-full w-full items-center justify-center bg-muted\",\n className\n )}\n {...props}\n >\n {typeof children === \"string\" ? (\n <Text className=\"text-base font-medium text-muted-foreground\">\n {children}\n </Text>\n ) : (\n children\n )}\n </View>\n );\n }\n);\n\n// Wrap the original AvatarRoot to provide context\nconst Avatar = React.forwardRef<View, AvatarRootProps>((props, ref) => {\n const [hasError, setHasError] = React.useState(false);\n const [imageLoaded, setImageLoaded] = React.useState(false);\n\n return (\n <AvatarContext.Provider\n value={{\n hasError,\n setHasError,\n imageLoaded,\n setImageLoaded,\n }}\n >\n <AvatarRoot ref={ref} {...props} />\n </AvatarContext.Provider>\n );\n});\n\nAvatarRoot.displayName = \"AvatarRoot\";\nAvatar.displayName = \"Avatar\";\nAvatarImage.displayName = \"AvatarImage\";\nAvatarFallback.displayName = \"AvatarFallback\";\n\nexport { Avatar, AvatarImage, AvatarFallback };\n",
"type": "registry:component"
}
]
}
14 changes: 14 additions & 0 deletions registry.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,20 @@
],
"dependencies": ["react-native", "class-variance-authority"],
"registryDependencies": []
},
{
"name": "avatar",
"type": "registry:component",
"title": "Avatar",
"description": "An avatar component for React Native applications.",
"files": [
{
"path": "registry/avatar/avatar.tsx",
"type": "registry:component"
}
],
"dependencies": ["react-native"],
"registryDependencies": []
}
]
}
159 changes: 159 additions & 0 deletions registry/avatar/avatar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import * as React from "react";
import {
Image,
View,
Text,
ImageSourcePropType,
ImageStyle,
} from "react-native";
import { cn } from "@/lib/utils";

interface AvatarRootProps {
className?: string;
children: React.ReactNode;
}

const AvatarRoot = React.forwardRef<View, AvatarRootProps>(
({ className, children, ...props }, ref) => {
return (
<View
ref={ref}
className={cn(
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
className
)}
{...props}
>
{children}
</View>
);
}
);

interface AvatarImageProps {
className?: string;
source: ImageSourcePropType;
alt?: string;
style?: ImageStyle;
onLoad?: () => void;
onError?: () => void;
}

const AvatarImage = React.forwardRef<Image, AvatarImageProps>(
({ className, source, alt, ...props }, ref) => {
const imageContext = React.useContext(AvatarContext);

const handleError = () => {
imageContext?.setHasError(true);
props.onError?.();
};

const handleLoad = () => {
imageContext?.setImageLoaded(true);
props.onLoad?.();
};

return (
<Image
ref={ref}
source={source}
accessibilityLabel={alt}
className={cn("h-full w-full object-cover", className)}
onError={handleError}
onLoad={handleLoad}
{...props}
/>
);
}
);

interface AvatarFallbackProps {
className?: string;
children: React.ReactNode;
delayMs?: number;
}

interface AvatarContextValue {
hasError: boolean;
setHasError: React.Dispatch<React.SetStateAction<boolean>>;
imageLoaded: boolean;
setImageLoaded: React.Dispatch<React.SetStateAction<boolean>>;
}

const AvatarContext = React.createContext<AvatarContextValue | null>(null);

const AvatarFallback = React.forwardRef<View, AvatarFallbackProps>(
({ className, children, delayMs = 600, ...props }, ref) => {
const [isShowing, setIsShowing] = React.useState(delayMs === 0);
const avatarContext = React.useContext(AvatarContext);

React.useEffect(() => {
if (delayMs === 0) return;

// Only show fallback if image has error or hasn't loaded after delay
const timer = setTimeout(() => {
if (!avatarContext?.imageLoaded) {
setIsShowing(true);
}
}, delayMs);

return () => clearTimeout(timer);
}, [delayMs, avatarContext?.imageLoaded]);

// Hide fallback if image loads successfully
React.useEffect(() => {
if (avatarContext?.imageLoaded) {
setIsShowing(false);
}
}, [avatarContext?.imageLoaded]);

if (!isShowing || avatarContext?.imageLoaded) {
return null;
}

return (
<View
ref={ref}
className={cn(
"absolute inset-0 flex h-full w-full items-center justify-center bg-muted",
className
)}
{...props}
>
{typeof children === "string" ? (
<Text className="text-base font-medium text-muted-foreground">
{children}
</Text>
) : (
children
)}
</View>
);
}
);

// Wrap the original AvatarRoot to provide context
const Avatar = React.forwardRef<View, AvatarRootProps>((props, ref) => {
const [hasError, setHasError] = React.useState(false);
const [imageLoaded, setImageLoaded] = React.useState(false);

return (
<AvatarContext.Provider
value={{
hasError,
setHasError,
imageLoaded,
setImageLoaded,
}}
>
<AvatarRoot ref={ref} {...props} />
</AvatarContext.Provider>
);
});

AvatarRoot.displayName = "AvatarRoot";
Avatar.displayName = "Avatar";
AvatarImage.displayName = "AvatarImage";
AvatarFallback.displayName = "AvatarFallback";

export { Avatar, AvatarImage, AvatarFallback };