canvas-ui-sdk 0.3.8 → 0.3.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/dist/index.js +65 -63
  2. package/dist/index.js.map +1 -1
  3. package/package.json +1 -1
  4. package/registry/blocks/component-palette.json +1 -1
  5. package/registry/blocks/component-search.json +1 -1
  6. package/registry/blocks/custom-component-helper.json +1 -1
  7. package/registry/blocks/faqs-table.json +1 -1
  8. package/registry/blocks/infinity-canvas.json +1 -1
  9. package/registry/blocks/menu-section.json +1 -1
  10. package/registry/blocks/messenger-sidebar.json +1 -1
  11. package/registry/blocks/mobile-bottom-nav.json +1 -1
  12. package/registry/blocks/pagination.json +1 -1
  13. package/registry/blocks/pill-tabs.json +1 -1
  14. package/registry/blocks/pricing-cards.json +1 -1
  15. package/registry/blocks/profile-card.json +1 -1
  16. package/registry/blocks/prompt-template.json +1 -1
  17. package/registry/blocks/screen-flowchart.json +1 -1
  18. package/registry/blocks/screen-prompt-builder.json +1 -1
  19. package/registry/blocks/screen-prompt-template.json +1 -1
  20. package/registry/blocks/sidebar-cards.json +1 -1
  21. package/registry/blocks/slideshow-grid-tiles.json +1 -1
  22. package/registry/blocks/social-feed.json +1 -1
  23. package/registry/blocks/upvoting-posts-table.json +1 -1
  24. package/registry/layout/double-sidebar.json +1 -1
  25. package/registry/layout/header.json +1 -1
  26. package/registry/layout/icon-sidebar.json +1 -1
  27. package/registry/layout/project-context-shell.json +1 -1
  28. package/registry/layout/sidebar-nav.json +1 -1
  29. package/registry/ui/button.json +1 -1
  30. package/registry/ui/line-tabs.json +1 -1
  31. package/registry/ui/selectable-pills.json +1 -1
  32. package/registry/ui/tabs.json +1 -1
