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.
Files changed (32) hide show
  1. package/dist/index.js +65 -63
  2. package/dist/index.js.map +1 -1
  3. package/package.json +1 -1
  4. package/registry/blocks/component-palette.json +1 -1
  5. package/registry/blocks/component-search.json +1 -1
  6. package/registry/blocks/custom-component-helper.json +1 -1
  7. package/registry/blocks/faqs-table.json +1 -1
  8. package/registry/blocks/infinity-canvas.json +1 -1
  9. package/registry/blocks/menu-section.json +1 -1
  10. package/registry/blocks/messenger-sidebar.json +1 -1
  11. package/registry/blocks/mobile-bottom-nav.json +1 -1
  12. package/registry/blocks/pagination.json +1 -1
  13. package/registry/blocks/pill-tabs.json +1 -1
  14. package/registry/blocks/pricing-cards.json +1 -1
  15. package/registry/blocks/profile-card.json +1 -1
  16. package/registry/blocks/prompt-template.json +1 -1
  17. package/registry/blocks/screen-flowchart.json +1 -1
  18. package/registry/blocks/screen-prompt-builder.json +1 -1
  19. package/registry/blocks/screen-prompt-template.json +1 -1
  20. package/registry/blocks/sidebar-cards.json +1 -1
  21. package/registry/blocks/slideshow-grid-tiles.json +1 -1
  22. package/registry/blocks/social-feed.json +1 -1
  23. package/registry/blocks/upvoting-posts-table.json +1 -1
  24. package/registry/layout/double-sidebar.json +1 -1
  25. package/registry/layout/header.json +1 -1
  26. package/registry/layout/icon-sidebar.json +1 -1
  27. package/registry/layout/project-context-shell.json +1 -1
  28. package/registry/layout/sidebar-nav.json +1 -1
  29. package/registry/ui/button.json +1 -1
  30. package/registry/ui/line-tabs.json +1 -1
  31. package/registry/ui/selectable-pills.json +1 -1
  32. package/registry/ui/tabs.json +1 -1
@@ -6,7 +6,7 @@
6
6
  {
7
7
  "path": "components/blocks/mobile-bottom-nav.tsx",
8
8
  "type": "registry:block",
9
- "content": "\"use client\";\n\nimport { cn } from \"../../lib/utils\";\nimport { LucideIcon, Home, MessageSquare, Search, User } from \"lucide-react\";\n\n// ============================================\n// Mobile Nav Tab Item\n// ============================================\n\nexport interface MobileNavTabConfig {\n id: string;\n label: string;\n icon: LucideIcon;\n isActive?: boolean;\n}\n\ninterface MobileNavTabProps {\n item: MobileNavTabConfig;\n variant?: \"dark\" | \"light\";\n onClick?: () => void;\n}\n\nfunction MobileNavTab({ item, variant = \"light\", onClick }: MobileNavTabProps) {\n const Icon = item.icon;\n const isActive = item.isActive;\n const isDark = variant === \"dark\";\n\n return (\n <button\n onClick={onClick}\n className={cn(\n // Match icon-sidebar dimensions: 64px × 64px\n \"relative flex flex-col items-center justify-center gap-1 w-16 h-16 rounded-[var(--radius-nav)] transition-colors\",\n // Dark variant\n isDark && isActive && \"bg-[var(--canvas-sidebar-dark-active-bg)]\",\n isDark && !isActive && \"hover:bg-[var(--canvas-sidebar-dark-active-bg)]/50\",\n // Light variant\n !isDark && isActive && \"bg-[var(--canvas-sidebar-light-active-bg)]\",\n !isDark && !isActive && \"hover:bg-[var(--canvas-sidebar-light-active-bg)]/50\"\n )}\n >\n <Icon\n className={cn(\n // Match icon-sidebar: 16px icons\n \"size-4\",\n isDark && isActive && \"text-[var(--canvas-sidebar-dark-active-text)]\",\n isDark && !isActive && \"text-[var(--canvas-sidebar-dark-text)]\",\n !isDark && isActive && \"text-[var(--canvas-sidebar-light-active-text)]\",\n !isDark && !isActive && \"text-[var(--canvas-sidebar-light-text)]\"\n )}\n />\n <span\n className={cn(\n // Match icon-sidebar: 12px labels, medium weight\n \"font-medium\",\n isDark && isActive && \"text-[var(--canvas-sidebar-dark-active-text)]\",\n isDark && !isActive && \"text-[var(--canvas-sidebar-dark-text)]\",\n !isDark && isActive && \"text-[var(--canvas-sidebar-light-active-text)]\",\n !isDark && !isActive && \"text-[var(--canvas-sidebar-light-text)]\"\n )}\n style={{ fontSize: \"var(--typo-sidebar-label-size)\" }}\n >\n {item.label}\n </span>\n </button>\n );\n}\n\n// ============================================\n// Default Navigation Items\n// ============================================\n\nexport const defaultMobileNavTabs: MobileNavTabConfig[] = [\n { id: \"home\", label: \"Home\", icon: Home, isActive: true },\n { id: \"messages\", label: \"Messages\", icon: MessageSquare },\n { id: \"discover\", label: \"Discover\", icon: Search },\n { id: \"account\", label: \"Account\", icon: User },\n];\n\n// ============================================\n// Mobile Bottom Navigation\n// ============================================\n\ninterface MobileBottomNavProps {\n /** Navigation tabs to display */\n tabs?: MobileNavTabConfig[];\n /** Visual variant - dark or light theme */\n variant?: \"dark\" | \"light\";\n /** Callback when a tab is clicked */\n onTabClick?: (tab: MobileNavTabConfig) => void;\n /** Additional class names */\n className?: string;\n}\n\n/**\n * Canvas Design System - Mobile Bottom Navigation\n * \n * A sticky bottom navigation bar with icon tabs.\n * Styling matches the icon-sidebar for consistency.\n * Supports both dark and light themes via the variant prop.\n * \n * @example\n * ```tsx\n * <MobileBottomNav\n * variant=\"light\"\n * tabs={defaultMobileNavTabs}\n * onTabClick={(tab) => console.log(tab.id)}\n * />\n * ```\n */\nexport function MobileBottomNav({\n tabs = defaultMobileNavTabs,\n variant = \"light\",\n onTabClick,\n className,\n}: MobileBottomNavProps) {\n const isDark = variant === \"dark\";\n\n return (\n <nav\n className={cn(\n \"fixed bottom-0 left-0 right-0 z-50\",\n \"flex items-center justify-center gap-5\",\n \"px-4 py-3\",\n // Dark variant\n isDark && \"bg-[var(--canvas-sidebar-dark-bg)] border-t border-[var(--canvas-sidebar-dark-border)]\",\n isDark && \"shadow-[0px_-4px_16px_0px_rgba(0,0,0,0.2)]\",\n // Light variant\n !isDark && \"bg-[var(--canvas-sidebar-light-bg)] border-t border-[var(--canvas-sidebar-light-border)]\",\n !isDark && \"shadow-[0px_-4px_16px_0px_rgba(0,0,0,0.04)]\",\n className\n )}\n >\n {tabs.map((tab) => (\n <MobileNavTab\n key={tab.id}\n item={tab}\n variant={variant}\n onClick={() => onTabClick?.(tab)}\n />\n ))}\n </nav>\n );\n}\n"
9
+ "content": "\"use client\";\n\nimport { cn } from \"../../lib/utils\";\nimport { LucideIcon, Home, MessageSquare, Search, User } from \"lucide-react\";\n\n// ============================================\n// Mobile Nav Tab Item\n// ============================================\n\nexport interface MobileNavTabConfig {\n id: string;\n label: string;\n icon: LucideIcon;\n isActive?: boolean;\n}\n\ninterface MobileNavTabProps {\n item: MobileNavTabConfig;\n variant?: \"dark\" | \"light\";\n onClick?: () => void;\n}\n\nfunction MobileNavTab({ item, variant = \"light\", onClick }: MobileNavTabProps) {\n const Icon = item.icon;\n const isActive = item.isActive;\n const isDark = variant === \"dark\";\n\n return (\n <button\n onClick={onClick}\n className={cn(\n // Match icon-sidebar dimensions: 64px × 64px\n \"cursor-pointer relative flex flex-col items-center justify-center gap-1 w-16 h-16 rounded-[var(--radius-nav)] transition-colors\",\n // Dark variant\n isDark && isActive && \"bg-[var(--canvas-sidebar-dark-active-bg)]\",\n isDark && !isActive && \"hover:bg-[var(--canvas-sidebar-dark-active-bg)]/50\",\n // Light variant\n !isDark && isActive && \"bg-[var(--canvas-sidebar-light-active-bg)]\",\n !isDark && !isActive && \"hover:bg-[var(--canvas-sidebar-light-active-bg)]/50\"\n )}\n >\n <Icon\n className={cn(\n // Match icon-sidebar: 16px icons\n \"size-4\",\n isDark && isActive && \"text-[var(--canvas-sidebar-dark-active-text)]\",\n isDark && !isActive && \"text-[var(--canvas-sidebar-dark-text)]\",\n !isDark && isActive && \"text-[var(--canvas-sidebar-light-active-text)]\",\n !isDark && !isActive && \"text-[var(--canvas-sidebar-light-text)]\"\n )}\n />\n <span\n className={cn(\n // Match icon-sidebar: 12px labels, medium weight\n \"font-medium\",\n isDark && isActive && \"text-[var(--canvas-sidebar-dark-active-text)]\",\n isDark && !isActive && \"text-[var(--canvas-sidebar-dark-text)]\",\n !isDark && isActive && \"text-[var(--canvas-sidebar-light-active-text)]\",\n !isDark && !isActive && \"text-[var(--canvas-sidebar-light-text)]\"\n )}\n style={{ fontSize: \"var(--typo-sidebar-label-size)\" }}\n >\n {item.label}\n </span>\n </button>\n );\n}\n\n// ============================================\n// Default Navigation Items\n// ============================================\n\nexport const defaultMobileNavTabs: MobileNavTabConfig[] = [\n { id: \"home\", label: \"Home\", icon: Home, isActive: true },\n { id: \"messages\", label: \"Messages\", icon: MessageSquare },\n { id: \"discover\", label: \"Discover\", icon: Search },\n { id: \"account\", label: \"Account\", icon: User },\n];\n\n// ============================================\n// Mobile Bottom Navigation\n// ============================================\n\ninterface MobileBottomNavProps {\n /** Navigation tabs to display */\n tabs?: MobileNavTabConfig[];\n /** Visual variant - dark or light theme */\n variant?: \"dark\" | \"light\";\n /** Callback when a tab is clicked */\n onTabClick?: (tab: MobileNavTabConfig) => void;\n /** Additional class names */\n className?: string;\n}\n\n/**\n * Canvas Design System - Mobile Bottom Navigation\n * \n * A sticky bottom navigation bar with icon tabs.\n * Styling matches the icon-sidebar for consistency.\n * Supports both dark and light themes via the variant prop.\n * \n * @example\n * ```tsx\n * <MobileBottomNav\n * variant=\"light\"\n * tabs={defaultMobileNavTabs}\n * onTabClick={(tab) => console.log(tab.id)}\n * />\n * ```\n */\nexport function MobileBottomNav({\n tabs = defaultMobileNavTabs,\n variant = \"light\",\n onTabClick,\n className,\n}: MobileBottomNavProps) {\n const isDark = variant === \"dark\";\n\n return (\n <nav\n className={cn(\n \"fixed bottom-0 left-0 right-0 z-50\",\n \"flex items-center justify-center gap-5\",\n \"px-4 py-3\",\n // Dark variant\n isDark && \"bg-[var(--canvas-sidebar-dark-bg)] border-t border-[var(--canvas-sidebar-dark-border)]\",\n isDark && \"shadow-[0px_-4px_16px_0px_rgba(0,0,0,0.2)]\",\n // Light variant\n !isDark && \"bg-[var(--canvas-sidebar-light-bg)] border-t border-[var(--canvas-sidebar-light-border)]\",\n !isDark && \"shadow-[0px_-4px_16px_0px_rgba(0,0,0,0.04)]\",\n className\n )}\n >\n {tabs.map((tab) => (\n <MobileNavTab\n key={tab.id}\n item={tab}\n variant={variant}\n onClick={() => onTabClick?.(tab)}\n />\n ))}\n </nav>\n );\n}\n"
10
10
  }
11
11
  ],
