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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "canvas-ui-sdk",
3
- "version": "0.3.19",
3
+ "version": "0.3.21",
4
4
  "type": "module",
5
5
  "description": "A comprehensive UI component library with design tokens for building beautiful interfaces",
6
6
  "bin": {
@@ -6,15 +6,16 @@
6
6
  {
7
7
  "path": "components/blocks/activity-feed.tsx",
8
8
  "type": "registry:block",
9
- "content": "\"use client\";\n\nimport { cn } from \"../../lib/utils\";\nimport { Avatar, AvatarImage, AvatarFallback } from \"../ui/avatar\";\nimport { TitleGroup } from \"./title-group\";\nimport { Check, Heart, MessageCircle, FileText } from \"lucide-react\";\n\n// ============================================\n// Types\n// ============================================\n\nexport interface ActivityAuthor {\n id: string;\n name: string;\n avatarUrl?: string;\n}\n\nexport interface ActivityAttachment {\n id: string;\n name: string;\n size: string;\n type?: \"document\" | \"image\" | \"other\";\n}\n\nexport interface BaseActivityItem {\n id: string;\n author: ActivityAuthor;\n timestamp: string;\n}\n\nexport interface StatusChangeActivity extends BaseActivityItem {\n type: \"status_change\";\n action: \"completed\" | \"updated\" | \"started\" | \"archived\";\n projectName: string;\n}\n\nexport interface CommentActivity extends BaseActivityItem {\n type: \"comment\";\n projectName: string;\n content: string;\n likes: number;\n replies: number;\n isLiked?: boolean;\n}\n\nexport interface AttachmentActivity extends BaseActivityItem {\n type: \"attachment\";\n action: \"completed\" | \"uploaded\" | \"shared\";\n projectName: string;\n attachment: ActivityAttachment;\n}\n\nexport type ActivityItem = StatusChangeActivity | CommentActivity | AttachmentActivity;\n\nexport interface ActivityFeedProps {\n /** Section title */\n title?: string;\n /** Section subtitle */\n subtitle?: string;\n /** Activity items to display */\n items?: ActivityItem[];\n /** Callback when like button is clicked */\n onLike?: (itemId: string) => void;\n /** Callback when reply button is clicked */\n onReply?: (itemId: string) => void;\n /** Callback when attachment is clicked */\n onAttachmentClick?: (itemId: string, attachmentId: string) => void;\n /** Additional class names */\n className?: string;\n}\n\n// ============================================\n// Default Data\n// ============================================\n\nconst defaultItems: ActivityItem[] = [\n {\n id: \"1\",\n type: \"status_change\",\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 action: \"completed\",\n projectName: \"Acme Project\",\n timestamp: \"Today at 8:15 AM\",\n },\n {\n id: \"2\",\n type: \"comment\",\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 projectName: \"Acme Project\",\n content: \"Thank you Mary, the invoice looks great! Could you email it to Jeffrey and the Acme team and ask them to please pay by tomorrow?\",\n likes: 30,\n replies: 10,\n isLiked: true,\n timestamp: \"Yesterday at 11:25 AM\",\n },\n {\n id: \"3\",\n type: \"attachment\",\n author: {\n id: \"mary\",\n name: \"Mary Trott\",\n avatarUrl: \"https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=150&h=150&fit=crop&crop=face\",\n },\n action: \"completed\",\n projectName: \"Acme Project\",\n attachment: {\n id: \"inv-1\",\n name: \"Invoice #23J2KF\",\n size: \"10 MB\",\n type: \"document\",\n },\n timestamp: \"3 days ago\",\n },\n];\n\n// ============================================\n// Sub-components\n// ============================================\n\ninterface ActivityLineProps {\n showLine: boolean;\n height?: string;\n}\n\nfunction ActivityLine({ showLine, height = \"64px\" }: ActivityLineProps) {\n if (!showLine) return null;\n return (\n <div\n style={{\n width: \"1px\",\n height,\n backgroundColor: \"var(--canvas-border-disabled)\",\n }}\n />\n );\n}\n\ninterface StatusIconProps {\n status: \"completed\" | \"updated\" | \"started\" | \"archived\";\n}\n\nfunction StatusIcon({ status }: StatusIconProps) {\n if (status === \"completed\") {\n return (\n <div\n className=\"flex items-center justify-center shrink-0\"\n style={{\n width: \"48px\",\n height: \"48px\",\n backgroundColor: \"var(--canvas-success)\",\n borderRadius: \"var(--spacing-3xl)\",\n border: \"1px solid var(--canvas-border)\",\n }}\n >\n <Check size={20} color=\"white\" strokeWidth={2.5} />\n </div>\n );\n }\n \n // Default fallback for other statuses\n return (\n <div\n className=\"flex items-center justify-center shrink-0\"\n style={{\n width: \"48px\",\n height: \"48px\",\n backgroundColor: \"var(--canvas-surface)\",\n borderRadius: \"var(--spacing-3xl)\",\n border: \"1px solid var(--canvas-border)\",\n }}\n />\n );\n}\n\ninterface ActivityAvatarProps {\n avatarUrl?: string;\n name: string;\n}\n\nfunction ActivityAvatar({ avatarUrl, name }: ActivityAvatarProps) {\n return (\n <Avatar\n className=\"shrink-0\"\n style={{\n width: \"48px\",\n height: \"48px\",\n borderRadius: \"var(--spacing-3xl)\",\n border: \"1px solid var(--canvas-border)\",\n }}\n >\n <AvatarImage src={avatarUrl} alt={name} />\n <AvatarFallback>\n {name.split(\" \").map(n => n[0]).join(\"\").slice(0, 2)}\n </AvatarFallback>\n </Avatar>\n );\n}\n\ninterface AttachmentCardProps {\n attachment: ActivityAttachment;\n onClick?: () => void;\n}\n\nfunction AttachmentCard({ attachment, onClick }: AttachmentCardProps) {\n return (\n <div\n className=\"flex items-center cursor-pointer\"\n style={{\n backgroundColor: \"var(--canvas-background)\",\n border: \"1px solid var(--canvas-border)\",\n borderRadius: \"var(--radius-2xl)\",\n padding: \"var(--spacing-xl)\",\n boxShadow: \"0px 1px 8px 0px rgba(0, 0, 0, 0.03)\",\n gap: \"0\",\n }}\n onClick={onClick}\n >\n {/* Icon container */}\n <div\n className=\"flex items-center justify-center shrink-0\"\n style={{\n width: \"64px\",\n height: \"64px\",\n backgroundColor: \"var(--canvas-surface-brand)\",\n border: \"1px solid var(--canvas-primary)\",\n borderRadius: \"var(--radius-md)\",\n }}\n >\n <FileText size={32} style={{ color: \"var(--canvas-primary)\" }} />\n </div>\n \n {/* File info */}\n <div\n className=\"flex flex-col justify-center\"\n style={{\n paddingLeft: \"var(--spacing-xl)\",\n paddingRight: \"var(--spacing-xl)\",\n paddingTop: \"var(--spacing-md)\",\n paddingBottom: \"var(--spacing-md)\",\n }}\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: \"var(--typo-body-s-weight)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n {attachment.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: \"var(--typo-body-s-weight)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text-placeholder)\",\n }}\n >\n {attachment.size}\n </span>\n </div>\n </div>\n );\n}\n\ninterface CommentCardProps {\n content: string;\n likes: number;\n replies: number;\n isLiked?: boolean;\n onLike?: () => void;\n onReply?: () => void;\n}\n\nfunction CommentCard({ content, likes, replies, isLiked, onLike, onReply }: CommentCardProps) {\n return (\n <div\n className=\"flex flex-col w-full\"\n style={{\n backgroundColor: \"var(--canvas-background)\",\n border: \"1px solid var(--canvas-border)\",\n borderRadius: \"var(--radius-md)\",\n padding: \"var(--spacing-4xl)\",\n boxShadow: \"0px 1px 8px 0px rgba(0, 0, 0, 0.03)\",\n gap: \"var(--spacing-lg)\",\n maxWidth: \"580px\",\n }}\n >\n {/* Comment content */}\n <p\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: \"var(--typo-body-s-weight)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text)\",\n margin: 0,\n }}\n >\n {content}\n </p>\n \n {/* Action icons */}\n <div\n className=\"flex items-center\"\n style={{\n gap: \"var(--spacing-lg)\",\n paddingTop: \"var(--spacing-xxs)\",\n paddingBottom: \"var(--spacing-xxs)\",\n }}\n >\n <button\n onClick={onLike}\n className=\"flex items-center justify-center p-0 border-0 bg-transparent cursor-pointer\"\n >\n <Heart\n size={20}\n fill={isLiked ? \"var(--canvas-destructive)\" : \"none\"}\n color={isLiked ? \"var(--canvas-destructive)\" : \"var(--canvas-text)\"}\n />\n </button>\n <button\n onClick={onReply}\n className=\"flex items-center justify-center p-0 border-0 bg-transparent cursor-pointer\"\n >\n <MessageCircle size={20} style={{ color: \"var(--canvas-text)\" }} />\n </button>\n </div>\n \n {/* Stats */}\n <div\n className=\"flex items-start\"\n style={{\n gap: \"var(--spacing-xl)\",\n paddingTop: \"var(--spacing-xs)\",\n }}\n >\n <span\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 500,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text-placeholder)\",\n }}\n >\n {likes} likes\n </span>\n <span\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 500,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text-placeholder)\",\n }}\n >\n {replies} replies\n </span>\n </div>\n </div>\n );\n}\n\nfunction getActionText(action: string): string {\n switch (action) {\n case \"completed\":\n return \"marked\";\n case \"updated\":\n return \"updated\";\n case \"started\":\n return \"started\";\n case \"uploaded\":\n return \"uploaded\";\n case \"shared\":\n return \"shared\";\n default:\n return action;\n }\n}\n\nfunction getActionSuffix(action: string): string {\n switch (action) {\n case \"completed\":\n return \"as complete\";\n case \"updated\":\n return \"\";\n case \"started\":\n return \"\";\n default:\n return \"\";\n }\n}\n\n// ============================================\n// Main Component\n// ============================================\n\n/**\n * Canvas Design System - Activity Feed Block\n * \n * A timeline-style activity feed showing user actions, comments, and file\n * attachments with connecting lines. Useful for project updates, notifications,\n * and collaboration views.\n * \n * @example\n * ```tsx\n * <ActivityFeed\n * title=\"Project status\"\n * subtitle=\"Last updated today\"\n * items={activityItems}\n * onLike={(id) => console.log(\"Liked\", id)}\n * />\n * ```\n */\nexport function ActivityFeed({\n title = \"Project status\",\n subtitle = \"Last updated today\",\n items = defaultItems,\n onLike,\n onReply,\n onAttachmentClick,\n className,\n}: ActivityFeedProps) {\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} />\n\n {/* Activity List */}\n <div className=\"flex flex-col w-full overflow-hidden\">\n {items.map((item, index) => {\n const isLast = index === items.length - 1;\n \n return (\n <div\n key={item.id}\n className=\"flex flex-col w-full\"\n style={{\n paddingTop: index === 0 ? \"0\" : \"var(--spacing-xl)\",\n paddingBottom: isLast ? \"0\" : \"var(--spacing-xl)\",\n }}\n >\n <div\n className=\"flex w-full\"\n style={{ gap: \"var(--spacing-xl)\" }}\n >\n {/* Left column - Avatar/Icon with line */}\n <div\n className=\"flex flex-col items-center shrink-0\"\n style={{ gap: \"var(--spacing-md)\" }}\n >\n {item.type === \"status_change\" ? (\n <StatusIcon status={item.action} />\n ) : (\n <ActivityAvatar\n avatarUrl={item.author.avatarUrl}\n name={item.author.name}\n />\n )}\n <ActivityLine showLine={!isLast} height={item.type === \"comment\" ? \"100%\" : \"64px\"} />\n </div>\n\n {/* Right column - Content */}\n <div\n className=\"flex flex-col flex-1 min-w-0\"\n style={{ gap: \"var(--spacing-lg)\" }}\n >\n {/* Activity header row */}\n <div\n className=\"flex flex-col justify-center\"\n style={{\n minHeight: \"48px\",\n gap: \"0\",\n }}\n >\n {/* Title line */}\n <p\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n margin: 0,\n }}\n >\n <span\n style={{\n fontWeight: 600,\n color: \"var(--canvas-text)\",\n }}\n >\n {item.author.name}\n </span>\n {\" \"}\n <span\n style={{\n fontWeight: \"var(--typo-body-s-weight)\",\n color: \"var(--canvas-text-placeholder)\",\n }}\n >\n {item.type === \"comment\" ? \"comments on\" : getActionText((item as StatusChangeActivity | AttachmentActivity).action)}\n </span>\n {\" \"}\n <span\n style={{\n fontWeight: \"var(--typo-body-s-weight)\",\n color: \"var(--canvas-text)\",\n }}\n >\n {item.type === \"comment\" \n ? (item as CommentActivity).projectName\n : (item as StatusChangeActivity | AttachmentActivity).projectName\n }\n </span>\n {item.type !== \"comment\" && getActionSuffix((item as StatusChangeActivity | AttachmentActivity).action) && (\n <>\n {\" \"}\n <span\n style={{\n fontWeight: \"var(--typo-body-s-weight)\",\n color: \"var(--canvas-text-placeholder)\",\n }}\n >\n {getActionSuffix((item as StatusChangeActivity | AttachmentActivity).action)}\n </span>\n </>\n )}\n </p>\n \n {/* Timestamp */}\n <p\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: \"var(--typo-body-s-weight)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text-placeholder)\",\n margin: 0,\n }}\n >\n {item.timestamp}\n </p>\n </div>\n\n {/* Additional content based on type */}\n {item.type === \"comment\" && (\n <CommentCard\n content={(item as CommentActivity).content}\n likes={(item as CommentActivity).likes}\n replies={(item as CommentActivity).replies}\n isLiked={(item as CommentActivity).isLiked}\n onLike={() => onLike?.(item.id)}\n onReply={() => onReply?.(item.id)}\n />\n )}\n\n {item.type === \"attachment\" && (\n <AttachmentCard\n attachment={(item as AttachmentActivity).attachment}\n onClick={() => onAttachmentClick?.(item.id, (item as AttachmentActivity).attachment.id)}\n />\n )}\n </div>\n </div>\n </div>\n );\n })}\n </div>\n </div>\n );\n}\n"
9
+ "content": "\"use client\";\n\nimport { cn } from \"../../lib/utils\";\nimport { Avatar, AvatarImage, AvatarFallback } from \"../ui/avatar\";\nimport { TitleGroup } from \"./title-group\";\nimport { Check, Heart, MessageCircle, FileText } from \"lucide-react\";\nimport { AVATAR_ETHAN_BROOKS, AVATAR_NICOLE_PALMER } from \"./demo-avatars\";\n\n// ============================================\n// Types\n// ============================================\n\nexport interface ActivityAuthor {\n id: string;\n name: string;\n avatarUrl?: string;\n}\n\nexport interface ActivityAttachment {\n id: string;\n name: string;\n size: string;\n type?: \"document\" | \"image\" | \"other\";\n}\n\nexport interface BaseActivityItem {\n id: string;\n author: ActivityAuthor;\n timestamp: string;\n}\n\nexport interface StatusChangeActivity extends BaseActivityItem {\n type: \"status_change\";\n action: \"completed\" | \"updated\" | \"started\" | \"archived\";\n projectName: string;\n}\n\nexport interface CommentActivity extends BaseActivityItem {\n type: \"comment\";\n projectName: string;\n content: string;\n likes: number;\n replies: number;\n isLiked?: boolean;\n}\n\nexport interface AttachmentActivity extends BaseActivityItem {\n type: \"attachment\";\n action: \"completed\" | \"uploaded\" | \"shared\";\n projectName: string;\n attachment: ActivityAttachment;\n}\n\nexport type ActivityItem = StatusChangeActivity | CommentActivity | AttachmentActivity;\n\nexport interface ActivityFeedProps {\n /** Section title */\n title?: string;\n /** Section subtitle */\n subtitle?: string;\n /** Activity items to display */\n items?: ActivityItem[];\n /** Callback when like button is clicked */\n onLike?: (itemId: string) => void;\n /** Callback when reply button is clicked */\n onReply?: (itemId: string) => void;\n /** Callback when attachment is clicked */\n onAttachmentClick?: (itemId: string, attachmentId: string) => void;\n /** Additional class names */\n className?: string;\n}\n\n// ============================================\n// Default Data\n// ============================================\n\nconst defaultItems: ActivityItem[] = [\n {\n id: \"1\",\n type: \"status_change\",\n author: {\n id: \"ethan\",\n name: \"Ethan Brooks\",\n avatarUrl: AVATAR_ETHAN_BROOKS,\n },\n action: \"completed\",\n projectName: \"Acme Project\",\n timestamp: \"Today at 8:15 AM\",\n },\n {\n id: \"2\",\n type: \"comment\",\n author: {\n id: \"ethan\",\n name: \"Ethan Brooks\",\n avatarUrl: AVATAR_ETHAN_BROOKS,\n },\n projectName: \"Acme Project\",\n content: \"Thank you Nicole, the invoice looks great! Could you email it to Marcus and the Acme team and ask them to please pay by tomorrow?\",\n likes: 30,\n replies: 10,\n isLiked: true,\n timestamp: \"Yesterday at 11:25 AM\",\n },\n {\n id: \"3\",\n type: \"attachment\",\n author: {\n id: \"nicole\",\n name: \"Nicole Palmer\",\n avatarUrl: AVATAR_NICOLE_PALMER,\n },\n action: \"completed\",\n projectName: \"Acme Project\",\n attachment: {\n id: \"inv-1\",\n name: \"Invoice #23J2KF\",\n size: \"10 MB\",\n type: \"document\",\n },\n timestamp: \"3 days ago\",\n },\n];\n\n// ============================================\n// Sub-components\n// ============================================\n\ninterface ActivityLineProps {\n showLine: boolean;\n height?: string;\n}\n\nfunction ActivityLine({ showLine, height = \"64px\" }: ActivityLineProps) {\n if (!showLine) return null;\n return (\n <div\n style={{\n width: \"1px\",\n height,\n backgroundColor: \"var(--canvas-border-disabled)\",\n }}\n />\n );\n}\n\ninterface StatusIconProps {\n status: \"completed\" | \"updated\" | \"started\" | \"archived\";\n}\n\nfunction StatusIcon({ status }: StatusIconProps) {\n if (status === \"completed\") {\n return (\n <div\n className=\"flex items-center justify-center shrink-0\"\n style={{\n width: \"48px\",\n height: \"48px\",\n backgroundColor: \"var(--canvas-success)\",\n borderRadius: \"var(--spacing-3xl)\",\n border: \"1px solid var(--canvas-border)\",\n }}\n >\n <Check size={20} color=\"white\" strokeWidth={2.5} />\n </div>\n );\n }\n \n // Default fallback for other statuses\n return (\n <div\n className=\"flex items-center justify-center shrink-0\"\n style={{\n width: \"48px\",\n height: \"48px\",\n backgroundColor: \"var(--canvas-surface)\",\n borderRadius: \"var(--spacing-3xl)\",\n border: \"1px solid var(--canvas-border)\",\n }}\n />\n );\n}\n\ninterface ActivityAvatarProps {\n avatarUrl?: string;\n name: string;\n}\n\nfunction ActivityAvatar({ avatarUrl, name }: ActivityAvatarProps) {\n return (\n <Avatar\n className=\"shrink-0\"\n style={{\n width: \"48px\",\n height: \"48px\",\n borderRadius: \"var(--spacing-3xl)\",\n border: \"1px solid var(--canvas-border)\",\n }}\n >\n <AvatarImage src={avatarUrl} alt={name} />\n <AvatarFallback>\n {name.split(\" \").map(n => n[0]).join(\"\").slice(0, 2)}\n </AvatarFallback>\n </Avatar>\n );\n}\n\ninterface AttachmentCardProps {\n attachment: ActivityAttachment;\n onClick?: () => void;\n}\n\nfunction AttachmentCard({ attachment, onClick }: AttachmentCardProps) {\n return (\n <div\n className=\"flex items-center cursor-pointer\"\n style={{\n backgroundColor: \"var(--canvas-background)\",\n border: \"1px solid var(--canvas-border)\",\n borderRadius: \"var(--radius-2xl)\",\n padding: \"var(--spacing-xl)\",\n boxShadow: \"0px 1px 8px 0px rgba(0, 0, 0, 0.03)\",\n gap: \"0\",\n }}\n onClick={onClick}\n >\n {/* Icon container */}\n <div\n className=\"flex items-center justify-center shrink-0\"\n style={{\n width: \"64px\",\n height: \"64px\",\n backgroundColor: \"var(--canvas-surface-brand)\",\n border: \"1px solid var(--canvas-primary)\",\n borderRadius: \"var(--radius-md)\",\n }}\n >\n <FileText size={32} style={{ color: \"var(--canvas-primary)\" }} />\n </div>\n \n {/* File info */}\n <div\n className=\"flex flex-col justify-center\"\n style={{\n paddingLeft: \"var(--spacing-xl)\",\n paddingRight: \"var(--spacing-xl)\",\n paddingTop: \"var(--spacing-md)\",\n paddingBottom: \"var(--spacing-md)\",\n }}\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: \"var(--typo-body-s-weight)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n {attachment.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: \"var(--typo-body-s-weight)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text-placeholder)\",\n }}\n >\n {attachment.size}\n </span>\n </div>\n </div>\n );\n}\n\ninterface CommentCardProps {\n content: string;\n likes: number;\n replies: number;\n isLiked?: boolean;\n onLike?: () => void;\n onReply?: () => void;\n}\n\nfunction CommentCard({ content, likes, replies, isLiked, onLike, onReply }: CommentCardProps) {\n return (\n <div\n className=\"flex flex-col w-full\"\n style={{\n backgroundColor: \"var(--canvas-background)\",\n border: \"1px solid var(--canvas-border)\",\n borderRadius: \"var(--radius-md)\",\n padding: \"var(--spacing-4xl)\",\n boxShadow: \"0px 1px 8px 0px rgba(0, 0, 0, 0.03)\",\n gap: \"var(--spacing-lg)\",\n maxWidth: \"580px\",\n }}\n >\n {/* Comment content */}\n <p\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: \"var(--typo-body-s-weight)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text)\",\n margin: 0,\n }}\n >\n {content}\n </p>\n \n {/* Action icons */}\n <div\n className=\"flex items-center\"\n style={{\n gap: \"var(--spacing-lg)\",\n paddingTop: \"var(--spacing-xxs)\",\n paddingBottom: \"var(--spacing-xxs)\",\n }}\n >\n <button\n onClick={onLike}\n className=\"flex items-center justify-center p-0 border-0 bg-transparent cursor-pointer\"\n >\n <Heart\n size={20}\n fill={isLiked ? \"var(--canvas-destructive)\" : \"none\"}\n color={isLiked ? \"var(--canvas-destructive)\" : \"var(--canvas-text)\"}\n />\n </button>\n <button\n onClick={onReply}\n className=\"flex items-center justify-center p-0 border-0 bg-transparent cursor-pointer\"\n >\n <MessageCircle size={20} style={{ color: \"var(--canvas-text)\" }} />\n </button>\n </div>\n \n {/* Stats */}\n <div\n className=\"flex items-start\"\n style={{\n gap: \"var(--spacing-xl)\",\n paddingTop: \"var(--spacing-xs)\",\n }}\n >\n <span\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 500,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text-placeholder)\",\n }}\n >\n {likes} likes\n </span>\n <span\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 500,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text-placeholder)\",\n }}\n >\n {replies} replies\n </span>\n </div>\n </div>\n );\n}\n\nfunction getActionText(action: string): string {\n switch (action) {\n case \"completed\":\n return \"marked\";\n case \"updated\":\n return \"updated\";\n case \"started\":\n return \"started\";\n case \"uploaded\":\n return \"uploaded\";\n case \"shared\":\n return \"shared\";\n default:\n return action;\n }\n}\n\nfunction getActionSuffix(action: string): string {\n switch (action) {\n case \"completed\":\n return \"as complete\";\n case \"updated\":\n return \"\";\n case \"started\":\n return \"\";\n default:\n return \"\";\n }\n}\n\n// ============================================\n// Main Component\n// ============================================\n\n/**\n * Canvas Design System - Activity Feed Block\n * \n * A timeline-style activity feed showing user actions, comments, and file\n * attachments with connecting lines. Useful for project updates, notifications,\n * and collaboration views.\n * \n * @example\n * ```tsx\n * <ActivityFeed\n * title=\"Project status\"\n * subtitle=\"Last updated today\"\n * items={activityItems}\n * onLike={(id) => console.log(\"Liked\", id)}\n * />\n * ```\n */\nexport function ActivityFeed({\n title = \"Project status\",\n subtitle = \"Last updated today\",\n items = defaultItems,\n onLike,\n onReply,\n onAttachmentClick,\n className,\n}: ActivityFeedProps) {\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} />\n\n {/* Activity List */}\n <div className=\"flex flex-col w-full overflow-hidden\">\n {items.map((item, index) => {\n const isLast = index === items.length - 1;\n \n return (\n <div\n key={item.id}\n className=\"flex flex-col w-full\"\n style={{\n paddingTop: index === 0 ? \"0\" : \"var(--spacing-xl)\",\n paddingBottom: isLast ? \"0\" : \"var(--spacing-xl)\",\n }}\n >\n <div\n className=\"flex w-full\"\n style={{ gap: \"var(--spacing-xl)\" }}\n >\n {/* Left column - Avatar/Icon with line */}\n <div\n className=\"flex flex-col items-center shrink-0\"\n style={{ gap: \"var(--spacing-md)\" }}\n >\n {item.type === \"status_change\" ? (\n <StatusIcon status={item.action} />\n ) : (\n <ActivityAvatar\n avatarUrl={item.author.avatarUrl}\n name={item.author.name}\n />\n )}\n <ActivityLine showLine={!isLast} height={item.type === \"comment\" ? \"100%\" : \"64px\"} />\n </div>\n\n {/* Right column - Content */}\n <div\n className=\"flex flex-col flex-1 min-w-0\"\n style={{ gap: \"var(--spacing-lg)\" }}\n >\n {/* Activity header row */}\n <div\n className=\"flex flex-col justify-center\"\n style={{\n minHeight: \"48px\",\n gap: \"0\",\n }}\n >\n {/* Title line */}\n <p\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n margin: 0,\n }}\n >\n <span\n style={{\n fontWeight: 600,\n color: \"var(--canvas-text)\",\n }}\n >\n {item.author.name}\n </span>\n {\" \"}\n <span\n style={{\n fontWeight: \"var(--typo-body-s-weight)\",\n color: \"var(--canvas-text-placeholder)\",\n }}\n >\n {item.type === \"comment\" ? \"comments on\" : getActionText((item as StatusChangeActivity | AttachmentActivity).action)}\n </span>\n {\" \"}\n <span\n style={{\n fontWeight: \"var(--typo-body-s-weight)\",\n color: \"var(--canvas-text)\",\n }}\n >\n {item.type === \"comment\" \n ? (item as CommentActivity).projectName\n : (item as StatusChangeActivity | AttachmentActivity).projectName\n }\n </span>\n {item.type !== \"comment\" && getActionSuffix((item as StatusChangeActivity | AttachmentActivity).action) && (\n <>\n {\" \"}\n <span\n style={{\n fontWeight: \"var(--typo-body-s-weight)\",\n color: \"var(--canvas-text-placeholder)\",\n }}\n >\n {getActionSuffix((item as StatusChangeActivity | AttachmentActivity).action)}\n </span>\n </>\n )}\n </p>\n \n {/* Timestamp */}\n <p\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: \"var(--typo-body-s-weight)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text-placeholder)\",\n margin: 0,\n }}\n >\n {item.timestamp}\n </p>\n </div>\n\n {/* Additional content based on type */}\n {item.type === \"comment\" && (\n <CommentCard\n content={(item as CommentActivity).content}\n likes={(item as CommentActivity).likes}\n replies={(item as CommentActivity).replies}\n isLiked={(item as CommentActivity).isLiked}\n onLike={() => onLike?.(item.id)}\n onReply={() => onReply?.(item.id)}\n />\n )}\n\n {item.type === \"attachment\" && (\n <AttachmentCard\n attachment={(item as AttachmentActivity).attachment}\n onClick={() => onAttachmentClick?.(item.id, (item as AttachmentActivity).attachment.id)}\n />\n )}\n </div>\n </div>\n </div>\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
- "avatar",
18
- "title-group"
16
+ "lib/utils",
17
+ "ui/avatar",
18
+ "blocks/title-group",
19
+ "blocks/demo-avatars"
19
20
  ]
20
21
  }
