canvas-ui-sdk 0.3.7 → 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 (76) hide show
  1. package/dist/index.js +326 -277
  2. package/dist/index.js.map +1 -1
  3. package/mcp/dist/index.js +14 -2
  4. package/package.json +1 -1
  5. package/registry/blocks/canvas-item.json +1 -1
  6. package/registry/blocks/chat-message.json +1 -1
  7. package/registry/blocks/component-palette.json +1 -1
  8. package/registry/blocks/component-search.json +1 -1
  9. package/registry/blocks/content-dropzone.json +1 -1
  10. package/registry/blocks/credit-card-display.json +1 -1
  11. package/registry/blocks/custom-component-helper.json +1 -1
  12. package/registry/blocks/empty-state.json +1 -1
  13. package/registry/blocks/faqs-table.json +1 -1
  14. package/registry/blocks/filter-popover.json +1 -1
  15. package/registry/blocks/fixed-column-data-table.json +1 -1
  16. package/registry/blocks/infinity-canvas.json +1 -1
  17. package/registry/blocks/menu-section.json +1 -1
  18. package/registry/blocks/messenger-sidebar.json +1 -1
  19. package/registry/blocks/mobile-bottom-nav.json +1 -1
  20. package/registry/blocks/monthly-calendar-widget.json +1 -1
  21. package/registry/blocks/page-header-section.json +1 -1
  22. package/registry/blocks/page-previews.json +1 -1
  23. package/registry/blocks/pagination.json +1 -1
  24. package/registry/blocks/persona-card.json +1 -1
  25. package/registry/blocks/pill-tabs.json +1 -1
  26. package/registry/blocks/pricing-cards.json +1 -1
  27. package/registry/blocks/profile-card.json +1 -1
  28. package/registry/blocks/profile-info-cards.json +1 -1
  29. package/registry/blocks/prompt-template.json +1 -1
  30. package/registry/blocks/screen-flowchart.json +1 -1
  31. package/registry/blocks/screen-prompt-builder.json +1 -1
  32. package/registry/blocks/screen-prompt-template.json +1 -1
  33. package/registry/blocks/search-bar.json +1 -1
  34. package/registry/blocks/sidebar-cards.json +1 -1
  35. package/registry/blocks/sidebar-profile-card.json +1 -1
  36. package/registry/blocks/slideshow-grid-tiles.json +1 -1
  37. package/registry/blocks/social-feed.json +1 -1
  38. package/registry/blocks/step-tracker.json +1 -1
  39. package/registry/blocks/upvoting-posts-table.json +1 -1
  40. package/registry/blocks/vertical-step-tracker.json +1 -1
  41. package/registry/blocks/video-chat-controls.json +1 -1
  42. package/registry/layout/account-settings-shell.json +1 -1
  43. package/registry/layout/dashboard-shell.json +1 -1
  44. package/registry/layout/double-sidebar-shell.json +1 -1
  45. package/registry/layout/double-sidebar.json +1 -1
  46. package/registry/layout/header.json +1 -1
  47. package/registry/layout/icon-sidebar-shell.json +1 -1
  48. package/registry/layout/icon-sidebar.json +1 -1
  49. package/registry/layout/mobile-menu-shell.json +1 -1
  50. package/registry/layout/multistep-progressbar-shell.json +1 -1
  51. package/registry/layout/multistep-shell.json +1 -1
  52. package/registry/layout/multistep-sidebar-shell.json +1 -1
  53. package/registry/layout/project-context-shell.json +1 -1
  54. package/registry/layout/search-bar-shell.json +1 -1
  55. package/registry/layout/sidebar-nav.json +1 -1
  56. package/registry/layout/sidebar.json +1 -1
  57. package/registry/layout/standard-page-shell.json +1 -1
  58. package/registry/layout/vertical-multistep-shell.json +1 -1
  59. package/registry/ui/avatar.json +1 -1
  60. package/registry/ui/button.json +1 -1
  61. package/registry/ui/checkbox.json +1 -1
  62. package/registry/ui/date-input.json +1 -1
  63. package/registry/ui/image-uploader.json +1 -1
  64. package/registry/ui/line-tabs.json +1 -1
  65. package/registry/ui/multiselect-checkbox-field.json +1 -1
  66. package/registry/ui/multiselect-tags.json +1 -1
  67. package/registry/ui/radio-group.json +1 -1
  68. package/registry/ui/searchbox.json +1 -1
  69. package/registry/ui/select.json +1 -1
  70. package/registry/ui/selectable-pills.json +1 -1
  71. package/registry/ui/slider.json +1 -1
  72. package/registry/ui/switch.json +1 -1
  73. package/registry/ui/tabs.json +1 -1
  74. package/registry/ui/text-input.json +1 -1
  75. package/registry/ui/textarea.json +1 -1
  76. package/styles/tokens.reference.css +9 -0