@@ -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-[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"
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 \"cursor-pointer 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/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=\"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"
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 \"cursor-pointer 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/sidebar-nav.tsx",
8
8
  "type": "registry:layout",
9
- "content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport { LucideIcon, ChevronDown } from \"lucide-react\";\n\nexport interface NavItem {\n id: string;\n label: string;\n icon: LucideIcon;\n href?: string;\n isActive?: boolean;\n /** Nested subtabs - when present, the item becomes expandable */\n children?: Omit<NavItem, \"children\" | \"icon\">[];\n}\n\nexport interface NavSection {\n id: string;\n title: string;\n items: NavItem[];\n}\n\ninterface SidebarNavProps {\n sections: NavSection[];\n variant?: \"dark\" | \"light\";\n onItemClick?: (item: NavItem | Omit<NavItem, \"children\" | \"icon\">) => void;\n}\n\n/**\n * Canvas Design System - Sidebar Navigation\n * \n * Renders navigation sections with collapsible groups and nav items.\n * Supports dark (desktop) and light (mobile sheet) variants.\n * Items with children become expandable with nested subtabs.\n */\nexport function SidebarNav({ sections, variant = \"dark\", onItemClick }: SidebarNavProps) {\n const isDark = variant === \"dark\";\n \n // Track which items are expanded (by item id)\n const [expandedItems, setExpandedItems] = useState<Set<string>>(() => {\n // Auto-expand items that have active children\n const initialExpanded = new Set<string>();\n sections.forEach(section => {\n section.items.forEach(item => {\n if (item.children?.some(child => child.isActive)) {\n initialExpanded.add(item.id);\n }\n });\n });\n return initialExpanded;\n });\n\n const toggleExpanded = (itemId: string) => {\n setExpandedItems(prev => {\n const next = new Set(prev);\n if (next.has(itemId)) {\n next.delete(itemId);\n } else {\n next.add(itemId);\n }\n return next;\n });\n };\n \n return (\n <nav className=\"flex flex-col gap-[var(--spacing-xs)] w-full\">\n {sections.map((section) => (\n <div key={section.id} className=\"flex flex-col w-full\">\n {/* Section Title */}\n <div className=\"flex items-center pt-11 pb-[var(--spacing-md)] pl-[var(--spacing-2xl)] pr-0\">\n <span\n className={cn(\n \"uppercase\",\n isDark \n ? \"text-[var(--canvas-sidebar-dark-text)]\" \n : \"text-[var(--canvas-neutral-placeholder)]\"\n )}\n style={{\n fontFamily: \"var(--typo-sidebar-label-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-sidebar-label-size)\",\n fontWeight: \"var(--typo-sidebar-label-weight)\",\n letterSpacing: \"var(--typo-sidebar-label-spacing)\",\n lineHeight: \"var(--typo-sidebar-label-line-height)\",\n }}\n >\n {section.title}\n </span>\n </div>\n \n {/* Nav Items */}\n <div className=\"flex flex-col gap-0\">\n {section.items.map((item) => {\n const Icon = item.icon;\n const isActive = item.isActive;\n const hasChildren = item.children && item.children.length > 0;\n const isExpanded = expandedItems.has(item.id);\n \n return (\n <div key={item.id} className=\"flex flex-col\">\n {/* Parent Nav Item */}\n <button\n onClick={() => {\n if (hasChildren) {\n toggleExpanded(item.id);\n } else {\n onItemClick?.(item);\n }\n }}\n className={cn(\n \"flex items-center gap-[var(--spacing-md)] h-11 px-[var(--spacing-xl)] rounded-[var(--radius-nav)] w-full text-left transition-colors\",\n // Dark variant (desktop sidebar)\n isDark && isActive && !hasChildren && \"bg-[var(--canvas-sidebar-dark-active-bg)] text-[var(--canvas-sidebar-dark-active-text)]\",\n isDark && !isActive && \"text-[var(--canvas-sidebar-dark-text)] hover:bg-[var(--canvas-sidebar-dark-active-bg)]/50\",\n isDark && hasChildren && isExpanded && \"text-[var(--canvas-sidebar-dark-active-text)]\",\n // Light variant (mobile sheet)\n !isDark && isActive && !hasChildren && \"bg-[var(--canvas-sidebar-light-active-bg)] text-[var(--canvas-sidebar-light-active-text)]\",\n !isDark && !isActive && \"text-[var(--canvas-sidebar-light-text)] hover:bg-[var(--canvas-sidebar-light-active-bg)]/50\",\n !isDark && hasChildren && isExpanded && \"text-[var(--canvas-sidebar-light-active-text)]\"\n )}\n >\n <Icon \n className={cn(\n \"size-4 shrink-0\",\n isDark && isActive && !hasChildren && \"text-[var(--canvas-sidebar-dark-active-text)]\",\n isDark && !isActive && \"text-[var(--canvas-sidebar-dark-text)]\",\n isDark && hasChildren && isExpanded && \"text-[var(--canvas-sidebar-dark-active-text)]\",\n !isDark && isActive && !hasChildren && \"text-[var(--canvas-sidebar-light-active-text)]\",\n !isDark && !isActive && \"text-[var(--canvas-sidebar-light-text)]\",\n !isDark && hasChildren && isExpanded && \"text-[var(--canvas-sidebar-light-active-text)]\"\n )} \n />\n <span \n className=\"flex-1 truncate\"\n style={{\n fontFamily: \"var(--typo-sidebar-tab-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-sidebar-tab-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 {hasChildren && (\n <ChevronDown \n className={cn(\n \"size-4 shrink-0 transition-transform duration-200\",\n isExpanded && \"rotate-180\",\n isDark ? \"text-[var(--canvas-sidebar-dark-text)]\" : \"text-[var(--canvas-neutral-text)]\"\n )}\n />\n )}\n </button>\n\n {/* Nested Subtabs */}\n {hasChildren && isExpanded && (\n <div className=\"flex flex-col mt-1 mb-1\">\n {item.children!.map((child) => {\n const isChildActive = child.isActive;\n \n return (\n <button\n key={child.id}\n onClick={() => onItemClick?.(child)}\n className={cn(\n \"flex items-center h-9 pl-12 pr-[var(--spacing-xl)] rounded-[var(--radius-nav)] w-full text-left transition-colors\",\n // Dark variant\n isDark && isChildActive && \"bg-[var(--canvas-sidebar-dark-active-bg)] text-[var(--canvas-sidebar-dark-active-text)]\",\n isDark && !isChildActive && \"text-[var(--canvas-sidebar-dark-text)] hover:bg-[var(--canvas-sidebar-dark-active-bg)]/50\",\n // Light variant\n !isDark && isChildActive && \"bg-[var(--canvas-sidebar-light-active-bg)] text-[var(--canvas-sidebar-light-active-text)]\",\n !isDark && !isChildActive && \"text-[var(--canvas-sidebar-light-text)] hover:bg-[var(--canvas-sidebar-light-active-bg)]/50\"\n )}\n >\n <span \n className=\"truncate\"\n style={{\n fontFamily: \"var(--typo-sidebar-subtab-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-sidebar-subtab-size)\",\n fontWeight: \"var(--typo-sidebar-subtab-weight)\",\n letterSpacing: \"var(--typo-sidebar-subtab-spacing)\",\n lineHeight: \"var(--typo-sidebar-subtab-line-height)\",\n }}\n >\n {child.label}\n </span>\n </button>\n );\n })}\n </div>\n )}\n </div>\n );\n })}\n </div>\n </div>\n ))}\n </nav>\n );\n}\n"
9
+ "content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport { LucideIcon, ChevronDown } from \"lucide-react\";\n\nexport interface NavItem {\n id: string;\n label: string;\n icon: LucideIcon;\n href?: string;\n isActive?: boolean;\n /** Nested subtabs - when present, the item becomes expandable */\n children?: Omit<NavItem, \"children\" | \"icon\">[];\n}\n\nexport interface NavSection {\n id: string;\n title: string;\n items: NavItem[];\n}\n\ninterface SidebarNavProps {\n sections: NavSection[];\n variant?: \"dark\" | \"light\";\n onItemClick?: (item: NavItem | Omit<NavItem, \"children\" | \"icon\">) => void;\n}\n\n/**\n * Canvas Design System - Sidebar Navigation\n * \n * Renders navigation sections with collapsible groups and nav items.\n * Supports dark (desktop) and light (mobile sheet) variants.\n * Items with children become expandable with nested subtabs.\n */\nexport function SidebarNav({ sections, variant = \"dark\", onItemClick }: SidebarNavProps) {\n const isDark = variant === \"dark\";\n \n // Track which items are expanded (by item id)\n const [expandedItems, setExpandedItems] = useState<Set<string>>(() => {\n // Auto-expand items that have active children\n const initialExpanded = new Set<string>();\n sections.forEach(section => {\n section.items.forEach(item => {\n if (item.children?.some(child => child.isActive)) {\n initialExpanded.add(item.id);\n }\n });\n });\n return initialExpanded;\n });\n\n const toggleExpanded = (itemId: string) => {\n setExpandedItems(prev => {\n const next = new Set(prev);\n if (next.has(itemId)) {\n next.delete(itemId);\n } else {\n next.add(itemId);\n }\n return next;\n });\n };\n \n return (\n <nav className=\"flex flex-col gap-[var(--spacing-xs)] w-full\">\n {sections.map((section) => (\n <div key={section.id} className=\"flex flex-col w-full\">\n {/* Section Title */}\n <div className=\"flex items-center pt-11 pb-[var(--spacing-md)] pl-[var(--spacing-2xl)] pr-0\">\n <span\n className={cn(\n \"uppercase\",\n isDark \n ? \"text-[var(--canvas-sidebar-dark-text)]\" \n : \"text-[var(--canvas-neutral-placeholder)]\"\n )}\n style={{\n fontFamily: \"var(--typo-sidebar-label-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-sidebar-label-size)\",\n fontWeight: \"var(--typo-sidebar-label-weight)\",\n letterSpacing: \"var(--typo-sidebar-label-spacing)\",\n lineHeight: \"var(--typo-sidebar-label-line-height)\",\n }}\n >\n {section.title}\n </span>\n </div>\n \n {/* Nav Items */}\n <div className=\"flex flex-col gap-0\">\n {section.items.map((item) => {\n const Icon = item.icon;\n const isActive = item.isActive;\n const hasChildren = item.children && item.children.length > 0;\n const isExpanded = expandedItems.has(item.id);\n \n return (\n <div key={item.id} className=\"flex flex-col\">\n {/* Parent Nav Item */}\n <button\n onClick={() => {\n if (hasChildren) {\n toggleExpanded(item.id);\n } else {\n onItemClick?.(item);\n }\n }}\n className={cn(\n \"cursor-pointer flex items-center gap-[var(--spacing-md)] h-11 px-[var(--spacing-xl)] rounded-[var(--radius-nav)] w-full text-left transition-colors\",\n // Dark variant (desktop sidebar)\n isDark && isActive && !hasChildren && \"bg-[var(--canvas-sidebar-dark-active-bg)] text-[var(--canvas-sidebar-dark-active-text)]\",\n isDark && !isActive && \"text-[var(--canvas-sidebar-dark-text)] hover:bg-[var(--canvas-sidebar-dark-active-bg)]/50\",\n isDark && hasChildren && isExpanded && \"text-[var(--canvas-sidebar-dark-active-text)]\",\n // Light variant (mobile sheet)\n !isDark && isActive && !hasChildren && \"bg-[var(--canvas-sidebar-light-active-bg)] text-[var(--canvas-sidebar-light-active-text)]\",\n !isDark && !isActive && \"text-[var(--canvas-sidebar-light-text)] hover:bg-[var(--canvas-sidebar-light-active-bg)]/50\",\n !isDark && hasChildren && isExpanded && \"text-[var(--canvas-sidebar-light-active-text)]\"\n )}\n >\n <Icon \n className={cn(\n \"size-4 shrink-0\",\n isDark && isActive && !hasChildren && \"text-[var(--canvas-sidebar-dark-active-text)]\",\n isDark && !isActive && \"text-[var(--canvas-sidebar-dark-text)]\",\n isDark && hasChildren && isExpanded && \"text-[var(--canvas-sidebar-dark-active-text)]\",\n !isDark && isActive && !hasChildren && \"text-[var(--canvas-sidebar-light-active-text)]\",\n !isDark && !isActive && \"text-[var(--canvas-sidebar-light-text)]\",\n !isDark && hasChildren && isExpanded && \"text-[var(--canvas-sidebar-light-active-text)]\"\n )} \n />\n <span \n className=\"flex-1 truncate\"\n style={{\n fontFamily: \"var(--typo-sidebar-tab-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-sidebar-tab-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 {hasChildren && (\n <ChevronDown \n className={cn(\n \"size-4 shrink-0 transition-transform duration-200\",\n isExpanded && \"rotate-180\",\n isDark ? \"text-[var(--canvas-sidebar-dark-text)]\" : \"text-[var(--canvas-neutral-text)]\"\n )}\n />\n )}\n </button>\n\n {/* Nested Subtabs */}\n {hasChildren && isExpanded && (\n <div className=\"flex flex-col mt-1 mb-1\">\n {item.children!.map((child) => {\n const isChildActive = child.isActive;\n \n return (\n <button\n key={child.id}\n onClick={() => onItemClick?.(child)}\n className={cn(\n \"cursor-pointer flex items-center h-9 pl-12 pr-[var(--spacing-xl)] rounded-[var(--radius-nav)] w-full text-left transition-colors\",\n // Dark variant\n isDark && isChildActive && \"bg-[var(--canvas-sidebar-dark-active-bg)] text-[var(--canvas-sidebar-dark-active-text)]\",\n isDark && !isChildActive && \"text-[var(--canvas-sidebar-dark-text)] hover:bg-[var(--canvas-sidebar-dark-active-bg)]/50\",\n // Light variant\n !isDark && isChildActive && \"bg-[var(--canvas-sidebar-light-active-bg)] text-[var(--canvas-sidebar-light-active-text)]\",\n !isDark && !isChildActive && \"text-[var(--canvas-sidebar-light-text)] hover:bg-[var(--canvas-sidebar-light-active-bg)]/50\"\n )}\n >\n <span \n className=\"truncate\"\n style={{\n fontFamily: \"var(--typo-sidebar-subtab-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-sidebar-subtab-size)\",\n fontWeight: \"var(--typo-sidebar-subtab-weight)\",\n letterSpacing: \"var(--typo-sidebar-subtab-spacing)\",\n lineHeight: \"var(--typo-sidebar-subtab-line-height)\",\n }}\n >\n {child.label}\n </span>\n </button>\n );\n })}\n </div>\n )}\n </div>\n );\n })}\n </div>\n </div>\n ))}\n </nav>\n );\n}\n"
10
10
  }
