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,15 +6,16 @@
6
6
  {
7
7
  "path": "components/blocks/slideshow-grid-tiles.tsx",
8
8
  "type": "registry:block",
9
- "content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport { TitleGroup } from \"./title-group\";\nimport { Avatar, AvatarImage, AvatarFallback } from \"../ui/avatar\";\nimport { ChevronLeft, ChevronRight, FolderPlus, ThumbsUp, Eye } from \"lucide-react\";\n\n// ============================================\n// Types\n// ============================================\n\nexport interface SlideshowTileItem {\n id: string;\n /** Array of image URLs for the slideshow */\n images: string[];\n /** User/creator info */\n user: {\n name: string;\n avatarUrl?: string;\n location: string;\n };\n /** Like/upvote count */\n likes: number | string;\n /** View count */\n views: number | string;\n}\n\nexport interface SortOption {\n id: string;\n label: string;\n}\n\nexport interface SlideshowGridTilesProps {\n /** Block title */\n title?: string;\n /** Subtitle text (e.g., \"Showing 20 designs\") */\n subtitle?: string;\n /** Array of tile items */\n items?: SlideshowTileItem[];\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 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 save button is clicked on a tile */\n onSave?: (item: SlideshowTileItem) => void;\n /** Callback when a tile is clicked */\n onItemClick?: (item: SlideshowTileItem) => void;\n /** Additional class names */\n className?: string;\n}\n\n// ============================================\n// Default Data\n// ============================================\n\nconst defaultItems: SlideshowTileItem[] = [\n {\n id: \"1\",\n images: [\n \"https://images.unsplash.com/photo-1618005182384-a83a8bd57fbe?w=800&h=600&fit=crop\",\n \"https://images.unsplash.com/photo-1558591710-4b4a1ae0f04d?w=800&h=600&fit=crop\",\n ],\n user: {\n name: \"Aya Williams\",\n avatarUrl: \"https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=150&h=150&fit=crop&crop=face\",\n location: \"Copenhagen, Denmark\",\n },\n likes: \"25k\",\n views: \"6.5k\",\n },\n {\n id: \"2\",\n images: [\n \"https://images.unsplash.com/photo-1633356122544-f134324a6cee?w=800&h=600&fit=crop\",\n \"https://images.unsplash.com/photo-1611162617474-5b21e879e113?w=800&h=600&fit=crop\",\n ],\n user: {\n name: \"Jeffrey Connor\",\n avatarUrl: \"https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=150&h=150&fit=crop&crop=face\",\n location: \"San Francisco, CA\",\n },\n likes: \"11k\",\n views: \"2.5k\",\n },\n {\n id: \"3\",\n images: [\n \"https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=800&h=600&fit=crop\",\n \"https://images.unsplash.com/photo-1503023345310-bd7c1de61c7d?w=800&h=600&fit=crop\",\n ],\n user: {\n name: \"Gabi Del Rosario\",\n avatarUrl: \"https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=150&h=150&fit=crop&crop=face\",\n location: \"Honolulu, HI\",\n },\n likes: \"8k\",\n views: \"520\",\n },\n {\n id: \"4\",\n images: [\n \"https://images.unsplash.com/photo-1561998338-13ad7883b20f?w=800&h=600&fit=crop\",\n \"https://images.unsplash.com/photo-1579783902614-a3fb3927b6a5?w=800&h=600&fit=crop\",\n ],\n user: {\n name: \"Stacy Jones\",\n avatarUrl: \"https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=150&h=150&fit=crop&crop=face\",\n location: \"New York, NY\",\n },\n likes: \"9.5k\",\n views: \"12k\",\n },\n];\n\nconst defaultSortOptions: SortOption[] = [\n { id: \"popular\", label: \"Most Popular\" },\n { id: \"recent\", label: \"Most Recent\" },\n { id: \"likes\", label: \"Most Liked\" },\n { id: \"views\", label: \"Most Viewed\" },\n];\n\n// ============================================\n// Sub-components\n// ============================================\n\ninterface TileCardProps {\n item: SlideshowTileItem;\n onSave?: (item: SlideshowTileItem) => void;\n onClick?: (item: SlideshowTileItem) => void;\n}\n\nfunction TileCard({ item, onSave, onClick }: TileCardProps) {\n const [currentImageIndex, setCurrentImageIndex] = useState(0);\n\n const handlePrevImage = (e: React.MouseEvent) => {\n e.stopPropagation();\n setCurrentImageIndex((prev) => \n prev === 0 ? item.images.length - 1 : prev - 1\n );\n };\n\n const handleNextImage = (e: React.MouseEvent) => {\n e.stopPropagation();\n setCurrentImageIndex((prev) => \n prev === item.images.length - 1 ? 0 : prev + 1\n );\n };\n\n const handleSave = (e: React.MouseEvent) => {\n e.stopPropagation();\n onSave?.(item);\n };\n\n return (\n <div \n className=\"flex flex-col cursor-pointer\"\n style={{ gap: \"var(--spacing-xl)\" }}\n onClick={() => onClick?.(item)}\n >\n {/* Image Container */}\n <div \n className=\"group relative w-full overflow-hidden\"\n style={{ \n height: \"240px\",\n borderRadius: \"var(--radius-md, 8px)\",\n border: \"1px solid var(--canvas-border)\",\n }}\n >\n {/* Main Image */}\n <img\n src={item.images[currentImageIndex]}\n alt={`${item.user.name}'s portfolio`}\n className=\"absolute inset-0 w-full h-full object-cover transition-opacity duration-300\"\n style={{ borderRadius: \"var(--radius-md, 8px)\" }}\n />\n\n {/* Hover Overlay - Navigation Arrows */}\n <div className=\"absolute inset-0 flex items-center justify-between px-2 opacity-0 group-hover:opacity-100 transition-opacity duration-200\">\n {/* Left Arrow */}\n <button\n onClick={handlePrevImage}\n className=\"cursor-pointer flex items-center justify-center shrink-0 transition-transform hover:scale-105\"\n style={{\n width: \"32px\",\n height: \"32px\",\n backgroundColor: \"var(--canvas-background)\",\n border: \"1px solid var(--canvas-border)\",\n borderRadius: \"var(--radius-full, 24px)\",\n boxShadow: \"0px 1px 2px 0px rgba(0,0,0,0.02)\",\n }}\n >\n <ChevronLeft \n className=\"w-5 h-5\" \n style={{ color: \"var(--canvas-text-muted)\" }}\n />\n </button>\n\n {/* Right Arrow */}\n <button\n onClick={handleNextImage}\n className=\"cursor-pointer flex items-center justify-center shrink-0 transition-transform hover:scale-105\"\n style={{\n width: \"32px\",\n height: \"32px\",\n backgroundColor: \"var(--canvas-background)\",\n border: \"1px solid var(--canvas-border)\",\n borderRadius: \"var(--radius-full, 24px)\",\n boxShadow: \"0px 1px 2px 0px rgba(0,0,0,0.02)\",\n }}\n >\n <ChevronRight \n className=\"w-5 h-5\" \n style={{ color: \"var(--canvas-text-muted)\" }}\n />\n </button>\n </div>\n\n {/* Hover Overlay - Save Badge */}\n <div className=\"absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity duration-200\">\n <button\n onClick={handleSave}\n className=\"cursor-pointer flex items-center transition-transform hover:scale-105\"\n style={{\n height: \"var(--spacing-5xl, 40px)\",\n paddingLeft: \"var(--spacing-xl, 16px)\",\n paddingRight: \"var(--spacing-xl, 16px)\",\n gap: \"var(--spacing-md, 8px)\",\n backgroundColor: \"var(--canvas-background)\",\n border: \"1px solid var(--canvas-border)\",\n borderRadius: \"var(--radius-full, 40px)\",\n }}\n >\n <FolderPlus \n className=\"w-5 h-5\" \n style={{ color: \"var(--canvas-text)\" }}\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)\",\n }}\n >\n Save\n </span>\n </button>\n </div>\n </div>\n\n {/* User Info Row */}\n <div \n className=\"flex items-center w-full\"\n style={{ gap: \"var(--spacing-xl)\" }}\n >\n {/* Avatar */}\n <Avatar \n className=\"shrink-0\"\n style={{ \n width: \"48px\", \n height: \"48px\",\n border: \"1px solid var(--canvas-border)\",\n }}\n >\n <AvatarImage src={item.user.avatarUrl} alt={item.user.name} />\n <AvatarFallback>\n {item.user.name.split(\" \").map(n => n[0]).join(\"\").slice(0, 2)}\n </AvatarFallback>\n </Avatar>\n\n {/* Name and Location */}\n <div className=\"flex flex-col flex-1 min-w-0 justify-center\">\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: 600,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text)\",\n margin: 0,\n }}\n >\n {item.user.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: \"var(--typo-body-s-weight)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text-muted)\",\n margin: 0,\n }}\n >\n {item.user.location}\n </p>\n </div>\n\n {/* Stats */}\n <div \n className=\"flex items-center shrink-0\"\n style={{ gap: \"var(--spacing-md)\" }}\n >\n {/* Likes */}\n <div \n className=\"flex items-center\"\n style={{ gap: \"var(--spacing-sm)\" }}\n >\n <ThumbsUp \n className=\"w-5 h-5\" \n style={{ color: \"var(--canvas-text-muted)\" }}\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-muted)\",\n }}\n >\n {item.likes}\n </span>\n </div>\n\n {/* Views */}\n <div \n className=\"flex items-center\"\n style={{ gap: \"var(--spacing-sm)\" }}\n >\n <Eye \n className=\"w-5 h-5\" \n style={{ color: \"var(--canvas-text-muted)\" }}\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-muted)\",\n }}\n >\n {item.views}\n </span>\n </div>\n </div>\n </div>\n </div>\n );\n}\n\n// ============================================\n// Main Component\n// ============================================\n\n/**\n * Canvas Design System - Slideshow Grid Tiles Block\n * \n * A 2-column grid of portfolio/slideshow tiles with hover states\n * showing navigation arrows and save button. Each tile displays\n * a large image, user info, and engagement stats.\n * \n * @example\n * ```tsx\n * <SlideshowGridTiles\n * title=\"Portfolios\"\n * subtitle=\"Showing 20 designs\"\n * onSave={(item) => console.log(\"Saved\", item)}\n * />\n * ```\n */\nexport function SlideshowGridTiles({\n title = \"Portfolios\",\n subtitle,\n items = defaultItems,\n sortOptions = defaultSortOptions,\n actionButtonText = \"Add new\",\n onAddNew,\n onSort,\n onSave,\n onItemClick,\n className,\n}: SlideshowGridTilesProps) {\n const displaySubtitle = subtitle ?? `Showing ${items.length} designs`;\n\n return (\n <div\n className={cn(\"flex flex-col w-full\", className)}\n style={{ gap: \"var(--spacing-xl)\" }}\n >\n {/* Header Section */}\n <TitleGroup title={title} subtitle={displaySubtitle} sortOptions={sortOptions} onSort={onSort} actionButtonText={actionButtonText} onAction={onAddNew} />\n\n {/* Grid Section */}\n <div \n className=\"grid grid-cols-1 md:grid-cols-2 w-full\"\n style={{ gap: \"var(--spacing-4xl)\" }}\n >\n {items.map((item) => (\n <TileCard\n key={item.id}\n item={item}\n onSave={onSave}\n onClick={onItemClick}\n />\n ))}\n </div>\n </div>\n );\n}\n"
9
+ "content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport { TitleGroup } from \"./title-group\";\nimport { Avatar, AvatarImage, AvatarFallback } from \"../ui/avatar\";\nimport { ChevronLeft, ChevronRight, FolderPlus, ThumbsUp, Eye } from \"lucide-react\";\nimport { AVATAR_SARAH_CHEN, AVATAR_MARCUS_WEBB, AVATAR_MAYA_JOHNSON, AVATAR_NICOLE_PALMER } from \"./demo-avatars\";\n\n// ============================================\n// Types\n// ============================================\n\nexport interface SlideshowTileItem {\n id: string;\n /** Array of image URLs for the slideshow */\n images: string[];\n /** User/creator info */\n user: {\n name: string;\n avatarUrl?: string;\n location: string;\n };\n /** Like/upvote count */\n likes: number | string;\n /** View count */\n views: number | string;\n}\n\nexport interface SortOption {\n id: string;\n label: string;\n}\n\nexport interface SlideshowGridTilesProps {\n /** Block title */\n title?: string;\n /** Subtitle text (e.g., \"Showing 20 designs\") */\n subtitle?: string;\n /** Array of tile items */\n items?: SlideshowTileItem[];\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 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 save button is clicked on a tile */\n onSave?: (item: SlideshowTileItem) => void;\n /** Callback when a tile is clicked */\n onItemClick?: (item: SlideshowTileItem) => void;\n /** Additional class names */\n className?: string;\n}\n\n// ============================================\n// Default Data\n// ============================================\n\nconst defaultItems: SlideshowTileItem[] = [\n {\n id: \"1\",\n images: [\n \"https://images.unsplash.com/photo-1618005182384-a83a8bd57fbe?w=800&h=600&fit=crop\",\n \"https://images.unsplash.com/photo-1558591710-4b4a1ae0f04d?w=800&h=600&fit=crop\",\n ],\n user: {\n name: \"Sarah Chen\",\n avatarUrl: AVATAR_SARAH_CHEN,\n location: \"Copenhagen, Denmark\",\n },\n likes: \"25k\",\n views: \"6.5k\",\n },\n {\n id: \"2\",\n images: [\n \"https://images.unsplash.com/photo-1633356122544-f134324a6cee?w=800&h=600&fit=crop\",\n \"https://images.unsplash.com/photo-1611162617474-5b21e879e113?w=800&h=600&fit=crop\",\n ],\n user: {\n name: \"Marcus Webb\",\n avatarUrl: AVATAR_MARCUS_WEBB,\n location: \"San Francisco, CA\",\n },\n likes: \"11k\",\n views: \"2.5k\",\n },\n {\n id: \"3\",\n images: [\n \"https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=800&h=600&fit=crop\",\n \"https://images.unsplash.com/photo-1503023345310-bd7c1de61c7d?w=800&h=600&fit=crop\",\n ],\n user: {\n name: \"Maya Johnson\",\n avatarUrl: AVATAR_MAYA_JOHNSON,\n location: \"Honolulu, HI\",\n },\n likes: \"8k\",\n views: \"520\",\n },\n {\n id: \"4\",\n images: [\n \"https://images.unsplash.com/photo-1561998338-13ad7883b20f?w=800&h=600&fit=crop\",\n \"https://images.unsplash.com/photo-1579783902614-a3fb3927b6a5?w=800&h=600&fit=crop\",\n ],\n user: {\n name: \"Nicole Palmer\",\n avatarUrl: AVATAR_NICOLE_PALMER,\n location: \"New York, NY\",\n },\n likes: \"9.5k\",\n views: \"12k\",\n },\n];\n\nconst defaultSortOptions: SortOption[] = [\n { id: \"popular\", label: \"Most Popular\" },\n { id: \"recent\", label: \"Most Recent\" },\n { id: \"likes\", label: \"Most Liked\" },\n { id: \"views\", label: \"Most Viewed\" },\n];\n\n// ============================================\n// Sub-components\n// ============================================\n\ninterface TileCardProps {\n item: SlideshowTileItem;\n onSave?: (item: SlideshowTileItem) => void;\n onClick?: (item: SlideshowTileItem) => void;\n}\n\nfunction TileCard({ item, onSave, onClick }: TileCardProps) {\n const [currentImageIndex, setCurrentImageIndex] = useState(0);\n\n const handlePrevImage = (e: React.MouseEvent) => {\n e.stopPropagation();\n setCurrentImageIndex((prev) => \n prev === 0 ? item.images.length - 1 : prev - 1\n );\n };\n\n const handleNextImage = (e: React.MouseEvent) => {\n e.stopPropagation();\n setCurrentImageIndex((prev) => \n prev === item.images.length - 1 ? 0 : prev + 1\n );\n };\n\n const handleSave = (e: React.MouseEvent) => {\n e.stopPropagation();\n onSave?.(item);\n };\n\n return (\n <div \n className=\"flex flex-col cursor-pointer\"\n style={{ gap: \"var(--spacing-xl)\" }}\n onClick={() => onClick?.(item)}\n >\n {/* Image Container */}\n <div \n className=\"group relative w-full overflow-hidden\"\n style={{ \n height: \"240px\",\n borderRadius: \"var(--radius-md, 8px)\",\n border: \"1px solid var(--canvas-border)\",\n }}\n >\n {/* Main Image */}\n <img\n src={item.images[currentImageIndex]}\n alt={`${item.user.name}'s portfolio`}\n className=\"absolute inset-0 w-full h-full object-cover transition-opacity duration-300\"\n style={{ borderRadius: \"var(--radius-md, 8px)\" }}\n />\n\n {/* Hover Overlay - Navigation Arrows */}\n <div className=\"absolute inset-0 flex items-center justify-between px-2 opacity-0 group-hover:opacity-100 transition-opacity duration-200\">\n {/* Left Arrow */}\n <button\n onClick={handlePrevImage}\n className=\"cursor-pointer flex items-center justify-center shrink-0 transition-transform hover:scale-105\"\n style={{\n width: \"32px\",\n height: \"32px\",\n backgroundColor: \"var(--canvas-background)\",\n border: \"1px solid var(--canvas-border)\",\n borderRadius: \"var(--radius-full, 24px)\",\n boxShadow: \"0px 1px 2px 0px rgba(0,0,0,0.02)\",\n }}\n >\n <ChevronLeft \n className=\"w-5 h-5\" \n style={{ color: \"var(--canvas-text-muted)\" }}\n />\n </button>\n\n {/* Right Arrow */}\n <button\n onClick={handleNextImage}\n className=\"cursor-pointer flex items-center justify-center shrink-0 transition-transform hover:scale-105\"\n style={{\n width: \"32px\",\n height: \"32px\",\n backgroundColor: \"var(--canvas-background)\",\n border: \"1px solid var(--canvas-border)\",\n borderRadius: \"var(--radius-full, 24px)\",\n boxShadow: \"0px 1px 2px 0px rgba(0,0,0,0.02)\",\n }}\n >\n <ChevronRight \n className=\"w-5 h-5\" \n style={{ color: \"var(--canvas-text-muted)\" }}\n />\n </button>\n </div>\n\n {/* Hover Overlay - Save Badge */}\n <div className=\"absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity duration-200\">\n <button\n onClick={handleSave}\n className=\"cursor-pointer flex items-center transition-transform hover:scale-105\"\n style={{\n height: \"var(--spacing-5xl, 40px)\",\n paddingLeft: \"var(--spacing-xl, 16px)\",\n paddingRight: \"var(--spacing-xl, 16px)\",\n gap: \"var(--spacing-md, 8px)\",\n backgroundColor: \"var(--canvas-background)\",\n border: \"1px solid var(--canvas-border)\",\n borderRadius: \"var(--radius-full, 40px)\",\n }}\n >\n <FolderPlus \n className=\"w-5 h-5\" \n style={{ color: \"var(--canvas-text)\" }}\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)\",\n }}\n >\n Save\n </span>\n </button>\n </div>\n </div>\n\n {/* User Info Row */}\n <div \n className=\"flex items-center w-full\"\n style={{ gap: \"var(--spacing-xl)\" }}\n >\n {/* Avatar */}\n <Avatar \n className=\"shrink-0\"\n style={{ \n width: \"48px\", \n height: \"48px\",\n border: \"1px solid var(--canvas-border)\",\n }}\n >\n <AvatarImage src={item.user.avatarUrl} alt={item.user.name} />\n <AvatarFallback>\n {item.user.name.split(\" \").map(n => n[0]).join(\"\").slice(0, 2)}\n </AvatarFallback>\n </Avatar>\n\n {/* Name and Location */}\n <div className=\"flex flex-col flex-1 min-w-0 justify-center\">\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: 600,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text)\",\n margin: 0,\n }}\n >\n {item.user.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: \"var(--typo-body-s-weight)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text-muted)\",\n margin: 0,\n }}\n >\n {item.user.location}\n </p>\n </div>\n\n {/* Stats */}\n <div \n className=\"flex items-center shrink-0\"\n style={{ gap: \"var(--spacing-md)\" }}\n >\n {/* Likes */}\n <div \n className=\"flex items-center\"\n style={{ gap: \"var(--spacing-sm)\" }}\n >\n <ThumbsUp \n className=\"w-5 h-5\" \n style={{ color: \"var(--canvas-text-muted)\" }}\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-muted)\",\n }}\n >\n {item.likes}\n </span>\n </div>\n\n {/* Views */}\n <div \n className=\"flex items-center\"\n style={{ gap: \"var(--spacing-sm)\" }}\n >\n <Eye \n className=\"w-5 h-5\" \n style={{ color: \"var(--canvas-text-muted)\" }}\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-muted)\",\n }}\n >\n {item.views}\n </span>\n </div>\n </div>\n </div>\n </div>\n );\n}\n\n// ============================================\n// Main Component\n// ============================================\n\n/**\n * Canvas Design System - Slideshow Grid Tiles Block\n * \n * A 2-column grid of portfolio/slideshow tiles with hover states\n * showing navigation arrows and save button. Each tile displays\n * a large image, user info, and engagement stats.\n * \n * @example\n * ```tsx\n * <SlideshowGridTiles\n * title=\"Portfolios\"\n * subtitle=\"Showing 20 designs\"\n * onSave={(item) => console.log(\"Saved\", item)}\n * />\n * ```\n */\nexport function SlideshowGridTiles({\n title = \"Portfolios\",\n subtitle,\n items = defaultItems,\n sortOptions = defaultSortOptions,\n actionButtonText = \"Add new\",\n onAddNew,\n onSort,\n onSave,\n onItemClick,\n className,\n}: SlideshowGridTilesProps) {\n const displaySubtitle = subtitle ?? `Showing ${items.length} designs`;\n\n return (\n <div\n className={cn(\"flex flex-col w-full\", className)}\n style={{ gap: \"var(--spacing-xl)\" }}\n >\n {/* Header Section */}\n <TitleGroup title={title} subtitle={displaySubtitle} sortOptions={sortOptions} onSort={onSort} actionButtonText={actionButtonText} onAction={onAddNew} />\n\n {/* Grid Section */}\n <div \n className=\"grid grid-cols-1 md:grid-cols-2 w-full\"\n style={{ gap: \"var(--spacing-4xl)\" }}\n >\n {items.map((item) => (\n <TileCard\n key={item.id}\n item={item}\n onSave={onSave}\n onClick={onItemClick}\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
- "title-group",
18
- "avatar"
16
+ "lib/utils",
17
+ "blocks/title-group",
18
+ "ui/avatar",
19
+ "blocks/demo-avatars"
19
20
  ]
