canvas-ui-sdk 0.3.13 → 0.3.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,7 +6,7 @@
6
6
  {
7
7
  "path": "components/blocks/social-feed.tsx",
8
8
  "type": "registry:block",
9
- "content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport { Avatar, AvatarImage, AvatarFallback } from \"../ui/avatar\";\nimport { Button } from \"../ui/button\";\nimport { \n Heart, \n MessageCircle, \n RefreshCw, \n Send, \n Paperclip, \n Video, \n Link2, \n MoreHorizontal,\n Play\n} from \"lucide-react\";\n\n// ============================================\n// Types\n// ============================================\n\nexport interface PostAuthor {\n id: string;\n name: string;\n avatarUrl?: string;\n}\n\nexport interface LinkPreview {\n url: string;\n domain: string;\n title: string;\n description?: string;\n imageUrl?: string;\n}\n\nexport interface VideoMedia {\n thumbnailUrl: string;\n videoUrl?: string;\n}\n\nexport interface RepostContent {\n author: PostAuthor;\n date: string;\n content: string;\n images?: string[];\n}\n\nexport interface SocialFeedPost {\n id: string;\n author: PostAuthor;\n date: string;\n content: string;\n /** Image URLs for the post */\n images?: string[];\n /** Video media */\n video?: VideoMedia;\n /** Link preview card */\n linkPreview?: LinkPreview;\n /** Reposted/quoted content */\n repost?: RepostContent;\n likesCount: number;\n repliesCount: number;\n isLiked?: boolean;\n /** Nested replies */\n replies?: SocialFeedPost[];\n /** Whether this is a reply (for indentation) */\n isReply?: boolean;\n}\n\nexport interface SocialFeedProps {\n /** Section title */\n title?: string;\n /** Posts data */\n posts?: SocialFeedPost[];\n /** Current user for composer */\n currentUser?: PostAuthor;\n /** Placeholder text for composer */\n composerPlaceholder?: string;\n /** Image preview in composer */\n composerImagePreview?: string;\n /** Callback when post is submitted */\n onPost?: (content: string) => void;\n /** Callback when like is clicked */\n onLike?: (postId: string) => void;\n /** Callback when comment is clicked */\n onComment?: (postId: string) => void;\n /** Callback when repost is clicked */\n onRepost?: (postId: string) => void;\n /** Callback when share is clicked */\n onShare?: (postId: string) => void;\n /** Callback when menu is clicked */\n onMenuClick?: (postId: string) => void;\n /** Additional class names */\n className?: string;\n}\n\n// ============================================\n// Default Data\n// ============================================\n\nconst defaultCurrentUser: PostAuthor = {\n id: \"current\",\n name: \"You\",\n avatarUrl: \"https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=150&h=150&fit=crop&crop=face\",\n};\n\nconst defaultPosts: SocialFeedPost[] = [\n {\n id: \"1\",\n author: {\n id: \"raj\",\n name: \"Raj Mishra\",\n avatarUrl: \"https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=150&h=150&fit=crop&crop=face\",\n },\n date: \"Feb 23, 1:32 PM\",\n content: \"Thinking about traveling to Paris again!\",\n repost: {\n author: {\n id: \"jeffrey\",\n name: \"Jeffrey Connor\",\n avatarUrl: \"https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=150&h=150&fit=crop&crop=face\",\n },\n date: \"Nov 23, 5:34 PM\",\n content: \"What a place, the history, architecture and culture is wonderful. So many sites to see, one more amazing then the next. A must see if you are going to visit the great cities of the world.\",\n images: [\n \"https://images.unsplash.com/photo-1502602898657-3e91760cbb34?w=320&h=320&fit=crop\",\n \"https://images.unsplash.com/photo-1499856871958-5b9627545d1a?w=320&h=320&fit=crop\",\n ],\n },\n likesCount: 30,\n repliesCount: 10,\n isLiked: false,\n },\n {\n id: \"2\",\n author: {\n id: \"mary\",\n name: \"Mary Trott\",\n avatarUrl: \"https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=150&h=150&fit=crop&crop=face\",\n },\n date: \"Feb 23, 1:32 PM\",\n content: \"Learning how to Bubble\",\n video: {\n thumbnailUrl: \"https://images.unsplash.com/photo-1461749280684-dccba630e2f6?w=480&h=380&fit=crop\",\n },\n likesCount: 30,\n repliesCount: 10,\n isLiked: false,\n replies: [\n {\n id: \"2-reply-1\",\n author: {\n id: \"aya\",\n name: \"Aya Williams\",\n avatarUrl: \"https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=150&h=150&fit=crop&crop=face\",\n },\n date: \"Mar 12, 11:23 AM\",\n content: \"Check out these flight deals to Paris!\",\n linkPreview: {\n url: \"https://expedia.com/flights/paris\",\n domain: \"expedia.com\",\n title: \"Paris flights\",\n description: \"Your one-stop travel site for your dream vacation. Bundle your stay...\",\n imageUrl: \"https://images.unsplash.com/photo-1502602898657-3e91760cbb34?w=200&h=200&fit=crop\",\n },\n likesCount: 30,\n repliesCount: 10,\n isLiked: false,\n isReply: true,\n },\n ],\n },\n];\n\n// ============================================\n// Sub-components\n// ============================================\n\ninterface PostComposerProps {\n placeholder?: string;\n imagePreview?: string;\n onPost?: (content: string) => void;\n}\n\nfunction PostComposer({ placeholder = \"What's on your mind?\", imagePreview, onPost }: PostComposerProps) {\n const [content, setContent] = useState(\"\");\n\n const handlePost = () => {\n if (content.trim() || imagePreview) {\n onPost?.(content);\n setContent(\"\");\n }\n };\n\n return (\n <div\n className=\"flex flex-col w-full overflow-hidden\"\n style={{\n border: \"1px solid var(--canvas-border)\",\n boxShadow: \"0px 1px 8px 0px rgba(0,0,0,0.03)\",\n }}\n >\n {/* Text input area */}\n <div\n className=\"w-full\"\n style={{\n padding: \"var(--spacing-xl)\",\n background: \"var(--canvas-background)\",\n boxShadow: \"0px 1px 2px 0px rgba(0,0,0,0.02)\",\n }}\n >\n <textarea\n value={content}\n onChange={(e) => setContent(e.target.value)}\n placeholder={placeholder}\n className=\"w-full resize-none border-0 bg-transparent outline-none\"\n rows={2}\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: content ? \"var(--canvas-text)\" : \"var(--canvas-text-placeholder)\",\n }}\n />\n </div>\n\n {/* Image preview */}\n {imagePreview && (\n <div\n className=\"w-full\"\n style={{\n padding: \"0 var(--spacing-xl)\",\n background: \"var(--canvas-background)\",\n }}\n >\n <div\n className=\"overflow-hidden\"\n style={{\n width: 240,\n height: 180,\n borderRadius: \"var(--radius-md)\",\n border: \"1px solid var(--canvas-border)\",\n }}\n >\n <img\n src={imagePreview}\n alt=\"Preview\"\n className=\"w-full h-full object-cover\"\n />\n </div>\n </div>\n )}\n\n {/* Action bar */}\n <div\n className=\"flex items-center justify-between w-full\"\n style={{\n padding: \"var(--spacing-xl)\",\n background: \"var(--canvas-background)\",\n borderTop: \"1px solid var(--canvas-border)\",\n }}\n >\n <div className=\"flex items-center\" style={{ gap: \"var(--spacing-lg)\" }}>\n <button type=\"button\" className=\"cursor-pointer\" style={{ color: \"var(--canvas-text)\" }}>\n <Paperclip className=\"w-5 h-5\" />\n </button>\n <button type=\"button\" className=\"cursor-pointer\" style={{ color: \"var(--canvas-text)\" }}>\n <Video className=\"w-5 h-5\" />\n </button>\n <button type=\"button\" className=\"cursor-pointer\" style={{ color: \"var(--canvas-text)\" }}>\n <Link2 className=\"w-5 h-5\" />\n </button>\n </div>\n <Button variant=\"primary\" size=\"sm\" onClick={handlePost}>\n Post\n </Button>\n </div>\n </div>\n );\n}\n\ninterface ActionIconsRowProps {\n isLiked?: boolean;\n onLike?: () => void;\n onComment?: () => void;\n onRepost?: () => void;\n onShare?: () => void;\n}\n\nfunction ActionIconsRow({ isLiked, onLike, onComment, onRepost, onShare }: ActionIconsRowProps) {\n return (\n <div\n className=\"flex items-center\"\n style={{ gap: \"var(--spacing-lg)\", padding: \"var(--spacing-xxs) 0\" }}\n >\n <button\n type=\"button\"\n onClick={onLike}\n className=\"cursor-pointer\"\n style={{ color: isLiked ? \"var(--canvas-destructive)\" : \"var(--canvas-text)\" }}\n >\n <Heart\n className=\"w-5 h-5\"\n style={{\n fill: isLiked ? \"var(--canvas-destructive)\" : \"transparent\",\n }}\n />\n </button>\n <button type=\"button\" onClick={onComment} className=\"cursor-pointer\" style={{ color: \"var(--canvas-text)\" }}>\n <MessageCircle className=\"w-5 h-5\" />\n </button>\n <button type=\"button\" onClick={onRepost} className=\"cursor-pointer\" style={{ color: \"var(--canvas-text)\" }}>\n <RefreshCw className=\"w-5 h-5\" />\n </button>\n <button type=\"button\" onClick={onShare} className=\"cursor-pointer\" style={{ color: \"var(--canvas-text)\" }}>\n <Send className=\"w-5 h-5\" />\n </button>\n </div>\n );\n}\n\ninterface StatsRowProps {\n likesCount: number;\n repliesCount: number;\n}\n\nfunction StatsRow({ likesCount, repliesCount }: StatsRowProps) {\n return (\n <div\n className=\"flex items-center\"\n style={{ gap: \"var(--spacing-xl)\", paddingTop: \"var(--spacing-xs)\" }}\n >\n <span\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 500,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text-placeholder)\",\n }}\n >\n {likesCount} likes\n </span>\n <span\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 500,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text-placeholder)\",\n }}\n >\n {repliesCount} replies\n </span>\n </div>\n );\n}\n\ninterface VideoThumbnailProps {\n thumbnailUrl: string;\n onClick?: () => void;\n}\n\nfunction VideoThumbnail({ thumbnailUrl, onClick }: VideoThumbnailProps) {\n return (\n <div\n className=\"relative overflow-hidden cursor-pointer\"\n style={{\n width: 480,\n height: 380,\n maxWidth: \"100%\",\n borderRadius: \"var(--radius-3xl)\",\n border: \"1px solid var(--canvas-border)\",\n }}\n onClick={onClick}\n >\n <img\n src={thumbnailUrl}\n alt=\"Video thumbnail\"\n className=\"w-full h-full object-cover\"\n />\n {/* Play button */}\n <div\n className=\"absolute inset-0 flex items-center justify-center\"\n >\n <div\n className=\"flex items-center justify-center\"\n style={{\n width: 128,\n height: 80,\n background: \"var(--canvas-destructive)\",\n borderRadius: \"var(--radius-2xl)\",\n }}\n >\n <Play className=\"w-12 h-12 text-white fill-white\" />\n </div>\n </div>\n </div>\n );\n}\n\ninterface LinkPreviewCardProps {\n linkPreview: LinkPreview;\n onClick?: () => void;\n}\n\nfunction LinkPreviewCard({ linkPreview, onClick }: LinkPreviewCardProps) {\n return (\n <div\n className=\"flex overflow-hidden cursor-pointer\"\n style={{\n width: 580,\n maxWidth: \"100%\",\n background: \"var(--canvas-background)\",\n border: \"1px solid var(--canvas-border)\",\n borderRadius: \"var(--radius-2xl)\",\n boxShadow: \"0px 1px 8px 0px rgba(0,0,0,0.03)\",\n }}\n onClick={onClick}\n >\n {linkPreview.imageUrl && (\n <div\n className=\"shrink-0 self-stretch overflow-hidden\"\n style={{\n width: 200,\n borderRight: \"1px solid var(--canvas-border)\",\n borderTopLeftRadius: \"var(--radius-md)\",\n borderBottomLeftRadius: \"var(--radius-md)\",\n }}\n >\n <img\n src={linkPreview.imageUrl}\n alt={linkPreview.title}\n className=\"w-full h-full object-cover\"\n />\n </div>\n )}\n <div\n className=\"flex flex-col flex-1\"\n style={{ padding: \"var(--spacing-4xl)\", gap: \"var(--spacing-lg)\" }}\n >\n <div className=\"flex flex-col\" style={{ gap: 0 }}>\n <span\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text-placeholder)\",\n }}\n >\n {linkPreview.domain}\n </span>\n <span\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 600,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n {linkPreview.title}\n </span>\n </div>\n {linkPreview.description && (\n <p\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text)\",\n margin: 0,\n }}\n >\n {linkPreview.description}\n </p>\n )}\n </div>\n </div>\n );\n}\n\ninterface RepostCardProps {\n repost: RepostContent;\n onLike?: () => void;\n onComment?: () => void;\n onRepost?: () => void;\n onShare?: () => void;\n}\n\nfunction RepostCard({ repost, onLike, onComment, onRepost, onShare }: RepostCardProps) {\n return (\n <div\n className=\"flex flex-col w-full\"\n style={{\n padding: \"var(--spacing-4xl)\",\n background: \"var(--canvas-background)\",\n border: \"1px solid var(--canvas-border)\",\n borderRadius: \"var(--radius-md)\",\n boxShadow: \"0px 1px 8px 0px rgba(0,0,0,0.03)\",\n gap: \"var(--spacing-xl)\",\n }}\n >\n {/* Author row */}\n <div className=\"flex items-start w-full\" style={{ gap: \"var(--spacing-xl)\" }}>\n <Avatar\n className=\"shrink-0\"\n style={{\n width: 48,\n height: 48,\n borderRadius: \"var(--spacing-3xl)\",\n border: \"1px solid var(--canvas-border)\",\n }}\n >\n <AvatarImage src={repost.author.avatarUrl} />\n <AvatarFallback>\n {repost.author.name.split(\" \").map(n => n[0]).join(\"\").slice(0, 2)}\n </AvatarFallback>\n </Avatar>\n <div className=\"flex flex-col flex-1\" style={{ gap: \"var(--spacing-lg)\" }}>\n <div className=\"flex items-center justify-between w-full\">\n <div className=\"flex flex-col\" style={{ gap: 0 }}>\n <span\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 600,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n {repost.author.name}\n </span>\n <span\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text-placeholder)\",\n }}\n >\n {repost.date}\n </span>\n </div>\n <button\n type=\"button\"\n className=\"cursor-pointer flex items-center justify-center\"\n style={{\n width: 32,\n height: 32,\n borderRadius: \"var(--spacing-6xl)\",\n color: \"var(--canvas-text-placeholder)\",\n }}\n >\n <MoreHorizontal className=\"w-5 h-5\" />\n </button>\n </div>\n {/* Content */}\n <p\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text)\",\n margin: 0,\n }}\n >\n {repost.content}\n </p>\n {/* Images */}\n {repost.images && repost.images.length > 0 && (\n <div className=\"flex\" style={{ gap: \"var(--spacing-xl)\" }}>\n {repost.images.map((img, idx) => (\n <div\n key={idx}\n className=\"overflow-hidden\"\n style={{\n width: 320,\n height: 320,\n borderRadius: \"var(--radius-2xl)\",\n border: \"1px solid var(--canvas-border)\",\n }}\n >\n <img src={img} alt=\"\" className=\"w-full h-full object-cover\" />\n </div>\n ))}\n </div>\n )}\n {/* Actions */}\n <ActionIconsRow\n isLiked={false}\n onLike={onLike}\n onComment={onComment}\n onRepost={onRepost}\n onShare={onShare}\n />\n <StatsRow likesCount={30} repliesCount={10} />\n </div>\n </div>\n </div>\n );\n}\n\ninterface PostCellProps {\n post: SocialFeedPost;\n onLike?: () => void;\n onComment?: () => void;\n onRepost?: () => void;\n onShare?: () => void;\n onMenuClick?: () => void;\n}\n\nfunction PostCell({ post, onLike, onComment, onRepost, onShare, onMenuClick }: PostCellProps) {\n return (\n <div\n className=\"flex w-full\"\n style={{\n paddingLeft: post.isReply ? \"var(--spacing-7xl)\" : 0,\n paddingTop: post.isReply ? \"var(--spacing-3xl)\" : \"var(--spacing-xl)\",\n paddingBottom: post.isReply ? 0 : \"var(--spacing-3xl)\",\n borderBottom: post.isReply ? \"none\" : \"1px solid var(--canvas-border)\",\n gap: \"var(--spacing-xl)\",\n }}\n >\n {/* Avatar column with reply line */}\n <div className=\"flex flex-col items-center shrink-0\" style={{ gap: \"var(--spacing-md)\" }}>\n <Avatar\n style={{\n width: 48,\n height: 48,\n borderRadius: \"var(--spacing-3xl)\",\n border: \"1px solid var(--canvas-border)\",\n }}\n >\n <AvatarImage src={post.author.avatarUrl} />\n <AvatarFallback>\n {post.author.name.split(\" \").map(n => n[0]).join(\"\").slice(0, 2)}\n </AvatarFallback>\n </Avatar>\n {/* Reply line */}\n {post.replies && post.replies.length > 0 && (\n <div\n className=\"flex-1 w-px\"\n style={{ background: \"var(--canvas-border)\", minHeight: 20 }}\n />\n )}\n </div>\n\n {/* Content column */}\n <div className=\"flex flex-col flex-1 min-w-0\" style={{ gap: \"var(--spacing-lg)\" }}>\n {/* Header */}\n <div className=\"flex items-center justify-between w-full\">\n <div className=\"flex flex-col\" style={{ gap: 0 }}>\n <span\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 600,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n {post.author.name}\n </span>\n <span\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text-placeholder)\",\n }}\n >\n {post.date}\n </span>\n </div>\n <button\n type=\"button\"\n onClick={onMenuClick}\n className=\"cursor-pointer flex items-center justify-center\"\n style={{\n width: 32,\n height: 32,\n borderRadius: \"var(--spacing-6xl)\",\n color: \"var(--canvas-text-placeholder)\",\n }}\n >\n <MoreHorizontal className=\"w-5 h-5\" />\n </button>\n </div>\n\n {/* Content text */}\n <p\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text)\",\n margin: 0,\n }}\n >\n {post.content}\n </p>\n\n {/* Repost card */}\n {post.repost && (\n <RepostCard\n repost={post.repost}\n onLike={onLike}\n onComment={onComment}\n onRepost={onRepost}\n onShare={onShare}\n />\n )}\n\n {/* Images */}\n {post.images && post.images.length > 0 && (\n <div className=\"flex\" style={{ gap: \"var(--spacing-xl)\" }}>\n {post.images.map((img, idx) => (\n <div\n key={idx}\n className=\"overflow-hidden\"\n style={{\n width: 320,\n height: 320,\n borderRadius: \"var(--radius-2xl)\",\n border: \"1px solid var(--canvas-border)\",\n }}\n >\n <img src={img} alt=\"\" className=\"w-full h-full object-cover\" />\n </div>\n ))}\n </div>\n )}\n\n {/* Video */}\n {post.video && (\n <VideoThumbnail thumbnailUrl={post.video.thumbnailUrl} />\n )}\n\n {/* Link preview */}\n {post.linkPreview && (\n <LinkPreviewCard linkPreview={post.linkPreview} />\n )}\n\n {/* Actions */}\n <ActionIconsRow\n isLiked={post.isLiked}\n onLike={onLike}\n onComment={onComment}\n onRepost={onRepost}\n onShare={onShare}\n />\n\n {/* Stats */}\n <StatsRow likesCount={post.likesCount} repliesCount={post.repliesCount} />\n\n {/* Nested replies */}\n {post.replies && post.replies.length > 0 && (\n <div className=\"flex flex-col w-full\">\n {post.replies.map((reply) => (\n <PostCell\n key={reply.id}\n post={reply}\n onLike={onLike}\n onComment={onComment}\n onRepost={onRepost}\n onShare={onShare}\n />\n ))}\n </div>\n )}\n </div>\n </div>\n );\n}\n\n// ============================================\n// Main Component\n// ============================================\n\n/**\n * Canvas Design System - Social Feed Block\n *\n * A social media-style feed component with post composer, posts with various\n * content types (text, images, video, reposts, link cards), social interactions,\n * and threaded replies.\n *\n * @example\n * ```tsx\n * <SocialFeed\n * title=\"Social Feed\"\n * posts={[...]}\n * onLike={(postId) => console.log(\"Liked\", postId)}\n * onPost={(content) => console.log(\"Posted\", content)}\n * />\n * ```\n */\nexport function SocialFeed({\n title = \"Social Feed\",\n posts = defaultPosts,\n currentUser = defaultCurrentUser,\n composerPlaceholder = \"What's on your mind?\",\n composerImagePreview,\n onPost,\n onLike,\n onComment,\n onRepost,\n onShare,\n onMenuClick,\n className,\n}: SocialFeedProps) {\n return (\n <div\n className={cn(\"flex flex-col w-full\", className)}\n style={{ gap: \"var(--spacing-xl)\" }}\n >\n {/* Header Section */}\n {title && (\n <div className=\"flex flex-wrap items-start w-full\" style={{ gap: \"var(--spacing-xl)\" }}>\n <div className=\"flex flex-col flex-1 min-w-[200px]\" style={{ gap: \"var(--spacing-xs)\" }}>\n <h2\n style={{\n fontFamily: \"var(--typo-h6-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-h6-size)\",\n fontWeight: \"var(--typo-h6-weight)\",\n letterSpacing: \"var(--typo-h6-spacing)\",\n lineHeight: \"var(--typo-h6-line-height)\",\n color: \"var(--canvas-text)\",\n margin: 0,\n }}\n >\n {title}\n </h2>\n </div>\n </div>\n )}\n\n {/* Feed content */}\n <div className=\"flex flex-col w-full overflow-hidden\">\n {/* First section: Composer + first set of posts */}\n <div\n className=\"flex flex-col w-full\"\n style={{\n borderBottom: \"1px solid var(--canvas-border)\",\n paddingBottom: \"var(--spacing-5xl)\",\n }}\n >\n {/* Post Composer */}\n <PostComposer\n placeholder={composerPlaceholder}\n imagePreview={composerImagePreview}\n onPost={onPost}\n />\n\n {/* Posts */}\n <div className=\"flex flex-col w-full\" style={{ paddingTop: \"var(--spacing-xl)\" }}>\n {posts.map((post) => (\n <PostCell\n key={post.id}\n post={post}\n onLike={() => onLike?.(post.id)}\n onComment={() => onComment?.(post.id)}\n onRepost={() => onRepost?.(post.id)}\n onShare={() => onShare?.(post.id)}\n onMenuClick={() => onMenuClick?.(post.id)}\n />\n ))}\n </div>\n </div>\n </div>\n </div>\n );\n}\n"
9
+ "content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport { Avatar, AvatarImage, AvatarFallback } from \"../ui/avatar\";\nimport { Button } from \"../ui/button\";\nimport { \n Heart, \n MessageCircle, \n RefreshCw, \n Send, \n Paperclip, \n Video, \n Link2, \n MoreHorizontal,\n Play\n} from \"lucide-react\";\n\n// ============================================\n// Types\n// ============================================\n\nexport interface PostAuthor {\n id: string;\n name: string;\n avatarUrl?: string;\n}\n\nexport interface LinkPreview {\n url: string;\n domain: string;\n title: string;\n description?: string;\n imageUrl?: string;\n}\n\nexport interface VideoMedia {\n thumbnailUrl: string;\n videoUrl?: string;\n}\n\nexport interface RepostContent {\n author: PostAuthor;\n date: string;\n content: string;\n images?: string[];\n}\n\nexport interface SocialFeedPost {\n id: string;\n author: PostAuthor;\n date: string;\n content: string;\n /** Image URLs for the post */\n images?: string[];\n /** Video media */\n video?: VideoMedia;\n /** Link preview card */\n linkPreview?: LinkPreview;\n /** Reposted/quoted content */\n repost?: RepostContent;\n likesCount: number;\n repliesCount: number;\n isLiked?: boolean;\n /** Nested replies */\n replies?: SocialFeedPost[];\n /** Whether this is a reply (for indentation) */\n isReply?: boolean;\n}\n\nexport interface SocialFeedProps {\n /** Section title */\n title?: string;\n /** Posts data */\n posts?: SocialFeedPost[];\n /** Current user for composer */\n currentUser?: PostAuthor;\n /** Placeholder text for composer */\n composerPlaceholder?: string;\n /** Image preview in composer */\n composerImagePreview?: string;\n /** Callback when post is submitted */\n onPost?: (content: string) => void;\n /** Callback when like is clicked */\n onLike?: (postId: string) => void;\n /** Callback when comment is clicked */\n onComment?: (postId: string) => void;\n /** Callback when repost is clicked */\n onRepost?: (postId: string) => void;\n /** Callback when share is clicked */\n onShare?: (postId: string) => void;\n /** Callback when menu is clicked */\n onMenuClick?: (postId: string) => void;\n /** Additional class names */\n className?: string;\n}\n\n// ============================================\n// Default Data\n// ============================================\n\nconst defaultCurrentUser: PostAuthor = {\n id: \"current\",\n name: \"You\",\n avatarUrl: \"https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=150&h=150&fit=crop&crop=face\",\n};\n\nconst defaultPosts: SocialFeedPost[] = [\n {\n id: \"1\",\n author: {\n id: \"raj\",\n name: \"Raj Mishra\",\n avatarUrl: \"https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=150&h=150&fit=crop&crop=face\",\n },\n date: \"Feb 23, 1:32 PM\",\n content: \"Thinking about traveling to Paris again!\",\n repost: {\n author: {\n id: \"jeffrey\",\n name: \"Jeffrey Connor\",\n avatarUrl: \"https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=150&h=150&fit=crop&crop=face\",\n },\n date: \"Nov 23, 5:34 PM\",\n content: \"What a place, the history, architecture and culture is wonderful. So many sites to see, one more amazing then the next. A must see if you are going to visit the great cities of the world.\",\n images: [\n \"https://images.unsplash.com/photo-1502602898657-3e91760cbb34?w=320&h=320&fit=crop\",\n \"https://images.unsplash.com/photo-1499856871958-5b9627545d1a?w=320&h=320&fit=crop\",\n ],\n },\n likesCount: 30,\n repliesCount: 10,\n isLiked: false,\n },\n {\n id: \"2\",\n author: {\n id: \"mary\",\n name: \"Mary Trott\",\n avatarUrl: \"https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=150&h=150&fit=crop&crop=face\",\n },\n date: \"Feb 23, 1:32 PM\",\n content: \"Learning how to Bubble\",\n video: {\n thumbnailUrl: \"https://images.unsplash.com/photo-1461749280684-dccba630e2f6?w=480&h=380&fit=crop\",\n },\n likesCount: 30,\n repliesCount: 10,\n isLiked: false,\n replies: [\n {\n id: \"2-reply-1\",\n author: {\n id: \"aya\",\n name: \"Aya Williams\",\n avatarUrl: \"https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=150&h=150&fit=crop&crop=face\",\n },\n date: \"Mar 12, 11:23 AM\",\n content: \"Check out these flight deals to Paris!\",\n linkPreview: {\n url: \"https://expedia.com/flights/paris\",\n domain: \"expedia.com\",\n title: \"Paris flights\",\n description: \"Your one-stop travel site for your dream vacation. Bundle your stay...\",\n imageUrl: \"https://images.unsplash.com/photo-1502602898657-3e91760cbb34?w=200&h=200&fit=crop\",\n },\n likesCount: 30,\n repliesCount: 10,\n isLiked: false,\n isReply: true,\n },\n ],\n },\n];\n\n// ============================================\n// Sub-components\n// ============================================\n\ninterface PostComposerProps {\n placeholder?: string;\n imagePreview?: string;\n onPost?: (content: string) => void;\n}\n\nfunction PostComposer({ placeholder = \"What's on your mind?\", imagePreview, onPost }: PostComposerProps) {\n const [content, setContent] = useState(\"\");\n\n const handlePost = () => {\n if (content.trim() || imagePreview) {\n onPost?.(content);\n setContent(\"\");\n }\n };\n\n return (\n <div\n className=\"flex flex-col w-full overflow-hidden\"\n style={{\n border: \"1px solid var(--canvas-border)\",\n boxShadow: \"0px 1px 8px 0px rgba(0,0,0,0.03)\",\n }}\n >\n {/* Text input area */}\n <div\n className=\"w-full\"\n style={{\n padding: \"var(--spacing-xl)\",\n background: \"var(--canvas-background)\",\n boxShadow: \"0px 1px 2px 0px rgba(0,0,0,0.02)\",\n }}\n >\n <textarea\n value={content}\n onChange={(e) => setContent(e.target.value)}\n placeholder={placeholder}\n className=\"w-full resize-none border-0 bg-transparent outline-none\"\n rows={2}\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: content ? \"var(--canvas-text)\" : \"var(--canvas-text-placeholder)\",\n }}\n />\n </div>\n\n {/* Image preview */}\n {imagePreview && (\n <div\n className=\"w-full\"\n style={{\n padding: \"0 var(--spacing-xl)\",\n background: \"var(--canvas-background)\",\n }}\n >\n <div\n className=\"overflow-hidden\"\n style={{\n width: 240,\n height: 180,\n borderRadius: \"var(--radius-md)\",\n border: \"1px solid var(--canvas-border)\",\n }}\n >\n <img\n src={imagePreview}\n alt=\"Preview\"\n className=\"w-full h-full object-cover\"\n />\n </div>\n </div>\n )}\n\n {/* Action bar */}\n <div\n className=\"flex items-center justify-between w-full\"\n style={{\n padding: \"var(--spacing-xl)\",\n background: \"var(--canvas-background)\",\n borderTop: \"1px solid var(--canvas-border)\",\n }}\n >\n <div className=\"flex items-center\" style={{ gap: \"var(--spacing-lg)\" }}>\n <button type=\"button\" className=\"cursor-pointer\" style={{ color: \"var(--canvas-text)\" }}>\n <Paperclip className=\"w-5 h-5\" />\n </button>\n <button type=\"button\" className=\"cursor-pointer\" style={{ color: \"var(--canvas-text)\" }}>\n <Video className=\"w-5 h-5\" />\n </button>\n <button type=\"button\" className=\"cursor-pointer\" style={{ color: \"var(--canvas-text)\" }}>\n <Link2 className=\"w-5 h-5\" />\n </button>\n </div>\n <Button variant=\"primary\" size=\"sm\" onClick={handlePost}>\n Post\n </Button>\n </div>\n </div>\n );\n}\n\ninterface ActionIconsRowProps {\n isLiked?: boolean;\n onLike?: () => void;\n onComment?: () => void;\n onRepost?: () => void;\n onShare?: () => void;\n}\n\nfunction ActionIconsRow({ isLiked, onLike, onComment, onRepost, onShare }: ActionIconsRowProps) {\n return (\n <div\n className=\"flex items-center\"\n style={{ gap: \"var(--spacing-lg)\", padding: \"var(--spacing-xxs) 0\" }}\n >\n <button\n type=\"button\"\n onClick={onLike}\n className=\"cursor-pointer\"\n style={{ color: isLiked ? \"var(--canvas-destructive)\" : \"var(--canvas-text)\" }}\n >\n <Heart\n className=\"w-5 h-5\"\n style={{\n fill: isLiked ? \"var(--canvas-destructive)\" : \"transparent\",\n }}\n />\n </button>\n <button type=\"button\" onClick={onComment} className=\"cursor-pointer\" style={{ color: \"var(--canvas-text)\" }}>\n <MessageCircle className=\"w-5 h-5\" />\n </button>\n <button type=\"button\" onClick={onRepost} className=\"cursor-pointer\" style={{ color: \"var(--canvas-text)\" }}>\n <RefreshCw className=\"w-5 h-5\" />\n </button>\n <button type=\"button\" onClick={onShare} className=\"cursor-pointer\" style={{ color: \"var(--canvas-text)\" }}>\n <Send className=\"w-5 h-5\" />\n </button>\n </div>\n );\n}\n\ninterface StatsRowProps {\n likesCount: number;\n repliesCount: number;\n}\n\nfunction StatsRow({ likesCount, repliesCount }: StatsRowProps) {\n return (\n <div\n className=\"flex items-center\"\n style={{ gap: \"var(--spacing-xl)\", paddingTop: \"var(--spacing-xs)\" }}\n >\n <span\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 500,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text-placeholder)\",\n }}\n >\n {likesCount} likes\n </span>\n <span\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 500,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text-placeholder)\",\n }}\n >\n {repliesCount} replies\n </span>\n </div>\n );\n}\n\ninterface VideoThumbnailProps {\n thumbnailUrl: string;\n onClick?: () => void;\n}\n\nfunction VideoThumbnail({ thumbnailUrl, onClick }: VideoThumbnailProps) {\n return (\n <div\n className=\"relative overflow-hidden cursor-pointer\"\n style={{\n width: 480,\n height: 380,\n maxWidth: \"100%\",\n borderRadius: \"var(--radius-3xl)\",\n border: \"1px solid var(--canvas-border)\",\n }}\n onClick={onClick}\n >\n <img\n src={thumbnailUrl}\n alt=\"Video thumbnail\"\n className=\"w-full h-full object-cover\"\n />\n {/* Play button */}\n <div\n className=\"absolute inset-0 flex items-center justify-center\"\n >\n <div\n className=\"flex items-center justify-center\"\n style={{\n width: 128,\n height: 80,\n background: \"var(--canvas-destructive)\",\n borderRadius: \"var(--radius-2xl)\",\n }}\n >\n <Play className=\"w-12 h-12 text-white fill-white\" />\n </div>\n </div>\n </div>\n );\n}\n\ninterface LinkPreviewCardProps {\n linkPreview: LinkPreview;\n onClick?: () => void;\n}\n\nfunction LinkPreviewCard({ linkPreview, onClick }: LinkPreviewCardProps) {\n return (\n <div\n className=\"flex overflow-hidden cursor-pointer\"\n style={{\n width: 580,\n maxWidth: \"100%\",\n background: \"var(--canvas-background)\",\n border: \"1px solid var(--canvas-border)\",\n borderRadius: \"var(--radius-2xl)\",\n boxShadow: \"0px 1px 8px 0px rgba(0,0,0,0.03)\",\n }}\n onClick={onClick}\n >\n {linkPreview.imageUrl && (\n <div\n className=\"shrink-0 self-stretch overflow-hidden\"\n style={{\n width: 200,\n borderRight: \"1px solid var(--canvas-border)\",\n borderTopLeftRadius: \"var(--radius-md)\",\n borderBottomLeftRadius: \"var(--radius-md)\",\n }}\n >\n <img\n src={linkPreview.imageUrl}\n alt={linkPreview.title}\n className=\"w-full h-full object-cover\"\n />\n </div>\n )}\n <div\n className=\"flex flex-col flex-1\"\n style={{ padding: \"var(--spacing-4xl)\", gap: \"var(--spacing-lg)\" }}\n >\n <div className=\"flex flex-col\" style={{ gap: 0 }}>\n <span\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text-placeholder)\",\n }}\n >\n {linkPreview.domain}\n </span>\n <span\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 600,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n {linkPreview.title}\n </span>\n </div>\n {linkPreview.description && (\n <p\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text)\",\n margin: 0,\n }}\n >\n {linkPreview.description}\n </p>\n )}\n </div>\n </div>\n );\n}\n\ninterface RepostCardProps {\n repost: RepostContent;\n onLike?: () => void;\n onComment?: () => void;\n onRepost?: () => void;\n onShare?: () => void;\n}\n\nfunction RepostCard({ repost, onLike, onComment, onRepost, onShare }: RepostCardProps) {\n return (\n <div\n className=\"flex flex-col w-full\"\n style={{\n padding: \"var(--spacing-4xl)\",\n background: \"var(--canvas-background)\",\n border: \"1px solid var(--canvas-border)\",\n borderRadius: \"var(--radius-md)\",\n boxShadow: \"0px 1px 8px 0px rgba(0,0,0,0.03)\",\n gap: \"var(--spacing-xl)\",\n }}\n >\n {/* Author row */}\n <div className=\"flex items-start w-full\" style={{ gap: \"var(--spacing-xl)\" }}>\n <Avatar\n className=\"shrink-0\"\n style={{\n width: 48,\n height: 48,\n borderRadius: \"var(--spacing-3xl)\",\n border: \"1px solid var(--canvas-border)\",\n }}\n >\n <AvatarImage src={repost.author.avatarUrl} />\n <AvatarFallback>\n {repost.author.name.split(\" \").map(n => n[0]).join(\"\").slice(0, 2)}\n </AvatarFallback>\n </Avatar>\n <div className=\"flex flex-col flex-1\" style={{ gap: \"var(--spacing-lg)\" }}>\n <div className=\"flex items-center justify-between w-full\">\n <div className=\"flex flex-col\" style={{ gap: 0 }}>\n <span\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 600,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n {repost.author.name}\n </span>\n <span\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text-placeholder)\",\n }}\n >\n {repost.date}\n </span>\n </div>\n <button\n type=\"button\"\n className=\"cursor-pointer flex items-center justify-center\"\n style={{\n width: 32,\n height: 32,\n borderRadius: \"var(--spacing-6xl)\",\n color: \"var(--canvas-text-placeholder)\",\n }}\n >\n <MoreHorizontal className=\"w-5 h-5\" />\n </button>\n </div>\n {/* Content */}\n <p\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text)\",\n margin: 0,\n }}\n >\n {repost.content}\n </p>\n {/* Images */}\n {repost.images && repost.images.length > 0 && (\n <div className=\"flex\" style={{ gap: \"var(--spacing-xl)\" }}>\n {repost.images.map((img, idx) => (\n <div\n key={idx}\n className=\"overflow-hidden\"\n style={{\n width: 320,\n height: 320,\n borderRadius: \"var(--radius-2xl)\",\n border: \"1px solid var(--canvas-border)\",\n }}\n >\n <img src={img} alt=\"\" className=\"w-full h-full object-cover\" />\n </div>\n ))}\n </div>\n )}\n {/* Actions */}\n <ActionIconsRow\n isLiked={false}\n onLike={onLike}\n onComment={onComment}\n onRepost={onRepost}\n onShare={onShare}\n />\n <StatsRow likesCount={30} repliesCount={10} />\n </div>\n </div>\n </div>\n );\n}\n\ninterface PostCellProps {\n post: SocialFeedPost;\n onLike?: () => void;\n onComment?: () => void;\n onRepost?: () => void;\n onShare?: () => void;\n onMenuClick?: () => void;\n}\n\nfunction PostCell({ post, onLike, onComment, onRepost, onShare, onMenuClick }: PostCellProps) {\n return (\n <div\n className=\"flex w-full\"\n style={{\n paddingLeft: post.isReply ? \"var(--spacing-7xl)\" : 0,\n paddingTop: post.isReply ? \"var(--spacing-3xl)\" : \"var(--spacing-xl)\",\n paddingBottom: post.isReply ? 0 : \"var(--spacing-3xl)\",\n borderBottom: post.isReply ? \"none\" : \"1px solid var(--canvas-border)\",\n gap: \"var(--spacing-xl)\",\n }}\n >\n {/* Avatar column with reply line */}\n <div className=\"flex flex-col items-center shrink-0\" style={{ gap: \"var(--spacing-md)\" }}>\n <Avatar\n style={{\n width: 48,\n height: 48,\n borderRadius: \"var(--spacing-3xl)\",\n border: \"1px solid var(--canvas-border)\",\n }}\n >\n <AvatarImage src={post.author.avatarUrl} />\n <AvatarFallback>\n {post.author.name.split(\" \").map(n => n[0]).join(\"\").slice(0, 2)}\n </AvatarFallback>\n </Avatar>\n {/* Reply line */}\n {post.replies && post.replies.length > 0 && (\n <div\n className=\"flex-1 w-px\"\n style={{ background: \"var(--canvas-border)\", minHeight: 20 }}\n />\n )}\n </div>\n\n {/* Content column */}\n <div className=\"flex flex-col flex-1 min-w-0\" style={{ gap: \"var(--spacing-lg)\" }}>\n {/* Header */}\n <div className=\"flex items-center justify-between w-full\">\n <div className=\"flex flex-col\" style={{ gap: 0 }}>\n <span\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 600,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n {post.author.name}\n </span>\n <span\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text-placeholder)\",\n }}\n >\n {post.date}\n </span>\n </div>\n <button\n type=\"button\"\n onClick={onMenuClick}\n className=\"cursor-pointer flex items-center justify-center\"\n style={{\n width: 32,\n height: 32,\n borderRadius: \"var(--spacing-6xl)\",\n color: \"var(--canvas-text-placeholder)\",\n }}\n >\n <MoreHorizontal className=\"w-5 h-5\" />\n </button>\n </div>\n\n {/* Content text */}\n <p\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text)\",\n margin: 0,\n }}\n >\n {post.content}\n </p>\n\n {/* Repost card */}\n {post.repost && (\n <RepostCard\n repost={post.repost}\n onLike={onLike}\n onComment={onComment}\n onRepost={onRepost}\n onShare={onShare}\n />\n )}\n\n {/* Images */}\n {post.images && post.images.length > 0 && (\n <div className=\"flex\" style={{ gap: \"var(--spacing-xl)\" }}>\n {post.images.map((img, idx) => (\n <div\n key={idx}\n className=\"overflow-hidden\"\n style={{\n width: 320,\n height: 320,\n borderRadius: \"var(--radius-2xl)\",\n border: \"1px solid var(--canvas-border)\",\n }}\n >\n <img src={img} alt=\"\" className=\"w-full h-full object-cover\" />\n </div>\n ))}\n </div>\n )}\n\n {/* Video */}\n {post.video && (\n <VideoThumbnail thumbnailUrl={post.video.thumbnailUrl} />\n )}\n\n {/* Link preview */}\n {post.linkPreview && (\n <LinkPreviewCard linkPreview={post.linkPreview} />\n )}\n\n {/* Actions */}\n <ActionIconsRow\n isLiked={post.isLiked}\n onLike={onLike}\n onComment={onComment}\n onRepost={onRepost}\n onShare={onShare}\n />\n\n {/* Stats */}\n <StatsRow likesCount={post.likesCount} repliesCount={post.repliesCount} />\n\n {/* Nested replies */}\n {post.replies && post.replies.length > 0 && (\n <div className=\"flex flex-col w-full\">\n {post.replies.map((reply) => (\n <PostCell\n key={reply.id}\n post={reply}\n onLike={onLike}\n onComment={onComment}\n onRepost={onRepost}\n onShare={onShare}\n />\n ))}\n </div>\n )}\n </div>\n </div>\n );\n}\n\n// ============================================\n// Main Component\n// ============================================\n\n/**\n * Canvas Design System - Social Feed Block\n *\n * A social media-style feed component with post composer, posts with various\n * content types (text, images, video, reposts, link cards), social interactions,\n * and threaded replies.\n *\n * @example\n * ```tsx\n * <SocialFeed\n * title=\"Social Feed\"\n * posts={[...]}\n * onLike={(postId) => console.log(\"Liked\", postId)}\n * onPost={(content) => console.log(\"Posted\", content)}\n * />\n * ```\n */\nexport function SocialFeed({\n title = \"Social Feed\",\n posts = defaultPosts,\n currentUser = defaultCurrentUser,\n composerPlaceholder = \"What's on your mind?\",\n composerImagePreview,\n onPost,\n onLike,\n onComment,\n onRepost,\n onShare,\n onMenuClick,\n className,\n}: SocialFeedProps) {\n return (\n <div\n className={cn(\"flex flex-col w-full\", className)}\n style={{ gap: \"var(--spacing-xl)\" }}\n >\n {/* Header Section */}\n {title && (\n <div className=\"flex flex-wrap items-end w-full\" style={{ gap: \"var(--spacing-xl)\" }}>\n <div className=\"flex flex-col flex-1 min-w-[200px]\" style={{ gap: \"var(--spacing-xs)\" }}>\n <h2\n style={{\n fontFamily: \"var(--typo-h6-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-h6-size)\",\n fontWeight: \"var(--typo-h6-weight)\",\n letterSpacing: \"var(--typo-h6-spacing)\",\n lineHeight: \"var(--typo-h6-line-height)\",\n color: \"var(--canvas-text)\",\n margin: 0,\n }}\n >\n {title}\n </h2>\n </div>\n </div>\n )}\n\n {/* Feed content */}\n <div className=\"flex flex-col w-full overflow-hidden\">\n {/* First section: Composer + first set of posts */}\n <div\n className=\"flex flex-col w-full\"\n style={{\n borderBottom: \"1px solid var(--canvas-border)\",\n paddingBottom: \"var(--spacing-5xl)\",\n }}\n >\n {/* Post Composer */}\n <PostComposer\n placeholder={composerPlaceholder}\n imagePreview={composerImagePreview}\n onPost={onPost}\n />\n\n {/* Posts */}\n <div className=\"flex flex-col w-full\" style={{ paddingTop: \"var(--spacing-xl)\" }}>\n {posts.map((post) => (\n <PostCell\n key={post.id}\n post={post}\n onLike={() => onLike?.(post.id)}\n onComment={() => onComment?.(post.id)}\n onRepost={() => onRepost?.(post.id)}\n onShare={() => onShare?.(post.id)}\n onMenuClick={() => onMenuClick?.(post.id)}\n />\n ))}\n </div>\n </div>\n </div>\n </div>\n );\n}\n"
10
10
  }
