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
7 changes: 5 additions & 2 deletions frontend/components.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,15 @@
"cssVariables": true,
"prefix": ""
},
"iconLibrary": "lucide",
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"iconLibrary": "lucide"
}
"registries": {
"@animate-ui": "https://animate-ui.com/r/{name}.json"
}
}
67 changes: 67 additions & 0 deletions frontend/components/animate-ui/components/animate/avatar-group.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import * as React from 'react';
import * as motion from 'motion/react-client';

import {
AvatarGroup as AvatarGroupPrimitive,
AvatarGroupTooltip as AvatarGroupTooltipPrimitive,
AvatarGroupTooltipArrow as AvatarGroupTooltipArrowPrimitive,
type AvatarGroupProps as AvatarGroupPropsPrimitive,
type AvatarGroupTooltipProps as AvatarGroupTooltipPropsPrimitive,
} from '@/components/animate-ui/primitives/animate/avatar-group';
import {cn} from '@/lib/utils';

type AvatarGroupProps = AvatarGroupPropsPrimitive;

function AvatarGroup({
className,
invertOverlap = true,
...props
}: AvatarGroupProps) {
return (
<AvatarGroupPrimitive
className={cn('h-12 -space-x-3', className)}
invertOverlap={invertOverlap}
{...props}
/>
);
}

type AvatarGroupTooltipProps = Omit<
AvatarGroupTooltipPropsPrimitive,
'asChild'
> & {
children: React.ReactNode;
layout?: boolean | 'position' | 'size' | 'preserve-aspect';
};

function AvatarGroupTooltip({
className,
children,
layout = 'preserve-aspect',
...props
}: AvatarGroupTooltipProps) {
return (
<AvatarGroupTooltipPrimitive
className={cn(
'bg-primary text-primary-foreground z-50 w-fit rounded-md px-3 py-1.5 text-xs text-balance',
className,
)}
{...props}
>
<motion.div layout={layout} className="overflow-hidden">
{children}
</motion.div>
<AvatarGroupTooltipArrowPrimitive
className="fill-primary size-3 data-[side='bottom']:translate-y-[1px] data-[side='right']:translate-x-[1px] data-[side='left']:translate-x-[-1px] data-[side='top']:translate-y-[-1px]"
tipRadius={2}
/>
</AvatarGroupTooltipPrimitive>
);
}

export {
AvatarGroup,
AvatarGroupTooltip,
type AvatarGroupProps,
type AvatarGroupTooltipProps,
};
144 changes: 144 additions & 0 deletions frontend/components/animate-ui/primitives/animate/avatar-group.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
'use client';

import * as React from 'react';
import {HTMLMotionProps, motion, type Transition} from 'motion/react';

import {
TooltipProvider,
Tooltip,
TooltipTrigger,
TooltipContent,
TooltipArrow,
type TooltipProviderProps,
type TooltipProps,
type TooltipContentProps,
type TooltipArrowProps,
} from '@/components/animate-ui/primitives/animate/tooltip';

type AvatarProps = Omit<HTMLMotionProps<'div'>, 'translate'> & {
children: React.ReactNode;
zIndex: number;
translate?: string | number;
} & Omit<TooltipProps, 'children'>;

function AvatarContainer({
zIndex,
translate,
side,
sideOffset,
align,
alignOffset,
...props
}: AvatarProps) {
return (
<Tooltip
side={side}
sideOffset={sideOffset}
align={align}
alignOffset={alignOffset}
>
<TooltipTrigger asChild>
<motion.div
data-slot="avatar-container"
initial="initial"
whileHover="hover"
whileTap="hover"
style={{position: 'relative', zIndex}}
>
<motion.div
variants={{
initial: {y: 0},
hover: {y: translate},
}}
{...props}
/>
</motion.div>
</TooltipTrigger>
</Tooltip>
);
}

type AvatarGroupProps = Omit<React.ComponentProps<'div'>, 'translate'> & {
children: React.ReactElement[];
invertOverlap?: boolean;
translate?: string | number;
transition?: Transition;
tooltipTransition?: Transition;
} & Omit<TooltipProviderProps, 'children'> &
Omit<TooltipProps, 'children'>;