20
21
  }
@@ -6,16 +6,17 @@
6
6
  {
7
7
  "path": "components/blocks/social-feed.tsx",
8
8
  "type": "registry:block",
9
- "content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport { Avatar, AvatarImage, AvatarFallback } from \"../ui/avatar\";\nimport { TitleGroup } from \"./title-group\";\nimport { Button } from \"../ui/button\";\nimport { \n Heart, \n MessageCircle, \n RefreshCw, \n Send, \n Paperclip, \n Video, \n Link2, \n MoreHorizontal,\n Play\n} from \"lucide-react\";\n\n// ============================================\n// Types\n// ============================================\n\nexport interface PostAuthor {\n id: string;\n name: string;\n avatarUrl?: string;\n}\n\nexport interface LinkPreview {\n url: string;\n domain: string;\n title: string;\n description?: string;\n imageUrl?: string;\n}\n\nexport interface VideoMedia {\n thumbnailUrl: string;\n videoUrl?: string;\n}\n\nexport interface RepostContent {\n author: PostAuthor;\n date: string;\n content: string;\n images?: string[];\n}\n\nexport interface SocialFeedPost {\n id: string;\n author: PostAuthor;\n date: string;\n content: string;\n /** Image URLs for the post */\n images?: string[];\n /** Video media */\n video?: VideoMedia;\n /** Link preview card */\n linkPreview?: LinkPreview;\n /** Reposted/quoted content */\n repost?: RepostContent;\n likesCount: number;\n repliesCount: number;\n isLiked?: boolean;\n /** Nested replies */\n replies?: SocialFeedPost[];\n /** Whether this is a reply (for indentation) */\n isReply?: boolean;\n}\n\nexport interface SocialFeedProps {\n /** Section title */\n title?: string;\n /** Posts data */\n posts?: SocialFeedPost[];\n /** Current user for composer */\n currentUser?: PostAuthor;\n /** Placeholder text for composer */\n composerPlaceholder?: string;\n /** Image preview in composer */\n composerImagePreview?: string;\n /** Callback when post is submitted */\n onPost?: (content: string) => void;\n /** Callback when like is clicked */\n onLike?: (postId: string) => void;\n /** Callback when comment is clicked */\n onComment?: (postId: string) => void;\n /** Callback when repost is clicked */\n onRepost?: (postId: string) => void;\n /** Callback when share is clicked */\n onShare?: (postId: string) => void;\n /** Callback when menu is clicked */\n onMenuClick?: (postId: string) => void;\n /** Additional class names */\n className?: string;\n}\n\n// ============================================\n// Default Data\n// ============================================\n\nconst defaultCurrentUser: PostAuthor = {\n id: \"current\",\n name: \"You\",\n avatarUrl: \"https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=150&h=150&fit=crop&crop=face\",\n};\n\nconst defaultPosts: SocialFeedPost[] = [\n {\n id: \"1\",\n author: {\n id: \"raj\",\n name: \"Raj Mishra\",\n avatarUrl: \"https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=150&h=150&fit=crop&crop=face\",\n },\n date: \"Feb 23, 1:32 PM\",\n content: \"Thinking about traveling to Paris again!\",\n repost: {\n author: {\n id: \"jeffrey\",\n name: \"Jeffrey Connor\",\n avatarUrl: \"https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=150&h=150&fit=crop&crop=face\",\n },\n date: \"Nov 23, 5:34 PM\",\n content: \"What a place, the history, architecture and culture is wonderful. So many sites to see, one more amazing then the next. A must see if you are going to visit the great cities of the world.\",\n images: [\n \"https://images.unsplash.com/photo-1502602898657-3e91760cbb34?w=320&h=320&fit=crop\",\n \"https://images.unsplash.com/photo-1499856871958-5b9627545d1a?w=320&h=320&fit=crop\",\n ],\n },\n likesCount: 30,\n repliesCount: 10,\n isLiked: false,\n },\n {\n id: \"2\",\n author: {\n id: \"mary\",\n name: \"Mary Trott\",\n avatarUrl: \"https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=150&h=150&fit=crop&crop=face\",\n },\n date: \"Feb 23, 1:32 PM\",\n content: \"Learning how to Bubble\",\n video: {\n thumbnailUrl: \"https://images.unsplash.com/photo-1461749280684-dccba630e2f6?w=480&h=380&fit=crop\",\n },\n likesCount: 30,\n repliesCount: 10,\n isLiked: false,\n replies: [\n {\n id: \"2-reply-1\",\n author: {\n id: \"aya\",\n name: \"Aya Williams\",\n avatarUrl: \"https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=150&h=150&fit=crop&crop=face\",\n },\n date: \"Mar 12, 11:23 AM\",\n content: \"Check out these flight deals to Paris!\",\n linkPreview: {\n url: \"https://expedia.com/flights/paris\",\n domain: \"expedia.com\",\n title: \"Paris flights\",\n description: \"Your one-stop travel site for your dream vacation. Bundle your stay...\",\n imageUrl: \"https://images.unsplash.com/photo-1502602898657-3e91760cbb34?w=200&h=200&fit=crop\",\n },\n likesCount: 30,\n repliesCount: 10,\n isLiked: false,\n isReply: true,\n },\n ],\n },\n];\n\n// ============================================\n// Sub-components\n// ============================================\n\ninterface PostComposerProps {\n placeholder?: string;\n imagePreview?: string;\n onPost?: (content: string) => void;\n}\n\nfunction PostComposer({ placeholder = \"What's on your mind?\", imagePreview, onPost }: PostComposerProps) {\n const [content, setContent] = useState(\"\");\n\n const handlePost = () => {\n if (content.trim() || imagePreview) {\n onPost?.(content);\n setContent(\"\");\n }\n };\n\n return (\n <div\n className=\"flex flex-col w-full overflow-hidden\"\n style={{\n border: \"1px solid var(--canvas-border)\",\n boxShadow: \"0px 1px 8px 0px rgba(0,0,0,0.03)\",\n }}\n >\n {/* Text input area */}\n <div\n className=\"w-full\"\n style={{\n padding: \"var(--spacing-xl)\",\n background: \"var(--canvas-background)\",\n boxShadow: \"0px 1px 2px 0px rgba(0,0,0,0.02)\",\n }}\n >\n <textarea\n value={content}\n onChange={(e) => setContent(e.target.value)}\n placeholder={placeholder}\n className=\"w-full resize-none border-0 bg-transparent outline-none\"\n rows={2}\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: content ? \"var(--canvas-text)\" : \"var(--canvas-text-placeholder)\",\n }}\n />\n </div>\n\n {/* Image preview */}\n {imagePreview && (\n <div\n className=\"w-full\"\n style={{\n padding: \"0 var(--spacing-xl)\",\n background: \"var(--canvas-background)\",\n }}\n >\n <div\n className=\"overflow-hidden\"\n style={{\n width: 240,\n height: 180,\n borderRadius: \"var(--radius-md)\",\n border: \"1px solid var(--canvas-border)\",\n }}\n >\n <img\n src={imagePreview}\n alt=\"Preview\"\n className=\"w-full h-full object-cover\"\n />\n </div>\n </div>\n )}\n\n {/* Action bar */}\n <div\n className=\"flex items-center justify-between w-full\"\n style={{\n padding: \"var(--spacing-xl)\",\n background: \"var(--canvas-background)\",\n borderTop: \"1px solid var(--canvas-border)\",\n }}\n >\n <div className=\"flex items-center\" style={{ gap: \"var(--spacing-lg)\" }}>\n <button type=\"button\" className=\"cursor-pointer\" style={{ color: \"var(--canvas-text)\" }}>\n <Paperclip className=\"w-5 h-5\" />\n </button>\n <button type=\"button\" className=\"cursor-pointer\" style={{ color: \"var(--canvas-text)\" }}>\n <Video className=\"w-5 h-5\" />\n </button>\n <button type=\"button\" className=\"cursor-pointer\" style={{ color: \"var(--canvas-text)\" }}>\n <Link2 className=\"w-5 h-5\" />\n </button>\n </div>\n <Button variant=\"primary\" size=\"sm\" onClick={handlePost}>\n Post\n </Button>\n </div>\n </div>\n );\n}\n\ninterface ActionIconsRowProps {\n isLiked?: boolean;\n onLike?: () => void;\n onComment?: () => void;\n onRepost?: () => void;\n onShare?: () => void;\n}\n\nfunction ActionIconsRow({ isLiked, onLike, onComment, onRepost, onShare }: ActionIconsRowProps) {\n return (\n <div\n className=\"flex items-center\"\n style={{ gap: \"var(--spacing-lg)\", padding: \"var(--spacing-xxs) 0\" }}\n >\n <button\n type=\"button\"\n onClick={onLike}\n className=\"cursor-pointer\"\n style={{ color: isLiked ? \"var(--canvas-destructive)\" : \"var(--canvas-text)\" }}\n >\n <Heart\n className=\"w-5 h-5\"\n style={{\n fill: isLiked ? \"var(--canvas-destructive)\" : \"transparent\",\n }}\n />\n </button>\n <button type=\"button\" onClick={onComment} className=\"cursor-pointer\" style={{ color: \"var(--canvas-text)\" }}>\n <MessageCircle className=\"w-5 h-5\" />\n </button>\n <button type=\"button\" onClick={onRepost} className=\"cursor-pointer\" style={{ color: \"var(--canvas-text)\" }}>\n <RefreshCw className=\"w-5 h-5\" />\n </button>\n <button type=\"button\" onClick={onShare} className=\"cursor-pointer\" style={{ color: \"var(--canvas-text)\" }}>\n <Send className=\"w-5 h-5\" />\n </button>\n </div>\n );\n}\n\ninterface StatsRowProps {\n likesCount: number;\n repliesCount: number;\n}\n\nfunction StatsRow({ likesCount, repliesCount }: StatsRowProps) {\n return (\n <div\n className=\"flex items-center\"\n style={{ gap: \"var(--spacing-xl)\", paddingTop: \"var(--spacing-xs)\" }}\n >\n <span\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 500,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text-placeholder)\",\n }}\n >\n {likesCount} likes\n </span>\n <span\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 500,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text-placeholder)\",\n }}\n >\n {repliesCount} replies\n </span>\n </div>\n );\n}\n\ninterface VideoThumbnailProps {\n thumbnailUrl: string;\n onClick?: () => void;\n}\n\nfunction VideoThumbnail({ thumbnailUrl, onClick }: VideoThumbnailProps) {\n return (\n <div\n className=\"relative overflow-hidden cursor-pointer\"\n style={{\n width: 480,\n height: 380,\n maxWidth: \"100%\",\n borderRadius: \"var(--radius-3xl)\",\n border: \"1px solid var(--canvas-border)\",\n }}\n onClick={onClick}\n >\n <img\n src={thumbnailUrl}\n alt=\"Video thumbnail\"\n className=\"w-full h-full object-cover\"\n />\n {/* Play button */}\n <div\n className=\"absolute inset-0 flex items-center justify-center\"\n >\n <div\n className=\"flex items-center justify-center\"\n style={{\n width: 128,\n height: 80,\n background: \"var(--canvas-destructive)\",\n borderRadius: \"var(--radius-2xl)\",\n }}\n >\n <Play className=\"w-12 h-12 text-white fill-white\" />\n </div>\n </div>\n </div>\n );\n}\n\ninterface LinkPreviewCardProps {\n linkPreview: LinkPreview;\n onClick?: () => void;\n}\n\nfunction LinkPreviewCard({ linkPreview, onClick }: LinkPreviewCardProps) {\n return (\n <div\n className=\"flex overflow-hidden cursor-pointer\"\n style={{\n width: 580,\n maxWidth: \"100%\",\n background: \"var(--canvas-background)\",\n border: \"1px solid var(--canvas-border)\",\n borderRadius: \"var(--radius-2xl)\",\n boxShadow: \"0px 1px 8px 0px rgba(0,0,0,0.03)\",\n }}\n onClick={onClick}\n >\n {linkPreview.imageUrl && (\n <div\n className=\"shrink-0 self-stretch overflow-hidden\"\n style={{\n width: 200,\n borderRight: \"1px solid var(--canvas-border)\",\n borderTopLeftRadius: \"var(--radius-md)\",\n borderBottomLeftRadius: \"var(--radius-md)\",\n }}\n >\n <img\n src={linkPreview.imageUrl}\n alt={linkPreview.title}\n className=\"w-full h-full object-cover\"\n />\n </div>\n )}\n <div\n className=\"flex flex-col flex-1\"\n style={{ padding: \"var(--spacing-4xl)\", gap: \"var(--spacing-lg)\" }}\n >\n <div className=\"flex flex-col\" style={{ gap: 0 }}>\n <span\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text-placeholder)\",\n }}\n >\n {linkPreview.domain}\n </span>\n <span\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 600,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n {linkPreview.title}\n </span>\n </div>\n {linkPreview.description && (\n <p\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text)\",\n margin: 0,\n }}\n >\n {linkPreview.description}\n </p>\n )}\n </div>\n </div>\n );\n}\n\ninterface RepostCardProps {\n repost: RepostContent;\n onLike?: () => void;\n onComment?: () => void;\n onRepost?: () => void;\n onShare?: () => void;\n}\n\nfunction RepostCard({ repost, onLike, onComment, onRepost, onShare }: RepostCardProps) {\n return (\n <div\n className=\"flex flex-col w-full\"\n style={{\n padding: \"var(--spacing-4xl)\",\n background: \"var(--canvas-background)\",\n border: \"1px solid var(--canvas-border)\",\n borderRadius: \"var(--radius-md)\",\n boxShadow: \"0px 1px 8px 0px rgba(0,0,0,0.03)\",\n gap: \"var(--spacing-xl)\",\n }}\n >\n {/* Author row */}\n <div className=\"flex items-start w-full\" style={{ gap: \"var(--spacing-xl)\" }}>\n <Avatar\n className=\"shrink-0\"\n style={{\n width: 48,\n height: 48,\n borderRadius: \"var(--spacing-3xl)\",\n border: \"1px solid var(--canvas-border)\",\n }}\n >\n <AvatarImage src={repost.author.avatarUrl} />\n <AvatarFallback>\n {repost.author.name.split(\" \").map(n => n[0]).join(\"\").slice(0, 2)}\n </AvatarFallback>\n </Avatar>\n <div className=\"flex flex-col flex-1\" style={{ gap: \"var(--spacing-lg)\" }}>\n <div className=\"flex items-center justify-between w-full\">\n <div className=\"flex flex-col\" style={{ gap: 0 }}>\n <span\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 600,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n {repost.author.name}\n </span>\n <span\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text-placeholder)\",\n }}\n >\n {repost.date}\n </span>\n </div>\n <button\n type=\"button\"\n className=\"cursor-pointer flex items-center justify-center\"\n style={{\n width: 32,\n height: 32,\n borderRadius: \"var(--spacing-6xl)\",\n color: \"var(--canvas-text-placeholder)\",\n }}\n >\n <MoreHorizontal className=\"w-5 h-5\" />\n </button>\n </div>\n {/* Content */}\n <p\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text)\",\n margin: 0,\n }}\n >\n {repost.content}\n </p>\n {/* Images */}\n {repost.images && repost.images.length > 0 && (\n <div className=\"flex\" style={{ gap: \"var(--spacing-xl)\" }}>\n {repost.images.map((img, idx) => (\n <div\n key={idx}\n className=\"overflow-hidden\"\n style={{\n width: 320,\n height: 320,\n borderRadius: \"var(--radius-2xl)\",\n border: \"1px solid var(--canvas-border)\",\n }}\n >\n <img src={img} alt=\"\" className=\"w-full h-full object-cover\" />\n </div>\n ))}\n </div>\n )}\n {/* Actions */}\n <ActionIconsRow\n isLiked={false}\n onLike={onLike}\n onComment={onComment}\n onRepost={onRepost}\n onShare={onShare}\n />\n <StatsRow likesCount={30} repliesCount={10} />\n </div>\n </div>\n </div>\n );\n}\n\ninterface PostCellProps {\n post: SocialFeedPost;\n onLike?: () => void;\n onComment?: () => void;\n onRepost?: () => void;\n onShare?: () => void;\n onMenuClick?: () => void;\n}\n\nfunction PostCell({ post, onLike, onComment, onRepost, onShare, onMenuClick }: PostCellProps) {\n return (\n <div\n className=\"flex w-full\"\n style={{\n paddingLeft: post.isReply ? \"var(--spacing-7xl)\" : 0,\n paddingTop: post.isReply ? \"var(--spacing-3xl)\" : \"var(--spacing-xl)\",\n paddingBottom: post.isReply ? 0 : \"var(--spacing-3xl)\",\n borderBottom: post.isReply ? \"none\" : \"1px solid var(--canvas-border)\",\n gap: \"var(--spacing-xl)\",\n }}\n >\n {/* Avatar column with reply line */}\n <div className=\"flex flex-col items-center shrink-0\" style={{ gap: \"var(--spacing-md)\" }}>\n <Avatar\n style={{\n width: 48,\n height: 48,\n borderRadius: \"var(--spacing-3xl)\",\n border: \"1px solid var(--canvas-border)\",\n }}\n >\n <AvatarImage src={post.author.avatarUrl} />\n <AvatarFallback>\n {post.author.name.split(\" \").map(n => n[0]).join(\"\").slice(0, 2)}\n </AvatarFallback>\n </Avatar>\n {/* Reply line */}\n {post.replies && post.replies.length > 0 && (\n <div\n className=\"flex-1 w-px\"\n style={{ background: \"var(--canvas-border)\", minHeight: 20 }}\n />\n )}\n </div>\n\n {/* Content column */}\n <div className=\"flex flex-col flex-1 min-w-0\" style={{ gap: \"var(--spacing-lg)\" }}>\n {/* Header */}\n <div className=\"flex items-center justify-between w-full\">\n <div className=\"flex flex-col\" style={{ gap: 0 }}>\n <span\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 600,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n {post.author.name}\n </span>\n <span\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text-placeholder)\",\n }}\n >\n {post.date}\n </span>\n </div>\n <button\n type=\"button\"\n onClick={onMenuClick}\n className=\"cursor-pointer flex items-center justify-center\"\n style={{\n width: 32,\n height: 32,\n borderRadius: \"var(--spacing-6xl)\",\n color: \"var(--canvas-text-placeholder)\",\n }}\n >\n <MoreHorizontal className=\"w-5 h-5\" />\n </button>\n </div>\n\n {/* Content text */}\n <p\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text)\",\n margin: 0,\n }}\n >\n {post.content}\n </p>\n\n {/* Repost card */}\n {post.repost && (\n <RepostCard\n repost={post.repost}\n onLike={onLike}\n onComment={onComment}\n onRepost={onRepost}\n onShare={onShare}\n />\n )}\n\n {/* Images */}\n {post.images && post.images.length > 0 && (\n <div className=\"flex\" style={{ gap: \"var(--spacing-xl)\" }}>\n {post.images.map((img, idx) => (\n <div\n key={idx}\n className=\"overflow-hidden\"\n style={{\n width: 320,\n height: 320,\n borderRadius: \"var(--radius-2xl)\",\n border: \"1px solid var(--canvas-border)\",\n }}\n >\n <img src={img} alt=\"\" className=\"w-full h-full object-cover\" />\n </div>\n ))}\n </div>\n )}\n\n {/* Video */}\n {post.video && (\n <VideoThumbnail thumbnailUrl={post.video.thumbnailUrl} />\n )}\n\n {/* Link preview */}\n {post.linkPreview && (\n <LinkPreviewCard linkPreview={post.linkPreview} />\n )}\n\n {/* Actions */}\n <ActionIconsRow\n isLiked={post.isLiked}\n onLike={onLike}\n onComment={onComment}\n onRepost={onRepost}\n onShare={onShare}\n />\n\n {/* Stats */}\n <StatsRow likesCount={post.likesCount} repliesCount={post.repliesCount} />\n\n {/* Nested replies */}\n {post.replies && post.replies.length > 0 && (\n <div className=\"flex flex-col w-full\">\n {post.replies.map((reply) => (\n <PostCell\n key={reply.id}\n post={reply}\n onLike={onLike}\n onComment={onComment}\n onRepost={onRepost}\n onShare={onShare}\n />\n ))}\n </div>\n )}\n </div>\n </div>\n );\n}\n\n// ============================================\n// Main Component\n// ============================================\n\n/**\n * Canvas Design System - Social Feed Block\n *\n * A social media-style feed component with post composer, posts with various\n * content types (text, images, video, reposts, link cards), social interactions,\n * and threaded replies.\n *\n * @example\n * ```tsx\n * <SocialFeed\n * title=\"Social Feed\"\n * posts={[...]}\n * onLike={(postId) => console.log(\"Liked\", postId)}\n * onPost={(content) => console.log(\"Posted\", content)}\n * />\n * ```\n */\nexport function SocialFeed({\n title = \"Social Feed\",\n posts = defaultPosts,\n currentUser = defaultCurrentUser,\n composerPlaceholder = \"What's on your mind?\",\n composerImagePreview,\n onPost,\n onLike,\n onComment,\n onRepost,\n onShare,\n onMenuClick,\n className,\n}: SocialFeedProps) {\n return (\n <div\n className={cn(\"flex flex-col w-full\", className)}\n style={{ gap: \"var(--spacing-xl)\" }}\n >\n {/* Header Section */}\n {title && <TitleGroup title={title} />}\n\n {/* Feed content */}\n <div className=\"flex flex-col w-full overflow-hidden\">\n {/* First section: Composer + first set of posts */}\n <div\n className=\"flex flex-col w-full\"\n style={{\n borderBottom: \"1px solid var(--canvas-border)\",\n paddingBottom: \"var(--spacing-5xl)\",\n }}\n >\n {/* Post Composer */}\n <PostComposer\n placeholder={composerPlaceholder}\n imagePreview={composerImagePreview}\n onPost={onPost}\n />\n\n {/* Posts */}\n <div className=\"flex flex-col w-full\" style={{ paddingTop: \"var(--spacing-xl)\" }}>\n {posts.map((post) => (\n <PostCell\n key={post.id}\n post={post}\n onLike={() => onLike?.(post.id)}\n onComment={() => onComment?.(post.id)}\n onRepost={() => onRepost?.(post.id)}\n onShare={() => onShare?.(post.id)}\n onMenuClick={() => onMenuClick?.(post.id)}\n />\n ))}\n </div>\n </div>\n </div>\n </div>\n );\n}\n"
9
+ "content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport { Avatar, AvatarImage, AvatarFallback } from \"../ui/avatar\";\nimport { TitleGroup } from \"./title-group\";\nimport { Button } from \"../ui/button\";\nimport {\n Heart,\n MessageCircle,\n RefreshCw,\n Send,\n Paperclip,\n Video,\n Link2,\n MoreHorizontal,\n Play\n} from \"lucide-react\";\nimport { AVATAR_ETHAN_BROOKS, AVATAR_MARCUS_WEBB, AVATAR_NICOLE_PALMER, AVATAR_SARAH_CHEN } 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 LinkPreview {\n url: string;\n domain: string;\n title: string;\n description?: string;\n imageUrl?: string;\n}\n\nexport interface VideoMedia {\n thumbnailUrl: string;\n videoUrl?: string;\n}\n\nexport interface RepostContent {\n author: PostAuthor;\n date: string;\n content: string;\n images?: string[];\n}\n\nexport interface SocialFeedPost {\n id: string;\n author: PostAuthor;\n date: string;\n content: string;\n /** Image URLs for the post */\n images?: string[];\n /** Video media */\n video?: VideoMedia;\n /** Link preview card */\n linkPreview?: LinkPreview;\n /** Reposted/quoted content */\n repost?: RepostContent;\n likesCount: number;\n repliesCount: number;\n isLiked?: boolean;\n /** Nested replies */\n replies?: SocialFeedPost[];\n /** Whether this is a reply (for indentation) */\n isReply?: boolean;\n}\n\nexport interface SocialFeedProps {\n /** Section title */\n title?: string;\n /** Posts data */\n posts?: SocialFeedPost[];\n /** Current user for composer */\n currentUser?: PostAuthor;\n /** Placeholder text for composer */\n composerPlaceholder?: string;\n /** Image preview in composer */\n composerImagePreview?: string;\n /** Callback when post is submitted */\n onPost?: (content: string) => void;\n /** Callback when like is clicked */\n onLike?: (postId: string) => void;\n /** Callback when comment is clicked */\n onComment?: (postId: string) => void;\n /** Callback when repost is clicked */\n onRepost?: (postId: string) => void;\n /** Callback when share is clicked */\n onShare?: (postId: string) => void;\n /** Callback when menu is clicked */\n onMenuClick?: (postId: string) => void;\n /** Additional class names */\n className?: string;\n}\n\n// ============================================\n// Default Data\n// ============================================\n\nconst defaultCurrentUser: PostAuthor = {\n id: \"current\",\n name: \"You\",\n avatarUrl: AVATAR_NICOLE_PALMER,\n};\n\nconst defaultPosts: SocialFeedPost[] = [\n {\n id: \"1\",\n author: {\n id: \"ethan\",\n name: \"Ethan Brooks\",\n avatarUrl: AVATAR_ETHAN_BROOKS,\n },\n date: \"Feb 23, 1:32 PM\",\n content: \"Thinking about traveling to Paris again!\",\n repost: {\n author: {\n id: \"marcus\",\n name: \"Marcus Webb\",\n avatarUrl: AVATAR_MARCUS_WEBB,\n },\n date: \"Nov 23, 5:34 PM\",\n content: \"What a place, the history, architecture and culture is wonderful. So many sites to see, one more amazing then the next. A must see if you are going to visit the great cities of the world.\",\n images: [\n \"https://images.unsplash.com/photo-1502602898657-3e91760cbb34?w=320&h=320&fit=crop\",\n \"https://images.unsplash.com/photo-1499856871958-5b9627545d1a?w=320&h=320&fit=crop\",\n ],\n },\n likesCount: 30,\n repliesCount: 10,\n isLiked: false,\n },\n {\n id: \"2\",\n author: {\n id: \"nicole\",\n name: \"Nicole Palmer\",\n avatarUrl: AVATAR_NICOLE_PALMER,\n },\n date: \"Feb 23, 1:32 PM\",\n content: \"Learning how to Bubble\",\n video: {\n thumbnailUrl: \"https://images.unsplash.com/photo-1461749280684-dccba630e2f6?w=480&h=380&fit=crop\",\n },\n likesCount: 30,\n repliesCount: 10,\n isLiked: false,\n replies: [\n {\n id: \"2-reply-1\",\n author: {\n id: \"sarah\",\n name: \"Sarah Chen\",\n avatarUrl: AVATAR_SARAH_CHEN,\n },\n date: \"Mar 12, 11:23 AM\",\n content: \"Check out these flight deals to Paris!\",\n linkPreview: {\n url: \"https://expedia.com/flights/paris\",\n domain: \"expedia.com\",\n title: \"Paris flights\",\n description: \"Your one-stop travel site for your dream vacation. Bundle your stay...\",\n imageUrl: \"https://images.unsplash.com/photo-1502602898657-3e91760cbb34?w=200&h=200&fit=crop\",\n },\n likesCount: 30,\n repliesCount: 10,\n isLiked: false,\n isReply: true,\n },\n ],\n },\n];\n\n// ============================================\n// Sub-components\n// ============================================\n\ninterface PostComposerProps {\n placeholder?: string;\n imagePreview?: string;\n onPost?: (content: string) => void;\n}\n\nfunction PostComposer({ placeholder = \"What's on your mind?\", imagePreview, onPost }: PostComposerProps) {\n const [content, setContent] = useState(\"\");\n\n const handlePost = () => {\n if (content.trim() || imagePreview) {\n onPost?.(content);\n setContent(\"\");\n }\n };\n\n return (\n <div\n className=\"flex flex-col w-full overflow-hidden\"\n style={{\n border: \"1px solid var(--canvas-border)\",\n boxShadow: \"0px 1px 8px 0px rgba(0,0,0,0.03)\",\n }}\n >\n {/* Text input area */}\n <div\n className=\"w-full\"\n style={{\n padding: \"var(--spacing-xl)\",\n background: \"var(--canvas-background)\",\n boxShadow: \"0px 1px 2px 0px rgba(0,0,0,0.02)\",\n }}\n >\n <textarea\n value={content}\n onChange={(e) => setContent(e.target.value)}\n placeholder={placeholder}\n className=\"w-full resize-none border-0 bg-transparent outline-none\"\n rows={2}\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: content ? \"var(--canvas-text)\" : \"var(--canvas-text-placeholder)\",\n }}\n />\n </div>\n\n {/* Image preview */}\n {imagePreview && (\n <div\n className=\"w-full\"\n style={{\n padding: \"0 var(--spacing-xl)\",\n background: \"var(--canvas-background)\",\n }}\n >\n <div\n className=\"overflow-hidden\"\n style={{\n width: 240,\n height: 180,\n borderRadius: \"var(--radius-md)\",\n border: \"1px solid var(--canvas-border)\",\n }}\n >\n <img\n src={imagePreview}\n alt=\"Preview\"\n className=\"w-full h-full object-cover\"\n />\n </div>\n </div>\n )}\n\n {/* Action bar */}\n <div\n className=\"flex items-center justify-between w-full\"\n style={{\n padding: \"var(--spacing-xl)\",\n background: \"var(--canvas-background)\",\n borderTop: \"1px solid var(--canvas-border)\",\n }}\n >\n <div className=\"flex items-center\" style={{ gap: \"var(--spacing-lg)\" }}>\n <button type=\"button\" className=\"cursor-pointer\" style={{ color: \"var(--canvas-text)\" }}>\n <Paperclip className=\"w-5 h-5\" />\n </button>\n <button type=\"button\" className=\"cursor-pointer\" style={{ color: \"var(--canvas-text)\" }}>\n <Video className=\"w-5 h-5\" />\n </button>\n <button type=\"button\" className=\"cursor-pointer\" style={{ color: \"var(--canvas-text)\" }}>\n <Link2 className=\"w-5 h-5\" />\n </button>\n </div>\n <Button variant=\"primary\" size=\"sm\" onClick={handlePost}>\n Post\n </Button>\n </div>\n </div>\n );\n}\n\ninterface ActionIconsRowProps {\n isLiked?: boolean;\n onLike?: () => void;\n onComment?: () => void;\n onRepost?: () => void;\n onShare?: () => void;\n}\n\nfunction ActionIconsRow({ isLiked, onLike, onComment, onRepost, onShare }: ActionIconsRowProps) {\n return (\n <div\n className=\"flex items-center\"\n style={{ gap: \"var(--spacing-lg)\", padding: \"var(--spacing-xxs) 0\" }}\n >\n <button\n type=\"button\"\n onClick={onLike}\n className=\"cursor-pointer\"\n style={{ color: isLiked ? \"var(--canvas-destructive)\" : \"var(--canvas-text)\" }}\n >\n <Heart\n className=\"w-5 h-5\"\n style={{\n fill: isLiked ? \"var(--canvas-destructive)\" : \"transparent\",\n }}\n />\n </button>\n <button type=\"button\" onClick={onComment} className=\"cursor-pointer\" style={{ color: \"var(--canvas-text)\" }}>\n <MessageCircle className=\"w-5 h-5\" />\n </button>\n <button type=\"button\" onClick={onRepost} className=\"cursor-pointer\" style={{ color: \"var(--canvas-text)\" }}>\n <RefreshCw className=\"w-5 h-5\" />\n </button>\n <button type=\"button\" onClick={onShare} className=\"cursor-pointer\" style={{ color: \"var(--canvas-text)\" }}>\n <Send className=\"w-5 h-5\" />\n </button>\n </div>\n );\n}\n\ninterface StatsRowProps {\n likesCount: number;\n repliesCount: number;\n}\n\nfunction StatsRow({ likesCount, repliesCount }: StatsRowProps) {\n return (\n <div\n className=\"flex items-center\"\n style={{ gap: \"var(--spacing-xl)\", paddingTop: \"var(--spacing-xs)\" }}\n >\n <span\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 500,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text-placeholder)\",\n }}\n >\n {likesCount} likes\n </span>\n <span\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 500,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text-placeholder)\",\n }}\n >\n {repliesCount} replies\n </span>\n </div>\n );\n}\n\ninterface VideoThumbnailProps {\n thumbnailUrl: string;\n onClick?: () => void;\n}\n\nfunction VideoThumbnail({ thumbnailUrl, onClick }: VideoThumbnailProps) {\n return (\n <div\n className=\"relative overflow-hidden cursor-pointer\"\n style={{\n width: 480,\n height: 380,\n maxWidth: \"100%\",\n borderRadius: \"var(--radius-3xl)\",\n border: \"1px solid var(--canvas-border)\",\n }}\n onClick={onClick}\n >\n <img\n src={thumbnailUrl}\n alt=\"Video thumbnail\"\n className=\"w-full h-full object-cover\"\n />\n {/* Play button */}\n <div\n className=\"absolute inset-0 flex items-center justify-center\"\n >\n <div\n className=\"flex items-center justify-center\"\n style={{\n width: 128,\n height: 80,\n background: \"var(--canvas-destructive)\",\n borderRadius: \"var(--radius-2xl)\",\n }}\n >\n <Play className=\"w-12 h-12 text-white fill-white\" />\n </div>\n </div>\n </div>\n );\n}\n\ninterface LinkPreviewCardProps {\n linkPreview: LinkPreview;\n onClick?: () => void;\n}\n\nfunction LinkPreviewCard({ linkPreview, onClick }: LinkPreviewCardProps) {\n return (\n <div\n className=\"flex overflow-hidden cursor-pointer\"\n style={{\n width: 580,\n maxWidth: \"100%\",\n background: \"var(--canvas-background)\",\n border: \"1px solid var(--canvas-border)\",\n borderRadius: \"var(--radius-2xl)\",\n boxShadow: \"0px 1px 8px 0px rgba(0,0,0,0.03)\",\n }}\n onClick={onClick}\n >\n {linkPreview.imageUrl && (\n <div\n className=\"shrink-0 self-stretch overflow-hidden\"\n style={{\n width: 200,\n borderRight: \"1px solid var(--canvas-border)\",\n borderTopLeftRadius: \"var(--radius-md)\",\n borderBottomLeftRadius: \"var(--radius-md)\",\n }}\n >\n <img\n src={linkPreview.imageUrl}\n alt={linkPreview.title}\n className=\"w-full h-full object-cover\"\n />\n </div>\n )}\n <div\n className=\"flex flex-col flex-1\"\n style={{ padding: \"var(--spacing-4xl)\", gap: \"var(--spacing-lg)\" }}\n >\n <div className=\"flex flex-col\" style={{ gap: 0 }}>\n <span\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text-placeholder)\",\n }}\n >\n {linkPreview.domain}\n </span>\n <span\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 600,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n {linkPreview.title}\n </span>\n </div>\n {linkPreview.description && (\n <p\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text)\",\n margin: 0,\n }}\n >\n {linkPreview.description}\n </p>\n )}\n </div>\n </div>\n );\n}\n\ninterface RepostCardProps {\n repost: RepostContent;\n onLike?: () => void;\n onComment?: () => void;\n onRepost?: () => void;\n onShare?: () => void;\n}\n\nfunction RepostCard({ repost, onLike, onComment, onRepost, onShare }: RepostCardProps) {\n return (\n <div\n className=\"flex flex-col w-full\"\n style={{\n padding: \"var(--spacing-4xl)\",\n background: \"var(--canvas-background)\",\n border: \"1px solid var(--canvas-border)\",\n borderRadius: \"var(--radius-md)\",\n boxShadow: \"0px 1px 8px 0px rgba(0,0,0,0.03)\",\n gap: \"var(--spacing-xl)\",\n }}\n >\n {/* Author row */}\n <div className=\"flex items-start w-full\" style={{ gap: \"var(--spacing-xl)\" }}>\n <Avatar\n className=\"shrink-0\"\n style={{\n width: 48,\n height: 48,\n borderRadius: \"var(--spacing-3xl)\",\n border: \"1px solid var(--canvas-border)\",\n }}\n >\n <AvatarImage src={repost.author.avatarUrl} />\n <AvatarFallback>\n {repost.author.name.split(\" \").map(n => n[0]).join(\"\").slice(0, 2)}\n </AvatarFallback>\n </Avatar>\n <div className=\"flex flex-col flex-1\" style={{ gap: \"var(--spacing-lg)\" }}>\n <div className=\"flex items-center justify-between w-full\">\n <div className=\"flex flex-col\" style={{ gap: 0 }}>\n <span\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 600,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n {repost.author.name}\n </span>\n <span\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text-placeholder)\",\n }}\n >\n {repost.date}\n </span>\n </div>\n <button\n type=\"button\"\n className=\"cursor-pointer flex items-center justify-center\"\n style={{\n width: 32,\n height: 32,\n borderRadius: \"var(--spacing-6xl)\",\n color: \"var(--canvas-text-placeholder)\",\n }}\n >\n <MoreHorizontal className=\"w-5 h-5\" />\n </button>\n </div>\n {/* Content */}\n <p\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text)\",\n margin: 0,\n }}\n >\n {repost.content}\n </p>\n {/* Images */}\n {repost.images && repost.images.length > 0 && (\n <div className=\"flex\" style={{ gap: \"var(--spacing-xl)\" }}>\n {repost.images.map((img, idx) => (\n <div\n key={idx}\n className=\"overflow-hidden\"\n style={{\n width: 320,\n height: 320,\n borderRadius: \"var(--radius-2xl)\",\n border: \"1px solid var(--canvas-border)\",\n }}\n >\n <img src={img} alt=\"\" className=\"w-full h-full object-cover\" />\n </div>\n ))}\n </div>\n )}\n {/* Actions */}\n <ActionIconsRow\n isLiked={false}\n onLike={onLike}\n onComment={onComment}\n onRepost={onRepost}\n onShare={onShare}\n />\n <StatsRow likesCount={30} repliesCount={10} />\n </div>\n </div>\n </div>\n );\n}\n\ninterface PostCellProps {\n post: SocialFeedPost;\n onLike?: () => void;\n onComment?: () => void;\n onRepost?: () => void;\n onShare?: () => void;\n onMenuClick?: () => void;\n}\n\nfunction PostCell({ post, onLike, onComment, onRepost, onShare, onMenuClick }: PostCellProps) {\n return (\n <div\n className=\"flex w-full\"\n style={{\n paddingLeft: post.isReply ? \"var(--spacing-7xl)\" : 0,\n paddingTop: post.isReply ? \"var(--spacing-3xl)\" : \"var(--spacing-xl)\",\n paddingBottom: post.isReply ? 0 : \"var(--spacing-3xl)\",\n borderBottom: post.isReply ? \"none\" : \"1px solid var(--canvas-border)\",\n gap: \"var(--spacing-xl)\",\n }}\n >\n {/* Avatar column with reply line */}\n <div className=\"flex flex-col items-center shrink-0\" style={{ gap: \"var(--spacing-md)\" }}>\n <Avatar\n style={{\n width: 48,\n height: 48,\n borderRadius: \"var(--spacing-3xl)\",\n border: \"1px solid var(--canvas-border)\",\n }}\n >\n <AvatarImage src={post.author.avatarUrl} />\n <AvatarFallback>\n {post.author.name.split(\" \").map(n => n[0]).join(\"\").slice(0, 2)}\n </AvatarFallback>\n </Avatar>\n {/* Reply line */}\n {post.replies && post.replies.length > 0 && (\n <div\n className=\"flex-1 w-px\"\n style={{ background: \"var(--canvas-border)\", minHeight: 20 }}\n />\n )}\n </div>\n\n {/* Content column */}\n <div className=\"flex flex-col flex-1 min-w-0\" style={{ gap: \"var(--spacing-lg)\" }}>\n {/* Header */}\n <div className=\"flex items-center justify-between w-full\">\n <div className=\"flex flex-col\" style={{ gap: 0 }}>\n <span\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 600,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n {post.author.name}\n </span>\n <span\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text-placeholder)\",\n }}\n >\n {post.date}\n </span>\n </div>\n <button\n type=\"button\"\n onClick={onMenuClick}\n className=\"cursor-pointer flex items-center justify-center\"\n style={{\n width: 32,\n height: 32,\n borderRadius: \"var(--spacing-6xl)\",\n color: \"var(--canvas-text-placeholder)\",\n }}\n >\n <MoreHorizontal className=\"w-5 h-5\" />\n </button>\n </div>\n\n {/* Content text */}\n <p\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text)\",\n margin: 0,\n }}\n >\n {post.content}\n </p>\n\n {/* Repost card */}\n {post.repost && (\n <RepostCard\n repost={post.repost}\n onLike={onLike}\n onComment={onComment}\n onRepost={onRepost}\n onShare={onShare}\n />\n )}\n\n {/* Images */}\n {post.images && post.images.length > 0 && (\n <div className=\"flex\" style={{ gap: \"var(--spacing-xl)\" }}>\n {post.images.map((img, idx) => (\n <div\n key={idx}\n className=\"overflow-hidden\"\n style={{\n width: 320,\n height: 320,\n borderRadius: \"var(--radius-2xl)\",\n border: \"1px solid var(--canvas-border)\",\n }}\n >\n <img src={img} alt=\"\" className=\"w-full h-full object-cover\" />\n </div>\n ))}\n </div>\n )}\n\n {/* Video */}\n {post.video && (\n <VideoThumbnail thumbnailUrl={post.video.thumbnailUrl} />\n )}\n\n {/* Link preview */}\n {post.linkPreview && (\n <LinkPreviewCard linkPreview={post.linkPreview} />\n )}\n\n {/* Actions */}\n <ActionIconsRow\n isLiked={post.isLiked}\n onLike={onLike}\n onComment={onComment}\n onRepost={onRepost}\n onShare={onShare}\n />\n\n {/* Stats */}\n <StatsRow likesCount={post.likesCount} repliesCount={post.repliesCount} />\n\n {/* Nested replies */}\n {post.replies && post.replies.length > 0 && (\n <div className=\"flex flex-col w-full\">\n {post.replies.map((reply) => (\n <PostCell\n key={reply.id}\n post={reply}\n onLike={onLike}\n onComment={onComment}\n onRepost={onRepost}\n onShare={onShare}\n />\n ))}\n </div>\n )}\n </div>\n </div>\n );\n}\n\n// ============================================\n// Main Component\n// ============================================\n\n/**\n * Canvas Design System - Social Feed Block\n *\n * A social media-style feed component with post composer, posts with various\n * content types (text, images, video, reposts, link cards), social interactions,\n * and threaded replies.\n *\n * @example\n * ```tsx\n * <SocialFeed\n * title=\"Social Feed\"\n * posts={[...]}\n * onLike={(postId) => console.log(\"Liked\", postId)}\n * onPost={(content) => console.log(\"Posted\", content)}\n * />\n * ```\n */\nexport function SocialFeed({\n title = \"Social Feed\",\n posts = defaultPosts,\n currentUser = defaultCurrentUser,\n composerPlaceholder = \"What's on your mind?\",\n composerImagePreview,\n onPost,\n onLike,\n onComment,\n onRepost,\n onShare,\n onMenuClick,\n className,\n}: SocialFeedProps) {\n return (\n <div\n className={cn(\"flex flex-col w-full\", className)}\n style={{ gap: \"var(--spacing-xl)\" }}\n >\n {/* Header Section */}\n {title && <TitleGroup title={title} />}\n\n {/* Feed content */}\n <div className=\"flex flex-col w-full overflow-hidden\">\n {/* First section: Composer + first set of posts */}\n <div\n className=\"flex flex-col w-full\"\n style={{\n borderBottom: \"1px solid var(--canvas-border)\",\n paddingBottom: \"var(--spacing-5xl)\",\n }}\n >\n {/* Post Composer */}\n <PostComposer\n placeholder={composerPlaceholder}\n imagePreview={composerImagePreview}\n onPost={onPost}\n />\n\n {/* Posts */}\n <div className=\"flex flex-col w-full\" style={{ paddingTop: \"var(--spacing-xl)\" }}>\n {posts.map((post) => (\n <PostCell\n key={post.id}\n post={post}\n onLike={() => onLike?.(post.id)}\n onComment={() => onComment?.(post.id)}\n onRepost={() => onRepost?.(post.id)}\n onShare={() => onShare?.(post.id)}\n onMenuClick={() => onMenuClick?.(post.id)}\n />\n ))}\n </div>\n </div>\n </div>\n </div>\n );\n}\n"
10
10
  }