11
11
  ],
12
12
  "dependencies": [
@@ -6,7 +6,7 @@
6
6
  {
7
7
  "path": "components/blocks/standard-data-table.tsx",
8
8
  "type": "registry:block",
9
- "content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport { Button } from \"../ui/button\";\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from \"../ui/select\";\nimport { Avatar, AvatarImage, AvatarFallback } from \"../ui/avatar\";\nimport { MenufocusTemplate } from \"./menufocus-template\";\n\n// ============================================\n// Types\n// ============================================\n\nexport interface TableColumn {\n id: string;\n label: string;\n /** Width class or style for the column */\n width?: string;\n}\n\nexport interface TableRow {\n id: string;\n name: string;\n avatarUrl?: string;\n email: string;\n title: string;\n role: string;\n}\n\nexport interface SortOption {\n id: string;\n label: string;\n}\n\nexport interface FilterOption {\n id: string;\n label: string;\n}\n\nexport interface StandardDataTableProps {\n /** Table title */\n title?: string;\n /** Number of results to display */\n resultCount?: number;\n /** Custom result count text (overrides default \"{count} results\") */\n resultCountText?: string;\n /** Column definitions */\n columns?: TableColumn[];\n /** Table data rows */\n data?: TableRow[];\n /** Sort options for the sort dropdown */\n sortOptions?: SortOption[];\n /** Filter options for the filter dropdown */\n filterOptions?: FilterOption[];\n /** Primary action button text */\n actionButtonText?: string;\n /** Callback when add/action button is clicked */\n onAddNew?: () => void;\n /** Callback when sort value changes */\n onSort?: (value: string) => void;\n /** Callback when filter value changes */\n onFilter?: (value: string) => void;\n /** Callback when row action is clicked */\n onRowAction?: (action: string, row: TableRow) => void;\n /** Additional class names */\n className?: string;\n}\n\n// ============================================\n// Default Data\n// ============================================\n\nconst defaultColumns: TableColumn[] = [\n { id: \"name\", label: \"Name\" },\n { id: \"email\", label: \"Email\" },\n { id: \"title\", label: \"Title\" },\n { id: \"role\", label: \"Role\" },\n];\n\nconst defaultData: TableRow[] = [\n {\n id: \"1\",\n name: \"Jeff Connor\",\n avatarUrl: \"https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=150&h=150&fit=crop&crop=face\",\n email: \"jconnor@testemail.com\",\n title: \"Co-founder & CEO\",\n role: \"Standard\",\n },\n {\n id: \"2\",\n name: \"Aya Williams\",\n avatarUrl: \"https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=150&h=150&fit=crop&crop=face\",\n email: \"ayawilliams@testemail.com\",\n title: \"Chief Marketing Officer\",\n role: \"Standard\",\n },\n];\n\nconst defaultSortOptions: SortOption[] = [\n { id: \"name-asc\", label: \"Name (A-Z)\" },\n { id: \"name-desc\", label: \"Name (Z-A)\" },\n { id: \"date-newest\", label: \"Newest first\" },\n { id: \"date-oldest\", label: \"Oldest first\" },\n];\n\nconst defaultFilterOptions: FilterOption[] = [\n { id: \"all\", label: \"All roles\" },\n { id: \"admin\", label: \"Admin\" },\n { id: \"standard\", label: \"Standard\" },\n { id: \"viewer\", label: \"Viewer\" },\n];\n\n// ============================================\n// Sub-components\n// ============================================\n\ninterface TableHeaderCellProps {\n children: React.ReactNode;\n className?: string;\n}\n\nfunction TableHeaderCell({ children, className }: TableHeaderCellProps) {\n return (\n <div\n className={cn(\n \"flex items-center h-8 overflow-hidden\",\n className\n )}\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 600,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n {children}\n </div>\n );\n}\n\ninterface TableCellProps {\n children: React.ReactNode;\n className?: string;\n}\n\nfunction TableCell({ children, className }: TableCellProps) {\n return (\n <div\n className={cn(\n \"flex items-center gap-2 h-8 overflow-hidden\",\n className\n )}\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: \"var(--typo-body-s-weight)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n {children}\n </div>\n );\n}\n\n\n// ============================================\n// Main Component\n// ============================================\n\n/**\n * Canvas Design System - Standard Data Table Block\n * \n * A configurable data table with header section including title,\n * sort/filter dropdowns, and action button. Displays tabular data\n * with avatar support in the name column.\n * \n * @example\n * ```tsx\n * <StandardDataTable\n * title=\"Teammates\"\n * data={[\n * { id: \"1\", name: \"John\", email: \"john@example.com\", title: \"Engineer\", role: \"Admin\" }\n * ]}\n * onAddNew={() => console.log(\"Add new\")}\n * />\n * ```\n */\nexport function StandardDataTable({\n title = \"Teammates\",\n resultCount,\n resultCountText,\n columns = defaultColumns,\n data = defaultData,\n sortOptions = defaultSortOptions,\n filterOptions = defaultFilterOptions,\n actionButtonText = \"Add new\",\n onAddNew,\n onSort,\n onFilter,\n onRowAction,\n className,\n}: StandardDataTableProps) {\n const [sortValue, setSortValue] = useState<string>(\"\");\n const [filterValue, setFilterValue] = useState<string>(\"\");\n\n const displayResultCount = resultCount ?? data.length;\n const displayResultText = resultCountText ?? `${displayResultCount} results`;\n\n const handleSortChange = (value: string) => {\n setSortValue(value);\n onSort?.(value);\n };\n\n const handleFilterChange = (value: string) => {\n setFilterValue(value);\n onFilter?.(value);\n };\n\n return (\n <div \n className={cn(\"flex flex-col w-full\", className)}\n style={{ gap: \"var(--spacing-3xl)\" }}\n >\n {/* Header Section */}\n <div \n className=\"flex flex-wrap items-start w-full\"\n style={{ gap: \"var(--spacing-xl)\" }}\n >\n {/* Title and Count */}\n <div \n className=\"flex flex-col flex-1 min-w-[200px]\"\n style={{ gap: \"var(--spacing-xs)\" }}\n >\n <h2\n style={{\n fontFamily: \"var(--typo-h6-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-h6-size)\",\n fontWeight: \"var(--typo-h6-weight)\",\n letterSpacing: \"var(--typo-h6-spacing)\",\n lineHeight: \"var(--typo-h6-line-height)\",\n color: \"var(--canvas-text)\",\n margin: 0,\n }}\n >\n {title}\n </h2>\n <p\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: \"var(--typo-body-s-weight)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text-muted)\",\n margin: 0,\n }}\n >\n {displayResultText}\n </p>\n </div>\n\n {/* Controls */}\n <div \n className=\"flex items-start justify-end shrink-0 gap-3\"\n >\n {/* Sort Dropdown */}\n <div className=\"w-[120px]\">\n <Select value={sortValue || undefined} onValueChange={handleSortChange}>\n <SelectTrigger inputSize=\"sm\">\n <SelectValue placeholder=\"Sort\" />\n </SelectTrigger>\n <SelectContent>\n {sortOptions.map((option) => (\n <SelectItem key={option.id} value={option.id}>\n {option.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n\n {/* Filter Dropdown */}\n <div className=\"w-[120px]\">\n <Select value={filterValue || undefined} onValueChange={handleFilterChange}>\n <SelectTrigger inputSize=\"sm\">\n <SelectValue placeholder=\"Filter\" />\n </SelectTrigger>\n <SelectContent>\n {filterOptions.map((option) => (\n <SelectItem key={option.id} value={option.id}>\n {option.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n\n {/* Action Button */}\n <Button\n variant=\"primary\"\n size=\"sm\"\n onClick={onAddNew}\n style={{\n height: \"var(--btn-small-height)\",\n paddingLeft: \"var(--btn-small-px)\",\n paddingRight: \"var(--btn-small-px)\",\n fontSize: \"var(--btn-small-font-size)\",\n borderRadius: \"var(--btn-small-radius)\",\n backgroundColor: \"var(--btn-primary-bg)\",\n color: \"var(--btn-primary-text)\",\n borderColor: \"var(--btn-primary-border)\",\n }}\n >\n {actionButtonText}\n </Button>\n </div>\n </div>\n\n {/* Table Section */}\n <div className=\"w-full overflow-x-auto\">\n <table className=\"w-full min-w-max border-collapse\">\n <thead>\n <tr style={{ borderBottom: \"1px solid var(--canvas-border)\" }}>\n <th className=\"text-left pr-8 min-w-[150px]\">\n <TableHeaderCell>\n {columns.find(c => c.id === \"name\")?.label || \"Name\"}\n </TableHeaderCell>\n </th>\n <th className=\"text-left px-8 min-w-[150px]\">\n <TableHeaderCell>\n {columns.find(c => c.id === \"email\")?.label || \"Email\"}\n </TableHeaderCell>\n </th>\n <th className=\"text-left px-8 min-w-[120px]\">\n <TableHeaderCell>\n {columns.find(c => c.id === \"title\")?.label || \"Title\"}\n </TableHeaderCell>\n </th>\n <th className=\"text-left px-8 min-w-[80px]\">\n <TableHeaderCell>\n {columns.find(c => c.id === \"role\")?.label || \"Role\"}\n </TableHeaderCell>\n </th>\n <th className=\"w-8 pr-1\">\n <TableHeaderCell>&nbsp;</TableHeaderCell>\n </th>\n </tr>\n </thead>\n <tbody>\n {data.map((row) => (\n <tr\n key={row.id}\n style={{ borderBottom: \"1px solid var(--canvas-border)\" }}\n >\n <td\n className=\"pr-8\"\n style={{\n paddingTop: \"var(--spacing-md)\",\n paddingBottom: \"var(--spacing-md)\",\n }}\n >\n <TableCell>\n <Avatar className=\"size-8 border border-[var(--canvas-border)]\">\n <AvatarImage src={row.avatarUrl} alt={row.name} />\n <AvatarFallback>\n {row.name.split(\" \").map(n => n[0]).join(\"\").slice(0, 2)}\n </AvatarFallback>\n </Avatar>\n <span className=\"whitespace-nowrap\">{row.name}</span>\n </TableCell>\n </td>\n <td\n className=\"px-8\"\n style={{\n paddingTop: \"var(--spacing-md)\",\n paddingBottom: \"var(--spacing-md)\",\n }}\n >\n <TableCell>\n <span className=\"whitespace-nowrap\">{row.email}</span>\n </TableCell>\n </td>\n <td\n className=\"px-8\"\n style={{\n paddingTop: \"var(--spacing-md)\",\n paddingBottom: \"var(--spacing-md)\",\n }}\n >\n <TableCell>\n <span className=\"whitespace-nowrap\">{row.title}</span>\n </TableCell>\n </td>\n <td\n className=\"px-8\"\n style={{\n paddingTop: \"var(--spacing-md)\",\n paddingBottom: \"var(--spacing-md)\",\n }}\n >\n <TableCell>\n <span className=\"whitespace-nowrap\">{row.role}</span>\n </TableCell>\n </td>\n <td\n className=\"pr-1\"\n style={{\n paddingTop: \"var(--spacing-md)\",\n paddingBottom: \"var(--spacing-md)\",\n }}\n >\n <MenufocusTemplate\n ariaLabel=\"Row actions\"\n items={[\n { id: \"edit\", label: \"Edit\", onClick: () => onRowAction?.(\"edit\", row) },\n { id: \"view\", label: \"View details\", onClick: () => onRowAction?.(\"view\", row) },\n { id: \"delete\", label: \"Delete\", variant: \"destructive\", onClick: () => onRowAction?.(\"delete\", row) },\n ]}\n />\n </td>\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n </div>\n );\n}\n\n"
9
+ "content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport { Button } from \"../ui/button\";\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from \"../ui/select\";\nimport { Avatar, AvatarImage, AvatarFallback } from \"../ui/avatar\";\nimport { MenufocusTemplate } from \"./menufocus-template\";\n\n// ============================================\n// Types\n// ============================================\n\nexport interface TableColumn {\n id: string;\n label: string;\n /** Width class or style for the column */\n width?: string;\n}\n\nexport interface TableRow {\n id: string;\n name: string;\n avatarUrl?: string;\n email: string;\n title: string;\n role: string;\n}\n\nexport interface SortOption {\n id: string;\n label: string;\n}\n\nexport interface FilterOption {\n id: string;\n label: string;\n}\n\nexport interface StandardDataTableProps {\n /** Table title */\n title?: string;\n /** Number of results to display */\n resultCount?: number;\n /** Custom result count text (overrides default \"{count} results\") */\n resultCountText?: string;\n /** Column definitions */\n columns?: TableColumn[];\n /** Table data rows */\n data?: TableRow[];\n /** Sort options for the sort dropdown */\n sortOptions?: SortOption[];\n /** Filter options for the filter dropdown */\n filterOptions?: FilterOption[];\n /** Primary action button text */\n actionButtonText?: string;\n /** Callback when add/action button is clicked */\n onAddNew?: () => void;\n /** Callback when sort value changes */\n onSort?: (value: string) => void;\n /** Callback when filter value changes */\n onFilter?: (value: string) => void;\n /** Callback when row action is clicked */\n onRowAction?: (action: string, row: TableRow) => void;\n /** Additional class names */\n className?: string;\n}\n\n// ============================================\n// Default Data\n// ============================================\n\nconst defaultColumns: TableColumn[] = [\n { id: \"name\", label: \"Name\" },\n { id: \"email\", label: \"Email\" },\n { id: \"title\", label: \"Title\" },\n { id: \"role\", label: \"Role\" },\n];\n\nconst defaultData: TableRow[] = [\n {\n id: \"1\",\n name: \"Jeff Connor\",\n avatarUrl: \"https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=150&h=150&fit=crop&crop=face\",\n email: \"jconnor@testemail.com\",\n title: \"Co-founder & CEO\",\n role: \"Standard\",\n },\n {\n id: \"2\",\n name: \"Aya Williams\",\n avatarUrl: \"https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=150&h=150&fit=crop&crop=face\",\n email: \"ayawilliams@testemail.com\",\n title: \"Chief Marketing Officer\",\n role: \"Standard\",\n },\n];\n\nconst defaultSortOptions: SortOption[] = [\n { id: \"name-asc\", label: \"Name (A-Z)\" },\n { id: \"name-desc\", label: \"Name (Z-A)\" },\n { id: \"date-newest\", label: \"Newest first\" },\n { id: \"date-oldest\", label: \"Oldest first\" },\n];\n\nconst defaultFilterOptions: FilterOption[] = [\n { id: \"all\", label: \"All roles\" },\n { id: \"admin\", label: \"Admin\" },\n { id: \"standard\", label: \"Standard\" },\n { id: \"viewer\", label: \"Viewer\" },\n];\n\n// ============================================\n// Sub-components\n// ============================================\n\ninterface TableHeaderCellProps {\n children: React.ReactNode;\n className?: string;\n}\n\nfunction TableHeaderCell({ children, className }: TableHeaderCellProps) {\n return (\n <div\n className={cn(\n \"flex items-center h-8 overflow-hidden\",\n className\n )}\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 600,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n {children}\n </div>\n );\n}\n\ninterface TableCellProps {\n children: React.ReactNode;\n className?: string;\n}\n\nfunction TableCell({ children, className }: TableCellProps) {\n return (\n <div\n className={cn(\n \"flex items-center gap-2 h-8 overflow-hidden\",\n className\n )}\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: \"var(--typo-body-s-weight)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n {children}\n </div>\n );\n}\n\n\n// ============================================\n// Main Component\n// ============================================\n\n/**\n * Canvas Design System - Standard Data Table Block\n * \n * A configurable data table with header section including title,\n * sort/filter dropdowns, and action button. Displays tabular data\n * with avatar support in the name column.\n * \n * @example\n * ```tsx\n * <StandardDataTable\n * title=\"Teammates\"\n * data={[\n * { id: \"1\", name: \"John\", email: \"john@example.com\", title: \"Engineer\", role: \"Admin\" }\n * ]}\n * onAddNew={() => console.log(\"Add new\")}\n * />\n * ```\n */\nexport function StandardDataTable({\n title = \"Teammates\",\n resultCount,\n resultCountText,\n columns = defaultColumns,\n data = defaultData,\n sortOptions = defaultSortOptions,\n filterOptions = defaultFilterOptions,\n actionButtonText = \"Add new\",\n onAddNew,\n onSort,\n onFilter,\n onRowAction,\n className,\n}: StandardDataTableProps) {\n const [sortValue, setSortValue] = useState<string>(\"\");\n const [filterValue, setFilterValue] = useState<string>(\"\");\n\n const displayResultCount = resultCount ?? data.length;\n const displayResultText = resultCountText ?? `${displayResultCount} results`;\n\n const handleSortChange = (value: string) => {\n setSortValue(value);\n onSort?.(value);\n };\n\n const handleFilterChange = (value: string) => {\n setFilterValue(value);\n onFilter?.(value);\n };\n\n return (\n <div \n className={cn(\"flex flex-col w-full\", className)}\n style={{ gap: \"var(--spacing-xl)\" }}\n >\n {/* Header Section */}\n <div \n className=\"flex flex-wrap items-end w-full\"\n style={{ gap: \"var(--spacing-xl)\" }}\n >\n {/* Title and Count */}\n <div \n className=\"flex flex-col flex-1 min-w-[200px]\"\n style={{ gap: \"var(--spacing-xs)\" }}\n >\n <h2\n style={{\n fontFamily: \"var(--typo-h6-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-h6-size)\",\n fontWeight: \"var(--typo-h6-weight)\",\n letterSpacing: \"var(--typo-h6-spacing)\",\n lineHeight: \"var(--typo-h6-line-height)\",\n color: \"var(--canvas-text)\",\n margin: 0,\n }}\n >\n {title}\n </h2>\n <p\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: \"var(--typo-body-s-weight)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text-muted)\",\n margin: 0,\n }}\n >\n {displayResultText}\n </p>\n </div>\n\n {/* Controls */}\n <div \n className=\"flex items-end justify-end shrink-0 gap-3 max-sm:w-full max-sm:flex-wrap\"\n >\n {/* Sort Dropdown */}\n <div className=\"w-[120px] max-sm:flex-1 max-sm:min-w-[120px]\">\n <Select value={sortValue || undefined} onValueChange={handleSortChange}>\n <SelectTrigger inputSize=\"sm\">\n <SelectValue placeholder=\"Sort\" />\n </SelectTrigger>\n <SelectContent>\n {sortOptions.map((option) => (\n <SelectItem key={option.id} value={option.id}>\n {option.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n\n {/* Filter Dropdown */}\n <div className=\"w-[120px] max-sm:flex-1 max-sm:min-w-[120px]\">\n <Select value={filterValue || undefined} onValueChange={handleFilterChange}>\n <SelectTrigger inputSize=\"sm\">\n <SelectValue placeholder=\"Filter\" />\n </SelectTrigger>\n <SelectContent>\n {filterOptions.map((option) => (\n <SelectItem key={option.id} value={option.id}>\n {option.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n\n {/* Action Button */}\n <Button\n variant=\"primary\"\n size=\"sm\"\n onClick={onAddNew}\n style={{\n height: \"var(--btn-small-height)\",\n paddingLeft: \"var(--btn-small-px)\",\n paddingRight: \"var(--btn-small-px)\",\n fontSize: \"var(--btn-small-font-size)\",\n borderRadius: \"var(--btn-small-radius)\",\n backgroundColor: \"var(--btn-primary-bg)\",\n color: \"var(--btn-primary-text)\",\n borderColor: \"var(--btn-primary-border)\",\n }}\n >\n {actionButtonText}\n </Button>\n </div>\n </div>\n\n {/* Table Section */}\n <div className=\"w-full overflow-x-auto\">\n <table className=\"w-full min-w-max border-collapse\">\n <thead>\n <tr style={{ borderBottom: \"1px solid var(--canvas-border)\" }}>\n <th className=\"text-left pr-8 min-w-[150px]\">\n <TableHeaderCell>\n {columns.find(c => c.id === \"name\")?.label || \"Name\"}\n </TableHeaderCell>\n </th>\n <th className=\"text-left px-8 min-w-[150px]\">\n <TableHeaderCell>\n {columns.find(c => c.id === \"email\")?.label || \"Email\"}\n </TableHeaderCell>\n </th>\n <th className=\"text-left px-8 min-w-[120px]\">\n <TableHeaderCell>\n {columns.find(c => c.id === \"title\")?.label || \"Title\"}\n </TableHeaderCell>\n </th>\n <th className=\"text-left px-8 min-w-[80px]\">\n <TableHeaderCell>\n {columns.find(c => c.id === \"role\")?.label || \"Role\"}\n </TableHeaderCell>\n </th>\n <th className=\"w-8 pr-1\">\n <TableHeaderCell>&nbsp;</TableHeaderCell>\n </th>\n </tr>\n </thead>\n <tbody>\n {data.map((row) => (\n <tr\n key={row.id}\n style={{ borderBottom: \"1px solid var(--canvas-border)\" }}\n >\n <td\n className=\"pr-8\"\n style={{\n paddingTop: \"var(--spacing-md)\",\n paddingBottom: \"var(--spacing-md)\",\n }}\n >\n <TableCell>\n <Avatar className=\"size-8 border border-[var(--canvas-border)]\">\n <AvatarImage src={row.avatarUrl} alt={row.name} />\n <AvatarFallback>\n {row.name.split(\" \").map(n => n[0]).join(\"\").slice(0, 2)}\n </AvatarFallback>\n </Avatar>\n <span className=\"whitespace-nowrap\">{row.name}</span>\n </TableCell>\n </td>\n <td\n className=\"px-8\"\n style={{\n paddingTop: \"var(--spacing-md)\",\n paddingBottom: \"var(--spacing-md)\",\n }}\n >\n <TableCell>\n <span className=\"whitespace-nowrap\">{row.email}</span>\n </TableCell>\n </td>\n <td\n className=\"px-8\"\n style={{\n paddingTop: \"var(--spacing-md)\",\n paddingBottom: \"var(--spacing-md)\",\n }}\n >\n <TableCell>\n <span className=\"whitespace-nowrap\">{row.title}</span>\n </TableCell>\n </td>\n <td\n className=\"px-8\"\n style={{\n paddingTop: \"var(--spacing-md)\",\n paddingBottom: \"var(--spacing-md)\",\n }}\n >\n <TableCell>\n <span className=\"whitespace-nowrap\">{row.role}</span>\n </TableCell>\n </td>\n <td\n className=\"pr-1\"\n style={{\n paddingTop: \"var(--spacing-md)\",\n paddingBottom: \"var(--spacing-md)\",\n }}\n >\n <MenufocusTemplate\n ariaLabel=\"Row actions\"\n items={[\n { id: \"edit\", label: \"Edit\", onClick: () => onRowAction?.(\"edit\", row) },\n { id: \"view\", label: \"View details\", onClick: () => onRowAction?.(\"view\", row) },\n { id: \"delete\", label: \"Delete\", variant: \"destructive\", onClick: () => onRowAction?.(\"delete\", row) },\n ]}\n />\n </td>\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n </div>\n );\n}\n\n"
10
10
  }
11
11
  ],