@@ -6,7 +6,7 @@
6
6
  {
7
7
  "path": "components/blocks/vertical-step-tracker.tsx",
8
8
  "type": "registry:block",
9
- "content": "\"use client\";\n\nimport { cn } from \"../../lib/utils\";\nimport { Step } from \"./step-tracker\";\n\nexport interface VerticalStepTrackerProps {\n /** Array of step objects with id, label, and optional description */\n steps: Step[];\n /** Current active step (0-indexed) */\n currentStep: number;\n /** Optional callback when a step is clicked */\n onStepClick?: (stepIndex: number) => void;\n /** Optional section title (e.g., \"REGISTRATION\") */\n title?: string;\n /** Additional class name */\n className?: string;\n}\n\n/**\n * Canvas Design System - Vertical Step Tracker\n * \n * A vertical step progress indicator with:\n * - 24px circles with 8px white inner dot\n * - Vertical connecting lines between steps\n * - Active step: primary background + white dot + primary text\n * - Inactive steps: border background + white dot + placeholder text\n * - Optional section title (uppercase)\n * \n * @example\n * ```tsx\n * <VerticalStepTracker\n * steps={[\n * { id: \"step1\", label: \"Step 1\" },\n * { id: \"step2\", label: \"Step 2\" },\n * ]}\n * currentStep={0}\n * title=\"REGISTRATION\"\n * />\n * ```\n */\nexport function VerticalStepTracker({\n steps,\n currentStep,\n onStepClick,\n title,\n className,\n}: VerticalStepTrackerProps) {\n return (\n <div className={cn(\"flex flex-col gap-4\", className)}>\n {/* Optional section title */}\n {title && (\n <p\n className=\"uppercase\"\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 600,\n letterSpacing: \"var(--typo-body-s-spacing)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text-placeholder)\",\n }}\n >\n {title}\n </p>\n )}\n\n {/* Steps list */}\n <div className=\"flex flex-col\">\n {steps.map((step, index) => {\n const isActive = index === currentStep;\n const isCompleted = index < currentStep;\n const isLast = index === steps.length - 1;\n\n return (\n <div key={step.id} className=\"flex gap-2 items-start\">\n {/* Circle and line container */}\n <div className=\"flex flex-col items-center\">\n {/* Step circle - 24px with 8px inner dot */}\n <button\n type=\"button\"\n onClick={() => onStepClick?.(index)}\n disabled={!onStepClick}\n className={cn(\n \"relative flex items-center justify-center size-6 rounded-full shrink-0\",\n \"border-2 transition-colors\",\n onStepClick && \"cursor-pointer hover:opacity-80\",\n !onStepClick && \"cursor-default\",\n (isActive || isCompleted) && \"bg-[var(--canvas-primary)] border-[var(--canvas-primary)]\",\n !isActive && !isCompleted && \"bg-[var(--canvas-border)] border-[var(--canvas-border)]\"\n )}\n >\n {/* White inner dot - 8px */}\n <div className=\"size-2 rounded-full bg-white\" />\n </button>\n\n {/* Connecting line to next step */}\n {!isLast && (\n <div \n className=\"w-0.5 h-6 bg-[var(--canvas-border)]\"\n />\n )}\n </div>\n\n {/* Step label */}\n <button\n type=\"button\"\n onClick={() => onStepClick?.(index)}\n disabled={!onStepClick}\n className={cn(\n \"h-6 flex items-center\",\n onStepClick && \"cursor-pointer hover:opacity-80\",\n !onStepClick && \"cursor-default\"\n )}\n >\n <span\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 500,\n letterSpacing: \"var(--typo-body-s-spacing)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: (isActive || isCompleted) \n ? \"var(--canvas-primary)\" \n : \"var(--canvas-text-placeholder)\",\n }}\n >\n {step.label}\n </span>\n </button>\n </div>\n );\n })}\n </div>\n </div>\n );\n}\n\n/** Default steps for vertical tracker demo */\nexport const defaultVerticalSteps: Step[] = [\n { id: \"step-1\", label: \"Step 1\", description: \"Enter your basic information\" },\n { id: \"step-2\", label: \"Step 2\", description: \"Verify your identity\" },\n { id: \"step-3\", label: \"Step 3\", description: \"Set up your profile\" },\n { id: \"step-4\", label: \"Step 4\", description: \"Choose your preferences\" },\n { id: \"step-5\", label: \"Step 5\", description: \"Review your details\" },\n { id: \"step-6\", label: \"Step 6\", description: \"Complete registration\" },\n];\n\n\n\n\n\n\n\n\n"
9
+ "content": "\"use client\";\n\nimport { cn } from \"../../lib/utils\";\nimport { Step } from \"./step-tracker\";\n\nexport interface VerticalStepTrackerProps {\n /** Array of step objects with id, label, and optional description */\n steps: Step[];\n /** Current active step (0-indexed) */\n currentStep: number;\n /** Optional callback when a step is clicked */\n onStepClick?: (stepIndex: number) => void;\n /** Optional section title (e.g., \"REGISTRATION\") */\n title?: string;\n /** Additional class name */\n className?: string;\n}\n\n/**\n * Canvas Design System - Vertical Step Tracker\n * \n * A vertical step progress indicator with:\n * - 24px circles with 8px white inner dot\n * - Vertical connecting lines between steps\n * - Active step: primary background + white dot + primary text\n * - Inactive steps: border background + white dot + placeholder text\n * - Optional section title (uppercase)\n * \n * @example\n * ```tsx\n * <VerticalStepTracker\n * steps={[\n * { id: \"step1\", label: \"Step 1\" },\n * { id: \"step2\", label: \"Step 2\" },\n * ]}\n * currentStep={0}\n * title=\"REGISTRATION\"\n * />\n * ```\n */\nexport function VerticalStepTracker({\n steps,\n currentStep,\n onStepClick,\n title,\n className,\n}: VerticalStepTrackerProps) {\n return (\n <div className={cn(\"flex flex-col gap-4\", className)}>\n {/* Optional section title */}\n {title && (\n <p\n className=\"uppercase\"\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 600,\n letterSpacing: \"var(--typo-body-s-spacing)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: \"var(--canvas-text-placeholder)\",\n }}\n >\n {title}\n </p>\n )}\n\n {/* Steps list */}\n <div className=\"flex flex-col\">\n {steps.map((step, index) => {\n const isActive = index === currentStep;\n const isCompleted = index < currentStep;\n const isLast = index === steps.length - 1;\n\n return (\n <div key={step.id} className=\"flex gap-2 items-start\">\n {/* Circle and line container */}\n <div className=\"flex flex-col items-center\">\n {/* Step circle - 24px with 8px inner dot */}\n <button\n type=\"button\"\n onClick={() => onStepClick?.(index)}\n disabled={!onStepClick}\n className={cn(\n \"relative flex items-center justify-center size-6 rounded-full shrink-0\",\n \"border-2 transition-colors\",\n onStepClick && \"cursor-pointer hover:opacity-80\",\n !onStepClick && \"cursor-default\",\n (isActive || isCompleted) && \"bg-[var(--canvas-primary)] border-[var(--canvas-primary)]\",\n !isActive && !isCompleted && \"bg-[var(--canvas-border)] border-[var(--canvas-border)]\"\n )}\n >\n {/* White inner dot - 8px */}\n <div className=\"size-2 rounded-full bg-[var(--canvas-background)]\" />\n </button>\n\n {/* Connecting line to next step */}\n {!isLast && (\n <div \n className=\"w-0.5 h-6 bg-[var(--canvas-border)]\"\n />\n )}\n </div>\n\n {/* Step label */}\n <button\n type=\"button\"\n onClick={() => onStepClick?.(index)}\n disabled={!onStepClick}\n className={cn(\n \"h-6 flex items-center\",\n onStepClick && \"cursor-pointer hover:opacity-80\",\n !onStepClick && \"cursor-default\"\n )}\n >\n <span\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size)\",\n fontWeight: 500,\n letterSpacing: \"var(--typo-body-s-spacing)\",\n lineHeight: \"var(--typo-body-s-line-height)\",\n color: (isActive || isCompleted) \n ? \"var(--canvas-primary)\" \n : \"var(--canvas-text-placeholder)\",\n }}\n >\n {step.label}\n </span>\n </button>\n </div>\n );\n })}\n </div>\n </div>\n );\n}\n\n/** Default steps for vertical tracker demo */\nexport const defaultVerticalSteps: Step[] = [\n { id: \"step-1\", label: \"Step 1\", description: \"Enter your basic information\" },\n { id: \"step-2\", label: \"Step 2\", description: \"Verify your identity\" },\n { id: \"step-3\", label: \"Step 3\", description: \"Set up your profile\" },\n { id: \"step-4\", label: \"Step 4\", description: \"Choose your preferences\" },\n { id: \"step-5\", label: \"Step 5\", description: \"Review your details\" },\n { id: \"step-6\", label: \"Step 6\", description: \"Complete registration\" },\n];\n\n\n\n\n\n\n\n\n"
10
10
  }
11
11
  ],
12
12
  "dependencies": [],