11
11
  ],
12
12
  "dependencies": [
13
13
  "lucide-react"
14
14
  ],
15
15
  "registryDependencies": [
16
- "utils",
17
- "avatar",
18
- "title-group",
19
- "button"
16
+ "lib/utils",
17
+ "ui/avatar",
18
+ "blocks/title-group",
19
+ "ui/button",
20
+ "blocks/demo-avatars"
20
21
  ]
21
22
  }
@@ -6,14 +6,15 @@
6
6
  {
7
7
  "path": "components/blocks/standard-data-table.tsx",
8
8
  "type": "registry:block",
9
- "content": "\"use client\";\n\nimport { cn } from \"../../lib/utils\";\nimport { Avatar, AvatarImage, AvatarFallback } from \"../ui/avatar\";\nimport { MenufocusTemplate } from \"./menufocus-template\";\nimport { TitleGroup } from \"./title-group\";\n\n// ============================================\n// Types\n// ============================================\n\nexport interface TableColumn {\n id: string;\n label: string;\n /** Width class or style for the column */\n width?: string;\n}\n\nexport interface TableRow {\n id: string;\n name: string;\n avatarUrl?: string;\n email: string;\n title: string;\n role: string;\n}\n\nexport interface SortOption {\n id: string;\n label: string;\n}\n\nexport interface StandardDataTableProps {\n /** Table title */\n title?: string;\n /** Number of results to display */\n resultCount?: number;\n /** Custom result count text (overrides default \"{count} results\") */\n resultCountText?: string;\n /** Column definitions */\n columns?: TableColumn[];\n /** Table data rows */\n data?: TableRow[];\n /** Sort options for the sort dropdown */\n sortOptions?: SortOption[];\n /** Filter options for the filter dropdown */\n filterOptions?: { 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 row action is clicked */\n onRowAction?: (action: string, row: TableRow) => void;\n /** Additional class names */\n className?: string;\n}\n\n// ============================================\n// Default Data\n// ============================================\n\nconst defaultColumns: TableColumn[] = [\n { id: \"name\", label: \"Name\" },\n { id: \"email\", label: \"Email\" },\n { id: \"title\", label: \"Title\" },\n { id: \"role\", label: \"Role\" },\n];\n\nconst defaultData: TableRow[] = [\n {\n id: \"1\",\n name: \"Jeff Connor\",\n avatarUrl: \"https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=150&h=150&fit=crop&crop=face\",\n email: \"jconnor@testemail.com\",\n title: \"Co-founder & CEO\",\n role: \"Standard\",\n },\n {\n id: \"2\",\n name: \"Aya Williams\",\n avatarUrl: \"https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=150&h=150&fit=crop&crop=face\",\n email: \"ayawilliams@testemail.com\",\n title: \"Chief Marketing Officer\",\n role: \"Standard\",\n },\n];\n\nconst defaultSortOptions: SortOption[] = [\n { id: \"name-asc\", label: \"Name (A-Z)\" },\n { id: \"name-desc\", label: \"Name (Z-A)\" },\n { id: \"date-newest\", label: \"Newest first\" },\n { id: \"date-oldest\", label: \"Oldest first\" },\n];\n\n// ============================================\n// Sub-components\n// ============================================\n\ninterface TableHeaderCellProps {\n children: React.ReactNode;\n className?: string;\n}\n\nfunction TableHeaderCell({ children, className }: TableHeaderCellProps) {\n return (\n <div\n className={cn(\n \"flex items-center h-8 overflow-hidden\",\n className\n )}\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 600,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n {children}\n </div>\n );\n}\n\ninterface TableCellProps {\n children: React.ReactNode;\n className?: string;\n}\n\nfunction TableCell({ children, className }: TableCellProps) {\n return (\n <div\n className={cn(\n \"flex items-center gap-2 h-8 overflow-hidden\",\n className\n )}\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: \"var(--typo-body-s-weight)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n {children}\n </div>\n );\n}\n\n\n// ============================================\n// Main Component\n// ============================================\n\n/**\n * Canvas Design System - Standard Data Table Block\n * \n * A configurable data table with header section including title,\n * sort/filter dropdowns, and action button. Displays tabular data\n * with avatar support in the name column.\n * \n * @example\n * ```tsx\n * <StandardDataTable\n * title=\"Teammates\"\n * data={[\n * { id: \"1\", name: \"John\", email: \"john@example.com\", title: \"Engineer\", role: \"Admin\" }\n * ]}\n * onAddNew={() => console.log(\"Add new\")}\n * />\n * ```\n */\nexport function StandardDataTable({\n title = \"Teammates\",\n resultCount,\n resultCountText,\n columns = defaultColumns,\n data = defaultData,\n sortOptions = defaultSortOptions,\n actionButtonText = \"Add new\",\n onAddNew,\n onSort,\n onRowAction,\n className,\n}: StandardDataTableProps) {\n const displayResultCount = resultCount ?? data.length;\n const displayResultText = resultCountText ?? `${displayResultCount} results`;\n\n return (\n <div\n className={cn(\"flex flex-col w-full\", className)}\n style={{ gap: \"var(--spacing-xl)\" }}\n >\n {/* Header Section */}\n <TitleGroup title={title} subtitle={displayResultText} sortOptions={sortOptions} onSort={onSort} actionButtonText={actionButtonText} onAction={onAddNew} />\n\n {/* Table Section */}\n <div className=\"w-full overflow-x-auto\">\n <table className=\"w-full min-w-max border-collapse\">\n <thead>\n <tr style={{ borderBottom: \"1px solid var(--canvas-border)\" }}>\n <th className=\"text-left pr-8 min-w-[150px]\">\n <TableHeaderCell>\n {columns.find(c => c.id === \"name\")?.label || \"Name\"}\n </TableHeaderCell>\n </th>\n <th className=\"text-left px-8 min-w-[150px]\">\n <TableHeaderCell>\n {columns.find(c => c.id === \"email\")?.label || \"Email\"}\n </TableHeaderCell>\n </th>\n <th className=\"text-left px-8 min-w-[120px]\">\n <TableHeaderCell>\n {columns.find(c => c.id === \"title\")?.label || \"Title\"}\n </TableHeaderCell>\n </th>\n <th className=\"text-left px-8 min-w-[80px]\">\n <TableHeaderCell>\n {columns.find(c => c.id === \"role\")?.label || \"Role\"}\n </TableHeaderCell>\n </th>\n <th className=\"w-8 pr-1\">\n <TableHeaderCell>&nbsp;</TableHeaderCell>\n </th>\n </tr>\n </thead>\n <tbody>\n {data.map((row) => (\n <tr\n key={row.id}\n className=\"hover:bg-[var(--canvas-surface)] transition-colors\"\n style={{ borderBottom: \"1px solid var(--canvas-border)\" }}\n >\n <td\n className=\"pr-8\"\n style={{\n paddingTop: \"var(--spacing-md)\",\n paddingBottom: \"var(--spacing-md)\",\n }}\n >\n <TableCell>\n <Avatar className=\"size-8 border border-[var(--canvas-border)]\">\n <AvatarImage src={row.avatarUrl} alt={row.name} />\n <AvatarFallback>\n {row.name.split(\" \").map(n => n[0]).join(\"\").slice(0, 2)}\n </AvatarFallback>\n </Avatar>\n <span className=\"whitespace-nowrap\">{row.name}</span>\n </TableCell>\n </td>\n <td\n className=\"px-8\"\n style={{\n paddingTop: \"var(--spacing-md)\",\n paddingBottom: \"var(--spacing-md)\",\n }}\n >\n <TableCell>\n <span className=\"whitespace-nowrap\">{row.email}</span>\n </TableCell>\n </td>\n <td\n className=\"px-8\"\n style={{\n paddingTop: \"var(--spacing-md)\",\n paddingBottom: \"var(--spacing-md)\",\n }}\n >\n <TableCell>\n <span className=\"whitespace-nowrap\">{row.title}</span>\n </TableCell>\n </td>\n <td\n className=\"px-8\"\n style={{\n paddingTop: \"var(--spacing-md)\",\n paddingBottom: \"var(--spacing-md)\",\n }}\n >\n <TableCell>\n <span className=\"whitespace-nowrap\">{row.role}</span>\n </TableCell>\n </td>\n <td\n className=\"pr-1\"\n style={{\n paddingTop: \"var(--spacing-md)\",\n paddingBottom: \"var(--spacing-md)\",\n }}\n >\n <MenufocusTemplate\n ariaLabel=\"Row actions\"\n items={[\n { id: \"edit\", label: \"Edit\", onClick: () => onRowAction?.(\"edit\", row) },\n { id: \"view\", label: \"View details\", onClick: () => onRowAction?.(\"view\", row) },\n { id: \"delete\", label: \"Delete\", variant: \"destructive\", onClick: () => onRowAction?.(\"delete\", row) },\n ]}\n />\n </td>\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n </div>\n );\n}\n\n"
9
+ "content": "\"use client\";\n\nimport { cn } from \"../../lib/utils\";\nimport { Avatar, AvatarImage, AvatarFallback } from \"../ui/avatar\";\nimport { MenufocusTemplate } from \"./menufocus-template\";\nimport { TitleGroup } from \"./title-group\";\nimport { AVATAR_MARCUS_WEBB, AVATAR_SARAH_CHEN } from \"./demo-avatars\";\n\n// ============================================\n// Types\n// ============================================\n\nexport interface TableColumn {\n id: string;\n label: string;\n /** Width class or style for the column */\n width?: string;\n}\n\nexport interface TableRow {\n id: string;\n name: string;\n avatarUrl?: string;\n email: string;\n title: string;\n role: string;\n}\n\nexport interface SortOption {\n id: string;\n label: string;\n}\n\nexport interface StandardDataTableProps {\n /** Table title */\n title?: string;\n /** Number of results to display */\n resultCount?: number;\n /** Custom result count text (overrides default \"{count} results\") */\n resultCountText?: string;\n /** Column definitions */\n columns?: TableColumn[];\n /** Table data rows */\n data?: TableRow[];\n /** Sort options for the sort dropdown */\n sortOptions?: SortOption[];\n /** Filter options for the filter dropdown */\n filterOptions?: { 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 row action is clicked */\n onRowAction?: (action: string, row: TableRow) => void;\n /** Additional class names */\n className?: string;\n}\n\n// ============================================\n// Default Data\n// ============================================\n\nconst defaultColumns: TableColumn[] = [\n { id: \"name\", label: \"Name\" },\n { id: \"email\", label: \"Email\" },\n { id: \"title\", label: \"Title\" },\n { id: \"role\", label: \"Role\" },\n];\n\nconst defaultData: TableRow[] = [\n {\n id: \"1\",\n name: \"Marcus Webb\",\n avatarUrl: AVATAR_MARCUS_WEBB,\n email: \"mwebb@testemail.com\",\n title: \"Co-founder & CEO\",\n role: \"Standard\",\n },\n {\n id: \"2\",\n name: \"Sarah Chen\",\n avatarUrl: AVATAR_SARAH_CHEN,\n email: \"schen@testemail.com\",\n title: \"Chief Marketing Officer\",\n role: \"Standard\",\n },\n];\n\nconst defaultSortOptions: SortOption[] = [\n { id: \"name-asc\", label: \"Name (A-Z)\" },\n { id: \"name-desc\", label: \"Name (Z-A)\" },\n { id: \"date-newest\", label: \"Newest first\" },\n { id: \"date-oldest\", label: \"Oldest first\" },\n];\n\n// ============================================\n// Sub-components\n// ============================================\n\ninterface TableHeaderCellProps {\n children: React.ReactNode;\n className?: string;\n}\n\nfunction TableHeaderCell({ children, className }: TableHeaderCellProps) {\n return (\n <div\n className={cn(\n \"flex items-center h-8 overflow-hidden\",\n className\n )}\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 600,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n {children}\n </div>\n );\n}\n\ninterface TableCellProps {\n children: React.ReactNode;\n className?: string;\n}\n\nfunction TableCell({ children, className }: TableCellProps) {\n return (\n <div\n className={cn(\n \"flex items-center gap-2 h-8 overflow-hidden\",\n className\n )}\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: \"var(--typo-body-s-weight)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n {children}\n </div>\n );\n}\n\n\n// ============================================\n// Main Component\n// ============================================\n\n/**\n * Canvas Design System - Standard Data Table Block\n * \n * A configurable data table with header section including title,\n * sort/filter dropdowns, and action button. Displays tabular data\n * with avatar support in the name column.\n * \n * @example\n * ```tsx\n * <StandardDataTable\n * title=\"Teammates\"\n * data={[\n * { id: \"1\", name: \"John\", email: \"john@example.com\", title: \"Engineer\", role: \"Admin\" }\n * ]}\n * onAddNew={() => console.log(\"Add new\")}\n * />\n * ```\n */\nexport function StandardDataTable({\n title = \"Teammates\",\n resultCount,\n resultCountText,\n columns = defaultColumns,\n data = defaultData,\n sortOptions = defaultSortOptions,\n actionButtonText = \"Add new\",\n onAddNew,\n onSort,\n onRowAction,\n className,\n}: StandardDataTableProps) {\n const displayResultCount = resultCount ?? data.length;\n const displayResultText = resultCountText ?? `${displayResultCount} results`;\n\n return (\n <div\n className={cn(\"flex flex-col w-full\", className)}\n style={{ gap: \"var(--spacing-xl)\" }}\n >\n {/* Header Section */}\n <TitleGroup title={title} subtitle={displayResultText} sortOptions={sortOptions} onSort={onSort} actionButtonText={actionButtonText} onAction={onAddNew} />\n\n {/* Table Section */}\n <div className=\"w-full overflow-x-auto\">\n <table className=\"w-full min-w-max border-collapse\">\n <thead>\n <tr style={{ borderBottom: \"1px solid var(--canvas-border)\" }}>\n <th className=\"text-left pr-8 min-w-[150px]\">\n <TableHeaderCell>\n {columns.find(c => c.id === \"name\")?.label || \"Name\"}\n </TableHeaderCell>\n </th>\n <th className=\"text-left px-8 min-w-[150px]\">\n <TableHeaderCell>\n {columns.find(c => c.id === \"email\")?.label || \"Email\"}\n </TableHeaderCell>\n </th>\n <th className=\"text-left px-8 min-w-[120px]\">\n <TableHeaderCell>\n {columns.find(c => c.id === \"title\")?.label || \"Title\"}\n </TableHeaderCell>\n </th>\n <th className=\"text-left px-8 min-w-[80px]\">\n <TableHeaderCell>\n {columns.find(c => c.id === \"role\")?.label || \"Role\"}\n </TableHeaderCell>\n </th>\n <th className=\"w-8 pr-1\">\n <TableHeaderCell>&nbsp;</TableHeaderCell>\n </th>\n </tr>\n </thead>\n <tbody>\n {data.map((row) => (\n <tr\n key={row.id}\n className=\"hover:bg-[var(--canvas-surface)] transition-colors\"\n style={{ borderBottom: \"1px solid var(--canvas-border)\" }}\n >\n <td\n className=\"pr-8\"\n style={{\n paddingTop: \"var(--spacing-md)\",\n paddingBottom: \"var(--spacing-md)\",\n }}\n >\n <TableCell>\n <Avatar className=\"size-8 border border-[var(--canvas-border)]\">\n <AvatarImage src={row.avatarUrl} alt={row.name} />\n <AvatarFallback>\n {row.name.split(\" \").map(n => n[0]).join(\"\").slice(0, 2)}\n </AvatarFallback>\n </Avatar>\n <span className=\"whitespace-nowrap\">{row.name}</span>\n </TableCell>\n </td>\n <td\n className=\"px-8\"\n style={{\n paddingTop: \"var(--spacing-md)\",\n paddingBottom: \"var(--spacing-md)\",\n }}\n >\n <TableCell>\n <span className=\"whitespace-nowrap\">{row.email}</span>\n </TableCell>\n </td>\n <td\n className=\"px-8\"\n style={{\n paddingTop: \"var(--spacing-md)\",\n paddingBottom: \"var(--spacing-md)\",\n }}\n >\n <TableCell>\n <span className=\"whitespace-nowrap\">{row.title}</span>\n </TableCell>\n </td>\n <td\n className=\"px-8\"\n style={{\n paddingTop: \"var(--spacing-md)\",\n paddingBottom: \"var(--spacing-md)\",\n }}\n >\n <TableCell>\n <span className=\"whitespace-nowrap\">{row.role}</span>\n </TableCell>\n </td>\n <td\n className=\"pr-1\"\n style={{\n paddingTop: \"var(--spacing-md)\",\n paddingBottom: \"var(--spacing-md)\",\n }}\n >\n <MenufocusTemplate\n ariaLabel=\"Row actions\"\n items={[\n { id: \"edit\", label: \"Edit\", onClick: () => onRowAction?.(\"edit\", row) },\n { id: \"view\", label: \"View details\", onClick: () => onRowAction?.(\"view\", row) },\n { id: \"delete\", label: \"Delete\", variant: \"destructive\", onClick: () => onRowAction?.(\"delete\", row) },\n ]}\n />\n </td>\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n </div>\n );\n}\n\n"
10
10
  }
