Skip to content
edmi-ui

Inline Citation

Inline source chip with a hover card that pages through the sources.

StackBlitz ↗StackBlitz ↗StackBlitz ↗

Nvidia beat revenue estimates for the quarterreuters.com +2 and raised guidance.

Nvidia beat revenue estimates for the quarterreuters.com +2 and raised guidance.

Nvidia beat revenue estimates for the quarter and raised guidance.

Built on badge, button, carousel and hover-card.

Terminal window
npx shadcn@latest add @edmi-ui/ai-inline-citation
Terminal window
pnpm dlx shadcn@latest add @edmi-ui/ai-inline-citation
Terminal window
yarn dlx shadcn@latest add @edmi-ui/ai-inline-citation
Terminal window
bunx shadcn@latest add @edmi-ui/ai-inline-citation

Pick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.

Terminal window
npx shadcn-vue@latest add @edmi-ui/ai-inline-citation
Terminal window
pnpm dlx shadcn-vue@latest add @edmi-ui/ai-inline-citation
Terminal window
yarn dlx shadcn-vue@latest add @edmi-ui/ai-inline-citation
Terminal window
bunx shadcn-vue@latest add @edmi-ui/ai-inline-citation

Pick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.

Terminal window
npx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/ai-inline-citation.json
Terminal window
pnpm dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/ai-inline-citation.json
Terminal window
yarn dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/ai-inline-citation.json
Terminal window
bunx --bun shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/ai-inline-citation.json

Pick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.

