Manifest UI
blockpost-cardsource: ok · hit
Post Card
Post card with image, title, excerpt, author info and read more button. Supports default, compact, horizontal, and covered variants. Includes post-detail for fullscreen view.
npx shadcn add "https://ui.manifest.build/r/post-card.json"free
No selection tags yet — empty means no evidence, not lack of support.
Dependencies
Bundle weight · lighticonslucide-react
Registry dependencies
buttontooltiphttps://ui.manifest.build/r/manifest-types.json
Files3 · source cached 10/8/2026, 10:49:21 PM
registry/blogging/post-card.tsx
'use client'
import { Button } from '@/components/ui/button'
// Import types from shared types file to avoid circular dependencies
import type { Post } from './types'
// Re-export for backward compatibility
export type { Post } from './types'
import { demoPost } from './demo/blogging'
/**
* ═══════════════════════════════════════════════════════════════════════════
* PostCardProps
* ═══════════════════════════════════════════════════════════════════════════
*
* Props for the PostCard component, a blog post card with multiple layout variants.
*/
export interface PostCardProps {
data?: {
/** The blog post to display. */
post?: Post
}
actions?: {
/** Called when the read more button is clicked. */
onReadMore?: (post: Post) => void
}
appearance?: {
/**
* Card layout variant.
* @default "default"
*/
variant?: 'default' | 'compact' | 'horizontal' | 'covered'
/**
* Whether to show the cover image.
* @default true
*/
showImage?: boolean
/**
* Whether to show author information.
* @default true
*/
showAuthor?: boolean
/**
* Whether to show the category label.
* @default true
*/
showCategory?: boolean
}
}
/**
* A blog post card component with multiple layout variants.
* Supports default, compact, horizontal, and covered (overlay) styles.
*
* Features:
* - Four layout variants (default, compact, horizontal, covered)
* - Cover image with hover zoom effect
* - Author avatar and info display
* - Category and tags display
* - Read more action button
* - Responsive design
*
* @component
* @example
* ```tsx
* <PostCard
* data={{
* post: {
* title: "Getting Started Guide",
* excerpt: "Learn the basics of our component library.",
* coverImage: "https://example.com/image.jpg",
* author: { name: "Sarah Chen", avatar: "https://example.com/avatar.jpg" },
* publishedAt: "2024-01-15",
* readTime: "5 min read",
* tags: ["Tutorial", "Components"],
* category: "Tutorial"
* }
* }}
* actions={{
* onReadMore: (post) => console.log("Read more:", post.title)
* }}
* appearance={{
* variant: "default",
* showImage: true,
* showAuthor: true,
* showCategory: true
* }}
* />
* ```
*/
export function PostCard({ data, actions, appearance }: PostCardProps) {
const resolved: NonNullable<PostCardProps['data']> = data ?? { post: demoPost }
const post = resolved.post
if (!post) {
return null
}
const onReadMore = actions?.onReadMore
const variant = appearance?.variant ?? 'default'
const showImage = appearance?.showImage ?? true
const showAuthor = appearance?.showAuthor ?? true
const showCategory = appearance?.showCategory ?? true
// Handle "Read more" click - only call callback if provided, otherwise do nothing
// This lets users decide to use an external link or open fullscreen mode
const handleReadMore = () => {
if (onReadMore) {
onReadMore(post)
}
}
const formatDate = (dateStr: string) => {
return new Date(dateStr).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric'
})
}
if (variant === 'covered') {
return (
<div className="relative overflow-hidden rounded-lg border">
<div className="min-h-[280px] sm:aspect-[16/9] sm:min-h-0 w-full">
{post.coverImage ? (
<img
src={post.coverImage}
alt={post.title || ''}
className="absolute inset-0 h-full w-full object-cover"
/>
) : (
<div className="absolute inset-0 h-full w-full bg-muted" />
)}
</div>
{/* Minimal overlay - solid color instead of gradient per ChatGPT guidelines */}
<div className="absolute inset-0 bg-black/60" />
<div className="absolute inset-0 flex flex-col justify-end p-4 text-white">
<div>
{showCategory && post.category && (
<p className="text-[10px] font-medium uppercase tracking-wide text-white/70">
{post.category}
</p>
)}
{post.title && (
<h2 className="mt-1 text-lg font-semibold leading-tight">
{post.title}
</h2>
)}
{post.excerpt && (
<p className="mt-1 line-clamp-2 text-sm text-white/80">
{post.excerpt}
</p>
)}
{post.tags && post.tags.length > 0 && (
<div className="mt-2 flex flex-wrap gap-1">
{post.tags.slice(0, 2).map((tag) => (
<span
key={tag}
className="rounded-md bg-white/20 px-2 py-0.5 text-xs"
>
{tag}
</span>
))}
</div>
)}
<div className="mt-3 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
{showAuthor && (
<div className="flex items-center gap-2">
{post.author?.avatar && (
<img
src={post.author.avatar}
alt={post.author?.name || ''}
className="h-6 w-6 rounded-full ring-1 ring-white/30"
/>
)}
<div className="text-xs">
{post.author?.name && (
<p className="font-medium">{post.author.name}</p>
)}
{post.publishedAt && (
<p className="text-white/60">
{formatDate(post.publishedAt)}
</p>
)}
</div>
</div>
)}
<Button
size="sm"
variant="secondary"
className="w-full sm:w-auto"
onClick={handleReadMore}
>
Read article
</Button>
</div>
</div>
</div>
</div>
)
}
if (variant === 'horizontal') {
return (
<div className="flex flex-col sm:flex-row gap-4 rounded-lg border bg-card p-3">
{showImage && post.coverImage && (
<div className="aspect-video sm:aspect-square sm:h-24 sm:w-24 shrink-0 overflow-hidden rounded-md">
<img
src={post.coverImage}
alt={post.title || ''}
className="h-full w-full object-cover"
/>
</div>
)}
<div className="flex flex-1 flex-col justify-between">
<div>
{showCategory && post.category && (
<p className="mb-0.5 text-[10px] font-medium uppercase tracking-wide text-muted-foreground">
{post.category}
</p>
)}
{post.title && (
<h3 className="line-clamp-2 text-sm font-medium leading-tight">
{post.title}
</h3>
)}
{post.excerpt && (
<p className="mt-1 line-clamp-2 text-xs text-muted-foreground">
{post.excerpt}
</p>
)}
{post.tags && post.tags.length > 0 && (
<div className="mt-1.5 flex flex-wrap gap-1">
{post.tags.slice(0, 2).map((tag) => (
<span
key={tag}
className="rounded-full bg-muted px-2 py-0.5 text-xs text-muted-foreground"
>
{tag}
</span>
))}
</div>
)}
</div>
<div className="mt-3 flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
<div className="flex items-center gap-2 text-xs text-muted-foreground">
{showAuthor && post.author?.avatar && (
<img
src={post.author.avatar}
alt={post.author?.name || ''}
className="h-4 w-4 rounded-full"
/>
)}
{post.publishedAt && (
<span>{formatDate(post.publishedAt)}</span>
)}
{post.readTime && (
<>
<span>·</span>
<span>{post.readTime}</span>
</>
)}
</div>
<Button
size="sm"
className="w-full sm:w-auto"
onClick={handleReadMore}
>
Read
</Button>
</div>
</div>
</div>
)
}
if (variant === 'compact') {
return (
<div className="flex h-full flex-col justify-between rounded-lg border bg-card p-3">
<div>
{showCategory && post.category && (
<p className="mb-0.5 text-[10px] font-medium uppercase tracking-wide text-muted-foreground">
{post.category}
</p>
)}
{post.title && (
<h3 className="line-clamp-2 text-sm font-medium">{post.title}</h3>
)}
{post.excerpt && (
<p className="mt-1 line-clamp-2 text-xs text-muted-foreground">
{post.excerpt}
</p>
)}
{post.tags && post.tags.length > 0 && (
<div className="mt-1.5 flex flex-wrap gap-1">
{post.tags.slice(0, 2).map((tag) => (
<span
key={tag}
className="rounded-full bg-muted px-2 py-0.5 text-xs text-muted-foreground"
>
{tag}
</span>
))}
</div>
)}
</div>
<div className="mt-3 flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
<div className="flex items-center gap-2">
{showAuthor && post.author?.avatar && (
<img
src={post.author.avatar}
alt={post.author?.name || ''}
className="h-5 w-5 rounded-full"
/>
)}
{post.publishedAt && (
<span className="text-xs text-muted-foreground">
{formatDate(post.publishedAt)}
</span>
)}
</div>
<Button size="sm" onClick={handleReadMore}>
Read more
</Button>
</div>
</div>
)
}
// Default variant
return (
<div className="flex h-full flex-col overflow-hidden rounded-lg border bg-card">
{showImage && post.coverImage && (
<div className="aspect-video overflow-hidden">
<img
src={post.coverImage}
alt={post.title || ''}
className="h-full w-full object-cover transition-transform hover:scale-105"
/>
</div>
)}
<div className="flex flex-1 flex-col justify-between p-4">
<div>
{showCategory && post.category && (
<p className="mb-1 text-[10px] font-medium uppercase tracking-wide text-muted-foreground">
{post.category}
</p>
)}
{post.title && (
<h3 className="line-clamp-2 font-medium">{post.title}</h3>
)}
{post.excerpt && (
<p className="mt-2 line-clamp-2 text-sm text-muted-foreground">
{post.excerpt}
</p>
)}
{post.tags && post.tags.length > 0 && (
<div className="mt-2 flex flex-wrap gap-1">
{post.tags.slice(0, 2).map((tag) => (
<span
key={tag}
className="rounded-full bg-muted px-2 py-0.5 text-xs text-muted-foreground"
>
{tag}
</span>
))}
</div>
)}
</div>
<div className="mt-4 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
{showAuthor && (
<div className="flex items-center gap-2">
{post.author?.avatar && (
<img
src={post.author.avatar}
alt={post.author?.name || ''}
className="h-6 w-6 rounded-full"
/>
)}
<div className="text-xs">
{post.author?.name && (
<p className="font-medium">{post.author.name}</p>
)}
{post.publishedAt && (
<p className="text-muted-foreground">
{formatDate(post.publishedAt)}
</p>
)}
</div>
</div>
)}
<Button size="sm" onClick={handleReadMore}>
Read
</Button>
</div>
</div>
</div>
)
}
registry/blogging/post-detail.tsx
'use client';
import { Button } from '@/components/ui/button';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
import { Calendar, Clock, ExternalLink, Maximize2 } from 'lucide-react';
import { useMemo } from 'react';
import type { Post } from './types';
import { demoPostDetailData } from './demo/blogging';
// DOM-based allowlist HTML sanitizer for post content
const ALLOWED_TAGS = new Set([
'p', 'br', 'b', 'i', 'em', 'strong', 'a', 'ul', 'ol', 'li',
'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'code', 'pre',
'span', 'div', 'img', 'figure', 'figcaption', 'hr',
]);
const ALLOWED_ATTRS: Record<string, Set<string>> = {
a: new Set(['href', 'target', 'rel', 'title']),
img: new Set(['src', 'alt', 'width', 'height']),
'*': new Set(['class', 'id']),
};
const DANGEROUS_URL = /^\s*(javascript|data):/i;
function sanitizeNode(node: Node, doc: Document): void {
const children = Array.from(node.childNodes);
for (const child of children) {
if (child.nodeType === 3 /* TEXT */) continue;
if (child.nodeType !== 1 /* ELEMENT */) {
child.remove();
continue;
}
const el = child as Element;
const tag = el.tagName.toLowerCase();
if (!ALLOWED_TAGS.has(tag)) {
// Unwrap: keep text content, discard the tag
while (el.firstChild) node.insertBefore(el.firstChild, el);
el.remove();
continue;
}
// Strip disallowed attributes
const tagAllowed = ALLOWED_ATTRS[tag];
const globalAllowed = ALLOWED_ATTRS['*'];
for (const attr of Array.from(el.attributes)) {
const name = attr.name.toLowerCase();
if (!tagAllowed?.has(name) && !globalAllowed?.has(name)) {
el.removeAttribute(attr.name);
}
}
// Block dangerous URL schemes on href/src
for (const urlAttr of ['href', 'src']) {
const val = el.getAttribute(urlAttr);
if (val && DANGEROUS_URL.test(val)) el.removeAttribute(urlAttr);
}
sanitizeNode(el, doc);
}
}
function sanitizeHtml(html: string): string {
if (typeof document === 'undefined') return html;
const doc = new DOMParser().parseFromString(html, 'text/html');
sanitizeNode(doc.body, doc);
return doc.body.innerHTML;
}
function TagList({
tags,
maxVisible = 2,
size = 'default',
}: {
tags: string[];
maxVisible?: number;
size?: 'small' | 'default';
}) {
const visibleTags = tags.slice(0, maxVisible);
const remainingTags = tags.slice(maxVisible);
const hasMore = remainingTags.length > 0;
const tagClass =
size === 'small'
? 'rounded-full bg-muted px-2.5 py-0.5 text-xs font-medium'
: 'rounded-full bg-muted px-3 py-1 text-xs font-medium';
return (
<>
{visibleTags.map((tag) => (
<span key={tag} className={tagClass}>
{tag}
</span>
))}
{hasMore && (
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<span className={`${tagClass} cursor-default`}>+{remainingTags.length}</span>
</TooltipTrigger>
<TooltipContent>
<p>{remainingTags.join(', ')}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
</>
);
}
/**
* ═══════════════════════════════════════════════════════════════════════════
* PostDetailProps
* ═══════════════════════════════════════════════════════════════════════════
*
* Props for the PostDetail component, a full post detail view with Medium-style typography.
*/
export interface PostDetailProps {
data?: {
/** The main blog post to display. */
post?: Post;
/** HTML content of the post body. */
content?: string;
/** Related posts to show at the bottom of the article. */
relatedPosts?: Post[];
};
actions?: {
/** Called when the back button is clicked. */
onBack?: () => void;
/** Called when the read more button is clicked (inline mode). */
onReadMore?: () => void;
/** Called when a related post is clicked. */
onReadRelated?: (post: Post) => void;
};
appearance?: {
/**
* Whether to show the cover image.
* @default true
*/
showCover?: boolean;
/**
* Whether to show author information.
* @default true
*/
showAuthor?: boolean;
/**
* Display mode for the component.
* - inline: Compact card view with truncated content
* - pip: Picture-in-picture view with truncated content
* - fullscreen: Full article view with complete content
* @default "fullscreen"
*/
displayMode?: 'inline' | 'pip' | 'fullscreen';
};
}
/**
* A full post detail component with Medium-style typography.
* Supports inline preview and fullscreen reading modes.
*
* Features:
* - Medium-style typography and spacing
* - Cover image display
* - Author info with avatar
* - Tag list with overflow tooltip
* - Related posts section
* - Inline (truncated) and fullscreen modes
* - MCP Apps display mode integration
*
* @component
* @example
* ```tsx
* <PostDetail
* data={{
* post: {
* id: "1",
* title: "Getting Started",
* excerpt: "Learn the basics...",
* coverImage: "https://example.com/cover.jpg",
* author: { name: "Sarah Chen", avatar: "https://example.com/avatar.jpg" },
* publishedAt: "2024-01-15",
* readTime: "5 min read",
* tags: ["Tutorial", "Components"],
* category: "Tutorial"
* },
* content: "<p>Full post content here...</p>",
* relatedPosts: [...]
* }}
* actions={{
* onReadMore: () => console.log("Expand to fullscreen"),
* onReadRelated: (post) => console.log("Read related:", post.title)
* }}
* appearance={{
* showCover: true,
* showAuthor: true,
* displayMode: "fullscreen"
* }}
* />
* ```
*/
export function PostDetail({ data, actions, appearance }: PostDetailProps) {
const resolved: NonNullable<PostDetailProps['data']> = data ?? demoPostDetailData;
const post = resolved.post;
const rawContent = resolved.content;
const content = useMemo(() => rawContent ? sanitizeHtml(rawContent) : undefined, [rawContent]);
const relatedPosts = resolved.relatedPosts ?? [];
const onReadMore = actions?.onReadMore;
const showCover = appearance?.showCover ?? true;
const showAuthor = appearance?.showAuthor ?? true;
const displayMode = appearance?.displayMode ?? 'inline';
const handleReadMore = () => {
onReadMore?.();
};
const formatDate = (dateStr: string) => {
return new Date(dateStr).toLocaleDateString('en-US', {
month: 'long',
day: 'numeric',
year: 'numeric',
});
};
// Inline mode - card view with truncated content
if (displayMode === 'inline') {
return (
<div className="flex flex-col sm:flex-row gap-4 rounded-lg border bg-card p-3">
{showCover && post?.coverImage && (
<div className="aspect-video sm:aspect-square sm:h-24 sm:w-24 shrink-0 overflow-hidden rounded-md">
<img
src={post.coverImage}
alt={post?.title || ''}
className="h-full w-full object-cover"
/>
</div>
)}
<div className="flex flex-1 flex-col justify-between min-w-0">
<div>
<div className="flex items-start justify-between gap-2">
<div className="flex-1 min-w-0">
{post?.category && (
<p className="mb-0.5 text-[10px] font-medium uppercase tracking-wide text-muted-foreground">
{post.category}
</p>
)}
{post?.title && (
<h1 className="line-clamp-2 text-sm font-bold leading-tight">{post.title}</h1>
)}
</div>
<button
onClick={handleReadMore}
className="shrink-0 p-1 rounded-md hover:bg-muted transition-colors text-muted-foreground hover:text-foreground cursor-pointer"
aria-label="Expand to fullscreen"
>
<Maximize2 className="h-4 w-4" />
</button>
</div>
{post?.excerpt && (
<p className="mt-1 line-clamp-2 text-xs text-muted-foreground">{post.excerpt}</p>
)}
{post?.tags && post.tags.length > 0 && (
<div className="mt-1.5 flex flex-wrap gap-1">
<TagList tags={post.tags} maxVisible={2} size="small" />
</div>
)}
</div>
<div className="mt-3 flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
<div className="flex items-center gap-2 text-xs text-muted-foreground">
{showAuthor && post?.author?.avatar && (
<img
src={post.author.avatar}
alt={post?.author?.name || ''}
className="h-4 w-4 rounded-full"
/>
)}
{showAuthor && post?.author?.name && <span>{post.author.name}</span>}
{post?.publishedAt && (
<span className="flex items-center gap-1">
<Calendar className="h-3 w-3" />
{formatDate(post.publishedAt)}
</span>
)}
{post?.readTime && (
<span className="flex items-center gap-1">
<Clock className="h-3 w-3" />
{post.readTime}
</span>
)}
</div>
<Button size="sm" onClick={handleReadMore}>
Read
</Button>
</div>
</div>
</div>
);
}
// PiP mode - horizontal layout with image on left, similar to post-card horizontal
if (displayMode === 'pip') {
return (
<div className="flex flex-col sm:flex-row gap-4 rounded-lg border bg-card p-3">
{showCover && post?.coverImage && (
<div className="aspect-video sm:aspect-square sm:h-24 sm:w-24 shrink-0 overflow-hidden rounded-md">
<img
src={post.coverImage}
alt={post?.title || ''}
className="h-full w-full object-cover"
/>
</div>
)}
<div className="flex flex-1 flex-col justify-between min-w-0">
<div>
{post?.category && (
<p className="mb-0.5 text-[10px] font-medium uppercase tracking-wide text-muted-foreground">
{post.category}
</p>
)}
{post?.title && (
<h1 className="line-clamp-2 text-sm font-bold leading-tight">{post.title}</h1>
)}
{post?.excerpt && (
<p className="mt-1 line-clamp-2 text-xs text-muted-foreground">{post.excerpt}</p>
)}
{post?.tags && post.tags.length > 0 && (
<div className="mt-1.5 flex flex-wrap gap-1">
<TagList tags={post.tags} maxVisible={2} size="small" />
</div>
)}
</div>
<div className="mt-3 flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
<div className="flex items-center gap-2 text-xs text-muted-foreground">
{showAuthor && post?.author?.avatar && (
<img
src={post.author.avatar}
alt={post?.author?.name || ''}
className="h-4 w-4 rounded-full"
/>
)}
{showAuthor && post?.author?.name && <span>{post.author.name}</span>}
{post?.publishedAt && (
<span className="flex items-center gap-1">
<Calendar className="h-3 w-3" />
{formatDate(post.publishedAt)}
</span>
)}
{post?.readTime && (
<span className="flex items-center gap-1">
<Clock className="h-3 w-3" />
{post.readTime}
</span>
)}
</div>
<Button size="sm" onClick={handleReadMore}>
Read
</Button>
</div>
</div>
</div>
);
}
// Fullscreen mode
return (
<div className="min-h-screen fs-mode bg-background">
<article className="mx-auto w-full max-w-[680px] px-6 py-10">
{showCover && post?.coverImage && (
<div className="aspect-video w-full overflow-hidden rounded-lg mb-8">
<img
src={post.coverImage}
alt={post?.title || ''}
className="h-full w-full object-cover"
/>
</div>
)}
{post?.category && (
<p className="mb-2 text-[10px] font-medium uppercase tracking-wide text-muted-foreground">
{post.category}
</p>
)}
{post?.title && (
<h1 className="text-[32px] font-bold leading-[1.25] tracking-tight md:text-[42px]">
{post.title}
</h1>
)}
{post?.tags && post.tags.length > 0 && (
<div className="mt-4 flex flex-wrap items-center gap-2">
<TagList tags={post.tags} maxVisible={2} size="default" />
</div>
)}
{showAuthor && post?.author && (
<div className="mt-8 flex items-center gap-4 border-b pb-8">
{post.author.avatar && (
<img
src={post.author.avatar}
alt={post.author.name || ''}
className="h-12 w-12 rounded-full"
/>
)}
<div>
{post.author.name && <p className="font-medium">{post.author.name}</p>}
{(post?.publishedAt || post?.readTime) && (
<div className="flex items-center gap-3 text-sm text-muted-foreground">
{post?.publishedAt && (
<span className="flex items-center gap-1">
<Calendar className="h-3.5 w-3.5" />
{formatDate(post.publishedAt)}
</span>
)}
{post?.readTime && (
<span className="flex items-center gap-1">
<Clock className="h-3.5 w-3.5" />
{post.readTime}
</span>
)}
</div>
)}
</div>
</div>
)}
{/* Medium-style content */}
<div className="mt-10">
{post?.excerpt && (
<p className="text-[21px] leading-[1.8] text-muted-foreground mb-8">{post.excerpt}</p>
)}
{content && (
<div
className="
text-[21px] leading-[1.8] tracking-[-0.003em]
[&>p]:mb-8
[&>h2]:text-[26px] [&>h2]:font-bold [&>h2]:mt-12 [&>h2]:mb-4 [&>h2]:leading-[1.3]
[&>h3]:text-[22px] [&>h3]:font-bold [&>h3]:mt-10 [&>h3]:mb-3 [&>h3]:leading-[1.3]
[&>ul]:mb-8 [&>ul]:pl-6 [&>ul>li]:mb-2
[&>ol]:mb-8 [&>ol]:pl-6 [&>ol>li]:mb-2
[&>blockquote]:border-l-4 [&>blockquote]:border-foreground [&>blockquote]:pl-6 [&>blockquote]:my-8 [&>blockquote]:italic
"
dangerouslySetInnerHTML={{ __html: content }}
/>
)}
</div>
{relatedPosts && relatedPosts.length > 0 && (
<div className="mt-16 border-t pt-10">
<h3 className="mb-6 text-lg font-semibold">Related Posts</h3>
<div className="space-y-4">
{relatedPosts.map((related) => (
<a
key={related.title || related.url}
href={related.url || '#'}
target="_blank"
rel="noopener noreferrer"
className="flex w-full items-center gap-4 rounded-lg p-3 text-left transition-colors hover:bg-muted cursor-pointer"
>
{related.coverImage && (
<div className="h-16 w-16 shrink-0 overflow-hidden rounded-lg">
<img
src={related.coverImage}
alt={related.title || ''}
className="h-full w-full object-cover"
/>
</div>
)}
<div className="min-w-0 flex-1">
{related.title && <p className="font-medium">{related.title}</p>}
{related.excerpt && (
<p className="mt-1 line-clamp-1 text-sm text-muted-foreground">
{related.excerpt}
</p>
)}
{related.readTime && (
<p className="mt-1 text-xs text-muted-foreground">{related.readTime}</p>
)}
</div>
<ExternalLink className="h-4 w-4 shrink-0 text-muted-foreground" />
</a>
))}
</div>
</div>
)}
</article>
</div>
);
}
registry/blogging/demo/blogging.ts
// Demo data for Blogging category components
// This file contains sample data used for component previews and documentation
import type { Post } from '../types';
// Single post for PostCard default
export const demoPost: Post = {
title: 'Getting Started with Agentic UI Components',
excerpt:
'Learn how to build conversational interfaces with our comprehensive component library designed for AI-powered applications.',
coverImage: 'https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?w=800',
author: {
name: 'Sarah Chen',
avatar: 'https://i.pravatar.cc/150?u=sarah',
},
publishedAt: '2024-01-15',
readTime: '5 min read',
tags: ['Tutorial', 'Components'],
category: 'Tutorial',
};
// Demo content for PostDetail (HTML content for full article view)
export const demoPostContent = `
<p>Building modern AI-powered applications requires a new approach to UI design. Traditional web components don't always translate well to conversational interfaces, where context and flow are paramount.</p>
<p>Our Agentic UI component library provides a collection of purpose-built components that work seamlessly within chat interfaces. From payment flows to product displays, each component is designed with the unique constraints of conversational UIs in mind.</p>
<h2>Key Features</h2>
<p>Each component supports three display modes: inline (within the chat flow), fullscreen (for complex interactions), and picture-in-picture (persistent visibility). This flexibility allows you to create rich, interactive experiences without breaking the conversational flow.</p>
<p>Components are designed mobile-first and touch-friendly, ensuring a great experience across all devices. They automatically adapt to light and dark themes, and integrate seamlessly with MCP tools for backend communication.</p>
`;
// Related posts for PostDetail
export const demoRelatedPosts: Post[] = [
{
title: 'Designing for Conversational Interfaces',
excerpt:
'Best practices for creating intuitive UI components that work within chat environments.',
coverImage: 'https://images.unsplash.com/photo-1441974231531-c6227db76b6e?w=800',
author: { name: 'Alex Rivera', avatar: 'https://i.pravatar.cc/150?u=alex' },
publishedAt: '2024-01-12',
readTime: '8 min read',
tags: ['Design', 'UX'],
category: 'Design',
url: 'https://example.com/posts/designing-conversational-interfaces',
},
{
title: 'MCP Integration Patterns',
excerpt: 'How to leverage Model Context Protocol for seamless backend communication.',
coverImage: 'https://images.unsplash.com/photo-1469474968028-56623f02e42e?w=800',
author: { name: 'Jordan Kim', avatar: 'https://i.pravatar.cc/150?u=jordan' },
publishedAt: '2024-01-10',
readTime: '12 min read',
tags: ['MCP', 'Backend'],
category: 'Development',
url: 'https://example.com/posts/mcp-integration-patterns',
},
];
// Full PostDetail demo data (combines post, content, and relatedPosts)
export const demoPostDetailData = {
post: {
...demoPost,
tags: ['Tutorial', 'Components', 'AI', 'React', 'TypeScript'],
},
content: demoPostContent,
relatedPosts: demoRelatedPosts,
};
// 15 posts for PostList default
export const demoPosts: Post[] = [
{
title: 'Getting Started with Agentic UI Components',
excerpt:
'Learn how to build conversational interfaces with our comprehensive component library designed for AI-powered applications.',
coverImage: 'https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?w=800',
author: {
name: 'Sarah Chen',
avatar: 'https://i.pravatar.cc/150?u=sarah',
},
publishedAt: '2024-01-15',
readTime: '5 min read',
tags: ['Tutorial', 'Components', 'AI'],
category: 'Tutorial',
},
{
title: 'Designing for Conversational Interfaces with Manifest UI',
excerpt:
'Best practices for creating intuitive UI components that work within chat environments.',
coverImage: 'https://images.unsplash.com/photo-1441974231531-c6227db76b6e?w=800',
author: {
name: 'Alex Rivera',
avatar: 'https://i.pravatar.cc/150?u=alex',
},
publishedAt: '2024-01-12',
readTime: '8 min read',
tags: ['Design', 'UX'],
category: 'Design',
},
{
title: 'MCP Integration Patterns',
excerpt:
'How to leverage Model Context Protocol for seamless backend communication in your agentic applications.',
coverImage: 'https://images.unsplash.com/photo-1469474968028-56623f02e42e?w=800',
author: {
name: 'Jordan Kim',
avatar: 'https://i.pravatar.cc/150?u=jordan',
},
publishedAt: '2024-01-10',
readTime: '12 min read',
tags: ['MCP', 'Backend', 'Integration'],
category: 'Development',
},
{
title: 'Building Payment Flows in Chat',
excerpt:
'A complete guide to implementing secure, user-friendly payment experiences within conversational interfaces.',
coverImage: 'https://images.unsplash.com/photo-1472214103451-9374bd1c798e?w=800',
author: {
name: 'Morgan Lee',
avatar: 'https://i.pravatar.cc/150?u=morgan',
},
publishedAt: '2024-01-08',
readTime: '10 min read',
tags: ['Payments', 'Security'],
category: 'Tutorial',
},
{
title: 'Real-time Collaboration in AI Apps',
excerpt:
'Implementing WebSocket connections and real-time updates for collaborative agentic experiences.',
coverImage: 'https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=800',
author: {
name: 'Casey Taylor',
avatar: 'https://i.pravatar.cc/150?u=casey',
},
publishedAt: '2024-01-06',
readTime: '15 min read',
tags: ['WebSocket', 'Real-time', 'Collaboration'],
category: 'Development',
},
{
title: 'Accessibility in Chat Interfaces',
excerpt:
'Making your conversational UI accessible to all users with screen readers and keyboard navigation.',
coverImage: 'https://images.unsplash.com/photo-1507525428034-b723cf961d3e?w=800',
author: {
name: 'Jamie Park',
avatar: 'https://i.pravatar.cc/150?u=jamie',
},
publishedAt: '2024-01-04',
readTime: '9 min read',
tags: ['Accessibility', 'A11y', 'UX'],
category: 'Design',
},
{
title: 'State Management for Complex Workflows',
excerpt:
'Managing complex multi-step workflows in agentic applications using modern state patterns.',
coverImage: 'https://images.unsplash.com/photo-1501785888041-af3ef285b470?w=800',
author: {
name: 'Drew Martinez',
avatar: 'https://i.pravatar.cc/150?u=drew',
},
publishedAt: '2024-01-02',
readTime: '11 min read',
tags: ['State', 'Workflow', 'Architecture'],
category: 'Development',
},
{
title: 'Testing Conversational Components',
excerpt: 'Strategies for unit testing and integration testing of chat-based UI components.',
coverImage: 'https://images.unsplash.com/photo-1433086966358-54859d0ed716?w=800',
author: {
name: 'Riley Johnson',
avatar: 'https://i.pravatar.cc/150?u=riley',
},
publishedAt: '2023-12-30',
readTime: '8 min read',
tags: ['Testing', 'Quality', 'CI/CD'],
category: 'Development',
},
{
title: 'Theming and Dark Mode Support',
excerpt: 'Implementing flexible theming systems with dark mode for agentic UI components.',
coverImage: 'https://images.unsplash.com/photo-1475924156734-496f6cac6ec1?w=800',
author: {
name: 'Avery Williams',
avatar: 'https://i.pravatar.cc/150?u=avery',
},
publishedAt: '2023-12-28',
readTime: '7 min read',
tags: ['Theming', 'Dark Mode', 'CSS'],
category: 'Design',
},
{
title: 'Performance Optimization Techniques',
excerpt: 'Optimizing render performance and reducing bundle size in chat applications.',
coverImage: 'https://images.unsplash.com/photo-1518173946687-a2e8a36af77a?w=800',
author: {
name: 'Quinn Anderson',
avatar: 'https://i.pravatar.cc/150?u=quinn',
},
publishedAt: '2023-12-25',
readTime: '13 min read',
tags: ['Performance', 'Optimization', 'React'],
category: 'Development',
},
{
title: 'Error Handling and Recovery',
excerpt:
'Graceful error handling patterns and user-friendly recovery flows in conversational UIs.',
coverImage: 'https://images.unsplash.com/photo-1509316975850-ff9c5deb0cd9?w=800',
author: {
name: 'Sage Thompson',
avatar: 'https://i.pravatar.cc/150?u=sage',
},
publishedAt: '2023-12-22',
readTime: '10 min read',
tags: ['Error Handling', 'UX', 'Resilience'],
category: 'Development',
},
{
title: 'Internationalization Best Practices',
excerpt: 'Making your agentic UI components work across languages and locales.',
coverImage: 'https://images.unsplash.com/photo-1426604966848-d7adac402bff?w=800',
author: {
name: 'Blake Garcia',
avatar: 'https://i.pravatar.cc/150?u=blake',
},
publishedAt: '2023-12-20',
readTime: '9 min read',
tags: ['i18n', 'Localization', 'Global'],
category: 'Design',
},
{
title: 'Mobile-First Chat Design',
excerpt: 'Designing conversational interfaces that work beautifully on mobile devices.',
coverImage: 'https://images.unsplash.com/photo-1447752875215-b2761acb3c5d?w=800',
author: {
name: 'Charlie Brown',
avatar: 'https://i.pravatar.cc/150?u=charlie',
},
publishedAt: '2023-12-18',
readTime: '8 min read',
tags: ['Mobile', 'Responsive', 'Design'],
category: 'Design',
},
{
title: 'Analytics and User Insights',
excerpt: 'Tracking user interactions and deriving insights from conversational UI usage.',
coverImage: 'https://images.unsplash.com/photo-1500534314209-a25ddb2bd429?w=800',
author: {
name: 'Sydney Chen',
avatar: 'https://i.pravatar.cc/150?u=sydney',
},
publishedAt: '2023-12-15',
readTime: '11 min read',
tags: ['Analytics', 'Insights', 'Data'],
category: 'Tutorial',
},
{
title: 'Building Reusable Component Libraries',
excerpt: 'Creating a scalable component library for agentic UIs that teams can share.',
coverImage: 'https://images.unsplash.com/photo-1465056836900-8f1e940f2114?w=800',
author: {
name: 'Taylor Swift',
avatar: 'https://i.pravatar.cc/150?u=taylor',
},
publishedAt: '2023-12-12',
readTime: '14 min read',
tags: ['Components', 'Library', 'Scalability'],
category: 'Development',
},
];