---
/**
* CitationList: the sources behind an AI answer. Numbered rows with a
* monogram, page title, domain and which engines cited it. Mark your own
* domain with `own` and those rows get an accent "Yours" pill, so a reader
* can see at a glance where the brand shows up.
*
* @example
* <CitationList own="northwind.test" sources={[{ title: "How to rewax a canvas pack", url: "https://northwind.test/journal/rewax", engines: ["ChatGPT", "Perplexity"] }]} />
*/
interface Source {
title: string;
url: string;
/** Engines that cited this page, e.g. ["ChatGPT", "Perplexity"]. */
engines?: string[];
}
interface Props {
sources: Source[];
heading?: string;
/** Your domain. Matching rows (and subdomains) are marked "Yours". */
own?: string;
class?: string;
}
const { sources, heading = "Sources", own, class: className } = Astro.props;
const host = (url: string) => {
try {
return new URL(url).hostname.replace(/^www\./, "");
} catch {
return url;
}
};
const rows = sources.map((s) => {
const domain = host(s.url);
const mine = !!own && (domain === own || domain.endsWith(`.${own}`));
return { ...s, domain, mine, letter: domain.charAt(0).toUpperCase() };
});
const ownCount = rows.filter((r) => r.mine).length;
---
<div class:list={["vs-citation-list", className]}>
<div class="vs-citation-list__head">
<p class="vs-citation-list__heading">{heading}</p>
<p class="vs-citation-list__count">
{sources.length} cited{own && <> · <span>{ownCount} yours</span></>}
</p>
</div>
<ol class="vs-citation-list__rows">
{
rows.map((r, i) => (
<li class:list={{ "is-own": r.mine }}>
<a href={r.url} target="_blank" rel="noopener noreferrer">
<span class="vs-citation-list__num">{i + 1}</span>
<span class="vs-citation-list__mono" aria-hidden="true">{r.letter}</span>
<span class="vs-citation-list__text">
<span class="vs-citation-list__title">{r.title}</span>
<span class="vs-citation-list__domain">
{r.domain}
{r.mine && <span class="vs-citation-list__yours">Yours</span>}
</span>
</span>
{
r.engines && r.engines.length > 0 && (
<span class="vs-citation-list__engines">
<span class="vs-sr-only">Cited by</span>
{r.engines.map((e) => (
<span class="vs-citation-list__engine">{e}</span>
))}
</span>
)
}
</a>
</li>
))
}
</ol>
</div>
<style>
.vs-citation-list {
container-type: inline-size;
padding: 6px;
border-radius: calc(var(--vs-radius-window) + 4px);
background: var(--vs-surface);
color: var(--vs-ink);
font-family: var(--vs-font-sans);
font-stretch: var(--vs-font-stretch);
box-shadow: var(--vs-shadow-card);
}
.vs-citation-list__head {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 12px;
padding: 10px 12px 8px;
}
.vs-citation-list__heading {
margin: 0;
font-size: 0.9375rem;
font-weight: 600;
}
.vs-citation-list__count {
margin: 0;
font-size: 0.8125rem;
color: var(--vs-ink-3);
}
.vs-citation-list__count span {
color: var(--vs-accent-ink);
}
.vs-citation-list__rows {
margin: 0;
padding: 0;
list-style: none;
}
.vs-citation-list__rows a {
display: grid;
grid-template-columns: 18px 32px minmax(0, 1fr);
align-items: center;
gap: 10px;
padding: 10px 12px;
border-radius: var(--vs-radius-card);
color: inherit;
text-decoration: none;
transition: background-color var(--vs-duration-fast) ease-out;
animation: vs-fade-up var(--vs-duration) var(--vs-ease-out) both;
}
.vs-citation-list__rows li:nth-child(2) a { animation-delay: 40ms; }
.vs-citation-list__rows li:nth-child(3) a { animation-delay: 80ms; }
.vs-citation-list__rows li:nth-child(4) a { animation-delay: 120ms; }
.vs-citation-list__rows li:nth-child(n + 5) a { animation-delay: 160ms; }
.vs-citation-list__rows a:hover {
background: var(--vs-hover);
}
.vs-citation-list__rows a:focus-visible {
outline: none;
box-shadow: var(--vs-focus-ring);
}
.vs-citation-list__num {
font-size: 0.8125rem;
font-variant-numeric: tabular-nums;
color: var(--vs-ink-3);
text-align: right;
}
.vs-citation-list__mono {
display: grid;
place-items: center;
width: 32px;
height: 32px;
border-radius: var(--vs-radius-control);
background: var(--vs-inset);
border: 1px solid var(--vs-line);
font-size: 0.8125rem;
font-weight: 600;
color: var(--vs-ink-2);
}
.is-own .vs-citation-list__mono {
background: var(--vs-accent-tint);
border-color: transparent;
color: var(--vs-accent-ink);
}
.vs-citation-list__text {
display: grid;
gap: 2px;
min-width: 0;
}
.vs-citation-list__title {
overflow: hidden;
font-size: 0.9375rem;
font-weight: 500;
white-space: nowrap;
text-overflow: ellipsis;
}
.vs-citation-list__domain {
display: flex;
align-items: center;
gap: 8px;
font-size: 0.8125rem;
color: var(--vs-ink-3);
}
.vs-citation-list__yours {
padding: 1px 8px;
border-radius: var(--vs-radius-pill);
background: var(--vs-accent-tint);
color: var(--vs-accent-ink);
font-size: 0.75rem;
font-weight: 500;
}
.vs-citation-list__engines {
display: none;
gap: 4px;
justify-content: flex-end;
}
.vs-citation-list__engine {
padding: 3px 8px;
border-radius: var(--vs-radius-pill);
background: var(--vs-inset);
border: 1px solid var(--vs-line-soft);
font-size: 0.75rem;
color: var(--vs-ink-2);
white-space: nowrap;
}
@container (min-width: 520px) {
.vs-citation-list__rows a {
grid-template-columns: 18px 32px minmax(0, 1fr) auto;
}
.vs-citation-list__engines {
display: flex;
}
}
</style>