canvas-ui-sdk 0.1.6 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (153) hide show
  1. package/dist/cli/index.js +516 -0
  2. package/dist/index.d.ts +67 -3
  3. package/dist/index.js +2588 -301
  4. package/dist/index.js.map +1 -1
  5. package/mcp/dist/index.js +5 -1
  6. package/package.json +18 -2
  7. package/registry/blocks/activity-feed.json +19 -0
  8. package/registry/blocks/blog-cards.json +16 -0
  9. package/registry/blocks/bottom-input-chat-widget.json +19 -0
  10. package/registry/blocks/canvas-item.json +18 -0
  11. package/registry/blocks/category-grid.json +16 -0
  12. package/registry/blocks/centered-hero.json +14 -0
  13. package/registry/blocks/chat-message.json +18 -0
  14. package/registry/blocks/circular-progress-bar-list.json +18 -0
  15. package/registry/blocks/component-palette.json +21 -0
  16. package/registry/blocks/component-search.json +19 -0
  17. package/registry/blocks/content-dropzone.json +16 -0
  18. package/registry/blocks/content-with-image.json +14 -0
  19. package/registry/blocks/core-values-grid.json +16 -0
  20. package/registry/blocks/credit-card-display.json +16 -0
  21. package/registry/blocks/cta-banner.json +14 -0
  22. package/registry/blocks/custom-component-helper.json +19 -0
  23. package/registry/blocks/destination-cards.json +16 -0
  24. package/registry/blocks/empty-state.json +16 -0
  25. package/registry/blocks/faq-accordion.json +16 -0
  26. package/registry/blocks/faqs-table.json +18 -0
  27. package/registry/blocks/feature-with-image.json +16 -0
  28. package/registry/blocks/featured-news-cards.json +16 -0
  29. package/registry/blocks/featured-places.json +16 -0
  30. package/registry/blocks/features-comparison.json +16 -0
  31. package/registry/blocks/filter-popover.json +28 -0
  32. package/registry/blocks/fixed-column-data-table.json +20 -0
  33. package/registry/blocks/flair-banner.json +16 -0
  34. package/registry/blocks/footer-navbar.json +17 -0
  35. package/registry/blocks/form-group.json +29 -0
  36. package/registry/blocks/gallery-section.json +14 -0
  37. package/registry/blocks/gradient-banner.json +16 -0
  38. package/registry/blocks/graph-metric-tiles.json +20 -0
  39. package/registry/blocks/grid-tiles-list.json +20 -0
  40. package/registry/blocks/hero-dark-centered.json +16 -0
  41. package/registry/blocks/hero-dark-with-image.json +16 -0
  42. package/registry/blocks/hero-fullwidth-image.json +16 -0
  43. package/registry/blocks/hero-section.json +16 -0
  44. package/registry/blocks/how-it-works.json +16 -0
  45. package/registry/blocks/image-feed-with-nested-comments.json +20 -0
  46. package/registry/blocks/infinity-canvas.json +58 -0
  47. package/registry/blocks/large-image-labels-list.json +19 -0
  48. package/registry/blocks/loader.json +19 -0
  49. package/registry/blocks/login-branding-panel.json +16 -0
  50. package/registry/blocks/menu-section.json +18 -0
  51. package/registry/blocks/menufocus-template.json +19 -0
  52. package/registry/blocks/messenger-sidebar.json +19 -0
  53. package/registry/blocks/metrics-section.json +14 -0
  54. package/registry/blocks/mobile-bottom-nav.json +18 -0
  55. package/registry/blocks/monthly-calendar-widget.json +20 -0
  56. package/registry/blocks/nested-comments-table.json +21 -0
  57. package/registry/blocks/nested-data-table.json +22 -0
  58. package/registry/blocks/office-locations.json +14 -0
  59. package/registry/blocks/page-header-section.json +17 -0
  60. package/registry/blocks/page-previews.json +29 -0
  61. package/registry/blocks/pagination.json +20 -0
  62. package/registry/blocks/participant-list.json +17 -0
  63. package/registry/blocks/persona-card.json +18 -0
  64. package/registry/blocks/pill-tabs.json +19 -0
  65. package/registry/blocks/pricing-cards.json +16 -0
  66. package/registry/blocks/pricing-cta.json +14 -0
  67. package/registry/blocks/profile-card.json +20 -0
  68. package/registry/blocks/profile-grid-tiles-list.json +21 -0
  69. package/registry/blocks/profile-image-uploader.json +19 -0
  70. package/registry/blocks/profile-info-cards.json +19 -0
  71. package/registry/blocks/progress-bar.json +16 -0
  72. package/registry/blocks/prompt-template.json +18 -0
  73. package/registry/blocks/reviews-grid.json +14 -0
  74. package/registry/blocks/reviews-table.json +19 -0
  75. package/registry/blocks/screen-flowchart.json +19 -0
  76. package/registry/blocks/screen-prompt-builder.json +19 -0
  77. package/registry/blocks/screen-prompt-template.json +18 -0
  78. package/registry/blocks/search-bar.json +19 -0
  79. package/registry/blocks/search-sidebar.json +25 -0
  80. package/registry/blocks/settings-list-row.json +20 -0
  81. package/registry/blocks/sidebar-cards.json +18 -0
  82. package/registry/blocks/sidebar-profile-card.json +21 -0
  83. package/registry/blocks/slideshow-grid-tiles.json +21 -0
  84. package/registry/blocks/social-feed.json +20 -0
  85. package/registry/blocks/social-proof.json +14 -0
  86. package/registry/blocks/standard-data-table.json +20 -0
  87. package/registry/blocks/standard-list-with-image.json +17 -0
  88. package/registry/blocks/step-tracker.json +16 -0
  89. package/registry/blocks/team-cards-grid.json +16 -0
  90. package/registry/blocks/team-circular-grid.json +16 -0
  91. package/registry/blocks/testimonial-carousel.json +16 -0
  92. package/registry/blocks/upvoting-posts-table.json +22 -0
  93. package/registry/blocks/vertical-how-it-works.json +16 -0
  94. package/registry/blocks/vertical-step-tracker.json +17 -0
  95. package/registry/blocks/video-chat-controls.json +18 -0
  96. package/registry/blocks/video-content-section.json +16 -0
  97. package/registry/blocks/video-playlist.json +18 -0
  98. package/registry/blocks/webcam-preview.json +18 -0
  99. package/registry/blocks/youtube-player.json +16 -0
  100. package/registry/hooks/use-css-variable-sync.json +14 -0
  101. package/registry/hooks/use-mobile.json +14 -0
  102. package/registry/index.json +730 -0
  103. package/registry/layout/account-settings-shell.json +20 -0
  104. package/registry/layout/dashboard-shell.json +23 -0
  105. package/registry/layout/double-sidebar-shell.json +23 -0
  106. package/registry/layout/double-sidebar.json +20 -0
  107. package/registry/layout/header.json +22 -0
  108. package/registry/layout/icon-sidebar-shell.json +23 -0
  109. package/registry/layout/icon-sidebar.json +19 -0
  110. package/registry/layout/mobile-menu-shell.json +19 -0
  111. package/registry/layout/multistep-progressbar-shell.json +23 -0
  112. package/registry/layout/multistep-shell.json +21 -0
  113. package/registry/layout/multistep-sidebar-shell.json +22 -0
  114. package/registry/layout/project-context-shell.json +20 -0
  115. package/registry/layout/search-bar-shell.json +22 -0
  116. package/registry/layout/sidebar-nav.json +18 -0
  117. package/registry/layout/sidebar.json +20 -0
  118. package/registry/layout/standard-page-shell.json +21 -0
  119. package/registry/layout/vertical-multistep-shell.json +23 -0
  120. package/registry/lib/utils.json +17 -0
  121. package/registry/ui/avatar.json +18 -0
  122. package/registry/ui/button.json +19 -0
  123. package/registry/ui/calendar.json +20 -0
  124. package/registry/ui/checkbox.json +19 -0
  125. package/registry/ui/date-input.json +18 -0
  126. package/registry/ui/dialog.json +19 -0
  127. package/registry/ui/dropdown-menu.json +19 -0
  128. package/registry/ui/file-uploader.json +18 -0
  129. package/registry/ui/image-uploader.json +18 -0
  130. package/registry/ui/input.json +16 -0
  131. package/registry/ui/label.json +18 -0
  132. package/registry/ui/line-tabs.json +16 -0
  133. package/registry/ui/multiselect-checkbox-field.json +18 -0
  134. package/registry/ui/multiselect-tags.json +18 -0
  135. package/registry/ui/popover.json +18 -0
  136. package/registry/ui/radio-group.json +19 -0
  137. package/registry/ui/range-input.json +17 -0
  138. package/registry/ui/scroll-area.json +18 -0
  139. package/registry/ui/searchbox.json +18 -0
  140. package/registry/ui/select.json +20 -0
  141. package/registry/ui/selectable-pills.json +16 -0
  142. package/registry/ui/separator.json +18 -0
  143. package/registry/ui/sheet.json +19 -0
  144. package/registry/ui/sidebar.json +27 -0
  145. package/registry/ui/skeleton.json +16 -0
  146. package/registry/ui/slider.json +18 -0
  147. package/registry/ui/switch.json +18 -0
  148. package/registry/ui/tabs.json +18 -0
  149. package/registry/ui/text-input.json +16 -0
  150. package/registry/ui/textarea.json +18 -0
  151. package/registry/ui/tooltip.json +18 -0
  152. package/registry/ui/typography.json +16 -0
  153. package/styles/tokens.reference.css +35 -3
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "credit-card-display",
3
+ "type": "registry:block",
4
+ "description": "Visual credit card display showing card details.",
5
+ "files": [
6
+ {
7
+ "path": "components/blocks/credit-card-display.tsx",
8
+ "type": "registry:block",
9
+ "content": "\"use client\";\n\nimport { cn } from \"../../lib/utils\";\n\ninterface CreditCardDisplayProps {\n /** Card type (visa, mastercard, amex, etc.) */\n cardType?: \"visa\" | \"mastercard\" | \"amex\" | \"discover\";\n /** Last 4 digits of card number */\n lastFourDigits?: string;\n /** Cardholder name */\n cardholderName?: string;\n /** Expiry date (MM/YY format) */\n expiryDate?: string;\n /** Additional class name */\n className?: string;\n}\n\n/**\n * Credit Card Display\n * \n * A visual credit card representation for payment settings.\n * Shows card type, masked number, cardholder name, and expiry.\n * Uses the flair background color with a subtle lighting gradient.\n */\nexport function CreditCardDisplay({\n cardType = \"visa\",\n lastFourDigits = \"1234\",\n cardholderName = \"Card Holder\",\n expiryDate = \"12/24\",\n className,\n}: CreditCardDisplayProps) {\n // Card type logos (simplified text representations)\n const cardTypeDisplay: Record<string, { name: string }> = {\n visa: { name: \"VISA\" },\n mastercard: { name: \"Mastercard\" },\n amex: { name: \"AMEX\" },\n discover: { name: \"Discover\" },\n };\n\n const cardInfo = cardTypeDisplay[cardType] || cardTypeDisplay.visa;\n\n return (\n <div\n className={cn(\n \"relative w-[375px] h-[240px] rounded-[var(--radius-lg)] overflow-hidden\",\n \"p-6 flex flex-col justify-between\",\n \"shadow-lg\",\n className\n )}\n style={{\n background: `linear-gradient(135deg, \n color-mix(in srgb, var(--canvas-flair-bg) 100%, white 0%) 0%, \n var(--canvas-flair-bg) 50%, \n color-mix(in srgb, var(--canvas-flair-bg) 100%, black 20%) 100%)`,\n }}\n >\n {/* Lighting overlay for depth */}\n <div \n className=\"absolute inset-0 pointer-events-none\"\n style={{\n background: `linear-gradient(145deg, \n rgba(255,255,255,0.15) 0%, \n rgba(255,255,255,0.05) 30%, \n transparent 50%, \n rgba(0,0,0,0.1) 100%)`,\n }}\n />\n\n {/* Top row - Card type logo */}\n <div className=\"relative flex justify-between items-start\">\n <span\n className=\"text-white font-bold italic\"\n style={{\n fontSize: \"28px\",\n letterSpacing: \"1px\",\n fontFamily: \"var(--typo-global-font)\",\n }}\n >\n {cardInfo.name}\n </span>\n </div>\n\n {/* Middle - Card number */}\n <div className=\"relative flex items-center gap-4\">\n <span \n className=\"text-white tracking-[4px]\"\n style={{\n fontFamily: \"monospace\",\n fontSize: \"18px\",\n }}\n >\n ****\n </span>\n <span \n className=\"text-white tracking-[4px]\"\n style={{\n fontFamily: \"monospace\",\n fontSize: \"18px\",\n }}\n >\n ****\n </span>\n <span \n className=\"text-white tracking-[4px]\"\n style={{\n fontFamily: \"monospace\",\n fontSize: \"18px\",\n }}\n >\n ****\n </span>\n <span \n className=\"text-white tracking-[4px]\"\n style={{\n fontFamily: \"monospace\",\n fontSize: \"18px\",\n }}\n >\n {lastFourDigits}\n </span>\n </div>\n\n {/* Bottom row - Name and Expiry */}\n <div className=\"relative flex justify-between items-end\">\n <div>\n <span \n className=\"text-white/70 text-xs block mb-1\"\n style={{ fontFamily: \"var(--typo-global-font)\" }}\n >\n CARDHOLDER\n </span>\n <span \n className=\"text-white font-medium\"\n style={{\n fontSize: \"16px\",\n fontFamily: \"var(--typo-global-font)\",\n }}\n >\n {cardholderName}\n </span>\n </div>\n <div className=\"text-right\">\n <span \n className=\"text-white/70 text-xs block mb-1\"\n style={{ fontFamily: \"var(--typo-global-font)\" }}\n >\n EXPIRES\n </span>\n <span \n className=\"text-white font-medium\"\n style={{\n fontSize: \"16px\",\n fontFamily: \"var(--typo-global-font)\",\n }}\n >\n {expiryDate}\n </span>\n </div>\n </div>\n </div>\n );\n}\n"
10
+ }
11
+ ],
12
+ "dependencies": [],
13
+ "registryDependencies": [
14
+ "utils"
15
+ ]
16
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "cta-banner",
3
+ "type": "registry:block",
4
+ "description": "Call to action banner with title and button.",
5
+ "files": [
6
+ {
7
+ "path": "components/blocks/marketing/cta-banner.tsx",
8
+ "type": "registry:block",
9
+ "content": "\"use client\";\n\nimport { Button } from \"../../ui/button\";\nimport { Typography } from \"../../ui/typography\";\n\ninterface CtaBannerProps {\n title?: string;\n buttonText?: string;\n onButtonClick?: () => void;\n}\n\nexport function CtaBanner({ \n title = \"Ready for your next adventure?\",\n buttonText = \"Book now\",\n onButtonClick \n}: CtaBannerProps) {\n return (\n <section \n className=\"w-full px-4 md:px-8 lg:px-10\"\n >\n <div \n className=\"w-full flex flex-col items-center justify-center text-center\"\n style={{\n backgroundColor: \"var(--canvas-dark-section-bg)\",\n borderRadius: \"var(--radius-3xl)\",\n padding: \"var(--spacing-6xl) var(--spacing-5xl) var(--spacing-7xl)\",\n gap: \"var(--spacing-4xl)\",\n }}\n >\n <Typography \n variant=\"h3\" \n as=\"h2\"\n style={{ color: \"white\" }}\n >\n {title}\n </Typography>\n <Button \n variant=\"primary\" \n size=\"lg\"\n onClick={onButtonClick}\n >\n {buttonText}\n </Button>\n </div>\n </section>\n );\n}\n\n"
10
+ }
11
+ ],
12
+ "dependencies": [],
13
+ "registryDependencies": []
14
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "custom-component-helper",
3
+ "type": "registry:block",
4
+ "description": "",
5
+ "files": [
6
+ {
7
+ "path": "components/blocks/custom-component-helper.tsx",
8
+ "type": "registry:block",
9
+ "content": "\"use client\";\n\nimport { useState, useMemo } from \"react\";\nimport { Check, Copy, Sparkles } from \"lucide-react\";\nimport { cn } from \"../../lib/utils\";\nimport { ComponentSearch, type ComponentOption } from \"./component-search\";\nimport { projectContext } from \"../../data/project-context\";\n\n// ═══════════════════════════════════════════════════════════\n// TYPES\n// ═══════════════════════════════════════════════════════════\n\ntype ComponentType = \"block\" | \"page-template\" | \"ui-component\";\n\ninterface CustomComponentHelperProps {\n className?: string;\n}\n\n// ═══════════════════════════════════════════════════════════\n// COMPONENT\n// ═══════════════════════════════════════════════════════════\n\nexport function CustomComponentHelper({ className }: CustomComponentHelperProps) {\n const [componentType, setComponentType] = useState<ComponentType>(\"block\");\n const [componentName, setComponentName] = useState(\"\");\n const [componentDescription, setComponentDescription] = useState(\"\");\n const [referenceComponents, setReferenceComponents] = useState<ComponentOption[]>([]);\n const [copied, setCopied] = useState(false);\n\n const { personas } = projectContext;\n\n // Generate the prompt\n const generatedPrompt = useMemo(() => {\n if (!componentName && !componentDescription) {\n return \"\";\n }\n\n const typeLabel = {\n block: \"block\",\n \"page-template\": \"page template\",\n \"ui-component\": \"UI component\",\n }[componentType];\n\n const parts: string[] = [\n \"Please create a plan for the following, then wait for my approval before making changes:\",\n \"\",\n ];\n\n // Context references\n parts.push(\"CONTEXT:\");\n parts.push(\"- Read src/data/scope.md for project scope and requirements\");\n parts.push(\"- Reference src/data/project-context.ts for user personas and project goals\");\n parts.push(\"\");\n\n parts.push(`Create a new ${typeLabel} component:`);\n parts.push(\"\");\n\n if (componentName) {\n parts.push(`Name: ${componentName}`);\n }\n\n parts.push(`Type: ${typeLabel.charAt(0).toUpperCase() + typeLabel.slice(1)}`);\n\n if (componentDescription) {\n parts.push(`Description: ${componentDescription}`);\n }\n\n // Include personas for context\n if (personas.length > 0) {\n parts.push(\"\");\n parts.push(\"Design this component to serve these user personas:\");\n personas.forEach((p) => {\n parts.push(`- ${p.name} (${p.role})`);\n });\n }\n\n // Reference components\n if (referenceComponents.length > 0) {\n parts.push(\"\");\n parts.push(\"Reference these existing components for style/patterns:\");\n referenceComponents.forEach((c) => {\n parts.push(`- ${c.name} (${c.path})`);\n });\n }\n\n // Requirements\n parts.push(\"\");\n parts.push(\"Requirements:\");\n parts.push(\"1. Build using ShadCN primitives (Button, Dialog, Input, etc.)\");\n parts.push(\"2. Implement CSS variables for theming:\");\n parts.push(\" - var(--canvas-*) for colors (primary, background, text, border, surface)\");\n parts.push(\" - var(--spacing-*) for spacing (sm, md, lg, xl)\");\n parts.push(\" - var(--radius-*) for border radius\");\n parts.push(\" - var(--typo-*) for typography\");\n\n // File location based on type\n const kebabName = toKebabCase(componentName || \"new-component\");\n if (componentType === \"block\") {\n parts.push(`3. Create file at src/components/blocks/${kebabName}.tsx`);\n parts.push(\"4. Export from src/components/blocks/index.ts\");\n } else if (componentType === \"page-template\") {\n const pageName = toKebabCase(componentName || \"new-page\");\n parts.push(`3. Create page at src/app/${pageName}/page.tsx`);\n parts.push(`4. Create layout at src/app/${pageName}/layout.tsx`);\n } else {\n parts.push(`3. Create file at src/components/ui/${kebabName}.tsx`);\n parts.push(\"4. Export from src/components/ui/index.ts (if exists)\");\n }\n\n parts.push(\"5. Add entry to src/lib/component-registry.ts after creation\");\n parts.push(\"6. Follow existing component patterns in the codebase\");\n parts.push(\"7. Ensure the component aligns with the project scope and serves the target personas\");\n\n return parts.join(\"\\n\");\n }, [componentType, componentName, componentDescription, referenceComponents, personas]);\n\n const handleCopy = async () => {\n if (!generatedPrompt) return;\n await navigator.clipboard.writeText(generatedPrompt);\n setCopied(true);\n setTimeout(() => setCopied(false), 2000);\n };\n\n const hasContent = componentName || componentDescription;\n\n return (\n <div className={cn(\"space-y-6\", className)}>\n {/* Section Header */}\n <div>\n <h3 className=\"text-lg font-semibold text-[var(--canvas-text)]\">\n Create Custom Component\n </h3>\n <p className=\"text-sm text-[var(--canvas-text-muted)] mt-1\">\n Generate a prompt to create a new ShadCN-based component with design variables\n </p>\n </div>\n\n {/* Component Type */}\n <div className=\"space-y-2\">\n <label className=\"text-sm font-medium text-[var(--canvas-text)]\">\n Component Type\n </label>\n <div className=\"flex gap-2\">\n {[\n { id: \"block\", label: \"Block\" },\n { id: \"page-template\", label: \"Page Template\" },\n { id: \"ui-component\", label: \"UI Component\" },\n ].map((type) => (\n <button\n key={type.id}\n onClick={() => setComponentType(type.id as ComponentType)}\n className={cn(\n \"px-4 py-2 rounded-lg text-sm font-medium transition-all\",\n componentType === type.id\n ? \"bg-[var(--canvas-primary)] text-white\"\n : \"bg-[var(--canvas-surface)] text-[var(--canvas-text-muted)] border border-[var(--canvas-border)] hover:border-[var(--canvas-primary)] hover:text-[var(--canvas-primary)]\"\n )}\n >\n {type.label}\n </button>\n ))}\n </div>\n </div>\n\n {/* Component Name */}\n <div className=\"space-y-2\">\n <label\n htmlFor=\"component-name\"\n className=\"text-sm font-medium text-[var(--canvas-text)]\"\n >\n Component Name\n </label>\n <input\n id=\"component-name\"\n type=\"text\"\n value={componentName}\n onChange={(e) => setComponentName(e.target.value)}\n placeholder=\"e.g., MultiStepPopup, ImageCarousel, StatCard\"\n className=\"w-full px-3 py-2 rounded-lg border border-[var(--canvas-border)] bg-[var(--canvas-background)] text-[var(--canvas-text)] text-sm placeholder:text-[var(--canvas-text-placeholder)] focus:outline-none focus:ring-2 focus:ring-[var(--canvas-primary)] focus:border-transparent\"\n />\n </div>\n\n {/* Component Description */}\n <div className=\"space-y-2\">\n <label\n htmlFor=\"component-description\"\n className=\"text-sm font-medium text-[var(--canvas-text)]\"\n >\n Description\n </label>\n <textarea\n id=\"component-description\"\n value={componentDescription}\n onChange={(e) => setComponentDescription(e.target.value)}\n placeholder=\"Describe what this component should do, its features, and any specific requirements (e.g., a multi-step popup with steps listed on the left side and content on the right, progress indicator, next/back buttons)\"\n rows={4}\n className=\"w-full px-3 py-2 rounded-lg border border-[var(--canvas-border)] bg-[var(--canvas-background)] text-[var(--canvas-text)] text-sm placeholder:text-[var(--canvas-text-placeholder)] focus:outline-none focus:ring-2 focus:ring-[var(--canvas-primary)] focus:border-transparent resize-none\"\n />\n </div>\n\n {/* Reference Components */}\n <div className=\"space-y-2\">\n <label className=\"text-sm font-medium text-[var(--canvas-text)]\">\n Reference Components (optional)\n </label>\n <p className=\"text-xs text-[var(--canvas-text-muted)] mb-2\">\n Select existing components to use as style/pattern references\n </p>\n <ComponentSearch\n selectedComponents={referenceComponents}\n onSelectionChange={setReferenceComponents}\n />\n </div>\n\n {/* Generated Prompt Preview */}\n {hasContent && (\n <div className=\"rounded-lg border border-dashed border-[var(--canvas-border)] bg-[var(--canvas-surface)] p-4\">\n {/* Header */}\n <div className=\"flex items-center justify-between mb-3\">\n <div className=\"flex items-center gap-2 text-sm font-medium text-[var(--canvas-text-muted)]\">\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 text-xs font-medium transition-all\",\n copied\n ? \"bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400\"\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 >\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-sm 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\">\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-sm text-[var(--canvas-text-muted)]\">\n Enter a component name or description to generate a prompt\n </p>\n </div>\n )}\n </div>\n );\n}\n\n// ═══════════════════════════════════════════════════════════\n// HELPERS\n// ═══════════════════════════════════════════════════════════\n\nfunction toKebabCase(str: string): string {\n return str\n .replace(/([a-z])([A-Z])/g, \"$1-$2\")\n .replace(/\\s+/g, \"-\")\n .toLowerCase();\n}\n"
10
+ }
11
+ ],
12
+ "dependencies": [
13
+ "lucide-react"
14
+ ],
15
+ "registryDependencies": [
16
+ "utils",
17
+ "component-search"
18
+ ]
19
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "destination-cards",
3
+ "type": "registry:block",
4
+ "description": "Grid of destination cards with images and titles.",
5
+ "files": [
6
+ {
7
+ "path": "components/blocks/marketing/destination-cards.tsx",
8
+ "type": "registry:block",
9
+ "content": "\"use client\";\n\nimport { ArrowRight } from \"@phosphor-icons/react\";\nimport { Typography } from \"../../ui/typography\";\n\ninterface DestinationCard {\n id: string;\n name: string;\n count: string;\n price: string;\n image: string;\n}\n\ninterface DestinationCardsProps {\n title: string;\n destinations: DestinationCard[];\n onExploreMore?: () => void;\n}\n\nconst defaultDestinations: DestinationCard[] = [\n {\n id: \"1\",\n name: \"Las Vegas\",\n count: \"246 homes\",\n price: \"from $240\",\n image: \"https://images.unsplash.com/photo-1605833556294-ea5c7a74f57d?w=400&h=300&fit=crop\",\n },\n {\n id: \"2\",\n name: \"Denver\",\n count: \"88 homes\",\n price: \"from $146\",\n image: \"https://images.unsplash.com/photo-1546156929-a4c0ac411f47?w=400&h=300&fit=crop\",\n },\n {\n id: \"3\",\n name: \"New York\",\n count: \"842 homes\",\n price: \"from $354\",\n image: \"https://images.unsplash.com/photo-1496442226666-8d4d0e62e6e9?w=400&h=300&fit=crop\",\n },\n {\n id: \"4\",\n name: \"San Francisco\",\n count: \"194 homes\",\n price: \"from $278\",\n image: \"https://images.unsplash.com/photo-1501594907352-04cda38ebc29?w=400&h=300&fit=crop\",\n },\n];\n\nexport function DestinationCards({ \n title = \"Top destinations\", \n destinations = defaultDestinations,\n onExploreMore \n}: Partial<DestinationCardsProps>) {\n return (\n <section \n className=\"w-full px-4 md:px-8 lg:px-10 py-10 md:py-16\"\n style={{\n backgroundColor: \"var(--canvas-background)\",\n }}\n >\n <div className=\"w-full max-w-[1240px] mx-auto\">\n {/* Header */}\n <div className=\"flex items-center justify-between mb-6\" style={{ marginBottom: \"var(--spacing-3xl)\" }}>\n <Typography variant=\"h3\" as=\"h2\">\n {title}\n </Typography>\n <button \n onClick={onExploreMore}\n className=\"flex items-center gap-2 hover:opacity-80 transition-opacity\"\n style={{ paddingTop: \"var(--spacing-2xl)\" }}\n >\n <Typography variant=\"body-l\" as=\"span\" style={{ fontWeight: 600 }}>\n Explore more\n </Typography>\n <div \n className=\"flex items-center justify-center\"\n style={{\n width: \"24px\",\n height: \"24px\",\n backgroundColor: \"var(--canvas-text)\",\n borderRadius: \"var(--radius-4xl)\",\n }}\n >\n <ArrowRight size={16} color=\"white\" weight=\"bold\" />\n </div>\n </button>\n </div>\n \n {/* Cards Grid */}\n <div className=\"grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-10\">\n {destinations.map((dest) => (\n <div \n key={dest.id}\n className=\"group cursor-pointer overflow-hidden\"\n style={{\n border: \"1px solid var(--canvas-border)\",\n borderRadius: \"var(--radius-xl)\",\n }}\n >\n <div \n className=\"w-full overflow-hidden\"\n style={{ \n height: \"240px\",\n borderBottom: \"1px solid var(--canvas-border)\",\n }}\n >\n <img \n src={dest.image} \n alt={dest.name}\n className=\"w-full h-full object-cover group-hover:scale-105 transition-transform duration-300\"\n />\n </div>\n <div \n className=\"flex flex-col\"\n style={{ \n padding: \"0 var(--spacing-3xl) var(--spacing-3xl)\",\n gap: \"var(--spacing-xs)\",\n }}\n >\n <Typography variant=\"body-xl\" as=\"h3\" className=\"mt-6\" style={{ fontWeight: 600 }}>\n {dest.name}\n </Typography>\n <Typography variant=\"body-m\" color=\"muted\">\n {dest.count}\n </Typography>\n <Typography variant=\"h6\" as=\"p\" className=\"mt-5\">\n {dest.price}\n </Typography>\n </div>\n </div>\n ))}\n </div>\n </div>\n </section>\n );\n}\n"
10
+ }
11
+ ],
12
+ "dependencies": [
13
+ "@phosphor-icons/react"
14
+ ],
15
+ "registryDependencies": []
16
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "empty-state",
3
+ "type": "registry:block",
4
+ "description": "",
5
+ "files": [
6
+ {
7
+ "path": "components/blocks/empty-state.tsx",
8
+ "type": "registry:block",
9
+ "content": "\"use client\";\n\nimport { cn } from \"../../lib/utils\";\n\ninterface EmptyStateProps {\n icon: string;\n title: string;\n description?: string;\n className?: string;\n}\n\nexport function EmptyState({ icon, title, description, className }: EmptyStateProps) {\n return (\n <div\n className={cn(\n \"flex flex-col items-center justify-center py-12 px-6\",\n \"rounded-xl border-2 border-dashed border-[var(--canvas-border)]\",\n \"bg-[var(--canvas-surface)]\",\n className\n )}\n >\n <span className=\"text-4xl mb-3\">{icon}</span>\n <p className=\"text-[var(--canvas-text-muted)] font-medium text-center\">\n {title}\n </p>\n {description && (\n <p className=\"text-sm text-[var(--canvas-text-placeholder)] text-center mt-1 max-w-md\">\n {description}\n </p>\n )}\n </div>\n );\n}\n"
10
+ }
11
+ ],
12
+ "dependencies": [],
13
+ "registryDependencies": [
14
+ "utils"
15
+ ]
16
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "faq-accordion",
3
+ "type": "registry:block",
4
+ "description": "Expandable FAQ accordion for pricing pages.",
5
+ "files": [
6
+ {
7
+ "path": "components/blocks/pricing/faq-accordion.tsx",
8
+ "type": "registry:block",
9
+ "content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { CaretUp, CaretDown } from \"@phosphor-icons/react\";\nimport { Typography } from \"../../ui/typography\";\n\ninterface FAQItem {\n question: string;\n answer: string;\n}\n\nconst faqs: FAQItem[] = [\n {\n question: \"What kind of support is provided?\",\n answer:\n \"We provide customer support via email, and aim to reply to all requests within 48 hours. Those with Deluxe plan and above gets priority support.\",\n },\n {\n question: \"What's the difference between a Professional plan and an Enterprise plan?\",\n answer:\n \"The Professional plan is designed for large teams with advanced needs, while the Enterprise plan offers custom solutions, dedicated account management, and enhanced security features tailored to your organization.\",\n },\n {\n question: \"Can I cancel my plan at any time?\",\n answer:\n \"Yes, you can cancel your subscription at any time. Your access will continue until the end of your current billing period, and you won't be charged again.\",\n },\n {\n question: \"Where are the uploaded files hosted?\",\n answer:\n \"All files are securely hosted on enterprise-grade cloud infrastructure with data centers located globally. We ensure high availability, redundancy, and compliance with industry security standards.\",\n },\n];\n\nexport function FaqAccordion() {\n const [expandedIndex, setExpandedIndex] = useState<number>(0);\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-[768px] mx-auto flex flex-col items-center\">\n {/* Header */}\n <div className=\"w-full pb-12 text-center\">\n <Typography variant=\"h3\" as=\"h2\">\n FAQs\n </Typography>\n </div>\n\n {/* FAQ Items */}\n <div className=\"w-full flex flex-col\">\n {faqs.map((faq, index) => {\n const isExpanded = expandedIndex === index;\n\n return (\n <div\n key={index}\n className=\"flex flex-col gap-4 py-6 border-b\"\n style={{ borderColor: \"var(--canvas-border)\" }}\n >\n <button\n onClick={() => setExpandedIndex(isExpanded ? -1 : index)}\n className=\"flex items-center justify-between w-full text-left\"\n >\n <Typography variant=\"body-xl\" as=\"span\" style={{ fontWeight: 600 }}>\n {faq.question}\n </Typography>\n {isExpanded ? (\n <CaretUp\n size={24}\n className=\"shrink-0 ml-4\"\n style={{ color: \"var(--canvas-text-muted)\" }}\n />\n ) : (\n <CaretDown\n size={24}\n className=\"shrink-0 ml-4\"\n style={{ color: \"var(--canvas-text-muted)\" }}\n />\n )}\n </button>\n\n {isExpanded && (\n <Typography variant=\"body-l\" color=\"muted\">\n {faq.answer}\n </Typography>\n )}\n </div>\n );\n })}\n </div>\n\n {/* Footer */}\n <div className=\"flex items-center gap-4 pt-6\">\n <Typography variant=\"body-l\" as=\"span\" color=\"muted\">\n Have more questions?\n </Typography>\n <button\n style={{\n color: \"var(--canvas-primary)\",\n }}\n >\n <Typography variant=\"body-l\" as=\"span\" style={{ fontWeight: 600, color: \"var(--canvas-primary)\" }}>\n Contact us\n </Typography>\n </button>\n </div>\n </div>\n </section>\n );\n}\n"
10
+ }
11
+ ],
12
+ "dependencies": [
13
+ "@phosphor-icons/react"
14
+ ],
15
+ "registryDependencies": []
16
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "faqs-table",
3
+ "type": "registry:block",
4
+ "description": "Expandable FAQ list with accordion behavior. Shows questions with plus/minus toggle icons, revealing answers one at a time.",
5
+ "files": [
6
+ {
7
+ "path": "components/blocks/faqs-table.tsx",
8
+ "type": "registry:block",
9
+ "content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport { Plus, Minus } from \"lucide-react\";\n\n// ============================================\n// Types\n// ============================================\n\nexport interface FAQItem {\n id: string;\n question: string;\n answer: string;\n}\n\nexport interface FaqsTableProps {\n /** Table title */\n title?: string;\n /** FAQ items to display */\n items?: FAQItem[];\n /** ID of the item that should be expanded by default */\n defaultExpandedId?: string;\n /** Additional class names */\n className?: string;\n}\n\n// ============================================\n// Default Data\n// ============================================\n\nconst defaultItems: FAQItem[] = [\n {\n id: \"1\",\n question: \"What is Canvas?\",\n answer:\n \"Canvas uses a modular approach that allows you to easily plug in standardized components for most of your application's UX. That allows you to not reinvent the wheel and focus most of your efforts on your product's unique parts.\",\n },\n {\n id: \"2\",\n question: \"How can I purchase a license?\",\n answer:\n \"You can purchase a license directly from our website. We offer individual, team, and enterprise plans to suit your needs. All plans include access to our full component library and design system.\",\n },\n {\n id: \"3\",\n question: \"Do you offer refunds?\",\n answer:\n \"Yes, we offer a 30-day money-back guarantee. If you're not satisfied with your purchase, contact our support team for a full refund.\",\n },\n {\n id: \"4\",\n question: \"Can I use Canvas for commercial projects?\",\n answer:\n \"Absolutely! All our licenses allow for unlimited commercial projects. You can use Canvas components in client work, SaaS products, and internal tools.\",\n },\n];\n\n// ============================================\n// Main Component\n// ============================================\n\n/**\n * Canvas Design System - FAQs Table Block\n *\n * An expandable FAQ list with plus/minus toggle icons.\n * Displays one item expanded at a time (accordion behavior).\n *\n * @example\n * ```tsx\n * <FaqsTable\n * title=\"Common questions\"\n * items={[\n * { id: \"1\", question: \"What is Canvas?\", answer: \"...\" }\n * ]}\n * />\n * ```\n */\nexport function FaqsTable({\n title = \"Common questions\",\n items = defaultItems,\n defaultExpandedId,\n className,\n}: FaqsTableProps) {\n const [expandedId, setExpandedId] = useState<string | null>(\n defaultExpandedId ?? items[0]?.id ?? null\n );\n\n const toggleItem = (id: string) => {\n setExpandedId((current) => (current === id ? null : id));\n };\n\n return (\n <div\n className={cn(\"flex flex-col w-full\", className)}\n style={{ gap: \"var(--spacing-xl)\" }}\n >\n {/* Title Section */}\n <div\n className=\"flex flex-wrap items-start w-full\"\n style={{ gap: \"var(--spacing-xl)\" }}\n >\n <div\n className=\"flex flex-col flex-1 min-w-[200px]\"\n style={{ gap: \"var(--spacing-xs)\" }}\n >\n <h2\n style={{\n fontFamily: \"var(--typo-h6-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-h6-size)\",\n fontWeight: \"var(--typo-h6-weight)\",\n letterSpacing: \"var(--typo-h6-spacing)\",\n lineHeight: \"var(--typo-h6-line-height)\",\n color: \"var(--canvas-text)\",\n margin: 0,\n }}\n >\n {title}\n </h2>\n </div>\n </div>\n\n {/* FAQ List */}\n <div className=\"flex flex-col w-full\">\n {items.map((item, index) => {\n const isExpanded = expandedId === item.id;\n const isFirst = index === 0;\n const isLast = index === items.length - 1;\n\n return (\n <div\n key={item.id}\n className=\"flex flex-col w-full\"\n style={{\n borderTop: isFirst ? \"1px solid var(--canvas-border)\" : \"none\",\n borderBottom: \"1px solid var(--canvas-border)\",\n paddingTop: \"var(--spacing-xl)\",\n paddingBottom: \"var(--spacing-xl)\",\n }}\n >\n {/* Question Row */}\n <button\n onClick={() => toggleItem(item.id)}\n className=\"flex items-center justify-between w-full text-left\"\n style={{ gap: \"var(--spacing-xl)\" }}\n aria-expanded={isExpanded}\n aria-controls={`faq-answer-${item.id}`}\n >\n <span\n className=\"flex-1\"\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 600,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n {item.question}\n </span>\n <div\n className=\"flex items-center justify-center shrink-0\"\n style={{\n width: \"32px\",\n height: \"32px\",\n padding: \"var(--spacing-md)\",\n borderRadius: \"var(--radius-full)\",\n }}\n >\n {isExpanded ? (\n <Minus\n size={20}\n style={{ color: \"var(--canvas-text)\" }}\n strokeWidth={1.5}\n />\n ) : (\n <Plus\n size={20}\n style={{ color: \"var(--canvas-text)\" }}\n strokeWidth={1.5}\n />\n )}\n </div>\n </button>\n\n {/* Answer (collapsible) */}\n {isExpanded && (\n <div\n id={`faq-answer-${item.id}`}\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: \"var(--typo-body-s-weight)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text-muted)\",\n paddingTop: \"var(--spacing-md)\",\n }}\n >\n {item.answer}\n </div>\n )}\n </div>\n );\n })}\n </div>\n </div>\n );\n}\n"
10
+ }
11
+ ],
12
+ "dependencies": [
13
+ "lucide-react"
14
+ ],
15
+ "registryDependencies": [
16
+ "utils"
17
+ ]
18
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "feature-with-image",
3
+ "type": "registry:block",
4
+ "description": "Feature highlight with image and text side by side.",
5
+ "files": [
6
+ {
7
+ "path": "components/blocks/marketing/feature-with-image.tsx",
8
+ "type": "registry:block",
9
+ "content": "\"use client\";\n\nimport { Check, ArrowRight } from \"@phosphor-icons/react\";\nimport { Typography } from \"../../ui/typography\";\n\ninterface FeatureBenefit {\n id: string;\n text: string;\n}\n\ninterface FeatureWithImageProps {\n title?: string;\n description?: string;\n benefits?: FeatureBenefit[];\n ctaText?: string;\n onCtaClick?: () => void;\n image?: string;\n imagePosition?: \"left\" | \"right\";\n}\n\nconst defaultBenefits: FeatureBenefit[] = [\n { id: \"1\", text: \"Earn 5x points on all bookings through the platform\" },\n { id: \"2\", text: \"Earn 3x points on dining and fuel\" },\n { id: \"3\", text: \"Guaranteed early check-in, when available\" },\n { id: \"4\", text: \"No foreign transaction fees\" },\n];\n\nexport function FeatureWithImage({ \n title = \"The ultimate travel credit card\",\n description = \"Get a $150 statement credit after your first purchase, plus 5,000 bonus points when you meet the spending requirement. Earn 5X points on all bookings through our platform, 2X points on dining and fuel, and enjoy automatic VIP perks. Terms apply.\",\n benefits = defaultBenefits,\n ctaText = \"Learn more\",\n onCtaClick,\n image = \"https://images.unsplash.com/photo-1556742049-0cfed4f6a45d?w=580&h=420&fit=crop\",\n imagePosition = \"left\",\n}: FeatureWithImageProps) {\n return (\n <section \n className=\"w-full px-4 md:px-8 lg:px-20\"\n style={{\n backgroundColor: \"var(--canvas-background)\",\n }}\n >\n <div \n className={`w-full max-w-[1240px] mx-auto flex flex-col ${imagePosition === \"left\" ? \"lg:flex-row\" : \"lg:flex-row-reverse\"} gap-12 lg:gap-20 items-center py-12 md:py-24`}\n >\n {/* Image */}\n <div \n className=\"w-full lg:w-[580px] h-[320px] lg:h-[420px] overflow-hidden shrink-0\"\n style={{\n borderRadius: \"var(--radius-xl)\",\n border: \"1px solid var(--canvas-border)\",\n boxShadow: \"0px 4px 96px 0px rgba(13, 18, 28, 0.08)\",\n }}\n >\n <img \n src={image} \n alt={title}\n className=\"w-full h-full object-cover\"\n />\n </div>\n\n {/* Content */}\n <div className=\"flex flex-col gap-6 max-w-[580px]\">\n <Typography variant=\"h3\" as=\"h2\">\n {title}\n </Typography>\n \n <Typography variant=\"body-l\" color=\"muted\">\n {description}\n </Typography>\n\n {/* Benefits list */}\n <div className=\"flex flex-col gap-1\">\n {benefits.map((benefit) => (\n <div \n key={benefit.id}\n className=\"flex items-center gap-2 h-7\"\n >\n <Check size={20} style={{ color: \"var(--canvas-text)\" }} />\n <Typography variant=\"body-l\" color=\"muted\">\n {benefit.text}\n </Typography>\n </div>\n ))}\n </div>\n\n {/* CTA Button */}\n <button \n onClick={onCtaClick}\n className=\"flex items-center gap-2 pt-5 hover:opacity-80 transition-opacity w-fit\"\n >\n <Typography variant=\"body-m\" as=\"span\" style={{ fontWeight: 500 }}>\n {ctaText}\n </Typography>\n <div \n className=\"flex items-center justify-center\"\n style={{\n width: \"24px\",\n height: \"24px\",\n backgroundColor: \"var(--canvas-text)\",\n borderRadius: \"var(--radius-4xl)\",\n }}\n >\n <ArrowRight size={16} color=\"white\" weight=\"bold\" />\n </div>\n </button>\n </div>\n </div>\n </section>\n );\n}\n"
10
+ }
11
+ ],
12
+ "dependencies": [
13
+ "@phosphor-icons/react"
14
+ ],
15
+ "registryDependencies": []
16
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "featured-news-cards",
3
+ "type": "registry:block",
4
+ "description": "Featured news/press cards section.",
5
+ "files": [
6
+ {
7
+ "path": "components/blocks/marketing/featured-news-cards.tsx",
8
+ "type": "registry:block",
9
+ "content": "\"use client\";\n\nimport { ArrowRight } from \"@phosphor-icons/react\";\nimport { Typography } from \"../../ui/typography\";\n\ninterface NewsItem {\n title: string;\n description: string;\n image: string;\n href?: string;\n}\n\ninterface FeaturedNewsCardsProps {\n variant?: \"light\" | \"dark\";\n subtitle?: string;\n title?: string;\n description?: string;\n items?: NewsItem[];\n}\n\nconst defaultItems: NewsItem[] = [\n {\n title: \"How this company is using AI to 10x its revenue\",\n description:\n \"Explore how this company is leveraging AI to enhance innovation, streamline operations, and improve customer experiences\",\n image:\n \"https://images.unsplash.com/photo-1497366216548-37526070297c?w=800&h=500&fit=crop\",\n href: \"#\",\n },\n {\n title: \"Fundraise on confidence\",\n description:\n \"Discover how our founders successfully raised over $200 million and gain insights to fuel your own fundraising journey.\",\n image:\n \"https://images.unsplash.com/photo-1486406146926-c627a92ad1ab?w=800&h=500&fit=crop\",\n href: \"#\",\n },\n];\n\nexport function FeaturedNewsCards({\n variant = \"light\",\n subtitle = \"IN THE PRESS\",\n title = \"Featured news\",\n description = \"Stay updated with the latest headlines, media coverage, and featured stories about us from top publications\",\n items = defaultItems,\n}: FeaturedNewsCardsProps) {\n const isDark = variant === \"dark\";\n\n return (\n <section\n className=\"w-full px-6 md:px-20 py-16 md:py-24\"\n style={{\n backgroundColor: isDark\n ? \"var(--canvas-dark-section-bg)\"\n : \"var(--canvas-background)\",\n }}\n >\n <div className=\"max-w-[1240px] mx-auto flex flex-col items-center gap-12 md:gap-16\">\n {/* Header */}\n <div className=\"flex flex-col items-center gap-4 md:gap-6 max-w-[768px] text-center\">\n <div className=\"flex flex-col items-center gap-3\">\n <Typography variant=\"body-xs\" as=\"span\" color=\"muted\" className=\"uppercase tracking-wide\">\n {subtitle}\n </Typography>\n <Typography\n variant=\"h3\"\n as=\"h2\"\n {...(isDark && { style: { color: \"white\" } })}\n >\n {title}\n </Typography>\n </div>\n <Typography\n variant=\"body-l\"\n color=\"muted\"\n {...(isDark && { style: { color: \"rgba(255, 255, 255, 0.7)\" } })}\n >\n {description}\n </Typography>\n </div>\n\n {/* News Cards */}\n <div className=\"grid grid-cols-1 md:grid-cols-2 gap-8 md:gap-10 w-full\">\n {items.map((item, index) => (\n <div\n key={index}\n className=\"rounded-xl overflow-hidden\"\n style={{\n backgroundColor: isDark\n ? \"var(--canvas-dark-card-bg)\"\n : \"var(--canvas-background)\",\n border: isDark ? \"none\" : \"1px solid var(--canvas-border)\",\n }}\n >\n {/* Image */}\n <div className=\"w-full h-[220px] md:h-[340px]\">\n <img\n src={item.image}\n alt={item.title}\n className=\"w-full h-full object-cover\"\n />\n </div>\n\n {/* Content */}\n <div className=\"flex flex-col gap-1 p-6\">\n <Typography\n variant=\"h6\"\n as=\"h3\"\n {...(isDark && { style: { color: \"white\" } })}\n >\n {item.title}\n </Typography>\n <Typography\n variant=\"body-m\"\n color=\"muted\"\n {...(isDark && { style: { color: \"rgba(255, 255, 255, 0.7)\" } })}\n >\n {item.description}\n </Typography>\n\n {/* Read More Link */}\n <a\n href={item.href || \"#\"}\n className=\"flex items-center gap-2 pt-5 group\"\n >\n <Typography\n variant=\"body-m\"\n as=\"span\"\n {...(isDark && { style: { color: \"white\" } })}\n >\n Read more\n </Typography>\n <div\n className=\"w-6 h-6 rounded-full flex items-center justify-center group-hover:translate-x-1 transition-transform\"\n style={{\n backgroundColor: isDark\n ? \"rgba(255, 255, 255, 0.5)\"\n : \"var(--canvas-text)\",\n }}\n >\n <ArrowRight size={16} weight=\"bold\" color=\"white\" />\n </div>\n </a>\n </div>\n </div>\n ))}\n </div>\n </div>\n </section>\n );\n}\n"
10
+ }
11
+ ],
12
+ "dependencies": [
13
+ "@phosphor-icons/react"
14
+ ],
15
+ "registryDependencies": []
16
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "featured-places",
3
+ "type": "registry:block",
4
+ "description": "Featured places section with filter pills and card grid.",
5
+ "files": [
6
+ {
7
+ "path": "components/blocks/marketing/featured-places.tsx",
8
+ "type": "registry:block",
9
+ "content": "\"use client\";\n\nimport { ArrowRight, Star, Camera, Buildings, Palette, Mountains } from \"@phosphor-icons/react\";\nimport { Typography } from \"../../ui/typography\";\nimport { useState } from \"react\";\n\ninterface FeaturedPlace {\n id: string;\n name: string;\n count: string;\n price: string;\n rating: number;\n image: string;\n}\n\ninterface FilterPill {\n id: string;\n label: string;\n icon: React.ReactNode;\n}\n\nconst defaultFilters: FilterPill[] = [\n { id: \"views\", label: \"Amazing views\", icon: <Camera size={20} /> },\n { id: \"cities\", label: \"Top cities\", icon: <Buildings size={20} /> },\n { id: \"design\", label: \"Design\", icon: <Palette size={20} /> },\n { id: \"countryside\", label: \"Countryside\", icon: <Mountains size={20} /> },\n];\n\nconst defaultPlaces: FeaturedPlace[] = [\n {\n id: \"1\",\n name: \"Sausalito, California\",\n count: \"246 homes\",\n price: \"from $240\",\n rating: 4.92,\n image: \"https://images.unsplash.com/photo-1600596542815-ffad4c1539a9?w=400&h=300&fit=crop\",\n },\n {\n id: \"2\",\n name: \"Portola Valley, California\",\n count: \"634 homes\",\n price: \"from $354\",\n rating: 4.98,\n image: \"https://images.unsplash.com/photo-1600585154340-be6161a56a0c?w=400&h=300&fit=crop\",\n },\n {\n id: \"3\",\n name: \"San Francisco, California\",\n count: \"194 homes\",\n price: \"from $278\",\n rating: 4.87,\n image: \"https://images.unsplash.com/photo-1449844908441-8829872d2607?w=400&h=300&fit=crop\",\n },\n {\n id: \"4\",\n name: \"Sonoma, California\",\n count: \"176 homes\",\n price: \"from $543\",\n rating: 4.96,\n image: \"https://images.unsplash.com/photo-1564013799919-ab600027ffc6?w=400&h=300&fit=crop\",\n },\n {\n id: \"5\",\n name: \"Half Moon Bay, California\",\n count: \"83 homes\",\n price: \"from $389\",\n rating: 4.82,\n image: \"https://images.unsplash.com/photo-1600047509807-ba8f99d2cdde?w=400&h=300&fit=crop\",\n },\n {\n id: \"6\",\n name: \"Sebastopol, California\",\n count: \"65 homes\",\n price: \"from $176\",\n rating: 4.84,\n image: \"https://images.unsplash.com/photo-1600566753190-17f0baa2a6c3?w=400&h=300&fit=crop\",\n },\n];\n\nexport function FeaturedPlaces() {\n const [activeFilter, setActiveFilter] = useState(\"views\");\n\n return (\n <section \n className=\"w-full px-4 md:px-8 lg:px-10 py-10 md:py-16\"\n style={{\n backgroundColor: \"var(--canvas-background)\",\n }}\n >\n <div className=\"w-full max-w-[1240px] mx-auto\">\n {/* Header */}\n <div className=\"flex items-center justify-between\" style={{ marginBottom: \"var(--spacing-3xl)\" }}>\n <Typography variant=\"h3\" as=\"h2\">\n Featured places\n </Typography>\n <button \n className=\"flex items-center gap-2 hover:opacity-80 transition-opacity\"\n style={{ paddingTop: \"var(--spacing-2xl)\" }}\n >\n <Typography variant=\"body-l\" as=\"span\" style={{ fontWeight: 600 }}>\n Explore more\n </Typography>\n <div \n className=\"flex items-center justify-center\"\n style={{\n width: \"24px\",\n height: \"24px\",\n backgroundColor: \"var(--canvas-text)\",\n borderRadius: \"var(--radius-4xl)\",\n }}\n >\n <ArrowRight size={16} color=\"white\" weight=\"bold\" />\n </div>\n </button>\n </div>\n\n {/* Filter Pills */}\n <div className=\"flex flex-wrap gap-4 mb-6\" style={{ marginBottom: \"var(--spacing-3xl)\" }}>\n {defaultFilters.map((filter) => (\n <button\n key={filter.id}\n onClick={() => setActiveFilter(filter.id)}\n className=\"flex items-center gap-2 transition-colors\"\n style={{\n height: \"44px\",\n padding: \"0 var(--spacing-xl)\",\n borderRadius: \"var(--spacing-5xl)\",\n border: `1px solid ${activeFilter === filter.id ? \"var(--canvas-primary)\" : \"var(--canvas-border)\"}`,\n backgroundColor: activeFilter === filter.id ? \"var(--canvas-surface-brand)\" : \"var(--canvas-background)\",\n color: activeFilter === filter.id ? \"var(--canvas-primary)\" : \"var(--canvas-text-placeholder)\",\n fontFamily: \"var(--typo-global-font)\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 500,\n }}\n >\n {filter.icon}\n {filter.label}\n </button>\n ))}\n </div>\n\n {/* Cards Grid */}\n <div className=\"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-10\">\n {defaultPlaces.map((place) => (\n <div \n key={place.id}\n className=\"group cursor-pointer overflow-hidden\"\n style={{\n border: \"1px solid var(--canvas-border)\",\n borderRadius: \"var(--radius-xl)\",\n }}\n >\n <div \n className=\"w-full overflow-hidden\"\n style={{ \n height: \"240px\",\n borderBottom: \"1px solid var(--canvas-border)\",\n }}\n >\n <img \n src={place.image} \n alt={place.name}\n className=\"w-full h-full object-cover group-hover:scale-105 transition-transform duration-300\"\n />\n </div>\n <div \n className=\"flex flex-col\"\n style={{ \n padding: \"0 var(--spacing-3xl) var(--spacing-3xl)\",\n gap: \"var(--spacing-xs)\",\n }}\n >\n <div className=\"flex items-center justify-between mt-6\">\n <Typography variant=\"body-xl\" as=\"h3\" style={{ fontWeight: 600 }}>\n {place.name}\n </Typography>\n <div className=\"flex items-center gap-1\">\n <Star size={16} weight=\"fill\" style={{ color: \"var(--canvas-primary)\" }} />\n <Typography variant=\"body-s\" as=\"span\">\n {place.rating}\n </Typography>\n </div>\n </div>\n <Typography variant=\"body-m\" color=\"muted\">\n {place.count}\n </Typography>\n <Typography variant=\"h6\" as=\"p\" className=\"mt-5\">\n {place.price}\n </Typography>\n </div>\n </div>\n ))}\n </div>\n </div>\n </section>\n );\n}\n"
10
+ }
11
+ ],
12
+ "dependencies": [
13
+ "@phosphor-icons/react"
14
+ ],
15
+ "registryDependencies": []
16
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "features-comparison",
3
+ "type": "registry:block",
4
+ "description": "Feature comparison table across pricing tiers.",
5
+ "files": [
6
+ {
7
+ "path": "components/blocks/pricing/features-comparison.tsx",
8
+ "type": "registry:block",
9
+ "content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { Check, CaretUp, CaretDown, Info } from \"@phosphor-icons/react\";\nimport { Button } from \"../../ui/button\";\nimport { Typography } from \"../../ui/typography\";\n\ninterface FeatureRow {\n name: string;\n hasInfo?: boolean;\n values: (string | boolean)[];\n}\n\ninterface FeatureCategory {\n name: string;\n features: FeatureRow[];\n}\n\nconst plans = [\"Starter\", \"Deluxe\", \"Professional\"];\n\nconst categories: FeatureCategory[] = [\n {\n name: \"Collaboration\",\n features: [\n { name: \"File uploads\", hasInfo: true, values: [\"1 TB\", \"50 TB\", \"Unlimited\"] },\n { name: \"Team projects\", hasInfo: true, values: [true, true, true] },\n { name: \"Version history\", hasInfo: true, values: [\"-\", \"30 days\", \"Unlimited\"] },\n ],\n },\n {\n name: \"Productivity\",\n features: [\n { name: \"App integration\", hasInfo: true, values: [\"5\", \"20\", \"50\"] },\n { name: \"Customizable dashboard\", hasInfo: true, values: [\"-\", true, true] },\n { name: \"Audio & video\", hasInfo: true, values: [\"-\", true, true] },\n ],\n },\n {\n name: \"Advanced workflows\",\n features: [],\n },\n {\n name: \"Security & compliance\",\n features: [],\n },\n {\n name: \"Support\",\n features: [],\n },\n];\n\nexport function FeaturesComparison() {\n const [expandedCategories, setExpandedCategories] = useState<string[]>([\n \"Collaboration\",\n \"Productivity\",\n ]);\n\n const toggleCategory = (name: string) => {\n setExpandedCategories((prev) =>\n prev.includes(name)\n ? prev.filter((c) => c !== name)\n : [...prev, name]\n );\n };\n\n const renderValue = (value: string | boolean) => {\n if (value === true) {\n return (\n <Check\n size={18}\n weight=\"bold\"\n style={{ color: \"var(--canvas-primary-hover)\" }}\n />\n );\n }\n return (\n <Typography variant=\"body-m\" as=\"span\" color=\"muted\">\n {value}\n </Typography>\n );\n };\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\" style={{ fontWeight: 600 }}>\n FEATURES\n </Typography>\n <Typography variant=\"h3\" as=\"h2\">\n Powerful features\n </Typography>\n <Typography variant=\"body-l\" color=\"muted\">\n Compare the features for our plans\n </Typography>\n </div>\n\n {/* Comparison Table */}\n <div className=\"w-full flex flex-col\">\n {/* Plan Headers */}\n <div\n className=\"flex border-b\"\n style={{ borderColor: \"var(--canvas-surface)\" }}\n >\n <div className=\"w-[240px] shrink-0\" />\n <div className=\"flex-1 grid grid-cols-3 gap-4\">\n {plans.map((plan) => (\n <div\n key={plan}\n className=\"flex flex-col items-center gap-3 py-5 px-4\"\n >\n <Typography variant=\"body-xl\" as=\"span\" style={{ fontWeight: 600 }}>\n {plan}\n </Typography>\n <Button variant=\"primary\" size=\"default\" className=\"w-full\">\n Select plan\n </Button>\n </div>\n ))}\n </div>\n </div>\n\n {/* Categories */}\n {categories.map((category) => {\n const isExpanded = expandedCategories.includes(category.name);\n const hasFeatures = category.features.length > 0;\n\n return (\n <div key={category.name} className=\"flex flex-col\">\n {/* Category Header */}\n <button\n onClick={() => toggleCategory(category.name)}\n className=\"flex items-center justify-between w-full h-14 px-5 border-b\"\n style={{\n backgroundColor: \"var(--canvas-surface)\",\n borderColor: \"var(--canvas-border)\",\n }}\n >\n <Typography variant=\"body-xl\" as=\"span\" style={{ fontWeight: 600 }}>\n {category.name}\n </Typography>\n {isExpanded ? (\n <CaretUp size={24} style={{ color: \"var(--canvas-text-muted)\" }} />\n ) : (\n <CaretDown size={24} style={{ color: \"var(--canvas-text-muted)\" }} />\n )}\n </button>\n\n {/* Feature Rows */}\n {isExpanded && hasFeatures && (\n <div className=\"flex flex-col\">\n {category.features.map((feature) => (\n <div\n key={feature.name}\n className=\"flex border-b\"\n style={{ borderColor: \"var(--canvas-border)\" }}\n >\n {/* Feature Name */}\n <div className=\"w-[240px] shrink-0 flex items-center gap-2 h-14 pl-6\">\n <Typography variant=\"body-m\" as=\"span\">\n {feature.name}\n </Typography>\n {feature.hasInfo && (\n <Info\n size={16}\n className=\"shrink-0\"\n style={{ color: \"var(--canvas-text-placeholder)\" }}\n />\n )}\n </div>\n\n {/* Values */}\n <div className=\"flex-1 grid grid-cols-3 gap-4\">\n {feature.values.map((value, idx) => (\n <div\n key={idx}\n className=\"flex items-center justify-center h-14 px-4\"\n >\n {renderValue(value)}\n </div>\n ))}\n </div>\n </div>\n ))}\n </div>\n )}\n </div>\n );\n })}\n </div>\n </div>\n </section>\n );\n}\n"
10
+ }
11
+ ],
12
+ "dependencies": [
13
+ "@phosphor-icons/react"
14
+ ],
15
+ "registryDependencies": []
16
+ }
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "filter-popover",
3
+ "type": "registry:block",
4
+ "description": "Popover with filter controls (checkboxes, date ranges, etc.).",
5
+ "files": [
6
+ {
7
+ "path": "components/blocks/filter-popover.tsx",
8
+ "type": "registry:block",
9
+ "content": "\"use client\";\n\nimport { useState, useEffect } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport { Filter, ChevronDown } from \"lucide-react\";\nimport { Button } from \"../ui/button\";\nimport {\n Popover,\n PopoverContent,\n PopoverTrigger,\n} from \"../ui/popover\";\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from \"../ui/select\";\nimport { CheckboxWithLabel, Checkbox } from \"../ui/checkbox\";\nimport { RadioGroup, RadioGroupItem } from \"../ui/radio-group\";\nimport { Switch } from \"../ui/switch\";\nimport { TextInput } from \"../ui/text-input\";\nimport { Searchbox } from \"../ui/searchbox\";\nimport { DateInput } from \"../ui/date-input\";\nimport { MultiselectTags } from \"../ui/multiselect-tags\";\n\n// ============================================\n// Filter Option Types\n// ============================================\n\nexport interface FilterOption {\n id: string;\n label: string;\n}\n\nexport interface FilterDropdownConfig {\n id: string;\n label: string;\n placeholder: string;\n options: FilterOption[];\n}\n\nexport interface FilterCheckboxGroupConfig {\n id: string;\n label: string;\n options: FilterOption[];\n}\n\nexport interface FilterDateRangeConfig {\n id: string;\n label: string;\n startPlaceholder?: string;\n endPlaceholder?: string;\n}\n\nexport interface FilterState {\n dropdowns: Record<string, string>;\n checkboxes: Record<string, boolean>;\n dateRange: { start: string; end: string };\n}\n\n// ============================================\n// Default Filter Configuration\n// ============================================\n\nconst defaultDropdowns: FilterDropdownConfig[] = [\n {\n id: \"category\",\n label: \"Category\",\n placeholder: \"All categories\",\n options: [\n { id: \"all\", label: \"All categories\" },\n { id: \"restaurants\", label: \"Restaurants\" },\n { id: \"hotels\", label: \"Hotels\" },\n { id: \"attractions\", label: \"Attractions\" },\n ],\n },\n {\n id: \"location\",\n label: \"Location\",\n placeholder: \"All locations\",\n options: [\n { id: \"all\", label: \"All locations\" },\n { id: \"new-york\", label: \"New York\" },\n { id: \"los-angeles\", label: \"Los Angeles\" },\n { id: \"chicago\", label: \"Chicago\" },\n ],\n },\n];\n\nconst defaultCheckboxGroup: FilterCheckboxGroupConfig = {\n id: \"status\",\n label: \"Status\",\n options: [\n { id: \"active\", label: \"Active\" },\n { id: \"pending\", label: \"Pending\" },\n { id: \"completed\", label: \"Completed\" },\n ],\n};\n\nconst defaultDateRange: FilterDateRangeConfig = {\n id: \"dateRange\",\n label: \"Date Range\",\n startPlaceholder: \"Start date\",\n endPlaceholder: \"End date\",\n};\n\n// ============================================\n// Filter Dropdown Component\n// ============================================\n\ninterface FilterDropdownProps {\n config: FilterDropdownConfig;\n value: string;\n onChange: (value: string) => void;\n}\n\nfunction FilterDropdown({ config, value, onChange }: FilterDropdownProps) {\n return (\n <div className=\"space-y-2\">\n <label className=\"text-sm font-medium text-[var(--canvas-text-muted)]\">\n {config.label}\n </label>\n <Select value={value || undefined} onValueChange={onChange}>\n <SelectTrigger inputSize=\"sm\">\n <SelectValue placeholder={config.placeholder} />\n </SelectTrigger>\n <SelectContent position=\"popper\" side=\"bottom\" sideOffset={4}>\n {config.options.map((option) => (\n <SelectItem key={option.id} value={option.id}>\n {option.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n );\n}\n\n// ============================================\n// Filter Popover Component\n// ============================================\n\ninterface FilterPopoverProps {\n /** Dropdown filter configurations */\n dropdowns?: FilterDropdownConfig[];\n /** Checkbox group configuration */\n checkboxGroup?: FilterCheckboxGroupConfig;\n /** Date range configuration */\n dateRange?: FilterDateRangeConfig;\n /** Current filter state */\n filterState?: FilterState;\n /** Callback when filters are applied */\n onApply?: (state: FilterState) => void;\n /** Callback when filters are cleared */\n onClear?: () => void;\n /** Trigger style variant - \"button\" for button style, \"dropdown\" for select-like style */\n triggerVariant?: \"button\" | \"dropdown\";\n /** Placeholder text shown in the trigger (used for both variants) */\n triggerPlaceholder?: string;\n /** Additional class names for the trigger */\n className?: string;\n}\n\n/**\n * Canvas Design System - Filter Popover Component\n * \n * A filter button that opens a popover with various filter options.\n * Inspired by shadcnstudio Category Filter 6 structure.\n * \n * @example\n * ```tsx\n * // Button variant (default)\n * <FilterPopover\n * onApply={(filters) => console.log(filters)}\n * onClear={() => console.log(\"Cleared\")}\n * />\n * \n * // Dropdown variant\n * <FilterPopover\n * triggerVariant=\"dropdown\"\n * triggerPlaceholder=\"Filter\"\n * onApply={(filters) => console.log(filters)}\n * />\n * ```\n */\nexport function FilterPopover({\n dropdowns = defaultDropdowns,\n checkboxGroup = defaultCheckboxGroup,\n dateRange = defaultDateRange,\n filterState,\n onApply,\n onClear,\n triggerVariant = \"button\",\n triggerPlaceholder = \"Filter\",\n className,\n}: FilterPopoverProps) {\n const [isOpen, setIsOpen] = useState(false);\n const [mounted, setMounted] = useState(false);\n\n // Ensure hydration consistency for Radix components\n useEffect(() => {\n setMounted(true);\n }, []);\n\n // Internal state for filter values\n const [localState, setLocalState] = useState<FilterState>(() => ({\n dropdowns: filterState?.dropdowns || {},\n checkboxes: filterState?.checkboxes || {},\n dateRange: filterState?.dateRange || { start: \"\", end: \"\" },\n }));\n\n const handleDropdownChange = (id: string, value: string) => {\n setLocalState((prev) => ({\n ...prev,\n dropdowns: { ...prev.dropdowns, [id]: value },\n }));\n };\n\n const handleCheckboxChange = (id: string, checked: boolean) => {\n setLocalState((prev) => ({\n ...prev,\n checkboxes: { ...prev.checkboxes, [id]: checked },\n }));\n };\n\n const handleDateChange = (field: \"start\" | \"end\", value: string) => {\n setLocalState((prev) => ({\n ...prev,\n dateRange: { ...prev.dateRange, [field]: value },\n }));\n };\n\n const handleClear = () => {\n const clearedState: FilterState = {\n dropdowns: {},\n checkboxes: {},\n dateRange: { start: \"\", end: \"\" },\n };\n setLocalState(clearedState);\n onClear?.();\n };\n\n const handleApply = () => {\n onApply?.(localState);\n setIsOpen(false);\n };\n\n const handleCancel = () => {\n // Reset to initial state\n setLocalState({\n dropdowns: filterState?.dropdowns || {},\n checkboxes: filterState?.checkboxes || {},\n dateRange: filterState?.dateRange || { start: \"\", end: \"\" },\n });\n setIsOpen(false);\n };\n\n // Button trigger component\n const ButtonTrigger = (\n <Button\n variant=\"neutral\"\n className={cn(\"gap-2\", className)}\n style={{ \n borderRadius: \"var(--btn-standard-radius)\",\n height: \"var(--btn-standard-height)\",\n paddingLeft: \"var(--btn-standard-px)\",\n paddingRight: \"var(--btn-standard-px)\",\n fontSize: \"var(--btn-standard-font-size)\",\n }}\n >\n <Filter className=\"size-4\" />\n {triggerPlaceholder}\n </Button>\n );\n\n // Dropdown trigger component (styled like SelectTrigger)\n const DropdownTrigger = (\n <button\n className={cn(\n \"flex items-center justify-between gap-2 bg-white border text-[var(--canvas-text)] whitespace-nowrap transition-colors outline-none focus:border-[var(--canvas-border-input-focus)] focus:ring-2 focus:ring-[var(--canvas-border-input-focus)] focus:ring-offset-2 data-[state=open]:border-[var(--canvas-border-input-focus)]\",\n className\n )}\n style={{\n width: \"120px\",\n height: \"var(--input-small-height)\",\n paddingLeft: \"var(--input-small-px)\",\n paddingRight: \"var(--input-small-px)\",\n fontSize: \"var(--input-small-font-size)\",\n borderRadius: \"var(--input-small-radius)\",\n borderColor: \"var(--canvas-border-input)\",\n }}\n >\n <span className=\"text-[var(--canvas-text-placeholder)]\">{triggerPlaceholder}</span>\n <ChevronDown className=\"size-4 opacity-50\" />\n </button>\n );\n\n // Render placeholder during SSR to prevent hydration mismatch\n if (!mounted) {\n return triggerVariant === \"dropdown\" ? DropdownTrigger : ButtonTrigger;\n }\n\n return (\n <Popover open={isOpen} onOpenChange={setIsOpen}>\n <PopoverTrigger asChild>\n {triggerVariant === \"dropdown\" ? DropdownTrigger : ButtonTrigger}\n </PopoverTrigger>\n\n <PopoverContent\n align=\"end\"\n side=\"bottom\"\n sideOffset={4}\n avoidCollisions={false}\n className=\"w-80 p-0 bg-white border border-[var(--canvas-border)] shadow-lg\"\n >\n {/* Filter Content - All Input Types */}\n <div className=\"p-4 space-y-5 max-h-[480px] overflow-y-auto\">\n {/* Text Input */}\n <div className=\"space-y-2\">\n <label className=\"text-sm font-medium text-[var(--canvas-text-muted)]\">\n Text Input\n </label>\n <TextInput inputSize=\"sm\" placeholder=\"Enter text...\" />\n </div>\n\n {/* Searchbox */}\n <div className=\"space-y-2\">\n <label className=\"text-sm font-medium text-[var(--canvas-text-muted)]\">\n Searchbox\n </label>\n <Searchbox inputSize=\"sm\" placeholder=\"Search...\" />\n </div>\n\n {/* Dropdown */}\n {dropdowns.slice(0, 1).map((dropdown) => (\n <FilterDropdown\n key={dropdown.id}\n config={dropdown}\n value={localState.dropdowns[dropdown.id] || \"\"}\n onChange={(value) => handleDropdownChange(dropdown.id, value)}\n />\n ))}\n\n {/* Date Input */}\n <div className=\"space-y-2\">\n <label className=\"text-sm font-medium text-[var(--canvas-text-muted)]\">\n Date Input\n </label>\n <DateInput inputSize=\"sm\" />\n </div>\n\n\n {/* Radio Buttons */}\n <div className=\"space-y-2\">\n <label className=\"text-sm font-medium text-[var(--canvas-text-muted)]\">\n Radio Buttons\n </label>\n <RadioGroup defaultValue=\"option1\" className=\"flex\">\n <div className=\"flex items-center gap-2\">\n <RadioGroupItem value=\"option1\" id=\"radio1\" />\n <label \n htmlFor=\"radio1\" \n className=\"text-[var(--canvas-text)] cursor-pointer\"\n style={{ fontSize: \"var(--input-small-font-size)\" }}\n >\n Option 1\n </label>\n </div>\n <div className=\"flex items-center gap-2\">\n <RadioGroupItem value=\"option2\" id=\"radio2\" />\n <label \n htmlFor=\"radio2\" \n className=\"text-[var(--canvas-text)] cursor-pointer\"\n style={{ fontSize: \"var(--input-small-font-size)\" }}\n >\n Option 2\n </label>\n </div>\n </RadioGroup>\n </div>\n\n {/* Radio Buttons List */}\n <div className=\"space-y-2\">\n <label className=\"text-sm font-medium text-[var(--canvas-text-muted)]\">\n Radio Buttons List\n </label>\n <RadioGroup defaultValue=\"list-opt1\" className=\"flex flex-col gap-2\">\n <div className=\"flex items-center gap-2\">\n <RadioGroupItem value=\"list-opt1\" id=\"radio-list-1\" />\n <label \n htmlFor=\"radio-list-1\" \n className=\"text-[var(--canvas-text)] cursor-pointer\"\n style={{ fontSize: \"var(--input-small-font-size)\" }}\n >\n First option\n </label>\n </div>\n <div className=\"flex items-center gap-2\">\n <RadioGroupItem value=\"list-opt2\" id=\"radio-list-2\" />\n <label \n htmlFor=\"radio-list-2\" \n className=\"text-[var(--canvas-text)] cursor-pointer\"\n style={{ fontSize: \"var(--input-small-font-size)\" }}\n >\n Second option\n </label>\n </div>\n <div className=\"flex items-center gap-2\">\n <RadioGroupItem value=\"list-opt3\" id=\"radio-list-3\" />\n <label \n htmlFor=\"radio-list-3\" \n className=\"text-[var(--canvas-text)] cursor-pointer\"\n style={{ fontSize: \"var(--input-small-font-size)\" }}\n >\n Third option\n </label>\n </div>\n </RadioGroup>\n </div>\n\n {/* Checkbox */}\n <div className=\"space-y-2\">\n <label className=\"text-sm font-medium text-[var(--canvas-text-muted)]\">\n Checkbox\n </label>\n <div className=\"flex items-center gap-2\">\n <Checkbox id=\"single-checkbox\" />\n <label \n htmlFor=\"single-checkbox\" \n className=\"text-[var(--canvas-text)] cursor-pointer\"\n style={{ fontSize: \"var(--input-small-font-size)\" }}\n >\n I agree to terms\n </label>\n </div>\n </div>\n\n {/* Checkbox List */}\n {checkboxGroup && (\n <div className=\"space-y-2\">\n <label className=\"text-sm font-medium text-[var(--canvas-text-muted)]\">\n Checkbox List\n </label>\n <div className=\"space-y-2\">\n {checkboxGroup.options.map((option) => (\n <CheckboxWithLabel\n key={option.id}\n checked={localState.checkboxes[option.id] || false}\n onCheckedChange={(checked) =>\n handleCheckboxChange(option.id, checked === true)\n }\n >\n {option.label}\n </CheckboxWithLabel>\n ))}\n </div>\n </div>\n )}\n\n {/* Toggle */}\n <div className=\"space-y-2\">\n <label className=\"text-sm font-medium text-[var(--canvas-text-muted)]\">\n Toggle\n </label>\n <div className=\"flex items-center gap-2\">\n <Switch id=\"toggle-switch\" />\n <label \n htmlFor=\"toggle-switch\" \n className=\"text-[var(--canvas-text)] cursor-pointer\"\n style={{ fontSize: \"var(--input-small-font-size)\" }}\n >\n Enable notifications\n </label>\n </div>\n </div>\n\n {/* Multiselect Tags */}\n <div className=\"space-y-2\">\n <label className=\"text-sm font-medium text-[var(--canvas-text-muted)]\">\n Multiselect Tags\n </label>\n <MultiselectTags inputSize=\"sm\" tags={[\"Tag 1\", \"Tag 2\"]} placeholder=\"Add...\" />\n </div>\n </div>\n\n {/* Footer */}\n <div className=\"flex items-center justify-between px-4 py-3 border-t border-[var(--canvas-border)]\">\n <Button\n variant=\"ghost\"\n size=\"sm\"\n onClick={handleClear}\n className=\"text-[var(--canvas-text-muted)] hover:text-[var(--canvas-text)] hover:bg-transparent\"\n >\n Reset\n </Button>\n <Button variant=\"primary\" size=\"sm\" onClick={handleApply}>\n Apply\n </Button>\n </div>\n </PopoverContent>\n </Popover>\n );\n}\n\n"
10
+ }
11
+ ],
12
+ "dependencies": [
13
+ "lucide-react"
14
+ ],
15
+ "registryDependencies": [
16
+ "utils",
17
+ "button",
18
+ "popover",
19
+ "select",
20
+ "checkbox",
21
+ "radio-group",
22
+ "switch",
23
+ "text-input",
24
+ "searchbox",
25
+ "date-input",
26
+ "multiselect-tags"
27
+ ]
28
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "fixed-column-data-table",
3
+ "type": "registry:block",
4
+ "description": "Data table with fixed first column (Name with avatar) that stays visible during horizontal scroll. Ideal for invoice-style tables with columns: Amount, Status badge, Logo, Company, Date Sent.",
5
+ "files": [
6
+ {
7
+ "path": "components/blocks/fixed-column-data-table.tsx",
8
+ "type": "registry:block",
9
+ "content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport { Button } from \"../ui/button\";\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from \"../ui/select\";\nimport { Avatar, AvatarImage, AvatarFallback } from \"../ui/avatar\";\nimport { MenufocusTemplate } from \"./menufocus-template\";\n\n// ============================================\n// Types\n// ============================================\n\nexport type FixedColumnTableStatus = \"pending\" | \"paid\" | \"overdue\";\n\nexport interface FixedColumnTableRow {\n id: string;\n name: string;\n avatarUrl?: string;\n amount: string;\n status: FixedColumnTableStatus;\n logoUrl?: string;\n company: string;\n dateSent: string;\n}\n\nexport interface FixedColumnTableColumn {\n id: string;\n label: string;\n width?: 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 FixedColumnDataTableProps {\n /** Table title */\n title?: string;\n /** Number of results to display */\n resultCount?: number;\n /** Custom result count text (overrides default \"{count} results\") */\n resultCountText?: string;\n /** Table data rows */\n data?: FixedColumnTableRow[];\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 add/action button is clicked */\n onAddNew?: () => void;\n /** Callback when sort value changes */\n onSort?: (value: string) => void;\n /** Callback when filter value changes */\n onFilter?: (value: string) => void;\n /** Callback when row action is clicked */\n onRowAction?: (action: string, row: FixedColumnTableRow) => void;\n /** Additional class names */\n className?: string;\n}\n\n// ============================================\n// Default Data\n// ============================================\n\nconst defaultData: FixedColumnTableRow[] = [\n {\n id: \"1\",\n name: \"Jeff Connor\",\n avatarUrl: \"https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=150&h=150&fit=crop&crop=face\",\n amount: \"$3,200\",\n status: \"pending\",\n logoUrl: \"\",\n company: \"Airdev\",\n dateSent: \"5/23/2024\",\n },\n {\n id: \"2\",\n name: \"Aya Williams\",\n avatarUrl: \"https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=150&h=150&fit=crop&crop=face\",\n amount: \"$2,400\",\n status: \"paid\",\n logoUrl: \"\",\n company: \"Airdev\",\n dateSent: \"2/19/2024\",\n },\n];\n\nconst defaultSortOptions: SortOption[] = [\n { id: \"name-asc\", label: \"Name (A-Z)\" },\n { id: \"name-desc\", label: \"Name (Z-A)\" },\n { id: \"date-newest\", label: \"Newest first\" },\n { id: \"date-oldest\", label: \"Oldest first\" },\n { id: \"amount-high\", label: \"Amount (High)\" },\n { id: \"amount-low\", label: \"Amount (Low)\" },\n];\n\nconst defaultFilterOptions: FilterOption[] = [\n { id: \"all\", label: \"All statuses\" },\n { id: \"pending\", label: \"Pending\" },\n { id: \"paid\", label: \"Paid\" },\n { id: \"overdue\", label: \"Overdue\" },\n];\n\n// ============================================\n// Sub-components\n// ============================================\n\ninterface TableHeaderCellProps {\n children: React.ReactNode;\n className?: string;\n isFixed?: boolean;\n}\n\nfunction TableHeaderCell({ children, className, isFixed }: TableHeaderCellProps) {\n return (\n <th\n className={cn(\n \"text-left h-8\",\n isFixed && \"sticky left-0 z-10\",\n className\n )}\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 600,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text)\",\n backgroundColor: isFixed ? \"var(--canvas-background)\" : undefined,\n }}\n >\n {children}\n </th>\n );\n}\n\ninterface TableCellProps {\n children: React.ReactNode;\n className?: string;\n isFixed?: boolean;\n}\n\nfunction TableCell({ children, className, isFixed }: TableCellProps) {\n return (\n <td\n className={cn(\n \"h-12\",\n isFixed && \"sticky left-0 z-10\",\n className\n )}\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: \"var(--typo-body-s-weight)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text)\",\n backgroundColor: isFixed ? \"var(--canvas-background)\" : undefined,\n paddingTop: \"var(--spacing-md)\",\n paddingBottom: \"var(--spacing-md)\",\n }}\n >\n {children}\n </td>\n );\n}\n\ninterface StatusBadgeProps {\n status: FixedColumnTableStatus;\n}\n\nfunction StatusBadge({ status }: StatusBadgeProps) {\n const statusConfig: Record<FixedColumnTableStatus, { label: string; bgColor: string; textColor: string }> = {\n pending: {\n label: \"Pending\",\n bgColor: \"var(--canvas-surface-brand)\",\n textColor: \"var(--canvas-primary)\",\n },\n paid: {\n label: \"Paid\",\n bgColor: \"var(--canvas-success-surface, #edfdf8)\",\n textColor: \"var(--canvas-success)\",\n },\n overdue: {\n label: \"Overdue\",\n bgColor: \"var(--canvas-destructive-surface, #fef2f2)\",\n textColor: \"var(--canvas-destructive)\",\n },\n };\n\n const config = statusConfig[status];\n\n return (\n <span\n className=\"inline-flex items-center justify-center whitespace-nowrap\"\n style={{\n backgroundColor: config.bgColor,\n color: config.textColor,\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 padding: \"var(--spacing-md) var(--spacing-xl)\",\n borderRadius: \"var(--spacing-3xl)\",\n height: \"32px\",\n }}\n >\n {config.label}\n </span>\n );\n}\n\ninterface CompanyLogoProps {\n logoUrl?: string;\n company: string;\n}\n\nfunction CompanyLogo({ logoUrl, company }: CompanyLogoProps) {\n // Default favicon-style logo if no URL provided\n if (!logoUrl) {\n return (\n <div\n className=\"flex items-center justify-center\"\n style={{\n width: \"32px\",\n height: \"32px\",\n backgroundColor: \"var(--canvas-primary)\",\n borderRadius: \"var(--radius-xs)\",\n }}\n >\n <svg\n width=\"20\"\n height=\"20\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"var(--canvas-primary-foreground)\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n >\n <path d=\"M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z\" />\n <polyline points=\"9,22 9,12 15,12 15,22\" />\n </svg>\n </div>\n );\n }\n\n return (\n <img\n src={logoUrl}\n alt={`${company} logo`}\n className=\"object-contain\"\n style={{\n width: \"32px\",\n height: \"32px\",\n borderRadius: \"var(--radius-xs)\",\n }}\n />\n );\n}\n\n// ============================================\n// Main Component\n// ============================================\n\n/**\n * Canvas Design System - Fixed Column Data Table Block\n * \n * A data table with a fixed first column (Name with avatar) that stays\n * visible during horizontal scrolling. Ideal for invoice-style tables\n * with many columns that need horizontal scrolling on smaller screens.\n * \n * @example\n * ```tsx\n * <FixedColumnDataTable\n * title=\"Invoices\"\n * data={[\n * { id: \"1\", name: \"John\", amount: \"$1,000\", status: \"paid\", company: \"Acme\", dateSent: \"1/1/2024\" }\n * ]}\n * onAddNew={() => console.log(\"Add new\")}\n * />\n * ```\n */\nexport function FixedColumnDataTable({\n title = \"Invoices\",\n resultCount,\n resultCountText,\n data = defaultData,\n sortOptions = defaultSortOptions,\n filterOptions = defaultFilterOptions,\n actionButtonText = \"Add new\",\n onAddNew,\n onSort,\n onFilter,\n onRowAction,\n className,\n}: FixedColumnDataTableProps) {\n const [sortValue, setSortValue] = useState<string>(\"\");\n const [filterValue, setFilterValue] = useState<string>(\"\");\n\n const displayResultCount = resultCount ?? data.length;\n const displayResultText = resultCountText ?? `${displayResultCount} results`;\n\n const handleSortChange = (value: string) => {\n setSortValue(value);\n onSort?.(value);\n };\n\n const handleFilterChange = (value: string) => {\n setFilterValue(value);\n onFilter?.(value);\n };\n\n return (\n <div \n className={cn(\"flex flex-col w-full\", className)}\n style={{ gap: \"var(--spacing-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 Count */}\n <div \n className=\"flex flex-col flex-1 min-w-[200px]\"\n style={{ gap: \"var(--spacing-xs)\" }}\n >\n <h2\n style={{\n fontFamily: \"var(--typo-h6-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-h6-size)\",\n fontWeight: \"var(--typo-h6-weight)\",\n letterSpacing: \"var(--typo-h6-spacing)\",\n lineHeight: \"var(--typo-h6-line-height)\",\n color: \"var(--canvas-text)\",\n margin: 0,\n }}\n >\n {title}\n </h2>\n <p\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: \"var(--typo-body-s-weight)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text-muted)\",\n margin: 0,\n }}\n >\n {displayResultText}\n </p>\n </div>\n\n {/* Controls */}\n <div \n className=\"flex items-start justify-end shrink-0 gap-3\"\n >\n {/* Sort Dropdown */}\n <div className=\"w-[120px]\">\n <Select value={sortValue || undefined} onValueChange={handleSortChange}>\n <SelectTrigger inputSize=\"sm\">\n <SelectValue placeholder=\"Sort\" />\n </SelectTrigger>\n <SelectContent>\n {sortOptions.map((option) => (\n <SelectItem key={option.id} value={option.id}>\n {option.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n\n {/* Filter Dropdown */}\n <div className=\"w-[120px]\">\n <Select value={filterValue || undefined} onValueChange={handleFilterChange}>\n <SelectTrigger inputSize=\"sm\">\n <SelectValue placeholder=\"Filter\" />\n </SelectTrigger>\n <SelectContent>\n {filterOptions.map((option) => (\n <SelectItem key={option.id} value={option.id}>\n {option.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n\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 {/* Table Section with horizontal scroll */}\n <div className=\"w-full overflow-x-auto\">\n <table className=\"w-full min-w-max border-collapse\">\n <thead>\n <tr style={{ borderBottom: \"1px solid var(--canvas-border)\" }}>\n {/* Fixed Name Column Header */}\n <TableHeaderCell isFixed className=\"pr-8 min-w-[200px]\">\n Name\n </TableHeaderCell>\n {/* Scrollable Column Headers */}\n <TableHeaderCell className=\"px-6 min-w-[160px]\">\n Amount\n </TableHeaderCell>\n <TableHeaderCell className=\"px-6 min-w-[160px]\">\n Status\n </TableHeaderCell>\n <TableHeaderCell className=\"px-6 min-w-[120px]\">\n Logo\n </TableHeaderCell>\n <TableHeaderCell className=\"px-6 min-w-[160px]\">\n Company\n </TableHeaderCell>\n <TableHeaderCell className=\"px-6 min-w-[160px]\">\n Date Sent\n </TableHeaderCell>\n <TableHeaderCell className=\"w-12 px-4\">\n &nbsp;\n </TableHeaderCell>\n </tr>\n </thead>\n <tbody>\n {data.map((row) => (\n <tr\n key={row.id}\n style={{ borderBottom: \"1px solid var(--canvas-border)\" }}\n >\n {/* Fixed Name Column */}\n <TableCell isFixed className=\"pr-8\">\n <div className=\"flex items-center gap-2\">\n <Avatar className=\"size-8 border border-[var(--canvas-border)]\">\n <AvatarImage src={row.avatarUrl} alt={row.name} />\n <AvatarFallback>\n {row.name.split(\" \").map(n => n[0]).join(\"\").slice(0, 2)}\n </AvatarFallback>\n </Avatar>\n <span className=\"whitespace-nowrap\">{row.name}</span>\n </div>\n </TableCell>\n {/* Scrollable Columns */}\n <TableCell className=\"px-6\">\n <span className=\"whitespace-nowrap\">{row.amount}</span>\n </TableCell>\n <TableCell className=\"px-6\">\n <StatusBadge status={row.status} />\n </TableCell>\n <TableCell className=\"px-6\">\n <CompanyLogo logoUrl={row.logoUrl} company={row.company} />\n </TableCell>\n <TableCell className=\"px-6\">\n <span className=\"whitespace-nowrap\">{row.company}</span>\n </TableCell>\n <TableCell className=\"px-6\">\n <span className=\"whitespace-nowrap\">{row.dateSent}</span>\n </TableCell>\n <TableCell className=\"px-4\">\n <MenufocusTemplate\n ariaLabel=\"Row actions\"\n items={[\n { id: \"edit\", label: \"Edit\", onClick: () => onRowAction?.(\"edit\", row) },\n { id: \"view\", label: \"View details\", onClick: () => onRowAction?.(\"view\", row) },\n { id: \"download\", label: \"Download\", onClick: () => onRowAction?.(\"download\", row) },\n { id: \"delete\", label: \"Delete\", variant: \"destructive\", onClick: () => onRowAction?.(\"delete\", row) },\n ]}\n />\n </TableCell>\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n </div>\n );\n}\n"
10
+ }
11
+ ],
12
+ "dependencies": [],
13
+ "registryDependencies": [
14
+ "utils",
15
+ "button",
16
+ "select",
17
+ "avatar",
18
+ "menufocus-template"
19
+ ]
20
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "flair-banner",
3
+ "type": "registry:block",
4
+ "description": "Dark blue hero banner with large title text. Used at top of pages.",
5
+ "files": [
6
+ {
7
+ "path": "components/blocks/flair-banner.tsx",
8
+ "type": "registry:block",
9
+ "content": "\"use client\";\n\nimport { cn } from \"../../lib/utils\";\n\ninterface FlairBannerProps {\n /** Large title text */\n title?: string;\n /** Additional class names */\n className?: string;\n /** Optional background image URL */\n backgroundImage?: string;\n /** Opacity of the flair color overlay when using background image (0-1, default 0.7) */\n overlayOpacity?: number;\n}\n\n/**\n * Canvas Design System - Flair Banner Component\n * \n * A hero banner with centered title. Supports solid flair color background\n * or an image background with a semi-transparent flair color overlay.\n * Typically used at the top of standard pages below the header.\n * \n * @example\n * ```tsx\n * // Solid color background (default)\n * <FlairBanner title=\"Large title\" />\n * \n * // Image background with flair overlay\n * <FlairBanner \n * title=\"Large title\" \n * backgroundImage=\"/brand-assets/bg.jpg\"\n * overlayOpacity={0.6}\n * />\n * ```\n */\nexport function FlairBanner({\n title = \"Large title\",\n className,\n backgroundImage,\n overlayOpacity = 0.7,\n}: FlairBannerProps) {\n const hasImage = Boolean(backgroundImage);\n\n return (\n <div\n className={cn(\n \"relative flex flex-col items-center justify-center w-full overflow-hidden\",\n \"h-[var(--flair-banner-height)]\",\n !hasImage && \"bg-[var(--canvas-flair-bg)]\",\n className\n )}\n >\n {/* Background image layer */}\n {hasImage && (\n <div\n className=\"absolute inset-0 bg-cover bg-center bg-no-repeat\"\n style={{\n backgroundImage: `url(${backgroundImage})`,\n }}\n />\n )}\n \n {/* Flair color overlay */}\n {hasImage && (\n <div\n className=\"absolute inset-0 bg-[var(--canvas-flair-bg)]\"\n style={{\n opacity: overlayOpacity,\n }}\n />\n )}\n\n {/* Content */}\n <div className=\"relative z-10 flex flex-col items-center justify-center text-center text-white px-[var(--spacing-xl)]\">\n {/* Title - Uses typography variables */}\n <h1 \n style={{\n fontFamily: \"var(--typo-banner-title-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-banner-title-size)\",\n fontWeight: \"var(--typo-banner-title-weight)\",\n letterSpacing: \"var(--typo-banner-title-spacing)\",\n lineHeight: \"var(--typo-banner-title-line-height)\",\n }}\n >\n {title}\n </h1>\n </div>\n </div>\n );\n}\n\n"
10
+ }
11
+ ],
12
+ "dependencies": [],
13
+ "registryDependencies": [
14
+ "utils"
15
+ ]
16
+ }
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "footer-navbar",
3
+ "type": "registry:block",
4
+ "description": "Footer with logo, nav links, and social icons.",
5
+ "files": [
6
+ {
7
+ "path": "components/blocks/marketing/footer-navbar.tsx",
8
+ "type": "registry:block",
9
+ "content": "\"use client\";\n\nimport Link from \"next/link\";\nimport { FacebookLogo, TwitterLogo, LinkedinLogo, InstagramLogo } from \"@phosphor-icons/react\";\nimport { Typography } from \"../../ui/typography\";\n\ninterface FooterNavbarProps {\n companyName?: string;\n}\n\nexport function FooterNavbar({ companyName = \"Sample app\" }: FooterNavbarProps) {\n return (\n <footer \n className=\"w-full px-4 md:px-8 lg:px-10 py-8 md:py-12\"\n style={{\n backgroundColor: \"var(--canvas-background)\",\n }}\n >\n <div className=\"w-full max-w-[1240px] mx-auto flex flex-col md:flex-row items-start md:items-center justify-between gap-6\">\n {/* Left Side - Links */}\n <div className=\"flex flex-col\" style={{ gap: \"var(--spacing-md)\" }}>\n <div \n className=\"flex items-center flex-wrap\"\n style={{ gap: \"var(--spacing-4xl)\" }}\n >\n <Link href=\"/\" className=\"hover:opacity-80 transition-opacity\">\n <Typography variant=\"body-m\" as=\"span\" color=\"muted\" style={{ fontWeight: 600 }}>\n Home\n </Typography>\n </Link>\n <Link href=\"#\" className=\"hover:opacity-80 transition-opacity\">\n <Typography variant=\"body-m\" as=\"span\" color=\"muted\" style={{ fontWeight: 600 }}>\n About us\n </Typography>\n </Link>\n </div>\n <div \n className=\"flex items-center flex-wrap\"\n style={{ gap: \"var(--spacing-3xl)\" }}\n >\n <Typography variant=\"body-m\" as=\"span\" color=\"muted\">\n © {companyName}\n </Typography>\n <span style={{ color: \"var(--canvas-text-placeholder)\" }}>|</span>\n <Link href=\"#\" className=\"hover:opacity-80 transition-opacity\">\n <Typography variant=\"body-m\" as=\"span\" color=\"muted\">\n Privacy Policy\n </Typography>\n </Link>\n <span style={{ color: \"var(--canvas-text-placeholder)\" }}>|</span>\n <Link href=\"#\" className=\"hover:opacity-80 transition-opacity\">\n <Typography variant=\"body-m\" as=\"span\" color=\"muted\">\n Terms of use\n </Typography>\n </Link>\n </div>\n </div>\n\n {/* Right Side - Social Icons */}\n <div \n className=\"flex items-center\"\n style={{ gap: \"var(--spacing-4xl)\" }}\n >\n <Link \n href=\"#\" \n className=\"hover:opacity-80 transition-opacity\"\n style={{ color: \"var(--canvas-text-placeholder)\" }}\n >\n <FacebookLogo size={24} />\n </Link>\n <Link \n href=\"#\" \n className=\"hover:opacity-80 transition-opacity\"\n style={{ color: \"var(--canvas-text-placeholder)\" }}\n >\n <TwitterLogo size={24} />\n </Link>\n <Link \n href=\"#\" \n className=\"hover:opacity-80 transition-opacity\"\n style={{ color: \"var(--canvas-text-placeholder)\" }}\n >\n <LinkedinLogo size={24} />\n </Link>\n <Link \n href=\"#\" \n className=\"hover:opacity-80 transition-opacity\"\n style={{ color: \"var(--canvas-text-placeholder)\" }}\n >\n <InstagramLogo size={24} />\n </Link>\n </div>\n </div>\n </footer>\n );\n}\n"
10
+ }
11
+ ],
12
+ "dependencies": [
13
+ "next",
14
+ "@phosphor-icons/react"
15
+ ],
16
+ "registryDependencies": []
17
+ }