12
12
  "dependencies": [
@@ -6,7 +6,7 @@
6
6
  {
7
7
  "path": "components/blocks/pagination.tsx",
8
8
  "type": "registry:block",
9
- "content": "\"use client\";\n\nimport { useMemo } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport { useIsMobile } from \"../../hooks/use-mobile\";\nimport {\n ChevronLeft,\n ChevronRight,\n ChevronsLeft,\n ChevronsRight,\n MoreHorizontal,\n} from \"lucide-react\";\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from \"../ui/select\";\n\n// ============================================\n// Types\n// ============================================\n\nexport interface PaginationProps {\n /** Current active page (1-indexed) */\n currentPage?: number;\n /** Total number of pages */\n totalPages?: number;\n /** Total number of items (for results text) */\n totalItems?: number;\n /** Items displayed per page */\n itemsPerPage?: number;\n /** Available items per page options */\n itemsPerPageOptions?: number[];\n /** Callback when page changes */\n onPageChange?: (page: number) => void;\n /** Callback when items per page changes */\n onItemsPerPageChange?: (itemsPerPage: number) => void;\n /** Show \"Viewing X-Y of Z results\" text (desktop only) */\n showResultsText?: boolean;\n /** Show \"Show per page\" dropdown (desktop only) */\n showItemsPerPage?: boolean;\n /** Maximum number of visible page buttons on desktop */\n maxVisiblePages?: number;\n /** Maximum number of visible page buttons on mobile (default: 3) */\n mobileMaxVisiblePages?: number;\n /** Force compact mode (fewer visible pages) regardless of viewport */\n compact?: boolean;\n /** Additional class names */\n className?: string;\n}\n\n// ============================================\n// Sub-components\n// ============================================\n\ninterface PaginationButtonProps {\n page: number;\n isSelected?: boolean;\n onClick?: () => void;\n className?: string;\n}\n\n/**\n * Individual page number button\n */\nfunction PaginationButton({\n page,\n isSelected = false,\n onClick,\n className,\n}: PaginationButtonProps) {\n return (\n <button\n type=\"button\"\n onClick={onClick}\n className={cn(\n \"flex items-center justify-center shrink-0 transition-colors\",\n \"font-semibold\",\n \"size-[32px] rounded-[var(--radius-xs)]\",\n \"border bg-[var(--canvas-background)]\",\n isSelected\n ? \"border-[var(--canvas-primary)] text-[var(--canvas-primary)]\"\n : \"border-[var(--canvas-border)] text-[var(--canvas-text)] hover:bg-[var(--canvas-surface)]\",\n className\n )}\n style={{ fontSize: \"var(--typo-body-s-size)\", lineHeight: \"var(--typo-body-s-line-height)\" }}\n aria-current={isSelected ? \"page\" : undefined}\n >\n {page}\n </button>\n );\n}\n\ninterface NavigationButtonProps {\n type: \"first\" | \"prev\" | \"next\" | \"last\";\n disabled?: boolean;\n onClick?: () => void;\n className?: string;\n}\n\n/**\n * Navigation button (First, Prev, Next, Last)\n */\nfunction NavigationButton({\n type,\n disabled = false,\n onClick,\n className,\n}: NavigationButtonProps) {\n const Icon = {\n first: ChevronsLeft,\n prev: ChevronLeft,\n next: ChevronRight,\n last: ChevronsRight,\n }[type];\n\n const label = {\n first: null,\n prev: \"Prev\",\n next: \"Next\",\n last: null,\n }[type];\n\n const isIconOnly = type === \"first\" || type === \"last\";\n\n return (\n <button\n type=\"button\"\n onClick={onClick}\n disabled={disabled}\n className={cn(\n \"flex items-center justify-center shrink-0 transition-colors\",\n \"h-[32px] rounded-[var(--radius-xs)]\",\n \"border bg-[var(--canvas-background)] border-[var(--canvas-border)]\",\n isIconOnly ? \"w-[32px]\" : \"px-[var(--spacing-md)] gap-[var(--spacing-xs)]\",\n disabled\n ? \"text-[var(--canvas-text-placeholder)] cursor-not-allowed\"\n : \"text-[var(--canvas-text)] hover:bg-[var(--canvas-surface)]\",\n className\n )}\n aria-label={type}\n >\n {(type === \"first\" || type === \"prev\") && (\n <Icon className=\"size-[20px]\" />\n )}\n {label && (\n <span className=\"font-semibold\" style={{ fontSize: \"var(--typo-body-s-size)\", lineHeight: \"var(--typo-body-s-line-height)\" }}>{label}</span>\n )}\n {(type === \"next\" || type === \"last\") && (\n <Icon className=\"size-[20px]\" />\n )}\n </button>\n );\n}\n\n/**\n * Ellipsis indicator for truncated pages\n */\nfunction PaginationEllipsis({ className }: { className?: string }) {\n return (\n <div\n className={cn(\n \"flex items-center justify-center\",\n \"w-[24px] h-[32px] rounded-[var(--radius-xs)]\",\n className\n )}\n aria-hidden=\"true\"\n >\n <MoreHorizontal className=\"size-[20px] text-[var(--canvas-text-muted)]\" />\n </div>\n );\n}\n\n// ============================================\n// Helper Functions\n// ============================================\n\n/**\n * Generate array of page numbers to display with ellipsis\n */\nfunction generatePageNumbers(\n currentPage: number,\n totalPages: number,\n maxVisible: number\n): (number | \"ellipsis\")[] {\n if (totalPages <= maxVisible) {\n return Array.from({ length: totalPages }, (_, i) => i + 1);\n }\n\n const pages: (number | \"ellipsis\")[] = [];\n const halfVisible = Math.floor((maxVisible - 2) / 2);\n\n // Always show first page\n pages.push(1);\n\n // Calculate start and end of visible range\n let start = Math.max(2, currentPage - halfVisible);\n let end = Math.min(totalPages - 1, currentPage + halfVisible);\n\n // Adjust if we're near the start\n if (currentPage <= halfVisible + 2) {\n end = Math.min(totalPages - 1, maxVisible - 1);\n start = 2;\n }\n\n // Adjust if we're near the end\n if (currentPage >= totalPages - halfVisible - 1) {\n start = Math.max(2, totalPages - maxVisible + 2);\n end = totalPages - 1;\n }\n\n // Add ellipsis before middle section if needed\n if (start > 2) {\n pages.push(\"ellipsis\");\n }\n\n // Add middle pages\n for (let i = start; i <= end; i++) {\n pages.push(i);\n }\n\n // Add ellipsis after middle section if needed\n if (end < totalPages - 1) {\n pages.push(\"ellipsis\");\n }\n\n // Always show last page\n if (totalPages > 1) {\n pages.push(totalPages);\n }\n\n return pages;\n}\n\n/**\n * Format number with commas (e.g., 2500 -> 2,500)\n */\nfunction formatNumber(num: number): string {\n return num.toLocaleString();\n}\n\n// ============================================\n// Main Component\n// ============================================\n\n/**\n * Canvas Design System - Pagination Component\n *\n * A responsive pagination component with results text, items per page dropdown,\n * and page navigation. Automatically adjusts layout for mobile screens.\n *\n * @example\n * ```tsx\n * // Basic usage\n * <Pagination\n * currentPage={1}\n * totalPages={10}\n * onPageChange={(page) => setPage(page)}\n * />\n *\n * // With all features\n * <Pagination\n * currentPage={1}\n * totalPages={50}\n * totalItems={2500}\n * itemsPerPage={50}\n * itemsPerPageOptions={[30, 50, 100]}\n * showResultsText\n * showItemsPerPage\n * onPageChange={(page) => setPage(page)}\n * onItemsPerPageChange={(perPage) => setPerPage(perPage)}\n * />\n * ```\n */\nexport function Pagination({\n currentPage = 1,\n totalPages = 10,\n totalItems = 2500,\n itemsPerPage = 50,\n itemsPerPageOptions = [30, 50, 100],\n onPageChange,\n onItemsPerPageChange,\n showResultsText = true,\n showItemsPerPage = true,\n maxVisiblePages = 5,\n mobileMaxVisiblePages = 3,\n compact = false,\n className,\n}: PaginationProps) {\n const isMobile = useIsMobile();\n \n // Use fewer visible pages on mobile or when compact mode is forced\n const effectiveMaxVisiblePages = (isMobile || compact) ? mobileMaxVisiblePages : maxVisiblePages;\n \n // Calculate visible page range\n const pageNumbers = useMemo(\n () => generatePageNumbers(currentPage, totalPages, effectiveMaxVisiblePages),\n [currentPage, totalPages, effectiveMaxVisiblePages]\n );\n\n // Calculate results range\n const startItem = (currentPage - 1) * itemsPerPage + 1;\n const endItem = Math.min(currentPage * itemsPerPage, totalItems);\n\n // Navigation handlers\n const goToPage = (page: number) => {\n if (page >= 1 && page <= totalPages && page !== currentPage) {\n onPageChange?.(page);\n }\n };\n\n const goToFirst = () => goToPage(1);\n const goToPrev = () => goToPage(currentPage - 1);\n const goToNext = () => goToPage(currentPage + 1);\n const goToLast = () => goToPage(totalPages);\n\n const isFirstPage = currentPage === 1;\n const isLastPage = currentPage === totalPages;\n\n return (\n <div\n className={cn(\n \"flex items-center justify-between\",\n \"h-[56px] py-[var(--spacing-lg)]\",\n \"w-full\",\n className\n )}\n >\n {/* Left section: Results text & Items per page (desktop only) */}\n <div className=\"hidden md:flex items-center gap-[var(--spacing-2xl)]\">\n {/* Results text */}\n {showResultsText && (\n <span\n className=\"text-[var(--canvas-text)] whitespace-nowrap\"\n style={{ fontFamily: \"var(--typo-global-font)\", fontSize: \"var(--typo-body-s-size)\", lineHeight: \"var(--typo-body-s-line-height)\" }}\n >\n Viewing {formatNumber(startItem)}–{formatNumber(endItem)} of{\" \"}\n {formatNumber(totalItems)} results\n </span>\n )}\n\n {/* Items per page dropdown */}\n {showItemsPerPage && (\n <div className=\"flex items-center gap-[var(--spacing-sm)]\">\n <span\n className=\"text-[14px] leading-[20px] text-[var(--canvas-text)] whitespace-nowrap\"\n style={{ fontFamily: \"var(--typo-global-font)\" }}\n >\n Show per page\n </span>\n <Select\n value={String(itemsPerPage)}\n onValueChange={(value) => onItemsPerPageChange?.(Number(value))}\n >\n <SelectTrigger\n inputSize=\"sm\"\n className=\"w-[70px]\"\n >\n <SelectValue />\n </SelectTrigger>\n <SelectContent position=\"popper\" side=\"bottom\" sideOffset={4}>\n {itemsPerPageOptions.map((option) => (\n <SelectItem key={option} value={String(option)}>\n {option}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n )}\n </div>\n\n {/* Right section: Navigation (always visible, centered on mobile) */}\n <div className=\"flex items-center gap-[var(--spacing-sm)] mx-auto md:mx-0\">\n {/* First & Prev buttons */}\n <div className=\"flex items-center gap-[var(--spacing-sm)]\">\n {!compact && (\n <NavigationButton\n type=\"first\"\n disabled={isFirstPage}\n onClick={goToFirst}\n />\n )}\n <NavigationButton\n type=\"prev\"\n disabled={isFirstPage}\n onClick={goToPrev}\n />\n </div>\n\n {/* Page numbers */}\n <div className=\"flex items-center gap-[var(--spacing-sm)]\">\n {pageNumbers.map((item, index) =>\n item === \"ellipsis\" ? (\n <PaginationEllipsis key={`ellipsis-${index}`} />\n ) : (\n <PaginationButton\n key={item}\n page={item}\n isSelected={item === currentPage}\n onClick={() => goToPage(item)}\n />\n )\n )}\n </div>\n\n {/* Next & Last buttons */}\n <div className=\"flex items-center gap-[var(--spacing-sm)]\">\n <NavigationButton\n type=\"next\"\n disabled={isLastPage}\n onClick={goToNext}\n />\n {!compact && (\n <NavigationButton\n type=\"last\"\n disabled={isLastPage}\n onClick={goToLast}\n />\n )}\n </div>\n </div>\n </div>\n );\n}\n"
9
+ "content": "\"use client\";\n\nimport { useMemo } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport { useIsMobile } from \"../../hooks/use-mobile\";\nimport {\n ChevronLeft,\n ChevronRight,\n ChevronsLeft,\n ChevronsRight,\n MoreHorizontal,\n} from \"lucide-react\";\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from \"../ui/select\";\n\n// ============================================\n// Types\n// ============================================\n\nexport interface PaginationProps {\n /** Current active page (1-indexed) */\n currentPage?: number;\n /** Total number of pages */\n totalPages?: number;\n /** Total number of items (for results text) */\n totalItems?: number;\n /** Items displayed per page */\n itemsPerPage?: number;\n /** Available items per page options */\n itemsPerPageOptions?: number[];\n /** Callback when page changes */\n onPageChange?: (page: number) => void;\n /** Callback when items per page changes */\n onItemsPerPageChange?: (itemsPerPage: number) => void;\n /** Show \"Viewing X-Y of Z results\" text (desktop only) */\n showResultsText?: boolean;\n /** Show \"Show per page\" dropdown (desktop only) */\n showItemsPerPage?: boolean;\n /** Maximum number of visible page buttons on desktop */\n maxVisiblePages?: number;\n /** Maximum number of visible page buttons on mobile (default: 3) */\n mobileMaxVisiblePages?: number;\n /** Force compact mode (fewer visible pages) regardless of viewport */\n compact?: boolean;\n /** Additional class names */\n className?: string;\n}\n\n// ============================================\n// Sub-components\n// ============================================\n\ninterface PaginationButtonProps {\n page: number;\n isSelected?: boolean;\n onClick?: () => void;\n className?: string;\n}\n\n/**\n * Individual page number button\n */\nfunction PaginationButton({\n page,\n isSelected = false,\n onClick,\n className,\n}: PaginationButtonProps) {\n return (\n <button\n type=\"button\"\n onClick={onClick}\n className={cn(\n \"cursor-pointer flex items-center justify-center shrink-0 transition-colors\",\n \"font-semibold\",\n \"size-[32px] rounded-[var(--radius-xs)]\",\n \"border bg-[var(--canvas-background)]\",\n isSelected\n ? \"border-[var(--canvas-primary)] text-[var(--canvas-primary)]\"\n : \"border-[var(--canvas-border)] text-[var(--canvas-text)] hover:bg-[var(--canvas-surface)]\",\n className\n )}\n style={{ fontSize: \"var(--typo-body-s-size)\", lineHeight: \"var(--typo-body-s-line-height)\" }}\n aria-current={isSelected ? \"page\" : undefined}\n >\n {page}\n </button>\n );\n}\n\ninterface NavigationButtonProps {\n type: \"first\" | \"prev\" | \"next\" | \"last\";\n disabled?: boolean;\n onClick?: () => void;\n className?: string;\n}\n\n/**\n * Navigation button (First, Prev, Next, Last)\n */\nfunction NavigationButton({\n type,\n disabled = false,\n onClick,\n className,\n}: NavigationButtonProps) {\n const Icon = {\n first: ChevronsLeft,\n prev: ChevronLeft,\n next: ChevronRight,\n last: ChevronsRight,\n }[type];\n\n const label = {\n first: null,\n prev: \"Prev\",\n next: \"Next\",\n last: null,\n }[type];\n\n const isIconOnly = type === \"first\" || type === \"last\";\n\n return (\n <button\n type=\"button\"\n onClick={onClick}\n disabled={disabled}\n className={cn(\n \"flex items-center justify-center shrink-0 transition-colors\",\n \"h-[32px] rounded-[var(--radius-xs)]\",\n \"border bg-[var(--canvas-background)] border-[var(--canvas-border)]\",\n isIconOnly ? \"w-[32px]\" : \"px-[var(--spacing-md)] gap-[var(--spacing-xs)]\",\n disabled\n ? \"text-[var(--canvas-text-placeholder)] cursor-not-allowed\"\n : \"cursor-pointer text-[var(--canvas-text)] hover:bg-[var(--canvas-surface)]\",\n className\n )}\n aria-label={type}\n >\n {(type === \"first\" || type === \"prev\") && (\n <Icon className=\"size-[20px]\" />\n )}\n {label && (\n <span className=\"font-semibold\" style={{ fontSize: \"var(--typo-body-s-size)\", lineHeight: \"var(--typo-body-s-line-height)\" }}>{label}</span>\n )}\n {(type === \"next\" || type === \"last\") && (\n <Icon className=\"size-[20px]\" />\n )}\n </button>\n );\n}\n\n/**\n * Ellipsis indicator for truncated pages\n */\nfunction PaginationEllipsis({ className }: { className?: string }) {\n return (\n <div\n className={cn(\n \"flex items-center justify-center\",\n \"w-[24px] h-[32px] rounded-[var(--radius-xs)]\",\n className\n )}\n aria-hidden=\"true\"\n >\n <MoreHorizontal className=\"size-[20px] text-[var(--canvas-text-muted)]\" />\n </div>\n );\n}\n\n// ============================================\n// Helper Functions\n// ============================================\n\n/**\n * Generate array of page numbers to display with ellipsis\n */\nfunction generatePageNumbers(\n currentPage: number,\n totalPages: number,\n maxVisible: number\n): (number | \"ellipsis\")[] {\n if (totalPages <= maxVisible) {\n return Array.from({ length: totalPages }, (_, i) => i + 1);\n }\n\n const pages: (number | \"ellipsis\")[] = [];\n const halfVisible = Math.floor((maxVisible - 2) / 2);\n\n // Always show first page\n pages.push(1);\n\n // Calculate start and end of visible range\n let start = Math.max(2, currentPage - halfVisible);\n let end = Math.min(totalPages - 1, currentPage + halfVisible);\n\n // Adjust if we're near the start\n if (currentPage <= halfVisible + 2) {\n end = Math.min(totalPages - 1, maxVisible - 1);\n start = 2;\n }\n\n // Adjust if we're near the end\n if (currentPage >= totalPages - halfVisible - 1) {\n start = Math.max(2, totalPages - maxVisible + 2);\n end = totalPages - 1;\n }\n\n // Add ellipsis before middle section if needed\n if (start > 2) {\n pages.push(\"ellipsis\");\n }\n\n // Add middle pages\n for (let i = start; i <= end; i++) {\n pages.push(i);\n }\n\n // Add ellipsis after middle section if needed\n if (end < totalPages - 1) {\n pages.push(\"ellipsis\");\n }\n\n // Always show last page\n if (totalPages > 1) {\n pages.push(totalPages);\n }\n\n return pages;\n}\n\n/**\n * Format number with commas (e.g., 2500 -> 2,500)\n */\nfunction formatNumber(num: number): string {\n return num.toLocaleString();\n}\n\n// ============================================\n// Main Component\n// ============================================\n\n/**\n * Canvas Design System - Pagination Component\n *\n * A responsive pagination component with results text, items per page dropdown,\n * and page navigation. Automatically adjusts layout for mobile screens.\n *\n * @example\n * ```tsx\n * // Basic usage\n * <Pagination\n * currentPage={1}\n * totalPages={10}\n * onPageChange={(page) => setPage(page)}\n * />\n *\n * // With all features\n * <Pagination\n * currentPage={1}\n * totalPages={50}\n * totalItems={2500}\n * itemsPerPage={50}\n * itemsPerPageOptions={[30, 50, 100]}\n * showResultsText\n * showItemsPerPage\n * onPageChange={(page) => setPage(page)}\n * onItemsPerPageChange={(perPage) => setPerPage(perPage)}\n * />\n * ```\n */\nexport function Pagination({\n currentPage = 1,\n totalPages = 10,\n totalItems = 2500,\n itemsPerPage = 50,\n itemsPerPageOptions = [30, 50, 100],\n onPageChange,\n onItemsPerPageChange,\n showResultsText = true,\n showItemsPerPage = true,\n maxVisiblePages = 5,\n mobileMaxVisiblePages = 3,\n compact = false,\n className,\n}: PaginationProps) {\n const isMobile = useIsMobile();\n \n // Use fewer visible pages on mobile or when compact mode is forced\n const effectiveMaxVisiblePages = (isMobile || compact) ? mobileMaxVisiblePages : maxVisiblePages;\n \n // Calculate visible page range\n const pageNumbers = useMemo(\n () => generatePageNumbers(currentPage, totalPages, effectiveMaxVisiblePages),\n [currentPage, totalPages, effectiveMaxVisiblePages]\n );\n\n // Calculate results range\n const startItem = (currentPage - 1) * itemsPerPage + 1;\n const endItem = Math.min(currentPage * itemsPerPage, totalItems);\n\n // Navigation handlers\n const goToPage = (page: number) => {\n if (page >= 1 && page <= totalPages && page !== currentPage) {\n onPageChange?.(page);\n }\n };\n\n const goToFirst = () => goToPage(1);\n const goToPrev = () => goToPage(currentPage - 1);\n const goToNext = () => goToPage(currentPage + 1);\n const goToLast = () => goToPage(totalPages);\n\n const isFirstPage = currentPage === 1;\n const isLastPage = currentPage === totalPages;\n\n return (\n <div\n className={cn(\n \"flex items-center justify-between\",\n \"h-[56px] py-[var(--spacing-lg)]\",\n \"w-full\",\n className\n )}\n >\n {/* Left section: Results text & Items per page (desktop only) */}\n <div className=\"hidden md:flex items-center gap-[var(--spacing-2xl)]\">\n {/* Results text */}\n {showResultsText && (\n <span\n className=\"text-[var(--canvas-text)] whitespace-nowrap\"\n style={{ fontFamily: \"var(--typo-global-font)\", fontSize: \"var(--typo-body-s-size)\", lineHeight: \"var(--typo-body-s-line-height)\" }}\n >\n Viewing {formatNumber(startItem)}–{formatNumber(endItem)} of{\" \"}\n {formatNumber(totalItems)} results\n </span>\n )}\n\n {/* Items per page dropdown */}\n {showItemsPerPage && (\n <div className=\"flex items-center gap-[var(--spacing-sm)]\">\n <span\n className=\"text-[14px] leading-[20px] text-[var(--canvas-text)] whitespace-nowrap\"\n style={{ fontFamily: \"var(--typo-global-font)\" }}\n >\n Show per page\n </span>\n <Select\n value={String(itemsPerPage)}\n onValueChange={(value) => onItemsPerPageChange?.(Number(value))}\n >\n <SelectTrigger\n inputSize=\"sm\"\n className=\"w-[70px]\"\n >\n <SelectValue />\n </SelectTrigger>\n <SelectContent position=\"popper\" side=\"bottom\" sideOffset={4}>\n {itemsPerPageOptions.map((option) => (\n <SelectItem key={option} value={String(option)}>\n {option}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n )}\n </div>\n\n {/* Right section: Navigation (always visible, centered on mobile) */}\n <div className=\"flex items-center gap-[var(--spacing-sm)] mx-auto md:mx-0\">\n {/* First & Prev buttons */}\n <div className=\"flex items-center gap-[var(--spacing-sm)]\">\n {!compact && (\n <NavigationButton\n type=\"first\"\n disabled={isFirstPage}\n onClick={goToFirst}\n />\n )}\n <NavigationButton\n type=\"prev\"\n disabled={isFirstPage}\n onClick={goToPrev}\n />\n </div>\n\n {/* Page numbers */}\n <div className=\"flex items-center gap-[var(--spacing-sm)]\">\n {pageNumbers.map((item, index) =>\n item === \"ellipsis\" ? (\n <PaginationEllipsis key={`ellipsis-${index}`} />\n ) : (\n <PaginationButton\n key={item}\n page={item}\n isSelected={item === currentPage}\n onClick={() => goToPage(item)}\n />\n )\n )}\n </div>\n\n {/* Next & Last buttons */}\n <div className=\"flex items-center gap-[var(--spacing-sm)]\">\n <NavigationButton\n type=\"next\"\n disabled={isLastPage}\n onClick={goToNext}\n />\n {!compact && (\n <NavigationButton\n type=\"last\"\n disabled={isLastPage}\n onClick={goToLast}\n />\n )}\n </div>\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/pill-tabs.tsx",
8
8
  "type": "registry:block",
9
- "content": "\"use client\";\n\nimport { cn } from \"../../lib/utils\";\nimport { Typography } from \"../ui/typography\";\nimport {\n Briefcase,\n Star,\n User,\n type LucideIcon,\n} from \"lucide-react\";\n\ntype TabIconType = \"briefcase\" | \"star\" | \"user\";\n\ninterface PillTab {\n id: string;\n label: string;\n icon?: TabIconType;\n}\n\nconst tabIcons: Record<TabIconType, LucideIcon> = {\n briefcase: Briefcase,\n star: Star,\n user: User,\n};\n\nexport interface PillTabsProps {\n tabs: PillTab[];\n activeTab?: string;\n onTabChange?: (tabId: string) => void;\n className?: string;\n}\n\n/**\n * Canvas Design System - Pill Tabs Component\n *\n * A horizontal tab navigation with pill-shaped tabs.\n * Each tab can have an optional icon.\n */\nexport function PillTabs({\n tabs,\n activeTab,\n onTabChange,\n className,\n}: PillTabsProps) {\n return (\n <div className={cn(\"flex gap-[var(--spacing-xl)]\", className)}>\n {tabs.map((tab) => {\n const isActive = activeTab === tab.id;\n const IconComponent = tab.icon ? tabIcons[tab.icon] : null;\n\n return (\n <button\n key={tab.id}\n onClick={() => onTabChange?.(tab.id)}\n className={cn(\n \"flex items-center gap-[var(--spacing-md)] h-10 px-[var(--spacing-xl)] rounded-full transition-colors\",\n \"bg-[var(--canvas-border)]\",\n isActive && \"bg-[var(--canvas-surface-brand)]\"\n )}\n >\n {IconComponent && (\n <IconComponent\n className={cn(\n \"size-4\",\n isActive\n ? \"text-[var(--canvas-text)]\"\n : \"text-[var(--canvas-text)]\"\n )}\n />\n )}\n <Typography\n variant=\"body-s\"\n className={cn(\n isActive\n ? \"text-[var(--canvas-text)]\"\n : \"text-[var(--canvas-text)]\"\n )}\n >\n {tab.label}\n </Typography>\n </button>\n );\n })}\n </div>\n );\n}\n\n"
9
+ "content": "\"use client\";\n\nimport { cn } from \"../../lib/utils\";\nimport { Typography } from \"../ui/typography\";\nimport {\n Briefcase,\n Star,\n User,\n type LucideIcon,\n} from \"lucide-react\";\n\ntype TabIconType = \"briefcase\" | \"star\" | \"user\";\n\ninterface PillTab {\n id: string;\n label: string;\n icon?: TabIconType;\n}\n\nconst tabIcons: Record<TabIconType, LucideIcon> = {\n briefcase: Briefcase,\n star: Star,\n user: User,\n};\n\nexport interface PillTabsProps {\n tabs: PillTab[];\n activeTab?: string;\n onTabChange?: (tabId: string) => void;\n className?: string;\n}\n\n/**\n * Canvas Design System - Pill Tabs Component\n *\n * A horizontal tab navigation with pill-shaped tabs.\n * Each tab can have an optional icon.\n */\nexport function PillTabs({\n tabs,\n activeTab,\n onTabChange,\n className,\n}: PillTabsProps) {\n return (\n <div className={cn(\"flex gap-[var(--spacing-xl)]\", className)}>\n {tabs.map((tab) => {\n const isActive = activeTab === tab.id;\n const IconComponent = tab.icon ? tabIcons[tab.icon] : null;\n\n return (\n <button\n key={tab.id}\n onClick={() => onTabChange?.(tab.id)}\n className={cn(\n \"cursor-pointer flex items-center gap-[var(--spacing-md)] h-10 px-[var(--spacing-xl)] rounded-full transition-colors\",\n \"bg-[var(--canvas-border)]\",\n isActive && \"bg-[var(--canvas-surface-brand)]\"\n )}\n >\n {IconComponent && (\n <IconComponent\n className={cn(\n \"size-4\",\n isActive\n ? \"text-[var(--canvas-text)]\"\n : \"text-[var(--canvas-text)]\"\n )}\n />\n )}\n <Typography\n variant=\"body-s\"\n className={cn(\n isActive\n ? \"text-[var(--canvas-text)]\"\n : \"text-[var(--canvas-text)]\"\n )}\n >\n {tab.label}\n </Typography>\n </button>\n );\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/pricing/pricing-cards.tsx",
8
8
  "type": "registry:block",
9
- "content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { Check, Info, Sparkle } from \"@phosphor-icons/react\";\nimport { Button } from \"../../ui/button\";\nimport { Typography } from \"../../ui/typography\";\n\ninterface PlanFeature {\n text: string;\n hasInfo?: boolean;\n}\n\ninterface PricingPlan {\n name: string;\n price: number;\n period: string;\n description: string;\n features: PlanFeature[];\n isPopular?: boolean;\n hasAI?: boolean;\n}\n\nconst plans: PricingPlan[] = [\n {\n name: \"Starter\",\n price: 25,\n period: \"/month\",\n description: \"Best for hobbyists or individuals\",\n features: [\n { text: \"1 TB storage\", hasInfo: true },\n { text: \"Up to 5 apps and integrations\" },\n { text: \"2 collaborators\" },\n ],\n },\n {\n name: \"Deluxe\",\n price: 70,\n period: \"/month\",\n description: \"Best for small teams\",\n isPopular: true,\n hasAI: true,\n features: [\n { text: \"50 TB storage\" },\n { text: \"Up to 20 apps and integrations\" },\n { text: \"5 collaborators\" },\n { text: \"Dedicated support\" },\n { text: \"Unlimited workspace\" },\n { text: \"Unlimited access\" },\n ],\n },\n {\n name: \"Professional\",\n price: 120,\n period: \"/month\",\n description: \"Best for large teams or enterprises\",\n hasAI: true,\n features: [\n { text: \"Unlimited storage\" },\n { text: \"Up to 50 apps and integrations\" },\n { text: \"12 collaborators\" },\n { text: \"Dedicated support\" },\n { text: \"Unlimited workspace\" },\n { text: \"Unlimited access\" },\n { text: \"Unlimited version history\" },\n ],\n },\n];\n\nexport function PricingCards() {\n const [isAnnual, setIsAnnual] = useState(false);\n\n return (\n <section\n className=\"w-full px-4 md:px-8 lg:px-20 py-16 md:py-24\"\n style={{ backgroundColor: \"var(--canvas-background)\" }}\n >\n <div className=\"w-full max-w-[1240px] mx-auto flex flex-col items-center gap-12\">\n {/* Header */}\n <div className=\"flex flex-col items-center gap-3 text-center\">\n <Typography variant=\"body-s\" as=\"p\" color=\"muted\">\n PRICING\n </Typography>\n <Typography variant=\"h3\" as=\"h1\">\n Choose the best plan for your team\n </Typography>\n <Typography variant=\"body-l\" color=\"muted\">\n Pay by the month or the year, and cancel at any time\n </Typography>\n\n {/* Billing Toggle */}\n <div className=\"flex items-center gap-2.5 mt-2\">\n <button\n onClick={() => setIsAnnual(!isAnnual)}\n className=\"relative w-20 h-11 rounded-full transition-colors\"\n style={{\n backgroundColor: isAnnual\n ? \"var(--canvas-primary)\"\n : \"var(--canvas-border)\",\n }}\n >\n <div\n className=\"absolute top-1 w-9 h-9 bg-[var(--canvas-background)] rounded-full shadow transition-all\"\n style={{\n left: isAnnual ? \"calc(100% - 40px)\" : \"4px\",\n }}\n />\n </button>\n <Typography variant=\"body-m\" as=\"span\">\n Billed annually\n </Typography>\n <span\n className=\"px-2 py-0.5 rounded\"\n style={{\n backgroundColor: \"var(--canvas-surface-brand)\",\n fontFamily: \"var(--typo-global-font)\",\n fontSize: \"12px\",\n fontWeight: 600,\n color: \"var(--canvas-primary)\",\n }}\n >\n SAVE 20%\n </span>\n </div>\n </div>\n\n {/* Pricing Cards */}\n <div className=\"w-full flex flex-col gap-8\">\n <div className=\"grid grid-cols-1 md:grid-cols-3 gap-8\">\n {plans.map((plan) => (\n <div\n key={plan.name}\n className=\"flex flex-col rounded-xl overflow-hidden\"\n style={{\n boxShadow: \"0px 1px 8px 0px rgba(0,0,0,0.03)\",\n }}\n >\n {/* Popular Badge */}\n {plan.isPopular ? (\n <div\n className=\"w-full py-3 text-center\"\n style={{\n backgroundColor: \"var(--canvas-text)\",\n }}\n >\n <Typography variant=\"body-xs\" as=\"span\" style={{ color: \"white\", fontWeight: 600 }}>\n MOST POPULAR\n </Typography>\n </div>\n ) : (\n <div className=\"h-11\" />\n )}\n\n {/* Card Content */}\n <div\n className=\"flex-1 flex flex-col gap-6 p-8\"\n style={{\n backgroundColor: \"var(--canvas-background)\",\n border: \"1px solid var(--canvas-border)\",\n borderRadius: plan.isPopular\n ? \"0 0 var(--radius-md) var(--radius-md)\"\n : \"var(--radius-md)\",\n }}\n >\n {/* Plan Info */}\n <div className=\"flex flex-col gap-3\">\n <Typography variant=\"h5\" as=\"h3\">\n {plan.name}\n </Typography>\n <div className=\"flex items-end gap-1\">\n <Typography variant=\"h2\" as=\"span\">\n ${isAnnual ? Math.round(plan.price * 0.8) : plan.price}\n </Typography>\n <Typography variant=\"body-l\" as=\"span\" color=\"muted\" className=\"pb-2.5\">\n {plan.period}\n </Typography>\n </div>\n <Typography variant=\"body-m\" color=\"muted\">\n {plan.description}\n </Typography>\n </div>\n\n {/* CTA Button */}\n <Button variant=\"primary\" size=\"lg\" className=\"w-full\">\n Select plan\n </Button>\n\n {/* Features List */}\n <div className=\"flex flex-col\">\n {/* AI Badge */}\n {plan.hasAI && (\n <div className=\"flex items-center gap-2 py-2\">\n <Sparkle\n size={20}\n weight=\"fill\"\n style={{ color: \"var(--canvas-primary)\" }}\n />\n <Typography\n variant=\"body-m\"\n as=\"span\"\n style={{ fontWeight: 500, color: \"var(--canvas-primary-hover)\" }}\n >\n AI add-on available\n </Typography>\n <span\n className=\"px-2 py-0.5 rounded\"\n style={{\n backgroundColor: \"var(--canvas-surface-brand)\",\n fontFamily: \"var(--typo-global-font)\",\n fontSize: \"12px\",\n fontWeight: 600,\n color: \"var(--canvas-primary)\",\n }}\n >\n NEW\n </span>\n </div>\n )}\n\n {plan.features.map((feature, idx) => (\n <div\n key={idx}\n className=\"flex items-center gap-2 py-2\"\n >\n <Check\n size={20}\n weight=\"bold\"\n style={{ color: \"var(--canvas-primary-hover)\" }}\n />\n <Typography variant=\"body-m\" as=\"span\" color=\"muted\" className=\"flex-1\">\n {feature.text}\n </Typography>\n {feature.hasInfo && (\n <Info\n size={20}\n style={{ color: \"var(--canvas-text-placeholder)\" }}\n />\n )}\n </div>\n ))}\n </div>\n </div>\n </div>\n ))}\n </div>\n\n {/* Contact Us Banner */}\n <div\n className=\"w-full flex flex-col md:flex-row items-start md:items-end justify-between gap-4 p-8 rounded-xl\"\n style={{\n backgroundColor: \"var(--canvas-background)\",\n border: \"1px solid var(--canvas-border)\",\n boxShadow: \"0px 1px 8px 0px rgba(14,30,47,0.03)\",\n }}\n >\n <div className=\"flex flex-col gap-4 flex-1\">\n <Typography variant=\"h5\" as=\"h3\">\n Contact us\n </Typography>\n <Typography variant=\"body-m\" color=\"muted\">\n For advanced security and more flexible controls, speak to\n someone from our team who will help you scale your business\n quickly with custom add-on features.\n </Typography>\n </div>\n <Button variant=\"primary\" size=\"lg\">\n Talk to us\n </Button>\n </div>\n </div>\n </div>\n </section>\n );\n}\n"
9
+ "content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { Check, Info, Sparkle } from \"@phosphor-icons/react\";\nimport { Button } from \"../../ui/button\";\nimport { Typography } from \"../../ui/typography\";\n\ninterface PlanFeature {\n text: string;\n hasInfo?: boolean;\n}\n\ninterface PricingPlan {\n name: string;\n price: number;\n period: string;\n description: string;\n features: PlanFeature[];\n isPopular?: boolean;\n hasAI?: boolean;\n}\n\nconst plans: PricingPlan[] = [\n {\n name: \"Starter\",\n price: 25,\n period: \"/month\",\n description: \"Best for hobbyists or individuals\",\n features: [\n { text: \"1 TB storage\", hasInfo: true },\n { text: \"Up to 5 apps and integrations\" },\n { text: \"2 collaborators\" },\n ],\n },\n {\n name: \"Deluxe\",\n price: 70,\n period: \"/month\",\n description: \"Best for small teams\",\n isPopular: true,\n hasAI: true,\n features: [\n { text: \"50 TB storage\" },\n { text: \"Up to 20 apps and integrations\" },\n { text: \"5 collaborators\" },\n { text: \"Dedicated support\" },\n { text: \"Unlimited workspace\" },\n { text: \"Unlimited access\" },\n ],\n },\n {\n name: \"Professional\",\n price: 120,\n period: \"/month\",\n description: \"Best for large teams or enterprises\",\n hasAI: true,\n features: [\n { text: \"Unlimited storage\" },\n { text: \"Up to 50 apps and integrations\" },\n { text: \"12 collaborators\" },\n { text: \"Dedicated support\" },\n { text: \"Unlimited workspace\" },\n { text: \"Unlimited access\" },\n { text: \"Unlimited version history\" },\n ],\n },\n];\n\nexport function PricingCards() {\n const [isAnnual, setIsAnnual] = useState(false);\n\n return (\n <section\n className=\"w-full px-4 md:px-8 lg:px-20 py-16 md:py-24\"\n style={{ backgroundColor: \"var(--canvas-background)\" }}\n >\n <div className=\"w-full max-w-[1240px] mx-auto flex flex-col items-center gap-12\">\n {/* Header */}\n <div className=\"flex flex-col items-center gap-3 text-center\">\n <Typography variant=\"body-s\" as=\"p\" color=\"muted\">\n PRICING\n </Typography>\n <Typography variant=\"h3\" as=\"h1\">\n Choose the best plan for your team\n </Typography>\n <Typography variant=\"body-l\" color=\"muted\">\n Pay by the month or the year, and cancel at any time\n </Typography>\n\n {/* Billing Toggle */}\n <div className=\"flex items-center gap-2.5 mt-2\">\n <button\n onClick={() => setIsAnnual(!isAnnual)}\n className=\"cursor-pointer relative w-20 h-11 rounded-full transition-colors\"\n style={{\n backgroundColor: isAnnual\n ? \"var(--canvas-primary)\"\n : \"var(--canvas-border)\",\n }}\n >\n <div\n className=\"absolute top-1 w-9 h-9 bg-[var(--canvas-background)] rounded-full shadow transition-all\"\n style={{\n left: isAnnual ? \"calc(100% - 40px)\" : \"4px\",\n }}\n />\n </button>\n <Typography variant=\"body-m\" as=\"span\">\n Billed annually\n </Typography>\n <span\n className=\"px-2 py-0.5 rounded\"\n style={{\n backgroundColor: \"var(--canvas-surface-brand)\",\n fontFamily: \"var(--typo-global-font)\",\n fontSize: \"12px\",\n fontWeight: 600,\n color: \"var(--canvas-primary)\",\n }}\n >\n SAVE 20%\n </span>\n </div>\n </div>\n\n {/* Pricing Cards */}\n <div className=\"w-full flex flex-col gap-8\">\n <div className=\"grid grid-cols-1 md:grid-cols-3 gap-8\">\n {plans.map((plan) => (\n <div\n key={plan.name}\n className=\"flex flex-col rounded-xl overflow-hidden\"\n style={{\n boxShadow: \"0px 1px 8px 0px rgba(0,0,0,0.03)\",\n }}\n >\n {/* Popular Badge */}\n {plan.isPopular ? (\n <div\n className=\"w-full py-3 text-center\"\n style={{\n backgroundColor: \"var(--canvas-text)\",\n }}\n >\n <Typography variant=\"body-xs\" as=\"span\" style={{ color: \"white\", fontWeight: 600 }}>\n MOST POPULAR\n </Typography>\n </div>\n ) : (\n <div className=\"h-11\" />\n )}\n\n {/* Card Content */}\n <div\n className=\"flex-1 flex flex-col gap-6 p-8\"\n style={{\n backgroundColor: \"var(--canvas-background)\",\n border: \"1px solid var(--canvas-border)\",\n borderRadius: plan.isPopular\n ? \"0 0 var(--radius-md) var(--radius-md)\"\n : \"var(--radius-md)\",\n }}\n >\n {/* Plan Info */}\n <div className=\"flex flex-col gap-3\">\n <Typography variant=\"h5\" as=\"h3\">\n {plan.name}\n </Typography>\n <div className=\"flex items-end gap-1\">\n <Typography variant=\"h2\" as=\"span\">\n ${isAnnual ? Math.round(plan.price * 0.8) : plan.price}\n </Typography>\n <Typography variant=\"body-l\" as=\"span\" color=\"muted\" className=\"pb-2.5\">\n {plan.period}\n </Typography>\n </div>\n <Typography variant=\"body-m\" color=\"muted\">\n {plan.description}\n </Typography>\n </div>\n\n {/* CTA Button */}\n <Button variant=\"primary\" size=\"lg\" className=\"w-full\">\n Select plan\n </Button>\n\n {/* Features List */}\n <div className=\"flex flex-col\">\n {/* AI Badge */}\n {plan.hasAI && (\n <div className=\"flex items-center gap-2 py-2\">\n <Sparkle\n size={20}\n weight=\"fill\"\n style={{ color: \"var(--canvas-primary)\" }}\n />\n <Typography\n variant=\"body-m\"\n as=\"span\"\n style={{ fontWeight: 500, color: \"var(--canvas-primary-hover)\" }}\n >\n AI add-on available\n </Typography>\n <span\n className=\"px-2 py-0.5 rounded\"\n style={{\n backgroundColor: \"var(--canvas-surface-brand)\",\n fontFamily: \"var(--typo-global-font)\",\n fontSize: \"12px\",\n fontWeight: 600,\n color: \"var(--canvas-primary)\",\n }}\n >\n NEW\n </span>\n </div>\n )}\n\n {plan.features.map((feature, idx) => (\n <div\n key={idx}\n className=\"flex items-center gap-2 py-2\"\n >\n <Check\n size={20}\n weight=\"bold\"\n style={{ color: \"var(--canvas-primary-hover)\" }}\n />\n <Typography variant=\"body-m\" as=\"span\" color=\"muted\" className=\"flex-1\">\n {feature.text}\n </Typography>\n {feature.hasInfo && (\n <Info\n size={20}\n style={{ color: \"var(--canvas-text-placeholder)\" }}\n />\n )}\n </div>\n ))}\n </div>\n </div>\n </div>\n ))}\n </div>\n\n {/* Contact Us Banner */}\n <div\n className=\"w-full flex flex-col md:flex-row items-start md:items-end justify-between gap-4 p-8 rounded-xl\"\n style={{\n backgroundColor: \"var(--canvas-background)\",\n border: \"1px solid var(--canvas-border)\",\n boxShadow: \"0px 1px 8px 0px rgba(14,30,47,0.03)\",\n }}\n >\n <div className=\"flex flex-col gap-4 flex-1\">\n <Typography variant=\"h5\" as=\"h3\">\n Contact us\n </Typography>\n <Typography variant=\"body-m\" color=\"muted\">\n For advanced security and more flexible controls, speak to\n someone from our team who will help you scale your business\n quickly with custom add-on features.\n </Typography>\n </div>\n <Button variant=\"primary\" size=\"lg\">\n Talk to us\n </Button>\n </div>\n </div>\n </div>\n </section>\n );\n}\n"
10
10
  }
11
11
  ],