@@ -6,7 +6,7 @@
6
6
  {
7
7
  "path": "components/blocks/video-chat-controls.tsx",
8
8
  "type": "registry:block",
9
- "content": "\"use client\";\n\nimport { cn } from \"../../lib/utils\";\nimport { Video, VideoOff, Mic, MicOff, Cast, LogOut } from \"lucide-react\";\n\ninterface VideoChatControlsProps {\n /** Whether the video is currently on */\n isVideoOn?: boolean;\n /** Whether the mic is currently on */\n isMicOn?: boolean;\n /** Callback when video toggle is clicked */\n onToggleVideo?: () => void;\n /** Callback when mic toggle is clicked */\n onToggleMic?: () => void;\n /** Callback when cast button is clicked */\n onCast?: () => void;\n /** Callback when leave button is clicked */\n onLeave?: () => void;\n /** Additional class names */\n className?: string;\n}\n\ninterface ControlButtonProps {\n icon: React.ReactNode;\n onClick?: () => void;\n isActive?: boolean;\n label: string;\n}\n\nfunction ControlButton({ icon, onClick, isActive = true, label }: ControlButtonProps) {\n return (\n <button\n type=\"button\"\n onClick={onClick}\n aria-label={label}\n className={cn(\n \"w-10 h-10 rounded-full flex items-center justify-center\",\n \"shadow-sm transition-colors\",\n isActive\n ? \"bg-[var(--canvas-sidebar-light-text)] text-white hover:opacity-80\"\n : \"bg-[var(--canvas-destructive)] text-white hover:opacity-80\"\n )}\n >\n {icon}\n </button>\n );\n}\n\n/**\n * Canvas Design System - Video Chat Controls Component\n * \n * Row of circular control buttons for video chat: Camera, Mic, Cast, Leave.\n * \n * @example\n * ```tsx\n * <VideoChatControls\n * isVideoOn={true}\n * isMicOn={true}\n * onToggleVideo={() => setVideoOn(!videoOn)}\n * onToggleMic={() => setMicOn(!micOn)}\n * onLeave={() => router.push('/')}\n * />\n * ```\n */\nexport function VideoChatControls({\n isVideoOn = true,\n isMicOn = true,\n onToggleVideo,\n onToggleMic,\n onCast,\n onLeave,\n className,\n}: VideoChatControlsProps) {\n return (\n <div className={cn(\"flex items-center gap-2\", className)}>\n <ControlButton\n icon={isVideoOn ? <Video className=\"w-5 h-5\" /> : <VideoOff className=\"w-5 h-5\" />}\n onClick={onToggleVideo}\n isActive={isVideoOn}\n label={isVideoOn ? \"Turn off camera\" : \"Turn on camera\"}\n />\n <ControlButton\n icon={isMicOn ? <Mic className=\"w-5 h-5\" /> : <MicOff className=\"w-5 h-5\" />}\n onClick={onToggleMic}\n isActive={isMicOn}\n label={isMicOn ? \"Mute microphone\" : \"Unmute microphone\"}\n />\n <ControlButton\n icon={<Cast className=\"w-5 h-5\" />}\n onClick={onCast}\n label=\"Cast to device\"\n />\n <ControlButton\n icon={<LogOut className=\"w-5 h-5\" />}\n onClick={onLeave}\n label=\"Leave call\"\n />\n </div>\n );\n}\n\n"
9
+ "content": "\"use client\";\n\nimport { cn } from \"../../lib/utils\";\nimport { Video, VideoOff, Mic, MicOff, Cast, LogOut } from \"lucide-react\";\n\ninterface VideoChatControlsProps {\n /** Whether the video is currently on */\n isVideoOn?: boolean;\n /** Whether the mic is currently on */\n isMicOn?: boolean;\n /** Callback when video toggle is clicked */\n onToggleVideo?: () => void;\n /** Callback when mic toggle is clicked */\n onToggleMic?: () => void;\n /** Callback when cast button is clicked */\n onCast?: () => void;\n /** Callback when leave button is clicked */\n onLeave?: () => void;\n /** Additional class names */\n className?: string;\n}\n\ninterface ControlButtonProps {\n icon: React.ReactNode;\n onClick?: () => void;\n isActive?: boolean;\n label: string;\n}\n\nfunction ControlButton({ icon, onClick, isActive = true, label }: ControlButtonProps) {\n return (\n <button\n type=\"button\"\n onClick={onClick}\n aria-label={label}\n className={cn(\n \"w-10 h-10 rounded-full flex items-center justify-center\",\n \"shadow-sm transition-colors\",\n isActive\n ? \"bg-[var(--canvas-sidebar-light-text)] text-[var(--canvas-primary-foreground)] hover:opacity-80\"\n : \"bg-[var(--canvas-destructive)] text-[var(--canvas-primary-foreground)] hover:opacity-80\"\n )}\n >\n {icon}\n </button>\n );\n}\n\n/**\n * Canvas Design System - Video Chat Controls Component\n * \n * Row of circular control buttons for video chat: Camera, Mic, Cast, Leave.\n * \n * @example\n * ```tsx\n * <VideoChatControls\n * isVideoOn={true}\n * isMicOn={true}\n * onToggleVideo={() => setVideoOn(!videoOn)}\n * onToggleMic={() => setMicOn(!micOn)}\n * onLeave={() => router.push('/')}\n * />\n * ```\n */\nexport function VideoChatControls({\n isVideoOn = true,\n isMicOn = true,\n onToggleVideo,\n onToggleMic,\n onCast,\n onLeave,\n className,\n}: VideoChatControlsProps) {\n return (\n <div className={cn(\"flex items-center gap-2\", className)}>\n <ControlButton\n icon={isVideoOn ? <Video className=\"w-5 h-5\" /> : <VideoOff className=\"w-5 h-5\" />}\n onClick={onToggleVideo}\n isActive={isVideoOn}\n label={isVideoOn ? \"Turn off camera\" : \"Turn on camera\"}\n />\n <ControlButton\n icon={isMicOn ? <Mic className=\"w-5 h-5\" /> : <MicOff className=\"w-5 h-5\" />}\n onClick={onToggleMic}\n isActive={isMicOn}\n label={isMicOn ? \"Mute microphone\" : \"Unmute microphone\"}\n />\n <ControlButton\n icon={<Cast className=\"w-5 h-5\" />}\n onClick={onCast}\n label=\"Cast to device\"\n />\n <ControlButton\n icon={<LogOut className=\"w-5 h-5\" />}\n onClick={onLeave}\n label=\"Leave call\"\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/account-settings-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 { User, Key, CreditCard, Bell, LucideIcon } from \"lucide-react\";\n\nexport type AccountTab = \"profile\" | \"security\" | \"payments\" | \"notifications\";\n\ninterface AccountTabConfig {\n id: AccountTab;\n label: string;\n icon: LucideIcon;\n}\n\nexport const accountTabs: AccountTabConfig[] = [\n { id: \"profile\", label: \"Profile\", icon: User },\n { id: \"security\", label: \"Login and security\", icon: Key },\n { id: \"payments\", label: \"Payments\", icon: CreditCard },\n { id: \"notifications\", label: \"Notifications\", icon: Bell },\n];\n\ninterface AccountSettingsShellProps {\n /** Currently active tab */\n activeTab?: AccountTab;\n /** Callback when tab changes */\n onTabChange?: (tab: AccountTab) => void;\n /** Main content - renders based on active tab */\n children: React.ReactNode;\n /** Page title */\n pageTitle?: string;\n}\n\n/**\n * Canvas Design System - Account Settings Shell\n * \n * A layout for account/settings pages with:\n * - Fixed header with logo (no sidebar)\n * - Page title section with bottom border\n * - Two-column layout: sidebar navigation (320px) + content area\n * - Mobile responsive: sidebar stacks above content\n * \n * @example\n * ```tsx\n * <AccountSettingsShell\n * activeTab={activeTab}\n * onTabChange={setActiveTab}\n * >\n * {activeTab === \"profile\" && <ProfileContent />}\n * {activeTab === \"security\" && <SecurityContent />}\n * </AccountSettingsShell>\n * ```\n */\nexport function AccountSettingsShell({\n activeTab: controlledActiveTab,\n onTabChange,\n children,\n pageTitle = \"Account settings\",\n}: AccountSettingsShellProps) {\n // Sync CSS variables when rendered in iframes\n useCSSVariableSync();\n\n // Internal state for uncontrolled mode\n const [internalActiveTab, setInternalActiveTab] = useState<AccountTab>(\"profile\");\n \n // Use controlled or uncontrolled mode\n const activeTab = controlledActiveTab ?? internalActiveTab;\n \n const handleTabChange = (tab: AccountTab) => {\n if (onTabChange) {\n onTabChange(tab);\n } else {\n setInternalActiveTab(tab);\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 {/* Page Title Section */}\n <div className=\"w-full border-b border-[var(--canvas-neutral-border)]\">\n <div className=\"w-full max-w-[1200px] mx-auto px-[var(--spacing-xl)] py-[var(--spacing-6xl)]\">\n <h1\n style={{\n fontFamily: \"var(--typo-page-title-font)\",\n fontSize: \"var(--typo-page-title-size)\",\n fontWeight: \"var(--typo-page-title-weight)\",\n lineHeight: \"var(--typo-page-title-line-height)\",\n letterSpacing: \"var(--typo-page-title-spacing)\",\n color: \"var(--typo-page-title-color)\",\n }}\n >\n {pageTitle}\n </h1>\n </div>\n </div>\n\n {/* Main Content - Two Column Layout */}\n <main className=\"w-full flex justify-center pb-[var(--spacing-8xl)]\">\n <div className=\"w-full max-w-[1200px] px-[var(--spacing-xl)] pt-[var(--spacing-5xl)]\">\n <div className=\"flex flex-col lg:flex-row gap-[var(--spacing-5xl)]\">\n {/* Sidebar Navigation */}\n <aside className=\"w-full lg:w-[320px] shrink-0\">\n <div className=\"bg-white border border-[var(--canvas-neutral-border)] rounded-[var(--radius-lg)] p-[var(--spacing-xl)] lg:p-[var(--spacing-4xl)]\">\n <nav className=\"flex flex-col gap-0\">\n {accountTabs.map((tab) => {\n const Icon = tab.icon;\n const isActive = activeTab === tab.id;\n \n return (\n <button\n key={tab.id}\n onClick={() => handleTabChange(tab.id)}\n className={cn(\n \"flex items-center gap-[var(--spacing-md)] h-11 px-[var(--spacing-xl)] rounded-[var(--radius-md)] w-full text-left transition-colors\",\n isActive\n ? \"bg-[var(--canvas-surface-brand)] text-[var(--canvas-primary)]\"\n : \"text-[var(--canvas-neutral-text)] hover:bg-[var(--canvas-surface)]\"\n )}\n >\n <Icon \n className={cn(\n \"size-5 shrink-0\",\n isActive ? \"text-[var(--canvas-primary)]\" : \"text-[var(--canvas-neutral-text)]\"\n )} \n />\n <span\n className=\"flex-1 truncate\"\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size, 14px)\",\n fontWeight: 500,\n lineHeight: \"var(--typo-body-s-line-height, 20px)\",\n }}\n >\n {tab.label}\n </span>\n </button>\n );\n })}\n </nav>\n </div>\n </aside>\n\n {/* Content Area */}\n <div className=\"flex-1 min-w-0\">\n {children}\n </div>\n </div>\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 { User, Key, CreditCard, Bell, LucideIcon } from \"lucide-react\";\n\nexport type AccountTab = \"profile\" | \"security\" | \"payments\" | \"notifications\";\n\ninterface AccountTabConfig {\n id: AccountTab;\n label: string;\n icon: LucideIcon;\n}\n\nexport const accountTabs: AccountTabConfig[] = [\n { id: \"profile\", label: \"Profile\", icon: User },\n { id: \"security\", label: \"Login and security\", icon: Key },\n { id: \"payments\", label: \"Payments\", icon: CreditCard },\n { id: \"notifications\", label: \"Notifications\", icon: Bell },\n];\n\ninterface AccountSettingsShellProps {\n /** Currently active tab */\n activeTab?: AccountTab;\n /** Callback when tab changes */\n onTabChange?: (tab: AccountTab) => void;\n /** Main content - renders based on active tab */\n children: React.ReactNode;\n /** Page title */\n pageTitle?: string;\n}\n\n/**\n * Canvas Design System - Account Settings Shell\n * \n * A layout for account/settings pages with:\n * - Fixed header with logo (no sidebar)\n * - Page title section with bottom border\n * - Two-column layout: sidebar navigation (320px) + content area\n * - Mobile responsive: sidebar stacks above content\n * \n * @example\n * ```tsx\n * <AccountSettingsShell\n * activeTab={activeTab}\n * onTabChange={setActiveTab}\n * >\n * {activeTab === \"profile\" && <ProfileContent />}\n * {activeTab === \"security\" && <SecurityContent />}\n * </AccountSettingsShell>\n * ```\n */\nexport function AccountSettingsShell({\n activeTab: controlledActiveTab,\n onTabChange,\n children,\n pageTitle = \"Account settings\",\n}: AccountSettingsShellProps) {\n // Sync CSS variables when rendered in iframes\n useCSSVariableSync();\n\n // Internal state for uncontrolled mode\n const [internalActiveTab, setInternalActiveTab] = useState<AccountTab>(\"profile\");\n \n // Use controlled or uncontrolled mode\n const activeTab = controlledActiveTab ?? internalActiveTab;\n \n const handleTabChange = (tab: AccountTab) => {\n if (onTabChange) {\n onTabChange(tab);\n } else {\n setInternalActiveTab(tab);\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 {/* Page Title Section */}\n <div className=\"w-full border-b border-[var(--canvas-neutral-border)]\">\n <div className=\"w-full max-w-[1200px] mx-auto px-[var(--spacing-xl)] py-[var(--spacing-6xl)]\">\n <h1\n style={{\n fontFamily: \"var(--typo-h4-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-h4-size)\",\n fontWeight: \"var(--typo-h4-weight)\",\n lineHeight: \"var(--typo-h4-line-height)\",\n letterSpacing: \"var(--typo-h4-spacing)\",\n color: \"var(--typo-h4-color)\",\n }}\n >\n {pageTitle}\n </h1>\n </div>\n </div>\n\n {/* Main Content - Two Column Layout */}\n <main className=\"w-full flex justify-center pb-[var(--spacing-8xl)]\">\n <div className=\"w-full max-w-[1200px] px-[var(--spacing-xl)] pt-[var(--spacing-5xl)]\">\n <div className=\"flex flex-col lg:flex-row gap-[var(--spacing-5xl)]\">\n {/* Sidebar Navigation */}\n <aside className=\"w-full lg:w-[320px] shrink-0\">\n <div className=\"bg-[var(--canvas-background)] border border-[var(--canvas-neutral-border)] rounded-[var(--radius-lg)] p-[var(--spacing-xl)] lg:p-[var(--spacing-4xl)]\">\n <nav className=\"flex flex-col gap-0\">\n {accountTabs.map((tab) => {\n const Icon = tab.icon;\n const isActive = activeTab === tab.id;\n \n return (\n <button\n key={tab.id}\n onClick={() => handleTabChange(tab.id)}\n className={cn(\n \"flex items-center gap-[var(--spacing-md)] h-11 px-[var(--spacing-xl)] rounded-[var(--radius-md)] w-full text-left transition-colors\",\n isActive\n ? \"bg-[var(--canvas-surface-brand)] text-[var(--canvas-primary)]\"\n : \"text-[var(--canvas-neutral-text)] hover:bg-[var(--canvas-surface)]\"\n )}\n >\n <Icon \n className={cn(\n \"size-5 shrink-0\",\n isActive ? \"text-[var(--canvas-primary)]\" : \"text-[var(--canvas-neutral-text)]\"\n )} \n />\n <span\n className=\"flex-1 truncate\"\n style={{\n fontFamily: \"var(--typo-body-s-font, var(--typo-global-font))\",\n fontSize: \"var(--typo-body-s-size, 14px)\",\n fontWeight: 500,\n lineHeight: \"var(--typo-body-s-line-height, 20px)\",\n }}\n >\n {tab.label}\n </span>\n </button>\n );\n })}\n </nav>\n </div>\n </aside>\n\n {/* Content Area */}\n <div className=\"flex-1 min-w-0\">\n {children}\n </div>\n </div>\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/dashboard-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 { Sidebar, NavSection, NavItem } from \"./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 DashboardShellProps {\n /** Navigation sections for the sidebar */\n navigation: NavSection[];\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 or subtab is clicked */\n onNavItemClick?: (item: NavItem | Omit<NavItem, \"children\" | \"icon\">) => 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 - Dashboard Shell\n * \n * A composable page layout that provides:\n * - Fixed header (80px)\n * - Fixed dark sidebar on desktop (320px, hidden on mobile)\n * - Floating sidebar toggle button on mobile (left edge)\n * - Mobile sheet navigation for dashboard 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 * @example\n * ```tsx\n * <DashboardShell \n * navigation={navSections}\n * pageHeader={<Breadcrumbs />}\n * >\n * <StatsBlock />\n * <ChartBlock />\n * <TableBlock />\n * </DashboardShell>\n * ```\n */\nexport function DashboardShell({\n navigation,\n pageHeader,\n children,\n onNavItemClick,\n onAppMenuClick,\n contentClassName,\n}: DashboardShellProps) {\n useCSSVariableSync();\n const [sidebarOpen, setSidebarOpen] = useState(false);\n\n const handleNavItemClick = (item: NavItem | Omit<NavItem, \"children\" | \"icon\">) => {\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 sidebar */}\n <div className=\"fixed top-0 left-0 right-0 lg:left-[var(--sidebar-width)] z-40\">\n <Header onMenuClick={handleAppMenuClick} />\n </div>\n\n {/* Desktop Sidebar - Fixed on left, visible lg+ */}\n <div className=\"hidden lg:block fixed top-0 left-0 bottom-0 z-50 w-[var(--sidebar-width)]\">\n <Sidebar \n sections={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-white\",\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 Sidebar Sheet */}\n <Sheet open={sidebarOpen} onOpenChange={setSidebarOpen}>\n <SheetContent side=\"left\" className=\"p-0 w-[var(--sidebar-width)]\">\n <VisuallyHidden.Root>\n <SheetTitle>Dashboard Navigation</SheetTitle>\n </VisuallyHidden.Root>\n <Sidebar \n sections={navigation} \n variant=\"dark\" \n onItemClick={handleNavItemClick}\n />\n </SheetContent>\n </Sheet>\n\n {/* Main Content Area */}\n <main\n className={cn(\n \"pt-[var(--header-height)]\",\n \"lg:pl-[var(--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 { NavSection, NavItem } from \"./sidebar\";\n\n"
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 { Sidebar, NavSection, NavItem } from \"./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 DashboardShellProps {\n /** Navigation sections for the sidebar */\n navigation: NavSection[];\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 or subtab is clicked */\n onNavItemClick?: (item: NavItem | Omit<NavItem, \"children\" | \"icon\">) => 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 - Dashboard Shell\n * \n * A composable page layout that provides:\n * - Fixed header (80px)\n * - Fixed dark sidebar on desktop (320px, hidden on mobile)\n * - Floating sidebar toggle button on mobile (left edge)\n * - Mobile sheet navigation for dashboard 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 * @example\n * ```tsx\n * <DashboardShell \n * navigation={navSections}\n * pageHeader={<Breadcrumbs />}\n * >\n * <StatsBlock />\n * <ChartBlock />\n * <TableBlock />\n * </DashboardShell>\n * ```\n */\nexport function DashboardShell({\n navigation,\n pageHeader,\n children,\n onNavItemClick,\n onAppMenuClick,\n contentClassName,\n}: DashboardShellProps) {\n useCSSVariableSync();\n const [sidebarOpen, setSidebarOpen] = useState(false);\n\n const handleNavItemClick = (item: NavItem | Omit<NavItem, \"children\" | \"icon\">) => {\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 sidebar */}\n <div className=\"fixed top-0 left-0 right-0 lg:left-[var(--sidebar-width)] z-40\">\n <Header onMenuClick={handleAppMenuClick} />\n </div>\n\n {/* Desktop Sidebar - Fixed on left, visible lg+ */}\n <div className=\"hidden lg:block fixed top-0 left-0 bottom-0 z-50 w-[var(--sidebar-width)]\">\n <Sidebar \n sections={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 Sidebar Sheet */}\n <Sheet open={sidebarOpen} onOpenChange={setSidebarOpen}>\n <SheetContent side=\"left\" className=\"p-0 w-[var(--sidebar-width)]\">\n <VisuallyHidden.Root>\n <SheetTitle>Dashboard Navigation</SheetTitle>\n </VisuallyHidden.Root>\n <Sidebar \n sections={navigation} \n variant=\"dark\" \n onItemClick={handleNavItemClick}\n />\n </SheetContent>\n </Sheet>\n\n {/* Main Content Area */}\n <main\n className={cn(\n \"pt-[var(--header-height)]\",\n \"lg:pl-[var(--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 { NavSection, NavItem } from \"./sidebar\";\n\n"
10
10
  }