@@ -6,14 +6,15 @@
6
6
  {
7
7
  "path": "components/blocks/bottom-input-chat-widget.tsx",
8
8
  "type": "registry:block",
9
- "content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { ChevronLeft, ChevronRight, MoreVertical, Paperclip } from \"lucide-react\";\nimport { Avatar, AvatarFallback, AvatarImage } from \"../ui/avatar\";\nimport { cn } from \"../../lib/utils\";\n\n// ============================================\n// Types\n// ============================================\n\nexport interface ChatThread {\n id: string;\n name: string;\n avatarUrl?: string;\n preview: string;\n unreadCount?: number;\n}\n\nexport interface ChatMessageData {\n id: string;\n senderName: string;\n senderAvatar?: string;\n content: string;\n timestamp: string;\n}\n\ninterface MessageThreadItemProps {\n thread: ChatThread;\n onClick?: () => void;\n}\n\ninterface ConversationMessageProps {\n message: ChatMessageData;\n}\n\ninterface ChatInputBarProps {\n placeholder?: string;\n onSend?: (message: string) => void;\n}\n\ninterface ConversationHeaderProps {\n name: string;\n onBackClick?: () => void;\n onMenuClick?: () => void;\n}\n\nexport interface BottomInputChatWidgetProps {\n variant?: \"threads\" | \"conversation\";\n title?: string;\n threads?: ChatThread[];\n messages?: ChatMessageData[];\n conversationName?: string;\n onThreadClick?: (threadId: string) => void;\n onBackClick?: () => void;\n onSend?: (message: string) => void;\n className?: string;\n}\n\n// ============================================\n// Default Data\n// ============================================\n\nconst defaultThreads: ChatThread[] = [\n {\n id: \"1\",\n name: \"Jeffrey Connor\",\n avatarUrl: \"https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=150&h=150&fit=crop&crop=face\",\n preview: \"That's true. Let's go ahead and book the mountain cabin then!\",\n unreadCount: 12,\n },\n {\n id: \"2\",\n name: \"Mary Trott\",\n avatarUrl: \"https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=150&h=150&fit=crop&crop=face\",\n preview: \"Sounds great! Let's meet at 2PM.\",\n },\n {\n id: \"3\",\n name: \"Taylor Reed\",\n avatarUrl: \"https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=150&h=150&fit=crop&crop=face\",\n preview: \"Thanks for sending the conference agenda. I'll add my comments in the document.\",\n },\n];\n\nconst defaultMessages: ChatMessageData[] = [\n {\n id: \"1\",\n senderName: \"Jeffrey Connor\",\n senderAvatar: \"https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=150&h=150&fit=crop&crop=face\",\n content: \"That's true. Let's go ahead and book the mountain cabin then!\",\n timestamp: \"Feb 23, 1:32 PM\",\n },\n {\n id: \"2\",\n senderName: \"Mary Trott\",\n senderAvatar: \"https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=150&h=150&fit=crop&crop=face\",\n content: \"Sounds great! Let's meet at 2PM.\",\n timestamp: \"Feb 23, 3:50 PM\",\n },\n {\n id: \"3\",\n senderName: \"Taylor Reed\",\n senderAvatar: \"https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=150&h=150&fit=crop&crop=face\",\n content: \"Thanks for sending the conference agenda. I'll add my comments in the document.\",\n timestamp: \"Feb 23, 6:13 PM\",\n },\n];\n\n// ============================================\n// Sub-components\n// ============================================\n\nfunction MessageThreadItem({ thread, onClick }: MessageThreadItemProps) {\n const initials = thread.name\n .split(\" \")\n .map((n) => n[0])\n .join(\"\");\n\n return (\n <div\n className=\"flex items-start gap-[var(--spacing-xl)] py-[var(--spacing-2xl)] cursor-pointer hover:opacity-80 transition-opacity\"\n style={{\n borderTop: \"1px solid var(--canvas-border)\",\n borderBottom: \"1px solid var(--canvas-border)\",\n }}\n onClick={onClick}\n >\n {/* Avatar */}\n <Avatar\n className=\"size-12 shrink-0\"\n style={{\n borderRadius: \"var(--spacing-3xl)\",\n border: \"1px solid var(--canvas-border)\",\n }}\n >\n <AvatarImage src={thread.avatarUrl} alt={thread.name} />\n <AvatarFallback\n style={{\n backgroundColor: \"var(--canvas-surface)\",\n color: \"var(--canvas-text-muted)\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 600,\n }}\n >\n {initials}\n </AvatarFallback>\n </Avatar>\n\n {/* Content */}\n <div className=\"flex-1 min-w-0\">\n <p\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 {thread.name}\n </p>\n <p\n className=\"truncate\"\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 {thread.preview}\n </p>\n </div>\n\n {/* Right side: unread badge + chevron */}\n <div className=\"flex items-center gap-[var(--spacing-xl)] self-stretch shrink-0\">\n {thread.unreadCount && thread.unreadCount > 0 && (\n <div\n className=\"flex items-center justify-center size-10 rounded-full\"\n style={{\n backgroundColor: \"var(--canvas-primary)\",\n }}\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-primary-foreground)\",\n }}\n >\n {thread.unreadCount}\n </span>\n </div>\n )}\n <ChevronRight\n className=\"size-10\"\n style={{ color: \"var(--canvas-text-placeholder)\" }}\n strokeWidth={1.5}\n />\n </div>\n </div>\n );\n}\n\nfunction ConversationMessage({ message }: ConversationMessageProps) {\n const initials = message.senderName\n .split(\" \")\n .map((n) => n[0])\n .join(\"\");\n\n return (\n <div className=\"flex items-start gap-[var(--spacing-xl)] py-[var(--spacing-2xl)]\">\n {/* Avatar */}\n <Avatar\n className=\"size-12 shrink-0\"\n style={{\n borderRadius: \"var(--radius-xs)\",\n border: \"1px solid var(--canvas-border)\",\n }}\n >\n <AvatarImage src={message.senderAvatar} alt={message.senderName} />\n <AvatarFallback\n style={{\n backgroundColor: \"var(--canvas-surface)\",\n color: \"var(--canvas-text-muted)\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 600,\n borderRadius: \"var(--radius-xs)\",\n }}\n >\n {initials}\n </AvatarFallback>\n </Avatar>\n\n {/* Content */}\n <div className=\"flex-1 min-w-0\">\n <div className=\"flex items-center gap-[var(--spacing-xl)]\">\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 {message.senderName}\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 {message.timestamp}\n </span>\n </div>\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 }}\n >\n {message.content}\n </p>\n </div>\n </div>\n );\n}\n\nfunction ConversationHeader({ name, onBackClick, onMenuClick }: ConversationHeaderProps) {\n return (\n <div className=\"flex items-center gap-[var(--spacing-xs)]\">\n <button\n onClick={onBackClick}\n className=\"size-10 flex items-center justify-center hover:opacity-70 transition-opacity\"\n aria-label=\"Go back\"\n >\n <ChevronLeft\n className=\"size-6\"\n style={{ color: \"var(--canvas-text-placeholder)\" }}\n strokeWidth={1.5}\n />\n </button>\n <span\n className=\"flex-1\"\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: \"1.5\",\n color: \"var(--canvas-text)\",\n }}\n >\n {name}\n </span>\n <button\n onClick={onMenuClick}\n className=\"size-8 flex items-center justify-center hover:opacity-70 transition-opacity rounded-full\"\n aria-label=\"More options\"\n >\n <MoreVertical\n className=\"size-5\"\n style={{ color: \"var(--canvas-text-placeholder)\" }}\n />\n </button>\n </div>\n );\n}\n\nfunction ChatInputBar({ placeholder = \"Send a message\", onSend }: ChatInputBarProps) {\n const [value, setValue] = useState(\"\");\n\n const handleSend = () => {\n if (value.trim() && onSend) {\n onSend(value.trim());\n setValue(\"\");\n }\n };\n\n const handleKeyDown = (e: React.KeyboardEvent) => {\n if (e.key === \"Enter\" && !e.shiftKey) {\n e.preventDefault();\n handleSend();\n }\n };\n\n return (\n <div\n className=\"flex items-center gap-[var(--spacing-xl)] h-20\"\n style={{\n borderTop: \"1px solid var(--canvas-border)\",\n borderBottom: \"1px solid var(--canvas-border)\",\n }}\n >\n {/* Input field */}\n <div className=\"flex-1 flex items-center gap-[var(--spacing-md)] h-11\">\n <input\n type=\"text\"\n value={value}\n onChange={(e) => setValue(e.target.value)}\n onKeyDown={handleKeyDown}\n placeholder={placeholder}\n className=\"flex-1 bg-transparent outline-none\"\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 <button\n className=\"size-6 flex items-center justify-center hover:opacity-70 transition-opacity\"\n aria-label=\"Attach file\"\n >\n <Paperclip\n className=\"size-6\"\n style={{ color: \"var(--canvas-text-placeholder)\" }}\n />\n </button>\n </div>\n\n {/* Send button */}\n <button\n onClick={handleSend}\n className=\"h-11 px-[var(--spacing-xl)] flex items-center justify-center transition-colors hover:opacity-90\"\n style={{\n backgroundColor: \"var(--btn-primary-bg)\",\n color: \"var(--btn-primary-text)\",\n borderRadius: \"var(--radius-xs)\",\n boxShadow: \"0px 1px 8px 0px rgba(0,0,0,0.03)\",\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 }}\n >\n Send\n </button>\n </div>\n );\n}\n\n// ============================================\n// Main Component\n// ============================================\n\nexport function BottomInputChatWidget({\n variant = \"threads\",\n title = \"Messages\",\n threads = defaultThreads,\n messages = defaultMessages,\n conversationName = \"Mary Trott\",\n onThreadClick,\n onBackClick,\n onSend,\n className,\n}: BottomInputChatWidgetProps) {\n if (variant === \"threads\") {\n return (\n <div\n className={cn(\n \"flex flex-col gap-[var(--spacing-3xl)] py-[var(--spacing-xl)]\",\n className\n )}\n style={{\n backgroundColor: \"var(--canvas-background)\",\n }}\n >\n {/* Title */}\n <div className=\"flex flex-col gap-[var(--spacing-xl)]\">\n <h2\n style={{\n fontFamily: \"var(--typo-h6-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-h6-size)\",\n fontWeight: \"var(--typo-h6-weight)\",\n lineHeight: \"var(--typo-h6-line-height)\",\n color: \"var(--canvas-text)\",\n margin: 0,\n }}\n >\n {title}\n </h2>\n </div>\n\n {/* Thread list */}\n <div className=\"flex flex-col\">\n {threads.map((thread, index) => (\n <div\n key={thread.id}\n style={{\n borderTop: index === 0 ? \"1px solid var(--canvas-border)\" : \"none\",\n }}\n >\n <MessageThreadItem\n thread={thread}\n onClick={() => onThreadClick?.(thread.id)}\n />\n </div>\n ))}\n </div>\n </div>\n );\n }\n\n // Conversation variant\n return (\n <div\n className={cn(\n \"flex flex-col gap-[var(--spacing-3xl)] py-[var(--spacing-xl)]\",\n className\n )}\n style={{\n backgroundColor: \"var(--canvas-background)\",\n }}\n >\n {/* Messages section */}\n <div className=\"flex flex-col gap-[var(--spacing-xl)]\">\n {/* Header */}\n <ConversationHeader\n name={conversationName}\n onBackClick={onBackClick}\n />\n\n {/* Messages list */}\n <div className=\"flex flex-col\">\n {messages.map((message, index) => (\n <div\n key={message.id}\n style={{\n borderTop: index === 0 ? \"1px solid var(--canvas-border)\" : \"none\",\n }}\n >\n <ConversationMessage message={message} />\n </div>\n ))}\n </div>\n\n {/* Input bar */}\n <ChatInputBar onSend={onSend} />\n </div>\n </div>\n );\n}\n"
9
+ "content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { ChevronLeft, ChevronRight, MoreVertical, Paperclip } from \"lucide-react\";\nimport { Avatar, AvatarFallback, AvatarImage } from \"../ui/avatar\";\nimport { cn } from \"../../lib/utils\";\nimport { AVATAR_ETHAN_BROOKS, AVATAR_SARAH_CHEN, AVATAR_NICOLE_PALMER } from \"./demo-avatars\";\n\n// ============================================\n// Types\n// ============================================\n\nexport interface ChatThread {\n id: string;\n name: string;\n avatarUrl?: string;\n preview: string;\n unreadCount?: number;\n}\n\nexport interface ChatMessageData {\n id: string;\n senderName: string;\n senderAvatar?: string;\n content: string;\n timestamp: string;\n}\n\ninterface MessageThreadItemProps {\n thread: ChatThread;\n onClick?: () => void;\n}\n\ninterface ConversationMessageProps {\n message: ChatMessageData;\n}\n\ninterface ChatInputBarProps {\n placeholder?: string;\n onSend?: (message: string) => void;\n}\n\ninterface ConversationHeaderProps {\n name: string;\n onBackClick?: () => void;\n onMenuClick?: () => void;\n}\n\nexport interface BottomInputChatWidgetProps {\n variant?: \"threads\" | \"conversation\";\n title?: string;\n threads?: ChatThread[];\n messages?: ChatMessageData[];\n conversationName?: string;\n onThreadClick?: (threadId: string) => void;\n onBackClick?: () => void;\n onSend?: (message: string) => void;\n className?: string;\n}\n\n// ============================================\n// Default Data\n// ============================================\n\nconst defaultThreads: ChatThread[] = [\n {\n id: \"1\",\n name: \"Ethan Brooks\",\n avatarUrl: AVATAR_ETHAN_BROOKS,\n preview: \"That's true. Let's go ahead and book the mountain cabin then!\",\n unreadCount: 12,\n },\n {\n id: \"2\",\n name: \"Sarah Chen\",\n avatarUrl: AVATAR_SARAH_CHEN,\n preview: \"Sounds great! Let's meet at 2PM.\",\n },\n {\n id: \"3\",\n name: \"Nicole Palmer\",\n avatarUrl: AVATAR_NICOLE_PALMER,\n preview: \"Thanks for sending the conference agenda. I'll add my comments in the document.\",\n },\n];\n\nconst defaultMessages: ChatMessageData[] = [\n {\n id: \"1\",\n senderName: \"Ethan Brooks\",\n senderAvatar: AVATAR_ETHAN_BROOKS,\n content: \"That's true. Let's go ahead and book the mountain cabin then!\",\n timestamp: \"Feb 23, 1:32 PM\",\n },\n {\n id: \"2\",\n senderName: \"Sarah Chen\",\n senderAvatar: AVATAR_SARAH_CHEN,\n content: \"Sounds great! Let's meet at 2PM.\",\n timestamp: \"Feb 23, 3:50 PM\",\n },\n {\n id: \"3\",\n senderName: \"Nicole Palmer\",\n senderAvatar: AVATAR_NICOLE_PALMER,\n content: \"Thanks for sending the conference agenda. I'll add my comments in the document.\",\n timestamp: \"Feb 23, 6:13 PM\",\n },\n];\n\n// ============================================\n// Sub-components\n// ============================================\n\nfunction MessageThreadItem({ thread, onClick }: MessageThreadItemProps) {\n const initials = thread.name\n .split(\" \")\n .map((n) => n[0])\n .join(\"\");\n\n return (\n <div\n className=\"flex items-start gap-[var(--spacing-xl)] py-[var(--spacing-2xl)] cursor-pointer hover:opacity-80 transition-opacity\"\n style={{\n borderTop: \"1px solid var(--canvas-border)\",\n borderBottom: \"1px solid var(--canvas-border)\",\n }}\n onClick={onClick}\n >\n {/* Avatar */}\n <Avatar\n className=\"size-12 shrink-0\"\n style={{\n borderRadius: \"var(--spacing-3xl)\",\n border: \"1px solid var(--canvas-border)\",\n }}\n >\n <AvatarImage src={thread.avatarUrl} alt={thread.name} />\n <AvatarFallback\n style={{\n backgroundColor: \"var(--canvas-surface)\",\n color: \"var(--canvas-text-muted)\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 600,\n }}\n >\n {initials}\n </AvatarFallback>\n </Avatar>\n\n {/* Content */}\n <div className=\"flex-1 min-w-0\">\n <p\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 {thread.name}\n </p>\n <p\n className=\"truncate\"\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 {thread.preview}\n </p>\n </div>\n\n {/* Right side: unread badge + chevron */}\n <div className=\"flex items-center gap-[var(--spacing-xl)] self-stretch shrink-0\">\n {thread.unreadCount && thread.unreadCount > 0 && (\n <div\n className=\"flex items-center justify-center size-10 rounded-full\"\n style={{\n backgroundColor: \"var(--canvas-primary)\",\n }}\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-primary-foreground)\",\n }}\n >\n {thread.unreadCount}\n </span>\n </div>\n )}\n <ChevronRight\n className=\"size-10\"\n style={{ color: \"var(--canvas-text-placeholder)\" }}\n strokeWidth={1.5}\n />\n </div>\n </div>\n );\n}\n\nfunction ConversationMessage({ message }: ConversationMessageProps) {\n const initials = message.senderName\n .split(\" \")\n .map((n) => n[0])\n .join(\"\");\n\n return (\n <div className=\"flex items-start gap-[var(--spacing-xl)] py-[var(--spacing-2xl)]\">\n {/* Avatar */}\n <Avatar\n className=\"size-12 shrink-0\"\n style={{\n borderRadius: \"var(--radius-xs)\",\n border: \"1px solid var(--canvas-border)\",\n }}\n >\n <AvatarImage src={message.senderAvatar} alt={message.senderName} />\n <AvatarFallback\n style={{\n backgroundColor: \"var(--canvas-surface)\",\n color: \"var(--canvas-text-muted)\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 600,\n borderRadius: \"var(--radius-xs)\",\n }}\n >\n {initials}\n </AvatarFallback>\n </Avatar>\n\n {/* Content */}\n <div className=\"flex-1 min-w-0\">\n <div className=\"flex items-center gap-[var(--spacing-xl)]\">\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 {message.senderName}\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 {message.timestamp}\n </span>\n </div>\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 }}\n >\n {message.content}\n </p>\n </div>\n </div>\n );\n}\n\nfunction ConversationHeader({ name, onBackClick, onMenuClick }: ConversationHeaderProps) {\n return (\n <div className=\"flex items-center gap-[var(--spacing-xs)]\">\n <button\n onClick={onBackClick}\n className=\"size-10 flex items-center justify-center hover:opacity-70 transition-opacity\"\n aria-label=\"Go back\"\n >\n <ChevronLeft\n className=\"size-6\"\n style={{ color: \"var(--canvas-text-placeholder)\" }}\n strokeWidth={1.5}\n />\n </button>\n <span\n className=\"flex-1\"\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: \"1.5\",\n color: \"var(--canvas-text)\",\n }}\n >\n {name}\n </span>\n <button\n onClick={onMenuClick}\n className=\"size-8 flex items-center justify-center hover:opacity-70 transition-opacity rounded-full\"\n aria-label=\"More options\"\n >\n <MoreVertical\n className=\"size-5\"\n style={{ color: \"var(--canvas-text-placeholder)\" }}\n />\n </button>\n </div>\n );\n}\n\nfunction ChatInputBar({ placeholder = \"Send a message\", onSend }: ChatInputBarProps) {\n const [value, setValue] = useState(\"\");\n\n const handleSend = () => {\n if (value.trim() && onSend) {\n onSend(value.trim());\n setValue(\"\");\n }\n };\n\n const handleKeyDown = (e: React.KeyboardEvent) => {\n if (e.key === \"Enter\" && !e.shiftKey) {\n e.preventDefault();\n handleSend();\n }\n };\n\n return (\n <div\n className=\"flex items-center gap-[var(--spacing-xl)] h-20\"\n style={{\n borderTop: \"1px solid var(--canvas-border)\",\n borderBottom: \"1px solid var(--canvas-border)\",\n }}\n >\n {/* Input field */}\n <div className=\"flex-1 flex items-center gap-[var(--spacing-md)] h-11\">\n <input\n type=\"text\"\n value={value}\n onChange={(e) => setValue(e.target.value)}\n onKeyDown={handleKeyDown}\n placeholder={placeholder}\n className=\"flex-1 bg-transparent outline-none\"\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 <button\n className=\"size-6 flex items-center justify-center hover:opacity-70 transition-opacity\"\n aria-label=\"Attach file\"\n >\n <Paperclip\n className=\"size-6\"\n style={{ color: \"var(--canvas-text-placeholder)\" }}\n />\n </button>\n </div>\n\n {/* Send button */}\n <button\n onClick={handleSend}\n className=\"h-11 px-[var(--spacing-xl)] flex items-center justify-center transition-colors hover:opacity-90\"\n style={{\n backgroundColor: \"var(--btn-primary-bg)\",\n color: \"var(--btn-primary-text)\",\n borderRadius: \"var(--radius-xs)\",\n boxShadow: \"0px 1px 8px 0px rgba(0,0,0,0.03)\",\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 }}\n >\n Send\n </button>\n </div>\n );\n}\n\n// ============================================\n// Main Component\n// ============================================\n\nexport function BottomInputChatWidget({\n variant = \"threads\",\n title = \"Messages\",\n threads = defaultThreads,\n messages = defaultMessages,\n conversationName = \"Sarah Chen\",\n onThreadClick,\n onBackClick,\n onSend,\n className,\n}: BottomInputChatWidgetProps) {\n if (variant === \"threads\") {\n return (\n <div\n className={cn(\n \"flex flex-col gap-[var(--spacing-3xl)] py-[var(--spacing-xl)]\",\n className\n )}\n style={{\n backgroundColor: \"var(--canvas-background)\",\n }}\n >\n {/* Title */}\n <div className=\"flex flex-col gap-[var(--spacing-xl)]\">\n <h2\n style={{\n fontFamily: \"var(--typo-h6-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-h6-size)\",\n fontWeight: \"var(--typo-h6-weight)\",\n lineHeight: \"var(--typo-h6-line-height)\",\n color: \"var(--canvas-text)\",\n margin: 0,\n }}\n >\n {title}\n </h2>\n </div>\n\n {/* Thread list */}\n <div className=\"flex flex-col\">\n {threads.map((thread, index) => (\n <div\n key={thread.id}\n style={{\n borderTop: index === 0 ? \"1px solid var(--canvas-border)\" : \"none\",\n }}\n >\n <MessageThreadItem\n thread={thread}\n onClick={() => onThreadClick?.(thread.id)}\n />\n </div>\n ))}\n </div>\n </div>\n );\n }\n\n // Conversation variant\n return (\n <div\n className={cn(\n \"flex flex-col gap-[var(--spacing-3xl)] py-[var(--spacing-xl)]\",\n className\n )}\n style={{\n backgroundColor: \"var(--canvas-background)\",\n }}\n >\n {/* Messages section */}\n <div className=\"flex flex-col gap-[var(--spacing-xl)]\">\n {/* Header */}\n <ConversationHeader\n name={conversationName}\n onBackClick={onBackClick}\n />\n\n {/* Messages list */}\n <div className=\"flex flex-col\">\n {messages.map((message, index) => (\n <div\n key={message.id}\n style={{\n borderTop: index === 0 ? \"1px solid var(--canvas-border)\" : \"none\",\n }}\n >\n <ConversationMessage message={message} />\n </div>\n ))}\n </div>\n\n {/* Input bar */}\n <ChatInputBar onSend={onSend} />\n </div>\n </div>\n );\n}\n"
10
10
  }