import {
InlineCitation,
InlineCitationCard,
InlineCitationCardBody,
InlineCitationCardTrigger,
InlineCitationCarousel,
InlineCitationCarouselContent,
InlineCitationCarouselHeader,
InlineCitationCarouselIndex,
InlineCitationCarouselNext,
InlineCitationCarouselPrev,
InlineCitationText,
} from "@/components/ai/inline-citation";
<p className="max-w-md text-sm leading-[1.7]">
<InlineCitation>
<InlineCitationText>
Nvidia beat revenue estimates for the quarter
</InlineCitationText>
<InlineCitationCard>
<InlineCitationCardTrigger />
<InlineCitationCardBody>
<InlineCitationCarousel>
<InlineCitationCarouselHeader>
<InlineCitationCarouselPrev />
<InlineCitationCarouselNext />
<InlineCitationCarouselIndex />
</InlineCitationCarouselHeader>
<InlineCitationCarouselContent>
</InlineCitationCarouselContent>
</InlineCitationCarousel>
</InlineCitationCardBody>
</InlineCitationCard>
</InlineCitation>{" "}
and raised guidance.
</p>
<script setup lang="ts">
import {
InlineCitation,
InlineCitationCard,
InlineCitationCardBody,
InlineCitationCardTrigger,
InlineCitationCarousel,
InlineCitationCarouselContent,
InlineCitationCarouselHeader,
InlineCitationCarouselIndex,
InlineCitationCarouselItem,
InlineCitationCarouselNext,
InlineCitationCarouselPrev,
InlineCitationQuote,
InlineCitationSource,
InlineCitationText,
} from "@/components/ai/inline-citation";
</script>
<template>
<p class="max-w-md text-sm leading-[1.7]">
<InlineCitation>
<InlineCitationText>Nvidia beat revenue estimates for the quarter</InlineCitationText>
<InlineCitationCard>
<InlineCitationCardTrigger />
<InlineCitationCardBody>
<InlineCitationCarousel>
<InlineCitationCarouselHeader>
<InlineCitationCarouselPrev />
<InlineCitationCarouselNext />
<InlineCitationCarouselIndex />
</InlineCitationCarouselHeader>
<InlineCitationCarouselContent>
<InlineCitationCarouselItem :key="s.url">
<InlineCitationSource :title="s.title" :url="s.url" />
<InlineCitationQuote>{{ s.quote }}</InlineCitationQuote>
</InlineCitationCarouselItem>
</InlineCitationCarouselContent>
</InlineCitationCarousel>
</InlineCitationCardBody>
</InlineCitationCard>
</InlineCitation>
and raised guidance.
</p>
</template>
<script lang="ts">
import {
InlineCitation,
InlineCitationCard,
InlineCitationCardBody,
InlineCitationCardTrigger,
InlineCitationCarousel,
InlineCitationCarouselContent,
InlineCitationCarouselHeader,
InlineCitationCarouselIndex,
InlineCitationCarouselItem,
InlineCitationCarouselNext,
InlineCitationCarouselPrev,
InlineCitationQuote,
InlineCitationSource,
InlineCitationText,
} from "$lib/components/ai/inline-citation";
</script>
<p class="max-w-md text-sm leading-[1.7]">
<InlineCitation>
<InlineCitationText>Nvidia beat revenue estimates for the quarter</InlineCitationText>
<InlineCitationCard>
<InlineCitationCardTrigger />
<InlineCitationCardBody>
<InlineCitationCarousel>
<InlineCitationCarouselHeader>
<InlineCitationCarouselPrev />
<InlineCitationCarouselNext />
<InlineCitationCarouselIndex />
</InlineCitationCarouselHeader>
<InlineCitationCarouselContent>
{#each sources as s (s.url)}
<InlineCitationCarouselItem>
<InlineCitationSource title={s.title} url={s.url} />
<InlineCitationQuote>{s.quote}</InlineCitationQuote>
</InlineCitationCarouselItem>
{/each}
</InlineCitationCarouselContent>
</InlineCitationCarousel>
</InlineCitationCardBody>
</InlineCitationCard>
</InlineCitation>
and raised guidance.
</p>
registry/components/ai/inline-citation.tsx
"use client";
import { cn } from "cn";
import type { ComponentProps } from "react";
import {
createContext,
useCallback,
useContext,
useEffect,
useState,
} from "react";
import { CaretLeftIcon, CaretRightIcon } from "@phosphor-icons/react";
import { Badge } from "@/registry/edmi/ui/badge";
import { Button } from "@/registry/edmi/ui/button";
import type { CarouselApi } from "@/registry/edmi/ui/carousel";
import {
Carousel,
CarouselContent,
CarouselItem,
} from "@/registry/edmi/ui/carousel";
import {
HoverCard,
HoverCardContent,
HoverCardTrigger,
} from "@/registry/edmi/ui/hover-card";
export type InlineCitationProps = ComponentProps<"span">;
export const InlineCitation = ({
className,
...props
}: InlineCitationProps) => (
<span
data-slot="ai-inline-citation"
className={cn("group/citation inline items-center gap-1", className)}
{...props}
/>
);
export type InlineCitationTextProps = ComponentProps<"span">;
export const InlineCitationText = ({
className,
...props
}: InlineCitationTextProps) => (
<span
data-slot="ai-inline-citation-text"
className={cn(
"transition-colors group-hover/citation:bg-accent",
className,
)}
{...props}
/>
);
export type InlineCitationCardProps = ComponentProps<typeof HoverCard>;
export const InlineCitationCard = (props: InlineCitationCardProps) => (
<HoverCard {...props} />
);
export type InlineCitationCardTriggerProps = ComponentProps<typeof Badge> & {
sources: string[];
};
const hostOf = (url: string) => {
try {
return new URL(url).hostname.replace(/^www\./, "");
} catch {
return url;
}
};
export const InlineCitationCardTrigger = ({
sources,
className,
...props
}: InlineCitationCardTriggerProps) => (
<HoverCardTrigger
closeDelay={0}
delay={0}
render={
<Badge
data-slot="ai-inline-citation-trigger"
className={cn(
"ml-1 h-5 cursor-default px-[7px] align-[1px] text-[11.5px] font-normal text-foreground-2",
className,
)}
shape="pill"
variant="secondary"
{...props}
/>
}
>
{sources[0] ? (
<>
{hostOf(sources[0])} {sources.length > 1 && `+${sources.length - 1}`}
</>
) : (
"unknown"
)}
</HoverCardTrigger>
);
export type InlineCitationCardBodyProps = ComponentProps<
typeof HoverCardContent
>;
export const InlineCitationCardBody = ({
className,
...props
}: InlineCitationCardBodyProps) => (
<HoverCardContent
className={cn("relative w-80 overflow-hidden 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">;
// CarouselContent has a -ml-4 gutter, so the left padding is gutter (16) + content inset (14).
export const InlineCitationCarouselItem = ({
className,
...props
}: InlineCitationCarouselItemProps) => (
<CarouselItem
className={cn("w-full space-y-1 py-3 pr-3.5 pl-[30px]", className)}
{...props}
/>
);
export type InlineCitationCarouselHeaderProps = ComponentProps<"div">;
export const InlineCitationCarouselHeader = ({
className,
...props
}: InlineCitationCarouselHeaderProps) => (
<div
data-slot="ai-inline-citation-header"
className={cn(
"flex items-center gap-0.5 border-b border-border px-2.5 py-2",
className,
)}
{...props}
/>
);
export type InlineCitationCarouselIndexProps = ComponentProps<"div">;
export const InlineCitationCarouselIndex = ({
children,
className,
...props
}: InlineCitationCarouselIndexProps) => {
const api = useCarouselApi();
const [current, setCurrent] = useState(0);
const [count, setCount] = useState(0);
const syncState = useCallback(() => {
if (!api) {
return;
}
setCount(api.scrollSnapList().length);
setCurrent(api.selectedScrollSnap() + 1);
}, [api]);
useEffect(() => {
if (!api) {
return;
}
syncState();
api.on("select", syncState);
return () => {
api.off("select", syncState);
};
}, [api, syncState]);
return (
<div
className={cn(
"ml-auto font-mono text-xs text-muted-foreground tabular-nums",
className,
)}
{...props}
>
{children ?? `${current}/${count}`}
</div>
);
};
export type InlineCitationCarouselPrevProps = ComponentProps<typeof Button>;
export const InlineCitationCarouselPrev = ({
className,
children,
...props
}: InlineCitationCarouselPrevProps) => {
const api = useCarouselApi();
const handleClick = useCallback(() => {
if (api) {
api.scrollPrev();
}
}, [api]);
return (
<Button
aria-label="Previous"
className={className}
onClick={handleClick}
size="icon-xs"
type="button"
variant="ghost"
{...props}
>
{children ?? (
<CaretLeftIcon
/>
)}
</Button>
);
};
export type InlineCitationCarouselNextProps = ComponentProps<typeof Button>;
export const InlineCitationCarouselNext = ({
className,
children,
...props
}: InlineCitationCarouselNextProps) => {
const api = useCarouselApi();
const handleClick = useCallback(() => {
if (api) {
api.scrollNext();
}
}, [api]);
return (
<Button
aria-label="Next"
className={className}
onClick={handleClick}
size="icon-xs"
type="button"
variant="ghost"
{...props}
>
{children ?? (
<CaretRightIcon
/>
)}
</Button>
);
};
export type InlineCitationSourceProps = ComponentProps<"div"> & {
title?: string;
url?: string;
description?: string;
};
export const InlineCitationSource = ({
title,
url,
description,
className,
children,
...props
}: InlineCitationSourceProps) => (
<div
data-slot="ai-inline-citation-source"
className={cn("space-y-0.5", className)}
{...props}
>
{title && (
<h4 className="truncate text-[13.5px] leading-tight font-semibold">
{title}
</h4>
)}
{url && (
<p className="truncate font-mono text-[11.5px] text-muted-foreground">
{url.replace(/^https?:\/\//, "")}
</p>
)}
{description && (
<p className="line-clamp-3 text-[13px] leading-relaxed text-muted-foreground">
{description}
</p>
)}
{children}
</div>
);
export type InlineCitationQuoteProps = ComponentProps<"blockquote">;
export const InlineCitationQuote = ({
children,
className,
...props
}: InlineCitationQuoteProps) => (
<blockquote
data-slot="ai-inline-citation-quote"
className={cn(
"mt-2.5 border-l-2 border-border pl-2.5 text-[12.5px] text-foreground-2",
className,
)}
{...props}
>
{children}
</blockquote>
);
registry/components/ai/inline-citation/InlineCitation.vue
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/registry/edmi/lib/utils"
const props = defineProps<{
class?: HTMLAttributes["class"]
}>()
</script>
<template>
<span data-slot="ai-inline-citation" :class="cn('group/citation inline items-center gap-1', props.class)">
<slot />
</span>
</template>
registry/components/ai/inline-citation/InlineCitationCard.vue
<script setup lang="ts">
import type { HoverCardRootProps } from "reka-ui"
import { HoverCard } from "@/registry/edmi/ui/hover-card"
interface InlineCitationCardProps extends /* @vue-ignore */ HoverCardRootProps {}
defineProps<InlineCitationCardProps>()
</script>
<template>
<!-- HoverCard props (open, delays) fall through as attrs and win over the 0 defaults. -->
<HoverCard :open-delay="0" :close-delay="0">
<slot />
</HoverCard>
</template>
registry/components/ai/inline-citation/InlineCitationCardBody.vue
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/registry/edmi/lib/utils"
import { HoverCardContent } from "@/registry/edmi/ui/hover-card"
const props = defineProps<{
class?: HTMLAttributes["class"]
}>()
</script>
<template>
<HoverCardContent :class="cn('relative w-80 overflow-hidden p-0', props.class)">
<slot />
</HoverCardContent>
</template>
registry/components/ai/inline-citation/InlineCitationCardTrigger.vue
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { computed } from "vue"
import { cn } from "@/registry/edmi/lib/utils"
import { Badge } from "@/registry/edmi/ui/badge"
import { HoverCardTrigger } from "@/registry/edmi/ui/hover-card"
const props = defineProps<{
/** Source URLs; shows the first host and `+N`. */
sources: string[]
class?: HTMLAttributes["class"]
}>()
const displayText = computed(() => {
const first = props.sources[0]
if (!first)
return "unknown"
let host = first
try {
host = new URL(first).hostname.replace(/^www\./, "")
}
catch {}
const more = props.sources.length - 1
return more > 0 ? `${host} +${more}` : host
})
</script>
<template>
<HoverCardTrigger as-child>
<Badge
as="span"
data-slot="ai-inline-citation-trigger"
shape="pill"
variant="secondary"
:class="cn('ml-1 h-5 cursor-default px-[7px] align-[1px] text-[11.5px] font-normal text-foreground-2', props.class)"
>
{{ displayText }}
</Badge>
</HoverCardTrigger>
</template>
registry/components/ai/inline-citation/InlineCitationCarousel.vue
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/registry/edmi/lib/utils"
import { Carousel } from "@/registry/edmi/ui/carousel"
const props = defineProps<{
class?: HTMLAttributes["class"]
}>()
</script>
<template>
<Carousel :class="cn('w-full', props.class)">
<slot />
</Carousel>
</template>
registry/components/ai/inline-citation/InlineCitationCarouselContent.vue
<script setup lang="ts">
import { CarouselContent } from "@/registry/edmi/ui/carousel"
</script>
<template>
<CarouselContent>
<slot />
</CarouselContent>
</template>
registry/components/ai/inline-citation/InlineCitationCarouselHeader.vue
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/registry/edmi/lib/utils"
const props = defineProps<{
class?: HTMLAttributes["class"]
}>()
</script>
<template>
<div
data-slot="ai-inline-citation-header"
:class="cn('flex items-center gap-0.5 border-b border-border px-2.5 py-2', props.class)"
>
<slot />
</div>
</template>
registry/components/ai/inline-citation/InlineCitationCarouselIndex.vue
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { computed, ref, watch } from "vue"
import { cn } from "@/registry/edmi/lib/utils"
import { useCarousel } from "@/registry/edmi/ui/carousel"
const props = defineProps<{
class?: HTMLAttributes["class"]
}>()
const { carouselApi } = useCarousel()
const current = ref(0)
const count = ref(0)
const displayText = computed(() => `${current.value}/${count.value}`)
function updateState() {
const api = carouselApi.value
if (!api)
return
count.value = api.scrollSnapList().length
current.value = api.selectedScrollSnap() + 1
}
watch(carouselApi, (api, _, onCleanup) => {
if (!api)
return
updateState()
api.on("select", updateState)
api.on("reInit", updateState)
onCleanup(() => {
api.off("select", updateState)
api.off("reInit", updateState)
})
}, { immediate: true })
</script>
<template>
<div :class="cn('ml-auto font-mono text-xs text-muted-foreground tabular-nums', props.class)">
<slot>{{ displayText }}</slot>
</div>
</template>
registry/components/ai/inline-citation/InlineCitationCarouselItem.vue
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/registry/edmi/lib/utils"
import { CarouselItem } from "@/registry/edmi/ui/carousel"
const props = defineProps<{
class?: HTMLAttributes["class"]
}>()
</script>
<template>
<!-- CarouselContent has a -ml-4 gutter, so the left padding is gutter (16) + content inset (14). -->
<CarouselItem :class="cn('w-full space-y-1 py-3 pr-3.5 pl-[30px]', props.class)">
<slot />
</CarouselItem>
</template>
registry/components/ai/inline-citation/InlineCitationCarouselNext.vue
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { ChevronRightIcon } from "@lucide/vue"
import { Button } from "@/registry/edmi/ui/button"
import { useCarousel } from "@/registry/edmi/ui/carousel"
const props = defineProps<{
class?: HTMLAttributes["class"]
}>()
const { scrollNext } = useCarousel()
</script>
<template>
<Button
type="button"
variant="ghost"
size="icon-xs"
aria-label="Next"
:class="props.class"
@click="scrollNext"
>
<slot><ChevronRightIcon /></slot>
</Button>
</template>
registry/components/ai/inline-citation/InlineCitationCarouselPrev.vue
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { ChevronLeftIcon } from "@lucide/vue"
import { Button } from "@/registry/edmi/ui/button"
import { useCarousel } from "@/registry/edmi/ui/carousel"
const props = defineProps<{
class?: HTMLAttributes["class"]
}>()
const { scrollPrev } = useCarousel()
</script>
<template>
<Button
type="button"
variant="ghost"
size="icon-xs"
aria-label="Previous"
:class="props.class"
@click="scrollPrev"
>
<slot><ChevronLeftIcon /></slot>
</Button>
</template>
registry/components/ai/inline-citation/InlineCitationQuote.vue
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/registry/edmi/lib/utils"
const props = defineProps<{
class?: HTMLAttributes["class"]
}>()
</script>
<template>
<blockquote
data-slot="ai-inline-citation-quote"
:class="cn('mt-2.5 border-l-2 border-border pl-2.5 text-[12.5px] text-foreground-2', props.class)"
>
<slot />
</blockquote>
</template>
registry/components/ai/inline-citation/InlineCitationSource.vue
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { computed } from "vue"
import { cn } from "@/registry/edmi/lib/utils"
const props = defineProps<{
title?: string
url?: string
description?: string
class?: HTMLAttributes["class"]
}>()
const shownUrl = computed(() => props.url?.replace(/^https?:\/\//, ""))
</script>
<template>
<div data-slot="ai-inline-citation-source" :class="cn('space-y-0.5', props.class)">
<h4 v-if="title" class="truncate text-[13.5px] leading-tight font-semibold">
{{ title }}
</h4>
<p v-if="shownUrl" class="truncate font-mono text-[11.5px] text-muted-foreground">
{{ shownUrl }}
</p>
<p v-if="description" class="line-clamp-3 text-[13px] leading-relaxed text-muted-foreground">
{{ description }}
</p>
<slot />
</div>
</template>
registry/components/ai/inline-citation/InlineCitationText.vue
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/registry/edmi/lib/utils"
const props = defineProps<{
class?: HTMLAttributes["class"]
}>()
</script>
<template>
<span data-slot="ai-inline-citation-text" :class="cn('transition-colors group-hover/citation:bg-accent', props.class)">
<slot />
</span>
</template>
registry/components/ai/inline-citation/index.ts
export { default as InlineCitation } from "./InlineCitation.vue";
export { default as InlineCitationCard } from "./InlineCitationCard.vue";
export { default as InlineCitationCardBody } from "./InlineCitationCardBody.vue";
export { default as InlineCitationCardTrigger } from "./InlineCitationCardTrigger.vue";
export { default as InlineCitationCarousel } from "./InlineCitationCarousel.vue";
export { default as InlineCitationCarouselContent } from "./InlineCitationCarouselContent.vue";
export { default as InlineCitationCarouselHeader } from "./InlineCitationCarouselHeader.vue";
export { default as InlineCitationCarouselIndex } from "./InlineCitationCarouselIndex.vue";
export { default as InlineCitationCarouselItem } from "./InlineCitationCarouselItem.vue";
export { default as InlineCitationCarouselNext } from "./InlineCitationCarouselNext.vue";
export { default as InlineCitationCarouselPrev } from "./InlineCitationCarouselPrev.vue";
export { default as InlineCitationQuote } from "./InlineCitationQuote.vue";
export { default as InlineCitationSource } from "./InlineCitationSource.vue";
export { default as InlineCitationText } from "./InlineCitationText.vue";
src/lib/registry/ai/inline-citation/index.ts
import Root from "./inline-citation.svelte";
import Card from "./inline-citation-card.svelte";
import CardBody from "./inline-citation-card-body.svelte";
import CardTrigger from "./inline-citation-card-trigger.svelte";
import Carousel from "./inline-citation-carousel.svelte";
import CarouselContent from "./inline-citation-carousel-content.svelte";
import CarouselHeader from "./inline-citation-carousel-header.svelte";
import CarouselIndex from "./inline-citation-carousel-index.svelte";
import CarouselItem from "./inline-citation-carousel-item.svelte";
import CarouselNext from "./inline-citation-carousel-next.svelte";
import CarouselPrev from "./inline-citation-carousel-prev.svelte";
import Quote from "./inline-citation-quote.svelte";
import Source from "./inline-citation-source.svelte";
import Text from "./inline-citation-text.svelte";
export {
Card,
Card as InlineCitationCard,
CardBody,
CardBody as InlineCitationCardBody,
CardTrigger,
CardTrigger as InlineCitationCardTrigger,
Carousel,
Carousel as InlineCitationCarousel,
CarouselContent,
CarouselContent as InlineCitationCarouselContent,
CarouselHeader,
CarouselHeader as InlineCitationCarouselHeader,
CarouselIndex,
CarouselIndex as InlineCitationCarouselIndex,
CarouselItem,
CarouselItem as InlineCitationCarouselItem,
CarouselNext,
CarouselNext as InlineCitationCarouselNext,
CarouselPrev,
CarouselPrev as InlineCitationCarouselPrev,
Quote,
Quote as InlineCitationQuote,
Root,
//
Root as InlineCitation,
Source,
Source as InlineCitationSource,
Text,
Text as InlineCitationText,
};
src/lib/registry/ai/inline-citation/inline-citation-card-body.svelte
<script lang="ts">
import { cn } from "$lib/utils.js";
import { HoverCardContent } from "$lib/registry/ui/hover-card/index.js";
import type { ComponentProps } from "svelte";
let {
class: className,
children,
...restProps
}: ComponentProps<typeof HoverCardContent> = $props();
</script>
<HoverCardContent class={cn("relative w-80 overflow-hidden p-0", className)} {...restProps}>
{@render children?.()}
</HoverCardContent>
src/lib/registry/ai/inline-citation/inline-citation-card-trigger.svelte
<script lang="ts">
import { cn } from "$lib/utils.js";
import { HoverCardTrigger } from "$lib/registry/ui/hover-card/index.js";
import { badgeVariants } from "$lib/registry/ui/badge/index.js";
import type { ComponentProps } from "svelte";
let {
class: className,
sources,
...restProps
}: Omit<ComponentProps<typeof HoverCardTrigger>, "children"> & {
/** Source URLs; shows the first host and `+N`. */
sources: string[];
} = $props();
const text = $derived.by(() => {
const first = sources[0];
if (!first) return "unknown";
let host = first;
try {
host = new URL(first).hostname.replace(/^www\./, "");
} catch {}
const more = sources.length - 1;
return more > 0 ? `${host} +${more}` : host;
});
</script>
<!-- A span trigger styled as the secondary pill badge. -->
<HoverCardTrigger
data-slot="ai-inline-citation-trigger"
class={cn(
badgeVariants({ variant: "secondary", shape: "pill" }),
"ml-1 h-5 cursor-default px-[7px] align-[1px] text-[11.5px] font-normal text-foreground-2",
className
)}
{...restProps}
>
{text}
</HoverCardTrigger>
src/lib/registry/ai/inline-citation/inline-citation-card.svelte
<script lang="ts">
import { HoverCard } from "$lib/registry/ui/hover-card/index.js";
import type { ComponentProps } from "svelte";
let {
open = $bindable(false),
openDelay = 0,
closeDelay = 0,
children,
...restProps
}: ComponentProps<typeof HoverCard> = $props();
</script>
<HoverCard bind:open {openDelay} {closeDelay} {...restProps}>
{@render children?.()}
</HoverCard>
src/lib/registry/ai/inline-citation/inline-citation-carousel-content.svelte
<script lang="ts">
import { CarouselContent } from "$lib/registry/ui/carousel/index.js";
import type { ComponentProps } from "svelte";
let { children, ...restProps }: ComponentProps<typeof CarouselContent> = $props();
</script>
<CarouselContent {...restProps}>
{@render children?.()}
</CarouselContent>
src/lib/registry/ai/inline-citation/inline-citation-carousel-header.svelte
<script lang="ts">
import { cn } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
let { class: className, children, ...restProps }: HTMLAttributes<HTMLDivElement> = $props();
</script>
<div
data-slot="ai-inline-citation-header"
class={cn("flex items-center gap-0.5 border-b border-border px-2.5 py-2", className)}
{...restProps}
>
{@render children?.()}
</div>
src/lib/registry/ai/inline-citation/inline-citation-carousel-index.svelte
<script lang="ts">
import { cn } from "$lib/utils.js";
import { getEmblaContext } from "$lib/registry/ui/carousel/context.js";
import type { HTMLAttributes } from "svelte/elements";
let { class: className, children, ...restProps }: HTMLAttributes<HTMLDivElement> = $props();
const embla = getEmblaContext("<InlineCitationCarouselIndex/>");
</script>
<div
class={cn("ml-auto font-mono text-xs text-muted-foreground tabular-nums", className)}
{...restProps}
>
{#if children}
{@render children()}
{:else}
{embla.selectedIndex + 1}/{embla.scrollSnaps.length}
{/if}
</div>
src/lib/registry/ai/inline-citation/inline-citation-carousel-item.svelte
<script lang="ts">
import { cn } from "$lib/utils.js";
import { CarouselItem } from "$lib/registry/ui/carousel/index.js";
import type { ComponentProps } from "svelte";
let {
class: className,
children,
...restProps
}: ComponentProps<typeof CarouselItem> = $props();
</script>
<!-- CarouselContent has a -ms-4 gutter, so the left padding is gutter (16) + content inset (14). -->
<CarouselItem class={cn("w-full space-y-1 py-3 pr-3.5 ps-[30px]", className)} {...restProps}>
{@render children?.()}
</CarouselItem>
src/lib/registry/ai/inline-citation/inline-citation-carousel-next.svelte
<script lang="ts">
import CaretRightIcon from "phosphor-svelte/lib/CaretRight";
import { Button, type ButtonProps } from "$lib/registry/ui/button/index.js";
import { getEmblaContext } from "$lib/registry/ui/carousel/context.js";
let { children, ...restProps }: Omit<ButtonProps, "href" | "onclick"> = $props();
const embla = getEmblaContext("<InlineCitationCarouselNext/>");
</script>
<Button
type="button"
variant="ghost"
size="icon-xs"
aria-label="Next"
onclick={() => embla.scrollNext()}
{...restProps}
>
{#if children}
{@render children()}
{:else}
<CaretRightIcon
/>
{/if}
</Button>
src/lib/registry/ai/inline-citation/inline-citation-carousel-prev.svelte
<script lang="ts">
import CaretLeftIcon from "phosphor-svelte/lib/CaretLeft";
import { Button, type ButtonProps } from "$lib/registry/ui/button/index.js";
import { getEmblaContext } from "$lib/registry/ui/carousel/context.js";
let { children, ...restProps }: Omit<ButtonProps, "href" | "onclick"> = $props();
const embla = getEmblaContext("<InlineCitationCarouselPrev/>");
</script>
<Button
type="button"
variant="ghost"
size="icon-xs"
aria-label="Previous"
onclick={() => embla.scrollPrev()}
{...restProps}
>
{#if children}
{@render children()}
{:else}
<CaretLeftIcon
/>
{/if}
</Button>
src/lib/registry/ai/inline-citation/inline-citation-carousel.svelte
<script lang="ts">
import { cn } from "$lib/utils.js";
import { Carousel } from "$lib/registry/ui/carousel/index.js";
import type { ComponentProps } from "svelte";
let {
class: className,
children,
...restProps
}: ComponentProps<typeof Carousel> = $props();
</script>
<Carousel class={cn("w-full", className)} {...restProps}>
{@render children?.()}
</Carousel>
src/lib/registry/ai/inline-citation/inline-citation-quote.svelte
<script lang="ts">
import { cn } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
let { class: className, children, ...restProps }: HTMLAttributes<HTMLQuoteElement> = $props();
</script>
<blockquote
data-slot="ai-inline-citation-quote"
class={cn("mt-2.5 border-l-2 border-border pl-2.5 text-[12.5px] text-foreground-2", className)}
{...restProps}
>
{@render children?.()}
</blockquote>
src/lib/registry/ai/inline-citation/inline-citation-source.svelte
<script lang="ts">
import { cn } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
let {
class: className,
title,
url,
description,
children,
...restProps
}: HTMLAttributes<HTMLDivElement> & {
title?: string;
url?: string;
description?: string;
} = $props();
</script>
<div data-slot="ai-inline-citation-source" class={cn("space-y-0.5", className)} {...restProps}>
{#if title}
<h4 class="truncate text-[13.5px] leading-tight font-semibold">{title}</h4>
{/if}
{#if url}
<p class="truncate font-mono text-[11.5px] text-muted-foreground">{url.replace(/^https?:\/\//, "")}</p>
{/if}
{#if description}
<p class="line-clamp-3 text-[13px] leading-relaxed text-muted-foreground">{description}</p>
{/if}
{@render children?.()}
</div>
src/lib/registry/ai/inline-citation/inline-citation-text.svelte
<script lang="ts">
import { cn } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
let { class: className, children, ...restProps }: HTMLAttributes<HTMLSpanElement> = $props();
</script>
<span
data-slot="ai-inline-citation-text"
class={cn("transition-colors group-hover/citation:bg-accent", className)}
{...restProps}
>
{@render children?.()}
</span>
src/lib/registry/ai/inline-citation/inline-citation.svelte
<script lang="ts">
import { cn } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
let { class: className, children, ...restProps }: HTMLAttributes<HTMLSpanElement> = $props();
</script>
<span data-slot="ai-inline-citation" class={cn("group/citation inline items-center gap-1", className)} {...restProps}>
{@render children?.()}
</span>
Part Prop Type Default
InlineCitationCardTrigger sources string[] (URLs; shows the first host and +N)
InlineCitationCarousel Carousel props
InlineCitationCarouselHeader arrows left, index right
InlineCitationSource title, url, description string
InlineCitationQuote blockquote
  • The hover card opens instantly (no delay) like the stock component and is a solid --popover surface.
  • Header arrows are ghost icon-xs buttons; the page index is mono.
  • The quote is a 2px --border rule, not italic.