shadcn-glass-ui 2.1.1 → 2.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/README.md +80 -325
- package/dist/cli/index.cjs +1 -1
- package/dist/components.cjs +4 -4
- package/dist/components.js +1 -1
- package/dist/hooks.cjs +2 -2
- package/dist/index.cjs +1659 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1651 -4
- package/dist/index.js.map +1 -1
- package/dist/r/registry.json +36 -0
- package/dist/r/sidebar-context.json +35 -0
- package/dist/r/sidebar-glass.json +40 -0
- package/dist/r/sidebar-menu.json +39 -0
- package/dist/r/split-layout-accordion.json +24 -0
- package/dist/r/split-layout-context.json +21 -0
- package/dist/r/split-layout-glass.json +25 -0
- package/dist/shadcn-glass-ui.css +1 -1
- package/dist/{theme-context-BHXYJ4RE.cjs → theme-context-Y98bGvcm.cjs} +2 -2
- package/dist/{theme-context-BHXYJ4RE.cjs.map → theme-context-Y98bGvcm.cjs.map} +1 -1
- package/dist/themes.cjs +1 -1
- package/dist/{trust-score-card-glass-CGXmOIfq.cjs → trust-score-card-glass-2rjz00d_.cjs} +47 -5
- package/dist/trust-score-card-glass-2rjz00d_.cjs.map +1 -0
- package/dist/{trust-score-card-glass-L9g0qamo.js → trust-score-card-glass-zjkx4OC2.js} +3 -3
- package/dist/trust-score-card-glass-zjkx4OC2.js.map +1 -0
- package/dist/{use-focus-CeNHOiBa.cjs → use-focus-DbpBEuee.cjs} +2 -2
- package/dist/{use-focus-CeNHOiBa.cjs.map → use-focus-DbpBEuee.cjs.map} +1 -1
- package/dist/{use-wallpaper-tint-Bt2G3g1v.cjs → use-wallpaper-tint-DbawS9zh.cjs} +2 -2
- package/dist/{use-wallpaper-tint-Bt2G3g1v.cjs.map → use-wallpaper-tint-DbawS9zh.cjs.map} +1 -1
- package/dist/{utils-LYxxWvUn.cjs → utils-XlyXIhuP.cjs} +2 -2
- package/dist/{utils-LYxxWvUn.cjs.map → utils-XlyXIhuP.cjs.map} +1 -1
- package/dist/utils.cjs +1 -1
- package/docs/GETTING_STARTED.md +3 -3
- package/docs/components/SPLIT_LAYOUT_GLASS.md +607 -0
- package/package.json +1 -2
- package/dist/trust-score-card-glass-CGXmOIfq.cjs.map +0 -1
- package/dist/trust-score-card-glass-L9g0qamo.js.map +0 -1
package/dist/r/registry.json
CHANGED
|
@@ -356,6 +356,42 @@
|
|
|
356
356
|
"type": "registry:component",
|
|
357
357
|
"title": "Expandable Header Glass",
|
|
358
358
|
"description": "Expandable Header Glass component with glass effects"
|
|
359
|
+
},
|
|
360
|
+
{
|
|
361
|
+
"name": "sidebar-menu",
|
|
362
|
+
"type": "registry:ui",
|
|
363
|
+
"title": "Sidebar Menu",
|
|
364
|
+
"description": "SidebarGlass Menu Components"
|
|
365
|
+
},
|
|
366
|
+
{
|
|
367
|
+
"name": "sidebar-glass",
|
|
368
|
+
"type": "registry:ui",
|
|
369
|
+
"title": "Sidebar Glass",
|
|
370
|
+
"description": "SidebarGlass Core Components"
|
|
371
|
+
},
|
|
372
|
+
{
|
|
373
|
+
"name": "sidebar-context",
|
|
374
|
+
"type": "registry:ui",
|
|
375
|
+
"title": "Sidebar Context",
|
|
376
|
+
"description": "SidebarGlass Context"
|
|
377
|
+
},
|
|
378
|
+
{
|
|
379
|
+
"name": "split-layout-glass",
|
|
380
|
+
"type": "registry:block",
|
|
381
|
+
"title": "Split Layout Glass",
|
|
382
|
+
"description": "SplitLayoutGlass Component (Compound API only)"
|
|
383
|
+
},
|
|
384
|
+
{
|
|
385
|
+
"name": "split-layout-context",
|
|
386
|
+
"type": "registry:block",
|
|
387
|
+
"title": "Split Layout Context",
|
|
388
|
+
"description": "SplitLayoutGlass Context"
|
|
389
|
+
},
|
|
390
|
+
{
|
|
391
|
+
"name": "split-layout-accordion",
|
|
392
|
+
"type": "registry:block",
|
|
393
|
+
"title": "Split Layout Accordion",
|
|
394
|
+
"description": "SplitLayoutGlass Accordion Components"
|
|
359
395
|
}
|
|
360
396
|
]
|
|
361
397
|
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "sidebar-context",
|
|
4
|
+
"type": "registry:ui",
|
|
5
|
+
"title": "Sidebar Context",
|
|
6
|
+
"description": "SidebarGlass Context",
|
|
7
|
+
"dependencies": [],
|
|
8
|
+
"registryDependencies": [],
|
|
9
|
+
"files": [
|
|
10
|
+
{
|
|
11
|
+
"path": "components/glass/ui/sidebar-context.tsx",
|
|
12
|
+
"type": "registry:component",
|
|
13
|
+
"content": "/* eslint-disable react-refresh/only-export-components */\n/**\n * SidebarGlass Context\n *\n * Provides state management for SidebarGlass compound component.\n * 100% compatible with shadcn/ui Sidebar API.\n *\n * @module sidebar-context\n */\n\nimport {\n createContext,\n useContext,\n useState,\n useCallback,\n useMemo,\n useEffect,\n type FC,\n type ReactNode,\n} from 'react';\n\n// ========================================\n// TYPES\n// ========================================\n\nexport type SidebarSide = 'left' | 'right';\nexport type SidebarVariant = 'sidebar' | 'floating' | 'inset';\nexport type SidebarCollapsible = 'offcanvas' | 'icon' | 'none';\n\n/**\n * Context value for SidebarGlass compound components\n * 100% compatible with shadcn/ui useSidebar() hook\n */\nexport interface SidebarContextValue {\n /** Sidebar state: \"expanded\" | \"collapsed\" */\n state: 'expanded' | 'collapsed';\n /** Whether sidebar is open (desktop) */\n open: boolean;\n /** Set sidebar open state */\n setOpen: (open: boolean) => void;\n /** Whether mobile drawer is open */\n openMobile: boolean;\n /** Set mobile drawer open state */\n setOpenMobile: (open: boolean) => void;\n /** Whether viewport is mobile */\n isMobile: boolean;\n /** Toggle sidebar open/close */\n toggleSidebar: () => void;\n\n // === CONFIG ===\n /** Which side the sidebar is on */\n side: SidebarSide;\n /** Sidebar variant */\n variant: SidebarVariant;\n /** Collapsible mode */\n collapsible: SidebarCollapsible;\n}\n\n// ========================================\n// CONSTANTS\n// ========================================\n\nconst MOBILE_BREAKPOINT = 768;\nconst SIDEBAR_COOKIE_NAME = 'sidebar:state';\nconst SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7; // 7 days\n\n// ========================================\n// CONTEXT\n// ========================================\n\nconst SidebarContext = createContext<SidebarContextValue | null>(null);\n\n/**\n * Hook to access Sidebar context (100% shadcn compatible)\n *\n * @throws Error if used outside of SidebarGlass.Provider\n *\n * @example\n * ```tsx\n * function MyComponent() {\n * const { state, open, toggleSidebar, isMobile } = useSidebar();\n *\n * return (\n * <button onClick={toggleSidebar}>\n * {state === 'expanded' ? 'Collapse' : 'Expand'}\n * </button>\n * );\n * }\n * ```\n */\nexport function useSidebar(): SidebarContextValue {\n const context = useContext(SidebarContext);\n if (!context) {\n throw new Error(\n 'useSidebar must be used within SidebarGlass.Provider. ' +\n 'Wrap your component tree with <SidebarGlass.Provider>.'\n );\n }\n return context;\n}\n\n/**\n * Optional hook that returns null if outside provider (doesn't throw)\n */\nexport function useSidebarOptional(): SidebarContextValue | null {\n return useContext(SidebarContext);\n}\n\n// ========================================\n// PROVIDER\n// ========================================\n\n/**\n * Props for SidebarGlass.Provider (100% shadcn compatible)\n */\nexport interface SidebarProviderProps {\n children: ReactNode;\n\n /** Controlled open state */\n open?: boolean;\n /** Callback when open state changes */\n onOpenChange?: (open: boolean) => void;\n /** Default open state (uncontrolled) @default true */\n defaultOpen?: boolean;\n\n /** Which side the sidebar is on @default \"left\" */\n side?: SidebarSide;\n /** Sidebar variant @default \"sidebar\" */\n variant?: SidebarVariant;\n /** Collapsible mode @default \"offcanvas\" */\n collapsible?: SidebarCollapsible;\n\n /** Cookie name for persistence @default \"sidebar:state\" */\n cookieName?: string;\n /** Keyboard shortcut key (Cmd/Ctrl + key) @default \"b\" */\n keyboardShortcut?: string | false;\n}\n\n/**\n * Provider component for SidebarGlass compound components\n *\n * @example\n * ```tsx\n * <SidebarGlass.Provider defaultOpen>\n * <SidebarGlass.Root>\n * <SidebarGlass.Header />\n * <SidebarGlass.Content>\n * <SidebarGlass.Menu>...</SidebarGlass.Menu>\n * </SidebarGlass.Content>\n * <SidebarGlass.Footer />\n * </SidebarGlass.Root>\n * <SidebarGlass.Inset>\n * <main>Main content</main>\n * </SidebarGlass.Inset>\n * </SidebarGlass.Provider>\n * ```\n */\nexport const SidebarProvider: FC<SidebarProviderProps> = ({\n children,\n open: controlledOpen,\n onOpenChange,\n defaultOpen = true,\n side = 'left',\n variant = 'sidebar',\n collapsible = 'offcanvas',\n cookieName = SIDEBAR_COOKIE_NAME,\n keyboardShortcut = 'b',\n}) => {\n // === OPEN STATE (controlled/uncontrolled with cookie persistence) ===\n const [internalOpen, setInternalOpen] = useState(() => {\n // Try to read from cookie\n if (typeof document !== 'undefined') {\n const cookies = document.cookie.split(';');\n const sidebarCookie = cookies.find((c) => c.trim().startsWith(`${cookieName}=`));\n if (sidebarCookie) {\n return sidebarCookie.split('=')[1] === 'true';\n }\n }\n return defaultOpen;\n });\n\n const isControlled = controlledOpen !== undefined;\n const open = isControlled ? controlledOpen : internalOpen;\n\n const setOpen = useCallback(\n (value: boolean) => {\n if (!isControlled) {\n setInternalOpen(value);\n }\n onOpenChange?.(value);\n\n // Persist to cookie\n if (typeof document !== 'undefined') {\n document.cookie = `${cookieName}=${value}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;\n }\n },\n [isControlled, onOpenChange, cookieName]\n );\n\n // === MOBILE STATE ===\n const [openMobile, setOpenMobile] = useState(false);\n\n // === RESPONSIVE DETECTION ===\n const [isMobile, setIsMobile] = useState(() => {\n if (typeof window === 'undefined') return false;\n return window.innerWidth < MOBILE_BREAKPOINT;\n });\n\n useEffect(() => {\n if (typeof window === 'undefined') return;\n\n const checkMobile = () => {\n const mobile = window.innerWidth < MOBILE_BREAKPOINT;\n setIsMobile(mobile);\n // Close mobile drawer when switching to desktop (in same callback)\n if (!mobile) {\n setOpenMobile(false);\n }\n };\n\n checkMobile();\n window.addEventListener('resize', checkMobile);\n return () => window.removeEventListener('resize', checkMobile);\n }, []);\n\n // === TOGGLE ACTION ===\n const toggleSidebar = useCallback(() => {\n if (isMobile) {\n setOpenMobile((prev) => !prev);\n } else {\n setOpen(!open);\n }\n }, [isMobile, open, setOpen]);\n\n // === KEYBOARD NAVIGATION ===\n useEffect(() => {\n if (!keyboardShortcut) return;\n\n const handleKeyDown = (e: KeyboardEvent) => {\n // Cmd/Ctrl + key - toggle sidebar\n if (e.key === keyboardShortcut && (e.metaKey || e.ctrlKey)) {\n e.preventDefault();\n toggleSidebar();\n }\n };\n\n document.addEventListener('keydown', handleKeyDown);\n return () => document.removeEventListener('keydown', handleKeyDown);\n }, [keyboardShortcut, toggleSidebar]);\n\n // === CONTEXT VALUE ===\n const value = useMemo<SidebarContextValue>(\n () => ({\n state: open ? 'expanded' : 'collapsed',\n open,\n setOpen,\n openMobile,\n setOpenMobile,\n isMobile,\n toggleSidebar,\n side,\n variant,\n collapsible,\n }),\n [open, setOpen, openMobile, isMobile, toggleSidebar, side, variant, collapsible]\n );\n\n return <SidebarContext.Provider value={value}>{children}</SidebarContext.Provider>;\n};\n\nSidebarProvider.displayName = 'SidebarGlass.Provider';\n"
|
|
14
|
+
}
|
|
15
|
+
],
|
|
16
|
+
"categories": [
|
|
17
|
+
"ui"
|
|
18
|
+
],
|
|
19
|
+
"cssVars": {
|
|
20
|
+
"light": {
|
|
21
|
+
"--glass-bg": "rgba(255, 255, 255, 0.1)",
|
|
22
|
+
"--glass-border": "rgba(255, 255, 255, 0.2)",
|
|
23
|
+
"--blur-sm": "8px",
|
|
24
|
+
"--blur-md": "16px",
|
|
25
|
+
"--blur-lg": "24px"
|
|
26
|
+
},
|
|
27
|
+
"dark": {
|
|
28
|
+
"--glass-bg": "rgba(255, 255, 255, 0.05)",
|
|
29
|
+
"--glass-border": "rgba(255, 255, 255, 0.1)",
|
|
30
|
+
"--blur-sm": "8px",
|
|
31
|
+
"--blur-md": "16px",
|
|
32
|
+
"--blur-lg": "24px"
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "sidebar-glass",
|
|
4
|
+
"type": "registry:ui",
|
|
5
|
+
"title": "Sidebar Glass",
|
|
6
|
+
"description": "SidebarGlass Core Components",
|
|
7
|
+
"dependencies": [
|
|
8
|
+
"@radix-ui/react-slot",
|
|
9
|
+
"lucide-react"
|
|
10
|
+
],
|
|
11
|
+
"registryDependencies": [
|
|
12
|
+
"cn"
|
|
13
|
+
],
|
|
14
|
+
"files": [
|
|
15
|
+
{
|
|
16
|
+
"path": "components/glass/ui/sidebar-glass.tsx",
|
|
17
|
+
"type": "registry:component",
|
|
18
|
+
"content": "/**\n * SidebarGlass Core Components\n *\n * Layout components for building sidebars with glassmorphism effects.\n * 100% API compatible with shadcn/ui Sidebar.\n *\n * @module sidebar-glass\n */\n\nimport {\n forwardRef,\n type ReactNode,\n type CSSProperties,\n type ComponentPropsWithoutRef,\n} from 'react';\nimport { PanelLeft } from 'lucide-react';\nimport { Slot } from '@radix-ui/react-slot';\nimport { cn } from '@/lib/utils';\nimport {\n useSidebar,\n type SidebarSide,\n type SidebarVariant,\n type SidebarCollapsible,\n} from './sidebar-context';\nimport { ModalGlass } from '@/components/glass/ui/modal-glass';\nimport '@/glass-theme.css';\n\n// ========================================\n// SIDEBAR ROOT\n// ========================================\n\nexport interface SidebarRootProps extends ComponentPropsWithoutRef<'aside'> {\n children: ReactNode;\n /** Override side from provider */\n side?: SidebarSide;\n /** Override variant from provider */\n variant?: SidebarVariant;\n /** Override collapsible from provider */\n collapsible?: SidebarCollapsible;\n}\n\n/**\n * SidebarGlass.Root - Main sidebar container\n *\n * @example\n * ```tsx\n * <SidebarGlass.Root>\n * <SidebarGlass.Header />\n * <SidebarGlass.Content>...</SidebarGlass.Content>\n * <SidebarGlass.Footer />\n * </SidebarGlass.Root>\n * ```\n */\nexport const SidebarRoot = forwardRef<HTMLElement, SidebarRootProps>(\n (\n {\n children,\n side: sideProp,\n variant: variantProp,\n collapsible: collapsibleProp,\n className,\n ...props\n },\n ref\n ) => {\n const context = useSidebar();\n const side = sideProp ?? context.side;\n const variant = variantProp ?? context.variant;\n const collapsible = collapsibleProp ?? context.collapsible;\n const { state, open, openMobile, isMobile, setOpenMobile } = context;\n\n // Mobile: render as Sheet/Drawer\n if (isMobile) {\n return (\n <ModalGlass.Root open={openMobile} onOpenChange={setOpenMobile}>\n <ModalGlass.Overlay />\n <aside\n ref={ref}\n data-sidebar=\"sidebar\"\n data-side={side}\n data-variant={variant}\n data-collapsible={collapsible}\n data-state=\"expanded\"\n data-mobile=\"true\"\n className={cn(\n 'fixed inset-y-0 z-50 flex flex-col',\n 'w-[var(--sidebar-width-mobile)]',\n side === 'left' ? 'left-0' : 'right-0',\n className\n )}\n style={\n {\n background: 'var(--sidebar-bg)',\n color: 'var(--sidebar-foreground)',\n borderRight: side === 'left' ? '1px solid var(--sidebar-border)' : undefined,\n borderLeft: side === 'right' ? '1px solid var(--sidebar-border)' : undefined,\n backdropFilter: 'blur(var(--sidebar-backdrop-blur))',\n WebkitBackdropFilter: 'blur(var(--sidebar-backdrop-blur))',\n boxShadow: 'var(--sidebar-glow)',\n } as CSSProperties\n }\n {...props}\n >\n {children}\n </aside>\n </ModalGlass.Root>\n );\n }\n\n // Desktop: collapsible sidebar\n const isCollapsed = !open && collapsible !== 'none';\n const width =\n isCollapsed && collapsible === 'icon' ? 'var(--sidebar-width-icon)' : 'var(--sidebar-width)';\n\n return (\n <aside\n ref={ref}\n data-sidebar=\"sidebar\"\n data-side={side}\n data-variant={variant}\n data-collapsible={collapsible}\n data-state={state}\n className={cn(\n 'group/sidebar relative flex flex-col',\n 'transition-[width] duration-300 ease-in-out',\n // Offcanvas: completely hidden when collapsed\n collapsible === 'offcanvas' && !open && 'w-0 overflow-hidden',\n // Variant-specific styles\n variant === 'floating' && 'rounded-xl m-2',\n variant === 'inset' && 'rounded-xl',\n className\n )}\n style={\n {\n width: collapsible === 'offcanvas' && !open ? 0 : width,\n background: 'var(--sidebar-bg)',\n color: 'var(--sidebar-foreground)',\n borderRight:\n side === 'left' && variant !== 'floating'\n ? '1px solid var(--sidebar-border)'\n : undefined,\n borderLeft:\n side === 'right' && variant !== 'floating'\n ? '1px solid var(--sidebar-border)'\n : undefined,\n border: variant === 'floating' ? '1px solid var(--sidebar-border)' : undefined,\n backdropFilter: 'blur(var(--sidebar-backdrop-blur))',\n WebkitBackdropFilter: 'blur(var(--sidebar-backdrop-blur))',\n boxShadow: variant === 'floating' ? 'var(--sidebar-glow)' : undefined,\n } as CSSProperties\n }\n {...props}\n >\n {children}\n </aside>\n );\n }\n);\n\nSidebarRoot.displayName = 'SidebarGlass.Root';\n\n// ========================================\n// SIDEBAR HEADER\n// ========================================\n\nexport interface SidebarHeaderProps extends ComponentPropsWithoutRef<'div'> {\n children: ReactNode;\n}\n\n/**\n * SidebarGlass.Header - Sticky header section\n */\nexport const SidebarHeader = forwardRef<HTMLDivElement, SidebarHeaderProps>(\n ({ children, className, ...props }, ref) => {\n return (\n <div\n ref={ref}\n data-sidebar=\"header\"\n className={cn('flex shrink-0 flex-col gap-2 p-4', 'border-b', className)}\n style={{\n borderColor: 'var(--sidebar-border)',\n }}\n {...props}\n >\n {children}\n </div>\n );\n }\n);\n\nSidebarHeader.displayName = 'SidebarGlass.Header';\n\n// ========================================\n// SIDEBAR CONTENT\n// ========================================\n\nexport interface SidebarContentProps extends ComponentPropsWithoutRef<'div'> {\n children: ReactNode;\n}\n\n/**\n * SidebarGlass.Content - Scrollable content area\n */\nexport const SidebarContent = forwardRef<HTMLDivElement, SidebarContentProps>(\n ({ children, className, ...props }, ref) => {\n return (\n <div\n ref={ref}\n data-sidebar=\"content\"\n className={cn(\n 'flex flex-1 flex-col gap-4 overflow-auto p-4',\n // Hide scrollbar but allow scrolling\n '[&::-webkit-scrollbar]:w-1.5',\n '[&::-webkit-scrollbar-track]:bg-transparent',\n '[&::-webkit-scrollbar-thumb]:rounded-full',\n '[&::-webkit-scrollbar-thumb]:bg-white/10',\n className\n )}\n {...props}\n >\n {children}\n </div>\n );\n }\n);\n\nSidebarContent.displayName = 'SidebarGlass.Content';\n\n// ========================================\n// SIDEBAR FOOTER\n// ========================================\n\nexport interface SidebarFooterProps extends ComponentPropsWithoutRef<'div'> {\n children: ReactNode;\n}\n\n/**\n * SidebarGlass.Footer - Sticky footer section\n */\nexport const SidebarFooter = forwardRef<HTMLDivElement, SidebarFooterProps>(\n ({ children, className, ...props }, ref) => {\n return (\n <div\n ref={ref}\n data-sidebar=\"footer\"\n className={cn('flex shrink-0 flex-col gap-2 p-4', 'border-t', className)}\n style={{\n borderColor: 'var(--sidebar-border)',\n }}\n {...props}\n >\n {children}\n </div>\n );\n }\n);\n\nSidebarFooter.displayName = 'SidebarGlass.Footer';\n\n// ========================================\n// SIDEBAR RAIL\n// ========================================\n\nexport type SidebarRailProps = ComponentPropsWithoutRef<'button'>;\n\n/**\n * SidebarGlass.Rail - Interactive rail for toggling sidebar\n *\n * Shows on hover and allows click to toggle collapsed/expanded state.\n */\nexport const SidebarRail = forwardRef<HTMLButtonElement, SidebarRailProps>(\n ({ className, ...props }, ref) => {\n const { toggleSidebar, side } = useSidebar();\n\n return (\n <button\n ref={ref}\n data-sidebar=\"rail\"\n aria-label=\"Toggle Sidebar\"\n tabIndex={-1}\n onClick={toggleSidebar}\n className={cn(\n 'absolute inset-y-0 z-20 w-4',\n 'hidden group-hover/sidebar:block',\n '-translate-x-1/2 transition-all ease-linear',\n 'after:absolute after:inset-y-0 after:left-1/2 after:w-[2px]',\n 'hover:after:bg-[var(--sidebar-border)]',\n 'cursor-col-resize',\n side === 'left' ? '-right-2' : '-left-2',\n className\n )}\n {...props}\n />\n );\n }\n);\n\nSidebarRail.displayName = 'SidebarGlass.Rail';\n\n// ========================================\n// SIDEBAR INSET\n// ========================================\n\nexport interface SidebarInsetProps extends ComponentPropsWithoutRef<'main'> {\n children: ReactNode;\n}\n\n/**\n * SidebarGlass.Inset - Main content area next to sidebar\n *\n * Use this for the main content that sits beside the sidebar.\n */\nexport const SidebarInset = forwardRef<HTMLElement, SidebarInsetProps>(\n ({ children, className, ...props }, ref) => {\n return (\n <main\n ref={ref}\n data-sidebar=\"inset\"\n className={cn('flex flex-1 flex-col', 'min-h-screen', className)}\n {...props}\n >\n {children}\n </main>\n );\n }\n);\n\nSidebarInset.displayName = 'SidebarGlass.Inset';\n\n// ========================================\n// SIDEBAR TRIGGER\n// ========================================\n\nexport interface SidebarTriggerProps extends ComponentPropsWithoutRef<'button'> {\n /** Render as child element */\n asChild?: boolean;\n}\n\n/**\n * SidebarGlass.Trigger - Toggle button for sidebar\n *\n * @example\n * ```tsx\n * // Default button\n * <SidebarGlass.Trigger />\n *\n * // Custom trigger\n * <SidebarGlass.Trigger asChild>\n * <ButtonGlass variant=\"ghost\" size=\"icon\">\n * <Menu />\n * </ButtonGlass>\n * </SidebarGlass.Trigger>\n * ```\n */\nexport const SidebarTrigger = forwardRef<HTMLButtonElement, SidebarTriggerProps>(\n ({ asChild = false, className, children, ...props }, ref) => {\n const { toggleSidebar } = useSidebar();\n\n const Comp = asChild ? Slot : 'button';\n\n return (\n <Comp\n ref={ref}\n data-sidebar=\"trigger\"\n onClick={toggleSidebar}\n className={cn(\n 'inline-flex items-center justify-center',\n 'h-9 w-9 rounded-lg',\n 'text-sm font-medium',\n 'transition-colors',\n 'text-[var(--sidebar-foreground)]/60',\n 'hover:bg-[var(--sidebar-accent)]',\n 'hover:text-[var(--sidebar-accent-foreground)]',\n 'focus-visible:outline-none focus-visible:ring-2',\n 'focus-visible:ring-[var(--sidebar-ring)]',\n 'disabled:pointer-events-none disabled:opacity-50',\n className\n )}\n {...props}\n >\n {children ?? <PanelLeft className=\"h-6 w-6\" />}\n {!children && <span className=\"sr-only\">Toggle Sidebar</span>}\n </Comp>\n );\n }\n);\n\nSidebarTrigger.displayName = 'SidebarGlass.Trigger';\n\n// ========================================\n// SIDEBAR SEPARATOR\n// ========================================\n\nexport type SidebarSeparatorProps = ComponentPropsWithoutRef<'hr'>;\n\n/**\n * SidebarGlass.Separator - Visual divider\n */\nexport const SidebarSeparator = forwardRef<HTMLHRElement, SidebarSeparatorProps>(\n ({ className, ...props }, ref) => {\n return (\n <hr\n ref={ref}\n data-sidebar=\"separator\"\n className={cn('mx-4 my-2 h-px border-0', className)}\n style={{\n background: 'var(--sidebar-border)',\n }}\n {...props}\n />\n );\n }\n);\n\nSidebarSeparator.displayName = 'SidebarGlass.Separator';\n"
|
|
19
|
+
}
|
|
20
|
+
],
|
|
21
|
+
"categories": [
|
|
22
|
+
"ui"
|
|
23
|
+
],
|
|
24
|
+
"cssVars": {
|
|
25
|
+
"light": {
|
|
26
|
+
"--glass-bg": "rgba(255, 255, 255, 0.1)",
|
|
27
|
+
"--glass-border": "rgba(255, 255, 255, 0.2)",
|
|
28
|
+
"--blur-sm": "8px",
|
|
29
|
+
"--blur-md": "16px",
|
|
30
|
+
"--blur-lg": "24px"
|
|
31
|
+
},
|
|
32
|
+
"dark": {
|
|
33
|
+
"--glass-bg": "rgba(255, 255, 255, 0.05)",
|
|
34
|
+
"--glass-border": "rgba(255, 255, 255, 0.1)",
|
|
35
|
+
"--blur-sm": "8px",
|
|
36
|
+
"--blur-md": "16px",
|
|
37
|
+
"--blur-lg": "24px"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "sidebar-menu",
|
|
4
|
+
"type": "registry:ui",
|
|
5
|
+
"title": "Sidebar Menu",
|
|
6
|
+
"description": "SidebarGlass Menu Components",
|
|
7
|
+
"dependencies": [
|
|
8
|
+
"@radix-ui/react-slot"
|
|
9
|
+
],
|
|
10
|
+
"registryDependencies": [
|
|
11
|
+
"cn"
|
|
12
|
+
],
|
|
13
|
+
"files": [
|
|
14
|
+
{
|
|
15
|
+
"path": "components/glass/ui/sidebar-menu.tsx",
|
|
16
|
+
"type": "registry:component",
|
|
17
|
+
"content": "/* eslint-disable react-refresh/only-export-components */\n/**\n * SidebarGlass Menu Components\n *\n * Menu components for building navigation within the sidebar.\n * 100% API compatible with shadcn/ui Sidebar menu components.\n *\n * @module sidebar-menu\n */\n\nimport {\n forwardRef,\n type ReactNode,\n type CSSProperties,\n type ComponentPropsWithoutRef,\n createContext,\n useContext,\n} from 'react';\nimport { Slot } from '@radix-ui/react-slot';\nimport { cn } from '@/lib/utils';\nimport { useSidebar } from './sidebar-context';\nimport { TooltipGlassSimple } from '@/components/glass/ui/tooltip-glass';\nimport { SkeletonGlass } from '@/components/glass/ui/skeleton-glass';\n\n// ========================================\n// SIDEBAR GROUP\n// ========================================\n\nexport interface SidebarGroupProps extends ComponentPropsWithoutRef<'div'> {\n children: ReactNode;\n}\n\n/**\n * SidebarGlass.Group - Container for a group of menu items\n */\nexport const SidebarGroup = forwardRef<HTMLDivElement, SidebarGroupProps>(\n ({ children, className, ...props }, ref) => {\n return (\n <div\n ref={ref}\n data-sidebar=\"group\"\n className={cn('flex flex-col gap-2 p-2', className)}\n {...props}\n >\n {children}\n </div>\n );\n }\n);\n\nSidebarGroup.displayName = 'SidebarGlass.Group';\n\n// ========================================\n// SIDEBAR GROUP LABEL\n// ========================================\n\nexport interface SidebarGroupLabelProps extends ComponentPropsWithoutRef<'div'> {\n children: ReactNode;\n /** Render as child element */\n asChild?: boolean;\n}\n\n/**\n * SidebarGlass.GroupLabel - Label for a group of menu items\n */\nexport const SidebarGroupLabel = forwardRef<HTMLDivElement, SidebarGroupLabelProps>(\n ({ children, asChild = false, className, ...props }, ref) => {\n const { state } = useSidebar();\n const Comp = asChild ? Slot : 'div';\n\n return (\n <Comp\n ref={ref}\n data-sidebar=\"group-label\"\n data-state={state}\n className={cn(\n 'flex h-8 shrink-0 items-center px-2',\n 'text-xs font-medium text-[var(--sidebar-foreground)]/60',\n 'transition-[margin,opacity] duration-200 ease-linear',\n // Collapsed state\n 'group-data-[state=collapsed]/sidebar:h-0 group-data-[state=collapsed]/sidebar:overflow-hidden',\n 'group-data-[state=collapsed]/sidebar:opacity-0',\n className\n )}\n {...props}\n >\n {children}\n </Comp>\n );\n }\n);\n\nSidebarGroupLabel.displayName = 'SidebarGlass.GroupLabel';\n\n// ========================================\n// SIDEBAR GROUP ACTION\n// ========================================\n\nexport interface SidebarGroupActionProps extends ComponentPropsWithoutRef<'button'> {\n /** Render as child element */\n asChild?: boolean;\n}\n\n/**\n * SidebarGlass.GroupAction - Action button in group header\n */\nexport const SidebarGroupAction = forwardRef<HTMLButtonElement, SidebarGroupActionProps>(\n ({ asChild = false, className, ...props }, ref) => {\n const Comp = asChild ? Slot : 'button';\n\n return (\n <Comp\n ref={ref}\n data-sidebar=\"group-action\"\n className={cn(\n 'absolute right-2 top-2.5',\n 'flex aspect-square w-5 items-center justify-center',\n 'rounded-md p-0 text-[var(--sidebar-foreground)]/60',\n 'ring-[var(--sidebar-ring)]',\n 'transition-transform hover:bg-[var(--sidebar-accent)]',\n 'hover:text-[var(--sidebar-foreground)]',\n 'focus-visible:outline-none focus-visible:ring-2',\n '[&>svg]:size-4 [&>svg]:shrink-0',\n // Hide when collapsed\n 'group-data-[state=collapsed]/sidebar:hidden',\n // Show on hover\n 'after:absolute after:-inset-2 after:md:hidden',\n className\n )}\n {...props}\n />\n );\n }\n);\n\nSidebarGroupAction.displayName = 'SidebarGlass.GroupAction';\n\n// ========================================\n// SIDEBAR GROUP CONTENT\n// ========================================\n\nexport interface SidebarGroupContentProps extends ComponentPropsWithoutRef<'div'> {\n children: ReactNode;\n}\n\n/**\n * SidebarGlass.GroupContent - Content wrapper for group items\n */\nexport const SidebarGroupContent = forwardRef<HTMLDivElement, SidebarGroupContentProps>(\n ({ children, className, ...props }, ref) => {\n return (\n <div\n ref={ref}\n data-sidebar=\"group-content\"\n className={cn('flex flex-col gap-1', className)}\n {...props}\n >\n {children}\n </div>\n );\n }\n);\n\nSidebarGroupContent.displayName = 'SidebarGlass.GroupContent';\n\n// ========================================\n// SIDEBAR MENU\n// ========================================\n\nexport interface SidebarMenuProps extends ComponentPropsWithoutRef<'ul'> {\n children: ReactNode;\n}\n\n/**\n * SidebarGlass.Menu - Container for menu items\n */\nexport const SidebarMenu = forwardRef<HTMLUListElement, SidebarMenuProps>(\n ({ children, className, ...props }, ref) => {\n return (\n <ul\n ref={ref}\n data-sidebar=\"menu\"\n className={cn('flex w-full flex-col gap-1', className)}\n {...props}\n >\n {children}\n </ul>\n );\n }\n);\n\nSidebarMenu.displayName = 'SidebarGlass.Menu';\n\n// ========================================\n// SIDEBAR MENU ITEM\n// ========================================\n\nexport interface SidebarMenuItemProps extends ComponentPropsWithoutRef<'li'> {\n children: ReactNode;\n}\n\n/**\n * SidebarGlass.MenuItem - Container for a single menu item\n */\nexport const SidebarMenuItem = forwardRef<HTMLLIElement, SidebarMenuItemProps>(\n ({ children, className, ...props }, ref) => {\n return (\n <li\n ref={ref}\n data-sidebar=\"menu-item\"\n className={cn('group/menu-item relative text-[var(--sidebar-foreground)]/60', className)}\n {...props}\n >\n {children}\n </li>\n );\n }\n);\n\nSidebarMenuItem.displayName = 'SidebarGlass.MenuItem';\n\n// ========================================\n// SIDEBAR MENU BUTTON\n// ========================================\n\nexport type SidebarMenuButtonSize = 'default' | 'sm' | 'lg';\nexport type SidebarMenuButtonVariant = 'default' | 'outline';\n\nexport interface SidebarMenuButtonProps extends ComponentPropsWithoutRef<'button'> {\n /** Render as child element */\n asChild?: boolean;\n /** Whether this item is active */\n isActive?: boolean;\n /** Tooltip text when collapsed */\n tooltip?: string | ReactNode;\n /** Button size */\n size?: SidebarMenuButtonSize;\n /** Button variant */\n variant?: SidebarMenuButtonVariant;\n}\n\nconst menuButtonSizeClasses: Record<SidebarMenuButtonSize, string> = {\n default: 'h-8 text-sm',\n sm: 'h-7 text-xs',\n lg: 'h-10 text-sm group-data-[state=collapsed]/sidebar:!p-0',\n};\n\n/**\n * SidebarGlass.MenuButton - Interactive menu button\n */\nexport const SidebarMenuButton = forwardRef<HTMLButtonElement, SidebarMenuButtonProps>(\n (\n {\n asChild = false,\n isActive = false,\n tooltip,\n size = 'default',\n variant = 'default',\n className,\n children,\n ...props\n },\n ref\n ) => {\n const { state, isMobile } = useSidebar();\n const Comp = asChild ? Slot : 'button';\n\n const button = (\n <Comp\n ref={ref}\n data-sidebar=\"menu-button\"\n data-active={isActive}\n data-size={size}\n className={cn(\n 'peer/menu-button flex w-full items-center gap-2',\n 'overflow-hidden rounded-md px-2',\n 'ring-[var(--sidebar-ring)]',\n 'transition-[width,height,padding] duration-200 ease-linear',\n 'focus-visible:outline-none focus-visible:ring-2',\n 'active:bg-[var(--sidebar-accent)] active:text-[var(--sidebar-accent-foreground)]',\n 'disabled:pointer-events-none disabled:opacity-50',\n 'aria-disabled:pointer-events-none aria-disabled:opacity-50',\n '[&>span:last-child]:truncate',\n '[&>svg]:size-4 [&>svg]:shrink-0',\n // Size variants\n menuButtonSizeClasses[size],\n // Variant styles\n variant === 'default' && [\n 'hover:bg-[var(--sidebar-accent)] hover:text-[var(--sidebar-accent-foreground)]',\n ],\n variant === 'outline' && [\n 'bg-transparent shadow-none',\n 'hover:bg-[var(--sidebar-accent)] hover:text-[var(--sidebar-accent-foreground)]',\n 'hover:shadow-[0_0_0_1px_var(--sidebar-border)]',\n ],\n // Active state\n isActive && [\n 'bg-[var(--sidebar-primary)] text-[var(--sidebar-primary-foreground)]',\n 'hover:bg-[var(--sidebar-primary)] hover:text-[var(--sidebar-primary-foreground)]',\n ],\n // Collapsed state - icon only\n 'group-data-[state=collapsed]/sidebar:w-8 group-data-[state=collapsed]/sidebar:!px-0',\n 'group-data-[state=collapsed]/sidebar:justify-center',\n className\n )}\n {...props}\n >\n {children}\n </Comp>\n );\n\n // No tooltip on mobile or expanded state\n if (!tooltip || isMobile || state === 'expanded') {\n return button;\n }\n\n return (\n <TooltipGlassSimple\n content={typeof tooltip === 'string' ? tooltip : String(tooltip)}\n side=\"right\"\n >\n {button}\n </TooltipGlassSimple>\n );\n }\n);\n\nSidebarMenuButton.displayName = 'SidebarGlass.MenuButton';\n\n// ========================================\n// SIDEBAR MENU ACTION\n// ========================================\n\nexport interface SidebarMenuActionProps extends ComponentPropsWithoutRef<'button'> {\n /** Render as child element */\n asChild?: boolean;\n /** Only show on hover */\n showOnHover?: boolean;\n}\n\n/**\n * SidebarGlass.MenuAction - Action button within menu item\n */\nexport const SidebarMenuAction = forwardRef<HTMLButtonElement, SidebarMenuActionProps>(\n ({ asChild = false, showOnHover = false, className, ...props }, ref) => {\n const Comp = asChild ? Slot : 'button';\n\n return (\n <Comp\n ref={ref}\n data-sidebar=\"menu-action\"\n className={cn(\n 'absolute right-1 top-1.5',\n 'flex aspect-square w-5 items-center justify-center',\n 'rounded-md p-0 text-[var(--sidebar-foreground)]/60',\n 'ring-[var(--sidebar-ring)]',\n 'transition-transform hover:bg-[var(--sidebar-accent)]',\n 'hover:text-[var(--sidebar-foreground)]',\n 'focus-visible:outline-none focus-visible:ring-2',\n '[&>svg]:size-4 [&>svg]:shrink-0',\n // Hide when collapsed\n 'group-data-[state=collapsed]/sidebar:hidden',\n // Show on hover\n showOnHover &&\n 'peer-hover/menu-button:opacity-100 group-focus-within/menu-item:opacity-100',\n showOnHover && 'data-[state=open]:opacity-100 md:opacity-0',\n // Tap area for touch\n 'after:absolute after:-inset-2 after:md:hidden',\n className\n )}\n {...props}\n />\n );\n }\n);\n\nSidebarMenuAction.displayName = 'SidebarGlass.MenuAction';\n\n// ========================================\n// SIDEBAR MENU BADGE\n// ========================================\n\nexport interface SidebarMenuBadgeProps extends ComponentPropsWithoutRef<'div'> {\n children: ReactNode;\n}\n\n/**\n * SidebarGlass.MenuBadge - Badge within menu item\n */\nexport const SidebarMenuBadge = forwardRef<HTMLDivElement, SidebarMenuBadgeProps>(\n ({ children, className, ...props }, ref) => {\n return (\n <div\n ref={ref}\n data-sidebar=\"menu-badge\"\n className={cn(\n 'pointer-events-none absolute right-1 flex h-5 min-w-5',\n 'select-none items-center justify-center',\n 'rounded-md px-1 text-xs font-medium tabular-nums',\n 'text-[var(--sidebar-foreground)]/60',\n // Hide when collapsed\n 'group-data-[state=collapsed]/sidebar:hidden',\n className\n )}\n {...props}\n >\n {children}\n </div>\n );\n }\n);\n\nSidebarMenuBadge.displayName = 'SidebarGlass.MenuBadge';\n\n// ========================================\n// SIDEBAR MENU SKELETON\n// ========================================\n\nexport interface SidebarMenuSkeletonProps extends ComponentPropsWithoutRef<'div'> {\n /** Show icon placeholder */\n showIcon?: boolean;\n}\n\n/**\n * SidebarGlass.MenuSkeleton - Loading skeleton for menu items\n */\nexport const SidebarMenuSkeleton = forwardRef<HTMLDivElement, SidebarMenuSkeletonProps>(\n ({ showIcon = false, className, ...props }, ref) => {\n // Fixed width for consistent appearance (avoids Math.random during render)\n const width = '70%';\n\n return (\n <div\n ref={ref}\n data-sidebar=\"menu-skeleton\"\n className={cn('flex h-8 items-center gap-2 rounded-md px-2', className)}\n {...props}\n >\n {showIcon && <SkeletonGlass className=\"size-4 rounded-md\" />}\n <SkeletonGlass\n className=\"h-4 max-w-[var(--skeleton-width)] flex-1\"\n style={{ '--skeleton-width': width } as CSSProperties}\n />\n </div>\n );\n }\n);\n\nSidebarMenuSkeleton.displayName = 'SidebarGlass.MenuSkeleton';\n\n// ========================================\n// SIDEBAR MENU SUB (SUBMENU CONTAINER)\n// ========================================\n\n// Context for submenu state\ninterface SidebarMenuSubContextValue {\n open: boolean;\n}\n\nconst SidebarMenuSubContext = createContext<SidebarMenuSubContextValue | null>(null);\n\nexport interface SidebarMenuSubProps extends ComponentPropsWithoutRef<'ul'> {\n children: ReactNode;\n}\n\n/**\n * SidebarGlass.MenuSub - Container for submenu items\n */\nexport const SidebarMenuSub = forwardRef<HTMLUListElement, SidebarMenuSubProps>(\n ({ children, className, ...props }, ref) => {\n return (\n <SidebarMenuSubContext.Provider value={{ open: true }}>\n <ul\n ref={ref}\n data-sidebar=\"menu-sub\"\n className={cn(\n 'flex min-w-0 flex-col gap-1',\n 'mx-3.5 border-l border-[var(--sidebar-border)] px-2.5 py-0.5',\n // Hide when collapsed\n 'group-data-[state=collapsed]/sidebar:hidden',\n className\n )}\n {...props}\n >\n {children}\n </ul>\n </SidebarMenuSubContext.Provider>\n );\n }\n);\n\nSidebarMenuSub.displayName = 'SidebarGlass.MenuSub';\n\n// ========================================\n// SIDEBAR MENU SUB ITEM\n// ========================================\n\nexport interface SidebarMenuSubItemProps extends ComponentPropsWithoutRef<'li'> {\n children: ReactNode;\n}\n\n/**\n * SidebarGlass.MenuSubItem - Container for a submenu item\n */\nexport const SidebarMenuSubItem = forwardRef<HTMLLIElement, SidebarMenuSubItemProps>(\n ({ children, className, ...props }, ref) => {\n return (\n <li ref={ref} data-sidebar=\"menu-sub-item\" className={cn(className)} {...props}>\n {children}\n </li>\n );\n }\n);\n\nSidebarMenuSubItem.displayName = 'SidebarGlass.MenuSubItem';\n\n// ========================================\n// SIDEBAR MENU SUB BUTTON\n// ========================================\n\nexport interface SidebarMenuSubButtonProps extends ComponentPropsWithoutRef<'a'> {\n /** Render as child element */\n asChild?: boolean;\n /** Whether this item is active */\n isActive?: boolean;\n /** Button size */\n size?: 'sm' | 'md';\n}\n\n/**\n * SidebarGlass.MenuSubButton - Interactive submenu button\n */\nexport const SidebarMenuSubButton = forwardRef<HTMLAnchorElement, SidebarMenuSubButtonProps>(\n ({ asChild = false, isActive = false, size = 'md', className, ...props }, ref) => {\n const Comp = asChild ? Slot : 'a';\n\n return (\n <Comp\n ref={ref}\n data-sidebar=\"menu-sub-button\"\n data-active={isActive}\n data-size={size}\n className={cn(\n 'flex min-w-0 items-center gap-2',\n '-ml-px rounded-md border-l border-transparent',\n 'text-[var(--sidebar-foreground)]/60',\n 'ring-[var(--sidebar-ring)]',\n 'transition-colors',\n 'hover:border-[var(--sidebar-border)]',\n 'hover:bg-[var(--sidebar-accent)]',\n 'hover:text-[var(--sidebar-accent-foreground)]',\n 'focus-visible:outline-none focus-visible:ring-2',\n 'active:bg-[var(--sidebar-accent)] active:text-[var(--sidebar-accent-foreground)]',\n 'disabled:pointer-events-none disabled:opacity-50',\n '[&>span:last-child]:truncate',\n '[&>svg]:size-4 [&>svg]:shrink-0',\n // Size variants\n size === 'sm' && 'h-7 px-2 text-xs',\n size === 'md' && 'h-8 px-2 text-sm',\n // Active state\n isActive && ['border-[var(--sidebar-primary)]', 'text-[var(--sidebar-foreground)]'],\n className\n )}\n {...props}\n />\n );\n }\n);\n\nSidebarMenuSubButton.displayName = 'SidebarGlass.MenuSubButton';\n\n// Export hook for submenu context (optional use)\nexport function useSidebarMenuSub() {\n const context = useContext(SidebarMenuSubContext);\n return context;\n}\n"
|
|
18
|
+
}
|
|
19
|
+
],
|
|
20
|
+
"categories": [
|
|
21
|
+
"ui"
|
|
22
|
+
],
|
|
23
|
+
"cssVars": {
|
|
24
|
+
"light": {
|
|
25
|
+
"--glass-bg": "rgba(255, 255, 255, 0.1)",
|
|
26
|
+
"--glass-border": "rgba(255, 255, 255, 0.2)",
|
|
27
|
+
"--blur-sm": "8px",
|
|
28
|
+
"--blur-md": "16px",
|
|
29
|
+
"--blur-lg": "24px"
|
|
30
|
+
},
|
|
31
|
+
"dark": {
|
|
32
|
+
"--glass-bg": "rgba(255, 255, 255, 0.05)",
|
|
33
|
+
"--glass-border": "rgba(255, 255, 255, 0.1)",
|
|
34
|
+
"--blur-sm": "8px",
|
|
35
|
+
"--blur-md": "16px",
|
|
36
|
+
"--blur-lg": "24px"
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "split-layout-accordion",
|
|
4
|
+
"type": "registry:block",
|
|
5
|
+
"title": "Split Layout Accordion",
|
|
6
|
+
"description": "SplitLayoutGlass Accordion Components",
|
|
7
|
+
"dependencies": [
|
|
8
|
+
"lucide-react",
|
|
9
|
+
"react"
|
|
10
|
+
],
|
|
11
|
+
"registryDependencies": [
|
|
12
|
+
"cn"
|
|
13
|
+
],
|
|
14
|
+
"files": [
|
|
15
|
+
{
|
|
16
|
+
"path": "components/glass/composite/split-layout-accordion.tsx",
|
|
17
|
+
"type": "registry:component",
|
|
18
|
+
"content": "/* eslint-disable react-refresh/only-export-components */\n/**\n * SplitLayoutGlass Accordion Components\n *\n * Mobile-specific components that render content in accordion pattern.\n * Details expand below the selected item instead of showing in a separate panel.\n *\n * @module split-layout-accordion\n */\n\nimport { forwardRef, type ReactNode, useId, useRef, useEffect, useCallback, useState } from 'react';\nimport { ChevronDown } from 'lucide-react';\nimport { cn } from '@/lib/utils';\nimport { GlassCard } from '@/components/glass/ui/glass-card';\nimport { useSplitLayout, useSplitLayoutOptional } from './split-layout-context';\n\n// ========================================\n// ACCORDION ROOT\n// ========================================\n\n/**\n * Props for SplitLayoutAccordion.Root component\n * Container for accordion items in mobile view\n */\nexport interface SplitLayoutAccordionRootProps extends React.HTMLAttributes<HTMLDivElement> {\n children: ReactNode;\n /**\n * ARIA label for the accordion region\n * @default \"Content accordion\"\n */\n label?: string;\n}\n\nconst SplitLayoutAccordionRoot = forwardRef<HTMLDivElement, SplitLayoutAccordionRootProps>(\n ({ children, label = 'Content accordion', className, ...props }, ref) => {\n const context = useSplitLayoutOptional();\n const intensity = context?.intensity ?? 'medium';\n\n return (\n <GlassCard\n asChild\n intensity={intensity}\n padding=\"none\"\n className={cn('divide-y divide-white/10', className)}\n >\n <div ref={ref} role=\"region\" aria-label={label} data-split-accordion=\"\" {...props}>\n {children}\n </div>\n </GlassCard>\n );\n }\n);\n\nSplitLayoutAccordionRoot.displayName = 'SplitLayoutAccordion.Root';\n\n// ========================================\n// ACCORDION ITEM\n// ========================================\n\n/**\n * Props for SplitLayoutAccordion.Item component\n * A single accordion item with trigger and collapsible content\n */\nexport interface SplitLayoutAccordionItemProps extends Omit<\n React.HTMLAttributes<HTMLDivElement>,\n 'children'\n> {\n /**\n * Unique key for this item (used for selection)\n */\n itemKey: string;\n /**\n * Content shown in the trigger/header area\n */\n trigger: ReactNode;\n /**\n * Content shown when expanded (details)\n */\n children: ReactNode;\n /**\n * Disable this item\n * @default false\n */\n disabled?: boolean;\n}\n\nconst SplitLayoutAccordionItem = forwardRef<HTMLDivElement, SplitLayoutAccordionItemProps>(\n ({ itemKey, trigger, children, disabled = false, className, ...props }, ref) => {\n const { selectedKey, setSelectedKey } = useSplitLayout();\n const isExpanded = selectedKey === itemKey;\n const contentRef = useRef<HTMLDivElement>(null);\n const [contentHeight, setContentHeight] = useState<number | null>(null);\n const triggerId = useId();\n const contentId = useId();\n\n // Measure content height for smooth animation\n useEffect(() => {\n if (contentRef.current) {\n setContentHeight(contentRef.current.scrollHeight);\n }\n }, [children, isExpanded]);\n\n const handleToggle = useCallback(() => {\n if (disabled) return;\n setSelectedKey(isExpanded ? null : itemKey);\n }, [disabled, isExpanded, itemKey, setSelectedKey]);\n\n const handleKeyDown = useCallback(\n (e: React.KeyboardEvent) => {\n if (disabled) return;\n\n switch (e.key) {\n case 'Enter':\n case ' ':\n e.preventDefault();\n handleToggle();\n break;\n case 'ArrowDown': {\n e.preventDefault();\n // Focus next item trigger\n const next = (e.currentTarget as HTMLElement).parentElement?.nextElementSibling;\n const nextTrigger = next?.querySelector('[data-accordion-trigger]') as HTMLElement;\n nextTrigger?.focus();\n break;\n }\n case 'ArrowUp': {\n e.preventDefault();\n // Focus previous item trigger\n const prev = (e.currentTarget as HTMLElement).parentElement?.previousElementSibling;\n const prevTrigger = prev?.querySelector('[data-accordion-trigger]') as HTMLElement;\n prevTrigger?.focus();\n break;\n }\n case 'Home': {\n e.preventDefault();\n // Focus first item trigger\n const first = (e.currentTarget as HTMLElement)\n .closest('[data-split-accordion]')\n ?.querySelector('[data-accordion-trigger]') as HTMLElement;\n first?.focus();\n break;\n }\n case 'End': {\n e.preventDefault();\n // Focus last item trigger\n const triggers = (e.currentTarget as HTMLElement)\n .closest('[data-split-accordion]')\n ?.querySelectorAll('[data-accordion-trigger]');\n const last = triggers?.[triggers.length - 1] as HTMLElement;\n last?.focus();\n break;\n }\n }\n },\n [disabled, handleToggle]\n );\n\n return (\n <div\n ref={ref}\n data-accordion-item=\"\"\n data-state={isExpanded ? 'open' : 'closed'}\n data-disabled={disabled || undefined}\n className={cn('overflow-hidden', disabled && 'opacity-50 cursor-not-allowed', className)}\n {...props}\n >\n {/* Trigger/Header */}\n <button\n type=\"button\"\n id={triggerId}\n data-accordion-trigger=\"\"\n aria-expanded={isExpanded}\n aria-controls={contentId}\n aria-disabled={disabled}\n disabled={disabled}\n onClick={handleToggle}\n onKeyDown={handleKeyDown}\n className={cn(\n 'flex w-full items-center justify-between gap-4',\n 'p-4 text-left',\n 'transition-colors duration-200',\n 'hover:bg-white/5 focus-visible:bg-white/5',\n 'focus:outline-none focus-visible:ring-2 focus-visible:ring-white/20 focus-visible:ring-inset',\n disabled && 'pointer-events-none'\n )}\n >\n <div className=\"flex-1\">{trigger}</div>\n <ChevronDown\n className={cn(\n 'h-4 w-4 shrink-0 text-white/60',\n 'transition-transform duration-300 ease-out',\n isExpanded && 'rotate-180'\n )}\n />\n </button>\n\n {/* Content */}\n <div\n id={contentId}\n ref={contentRef}\n role=\"region\"\n aria-labelledby={triggerId}\n data-accordion-content=\"\"\n data-state={isExpanded ? 'open' : 'closed'}\n className={cn('overflow-hidden', 'transition-[max-height,opacity] duration-300 ease-out')}\n style={{\n maxHeight: isExpanded ? (contentHeight ?? 'auto') : 0,\n opacity: isExpanded ? 1 : 0,\n }}\n >\n <div className=\"p-4 pt-0 border-t border-white/5\">{children}</div>\n </div>\n </div>\n );\n }\n);\n\nSplitLayoutAccordionItem.displayName = 'SplitLayoutAccordion.Item';\n\n// ========================================\n// COMPOUND EXPORT\n// ========================================\n\n/**\n * SplitLayoutAccordion compound component for mobile accordion view\n *\n * @example\n * ```tsx\n * // Use with mobileMode=\"accordion\" in Provider\n * <SplitLayoutGlass.Provider mobileMode=\"accordion\">\n * {isMobile ? (\n * <SplitLayoutAccordion.Root>\n * {years.map((year) => (\n * <SplitLayoutAccordion.Item\n * key={year.id}\n * itemKey={year.id}\n * trigger={<YearTitle year={year} />}\n * >\n * <YearDetails year={year} />\n * </SplitLayoutAccordion.Item>\n * ))}\n * </SplitLayoutAccordion.Root>\n * ) : (\n * <SplitLayoutGlass.Root>...</SplitLayoutGlass.Root>\n * )}\n * </SplitLayoutGlass.Provider>\n * ```\n */\nexport const SplitLayoutAccordion = {\n Root: SplitLayoutAccordionRoot,\n Item: SplitLayoutAccordionItem,\n};\n"
|
|
19
|
+
}
|
|
20
|
+
],
|
|
21
|
+
"categories": [
|
|
22
|
+
"composite"
|
|
23
|
+
]
|
|
24
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "split-layout-context",
|
|
4
|
+
"type": "registry:block",
|
|
5
|
+
"title": "Split Layout Context",
|
|
6
|
+
"description": "SplitLayoutGlass Context",
|
|
7
|
+
"dependencies": [],
|
|
8
|
+
"registryDependencies": [
|
|
9
|
+
"variants"
|
|
10
|
+
],
|
|
11
|
+
"files": [
|
|
12
|
+
{
|
|
13
|
+
"path": "components/glass/composite/split-layout-context.tsx",
|
|
14
|
+
"type": "registry:component",
|
|
15
|
+
"content": "/* eslint-disable react-refresh/only-export-components */\n/**\n * SplitLayoutGlass Context\n *\n * Provides state management for compound SplitLayoutGlass component.\n * Handles selection state, mobile detection, and toggle functionality.\n *\n * @module split-layout-context\n */\n\nimport {\n createContext,\n useContext,\n useState,\n useCallback,\n useMemo,\n useEffect,\n type FC,\n type ReactNode,\n} from 'react';\n\n// ========================================\n// TYPES\n// ========================================\n\nimport type { IntensityType } from '@/lib/variants/glass-card-variants';\n\nexport type Breakpoint = 'sm' | 'md' | 'lg' | 'xl' | '2xl';\nexport type MobileMode = 'stack' | 'accordion' | 'drawer';\n\n/**\n * Context value for SplitLayoutGlass compound components\n *\n * API is designed to be consistent with shadcn/ui Sidebar patterns.\n */\nexport interface SplitLayoutContextValue {\n // === STATE ===\n /** Currently selected key for master-detail pattern */\n selectedKey: string | null;\n /** Set selected key */\n setSelectedKey: (key: string | null) => void;\n\n /** Sidebar open state (desktop) */\n isOpen: boolean;\n /** Set sidebar open state */\n setIsOpen: (open: boolean) => void;\n\n /** Mobile drawer/accordion open state */\n isMobileOpen: boolean;\n /** Set mobile open state */\n setMobileOpen: (open: boolean) => void;\n\n // === SHADCN ALIASES ===\n /** Sidebar state: \"expanded\" | \"collapsed\" (shadcn pattern) */\n state: 'expanded' | 'collapsed';\n /** Alias for isOpen (shadcn naming) */\n open: boolean;\n /** Alias for setIsOpen (shadcn naming) */\n setOpen: (open: boolean) => void;\n /** Alias for isMobileOpen (shadcn naming) */\n openMobile: boolean;\n /** Alias for setMobileOpen (shadcn naming) */\n setOpenMobile: (open: boolean) => void;\n /** Alias for toggle (shadcn naming) */\n toggleSidebar: () => void;\n\n // === RESPONSIVE ===\n /** Current viewport is below breakpoint */\n isMobile: boolean;\n\n // === CONFIG ===\n /** Breakpoint for desktop layout */\n breakpoint: Breakpoint;\n /** Mobile layout mode */\n mobileMode: MobileMode;\n /** Glass intensity for panels */\n intensity: IntensityType;\n /** Sticky offset from viewport top */\n stickyOffset: number;\n\n // === ACTIONS ===\n /** Toggle sidebar (desktop) or drawer (mobile) */\n toggle: () => void;\n}\n\n// ========================================\n// CONTEXT\n// ========================================\n\nconst SplitLayoutContext = createContext<SplitLayoutContextValue | null>(null);\n\n/**\n * Hook to access SplitLayout context\n *\n * @throws Error if used outside of SplitLayoutGlass.Provider\n *\n * @example\n * ```tsx\n * function YearCard({ year }) {\n * const { selectedKey, setSelectedKey, isMobile } = useSplitLayout();\n * const isSelected = selectedKey === year.id;\n *\n * return (\n * <button\n * onClick={() => setSelectedKey(year.id)}\n * className={cn('p-3', isSelected && 'bg-primary/10')}\n * >\n * {year.year}\n * </button>\n * );\n * }\n * ```\n */\nexport function useSplitLayout(): SplitLayoutContextValue {\n const context = useContext(SplitLayoutContext);\n if (!context) {\n throw new Error(\n 'useSplitLayout must be used within SplitLayoutGlass.Provider. ' +\n 'Wrap your component tree with <SplitLayoutGlass.Provider>.'\n );\n }\n return context;\n}\n\n/**\n * Optional hook that returns null if outside provider (doesn't throw)\n */\nexport function useSplitLayoutOptional(): SplitLayoutContextValue | null {\n return useContext(SplitLayoutContext);\n}\n\n// ========================================\n// BREAKPOINT MAP\n// ========================================\n\nconst BREAKPOINT_VALUES: Record<Breakpoint, number> = {\n sm: 640,\n md: 768,\n lg: 1024,\n xl: 1440,\n '2xl': 1536,\n};\n\n// ========================================\n// PROVIDER\n// ========================================\n\n/**\n * Props for SplitLayoutGlass.Provider\n */\nexport interface SplitLayoutProviderProps {\n children: ReactNode;\n\n // === SELECTION STATE ===\n /** Controlled selected key */\n selectedKey?: string | null;\n /** Callback when selected key changes */\n onSelectedKeyChange?: (key: string | null) => void;\n /** Default selected key (uncontrolled) */\n defaultSelectedKey?: string | null;\n\n // === OPEN STATE ===\n /** Controlled open state (desktop) */\n open?: boolean;\n /** Callback when open state changes */\n onOpenChange?: (open: boolean) => void;\n /** Default open state */\n defaultOpen?: boolean;\n\n // === CONFIG ===\n /** Breakpoint for mobile/desktop switch @default \"md\" */\n breakpoint?: Breakpoint;\n /** Mobile layout mode @default \"stack\" */\n mobileMode?: MobileMode;\n /** Glass intensity for panels @default \"medium\" */\n intensity?: IntensityType;\n /** Sticky offset from viewport top @default 24 */\n stickyOffset?: number;\n\n // === PERSISTENCE ===\n /** URL param name for selectedKey persistence */\n urlParamName?: string;\n\n // === KEYBOARD ===\n /** Keyboard shortcut key for toggle (Cmd/Ctrl + key) @default \"b\" */\n keyboardShortcut?: string | false;\n}\n\n/**\n * Provider component for SplitLayoutGlass compound components\n *\n * @example\n * ```tsx\n * <SplitLayoutGlass.Provider\n * defaultSelectedKey=\"2024\"\n * mobileMode=\"accordion\"\n * onSelectedKeyChange={(key) => console.log('Selected:', key)}\n * >\n * <SplitLayoutGlass.Root>\n * ...\n * </SplitLayoutGlass.Root>\n * </SplitLayoutGlass.Provider>\n * ```\n */\nexport const SplitLayoutProvider: FC<SplitLayoutProviderProps> = ({\n children,\n selectedKey: controlledSelectedKey,\n onSelectedKeyChange,\n defaultSelectedKey = null,\n open: controlledOpen,\n onOpenChange,\n defaultOpen = true,\n breakpoint = 'md',\n mobileMode = 'stack',\n intensity = 'medium',\n stickyOffset = 24,\n urlParamName,\n keyboardShortcut = 'b',\n}) => {\n // === SELECTION STATE (controlled/uncontrolled) ===\n const [internalSelectedKey, setInternalSelectedKey] = useState<string | null>(() => {\n // Try to read from URL if urlParamName is provided\n if (urlParamName && typeof window !== 'undefined') {\n const params = new URLSearchParams(window.location.search);\n const urlValue = params.get(urlParamName);\n if (urlValue) return urlValue;\n }\n return defaultSelectedKey;\n });\n\n const isSelectedKeyControlled = controlledSelectedKey !== undefined;\n const selectedKey = isSelectedKeyControlled ? controlledSelectedKey : internalSelectedKey;\n\n const setSelectedKey = useCallback(\n (key: string | null) => {\n if (!isSelectedKeyControlled) {\n setInternalSelectedKey(key);\n }\n onSelectedKeyChange?.(key);\n\n // Update URL if urlParamName is provided\n if (urlParamName && typeof window !== 'undefined') {\n const url = new URL(window.location.href);\n if (key) {\n url.searchParams.set(urlParamName, key);\n } else {\n url.searchParams.delete(urlParamName);\n }\n window.history.replaceState({}, '', url.toString());\n }\n },\n [isSelectedKeyControlled, onSelectedKeyChange, urlParamName]\n );\n\n // === OPEN STATE (controlled/uncontrolled) ===\n const [internalOpen, setInternalOpen] = useState(defaultOpen);\n const isOpenControlled = controlledOpen !== undefined;\n const isOpen = isOpenControlled ? controlledOpen : internalOpen;\n\n const setIsOpen = useCallback(\n (open: boolean) => {\n if (!isOpenControlled) {\n setInternalOpen(open);\n }\n onOpenChange?.(open);\n },\n [isOpenControlled, onOpenChange]\n );\n\n // === MOBILE OPEN STATE ===\n const [isMobileOpen, setMobileOpen] = useState(false);\n\n // === RESPONSIVE DETECTION ===\n const [isMobile, setIsMobile] = useState(() => {\n if (typeof window === 'undefined') return false;\n return window.innerWidth < BREAKPOINT_VALUES[breakpoint];\n });\n\n useEffect(() => {\n if (typeof window === 'undefined') return;\n\n const checkMobile = () => {\n setIsMobile(window.innerWidth < BREAKPOINT_VALUES[breakpoint]);\n };\n\n // Check on mount\n checkMobile();\n\n // Listen for resize\n window.addEventListener('resize', checkMobile);\n return () => window.removeEventListener('resize', checkMobile);\n }, [breakpoint]);\n\n // === TOGGLE ACTION ===\n const toggle = useCallback(() => {\n if (isMobile) {\n setMobileOpen((prev) => !prev);\n } else {\n setIsOpen(!isOpen);\n }\n }, [isMobile, isOpen, setIsOpen]);\n\n // === KEYBOARD NAVIGATION ===\n useEffect(() => {\n if (!keyboardShortcut) return;\n\n const handleKeyDown = (e: KeyboardEvent) => {\n // Escape - close mobile drawer/accordion\n if (e.key === 'Escape' && isMobile && isMobileOpen) {\n e.preventDefault();\n setMobileOpen(false);\n return;\n }\n\n // Cmd/Ctrl + key - toggle sidebar\n if (e.key === keyboardShortcut && (e.metaKey || e.ctrlKey)) {\n e.preventDefault();\n toggle();\n }\n };\n\n document.addEventListener('keydown', handleKeyDown);\n return () => document.removeEventListener('keydown', handleKeyDown);\n }, [keyboardShortcut, isMobile, isMobileOpen, toggle]);\n\n // === CONTEXT VALUE ===\n const value = useMemo<SplitLayoutContextValue>(\n () => ({\n // Original API\n selectedKey,\n setSelectedKey,\n isOpen,\n setIsOpen,\n isMobileOpen,\n setMobileOpen,\n isMobile,\n breakpoint,\n mobileMode,\n intensity,\n stickyOffset,\n toggle,\n\n // shadcn aliases\n state: isOpen ? 'expanded' : 'collapsed',\n open: isOpen,\n setOpen: setIsOpen,\n openMobile: isMobileOpen,\n setOpenMobile: setMobileOpen,\n toggleSidebar: toggle,\n }),\n [\n selectedKey,\n setSelectedKey,\n isOpen,\n setIsOpen,\n isMobileOpen,\n isMobile,\n breakpoint,\n mobileMode,\n intensity,\n stickyOffset,\n toggle,\n ]\n );\n\n return <SplitLayoutContext.Provider value={value}>{children}</SplitLayoutContext.Provider>;\n};\n\nSplitLayoutProvider.displayName = 'SplitLayoutGlass.Provider';\n"
|
|
16
|
+
}
|
|
17
|
+
],
|
|
18
|
+
"categories": [
|
|
19
|
+
"composite"
|
|
20
|
+
]
|
|
21
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "split-layout-glass",
|
|
4
|
+
"type": "registry:block",
|
|
5
|
+
"title": "Split Layout Glass",
|
|
6
|
+
"description": "SplitLayoutGlass Component (Compound API only)",
|
|
7
|
+
"dependencies": [
|
|
8
|
+
"@radix-ui/react-slot",
|
|
9
|
+
"lucide-react",
|
|
10
|
+
"react"
|
|
11
|
+
],
|
|
12
|
+
"registryDependencies": [
|
|
13
|
+
"cn"
|
|
14
|
+
],
|
|
15
|
+
"files": [
|
|
16
|
+
{
|
|
17
|
+
"path": "components/glass/composite/split-layout-glass.tsx",
|
|
18
|
+
"type": "registry:component",
|
|
19
|
+
"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 * <SplitLayoutGlass.Provider defaultSelectedKey=\"2024\">\n * <SplitLayoutGlass.Root ratio={{ sidebar: 1, main: 2 }}>\n * <SplitLayoutGlass.Sidebar>\n * <SplitLayoutGlass.SidebarHeader>Header</SplitLayoutGlass.SidebarHeader>\n * <SplitLayoutGlass.SidebarContent>Content</SplitLayoutGlass.SidebarContent>\n * </SplitLayoutGlass.Sidebar>\n * <SplitLayoutGlass.Main>\n * <SplitLayoutGlass.MainContent>Content</SplitLayoutGlass.MainContent>\n * </SplitLayoutGlass.Main>\n * </SplitLayoutGlass.Root>\n * </SplitLayoutGlass.Provider>\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<HTMLDivElement> {\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<HTMLDivElement, SplitLayoutRootProps>(\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 <div\n ref={ref}\n data-split-layout-root=\"\"\n data-state={context?.state ?? 'expanded'}\n className={cn(\n 'grid',\n // Align items to start so each column has height based on content\n 'items-start',\n mobileLayout === 'stack' && 'grid-cols-1',\n mobileLayout === 'main-only' && 'grid-cols-1 *:data-split-sidebar:hidden',\n mobileLayout === 'sidebar-only' && 'grid-cols-1 *:data-split-main:hidden',\n gridClasses[bp],\n gapClasses[bp],\n className\n )}\n style={cssVars}\n {...props}\n >\n {children}\n </div>\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<HTMLElement> {\n children: ReactNode;\n /**\n * ARIA label for accessibility\n * @default \"Sidebar navigation\"\n */\n label?: string;\n}\n\nconst SplitLayoutSidebar = forwardRef<HTMLElement, SplitLayoutSidebarProps>(\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 <GlassCard\n asChild\n intensity={intensity}\n padding=\"none\"\n className={cn(\n 'overflow-hidden rounded-xl',\n `${bp}:sticky`,\n `${bp}:top-[var(--sticky-offset)]`,\n `${bp}:max-h-[var(--sticky-max-height)]`,\n `${bp}:flex`,\n `${bp}:flex-col`,\n className\n )}\n >\n <aside ref={ref} data-split-sidebar=\"\" aria-label={label} {...props}>\n {children}\n </aside>\n </GlassCard>\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<HTMLDivElement> {\n children: ReactNode;\n}\n\nconst SplitLayoutSidebarHeader = forwardRef<HTMLDivElement, SplitLayoutSidebarHeaderProps>(\n ({ children, className, ...props }, ref) => (\n <div\n ref={ref}\n data-split-sidebar-header=\"\"\n className={cn('shrink-0 p-4 border-b border-white/10', className)}\n {...props}\n >\n {children}\n </div>\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<HTMLDivElement> {\n children: ReactNode;\n /**\n * Auto-wrap in ScrollArea for independent scrolling\n * @default true\n */\n scrollable?: boolean;\n}\n\nconst SplitLayoutSidebarContent = forwardRef<HTMLDivElement, SplitLayoutSidebarContentProps>(\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 <ScrollArea\n ref={ref as React.Ref<HTMLDivElement>}\n data-split-sidebar-content=\"\"\n className={cn('flex-1 min-h-0', className)}\n {...divProps}\n >\n <div className=\"p-4\">{children}</div>\n </ScrollArea>\n );\n }\n\n return (\n <div\n ref={ref}\n data-split-sidebar-content=\"\"\n className={cn('flex-1 min-h-0 overflow-auto p-4', className)}\n {...props}\n >\n {children}\n </div>\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<HTMLDivElement> {\n children: ReactNode;\n}\n\nconst SplitLayoutSidebarFooter = forwardRef<HTMLDivElement, SplitLayoutSidebarFooterProps>(\n ({ children, className, ...props }, ref) => (\n <div\n ref={ref}\n data-split-sidebar-footer=\"\"\n className={cn('shrink-0 p-4 border-t border-white/10', className)}\n {...props}\n >\n {children}\n </div>\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<HTMLElement> {\n children: ReactNode;\n /**\n * ARIA label for accessibility\n * @default \"Main content\"\n */\n label?: string;\n}\n\nconst SplitLayoutMain = forwardRef<HTMLElement, SplitLayoutMainProps>(\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 <GlassCard\n asChild\n intensity={intensity}\n padding=\"none\"\n className={cn(\n 'overflow-hidden rounded-xl',\n `${bp}:sticky`,\n `${bp}:top-[var(--sticky-offset)]`,\n `${bp}:max-h-[var(--sticky-max-height)]`,\n `${bp}:flex`,\n `${bp}:flex-col`,\n className\n )}\n >\n <main ref={ref} data-split-main=\"\" aria-label={label} {...props}>\n {children}\n </main>\n </GlassCard>\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<HTMLDivElement> {\n children: ReactNode;\n}\n\nconst SplitLayoutMainHeader = forwardRef<HTMLDivElement, SplitLayoutMainHeaderProps>(\n ({ children, className, ...props }, ref) => (\n <div\n ref={ref}\n data-split-main-header=\"\"\n className={cn('shrink-0 p-6 border-b border-white/10', className)}\n {...props}\n >\n {children}\n </div>\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<HTMLDivElement> {\n children: ReactNode;\n /**\n * Auto-wrap in ScrollArea for independent scrolling\n * @default true\n */\n scrollable?: boolean;\n}\n\nconst SplitLayoutMainContent = forwardRef<HTMLDivElement, SplitLayoutMainContentProps>(\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 <ScrollArea\n ref={ref as React.Ref<HTMLDivElement>}\n data-split-main-content=\"\"\n className={cn('flex-1 min-h-0', className)}\n {...divProps}\n >\n <div className=\"p-6\">{children}</div>\n </ScrollArea>\n );\n }\n\n return (\n <div\n ref={ref}\n data-split-main-content=\"\"\n className={cn('flex-1 min-h-0 overflow-auto p-6', className)}\n {...props}\n >\n {children}\n </div>\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<HTMLDivElement> {\n children: ReactNode;\n}\n\nconst SplitLayoutMainFooter = forwardRef<HTMLDivElement, SplitLayoutMainFooterProps>(\n ({ children, className, ...props }, ref) => (\n <div\n ref={ref}\n data-split-main-footer=\"\"\n className={cn('shrink-0 p-6 border-t border-white/10', className)}\n {...props}\n >\n {children}\n </div>\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<HTMLButtonElement> {\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<HTMLButtonElement, SplitLayoutTriggerProps>(\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 <Slot\n ref={ref}\n onClick={toggle}\n aria-label={currentOpen ? 'Close sidebar' : 'Open sidebar'}\n aria-expanded={currentOpen}\n data-state={currentOpen ? 'open' : 'closed'}\n className={cn(visibilityClass, className)}\n {...props}\n >\n {children}\n </Slot>\n );\n }\n\n return (\n <ButtonGlass\n ref={ref}\n variant=\"ghost\"\n size=\"icon\"\n onClick={toggle}\n aria-label={currentOpen ? 'Close sidebar' : 'Open sidebar'}\n aria-expanded={currentOpen}\n data-state={currentOpen ? 'open' : 'closed'}\n className={cn(visibilityClass, className)}\n {...props}\n >\n {children ?? <Icon className=\"h-5 w-5\" />}\n </ButtonGlass>\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 * <SplitLayoutGlass.Provider>\n * <SplitLayoutGlass.Root>\n * <SplitLayoutGlass.Sidebar>...</SplitLayoutGlass.Sidebar>\n * <SplitLayoutGlass.Main>...</SplitLayoutGlass.Main>\n * </SplitLayoutGlass.Root>\n * </SplitLayoutGlass.Provider>\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"
|
|
20
|
+
}
|
|
21
|
+
],
|
|
22
|
+
"categories": [
|
|
23
|
+
"composite"
|
|
24
|
+
]
|
|
25
|
+
}
|