11
11
  ],
12
12
  "dependencies": [
@@ -6,7 +6,7 @@
6
6
  {
7
7
  "path": "components/layout/double-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 { DoubleSidebar, DoubleSidebarSection, NavTab, defaultDoubleSidebarSections } from \"./double-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 DoubleSidebarShellProps {\n /** Navigation sections for the double sidebar */\n sections?: DoubleSidebarSection[];\n /** Visual variant for the icon column */\n iconVariant?: \"dark\" | \"light\";\n /** Visual variant for the navigation column */\n navVariant?: \"dark\" | \"light\";\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 tab is clicked */\n onTabClick?: (section: DoubleSidebarSection, tab: NavTab) => 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 - Double Sidebar Shell\n * \n * A composable page layout with a two-column sidebar that provides:\n * - Fixed header (80px)\n * - Fixed double sidebar on desktop (96px icons + 280px nav = 376px total)\n * - Floating sidebar toggle button on mobile (left edge)\n * - Mobile sheet navigation with both sidebar columns\n * - Each sidebar column can be independently themed (light/dark)\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 * <DoubleSidebarShell \n * sections={sections}\n * iconVariant=\"light\" \n * navVariant=\"light\"\n * >\n * <ContentDropzone label=\"Main content area\" />\n * </DoubleSidebarShell>\n * ```\n */\nexport function DoubleSidebarShell({\n sections = defaultDoubleSidebarSections,\n iconVariant = \"light\",\n navVariant = \"light\",\n pageHeader,\n children,\n onTabClick,\n onAppMenuClick,\n contentClassName,\n}: DoubleSidebarShellProps) {\n useCSSVariableSync();\n const [sidebarOpen, setSidebarOpen] = useState(false);\n\n const handleTabClick = (section: DoubleSidebarSection, tab: NavTab) => {\n onTabClick?.(section, tab);\n // Close sidebar when tab 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 double sidebar */}\n <div className=\"fixed top-0 left-0 right-0 lg:left-[var(--double-sidebar-width)] z-40\">\n <Header onMenuClick={handleAppMenuClick} />\n </div>\n\n {/* Desktop Double Sidebar - Fixed on left, visible lg+ */}\n <div className=\"hidden lg:block fixed top-0 left-0 bottom-0 z-50 w-[var(--double-sidebar-width)]\">\n <DoubleSidebar \n sections={sections}\n iconVariant={iconVariant}\n navVariant={navVariant}\n onTabClick={handleTabClick}\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-white\",\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 Double Sidebar Sheet */}\n <Sheet open={sidebarOpen} onOpenChange={setSidebarOpen}>\n <SheetContent side=\"left\" className=\"p-0 w-[var(--double-sidebar-width)]\">\n <VisuallyHidden.Root>\n <SheetTitle>Navigation</SheetTitle>\n </VisuallyHidden.Root>\n <DoubleSidebar \n sections={sections}\n iconVariant={iconVariant}\n navVariant={navVariant}\n onTabClick={handleTabClick}\n onClose={() => setSidebarOpen(false)}\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(--double-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 { DoubleSidebarSection, NavTab } from \"./double-sidebar\";\n\n\n"
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 { DoubleSidebar, DoubleSidebarSection, NavTab, defaultDoubleSidebarSections } from \"./double-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 DoubleSidebarShellProps {\n /** Navigation sections for the double sidebar */\n sections?: DoubleSidebarSection[];\n /** Visual variant for the icon column */\n iconVariant?: \"dark\" | \"light\";\n /** Visual variant for the navigation column */\n navVariant?: \"dark\" | \"light\";\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 tab is clicked */\n onTabClick?: (section: DoubleSidebarSection, tab: NavTab) => 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 - Double Sidebar Shell\n * \n * A composable page layout with a two-column sidebar that provides:\n * - Fixed header (80px)\n * - Fixed double sidebar on desktop (96px icons + 280px nav = 376px total)\n * - Floating sidebar toggle button on mobile (left edge)\n * - Mobile sheet navigation with both sidebar columns\n * - Each sidebar column can be independently themed (light/dark)\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 * <DoubleSidebarShell \n * sections={sections}\n * iconVariant=\"light\" \n * navVariant=\"light\"\n * >\n * <ContentDropzone label=\"Main content area\" />\n * </DoubleSidebarShell>\n * ```\n */\nexport function DoubleSidebarShell({\n sections = defaultDoubleSidebarSections,\n iconVariant = \"light\",\n navVariant = \"light\",\n pageHeader,\n children,\n onTabClick,\n onAppMenuClick,\n contentClassName,\n}: DoubleSidebarShellProps) {\n useCSSVariableSync();\n const [sidebarOpen, setSidebarOpen] = useState(false);\n\n const handleTabClick = (section: DoubleSidebarSection, tab: NavTab) => {\n onTabClick?.(section, tab);\n // Close sidebar when tab 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 double sidebar */}\n <div className=\"fixed top-0 left-0 right-0 lg:left-[var(--double-sidebar-width)] z-40\">\n <Header onMenuClick={handleAppMenuClick} />\n </div>\n\n {/* Desktop Double Sidebar - Fixed on left, visible lg+ */}\n <div className=\"hidden lg:block fixed top-0 left-0 bottom-0 z-50 w-[var(--double-sidebar-width)]\">\n <DoubleSidebar \n sections={sections}\n iconVariant={iconVariant}\n navVariant={navVariant}\n onTabClick={handleTabClick}\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 Double Sidebar Sheet */}\n <Sheet open={sidebarOpen} onOpenChange={setSidebarOpen}>\n <SheetContent side=\"left\" className=\"p-0 w-[var(--double-sidebar-width)]\">\n <VisuallyHidden.Root>\n <SheetTitle>Navigation</SheetTitle>\n </VisuallyHidden.Root>\n <DoubleSidebar \n sections={sections}\n iconVariant={iconVariant}\n navVariant={navVariant}\n onTabClick={handleTabClick}\n onClose={() => setSidebarOpen(false)}\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(--double-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 { DoubleSidebarSection, NavTab } from \"./double-sidebar\";\n\n\n"
10
10
  }
11
11
  ],