12
12
  "dependencies": [
@@ -6,7 +6,7 @@
6
6
  {
7
7
  "path": "components/blocks/profile-card.tsx",
8
8
  "type": "registry:block",
9
- "content": "\"use client\";\n\nimport { cn } from \"../../lib/utils\";\nimport { Avatar, AvatarImage, AvatarFallback } from \"../ui/avatar\";\nimport { Typography } from \"../ui/typography\";\nimport {\n MapPin,\n Calendar,\n Globe,\n Star,\n Facebook,\n Twitter,\n Linkedin,\n Instagram,\n MoreHorizontal,\n} from \"lucide-react\";\n\ninterface ProfileStat {\n value: string;\n label: string;\n}\n\ninterface ProfileTag {\n label: string;\n}\n\ninterface SocialLink {\n type: \"website\" | \"facebook\" | \"twitter\" | \"linkedin\" | \"instagram\";\n label: string;\n href?: string;\n}\n\nexport interface ProfileCardProps {\n /** Profile avatar image URL */\n avatarUrl?: string;\n /** Avatar fallback initials */\n avatarFallback?: string;\n /** Whether to show online status indicator */\n showStatus?: boolean;\n /** User's display name */\n name: string;\n /** User's username (with @) */\n username: string;\n /** Star rating (0-5) */\n rating?: number;\n /** Location text */\n location?: string;\n /** Join date text */\n joinDate?: string;\n /** Stats to display */\n stats?: ProfileStat[];\n /** Bio text */\n bio?: string;\n /** Tags/skills */\n tags?: ProfileTag[];\n /** Social links */\n socialLinks?: SocialLink[];\n /** Additional class names */\n className?: string;\n /** Show menu button */\n showMenu?: boolean;\n /** Menu click handler */\n onMenuClick?: () => void;\n}\n\nconst socialIcons = {\n website: Globe,\n facebook: Facebook,\n twitter: Twitter,\n linkedin: Linkedin,\n instagram: Instagram,\n};\n\n/**\n * Canvas Design System - Profile Card Component\n *\n * A centered profile card with avatar overlapping banner, stats, tags, and social links.\n * Uses CSS variables for theming to support live preview.\n */\nexport function ProfileCard({\n avatarUrl,\n avatarFallback = \"JC\",\n showStatus = true,\n name,\n username,\n rating = 5,\n location,\n joinDate,\n stats = [],\n bio,\n tags = [],\n socialLinks = [],\n className,\n showMenu = true,\n onMenuClick,\n}: ProfileCardProps) {\n return (\n <div\n className={cn(\n \"flex flex-col items-center w-full max-w-[992px] mx-auto\",\n className\n )}\n >\n {/* Avatar Section - Overlaps banner via negative margin in parent */}\n <div className=\"relative flex flex-col items-center w-full\">\n {/* Avatar with Status */}\n <div className=\"relative -mt-16\">\n <Avatar className=\"size-32 border-4 border-[var(--canvas-background)]\">\n <AvatarImage src={avatarUrl} alt={name} />\n <AvatarFallback\n className=\"font-semibold bg-[var(--canvas-surface)] text-[var(--canvas-text-muted)]\"\n style={{ fontSize: \"var(--typo-body-xl-size)\" }}\n >\n {avatarFallback}\n </AvatarFallback>\n </Avatar>\n {showStatus && (\n <div className=\"absolute bottom-[9px] right-[9px] size-5 rounded-full bg-[var(--canvas-success)] border-[3px] border-[var(--canvas-background)]\" />\n )}\n </div>\n\n {/* Menu Button - Positioned to the right */}\n {showMenu && (\n <button\n onClick={onMenuClick}\n className=\"absolute top-4 right-0 flex items-center justify-center size-7 rounded-[var(--radius-xs)] border border-[var(--canvas-border)] bg-[var(--canvas-background)] hover:bg-[var(--canvas-surface)] transition-colors\"\n aria-label=\"More options\"\n >\n <MoreHorizontal className=\"size-4 text-[var(--canvas-text-muted)]\" />\n </button>\n )}\n </div>\n\n {/* Name & Username - Centered */}\n <div className=\"flex flex-col items-center gap-1 mt-4\">\n <Typography variant=\"body-xl\" className=\"text-center\" style={{ fontWeight: 600 }}>\n {name}\n </Typography>\n <Typography variant=\"body-s\" color=\"muted\" className=\"text-center\">\n {username}\n </Typography>\n </div>\n\n {/* Star Rating - Centered */}\n {rating > 0 && (\n <div className=\"flex items-center gap-0.5 mt-3\">\n {[...Array(5)].map((_, i) => (\n <Star\n key={i}\n className={cn(\n \"size-4\",\n i < rating\n ? \"fill-[var(--canvas-primary)] text-[var(--canvas-primary)]\"\n : \"fill-[var(--canvas-border)] text-[var(--canvas-border)]\"\n )}\n />\n ))}\n </div>\n )}\n\n {/* Location & Join Date - Centered */}\n <div className=\"flex flex-col items-center gap-2 mt-4\">\n {location && (\n <div className=\"flex items-center gap-[var(--spacing-sm)]\">\n <MapPin className=\"size-4 text-[var(--canvas-text-muted)]\" />\n <Typography variant=\"body-s\" color=\"muted\">\n {location}\n </Typography>\n </div>\n )}\n {joinDate && (\n <div className=\"flex items-center gap-[var(--spacing-sm)]\">\n <Calendar className=\"size-4 text-[var(--canvas-text-muted)]\" />\n <Typography variant=\"body-s\" color=\"muted\">\n {joinDate}\n </Typography>\n </div>\n )}\n </div>\n\n {/* Stats Section with top border */}\n {stats.length > 0 && (\n <div className=\"flex items-center justify-center gap-[var(--spacing-3xl)] w-full mt-6 pt-6 border-t border-[var(--canvas-border)]\">\n {stats.map((stat, index) => (\n <div key={index} className=\"flex flex-col items-center gap-0.5\">\n <Typography variant=\"body-l\" style={{ fontWeight: 600 }}>\n {stat.value}\n </Typography>\n <Typography variant=\"body-s\" color=\"muted\" className=\"text-center\">\n {stat.label}\n </Typography>\n </div>\n ))}\n </div>\n )}\n\n {/* Bio - Centered */}\n {bio && (\n <div className=\"w-full max-w-[576px] mt-6\">\n <Typography variant=\"body-s\" className=\"text-center\">\n {bio}\n </Typography>\n </div>\n )}\n\n {/* Tags - Centered */}\n {tags.length > 0 && (\n <div className=\"flex flex-wrap justify-center gap-[var(--spacing-md)] mt-6\">\n {tags.map((tag, index) => (\n <span\n key={index}\n className=\"px-[var(--spacing-lg)] py-[var(--spacing-xs)] h-7 flex items-center rounded-[var(--radius-xs)] bg-[var(--canvas-surface-brand)] text-[var(--canvas-primary)]\"\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 500,\n lineHeight: \"var(--typo-body-s-line-height)\",\n }}\n >\n {tag.label}\n </span>\n ))}\n </div>\n )}\n\n {/* Social Links - Top border only */}\n {socialLinks.length > 0 && (\n <div className=\"flex items-center justify-center w-full mt-6 pt-6 border-t border-[var(--canvas-border)]\">\n <div className=\"flex items-center justify-between w-full max-w-[700px]\">\n {socialLinks.map((link, index) => {\n const IconComponent = socialIcons[link.type];\n return (\n <a\n key={index}\n href={link.href || \"#\"}\n className=\"flex items-center gap-[var(--spacing-sm)] text-[var(--canvas-text-muted)] hover:text-[var(--canvas-text)] transition-colors\"\n >\n <IconComponent className=\"size-4\" />\n <Typography variant=\"body-s\" color=\"muted\" as=\"span\">\n {link.label}\n </Typography>\n </a>\n );\n })}\n </div>\n </div>\n )}\n </div>\n );\n}\n"
9
+ "content": "\"use client\";\n\nimport { cn } from \"../../lib/utils\";\nimport { Avatar, AvatarImage, AvatarFallback } from \"../ui/avatar\";\nimport { Typography } from \"../ui/typography\";\nimport {\n MapPin,\n Calendar,\n Globe,\n Star,\n Facebook,\n Twitter,\n Linkedin,\n Instagram,\n MoreHorizontal,\n} from \"lucide-react\";\n\ninterface ProfileStat {\n value: string;\n label: string;\n}\n\ninterface ProfileTag {\n label: string;\n}\n\ninterface SocialLink {\n type: \"website\" | \"facebook\" | \"twitter\" | \"linkedin\" | \"instagram\";\n label: string;\n href?: string;\n}\n\nexport interface ProfileCardProps {\n /** Profile avatar image URL */\n avatarUrl?: string;\n /** Avatar fallback initials */\n avatarFallback?: string;\n /** Whether to show online status indicator */\n showStatus?: boolean;\n /** User's display name */\n name: string;\n /** User's username (with @) */\n username: string;\n /** Star rating (0-5) */\n rating?: number;\n /** Location text */\n location?: string;\n /** Join date text */\n joinDate?: string;\n /** Stats to display */\n stats?: ProfileStat[];\n /** Bio text */\n bio?: string;\n /** Tags/skills */\n tags?: ProfileTag[];\n /** Social links */\n socialLinks?: SocialLink[];\n /** Additional class names */\n className?: string;\n /** Show menu button */\n showMenu?: boolean;\n /** Menu click handler */\n onMenuClick?: () => void;\n}\n\nconst socialIcons = {\n website: Globe,\n facebook: Facebook,\n twitter: Twitter,\n linkedin: Linkedin,\n instagram: Instagram,\n};\n\n/**\n * Canvas Design System - Profile Card Component\n *\n * A centered profile card with avatar overlapping banner, stats, tags, and social links.\n * Uses CSS variables for theming to support live preview.\n */\nexport function ProfileCard({\n avatarUrl,\n avatarFallback = \"JC\",\n showStatus = true,\n name,\n username,\n rating = 5,\n location,\n joinDate,\n stats = [],\n bio,\n tags = [],\n socialLinks = [],\n className,\n showMenu = true,\n onMenuClick,\n}: ProfileCardProps) {\n return (\n <div\n className={cn(\n \"flex flex-col items-center w-full max-w-[992px] mx-auto\",\n className\n )}\n >\n {/* Avatar Section - Overlaps banner via negative margin in parent */}\n <div className=\"relative flex flex-col items-center w-full\">\n {/* Avatar with Status */}\n <div className=\"relative -mt-16\">\n <Avatar className=\"size-32 border-4 border-[var(--canvas-background)]\">\n <AvatarImage src={avatarUrl} alt={name} />\n <AvatarFallback\n className=\"font-semibold bg-[var(--canvas-surface)] text-[var(--canvas-text-muted)]\"\n style={{ fontSize: \"var(--typo-body-xl-size)\" }}\n >\n {avatarFallback}\n </AvatarFallback>\n </Avatar>\n {showStatus && (\n <div className=\"absolute bottom-[9px] right-[9px] size-5 rounded-full bg-[var(--canvas-success)] border-[3px] border-[var(--canvas-background)]\" />\n )}\n </div>\n\n {/* Menu Button - Positioned to the right */}\n {showMenu && (\n <button\n onClick={onMenuClick}\n className=\"cursor-pointer absolute top-4 right-0 flex items-center justify-center size-7 rounded-[var(--radius-xs)] border border-[var(--canvas-border)] bg-[var(--canvas-background)] hover:bg-[var(--canvas-surface)] transition-colors\"\n aria-label=\"More options\"\n >\n <MoreHorizontal className=\"size-4 text-[var(--canvas-text-muted)]\" />\n </button>\n )}\n </div>\n\n {/* Name & Username - Centered */}\n <div className=\"flex flex-col items-center gap-1 mt-4\">\n <Typography variant=\"body-xl\" className=\"text-center\" style={{ fontWeight: 600 }}>\n {name}\n </Typography>\n <Typography variant=\"body-s\" color=\"muted\" className=\"text-center\">\n {username}\n </Typography>\n </div>\n\n {/* Star Rating - Centered */}\n {rating > 0 && (\n <div className=\"flex items-center gap-0.5 mt-3\">\n {[...Array(5)].map((_, i) => (\n <Star\n key={i}\n className={cn(\n \"size-4\",\n i < rating\n ? \"fill-[var(--canvas-primary)] text-[var(--canvas-primary)]\"\n : \"fill-[var(--canvas-border)] text-[var(--canvas-border)]\"\n )}\n />\n ))}\n </div>\n )}\n\n {/* Location & Join Date - Centered */}\n <div className=\"flex flex-col items-center gap-2 mt-4\">\n {location && (\n <div className=\"flex items-center gap-[var(--spacing-sm)]\">\n <MapPin className=\"size-4 text-[var(--canvas-text-muted)]\" />\n <Typography variant=\"body-s\" color=\"muted\">\n {location}\n </Typography>\n </div>\n )}\n {joinDate && (\n <div className=\"flex items-center gap-[var(--spacing-sm)]\">\n <Calendar className=\"size-4 text-[var(--canvas-text-muted)]\" />\n <Typography variant=\"body-s\" color=\"muted\">\n {joinDate}\n </Typography>\n </div>\n )}\n </div>\n\n {/* Stats Section with top border */}\n {stats.length > 0 && (\n <div className=\"flex items-center justify-center gap-[var(--spacing-3xl)] w-full mt-6 pt-6 border-t border-[var(--canvas-border)]\">\n {stats.map((stat, index) => (\n <div key={index} className=\"flex flex-col items-center gap-0.5\">\n <Typography variant=\"body-l\" style={{ fontWeight: 600 }}>\n {stat.value}\n </Typography>\n <Typography variant=\"body-s\" color=\"muted\" className=\"text-center\">\n {stat.label}\n </Typography>\n </div>\n ))}\n </div>\n )}\n\n {/* Bio - Centered */}\n {bio && (\n <div className=\"w-full max-w-[576px] mt-6\">\n <Typography variant=\"body-s\" className=\"text-center\">\n {bio}\n </Typography>\n </div>\n )}\n\n {/* Tags - Centered */}\n {tags.length > 0 && (\n <div className=\"flex flex-wrap justify-center gap-[var(--spacing-md)] mt-6\">\n {tags.map((tag, index) => (\n <span\n key={index}\n className=\"px-[var(--spacing-lg)] py-[var(--spacing-xs)] h-7 flex items-center rounded-[var(--radius-xs)] bg-[var(--canvas-surface-brand)] text-[var(--canvas-primary)]\"\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 500,\n lineHeight: \"var(--typo-body-s-line-height)\",\n }}\n >\n {tag.label}\n </span>\n ))}\n </div>\n )}\n\n {/* Social Links - Top border only */}\n {socialLinks.length > 0 && (\n <div className=\"flex items-center justify-center w-full mt-6 pt-6 border-t border-[var(--canvas-border)]\">\n <div className=\"flex items-center justify-between w-full max-w-[700px]\">\n {socialLinks.map((link, index) => {\n const IconComponent = socialIcons[link.type];\n return (\n <a\n key={index}\n href={link.href || \"#\"}\n className=\"cursor-pointer flex items-center gap-[var(--spacing-sm)] text-[var(--canvas-text-muted)] hover:text-[var(--canvas-text)] transition-colors\"\n >\n <IconComponent className=\"size-4\" />\n <Typography variant=\"body-s\" color=\"muted\" as=\"span\">\n {link.label}\n </Typography>\n </a>\n );\n })}\n </div>\n </div>\n )}\n </div>\n );\n}\n"
10
10
  }
11
11
  ],
