canvas-ui-sdk 0.3.6 → 0.3.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +272 -225
- package/dist/index.js.map +1 -1
- package/mcp/dist/index.js +14 -2
- package/package.json +1 -1
- package/registry/blocks/canvas-item.json +1 -1
- package/registry/blocks/chat-message.json +1 -1
- package/registry/blocks/component-palette.json +1 -1
- package/registry/blocks/component-search.json +1 -1
- package/registry/blocks/content-dropzone.json +1 -1
- package/registry/blocks/credit-card-display.json +1 -1
- package/registry/blocks/custom-component-helper.json +1 -1
- package/registry/blocks/empty-state.json +1 -1
- package/registry/blocks/filter-popover.json +1 -1
- package/registry/blocks/fixed-column-data-table.json +1 -1
- package/registry/blocks/infinity-canvas.json +1 -1
- package/registry/blocks/menu-section.json +1 -1
- package/registry/blocks/messenger-sidebar.json +1 -1
- package/registry/blocks/mobile-bottom-nav.json +1 -1
- package/registry/blocks/monthly-calendar-widget.json +1 -1
- package/registry/blocks/page-header-section.json +1 -1
- package/registry/blocks/page-previews.json +1 -1
- package/registry/blocks/pagination.json +1 -1
- package/registry/blocks/persona-card.json +1 -1
- package/registry/blocks/pricing-cards.json +1 -1
- package/registry/blocks/profile-card.json +1 -1
- package/registry/blocks/profile-info-cards.json +1 -1
- package/registry/blocks/prompt-template.json +1 -1
- package/registry/blocks/screen-flowchart.json +1 -1
- package/registry/blocks/screen-prompt-builder.json +1 -1
- package/registry/blocks/screen-prompt-template.json +1 -1
- package/registry/blocks/search-bar.json +1 -1
- package/registry/blocks/sidebar-cards.json +1 -1
- package/registry/blocks/sidebar-profile-card.json +1 -1
- package/registry/blocks/step-tracker.json +1 -1
- package/registry/blocks/vertical-step-tracker.json +1 -1
- package/registry/blocks/video-chat-controls.json +1 -1
- package/registry/layout/account-settings-shell.json +1 -1
- package/registry/layout/dashboard-shell.json +1 -1
- package/registry/layout/double-sidebar-shell.json +1 -1
- package/registry/layout/double-sidebar.json +1 -1
- package/registry/layout/header.json +1 -1
- package/registry/layout/icon-sidebar-shell.json +1 -1
- package/registry/layout/icon-sidebar.json +1 -1
- package/registry/layout/mobile-menu-shell.json +1 -1
- package/registry/layout/multistep-progressbar-shell.json +1 -1
- package/registry/layout/multistep-shell.json +1 -1
- package/registry/layout/multistep-sidebar-shell.json +1 -1
- package/registry/layout/project-context-shell.json +1 -1
- package/registry/layout/search-bar-shell.json +1 -1
- package/registry/layout/sidebar.json +1 -1
- package/registry/layout/standard-page-shell.json +1 -1
- package/registry/layout/vertical-multistep-shell.json +1 -1
- package/registry/ui/avatar.json +1 -1
- package/registry/ui/checkbox.json +1 -1
- package/registry/ui/date-input.json +1 -1
- package/registry/ui/image-uploader.json +1 -1
- package/registry/ui/multiselect-checkbox-field.json +1 -1
- package/registry/ui/multiselect-tags.json +1 -1
- package/registry/ui/radio-group.json +1 -1
- package/registry/ui/searchbox.json +1 -1
- package/registry/ui/select.json +1 -1
- package/registry/ui/selectable-pills.json +1 -1
- package/registry/ui/slider.json +1 -1
- package/registry/ui/switch.json +1 -1
- package/registry/ui/text-input.json +1 -1
- package/registry/ui/textarea.json +1 -1
- package/styles/tokens.reference.css +9 -0
package/mcp/dist/index.js
CHANGED
|
@@ -21550,8 +21550,20 @@ var colorVariables = [
|
|
|
21550
21550
|
{ name: "--canvas-primary", default: "#1165ef", label: "Primary" },
|
|
21551
21551
|
{ name: "--canvas-primary-dark", default: "#093378", label: "Primary Dark" },
|
|
21552
21552
|
{ name: "--canvas-primary-foreground", default: "#ffffff", label: "Primary Text" },
|
|
21553
|
-
{ name: "--canvas-flair-bg", default: "#093378", label: "Flair Background" }
|
|
21554
|
-
|
|
21553
|
+
{ name: "--canvas-flair-bg", default: "#093378", label: "Flair Background" }
|
|
21554
|
+
]
|
|
21555
|
+
},
|
|
21556
|
+
{
|
|
21557
|
+
category: "Status",
|
|
21558
|
+
variables: [
|
|
21559
|
+
{ name: "--canvas-success", default: "#08875d", label: "Success" },
|
|
21560
|
+
{ name: "--canvas-success-surface", default: "#edfdf8", label: "Success Surface" },
|
|
21561
|
+
{ name: "--canvas-warning", default: "#d97706", label: "Warning" },
|
|
21562
|
+
{ name: "--canvas-warning-surface", default: "#fffbeb", label: "Warning Surface" },
|
|
21563
|
+
{ name: "--canvas-info", default: "#2563eb", label: "Info" },
|
|
21564
|
+
{ name: "--canvas-info-surface", default: "#eff6ff", label: "Info Surface" },
|
|
21565
|
+
{ name: "--canvas-destructive", default: "#ef4444", label: "Destructive" },
|
|
21566
|
+
{ name: "--canvas-destructive-surface", default: "#fef2f2", label: "Destructive Surface" }
|
|
21555
21567
|
]
|
|
21556
21568
|
},
|
|
21557
21569
|
{
|
package/package.json
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
{
|
|
7
7
|
"path": "components/blocks/canvas-item.tsx",
|
|
8
8
|
"type": "registry:block",
|
|
9
|
-
"content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport { X, GripHorizontal } from \"lucide-react\";\n\nexport interface CanvasItemData {\n id: string;\n componentType: string;\n x: number;\n y: number;\n width?: number;\n height?: number;\n}\n\ninterface CanvasItemProps {\n item: CanvasItemData;\n isSelected: boolean;\n onSelect: (id: string) => void;\n onDelete: (id: string) => void;\n onDragStart: (id: string, startX: number, startY: number, itemX: number, itemY: number) => void;\n scale: number;\n children: React.ReactNode;\n}\n\n/**\n * Canvas Item - Wrapper for components placed on the infinity canvas\n * \n * Handles:\n * - Absolute positioning based on x, y coordinates\n * - Selection state with visual border\n * - Drag handle for repositioning\n * - Delete button when selected\n */\nexport function CanvasItem({\n item,\n isSelected,\n onSelect,\n onDelete,\n onDragStart,\n scale,\n children,\n}: CanvasItemProps) {\n const [isDragging, setIsDragging] = useState(false);\n\n const handleDragHandleMouseDown = (e: React.MouseEvent) => {\n e.preventDefault();\n e.stopPropagation();\n setIsDragging(true);\n onSelect(item.id);\n // Pass the mouse start position and current item position\n onDragStart(item.id, e.clientX, e.clientY, item.x, item.y);\n };\n\n const handleClick = (e: React.MouseEvent) => {\n e.stopPropagation();\n onSelect(item.id);\n };\n\n const handleDelete = (e: React.MouseEvent) => {\n e.stopPropagation();\n onDelete(item.id);\n };\n\n return (\n <div\n className={cn(\n \"absolute group\",\n \"transition-shadow duration-150\",\n isSelected && \"ring-2 ring-[var(--canvas-primary)] ring-offset-2\",\n isDragging && \"opacity-90 shadow-2xl z-50\"\n )}\n style={{\n left: item.x,\n top: item.y,\n }}\n onClick={handleClick}\n >\n {/* Control bar - visible on hover or when selected */}\n <div\n className={cn(\n \"absolute -top-9 left-0 right-0 flex items-center justify-between px-2 py-1.5\",\n \"bg-[var(--canvas-text)] rounded-t-md\",\n \"opacity-0 group-hover:opacity-100 transition-opacity\",\n isSelected && \"opacity-100\"\n )}\n >\n {/* Drag handle - this is what you grab to drag */}\n <div\n onMouseDown={handleDragHandleMouseDown}\n className=\"flex items-center gap-1.5 text-white/90
|
|
9
|
+
"content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport { X, GripHorizontal } from \"lucide-react\";\n\nexport interface CanvasItemData {\n id: string;\n componentType: string;\n x: number;\n y: number;\n width?: number;\n height?: number;\n}\n\ninterface CanvasItemProps {\n item: CanvasItemData;\n isSelected: boolean;\n onSelect: (id: string) => void;\n onDelete: (id: string) => void;\n onDragStart: (id: string, startX: number, startY: number, itemX: number, itemY: number) => void;\n scale: number;\n children: React.ReactNode;\n}\n\n/**\n * Canvas Item - Wrapper for components placed on the infinity canvas\n * \n * Handles:\n * - Absolute positioning based on x, y coordinates\n * - Selection state with visual border\n * - Drag handle for repositioning\n * - Delete button when selected\n */\nexport function CanvasItem({\n item,\n isSelected,\n onSelect,\n onDelete,\n onDragStart,\n scale,\n children,\n}: CanvasItemProps) {\n const [isDragging, setIsDragging] = useState(false);\n\n const handleDragHandleMouseDown = (e: React.MouseEvent) => {\n e.preventDefault();\n e.stopPropagation();\n setIsDragging(true);\n onSelect(item.id);\n // Pass the mouse start position and current item position\n onDragStart(item.id, e.clientX, e.clientY, item.x, item.y);\n };\n\n const handleClick = (e: React.MouseEvent) => {\n e.stopPropagation();\n onSelect(item.id);\n };\n\n const handleDelete = (e: React.MouseEvent) => {\n e.stopPropagation();\n onDelete(item.id);\n };\n\n return (\n <div\n className={cn(\n \"absolute group\",\n \"transition-shadow duration-150\",\n isSelected && \"ring-2 ring-[var(--canvas-primary)] ring-offset-2\",\n isDragging && \"opacity-90 shadow-2xl z-50\"\n )}\n style={{\n left: item.x,\n top: item.y,\n }}\n onClick={handleClick}\n >\n {/* Control bar - visible on hover or when selected */}\n <div\n className={cn(\n \"absolute -top-9 left-0 right-0 flex items-center justify-between px-2 py-1.5\",\n \"bg-[var(--canvas-text)] rounded-t-md\",\n \"opacity-0 group-hover:opacity-100 transition-opacity\",\n isSelected && \"opacity-100\"\n )}\n >\n {/* Drag handle - this is what you grab to drag */}\n <div\n onMouseDown={handleDragHandleMouseDown}\n className=\"flex items-center gap-1.5 text-white/90 cursor-grab active:cursor-grabbing select-none\"\n style={{ fontSize: \"var(--typo-body-xs-size)\" }}\n >\n <GripHorizontal className=\"size-4\" />\n <span className=\"text-[11px] font-medium\">{item.componentType}</span>\n </div>\n\n {/* Delete button */}\n <button\n onClick={handleDelete}\n className=\"p-1 rounded hover:bg-white/20 text-white/80 hover:text-white transition-colors\"\n aria-label=\"Delete component\"\n >\n <X className=\"size-3.5\" />\n </button>\n </div>\n\n {/* Component content */}\n <div className=\"pointer-events-none\">\n {children}\n </div>\n </div>\n );\n}\n"
|
|
10
10
|
}
|
|
11
11
|
],
|
|
12
12
|
"dependencies": [
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
{
|
|
7
7
|
"path": "components/blocks/chat-message.tsx",
|
|
8
8
|
"type": "registry:block",
|
|
9
|
-
"content": "\"use client\";\n\nimport { useRef } from \"react\";\nimport { Image, Paperclip, X } from \"lucide-react\";\nimport { Avatar, AvatarFallback, AvatarImage } from \"../ui/avatar\";\n\nexport interface ChatBubbleMessage {\n id: string;\n content: string;\n timestamp: string;\n isSent: boolean;\n senderName?: string;\n senderAvatar?: string;\n}\n\ninterface ChatBubbleProps {\n message: ChatBubbleMessage;\n}\n\nexport function ChatBubble({ message }: ChatBubbleProps) {\n const { content, isSent, senderAvatar, senderName } = message;\n\n if (isSent) {\n return (\n <div className=\"flex justify-end\">\n <div\n className=\"max-w-[375px] p-[var(--spacing-2xl)] rounded-tl-[var(--radius-xl)] rounded-tr-[var(--radius-xl)] rounded-bl-[var(--radius-xl)]\"\n style={{\n backgroundColor: \"var(--canvas-primary)\",\n color: \"var(--canvas-primary-foreground)\",\n }}\n >\n <p\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n }}\n >\n {content}\n </p>\n </div>\n </div>\n );\n }\n\n return (\n <div className=\"flex items-end gap-[var(--spacing-md)]\">\n <Avatar className=\"size-8 shrink-0\">\n <AvatarImage src={senderAvatar} alt={senderName} />\n <AvatarFallback\n
|
|
9
|
+
"content": "\"use client\";\n\nimport { useRef } from \"react\";\nimport { Image, Paperclip, X } from \"lucide-react\";\nimport { Avatar, AvatarFallback, AvatarImage } from \"../ui/avatar\";\n\nexport interface ChatBubbleMessage {\n id: string;\n content: string;\n timestamp: string;\n isSent: boolean;\n senderName?: string;\n senderAvatar?: string;\n}\n\ninterface ChatBubbleProps {\n message: ChatBubbleMessage;\n}\n\nexport function ChatBubble({ message }: ChatBubbleProps) {\n const { content, isSent, senderAvatar, senderName } = message;\n\n if (isSent) {\n return (\n <div className=\"flex justify-end\">\n <div\n className=\"max-w-[375px] p-[var(--spacing-2xl)] rounded-tl-[var(--radius-xl)] rounded-tr-[var(--radius-xl)] rounded-bl-[var(--radius-xl)]\"\n style={{\n backgroundColor: \"var(--canvas-primary)\",\n color: \"var(--canvas-primary-foreground)\",\n }}\n >\n <p\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n }}\n >\n {content}\n </p>\n </div>\n </div>\n );\n }\n\n return (\n <div className=\"flex items-end gap-[var(--spacing-md)]\">\n <Avatar className=\"size-8 shrink-0\">\n <AvatarImage src={senderAvatar} alt={senderName} />\n <AvatarFallback\n style={{\n fontSize: \"8px\",\n backgroundColor: \"var(--canvas-subtle)\",\n color: \"var(--canvas-muted-foreground)\",\n }}\n >\n {senderName\n ?.split(\" \")\n .map((n) => n[0])\n .join(\"\")}\n </AvatarFallback>\n </Avatar>\n <div\n className=\"max-w-[375px] p-[var(--spacing-2xl)] rounded-tl-[var(--radius-xl)] rounded-tr-[var(--radius-xl)] rounded-br-[var(--radius-xl)]\"\n style={{\n backgroundColor: \"var(--canvas-border)\",\n color: \"var(--canvas-foreground)\",\n }}\n >\n <p\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n }}\n >\n {content}\n </p>\n </div>\n </div>\n );\n}\n\ninterface ChatDateSeparatorProps {\n date: string;\n}\n\nexport function ChatDateSeparator({ date }: ChatDateSeparatorProps) {\n return (\n <div className=\"flex items-center justify-center\">\n <span\n className=\"font-medium\"\n style={{\n fontSize: \"var(--typo-body-xs-size)\",\n color: \"var(--canvas-muted-foreground)\",\n }}\n >\n {date}\n </span>\n </div>\n );\n}\n\ninterface AttachmentPill {\n id: string;\n name: string;\n type: \"image\" | \"file\";\n url?: string;\n}\n\ninterface MessengerInputProps {\n attachments?: AttachmentPill[];\n onRemoveAttachment?: (id: string) => void;\n onSend?: () => void;\n}\n\nexport function MessengerInput({\n attachments = [],\n onRemoveAttachment,\n onSend,\n}: MessengerInputProps) {\n const fileInputRef = useRef<HTMLInputElement>(null);\n\n const handleAttachmentClick = () => {\n fileInputRef.current?.click();\n };\n\n return (\n <div\n className=\"border\"\n style={{\n borderColor: \"var(--canvas-border)\",\n backgroundColor: \"var(--canvas-background)\",\n borderRadius: \"var(--radius-md)\",\n }}\n >\n {/* Textarea */}\n <textarea\n placeholder=\"Send a message\"\n rows={2}\n className=\"w-full p-3 resize-none focus:outline-none\"\n style={{\n backgroundColor: \"transparent\",\n color: \"var(--canvas-foreground)\",\n fontSize: \"var(--typo-body-xs-size)\",\n lineHeight: \"var(--typo-body-xs-line-height)\",\n }}\n />\n\n {/* Attachments Row */}\n {attachments.length > 0 && (\n <div\n className=\"flex flex-wrap gap-2 px-3 py-2 border-t\"\n style={{ borderColor: \"var(--canvas-border)\" }}\n >\n {attachments.map((attachment) => (\n <a\n key={attachment.id}\n href={attachment.url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"relative flex items-center gap-[var(--spacing-md)] h-8 px-[var(--spacing-md)] rounded-[var(--radius-xs)] border cursor-pointer hover:opacity-80 transition-opacity\"\n style={{\n backgroundColor: \"var(--canvas-background)\",\n borderColor: \"var(--canvas-border)\",\n }}\n >\n {attachment.type === \"image\" ? (\n <Image\n className=\"size-5\"\n style={{ color: \"var(--canvas-primary)\" }}\n />\n ) : (\n <Paperclip\n className=\"size-5\"\n style={{ color: \"var(--canvas-primary)\" }}\n />\n )}\n <span\n style={{ color: \"var(--canvas-muted-foreground)\", fontSize: \"var(--typo-body-xs-size)\" }}\n >\n {attachment.name}\n </span>\n <button\n onClick={(e) => {\n e.preventDefault();\n e.stopPropagation();\n onRemoveAttachment?.(attachment.id);\n }}\n className=\"absolute -top-1 -right-1 size-4 rounded-full flex items-center justify-center hover:opacity-70\"\n style={{ backgroundColor: \"var(--canvas-muted-foreground)\" }}\n >\n <X className=\"size-2\" style={{ color: \"white\" }} />\n </button>\n </a>\n ))}\n </div>\n )}\n\n {/* Bottom Bar */}\n <div\n className=\"flex items-center justify-between px-[var(--spacing-xl)] py-[var(--spacing-lg)] border-t\"\n style={{ borderColor: \"var(--canvas-border)\" }}\n >\n <input\n ref={fileInputRef}\n type=\"file\"\n accept=\"image/*\"\n className=\"hidden\"\n aria-hidden=\"true\"\n />\n <button\n onClick={handleAttachmentClick}\n className=\"hover:opacity-70\"\n aria-label=\"Add attachment\"\n >\n <Paperclip\n className=\"size-5\"\n style={{ color: \"var(--canvas-muted-foreground)\" }}\n />\n </button>\n <button\n onClick={onSend}\n className=\"h-10 px-[var(--spacing-lg)] rounded-[var(--radius-xs)] font-semibold transition-colors hover:opacity-90\"\n style={{\n backgroundColor: \"var(--canvas-primary)\",\n color: \"var(--canvas-primary-foreground)\",\n fontSize: \"var(--typo-body-xs-size)\",\n }}\n >\n Send\n </button>\n </div>\n </div>\n );\n}\n\n// Alias for video chat compatibility\nexport const ChatInput = MessengerInput;\n\n// Simple chat message list for video chat\ninterface ChatMessageListProps {\n messages?: ChatBubbleMessage[];\n className?: string;\n}\n\nexport function ChatMessageList({ messages = [], className }: ChatMessageListProps) {\n return (\n <div className={className}>\n {messages.map((message) => (\n <ChatBubble key={message.id} message={message} />\n ))}\n </div>\n );\n}\n"
|
|
10
10
|
}
|
|
11
11
|
],
|
|
12
12
|
"dependencies": [
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
{
|
|
7
7
|
"path": "components/blocks/component-palette.tsx",
|
|
8
8
|
"type": "registry:block",
|
|
9
|
-
"content": "\"use client\";\n\nimport { useState, useEffect } from \"react\";\nimport { useDraggable } from \"@dnd-kit/core\";\nimport { CSS } from \"@dnd-kit/utilities\";\nimport { cn } from \"../../lib/utils\";\nimport { \n ChevronDown, \n ChevronRight,\n Layout,\n LayoutGrid,\n MessageSquare,\n Megaphone,\n CreditCard,\n User,\n Table,\n List,\n Image,\n Type,\n Video,\n Search,\n Settings,\n LogIn,\n Phone,\n ShoppingCart,\n FileText,\n Square,\n CheckSquare,\n Calendar,\n ToggleLeft,\n CircleDot,\n Hash,\n SlidersHorizontal,\n Tags,\n Star,\n MapPin,\n Users,\n Play,\n Newspaper,\n Building,\n Award,\n Layers,\n} from \"lucide-react\";\nimport { ScrollArea } from \"../ui/scroll-area\";\n\n// Component definitions for the palette\nexport interface PaletteComponent {\n id: string;\n type: string;\n label: string;\n icon: React.ReactNode;\n category: string;\n}\n\nconst paletteComponents: PaletteComponent[] = [\n // =====================\n // PAGE TEMPLATES\n // =====================\n { id: \"page-about\", type: \"PageAbout\", label: \"About\", icon: <Layout className=\"size-4\" />, category: \"Page Templates\" },\n { id: \"page-account\", type: \"PageAccount\", label: \"Account Settings\", icon: <Settings className=\"size-4\" />, category: \"Page Templates\" },\n { id: \"page-admin-portal\", type: \"PageAdminPortal\", label: \"Admin Portal\", icon: <Layout className=\"size-4\" />, category: \"Page Templates\" },\n { id: \"page-centered-profile\", type: \"PageCenteredProfile\", label: \"Centered Profile\", icon: <User className=\"size-4\" />, category: \"Page Templates\" },\n { id: \"page-double-sidebar\", type: \"PageDoubleSidebar\", label: \"Double Sidebar\", icon: <Layout className=\"size-4\" />, category: \"Page Templates\" },\n { id: \"page-icon-sidebar\", type: \"PageIconSidebar\", label: \"Icon Sidebar\", icon: <Layout className=\"size-4\" />, category: \"Page Templates\" },\n { id: \"page-login\", type: \"PageLogin\", label: \"Login / Signup\", icon: <LogIn className=\"size-4\" />, category: \"Page Templates\" },\n { id: \"page-menu-sections\", type: \"PageMenuSections\", label: \"Menu Sections\", icon: <List className=\"size-4\" />, category: \"Page Templates\" },\n { id: \"page-messenger\", type: \"PageMessenger\", label: \"Messenger\", icon: <MessageSquare className=\"size-4\" />, category: \"Page Templates\" },\n { id: \"page-mobile-menu\", type: \"PageMobileMenu\", label: \"Mobile Menu\", icon: <Layout className=\"size-4\" />, category: \"Page Templates\" },\n { id: \"page-multistep-progressbar\", type: \"PageMultistepProgressbar\", label: \"Multistep + Progress\", icon: <List className=\"size-4\" />, category: \"Page Templates\" },\n { id: \"page-multistep-sidebar\", type: \"PageMultistepSidebar\", label: \"Multistep + Sidebar\", icon: <List className=\"size-4\" />, category: \"Page Templates\" },\n { id: \"page-pricing\", type: \"PagePricing\", label: \"Pricing\", icon: <CreditCard className=\"size-4\" />, category: \"Page Templates\" },\n { id: \"page-product-homepage\", type: \"PageProductHomepage\", label: \"Product Homepage\", icon: <Layout className=\"size-4\" />, category: \"Page Templates\" },\n { id: \"page-reset-password\", type: \"PageResetPassword\", label: \"Reset Password\", icon: <LogIn className=\"size-4\" />, category: \"Page Templates\" },\n { id: \"page-search-bar\", type: \"PageSearchBar\", label: \"Search Bar\", icon: <Search className=\"size-4\" />, category: \"Page Templates\" },\n { id: \"page-sidebar-profile\", type: \"PageSidebarProfile\", label: \"Sidebar Profile\", icon: <User className=\"size-4\" />, category: \"Page Templates\" },\n { id: \"page-standard\", type: \"PageStandard\", label: \"Standard Page\", icon: <Layout className=\"size-4\" />, category: \"Page Templates\" },\n { id: \"page-standard-multistep\", type: \"PageStandardMultistep\", label: \"Standard Multistep\", icon: <List className=\"size-4\" />, category: \"Page Templates\" },\n { id: \"page-standard-search\", type: \"PageStandardSearch\", label: \"Standard Search\", icon: <Search className=\"size-4\" />, category: \"Page Templates\" },\n { id: \"page-vertical-multistep\", type: \"PageVerticalMultistep\", label: \"Vertical Multistep\", icon: <List className=\"size-4\" />, category: \"Page Templates\" },\n { id: \"page-video-chat\", type: \"PageVideoChat\", label: \"Video Chat\", icon: <Video className=\"size-4\" />, category: \"Page Templates\" },\n { id: \"page-video-list\", type: \"PageVideoList\", label: \"Video List\", icon: <Play className=\"size-4\" />, category: \"Page Templates\" },\n\n // =====================\n // BLOCKS\n // =====================\n // Data & Tables\n { id: \"standard-data-table\", type: \"StandardDataTable\", label: \"Data Table\", icon: <Table className=\"size-4\" />, category: \"Blocks\" },\n \n // Cards & Profiles\n { id: \"profile-card\", type: \"ProfileCard\", label: \"Profile Card\", icon: <User className=\"size-4\" />, category: \"Blocks\" },\n { id: \"sidebar-profile-card\", type: \"SidebarProfileCard\", label: \"Sidebar Profile Card\", icon: <User className=\"size-4\" />, category: \"Blocks\" },\n { id: \"profile-info-cards\", type: \"ProfileInfoCards\", label: \"Profile Info Cards\", icon: <LayoutGrid className=\"size-4\" />, category: \"Blocks\" },\n { id: \"sidebar-cards\", type: \"SidebarCards\", label: \"Sidebar Cards\", icon: <Layers className=\"size-4\" />, category: \"Blocks\" },\n { id: \"credit-card-display\", type: \"CreditCardDisplay\", label: \"Credit Card Display\", icon: <CreditCard className=\"size-4\" />, category: \"Blocks\" },\n \n // Navigation & Progress\n { id: \"step-tracker\", type: \"StepTracker\", label: \"Step Tracker\", icon: <List className=\"size-4\" />, category: \"Blocks\" },\n { id: \"vertical-step-tracker\", type: \"VerticalStepTracker\", label: \"Vertical Step Tracker\", icon: <List className=\"size-4\" />, category: \"Blocks\" },\n { id: \"progress-bar\", type: \"ProgressBar\", label: \"Progress Bar\", icon: <SlidersHorizontal className=\"size-4\" />, category: \"Blocks\" },\n { id: \"pill-tabs\", type: \"PillTabs\", label: \"Pill Tabs\", icon: <LayoutGrid className=\"size-4\" />, category: \"Blocks\" },\n { id: \"mobile-bottom-nav\", type: \"MobileBottomNav\", label: \"Mobile Bottom Nav\", icon: <Layout className=\"size-4\" />, category: \"Blocks\" },\n \n // Banners & Headers\n { id: \"flair-banner\", type: \"FlairBanner\", label: \"Flair Banner\", icon: <Type className=\"size-4\" />, category: \"Blocks\" },\n { id: \"gradient-banner\", type: \"GradientBanner\", label: \"Gradient Banner\", icon: <Type className=\"size-4\" />, category: \"Blocks\" },\n { id: \"page-header-section\", type: \"PageHeaderSection\", label: \"Page Header Section\", icon: <FileText className=\"size-4\" />, category: \"Blocks\" },\n \n // Chat & Messaging\n { id: \"messenger-sidebar\", type: \"MessengerSidebar\", label: \"Messenger Sidebar\", icon: <MessageSquare className=\"size-4\" />, category: \"Blocks\" },\n { id: \"chat-message\", type: \"ChatMessage\", label: \"Chat Message\", icon: <MessageSquare className=\"size-4\" />, category: \"Blocks\" },\n \n // Video\n { id: \"video-chat-controls\", type: \"VideoChatControls\", label: \"Video Chat Controls\", icon: <Video className=\"size-4\" />, category: \"Blocks\" },\n { id: \"webcam-preview\", type: \"WebcamPreview\", label: \"Webcam Preview\", icon: <Video className=\"size-4\" />, category: \"Blocks\" },\n { id: \"participant-list\", type: \"ParticipantList\", label: \"Participant List\", icon: <Users className=\"size-4\" />, category: \"Blocks\" },\n { id: \"video-content-section\", type: \"VideoContentSection\", label: \"Video Content Section\", icon: <Play className=\"size-4\" />, category: \"Blocks\" },\n { id: \"video-playlist\", type: \"VideoPlaylist\", label: \"Video Playlist\", icon: <List className=\"size-4\" />, category: \"Blocks\" },\n \n // Search & Filters\n { id: \"search-bar\", type: \"SearchBar\", label: \"Search Bar\", icon: <Search className=\"size-4\" />, category: \"Blocks\" },\n { id: \"filter-popover\", type: \"FilterPopover\", label: \"Filter Popover\", icon: <SlidersHorizontal className=\"size-4\" />, category: \"Blocks\" },\n \n // Forms & Settings\n { id: \"settings-list-row\", type: \"SettingsListRow\", label: \"Settings List Row\", icon: <Settings className=\"size-4\" />, category: \"Blocks\" },\n { id: \"profile-image-uploader\", type: \"ProfileImageUploader\", label: \"Profile Image Uploader\", icon: <Image className=\"size-4\" />, category: \"Blocks\" },\n { id: \"login-branding-panel\", type: \"LoginBrandingPanel\", label: \"Login Branding Panel\", icon: <Layout className=\"size-4\" />, category: \"Blocks\" },\n \n // Marketing - Heroes\n { id: \"hero-section\", type: \"HeroSection\", label: \"Hero Section\", icon: <Image className=\"size-4\" />, category: \"Blocks\" },\n { id: \"hero-dark-with-image\", type: \"HeroDarkWithImage\", label: \"Hero Dark + Image\", icon: <Image className=\"size-4\" />, category: \"Blocks\" },\n { id: \"centered-hero\", type: \"CenteredHero\", label: \"Centered Hero\", icon: <Image className=\"size-4\" />, category: \"Blocks\" },\n \n // Marketing - Social Proof\n { id: \"testimonial-carousel\", type: \"TestimonialCarousel\", label: \"Testimonial Carousel\", icon: <MessageSquare className=\"size-4\" />, category: \"Blocks\" },\n { id: \"reviews-grid\", type: \"ReviewsGrid\", label: \"Reviews Grid\", icon: <Star className=\"size-4\" />, category: \"Blocks\" },\n { id: \"social-proof\", type: \"SocialProof\", label: \"Social Proof (Logos)\", icon: <Award className=\"size-4\" />, category: \"Blocks\" },\n { id: \"metrics-section\", type: \"MetricsSection\", label: \"Metrics Section\", icon: <Hash className=\"size-4\" />, category: \"Blocks\" },\n \n // Marketing - Features\n { id: \"feature-with-image\", type: \"FeatureWithImage\", label: \"Feature + Image\", icon: <Image className=\"size-4\" />, category: \"Blocks\" },\n { id: \"core-values-grid\", type: \"CoreValuesGrid\", label: \"Core Values Grid\", icon: <LayoutGrid className=\"size-4\" />, category: \"Blocks\" },\n { id: \"destination-cards\", type: \"DestinationCards\", label: \"Destination Cards\", icon: <MapPin className=\"size-4\" />, category: \"Blocks\" },\n \n // Marketing - Team\n { id: \"team-cards-grid\", type: \"TeamCardsGrid\", label: \"Team Cards Grid\", icon: <Users className=\"size-4\" />, category: \"Blocks\" },\n { id: \"team-circular-grid\", type: \"TeamCircularGrid\", label: \"Team Circular Grid\", icon: <Users className=\"size-4\" />, category: \"Blocks\" },\n \n // Marketing - CTA & Footer\n { id: \"cta-banner\", type: \"CtaBanner\", label: \"CTA Banner\", icon: <Megaphone className=\"size-4\" />, category: \"Blocks\" },\n { id: \"footer-navbar\", type: \"FooterNavbar\", label: \"Footer Navbar\", icon: <Layout className=\"size-4\" />, category: \"Blocks\" },\n \n // Marketing - Other\n { id: \"featured-news-cards\", type: \"FeaturedNewsCards\", label: \"Featured News Cards\", icon: <Newspaper className=\"size-4\" />, category: \"Blocks\" },\n { id: \"office-locations\", type: \"OfficeLocations\", label: \"Office Locations\", icon: <Building className=\"size-4\" />, category: \"Blocks\" },\n \n // Pricing\n { id: \"pricing-cards\", type: \"PricingCards\", label: \"Pricing Cards\", icon: <CreditCard className=\"size-4\" />, category: \"Blocks\" },\n { id: \"faq-accordion\", type: \"FaqAccordion\", label: \"FAQ Accordion\", icon: <List className=\"size-4\" />, category: \"Blocks\" },\n { id: \"features-comparison\", type: \"FeaturesComparison\", label: \"Features Comparison\", icon: <Table className=\"size-4\" />, category: \"Blocks\" },\n\n // =====================\n // COMPONENTS (UI Primitives)\n // =====================\n { id: \"button\", type: \"Button\", label: \"Button\", icon: <Square className=\"size-4\" />, category: \"Components\" },\n { id: \"checkbox\", type: \"Checkbox\", label: \"Checkbox\", icon: <CheckSquare className=\"size-4\" />, category: \"Components\" },\n { id: \"date-input\", type: \"DateInput\", label: \"Date Input\", icon: <Calendar className=\"size-4\" />, category: \"Components\" },\n { id: \"input\", type: \"Input\", label: \"Text Input\", icon: <Type className=\"size-4\" />, category: \"Components\" },\n { id: \"select\", type: \"Select\", label: \"Select\", icon: <List className=\"size-4\" />, category: \"Components\" },\n { id: \"switch\", type: \"Switch\", label: \"Switch\", icon: <ToggleLeft className=\"size-4\" />, category: \"Components\" },\n { id: \"radio-group\", type: \"RadioGroup\", label: \"Radio Group\", icon: <CircleDot className=\"size-4\" />, category: \"Components\" },\n { id: \"multiselect-tags\", type: \"MultiselectTags\", label: \"Multiselect Tags\", icon: <Tags className=\"size-4\" />, category: \"Components\" },\n { id: \"avatar\", type: \"Avatar\", label: \"Avatar\", icon: <User className=\"size-4\" />, category: \"Components\" },\n { id: \"badge\", type: \"Badge\", label: \"Badge\", icon: <Award className=\"size-4\" />, category: \"Components\" },\n];\n\n// Group components by category\nconst componentsByCategory = paletteComponents.reduce((acc, comp) => {\n if (!acc[comp.category]) {\n acc[comp.category] = [];\n }\n acc[comp.category].push(comp);\n return acc;\n}, {} as Record<string, PaletteComponent[]>);\n\n// Define category order\nconst categoryOrder = [\"Page Templates\", \"Blocks\", \"Components\"];\n\ninterface DraggableComponentProps {\n component: PaletteComponent;\n}\n\nfunction DraggableComponent({ component }: DraggableComponentProps) {\n const [isMounted, setIsMounted] = useState(false);\n \n const { attributes, listeners, setNodeRef, transform, isDragging } = useDraggable({\n id: component.id,\n data: {\n type: component.type,\n label: component.label,\n },\n });\n\n // Only apply dnd-kit attributes after hydration to prevent mismatch\n useEffect(() => {\n setIsMounted(true);\n }, []);\n\n const style = {\n transform: CSS.Transform.toString(transform),\n opacity: isDragging ? 0.5 : 1,\n };\n\n return (\n <div\n ref={setNodeRef}\n style={style}\n // Only spread dnd-kit attributes after client-side mount to avoid hydration mismatch\n {...(isMounted ? attributes : {})}\n {...(isMounted ? listeners : {})}\n className={cn(\n \"flex items-center gap-3 px-3 py-2.5 rounded-md cursor-grab active:cursor-grabbing\",\n \"border border-transparent\",\n \"hover:bg-[var(--canvas-surface)] hover:border-[var(--canvas-border)]\",\n \"transition-colors group\"\n )}\n >\n <div \n className=\"flex items-center justify-center size-8 rounded-md bg-[var(--canvas-surface-brand)] text-[var(--canvas-primary)]\"\n >\n {component.icon}\n </div>\n <span \n className=\"text-sm text-[var(--canvas-text)]\"\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n }}\n >\n {component.label}\n </span>\n </div>\n );\n}\n\ninterface CategorySectionProps {\n category: string;\n components: PaletteComponent[];\n defaultExpanded?: boolean;\n}\n\nfunction CategorySection({ category, components, defaultExpanded = true }: CategorySectionProps) {\n const [isExpanded, setIsExpanded] = useState(defaultExpanded);\n\n return (\n <div className=\"mb-2\">\n <button\n onClick={() => setIsExpanded(!isExpanded)}\n className=\"flex items-center gap-2 w-full px-3 py-2 text-left hover:bg-[var(--canvas-surface)] rounded-md transition-colors\"\n >\n {isExpanded ? (\n <ChevronDown className=\"size-4 text-[var(--canvas-text-muted)]\" />\n ) : (\n <ChevronRight className=\"size-4 text-[var(--canvas-text-muted)]\" />\n )}\n <span \n className=\"text-xs font-semibold uppercase tracking-wider text-[var(--canvas-text-muted)]\"\n >\n {category}\n </span>\n <span className=\"ml-auto text-xs text-[var(--canvas-text-placeholder)]\">\n {components.length}\n </span>\n </button>\n \n {isExpanded && (\n <div className=\"mt-1 ml-2 space-y-0.5\">\n {components.map((component) => (\n <DraggableComponent key={component.id} component={component} />\n ))}\n </div>\n )}\n </div>\n );\n}\n\ninterface ComponentPaletteProps {\n className?: string;\n}\n\n/**\n * Component Palette - Sidebar with draggable components\n * \n * Features:\n * - Organized by category (Page Templates, Blocks, Components)\n * - Collapsible sections\n * - Drag to add to canvas\n */\nexport function ComponentPalette({ className }: ComponentPaletteProps) {\n return (\n <aside\n className={cn(\n \"w-[280px] h-full flex flex-col\",\n \"bg-white border-r border-[var(--canvas-border)]\",\n className\n )}\n >\n {/* Header */}\n <div className=\"px-4 py-4 border-b border-[var(--canvas-border)]\">\n <h2 \n className=\"text-sm font-semibold text-[var(--canvas-text)]\"\n style={{\n fontFamily: \"var(--typo-body-m-font, var(--typo-global-font))\",\n }}\n >\n Components\n </h2>\n <p \n className=\"text-xs text-[var(--canvas-text-muted)] mt-1\"\n >\n Drag components onto the canvas\n </p>\n </div>\n\n {/* Component List */}\n <ScrollArea className=\"flex-1\">\n <div className=\"p-3\">\n {categoryOrder.map((category) => {\n const components = componentsByCategory[category];\n if (!components) return null;\n return (\n <CategorySection\n key={category}\n category={category}\n components={components}\n defaultExpanded={category !== \"Page Templates\"} // Collapse templates by default\n />\n );\n })}\n </div>\n </ScrollArea>\n\n {/* Footer hint */}\n <div className=\"px-4 py-3 border-t border-[var(--canvas-border)] bg-[var(--canvas-surface)]\">\n <p className=\"text-xs text-[var(--canvas-text-placeholder)]\">\n Tip: Press <kbd className=\"px-1.5 py-0.5 bg-white rounded border border-[var(--canvas-border)] text-[10px]\">Delete</kbd> to remove selected\n </p>\n </div>\n </aside>\n );\n}\n"
|
|
9
|
+
"content": "\"use client\";\n\nimport { useState, useEffect } from \"react\";\nimport { useDraggable } from \"@dnd-kit/core\";\nimport { CSS } from \"@dnd-kit/utilities\";\nimport { cn } from \"../../lib/utils\";\nimport { \n ChevronDown, \n ChevronRight,\n Layout,\n LayoutGrid,\n MessageSquare,\n Megaphone,\n CreditCard,\n User,\n Table,\n List,\n Image,\n Type,\n Video,\n Search,\n Settings,\n LogIn,\n Phone,\n ShoppingCart,\n FileText,\n Square,\n CheckSquare,\n Calendar,\n ToggleLeft,\n CircleDot,\n Hash,\n SlidersHorizontal,\n Tags,\n Star,\n MapPin,\n Users,\n Play,\n Newspaper,\n Building,\n Award,\n Layers,\n} from \"lucide-react\";\nimport { ScrollArea } from \"../ui/scroll-area\";\n\n// Component definitions for the palette\nexport interface PaletteComponent {\n id: string;\n type: string;\n label: string;\n icon: React.ReactNode;\n category: string;\n}\n\nconst paletteComponents: PaletteComponent[] = [\n // =====================\n // PAGE TEMPLATES\n // =====================\n { id: \"page-about\", type: \"PageAbout\", label: \"About\", icon: <Layout className=\"size-4\" />, category: \"Page Templates\" },\n { id: \"page-account\", type: \"PageAccount\", label: \"Account Settings\", icon: <Settings className=\"size-4\" />, category: \"Page Templates\" },\n { id: \"page-admin-portal\", type: \"PageAdminPortal\", label: \"Admin Portal\", icon: <Layout className=\"size-4\" />, category: \"Page Templates\" },\n { id: \"page-centered-profile\", type: \"PageCenteredProfile\", label: \"Centered Profile\", icon: <User className=\"size-4\" />, category: \"Page Templates\" },\n { id: \"page-double-sidebar\", type: \"PageDoubleSidebar\", label: \"Double Sidebar\", icon: <Layout className=\"size-4\" />, category: \"Page Templates\" },\n { id: \"page-icon-sidebar\", type: \"PageIconSidebar\", label: \"Icon Sidebar\", icon: <Layout className=\"size-4\" />, category: \"Page Templates\" },\n { id: \"page-login\", type: \"PageLogin\", label: \"Login / Signup\", icon: <LogIn className=\"size-4\" />, category: \"Page Templates\" },\n { id: \"page-menu-sections\", type: \"PageMenuSections\", label: \"Menu Sections\", icon: <List className=\"size-4\" />, category: \"Page Templates\" },\n { id: \"page-messenger\", type: \"PageMessenger\", label: \"Messenger\", icon: <MessageSquare className=\"size-4\" />, category: \"Page Templates\" },\n { id: \"page-mobile-menu\", type: \"PageMobileMenu\", label: \"Mobile Menu\", icon: <Layout className=\"size-4\" />, category: \"Page Templates\" },\n { id: \"page-multistep-progressbar\", type: \"PageMultistepProgressbar\", label: \"Multistep + Progress\", icon: <List className=\"size-4\" />, category: \"Page Templates\" },\n { id: \"page-multistep-sidebar\", type: \"PageMultistepSidebar\", label: \"Multistep + Sidebar\", icon: <List className=\"size-4\" />, category: \"Page Templates\" },\n { id: \"page-pricing\", type: \"PagePricing\", label: \"Pricing\", icon: <CreditCard className=\"size-4\" />, category: \"Page Templates\" },\n { id: \"page-product-homepage\", type: \"PageProductHomepage\", label: \"Product Homepage\", icon: <Layout className=\"size-4\" />, category: \"Page Templates\" },\n { id: \"page-reset-password\", type: \"PageResetPassword\", label: \"Reset Password\", icon: <LogIn className=\"size-4\" />, category: \"Page Templates\" },\n { id: \"page-search-bar\", type: \"PageSearchBar\", label: \"Search Bar\", icon: <Search className=\"size-4\" />, category: \"Page Templates\" },\n { id: \"page-sidebar-profile\", type: \"PageSidebarProfile\", label: \"Sidebar Profile\", icon: <User className=\"size-4\" />, category: \"Page Templates\" },\n { id: \"page-standard\", type: \"PageStandard\", label: \"Standard Page\", icon: <Layout className=\"size-4\" />, category: \"Page Templates\" },\n { id: \"page-standard-multistep\", type: \"PageStandardMultistep\", label: \"Standard Multistep\", icon: <List className=\"size-4\" />, category: \"Page Templates\" },\n { id: \"page-standard-search\", type: \"PageStandardSearch\", label: \"Standard Search\", icon: <Search className=\"size-4\" />, category: \"Page Templates\" },\n { id: \"page-vertical-multistep\", type: \"PageVerticalMultistep\", label: \"Vertical Multistep\", icon: <List className=\"size-4\" />, category: \"Page Templates\" },\n { id: \"page-video-chat\", type: \"PageVideoChat\", label: \"Video Chat\", icon: <Video className=\"size-4\" />, category: \"Page Templates\" },\n { id: \"page-video-list\", type: \"PageVideoList\", label: \"Video List\", icon: <Play className=\"size-4\" />, category: \"Page Templates\" },\n\n // =====================\n // BLOCKS\n // =====================\n // Data & Tables\n { id: \"standard-data-table\", type: \"StandardDataTable\", label: \"Data Table\", icon: <Table className=\"size-4\" />, category: \"Blocks\" },\n \n // Cards & Profiles\n { id: \"profile-card\", type: \"ProfileCard\", label: \"Profile Card\", icon: <User className=\"size-4\" />, category: \"Blocks\" },\n { id: \"sidebar-profile-card\", type: \"SidebarProfileCard\", label: \"Sidebar Profile Card\", icon: <User className=\"size-4\" />, category: \"Blocks\" },\n { id: \"profile-info-cards\", type: \"ProfileInfoCards\", label: \"Profile Info Cards\", icon: <LayoutGrid className=\"size-4\" />, category: \"Blocks\" },\n { id: \"sidebar-cards\", type: \"SidebarCards\", label: \"Sidebar Cards\", icon: <Layers className=\"size-4\" />, category: \"Blocks\" },\n { id: \"credit-card-display\", type: \"CreditCardDisplay\", label: \"Credit Card Display\", icon: <CreditCard className=\"size-4\" />, category: \"Blocks\" },\n \n // Navigation & Progress\n { id: \"step-tracker\", type: \"StepTracker\", label: \"Step Tracker\", icon: <List className=\"size-4\" />, category: \"Blocks\" },\n { id: \"vertical-step-tracker\", type: \"VerticalStepTracker\", label: \"Vertical Step Tracker\", icon: <List className=\"size-4\" />, category: \"Blocks\" },\n { id: \"progress-bar\", type: \"ProgressBar\", label: \"Progress Bar\", icon: <SlidersHorizontal className=\"size-4\" />, category: \"Blocks\" },\n { id: \"pill-tabs\", type: \"PillTabs\", label: \"Pill Tabs\", icon: <LayoutGrid className=\"size-4\" />, category: \"Blocks\" },\n { id: \"mobile-bottom-nav\", type: \"MobileBottomNav\", label: \"Mobile Bottom Nav\", icon: <Layout className=\"size-4\" />, category: \"Blocks\" },\n \n // Banners & Headers\n { id: \"flair-banner\", type: \"FlairBanner\", label: \"Flair Banner\", icon: <Type className=\"size-4\" />, category: \"Blocks\" },\n { id: \"gradient-banner\", type: \"GradientBanner\", label: \"Gradient Banner\", icon: <Type className=\"size-4\" />, category: \"Blocks\" },\n { id: \"page-header-section\", type: \"PageHeaderSection\", label: \"Page Header Section\", icon: <FileText className=\"size-4\" />, category: \"Blocks\" },\n \n // Chat & Messaging\n { id: \"messenger-sidebar\", type: \"MessengerSidebar\", label: \"Messenger Sidebar\", icon: <MessageSquare className=\"size-4\" />, category: \"Blocks\" },\n { id: \"chat-message\", type: \"ChatMessage\", label: \"Chat Message\", icon: <MessageSquare className=\"size-4\" />, category: \"Blocks\" },\n \n // Video\n { id: \"video-chat-controls\", type: \"VideoChatControls\", label: \"Video Chat Controls\", icon: <Video className=\"size-4\" />, category: \"Blocks\" },\n { id: \"webcam-preview\", type: \"WebcamPreview\", label: \"Webcam Preview\", icon: <Video className=\"size-4\" />, category: \"Blocks\" },\n { id: \"participant-list\", type: \"ParticipantList\", label: \"Participant List\", icon: <Users className=\"size-4\" />, category: \"Blocks\" },\n { id: \"video-content-section\", type: \"VideoContentSection\", label: \"Video Content Section\", icon: <Play className=\"size-4\" />, category: \"Blocks\" },\n { id: \"video-playlist\", type: \"VideoPlaylist\", label: \"Video Playlist\", icon: <List className=\"size-4\" />, category: \"Blocks\" },\n \n // Search & Filters\n { id: \"search-bar\", type: \"SearchBar\", label: \"Search Bar\", icon: <Search className=\"size-4\" />, category: \"Blocks\" },\n { id: \"filter-popover\", type: \"FilterPopover\", label: \"Filter Popover\", icon: <SlidersHorizontal className=\"size-4\" />, category: \"Blocks\" },\n \n // Forms & Settings\n { id: \"settings-list-row\", type: \"SettingsListRow\", label: \"Settings List Row\", icon: <Settings className=\"size-4\" />, category: \"Blocks\" },\n { id: \"profile-image-uploader\", type: \"ProfileImageUploader\", label: \"Profile Image Uploader\", icon: <Image className=\"size-4\" />, category: \"Blocks\" },\n { id: \"login-branding-panel\", type: \"LoginBrandingPanel\", label: \"Login Branding Panel\", icon: <Layout className=\"size-4\" />, category: \"Blocks\" },\n \n // Marketing - Heroes\n { id: \"hero-section\", type: \"HeroSection\", label: \"Hero Section\", icon: <Image className=\"size-4\" />, category: \"Blocks\" },\n { id: \"hero-dark-with-image\", type: \"HeroDarkWithImage\", label: \"Hero Dark + Image\", icon: <Image className=\"size-4\" />, category: \"Blocks\" },\n { id: \"centered-hero\", type: \"CenteredHero\", label: \"Centered Hero\", icon: <Image className=\"size-4\" />, category: \"Blocks\" },\n \n // Marketing - Social Proof\n { id: \"testimonial-carousel\", type: \"TestimonialCarousel\", label: \"Testimonial Carousel\", icon: <MessageSquare className=\"size-4\" />, category: \"Blocks\" },\n { id: \"reviews-grid\", type: \"ReviewsGrid\", label: \"Reviews Grid\", icon: <Star className=\"size-4\" />, category: \"Blocks\" },\n { id: \"social-proof\", type: \"SocialProof\", label: \"Social Proof (Logos)\", icon: <Award className=\"size-4\" />, category: \"Blocks\" },\n { id: \"metrics-section\", type: \"MetricsSection\", label: \"Metrics Section\", icon: <Hash className=\"size-4\" />, category: \"Blocks\" },\n \n // Marketing - Features\n { id: \"feature-with-image\", type: \"FeatureWithImage\", label: \"Feature + Image\", icon: <Image className=\"size-4\" />, category: \"Blocks\" },\n { id: \"core-values-grid\", type: \"CoreValuesGrid\", label: \"Core Values Grid\", icon: <LayoutGrid className=\"size-4\" />, category: \"Blocks\" },\n { id: \"destination-cards\", type: \"DestinationCards\", label: \"Destination Cards\", icon: <MapPin className=\"size-4\" />, category: \"Blocks\" },\n \n // Marketing - Team\n { id: \"team-cards-grid\", type: \"TeamCardsGrid\", label: \"Team Cards Grid\", icon: <Users className=\"size-4\" />, category: \"Blocks\" },\n { id: \"team-circular-grid\", type: \"TeamCircularGrid\", label: \"Team Circular Grid\", icon: <Users className=\"size-4\" />, category: \"Blocks\" },\n \n // Marketing - CTA & Footer\n { id: \"cta-banner\", type: \"CtaBanner\", label: \"CTA Banner\", icon: <Megaphone className=\"size-4\" />, category: \"Blocks\" },\n { id: \"footer-navbar\", type: \"FooterNavbar\", label: \"Footer Navbar\", icon: <Layout className=\"size-4\" />, category: \"Blocks\" },\n \n // Marketing - Other\n { id: \"featured-news-cards\", type: \"FeaturedNewsCards\", label: \"Featured News Cards\", icon: <Newspaper className=\"size-4\" />, category: \"Blocks\" },\n { id: \"office-locations\", type: \"OfficeLocations\", label: \"Office Locations\", icon: <Building className=\"size-4\" />, category: \"Blocks\" },\n \n // Pricing\n { id: \"pricing-cards\", type: \"PricingCards\", label: \"Pricing Cards\", icon: <CreditCard className=\"size-4\" />, category: \"Blocks\" },\n { id: \"faq-accordion\", type: \"FaqAccordion\", label: \"FAQ Accordion\", icon: <List className=\"size-4\" />, category: \"Blocks\" },\n { id: \"features-comparison\", type: \"FeaturesComparison\", label: \"Features Comparison\", icon: <Table className=\"size-4\" />, category: \"Blocks\" },\n\n // =====================\n // COMPONENTS (UI Primitives)\n // =====================\n { id: \"button\", type: \"Button\", label: \"Button\", icon: <Square className=\"size-4\" />, category: \"Components\" },\n { id: \"checkbox\", type: \"Checkbox\", label: \"Checkbox\", icon: <CheckSquare className=\"size-4\" />, category: \"Components\" },\n { id: \"date-input\", type: \"DateInput\", label: \"Date Input\", icon: <Calendar className=\"size-4\" />, category: \"Components\" },\n { id: \"input\", type: \"Input\", label: \"Text Input\", icon: <Type className=\"size-4\" />, category: \"Components\" },\n { id: \"select\", type: \"Select\", label: \"Select\", icon: <List className=\"size-4\" />, category: \"Components\" },\n { id: \"switch\", type: \"Switch\", label: \"Switch\", icon: <ToggleLeft className=\"size-4\" />, category: \"Components\" },\n { id: \"radio-group\", type: \"RadioGroup\", label: \"Radio Group\", icon: <CircleDot className=\"size-4\" />, category: \"Components\" },\n { id: \"multiselect-tags\", type: \"MultiselectTags\", label: \"Multiselect Tags\", icon: <Tags className=\"size-4\" />, category: \"Components\" },\n { id: \"avatar\", type: \"Avatar\", label: \"Avatar\", icon: <User className=\"size-4\" />, category: \"Components\" },\n { id: \"badge\", type: \"Badge\", label: \"Badge\", icon: <Award className=\"size-4\" />, category: \"Components\" },\n];\n\n// Group components by category\nconst componentsByCategory = paletteComponents.reduce((acc, comp) => {\n if (!acc[comp.category]) {\n acc[comp.category] = [];\n }\n acc[comp.category].push(comp);\n return acc;\n}, {} as Record<string, PaletteComponent[]>);\n\n// Define category order\nconst categoryOrder = [\"Page Templates\", \"Blocks\", \"Components\"];\n\ninterface DraggableComponentProps {\n component: PaletteComponent;\n}\n\nfunction DraggableComponent({ component }: DraggableComponentProps) {\n const [isMounted, setIsMounted] = useState(false);\n \n const { attributes, listeners, setNodeRef, transform, isDragging } = useDraggable({\n id: component.id,\n data: {\n type: component.type,\n label: component.label,\n },\n });\n\n // Only apply dnd-kit attributes after hydration to prevent mismatch\n useEffect(() => {\n setIsMounted(true);\n }, []);\n\n const style = {\n transform: CSS.Transform.toString(transform),\n opacity: isDragging ? 0.5 : 1,\n };\n\n return (\n <div\n ref={setNodeRef}\n style={style}\n // Only spread dnd-kit attributes after client-side mount to avoid hydration mismatch\n {...(isMounted ? attributes : {})}\n {...(isMounted ? listeners : {})}\n className={cn(\n \"flex items-center gap-3 px-3 py-2.5 rounded-md cursor-grab active:cursor-grabbing\",\n \"border border-transparent\",\n \"hover:bg-[var(--canvas-surface)] hover:border-[var(--canvas-border)]\",\n \"transition-colors group\"\n )}\n >\n <div \n className=\"flex items-center justify-center size-8 rounded-md bg-[var(--canvas-surface-brand)] text-[var(--canvas-primary)]\"\n >\n {component.icon}\n </div>\n <span \n className=\"text-[var(--canvas-text)]\"\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n }}\n >\n {component.label}\n </span>\n </div>\n );\n}\n\ninterface CategorySectionProps {\n category: string;\n components: PaletteComponent[];\n defaultExpanded?: boolean;\n}\n\nfunction CategorySection({ category, components, defaultExpanded = true }: CategorySectionProps) {\n const [isExpanded, setIsExpanded] = useState(defaultExpanded);\n\n return (\n <div className=\"mb-2\">\n <button\n onClick={() => setIsExpanded(!isExpanded)}\n className=\"flex items-center gap-2 w-full px-3 py-2 text-left hover:bg-[var(--canvas-surface)] rounded-md transition-colors\"\n >\n {isExpanded ? (\n <ChevronDown className=\"size-4 text-[var(--canvas-text-muted)]\" />\n ) : (\n <ChevronRight className=\"size-4 text-[var(--canvas-text-muted)]\" />\n )}\n <span \n className=\"font-semibold uppercase tracking-wider text-[var(--canvas-text-muted)]\"\n style={{ fontSize: \"var(--typo-body-xs-size)\" }}\n >\n {category}\n </span>\n <span className=\"ml-auto text-[var(--canvas-text-placeholder)]\" style={{ fontSize: \"var(--typo-body-xs-size)\" }}>\n {components.length}\n </span>\n </button>\n \n {isExpanded && (\n <div className=\"mt-1 ml-2 space-y-0.5\">\n {components.map((component) => (\n <DraggableComponent key={component.id} component={component} />\n ))}\n </div>\n )}\n </div>\n );\n}\n\ninterface ComponentPaletteProps {\n className?: string;\n}\n\n/**\n * Component Palette - Sidebar with draggable components\n * \n * Features:\n * - Organized by category (Page Templates, Blocks, Components)\n * - Collapsible sections\n * - Drag to add to canvas\n */\nexport function ComponentPalette({ className }: ComponentPaletteProps) {\n return (\n <aside\n className={cn(\n \"w-[280px] h-full flex flex-col\",\n \"bg-[var(--canvas-background)] border-r border-[var(--canvas-border)]\",\n className\n )}\n >\n {/* Header */}\n <div className=\"px-4 py-4 border-b border-[var(--canvas-border)]\">\n <h2 \n className=\"font-semibold text-[var(--canvas-text)]\"\n style={{\n fontFamily: \"var(--typo-body-m-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n }}\n >\n Components\n </h2>\n <p \n className=\"text-[var(--canvas-text-muted)] mt-1\"\n style={{ fontSize: \"var(--typo-body-xs-size)\" }}\n >\n Drag components onto the canvas\n </p>\n </div>\n\n {/* Component List */}\n <ScrollArea className=\"flex-1\">\n <div className=\"p-3\">\n {categoryOrder.map((category) => {\n const components = componentsByCategory[category];\n if (!components) return null;\n return (\n <CategorySection\n key={category}\n category={category}\n components={components}\n defaultExpanded={category !== \"Page Templates\"} // Collapse templates by default\n />\n );\n })}\n </div>\n </ScrollArea>\n\n {/* Footer hint */}\n <div className=\"px-4 py-3 border-t border-[var(--canvas-border)] bg-[var(--canvas-surface)]\">\n <p className=\"text-[var(--canvas-text-placeholder)]\" style={{ fontSize: \"var(--typo-body-xs-size)\" }}>\n Tip: Press <kbd className=\"px-1.5 py-0.5 bg-[var(--canvas-background)] rounded border border-[var(--canvas-border)]\" style={{ fontSize: \"10px\" }}>Delete</kbd> to remove selected\n </p>\n </div>\n </aside>\n );\n}\n"
|
|
10
10
|
}
|
|
11
11
|
],
|
|
12
12
|
"dependencies": [
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
{
|
|
7
7
|
"path": "components/blocks/component-search.tsx",
|
|
8
8
|
"type": "registry:block",
|
|
9
|
-
"content": "\"use client\";\n\nimport { useState, useMemo } from \"react\";\nimport { Search, X, ChevronDown, ChevronUp } from \"lucide-react\";\nimport { cn } from \"../../lib/utils\";\nimport {\n layoutShells,\n blocks,\n marketingBlocks,\n pricingBlocks,\n videoBlocks,\n pageTemplates,\n} from \"../../lib/component-registry\";\n\n// ═══════════════════════════════════════════════════════════\n// TYPES\n// ═══════════════════════════════════════════════════════════\n\nexport interface ComponentOption {\n id: string;\n name: string;\n category: string;\n path: string;\n description: string;\n}\n\ninterface ComponentSearchProps {\n selectedComponents: ComponentOption[];\n onSelectionChange: (components: ComponentOption[]) => void;\n className?: string;\n}\n\n// ═══════════════════════════════════════════════════════════\n// BUILD COMPONENT OPTIONS FROM REGISTRY\n// ═══════════════════════════════════════════════════════════\n\nfunction buildComponentOptions(): ComponentOption[] {\n const options: ComponentOption[] = [];\n\n // Layout Shells\n Object.entries(layoutShells).forEach(([name, config]) => {\n options.push({\n id: `shell-${name}`,\n name,\n category: \"Layout Shells\",\n path: config.path,\n description: config.description,\n });\n });\n\n // Blocks\n Object.entries(blocks).forEach(([name, config]) => {\n options.push({\n id: `block-${name}`,\n name,\n category: \"Blocks\",\n path: config.path,\n description: config.description,\n });\n });\n\n // Marketing Blocks\n Object.entries(marketingBlocks).forEach(([name, config]) => {\n options.push({\n id: `marketing-${name}`,\n name,\n category: \"Marketing\",\n path: config.path,\n description: config.description,\n });\n });\n\n // Pricing Blocks\n Object.entries(pricingBlocks).forEach(([name, config]) => {\n options.push({\n id: `pricing-${name}`,\n name,\n category: \"Pricing\",\n path: config.path,\n description: config.description,\n });\n });\n\n // Video Blocks\n Object.entries(videoBlocks).forEach(([name, config]) => {\n options.push({\n id: `video-${name}`,\n name,\n category: \"Video/Media\",\n path: config.path,\n description: config.description,\n });\n });\n\n // Page Templates\n Object.entries(pageTemplates).forEach(([name, config]) => {\n options.push({\n id: `template-${name}`,\n name,\n category: \"Page Templates\",\n path: config.path,\n description: config.description,\n });\n });\n\n return options;\n}\n\nconst ALL_COMPONENTS = buildComponentOptions();\nconst CATEGORIES = [...new Set(ALL_COMPONENTS.map((c) => c.category))];\n\n// ═══════════════════════════════════════════════════════════\n// COMPONENT\n// ═══════════════════════════════════════════════════════════\n\nexport function ComponentSearch({\n selectedComponents,\n onSelectionChange,\n className,\n}: ComponentSearchProps) {\n const [searchQuery, setSearchQuery] = useState(\"\");\n const [isOpen, setIsOpen] = useState(false);\n const [expandedCategories, setExpandedCategories] = useState<Set<string>>(\n new Set(CATEGORIES)\n );\n\n // Filter components based on search\n const filteredComponents = useMemo(() => {\n if (!searchQuery.trim()) return ALL_COMPONENTS;\n\n const query = searchQuery.toLowerCase();\n return ALL_COMPONENTS.filter(\n (c) =>\n c.name.toLowerCase().includes(query) ||\n c.description.toLowerCase().includes(query) ||\n c.category.toLowerCase().includes(query)\n );\n }, [searchQuery]);\n\n // Group by category\n const groupedComponents = useMemo(() => {\n const groups: Record<string, ComponentOption[]> = {};\n filteredComponents.forEach((c) => {\n if (!groups[c.category]) groups[c.category] = [];\n groups[c.category].push(c);\n });\n return groups;\n }, [filteredComponents]);\n\n const toggleCategory = (category: string) => {\n setExpandedCategories((prev) => {\n const next = new Set(prev);\n if (next.has(category)) {\n next.delete(category);\n } else {\n next.add(category);\n }\n return next;\n });\n };\n\n const toggleComponent = (component: ComponentOption) => {\n const isSelected = selectedComponents.some((c) => c.id === component.id);\n if (isSelected) {\n onSelectionChange(selectedComponents.filter((c) => c.id !== component.id));\n } else {\n onSelectionChange([...selectedComponents, component]);\n }\n };\n\n const removeComponent = (componentId: string) => {\n onSelectionChange(selectedComponents.filter((c) => c.id !== componentId));\n };\n\n return (\n <div className={cn(\"space-y-3\", className)}>\n {/* Selected Components Chips */}\n {selectedComponents.length > 0 && (\n <div className=\"flex flex-wrap gap-2\">\n {selectedComponents.map((component) => (\n <div\n key={component.id}\n className=\"flex items-center gap-1.5 px-2.5 py-1 rounded-full bg-[var(--canvas-surface-brand)] text-[var(--canvas-primary)] text-sm\"\n >\n <span className=\"font-medium\">{component.name}</span>\n <button\n onClick={() => removeComponent(component.id)}\n className=\"p-0.5 rounded-full hover:bg-[var(--canvas-primary)]/20 transition-colors\"\n >\n <X className=\"size-3\" />\n </button>\n </div>\n ))}\n </div>\n )}\n\n {/* Search Input */}\n <div className=\"relative\">\n <div\n className={cn(\n \"flex items-center gap-2 px-3 py-2.5 rounded-lg border bg-[var(--canvas-background)] cursor-text\",\n isOpen\n ? \"border-[var(--canvas-primary)] ring-2 ring-[var(--canvas-primary)]/20\"\n : \"border-[var(--canvas-border)]\"\n )}\n onClick={() => setIsOpen(true)}\n >\n <Search className=\"size-4 text-[var(--canvas-text-muted)]\" />\n <input\n type=\"text\"\n value={searchQuery}\n onChange={(e) => setSearchQuery(e.target.value)}\n onFocus={() => setIsOpen(true)}\n placeholder=\"Search components...\"\n className=\"flex-1 bg-transparent text-sm text-[var(--canvas-text)] placeholder:text-[var(--canvas-text-placeholder)] outline-none\"\n />\n <span className=\"text-xs text-[var(--canvas-text-muted)]\">\n {selectedComponents.length} selected\n </span>\n </div>\n\n {/* Dropdown */}\n {isOpen && (\n <>\n {/* Backdrop */}\n <div\n className=\"fixed inset-0 z-10\"\n onClick={() => setIsOpen(false)}\n />\n\n {/* Options List */}\n <div className=\"absolute top-full left-0 right-0 mt-1 z-20 max-h-[400px] overflow-y-auto rounded-lg border border-[var(--canvas-border)] bg-[var(--canvas-background)] shadow-lg\">\n {Object.entries(groupedComponents).map(([category, components]) => (\n <div key={category}>\n {/* Category Header */}\n <button\n onClick={() => toggleCategory(category)}\n className=\"w-full flex items-center justify-between px-3 py-2 bg-[var(--canvas-surface)] border-b border-[var(--canvas-border)] text-xs font-semibold text-[var(--canvas-text-muted)] uppercase tracking-wide hover:bg-[var(--canvas-surface-brand)]/50\"\n >\n <span>\n {category} ({components.length})\n </span>\n {expandedCategories.has(category) ? (\n <ChevronUp className=\"size-3\" />\n ) : (\n <ChevronDown className=\"size-3\" />\n )}\n </button>\n\n {/* Components in Category */}\n {expandedCategories.has(category) && (\n <div>\n {components.map((component) => {\n const isSelected = selectedComponents.some(\n (c) => c.id === component.id\n );\n return (\n <button\n key={component.id}\n onClick={() => toggleComponent(component)}\n className={cn(\n \"w-full flex items-start gap-3 px-3 py-2.5 text-left transition-colors border-b border-[var(--canvas-border)]/50 last:border-b-0\",\n isSelected\n ? \"bg-[var(--canvas-surface-brand)]/50\"\n : \"hover:bg-[var(--canvas-surface)]\"\n )}\n >\n {/* Checkbox */}\n <div\n className={cn(\n \"size-4 rounded border mt-0.5 flex items-center justify-center shrink-0\",\n isSelected\n ? \"bg-[var(--canvas-primary)] border-[var(--canvas-primary)]\"\n : \"border-[var(--canvas-border)]\"\n )}\n >\n {isSelected && (\n <svg\n className=\"size-3 text-white\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n strokeWidth={3}\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n d=\"M5 13l4 4L19 7\"\n />\n </svg>\n )}\n </div>\n\n {/* Component Info */}\n <div className=\"flex-1 min-w-0\">\n <div className=\"flex items-center gap-2\">\n <span\n className={cn(\n \"text-sm font-medium\",\n isSelected\n ? \"text-[var(--canvas-primary)]\"\n : \"text-[var(--canvas-text)]\"\n )}\n >\n {component.name}\n </span>\n <span className=\"text-xs text-[var(--canvas-text-placeholder)]\">\n {component.path}\n </span>\n </div>\n <p className=\"text-xs text-[var(--canvas-text-muted)] mt-0.5 line-clamp-2\">\n {component.description}\n </p>\n </div>\n </button>\n );\n })}\n </div>\n )}\n </div>\n ))}\n\n {filteredComponents.length === 0 && (\n <div className=\"px-3 py-6 text-center text-sm text-[var(--canvas-text-muted)]\">\n No components found for \"{searchQuery}\"\n </div>\n )}\n </div>\n </>\n )}\n </div>\n </div>\n );\n}\n"
|
|
9
|
+
"content": "\"use client\";\n\nimport { useState, useMemo } from \"react\";\nimport { Search, X, ChevronDown, ChevronUp } from \"lucide-react\";\nimport { cn } from \"../../lib/utils\";\nimport {\n layoutShells,\n blocks,\n marketingBlocks,\n pricingBlocks,\n videoBlocks,\n pageTemplates,\n} from \"../../lib/component-registry\";\n\n// ═══════════════════════════════════════════════════════════\n// TYPES\n// ═══════════════════════════════════════════════════════════\n\nexport interface ComponentOption {\n id: string;\n name: string;\n category: string;\n path: string;\n description: string;\n}\n\ninterface ComponentSearchProps {\n selectedComponents: ComponentOption[];\n onSelectionChange: (components: ComponentOption[]) => void;\n className?: string;\n}\n\n// ═══════════════════════════════════════════════════════════\n// BUILD COMPONENT OPTIONS FROM REGISTRY\n// ═══════════════════════════════════════════════════════════\n\nfunction buildComponentOptions(): ComponentOption[] {\n const options: ComponentOption[] = [];\n\n // Layout Shells\n Object.entries(layoutShells).forEach(([name, config]) => {\n options.push({\n id: `shell-${name}`,\n name,\n category: \"Layout Shells\",\n path: config.path,\n description: config.description,\n });\n });\n\n // Blocks\n Object.entries(blocks).forEach(([name, config]) => {\n options.push({\n id: `block-${name}`,\n name,\n category: \"Blocks\",\n path: config.path,\n description: config.description,\n });\n });\n\n // Marketing Blocks\n Object.entries(marketingBlocks).forEach(([name, config]) => {\n options.push({\n id: `marketing-${name}`,\n name,\n category: \"Marketing\",\n path: config.path,\n description: config.description,\n });\n });\n\n // Pricing Blocks\n Object.entries(pricingBlocks).forEach(([name, config]) => {\n options.push({\n id: `pricing-${name}`,\n name,\n category: \"Pricing\",\n path: config.path,\n description: config.description,\n });\n });\n\n // Video Blocks\n Object.entries(videoBlocks).forEach(([name, config]) => {\n options.push({\n id: `video-${name}`,\n name,\n category: \"Video/Media\",\n path: config.path,\n description: config.description,\n });\n });\n\n // Page Templates\n Object.entries(pageTemplates).forEach(([name, config]) => {\n options.push({\n id: `template-${name}`,\n name,\n category: \"Page Templates\",\n path: config.path,\n description: config.description,\n });\n });\n\n return options;\n}\n\nconst ALL_COMPONENTS = buildComponentOptions();\nconst CATEGORIES = [...new Set(ALL_COMPONENTS.map((c) => c.category))];\n\n// ═══════════════════════════════════════════════════════════\n// COMPONENT\n// ═══════════════════════════════════════════════════════════\n\nexport function ComponentSearch({\n selectedComponents,\n onSelectionChange,\n className,\n}: ComponentSearchProps) {\n const [searchQuery, setSearchQuery] = useState(\"\");\n const [isOpen, setIsOpen] = useState(false);\n const [expandedCategories, setExpandedCategories] = useState<Set<string>>(\n new Set(CATEGORIES)\n );\n\n // Filter components based on search\n const filteredComponents = useMemo(() => {\n if (!searchQuery.trim()) return ALL_COMPONENTS;\n\n const query = searchQuery.toLowerCase();\n return ALL_COMPONENTS.filter(\n (c) =>\n c.name.toLowerCase().includes(query) ||\n c.description.toLowerCase().includes(query) ||\n c.category.toLowerCase().includes(query)\n );\n }, [searchQuery]);\n\n // Group by category\n const groupedComponents = useMemo(() => {\n const groups: Record<string, ComponentOption[]> = {};\n filteredComponents.forEach((c) => {\n if (!groups[c.category]) groups[c.category] = [];\n groups[c.category].push(c);\n });\n return groups;\n }, [filteredComponents]);\n\n const toggleCategory = (category: string) => {\n setExpandedCategories((prev) => {\n const next = new Set(prev);\n if (next.has(category)) {\n next.delete(category);\n } else {\n next.add(category);\n }\n return next;\n });\n };\n\n const toggleComponent = (component: ComponentOption) => {\n const isSelected = selectedComponents.some((c) => c.id === component.id);\n if (isSelected) {\n onSelectionChange(selectedComponents.filter((c) => c.id !== component.id));\n } else {\n onSelectionChange([...selectedComponents, component]);\n }\n };\n\n const removeComponent = (componentId: string) => {\n onSelectionChange(selectedComponents.filter((c) => c.id !== componentId));\n };\n\n return (\n <div className={cn(\"space-y-3\", className)}>\n {/* Selected Components Chips */}\n {selectedComponents.length > 0 && (\n <div className=\"flex flex-wrap gap-2\">\n {selectedComponents.map((component) => (\n <div\n key={component.id}\n className=\"flex items-center gap-1.5 px-2.5 py-1 rounded-full bg-[var(--canvas-surface-brand)] text-[var(--canvas-primary)]\"\n style={{ fontSize: \"var(--typo-body-s-size)\" }}\n >\n <span className=\"font-medium\">{component.name}</span>\n <button\n onClick={() => removeComponent(component.id)}\n className=\"p-0.5 rounded-full hover:bg-[var(--canvas-primary)]/20 transition-colors\"\n >\n <X className=\"size-3\" />\n </button>\n </div>\n ))}\n </div>\n )}\n\n {/* Search Input */}\n <div className=\"relative\">\n <div\n className={cn(\n \"flex items-center gap-2 px-3 py-2.5 rounded-lg border bg-[var(--canvas-background)] cursor-text\",\n isOpen\n ? \"border-[var(--canvas-primary)] ring-2 ring-[var(--canvas-primary)]/20\"\n : \"border-[var(--canvas-border)]\"\n )}\n onClick={() => setIsOpen(true)}\n >\n <Search className=\"size-4 text-[var(--canvas-text-muted)]\" />\n <input\n type=\"text\"\n value={searchQuery}\n onChange={(e) => setSearchQuery(e.target.value)}\n onFocus={() => setIsOpen(true)}\n placeholder=\"Search components...\"\n className=\"flex-1 bg-transparent text-[var(--canvas-text)] placeholder:text-[var(--canvas-text-placeholder)] outline-none\"\n style={{ fontSize: \"var(--typo-body-s-size)\" }}\n />\n <span className=\"text-[var(--canvas-text-muted)]\" style={{ fontSize: \"var(--typo-body-xs-size)\" }}>\n {selectedComponents.length} selected\n </span>\n </div>\n\n {/* Dropdown */}\n {isOpen && (\n <>\n {/* Backdrop */}\n <div\n className=\"fixed inset-0 z-10\"\n onClick={() => setIsOpen(false)}\n />\n\n {/* Options List */}\n <div className=\"absolute top-full left-0 right-0 mt-1 z-20 max-h-[400px] overflow-y-auto rounded-lg border border-[var(--canvas-border)] bg-[var(--canvas-background)] shadow-lg\">\n {Object.entries(groupedComponents).map(([category, components]) => (\n <div key={category}>\n {/* Category Header */}\n <button\n onClick={() => toggleCategory(category)}\n className=\"w-full flex items-center justify-between px-3 py-2 bg-[var(--canvas-surface)] border-b border-[var(--canvas-border)] font-semibold text-[var(--canvas-text-muted)] uppercase tracking-wide hover:bg-[var(--canvas-surface-brand)]/50\"\n style={{ fontSize: \"var(--typo-body-xs-size)\" }}\n >\n <span>\n {category} ({components.length})\n </span>\n {expandedCategories.has(category) ? (\n <ChevronUp className=\"size-3\" />\n ) : (\n <ChevronDown className=\"size-3\" />\n )}\n </button>\n\n {/* Components in Category */}\n {expandedCategories.has(category) && (\n <div>\n {components.map((component) => {\n const isSelected = selectedComponents.some(\n (c) => c.id === component.id\n );\n return (\n <button\n key={component.id}\n onClick={() => toggleComponent(component)}\n className={cn(\n \"w-full flex items-start gap-3 px-3 py-2.5 text-left transition-colors border-b border-[var(--canvas-border)]/50 last:border-b-0\",\n isSelected\n ? \"bg-[var(--canvas-surface-brand)]/50\"\n : \"hover:bg-[var(--canvas-surface)]\"\n )}\n >\n {/* Checkbox */}\n <div\n className={cn(\n \"size-4 rounded border mt-0.5 flex items-center justify-center shrink-0\",\n isSelected\n ? \"bg-[var(--canvas-primary)] border-[var(--canvas-primary)]\"\n : \"border-[var(--canvas-border)]\"\n )}\n >\n {isSelected && (\n <svg\n className=\"size-3 text-[var(--canvas-primary-foreground)]\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n strokeWidth={3}\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n d=\"M5 13l4 4L19 7\"\n />\n </svg>\n )}\n </div>\n\n {/* Component Info */}\n <div className=\"flex-1 min-w-0\">\n <div className=\"flex items-center gap-2\">\n <span\n className={cn(\n \"font-medium\",\n isSelected\n ? \"text-[var(--canvas-primary)]\"\n : \"text-[var(--canvas-text)]\"\n )}\n style={{ fontSize: \"var(--typo-body-s-size)\" }}\n >\n {component.name}\n </span>\n <span className=\"text-[var(--canvas-text-placeholder)]\" style={{ fontSize: \"var(--typo-body-xs-size)\" }}>\n {component.path}\n </span>\n </div>\n <p className=\"text-[var(--canvas-text-muted)] mt-0.5 line-clamp-2\" style={{ fontSize: \"var(--typo-body-xs-size)\" }}>\n {component.description}\n </p>\n </div>\n </button>\n );\n })}\n </div>\n )}\n </div>\n ))}\n\n {filteredComponents.length === 0 && (\n <div className=\"px-3 py-6 text-center text-[var(--canvas-text-muted)]\" style={{ fontSize: \"var(--typo-body-s-size)\" }}>\n No components found for \"{searchQuery}\"\n </div>\n )}\n </div>\n </>\n )}\n </div>\n </div>\n );\n}\n"
|
|
10
10
|
}
|
|
11
11
|
],
|
|
12
12
|
"dependencies": [
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
{
|
|
7
7
|
"path": "components/blocks/content-dropzone.tsx",
|
|
8
8
|
"type": "registry:block",
|
|
9
|
-
"content": "import { cn } from \"../../lib/utils\";\nimport { ReactNode, Children } from \"react\";\n\ninterface ContentDropzoneProps {\n /** Height of the dropzone (only applies when empty) */\n height?: string;\n /** Additional class names */\n className?: string;\n /** Content elements */\n children?: ReactNode;\n}\n\n/**\n * Canvas Design System - Content Dropzone\n * \n * A placeholder component representing where content blocks would be inserted.\n * When empty, shows a dashed border placeholder.\n * When children are added, becomes a flex column with 40px gap spacing.\n */\nexport function ContentDropzone({ \n height = \"480px\",\n className,\n children,\n}: ContentDropzoneProps) {\n const hasChildren = Children.count(children) > 0;\n\n if (hasChildren) {\n return (\n <div \n className={cn(\n \"flex flex-col gap-10\",\n className\n )}\n >\n {children}\n </div>\n );\n }\n\n return (\n <div \n className={cn(\n \"flex items-center justify-center\",\n \"bg-
|
|
9
|
+
"content": "import { cn } from \"../../lib/utils\";\nimport { ReactNode, Children } from \"react\";\n\ninterface ContentDropzoneProps {\n /** Height of the dropzone (only applies when empty) */\n height?: string;\n /** Additional class names */\n className?: string;\n /** Content elements */\n children?: ReactNode;\n}\n\n/**\n * Canvas Design System - Content Dropzone\n * \n * A placeholder component representing where content blocks would be inserted.\n * When empty, shows a dashed border placeholder.\n * When children are added, becomes a flex column with 40px gap spacing.\n */\nexport function ContentDropzone({ \n height = \"480px\",\n className,\n children,\n}: ContentDropzoneProps) {\n const hasChildren = Children.count(children) > 0;\n\n if (hasChildren) {\n return (\n <div \n className={cn(\n \"flex flex-col gap-10\",\n className\n )}\n >\n {children}\n </div>\n );\n }\n\n return (\n <div \n className={cn(\n \"flex items-center justify-center\",\n \"bg-[var(--canvas-background)]\",\n \"border-2 border-dashed border-[var(--canvas-border)]\",\n \"rounded-[var(--radius-nav)]\",\n \"overflow-hidden\",\n className\n )}\n style={{ minHeight: height }}\n />\n );\n}\n\n"
|
|
10
10
|
}
|
|
11
11
|
],
|
|
12
12
|
"dependencies": [],
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
{
|
|
7
7
|
"path": "components/blocks/credit-card-display.tsx",
|
|
8
8
|
"type": "registry:block",
|
|
9
|
-
"content": "\"use client\";\n\nimport { cn } from \"../../lib/utils\";\n\ninterface CreditCardDisplayProps {\n /** Card type (visa, mastercard, amex, etc.) */\n cardType?: \"visa\" | \"mastercard\" | \"amex\" | \"discover\";\n /** Last 4 digits of card number */\n lastFourDigits?: string;\n /** Cardholder name */\n cardholderName?: string;\n /** Expiry date (MM/YY format) */\n expiryDate?: string;\n /** Additional class name */\n className?: string;\n}\n\n/**\n * Credit Card Display\n * \n * A visual credit card representation for payment settings.\n * Shows card type, masked number, cardholder name, and expiry.\n * Uses the flair background color with a subtle lighting gradient.\n */\nexport function CreditCardDisplay({\n cardType = \"visa\",\n lastFourDigits = \"1234\",\n cardholderName = \"Card Holder\",\n expiryDate = \"12/24\",\n className,\n}: CreditCardDisplayProps) {\n // Card type logos (simplified text representations)\n const cardTypeDisplay: Record<string, { name: string }> = {\n visa: { name: \"VISA\" },\n mastercard: { name: \"Mastercard\" },\n amex: { name: \"AMEX\" },\n discover: { name: \"Discover\" },\n };\n\n const cardInfo = cardTypeDisplay[cardType] || cardTypeDisplay.visa;\n\n return (\n <div\n className={cn(\n \"relative w-[375px] h-[240px] rounded-[var(--radius-lg)] overflow-hidden\",\n \"p-6 flex flex-col justify-between\",\n \"shadow-lg\",\n className\n )}\n style={{\n background: `linear-gradient(135deg, \n color-mix(in srgb, var(--canvas-flair-bg) 100%, white 0%) 0%, \n var(--canvas-flair-bg) 50%, \n color-mix(in srgb, var(--canvas-flair-bg) 100%, black 20%) 100%)`,\n }}\n >\n {/* Lighting overlay for depth */}\n <div \n className=\"absolute inset-0 pointer-events-none\"\n style={{\n background: `linear-gradient(145deg, \n rgba(255,255,255,0.15) 0%, \n rgba(255,255,255,0.05) 30%, \n transparent 50%, \n rgba(0,0,0,0.1) 100%)`,\n }}\n />\n\n {/* Top row - Card type logo */}\n <div className=\"relative flex justify-between items-start\">\n <span\n className=\"text-white font-bold italic\"\n style={{\n fontSize: \"28px\",\n letterSpacing: \"1px\",\n fontFamily: \"var(--typo-global-font)\",\n }}\n >\n {cardInfo.name}\n </span>\n </div>\n\n {/* Middle - Card number */}\n <div className=\"relative flex items-center gap-4\">\n <span \n className=\"text-white tracking-[4px]\"\n style={{\n fontFamily: \"monospace\",\n fontSize: \"18px\",\n }}\n >\n ****\n </span>\n <span \n className=\"text-white tracking-[4px]\"\n style={{\n fontFamily: \"monospace\",\n fontSize: \"18px\",\n }}\n >\n ****\n </span>\n <span \n className=\"text-white tracking-[4px]\"\n style={{\n fontFamily: \"monospace\",\n fontSize: \"18px\",\n }}\n >\n ****\n </span>\n <span \n className=\"text-white tracking-[4px]\"\n style={{\n fontFamily: \"monospace\",\n fontSize: \"18px\",\n }}\n >\n {lastFourDigits}\n </span>\n </div>\n\n {/* Bottom row - Name and Expiry */}\n <div className=\"relative flex justify-between items-end\">\n <div>\n <span \n className=\"text-white/70
|
|
9
|
+
"content": "\"use client\";\n\nimport { cn } from \"../../lib/utils\";\n\ninterface CreditCardDisplayProps {\n /** Card type (visa, mastercard, amex, etc.) */\n cardType?: \"visa\" | \"mastercard\" | \"amex\" | \"discover\";\n /** Last 4 digits of card number */\n lastFourDigits?: string;\n /** Cardholder name */\n cardholderName?: string;\n /** Expiry date (MM/YY format) */\n expiryDate?: string;\n /** Additional class name */\n className?: string;\n}\n\n/**\n * Credit Card Display\n * \n * A visual credit card representation for payment settings.\n * Shows card type, masked number, cardholder name, and expiry.\n * Uses the flair background color with a subtle lighting gradient.\n */\nexport function CreditCardDisplay({\n cardType = \"visa\",\n lastFourDigits = \"1234\",\n cardholderName = \"Card Holder\",\n expiryDate = \"12/24\",\n className,\n}: CreditCardDisplayProps) {\n // Card type logos (simplified text representations)\n const cardTypeDisplay: Record<string, { name: string }> = {\n visa: { name: \"VISA\" },\n mastercard: { name: \"Mastercard\" },\n amex: { name: \"AMEX\" },\n discover: { name: \"Discover\" },\n };\n\n const cardInfo = cardTypeDisplay[cardType] || cardTypeDisplay.visa;\n\n return (\n <div\n className={cn(\n \"relative w-[375px] h-[240px] rounded-[var(--radius-lg)] overflow-hidden\",\n \"p-6 flex flex-col justify-between\",\n \"shadow-lg\",\n className\n )}\n style={{\n background: `linear-gradient(135deg, \n color-mix(in srgb, var(--canvas-flair-bg) 100%, white 0%) 0%, \n var(--canvas-flair-bg) 50%, \n color-mix(in srgb, var(--canvas-flair-bg) 100%, black 20%) 100%)`,\n }}\n >\n {/* Lighting overlay for depth */}\n <div \n className=\"absolute inset-0 pointer-events-none\"\n style={{\n background: `linear-gradient(145deg, \n rgba(255,255,255,0.15) 0%, \n rgba(255,255,255,0.05) 30%, \n transparent 50%, \n rgba(0,0,0,0.1) 100%)`,\n }}\n />\n\n {/* Top row - Card type logo */}\n <div className=\"relative flex justify-between items-start\">\n <span\n className=\"text-white font-bold italic\"\n style={{\n fontSize: \"28px\",\n letterSpacing: \"1px\",\n fontFamily: \"var(--typo-global-font)\",\n }}\n >\n {cardInfo.name}\n </span>\n </div>\n\n {/* Middle - Card number */}\n <div className=\"relative flex items-center gap-4\">\n <span \n className=\"text-white tracking-[4px]\"\n style={{\n fontFamily: \"monospace\",\n fontSize: \"18px\",\n }}\n >\n ****\n </span>\n <span \n className=\"text-white tracking-[4px]\"\n style={{\n fontFamily: \"monospace\",\n fontSize: \"18px\",\n }}\n >\n ****\n </span>\n <span \n className=\"text-white tracking-[4px]\"\n style={{\n fontFamily: \"monospace\",\n fontSize: \"18px\",\n }}\n >\n ****\n </span>\n <span \n className=\"text-white tracking-[4px]\"\n style={{\n fontFamily: \"monospace\",\n fontSize: \"18px\",\n }}\n >\n {lastFourDigits}\n </span>\n </div>\n\n {/* Bottom row - Name and Expiry */}\n <div className=\"relative flex justify-between items-end\">\n <div>\n <span \n className=\"text-white/70 block mb-1\"\n style={{ fontFamily: \"var(--typo-global-font)\", fontSize: \"var(--typo-body-xs-size)\" }}\n >\n CARDHOLDER\n </span>\n <span \n className=\"text-white font-medium\"\n style={{\n fontSize: \"var(--typo-body-m-size)\",\n fontFamily: \"var(--typo-global-font)\",\n }}\n >\n {cardholderName}\n </span>\n </div>\n <div className=\"text-right\">\n <span \n className=\"text-white/70 block mb-1\"\n style={{ fontFamily: \"var(--typo-global-font)\", fontSize: \"var(--typo-body-xs-size)\" }}\n >\n EXPIRES\n </span>\n <span \n className=\"text-white font-medium\"\n style={{\n fontSize: \"var(--typo-body-m-size)\",\n fontFamily: \"var(--typo-global-font)\",\n }}\n >\n {expiryDate}\n </span>\n </div>\n </div>\n </div>\n );\n}\n"
|
|
10
10
|
}
|
|
11
11
|
],
|
|
12
12
|
"dependencies": [],
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
{
|
|
7
7
|
"path": "components/blocks/custom-component-helper.tsx",
|
|
8
8
|
"type": "registry:block",
|
|
9
|
-
"content": "\"use client\";\n\nimport { useState, useMemo } from \"react\";\nimport { Check, Copy, Sparkles } from \"lucide-react\";\nimport { cn } from \"../../lib/utils\";\nimport { ComponentSearch, type ComponentOption } from \"./component-search\";\nimport { projectContext } from \"../../data/project-context\";\n\n// ═══════════════════════════════════════════════════════════\n// TYPES\n// ═══════════════════════════════════════════════════════════\n\ntype ComponentType = \"block\" | \"page-template\" | \"ui-component\";\n\ninterface CustomComponentHelperProps {\n className?: string;\n}\n\n// ═══════════════════════════════════════════════════════════\n// COMPONENT\n// ═══════════════════════════════════════════════════════════\n\nexport function CustomComponentHelper({ className }: CustomComponentHelperProps) {\n const [componentType, setComponentType] = useState<ComponentType>(\"block\");\n const [componentName, setComponentName] = useState(\"\");\n const [componentDescription, setComponentDescription] = useState(\"\");\n const [referenceComponents, setReferenceComponents] = useState<ComponentOption[]>([]);\n const [copied, setCopied] = useState(false);\n\n const { personas } = projectContext;\n\n // Generate the prompt\n const generatedPrompt = useMemo(() => {\n if (!componentName && !componentDescription) {\n return \"\";\n }\n\n const typeLabel = {\n block: \"block\",\n \"page-template\": \"page template\",\n \"ui-component\": \"UI component\",\n }[componentType];\n\n const parts: string[] = [\n \"Please create a plan for the following, then wait for my approval before making changes:\",\n \"\",\n ];\n\n // Context references\n parts.push(\"CONTEXT:\");\n parts.push(\"- Read src/data/scope.md for project scope and requirements\");\n parts.push(\"- Reference src/data/project-context.ts for user personas and project goals\");\n parts.push(\"\");\n\n parts.push(`Create a new ${typeLabel} component:`);\n parts.push(\"\");\n\n if (componentName) {\n parts.push(`Name: ${componentName}`);\n }\n\n parts.push(`Type: ${typeLabel.charAt(0).toUpperCase() + typeLabel.slice(1)}`);\n\n if (componentDescription) {\n parts.push(`Description: ${componentDescription}`);\n }\n\n // Include personas for context\n if (personas.length > 0) {\n parts.push(\"\");\n parts.push(\"Design this component to serve these user personas:\");\n personas.forEach((p) => {\n parts.push(`- ${p.name} (${p.role})`);\n });\n }\n\n // Reference components\n if (referenceComponents.length > 0) {\n parts.push(\"\");\n parts.push(\"Reference these existing components for style/patterns:\");\n referenceComponents.forEach((c) => {\n parts.push(`- ${c.name} (${c.path})`);\n });\n }\n\n // Requirements\n parts.push(\"\");\n parts.push(\"Requirements:\");\n parts.push(\"1. Build using ShadCN primitives (Button, Dialog, Input, etc.)\");\n parts.push(\"2. Implement CSS variables for theming:\");\n parts.push(\" - var(--canvas-*) for colors (primary, background, text, border, surface)\");\n parts.push(\" - var(--spacing-*) for spacing (sm, md, lg, xl)\");\n parts.push(\" - var(--radius-*) for border radius\");\n parts.push(\" - var(--typo-*) for typography\");\n\n // File location based on type\n const kebabName = toKebabCase(componentName || \"new-component\");\n if (componentType === \"block\") {\n parts.push(`3. Create file at src/components/blocks/${kebabName}.tsx`);\n parts.push(\"4. Export from src/components/blocks/index.ts\");\n } else if (componentType === \"page-template\") {\n const pageName = toKebabCase(componentName || \"new-page\");\n parts.push(`3. Create page at src/app/${pageName}/page.tsx`);\n parts.push(`4. Create layout at src/app/${pageName}/layout.tsx`);\n } else {\n parts.push(`3. Create file at src/components/ui/${kebabName}.tsx`);\n parts.push(\"4. Export from src/components/ui/index.ts (if exists)\");\n }\n\n parts.push(\"5. Add entry to src/lib/component-registry.ts after creation\");\n parts.push(\"6. Follow existing component patterns in the codebase\");\n parts.push(\"7. Ensure the component aligns with the project scope and serves the target personas\");\n\n return parts.join(\"\\n\");\n }, [componentType, componentName, componentDescription, referenceComponents, personas]);\n\n const handleCopy = async () => {\n if (!generatedPrompt) return;\n await navigator.clipboard.writeText(generatedPrompt);\n setCopied(true);\n setTimeout(() => setCopied(false), 2000);\n };\n\n const hasContent = componentName || componentDescription;\n\n return (\n <div className={cn(\"space-y-6\", className)}>\n {/* Section Header */}\n <div>\n <h3 className=\"text-lg font-semibold text-[var(--canvas-text)]\">\n Create Custom Component\n </h3>\n <p className=\"text-sm text-[var(--canvas-text-muted)] mt-1\">\n Generate a prompt to create a new ShadCN-based component with design variables\n </p>\n </div>\n\n {/* Component Type */}\n <div className=\"space-y-2\">\n <label className=\"text-sm font-medium text-[var(--canvas-text)]\">\n Component Type\n </label>\n <div className=\"flex gap-2\">\n {[\n { id: \"block\", label: \"Block\" },\n { id: \"page-template\", label: \"Page Template\" },\n { id: \"ui-component\", label: \"UI Component\" },\n ].map((type) => (\n <button\n key={type.id}\n onClick={() => setComponentType(type.id as ComponentType)}\n className={cn(\n \"px-4 py-2 rounded-lg text-sm font-medium transition-all\",\n componentType === type.id\n ? \"bg-[var(--canvas-primary)] text-white\"\n : \"bg-[var(--canvas-surface)] text-[var(--canvas-text-muted)] border border-[var(--canvas-border)] hover:border-[var(--canvas-primary)] hover:text-[var(--canvas-primary)]\"\n )}\n >\n {type.label}\n </button>\n ))}\n </div>\n </div>\n\n {/* Component Name */}\n <div className=\"space-y-2\">\n <label\n htmlFor=\"component-name\"\n className=\"text-sm font-medium text-[var(--canvas-text)]\"\n >\n Component Name\n </label>\n <input\n id=\"component-name\"\n type=\"text\"\n value={componentName}\n onChange={(e) => setComponentName(e.target.value)}\n placeholder=\"e.g., MultiStepPopup, ImageCarousel, StatCard\"\n className=\"w-full px-3 py-2 rounded-lg border border-[var(--canvas-border)] bg-[var(--canvas-background)] text-[var(--canvas-text)] text-sm placeholder:text-[var(--canvas-text-placeholder)] focus:outline-none focus:ring-2 focus:ring-[var(--canvas-primary)] focus:border-transparent\"\n />\n </div>\n\n {/* Component Description */}\n <div className=\"space-y-2\">\n <label\n htmlFor=\"component-description\"\n className=\"text-sm font-medium text-[var(--canvas-text)]\"\n >\n Description\n </label>\n <textarea\n id=\"component-description\"\n value={componentDescription}\n onChange={(e) => setComponentDescription(e.target.value)}\n placeholder=\"Describe what this component should do, its features, and any specific requirements (e.g., a multi-step popup with steps listed on the left side and content on the right, progress indicator, next/back buttons)\"\n rows={4}\n className=\"w-full px-3 py-2 rounded-lg border border-[var(--canvas-border)] bg-[var(--canvas-background)] text-[var(--canvas-text)] text-sm placeholder:text-[var(--canvas-text-placeholder)] focus:outline-none focus:ring-2 focus:ring-[var(--canvas-primary)] focus:border-transparent resize-none\"\n />\n </div>\n\n {/* Reference Components */}\n <div className=\"space-y-2\">\n <label className=\"text-sm font-medium text-[var(--canvas-text)]\">\n Reference Components (optional)\n </label>\n <p className=\"text-xs text-[var(--canvas-text-muted)] mb-2\">\n Select existing components to use as style/pattern references\n </p>\n <ComponentSearch\n selectedComponents={referenceComponents}\n onSelectionChange={setReferenceComponents}\n />\n </div>\n\n {/* Generated Prompt Preview */}\n {hasContent && (\n <div className=\"rounded-lg border border-dashed border-[var(--canvas-border)] bg-[var(--canvas-surface)] p-4\">\n {/* Header */}\n <div className=\"flex items-center justify-between mb-3\">\n <div className=\"flex items-center gap-2 text-sm font-medium text-[var(--canvas-text-muted)]\">\n <Sparkles className=\"size-4 text-[var(--canvas-primary)]\" />\n Generated Prompt\n </div>\n <button\n onClick={handleCopy}\n disabled={!generatedPrompt}\n className={cn(\n \"flex items-center gap-1.5 px-2.5 py-1.5 rounded-md text-xs font-medium transition-all\",\n copied\n ? \"bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400\"\n : \"bg-[var(--canvas-background)] text-[var(--canvas-text-muted)] hover:text-[var(--canvas-text)] border border-[var(--canvas-border)] hover:border-[var(--canvas-primary)]\"\n )}\n >\n {copied ? (\n <>\n <Check className=\"size-3\" />\n Copied!\n </>\n ) : (\n <>\n <Copy className=\"size-3\" />\n Copy prompt\n </>\n )}\n </button>\n </div>\n\n {/* Prompt Text */}\n <pre className=\"text-sm text-[var(--canvas-text)] leading-relaxed font-mono whitespace-pre-wrap bg-[var(--canvas-background)] rounded-md p-3 border border-[var(--canvas-border)] max-h-[300px] overflow-y-auto\">\n {generatedPrompt}\n </pre>\n </div>\n )}\n\n {/* Empty State */}\n {!hasContent && (\n <div className=\"rounded-lg border-2 border-dashed border-[var(--canvas-border)] bg-[var(--canvas-surface)] p-8 text-center\">\n <p className=\"text-sm text-[var(--canvas-text-muted)]\">\n Enter a component name or description to generate a prompt\n </p>\n </div>\n )}\n </div>\n );\n}\n\n// ═══════════════════════════════════════════════════════════\n// HELPERS\n// ═══════════════════════════════════════════════════════════\n\nfunction toKebabCase(str: string): string {\n return str\n .replace(/([a-z])([A-Z])/g, \"$1-$2\")\n .replace(/\\s+/g, \"-\")\n .toLowerCase();\n}\n"
|
|
9
|
+
"content": "\"use client\";\n\nimport { useState, useMemo } from \"react\";\nimport { Check, Copy, Sparkles } from \"lucide-react\";\nimport { cn } from \"../../lib/utils\";\nimport { ComponentSearch, type ComponentOption } from \"./component-search\";\nimport { projectContext } from \"../../data/project-context\";\n\n// ═══════════════════════════════════════════════════════════\n// TYPES\n// ═══════════════════════════════════════════════════════════\n\ntype ComponentType = \"block\" | \"page-template\" | \"ui-component\";\n\ninterface CustomComponentHelperProps {\n className?: string;\n}\n\n// ═══════════════════════════════════════════════════════════\n// COMPONENT\n// ═══════════════════════════════════════════════════════════\n\nexport function CustomComponentHelper({ className }: CustomComponentHelperProps) {\n const [componentType, setComponentType] = useState<ComponentType>(\"block\");\n const [componentName, setComponentName] = useState(\"\");\n const [componentDescription, setComponentDescription] = useState(\"\");\n const [referenceComponents, setReferenceComponents] = useState<ComponentOption[]>([]);\n const [copied, setCopied] = useState(false);\n\n const { personas } = projectContext;\n\n // Generate the prompt\n const generatedPrompt = useMemo(() => {\n if (!componentName && !componentDescription) {\n return \"\";\n }\n\n const typeLabel = {\n block: \"block\",\n \"page-template\": \"page template\",\n \"ui-component\": \"UI component\",\n }[componentType];\n\n const parts: string[] = [\n \"Please create a plan for the following, then wait for my approval before making changes:\",\n \"\",\n ];\n\n // Context references\n parts.push(\"CONTEXT:\");\n parts.push(\"- Read src/data/scope.md for project scope and requirements\");\n parts.push(\"- Reference src/data/project-context.ts for user personas and project goals\");\n parts.push(\"\");\n\n parts.push(`Create a new ${typeLabel} component:`);\n parts.push(\"\");\n\n if (componentName) {\n parts.push(`Name: ${componentName}`);\n }\n\n parts.push(`Type: ${typeLabel.charAt(0).toUpperCase() + typeLabel.slice(1)}`);\n\n if (componentDescription) {\n parts.push(`Description: ${componentDescription}`);\n }\n\n // Include personas for context\n if (personas.length > 0) {\n parts.push(\"\");\n parts.push(\"Design this component to serve these user personas:\");\n personas.forEach((p) => {\n parts.push(`- ${p.name} (${p.role})`);\n });\n }\n\n // Reference components\n if (referenceComponents.length > 0) {\n parts.push(\"\");\n parts.push(\"Reference these existing components for style/patterns:\");\n referenceComponents.forEach((c) => {\n parts.push(`- ${c.name} (${c.path})`);\n });\n }\n\n // Requirements\n parts.push(\"\");\n parts.push(\"Requirements:\");\n parts.push(\"1. Build using ShadCN primitives (Button, Dialog, Input, etc.)\");\n parts.push(\"2. Implement CSS variables for theming:\");\n parts.push(\" - var(--canvas-*) for colors (primary, background, text, border, surface)\");\n parts.push(\" - var(--spacing-*) for spacing (sm, md, lg, xl)\");\n parts.push(\" - var(--radius-*) for border radius\");\n parts.push(\" - var(--typo-*) for typography\");\n\n // File location based on type\n const kebabName = toKebabCase(componentName || \"new-component\");\n if (componentType === \"block\") {\n parts.push(`3. Create file at src/components/blocks/${kebabName}.tsx`);\n parts.push(\"4. Export from src/components/blocks/index.ts\");\n } else if (componentType === \"page-template\") {\n const pageName = toKebabCase(componentName || \"new-page\");\n parts.push(`3. Create page at src/app/${pageName}/page.tsx`);\n parts.push(`4. Create layout at src/app/${pageName}/layout.tsx`);\n } else {\n parts.push(`3. Create file at src/components/ui/${kebabName}.tsx`);\n parts.push(\"4. Export from src/components/ui/index.ts (if exists)\");\n }\n\n parts.push(\"5. Add entry to src/lib/component-registry.ts after creation\");\n parts.push(\"6. Follow existing component patterns in the codebase\");\n parts.push(\"7. Ensure the component aligns with the project scope and serves the target personas\");\n\n return parts.join(\"\\n\");\n }, [componentType, componentName, componentDescription, referenceComponents, personas]);\n\n const handleCopy = async () => {\n if (!generatedPrompt) return;\n await navigator.clipboard.writeText(generatedPrompt);\n setCopied(true);\n setTimeout(() => setCopied(false), 2000);\n };\n\n const hasContent = componentName || componentDescription;\n\n return (\n <div className={cn(\"space-y-6\", className)}>\n {/* Section Header */}\n <div>\n <h3 className=\"font-semibold text-[var(--canvas-text)]\" style={{ fontSize: \"var(--typo-body-l-size)\" }}>\n Create Custom Component\n </h3>\n <p className=\"text-[var(--canvas-text-muted)] mt-1\" style={{ fontSize: \"var(--typo-body-s-size)\" }}>\n Generate a prompt to create a new ShadCN-based component with design variables\n </p>\n </div>\n\n {/* Component Type */}\n <div className=\"space-y-2\">\n <label className=\"text-[var(--canvas-text)]\" style={{ fontSize: \"var(--typo-body-s-size)\", fontWeight: 500 }}>\n Component Type\n </label>\n <div className=\"flex gap-2\">\n {[\n { id: \"block\", label: \"Block\" },\n { id: \"page-template\", label: \"Page Template\" },\n { id: \"ui-component\", label: \"UI Component\" },\n ].map((type) => (\n <button\n key={type.id}\n onClick={() => setComponentType(type.id as ComponentType)}\n className={cn(\n \"px-4 py-2 rounded-lg font-medium transition-all\",\n componentType === type.id\n ? \"bg-[var(--canvas-primary)] text-[var(--canvas-primary-foreground)]\"\n : \"bg-[var(--canvas-surface)] text-[var(--canvas-text-muted)] border border-[var(--canvas-border)] hover:border-[var(--canvas-primary)] hover:text-[var(--canvas-primary)]\"\n )}\n style={{ fontSize: \"var(--typo-body-s-size)\" }}\n >\n {type.label}\n </button>\n ))}\n </div>\n </div>\n\n {/* Component Name */}\n <div className=\"space-y-2\">\n <label\n htmlFor=\"component-name\"\n className=\"text-[var(--canvas-text)]\" style={{ fontSize: \"var(--typo-body-s-size)\", fontWeight: 500 }}\n >\n Component Name\n </label>\n <input\n id=\"component-name\"\n type=\"text\"\n value={componentName}\n onChange={(e) => setComponentName(e.target.value)}\n placeholder=\"e.g., MultiStepPopup, ImageCarousel, StatCard\"\n className=\"w-full px-3 py-2 rounded-lg border border-[var(--canvas-border)] bg-[var(--canvas-background)] text-[var(--canvas-text)] placeholder:text-[var(--canvas-text-placeholder)] focus:outline-none focus:ring-2 focus:ring-[var(--canvas-primary)] focus:border-transparent\"\n style={{ fontSize: \"var(--typo-body-s-size)\" }}\n />\n </div>\n\n {/* Component Description */}\n <div className=\"space-y-2\">\n <label\n htmlFor=\"component-description\"\n className=\"text-[var(--canvas-text)]\" style={{ fontSize: \"var(--typo-body-s-size)\", fontWeight: 500 }}\n >\n Description\n </label>\n <textarea\n id=\"component-description\"\n value={componentDescription}\n onChange={(e) => setComponentDescription(e.target.value)}\n placeholder=\"Describe what this component should do, its features, and any specific requirements (e.g., a multi-step popup with steps listed on the left side and content on the right, progress indicator, next/back buttons)\"\n rows={4}\n className=\"w-full px-3 py-2 rounded-lg border border-[var(--canvas-border)] bg-[var(--canvas-background)] text-[var(--canvas-text)] placeholder:text-[var(--canvas-text-placeholder)] focus:outline-none focus:ring-2 focus:ring-[var(--canvas-primary)] focus:border-transparent resize-none\"\n style={{ fontSize: \"var(--typo-body-s-size)\" }}\n />\n </div>\n\n {/* Reference Components */}\n <div className=\"space-y-2\">\n <label className=\"text-[var(--canvas-text)]\" style={{ fontSize: \"var(--typo-body-s-size)\", fontWeight: 500 }}>\n Reference Components (optional)\n </label>\n <p className=\"text-[var(--canvas-text-muted)] mb-2\" style={{ fontSize: \"var(--typo-body-xs-size)\" }}>\n Select existing components to use as style/pattern references\n </p>\n <ComponentSearch\n selectedComponents={referenceComponents}\n onSelectionChange={setReferenceComponents}\n />\n </div>\n\n {/* Generated Prompt Preview */}\n {hasContent && (\n <div className=\"rounded-lg border border-dashed border-[var(--canvas-border)] bg-[var(--canvas-surface)] p-4\">\n {/* Header */}\n <div className=\"flex items-center justify-between mb-3\">\n <div className=\"flex items-center gap-2 font-medium text-[var(--canvas-text-muted)]\" style={{ fontSize: \"var(--typo-body-s-size)\" }}>\n <Sparkles className=\"size-4 text-[var(--canvas-primary)]\" />\n Generated Prompt\n </div>\n <button\n onClick={handleCopy}\n disabled={!generatedPrompt}\n className={cn(\n \"flex items-center gap-1.5 px-2.5 py-1.5 rounded-md font-medium transition-all\",\n copied\n ? \"bg-[var(--canvas-success-surface)] text-[var(--canvas-success)]\"\n : \"bg-[var(--canvas-background)] text-[var(--canvas-text-muted)] hover:text-[var(--canvas-text)] border border-[var(--canvas-border)] hover:border-[var(--canvas-primary)]\"\n )}\n style={{ fontSize: \"var(--typo-body-xs-size)\" }}\n >\n {copied ? (\n <>\n <Check className=\"size-3\" />\n Copied!\n </>\n ) : (\n <>\n <Copy className=\"size-3\" />\n Copy prompt\n </>\n )}\n </button>\n </div>\n\n {/* Prompt Text */}\n <pre className=\"text-[var(--canvas-text)] leading-relaxed font-mono whitespace-pre-wrap bg-[var(--canvas-background)] rounded-md p-3 border border-[var(--canvas-border)] max-h-[300px] overflow-y-auto\" style={{ fontSize: \"var(--typo-body-s-size)\" }}>\n {generatedPrompt}\n </pre>\n </div>\n )}\n\n {/* Empty State */}\n {!hasContent && (\n <div className=\"rounded-lg border-2 border-dashed border-[var(--canvas-border)] bg-[var(--canvas-surface)] p-8 text-center\">\n <p className=\"text-[var(--canvas-text-muted)]\" style={{ fontSize: \"var(--typo-body-s-size)\" }}>\n Enter a component name or description to generate a prompt\n </p>\n </div>\n )}\n </div>\n );\n}\n\n// ═══════════════════════════════════════════════════════════\n// HELPERS\n// ═══════════════════════════════════════════════════════════\n\nfunction toKebabCase(str: string): string {\n return str\n .replace(/([a-z])([A-Z])/g, \"$1-$2\")\n .replace(/\\s+/g, \"-\")\n .toLowerCase();\n}\n"
|
|
10
10
|
}
|
|
11
11
|
],
|
|
12
12
|
"dependencies": [
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
{
|
|
7
7
|
"path": "components/blocks/empty-state.tsx",
|
|
8
8
|
"type": "registry:block",
|
|
9
|
-
"content": "\"use client\";\n\nimport { cn } from \"../../lib/utils\";\n\ninterface EmptyStateProps {\n icon: string;\n title: string;\n description?: string;\n className?: string;\n}\n\nexport function EmptyState({ icon, title, description, className }: EmptyStateProps) {\n return (\n <div\n className={cn(\n \"flex flex-col items-center justify-center py-12 px-6\",\n \"rounded-xl border-2 border-dashed border-[var(--canvas-border)]\",\n \"bg-[var(--canvas-surface)]\",\n className\n )}\n >\n <span className=\"text-4xl mb-3\">{icon}</span>\n <p className=\"text-[var(--canvas-text-muted)] font-medium text-center\">\n {title}\n </p>\n {description && (\n <p className=\"text-
|
|
9
|
+
"content": "\"use client\";\n\nimport { cn } from \"../../lib/utils\";\n\ninterface EmptyStateProps {\n icon: string;\n title: string;\n description?: string;\n className?: string;\n}\n\nexport function EmptyState({ icon, title, description, className }: EmptyStateProps) {\n return (\n <div\n className={cn(\n \"flex flex-col items-center justify-center py-12 px-6\",\n \"rounded-xl border-2 border-dashed border-[var(--canvas-border)]\",\n \"bg-[var(--canvas-surface)]\",\n className\n )}\n >\n <span className=\"text-4xl mb-3\">{icon}</span>\n <p className=\"text-[var(--canvas-text-muted)] font-medium text-center\">\n {title}\n </p>\n {description && (\n <p className=\"text-[var(--canvas-text-placeholder)] text-center mt-1 max-w-md\" style={{ fontSize: \"var(--typo-body-s-size)\" }}>\n {description}\n </p>\n )}\n </div>\n );\n}\n"
|
|
10
10
|
}
|
|
11
11
|
],
|
|
12
12
|
"dependencies": [],
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
{
|
|
7
7
|
"path": "components/blocks/filter-popover.tsx",
|
|
8
8
|
"type": "registry:block",
|
|
9
|
-
"content": "\"use client\";\n\nimport { useState, useEffect } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport { Filter, ChevronDown } from \"lucide-react\";\nimport { Button } from \"../ui/button\";\nimport {\n Popover,\n PopoverContent,\n PopoverTrigger,\n} from \"../ui/popover\";\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from \"../ui/select\";\nimport { CheckboxWithLabel, Checkbox } from \"../ui/checkbox\";\nimport { RadioGroup, RadioGroupItem } from \"../ui/radio-group\";\nimport { Switch } from \"../ui/switch\";\nimport { TextInput } from \"../ui/text-input\";\nimport { Searchbox } from \"../ui/searchbox\";\nimport { DateInput } from \"../ui/date-input\";\nimport { MultiselectTags } from \"../ui/multiselect-tags\";\n\n// ============================================\n// Filter Option Types\n// ============================================\n\nexport interface FilterOption {\n id: string;\n label: string;\n}\n\nexport interface FilterDropdownConfig {\n id: string;\n label: string;\n placeholder: string;\n options: FilterOption[];\n}\n\nexport interface FilterCheckboxGroupConfig {\n id: string;\n label: string;\n options: FilterOption[];\n}\n\nexport interface FilterDateRangeConfig {\n id: string;\n label: string;\n startPlaceholder?: string;\n endPlaceholder?: string;\n}\n\nexport interface FilterState {\n dropdowns: Record<string, string>;\n checkboxes: Record<string, boolean>;\n dateRange: { start: string; end: string };\n}\n\n// ============================================\n// Default Filter Configuration\n// ============================================\n\nconst defaultDropdowns: FilterDropdownConfig[] = [\n {\n id: \"category\",\n label: \"Category\",\n placeholder: \"All categories\",\n options: [\n { id: \"all\", label: \"All categories\" },\n { id: \"restaurants\", label: \"Restaurants\" },\n { id: \"hotels\", label: \"Hotels\" },\n { id: \"attractions\", label: \"Attractions\" },\n ],\n },\n {\n id: \"location\",\n label: \"Location\",\n placeholder: \"All locations\",\n options: [\n { id: \"all\", label: \"All locations\" },\n { id: \"new-york\", label: \"New York\" },\n { id: \"los-angeles\", label: \"Los Angeles\" },\n { id: \"chicago\", label: \"Chicago\" },\n ],\n },\n];\n\nconst defaultCheckboxGroup: FilterCheckboxGroupConfig = {\n id: \"status\",\n label: \"Status\",\n options: [\n { id: \"active\", label: \"Active\" },\n { id: \"pending\", label: \"Pending\" },\n { id: \"completed\", label: \"Completed\" },\n ],\n};\n\nconst defaultDateRange: FilterDateRangeConfig = {\n id: \"dateRange\",\n label: \"Date Range\",\n startPlaceholder: \"Start date\",\n endPlaceholder: \"End date\",\n};\n\n// ============================================\n// Filter Dropdown Component\n// ============================================\n\ninterface FilterDropdownProps {\n config: FilterDropdownConfig;\n value: string;\n onChange: (value: string) => void;\n}\n\nfunction FilterDropdown({ config, value, onChange }: FilterDropdownProps) {\n return (\n <div className=\"space-y-2\">\n <label className=\"text-sm font-medium text-[var(--canvas-text-muted)]\">\n {config.label}\n </label>\n <Select value={value || undefined} onValueChange={onChange}>\n <SelectTrigger inputSize=\"sm\">\n <SelectValue placeholder={config.placeholder} />\n </SelectTrigger>\n <SelectContent position=\"popper\" side=\"bottom\" sideOffset={4}>\n {config.options.map((option) => (\n <SelectItem key={option.id} value={option.id}>\n {option.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n );\n}\n\n// ============================================\n// Filter Popover Component\n// ============================================\n\ninterface FilterPopoverProps {\n /** Dropdown filter configurations */\n dropdowns?: FilterDropdownConfig[];\n /** Checkbox group configuration */\n checkboxGroup?: FilterCheckboxGroupConfig;\n /** Date range configuration */\n dateRange?: FilterDateRangeConfig;\n /** Current filter state */\n filterState?: FilterState;\n /** Callback when filters are applied */\n onApply?: (state: FilterState) => void;\n /** Callback when filters are cleared */\n onClear?: () => void;\n /** Trigger style variant - \"button\" for button style, \"dropdown\" for select-like style */\n triggerVariant?: \"button\" | \"dropdown\";\n /** Placeholder text shown in the trigger (used for both variants) */\n triggerPlaceholder?: string;\n /** Additional class names for the trigger */\n className?: string;\n}\n\n/**\n * Canvas Design System - Filter Popover Component\n * \n * A filter button that opens a popover with various filter options.\n * Inspired by shadcnstudio Category Filter 6 structure.\n * \n * @example\n * ```tsx\n * // Button variant (default)\n * <FilterPopover\n * onApply={(filters) => console.log(filters)}\n * onClear={() => console.log(\"Cleared\")}\n * />\n * \n * // Dropdown variant\n * <FilterPopover\n * triggerVariant=\"dropdown\"\n * triggerPlaceholder=\"Filter\"\n * onApply={(filters) => console.log(filters)}\n * />\n * ```\n */\nexport function FilterPopover({\n dropdowns = defaultDropdowns,\n checkboxGroup = defaultCheckboxGroup,\n dateRange = defaultDateRange,\n filterState,\n onApply,\n onClear,\n triggerVariant = \"button\",\n triggerPlaceholder = \"Filter\",\n className,\n}: FilterPopoverProps) {\n const [isOpen, setIsOpen] = useState(false);\n const [mounted, setMounted] = useState(false);\n\n // Ensure hydration consistency for Radix components\n useEffect(() => {\n setMounted(true);\n }, []);\n\n // Internal state for filter values\n const [localState, setLocalState] = useState<FilterState>(() => ({\n dropdowns: filterState?.dropdowns || {},\n checkboxes: filterState?.checkboxes || {},\n dateRange: filterState?.dateRange || { start: \"\", end: \"\" },\n }));\n\n const handleDropdownChange = (id: string, value: string) => {\n setLocalState((prev) => ({\n ...prev,\n dropdowns: { ...prev.dropdowns, [id]: value },\n }));\n };\n\n const handleCheckboxChange = (id: string, checked: boolean) => {\n setLocalState((prev) => ({\n ...prev,\n checkboxes: { ...prev.checkboxes, [id]: checked },\n }));\n };\n\n const handleDateChange = (field: \"start\" | \"end\", value: string) => {\n setLocalState((prev) => ({\n ...prev,\n dateRange: { ...prev.dateRange, [field]: value },\n }));\n };\n\n const handleClear = () => {\n const clearedState: FilterState = {\n dropdowns: {},\n checkboxes: {},\n dateRange: { start: \"\", end: \"\" },\n };\n setLocalState(clearedState);\n onClear?.();\n };\n\n const handleApply = () => {\n onApply?.(localState);\n setIsOpen(false);\n };\n\n const handleCancel = () => {\n // Reset to initial state\n setLocalState({\n dropdowns: filterState?.dropdowns || {},\n checkboxes: filterState?.checkboxes || {},\n dateRange: filterState?.dateRange || { start: \"\", end: \"\" },\n });\n setIsOpen(false);\n };\n\n // Button trigger component\n const ButtonTrigger = (\n <Button\n variant=\"neutral\"\n className={cn(\"gap-2\", className)}\n style={{ \n borderRadius: \"var(--btn-standard-radius)\",\n height: \"var(--btn-standard-height)\",\n paddingLeft: \"var(--btn-standard-px)\",\n paddingRight: \"var(--btn-standard-px)\",\n fontSize: \"var(--btn-standard-font-size)\",\n }}\n >\n <Filter className=\"size-4\" />\n {triggerPlaceholder}\n </Button>\n );\n\n // Dropdown trigger component (styled like SelectTrigger)\n const DropdownTrigger = (\n <button\n className={cn(\n \"flex items-center justify-between gap-2 bg-white border text-[var(--canvas-text)] whitespace-nowrap transition-colors outline-none focus:border-[var(--canvas-border-input-focus)] focus:ring-2 focus:ring-[var(--canvas-border-input-focus)] focus:ring-offset-2 data-[state=open]:border-[var(--canvas-border-input-focus)]\",\n className\n )}\n style={{\n width: \"120px\",\n height: \"var(--input-small-height)\",\n paddingLeft: \"var(--input-small-px)\",\n paddingRight: \"var(--input-small-px)\",\n fontSize: \"var(--input-small-font-size)\",\n borderRadius: \"var(--input-small-radius)\",\n borderColor: \"var(--canvas-border-input)\",\n }}\n >\n <span className=\"text-[var(--canvas-text-placeholder)]\">{triggerPlaceholder}</span>\n <ChevronDown className=\"size-4 opacity-50\" />\n </button>\n );\n\n // Render placeholder during SSR to prevent hydration mismatch\n if (!mounted) {\n return triggerVariant === \"dropdown\" ? DropdownTrigger : ButtonTrigger;\n }\n\n return (\n <Popover open={isOpen} onOpenChange={setIsOpen}>\n <PopoverTrigger asChild>\n {triggerVariant === \"dropdown\" ? DropdownTrigger : ButtonTrigger}\n </PopoverTrigger>\n\n <PopoverContent\n align=\"end\"\n side=\"bottom\"\n sideOffset={4}\n avoidCollisions={false}\n className=\"w-80 p-0 bg-white border border-[var(--canvas-border)] shadow-lg\"\n >\n {/* Filter Content - All Input Types */}\n <div className=\"p-4 space-y-5 max-h-[480px] overflow-y-auto\">\n {/* Text Input */}\n <div className=\"space-y-2\">\n <label className=\"text-sm font-medium text-[var(--canvas-text-muted)]\">\n Text Input\n </label>\n <TextInput inputSize=\"sm\" placeholder=\"Enter text...\" />\n </div>\n\n {/* Searchbox */}\n <div className=\"space-y-2\">\n <label className=\"text-sm font-medium text-[var(--canvas-text-muted)]\">\n Searchbox\n </label>\n <Searchbox inputSize=\"sm\" placeholder=\"Search...\" />\n </div>\n\n {/* Dropdown */}\n {dropdowns.slice(0, 1).map((dropdown) => (\n <FilterDropdown\n key={dropdown.id}\n config={dropdown}\n value={localState.dropdowns[dropdown.id] || \"\"}\n onChange={(value) => handleDropdownChange(dropdown.id, value)}\n />\n ))}\n\n {/* Date Input */}\n <div className=\"space-y-2\">\n <label className=\"text-sm font-medium text-[var(--canvas-text-muted)]\">\n Date Input\n </label>\n <DateInput inputSize=\"sm\" />\n </div>\n\n\n {/* Radio Buttons */}\n <div className=\"space-y-2\">\n <label className=\"text-sm font-medium text-[var(--canvas-text-muted)]\">\n Radio Buttons\n </label>\n <RadioGroup defaultValue=\"option1\" className=\"flex\">\n <div className=\"flex items-center gap-2\">\n <RadioGroupItem value=\"option1\" id=\"radio1\" />\n <label \n htmlFor=\"radio1\" \n className=\"text-[var(--canvas-text)] cursor-pointer\"\n style={{ fontSize: \"var(--input-small-font-size)\" }}\n >\n Option 1\n </label>\n </div>\n <div className=\"flex items-center gap-2\">\n <RadioGroupItem value=\"option2\" id=\"radio2\" />\n <label \n htmlFor=\"radio2\" \n className=\"text-[var(--canvas-text)] cursor-pointer\"\n style={{ fontSize: \"var(--input-small-font-size)\" }}\n >\n Option 2\n </label>\n </div>\n </RadioGroup>\n </div>\n\n {/* Radio Buttons List */}\n <div className=\"space-y-2\">\n <label className=\"text-sm font-medium text-[var(--canvas-text-muted)]\">\n Radio Buttons List\n </label>\n <RadioGroup defaultValue=\"list-opt1\" className=\"flex flex-col gap-2\">\n <div className=\"flex items-center gap-2\">\n <RadioGroupItem value=\"list-opt1\" id=\"radio-list-1\" />\n <label \n htmlFor=\"radio-list-1\" \n className=\"text-[var(--canvas-text)] cursor-pointer\"\n style={{ fontSize: \"var(--input-small-font-size)\" }}\n >\n First option\n </label>\n </div>\n <div className=\"flex items-center gap-2\">\n <RadioGroupItem value=\"list-opt2\" id=\"radio-list-2\" />\n <label \n htmlFor=\"radio-list-2\" \n className=\"text-[var(--canvas-text)] cursor-pointer\"\n style={{ fontSize: \"var(--input-small-font-size)\" }}\n >\n Second option\n </label>\n </div>\n <div className=\"flex items-center gap-2\">\n <RadioGroupItem value=\"list-opt3\" id=\"radio-list-3\" />\n <label \n htmlFor=\"radio-list-3\" \n className=\"text-[var(--canvas-text)] cursor-pointer\"\n style={{ fontSize: \"var(--input-small-font-size)\" }}\n >\n Third option\n </label>\n </div>\n </RadioGroup>\n </div>\n\n {/* Checkbox */}\n <div className=\"space-y-2\">\n <label className=\"text-sm font-medium text-[var(--canvas-text-muted)]\">\n Checkbox\n </label>\n <div className=\"flex items-center gap-2\">\n <Checkbox id=\"single-checkbox\" />\n <label \n htmlFor=\"single-checkbox\" \n className=\"text-[var(--canvas-text)] cursor-pointer\"\n style={{ fontSize: \"var(--input-small-font-size)\" }}\n >\n I agree to terms\n </label>\n </div>\n </div>\n\n {/* Checkbox List */}\n {checkboxGroup && (\n <div className=\"space-y-2\">\n <label className=\"text-sm font-medium text-[var(--canvas-text-muted)]\">\n Checkbox List\n </label>\n <div className=\"space-y-2\">\n {checkboxGroup.options.map((option) => (\n <CheckboxWithLabel\n key={option.id}\n checked={localState.checkboxes[option.id] || false}\n onCheckedChange={(checked) =>\n handleCheckboxChange(option.id, checked === true)\n }\n >\n {option.label}\n </CheckboxWithLabel>\n ))}\n </div>\n </div>\n )}\n\n {/* Toggle */}\n <div className=\"space-y-2\">\n <label className=\"text-sm font-medium text-[var(--canvas-text-muted)]\">\n Toggle\n </label>\n <div className=\"flex items-center gap-2\">\n <Switch id=\"toggle-switch\" />\n <label \n htmlFor=\"toggle-switch\" \n className=\"text-[var(--canvas-text)] cursor-pointer\"\n style={{ fontSize: \"var(--input-small-font-size)\" }}\n >\n Enable notifications\n </label>\n </div>\n </div>\n\n {/* Multiselect Tags */}\n <div className=\"space-y-2\">\n <label className=\"text-sm font-medium text-[var(--canvas-text-muted)]\">\n Multiselect Tags\n </label>\n <MultiselectTags inputSize=\"sm\" tags={[\"Tag 1\", \"Tag 2\"]} placeholder=\"Add...\" />\n </div>\n </div>\n\n {/* Footer */}\n <div className=\"flex items-center justify-between px-4 py-3 border-t border-[var(--canvas-border)]\">\n <Button\n variant=\"ghost\"\n size=\"sm\"\n onClick={handleClear}\n className=\"text-[var(--canvas-text-muted)] hover:text-[var(--canvas-text)] hover:bg-transparent\"\n >\n Reset\n </Button>\n <Button variant=\"primary\" size=\"sm\" onClick={handleApply}>\n Apply\n </Button>\n </div>\n </PopoverContent>\n </Popover>\n );\n}\n\n"
|
|
9
|
+
"content": "\"use client\";\n\nimport { useState, useEffect } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport { Filter, ChevronDown } from \"lucide-react\";\nimport { Button } from \"../ui/button\";\nimport {\n Popover,\n PopoverContent,\n PopoverTrigger,\n} from \"../ui/popover\";\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from \"../ui/select\";\nimport { CheckboxWithLabel, Checkbox } from \"../ui/checkbox\";\nimport { RadioGroup, RadioGroupItem } from \"../ui/radio-group\";\nimport { Switch } from \"../ui/switch\";\nimport { TextInput } from \"../ui/text-input\";\nimport { Searchbox } from \"../ui/searchbox\";\nimport { DateInput } from \"../ui/date-input\";\nimport { MultiselectTags } from \"../ui/multiselect-tags\";\n\n// ============================================\n// Filter Option Types\n// ============================================\n\nexport interface FilterOption {\n id: string;\n label: string;\n}\n\nexport interface FilterDropdownConfig {\n id: string;\n label: string;\n placeholder: string;\n options: FilterOption[];\n}\n\nexport interface FilterCheckboxGroupConfig {\n id: string;\n label: string;\n options: FilterOption[];\n}\n\nexport interface FilterDateRangeConfig {\n id: string;\n label: string;\n startPlaceholder?: string;\n endPlaceholder?: string;\n}\n\nexport interface FilterState {\n dropdowns: Record<string, string>;\n checkboxes: Record<string, boolean>;\n dateRange: { start: string; end: string };\n}\n\n// ============================================\n// Default Filter Configuration\n// ============================================\n\nconst defaultDropdowns: FilterDropdownConfig[] = [\n {\n id: \"category\",\n label: \"Category\",\n placeholder: \"All categories\",\n options: [\n { id: \"all\", label: \"All categories\" },\n { id: \"restaurants\", label: \"Restaurants\" },\n { id: \"hotels\", label: \"Hotels\" },\n { id: \"attractions\", label: \"Attractions\" },\n ],\n },\n {\n id: \"location\",\n label: \"Location\",\n placeholder: \"All locations\",\n options: [\n { id: \"all\", label: \"All locations\" },\n { id: \"new-york\", label: \"New York\" },\n { id: \"los-angeles\", label: \"Los Angeles\" },\n { id: \"chicago\", label: \"Chicago\" },\n ],\n },\n];\n\nconst defaultCheckboxGroup: FilterCheckboxGroupConfig = {\n id: \"status\",\n label: \"Status\",\n options: [\n { id: \"active\", label: \"Active\" },\n { id: \"pending\", label: \"Pending\" },\n { id: \"completed\", label: \"Completed\" },\n ],\n};\n\nconst defaultDateRange: FilterDateRangeConfig = {\n id: \"dateRange\",\n label: \"Date Range\",\n startPlaceholder: \"Start date\",\n endPlaceholder: \"End date\",\n};\n\n// ============================================\n// Filter Dropdown Component\n// ============================================\n\ninterface FilterDropdownProps {\n config: FilterDropdownConfig;\n value: string;\n onChange: (value: string) => void;\n}\n\nfunction FilterDropdown({ config, value, onChange }: FilterDropdownProps) {\n return (\n <div className=\"space-y-2\">\n <label className=\"text-[var(--canvas-text-muted)]\" style={{ fontSize: \"var(--typo-input-label-size)\", fontWeight: \"var(--typo-input-label-weight)\" }}>\n {config.label}\n </label>\n <Select value={value || undefined} onValueChange={onChange}>\n <SelectTrigger inputSize=\"sm\">\n <SelectValue placeholder={config.placeholder} />\n </SelectTrigger>\n <SelectContent position=\"popper\" side=\"bottom\" sideOffset={4}>\n {config.options.map((option) => (\n <SelectItem key={option.id} value={option.id}>\n {option.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n );\n}\n\n// ============================================\n// Filter Popover Component\n// ============================================\n\ninterface FilterPopoverProps {\n /** Dropdown filter configurations */\n dropdowns?: FilterDropdownConfig[];\n /** Checkbox group configuration */\n checkboxGroup?: FilterCheckboxGroupConfig;\n /** Date range configuration */\n dateRange?: FilterDateRangeConfig;\n /** Current filter state */\n filterState?: FilterState;\n /** Callback when filters are applied */\n onApply?: (state: FilterState) => void;\n /** Callback when filters are cleared */\n onClear?: () => void;\n /** Trigger style variant - \"button\" for button style, \"dropdown\" for select-like style */\n triggerVariant?: \"button\" | \"dropdown\";\n /** Placeholder text shown in the trigger (used for both variants) */\n triggerPlaceholder?: string;\n /** Additional class names for the trigger */\n className?: string;\n}\n\n/**\n * Canvas Design System - Filter Popover Component\n * \n * A filter button that opens a popover with various filter options.\n * Inspired by shadcnstudio Category Filter 6 structure.\n * \n * @example\n * ```tsx\n * // Button variant (default)\n * <FilterPopover\n * onApply={(filters) => console.log(filters)}\n * onClear={() => console.log(\"Cleared\")}\n * />\n * \n * // Dropdown variant\n * <FilterPopover\n * triggerVariant=\"dropdown\"\n * triggerPlaceholder=\"Filter\"\n * onApply={(filters) => console.log(filters)}\n * />\n * ```\n */\nexport function FilterPopover({\n dropdowns = defaultDropdowns,\n checkboxGroup = defaultCheckboxGroup,\n dateRange = defaultDateRange,\n filterState,\n onApply,\n onClear,\n triggerVariant = \"button\",\n triggerPlaceholder = \"Filter\",\n className,\n}: FilterPopoverProps) {\n const [isOpen, setIsOpen] = useState(false);\n const [mounted, setMounted] = useState(false);\n\n // Ensure hydration consistency for Radix components\n useEffect(() => {\n setMounted(true);\n }, []);\n\n // Internal state for filter values\n const [localState, setLocalState] = useState<FilterState>(() => ({\n dropdowns: filterState?.dropdowns || {},\n checkboxes: filterState?.checkboxes || {},\n dateRange: filterState?.dateRange || { start: \"\", end: \"\" },\n }));\n\n const handleDropdownChange = (id: string, value: string) => {\n setLocalState((prev) => ({\n ...prev,\n dropdowns: { ...prev.dropdowns, [id]: value },\n }));\n };\n\n const handleCheckboxChange = (id: string, checked: boolean) => {\n setLocalState((prev) => ({\n ...prev,\n checkboxes: { ...prev.checkboxes, [id]: checked },\n }));\n };\n\n const handleDateChange = (field: \"start\" | \"end\", value: string) => {\n setLocalState((prev) => ({\n ...prev,\n dateRange: { ...prev.dateRange, [field]: value },\n }));\n };\n\n const handleClear = () => {\n const clearedState: FilterState = {\n dropdowns: {},\n checkboxes: {},\n dateRange: { start: \"\", end: \"\" },\n };\n setLocalState(clearedState);\n onClear?.();\n };\n\n const handleApply = () => {\n onApply?.(localState);\n setIsOpen(false);\n };\n\n const handleCancel = () => {\n // Reset to initial state\n setLocalState({\n dropdowns: filterState?.dropdowns || {},\n checkboxes: filterState?.checkboxes || {},\n dateRange: filterState?.dateRange || { start: \"\", end: \"\" },\n });\n setIsOpen(false);\n };\n\n // Button trigger component\n const ButtonTrigger = (\n <Button\n variant=\"neutral\"\n className={cn(\"gap-2\", className)}\n style={{ \n borderRadius: \"var(--btn-standard-radius)\",\n height: \"var(--btn-standard-height)\",\n paddingLeft: \"var(--btn-standard-px)\",\n paddingRight: \"var(--btn-standard-px)\",\n fontSize: \"var(--btn-standard-font-size)\",\n }}\n >\n <Filter className=\"size-4\" />\n {triggerPlaceholder}\n </Button>\n );\n\n // Dropdown trigger component (styled like SelectTrigger)\n const DropdownTrigger = (\n <button\n className={cn(\n \"flex items-center justify-between gap-2 bg-[var(--canvas-background)] border text-[var(--canvas-text)] whitespace-nowrap transition-colors outline-none focus:border-[var(--canvas-border-input-focus)] focus:ring-2 focus:ring-[var(--canvas-border-input-focus)] focus:ring-offset-2 data-[state=open]:border-[var(--canvas-border-input-focus)]\",\n className\n )}\n style={{\n width: \"120px\",\n height: \"var(--input-small-height)\",\n paddingLeft: \"var(--input-small-px)\",\n paddingRight: \"var(--input-small-px)\",\n fontSize: \"var(--input-small-font-size)\",\n borderRadius: \"var(--input-small-radius)\",\n borderColor: \"var(--canvas-border-input)\",\n }}\n >\n <span className=\"text-[var(--canvas-text-placeholder)]\">{triggerPlaceholder}</span>\n <ChevronDown className=\"size-4 opacity-50\" />\n </button>\n );\n\n // Render placeholder during SSR to prevent hydration mismatch\n if (!mounted) {\n return triggerVariant === \"dropdown\" ? DropdownTrigger : ButtonTrigger;\n }\n\n return (\n <Popover open={isOpen} onOpenChange={setIsOpen}>\n <PopoverTrigger asChild>\n {triggerVariant === \"dropdown\" ? DropdownTrigger : ButtonTrigger}\n </PopoverTrigger>\n\n <PopoverContent\n align=\"end\"\n side=\"bottom\"\n sideOffset={4}\n avoidCollisions={false}\n className=\"w-80 p-0 bg-[var(--canvas-background)] border border-[var(--canvas-border)] shadow-lg\"\n >\n {/* Filter Content - All Input Types */}\n <div className=\"p-4 space-y-5 max-h-[480px] overflow-y-auto\">\n {/* Text Input */}\n <div className=\"space-y-2\">\n <label className=\"text-[var(--canvas-text-muted)]\" style={{ fontSize: \"var(--typo-input-label-size)\", fontWeight: \"var(--typo-input-label-weight)\" }}>\n Text Input\n </label>\n <TextInput inputSize=\"sm\" placeholder=\"Enter text...\" />\n </div>\n\n {/* Searchbox */}\n <div className=\"space-y-2\">\n <label className=\"text-[var(--canvas-text-muted)]\" style={{ fontSize: \"var(--typo-input-label-size)\", fontWeight: \"var(--typo-input-label-weight)\" }}>\n Searchbox\n </label>\n <Searchbox inputSize=\"sm\" placeholder=\"Search...\" />\n </div>\n\n {/* Dropdown */}\n {dropdowns.slice(0, 1).map((dropdown) => (\n <FilterDropdown\n key={dropdown.id}\n config={dropdown}\n value={localState.dropdowns[dropdown.id] || \"\"}\n onChange={(value) => handleDropdownChange(dropdown.id, value)}\n />\n ))}\n\n {/* Date Input */}\n <div className=\"space-y-2\">\n <label className=\"text-[var(--canvas-text-muted)]\" style={{ fontSize: \"var(--typo-input-label-size)\", fontWeight: \"var(--typo-input-label-weight)\" }}>\n Date Input\n </label>\n <DateInput inputSize=\"sm\" />\n </div>\n\n\n {/* Radio Buttons */}\n <div className=\"space-y-2\">\n <label className=\"text-[var(--canvas-text-muted)]\" style={{ fontSize: \"var(--typo-input-label-size)\", fontWeight: \"var(--typo-input-label-weight)\" }}>\n Radio Buttons\n </label>\n <RadioGroup defaultValue=\"option1\" className=\"flex\">\n <div className=\"flex items-center gap-2\">\n <RadioGroupItem value=\"option1\" id=\"radio1\" />\n <label \n htmlFor=\"radio1\" \n className=\"text-[var(--canvas-text)] cursor-pointer\"\n style={{ fontSize: \"var(--input-small-font-size)\" }}\n >\n Option 1\n </label>\n </div>\n <div className=\"flex items-center gap-2\">\n <RadioGroupItem value=\"option2\" id=\"radio2\" />\n <label \n htmlFor=\"radio2\" \n className=\"text-[var(--canvas-text)] cursor-pointer\"\n style={{ fontSize: \"var(--input-small-font-size)\" }}\n >\n Option 2\n </label>\n </div>\n </RadioGroup>\n </div>\n\n {/* Radio Buttons List */}\n <div className=\"space-y-2\">\n <label className=\"text-[var(--canvas-text-muted)]\" style={{ fontSize: \"var(--typo-input-label-size)\", fontWeight: \"var(--typo-input-label-weight)\" }}>\n Radio Buttons List\n </label>\n <RadioGroup defaultValue=\"list-opt1\" className=\"flex flex-col gap-2\">\n <div className=\"flex items-center gap-2\">\n <RadioGroupItem value=\"list-opt1\" id=\"radio-list-1\" />\n <label \n htmlFor=\"radio-list-1\" \n className=\"text-[var(--canvas-text)] cursor-pointer\"\n style={{ fontSize: \"var(--input-small-font-size)\" }}\n >\n First option\n </label>\n </div>\n <div className=\"flex items-center gap-2\">\n <RadioGroupItem value=\"list-opt2\" id=\"radio-list-2\" />\n <label \n htmlFor=\"radio-list-2\" \n className=\"text-[var(--canvas-text)] cursor-pointer\"\n style={{ fontSize: \"var(--input-small-font-size)\" }}\n >\n Second option\n </label>\n </div>\n <div className=\"flex items-center gap-2\">\n <RadioGroupItem value=\"list-opt3\" id=\"radio-list-3\" />\n <label \n htmlFor=\"radio-list-3\" \n className=\"text-[var(--canvas-text)] cursor-pointer\"\n style={{ fontSize: \"var(--input-small-font-size)\" }}\n >\n Third option\n </label>\n </div>\n </RadioGroup>\n </div>\n\n {/* Checkbox */}\n <div className=\"space-y-2\">\n <label className=\"text-[var(--canvas-text-muted)]\" style={{ fontSize: \"var(--typo-input-label-size)\", fontWeight: \"var(--typo-input-label-weight)\" }}>\n Checkbox\n </label>\n <div className=\"flex items-center gap-2\">\n <Checkbox id=\"single-checkbox\" />\n <label \n htmlFor=\"single-checkbox\" \n className=\"text-[var(--canvas-text)] cursor-pointer\"\n style={{ fontSize: \"var(--input-small-font-size)\" }}\n >\n I agree to terms\n </label>\n </div>\n </div>\n\n {/* Checkbox List */}\n {checkboxGroup && (\n <div className=\"space-y-2\">\n <label className=\"text-[var(--canvas-text-muted)]\" style={{ fontSize: \"var(--typo-input-label-size)\", fontWeight: \"var(--typo-input-label-weight)\" }}>\n Checkbox List\n </label>\n <div className=\"space-y-2\">\n {checkboxGroup.options.map((option) => (\n <CheckboxWithLabel\n key={option.id}\n checked={localState.checkboxes[option.id] || false}\n onCheckedChange={(checked) =>\n handleCheckboxChange(option.id, checked === true)\n }\n >\n {option.label}\n </CheckboxWithLabel>\n ))}\n </div>\n </div>\n )}\n\n {/* Toggle */}\n <div className=\"space-y-2\">\n <label className=\"text-[var(--canvas-text-muted)]\" style={{ fontSize: \"var(--typo-input-label-size)\", fontWeight: \"var(--typo-input-label-weight)\" }}>\n Toggle\n </label>\n <div className=\"flex items-center gap-2\">\n <Switch id=\"toggle-switch\" />\n <label \n htmlFor=\"toggle-switch\" \n className=\"text-[var(--canvas-text)] cursor-pointer\"\n style={{ fontSize: \"var(--input-small-font-size)\" }}\n >\n Enable notifications\n </label>\n </div>\n </div>\n\n {/* Multiselect Tags */}\n <div className=\"space-y-2\">\n <label className=\"text-[var(--canvas-text-muted)]\" style={{ fontSize: \"var(--typo-input-label-size)\", fontWeight: \"var(--typo-input-label-weight)\" }}>\n Multiselect Tags\n </label>\n <MultiselectTags inputSize=\"sm\" tags={[\"Tag 1\", \"Tag 2\"]} placeholder=\"Add...\" />\n </div>\n </div>\n\n {/* Footer */}\n <div className=\"flex items-center justify-between px-4 py-3 border-t border-[var(--canvas-border)]\">\n <Button\n variant=\"ghost\"\n size=\"sm\"\n onClick={handleClear}\n className=\"text-[var(--canvas-text-muted)] hover:text-[var(--canvas-text)] hover:bg-transparent\"\n >\n Reset\n </Button>\n <Button variant=\"primary\" size=\"sm\" onClick={handleApply}>\n Apply\n </Button>\n </div>\n </PopoverContent>\n </Popover>\n );\n}\n\n"
|
|
10
10
|
}
|
|
11
11
|
],
|
|
12
12
|
"dependencies": [
|