@Yaya12085Lorem ipsum dolor sit amet, consectetur adipiscing elit. In massa ipsum, laoreet quis mollis nec, feugiat in dui. Suspendisse et enim pretium, ullamcorper enim laoreet.
cards
Quote Testimonial Card
A social-style single quote card: a multi-line blockquote, an @username attribution, and a footer row with comment / send / share action buttons plus a stacked viewer-avatar cluster. Card surface maps to --muted (theme-robust on dark + light); text hierarchy uses color-mix against --foreground / --background; viewer dots use --primary / --primary-foreground. Pure-CSS hover scales the card to 1.03× at 200ms via a CSS transition — no JS required. Ported from uiverse.io/Yaya12085/tall-goose-27 (MIT).
Ported from Yaya12085 (MIT)
Install
1. Register the namespace (once per project):
json
// components.json — register the @bottega namespace once
{
"registries": {
"@bottega": { "url": "https://bottega.ariacode.ca/r/{name}.json" }
}
}2. Add the component:
bash
npx shadcn add @bottega/quote-testimonial-cardUsage
Usagetsx
<QuoteTestimonialCard />Props
| Prop | Type | Default | Description |
|---|---|---|---|
| quote | string | Lorem ipsum dolor sit amet, consectetur adipiscing elit. In massa ipsum, laoreet quis mollis nec, feugiat in dui. Suspendisse et enim pretium, ullamcorper enim laoreet. | The quote body text. |
| username | string | @Yaya12085 | Attribution handle shown below the quote, e.g. "@handle". |
| commentCount | number | 18 | Number of comments. |
| sendCount | number | 7 | Number of sends. |
| shareCount | number | 2 | Number of shares. |
| viewerCount | number | 20 | Number of viewers shown in the avatar cluster. |
| className | string | Extra class names merged onto the root article. |
Source
quote-testimonial-card.tsxtsx
import type React from "react";
import styles from "./quote-testimonial-card.module.css";
export type QuoteTestimonialCardProps = {
/** The quote body text. */
quote?: string;
/** Attribution handle shown below the quote, e.g. "@handle". */
username?: string;
/** Number of comments. */
commentCount?: number;
/** Number of sends. */
sendCount?: number;
/** Number of shares. */
shareCount?: number;
/** Number of viewers shown in the avatar cluster. */
viewerCount?: number;
/** Extra class names merged onto the root article. */
className?: string;
};
// ---- Icons (aria-hidden; colour inherits via currentColor) ----
function CommentIcon() {
return (
<svg
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
className={styles.icon}
>
<path
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.5"
d="M16 10H16.01M12 10H12.01M8 10H8.01M3 10C3 4.64706 5.11765 3 12 3C18.8824 3 21 4.64706 21 10C21 15.3529 18.8824 17 12 17C11.6592 17 11.3301 16.996 11.0124 16.9876L7 21V16.4939C4.0328 15.6692 3 13.7383 3 10Z"
/>
</svg>
);
}
function SendIcon() {
return (
<svg
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
fill="currentColor"
viewBox="-2.5 0 32 32"
className={styles.icon}
>
{/* Paper-plane send icon, fills use currentColor */}
<path d="M0 10.284l0.505 0.36c0.089 0.064 0.92 0.621 2.604 0.621 0.27 0 0.55-0.015 0.836-0.044 3.752 4.346 6.411 7.472 7.060 8.299-1.227 2.735-1.42 5.808-0.537 8.686l0.256 0.834 7.63-7.631 8.309 8.309 0.742-0.742-8.309-8.309 7.631-7.631-0.834-0.255c-2.829-0.868-5.986-0.672-8.686 0.537-0.825-0.648-3.942-3.3-8.28-7.044 0.11-0.669 0.23-2.183-0.575-3.441l-0.352-0.549-8.001 8.001zM1.729 10.039l6.032-6.033c0.385 1.122 0.090 2.319 0.086 2.334l-0.080 0.314 0.245 0.214c7.409 6.398 8.631 7.39 8.992 7.546l-0.002 0.006 0.195 0.058 0.185-0.087c2.257-1.079 4.903-1.378 7.343-0.836l-13.482 13.481c-0.55-2.47-0.262-5.045 0.837-7.342l0.104-0.218-0.098-0.221-0.031 0.013c-0.322-0.632-1.831-2.38-7.498-8.944l-0.185-0.215-0.282 0.038c-0.338 0.045-0.668 0.069-0.981 0.069-0.595 0-1.053-0.083-1.38-0.176z" />
</svg>
);
}
function ShareIcon() {
return (
<svg
aria-hidden="true"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={styles.icon}
>
{/* Semi-transparent circle fills — decorative tint via currentColor */}
<path opacity="0.1" d="M21 6C21 7.65685 19.6569 9 18 9C16.3431 9 15 7.65685 15 6C15 4.34315 16.3431 3 18 3C19.6569 3 21 4.34315 21 6Z" fill="currentColor" />
<path opacity="0.1" d="M21 18C21 19.6569 19.6569 21 18 21C16.3431 21 15 19.6569 15 18C15 16.3431 16.3431 15 18 15C19.6569 15 21 16.3431 21 18Z" fill="currentColor" />
<path opacity="0.1" d="M9 12C9 13.6569 7.65685 15 6 15C4.34315 15 3 13.6569 3 12C3 10.3431 4.34315 9 6 9C7.65685 9 9 10.3431 9 12Z" fill="currentColor" />
<path d="M21 6C21 7.65685 19.6569 9 18 9C16.3431 9 15 7.65685 15 6C15 4.34315 16.3431 3 18 3C19.6569 3 21 4.34315 21 6Z" stroke="currentColor" strokeWidth="2" />
<path d="M21 18C21 19.6569 19.6569 21 18 21C16.3431 21 15 19.6569 15 18C15 16.3431 16.3431 15 18 15C19.6569 15 21 16.3431 21 18Z" stroke="currentColor" strokeWidth="2" />
<path d="M9 12C9 13.6569 7.65685 15 6 15C4.34315 15 3 13.6569 3 12C3 10.3431 4.34315 9 6 9C7.65685 9 9 10.3431 9 12Z" stroke="currentColor" strokeWidth="2" />
<path d="M8.7207 10.6397L15.0001 7.5" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
<path d="M8.70605 13.353L15 16.5" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg>
);
}
function PersonIcon() {
return (
<svg
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
className={styles.viewerIcon}
>
<path stroke="currentColor" strokeWidth="2" d="M17 8C17 10.7614 14.7614 13 12 13C9.23858 13 7 10.7614 7 8C7 5.23858 9.23858 3 12 3C14.7614 3 17 5.23858 17 8Z" />
<path stroke="currentColor" strokeLinecap="round" strokeWidth="2" d="M3 21C3.95728 17.9237 6.41998 17 12 17C17.58 17 20.0427 17.9237 21 21" />
</svg>
);
}
/**
* QuoteTestimonialCard — a single social-style quote card with an
* @username attribution and a comment/send/share action row. Hover
* scales the card up via a pure-CSS transition (200ms). Server component.
*
* Ported from uiverse.io/Yaya12085/tall-goose-27 (MIT).
*/
export function QuoteTestimonialCard({
quote = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. In massa ipsum, laoreet quis mollis nec, feugiat in dui. Suspendisse et enim pretium, ullamcorper enim laoreet.",
username = "@Yaya12085",
commentCount = 18,
sendCount = 7,
shareCount = 2,
viewerCount = 20,
className = "",
}: QuoteTestimonialCardProps) {
const rootClass = [styles.card, className].filter(Boolean).join(" ");
return (
<article className={rootClass} aria-label={`Quote by ${username}`}>
<div className={styles.body}>
<blockquote className={styles.quote}>
<p className={styles.text}>{quote}</p>
</blockquote>
<cite className={styles.username}>{username}</cite>
<footer className={styles.footer}>
<div className={styles.actions}>
<button
type="button"
className={styles.actionBtn}
aria-label={`${commentCount} comments`}
>
<CommentIcon />
<span>{commentCount}</span>
</button>
<button
type="button"
className={styles.actionBtn}
aria-label={`Send — ${sendCount}`}
>
<SendIcon />
<span>{sendCount}</span>
</button>
<button
type="button"
className={styles.actionBtn}
aria-label={`Share — ${shareCount} shares`}
>
<ShareIcon />
<span>{shareCount}</span>
</button>
</div>
<div className={styles.viewers} aria-label={`${viewerCount} viewers`}>
<span className={styles.viewerDot} aria-hidden="true">
<PersonIcon />
</span>
<span className={styles.viewerDot} aria-hidden="true">
<PersonIcon />
</span>
<span className={styles.viewerDot} aria-hidden="true">
<PersonIcon />
</span>
<span className={styles.viewerCount}>+{viewerCount}</span>
</div>
</footer>
</div>
</article>
);
}
Dependencies
- @bottega/tokens