12
12
  "dependencies": [],
@@ -6,7 +6,7 @@
6
6
  {
7
7
  "path": "components/blocks/standard-list-with-image.tsx",
8
8
  "type": "registry:block",
9
- "content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from \"../ui/select\";\n\n// ============================================\n// Types\n// ============================================\n\nexport interface Tag {\n id: string;\n label: string;\n}\n\nexport interface ListItem {\n id: string;\n title: string;\n author: string;\n date: string;\n description: string;\n imageUrl: string;\n tags?: Tag[];\n}\n\nexport interface SortOption {\n id: string;\n label: string;\n}\n\nexport interface FilterOption {\n id: string;\n label: string;\n}\n\nexport interface StandardListWithImageProps {\n /** Block title */\n title?: string;\n /** Block subtitle/description */\n subtitle?: string;\n /** List items to display */\n items?: ListItem[];\n /** Sort options for the sort dropdown */\n sortOptions?: SortOption[];\n /** Filter options for the filter dropdown */\n filterOptions?: FilterOption[];\n /** Callback when sort value changes */\n onSort?: (value: string) => void;\n /** Callback when filter value changes */\n onFilter?: (value: string) => void;\n /** Callback when an item is clicked */\n onItemClick?: (item: ListItem) => void;\n /** Additional class names */\n className?: string;\n}\n\n// ============================================\n// Default Data\n// ============================================\n\nconst defaultItems: ListItem[] = [\n {\n id: \"1\",\n title: \"Will AI make software developers obsolete?\",\n author: \"Jeffrey Connor\",\n date: \"Sep 21\",\n description:\n \"Will AI replace software developers? Find out as we explore the potential impact of artificial intelligence on the future of software development.\",\n imageUrl:\n \"https://images.unsplash.com/photo-1677442136019-21780ecad995?w=400&h=400&fit=crop\",\n tags: [\n { id: \"ai\", label: \"AI\" },\n { id: \"software\", label: \"Software\" },\n { id: \"technology\", label: \"Technology\" },\n ],\n },\n {\n id: \"2\",\n title: \"Building software that users will love: 5 principles to follow\",\n author: \"Mary Trott\",\n date: \"Aug 2\",\n description:\n \"The most successful businesses follow a few simple rules when building software products that excite their users. See them here.\",\n imageUrl:\n \"https://images.unsplash.com/photo-1498050108023-c5249f4df085?w=400&h=400&fit=crop\",\n tags: [\n { id: \"software\", label: \"Software\" },\n { id: \"best-practices\", label: \"Best practices\" },\n ],\n },\n];\n\nconst defaultSortOptions: SortOption[] = [\n { id: \"date-newest\", label: \"Newest first\" },\n { id: \"date-oldest\", label: \"Oldest first\" },\n { id: \"title-asc\", label: \"Title (A-Z)\" },\n { id: \"title-desc\", label: \"Title (Z-A)\" },\n];\n\nconst defaultFilterOptions: FilterOption[] = [\n { id: \"all\", label: \"All categories\" },\n { id: \"ai\", label: \"AI\" },\n { id: \"software\", label: \"Software\" },\n { id: \"technology\", label: \"Technology\" },\n];\n\n// ============================================\n// Sub-components\n// ============================================\n\ninterface TagPillProps {\n label: string;\n}\n\nfunction TagPill({ label }: TagPillProps) {\n return (\n <span\n className=\"inline-flex items-center overflow-hidden\"\n style={{\n height: \"32px\",\n paddingLeft: \"var(--spacing-lg)\",\n paddingRight: \"var(--spacing-lg)\",\n paddingTop: \"var(--spacing-xs)\",\n paddingBottom: \"var(--spacing-xs)\",\n backgroundColor: \"var(--canvas-surface-brand)\",\n borderRadius: \"var(--radius-xs)\",\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: \"var(--typo-body-s-weight)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-primary)\",\n }}\n >\n {label}\n </span>\n );\n}\n\ninterface ListItemCardProps {\n item: ListItem;\n onClick?: () => void;\n}\n\nfunction ListItemCard({ item, onClick }: ListItemCardProps) {\n return (\n <div\n className={cn(\n \"flex w-full cursor-pointer\",\n onClick && \"hover:bg-[var(--canvas-surface)]\"\n )}\n style={{\n gap: \"var(--spacing-3xl)\",\n paddingTop: \"var(--spacing-3xl)\",\n paddingBottom: \"var(--spacing-3xl)\",\n borderBottom: \"1px solid var(--canvas-border)\",\n }}\n onClick={onClick}\n >\n {/* Image */}\n <div\n className=\"shrink-0 overflow-hidden\"\n style={{\n width: \"200px\",\n height: \"200px\",\n borderRadius: \"var(--radius-md)\",\n border: \"1px solid var(--canvas-border)\",\n }}\n >\n <img\n src={item.imageUrl}\n alt={item.title}\n className=\"w-full h-full object-cover\"\n />\n </div>\n\n {/* Content */}\n <div\n className=\"flex flex-col flex-1 min-w-0\"\n style={{ gap: \"var(--spacing-3xl)\" }}\n >\n {/* Title and Meta */}\n <div\n className=\"flex flex-col\"\n style={{ gap: \"var(--spacing-xs)\" }}\n >\n {/* Title */}\n <h3\n className=\"m-0\"\n style={{\n fontFamily: \"var(--typo-body-xl-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-xl-size)\",\n fontWeight: 600,\n lineHeight: \"var(--typo-body-xl-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n {item.title}\n </h3>\n\n {/* Author and Date */}\n <div\n className=\"flex items-center justify-between w-full\"\n style={{ gap: \"var(--spacing-xl)\" }}\n >\n <p\n className=\"m-0\"\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: \"var(--typo-body-s-weight)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n <span style={{ color: \"var(--canvas-text-muted)\" }}>by</span>{\" \"}\n <span style={{ fontWeight: 500 }}>{item.author}</span>\n </p>\n <p\n className=\"m-0 whitespace-nowrap\"\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: \"var(--typo-body-s-weight)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text-muted)\",\n }}\n >\n {item.date}\n </p>\n </div>\n </div>\n\n {/* Description */}\n <p\n className=\"m-0\"\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: \"var(--typo-body-s-weight)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text-muted)\",\n }}\n >\n {item.description}\n </p>\n\n {/* Tags */}\n {item.tags && item.tags.length > 0 && (\n <div\n className=\"flex flex-wrap\"\n style={{ gap: \"var(--spacing-md)\" }}\n >\n {item.tags.map((tag) => (\n <TagPill key={tag.id} label={tag.label} />\n ))}\n </div>\n )}\n </div>\n </div>\n );\n}\n\n// ============================================\n// Main Component\n// ============================================\n\n/**\n * Canvas Design System - Standard List with Image Block\n *\n * A configurable list block with image thumbnails, commonly used for\n * blog posts, articles, or any content with featured images.\n * Includes header section with title, subtitle, and sort/filter controls.\n *\n * @example\n * ```tsx\n * <StandardListWithImage\n * title=\"Blog\"\n * subtitle=\"Read our latest articles\"\n * items={blogPosts}\n * onItemClick={(item) => console.log(\"Clicked:\", item.title)}\n * />\n * ```\n */\nexport function StandardListWithImage({\n title = \"Blog\",\n subtitle = \"Read our latest articles about tech\",\n items = defaultItems,\n sortOptions = defaultSortOptions,\n filterOptions = defaultFilterOptions,\n onSort,\n onFilter,\n onItemClick,\n className,\n}: StandardListWithImageProps) {\n const [sortValue, setSortValue] = useState<string>(\"\");\n const [filterValue, setFilterValue] = useState<string>(\"\");\n\n const handleSortChange = (value: string) => {\n setSortValue(value);\n onSort?.(value);\n };\n\n const handleFilterChange = (value: string) => {\n setFilterValue(value);\n onFilter?.(value);\n };\n\n return (\n <div\n className={cn(\"flex flex-col w-full\", className)}\n style={{ gap: \"var(--spacing-xl)\" }}\n >\n {/* Header Section */}\n <div\n className=\"flex flex-wrap items-start w-full\"\n style={{ gap: \"var(--spacing-xl)\" }}\n >\n {/* Title and Subtitle */}\n <div\n className=\"flex flex-col flex-1 min-w-[200px]\"\n style={{ gap: \"var(--spacing-xs)\" }}\n >\n <h2\n className=\"m-0\"\n style={{\n fontFamily: \"var(--typo-h6-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-h6-size)\",\n fontWeight: \"var(--typo-h6-weight)\",\n letterSpacing: \"var(--typo-h6-spacing)\",\n lineHeight: \"var(--typo-h6-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n {title}\n </h2>\n <p\n className=\"m-0\"\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: \"var(--typo-body-s-weight)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text-muted)\",\n }}\n >\n {subtitle}\n </p>\n </div>\n\n {/* Controls */}\n <div className=\"flex items-start justify-end shrink-0 gap-3\">\n {/* Sort Dropdown */}\n <div className=\"w-[120px]\">\n <Select\n value={sortValue || undefined}\n onValueChange={handleSortChange}\n >\n <SelectTrigger inputSize=\"sm\">\n <SelectValue placeholder=\"Sort\" />\n </SelectTrigger>\n <SelectContent>\n {sortOptions.map((option) => (\n <SelectItem key={option.id} value={option.id}>\n {option.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n\n {/* Filter Dropdown */}\n <div className=\"w-[120px]\">\n <Select\n value={filterValue || undefined}\n onValueChange={handleFilterChange}\n >\n <SelectTrigger inputSize=\"sm\">\n <SelectValue placeholder=\"Filter\" />\n </SelectTrigger>\n <SelectContent>\n {filterOptions.map((option) => (\n <SelectItem key={option.id} value={option.id}>\n {option.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n </div>\n </div>\n\n {/* List Section */}\n <div\n className=\"flex flex-col w-full\"\n style={{ borderTop: \"1px solid var(--canvas-border)\" }}\n >\n {items.map((item) => (\n <ListItemCard\n key={item.id}\n item={item}\n onClick={onItemClick ? () => onItemClick(item) : undefined}\n />\n ))}\n </div>\n </div>\n );\n}\n"
9
+ "content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from \"../ui/select\";\n\n// ============================================\n// Types\n// ============================================\n\nexport interface Tag {\n id: string;\n label: string;\n}\n\nexport interface ListItem {\n id: string;\n title: string;\n author: string;\n date: string;\n description: string;\n imageUrl: string;\n tags?: Tag[];\n}\n\nexport interface SortOption {\n id: string;\n label: string;\n}\n\nexport interface FilterOption {\n id: string;\n label: string;\n}\n\nexport interface StandardListWithImageProps {\n /** Block title */\n title?: string;\n /** Block subtitle/description */\n subtitle?: string;\n /** List items to display */\n items?: ListItem[];\n /** Sort options for the sort dropdown */\n sortOptions?: SortOption[];\n /** Filter options for the filter dropdown */\n filterOptions?: FilterOption[];\n /** Callback when sort value changes */\n onSort?: (value: string) => void;\n /** Callback when filter value changes */\n onFilter?: (value: string) => void;\n /** Callback when an item is clicked */\n onItemClick?: (item: ListItem) => void;\n /** Additional class names */\n className?: string;\n}\n\n// ============================================\n// Default Data\n// ============================================\n\nconst defaultItems: ListItem[] = [\n {\n id: \"1\",\n title: \"Will AI make software developers obsolete?\",\n author: \"Jeffrey Connor\",\n date: \"Sep 21\",\n description:\n \"Will AI replace software developers? Find out as we explore the potential impact of artificial intelligence on the future of software development.\",\n imageUrl:\n \"https://images.unsplash.com/photo-1677442136019-21780ecad995?w=400&h=400&fit=crop\",\n tags: [\n { id: \"ai\", label: \"AI\" },\n { id: \"software\", label: \"Software\" },\n { id: \"technology\", label: \"Technology\" },\n ],\n },\n {\n id: \"2\",\n title: \"Building software that users will love: 5 principles to follow\",\n author: \"Mary Trott\",\n date: \"Aug 2\",\n description:\n \"The most successful businesses follow a few simple rules when building software products that excite their users. See them here.\",\n imageUrl:\n \"https://images.unsplash.com/photo-1498050108023-c5249f4df085?w=400&h=400&fit=crop\",\n tags: [\n { id: \"software\", label: \"Software\" },\n { id: \"best-practices\", label: \"Best practices\" },\n ],\n },\n];\n\nconst defaultSortOptions: SortOption[] = [\n { id: \"date-newest\", label: \"Newest first\" },\n { id: \"date-oldest\", label: \"Oldest first\" },\n { id: \"title-asc\", label: \"Title (A-Z)\" },\n { id: \"title-desc\", label: \"Title (Z-A)\" },\n];\n\nconst defaultFilterOptions: FilterOption[] = [\n { id: \"all\", label: \"All categories\" },\n { id: \"ai\", label: \"AI\" },\n { id: \"software\", label: \"Software\" },\n { id: \"technology\", label: \"Technology\" },\n];\n\n// ============================================\n// Sub-components\n// ============================================\n\ninterface TagPillProps {\n label: string;\n}\n\nfunction TagPill({ label }: TagPillProps) {\n return (\n <span\n className=\"inline-flex items-center overflow-hidden\"\n style={{\n height: \"32px\",\n paddingLeft: \"var(--spacing-lg)\",\n paddingRight: \"var(--spacing-lg)\",\n paddingTop: \"var(--spacing-xs)\",\n paddingBottom: \"var(--spacing-xs)\",\n backgroundColor: \"var(--canvas-surface-brand)\",\n borderRadius: \"var(--radius-xs)\",\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: \"var(--typo-body-s-weight)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-primary)\",\n }}\n >\n {label}\n </span>\n );\n}\n\ninterface ListItemCardProps {\n item: ListItem;\n onClick?: () => void;\n}\n\nfunction ListItemCard({ item, onClick }: ListItemCardProps) {\n return (\n <div\n className={cn(\n \"flex w-full cursor-pointer\",\n onClick && \"hover:bg-[var(--canvas-surface)]\"\n )}\n style={{\n gap: \"var(--spacing-3xl)\",\n paddingTop: \"var(--spacing-3xl)\",\n paddingBottom: \"var(--spacing-3xl)\",\n borderBottom: \"1px solid var(--canvas-border)\",\n }}\n onClick={onClick}\n >\n {/* Image */}\n <div\n className=\"shrink-0 overflow-hidden\"\n style={{\n width: \"200px\",\n height: \"200px\",\n borderRadius: \"var(--radius-md)\",\n border: \"1px solid var(--canvas-border)\",\n }}\n >\n <img\n src={item.imageUrl}\n alt={item.title}\n className=\"w-full h-full object-cover\"\n />\n </div>\n\n {/* Content */}\n <div\n className=\"flex flex-col flex-1 min-w-0\"\n style={{ gap: \"var(--spacing-3xl)\" }}\n >\n {/* Title and Meta */}\n <div\n className=\"flex flex-col\"\n style={{ gap: \"var(--spacing-xs)\" }}\n >\n {/* Title */}\n <h3\n className=\"m-0\"\n style={{\n fontFamily: \"var(--typo-body-xl-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-xl-size)\",\n fontWeight: 600,\n lineHeight: \"var(--typo-body-xl-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n {item.title}\n </h3>\n\n {/* Author and Date */}\n <div\n className=\"flex items-center justify-between w-full\"\n style={{ gap: \"var(--spacing-xl)\" }}\n >\n <p\n className=\"m-0\"\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: \"var(--typo-body-s-weight)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n <span style={{ color: \"var(--canvas-text-muted)\" }}>by</span>{\" \"}\n <span style={{ fontWeight: 500 }}>{item.author}</span>\n </p>\n <p\n className=\"m-0 whitespace-nowrap\"\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: \"var(--typo-body-s-weight)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text-muted)\",\n }}\n >\n {item.date}\n </p>\n </div>\n </div>\n\n {/* Description */}\n <p\n className=\"m-0\"\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: \"var(--typo-body-s-weight)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text-muted)\",\n }}\n >\n {item.description}\n </p>\n\n {/* Tags */}\n {item.tags && item.tags.length > 0 && (\n <div\n className=\"flex flex-wrap\"\n style={{ gap: \"var(--spacing-md)\" }}\n >\n {item.tags.map((tag) => (\n <TagPill key={tag.id} label={tag.label} />\n ))}\n </div>\n )}\n </div>\n </div>\n );\n}\n\n// ============================================\n// Main Component\n// ============================================\n\n/**\n * Canvas Design System - Standard List with Image Block\n *\n * A configurable list block with image thumbnails, commonly used for\n * blog posts, articles, or any content with featured images.\n * Includes header section with title, subtitle, and sort/filter controls.\n *\n * @example\n * ```tsx\n * <StandardListWithImage\n * title=\"Blog\"\n * subtitle=\"Read our latest articles\"\n * items={blogPosts}\n * onItemClick={(item) => console.log(\"Clicked:\", item.title)}\n * />\n * ```\n */\nexport function StandardListWithImage({\n title = \"Blog\",\n subtitle = \"Read our latest articles about tech\",\n items = defaultItems,\n sortOptions = defaultSortOptions,\n filterOptions = defaultFilterOptions,\n onSort,\n onFilter,\n onItemClick,\n className,\n}: StandardListWithImageProps) {\n const [sortValue, setSortValue] = useState<string>(\"\");\n const [filterValue, setFilterValue] = useState<string>(\"\");\n\n const handleSortChange = (value: string) => {\n setSortValue(value);\n onSort?.(value);\n };\n\n const handleFilterChange = (value: string) => {\n setFilterValue(value);\n onFilter?.(value);\n };\n\n return (\n <div\n className={cn(\"flex flex-col w-full\", className)}\n style={{ gap: \"var(--spacing-xl)\" }}\n >\n {/* Header Section */}\n <div\n className=\"flex flex-wrap items-end w-full\"\n style={{ gap: \"var(--spacing-xl)\" }}\n >\n {/* Title and Subtitle */}\n <div\n className=\"flex flex-col flex-1 min-w-[200px]\"\n style={{ gap: \"var(--spacing-xs)\" }}\n >\n <h2\n className=\"m-0\"\n style={{\n fontFamily: \"var(--typo-h6-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-h6-size)\",\n fontWeight: \"var(--typo-h6-weight)\",\n letterSpacing: \"var(--typo-h6-spacing)\",\n lineHeight: \"var(--typo-h6-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n {title}\n </h2>\n <p\n className=\"m-0\"\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: \"var(--typo-body-s-weight)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text-muted)\",\n }}\n >\n {subtitle}\n </p>\n </div>\n\n {/* Controls */}\n <div className=\"flex items-end justify-end shrink-0 gap-3 max-sm:w-full max-sm:flex-wrap\">\n {/* Sort Dropdown */}\n <div className=\"w-[120px] max-sm:flex-1 max-sm:min-w-[120px]\">\n <Select\n value={sortValue || undefined}\n onValueChange={handleSortChange}\n >\n <SelectTrigger inputSize=\"sm\">\n <SelectValue placeholder=\"Sort\" />\n </SelectTrigger>\n <SelectContent>\n {sortOptions.map((option) => (\n <SelectItem key={option.id} value={option.id}>\n {option.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n\n {/* Filter Dropdown */}\n <div className=\"w-[120px] max-sm:flex-1 max-sm:min-w-[120px]\">\n <Select\n value={filterValue || undefined}\n onValueChange={handleFilterChange}\n >\n <SelectTrigger inputSize=\"sm\">\n <SelectValue placeholder=\"Filter\" />\n </SelectTrigger>\n <SelectContent>\n {filterOptions.map((option) => (\n <SelectItem key={option.id} value={option.id}>\n {option.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n </div>\n </div>\n\n {/* List Section */}\n <div\n className=\"flex flex-col w-full\"\n style={{ borderTop: \"1px solid var(--canvas-border)\" }}\n >\n {items.map((item) => (\n <ListItemCard\n key={item.id}\n item={item}\n onClick={onItemClick ? () => onItemClick(item) : undefined}\n />\n ))}\n </div>\n </div>\n );\n}\n"
10
10
  }