11
11
  ],
12
12
  "dependencies": [
@@ -6,7 +6,7 @@
6
6
  {
7
7
  "path": "components/ui/button.tsx",
8
8
  "type": "registry:ui",
9
- "content": "import * as React from \"react\"\nimport { Slot } from \"@radix-ui/react-slot\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"../../lib/utils\"\n\n// Base button styles (layout and structure only, colors applied via CSS variables)\nconst buttonVariants = cva(\n \"inline-flex items-center justify-center gap-2 whitespace-nowrap font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:ring-ring/50 focus-visible:ring-[3px]\",\n {\n variants: {\n variant: {\n // Custom Canvas variants (styled via CSS variables in component)\n primary: \"\",\n \"primary-outline\": \"border\",\n \"primary-neutral\": \"\",\n neutral: \"border\",\n \"neutral-delete\": \"border\",\n delete: \"\",\n // Legacy shadcn variants (for backwards compatibility)\n default: \"bg-primary text-primary-foreground hover:bg-primary/90\",\n destructive: \"bg-destructive text-white hover:bg-destructive/90\",\n outline: \"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground\",\n secondary: \"bg-secondary text-secondary-foreground hover:bg-secondary/80\",\n ghost: \"hover:bg-accent hover:text-accent-foreground\",\n link: \"text-primary underline-offset-4 hover:underline\",\n },\n size: {\n // Custom Canvas sizes (styled via CSS variables in component)\n mini: \"\",\n sm: \"\",\n default: \"\",\n lg: \"\",\n // Legacy shadcn sizes\n icon: \"size-9\",\n \"icon-sm\": \"size-8\",\n \"icon-lg\": \"size-10\",\n },\n },\n defaultVariants: {\n variant: \"default\",\n size: \"default\",\n },\n }\n)\n\n// CSS variable mappings for Canvas design system variants\nconst variantStyles: Record<string, React.CSSProperties> = {\n primary: {\n backgroundColor: \"var(--btn-primary-bg)\",\n color: \"var(--btn-primary-text)\",\n borderColor: \"var(--btn-primary-border)\",\n },\n \"primary-outline\": {\n backgroundColor: \"var(--btn-primary-outline-bg)\",\n color: \"var(--btn-primary-outline-text)\",\n borderColor: \"var(--btn-primary-outline-border)\",\n borderWidth: \"1px\",\n borderStyle: \"solid\",\n },\n \"primary-neutral\": {\n backgroundColor: \"var(--btn-primary-neutral-bg)\",\n color: \"var(--btn-primary-neutral-text)\",\n borderColor: \"var(--btn-primary-neutral-border)\",\n },\n neutral: {\n backgroundColor: \"var(--btn-neutral-bg)\",\n color: \"var(--btn-neutral-text)\",\n borderColor: \"var(--btn-neutral-border)\",\n borderWidth: \"1px\",\n borderStyle: \"solid\",\n },\n \"neutral-delete\": {\n backgroundColor: \"var(--btn-neutral-delete-bg)\",\n color: \"var(--btn-neutral-delete-text)\",\n borderColor: \"var(--btn-neutral-delete-border)\",\n borderWidth: \"1px\",\n borderStyle: \"solid\",\n },\n delete: {\n backgroundColor: \"var(--btn-delete-bg)\",\n color: \"var(--btn-delete-text)\",\n borderColor: \"var(--btn-delete-border)\",\n },\n}\n\n// CSS variable mappings for Canvas design system sizes\nconst sizeStyles: Record<string, React.CSSProperties> = {\n mini: {\n height: \"var(--btn-mini-height)\",\n paddingLeft: \"var(--btn-mini-px)\",\n paddingRight: \"var(--btn-mini-px)\",\n fontSize: \"var(--btn-mini-font-size)\",\n borderRadius: \"var(--btn-mini-radius)\",\n fontWeight: \"var(--btn-mini-font-weight)\" as React.CSSProperties[\"fontWeight\"],\n letterSpacing: \"var(--btn-mini-letter-spacing)\",\n fontFamily: \"var(--btn-mini-font-family, var(--typo-button-font, var(--typo-global-font)))\",\n },\n sm: {\n height: \"var(--btn-small-height)\",\n paddingLeft: \"var(--btn-small-px)\",\n paddingRight: \"var(--btn-small-px)\",\n fontSize: \"var(--btn-small-font-size)\",\n borderRadius: \"var(--btn-small-radius)\",\n fontWeight: \"var(--btn-small-font-weight)\" as React.CSSProperties[\"fontWeight\"],\n letterSpacing: \"var(--btn-small-letter-spacing)\",\n fontFamily: \"var(--btn-small-font-family, var(--typo-button-font, var(--typo-global-font)))\",\n },\n default: {\n height: \"var(--btn-standard-height)\",\n paddingLeft: \"var(--btn-standard-px)\",\n paddingRight: \"var(--btn-standard-px)\",\n fontSize: \"var(--btn-standard-font-size)\",\n borderRadius: \"var(--btn-standard-radius)\",\n fontWeight: \"var(--btn-standard-font-weight)\" as React.CSSProperties[\"fontWeight\"],\n letterSpacing: \"var(--btn-standard-letter-spacing)\",\n fontFamily: \"var(--btn-standard-font-family, var(--typo-button-font, var(--typo-global-font)))\",\n },\n lg: {\n height: \"var(--btn-large-height)\",\n paddingLeft: \"var(--btn-large-px)\",\n paddingRight: \"var(--btn-large-px)\",\n fontSize: \"var(--btn-large-font-size)\",\n borderRadius: \"var(--btn-large-radius)\",\n fontWeight: \"var(--btn-large-font-weight)\" as React.CSSProperties[\"fontWeight\"],\n letterSpacing: \"var(--btn-large-letter-spacing)\",\n fontFamily: \"var(--btn-large-font-family, var(--typo-button-font, var(--typo-global-font)))\",\n },\n}\n\n// Custom Canvas variants that use CSS variables\nconst canvasVariants = [\"primary\", \"primary-outline\", \"primary-neutral\", \"neutral\", \"neutral-delete\", \"delete\"]\nconst canvasSizes = [\"mini\", \"sm\", \"default\", \"lg\"]\n\nfunction Button({\n className,\n variant = \"default\",\n size = \"default\",\n asChild = false,\n style,\n ...props\n}: React.ComponentProps<\"button\"> &\n VariantProps<typeof buttonVariants> & {\n asChild?: boolean\n }) {\n const Comp = asChild ? Slot : \"button\"\n\n // Build style object with CSS variables for Canvas variants/sizes\n const computedStyle: React.CSSProperties = { ...style }\n \n if (variant && canvasVariants.includes(variant)) {\n Object.assign(computedStyle, variantStyles[variant])\n }\n \n if (size && canvasSizes.includes(size)) {\n Object.assign(computedStyle, sizeStyles[size])\n }\n\n return (\n <Comp\n data-slot=\"button\"\n data-variant={variant}\n data-size={size}\n className={cn(buttonVariants({ variant, size, className }))}\n style={Object.keys(computedStyle).length > 0 ? computedStyle : undefined}\n {...props}\n />\n )\n}\n\nexport { Button, buttonVariants }\n"
9
+ "content": "import * as React from \"react\"\nimport { Slot } from \"@radix-ui/react-slot\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"../../lib/utils\"\n\n// Base button styles (layout and structure only, colors applied via CSS variables)\nconst buttonVariants = cva(\n \"inline-flex items-center justify-center gap-2 whitespace-nowrap font-medium transition-all cursor-pointer disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:ring-ring/50 focus-visible:ring-[3px]\",\n {\n variants: {\n variant: {\n // Custom Canvas variants (styled via CSS variables in component)\n primary: \"\",\n \"primary-outline\": \"border\",\n \"primary-neutral\": \"\",\n neutral: \"border\",\n \"neutral-delete\": \"border\",\n delete: \"\",\n // Legacy shadcn variants (for backwards compatibility)\n default: \"bg-primary text-primary-foreground hover:bg-primary/90\",\n destructive: \"bg-destructive text-white hover:bg-destructive/90\",\n outline: \"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground\",\n secondary: \"bg-secondary text-secondary-foreground hover:bg-secondary/80\",\n ghost: \"hover:bg-accent hover:text-accent-foreground\",\n link: \"text-primary underline-offset-4 hover:underline\",\n },\n size: {\n // Custom Canvas sizes (styled via CSS variables in component)\n mini: \"\",\n sm: \"\",\n default: \"\",\n lg: \"\",\n // Legacy shadcn sizes\n icon: \"size-9\",\n \"icon-sm\": \"size-8\",\n \"icon-lg\": \"size-10\",\n },\n },\n defaultVariants: {\n variant: \"default\",\n size: \"default\",\n },\n }\n)\n\n// CSS variable mappings for Canvas design system variants\nconst variantStyles: Record<string, React.CSSProperties> = {\n primary: {\n backgroundColor: \"var(--btn-primary-bg)\",\n color: \"var(--btn-primary-text)\",\n borderColor: \"var(--btn-primary-border)\",\n },\n \"primary-outline\": {\n backgroundColor: \"var(--btn-primary-outline-bg)\",\n color: \"var(--btn-primary-outline-text)\",\n borderColor: \"var(--btn-primary-outline-border)\",\n borderWidth: \"1px\",\n borderStyle: \"solid\",\n },\n \"primary-neutral\": {\n backgroundColor: \"var(--btn-primary-neutral-bg)\",\n color: \"var(--btn-primary-neutral-text)\",\n borderColor: \"var(--btn-primary-neutral-border)\",\n },\n neutral: {\n backgroundColor: \"var(--btn-neutral-bg)\",\n color: \"var(--btn-neutral-text)\",\n borderColor: \"var(--btn-neutral-border)\",\n borderWidth: \"1px\",\n borderStyle: \"solid\",\n },\n \"neutral-delete\": {\n backgroundColor: \"var(--btn-neutral-delete-bg)\",\n color: \"var(--btn-neutral-delete-text)\",\n borderColor: \"var(--btn-neutral-delete-border)\",\n borderWidth: \"1px\",\n borderStyle: \"solid\",\n },\n delete: {\n backgroundColor: \"var(--btn-delete-bg)\",\n color: \"var(--btn-delete-text)\",\n borderColor: \"var(--btn-delete-border)\",\n },\n}\n\n// CSS variable mappings for Canvas design system sizes\nconst sizeStyles: Record<string, React.CSSProperties> = {\n mini: {\n height: \"var(--btn-mini-height)\",\n paddingLeft: \"var(--btn-mini-px)\",\n paddingRight: \"var(--btn-mini-px)\",\n fontSize: \"var(--btn-mini-font-size)\",\n borderRadius: \"var(--btn-mini-radius)\",\n fontWeight: \"var(--btn-mini-font-weight)\" as React.CSSProperties[\"fontWeight\"],\n letterSpacing: \"var(--btn-mini-letter-spacing)\",\n fontFamily: \"var(--btn-mini-font-family, var(--typo-button-font, var(--typo-global-font)))\",\n },\n sm: {\n height: \"var(--btn-small-height)\",\n paddingLeft: \"var(--btn-small-px)\",\n paddingRight: \"var(--btn-small-px)\",\n fontSize: \"var(--btn-small-font-size)\",\n borderRadius: \"var(--btn-small-radius)\",\n fontWeight: \"var(--btn-small-font-weight)\" as React.CSSProperties[\"fontWeight\"],\n letterSpacing: \"var(--btn-small-letter-spacing)\",\n fontFamily: \"var(--btn-small-font-family, var(--typo-button-font, var(--typo-global-font)))\",\n },\n default: {\n height: \"var(--btn-standard-height)\",\n paddingLeft: \"var(--btn-standard-px)\",\n paddingRight: \"var(--btn-standard-px)\",\n fontSize: \"var(--btn-standard-font-size)\",\n borderRadius: \"var(--btn-standard-radius)\",\n fontWeight: \"var(--btn-standard-font-weight)\" as React.CSSProperties[\"fontWeight\"],\n letterSpacing: \"var(--btn-standard-letter-spacing)\",\n fontFamily: \"var(--btn-standard-font-family, var(--typo-button-font, var(--typo-global-font)))\",\n },\n lg: {\n height: \"var(--btn-large-height)\",\n paddingLeft: \"var(--btn-large-px)\",\n paddingRight: \"var(--btn-large-px)\",\n fontSize: \"var(--btn-large-font-size)\",\n borderRadius: \"var(--btn-large-radius)\",\n fontWeight: \"var(--btn-large-font-weight)\" as React.CSSProperties[\"fontWeight\"],\n letterSpacing: \"var(--btn-large-letter-spacing)\",\n fontFamily: \"var(--btn-large-font-family, var(--typo-button-font, var(--typo-global-font)))\",\n },\n}\n\n// Custom Canvas variants that use CSS variables\nconst canvasVariants = [\"primary\", \"primary-outline\", \"primary-neutral\", \"neutral\", \"neutral-delete\", \"delete\"]\nconst canvasSizes = [\"mini\", \"sm\", \"default\", \"lg\"]\n\nfunction Button({\n className,\n variant = \"default\",\n size = \"default\",\n asChild = false,\n style,\n ...props\n}: React.ComponentProps<\"button\"> &\n VariantProps<typeof buttonVariants> & {\n asChild?: boolean\n }) {\n const Comp = asChild ? Slot : \"button\"\n\n // Build style object with CSS variables for Canvas variants/sizes\n const computedStyle: React.CSSProperties = { ...style }\n \n if (variant && canvasVariants.includes(variant)) {\n Object.assign(computedStyle, variantStyles[variant])\n }\n \n if (size && canvasSizes.includes(size)) {\n Object.assign(computedStyle, sizeStyles[size])\n }\n\n return (\n <Comp\n data-slot=\"button\"\n data-variant={variant}\n data-size={size}\n className={cn(buttonVariants({ variant, size, className }))}\n style={Object.keys(computedStyle).length > 0 ? computedStyle : undefined}\n {...props}\n />\n )\n}\n\nexport { Button, buttonVariants }\n"
10
10
  }
11
11
  ],
