canvas-ui-sdk 0.3.8 → 0.3.9

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.
Files changed (32) hide show
  1. package/dist/index.js +65 -63
  2. package/dist/index.js.map +1 -1
  3. package/package.json +1 -1
  4. package/registry/blocks/component-palette.json +1 -1
  5. package/registry/blocks/component-search.json +1 -1
  6. package/registry/blocks/custom-component-helper.json +1 -1
  7. package/registry/blocks/faqs-table.json +1 -1
  8. package/registry/blocks/infinity-canvas.json +1 -1
  9. package/registry/blocks/menu-section.json +1 -1
  10. package/registry/blocks/messenger-sidebar.json +1 -1
  11. package/registry/blocks/mobile-bottom-nav.json +1 -1
  12. package/registry/blocks/pagination.json +1 -1
  13. package/registry/blocks/pill-tabs.json +1 -1
  14. package/registry/blocks/pricing-cards.json +1 -1
  15. package/registry/blocks/profile-card.json +1 -1
  16. package/registry/blocks/prompt-template.json +1 -1
  17. package/registry/blocks/screen-flowchart.json +1 -1
  18. package/registry/blocks/screen-prompt-builder.json +1 -1
  19. package/registry/blocks/screen-prompt-template.json +1 -1
  20. package/registry/blocks/sidebar-cards.json +1 -1
  21. package/registry/blocks/slideshow-grid-tiles.json +1 -1
  22. package/registry/blocks/social-feed.json +1 -1
  23. package/registry/blocks/upvoting-posts-table.json +1 -1
  24. package/registry/layout/double-sidebar.json +1 -1
  25. package/registry/layout/header.json +1 -1
  26. package/registry/layout/icon-sidebar.json +1 -1
  27. package/registry/layout/project-context-shell.json +1 -1
  28. package/registry/layout/sidebar-nav.json +1 -1
  29. package/registry/ui/button.json +1 -1
  30. package/registry/ui/line-tabs.json +1 -1
  31. package/registry/ui/selectable-pills.json +1 -1
  32. package/registry/ui/tabs.json +1 -1
@@ -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\" style={{ color: \"var(--canvas-text)\" }}>\n <Paperclip className=\"w-5 h-5\" />\n </button>\n <button type=\"button\" style={{ color: \"var(--canvas-text)\" }}>\n <Video className=\"w-5 h-5\" />\n </button>\n <button type=\"button\" 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 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} style={{ color: \"var(--canvas-text)\" }}>\n <MessageCircle className=\"w-5 h-5\" />\n </button>\n <button type=\"button\" onClick={onRepost} style={{ color: \"var(--canvas-text)\" }}>\n <RefreshCw className=\"w-5 h-5\" />\n </button>\n <button type=\"button\" onClick={onShare} 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=\"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=\"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-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"
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=\"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=\"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=\"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 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=\"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=\"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-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"
10
10
  }
11
11
  ],