12
12
  "dependencies": [
@@ -6,7 +6,7 @@
6
6
  {
7
7
  "path": "components/layout/double-sidebar.tsx",
8
8
  "type": "registry:layout",
9
- "content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport { LucideIcon, Home, Users, Calendar, MessageSquare, PieChart, FileText, ShoppingBag } from \"lucide-react\";\nimport { ScrollArea } from \"../ui/scroll-area\";\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// Types\n// ============================================\n\nexport interface NavTab {\n id: string;\n label: string;\n isActive?: boolean;\n}\n\nexport interface DoubleSidebarSection {\n id: string;\n icon: LucideIcon;\n label: string;\n badge?: boolean;\n tabs: NavTab[];\n}\n\n// ============================================\n// Default Sections\n// ============================================\n\nexport const defaultDoubleSidebarSections: DoubleSidebarSection[] = [\n {\n id: \"home\",\n icon: Home,\n label: \"Home\",\n tabs: [\n { id: \"tab1\", label: \"Tab 1\", isActive: true },\n { id: \"tab2\", label: \"Tab 2\" },\n { id: \"tab3\", label: \"Tab 3\" },\n { id: \"tab4\", label: \"Tab 4\" },\n { id: \"tab5\", label: \"Tab 5\" },\n { id: \"tab6\", label: \"Tab 6\" },\n ],\n },\n {\n id: \"teams\",\n icon: Users,\n label: \"Teams\",\n tabs: [\n { id: \"all-teams\", label: \"All Teams\" },\n { id: \"my-team\", label: \"My Team\" },\n { id: \"invites\", label: \"Invites\" },\n ],\n },\n {\n id: \"calendar\",\n icon: Calendar,\n label: \"Calendar\",\n tabs: [\n { id: \"schedule\", label: \"Schedule\" },\n { id: \"events\", label: \"Events\" },\n { id: \"reminders\", label: \"Reminders\" },\n ],\n },\n {\n id: \"messages\",\n icon: MessageSquare,\n label: \"Messages\",\n badge: true,\n tabs: [\n { id: \"inbox\", label: \"Inbox\" },\n { id: \"sent\", label: \"Sent\" },\n { id: \"drafts\", label: \"Drafts\" },\n { id: \"archived\", label: \"Archived\" },\n ],\n },\n {\n id: \"reports\",\n icon: PieChart,\n label: \"Reports\",\n tabs: [\n { id: \"overview\", label: \"Overview\" },\n { id: \"analytics\", label: \"Analytics\" },\n { id: \"exports\", label: \"Exports\" },\n ],\n },\n {\n id: \"docs\",\n icon: FileText,\n label: \"Docs\",\n tabs: [\n { id: \"recent\", label: \"Recent\" },\n { id: \"shared\", label: \"Shared\" },\n { id: \"favorites\", label: \"Favorites\" },\n ],\n },\n {\n id: \"orders\",\n icon: ShoppingBag,\n label: \"Orders\",\n tabs: [\n { id: \"pending\", label: \"Pending\" },\n { id: \"completed\", label: \"Completed\" },\n { id: \"cancelled\", label: \"Cancelled\" },\n ],\n },\n];\n\n// ============================================\n// Icon Column Item\n// ============================================\n\ninterface IconColumnItemProps {\n section: DoubleSidebarSection;\n isActive: boolean;\n variant: \"dark\" | \"light\";\n onClick: () => void;\n}\n\nfunction IconColumnItem({ section, isActive, variant, onClick }: IconColumnItemProps) {\n const Icon = section.icon;\n const isDark = variant === \"dark\";\n\n return (\n <button\n onClick={onClick}\n className={cn(\n \"relative flex flex-col items-center justify-center gap-1 w-11 h-11 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 {/* Notification Badge */}\n {section.badge && (\n <div className=\"absolute top-1.5 right-1.5 size-1.5 rounded-full bg-[var(--canvas-destructive)]\" />\n )}\n </button>\n );\n}\n\n// ============================================\n// Nav Column Tab Item\n// ============================================\n\ninterface NavTabItemProps {\n tab: NavTab;\n isActive: boolean;\n variant: \"dark\" | \"light\";\n onClick: () => void;\n}\n\nfunction NavTabItem({ tab, isActive, variant, onClick }: NavTabItemProps) {\n const isDark = variant === \"dark\";\n\n return (\n <button\n onClick={onClick}\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\n isDark && isActive && \"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 // Light variant\n !isDark && isActive && \"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 )}\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 {tab.label}\n </span>\n </button>\n );\n}\n\n// ============================================\n// Double Sidebar Component\n// ============================================\n\ninterface DoubleSidebarProps {\n /** Navigation sections - each section maps to an icon and has tabs */\n sections?: DoubleSidebarSection[];\n /** Visual variant for the icon column */\n iconVariant?: \"dark\" | \"light\";\n /** Visual variant for the navigation column */\n navVariant?: \"dark\" | \"light\";\n /** Callback when a tab is clicked */\n onTabClick?: (section: DoubleSidebarSection, tab: NavTab) => void;\n /** Callback when closing (for mobile sheet) */\n onClose?: () => void;\n /** Additional class names */\n className?: string;\n}\n\n/**\n * Canvas Design System - Double Sidebar Component\n * \n * A two-column sidebar where:\n * - Left column (96px): Icon buttons that switch the active section\n * - Right column (280px): Text-based navigation tabs for the active section\n * \n * Each column can be independently themed (light/dark).\n */\nexport function DoubleSidebar({\n sections = defaultDoubleSidebarSections,\n iconVariant = \"light\",\n navVariant = \"light\",\n onTabClick,\n onClose,\n className\n}: DoubleSidebarProps) {\n const [activeSectionId, setActiveSectionId] = useState(sections[0]?.id || \"\");\n const [activeTabId, setActiveTabId] = useState(sections[0]?.tabs[0]?.id || \"\");\n \n const themeImages = useThemeImages();\n const { branding, isMounted } = useThemeBranding();\n\n const activeSection = sections.find(s => s.id === activeSectionId);\n const isIconDark = iconVariant === \"dark\";\n const isNavDark = navVariant === \"dark\";\n\n // Get the appropriate logo based on icon column variant\n const logoUrl = isIconDark ? 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 const handleSectionClick = (section: DoubleSidebarSection) => {\n setActiveSectionId(section.id);\n // Set first tab of the section as active\n if (section.tabs.length > 0) {\n setActiveTabId(section.tabs[0].id);\n }\n };\n\n const handleTabClick = (tab: NavTab) => {\n setActiveTabId(tab.id);\n if (activeSection) {\n onTabClick?.(activeSection, tab);\n onClose?.();\n }\n };\n\n return (\n <div className={cn(\"flex h-full\", className)}>\n {/* Icon Column (96px) */}\n <div\n className={cn(\n \"flex flex-col items-center w-[var(--icon-sidebar-width)] shrink-0\",\n isIconDark && \"bg-[var(--canvas-sidebar-dark-bg)] border-r border-[var(--canvas-sidebar-dark-border)]\",\n !isIconDark && \"bg-white border-r border-[var(--canvas-border)]\"\n )}\n >\n {/* Logo - 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 <img\n src={logoUrl}\n alt=\"Logo\"\n className=\"size-8 object-contain\"\n />\n ) : (\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 {/* Icon Navigation */}\n <nav className=\"flex flex-col items-center gap-1 flex-1 px-4 pb-5 mt-2\">\n {sections.map((section) => (\n <IconColumnItem\n key={section.id}\n section={section}\n isActive={section.id === activeSectionId}\n variant={iconVariant}\n onClick={() => handleSectionClick(section)}\n />\n ))}\n </nav>\n </div>\n\n {/* Navigation Column (280px) */}\n <div\n className={cn(\n \"flex flex-col w-[var(--nav-sidebar-width)]\",\n isNavDark && \"bg-[var(--canvas-sidebar-dark-bg)] border-r border-[var(--canvas-sidebar-dark-border)]\",\n !isNavDark && \"bg-white border-r border-[var(--canvas-border)] shadow-[0_4px_16px_0_rgba(0,0,0,0.04)]\"\n )}\n >\n {/* Navigation Content */}\n <ScrollArea className=\"flex-1 pt-[var(--header-height)] px-[var(--spacing-2xl)] pb-[var(--spacing-5xl)]\">\n <nav className=\"flex flex-col gap-0\">\n {activeSection?.tabs.map((tab) => (\n <NavTabItem\n key={tab.id}\n tab={tab}\n isActive={tab.id === activeTabId}\n variant={navVariant}\n onClick={() => handleTabClick(tab)}\n />\n ))}\n </nav>\n </ScrollArea>\n </div>\n </div>\n );\n}\n\n"
9
+ "content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport { LucideIcon, Home, Users, Calendar, MessageSquare, PieChart, FileText, ShoppingBag } from \"lucide-react\";\nimport { ScrollArea } from \"../ui/scroll-area\";\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// Types\n// ============================================\n\nexport interface NavTab {\n id: string;\n label: string;\n isActive?: boolean;\n}\n\nexport interface DoubleSidebarSection {\n id: string;\n icon: LucideIcon;\n label: string;\n badge?: boolean;\n tabs: NavTab[];\n}\n\n// ============================================\n// Default Sections\n// ============================================\n\nexport const defaultDoubleSidebarSections: DoubleSidebarSection[] = [\n {\n id: \"home\",\n icon: Home,\n label: \"Home\",\n tabs: [\n { id: \"tab1\", label: \"Tab 1\", isActive: true },\n { id: \"tab2\", label: \"Tab 2\" },\n { id: \"tab3\", label: \"Tab 3\" },\n { id: \"tab4\", label: \"Tab 4\" },\n { id: \"tab5\", label: \"Tab 5\" },\n { id: \"tab6\", label: \"Tab 6\" },\n ],\n },\n {\n id: \"teams\",\n icon: Users,\n label: \"Teams\",\n tabs: [\n { id: \"all-teams\", label: \"All Teams\" },\n { id: \"my-team\", label: \"My Team\" },\n { id: \"invites\", label: \"Invites\" },\n ],\n },\n {\n id: \"calendar\",\n icon: Calendar,\n label: \"Calendar\",\n tabs: [\n { id: \"schedule\", label: \"Schedule\" },\n { id: \"events\", label: \"Events\" },\n { id: \"reminders\", label: \"Reminders\" },\n ],\n },\n {\n id: \"messages\",\n icon: MessageSquare,\n label: \"Messages\",\n badge: true,\n tabs: [\n { id: \"inbox\", label: \"Inbox\" },\n { id: \"sent\", label: \"Sent\" },\n { id: \"drafts\", label: \"Drafts\" },\n { id: \"archived\", label: \"Archived\" },\n ],\n },\n {\n id: \"reports\",\n icon: PieChart,\n label: \"Reports\",\n tabs: [\n { id: \"overview\", label: \"Overview\" },\n { id: \"analytics\", label: \"Analytics\" },\n { id: \"exports\", label: \"Exports\" },\n ],\n },\n {\n id: \"docs\",\n icon: FileText,\n label: \"Docs\",\n tabs: [\n { id: \"recent\", label: \"Recent\" },\n { id: \"shared\", label: \"Shared\" },\n { id: \"favorites\", label: \"Favorites\" },\n ],\n },\n {\n id: \"orders\",\n icon: ShoppingBag,\n label: \"Orders\",\n tabs: [\n { id: \"pending\", label: \"Pending\" },\n { id: \"completed\", label: \"Completed\" },\n { id: \"cancelled\", label: \"Cancelled\" },\n ],\n },\n];\n\n// ============================================\n// Icon Column Item\n// ============================================\n\ninterface IconColumnItemProps {\n section: DoubleSidebarSection;\n isActive: boolean;\n variant: \"dark\" | \"light\";\n onClick: () => void;\n}\n\nfunction IconColumnItem({ section, isActive, variant, onClick }: IconColumnItemProps) {\n const Icon = section.icon;\n const isDark = variant === \"dark\";\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-11 h-11 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 {/* Notification Badge */}\n {section.badge && (\n <div className=\"absolute top-1.5 right-1.5 size-1.5 rounded-full bg-[var(--canvas-destructive)]\" />\n )}\n </button>\n );\n}\n\n// ============================================\n// Nav Column Tab Item\n// ============================================\n\ninterface NavTabItemProps {\n tab: NavTab;\n isActive: boolean;\n variant: \"dark\" | \"light\";\n onClick: () => void;\n}\n\nfunction NavTabItem({ tab, isActive, variant, onClick }: NavTabItemProps) {\n const isDark = variant === \"dark\";\n\n return (\n <button\n onClick={onClick}\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\n isDark && isActive && \"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 // Light variant\n !isDark && isActive && \"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 )}\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 {tab.label}\n </span>\n </button>\n );\n}\n\n// ============================================\n// Double Sidebar Component\n// ============================================\n\ninterface DoubleSidebarProps {\n /** Navigation sections - each section maps to an icon and has tabs */\n sections?: DoubleSidebarSection[];\n /** Visual variant for the icon column */\n iconVariant?: \"dark\" | \"light\";\n /** Visual variant for the navigation column */\n navVariant?: \"dark\" | \"light\";\n /** Callback when a tab is clicked */\n onTabClick?: (section: DoubleSidebarSection, tab: NavTab) => void;\n /** Callback when closing (for mobile sheet) */\n onClose?: () => void;\n /** Additional class names */\n className?: string;\n}\n\n/**\n * Canvas Design System - Double Sidebar Component\n * \n * A two-column sidebar where:\n * - Left column (96px): Icon buttons that switch the active section\n * - Right column (280px): Text-based navigation tabs for the active section\n * \n * Each column can be independently themed (light/dark).\n */\nexport function DoubleSidebar({\n sections = defaultDoubleSidebarSections,\n iconVariant = \"light\",\n navVariant = \"light\",\n onTabClick,\n onClose,\n className\n}: DoubleSidebarProps) {\n const [activeSectionId, setActiveSectionId] = useState(sections[0]?.id || \"\");\n const [activeTabId, setActiveTabId] = useState(sections[0]?.tabs[0]?.id || \"\");\n \n const themeImages = useThemeImages();\n const { branding, isMounted } = useThemeBranding();\n\n const activeSection = sections.find(s => s.id === activeSectionId);\n const isIconDark = iconVariant === \"dark\";\n const isNavDark = navVariant === \"dark\";\n\n // Get the appropriate logo based on icon column variant\n const logoUrl = isIconDark ? 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 const handleSectionClick = (section: DoubleSidebarSection) => {\n setActiveSectionId(section.id);\n // Set first tab of the section as active\n if (section.tabs.length > 0) {\n setActiveTabId(section.tabs[0].id);\n }\n };\n\n const handleTabClick = (tab: NavTab) => {\n setActiveTabId(tab.id);\n if (activeSection) {\n onTabClick?.(activeSection, tab);\n onClose?.();\n }\n };\n\n return (\n <div className={cn(\"flex h-full\", className)}>\n {/* Icon Column (96px) */}\n <div\n className={cn(\n \"flex flex-col items-center w-[var(--icon-sidebar-width)] shrink-0\",\n isIconDark && \"bg-[var(--canvas-sidebar-dark-bg)] border-r border-[var(--canvas-sidebar-dark-border)]\",\n !isIconDark && \"bg-[var(--canvas-background)] border-r border-[var(--canvas-border)]\"\n )}\n >\n {/* Logo - 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 <img\n src={logoUrl}\n alt=\"Logo\"\n className=\"size-8 object-contain\"\n />\n ) : (\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 {/* Icon Navigation */}\n <nav className=\"flex flex-col items-center gap-1 flex-1 px-4 pb-5 mt-2\">\n {sections.map((section) => (\n <IconColumnItem\n key={section.id}\n section={section}\n isActive={section.id === activeSectionId}\n variant={iconVariant}\n onClick={() => handleSectionClick(section)}\n />\n ))}\n </nav>\n </div>\n\n {/* Navigation Column (280px) */}\n <div\n className={cn(\n \"flex flex-col w-[var(--nav-sidebar-width)]\",\n isNavDark && \"bg-[var(--canvas-sidebar-dark-bg)] border-r border-[var(--canvas-sidebar-dark-border)]\",\n !isNavDark && \"bg-[var(--canvas-background)] border-r border-[var(--canvas-border)] shadow-[0_4px_16px_0_rgba(0,0,0,0.04)]\"\n )}\n >\n {/* Navigation Content */}\n <ScrollArea className=\"flex-1 pt-[var(--header-height)] px-[var(--spacing-2xl)] pb-[var(--spacing-5xl)]\">\n <nav className=\"flex flex-col gap-0\">\n {activeSection?.tabs.map((tab) => (\n <NavTabItem\n key={tab.id}\n tab={tab}\n isActive={tab.id === activeTabId}\n variant={navVariant}\n onClick={() => handleTabClick(tab)}\n />\n ))}\n </nav>\n </ScrollArea>\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/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=\"cursor-pointer 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=\"cursor-pointer 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=\"cursor-pointer 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=\"cursor-pointer 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=\"cursor-pointer 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={`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 aria-label=\"Search\"\n >\n <Search className=\"size-4\" />\n </button>\n \n <Popover>\n <PopoverTrigger asChild>\n <button\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 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={`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 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={`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 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=\"cursor-pointer 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=\"cursor-pointer 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-white\",\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"
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-white 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/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": [],