11
11
  ],
12
12
  "dependencies": [],
13
13
  "registryDependencies": [
14
- "utils",
15
- "avatar",
16
- "menufocus-template",
17
- "title-group"
14
+ "lib/utils",
15
+ "ui/avatar",
16
+ "blocks/menufocus-template",
17
+ "blocks/title-group",
18
+ "blocks/demo-avatars"
18
19
  ]
19
20
  }
@@ -6,12 +6,12 @@
6
6
  {
7
7
  "path": "components/blocks/standard-list-with-image.tsx",
8
8
  "type": "registry:block",
9
- "content": "\"use client\";\n\nimport { cn } from \"../../lib/utils\";\nimport { TitleGroup } from \"./title-group\";\n\n// ============================================\n// Types\n// ============================================\n\nexport interface Tag {\n id: string;\n label: string;\n}\n\nexport interface ListItem {\n id: string;\n title: string;\n author: string;\n date: string;\n description: string;\n imageUrl: string;\n tags?: Tag[];\n}\n\nexport interface SortOption {\n id: string;\n label: string;\n}\n\nexport interface StandardListWithImageProps {\n /** Block title */\n title?: string;\n /** Block subtitle/description */\n subtitle?: string;\n /** List items to display */\n items?: ListItem[];\n /** Sort options for the sort dropdown */\n sortOptions?: SortOption[];\n /** Filter options for the filter dropdown */\n filterOptions?: { id: string; label: string }[];\n /** Callback when sort value changes */\n onSort?: (value: string) => void;\n /** Callback when filter value changes */\n onFilter?: (value: string) => void;\n /** Callback when an item is clicked */\n onItemClick?: (item: ListItem) => void;\n /** Additional class names */\n className?: string;\n}\n\n// ============================================\n// Default Data\n// ============================================\n\nconst defaultItems: ListItem[] = [\n {\n id: \"1\",\n title: \"Will AI make software developers obsolete?\",\n author: \"Jeffrey Connor\",\n date: \"Sep 21\",\n description:\n \"Will AI replace software developers? Find out as we explore the potential impact of artificial intelligence on the future of software development.\",\n imageUrl:\n \"https://images.unsplash.com/photo-1677442136019-21780ecad995?w=400&h=400&fit=crop\",\n tags: [\n { id: \"ai\", label: \"AI\" },\n { id: \"software\", label: \"Software\" },\n { id: \"technology\", label: \"Technology\" },\n ],\n },\n {\n id: \"2\",\n title: \"Building software that users will love: 5 principles to follow\",\n author: \"Mary Trott\",\n date: \"Aug 2\",\n description:\n \"The most successful businesses follow a few simple rules when building software products that excite their users. See them here.\",\n imageUrl:\n \"https://images.unsplash.com/photo-1498050108023-c5249f4df085?w=400&h=400&fit=crop\",\n tags: [\n { id: \"software\", label: \"Software\" },\n { id: \"best-practices\", label: \"Best practices\" },\n ],\n },\n];\n\nconst defaultSortOptions: SortOption[] = [\n { id: \"date-newest\", label: \"Newest first\" },\n { id: \"date-oldest\", label: \"Oldest first\" },\n { id: \"title-asc\", label: \"Title (A-Z)\" },\n { id: \"title-desc\", label: \"Title (Z-A)\" },\n];\n\n// ============================================\n// Sub-components\n// ============================================\n\ninterface TagPillProps {\n label: string;\n}\n\nfunction TagPill({ label }: TagPillProps) {\n return (\n <span\n className=\"inline-flex items-center overflow-hidden\"\n style={{\n height: \"32px\",\n paddingLeft: \"var(--spacing-lg)\",\n paddingRight: \"var(--spacing-lg)\",\n paddingTop: \"var(--spacing-xs)\",\n paddingBottom: \"var(--spacing-xs)\",\n backgroundColor: \"var(--canvas-surface-brand)\",\n borderRadius: \"var(--radius-xs)\",\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: \"var(--typo-body-s-weight)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-primary)\",\n }}\n >\n {label}\n </span>\n );\n}\n\ninterface ListItemCardProps {\n item: ListItem;\n onClick?: () => void;\n}\n\nfunction ListItemCard({ item, onClick }: ListItemCardProps) {\n return (\n <div\n className={cn(\n \"flex w-full cursor-pointer\",\n onClick && \"hover:bg-[var(--canvas-surface)]\"\n )}\n style={{\n gap: \"var(--spacing-3xl)\",\n paddingTop: \"var(--spacing-3xl)\",\n paddingBottom: \"var(--spacing-3xl)\",\n borderBottom: \"1px solid var(--canvas-border)\",\n }}\n onClick={onClick}\n >\n {/* Image */}\n <div\n className=\"shrink-0 overflow-hidden\"\n style={{\n width: \"200px\",\n height: \"200px\",\n borderRadius: \"var(--radius-md)\",\n border: \"1px solid var(--canvas-border)\",\n }}\n >\n <img\n src={item.imageUrl}\n alt={item.title}\n className=\"w-full h-full object-cover\"\n />\n </div>\n\n {/* Content */}\n <div\n className=\"flex flex-col flex-1 min-w-0\"\n style={{ gap: \"var(--spacing-3xl)\" }}\n >\n {/* Title and Meta */}\n <div\n className=\"flex flex-col\"\n style={{ gap: \"var(--spacing-xs)\" }}\n >\n {/* Title */}\n <h3\n className=\"m-0\"\n style={{\n fontFamily: \"var(--typo-body-xl-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-xl-size)\",\n fontWeight: 600,\n lineHeight: \"var(--typo-body-xl-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n {item.title}\n </h3>\n\n {/* Author and Date */}\n <div\n className=\"flex items-center justify-between w-full\"\n style={{ gap: \"var(--spacing-xl)\" }}\n >\n <p\n className=\"m-0\"\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: \"var(--typo-body-s-weight)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n <span style={{ color: \"var(--canvas-text-muted)\" }}>by</span>{\" \"}\n <span style={{ fontWeight: 500 }}>{item.author}</span>\n </p>\n <p\n className=\"m-0 whitespace-nowrap\"\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: \"var(--typo-body-s-weight)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text-muted)\",\n }}\n >\n {item.date}\n </p>\n </div>\n </div>\n\n {/* Description */}\n <p\n className=\"m-0\"\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: \"var(--typo-body-s-weight)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text-muted)\",\n }}\n >\n {item.description}\n </p>\n\n {/* Tags */}\n {item.tags && item.tags.length > 0 && (\n <div\n className=\"flex flex-wrap\"\n style={{ gap: \"var(--spacing-md)\" }}\n >\n {item.tags.map((tag) => (\n <TagPill key={tag.id} label={tag.label} />\n ))}\n </div>\n )}\n </div>\n </div>\n );\n}\n\n// ============================================\n// Main Component\n// ============================================\n\n/**\n * Canvas Design System - Standard List with Image Block\n *\n * A configurable list block with image thumbnails, commonly used for\n * blog posts, articles, or any content with featured images.\n * Includes header section with title, subtitle, and sort/filter controls.\n *\n * @example\n * ```tsx\n * <StandardListWithImage\n * title=\"Blog\"\n * subtitle=\"Read our latest articles\"\n * items={blogPosts}\n * onItemClick={(item) => console.log(\"Clicked:\", item.title)}\n * />\n * ```\n */\nexport function StandardListWithImage({\n title = \"Blog\",\n subtitle = \"Read our latest articles about tech\",\n items = defaultItems,\n sortOptions = defaultSortOptions,\n onSort,\n onItemClick,\n className,\n}: StandardListWithImageProps) {\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} />\n\n {/* List Section */}\n <div\n className=\"flex flex-col w-full\"\n style={{ borderTop: \"1px solid var(--canvas-border)\" }}\n >\n {items.map((item) => (\n <ListItemCard\n key={item.id}\n item={item}\n onClick={onItemClick ? () => onItemClick(item) : undefined}\n />\n ))}\n </div>\n </div>\n );\n}\n"
9
+ "content": "\"use client\";\n\nimport { cn } from \"../../lib/utils\";\nimport { TitleGroup } from \"./title-group\";\n\n// ============================================\n// Types\n// ============================================\n\nexport interface Tag {\n id: string;\n label: string;\n}\n\nexport interface ListItem {\n id: string;\n title: string;\n author: string;\n date: string;\n description: string;\n imageUrl: string;\n tags?: Tag[];\n}\n\nexport interface SortOption {\n id: string;\n label: string;\n}\n\nexport interface StandardListWithImageProps {\n /** Block title */\n title?: string;\n /** Block subtitle/description */\n subtitle?: string;\n /** List items to display */\n items?: ListItem[];\n /** Sort options for the sort dropdown */\n sortOptions?: SortOption[];\n /** Filter options for the filter dropdown */\n filterOptions?: { id: string; label: string }[];\n /** Callback when sort value changes */\n onSort?: (value: string) => void;\n /** Callback when filter value changes */\n onFilter?: (value: string) => void;\n /** Callback when an item is clicked */\n onItemClick?: (item: ListItem) => void;\n /** Additional class names */\n className?: string;\n}\n\n// ============================================\n// Default Data\n// ============================================\n\nconst defaultItems: ListItem[] = [\n {\n id: \"1\",\n title: \"Will AI make software developers obsolete?\",\n author: \"Marcus Webb\",\n date: \"Sep 21\",\n description:\n \"Will AI replace software developers? Find out as we explore the potential impact of artificial intelligence on the future of software development.\",\n imageUrl:\n \"https://images.unsplash.com/photo-1677442136019-21780ecad995?w=400&h=400&fit=crop\",\n tags: [\n { id: \"ai\", label: \"AI\" },\n { id: \"software\", label: \"Software\" },\n { id: \"technology\", label: \"Technology\" },\n ],\n },\n {\n id: \"2\",\n title: \"Building software that users will love: 5 principles to follow\",\n author: \"Sarah Chen\",\n date: \"Aug 2\",\n description:\n \"The most successful businesses follow a few simple rules when building software products that excite their users. See them here.\",\n imageUrl:\n \"https://images.unsplash.com/photo-1498050108023-c5249f4df085?w=400&h=400&fit=crop\",\n tags: [\n { id: \"software\", label: \"Software\" },\n { id: \"best-practices\", label: \"Best practices\" },\n ],\n },\n];\n\nconst defaultSortOptions: SortOption[] = [\n { id: \"date-newest\", label: \"Newest first\" },\n { id: \"date-oldest\", label: \"Oldest first\" },\n { id: \"title-asc\", label: \"Title (A-Z)\" },\n { id: \"title-desc\", label: \"Title (Z-A)\" },\n];\n\n// ============================================\n// Sub-components\n// ============================================\n\ninterface TagPillProps {\n label: string;\n}\n\nfunction TagPill({ label }: TagPillProps) {\n return (\n <span\n className=\"inline-flex items-center overflow-hidden\"\n style={{\n height: \"32px\",\n paddingLeft: \"var(--spacing-lg)\",\n paddingRight: \"var(--spacing-lg)\",\n paddingTop: \"var(--spacing-xs)\",\n paddingBottom: \"var(--spacing-xs)\",\n backgroundColor: \"var(--canvas-surface-brand)\",\n borderRadius: \"var(--radius-xs)\",\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: \"var(--typo-body-s-weight)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-primary)\",\n }}\n >\n {label}\n </span>\n );\n}\n\ninterface ListItemCardProps {\n item: ListItem;\n onClick?: () => void;\n}\n\nfunction ListItemCard({ item, onClick }: ListItemCardProps) {\n return (\n <div\n className={cn(\n \"flex w-full cursor-pointer\",\n onClick && \"hover:bg-[var(--canvas-surface)]\"\n )}\n style={{\n gap: \"var(--spacing-3xl)\",\n paddingTop: \"var(--spacing-3xl)\",\n paddingBottom: \"var(--spacing-3xl)\",\n borderBottom: \"1px solid var(--canvas-border)\",\n }}\n onClick={onClick}\n >\n {/* Image */}\n <div\n className=\"shrink-0 overflow-hidden\"\n style={{\n width: \"200px\",\n height: \"200px\",\n borderRadius: \"var(--radius-md)\",\n border: \"1px solid var(--canvas-border)\",\n }}\n >\n <img\n src={item.imageUrl}\n alt={item.title}\n className=\"w-full h-full object-cover\"\n />\n </div>\n\n {/* Content */}\n <div\n className=\"flex flex-col flex-1 min-w-0\"\n style={{ gap: \"var(--spacing-3xl)\" }}\n >\n {/* Title and Meta */}\n <div\n className=\"flex flex-col\"\n style={{ gap: \"var(--spacing-xs)\" }}\n >\n {/* Title */}\n <h3\n className=\"m-0\"\n style={{\n fontFamily: \"var(--typo-body-xl-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-xl-size)\",\n fontWeight: 600,\n lineHeight: \"var(--typo-body-xl-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n {item.title}\n </h3>\n\n {/* Author and Date */}\n <div\n className=\"flex items-center justify-between w-full\"\n style={{ gap: \"var(--spacing-xl)\" }}\n >\n <p\n className=\"m-0\"\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: \"var(--typo-body-s-weight)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n <span style={{ color: \"var(--canvas-text-muted)\" }}>by</span>{\" \"}\n <span style={{ fontWeight: 500 }}>{item.author}</span>\n </p>\n <p\n className=\"m-0 whitespace-nowrap\"\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: \"var(--typo-body-s-weight)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text-muted)\",\n }}\n >\n {item.date}\n </p>\n </div>\n </div>\n\n {/* Description */}\n <p\n className=\"m-0\"\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: \"var(--typo-body-s-weight)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text-muted)\",\n }}\n >\n {item.description}\n </p>\n\n {/* Tags */}\n {item.tags && item.tags.length > 0 && (\n <div\n className=\"flex flex-wrap\"\n style={{ gap: \"var(--spacing-md)\" }}\n >\n {item.tags.map((tag) => (\n <TagPill key={tag.id} label={tag.label} />\n ))}\n </div>\n )}\n </div>\n </div>\n );\n}\n\n// ============================================\n// Main Component\n// ============================================\n\n/**\n * Canvas Design System - Standard List with Image Block\n *\n * A configurable list block with image thumbnails, commonly used for\n * blog posts, articles, or any content with featured images.\n * Includes header section with title, subtitle, and sort/filter controls.\n *\n * @example\n * ```tsx\n * <StandardListWithImage\n * title=\"Blog\"\n * subtitle=\"Read our latest articles\"\n * items={blogPosts}\n * onItemClick={(item) => console.log(\"Clicked:\", item.title)}\n * />\n * ```\n */\nexport function StandardListWithImage({\n title = \"Blog\",\n subtitle = \"Read our latest articles about tech\",\n items = defaultItems,\n sortOptions = defaultSortOptions,\n onSort,\n onItemClick,\n className,\n}: StandardListWithImageProps) {\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} />\n\n {/* List Section */}\n <div\n className=\"flex flex-col w-full\"\n style={{ borderTop: \"1px solid var(--canvas-border)\" }}\n >\n {items.map((item) => (\n <ListItemCard\n key={item.id}\n item={item}\n onClick={onItemClick ? () => onItemClick(item) : undefined}\n />\n ))}\n </div>\n </div>\n );\n}\n"
10
10
  }