12
12
  "dependencies": [
@@ -6,7 +6,7 @@
6
6
  {
7
7
  "path": "components/blocks/prompt-template.tsx",
8
8
  "type": "registry:block",
9
- "content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { Check, Copy, Sparkles } from \"lucide-react\";\nimport { cn } from \"../../lib/utils\";\n\n// ═══════════════════════════════════════════════════════════\n// PROMPT TEMPLATE - Main copyable prompt block\n// ═══════════════════════════════════════════════════════════\n\ninterface PromptTemplateProps {\n prompt: string;\n title?: string;\n className?: string;\n}\n\nexport function PromptTemplate({ \n prompt, \n title = \"Generate with Cursor\",\n className \n}: PromptTemplateProps) {\n const [copied, setCopied] = useState(false);\n\n const handleCopy = async () => {\n await navigator.clipboard.writeText(prompt);\n setCopied(true);\n setTimeout(() => setCopied(false), 2000);\n };\n\n return (\n <div\n className={cn(\n \"relative rounded-lg border border-dashed\",\n \"border-[var(--canvas-border)] bg-[var(--canvas-surface)]\",\n \"p-4\",\n className\n )}\n >\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 {title}\n </div>\n <button\n onClick={handleCopy}\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 {prompt}\n </pre>\n </div>\n );\n}\n\n// ═══════════════════════════════════════════════════════════\n// MINI PROMPT CHIP - Small copyable prompt button\n// ═══════════════════════════════════════════════════════════\n\ninterface MiniPromptChipProps {\n label: string;\n prompt: string;\n}\n\nexport function MiniPromptChip({ label, prompt }: MiniPromptChipProps) {\n const [copied, setCopied] = useState(false);\n\n const handleCopy = async () => {\n await navigator.clipboard.writeText(prompt);\n setCopied(true);\n setTimeout(() => setCopied(false), 2000);\n };\n\n return (\n <button\n onClick={handleCopy}\n className={cn(\n \"inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full font-medium transition-all\",\n copied\n ? \"bg-[var(--canvas-success-surface)] text-[var(--canvas-success)]\"\n : \"bg-[var(--canvas-surface)] text-[var(--canvas-text-muted)] hover:bg-[var(--canvas-surface-hover)] hover:text-[var(--canvas-text)] border border-[var(--canvas-border)]\"\n )}\n style={{ fontSize: \"var(--typo-body-xs-size)\" }}\n title={`Copy: ${prompt.slice(0, 100)}...`}\n >\n {copied ? <Check className=\"size-3\" /> : <Copy className=\"size-3\" />}\n {copied ? \"Copied!\" : label}\n </button>\n );\n}\n\n// ═══════════════════════════════════════════════════════════\n// PROMPT CHIPS ROW - Group of mini prompts\n// ═══════════════════════════════════════════════════════════\n\ninterface PromptChip {\n label: string;\n prompt: string;\n}\n\ninterface PromptChipsRowProps {\n chips: PromptChip[];\n label?: string;\n}\n\nexport function PromptChipsRow({ chips, label = \"More prompts:\" }: PromptChipsRowProps) {\n return (\n <div className=\"pt-4 border-t border-[var(--canvas-border)]\">\n <p className=\"text-[var(--canvas-text-muted)] mb-2 font-medium\" style={{ fontSize: \"var(--typo-body-xs-size)\" }}>\n {label}\n </p>\n <div className=\"flex flex-wrap gap-2\">\n {chips.map((chip) => (\n <MiniPromptChip key={chip.label} label={chip.label} prompt={chip.prompt} />\n ))}\n </div>\n </div>\n );\n}\n"
9
+ "content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { Check, Copy, Sparkles } from \"lucide-react\";\nimport { cn } from \"../../lib/utils\";\n\n// ═══════════════════════════════════════════════════════════\n// PROMPT TEMPLATE - Main copyable prompt block\n// ═══════════════════════════════════════════════════════════\n\ninterface PromptTemplateProps {\n prompt: string;\n title?: string;\n className?: string;\n}\n\nexport function PromptTemplate({ \n prompt, \n title = \"Generate with Cursor\",\n className \n}: PromptTemplateProps) {\n const [copied, setCopied] = useState(false);\n\n const handleCopy = async () => {\n await navigator.clipboard.writeText(prompt);\n setCopied(true);\n setTimeout(() => setCopied(false), 2000);\n };\n\n return (\n <div\n className={cn(\n \"relative rounded-lg border border-dashed\",\n \"border-[var(--canvas-border)] bg-[var(--canvas-surface)]\",\n \"p-4\",\n className\n )}\n >\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 {title}\n </div>\n <button\n onClick={handleCopy}\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 {prompt}\n </pre>\n </div>\n );\n}\n\n// ═══════════════════════════════════════════════════════════\n// MINI PROMPT CHIP - Small copyable prompt button\n// ═══════════════════════════════════════════════════════════\n\ninterface MiniPromptChipProps {\n label: string;\n prompt: string;\n}\n\nexport function MiniPromptChip({ label, prompt }: MiniPromptChipProps) {\n const [copied, setCopied] = useState(false);\n\n const handleCopy = async () => {\n await navigator.clipboard.writeText(prompt);\n setCopied(true);\n setTimeout(() => setCopied(false), 2000);\n };\n\n return (\n <button\n onClick={handleCopy}\n className={cn(\n \"cursor-pointer inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full font-medium transition-all\",\n copied\n ? \"bg-[var(--canvas-success-surface)] text-[var(--canvas-success)]\"\n : \"bg-[var(--canvas-surface)] text-[var(--canvas-text-muted)] hover:bg-[var(--canvas-surface-hover)] hover:text-[var(--canvas-text)] border border-[var(--canvas-border)]\"\n )}\n style={{ fontSize: \"var(--typo-body-xs-size)\" }}\n title={`Copy: ${prompt.slice(0, 100)}...`}\n >\n {copied ? <Check className=\"size-3\" /> : <Copy className=\"size-3\" />}\n {copied ? \"Copied!\" : label}\n </button>\n );\n}\n\n// ═══════════════════════════════════════════════════════════\n// PROMPT CHIPS ROW - Group of mini prompts\n// ═══════════════════════════════════════════════════════════\n\ninterface PromptChip {\n label: string;\n prompt: string;\n}\n\ninterface PromptChipsRowProps {\n chips: PromptChip[];\n label?: string;\n}\n\nexport function PromptChipsRow({ chips, label = \"More prompts:\" }: PromptChipsRowProps) {\n return (\n <div className=\"pt-4 border-t border-[var(--canvas-border)]\">\n <p className=\"text-[var(--canvas-text-muted)] mb-2 font-medium\" style={{ fontSize: \"var(--typo-body-xs-size)\" }}>\n {label}\n </p>\n <div className=\"flex flex-wrap gap-2\">\n {chips.map((chip) => (\n <MiniPromptChip key={chip.label} label={chip.label} prompt={chip.prompt} />\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/screen-flowchart.tsx",
8
8
  "type": "registry:block",
9
- "content": "\"use client\";\n\nimport { useMemo, useCallback } from \"react\";\nimport ReactFlow, {\n Node,\n Edge,\n Controls,\n Background,\n BackgroundVariant,\n useNodesState,\n useEdgesState,\n Handle,\n Position,\n NodeProps,\n MarkerType,\n} from \"reactflow\";\nimport \"reactflow/dist/style.css\";\nimport { cn } from \"../../lib/utils\";\nimport type { Screen, ScreenConnection } from \"../../types/project\";\nimport { ExternalLink } from \"lucide-react\";\n\n// ═══════════════════════════════════════════════════════════\n// SCREEN NODE - Custom React Flow node\n// ═══════════════════════════════════════════════════════════\n\ninterface ScreenNodeData {\n screen: Screen;\n childScreens: Screen[];\n}\n\nfunction ScreenNode({ data, selected }: NodeProps<ScreenNodeData>) {\n const { screen, childScreens } = data;\n const hasChildren = childScreens.length > 0;\n\n const statusColors = {\n draft: { bg: \"var(--canvas-warning-surface)\", text: \"var(--canvas-warning)\", border: \"var(--canvas-warning)\" },\n review: { bg: \"var(--canvas-info-surface)\", text: \"var(--canvas-info)\", border: \"var(--canvas-info)\" },\n approved: { bg: \"var(--canvas-success-surface)\", text: \"var(--canvas-success)\", border: \"var(--canvas-success)\" },\n };\n\n const typeLabels = {\n page: null,\n tab: \"Tab\",\n modal: \"Modal\",\n drawer: \"Drawer\",\n state: \"State\",\n };\n\n return (\n <div\n className={cn(\n \"rounded-lg border-2 bg-[var(--canvas-background)] shadow-sm min-w-[160px]\",\n selected\n ? \"border-[var(--canvas-primary)] shadow-md\"\n : \"border-[var(--canvas-border)]\"\n )}\n >\n {/* Input handle */}\n <Handle\n type=\"target\"\n position={Position.Top}\n className=\"!w-3 !h-3 !bg-[var(--canvas-primary)] !border-2 !border-white\"\n />\n\n {/* Main content */}\n <div className=\"p-3\">\n <div className=\"flex items-center gap-2 mb-1\">\n <span style={{ fontSize: \"var(--typo-body-l-size)\" }}>{screen.icon || \"📄\"}</span>\n <span className=\"font-medium text-[var(--canvas-text)] truncate\" style={{ fontSize: \"var(--typo-body-s-size)\" }}>\n {screen.name}\n </span>\n </div>\n <div className=\"flex items-center gap-2\">\n <span className=\"text-[var(--canvas-text-muted)] font-mono\" style={{ fontSize: \"var(--typo-body-xs-size)\" }}>\n /{screen.slug}\n </span>\n {typeLabels[screen.type] && (\n <span className=\"text-[10px] px-1.5 py-0.5 rounded bg-[var(--canvas-surface)] text-[var(--canvas-text-muted)]\">\n {typeLabels[screen.type]}\n </span>\n )}\n </div>\n\n {/* Status badge */}\n <div className=\"flex items-center justify-between mt-2 pt-2 border-t border-[var(--canvas-border)]\">\n <span\n className=\"text-[10px] px-1.5 py-0.5 rounded border font-medium capitalize\"\n style={{\n backgroundColor: statusColors[screen.status].bg,\n color: statusColors[screen.status].text,\n borderColor: statusColors[screen.status].border,\n }}\n >\n {screen.status}\n </span>\n </div>\n </div>\n\n {/* Children list */}\n {hasChildren && (\n <div className=\"border-t border-[var(--canvas-border)]\">\n {childScreens.map((child, i) => (\n <div\n key={child.id}\n className={cn(\n \"px-3 py-2 flex items-center gap-2\",\n \"hover:bg-[var(--canvas-surface)]\",\n i < childScreens.length - 1 && \"border-b border-[var(--canvas-border)]\"\n )}\n >\n <span className=\"text-[var(--canvas-text-muted)]\">\n {child.type === \"modal\" && \"◻️\"}\n {child.type === \"tab\" && \"📑\"}\n {child.type === \"drawer\" && \"📥\"}\n {child.type === \"state\" && \"🔄\"}\n </span>\n <span className=\"text-[var(--canvas-text)] truncate\" style={{ fontSize: \"var(--typo-body-s-size)\" }}>\n {child.name}\n </span>\n <span className=\"text-[var(--canvas-text-muted)] font-mono ml-auto\" style={{ fontSize: \"var(--typo-body-xs-size)\" }}>\n /{child.slug}\n </span>\n </div>\n ))}\n </div>\n )}\n\n {/* Output handle */}\n <Handle\n type=\"source\"\n position={Position.Bottom}\n className=\"!w-3 !h-3 !bg-[var(--canvas-primary)] !border-2 !border-white\"\n />\n\n {/* Right handle for branching */}\n <Handle\n type=\"source\"\n position={Position.Right}\n id=\"right\"\n className=\"!w-3 !h-3 !bg-[var(--canvas-text-muted)] !border-2 !border-white\"\n />\n </div>\n );\n}\n\n// ═══════════════════════════════════════════════════════════\n// NODE TYPES\n// ═══════════════════════════════════════════════════════════\n\nconst nodeTypes = {\n screen: ScreenNode,\n};\n\n// ═══════════════════════════════════════════════════════════\n// SCREEN FLOWCHART\n// ═══════════════════════════════════════════════════════════\n\ninterface ScreenFlowchartProps {\n screens: Screen[];\n connections: ScreenConnection[];\n className?: string;\n onOpenFullscreen?: () => void;\n}\n\nexport function ScreenFlowchart({\n screens,\n connections,\n className,\n onOpenFullscreen,\n}: ScreenFlowchartProps) {\n // Transform screens to React Flow nodes\n const initialNodes = useMemo(() => {\n // Only show top-level screens (no parentId)\n const topLevelScreens = screens.filter((s) => !s.parentId);\n\n return topLevelScreens.map((screen) => ({\n id: screen.id,\n type: \"screen\",\n position: screen.position,\n data: {\n screen,\n childScreens: screens.filter((s) => s.parentId === screen.id),\n },\n }));\n }, [screens]);\n\n // Transform connections to React Flow edges\n const initialEdges = useMemo(() => {\n return connections.map((conn) => ({\n id: conn.id,\n source: conn.sourceId,\n target: conn.targetId,\n label: conn.label,\n type: \"smoothstep\",\n animated: conn.type === \"redirect\",\n style: { stroke: \"var(--canvas-primary)\", strokeWidth: 2 },\n labelStyle: { \n fill: \"var(--canvas-text-muted)\", \n fontSize: 11,\n fontWeight: 500,\n },\n labelBgStyle: { \n fill: \"var(--canvas-background)\", \n fillOpacity: 0.9,\n },\n labelBgPadding: [4, 2] as [number, number],\n labelBgBorderRadius: 4,\n markerEnd: {\n type: MarkerType.ArrowClosed,\n color: \"var(--canvas-primary)\",\n },\n }));\n }, [connections]);\n\n const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);\n const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);\n\n if (screens.length === 0) {\n return null;\n }\n\n return (\n <div className={cn(\"relative\", className)}>\n {/* Fullscreen button */}\n {onOpenFullscreen && (\n <button\n onClick={onOpenFullscreen}\n className=\"absolute top-3 right-3 z-10 flex items-center gap-1.5 px-3 py-1.5 rounded-md font-medium bg-[var(--canvas-background)] text-[var(--canvas-text-muted)] border border-[var(--canvas-border)] hover:border-[var(--canvas-primary)] hover:text-[var(--canvas-primary)] transition-colors\"\n style={{ fontSize: \"var(--typo-body-xs-size)\" }}\n >\n <ExternalLink className=\"size-3\" />\n Open in new tab\n </button>\n )}\n\n <ReactFlow\n nodes={nodes}\n edges={edges}\n onNodesChange={onNodesChange}\n onEdgesChange={onEdgesChange}\n nodeTypes={nodeTypes}\n fitView\n fitViewOptions={{ padding: 0.2 }}\n minZoom={0.25}\n maxZoom={2}\n className=\"bg-[var(--canvas-surface)] rounded-lg border border-[var(--canvas-border)]\"\n >\n <Background variant={BackgroundVariant.Dots} gap={20} size={1} />\n <Controls className=\"!bg-[var(--canvas-background)] !border-[var(--canvas-border)] !shadow-sm\" />\n </ReactFlow>\n </div>\n );\n}\n"
9
+ "content": "\"use client\";\n\nimport { useMemo, useCallback } from \"react\";\nimport ReactFlow, {\n Node,\n Edge,\n Controls,\n Background,\n BackgroundVariant,\n useNodesState,\n useEdgesState,\n Handle,\n Position,\n NodeProps,\n MarkerType,\n} from \"reactflow\";\nimport \"reactflow/dist/style.css\";\nimport { cn } from \"../../lib/utils\";\nimport type { Screen, ScreenConnection } from \"../../types/project\";\nimport { ExternalLink } from \"lucide-react\";\n\n// ═══════════════════════════════════════════════════════════\n// SCREEN NODE - Custom React Flow node\n// ═══════════════════════════════════════════════════════════\n\ninterface ScreenNodeData {\n screen: Screen;\n childScreens: Screen[];\n}\n\nfunction ScreenNode({ data, selected }: NodeProps<ScreenNodeData>) {\n const { screen, childScreens } = data;\n const hasChildren = childScreens.length > 0;\n\n const statusColors = {\n draft: { bg: \"var(--canvas-warning-surface)\", text: \"var(--canvas-warning)\", border: \"var(--canvas-warning)\" },\n review: { bg: \"var(--canvas-info-surface)\", text: \"var(--canvas-info)\", border: \"var(--canvas-info)\" },\n approved: { bg: \"var(--canvas-success-surface)\", text: \"var(--canvas-success)\", border: \"var(--canvas-success)\" },\n };\n\n const typeLabels = {\n page: null,\n tab: \"Tab\",\n modal: \"Modal\",\n drawer: \"Drawer\",\n state: \"State\",\n };\n\n return (\n <div\n className={cn(\n \"rounded-lg border-2 bg-[var(--canvas-background)] shadow-sm min-w-[160px]\",\n selected\n ? \"border-[var(--canvas-primary)] shadow-md\"\n : \"border-[var(--canvas-border)]\"\n )}\n >\n {/* Input handle */}\n <Handle\n type=\"target\"\n position={Position.Top}\n className=\"!w-3 !h-3 !bg-[var(--canvas-primary)] !border-2 !border-white\"\n />\n\n {/* Main content */}\n <div className=\"p-3\">\n <div className=\"flex items-center gap-2 mb-1\">\n <span style={{ fontSize: \"var(--typo-body-l-size)\" }}>{screen.icon || \"📄\"}</span>\n <span className=\"font-medium text-[var(--canvas-text)] truncate\" style={{ fontSize: \"var(--typo-body-s-size)\" }}>\n {screen.name}\n </span>\n </div>\n <div className=\"flex items-center gap-2\">\n <span className=\"text-[var(--canvas-text-muted)] font-mono\" style={{ fontSize: \"var(--typo-body-xs-size)\" }}>\n /{screen.slug}\n </span>\n {typeLabels[screen.type] && (\n <span className=\"text-[10px] px-1.5 py-0.5 rounded bg-[var(--canvas-surface)] text-[var(--canvas-text-muted)]\">\n {typeLabels[screen.type]}\n </span>\n )}\n </div>\n\n {/* Status badge */}\n <div className=\"flex items-center justify-between mt-2 pt-2 border-t border-[var(--canvas-border)]\">\n <span\n className=\"text-[10px] px-1.5 py-0.5 rounded border font-medium capitalize\"\n style={{\n backgroundColor: statusColors[screen.status].bg,\n color: statusColors[screen.status].text,\n borderColor: statusColors[screen.status].border,\n }}\n >\n {screen.status}\n </span>\n </div>\n </div>\n\n {/* Children list */}\n {hasChildren && (\n <div className=\"border-t border-[var(--canvas-border)]\">\n {childScreens.map((child, i) => (\n <div\n key={child.id}\n className={cn(\n \"px-3 py-2 flex items-center gap-2\",\n \"hover:bg-[var(--canvas-surface)]\",\n i < childScreens.length - 1 && \"border-b border-[var(--canvas-border)]\"\n )}\n >\n <span className=\"text-[var(--canvas-text-muted)]\">\n {child.type === \"modal\" && \"◻️\"}\n {child.type === \"tab\" && \"📑\"}\n {child.type === \"drawer\" && \"📥\"}\n {child.type === \"state\" && \"🔄\"}\n </span>\n <span className=\"text-[var(--canvas-text)] truncate\" style={{ fontSize: \"var(--typo-body-s-size)\" }}>\n {child.name}\n </span>\n <span className=\"text-[var(--canvas-text-muted)] font-mono ml-auto\" style={{ fontSize: \"var(--typo-body-xs-size)\" }}>\n /{child.slug}\n </span>\n </div>\n ))}\n </div>\n )}\n\n {/* Output handle */}\n <Handle\n type=\"source\"\n position={Position.Bottom}\n className=\"!w-3 !h-3 !bg-[var(--canvas-primary)] !border-2 !border-white\"\n />\n\n {/* Right handle for branching */}\n <Handle\n type=\"source\"\n position={Position.Right}\n id=\"right\"\n className=\"!w-3 !h-3 !bg-[var(--canvas-text-muted)] !border-2 !border-white\"\n />\n </div>\n );\n}\n\n// ═══════════════════════════════════════════════════════════\n// NODE TYPES\n// ═══════════════════════════════════════════════════════════\n\nconst nodeTypes = {\n screen: ScreenNode,\n};\n\n// ═══════════════════════════════════════════════════════════\n// SCREEN FLOWCHART\n// ═══════════════════════════════════════════════════════════\n\ninterface ScreenFlowchartProps {\n screens: Screen[];\n connections: ScreenConnection[];\n className?: string;\n onOpenFullscreen?: () => void;\n}\n\nexport function ScreenFlowchart({\n screens,\n connections,\n className,\n onOpenFullscreen,\n}: ScreenFlowchartProps) {\n // Transform screens to React Flow nodes\n const initialNodes = useMemo(() => {\n // Only show top-level screens (no parentId)\n const topLevelScreens = screens.filter((s) => !s.parentId);\n\n return topLevelScreens.map((screen) => ({\n id: screen.id,\n type: \"screen\",\n position: screen.position,\n data: {\n screen,\n childScreens: screens.filter((s) => s.parentId === screen.id),\n },\n }));\n }, [screens]);\n\n // Transform connections to React Flow edges\n const initialEdges = useMemo(() => {\n return connections.map((conn) => ({\n id: conn.id,\n source: conn.sourceId,\n target: conn.targetId,\n label: conn.label,\n type: \"smoothstep\",\n animated: conn.type === \"redirect\",\n style: { stroke: \"var(--canvas-primary)\", strokeWidth: 2 },\n labelStyle: { \n fill: \"var(--canvas-text-muted)\", \n fontSize: 11,\n fontWeight: 500,\n },\n labelBgStyle: { \n fill: \"var(--canvas-background)\", \n fillOpacity: 0.9,\n },\n labelBgPadding: [4, 2] as [number, number],\n labelBgBorderRadius: 4,\n markerEnd: {\n type: MarkerType.ArrowClosed,\n color: \"var(--canvas-primary)\",\n },\n }));\n }, [connections]);\n\n const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);\n const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);\n\n if (screens.length === 0) {\n return null;\n }\n\n return (\n <div className={cn(\"relative\", className)}>\n {/* Fullscreen button */}\n {onOpenFullscreen && (\n <button\n onClick={onOpenFullscreen}\n className=\"cursor-pointer absolute top-3 right-3 z-10 flex items-center gap-1.5 px-3 py-1.5 rounded-md font-medium bg-[var(--canvas-background)] text-[var(--canvas-text-muted)] border border-[var(--canvas-border)] hover:border-[var(--canvas-primary)] hover:text-[var(--canvas-primary)] transition-colors\"\n style={{ fontSize: \"var(--typo-body-xs-size)\" }}\n >\n <ExternalLink className=\"size-3\" />\n Open in new tab\n </button>\n )}\n\n <ReactFlow\n nodes={nodes}\n edges={edges}\n onNodesChange={onNodesChange}\n onEdgesChange={onEdgesChange}\n nodeTypes={nodeTypes}\n fitView\n fitViewOptions={{ padding: 0.2 }}\n minZoom={0.25}\n maxZoom={2}\n className=\"bg-[var(--canvas-surface)] rounded-lg border border-[var(--canvas-border)]\"\n >\n <Background variant={BackgroundVariant.Dots} gap={20} size={1} />\n <Controls className=\"!bg-[var(--canvas-background)] !border-[var(--canvas-border)] !shadow-sm\" />\n </ReactFlow>\n </div>\n );\n}\n"
10
10
  }
11
11
  ],