12
12
  "dependencies": [
@@ -6,7 +6,7 @@
6
6
  {
7
7
  "path": "components/ui/line-tabs.tsx",
8
8
  "type": "registry:ui",
9
- "content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { cn } from \"../../lib/utils\";\n\nexport interface LineTab {\n id: string;\n label: string;\n}\n\ninterface LineTabsProps {\n /** Array of tab items */\n tabs: LineTab[];\n /** ID of the currently active tab */\n activeTab?: string;\n /** Callback when a tab is clicked */\n onTabChange?: (tabId: string) => void;\n /** Additional class names */\n className?: string;\n}\n\n/**\n * Canvas Design System - Line Tabs Component\n * \n * Horizontal underline-style tabs for page navigation.\n * Active tab has blue text with 2px blue bottom border.\n * Inactive tabs have gray text with 1px gray bottom border.\n * \n * @example\n * ```tsx\n * <LineTabs \n * tabs={[\n * { id: \"tab1\", label: \"Tab 1\" },\n * { id: \"tab2\", label: \"Tab 2\" },\n * ]} \n * activeTab=\"tab1\"\n * onTabChange={(id) => console.log(id)}\n * />\n * ```\n */\nexport function LineTabs({\n tabs,\n activeTab: controlledActiveTab,\n onTabChange,\n className,\n}: LineTabsProps) {\n // Use internal state if not controlled\n const [internalActiveTab, setInternalActiveTab] = useState(tabs[0]?.id || \"\");\n const activeTab = controlledActiveTab ?? internalActiveTab;\n\n const handleTabClick = (tabId: string) => {\n if (!controlledActiveTab) {\n setInternalActiveTab(tabId);\n }\n onTabChange?.(tabId);\n };\n\n return (\n <div className={cn(\"flex items-end w-full\", className)}>\n {tabs.map((tab, index) => {\n const isActive = tab.id === activeTab;\n const isLast = index === tabs.length - 1;\n\n return (\n <div key={tab.id} className=\"flex items-end h-12\">\n {/* Tab Button */}\n <button\n onClick={() => handleTabClick(tab.id)}\n className={cn(\n \"flex items-center justify-center h-full px-0 transition-colors\",\n isActive\n ? \"border-b-2 border-[var(--canvas-primary)] text-[var(--canvas-primary)]\"\n : \"border-b border-[var(--canvas-border)] text-[var(--canvas-text-muted)] hover:text-[var(--canvas-text)]\"\n )}\n >\n <span className=\"text-base font-medium leading-6\">\n {tab.label}\n </span>\n </button>\n\n {/* Divider - 24px wide line */}\n {!isLast && (\n <div className=\"w-6 h-0 border-b border-[var(--canvas-border)]\" />\n )}\n </div>\n );\n })}\n\n {/* Trailing line to fill remaining space */}\n <div className=\"flex-1 h-0 border-b border-[var(--canvas-border)]\" />\n </div>\n );\n}\n\n"
9
+ "content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { cn } from \"../../lib/utils\";\n\nexport interface LineTab {\n id: string;\n label: string;\n}\n\ninterface LineTabsProps {\n /** Array of tab items */\n tabs: LineTab[];\n /** ID of the currently active tab */\n activeTab?: string;\n /** Callback when a tab is clicked */\n onTabChange?: (tabId: string) => void;\n /** Additional class names */\n className?: string;\n}\n\n/**\n * Canvas Design System - Line Tabs Component\n * \n * Horizontal underline-style tabs for page navigation.\n * Active tab has blue text with 2px blue bottom border.\n * Inactive tabs have gray text with 1px gray bottom border.\n * \n * @example\n * ```tsx\n * <LineTabs \n * tabs={[\n * { id: \"tab1\", label: \"Tab 1\" },\n * { id: \"tab2\", label: \"Tab 2\" },\n * ]} \n * activeTab=\"tab1\"\n * onTabChange={(id) => console.log(id)}\n * />\n * ```\n */\nexport function LineTabs({\n tabs,\n activeTab: controlledActiveTab,\n onTabChange,\n className,\n}: LineTabsProps) {\n // Use internal state if not controlled\n const [internalActiveTab, setInternalActiveTab] = useState(tabs[0]?.id || \"\");\n const activeTab = controlledActiveTab ?? internalActiveTab;\n\n const handleTabClick = (tabId: string) => {\n if (!controlledActiveTab) {\n setInternalActiveTab(tabId);\n }\n onTabChange?.(tabId);\n };\n\n return (\n <div className={cn(\"flex items-end w-full\", className)}>\n {tabs.map((tab, index) => {\n const isActive = tab.id === activeTab;\n const isLast = index === tabs.length - 1;\n\n return (\n <div key={tab.id} className=\"flex items-end h-12\">\n {/* Tab Button */}\n <button\n onClick={() => handleTabClick(tab.id)}\n className={cn(\n \"flex items-center justify-center h-full px-0 transition-colors cursor-pointer\",\n isActive\n ? \"border-b-2 border-[var(--canvas-primary)] text-[var(--canvas-primary)]\"\n : \"border-b border-[var(--canvas-border)] text-[var(--canvas-text-muted)] hover:text-[var(--canvas-text)]\"\n )}\n >\n <span className=\"text-base font-medium leading-6\">\n {tab.label}\n </span>\n </button>\n\n {/* Divider - 24px wide line */}\n {!isLast && (\n <div className=\"w-6 h-0 border-b border-[var(--canvas-border)]\" />\n )}\n </div>\n );\n })}\n\n {/* Trailing line to fill remaining space */}\n <div className=\"flex-1 h-0 border-b border-[var(--canvas-border)]\" />\n </div>\n );\n}\n\n"
10
10
  }
11
11
  ],
