veza/apps/web/desy/components/ui/avatar.tsx
2026-01-22 17:23:11 +01:00

189 lines
4.9 KiB
TypeScript

'use client'
import * as React from 'react'
import * as AvatarPrimitive from '@radix-ui/react-avatar'
import { cva, type VariantProps } from 'class-variance-authority'
import { cn } from '@/lib/utils'
const avatarVariants = cva(
'relative flex shrink-0 overflow-hidden',
{
variants: {
size: {
xs: 'size-6 text-[10px]',
sm: 'size-8 text-xs',
default: 'size-10 text-sm',
lg: 'size-12 text-base',
xl: 'size-16 text-lg',
'2xl': 'size-24 text-2xl',
},
shape: {
circle: 'rounded-full',
square: 'rounded-lg',
hex: '[clip-path:polygon(25%_0%,75%_0%,100%_50%,75%_100%,25%_100%,0%_50%)]',
},
ring: {
none: '',
default: 'ring-2 ring-border ring-offset-2 ring-offset-background',
primary: 'ring-2 ring-primary ring-offset-2 ring-offset-background',
gradient: 'p-0.5 bg-gradient-to-br from-primary to-secondary',
story: 'p-0.5 bg-gradient-to-br from-primary via-secondary to-primary',
live: 'p-0.5 bg-secondary animate-pulse',
},
},
defaultVariants: {
size: 'default',
shape: 'circle',
ring: 'none',
},
},
)
interface AvatarProps
extends React.ComponentProps<typeof AvatarPrimitive.Root>,
VariantProps<typeof avatarVariants> {
status?: 'online' | 'away' | 'busy' | 'offline'
}
function Avatar({
className,
size,
shape,
ring,
status,
children,
...props
}: AvatarProps) {
const hasRingWithPadding = ring === 'gradient' || ring === 'story' || ring === 'live'
return (
<div className={cn(avatarVariants({ size, shape, ring }), 'inline-flex')}>
<AvatarPrimitive.Root
data-slot="avatar"
className={cn(
'relative flex size-full shrink-0 overflow-hidden bg-muted',
shape === 'circle' && 'rounded-full',
shape === 'square' && 'rounded-md',
shape === 'hex' && '[clip-path:polygon(25%_0%,75%_0%,100%_50%,75%_100%,25%_100%,0%_50%)]',
hasRingWithPadding && 'bg-background',
className,
)}
{...props}
>
{children}
</AvatarPrimitive.Root>
{status && (
<AvatarStatus status={status} size={size} />
)}
</div>
)
}
function AvatarImage({
className,
...props
}: React.ComponentProps<typeof AvatarPrimitive.Image>) {
return (
<AvatarPrimitive.Image
data-slot="avatar-image"
className={cn('aspect-square size-full object-cover', className)}
{...props}
/>
)
}
function AvatarFallback({
className,
...props
}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
return (
<AvatarPrimitive.Fallback
data-slot="avatar-fallback"
className={cn(
'flex size-full items-center justify-center bg-gradient-to-br from-primary to-secondary font-semibold text-primary-foreground',
className,
)}
{...props}
/>
)
}
interface AvatarStatusProps {
status: 'online' | 'away' | 'busy' | 'offline'
size?: VariantProps<typeof avatarVariants>['size']
}
function AvatarStatus({ status, size }: AvatarStatusProps) {
const statusStyles = {
online: 'bg-success shadow-[0_0_8px_oklch(0.72_0.19_145_/_0.6)]',
away: 'bg-warning',
busy: 'bg-destructive',
offline: 'bg-muted-foreground',
}
const sizeStyles = {
xs: 'size-1.5 border',
sm: 'size-2 border',
default: 'size-2.5 border-2',
lg: 'size-3 border-2',
xl: 'size-4 border-2',
'2xl': 'size-5 border-[3px]',
}
return (
<span
data-slot="avatar-status"
className={cn(
'absolute bottom-0 right-0 rounded-full border-background',
statusStyles[status],
sizeStyles[size || 'default'],
)}
/>
)
}
// Avatar Group for stacked avatars
interface AvatarGroupProps extends React.ComponentProps<'div'> {
max?: number
size?: VariantProps<typeof avatarVariants>['size']
}
function AvatarGroup({
className,
children,
max = 4,
size = 'default',
...props
}: AvatarGroupProps) {
const childArray = React.Children.toArray(children)
const visibleChildren = childArray.slice(0, max)
const remainingCount = childArray.length - max
return (
<div
data-slot="avatar-group"
className={cn('flex -space-x-2', className)}
{...props}
>
{visibleChildren.map((child, index) => (
<div key={index} className="relative" style={{ zIndex: visibleChildren.length - index }}>
{React.isValidElement(child)
? React.cloneElement(child as React.ReactElement<AvatarProps>, { size, ring: 'default' })
: child
}
</div>
))}
{remainingCount > 0 && (
<Avatar size={size} ring="default">
<AvatarFallback className="bg-muted text-muted-foreground text-xs font-medium">
+{remainingCount}
</AvatarFallback>
</Avatar>
)}
</div>
)
}
export { Avatar, AvatarImage, AvatarFallback, AvatarStatus, AvatarGroup, avatarVariants }