|
|
|
|
| import { Badge } from '@/components/ui/badge'; |
| import { |
| Carousel, |
| type CarouselApi, |
| CarouselContent, |
| CarouselItem, |
| } from '@/components/ui/carousel'; |
| import { HoverCard, HoverCardContent, HoverCardTrigger } from '@/components/ui/hover-card'; |
| import { cn } from '@/lib/utils'; |
| import { ArrowLeftIcon, ArrowRightIcon } from 'lucide-react'; |
| import { |
| type ComponentProps, |
| createContext, |
| useCallback, |
| useContext, |
| useEffect, |
| useState, |
| } from 'react'; |
|
|
| export type InlineCitationProps = ComponentProps<'span'>; |
|
|
| export const InlineCitation = ({ className, ...props }: InlineCitationProps) => ( |
| <span className={cn('group inline items-center gap-1', className)} {...props} /> |
| ); |
|
|
| export type InlineCitationTextProps = ComponentProps<'span'>; |
|
|
| export const InlineCitationText = ({ className, ...props }: InlineCitationTextProps) => ( |
| <span className={cn('transition-colors group-hover:bg-accent', className)} {...props} /> |
| ); |
|
|
| export type InlineCitationCardProps = ComponentProps<typeof HoverCard>; |
|
|
| export const InlineCitationCard = (props: InlineCitationCardProps) => ( |
| <HoverCard closeDelay={0} openDelay={0} {...props} /> |
| ); |
|
|
| export type InlineCitationCardTriggerProps = ComponentProps<typeof Badge> & { |
| sources: string[]; |
| }; |
|
|
| export const InlineCitationCardTrigger = ({ |
| sources, |
| className, |
| ...props |
| }: InlineCitationCardTriggerProps) => ( |
| <HoverCardTrigger asChild> |
| <Badge className={cn('ml-1 rounded-full', className)} variant="secondary" {...props}> |
| {sources[0] ? ( |
| <> |
| {new URL(sources[0]).hostname} {sources.length > 1 && `+${sources.length - 1}`} |
| </> |
| ) : ( |
| 'unknown' |
| )} |
| </Badge> |
| </HoverCardTrigger> |
| ); |
|
|
| export type InlineCitationCardBodyProps = ComponentProps<'div'>; |
|
|
| export const InlineCitationCardBody = ({ className, ...props }: InlineCitationCardBodyProps) => ( |
| <HoverCardContent className={cn('relative w-80 p-0', className)} {...props} /> |
| ); |
|
|
| const CarouselApiContext = createContext<CarouselApi | undefined>(undefined); |
|
|
| const useCarouselApi = () => { |
| const context = useContext(CarouselApiContext); |
| return context; |
| }; |
|
|
| export type InlineCitationCarouselProps = ComponentProps<typeof Carousel>; |
|
|
| export const InlineCitationCarousel = ({ |
| className, |
| children, |
| ...props |
| }: InlineCitationCarouselProps) => { |
| const [api, setApi] = useState<CarouselApi>(); |
|
|
| return ( |
| <CarouselApiContext.Provider value={api}> |
| <Carousel className={cn('w-full', className)} setApi={setApi} {...props}> |
| {children} |
| </Carousel> |
| </CarouselApiContext.Provider> |
| ); |
| }; |
|
|
| export type InlineCitationCarouselContentProps = ComponentProps<'div'>; |
|
|
| export const InlineCitationCarouselContent = (props: InlineCitationCarouselContentProps) => ( |
| <CarouselContent {...props} /> |
| ); |
|
|
| export type InlineCitationCarouselItemProps = ComponentProps<'div'>; |
|
|
| export const InlineCitationCarouselItem = ({ |
| className, |
| ...props |
| }: InlineCitationCarouselItemProps) => ( |
| <CarouselItem className={cn('w-full space-y-2 p-4 pl-8', className)} {...props} /> |
| ); |
|
|
| export type InlineCitationCarouselHeaderProps = ComponentProps<'div'>; |
|
|
| export const InlineCitationCarouselHeader = ({ |
| className, |
| ...props |
| }: InlineCitationCarouselHeaderProps) => ( |
| <div |
| className={cn( |
| 'flex items-center justify-between gap-2 rounded-t-md bg-secondary p-2', |
| className, |
| )} |
| {...props} |
| /> |
| ); |
|
|
| export type InlineCitationCarouselIndexProps = ComponentProps<'div'>; |
|
|
| export const InlineCitationCarouselIndex = ({ |
| children, |
| className, |
| ...props |
| }: InlineCitationCarouselIndexProps) => { |
| const api = useCarouselApi(); |
| const count = api?.scrollSnapList().length ?? 0; |
| const [current, setCurrent] = useState(0); |
|
|
| useEffect(() => { |
| if (!api) { |
| return; |
| } |
|
|
| |
| setCurrent(api.selectedScrollSnap() + 1); |
|
|
| const onSelect = () => { |
| setCurrent(api.selectedScrollSnap() + 1); |
| }; |
| api.on('select', onSelect); |
| return () => { |
| api.off('select', onSelect); |
| }; |
| }, [api]); |
|
|
| return ( |
| <div |
| className={cn( |
| 'flex flex-1 items-center justify-end px-3 py-1 text-muted-foreground text-xs', |
| className, |
| )} |
| {...props} |
| > |
| {children ?? `${current}/${count}`} |
| </div> |
| ); |
| }; |
|
|
| export type InlineCitationCarouselPrevProps = ComponentProps<'button'>; |
|
|
| export const InlineCitationCarouselPrev = ({ |
| className, |
| ...props |
| }: InlineCitationCarouselPrevProps) => { |
| const api = useCarouselApi(); |
|
|
| const handleClick = useCallback(() => { |
| if (api) { |
| api.scrollPrev(); |
| } |
| }, [api]); |
|
|
| return ( |
| <button |
| aria-label="Previous" |
| className={cn('shrink-0', className)} |
| onClick={handleClick} |
| type="button" |
| {...props} |
| > |
| <ArrowLeftIcon className="size-4 text-muted-foreground" /> |
| </button> |
| ); |
| }; |
|
|
| export type InlineCitationCarouselNextProps = ComponentProps<'button'>; |
|
|
| export const InlineCitationCarouselNext = ({ |
| className, |
| ...props |
| }: InlineCitationCarouselNextProps) => { |
| const api = useCarouselApi(); |
|
|
| const handleClick = useCallback(() => { |
| if (api) { |
| api.scrollNext(); |
| } |
| }, [api]); |
|
|
| return ( |
| <button |
| aria-label="Next" |
| className={cn('shrink-0', className)} |
| onClick={handleClick} |
| type="button" |
| {...props} |
| > |
| <ArrowRightIcon className="size-4 text-muted-foreground" /> |
| </button> |
| ); |
| }; |
|
|
| export type InlineCitationSourceProps = ComponentProps<'div'> & { |
| title?: string; |
| url?: string; |
| description?: string; |
| }; |
|
|
| export const InlineCitationSource = ({ |
| title, |
| url, |
| description, |
| className, |
| children, |
| ...props |
| }: InlineCitationSourceProps) => ( |
| <div className={cn('space-y-1', className)} {...props}> |
| {title && <h4 className="truncate font-medium text-sm leading-tight">{title}</h4>} |
| {url && <p className="truncate break-all text-muted-foreground text-xs">{url}</p>} |
| {description && ( |
| <p className="line-clamp-3 text-muted-foreground text-sm leading-relaxed">{description}</p> |
| )} |
| {children} |
| </div> |
| ); |
|
|
| export type InlineCitationQuoteProps = ComponentProps<'blockquote'>; |
|
|
| export const InlineCitationQuote = ({ |
| children, |
| className, |
| ...props |
| }: InlineCitationQuoteProps) => ( |
| <blockquote |
| className={cn('border-muted border-l-2 pl-3 text-muted-foreground text-sm italic', className)} |
| {...props} |
| > |
| {children} |
| </blockquote> |
| ); |
|
|