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.
- package/dist/cli/index.js +516 -0
- package/dist/index.d.ts +67 -3
- package/dist/index.js +2588 -301
- package/dist/index.js.map +1 -1
- package/mcp/dist/index.js +5 -1
- package/package.json +18 -2
- package/registry/blocks/activity-feed.json +19 -0
- package/registry/blocks/blog-cards.json +16 -0
- package/registry/blocks/bottom-input-chat-widget.json +19 -0
- package/registry/blocks/canvas-item.json +18 -0
- package/registry/blocks/category-grid.json +16 -0
- package/registry/blocks/centered-hero.json +14 -0
- package/registry/blocks/chat-message.json +18 -0
- package/registry/blocks/circular-progress-bar-list.json +18 -0
- package/registry/blocks/component-palette.json +21 -0
- package/registry/blocks/component-search.json +19 -0
- package/registry/blocks/content-dropzone.json +16 -0
- package/registry/blocks/content-with-image.json +14 -0
- package/registry/blocks/core-values-grid.json +16 -0
- package/registry/blocks/credit-card-display.json +16 -0
- package/registry/blocks/cta-banner.json +14 -0
- package/registry/blocks/custom-component-helper.json +19 -0
- package/registry/blocks/destination-cards.json +16 -0
- package/registry/blocks/empty-state.json +16 -0
- package/registry/blocks/faq-accordion.json +16 -0
- package/registry/blocks/faqs-table.json +18 -0
- package/registry/blocks/feature-with-image.json +16 -0
- package/registry/blocks/featured-news-cards.json +16 -0
- package/registry/blocks/featured-places.json +16 -0
- package/registry/blocks/features-comparison.json +16 -0
- package/registry/blocks/filter-popover.json +28 -0
- package/registry/blocks/fixed-column-data-table.json +20 -0
- package/registry/blocks/flair-banner.json +16 -0
- package/registry/blocks/footer-navbar.json +17 -0
- package/registry/blocks/form-group.json +29 -0
- package/registry/blocks/gallery-section.json +14 -0
- package/registry/blocks/gradient-banner.json +16 -0
- package/registry/blocks/graph-metric-tiles.json +20 -0
- package/registry/blocks/grid-tiles-list.json +20 -0
- package/registry/blocks/hero-dark-centered.json +16 -0
- package/registry/blocks/hero-dark-with-image.json +16 -0
- package/registry/blocks/hero-fullwidth-image.json +16 -0
- package/registry/blocks/hero-section.json +16 -0
- package/registry/blocks/how-it-works.json +16 -0
- package/registry/blocks/image-feed-with-nested-comments.json +20 -0
- package/registry/blocks/infinity-canvas.json +58 -0
- package/registry/blocks/large-image-labels-list.json +19 -0
- package/registry/blocks/loader.json +19 -0
- package/registry/blocks/login-branding-panel.json +16 -0
- package/registry/blocks/menu-section.json +18 -0
- package/registry/blocks/menufocus-template.json +19 -0
- package/registry/blocks/messenger-sidebar.json +19 -0
- package/registry/blocks/metrics-section.json +14 -0
- package/registry/blocks/mobile-bottom-nav.json +18 -0
- package/registry/blocks/monthly-calendar-widget.json +20 -0
- package/registry/blocks/nested-comments-table.json +21 -0
- package/registry/blocks/nested-data-table.json +22 -0
- package/registry/blocks/office-locations.json +14 -0
- package/registry/blocks/page-header-section.json +17 -0
- package/registry/blocks/page-previews.json +29 -0
- package/registry/blocks/pagination.json +20 -0
- package/registry/blocks/participant-list.json +17 -0
- package/registry/blocks/persona-card.json +18 -0
- package/registry/blocks/pill-tabs.json +19 -0
- package/registry/blocks/pricing-cards.json +16 -0
- package/registry/blocks/pricing-cta.json +14 -0
- package/registry/blocks/profile-card.json +20 -0
- package/registry/blocks/profile-grid-tiles-list.json +21 -0
- package/registry/blocks/profile-image-uploader.json +19 -0
- package/registry/blocks/profile-info-cards.json +19 -0
- package/registry/blocks/progress-bar.json +16 -0
- package/registry/blocks/prompt-template.json +18 -0
- package/registry/blocks/reviews-grid.json +14 -0
- package/registry/blocks/reviews-table.json +19 -0
- package/registry/blocks/screen-flowchart.json +19 -0
- package/registry/blocks/screen-prompt-builder.json +19 -0
- package/registry/blocks/screen-prompt-template.json +18 -0
- package/registry/blocks/search-bar.json +19 -0
- package/registry/blocks/search-sidebar.json +25 -0
- package/registry/blocks/settings-list-row.json +20 -0
- package/registry/blocks/sidebar-cards.json +18 -0
- package/registry/blocks/sidebar-profile-card.json +21 -0
- package/registry/blocks/slideshow-grid-tiles.json +21 -0
- package/registry/blocks/social-feed.json +20 -0
- package/registry/blocks/social-proof.json +14 -0
- package/registry/blocks/standard-data-table.json +20 -0
- package/registry/blocks/standard-list-with-image.json +17 -0
- package/registry/blocks/step-tracker.json +16 -0
- package/registry/blocks/team-cards-grid.json +16 -0
- package/registry/blocks/team-circular-grid.json +16 -0
- package/registry/blocks/testimonial-carousel.json +16 -0
- package/registry/blocks/upvoting-posts-table.json +22 -0
- package/registry/blocks/vertical-how-it-works.json +16 -0
- package/registry/blocks/vertical-step-tracker.json +17 -0
- package/registry/blocks/video-chat-controls.json +18 -0
- package/registry/blocks/video-content-section.json +16 -0
- package/registry/blocks/video-playlist.json +18 -0
- package/registry/blocks/webcam-preview.json +18 -0
- package/registry/blocks/youtube-player.json +16 -0
- package/registry/hooks/use-css-variable-sync.json +14 -0
- package/registry/hooks/use-mobile.json +14 -0
- package/registry/index.json +730 -0
- package/registry/layout/account-settings-shell.json +20 -0
- package/registry/layout/dashboard-shell.json +23 -0
- package/registry/layout/double-sidebar-shell.json +23 -0
- package/registry/layout/double-sidebar.json +20 -0
- package/registry/layout/header.json +22 -0
- package/registry/layout/icon-sidebar-shell.json +23 -0
- package/registry/layout/icon-sidebar.json +19 -0
- package/registry/layout/mobile-menu-shell.json +19 -0
- package/registry/layout/multistep-progressbar-shell.json +23 -0
- package/registry/layout/multistep-shell.json +21 -0
- package/registry/layout/multistep-sidebar-shell.json +22 -0
- package/registry/layout/project-context-shell.json +20 -0
- package/registry/layout/search-bar-shell.json +22 -0
- package/registry/layout/sidebar-nav.json +18 -0
- package/registry/layout/sidebar.json +20 -0
- package/registry/layout/standard-page-shell.json +21 -0
- package/registry/layout/vertical-multistep-shell.json +23 -0
- package/registry/lib/utils.json +17 -0
- package/registry/ui/avatar.json +18 -0
- package/registry/ui/button.json +19 -0
- package/registry/ui/calendar.json +20 -0
- package/registry/ui/checkbox.json +19 -0
- package/registry/ui/date-input.json +18 -0
- package/registry/ui/dialog.json +19 -0
- package/registry/ui/dropdown-menu.json +19 -0
- package/registry/ui/file-uploader.json +18 -0
- package/registry/ui/image-uploader.json +18 -0
- package/registry/ui/input.json +16 -0
- package/registry/ui/label.json +18 -0
- package/registry/ui/line-tabs.json +16 -0
- package/registry/ui/multiselect-checkbox-field.json +18 -0
- package/registry/ui/multiselect-tags.json +18 -0
- package/registry/ui/popover.json +18 -0
- package/registry/ui/radio-group.json +19 -0
- package/registry/ui/range-input.json +17 -0
- package/registry/ui/scroll-area.json +18 -0
- package/registry/ui/searchbox.json +18 -0
- package/registry/ui/select.json +20 -0
- package/registry/ui/selectable-pills.json +16 -0
- package/registry/ui/separator.json +18 -0
- package/registry/ui/sheet.json +19 -0
- package/registry/ui/sidebar.json +27 -0
- package/registry/ui/skeleton.json +16 -0
- package/registry/ui/slider.json +18 -0
- package/registry/ui/switch.json +18 -0
- package/registry/ui/tabs.json +18 -0
- package/registry/ui/text-input.json +16 -0
- package/registry/ui/textarea.json +18 -0
- package/registry/ui/tooltip.json +18 -0
- package/registry/ui/typography.json +16 -0
- package/styles/tokens.reference.css +35 -3
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "prompt-template",
|
|
3
|
+
"type": "registry:block",
|
|
4
|
+
"description": "",
|
|
5
|
+
"files": [
|
|
6
|
+
{
|
|
7
|
+
"path": "components/blocks/prompt-template.tsx",
|
|
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 text-sm font-medium text-[var(--canvas-text-muted)]\">\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 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 {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 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-surface)] text-[var(--canvas-text-muted)] hover:bg-[var(--canvas-surface-hover)] hover:text-[var(--canvas-text)] border border-[var(--canvas-border)]\"\n )}\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-xs text-[var(--canvas-text-muted)] mb-2 font-medium\">\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
|
+
}
|
|
11
|
+
],
|
|
12
|
+
"dependencies": [
|
|
13
|
+
"lucide-react"
|
|
14
|
+
],
|
|
15
|
+
"registryDependencies": [
|
|
16
|
+
"utils"
|
|
17
|
+
]
|
|
18
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "reviews-grid",
|
|
3
|
+
"type": "registry:block",
|
|
4
|
+
"description": "Grid of review cards with ratings and text.",
|
|
5
|
+
"files": [
|
|
6
|
+
{
|
|
7
|
+
"path": "components/blocks/marketing/reviews-grid.tsx",
|
|
8
|
+
"type": "registry:block",
|
|
9
|
+
"content": "\"use client\";\n\nimport { Typography } from \"../../ui/typography\";\n\ninterface Review {\n id: string;\n quote: string;\n author: string;\n location: string;\n avatar: string;\n}\n\nconst defaultReviews: Review[] = [\n {\n id: \"1\",\n quote: '\"The deals you get here are so much better!\"',\n author: \"Emma Pérez\",\n location: \"Mexico\",\n avatar: \"https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=100&h=100&fit=crop\",\n },\n {\n id: \"2\",\n quote: '\"I was able to find the perfect place that fit my budget and needs. Highly recommend!\"',\n author: \"Francis Gaddi\",\n location: \"United Kingdom\",\n avatar: \"https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=100&h=100&fit=crop\",\n },\n {\n id: \"3\",\n quote: '\"This is now my go-to platform for booking vacation accommodations.\"',\n author: \"Raj Mishra\",\n location: \"Los Angeles, CA\",\n avatar: \"https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=100&h=100&fit=crop\",\n },\n {\n id: \"4\",\n quote: '\"The experience of booking an accommodation on this platform was a breeze. I would definitely use it again in the future.\"',\n author: \"Stacy Jones\",\n location: \"France\",\n avatar: \"https://images.unsplash.com/photo-1544005313-94ddf0286df2?w=100&h=100&fit=crop\",\n },\n {\n id: \"5\",\n quote: '\"You get free travel insurance!\"',\n author: \"Aya Williams\",\n location: \"Germany\",\n avatar: \"https://images.unsplash.com/photo-1580489944761-15a19d654956?w=100&h=100&fit=crop\",\n },\n {\n id: \"6\",\n quote: '\"I had a great experience using this platform. The process was smooth from start to finish, and I was able to quickly find a place that met my criteria.\"',\n author: \"Mary Trott\",\n location: \"Chicago, IL\",\n avatar: \"https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=100&h=100&fit=crop\",\n },\n];\n\ninterface ReviewsGridProps {\n title?: string;\n subtitle?: string;\n reviews?: Review[];\n}\n\nexport function ReviewsGrid({ \n title = \"Loved by people worldwide\",\n subtitle = \"TESTIMONIALS\",\n reviews = defaultReviews \n}: ReviewsGridProps) {\n // Split reviews into 3 columns\n const columns = [\n reviews.slice(0, 2),\n reviews.slice(2, 4),\n reviews.slice(4, 6),\n ];\n\n return (\n <section \n className=\"w-full px-4 md:px-8 lg:px-10 py-10 md:py-16\"\n style={{\n backgroundColor: \"var(--canvas-background)\",\n }}\n >\n <div className=\"w-full max-w-[1240px] mx-auto\">\n {/* Header */}\n <div style={{ marginBottom: \"var(--spacing-7xl)\" }}>\n <Typography variant=\"body-s\" as=\"p\" color=\"muted\" style={{ marginBottom: \"var(--spacing-lg)\" }}>\n {subtitle}\n </Typography>\n <Typography variant=\"h3\" as=\"h2\">\n {title}\n </Typography>\n </div>\n\n {/* Reviews Grid - 3 columns */}\n <div className=\"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-10\">\n {columns.map((column, colIndex) => (\n <div key={colIndex} className=\"flex flex-col gap-8\">\n {column.map((review) => (\n <div \n key={review.id}\n className=\"flex flex-col\"\n style={{\n padding: \"var(--spacing-4xl)\",\n border: \"1px solid var(--canvas-border)\",\n borderRadius: \"var(--spacing-md)\",\n gap: \"var(--spacing-2xl)\",\n boxShadow: \"0px 1px 8px 0px rgba(0, 0, 0, 0.03)\",\n }}\n >\n <Typography variant=\"body-l\" color=\"muted\">\n {review.quote}\n </Typography>\n <div className=\"flex items-center\" style={{ gap: \"var(--spacing-xl)\" }}>\n <div \n className=\"shrink-0 overflow-hidden\"\n style={{\n width: \"48px\",\n height: \"48px\",\n borderRadius: \"var(--radius-full)\",\n }}\n >\n <img \n src={review.avatar} \n alt={review.author}\n className=\"w-full h-full object-cover\"\n />\n </div>\n <div>\n <Typography variant=\"body-m\" as=\"p\" style={{ fontWeight: 700 }}>\n {review.author}\n </Typography>\n <Typography variant=\"body-m\" color=\"muted\">\n {review.location}\n </Typography>\n </div>\n </div>\n </div>\n ))}\n </div>\n ))}\n </div>\n </div>\n </section>\n );\n}\n"
|
|
10
|
+
}
|
|
11
|
+
],
|
|
12
|
+
"dependencies": [],
|
|
13
|
+
"registryDependencies": []
|
|
14
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "reviews-table",
|
|
3
|
+
"type": "registry:block",
|
|
4
|
+
"description": "Review listing with user avatars, star ratings, dates, and expandable text. Includes sort/filter dropdowns and action button.",
|
|
5
|
+
"files": [
|
|
6
|
+
{
|
|
7
|
+
"path": "components/blocks/reviews-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\";\n\n// ============================================\n// Types\n// ============================================\n\nexport interface ReviewItem {\n id: string;\n name: string;\n avatarUrl?: string;\n rating: number; // 1-5\n date: string;\n reviewText: 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 ReviewsTableProps {\n /** Table title */\n title?: string;\n /** Number of reviews to display in subtitle */\n reviewCount?: number;\n /** Custom review count text (overrides default \"{count} customer reviews\") */\n reviewCountText?: string;\n /** Review data */\n reviews?: ReviewItem[];\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 \"More\" is clicked on a review */\n onReadMore?: (review: ReviewItem) => void;\n /** Additional class names */\n className?: string;\n}\n\n// ============================================\n// Default Data\n// ============================================\n\nconst defaultReviews: ReviewItem[] = [\n {\n id: \"1\",\n name: \"Jeffrey Connor\",\n avatarUrl: \"https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=150&h=150&fit=crop&crop=face\",\n rating: 5,\n date: \"Jul 22, 2024\",\n reviewText: \"Absolutely loved my experience at Sushi Ro! The ambiance was cozy, and the staff were incredibly attentive. The food was outstanding, with a diverse menu offering authentic flavors. Highly recommend trying the omakase...\",\n },\n {\n id: \"2\",\n name: \"Raj Mishra\",\n avatarUrl: \"https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=150&h=150&fit=crop&crop=face\",\n rating: 5,\n date: \"Jul 12, 2024\",\n reviewText: \"From the moment we walked in, we were greeted warmly and seated promptly. The omakase menu offered a delightful range of dishes, each bursting with flavor and beautifully presented...\",\n },\n];\n\nconst defaultSortOptions: SortOption[] = [\n { id: \"date-newest\", label: \"Newest first\" },\n { id: \"date-oldest\", label: \"Oldest first\" },\n { id: \"rating-high\", label: \"Highest rated\" },\n { id: \"rating-low\", label: \"Lowest rated\" },\n];\n\nconst defaultFilterOptions: FilterOption[] = [\n { id: \"all\", label: \"All ratings\" },\n { id: \"5-star\", label: \"5 stars\" },\n { id: \"4-star\", label: \"4 stars\" },\n { id: \"3-star\", label: \"3 stars\" },\n { id: \"2-star\", label: \"2 stars\" },\n { id: \"1-star\", label: \"1 star\" },\n];\n\n// ============================================\n// Sub-components\n// ============================================\n\ninterface StarRatingProps {\n rating: number;\n maxRating?: number;\n}\n\nfunction StarRating({ rating, maxRating = 5 }: StarRatingProps) {\n return (\n <div \n className=\"flex items-center\"\n style={{ gap: \"var(--spacing-xs)\" }}\n >\n {Array.from({ length: maxRating }, (_, i) => (\n <svg\n key={i}\n width=\"20\"\n height=\"20\"\n viewBox=\"0 0 20 20\"\n fill=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\"\n >\n <path\n d=\"M10 1.66667L12.575 6.88334L18.3333 7.725L14.1667 11.7833L15.15 17.5167L10 14.8083L4.85 17.5167L5.83333 11.7833L1.66667 7.725L7.425 6.88334L10 1.66667Z\"\n fill={i < rating ? \"var(--canvas-primary)\" : \"var(--canvas-border)\"}\n stroke={i < rating ? \"var(--canvas-primary)\" : \"var(--canvas-border)\"}\n strokeWidth=\"1.5\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n </svg>\n ))}\n </div>\n );\n}\n\ninterface ReviewListItemProps {\n review: ReviewItem;\n isFirst?: boolean;\n isLast?: boolean;\n onReadMore?: (review: ReviewItem) => void;\n}\n\nfunction ReviewListItem({ review, isFirst, isLast, onReadMore }: ReviewListItemProps) {\n return (\n <div\n className=\"w-full\"\n style={{\n borderTop: isFirst ? \"1px solid var(--canvas-border)\" : \"none\",\n borderBottom: \"1px solid var(--canvas-border)\",\n paddingTop: \"var(--spacing-3xl)\",\n paddingBottom: \"var(--spacing-3xl)\",\n }}\n >\n <div \n className=\"flex flex-col w-full\"\n style={{ gap: \"var(--spacing-xl)\" }}\n >\n {/* Top Row: Avatar + Name/Rating + Date */}\n <div \n className=\"flex items-start 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 borderRadius: \"var(--spacing-3xl)\",\n }}\n >\n <AvatarImage src={review.avatarUrl} alt={review.name} />\n <AvatarFallback\n style={{\n backgroundColor: \"var(--canvas-surface)\",\n color: \"var(--canvas-text-muted)\",\n }}\n >\n {review.name.split(\" \").map(n => n[0]).join(\"\").slice(0, 2)}\n </AvatarFallback>\n </Avatar>\n\n {/* Name and Rating */}\n <div \n className=\"flex flex-col flex-1 min-w-0\"\n style={{ gap: \"var(--spacing-xs)\" }}\n >\n <p\n className=\"m-0\"\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 600,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n {review.name}\n </p>\n <StarRating rating={review.rating} />\n </div>\n\n {/* Date */}\n <p\n className=\"m-0 shrink-0\"\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: \"var(--typo-body-s-weight)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text-muted)\",\n }}\n >\n {review.date}\n </p>\n </div>\n\n {/* Review Text */}\n <p\n className=\"m-0 w-full\"\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 {review.reviewText}\n </p>\n\n {/* More Link */}\n <button\n onClick={() => onReadMore?.(review)}\n className=\"p-0 border-none bg-transparent cursor-pointer text-left\"\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-primary)\",\n }}\n >\n More\n </button>\n </div>\n </div>\n );\n}\n\n// ============================================\n// Main Component\n// ============================================\n\n/**\n * Canvas Design System - Reviews Table Block\n * \n * A configurable reviews listing with header section including title,\n * sort/filter dropdowns, and action button. Displays review items\n * with avatar, star rating, date, and expandable text.\n * \n * @example\n * ```tsx\n * <ReviewsTable\n * title=\"Reviews\"\n * reviews={[\n * { id: \"1\", name: \"John Doe\", rating: 5, date: \"Jul 22, 2024\", reviewText: \"Great experience!\" }\n * ]}\n * onAddNew={() => console.log(\"Add new\")}\n * />\n * ```\n */\nexport function ReviewsTable({\n title = \"Reviews\",\n reviewCount,\n reviewCountText,\n reviews = defaultReviews,\n sortOptions = defaultSortOptions,\n filterOptions = defaultFilterOptions,\n actionButtonText = \"Add new\",\n onAddNew,\n onSort,\n onFilter,\n onReadMore,\n className,\n}: ReviewsTableProps) {\n const [sortValue, setSortValue] = useState<string>(\"\");\n const [filterValue, setFilterValue] = useState<string>(\"\");\n\n const displayReviewCount = reviewCount ?? reviews.length;\n const displayReviewText = reviewCountText ?? `${displayReviewCount} customer reviews`;\n\n const handleSortChange = (value: string) => {\n setSortValue(value);\n onSort?.(value);\n };\n\n const handleFilterChange = (value: string) => {\n setFilterValue(value);\n onFilter?.(value);\n };\n\n return (\n <div \n className={cn(\"flex flex-col w-full\", className)}\n style={{ gap: \"var(--spacing-xl)\" }}\n >\n {/* Header Section */}\n <div \n className=\"flex flex-wrap items-start w-full\"\n style={{ gap: \"var(--spacing-xl)\" }}\n >\n {/* Title and Count */}\n <div \n className=\"flex flex-col flex-1 min-w-[200px]\"\n style={{ gap: \"var(--spacing-xs)\" }}\n >\n <h2\n style={{\n fontFamily: \"var(--typo-h6-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-h6-size)\",\n fontWeight: \"var(--typo-h6-weight)\",\n letterSpacing: \"var(--typo-h6-spacing)\",\n lineHeight: \"var(--typo-h6-line-height)\",\n color: \"var(--canvas-text)\",\n margin: 0,\n }}\n >\n {title}\n </h2>\n <p\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: \"var(--typo-body-s-weight)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text-muted)\",\n margin: 0,\n }}\n >\n {displayReviewText}\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 {/* Reviews List */}\n <div className=\"flex flex-col w-full\">\n {reviews.map((review, index) => (\n <ReviewListItem\n key={review.id}\n review={review}\n isFirst={index === 0}\n isLast={index === reviews.length - 1}\n onReadMore={onReadMore}\n />\n ))}\n </div>\n </div>\n );\n}\n"
|
|
10
|
+
}
|
|
11
|
+
],
|
|
12
|
+
"dependencies": [],
|
|
13
|
+
"registryDependencies": [
|
|
14
|
+
"utils",
|
|
15
|
+
"button",
|
|
16
|
+
"select",
|
|
17
|
+
"avatar"
|
|
18
|
+
]
|
|
19
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "screen-flowchart",
|
|
3
|
+
"type": "registry:block",
|
|
4
|
+
"description": "",
|
|
5
|
+
"files": [
|
|
6
|
+
{
|
|
7
|
+
"path": "components/blocks/screen-flowchart.tsx",
|
|
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-yellow-100 text-yellow-700 border-yellow-200\",\n review: \"bg-blue-100 text-blue-700 border-blue-200\",\n approved: \"bg-green-100 text-green-700 border-green-200\",\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-white 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 className=\"text-lg\">{screen.icon || \"📄\"}</span>\n <span className=\"font-medium text-sm text-[var(--canvas-text)] truncate\">\n {screen.name}\n </span>\n </div>\n <div className=\"flex items-center gap-2\">\n <span className=\"text-xs text-[var(--canvas-text-muted)] font-mono\">\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={cn(\n \"text-[10px] px-1.5 py-0.5 rounded border font-medium capitalize\",\n statusColors[screen.status]\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 text-xs\",\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\">\n {child.name}\n </span>\n <span className=\"text-[var(--canvas-text-muted)] font-mono ml-auto\">\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 text-xs 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 >\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-white !border-[var(--canvas-border)] !shadow-sm\" />\n </ReactFlow>\n </div>\n );\n}\n"
|
|
10
|
+
}
|
|
11
|
+
],
|
|
12
|
+
"dependencies": [
|
|
13
|
+
"reactflow",
|
|
14
|
+
"lucide-react"
|
|
15
|
+
],
|
|
16
|
+
"registryDependencies": [
|
|
17
|
+
"utils"
|
|
18
|
+
]
|
|
19
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "screen-prompt-builder",
|
|
3
|
+
"type": "registry:block",
|
|
4
|
+
"description": "",
|
|
5
|
+
"files": [
|
|
6
|
+
{
|
|
7
|
+
"path": "components/blocks/screen-prompt-builder.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\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=\"text-lg font-semibold text-[var(--canvas-text)]\">\n Build from Components\n </h3>\n <p className=\"text-sm text-[var(--canvas-text-muted)] mt-1\">\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-sm font-medium text-[var(--canvas-text)]\">\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-sm font-medium text-[var(--canvas-text)]\"\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)] 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 {/* Screen Context */}\n <div className=\"space-y-2\">\n <label\n htmlFor=\"screen-context\"\n className=\"text-sm font-medium text-[var(--canvas-text)]\"\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)] 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 {/* Target Persona (optional) */}\n {personas.length > 0 && (\n <div className=\"space-y-2\">\n <label\n htmlFor=\"persona\"\n className=\"text-sm font-medium text-[var(--canvas-text)]\"\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)] text-sm focus:outline-none focus:ring-2 focus:ring-[var(--canvas-primary)] focus:border-transparent\"\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 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 Select components or enter screen details to generate a prompt\n </p>\n </div>\n )}\n </div>\n );\n}\n"
|
|
10
|
+
}
|
|
11
|
+
],
|
|
12
|
+
"dependencies": [
|
|
13
|
+
"lucide-react"
|
|
14
|
+
],
|
|
15
|
+
"registryDependencies": [
|
|
16
|
+
"utils",
|
|
17
|
+
"component-search"
|
|
18
|
+
]
|
|
19
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "screen-prompt-template",
|
|
3
|
+
"type": "registry:block",
|
|
4
|
+
"description": "",
|
|
5
|
+
"files": [
|
|
6
|
+
{
|
|
7
|
+
"path": "components/blocks/screen-prompt-template.tsx",
|
|
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 text-sm font-medium transition-all\",\n isActive\n ? \"bg-[var(--canvas-primary)] text-white 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 >\n <Icon className=\"size-4\" />\n {config.label}\n </button>\n );\n }\n )}\n </div>\n\n {/* Description */}\n <p className=\"text-xs text-[var(--canvas-text-muted)] mb-3\">\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 text-sm font-medium text-[var(--canvas-text-muted)]\">\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 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 {currentConfig.prompt}\n </pre>\n </div>\n );\n}\n"
|
|
10
|
+
}
|
|
11
|
+
],
|
|
12
|
+
"dependencies": [
|
|
13
|
+
"lucide-react"
|
|
14
|
+
],
|
|
15
|
+
"registryDependencies": [
|
|
16
|
+
"utils"
|
|
17
|
+
]
|
|
18
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "search-bar",
|
|
3
|
+
"type": "registry:block",
|
|
4
|
+
"description": "Prominent search input with icon.",
|
|
5
|
+
"files": [
|
|
6
|
+
{
|
|
7
|
+
"path": "components/blocks/search-bar.tsx",
|
|
8
|
+
"type": "registry:block",
|
|
9
|
+
"content": "\"use client\";\n\nimport { cn } from \"../../lib/utils\";\nimport { Search } from \"lucide-react\";\nimport { Button } from \"../ui/button\";\n\ninterface SearchBarProps {\n /** Placeholder text for the input */\n placeholder?: string;\n /** Current search value */\n value?: string;\n /** Callback when the input value changes */\n onChange?: (value: string) => void;\n /** Callback when search is triggered (button click or Enter key) */\n onSearch?: () => void;\n /** Search button label */\n buttonLabel?: string;\n /** Additional class names */\n className?: string;\n}\n\n/**\n * Canvas Design System - Search Bar Component\n * \n * A search input with a search icon on the left and a conditional search button.\n * The button appears only when the input has content.\n * Uses standard input sizing variables for consistent styling.\n * \n * @example\n * ```tsx\n * <SearchBar\n * placeholder=\"Search\"\n * value={searchValue}\n * onChange={setSearchValue}\n * onSearch={handleSearch}\n * />\n * ```\n */\nexport function SearchBar({\n placeholder = \"Search\",\n value = \"\",\n onChange,\n onSearch,\n buttonLabel = \"Search\",\n className,\n}: SearchBarProps) {\n const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {\n if (e.key === \"Enter\") {\n onSearch?.();\n }\n };\n\n const hasValue = value.length > 0;\n\n return (\n <div\n className={cn(\n \"flex items-center gap-2\",\n \"bg-white border border-[var(--canvas-border-input)]\",\n \"pr-2\",\n \"transition-colors\",\n \"focus-within:border-[var(--canvas-border-input-focus)] focus-within:ring-2 focus-within:ring-[var(--canvas-border-input-focus)] focus-within:ring-offset-2\",\n className\n )}\n style={{ \n height: \"var(--input-standard-height)\",\n paddingLeft: \"var(--input-standard-px)\",\n borderRadius: \"var(--input-standard-radius)\",\n }}\n >\n {/* Search Icon */}\n <Search className=\"size-4 shrink-0 text-[var(--canvas-text-muted)]\" />\n\n {/* Input */}\n <input\n type=\"text\"\n placeholder={placeholder}\n value={value}\n onChange={(e) => onChange?.(e.target.value)}\n onKeyDown={handleKeyDown}\n className={cn(\n \"flex-1 h-full bg-transparent\",\n \"text-[var(--canvas-text)]\",\n \"placeholder:text-[var(--canvas-text-placeholder)]\",\n \"outline-none border-none\"\n )}\n style={{ fontSize: \"var(--input-standard-font-size)\" }}\n />\n\n {/* Search Button - Only visible when input has content */}\n {hasValue && (\n <Button\n variant=\"primary\"\n size=\"sm\"\n onClick={onSearch}\n className=\"shrink-0\"\n style={{ \n height: \"calc(var(--input-standard-height) - 8px)\",\n borderRadius: \"var(--input-standard-radius)\",\n }}\n >\n {buttonLabel}\n </Button>\n )}\n </div>\n );\n}\n\n"
|
|
10
|
+
}
|
|
11
|
+
],
|
|
12
|
+
"dependencies": [
|
|
13
|
+
"lucide-react"
|
|
14
|
+
],
|
|
15
|
+
"registryDependencies": [
|
|
16
|
+
"utils",
|
|
17
|
+
"button"
|
|
18
|
+
]
|
|
19
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "search-sidebar",
|
|
3
|
+
"type": "registry:block",
|
|
4
|
+
"description": "Sidebar with search results or filters.",
|
|
5
|
+
"files": [
|
|
6
|
+
{
|
|
7
|
+
"path": "components/blocks/search-sidebar.tsx",
|
|
8
|
+
"type": "registry:block",
|
|
9
|
+
"content": "\"use client\";\n\nimport * as React from \"react\";\nimport { Minus, Plus } from \"lucide-react\";\nimport { cn } from \"../../lib/utils\";\nimport { CheckboxWithLabel } from \"../ui/checkbox\";\nimport { RadioGroup, RadioGroupItem } from \"../ui/radio-group\";\nimport { Switch } from \"../ui/switch\";\nimport { Slider } from \"../ui/slider\";\nimport { RangeInput } from \"../ui/range-input\";\nimport { SelectablePills, type PillOption } from \"../ui/selectable-pills\";\nimport { Label } from \"../ui/label\";\n\n// ============================================\n// Filter Section Header Component\n// ============================================\n\ninterface FilterSectionHeaderProps {\n title: string;\n isExpanded: boolean;\n onToggle: () => void;\n onClear?: () => void;\n showClear?: boolean;\n}\n\nfunction FilterSectionHeader({ title, isExpanded, onToggle, onClear, showClear = true }: FilterSectionHeaderProps) {\n return (\n <div className=\"flex items-center justify-between h-7 w-full\">\n <button\n type=\"button\"\n onClick={onToggle}\n className=\"flex items-center gap-1.5 hover:opacity-70 transition-opacity\"\n >\n {isExpanded ? (\n <Minus className=\"size-3.5 text-[var(--canvas-text)]\" />\n ) : (\n <Plus className=\"size-3.5 text-[var(--canvas-text)]\" />\n )}\n <span \n className=\"font-semibold text-[var(--canvas-text)]\"\n style={{ fontSize: \"var(--input-small-font-size)\" }}\n >\n {title}\n </span>\n </button>\n {showClear && onClear && isExpanded && (\n <button\n type=\"button\"\n onClick={onClear}\n className=\"text-[10px] text-[var(--canvas-destructive)] hover:underline\"\n >\n Clear\n </button>\n )}\n </div>\n );\n}\n\n// ============================================\n// Checkbox Filter Section\n// ============================================\n\nexport interface CheckboxFilterOption {\n id: string;\n label: string;\n}\n\ninterface CheckboxFilterSectionProps {\n title: string;\n options: CheckboxFilterOption[];\n selected: string[];\n onSelectionChange: (selected: string[]) => void;\n onClear?: () => void;\n}\n\nfunction CheckboxFilterSection({ \n title, \n options, \n selected, \n onSelectionChange,\n onClear,\n}: CheckboxFilterSectionProps) {\n const [isExpanded, setIsExpanded] = React.useState(true);\n\n const handleToggle = (optionId: string, checked: boolean) => {\n if (checked) {\n onSelectionChange([...selected, optionId]);\n } else {\n onSelectionChange(selected.filter(id => id !== optionId));\n }\n };\n\n const handleClear = () => {\n onSelectionChange([]);\n onClear?.();\n };\n\n return (\n <div className=\"flex flex-col gap-3 pb-6 border-b border-[var(--canvas-border)]\">\n <FilterSectionHeader \n title={title} \n isExpanded={isExpanded}\n onToggle={() => setIsExpanded(!isExpanded)}\n onClear={handleClear} \n showClear={selected.length > 0} \n />\n {isExpanded && (\n <div className=\"flex flex-col gap-3\">\n {options.map((option) => (\n <CheckboxWithLabel\n key={option.id}\n size=\"sm\"\n checked={selected.includes(option.id)}\n onCheckedChange={(checked) => handleToggle(option.id, checked === true)}\n >\n {option.label}\n </CheckboxWithLabel>\n ))}\n </div>\n )}\n </div>\n );\n}\n\n// ============================================\n// Pills Filter Section\n// ============================================\n\ninterface PillsFilterSectionProps {\n title: string;\n options: PillOption[];\n selected: string[];\n onSelectionChange: (selected: string[]) => void;\n onClear?: () => void;\n}\n\nfunction PillsFilterSection({ \n title, \n options, \n selected, \n onSelectionChange,\n onClear,\n}: PillsFilterSectionProps) {\n const [isExpanded, setIsExpanded] = React.useState(true);\n\n const handleClear = () => {\n onSelectionChange([]);\n onClear?.();\n };\n\n return (\n <div className=\"flex flex-col gap-3 pt-5 pb-6 border-b border-[var(--canvas-border)]\">\n <FilterSectionHeader \n title={title} \n isExpanded={isExpanded}\n onToggle={() => setIsExpanded(!isExpanded)}\n onClear={handleClear} \n showClear={selected.length > 0} \n />\n {isExpanded && (\n <SelectablePills\n options={options}\n selected={selected}\n onSelectionChange={onSelectionChange}\n inputSize=\"sm\"\n />\n )}\n </div>\n );\n}\n\n// ============================================\n// Price Range Filter Section\n// ============================================\n\ninterface PriceRangeFilterSectionProps {\n title: string;\n minValue: string;\n maxValue: string;\n onMinChange: (value: string) => void;\n onMaxChange: (value: string) => void;\n onClear?: () => void;\n}\n\nfunction PriceRangeFilterSection({ \n title, \n minValue, \n maxValue, \n onMinChange,\n onMaxChange,\n onClear,\n}: PriceRangeFilterSectionProps) {\n const [isExpanded, setIsExpanded] = React.useState(true);\n\n const handleClear = () => {\n onMinChange(\"\");\n onMaxChange(\"\");\n onClear?.();\n };\n\n const hasValue = minValue !== \"\" || maxValue !== \"\";\n\n return (\n <div className=\"flex flex-col gap-3 pt-5 pb-6 border-b border-[var(--canvas-border)]\">\n <FilterSectionHeader \n title={title} \n isExpanded={isExpanded}\n onToggle={() => setIsExpanded(!isExpanded)}\n onClear={handleClear} \n showClear={hasValue} \n />\n {isExpanded && (\n <RangeInput\n inputSize=\"sm\"\n minValue={minValue}\n maxValue={maxValue}\n onMinChange={onMinChange}\n onMaxChange={onMaxChange}\n minPlaceholder=\"Min\"\n maxPlaceholder=\"Max\"\n />\n )}\n </div>\n );\n}\n\n// ============================================\n// Radio Filter Section\n// ============================================\n\ninterface RadioFilterOption {\n id: string;\n label: string;\n}\n\ninterface RadioFilterSectionProps {\n title: string;\n options: RadioFilterOption[];\n selected: string;\n onSelectionChange: (selected: string) => void;\n onClear?: () => void;\n}\n\nfunction RadioFilterSection({ \n title, \n options, \n selected, \n onSelectionChange,\n onClear,\n}: RadioFilterSectionProps) {\n const [isExpanded, setIsExpanded] = React.useState(true);\n\n const handleClear = () => {\n onSelectionChange(\"\");\n onClear?.();\n };\n\n return (\n <div className=\"flex flex-col gap-3 pt-5 pb-6 border-b border-[var(--canvas-border)]\">\n <FilterSectionHeader \n title={title} \n isExpanded={isExpanded}\n onToggle={() => setIsExpanded(!isExpanded)}\n onClear={handleClear} \n showClear={selected !== \"\"} \n />\n {isExpanded && (\n <RadioGroup value={selected} onValueChange={onSelectionChange} className=\"gap-3\">\n {options.map((option) => (\n <div key={option.id} className=\"flex items-center gap-2\">\n <RadioGroupItem value={option.id} id={option.id} className=\"size-3.5\" />\n <Label \n htmlFor={option.id} \n className=\"font-normal text-[var(--canvas-text-muted)] cursor-pointer\"\n style={{ fontSize: \"var(--input-small-font-size)\" }}\n >\n {option.label}\n </Label>\n </div>\n ))}\n </RadioGroup>\n )}\n </div>\n );\n}\n\n// ============================================\n// Slider Filter Section\n// ============================================\n\ninterface SliderFilterSectionProps {\n title: string;\n value: number[];\n onValueChange: (value: number[]) => void;\n min?: number;\n max?: number;\n step?: number;\n labelFormatter?: (value: number[]) => string;\n onClear?: () => void;\n}\n\nfunction SliderFilterSection({ \n title, \n value, \n onValueChange,\n min = 0,\n max = 100,\n step = 1,\n labelFormatter,\n onClear,\n}: SliderFilterSectionProps) {\n const [isExpanded, setIsExpanded] = React.useState(true);\n\n const handleClear = () => {\n onValueChange([min, max]);\n onClear?.();\n };\n\n const isDefault = value[0] === min && value[1] === max;\n\n return (\n <div className=\"flex flex-col gap-3 pt-5 pb-6 border-b border-[var(--canvas-border)]\">\n <FilterSectionHeader \n title={title} \n isExpanded={isExpanded}\n onToggle={() => setIsExpanded(!isExpanded)}\n onClear={handleClear} \n showClear={!isDefault} \n />\n {isExpanded && (\n <Slider\n inputSize=\"sm\"\n value={value}\n onValueChange={onValueChange}\n min={min}\n max={max}\n step={step}\n labelFormatter={labelFormatter}\n />\n )}\n </div>\n );\n}\n\n// ============================================\n// Toggle Filter Section\n// ============================================\n\ninterface ToggleFilterSectionProps {\n title: string;\n label: string;\n checked: boolean;\n onCheckedChange: (checked: boolean) => void;\n onClear?: () => void;\n}\n\nfunction ToggleFilterSection({ \n title, \n label,\n checked, \n onCheckedChange,\n onClear,\n}: ToggleFilterSectionProps) {\n const [isExpanded, setIsExpanded] = React.useState(true);\n\n const handleClear = () => {\n onCheckedChange(false);\n onClear?.();\n };\n\n return (\n <div className=\"flex flex-col gap-3 pt-5 pb-6\">\n <FilterSectionHeader \n title={title} \n isExpanded={isExpanded}\n onToggle={() => setIsExpanded(!isExpanded)}\n onClear={handleClear} \n showClear={checked} \n />\n {isExpanded && (\n <div className=\"flex items-center gap-3\">\n <Switch checked={checked} onCheckedChange={onCheckedChange} />\n <span \n className=\"text-[var(--canvas-text-muted)]\"\n style={{ fontSize: \"var(--input-small-font-size)\" }}\n >\n {label}\n </span>\n </div>\n )}\n </div>\n );\n}\n\n// ============================================\n// Main SearchSidebar Component\n// ============================================\n\nexport interface SearchSidebarState {\n specials: string[];\n languages: string[];\n priceMin: string;\n priceMax: string;\n starRating: string;\n distance: number[];\n membershipDeals: boolean;\n}\n\nexport interface SearchSidebarProps {\n className?: string;\n state: SearchSidebarState;\n onStateChange: (state: SearchSidebarState) => void;\n}\n\n// Default filter options\nconst defaultSpecialsOptions: CheckboxFilterOption[] = [\n { id: \"deals\", label: \"Deals & discounts\" },\n { id: \"cancellation\", label: \"Free cancellation\" },\n { id: \"sellout\", label: \"Likely to sell out\" },\n { id: \"skipline\", label: \"Skip-the-line\" },\n { id: \"private\", label: \"Private tour\" },\n { id: \"exclusive\", label: \"Exclusive deals\" },\n];\n\nconst defaultLanguageOptions: PillOption[] = [\n { id: \"english\", label: \"English\" },\n { id: \"mandarin\", label: \"Mandarin\" },\n { id: \"spanish\", label: \"Spanish\" },\n { id: \"french\", label: \"French\" },\n { id: \"german\", label: \"German\" },\n { id: \"italian\", label: \"Italian\" },\n { id: \"japanese\", label: \"Japanese\" },\n];\n\nconst defaultStarRatingOptions: RadioFilterOption[] = [\n { id: \"5\", label: \"5 stars\" },\n { id: \"4\", label: \"4 stars\" },\n { id: \"3\", label: \"3 stars\" },\n { id: \"2\", label: \"2 stars\" },\n { id: \"1\", label: \"1 star\" },\n];\n\nexport function SearchSidebar({ \n className, \n state, \n onStateChange,\n}: SearchSidebarProps) {\n const updateState = (partial: Partial<SearchSidebarState>) => {\n onStateChange({ ...state, ...partial });\n };\n\n return (\n <aside className={cn(\"w-80 shrink-0 pt-4\", className)}>\n {/* Specials - Checkbox Filter */}\n <CheckboxFilterSection\n title=\"Specials\"\n options={defaultSpecialsOptions}\n selected={state.specials}\n onSelectionChange={(specials) => updateState({ specials })}\n />\n\n {/* Languages - Pills Filter */}\n <PillsFilterSection\n title=\"Languages\"\n options={defaultLanguageOptions}\n selected={state.languages}\n onSelectionChange={(languages) => updateState({ languages })}\n />\n\n {/* Price - Range Input Filter */}\n <PriceRangeFilterSection\n title=\"Price\"\n minValue={state.priceMin}\n maxValue={state.priceMax}\n onMinChange={(priceMin) => updateState({ priceMin })}\n onMaxChange={(priceMax) => updateState({ priceMax })}\n />\n\n {/* Star Rating - Radio Filter */}\n <RadioFilterSection\n title=\"Star rating\"\n options={defaultStarRatingOptions}\n selected={state.starRating}\n onSelectionChange={(starRating) => updateState({ starRating })}\n />\n\n {/* Distance - Slider Filter */}\n <SliderFilterSection\n title=\"Distance\"\n value={state.distance}\n onValueChange={(distance) => updateState({ distance })}\n min={0}\n max={100}\n labelFormatter={(values) => `${values[0]} - ${values[1]} miles`}\n />\n\n {/* Membership Deals - Toggle Filter */}\n <ToggleFilterSection\n title=\"Membership deals\"\n label=\"Show membership pricing\"\n checked={state.membershipDeals}\n onCheckedChange={(membershipDeals) => updateState({ membershipDeals })}\n />\n </aside>\n );\n}\n\n// Export sub-components for custom usage\nexport {\n FilterSectionHeader,\n CheckboxFilterSection,\n PillsFilterSection,\n PriceRangeFilterSection,\n RadioFilterSection,\n SliderFilterSection,\n ToggleFilterSection,\n};\n\n// Export default options\nexport {\n defaultSpecialsOptions,\n defaultLanguageOptions,\n defaultStarRatingOptions,\n};\n\n"
|
|
10
|
+
}
|
|
11
|
+
],
|
|
12
|
+
"dependencies": [
|
|
13
|
+
"lucide-react"
|
|
14
|
+
],
|
|
15
|
+
"registryDependencies": [
|
|
16
|
+
"utils",
|
|
17
|
+
"checkbox",
|
|
18
|
+
"radio-group",
|
|
19
|
+
"switch",
|
|
20
|
+
"slider",
|
|
21
|
+
"range-input",
|
|
22
|
+
"selectable-pills",
|
|
23
|
+
"label"
|
|
24
|
+
]
|
|
25
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "settings-list-row",
|
|
3
|
+
"type": "registry:block",
|
|
4
|
+
"description": "Row item for settings lists with label, value, and edit action.",
|
|
5
|
+
"files": [
|
|
6
|
+
{
|
|
7
|
+
"path": "components/blocks/settings-list-row.tsx",
|
|
8
|
+
"type": "registry:block",
|
|
9
|
+
"content": "\"use client\";\n\nimport { cn } from \"../../lib/utils\";\nimport { Pencil, MoreVertical } from \"lucide-react\";\nimport { Switch } from \"../ui/switch\";\nimport { Button } from \"../ui/button\";\n\ninterface SettingsListRowProps {\n /** Label for the setting */\n label: string;\n /** Value or description text */\n value?: string;\n /** Type of action on the right side */\n actionType?: \"edit\" | \"toggle\" | \"more\" | \"none\";\n /** For toggle: whether it's checked */\n checked?: boolean;\n /** For toggle: callback when changed */\n onCheckedChange?: (checked: boolean) => void;\n /** For edit/more: callback when clicked */\n onActionClick?: () => void;\n /** Whether this is the first row (has top border) */\n isFirst?: boolean;\n /** Whether this is the last row (no bottom border) */\n isLast?: boolean;\n /** Additional class name */\n className?: string;\n}\n\n/**\n * Settings List Row\n * \n * A reusable row for settings lists with:\n * - Label on the left\n * - Value/description in the middle\n * - Action on the right (edit icon, toggle, or more menu)\n */\nexport function SettingsListRow({\n label,\n value,\n actionType = \"edit\",\n checked,\n onCheckedChange,\n onActionClick,\n isFirst = false,\n isLast = false,\n className,\n}: SettingsListRowProps) {\n return (\n <div\n className={cn(\n \"flex items-center gap-[var(--spacing-xl)] min-h-[48px] py-3\",\n isFirst && \"border-t border-[var(--canvas-neutral-border)]\",\n !isLast && \"border-b border-[var(--canvas-neutral-border)]\",\n className\n )}\n >\n {/* Label */}\n <div \n className=\"w-[200px] shrink-0 text-[var(--canvas-text)]\"\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size, 14px)\",\n fontWeight: 500,\n lineHeight: \"var(--typo-body-s-line-height, 20px)\",\n }}\n >\n {label}\n </div>\n\n {/* Value */}\n <div \n className=\"flex-1 text-[var(--canvas-text)]\"\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size, 14px)\",\n fontWeight: 400,\n lineHeight: \"var(--typo-body-s-line-height, 20px)\",\n }}\n >\n {value}\n </div>\n\n {/* Action */}\n <div className=\"shrink-0\">\n {actionType === \"toggle\" && (\n <Switch\n checked={checked}\n onCheckedChange={onCheckedChange}\n />\n )}\n {actionType === \"edit\" && (\n <Button\n variant=\"ghost\"\n size=\"icon-sm\"\n onClick={onActionClick}\n className=\"text-[var(--canvas-neutral-text)] hover:text-[var(--canvas-text)]\"\n >\n <Pencil className=\"size-5\" />\n </Button>\n )}\n {actionType === \"more\" && (\n <Button\n variant=\"ghost\"\n size=\"icon-sm\"\n onClick={onActionClick}\n className=\"text-[var(--canvas-neutral-text)] hover:text-[var(--canvas-text)]\"\n >\n <MoreVertical className=\"size-5\" />\n </Button>\n )}\n </div>\n </div>\n );\n}\n\n"
|
|
10
|
+
}
|
|
11
|
+
],
|
|
12
|
+
"dependencies": [
|
|
13
|
+
"lucide-react"
|
|
14
|
+
],
|
|
15
|
+
"registryDependencies": [
|
|
16
|
+
"utils",
|
|
17
|
+
"switch",
|
|
18
|
+
"button"
|
|
19
|
+
]
|
|
20
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "sidebar-cards",
|
|
3
|
+
"type": "registry:block",
|
|
4
|
+
"description": "Card list for sidebar showing items with images/titles.",
|
|
5
|
+
"files": [
|
|
6
|
+
{
|
|
7
|
+
"path": "components/blocks/sidebar-cards.tsx",
|
|
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-white 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: \"16px\",\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: \"16px\",\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: \"16px\",\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: \"16px\",\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"
|
|
10
|
+
}
|
|
11
|
+
],
|
|
12
|
+
"dependencies": [
|
|
13
|
+
"lucide-react"
|
|
14
|
+
],
|
|
15
|
+
"registryDependencies": [
|
|
16
|
+
"utils"
|
|
17
|
+
]
|
|
18
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "sidebar-profile-card",
|
|
3
|
+
"type": "registry:block",
|
|
4
|
+
"description": "Compact profile card for sidebar with avatar and key info.",
|
|
5
|
+
"files": [
|
|
6
|
+
{
|
|
7
|
+
"path": "components/blocks/sidebar-profile-card.tsx",
|
|
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 { Button } from \"../ui/button\";\nimport {\n MapPin,\n BookOpen,\n Video,\n DollarSign,\n Star,\n Globe,\n Facebook,\n Twitter,\n Instagram,\n Eye,\n} from \"lucide-react\";\n\ninterface InfoRow {\n icon: \"location\" | \"education\" | \"sessions\" | \"earnings\";\n label: string;\n value: string;\n}\n\ninterface SidebarProfileCardProps {\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 role/title */\n role?: string;\n /** Star rating value */\n rating?: number;\n /** Number of reviews */\n reviewCount?: string;\n /** Certification text */\n certification?: string;\n /** Info rows (location, education, sessions, earnings) */\n infoRows?: InfoRow[];\n /** Contact button click handler */\n onContactClick?: () => void;\n /** Hire button click handler */\n onHireClick?: () => void;\n /** Additional class names */\n className?: string;\n}\n\nconst infoIcons = {\n location: MapPin,\n education: BookOpen,\n sessions: Video,\n earnings: DollarSign,\n};\n\n/**\n * Canvas Design System - Sidebar Profile Card Component\n *\n * A profile card designed for sidebar placement with avatar,\n * name, rating, action buttons, info rows, and social icons.\n */\nexport function SidebarProfileCard({\n avatarUrl,\n avatarFallback = \"JC\",\n showStatus = true,\n name,\n role,\n rating = 4.8,\n reviewCount = \"(2.4k)\",\n certification,\n infoRows = [],\n onContactClick,\n onHireClick,\n className,\n}: SidebarProfileCardProps) {\n return (\n <div\n className={cn(\n \"bg-[var(--canvas-background)] border border-[var(--canvas-border)] rounded-[var(--radius-md)]\",\n \"flex flex-col w-full\",\n className\n )}\n >\n {/* Main Content Section */}\n <div className=\"flex flex-col items-center gap-[var(--spacing-2xl)] px-[var(--spacing-4xl)] pt-[var(--spacing-4xl)]\">\n {/* Avatar with Status */}\n <div className=\"flex flex-col items-center gap-[var(--radius-md)]\">\n <div className=\"relative\">\n <Avatar className=\"size-[120px]\">\n <AvatarImage src={avatarUrl} alt={name} />\n <AvatarFallback className=\"text-2xl font-semibold bg-[var(--canvas-surface)] text-[var(--canvas-text-muted)]\">\n {avatarFallback}\n </AvatarFallback>\n </Avatar>\n {showStatus && (\n <div className=\"absolute bottom-[10px] right-[10px] size-5 rounded-full bg-emerald-500 border-[3px] border-white\" />\n )}\n </div>\n\n {/* Name & Role */}\n <div className=\"flex flex-col items-center gap-[var(--spacing-xs)]\">\n <Typography variant=\"body-xl\" className=\"text-center\" style={{ fontWeight: 600 }}>\n {name}\n </Typography>\n {role && (\n <Typography variant=\"body-s\" color=\"muted\" className=\"text-center\">\n {role}\n </Typography>\n )}\n </div>\n </div>\n\n {/* Star Rating */}\n <div className=\"flex items-center gap-1 justify-center\">\n <Star className=\"size-4 fill-[var(--canvas-primary)] text-[var(--canvas-primary)]\" />\n <Typography variant=\"body-xs\" className=\"font-semibold\">\n {rating}\n </Typography>\n <Typography variant=\"body-xs\" color=\"muted\">\n {reviewCount}\n </Typography>\n </div>\n\n {/* Action Buttons */}\n <div className=\"flex gap-[var(--spacing-xl)] w-full\">\n <Button\n variant=\"outline\"\n className=\"flex-1 h-10\"\n onClick={onContactClick}\n >\n Contact\n </Button>\n <Button\n variant=\"default\"\n className=\"flex-1 h-10\"\n onClick={onHireClick}\n >\n Hire me\n </Button>\n </div>\n\n {/* Divider */}\n <div className=\"w-full h-px bg-[var(--canvas-border)]\" />\n\n {/* Certification */}\n {certification && (\n <Typography variant=\"body-s\" color=\"muted\" className=\"text-center\">\n {certification}\n </Typography>\n )}\n\n {/* Info Rows */}\n {infoRows.length > 0 && (\n <div className=\"flex flex-col gap-[var(--spacing-lg)] w-full\">\n {infoRows.map((row, index) => {\n const IconComponent = infoIcons[row.icon];\n return (\n <div\n key={index}\n className=\"flex items-center justify-between w-full\"\n >\n <div className=\"flex items-center gap-[var(--spacing-sm)]\">\n <IconComponent className=\"size-4 text-[var(--canvas-text-muted)]\" />\n <Typography variant=\"body-s\" color=\"muted\">\n {row.label}\n </Typography>\n </div>\n <Typography variant=\"body-s\" className=\"font-semibold text-[var(--canvas-neutral-text)]\">\n {row.value}\n </Typography>\n </div>\n );\n })}\n </div>\n )}\n\n {/* Divider */}\n <div className=\"w-full h-px bg-[var(--canvas-border)]\" />\n\n {/* Social Icons */}\n <div className=\"flex gap-[var(--spacing-xl)] pb-[var(--spacing-3xl)]\">\n <Eye className=\"size-4 text-[var(--canvas-text-muted)] hover:text-[var(--canvas-text)] transition-colors cursor-pointer\" />\n <Facebook className=\"size-4 text-[var(--canvas-text-muted)] hover:text-[var(--canvas-text)] transition-colors cursor-pointer\" />\n <Twitter className=\"size-4 text-[var(--canvas-text-muted)] hover:text-[var(--canvas-text)] transition-colors cursor-pointer\" />\n <Instagram className=\"size-4 text-[var(--canvas-text-muted)] hover:text-[var(--canvas-text)] transition-colors cursor-pointer\" />\n </div>\n </div>\n </div>\n );\n}\n\n"
|
|
10
|
+
}
|
|
11
|
+
],
|
|
12
|
+
"dependencies": [
|
|
13
|
+
"lucide-react"
|
|
14
|
+
],
|
|
15
|
+
"registryDependencies": [
|
|
16
|
+
"utils",
|
|
17
|
+
"avatar",
|
|
18
|
+
"typography",
|
|
19
|
+
"button"
|
|
20
|
+
]
|
|
21
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "slideshow-grid-tiles",
|
|
3
|
+
"type": "registry:block",
|
|
4
|
+
"description": "2-column grid of portfolio/slideshow tiles with hover states. Features large images with slideshow navigation, save button, user info with avatar, and engagement stats (likes/views).",
|
|
5
|
+
"files": [
|
|
6
|
+
{
|
|
7
|
+
"path": "components/blocks/slideshow-grid-tiles.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 { 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"
|
|
10
|
+
}
|
|
11
|
+
],
|
|
12
|
+
"dependencies": [
|
|
13
|
+
"lucide-react"
|
|
14
|
+
],
|
|
15
|
+
"registryDependencies": [
|
|
16
|
+
"utils",
|
|
17
|
+
"button",
|
|
18
|
+
"select",
|
|
19
|
+
"avatar"
|
|
20
|
+
]
|
|
21
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "social-feed",
|
|
3
|
+
"type": "registry:block",
|
|
4
|
+
"description": "Social media-style feed with post composer, various content types (text, images, video, reposts, link cards), social interactions (like/comment/repost/share), and threaded replies.",
|
|
5
|
+
"files": [
|
|
6
|
+
{
|
|
7
|
+
"path": "components/blocks/social-feed.tsx",
|
|
8
|
+
"type": "registry:block",
|
|
9
|
+
"content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport { Avatar, AvatarImage, AvatarFallback } from \"../ui/avatar\";\nimport { Button } from \"../ui/button\";\nimport { \n Heart, \n MessageCircle, \n RefreshCw, \n Send, \n Paperclip, \n Video, \n Link2, \n MoreHorizontal,\n Play\n} from \"lucide-react\";\n\n// ============================================\n// Types\n// ============================================\n\nexport interface PostAuthor {\n id: string;\n name: string;\n avatarUrl?: string;\n}\n\nexport interface LinkPreview {\n url: string;\n domain: string;\n title: string;\n description?: string;\n imageUrl?: string;\n}\n\nexport interface VideoMedia {\n thumbnailUrl: string;\n videoUrl?: string;\n}\n\nexport interface RepostContent {\n author: PostAuthor;\n date: string;\n content: string;\n images?: string[];\n}\n\nexport interface SocialFeedPost {\n id: string;\n author: PostAuthor;\n date: string;\n content: string;\n /** Image URLs for the post */\n images?: string[];\n /** Video media */\n video?: VideoMedia;\n /** Link preview card */\n linkPreview?: LinkPreview;\n /** Reposted/quoted content */\n repost?: RepostContent;\n likesCount: number;\n repliesCount: number;\n isLiked?: boolean;\n /** Nested replies */\n replies?: SocialFeedPost[];\n /** Whether this is a reply (for indentation) */\n isReply?: boolean;\n}\n\nexport interface SocialFeedProps {\n /** Section title */\n title?: string;\n /** Posts data */\n posts?: SocialFeedPost[];\n /** Current user for composer */\n currentUser?: PostAuthor;\n /** Placeholder text for composer */\n composerPlaceholder?: string;\n /** Image preview in composer */\n composerImagePreview?: string;\n /** Callback when post is submitted */\n onPost?: (content: string) => void;\n /** Callback when like is clicked */\n onLike?: (postId: string) => void;\n /** Callback when comment is clicked */\n onComment?: (postId: string) => void;\n /** Callback when repost is clicked */\n onRepost?: (postId: string) => void;\n /** Callback when share is clicked */\n onShare?: (postId: string) => void;\n /** Callback when menu is clicked */\n onMenuClick?: (postId: string) => void;\n /** Additional class names */\n className?: string;\n}\n\n// ============================================\n// Default Data\n// ============================================\n\nconst defaultCurrentUser: PostAuthor = {\n id: \"current\",\n name: \"You\",\n avatarUrl: \"https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=150&h=150&fit=crop&crop=face\",\n};\n\nconst defaultPosts: SocialFeedPost[] = [\n {\n id: \"1\",\n author: {\n id: \"raj\",\n name: \"Raj Mishra\",\n avatarUrl: \"https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=150&h=150&fit=crop&crop=face\",\n },\n date: \"Feb 23, 1:32 PM\",\n content: \"Thinking about traveling to Paris again!\",\n repost: {\n author: {\n id: \"jeffrey\",\n name: \"Jeffrey Connor\",\n avatarUrl: \"https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=150&h=150&fit=crop&crop=face\",\n },\n date: \"Nov 23, 5:34 PM\",\n content: \"What a place, the history, architecture and culture is wonderful. So many sites to see, one more amazing then the next. A must see if you are going to visit the great cities of the world.\",\n images: [\n \"https://images.unsplash.com/photo-1502602898657-3e91760cbb34?w=320&h=320&fit=crop\",\n \"https://images.unsplash.com/photo-1499856871958-5b9627545d1a?w=320&h=320&fit=crop\",\n ],\n },\n likesCount: 30,\n repliesCount: 10,\n isLiked: false,\n },\n {\n id: \"2\",\n author: {\n id: \"mary\",\n name: \"Mary Trott\",\n avatarUrl: \"https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=150&h=150&fit=crop&crop=face\",\n },\n date: \"Feb 23, 1:32 PM\",\n content: \"Learning how to Bubble\",\n video: {\n thumbnailUrl: \"https://images.unsplash.com/photo-1461749280684-dccba630e2f6?w=480&h=380&fit=crop\",\n },\n likesCount: 30,\n repliesCount: 10,\n isLiked: false,\n replies: [\n {\n id: \"2-reply-1\",\n author: {\n id: \"aya\",\n name: \"Aya Williams\",\n avatarUrl: \"https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=150&h=150&fit=crop&crop=face\",\n },\n date: \"Mar 12, 11:23 AM\",\n content: \"Check out these flight deals to Paris!\",\n linkPreview: {\n url: \"https://expedia.com/flights/paris\",\n domain: \"expedia.com\",\n title: \"Paris flights\",\n description: \"Your one-stop travel site for your dream vacation. Bundle your stay...\",\n imageUrl: \"https://images.unsplash.com/photo-1502602898657-3e91760cbb34?w=200&h=200&fit=crop\",\n },\n likesCount: 30,\n repliesCount: 10,\n isLiked: false,\n isReply: true,\n },\n ],\n },\n];\n\n// ============================================\n// Sub-components\n// ============================================\n\ninterface PostComposerProps {\n placeholder?: string;\n imagePreview?: string;\n onPost?: (content: string) => void;\n}\n\nfunction PostComposer({ placeholder = \"What's on your mind?\", imagePreview, onPost }: PostComposerProps) {\n const [content, setContent] = useState(\"\");\n\n const handlePost = () => {\n if (content.trim() || imagePreview) {\n onPost?.(content);\n setContent(\"\");\n }\n };\n\n return (\n <div\n className=\"flex flex-col w-full overflow-hidden\"\n style={{\n border: \"1px solid var(--canvas-border)\",\n boxShadow: \"0px 1px 8px 0px rgba(0,0,0,0.03)\",\n }}\n >\n {/* Text input area */}\n <div\n className=\"w-full\"\n style={{\n padding: \"var(--spacing-xl)\",\n background: \"var(--canvas-background)\",\n boxShadow: \"0px 1px 2px 0px rgba(0,0,0,0.02)\",\n }}\n >\n <textarea\n value={content}\n onChange={(e) => setContent(e.target.value)}\n placeholder={placeholder}\n className=\"w-full resize-none border-0 bg-transparent outline-none\"\n rows={2}\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: content ? \"var(--canvas-text)\" : \"var(--canvas-text-placeholder)\",\n }}\n />\n </div>\n\n {/* Image preview */}\n {imagePreview && (\n <div\n className=\"w-full\"\n style={{\n padding: \"0 var(--spacing-xl)\",\n background: \"var(--canvas-background)\",\n }}\n >\n <div\n className=\"overflow-hidden\"\n style={{\n width: 240,\n height: 180,\n borderRadius: \"var(--radius-md)\",\n border: \"1px solid var(--canvas-border)\",\n }}\n >\n <img\n src={imagePreview}\n alt=\"Preview\"\n className=\"w-full h-full object-cover\"\n />\n </div>\n </div>\n )}\n\n {/* Action bar */}\n <div\n className=\"flex items-center justify-between w-full\"\n style={{\n padding: \"var(--spacing-xl)\",\n background: \"var(--canvas-background)\",\n borderTop: \"1px solid var(--canvas-border)\",\n }}\n >\n <div className=\"flex items-center\" style={{ gap: \"var(--spacing-lg)\" }}>\n <button type=\"button\" style={{ color: \"var(--canvas-text)\" }}>\n <Paperclip className=\"w-5 h-5\" />\n </button>\n <button type=\"button\" style={{ color: \"var(--canvas-text)\" }}>\n <Video className=\"w-5 h-5\" />\n </button>\n <button type=\"button\" style={{ color: \"var(--canvas-text)\" }}>\n <Link2 className=\"w-5 h-5\" />\n </button>\n </div>\n <Button variant=\"primary\" size=\"sm\" onClick={handlePost}>\n Post\n </Button>\n </div>\n </div>\n );\n}\n\ninterface ActionIconsRowProps {\n isLiked?: boolean;\n onLike?: () => void;\n onComment?: () => void;\n onRepost?: () => void;\n onShare?: () => void;\n}\n\nfunction ActionIconsRow({ isLiked, onLike, onComment, onRepost, onShare }: ActionIconsRowProps) {\n return (\n <div\n className=\"flex items-center\"\n style={{ gap: \"var(--spacing-lg)\", padding: \"var(--spacing-xxs) 0\" }}\n >\n <button\n type=\"button\"\n onClick={onLike}\n style={{ color: isLiked ? \"var(--canvas-destructive)\" : \"var(--canvas-text)\" }}\n >\n <Heart\n className=\"w-5 h-5\"\n style={{\n fill: isLiked ? \"var(--canvas-destructive)\" : \"transparent\",\n }}\n />\n </button>\n <button type=\"button\" onClick={onComment} style={{ color: \"var(--canvas-text)\" }}>\n <MessageCircle className=\"w-5 h-5\" />\n </button>\n <button type=\"button\" onClick={onRepost} style={{ color: \"var(--canvas-text)\" }}>\n <RefreshCw className=\"w-5 h-5\" />\n </button>\n <button type=\"button\" onClick={onShare} style={{ color: \"var(--canvas-text)\" }}>\n <Send className=\"w-5 h-5\" />\n </button>\n </div>\n );\n}\n\ninterface StatsRowProps {\n likesCount: number;\n repliesCount: number;\n}\n\nfunction StatsRow({ likesCount, repliesCount }: StatsRowProps) {\n return (\n <div\n className=\"flex items-center\"\n style={{ gap: \"var(--spacing-xl)\", paddingTop: \"var(--spacing-xs)\" }}\n >\n <span\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 500,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text-placeholder)\",\n }}\n >\n {likesCount} likes\n </span>\n <span\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 500,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text-placeholder)\",\n }}\n >\n {repliesCount} replies\n </span>\n </div>\n );\n}\n\ninterface VideoThumbnailProps {\n thumbnailUrl: string;\n onClick?: () => void;\n}\n\nfunction VideoThumbnail({ thumbnailUrl, onClick }: VideoThumbnailProps) {\n return (\n <div\n className=\"relative overflow-hidden cursor-pointer\"\n style={{\n width: 480,\n height: 380,\n maxWidth: \"100%\",\n borderRadius: \"var(--radius-3xl)\",\n border: \"1px solid var(--canvas-border)\",\n }}\n onClick={onClick}\n >\n <img\n src={thumbnailUrl}\n alt=\"Video thumbnail\"\n className=\"w-full h-full object-cover\"\n />\n {/* Play button */}\n <div\n className=\"absolute inset-0 flex items-center justify-center\"\n >\n <div\n className=\"flex items-center justify-center\"\n style={{\n width: 128,\n height: 80,\n background: \"var(--canvas-destructive)\",\n borderRadius: \"var(--radius-2xl)\",\n }}\n >\n <Play className=\"w-12 h-12 text-white fill-white\" />\n </div>\n </div>\n </div>\n );\n}\n\ninterface LinkPreviewCardProps {\n linkPreview: LinkPreview;\n onClick?: () => void;\n}\n\nfunction LinkPreviewCard({ linkPreview, onClick }: LinkPreviewCardProps) {\n return (\n <div\n className=\"flex overflow-hidden cursor-pointer\"\n style={{\n width: 580,\n maxWidth: \"100%\",\n background: \"var(--canvas-background)\",\n border: \"1px solid var(--canvas-border)\",\n borderRadius: \"var(--radius-2xl)\",\n boxShadow: \"0px 1px 8px 0px rgba(0,0,0,0.03)\",\n }}\n onClick={onClick}\n >\n {linkPreview.imageUrl && (\n <div\n className=\"shrink-0 self-stretch overflow-hidden\"\n style={{\n width: 200,\n borderRight: \"1px solid var(--canvas-border)\",\n borderTopLeftRadius: \"var(--radius-md)\",\n borderBottomLeftRadius: \"var(--radius-md)\",\n }}\n >\n <img\n src={linkPreview.imageUrl}\n alt={linkPreview.title}\n className=\"w-full h-full object-cover\"\n />\n </div>\n )}\n <div\n className=\"flex flex-col flex-1\"\n style={{ padding: \"var(--spacing-4xl)\", gap: \"var(--spacing-lg)\" }}\n >\n <div className=\"flex flex-col\" style={{ gap: 0 }}>\n <span\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text-placeholder)\",\n }}\n >\n {linkPreview.domain}\n </span>\n <span\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 600,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n {linkPreview.title}\n </span>\n </div>\n {linkPreview.description && (\n <p\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text)\",\n margin: 0,\n }}\n >\n {linkPreview.description}\n </p>\n )}\n </div>\n </div>\n );\n}\n\ninterface RepostCardProps {\n repost: RepostContent;\n onLike?: () => void;\n onComment?: () => void;\n onRepost?: () => void;\n onShare?: () => void;\n}\n\nfunction RepostCard({ repost, onLike, onComment, onRepost, onShare }: RepostCardProps) {\n return (\n <div\n className=\"flex flex-col w-full\"\n style={{\n padding: \"var(--spacing-4xl)\",\n background: \"var(--canvas-background)\",\n border: \"1px solid var(--canvas-border)\",\n borderRadius: \"var(--radius-md)\",\n boxShadow: \"0px 1px 8px 0px rgba(0,0,0,0.03)\",\n gap: \"var(--spacing-xl)\",\n }}\n >\n {/* Author row */}\n <div className=\"flex items-start w-full\" style={{ gap: \"var(--spacing-xl)\" }}>\n <Avatar\n className=\"shrink-0\"\n style={{\n width: 48,\n height: 48,\n borderRadius: \"var(--spacing-3xl)\",\n border: \"1px solid var(--canvas-border)\",\n }}\n >\n <AvatarImage src={repost.author.avatarUrl} />\n <AvatarFallback>\n {repost.author.name.split(\" \").map(n => n[0]).join(\"\").slice(0, 2)}\n </AvatarFallback>\n </Avatar>\n <div className=\"flex flex-col flex-1\" style={{ gap: \"var(--spacing-lg)\" }}>\n <div className=\"flex items-center justify-between w-full\">\n <div className=\"flex flex-col\" style={{ gap: 0 }}>\n <span\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 600,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n {repost.author.name}\n </span>\n <span\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text-placeholder)\",\n }}\n >\n {repost.date}\n </span>\n </div>\n <button\n type=\"button\"\n className=\"flex items-center justify-center\"\n style={{\n width: 32,\n height: 32,\n borderRadius: \"var(--spacing-6xl)\",\n color: \"var(--canvas-text-placeholder)\",\n }}\n >\n <MoreHorizontal className=\"w-5 h-5\" />\n </button>\n </div>\n {/* Content */}\n <p\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text)\",\n margin: 0,\n }}\n >\n {repost.content}\n </p>\n {/* Images */}\n {repost.images && repost.images.length > 0 && (\n <div className=\"flex\" style={{ gap: \"var(--spacing-xl)\" }}>\n {repost.images.map((img, idx) => (\n <div\n key={idx}\n className=\"overflow-hidden\"\n style={{\n width: 320,\n height: 320,\n borderRadius: \"var(--radius-2xl)\",\n border: \"1px solid var(--canvas-border)\",\n }}\n >\n <img src={img} alt=\"\" className=\"w-full h-full object-cover\" />\n </div>\n ))}\n </div>\n )}\n {/* Actions */}\n <ActionIconsRow\n isLiked={false}\n onLike={onLike}\n onComment={onComment}\n onRepost={onRepost}\n onShare={onShare}\n />\n <StatsRow likesCount={30} repliesCount={10} />\n </div>\n </div>\n </div>\n );\n}\n\ninterface PostCellProps {\n post: SocialFeedPost;\n onLike?: () => void;\n onComment?: () => void;\n onRepost?: () => void;\n onShare?: () => void;\n onMenuClick?: () => void;\n}\n\nfunction PostCell({ post, onLike, onComment, onRepost, onShare, onMenuClick }: PostCellProps) {\n return (\n <div\n className=\"flex w-full\"\n style={{\n paddingLeft: post.isReply ? \"var(--spacing-7xl)\" : 0,\n paddingTop: post.isReply ? \"var(--spacing-3xl)\" : \"var(--spacing-xl)\",\n paddingBottom: post.isReply ? 0 : \"var(--spacing-3xl)\",\n borderBottom: post.isReply ? \"none\" : \"1px solid var(--canvas-border)\",\n gap: \"var(--spacing-xl)\",\n }}\n >\n {/* Avatar column with reply line */}\n <div className=\"flex flex-col items-center shrink-0\" style={{ gap: \"var(--spacing-md)\" }}>\n <Avatar\n style={{\n width: 48,\n height: 48,\n borderRadius: \"var(--spacing-3xl)\",\n border: \"1px solid var(--canvas-border)\",\n }}\n >\n <AvatarImage src={post.author.avatarUrl} />\n <AvatarFallback>\n {post.author.name.split(\" \").map(n => n[0]).join(\"\").slice(0, 2)}\n </AvatarFallback>\n </Avatar>\n {/* Reply line */}\n {post.replies && post.replies.length > 0 && (\n <div\n className=\"flex-1 w-px\"\n style={{ background: \"var(--canvas-border)\", minHeight: 20 }}\n />\n )}\n </div>\n\n {/* Content column */}\n <div className=\"flex flex-col flex-1 min-w-0\" style={{ gap: \"var(--spacing-lg)\" }}>\n {/* Header */}\n <div className=\"flex items-center justify-between w-full\">\n <div className=\"flex flex-col\" style={{ gap: 0 }}>\n <span\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 600,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n {post.author.name}\n </span>\n <span\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text-placeholder)\",\n }}\n >\n {post.date}\n </span>\n </div>\n <button\n type=\"button\"\n onClick={onMenuClick}\n className=\"flex items-center justify-center\"\n style={{\n width: 32,\n height: 32,\n borderRadius: \"var(--spacing-6xl)\",\n color: \"var(--canvas-text-placeholder)\",\n }}\n >\n <MoreHorizontal className=\"w-5 h-5\" />\n </button>\n </div>\n\n {/* Content text */}\n <p\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text)\",\n margin: 0,\n }}\n >\n {post.content}\n </p>\n\n {/* Repost card */}\n {post.repost && (\n <RepostCard\n repost={post.repost}\n onLike={onLike}\n onComment={onComment}\n onRepost={onRepost}\n onShare={onShare}\n />\n )}\n\n {/* Images */}\n {post.images && post.images.length > 0 && (\n <div className=\"flex\" style={{ gap: \"var(--spacing-xl)\" }}>\n {post.images.map((img, idx) => (\n <div\n key={idx}\n className=\"overflow-hidden\"\n style={{\n width: 320,\n height: 320,\n borderRadius: \"var(--radius-2xl)\",\n border: \"1px solid var(--canvas-border)\",\n }}\n >\n <img src={img} alt=\"\" className=\"w-full h-full object-cover\" />\n </div>\n ))}\n </div>\n )}\n\n {/* Video */}\n {post.video && (\n <VideoThumbnail thumbnailUrl={post.video.thumbnailUrl} />\n )}\n\n {/* Link preview */}\n {post.linkPreview && (\n <LinkPreviewCard linkPreview={post.linkPreview} />\n )}\n\n {/* Actions */}\n <ActionIconsRow\n isLiked={post.isLiked}\n onLike={onLike}\n onComment={onComment}\n onRepost={onRepost}\n onShare={onShare}\n />\n\n {/* Stats */}\n <StatsRow likesCount={post.likesCount} repliesCount={post.repliesCount} />\n\n {/* Nested replies */}\n {post.replies && post.replies.length > 0 && (\n <div className=\"flex flex-col w-full\">\n {post.replies.map((reply) => (\n <PostCell\n key={reply.id}\n post={reply}\n onLike={onLike}\n onComment={onComment}\n onRepost={onRepost}\n onShare={onShare}\n />\n ))}\n </div>\n )}\n </div>\n </div>\n );\n}\n\n// ============================================\n// Main Component\n// ============================================\n\n/**\n * Canvas Design System - Social Feed Block\n *\n * A social media-style feed component with post composer, posts with various\n * content types (text, images, video, reposts, link cards), social interactions,\n * and threaded replies.\n *\n * @example\n * ```tsx\n * <SocialFeed\n * title=\"Social Feed\"\n * posts={[...]}\n * onLike={(postId) => console.log(\"Liked\", postId)}\n * onPost={(content) => console.log(\"Posted\", content)}\n * />\n * ```\n */\nexport function SocialFeed({\n title = \"Social Feed\",\n posts = defaultPosts,\n currentUser = defaultCurrentUser,\n composerPlaceholder = \"What's on your mind?\",\n composerImagePreview,\n onPost,\n onLike,\n onComment,\n onRepost,\n onShare,\n onMenuClick,\n className,\n}: SocialFeedProps) {\n return (\n <div\n className={cn(\"flex flex-col w-full\", className)}\n style={{ gap: \"var(--spacing-xl)\" }}\n >\n {/* Header Section */}\n {title && (\n <div className=\"flex flex-wrap items-start w-full\" style={{ gap: \"var(--spacing-xl)\" }}>\n <div className=\"flex flex-col flex-1 min-w-[200px]\" style={{ gap: \"var(--spacing-xs)\" }}>\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\n {/* Feed content */}\n <div className=\"flex flex-col w-full overflow-hidden\">\n {/* First section: Composer + first set of posts */}\n <div\n className=\"flex flex-col w-full\"\n style={{\n borderBottom: \"1px solid var(--canvas-border)\",\n paddingBottom: \"var(--spacing-5xl)\",\n }}\n >\n {/* Post Composer */}\n <PostComposer\n placeholder={composerPlaceholder}\n imagePreview={composerImagePreview}\n onPost={onPost}\n />\n\n {/* Posts */}\n <div className=\"flex flex-col w-full\" style={{ paddingTop: \"var(--spacing-xl)\" }}>\n {posts.map((post) => (\n <PostCell\n key={post.id}\n post={post}\n onLike={() => onLike?.(post.id)}\n onComment={() => onComment?.(post.id)}\n onRepost={() => onRepost?.(post.id)}\n onShare={() => onShare?.(post.id)}\n onMenuClick={() => onMenuClick?.(post.id)}\n />\n ))}\n </div>\n </div>\n </div>\n </div>\n );\n}\n"
|
|
10
|
+
}
|
|
11
|
+
],
|
|
12
|
+
"dependencies": [
|
|
13
|
+
"lucide-react"
|
|
14
|
+
],
|
|
15
|
+
"registryDependencies": [
|
|
16
|
+
"utils",
|
|
17
|
+
"avatar",
|
|
18
|
+
"button"
|
|
19
|
+
]
|
|
20
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "social-proof",
|
|
3
|
+
"type": "registry:block",
|
|
4
|
+
"description": "Logo bar showing 'as featured on' brands.",
|
|
5
|
+
"files": [
|
|
6
|
+
{
|
|
7
|
+
"path": "components/blocks/marketing/social-proof.tsx",
|
|
8
|
+
"type": "registry:block",
|
|
9
|
+
"content": "\"use client\";\n\nimport { Typography } from \"../../ui/typography\";\n\ninterface SocialProofProps {\n label?: string;\n logos?: { name: string; width: number }[];\n}\n\n// Simple text-based logos for demonstration\nconst defaultLogos = [\n { name: \"Google\", width: 106 },\n { name: \"Quartz\", width: 100 },\n { name: \"The New York Times\", width: 180 },\n { name: \"Forbes\", width: 90 },\n { name: \"Entrepreneur\", width: 130 },\n];\n\nexport function SocialProof({ \n label = \"AS FEATURED ON\",\n logos = defaultLogos,\n}: SocialProofProps) {\n return (\n <section \n className=\"w-full px-4 md:px-8 lg:px-20 py-10\"\n style={{\n backgroundColor: \"var(--canvas-surface)\",\n }}\n >\n <div className=\"w-full max-w-[1200px] mx-auto flex flex-col gap-8 items-center\">\n <Typography variant=\"body-s\" as=\"p\" color=\"muted\" className=\"text-center\">\n {label}\n </Typography>\n \n <div className=\"flex flex-wrap items-center justify-center gap-8 md:gap-12\">\n {logos.map((logo) => (\n <div \n key={logo.name}\n className=\"flex items-center justify-center h-9 opacity-60 hover:opacity-100 transition-opacity\"\n style={{ minWidth: `${logo.width}px` }}\n >\n <Typography variant=\"body-l\" as=\"span\" color=\"muted\" style={{ fontWeight: 600, letterSpacing: \"-0.5px\" }}>\n {logo.name}\n </Typography>\n </div>\n ))}\n </div>\n </div>\n </section>\n );\n}\n"
|
|
10
|
+
}
|
|
11
|
+
],
|
|
12
|
+
"dependencies": [],
|
|
13
|
+
"registryDependencies": []
|
|
14
|
+
}
|