12
12
  "dependencies": [],
@@ -6,7 +6,7 @@
6
6
  {
7
7
  "path": "components/ui/selectable-pills.tsx",
8
8
  "type": "registry:ui",
9
- "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"../../lib/utils\";\n\nexport interface PillOption {\n id: string;\n label: string;\n}\n\nexport interface SelectablePillsProps {\n inputSize?: \"sm\" | \"default\" | \"lg\";\n options: PillOption[];\n selected: string[];\n onSelectionChange: (selected: string[]) => void;\n allowMultiple?: boolean;\n className?: string;\n disabled?: boolean;\n}\n\nconst SelectablePills = React.forwardRef<HTMLDivElement, SelectablePillsProps>(\n ({ \n inputSize = \"default\", \n options, \n selected, \n onSelectionChange,\n allowMultiple = true,\n className,\n disabled,\n }, ref) => {\n // Size mappings - using fixed values that scale appropriately\n const sizeStyles = {\n sm: {\n pill: \"h-7 px-2.5 text-xs gap-1\",\n container: \"gap-2\",\n },\n default: {\n pill: \"h-9 px-3.5 text-sm gap-1.5\",\n container: \"gap-3\",\n },\n lg: {\n pill: \"h-11 px-4 text-base gap-2\",\n container: \"gap-4\",\n },\n };\n\n const styles = sizeStyles[inputSize];\n\n const handlePillClick = (optionId: string) => {\n if (disabled) return;\n\n if (allowMultiple) {\n // Toggle selection\n if (selected.includes(optionId)) {\n onSelectionChange(selected.filter(id => id !== optionId));\n } else {\n onSelectionChange([...selected, optionId]);\n }\n } else {\n // Single selection - toggle or replace\n if (selected.includes(optionId)) {\n onSelectionChange([]);\n } else {\n onSelectionChange([optionId]);\n }\n }\n };\n\n return (\n <div \n ref={ref}\n className={cn(\n \"flex flex-wrap\",\n styles.container,\n className\n )}\n >\n {options.map((option) => {\n const isSelected = selected.includes(option.id);\n \n return (\n <button\n key={option.id}\n type=\"button\"\n onClick={() => handlePillClick(option.id)}\n disabled={disabled}\n className={cn(\n \"inline-flex items-center justify-center rounded-[var(--radius-xs)] border transition-colors\",\n \"font-medium whitespace-nowrap\",\n \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--canvas-border-input-focus)] focus-visible:ring-offset-2\",\n \"disabled:cursor-not-allowed disabled:opacity-50\",\n styles.pill,\n isSelected\n ? \"bg-[var(--canvas-surface-brand)] border-[var(--canvas-primary)] text-[var(--canvas-primary)]\"\n : \"bg-[var(--canvas-background)] border-[var(--canvas-border)] text-[var(--canvas-text-muted)] hover:border-[var(--canvas-text-muted)]\"\n )}\n >\n {option.label}\n </button>\n );\n })}\n </div>\n );\n }\n);\n\nSelectablePills.displayName = \"SelectablePills\";\n\nexport { SelectablePills };\n\n"
9
+ "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"../../lib/utils\";\n\nexport interface PillOption {\n id: string;\n label: string;\n}\n\nexport interface SelectablePillsProps {\n inputSize?: \"sm\" | \"default\" | \"lg\";\n options: PillOption[];\n selected: string[];\n onSelectionChange: (selected: string[]) => void;\n allowMultiple?: boolean;\n className?: string;\n disabled?: boolean;\n}\n\nconst SelectablePills = React.forwardRef<HTMLDivElement, SelectablePillsProps>(\n ({ \n inputSize = \"default\", \n options, \n selected, \n onSelectionChange,\n allowMultiple = true,\n className,\n disabled,\n }, ref) => {\n // Size mappings - using fixed values that scale appropriately\n const sizeStyles = {\n sm: {\n pill: \"h-7 px-2.5 text-xs gap-1\",\n container: \"gap-2\",\n },\n default: {\n pill: \"h-9 px-3.5 text-sm gap-1.5\",\n container: \"gap-3\",\n },\n lg: {\n pill: \"h-11 px-4 text-base gap-2\",\n container: \"gap-4\",\n },\n };\n\n const styles = sizeStyles[inputSize];\n\n const handlePillClick = (optionId: string) => {\n if (disabled) return;\n\n if (allowMultiple) {\n // Toggle selection\n if (selected.includes(optionId)) {\n onSelectionChange(selected.filter(id => id !== optionId));\n } else {\n onSelectionChange([...selected, optionId]);\n }\n } else {\n // Single selection - toggle or replace\n if (selected.includes(optionId)) {\n onSelectionChange([]);\n } else {\n onSelectionChange([optionId]);\n }\n }\n };\n\n return (\n <div \n ref={ref}\n className={cn(\n \"flex flex-wrap\",\n styles.container,\n className\n )}\n >\n {options.map((option) => {\n const isSelected = selected.includes(option.id);\n \n return (\n <button\n key={option.id}\n type=\"button\"\n onClick={() => handlePillClick(option.id)}\n disabled={disabled}\n className={cn(\n \"inline-flex items-center justify-center rounded-[var(--radius-xs)] border transition-colors cursor-pointer\",\n \"font-medium whitespace-nowrap\",\n \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--canvas-border-input-focus)] focus-visible:ring-offset-2\",\n \"disabled:cursor-not-allowed disabled:opacity-50\",\n styles.pill,\n isSelected\n ? \"bg-[var(--canvas-surface-brand)] border-[var(--canvas-primary)] text-[var(--canvas-primary)]\"\n : \"bg-[var(--canvas-background)] border-[var(--canvas-border)] text-[var(--canvas-text-muted)] hover:border-[var(--canvas-text-muted)]\"\n )}\n >\n {option.label}\n </button>\n );\n })}\n </div>\n );\n }\n);\n\nSelectablePills.displayName = \"SelectablePills\";\n\nexport { SelectablePills };\n\n"
10
10
  }
