index.tsx281 lines · main
1'use client'
2
3import { cva, type VariantProps } from 'class-variance-authority'
4import React, { createContext, useContext } from 'react'
5import { cn } from 'ui'
6import { Breadcrumb } from 'ui/src/components/shadcn/ui/breadcrumb'
7
8import { PageContainer } from '../PageContainer'
9
10// ============================================================================
11// Variants
12// ============================================================================
13
14const 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
32type PageHeaderContextValue = {
33 size: 'default' | 'small' | 'large' | 'full'
34}
35
36const PageHeaderContext = createContext<PageHeaderContextValue>({
37 size: 'default',
38})
39
40const usePageHeaderContext = () => useContext(PageHeaderContext)
41
42// ============================================================================
43// Root
44// ============================================================================
45
46export 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 */
53const 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
73export 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 */
80const 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}
94PageHeaderBreadcrumb.displayName = 'PageHeaderBreadcrumb'
95
96// ============================================================================
97// Icon
98// ============================================================================
99
100export type PageHeaderIconProps = React.ComponentProps<'div'>
101
102/**
103 * Icon component for page header.
104 * Positioned to the left of title and description.
105 */
106const 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}
115PageHeaderIcon.displayName = 'PageHeaderIcon'
116
117// ============================================================================
118// Summary
119// ============================================================================
120
121export 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 */
128const 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}
139PageHeaderSummary.displayName = 'PageHeaderSummary'
140
141// ============================================================================
142// Title
143// ============================================================================
144
145export type PageHeaderTitleProps = React.ComponentProps<'h1'>
146
147/**
148 * Title component for page header.
149 * Primary heading for the page.
150 */
151const 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
163export type PageHeaderDescriptionProps = React.ComponentProps<'div'>
164
165/**
166 * Description component for page header.
167 * Supporting text rendered below the title.
168 */
169const 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
185export 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 */
193const 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}
212PageHeaderMeta.displayName = 'PageHeaderMeta'
213
214// ============================================================================
215// Actions
216// ============================================================================
217
218export 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 */
225const 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}
234PageHeaderAside.displayName = 'PageHeaderAside'
235
236// ============================================================================
237// Navigation
238// ============================================================================
239
240export 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 */
247const 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}
259PageHeaderNavigationTabs.displayName = 'PageHeaderNavigationTabs'
260
261// ============================================================================
262// Exports
263// ============================================================================
264
265export type PageHeaderProps = PageHeaderRootProps
266
267/**
268 * Page header root component.
269 * Use PageHeader, PageHeaderBreadcrumb, PageHeaderMeta, PageHeaderIcon, etc.
270 */
271export {
272 PageHeaderRoot as PageHeader,
273 PageHeaderAside,
274 PageHeaderBreadcrumb,
275 PageHeaderDescription,
276 PageHeaderIcon,
277 PageHeaderMeta,
278 PageHeaderNavigationTabs,
279 PageHeaderSummary,
280 PageHeaderTitle,
281}