function AvatarGroup({
ref,
children,
id,
transition = {type: 'spring', stiffness: 300, damping: 17},
invertOverlap = false,
translate = '-30%',
openDelay = 0,
closeDelay = 0,
side = 'top',
sideOffset = 25,
align = 'center',
alignOffset = 0,
tooltipTransition = {type: 'spring', stiffness: 300, damping: 35},
style,
...props
}: AvatarGroupProps) {
return (
<TooltipProvider
id={id}
openDelay={openDelay}
closeDelay={closeDelay}
transition={tooltipTransition}
>
<div
ref={ref}
data-slot="avatar-group"
style={{
display: 'flex',
alignItems: 'center',
...style,
}}
{...props}
>
{children?.map((child, index) => (
<AvatarContainer
key={index}
zIndex={
invertOverlap ? React.Children.count(children) - index : index
}
transition={transition}
translate={translate}
side={side}
sideOffset={sideOffset}
align={align}
alignOffset={alignOffset}
>
{child}
</AvatarContainer>
))}
</div>
</TooltipProvider>
);
}

type AvatarGroupTooltipProps = TooltipContentProps;

function AvatarGroupTooltip(props: AvatarGroupTooltipProps) {
return <TooltipContent {...props} />;
}

type AvatarGroupTooltipArrowProps = TooltipArrowProps;

function AvatarGroupTooltipArrow(props: AvatarGroupTooltipArrowProps) {
return <TooltipArrow {...props} />;
}

export {
AvatarGroup,
AvatarGroupTooltip,
AvatarGroupTooltipArrow,
type AvatarGroupProps,
type AvatarGroupTooltipProps,
type AvatarGroupTooltipArrowProps,
};
96 changes: 96 additions & 0 deletions frontend/components/animate-ui/primitives/animate/slot.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
'use client';

import * as React from 'react';
import {motion, isMotionComponent, type HTMLMotionProps} from 'motion/react';
import {cn} from '@/lib/utils';

type AnyProps = Record<string, unknown>;

type DOMMotionProps<T extends HTMLElement = HTMLElement> = Omit<
HTMLMotionProps<keyof HTMLElementTagNameMap>,
'ref'
> & { ref?: React.Ref<T> };

type WithAsChild<Base extends object> =
| (Base & { asChild: true; children: React.ReactElement })
| (Base & { asChild?: false | undefined });

type SlotProps<T extends HTMLElement = HTMLElement> = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
children?: any;
} & DOMMotionProps<T>;

function mergeRefs<T>(
...refs: (React.Ref<T> | undefined)[]
): React.RefCallback<T> {
return (node) => {
refs.forEach((ref) => {
if (!ref) return;
if (typeof ref === 'function') {
ref(node);
} else {
(ref as React.RefObject<T | null>).current = node;
}
});
};
}

function mergeProps<T extends HTMLElement>(
childProps: AnyProps,
slotProps: DOMMotionProps<T>,
): AnyProps {
const merged: AnyProps = {...childProps, ...slotProps};

if (childProps.className || slotProps.className) {
merged.className = cn(
childProps.className as string,
slotProps.className as string,
);
}

if (childProps.style || slotProps.style) {
merged.style = {
...(childProps.style as React.CSSProperties),
...(slotProps.style as React.CSSProperties),
};
}

return merged;
}

function Slot<T extends HTMLElement = HTMLElement>({
children,
ref,
...props
}: SlotProps<T>) {
const isAlreadyMotion =
typeof children.type === 'object' &&
children.type !== null &&
isMotionComponent(children.type);

const Base = React.useMemo(
() =>
isAlreadyMotion ?
(children.type as React.ElementType) :
motion.create(children.type as React.ElementType),
[isAlreadyMotion, children.type],
);

if (!React.isValidElement(children)) return null;

const {ref: childRef, ...childProps} = children.props as AnyProps;

const mergedProps = mergeProps(childProps, props);

return (
<Base {...mergedProps} ref={mergeRefs(childRef as React.Ref<T>, ref)} />
);
}

export {
Slot,
type SlotProps,
type WithAsChild,
type DOMMotionProps,
type AnyProps,
};
Loading
Loading