11
11
  ],
12
12
  "dependencies": [],
@@ -6,7 +6,7 @@
6
6
  {
7
7
  "path": "components/ui/tabs.tsx",
8
8
  "type": "registry:ui",
9
- "content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as TabsPrimitive from \"@radix-ui/react-tabs\"\n\nimport { cn } from \"../../lib/utils\"\n\nfunction Tabs({\n className,\n ...props\n}: React.ComponentProps<typeof TabsPrimitive.Root>) {\n return (\n <TabsPrimitive.Root\n data-slot=\"tabs\"\n className={cn(\"flex flex-col gap-2\", className)}\n {...props}\n />\n )\n}\n\nfunction TabsList({\n className,\n ...props\n}: React.ComponentProps<typeof TabsPrimitive.List>) {\n return (\n <TabsPrimitive.List\n data-slot=\"tabs-list\"\n className={cn(\n \"bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction TabsTrigger({\n className,\n ...props\n}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {\n return (\n <TabsPrimitive.Trigger\n data-slot=\"tabs-trigger\"\n className={cn(\n \"data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction TabsContent({\n className,\n ...props\n}: React.ComponentProps<typeof TabsPrimitive.Content>) {\n return (\n <TabsPrimitive.Content\n data-slot=\"tabs-content\"\n className={cn(\"flex-1 outline-none\", className)}\n {...props}\n />\n )\n}\n\nexport { Tabs, TabsList, TabsTrigger, TabsContent }\n"
9
+ "content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as TabsPrimitive from \"@radix-ui/react-tabs\"\n\nimport { cn } from \"../../lib/utils\"\n\nfunction Tabs({\n className,\n ...props\n}: React.ComponentProps<typeof TabsPrimitive.Root>) {\n return (\n <TabsPrimitive.Root\n data-slot=\"tabs\"\n className={cn(\"flex flex-col gap-2\", className)}\n {...props}\n />\n )\n}\n\nfunction TabsList({\n className,\n ...props\n}: React.ComponentProps<typeof TabsPrimitive.List>) {\n return (\n <TabsPrimitive.List\n data-slot=\"tabs-list\"\n className={cn(\n \"bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction TabsTrigger({\n className,\n ...props\n}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {\n return (\n <TabsPrimitive.Trigger\n data-slot=\"tabs-trigger\"\n className={cn(\n \"data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] cursor-pointer focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction TabsContent({\n className,\n ...props\n}: React.ComponentProps<typeof TabsPrimitive.Content>) {\n return (\n <TabsPrimitive.Content\n data-slot=\"tabs-content\"\n className={cn(\"flex-1 outline-none\", className)}\n {...props}\n />\n )\n}\n\nexport { Tabs, TabsList, TabsTrigger, TabsContent }\n"
10
10
  }
11
11
  ],
12
12
  "dependencies": [