11
11
  ],
12
12
  "dependencies": [
13
13
  "lucide-react"
14
14
  ],
15
15
  "registryDependencies": [
16
- "avatar",
17
- "utils"
16
+ "ui/avatar",
17
+ "lib/utils",
18
+ "blocks/demo-avatars"
18
19
  ]
19
20
  }
@@ -13,6 +13,6 @@
13
13
  "lucide-react"
14
14
  ],
15
15
  "registryDependencies": [
16
- "avatar"
16
+ "ui/avatar"
17
17
  ]
18
18
  }
@@ -11,8 +11,8 @@
11
11
  ],
12
12
  "dependencies": [],
13
13
  "registryDependencies": [
14
- "utils",
15
- "menufocus-template",
16
- "title-group"
14
+ "lib/utils",
15
+ "blocks/menufocus-template",
16
+ "blocks/title-group"
17
17
  ]
18
18
  }
@@ -13,7 +13,7 @@
13
13
  "lucide-react"
14
14
  ],
15
15
  "registryDependencies": [
16
- "utils",
17
- "component-registry"
16
+ "lib/utils",
17
+ "lib/component-registry"
18
18
  ]
19
19
  }
@@ -11,6 +11,6 @@
11
11
  ],
12
12
  "dependencies": [],
13
13
  "registryDependencies": [
14
- "utils"
14
+ "lib/utils"
15
15
  ]
16
16
  }
@@ -11,6 +11,6 @@
11
11
  ],
12
12
  "dependencies": [],
13
13
  "registryDependencies": [
14
- "utils"
14
+ "lib/utils"
15
15
  ]
16
16
  }
@@ -13,7 +13,7 @@
13
13
  "lucide-react"
14
14
  ],
15
15
  "registryDependencies": [
16
- "utils",
17
- "component-search"
16
+ "lib/utils",
17
+ "blocks/component-search"
18
18
  ]
19
19
  }