11
11
  ],
12
12
  "dependencies": [],
@@ -6,7 +6,7 @@
6
6
  {
7
7
  "path": "components/blocks/upvoting-posts-table.tsx",
8
8
  "type": "registry:block",
9
- "content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport { Button } from \"../ui/button\";\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from \"../ui/select\";\nimport { Avatar, AvatarImage, AvatarFallback } from \"../ui/avatar\";\nimport { Input } from \"../ui/input\";\nimport { ArrowUp, MessageCircle, Heart, Paperclip } from \"lucide-react\";\n\n// ============================================\n// Types\n// ============================================\n\nexport interface PostAuthor {\n id: string;\n name: string;\n avatarUrl?: string;\n}\n\nexport interface PostComment {\n id: string;\n author: PostAuthor;\n content: string;\n timestamp: string;\n likes: number;\n isLiked?: boolean;\n replies?: PostComment[];\n}\n\nexport interface Post {\n id: string;\n author: PostAuthor;\n date: string;\n title: string;\n content: string;\n imageUrl?: string;\n upvotes: number;\n isUpvoted?: boolean;\n comments?: PostComment[];\n}\n\nexport interface SortOption {\n id: string;\n label: string;\n}\n\nexport interface FilterOption {\n id: string;\n label: string;\n}\n\nexport interface UpvotingPostsTableProps {\n /** Table title */\n title?: string;\n /** Subtitle text */\n subtitle?: string;\n /** Posts data */\n posts?: Post[];\n /** Current user for comment input avatars */\n currentUser?: PostAuthor;\n /** Sort options for the sort dropdown */\n sortOptions?: SortOption[];\n /** Filter options for the filter dropdown */\n filterOptions?: FilterOption[];\n /** Primary action button text */\n actionButtonText?: string;\n /** Callback when add/action button is clicked */\n onAddNew?: () => void;\n /** Callback when sort value changes */\n onSort?: (value: string) => void;\n /** Callback when filter value changes */\n onFilter?: (value: string) => void;\n /** Callback when upvote is clicked */\n onUpvote?: (postId: string) => void;\n /** Callback when comment is submitted */\n onComment?: (postId: string, content: string) => void;\n /** Callback when reply is submitted */\n onReply?: (postId: string, commentId: string, content: string) => void;\n /** Callback when like is clicked */\n onLike?: (postId: string, commentId: string) => void;\n /** Additional class names */\n className?: string;\n}\n\n// ============================================\n// Default Data\n// ============================================\n\nconst defaultCurrentUser: PostAuthor = {\n id: \"current\",\n name: \"Current User\",\n avatarUrl: \"https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=150&h=150&fit=crop&crop=face\",\n};\n\nconst defaultPosts: Post[] = [\n {\n id: \"1\",\n author: {\n id: \"aya\",\n name: \"Aya Williams\",\n avatarUrl: \"https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=150&h=150&fit=crop&crop=face\",\n },\n date: \"May 23, 2024\",\n title: \"Latest travels\",\n content: \"Just touched down in the City of Lights! 🇫🇷 There's something truly magical about Paris - the cobblestone streets, the aroma of freshly baked croissants, and of course, the iconic Eiffel Tower piercing the sky. It's one of those moments where you realize dreams do come true. Can't wait to explore every corner of this enchanting city! #Paris #EiffelTower #TravelGoals\",\n imageUrl: \"https://images.unsplash.com/photo-1502602898657-3e91760cbb34?w=800&h=500&fit=crop\",\n upvotes: 8,\n isUpvoted: false,\n comments: [\n {\n id: \"c1\",\n author: {\n id: \"raj\",\n name: \"Raj Mishra\",\n avatarUrl: \"https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=150&h=150&fit=crop&crop=face\",\n },\n content: \"Wow, Paris looks absolutely stunning! The Eiffel Tower is such an iconic landmark. Hope you have an amazing time exploring the city and soaking in all its beauty. Safe travels!\",\n timestamp: \"Feb 23, 1:32 PM\",\n likes: 3,\n isLiked: true,\n replies: [\n {\n id: \"r1\",\n author: {\n id: \"raj\",\n name: \"Raj Mishra\",\n avatarUrl: \"https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=150&h=150&fit=crop&crop=face\",\n },\n content: \"Paris is truly a dream destination! The Eiffel Tower never fails to impress. Enjoy every moment of your adventure and make unforgettable memories. Can't wait to see more of your journey!\",\n timestamp: \"Mar 8, 11:23 AM\",\n likes: 0,\n isLiked: false,\n },\n ],\n },\n ],\n },\n];\n\nconst defaultSortOptions: SortOption[] = [\n { id: \"newest\", label: \"Newest first\" },\n { id: \"oldest\", label: \"Oldest first\" },\n { id: \"most-upvoted\", label: \"Most upvoted\" },\n];\n\nconst defaultFilterOptions: FilterOption[] = [\n { id: \"all\", label: \"All posts\" },\n { id: \"my-posts\", label: \"My posts\" },\n { id: \"following\", label: \"Following\" },\n];\n\n// ============================================\n// Sub-components\n// ============================================\n\ninterface CommentInputProps {\n avatarUrl?: string;\n avatarFallback?: string;\n placeholder?: string;\n buttonText?: string;\n size?: \"default\" | \"small\";\n showAttachment?: boolean;\n onSubmit?: (content: string) => void;\n}\n\nfunction CommentInput({\n avatarUrl,\n avatarFallback = \"U\",\n placeholder = \"Send a message\",\n buttonText = \"Send\",\n size = \"default\",\n showAttachment = true,\n onSubmit,\n}: CommentInputProps) {\n const [value, setValue] = useState(\"\");\n const avatarSize = size === \"small\" ? 40 : 48;\n\n const handleSubmit = () => {\n if (value.trim() && onSubmit) {\n onSubmit(value.trim());\n setValue(\"\");\n }\n };\n\n return (\n <div\n className=\"flex items-center w-full\"\n style={{ gap: \"var(--spacing-xl)\" }}\n >\n <Avatar\n className=\"shrink-0\"\n style={{\n width: avatarSize,\n height: avatarSize,\n borderRadius: \"var(--spacing-3xl)\",\n border: \"1px solid var(--canvas-border)\",\n }}\n >\n <AvatarImage src={avatarUrl} />\n <AvatarFallback>{avatarFallback}</AvatarFallback>\n </Avatar>\n <div className=\"flex-1 relative\">\n <Input\n value={value}\n onChange={(e) => setValue(e.target.value)}\n placeholder={placeholder}\n className=\"pr-10\"\n onKeyDown={(e) => {\n if (e.key === \"Enter\" && !e.shiftKey) {\n e.preventDefault();\n handleSubmit();\n }\n }}\n />\n {showAttachment && (\n <button\n type=\"button\"\n className=\"cursor-pointer absolute right-3 top-1/2 -translate-y-1/2\"\n style={{ color: \"var(--canvas-text-placeholder)\" }}\n >\n <Paperclip className=\"w-5 h-5\" />\n </button>\n )}\n </div>\n <Button variant=\"primary\" onClick={handleSubmit}>\n {buttonText}\n </Button>\n </div>\n );\n}\n\ninterface CommentActionsProps {\n likes: number;\n isLiked?: boolean;\n timestamp: string;\n onReply?: () => void;\n onLike?: () => void;\n}\n\nfunction CommentActions({\n likes,\n isLiked,\n timestamp,\n onReply,\n onLike,\n}: CommentActionsProps) {\n return (\n <div\n className=\"flex items-center\"\n style={{\n gap: \"var(--spacing-xl)\",\n paddingTop: \"var(--spacing-xs)\",\n }}\n >\n <button\n type=\"button\"\n onClick={onReply}\n className=\"cursor-pointer flex items-center\"\n style={{\n gap: \"var(--spacing-sm)\",\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 500,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text-placeholder)\",\n }}\n >\n Reply\n </button>\n <button\n type=\"button\"\n onClick={onLike}\n className=\"cursor-pointer flex items-center\"\n style={{\n gap: \"var(--spacing-sm)\",\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 500,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text-placeholder)\",\n }}\n >\n <Heart\n className=\"w-5 h-5\"\n style={{\n fill: isLiked ? \"var(--canvas-destructive)\" : \"transparent\",\n stroke: isLiked ? \"var(--canvas-destructive)\" : \"currentColor\",\n }}\n />\n {likes > 0 ? `${likes} likes` : \"Like\"}\n </button>\n <span\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 500,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text-placeholder)\",\n }}\n >\n {timestamp}\n </span>\n </div>\n );\n}\n\ninterface PostCommentItemProps {\n comment: PostComment;\n currentUser?: PostAuthor;\n depth?: number;\n onReply?: (content: string) => void;\n onLike?: () => void;\n}\n\nfunction PostCommentItem({\n comment,\n currentUser,\n depth = 0,\n onReply,\n onLike,\n}: PostCommentItemProps) {\n const [showReplyInput, setShowReplyInput] = useState(false);\n const [showReplies, setShowReplies] = useState(true);\n const hasReplies = comment.replies && comment.replies.length > 0;\n\n return (\n <div\n className=\"flex flex-col w-full\"\n style={{ gap: \"var(--spacing-xl)\" }}\n >\n {/* Comment content */}\n <div\n className=\"flex w-full\"\n style={{ gap: \"var(--spacing-xl)\" }}\n >\n <Avatar\n className=\"shrink-0\"\n style={{\n width: 48,\n height: 48,\n borderRadius: \"var(--spacing-3xl)\",\n border: \"1px solid var(--canvas-border)\",\n }}\n >\n <AvatarImage src={comment.author.avatarUrl} />\n <AvatarFallback>\n {comment.author.name.split(\" \").map(n => n[0]).join(\"\").slice(0, 2)}\n </AvatarFallback>\n </Avatar>\n <div className=\"flex-1 flex flex-col\" style={{ gap: \"var(--spacing-sm)\" }}>\n {/* Author and timestamp */}\n <div\n className=\"flex items-center\"\n style={{ gap: \"var(--spacing-xl)\" }}\n >\n <span\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 600,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n {comment.author.name}\n </span>\n <span\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 400,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text-placeholder)\",\n }}\n >\n {comment.timestamp}\n </span>\n </div>\n {/* Comment text */}\n <p\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 400,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text)\",\n margin: 0,\n }}\n >\n {comment.content}\n </p>\n {/* Actions */}\n <CommentActions\n likes={comment.likes}\n isLiked={comment.isLiked}\n timestamp={comment.timestamp}\n onReply={() => setShowReplyInput(!showReplyInput)}\n onLike={onLike}\n />\n </div>\n </div>\n\n {/* Reply input */}\n {showReplyInput && (\n <div style={{ paddingLeft: \"var(--spacing-7xl)\" }}>\n <CommentInput\n avatarUrl={currentUser?.avatarUrl}\n avatarFallback={currentUser?.name?.charAt(0) || \"U\"}\n placeholder=\"Send a message\"\n buttonText=\"Reply\"\n size=\"small\"\n showAttachment={false}\n onSubmit={(content) => {\n onReply?.(content);\n setShowReplyInput(false);\n }}\n />\n </div>\n )}\n\n {/* Nested replies */}\n {hasReplies && showReplies && (\n <div\n className=\"flex flex-col\"\n style={{\n paddingLeft: \"var(--spacing-7xl)\",\n gap: \"var(--spacing-xl)\",\n }}\n >\n {comment.replies!.map((reply) => (\n <PostCommentItem\n key={reply.id}\n comment={reply}\n currentUser={currentUser}\n depth={depth + 1}\n onReply={onReply}\n onLike={onLike}\n />\n ))}\n </div>\n )}\n\n {/* Hide replies toggle */}\n {hasReplies && (\n <div\n className=\"flex items-center\"\n style={{\n paddingLeft: \"var(--spacing-7xl)\",\n gap: \"var(--spacing-md)\",\n }}\n >\n <div\n className=\"flex-1 h-px\"\n style={{ backgroundColor: \"var(--canvas-border)\" }}\n />\n <button\n type=\"button\"\n onClick={() => setShowReplies(!showReplies)}\n className=\"cursor-pointer\"\n style={{\n fontFamily: \"var(--typo-body-xs-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-xs-size)\",\n fontWeight: 500,\n lineHeight: \"var(--typo-body-xs-line-height)\",\n color: \"var(--canvas-text-placeholder)\",\n }}\n >\n {showReplies ? \"Hide replies\" : \"Show replies\"}\n </button>\n </div>\n )}\n </div>\n );\n}\n\ninterface PostCardProps {\n post: Post;\n currentUser?: PostAuthor;\n onUpvote?: () => void;\n onComment?: (content: string) => void;\n onReply?: (commentId: string, content: string) => void;\n onLike?: (commentId: string) => void;\n}\n\nfunction PostCard({\n post,\n currentUser,\n onUpvote,\n onComment,\n onReply,\n onLike,\n}: PostCardProps) {\n return (\n <div\n className=\"flex flex-col w-full\"\n style={{\n gap: \"var(--spacing-xl)\",\n borderBottom: \"1px solid var(--canvas-border)\",\n paddingBottom: \"var(--spacing-5xl)\",\n }}\n >\n {/* Post content */}\n <div className=\"flex w-full\" style={{ gap: \"var(--spacing-3xl)\" }}>\n {/* Upvote button */}\n <div\n className=\"flex flex-col items-center shrink-0\"\n style={{ gap: \"var(--spacing-xs)\" }}\n >\n <button\n type=\"button\"\n onClick={onUpvote}\n className=\"cursor-pointer flex items-center justify-center\"\n style={{\n width: 40,\n height: 40,\n borderRadius: \"var(--radius-xs)\",\n border: \"1px solid var(--canvas-border)\",\n backgroundColor: \"var(--canvas-background)\",\n boxShadow: \"0px 1px 8px 0px rgba(0,0,0,0.03)\",\n color: post.isUpvoted ? \"var(--canvas-primary)\" : \"var(--canvas-text-muted)\",\n }}\n >\n <ArrowUp className=\"w-6 h-6\" />\n </button>\n <span\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 400,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text-muted)\",\n }}\n >\n {post.upvotes}\n </span>\n </div>\n\n {/* Post main content */}\n <div\n className=\"flex-1 flex flex-col\"\n style={{ gap: \"var(--spacing-3xl)\" }}\n >\n {/* Author row */}\n <div className=\"flex items-start\" style={{ gap: \"var(--spacing-xl)\" }}>\n <Avatar\n className=\"shrink-0\"\n style={{\n width: 48,\n height: 48,\n borderRadius: \"var(--spacing-3xl)\",\n border: \"1px solid var(--canvas-border)\",\n }}\n >\n <AvatarImage src={post.author.avatarUrl} />\n <AvatarFallback>\n {post.author.name.split(\" \").map(n => n[0]).join(\"\").slice(0, 2)}\n </AvatarFallback>\n </Avatar>\n <div className=\"flex flex-col\">\n <span\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 600,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n {post.author.name}\n </span>\n <span\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 400,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text-muted)\",\n }}\n >\n {post.date}\n </span>\n </div>\n </div>\n\n {/* Title and content */}\n <div className=\"flex flex-col\" style={{ gap: \"var(--spacing-xs)\" }}>\n <h3\n style={{\n fontFamily: \"var(--typo-body-xl-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-xl-size)\",\n fontWeight: 600,\n lineHeight: \"var(--typo-body-xl-line-height)\",\n color: \"var(--canvas-text)\",\n margin: 0,\n }}\n >\n {post.title}\n </h3>\n <p\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 400,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text)\",\n margin: 0,\n }}\n >\n {post.content}\n </p>\n </div>\n\n {/* Post image */}\n {post.imageUrl && (\n <div\n className=\"w-full overflow-hidden\"\n style={{\n maxWidth: 576,\n borderRadius: \"var(--radius-md)\",\n border: \"1px solid var(--canvas-border)\",\n }}\n >\n <img\n src={post.imageUrl}\n alt={post.title}\n className=\"w-full h-auto object-cover\"\n style={{ maxHeight: 375 }}\n />\n </div>\n )}\n\n {/* Comment button */}\n <div\n className=\"flex items-center justify-center w-full\"\n style={{\n borderTop: \"1px solid var(--canvas-border)\",\n paddingTop: \"var(--spacing-lg)\",\n paddingBottom: \"var(--spacing-lg)\",\n }}\n >\n <button\n type=\"button\"\n className=\"cursor-pointer flex items-center\"\n style={{\n gap: \"var(--spacing-sm)\",\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 500,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text-placeholder)\",\n }}\n >\n <MessageCircle className=\"w-5 h-5\" />\n Comment\n </button>\n </div>\n </div>\n </div>\n\n {/* Comment input */}\n <CommentInput\n avatarUrl={currentUser?.avatarUrl}\n avatarFallback={currentUser?.name?.charAt(0) || \"U\"}\n placeholder=\"Send a message\"\n buttonText=\"Send\"\n onSubmit={onComment}\n />\n\n {/* Comments section */}\n {post.comments && post.comments.length > 0 && (\n <div\n className=\"flex flex-col\"\n style={{\n paddingLeft: \"var(--spacing-7xl)\",\n paddingTop: \"var(--spacing-xl)\",\n gap: \"var(--spacing-md)\",\n }}\n >\n {post.comments.map((comment) => (\n <PostCommentItem\n key={comment.id}\n comment={comment}\n currentUser={currentUser}\n onReply={(content) => onReply?.(comment.id, content)}\n onLike={() => onLike?.(comment.id)}\n />\n ))}\n </div>\n )}\n </div>\n );\n}\n\n// ============================================\n// Main Component\n// ============================================\n\n/**\n * Canvas Design System - Upvoting Posts Table Block\n *\n * A social feed component with upvoting, comments, and nested replies.\n * Features header with sort/filter controls and action button.\n *\n * @example\n * ```tsx\n * <UpvotingPostsTable\n * title=\"My posts\"\n * subtitle=\"In the past year\"\n * posts={[...]}\n * onUpvote={(postId) => console.log(\"Upvoted\", postId)}\n * />\n * ```\n */\nexport function UpvotingPostsTable({\n title = \"My posts\",\n subtitle = \"In the past year\",\n posts = defaultPosts,\n currentUser = defaultCurrentUser,\n sortOptions = defaultSortOptions,\n filterOptions = defaultFilterOptions,\n actionButtonText = \"Add new\",\n onAddNew,\n onSort,\n onFilter,\n onUpvote,\n onComment,\n onReply,\n onLike,\n className,\n}: UpvotingPostsTableProps) {\n const [sortValue, setSortValue] = useState<string>(\"\");\n const [filterValue, setFilterValue] = useState<string>(\"\");\n\n const handleSortChange = (value: string) => {\n setSortValue(value);\n onSort?.(value);\n };\n\n const handleFilterChange = (value: string) => {\n setFilterValue(value);\n onFilter?.(value);\n };\n\n return (\n <div\n className={cn(\"flex flex-col w-full\", className)}\n style={{ gap: \"var(--spacing-3xl)\" }}\n >\n {/* Header Section */}\n <div\n className=\"flex flex-wrap items-start w-full\"\n style={{ gap: \"var(--spacing-xl)\" }}\n >\n {/* Title and Subtitle */}\n <div\n className=\"flex flex-col flex-1 min-w-[200px]\"\n style={{ gap: \"var(--spacing-xs)\" }}\n >\n <h2\n style={{\n fontFamily: \"var(--typo-h6-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-h6-size)\",\n fontWeight: \"var(--typo-h6-weight)\",\n letterSpacing: \"var(--typo-h6-spacing)\",\n lineHeight: \"var(--typo-h6-line-height)\",\n color: \"var(--canvas-text)\",\n margin: 0,\n }}\n >\n {title}\n </h2>\n <p\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: \"var(--typo-body-s-weight)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text-muted)\",\n margin: 0,\n }}\n >\n {subtitle}\n </p>\n </div>\n\n {/* Controls */}\n <div\n className=\"flex items-start justify-end shrink-0\"\n style={{ gap: \"var(--spacing-3xl)\" }}\n >\n {/* Sort Dropdown */}\n <div className=\"w-[120px]\">\n <Select value={sortValue || undefined} onValueChange={handleSortChange}>\n <SelectTrigger inputSize=\"sm\">\n <SelectValue placeholder=\"Sort\" />\n </SelectTrigger>\n <SelectContent>\n {sortOptions.map((option) => (\n <SelectItem key={option.id} value={option.id}>\n {option.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n\n {/* Filter Dropdown */}\n <div className=\"w-[120px]\">\n <Select value={filterValue || undefined} onValueChange={handleFilterChange}>\n <SelectTrigger inputSize=\"sm\">\n <SelectValue placeholder=\"Filter\" />\n </SelectTrigger>\n <SelectContent>\n {filterOptions.map((option) => (\n <SelectItem key={option.id} value={option.id}>\n {option.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n\n {/* Action Button */}\n <Button\n variant=\"primary\"\n size=\"sm\"\n onClick={onAddNew}\n style={{\n height: \"var(--btn-small-height)\",\n paddingLeft: \"var(--btn-small-px)\",\n paddingRight: \"var(--btn-small-px)\",\n fontSize: \"var(--btn-small-font-size)\",\n borderRadius: \"var(--btn-small-radius)\",\n backgroundColor: \"var(--btn-primary-bg)\",\n color: \"var(--btn-primary-text)\",\n borderColor: \"var(--btn-primary-border)\",\n }}\n >\n {actionButtonText}\n </Button>\n </div>\n </div>\n\n {/* Posts List */}\n <div className=\"flex flex-col w-full\">\n {posts.map((post) => (\n <PostCard\n key={post.id}\n post={post}\n currentUser={currentUser}\n onUpvote={() => onUpvote?.(post.id)}\n onComment={(content) => onComment?.(post.id, content)}\n onReply={(commentId, content) => onReply?.(post.id, commentId, content)}\n onLike={(commentId) => onLike?.(post.id, commentId)}\n />\n ))}\n </div>\n </div>\n );\n}\n"
9
+ "content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport { Button } from \"../ui/button\";\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from \"../ui/select\";\nimport { Avatar, AvatarImage, AvatarFallback } from \"../ui/avatar\";\nimport { Input } from \"../ui/input\";\nimport { ArrowUp, MessageCircle, Heart, Paperclip } from \"lucide-react\";\n\n// ============================================\n// Types\n// ============================================\n\nexport interface PostAuthor {\n id: string;\n name: string;\n avatarUrl?: string;\n}\n\nexport interface PostComment {\n id: string;\n author: PostAuthor;\n content: string;\n timestamp: string;\n likes: number;\n isLiked?: boolean;\n replies?: PostComment[];\n}\n\nexport interface Post {\n id: string;\n author: PostAuthor;\n date: string;\n title: string;\n content: string;\n imageUrl?: string;\n upvotes: number;\n isUpvoted?: boolean;\n comments?: PostComment[];\n}\n\nexport interface SortOption {\n id: string;\n label: string;\n}\n\nexport interface FilterOption {\n id: string;\n label: string;\n}\n\nexport interface UpvotingPostsTableProps {\n /** Table title */\n title?: string;\n /** Subtitle text */\n subtitle?: string;\n /** Posts data */\n posts?: Post[];\n /** Current user for comment input avatars */\n currentUser?: PostAuthor;\n /** Sort options for the sort dropdown */\n sortOptions?: SortOption[];\n /** Filter options for the filter dropdown */\n filterOptions?: FilterOption[];\n /** Primary action button text */\n actionButtonText?: string;\n /** Callback when add/action button is clicked */\n onAddNew?: () => void;\n /** Callback when sort value changes */\n onSort?: (value: string) => void;\n /** Callback when filter value changes */\n onFilter?: (value: string) => void;\n /** Callback when upvote is clicked */\n onUpvote?: (postId: string) => void;\n /** Callback when comment is submitted */\n onComment?: (postId: string, content: string) => void;\n /** Callback when reply is submitted */\n onReply?: (postId: string, commentId: string, content: string) => void;\n /** Callback when like is clicked */\n onLike?: (postId: string, commentId: string) => void;\n /** Additional class names */\n className?: string;\n}\n\n// ============================================\n// Default Data\n// ============================================\n\nconst defaultCurrentUser: PostAuthor = {\n id: \"current\",\n name: \"Current User\",\n avatarUrl: \"https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=150&h=150&fit=crop&crop=face\",\n};\n\nconst defaultPosts: Post[] = [\n {\n id: \"1\",\n author: {\n id: \"aya\",\n name: \"Aya Williams\",\n avatarUrl: \"https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=150&h=150&fit=crop&crop=face\",\n },\n date: \"May 23, 2024\",\n title: \"Latest travels\",\n content: \"Just touched down in the City of Lights! 🇫🇷 There's something truly magical about Paris - the cobblestone streets, the aroma of freshly baked croissants, and of course, the iconic Eiffel Tower piercing the sky. It's one of those moments where you realize dreams do come true. Can't wait to explore every corner of this enchanting city! #Paris #EiffelTower #TravelGoals\",\n imageUrl: \"https://images.unsplash.com/photo-1502602898657-3e91760cbb34?w=800&h=500&fit=crop\",\n upvotes: 8,\n isUpvoted: false,\n comments: [\n {\n id: \"c1\",\n author: {\n id: \"raj\",\n name: \"Raj Mishra\",\n avatarUrl: \"https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=150&h=150&fit=crop&crop=face\",\n },\n content: \"Wow, Paris looks absolutely stunning! The Eiffel Tower is such an iconic landmark. Hope you have an amazing time exploring the city and soaking in all its beauty. Safe travels!\",\n timestamp: \"Feb 23, 1:32 PM\",\n likes: 3,\n isLiked: true,\n replies: [\n {\n id: \"r1\",\n author: {\n id: \"raj\",\n name: \"Raj Mishra\",\n avatarUrl: \"https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=150&h=150&fit=crop&crop=face\",\n },\n content: \"Paris is truly a dream destination! The Eiffel Tower never fails to impress. Enjoy every moment of your adventure and make unforgettable memories. Can't wait to see more of your journey!\",\n timestamp: \"Mar 8, 11:23 AM\",\n likes: 0,\n isLiked: false,\n },\n ],\n },\n ],\n },\n];\n\nconst defaultSortOptions: SortOption[] = [\n { id: \"newest\", label: \"Newest first\" },\n { id: \"oldest\", label: \"Oldest first\" },\n { id: \"most-upvoted\", label: \"Most upvoted\" },\n];\n\nconst defaultFilterOptions: FilterOption[] = [\n { id: \"all\", label: \"All posts\" },\n { id: \"my-posts\", label: \"My posts\" },\n { id: \"following\", label: \"Following\" },\n];\n\n// ============================================\n// Sub-components\n// ============================================\n\ninterface CommentInputProps {\n avatarUrl?: string;\n avatarFallback?: string;\n placeholder?: string;\n buttonText?: string;\n size?: \"default\" | \"small\";\n showAttachment?: boolean;\n onSubmit?: (content: string) => void;\n}\n\nfunction CommentInput({\n avatarUrl,\n avatarFallback = \"U\",\n placeholder = \"Send a message\",\n buttonText = \"Send\",\n size = \"default\",\n showAttachment = true,\n onSubmit,\n}: CommentInputProps) {\n const [value, setValue] = useState(\"\");\n const avatarSize = size === \"small\" ? 40 : 48;\n\n const handleSubmit = () => {\n if (value.trim() && onSubmit) {\n onSubmit(value.trim());\n setValue(\"\");\n }\n };\n\n return (\n <div\n className=\"flex items-center w-full\"\n style={{ gap: \"var(--spacing-xl)\" }}\n >\n <Avatar\n className=\"shrink-0\"\n style={{\n width: avatarSize,\n height: avatarSize,\n borderRadius: \"var(--spacing-3xl)\",\n border: \"1px solid var(--canvas-border)\",\n }}\n >\n <AvatarImage src={avatarUrl} />\n <AvatarFallback>{avatarFallback}</AvatarFallback>\n </Avatar>\n <div className=\"flex-1 relative\">\n <Input\n value={value}\n onChange={(e) => setValue(e.target.value)}\n placeholder={placeholder}\n className=\"pr-10\"\n onKeyDown={(e) => {\n if (e.key === \"Enter\" && !e.shiftKey) {\n e.preventDefault();\n handleSubmit();\n }\n }}\n />\n {showAttachment && (\n <button\n type=\"button\"\n className=\"cursor-pointer absolute right-3 top-1/2 -translate-y-1/2\"\n style={{ color: \"var(--canvas-text-placeholder)\" }}\n >\n <Paperclip className=\"w-5 h-5\" />\n </button>\n )}\n </div>\n <Button variant=\"primary\" onClick={handleSubmit}>\n {buttonText}\n </Button>\n </div>\n );\n}\n\ninterface CommentActionsProps {\n likes: number;\n isLiked?: boolean;\n timestamp: string;\n onReply?: () => void;\n onLike?: () => void;\n}\n\nfunction CommentActions({\n likes,\n isLiked,\n timestamp,\n onReply,\n onLike,\n}: CommentActionsProps) {\n return (\n <div\n className=\"flex items-center\"\n style={{\n gap: \"var(--spacing-xl)\",\n paddingTop: \"var(--spacing-xs)\",\n }}\n >\n <button\n type=\"button\"\n onClick={onReply}\n className=\"cursor-pointer flex items-center\"\n style={{\n gap: \"var(--spacing-sm)\",\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 500,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text-placeholder)\",\n }}\n >\n Reply\n </button>\n <button\n type=\"button\"\n onClick={onLike}\n className=\"cursor-pointer flex items-center\"\n style={{\n gap: \"var(--spacing-sm)\",\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 500,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text-placeholder)\",\n }}\n >\n <Heart\n className=\"w-5 h-5\"\n style={{\n fill: isLiked ? \"var(--canvas-destructive)\" : \"transparent\",\n stroke: isLiked ? \"var(--canvas-destructive)\" : \"currentColor\",\n }}\n />\n {likes > 0 ? `${likes} likes` : \"Like\"}\n </button>\n <span\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 500,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text-placeholder)\",\n }}\n >\n {timestamp}\n </span>\n </div>\n );\n}\n\ninterface PostCommentItemProps {\n comment: PostComment;\n currentUser?: PostAuthor;\n depth?: number;\n onReply?: (content: string) => void;\n onLike?: () => void;\n}\n\nfunction PostCommentItem({\n comment,\n currentUser,\n depth = 0,\n onReply,\n onLike,\n}: PostCommentItemProps) {\n const [showReplyInput, setShowReplyInput] = useState(false);\n const [showReplies, setShowReplies] = useState(true);\n const hasReplies = comment.replies && comment.replies.length > 0;\n\n return (\n <div\n className=\"flex flex-col w-full\"\n style={{ gap: \"var(--spacing-xl)\" }}\n >\n {/* Comment content */}\n <div\n className=\"flex w-full\"\n style={{ gap: \"var(--spacing-xl)\" }}\n >\n <Avatar\n className=\"shrink-0\"\n style={{\n width: 48,\n height: 48,\n borderRadius: \"var(--spacing-3xl)\",\n border: \"1px solid var(--canvas-border)\",\n }}\n >\n <AvatarImage src={comment.author.avatarUrl} />\n <AvatarFallback>\n {comment.author.name.split(\" \").map(n => n[0]).join(\"\").slice(0, 2)}\n </AvatarFallback>\n </Avatar>\n <div className=\"flex-1 flex flex-col\" style={{ gap: \"var(--spacing-sm)\" }}>\n {/* Author and timestamp */}\n <div\n className=\"flex items-center\"\n style={{ gap: \"var(--spacing-xl)\" }}\n >\n <span\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 600,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n {comment.author.name}\n </span>\n <span\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 400,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text-placeholder)\",\n }}\n >\n {comment.timestamp}\n </span>\n </div>\n {/* Comment text */}\n <p\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 400,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text)\",\n margin: 0,\n }}\n >\n {comment.content}\n </p>\n {/* Actions */}\n <CommentActions\n likes={comment.likes}\n isLiked={comment.isLiked}\n timestamp={comment.timestamp}\n onReply={() => setShowReplyInput(!showReplyInput)}\n onLike={onLike}\n />\n </div>\n </div>\n\n {/* Reply input */}\n {showReplyInput && (\n <div style={{ paddingLeft: \"var(--spacing-7xl)\" }}>\n <CommentInput\n avatarUrl={currentUser?.avatarUrl}\n avatarFallback={currentUser?.name?.charAt(0) || \"U\"}\n placeholder=\"Send a message\"\n buttonText=\"Reply\"\n size=\"small\"\n showAttachment={false}\n onSubmit={(content) => {\n onReply?.(content);\n setShowReplyInput(false);\n }}\n />\n </div>\n )}\n\n {/* Nested replies */}\n {hasReplies && showReplies && (\n <div\n className=\"flex flex-col\"\n style={{\n paddingLeft: \"var(--spacing-7xl)\",\n gap: \"var(--spacing-xl)\",\n }}\n >\n {comment.replies!.map((reply) => (\n <PostCommentItem\n key={reply.id}\n comment={reply}\n currentUser={currentUser}\n depth={depth + 1}\n onReply={onReply}\n onLike={onLike}\n />\n ))}\n </div>\n )}\n\n {/* Hide replies toggle */}\n {hasReplies && (\n <div\n className=\"flex items-center\"\n style={{\n paddingLeft: \"var(--spacing-7xl)\",\n gap: \"var(--spacing-md)\",\n }}\n >\n <div\n className=\"flex-1 h-px\"\n style={{ backgroundColor: \"var(--canvas-border)\" }}\n />\n <button\n type=\"button\"\n onClick={() => setShowReplies(!showReplies)}\n className=\"cursor-pointer\"\n style={{\n fontFamily: \"var(--typo-body-xs-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-xs-size)\",\n fontWeight: 500,\n lineHeight: \"var(--typo-body-xs-line-height)\",\n color: \"var(--canvas-text-placeholder)\",\n }}\n >\n {showReplies ? \"Hide replies\" : \"Show replies\"}\n </button>\n </div>\n )}\n </div>\n );\n}\n\ninterface PostCardProps {\n post: Post;\n currentUser?: PostAuthor;\n onUpvote?: () => void;\n onComment?: (content: string) => void;\n onReply?: (commentId: string, content: string) => void;\n onLike?: (commentId: string) => void;\n}\n\nfunction PostCard({\n post,\n currentUser,\n onUpvote,\n onComment,\n onReply,\n onLike,\n}: PostCardProps) {\n return (\n <div\n className=\"flex flex-col w-full\"\n style={{\n gap: \"var(--spacing-xl)\",\n borderBottom: \"1px solid var(--canvas-border)\",\n paddingBottom: \"var(--spacing-5xl)\",\n }}\n >\n {/* Post content */}\n <div className=\"flex w-full\" style={{ gap: \"var(--spacing-3xl)\" }}>\n {/* Upvote button */}\n <div\n className=\"flex flex-col items-center shrink-0\"\n style={{ gap: \"var(--spacing-xs)\" }}\n >\n <button\n type=\"button\"\n onClick={onUpvote}\n className=\"cursor-pointer flex items-center justify-center\"\n style={{\n width: 40,\n height: 40,\n borderRadius: \"var(--radius-xs)\",\n border: \"1px solid var(--canvas-border)\",\n backgroundColor: \"var(--canvas-background)\",\n boxShadow: \"0px 1px 8px 0px rgba(0,0,0,0.03)\",\n color: post.isUpvoted ? \"var(--canvas-primary)\" : \"var(--canvas-text-muted)\",\n }}\n >\n <ArrowUp className=\"w-6 h-6\" />\n </button>\n <span\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 400,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text-muted)\",\n }}\n >\n {post.upvotes}\n </span>\n </div>\n\n {/* Post main content */}\n <div\n className=\"flex-1 flex flex-col\"\n style={{ gap: \"var(--spacing-3xl)\" }}\n >\n {/* Author row */}\n <div className=\"flex items-start\" style={{ gap: \"var(--spacing-xl)\" }}>\n <Avatar\n className=\"shrink-0\"\n style={{\n width: 48,\n height: 48,\n borderRadius: \"var(--spacing-3xl)\",\n border: \"1px solid var(--canvas-border)\",\n }}\n >\n <AvatarImage src={post.author.avatarUrl} />\n <AvatarFallback>\n {post.author.name.split(\" \").map(n => n[0]).join(\"\").slice(0, 2)}\n </AvatarFallback>\n </Avatar>\n <div className=\"flex flex-col\">\n <span\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 600,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n {post.author.name}\n </span>\n <span\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 400,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text-muted)\",\n }}\n >\n {post.date}\n </span>\n </div>\n </div>\n\n {/* Title and content */}\n <div className=\"flex flex-col\" style={{ gap: \"var(--spacing-xs)\" }}>\n <h3\n style={{\n fontFamily: \"var(--typo-body-xl-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-xl-size)\",\n fontWeight: 600,\n lineHeight: \"var(--typo-body-xl-line-height)\",\n color: \"var(--canvas-text)\",\n margin: 0,\n }}\n >\n {post.title}\n </h3>\n <p\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 400,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text)\",\n margin: 0,\n }}\n >\n {post.content}\n </p>\n </div>\n\n {/* Post image */}\n {post.imageUrl && (\n <div\n className=\"w-full overflow-hidden\"\n style={{\n maxWidth: 576,\n borderRadius: \"var(--radius-md)\",\n border: \"1px solid var(--canvas-border)\",\n }}\n >\n <img\n src={post.imageUrl}\n alt={post.title}\n className=\"w-full h-auto object-cover\"\n style={{ maxHeight: 375 }}\n />\n </div>\n )}\n\n {/* Comment button */}\n <div\n className=\"flex items-center justify-center w-full\"\n style={{\n borderTop: \"1px solid var(--canvas-border)\",\n paddingTop: \"var(--spacing-lg)\",\n paddingBottom: \"var(--spacing-lg)\",\n }}\n >\n <button\n type=\"button\"\n className=\"cursor-pointer flex items-center\"\n style={{\n gap: \"var(--spacing-sm)\",\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 500,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text-placeholder)\",\n }}\n >\n <MessageCircle className=\"w-5 h-5\" />\n Comment\n </button>\n </div>\n </div>\n </div>\n\n {/* Comment input */}\n <CommentInput\n avatarUrl={currentUser?.avatarUrl}\n avatarFallback={currentUser?.name?.charAt(0) || \"U\"}\n placeholder=\"Send a message\"\n buttonText=\"Send\"\n onSubmit={onComment}\n />\n\n {/* Comments section */}\n {post.comments && post.comments.length > 0 && (\n <div\n className=\"flex flex-col\"\n style={{\n paddingLeft: \"var(--spacing-7xl)\",\n paddingTop: \"var(--spacing-xl)\",\n gap: \"var(--spacing-md)\",\n }}\n >\n {post.comments.map((comment) => (\n <PostCommentItem\n key={comment.id}\n comment={comment}\n currentUser={currentUser}\n onReply={(content) => onReply?.(comment.id, content)}\n onLike={() => onLike?.(comment.id)}\n />\n ))}\n </div>\n )}\n </div>\n );\n}\n\n// ============================================\n// Main Component\n// ============================================\n\n/**\n * Canvas Design System - Upvoting Posts Table Block\n *\n * A social feed component with upvoting, comments, and nested replies.\n * Features header with sort/filter controls and action button.\n *\n * @example\n * ```tsx\n * <UpvotingPostsTable\n * title=\"My posts\"\n * subtitle=\"In the past year\"\n * posts={[...]}\n * onUpvote={(postId) => console.log(\"Upvoted\", postId)}\n * />\n * ```\n */\nexport function UpvotingPostsTable({\n title = \"My posts\",\n subtitle = \"In the past year\",\n posts = defaultPosts,\n currentUser = defaultCurrentUser,\n sortOptions = defaultSortOptions,\n filterOptions = defaultFilterOptions,\n actionButtonText = \"Add new\",\n onAddNew,\n onSort,\n onFilter,\n onUpvote,\n onComment,\n onReply,\n onLike,\n className,\n}: UpvotingPostsTableProps) {\n const [sortValue, setSortValue] = useState<string>(\"\");\n const [filterValue, setFilterValue] = useState<string>(\"\");\n\n const handleSortChange = (value: string) => {\n setSortValue(value);\n onSort?.(value);\n };\n\n const handleFilterChange = (value: string) => {\n setFilterValue(value);\n onFilter?.(value);\n };\n\n return (\n <div\n className={cn(\"flex flex-col w-full\", className)}\n style={{ gap: \"var(--spacing-xl)\" }}\n >\n {/* Header Section */}\n <div\n className=\"flex flex-wrap items-end w-full\"\n style={{ gap: \"var(--spacing-xl)\" }}\n >\n {/* Title and Subtitle */}\n <div\n className=\"flex flex-col flex-1 min-w-[200px]\"\n style={{ gap: \"var(--spacing-xs)\" }}\n >\n <h2\n style={{\n fontFamily: \"var(--typo-h6-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-h6-size)\",\n fontWeight: \"var(--typo-h6-weight)\",\n letterSpacing: \"var(--typo-h6-spacing)\",\n lineHeight: \"var(--typo-h6-line-height)\",\n color: \"var(--canvas-text)\",\n margin: 0,\n }}\n >\n {title}\n </h2>\n <p\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: \"var(--typo-body-s-weight)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text-muted)\",\n margin: 0,\n }}\n >\n {subtitle}\n </p>\n </div>\n\n {/* Controls */}\n <div\n className=\"flex items-end justify-end shrink-0 max-sm:w-full max-sm:flex-wrap\"\n style={{ gap: \"var(--spacing-3xl)\" }}\n >\n {/* Sort Dropdown */}\n <div className=\"w-[120px] max-sm:flex-1 max-sm:min-w-[120px]\">\n <Select value={sortValue || undefined} onValueChange={handleSortChange}>\n <SelectTrigger inputSize=\"sm\">\n <SelectValue placeholder=\"Sort\" />\n </SelectTrigger>\n <SelectContent>\n {sortOptions.map((option) => (\n <SelectItem key={option.id} value={option.id}>\n {option.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n\n {/* Filter Dropdown */}\n <div className=\"w-[120px] max-sm:flex-1 max-sm:min-w-[120px]\">\n <Select value={filterValue || undefined} onValueChange={handleFilterChange}>\n <SelectTrigger inputSize=\"sm\">\n <SelectValue placeholder=\"Filter\" />\n </SelectTrigger>\n <SelectContent>\n {filterOptions.map((option) => (\n <SelectItem key={option.id} value={option.id}>\n {option.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n\n {/* Action Button */}\n <Button\n variant=\"primary\"\n size=\"sm\"\n onClick={onAddNew}\n style={{\n height: \"var(--btn-small-height)\",\n paddingLeft: \"var(--btn-small-px)\",\n paddingRight: \"var(--btn-small-px)\",\n fontSize: \"var(--btn-small-font-size)\",\n borderRadius: \"var(--btn-small-radius)\",\n backgroundColor: \"var(--btn-primary-bg)\",\n color: \"var(--btn-primary-text)\",\n borderColor: \"var(--btn-primary-border)\",\n }}\n >\n {actionButtonText}\n </Button>\n </div>\n </div>\n\n {/* Posts List */}\n <div className=\"flex flex-col w-full\">\n {posts.map((post) => (\n <PostCard\n key={post.id}\n post={post}\n currentUser={currentUser}\n onUpvote={() => onUpvote?.(post.id)}\n onComment={(content) => onComment?.(post.id, content)}\n onReply={(commentId, content) => onReply?.(post.id, commentId, content)}\n onLike={(commentId) => onLike?.(post.id, commentId)}\n />\n ))}\n </div>\n </div>\n );\n}\n"
10
10
  }
11
11
  ],
12
12
  "dependencies": [