12
12
  "dependencies": [
@@ -6,7 +6,7 @@
6
6
  {
7
7
  "path": "components/layout/double-sidebar.tsx",
8
8
  "type": "registry:layout",
9
- "content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport { LucideIcon, Home, Users, Calendar, MessageSquare, PieChart, FileText, ShoppingBag } from \"lucide-react\";\nimport { ScrollArea } from \"../ui/scroll-area\";\nimport { useThemeImages, useThemeBranding } from \"../../context/theme-context\";\n\n// Phosphor Icons for Logo\nimport { Buildings, type Icon as PhosphorIcon } from \"@phosphor-icons/react\";\nimport {\n Diamond, Hexagon, Star, Lightning, Sparkle, Infinity, Code, Terminal, Cpu,\n Database, Globe, Cloud, WifiHigh, Briefcase, Storefront, Handshake, ChartLine,\n Palette as PaletteIcon, PencilSimple, Camera, MusicNote, Lightbulb, Leaf, Tree,\n Sun, Moon, Fire, Drop, ChatCircle, Envelope, Phone, Megaphone, Heart, Shield,\n Trophy, Rocket, Target, Flag,\n} from \"@phosphor-icons/react\";\n\n// ============================================\n// Icon Shape Presets for Logo Creator\n// ============================================\n\ntype IconShapeId = \"rounded\" | \"circle\" | \"square\";\n\ninterface IconShape {\n id: IconShapeId;\n renderBackground: (bgColor: string) => React.ReactNode;\n}\n\nconst iconShapes: IconShape[] = [\n {\n id: \"rounded\",\n renderBackground: (bgColor: string) => (\n <svg viewBox=\"0 0 32 32\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\" className=\"size-full absolute inset-0\">\n <rect width=\"32\" height=\"32\" rx=\"10\" style={{ fill: bgColor }} />\n </svg>\n ),\n },\n {\n id: \"circle\",\n renderBackground: (bgColor: string) => (\n <svg viewBox=\"0 0 32 32\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\" className=\"size-full absolute inset-0\">\n <circle cx=\"16\" cy=\"16\" r=\"16\" style={{ fill: bgColor }} />\n </svg>\n ),\n },\n {\n id: \"square\",\n renderBackground: (bgColor: string) => (\n <svg viewBox=\"0 0 32 32\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\" className=\"size-full absolute inset-0\">\n <rect width=\"32\" height=\"32\" style={{ fill: bgColor }} />\n </svg>\n ),\n },\n];\n\n// Map icon names to components\nconst iconMap: Record<string, PhosphorIcon> = {\n Diamond, Hexagon, Star, Lightning, Sparkle, Infinity, Code, Terminal, Cpu,\n Database, Globe, Cloud, WifiHigh, Briefcase, Buildings, Storefront, Handshake,\n ChartLine, Palette: PaletteIcon, PencilSimple, Camera, MusicNote, Lightbulb,\n Leaf, Tree, Sun, Moon, Fire, Drop, ChatCircle, Envelope, Phone, Megaphone,\n Heart, Shield, Trophy, Rocket, Target, Flag,\n};\n\n// Helper to resolve CSS variable references to actual hex colors\nfunction resolveBrandingColor(value: string): string {\n if (!value) return \"#ffffff\";\n if (value.startsWith(\"var(\")) {\n const varName = value.replace(\"var(\", \"\").replace(\")\", \"\");\n if (typeof window !== \"undefined\") {\n const computed = getComputedStyle(document.documentElement).getPropertyValue(varName).trim();\n return computed || \"#ffffff\";\n }\n return \"#ffffff\";\n }\n return value;\n}\n\n// ============================================\n// Types\n// ============================================\n\nexport interface NavTab {\n id: string;\n label: string;\n isActive?: boolean;\n}\n\nexport interface DoubleSidebarSection {\n id: string;\n icon: LucideIcon;\n label: string;\n badge?: boolean;\n tabs: NavTab[];\n}\n\n// ============================================\n// Default Sections\n// ============================================\n\nexport const defaultDoubleSidebarSections: DoubleSidebarSection[] = [\n {\n id: \"home\",\n icon: Home,\n label: \"Home\",\n tabs: [\n { id: \"tab1\", label: \"Tab 1\", isActive: true },\n { id: \"tab2\", label: \"Tab 2\" },\n { id: \"tab3\", label: \"Tab 3\" },\n { id: \"tab4\", label: \"Tab 4\" },\n { id: \"tab5\", label: \"Tab 5\" },\n { id: \"tab6\", label: \"Tab 6\" },\n ],\n },\n {\n id: \"teams\",\n icon: Users,\n label: \"Teams\",\n tabs: [\n { id: \"all-teams\", label: \"All Teams\" },\n { id: \"my-team\", label: \"My Team\" },\n { id: \"invites\", label: \"Invites\" },\n ],\n },\n {\n id: \"calendar\",\n icon: Calendar,\n label: \"Calendar\",\n tabs: [\n { id: \"schedule\", label: \"Schedule\" },\n { id: \"events\", label: \"Events\" },\n { id: \"reminders\", label: \"Reminders\" },\n ],\n },\n {\n id: \"messages\",\n icon: MessageSquare,\n label: \"Messages\",\n badge: true,\n tabs: [\n { id: \"inbox\", label: \"Inbox\" },\n { id: \"sent\", label: \"Sent\" },\n { id: \"drafts\", label: \"Drafts\" },\n { id: \"archived\", label: \"Archived\" },\n ],\n },\n {\n id: \"reports\",\n icon: PieChart,\n label: \"Reports\",\n tabs: [\n { id: \"overview\", label: \"Overview\" },\n { id: \"analytics\", label: \"Analytics\" },\n { id: \"exports\", label: \"Exports\" },\n ],\n },\n {\n id: \"docs\",\n icon: FileText,\n label: \"Docs\",\n tabs: [\n { id: \"recent\", label: \"Recent\" },\n { id: \"shared\", label: \"Shared\" },\n { id: \"favorites\", label: \"Favorites\" },\n ],\n },\n {\n id: \"orders\",\n icon: ShoppingBag,\n label: \"Orders\",\n tabs: [\n { id: \"pending\", label: \"Pending\" },\n { id: \"completed\", label: \"Completed\" },\n { id: \"cancelled\", label: \"Cancelled\" },\n ],\n },\n];\n\n// ============================================\n// Icon Column Item\n// ============================================\n\ninterface IconColumnItemProps {\n section: DoubleSidebarSection;\n isActive: boolean;\n variant: \"dark\" | \"light\";\n onClick: () => void;\n}\n\nfunction IconColumnItem({ section, isActive, variant, onClick }: IconColumnItemProps) {\n const Icon = section.icon;\n const isDark = variant === \"dark\";\n\n return (\n <button\n onClick={onClick}\n className={cn(\n \"relative flex flex-col items-center justify-center gap-1 w-11 h-11 rounded-[var(--radius-nav)] transition-colors\",\n // Dark variant\n isDark && isActive && \"bg-[var(--canvas-sidebar-dark-active-bg)]\",\n isDark && !isActive && \"hover:bg-[var(--canvas-sidebar-dark-active-bg)]/50\",\n // Light variant\n !isDark && isActive && \"bg-[var(--canvas-sidebar-light-active-bg)]\",\n !isDark && !isActive && \"hover:bg-[var(--canvas-sidebar-light-active-bg)]/50\"\n )}\n >\n <Icon\n className={cn(\n \"size-4\",\n isDark && isActive && \"text-[var(--canvas-sidebar-dark-active-text)]\",\n isDark && !isActive && \"text-[var(--canvas-sidebar-dark-text)]\",\n !isDark && isActive && \"text-[var(--canvas-sidebar-light-active-text)]\",\n !isDark && !isActive && \"text-[var(--canvas-sidebar-light-text)]\"\n )}\n />\n {/* Notification Badge */}\n {section.badge && (\n <div className=\"absolute top-1.5 right-1.5 size-1.5 rounded-full bg-[var(--canvas-destructive)]\" />\n )}\n </button>\n );\n}\n\n// ============================================\n// Nav Column Tab Item\n// ============================================\n\ninterface NavTabItemProps {\n tab: NavTab;\n isActive: boolean;\n variant: \"dark\" | \"light\";\n onClick: () => void;\n}\n\nfunction NavTabItem({ tab, isActive, variant, onClick }: NavTabItemProps) {\n const isDark = variant === \"dark\";\n\n return (\n <button\n onClick={onClick}\n className={cn(\n \"flex items-center gap-[var(--spacing-md)] h-11 px-[var(--spacing-xl)] rounded-[var(--radius-nav)] w-full text-left transition-colors\",\n // Dark variant\n isDark && isActive && \"bg-[var(--canvas-sidebar-dark-active-bg)] text-[var(--canvas-sidebar-dark-active-text)]\",\n isDark && !isActive && \"text-[var(--canvas-sidebar-dark-text)] hover:bg-[var(--canvas-sidebar-dark-active-bg)]/50\",\n // Light variant\n !isDark && isActive && \"bg-[var(--canvas-sidebar-light-active-bg)] text-[var(--canvas-sidebar-light-active-text)]\",\n !isDark && !isActive && \"text-[var(--canvas-sidebar-light-text)] hover:bg-[var(--canvas-sidebar-light-active-bg)]/50\"\n )}\n >\n <span \n className=\"flex-1 truncate\"\n style={{\n fontFamily: \"var(--typo-sidebar-tab-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-sidebar-tab-size)\",\n fontWeight: \"var(--typo-sidebar-tab-weight)\",\n letterSpacing: \"var(--typo-sidebar-tab-spacing)\",\n lineHeight: \"var(--typo-sidebar-tab-line-height)\",\n }}\n >\n {tab.label}\n </span>\n </button>\n );\n}\n\n// ============================================\n// Double Sidebar Component\n// ============================================\n\ninterface DoubleSidebarProps {\n /** Navigation sections - each section maps to an icon and has tabs */\n sections?: DoubleSidebarSection[];\n /** Visual variant for the icon column */\n iconVariant?: \"dark\" | \"light\";\n /** Visual variant for the navigation column */\n navVariant?: \"dark\" | \"light\";\n /** Callback when a tab is clicked */\n onTabClick?: (section: DoubleSidebarSection, tab: NavTab) => void;\n /** Callback when closing (for mobile sheet) */\n onClose?: () => void;\n /** Additional class names */\n className?: string;\n}\n\n/**\n * Canvas Design System - Double Sidebar Component\n * \n * A two-column sidebar where:\n * - Left column (96px): Icon buttons that switch the active section\n * - Right column (280px): Text-based navigation tabs for the active section\n * \n * Each column can be independently themed (light/dark).\n */\nexport function DoubleSidebar({\n sections = defaultDoubleSidebarSections,\n iconVariant = \"light\",\n navVariant = \"light\",\n onTabClick,\n onClose,\n className\n}: DoubleSidebarProps) {\n const [activeSectionId, setActiveSectionId] = useState(sections[0]?.id || \"\");\n const [activeTabId, setActiveTabId] = useState(sections[0]?.tabs[0]?.id || \"\");\n \n const themeImages = useThemeImages();\n const { branding, isMounted } = useThemeBranding();\n\n const activeSection = sections.find(s => s.id === activeSectionId);\n const isIconDark = iconVariant === \"dark\";\n const isNavDark = navVariant === \"dark\";\n\n // Get the appropriate logo based on icon column variant\n const logoUrl = isIconDark ? themeImages.logoDark : themeImages.logoLight;\n\n // Get the icon shape renderer\n const iconShape = iconShapes.find(s => s.id === branding.iconShape) || iconShapes[0];\n\n const handleSectionClick = (section: DoubleSidebarSection) => {\n setActiveSectionId(section.id);\n // Set first tab of the section as active\n if (section.tabs.length > 0) {\n setActiveTabId(section.tabs[0].id);\n }\n };\n\n const handleTabClick = (tab: NavTab) => {\n setActiveTabId(tab.id);\n if (activeSection) {\n onTabClick?.(activeSection, tab);\n onClose?.();\n }\n };\n\n return (\n <div className={cn(\"flex h-full\", className)}>\n {/* Icon Column (96px) */}\n <div\n className={cn(\n \"flex flex-col items-center w-[var(--icon-sidebar-width)] shrink-0\",\n isIconDark && \"bg-[var(--canvas-sidebar-dark-bg)] border-r border-[var(--canvas-sidebar-dark-border)]\",\n !isIconDark && \"bg-[var(--canvas-background)] border-r border-[var(--canvas-border)]\"\n )}\n >\n {/* Logo - Hidden until mounted to prevent hydration flash */}\n <div className={`flex items-center justify-center shrink-0 py-5 ${isMounted ? 'opacity-100' : 'opacity-0'}`}>\n {logoUrl ? (\n <img\n src={logoUrl}\n alt=\"Logo\"\n className=\"size-8 object-contain\"\n />\n ) : (\n // Uses CSS variables directly - no JavaScript resolution needed\n <div className=\"relative size-8 shrink-0\">\n {iconShape.renderBackground(branding.bgColor || \"var(--canvas-primary)\")}\n <div className=\"absolute inset-0 flex items-center justify-center z-10\">\n {(() => {\n const IconComponent = iconMap[branding.iconName || \"Buildings\"] || Buildings;\n return <IconComponent weight=\"bold\" size={18} color={branding.iconColor || \"var(--canvas-primary-foreground)\"} />;\n })()}\n </div>\n </div>\n )}\n </div>\n\n {/* Icon Navigation */}\n <nav className=\"flex flex-col items-center gap-1 flex-1 px-4 pb-5 mt-2\">\n {sections.map((section) => (\n <IconColumnItem\n key={section.id}\n section={section}\n isActive={section.id === activeSectionId}\n variant={iconVariant}\n onClick={() => handleSectionClick(section)}\n />\n ))}\n </nav>\n </div>\n\n {/* Navigation Column (280px) */}\n <div\n className={cn(\n \"flex flex-col w-[var(--nav-sidebar-width)]\",\n isNavDark && \"bg-[var(--canvas-sidebar-dark-bg)] border-r border-[var(--canvas-sidebar-dark-border)]\",\n !isNavDark && \"bg-[var(--canvas-background)] border-r border-[var(--canvas-border)] shadow-[0_4px_16px_0_rgba(0,0,0,0.04)]\"\n )}\n >\n {/* Navigation Content */}\n <ScrollArea className=\"flex-1 pt-[var(--header-height)] px-[var(--spacing-2xl)] pb-[var(--spacing-5xl)]\">\n <nav className=\"flex flex-col gap-0\">\n {activeSection?.tabs.map((tab) => (\n <NavTabItem\n key={tab.id}\n tab={tab}\n isActive={tab.id === activeTabId}\n variant={navVariant}\n onClick={() => handleTabClick(tab)}\n />\n ))}\n </nav>\n </ScrollArea>\n </div>\n </div>\n );\n}\n\n"
9
+ "content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport { LucideIcon, Home, Users, Calendar, MessageSquare, PieChart, FileText, ShoppingBag } from \"lucide-react\";\nimport { ScrollArea } from \"../ui/scroll-area\";\nimport { useThemeImages, useThemeBranding } from \"../../context/theme-context\";\n\n// Phosphor Icons for Logo\nimport { Buildings, type Icon as PhosphorIcon } from \"@phosphor-icons/react\";\nimport {\n Diamond, Hexagon, Star, Lightning, Sparkle, Infinity, Code, Terminal, Cpu,\n Database, Globe, Cloud, WifiHigh, Briefcase, Storefront, Handshake, ChartLine,\n Palette as PaletteIcon, PencilSimple, Camera, MusicNote, Lightbulb, Leaf, Tree,\n Sun, Moon, Fire, Drop, ChatCircle, Envelope, Phone, Megaphone, Heart, Shield,\n Trophy, Rocket, Target, Flag,\n} from \"@phosphor-icons/react\";\n\n// ============================================\n// Icon Shape Presets for Logo Creator\n// ============================================\n\ntype IconShapeId = \"rounded\" | \"circle\" | \"square\";\n\ninterface IconShape {\n id: IconShapeId;\n renderBackground: (bgColor: string) => React.ReactNode;\n}\n\nconst iconShapes: IconShape[] = [\n {\n id: \"rounded\",\n renderBackground: (bgColor: string) => (\n <svg viewBox=\"0 0 32 32\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\" className=\"size-full absolute inset-0\">\n <rect width=\"32\" height=\"32\" rx=\"10\" style={{ fill: bgColor }} />\n </svg>\n ),\n },\n {\n id: \"circle\",\n renderBackground: (bgColor: string) => (\n <svg viewBox=\"0 0 32 32\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\" className=\"size-full absolute inset-0\">\n <circle cx=\"16\" cy=\"16\" r=\"16\" style={{ fill: bgColor }} />\n </svg>\n ),\n },\n {\n id: \"square\",\n renderBackground: (bgColor: string) => (\n <svg viewBox=\"0 0 32 32\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\" className=\"size-full absolute inset-0\">\n <rect width=\"32\" height=\"32\" style={{ fill: bgColor }} />\n </svg>\n ),\n },\n];\n\n// Map icon names to components\nconst iconMap: Record<string, PhosphorIcon> = {\n Diamond, Hexagon, Star, Lightning, Sparkle, Infinity, Code, Terminal, Cpu,\n Database, Globe, Cloud, WifiHigh, Briefcase, Buildings, Storefront, Handshake,\n ChartLine, Palette: PaletteIcon, PencilSimple, Camera, MusicNote, Lightbulb,\n Leaf, Tree, Sun, Moon, Fire, Drop, ChatCircle, Envelope, Phone, Megaphone,\n Heart, Shield, Trophy, Rocket, Target, Flag,\n};\n\n// Helper to resolve CSS variable references to actual hex colors\nfunction resolveBrandingColor(value: string): string {\n if (!value) return \"#ffffff\";\n if (value.startsWith(\"var(\")) {\n const varName = value.replace(\"var(\", \"\").replace(\")\", \"\");\n if (typeof window !== \"undefined\") {\n const computed = getComputedStyle(document.documentElement).getPropertyValue(varName).trim();\n return computed || \"#ffffff\";\n }\n return \"#ffffff\";\n }\n return value;\n}\n\n// ============================================\n// Types\n// ============================================\n\nexport interface NavTab {\n id: string;\n label: string;\n isActive?: boolean;\n}\n\nexport interface DoubleSidebarSection {\n id: string;\n icon: LucideIcon;\n label: string;\n badge?: boolean;\n tabs: NavTab[];\n}\n\n// ============================================\n// Default Sections\n// ============================================\n\nexport const defaultDoubleSidebarSections: DoubleSidebarSection[] = [\n {\n id: \"home\",\n icon: Home,\n label: \"Home\",\n tabs: [\n { id: \"tab1\", label: \"Tab 1\", isActive: true },\n { id: \"tab2\", label: \"Tab 2\" },\n { id: \"tab3\", label: \"Tab 3\" },\n { id: \"tab4\", label: \"Tab 4\" },\n { id: \"tab5\", label: \"Tab 5\" },\n { id: \"tab6\", label: \"Tab 6\" },\n ],\n },\n {\n id: \"teams\",\n icon: Users,\n label: \"Teams\",\n tabs: [\n { id: \"all-teams\", label: \"All Teams\" },\n { id: \"my-team\", label: \"My Team\" },\n { id: \"invites\", label: \"Invites\" },\n ],\n },\n {\n id: \"calendar\",\n icon: Calendar,\n label: \"Calendar\",\n tabs: [\n { id: \"schedule\", label: \"Schedule\" },\n { id: \"events\", label: \"Events\" },\n { id: \"reminders\", label: \"Reminders\" },\n ],\n },\n {\n id: \"messages\",\n icon: MessageSquare,\n label: \"Messages\",\n badge: true,\n tabs: [\n { id: \"inbox\", label: \"Inbox\" },\n { id: \"sent\", label: \"Sent\" },\n { id: \"drafts\", label: \"Drafts\" },\n { id: \"archived\", label: \"Archived\" },\n ],\n },\n {\n id: \"reports\",\n icon: PieChart,\n label: \"Reports\",\n tabs: [\n { id: \"overview\", label: \"Overview\" },\n { id: \"analytics\", label: \"Analytics\" },\n { id: \"exports\", label: \"Exports\" },\n ],\n },\n {\n id: \"docs\",\n icon: FileText,\n label: \"Docs\",\n tabs: [\n { id: \"recent\", label: \"Recent\" },\n { id: \"shared\", label: \"Shared\" },\n { id: \"favorites\", label: \"Favorites\" },\n ],\n },\n {\n id: \"orders\",\n icon: ShoppingBag,\n label: \"Orders\",\n tabs: [\n { id: \"pending\", label: \"Pending\" },\n { id: \"completed\", label: \"Completed\" },\n { id: \"cancelled\", label: \"Cancelled\" },\n ],\n },\n];\n\n// ============================================\n// Icon Column Item\n// ============================================\n\ninterface IconColumnItemProps {\n section: DoubleSidebarSection;\n isActive: boolean;\n variant: \"dark\" | \"light\";\n onClick: () => void;\n}\n\nfunction IconColumnItem({ section, isActive, variant, onClick }: IconColumnItemProps) {\n const Icon = section.icon;\n const isDark = variant === \"dark\";\n\n return (\n <button\n onClick={onClick}\n className={cn(\n \"cursor-pointer relative flex flex-col items-center justify-center gap-1 w-11 h-11 rounded-[var(--radius-nav)] transition-colors\",\n // Dark variant\n isDark && isActive && \"bg-[var(--canvas-sidebar-dark-active-bg)]\",\n isDark && !isActive && \"hover:bg-[var(--canvas-sidebar-dark-active-bg)]/50\",\n // Light variant\n !isDark && isActive && \"bg-[var(--canvas-sidebar-light-active-bg)]\",\n !isDark && !isActive && \"hover:bg-[var(--canvas-sidebar-light-active-bg)]/50\"\n )}\n >\n <Icon\n className={cn(\n \"size-4\",\n isDark && isActive && \"text-[var(--canvas-sidebar-dark-active-text)]\",\n isDark && !isActive && \"text-[var(--canvas-sidebar-dark-text)]\",\n !isDark && isActive && \"text-[var(--canvas-sidebar-light-active-text)]\",\n !isDark && !isActive && \"text-[var(--canvas-sidebar-light-text)]\"\n )}\n />\n {/* Notification Badge */}\n {section.badge && (\n <div className=\"absolute top-1.5 right-1.5 size-1.5 rounded-full bg-[var(--canvas-destructive)]\" />\n )}\n </button>\n );\n}\n\n// ============================================\n// Nav Column Tab Item\n// ============================================\n\ninterface NavTabItemProps {\n tab: NavTab;\n isActive: boolean;\n variant: \"dark\" | \"light\";\n onClick: () => void;\n}\n\nfunction NavTabItem({ tab, isActive, variant, onClick }: NavTabItemProps) {\n const isDark = variant === \"dark\";\n\n return (\n <button\n onClick={onClick}\n className={cn(\n \"cursor-pointer flex items-center gap-[var(--spacing-md)] h-11 px-[var(--spacing-xl)] rounded-[var(--radius-nav)] w-full text-left transition-colors\",\n // Dark variant\n isDark && isActive && \"bg-[var(--canvas-sidebar-dark-active-bg)] text-[var(--canvas-sidebar-dark-active-text)]\",\n isDark && !isActive && \"text-[var(--canvas-sidebar-dark-text)] hover:bg-[var(--canvas-sidebar-dark-active-bg)]/50\",\n // Light variant\n !isDark && isActive && \"bg-[var(--canvas-sidebar-light-active-bg)] text-[var(--canvas-sidebar-light-active-text)]\",\n !isDark && !isActive && \"text-[var(--canvas-sidebar-light-text)] hover:bg-[var(--canvas-sidebar-light-active-bg)]/50\"\n )}\n >\n <span \n className=\"flex-1 truncate\"\n style={{\n fontFamily: \"var(--typo-sidebar-tab-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-sidebar-tab-size)\",\n fontWeight: \"var(--typo-sidebar-tab-weight)\",\n letterSpacing: \"var(--typo-sidebar-tab-spacing)\",\n lineHeight: \"var(--typo-sidebar-tab-line-height)\",\n }}\n >\n {tab.label}\n </span>\n </button>\n );\n}\n\n// ============================================\n// Double Sidebar Component\n// ============================================\n\ninterface DoubleSidebarProps {\n /** Navigation sections - each section maps to an icon and has tabs */\n sections?: DoubleSidebarSection[];\n /** Visual variant for the icon column */\n iconVariant?: \"dark\" | \"light\";\n /** Visual variant for the navigation column */\n navVariant?: \"dark\" | \"light\";\n /** Callback when a tab is clicked */\n onTabClick?: (section: DoubleSidebarSection, tab: NavTab) => void;\n /** Callback when closing (for mobile sheet) */\n onClose?: () => void;\n /** Additional class names */\n className?: string;\n}\n\n/**\n * Canvas Design System - Double Sidebar Component\n * \n * A two-column sidebar where:\n * - Left column (96px): Icon buttons that switch the active section\n * - Right column (280px): Text-based navigation tabs for the active section\n * \n * Each column can be independently themed (light/dark).\n */\nexport function DoubleSidebar({\n sections = defaultDoubleSidebarSections,\n iconVariant = \"light\",\n navVariant = \"light\",\n onTabClick,\n onClose,\n className\n}: DoubleSidebarProps) {\n const [activeSectionId, setActiveSectionId] = useState(sections[0]?.id || \"\");\n const [activeTabId, setActiveTabId] = useState(sections[0]?.tabs[0]?.id || \"\");\n \n const themeImages = useThemeImages();\n const { branding, isMounted } = useThemeBranding();\n\n const activeSection = sections.find(s => s.id === activeSectionId);\n const isIconDark = iconVariant === \"dark\";\n const isNavDark = navVariant === \"dark\";\n\n // Get the appropriate logo based on icon column variant\n const logoUrl = isIconDark ? themeImages.logoDark : themeImages.logoLight;\n\n // Get the icon shape renderer\n const iconShape = iconShapes.find(s => s.id === branding.iconShape) || iconShapes[0];\n\n const handleSectionClick = (section: DoubleSidebarSection) => {\n setActiveSectionId(section.id);\n // Set first tab of the section as active\n if (section.tabs.length > 0) {\n setActiveTabId(section.tabs[0].id);\n }\n };\n\n const handleTabClick = (tab: NavTab) => {\n setActiveTabId(tab.id);\n if (activeSection) {\n onTabClick?.(activeSection, tab);\n onClose?.();\n }\n };\n\n return (\n <div className={cn(\"flex h-full\", className)}>\n {/* Icon Column (96px) */}\n <div\n className={cn(\n \"flex flex-col items-center w-[var(--icon-sidebar-width)] shrink-0\",\n isIconDark && \"bg-[var(--canvas-sidebar-dark-bg)] border-r border-[var(--canvas-sidebar-dark-border)]\",\n !isIconDark && \"bg-[var(--canvas-background)] border-r border-[var(--canvas-border)]\"\n )}\n >\n {/* Logo - Hidden until mounted to prevent hydration flash */}\n <div className={`flex items-center justify-center shrink-0 py-5 ${isMounted ? 'opacity-100' : 'opacity-0'}`}>\n {logoUrl ? (\n <img\n src={logoUrl}\n alt=\"Logo\"\n className=\"size-8 object-contain\"\n />\n ) : (\n // Uses CSS variables directly - no JavaScript resolution needed\n <div className=\"relative size-8 shrink-0\">\n {iconShape.renderBackground(branding.bgColor || \"var(--canvas-primary)\")}\n <div className=\"absolute inset-0 flex items-center justify-center z-10\">\n {(() => {\n const IconComponent = iconMap[branding.iconName || \"Buildings\"] || Buildings;\n return <IconComponent weight=\"bold\" size={18} color={branding.iconColor || \"var(--canvas-primary-foreground)\"} />;\n })()}\n </div>\n </div>\n )}\n </div>\n\n {/* Icon Navigation */}\n <nav className=\"flex flex-col items-center gap-1 flex-1 px-4 pb-5 mt-2\">\n {sections.map((section) => (\n <IconColumnItem\n key={section.id}\n section={section}\n isActive={section.id === activeSectionId}\n variant={iconVariant}\n onClick={() => handleSectionClick(section)}\n />\n ))}\n </nav>\n </div>\n\n {/* Navigation Column (280px) */}\n <div\n className={cn(\n \"flex flex-col w-[var(--nav-sidebar-width)]\",\n isNavDark && \"bg-[var(--canvas-sidebar-dark-bg)] border-r border-[var(--canvas-sidebar-dark-border)]\",\n !isNavDark && \"bg-[var(--canvas-background)] border-r border-[var(--canvas-border)] shadow-[0_4px_16px_0_rgba(0,0,0,0.04)]\"\n )}\n >\n {/* Navigation Content */}\n <ScrollArea className=\"flex-1 pt-[var(--header-height)] px-[var(--spacing-2xl)] pb-[var(--spacing-5xl)]\">\n <nav className=\"flex flex-col gap-0\">\n {activeSection?.tabs.map((tab) => (\n <NavTabItem\n key={tab.id}\n tab={tab}\n isActive={tab.id === activeTabId}\n variant={navVariant}\n onClick={() => handleTabClick(tab)}\n />\n ))}\n </nav>\n </ScrollArea>\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/layout/header.tsx",
8
8
  "type": "registry:layout",
9
- "content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { Search, Bell, ShoppingCart, Menu, User, LogOut, MessageSquare, X, Home, Info, LayoutGrid, type LucideIcon } from \"lucide-react\";\nimport { Avatar, AvatarFallback, AvatarImage } from \"../ui/avatar\";\nimport { Button } from \"../ui/button\";\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuTrigger,\n} from \"../ui/dropdown-menu\";\nimport {\n Popover,\n PopoverContent,\n PopoverTrigger,\n} from \"../ui/popover\";\nimport { useThemeBranding } from \"../../context/theme-context\";\n\n// ============================================\n// Cart Types\n// ============================================\n\nexport interface CartItem {\n id: string;\n name: string;\n price: number;\n image: string;\n}\n\n// Sample cart items for demo\nconst defaultCartItems: CartItem[] = [\n {\n id: \"1\",\n name: \"Julian Bag\",\n price: 120,\n image: \"https://images.unsplash.com/photo-1591561954557-26941169b49e?w=150&h=150&fit=crop\",\n },\n {\n id: \"2\",\n name: \"Davis Keychain\",\n price: 60,\n image: \"https://images.unsplash.com/photo-1606107557195-0e29a4b5b4aa?w=150&h=150&fit=crop&crop=center\",\n },\n];\n\n// ============================================\n// Message Types\n// ============================================\n\nexport interface Message {\n id: string;\n senderName: string;\n senderAvatar: string;\n timestamp: string;\n}\n\n// Sample messages for demo\nconst defaultMessages: Message[] = [\n {\n id: \"1\",\n senderName: \"Jeff Conner\",\n senderAvatar: \"https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=100&h=100&fit=crop&crop=face\",\n timestamp: \"Jun 5, 2023 8:13 AM\",\n },\n {\n id: \"2\",\n senderName: \"Emma Pérez\",\n senderAvatar: \"https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=100&h=100&fit=crop&crop=face\",\n timestamp: \"May 2, 2023 11:54 AM\",\n },\n {\n id: \"3\",\n senderName: \"Raj Mishra\",\n senderAvatar: \"https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=100&h=100&fit=crop&crop=face\",\n timestamp: \"Jan 10, 2023 5:22 PM\",\n },\n {\n id: \"4\",\n senderName: \"John Freidman\",\n senderAvatar: \"https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=100&h=100&fit=crop&crop=face\",\n timestamp: \"Dec 20, 2022 2:22 PM\",\n },\n];\n\n// ============================================\n// Notification Types\n// ============================================\n\nexport interface Notification {\n id: string;\n userName: string;\n userAvatar: string;\n action: string;\n timestamp: string;\n}\n\n// Sample notifications for demo\nconst defaultNotifications: Notification[] = [\n {\n id: \"1\",\n userName: \"Aya Williams\",\n userAvatar: \"https://images.unsplash.com/photo-1531746020798-e6953c6e8e04?w=100&h=100&fit=crop&crop=face\",\n action: \"liked your photo\",\n timestamp: \"Apr 15, 2023 6:21 AM\",\n },\n {\n id: \"2\",\n userName: \"Francis Gaddi\",\n userAvatar: \"https://images.unsplash.com/photo-1506794778202-cad84cf45f1d?w=100&h=100&fit=crop&crop=face\",\n action: \"liked your photo\",\n timestamp: \"Jun 10, 2023 5:45 PM\",\n },\n {\n id: \"3\",\n userName: \"Stacy Jones\",\n userAvatar: \"https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=100&h=100&fit=crop&crop=face\",\n action: \"liked your photo\",\n timestamp: \"May 9, 2023 2:00 AM\",\n },\n {\n id: \"4\",\n userName: \"Gabi del Rosario\",\n userAvatar: \"https://images.unsplash.com/photo-1544005313-94ddf0286df2?w=100&h=100&fit=crop&crop=face\",\n action: \"liked your photo\",\n timestamp: \"Apr 8, 2023 8:55 PM\",\n },\n];\n\n// ============================================\n// Navigation Types\n// ============================================\n\nexport interface NavItem {\n id: string;\n label: string;\n icon?: LucideIcon;\n href?: string;\n onClick?: () => void;\n}\n\n// Default navigation items\nconst defaultNavItems: NavItem[] = [\n { id: \"home\", label: \"Home\", icon: Home },\n { id: \"about\", label: \"About\", icon: Info },\n { id: \"dashboard\", label: \"Dashboard\", icon: LayoutGrid },\n];\n\n// Phosphor Icons for Logo\nimport { Buildings, type Icon as PhosphorIcon } from \"@phosphor-icons/react\";\nimport {\n Diamond, Hexagon, Star, Lightning, Sparkle, Infinity, Code, Terminal, Cpu,\n Database, Globe, Cloud, WifiHigh, Briefcase, Storefront, Handshake, ChartLine,\n Palette as PaletteIcon, PencilSimple, Camera, MusicNote, Lightbulb, Leaf, Tree,\n Sun, Moon, Fire, Drop, ChatCircle, Envelope, Phone, Megaphone, Heart, Shield,\n Trophy, Rocket, Target, Flag,\n} from \"@phosphor-icons/react\";\n\n// Icon shape renderers - use style attribute for CSS variable support\nconst iconShapes = {\n rounded: (bgColor: string) => (\n <svg viewBox=\"0 0 32 32\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\" className=\"size-full absolute inset-0\">\n <rect width=\"32\" height=\"32\" rx=\"10\" style={{ fill: bgColor }} />\n </svg>\n ),\n circle: (bgColor: string) => (\n <svg viewBox=\"0 0 32 32\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\" className=\"size-full absolute inset-0\">\n <circle cx=\"16\" cy=\"16\" r=\"16\" style={{ fill: bgColor }} />\n </svg>\n ),\n square: (bgColor: string) => (\n <svg viewBox=\"0 0 32 32\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\" className=\"size-full absolute inset-0\">\n <rect width=\"32\" height=\"32\" style={{ fill: bgColor }} />\n </svg>\n ),\n};\n\n// Map icon names to components\nconst iconMap: Record<string, PhosphorIcon> = {\n Diamond, Hexagon, Star, Lightning, Sparkle, Infinity, Code, Terminal, Cpu,\n Database, Globe, Cloud, WifiHigh, Briefcase, Buildings, Storefront, Handshake,\n ChartLine, Palette: PaletteIcon, PencilSimple, Camera, MusicNote, Lightbulb,\n Leaf, Tree, Sun, Moon, Fire, Drop, ChatCircle, Envelope, Phone, Megaphone,\n Heart, Shield, Trophy, Rocket, Target, Flag,\n};\n\n// Helper to resolve CSS variable references to actual hex colors\nfunction resolveBrandingColor(value: string): string {\n if (!value) return \"#ffffff\";\n if (value.startsWith(\"var(\")) {\n const varName = value.replace(\"var(\", \"\").replace(\")\", \"\");\n if (typeof window !== \"undefined\") {\n const computed = getComputedStyle(document.documentElement).getPropertyValue(varName).trim();\n return computed || \"#ffffff\";\n }\n return \"#ffffff\";\n }\n return value;\n}\n\ninterface HeaderProps {\n /** Callback when mobile menu button is clicked */\n onMenuClick?: () => void;\n /** Whether to show the logo on desktop (for no-sidebar pages) */\n showDesktopLogo?: boolean;\n /** Visual variant - light (default) or dark mode */\n variant?: \"light\" | \"dark\";\n /** Callback when \"My Account\" is clicked */\n onAccountClick?: () => void;\n /** Callback when \"Logout\" is clicked */\n onLogout?: () => void;\n /** Avatar image URL */\n avatarUrl?: string;\n /** Cart items to display */\n cartItems?: CartItem[];\n /** Callback when checkout button is clicked */\n onCheckout?: () => void;\n /** Callback when remove item is clicked */\n onRemoveCartItem?: (id: string) => void;\n /** Messages to display */\n messages?: Message[];\n /** Callback when \"Mark as read\" is clicked for messages */\n onMarkAsRead?: () => void;\n /** Callback when \"view more\" is clicked for messages */\n onViewMoreMessages?: () => void;\n /** Notifications to display */\n notifications?: Notification[];\n /** Callback when \"Mark as read\" is clicked for notifications */\n onMarkNotificationsAsRead?: () => void;\n /** Callback when \"view more\" is clicked for notifications */\n onViewMoreNotifications?: () => void;\n /** Navigation items for header and mobile menu */\n navItems?: NavItem[];\n /** Callback when Login button is clicked */\n onLogin?: () => void;\n /** Callback when Sign up button is clicked */\n onSignUp?: () => void;\n /** Whether to show auth buttons (Login/Sign up) */\n showAuthButtons?: boolean;\n}\n\n/**\n * Canvas Design System - Header/Navbar Component\n * \n * Desktop (lg+): Full logo with wordmark, icon cluster, avatar\n * Mobile/Tablet: Favicon only, avatar, hamburger menu\n * \n * For pages without a sidebar, set showDesktopLogo={true} to display\n * the logo in the header on desktop.\n * \n * Set variant=\"dark\" for a dark themed header that matches the sidebar.\n */\nexport function Header({ \n onMenuClick, \n showDesktopLogo = false, \n variant = \"light\",\n onAccountClick,\n onLogout,\n avatarUrl = \"https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=150&h=150&fit=crop&crop=face\",\n cartItems = defaultCartItems,\n onCheckout,\n onRemoveCartItem,\n messages = defaultMessages,\n onMarkAsRead,\n onViewMoreMessages,\n notifications = defaultNotifications,\n onMarkNotificationsAsRead,\n onViewMoreNotifications,\n navItems = defaultNavItems,\n onLogin,\n onSignUp,\n showAuthButtons = false,\n}: HeaderProps) {\n const { branding, isMounted } = useThemeBranding();\n const isDark = variant === \"dark\";\n const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);\n \n // Calculate cart total\n const cartTotal = cartItems.reduce((sum, item) => sum + item.price, 0);\n\n // Cart popover content component\n const CartPopoverContent = () => (\n <div className=\"w-[320px]\">\n {/* Header */}\n <div \n className=\"flex items-center justify-between pb-[var(--spacing-xl)] border-b border-[var(--canvas-neutral-border)]\"\n >\n <span\n style={{\n fontFamily: \"var(--typo-body-m-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-m-size)\",\n fontWeight: 600,\n lineHeight: \"var(--typo-body-m-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n Your cart\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-neutral-text)\",\n }}\n >\n {cartItems.length} {cartItems.length === 1 ? \"item\" : \"items\"}\n </span>\n </div>\n\n {/* Cart Items */}\n <div className=\"py-[var(--spacing-xl)] space-y-[var(--spacing-xl)]\">\n {cartItems.map((item) => (\n <div key={item.id} className=\"flex gap-[var(--spacing-lg)]\">\n {/* Product Image */}\n <div \n className=\"size-16 rounded-[var(--radius-md)] overflow-hidden shrink-0 bg-[var(--canvas-neutral-surface)]\"\n >\n <img \n src={item.image} \n alt={item.name}\n className=\"size-full object-cover\"\n />\n </div>\n \n {/* Product Details */}\n <div className=\"flex flex-col justify-center min-w-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 {item.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)\",\n }}\n >\n ${item.price}\n </span>\n <button\n onClick={() => onRemoveCartItem?.(item.id)}\n className=\"text-left mt-[var(--spacing-xs)] hover:underline\"\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-primary)\",\n }}\n >\n Remove\n </button>\n </div>\n </div>\n ))}\n </div>\n\n {/* Total */}\n <div \n className=\"flex items-center justify-between py-[var(--spacing-xl)] border-t border-[var(--canvas-neutral-border)]\"\n >\n <span\n style={{\n fontFamily: \"var(--typo-body-m-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-m-size)\",\n fontWeight: 500,\n lineHeight: \"var(--typo-body-m-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n Total\n </span>\n <span\n style={{\n fontFamily: \"var(--typo-body-m-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-m-size)\",\n fontWeight: 600,\n lineHeight: \"var(--typo-body-m-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n ${cartTotal}\n </span>\n </div>\n\n {/* Checkout Button */}\n <Button \n className=\"w-full\" \n size=\"default\"\n onClick={onCheckout}\n >\n Checkout\n </Button>\n </div>\n );\n\n // Messages popover content component\n const MessagesPopoverContent = () => (\n <div className=\"w-[320px]\">\n {/* Header */}\n <div \n className=\"flex items-center justify-between pb-[var(--spacing-xl)] border-b border-[var(--canvas-neutral-border)]\"\n >\n <span\n style={{\n fontFamily: \"var(--typo-body-m-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-m-size)\",\n fontWeight: 600,\n lineHeight: \"var(--typo-body-m-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n Messages\n </span>\n <button\n onClick={onMarkAsRead}\n className=\"hover:underline\"\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-primary)\",\n }}\n >\n Mark as read\n </button>\n </div>\n\n {/* Messages List */}\n <div className=\"py-[var(--spacing-lg)]\">\n {messages.map((message, index) => (\n <div \n key={message.id} \n className={`flex gap-[var(--spacing-lg)] py-[var(--spacing-lg)] ${\n index < messages.length - 1 ? \"border-b border-[var(--canvas-neutral-border)]\" : \"\"\n }`}\n >\n {/* Sender Avatar */}\n <Avatar className=\"size-10 shrink-0\">\n <AvatarImage src={message.senderAvatar} alt={message.senderName} />\n <AvatarFallback \n className=\"bg-[var(--canvas-neutral-surface)] text-[var(--canvas-neutral-text)]\"\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n }}\n >\n {message.senderName.split(\" \").map(n => n[0]).join(\"\")}\n </AvatarFallback>\n </Avatar>\n \n {/* Message Details */}\n <div className=\"flex flex-col justify-center min-w-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: 400,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n <span style={{ fontWeight: 600 }}>{message.senderName}</span> sent you a message\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-neutral-text)\",\n }}\n >\n {message.timestamp}\n </span>\n </div>\n </div>\n ))}\n </div>\n\n {/* View More */}\n <div \n className=\"pt-[var(--spacing-lg)] border-t border-[var(--canvas-neutral-border)] text-center\"\n >\n <button\n onClick={onViewMoreMessages}\n className=\"hover:underline\"\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-primary)\",\n }}\n >\n view more\n </button>\n </div>\n </div>\n );\n\n // Notifications popover content component\n const NotificationsPopoverContent = () => (\n <div className=\"w-[320px]\">\n {/* Header */}\n <div \n className=\"flex items-center justify-between pb-[var(--spacing-xl)] border-b border-[var(--canvas-neutral-border)]\"\n >\n <span\n style={{\n fontFamily: \"var(--typo-body-m-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-m-size)\",\n fontWeight: 600,\n lineHeight: \"var(--typo-body-m-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n Notifications\n </span>\n <button\n onClick={onMarkNotificationsAsRead}\n className=\"hover:underline\"\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-primary)\",\n }}\n >\n Mark as read\n </button>\n </div>\n\n {/* Notifications List */}\n <div className=\"py-[var(--spacing-lg)]\">\n {notifications.map((notification, index) => (\n <div \n key={notification.id} \n className={`flex gap-[var(--spacing-lg)] py-[var(--spacing-lg)] ${\n index < notifications.length - 1 ? \"border-b border-[var(--canvas-neutral-border)]\" : \"\"\n }`}\n >\n {/* User Avatar */}\n <Avatar className=\"size-10 shrink-0\">\n <AvatarImage src={notification.userAvatar} alt={notification.userName} />\n <AvatarFallback \n className=\"bg-[var(--canvas-neutral-surface)] text-[var(--canvas-neutral-text)]\"\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n }}\n >\n {notification.userName.split(\" \").map(n => n[0]).join(\"\")}\n </AvatarFallback>\n </Avatar>\n \n {/* Notification Details */}\n <div className=\"flex flex-col justify-center min-w-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: 400,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n <span style={{ fontWeight: 600 }}>{notification.userName}</span> {notification.action}\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-neutral-text)\",\n }}\n >\n {notification.timestamp}\n </span>\n </div>\n </div>\n ))}\n </div>\n\n {/* View More */}\n <div \n className=\"pt-[var(--spacing-lg)] border-t border-[var(--canvas-neutral-border)] text-center\"\n >\n <button\n onClick={onViewMoreNotifications}\n className=\"hover:underline\"\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-primary)\",\n }}\n >\n view more\n </button>\n </div>\n </div>\n );\n\n // Get the icon shape renderer\n const shapeRenderer = iconShapes[branding.iconShape as keyof typeof iconShapes] || iconShapes.rounded;\n\n // Logo component used for both mobile and desktop (when showDesktopLogo is true)\n // Uses CSS variables directly - no JavaScript resolution needed\n const LogoIcon = () => {\n // Use CSS variables directly - the browser handles resolution\n const bgColor = branding.bgColor || \"var(--canvas-primary)\";\n const iconColor = branding.iconColor || \"var(--canvas-primary-foreground)\";\n const IconComponent = iconMap[branding.iconName || \"Buildings\"] || Buildings;\n \n return (\n <div className=\"relative size-8 shrink-0\">\n {shapeRenderer(bgColor)}\n <div className=\"absolute inset-0 flex items-center justify-center z-10\">\n <IconComponent weight=\"bold\" size={18} color={iconColor} />\n </div>\n </div>\n );\n };\n\n return (\n <header \n className={`h-[var(--header-height)] w-full border-b ${\n isDark \n ? \"bg-[var(--canvas-sidebar-dark-bg)] border-[var(--canvas-sidebar-dark-border)]\" \n : \"bg-[var(--canvas-background)] border-[var(--canvas-neutral-border)]\"\n }`}\n >\n <div className=\"flex items-center h-full px-4 lg:px-[var(--spacing-5xl)]\">\n {/* Logo - Visible on mobile, and on desktop when showDesktopLogo is true */}\n <div className={`flex items-center gap-[var(--spacing-md)] h-8 shrink-0 ${showDesktopLogo ? '' : 'lg:hidden'}`}>\n <LogoIcon />\n {/* Wordmark - only on desktop when showDesktopLogo is true */}\n {showDesktopLogo && (\n <span \n className={`hidden lg:block ${isDark ? \"text-white\" : \"text-[var(--canvas-text)]\"}`}\n style={{\n fontFamily: \"var(--typo-header-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-xl-size)\",\n fontWeight: 600,\n letterSpacing: \"var(--typo-header-spacing)\",\n lineHeight: \"var(--typo-header-line-height)\",\n }}\n >\n {branding.wordmark || \"canvas\"}\n </span>\n )}\n </div>\n\n {/* Spacer */}\n <div className=\"flex-1\" />\n\n {/* Navigation Links - Desktop Only */}\n <nav className=\"hidden lg:flex items-center gap-[var(--spacing-2xl)] h-full\">\n {navItems.map((item) => (\n <button\n key={item.id}\n onClick={() => {\n item.onClick?.();\n if (item.href) {\n window.location.href = item.href;\n }\n }}\n className={`cursor-pointer transition-colors ${\n isDark \n ? \"text-white/60 hover:text-white\" \n : \"text-[var(--canvas-neutral-text)] hover:text-[var(--canvas-text)]\"\n }`}\n style={{\n fontFamily: \"var(--typo-header-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-header-size)\",\n fontWeight: \"var(--typo-header-weight)\",\n letterSpacing: \"var(--typo-header-spacing)\",\n lineHeight: \"var(--typo-header-line-height)\",\n }}\n >\n {item.label}\n </button>\n ))}\n </nav>\n\n {/* Icons - Always Visible */}\n <div className=\"flex items-center gap-[var(--spacing-2xl)] ml-[var(--spacing-2xl)]\">\n <button \n className={`transition-colors ${\n isDark \n ? \"text-white/60 hover:text-white\" \n : \"text-[var(--canvas-neutral-text)] hover:text-[var(--canvas-text)]\"\n }`}\n aria-label=\"Search\"\n >\n <Search className=\"size-4\" />\n </button>\n \n <Popover>\n <PopoverTrigger asChild>\n <button \n className={`transition-colors ${\n isDark \n ? \"text-white/60 hover:text-white\" \n : \"text-[var(--canvas-neutral-text)] hover:text-[var(--canvas-text)]\"\n }`}\n aria-label=\"Notifications\"\n >\n <Bell className=\"size-4\" />\n </button>\n </PopoverTrigger>\n <PopoverContent align=\"end\" sideOffset={16} className=\"w-auto p-[var(--spacing-xl)]\">\n <NotificationsPopoverContent />\n </PopoverContent>\n </Popover>\n \n <Popover>\n <PopoverTrigger asChild>\n <button \n className={`transition-colors ${\n isDark \n ? \"text-white/60 hover:text-white\" \n : \"text-[var(--canvas-neutral-text)] hover:text-[var(--canvas-text)]\"\n }`}\n aria-label=\"Messages\"\n >\n <MessageSquare className=\"size-4\" />\n </button>\n </PopoverTrigger>\n <PopoverContent align=\"end\" sideOffset={16} className=\"w-auto p-[var(--spacing-xl)]\">\n <MessagesPopoverContent />\n </PopoverContent>\n </Popover>\n \n <Popover>\n <PopoverTrigger asChild>\n <button \n className={`transition-colors ${\n isDark \n ? \"text-white/60 hover:text-white\" \n : \"text-[var(--canvas-neutral-text)] hover:text-[var(--canvas-text)]\"\n }`}\n aria-label=\"Cart\"\n >\n <ShoppingCart className=\"size-4\" />\n </button>\n </PopoverTrigger>\n <PopoverContent align=\"end\" sideOffset={16} className=\"w-auto p-[var(--spacing-xl)]\">\n <CartPopoverContent />\n </PopoverContent>\n </Popover>\n \n {/* Auth Buttons - Desktop Only */}\n {showAuthButtons && (\n <div className=\"hidden lg:flex items-center gap-[var(--spacing-lg)]\">\n <Button \n variant=\"outline\" \n size=\"default\"\n onClick={onLogin}\n >\n Log in\n </Button>\n <Button \n variant=\"default\" \n size=\"default\"\n onClick={onSignUp}\n >\n Sign up\n </Button>\n </div>\n )}\n \n {/* Avatar with Dropdown */}\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <button className=\"rounded-full focus:outline-none focus:ring-2 focus:ring-[var(--canvas-primary)] focus:ring-offset-2\">\n <Avatar className={`size-10 border cursor-pointer ${\n isDark \n ? \"border-[var(--canvas-sidebar-dark-border)]\" \n : \"border-[var(--canvas-neutral-border)]\"\n }`}>\n <AvatarImage src={avatarUrl} alt=\"User avatar\" />\n <AvatarFallback \n className={\n isDark \n ? \"bg-white/10 text-white/60\" \n : \"bg-[var(--canvas-neutral-surface)] text-[var(--canvas-neutral-text)]\"\n }\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n }}\n >\n JC\n </AvatarFallback>\n </Avatar>\n </button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"end\" sideOffset={8}>\n <DropdownMenuItem onClick={onAccountClick}>\n <User className=\"size-4 mr-2\" />\n My Account\n </DropdownMenuItem>\n <DropdownMenuItem onClick={onLogout}>\n <LogOut className=\"size-4 mr-2\" />\n Logout\n </DropdownMenuItem>\n </DropdownMenuContent>\n </DropdownMenu>\n\n {/* Mobile Menu Button */}\n <Button \n variant=\"ghost\" \n size=\"icon\" \n onClick={() => {\n setIsMobileMenuOpen(true);\n onMenuClick?.();\n }}\n aria-label=\"Open menu\"\n className={`lg:hidden -ml-[var(--spacing-md)] ${isDark ? \"text-white/60 hover:text-white hover:bg-white/10\" : \"text-[var(--canvas-neutral-text)]\"}`}\n >\n <Menu className=\"size-4\" />\n </Button>\n </div>\n </div>\n\n {/* Mobile Menu Overlay */}\n {isMobileMenuOpen && (\n <div className=\"fixed inset-0 z-50 lg:hidden\">\n {/* Backdrop */}\n <div \n className=\"absolute inset-0 bg-black/50\"\n onClick={() => setIsMobileMenuOpen(false)}\n />\n \n {/* Menu Panel */}\n <div className=\"absolute right-0 top-0 h-full w-full max-w-sm bg-[var(--canvas-background)] shadow-xl\">\n {/* Close Button */}\n <div className=\"flex justify-end p-4\">\n <Button\n variant=\"ghost\"\n size=\"icon\"\n onClick={() => setIsMobileMenuOpen(false)}\n aria-label=\"Close menu\"\n >\n <X className=\"size-5\" />\n </Button>\n </div>\n \n {/* Navigation Items */}\n <nav className=\"px-6 py-4\">\n <div className=\"space-y-2\">\n {navItems.map((item) => {\n const Icon = item.icon;\n return (\n <button\n key={item.id}\n onClick={() => {\n item.onClick?.();\n if (item.href) {\n window.location.href = item.href;\n }\n setIsMobileMenuOpen(false);\n }}\n className=\"flex items-center gap-[var(--spacing-lg)] w-full py-[var(--spacing-lg)] text-left hover:bg-[var(--canvas-neutral-surface)] rounded-[var(--radius-md)] transition-colors\"\n >\n {Icon && (\n <div \n className=\"size-12 rounded-[var(--radius-md)] flex items-center justify-center shrink-0\"\n style={{\n backgroundColor: \"color-mix(in srgb, var(--canvas-primary) 10%, transparent)\",\n }}\n >\n <Icon \n className=\"size-5\"\n style={{ color: \"var(--canvas-primary)\" }}\n />\n </div>\n )}\n <span\n style={{\n fontFamily: \"var(--typo-body-m-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-m-size)\",\n fontWeight: 400,\n lineHeight: \"var(--typo-body-m-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n {item.label}\n </span>\n </button>\n );\n })}\n </div>\n </nav>\n \n {/* Auth Buttons */}\n {showAuthButtons && (\n <div className=\"absolute bottom-0 left-0 right-0 p-6 space-y-3 border-t border-[var(--canvas-neutral-border)]\">\n <Button \n variant=\"outline\" \n className=\"w-full\"\n size=\"lg\"\n onClick={() => {\n onLogin?.();\n setIsMobileMenuOpen(false);\n }}\n >\n Log in\n </Button>\n <Button \n variant=\"default\" \n className=\"w-full\"\n size=\"lg\"\n onClick={() => {\n onSignUp?.();\n setIsMobileMenuOpen(false);\n }}\n >\n Sign up\n </Button>\n </div>\n )}\n </div>\n </div>\n )}\n </header>\n );\n}\n\n"
9
+ "content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { Search, Bell, ShoppingCart, Menu, User, LogOut, MessageSquare, X, Home, Info, LayoutGrid, type LucideIcon } from \"lucide-react\";\nimport { Avatar, AvatarFallback, AvatarImage } from \"../ui/avatar\";\nimport { Button } from \"../ui/button\";\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuTrigger,\n} from \"../ui/dropdown-menu\";\nimport {\n Popover,\n PopoverContent,\n PopoverTrigger,\n} from \"../ui/popover\";\nimport { useThemeBranding } from \"../../context/theme-context\";\n\n// ============================================\n// Cart Types\n// ============================================\n\nexport interface CartItem {\n id: string;\n name: string;\n price: number;\n image: string;\n}\n\n// Sample cart items for demo\nconst defaultCartItems: CartItem[] = [\n {\n id: \"1\",\n name: \"Julian Bag\",\n price: 120,\n image: \"https://images.unsplash.com/photo-1591561954557-26941169b49e?w=150&h=150&fit=crop\",\n },\n {\n id: \"2\",\n name: \"Davis Keychain\",\n price: 60,\n image: \"https://images.unsplash.com/photo-1606107557195-0e29a4b5b4aa?w=150&h=150&fit=crop&crop=center\",\n },\n];\n\n// ============================================\n// Message Types\n// ============================================\n\nexport interface Message {\n id: string;\n senderName: string;\n senderAvatar: string;\n timestamp: string;\n}\n\n// Sample messages for demo\nconst defaultMessages: Message[] = [\n {\n id: \"1\",\n senderName: \"Jeff Conner\",\n senderAvatar: \"https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=100&h=100&fit=crop&crop=face\",\n timestamp: \"Jun 5, 2023 8:13 AM\",\n },\n {\n id: \"2\",\n senderName: \"Emma Pérez\",\n senderAvatar: \"https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=100&h=100&fit=crop&crop=face\",\n timestamp: \"May 2, 2023 11:54 AM\",\n },\n {\n id: \"3\",\n senderName: \"Raj Mishra\",\n senderAvatar: \"https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=100&h=100&fit=crop&crop=face\",\n timestamp: \"Jan 10, 2023 5:22 PM\",\n },\n {\n id: \"4\",\n senderName: \"John Freidman\",\n senderAvatar: \"https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=100&h=100&fit=crop&crop=face\",\n timestamp: \"Dec 20, 2022 2:22 PM\",\n },\n];\n\n// ============================================\n// Notification Types\n// ============================================\n\nexport interface Notification {\n id: string;\n userName: string;\n userAvatar: string;\n action: string;\n timestamp: string;\n}\n\n// Sample notifications for demo\nconst defaultNotifications: Notification[] = [\n {\n id: \"1\",\n userName: \"Aya Williams\",\n userAvatar: \"https://images.unsplash.com/photo-1531746020798-e6953c6e8e04?w=100&h=100&fit=crop&crop=face\",\n action: \"liked your photo\",\n timestamp: \"Apr 15, 2023 6:21 AM\",\n },\n {\n id: \"2\",\n userName: \"Francis Gaddi\",\n userAvatar: \"https://images.unsplash.com/photo-1506794778202-cad84cf45f1d?w=100&h=100&fit=crop&crop=face\",\n action: \"liked your photo\",\n timestamp: \"Jun 10, 2023 5:45 PM\",\n },\n {\n id: \"3\",\n userName: \"Stacy Jones\",\n userAvatar: \"https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=100&h=100&fit=crop&crop=face\",\n action: \"liked your photo\",\n timestamp: \"May 9, 2023 2:00 AM\",\n },\n {\n id: \"4\",\n userName: \"Gabi del Rosario\",\n userAvatar: \"https://images.unsplash.com/photo-1544005313-94ddf0286df2?w=100&h=100&fit=crop&crop=face\",\n action: \"liked your photo\",\n timestamp: \"Apr 8, 2023 8:55 PM\",\n },\n];\n\n// ============================================\n// Navigation Types\n// ============================================\n\nexport interface NavItem {\n id: string;\n label: string;\n icon?: LucideIcon;\n href?: string;\n onClick?: () => void;\n}\n\n// Default navigation items\nconst defaultNavItems: NavItem[] = [\n { id: \"home\", label: \"Home\", icon: Home },\n { id: \"about\", label: \"About\", icon: Info },\n { id: \"dashboard\", label: \"Dashboard\", icon: LayoutGrid },\n];\n\n// Phosphor Icons for Logo\nimport { Buildings, type Icon as PhosphorIcon } from \"@phosphor-icons/react\";\nimport {\n Diamond, Hexagon, Star, Lightning, Sparkle, Infinity, Code, Terminal, Cpu,\n Database, Globe, Cloud, WifiHigh, Briefcase, Storefront, Handshake, ChartLine,\n Palette as PaletteIcon, PencilSimple, Camera, MusicNote, Lightbulb, Leaf, Tree,\n Sun, Moon, Fire, Drop, ChatCircle, Envelope, Phone, Megaphone, Heart, Shield,\n Trophy, Rocket, Target, Flag,\n} from \"@phosphor-icons/react\";\n\n// Icon shape renderers - use style attribute for CSS variable support\nconst iconShapes = {\n rounded: (bgColor: string) => (\n <svg viewBox=\"0 0 32 32\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\" className=\"size-full absolute inset-0\">\n <rect width=\"32\" height=\"32\" rx=\"10\" style={{ fill: bgColor }} />\n </svg>\n ),\n circle: (bgColor: string) => (\n <svg viewBox=\"0 0 32 32\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\" className=\"size-full absolute inset-0\">\n <circle cx=\"16\" cy=\"16\" r=\"16\" style={{ fill: bgColor }} />\n </svg>\n ),\n square: (bgColor: string) => (\n <svg viewBox=\"0 0 32 32\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\" className=\"size-full absolute inset-0\">\n <rect width=\"32\" height=\"32\" style={{ fill: bgColor }} />\n </svg>\n ),\n};\n\n// Map icon names to components\nconst iconMap: Record<string, PhosphorIcon> = {\n Diamond, Hexagon, Star, Lightning, Sparkle, Infinity, Code, Terminal, Cpu,\n Database, Globe, Cloud, WifiHigh, Briefcase, Buildings, Storefront, Handshake,\n ChartLine, Palette: PaletteIcon, PencilSimple, Camera, MusicNote, Lightbulb,\n Leaf, Tree, Sun, Moon, Fire, Drop, ChatCircle, Envelope, Phone, Megaphone,\n Heart, Shield, Trophy, Rocket, Target, Flag,\n};\n\n// Helper to resolve CSS variable references to actual hex colors\nfunction resolveBrandingColor(value: string): string {\n if (!value) return \"#ffffff\";\n if (value.startsWith(\"var(\")) {\n const varName = value.replace(\"var(\", \"\").replace(\")\", \"\");\n if (typeof window !== \"undefined\") {\n const computed = getComputedStyle(document.documentElement).getPropertyValue(varName).trim();\n return computed || \"#ffffff\";\n }\n return \"#ffffff\";\n }\n return value;\n}\n\ninterface HeaderProps {\n /** Callback when mobile menu button is clicked */\n onMenuClick?: () => void;\n /** Whether to show the logo on desktop (for no-sidebar pages) */\n showDesktopLogo?: boolean;\n /** Visual variant - light (default) or dark mode */\n variant?: \"light\" | \"dark\";\n /** Callback when \"My Account\" is clicked */\n onAccountClick?: () => void;\n /** Callback when \"Logout\" is clicked */\n onLogout?: () => void;\n /** Avatar image URL */\n avatarUrl?: string;\n /** Cart items to display */\n cartItems?: CartItem[];\n /** Callback when checkout button is clicked */\n onCheckout?: () => void;\n /** Callback when remove item is clicked */\n onRemoveCartItem?: (id: string) => void;\n /** Messages to display */\n messages?: Message[];\n /** Callback when \"Mark as read\" is clicked for messages */\n onMarkAsRead?: () => void;\n /** Callback when \"view more\" is clicked for messages */\n onViewMoreMessages?: () => void;\n /** Notifications to display */\n notifications?: Notification[];\n /** Callback when \"Mark as read\" is clicked for notifications */\n onMarkNotificationsAsRead?: () => void;\n /** Callback when \"view more\" is clicked for notifications */\n onViewMoreNotifications?: () => void;\n /** Navigation items for header and mobile menu */\n navItems?: NavItem[];\n /** Callback when Login button is clicked */\n onLogin?: () => void;\n /** Callback when Sign up button is clicked */\n onSignUp?: () => void;\n /** Whether to show auth buttons (Login/Sign up) */\n showAuthButtons?: boolean;\n}\n\n/**\n * Canvas Design System - Header/Navbar Component\n * \n * Desktop (lg+): Full logo with wordmark, icon cluster, avatar\n * Mobile/Tablet: Favicon only, avatar, hamburger menu\n * \n * For pages without a sidebar, set showDesktopLogo={true} to display\n * the logo in the header on desktop.\n * \n * Set variant=\"dark\" for a dark themed header that matches the sidebar.\n */\nexport function Header({ \n onMenuClick, \n showDesktopLogo = false, \n variant = \"light\",\n onAccountClick,\n onLogout,\n avatarUrl = \"https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=150&h=150&fit=crop&crop=face\",\n cartItems = defaultCartItems,\n onCheckout,\n onRemoveCartItem,\n messages = defaultMessages,\n onMarkAsRead,\n onViewMoreMessages,\n notifications = defaultNotifications,\n onMarkNotificationsAsRead,\n onViewMoreNotifications,\n navItems = defaultNavItems,\n onLogin,\n onSignUp,\n showAuthButtons = false,\n}: HeaderProps) {\n const { branding, isMounted } = useThemeBranding();\n const isDark = variant === \"dark\";\n const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);\n \n // Calculate cart total\n const cartTotal = cartItems.reduce((sum, item) => sum + item.price, 0);\n\n // Cart popover content component\n const CartPopoverContent = () => (\n <div className=\"w-[320px]\">\n {/* Header */}\n <div \n className=\"flex items-center justify-between pb-[var(--spacing-xl)] border-b border-[var(--canvas-neutral-border)]\"\n >\n <span\n style={{\n fontFamily: \"var(--typo-body-m-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-m-size)\",\n fontWeight: 600,\n lineHeight: \"var(--typo-body-m-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n Your cart\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-neutral-text)\",\n }}\n >\n {cartItems.length} {cartItems.length === 1 ? \"item\" : \"items\"}\n </span>\n </div>\n\n {/* Cart Items */}\n <div className=\"py-[var(--spacing-xl)] space-y-[var(--spacing-xl)]\">\n {cartItems.map((item) => (\n <div key={item.id} className=\"flex gap-[var(--spacing-lg)]\">\n {/* Product Image */}\n <div \n className=\"size-16 rounded-[var(--radius-md)] overflow-hidden shrink-0 bg-[var(--canvas-neutral-surface)]\"\n >\n <img \n src={item.image} \n alt={item.name}\n className=\"size-full object-cover\"\n />\n </div>\n \n {/* Product Details */}\n <div className=\"flex flex-col justify-center min-w-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 {item.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)\",\n }}\n >\n ${item.price}\n </span>\n <button\n onClick={() => onRemoveCartItem?.(item.id)}\n className=\"cursor-pointer text-left mt-[var(--spacing-xs)] hover:underline\"\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-primary)\",\n }}\n >\n Remove\n </button>\n </div>\n </div>\n ))}\n </div>\n\n {/* Total */}\n <div \n className=\"flex items-center justify-between py-[var(--spacing-xl)] border-t border-[var(--canvas-neutral-border)]\"\n >\n <span\n style={{\n fontFamily: \"var(--typo-body-m-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-m-size)\",\n fontWeight: 500,\n lineHeight: \"var(--typo-body-m-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n Total\n </span>\n <span\n style={{\n fontFamily: \"var(--typo-body-m-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-m-size)\",\n fontWeight: 600,\n lineHeight: \"var(--typo-body-m-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n ${cartTotal}\n </span>\n </div>\n\n {/* Checkout Button */}\n <Button \n className=\"w-full\" \n size=\"default\"\n onClick={onCheckout}\n >\n Checkout\n </Button>\n </div>\n );\n\n // Messages popover content component\n const MessagesPopoverContent = () => (\n <div className=\"w-[320px]\">\n {/* Header */}\n <div \n className=\"flex items-center justify-between pb-[var(--spacing-xl)] border-b border-[var(--canvas-neutral-border)]\"\n >\n <span\n style={{\n fontFamily: \"var(--typo-body-m-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-m-size)\",\n fontWeight: 600,\n lineHeight: \"var(--typo-body-m-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n Messages\n </span>\n <button\n onClick={onMarkAsRead}\n className=\"cursor-pointer hover:underline\"\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-primary)\",\n }}\n >\n Mark as read\n </button>\n </div>\n\n {/* Messages List */}\n <div className=\"py-[var(--spacing-lg)]\">\n {messages.map((message, index) => (\n <div \n key={message.id} \n className={`flex gap-[var(--spacing-lg)] py-[var(--spacing-lg)] ${\n index < messages.length - 1 ? \"border-b border-[var(--canvas-neutral-border)]\" : \"\"\n }`}\n >\n {/* Sender Avatar */}\n <Avatar className=\"size-10 shrink-0\">\n <AvatarImage src={message.senderAvatar} alt={message.senderName} />\n <AvatarFallback \n className=\"bg-[var(--canvas-neutral-surface)] text-[var(--canvas-neutral-text)]\"\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n }}\n >\n {message.senderName.split(\" \").map(n => n[0]).join(\"\")}\n </AvatarFallback>\n </Avatar>\n \n {/* Message Details */}\n <div className=\"flex flex-col justify-center min-w-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: 400,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n <span style={{ fontWeight: 600 }}>{message.senderName}</span> sent you a message\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-neutral-text)\",\n }}\n >\n {message.timestamp}\n </span>\n </div>\n </div>\n ))}\n </div>\n\n {/* View More */}\n <div \n className=\"pt-[var(--spacing-lg)] border-t border-[var(--canvas-neutral-border)] text-center\"\n >\n <button\n onClick={onViewMoreMessages}\n className=\"cursor-pointer hover:underline\"\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-primary)\",\n }}\n >\n view more\n </button>\n </div>\n </div>\n );\n\n // Notifications popover content component\n const NotificationsPopoverContent = () => (\n <div className=\"w-[320px]\">\n {/* Header */}\n <div \n className=\"flex items-center justify-between pb-[var(--spacing-xl)] border-b border-[var(--canvas-neutral-border)]\"\n >\n <span\n style={{\n fontFamily: \"var(--typo-body-m-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-m-size)\",\n fontWeight: 600,\n lineHeight: \"var(--typo-body-m-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n Notifications\n </span>\n <button\n onClick={onMarkNotificationsAsRead}\n className=\"cursor-pointer hover:underline\"\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-primary)\",\n }}\n >\n Mark as read\n </button>\n </div>\n\n {/* Notifications List */}\n <div className=\"py-[var(--spacing-lg)]\">\n {notifications.map((notification, index) => (\n <div \n key={notification.id} \n className={`flex gap-[var(--spacing-lg)] py-[var(--spacing-lg)] ${\n index < notifications.length - 1 ? \"border-b border-[var(--canvas-neutral-border)]\" : \"\"\n }`}\n >\n {/* User Avatar */}\n <Avatar className=\"size-10 shrink-0\">\n <AvatarImage src={notification.userAvatar} alt={notification.userName} />\n <AvatarFallback \n className=\"bg-[var(--canvas-neutral-surface)] text-[var(--canvas-neutral-text)]\"\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n }}\n >\n {notification.userName.split(\" \").map(n => n[0]).join(\"\")}\n </AvatarFallback>\n </Avatar>\n \n {/* Notification Details */}\n <div className=\"flex flex-col justify-center min-w-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: 400,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n <span style={{ fontWeight: 600 }}>{notification.userName}</span> {notification.action}\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-neutral-text)\",\n }}\n >\n {notification.timestamp}\n </span>\n </div>\n </div>\n ))}\n </div>\n\n {/* View More */}\n <div \n className=\"pt-[var(--spacing-lg)] border-t border-[var(--canvas-neutral-border)] text-center\"\n >\n <button\n onClick={onViewMoreNotifications}\n className=\"cursor-pointer hover:underline\"\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-primary)\",\n }}\n >\n view more\n </button>\n </div>\n </div>\n );\n\n // Get the icon shape renderer\n const shapeRenderer = iconShapes[branding.iconShape as keyof typeof iconShapes] || iconShapes.rounded;\n\n // Logo component used for both mobile and desktop (when showDesktopLogo is true)\n // Uses CSS variables directly - no JavaScript resolution needed\n const LogoIcon = () => {\n // Use CSS variables directly - the browser handles resolution\n const bgColor = branding.bgColor || \"var(--canvas-primary)\";\n const iconColor = branding.iconColor || \"var(--canvas-primary-foreground)\";\n const IconComponent = iconMap[branding.iconName || \"Buildings\"] || Buildings;\n \n return (\n <div className=\"relative size-8 shrink-0\">\n {shapeRenderer(bgColor)}\n <div className=\"absolute inset-0 flex items-center justify-center z-10\">\n <IconComponent weight=\"bold\" size={18} color={iconColor} />\n </div>\n </div>\n );\n };\n\n return (\n <header \n className={`h-[var(--header-height)] w-full border-b ${\n isDark \n ? \"bg-[var(--canvas-sidebar-dark-bg)] border-[var(--canvas-sidebar-dark-border)]\" \n : \"bg-[var(--canvas-background)] border-[var(--canvas-neutral-border)]\"\n }`}\n >\n <div className=\"flex items-center h-full px-4 lg:px-[var(--spacing-5xl)]\">\n {/* Logo - Visible on mobile, and on desktop when showDesktopLogo is true */}\n <div className={`flex items-center gap-[var(--spacing-md)] h-8 shrink-0 ${showDesktopLogo ? '' : 'lg:hidden'}`}>\n <LogoIcon />\n {/* Wordmark - only on desktop when showDesktopLogo is true */}\n {showDesktopLogo && (\n <span \n className={`hidden lg:block ${isDark ? \"text-white\" : \"text-[var(--canvas-text)]\"}`}\n style={{\n fontFamily: \"var(--typo-header-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-xl-size)\",\n fontWeight: 600,\n letterSpacing: \"var(--typo-header-spacing)\",\n lineHeight: \"var(--typo-header-line-height)\",\n }}\n >\n {branding.wordmark || \"canvas\"}\n </span>\n )}\n </div>\n\n {/* Spacer */}\n <div className=\"flex-1\" />\n\n {/* Navigation Links - Desktop Only */}\n <nav className=\"hidden lg:flex items-center gap-[var(--spacing-2xl)] h-full\">\n {navItems.map((item) => (\n <button\n key={item.id}\n onClick={() => {\n item.onClick?.();\n if (item.href) {\n window.location.href = item.href;\n }\n }}\n className={`cursor-pointer transition-colors ${\n isDark \n ? \"text-white/60 hover:text-white\" \n : \"text-[var(--canvas-neutral-text)] hover:text-[var(--canvas-text)]\"\n }`}\n style={{\n fontFamily: \"var(--typo-header-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-header-size)\",\n fontWeight: \"var(--typo-header-weight)\",\n letterSpacing: \"var(--typo-header-spacing)\",\n lineHeight: \"var(--typo-header-line-height)\",\n }}\n >\n {item.label}\n </button>\n ))}\n </nav>\n\n {/* Icons - Always Visible */}\n <div className=\"flex items-center gap-[var(--spacing-2xl)] ml-[var(--spacing-2xl)]\">\n <button\n className={`cursor-pointer transition-colors ${\n isDark\n ? \"text-white/60 hover:text-white\"\n : \"text-[var(--canvas-neutral-text)] hover:text-[var(--canvas-text)]\"\n }`}\n aria-label=\"Search\"\n >\n <Search className=\"size-4\" />\n </button>\n \n <Popover>\n <PopoverTrigger asChild>\n <button\n className={`cursor-pointer transition-colors ${\n isDark\n ? \"text-white/60 hover:text-white\"\n : \"text-[var(--canvas-neutral-text)] hover:text-[var(--canvas-text)]\"\n }`}\n aria-label=\"Notifications\"\n >\n <Bell className=\"size-4\" />\n </button>\n </PopoverTrigger>\n <PopoverContent align=\"end\" sideOffset={16} className=\"w-auto p-[var(--spacing-xl)]\">\n <NotificationsPopoverContent />\n </PopoverContent>\n </Popover>\n \n <Popover>\n <PopoverTrigger asChild>\n <button\n className={`cursor-pointer transition-colors ${\n isDark\n ? \"text-white/60 hover:text-white\"\n : \"text-[var(--canvas-neutral-text)] hover:text-[var(--canvas-text)]\"\n }`}\n aria-label=\"Messages\"\n >\n <MessageSquare className=\"size-4\" />\n </button>\n </PopoverTrigger>\n <PopoverContent align=\"end\" sideOffset={16} className=\"w-auto p-[var(--spacing-xl)]\">\n <MessagesPopoverContent />\n </PopoverContent>\n </Popover>\n \n <Popover>\n <PopoverTrigger asChild>\n <button\n className={`cursor-pointer transition-colors ${\n isDark\n ? \"text-white/60 hover:text-white\"\n : \"text-[var(--canvas-neutral-text)] hover:text-[var(--canvas-text)]\"\n }`}\n aria-label=\"Cart\"\n >\n <ShoppingCart className=\"size-4\" />\n </button>\n </PopoverTrigger>\n <PopoverContent align=\"end\" sideOffset={16} className=\"w-auto p-[var(--spacing-xl)]\">\n <CartPopoverContent />\n </PopoverContent>\n </Popover>\n \n {/* Auth Buttons - Desktop Only */}\n {showAuthButtons && (\n <div className=\"hidden lg:flex items-center gap-[var(--spacing-lg)]\">\n <Button \n variant=\"outline\" \n size=\"default\"\n onClick={onLogin}\n >\n Log in\n </Button>\n <Button \n variant=\"default\" \n size=\"default\"\n onClick={onSignUp}\n >\n Sign up\n </Button>\n </div>\n )}\n \n {/* Avatar with Dropdown */}\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <button className=\"cursor-pointer rounded-full focus:outline-none focus:ring-2 focus:ring-[var(--canvas-primary)] focus:ring-offset-2\">\n <Avatar className={`size-10 border cursor-pointer ${\n isDark\n ? \"border-[var(--canvas-sidebar-dark-border)]\"\n : \"border-[var(--canvas-neutral-border)]\"\n }`}>\n <AvatarImage src={avatarUrl} alt=\"User avatar\" />\n <AvatarFallback \n className={\n isDark \n ? \"bg-white/10 text-white/60\" \n : \"bg-[var(--canvas-neutral-surface)] text-[var(--canvas-neutral-text)]\"\n }\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n }}\n >\n JC\n </AvatarFallback>\n </Avatar>\n </button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"end\" sideOffset={8}>\n <DropdownMenuItem onClick={onAccountClick}>\n <User className=\"size-4 mr-2\" />\n My Account\n </DropdownMenuItem>\n <DropdownMenuItem onClick={onLogout}>\n <LogOut className=\"size-4 mr-2\" />\n Logout\n </DropdownMenuItem>\n </DropdownMenuContent>\n </DropdownMenu>\n\n {/* Mobile Menu Button */}\n <Button \n variant=\"ghost\" \n size=\"icon\" \n onClick={() => {\n setIsMobileMenuOpen(true);\n onMenuClick?.();\n }}\n aria-label=\"Open menu\"\n className={`lg:hidden -ml-[var(--spacing-md)] ${isDark ? \"text-white/60 hover:text-white hover:bg-white/10\" : \"text-[var(--canvas-neutral-text)]\"}`}\n >\n <Menu className=\"size-4\" />\n </Button>\n </div>\n </div>\n\n {/* Mobile Menu Overlay */}\n {isMobileMenuOpen && (\n <div className=\"fixed inset-0 z-50 lg:hidden\">\n {/* Backdrop */}\n <div \n className=\"absolute inset-0 bg-black/50\"\n onClick={() => setIsMobileMenuOpen(false)}\n />\n \n {/* Menu Panel */}\n <div className=\"absolute right-0 top-0 h-full w-full max-w-sm bg-[var(--canvas-background)] shadow-xl\">\n {/* Close Button */}\n <div className=\"flex justify-end p-4\">\n <Button\n variant=\"ghost\"\n size=\"icon\"\n onClick={() => setIsMobileMenuOpen(false)}\n aria-label=\"Close menu\"\n >\n <X className=\"size-5\" />\n </Button>\n </div>\n \n {/* Navigation Items */}\n <nav className=\"px-6 py-4\">\n <div className=\"space-y-2\">\n {navItems.map((item) => {\n const Icon = item.icon;\n return (\n <button\n key={item.id}\n onClick={() => {\n item.onClick?.();\n if (item.href) {\n window.location.href = item.href;\n }\n setIsMobileMenuOpen(false);\n }}\n className=\"cursor-pointer flex items-center gap-[var(--spacing-lg)] w-full py-[var(--spacing-lg)] text-left hover:bg-[var(--canvas-neutral-surface)] rounded-[var(--radius-md)] transition-colors\"\n >\n {Icon && (\n <div \n className=\"size-12 rounded-[var(--radius-md)] flex items-center justify-center shrink-0\"\n style={{\n backgroundColor: \"color-mix(in srgb, var(--canvas-primary) 10%, transparent)\",\n }}\n >\n <Icon \n className=\"size-5\"\n style={{ color: \"var(--canvas-primary)\" }}\n />\n </div>\n )}\n <span\n style={{\n fontFamily: \"var(--typo-body-m-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-m-size)\",\n fontWeight: 400,\n lineHeight: \"var(--typo-body-m-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n {item.label}\n </span>\n </button>\n );\n })}\n </div>\n </nav>\n \n {/* Auth Buttons */}\n {showAuthButtons && (\n <div className=\"absolute bottom-0 left-0 right-0 p-6 space-y-3 border-t border-[var(--canvas-neutral-border)]\">\n <Button \n variant=\"outline\" \n className=\"w-full\"\n size=\"lg\"\n onClick={() => {\n onLogin?.();\n setIsMobileMenuOpen(false);\n }}\n >\n Log in\n </Button>\n <Button \n variant=\"default\" \n className=\"w-full\"\n size=\"lg\"\n onClick={() => {\n onSignUp?.();\n setIsMobileMenuOpen(false);\n }}\n >\n Sign up\n </Button>\n </div>\n )}\n </div>\n </div>\n )}\n </header>\n );\n}\n\n"
10
10
  }
11
11
  ],
12
12
  "dependencies": [