12
12
  "dependencies": [
@@ -6,7 +6,7 @@
6
6
  {
7
7
  "path": "components/blocks/screen-prompt-builder.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\ninterface ScreenPromptBuilderProps {\n className?: string;\n}\n\n// ═══════════════════════════════════════════════════════════\n// COMPONENT\n// ═══════════════════════════════════════════════════════════\n\nexport function ScreenPromptBuilder({ className }: ScreenPromptBuilderProps) {\n const [selectedComponents, setSelectedComponents] = useState<ComponentOption[]>([]);\n const [screenName, setScreenName] = useState(\"\");\n const [screenContext, setScreenContext] = useState(\"\");\n const [selectedPersona, setSelectedPersona] = useState(\"\");\n const [copied, setCopied] = useState(false);\n\n const { personas } = projectContext;\n\n // Generate the prompt\n const generatedPrompt = useMemo(() => {\n if (selectedComponents.length === 0 && !screenName && !screenContext) {\n return \"\";\n }\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 // Screen info\n const slugName = screenName\n ? screenName.toLowerCase().replace(/\\s+/g, \"-\")\n : \"[screen-name]\";\n parts.push(`Create a new screen at src/app/${slugName}/page.tsx:`);\n parts.push(\"\");\n\n if (screenName) {\n parts.push(`Screen: ${screenName}`);\n }\n\n if (screenContext) {\n parts.push(`Purpose: ${screenContext}`);\n }\n\n if (selectedPersona) {\n const persona = personas.find((p) => p.id === selectedPersona);\n if (persona) {\n parts.push(`Target Persona: ${persona.name} (${persona.role})`);\n parts.push(` - Goals: ${persona.goals.slice(0, 2).join(\", \")}`);\n parts.push(` - Pain Points: ${persona.painPoints.slice(0, 2).join(\", \")}`);\n }\n } else if (personas.length > 0) {\n parts.push(\"\");\n parts.push(\"Consider these user personas when designing:\");\n personas.forEach((p) => {\n parts.push(`- ${p.name} (${p.role}): ${p.goals[0] || \"See project-context.ts for details\"}`);\n });\n }\n\n // Selected components\n if (selectedComponents.length > 0) {\n parts.push(\"\");\n parts.push(\"Use these existing components:\");\n selectedComponents.forEach((c) => {\n parts.push(`- ${c.name} (${c.path}) - ${c.description.slice(0, 60)}${c.description.length > 60 ? \"...\" : \"\"}`);\n });\n }\n\n // Important instructions\n parts.push(\"\");\n parts.push(\"IMPORTANT:\");\n parts.push(\"1. Create a unique URL route for this screen\");\n parts.push(\"2. Use design variables for all styling:\");\n parts.push(\" - Colors: var(--canvas-primary), var(--canvas-background), var(--canvas-text), etc.\");\n parts.push(\" - Spacing: var(--spacing-sm), var(--spacing-md), var(--spacing-lg), etc.\");\n parts.push(\" - Typography: var(--typo-*) tokens\");\n parts.push(\" - Border radius: var(--radius-*) tokens\");\n parts.push(\"3. Follow the patterns in src/lib/component-registry.ts\");\n parts.push(\"4. Ensure the design aligns with the project scope and serves the target personas\");\n\n return parts.join(\"\\n\");\n }, [selectedComponents, screenName, screenContext, selectedPersona, 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 = selectedComponents.length > 0 || screenName || screenContext;\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 Build from Components\n </h3>\n <p className=\"text-[var(--canvas-text-muted)] mt-1\" style={{ fontSize: \"var(--typo-body-s-size)\" }}>\n Select existing components and describe your screen to generate a prompt\n </p>\n </div>\n\n {/* Component Selection */}\n <div className=\"space-y-2\">\n <label className=\"text-[var(--canvas-text)]\" style={{ fontSize: \"var(--typo-body-s-size)\", fontWeight: 500 }}>\n Select Components\n </label>\n <ComponentSearch\n selectedComponents={selectedComponents}\n onSelectionChange={setSelectedComponents}\n />\n </div>\n\n {/* Screen Name */}\n <div className=\"space-y-2\">\n <label\n htmlFor=\"screen-name\"\n className=\"text-[var(--canvas-text)]\" style={{ fontSize: \"var(--typo-body-s-size)\", fontWeight: 500 }}\n >\n Screen Name\n </label>\n <input\n id=\"screen-name\"\n type=\"text\"\n value={screenName}\n onChange={(e) => setScreenName(e.target.value)}\n placeholder=\"e.g., User Dashboard, Order History, Settings\"\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 {/* Screen Context */}\n <div className=\"space-y-2\">\n <label\n htmlFor=\"screen-context\"\n className=\"text-[var(--canvas-text)]\" style={{ fontSize: \"var(--typo-body-s-size)\", fontWeight: 500 }}\n >\n Screen Purpose & Context\n </label>\n <textarea\n id=\"screen-context\"\n value={screenContext}\n onChange={(e) => setScreenContext(e.target.value)}\n placeholder=\"Describe what this screen should do, what users will accomplish here, and any specific requirements...\"\n rows={3}\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 {/* Target Persona (optional) */}\n {personas.length > 0 && (\n <div className=\"space-y-2\">\n <label\n htmlFor=\"persona\"\n className=\"text-[var(--canvas-text)]\" style={{ fontSize: \"var(--typo-body-s-size)\", fontWeight: 500 }}\n >\n Target Persona (optional)\n </label>\n <select\n id=\"persona\"\n value={selectedPersona}\n onChange={(e) => setSelectedPersona(e.target.value)}\n className=\"w-full px-3 py-2 rounded-lg border border-[var(--canvas-border)] bg-[var(--canvas-background)] text-[var(--canvas-text)] focus:outline-none focus:ring-2 focus:ring-[var(--canvas-primary)] focus:border-transparent\"\n style={{ fontSize: \"var(--typo-body-s-size)\" }}\n >\n <option value=\"\">Select a persona...</option>\n {personas.map((persona) => (\n <option key={persona.id} value={persona.id}>\n {persona.avatar} {persona.name} - {persona.role}\n </option>\n ))}\n </select>\n </div>\n )}\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 Select components or enter screen details to generate a prompt\n </p>\n </div>\n )}\n </div>\n );\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\ninterface ScreenPromptBuilderProps {\n className?: string;\n}\n\n// ═══════════════════════════════════════════════════════════\n// COMPONENT\n// ═══════════════════════════════════════════════════════════\n\nexport function ScreenPromptBuilder({ className }: ScreenPromptBuilderProps) {\n const [selectedComponents, setSelectedComponents] = useState<ComponentOption[]>([]);\n const [screenName, setScreenName] = useState(\"\");\n const [screenContext, setScreenContext] = useState(\"\");\n const [selectedPersona, setSelectedPersona] = useState(\"\");\n const [copied, setCopied] = useState(false);\n\n const { personas } = projectContext;\n\n // Generate the prompt\n const generatedPrompt = useMemo(() => {\n if (selectedComponents.length === 0 && !screenName && !screenContext) {\n return \"\";\n }\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 // Screen info\n const slugName = screenName\n ? screenName.toLowerCase().replace(/\\s+/g, \"-\")\n : \"[screen-name]\";\n parts.push(`Create a new screen at src/app/${slugName}/page.tsx:`);\n parts.push(\"\");\n\n if (screenName) {\n parts.push(`Screen: ${screenName}`);\n }\n\n if (screenContext) {\n parts.push(`Purpose: ${screenContext}`);\n }\n\n if (selectedPersona) {\n const persona = personas.find((p) => p.id === selectedPersona);\n if (persona) {\n parts.push(`Target Persona: ${persona.name} (${persona.role})`);\n parts.push(` - Goals: ${persona.goals.slice(0, 2).join(\", \")}`);\n parts.push(` - Pain Points: ${persona.painPoints.slice(0, 2).join(\", \")}`);\n }\n } else if (personas.length > 0) {\n parts.push(\"\");\n parts.push(\"Consider these user personas when designing:\");\n personas.forEach((p) => {\n parts.push(`- ${p.name} (${p.role}): ${p.goals[0] || \"See project-context.ts for details\"}`);\n });\n }\n\n // Selected components\n if (selectedComponents.length > 0) {\n parts.push(\"\");\n parts.push(\"Use these existing components:\");\n selectedComponents.forEach((c) => {\n parts.push(`- ${c.name} (${c.path}) - ${c.description.slice(0, 60)}${c.description.length > 60 ? \"...\" : \"\"}`);\n });\n }\n\n // Important instructions\n parts.push(\"\");\n parts.push(\"IMPORTANT:\");\n parts.push(\"1. Create a unique URL route for this screen\");\n parts.push(\"2. Use design variables for all styling:\");\n parts.push(\" - Colors: var(--canvas-primary), var(--canvas-background), var(--canvas-text), etc.\");\n parts.push(\" - Spacing: var(--spacing-sm), var(--spacing-md), var(--spacing-lg), etc.\");\n parts.push(\" - Typography: var(--typo-*) tokens\");\n parts.push(\" - Border radius: var(--radius-*) tokens\");\n parts.push(\"3. Follow the patterns in src/lib/component-registry.ts\");\n parts.push(\"4. Ensure the design aligns with the project scope and serves the target personas\");\n\n return parts.join(\"\\n\");\n }, [selectedComponents, screenName, screenContext, selectedPersona, 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 = selectedComponents.length > 0 || screenName || screenContext;\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 Build from Components\n </h3>\n <p className=\"text-[var(--canvas-text-muted)] mt-1\" style={{ fontSize: \"var(--typo-body-s-size)\" }}>\n Select existing components and describe your screen to generate a prompt\n </p>\n </div>\n\n {/* Component Selection */}\n <div className=\"space-y-2\">\n <label className=\"text-[var(--canvas-text)]\" style={{ fontSize: \"var(--typo-body-s-size)\", fontWeight: 500 }}>\n Select Components\n </label>\n <ComponentSearch\n selectedComponents={selectedComponents}\n onSelectionChange={setSelectedComponents}\n />\n </div>\n\n {/* Screen Name */}\n <div className=\"space-y-2\">\n <label\n htmlFor=\"screen-name\"\n className=\"text-[var(--canvas-text)]\" style={{ fontSize: \"var(--typo-body-s-size)\", fontWeight: 500 }}\n >\n Screen Name\n </label>\n <input\n id=\"screen-name\"\n type=\"text\"\n value={screenName}\n onChange={(e) => setScreenName(e.target.value)}\n placeholder=\"e.g., User Dashboard, Order History, Settings\"\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 {/* Screen Context */}\n <div className=\"space-y-2\">\n <label\n htmlFor=\"screen-context\"\n className=\"text-[var(--canvas-text)]\" style={{ fontSize: \"var(--typo-body-s-size)\", fontWeight: 500 }}\n >\n Screen Purpose & Context\n </label>\n <textarea\n id=\"screen-context\"\n value={screenContext}\n onChange={(e) => setScreenContext(e.target.value)}\n placeholder=\"Describe what this screen should do, what users will accomplish here, and any specific requirements...\"\n rows={3}\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 {/* Target Persona (optional) */}\n {personas.length > 0 && (\n <div className=\"space-y-2\">\n <label\n htmlFor=\"persona\"\n className=\"text-[var(--canvas-text)]\" style={{ fontSize: \"var(--typo-body-s-size)\", fontWeight: 500 }}\n >\n Target Persona (optional)\n </label>\n <select\n id=\"persona\"\n value={selectedPersona}\n onChange={(e) => setSelectedPersona(e.target.value)}\n className=\"w-full px-3 py-2 rounded-lg border border-[var(--canvas-border)] bg-[var(--canvas-background)] text-[var(--canvas-text)] focus:outline-none focus:ring-2 focus:ring-[var(--canvas-primary)] focus:border-transparent\"\n style={{ fontSize: \"var(--typo-body-s-size)\" }}\n >\n <option value=\"\">Select a persona...</option>\n {personas.map((persona) => (\n <option key={persona.id} value={persona.id}>\n {persona.avatar} {persona.name} - {persona.role}\n </option>\n ))}\n </select>\n </div>\n )}\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 Select components or enter screen details to generate a prompt\n </p>\n </div>\n )}\n </div>\n );\n}\n"
10
10
  }
11
11
  ],
