160 lines
5.5 KiB
TypeScript
160 lines
5.5 KiB
TypeScript
'use client';
|
|
|
|
import * as React from 'react';
|
|
|
|
import { cn } from '@workspace/ui/lib/utils';
|
|
import {
|
|
Tooltip,
|
|
TooltipContent,
|
|
TooltipProvider,
|
|
TooltipTrigger,
|
|
type TooltipProps,
|
|
type TooltipContentProps,
|
|
} from '@/registry/components/tooltip';
|
|
|
|
type Align = 'start' | 'center' | 'end';
|
|
|
|
type AvatarProps = TooltipProps & {
|
|
children: React.ReactNode;
|
|
align?: Align;
|
|
invertOverlap?: boolean;
|
|
};
|
|
|
|
function AvatarContainer({
|
|
children,
|
|
align,
|
|
invertOverlap,
|
|
...props
|
|
}: AvatarProps) {
|
|
return (
|
|
<Tooltip {...props}>
|
|
<TooltipTrigger>
|
|
<span
|
|
data-slot="avatar-container"
|
|
className={cn(
|
|
align === 'start'
|
|
? 'items-start'
|
|
: align === 'center'
|
|
? 'items-center'
|
|
: 'items-end',
|
|
'relative grid w-[var(--avatar-size)] aspect-[1/calc(1+var(--avatar-mask-ratio))]',
|
|
'[&_[data-slot=avatar]]:size-[var(--avatar-size)] [&_[data-slot=avatar]]:rounded-full',
|
|
invertOverlap
|
|
? cn(
|
|
'[&:not(:first-of-type)]:[--circle:calc(((var(--avatar-border)*2)+var(--avatar-size))*0.5)]',
|
|
'[&:not(:first-of-type)]:mask-[radial-gradient(var(--circle)_var(--circle)_at_calc(var(--circle)-var(--avatar-column-size)-var(--avatar-border))_50%_,#0000_calc(var(--circle)-0.5px),#fff_var(--circle))]',
|
|
'[&:not(:first-of-type)]:mask-size-[100%_100%]',
|
|
'[&:not(:first-of-type)]:mask-position-[0_calc(var(--avatar-size)*var(--avatar-mask-base))]',
|
|
'[&:not(:first-of-type)]:transition-[mask-position] [&:not(:first-of-type)]:duration-300 [&:not(:first-of-type)]:ease-in-out',
|
|
'[&:hover+&]:mask-position-[0_calc(var(--avatar-size)_-_calc(var(--avatar-size)*(var(--avatar-mask-factor)+var(--avatar-mask-offset))))]',
|
|
)
|
|
: cn(
|
|
'[&:not(:last-of-type)]:[--circle:calc(((var(--avatar-border)*2)+var(--avatar-size))*0.5)]',
|
|
'[&:not(:last-of-type)]:mask-[radial-gradient(var(--circle)_var(--circle)_at_calc(var(--circle)+var(--avatar-column-size)-var(--avatar-border))_50%_,#0000_calc(var(--circle)-0.5px),#fff_var(--circle))]',
|
|
'[&:not(:last-of-type)]:mask-size-[100%_100%]',
|
|
'[&:not(:last-of-type)]:mask-position-[0_calc(var(--avatar-size)*var(--avatar-mask-base))]',
|
|
'[&:not(:last-of-type)]:transition-[mask-position] [&:not(:last-of-type)]:duration-300 [&:not(:last-of-type)]:ease-in-out',
|
|
'[&:has(+&:hover)]:mask-position-[0_calc(var(--avatar-size)_-_calc(var(--avatar-size)*(var(--avatar-mask-factor)+var(--avatar-mask-offset))))]',
|
|
),
|
|
'[&>span]:transition-[translate] [&>span]:duration-300 [&>span]:ease-in-out',
|
|
'[&:hover_span:first-of-type]:translate-y-[var(--avatar-translate-pct)]',
|
|
)}
|
|
>
|
|
{children}
|
|
</span>
|
|
</TooltipTrigger>
|
|
</Tooltip>
|
|
);
|
|
}
|
|
|
|
type AvatarGroupTooltipProps = TooltipContentProps;
|
|
|
|
function AvatarGroupTooltip(props: AvatarGroupTooltipProps) {
|
|
return <TooltipContent {...props} />;
|
|
}
|
|
|
|
type AvatarGroupProps = Omit<React.ComponentProps<'div'>, 'translate'> & {
|
|
children: React.ReactElement[];
|
|
invertOverlap?: boolean;
|
|
translate?: number;
|
|
size?: string | number;
|
|
border?: string | number;
|
|
columnSize?: string | number;
|
|
align?: Align;
|
|
tooltipProps?: Omit<TooltipProps, 'children'>;
|
|
};
|
|
|
|
function AvatarGroup({
|
|
ref,
|
|
children,
|
|
className,
|
|
invertOverlap = false,
|
|
size = '43px',
|
|
border = '3px',
|
|
columnSize = '37px',
|
|
align = 'end',
|
|
translate = -30,
|
|
tooltipProps = { side: 'top', sideOffset: 12 },
|
|
...props
|
|
}: AvatarGroupProps) {
|
|
const maskRatio = Math.abs(translate / 100);
|
|
const alignOffset =
|
|
align === 'start' ? 0 : align === 'center' ? maskRatio / 2 : maskRatio;
|
|
const maskBase = alignOffset - maskRatio / 2;
|
|
const maskFactor = 1 - alignOffset + maskRatio / 2;
|
|
|
|
return (
|
|
<TooltipProvider openDelay={0} closeDelay={0}>
|
|
<div
|
|
ref={ref}
|
|
data-slot="avatar-group"
|
|
style={
|
|
{
|
|
'--avatar-size': size,
|
|
'--avatar-border': border,
|
|
'--avatar-column-size': columnSize,
|
|
'--avatar-translate-pct': `${translate}%`,
|
|
'--avatar-mask-offset': -(translate / 100),
|
|
'--avatar-mask-ratio': maskRatio,
|
|
'--avatar-mask-base': maskBase,
|
|
'--avatar-mask-factor': maskFactor,
|
|
'--avatar-columns': React.Children.count(children),
|
|
} as React.CSSProperties
|
|
}
|
|
className="h-[var(--avatar-size)] w-[calc(var(--avatar-column-size)*(var(--avatar-columns))+calc(var(--avatar-size)-var(--avatar-column-size)))]"
|
|
>
|
|
<span
|
|
className={cn(
|
|
'grid h-[var(--avatar-size)] grid-cols-[repeat(var(--avatar-columns),var(--avatar-column-size))]',
|
|
align === 'start'
|
|
? 'content-start'
|
|
: align === 'center'
|
|
? 'content-center'
|
|
: 'content-end',
|
|
className,
|
|
)}
|
|
{...props}
|
|
>
|
|
{children?.map((child, index) => (
|
|
<AvatarContainer
|
|
key={index}
|
|
invertOverlap={invertOverlap}
|
|
{...tooltipProps}
|
|
align={align}
|
|
>
|
|
{child}
|
|
</AvatarContainer>
|
|
))}
|
|
</span>
|
|
</div>
|
|
</TooltipProvider>
|
|
);
|
|
}
|
|
|
|
export {
|
|
AvatarGroup,
|
|
AvatarGroupTooltip,
|
|
type AvatarGroupProps,
|
|
type AvatarGroupTooltipProps,
|
|
};
|