UI Library
Product card
A linked card with a name, tagline, metadata pills, and an arrow that nudges on hover. Every pill tone is a full literal class string, so Tailwind can actually see it.
Cardscardpillshover
Preview
apps/site/blocks/components/product-card.tsx
import { ArrowRight } from 'lucide-react';
export interface ProductCardProps {
name?: string;
tagline?: string;
href?: string;
/** Rendered as the accented pill. */
category?: string;
/** Rendered as the status pill; `muted` tones it down. */
status?: string;
statusTone?: 'active' | 'muted';
/** Extra neutral pills, e.g. star and fork counts. */
meta?: string[];
}
export function ProductCard({
name = 'Caspian Notes',
tagline = 'Markdown notes in VS Code, synced to your workspace.',
href = '#',
category,
status,
statusTone = 'active',
meta = [],
}: ProductCardProps) {
return (
<a
href={href}
className="group flex flex-col gap-3 rounded-xl border border-border bg-card p-5 transition-colors hover:border-foreground/30 hover:bg-accent/40"
>
<div className="flex items-start justify-between gap-3">
<div className="space-y-1">
<div className="text-base font-semibold tracking-tight">{name}</div>
<div className="text-sm text-muted-foreground">{tagline}</div>
</div>
{/* shrink-0 keeps the arrow from being squeezed by a long tagline. */}
<ArrowRight className="h-4 w-4 shrink-0 text-muted-foreground transition-transform group-hover:translate-x-0.5 group-hover:text-foreground" />
</div>
<div className="flex flex-wrap items-center gap-1.5">
{category ? <Pill tone="category">{category}</Pill> : null}
{status ? <Pill tone={statusTone === 'muted' ? 'muted' : 'status'}>{status}</Pill> : null}
{meta.map((m) => (
<Pill key={m} tone="muted">
{m}
</Pill>
))}
</div>
</a>
);
}
function Pill({
tone,
children,
}: {
tone: 'category' | 'status' | 'muted';
children: React.ReactNode;
}) {
// Full literal class strings per branch — Tailwind scans source text, so an
// interpolated `border-${tone}/30` would emit no utility at all.
const cls =
tone === 'category'
? 'border-primary/30 bg-primary/10 text-primary'
: tone === 'status'
? 'border-emerald-500/30 bg-emerald-500/10 text-emerald-500'
: 'border-border bg-muted text-muted-foreground';
return (
<span
className={`inline-flex h-5 items-center rounded-md border px-1.5 text-[10px] font-medium ${cls}`}
>
{children}
</span>
);
}
Dependencies
lucide-react— npm package
Colours come from CSS variables (--background, --primary, --border) using the shadcn/ui token names, so the block adopts your palette rather than importing ours.