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
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
{
|
|
7
7
|
"path": "components/layout/header.tsx",
|
|
8
8
|
"type": "registry:layout",
|
|
9
|
-
"content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { Search, Bell, ShoppingCart, Menu, User, LogOut, MessageSquare, X, Home, Info, LayoutGrid, type LucideIcon } from \"lucide-react\";\nimport { Avatar, AvatarFallback, AvatarImage } from \"../ui/avatar\";\nimport { Button } from \"../ui/button\";\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuTrigger,\n} from \"../ui/dropdown-menu\";\nimport {\n Popover,\n PopoverContent,\n PopoverTrigger,\n} from \"../ui/popover\";\nimport { useThemeBranding } from \"../../context/theme-context\";\n\n// ============================================\n// Cart Types\n// ============================================\n\nexport interface CartItem {\n id: string;\n name: string;\n price: number;\n image: string;\n}\n\n// Sample cart items for demo\nconst defaultCartItems: CartItem[] = [\n {\n id: \"1\",\n name: \"Julian Bag\",\n price: 120,\n image: \"https://images.unsplash.com/photo-1591561954557-26941169b49e?w=150&h=150&fit=crop\",\n },\n {\n id: \"2\",\n name: \"Davis Keychain\",\n price: 60,\n image: \"https://images.unsplash.com/photo-1606107557195-0e29a4b5b4aa?w=150&h=150&fit=crop&crop=center\",\n },\n];\n\n// ============================================\n// Message Types\n// ============================================\n\nexport interface Message {\n id: string;\n senderName: string;\n senderAvatar: string;\n timestamp: string;\n}\n\n// Sample messages for demo\nconst defaultMessages: Message[] = [\n {\n id: \"1\",\n senderName: \"Jeff Conner\",\n senderAvatar: \"https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=100&h=100&fit=crop&crop=face\",\n timestamp: \"Jun 5, 2023 8:13 AM\",\n },\n {\n id: \"2\",\n senderName: \"Emma Pérez\",\n senderAvatar: \"https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=100&h=100&fit=crop&crop=face\",\n timestamp: \"May 2, 2023 11:54 AM\",\n },\n {\n id: \"3\",\n senderName: \"Raj Mishra\",\n senderAvatar: \"https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=100&h=100&fit=crop&crop=face\",\n timestamp: \"Jan 10, 2023 5:22 PM\",\n },\n {\n id: \"4\",\n senderName: \"John Freidman\",\n senderAvatar: \"https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=100&h=100&fit=crop&crop=face\",\n timestamp: \"Dec 20, 2022 2:22 PM\",\n },\n];\n\n// ============================================\n// Notification Types\n// ============================================\n\nexport interface Notification {\n id: string;\n userName: string;\n userAvatar: string;\n action: string;\n timestamp: string;\n}\n\n// Sample notifications for demo\nconst defaultNotifications: Notification[] = [\n {\n id: \"1\",\n userName: \"Aya Williams\",\n userAvatar: \"https://images.unsplash.com/photo-1531746020798-e6953c6e8e04?w=100&h=100&fit=crop&crop=face\",\n action: \"liked your photo\",\n timestamp: \"Apr 15, 2023 6:21 AM\",\n },\n {\n id: \"2\",\n userName: \"Francis Gaddi\",\n userAvatar: \"https://images.unsplash.com/photo-1506794778202-cad84cf45f1d?w=100&h=100&fit=crop&crop=face\",\n action: \"liked your photo\",\n timestamp: \"Jun 10, 2023 5:45 PM\",\n },\n {\n id: \"3\",\n userName: \"Stacy Jones\",\n userAvatar: \"https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=100&h=100&fit=crop&crop=face\",\n action: \"liked your photo\",\n timestamp: \"May 9, 2023 2:00 AM\",\n },\n {\n id: \"4\",\n userName: \"Gabi del Rosario\",\n userAvatar: \"https://images.unsplash.com/photo-1544005313-94ddf0286df2?w=100&h=100&fit=crop&crop=face\",\n action: \"liked your photo\",\n timestamp: \"Apr 8, 2023 8:55 PM\",\n },\n];\n\n// ============================================\n// Navigation Types\n// ============================================\n\nexport interface NavItem {\n id: string;\n label: string;\n icon?: LucideIcon;\n href?: string;\n onClick?: () => void;\n}\n\n// Default navigation items\nconst defaultNavItems: NavItem[] = [\n { id: \"home\", label: \"Home\", icon: Home },\n { id: \"about\", label: \"About\", icon: Info },\n { id: \"dashboard\", label: \"Dashboard\", icon: LayoutGrid },\n];\n\n// Phosphor Icons for Logo\nimport { Buildings, type Icon as PhosphorIcon } from \"@phosphor-icons/react\";\nimport {\n Diamond, Hexagon, Star, Lightning, Sparkle, Infinity, Code, Terminal, Cpu,\n Database, Globe, Cloud, WifiHigh, Briefcase, Storefront, Handshake, ChartLine,\n Palette as PaletteIcon, PencilSimple, Camera, MusicNote, Lightbulb, Leaf, Tree,\n Sun, Moon, Fire, Drop, ChatCircle, Envelope, Phone, Megaphone, Heart, Shield,\n Trophy, Rocket, Target, Flag,\n} from \"@phosphor-icons/react\";\n\n// Icon shape renderers - use style attribute for CSS variable support\nconst iconShapes = {\n rounded: (bgColor: string) => (\n <svg viewBox=\"0 0 32 32\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\" className=\"size-full absolute inset-0\">\n <rect width=\"32\" height=\"32\" rx=\"10\" style={{ fill: bgColor }} />\n </svg>\n ),\n circle: (bgColor: string) => (\n <svg viewBox=\"0 0 32 32\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\" className=\"size-full absolute inset-0\">\n <circle cx=\"16\" cy=\"16\" r=\"16\" style={{ fill: bgColor }} />\n </svg>\n ),\n square: (bgColor: string) => (\n <svg viewBox=\"0 0 32 32\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\" className=\"size-full absolute inset-0\">\n <rect width=\"32\" height=\"32\" style={{ fill: bgColor }} />\n </svg>\n ),\n};\n\n// Map icon names to components\nconst iconMap: Record<string, PhosphorIcon> = {\n Diamond, Hexagon, Star, Lightning, Sparkle, Infinity, Code, Terminal, Cpu,\n Database, Globe, Cloud, WifiHigh, Briefcase, Buildings, Storefront, Handshake,\n ChartLine, Palette: PaletteIcon, PencilSimple, Camera, MusicNote, Lightbulb,\n Leaf, Tree, Sun, Moon, Fire, Drop, ChatCircle, Envelope, Phone, Megaphone,\n Heart, Shield, Trophy, Rocket, Target, Flag,\n};\n\n// Helper to resolve CSS variable references to actual hex colors\nfunction resolveBrandingColor(value: string): string {\n if (!value) return \"#ffffff\";\n if (value.startsWith(\"var(\")) {\n const varName = value.replace(\"var(\", \"\").replace(\")\", \"\");\n if (typeof window !== \"undefined\") {\n const computed = getComputedStyle(document.documentElement).getPropertyValue(varName).trim();\n return computed || \"#ffffff\";\n }\n return \"#ffffff\";\n }\n return value;\n}\n\ninterface HeaderProps {\n /** Callback when mobile menu button is clicked */\n onMenuClick?: () => void;\n /** Whether to show the logo on desktop (for no-sidebar pages) */\n showDesktopLogo?: boolean;\n /** Visual variant - light (default) or dark mode */\n variant?: \"light\" | \"dark\";\n /** Callback when \"My Account\" is clicked */\n onAccountClick?: () => void;\n /** Callback when \"Logout\" is clicked */\n onLogout?: () => void;\n /** Avatar image URL */\n avatarUrl?: string;\n /** Cart items to display */\n cartItems?: CartItem[];\n /** Callback when checkout button is clicked */\n onCheckout?: () => void;\n /** Callback when remove item is clicked */\n onRemoveCartItem?: (id: string) => void;\n /** Messages to display */\n messages?: Message[];\n /** Callback when \"Mark as read\" is clicked for messages */\n onMarkAsRead?: () => void;\n /** Callback when \"view more\" is clicked for messages */\n onViewMoreMessages?: () => void;\n /** Notifications to display */\n notifications?: Notification[];\n /** Callback when \"Mark as read\" is clicked for notifications */\n onMarkNotificationsAsRead?: () => void;\n /** Callback when \"view more\" is clicked for notifications */\n onViewMoreNotifications?: () => void;\n /** Navigation items for header and mobile menu */\n navItems?: NavItem[];\n /** Callback when Login button is clicked */\n onLogin?: () => void;\n /** Callback when Sign up button is clicked */\n onSignUp?: () => void;\n /** Whether to show auth buttons (Login/Sign up) */\n showAuthButtons?: boolean;\n}\n\n/**\n * Canvas Design System - Header/Navbar Component\n * \n * Desktop (lg+): Full logo with wordmark, icon cluster, avatar\n * Mobile/Tablet: Favicon only, avatar, hamburger menu\n * \n * For pages without a sidebar, set showDesktopLogo={true} to display\n * the logo in the header on desktop.\n * \n * Set variant=\"dark\" for a dark themed header that matches the sidebar.\n */\nexport function Header({ \n onMenuClick, \n showDesktopLogo = false, \n variant = \"light\",\n onAccountClick,\n onLogout,\n avatarUrl = \"https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=150&h=150&fit=crop&crop=face\",\n cartItems = defaultCartItems,\n onCheckout,\n onRemoveCartItem,\n messages = defaultMessages,\n onMarkAsRead,\n onViewMoreMessages,\n notifications = defaultNotifications,\n onMarkNotificationsAsRead,\n onViewMoreNotifications,\n navItems = defaultNavItems,\n onLogin,\n onSignUp,\n showAuthButtons = false,\n}: HeaderProps) {\n const { branding, isMounted } = useThemeBranding();\n const isDark = variant === \"dark\";\n const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);\n \n // Calculate cart total\n const cartTotal = cartItems.reduce((sum, item) => sum + item.price, 0);\n\n // Cart popover content component\n const CartPopoverContent = () => (\n <div className=\"w-[320px]\">\n {/* Header */}\n <div \n className=\"flex items-center justify-between pb-[var(--spacing-xl)] border-b border-[var(--canvas-neutral-border)]\"\n >\n <span\n style={{\n fontFamily: \"var(--typo-body-m-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-m-size)\",\n fontWeight: 600,\n lineHeight: \"var(--typo-body-m-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n Your cart\n </span>\n <span\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 400,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-neutral-text)\",\n }}\n >\n {cartItems.length} {cartItems.length === 1 ? \"item\" : \"items\"}\n </span>\n </div>\n\n {/* Cart Items */}\n <div className=\"py-[var(--spacing-xl)] space-y-[var(--spacing-xl)]\">\n {cartItems.map((item) => (\n <div key={item.id} className=\"flex gap-[var(--spacing-lg)]\">\n {/* Product Image */}\n <div \n className=\"size-16 rounded-[var(--radius-md)] overflow-hidden shrink-0 bg-[var(--canvas-neutral-surface)]\"\n >\n <img \n src={item.image} \n alt={item.name}\n className=\"size-full object-cover\"\n />\n </div>\n \n {/* Product Details */}\n <div className=\"flex flex-col justify-center min-w-0\">\n <span\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 600,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n {item.name}\n </span>\n <span\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 400,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n ${item.price}\n </span>\n <button\n onClick={() => onRemoveCartItem?.(item.id)}\n className=\"text-left mt-[var(--spacing-xs)] hover:underline\"\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 400,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-primary)\",\n }}\n >\n Remove\n </button>\n </div>\n </div>\n ))}\n </div>\n\n {/* Total */}\n <div \n className=\"flex items-center justify-between py-[var(--spacing-xl)] border-t border-[var(--canvas-neutral-border)]\"\n >\n <span\n style={{\n fontFamily: \"var(--typo-body-m-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-m-size)\",\n fontWeight: 500,\n lineHeight: \"var(--typo-body-m-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n Total\n </span>\n <span\n style={{\n fontFamily: \"var(--typo-body-m-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-m-size)\",\n fontWeight: 600,\n lineHeight: \"var(--typo-body-m-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n ${cartTotal}\n </span>\n </div>\n\n {/* Checkout Button */}\n <Button \n className=\"w-full\" \n size=\"default\"\n onClick={onCheckout}\n >\n Checkout\n </Button>\n </div>\n );\n\n // Messages popover content component\n const MessagesPopoverContent = () => (\n <div className=\"w-[320px]\">\n {/* Header */}\n <div \n className=\"flex items-center justify-between pb-[var(--spacing-xl)] border-b border-[var(--canvas-neutral-border)]\"\n >\n <span\n style={{\n fontFamily: \"var(--typo-body-m-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-m-size)\",\n fontWeight: 600,\n lineHeight: \"var(--typo-body-m-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n Messages\n </span>\n <button\n onClick={onMarkAsRead}\n className=\"hover:underline\"\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 500,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-primary)\",\n }}\n >\n Mark as read\n </button>\n </div>\n\n {/* Messages List */}\n <div className=\"py-[var(--spacing-lg)]\">\n {messages.map((message, index) => (\n <div \n key={message.id} \n className={`flex gap-[var(--spacing-lg)] py-[var(--spacing-lg)] ${\n index < messages.length - 1 ? \"border-b border-[var(--canvas-neutral-border)]\" : \"\"\n }`}\n >\n {/* Sender Avatar */}\n <Avatar className=\"size-10 shrink-0\">\n <AvatarImage src={message.senderAvatar} alt={message.senderName} />\n <AvatarFallback \n className=\"bg-[var(--canvas-neutral-surface)] text-[var(--canvas-neutral-text)]\"\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n }}\n >\n {message.senderName.split(\" \").map(n => n[0]).join(\"\")}\n </AvatarFallback>\n </Avatar>\n \n {/* Message Details */}\n <div className=\"flex flex-col justify-center min-w-0\">\n <span\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 400,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n <span style={{ fontWeight: 600 }}>{message.senderName}</span> sent you a message\n </span>\n <span\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 400,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-neutral-text)\",\n }}\n >\n {message.timestamp}\n </span>\n </div>\n </div>\n ))}\n </div>\n\n {/* View More */}\n <div \n className=\"pt-[var(--spacing-lg)] border-t border-[var(--canvas-neutral-border)] text-center\"\n >\n <button\n onClick={onViewMoreMessages}\n className=\"hover:underline\"\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 500,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-primary)\",\n }}\n >\n view more\n </button>\n </div>\n </div>\n );\n\n // Notifications popover content component\n const NotificationsPopoverContent = () => (\n <div className=\"w-[320px]\">\n {/* Header */}\n <div \n className=\"flex items-center justify-between pb-[var(--spacing-xl)] border-b border-[var(--canvas-neutral-border)]\"\n >\n <span\n style={{\n fontFamily: \"var(--typo-body-m-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-m-size)\",\n fontWeight: 600,\n lineHeight: \"var(--typo-body-m-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n Notifications\n </span>\n <button\n onClick={onMarkNotificationsAsRead}\n className=\"hover:underline\"\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 500,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-primary)\",\n }}\n >\n Mark as read\n </button>\n </div>\n\n {/* Notifications List */}\n <div className=\"py-[var(--spacing-lg)]\">\n {notifications.map((notification, index) => (\n <div \n key={notification.id} \n className={`flex gap-[var(--spacing-lg)] py-[var(--spacing-lg)] ${\n index < notifications.length - 1 ? \"border-b border-[var(--canvas-neutral-border)]\" : \"\"\n }`}\n >\n {/* User Avatar */}\n <Avatar className=\"size-10 shrink-0\">\n <AvatarImage src={notification.userAvatar} alt={notification.userName} />\n <AvatarFallback \n className=\"bg-[var(--canvas-neutral-surface)] text-[var(--canvas-neutral-text)]\"\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n }}\n >\n {notification.userName.split(\" \").map(n => n[0]).join(\"\")}\n </AvatarFallback>\n </Avatar>\n \n {/* Notification Details */}\n <div className=\"flex flex-col justify-center min-w-0\">\n <span\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 400,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n <span style={{ fontWeight: 600 }}>{notification.userName}</span> {notification.action}\n </span>\n <span\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 400,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-neutral-text)\",\n }}\n >\n {notification.timestamp}\n </span>\n </div>\n </div>\n ))}\n </div>\n\n {/* View More */}\n <div \n className=\"pt-[var(--spacing-lg)] border-t border-[var(--canvas-neutral-border)] text-center\"\n >\n <button\n onClick={onViewMoreNotifications}\n className=\"hover:underline\"\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 500,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-primary)\",\n }}\n >\n view more\n </button>\n </div>\n </div>\n );\n\n // Get the icon shape renderer\n const shapeRenderer = iconShapes[branding.iconShape as keyof typeof iconShapes] || iconShapes.rounded;\n\n // Logo component used for both mobile and desktop (when showDesktopLogo is true)\n // Uses CSS variables directly - no JavaScript resolution needed\n const LogoIcon = () => {\n // Use CSS variables directly - the browser handles resolution\n const bgColor = branding.bgColor || \"var(--canvas-primary)\";\n const iconColor = branding.iconColor || \"var(--canvas-primary-foreground)\";\n const IconComponent = iconMap[branding.iconName || \"Buildings\"] || Buildings;\n \n return (\n <div className=\"relative size-8 shrink-0\">\n {shapeRenderer(bgColor)}\n <div className=\"absolute inset-0 flex items-center justify-center z-10\">\n <IconComponent weight=\"bold\" size={18} color={iconColor} />\n </div>\n </div>\n );\n };\n\n return (\n <header \n className={`h-[var(--header-height)] w-full border-b ${\n isDark \n ? \"bg-[var(--canvas-sidebar-dark-bg)] border-[var(--canvas-sidebar-dark-border)]\" \n : \"bg-white border-[var(--canvas-neutral-border)]\"\n }`}\n >\n <div className=\"flex items-center h-full px-4 lg:px-[var(--spacing-5xl)]\">\n {/* Logo - Visible on mobile, and on desktop when showDesktopLogo is true */}\n <div className={`flex items-center gap-[var(--spacing-md)] h-8 shrink-0 ${showDesktopLogo ? '' : 'lg:hidden'}`}>\n <LogoIcon />\n {/* Wordmark - only on desktop when showDesktopLogo is true */}\n {showDesktopLogo && (\n <span \n className={`hidden lg:block ${isDark ? \"text-white\" : \"text-[var(--canvas-text)]\"}`}\n style={{\n fontFamily: \"var(--typo-header-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-xl-size)\",\n fontWeight: 600,\n letterSpacing: \"var(--typo-header-spacing)\",\n lineHeight: \"var(--typo-header-line-height)\",\n }}\n >\n {branding.wordmark || \"canvas\"}\n </span>\n )}\n </div>\n\n {/* Spacer */}\n <div className=\"flex-1\" />\n\n {/* Navigation Links - Desktop Only */}\n <nav className=\"hidden lg:flex items-center gap-[var(--spacing-2xl)] h-full\">\n {navItems.map((item) => (\n <button\n key={item.id}\n onClick={() => {\n item.onClick?.();\n if (item.href) {\n window.location.href = item.href;\n }\n }}\n className={`cursor-pointer transition-colors ${\n isDark \n ? \"text-white/60 hover:text-white\" \n : \"text-[var(--canvas-neutral-text)] hover:text-[var(--canvas-text)]\"\n }`}\n style={{\n fontFamily: \"var(--typo-header-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-header-size)\",\n fontWeight: \"var(--typo-header-weight)\",\n letterSpacing: \"var(--typo-header-spacing)\",\n lineHeight: \"var(--typo-header-line-height)\",\n }}\n >\n {item.label}\n </button>\n ))}\n </nav>\n\n {/* Icons - Always Visible */}\n <div className=\"flex items-center gap-[var(--spacing-2xl)] ml-[var(--spacing-2xl)]\">\n <button \n className={`transition-colors ${\n isDark \n ? \"text-white/60 hover:text-white\" \n : \"text-[var(--canvas-neutral-text)] hover:text-[var(--canvas-text)]\"\n }`}\n aria-label=\"Search\"\n >\n <Search className=\"size-4\" />\n </button>\n \n <Popover>\n <PopoverTrigger asChild>\n <button \n className={`transition-colors ${\n isDark \n ? \"text-white/60 hover:text-white\" \n : \"text-[var(--canvas-neutral-text)] hover:text-[var(--canvas-text)]\"\n }`}\n aria-label=\"Notifications\"\n >\n <Bell className=\"size-4\" />\n </button>\n </PopoverTrigger>\n <PopoverContent align=\"end\" sideOffset={16} className=\"w-auto p-[var(--spacing-xl)]\">\n <NotificationsPopoverContent />\n </PopoverContent>\n </Popover>\n \n <Popover>\n <PopoverTrigger asChild>\n <button \n className={`transition-colors ${\n isDark \n ? \"text-white/60 hover:text-white\" \n : \"text-[var(--canvas-neutral-text)] hover:text-[var(--canvas-text)]\"\n }`}\n aria-label=\"Messages\"\n >\n <MessageSquare className=\"size-4\" />\n </button>\n </PopoverTrigger>\n <PopoverContent align=\"end\" sideOffset={16} className=\"w-auto p-[var(--spacing-xl)]\">\n <MessagesPopoverContent />\n </PopoverContent>\n </Popover>\n \n <Popover>\n <PopoverTrigger asChild>\n <button \n className={`transition-colors ${\n isDark \n ? \"text-white/60 hover:text-white\" \n : \"text-[var(--canvas-neutral-text)] hover:text-[var(--canvas-text)]\"\n }`}\n aria-label=\"Cart\"\n >\n <ShoppingCart className=\"size-4\" />\n </button>\n </PopoverTrigger>\n <PopoverContent align=\"end\" sideOffset={16} className=\"w-auto p-[var(--spacing-xl)]\">\n <CartPopoverContent />\n </PopoverContent>\n </Popover>\n \n {/* Auth Buttons - Desktop Only */}\n {showAuthButtons && (\n <div className=\"hidden lg:flex items-center gap-[var(--spacing-lg)]\">\n <Button \n variant=\"outline\" \n size=\"default\"\n onClick={onLogin}\n >\n Log in\n </Button>\n <Button \n variant=\"default\" \n size=\"default\"\n onClick={onSignUp}\n >\n Sign up\n </Button>\n </div>\n )}\n \n {/* Avatar with Dropdown */}\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <button className=\"rounded-full focus:outline-none focus:ring-2 focus:ring-[var(--canvas-primary)] focus:ring-offset-2\">\n <Avatar className={`size-10 border cursor-pointer ${\n isDark \n ? \"border-[var(--canvas-sidebar-dark-border)]\" \n : \"border-[var(--canvas-neutral-border)]\"\n }`}>\n <AvatarImage src={avatarUrl} alt=\"User avatar\" />\n <AvatarFallback \n className={\n isDark \n ? \"bg-white/10 text-white/60\" \n : \"bg-[var(--canvas-neutral-surface)] text-[var(--canvas-neutral-text)]\"\n }\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n }}\n >\n JC\n </AvatarFallback>\n </Avatar>\n </button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"end\" sideOffset={8}>\n <DropdownMenuItem onClick={onAccountClick}>\n <User className=\"size-4 mr-2\" />\n My Account\n </DropdownMenuItem>\n <DropdownMenuItem onClick={onLogout}>\n <LogOut className=\"size-4 mr-2\" />\n Logout\n </DropdownMenuItem>\n </DropdownMenuContent>\n </DropdownMenu>\n\n {/* Mobile Menu Button */}\n <Button \n variant=\"ghost\" \n size=\"icon\" \n onClick={() => {\n setIsMobileMenuOpen(true);\n onMenuClick?.();\n }}\n aria-label=\"Open menu\"\n className={`lg:hidden -ml-[var(--spacing-md)] ${isDark ? \"text-white/60 hover:text-white hover:bg-white/10\" : \"text-[var(--canvas-neutral-text)]\"}`}\n >\n <Menu className=\"size-4\" />\n </Button>\n </div>\n </div>\n\n {/* Mobile Menu Overlay */}\n {isMobileMenuOpen && (\n <div className=\"fixed inset-0 z-50 lg:hidden\">\n {/* Backdrop */}\n <div \n className=\"absolute inset-0 bg-black/50\"\n onClick={() => setIsMobileMenuOpen(false)}\n />\n \n {/* Menu Panel */}\n <div className=\"absolute right-0 top-0 h-full w-full max-w-sm bg-white shadow-xl\">\n {/* Close Button */}\n <div className=\"flex justify-end p-4\">\n <Button\n variant=\"ghost\"\n size=\"icon\"\n onClick={() => setIsMobileMenuOpen(false)}\n aria-label=\"Close menu\"\n >\n <X className=\"size-5\" />\n </Button>\n </div>\n \n {/* Navigation Items */}\n <nav className=\"px-6 py-4\">\n <div className=\"space-y-2\">\n {navItems.map((item) => {\n const Icon = item.icon;\n return (\n <button\n key={item.id}\n onClick={() => {\n item.onClick?.();\n if (item.href) {\n window.location.href = item.href;\n }\n setIsMobileMenuOpen(false);\n }}\n className=\"flex items-center gap-[var(--spacing-lg)] w-full py-[var(--spacing-lg)] text-left hover:bg-[var(--canvas-neutral-surface)] rounded-[var(--radius-md)] transition-colors\"\n >\n {Icon && (\n <div \n className=\"size-12 rounded-[var(--radius-md)] flex items-center justify-center shrink-0\"\n style={{\n backgroundColor: \"color-mix(in srgb, var(--canvas-primary) 10%, transparent)\",\n }}\n >\n <Icon \n className=\"size-5\"\n style={{ color: \"var(--canvas-primary)\" }}\n />\n </div>\n )}\n <span\n style={{\n fontFamily: \"var(--typo-body-m-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-m-size)\",\n fontWeight: 400,\n lineHeight: \"var(--typo-body-m-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n {item.label}\n </span>\n </button>\n );\n })}\n </div>\n </nav>\n \n {/* Auth Buttons */}\n {showAuthButtons && (\n <div className=\"absolute bottom-0 left-0 right-0 p-6 space-y-3 border-t border-[var(--canvas-neutral-border)]\">\n <Button \n variant=\"outline\" \n className=\"w-full\"\n size=\"lg\"\n onClick={() => {\n onLogin?.();\n setIsMobileMenuOpen(false);\n }}\n >\n Log in\n </Button>\n <Button \n variant=\"default\" \n className=\"w-full\"\n size=\"lg\"\n onClick={() => {\n onSignUp?.();\n setIsMobileMenuOpen(false);\n }}\n >\n Sign up\n </Button>\n </div>\n )}\n </div>\n </div>\n )}\n </header>\n );\n}\n\n"
|
|
9
|
+
"content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { Search, Bell, ShoppingCart, Menu, User, LogOut, MessageSquare, X, Home, Info, LayoutGrid, type LucideIcon } from \"lucide-react\";\nimport { Avatar, AvatarFallback, AvatarImage } from \"../ui/avatar\";\nimport { Button } from \"../ui/button\";\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuTrigger,\n} from \"../ui/dropdown-menu\";\nimport {\n Popover,\n PopoverContent,\n PopoverTrigger,\n} from \"../ui/popover\";\nimport { useThemeBranding } from \"../../context/theme-context\";\n\n// ============================================\n// Cart Types\n// ============================================\n\nexport interface CartItem {\n id: string;\n name: string;\n price: number;\n image: string;\n}\n\n// Sample cart items for demo\nconst defaultCartItems: CartItem[] = [\n {\n id: \"1\",\n name: \"Julian Bag\",\n price: 120,\n image: \"https://images.unsplash.com/photo-1591561954557-26941169b49e?w=150&h=150&fit=crop\",\n },\n {\n id: \"2\",\n name: \"Davis Keychain\",\n price: 60,\n image: \"https://images.unsplash.com/photo-1606107557195-0e29a4b5b4aa?w=150&h=150&fit=crop&crop=center\",\n },\n];\n\n// ============================================\n// Message Types\n// ============================================\n\nexport interface Message {\n id: string;\n senderName: string;\n senderAvatar: string;\n timestamp: string;\n}\n\n// Sample messages for demo\nconst defaultMessages: Message[] = [\n {\n id: \"1\",\n senderName: \"Jeff Conner\",\n senderAvatar: \"https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=100&h=100&fit=crop&crop=face\",\n timestamp: \"Jun 5, 2023 8:13 AM\",\n },\n {\n id: \"2\",\n senderName: \"Emma Pérez\",\n senderAvatar: \"https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=100&h=100&fit=crop&crop=face\",\n timestamp: \"May 2, 2023 11:54 AM\",\n },\n {\n id: \"3\",\n senderName: \"Raj Mishra\",\n senderAvatar: \"https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=100&h=100&fit=crop&crop=face\",\n timestamp: \"Jan 10, 2023 5:22 PM\",\n },\n {\n id: \"4\",\n senderName: \"John Freidman\",\n senderAvatar: \"https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=100&h=100&fit=crop&crop=face\",\n timestamp: \"Dec 20, 2022 2:22 PM\",\n },\n];\n\n// ============================================\n// Notification Types\n// ============================================\n\nexport interface Notification {\n id: string;\n userName: string;\n userAvatar: string;\n action: string;\n timestamp: string;\n}\n\n// Sample notifications for demo\nconst defaultNotifications: Notification[] = [\n {\n id: \"1\",\n userName: \"Aya Williams\",\n userAvatar: \"https://images.unsplash.com/photo-1531746020798-e6953c6e8e04?w=100&h=100&fit=crop&crop=face\",\n action: \"liked your photo\",\n timestamp: \"Apr 15, 2023 6:21 AM\",\n },\n {\n id: \"2\",\n userName: \"Francis Gaddi\",\n userAvatar: \"https://images.unsplash.com/photo-1506794778202-cad84cf45f1d?w=100&h=100&fit=crop&crop=face\",\n action: \"liked your photo\",\n timestamp: \"Jun 10, 2023 5:45 PM\",\n },\n {\n id: \"3\",\n userName: \"Stacy Jones\",\n userAvatar: \"https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=100&h=100&fit=crop&crop=face\",\n action: \"liked your photo\",\n timestamp: \"May 9, 2023 2:00 AM\",\n },\n {\n id: \"4\",\n userName: \"Gabi del Rosario\",\n userAvatar: \"https://images.unsplash.com/photo-1544005313-94ddf0286df2?w=100&h=100&fit=crop&crop=face\",\n action: \"liked your photo\",\n timestamp: \"Apr 8, 2023 8:55 PM\",\n },\n];\n\n// ============================================\n// Navigation Types\n// ============================================\n\nexport interface NavItem {\n id: string;\n label: string;\n icon?: LucideIcon;\n href?: string;\n onClick?: () => void;\n}\n\n// Default navigation items\nconst defaultNavItems: NavItem[] = [\n { id: \"home\", label: \"Home\", icon: Home },\n { id: \"about\", label: \"About\", icon: Info },\n { id: \"dashboard\", label: \"Dashboard\", icon: LayoutGrid },\n];\n\n// Phosphor Icons for Logo\nimport { Buildings, type Icon as PhosphorIcon } from \"@phosphor-icons/react\";\nimport {\n Diamond, Hexagon, Star, Lightning, Sparkle, Infinity, Code, Terminal, Cpu,\n Database, Globe, Cloud, WifiHigh, Briefcase, Storefront, Handshake, ChartLine,\n Palette as PaletteIcon, PencilSimple, Camera, MusicNote, Lightbulb, Leaf, Tree,\n Sun, Moon, Fire, Drop, ChatCircle, Envelope, Phone, Megaphone, Heart, Shield,\n Trophy, Rocket, Target, Flag,\n} from \"@phosphor-icons/react\";\n\n// Icon shape renderers - use style attribute for CSS variable support\nconst iconShapes = {\n rounded: (bgColor: string) => (\n <svg viewBox=\"0 0 32 32\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\" className=\"size-full absolute inset-0\">\n <rect width=\"32\" height=\"32\" rx=\"10\" style={{ fill: bgColor }} />\n </svg>\n ),\n circle: (bgColor: string) => (\n <svg viewBox=\"0 0 32 32\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\" className=\"size-full absolute inset-0\">\n <circle cx=\"16\" cy=\"16\" r=\"16\" style={{ fill: bgColor }} />\n </svg>\n ),\n square: (bgColor: string) => (\n <svg viewBox=\"0 0 32 32\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\" className=\"size-full absolute inset-0\">\n <rect width=\"32\" height=\"32\" style={{ fill: bgColor }} />\n </svg>\n ),\n};\n\n// Map icon names to components\nconst iconMap: Record<string, PhosphorIcon> = {\n Diamond, Hexagon, Star, Lightning, Sparkle, Infinity, Code, Terminal, Cpu,\n Database, Globe, Cloud, WifiHigh, Briefcase, Buildings, Storefront, Handshake,\n ChartLine, Palette: PaletteIcon, PencilSimple, Camera, MusicNote, Lightbulb,\n Leaf, Tree, Sun, Moon, Fire, Drop, ChatCircle, Envelope, Phone, Megaphone,\n Heart, Shield, Trophy, Rocket, Target, Flag,\n};\n\n// Helper to resolve CSS variable references to actual hex colors\nfunction resolveBrandingColor(value: string): string {\n if (!value) return \"#ffffff\";\n if (value.startsWith(\"var(\")) {\n const varName = value.replace(\"var(\", \"\").replace(\")\", \"\");\n if (typeof window !== \"undefined\") {\n const computed = getComputedStyle(document.documentElement).getPropertyValue(varName).trim();\n return computed || \"#ffffff\";\n }\n return \"#ffffff\";\n }\n return value;\n}\n\ninterface HeaderProps {\n /** Callback when mobile menu button is clicked */\n onMenuClick?: () => void;\n /** Whether to show the logo on desktop (for no-sidebar pages) */\n showDesktopLogo?: boolean;\n /** Visual variant - light (default) or dark mode */\n variant?: \"light\" | \"dark\";\n /** Callback when \"My Account\" is clicked */\n onAccountClick?: () => void;\n /** Callback when \"Logout\" is clicked */\n onLogout?: () => void;\n /** Avatar image URL */\n avatarUrl?: string;\n /** Cart items to display */\n cartItems?: CartItem[];\n /** Callback when checkout button is clicked */\n onCheckout?: () => void;\n /** Callback when remove item is clicked */\n onRemoveCartItem?: (id: string) => void;\n /** Messages to display */\n messages?: Message[];\n /** Callback when \"Mark as read\" is clicked for messages */\n onMarkAsRead?: () => void;\n /** Callback when \"view more\" is clicked for messages */\n onViewMoreMessages?: () => void;\n /** Notifications to display */\n notifications?: Notification[];\n /** Callback when \"Mark as read\" is clicked for notifications */\n onMarkNotificationsAsRead?: () => void;\n /** Callback when \"view more\" is clicked for notifications */\n onViewMoreNotifications?: () => void;\n /** Navigation items for header and mobile menu */\n navItems?: NavItem[];\n /** Callback when Login button is clicked */\n onLogin?: () => void;\n /** Callback when Sign up button is clicked */\n onSignUp?: () => void;\n /** Whether to show auth buttons (Login/Sign up) */\n showAuthButtons?: boolean;\n}\n\n/**\n * Canvas Design System - Header/Navbar Component\n * \n * Desktop (lg+): Full logo with wordmark, icon cluster, avatar\n * Mobile/Tablet: Favicon only, avatar, hamburger menu\n * \n * For pages without a sidebar, set showDesktopLogo={true} to display\n * the logo in the header on desktop.\n * \n * Set variant=\"dark\" for a dark themed header that matches the sidebar.\n */\nexport function Header({ \n onMenuClick, \n showDesktopLogo = false, \n variant = \"light\",\n onAccountClick,\n onLogout,\n avatarUrl = \"https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=150&h=150&fit=crop&crop=face\",\n cartItems = defaultCartItems,\n onCheckout,\n onRemoveCartItem,\n messages = defaultMessages,\n onMarkAsRead,\n onViewMoreMessages,\n notifications = defaultNotifications,\n onMarkNotificationsAsRead,\n onViewMoreNotifications,\n navItems = defaultNavItems,\n onLogin,\n onSignUp,\n showAuthButtons = false,\n}: HeaderProps) {\n const { branding, isMounted } = useThemeBranding();\n const isDark = variant === \"dark\";\n const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);\n \n // Calculate cart total\n const cartTotal = cartItems.reduce((sum, item) => sum + item.price, 0);\n\n // Cart popover content component\n const CartPopoverContent = () => (\n <div className=\"w-[320px]\">\n {/* Header */}\n <div \n className=\"flex items-center justify-between pb-[var(--spacing-xl)] border-b border-[var(--canvas-neutral-border)]\"\n >\n <span\n style={{\n fontFamily: \"var(--typo-body-m-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-m-size)\",\n fontWeight: 600,\n lineHeight: \"var(--typo-body-m-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n Your cart\n </span>\n <span\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 400,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-neutral-text)\",\n }}\n >\n {cartItems.length} {cartItems.length === 1 ? \"item\" : \"items\"}\n </span>\n </div>\n\n {/* Cart Items */}\n <div className=\"py-[var(--spacing-xl)] space-y-[var(--spacing-xl)]\">\n {cartItems.map((item) => (\n <div key={item.id} className=\"flex gap-[var(--spacing-lg)]\">\n {/* Product Image */}\n <div \n className=\"size-16 rounded-[var(--radius-md)] overflow-hidden shrink-0 bg-[var(--canvas-neutral-surface)]\"\n >\n <img \n src={item.image} \n alt={item.name}\n className=\"size-full object-cover\"\n />\n </div>\n \n {/* Product Details */}\n <div className=\"flex flex-col justify-center min-w-0\">\n <span\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 600,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n {item.name}\n </span>\n <span\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 400,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n ${item.price}\n </span>\n <button\n onClick={() => onRemoveCartItem?.(item.id)}\n className=\"text-left mt-[var(--spacing-xs)] hover:underline\"\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 400,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-primary)\",\n }}\n >\n Remove\n </button>\n </div>\n </div>\n ))}\n </div>\n\n {/* Total */}\n <div \n className=\"flex items-center justify-between py-[var(--spacing-xl)] border-t border-[var(--canvas-neutral-border)]\"\n >\n <span\n style={{\n fontFamily: \"var(--typo-body-m-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-m-size)\",\n fontWeight: 500,\n lineHeight: \"var(--typo-body-m-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n Total\n </span>\n <span\n style={{\n fontFamily: \"var(--typo-body-m-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-m-size)\",\n fontWeight: 600,\n lineHeight: \"var(--typo-body-m-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n ${cartTotal}\n </span>\n </div>\n\n {/* Checkout Button */}\n <Button \n className=\"w-full\" \n size=\"default\"\n onClick={onCheckout}\n >\n Checkout\n </Button>\n </div>\n );\n\n // Messages popover content component\n const MessagesPopoverContent = () => (\n <div className=\"w-[320px]\">\n {/* Header */}\n <div \n className=\"flex items-center justify-between pb-[var(--spacing-xl)] border-b border-[var(--canvas-neutral-border)]\"\n >\n <span\n style={{\n fontFamily: \"var(--typo-body-m-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-m-size)\",\n fontWeight: 600,\n lineHeight: \"var(--typo-body-m-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n Messages\n </span>\n <button\n onClick={onMarkAsRead}\n className=\"hover:underline\"\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 500,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-primary)\",\n }}\n >\n Mark as read\n </button>\n </div>\n\n {/* Messages List */}\n <div className=\"py-[var(--spacing-lg)]\">\n {messages.map((message, index) => (\n <div \n key={message.id} \n className={`flex gap-[var(--spacing-lg)] py-[var(--spacing-lg)] ${\n index < messages.length - 1 ? \"border-b border-[var(--canvas-neutral-border)]\" : \"\"\n }`}\n >\n {/* Sender Avatar */}\n <Avatar className=\"size-10 shrink-0\">\n <AvatarImage src={message.senderAvatar} alt={message.senderName} />\n <AvatarFallback \n className=\"bg-[var(--canvas-neutral-surface)] text-[var(--canvas-neutral-text)]\"\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n }}\n >\n {message.senderName.split(\" \").map(n => n[0]).join(\"\")}\n </AvatarFallback>\n </Avatar>\n \n {/* Message Details */}\n <div className=\"flex flex-col justify-center min-w-0\">\n <span\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 400,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n <span style={{ fontWeight: 600 }}>{message.senderName}</span> sent you a message\n </span>\n <span\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 400,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-neutral-text)\",\n }}\n >\n {message.timestamp}\n </span>\n </div>\n </div>\n ))}\n </div>\n\n {/* View More */}\n <div \n className=\"pt-[var(--spacing-lg)] border-t border-[var(--canvas-neutral-border)] text-center\"\n >\n <button\n onClick={onViewMoreMessages}\n className=\"hover:underline\"\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 500,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-primary)\",\n }}\n >\n view more\n </button>\n </div>\n </div>\n );\n\n // Notifications popover content component\n const NotificationsPopoverContent = () => (\n <div className=\"w-[320px]\">\n {/* Header */}\n <div \n className=\"flex items-center justify-between pb-[var(--spacing-xl)] border-b border-[var(--canvas-neutral-border)]\"\n >\n <span\n style={{\n fontFamily: \"var(--typo-body-m-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-m-size)\",\n fontWeight: 600,\n lineHeight: \"var(--typo-body-m-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n Notifications\n </span>\n <button\n onClick={onMarkNotificationsAsRead}\n className=\"hover:underline\"\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 500,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-primary)\",\n }}\n >\n Mark as read\n </button>\n </div>\n\n {/* Notifications List */}\n <div className=\"py-[var(--spacing-lg)]\">\n {notifications.map((notification, index) => (\n <div \n key={notification.id} \n className={`flex gap-[var(--spacing-lg)] py-[var(--spacing-lg)] ${\n index < notifications.length - 1 ? \"border-b border-[var(--canvas-neutral-border)]\" : \"\"\n }`}\n >\n {/* User Avatar */}\n <Avatar className=\"size-10 shrink-0\">\n <AvatarImage src={notification.userAvatar} alt={notification.userName} />\n <AvatarFallback \n className=\"bg-[var(--canvas-neutral-surface)] text-[var(--canvas-neutral-text)]\"\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n }}\n >\n {notification.userName.split(\" \").map(n => n[0]).join(\"\")}\n </AvatarFallback>\n </Avatar>\n \n {/* Notification Details */}\n <div className=\"flex flex-col justify-center min-w-0\">\n <span\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 400,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n <span style={{ fontWeight: 600 }}>{notification.userName}</span> {notification.action}\n </span>\n <span\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 400,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-neutral-text)\",\n }}\n >\n {notification.timestamp}\n </span>\n </div>\n </div>\n ))}\n </div>\n\n {/* View More */}\n <div \n className=\"pt-[var(--spacing-lg)] border-t border-[var(--canvas-neutral-border)] text-center\"\n >\n <button\n onClick={onViewMoreNotifications}\n className=\"hover:underline\"\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 500,\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-primary)\",\n }}\n >\n view more\n </button>\n </div>\n </div>\n );\n\n // Get the icon shape renderer\n const shapeRenderer = iconShapes[branding.iconShape as keyof typeof iconShapes] || iconShapes.rounded;\n\n // Logo component used for both mobile and desktop (when showDesktopLogo is true)\n // Uses CSS variables directly - no JavaScript resolution needed\n const LogoIcon = () => {\n // Use CSS variables directly - the browser handles resolution\n const bgColor = branding.bgColor || \"var(--canvas-primary)\";\n const iconColor = branding.iconColor || \"var(--canvas-primary-foreground)\";\n const IconComponent = iconMap[branding.iconName || \"Buildings\"] || Buildings;\n \n return (\n <div className=\"relative size-8 shrink-0\">\n {shapeRenderer(bgColor)}\n <div className=\"absolute inset-0 flex items-center justify-center z-10\">\n <IconComponent weight=\"bold\" size={18} color={iconColor} />\n </div>\n </div>\n );\n };\n\n return (\n <header \n className={`h-[var(--header-height)] w-full border-b ${\n isDark \n ? \"bg-[var(--canvas-sidebar-dark-bg)] border-[var(--canvas-sidebar-dark-border)]\" \n : \"bg-[var(--canvas-background)] border-[var(--canvas-neutral-border)]\"\n }`}\n >\n <div className=\"flex items-center h-full px-4 lg:px-[var(--spacing-5xl)]\">\n {/* Logo - Visible on mobile, and on desktop when showDesktopLogo is true */}\n <div className={`flex items-center gap-[var(--spacing-md)] h-8 shrink-0 ${showDesktopLogo ? '' : 'lg:hidden'}`}>\n <LogoIcon />\n {/* Wordmark - only on desktop when showDesktopLogo is true */}\n {showDesktopLogo && (\n <span \n className={`hidden lg:block ${isDark ? \"text-white\" : \"text-[var(--canvas-text)]\"}`}\n style={{\n fontFamily: \"var(--typo-header-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-xl-size)\",\n fontWeight: 600,\n letterSpacing: \"var(--typo-header-spacing)\",\n lineHeight: \"var(--typo-header-line-height)\",\n }}\n >\n {branding.wordmark || \"canvas\"}\n </span>\n )}\n </div>\n\n {/* Spacer */}\n <div className=\"flex-1\" />\n\n {/* Navigation Links - Desktop Only */}\n <nav className=\"hidden lg:flex items-center gap-[var(--spacing-2xl)] h-full\">\n {navItems.map((item) => (\n <button\n key={item.id}\n onClick={() => {\n item.onClick?.();\n if (item.href) {\n window.location.href = item.href;\n }\n }}\n className={`cursor-pointer transition-colors ${\n isDark \n ? \"text-white/60 hover:text-white\" \n : \"text-[var(--canvas-neutral-text)] hover:text-[var(--canvas-text)]\"\n }`}\n style={{\n fontFamily: \"var(--typo-header-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-header-size)\",\n fontWeight: \"var(--typo-header-weight)\",\n letterSpacing: \"var(--typo-header-spacing)\",\n lineHeight: \"var(--typo-header-line-height)\",\n }}\n >\n {item.label}\n </button>\n ))}\n </nav>\n\n {/* Icons - Always Visible */}\n <div className=\"flex items-center gap-[var(--spacing-2xl)] ml-[var(--spacing-2xl)]\">\n <button \n className={`transition-colors ${\n isDark \n ? \"text-white/60 hover:text-white\" \n : \"text-[var(--canvas-neutral-text)] hover:text-[var(--canvas-text)]\"\n }`}\n aria-label=\"Search\"\n >\n <Search className=\"size-4\" />\n </button>\n \n <Popover>\n <PopoverTrigger asChild>\n <button \n className={`transition-colors ${\n isDark \n ? \"text-white/60 hover:text-white\" \n : \"text-[var(--canvas-neutral-text)] hover:text-[var(--canvas-text)]\"\n }`}\n aria-label=\"Notifications\"\n >\n <Bell className=\"size-4\" />\n </button>\n </PopoverTrigger>\n <PopoverContent align=\"end\" sideOffset={16} className=\"w-auto p-[var(--spacing-xl)]\">\n <NotificationsPopoverContent />\n </PopoverContent>\n </Popover>\n \n <Popover>\n <PopoverTrigger asChild>\n <button \n className={`transition-colors ${\n isDark \n ? \"text-white/60 hover:text-white\" \n : \"text-[var(--canvas-neutral-text)] hover:text-[var(--canvas-text)]\"\n }`}\n aria-label=\"Messages\"\n >\n <MessageSquare className=\"size-4\" />\n </button>\n </PopoverTrigger>\n <PopoverContent align=\"end\" sideOffset={16} className=\"w-auto p-[var(--spacing-xl)]\">\n <MessagesPopoverContent />\n </PopoverContent>\n </Popover>\n \n <Popover>\n <PopoverTrigger asChild>\n <button \n className={`transition-colors ${\n isDark \n ? \"text-white/60 hover:text-white\" \n : \"text-[var(--canvas-neutral-text)] hover:text-[var(--canvas-text)]\"\n }`}\n aria-label=\"Cart\"\n >\n <ShoppingCart className=\"size-4\" />\n </button>\n </PopoverTrigger>\n <PopoverContent align=\"end\" sideOffset={16} className=\"w-auto p-[var(--spacing-xl)]\">\n <CartPopoverContent />\n </PopoverContent>\n </Popover>\n \n {/* Auth Buttons - Desktop Only */}\n {showAuthButtons && (\n <div className=\"hidden lg:flex items-center gap-[var(--spacing-lg)]\">\n <Button \n variant=\"outline\" \n size=\"default\"\n onClick={onLogin}\n >\n Log in\n </Button>\n <Button \n variant=\"default\" \n size=\"default\"\n onClick={onSignUp}\n >\n Sign up\n </Button>\n </div>\n )}\n \n {/* Avatar with Dropdown */}\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <button className=\"rounded-full focus:outline-none focus:ring-2 focus:ring-[var(--canvas-primary)] focus:ring-offset-2\">\n <Avatar className={`size-10 border cursor-pointer ${\n isDark \n ? \"border-[var(--canvas-sidebar-dark-border)]\" \n : \"border-[var(--canvas-neutral-border)]\"\n }`}>\n <AvatarImage src={avatarUrl} alt=\"User avatar\" />\n <AvatarFallback \n className={\n isDark \n ? \"bg-white/10 text-white/60\" \n : \"bg-[var(--canvas-neutral-surface)] text-[var(--canvas-neutral-text)]\"\n }\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n }}\n >\n JC\n </AvatarFallback>\n </Avatar>\n </button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"end\" sideOffset={8}>\n <DropdownMenuItem onClick={onAccountClick}>\n <User className=\"size-4 mr-2\" />\n My Account\n </DropdownMenuItem>\n <DropdownMenuItem onClick={onLogout}>\n <LogOut className=\"size-4 mr-2\" />\n Logout\n </DropdownMenuItem>\n </DropdownMenuContent>\n </DropdownMenu>\n\n {/* Mobile Menu Button */}\n <Button \n variant=\"ghost\" \n size=\"icon\" \n onClick={() => {\n setIsMobileMenuOpen(true);\n onMenuClick?.();\n }}\n aria-label=\"Open menu\"\n className={`lg:hidden -ml-[var(--spacing-md)] ${isDark ? \"text-white/60 hover:text-white hover:bg-white/10\" : \"text-[var(--canvas-neutral-text)]\"}`}\n >\n <Menu className=\"size-4\" />\n </Button>\n </div>\n </div>\n\n {/* Mobile Menu Overlay */}\n {isMobileMenuOpen && (\n <div className=\"fixed inset-0 z-50 lg:hidden\">\n {/* Backdrop */}\n <div \n className=\"absolute inset-0 bg-black/50\"\n onClick={() => setIsMobileMenuOpen(false)}\n />\n \n {/* Menu Panel */}\n <div className=\"absolute right-0 top-0 h-full w-full max-w-sm bg-[var(--canvas-background)] shadow-xl\">\n {/* Close Button */}\n <div className=\"flex justify-end p-4\">\n <Button\n variant=\"ghost\"\n size=\"icon\"\n onClick={() => setIsMobileMenuOpen(false)}\n aria-label=\"Close menu\"\n >\n <X className=\"size-5\" />\n </Button>\n </div>\n \n {/* Navigation Items */}\n <nav className=\"px-6 py-4\">\n <div className=\"space-y-2\">\n {navItems.map((item) => {\n const Icon = item.icon;\n return (\n <button\n key={item.id}\n onClick={() => {\n item.onClick?.();\n if (item.href) {\n window.location.href = item.href;\n }\n setIsMobileMenuOpen(false);\n }}\n className=\"flex items-center gap-[var(--spacing-lg)] w-full py-[var(--spacing-lg)] text-left hover:bg-[var(--canvas-neutral-surface)] rounded-[var(--radius-md)] transition-colors\"\n >\n {Icon && (\n <div \n className=\"size-12 rounded-[var(--radius-md)] flex items-center justify-center shrink-0\"\n style={{\n backgroundColor: \"color-mix(in srgb, var(--canvas-primary) 10%, transparent)\",\n }}\n >\n <Icon \n className=\"size-5\"\n style={{ color: \"var(--canvas-primary)\" }}\n />\n </div>\n )}\n <span\n style={{\n fontFamily: \"var(--typo-body-m-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-m-size)\",\n fontWeight: 400,\n lineHeight: \"var(--typo-body-m-line-height)\",\n color: \"var(--canvas-text)\",\n }}\n >\n {item.label}\n </span>\n </button>\n );\n })}\n </div>\n </nav>\n \n {/* Auth Buttons */}\n {showAuthButtons && (\n <div className=\"absolute bottom-0 left-0 right-0 p-6 space-y-3 border-t border-[var(--canvas-neutral-border)]\">\n <Button \n variant=\"outline\" \n className=\"w-full\"\n size=\"lg\"\n onClick={() => {\n onLogin?.();\n setIsMobileMenuOpen(false);\n }}\n >\n Log in\n </Button>\n <Button \n variant=\"default\" \n className=\"w-full\"\n size=\"lg\"\n onClick={() => {\n onSignUp?.();\n setIsMobileMenuOpen(false);\n }}\n >\n Sign up\n </Button>\n </div>\n )}\n </div>\n </div>\n )}\n </header>\n );\n}\n\n"
|
|
10
10
|
}
|
|
11
11
|
],
|
|
12
12
|
"dependencies": [
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
{
|
|
7
7
|
"path": "components/layout/icon-sidebar-shell.tsx",
|
|
8
8
|
"type": "registry:layout",
|
|
9
|
-
"content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { ChevronRight } from \"lucide-react\";\nimport { Header } from \"./header\";\nimport { useCSSVariableSync } from \"../../hooks/use-css-variable-sync\";\nimport { IconSidebar, IconNavItemConfig, defaultIconNavItems } from \"./icon-sidebar\";\nimport { \n Sheet, \n SheetContent,\n SheetTitle,\n} from \"../ui/sheet\";\nimport { cn } from \"../../lib/utils\";\nimport * as VisuallyHidden from \"@radix-ui/react-visually-hidden\";\n\ninterface IconSidebarShellProps {\n /** Navigation items for the icon sidebar */\n navigation?: IconNavItemConfig[];\n /** Optional page header content (e.g., breadcrumbs, page title) */\n pageHeader?: React.ReactNode;\n /** Main content - the modular blocks */\n children: React.ReactNode;\n /** Callback when a nav item is clicked */\n onNavItemClick?: (item: IconNavItemConfig) => void;\n /** Callback when app menu (hamburger) is clicked - for future app-level menu */\n onAppMenuClick?: () => void;\n /** Additional class name for the main content area */\n contentClassName?: string;\n}\n\n/**\n * Canvas Design System - Icon Sidebar Shell\n * \n * A composable page layout with a narrow icon sidebar that provides:\n * - Fixed header (80px)\n * - Fixed narrow dark icon sidebar on desktop (96px, hidden on mobile)\n * - Floating sidebar toggle button on mobile (left edge)\n * - Mobile sheet navigation for icon sidebar\n * - Hamburger menu in header for app-level menu (future)\n * - Main content area with pageHeader slot and children slot for blocks\n * \n * Uses the same styling and spacing as DashboardShell for non-sidebar content.\n * \n * @example\n * ```tsx\n * <IconSidebarShell navigation={iconNavItems}>\n * <ContentDropzone label=\"Main content area\" />\n * </IconSidebarShell>\n * ```\n */\nexport function IconSidebarShell({\n navigation = defaultIconNavItems,\n pageHeader,\n children,\n onNavItemClick,\n onAppMenuClick,\n contentClassName,\n}: IconSidebarShellProps) {\n useCSSVariableSync();\n const [sidebarOpen, setSidebarOpen] = useState(false);\n\n const handleNavItemClick = (item: IconNavItemConfig) => {\n onNavItemClick?.(item);\n // Close sidebar when nav item is clicked\n setSidebarOpen(false);\n };\n\n const handleAppMenuClick = () => {\n // Placeholder for future app-level menu\n onAppMenuClick?.();\n console.log(\"App menu clicked - implement app-level mobile menu here\");\n };\n\n return (\n <div className=\"min-h-screen bg-[var(--background)]\">\n {/* Header - Fixed at top, offset on desktop to not overlap icon sidebar */}\n <div className=\"fixed top-0 left-0 right-0 lg:left-[var(--icon-sidebar-width)] z-40\">\n <Header onMenuClick={handleAppMenuClick} />\n </div>\n\n {/* Desktop Icon Sidebar - Fixed on left, visible lg+ */}\n <div className=\"hidden lg:block fixed top-0 left-0 bottom-0 z-50 w-[var(--icon-sidebar-width)]\">\n <IconSidebar \n items={navigation} \n variant=\"dark\" \n onItemClick={handleNavItemClick}\n />\n </div>\n\n {/* Mobile Sidebar Toggle Button - Floating on left edge */}\n <button\n onClick={() => setSidebarOpen(true)}\n className={cn(\n \"lg:hidden fixed left-0 z-30\",\n \"top-[calc(var(--header-height)+4px)]\",\n \"flex items-center justify-center\",\n \"size-11\",\n \"bg-
|
|
9
|
+
"content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { ChevronRight } from \"lucide-react\";\nimport { Header } from \"./header\";\nimport { useCSSVariableSync } from \"../../hooks/use-css-variable-sync\";\nimport { IconSidebar, IconNavItemConfig, defaultIconNavItems } from \"./icon-sidebar\";\nimport { \n Sheet, \n SheetContent,\n SheetTitle,\n} from \"../ui/sheet\";\nimport { cn } from \"../../lib/utils\";\nimport * as VisuallyHidden from \"@radix-ui/react-visually-hidden\";\n\ninterface IconSidebarShellProps {\n /** Navigation items for the icon sidebar */\n navigation?: IconNavItemConfig[];\n /** Optional page header content (e.g., breadcrumbs, page title) */\n pageHeader?: React.ReactNode;\n /** Main content - the modular blocks */\n children: React.ReactNode;\n /** Callback when a nav item is clicked */\n onNavItemClick?: (item: IconNavItemConfig) => void;\n /** Callback when app menu (hamburger) is clicked - for future app-level menu */\n onAppMenuClick?: () => void;\n /** Additional class name for the main content area */\n contentClassName?: string;\n}\n\n/**\n * Canvas Design System - Icon Sidebar Shell\n * \n * A composable page layout with a narrow icon sidebar that provides:\n * - Fixed header (80px)\n * - Fixed narrow dark icon sidebar on desktop (96px, hidden on mobile)\n * - Floating sidebar toggle button on mobile (left edge)\n * - Mobile sheet navigation for icon sidebar\n * - Hamburger menu in header for app-level menu (future)\n * - Main content area with pageHeader slot and children slot for blocks\n * \n * Uses the same styling and spacing as DashboardShell for non-sidebar content.\n * \n * @example\n * ```tsx\n * <IconSidebarShell navigation={iconNavItems}>\n * <ContentDropzone label=\"Main content area\" />\n * </IconSidebarShell>\n * ```\n */\nexport function IconSidebarShell({\n navigation = defaultIconNavItems,\n pageHeader,\n children,\n onNavItemClick,\n onAppMenuClick,\n contentClassName,\n}: IconSidebarShellProps) {\n useCSSVariableSync();\n const [sidebarOpen, setSidebarOpen] = useState(false);\n\n const handleNavItemClick = (item: IconNavItemConfig) => {\n onNavItemClick?.(item);\n // Close sidebar when nav item is clicked\n setSidebarOpen(false);\n };\n\n const handleAppMenuClick = () => {\n // Placeholder for future app-level menu\n onAppMenuClick?.();\n console.log(\"App menu clicked - implement app-level mobile menu here\");\n };\n\n return (\n <div className=\"min-h-screen bg-[var(--canvas-background)]\">\n {/* Header - Fixed at top, offset on desktop to not overlap icon sidebar */}\n <div className=\"fixed top-0 left-0 right-0 lg:left-[var(--icon-sidebar-width)] z-40\">\n <Header onMenuClick={handleAppMenuClick} />\n </div>\n\n {/* Desktop Icon Sidebar - Fixed on left, visible lg+ */}\n <div className=\"hidden lg:block fixed top-0 left-0 bottom-0 z-50 w-[var(--icon-sidebar-width)]\">\n <IconSidebar \n items={navigation} \n variant=\"dark\" \n onItemClick={handleNavItemClick}\n />\n </div>\n\n {/* Mobile Sidebar Toggle Button - Floating on left edge */}\n <button\n onClick={() => setSidebarOpen(true)}\n className={cn(\n \"lg:hidden fixed left-0 z-30\",\n \"top-[calc(var(--header-height)+4px)]\",\n \"flex items-center justify-center\",\n \"size-11\",\n \"bg-[var(--canvas-background)]\",\n \"border border-l-0 border-[var(--canvas-neutral-border)]\",\n \"rounded-r-[var(--radius-xs)]\",\n \"shadow-[0px_4px_16px_0px_rgba(0,0,0,0.04)]\",\n \"transition-opacity hover:opacity-80\"\n )}\n aria-label=\"Open sidebar\"\n >\n <ChevronRight className=\"size-6 text-[var(--canvas-primary)]\" />\n </button>\n\n {/* Mobile Icon Sidebar Sheet */}\n <Sheet open={sidebarOpen} onOpenChange={setSidebarOpen}>\n <SheetContent side=\"left\" className=\"p-0 w-[var(--icon-sidebar-width)]\">\n <VisuallyHidden.Root>\n <SheetTitle>Navigation</SheetTitle>\n </VisuallyHidden.Root>\n <IconSidebar \n items={navigation} \n variant=\"dark\" \n onItemClick={handleNavItemClick}\n />\n </SheetContent>\n </Sheet>\n\n {/* Main Content Area - Same styling as DashboardShell */}\n <main\n className={cn(\n \"pt-[var(--header-height)]\",\n \"lg:pl-[var(--icon-sidebar-width)]\",\n \"min-h-screen\"\n )}\n >\n <div \n className={cn(\n \"flex flex-col gap-[var(--spacing-6xl)]\",\n \"px-[var(--spacing-xl)] lg:px-[var(--spacing-5xl)]\",\n \"pt-10 pb-[var(--spacing-5xl)]\",\n contentClassName\n )}\n >\n {/* Page Header Slot */}\n {pageHeader && (\n <section className=\"pt-0\">\n {pageHeader}\n </section>\n )}\n\n {/* Main Content Slot - Blocks go here */}\n <section className=\"flex flex-col gap-[var(--spacing-6xl)]\">\n {children}\n </section>\n </div>\n </main>\n </div>\n );\n}\n\n// Re-export types for convenience\nexport type { IconNavItemConfig } from \"./icon-sidebar\";\n\n\n"
|
|
10
10
|
}
|
|
11
11
|
],
|
|
12
12
|
"dependencies": [
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
{
|
|
7
7
|
"path": "components/layout/icon-sidebar.tsx",
|
|
8
8
|
"type": "registry:layout",
|
|
9
|
-
"content": "\"use client\";\n\nimport { cn } from \"../../lib/utils\";\nimport { LucideIcon, Home, Users, Calendar, MessageSquare, PieChart, FileText, ShoppingBag } from \"lucide-react\";\nimport { useThemeImages, useThemeBranding } from \"../../context/theme-context\";\n\n// Phosphor Icons for Logo\nimport { Buildings, type Icon as PhosphorIcon } from \"@phosphor-icons/react\";\nimport {\n Diamond, Hexagon, Star, Lightning, Sparkle, Infinity, Code, Terminal, Cpu,\n Database, Globe, Cloud, WifiHigh, Briefcase, Storefront, Handshake, ChartLine,\n Palette as PaletteIcon, PencilSimple, Camera, MusicNote, Lightbulb, Leaf, Tree,\n Sun, Moon, Fire, Drop, ChatCircle, Envelope, Phone, Megaphone, Heart, Shield,\n Trophy, Rocket, Target, Flag,\n} from \"@phosphor-icons/react\";\n\n// ============================================\n// Icon Shape Presets for Logo Creator\n// ============================================\n\ntype IconShapeId = \"rounded\" | \"circle\" | \"square\";\n\ninterface IconShape {\n id: IconShapeId;\n renderBackground: (bgColor: string) => React.ReactNode;\n}\n\nconst iconShapes: IconShape[] = [\n {\n id: \"rounded\",\n renderBackground: (bgColor: string) => (\n <svg viewBox=\"0 0 32 32\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\" className=\"size-full absolute inset-0\">\n <rect width=\"32\" height=\"32\" rx=\"10\" style={{ fill: bgColor }} />\n </svg>\n ),\n },\n {\n id: \"circle\",\n renderBackground: (bgColor: string) => (\n <svg viewBox=\"0 0 32 32\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\" className=\"size-full absolute inset-0\">\n <circle cx=\"16\" cy=\"16\" r=\"16\" style={{ fill: bgColor }} />\n </svg>\n ),\n },\n {\n id: \"square\",\n renderBackground: (bgColor: string) => (\n <svg viewBox=\"0 0 32 32\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\" className=\"size-full absolute inset-0\">\n <rect width=\"32\" height=\"32\" style={{ fill: bgColor }} />\n </svg>\n ),\n },\n];\n\n// Map icon names to components\nconst iconMap: Record<string, PhosphorIcon> = {\n Diamond, Hexagon, Star, Lightning, Sparkle, Infinity, Code, Terminal, Cpu,\n Database, Globe, Cloud, WifiHigh, Briefcase, Buildings, Storefront, Handshake,\n ChartLine, Palette: PaletteIcon, PencilSimple, Camera, MusicNote, Lightbulb,\n Leaf, Tree, Sun, Moon, Fire, Drop, ChatCircle, Envelope, Phone, Megaphone,\n Heart, Shield, Trophy, Rocket, Target, Flag,\n};\n\n// Helper to resolve CSS variable references to actual hex colors\nfunction resolveBrandingColor(value: string): string {\n if (!value) return \"#ffffff\";\n if (value.startsWith(\"var(\")) {\n const varName = value.replace(\"var(\", \"\").replace(\")\", \"\");\n if (typeof window !== \"undefined\") {\n const computed = getComputedStyle(document.documentElement).getPropertyValue(varName).trim();\n return computed || \"#ffffff\";\n }\n return \"#ffffff\";\n }\n return value;\n}\n\n// ============================================\n// Icon Nav Item\n// ============================================\n\nexport interface IconNavItemConfig {\n id: string;\n label: string;\n icon: LucideIcon;\n href?: string;\n isActive?: boolean;\n hasNotification?: boolean;\n}\n\ninterface IconNavItemProps {\n item: IconNavItemConfig;\n variant?: \"dark\" | \"light\";\n onClick?: () => void;\n}\n\nfunction IconNavItem({ item, variant = \"dark\", onClick }: IconNavItemProps) {\n const Icon = item.icon;\n const isDark = variant === \"dark\";\n const isActive = item.isActive;\n\n return (\n <button\n onClick={onClick}\n className={cn(\n \"relative flex flex-col items-center justify-center gap-1 w-16 h-16 rounded-[var(--radius-nav)] transition-colors\",\n // Dark variant\n isDark && isActive && \"bg-[var(--canvas-sidebar-dark-active-bg)]\",\n isDark && !isActive && \"hover:bg-[var(--canvas-sidebar-dark-active-bg)]/50\",\n // Light variant\n !isDark && isActive && \"bg-[var(--canvas-sidebar-light-active-bg)]\",\n !isDark && !isActive && \"hover:bg-[var(--canvas-sidebar-light-active-bg)]/50\"\n )}\n >\n <Icon\n className={cn(\n \"size-4\",\n isDark && isActive && \"text-[var(--canvas-sidebar-dark-active-text)]\",\n isDark && !isActive && \"text-[var(--canvas-sidebar-dark-text)]\",\n !isDark && isActive && \"text-[var(--canvas-sidebar-light-active-text)]\",\n !isDark && !isActive && \"text-[var(--canvas-sidebar-light-text)]\"\n )}\n />\n <span\n className={cn(\n isDark && isActive && \"text-[var(--canvas-sidebar-dark-active-text)]\",\n isDark && !isActive && \"text-[var(--canvas-sidebar-dark-text)]\",\n !isDark && isActive && \"text-[var(--canvas-sidebar-light-active-text)]\",\n !isDark && !isActive && \"text-[var(--canvas-sidebar-light-text)]\"\n )}\n style={{\n fontFamily: \"var(--typo-sidebar-tab-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-xs-size)\",\n fontWeight: \"var(--typo-sidebar-tab-weight)\",\n letterSpacing: \"var(--typo-sidebar-tab-spacing)\",\n lineHeight: \"var(--typo-sidebar-tab-line-height)\",\n }}\n >\n {item.label}\n </span>\n\n {/* Notification Badge */}\n {item.hasNotification && (\n <div className=\"absolute top-2 right-4 size-1.5 rounded-full bg-[var(--canvas-destructive)]\" />\n )}\n </button>\n );\n}\n\n// ============================================\n// Default Navigation Items\n// ============================================\n\nexport const defaultIconNavItems: IconNavItemConfig[] = [\n { id: \"home\", label: \"Home\", icon: Home, isActive: true },\n { id: \"teams\", label: \"Teams\", icon: Users },\n { id: \"calendar\", label: \"Calendar\", icon: Calendar },\n { id: \"messages\", label: \"Messages\", icon: MessageSquare, hasNotification: true },\n { id: \"reports\", label: \"Reports\", icon: PieChart },\n { id: \"docs\", label: \"Docs\", icon: FileText },\n { id: \"orders\", label: \"Orders\", icon: ShoppingBag },\n];\n\n// ============================================\n// Icon Sidebar\n// ============================================\n\ninterface IconSidebarProps {\n /** Navigation items to display */\n items?: IconNavItemConfig[];\n /** Visual variant - dark for desktop, light for mobile sheet */\n variant?: \"dark\" | \"light\";\n /** Callback when a nav item is clicked */\n onItemClick?: (item: IconNavItemConfig) => void;\n /** Additional class names */\n className?: string;\n}\n\n/**\n * Canvas Design System - Icon Sidebar Component\n * \n * A narrow sidebar (96px) with vertically stacked icon navigation.\n * Desktop: Fixed dark sidebar on the left\n * Mobile: Light theme sidebar rendered inside a Sheet\n */\nexport function IconSidebar({\n items = defaultIconNavItems,\n variant = \"dark\",\n onItemClick,\n className\n}: IconSidebarProps) {\n const isDark = variant === \"dark\";\n const themeImages = useThemeImages();\n const { branding, isMounted } = useThemeBranding();\n\n // Get the appropriate logo based on variant\n const logoUrl = isDark ? themeImages.logoDark : themeImages.logoLight;\n\n // Get the icon shape renderer\n const iconShape = iconShapes.find(s => s.id === branding.iconShape) || iconShapes[0];\n\n return (\n <aside\n className={cn(\n \"flex flex-col items-center h-full w-[var(--icon-sidebar-width)]\",\n isDark && \"bg-[var(--canvas-sidebar-dark-bg)] border-r border-[var(--canvas-sidebar-dark-border)]\",\n !isDark && \"bg-
|
|
9
|
+
"content": "\"use client\";\n\nimport { cn } from \"../../lib/utils\";\nimport { LucideIcon, Home, Users, Calendar, MessageSquare, PieChart, FileText, ShoppingBag } from \"lucide-react\";\nimport { useThemeImages, useThemeBranding } from \"../../context/theme-context\";\n\n// Phosphor Icons for Logo\nimport { Buildings, type Icon as PhosphorIcon } from \"@phosphor-icons/react\";\nimport {\n Diamond, Hexagon, Star, Lightning, Sparkle, Infinity, Code, Terminal, Cpu,\n Database, Globe, Cloud, WifiHigh, Briefcase, Storefront, Handshake, ChartLine,\n Palette as PaletteIcon, PencilSimple, Camera, MusicNote, Lightbulb, Leaf, Tree,\n Sun, Moon, Fire, Drop, ChatCircle, Envelope, Phone, Megaphone, Heart, Shield,\n Trophy, Rocket, Target, Flag,\n} from \"@phosphor-icons/react\";\n\n// ============================================\n// Icon Shape Presets for Logo Creator\n// ============================================\n\ntype IconShapeId = \"rounded\" | \"circle\" | \"square\";\n\ninterface IconShape {\n id: IconShapeId;\n renderBackground: (bgColor: string) => React.ReactNode;\n}\n\nconst iconShapes: IconShape[] = [\n {\n id: \"rounded\",\n renderBackground: (bgColor: string) => (\n <svg viewBox=\"0 0 32 32\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\" className=\"size-full absolute inset-0\">\n <rect width=\"32\" height=\"32\" rx=\"10\" style={{ fill: bgColor }} />\n </svg>\n ),\n },\n {\n id: \"circle\",\n renderBackground: (bgColor: string) => (\n <svg viewBox=\"0 0 32 32\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\" className=\"size-full absolute inset-0\">\n <circle cx=\"16\" cy=\"16\" r=\"16\" style={{ fill: bgColor }} />\n </svg>\n ),\n },\n {\n id: \"square\",\n renderBackground: (bgColor: string) => (\n <svg viewBox=\"0 0 32 32\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\" className=\"size-full absolute inset-0\">\n <rect width=\"32\" height=\"32\" style={{ fill: bgColor }} />\n </svg>\n ),\n },\n];\n\n// Map icon names to components\nconst iconMap: Record<string, PhosphorIcon> = {\n Diamond, Hexagon, Star, Lightning, Sparkle, Infinity, Code, Terminal, Cpu,\n Database, Globe, Cloud, WifiHigh, Briefcase, Buildings, Storefront, Handshake,\n ChartLine, Palette: PaletteIcon, PencilSimple, Camera, MusicNote, Lightbulb,\n Leaf, Tree, Sun, Moon, Fire, Drop, ChatCircle, Envelope, Phone, Megaphone,\n Heart, Shield, Trophy, Rocket, Target, Flag,\n};\n\n// Helper to resolve CSS variable references to actual hex colors\nfunction resolveBrandingColor(value: string): string {\n if (!value) return \"#ffffff\";\n if (value.startsWith(\"var(\")) {\n const varName = value.replace(\"var(\", \"\").replace(\")\", \"\");\n if (typeof window !== \"undefined\") {\n const computed = getComputedStyle(document.documentElement).getPropertyValue(varName).trim();\n return computed || \"#ffffff\";\n }\n return \"#ffffff\";\n }\n return value;\n}\n\n// ============================================\n// Icon Nav Item\n// ============================================\n\nexport interface IconNavItemConfig {\n id: string;\n label: string;\n icon: LucideIcon;\n href?: string;\n isActive?: boolean;\n hasNotification?: boolean;\n}\n\ninterface IconNavItemProps {\n item: IconNavItemConfig;\n variant?: \"dark\" | \"light\";\n onClick?: () => void;\n}\n\nfunction IconNavItem({ item, variant = \"dark\", onClick }: IconNavItemProps) {\n const Icon = item.icon;\n const isDark = variant === \"dark\";\n const isActive = item.isActive;\n\n return (\n <button\n onClick={onClick}\n className={cn(\n \"relative flex flex-col items-center justify-center gap-1 w-16 h-16 rounded-[var(--radius-nav)] transition-colors\",\n // Dark variant\n isDark && isActive && \"bg-[var(--canvas-sidebar-dark-active-bg)]\",\n isDark && !isActive && \"hover:bg-[var(--canvas-sidebar-dark-active-bg)]/50\",\n // Light variant\n !isDark && isActive && \"bg-[var(--canvas-sidebar-light-active-bg)]\",\n !isDark && !isActive && \"hover:bg-[var(--canvas-sidebar-light-active-bg)]/50\"\n )}\n >\n <Icon\n className={cn(\n \"size-4\",\n isDark && isActive && \"text-[var(--canvas-sidebar-dark-active-text)]\",\n isDark && !isActive && \"text-[var(--canvas-sidebar-dark-text)]\",\n !isDark && isActive && \"text-[var(--canvas-sidebar-light-active-text)]\",\n !isDark && !isActive && \"text-[var(--canvas-sidebar-light-text)]\"\n )}\n />\n <span\n className={cn(\n isDark && isActive && \"text-[var(--canvas-sidebar-dark-active-text)]\",\n isDark && !isActive && \"text-[var(--canvas-sidebar-dark-text)]\",\n !isDark && isActive && \"text-[var(--canvas-sidebar-light-active-text)]\",\n !isDark && !isActive && \"text-[var(--canvas-sidebar-light-text)]\"\n )}\n style={{\n fontFamily: \"var(--typo-sidebar-tab-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-xs-size)\",\n fontWeight: \"var(--typo-sidebar-tab-weight)\",\n letterSpacing: \"var(--typo-sidebar-tab-spacing)\",\n lineHeight: \"var(--typo-sidebar-tab-line-height)\",\n }}\n >\n {item.label}\n </span>\n\n {/* Notification Badge */}\n {item.hasNotification && (\n <div className=\"absolute top-2 right-4 size-1.5 rounded-full bg-[var(--canvas-destructive)]\" />\n )}\n </button>\n );\n}\n\n// ============================================\n// Default Navigation Items\n// ============================================\n\nexport const defaultIconNavItems: IconNavItemConfig[] = [\n { id: \"home\", label: \"Home\", icon: Home, isActive: true },\n { id: \"teams\", label: \"Teams\", icon: Users },\n { id: \"calendar\", label: \"Calendar\", icon: Calendar },\n { id: \"messages\", label: \"Messages\", icon: MessageSquare, hasNotification: true },\n { id: \"reports\", label: \"Reports\", icon: PieChart },\n { id: \"docs\", label: \"Docs\", icon: FileText },\n { id: \"orders\", label: \"Orders\", icon: ShoppingBag },\n];\n\n// ============================================\n// Icon Sidebar\n// ============================================\n\ninterface IconSidebarProps {\n /** Navigation items to display */\n items?: IconNavItemConfig[];\n /** Visual variant - dark for desktop, light for mobile sheet */\n variant?: \"dark\" | \"light\";\n /** Callback when a nav item is clicked */\n onItemClick?: (item: IconNavItemConfig) => void;\n /** Additional class names */\n className?: string;\n}\n\n/**\n * Canvas Design System - Icon Sidebar Component\n * \n * A narrow sidebar (96px) with vertically stacked icon navigation.\n * Desktop: Fixed dark sidebar on the left\n * Mobile: Light theme sidebar rendered inside a Sheet\n */\nexport function IconSidebar({\n items = defaultIconNavItems,\n variant = \"dark\",\n onItemClick,\n className\n}: IconSidebarProps) {\n const isDark = variant === \"dark\";\n const themeImages = useThemeImages();\n const { branding, isMounted } = useThemeBranding();\n\n // Get the appropriate logo based on variant\n const logoUrl = isDark ? themeImages.logoDark : themeImages.logoLight;\n\n // Get the icon shape renderer\n const iconShape = iconShapes.find(s => s.id === branding.iconShape) || iconShapes[0];\n\n return (\n <aside\n className={cn(\n \"flex flex-col items-center h-full w-[var(--icon-sidebar-width)]\",\n isDark && \"bg-[var(--canvas-sidebar-dark-bg)] border-r border-[var(--canvas-sidebar-dark-border)]\",\n !isDark && \"bg-[var(--canvas-background)] border-r border-[var(--canvas-border)]\",\n className\n )}\n >\n {/* Logo Section - Just the icon, no wordmark */}\n {/* Hidden until mounted to prevent hydration flash */}\n <div className={`flex items-center justify-center shrink-0 py-5 ${isMounted ? 'opacity-100' : 'opacity-0'}`}>\n {logoUrl ? (\n // Custom logo\n <img\n src={logoUrl}\n alt=\"Logo\"\n className=\"size-8 object-contain\"\n />\n ) : (\n // Logo creator: dynamic icon shape + Phosphor icon (no wordmark for narrow sidebar)\n // Uses CSS variables directly - no JavaScript resolution needed\n <div className=\"relative size-8 shrink-0\">\n {iconShape.renderBackground(branding.bgColor || \"var(--canvas-primary)\")}\n <div className=\"absolute inset-0 flex items-center justify-center z-10\">\n {(() => {\n const IconComponent = iconMap[branding.iconName || \"Buildings\"] || Buildings;\n return <IconComponent weight=\"bold\" size={18} color={branding.iconColor || \"var(--canvas-primary-foreground)\"} />;\n })()}\n </div>\n </div>\n )}\n </div>\n\n {/* Navigation Items */}\n <nav className=\"flex flex-col items-center gap-1 flex-1 px-4 pb-5\">\n {items.map((item) => (\n <IconNavItem\n key={item.id}\n item={item}\n variant={variant}\n onClick={() => onItemClick?.(item)}\n />\n ))}\n </nav>\n </aside>\n );\n}\n\n"
|
|
10
10
|
}
|
|
11
11
|
],
|
|
12
12
|
"dependencies": [
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
{
|
|
7
7
|
"path": "components/layout/mobile-menu-shell.tsx",
|
|
8
8
|
"type": "registry:layout",
|
|
9
|
-
"content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport { Header } from \"./header\";\nimport { useCSSVariableSync } from \"../../hooks/use-css-variable-sync\";\nimport { MobileBottomNav, MobileNavTabConfig, defaultMobileNavTabs } from \"../blocks/mobile-bottom-nav\";\n\ninterface MobileMenuShellProps {\n /** Navigation tabs for the bottom nav */\n tabs?: MobileNavTabConfig[];\n /** Visual variant for the bottom nav - dark or light theme */\n variant?: \"dark\" | \"light\";\n /** ID of the currently active tab */\n activeTab?: string;\n /** Callback when a tab is clicked */\n onTabChange?: (tabId: string) => void;\n /** Main content */\n children: React.ReactNode;\n /** Additional class name for the main content area */\n contentClassName?: string;\n}\n\n/**\n * Canvas Design System - Mobile Menu Shell\n * \n * A layout with:\n * - Fixed header with logo (no sidebar)\n * - Main scrollable content area\n * - Sticky bottom navigation bar (supports dark/light themes)\n * \n * @example\n * ```tsx\n * <MobileMenuShell variant=\"light\">\n * <ContentDropzone />\n * </MobileMenuShell>\n * ```\n */\nexport function MobileMenuShell({\n tabs = defaultMobileNavTabs,\n variant = \"light\",\n activeTab,\n onTabChange,\n children,\n contentClassName,\n}: MobileMenuShellProps) {\n useCSSVariableSync();\n // Internal state for active tab if not controlled\n const [internalActiveTab, setInternalActiveTab] = useState(\n activeTab || tabs[0]?.id || \"home\"\n );\n\n const currentActiveTab = activeTab || internalActiveTab;\n\n // Apply active state to tabs\n const tabsWithActiveState = tabs.map((tab) => ({\n ...tab,\n isActive: tab.id === currentActiveTab,\n }));\n\n const handleTabClick = (tab: MobileNavTabConfig) => {\n if (onTabChange) {\n onTabChange(tab.id);\n } else {\n setInternalActiveTab(tab.id);\n }\n };\n\n return (\n <div className=\"min-h-screen bg-[var(--background)]\">\n {/* Header - Fixed at top with logo visible (no sidebar) */}\n <header className=\"sticky top-0 z-40\">\n <Header showDesktopLogo />\n </header>\n\n {/* Main Content Area */}\n <main className=\"w-full\">\n <div\n className={cn(\n \"w-full max-w-[var(--content-max-width)] mx-auto\",\n \"px-[var(--spacing-xl)] lg:px-[204px]\",\n \"py-[var(--spacing-6xl)]\",\n // Add bottom padding to account for fixed bottom nav (88px)\n \"pb-28\",\n contentClassName\n )}\n >\n {children}\n </div>\n </main>\n\n {/* Sticky Bottom Navigation */}\n <MobileBottomNav\n tabs={tabsWithActiveState}\n variant={variant}\n onTabClick={handleTabClick}\n />\n </div>\n );\n}\n\n"
|
|
9
|
+
"content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport { Header } from \"./header\";\nimport { useCSSVariableSync } from \"../../hooks/use-css-variable-sync\";\nimport { MobileBottomNav, MobileNavTabConfig, defaultMobileNavTabs } from \"../blocks/mobile-bottom-nav\";\n\ninterface MobileMenuShellProps {\n /** Navigation tabs for the bottom nav */\n tabs?: MobileNavTabConfig[];\n /** Visual variant for the bottom nav - dark or light theme */\n variant?: \"dark\" | \"light\";\n /** ID of the currently active tab */\n activeTab?: string;\n /** Callback when a tab is clicked */\n onTabChange?: (tabId: string) => void;\n /** Main content */\n children: React.ReactNode;\n /** Additional class name for the main content area */\n contentClassName?: string;\n}\n\n/**\n * Canvas Design System - Mobile Menu Shell\n * \n * A layout with:\n * - Fixed header with logo (no sidebar)\n * - Main scrollable content area\n * - Sticky bottom navigation bar (supports dark/light themes)\n * \n * @example\n * ```tsx\n * <MobileMenuShell variant=\"light\">\n * <ContentDropzone />\n * </MobileMenuShell>\n * ```\n */\nexport function MobileMenuShell({\n tabs = defaultMobileNavTabs,\n variant = \"light\",\n activeTab,\n onTabChange,\n children,\n contentClassName,\n}: MobileMenuShellProps) {\n useCSSVariableSync();\n // Internal state for active tab if not controlled\n const [internalActiveTab, setInternalActiveTab] = useState(\n activeTab || tabs[0]?.id || \"home\"\n );\n\n const currentActiveTab = activeTab || internalActiveTab;\n\n // Apply active state to tabs\n const tabsWithActiveState = tabs.map((tab) => ({\n ...tab,\n isActive: tab.id === currentActiveTab,\n }));\n\n const handleTabClick = (tab: MobileNavTabConfig) => {\n if (onTabChange) {\n onTabChange(tab.id);\n } else {\n setInternalActiveTab(tab.id);\n }\n };\n\n return (\n <div className=\"min-h-screen bg-[var(--canvas-background)]\">\n {/* Header - Fixed at top with logo visible (no sidebar) */}\n <header className=\"sticky top-0 z-40\">\n <Header showDesktopLogo />\n </header>\n\n {/* Main Content Area */}\n <main className=\"w-full\">\n <div\n className={cn(\n \"w-full max-w-[var(--content-max-width)] mx-auto\",\n \"px-[var(--spacing-xl)] lg:px-[204px]\",\n \"py-[var(--spacing-6xl)]\",\n // Add bottom padding to account for fixed bottom nav (88px)\n \"pb-28\",\n contentClassName\n )}\n >\n {children}\n </div>\n </main>\n\n {/* Sticky Bottom Navigation */}\n <MobileBottomNav\n tabs={tabsWithActiveState}\n variant={variant}\n onTabClick={handleTabClick}\n />\n </div>\n );\n}\n\n"
|
|
10
10
|
}
|
|
11
11
|
],
|
|
12
12
|
"dependencies": [],
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
{
|
|
7
7
|
"path": "components/layout/multistep-progressbar-shell.tsx",
|
|
8
8
|
"type": "registry:layout",
|
|
9
|
-
"content": "\"use client\";\n\nimport { cn } from \"../../lib/utils\";\nimport { Header } from \"./header\";\nimport { useCSSVariableSync } from \"../../hooks/use-css-variable-sync\";\nimport { FlairBanner } from \"../blocks/flair-banner\";\nimport { PageHeaderSection } from \"../blocks/page-header-section\";\nimport { ProgressBar } from \"../blocks/progress-bar\";\nimport { Button } from \"../ui/button\";\nimport { Typography } from \"../ui/typography\";\n\nexport interface MultistepProgressBarStep {\n id: string;\n title: string;\n description?: string;\n}\n\nexport interface MultistepProgressBarShellProps {\n /** Flair banner title */\n bannerTitle?: string;\n /** Flair banner description */\n bannerDescription?: string;\n /** Page title shown below the flair banner */\n pageTitle?: string;\n /** Page description shown below the flair banner */\n pageDescription?: string;\n /** Array of step objects */\n steps?: MultistepProgressBarStep[];\n /** Current active step (0-indexed) */\n currentStep?: number;\n /** Main content */\n children: React.ReactNode;\n /** Callback when Cancel is clicked */\n onCancel?: () => void;\n /** Callback when Continue is clicked */\n onContinue?: () => void;\n /** Cancel button text */\n cancelText?: string;\n /** Continue button text */\n continueText?: string;\n /** Whether Continue button is disabled */\n continueDisabled?: boolean;\n /** Callback when app menu (hamburger) is clicked */\n onAppMenuClick?: () => void;\n /** Additional class name for the content area */\n contentClassName?: string;\n}\n\n/** Default steps for demo/placeholder purposes */\nexport const defaultProgressBarSteps: MultistepProgressBarStep[] = [\n { id: \"step-1\", title: \"Step 1\", description: \"Enter your basic information\" },\n { id: \"step-2\", title: \"Step 2\", description: \"Provide additional details\" },\n { id: \"step-3\", title: \"Step 3\", description: \"Review and submit\" },\n];\n\n/**\n * Canvas Design System - Multistep Progress Bar Shell\n * \n * A layout for multi-step processes/wizards featuring:\n * - Fixed header with logo\n * - Page title and description section\n * - Horizontal progress bar showing completion\n * - Step-specific title and description\n * - Centered content area (max-width 992px)\n * - Navigation buttons (Cancel, Continue)\n * \n * @example\n * ```tsx\n * <MultistepProgressBarShell\n * pageTitle=\"Page title\"\n * pageDescription=\"Description\"\n * steps={[\n * { id: \"step1\", title: \"Step 1\", description: \"First step\" },\n * { id: \"step2\", title: \"Step 2\", description: \"Second step\" },\n * ]}\n * currentStep={0}\n * onCancel={() => router.back()}\n * onContinue={() => setStep(step + 1)}\n * >\n * <ContentDropzone />\n * </MultistepProgressBarShell>\n * ```\n */\nexport function MultistepProgressBarShell({\n bannerTitle = \"Large title\",\n bannerDescription = \"Description\",\n pageTitle = \"Page title\",\n pageDescription = \"Description\",\n steps = defaultProgressBarSteps,\n currentStep = 0,\n children,\n onCancel,\n onContinue,\n cancelText = \"Cancel\",\n continueText = \"Continue\",\n continueDisabled = false,\n onAppMenuClick,\n contentClassName,\n}: MultistepProgressBarShellProps) {\n useCSSVariableSync();\n\n const handleAppMenuClick = () => {\n onAppMenuClick?.();\n console.log(\"App menu clicked - implement app-level mobile menu here\");\n };\n\n const currentStepData = steps[currentStep] || { title: `Step ${currentStep + 1}`, description: \"Description\" };\n const totalSteps = steps.length;\n\n return (\n <div className=\"min-h-screen bg-[var(--background)]\">\n {/* Header - Fixed at top with logo visible */}\n <header className=\"sticky top-0 z-40\">\n <Header onMenuClick={handleAppMenuClick} showDesktopLogo />\n </header>\n\n {/* Flair Banner */}\n <FlairBanner title={bannerTitle} />\n\n {/* Page Header Section */}\n <PageHeaderSection \n title={pageTitle}\n description={pageDescription}\n showTabs={false}\n />\n\n {/* Content Section */}\n <div className=\"w-full\">\n <div \n className={cn(\n \"w-full max-w-[992px] mx-auto\",\n \"px-[var(--spacing-xl)] lg:px-0\",\n \"py-[var(--spacing-5xl)]\",\n contentClassName\n )}\n >\n {/* Progress Bar */}\n <ProgressBar \n currentStep={currentStep}\n totalSteps={totalSteps}\n />\n\n {/* Step Content Container */}\n <div className=\"flex flex-col gap-[var(--spacing-3xl)] pt-[var(--spacing-5xl)]\">\n {/* Step Title and Description */}\n <div className=\"flex flex-col gap-1\">\n <Typography variant=\"h6\" as=\"h3\">\n {currentStepData.title}\n </Typography>\n {currentStepData.description && (\n <Typography variant=\"body-m\" color=\"muted\">\n {currentStepData.description}\n </Typography>\n )}\n </div>\n\n {/* Main Content Area */}\n <div className=\"w-full\">\n {children}\n </div>\n\n {/* Navigation Buttons */}\n <div className=\"flex gap-[var(--spacing-3xl)] items-center justify-end\">\n <Button variant=\"neutral\" onClick={onCancel}>\n {cancelText}\n </Button>\n <Button variant=\"primary\" onClick={onContinue} disabled={continueDisabled}>\n {continueText}\n </Button>\n </div>\n </div>\n </div>\n </div>\n </div>\n );\n}\n\n"
|
|
9
|
+
"content": "\"use client\";\n\nimport { cn } from \"../../lib/utils\";\nimport { Header } from \"./header\";\nimport { useCSSVariableSync } from \"../../hooks/use-css-variable-sync\";\nimport { FlairBanner } from \"../blocks/flair-banner\";\nimport { PageHeaderSection } from \"../blocks/page-header-section\";\nimport { ProgressBar } from \"../blocks/progress-bar\";\nimport { Button } from \"../ui/button\";\nimport { Typography } from \"../ui/typography\";\n\nexport interface MultistepProgressBarStep {\n id: string;\n title: string;\n description?: string;\n}\n\nexport interface MultistepProgressBarShellProps {\n /** Flair banner title */\n bannerTitle?: string;\n /** Flair banner description */\n bannerDescription?: string;\n /** Page title shown below the flair banner */\n pageTitle?: string;\n /** Page description shown below the flair banner */\n pageDescription?: string;\n /** Array of step objects */\n steps?: MultistepProgressBarStep[];\n /** Current active step (0-indexed) */\n currentStep?: number;\n /** Main content */\n children: React.ReactNode;\n /** Callback when Cancel is clicked */\n onCancel?: () => void;\n /** Callback when Continue is clicked */\n onContinue?: () => void;\n /** Cancel button text */\n cancelText?: string;\n /** Continue button text */\n continueText?: string;\n /** Whether Continue button is disabled */\n continueDisabled?: boolean;\n /** Callback when app menu (hamburger) is clicked */\n onAppMenuClick?: () => void;\n /** Additional class name for the content area */\n contentClassName?: string;\n}\n\n/** Default steps for demo/placeholder purposes */\nexport const defaultProgressBarSteps: MultistepProgressBarStep[] = [\n { id: \"step-1\", title: \"Step 1\", description: \"Enter your basic information\" },\n { id: \"step-2\", title: \"Step 2\", description: \"Provide additional details\" },\n { id: \"step-3\", title: \"Step 3\", description: \"Review and submit\" },\n];\n\n/**\n * Canvas Design System - Multistep Progress Bar Shell\n * \n * A layout for multi-step processes/wizards featuring:\n * - Fixed header with logo\n * - Page title and description section\n * - Horizontal progress bar showing completion\n * - Step-specific title and description\n * - Centered content area (max-width 992px)\n * - Navigation buttons (Cancel, Continue)\n * \n * @example\n * ```tsx\n * <MultistepProgressBarShell\n * pageTitle=\"Page title\"\n * pageDescription=\"Description\"\n * steps={[\n * { id: \"step1\", title: \"Step 1\", description: \"First step\" },\n * { id: \"step2\", title: \"Step 2\", description: \"Second step\" },\n * ]}\n * currentStep={0}\n * onCancel={() => router.back()}\n * onContinue={() => setStep(step + 1)}\n * >\n * <ContentDropzone />\n * </MultistepProgressBarShell>\n * ```\n */\nexport function MultistepProgressBarShell({\n bannerTitle = \"Large title\",\n bannerDescription = \"Description\",\n pageTitle = \"Page title\",\n pageDescription = \"Description\",\n steps = defaultProgressBarSteps,\n currentStep = 0,\n children,\n onCancel,\n onContinue,\n cancelText = \"Cancel\",\n continueText = \"Continue\",\n continueDisabled = false,\n onAppMenuClick,\n contentClassName,\n}: MultistepProgressBarShellProps) {\n useCSSVariableSync();\n\n const handleAppMenuClick = () => {\n onAppMenuClick?.();\n console.log(\"App menu clicked - implement app-level mobile menu here\");\n };\n\n const currentStepData = steps[currentStep] || { title: `Step ${currentStep + 1}`, description: \"Description\" };\n const totalSteps = steps.length;\n\n return (\n <div className=\"min-h-screen bg-[var(--canvas-background)]\">\n {/* Header - Fixed at top with logo visible */}\n <header className=\"sticky top-0 z-40\">\n <Header onMenuClick={handleAppMenuClick} showDesktopLogo />\n </header>\n\n {/* Flair Banner */}\n <FlairBanner title={bannerTitle} />\n\n {/* Page Header Section */}\n <PageHeaderSection \n title={pageTitle}\n description={pageDescription}\n showTabs={false}\n />\n\n {/* Content Section */}\n <div className=\"w-full\">\n <div \n className={cn(\n \"w-full max-w-[992px] mx-auto\",\n \"px-[var(--spacing-xl)] lg:px-0\",\n \"py-[var(--spacing-5xl)]\",\n contentClassName\n )}\n >\n {/* Progress Bar */}\n <ProgressBar \n currentStep={currentStep}\n totalSteps={totalSteps}\n />\n\n {/* Step Content Container */}\n <div className=\"flex flex-col gap-[var(--spacing-3xl)] pt-[var(--spacing-5xl)]\">\n {/* Step Title and Description */}\n <div className=\"flex flex-col gap-1\">\n <Typography variant=\"h6\" as=\"h3\">\n {currentStepData.title}\n </Typography>\n {currentStepData.description && (\n <Typography variant=\"body-m\" color=\"muted\">\n {currentStepData.description}\n </Typography>\n )}\n </div>\n\n {/* Main Content Area */}\n <div className=\"w-full\">\n {children}\n </div>\n\n {/* Navigation Buttons */}\n <div className=\"flex gap-[var(--spacing-3xl)] items-center justify-end\">\n <Button variant=\"neutral\" onClick={onCancel}>\n {cancelText}\n </Button>\n <Button variant=\"primary\" onClick={onContinue} disabled={continueDisabled}>\n {continueText}\n </Button>\n </div>\n </div>\n </div>\n </div>\n </div>\n );\n}\n\n"
|
|
10
10
|
}
|
|
11
11
|
],
|
|
12
12
|
"dependencies": [],
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
{
|
|
7
7
|
"path": "components/layout/multistep-shell.tsx",
|
|
8
8
|
"type": "registry:layout",
|
|
9
|
-
"content": "\"use client\";\n\nimport { cn } from \"../../lib/utils\";\nimport { Header } from \"./header\";\nimport { useCSSVariableSync } from \"../../hooks/use-css-variable-sync\";\nimport { StepTracker, Step, defaultSteps } from \"../blocks/step-tracker\";\nimport { Button } from \"../ui/button\";\nimport { Typography } from \"../ui/typography\";\n\ninterface MultistepShellProps {\n /** Array of step objects */\n steps?: Step[];\n /** Current active step (0-indexed) */\n currentStep?: number;\n /** Callback when a step is clicked */\n onStepClick?: (stepIndex: number) => void;\n /** Main content */\n children: React.ReactNode;\n /** Callback when Cancel/Back is clicked */\n onCancel?: () => void;\n /** Callback when Continue/Submit is clicked */\n onContinue?: () => void;\n /** Cancel button text */\n cancelText?: string;\n /** Continue button text */\n continueText?: string;\n /** Whether Continue button is disabled */\n continueDisabled?: boolean;\n /** Callback when app menu (hamburger) is clicked */\n onAppMenuClick?: () => void;\n /** Additional class name for the main content area */\n contentClassName?: string;\n}\n\n/**\n * Canvas Design System - Multistep Shell\n * \n * A layout for multi-step processes/wizards with:\n * - Fixed header with logo\n * - Horizontal step tracker below header\n * - Step title and description\n * - Centered content area (max-width 768px)\n * - Navigation buttons (Cancel, Continue)\n * \n * @example\n * ```tsx\n * <MultistepShell\n * steps={[\n * { id: \"step1\", label: \"Step 1\", description: \"First step\" },\n * { id: \"step2\", label: \"Step 2\", description: \"Second step\" },\n * ]}\n * currentStep={0}\n * onCancel={() => router.back()}\n * onContinue={() => setStep(step + 1)}\n * >\n * <ContentDropzone />\n * </MultistepShell>\n * ```\n */\nexport function MultistepShell({\n steps = defaultSteps,\n currentStep = 0,\n onStepClick,\n children,\n onCancel,\n onContinue,\n cancelText,\n continueText,\n continueDisabled = false,\n onAppMenuClick,\n contentClassName,\n}: MultistepShellProps) {\n useCSSVariableSync();\n\n const handleAppMenuClick = () => {\n onAppMenuClick?.();\n console.log(\"App menu clicked - implement app-level mobile menu here\");\n };\n\n const currentStepData = steps[currentStep];\n const isFirstStep = currentStep === 0;\n const isLastStep = currentStep === steps.length - 1;\n\n return (\n <div className=\"min-h-screen bg-[var(--background)]\">\n {/* Header - Fixed at top with logo visible */}\n <header className=\"sticky top-0 z-40\">\n <Header onMenuClick={handleAppMenuClick} showDesktopLogo />\n </header>\n\n {/* Step Tracker Section */}\n <div className=\"w-full\">\n <StepTracker\n steps={steps}\n currentStep={currentStep}\n onStepClick={onStepClick}\n />\n </div>\n\n {/* Main Content Area - Centered */}\n <main className=\"w-full\">\n <div \n className={cn(\n \"w-full max-w-[768px] mx-auto\",\n \"px-[var(--spacing-xl)] lg:px-0\",\n \"py-[var(--spacing-5xl)]\",\n contentClassName\n )}\n >\n {/* Step Content Container */}\n <div className=\"flex flex-col gap-[var(--spacing-3xl)]\">\n {/* Step Title and Description */}\n <div className=\"flex flex-col gap-1\">\n <Typography variant=\"h6\" as=\"h3\">\n {currentStepData?.label || `Step ${currentStep + 1}`}\n </Typography>\n {currentStepData?.description && (\n <Typography variant=\"body-m\" color=\"muted\">\n {currentStepData.description}\n </Typography>\n )}\n </div>\n\n {/* Main Content Area */}\n <div className=\"w-full\">\n {children}\n </div>\n\n {/* Navigation Buttons */}\n <div className=\"flex gap-[var(--spacing-3xl)] items-center justify-end\">\n <Button variant=\"neutral\" onClick={onCancel}>\n {cancelText || (isFirstStep ? \"Cancel\" : \"Back\")}\n </Button>\n <Button variant=\"primary\" onClick={onContinue} disabled={continueDisabled}>\n {continueText || (isLastStep ? \"Submit\" : \"Continue\")}\n </Button>\n </div>\n </div>\n </div>\n </main>\n </div>\n );\n}\n"
|
|
9
|
+
"content": "\"use client\";\n\nimport { cn } from \"../../lib/utils\";\nimport { Header } from \"./header\";\nimport { useCSSVariableSync } from \"../../hooks/use-css-variable-sync\";\nimport { StepTracker, Step, defaultSteps } from \"../blocks/step-tracker\";\nimport { Button } from \"../ui/button\";\nimport { Typography } from \"../ui/typography\";\n\ninterface MultistepShellProps {\n /** Array of step objects */\n steps?: Step[];\n /** Current active step (0-indexed) */\n currentStep?: number;\n /** Callback when a step is clicked */\n onStepClick?: (stepIndex: number) => void;\n /** Main content */\n children: React.ReactNode;\n /** Callback when Cancel/Back is clicked */\n onCancel?: () => void;\n /** Callback when Continue/Submit is clicked */\n onContinue?: () => void;\n /** Cancel button text */\n cancelText?: string;\n /** Continue button text */\n continueText?: string;\n /** Whether Continue button is disabled */\n continueDisabled?: boolean;\n /** Callback when app menu (hamburger) is clicked */\n onAppMenuClick?: () => void;\n /** Additional class name for the main content area */\n contentClassName?: string;\n}\n\n/**\n * Canvas Design System - Multistep Shell\n * \n * A layout for multi-step processes/wizards with:\n * - Fixed header with logo\n * - Horizontal step tracker below header\n * - Step title and description\n * - Centered content area (max-width 768px)\n * - Navigation buttons (Cancel, Continue)\n * \n * @example\n * ```tsx\n * <MultistepShell\n * steps={[\n * { id: \"step1\", label: \"Step 1\", description: \"First step\" },\n * { id: \"step2\", label: \"Step 2\", description: \"Second step\" },\n * ]}\n * currentStep={0}\n * onCancel={() => router.back()}\n * onContinue={() => setStep(step + 1)}\n * >\n * <ContentDropzone />\n * </MultistepShell>\n * ```\n */\nexport function MultistepShell({\n steps = defaultSteps,\n currentStep = 0,\n onStepClick,\n children,\n onCancel,\n onContinue,\n cancelText,\n continueText,\n continueDisabled = false,\n onAppMenuClick,\n contentClassName,\n}: MultistepShellProps) {\n useCSSVariableSync();\n\n const handleAppMenuClick = () => {\n onAppMenuClick?.();\n console.log(\"App menu clicked - implement app-level mobile menu here\");\n };\n\n const currentStepData = steps[currentStep];\n const isFirstStep = currentStep === 0;\n const isLastStep = currentStep === steps.length - 1;\n\n return (\n <div className=\"min-h-screen bg-[var(--canvas-background)]\">\n {/* Header - Fixed at top with logo visible */}\n <header className=\"sticky top-0 z-40\">\n <Header onMenuClick={handleAppMenuClick} showDesktopLogo />\n </header>\n\n {/* Step Tracker Section */}\n <div className=\"w-full\">\n <StepTracker\n steps={steps}\n currentStep={currentStep}\n onStepClick={onStepClick}\n />\n </div>\n\n {/* Main Content Area - Centered */}\n <main className=\"w-full\">\n <div \n className={cn(\n \"w-full max-w-[768px] mx-auto\",\n \"px-[var(--spacing-xl)] lg:px-0\",\n \"py-[var(--spacing-5xl)]\",\n contentClassName\n )}\n >\n {/* Step Content Container */}\n <div className=\"flex flex-col gap-[var(--spacing-3xl)]\">\n {/* Step Title and Description */}\n <div className=\"flex flex-col gap-1\">\n <Typography variant=\"h6\" as=\"h3\">\n {currentStepData?.label || `Step ${currentStep + 1}`}\n </Typography>\n {currentStepData?.description && (\n <Typography variant=\"body-m\" color=\"muted\">\n {currentStepData.description}\n </Typography>\n )}\n </div>\n\n {/* Main Content Area */}\n <div className=\"w-full\">\n {children}\n </div>\n\n {/* Navigation Buttons */}\n <div className=\"flex gap-[var(--spacing-3xl)] items-center justify-end\">\n <Button variant=\"neutral\" onClick={onCancel}>\n {cancelText || (isFirstStep ? \"Cancel\" : \"Back\")}\n </Button>\n <Button variant=\"primary\" onClick={onContinue} disabled={continueDisabled}>\n {continueText || (isLastStep ? \"Submit\" : \"Continue\")}\n </Button>\n </div>\n </div>\n </div>\n </main>\n </div>\n );\n}\n"
|
|
10
10
|
}
|
|
11
11
|
],
|
|
12
12
|
"dependencies": [],
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
{
|
|
7
7
|
"path": "components/layout/multistep-sidebar-shell.tsx",
|
|
8
8
|
"type": "registry:layout",
|
|
9
|
-
"content": "\"use client\";\n\nimport { cn } from \"../../lib/utils\";\nimport { Header } from \"./header\";\nimport { useCSSVariableSync } from \"../../hooks/use-css-variable-sync\";\nimport { StepTracker, Step, defaultSteps } from \"../blocks/step-tracker\";\nimport { InfoCard, LinksCard, LinkItem, defaultSupportLinks } from \"../blocks/sidebar-cards\";\nimport { Button } from \"../ui/button\";\nimport { Typography } from \"../ui/typography\";\n\ninterface MultistepSidebarShellProps {\n /** Array of step objects */\n steps?: Step[];\n /** Current active step (0-indexed) */\n currentStep?: number;\n /** Callback when a step is clicked */\n onStepClick?: (stepIndex: number) => void;\n /** Info card title */\n infoTitle?: string;\n /** Info card description */\n infoDescription?: string;\n /** Links card title */\n linksTitle?: string;\n /** Links card items */\n links?: LinkItem[];\n /** Main content */\n children: React.ReactNode;\n /** Callback when Cancel/Back is clicked */\n onCancel?: () => void;\n /** Callback when Continue/Submit is clicked */\n onContinue?: () => void;\n /** Cancel button text */\n cancelText?: string;\n /** Continue button text */\n continueText?: string;\n /** Whether Continue button is disabled */\n continueDisabled?: boolean;\n /** Callback when app menu (hamburger) is clicked */\n onAppMenuClick?: () => void;\n /** Additional class name for the main content area */\n contentClassName?: string;\n}\n\n/**\n * Canvas Design System - Multistep Sidebar Shell\n * \n * A layout for multi-step processes with a sidebar:\n * - Fixed header with logo\n * - Horizontal step tracker below header\n * - Step title and description\n * - Two-column layout:\n * - Left: Main content with navigation buttons\n * - Right: Sidebar (320px) with Info and Links cards\n * \n * @example\n * ```tsx\n * <MultistepSidebarShell\n * steps={defaultSteps}\n * currentStep={0}\n * onCancel={() => router.back()}\n * onContinue={() => setStep(step + 1)}\n * >\n * <ContentDropzone />\n * </MultistepSidebarShell>\n * ```\n */\nexport function MultistepSidebarShell({\n steps = defaultSteps,\n currentStep = 0,\n onStepClick,\n infoTitle,\n infoDescription,\n linksTitle,\n links = defaultSupportLinks,\n children,\n onCancel,\n onContinue,\n cancelText,\n continueText,\n continueDisabled = false,\n onAppMenuClick,\n contentClassName,\n}: MultistepSidebarShellProps) {\n useCSSVariableSync();\n\n const handleAppMenuClick = () => {\n onAppMenuClick?.();\n console.log(\"App menu clicked - implement app-level mobile menu here\");\n };\n\n const currentStepData = steps[currentStep];\n const isFirstStep = currentStep === 0;\n const isLastStep = currentStep === steps.length - 1;\n\n return (\n <div className=\"min-h-screen bg-[var(--background)]\">\n {/* Header - Fixed at top with logo visible */}\n <header className=\"sticky top-0 z-40\">\n <Header onMenuClick={handleAppMenuClick} showDesktopLogo />\n </header>\n\n {/* Step Tracker Section */}\n <div className=\"w-full\">\n <StepTracker\n steps={steps}\n currentStep={currentStep}\n onStepClick={onStepClick}\n />\n </div>\n\n {/* Main Content Area - Two Column Layout */}\n <main className=\"w-full\">\n <div \n className={cn(\n \"w-full max-w-[1200px] mx-auto\",\n \"px-[var(--spacing-xl)] lg:px-0\",\n \"py-10\",\n \"flex gap-10\"\n )}\n >\n {/* Left: Main Content */}\n <div className={cn(\"flex-1 min-w-0\", contentClassName)}>\n {/* Step Content Container */}\n <div className=\"flex flex-col gap-[var(--spacing-3xl)]\">\n {/* Step Title and Description */}\n <div className=\"flex flex-col gap-1\">\n <Typography variant=\"h6\" as=\"h3\">\n {currentStepData?.label || `Step ${currentStep + 1}`}\n </Typography>\n {currentStepData?.description && (\n <Typography variant=\"body-m\" color=\"muted\">\n {currentStepData.description}\n </Typography>\n )}\n </div>\n\n {/* Main Content Area */}\n <div className=\"w-full\">\n {children}\n </div>\n\n {/* Navigation Buttons */}\n <div className=\"flex gap-[var(--spacing-3xl)] items-center justify-end\">\n <Button variant=\"neutral\" onClick={onCancel}>\n {cancelText || (isFirstStep ? \"Cancel\" : \"Back\")}\n </Button>\n <Button variant=\"primary\" onClick={onContinue} disabled={continueDisabled}>\n {continueText || (isLastStep ? \"Submit\" : \"Continue\")}\n </Button>\n </div>\n </div>\n </div>\n\n {/* Right: Sidebar */}\n <aside className=\"hidden lg:flex flex-col gap-10 shrink-0\">\n <InfoCard \n title={infoTitle} \n description={infoDescription} \n />\n <LinksCard \n title={linksTitle} \n links={links} \n />\n </aside>\n </div>\n </main>\n </div>\n );\n}\n"
|
|
9
|
+
"content": "\"use client\";\n\nimport { cn } from \"../../lib/utils\";\nimport { Header } from \"./header\";\nimport { useCSSVariableSync } from \"../../hooks/use-css-variable-sync\";\nimport { StepTracker, Step, defaultSteps } from \"../blocks/step-tracker\";\nimport { InfoCard, LinksCard, LinkItem, defaultSupportLinks } from \"../blocks/sidebar-cards\";\nimport { Button } from \"../ui/button\";\nimport { Typography } from \"../ui/typography\";\n\ninterface MultistepSidebarShellProps {\n /** Array of step objects */\n steps?: Step[];\n /** Current active step (0-indexed) */\n currentStep?: number;\n /** Callback when a step is clicked */\n onStepClick?: (stepIndex: number) => void;\n /** Info card title */\n infoTitle?: string;\n /** Info card description */\n infoDescription?: string;\n /** Links card title */\n linksTitle?: string;\n /** Links card items */\n links?: LinkItem[];\n /** Main content */\n children: React.ReactNode;\n /** Callback when Cancel/Back is clicked */\n onCancel?: () => void;\n /** Callback when Continue/Submit is clicked */\n onContinue?: () => void;\n /** Cancel button text */\n cancelText?: string;\n /** Continue button text */\n continueText?: string;\n /** Whether Continue button is disabled */\n continueDisabled?: boolean;\n /** Callback when app menu (hamburger) is clicked */\n onAppMenuClick?: () => void;\n /** Additional class name for the main content area */\n contentClassName?: string;\n}\n\n/**\n * Canvas Design System - Multistep Sidebar Shell\n * \n * A layout for multi-step processes with a sidebar:\n * - Fixed header with logo\n * - Horizontal step tracker below header\n * - Step title and description\n * - Two-column layout:\n * - Left: Main content with navigation buttons\n * - Right: Sidebar (320px) with Info and Links cards\n * \n * @example\n * ```tsx\n * <MultistepSidebarShell\n * steps={defaultSteps}\n * currentStep={0}\n * onCancel={() => router.back()}\n * onContinue={() => setStep(step + 1)}\n * >\n * <ContentDropzone />\n * </MultistepSidebarShell>\n * ```\n */\nexport function MultistepSidebarShell({\n steps = defaultSteps,\n currentStep = 0,\n onStepClick,\n infoTitle,\n infoDescription,\n linksTitle,\n links = defaultSupportLinks,\n children,\n onCancel,\n onContinue,\n cancelText,\n continueText,\n continueDisabled = false,\n onAppMenuClick,\n contentClassName,\n}: MultistepSidebarShellProps) {\n useCSSVariableSync();\n\n const handleAppMenuClick = () => {\n onAppMenuClick?.();\n console.log(\"App menu clicked - implement app-level mobile menu here\");\n };\n\n const currentStepData = steps[currentStep];\n const isFirstStep = currentStep === 0;\n const isLastStep = currentStep === steps.length - 1;\n\n return (\n <div className=\"min-h-screen bg-[var(--canvas-background)]\">\n {/* Header - Fixed at top with logo visible */}\n <header className=\"sticky top-0 z-40\">\n <Header onMenuClick={handleAppMenuClick} showDesktopLogo />\n </header>\n\n {/* Step Tracker Section */}\n <div className=\"w-full\">\n <StepTracker\n steps={steps}\n currentStep={currentStep}\n onStepClick={onStepClick}\n />\n </div>\n\n {/* Main Content Area - Two Column Layout */}\n <main className=\"w-full\">\n <div \n className={cn(\n \"w-full max-w-[1200px] mx-auto\",\n \"px-[var(--spacing-xl)] lg:px-0\",\n \"py-10\",\n \"flex gap-10\"\n )}\n >\n {/* Left: Main Content */}\n <div className={cn(\"flex-1 min-w-0\", contentClassName)}>\n {/* Step Content Container */}\n <div className=\"flex flex-col gap-[var(--spacing-3xl)]\">\n {/* Step Title and Description */}\n <div className=\"flex flex-col gap-1\">\n <Typography variant=\"h6\" as=\"h3\">\n {currentStepData?.label || `Step ${currentStep + 1}`}\n </Typography>\n {currentStepData?.description && (\n <Typography variant=\"body-m\" color=\"muted\">\n {currentStepData.description}\n </Typography>\n )}\n </div>\n\n {/* Main Content Area */}\n <div className=\"w-full\">\n {children}\n </div>\n\n {/* Navigation Buttons */}\n <div className=\"flex gap-[var(--spacing-3xl)] items-center justify-end\">\n <Button variant=\"neutral\" onClick={onCancel}>\n {cancelText || (isFirstStep ? \"Cancel\" : \"Back\")}\n </Button>\n <Button variant=\"primary\" onClick={onContinue} disabled={continueDisabled}>\n {continueText || (isLastStep ? \"Submit\" : \"Continue\")}\n </Button>\n </div>\n </div>\n </div>\n\n {/* Right: Sidebar */}\n <aside className=\"hidden lg:flex flex-col gap-10 shrink-0\">\n <InfoCard \n title={infoTitle} \n description={infoDescription} \n />\n <LinksCard \n title={linksTitle} \n links={links} \n />\n </aside>\n </div>\n </main>\n </div>\n );\n}\n"
|
|
10
10
|
}
|
|
11
11
|
],
|
|
12
12
|
"dependencies": [],
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
{
|
|
7
7
|
"path": "components/layout/project-context-shell.tsx",
|
|
8
8
|
"type": "registry:layout",
|
|
9
|
-
"content": "\"use client\";\n\nimport { ReactNode } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport { ScrollArea } from \"../ui/scroll-area\";\nimport {\n FileText,\n Users,\n LayoutGrid,\n Wand2,\n} from \"lucide-react\";\nimport { useThemeImages, useThemeBranding } from \"../../context/theme-context\";\nimport {\n Diamond,\n Hexagon,\n Star,\n Lightning,\n Sparkle,\n Infinity,\n Code,\n Terminal,\n Cpu,\n Database,\n Globe,\n Cloud,\n WifiHigh,\n Briefcase,\n Buildings,\n Storefront,\n Handshake,\n ChartLine,\n Palette as PaletteIcon,\n PencilSimple,\n Camera,\n MusicNote,\n Lightbulb,\n Leaf,\n Tree,\n Sun,\n Moon,\n Fire,\n Drop,\n ChatCircle,\n Envelope,\n Phone,\n Megaphone,\n Heart,\n Shield,\n Trophy,\n Rocket,\n Target,\n Flag,\n type Icon as PhosphorIcon,\n} from \"@phosphor-icons/react\";\n\n// ═══════════════════════════════════════════════════════════\n// LOGO ICON SHAPES\n// ═══════════════════════════════════════════════════════════\n\ntype IconShapeId = \"rounded\" | \"circle\" | \"square\";\n\nconst iconShapes: { id: IconShapeId; renderBackground: (bgColor: string) => React.ReactNode }[] = [\n {\n id: \"rounded\",\n renderBackground: (bgColor: string) => (\n <svg viewBox=\"0 0 32 32\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\" className=\"size-full absolute inset-0\">\n <rect width=\"32\" height=\"32\" rx=\"10\" style={{ fill: bgColor }} />\n </svg>\n ),\n },\n {\n id: \"circle\",\n renderBackground: (bgColor: string) => (\n <svg viewBox=\"0 0 32 32\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\" className=\"size-full absolute inset-0\">\n <circle cx=\"16\" cy=\"16\" r=\"16\" style={{ fill: bgColor }} />\n </svg>\n ),\n },\n {\n id: \"square\",\n renderBackground: (bgColor: string) => (\n <svg viewBox=\"0 0 32 32\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\" className=\"size-full absolute inset-0\">\n <rect width=\"32\" height=\"32\" style={{ fill: bgColor }} />\n </svg>\n ),\n },\n];\n\nconst iconMap: Record<string, PhosphorIcon> = {\n Diamond, Hexagon, Star, Lightning, Sparkle, Infinity, Code, Terminal, Cpu, Database,\n Globe, Cloud, WifiHigh, Briefcase, Buildings, Storefront, Handshake, ChartLine,\n Palette: PaletteIcon, PencilSimple, Camera, MusicNote, Lightbulb, Leaf, Tree,\n Sun, Moon, Fire, Drop, ChatCircle, Envelope, Phone, Megaphone, Heart, Shield,\n Trophy, Rocket, Target, Flag,\n};\n\nfunction resolveBrandingColor(value: string): string {\n if (!value) return \"#ffffff\";\n if (value.startsWith(\"var(\")) {\n const varName = value.replace(\"var(\", \"\").replace(\")\", \"\");\n if (typeof window !== \"undefined\") {\n const computed = getComputedStyle(document.documentElement).getPropertyValue(varName).trim();\n return computed || \"#ffffff\";\n }\n return \"#ffffff\";\n }\n return value;\n}\n\n// ═══════════════════════════════════════════════════════════\n// TAB TYPES\n// ═══════════════════════════════════════════════════════════\n\nexport type ProjectContextTab = \"scope\" | \"personas\" | \"screens\" | \"prompts\";\n\ninterface TabConfig {\n id: ProjectContextTab;\n label: string;\n icon: typeof FileText;\n description: string;\n}\n\nconst tabs: TabConfig[] = [\n {\n id: \"scope\",\n label: \"Scope\",\n icon: FileText,\n description: \"Upload your project scope document\",\n },\n {\n id: \"personas\",\n label: \"Personas\",\n icon: Users,\n description: \"Define who you're building for\",\n },\n {\n id: \"screens\",\n label: \"Screens\",\n icon: LayoutGrid,\n description: \"Map out your product's screens and flows\",\n },\n {\n id: \"prompts\",\n label: \"Prompt Helpers\",\n icon: Wand2,\n description: \"Build prompts with existing components\",\n },\n];\n\n// ═══════════════════════════════════════════════════════════\n// SHELL COMPONENT\n// ═══════════════════════════════════════════════════════════\n\ninterface ProjectContextShellProps {\n children: ReactNode;\n activeTab: ProjectContextTab;\n onTabChange: (tab: ProjectContextTab) => void;\n}\n\nexport function ProjectContextShell({\n children,\n activeTab,\n onTabChange,\n}: ProjectContextShellProps) {\n const activeTabConfig = tabs.find((t) => t.id === activeTab);\n const themeImages = useThemeImages();\n const { branding, isMounted } = useThemeBranding();\n \n // Get logo (use light variant for this light sidebar)\n const logoUrl = themeImages.logoLight;\n const iconShape = iconShapes.find(s => s.id === branding.iconShape) || iconShapes[0];\n\n return (\n <div className=\"min-h-screen flex bg-[var(--canvas-background)]\">\n {/* Sidebar */}\n <aside className=\"w-64 border-r border-[var(--canvas-border)] bg-[var(--canvas-background)] flex flex-col shrink-0\">\n {/* Logo + Title Header - matches main header height */}\n <div className=\"px-4 border-b border-[var(--canvas-border)] flex items-center h-[97px]\">\n {/* Logo */}\n <div className={`flex items-center ${isMounted ? 'opacity-100' : 'opacity-0'}`}>\n {logoUrl ? (\n <img \n src={logoUrl} \n alt=\"Logo\" \n className=\"h-8 w-auto object-contain\"\n />\n ) : (\n // Uses CSS variables directly - no JavaScript resolution needed\n <div className=\"flex items-center\">\n <div className=\"relative size-8 shrink-0\">\n {iconShape.renderBackground(branding.bgColor || \"var(--canvas-primary)\")}\n <div className=\"absolute inset-0 flex items-center justify-center z-10\">\n {(() => {\n const IconComponent = iconMap[branding.iconName || \"Buildings\"] || Buildings;\n return <IconComponent weight=\"bold\" size={18} color={branding.iconColor || \"var(--canvas-primary-foreground)\"} />;\n })()}\n </div>\n </div>\n <span className=\"
|
|
9
|
+
"content": "\"use client\";\n\nimport { ReactNode } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport { ScrollArea } from \"../ui/scroll-area\";\nimport {\n FileText,\n Users,\n LayoutGrid,\n Wand2,\n} from \"lucide-react\";\nimport { useThemeImages, useThemeBranding } from \"../../context/theme-context\";\nimport {\n Diamond,\n Hexagon,\n Star,\n Lightning,\n Sparkle,\n Infinity,\n Code,\n Terminal,\n Cpu,\n Database,\n Globe,\n Cloud,\n WifiHigh,\n Briefcase,\n Buildings,\n Storefront,\n Handshake,\n ChartLine,\n Palette as PaletteIcon,\n PencilSimple,\n Camera,\n MusicNote,\n Lightbulb,\n Leaf,\n Tree,\n Sun,\n Moon,\n Fire,\n Drop,\n ChatCircle,\n Envelope,\n Phone,\n Megaphone,\n Heart,\n Shield,\n Trophy,\n Rocket,\n Target,\n Flag,\n type Icon as PhosphorIcon,\n} from \"@phosphor-icons/react\";\n\n// ═══════════════════════════════════════════════════════════\n// LOGO ICON SHAPES\n// ═══════════════════════════════════════════════════════════\n\ntype IconShapeId = \"rounded\" | \"circle\" | \"square\";\n\nconst iconShapes: { id: IconShapeId; renderBackground: (bgColor: string) => React.ReactNode }[] = [\n {\n id: \"rounded\",\n renderBackground: (bgColor: string) => (\n <svg viewBox=\"0 0 32 32\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\" className=\"size-full absolute inset-0\">\n <rect width=\"32\" height=\"32\" rx=\"10\" style={{ fill: bgColor }} />\n </svg>\n ),\n },\n {\n id: \"circle\",\n renderBackground: (bgColor: string) => (\n <svg viewBox=\"0 0 32 32\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\" className=\"size-full absolute inset-0\">\n <circle cx=\"16\" cy=\"16\" r=\"16\" style={{ fill: bgColor }} />\n </svg>\n ),\n },\n {\n id: \"square\",\n renderBackground: (bgColor: string) => (\n <svg viewBox=\"0 0 32 32\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\" className=\"size-full absolute inset-0\">\n <rect width=\"32\" height=\"32\" style={{ fill: bgColor }} />\n </svg>\n ),\n },\n];\n\nconst iconMap: Record<string, PhosphorIcon> = {\n Diamond, Hexagon, Star, Lightning, Sparkle, Infinity, Code, Terminal, Cpu, Database,\n Globe, Cloud, WifiHigh, Briefcase, Buildings, Storefront, Handshake, ChartLine,\n Palette: PaletteIcon, PencilSimple, Camera, MusicNote, Lightbulb, Leaf, Tree,\n Sun, Moon, Fire, Drop, ChatCircle, Envelope, Phone, Megaphone, Heart, Shield,\n Trophy, Rocket, Target, Flag,\n};\n\nfunction resolveBrandingColor(value: string): string {\n if (!value) return \"#ffffff\";\n if (value.startsWith(\"var(\")) {\n const varName = value.replace(\"var(\", \"\").replace(\")\", \"\");\n if (typeof window !== \"undefined\") {\n const computed = getComputedStyle(document.documentElement).getPropertyValue(varName).trim();\n return computed || \"#ffffff\";\n }\n return \"#ffffff\";\n }\n return value;\n}\n\n// ═══════════════════════════════════════════════════════════\n// TAB TYPES\n// ═══════════════════════════════════════════════════════════\n\nexport type ProjectContextTab = \"scope\" | \"personas\" | \"screens\" | \"prompts\";\n\ninterface TabConfig {\n id: ProjectContextTab;\n label: string;\n icon: typeof FileText;\n description: string;\n}\n\nconst tabs: TabConfig[] = [\n {\n id: \"scope\",\n label: \"Scope\",\n icon: FileText,\n description: \"Upload your project scope document\",\n },\n {\n id: \"personas\",\n label: \"Personas\",\n icon: Users,\n description: \"Define who you're building for\",\n },\n {\n id: \"screens\",\n label: \"Screens\",\n icon: LayoutGrid,\n description: \"Map out your product's screens and flows\",\n },\n {\n id: \"prompts\",\n label: \"Prompt Helpers\",\n icon: Wand2,\n description: \"Build prompts with existing components\",\n },\n];\n\n// ═══════════════════════════════════════════════════════════\n// SHELL COMPONENT\n// ═══════════════════════════════════════════════════════════\n\ninterface ProjectContextShellProps {\n children: ReactNode;\n activeTab: ProjectContextTab;\n onTabChange: (tab: ProjectContextTab) => void;\n}\n\nexport function ProjectContextShell({\n children,\n activeTab,\n onTabChange,\n}: ProjectContextShellProps) {\n const activeTabConfig = tabs.find((t) => t.id === activeTab);\n const themeImages = useThemeImages();\n const { branding, isMounted } = useThemeBranding();\n \n // Get logo (use light variant for this light sidebar)\n const logoUrl = themeImages.logoLight;\n const iconShape = iconShapes.find(s => s.id === branding.iconShape) || iconShapes[0];\n\n return (\n <div className=\"min-h-screen flex bg-[var(--canvas-background)]\">\n {/* Sidebar */}\n <aside className=\"w-64 border-r border-[var(--canvas-border)] bg-[var(--canvas-background)] flex flex-col shrink-0\">\n {/* Logo + Title Header - matches main header height */}\n <div className=\"px-4 border-b border-[var(--canvas-border)] flex items-center h-[97px]\">\n {/* Logo */}\n <div className={`flex items-center ${isMounted ? 'opacity-100' : 'opacity-0'}`}>\n {logoUrl ? (\n <img \n src={logoUrl} \n alt=\"Logo\" \n className=\"h-8 w-auto object-contain\"\n />\n ) : (\n // Uses CSS variables directly - no JavaScript resolution needed\n <div className=\"flex items-center\">\n <div className=\"relative size-8 shrink-0\">\n {iconShape.renderBackground(branding.bgColor || \"var(--canvas-primary)\")}\n <div className=\"absolute inset-0 flex items-center justify-center z-10\">\n {(() => {\n const IconComponent = iconMap[branding.iconName || \"Buildings\"] || Buildings;\n return <IconComponent weight=\"bold\" size={18} color={branding.iconColor || \"var(--canvas-primary-foreground)\"} />;\n })()}\n </div>\n </div>\n <span className=\"font-semibold ml-2.5 text-[var(--canvas-text)]\" style={{ fontSize: \"var(--typo-body-xl-size)\" }}>\n {branding.wordmark || \"canvas\"}\n </span>\n </div>\n )}\n </div>\n </div>\n\n <nav className=\"flex-1 p-2\">\n {tabs.map((tab) => {\n const Icon = tab.icon;\n const isActive = activeTab === tab.id;\n\n return (\n <button\n key={tab.id}\n onClick={() => onTabChange(tab.id)}\n className={cn(\n \"w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-left transition-colors mb-1\",\n isActive\n ? \"bg-[var(--canvas-surface-brand)] text-[var(--canvas-primary)]\"\n : \"text-[var(--canvas-text-muted)] hover:bg-[var(--canvas-surface)] hover:text-[var(--canvas-text)]\"\n )}\n >\n <Icon className=\"size-4 shrink-0\" />\n <span className=\"font-medium\" style={{ fontSize: \"var(--typo-body-s-size)\" }}>{tab.label}</span>\n </button>\n );\n })}\n </nav>\n\n {/* Help section */}\n <div className=\"p-4 border-t border-[var(--canvas-border)]\">\n <div className=\"rounded-lg bg-[var(--canvas-surface)] p-3\">\n <p className=\"text-[var(--canvas-text-muted)] leading-relaxed\" style={{ fontSize: \"var(--typo-body-xs-size)\" }}>\n 💡 Use the prompt templates to generate content with Cursor AI\n </p>\n </div>\n </div>\n </aside>\n\n {/* Content Area */}\n <main className=\"flex-1 flex flex-col min-w-0 overflow-hidden\">\n {/* Tab Header */}\n <div className=\"border-b border-[var(--canvas-border)] px-8 py-6 bg-[var(--canvas-background)]\">\n {activeTabConfig && (\n <div>\n <h2 className=\"font-semibold text-[var(--canvas-text)]\" style={{ fontSize: \"var(--typo-body-xl-size)\" }}>\n {activeTabConfig.label}\n </h2>\n <p className=\"text-[var(--canvas-text-muted)]\" style={{ fontSize: \"var(--typo-body-s-size)\" }}>\n {activeTabConfig.description}\n </p>\n </div>\n )}\n </div>\n\n {/* Scrollable Content */}\n <ScrollArea className=\"flex-1\">\n <div className=\"p-8\">{children}</div>\n </ScrollArea>\n </main>\n </div>\n );\n}\n"
|
|
10
10
|
}
|
|
11
11
|
],
|
|
12
12
|
"dependencies": [
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
{
|
|
7
7
|
"path": "components/layout/search-bar-shell.tsx",
|
|
8
8
|
"type": "registry:layout",
|
|
9
|
-
"content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport { Header } from \"./header\";\nimport { useCSSVariableSync } from \"../../hooks/use-css-variable-sync\";\nimport { FlairBanner } from \"../blocks/flair-banner\";\nimport { PageHeaderSection } from \"../blocks/page-header-section\";\nimport { SearchBar } from \"../blocks/search-bar\";\nimport { FilterPopover, FilterState } from \"../blocks/filter-popover\";\n\ninterface SearchBarShellProps {\n /** Flair banner title */\n bannerTitle?: string;\n /** Flair banner description */\n bannerDescription?: string;\n /** Page title */\n pageTitle?: string;\n /** Page description */\n pageDescription?: string;\n /** Search bar placeholder */\n searchPlaceholder?: string;\n /** Callback when search is triggered */\n onSearch?: (query: string) => void;\n /** Callback when filters are applied */\n onFilterApply?: (filters: FilterState) => void;\n /** Main content */\n children: React.ReactNode;\n /** Additional class name for the main content area */\n contentClassName?: string;\n}\n\n/**\n * Canvas Design System - Search Bar Shell\n * \n * A layout with:\n * - Fixed header with logo (no sidebar)\n * - Page header section with title and description\n * - Search bar with filter button\n * - Content area for search results\n * \n * @example\n * ```tsx\n * <SearchBarShell\n * pageTitle=\"Page title\"\n * pageDescription=\"Description\"\n * onSearch={(query) => console.log(query)}\n * >\n * <ContentDropzone />\n * </SearchBarShell>\n * ```\n */\nexport function SearchBarShell({\n bannerTitle = \"Large title\",\n bannerDescription = \"Description\",\n pageTitle = \"Page title\",\n pageDescription = \"Description\",\n searchPlaceholder = \"Search\",\n onSearch,\n onFilterApply,\n children,\n contentClassName,\n}: SearchBarShellProps) {\n useCSSVariableSync();\n const [searchValue, setSearchValue] = useState(\"\");\n const [filterState, setFilterState] = useState<FilterState>({\n dropdowns: {},\n checkboxes: {},\n dateRange: { start: \"\", end: \"\" },\n });\n\n const handleSearch = () => {\n onSearch?.(searchValue);\n };\n\n const handleFilterApply = (newFilters: FilterState) => {\n setFilterState(newFilters);\n onFilterApply?.(newFilters);\n };\n\n const handleFilterClear = () => {\n const clearedState: FilterState = {\n dropdowns: {},\n checkboxes: {},\n dateRange: { start: \"\", end: \"\" },\n };\n setFilterState(clearedState);\n onFilterApply?.(clearedState);\n };\n\n return (\n <div className=\"min-h-screen bg-[var(--background)]\">\n {/* Header - Fixed at top with logo visible (no sidebar) */}\n <header className=\"sticky top-0 z-40\">\n <Header showDesktopLogo />\n </header>\n\n {/* Flair Banner */}\n <FlairBanner title={bannerTitle} />\n\n {/* Page Header Section - Title and Description only, no tabs */}\n <PageHeaderSection\n title={pageTitle}\n description={pageDescription}\n showTabs={false}\n />\n\n {/* Search Bar Section */}\n <div className=\"w-full\">\n <div\n className={cn(\n \"w-full max-w-[var(--content-max-width)] mx-auto\",\n \"px-[var(--spacing-xl)] lg:px-0\",\n \"pt-10 pb-10\",\n \"border-b border-[var(--canvas-border)]\"\n )}\n >\n <div className=\"flex items-center gap-6\">\n {/* Search Bar */}\n <SearchBar\n placeholder={searchPlaceholder}\n value={searchValue}\n onChange={setSearchValue}\n onSearch={handleSearch}\n className=\"flex-1\"\n />\n\n {/* Filter Button */}\n <FilterPopover\n filterState={filterState}\n onApply={handleFilterApply}\n onClear={handleFilterClear}\n />\n </div>\n </div>\n </div>\n\n {/* Main Content Area */}\n <main className=\"w-full\">\n <div\n className={cn(\n \"w-full max-w-[var(--content-max-width)] mx-auto\",\n \"px-[var(--spacing-xl)] lg:px-0\",\n \"py-[var(--spacing-6xl)]\",\n contentClassName\n )}\n >\n {children}\n </div>\n </main>\n </div>\n );\n}\n\n"
|
|
9
|
+
"content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport { Header } from \"./header\";\nimport { useCSSVariableSync } from \"../../hooks/use-css-variable-sync\";\nimport { FlairBanner } from \"../blocks/flair-banner\";\nimport { PageHeaderSection } from \"../blocks/page-header-section\";\nimport { SearchBar } from \"../blocks/search-bar\";\nimport { FilterPopover, FilterState } from \"../blocks/filter-popover\";\n\ninterface SearchBarShellProps {\n /** Flair banner title */\n bannerTitle?: string;\n /** Flair banner description */\n bannerDescription?: string;\n /** Page title */\n pageTitle?: string;\n /** Page description */\n pageDescription?: string;\n /** Search bar placeholder */\n searchPlaceholder?: string;\n /** Callback when search is triggered */\n onSearch?: (query: string) => void;\n /** Callback when filters are applied */\n onFilterApply?: (filters: FilterState) => void;\n /** Main content */\n children: React.ReactNode;\n /** Additional class name for the main content area */\n contentClassName?: string;\n}\n\n/**\n * Canvas Design System - Search Bar Shell\n * \n * A layout with:\n * - Fixed header with logo (no sidebar)\n * - Page header section with title and description\n * - Search bar with filter button\n * - Content area for search results\n * \n * @example\n * ```tsx\n * <SearchBarShell\n * pageTitle=\"Page title\"\n * pageDescription=\"Description\"\n * onSearch={(query) => console.log(query)}\n * >\n * <ContentDropzone />\n * </SearchBarShell>\n * ```\n */\nexport function SearchBarShell({\n bannerTitle = \"Large title\",\n bannerDescription = \"Description\",\n pageTitle = \"Page title\",\n pageDescription = \"Description\",\n searchPlaceholder = \"Search\",\n onSearch,\n onFilterApply,\n children,\n contentClassName,\n}: SearchBarShellProps) {\n useCSSVariableSync();\n const [searchValue, setSearchValue] = useState(\"\");\n const [filterState, setFilterState] = useState<FilterState>({\n dropdowns: {},\n checkboxes: {},\n dateRange: { start: \"\", end: \"\" },\n });\n\n const handleSearch = () => {\n onSearch?.(searchValue);\n };\n\n const handleFilterApply = (newFilters: FilterState) => {\n setFilterState(newFilters);\n onFilterApply?.(newFilters);\n };\n\n const handleFilterClear = () => {\n const clearedState: FilterState = {\n dropdowns: {},\n checkboxes: {},\n dateRange: { start: \"\", end: \"\" },\n };\n setFilterState(clearedState);\n onFilterApply?.(clearedState);\n };\n\n return (\n <div className=\"min-h-screen bg-[var(--canvas-background)]\">\n {/* Header - Fixed at top with logo visible (no sidebar) */}\n <header className=\"sticky top-0 z-40\">\n <Header showDesktopLogo />\n </header>\n\n {/* Flair Banner */}\n <FlairBanner title={bannerTitle} />\n\n {/* Page Header Section - Title and Description only, no tabs */}\n <PageHeaderSection\n title={pageTitle}\n description={pageDescription}\n showTabs={false}\n />\n\n {/* Search Bar Section */}\n <div className=\"w-full\">\n <div\n className={cn(\n \"w-full max-w-[var(--content-max-width)] mx-auto\",\n \"px-[var(--spacing-xl)] lg:px-0\",\n \"pt-10 pb-10\",\n \"border-b border-[var(--canvas-border)]\"\n )}\n >\n <div className=\"flex items-center gap-6\">\n {/* Search Bar */}\n <SearchBar\n placeholder={searchPlaceholder}\n value={searchValue}\n onChange={setSearchValue}\n onSearch={handleSearch}\n className=\"flex-1\"\n />\n\n {/* Filter Button */}\n <FilterPopover\n filterState={filterState}\n onApply={handleFilterApply}\n onClear={handleFilterClear}\n />\n </div>\n </div>\n </div>\n\n {/* Main Content Area */}\n <main className=\"w-full\">\n <div\n className={cn(\n \"w-full max-w-[var(--content-max-width)] mx-auto\",\n \"px-[var(--spacing-xl)] lg:px-0\",\n \"py-[var(--spacing-6xl)]\",\n contentClassName\n )}\n >\n {children}\n </div>\n </main>\n </div>\n );\n}\n\n"
|
|
10
10
|
}
|
|
11
11
|
],
|
|
12
12
|
"dependencies": [],
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
{
|
|
7
7
|
"path": "components/layout/sidebar.tsx",
|
|
8
8
|
"type": "registry:layout",
|
|
9
|
-
"content": "\"use client\";\n\nimport { cn } from \"../../lib/utils\";\nimport { ScrollArea } from \"../ui/scroll-area\";\nimport { SidebarNav, NavSection, NavItem } from \"./sidebar-nav\";\nimport { useThemeImages, useThemeBranding } from \"../../context/theme-context\";\n\n// Phosphor Icons (Bold variant) - same curated set as variables-modal\nimport {\n Diamond,\n Hexagon,\n Star,\n Lightning,\n Sparkle,\n Infinity,\n Code,\n Terminal,\n Cpu,\n Database,\n Globe,\n Cloud,\n WifiHigh,\n Briefcase,\n Buildings,\n Storefront,\n Handshake,\n ChartLine,\n Palette as PaletteIcon,\n PencilSimple,\n Camera,\n MusicNote,\n Lightbulb,\n Leaf,\n Tree,\n Sun,\n Moon,\n Fire,\n Drop,\n ChatCircle,\n Envelope,\n Phone,\n Megaphone,\n Heart,\n Shield,\n Trophy,\n Rocket,\n Target,\n Flag,\n type Icon as PhosphorIcon,\n} from \"@phosphor-icons/react\";\n\n// ============================================\n// Icon Shape Presets for Logo Creator\n// ============================================\n\ntype IconShapeId = \"rounded\" | \"circle\" | \"square\";\n\ninterface IconShape {\n id: IconShapeId;\n renderBackground: (bgColor: string) => React.ReactNode;\n}\n\nconst iconShapes: IconShape[] = [\n {\n id: \"rounded\",\n renderBackground: (bgColor: string) => (\n <svg viewBox=\"0 0 32 32\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\" className=\"size-full absolute inset-0\">\n <rect width=\"32\" height=\"32\" rx=\"10\" style={{ fill: bgColor }} />\n </svg>\n ),\n },\n {\n id: \"circle\",\n renderBackground: (bgColor: string) => (\n <svg viewBox=\"0 0 32 32\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\" className=\"size-full absolute inset-0\">\n <circle cx=\"16\" cy=\"16\" r=\"16\" style={{ fill: bgColor }} />\n </svg>\n ),\n },\n {\n id: \"square\",\n renderBackground: (bgColor: string) => (\n <svg viewBox=\"0 0 32 32\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\" className=\"size-full absolute inset-0\">\n <rect width=\"32\" height=\"32\" style={{ fill: bgColor }} />\n </svg>\n ),\n },\n];\n\n// Map icon names to components\nconst iconMap: Record<string, PhosphorIcon> = {\n Diamond,\n Hexagon,\n Star,\n Lightning,\n Sparkle,\n Infinity,\n Code,\n Terminal,\n Cpu,\n Database,\n Globe,\n Cloud,\n WifiHigh,\n Briefcase,\n Buildings,\n Storefront,\n Handshake,\n ChartLine,\n Palette: PaletteIcon,\n PencilSimple,\n Camera,\n MusicNote,\n Lightbulb,\n Leaf,\n Tree,\n Sun,\n Moon,\n Fire,\n Drop,\n ChatCircle,\n Envelope,\n Phone,\n Megaphone,\n Heart,\n Shield,\n Trophy,\n Rocket,\n Target,\n Flag,\n};\n\n// Helper to resolve CSS variable references to actual hex colors\nfunction resolveBrandingColor(value: string): string {\n if (!value) return \"#ffffff\";\n if (value.startsWith(\"var(\")) {\n const varName = value.replace(\"var(\", \"\").replace(\")\", \"\");\n if (typeof window !== \"undefined\") {\n const computed = getComputedStyle(document.documentElement).getPropertyValue(varName).trim();\n return computed || \"#ffffff\";\n }\n return \"#ffffff\";\n }\n return value;\n}\n\ninterface SidebarProps {\n /** Navigation sections to display */\n sections: NavSection[];\n /** Visual variant - dark for desktop, light for mobile sheet */\n variant?: \"dark\" | \"light\";\n /** Callback when a nav item or subtab is clicked */\n onItemClick?: (item: NavItem | Omit<NavItem, \"children\" | \"icon\">) => void;\n /** Additional class names */\n className?: string;\n}\n\n/**\n * Canvas Design System - Sidebar Component\n * \n * Desktop: Fixed dark sidebar (320px width) on the left\n * Mobile: Light theme sidebar rendered inside a Sheet\n */\nexport function Sidebar({ \n sections, \n variant = \"dark\", \n onItemClick,\n className \n}: SidebarProps) {\n const isDark = variant === \"dark\";\n const themeImages = useThemeImages();\n const { branding, isMounted } = useThemeBranding();\n \n // Get the appropriate logo based on variant\n const logoUrl = isDark ? themeImages.logoDark : themeImages.logoLight;\n \n // Get the icon shape renderer\n const iconShape = iconShapes.find(s => s.id === branding.iconShape) || iconShapes[0];\n \n return (\n <aside\n className={cn(\n \"flex flex-col h-full w-[var(--sidebar-width)]\",\n isDark && \"bg-[var(--canvas-sidebar-dark-bg)] border-r border-[var(--canvas-sidebar-dark-border)]\",\n !isDark && \"bg-
|
|
9
|
+
"content": "\"use client\";\n\nimport { cn } from \"../../lib/utils\";\nimport { ScrollArea } from \"../ui/scroll-area\";\nimport { SidebarNav, NavSection, NavItem } from \"./sidebar-nav\";\nimport { useThemeImages, useThemeBranding } from \"../../context/theme-context\";\n\n// Phosphor Icons (Bold variant) - same curated set as variables-modal\nimport {\n Diamond,\n Hexagon,\n Star,\n Lightning,\n Sparkle,\n Infinity,\n Code,\n Terminal,\n Cpu,\n Database,\n Globe,\n Cloud,\n WifiHigh,\n Briefcase,\n Buildings,\n Storefront,\n Handshake,\n ChartLine,\n Palette as PaletteIcon,\n PencilSimple,\n Camera,\n MusicNote,\n Lightbulb,\n Leaf,\n Tree,\n Sun,\n Moon,\n Fire,\n Drop,\n ChatCircle,\n Envelope,\n Phone,\n Megaphone,\n Heart,\n Shield,\n Trophy,\n Rocket,\n Target,\n Flag,\n type Icon as PhosphorIcon,\n} from \"@phosphor-icons/react\";\n\n// ============================================\n// Icon Shape Presets for Logo Creator\n// ============================================\n\ntype IconShapeId = \"rounded\" | \"circle\" | \"square\";\n\ninterface IconShape {\n id: IconShapeId;\n renderBackground: (bgColor: string) => React.ReactNode;\n}\n\nconst iconShapes: IconShape[] = [\n {\n id: \"rounded\",\n renderBackground: (bgColor: string) => (\n <svg viewBox=\"0 0 32 32\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\" className=\"size-full absolute inset-0\">\n <rect width=\"32\" height=\"32\" rx=\"10\" style={{ fill: bgColor }} />\n </svg>\n ),\n },\n {\n id: \"circle\",\n renderBackground: (bgColor: string) => (\n <svg viewBox=\"0 0 32 32\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\" className=\"size-full absolute inset-0\">\n <circle cx=\"16\" cy=\"16\" r=\"16\" style={{ fill: bgColor }} />\n </svg>\n ),\n },\n {\n id: \"square\",\n renderBackground: (bgColor: string) => (\n <svg viewBox=\"0 0 32 32\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\" className=\"size-full absolute inset-0\">\n <rect width=\"32\" height=\"32\" style={{ fill: bgColor }} />\n </svg>\n ),\n },\n];\n\n// Map icon names to components\nconst iconMap: Record<string, PhosphorIcon> = {\n Diamond,\n Hexagon,\n Star,\n Lightning,\n Sparkle,\n Infinity,\n Code,\n Terminal,\n Cpu,\n Database,\n Globe,\n Cloud,\n WifiHigh,\n Briefcase,\n Buildings,\n Storefront,\n Handshake,\n ChartLine,\n Palette: PaletteIcon,\n PencilSimple,\n Camera,\n MusicNote,\n Lightbulb,\n Leaf,\n Tree,\n Sun,\n Moon,\n Fire,\n Drop,\n ChatCircle,\n Envelope,\n Phone,\n Megaphone,\n Heart,\n Shield,\n Trophy,\n Rocket,\n Target,\n Flag,\n};\n\n// Helper to resolve CSS variable references to actual hex colors\nfunction resolveBrandingColor(value: string): string {\n if (!value) return \"#ffffff\";\n if (value.startsWith(\"var(\")) {\n const varName = value.replace(\"var(\", \"\").replace(\")\", \"\");\n if (typeof window !== \"undefined\") {\n const computed = getComputedStyle(document.documentElement).getPropertyValue(varName).trim();\n return computed || \"#ffffff\";\n }\n return \"#ffffff\";\n }\n return value;\n}\n\ninterface SidebarProps {\n /** Navigation sections to display */\n sections: NavSection[];\n /** Visual variant - dark for desktop, light for mobile sheet */\n variant?: \"dark\" | \"light\";\n /** Callback when a nav item or subtab is clicked */\n onItemClick?: (item: NavItem | Omit<NavItem, \"children\" | \"icon\">) => void;\n /** Additional class names */\n className?: string;\n}\n\n/**\n * Canvas Design System - Sidebar Component\n * \n * Desktop: Fixed dark sidebar (320px width) on the left\n * Mobile: Light theme sidebar rendered inside a Sheet\n */\nexport function Sidebar({ \n sections, \n variant = \"dark\", \n onItemClick,\n className \n}: SidebarProps) {\n const isDark = variant === \"dark\";\n const themeImages = useThemeImages();\n const { branding, isMounted } = useThemeBranding();\n \n // Get the appropriate logo based on variant\n const logoUrl = isDark ? themeImages.logoDark : themeImages.logoLight;\n \n // Get the icon shape renderer\n const iconShape = iconShapes.find(s => s.id === branding.iconShape) || iconShapes[0];\n \n return (\n <aside\n className={cn(\n \"flex flex-col h-full w-[var(--sidebar-width)]\",\n isDark && \"bg-[var(--canvas-sidebar-dark-bg)] border-r border-[var(--canvas-sidebar-dark-border)]\",\n !isDark && \"bg-[var(--canvas-background)] border-r border-[var(--canvas-border)]\",\n className\n )}\n >\n {/* Logo Section */}\n <div className={cn(\n \"flex items-center h-8 shrink-0\",\n \"pt-11 pb-0 pr-[var(--spacing-2xl)] pl-9\"\n )}>\n {logoUrl ? (\n // Custom logo replaces entire lockup (icon + wordmark)\n <img \n src={logoUrl} \n alt=\"Logo\" \n className={`h-8 w-auto object-contain ${isMounted ? 'opacity-100' : 'opacity-0'}`}\n />\n ) : (\n // Logo creator: dynamic icon shape + Phosphor icon + wordmark\n // Uses CSS variables directly - no JavaScript resolution needed\n <div className=\"flex items-center\">\n <div className=\"relative size-8 shrink-0\">\n {iconShape.renderBackground(branding.bgColor || \"var(--canvas-primary)\")}\n <div className=\"absolute inset-0 flex items-center justify-center z-10\">\n {(() => {\n const IconComponent = iconMap[branding.iconName || \"Buildings\"] || Buildings;\n return <IconComponent weight=\"bold\" size={18} color={branding.iconColor || \"var(--canvas-primary-foreground)\"} />;\n })()}\n </div>\n </div>\n <span className={cn(\n \"font-semibold ml-[var(--spacing-md)]\",\n isDark ? \"text-white\" : \"text-[var(--canvas-text)]\"\n )} style={{ fontSize: \"var(--typo-body-xl-size)\" }}>\n {branding.wordmark || \"canvas\"}\n </span>\n </div>\n )}\n </div>\n\n {/* Navigation */}\n <ScrollArea className=\"flex-1 px-[var(--spacing-2xl)] pb-[var(--spacing-5xl)]\">\n <SidebarNav \n sections={sections} \n variant={variant} \n onItemClick={onItemClick}\n />\n </ScrollArea>\n </aside>\n );\n}\n\n// Re-export types for convenience\nexport type { NavSection, NavItem } from \"./sidebar-nav\";\n\n"
|
|
10
10
|
}
|
|
11
11
|
],
|
|
12
12
|
"dependencies": [
|