11
11
  ],
12
12
  "dependencies": [],
13
13
  "registryDependencies": [
14
- "utils",
15
- "title-group"
14
+ "lib/utils",
15
+ "blocks/title-group"
16
16
  ]
17
17
  }
@@ -11,6 +11,6 @@
11
11
  ],
12
12
  "dependencies": [],
13
13
  "registryDependencies": [
14
- "utils"
14
+ "lib/utils"
15
15
  ]
16
16
  }
@@ -6,7 +6,7 @@
6
6
  {
7
7
  "path": "components/blocks/marketing/team-cards-grid.tsx",
8
8
  "type": "registry:block",
9
- "content": "\"use client\";\n\nimport { LinkedinLogo, XLogo } from \"@phosphor-icons/react\";\nimport { Typography } from \"../../ui/typography\";\n\ninterface TeamMember {\n name: string;\n role: string;\n image: string;\n linkedIn?: string;\n twitter?: string;\n}\n\ninterface TeamCardsGridProps {\n subtitle?: string;\n title?: string;\n description?: string;\n members?: TeamMember[];\n}\n\nconst defaultMembers: TeamMember[] = [\n {\n name: \"Jeffrey Connor\",\n role: \"Co-founder & CEO\",\n image:\n \"https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=400&h=400&fit=crop&crop=face\",\n linkedIn: \"#\",\n twitter: \"#\",\n },\n {\n name: \"Lily Sun\",\n role: \"Co-founder\",\n image:\n \"https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=400&h=400&fit=crop&crop=face\",\n linkedIn: \"#\",\n twitter: \"#\",\n },\n {\n name: \"Rajesh Mishra\",\n role: \"Chief Technology Officer\",\n image:\n \"https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=400&h=400&fit=crop&crop=face\",\n linkedIn: \"#\",\n twitter: \"#\",\n },\n {\n name: \"Francis Gaddi\",\n role: \"Chief Operating Officer\",\n image:\n \"https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=400&h=400&fit=crop&crop=face\",\n linkedIn: \"#\",\n twitter: \"#\",\n },\n {\n name: \"Aya Williams\",\n role: \"Chief Marketing Officer\",\n image:\n \"https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=400&h=400&fit=crop&crop=face\",\n linkedIn: \"#\",\n twitter: \"#\",\n },\n {\n name: \"Gabi del Rosario\",\n role: \"Chief Product Officer\",\n image:\n \"https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=400&h=400&fit=crop&crop=face\",\n linkedIn: \"#\",\n twitter: \"#\",\n },\n {\n name: \"Taylor Reed\",\n role: \"VP of Customer Success\",\n image:\n \"https://images.unsplash.com/photo-1519345182560-3f2917c472ef?w=400&h=400&fit=crop&crop=face\",\n linkedIn: \"#\",\n twitter: \"#\",\n },\n {\n name: \"Stacey Jones\",\n role: \"VP of Finance\",\n image:\n \"https://images.unsplash.com/photo-1580489944761-15a19d654956?w=400&h=400&fit=crop&crop=face\",\n linkedIn: \"#\",\n twitter: \"#\",\n },\n];\n\nexport function TeamCardsGrid({\n subtitle = \"PEOPLE\",\n title = \"Our team\",\n description = \"We are fortunate to work with amazing people all over the world\",\n members = defaultMembers,\n}: TeamCardsGridProps) {\n return (\n <section\n className=\"w-full px-6 md:px-20 py-16 md:py-24\"\n style={{ backgroundColor: \"var(--canvas-background)\" }}\n >\n <div className=\"max-w-[1240px] mx-auto flex flex-col gap-10 md:gap-12\">\n {/* Header */}\n <div className=\"flex flex-col gap-4 md:gap-6\">\n <div className=\"flex flex-col gap-3\">\n <Typography variant=\"body-xs\" as=\"span\" color=\"muted\" className=\"uppercase tracking-wide\">\n {subtitle}\n </Typography>\n <Typography variant=\"h3\" as=\"h2\">\n {title}\n </Typography>\n </div>\n <Typography variant=\"body-l\" color=\"muted\">\n {description}\n </Typography>\n </div>\n\n {/* Team Grid */}\n <div className=\"grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6 md:gap-8\">\n {members.map((member, index) => (\n <div\n key={index}\n className=\"rounded-xl overflow-hidden\"\n style={{\n border: \"1px solid var(--canvas-border)\",\n }}\n >\n {/* Photo */}\n <div\n className=\"w-full h-[220px] md:h-[286px]\"\n style={{\n borderBottom: \"1px solid var(--canvas-border)\",\n }}\n >\n <img\n src={member.image}\n alt={member.name}\n className=\"w-full h-full object-cover\"\n />\n </div>\n\n {/* Info */}\n <div className=\"flex flex-col gap-1 p-6 pb-6\">\n <Typography variant=\"body-xl\" as=\"h3\">\n {member.name}\n </Typography>\n <Typography variant=\"body-l\" as=\"p\" color=\"muted\">\n {member.role}\n </Typography>\n\n {/* Social Links */}\n <div className=\"flex gap-2.5 pt-2\">\n {member.linkedIn && (\n <a\n href={member.linkedIn}\n className=\"opacity-60 hover:opacity-100 transition-opacity\"\n style={{ color: \"var(--canvas-text)\" }}\n >\n <LinkedinLogo size={24} weight=\"fill\" />\n </a>\n )}\n {member.twitter && (\n <a\n href={member.twitter}\n className=\"opacity-60 hover:opacity-100 transition-opacity\"\n style={{ color: \"var(--canvas-text)\" }}\n >\n <XLogo size={22} weight=\"fill\" />\n </a>\n )}\n </div>\n </div>\n </div>\n ))}\n </div>\n </div>\n </section>\n );\n}\n"
9
+ "content": "\"use client\";\n\nimport { LinkedinLogo, XLogo } from \"@phosphor-icons/react\";\nimport { Typography } from \"../../ui/typography\";\nimport { AVATAR_MARCUS_WEBB, AVATAR_MAYA_JOHNSON, AVATAR_ETHAN_BROOKS, AVATAR_JASON_MORALES, AVATAR_SARAH_CHEN, AVATAR_NICOLE_PALMER, AVATAR_RYAN_KESSLER, AVATAR_LILY_TRAN } from \"../demo-avatars\";\n\ninterface TeamMember {\n name: string;\n role: string;\n image: string;\n linkedIn?: string;\n twitter?: string;\n}\n\ninterface TeamCardsGridProps {\n subtitle?: string;\n title?: string;\n description?: string;\n members?: TeamMember[];\n}\n\nconst defaultMembers: TeamMember[] = [\n {\n name: \"Marcus Webb\",\n role: \"Co-founder & CEO\",\n image: AVATAR_MARCUS_WEBB,\n linkedIn: \"#\",\n twitter: \"#\",\n },\n {\n name: \"Maya Johnson\",\n role: \"Co-founder\",\n image: AVATAR_MAYA_JOHNSON,\n linkedIn: \"#\",\n twitter: \"#\",\n },\n {\n name: \"Ethan Brooks\",\n role: \"Chief Technology Officer\",\n image: AVATAR_ETHAN_BROOKS,\n linkedIn: \"#\",\n twitter: \"#\",\n },\n {\n name: \"Jason Morales\",\n role: \"Chief Operating Officer\",\n image: AVATAR_JASON_MORALES,\n linkedIn: \"#\",\n twitter: \"#\",\n },\n {\n name: \"Sarah Chen\",\n role: \"Chief Marketing Officer\",\n image: AVATAR_SARAH_CHEN,\n linkedIn: \"#\",\n twitter: \"#\",\n },\n {\n name: \"Nicole Palmer\",\n role: \"Chief Product Officer\",\n image: AVATAR_NICOLE_PALMER,\n linkedIn: \"#\",\n twitter: \"#\",\n },\n {\n name: \"Ryan Kessler\",\n role: \"VP of Customer Success\",\n image: AVATAR_RYAN_KESSLER,\n linkedIn: \"#\",\n twitter: \"#\",\n },\n {\n name: \"Lily Tran\",\n role: \"VP of Finance\",\n image: AVATAR_LILY_TRAN,\n linkedIn: \"#\",\n twitter: \"#\",\n },\n];\n\nexport function TeamCardsGrid({\n subtitle = \"PEOPLE\",\n title = \"Our team\",\n description = \"We are fortunate to work with amazing people all over the world\",\n members = defaultMembers,\n}: TeamCardsGridProps) {\n return (\n <section\n className=\"w-full px-6 md:px-20 py-16 md:py-24\"\n style={{ backgroundColor: \"var(--canvas-background)\" }}\n >\n <div className=\"max-w-[1240px] mx-auto flex flex-col gap-10 md:gap-12\">\n {/* Header */}\n <div className=\"flex flex-col gap-4 md:gap-6\">\n <div className=\"flex flex-col gap-3\">\n <Typography variant=\"body-xs\" as=\"span\" color=\"muted\" className=\"uppercase tracking-wide\">\n {subtitle}\n </Typography>\n <Typography variant=\"h3\" as=\"h2\">\n {title}\n </Typography>\n </div>\n <Typography variant=\"body-l\" color=\"muted\">\n {description}\n </Typography>\n </div>\n\n {/* Team Grid */}\n <div className=\"grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6 md:gap-8\">\n {members.map((member, index) => (\n <div\n key={index}\n className=\"rounded-xl overflow-hidden\"\n style={{\n border: \"1px solid var(--canvas-border)\",\n }}\n >\n {/* Photo */}\n <div\n className=\"w-full h-[220px] md:h-[286px]\"\n style={{\n borderBottom: \"1px solid var(--canvas-border)\",\n }}\n >\n <img\n src={member.image}\n alt={member.name}\n className=\"w-full h-full object-cover\"\n />\n </div>\n\n {/* Info */}\n <div className=\"flex flex-col gap-1 p-6 pb-6\">\n <Typography variant=\"body-xl\" as=\"h3\">\n {member.name}\n </Typography>\n <Typography variant=\"body-l\" as=\"p\" color=\"muted\">\n {member.role}\n </Typography>\n\n {/* Social Links */}\n <div className=\"flex gap-2.5 pt-2\">\n {member.linkedIn && (\n <a\n href={member.linkedIn}\n className=\"opacity-60 hover:opacity-100 transition-opacity\"\n style={{ color: \"var(--canvas-text)\" }}\n >\n <LinkedinLogo size={24} weight=\"fill\" />\n </a>\n )}\n {member.twitter && (\n <a\n href={member.twitter}\n className=\"opacity-60 hover:opacity-100 transition-opacity\"\n style={{ color: \"var(--canvas-text)\" }}\n >\n <XLogo size={22} weight=\"fill\" />\n </a>\n )}\n </div>\n </div>\n </div>\n ))}\n </div>\n </div>\n </section>\n );\n}\n"
10
10
  }
