canvas-ui-sdk 0.3.19 → 0.3.21

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 (121) hide show
  1. package/dist/cli/index.js +28 -1
  2. package/dist/index.js +198 -182
  3. package/dist/index.js.map +1 -1
  4. package/package.json +1 -1
  5. package/registry/blocks/activity-feed.json +5 -4
  6. package/registry/blocks/bottom-input-chat-widget.json +4 -3
  7. package/registry/blocks/chat-message.json +1 -1
  8. package/registry/blocks/circular-progress-bar-list.json +3 -3
  9. package/registry/blocks/component-search.json +2 -2
  10. package/registry/blocks/content-dropzone.json +1 -1
  11. package/registry/blocks/credit-card-display.json +1 -1
  12. package/registry/blocks/custom-component-helper.json +2 -2
  13. package/registry/blocks/demo-avatars.json +14 -0
  14. package/registry/blocks/empty-state.json +1 -1
  15. package/registry/blocks/faqs-table.json +2 -2
  16. package/registry/blocks/filter-popover.json +11 -11
  17. package/registry/blocks/fixed-column-data-table.json +6 -5
  18. package/registry/blocks/flair-banner.json +1 -1
  19. package/registry/blocks/form-group.json +15 -15
  20. package/registry/blocks/gradient-banner.json +1 -1
  21. package/registry/blocks/graph-metric-tiles.json +1 -1
  22. package/registry/blocks/grid-tiles-list.json +2 -2
  23. package/registry/blocks/image-feed-with-nested-comments.json +6 -5
  24. package/registry/blocks/large-image-labels-list.json +2 -2
  25. package/registry/blocks/loader.json +2 -2
  26. package/registry/blocks/login-branding-panel.json +1 -1
  27. package/registry/blocks/menu-section.json +1 -1
  28. package/registry/blocks/menufocus-template.json +2 -2
  29. package/registry/blocks/messenger-sidebar.json +4 -3
  30. package/registry/blocks/mobile-bottom-nav.json +1 -1
  31. package/registry/blocks/monthly-calendar-widget.json +2 -2
  32. package/registry/blocks/nested-comments-table.json +7 -6
  33. package/registry/blocks/nested-data-table.json +6 -5
  34. package/registry/blocks/page-header-section.json +2 -2
  35. package/registry/blocks/page-previews.json +4 -4
  36. package/registry/blocks/pagination.json +3 -3
  37. package/registry/blocks/participant-list.json +2 -2
  38. package/registry/blocks/persona-card.json +1 -1
  39. package/registry/blocks/pill-tabs.json +2 -2
  40. package/registry/blocks/profile-card.json +3 -3
  41. package/registry/blocks/profile-grid-tiles-list.json +5 -4
  42. package/registry/blocks/profile-image-uploader.json +2 -2
  43. package/registry/blocks/profile-info-cards.json +2 -2
  44. package/registry/blocks/progress-bar.json +1 -1
  45. package/registry/blocks/prompt-template.json +1 -1
  46. package/registry/blocks/reviews-grid.json +1 -1
  47. package/registry/blocks/reviews-table.json +5 -4
  48. package/registry/blocks/screen-flowchart.json +1 -1
  49. package/registry/blocks/screen-prompt-builder.json +2 -2
  50. package/registry/blocks/screen-prompt-template.json +1 -1
  51. package/registry/blocks/search-bar.json +2 -2
  52. package/registry/blocks/search-sidebar.json +8 -8
  53. package/registry/blocks/settings-list-row.json +3 -3
  54. package/registry/blocks/sidebar-cards.json +1 -1
  55. package/registry/blocks/sidebar-profile-card.json +4 -4
  56. package/registry/blocks/slideshow-grid-tiles.json +5 -4
  57. package/registry/blocks/social-feed.json +6 -5
  58. package/registry/blocks/standard-data-table.json +6 -5
  59. package/registry/blocks/standard-list-with-image.json +3 -3
  60. package/registry/blocks/step-tracker.json +1 -1
  61. package/registry/blocks/team-cards-grid.json +1 -1
  62. package/registry/blocks/team-circular-grid.json +1 -1
  63. package/registry/blocks/testimonial-carousel.json +1 -1
  64. package/registry/blocks/title-group.json +4 -4
  65. package/registry/blocks/upvoting-posts-table.json +7 -6
  66. package/registry/blocks/vertical-step-tracker.json +2 -2
  67. package/registry/blocks/video-chat-controls.json +1 -1
  68. package/registry/blocks/video-content-section.json +1 -1
  69. package/registry/blocks/video-playlist.json +1 -1
  70. package/registry/blocks/webcam-preview.json +1 -1
  71. package/registry/blocks/youtube-player.json +1 -1
  72. package/registry/index.json +5 -0
  73. package/registry/layout/account-settings-shell.json +3 -3
  74. package/registry/layout/dashboard-shell.json +5 -5
  75. package/registry/layout/double-sidebar-shell.json +5 -5
  76. package/registry/layout/double-sidebar.json +2 -2
  77. package/registry/layout/header.json +6 -5
  78. package/registry/layout/icon-sidebar-shell.json +5 -5
  79. package/registry/layout/icon-sidebar.json +1 -1
  80. package/registry/layout/mobile-menu-shell.json +4 -4
  81. package/registry/layout/multistep-progressbar-shell.json +8 -8
  82. package/registry/layout/multistep-shell.json +6 -6
  83. package/registry/layout/multistep-sidebar-shell.json +7 -7
  84. package/registry/layout/project-context-shell.json +2 -2
  85. package/registry/layout/search-bar-shell.json +7 -7
  86. package/registry/layout/sidebar-nav.json +1 -1
  87. package/registry/layout/sidebar.json +3 -3
  88. package/registry/layout/standard-page-shell.json +6 -6
  89. package/registry/layout/vertical-multistep-shell.json +8 -8
  90. package/registry/ui/avatar.json +1 -1
  91. package/registry/ui/button.json +1 -1
  92. package/registry/ui/calendar.json +2 -2
  93. package/registry/ui/checkbox.json +1 -1
  94. package/registry/ui/date-input.json +1 -1
  95. package/registry/ui/dialog.json +1 -1
  96. package/registry/ui/dropdown-menu.json +1 -1
  97. package/registry/ui/file-uploader.json +1 -1
  98. package/registry/ui/image-uploader.json +1 -1
  99. package/registry/ui/input.json +1 -1
  100. package/registry/ui/label.json +1 -1
  101. package/registry/ui/line-tabs.json +1 -1
  102. package/registry/ui/multiselect-checkbox-field.json +1 -1
  103. package/registry/ui/multiselect-tags.json +1 -1
  104. package/registry/ui/popover.json +1 -1
  105. package/registry/ui/radio-group.json +1 -1
  106. package/registry/ui/range-input.json +2 -2
  107. package/registry/ui/scroll-area.json +1 -1
  108. package/registry/ui/searchbox.json +1 -1
  109. package/registry/ui/select.json +1 -1
  110. package/registry/ui/selectable-pills.json +1 -1
  111. package/registry/ui/separator.json +1 -1
  112. package/registry/ui/sheet.json +1 -1
  113. package/registry/ui/sidebar.json +8 -8
  114. package/registry/ui/skeleton.json +1 -1
  115. package/registry/ui/slider.json +1 -1
  116. package/registry/ui/switch.json +1 -1
  117. package/registry/ui/tabs.json +1 -1
  118. package/registry/ui/text-input.json +1 -1
  119. package/registry/ui/textarea.json +1 -1
  120. package/registry/ui/tooltip.json +1 -1
  121. package/registry/ui/typography.json +1 -1
