canvas-ui-sdk 0.1.6 → 0.2.0

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 (153) hide show
  1. package/dist/cli/index.js +516 -0
  2. package/dist/index.d.ts +67 -3
  3. package/dist/index.js +2588 -301
  4. package/dist/index.js.map +1 -1
  5. package/mcp/dist/index.js +5 -1
  6. package/package.json +18 -2
  7. package/registry/blocks/activity-feed.json +19 -0
  8. package/registry/blocks/blog-cards.json +16 -0
  9. package/registry/blocks/bottom-input-chat-widget.json +19 -0
  10. package/registry/blocks/canvas-item.json +18 -0
  11. package/registry/blocks/category-grid.json +16 -0
  12. package/registry/blocks/centered-hero.json +14 -0
  13. package/registry/blocks/chat-message.json +18 -0
  14. package/registry/blocks/circular-progress-bar-list.json +18 -0
  15. package/registry/blocks/component-palette.json +21 -0
  16. package/registry/blocks/component-search.json +19 -0
  17. package/registry/blocks/content-dropzone.json +16 -0
  18. package/registry/blocks/content-with-image.json +14 -0
  19. package/registry/blocks/core-values-grid.json +16 -0
  20. package/registry/blocks/credit-card-display.json +16 -0
  21. package/registry/blocks/cta-banner.json +14 -0
  22. package/registry/blocks/custom-component-helper.json +19 -0
  23. package/registry/blocks/destination-cards.json +16 -0
  24. package/registry/blocks/empty-state.json +16 -0
  25. package/registry/blocks/faq-accordion.json +16 -0
  26. package/registry/blocks/faqs-table.json +18 -0
  27. package/registry/blocks/feature-with-image.json +16 -0
  28. package/registry/blocks/featured-news-cards.json +16 -0
  29. package/registry/blocks/featured-places.json +16 -0
  30. package/registry/blocks/features-comparison.json +16 -0
  31. package/registry/blocks/filter-popover.json +28 -0
  32. package/registry/blocks/fixed-column-data-table.json +20 -0
  33. package/registry/blocks/flair-banner.json +16 -0
  34. package/registry/blocks/footer-navbar.json +17 -0
  35. package/registry/blocks/form-group.json +29 -0
  36. package/registry/blocks/gallery-section.json +14 -0
  37. package/registry/blocks/gradient-banner.json +16 -0
  38. package/registry/blocks/graph-metric-tiles.json +20 -0
  39. package/registry/blocks/grid-tiles-list.json +20 -0
  40. package/registry/blocks/hero-dark-centered.json +16 -0
  41. package/registry/blocks/hero-dark-with-image.json +16 -0
  42. package/registry/blocks/hero-fullwidth-image.json +16 -0
  43. package/registry/blocks/hero-section.json +16 -0
  44. package/registry/blocks/how-it-works.json +16 -0
  45. package/registry/blocks/image-feed-with-nested-comments.json +20 -0
  46. package/registry/blocks/infinity-canvas.json +58 -0
  47. package/registry/blocks/large-image-labels-list.json +19 -0
  48. package/registry/blocks/loader.json +19 -0
  49. package/registry/blocks/login-branding-panel.json +16 -0
  50. package/registry/blocks/menu-section.json +18 -0
  51. package/registry/blocks/menufocus-template.json +19 -0
  52. package/registry/blocks/messenger-sidebar.json +19 -0
  53. package/registry/blocks/metrics-section.json +14 -0
  54. package/registry/blocks/mobile-bottom-nav.json +18 -0
  55. package/registry/blocks/monthly-calendar-widget.json +20 -0
  56. package/registry/blocks/nested-comments-table.json +21 -0
  57. package/registry/blocks/nested-data-table.json +22 -0
  58. package/registry/blocks/office-locations.json +14 -0
  59. package/registry/blocks/page-header-section.json +17 -0
  60. package/registry/blocks/page-previews.json +29 -0
  61. package/registry/blocks/pagination.json +20 -0
  62. package/registry/blocks/participant-list.json +17 -0
  63. package/registry/blocks/persona-card.json +18 -0
  64. package/registry/blocks/pill-tabs.json +19 -0
  65. package/registry/blocks/pricing-cards.json +16 -0
  66. package/registry/blocks/pricing-cta.json +14 -0
  67. package/registry/blocks/profile-card.json +20 -0
  68. package/registry/blocks/profile-grid-tiles-list.json +21 -0
  69. package/registry/blocks/profile-image-uploader.json +19 -0
  70. package/registry/blocks/profile-info-cards.json +19 -0
  71. package/registry/blocks/progress-bar.json +16 -0
  72. package/registry/blocks/prompt-template.json +18 -0
  73. package/registry/blocks/reviews-grid.json +14 -0
  74. package/registry/blocks/reviews-table.json +19 -0
  75. package/registry/blocks/screen-flowchart.json +19 -0
  76. package/registry/blocks/screen-prompt-builder.json +19 -0
  77. package/registry/blocks/screen-prompt-template.json +18 -0
  78. package/registry/blocks/search-bar.json +19 -0
  79. package/registry/blocks/search-sidebar.json +25 -0
  80. package/registry/blocks/settings-list-row.json +20 -0
  81. package/registry/blocks/sidebar-cards.json +18 -0
  82. package/registry/blocks/sidebar-profile-card.json +21 -0
  83. package/registry/blocks/slideshow-grid-tiles.json +21 -0
  84. package/registry/blocks/social-feed.json +20 -0
  85. package/registry/blocks/social-proof.json +14 -0
  86. package/registry/blocks/standard-data-table.json +20 -0
  87. package/registry/blocks/standard-list-with-image.json +17 -0
  88. package/registry/blocks/step-tracker.json +16 -0
  89. package/registry/blocks/team-cards-grid.json +16 -0
  90. package/registry/blocks/team-circular-grid.json +16 -0
  91. package/registry/blocks/testimonial-carousel.json +16 -0
  92. package/registry/blocks/upvoting-posts-table.json +22 -0
  93. package/registry/blocks/vertical-how-it-works.json +16 -0
  94. package/registry/blocks/vertical-step-tracker.json +17 -0
  95. package/registry/blocks/video-chat-controls.json +18 -0
  96. package/registry/blocks/video-content-section.json +16 -0
  97. package/registry/blocks/video-playlist.json +18 -0
  98. package/registry/blocks/webcam-preview.json +18 -0
  99. package/registry/blocks/youtube-player.json +16 -0
  100. package/registry/hooks/use-css-variable-sync.json +14 -0
  101. package/registry/hooks/use-mobile.json +14 -0
  102. package/registry/index.json +730 -0
  103. package/registry/layout/account-settings-shell.json +20 -0
  104. package/registry/layout/dashboard-shell.json +23 -0
  105. package/registry/layout/double-sidebar-shell.json +23 -0
  106. package/registry/layout/double-sidebar.json +20 -0
  107. package/registry/layout/header.json +22 -0
  108. package/registry/layout/icon-sidebar-shell.json +23 -0
  109. package/registry/layout/icon-sidebar.json +19 -0
  110. package/registry/layout/mobile-menu-shell.json +19 -0
  111. package/registry/layout/multistep-progressbar-shell.json +23 -0
  112. package/registry/layout/multistep-shell.json +21 -0
  113. package/registry/layout/multistep-sidebar-shell.json +22 -0
  114. package/registry/layout/project-context-shell.json +20 -0
  115. package/registry/layout/search-bar-shell.json +22 -0
  116. package/registry/layout/sidebar-nav.json +18 -0
  117. package/registry/layout/sidebar.json +20 -0
  118. package/registry/layout/standard-page-shell.json +21 -0
  119. package/registry/layout/vertical-multistep-shell.json +23 -0
  120. package/registry/lib/utils.json +17 -0
  121. package/registry/ui/avatar.json +18 -0
  122. package/registry/ui/button.json +19 -0
  123. package/registry/ui/calendar.json +20 -0
  124. package/registry/ui/checkbox.json +19 -0
  125. package/registry/ui/date-input.json +18 -0
  126. package/registry/ui/dialog.json +19 -0
  127. package/registry/ui/dropdown-menu.json +19 -0
  128. package/registry/ui/file-uploader.json +18 -0
  129. package/registry/ui/image-uploader.json +18 -0
  130. package/registry/ui/input.json +16 -0
  131. package/registry/ui/label.json +18 -0
  132. package/registry/ui/line-tabs.json +16 -0
  133. package/registry/ui/multiselect-checkbox-field.json +18 -0
  134. package/registry/ui/multiselect-tags.json +18 -0
  135. package/registry/ui/popover.json +18 -0
  136. package/registry/ui/radio-group.json +19 -0
  137. package/registry/ui/range-input.json +17 -0
  138. package/registry/ui/scroll-area.json +18 -0
  139. package/registry/ui/searchbox.json +18 -0
  140. package/registry/ui/select.json +20 -0
  141. package/registry/ui/selectable-pills.json +16 -0
  142. package/registry/ui/separator.json +18 -0
  143. package/registry/ui/sheet.json +19 -0
  144. package/registry/ui/sidebar.json +27 -0
  145. package/registry/ui/skeleton.json +16 -0
  146. package/registry/ui/slider.json +18 -0
  147. package/registry/ui/switch.json +18 -0
  148. package/registry/ui/tabs.json +18 -0
  149. package/registry/ui/text-input.json +16 -0
  150. package/registry/ui/textarea.json +18 -0
  151. package/registry/ui/tooltip.json +18 -0
  152. package/registry/ui/typography.json +16 -0
  153. package/styles/tokens.reference.css +35 -3
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "infinity-canvas",
3
+ "type": "registry:block",
4
+ "description": "Pannable, zoomable infinite canvas for placing components. Supports drag-and-drop from palette.",
5
+ "files": [
6
+ {
7
+ "path": "components/blocks/infinity-canvas.tsx",
8
+ "type": "registry:block",
9
+ "content": "\"use client\";\n\nimport { useState, useCallback, useRef, useEffect } from \"react\";\nimport { TransformWrapper, TransformComponent, ReactZoomPanPinchRef } from \"react-zoom-pan-pinch\";\nimport { useDroppable } from \"@dnd-kit/core\";\nimport { cn } from \"../../lib/utils\";\nimport { CanvasItem, CanvasItemData } from \"./canvas-item\";\nimport { ZoomIn, ZoomOut, Maximize2, Grid3X3 } from \"lucide-react\";\n\n// =====================\n// Block Component Imports\n// =====================\nimport { ProfileCard } from \"./profile-card\";\nimport { StandardDataTable } from \"./standard-data-table\";\nimport { StepTracker, defaultSteps } from \"./step-tracker\";\nimport { VerticalStepTracker } from \"./vertical-step-tracker\";\nimport { ProgressBar } from \"./progress-bar\";\nimport { FlairBanner } from \"./flair-banner\";\nimport { GradientBanner } from \"./gradient-banner\";\nimport { MessengerSidebar } from \"./messenger-sidebar\";\nimport { PillTabs } from \"./pill-tabs\";\nimport { ChatBubble } from \"./chat-message\";\nimport { VideoChatControls } from \"./video-chat-controls\";\nimport { WebcamPreview } from \"./webcam-preview\";\nimport { ParticipantList } from \"./participant-list\";\nimport { VideoContentSection } from \"./video-content-section\";\nimport { VideoPlaylistCard } from \"./video-playlist\";\nimport { SearchBar } from \"./search-bar\";\nimport { FilterPopover } from \"./filter-popover\";\nimport { SettingsListRow } from \"./settings-list-row\";\nimport { ProfileImageUploader } from \"./profile-image-uploader\";\nimport { LoginBrandingPanel } from \"./login-branding-panel\";\nimport { SidebarProfileCard } from \"./sidebar-profile-card\";\nimport { StatsCard, PortfolioCard } from \"./profile-info-cards\";\nimport { LinksCard, InfoCard } from \"./sidebar-cards\";\nimport { CreditCardDisplay } from \"./credit-card-display\";\nimport { PageHeaderSection } from \"./page-header-section\";\nimport { MobileBottomNav } from \"./mobile-bottom-nav\";\n\n// Marketing blocks\nimport { \n HeroSection,\n HeroDarkWithImage,\n CenteredHero,\n TestimonialCarousel,\n ReviewsGrid,\n SocialProof,\n MetricsSection,\n FeatureWithImage,\n CoreValuesGrid,\n DestinationCards,\n TeamCardsGrid,\n TeamCircularGrid,\n CtaBanner,\n FooterNavbar,\n FeaturedNewsCards,\n OfficeLocations,\n} from \"./marketing\";\n\n// Pricing blocks\nimport { PricingCards, FaqAccordion, FeaturesComparison } from \"./pricing\";\n\n// Page Template Previews\nimport {\n PageAboutPreview,\n PageAccountPreview,\n PageAdminPortalPreview,\n PageCenteredProfilePreview,\n PageDoubleSidebarPreview,\n PageIconSidebarPreview,\n PageLoginPreview,\n PageMenuSectionsPreview,\n PageMessengerPreview,\n PageMobileMenuPreview,\n PageMultistepProgressbarPreview,\n PageMultistepSidebarPreview,\n PagePricingPreview,\n PageProductHomepagePreview,\n PageResetPasswordPreview,\n PageSearchBarPreview,\n PageSidebarProfilePreview,\n PageStandardPreview,\n PageStandardMultistepPreview,\n PageStandardSearchPreview,\n PageVerticalMultistepPreview,\n PageVideoChatPreview,\n PageVideoListPreview,\n} from \"./page-previews\";\n\n// UI Components\nimport { Button } from \"../ui/button\";\nimport { Checkbox } from \"../ui/checkbox\";\nimport { Input } from \"../ui/input\";\nimport { Switch } from \"../ui/switch\";\nimport { Avatar, AvatarImage, AvatarFallback } from \"../ui/avatar\";\nimport { RadioGroup, RadioGroupItem } from \"../ui/radio-group\";\nimport { Label } from \"../ui/label\";\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from \"../ui/select\";\n\n// =====================\n// Sample Data\n// =====================\nconst sampleProfileData = {\n name: \"Jeff Connor\",\n username: \"@jconnor\",\n avatarUrl: \"https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=150&h=150&fit=crop&crop=face\",\n rating: 5,\n location: \"San Francisco, CA\",\n joinDate: \"Joined January 2020\",\n stats: [\n { value: \"234\", label: \"Posts\" },\n { value: \"12.5k\", label: \"Followers\" },\n { value: \"892\", label: \"Following\" },\n ],\n bio: \"Product designer and creative director. Building beautiful digital experiences.\",\n tags: [{ label: \"Design\" }, { label: \"Product\" }, { label: \"Strategy\" }],\n};\n\nconst samplePillTabs = [\n { id: \"all\", label: \"All\" },\n { id: \"design\", label: \"Design\" },\n { id: \"dev\", label: \"Development\" },\n];\n\nconst sampleStats = [\n { value: \"234\", label: \"Posts\" },\n { value: \"12.5k\", label: \"Followers\" },\n { value: \"892\", label: \"Following\" },\n];\n\nconst sampleMobileNavItems = [\n { id: \"home\", label: \"Home\", icon: \"home\" as const },\n { id: \"search\", label: \"Search\", icon: \"search\" as const },\n { id: \"profile\", label: \"Profile\", icon: \"user\" as const },\n];\n\n\n// =====================\n// Component Renderers\n// =====================\nconst componentRenderers: Record<string, () => React.ReactNode> = {\n // =====================\n // PAGE TEMPLATES (scaled previews)\n // =====================\n PageAbout: () => <PageAboutPreview />,\n PageAccount: () => <PageAccountPreview />,\n PageAdminPortal: () => <PageAdminPortalPreview />,\n PageCenteredProfile: () => <PageCenteredProfilePreview />,\n PageDoubleSidebar: () => <PageDoubleSidebarPreview />,\n PageIconSidebar: () => <PageIconSidebarPreview />,\n PageLogin: () => <PageLoginPreview />,\n PageMenuSections: () => <PageMenuSectionsPreview />,\n PageMessenger: () => <PageMessengerPreview />,\n PageMobileMenu: () => <PageMobileMenuPreview />,\n PageMultistepProgressbar: () => <PageMultistepProgressbarPreview />,\n PageMultistepSidebar: () => <PageMultistepSidebarPreview />,\n PagePricing: () => <PagePricingPreview />,\n PageProductHomepage: () => <PageProductHomepagePreview />,\n PageResetPassword: () => <PageResetPasswordPreview />,\n PageSearchBar: () => <PageSearchBarPreview />,\n PageSidebarProfile: () => <PageSidebarProfilePreview />,\n PageStandard: () => <PageStandardPreview />,\n PageStandardMultistep: () => <PageStandardMultistepPreview />,\n PageStandardSearch: () => <PageStandardSearchPreview />,\n PageVerticalMultistep: () => <PageVerticalMultistepPreview />,\n PageVideoChat: () => <PageVideoChatPreview />,\n PageVideoList: () => <PageVideoListPreview />,\n\n // =====================\n // BLOCKS - Data & Tables\n // =====================\n StandardDataTable: () => (\n <div className=\"w-[600px] bg-white rounded-lg p-6 shadow-sm border border-[var(--canvas-border)]\">\n <StandardDataTable title=\"Team Members\" />\n </div>\n ),\n\n // =====================\n // BLOCKS - Cards & Profiles\n // =====================\n ProfileCard: () => (\n <div className=\"w-[400px] bg-white rounded-lg p-6 shadow-sm border border-[var(--canvas-border)]\">\n <ProfileCard {...sampleProfileData} showMenu={false} />\n </div>\n ),\n SidebarProfileCard: () => (\n <div className=\"w-[280px] bg-white rounded-lg p-4 shadow-sm border border-[var(--canvas-border)]\">\n <SidebarProfileCard \n name=\"Jeff Connor\" \n role=\"Product Designer\"\n avatarUrl={sampleProfileData.avatarUrl}\n />\n </div>\n ),\n ProfileInfoCards: () => (\n <div className=\"w-[400px] bg-white rounded-lg p-4 shadow-sm border border-[var(--canvas-border)]\">\n <StatsCard stats={sampleStats} />\n </div>\n ),\n SidebarCards: () => (\n <div className=\"w-[280px] bg-white rounded-lg p-4 shadow-sm border border-[var(--canvas-border)]\">\n <LinksCard />\n </div>\n ),\n CreditCardDisplay: () => (\n <div className=\"w-[360px] bg-white rounded-lg p-4 shadow-sm border border-[var(--canvas-border)]\">\n <CreditCardDisplay \n cardNumber=\"4242 4242 4242 4242\"\n cardHolder=\"JEFF CONNOR\"\n expiry=\"12/28\"\n />\n </div>\n ),\n\n // =====================\n // BLOCKS - Navigation & Progress\n // =====================\n StepTracker: () => (\n <div className=\"w-[500px] bg-white rounded-lg p-6 shadow-sm border border-[var(--canvas-border)]\">\n <StepTracker steps={defaultSteps} currentStep={1} />\n </div>\n ),\n VerticalStepTracker: () => (\n <div className=\"w-[280px] bg-white rounded-lg p-6 shadow-sm border border-[var(--canvas-border)]\">\n <VerticalStepTracker steps={defaultSteps} currentStep={1} />\n </div>\n ),\n ProgressBar: () => (\n <div className=\"w-[300px] bg-white rounded-lg p-6 shadow-sm border border-[var(--canvas-border)]\">\n <ProgressBar progress={65} />\n </div>\n ),\n PillTabs: () => (\n <div className=\"w-[300px] bg-white rounded-lg p-4 shadow-sm border border-[var(--canvas-border)]\">\n <PillTabs tabs={samplePillTabs} activeTab=\"all\" />\n </div>\n ),\n MobileBottomNav: () => (\n <div className=\"w-[375px] bg-white rounded-lg shadow-sm border border-[var(--canvas-border)] overflow-hidden\">\n <MobileBottomNav items={sampleMobileNavItems} activeItem=\"home\" />\n </div>\n ),\n\n // =====================\n // BLOCKS - Banners & Headers\n // =====================\n FlairBanner: () => (\n <div className=\"w-[500px] overflow-hidden rounded-lg shadow-sm\">\n <FlairBanner title=\"Welcome\" />\n </div>\n ),\n GradientBanner: () => (\n <div className=\"w-[500px] overflow-hidden rounded-lg shadow-sm\">\n <GradientBanner title=\"Welcome\" subtitle=\"Get started with your dashboard\" />\n </div>\n ),\n PageHeaderSection: () => (\n <div className=\"w-[600px] bg-white rounded-lg p-6 shadow-sm border border-[var(--canvas-border)]\">\n <PageHeaderSection \n title=\"Dashboard\" \n description=\"Manage your account and settings\"\n showTabs={false}\n />\n </div>\n ),\n\n // =====================\n // BLOCKS - Chat & Messaging\n // =====================\n MessengerSidebar: () => (\n <div className=\"w-[375px] h-[500px] overflow-hidden rounded-lg shadow-sm border border-[var(--canvas-border)]\">\n <MessengerSidebar />\n </div>\n ),\n ChatMessage: () => (\n <div className=\"w-[400px] bg-white rounded-lg p-4 shadow-sm border border-[var(--canvas-border)]\">\n <ChatBubble \n message={{\n id: \"1\",\n content: \"Hey! How's the project going?\",\n sender: \"Jeff Connor\",\n timestamp: \"2:30 PM\",\n isOwn: false,\n }}\n />\n </div>\n ),\n\n // =====================\n // BLOCKS - Video\n // =====================\n VideoChatControls: () => (\n <div className=\"w-[400px] bg-[var(--canvas-text)] rounded-lg p-4 shadow-sm\">\n <VideoChatControls />\n </div>\n ),\n WebcamPreview: () => (\n <div className=\"w-[320px] h-[240px] overflow-hidden rounded-lg shadow-sm border border-[var(--canvas-border)]\">\n <WebcamPreview />\n </div>\n ),\n ParticipantList: () => (\n <div className=\"w-[280px] bg-white rounded-lg p-4 shadow-sm border border-[var(--canvas-border)]\">\n <ParticipantList />\n </div>\n ),\n VideoContentSection: () => (\n <div className=\"w-[600px] bg-white rounded-lg p-4 shadow-sm border border-[var(--canvas-border)]\">\n <VideoContentSection />\n </div>\n ),\n VideoPlaylist: () => (\n <div className=\"w-[320px] bg-white rounded-lg p-4 shadow-sm border border-[var(--canvas-border)]\">\n <VideoPlaylistCard />\n </div>\n ),\n\n // =====================\n // BLOCKS - Search & Filters\n // =====================\n SearchBar: () => (\n <div className=\"w-[400px] bg-white rounded-lg p-4 shadow-sm border border-[var(--canvas-border)]\">\n <SearchBar placeholder=\"Search...\" />\n </div>\n ),\n FilterPopover: () => (\n <div className=\"w-[300px] bg-white rounded-lg p-4 shadow-sm border border-[var(--canvas-border)]\">\n <FilterPopover />\n </div>\n ),\n\n // =====================\n // BLOCKS - Forms & Settings\n // =====================\n SettingsListRow: () => (\n <div className=\"w-[400px] bg-white rounded-lg p-4 shadow-sm border border-[var(--canvas-border)]\">\n <SettingsListRow label=\"Email\" value=\"jeff@example.com\" />\n </div>\n ),\n ProfileImageUploader: () => (\n <div className=\"w-[200px] bg-white rounded-lg p-4 shadow-sm border border-[var(--canvas-border)]\">\n <ProfileImageUploader currentImage={sampleProfileData.avatarUrl} />\n </div>\n ),\n LoginBrandingPanel: () => (\n <div className=\"w-[400px] h-[300px] overflow-hidden rounded-lg shadow-sm\">\n <LoginBrandingPanel />\n </div>\n ),\n\n // =====================\n // BLOCKS - Marketing Heroes\n // =====================\n HeroSection: () => (\n <div className=\"w-[600px] overflow-hidden rounded-lg shadow-sm\">\n <HeroSection \n title=\"Plan your next adventure\" \n subtitle=\"Live like locals from anywhere\"\n />\n </div>\n ),\n HeroDarkWithImage: () => (\n <div className=\"w-[700px] overflow-hidden rounded-lg shadow-sm\">\n <HeroDarkWithImage />\n </div>\n ),\n CenteredHero: () => (\n <div className=\"w-[600px] overflow-hidden rounded-lg shadow-sm\">\n <CenteredHero \n title=\"Build something amazing\"\n subtitle=\"The platform for modern developers\"\n />\n </div>\n ),\n\n // =====================\n // BLOCKS - Marketing Social Proof\n // =====================\n TestimonialCarousel: () => (\n <div className=\"w-[700px] overflow-hidden rounded-lg shadow-sm\">\n <TestimonialCarousel />\n </div>\n ),\n ReviewsGrid: () => (\n <div className=\"w-[800px] overflow-hidden rounded-lg shadow-sm bg-white p-6\">\n <ReviewsGrid />\n </div>\n ),\n SocialProof: () => (\n <div className=\"w-[600px] overflow-hidden rounded-lg shadow-sm bg-white\">\n <SocialProof label=\"TRUSTED BY\" />\n </div>\n ),\n MetricsSection: () => (\n <div className=\"w-[700px] overflow-hidden rounded-lg shadow-sm bg-white p-6\">\n <MetricsSection />\n </div>\n ),\n\n // =====================\n // BLOCKS - Marketing Features\n // =====================\n FeatureWithImage: () => (\n <div className=\"w-[800px] overflow-hidden rounded-lg shadow-sm bg-white p-6\">\n <FeatureWithImage \n title=\"Powerful analytics\"\n description=\"Get insights into your data with our advanced analytics tools.\"\n />\n </div>\n ),\n CoreValuesGrid: () => (\n <div className=\"w-[800px] overflow-hidden rounded-lg shadow-sm bg-white p-6\">\n <CoreValuesGrid />\n </div>\n ),\n DestinationCards: () => (\n <div className=\"w-[800px] overflow-hidden rounded-lg shadow-sm bg-white p-6\">\n <DestinationCards />\n </div>\n ),\n\n // =====================\n // BLOCKS - Marketing Team\n // =====================\n TeamCardsGrid: () => (\n <div className=\"w-[800px] overflow-hidden rounded-lg shadow-sm bg-white p-6\">\n <TeamCardsGrid />\n </div>\n ),\n TeamCircularGrid: () => (\n <div className=\"w-[800px] overflow-hidden rounded-lg shadow-sm bg-white p-6\">\n <TeamCircularGrid />\n </div>\n ),\n\n // =====================\n // BLOCKS - Marketing CTA & Footer\n // =====================\n CtaBanner: () => (\n <div className=\"w-[500px] overflow-hidden rounded-lg shadow-sm\">\n <CtaBanner title=\"Ready to get started?\" buttonText=\"Sign up\" />\n </div>\n ),\n FooterNavbar: () => (\n <div className=\"w-[800px] overflow-hidden rounded-lg shadow-sm\">\n <FooterNavbar />\n </div>\n ),\n\n // =====================\n // BLOCKS - Marketing Other\n // =====================\n FeaturedNewsCards: () => (\n <div className=\"w-[800px] overflow-hidden rounded-lg shadow-sm bg-white p-6\">\n <FeaturedNewsCards />\n </div>\n ),\n OfficeLocations: () => (\n <div className=\"w-[800px] overflow-hidden rounded-lg shadow-sm bg-white p-6\">\n <OfficeLocations />\n </div>\n ),\n\n // =====================\n // BLOCKS - Pricing\n // =====================\n PricingCards: () => (\n <div className=\"w-[900px] overflow-hidden rounded-lg shadow-sm\">\n <PricingCards />\n </div>\n ),\n FaqAccordion: () => (\n <div className=\"w-[600px] overflow-hidden rounded-lg shadow-sm bg-white p-6\">\n <FaqAccordion />\n </div>\n ),\n FeaturesComparison: () => (\n <div className=\"w-[800px] overflow-hidden rounded-lg shadow-sm bg-white p-6\">\n <FeaturesComparison />\n </div>\n ),\n\n // =====================\n // UI COMPONENTS\n // =====================\n Button: () => (\n <div className=\"w-[200px] bg-white rounded-lg p-6 shadow-sm border border-[var(--canvas-border)] flex flex-col gap-3\">\n <Button>Primary Button</Button>\n <Button variant=\"outline\">Outline Button</Button>\n <Button variant=\"ghost\">Ghost Button</Button>\n </div>\n ),\n Checkbox: () => (\n <div className=\"w-[200px] bg-white rounded-lg p-6 shadow-sm border border-[var(--canvas-border)]\">\n <div className=\"flex items-center space-x-2\">\n <Checkbox id=\"terms\" defaultChecked />\n <Label htmlFor=\"terms\">Accept terms</Label>\n </div>\n </div>\n ),\n DateInput: () => (\n <div className=\"w-[250px] bg-white rounded-lg p-6 shadow-sm border border-[var(--canvas-border)]\">\n <Input type=\"date\" />\n </div>\n ),\n Input: () => (\n <div className=\"w-[300px] bg-white rounded-lg p-6 shadow-sm border border-[var(--canvas-border)]\">\n <Input placeholder=\"Enter your email...\" />\n </div>\n ),\n Select: () => (\n <div className=\"w-[250px] bg-white rounded-lg p-6 shadow-sm border border-[var(--canvas-border)]\">\n <Select>\n <SelectTrigger>\n <SelectValue placeholder=\"Select option\" />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"1\">Option 1</SelectItem>\n <SelectItem value=\"2\">Option 2</SelectItem>\n <SelectItem value=\"3\">Option 3</SelectItem>\n </SelectContent>\n </Select>\n </div>\n ),\n Switch: () => (\n <div className=\"w-[200px] bg-white rounded-lg p-6 shadow-sm border border-[var(--canvas-border)]\">\n <div className=\"flex items-center space-x-2\">\n <Switch id=\"notifications\" defaultChecked />\n <Label htmlFor=\"notifications\">Notifications</Label>\n </div>\n </div>\n ),\n RadioGroup: () => (\n <div className=\"w-[200px] bg-white rounded-lg p-6 shadow-sm border border-[var(--canvas-border)]\">\n <RadioGroup defaultValue=\"option-1\">\n <div className=\"flex items-center space-x-2\">\n <RadioGroupItem value=\"option-1\" id=\"option-1\" />\n <Label htmlFor=\"option-1\">Option 1</Label>\n </div>\n <div className=\"flex items-center space-x-2\">\n <RadioGroupItem value=\"option-2\" id=\"option-2\" />\n <Label htmlFor=\"option-2\">Option 2</Label>\n </div>\n </RadioGroup>\n </div>\n ),\n MultiselectTags: () => (\n <div className=\"w-[300px] bg-white rounded-lg p-6 shadow-sm border border-[var(--canvas-border)]\">\n <div className=\"flex flex-wrap gap-2\">\n <span className=\"px-2 py-1 bg-[var(--canvas-primary)] text-white text-xs rounded-full\">Design</span>\n <span className=\"px-2 py-1 bg-[var(--canvas-primary)] text-white text-xs rounded-full\">Development</span>\n <span className=\"px-2 py-1 border border-[var(--canvas-border)] text-[var(--canvas-text-muted)] text-xs rounded-full\">+ Add tag</span>\n </div>\n </div>\n ),\n Avatar: () => (\n <div className=\"w-[200px] bg-white rounded-lg p-6 shadow-sm border border-[var(--canvas-border)] flex gap-3\">\n <Avatar>\n <AvatarImage src={sampleProfileData.avatarUrl} />\n <AvatarFallback>JC</AvatarFallback>\n </Avatar>\n <Avatar>\n <AvatarFallback>AB</AvatarFallback>\n </Avatar>\n </div>\n ),\n Badge: () => (\n <div className=\"w-[250px] bg-white rounded-lg p-6 shadow-sm border border-[var(--canvas-border)] flex flex-wrap gap-2\">\n <span className=\"px-2 py-1 bg-[var(--canvas-primary)] text-white text-xs rounded-full\">Default</span>\n <span className=\"px-2 py-1 bg-[var(--canvas-surface)] text-[var(--canvas-text)] text-xs rounded-full\">Secondary</span>\n <span className=\"px-2 py-1 border border-[var(--canvas-border)] text-[var(--canvas-text-muted)] text-xs rounded-full\">Outline</span>\n <span className=\"px-2 py-1 bg-red-500 text-white text-xs rounded-full\">Destructive</span>\n </div>\n ),\n};\n\n// =====================\n// InfinityCanvas Component\n// =====================\ninterface InfinityCanvasProps {\n items: CanvasItemData[];\n onItemsChange: (items: CanvasItemData[]) => void;\n selectedId: string | null;\n onSelectItem: (id: string | null) => void;\n}\n\n/**\n * Infinity Canvas - Pannable, zoomable canvas for placing components\n * \n * Features:\n * - Pan by dragging empty space\n * - Zoom with scroll wheel or controls\n * - Drop zone for components from palette\n * - Grid background for visual reference\n */\nexport function InfinityCanvas({\n items,\n onItemsChange,\n selectedId,\n onSelectItem,\n}: InfinityCanvasProps) {\n const [showGrid, setShowGrid] = useState(true);\n const transformRef = useRef<ReactZoomPanPinchRef>(null);\n const [scale, setScale] = useState(1);\n \n // Drag state - stores starting positions\n const dragStateRef = useRef<{\n itemId: string;\n startMouseX: number;\n startMouseY: number;\n startItemX: number;\n startItemY: number;\n } | null>(null);\n const [isDragging, setIsDragging] = useState(false);\n\n // Set up droppable area\n const { setNodeRef, isOver } = useDroppable({\n id: \"canvas-drop-zone\",\n });\n\n // Handle item deletion\n const handleDeleteItem = useCallback((id: string) => {\n onItemsChange(items.filter(item => item.id !== id));\n if (selectedId === id) {\n onSelectItem(null);\n }\n }, [items, onItemsChange, selectedId, onSelectItem]);\n\n // Handle item drag start - receives mouse position and item position\n const handleDragStart = useCallback((id: string, startMouseX: number, startMouseY: number, startItemX: number, startItemY: number) => {\n dragStateRef.current = {\n itemId: id,\n startMouseX,\n startMouseY,\n startItemX,\n startItemY,\n };\n setIsDragging(true);\n }, []);\n\n // Handle mouse move for dragging\n useEffect(() => {\n if (!isDragging) return;\n\n const handleMouseMove = (e: MouseEvent) => {\n const dragState = dragStateRef.current;\n if (!dragState || !transformRef.current) return;\n\n const state = transformRef.current.instance.transformState;\n \n // Calculate delta from start position, accounting for zoom\n const deltaX = (e.clientX - dragState.startMouseX) / state.scale;\n const deltaY = (e.clientY - dragState.startMouseY) / state.scale;\n \n // New position = original position + delta\n const newX = dragState.startItemX + deltaX;\n const newY = dragState.startItemY + deltaY;\n\n onItemsChange(items.map(item => \n item.id === dragState.itemId \n ? { ...item, x: Math.max(0, newX), y: Math.max(0, newY) }\n : item\n ));\n };\n\n const handleMouseUp = () => {\n dragStateRef.current = null;\n setIsDragging(false);\n };\n\n window.addEventListener(\"mousemove\", handleMouseMove);\n window.addEventListener(\"mouseup\", handleMouseUp);\n\n return () => {\n window.removeEventListener(\"mousemove\", handleMouseMove);\n window.removeEventListener(\"mouseup\", handleMouseUp);\n };\n }, [isDragging, items, onItemsChange]);\n\n // Handle keyboard shortcuts\n useEffect(() => {\n const handleKeyDown = (e: KeyboardEvent) => {\n if (e.key === \"Delete\" || e.key === \"Backspace\") {\n if (selectedId && document.activeElement?.tagName !== \"INPUT\") {\n handleDeleteItem(selectedId);\n }\n }\n if (e.key === \"Escape\") {\n onSelectItem(null);\n }\n };\n\n window.addEventListener(\"keydown\", handleKeyDown);\n return () => window.removeEventListener(\"keydown\", handleKeyDown);\n }, [selectedId, handleDeleteItem, onSelectItem]);\n\n // Click on empty canvas to deselect\n const handleCanvasClick = () => {\n onSelectItem(null);\n };\n\n return (\n <div \n ref={setNodeRef}\n className={cn(\n \"relative flex-1 overflow-hidden\",\n \"bg-[#f8f9fa]\",\n isOver && \"ring-2 ring-inset ring-[var(--canvas-primary)]\"\n )}\n >\n {/* Zoom Controls */}\n <div className=\"absolute top-4 right-4 z-20 flex items-center gap-2\">\n <button\n onClick={() => setShowGrid(!showGrid)}\n className={cn(\n \"p-2 rounded-md border shadow-sm transition-colors\",\n showGrid \n ? \"bg-[var(--canvas-primary)] text-white border-[var(--canvas-primary)]\" \n : \"bg-white text-[var(--canvas-text-muted)] border-[var(--canvas-border)] hover:bg-[var(--canvas-surface)]\"\n )}\n aria-label=\"Toggle grid\"\n >\n <Grid3X3 className=\"size-4\" />\n </button>\n <div className=\"flex items-center bg-white rounded-md border border-[var(--canvas-border)] shadow-sm\">\n <button\n onClick={() => transformRef.current?.zoomOut()}\n className=\"p-2 hover:bg-[var(--canvas-surface)] rounded-l-md transition-colors\"\n aria-label=\"Zoom out\"\n >\n <ZoomOut className=\"size-4 text-[var(--canvas-text-muted)]\" />\n </button>\n <span className=\"px-3 text-sm text-[var(--canvas-text-muted)] border-x border-[var(--canvas-border)] min-w-[60px] text-center\">\n {Math.round(scale * 100)}%\n </span>\n <button\n onClick={() => transformRef.current?.zoomIn()}\n className=\"p-2 hover:bg-[var(--canvas-surface)] transition-colors\"\n aria-label=\"Zoom in\"\n >\n <ZoomIn className=\"size-4 text-[var(--canvas-text-muted)]\" />\n </button>\n <button\n onClick={() => transformRef.current?.resetTransform()}\n className=\"p-2 hover:bg-[var(--canvas-surface)] rounded-r-md transition-colors border-l border-[var(--canvas-border)]\"\n aria-label=\"Reset view\"\n >\n <Maximize2 className=\"size-4 text-[var(--canvas-text-muted)]\" />\n </button>\n </div>\n </div>\n\n {/* Canvas */}\n <TransformWrapper\n ref={transformRef}\n initialScale={1}\n minScale={0.1}\n maxScale={2}\n limitToBounds={false}\n onTransformed={(_, state) => setScale(state.scale)}\n panning={{\n disabled: isDragging,\n velocityDisabled: true,\n }}\n wheel={{\n smoothStep: 0.05,\n }}\n >\n <TransformComponent\n wrapperStyle={{\n width: \"100%\",\n height: \"100%\",\n }}\n contentStyle={{\n width: \"5000px\",\n height: \"5000px\",\n }}\n >\n <div\n className={cn(\n \"w-full h-full relative\",\n showGrid && \"bg-[url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAiIGhlaWdodD0iNDAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PHBhdHRlcm4gaWQ9ImdyaWQiIHdpZHRoPSI0MCIgaGVpZ2h0PSI0MCIgcGF0dGVyblVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+PHBhdGggZD0iTSAwIDEwIEwgNDAgMTAgTSAxMCAwIEwgMTAgNDAgTSAwIDIwIEwgNDAgMjAgTSAyMCAwIEwgMjAgNDAgTSAwIDMwIEwgNDAgMzAgTSAzMCAwIEwgMzAgNDAiIGZpbGw9Im5vbmUiIHN0cm9rZT0iI2UwZTBlMCIgc3Ryb2tlLXdpZHRoPSIxIi8+PHBhdGggZD0iTSA0MCAwIEwgMCA0MCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSJub25lIi8+PC9wYXR0ZXJuPjwvZGVmcz48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSJ1cmwoI2dyaWQpIi8+PC9zdmc+')]\"\n )}\n onClick={handleCanvasClick}\n >\n {/* Render canvas items */}\n {items.map((item) => {\n const renderer = componentRenderers[item.componentType];\n if (!renderer) return null;\n\n return (\n <CanvasItem\n key={item.id}\n item={item}\n isSelected={selectedId === item.id}\n onSelect={onSelectItem}\n onDelete={handleDeleteItem}\n onDragStart={handleDragStart}\n scale={scale}\n >\n {renderer()}\n </CanvasItem>\n );\n })}\n\n {/* Empty state hint */}\n {items.length === 0 && (\n <div className=\"absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-center pointer-events-none\">\n <p className=\"text-[var(--canvas-text-muted)] text-lg\">\n Drag components from the sidebar to get started\n </p>\n <p className=\"text-[var(--canvas-text-placeholder)] text-sm mt-2\">\n Scroll to zoom • Drag to pan\n </p>\n </div>\n )}\n </div>\n </TransformComponent>\n </TransformWrapper>\n </div>\n );\n}\n"
10
+ }
11
+ ],
12
+ "dependencies": [
13
+ "react-zoom-pan-pinch",
14
+ "@dnd-kit/core",
15
+ "lucide-react"
16
+ ],
17
+ "registryDependencies": [
18
+ "utils",
19
+ "canvas-item",
20
+ "profile-card",
21
+ "standard-data-table",
22
+ "step-tracker",
23
+ "vertical-step-tracker",
24
+ "progress-bar",
25
+ "flair-banner",
26
+ "gradient-banner",
27
+ "messenger-sidebar",
28
+ "pill-tabs",
29
+ "chat-message",
30
+ "video-chat-controls",
31
+ "webcam-preview",
32
+ "participant-list",
33
+ "video-content-section",
34
+ "video-playlist",
35
+ "search-bar",
36
+ "filter-popover",
37
+ "settings-list-row",
38
+ "profile-image-uploader",
39
+ "login-branding-panel",
40
+ "sidebar-profile-card",
41
+ "profile-info-cards",
42
+ "sidebar-cards",
43
+ "credit-card-display",
44
+ "page-header-section",
45
+ "mobile-bottom-nav",
46
+ "marketing",
47
+ "pricing",
48
+ "page-previews",
49
+ "button",
50
+ "checkbox",
51
+ "input",
52
+ "switch",
53
+ "avatar",
54
+ "radio-group",
55
+ "label",
56
+ "select"
57
+ ]
58
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "large-image-labels-list",
3
+ "type": "registry:block",
4
+ "description": "Property-style listing with large images, ratings, prices, and icon labels. Includes header with sort/filter controls.",
5
+ "files": [
6
+ {
7
+ "path": "components/blocks/large-image-labels-list.tsx",
8
+ "type": "registry:block",
9
+ "content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from \"../ui/select\";\nimport { Heart, Star, MapPin, Users, Zap, Briefcase } from \"lucide-react\";\n\n// ============================================\n// Types\n// ============================================\n\nexport interface IconLabelConfig {\n id: string;\n icon: \"heart\" | \"users\" | \"zap\" | \"map-pin\" | \"briefcase\";\n label: string;\n}\n\nexport interface ListItem {\n id: string;\n imageUrl: string;\n title: string;\n price: string;\n priceUnit?: string;\n rating: number;\n reviewCount: number;\n location: string;\n description: string;\n labels: IconLabelConfig[];\n tag?: string;\n isFavorite?: boolean;\n}\n\nexport interface SortOption {\n id: string;\n label: string;\n}\n\nexport interface FilterOption {\n id: string;\n label: string;\n}\n\nexport interface LargeImageLabelsListProps {\n /** Section title */\n title?: string;\n /** Number of results (defaults to items.length) */\n resultCount?: number;\n /** Custom result count text (overrides default \"{count} results\") */\n resultCountText?: string;\n /** List items to display */\n items?: ListItem[];\n /** Sort options for the sort dropdown */\n sortOptions?: SortOption[];\n /** Filter options for the filter dropdown */\n filterOptions?: FilterOption[];\n /** Callback when sort value changes */\n onSort?: (value: string) => void;\n /** Callback when filter value changes */\n onFilter?: (value: string) => void;\n /** Callback when favorite button is clicked */\n onFavorite?: (item: ListItem) => void;\n /** Callback when 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 imageUrl: \"https://images.unsplash.com/photo-1502672260266-1c1ef2d93688?w=800&h=600&fit=crop\",\n title: \"Waikiki Beach Apartment\",\n price: \"$140\",\n priceUnit: \"/night\",\n rating: 5,\n reviewCount: 210,\n location: \"Honolulu, HI\",\n description: \"Stylish apartment near Waikiki beach that offers the perfect blend of comfort and convenience for your Hawaiian escape\",\n labels: [\n { id: \"1\", icon: \"heart\", label: \"Popular\" },\n { id: \"2\", icon: \"users\", label: \"Perfect for families\" },\n { id: \"3\", icon: \"zap\", label: \"Instant booking\" },\n ],\n tag: \"Popular\",\n isFavorite: false,\n },\n {\n id: \"2\",\n imageUrl: \"https://images.unsplash.com/photo-1560448204-e02f11c3d0e2?w=800&h=600&fit=crop\",\n title: \"Central Danish Apartment\",\n price: \"$180\",\n priceUnit: \"/night\",\n rating: 5,\n reviewCount: 98,\n location: \"Copenhagen, Denmark\",\n description: \"A centrally located haven nestled in the heart of the city of Copenhagen, offering unparalleled convenience and accessibility\",\n labels: [\n { id: \"1\", icon: \"map-pin\", label: \"Central\" },\n { id: \"2\", icon: \"briefcase\", label: \"Business travelers\" },\n { id: \"3\", icon: \"zap\", label: \"Instant booking\" },\n ],\n isFavorite: false,\n },\n];\n\nconst defaultSortOptions: SortOption[] = [\n { id: \"price-low\", label: \"Price (Low to High)\" },\n { id: \"price-high\", label: \"Price (High to Low)\" },\n { id: \"rating\", label: \"Highest Rated\" },\n { id: \"newest\", label: \"Newest\" },\n];\n\nconst defaultFilterOptions: FilterOption[] = [\n { id: \"all\", label: \"All Properties\" },\n { id: \"instant\", label: \"Instant Booking\" },\n { id: \"popular\", label: \"Popular\" },\n { id: \"family\", label: \"Family Friendly\" },\n];\n\n// ============================================\n// Sub-components\n// ============================================\n\nfunction StarRating({ rating, reviewCount }: { rating: number; reviewCount: number }) {\n return (\n <div \n className=\"flex items-center\"\n style={{ gap: \"var(--spacing-xxs, 2px)\" }}\n >\n {[1, 2, 3, 4, 5].map((star) => (\n <Star\n key={star}\n className=\"size-5\"\n fill={star <= rating ? \"var(--canvas-primary)\" : \"var(--canvas-border)\"}\n stroke={star <= rating ? \"var(--canvas-primary)\" : \"var(--canvas-border)\"}\n />\n ))}\n <span\n style={{\n fontFamily: \"var(--typo-body-xs-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-xs-size)\",\n fontWeight: \"var(--typo-body-xs-weight)\",\n lineHeight: \"var(--typo-body-xs-line-height)\",\n color: \"var(--canvas-text-muted)\",\n paddingLeft: \"var(--spacing-sm)\",\n }}\n >\n {reviewCount}\n </span>\n </div>\n );\n}\n\nfunction IconLabel({ icon, label }: { icon: IconLabelConfig[\"icon\"]; label: string }) {\n const iconMap = {\n heart: Heart,\n users: Users,\n zap: Zap,\n \"map-pin\": MapPin,\n briefcase: Briefcase,\n };\n const IconComponent = iconMap[icon];\n\n return (\n <div\n className=\"flex items-center\"\n style={{ gap: \"var(--spacing-sm)\" }}\n >\n <IconComponent\n className=\"size-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: \"var(--typo-body-s-weight)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n {label}\n </span>\n </div>\n );\n}\n\ninterface ListItemCardProps {\n item: ListItem;\n onFavorite?: (item: ListItem) => void;\n onClick?: (item: ListItem) => void;\n}\n\nfunction ListItemCard({ item, onFavorite, onClick }: ListItemCardProps) {\n const [isFavorite, setIsFavorite] = useState(item.isFavorite ?? false);\n\n const handleFavorite = (e: React.MouseEvent) => {\n e.stopPropagation();\n setIsFavorite(!isFavorite);\n onFavorite?.(item);\n };\n\n return (\n <div\n className=\"flex flex-col md:flex-row w-full cursor-pointer\"\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?.(item)}\n >\n {/* Image Section */}\n <div className=\"relative md:flex-1 aspect-[4/3] md:aspect-auto md:min-h-[200px]\">\n <img\n src={item.imageUrl}\n alt={item.title}\n className=\"w-full h-full object-cover\"\n style={{\n borderRadius: \"var(--radius-md)\",\n border: \"1px solid var(--canvas-border)\",\n }}\n />\n {/* Favorite Button */}\n <button\n onClick={handleFavorite}\n className=\"absolute flex items-center justify-center\"\n style={{\n top: \"var(--spacing-md)\",\n left: \"var(--spacing-md)\",\n width: \"32px\",\n height: \"32px\",\n borderRadius: \"var(--radius-full, 24px)\",\n backgroundColor: \"var(--canvas-background)\",\n border: \"1px solid var(--canvas-text)\",\n boxShadow: \"0px 1px 8px 0px rgba(0,0,0,0.03)\",\n }}\n >\n <Heart\n className=\"size-5\"\n fill={isFavorite ? \"var(--canvas-primary)\" : \"transparent\"}\n stroke=\"var(--canvas-text)\"\n />\n </button>\n {/* Tag Badge */}\n {item.tag && (\n <div\n className=\"absolute flex items-center\"\n style={{\n top: \"var(--spacing-md)\",\n left: \"47px\",\n height: \"32px\",\n paddingLeft: \"var(--spacing-lg)\",\n paddingRight: \"var(--spacing-lg)\",\n borderRadius: \"var(--radius-xs)\",\n backgroundColor: \"var(--canvas-background)\",\n border: \"1px solid var(--canvas-text)\",\n boxShadow: \"0px 1px 8px 0px rgba(0,0,0,0.03)\",\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 500,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n {item.tag}\n </div>\n )}\n </div>\n\n {/* Content Section */}\n <div\n className=\"flex flex-col md:flex-1\"\n style={{ gap: \"var(--spacing-3xl)\" }}\n >\n {/* Header Info */}\n <div\n className=\"flex flex-col\"\n style={{ gap: \"var(--spacing-xs)\" }}\n >\n {/* Title and Price Row */}\n <div className=\"flex items-center justify-between w-full\">\n <h3\n style={{\n fontFamily: \"var(--typo-body-xl-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-xl-size)\",\n fontWeight: 600,\n lineHeight: \"var(--typo-body-xl-line-height)\",\n color: \"var(--canvas-text)\",\n margin: 0,\n }}\n >\n {item.title}\n </h3>\n <div className=\"flex items-baseline shrink-0\">\n <span\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.price}\n </span>\n {item.priceUnit && (\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.priceUnit}\n </span>\n )}\n </div>\n </div>\n\n {/* Rating */}\n <StarRating rating={item.rating} reviewCount={item.reviewCount} />\n\n {/* Location */}\n <p\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: \"var(--typo-body-s-weight)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text-muted)\",\n margin: 0,\n }}\n >\n {item.location}\n </p>\n </div>\n\n {/* Description */}\n <p\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: \"var(--typo-body-s-weight)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text-muted)\",\n margin: 0,\n }}\n >\n {item.description}\n </p>\n\n {/* Labels */}\n <div\n className=\"flex flex-col\"\n style={{ gap: \"var(--spacing-md)\" }}\n >\n {item.labels.map((labelConfig) => (\n <IconLabel\n key={labelConfig.id}\n icon={labelConfig.icon}\n label={labelConfig.label}\n />\n ))}\n </div>\n </div>\n </div>\n );\n}\n\n// ============================================\n// Main Component\n// ============================================\n\n/**\n * Canvas Design System - Large Image Labels List Block\n * \n * A property-style listing component with large images, ratings, prices,\n * and icon labels. Includes a header section with title, result count,\n * and sort/filter controls.\n * \n * @example\n * ```tsx\n * <LargeImageLabelsList\n * title=\"Properties\"\n * items={[\n * {\n * id: \"1\",\n * imageUrl: \"https://example.com/image.jpg\",\n * title: \"Beach Apartment\",\n * price: \"$140\",\n * priceUnit: \"/night\",\n * rating: 5,\n * reviewCount: 210,\n * location: \"Honolulu, HI\",\n * description: \"Beautiful beachfront property...\",\n * labels: [{ id: \"1\", icon: \"heart\", label: \"Popular\" }],\n * tag: \"Popular\",\n * }\n * ]}\n * onFavorite={(item) => console.log(\"Favorited:\", item.title)}\n * />\n * ```\n */\nexport function LargeImageLabelsList({\n title = \"Properties\",\n resultCount,\n resultCountText,\n items = defaultItems,\n sortOptions = defaultSortOptions,\n filterOptions = defaultFilterOptions,\n onSort,\n onFilter,\n onFavorite,\n onItemClick,\n className,\n}: LargeImageLabelsListProps) {\n const [sortValue, setSortValue] = useState<string>(\"\");\n const [filterValue, setFilterValue] = useState<string>(\"\");\n\n const displayResultCount = resultCount ?? items.length;\n const displayResultText = resultCountText ?? `${displayResultCount} results`;\n\n const handleSortChange = (value: string) => {\n setSortValue(value);\n onSort?.(value);\n };\n\n const handleFilterChange = (value: string) => {\n setFilterValue(value);\n onFilter?.(value);\n };\n\n return (\n <div \n className={cn(\"flex flex-col w-full\", className)}\n style={{ gap: \"var(--spacing-xl)\" }}\n >\n {/* Header Section */}\n <div \n className=\"flex flex-wrap items-start w-full\"\n style={{ gap: \"var(--spacing-xl)\" }}\n >\n {/* Title and Count */}\n <div \n className=\"flex flex-col flex-1 min-w-[200px]\"\n style={{ gap: \"var(--spacing-xs)\" }}\n >\n <h2\n style={{\n fontFamily: \"var(--typo-h6-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-h6-size)\",\n fontWeight: \"var(--typo-h6-weight)\",\n letterSpacing: \"var(--typo-h6-spacing)\",\n lineHeight: \"var(--typo-h6-line-height)\",\n color: \"var(--canvas-text)\",\n margin: 0,\n }}\n >\n {title}\n </h2>\n <p\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: \"var(--typo-body-s-weight)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text-muted)\",\n margin: 0,\n }}\n >\n {displayResultText}\n </p>\n </div>\n\n {/* Controls */}\n <div \n className=\"flex items-start justify-end shrink-0 gap-3\"\n >\n {/* Sort Dropdown */}\n <div className=\"w-[120px]\">\n <Select value={sortValue || undefined} onValueChange={handleSortChange}>\n <SelectTrigger inputSize=\"sm\">\n <SelectValue placeholder=\"Sort\" />\n </SelectTrigger>\n <SelectContent>\n {sortOptions.map((option) => (\n <SelectItem key={option.id} value={option.id}>\n {option.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n\n {/* Filter Dropdown */}\n <div className=\"w-[120px]\">\n <Select value={filterValue || undefined} onValueChange={handleFilterChange}>\n <SelectTrigger inputSize=\"sm\">\n <SelectValue placeholder=\"Filter\" />\n </SelectTrigger>\n <SelectContent>\n {filterOptions.map((option) => (\n <SelectItem key={option.id} value={option.id}>\n {option.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n </div>\n </div>\n\n {/* List Section */}\n <div \n className=\"flex flex-col w-full\"\n style={{ borderTop: \"1px solid var(--canvas-border)\" }}\n >\n {items.map((item) => (\n <ListItemCard\n key={item.id}\n item={item}\n onFavorite={onFavorite}\n onClick={onItemClick}\n />\n ))}\n </div>\n </div>\n );\n}\n"
10
+ }
11
+ ],
12
+ "dependencies": [
13
+ "lucide-react"
14
+ ],
15
+ "registryDependencies": [
16
+ "utils",
17
+ "select"
18
+ ]
19
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "loader",
3
+ "type": "registry:block",
4
+ "description": "Loading feedback component with animated spinner (loading state) and checkmark (success state). Includes title, description, and optional action button.",
5
+ "files": [
6
+ {
7
+ "path": "components/blocks/loader.tsx",
8
+ "type": "registry:block",
9
+ "content": "\"use client\";\n\nimport { cn } from \"../../lib/utils\";\nimport { Check } from \"lucide-react\";\nimport { Button } from \"../ui/button\";\n\n// ============================================\n// Loader Component Types\n// ============================================\n\nexport interface LoaderProps {\n /** The current state of the loader */\n state?: \"loading\" | \"success\";\n /** Title text displayed below the icon */\n title?: string;\n /** Description text displayed below the title */\n description?: string;\n /** Text for the action button (success state only) */\n buttonText?: string;\n /** Callback when the action button is clicked */\n onButtonClick?: () => void;\n /** Additional class name */\n className?: string;\n}\n\n// ============================================\n// Animated Spinner Component\n// ============================================\n\nfunction LoadingSpinner({ className }: { className?: string }) {\n return (\n <div\n className={cn(\"relative\", className)}\n style={{\n width: \"var(--loader-spinner-size)\",\n height: \"var(--loader-spinner-size)\",\n }}\n >\n <svg\n className=\"animate-spin\"\n viewBox=\"0 0 64 64\"\n fill=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\"\n style={{\n width: \"100%\",\n height: \"100%\",\n animation: \"spin 1s linear infinite\",\n }}\n >\n {/* Background circle */}\n <circle\n cx=\"32\"\n cy=\"32\"\n r=\"28\"\n stroke=\"var(--loader-spinner-stroke)\"\n strokeWidth=\"8\"\n fill=\"none\"\n />\n {/* Animated arc */}\n <circle\n cx=\"32\"\n cy=\"32\"\n r=\"28\"\n stroke=\"var(--loader-spinner-fill)\"\n strokeWidth=\"8\"\n fill=\"none\"\n strokeLinecap=\"round\"\n strokeDasharray=\"44 132\"\n style={{\n transformOrigin: \"center\",\n }}\n />\n </svg>\n </div>\n );\n}\n\n// ============================================\n// Success Checkmark Component\n// ============================================\n\nfunction SuccessCheckmark({ className }: { className?: string }) {\n return (\n <div\n className={cn(\n \"flex items-center justify-center rounded-full\",\n className\n )}\n style={{\n width: \"var(--loader-success-size)\",\n height: \"var(--loader-success-size)\",\n backgroundColor: \"var(--loader-success-bg)\",\n }}\n >\n <Check\n className=\"text-white\"\n style={{\n width: \"calc(var(--loader-success-size) / 2)\",\n height: \"calc(var(--loader-success-size) / 2)\",\n }}\n strokeWidth={3}\n />\n </div>\n );\n}\n\n// ============================================\n// Loader Component\n// ============================================\n\n/**\n * Canvas Design System - Loader Component\n *\n * A loading feedback component with two states:\n * - Loading: Animated spinner with title and description\n * - Success: Green checkmark with title, description, and optional action button\n *\n * Uses CSS variables for theming:\n * - --loader-spinner-size: Size of the spinner (default: 64px)\n * - --loader-spinner-stroke: Background stroke color\n * - --loader-spinner-fill: Animated arc color\n * - --loader-success-size: Size of success checkmark (default: 48px)\n * - --loader-success-bg: Background color of success circle\n * - --loader-gap: Gap between elements\n *\n * @example\n * ```tsx\n * // Loading state\n * <Loader\n * state=\"loading\"\n * title=\"Please wait...\"\n * description=\"We are processing your request\"\n * />\n *\n * // Success state with button\n * <Loader\n * state=\"success\"\n * title=\"Success!\"\n * description=\"Your payment has been processed\"\n * buttonText=\"Go to portal\"\n * onButtonClick={() => router.push('/portal')}\n * />\n * ```\n */\nexport function Loader({\n state = \"loading\",\n title = state === \"loading\" ? \"Please wait...\" : \"Success!\",\n description = state === \"loading\"\n ? \"We are currently processing your payment\"\n : \"Your payment has been processed successfully\",\n buttonText,\n onButtonClick,\n className,\n}: LoaderProps) {\n return (\n <div\n className={cn(\n \"flex flex-col items-center justify-center text-center\",\n className\n )}\n style={{\n gap: \"var(--loader-gap)\",\n }}\n >\n {/* Icon */}\n {state === \"loading\" ? <LoadingSpinner /> : <SuccessCheckmark />}\n\n {/* Title */}\n <p\n className=\"font-bold\"\n style={{\n fontFamily: \"var(--typo-body-xl-font)\",\n fontSize: \"var(--typo-body-xl-size)\",\n lineHeight: \"1.5\",\n color: \"var(--canvas-text)\",\n }}\n >\n {title}\n </p>\n\n {/* Description */}\n <p\n style={{\n fontFamily: \"var(--typo-body-s-font)\",\n fontSize: \"var(--typo-body-s-size)\",\n lineHeight: \"1.5\",\n color: \"var(--canvas-text-muted)\",\n marginTop: \"calc(var(--loader-gap) * -0.5)\",\n }}\n >\n {description}\n </p>\n\n {/* Action Button (success state only) */}\n {state === \"success\" && buttonText && (\n <div style={{ paddingTop: \"var(--spacing-3xl)\" }}>\n <Button\n variant=\"neutral\"\n onClick={onButtonClick}\n style={{\n height: \"var(--btn-standard-height)\",\n paddingLeft: \"var(--btn-standard-px)\",\n paddingRight: \"var(--btn-standard-px)\",\n fontSize: \"var(--btn-standard-font-size)\",\n fontWeight: \"var(--btn-standard-font-weight)\" as React.CSSProperties[\"fontWeight\"],\n borderRadius: \"var(--btn-standard-radius)\",\n backgroundColor: \"var(--btn-neutral-bg)\",\n color: \"var(--btn-neutral-text)\",\n borderColor: \"var(--btn-neutral-border)\",\n borderWidth: \"1px\",\n borderStyle: \"solid\",\n }}\n >\n {buttonText}\n </Button>\n </div>\n )}\n </div>\n );\n}\n"
10
+ }
11
+ ],
12
+ "dependencies": [
13
+ "lucide-react"
14
+ ],
15
+ "registryDependencies": [
16
+ "utils",
17
+ "button"
18
+ ]
19
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "login-branding-panel",
3
+ "type": "registry:block",
4
+ "description": "Branding side panel for login/signup pages with logo and background.",
5
+ "files": [
6
+ {
7
+ "path": "components/blocks/login-branding-panel.tsx",
8
+ "type": "registry:block",
9
+ "content": "\"use client\";\n\nimport { cn } from \"../../lib/utils\";\n\ninterface LoginBrandingPanelProps {\n /** Background image URL */\n backgroundImage?: string;\n /** Title text */\n title?: string;\n /** Description text */\n description?: string;\n /** Opacity of the flair color overlay (0-1, default 0.7) */\n overlayOpacity?: number;\n /** Additional class names */\n className?: string;\n}\n\n/**\n * Canvas Design System - Login Branding Panel Component\n *\n * A right-side branding panel for login/signup pages featuring:\n * - Full-height background image with semi-transparent flair color overlay\n * - Title and description text at the bottom\n *\n * The overlay uses the flair background CSS variable for live theming.\n */\nexport function LoginBrandingPanel({\n backgroundImage = \"/brand-assets/bg.jpg\",\n title = \"Title\",\n description = \"Description\",\n overlayOpacity = 0.7,\n className,\n}: LoginBrandingPanelProps) {\n return (\n <div\n className={cn(\n \"relative flex-1 flex flex-col overflow-hidden\",\n className\n )}\n >\n {/* Background Image */}\n <img\n src={backgroundImage}\n alt=\"\"\n className=\"absolute inset-0 w-full h-full object-cover pointer-events-none\"\n />\n\n {/* Flair Color Overlay */}\n <div\n className=\"absolute inset-0 bg-[var(--canvas-flair-bg)]\"\n style={{\n opacity: overlayOpacity,\n }}\n />\n\n {/* Text Content at Bottom */}\n <div className=\"relative z-10 flex flex-col justify-end h-full p-16\">\n <div className=\"space-y-1.5\">\n <p\n className=\"text-white font-bold\"\n style={{\n fontSize: \"var(--typo-body-l-size)\",\n lineHeight: \"var(--typo-body-l-line-height)\",\n }}\n >\n {title}\n </p>\n <p\n className=\"text-white/90\"\n style={{\n fontSize: \"var(--typo-body-l-size)\",\n lineHeight: \"var(--typo-body-l-line-height)\",\n fontWeight: \"var(--typo-body-l-weight)\",\n }}\n >\n {description}\n </p>\n </div>\n </div>\n </div>\n );\n}\n\n"
10
+ }
11
+ ],
12
+ "dependencies": [],
13
+ "registryDependencies": [
14
+ "utils"
15
+ ]
16
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "menu-section",
3
+ "type": "registry:block",
4
+ "description": "Section within sidebar navigation with title and items.",
5
+ "files": [
6
+ {
7
+ "path": "components/blocks/menu-section.tsx",
8
+ "type": "registry:block",
9
+ "content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport { ChevronRight } from \"lucide-react\";\n\ninterface MenuSectionItemProps {\n /** Menu item label */\n label: string;\n /** Optional children content shown when expanded */\n children?: React.ReactNode;\n /** Whether the item starts expanded */\n defaultExpanded?: boolean;\n /** Additional class names */\n className?: string;\n}\n\n/**\n * Canvas Design System - Menu Section Item Component\n * \n * An individual expandable menu item with a chevron icon that rotates when expanded.\n * \n * @example\n * ```tsx\n * <MenuSectionItem label=\"Menu tab\">\n * <p>Expanded content goes here</p>\n * </MenuSectionItem>\n * ```\n */\nexport function MenuSectionItem({\n label,\n children,\n defaultExpanded = false,\n className,\n}: MenuSectionItemProps) {\n const [expanded, setExpanded] = useState(defaultExpanded);\n\n return (\n <div className={cn(\"w-full\", className)}>\n {/* Clickable header */}\n <button\n type=\"button\"\n onClick={() => setExpanded(!expanded)}\n className=\"w-full flex items-center gap-4 py-6 text-left group\"\n >\n {/* Circular icon button with chevron */}\n <div \n className={cn(\n \"size-8 rounded-full flex items-center justify-center shrink-0\",\n \"bg-[var(--canvas-surface)] border border-[var(--canvas-border-disabled)]\",\n \"transition-colors group-hover:bg-[var(--canvas-surface-brand)]\"\n )}\n >\n <ChevronRight \n className={cn(\n \"size-5 text-[var(--canvas-text-placeholder)] transition-transform duration-200\",\n expanded && \"rotate-90\"\n )}\n />\n </div>\n \n {/* Label - Uses typography variables */}\n <span \n className=\"text-[var(--canvas-text)]\"\n style={{\n fontFamily: \"var(--typo-menu-label-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-menu-label-size)\",\n fontWeight: \"var(--typo-menu-label-weight)\",\n letterSpacing: \"var(--typo-menu-label-spacing)\",\n lineHeight: \"var(--typo-menu-label-line-height)\",\n }}\n >\n {label}\n </span>\n </button>\n\n {/* Expandable content */}\n {children && (\n <div\n className={cn(\n \"overflow-hidden transition-all duration-200 ease-in-out\",\n expanded ? \"max-h-[1000px] opacity-100\" : \"max-h-0 opacity-0\"\n )}\n >\n <div className=\"pl-12 pb-6\">\n {children}\n </div>\n </div>\n )}\n </div>\n );\n}\n\ninterface MenuSectionProps {\n /** Array of menu items */\n items: Array<{\n id: string;\n label: string;\n children?: React.ReactNode;\n defaultExpanded?: boolean;\n }>;\n /** Additional class names */\n className?: string;\n}\n\n/**\n * Canvas Design System - Menu Section Component\n * \n * A group of expandable menu items with border separators.\n * \n * @example\n * ```tsx\n * <MenuSection\n * items={[\n * { id: \"1\", label: \"Menu tab\" },\n * { id: \"2\", label: \"Another menu tab\" },\n * ]}\n * />\n * ```\n */\nexport function MenuSection({\n items,\n className,\n}: MenuSectionProps) {\n return (\n <div className={cn(\"w-full\", className)}>\n {items.map((item, index) => (\n <div\n key={item.id}\n className={cn(\n \"border-[var(--canvas-border)]\",\n // All items get bottom border only\n \"border-b\"\n )}\n >\n <MenuSectionItem\n label={item.label}\n defaultExpanded={item.defaultExpanded}\n >\n {item.children}\n </MenuSectionItem>\n </div>\n ))}\n </div>\n );\n}\n\ninterface SectionHeaderProps {\n /** Section title */\n title: string;\n /** Section description */\n description?: string;\n /** Additional class names */\n className?: string;\n}\n\n/**\n * Canvas Design System - Section Header Component\n * \n * A simple title and description header for content sections.\n * \n * @example\n * ```tsx\n * <SectionHeader title=\"Title\" description=\"Description\" />\n * ```\n */\nexport function SectionHeader({\n title,\n description,\n className,\n}: SectionHeaderProps) {\n return (\n <div className={cn(\"flex flex-col gap-1\", className)}>\n {/* Title - 24px semibold */}\n <h3 className=\"text-2xl font-semibold text-[var(--canvas-text)] leading-[30px]\">\n {title}\n </h3>\n {/* Description - 16px regular */}\n {description && (\n <p className=\"text-base font-normal text-[var(--canvas-text-muted)] leading-6\">\n {description}\n </p>\n )}\n </div>\n );\n}\n\n"
10
+ }
11
+ ],
12
+ "dependencies": [
13
+ "lucide-react"
14
+ ],
15
+ "registryDependencies": [
16
+ "utils"
17
+ ]
18
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "menufocus-template",
3
+ "type": "registry:block",
4
+ "description": "Dropdown menu with trigger button (three dots). Used for row actions in tables.",
5
+ "files": [
6
+ {
7
+ "path": "components/blocks/menufocus-template.tsx",
8
+ "type": "registry:block",
9
+ "content": "\"use client\";\n\nimport { MoreVertical } from \"lucide-react\";\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuTrigger,\n} from \"../ui/dropdown-menu\";\nimport { cn } from \"../../lib/utils\";\n\n// ============================================\n// Types\n// ============================================\n\nexport interface MenufocusItem {\n /** Unique identifier for the menu item */\n id: string;\n /** Display label for the menu item */\n label: string;\n /** Callback when item is clicked */\n onClick?: () => void;\n /** Visual variant - use \"destructive\" for delete actions */\n variant?: \"default\" | \"destructive\";\n /** Whether the item is disabled */\n disabled?: boolean;\n}\n\nexport interface MenufocusTemplateProps {\n /** Array of menu items to display */\n items?: MenufocusItem[];\n /** Accessible label for the trigger button */\n ariaLabel?: string;\n /** Size of the trigger button */\n size?: \"sm\" | \"default\";\n /** Additional class names for the trigger button */\n className?: string;\n /** Alignment of the dropdown menu */\n align?: \"start\" | \"center\" | \"end\";\n}\n\n// ============================================\n// Default Items\n// ============================================\n\nconst defaultItems: MenufocusItem[] = [\n { id: \"edit\", label: \"Edit\" },\n { id: \"view\", label: \"View details\" },\n { id: \"delete\", label: \"Delete\", variant: \"destructive\" },\n];\n\n// ============================================\n// Component\n// ============================================\n\n/**\n * Canvas Design System - Menufocus Template\n * \n * A reusable ellipsis menu component with configurable menu items.\n * Commonly used for row actions in tables, card actions, or any\n * context where a compact action menu is needed.\n * \n * @example\n * ```tsx\n * <MenufocusTemplate\n * items={[\n * { id: \"edit\", label: \"Edit\", onClick: handleEdit },\n * { id: \"delete\", label: \"Delete\", variant: \"destructive\", onClick: handleDelete },\n * ]}\n * />\n * ```\n */\nexport function MenufocusTemplate({\n items = defaultItems,\n ariaLabel = \"Actions menu\",\n size = \"default\",\n className,\n align = \"end\",\n}: MenufocusTemplateProps) {\n const buttonSize = size === \"sm\" ? \"size-7\" : \"size-8\";\n const iconSize = size === \"sm\" ? \"size-4\" : \"size-5\";\n\n return (\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <button\n className={cn(\n \"flex items-center justify-center rounded-full hover:bg-[var(--canvas-surface)] transition-colors focus:outline-none focus:ring-2 focus:ring-[var(--canvas-primary)] focus:ring-offset-2\",\n buttonSize,\n className\n )}\n aria-label={ariaLabel}\n >\n <MoreVertical \n className={iconSize}\n style={{ color: \"var(--canvas-text-placeholder)\" }}\n />\n </button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align={align} sideOffset={4}>\n {items.map((item) => (\n <DropdownMenuItem\n key={item.id}\n onClick={item.onClick}\n variant={item.variant}\n disabled={item.disabled}\n >\n {item.label}\n </DropdownMenuItem>\n ))}\n </DropdownMenuContent>\n </DropdownMenu>\n );\n}\n\n\n\n\n\n\n"
10
+ }
11
+ ],
12
+ "dependencies": [
13
+ "lucide-react"
14
+ ],
15
+ "registryDependencies": [
16
+ "dropdown-menu",
17
+ "utils"
18
+ ]
19
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "messenger-sidebar",
3
+ "type": "registry:block",
4
+ "description": "Thread list sidebar for messaging with search, avatars, timestamps, and unread badges.",
5
+ "files": [
6
+ {
7
+ "path": "components/blocks/messenger-sidebar.tsx",
8
+ "type": "registry:block",
9
+ "content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { MoreHorizontal, PenSquare } from \"lucide-react\";\nimport { Avatar, AvatarFallback, AvatarImage } from \"../ui/avatar\";\nimport { Searchbox } from \"../ui/searchbox\";\n\ninterface ThreadItem {\n id: string;\n name: string;\n avatar?: string;\n lastMessage: string;\n timestamp: string;\n unreadCount?: number;\n isOnline?: boolean;\n}\n\nconst sampleThreads: ThreadItem[] = [\n {\n id: \"1\",\n name: \"Mary Trott\",\n avatar: \"https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=100&h=100&fit=crop\",\n lastMessage: \"Mary: Thank you so much for sending your...\",\n timestamp: \"Just now\",\n unreadCount: 3,\n },\n {\n id: \"2\",\n name: \"Raj, Mary, Jeff\",\n avatar: \"https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=100&h=100&fit=crop\",\n lastMessage: \"You: Hi Raj, could you take a look at the doc\",\n timestamp: \"30 mins ago\",\n unreadCount: 3,\n },\n {\n id: \"3\",\n name: \"Raj Mishra\",\n avatar: \"https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=100&h=100&fit=crop\",\n lastMessage: \"You: Hi Raj, could you take a look at the doc\",\n timestamp: \"30 mins ago\",\n },\n];\n\ninterface MessengerSidebarProps {\n selectedThreadId?: string;\n onSelectThread?: (threadId: string) => void;\n className?: string;\n}\n\nexport function MessengerSidebar({\n selectedThreadId,\n onSelectThread,\n className,\n}: MessengerSidebarProps) {\n const [searchValue, setSearchValue] = useState(\"\");\n\n const filteredThreads = sampleThreads.filter((thread) =>\n thread.name.toLowerCase().includes(searchValue.toLowerCase())\n );\n\n return (\n <aside\n className={`flex flex-col h-full border-r w-full md:w-[375px] shrink-0 ${className || \"\"}`}\n style={{\n borderColor: \"var(--canvas-border)\",\n backgroundColor: \"var(--canvas-background)\",\n }}\n >\n {/* Header */}\n <div\n className=\"flex items-center justify-between px-4 lg:px-[var(--spacing-5xl)] py-[var(--spacing-xl)] border-b shrink-0\"\n style={{ borderColor: \"var(--canvas-border)\" }}\n >\n <h1\n className=\"font-semibold\"\n style={{\n color: \"var(--canvas-foreground)\",\n fontSize: \"20px\",\n lineHeight: \"28px\",\n }}\n >\n Messages\n </h1>\n <div className=\"flex items-center gap-[var(--spacing-md)]\">\n <button\n className=\"flex items-center justify-center size-8 rounded-[var(--radius-xs)] border transition-colors hover:opacity-80\"\n style={{\n backgroundColor: \"var(--canvas-background)\",\n borderColor: \"var(--canvas-border)\",\n }}\n aria-label=\"More options\"\n >\n <MoreHorizontal\n className=\"size-4\"\n style={{ color: \"var(--canvas-text-muted)\" }}\n />\n </button>\n <button\n className=\"flex items-center justify-center size-8 rounded-[var(--radius-xs)] border transition-colors hover:opacity-80\"\n style={{\n backgroundColor: \"var(--canvas-background)\",\n borderColor: \"var(--canvas-border)\",\n }}\n aria-label=\"Compose message\"\n >\n <PenSquare\n className=\"size-4\"\n style={{ color: \"var(--canvas-text-muted)\" }}\n />\n </button>\n </div>\n </div>\n\n {/* Search */}\n <div className=\"px-4 lg:px-[var(--spacing-5xl)] py-[var(--spacing-xl)] shrink-0 border-b\" style={{ borderColor: \"var(--canvas-border)\" }}>\n <Searchbox\n value={searchValue}\n onChange={setSearchValue}\n placeholder=\"Search messages\"\n inputSize=\"sm\"\n />\n </div>\n\n {/* Thread List */}\n <div className=\"flex-1 overflow-y-auto\">\n {filteredThreads.map((thread) => (\n <ThreadRow\n key={thread.id}\n thread={thread}\n isSelected={selectedThreadId === thread.id}\n onSelect={() => onSelectThread?.(thread.id)}\n />\n ))}\n </div>\n </aside>\n );\n}\n\ninterface ThreadRowProps {\n thread: ThreadItem;\n isSelected?: boolean;\n onSelect?: () => void;\n}\n\nfunction ThreadRow({ thread, isSelected, onSelect }: ThreadRowProps) {\n return (\n <button\n onClick={onSelect}\n className=\"w-full flex items-center gap-[var(--spacing-xl)] px-4 lg:px-[var(--spacing-5xl)] py-[var(--spacing-xl)] transition-colors text-left border-b\"\n style={{\n backgroundColor: isSelected\n ? \"var(--canvas-surface)\"\n : \"var(--canvas-background)\",\n borderColor: \"var(--canvas-border)\",\n }}\n >\n {/* Avatar with unread badge */}\n <div className=\"relative shrink-0\">\n <Avatar className=\"size-12\">\n <AvatarImage src={thread.avatar} alt={thread.name} />\n <AvatarFallback\n className=\"text-xs\"\n style={{\n backgroundColor: \"var(--canvas-primary)\",\n color: \"var(--canvas-primary-foreground)\",\n }}\n >\n {thread.name\n .split(\" \")\n .map((n) => n[0])\n .join(\"\")}\n </AvatarFallback>\n </Avatar>\n {thread.unreadCount && (\n <div\n className=\"absolute size-5 rounded-full flex items-center justify-center text-[10px] font-semibold\"\n style={{\n backgroundColor: \"var(--canvas-text-placeholder)\",\n color: \"var(--canvas-primary-foreground)\",\n bottom: \"1px\",\n right: \"-2px\",\n }}\n >\n {thread.unreadCount}\n </div>\n )}\n </div>\n\n {/* Content */}\n <div className=\"flex-1 min-w-0 flex flex-col gap-[var(--spacing-xs)]\">\n <div className=\"flex items-center justify-between gap-2\">\n <span\n className=\"font-semibold truncate\"\n style={{\n color: \"var(--canvas-foreground)\",\n fontSize: \"var(--typo-body-s-size)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n }}\n >\n {thread.name}\n </span>\n <span\n className=\"shrink-0 font-normal\"\n style={{\n color: \"var(--canvas-text-muted)\",\n fontSize: \"var(--typo-body-xs-size)\",\n lineHeight: \"var(--typo-body-xs-line-height)\",\n }}\n >\n {thread.timestamp}\n </span>\n </div>\n <div className=\"flex items-center gap-2\">\n <span\n className=\"truncate\"\n style={{\n color: \"var(--canvas-text-muted)\",\n fontSize: \"var(--typo-body-s-size)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n }}\n >\n {thread.lastMessage}\n </span>\n </div>\n </div>\n </button>\n );\n}\n\n"
10
+ }
11
+ ],
12
+ "dependencies": [
13
+ "lucide-react"
14
+ ],
15
+ "registryDependencies": [
16
+ "avatar",
17
+ "searchbox"
18
+ ]
19
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "metrics-section",
3
+ "type": "registry:block",
4
+ "description": "Row of large metric numbers with labels.",
5
+ "files": [
6
+ {
7
+ "path": "components/blocks/marketing/metrics-section.tsx",
8
+ "type": "registry:block",
9
+ "content": "\"use client\";\n\nimport { Typography } from \"../../ui/typography\";\n\ninterface Metric {\n value: string;\n label: string;\n}\n\ninterface MetricsSectionProps {\n subtitle?: string;\n title?: string;\n metrics?: Metric[];\n}\n\nconst defaultMetrics: Metric[] = [\n { value: \"1,200+\", label: \"Team members\" },\n { value: \"2016\", label: \"Year founded\" },\n { value: \"3.5M+\", label: \"Users\" },\n { value: \"$22M\", label: \"In total funding\" },\n];\n\nexport function MetricsSection({\n subtitle = \"KEY METRICS\",\n title = \"At a glance\",\n metrics = defaultMetrics,\n}: MetricsSectionProps) {\n return (\n <section\n className=\"w-full px-6 md:px-20 py-12 md:py-16\"\n style={{ backgroundColor: \"var(--canvas-background)\" }}\n >\n <div className=\"max-w-[1240px] mx-auto flex flex-col gap-8 md:gap-12\">\n {/* Header */}\n <div className=\"flex flex-col gap-3\">\n <Typography variant=\"body-xs\" as=\"span\" className=\"uppercase tracking-wide\" color=\"muted\">\n {subtitle}\n </Typography>\n <Typography variant=\"h3\" as=\"h2\">\n {title}\n </Typography>\n </div>\n\n {/* Metrics Grid */}\n <div className=\"grid grid-cols-2 md:grid-cols-4 gap-6 md:gap-16\">\n {metrics.map((metric, index) => (\n <div\n key={index}\n className=\"flex flex-col gap-2 pl-4 md:pl-5 py-1.5\"\n style={{\n borderLeft: \"1px solid var(--canvas-border)\",\n }}\n >\n <Typography variant=\"h1\" as=\"span\">\n {metric.value}\n </Typography>\n <Typography variant=\"body-l\" as=\"span\" color=\"muted\">\n {metric.label}\n </Typography>\n </div>\n ))}\n </div>\n </div>\n </section>\n );\n}\n"
10
+ }
11
+ ],
12
+ "dependencies": [],
13
+ "registryDependencies": []
14
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "mobile-bottom-nav",
3
+ "type": "registry:block",
4
+ "description": "Fixed bottom navigation bar for mobile layouts.",
5
+ "files": [
6
+ {
7
+ "path": "components/blocks/mobile-bottom-nav.tsx",
8
+ "type": "registry:block",
9
+ "content": "\"use client\";\n\nimport { cn } from \"../../lib/utils\";\nimport { LucideIcon, Home, MessageSquare, Search, User } from \"lucide-react\";\n\n// ============================================\n// Mobile Nav Tab Item\n// ============================================\n\nexport interface MobileNavTabConfig {\n id: string;\n label: string;\n icon: LucideIcon;\n isActive?: boolean;\n}\n\ninterface MobileNavTabProps {\n item: MobileNavTabConfig;\n variant?: \"dark\" | \"light\";\n onClick?: () => void;\n}\n\nfunction MobileNavTab({ item, variant = \"light\", onClick }: MobileNavTabProps) {\n const Icon = item.icon;\n const isActive = item.isActive;\n const isDark = variant === \"dark\";\n\n return (\n <button\n onClick={onClick}\n className={cn(\n // Match icon-sidebar dimensions: 64px × 64px\n \"relative flex flex-col items-center justify-center gap-1 w-16 h-16 rounded-[var(--radius-nav)] transition-colors\",\n // Dark variant\n isDark && isActive && \"bg-[var(--canvas-sidebar-dark-active-bg)]\",\n isDark && !isActive && \"hover:bg-[var(--canvas-sidebar-dark-active-bg)]/50\",\n // Light variant\n !isDark && isActive && \"bg-[var(--canvas-sidebar-light-active-bg)]\",\n !isDark && !isActive && \"hover:bg-[var(--canvas-sidebar-light-active-bg)]/50\"\n )}\n >\n <Icon\n className={cn(\n // Match icon-sidebar: 16px icons\n \"size-4\",\n isDark && isActive && \"text-[var(--canvas-sidebar-dark-active-text)]\",\n isDark && !isActive && \"text-[var(--canvas-sidebar-dark-text)]\",\n !isDark && isActive && \"text-[var(--canvas-sidebar-light-active-text)]\",\n !isDark && !isActive && \"text-[var(--canvas-sidebar-light-text)]\"\n )}\n />\n <span\n className={cn(\n // Match icon-sidebar: 12px labels, medium weight\n \"text-xs font-medium\",\n isDark && isActive && \"text-[var(--canvas-sidebar-dark-active-text)]\",\n isDark && !isActive && \"text-[var(--canvas-sidebar-dark-text)]\",\n !isDark && isActive && \"text-[var(--canvas-sidebar-light-active-text)]\",\n !isDark && !isActive && \"text-[var(--canvas-sidebar-light-text)]\"\n )}\n >\n {item.label}\n </span>\n </button>\n );\n}\n\n// ============================================\n// Default Navigation Items\n// ============================================\n\nexport const defaultMobileNavTabs: MobileNavTabConfig[] = [\n { id: \"home\", label: \"Home\", icon: Home, isActive: true },\n { id: \"messages\", label: \"Messages\", icon: MessageSquare },\n { id: \"discover\", label: \"Discover\", icon: Search },\n { id: \"account\", label: \"Account\", icon: User },\n];\n\n// ============================================\n// Mobile Bottom Navigation\n// ============================================\n\ninterface MobileBottomNavProps {\n /** Navigation tabs to display */\n tabs?: MobileNavTabConfig[];\n /** Visual variant - dark or light theme */\n variant?: \"dark\" | \"light\";\n /** Callback when a tab is clicked */\n onTabClick?: (tab: MobileNavTabConfig) => void;\n /** Additional class names */\n className?: string;\n}\n\n/**\n * Canvas Design System - Mobile Bottom Navigation\n * \n * A sticky bottom navigation bar with icon tabs.\n * Styling matches the icon-sidebar for consistency.\n * Supports both dark and light themes via the variant prop.\n * \n * @example\n * ```tsx\n * <MobileBottomNav\n * variant=\"light\"\n * tabs={defaultMobileNavTabs}\n * onTabClick={(tab) => console.log(tab.id)}\n * />\n * ```\n */\nexport function MobileBottomNav({\n tabs = defaultMobileNavTabs,\n variant = \"light\",\n onTabClick,\n className,\n}: MobileBottomNavProps) {\n const isDark = variant === \"dark\";\n\n return (\n <nav\n className={cn(\n \"fixed bottom-0 left-0 right-0 z-50\",\n \"flex items-center justify-center gap-5\",\n \"px-4 py-3\",\n // Dark variant\n isDark && \"bg-[var(--canvas-sidebar-dark-bg)] border-t border-[var(--canvas-sidebar-dark-border)]\",\n isDark && \"shadow-[0px_-4px_16px_0px_rgba(0,0,0,0.2)]\",\n // Light variant\n !isDark && \"bg-[var(--canvas-sidebar-light-bg)] border-t border-[var(--canvas-sidebar-light-border)]\",\n !isDark && \"shadow-[0px_-4px_16px_0px_rgba(0,0,0,0.04)]\",\n className\n )}\n >\n {tabs.map((tab) => (\n <MobileNavTab\n key={tab.id}\n item={tab}\n variant={variant}\n onClick={() => onTabClick?.(tab)}\n />\n ))}\n </nav>\n );\n}\n"
10
+ }
11
+ ],
12
+ "dependencies": [
13
+ "lucide-react"
14
+ ],
15
+ "registryDependencies": [
16
+ "utils"
17
+ ]
18
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "monthly-calendar-widget",
3
+ "type": "registry:block",
4
+ "description": "Dual-month calendar for date range selection with price labels, disabled dates, and today indicator. Used for booking and scheduling interfaces.",
5
+ "files": [
6
+ {
7
+ "path": "components/blocks/monthly-calendar-widget.tsx",
8
+ "type": "registry:block",
9
+ "content": "\"use client\";\n\nimport { useState, useMemo } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport { Button } from \"../ui/button\";\nimport { ChevronLeft, ChevronRight, ArrowRight } from \"lucide-react\";\nimport {\n format,\n startOfMonth,\n endOfMonth,\n startOfWeek,\n endOfWeek,\n addDays,\n addMonths,\n subMonths,\n isSameMonth,\n isSameDay,\n isWithinInterval,\n isBefore,\n isAfter,\n} from \"date-fns\";\n\n// ============================================\n// Types\n// ============================================\n\nexport interface PricedDate {\n date: Date;\n price: string;\n}\n\nexport interface DateRange {\n start: Date | null;\n end: Date | null;\n}\n\nexport interface MonthlyCalendarWidgetProps {\n /** Widget title */\n title?: string;\n /** Widget subtitle */\n subtitle?: string;\n /** Initial month to display (defaults to current month) */\n initialMonth?: Date;\n /** Currently selected date range */\n selectedRange?: DateRange;\n /** Array of dates that should be disabled/unavailable */\n disabledDates?: Date[];\n /** Array of dates with prices to display */\n pricedDates?: PricedDate[];\n /** Override for \"today\" (useful for demos) */\n todayDate?: Date;\n /** Callback when a date is selected */\n onDateSelect?: (date: Date) => void;\n /** Callback when the date range changes */\n onRangeChange?: (range: DateRange) => void;\n /** Callback when Confirm button is clicked */\n onConfirm?: () => void;\n /** Callback when Cancel button is clicked */\n onCancel?: () => void;\n /** Additional class names */\n className?: string;\n}\n\n// ============================================\n// Helper Functions\n// ============================================\n\nfunction isDateDisabled(date: Date, disabledDates: Date[]): boolean {\n return disabledDates.some((d) => isSameDay(d, date));\n}\n\nfunction isDateInRange(date: Date, range: DateRange): boolean {\n if (!range.start || !range.end) return false;\n return isWithinInterval(date, { start: range.start, end: range.end });\n}\n\nfunction isRangeStart(date: Date, range: DateRange): boolean {\n return range.start ? isSameDay(date, range.start) : false;\n}\n\nfunction isRangeEnd(date: Date, range: DateRange): boolean {\n return range.end ? isSameDay(date, range.end) : false;\n}\n\nfunction getDatePrice(date: Date, pricedDates: PricedDate[]): string | null {\n const priced = pricedDates.find((p) => isSameDay(p.date, date));\n return priced ? priced.price : null;\n}\n\n// ============================================\n// Sub-components\n// ============================================\n\ninterface DateCellProps {\n date: Date;\n currentMonth: Date;\n today: Date;\n selectedRange: DateRange;\n disabledDates: Date[];\n pricedDates: PricedDate[];\n onSelect: (date: Date) => void;\n}\n\nfunction DateCell({\n date,\n currentMonth,\n today,\n selectedRange,\n disabledDates,\n pricedDates,\n onSelect,\n}: DateCellProps) {\n const isCurrentMonth = isSameMonth(date, currentMonth);\n const isToday = isSameDay(date, today);\n const isDisabled = isDateDisabled(date, disabledDates);\n const isInRange = isDateInRange(date, selectedRange);\n const isStart = isRangeStart(date, selectedRange);\n const isEnd = isRangeEnd(date, selectedRange);\n const isSelected = isStart || isEnd;\n const price = getDatePrice(date, pricedDates);\n\n // Determine if this is a past date (before today, should show as disabled)\n const isPast = isBefore(date, today) && !isSameDay(date, today);\n\n // Don't render dates from other months\n if (!isCurrentMonth) {\n return (\n <div className=\"flex justify-center items-center\">\n <div className=\"size-12\" />\n </div>\n );\n }\n\n const handleClick = () => {\n if (!isDisabled && !isPast) {\n onSelect(date);\n }\n };\n\n // Determine styling based on state\n let bgColor = \"transparent\";\n let textColor = \"var(--canvas-text-placeholder)\";\n let showStrikethrough = false;\n let priceTextColor = \"var(--canvas-text)\";\n\n if (isDisabled || isPast) {\n textColor = \"var(--canvas-border-disabled)\";\n showStrikethrough = true;\n } else if (isSelected) {\n bgColor = \"var(--canvas-primary)\";\n textColor = \"var(--canvas-primary-foreground)\";\n priceTextColor = \"var(--canvas-primary-foreground)\";\n } else if (isInRange) {\n bgColor = \"var(--canvas-surface-brand)\";\n textColor = \"var(--canvas-primary)\";\n }\n\n return (\n <div className=\"flex justify-center items-center\">\n <button\n type=\"button\"\n onClick={handleClick}\n disabled={isDisabled || isPast}\n className={cn(\n \"relative flex flex-col items-center justify-center rounded-full size-12 transition-colors\",\n !isDisabled && !isPast && \"hover:bg-[var(--canvas-surface)] cursor-pointer\",\n (isDisabled || isPast) && \"cursor-not-allowed\"\n )}\n style={{\n backgroundColor: bgColor,\n }}\n >\n {/* Today indicator */}\n {isToday && !isSelected && (\n <div\n className=\"absolute top-1.5 rounded-full\"\n style={{\n width: \"var(--spacing-sm)\",\n height: \"var(--spacing-sm)\",\n backgroundColor: \"var(--canvas-primary)\",\n }}\n />\n )}\n\n {/* Date number */}\n <span\n className={cn(\n \"font-semibold text-base leading-6\",\n showStrikethrough && \"line-through\"\n )}\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n color: textColor,\n }}\n >\n {format(date, \"d\")}\n </span>\n\n {/* Price label */}\n {price && !isDisabled && !isPast && (\n <span\n className=\"absolute text-[6px] font-normal\"\n style={{\n bottom: \"4px\",\n color: priceTextColor,\n fontFamily: \"var(--typo-body-xs-font, var(--typo-global-font))\",\n }}\n >\n {price}\n </span>\n )}\n </button>\n </div>\n );\n}\n\ninterface MonthCalendarProps {\n month: Date;\n today: Date;\n selectedRange: DateRange;\n disabledDates: Date[];\n pricedDates: PricedDate[];\n onDateSelect: (date: Date) => void;\n onPrevMonth?: () => void;\n onNextMonth?: () => void;\n showPrevArrow?: boolean;\n showNextArrow?: boolean;\n}\n\nfunction MonthCalendar({\n month,\n today,\n selectedRange,\n disabledDates,\n pricedDates,\n onDateSelect,\n onPrevMonth,\n onNextMonth,\n showPrevArrow = false,\n showNextArrow = false,\n}: MonthCalendarProps) {\n const dayHeaders = [\"SUN\", \"MON\", \"TUE\", \"WED\", \"THU\", \"FRI\", \"SAT\"];\n\n // Generate calendar grid\n const monthStart = startOfMonth(month);\n const monthEnd = endOfMonth(month);\n const calendarStart = startOfWeek(monthStart, { weekStartsOn: 0 });\n const calendarEnd = endOfWeek(monthEnd, { weekStartsOn: 0 });\n\n const weeks: Date[][] = [];\n let currentDate = calendarStart;\n\n while (currentDate <= calendarEnd) {\n const week: Date[] = [];\n for (let i = 0; i < 7; i++) {\n week.push(currentDate);\n currentDate = addDays(currentDate, 1);\n }\n weeks.push(week);\n }\n\n return (\n <div\n className=\"flex flex-col\"\n style={{ gap: \"var(--spacing-lg)\", minWidth: \"336px\" }}\n >\n {/* Month Header */}\n <div\n className=\"flex items-center justify-center\"\n style={{\n paddingLeft: showPrevArrow ? \"0\" : \"var(--spacing-4xl)\",\n paddingRight: showNextArrow ? \"0\" : \"var(--spacing-4xl)\",\n }}\n >\n {showPrevArrow && (\n <button\n type=\"button\"\n onClick={onPrevMonth}\n className=\"size-8 flex items-center justify-center hover:bg-[var(--canvas-surface)] rounded-md transition-colors shrink-0\"\n >\n <ChevronLeft\n className=\"size-4\"\n style={{ color: \"var(--canvas-text-muted)\" }}\n />\n </button>\n )}\n <span\n className=\"flex-1 text-center font-medium\"\n style={{\n fontFamily: \"var(--typo-body-xl-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-xl-size)\",\n lineHeight: \"30px\",\n color: \"var(--canvas-text)\",\n }}\n >\n {format(month, \"MMMM\")}\n </span>\n {showNextArrow && (\n <button\n type=\"button\"\n onClick={onNextMonth}\n className=\"size-8 flex items-center justify-center hover:bg-[var(--canvas-surface)] rounded-md transition-colors shrink-0\"\n >\n <ChevronRight\n className=\"size-4\"\n style={{ color: \"var(--canvas-text-muted)\" }}\n />\n </button>\n )}\n </div>\n\n {/* Day Headers */}\n <div\n className=\"grid grid-cols-7\"\n style={{\n paddingLeft: showPrevArrow ? \"0\" : \"var(--spacing-4xl)\",\n paddingRight: showNextArrow ? \"0\" : \"var(--spacing-4xl)\",\n }}\n >\n {dayHeaders.map((day) => (\n <div\n key={day}\n className=\"text-center\"\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: \"20px\",\n color: \"var(--canvas-text-muted)\",\n }}\n >\n {day}\n </div>\n ))}\n </div>\n\n {/* Week Rows */}\n {weeks.map((week, weekIndex) => (\n <div\n key={weekIndex}\n className=\"grid grid-cols-7\"\n style={{\n paddingLeft: showPrevArrow ? \"0\" : \"var(--spacing-4xl)\",\n paddingRight: showNextArrow ? \"0\" : \"var(--spacing-4xl)\",\n paddingTop: \"var(--spacing-md)\",\n paddingBottom: \"var(--spacing-md)\",\n }}\n >\n {week.map((date, dateIndex) => (\n <DateCell\n key={dateIndex}\n date={date}\n currentMonth={month}\n today={today}\n selectedRange={selectedRange}\n disabledDates={disabledDates}\n pricedDates={pricedDates}\n onSelect={onDateSelect}\n />\n ))}\n </div>\n ))}\n </div>\n );\n}\n\n// ============================================\n// Main Component\n// ============================================\n\n/**\n * Canvas Design System - Monthly Calendar Widget Block\n *\n * A dual-month calendar widget for date range selection with support for\n * disabled dates, price labels, and today indicator. Commonly used for\n * booking and scheduling interfaces.\n *\n * @example\n * ```tsx\n * <MonthlyCalendarWidget\n * title=\"Browse availability\"\n * subtitle=\"Book your stay\"\n * onRangeChange={(range) => console.log(range)}\n * onConfirm={() => console.log(\"Confirmed\")}\n * />\n * ```\n */\nexport function MonthlyCalendarWidget({\n title = \"Browse availability\",\n subtitle = \"Book your stay\",\n initialMonth,\n selectedRange: controlledRange,\n disabledDates = [],\n pricedDates = [],\n todayDate,\n onDateSelect,\n onRangeChange,\n onConfirm,\n onCancel,\n className,\n}: MonthlyCalendarWidgetProps) {\n const today = todayDate || new Date();\n const [currentMonth, setCurrentMonth] = useState<Date>(\n initialMonth || startOfMonth(today)\n );\n const [internalRange, setInternalRange] = useState<DateRange>({\n start: null,\n end: null,\n });\n\n const selectedRange = controlledRange ?? internalRange;\n const nextMonth = addMonths(currentMonth, 1);\n\n const handleDateSelect = (date: Date) => {\n onDateSelect?.(date);\n\n let newRange: DateRange;\n\n if (!selectedRange.start || (selectedRange.start && selectedRange.end)) {\n // Start new selection\n newRange = { start: date, end: null };\n } else {\n // Complete the selection\n if (isBefore(date, selectedRange.start)) {\n newRange = { start: date, end: selectedRange.start };\n } else {\n newRange = { start: selectedRange.start, end: date };\n }\n }\n\n if (!controlledRange) {\n setInternalRange(newRange);\n }\n onRangeChange?.(newRange);\n };\n\n const handlePrevMonth = () => {\n setCurrentMonth((prev) => subMonths(prev, 1));\n };\n\n const handleNextMonth = () => {\n setCurrentMonth((prev) => addMonths(prev, 1));\n };\n\n const formatInputDate = (date: Date | null): string => {\n if (!date) return \"\";\n return format(date, \"MMM d, yyyy\");\n };\n\n return (\n <div\n className={cn(\"flex flex-col w-full\", className)}\n style={{ gap: \"var(--spacing-xl)\" }}\n >\n {/* Title Section */}\n <div\n className=\"flex flex-wrap items-start w-full\"\n style={{ gap: \"var(--spacing-xl)\" }}\n >\n <div\n className=\"flex flex-col flex-1 min-w-[200px]\"\n style={{ gap: \"var(--spacing-xs)\" }}\n >\n <h2\n style={{\n fontFamily: \"var(--typo-h6-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-h6-size)\",\n fontWeight: \"var(--typo-h6-weight)\",\n letterSpacing: \"var(--typo-h6-spacing)\",\n lineHeight: \"var(--typo-h6-line-height)\",\n color: \"var(--canvas-text)\",\n margin: 0,\n }}\n >\n {title}\n </h2>\n <p\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: \"var(--typo-body-s-weight)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text-muted)\",\n margin: 0,\n }}\n >\n {subtitle}\n </p>\n </div>\n </div>\n\n {/* Calendar Content */}\n <div\n className=\"flex flex-col w-full\"\n style={{ gap: \"var(--spacing-3xl)\" }}\n >\n {/* Dual Month Grid */}\n <div className=\"flex items-start justify-between w-full gap-4 overflow-x-auto\">\n <MonthCalendar\n month={currentMonth}\n today={today}\n selectedRange={selectedRange}\n disabledDates={disabledDates}\n pricedDates={pricedDates}\n onDateSelect={handleDateSelect}\n onPrevMonth={handlePrevMonth}\n showPrevArrow={true}\n showNextArrow={false}\n />\n <MonthCalendar\n month={nextMonth}\n today={today}\n selectedRange={selectedRange}\n disabledDates={disabledDates}\n pricedDates={pricedDates}\n onDateSelect={handleDateSelect}\n onNextMonth={handleNextMonth}\n showPrevArrow={false}\n showNextArrow={true}\n />\n </div>\n\n {/* Footer Section */}\n <div className=\"flex items-start justify-between w-full\">\n {/* Date Inputs */}\n <div\n className=\"flex items-center justify-center\"\n style={{ gap: \"var(--spacing-md)\", width: \"286px\" }}\n >\n {/* Start Date Input */}\n <div\n className=\"flex flex-col\"\n style={{ gap: \"var(--spacing-xs)\", width: \"128px\" }}\n >\n <div\n className=\"flex items-center h-11 rounded\"\n style={{\n backgroundColor: \"var(--canvas-background)\",\n border: \"1px solid var(--canvas-border)\",\n borderRadius: \"var(--radius-xs)\",\n paddingLeft: \"var(--spacing-xl)\",\n paddingRight: \"var(--spacing-xl)\",\n boxShadow: \"0px 1px 2px 0px rgba(0,0,0,0.02)\",\n }}\n >\n <span\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: \"var(--typo-body-s-weight)\",\n lineHeight: \"24px\",\n color: selectedRange.start\n ? \"var(--canvas-text)\"\n : \"var(--canvas-text-placeholder)\",\n }}\n >\n {selectedRange.start\n ? formatInputDate(selectedRange.start)\n : \"Start date\"}\n </span>\n </div>\n </div>\n\n {/* Arrow */}\n <ArrowRight\n className=\"size-5 shrink-0\"\n style={{ color: \"var(--canvas-text-muted)\" }}\n />\n\n {/* End Date Input */}\n <div\n className=\"flex flex-col\"\n style={{ gap: \"var(--spacing-xs)\", width: \"128px\" }}\n >\n <div\n className=\"flex items-center h-11 rounded\"\n style={{\n backgroundColor: \"var(--canvas-background)\",\n border: \"1px solid var(--canvas-border)\",\n borderRadius: \"var(--radius-xs)\",\n paddingLeft: \"var(--spacing-xl)\",\n paddingRight: \"var(--spacing-xl)\",\n boxShadow: \"0px 1px 2px 0px rgba(0,0,0,0.02)\",\n }}\n >\n <span\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: \"var(--typo-body-s-weight)\",\n lineHeight: \"24px\",\n color: selectedRange.end\n ? \"var(--canvas-text)\"\n : \"var(--canvas-text-placeholder)\",\n }}\n >\n {selectedRange.end\n ? formatInputDate(selectedRange.end)\n : \"End date\"}\n </span>\n </div>\n </div>\n </div>\n\n {/* Action Buttons */}\n <div\n className=\"flex items-center justify-center\"\n style={{ gap: \"var(--spacing-3xl)\" }}\n >\n <Button variant=\"outline\" size=\"lg\" onClick={onCancel}>\n Cancel\n </Button>\n <Button variant=\"primary\" size=\"lg\" onClick={onConfirm}>\n Confirm\n </Button>\n </div>\n </div>\n </div>\n </div>\n );\n}\n"
10
+ }
11
+ ],
12
+ "dependencies": [
13
+ "lucide-react",
14
+ "date-fns"
15
+ ],
16
+ "registryDependencies": [
17
+ "utils",
18
+ "button"
19
+ ]
20
+ }
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "nested-comments-table",
3
+ "type": "registry:block",
4
+ "description": "Threaded discussion component with nested comments, reply/like actions, and collapsible threads.",
5
+ "files": [
6
+ {
7
+ "path": "components/blocks/nested-comments-table.tsx",
8
+ "type": "registry:block",
9
+ "content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport { Button } from \"../ui/button\";\nimport { Avatar, AvatarImage, AvatarFallback } from \"../ui/avatar\";\nimport { Input } from \"../ui/input\";\nimport { Heart, Paperclip } from \"lucide-react\";\n\n// ============================================\n// Types\n// ============================================\n\nexport interface CommentAuthor {\n id: string;\n name: string;\n avatarUrl?: string;\n}\n\nexport interface Comment {\n id: string;\n author: CommentAuthor;\n content: string;\n timestamp: string;\n likes: number;\n isLiked?: boolean;\n replies?: Comment[];\n}\n\nexport interface NestedCommentsTableProps {\n /** Section title */\n title?: string;\n /** Section subtitle */\n subtitle?: string;\n /** Comments data */\n comments?: Comment[];\n /** Current user for comment input avatars */\n currentUser?: CommentAuthor;\n /** Callback when main comment is submitted */\n onComment?: (content: string) => void;\n /** Callback when reply is submitted */\n onReply?: (commentId: string, content: string) => void;\n /** Callback when like is clicked */\n onLike?: (commentId: string) => void;\n /** Additional class names */\n className?: string;\n}\n\n// ============================================\n// Default Data\n// ============================================\n\nconst defaultCurrentUser: CommentAuthor = {\n id: \"current\",\n name: \"Aya Williams\",\n avatarUrl: \"https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=150&h=150&fit=crop&crop=face\",\n};\n\nconst defaultComments: Comment[] = [\n {\n id: \"c1\",\n author: {\n id: \"raj\",\n name: \"Raj Mishra\",\n avatarUrl: \"https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=150&h=150&fit=crop&crop=face\",\n },\n content: \"Wow, Paris looks absolutely stunning! The Eiffel Tower is such an iconic landmark. Hope you have an amazing time exploring the city and soaking in all its beauty. Safe travels!\",\n timestamp: \"Feb 23, 1:32 PM\",\n likes: 3,\n isLiked: true,\n replies: [\n {\n id: \"r1\",\n author: {\n id: \"mary\",\n name: \"Raj Mishra\",\n avatarUrl: \"https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=150&h=150&fit=crop&crop=face\",\n },\n content: \"Paris is truly a dream destination! The Eiffel Tower never fails to impress. Enjoy every moment of your adventure and make unforgettable memories. Can't wait to see more of your journey!\",\n timestamp: \"Mar 8, 11:23 AM\",\n likes: 0,\n isLiked: false,\n },\n ],\n },\n];\n\n// ============================================\n// Sub-components\n// ============================================\n\ninterface CommentInputProps {\n avatarUrl?: string;\n avatarFallback?: string;\n placeholder?: string;\n buttonText?: string;\n size?: \"default\" | \"small\";\n showAttachment?: boolean;\n onSubmit?: (content: string) => void;\n}\n\nfunction CommentInput({\n avatarUrl,\n avatarFallback = \"U\",\n placeholder = \"Send a message\",\n buttonText = \"Send\",\n size = \"default\",\n showAttachment = true,\n onSubmit,\n}: CommentInputProps) {\n const [value, setValue] = useState(\"\");\n const avatarSize = size === \"small\" ? 40 : 48;\n\n const handleSubmit = () => {\n if (value.trim() && onSubmit) {\n onSubmit(value.trim());\n setValue(\"\");\n }\n };\n\n return (\n <div\n className=\"flex items-center w-full\"\n style={{ gap: \"var(--spacing-xl)\" }}\n >\n <Avatar\n className=\"shrink-0\"\n style={{\n width: avatarSize,\n height: avatarSize,\n borderRadius: \"var(--spacing-3xl)\",\n border: \"1px solid var(--canvas-border)\",\n }}\n >\n <AvatarImage src={avatarUrl} />\n <AvatarFallback>{avatarFallback}</AvatarFallback>\n </Avatar>\n <div className=\"flex-1 relative\">\n <Input\n value={value}\n onChange={(e) => setValue(e.target.value)}\n placeholder={placeholder}\n className=\"pr-10\"\n onKeyDown={(e) => {\n if (e.key === \"Enter\" && !e.shiftKey) {\n e.preventDefault();\n handleSubmit();\n }\n }}\n />\n {showAttachment && (\n <button\n type=\"button\"\n className=\"absolute right-3 top-1/2 -translate-y-1/2\"\n style={{ color: \"var(--canvas-text-placeholder)\" }}\n >\n <Paperclip className=\"w-5 h-5\" />\n </button>\n )}\n </div>\n <Button variant=\"primary\" onClick={handleSubmit}>\n {buttonText}\n </Button>\n </div>\n );\n}\n\ninterface CommentActionsProps {\n likes: number;\n isLiked?: boolean;\n timestamp: string;\n onReply?: () => void;\n onLike?: () => void;\n}\n\nfunction CommentActions({\n likes,\n isLiked,\n timestamp,\n onReply,\n onLike,\n}: CommentActionsProps) {\n return (\n <div\n className=\"flex items-center\"\n style={{\n gap: \"var(--spacing-xl)\",\n paddingTop: \"var(--spacing-xs)\",\n }}\n >\n <button\n type=\"button\"\n onClick={onReply}\n className=\"flex items-center\"\n style={{\n gap: \"var(--spacing-sm)\",\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 500,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text-placeholder)\",\n }}\n >\n Reply\n </button>\n <button\n type=\"button\"\n onClick={onLike}\n className=\"flex items-center\"\n style={{\n gap: \"var(--spacing-sm)\",\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 500,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text-placeholder)\",\n }}\n >\n <Heart\n className=\"w-5 h-5\"\n style={{\n fill: isLiked ? \"var(--canvas-destructive)\" : \"transparent\",\n stroke: isLiked ? \"var(--canvas-destructive)\" : \"currentColor\",\n }}\n />\n {likes > 0 ? `${likes} likes` : \"Like\"}\n </button>\n <span\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 500,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text-placeholder)\",\n }}\n >\n {timestamp}\n </span>\n </div>\n );\n}\n\ninterface CommentItemProps {\n comment: Comment;\n currentUser?: CommentAuthor;\n depth?: number;\n onReply?: (content: string) => void;\n onLike?: () => void;\n}\n\nfunction CommentItem({\n comment,\n currentUser,\n depth = 0,\n onReply,\n onLike,\n}: CommentItemProps) {\n const [showReplyInput, setShowReplyInput] = useState(false);\n const [showReplies, setShowReplies] = useState(true);\n const hasReplies = comment.replies && comment.replies.length > 0;\n\n // Determine padding based on depth\n const getPaddingLeft = () => {\n if (depth === 0) return \"var(--spacing-7xl)\"; // 64px for first level\n return \"var(--spacing-10xl)\"; // 128px for deeper levels\n };\n\n return (\n <div\n className=\"flex flex-col w-full\"\n style={{ gap: \"var(--spacing-xl)\" }}\n >\n {/* Comment content */}\n <div\n className=\"flex w-full\"\n style={{ gap: \"var(--spacing-xl)\" }}\n >\n <Avatar\n className=\"shrink-0\"\n style={{\n width: 48,\n height: 48,\n borderRadius: \"var(--spacing-3xl)\",\n border: \"1px solid var(--canvas-border)\",\n }}\n >\n <AvatarImage src={comment.author.avatarUrl} />\n <AvatarFallback>\n {comment.author.name.split(\" \").map(n => n[0]).join(\"\").slice(0, 2)}\n </AvatarFallback>\n </Avatar>\n <div className=\"flex-1 flex flex-col\" style={{ gap: \"var(--spacing-sm)\" }}>\n {/* Author and timestamp */}\n <div\n className=\"flex items-center\"\n style={{ gap: \"var(--spacing-xl)\" }}\n >\n <span\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 600,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n {comment.author.name}\n </span>\n <span\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 400,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text-placeholder)\",\n }}\n >\n {comment.timestamp}\n </span>\n </div>\n {/* Comment text */}\n <p\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 400,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text)\",\n margin: 0,\n }}\n >\n {comment.content}\n </p>\n {/* Actions */}\n <CommentActions\n likes={comment.likes}\n isLiked={comment.isLiked}\n timestamp={comment.timestamp}\n onReply={() => setShowReplyInput(!showReplyInput)}\n onLike={onLike}\n />\n </div>\n </div>\n\n {/* Reply input */}\n {showReplyInput && (\n <div style={{ paddingLeft: \"var(--spacing-7xl)\" }}>\n <CommentInput\n avatarUrl={currentUser?.avatarUrl}\n avatarFallback={currentUser?.name?.charAt(0) || \"U\"}\n placeholder=\"Send a message\"\n buttonText=\"Reply\"\n size=\"small\"\n showAttachment={false}\n onSubmit={(content) => {\n onReply?.(content);\n setShowReplyInput(false);\n }}\n />\n </div>\n )}\n\n {/* Hide replies toggle */}\n {hasReplies && (\n <div\n className=\"flex items-center\"\n style={{\n paddingLeft: \"var(--spacing-7xl)\",\n gap: \"var(--spacing-md)\",\n width: 196,\n }}\n >\n <div\n className=\"flex-1 h-px\"\n style={{ backgroundColor: \"var(--canvas-border)\" }}\n />\n <button\n type=\"button\"\n onClick={() => setShowReplies(!showReplies)}\n style={{\n fontFamily: \"var(--typo-body-xs-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-xs-size)\",\n fontWeight: 500,\n lineHeight: \"var(--typo-body-xs-line-height)\",\n color: \"var(--canvas-text-placeholder)\",\n whiteSpace: \"nowrap\",\n }}\n >\n {showReplies ? \"Hide replies\" : \"Show replies\"}\n </button>\n </div>\n )}\n\n {/* Nested replies */}\n {hasReplies && showReplies && (\n <div\n className=\"flex flex-col\"\n style={{\n paddingLeft: getPaddingLeft(),\n paddingTop: \"var(--spacing-xl)\",\n gap: \"var(--spacing-md)\",\n }}\n >\n {comment.replies!.map((reply) => (\n <CommentItem\n key={reply.id}\n comment={reply}\n currentUser={currentUser}\n depth={depth + 1}\n onReply={onReply}\n onLike={onLike}\n />\n ))}\n </div>\n )}\n </div>\n );\n}\n\n// ============================================\n// Main Component\n// ============================================\n\n/**\n * Canvas Design System - Nested Comments Table Block\n *\n * A threaded discussion component with nested comments, reply/like actions,\n * and collapsible threads. Perfect for discussion sections, comment threads,\n * or messaging interfaces.\n *\n * @example\n * ```tsx\n * <NestedCommentsTable\n * title=\"My discussions\"\n * subtitle=\"In the past year\"\n * comments={[...]}\n * onComment={(content) => console.log(\"Comment:\", content)}\n * />\n * ```\n */\nexport function NestedCommentsTable({\n title = \"My discussions\",\n subtitle = \"In the past year\",\n comments = defaultComments,\n currentUser = defaultCurrentUser,\n onComment,\n onReply,\n onLike,\n className,\n}: NestedCommentsTableProps) {\n return (\n <div\n className={cn(\"flex flex-col w-full\", className)}\n style={{ gap: \"var(--spacing-xl)\" }}\n >\n {/* Header Section */}\n <div\n className=\"flex flex-wrap items-start w-full\"\n style={{ gap: \"var(--spacing-xl)\" }}\n >\n {/* Title and Subtitle */}\n <div\n className=\"flex flex-col flex-1 min-w-[200px]\"\n style={{ gap: \"var(--spacing-xs)\" }}\n >\n <h2\n style={{\n fontFamily: \"var(--typo-h6-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-h6-size)\",\n fontWeight: \"var(--typo-h6-weight)\",\n letterSpacing: \"var(--typo-h6-spacing)\",\n lineHeight: \"var(--typo-h6-line-height)\",\n color: \"var(--canvas-text)\",\n margin: 0,\n }}\n >\n {title}\n </h2>\n <p\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: \"var(--typo-body-s-weight)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text-muted)\",\n margin: 0,\n }}\n >\n {subtitle}\n </p>\n </div>\n </div>\n\n {/* Comments List Shell */}\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 {/* Main Comment Input */}\n <CommentInput\n avatarUrl={currentUser?.avatarUrl}\n avatarFallback={currentUser?.name?.charAt(0) || \"U\"}\n placeholder=\"Send a message\"\n buttonText=\"Send\"\n onSubmit={onComment}\n />\n\n {/* Comments */}\n {comments && comments.length > 0 && (\n <div\n className=\"flex flex-col\"\n style={{\n paddingLeft: \"var(--spacing-7xl)\",\n paddingTop: \"var(--spacing-xl)\",\n gap: \"var(--spacing-md)\",\n }}\n >\n {comments.map((comment) => (\n <CommentItem\n key={comment.id}\n comment={comment}\n currentUser={currentUser}\n onReply={(content) => onReply?.(comment.id, content)}\n onLike={() => onLike?.(comment.id)}\n />\n ))}\n </div>\n )}\n </div>\n </div>\n );\n}\n"
10
+ }
11
+ ],
12
+ "dependencies": [
13
+ "lucide-react"
14
+ ],
15
+ "registryDependencies": [
16
+ "utils",
17
+ "button",
18
+ "avatar",
19
+ "input"
20
+ ]
21
+ }