11
11
  ],
12
12
  "dependencies": [
@@ -6,7 +6,7 @@
6
6
  {
7
7
  "path": "components/blocks/marketing/team-circular-grid.tsx",
8
8
  "type": "registry:block",
9
- "content": "\"use client\";\n\nimport { LinkedinLogo, XLogo } from \"@phosphor-icons/react\";\nimport { Typography } from \"../../ui/typography\";\n\ninterface TeamMember {\n name: string;\n role: string;\n image: string;\n linkedIn?: string;\n twitter?: string;\n}\n\ninterface TeamCircularGridProps {\n subtitle?: string;\n title?: string;\n description?: string;\n members?: TeamMember[];\n}\n\nconst defaultMembers: TeamMember[] = [\n {\n name: \"Jeffrey Connor\",\n role: \"Co-founder & CEO\",\n image:\n \"https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=400&h=400&fit=crop&crop=face\",\n linkedIn: \"#\",\n twitter: \"#\",\n },\n {\n name: \"Lily Sun\",\n role: \"Co-founder\",\n image:\n \"https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=400&h=400&fit=crop&crop=face\",\n linkedIn: \"#\",\n twitter: \"#\",\n },\n {\n name: \"Rajesh Mishra\",\n role: \"Chief Technology Officer\",\n image:\n \"https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=400&h=400&fit=crop&crop=face\",\n linkedIn: \"#\",\n twitter: \"#\",\n },\n {\n name: \"Francis Gaddi\",\n role: \"Chief Operating Officer\",\n image:\n \"https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=400&h=400&fit=crop&crop=face\",\n linkedIn: \"#\",\n twitter: \"#\",\n },\n {\n name: \"Aya Williams\",\n role: \"Chief Marketing Officer\",\n image:\n \"https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=400&h=400&fit=crop&crop=face\",\n linkedIn: \"#\",\n twitter: \"#\",\n },\n {\n name: \"Gabi del Rosario\",\n role: \"Chief Product Officer\",\n image:\n \"https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=400&h=400&fit=crop&crop=face\",\n linkedIn: \"#\",\n twitter: \"#\",\n },\n {\n name: \"Taylor Reed\",\n role: \"VP of Customer Success\",\n image:\n \"https://images.unsplash.com/photo-1519345182560-3f2917c472ef?w=400&h=400&fit=crop&crop=face\",\n linkedIn: \"#\",\n twitter: \"#\",\n },\n {\n name: \"Stacey Jones\",\n role: \"VP of Finance\",\n image:\n \"https://images.unsplash.com/photo-1580489944761-15a19d654956?w=400&h=400&fit=crop&crop=face\",\n linkedIn: \"#\",\n twitter: \"#\",\n },\n];\n\nexport function TeamCircularGrid({\n subtitle = \"PEOPLE\",\n title = \"Our team\",\n description = \"We are fortunate to work with amazing people all over the world\",\n members = defaultMembers,\n}: TeamCircularGridProps) {\n return (\n <section\n className=\"w-full px-6 md:px-20 py-16 md:py-24\"\n style={{ backgroundColor: \"var(--canvas-background)\" }}\n >\n <div className=\"max-w-[1240px] mx-auto flex flex-col items-center gap-10 md:gap-12\">\n {/* Header */}\n <div className=\"flex flex-col items-center gap-4 md:gap-6 text-center\">\n <div className=\"flex flex-col items-center gap-3\">\n <Typography variant=\"body-xs\" as=\"span\" color=\"muted\" className=\"uppercase tracking-wide\">\n {subtitle}\n </Typography>\n <Typography variant=\"h3\" as=\"h2\">\n {title}\n </Typography>\n </div>\n <Typography variant=\"body-l\" color=\"muted\">\n {description}\n </Typography>\n </div>\n\n {/* Team Grid */}\n <div className=\"grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-6 md:gap-8 w-full\">\n {members.map((member, index) => (\n <div\n key={index}\n className=\"flex flex-col items-center gap-5 rounded-xl\"\n >\n {/* Circular Photo */}\n <div className=\"w-[160px] h-[160px] md:w-[240px] md:h-[240px] rounded-full overflow-hidden\">\n <img\n src={member.image}\n alt={member.name}\n className=\"w-full h-full object-cover\"\n />\n </div>\n\n {/* Info */}\n <div className=\"flex flex-col items-center gap-1 text-center px-4 pb-4\">\n <Typography variant=\"body-xl\" as=\"h3\">\n {member.name}\n </Typography>\n <Typography variant=\"body-l\" as=\"p\" color=\"muted\">\n {member.role}\n </Typography>\n\n {/* Social Links */}\n <div className=\"flex gap-2.5 pt-2\">\n {member.linkedIn && (\n <a\n href={member.linkedIn}\n className=\"opacity-60 hover:opacity-100 transition-opacity\"\n style={{ color: \"var(--canvas-text)\" }}\n >\n <LinkedinLogo size={24} weight=\"fill\" />\n </a>\n )}\n {member.twitter && (\n <a\n href={member.twitter}\n className=\"opacity-60 hover:opacity-100 transition-opacity\"\n style={{ color: \"var(--canvas-text)\" }}\n >\n <XLogo size={22} weight=\"fill\" />\n </a>\n )}\n </div>\n </div>\n </div>\n ))}\n </div>\n </div>\n </section>\n );\n}\n"
9
+ "content": "\"use client\";\n\nimport { LinkedinLogo, XLogo } from \"@phosphor-icons/react\";\nimport { Typography } from \"../../ui/typography\";\nimport { AVATAR_MARCUS_WEBB, AVATAR_MAYA_JOHNSON, AVATAR_ETHAN_BROOKS, AVATAR_JASON_MORALES, AVATAR_SARAH_CHEN, AVATAR_NICOLE_PALMER, AVATAR_RYAN_KESSLER, AVATAR_LILY_TRAN } from \"../demo-avatars\";\n\ninterface TeamMember {\n name: string;\n role: string;\n image: string;\n linkedIn?: string;\n twitter?: string;\n}\n\ninterface TeamCircularGridProps {\n subtitle?: string;\n title?: string;\n description?: string;\n members?: TeamMember[];\n}\n\nconst defaultMembers: TeamMember[] = [\n {\n name: \"Marcus Webb\",\n role: \"Co-founder & CEO\",\n image: AVATAR_MARCUS_WEBB,\n linkedIn: \"#\",\n twitter: \"#\",\n },\n {\n name: \"Maya Johnson\",\n role: \"Co-founder\",\n image: AVATAR_MAYA_JOHNSON,\n linkedIn: \"#\",\n twitter: \"#\",\n },\n {\n name: \"Ethan Brooks\",\n role: \"Chief Technology Officer\",\n image: AVATAR_ETHAN_BROOKS,\n linkedIn: \"#\",\n twitter: \"#\",\n },\n {\n name: \"Jason Morales\",\n role: \"Chief Operating Officer\",\n image: AVATAR_JASON_MORALES,\n linkedIn: \"#\",\n twitter: \"#\",\n },\n {\n name: \"Sarah Chen\",\n role: \"Chief Marketing Officer\",\n image: AVATAR_SARAH_CHEN,\n linkedIn: \"#\",\n twitter: \"#\",\n },\n {\n name: \"Nicole Palmer\",\n role: \"Chief Product Officer\",\n image: AVATAR_NICOLE_PALMER,\n linkedIn: \"#\",\n twitter: \"#\",\n },\n {\n name: \"Ryan Kessler\",\n role: \"VP of Customer Success\",\n image: AVATAR_RYAN_KESSLER,\n linkedIn: \"#\",\n twitter: \"#\",\n },\n {\n name: \"Lily Tran\",\n role: \"VP of Finance\",\n image: AVATAR_LILY_TRAN,\n linkedIn: \"#\",\n twitter: \"#\",\n },\n];\n\nexport function TeamCircularGrid({\n subtitle = \"PEOPLE\",\n title = \"Our team\",\n description = \"We are fortunate to work with amazing people all over the world\",\n members = defaultMembers,\n}: TeamCircularGridProps) {\n return (\n <section\n className=\"w-full px-6 md:px-20 py-16 md:py-24\"\n style={{ backgroundColor: \"var(--canvas-background)\" }}\n >\n <div className=\"max-w-[1240px] mx-auto flex flex-col items-center gap-10 md:gap-12\">\n {/* Header */}\n <div className=\"flex flex-col items-center gap-4 md:gap-6 text-center\">\n <div className=\"flex flex-col items-center gap-3\">\n <Typography variant=\"body-xs\" as=\"span\" color=\"muted\" className=\"uppercase tracking-wide\">\n {subtitle}\n </Typography>\n <Typography variant=\"h3\" as=\"h2\">\n {title}\n </Typography>\n </div>\n <Typography variant=\"body-l\" color=\"muted\">\n {description}\n </Typography>\n </div>\n\n {/* Team Grid */}\n <div className=\"grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-6 md:gap-8 w-full\">\n {members.map((member, index) => (\n <div\n key={index}\n className=\"flex flex-col items-center gap-5 rounded-xl\"\n >\n {/* Circular Photo */}\n <div className=\"w-[160px] h-[160px] md:w-[240px] md:h-[240px] rounded-full overflow-hidden\">\n <img\n src={member.image}\n alt={member.name}\n className=\"w-full h-full object-cover\"\n />\n </div>\n\n {/* Info */}\n <div className=\"flex flex-col items-center gap-1 text-center px-4 pb-4\">\n <Typography variant=\"body-xl\" as=\"h3\">\n {member.name}\n </Typography>\n <Typography variant=\"body-l\" as=\"p\" color=\"muted\">\n {member.role}\n </Typography>\n\n {/* Social Links */}\n <div className=\"flex gap-2.5 pt-2\">\n {member.linkedIn && (\n <a\n href={member.linkedIn}\n className=\"opacity-60 hover:opacity-100 transition-opacity\"\n style={{ color: \"var(--canvas-text)\" }}\n >\n <LinkedinLogo size={24} weight=\"fill\" />\n </a>\n )}\n {member.twitter && (\n <a\n href={member.twitter}\n className=\"opacity-60 hover:opacity-100 transition-opacity\"\n style={{ color: \"var(--canvas-text)\" }}\n >\n <XLogo size={22} weight=\"fill\" />\n </a>\n )}\n </div>\n </div>\n </div>\n ))}\n </div>\n </div>\n </section>\n );\n}\n"
10
10
  }
11
11
  ],
