canvas-ui-sdk 0.3.8 → 0.3.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +65 -63
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/registry/blocks/component-palette.json +1 -1
- package/registry/blocks/component-search.json +1 -1
- package/registry/blocks/custom-component-helper.json +1 -1
- package/registry/blocks/faqs-table.json +1 -1
- package/registry/blocks/infinity-canvas.json +1 -1
- package/registry/blocks/menu-section.json +1 -1
- package/registry/blocks/messenger-sidebar.json +1 -1
- package/registry/blocks/mobile-bottom-nav.json +1 -1
- package/registry/blocks/pagination.json +1 -1
- package/registry/blocks/pill-tabs.json +1 -1
- package/registry/blocks/pricing-cards.json +1 -1
- package/registry/blocks/profile-card.json +1 -1
- package/registry/blocks/prompt-template.json +1 -1
- package/registry/blocks/screen-flowchart.json +1 -1
- package/registry/blocks/screen-prompt-builder.json +1 -1
- package/registry/blocks/screen-prompt-template.json +1 -1
- package/registry/blocks/sidebar-cards.json +1 -1
- package/registry/blocks/slideshow-grid-tiles.json +1 -1
- package/registry/blocks/social-feed.json +1 -1
- package/registry/blocks/upvoting-posts-table.json +1 -1
- package/registry/layout/double-sidebar.json +1 -1
- package/registry/layout/header.json +1 -1
- package/registry/layout/icon-sidebar.json +1 -1
- package/registry/layout/project-context-shell.json +1 -1
- package/registry/layout/sidebar-nav.json +1 -1
- package/registry/ui/button.json +1 -1
- package/registry/ui/line-tabs.json +1 -1
- package/registry/ui/selectable-pills.json +1 -1
- package/registry/ui/tabs.json +1 -1
package/package.json
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
{
|
|
7
7
|
"path": "components/blocks/component-palette.tsx",
|
|
8
8
|
"type": "registry:block",
|
|
9
|
-
"content": "\"use client\";\n\nimport { useState, useEffect } from \"react\";\nimport { useDraggable } from \"@dnd-kit/core\";\nimport { CSS } from \"@dnd-kit/utilities\";\nimport { cn } from \"../../lib/utils\";\nimport { \n ChevronDown, \n ChevronRight,\n Layout,\n LayoutGrid,\n MessageSquare,\n Megaphone,\n CreditCard,\n User,\n Table,\n List,\n Image,\n Type,\n Video,\n Search,\n Settings,\n LogIn,\n Phone,\n ShoppingCart,\n FileText,\n Square,\n CheckSquare,\n Calendar,\n ToggleLeft,\n CircleDot,\n Hash,\n SlidersHorizontal,\n Tags,\n Star,\n MapPin,\n Users,\n Play,\n Newspaper,\n Building,\n Award,\n Layers,\n} from \"lucide-react\";\nimport { ScrollArea } from \"../ui/scroll-area\";\n\n// Component definitions for the palette\nexport interface PaletteComponent {\n id: string;\n type: string;\n label: string;\n icon: React.ReactNode;\n category: string;\n}\n\nconst paletteComponents: PaletteComponent[] = [\n // =====================\n // PAGE TEMPLATES\n // =====================\n { id: \"page-about\", type: \"PageAbout\", label: \"About\", icon: <Layout className=\"size-4\" />, category: \"Page Templates\" },\n { id: \"page-account\", type: \"PageAccount\", label: \"Account Settings\", icon: <Settings className=\"size-4\" />, category: \"Page Templates\" },\n { id: \"page-admin-portal\", type: \"PageAdminPortal\", label: \"Admin Portal\", icon: <Layout className=\"size-4\" />, category: \"Page Templates\" },\n { id: \"page-centered-profile\", type: \"PageCenteredProfile\", label: \"Centered Profile\", icon: <User className=\"size-4\" />, category: \"Page Templates\" },\n { id: \"page-double-sidebar\", type: \"PageDoubleSidebar\", label: \"Double Sidebar\", icon: <Layout className=\"size-4\" />, category: \"Page Templates\" },\n { id: \"page-icon-sidebar\", type: \"PageIconSidebar\", label: \"Icon Sidebar\", icon: <Layout className=\"size-4\" />, category: \"Page Templates\" },\n { id: \"page-login\", type: \"PageLogin\", label: \"Login / Signup\", icon: <LogIn className=\"size-4\" />, category: \"Page Templates\" },\n { id: \"page-menu-sections\", type: \"PageMenuSections\", label: \"Menu Sections\", icon: <List className=\"size-4\" />, category: \"Page Templates\" },\n { id: \"page-messenger\", type: \"PageMessenger\", label: \"Messenger\", icon: <MessageSquare className=\"size-4\" />, category: \"Page Templates\" },\n { id: \"page-mobile-menu\", type: \"PageMobileMenu\", label: \"Mobile Menu\", icon: <Layout className=\"size-4\" />, category: \"Page Templates\" },\n { id: \"page-multistep-progressbar\", type: \"PageMultistepProgressbar\", label: \"Multistep + Progress\", icon: <List className=\"size-4\" />, category: \"Page Templates\" },\n { id: \"page-multistep-sidebar\", type: \"PageMultistepSidebar\", label: \"Multistep + Sidebar\", icon: <List className=\"size-4\" />, category: \"Page Templates\" },\n { id: \"page-pricing\", type: \"PagePricing\", label: \"Pricing\", icon: <CreditCard className=\"size-4\" />, category: \"Page Templates\" },\n { id: \"page-product-homepage\", type: \"PageProductHomepage\", label: \"Product Homepage\", icon: <Layout className=\"size-4\" />, category: \"Page Templates\" },\n { id: \"page-reset-password\", type: \"PageResetPassword\", label: \"Reset Password\", icon: <LogIn className=\"size-4\" />, category: \"Page Templates\" },\n { id: \"page-search-bar\", type: \"PageSearchBar\", label: \"Search Bar\", icon: <Search className=\"size-4\" />, category: \"Page Templates\" },\n { id: \"page-sidebar-profile\", type: \"PageSidebarProfile\", label: \"Sidebar Profile\", icon: <User className=\"size-4\" />, category: \"Page Templates\" },\n { id: \"page-standard\", type: \"PageStandard\", label: \"Standard Page\", icon: <Layout className=\"size-4\" />, category: \"Page Templates\" },\n { id: \"page-standard-multistep\", type: \"PageStandardMultistep\", label: \"Standard Multistep\", icon: <List className=\"size-4\" />, category: \"Page Templates\" },\n { id: \"page-standard-search\", type: \"PageStandardSearch\", label: \"Standard Search\", icon: <Search className=\"size-4\" />, category: \"Page Templates\" },\n { id: \"page-vertical-multistep\", type: \"PageVerticalMultistep\", label: \"Vertical Multistep\", icon: <List className=\"size-4\" />, category: \"Page Templates\" },\n { id: \"page-video-chat\", type: \"PageVideoChat\", label: \"Video Chat\", icon: <Video className=\"size-4\" />, category: \"Page Templates\" },\n { id: \"page-video-list\", type: \"PageVideoList\", label: \"Video List\", icon: <Play className=\"size-4\" />, category: \"Page Templates\" },\n\n // =====================\n // BLOCKS\n // =====================\n // Data & Tables\n { id: \"standard-data-table\", type: \"StandardDataTable\", label: \"Data Table\", icon: <Table className=\"size-4\" />, category: \"Blocks\" },\n \n // Cards & Profiles\n { id: \"profile-card\", type: \"ProfileCard\", label: \"Profile Card\", icon: <User className=\"size-4\" />, category: \"Blocks\" },\n { id: \"sidebar-profile-card\", type: \"SidebarProfileCard\", label: \"Sidebar Profile Card\", icon: <User className=\"size-4\" />, category: \"Blocks\" },\n { id: \"profile-info-cards\", type: \"ProfileInfoCards\", label: \"Profile Info Cards\", icon: <LayoutGrid className=\"size-4\" />, category: \"Blocks\" },\n { id: \"sidebar-cards\", type: \"SidebarCards\", label: \"Sidebar Cards\", icon: <Layers className=\"size-4\" />, category: \"Blocks\" },\n { id: \"credit-card-display\", type: \"CreditCardDisplay\", label: \"Credit Card Display\", icon: <CreditCard className=\"size-4\" />, category: \"Blocks\" },\n \n // Navigation & Progress\n { id: \"step-tracker\", type: \"StepTracker\", label: \"Step Tracker\", icon: <List className=\"size-4\" />, category: \"Blocks\" },\n { id: \"vertical-step-tracker\", type: \"VerticalStepTracker\", label: \"Vertical Step Tracker\", icon: <List className=\"size-4\" />, category: \"Blocks\" },\n { id: \"progress-bar\", type: \"ProgressBar\", label: \"Progress Bar\", icon: <SlidersHorizontal className=\"size-4\" />, category: \"Blocks\" },\n { id: \"pill-tabs\", type: \"PillTabs\", label: \"Pill Tabs\", icon: <LayoutGrid className=\"size-4\" />, category: \"Blocks\" },\n { id: \"mobile-bottom-nav\", type: \"MobileBottomNav\", label: \"Mobile Bottom Nav\", icon: <Layout className=\"size-4\" />, category: \"Blocks\" },\n \n // Banners & Headers\n { id: \"flair-banner\", type: \"FlairBanner\", label: \"Flair Banner\", icon: <Type className=\"size-4\" />, category: \"Blocks\" },\n { id: \"gradient-banner\", type: \"GradientBanner\", label: \"Gradient Banner\", icon: <Type className=\"size-4\" />, category: \"Blocks\" },\n { id: \"page-header-section\", type: \"PageHeaderSection\", label: \"Page Header Section\", icon: <FileText className=\"size-4\" />, category: \"Blocks\" },\n \n // Chat & Messaging\n { id: \"messenger-sidebar\", type: \"MessengerSidebar\", label: \"Messenger Sidebar\", icon: <MessageSquare className=\"size-4\" />, category: \"Blocks\" },\n { id: \"chat-message\", type: \"ChatMessage\", label: \"Chat Message\", icon: <MessageSquare className=\"size-4\" />, category: \"Blocks\" },\n \n // Video\n { id: \"video-chat-controls\", type: \"VideoChatControls\", label: \"Video Chat Controls\", icon: <Video className=\"size-4\" />, category: \"Blocks\" },\n { id: \"webcam-preview\", type: \"WebcamPreview\", label: \"Webcam Preview\", icon: <Video className=\"size-4\" />, category: \"Blocks\" },\n { id: \"participant-list\", type: \"ParticipantList\", label: \"Participant List\", icon: <Users className=\"size-4\" />, category: \"Blocks\" },\n { id: \"video-content-section\", type: \"VideoContentSection\", label: \"Video Content Section\", icon: <Play className=\"size-4\" />, category: \"Blocks\" },\n { id: \"video-playlist\", type: \"VideoPlaylist\", label: \"Video Playlist\", icon: <List className=\"size-4\" />, category: \"Blocks\" },\n \n // Search & Filters\n { id: \"search-bar\", type: \"SearchBar\", label: \"Search Bar\", icon: <Search className=\"size-4\" />, category: \"Blocks\" },\n { id: \"filter-popover\", type: \"FilterPopover\", label: \"Filter Popover\", icon: <SlidersHorizontal className=\"size-4\" />, category: \"Blocks\" },\n \n // Forms & Settings\n { id: \"settings-list-row\", type: \"SettingsListRow\", label: \"Settings List Row\", icon: <Settings className=\"size-4\" />, category: \"Blocks\" },\n { id: \"profile-image-uploader\", type: \"ProfileImageUploader\", label: \"Profile Image Uploader\", icon: <Image className=\"size-4\" />, category: \"Blocks\" },\n { id: \"login-branding-panel\", type: \"LoginBrandingPanel\", label: \"Login Branding Panel\", icon: <Layout className=\"size-4\" />, category: \"Blocks\" },\n \n // Marketing - Heroes\n { id: \"hero-section\", type: \"HeroSection\", label: \"Hero Section\", icon: <Image className=\"size-4\" />, category: \"Blocks\" },\n { id: \"hero-dark-with-image\", type: \"HeroDarkWithImage\", label: \"Hero Dark + Image\", icon: <Image className=\"size-4\" />, category: \"Blocks\" },\n { id: \"centered-hero\", type: \"CenteredHero\", label: \"Centered Hero\", icon: <Image className=\"size-4\" />, category: \"Blocks\" },\n \n // Marketing - Social Proof\n { id: \"testimonial-carousel\", type: \"TestimonialCarousel\", label: \"Testimonial Carousel\", icon: <MessageSquare className=\"size-4\" />, category: \"Blocks\" },\n { id: \"reviews-grid\", type: \"ReviewsGrid\", label: \"Reviews Grid\", icon: <Star className=\"size-4\" />, category: \"Blocks\" },\n { id: \"social-proof\", type: \"SocialProof\", label: \"Social Proof (Logos)\", icon: <Award className=\"size-4\" />, category: \"Blocks\" },\n { id: \"metrics-section\", type: \"MetricsSection\", label: \"Metrics Section\", icon: <Hash className=\"size-4\" />, category: \"Blocks\" },\n \n // Marketing - Features\n { id: \"feature-with-image\", type: \"FeatureWithImage\", label: \"Feature + Image\", icon: <Image className=\"size-4\" />, category: \"Blocks\" },\n { id: \"core-values-grid\", type: \"CoreValuesGrid\", label: \"Core Values Grid\", icon: <LayoutGrid className=\"size-4\" />, category: \"Blocks\" },\n { id: \"destination-cards\", type: \"DestinationCards\", label: \"Destination Cards\", icon: <MapPin className=\"size-4\" />, category: \"Blocks\" },\n \n // Marketing - Team\n { id: \"team-cards-grid\", type: \"TeamCardsGrid\", label: \"Team Cards Grid\", icon: <Users className=\"size-4\" />, category: \"Blocks\" },\n { id: \"team-circular-grid\", type: \"TeamCircularGrid\", label: \"Team Circular Grid\", icon: <Users className=\"size-4\" />, category: \"Blocks\" },\n \n // Marketing - CTA & Footer\n { id: \"cta-banner\", type: \"CtaBanner\", label: \"CTA Banner\", icon: <Megaphone className=\"size-4\" />, category: \"Blocks\" },\n { id: \"footer-navbar\", type: \"FooterNavbar\", label: \"Footer Navbar\", icon: <Layout className=\"size-4\" />, category: \"Blocks\" },\n \n // Marketing - Other\n { id: \"featured-news-cards\", type: \"FeaturedNewsCards\", label: \"Featured News Cards\", icon: <Newspaper className=\"size-4\" />, category: \"Blocks\" },\n { id: \"office-locations\", type: \"OfficeLocations\", label: \"Office Locations\", icon: <Building className=\"size-4\" />, category: \"Blocks\" },\n \n // Pricing\n { id: \"pricing-cards\", type: \"PricingCards\", label: \"Pricing Cards\", icon: <CreditCard className=\"size-4\" />, category: \"Blocks\" },\n { id: \"faq-accordion\", type: \"FaqAccordion\", label: \"FAQ Accordion\", icon: <List className=\"size-4\" />, category: \"Blocks\" },\n { id: \"features-comparison\", type: \"FeaturesComparison\", label: \"Features Comparison\", icon: <Table className=\"size-4\" />, category: \"Blocks\" },\n\n // =====================\n // COMPONENTS (UI Primitives)\n // =====================\n { id: \"button\", type: \"Button\", label: \"Button\", icon: <Square className=\"size-4\" />, category: \"Components\" },\n { id: \"checkbox\", type: \"Checkbox\", label: \"Checkbox\", icon: <CheckSquare className=\"size-4\" />, category: \"Components\" },\n { id: \"date-input\", type: \"DateInput\", label: \"Date Input\", icon: <Calendar className=\"size-4\" />, category: \"Components\" },\n { id: \"input\", type: \"Input\", label: \"Text Input\", icon: <Type className=\"size-4\" />, category: \"Components\" },\n { id: \"select\", type: \"Select\", label: \"Select\", icon: <List className=\"size-4\" />, category: \"Components\" },\n { id: \"switch\", type: \"Switch\", label: \"Switch\", icon: <ToggleLeft className=\"size-4\" />, category: \"Components\" },\n { id: \"radio-group\", type: \"RadioGroup\", label: \"Radio Group\", icon: <CircleDot className=\"size-4\" />, category: \"Components\" },\n { id: \"multiselect-tags\", type: \"MultiselectTags\", label: \"Multiselect Tags\", icon: <Tags className=\"size-4\" />, category: \"Components\" },\n { id: \"avatar\", type: \"Avatar\", label: \"Avatar\", icon: <User className=\"size-4\" />, category: \"Components\" },\n { id: \"badge\", type: \"Badge\", label: \"Badge\", icon: <Award className=\"size-4\" />, category: \"Components\" },\n];\n\n// Group components by category\nconst componentsByCategory = paletteComponents.reduce((acc, comp) => {\n if (!acc[comp.category]) {\n acc[comp.category] = [];\n }\n acc[comp.category].push(comp);\n return acc;\n}, {} as Record<string, PaletteComponent[]>);\n\n// Define category order\nconst categoryOrder = [\"Page Templates\", \"Blocks\", \"Components\"];\n\ninterface DraggableComponentProps {\n component: PaletteComponent;\n}\n\nfunction DraggableComponent({ component }: DraggableComponentProps) {\n const [isMounted, setIsMounted] = useState(false);\n \n const { attributes, listeners, setNodeRef, transform, isDragging } = useDraggable({\n id: component.id,\n data: {\n type: component.type,\n label: component.label,\n },\n });\n\n // Only apply dnd-kit attributes after hydration to prevent mismatch\n useEffect(() => {\n setIsMounted(true);\n }, []);\n\n const style = {\n transform: CSS.Transform.toString(transform),\n opacity: isDragging ? 0.5 : 1,\n };\n\n return (\n <div\n ref={setNodeRef}\n style={style}\n // Only spread dnd-kit attributes after client-side mount to avoid hydration mismatch\n {...(isMounted ? attributes : {})}\n {...(isMounted ? listeners : {})}\n className={cn(\n \"flex items-center gap-3 px-3 py-2.5 rounded-md cursor-grab active:cursor-grabbing\",\n \"border border-transparent\",\n \"hover:bg-[var(--canvas-surface)] hover:border-[var(--canvas-border)]\",\n \"transition-colors group\"\n )}\n >\n <div \n className=\"flex items-center justify-center size-8 rounded-md bg-[var(--canvas-surface-brand)] text-[var(--canvas-primary)]\"\n >\n {component.icon}\n </div>\n <span \n className=\"text-[var(--canvas-text)]\"\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n }}\n >\n {component.label}\n </span>\n </div>\n );\n}\n\ninterface CategorySectionProps {\n category: string;\n components: PaletteComponent[];\n defaultExpanded?: boolean;\n}\n\nfunction CategorySection({ category, components, defaultExpanded = true }: CategorySectionProps) {\n const [isExpanded, setIsExpanded] = useState(defaultExpanded);\n\n return (\n <div className=\"mb-2\">\n <button\n onClick={() => setIsExpanded(!isExpanded)}\n className=\"flex items-center gap-2 w-full px-3 py-2 text-left hover:bg-[var(--canvas-surface)] rounded-md transition-colors\"\n >\n {isExpanded ? (\n <ChevronDown className=\"size-4 text-[var(--canvas-text-muted)]\" />\n ) : (\n <ChevronRight className=\"size-4 text-[var(--canvas-text-muted)]\" />\n )}\n <span \n className=\"font-semibold uppercase tracking-wider text-[var(--canvas-text-muted)]\"\n style={{ fontSize: \"var(--typo-body-xs-size)\" }}\n >\n {category}\n </span>\n <span className=\"ml-auto text-[var(--canvas-text-placeholder)]\" style={{ fontSize: \"var(--typo-body-xs-size)\" }}>\n {components.length}\n </span>\n </button>\n \n {isExpanded && (\n <div className=\"mt-1 ml-2 space-y-0.5\">\n {components.map((component) => (\n <DraggableComponent key={component.id} component={component} />\n ))}\n </div>\n )}\n </div>\n );\n}\n\ninterface ComponentPaletteProps {\n className?: string;\n}\n\n/**\n * Component Palette - Sidebar with draggable components\n * \n * Features:\n * - Organized by category (Page Templates, Blocks, Components)\n * - Collapsible sections\n * - Drag to add to canvas\n */\nexport function ComponentPalette({ className }: ComponentPaletteProps) {\n return (\n <aside\n className={cn(\n \"w-[280px] h-full flex flex-col\",\n \"bg-[var(--canvas-background)] border-r border-[var(--canvas-border)]\",\n className\n )}\n >\n {/* Header */}\n <div className=\"px-4 py-4 border-b border-[var(--canvas-border)]\">\n <h2 \n className=\"font-semibold text-[var(--canvas-text)]\"\n style={{\n fontFamily: \"var(--typo-body-m-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n }}\n >\n Components\n </h2>\n <p \n className=\"text-[var(--canvas-text-muted)] mt-1\"\n style={{ fontSize: \"var(--typo-body-xs-size)\" }}\n >\n Drag components onto the canvas\n </p>\n </div>\n\n {/* Component List */}\n <ScrollArea className=\"flex-1\">\n <div className=\"p-3\">\n {categoryOrder.map((category) => {\n const components = componentsByCategory[category];\n if (!components) return null;\n return (\n <CategorySection\n key={category}\n category={category}\n components={components}\n defaultExpanded={category !== \"Page Templates\"} // Collapse templates by default\n />\n );\n })}\n </div>\n </ScrollArea>\n\n {/* Footer hint */}\n <div className=\"px-4 py-3 border-t border-[var(--canvas-border)] bg-[var(--canvas-surface)]\">\n <p className=\"text-[var(--canvas-text-placeholder)]\" style={{ fontSize: \"var(--typo-body-xs-size)\" }}>\n Tip: Press <kbd className=\"px-1.5 py-0.5 bg-[var(--canvas-background)] rounded border border-[var(--canvas-border)]\" style={{ fontSize: \"10px\" }}>Delete</kbd> to remove selected\n </p>\n </div>\n </aside>\n );\n}\n"
|
|
9
|
+
"content": "\"use client\";\n\nimport { useState, useEffect } from \"react\";\nimport { useDraggable } from \"@dnd-kit/core\";\nimport { CSS } from \"@dnd-kit/utilities\";\nimport { cn } from \"../../lib/utils\";\nimport { \n ChevronDown, \n ChevronRight,\n Layout,\n LayoutGrid,\n MessageSquare,\n Megaphone,\n CreditCard,\n User,\n Table,\n List,\n Image,\n Type,\n Video,\n Search,\n Settings,\n LogIn,\n Phone,\n ShoppingCart,\n FileText,\n Square,\n CheckSquare,\n Calendar,\n ToggleLeft,\n CircleDot,\n Hash,\n SlidersHorizontal,\n Tags,\n Star,\n MapPin,\n Users,\n Play,\n Newspaper,\n Building,\n Award,\n Layers,\n} from \"lucide-react\";\nimport { ScrollArea } from \"../ui/scroll-area\";\n\n// Component definitions for the palette\nexport interface PaletteComponent {\n id: string;\n type: string;\n label: string;\n icon: React.ReactNode;\n category: string;\n}\n\nconst paletteComponents: PaletteComponent[] = [\n // =====================\n // PAGE TEMPLATES\n // =====================\n { id: \"page-about\", type: \"PageAbout\", label: \"About\", icon: <Layout className=\"size-4\" />, category: \"Page Templates\" },\n { id: \"page-account\", type: \"PageAccount\", label: \"Account Settings\", icon: <Settings className=\"size-4\" />, category: \"Page Templates\" },\n { id: \"page-admin-portal\", type: \"PageAdminPortal\", label: \"Admin Portal\", icon: <Layout className=\"size-4\" />, category: \"Page Templates\" },\n { id: \"page-centered-profile\", type: \"PageCenteredProfile\", label: \"Centered Profile\", icon: <User className=\"size-4\" />, category: \"Page Templates\" },\n { id: \"page-double-sidebar\", type: \"PageDoubleSidebar\", label: \"Double Sidebar\", icon: <Layout className=\"size-4\" />, category: \"Page Templates\" },\n { id: \"page-icon-sidebar\", type: \"PageIconSidebar\", label: \"Icon Sidebar\", icon: <Layout className=\"size-4\" />, category: \"Page Templates\" },\n { id: \"page-login\", type: \"PageLogin\", label: \"Login / Signup\", icon: <LogIn className=\"size-4\" />, category: \"Page Templates\" },\n { id: \"page-menu-sections\", type: \"PageMenuSections\", label: \"Menu Sections\", icon: <List className=\"size-4\" />, category: \"Page Templates\" },\n { id: \"page-messenger\", type: \"PageMessenger\", label: \"Messenger\", icon: <MessageSquare className=\"size-4\" />, category: \"Page Templates\" },\n { id: \"page-mobile-menu\", type: \"PageMobileMenu\", label: \"Mobile Menu\", icon: <Layout className=\"size-4\" />, category: \"Page Templates\" },\n { id: \"page-multistep-progressbar\", type: \"PageMultistepProgressbar\", label: \"Multistep + Progress\", icon: <List className=\"size-4\" />, category: \"Page Templates\" },\n { id: \"page-multistep-sidebar\", type: \"PageMultistepSidebar\", label: \"Multistep + Sidebar\", icon: <List className=\"size-4\" />, category: \"Page Templates\" },\n { id: \"page-pricing\", type: \"PagePricing\", label: \"Pricing\", icon: <CreditCard className=\"size-4\" />, category: \"Page Templates\" },\n { id: \"page-product-homepage\", type: \"PageProductHomepage\", label: \"Product Homepage\", icon: <Layout className=\"size-4\" />, category: \"Page Templates\" },\n { id: \"page-reset-password\", type: \"PageResetPassword\", label: \"Reset Password\", icon: <LogIn className=\"size-4\" />, category: \"Page Templates\" },\n { id: \"page-search-bar\", type: \"PageSearchBar\", label: \"Search Bar\", icon: <Search className=\"size-4\" />, category: \"Page Templates\" },\n { id: \"page-sidebar-profile\", type: \"PageSidebarProfile\", label: \"Sidebar Profile\", icon: <User className=\"size-4\" />, category: \"Page Templates\" },\n { id: \"page-standard\", type: \"PageStandard\", label: \"Standard Page\", icon: <Layout className=\"size-4\" />, category: \"Page Templates\" },\n { id: \"page-standard-multistep\", type: \"PageStandardMultistep\", label: \"Standard Multistep\", icon: <List className=\"size-4\" />, category: \"Page Templates\" },\n { id: \"page-standard-search\", type: \"PageStandardSearch\", label: \"Standard Search\", icon: <Search className=\"size-4\" />, category: \"Page Templates\" },\n { id: \"page-vertical-multistep\", type: \"PageVerticalMultistep\", label: \"Vertical Multistep\", icon: <List className=\"size-4\" />, category: \"Page Templates\" },\n { id: \"page-video-chat\", type: \"PageVideoChat\", label: \"Video Chat\", icon: <Video className=\"size-4\" />, category: \"Page Templates\" },\n { id: \"page-video-list\", type: \"PageVideoList\", label: \"Video List\", icon: <Play className=\"size-4\" />, category: \"Page Templates\" },\n\n // =====================\n // BLOCKS\n // =====================\n // Data & Tables\n { id: \"standard-data-table\", type: \"StandardDataTable\", label: \"Data Table\", icon: <Table className=\"size-4\" />, category: \"Blocks\" },\n \n // Cards & Profiles\n { id: \"profile-card\", type: \"ProfileCard\", label: \"Profile Card\", icon: <User className=\"size-4\" />, category: \"Blocks\" },\n { id: \"sidebar-profile-card\", type: \"SidebarProfileCard\", label: \"Sidebar Profile Card\", icon: <User className=\"size-4\" />, category: \"Blocks\" },\n { id: \"profile-info-cards\", type: \"ProfileInfoCards\", label: \"Profile Info Cards\", icon: <LayoutGrid className=\"size-4\" />, category: \"Blocks\" },\n { id: \"sidebar-cards\", type: \"SidebarCards\", label: \"Sidebar Cards\", icon: <Layers className=\"size-4\" />, category: \"Blocks\" },\n { id: \"credit-card-display\", type: \"CreditCardDisplay\", label: \"Credit Card Display\", icon: <CreditCard className=\"size-4\" />, category: \"Blocks\" },\n \n // Navigation & Progress\n { id: \"step-tracker\", type: \"StepTracker\", label: \"Step Tracker\", icon: <List className=\"size-4\" />, category: \"Blocks\" },\n { id: \"vertical-step-tracker\", type: \"VerticalStepTracker\", label: \"Vertical Step Tracker\", icon: <List className=\"size-4\" />, category: \"Blocks\" },\n { id: \"progress-bar\", type: \"ProgressBar\", label: \"Progress Bar\", icon: <SlidersHorizontal className=\"size-4\" />, category: \"Blocks\" },\n { id: \"pill-tabs\", type: \"PillTabs\", label: \"Pill Tabs\", icon: <LayoutGrid className=\"size-4\" />, category: \"Blocks\" },\n { id: \"mobile-bottom-nav\", type: \"MobileBottomNav\", label: \"Mobile Bottom Nav\", icon: <Layout className=\"size-4\" />, category: \"Blocks\" },\n \n // Banners & Headers\n { id: \"flair-banner\", type: \"FlairBanner\", label: \"Flair Banner\", icon: <Type className=\"size-4\" />, category: \"Blocks\" },\n { id: \"gradient-banner\", type: \"GradientBanner\", label: \"Gradient Banner\", icon: <Type className=\"size-4\" />, category: \"Blocks\" },\n { id: \"page-header-section\", type: \"PageHeaderSection\", label: \"Page Header Section\", icon: <FileText className=\"size-4\" />, category: \"Blocks\" },\n \n // Chat & Messaging\n { id: \"messenger-sidebar\", type: \"MessengerSidebar\", label: \"Messenger Sidebar\", icon: <MessageSquare className=\"size-4\" />, category: \"Blocks\" },\n { id: \"chat-message\", type: \"ChatMessage\", label: \"Chat Message\", icon: <MessageSquare className=\"size-4\" />, category: \"Blocks\" },\n \n // Video\n { id: \"video-chat-controls\", type: \"VideoChatControls\", label: \"Video Chat Controls\", icon: <Video className=\"size-4\" />, category: \"Blocks\" },\n { id: \"webcam-preview\", type: \"WebcamPreview\", label: \"Webcam Preview\", icon: <Video className=\"size-4\" />, category: \"Blocks\" },\n { id: \"participant-list\", type: \"ParticipantList\", label: \"Participant List\", icon: <Users className=\"size-4\" />, category: \"Blocks\" },\n { id: \"video-content-section\", type: \"VideoContentSection\", label: \"Video Content Section\", icon: <Play className=\"size-4\" />, category: \"Blocks\" },\n { id: \"video-playlist\", type: \"VideoPlaylist\", label: \"Video Playlist\", icon: <List className=\"size-4\" />, category: \"Blocks\" },\n \n // Search & Filters\n { id: \"search-bar\", type: \"SearchBar\", label: \"Search Bar\", icon: <Search className=\"size-4\" />, category: \"Blocks\" },\n { id: \"filter-popover\", type: \"FilterPopover\", label: \"Filter Popover\", icon: <SlidersHorizontal className=\"size-4\" />, category: \"Blocks\" },\n \n // Forms & Settings\n { id: \"settings-list-row\", type: \"SettingsListRow\", label: \"Settings List Row\", icon: <Settings className=\"size-4\" />, category: \"Blocks\" },\n { id: \"profile-image-uploader\", type: \"ProfileImageUploader\", label: \"Profile Image Uploader\", icon: <Image className=\"size-4\" />, category: \"Blocks\" },\n { id: \"login-branding-panel\", type: \"LoginBrandingPanel\", label: \"Login Branding Panel\", icon: <Layout className=\"size-4\" />, category: \"Blocks\" },\n \n // Marketing - Heroes\n { id: \"hero-section\", type: \"HeroSection\", label: \"Hero Section\", icon: <Image className=\"size-4\" />, category: \"Blocks\" },\n { id: \"hero-dark-with-image\", type: \"HeroDarkWithImage\", label: \"Hero Dark + Image\", icon: <Image className=\"size-4\" />, category: \"Blocks\" },\n { id: \"centered-hero\", type: \"CenteredHero\", label: \"Centered Hero\", icon: <Image className=\"size-4\" />, category: \"Blocks\" },\n \n // Marketing - Social Proof\n { id: \"testimonial-carousel\", type: \"TestimonialCarousel\", label: \"Testimonial Carousel\", icon: <MessageSquare className=\"size-4\" />, category: \"Blocks\" },\n { id: \"reviews-grid\", type: \"ReviewsGrid\", label: \"Reviews Grid\", icon: <Star className=\"size-4\" />, category: \"Blocks\" },\n { id: \"social-proof\", type: \"SocialProof\", label: \"Social Proof (Logos)\", icon: <Award className=\"size-4\" />, category: \"Blocks\" },\n { id: \"metrics-section\", type: \"MetricsSection\", label: \"Metrics Section\", icon: <Hash className=\"size-4\" />, category: \"Blocks\" },\n \n // Marketing - Features\n { id: \"feature-with-image\", type: \"FeatureWithImage\", label: \"Feature + Image\", icon: <Image className=\"size-4\" />, category: \"Blocks\" },\n { id: \"core-values-grid\", type: \"CoreValuesGrid\", label: \"Core Values Grid\", icon: <LayoutGrid className=\"size-4\" />, category: \"Blocks\" },\n { id: \"destination-cards\", type: \"DestinationCards\", label: \"Destination Cards\", icon: <MapPin className=\"size-4\" />, category: \"Blocks\" },\n \n // Marketing - Team\n { id: \"team-cards-grid\", type: \"TeamCardsGrid\", label: \"Team Cards Grid\", icon: <Users className=\"size-4\" />, category: \"Blocks\" },\n { id: \"team-circular-grid\", type: \"TeamCircularGrid\", label: \"Team Circular Grid\", icon: <Users className=\"size-4\" />, category: \"Blocks\" },\n \n // Marketing - CTA & Footer\n { id: \"cta-banner\", type: \"CtaBanner\", label: \"CTA Banner\", icon: <Megaphone className=\"size-4\" />, category: \"Blocks\" },\n { id: \"footer-navbar\", type: \"FooterNavbar\", label: \"Footer Navbar\", icon: <Layout className=\"size-4\" />, category: \"Blocks\" },\n \n // Marketing - Other\n { id: \"featured-news-cards\", type: \"FeaturedNewsCards\", label: \"Featured News Cards\", icon: <Newspaper className=\"size-4\" />, category: \"Blocks\" },\n { id: \"office-locations\", type: \"OfficeLocations\", label: \"Office Locations\", icon: <Building className=\"size-4\" />, category: \"Blocks\" },\n \n // Pricing\n { id: \"pricing-cards\", type: \"PricingCards\", label: \"Pricing Cards\", icon: <CreditCard className=\"size-4\" />, category: \"Blocks\" },\n { id: \"faq-accordion\", type: \"FaqAccordion\", label: \"FAQ Accordion\", icon: <List className=\"size-4\" />, category: \"Blocks\" },\n { id: \"features-comparison\", type: \"FeaturesComparison\", label: \"Features Comparison\", icon: <Table className=\"size-4\" />, category: \"Blocks\" },\n\n // =====================\n // COMPONENTS (UI Primitives)\n // =====================\n { id: \"button\", type: \"Button\", label: \"Button\", icon: <Square className=\"size-4\" />, category: \"Components\" },\n { id: \"checkbox\", type: \"Checkbox\", label: \"Checkbox\", icon: <CheckSquare className=\"size-4\" />, category: \"Components\" },\n { id: \"date-input\", type: \"DateInput\", label: \"Date Input\", icon: <Calendar className=\"size-4\" />, category: \"Components\" },\n { id: \"input\", type: \"Input\", label: \"Text Input\", icon: <Type className=\"size-4\" />, category: \"Components\" },\n { id: \"select\", type: \"Select\", label: \"Select\", icon: <List className=\"size-4\" />, category: \"Components\" },\n { id: \"switch\", type: \"Switch\", label: \"Switch\", icon: <ToggleLeft className=\"size-4\" />, category: \"Components\" },\n { id: \"radio-group\", type: \"RadioGroup\", label: \"Radio Group\", icon: <CircleDot className=\"size-4\" />, category: \"Components\" },\n { id: \"multiselect-tags\", type: \"MultiselectTags\", label: \"Multiselect Tags\", icon: <Tags className=\"size-4\" />, category: \"Components\" },\n { id: \"avatar\", type: \"Avatar\", label: \"Avatar\", icon: <User className=\"size-4\" />, category: \"Components\" },\n { id: \"badge\", type: \"Badge\", label: \"Badge\", icon: <Award className=\"size-4\" />, category: \"Components\" },\n];\n\n// Group components by category\nconst componentsByCategory = paletteComponents.reduce((acc, comp) => {\n if (!acc[comp.category]) {\n acc[comp.category] = [];\n }\n acc[comp.category].push(comp);\n return acc;\n}, {} as Record<string, PaletteComponent[]>);\n\n// Define category order\nconst categoryOrder = [\"Page Templates\", \"Blocks\", \"Components\"];\n\ninterface DraggableComponentProps {\n component: PaletteComponent;\n}\n\nfunction DraggableComponent({ component }: DraggableComponentProps) {\n const [isMounted, setIsMounted] = useState(false);\n \n const { attributes, listeners, setNodeRef, transform, isDragging } = useDraggable({\n id: component.id,\n data: {\n type: component.type,\n label: component.label,\n },\n });\n\n // Only apply dnd-kit attributes after hydration to prevent mismatch\n useEffect(() => {\n setIsMounted(true);\n }, []);\n\n const style = {\n transform: CSS.Transform.toString(transform),\n opacity: isDragging ? 0.5 : 1,\n };\n\n return (\n <div\n ref={setNodeRef}\n style={style}\n // Only spread dnd-kit attributes after client-side mount to avoid hydration mismatch\n {...(isMounted ? attributes : {})}\n {...(isMounted ? listeners : {})}\n className={cn(\n \"flex items-center gap-3 px-3 py-2.5 rounded-md cursor-grab active:cursor-grabbing\",\n \"border border-transparent\",\n \"hover:bg-[var(--canvas-surface)] hover:border-[var(--canvas-border)]\",\n \"transition-colors group\"\n )}\n >\n <div \n className=\"flex items-center justify-center size-8 rounded-md bg-[var(--canvas-surface-brand)] text-[var(--canvas-primary)]\"\n >\n {component.icon}\n </div>\n <span \n className=\"text-[var(--canvas-text)]\"\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n }}\n >\n {component.label}\n </span>\n </div>\n );\n}\n\ninterface CategorySectionProps {\n category: string;\n components: PaletteComponent[];\n defaultExpanded?: boolean;\n}\n\nfunction CategorySection({ category, components, defaultExpanded = true }: CategorySectionProps) {\n const [isExpanded, setIsExpanded] = useState(defaultExpanded);\n\n return (\n <div className=\"mb-2\">\n <button\n onClick={() => setIsExpanded(!isExpanded)}\n className=\"cursor-pointer flex items-center gap-2 w-full px-3 py-2 text-left hover:bg-[var(--canvas-surface)] rounded-md transition-colors\"\n >\n {isExpanded ? (\n <ChevronDown className=\"size-4 text-[var(--canvas-text-muted)]\" />\n ) : (\n <ChevronRight className=\"size-4 text-[var(--canvas-text-muted)]\" />\n )}\n <span \n className=\"font-semibold uppercase tracking-wider text-[var(--canvas-text-muted)]\"\n style={{ fontSize: \"var(--typo-body-xs-size)\" }}\n >\n {category}\n </span>\n <span className=\"ml-auto text-[var(--canvas-text-placeholder)]\" style={{ fontSize: \"var(--typo-body-xs-size)\" }}>\n {components.length}\n </span>\n </button>\n \n {isExpanded && (\n <div className=\"mt-1 ml-2 space-y-0.5\">\n {components.map((component) => (\n <DraggableComponent key={component.id} component={component} />\n ))}\n </div>\n )}\n </div>\n );\n}\n\ninterface ComponentPaletteProps {\n className?: string;\n}\n\n/**\n * Component Palette - Sidebar with draggable components\n * \n * Features:\n * - Organized by category (Page Templates, Blocks, Components)\n * - Collapsible sections\n * - Drag to add to canvas\n */\nexport function ComponentPalette({ className }: ComponentPaletteProps) {\n return (\n <aside\n className={cn(\n \"w-[280px] h-full flex flex-col\",\n \"bg-[var(--canvas-background)] border-r border-[var(--canvas-border)]\",\n className\n )}\n >\n {/* Header */}\n <div className=\"px-4 py-4 border-b border-[var(--canvas-border)]\">\n <h2 \n className=\"font-semibold text-[var(--canvas-text)]\"\n style={{\n fontFamily: \"var(--typo-body-m-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n }}\n >\n Components\n </h2>\n <p \n className=\"text-[var(--canvas-text-muted)] mt-1\"\n style={{ fontSize: \"var(--typo-body-xs-size)\" }}\n >\n Drag components onto the canvas\n </p>\n </div>\n\n {/* Component List */}\n <ScrollArea className=\"flex-1\">\n <div className=\"p-3\">\n {categoryOrder.map((category) => {\n const components = componentsByCategory[category];\n if (!components) return null;\n return (\n <CategorySection\n key={category}\n category={category}\n components={components}\n defaultExpanded={category !== \"Page Templates\"} // Collapse templates by default\n />\n );\n })}\n </div>\n </ScrollArea>\n\n {/* Footer hint */}\n <div className=\"px-4 py-3 border-t border-[var(--canvas-border)] bg-[var(--canvas-surface)]\">\n <p className=\"text-[var(--canvas-text-placeholder)]\" style={{ fontSize: \"var(--typo-body-xs-size)\" }}>\n Tip: Press <kbd className=\"px-1.5 py-0.5 bg-[var(--canvas-background)] rounded border border-[var(--canvas-border)]\" style={{ fontSize: \"10px\" }}>Delete</kbd> to remove selected\n </p>\n </div>\n </aside>\n );\n}\n"
|
|
10
10
|
}
|
|
11
11
|
],
|
|
12
12
|
"dependencies": [
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
{
|
|
7
7
|
"path": "components/blocks/component-search.tsx",
|
|
8
8
|
"type": "registry:block",
|
|
9
|
-
"content": "\"use client\";\n\nimport { useState, useMemo } from \"react\";\nimport { Search, X, ChevronDown, ChevronUp } from \"lucide-react\";\nimport { cn } from \"../../lib/utils\";\nimport {\n layoutShells,\n blocks,\n marketingBlocks,\n pricingBlocks,\n videoBlocks,\n pageTemplates,\n} from \"../../lib/component-registry\";\n\n// ═══════════════════════════════════════════════════════════\n// TYPES\n// ═══════════════════════════════════════════════════════════\n\nexport interface ComponentOption {\n id: string;\n name: string;\n category: string;\n path: string;\n description: string;\n}\n\ninterface ComponentSearchProps {\n selectedComponents: ComponentOption[];\n onSelectionChange: (components: ComponentOption[]) => void;\n className?: string;\n}\n\n// ═══════════════════════════════════════════════════════════\n// BUILD COMPONENT OPTIONS FROM REGISTRY\n// ═══════════════════════════════════════════════════════════\n\nfunction buildComponentOptions(): ComponentOption[] {\n const options: ComponentOption[] = [];\n\n // Layout Shells\n Object.entries(layoutShells).forEach(([name, config]) => {\n options.push({\n id: `shell-${name}`,\n name,\n category: \"Layout Shells\",\n path: config.path,\n description: config.description,\n });\n });\n\n // Blocks\n Object.entries(blocks).forEach(([name, config]) => {\n options.push({\n id: `block-${name}`,\n name,\n category: \"Blocks\",\n path: config.path,\n description: config.description,\n });\n });\n\n // Marketing Blocks\n Object.entries(marketingBlocks).forEach(([name, config]) => {\n options.push({\n id: `marketing-${name}`,\n name,\n category: \"Marketing\",\n path: config.path,\n description: config.description,\n });\n });\n\n // Pricing Blocks\n Object.entries(pricingBlocks).forEach(([name, config]) => {\n options.push({\n id: `pricing-${name}`,\n name,\n category: \"Pricing\",\n path: config.path,\n description: config.description,\n });\n });\n\n // Video Blocks\n Object.entries(videoBlocks).forEach(([name, config]) => {\n options.push({\n id: `video-${name}`,\n name,\n category: \"Video/Media\",\n path: config.path,\n description: config.description,\n });\n });\n\n // Page Templates\n Object.entries(pageTemplates).forEach(([name, config]) => {\n options.push({\n id: `template-${name}`,\n name,\n category: \"Page Templates\",\n path: config.path,\n description: config.description,\n });\n });\n\n return options;\n}\n\nconst ALL_COMPONENTS = buildComponentOptions();\nconst CATEGORIES = [...new Set(ALL_COMPONENTS.map((c) => c.category))];\n\n// ═══════════════════════════════════════════════════════════\n// COMPONENT\n// ═══════════════════════════════════════════════════════════\n\nexport function ComponentSearch({\n selectedComponents,\n onSelectionChange,\n className,\n}: ComponentSearchProps) {\n const [searchQuery, setSearchQuery] = useState(\"\");\n const [isOpen, setIsOpen] = useState(false);\n const [expandedCategories, setExpandedCategories] = useState<Set<string>>(\n new Set(CATEGORIES)\n );\n\n // Filter components based on search\n const filteredComponents = useMemo(() => {\n if (!searchQuery.trim()) return ALL_COMPONENTS;\n\n const query = searchQuery.toLowerCase();\n return ALL_COMPONENTS.filter(\n (c) =>\n c.name.toLowerCase().includes(query) ||\n c.description.toLowerCase().includes(query) ||\n c.category.toLowerCase().includes(query)\n );\n }, [searchQuery]);\n\n // Group by category\n const groupedComponents = useMemo(() => {\n const groups: Record<string, ComponentOption[]> = {};\n filteredComponents.forEach((c) => {\n if (!groups[c.category]) groups[c.category] = [];\n groups[c.category].push(c);\n });\n return groups;\n }, [filteredComponents]);\n\n const toggleCategory = (category: string) => {\n setExpandedCategories((prev) => {\n const next = new Set(prev);\n if (next.has(category)) {\n next.delete(category);\n } else {\n next.add(category);\n }\n return next;\n });\n };\n\n const toggleComponent = (component: ComponentOption) => {\n const isSelected = selectedComponents.some((c) => c.id === component.id);\n if (isSelected) {\n onSelectionChange(selectedComponents.filter((c) => c.id !== component.id));\n } else {\n onSelectionChange([...selectedComponents, component]);\n }\n };\n\n const removeComponent = (componentId: string) => {\n onSelectionChange(selectedComponents.filter((c) => c.id !== componentId));\n };\n\n return (\n <div className={cn(\"space-y-3\", className)}>\n {/* Selected Components Chips */}\n {selectedComponents.length > 0 && (\n <div className=\"flex flex-wrap gap-2\">\n {selectedComponents.map((component) => (\n <div\n key={component.id}\n className=\"flex items-center gap-1.5 px-2.5 py-1 rounded-full bg-[var(--canvas-surface-brand)] text-[var(--canvas-primary)]\"\n style={{ fontSize: \"var(--typo-body-s-size)\" }}\n >\n <span className=\"font-medium\">{component.name}</span>\n <button\n onClick={() => removeComponent(component.id)}\n className=\"p-0.5 rounded-full hover:bg-[var(--canvas-primary)]/20 transition-colors\"\n >\n <X className=\"size-3\" />\n </button>\n </div>\n ))}\n </div>\n )}\n\n {/* Search Input */}\n <div className=\"relative\">\n <div\n className={cn(\n \"flex items-center gap-2 px-3 py-2.5 rounded-lg border bg-[var(--canvas-background)] cursor-text\",\n isOpen\n ? \"border-[var(--canvas-primary)] ring-2 ring-[var(--canvas-primary)]/20\"\n : \"border-[var(--canvas-border)]\"\n )}\n onClick={() => setIsOpen(true)}\n >\n <Search className=\"size-4 text-[var(--canvas-text-muted)]\" />\n <input\n type=\"text\"\n value={searchQuery}\n onChange={(e) => setSearchQuery(e.target.value)}\n onFocus={() => setIsOpen(true)}\n placeholder=\"Search components...\"\n className=\"flex-1 bg-transparent text-[var(--canvas-text)] placeholder:text-[var(--canvas-text-placeholder)] outline-none\"\n style={{ fontSize: \"var(--typo-body-s-size)\" }}\n />\n <span className=\"text-[var(--canvas-text-muted)]\" style={{ fontSize: \"var(--typo-body-xs-size)\" }}>\n {selectedComponents.length} selected\n </span>\n </div>\n\n {/* Dropdown */}\n {isOpen && (\n <>\n {/* Backdrop */}\n <div\n className=\"fixed inset-0 z-10\"\n onClick={() => setIsOpen(false)}\n />\n\n {/* Options List */}\n <div className=\"absolute top-full left-0 right-0 mt-1 z-20 max-h-[400px] overflow-y-auto rounded-lg border border-[var(--canvas-border)] bg-[var(--canvas-background)] shadow-lg\">\n {Object.entries(groupedComponents).map(([category, components]) => (\n <div key={category}>\n {/* Category Header */}\n <button\n onClick={() => toggleCategory(category)}\n className=\"w-full flex items-center justify-between px-3 py-2 bg-[var(--canvas-surface)] border-b border-[var(--canvas-border)] font-semibold text-[var(--canvas-text-muted)] uppercase tracking-wide hover:bg-[var(--canvas-surface-brand)]/50\"\n style={{ fontSize: \"var(--typo-body-xs-size)\" }}\n >\n <span>\n {category} ({components.length})\n </span>\n {expandedCategories.has(category) ? (\n <ChevronUp className=\"size-3\" />\n ) : (\n <ChevronDown className=\"size-3\" />\n )}\n </button>\n\n {/* Components in Category */}\n {expandedCategories.has(category) && (\n <div>\n {components.map((component) => {\n const isSelected = selectedComponents.some(\n (c) => c.id === component.id\n );\n return (\n <button\n key={component.id}\n onClick={() => toggleComponent(component)}\n className={cn(\n \"w-full flex items-start gap-3 px-3 py-2.5 text-left transition-colors border-b border-[var(--canvas-border)]/50 last:border-b-0\",\n isSelected\n ? \"bg-[var(--canvas-surface-brand)]/50\"\n : \"hover:bg-[var(--canvas-surface)]\"\n )}\n >\n {/* Checkbox */}\n <div\n className={cn(\n \"size-4 rounded border mt-0.5 flex items-center justify-center shrink-0\",\n isSelected\n ? \"bg-[var(--canvas-primary)] border-[var(--canvas-primary)]\"\n : \"border-[var(--canvas-border)]\"\n )}\n >\n {isSelected && (\n <svg\n className=\"size-3 text-[var(--canvas-primary-foreground)]\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n strokeWidth={3}\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n d=\"M5 13l4 4L19 7\"\n />\n </svg>\n )}\n </div>\n\n {/* Component Info */}\n <div className=\"flex-1 min-w-0\">\n <div className=\"flex items-center gap-2\">\n <span\n className={cn(\n \"font-medium\",\n isSelected\n ? \"text-[var(--canvas-primary)]\"\n : \"text-[var(--canvas-text)]\"\n )}\n style={{ fontSize: \"var(--typo-body-s-size)\" }}\n >\n {component.name}\n </span>\n <span className=\"text-[var(--canvas-text-placeholder)]\" style={{ fontSize: \"var(--typo-body-xs-size)\" }}>\n {component.path}\n </span>\n </div>\n <p className=\"text-[var(--canvas-text-muted)] mt-0.5 line-clamp-2\" style={{ fontSize: \"var(--typo-body-xs-size)\" }}>\n {component.description}\n </p>\n </div>\n </button>\n );\n })}\n </div>\n )}\n </div>\n ))}\n\n {filteredComponents.length === 0 && (\n <div className=\"px-3 py-6 text-center text-[var(--canvas-text-muted)]\" style={{ fontSize: \"var(--typo-body-s-size)\" }}>\n No components found for \"{searchQuery}\"\n </div>\n )}\n </div>\n </>\n )}\n </div>\n </div>\n );\n}\n"
|
|
9
|
+
"content": "\"use client\";\n\nimport { useState, useMemo } from \"react\";\nimport { Search, X, ChevronDown, ChevronUp } from \"lucide-react\";\nimport { cn } from \"../../lib/utils\";\nimport {\n layoutShells,\n blocks,\n marketingBlocks,\n pricingBlocks,\n videoBlocks,\n pageTemplates,\n} from \"../../lib/component-registry\";\n\n// ═══════════════════════════════════════════════════════════\n// TYPES\n// ═══════════════════════════════════════════════════════════\n\nexport interface ComponentOption {\n id: string;\n name: string;\n category: string;\n path: string;\n description: string;\n}\n\ninterface ComponentSearchProps {\n selectedComponents: ComponentOption[];\n onSelectionChange: (components: ComponentOption[]) => void;\n className?: string;\n}\n\n// ═══════════════════════════════════════════════════════════\n// BUILD COMPONENT OPTIONS FROM REGISTRY\n// ═══════════════════════════════════════════════════════════\n\nfunction buildComponentOptions(): ComponentOption[] {\n const options: ComponentOption[] = [];\n\n // Layout Shells\n Object.entries(layoutShells).forEach(([name, config]) => {\n options.push({\n id: `shell-${name}`,\n name,\n category: \"Layout Shells\",\n path: config.path,\n description: config.description,\n });\n });\n\n // Blocks\n Object.entries(blocks).forEach(([name, config]) => {\n options.push({\n id: `block-${name}`,\n name,\n category: \"Blocks\",\n path: config.path,\n description: config.description,\n });\n });\n\n // Marketing Blocks\n Object.entries(marketingBlocks).forEach(([name, config]) => {\n options.push({\n id: `marketing-${name}`,\n name,\n category: \"Marketing\",\n path: config.path,\n description: config.description,\n });\n });\n\n // Pricing Blocks\n Object.entries(pricingBlocks).forEach(([name, config]) => {\n options.push({\n id: `pricing-${name}`,\n name,\n category: \"Pricing\",\n path: config.path,\n description: config.description,\n });\n });\n\n // Video Blocks\n Object.entries(videoBlocks).forEach(([name, config]) => {\n options.push({\n id: `video-${name}`,\n name,\n category: \"Video/Media\",\n path: config.path,\n description: config.description,\n });\n });\n\n // Page Templates\n Object.entries(pageTemplates).forEach(([name, config]) => {\n options.push({\n id: `template-${name}`,\n name,\n category: \"Page Templates\",\n path: config.path,\n description: config.description,\n });\n });\n\n return options;\n}\n\nconst ALL_COMPONENTS = buildComponentOptions();\nconst CATEGORIES = [...new Set(ALL_COMPONENTS.map((c) => c.category))];\n\n// ═══════════════════════════════════════════════════════════\n// COMPONENT\n// ═══════════════════════════════════════════════════════════\n\nexport function ComponentSearch({\n selectedComponents,\n onSelectionChange,\n className,\n}: ComponentSearchProps) {\n const [searchQuery, setSearchQuery] = useState(\"\");\n const [isOpen, setIsOpen] = useState(false);\n const [expandedCategories, setExpandedCategories] = useState<Set<string>>(\n new Set(CATEGORIES)\n );\n\n // Filter components based on search\n const filteredComponents = useMemo(() => {\n if (!searchQuery.trim()) return ALL_COMPONENTS;\n\n const query = searchQuery.toLowerCase();\n return ALL_COMPONENTS.filter(\n (c) =>\n c.name.toLowerCase().includes(query) ||\n c.description.toLowerCase().includes(query) ||\n c.category.toLowerCase().includes(query)\n );\n }, [searchQuery]);\n\n // Group by category\n const groupedComponents = useMemo(() => {\n const groups: Record<string, ComponentOption[]> = {};\n filteredComponents.forEach((c) => {\n if (!groups[c.category]) groups[c.category] = [];\n groups[c.category].push(c);\n });\n return groups;\n }, [filteredComponents]);\n\n const toggleCategory = (category: string) => {\n setExpandedCategories((prev) => {\n const next = new Set(prev);\n if (next.has(category)) {\n next.delete(category);\n } else {\n next.add(category);\n }\n return next;\n });\n };\n\n const toggleComponent = (component: ComponentOption) => {\n const isSelected = selectedComponents.some((c) => c.id === component.id);\n if (isSelected) {\n onSelectionChange(selectedComponents.filter((c) => c.id !== component.id));\n } else {\n onSelectionChange([...selectedComponents, component]);\n }\n };\n\n const removeComponent = (componentId: string) => {\n onSelectionChange(selectedComponents.filter((c) => c.id !== componentId));\n };\n\n return (\n <div className={cn(\"space-y-3\", className)}>\n {/* Selected Components Chips */}\n {selectedComponents.length > 0 && (\n <div className=\"flex flex-wrap gap-2\">\n {selectedComponents.map((component) => (\n <div\n key={component.id}\n className=\"flex items-center gap-1.5 px-2.5 py-1 rounded-full bg-[var(--canvas-surface-brand)] text-[var(--canvas-primary)]\"\n style={{ fontSize: \"var(--typo-body-s-size)\" }}\n >\n <span className=\"font-medium\">{component.name}</span>\n <button\n onClick={() => removeComponent(component.id)}\n className=\"cursor-pointer p-0.5 rounded-full hover:bg-[var(--canvas-primary)]/20 transition-colors\"\n >\n <X className=\"size-3\" />\n </button>\n </div>\n ))}\n </div>\n )}\n\n {/* Search Input */}\n <div className=\"relative\">\n <div\n className={cn(\n \"flex items-center gap-2 px-3 py-2.5 rounded-lg border bg-[var(--canvas-background)] cursor-text\",\n isOpen\n ? \"border-[var(--canvas-primary)] ring-2 ring-[var(--canvas-primary)]/20\"\n : \"border-[var(--canvas-border)]\"\n )}\n onClick={() => setIsOpen(true)}\n >\n <Search className=\"size-4 text-[var(--canvas-text-muted)]\" />\n <input\n type=\"text\"\n value={searchQuery}\n onChange={(e) => setSearchQuery(e.target.value)}\n onFocus={() => setIsOpen(true)}\n placeholder=\"Search components...\"\n className=\"flex-1 bg-transparent text-[var(--canvas-text)] placeholder:text-[var(--canvas-text-placeholder)] outline-none\"\n style={{ fontSize: \"var(--typo-body-s-size)\" }}\n />\n <span className=\"text-[var(--canvas-text-muted)]\" style={{ fontSize: \"var(--typo-body-xs-size)\" }}>\n {selectedComponents.length} selected\n </span>\n </div>\n\n {/* Dropdown */}\n {isOpen && (\n <>\n {/* Backdrop */}\n <div\n className=\"fixed inset-0 z-10\"\n onClick={() => setIsOpen(false)}\n />\n\n {/* Options List */}\n <div className=\"absolute top-full left-0 right-0 mt-1 z-20 max-h-[400px] overflow-y-auto rounded-lg border border-[var(--canvas-border)] bg-[var(--canvas-background)] shadow-lg\">\n {Object.entries(groupedComponents).map(([category, components]) => (\n <div key={category}>\n {/* Category Header */}\n <button\n onClick={() => toggleCategory(category)}\n className=\"cursor-pointer w-full flex items-center justify-between px-3 py-2 bg-[var(--canvas-surface)] border-b border-[var(--canvas-border)] font-semibold text-[var(--canvas-text-muted)] uppercase tracking-wide hover:bg-[var(--canvas-surface-brand)]/50\"\n style={{ fontSize: \"var(--typo-body-xs-size)\" }}\n >\n <span>\n {category} ({components.length})\n </span>\n {expandedCategories.has(category) ? (\n <ChevronUp className=\"size-3\" />\n ) : (\n <ChevronDown className=\"size-3\" />\n )}\n </button>\n\n {/* Components in Category */}\n {expandedCategories.has(category) && (\n <div>\n {components.map((component) => {\n const isSelected = selectedComponents.some(\n (c) => c.id === component.id\n );\n return (\n <button\n key={component.id}\n onClick={() => toggleComponent(component)}\n className={cn(\n \"cursor-pointer w-full flex items-start gap-3 px-3 py-2.5 text-left transition-colors border-b border-[var(--canvas-border)]/50 last:border-b-0\",\n isSelected\n ? \"bg-[var(--canvas-surface-brand)]/50\"\n : \"hover:bg-[var(--canvas-surface)]\"\n )}\n >\n {/* Checkbox */}\n <div\n className={cn(\n \"size-4 rounded border mt-0.5 flex items-center justify-center shrink-0\",\n isSelected\n ? \"bg-[var(--canvas-primary)] border-[var(--canvas-primary)]\"\n : \"border-[var(--canvas-border)]\"\n )}\n >\n {isSelected && (\n <svg\n className=\"size-3 text-[var(--canvas-primary-foreground)]\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n strokeWidth={3}\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n d=\"M5 13l4 4L19 7\"\n />\n </svg>\n )}\n </div>\n\n {/* Component Info */}\n <div className=\"flex-1 min-w-0\">\n <div className=\"flex items-center gap-2\">\n <span\n className={cn(\n \"font-medium\",\n isSelected\n ? \"text-[var(--canvas-primary)]\"\n : \"text-[var(--canvas-text)]\"\n )}\n style={{ fontSize: \"var(--typo-body-s-size)\" }}\n >\n {component.name}\n </span>\n <span className=\"text-[var(--canvas-text-placeholder)]\" style={{ fontSize: \"var(--typo-body-xs-size)\" }}>\n {component.path}\n </span>\n </div>\n <p className=\"text-[var(--canvas-text-muted)] mt-0.5 line-clamp-2\" style={{ fontSize: \"var(--typo-body-xs-size)\" }}>\n {component.description}\n </p>\n </div>\n </button>\n );\n })}\n </div>\n )}\n </div>\n ))}\n\n {filteredComponents.length === 0 && (\n <div className=\"px-3 py-6 text-center text-[var(--canvas-text-muted)]\" style={{ fontSize: \"var(--typo-body-s-size)\" }}>\n No components found for \"{searchQuery}\"\n </div>\n )}\n </div>\n </>\n )}\n </div>\n </div>\n );\n}\n"
|
|
10
10
|
}
|
|
11
11
|
],
|
|
12
12
|
"dependencies": [
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
{
|
|
7
7
|
"path": "components/blocks/custom-component-helper.tsx",
|
|
8
8
|
"type": "registry:block",
|
|
9
|
-
"content": "\"use client\";\n\nimport { useState, useMemo } from \"react\";\nimport { Check, Copy, Sparkles } from \"lucide-react\";\nimport { cn } from \"../../lib/utils\";\nimport { ComponentSearch, type ComponentOption } from \"./component-search\";\nimport { projectContext } from \"../../data/project-context\";\n\n// ═══════════════════════════════════════════════════════════\n// TYPES\n// ═══════════════════════════════════════════════════════════\n\ntype ComponentType = \"block\" | \"page-template\" | \"ui-component\";\n\ninterface CustomComponentHelperProps {\n className?: string;\n}\n\n// ═══════════════════════════════════════════════════════════\n// COMPONENT\n// ═══════════════════════════════════════════════════════════\n\nexport function CustomComponentHelper({ className }: CustomComponentHelperProps) {\n const [componentType, setComponentType] = useState<ComponentType>(\"block\");\n const [componentName, setComponentName] = useState(\"\");\n const [componentDescription, setComponentDescription] = useState(\"\");\n const [referenceComponents, setReferenceComponents] = useState<ComponentOption[]>([]);\n const [copied, setCopied] = useState(false);\n\n const { personas } = projectContext;\n\n // Generate the prompt\n const generatedPrompt = useMemo(() => {\n if (!componentName && !componentDescription) {\n return \"\";\n }\n\n const typeLabel = {\n block: \"block\",\n \"page-template\": \"page template\",\n \"ui-component\": \"UI component\",\n }[componentType];\n\n const parts: string[] = [\n \"Please create a plan for the following, then wait for my approval before making changes:\",\n \"\",\n ];\n\n // Context references\n parts.push(\"CONTEXT:\");\n parts.push(\"- Read src/data/scope.md for project scope and requirements\");\n parts.push(\"- Reference src/data/project-context.ts for user personas and project goals\");\n parts.push(\"\");\n\n parts.push(`Create a new ${typeLabel} component:`);\n parts.push(\"\");\n\n if (componentName) {\n parts.push(`Name: ${componentName}`);\n }\n\n parts.push(`Type: ${typeLabel.charAt(0).toUpperCase() + typeLabel.slice(1)}`);\n\n if (componentDescription) {\n parts.push(`Description: ${componentDescription}`);\n }\n\n // Include personas for context\n if (personas.length > 0) {\n parts.push(\"\");\n parts.push(\"Design this component to serve these user personas:\");\n personas.forEach((p) => {\n parts.push(`- ${p.name} (${p.role})`);\n });\n }\n\n // Reference components\n if (referenceComponents.length > 0) {\n parts.push(\"\");\n parts.push(\"Reference these existing components for style/patterns:\");\n referenceComponents.forEach((c) => {\n parts.push(`- ${c.name} (${c.path})`);\n });\n }\n\n // Requirements\n parts.push(\"\");\n parts.push(\"Requirements:\");\n parts.push(\"1. Build using ShadCN primitives (Button, Dialog, Input, etc.)\");\n parts.push(\"2. Implement CSS variables for theming:\");\n parts.push(\" - var(--canvas-*) for colors (primary, background, text, border, surface)\");\n parts.push(\" - var(--spacing-*) for spacing (sm, md, lg, xl)\");\n parts.push(\" - var(--radius-*) for border radius\");\n parts.push(\" - var(--typo-*) for typography\");\n\n // File location based on type\n const kebabName = toKebabCase(componentName || \"new-component\");\n if (componentType === \"block\") {\n parts.push(`3. Create file at src/components/blocks/${kebabName}.tsx`);\n parts.push(\"4. Export from src/components/blocks/index.ts\");\n } else if (componentType === \"page-template\") {\n const pageName = toKebabCase(componentName || \"new-page\");\n parts.push(`3. Create page at src/app/${pageName}/page.tsx`);\n parts.push(`4. Create layout at src/app/${pageName}/layout.tsx`);\n } else {\n parts.push(`3. Create file at src/components/ui/${kebabName}.tsx`);\n parts.push(\"4. Export from src/components/ui/index.ts (if exists)\");\n }\n\n parts.push(\"5. Add entry to src/lib/component-registry.ts after creation\");\n parts.push(\"6. Follow existing component patterns in the codebase\");\n parts.push(\"7. Ensure the component aligns with the project scope and serves the target personas\");\n\n return parts.join(\"\\n\");\n }, [componentType, componentName, componentDescription, referenceComponents, personas]);\n\n const handleCopy = async () => {\n if (!generatedPrompt) return;\n await navigator.clipboard.writeText(generatedPrompt);\n setCopied(true);\n setTimeout(() => setCopied(false), 2000);\n };\n\n const hasContent = componentName || componentDescription;\n\n return (\n <div className={cn(\"space-y-6\", className)}>\n {/* Section Header */}\n <div>\n <h3 className=\"font-semibold text-[var(--canvas-text)]\" style={{ fontSize: \"var(--typo-body-l-size)\" }}>\n Create Custom Component\n </h3>\n <p className=\"text-[var(--canvas-text-muted)] mt-1\" style={{ fontSize: \"var(--typo-body-s-size)\" }}>\n Generate a prompt to create a new ShadCN-based component with design variables\n </p>\n </div>\n\n {/* Component Type */}\n <div className=\"space-y-2\">\n <label className=\"text-[var(--canvas-text)]\" style={{ fontSize: \"var(--typo-body-s-size)\", fontWeight: 500 }}>\n Component Type\n </label>\n <div className=\"flex gap-2\">\n {[\n { id: \"block\", label: \"Block\" },\n { id: \"page-template\", label: \"Page Template\" },\n { id: \"ui-component\", label: \"UI Component\" },\n ].map((type) => (\n <button\n key={type.id}\n onClick={() => setComponentType(type.id as ComponentType)}\n className={cn(\n \"px-4 py-2 rounded-lg font-medium transition-all\",\n componentType === type.id\n ? \"bg-[var(--canvas-primary)] text-[var(--canvas-primary-foreground)]\"\n : \"bg-[var(--canvas-surface)] text-[var(--canvas-text-muted)] border border-[var(--canvas-border)] hover:border-[var(--canvas-primary)] hover:text-[var(--canvas-primary)]\"\n )}\n style={{ fontSize: \"var(--typo-body-s-size)\" }}\n >\n {type.label}\n </button>\n ))}\n </div>\n </div>\n\n {/* Component Name */}\n <div className=\"space-y-2\">\n <label\n htmlFor=\"component-name\"\n className=\"text-[var(--canvas-text)]\" style={{ fontSize: \"var(--typo-body-s-size)\", fontWeight: 500 }}\n >\n Component Name\n </label>\n <input\n id=\"component-name\"\n type=\"text\"\n value={componentName}\n onChange={(e) => setComponentName(e.target.value)}\n placeholder=\"e.g., MultiStepPopup, ImageCarousel, StatCard\"\n className=\"w-full px-3 py-2 rounded-lg border border-[var(--canvas-border)] bg-[var(--canvas-background)] text-[var(--canvas-text)] placeholder:text-[var(--canvas-text-placeholder)] focus:outline-none focus:ring-2 focus:ring-[var(--canvas-primary)] focus:border-transparent\"\n style={{ fontSize: \"var(--typo-body-s-size)\" }}\n />\n </div>\n\n {/* Component Description */}\n <div className=\"space-y-2\">\n <label\n htmlFor=\"component-description\"\n className=\"text-[var(--canvas-text)]\" style={{ fontSize: \"var(--typo-body-s-size)\", fontWeight: 500 }}\n >\n Description\n </label>\n <textarea\n id=\"component-description\"\n value={componentDescription}\n onChange={(e) => setComponentDescription(e.target.value)}\n placeholder=\"Describe what this component should do, its features, and any specific requirements (e.g., a multi-step popup with steps listed on the left side and content on the right, progress indicator, next/back buttons)\"\n rows={4}\n className=\"w-full px-3 py-2 rounded-lg border border-[var(--canvas-border)] bg-[var(--canvas-background)] text-[var(--canvas-text)] placeholder:text-[var(--canvas-text-placeholder)] focus:outline-none focus:ring-2 focus:ring-[var(--canvas-primary)] focus:border-transparent resize-none\"\n style={{ fontSize: \"var(--typo-body-s-size)\" }}\n />\n </div>\n\n {/* Reference Components */}\n <div className=\"space-y-2\">\n <label className=\"text-[var(--canvas-text)]\" style={{ fontSize: \"var(--typo-body-s-size)\", fontWeight: 500 }}>\n Reference Components (optional)\n </label>\n <p className=\"text-[var(--canvas-text-muted)] mb-2\" style={{ fontSize: \"var(--typo-body-xs-size)\" }}>\n Select existing components to use as style/pattern references\n </p>\n <ComponentSearch\n selectedComponents={referenceComponents}\n onSelectionChange={setReferenceComponents}\n />\n </div>\n\n {/* Generated Prompt Preview */}\n {hasContent && (\n <div className=\"rounded-lg border border-dashed border-[var(--canvas-border)] bg-[var(--canvas-surface)] p-4\">\n {/* Header */}\n <div className=\"flex items-center justify-between mb-3\">\n <div className=\"flex items-center gap-2 font-medium text-[var(--canvas-text-muted)]\" style={{ fontSize: \"var(--typo-body-s-size)\" }}>\n <Sparkles className=\"size-4 text-[var(--canvas-primary)]\" />\n Generated Prompt\n </div>\n <button\n onClick={handleCopy}\n disabled={!generatedPrompt}\n className={cn(\n \"flex items-center gap-1.5 px-2.5 py-1.5 rounded-md font-medium transition-all\",\n copied\n ? \"bg-[var(--canvas-success-surface)] text-[var(--canvas-success)]\"\n : \"bg-[var(--canvas-background)] text-[var(--canvas-text-muted)] hover:text-[var(--canvas-text)] border border-[var(--canvas-border)] hover:border-[var(--canvas-primary)]\"\n )}\n style={{ fontSize: \"var(--typo-body-xs-size)\" }}\n >\n {copied ? (\n <>\n <Check className=\"size-3\" />\n Copied!\n </>\n ) : (\n <>\n <Copy className=\"size-3\" />\n Copy prompt\n </>\n )}\n </button>\n </div>\n\n {/* Prompt Text */}\n <pre className=\"text-[var(--canvas-text)] leading-relaxed font-mono whitespace-pre-wrap bg-[var(--canvas-background)] rounded-md p-3 border border-[var(--canvas-border)] max-h-[300px] overflow-y-auto\" style={{ fontSize: \"var(--typo-body-s-size)\" }}>\n {generatedPrompt}\n </pre>\n </div>\n )}\n\n {/* Empty State */}\n {!hasContent && (\n <div className=\"rounded-lg border-2 border-dashed border-[var(--canvas-border)] bg-[var(--canvas-surface)] p-8 text-center\">\n <p className=\"text-[var(--canvas-text-muted)]\" style={{ fontSize: \"var(--typo-body-s-size)\" }}>\n Enter a component name or description to generate a prompt\n </p>\n </div>\n )}\n </div>\n );\n}\n\n// ═══════════════════════════════════════════════════════════\n// HELPERS\n// ═══════════════════════════════════════════════════════════\n\nfunction toKebabCase(str: string): string {\n return str\n .replace(/([a-z])([A-Z])/g, \"$1-$2\")\n .replace(/\\s+/g, \"-\")\n .toLowerCase();\n}\n"
|
|
9
|
+
"content": "\"use client\";\n\nimport { useState, useMemo } from \"react\";\nimport { Check, Copy, Sparkles } from \"lucide-react\";\nimport { cn } from \"../../lib/utils\";\nimport { ComponentSearch, type ComponentOption } from \"./component-search\";\nimport { projectContext } from \"../../data/project-context\";\n\n// ═══════════════════════════════════════════════════════════\n// TYPES\n// ═══════════════════════════════════════════════════════════\n\ntype ComponentType = \"block\" | \"page-template\" | \"ui-component\";\n\ninterface CustomComponentHelperProps {\n className?: string;\n}\n\n// ═══════════════════════════════════════════════════════════\n// COMPONENT\n// ═══════════════════════════════════════════════════════════\n\nexport function CustomComponentHelper({ className }: CustomComponentHelperProps) {\n const [componentType, setComponentType] = useState<ComponentType>(\"block\");\n const [componentName, setComponentName] = useState(\"\");\n const [componentDescription, setComponentDescription] = useState(\"\");\n const [referenceComponents, setReferenceComponents] = useState<ComponentOption[]>([]);\n const [copied, setCopied] = useState(false);\n\n const { personas } = projectContext;\n\n // Generate the prompt\n const generatedPrompt = useMemo(() => {\n if (!componentName && !componentDescription) {\n return \"\";\n }\n\n const typeLabel = {\n block: \"block\",\n \"page-template\": \"page template\",\n \"ui-component\": \"UI component\",\n }[componentType];\n\n const parts: string[] = [\n \"Please create a plan for the following, then wait for my approval before making changes:\",\n \"\",\n ];\n\n // Context references\n parts.push(\"CONTEXT:\");\n parts.push(\"- Read src/data/scope.md for project scope and requirements\");\n parts.push(\"- Reference src/data/project-context.ts for user personas and project goals\");\n parts.push(\"\");\n\n parts.push(`Create a new ${typeLabel} component:`);\n parts.push(\"\");\n\n if (componentName) {\n parts.push(`Name: ${componentName}`);\n }\n\n parts.push(`Type: ${typeLabel.charAt(0).toUpperCase() + typeLabel.slice(1)}`);\n\n if (componentDescription) {\n parts.push(`Description: ${componentDescription}`);\n }\n\n // Include personas for context\n if (personas.length > 0) {\n parts.push(\"\");\n parts.push(\"Design this component to serve these user personas:\");\n personas.forEach((p) => {\n parts.push(`- ${p.name} (${p.role})`);\n });\n }\n\n // Reference components\n if (referenceComponents.length > 0) {\n parts.push(\"\");\n parts.push(\"Reference these existing components for style/patterns:\");\n referenceComponents.forEach((c) => {\n parts.push(`- ${c.name} (${c.path})`);\n });\n }\n\n // Requirements\n parts.push(\"\");\n parts.push(\"Requirements:\");\n parts.push(\"1. Build using ShadCN primitives (Button, Dialog, Input, etc.)\");\n parts.push(\"2. Implement CSS variables for theming:\");\n parts.push(\" - var(--canvas-*) for colors (primary, background, text, border, surface)\");\n parts.push(\" - var(--spacing-*) for spacing (sm, md, lg, xl)\");\n parts.push(\" - var(--radius-*) for border radius\");\n parts.push(\" - var(--typo-*) for typography\");\n\n // File location based on type\n const kebabName = toKebabCase(componentName || \"new-component\");\n if (componentType === \"block\") {\n parts.push(`3. Create file at src/components/blocks/${kebabName}.tsx`);\n parts.push(\"4. Export from src/components/blocks/index.ts\");\n } else if (componentType === \"page-template\") {\n const pageName = toKebabCase(componentName || \"new-page\");\n parts.push(`3. Create page at src/app/${pageName}/page.tsx`);\n parts.push(`4. Create layout at src/app/${pageName}/layout.tsx`);\n } else {\n parts.push(`3. Create file at src/components/ui/${kebabName}.tsx`);\n parts.push(\"4. Export from src/components/ui/index.ts (if exists)\");\n }\n\n parts.push(\"5. Add entry to src/lib/component-registry.ts after creation\");\n parts.push(\"6. Follow existing component patterns in the codebase\");\n parts.push(\"7. Ensure the component aligns with the project scope and serves the target personas\");\n\n return parts.join(\"\\n\");\n }, [componentType, componentName, componentDescription, referenceComponents, personas]);\n\n const handleCopy = async () => {\n if (!generatedPrompt) return;\n await navigator.clipboard.writeText(generatedPrompt);\n setCopied(true);\n setTimeout(() => setCopied(false), 2000);\n };\n\n const hasContent = componentName || componentDescription;\n\n return (\n <div className={cn(\"space-y-6\", className)}>\n {/* Section Header */}\n <div>\n <h3 className=\"font-semibold text-[var(--canvas-text)]\" style={{ fontSize: \"var(--typo-body-l-size)\" }}>\n Create Custom Component\n </h3>\n <p className=\"text-[var(--canvas-text-muted)] mt-1\" style={{ fontSize: \"var(--typo-body-s-size)\" }}>\n Generate a prompt to create a new ShadCN-based component with design variables\n </p>\n </div>\n\n {/* Component Type */}\n <div className=\"space-y-2\">\n <label className=\"text-[var(--canvas-text)]\" style={{ fontSize: \"var(--typo-body-s-size)\", fontWeight: 500 }}>\n Component Type\n </label>\n <div className=\"flex gap-2\">\n {[\n { id: \"block\", label: \"Block\" },\n { id: \"page-template\", label: \"Page Template\" },\n { id: \"ui-component\", label: \"UI Component\" },\n ].map((type) => (\n <button\n key={type.id}\n onClick={() => setComponentType(type.id as ComponentType)}\n className={cn(\n \"cursor-pointer px-4 py-2 rounded-lg font-medium transition-all\",\n componentType === type.id\n ? \"bg-[var(--canvas-primary)] text-[var(--canvas-primary-foreground)]\"\n : \"bg-[var(--canvas-surface)] text-[var(--canvas-text-muted)] border border-[var(--canvas-border)] hover:border-[var(--canvas-primary)] hover:text-[var(--canvas-primary)]\"\n )}\n style={{ fontSize: \"var(--typo-body-s-size)\" }}\n >\n {type.label}\n </button>\n ))}\n </div>\n </div>\n\n {/* Component Name */}\n <div className=\"space-y-2\">\n <label\n htmlFor=\"component-name\"\n className=\"text-[var(--canvas-text)]\" style={{ fontSize: \"var(--typo-body-s-size)\", fontWeight: 500 }}\n >\n Component Name\n </label>\n <input\n id=\"component-name\"\n type=\"text\"\n value={componentName}\n onChange={(e) => setComponentName(e.target.value)}\n placeholder=\"e.g., MultiStepPopup, ImageCarousel, StatCard\"\n className=\"w-full px-3 py-2 rounded-lg border border-[var(--canvas-border)] bg-[var(--canvas-background)] text-[var(--canvas-text)] placeholder:text-[var(--canvas-text-placeholder)] focus:outline-none focus:ring-2 focus:ring-[var(--canvas-primary)] focus:border-transparent\"\n style={{ fontSize: \"var(--typo-body-s-size)\" }}\n />\n </div>\n\n {/* Component Description */}\n <div className=\"space-y-2\">\n <label\n htmlFor=\"component-description\"\n className=\"text-[var(--canvas-text)]\" style={{ fontSize: \"var(--typo-body-s-size)\", fontWeight: 500 }}\n >\n Description\n </label>\n <textarea\n id=\"component-description\"\n value={componentDescription}\n onChange={(e) => setComponentDescription(e.target.value)}\n placeholder=\"Describe what this component should do, its features, and any specific requirements (e.g., a multi-step popup with steps listed on the left side and content on the right, progress indicator, next/back buttons)\"\n rows={4}\n className=\"w-full px-3 py-2 rounded-lg border border-[var(--canvas-border)] bg-[var(--canvas-background)] text-[var(--canvas-text)] placeholder:text-[var(--canvas-text-placeholder)] focus:outline-none focus:ring-2 focus:ring-[var(--canvas-primary)] focus:border-transparent resize-none\"\n style={{ fontSize: \"var(--typo-body-s-size)\" }}\n />\n </div>\n\n {/* Reference Components */}\n <div className=\"space-y-2\">\n <label className=\"text-[var(--canvas-text)]\" style={{ fontSize: \"var(--typo-body-s-size)\", fontWeight: 500 }}>\n Reference Components (optional)\n </label>\n <p className=\"text-[var(--canvas-text-muted)] mb-2\" style={{ fontSize: \"var(--typo-body-xs-size)\" }}>\n Select existing components to use as style/pattern references\n </p>\n <ComponentSearch\n selectedComponents={referenceComponents}\n onSelectionChange={setReferenceComponents}\n />\n </div>\n\n {/* Generated Prompt Preview */}\n {hasContent && (\n <div className=\"rounded-lg border border-dashed border-[var(--canvas-border)] bg-[var(--canvas-surface)] p-4\">\n {/* Header */}\n <div className=\"flex items-center justify-between mb-3\">\n <div className=\"flex items-center gap-2 font-medium text-[var(--canvas-text-muted)]\" style={{ fontSize: \"var(--typo-body-s-size)\" }}>\n <Sparkles className=\"size-4 text-[var(--canvas-primary)]\" />\n Generated Prompt\n </div>\n <button\n onClick={handleCopy}\n disabled={!generatedPrompt}\n className={cn(\n \"cursor-pointer flex items-center gap-1.5 px-2.5 py-1.5 rounded-md font-medium transition-all\",\n copied\n ? \"bg-[var(--canvas-success-surface)] text-[var(--canvas-success)]\"\n : \"bg-[var(--canvas-background)] text-[var(--canvas-text-muted)] hover:text-[var(--canvas-text)] border border-[var(--canvas-border)] hover:border-[var(--canvas-primary)]\"\n )}\n style={{ fontSize: \"var(--typo-body-xs-size)\" }}\n >\n {copied ? (\n <>\n <Check className=\"size-3\" />\n Copied!\n </>\n ) : (\n <>\n <Copy className=\"size-3\" />\n Copy prompt\n </>\n )}\n </button>\n </div>\n\n {/* Prompt Text */}\n <pre className=\"text-[var(--canvas-text)] leading-relaxed font-mono whitespace-pre-wrap bg-[var(--canvas-background)] rounded-md p-3 border border-[var(--canvas-border)] max-h-[300px] overflow-y-auto\" style={{ fontSize: \"var(--typo-body-s-size)\" }}>\n {generatedPrompt}\n </pre>\n </div>\n )}\n\n {/* Empty State */}\n {!hasContent && (\n <div className=\"rounded-lg border-2 border-dashed border-[var(--canvas-border)] bg-[var(--canvas-surface)] p-8 text-center\">\n <p className=\"text-[var(--canvas-text-muted)]\" style={{ fontSize: \"var(--typo-body-s-size)\" }}>\n Enter a component name or description to generate a prompt\n </p>\n </div>\n )}\n </div>\n );\n}\n\n// ═══════════════════════════════════════════════════════════\n// HELPERS\n// ═══════════════════════════════════════════════════════════\n\nfunction toKebabCase(str: string): string {\n return str\n .replace(/([a-z])([A-Z])/g, \"$1-$2\")\n .replace(/\\s+/g, \"-\")\n .toLowerCase();\n}\n"
|
|
10
10
|
}
|
|
11
11
|
],
|
|
12
12
|
"dependencies": [
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
{
|
|
7
7
|
"path": "components/blocks/faqs-table.tsx",
|
|
8
8
|
"type": "registry:block",
|
|
9
|
-
"content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport { Plus, Minus } from \"lucide-react\";\n\n// ============================================\n// Types\n// ============================================\n\nexport interface FAQItem {\n id: string;\n question: string;\n answer: string;\n}\n\nexport interface FaqsTableProps {\n /** Table title */\n title?: string;\n /** FAQ items to display */\n items?: FAQItem[];\n /** ID of the item that should be expanded by default */\n defaultExpandedId?: string;\n /** Additional class names */\n className?: string;\n}\n\n// ============================================\n// Default Data\n// ============================================\n\nconst defaultItems: FAQItem[] = [\n {\n id: \"1\",\n question: \"What is Canvas?\",\n answer:\n \"Canvas uses a modular approach that allows you to easily plug in standardized components for most of your application's UX. That allows you to not reinvent the wheel and focus most of your efforts on your product's unique parts.\",\n },\n {\n id: \"2\",\n question: \"How can I purchase a license?\",\n answer:\n \"You can purchase a license directly from our website. We offer individual, team, and enterprise plans to suit your needs. All plans include access to our full component library and design system.\",\n },\n {\n id: \"3\",\n question: \"Do you offer refunds?\",\n answer:\n \"Yes, we offer a 30-day money-back guarantee. If you're not satisfied with your purchase, contact our support team for a full refund.\",\n },\n {\n id: \"4\",\n question: \"Can I use Canvas for commercial projects?\",\n answer:\n \"Absolutely! All our licenses allow for unlimited commercial projects. You can use Canvas components in client work, SaaS products, and internal tools.\",\n },\n];\n\n// ============================================\n// Main Component\n// ============================================\n\n/**\n * Canvas Design System - FAQs Table Block\n *\n * An expandable FAQ list with plus/minus toggle icons.\n * Displays one item expanded at a time (accordion behavior).\n *\n * @example\n * ```tsx\n * <FaqsTable\n * title=\"Common questions\"\n * items={[\n * { id: \"1\", question: \"What is Canvas?\", answer: \"...\" }\n * ]}\n * />\n * ```\n */\nexport function FaqsTable({\n title = \"Common questions\",\n items = defaultItems,\n defaultExpandedId,\n className,\n}: FaqsTableProps) {\n const [expandedId, setExpandedId] = useState<string | null>(\n defaultExpandedId ?? items[0]?.id ?? null\n );\n\n const toggleItem = (id: string) => {\n setExpandedId((current) => (current === id ? null : id));\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 </div>\n </div>\n\n {/* FAQ List */}\n <div className=\"flex flex-col w-full\">\n {items.map((item, index) => {\n const isExpanded = expandedId === item.id;\n const isFirst = index === 0;\n const isLast = index === items.length - 1;\n\n return (\n <div\n key={item.id}\n className=\"flex flex-col w-full\"\n style={{\n borderTop: isFirst ? \"1px solid var(--canvas-border)\" : \"none\",\n borderBottom: \"1px solid var(--canvas-border)\",\n paddingTop: \"var(--spacing-xl)\",\n paddingBottom: \"var(--spacing-xl)\",\n }}\n >\n {/* Question Row */}\n <button\n onClick={() => toggleItem(item.id)}\n className=\"flex items-center justify-between w-full text-left\"\n style={{ gap: \"var(--spacing-xl)\" }}\n aria-expanded={isExpanded}\n aria-controls={`faq-answer-${item.id}`}\n >\n <span\n className=\"flex-1\"\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 600,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n {item.question}\n </span>\n <div\n className=\"flex items-center justify-center shrink-0\"\n style={{\n width: \"32px\",\n height: \"32px\",\n padding: \"var(--spacing-md)\",\n borderRadius: \"var(--radius-full)\",\n }}\n >\n {isExpanded ? (\n <Minus\n size={20}\n style={{ color: \"var(--canvas-text)\" }}\n strokeWidth={1.5}\n />\n ) : (\n <Plus\n size={20}\n style={{ color: \"var(--canvas-text)\" }}\n strokeWidth={1.5}\n />\n )}\n </div>\n </button>\n\n {/* Answer (collapsible) */}\n {isExpanded && (\n <div\n id={`faq-answer-${item.id}`}\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 paddingTop: \"var(--spacing-md)\",\n }}\n >\n {item.answer}\n </div>\n )}\n </div>\n );\n })}\n </div>\n </div>\n );\n}\n"
|
|
9
|
+
"content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport { Plus, Minus } from \"lucide-react\";\n\n// ============================================\n// Types\n// ============================================\n\nexport interface FAQItem {\n id: string;\n question: string;\n answer: string;\n}\n\nexport interface FaqsTableProps {\n /** Table title */\n title?: string;\n /** FAQ items to display */\n items?: FAQItem[];\n /** ID of the item that should be expanded by default */\n defaultExpandedId?: string;\n /** Additional class names */\n className?: string;\n}\n\n// ============================================\n// Default Data\n// ============================================\n\nconst defaultItems: FAQItem[] = [\n {\n id: \"1\",\n question: \"What is Canvas?\",\n answer:\n \"Canvas uses a modular approach that allows you to easily plug in standardized components for most of your application's UX. That allows you to not reinvent the wheel and focus most of your efforts on your product's unique parts.\",\n },\n {\n id: \"2\",\n question: \"How can I purchase a license?\",\n answer:\n \"You can purchase a license directly from our website. We offer individual, team, and enterprise plans to suit your needs. All plans include access to our full component library and design system.\",\n },\n {\n id: \"3\",\n question: \"Do you offer refunds?\",\n answer:\n \"Yes, we offer a 30-day money-back guarantee. If you're not satisfied with your purchase, contact our support team for a full refund.\",\n },\n {\n id: \"4\",\n question: \"Can I use Canvas for commercial projects?\",\n answer:\n \"Absolutely! All our licenses allow for unlimited commercial projects. You can use Canvas components in client work, SaaS products, and internal tools.\",\n },\n];\n\n// ============================================\n// Main Component\n// ============================================\n\n/**\n * Canvas Design System - FAQs Table Block\n *\n * An expandable FAQ list with plus/minus toggle icons.\n * Displays one item expanded at a time (accordion behavior).\n *\n * @example\n * ```tsx\n * <FaqsTable\n * title=\"Common questions\"\n * items={[\n * { id: \"1\", question: \"What is Canvas?\", answer: \"...\" }\n * ]}\n * />\n * ```\n */\nexport function FaqsTable({\n title = \"Common questions\",\n items = defaultItems,\n defaultExpandedId,\n className,\n}: FaqsTableProps) {\n const [expandedId, setExpandedId] = useState<string | null>(\n defaultExpandedId ?? items[0]?.id ?? null\n );\n\n const toggleItem = (id: string) => {\n setExpandedId((current) => (current === id ? null : id));\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 </div>\n </div>\n\n {/* FAQ List */}\n <div className=\"flex flex-col w-full\">\n {items.map((item, index) => {\n const isExpanded = expandedId === item.id;\n const isFirst = index === 0;\n const isLast = index === items.length - 1;\n\n return (\n <div\n key={item.id}\n className=\"flex flex-col w-full\"\n style={{\n borderTop: isFirst ? \"1px solid var(--canvas-border)\" : \"none\",\n borderBottom: \"1px solid var(--canvas-border)\",\n paddingTop: \"var(--spacing-xl)\",\n paddingBottom: \"var(--spacing-xl)\",\n }}\n >\n {/* Question Row */}\n <button\n onClick={() => toggleItem(item.id)}\n className=\"cursor-pointer flex items-center justify-between w-full text-left\"\n style={{ gap: \"var(--spacing-xl)\" }}\n aria-expanded={isExpanded}\n aria-controls={`faq-answer-${item.id}`}\n >\n <span\n className=\"flex-1\"\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 600,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n {item.question}\n </span>\n <div\n className=\"flex items-center justify-center shrink-0\"\n style={{\n width: \"32px\",\n height: \"32px\",\n padding: \"var(--spacing-md)\",\n borderRadius: \"var(--radius-full)\",\n }}\n >\n {isExpanded ? (\n <Minus\n size={20}\n style={{ color: \"var(--canvas-text)\" }}\n strokeWidth={1.5}\n />\n ) : (\n <Plus\n size={20}\n style={{ color: \"var(--canvas-text)\" }}\n strokeWidth={1.5}\n />\n )}\n </div>\n </button>\n\n {/* Answer (collapsible) */}\n {isExpanded && (\n <div\n id={`faq-answer-${item.id}`}\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 paddingTop: \"var(--spacing-md)\",\n }}\n >\n {item.answer}\n </div>\n )}\n </div>\n );\n })}\n </div>\n </div>\n );\n}\n"
|
|
10
10
|
}
|
|
11
11
|
],
|
|
12
12
|
"dependencies": [
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
{
|
|
7
7
|
"path": "components/blocks/infinity-canvas.tsx",
|
|
8
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-[var(--canvas-background)] 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-[var(--canvas-background)] 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-[var(--canvas-background)] 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-[var(--canvas-background)] 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-[var(--canvas-background)] rounded-lg p-4 shadow-sm border border-[var(--canvas-border)]\">\n <LinksCard />\n </div>\n ),\n CreditCardDisplay: () => (\n <div className=\"w-[360px] bg-[var(--canvas-background)] 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-[var(--canvas-background)] 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-[var(--canvas-background)] 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-[var(--canvas-background)] 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-[var(--canvas-background)] 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-[var(--canvas-background)] 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-[var(--canvas-background)] 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-[var(--canvas-background)] 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-[var(--canvas-background)] rounded-lg p-4 shadow-sm border border-[var(--canvas-border)]\">\n <ParticipantList />\n </div>\n ),\n VideoContentSection: () => (\n <div className=\"w-[600px] bg-[var(--canvas-background)] rounded-lg p-4 shadow-sm border border-[var(--canvas-border)]\">\n <VideoContentSection />\n </div>\n ),\n VideoPlaylist: () => (\n <div className=\"w-[320px] bg-[var(--canvas-background)] 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-[var(--canvas-background)] 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-[var(--canvas-background)] 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-[var(--canvas-background)] 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-[var(--canvas-background)] 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-[var(--canvas-background)] p-6\">\n <ReviewsGrid />\n </div>\n ),\n SocialProof: () => (\n <div className=\"w-[600px] overflow-hidden rounded-lg shadow-sm bg-[var(--canvas-background)]\">\n <SocialProof label=\"TRUSTED BY\" />\n </div>\n ),\n MetricsSection: () => (\n <div className=\"w-[700px] overflow-hidden rounded-lg shadow-sm bg-[var(--canvas-background)] 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-[var(--canvas-background)] 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-[var(--canvas-background)] p-6\">\n <CoreValuesGrid />\n </div>\n ),\n DestinationCards: () => (\n <div className=\"w-[800px] overflow-hidden rounded-lg shadow-sm bg-[var(--canvas-background)] 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-[var(--canvas-background)] p-6\">\n <TeamCardsGrid />\n </div>\n ),\n TeamCircularGrid: () => (\n <div className=\"w-[800px] overflow-hidden rounded-lg shadow-sm bg-[var(--canvas-background)] 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-[var(--canvas-background)] p-6\">\n <FeaturedNewsCards />\n </div>\n ),\n OfficeLocations: () => (\n <div className=\"w-[800px] overflow-hidden rounded-lg shadow-sm bg-[var(--canvas-background)] 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-[var(--canvas-background)] p-6\">\n <FaqAccordion />\n </div>\n ),\n FeaturesComparison: () => (\n <div className=\"w-[800px] overflow-hidden rounded-lg shadow-sm bg-[var(--canvas-background)] p-6\">\n <FeaturesComparison />\n </div>\n ),\n\n // =====================\n // UI COMPONENTS\n // =====================\n Button: () => (\n <div className=\"w-[200px] bg-[var(--canvas-background)] 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-[var(--canvas-background)] 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-[var(--canvas-background)] 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-[var(--canvas-background)] 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-[var(--canvas-background)] 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-[var(--canvas-background)] 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-[var(--canvas-background)] 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-[var(--canvas-background)] 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-[var(--canvas-primary-foreground)] text-xs rounded-full\">Design</span>\n <span className=\"px-2 py-1 bg-[var(--canvas-primary)] text-[var(--canvas-primary-foreground)] 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-[var(--canvas-background)] 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-[var(--canvas-background)] 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-[var(--canvas-primary-foreground)] 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-[var(--canvas-destructive)] text-[var(--canvas-primary-foreground)] 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-[var(--canvas-primary-foreground)] border-[var(--canvas-primary)]\"\n : \"bg-[var(--canvas-background)] 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-[var(--canvas-background)] 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-[var(--canvas-text-muted)] border-x border-[var(--canvas-border)] min-w-[60px] text-center\" style={{ fontSize: \"var(--typo-body-s-size)\" }}>\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)]\" style={{ fontSize: \"var(--typo-body-l-size)\" }}>\n Drag components from the sidebar to get started\n </p>\n <p className=\"text-[var(--canvas-text-placeholder)] mt-2\" style={{ fontSize: \"var(--typo-body-s-size)\" }}>\n Scroll to zoom • Drag to pan\n </p>\n </div>\n )}\n </div>\n </TransformComponent>\n </TransformWrapper>\n </div>\n );\n}\n"
|
|
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-[var(--canvas-background)] 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-[var(--canvas-background)] 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-[var(--canvas-background)] 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-[var(--canvas-background)] 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-[var(--canvas-background)] rounded-lg p-4 shadow-sm border border-[var(--canvas-border)]\">\n <LinksCard />\n </div>\n ),\n CreditCardDisplay: () => (\n <div className=\"w-[360px] bg-[var(--canvas-background)] 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-[var(--canvas-background)] 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-[var(--canvas-background)] 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-[var(--canvas-background)] 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-[var(--canvas-background)] 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-[var(--canvas-background)] 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-[var(--canvas-background)] 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-[var(--canvas-background)] 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-[var(--canvas-background)] rounded-lg p-4 shadow-sm border border-[var(--canvas-border)]\">\n <ParticipantList />\n </div>\n ),\n VideoContentSection: () => (\n <div className=\"w-[600px] bg-[var(--canvas-background)] rounded-lg p-4 shadow-sm border border-[var(--canvas-border)]\">\n <VideoContentSection />\n </div>\n ),\n VideoPlaylist: () => (\n <div className=\"w-[320px] bg-[var(--canvas-background)] 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-[var(--canvas-background)] 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-[var(--canvas-background)] 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-[var(--canvas-background)] 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-[var(--canvas-background)] 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-[var(--canvas-background)] p-6\">\n <ReviewsGrid />\n </div>\n ),\n SocialProof: () => (\n <div className=\"w-[600px] overflow-hidden rounded-lg shadow-sm bg-[var(--canvas-background)]\">\n <SocialProof label=\"TRUSTED BY\" />\n </div>\n ),\n MetricsSection: () => (\n <div className=\"w-[700px] overflow-hidden rounded-lg shadow-sm bg-[var(--canvas-background)] 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-[var(--canvas-background)] 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-[var(--canvas-background)] p-6\">\n <CoreValuesGrid />\n </div>\n ),\n DestinationCards: () => (\n <div className=\"w-[800px] overflow-hidden rounded-lg shadow-sm bg-[var(--canvas-background)] 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-[var(--canvas-background)] p-6\">\n <TeamCardsGrid />\n </div>\n ),\n TeamCircularGrid: () => (\n <div className=\"w-[800px] overflow-hidden rounded-lg shadow-sm bg-[var(--canvas-background)] 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-[var(--canvas-background)] p-6\">\n <FeaturedNewsCards />\n </div>\n ),\n OfficeLocations: () => (\n <div className=\"w-[800px] overflow-hidden rounded-lg shadow-sm bg-[var(--canvas-background)] 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-[var(--canvas-background)] p-6\">\n <FaqAccordion />\n </div>\n ),\n FeaturesComparison: () => (\n <div className=\"w-[800px] overflow-hidden rounded-lg shadow-sm bg-[var(--canvas-background)] p-6\">\n <FeaturesComparison />\n </div>\n ),\n\n // =====================\n // UI COMPONENTS\n // =====================\n Button: () => (\n <div className=\"w-[200px] bg-[var(--canvas-background)] 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-[var(--canvas-background)] 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-[var(--canvas-background)] 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-[var(--canvas-background)] 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-[var(--canvas-background)] 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-[var(--canvas-background)] 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-[var(--canvas-background)] 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-[var(--canvas-background)] 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-[var(--canvas-primary-foreground)] text-xs rounded-full\">Design</span>\n <span className=\"px-2 py-1 bg-[var(--canvas-primary)] text-[var(--canvas-primary-foreground)] 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-[var(--canvas-background)] 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-[var(--canvas-background)] 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-[var(--canvas-primary-foreground)] 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-[var(--canvas-destructive)] text-[var(--canvas-primary-foreground)] 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 \"cursor-pointer p-2 rounded-md border shadow-sm transition-colors\",\n showGrid\n ? \"bg-[var(--canvas-primary)] text-[var(--canvas-primary-foreground)] border-[var(--canvas-primary)]\"\n : \"bg-[var(--canvas-background)] 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-[var(--canvas-background)] rounded-md border border-[var(--canvas-border)] shadow-sm\">\n <button\n onClick={() => transformRef.current?.zoomOut()}\n className=\"cursor-pointer 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-[var(--canvas-text-muted)] border-x border-[var(--canvas-border)] min-w-[60px] text-center\" style={{ fontSize: \"var(--typo-body-s-size)\" }}>\n {Math.round(scale * 100)}%\n </span>\n <button\n onClick={() => transformRef.current?.zoomIn()}\n className=\"cursor-pointer 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=\"cursor-pointer 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)]\" style={{ fontSize: \"var(--typo-body-l-size)\" }}>\n Drag components from the sidebar to get started\n </p>\n <p className=\"text-[var(--canvas-text-placeholder)] mt-2\" style={{ fontSize: \"var(--typo-body-s-size)\" }}>\n Scroll to zoom • Drag to pan\n </p>\n </div>\n )}\n </div>\n </TransformComponent>\n </TransformWrapper>\n </div>\n );\n}\n"
|
|
10
10
|
}
|
|
11
11
|
],
|
|
12
12
|
"dependencies": [
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
{
|
|
7
7
|
"path": "components/blocks/menu-section.tsx",
|
|
8
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=\"font-semibold text-[var(--canvas-text)]\" style={{ fontSize: \"var(--typo-h6-size)\", lineHeight: \"var(--typo-h6-line-height)\" }}>\n {title}\n </h3>\n {/* Description - 16px regular */}\n {description && (\n <p className=\"font-normal text-[var(--canvas-text-muted)]\" style={{ fontSize: \"var(--typo-body-m-size)\", lineHeight: \"var(--typo-body-m-line-height)\" }}>\n {description}\n </p>\n )}\n </div>\n );\n}\n\n"
|
|
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=\"cursor-pointer 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=\"font-semibold text-[var(--canvas-text)]\" style={{ fontSize: \"var(--typo-h6-size)\", lineHeight: \"var(--typo-h6-line-height)\" }}>\n {title}\n </h3>\n {/* Description - 16px regular */}\n {description && (\n <p className=\"font-normal text-[var(--canvas-text-muted)]\" style={{ fontSize: \"var(--typo-body-m-size)\", lineHeight: \"var(--typo-body-m-line-height)\" }}>\n {description}\n </p>\n )}\n </div>\n );\n}\n\n"
|
|
10
10
|
}
|
|
11
11
|
],
|
|
12
12
|
"dependencies": [
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
{
|
|
7
7
|
"path": "components/blocks/messenger-sidebar.tsx",
|
|
8
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: \"var(--typo-body-xl-size)\",\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 style={{\n fontSize: \"var(--typo-body-xs-size)\",\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"
|
|
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: \"var(--typo-body-xl-size)\",\n lineHeight: \"28px\",\n }}\n >\n Messages\n </h1>\n <div className=\"flex items-center gap-[var(--spacing-md)]\">\n <button\n className=\"cursor-pointer 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=\"cursor-pointer 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=\"cursor-pointer 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 style={{\n fontSize: \"var(--typo-body-xs-size)\",\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
10
|
}
|
|
11
11
|
],
|
|
12
12
|
"dependencies": [
|