12
12
  "dependencies": [
@@ -6,7 +6,7 @@
6
6
  {
7
7
  "path": "components/blocks/screen-prompt-template.tsx",
8
8
  "type": "registry:block",
9
- "content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { Check, Copy, Sparkles, FileBox, GitBranch } from \"lucide-react\";\nimport { cn } from \"../../lib/utils\";\nimport { promptTemplates } from \"../../data/prompt-templates\";\n\ntype ScreenPromptType = \"single\" | \"flow\";\n\nconst promptTypeConfig = {\n single: {\n icon: FileBox,\n label: \"Single Screen\",\n description: \"Add one screen at a time\",\n prompt: promptTemplates.screens.single,\n },\n flow: {\n icon: GitBranch,\n label: \"Screen Flow\",\n description: \"Create a multi-screen user flow\",\n prompt: promptTemplates.screens.flow,\n },\n};\n\nexport function ScreenPromptTemplate() {\n const [promptType, setPromptType] = useState<ScreenPromptType>(\"single\");\n const [copied, setCopied] = useState(false);\n\n const currentConfig = promptTypeConfig[promptType];\n\n const handleCopy = async () => {\n await navigator.clipboard.writeText(currentConfig.prompt);\n setCopied(true);\n setTimeout(() => setCopied(false), 2000);\n };\n\n return (\n <div className=\"rounded-lg border border-dashed border-[var(--canvas-border)] bg-[var(--canvas-surface)] p-4\">\n {/* Type Toggle */}\n <div className=\"flex flex-wrap gap-2 mb-4\">\n {(Object.entries(promptTypeConfig) as [ScreenPromptType, (typeof promptTypeConfig)[ScreenPromptType]][]).map(\n ([type, config]) => {\n const Icon = config.icon;\n const isActive = promptType === type;\n \n return (\n <button\n key={type}\n onClick={() => setPromptType(type)}\n className={cn(\n \"flex items-center gap-2 px-4 py-2 rounded-lg font-medium transition-all\",\n isActive\n ? \"bg-[var(--canvas-primary)] text-[var(--canvas-primary-foreground)] shadow-sm\"\n : \"bg-[var(--canvas-background)] 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 <Icon className=\"size-4\" />\n {config.label}\n </button>\n );\n }\n )}\n </div>\n\n {/* Description */}\n <p className=\"text-[var(--canvas-text-muted)] mb-3\" style={{ fontSize: \"var(--typo-body-xs-size)\" }}>\n {currentConfig.description}\n </p>\n\n {/* Divider */}\n <div className=\"border-t border-[var(--canvas-border)] my-4\" />\n\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 Generate with Cursor\n </div>\n <button\n onClick={handleCopy}\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 {currentConfig.prompt}\n </pre>\n </div>\n );\n}\n"
9
+ "content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { Check, Copy, Sparkles, FileBox, GitBranch } from \"lucide-react\";\nimport { cn } from \"../../lib/utils\";\nimport { promptTemplates } from \"../../data/prompt-templates\";\n\ntype ScreenPromptType = \"single\" | \"flow\";\n\nconst promptTypeConfig = {\n single: {\n icon: FileBox,\n label: \"Single Screen\",\n description: \"Add one screen at a time\",\n prompt: promptTemplates.screens.single,\n },\n flow: {\n icon: GitBranch,\n label: \"Screen Flow\",\n description: \"Create a multi-screen user flow\",\n prompt: promptTemplates.screens.flow,\n },\n};\n\nexport function ScreenPromptTemplate() {\n const [promptType, setPromptType] = useState<ScreenPromptType>(\"single\");\n const [copied, setCopied] = useState(false);\n\n const currentConfig = promptTypeConfig[promptType];\n\n const handleCopy = async () => {\n await navigator.clipboard.writeText(currentConfig.prompt);\n setCopied(true);\n setTimeout(() => setCopied(false), 2000);\n };\n\n return (\n <div className=\"rounded-lg border border-dashed border-[var(--canvas-border)] bg-[var(--canvas-surface)] p-4\">\n {/* Type Toggle */}\n <div className=\"flex flex-wrap gap-2 mb-4\">\n {(Object.entries(promptTypeConfig) as [ScreenPromptType, (typeof promptTypeConfig)[ScreenPromptType]][]).map(\n ([type, config]) => {\n const Icon = config.icon;\n const isActive = promptType === type;\n \n return (\n <button\n key={type}\n onClick={() => setPromptType(type)}\n className={cn(\n \"cursor-pointer flex items-center gap-2 px-4 py-2 rounded-lg font-medium transition-all\",\n isActive\n ? \"bg-[var(--canvas-primary)] text-[var(--canvas-primary-foreground)] shadow-sm\"\n : \"bg-[var(--canvas-background)] 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 <Icon className=\"size-4\" />\n {config.label}\n </button>\n );\n }\n )}\n </div>\n\n {/* Description */}\n <p className=\"text-[var(--canvas-text-muted)] mb-3\" style={{ fontSize: \"var(--typo-body-xs-size)\" }}>\n {currentConfig.description}\n </p>\n\n {/* Divider */}\n <div className=\"border-t border-[var(--canvas-border)] my-4\" />\n\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 Generate with Cursor\n </div>\n <button\n onClick={handleCopy}\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 {currentConfig.prompt}\n </pre>\n </div>\n );\n}\n"
10
10
  }
11
11
  ],