12
12
  "dependencies": [
@@ -6,7 +6,7 @@
6
6
  {
7
7
  "path": "components/blocks/marketing/testimonial-carousel.tsx",
8
8
  "type": "registry:block",
9
- "content": "\"use client\";\n\nimport { CaretLeft, CaretRight } from \"@phosphor-icons/react\";\nimport { useState } from \"react\";\nimport { Typography } from \"../../ui/typography\";\n\ninterface Testimonial {\n id: string;\n quote: string;\n highlightedWords?: string[];\n author: string;\n location: string;\n image: string;\n}\n\nconst defaultTestimonials: Testimonial[] = [\n {\n id: \"1\",\n quote: '\"Great prices on incredible places! Booking is easy, and I always find the perfect stay. Highly recommend!\"',\n highlightedWords: [\"Great prices\", \"easy\"],\n author: \"Mary Trott\",\n location: \"San Francisco, California\",\n image: \"https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=320&h=400&fit=crop\",\n },\n {\n id: \"2\",\n quote: '\"The experience was seamless from start to finish. I found exactly what I was looking for within minutes!\"',\n highlightedWords: [\"seamless\", \"minutes\"],\n author: \"John Chen\",\n location: \"New York, NY\",\n image: \"https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=320&h=400&fit=crop\",\n },\n];\n\ninterface TestimonialCarouselProps {\n testimonials?: Testimonial[];\n}\n\nexport function TestimonialCarousel({ testimonials = defaultTestimonials }: TestimonialCarouselProps) {\n const [currentIndex, setCurrentIndex] = useState(0);\n const current = testimonials[currentIndex];\n\n const handlePrev = () => {\n setCurrentIndex((prev) => (prev === 0 ? testimonials.length - 1 : prev - 1));\n };\n\n const handleNext = () => {\n setCurrentIndex((prev) => (prev === testimonials.length - 1 ? 0 : prev + 1));\n };\n\n // Function to highlight specific words\n const renderQuote = (quote: string, highlights?: string[]) => {\n if (!highlights || highlights.length === 0) return quote;\n \n let result = quote;\n highlights.forEach((word) => {\n result = result.replace(\n new RegExp(`(${word})`, \"gi\"),\n `<span style=\"color: var(--canvas-highlight-orange);\">$1</span>`\n );\n });\n return <span dangerouslySetInnerHTML={{ __html: result }} />;\n };\n\n return (\n <section \n className=\"w-full px-4 md:px-8 lg:px-10 py-10 md:py-16\"\n style={{\n backgroundColor: \"var(--canvas-dark-section-bg)\",\n }}\n >\n <div \n className=\"w-full max-w-[1200px] mx-auto flex flex-col lg:flex-row items-center\"\n style={{ gap: \"var(--spacing-6xl)\" }}\n >\n {/* Content */}\n <div className=\"flex-1 flex flex-col\" style={{ gap: \"var(--spacing-3xl)\" }}>\n <Typography variant=\"h3\" as=\"p\" style={{ color: \"white\" }}>\n {renderQuote(current.quote, current.highlightedWords)}\n </Typography>\n \n <div className=\"flex flex-col\" style={{ gap: \"var(--spacing-2xl)\" }}>\n <div>\n <Typography variant=\"body-l\" style={{ color: \"white\" }}>\n {current.author}\n </Typography>\n <Typography variant=\"body-l\" color=\"muted\" style={{ color: \"var(--canvas-text-placeholder)\" }}>\n {current.location}\n </Typography>\n </div>\n \n {/* Navigation */}\n <div className=\"flex gap-5\">\n <button \n onClick={handlePrev}\n className=\"flex items-center justify-center hover:opacity-80 transition-opacity\"\n style={{\n width: \"48px\",\n height: \"48px\",\n backgroundColor: \"var(--canvas-border)\",\n borderRadius: \"var(--spacing-md)\",\n }}\n >\n <CaretLeft size={32} style={{ color: \"var(--canvas-text)\" }} weight=\"bold\" />\n </button>\n <button \n onClick={handleNext}\n className=\"flex items-center justify-center hover:opacity-80 transition-opacity\"\n style={{\n width: \"48px\",\n height: \"48px\",\n backgroundColor: \"var(--canvas-border)\",\n borderRadius: \"var(--spacing-md)\",\n }}\n >\n <CaretRight size={32} style={{ color: \"var(--canvas-text)\" }} weight=\"bold\" />\n </button>\n </div>\n </div>\n </div>\n \n {/* Image */}\n <div \n className=\"shrink-0 overflow-hidden hidden lg:block\"\n style={{\n width: \"320px\",\n height: \"400px\",\n borderRadius: \"var(--spacing-lg)\",\n }}\n >\n <img \n src={current.image} \n alt={current.author}\n className=\"w-full h-full object-cover\"\n />\n </div>\n </div>\n </section>\n );\n}\n"
9
+ "content": "\"use client\";\n\nimport { CaretLeft, CaretRight } from \"@phosphor-icons/react\";\nimport { useState } from \"react\";\nimport { Typography } from \"../../ui/typography\";\nimport { AVATAR_NICOLE_PALMER, AVATAR_ETHAN_BROOKS } from \"../demo-avatars\";\n\ninterface Testimonial {\n id: string;\n quote: string;\n highlightedWords?: string[];\n author: string;\n location: string;\n image: string;\n}\n\nconst defaultTestimonials: Testimonial[] = [\n {\n id: \"1\",\n quote: '\"Great prices on incredible places! Booking is easy, and I always find the perfect stay. Highly recommend!\"',\n highlightedWords: [\"Great prices\", \"easy\"],\n author: \"Nicole Palmer\",\n location: \"San Francisco, California\",\n image: AVATAR_NICOLE_PALMER,\n },\n {\n id: \"2\",\n quote: '\"The experience was seamless from start to finish. I found exactly what I was looking for within minutes!\"',\n highlightedWords: [\"seamless\", \"minutes\"],\n author: \"Ethan Brooks\",\n location: \"New York, NY\",\n image: AVATAR_ETHAN_BROOKS,\n },\n];\n\ninterface TestimonialCarouselProps {\n testimonials?: Testimonial[];\n}\n\nexport function TestimonialCarousel({ testimonials = defaultTestimonials }: TestimonialCarouselProps) {\n const [currentIndex, setCurrentIndex] = useState(0);\n const current = testimonials[currentIndex];\n\n const handlePrev = () => {\n setCurrentIndex((prev) => (prev === 0 ? testimonials.length - 1 : prev - 1));\n };\n\n const handleNext = () => {\n setCurrentIndex((prev) => (prev === testimonials.length - 1 ? 0 : prev + 1));\n };\n\n // Function to highlight specific words\n const renderQuote = (quote: string, highlights?: string[]) => {\n if (!highlights || highlights.length === 0) return quote;\n \n let result = quote;\n highlights.forEach((word) => {\n result = result.replace(\n new RegExp(`(${word})`, \"gi\"),\n `<span style=\"color: var(--canvas-highlight-orange);\">$1</span>`\n );\n });\n return <span dangerouslySetInnerHTML={{ __html: result }} />;\n };\n\n return (\n <section \n className=\"w-full px-4 md:px-8 lg:px-10 py-10 md:py-16\"\n style={{\n backgroundColor: \"var(--canvas-dark-section-bg)\",\n }}\n >\n <div \n className=\"w-full max-w-[1200px] mx-auto flex flex-col lg:flex-row items-center\"\n style={{ gap: \"var(--spacing-6xl)\" }}\n >\n {/* Content */}\n <div className=\"flex-1 flex flex-col\" style={{ gap: \"var(--spacing-3xl)\" }}>\n <Typography variant=\"h3\" as=\"p\" style={{ color: \"white\" }}>\n {renderQuote(current.quote, current.highlightedWords)}\n </Typography>\n \n <div className=\"flex flex-col\" style={{ gap: \"var(--spacing-2xl)\" }}>\n <div>\n <Typography variant=\"body-l\" style={{ color: \"white\" }}>\n {current.author}\n </Typography>\n <Typography variant=\"body-l\" color=\"muted\" style={{ color: \"var(--canvas-text-placeholder)\" }}>\n {current.location}\n </Typography>\n </div>\n \n {/* Navigation */}\n <div className=\"flex gap-5\">\n <button \n onClick={handlePrev}\n className=\"flex items-center justify-center hover:opacity-80 transition-opacity\"\n style={{\n width: \"48px\",\n height: \"48px\",\n backgroundColor: \"var(--canvas-border)\",\n borderRadius: \"var(--spacing-md)\",\n }}\n >\n <CaretLeft size={32} style={{ color: \"var(--canvas-text)\" }} weight=\"bold\" />\n </button>\n <button \n onClick={handleNext}\n className=\"flex items-center justify-center hover:opacity-80 transition-opacity\"\n style={{\n width: \"48px\",\n height: \"48px\",\n backgroundColor: \"var(--canvas-border)\",\n borderRadius: \"var(--spacing-md)\",\n }}\n >\n <CaretRight size={32} style={{ color: \"var(--canvas-text)\" }} weight=\"bold\" />\n </button>\n </div>\n </div>\n </div>\n \n {/* Image */}\n <div \n className=\"shrink-0 overflow-hidden hidden lg:block\"\n style={{\n width: \"320px\",\n height: \"400px\",\n borderRadius: \"var(--spacing-lg)\",\n }}\n >\n <img \n src={current.image} \n alt={current.author}\n className=\"w-full h-full object-cover\"\n />\n </div>\n </div>\n </section>\n );\n}\n"
10
10
  }
11
11
  ],
12
12
  "dependencies": [
@@ -11,9 +11,9 @@
11
11
  ],
12
12
  "dependencies": [],
13
13
  "registryDependencies": [
14
- "utils",
15
- "button",
16
- "select",
17
- "filter-popover"
14
+ "lib/utils",
15
+ "ui/button",
16
+ "ui/select",
17
+ "blocks/filter-popover"
18
18
  ]
19
19
  }