@@ -6,17 +6,18 @@
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 { Avatar, AvatarImage, AvatarFallback } from \"../ui/avatar\";\nimport { Input } from \"../ui/input\";\nimport { ArrowUp, MessageCircle, Heart, Paperclip } from \"lucide-react\";\nimport { TitleGroup } from \"./title-group\";\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 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?: { id: string; label: string }[];\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\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 actionButtonText = \"Add new\",\n onAddNew,\n onSort,\n onUpvote,\n onComment,\n onReply,\n onLike,\n className,\n}: UpvotingPostsTableProps) {\n return (\n <div\n className={cn(\"flex flex-col w-full\", className)}\n style={{ gap: \"var(--spacing-xl)\" }}\n >\n {/* Header Section */}\n <TitleGroup title={title} subtitle={subtitle} sortOptions={sortOptions} onSort={onSort} actionButtonText={actionButtonText} onAction={onAddNew} />\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 { Avatar, AvatarImage, AvatarFallback } from \"../ui/avatar\";\nimport { Input } from \"../ui/input\";\nimport { ArrowUp, MessageCircle, Heart, Paperclip } from \"lucide-react\";\nimport { TitleGroup } from \"./title-group\";\nimport { AVATAR_NICOLE_PALMER, AVATAR_SARAH_CHEN, AVATAR_ETHAN_BROOKS } from \"./demo-avatars\";\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 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?: { id: string; label: string }[];\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: AVATAR_NICOLE_PALMER,\n};\n\nconst defaultPosts: Post[] = [\n {\n id: \"1\",\n author: {\n id: \"aya\",\n name: \"Sarah Chen\",\n avatarUrl: AVATAR_SARAH_CHEN,\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: \"Ethan Brooks\",\n avatarUrl: AVATAR_ETHAN_BROOKS,\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: \"nicole\",\n name: \"Nicole Palmer\",\n avatarUrl: AVATAR_NICOLE_PALMER,\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\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 actionButtonText = \"Add new\",\n onAddNew,\n onSort,\n onUpvote,\n onComment,\n onReply,\n onLike,\n className,\n}: UpvotingPostsTableProps) {\n return (\n <div\n className={cn(\"flex flex-col w-full\", className)}\n style={{ gap: \"var(--spacing-xl)\" }}\n >\n {/* Header Section */}\n <TitleGroup title={title} subtitle={subtitle} sortOptions={sortOptions} onSort={onSort} actionButtonText={actionButtonText} onAction={onAddNew} />\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": [
13
13
  "lucide-react"
14
14
  ],
