{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "split-layout-glass", "type": "registry:block", "title": "Split Layout Glass", "description": "SplitLayoutGlass Component (Compound API only)", "dependencies": [ "@radix-ui/react-slot", "lucide-react", "react" ], "registryDependencies": [ "cn" ], "files": [ { "path": "components/glass/composite/split-layout-glass.tsx", "type": "registry:component", "content": "/* eslint-disable react-refresh/only-export-components */\n/**\n * SplitLayoutGlass Component (Compound API only)\n *\n * A responsive two-column layout with sticky scroll behavior and glassmorphism styling.\n * Features independent scrolling in each panel after sticky positioning activates.\n *\n * @pattern MDN, GitHub Docs, Linear, shadcn Sidebar\n *\n * @example\n * ```tsx\n * \n * \n * \n * Header\n * Content\n * \n * \n * Content\n * \n * \n * \n * ```\n *\n * @since v2.2.0 - Legacy props API removed, Compound API only\n * @module split-layout-glass\n */\n\nimport { forwardRef, type CSSProperties, type ReactNode } from 'react';\nimport { Menu, X, PanelLeftClose, PanelLeftOpen } from 'lucide-react';\nimport { Slot } from '@radix-ui/react-slot';\nimport { cn } from '@/lib/utils';\nimport { GlassCard } from '@/components/glass/ui/glass-card';\nimport { ButtonGlass } from '@/components/glass/ui/button-glass';\nimport { ScrollArea } from '@/components/ui/scroll-area';\n\n// Import context\nimport {\n SplitLayoutProvider,\n useSplitLayout,\n useSplitLayoutOptional,\n type SplitLayoutProviderProps,\n type SplitLayoutContextValue,\n type Breakpoint,\n type MobileMode,\n} from './split-layout-context';\n\nimport '@/glass-theme.css';\n\n// ========================================\n// GRID STYLES HOOK\n// ========================================\n\ninterface GridStylesConfig {\n ratio?: { sidebar: number; main: number };\n minSidebarWidth?: string;\n maxSidebarWidth?: string;\n gap?: number | { mobile?: number; desktop?: number };\n stickyOffset?: number;\n}\n\nfunction useGridStyles(config: GridStylesConfig) {\n const {\n ratio = { sidebar: 1, main: 2 },\n minSidebarWidth = '280px',\n maxSidebarWidth,\n gap = { mobile: 16, desktop: 24 },\n stickyOffset = 24,\n } = config;\n\n const gapMobile = typeof gap === 'number' ? gap : (gap.mobile ?? 16);\n const gapDesktop = typeof gap === 'number' ? gap : (gap.desktop ?? 24);\n\n const gridTemplate = maxSidebarWidth\n ? `minmax(${minSidebarWidth}, ${maxSidebarWidth}) ${ratio.main}fr`\n : `minmax(${minSidebarWidth}, ${ratio.sidebar}fr) ${ratio.main}fr`;\n\n const cssVars = {\n '--grid-template': gridTemplate,\n '--sticky-offset': `${stickyOffset}px`,\n '--sticky-max-height': `calc(100vh - calc(${stickyOffset}px * 2))`,\n '--gap-mobile': `${gapMobile}px`,\n '--gap-desktop': `${gapDesktop}px`,\n } as CSSProperties;\n\n return { cssVars };\n}\n\n// ========================================\n// ROOT COMPONENT\n// ========================================\n\n/**\n * Props for SplitLayoutGlass.Root component\n */\nexport interface SplitLayoutRootProps extends React.HTMLAttributes {\n children: ReactNode;\n /**\n * Sidebar to main ratio in fr units\n * @default { sidebar: 1, main: 2 } (33% / 67%)\n * @example { sidebar: 1, main: 3 } = 25% / 75%\n */\n ratio?: { sidebar: number; main: number };\n /**\n * Minimum sidebar width (CSS value)\n * Prevents sidebar from shrinking below this on tablet\n * @default \"280px\"\n */\n minSidebarWidth?: string;\n /**\n * Maximum sidebar width (CSS value)\n * @example \"400px\" - sidebar won't exceed 400px\n */\n maxSidebarWidth?: string;\n /**\n * Gap between panels\n * @default { mobile: 16, desktop: 24 }\n */\n gap?: number | { mobile?: number; desktop?: number };\n /**\n * Breakpoint for desktop layout (overrides Provider's breakpoint)\n * @default \"md\" (768px)\n */\n breakpoint?: Breakpoint;\n /**\n * Mobile layout mode (below breakpoint)\n * - \"stack\": sidebar above main\n * - \"main-only\": hide sidebar\n * - \"sidebar-only\": hide main\n * @default \"stack\"\n */\n mobileLayout?: 'stack' | 'main-only' | 'sidebar-only';\n}\n\nconst SplitLayoutRoot = forwardRef(\n (\n {\n children,\n ratio = { sidebar: 1, main: 2 },\n minSidebarWidth = '300px',\n maxSidebarWidth,\n gap = { mobile: 16, desktop: 24 },\n breakpoint: breakpointProp,\n mobileLayout = 'stack',\n className,\n ...props\n },\n ref\n ) => {\n const context = useSplitLayoutOptional();\n const breakpoint = breakpointProp ?? context?.breakpoint ?? 'md';\n const stickyOffset = context?.stickyOffset ?? 24;\n\n const { cssVars } = useGridStyles({\n ratio,\n minSidebarWidth,\n maxSidebarWidth,\n gap,\n stickyOffset,\n });\n\n const bp = breakpoint;\n\n // Build gap classes based on breakpoint\n const gapClasses = {\n sm: 'gap-[var(--gap-mobile)] sm:gap-[var(--gap-desktop)]',\n md: 'gap-[var(--gap-mobile)] md:gap-[var(--gap-desktop)]',\n lg: 'gap-[var(--gap-mobile)] lg:gap-[var(--gap-desktop)]',\n xl: 'gap-[var(--gap-mobile)] xl:gap-[var(--gap-desktop)]',\n '2xl': 'gap-[var(--gap-mobile)] 2xl:gap-[var(--gap-desktop)]',\n };\n\n const gridClasses = {\n sm: 'sm:grid-cols-(--grid-template)',\n md: 'md:grid-cols-(--grid-template)',\n lg: 'lg:grid-cols-(--grid-template)',\n xl: 'xl:grid-cols-(--grid-template)',\n '2xl': '2xl:grid-cols-(--grid-template)',\n };\n\n return (\n \n {children}\n \n );\n }\n);\n\nSplitLayoutRoot.displayName = 'SplitLayoutGlass.Root';\n\n// ========================================\n// SIDEBAR COMPONENTS\n// ========================================\n\n/**\n * Props for SplitLayoutGlass.Sidebar component\n */\nexport interface SplitLayoutSidebarProps extends React.HTMLAttributes {\n children: ReactNode;\n /**\n * ARIA label for accessibility\n * @default \"Sidebar navigation\"\n */\n label?: string;\n}\n\nconst SplitLayoutSidebar = forwardRef(\n ({ children, label = 'Sidebar navigation', className, ...props }, ref) => {\n const context = useSplitLayoutOptional();\n const breakpoint = context?.breakpoint ?? 'md';\n const intensity = context?.intensity ?? 'medium';\n const bp = breakpoint;\n\n return (\n \n \n \n );\n }\n);\n\nSplitLayoutSidebar.displayName = 'SplitLayoutGlass.Sidebar';\n\n/**\n * Props for SplitLayoutGlass.SidebarHeader component\n * Header stays pinned at top when sidebar content scrolls\n */\nexport interface SplitLayoutSidebarHeaderProps extends React.HTMLAttributes {\n children: ReactNode;\n}\n\nconst SplitLayoutSidebarHeader = forwardRef(\n ({ children, className, ...props }, ref) => (\n \n {children}\n \n )\n);\n\nSplitLayoutSidebarHeader.displayName = 'SplitLayoutGlass.SidebarHeader';\n\n/**\n * Props for SplitLayoutGlass.SidebarContent component\n * Scrollable area for sidebar items\n */\nexport interface SplitLayoutSidebarContentProps extends React.HTMLAttributes {\n children: ReactNode;\n /**\n * Auto-wrap in ScrollArea for independent scrolling\n * @default true\n */\n scrollable?: boolean;\n}\n\nconst SplitLayoutSidebarContent = forwardRef(\n ({ children, scrollable = true, className, ...props }, ref) => {\n if (scrollable) {\n // Extract only div-compatible props (ScrollArea doesn't accept all HTML div props)\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n const { dir, ...divProps } = props;\n return (\n }\n data-split-sidebar-content=\"\"\n className={cn('flex-1 min-h-0', className)}\n {...divProps}\n >\n
{children}
\n \n );\n }\n\n return (\n \n {children}\n \n );\n }\n);\n\nSplitLayoutSidebarContent.displayName = 'SplitLayoutGlass.SidebarContent';\n\n/**\n * Props for SplitLayoutGlass.SidebarFooter component\n * Footer stays pinned at bottom when sidebar content scrolls\n */\nexport interface SplitLayoutSidebarFooterProps extends React.HTMLAttributes {\n children: ReactNode;\n}\n\nconst SplitLayoutSidebarFooter = forwardRef(\n ({ children, className, ...props }, ref) => (\n \n {children}\n \n )\n);\n\nSplitLayoutSidebarFooter.displayName = 'SplitLayoutGlass.SidebarFooter';\n\n// ========================================\n// MAIN COMPONENTS\n// ========================================\n\n/**\n * Props for SplitLayoutGlass.Main component\n */\nexport interface SplitLayoutMainProps extends React.HTMLAttributes {\n children: ReactNode;\n /**\n * ARIA label for accessibility\n * @default \"Main content\"\n */\n label?: string;\n}\n\nconst SplitLayoutMain = forwardRef(\n ({ children, label = 'Main content', className, ...props }, ref) => {\n const context = useSplitLayoutOptional();\n const breakpoint = context?.breakpoint ?? 'md';\n const intensity = context?.intensity ?? 'medium';\n const bp = breakpoint;\n\n return (\n \n
\n {children}\n
\n \n );\n }\n);\n\nSplitLayoutMain.displayName = 'SplitLayoutGlass.Main';\n\n/**\n * Props for SplitLayoutGlass.MainHeader component\n * Header stays pinned at top when main content scrolls\n */\nexport interface SplitLayoutMainHeaderProps extends React.HTMLAttributes {\n children: ReactNode;\n}\n\nconst SplitLayoutMainHeader = forwardRef(\n ({ children, className, ...props }, ref) => (\n \n {children}\n \n )\n);\n\nSplitLayoutMainHeader.displayName = 'SplitLayoutGlass.MainHeader';\n\n/**\n * Props for SplitLayoutGlass.MainContent component\n * Scrollable area for main content\n */\nexport interface SplitLayoutMainContentProps extends React.HTMLAttributes {\n children: ReactNode;\n /**\n * Auto-wrap in ScrollArea for independent scrolling\n * @default true\n */\n scrollable?: boolean;\n}\n\nconst SplitLayoutMainContent = forwardRef(\n ({ children, scrollable = true, className, ...props }, ref) => {\n if (scrollable) {\n // Extract only div-compatible props (ScrollArea doesn't accept all HTML div props)\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n const { dir, ...divProps } = props;\n return (\n }\n data-split-main-content=\"\"\n className={cn('flex-1 min-h-0', className)}\n {...divProps}\n >\n
{children}
\n \n );\n }\n\n return (\n \n {children}\n \n );\n }\n);\n\nSplitLayoutMainContent.displayName = 'SplitLayoutGlass.MainContent';\n\n/**\n * Props for SplitLayoutGlass.MainFooter component\n * Footer stays pinned at bottom when main content scrolls\n */\nexport interface SplitLayoutMainFooterProps extends React.HTMLAttributes {\n children: ReactNode;\n}\n\nconst SplitLayoutMainFooter = forwardRef(\n ({ children, className, ...props }, ref) => (\n \n {children}\n \n )\n);\n\nSplitLayoutMainFooter.displayName = 'SplitLayoutGlass.MainFooter';\n\n// ========================================\n// TRIGGER COMPONENT\n// ========================================\n\n/**\n * Props for SplitLayoutGlass.Trigger component\n * Toggle button for sidebar collapse/expand\n */\nexport interface SplitLayoutTriggerProps extends React.ButtonHTMLAttributes {\n /**\n * Use Radix Slot for custom trigger elements\n * @default false\n */\n asChild?: boolean;\n /**\n * Show on desktop (hidden by default)\n * @default false\n */\n showOnDesktop?: boolean;\n /**\n * Icon variant\n * - \"menu\": hamburger/X icons\n * - \"panel\": panel collapse/expand icons\n * @default \"menu\"\n */\n variant?: 'menu' | 'panel';\n}\n\nconst SplitLayoutTrigger = forwardRef(\n (\n { asChild = false, showOnDesktop = false, variant = 'menu', className, children, ...props },\n ref\n ) => {\n const { toggle, isOpen, isMobileOpen, isMobile, breakpoint } = useSplitLayout();\n\n const currentOpen = isMobile ? isMobileOpen : isOpen;\n\n const Icon =\n variant === 'menu' ? (currentOpen ? X : Menu) : currentOpen ? PanelLeftClose : PanelLeftOpen;\n\n const bp = breakpoint;\n const visibilityClass = showOnDesktop ? '' : `${bp}:hidden`;\n\n if (asChild) {\n return (\n \n {children}\n \n );\n }\n\n return (\n \n {children ?? }\n \n );\n }\n);\n\nSplitLayoutTrigger.displayName = 'SplitLayoutGlass.Trigger';\n\n// ========================================\n// COMPOUND COMPONENT EXPORT\n// ========================================\n\n/**\n * SplitLayoutGlass compound component\n *\n * @example\n * ```tsx\n * \n * \n * ...\n * ...\n * \n * \n * ```\n */\nexport const SplitLayoutGlass = {\n Provider: SplitLayoutProvider,\n Root: SplitLayoutRoot,\n Sidebar: SplitLayoutSidebar,\n SidebarHeader: SplitLayoutSidebarHeader,\n SidebarContent: SplitLayoutSidebarContent,\n SidebarFooter: SplitLayoutSidebarFooter,\n Main: SplitLayoutMain,\n MainHeader: SplitLayoutMainHeader,\n MainContent: SplitLayoutMainContent,\n MainFooter: SplitLayoutMainFooter,\n Trigger: SplitLayoutTrigger,\n};\n\n// ========================================\n// ACCORDION EXPORT (re-export from separate file)\n// ========================================\n\nexport { SplitLayoutAccordion } from './split-layout-accordion';\nexport type {\n SplitLayoutAccordionRootProps,\n SplitLayoutAccordionItemProps,\n} from './split-layout-accordion';\n\n// ========================================\n// TYPE EXPORTS\n// ========================================\n\nexport type { SplitLayoutProviderProps, SplitLayoutContextValue, Breakpoint, MobileMode };\n\nexport { useSplitLayout, useSplitLayoutOptional };\n" } ], "categories": [ "composite" ] }