12
12
  "dependencies": [
@@ -6,7 +6,7 @@
6
6
  {
7
7
  "path": "components/blocks/sidebar-cards.tsx",
8
8
  "type": "registry:block",
9
- "content": "\"use client\";\n\nimport { cn } from \"../../lib/utils\";\nimport { Mail, Phone, MessageCircle, User, LucideIcon } from \"lucide-react\";\n\n// Shared card styling\nconst cardClassName = cn(\n \"bg-[var(--canvas-background)] border border-[var(--canvas-border)]\",\n \"rounded-xl p-8\"\n);\n\n// ============================================================================\n// InfoCard\n// ============================================================================\n\nexport interface InfoCardProps {\n /** Card title (displayed as uppercase header) */\n title?: string;\n /** Card description text */\n description?: string;\n /** Additional class name */\n className?: string;\n}\n\n/**\n * Canvas Design System - Info Card\n * \n * A sidebar card displaying a title and descriptive text.\n * Used for \"About Us\" style content.\n */\nexport function InfoCard({\n title = \"ABOUT US\",\n description = \"Canvas is a no-code framework built on top of Bubble that makes creating beautiful responsive web applications easy and fast.\\n\\nCanvas is developed and maintained by AirDev in San Francisco, based on our experience with hundreds of client engagements.\",\n className,\n}: InfoCardProps) {\n return (\n <div className={cn(cardClassName, \"w-80\", className)}>\n <h3 \n className=\"text-[var(--canvas-text-placeholder)] font-semibold mb-4\"\n style={{\n fontFamily: \"var(--typo-body-m-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-m-size)\",\n }}\n >\n {title}\n </h3>\n <p \n className=\"text-[var(--canvas-text-muted)] whitespace-pre-line\"\n style={{\n fontFamily: \"var(--typo-body-m-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-m-size)\",\n lineHeight: \"24px\",\n }}\n >\n {description}\n </p>\n </div>\n );\n}\n\n// ============================================================================\n// LinksCard\n// ============================================================================\n\nexport interface LinkItem {\n id: string;\n label: string;\n icon: LucideIcon;\n href?: string;\n onClick?: () => void;\n}\n\nexport interface LinksCardProps {\n /** Card title (displayed as uppercase header) */\n title?: string;\n /** Array of link items */\n links?: LinkItem[];\n /** Additional class name */\n className?: string;\n}\n\n/** Default support links */\nexport const defaultSupportLinks: LinkItem[] = [\n { id: \"email\", label: \"Email us\", icon: Mail },\n { id: \"phone\", label: \"Call us\", icon: Phone },\n { id: \"text\", label: \"Text us\", icon: MessageCircle },\n { id: \"chat\", label: \"Chat now\", icon: User },\n];\n\n/**\n * Canvas Design System - Links Card\n * \n * A sidebar card displaying a title and a list of icon links.\n * Used for \"Support\" style contact links.\n */\nexport function LinksCard({\n title = \"SUPPORT\",\n links = defaultSupportLinks,\n className,\n}: LinksCardProps) {\n return (\n <div className={cn(cardClassName, \"w-80\", className)}>\n <h3 \n className=\"text-[var(--canvas-text-placeholder)] font-semibold mb-4\"\n style={{\n fontFamily: \"var(--typo-body-m-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-m-size)\",\n }}\n >\n {title}\n </h3>\n <div className=\"flex flex-col gap-4\">\n {links.map((link) => {\n const Icon = link.icon;\n const content = (\n <>\n <Icon className=\"size-5 text-[var(--canvas-primary)]\" />\n <span \n className=\"text-[var(--canvas-primary)] font-medium\"\n style={{\n fontFamily: \"var(--typo-body-m-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-m-size)\",\n }}\n >\n {link.label}\n </span>\n </>\n );\n\n if (link.href) {\n return (\n <a\n key={link.id}\n href={link.href}\n className=\"flex items-center gap-1.5 hover:opacity-80 transition-opacity\"\n >\n {content}\n </a>\n );\n }\n\n return (\n <button\n key={link.id}\n type=\"button\"\n onClick={link.onClick}\n className=\"flex items-center gap-1.5 hover:opacity-80 transition-opacity\"\n >\n {content}\n </button>\n );\n })}\n </div>\n </div>\n );\n}\n\n"
9
+ "content": "\"use client\";\n\nimport { cn } from \"../../lib/utils\";\nimport { Mail, Phone, MessageCircle, User, LucideIcon } from \"lucide-react\";\n\n// Shared card styling\nconst cardClassName = cn(\n \"bg-[var(--canvas-background)] border border-[var(--canvas-border)]\",\n \"rounded-xl p-8\"\n);\n\n// ============================================================================\n// InfoCard\n// ============================================================================\n\nexport interface InfoCardProps {\n /** Card title (displayed as uppercase header) */\n title?: string;\n /** Card description text */\n description?: string;\n /** Additional class name */\n className?: string;\n}\n\n/**\n * Canvas Design System - Info Card\n * \n * A sidebar card displaying a title and descriptive text.\n * Used for \"About Us\" style content.\n */\nexport function InfoCard({\n title = \"ABOUT US\",\n description = \"Canvas is a no-code framework built on top of Bubble that makes creating beautiful responsive web applications easy and fast.\\n\\nCanvas is developed and maintained by AirDev in San Francisco, based on our experience with hundreds of client engagements.\",\n className,\n}: InfoCardProps) {\n return (\n <div className={cn(cardClassName, \"w-80\", className)}>\n <h3 \n className=\"text-[var(--canvas-text-placeholder)] font-semibold mb-4\"\n style={{\n fontFamily: \"var(--typo-body-m-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-m-size)\",\n }}\n >\n {title}\n </h3>\n <p \n className=\"text-[var(--canvas-text-muted)] whitespace-pre-line\"\n style={{\n fontFamily: \"var(--typo-body-m-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-m-size)\",\n lineHeight: \"24px\",\n }}\n >\n {description}\n </p>\n </div>\n );\n}\n\n// ============================================================================\n// LinksCard\n// ============================================================================\n\nexport interface LinkItem {\n id: string;\n label: string;\n icon: LucideIcon;\n href?: string;\n onClick?: () => void;\n}\n\nexport interface LinksCardProps {\n /** Card title (displayed as uppercase header) */\n title?: string;\n /** Array of link items */\n links?: LinkItem[];\n /** Additional class name */\n className?: string;\n}\n\n/** Default support links */\nexport const defaultSupportLinks: LinkItem[] = [\n { id: \"email\", label: \"Email us\", icon: Mail },\n { id: \"phone\", label: \"Call us\", icon: Phone },\n { id: \"text\", label: \"Text us\", icon: MessageCircle },\n { id: \"chat\", label: \"Chat now\", icon: User },\n];\n\n/**\n * Canvas Design System - Links Card\n * \n * A sidebar card displaying a title and a list of icon links.\n * Used for \"Support\" style contact links.\n */\nexport function LinksCard({\n title = \"SUPPORT\",\n links = defaultSupportLinks,\n className,\n}: LinksCardProps) {\n return (\n <div className={cn(cardClassName, \"w-80\", className)}>\n <h3 \n className=\"text-[var(--canvas-text-placeholder)] font-semibold mb-4\"\n style={{\n fontFamily: \"var(--typo-body-m-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-m-size)\",\n }}\n >\n {title}\n </h3>\n <div className=\"flex flex-col gap-4\">\n {links.map((link) => {\n const Icon = link.icon;\n const content = (\n <>\n <Icon className=\"size-5 text-[var(--canvas-primary)]\" />\n <span \n className=\"text-[var(--canvas-primary)] font-medium\"\n style={{\n fontFamily: \"var(--typo-body-m-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-m-size)\",\n }}\n >\n {link.label}\n </span>\n </>\n );\n\n if (link.href) {\n return (\n <a\n key={link.id}\n href={link.href}\n className=\"cursor-pointer flex items-center gap-1.5 hover:opacity-80 transition-opacity\"\n >\n {content}\n </a>\n );\n }\n\n return (\n <button\n key={link.id}\n type=\"button\"\n onClick={link.onClick}\n className=\"cursor-pointer flex items-center gap-1.5 hover:opacity-80 transition-opacity\"\n >\n {content}\n </button>\n );\n })}\n </div>\n </div>\n );\n}\n\n"
10
10
  }
11
11
  ],