15
15
  "registryDependencies": [
16
- "utils",
17
- "button",
18
- "avatar",
19
- "input",
20
- "title-group"
16
+ "lib/utils",
17
+ "ui/button",
18
+ "ui/avatar",
19
+ "ui/input",
20
+ "blocks/title-group",
21
+ "blocks/demo-avatars"
21
22
  ]
22
23
  }
@@ -11,7 +11,7 @@
11
11
  ],
12
12
  "dependencies": [],
13
13
  "registryDependencies": [
14
- "utils",
15
- "step-tracker"
14
+ "lib/utils",
15
+ "blocks/step-tracker"
16
16
  ]
17
17
  }
@@ -13,6 +13,6 @@
13
13
  "lucide-react"
14
14
  ],
15
15
  "registryDependencies": [
16
- "utils"
16
+ "lib/utils"
17
17
  ]
18
18
  }
@@ -11,6 +11,6 @@
11
11
  ],
12
12
  "dependencies": [],
13
13
  "registryDependencies": [
14
- "utils"
14
+ "lib/utils"
15
15
  ]
16
16
  }
@@ -13,6 +13,6 @@
13
13
  "lucide-react"
14
14
  ],
15
15
  "registryDependencies": [
16
- "utils"
16
+ "lib/utils"
17
17
  ]
18
18
  }
@@ -13,6 +13,6 @@
13
13
  "lucide-react"
14
14
  ],
15
15
  "registryDependencies": [
16
- "utils"
16
+ "lib/utils"
17
17
  ]
18
18
  }
@@ -11,6 +11,6 @@
11
11
  ],
12
12
  "dependencies": [],
13
13
  "registryDependencies": [
14
- "utils"
14
+ "lib/utils"
15
15
  ]
16
16
  }
@@ -106,6 +106,11 @@
106
106
  "type": "registry:ui",
107
107
  "description": ""
108
108
  },
109
+ {
110
+ "name": "demo-avatars",
111
+ "type": "registry:block",
112
+ "description": ""
113
+ },
109
114
  {
110
115
  "name": "destination-cards",
111
116
  "type": "registry:block",
@@ -13,8 +13,8 @@
13
13
  "lucide-react"
14
14
  ],
15
15
  "registryDependencies": [
16
- "utils",
17
- "header",
18
- "use-css-variable-sync"
16
+ "lib/utils",
17
+ "layout/header",
18
+ "hooks/use-css-variable-sync"
19
19
  ]
20
20
  }
