index.tsx281 lines · main
| 1 | 'use client' |
| 2 | |
| 3 | import { cva, type VariantProps } from 'class-variance-authority' |
| 4 | import React, { createContext, useContext } from 'react' |
| 5 | import { cn } from 'ui' |
| 6 | import { Breadcrumb } from 'ui/src/components/shadcn/ui/breadcrumb' |
| 7 | |
| 8 | import { PageContainer } from '../PageContainer' |
| 9 | |
| 10 | // ============================================================================ |
| 11 | // Variants |
| 12 | // ============================================================================ |
| 13 | |
| 14 | const pageHeaderVariants = cva(['flex flex-col gap-4 w-full'], { |
| 15 | variants: { |
| 16 | size: { |
| 17 | default: 'pt-12', |
| 18 | small: 'pt-12', |
| 19 | large: 'pt-12', |
| 20 | full: 'pt-6', |
| 21 | }, |
| 22 | }, |
| 23 | defaultVariants: { |
| 24 | size: 'default', |
| 25 | }, |
| 26 | }) |
| 27 | |
| 28 | // ============================================================================ |
| 29 | // Context |
| 30 | // ============================================================================ |
| 31 | |
| 32 | type PageHeaderContextValue = { |
| 33 | size: 'default' | 'small' | 'large' | 'full' |
| 34 | } |
| 35 | |
| 36 | const PageHeaderContext = createContext<PageHeaderContextValue>({ |
| 37 | size: 'default', |
| 38 | }) |
| 39 | |
| 40 | const usePageHeaderContext = () => useContext(PageHeaderContext) |
| 41 | |
| 42 | // ============================================================================ |
| 43 | // Root |
| 44 | // ============================================================================ |
| 45 | |
| 46 | export type PageHeaderRootProps = React.ComponentProps<'div'> & |
| 47 | VariantProps<typeof pageHeaderVariants> |
| 48 | |
| 49 | /** |
| 50 | * Root component for page header. |
| 51 | * Renders children in order without searching for specific components. |
| 52 | */ |
| 53 | const PageHeaderRoot = ({ className, size, children, ...props }: PageHeaderRootProps) => { |
| 54 | const contextSize: 'default' | 'small' | 'large' | 'full' = size ?? 'default' |
| 55 | return ( |
| 56 | <PageHeaderContext.Provider value={{ size: contextSize }}> |
| 57 | <div |
| 58 | data-slot="page-header" |
| 59 | data-size={contextSize} |
| 60 | className={cn(pageHeaderVariants({ size: contextSize }), className)} |
| 61 | {...props} |
| 62 | > |
| 63 | {children} |
| 64 | </div> |
| 65 | </PageHeaderContext.Provider> |
| 66 | ) |
| 67 | } |
| 68 | |
| 69 | // ============================================================================ |
| 70 | // Breadcrumb |
| 71 | // ============================================================================ |
| 72 | |
| 73 | export type PageHeaderBreadcrumbProps = React.ComponentProps<typeof Breadcrumb> |
| 74 | |
| 75 | /** |
| 76 | * Breadcrumb component for page header. |
| 77 | * A wrapper around Breadcrumb with page header styling. |
| 78 | * Should be placed as the first child of PageHeader. |
| 79 | */ |
| 80 | const PageHeaderBreadcrumb = ({ className, children, ...props }: PageHeaderBreadcrumbProps) => { |
| 81 | const { size } = usePageHeaderContext() |
| 82 | return ( |
| 83 | <PageContainer size={size}> |
| 84 | <Breadcrumb |
| 85 | data-slot="page-header-breadcrumb" |
| 86 | className={cn('flex items-center gap-4 [&_li]:text-xs', className)} |
| 87 | {...props} |
| 88 | > |
| 89 | {children} |
| 90 | </Breadcrumb> |
| 91 | </PageContainer> |
| 92 | ) |
| 93 | } |
| 94 | PageHeaderBreadcrumb.displayName = 'PageHeaderBreadcrumb' |
| 95 | |
| 96 | // ============================================================================ |
| 97 | // Icon |
| 98 | // ============================================================================ |
| 99 | |
| 100 | export type PageHeaderIconProps = React.ComponentProps<'div'> |
| 101 | |
| 102 | /** |
| 103 | * Icon component for page header. |
| 104 | * Positioned to the left of title and description. |
| 105 | */ |
| 106 | const PageHeaderIcon = ({ className, ...props }: PageHeaderIconProps) => { |
| 107 | return ( |
| 108 | <div |
| 109 | data-slot="page-header-icon" |
| 110 | className={cn('text-foreground-light', className)} |
| 111 | {...props} |
| 112 | /> |
| 113 | ) |
| 114 | } |
| 115 | PageHeaderIcon.displayName = 'PageHeaderIcon' |
| 116 | |
| 117 | // ============================================================================ |
| 118 | // Summary |
| 119 | // ============================================================================ |
| 120 | |
| 121 | export type PageHeaderSummaryProps = React.ComponentProps<'div'> |
| 122 | |
| 123 | /** |
| 124 | * Summary component to contain title and description. |
| 125 | * Provides layout structure for text content. |
| 126 | * Should be placed inside PageHeaderMeta. |
| 127 | */ |
| 128 | const PageHeaderSummary = ({ className, children, ...props }: PageHeaderSummaryProps) => { |
| 129 | return ( |
| 130 | <div |
| 131 | data-slot="page-header-summary" |
| 132 | className={cn('flex flex-col gap-1', className)} |
| 133 | {...props} |
| 134 | > |
| 135 | {children} |
| 136 | </div> |
| 137 | ) |
| 138 | } |
| 139 | PageHeaderSummary.displayName = 'PageHeaderSummary' |
| 140 | |
| 141 | // ============================================================================ |
| 142 | // Title |
| 143 | // ============================================================================ |
| 144 | |
| 145 | export type PageHeaderTitleProps = React.ComponentProps<'h1'> |
| 146 | |
| 147 | /** |
| 148 | * Title component for page header. |
| 149 | * Primary heading for the page. |
| 150 | */ |
| 151 | const PageHeaderTitle = ({ className, children, ...props }: PageHeaderTitleProps) => { |
| 152 | return ( |
| 153 | <h1 data-slot="page-header-title" className={cn('heading-title', className)} {...props}> |
| 154 | {children} |
| 155 | </h1> |
| 156 | ) |
| 157 | } |
| 158 | |
| 159 | // ============================================================================ |
| 160 | // Description |
| 161 | // ============================================================================ |
| 162 | |
| 163 | export type PageHeaderDescriptionProps = React.ComponentProps<'div'> |
| 164 | |
| 165 | /** |
| 166 | * Description component for page header. |
| 167 | * Supporting text rendered below the title. |
| 168 | */ |
| 169 | const PageHeaderDescription = ({ className, children, ...props }: PageHeaderDescriptionProps) => { |
| 170 | return ( |
| 171 | <div |
| 172 | data-slot="page-header-description" |
| 173 | className={cn('heading-subSection text-foreground-light', className)} |
| 174 | {...props} |
| 175 | > |
| 176 | {children} |
| 177 | </div> |
| 178 | ) |
| 179 | } |
| 180 | |
| 181 | // ============================================================================ |
| 182 | // Meta |
| 183 | // ============================================================================ |
| 184 | |
| 185 | export type PageHeaderMetaProps = React.ComponentProps<'div'> |
| 186 | |
| 187 | /** |
| 188 | * Meta wrapper for page header. |
| 189 | * Contains icon, summary, and aside components with proper layout. |
| 190 | * Should be placed after PageHeaderBreadcrumb (if present) and before PageHeaderNavigationTabs. |
| 191 | * Uses CSS to style children based on their data-slot attributes. |
| 192 | */ |
| 193 | const PageHeaderMeta = ({ className, children, ...props }: PageHeaderMetaProps) => { |
| 194 | const { size } = usePageHeaderContext() |
| 195 | return ( |
| 196 | <PageContainer size={size}> |
| 197 | <div |
| 198 | data-slot="page-header-meta" |
| 199 | className={cn( |
| 200 | 'flex flex-col @xl:flex-row @xl:justify-between @xl:items-center gap-4', |
| 201 | '*:data-[slot="page-header-icon"]:shrink-0', |
| 202 | '*:data-[slot="page-header-summary"]:flex-1', |
| 203 | className |
| 204 | )} |
| 205 | {...props} |
| 206 | > |
| 207 | {children} |
| 208 | </div> |
| 209 | </PageContainer> |
| 210 | ) |
| 211 | } |
| 212 | PageHeaderMeta.displayName = 'PageHeaderMeta' |
| 213 | |
| 214 | // ============================================================================ |
| 215 | // Actions |
| 216 | // ============================================================================ |
| 217 | |
| 218 | export type PageHeaderAsideProps = React.ComponentProps<'div'> |
| 219 | |
| 220 | /** |
| 221 | * Actions component for page header. |
| 222 | * Container for buttons and other action elements. |
| 223 | * Should be placed inside PageHeaderMeta. |
| 224 | */ |
| 225 | const PageHeaderAside = ({ className, ...props }: PageHeaderAsideProps) => { |
| 226 | return ( |
| 227 | <div |
| 228 | data-slot="page-header-actions" |
| 229 | className={cn('flex items-center gap-2 shrink-0', className)} |
| 230 | {...props} |
| 231 | /> |
| 232 | ) |
| 233 | } |
| 234 | PageHeaderAside.displayName = 'PageHeaderAside' |
| 235 | |
| 236 | // ============================================================================ |
| 237 | // Navigation |
| 238 | // ============================================================================ |
| 239 | |
| 240 | export type PageHeaderNavigationTabsProps = React.ComponentProps<'div'> |
| 241 | |
| 242 | /** |
| 243 | * Navigation component for page header. |
| 244 | * Container for tab navigation (NavMenu). |
| 245 | * Should be placed as the last child of PageHeader. |
| 246 | */ |
| 247 | const PageHeaderNavigationTabs = ({ className, ...props }: PageHeaderNavigationTabsProps) => { |
| 248 | const { size } = usePageHeaderContext() |
| 249 | return ( |
| 250 | <PageContainer size={size} className={cn(size === 'full' && 'border-b')}> |
| 251 | <div |
| 252 | data-slot="page-header-footer" |
| 253 | className={cn('w-full [&>nav]:border-b-0', size !== 'full' && 'border-b', className)} |
| 254 | {...props} |
| 255 | /> |
| 256 | </PageContainer> |
| 257 | ) |
| 258 | } |
| 259 | PageHeaderNavigationTabs.displayName = 'PageHeaderNavigationTabs' |
| 260 | |
| 261 | // ============================================================================ |
| 262 | // Exports |
| 263 | // ============================================================================ |
| 264 | |
| 265 | export type PageHeaderProps = PageHeaderRootProps |
| 266 | |
| 267 | /** |
| 268 | * Page header root component. |
| 269 | * Use PageHeader, PageHeaderBreadcrumb, PageHeaderMeta, PageHeaderIcon, etc. |
| 270 | */ |
| 271 | export { |
| 272 | PageHeaderRoot as PageHeader, |
| 273 | PageHeaderAside, |
| 274 | PageHeaderBreadcrumb, |
| 275 | PageHeaderDescription, |
| 276 | PageHeaderIcon, |
| 277 | PageHeaderMeta, |
| 278 | PageHeaderNavigationTabs, |
| 279 | PageHeaderSummary, |
| 280 | PageHeaderTitle, |
| 281 | } |