12
12
  "dependencies": [
@@ -6,7 +6,7 @@
6
6
  {
7
7
  "path": "components/blocks/slideshow-grid-tiles.tsx",
8
8
  "type": "registry:block",
9
- "content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport { Button } from \"../ui/button\";\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from \"../ui/select\";\nimport { Avatar, AvatarImage, AvatarFallback } from \"../ui/avatar\";\nimport { ChevronLeft, ChevronRight, FolderPlus, ThumbsUp, Eye } from \"lucide-react\";\n\n// ============================================\n// Types\n// ============================================\n\nexport interface SlideshowTileItem {\n id: string;\n /** Array of image URLs for the slideshow */\n images: string[];\n /** User/creator info */\n user: {\n name: string;\n avatarUrl?: string;\n location: string;\n };\n /** Like/upvote count */\n likes: number | string;\n /** View count */\n views: number | string;\n}\n\nexport interface SortOption {\n id: string;\n label: string;\n}\n\nexport interface FilterOption {\n id: string;\n label: string;\n}\n\nexport interface SlideshowGridTilesProps {\n /** Block title */\n title?: string;\n /** Subtitle text (e.g., \"Showing 20 designs\") */\n subtitle?: string;\n /** Array of tile items */\n items?: SlideshowTileItem[];\n /** Sort options for the sort dropdown */\n sortOptions?: SortOption[];\n /** Filter options for the filter dropdown */\n filterOptions?: FilterOption[];\n /** Primary action button text */\n actionButtonText?: string;\n /** Callback when action button is clicked */\n onAddNew?: () => void;\n /** Callback when sort value changes */\n onSort?: (value: string) => void;\n /** Callback when filter value changes */\n onFilter?: (value: string) => void;\n /** Callback when save button is clicked on a tile */\n onSave?: (item: SlideshowTileItem) => void;\n /** Callback when a tile is clicked */\n onItemClick?: (item: SlideshowTileItem) => void;\n /** Additional class names */\n className?: string;\n}\n\n// ============================================\n// Default Data\n// ============================================\n\nconst defaultItems: SlideshowTileItem[] = [\n {\n id: \"1\",\n images: [\n \"https://images.unsplash.com/photo-1618005182384-a83a8bd57fbe?w=800&h=600&fit=crop\",\n \"https://images.unsplash.com/photo-1558591710-4b4a1ae0f04d?w=800&h=600&fit=crop\",\n ],\n user: {\n name: \"Aya Williams\",\n avatarUrl: \"https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=150&h=150&fit=crop&crop=face\",\n location: \"Copenhagen, Denmark\",\n },\n likes: \"25k\",\n views: \"6.5k\",\n },\n {\n id: \"2\",\n images: [\n \"https://images.unsplash.com/photo-1633356122544-f134324a6cee?w=800&h=600&fit=crop\",\n \"https://images.unsplash.com/photo-1611162617474-5b21e879e113?w=800&h=600&fit=crop\",\n ],\n user: {\n name: \"Jeffrey Connor\",\n avatarUrl: \"https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=150&h=150&fit=crop&crop=face\",\n location: \"San Francisco, CA\",\n },\n likes: \"11k\",\n views: \"2.5k\",\n },\n {\n id: \"3\",\n images: [\n \"https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=800&h=600&fit=crop\",\n \"https://images.unsplash.com/photo-1503023345310-bd7c1de61c7d?w=800&h=600&fit=crop\",\n ],\n user: {\n name: \"Gabi Del Rosario\",\n avatarUrl: \"https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=150&h=150&fit=crop&crop=face\",\n location: \"Honolulu, HI\",\n },\n likes: \"8k\",\n views: \"520\",\n },\n {\n id: \"4\",\n images: [\n \"https://images.unsplash.com/photo-1561998338-13ad7883b20f?w=800&h=600&fit=crop\",\n \"https://images.unsplash.com/photo-1579783902614-a3fb3927b6a5?w=800&h=600&fit=crop\",\n ],\n user: {\n name: \"Stacy Jones\",\n avatarUrl: \"https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=150&h=150&fit=crop&crop=face\",\n location: \"New York, NY\",\n },\n likes: \"9.5k\",\n views: \"12k\",\n },\n];\n\nconst defaultSortOptions: SortOption[] = [\n { id: \"popular\", label: \"Most Popular\" },\n { id: \"recent\", label: \"Most Recent\" },\n { id: \"likes\", label: \"Most Liked\" },\n { id: \"views\", label: \"Most Viewed\" },\n];\n\nconst defaultFilterOptions: FilterOption[] = [\n { id: \"all\", label: \"All Categories\" },\n { id: \"illustration\", label: \"Illustration\" },\n { id: \"photography\", label: \"Photography\" },\n { id: \"ui-design\", label: \"UI Design\" },\n { id: \"3d\", label: \"3D Art\" },\n];\n\n// ============================================\n// Sub-components\n// ============================================\n\ninterface TileCardProps {\n item: SlideshowTileItem;\n onSave?: (item: SlideshowTileItem) => void;\n onClick?: (item: SlideshowTileItem) => void;\n}\n\nfunction TileCard({ item, onSave, onClick }: TileCardProps) {\n const [currentImageIndex, setCurrentImageIndex] = useState(0);\n\n const handlePrevImage = (e: React.MouseEvent) => {\n e.stopPropagation();\n setCurrentImageIndex((prev) => \n prev === 0 ? item.images.length - 1 : prev - 1\n );\n };\n\n const handleNextImage = (e: React.MouseEvent) => {\n e.stopPropagation();\n setCurrentImageIndex((prev) => \n prev === item.images.length - 1 ? 0 : prev + 1\n );\n };\n\n const handleSave = (e: React.MouseEvent) => {\n e.stopPropagation();\n onSave?.(item);\n };\n\n return (\n <div \n className=\"flex flex-col cursor-pointer\"\n style={{ gap: \"var(--spacing-xl)\" }}\n onClick={() => onClick?.(item)}\n >\n {/* Image Container */}\n <div \n className=\"group relative w-full overflow-hidden\"\n style={{ \n height: \"240px\",\n borderRadius: \"var(--radius-md, 8px)\",\n border: \"1px solid var(--canvas-border)\",\n }}\n >\n {/* Main Image */}\n <img\n src={item.images[currentImageIndex]}\n alt={`${item.user.name}'s portfolio`}\n className=\"absolute inset-0 w-full h-full object-cover transition-opacity duration-300\"\n style={{ borderRadius: \"var(--radius-md, 8px)\" }}\n />\n\n {/* Hover Overlay - Navigation Arrows */}\n <div className=\"absolute inset-0 flex items-center justify-between px-2 opacity-0 group-hover:opacity-100 transition-opacity duration-200\">\n {/* Left Arrow */}\n <button\n onClick={handlePrevImage}\n className=\"flex items-center justify-center shrink-0 transition-transform hover:scale-105\"\n style={{\n width: \"32px\",\n height: \"32px\",\n backgroundColor: \"var(--canvas-background)\",\n border: \"1px solid var(--canvas-border)\",\n borderRadius: \"var(--radius-full, 24px)\",\n boxShadow: \"0px 1px 2px 0px rgba(0,0,0,0.02)\",\n }}\n >\n <ChevronLeft \n className=\"w-5 h-5\" \n style={{ color: \"var(--canvas-text-muted)\" }}\n />\n </button>\n\n {/* Right Arrow */}\n <button\n onClick={handleNextImage}\n className=\"flex items-center justify-center shrink-0 transition-transform hover:scale-105\"\n style={{\n width: \"32px\",\n height: \"32px\",\n backgroundColor: \"var(--canvas-background)\",\n border: \"1px solid var(--canvas-border)\",\n borderRadius: \"var(--radius-full, 24px)\",\n boxShadow: \"0px 1px 2px 0px rgba(0,0,0,0.02)\",\n }}\n >\n <ChevronRight \n className=\"w-5 h-5\" \n style={{ color: \"var(--canvas-text-muted)\" }}\n />\n </button>\n </div>\n\n {/* Hover Overlay - Save Badge */}\n <div className=\"absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity duration-200\">\n <button\n onClick={handleSave}\n className=\"flex items-center transition-transform hover:scale-105\"\n style={{\n height: \"var(--spacing-5xl, 40px)\",\n paddingLeft: \"var(--spacing-xl, 16px)\",\n paddingRight: \"var(--spacing-xl, 16px)\",\n gap: \"var(--spacing-md, 8px)\",\n backgroundColor: \"var(--canvas-background)\",\n border: \"1px solid var(--canvas-border)\",\n borderRadius: \"var(--radius-full, 40px)\",\n }}\n >\n <FolderPlus \n className=\"w-5 h-5\" \n style={{ color: \"var(--canvas-text)\" }}\n />\n <span\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 500,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n Save\n </span>\n </button>\n </div>\n </div>\n\n {/* User Info Row */}\n <div \n className=\"flex items-center w-full\"\n style={{ gap: \"var(--spacing-xl)\" }}\n >\n {/* Avatar */}\n <Avatar \n className=\"shrink-0\"\n style={{ \n width: \"48px\", \n height: \"48px\",\n border: \"1px solid var(--canvas-border)\",\n }}\n >\n <AvatarImage src={item.user.avatarUrl} alt={item.user.name} />\n <AvatarFallback>\n {item.user.name.split(\" \").map(n => n[0]).join(\"\").slice(0, 2)}\n </AvatarFallback>\n </Avatar>\n\n {/* Name and Location */}\n <div className=\"flex flex-col flex-1 min-w-0 justify-center\">\n <p\n className=\"truncate\"\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 600,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text)\",\n margin: 0,\n }}\n >\n {item.user.name}\n </p>\n <p\n className=\"truncate\"\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: \"var(--typo-body-s-weight)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text-muted)\",\n margin: 0,\n }}\n >\n {item.user.location}\n </p>\n </div>\n\n {/* Stats */}\n <div \n className=\"flex items-center shrink-0\"\n style={{ gap: \"var(--spacing-md)\" }}\n >\n {/* Likes */}\n <div \n className=\"flex items-center\"\n style={{ gap: \"var(--spacing-sm)\" }}\n >\n <ThumbsUp \n className=\"w-5 h-5\" \n style={{ color: \"var(--canvas-text-muted)\" }}\n />\n <span\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: \"var(--typo-body-s-weight)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text-muted)\",\n }}\n >\n {item.likes}\n </span>\n </div>\n\n {/* Views */}\n <div \n className=\"flex items-center\"\n style={{ gap: \"var(--spacing-sm)\" }}\n >\n <Eye \n className=\"w-5 h-5\" \n style={{ color: \"var(--canvas-text-muted)\" }}\n />\n <span\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: \"var(--typo-body-s-weight)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text-muted)\",\n }}\n >\n {item.views}\n </span>\n </div>\n </div>\n </div>\n </div>\n );\n}\n\n// ============================================\n// Main Component\n// ============================================\n\n/**\n * Canvas Design System - Slideshow Grid Tiles Block\n * \n * A 2-column grid of portfolio/slideshow tiles with hover states\n * showing navigation arrows and save button. Each tile displays\n * a large image, user info, and engagement stats.\n * \n * @example\n * ```tsx\n * <SlideshowGridTiles\n * title=\"Portfolios\"\n * subtitle=\"Showing 20 designs\"\n * onSave={(item) => console.log(\"Saved\", item)}\n * />\n * ```\n */\nexport function SlideshowGridTiles({\n title = \"Portfolios\",\n subtitle,\n items = defaultItems,\n sortOptions = defaultSortOptions,\n filterOptions = defaultFilterOptions,\n actionButtonText = \"Add new\",\n onAddNew,\n onSort,\n onFilter,\n onSave,\n onItemClick,\n className,\n}: SlideshowGridTilesProps) {\n const [sortValue, setSortValue] = useState<string>(\"\");\n const [filterValue, setFilterValue] = useState<string>(\"\");\n\n const displaySubtitle = subtitle ?? `Showing ${items.length} designs`;\n\n const handleSortChange = (value: string) => {\n setSortValue(value);\n onSort?.(value);\n };\n\n const handleFilterChange = (value: string) => {\n setFilterValue(value);\n onFilter?.(value);\n };\n\n return (\n <div \n className={cn(\"flex flex-col w-full\", className)}\n style={{ gap: \"var(--spacing-3xl)\" }}\n >\n {/* Header Section */}\n <div \n className=\"flex flex-wrap items-start w-full\"\n style={{ gap: \"var(--spacing-xl)\" }}\n >\n {/* Title and Subtitle */}\n <div \n className=\"flex flex-col flex-1 min-w-[200px]\"\n style={{ gap: \"var(--spacing-xs)\" }}\n >\n <h2\n style={{\n fontFamily: \"var(--typo-h6-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-h6-size)\",\n fontWeight: \"var(--typo-h6-weight)\",\n letterSpacing: \"var(--typo-h6-spacing)\",\n lineHeight: \"var(--typo-h6-line-height)\",\n color: \"var(--canvas-text)\",\n margin: 0,\n }}\n >\n {title}\n </h2>\n <p\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: \"var(--typo-body-s-weight)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text-muted)\",\n margin: 0,\n }}\n >\n {displaySubtitle}\n </p>\n </div>\n\n {/* Controls */}\n <div \n className=\"flex items-start justify-end shrink-0\"\n style={{ gap: \"var(--spacing-3xl)\" }}\n >\n {/* Sort Dropdown */}\n <div className=\"w-[120px]\">\n <Select value={sortValue || undefined} onValueChange={handleSortChange}>\n <SelectTrigger inputSize=\"sm\">\n <SelectValue placeholder=\"Sort\" />\n </SelectTrigger>\n <SelectContent>\n {sortOptions.map((option) => (\n <SelectItem key={option.id} value={option.id}>\n {option.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n\n {/* Filter Dropdown */}\n <div className=\"w-[120px]\">\n <Select value={filterValue || undefined} onValueChange={handleFilterChange}>\n <SelectTrigger inputSize=\"sm\">\n <SelectValue placeholder=\"Filter\" />\n </SelectTrigger>\n <SelectContent>\n {filterOptions.map((option) => (\n <SelectItem key={option.id} value={option.id}>\n {option.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n\n {/* Action Button */}\n <Button\n variant=\"primary\"\n size=\"sm\"\n onClick={onAddNew}\n style={{\n height: \"var(--btn-small-height)\",\n paddingLeft: \"var(--btn-small-px)\",\n paddingRight: \"var(--btn-small-px)\",\n fontSize: \"var(--btn-small-font-size)\",\n borderRadius: \"var(--btn-small-radius)\",\n backgroundColor: \"var(--btn-primary-bg)\",\n color: \"var(--btn-primary-text)\",\n borderColor: \"var(--btn-primary-border)\",\n }}\n >\n {actionButtonText}\n </Button>\n </div>\n </div>\n\n {/* Grid Section */}\n <div \n className=\"grid grid-cols-1 md:grid-cols-2 w-full\"\n style={{ gap: \"var(--spacing-4xl)\" }}\n >\n {items.map((item) => (\n <TileCard\n key={item.id}\n item={item}\n onSave={onSave}\n onClick={onItemClick}\n />\n ))}\n </div>\n </div>\n );\n}\n"
9
+ "content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport { Button } from \"../ui/button\";\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from \"../ui/select\";\nimport { Avatar, AvatarImage, AvatarFallback } from \"../ui/avatar\";\nimport { ChevronLeft, ChevronRight, FolderPlus, ThumbsUp, Eye } from \"lucide-react\";\n\n// ============================================\n// Types\n// ============================================\n\nexport interface SlideshowTileItem {\n id: string;\n /** Array of image URLs for the slideshow */\n images: string[];\n /** User/creator info */\n user: {\n name: string;\n avatarUrl?: string;\n location: string;\n };\n /** Like/upvote count */\n likes: number | string;\n /** View count */\n views: number | string;\n}\n\nexport interface SortOption {\n id: string;\n label: string;\n}\n\nexport interface FilterOption {\n id: string;\n label: string;\n}\n\nexport interface SlideshowGridTilesProps {\n /** Block title */\n title?: string;\n /** Subtitle text (e.g., \"Showing 20 designs\") */\n subtitle?: string;\n /** Array of tile items */\n items?: SlideshowTileItem[];\n /** Sort options for the sort dropdown */\n sortOptions?: SortOption[];\n /** Filter options for the filter dropdown */\n filterOptions?: FilterOption[];\n /** Primary action button text */\n actionButtonText?: string;\n /** Callback when action button is clicked */\n onAddNew?: () => void;\n /** Callback when sort value changes */\n onSort?: (value: string) => void;\n /** Callback when filter value changes */\n onFilter?: (value: string) => void;\n /** Callback when save button is clicked on a tile */\n onSave?: (item: SlideshowTileItem) => void;\n /** Callback when a tile is clicked */\n onItemClick?: (item: SlideshowTileItem) => void;\n /** Additional class names */\n className?: string;\n}\n\n// ============================================\n// Default Data\n// ============================================\n\nconst defaultItems: SlideshowTileItem[] = [\n {\n id: \"1\",\n images: [\n \"https://images.unsplash.com/photo-1618005182384-a83a8bd57fbe?w=800&h=600&fit=crop\",\n \"https://images.unsplash.com/photo-1558591710-4b4a1ae0f04d?w=800&h=600&fit=crop\",\n ],\n user: {\n name: \"Aya Williams\",\n avatarUrl: \"https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=150&h=150&fit=crop&crop=face\",\n location: \"Copenhagen, Denmark\",\n },\n likes: \"25k\",\n views: \"6.5k\",\n },\n {\n id: \"2\",\n images: [\n \"https://images.unsplash.com/photo-1633356122544-f134324a6cee?w=800&h=600&fit=crop\",\n \"https://images.unsplash.com/photo-1611162617474-5b21e879e113?w=800&h=600&fit=crop\",\n ],\n user: {\n name: \"Jeffrey Connor\",\n avatarUrl: \"https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=150&h=150&fit=crop&crop=face\",\n location: \"San Francisco, CA\",\n },\n likes: \"11k\",\n views: \"2.5k\",\n },\n {\n id: \"3\",\n images: [\n \"https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=800&h=600&fit=crop\",\n \"https://images.unsplash.com/photo-1503023345310-bd7c1de61c7d?w=800&h=600&fit=crop\",\n ],\n user: {\n name: \"Gabi Del Rosario\",\n avatarUrl: \"https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=150&h=150&fit=crop&crop=face\",\n location: \"Honolulu, HI\",\n },\n likes: \"8k\",\n views: \"520\",\n },\n {\n id: \"4\",\n images: [\n \"https://images.unsplash.com/photo-1561998338-13ad7883b20f?w=800&h=600&fit=crop\",\n \"https://images.unsplash.com/photo-1579783902614-a3fb3927b6a5?w=800&h=600&fit=crop\",\n ],\n user: {\n name: \"Stacy Jones\",\n avatarUrl: \"https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=150&h=150&fit=crop&crop=face\",\n location: \"New York, NY\",\n },\n likes: \"9.5k\",\n views: \"12k\",\n },\n];\n\nconst defaultSortOptions: SortOption[] = [\n { id: \"popular\", label: \"Most Popular\" },\n { id: \"recent\", label: \"Most Recent\" },\n { id: \"likes\", label: \"Most Liked\" },\n { id: \"views\", label: \"Most Viewed\" },\n];\n\nconst defaultFilterOptions: FilterOption[] = [\n { id: \"all\", label: \"All Categories\" },\n { id: \"illustration\", label: \"Illustration\" },\n { id: \"photography\", label: \"Photography\" },\n { id: \"ui-design\", label: \"UI Design\" },\n { id: \"3d\", label: \"3D Art\" },\n];\n\n// ============================================\n// Sub-components\n// ============================================\n\ninterface TileCardProps {\n item: SlideshowTileItem;\n onSave?: (item: SlideshowTileItem) => void;\n onClick?: (item: SlideshowTileItem) => void;\n}\n\nfunction TileCard({ item, onSave, onClick }: TileCardProps) {\n const [currentImageIndex, setCurrentImageIndex] = useState(0);\n\n const handlePrevImage = (e: React.MouseEvent) => {\n e.stopPropagation();\n setCurrentImageIndex((prev) => \n prev === 0 ? item.images.length - 1 : prev - 1\n );\n };\n\n const handleNextImage = (e: React.MouseEvent) => {\n e.stopPropagation();\n setCurrentImageIndex((prev) => \n prev === item.images.length - 1 ? 0 : prev + 1\n );\n };\n\n const handleSave = (e: React.MouseEvent) => {\n e.stopPropagation();\n onSave?.(item);\n };\n\n return (\n <div \n className=\"flex flex-col cursor-pointer\"\n style={{ gap: \"var(--spacing-xl)\" }}\n onClick={() => onClick?.(item)}\n >\n {/* Image Container */}\n <div \n className=\"group relative w-full overflow-hidden\"\n style={{ \n height: \"240px\",\n borderRadius: \"var(--radius-md, 8px)\",\n border: \"1px solid var(--canvas-border)\",\n }}\n >\n {/* Main Image */}\n <img\n src={item.images[currentImageIndex]}\n alt={`${item.user.name}'s portfolio`}\n className=\"absolute inset-0 w-full h-full object-cover transition-opacity duration-300\"\n style={{ borderRadius: \"var(--radius-md, 8px)\" }}\n />\n\n {/* Hover Overlay - Navigation Arrows */}\n <div className=\"absolute inset-0 flex items-center justify-between px-2 opacity-0 group-hover:opacity-100 transition-opacity duration-200\">\n {/* Left Arrow */}\n <button\n onClick={handlePrevImage}\n className=\"cursor-pointer flex items-center justify-center shrink-0 transition-transform hover:scale-105\"\n style={{\n width: \"32px\",\n height: \"32px\",\n backgroundColor: \"var(--canvas-background)\",\n border: \"1px solid var(--canvas-border)\",\n borderRadius: \"var(--radius-full, 24px)\",\n boxShadow: \"0px 1px 2px 0px rgba(0,0,0,0.02)\",\n }}\n >\n <ChevronLeft \n className=\"w-5 h-5\" \n style={{ color: \"var(--canvas-text-muted)\" }}\n />\n </button>\n\n {/* Right Arrow */}\n <button\n onClick={handleNextImage}\n className=\"cursor-pointer flex items-center justify-center shrink-0 transition-transform hover:scale-105\"\n style={{\n width: \"32px\",\n height: \"32px\",\n backgroundColor: \"var(--canvas-background)\",\n border: \"1px solid var(--canvas-border)\",\n borderRadius: \"var(--radius-full, 24px)\",\n boxShadow: \"0px 1px 2px 0px rgba(0,0,0,0.02)\",\n }}\n >\n <ChevronRight \n className=\"w-5 h-5\" \n style={{ color: \"var(--canvas-text-muted)\" }}\n />\n </button>\n </div>\n\n {/* Hover Overlay - Save Badge */}\n <div className=\"absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity duration-200\">\n <button\n onClick={handleSave}\n className=\"cursor-pointer flex items-center transition-transform hover:scale-105\"\n style={{\n height: \"var(--spacing-5xl, 40px)\",\n paddingLeft: \"var(--spacing-xl, 16px)\",\n paddingRight: \"var(--spacing-xl, 16px)\",\n gap: \"var(--spacing-md, 8px)\",\n backgroundColor: \"var(--canvas-background)\",\n border: \"1px solid var(--canvas-border)\",\n borderRadius: \"var(--radius-full, 40px)\",\n }}\n >\n <FolderPlus \n className=\"w-5 h-5\" \n style={{ color: \"var(--canvas-text)\" }}\n />\n <span\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 500,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n Save\n </span>\n </button>\n </div>\n </div>\n\n {/* User Info Row */}\n <div \n className=\"flex items-center w-full\"\n style={{ gap: \"var(--spacing-xl)\" }}\n >\n {/* Avatar */}\n <Avatar \n className=\"shrink-0\"\n style={{ \n width: \"48px\", \n height: \"48px\",\n border: \"1px solid var(--canvas-border)\",\n }}\n >\n <AvatarImage src={item.user.avatarUrl} alt={item.user.name} />\n <AvatarFallback>\n {item.user.name.split(\" \").map(n => n[0]).join(\"\").slice(0, 2)}\n </AvatarFallback>\n </Avatar>\n\n {/* Name and Location */}\n <div className=\"flex flex-col flex-1 min-w-0 justify-center\">\n <p\n className=\"truncate\"\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 600,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text)\",\n margin: 0,\n }}\n >\n {item.user.name}\n </p>\n <p\n className=\"truncate\"\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: \"var(--typo-body-s-weight)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text-muted)\",\n margin: 0,\n }}\n >\n {item.user.location}\n </p>\n </div>\n\n {/* Stats */}\n <div \n className=\"flex items-center shrink-0\"\n style={{ gap: \"var(--spacing-md)\" }}\n >\n {/* Likes */}\n <div \n className=\"flex items-center\"\n style={{ gap: \"var(--spacing-sm)\" }}\n >\n <ThumbsUp \n className=\"w-5 h-5\" \n style={{ color: \"var(--canvas-text-muted)\" }}\n />\n <span\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: \"var(--typo-body-s-weight)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text-muted)\",\n }}\n >\n {item.likes}\n </span>\n </div>\n\n {/* Views */}\n <div \n className=\"flex items-center\"\n style={{ gap: \"var(--spacing-sm)\" }}\n >\n <Eye \n className=\"w-5 h-5\" \n style={{ color: \"var(--canvas-text-muted)\" }}\n />\n <span\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: \"var(--typo-body-s-weight)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text-muted)\",\n }}\n >\n {item.views}\n </span>\n </div>\n </div>\n </div>\n </div>\n );\n}\n\n// ============================================\n// Main Component\n// ============================================\n\n/**\n * Canvas Design System - Slideshow Grid Tiles Block\n * \n * A 2-column grid of portfolio/slideshow tiles with hover states\n * showing navigation arrows and save button. Each tile displays\n * a large image, user info, and engagement stats.\n * \n * @example\n * ```tsx\n * <SlideshowGridTiles\n * title=\"Portfolios\"\n * subtitle=\"Showing 20 designs\"\n * onSave={(item) => console.log(\"Saved\", item)}\n * />\n * ```\n */\nexport function SlideshowGridTiles({\n title = \"Portfolios\",\n subtitle,\n items = defaultItems,\n sortOptions = defaultSortOptions,\n filterOptions = defaultFilterOptions,\n actionButtonText = \"Add new\",\n onAddNew,\n onSort,\n onFilter,\n onSave,\n onItemClick,\n className,\n}: SlideshowGridTilesProps) {\n const [sortValue, setSortValue] = useState<string>(\"\");\n const [filterValue, setFilterValue] = useState<string>(\"\");\n\n const displaySubtitle = subtitle ?? `Showing ${items.length} designs`;\n\n const handleSortChange = (value: string) => {\n setSortValue(value);\n onSort?.(value);\n };\n\n const handleFilterChange = (value: string) => {\n setFilterValue(value);\n onFilter?.(value);\n };\n\n return (\n <div \n className={cn(\"flex flex-col w-full\", className)}\n style={{ gap: \"var(--spacing-3xl)\" }}\n >\n {/* Header Section */}\n <div \n className=\"flex flex-wrap items-start w-full\"\n style={{ gap: \"var(--spacing-xl)\" }}\n >\n {/* Title and Subtitle */}\n <div \n className=\"flex flex-col flex-1 min-w-[200px]\"\n style={{ gap: \"var(--spacing-xs)\" }}\n >\n <h2\n style={{\n fontFamily: \"var(--typo-h6-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-h6-size)\",\n fontWeight: \"var(--typo-h6-weight)\",\n letterSpacing: \"var(--typo-h6-spacing)\",\n lineHeight: \"var(--typo-h6-line-height)\",\n color: \"var(--canvas-text)\",\n margin: 0,\n }}\n >\n {title}\n </h2>\n <p\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: \"var(--typo-body-s-weight)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text-muted)\",\n margin: 0,\n }}\n >\n {displaySubtitle}\n </p>\n </div>\n\n {/* Controls */}\n <div \n className=\"flex items-start justify-end shrink-0\"\n style={{ gap: \"var(--spacing-3xl)\" }}\n >\n {/* Sort Dropdown */}\n <div className=\"w-[120px]\">\n <Select value={sortValue || undefined} onValueChange={handleSortChange}>\n <SelectTrigger inputSize=\"sm\">\n <SelectValue placeholder=\"Sort\" />\n </SelectTrigger>\n <SelectContent>\n {sortOptions.map((option) => (\n <SelectItem key={option.id} value={option.id}>\n {option.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n\n {/* Filter Dropdown */}\n <div className=\"w-[120px]\">\n <Select value={filterValue || undefined} onValueChange={handleFilterChange}>\n <SelectTrigger inputSize=\"sm\">\n <SelectValue placeholder=\"Filter\" />\n </SelectTrigger>\n <SelectContent>\n {filterOptions.map((option) => (\n <SelectItem key={option.id} value={option.id}>\n {option.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n\n {/* Action Button */}\n <Button\n variant=\"primary\"\n size=\"sm\"\n onClick={onAddNew}\n style={{\n height: \"var(--btn-small-height)\",\n paddingLeft: \"var(--btn-small-px)\",\n paddingRight: \"var(--btn-small-px)\",\n fontSize: \"var(--btn-small-font-size)\",\n borderRadius: \"var(--btn-small-radius)\",\n backgroundColor: \"var(--btn-primary-bg)\",\n color: \"var(--btn-primary-text)\",\n borderColor: \"var(--btn-primary-border)\",\n }}\n >\n {actionButtonText}\n </Button>\n </div>\n </div>\n\n {/* Grid Section */}\n <div \n className=\"grid grid-cols-1 md:grid-cols-2 w-full\"\n style={{ gap: \"var(--spacing-4xl)\" }}\n >\n {items.map((item) => (\n <TileCard\n key={item.id}\n item={item}\n onSave={onSave}\n onClick={onItemClick}\n />\n ))}\n </div>\n </div>\n );\n}\n"
10
10
  }
11
11
  ],
12
12
  "dependencies": [