@@ -14,10 +14,10 @@
14
14
  "@radix-ui/react-visually-hidden"
15
15
  ],
16
16
  "registryDependencies": [
17
- "header",
18
- "use-css-variable-sync",
19
- "sidebar",
20
- "sheet",
21
- "utils"
17
+ "layout/header",
18
+ "hooks/use-css-variable-sync",
19
+ "layout/sidebar",
20
+ "ui/sheet",
21
+ "lib/utils"
22
22
  ]
23
23
  }
@@ -14,10 +14,10 @@
14
14
  "@radix-ui/react-visually-hidden"
15
15
  ],
16
16
  "registryDependencies": [
17
- "header",
18
- "use-css-variable-sync",
19
- "double-sidebar",
20
- "sheet",
21
- "utils"
17
+ "layout/header",
18
+ "hooks/use-css-variable-sync",
19
+ "layout/double-sidebar",
20
+ "ui/sheet",
21
+ "lib/utils"
22
22
  ]
23
23
  }
@@ -14,7 +14,7 @@
14
14
  "@phosphor-icons/react"
15
15
  ],
16
16
  "registryDependencies": [
17
- "utils",
18
- "scroll-area"
17
+ "lib/utils",
18
+ "ui/scroll-area"
19
19
  ]
20
20
  }
@@ -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=\"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 variant=\"primary\"\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 {isMounted ? (\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 <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 )}\n\n {isMounted ? (\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 <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 )}\n\n {isMounted ? (\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 <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 )}\n \n {/* Auth Buttons - Desktop Only */}\n {showAuthButtons && (\n <div className=\"hidden lg:flex items-center gap-[var(--spacing-lg)]\">\n <Button\n variant=\"primary-outline\"\n size=\"default\"\n onClick={onLogin}\n >\n Log in\n </Button>\n <Button\n variant=\"primary\"\n size=\"default\"\n onClick={onSignUp}\n >\n Sign up\n </Button>\n </div>\n )}\n \n {/* Avatar with Dropdown */}\n {isMounted ? (\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 <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 )}\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=\"primary-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=\"primary\"\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\";\nimport { AVATAR_ETHAN_BROOKS, AVATAR_SARAH_CHEN, AVATAR_JASON_MORALES, AVATAR_MARCUS_WEBB, AVATAR_ALEX_REEVES, AVATAR_MAYA_JOHNSON, AVATAR_HANNAH_KIM } from \"../blocks/demo-avatars\";\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: \"Ethan Brooks\",\n senderAvatar: AVATAR_ETHAN_BROOKS,\n timestamp: \"Jun 5, 2023 8:13 AM\",\n },\n {\n id: \"2\",\n senderName: \"Sarah Chen\",\n senderAvatar: AVATAR_SARAH_CHEN,\n timestamp: \"May 2, 2023 11:54 AM\",\n },\n {\n id: \"3\",\n senderName: \"Jason Morales\",\n senderAvatar: AVATAR_JASON_MORALES,\n timestamp: \"Jan 10, 2023 5:22 PM\",\n },\n {\n id: \"4\",\n senderName: \"Marcus Webb\",\n senderAvatar: AVATAR_MARCUS_WEBB,\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: \"Sarah Chen\",\n userAvatar: AVATAR_SARAH_CHEN,\n action: \"liked your photo\",\n timestamp: \"Apr 15, 2023 6:21 AM\",\n },\n {\n id: \"2\",\n userName: \"Alex Reeves\",\n userAvatar: AVATAR_ALEX_REEVES,\n action: \"liked your photo\",\n timestamp: \"Jun 10, 2023 5:45 PM\",\n },\n {\n id: \"3\",\n userName: \"Maya Johnson\",\n userAvatar: AVATAR_MAYA_JOHNSON,\n action: \"liked your photo\",\n timestamp: \"May 9, 2023 2:00 AM\",\n },\n {\n id: \"4\",\n userName: \"Hannah Kim\",\n userAvatar: AVATAR_HANNAH_KIM,\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 = AVATAR_SARAH_CHEN,\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 variant=\"primary\"\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 {isMounted ? (\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 <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 )}\n\n {isMounted ? (\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 <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 )}\n\n {isMounted ? (\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 <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 )}\n \n {/* Auth Buttons - Desktop Only */}\n {showAuthButtons && (\n <div className=\"hidden lg:flex items-center gap-[var(--spacing-lg)]\">\n <Button\n variant=\"primary-outline\"\n size=\"default\"\n onClick={onLogin}\n >\n Log in\n </Button>\n <Button\n variant=\"primary\"\n size=\"default\"\n onClick={onSignUp}\n >\n Sign up\n </Button>\n </div>\n )}\n \n {/* Avatar with Dropdown */}\n {isMounted ? (\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 <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 )}\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=\"primary-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=\"primary\"\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": [
@@ -14,9 +14,10 @@
14
14
  "@phosphor-icons/react"
15
15
  ],
