canvas-ui-sdk 0.3.23 → 4.0.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/README.md +25 -5
- package/dist/charts.js +11 -6
- package/dist/charts.js.map +1 -1
- package/dist/index.d.ts +1233 -153
- package/dist/index.js +3562 -447
- package/dist/index.js.map +1 -1
- package/mcp/dist/index.js +1195 -149
- package/package.json +1 -1
- package/prompts/.cursorrules +96 -0
- package/prompts/.windsurfrules +96 -0
- package/prompts/CLAUDE.md +22 -0
- package/prompts/copilot-instructions.md +96 -0
- package/registry/blocks/activity-feed.json +12 -1
- package/registry/blocks/blog-cards.json +10 -2
- package/registry/blocks/bottom-action-bar.json +27 -0
- package/registry/blocks/bottom-input-chat-widget.json +9 -1
- package/registry/blocks/category-grid.json +10 -2
- package/registry/blocks/centered-hero.json +9 -1
- package/registry/blocks/chat-message.json +8 -1
- package/registry/blocks/circular-progress-bar-list.json +11 -1
- package/registry/blocks/confirmation-popup.json +10 -1
- package/registry/blocks/contact-form-popup.json +10 -1
- package/registry/blocks/content-dropzone.json +8 -0
- package/registry/blocks/content-with-image.json +9 -1
- package/registry/blocks/core-values-grid.json +10 -2
- package/registry/blocks/credit-card-display.json +9 -1
- package/registry/blocks/cta-banner.json +10 -2
- package/registry/blocks/destination-cards.json +10 -1
- package/registry/blocks/detail-drawer.json +10 -1
- package/registry/blocks/details-popup.json +10 -1
- package/registry/blocks/editable-list.json +29 -0
- package/registry/blocks/empty-state.json +10 -2
- package/registry/blocks/faq-accordion.json +9 -1
- package/registry/blocks/faqs-table.json +10 -1
- package/registry/blocks/feature-with-image.json +9 -1
- package/registry/blocks/featured-news-cards.json +10 -2
- package/registry/blocks/featured-places.json +10 -2
- package/registry/blocks/features-comparison.json +9 -1
- package/registry/blocks/feedback-popup.json +9 -1
- package/registry/blocks/filter-popover.json +8 -1
- package/registry/blocks/fixed-column-data-table.json +11 -1
- package/registry/blocks/flair-banner.json +9 -1
- package/registry/blocks/footer-navbar.json +9 -1
- package/registry/blocks/form-group.json +14 -3
- package/registry/blocks/form-popup.json +31 -0
- package/registry/blocks/gallery-section.json +10 -2
- package/registry/blocks/gradient-banner.json +10 -2
- package/registry/blocks/graph-metric-tiles.json +1 -1
- package/registry/blocks/grid-tiles-list.json +10 -1
- package/registry/blocks/hero-dark-centered.json +9 -1
- package/registry/blocks/hero-dark-with-image.json +9 -1
- package/registry/blocks/hero-fullwidth-image.json +9 -1
- package/registry/blocks/hero-section.json +9 -1
- package/registry/blocks/how-it-works.json +9 -1
- package/registry/blocks/image-feed-with-nested-comments.json +10 -1
- package/registry/blocks/image-popup.json +10 -1
- package/registry/blocks/invoice-popup.json +10 -1
- package/registry/blocks/large-image-labels-list.json +10 -1
- package/registry/blocks/list-popup.json +28 -0
- package/registry/blocks/loader.json +9 -1
- package/registry/blocks/login-branding-panel.json +10 -2
- package/registry/blocks/menu-section.json +9 -1
- package/registry/blocks/menufocus-template.json +9 -1
- package/registry/blocks/messenger-sidebar.json +11 -2
- package/registry/blocks/metrics-section.json +10 -2
- package/registry/blocks/mobile-bottom-nav.json +10 -2
- package/registry/blocks/monthly-calendar-widget.json +9 -1
- package/registry/blocks/multistep-form-popup.json +34 -0
- package/registry/blocks/nested-comments-table.json +9 -1
- package/registry/blocks/nested-data-table.json +10 -1
- package/registry/blocks/nps-survey-popup.json +27 -0
- package/registry/blocks/office-locations.json +10 -2
- package/registry/blocks/order-summary-sidebar.json +27 -0
- package/registry/blocks/page-header-section.json +9 -1
- package/registry/blocks/pagination.json +8 -1
- package/registry/blocks/participant-list.json +9 -1
- package/registry/blocks/persona-card.json +10 -1
- package/registry/blocks/personalize-feed-popup.json +27 -0
- package/registry/blocks/pill-tabs.json +9 -1
- package/registry/blocks/place-detail-panel.json +11 -1
- package/registry/blocks/pricing-cards.json +10 -2
- package/registry/blocks/pricing-cta.json +9 -1
- package/registry/blocks/pricing-plans-popup.json +10 -1
- package/registry/blocks/profile-card.json +12 -2
- package/registry/blocks/profile-grid-tiles-list.json +10 -1
- package/registry/blocks/profile-image-uploader.json +9 -1
- package/registry/blocks/profile-info-cards.json +10 -1
- package/registry/blocks/progress-bar.json +8 -1
- package/registry/blocks/prompt-template.json +1 -1
- package/registry/blocks/purchase-confirmation-popup.json +10 -1
- package/registry/blocks/reservation-card.json +26 -0
- package/registry/blocks/reviews-grid.json +10 -2
- package/registry/blocks/reviews-table.json +10 -1
- package/registry/blocks/screen-prompt-template.json +1 -1
- package/registry/blocks/search-bar.json +9 -2
- package/registry/blocks/search-sidebar.json +9 -2
- package/registry/blocks/settings-list-row.json +9 -1
- package/registry/blocks/share-project-popup.json +36 -0
- package/registry/blocks/sidebar-cards.json +10 -2
- package/registry/blocks/sidebar-profile-card.json +10 -2
- package/registry/blocks/slideshow-grid-tiles.json +10 -2
- package/registry/blocks/slideshow-popup.json +10 -1
- package/registry/blocks/small-edit-popup.json +29 -0
- package/registry/blocks/social-feed.json +10 -1
- package/registry/blocks/social-proof.json +9 -1
- package/registry/blocks/standard-data-table.json +13 -1
- package/registry/blocks/standard-list-with-image.json +10 -1
- package/registry/blocks/step-tracker.json +9 -1
- package/registry/blocks/store-location-map.json +9 -1
- package/registry/blocks/team-cards-grid.json +9 -1
- package/registry/blocks/team-circular-grid.json +9 -1
- package/registry/blocks/terms-of-service-popup.json +10 -1
- package/registry/blocks/testimonial-carousel.json +10 -2
- package/registry/blocks/tile-image-gallery.json +26 -0
- package/registry/blocks/title-group.json +10 -1
- package/registry/blocks/upvoting-posts-table.json +10 -1
- package/registry/blocks/vertical-how-it-works.json +9 -1
- package/registry/blocks/vertical-step-tracker.json +9 -1
- package/registry/blocks/video-chat-controls.json +9 -1
- package/registry/blocks/video-content-section.json +9 -1
- package/registry/blocks/video-playlist.json +9 -1
- package/registry/blocks/video-popup.json +9 -1
- package/registry/blocks/view-profile-popup.json +10 -1
- package/registry/blocks/webcam-preview.json +9 -1
- package/registry/hooks/use-css-variable-sync.json +10 -1
- package/registry/hooks/use-mobile.json +9 -1
- package/registry/index.json +1526 -147
- package/registry/layout/account-settings-shell.json +10 -1
- package/registry/layout/dashboard-shell.json +12 -1
- package/registry/layout/double-sidebar-shell.json +11 -2
- package/registry/layout/double-sidebar.json +9 -1
- package/registry/layout/header.json +10 -1
- package/registry/layout/icon-sidebar-shell.json +9 -1
- package/registry/layout/icon-sidebar.json +9 -1
- package/registry/layout/mobile-menu-shell.json +10 -1
- package/registry/layout/multistep-progressbar-shell.json +9 -1
- package/registry/layout/multistep-shell.json +11 -1
- package/registry/layout/multistep-sidebar-shell.json +10 -2
- package/registry/layout/project-context-shell.json +1 -1
- package/registry/layout/search-bar-shell.json +8 -1
- package/registry/layout/sidebar-nav.json +7 -1
- package/registry/layout/sidebar.json +9 -2
- package/registry/layout/standard-page-shell.json +10 -1
- package/registry/layout/vertical-multistep-shell.json +10 -1
- package/registry/ui/avatar.json +9 -1
- package/registry/ui/button.json +9 -1
- package/registry/ui/calendar.json +9 -1
- package/registry/ui/checkbox.json +8 -1
- package/registry/ui/date-input.json +9 -1
- package/registry/ui/dialog.json +8 -1
- package/registry/ui/dropdown-menu.json +8 -1
- package/registry/ui/file-uploader.json +9 -1
- package/registry/ui/image-uploader.json +9 -1
- package/registry/ui/input.json +8 -1
- package/registry/ui/label.json +8 -1
- package/registry/ui/line-tabs.json +9 -1
- package/registry/ui/multiselect-checkbox-field.json +9 -1
- package/registry/ui/multiselect-tags.json +9 -1
- package/registry/ui/popover.json +8 -1
- package/registry/ui/radio-group.json +9 -1
- package/registry/ui/range-input.json +8 -1
- package/registry/ui/scroll-area.json +8 -1
- package/registry/ui/searchbox.json +9 -1
- package/registry/ui/select.json +9 -1
- package/registry/ui/selectable-pills.json +11 -1
- package/registry/ui/separator.json +8 -1
- package/registry/ui/sheet.json +9 -1
- package/registry/ui/sidebar.json +8 -1
- package/registry/ui/skeleton.json +8 -1
- package/registry/ui/slider.json +10 -2
- package/registry/ui/switch.json +9 -1
- package/registry/ui/tabs.json +8 -1
- package/registry/ui/text-input.json +8 -1
- package/registry/ui/textarea.json +9 -1
- package/registry/ui/tooltip.json +8 -1
- package/registry/ui/typography.json +9 -1
- package/styles/tokens.reference.css +21 -0
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "reviews-table",
|
|
3
3
|
"type": "registry:block",
|
|
4
|
-
"description": "
|
|
4
|
+
"description": "Vertical list of review entries with user avatar, star rating (1-5), date, and expandable review text. Header with sort/filter controls and action button. Full-width block (~400-600px). Use for product reviews, testimonials, feedback lists, or any user-submitted ratings.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"reviews",
|
|
7
|
+
"ratings",
|
|
8
|
+
"stars",
|
|
9
|
+
"testimonials",
|
|
10
|
+
"feedback",
|
|
11
|
+
"comments"
|
|
12
|
+
],
|
|
13
|
+
"visualWeight": "heavy",
|
|
5
14
|
"files": [
|
|
6
15
|
{
|
|
7
16
|
"path": "components/blocks/reviews-table.tsx",
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
{
|
|
7
7
|
"path": "components/blocks/screen-prompt-template.tsx",
|
|
8
8
|
"type": "registry:block",
|
|
9
|
-
"content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { Check, Copy, Sparkles, FileBox, GitBranch } from \"lucide-react\";\nimport { cn } from \"../../lib/utils\";\nimport { promptTemplates } from \"../../data/prompt-templates\";\n\ntype ScreenPromptType = \"single\" | \"flow\";\n\nconst promptTypeConfig = {\n single: {\n icon: FileBox,\n label: \"Single Screen\",\n description: \"Add one screen at a time\",\n prompt: promptTemplates.screens.single,\n },\n flow: {\n icon: GitBranch,\n label: \"Screen Flow\",\n description: \"Create a multi-screen user flow\",\n prompt: promptTemplates.screens.flow,\n },\n};\n\nexport function ScreenPromptTemplate() {\n const [promptType, setPromptType] = useState<ScreenPromptType>(\"single\");\n const [copied, setCopied] = useState(false);\n\n const currentConfig = promptTypeConfig[promptType];\n\n const handleCopy = async () => {\n await navigator.clipboard.writeText(currentConfig.prompt);\n setCopied(true);\n setTimeout(() => setCopied(false), 2000);\n };\n\n return (\n <div className=\"rounded-lg border border-dashed border-[var(--canvas-border)] bg-[var(--canvas-surface)] p-4\">\n {/* Type Toggle */}\n <div className=\"flex flex-wrap gap-2 mb-4\">\n {(Object.entries(promptTypeConfig) as [ScreenPromptType, (typeof promptTypeConfig)[ScreenPromptType]][]).map(\n ([type, config]) => {\n const Icon = config.icon;\n const isActive = promptType === type;\n \n return (\n <button\n key={type}\n onClick={() => setPromptType(type)}\n className={cn(\n \"cursor-pointer flex items-center gap-2 px-4 py-2 rounded-lg font-medium transition-all\",\n isActive\n ? \"bg-[var(--canvas-primary)] text-[var(--canvas-primary-foreground)] shadow-sm\"\n : \"bg-[var(--canvas-background)] text-[var(--canvas-text-muted)] border border-[var(--canvas-border)] hover:border-[var(--canvas-primary)] hover:text-[var(--canvas-primary)]\"\n )}\n style={{ fontSize: \"var(--typo-body-s-size)\" }}\n >\n <Icon className=\"size-4\" />\n {config.label}\n </button>\n );\n }\n )}\n </div>\n\n {/* Description */}\n <p className=\"text-[var(--canvas-text-muted)] mb-3\" style={{ fontSize: \"var(--typo-body-xs-size)\" }}>\n {currentConfig.description}\n </p>\n\n {/* Divider */}\n <div className=\"border-t border-[var(--canvas-border)] my-4\" />\n\n {/* Header */}\n <div className=\"flex items-center justify-between mb-3\">\n <div className=\"flex items-center gap-2 font-medium text-[var(--canvas-text-muted)]\" style={{ fontSize: \"var(--typo-body-s-size)\" }}>\n <Sparkles className=\"size-4 text-[var(--canvas-primary)]\" />\n Generate with
|
|
9
|
+
"content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { Check, Copy, Sparkles, FileBox, GitBranch } from \"lucide-react\";\nimport { cn } from \"../../lib/utils\";\nimport { promptTemplates } from \"../../data/prompt-templates\";\n\ntype ScreenPromptType = \"single\" | \"flow\";\n\nconst promptTypeConfig = {\n single: {\n icon: FileBox,\n label: \"Single Screen\",\n description: \"Add one screen at a time\",\n prompt: promptTemplates.screens.single,\n },\n flow: {\n icon: GitBranch,\n label: \"Screen Flow\",\n description: \"Create a multi-screen user flow\",\n prompt: promptTemplates.screens.flow,\n },\n};\n\nexport function ScreenPromptTemplate() {\n const [promptType, setPromptType] = useState<ScreenPromptType>(\"single\");\n const [copied, setCopied] = useState(false);\n\n const currentConfig = promptTypeConfig[promptType];\n\n const handleCopy = async () => {\n await navigator.clipboard.writeText(currentConfig.prompt);\n setCopied(true);\n setTimeout(() => setCopied(false), 2000);\n };\n\n return (\n <div className=\"rounded-lg border border-dashed border-[var(--canvas-border)] bg-[var(--canvas-surface)] p-4\">\n {/* Type Toggle */}\n <div className=\"flex flex-wrap gap-2 mb-4\">\n {(Object.entries(promptTypeConfig) as [ScreenPromptType, (typeof promptTypeConfig)[ScreenPromptType]][]).map(\n ([type, config]) => {\n const Icon = config.icon;\n const isActive = promptType === type;\n \n return (\n <button\n key={type}\n onClick={() => setPromptType(type)}\n className={cn(\n \"cursor-pointer flex items-center gap-2 px-4 py-2 rounded-lg font-medium transition-all\",\n isActive\n ? \"bg-[var(--canvas-primary)] text-[var(--canvas-primary-foreground)] shadow-sm\"\n : \"bg-[var(--canvas-background)] text-[var(--canvas-text-muted)] border border-[var(--canvas-border)] hover:border-[var(--canvas-primary)] hover:text-[var(--canvas-primary)]\"\n )}\n style={{ fontSize: \"var(--typo-body-s-size)\" }}\n >\n <Icon className=\"size-4\" />\n {config.label}\n </button>\n );\n }\n )}\n </div>\n\n {/* Description */}\n <p className=\"text-[var(--canvas-text-muted)] mb-3\" style={{ fontSize: \"var(--typo-body-xs-size)\" }}>\n {currentConfig.description}\n </p>\n\n {/* Divider */}\n <div className=\"border-t border-[var(--canvas-border)] my-4\" />\n\n {/* Header */}\n <div className=\"flex items-center justify-between mb-3\">\n <div className=\"flex items-center gap-2 font-medium text-[var(--canvas-text-muted)]\" style={{ fontSize: \"var(--typo-body-s-size)\" }}>\n <Sparkles className=\"size-4 text-[var(--canvas-primary)]\" />\n Generate with AI\n </div>\n <button\n onClick={handleCopy}\n className={cn(\n \"cursor-pointer flex items-center gap-1.5 px-2.5 py-1.5 rounded-md font-medium transition-all\",\n copied\n ? \"bg-[var(--canvas-success-surface)] text-[var(--canvas-success)]\"\n : \"bg-[var(--canvas-background)] text-[var(--canvas-text-muted)] hover:text-[var(--canvas-text)] border border-[var(--canvas-border)] hover:border-[var(--canvas-primary)]\"\n )}\n style={{ fontSize: \"var(--typo-body-xs-size)\" }}\n >\n {copied ? (\n <>\n <Check className=\"size-3\" />\n Copied!\n </>\n ) : (\n <>\n <Copy className=\"size-3\" />\n Copy prompt\n </>\n )}\n </button>\n </div>\n\n {/* Prompt text */}\n <pre className=\"text-[var(--canvas-text)] leading-relaxed font-mono whitespace-pre-wrap bg-[var(--canvas-background)] rounded-md p-3 border border-[var(--canvas-border)] max-h-[300px] overflow-y-auto\" style={{ fontSize: \"var(--typo-body-s-size)\" }}>\n {currentConfig.prompt}\n </pre>\n </div>\n );\n}\n"
|
|
10
10
|
}
|
|
11
11
|
],
|
|
12
12
|
"dependencies": [
|
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "search-bar",
|
|
3
3
|
"type": "registry:block",
|
|
4
|
-
"description": "Prominent search input with icon.",
|
|
4
|
+
"description": "Prominent search input with search icon. Use for search-focused interfaces like documentation, directories, or knowledge bases.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"search",
|
|
7
|
+
"input",
|
|
8
|
+
"find",
|
|
9
|
+
"query"
|
|
10
|
+
],
|
|
11
|
+
"visualWeight": "light",
|
|
5
12
|
"files": [
|
|
6
13
|
{
|
|
7
14
|
"path": "components/blocks/search-bar.tsx",
|
|
8
15
|
"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-[var(--canvas-background)] 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"
|
|
16
|
+
"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-[var(--canvas-background)] 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)\", fontFamily: \"var(--typo-global-font)\" }}\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
17
|
}
|
|
11
18
|
],
|
|
12
19
|
"dependencies": [
|
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "search-sidebar",
|
|
3
3
|
"type": "registry:block",
|
|
4
|
-
"description": "Sidebar with search results or
|
|
4
|
+
"description": "Sidebar panel with search results list or filter controls. Use alongside main content for search-driven interfaces.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"search",
|
|
7
|
+
"sidebar",
|
|
8
|
+
"results",
|
|
9
|
+
"filter"
|
|
10
|
+
],
|
|
11
|
+
"visualWeight": "medium",
|
|
5
12
|
"files": [
|
|
6
13
|
{
|
|
7
14
|
"path": "components/blocks/search-sidebar.tsx",
|
|
8
15
|
"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"
|
|
16
|
+
"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)\", fontFamily: \"var(--typo-global-font)\" }}\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)\", fontFamily: \"var(--typo-global-font)\" }}\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)\", fontFamily: \"var(--typo-global-font)\" }}\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
17
|
}
|
|
11
18
|
],
|
|
12
19
|
"dependencies": [
|
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "settings-list-row",
|
|
3
3
|
"type": "registry:block",
|
|
4
|
-
"description": "
|
|
4
|
+
"description": "Single row item for settings pages with label on left, current value in center, and edit button on right. Use for user profile fields, account settings, or any editable key-value setting.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"settings",
|
|
7
|
+
"row",
|
|
8
|
+
"editable",
|
|
9
|
+
"preference",
|
|
10
|
+
"key-value"
|
|
11
|
+
],
|
|
12
|
+
"visualWeight": "light",
|
|
5
13
|
"files": [
|
|
6
14
|
{
|
|
7
15
|
"path": "components/blocks/settings-list-row.tsx",
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "share-project-popup",
|
|
3
|
+
"type": "registry:block",
|
|
4
|
+
"description": "Project sharing modal with visibility dropdown, URL copy field, user invite select, and people-with-access list with role dropdowns and enable/disable toggles. Centered dialog (~450px tall). Use for sharing settings, collaboration invites, or permission management.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"share",
|
|
7
|
+
"invite",
|
|
8
|
+
"modal",
|
|
9
|
+
"collaboration",
|
|
10
|
+
"permissions",
|
|
11
|
+
"access"
|
|
12
|
+
],
|
|
13
|
+
"visualWeight": "medium",
|
|
14
|
+
"files": [
|
|
15
|
+
{
|
|
16
|
+
"path": "components/blocks/share-project-popup.tsx",
|
|
17
|
+
"type": "registry:block",
|
|
18
|
+
"content": "\"use client\";\n\nimport React, { useState, useEffect } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport {\n Dialog,\n DialogContent,\n DialogTitle,\n DialogDescription,\n} from \"../ui/dialog\";\nimport { Button } from \"../ui/button\";\nimport { Label } from \"../ui/label\";\nimport { Input } from \"../ui/input\";\nimport {\n Select,\n SelectTrigger,\n SelectContent,\n SelectItem,\n SelectValue,\n} from \"../ui/select\";\nimport {\n Popover,\n PopoverTrigger,\n PopoverContent,\n} from \"../ui/popover\";\nimport { Switch } from \"../ui/switch\";\nimport { Avatar, AvatarImage, AvatarFallback } from \"../ui/avatar\";\nimport { Copy, Check, ChevronDown, Globe } from \"lucide-react\";\nimport {\n AVATAR_MARCUS_WEBB,\n AVATAR_SARAH_CHEN,\n AVATAR_MAYA_JOHNSON,\n AVATAR_LILY_TRAN,\n} from \"./demo-avatars\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface ShareRole {\n /** Unique role identifier */\n id: string;\n /** Display label */\n label: string;\n /** Description shown in the role popover */\n description: string;\n}\n\nexport interface SharedPerson {\n /** Unique person identifier */\n id: string;\n /** Display name */\n name: string;\n /** Avatar image URL or base64 */\n avatarUrl?: string;\n /** Avatar fallback initials */\n avatarFallback?: string;\n /** Current role id — must match a ShareRole.id */\n role: string;\n /** Whether access is enabled */\n enabled: boolean;\n}\n\nexport interface ShareProjectPopupProps {\n /** Controls dialog visibility */\n open?: boolean;\n /** Callback when dialog open state changes */\n onOpenChange?: (open: boolean) => void;\n /** Popup title */\n title?: string;\n /** Subtitle description */\n description?: string;\n /** Visibility options for the dropdown */\n visibilityOptions?: { id: string; label: string }[];\n /** Default selected visibility option id */\n defaultVisibility?: string;\n /** The shareable URL */\n shareUrl?: string;\n /** Invite user options for the select */\n inviteOptions?: { id: string; label: string }[];\n /** Invite button label */\n inviteLabel?: string;\n /** Role definitions for the dropdown */\n roles?: ShareRole[];\n /** People with access */\n people?: SharedPerson[];\n /** Callback when invite button is clicked */\n onInvite?: (userId: string) => void;\n /** Callback when a person's role changes */\n onRoleChange?: (personId: string, roleId: string) => void;\n /** Callback when a person's toggle changes */\n onToggle?: (personId: string, enabled: boolean) => void;\n /** Callback when the URL is copied */\n onCopyLink?: () => void;\n /** Additional class names for the dialog content */\n className?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Defaults\n// ---------------------------------------------------------------------------\n\nconst DEFAULT_ROLES: ShareRole[] = [\n {\n id: \"full\",\n label: \"Full\",\n description: \"Can view, edit, and manage all aspects of the project.\",\n },\n {\n id: \"edit\",\n label: \"Edit\",\n description: \"Can view and edit content, but cannot manage settings.\",\n },\n {\n id: \"comment\",\n label: \"Comment\",\n description: \"Can view and leave comments, but cannot edit content.\",\n },\n {\n id: \"view\",\n label: \"View only\",\n description: \"Can view content only. No editing or commenting.\",\n },\n];\n\nconst DEFAULT_VISIBILITY_OPTIONS = [\n { id: \"public\", label: \"Public\" },\n { id: \"private\", label: \"Private\" },\n { id: \"team\", label: \"Team only\" },\n];\n\nconst DEFAULT_INVITE_OPTIONS = [\n { id: \"user-1\", label: \"Alex Rivera\" },\n { id: \"user-2\", label: \"Morgan Lee\" },\n { id: \"user-3\", label: \"Casey Jordan\" },\n];\n\nconst DEFAULT_PEOPLE: SharedPerson[] = [\n {\n id: \"p1\",\n name: \"John Connor\",\n avatarUrl: AVATAR_MARCUS_WEBB,\n avatarFallback: \"JC\",\n role: \"full\",\n enabled: true,\n },\n {\n id: \"p2\",\n name: \"Raj Mishra\",\n avatarUrl: AVATAR_SARAH_CHEN,\n avatarFallback: \"RM\",\n role: \"edit\",\n enabled: true,\n },\n {\n id: \"p3\",\n name: \"Mary Trott\",\n avatarUrl: AVATAR_MAYA_JOHNSON,\n avatarFallback: \"MT\",\n role: \"comment\",\n enabled: true,\n },\n {\n id: \"p4\",\n name: \"Lily Sun\",\n avatarUrl: AVATAR_LILY_TRAN,\n avatarFallback: \"LS\",\n role: \"view\",\n enabled: false,\n },\n];\n\n// ---------------------------------------------------------------------------\n// Role Dropdown (uses Popover for rich options)\n// ---------------------------------------------------------------------------\n\nfunction RoleDropdown({\n roles,\n currentRole,\n onRoleChange,\n}: {\n roles: ShareRole[];\n currentRole: string;\n onRoleChange: (roleId: string) => void;\n}) {\n const [open, setOpen] = useState(false);\n const current = roles.find((r) => r.id === currentRole);\n\n return (\n <Popover open={open} onOpenChange={setOpen}>\n <PopoverTrigger asChild>\n <button\n className=\"flex items-center cursor-pointer shrink-0\"\n style={{\n gap: \"var(--spacing-xs)\",\n padding: \"var(--spacing-xs) var(--spacing-sm)\",\n borderRadius: \"var(--radius-sm)\",\n border: \"1px solid var(--canvas-border)\",\n background: \"var(--canvas-background)\",\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n color: \"var(--canvas-text)\",\n }}\n >\n {current?.label ?? currentRole}\n <ChevronDown\n className=\"shrink-0\"\n style={{\n width: 14,\n height: 14,\n color: \"var(--canvas-text-muted)\",\n }}\n />\n </button>\n </PopoverTrigger>\n <PopoverContent\n align=\"end\"\n sideOffset={4}\n className=\"p-0 w-[260px]\"\n style={{ backgroundColor: \"var(--canvas-background)\" }}\n >\n <div className=\"flex flex-col\">\n {roles.map((role, idx) => {\n const isSelected = role.id === currentRole;\n return (\n <button\n key={role.id}\n className=\"flex items-start text-left cursor-pointer w-full\"\n style={{\n padding: \"var(--spacing-lg) var(--spacing-xl)\",\n gap: \"var(--spacing-lg)\",\n background: \"none\",\n border: \"none\",\n borderBottom:\n idx < roles.length - 1\n ? \"1px solid var(--canvas-border)\"\n : \"none\",\n }}\n onClick={() => {\n onRoleChange(role.id);\n setOpen(false);\n }}\n >\n {/* Checkmark area */}\n <div\n className=\"shrink-0 flex items-center justify-center\"\n style={{ width: 16, height: 20 }}\n >\n {isSelected && (\n <Check\n style={{\n width: 16,\n height: 16,\n color: \"var(--canvas-primary)\",\n }}\n />\n )}\n </div>\n\n {/* Label & description */}\n <div className=\"flex flex-col min-w-0\">\n <span\n style={{\n fontFamily:\n \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 600,\n lineHeight: \"20px\",\n color: \"var(--canvas-text)\",\n }}\n >\n {role.label}\n </span>\n <span\n style={{\n fontFamily:\n \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-xs-size)\",\n lineHeight: \"18px\",\n color: \"var(--canvas-text-muted)\",\n }}\n >\n {role.description}\n </span>\n </div>\n </button>\n );\n })}\n </div>\n </PopoverContent>\n </Popover>\n );\n}\n\n// ---------------------------------------------------------------------------\n// Person Row\n// ---------------------------------------------------------------------------\n\nfunction PersonRow({\n person,\n roles,\n onRoleChange,\n onToggle,\n}: {\n person: SharedPerson;\n roles: ShareRole[];\n onRoleChange: (roleId: string) => void;\n onToggle: (enabled: boolean) => void;\n}) {\n return (\n <div\n className=\"flex items-center\"\n style={{\n gap: \"var(--spacing-lg)\",\n paddingTop: \"var(--spacing-lg)\",\n paddingBottom: \"var(--spacing-lg)\",\n }}\n >\n {/* Avatar + Name */}\n <Avatar className=\"size-8 shrink-0\">\n {person.avatarUrl && (\n <AvatarImage src={person.avatarUrl} alt={person.name} />\n )}\n <AvatarFallback\n className=\"text-xs font-medium bg-[var(--canvas-surface)] text-[var(--canvas-text-muted)]\"\n >\n {person.avatarFallback ?? person.name.slice(0, 2).toUpperCase()}\n </AvatarFallback>\n </Avatar>\n\n <span\n className=\"flex-1 min-w-0 truncate\"\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n lineHeight: \"20px\",\n fontWeight: 500,\n color: \"var(--canvas-text)\",\n }}\n >\n {person.name}\n </span>\n\n {/* Role dropdown */}\n <RoleDropdown\n roles={roles}\n currentRole={person.role}\n onRoleChange={onRoleChange}\n />\n\n {/* Toggle switch */}\n <Switch\n checked={person.enabled}\n onCheckedChange={onToggle}\n />\n </div>\n );\n}\n\n// ---------------------------------------------------------------------------\n// ShareProjectPopup\n// ---------------------------------------------------------------------------\n\nexport function ShareProjectPopup({\n open,\n onOpenChange,\n title = \"Share this project\",\n description = \"Manage access and permissions for your project.\",\n visibilityOptions = DEFAULT_VISIBILITY_OPTIONS,\n defaultVisibility = \"public\",\n shareUrl = \"https://airdev.co/project/23945\",\n inviteOptions = DEFAULT_INVITE_OPTIONS,\n inviteLabel = \"Invite user\",\n roles = DEFAULT_ROLES,\n people: peopleProp = DEFAULT_PEOPLE,\n onInvite,\n onRoleChange,\n onToggle,\n onCopyLink,\n className,\n}: ShareProjectPopupProps) {\n const [copied, setCopied] = useState(false);\n const [people, setPeople] = useState(peopleProp);\n const [visibility, setVisibility] = useState(defaultVisibility);\n const [selectedInvite, setSelectedInvite] = useState(\"\");\n\n // Sync when prop changes\n useEffect(() => {\n setPeople(peopleProp);\n }, [peopleProp]);\n\n // Reset copied state on close\n useEffect(() => {\n if (!open) {\n setCopied(false);\n setSelectedInvite(\"\");\n }\n }, [open]);\n\n const handleCopy = async () => {\n await navigator.clipboard.writeText(shareUrl);\n setCopied(true);\n onCopyLink?.();\n setTimeout(() => setCopied(false), 2000);\n };\n\n const handleRoleChange = (personId: string, roleId: string) => {\n setPeople((prev) =>\n prev.map((p) => (p.id === personId ? { ...p, role: roleId } : p))\n );\n onRoleChange?.(personId, roleId);\n };\n\n const handleToggle = (personId: string, enabled: boolean) => {\n setPeople((prev) =>\n prev.map((p) => (p.id === personId ? { ...p, enabled } : p))\n );\n onToggle?.(personId, enabled);\n };\n\n const handleInvite = () => {\n if (selectedInvite) {\n onInvite?.(selectedInvite);\n setSelectedInvite(\"\");\n }\n };\n\n return (\n <Dialog open={open} onOpenChange={onOpenChange}>\n <DialogContent\n className={cn(\n \"p-0 gap-0 overflow-hidden\",\n \"rounded-[var(--radius-xl)]\",\n \"shadow-[0px_4px_24px_0px_rgba(0,0,0,0.1)]\",\n \"sm:max-w-[576px]\",\n className\n )}\n showCloseButton\n >\n <div\n className=\"flex flex-col\"\n style={{\n padding: \"var(--spacing-4xl)\",\n gap: \"var(--spacing-3xl)\",\n }}\n >\n {/* Title & Description */}\n <div className=\"flex flex-col\" style={{ gap: \"var(--spacing-xs)\" }}>\n <DialogTitle\n style={{\n fontFamily: \"var(--typo-h6-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-h6-size)\",\n lineHeight: \"var(--typo-h6-line-height)\",\n fontWeight: 600,\n color: \"var(--canvas-text)\",\n }}\n >\n {title}\n </DialogTitle>\n <DialogDescription\n style={{\n fontFamily: \"var(--typo-body-m-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-m-size)\",\n lineHeight: \"var(--typo-body-m-line-height)\",\n color: \"var(--canvas-text-muted)\",\n }}\n >\n {description}\n </DialogDescription>\n </div>\n\n {/* Visibility Section */}\n <div className=\"flex flex-col\" style={{ gap: \"var(--spacing-md)\" }}>\n <Label>Visibility</Label>\n <div\n className=\"flex items-center\"\n style={{ gap: \"var(--spacing-lg)\" }}\n >\n <div className=\"shrink-0\">\n <Select value={visibility} onValueChange={setVisibility}>\n <SelectTrigger\n className=\"w-[140px]\"\n style={{ height: \"var(--input-standard-height)\" }}\n >\n <div\n className=\"flex items-center\"\n style={{ gap: \"var(--spacing-sm)\" }}\n >\n <Globe\n style={{\n width: 14,\n height: 14,\n color: \"var(--canvas-text-muted)\",\n }}\n />\n <SelectValue />\n </div>\n </SelectTrigger>\n <SelectContent>\n {visibilityOptions.map((opt) => (\n <SelectItem key={opt.id} value={opt.id}>\n {opt.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n\n {/* URL + copy */}\n <div\n className=\"flex items-center flex-1 min-w-0\"\n style={{\n height: \"var(--input-standard-height)\",\n border: \"1px solid var(--canvas-border)\",\n borderRadius: \"var(--input-standard-radius)\",\n backgroundColor: \"var(--canvas-background)\",\n overflow: \"hidden\",\n }}\n >\n <Input\n readOnly\n value={shareUrl}\n className=\"border-0 shadow-none flex-1 min-w-0\"\n style={{\n height: \"100%\",\n fontSize: \"var(--typo-body-s-size)\",\n }}\n />\n <button\n onClick={handleCopy}\n className=\"shrink-0 flex items-center justify-center cursor-pointer\"\n style={{\n width: 36,\n height: \"100%\",\n background: \"none\",\n border: \"none\",\n borderLeft: \"1px solid var(--canvas-border)\",\n color: copied\n ? \"var(--canvas-primary)\"\n : \"var(--canvas-text-muted)\",\n }}\n >\n {copied ? (\n <Check style={{ width: 14, height: 14 }} />\n ) : (\n <Copy style={{ width: 14, height: 14 }} />\n )}\n </button>\n </div>\n </div>\n </div>\n\n {/* Invite Section */}\n <div className=\"flex flex-col\" style={{ gap: \"var(--spacing-md)\" }}>\n <Label>Invite a user</Label>\n <div\n className=\"flex items-center\"\n style={{ gap: \"var(--spacing-lg)\" }}\n >\n <div className=\"flex-1\">\n <Select\n value={selectedInvite}\n onValueChange={setSelectedInvite}\n >\n <SelectTrigger\n style={{ height: \"var(--input-standard-height)\" }}\n >\n <SelectValue placeholder=\"Search for a user...\" />\n </SelectTrigger>\n <SelectContent>\n {inviteOptions.map((opt) => (\n <SelectItem key={opt.id} value={opt.id}>\n {opt.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n <Button\n variant=\"primary\"\n onClick={handleInvite}\n disabled={!selectedInvite}\n >\n {inviteLabel}\n </Button>\n </div>\n </div>\n\n {/* People with access */}\n <div className=\"flex flex-col\" style={{ gap: \"var(--spacing-md)\" }}>\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: \"20px\",\n color: \"var(--canvas-text)\",\n }}\n >\n People with access ({people.length})\n </span>\n\n <div\n className=\"flex flex-col\"\n style={{\n borderTop: \"1px solid var(--canvas-border)\",\n }}\n >\n {people.map((person) => (\n <div\n key={person.id}\n style={{\n borderBottom: \"1px solid var(--canvas-border)\",\n }}\n >\n <PersonRow\n person={person}\n roles={roles}\n onRoleChange={(roleId) =>\n handleRoleChange(person.id, roleId)\n }\n onToggle={(enabled) =>\n handleToggle(person.id, enabled)\n }\n />\n </div>\n ))}\n </div>\n </div>\n </div>\n </DialogContent>\n </Dialog>\n );\n}\n"
|
|
19
|
+
}
|
|
20
|
+
],
|
|
21
|
+
"dependencies": [
|
|
22
|
+
"lucide-react"
|
|
23
|
+
],
|
|
24
|
+
"registryDependencies": [
|
|
25
|
+
"lib/utils",
|
|
26
|
+
"ui/dialog",
|
|
27
|
+
"ui/button",
|
|
28
|
+
"ui/label",
|
|
29
|
+
"ui/input",
|
|
30
|
+
"ui/select",
|
|
31
|
+
"ui/popover",
|
|
32
|
+
"ui/switch",
|
|
33
|
+
"ui/avatar",
|
|
34
|
+
"blocks/demo-avatars"
|
|
35
|
+
]
|
|
36
|
+
}
|
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sidebar-cards",
|
|
3
3
|
"type": "registry:block",
|
|
4
|
-
"description": "
|
|
4
|
+
"description": "Vertical list of card items for sidebar placement, each with thumbnail image and title. Use for related content, recommended items, or sidebar navigation with visual previews.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"sidebar",
|
|
7
|
+
"cards",
|
|
8
|
+
"thumbnails",
|
|
9
|
+
"related",
|
|
10
|
+
"recommended"
|
|
11
|
+
],
|
|
12
|
+
"visualWeight": "medium",
|
|
5
13
|
"files": [
|
|
6
14
|
{
|
|
7
15
|
"path": "components/blocks/sidebar-cards.tsx",
|
|
8
16
|
"type": "registry:block",
|
|
9
|
-
"content": "\"use client\";\n\nimport { cn } from \"../../lib/utils\";\nimport { Mail, Phone, MessageCircle, User, LucideIcon } from \"lucide-react\";\n\n// Shared card styling\nconst cardClassName = cn(\n \"bg-[var(--canvas-background)] border border-[var(--canvas-border)]\",\n \"rounded-xl p-8\"\n);\n\n// ============================================================================\n// InfoCard\n// ============================================================================\n\nexport interface InfoCardProps {\n /** Card title (displayed as uppercase header) */\n title?: string;\n /** Card description text */\n description?: string;\n /** Additional class name */\n className?: string;\n}\n\n/**\n * Canvas Design System - Info Card\n * \n * A sidebar card displaying a title and descriptive text.\n * Used for \"About Us\" style content.\n */\nexport function InfoCard({\n title = \"ABOUT US\",\n description = \"Canvas is a no-code framework built on top of Bubble that makes creating beautiful responsive web applications easy and fast.\\n\\nCanvas is developed and maintained by AirDev in San Francisco, based on our experience with hundreds of client engagements.\",\n className,\n}: InfoCardProps) {\n return (\n <div className={cn(cardClassName, \"w-80\", className)}>\n <h3 \n className=\"text-[var(--canvas-text-placeholder)] font-semibold mb-4\"\n style={{\n fontFamily: \"var(--typo-body-m-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-m-size)\",\n }}\n >\n {title}\n </h3>\n <p \n className=\"text-[var(--canvas-text-muted)] whitespace-pre-line\"\n style={{\n fontFamily: \"var(--typo-body-m-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-m-size)\",\n lineHeight: \"24px\",\n }}\n >\n {description}\n </p>\n </div>\n );\n}\n\n// ============================================================================\n// LinksCard\n// ============================================================================\n\nexport interface LinkItem {\n id: string;\n label: string;\n icon: LucideIcon;\n href?: string;\n onClick?: () => void;\n}\n\nexport interface LinksCardProps {\n /** Card title (displayed as uppercase header) */\n title?: string;\n /** Array of link items */\n links?: LinkItem[];\n /** Additional class name */\n className?: string;\n}\n\n/** Default support links */\nexport const defaultSupportLinks: LinkItem[] = [\n { id: \"email\", label: \"Email us\", icon: Mail },\n { id: \"phone\", label: \"Call us\", icon: Phone },\n { id: \"text\", label: \"Text us\", icon: MessageCircle },\n { id: \"chat\", label: \"Chat now\", icon: User },\n];\n\n/**\n * Canvas Design System - Links Card\n * \n * A sidebar card displaying a title and a list of icon links.\n * Used for \"Support\" style contact links.\n */\nexport function
|
|
17
|
+
"content": "\"use client\";\n\nimport { cn } from \"../../lib/utils\";\nimport { Mail, Phone, MessageCircle, User, LucideIcon } from \"lucide-react\";\n\n// Shared card styling\nconst cardClassName = cn(\n \"bg-[var(--canvas-background)] border border-[var(--canvas-border)]\",\n \"rounded-xl p-8\"\n);\n\n// ============================================================================\n// InfoCard\n// ============================================================================\n\nexport interface InfoCardProps {\n /** Card title (displayed as uppercase header) */\n title?: string;\n /** Card description text */\n description?: string;\n /** Additional class name */\n className?: string;\n}\n\n/**\n * Canvas Design System - Info Card\n * \n * A sidebar card displaying a title and descriptive text.\n * Used for \"About Us\" style content.\n */\nexport function InfoCard({\n title = \"ABOUT US\",\n description = \"Canvas is a no-code framework built on top of Bubble that makes creating beautiful responsive web applications easy and fast.\\n\\nCanvas is developed and maintained by AirDev in San Francisco, based on our experience with hundreds of client engagements.\",\n className,\n}: InfoCardProps) {\n return (\n <div className={cn(cardClassName, \"w-80\", className)}>\n <h3 \n className=\"text-[var(--canvas-text-placeholder)] font-semibold mb-4\"\n style={{\n fontFamily: \"var(--typo-body-m-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-m-size)\",\n }}\n >\n {title}\n </h3>\n <p \n className=\"text-[var(--canvas-text-muted)] whitespace-pre-line\"\n style={{\n fontFamily: \"var(--typo-body-m-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-m-size)\",\n lineHeight: \"24px\",\n }}\n >\n {description}\n </p>\n </div>\n );\n}\n\n// ============================================================================\n// LinksCard\n// ============================================================================\n\nexport interface LinkItem {\n id: string;\n label: string;\n icon: LucideIcon;\n href?: string;\n onClick?: () => void;\n}\n\nexport interface LinksCardProps {\n /** Card title (displayed as uppercase header) */\n title?: string;\n /** Array of link items */\n links?: LinkItem[];\n /** Additional class name */\n className?: string;\n}\n\n/** Default support links */\nexport const defaultSupportLinks: LinkItem[] = [\n { id: \"email\", label: \"Email us\", icon: Mail },\n { id: \"phone\", label: \"Call us\", icon: Phone },\n { id: \"text\", label: \"Text us\", icon: MessageCircle },\n { id: \"chat\", label: \"Chat now\", icon: User },\n];\n\n/**\n * Canvas Design System - Links Card\n * \n * A sidebar card displaying a title and a list of icon links.\n * Used for \"Support\" style contact links.\n */\nexport function SupportLinksCard({\n title = \"SUPPORT\",\n links = defaultSupportLinks,\n className,\n}: LinksCardProps) {\n return (\n <div className={cn(cardClassName, \"w-80\", className)}>\n <h3 \n className=\"text-[var(--canvas-text-placeholder)] font-semibold mb-4\"\n style={{\n fontFamily: \"var(--typo-body-m-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-m-size)\",\n }}\n >\n {title}\n </h3>\n <div className=\"flex flex-col gap-4\">\n {links.map((link) => {\n const Icon = link.icon;\n const content = (\n <>\n <Icon className=\"size-5 text-[var(--canvas-primary)]\" />\n <span \n className=\"text-[var(--canvas-primary)] font-medium\"\n style={{\n fontFamily: \"var(--typo-body-m-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-m-size)\",\n }}\n >\n {link.label}\n </span>\n </>\n );\n\n if (link.href) {\n return (\n <a\n key={link.id}\n href={link.href}\n className=\"cursor-pointer flex items-center gap-1.5 hover:opacity-80 transition-opacity\"\n >\n {content}\n </a>\n );\n }\n\n return (\n <button\n key={link.id}\n type=\"button\"\n onClick={link.onClick}\n className=\"cursor-pointer flex items-center gap-1.5 hover:opacity-80 transition-opacity\"\n >\n {content}\n </button>\n );\n })}\n </div>\n </div>\n );\n}\n\n"
|
|
10
18
|
}
|
|
11
19
|
],
|
|
12
20
|
"dependencies": [
|
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sidebar-profile-card",
|
|
3
3
|
"type": "registry:block",
|
|
4
|
-
"description": "Compact profile card for sidebar with avatar and
|
|
4
|
+
"description": "Compact profile card for sidebar placement with avatar, name, and role/title. Use in sidebars to show user info, author details, or contact information.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"sidebar",
|
|
7
|
+
"profile",
|
|
8
|
+
"avatar",
|
|
9
|
+
"compact",
|
|
10
|
+
"user"
|
|
11
|
+
],
|
|
12
|
+
"visualWeight": "light",
|
|
5
13
|
"files": [
|
|
6
14
|
{
|
|
7
15
|
"path": "components/blocks/sidebar-profile-card.tsx",
|
|
8
16
|
"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
|
|
17
|
+
"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 * The avatar extends above the card body so it can overlap\n * a gradient banner or similar background element.\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 className={cn(\"flex flex-col items-center w-full\", className)}>\n {/* Avatar with Status - sits above the card body */}\n <div className=\"relative z-10\" style={{ marginBottom: -60 }}>\n <Avatar className=\"size-[120px]\">\n <AvatarImage src={avatarUrl} alt={name} />\n <AvatarFallback className=\"font-semibold bg-[var(--canvas-surface)] text-[var(--canvas-text-muted)]\" style={{ fontSize: \"var(--typo-h6-size)\" }}>\n {avatarFallback}\n </AvatarFallback>\n </Avatar>\n {showStatus && (\n <div className=\"absolute bottom-[10px] right-[10px] size-5 rounded-full bg-[var(--canvas-success)] border-[3px] border-[var(--canvas-background)]\" />\n )}\n </div>\n\n {/* Card Body */}\n <div\n className=\"bg-[var(--canvas-background)] border border-[var(--canvas-border)] rounded-[var(--radius-md)] flex flex-col w-full\"\n >\n <div\n className=\"flex flex-col items-center gap-[var(--spacing-2xl)] px-[var(--spacing-4xl)]\"\n style={{ paddingTop: 72 }}\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\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=\"neutral\"\n className=\"flex-1\"\n onClick={onContactClick}\n >\n Contact\n </Button>\n <Button\n variant=\"primary\"\n className=\"flex-1\"\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 </div>\n );\n}\n"
|
|
10
18
|
}
|
|
11
19
|
],
|
|
12
20
|
"dependencies": [
|
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "slideshow-grid-tiles",
|
|
3
3
|
"type": "registry:block",
|
|
4
|
-
"description": "2-column grid of portfolio
|
|
4
|
+
"description": "2-column grid of portfolio tiles with image slideshow navigation (prev/next arrows), save button, user avatar with name/location, and engagement stats (likes, views). Use for design portfolios, photo galleries, creative showcases, or any visual content grid.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"portfolio",
|
|
7
|
+
"slideshow",
|
|
8
|
+
"grid",
|
|
9
|
+
"gallery",
|
|
10
|
+
"creative"
|
|
11
|
+
],
|
|
12
|
+
"visualWeight": "heavy",
|
|
5
13
|
"files": [
|
|
6
14
|
{
|
|
7
15
|
"path": "components/blocks/slideshow-grid-tiles.tsx",
|
|
8
16
|
"type": "registry:block",
|
|
9
|
-
"content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport { TitleGroup } from \"./title-group\";\nimport { Avatar, AvatarImage, AvatarFallback } from \"../ui/avatar\";\nimport { ChevronLeft, ChevronRight, FolderPlus, ThumbsUp, Eye } from \"lucide-react\";\nimport { AVATAR_SARAH_CHEN, AVATAR_MARCUS_WEBB, AVATAR_MAYA_JOHNSON, AVATAR_NICOLE_PALMER } from \"./demo-avatars\";\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 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?: { id: string; label: string }[];\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: \"Sarah Chen\",\n avatarUrl: AVATAR_SARAH_CHEN,\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: \"Marcus Webb\",\n avatarUrl: AVATAR_MARCUS_WEBB,\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: \"Maya Johnson\",\n avatarUrl: AVATAR_MAYA_JOHNSON,\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: \"Nicole Palmer\",\n avatarUrl: AVATAR_NICOLE_PALMER,\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\n// ============================================\n// Sub-components\n// ============================================\n\ninterface TileCardProps {\n item: SlideshowTileItem;\n onSave?: (item: SlideshowTileItem) => void;\n onClick?: (item: SlideshowTileItem) => void;\n}\n\nfunction TileCard({ item, onSave, onClick }: TileCardProps) {\n const [currentImageIndex, setCurrentImageIndex] = useState(0);\n\n const handlePrevImage = (e: React.MouseEvent) => {\n e.stopPropagation();\n setCurrentImageIndex((prev) => \n prev === 0 ? item.images.length - 1 : prev - 1\n );\n };\n\n const handleNextImage = (e: React.MouseEvent) => {\n e.stopPropagation();\n setCurrentImageIndex((prev) => \n prev === item.images.length - 1 ? 0 : prev + 1\n );\n };\n\n const handleSave = (e: React.MouseEvent) => {\n e.stopPropagation();\n onSave?.(item);\n };\n\n return (\n <div \n className=\"flex flex-col cursor-pointer\"\n style={{ gap: \"var(--spacing-xl)\" }}\n onClick={() => onClick?.(item)}\n >\n {/* Image Container */}\n <div \n className=\"group relative w-full overflow-hidden\"\n style={{ \n height: \"240px\",\n borderRadius: \"var(--radius-md, 8px)\",\n border: \"1px solid var(--canvas-border)\",\n }}\n >\n {/* Main Image */}\n <img\n src={item.images[currentImageIndex]}\n alt={`${item.user.name}'s portfolio`}\n className=\"absolute inset-0 w-full h-full object-cover transition-opacity duration-300\"\n style={{ borderRadius: \"var(--radius-md, 8px)\" }}\n />\n\n {/* Hover Overlay - Navigation Arrows */}\n <div className=\"absolute inset-0 flex items-center justify-between px-2 opacity-0 group-hover:opacity-100 transition-opacity duration-200\">\n {/* Left Arrow */}\n <button\n onClick={handlePrevImage}\n className=\"cursor-pointer flex items-center justify-center shrink-0 transition-transform hover:scale-105\"\n style={{\n width: \"32px\",\n height: \"32px\",\n backgroundColor: \"var(--canvas-background)\",\n border: \"1px solid var(--canvas-border)\",\n borderRadius: \"var(--radius-full, 24px)\",\n boxShadow: \"0px 1px 2px 0px rgba(0,0,0,0.02)\",\n }}\n >\n <ChevronLeft \n className=\"w-5 h-5\" \n style={{ color: \"var(--canvas-text-muted)\" }}\n />\n </button>\n\n {/* Right Arrow */}\n <button\n onClick={handleNextImage}\n className=\"cursor-pointer flex items-center justify-center shrink-0 transition-transform hover:scale-105\"\n style={{\n width: \"32px\",\n height: \"32px\",\n backgroundColor: \"var(--canvas-background)\",\n border: \"1px solid var(--canvas-border)\",\n borderRadius: \"var(--radius-full, 24px)\",\n boxShadow: \"0px 1px 2px 0px rgba(0,0,0,0.02)\",\n }}\n >\n <ChevronRight \n className=\"w-5 h-5\" \n style={{ color: \"var(--canvas-text-muted)\" }}\n />\n </button>\n </div>\n\n {/* Hover Overlay - Save Badge */}\n <div className=\"absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity duration-200\">\n <button\n onClick={handleSave}\n className=\"cursor-pointer flex items-center transition-transform hover:scale-105\"\n style={{\n height: \"var(--spacing-5xl, 40px)\",\n paddingLeft: \"var(--spacing-xl, 16px)\",\n paddingRight: \"var(--spacing-xl, 16px)\",\n gap: \"var(--spacing-md, 8px)\",\n backgroundColor: \"var(--canvas-background)\",\n border: \"1px solid var(--canvas-border)\",\n borderRadius: \"var(--radius-full, 40px)\",\n }}\n >\n <FolderPlus \n className=\"w-5 h-5\" \n style={{ color: \"var(--canvas-text)\" }}\n />\n <span\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 500,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n Save\n </span>\n </button>\n </div>\n </div>\n\n {/* User Info Row */}\n <div \n className=\"flex items-center w-full\"\n style={{ gap: \"var(--spacing-xl)\" }}\n >\n {/* Avatar */}\n <Avatar \n className=\"shrink-0\"\n style={{ \n width: \"48px\", \n height: \"48px\",\n border: \"1px solid var(--canvas-border)\",\n }}\n >\n <AvatarImage src={item.user.avatarUrl} alt={item.user.name} />\n <AvatarFallback>\n {item.user.name.split(\" \").map(n => n[0]).join(\"\").slice(0, 2)}\n </AvatarFallback>\n </Avatar>\n\n {/* Name and Location */}\n <div className=\"flex flex-col flex-1 min-w-0 justify-center\">\n <p\n className=\"truncate\"\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 600,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text)\",\n margin: 0,\n }}\n >\n {item.user.name}\n </p>\n <p\n className=\"truncate\"\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: \"var(--typo-body-s-weight)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text-muted)\",\n margin: 0,\n }}\n >\n {item.user.location}\n </p>\n </div>\n\n {/* Stats */}\n <div \n className=\"flex items-center shrink-0\"\n style={{ gap: \"var(--spacing-md)\" }}\n >\n {/* Likes */}\n <div \n className=\"flex items-center\"\n style={{ gap: \"var(--spacing-sm)\" }}\n >\n <ThumbsUp \n className=\"w-5 h-5\" \n style={{ color: \"var(--canvas-text-muted)\" }}\n />\n <span\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: \"var(--typo-body-s-weight)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text-muted)\",\n }}\n >\n {item.likes}\n </span>\n </div>\n\n {/* Views */}\n <div \n className=\"flex items-center\"\n style={{ gap: \"var(--spacing-sm)\" }}\n >\n <Eye \n className=\"w-5 h-5\" \n style={{ color: \"var(--canvas-text-muted)\" }}\n />\n <span\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: \"var(--typo-body-s-weight)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text-muted)\",\n }}\n >\n {item.views}\n </span>\n </div>\n </div>\n </div>\n </div>\n );\n}\n\n// ============================================\n// Main Component\n// ============================================\n\n/**\n * Canvas Design System - Slideshow Grid Tiles Block\n * \n * A 2-column grid of portfolio/slideshow tiles with hover states\n * showing navigation arrows and save button. Each tile displays\n * a large image, user info, and engagement stats.\n * \n * @example\n * ```tsx\n * <SlideshowGridTiles\n * title=\"Portfolios\"\n * subtitle=\"Showing 20 designs\"\n * onSave={(item) => console.log(\"Saved\", item)}\n * />\n * ```\n */\nexport function SlideshowGridTiles({\n title = \"Portfolios\",\n subtitle,\n items = defaultItems,\n sortOptions = defaultSortOptions,\n actionButtonText = \"Add new\",\n onAddNew,\n onSort,\n onSave,\n onItemClick,\n className,\n}: SlideshowGridTilesProps) {\n const displaySubtitle = subtitle ?? `Showing ${items.length} designs`;\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 <TitleGroup title={title} subtitle={displaySubtitle} sortOptions={sortOptions} onSort={onSort} actionButtonText={actionButtonText} onAction={onAddNew} />\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"
|
|
17
|
+
"content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport { TitleGroup } from \"./title-group\";\nimport { Avatar, AvatarImage, AvatarFallback } from \"../ui/avatar\";\nimport { ChevronLeft, ChevronRight, FolderPlus, ThumbsUp, Eye } from \"lucide-react\";\nimport { AVATAR_SARAH_CHEN, AVATAR_MARCUS_WEBB, AVATAR_MAYA_JOHNSON, AVATAR_NICOLE_PALMER } from \"./demo-avatars\";\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 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?: { id: string; label: string }[];\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: \"Sarah Chen\",\n avatarUrl: AVATAR_SARAH_CHEN,\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: \"Marcus Webb\",\n avatarUrl: AVATAR_MARCUS_WEBB,\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: \"Maya Johnson\",\n avatarUrl: AVATAR_MAYA_JOHNSON,\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: \"Nicole Palmer\",\n avatarUrl: AVATAR_NICOLE_PALMER,\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\n// ============================================\n// Sub-components\n// ============================================\n\ninterface TileCardProps {\n item: SlideshowTileItem;\n onSave?: (item: SlideshowTileItem) => void;\n onClick?: (item: SlideshowTileItem) => void;\n}\n\nfunction TileCard({ item, onSave, onClick }: TileCardProps) {\n const [currentImageIndex, setCurrentImageIndex] = useState(0);\n\n const handlePrevImage = (e: React.MouseEvent) => {\n e.stopPropagation();\n setCurrentImageIndex((prev) => \n prev === 0 ? item.images.length - 1 : prev - 1\n );\n };\n\n const handleNextImage = (e: React.MouseEvent) => {\n e.stopPropagation();\n setCurrentImageIndex((prev) => \n prev === item.images.length - 1 ? 0 : prev + 1\n );\n };\n\n const handleSave = (e: React.MouseEvent) => {\n e.stopPropagation();\n onSave?.(item);\n };\n\n return (\n <div \n className=\"flex flex-col cursor-pointer\"\n style={{ gap: \"var(--spacing-xl)\" }}\n onClick={() => onClick?.(item)}\n >\n {/* Image Container */}\n <div \n className=\"group relative w-full overflow-hidden\"\n style={{ \n height: \"240px\",\n borderRadius: \"var(--radius-md, 8px)\",\n border: \"1px solid var(--canvas-border)\",\n }}\n >\n {/* Main Image */}\n <img\n src={item.images[currentImageIndex]}\n alt={`${item.user.name}'s portfolio`}\n className=\"absolute inset-0 w-full h-full object-cover transition-opacity duration-300\"\n style={{ borderRadius: \"var(--radius-md, 8px)\" }}\n />\n\n {/* Hover Overlay - Navigation Arrows */}\n <div className=\"absolute inset-0 flex items-center justify-between px-2 opacity-0 group-hover:opacity-100 transition-opacity duration-200\">\n {/* Left Arrow */}\n <button\n onClick={handlePrevImage}\n className=\"cursor-pointer flex items-center justify-center shrink-0 transition-transform hover:scale-105\"\n style={{\n width: \"32px\",\n height: \"32px\",\n backgroundColor: \"var(--canvas-background)\",\n border: \"1px solid var(--canvas-border)\",\n borderRadius: \"var(--radius-full, 24px)\",\n boxShadow: \"0px 1px 2px 0px rgba(0,0,0,0.02)\",\n }}\n >\n <ChevronLeft \n className=\"w-5 h-5\" \n style={{ color: \"var(--canvas-text-muted)\" }}\n />\n </button>\n\n {/* Right Arrow */}\n <button\n onClick={handleNextImage}\n className=\"cursor-pointer flex items-center justify-center shrink-0 transition-transform hover:scale-105\"\n style={{\n width: \"32px\",\n height: \"32px\",\n backgroundColor: \"var(--canvas-background)\",\n border: \"1px solid var(--canvas-border)\",\n borderRadius: \"var(--radius-full, 24px)\",\n boxShadow: \"0px 1px 2px 0px rgba(0,0,0,0.02)\",\n }}\n >\n <ChevronRight \n className=\"w-5 h-5\" \n style={{ color: \"var(--canvas-text-muted)\" }}\n />\n </button>\n </div>\n\n {/* Hover Overlay - Save Badge */}\n <div className=\"absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity duration-200\">\n <button\n onClick={handleSave}\n className=\"cursor-pointer flex items-center transition-transform hover:scale-105\"\n style={{\n height: \"var(--spacing-5xl, 40px)\",\n paddingLeft: \"var(--spacing-xl, 16px)\",\n paddingRight: \"var(--spacing-xl, 16px)\",\n gap: \"var(--spacing-md, 8px)\",\n backgroundColor: \"var(--canvas-background)\",\n border: \"1px solid var(--canvas-border)\",\n borderRadius: \"var(--radius-full, 40px)\",\n }}\n >\n <FolderPlus \n className=\"w-5 h-5\" \n style={{ color: \"var(--canvas-text)\" }}\n />\n <span\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 500,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n Save\n </span>\n </button>\n </div>\n </div>\n\n {/* User Info Row */}\n <div \n className=\"flex items-center w-full\"\n style={{ gap: \"var(--spacing-xl)\" }}\n >\n {/* Avatar */}\n <Avatar \n className=\"shrink-0\"\n style={{ \n width: \"48px\", \n height: \"48px\",\n border: \"1px solid var(--canvas-border)\",\n }}\n >\n <AvatarImage src={item.user.avatarUrl} alt={item.user.name} />\n <AvatarFallback>\n {item.user.name.split(\" \").map(n => n[0]).join(\"\").slice(0, 2)}\n </AvatarFallback>\n </Avatar>\n\n {/* Name and Location */}\n <div className=\"flex flex-col flex-1 min-w-0 justify-center\">\n <p\n className=\"truncate\"\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 600,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text)\",\n margin: 0,\n }}\n >\n {item.user.name}\n </p>\n <p\n className=\"truncate\"\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: \"var(--typo-body-s-weight)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text-muted)\",\n margin: 0,\n }}\n >\n {item.user.location}\n </p>\n </div>\n\n {/* Stats */}\n <div \n className=\"flex items-center shrink-0\"\n style={{ gap: \"var(--spacing-md)\" }}\n >\n {/* Likes */}\n <div \n className=\"flex items-center\"\n style={{ gap: \"var(--spacing-sm)\" }}\n >\n <ThumbsUp \n className=\"w-5 h-5\" \n style={{ color: \"var(--canvas-text-muted)\" }}\n />\n <span\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: \"var(--typo-body-s-weight)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text-muted)\",\n }}\n >\n {item.likes}\n </span>\n </div>\n\n {/* Views */}\n <div \n className=\"flex items-center\"\n style={{ gap: \"var(--spacing-sm)\" }}\n >\n <Eye \n className=\"w-5 h-5\" \n style={{ color: \"var(--canvas-text-muted)\" }}\n />\n <span\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: \"var(--typo-body-s-weight)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text-muted)\",\n }}\n >\n {item.views}\n </span>\n </div>\n </div>\n </div>\n </div>\n );\n}\n\n// ============================================\n// Main Component\n// ============================================\n\n/**\n * Canvas Design System - Slideshow Grid Tiles Block\n * \n * A 2-column grid of portfolio/slideshow tiles with hover states\n * showing navigation arrows and save button. Each tile displays\n * a large image, user info, and engagement stats.\n * \n * @example\n * ```tsx\n * <SlideshowGridTiles\n * title=\"Portfolios\"\n * subtitle=\"Showing 20 designs\"\n * onSave={(item) => console.log(\"Saved\", item)}\n * />\n * ```\n */\nexport function SlideshowGridTiles({\n title = \"Portfolios\",\n subtitle,\n items = defaultItems,\n sortOptions = defaultSortOptions,\n actionButtonText = \"Add new\",\n onAddNew,\n onSort,\n onSave,\n onItemClick,\n className,\n}: SlideshowGridTilesProps) {\n const displaySubtitle = subtitle ?? `Showing ${items.length} designs`;\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 <TitleGroup title={title} subtitle={displaySubtitle} sortOptions={sortOptions} onSort={onSort} actionButtonText={actionButtonText} onAction={onAddNew} />\n\n {/* Grid Section */}\n <div \n className=\"grid grid-cols-1 sm: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
18
|
}
|
|
11
19
|
],
|
|
12
20
|
"dependencies": [
|
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "slideshow-popup",
|
|
3
3
|
"type": "registry:block",
|
|
4
|
-
"description": "",
|
|
4
|
+
"description": "Full-screen image gallery modal with prev/next navigation, dot indicators, keyboard support (arrow keys), and author/likes/views metadata bar. Use for image slideshows, photo galleries, or portfolio detail views.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"slideshow",
|
|
7
|
+
"gallery",
|
|
8
|
+
"modal",
|
|
9
|
+
"images",
|
|
10
|
+
"fullscreen",
|
|
11
|
+
"carousel"
|
|
12
|
+
],
|
|
13
|
+
"visualWeight": "medium",
|
|
5
14
|
"files": [
|
|
6
15
|
{
|
|
7
16
|
"path": "components/blocks/slideshow-popup.tsx",
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "small-edit-popup",
|
|
3
|
+
"type": "registry:block",
|
|
4
|
+
"description": "Compact edit form modal with text inputs arranged in flexible rows supporting half-width field pairing. Centered dialog (~250px tall). Use for quick edits, inline field editing, or small update forms.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"edit",
|
|
7
|
+
"form",
|
|
8
|
+
"modal",
|
|
9
|
+
"compact",
|
|
10
|
+
"quick-edit",
|
|
11
|
+
"update"
|
|
12
|
+
],
|
|
13
|
+
"visualWeight": "light",
|
|
14
|
+
"files": [
|
|
15
|
+
{
|
|
16
|
+
"path": "components/blocks/small-edit-popup.tsx",
|
|
17
|
+
"type": "registry:block",
|
|
18
|
+
"content": "\"use client\";\n\nimport React, { useState, useEffect } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport {\n Dialog,\n DialogContent,\n DialogTitle,\n DialogDescription,\n} from \"../ui/dialog\";\nimport { Button } from \"../ui/button\";\nimport { TextInput } from \"../ui/text-input\";\nimport { Label } from \"../ui/label\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface SmallEditField {\n /** Unique field identifier */\n id: string;\n /** Label text displayed above the field */\n label: string;\n /** Placeholder text */\n placeholder?: string;\n /** When true, field takes 50% width and pairs with next half field */\n half?: boolean;\n}\n\nexport interface SmallEditPopupProps {\n /** Controls dialog visibility */\n open?: boolean;\n /** Callback when dialog open state changes */\n onOpenChange?: (open: boolean) => void;\n /** Dialog title */\n title?: string;\n /** Descriptive body text */\n description?: string;\n /** Form field configuration */\n fields?: SmallEditField[];\n /** Cancel button label */\n cancelLabel?: string;\n /** Save button label */\n saveLabel?: string;\n /** Callback when save is clicked — receives field values */\n onSave?: (values: Record<string, string>) => void;\n /** Callback when cancel is clicked */\n onCancel?: () => void;\n /** Disables the save button */\n loading?: boolean;\n /** Additional class names */\n className?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Defaults\n// ---------------------------------------------------------------------------\n\nconst DEFAULT_TITLE = \"Edit profile\";\nconst DEFAULT_DESCRIPTION = \"Update your personal information below.\";\n\nconst DEFAULT_FIELDS: SmallEditField[] = [\n { id: \"field-1\", label: \"First name\", half: true },\n { id: \"field-2\", label: \"Last name\", half: true },\n { id: \"field-3\", label: \"Phone\", half: true },\n { id: \"field-4\", label: \"Location\", half: true },\n { id: \"field-5\", label: \"Email address\" },\n { id: \"field-6\", label: \"Bio\" },\n];\n\n// ---------------------------------------------------------------------------\n// SmallEditPopup\n// ---------------------------------------------------------------------------\n\nexport function SmallEditPopup({\n open,\n onOpenChange,\n title = DEFAULT_TITLE,\n description = DEFAULT_DESCRIPTION,\n fields = DEFAULT_FIELDS,\n cancelLabel = \"Cancel\",\n saveLabel = \"Save changes\",\n onSave,\n onCancel,\n loading = false,\n className,\n}: SmallEditPopupProps) {\n const [values, setValues] = useState<Record<string, string>>({});\n\n // Reset form values when dialog closes\n useEffect(() => {\n if (!open) {\n setValues({});\n }\n }, [open]);\n\n const handleChange = (id: string, value: string) => {\n setValues((prev) => ({ ...prev, [id]: value }));\n };\n\n const handleCancel = () => {\n onCancel?.();\n onOpenChange?.(false);\n };\n\n const handleSave = () => {\n onSave?.(values);\n };\n\n // Group fields into rows: half-width fields paired together\n const rows: SmallEditField[][] = [];\n let i = 0;\n while (i < fields.length) {\n const field = fields[i];\n if (field.half && i + 1 < fields.length && fields[i + 1].half) {\n rows.push([field, fields[i + 1]]);\n i += 2;\n } else {\n rows.push([field]);\n i += 1;\n }\n }\n\n return (\n <Dialog open={open} onOpenChange={onOpenChange}>\n <DialogContent\n className={cn(\n \"p-[var(--spacing-4xl)] gap-[var(--spacing-2xl)]\",\n \"rounded-[var(--radius-xl)]\",\n \"shadow-[0px_4px_24px_0px_rgba(0,0,0,0.1)]\",\n \"sm:max-w-[576px]\",\n className\n )}\n showCloseButton\n >\n {/* Title & Description */}\n <div className=\"flex flex-col\">\n <DialogTitle\n style={{\n fontFamily: \"var(--typo-h6-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-h6-size)\",\n lineHeight: \"var(--typo-h6-line-height)\",\n fontWeight: 600,\n color: \"var(--canvas-text)\",\n }}\n >\n {title}\n </DialogTitle>\n <DialogDescription\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-muted)\",\n }}\n >\n {description}\n </DialogDescription>\n </div>\n\n {/* Form fields */}\n <div className=\"flex flex-col\" style={{ gap: \"var(--spacing-2xl)\" }}>\n {rows.map((row, rowIdx) => (\n <div\n key={rowIdx}\n className={cn(\n \"flex gap-[var(--spacing-3xl)]\",\n row.length > 1 ? \"flex-col md:flex-row\" : \"flex-col\"\n )}\n >\n {row.map((field) => (\n <div\n key={field.id}\n className={cn(\n \"flex flex-col\",\n row.length > 1 ? \"flex-1\" : \"w-full\"\n )}\n style={{ gap: \"var(--spacing-xs)\" }}\n >\n <Label>{field.label}</Label>\n <TextInput\n value={values[field.id] ?? \"\"}\n onChange={(e) => handleChange(field.id, e.target.value)}\n placeholder={field.placeholder}\n />\n </div>\n ))}\n </div>\n ))}\n </div>\n\n {/* Actions */}\n <div className=\"flex w-full gap-[var(--spacing-3xl)] justify-end\">\n <Button variant=\"neutral\" onClick={handleCancel}>\n {cancelLabel}\n </Button>\n <Button\n variant=\"primary\"\n onClick={handleSave}\n disabled={loading}\n >\n {saveLabel}\n </Button>\n </div>\n </DialogContent>\n </Dialog>\n );\n}\n"
|
|
19
|
+
}
|
|
20
|
+
],
|
|
21
|
+
"dependencies": [],
|
|
22
|
+
"registryDependencies": [
|
|
23
|
+
"lib/utils",
|
|
24
|
+
"ui/dialog",
|
|
25
|
+
"ui/button",
|
|
26
|
+
"ui/text-input",
|
|
27
|
+
"ui/label"
|
|
28
|
+
]
|
|
29
|
+
}
|
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "social-feed",
|
|
3
3
|
"type": "registry:block",
|
|
4
|
-
"description": "Social media
|
|
4
|
+
"description": "Social media feed with post composer at top, and posts showing various content types (text, images, video, reposts, link cards). Each post has social interactions (like, comment, repost, share) and threaded replies. Use for community feeds, social networks, or activity streams.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"social",
|
|
7
|
+
"feed",
|
|
8
|
+
"posts",
|
|
9
|
+
"community",
|
|
10
|
+
"timeline",
|
|
11
|
+
"network"
|
|
12
|
+
],
|
|
13
|
+
"visualWeight": "heavy",
|
|
5
14
|
"files": [
|
|
6
15
|
{
|
|
7
16
|
"path": "components/blocks/social-feed.tsx",
|
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "social-proof",
|
|
3
3
|
"type": "registry:block",
|
|
4
|
-
"description": "
|
|
4
|
+
"description": "Horizontal logo bar with 'as featured in' label and partner/press brand logos. Use for trust indicators, partner logos, media mentions, or integration partner displays.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"logos",
|
|
7
|
+
"partners",
|
|
8
|
+
"trust",
|
|
9
|
+
"featured-in",
|
|
10
|
+
"brands"
|
|
11
|
+
],
|
|
12
|
+
"visualWeight": "light",
|
|
5
13
|
"files": [
|
|
6
14
|
{
|
|
7
15
|
"path": "components/blocks/marketing/social-proof.tsx",
|