16
16
  "registryDependencies": [
17
- "avatar",
18
- "button",
19
- "dropdown-menu",
20
- "popover"
17
+ "ui/avatar",
18
+ "ui/button",
19
+ "ui/dropdown-menu",
20
+ "ui/popover",
21
+ "blocks/demo-avatars"
21
22
  ]
22
23
  }
@@ -14,10 +14,10 @@
14
14
  "@radix-ui/react-visually-hidden"
15
15
  ],
16
16
  "registryDependencies": [
17
- "header",
18
- "use-css-variable-sync",
19
- "icon-sidebar",
20
- "sheet",
21
- "utils"
17
+ "layout/header",
18
+ "hooks/use-css-variable-sync",
19
+ "layout/icon-sidebar",
20
+ "ui/sheet",
21
+ "lib/utils"
22
22
  ]
23
23
  }
@@ -14,6 +14,6 @@
14
14
  "@phosphor-icons/react"
15
15
  ],
16
16
  "registryDependencies": [
17
- "utils"
17
+ "lib/utils"
18
18
  ]
19
19
  }
@@ -11,9 +11,9 @@
11
11
  ],
12
12
  "dependencies": [],
13
13
  "registryDependencies": [
14
- "utils",
15
- "header",
16
- "use-css-variable-sync",
17
- "mobile-bottom-nav"
14
+ "lib/utils",
15
+ "layout/header",
16
+ "hooks/use-css-variable-sync",
17
+ "blocks/mobile-bottom-nav"
18
18
  ]
19
19
  }
@@ -11,13 +11,13 @@
11
11
  ],
12
12
  "dependencies": [],
13
13
  "registryDependencies": [
14
- "utils",
15
- "header",
16
- "use-css-variable-sync",
17
- "flair-banner",
18
- "page-header-section",
19
- "progress-bar",
20
- "button",
21
- "typography"
14
+ "lib/utils",
15
+ "layout/header",
16
+ "hooks/use-css-variable-sync",
17
+ "blocks/flair-banner",
18
+ "blocks/page-header-section",
19
+ "blocks/progress-bar",
20
+ "ui/button",
21
+ "ui/typography"
22
22
  ]
23
23
  }
@@ -11,11 +11,11 @@
11
11
  ],
12
12
  "dependencies": [],
13
13
  "registryDependencies": [
14
- "utils",
15
- "header",
16
- "use-css-variable-sync",
17
- "step-tracker",
18
- "button",
19
- "typography"
14
+ "lib/utils",
15
+ "layout/header",
16
+ "hooks/use-css-variable-sync",
17
+ "blocks/step-tracker",
18
+ "ui/button",
19
+ "ui/typography"
20
20
  ]
21
21
  }
@@ -11,12 +11,12 @@
11
11
  ],
12
12
  "dependencies": [],
13
13
  "registryDependencies": [
14
- "utils",
15
- "header",
16
- "use-css-variable-sync",
17
- "step-tracker",
18
- "sidebar-cards",
19
- "button",
20
- "typography"
14
+ "lib/utils",
15
+ "layout/header",
16
+ "hooks/use-css-variable-sync",
17
+ "blocks/step-tracker",
18
+ "blocks/sidebar-cards",
19
+ "ui/button",
20
+ "ui/typography"
21
21
  ]
22
22
  }
@@ -14,7 +14,7 @@
14
14
  "@phosphor-icons/react"
15
15
  ],
16
16
  "registryDependencies": [
17
- "utils",
18
- "scroll-area"
17
+ "lib/utils",
18
+ "ui/scroll-area"
19
19
  ]
20
20
  }