index.tsx220 lines · main
1import { cva, type VariantProps } from 'class-variance-authority'
2import React from 'react'
3import { cn } from 'ui'
4
5// ============================================================================
6// Variants
7// ============================================================================
8
9const pageSectionRootVariants = cva(['pt-12 last:pb-12 gap-6'], {
10 variants: {
11 orientation: {
12 horizontal: 'grid @3xl:grid-cols-[1fr_2fr] @3xl:gap-12',
13 vertical: 'flex flex-col',
14 },
15 },
16 defaultVariants: {
17 orientation: 'vertical',
18 },
19})
20
21// ============================================================================
22// Root
23// ============================================================================
24
25export type PageSectionRootProps = React.ComponentProps<'div'> &
26 VariantProps<typeof pageSectionRootVariants>
27
28/**
29 * Root component for page section.
30 * Provides layout structure for section content with orientation variants.
31 * Renders children in order without searching for specific components.
32 */
33const PageSectionRoot = ({
34 className,
35 orientation = 'vertical',
36 children,
37 ...props
38}: PageSectionRootProps) => {
39 return (
40 <div
41 data-slot="page-section"
42 data-orientation={orientation}
43 className={cn(pageSectionRootVariants({ orientation }), className)}
44 {...props}
45 >
46 {children}
47 </div>
48 )
49}
50
51PageSectionRoot.displayName = 'PageSectionRoot'
52
53// ============================================================================
54// Summary
55// ============================================================================
56
57export type PageSectionSummaryProps = React.ComponentProps<'div'>
58
59/**
60 * Summary component to contain title and description.
61 * Provides layout structure for text content.
62 * Should be placed inside PageSectionMeta.
63 */
64const PageSectionSummary = ({ className, children, ...props }: PageSectionSummaryProps) => {
65 return (
66 <div
67 data-slot="page-section-summary"
68 className={cn('flex flex-col gap-1', className)}
69 {...props}
70 >
71 {children}
72 </div>
73 )
74}
75PageSectionSummary.displayName = 'PageSectionSummary'
76
77// ============================================================================
78// Title
79// ============================================================================
80
81export type PageSectionTitleProps = React.ComponentProps<'h2'>
82
83/**
84 * Title component for page section.
85 * Primary heading for the section.
86 */
87const PageSectionTitle = ({ className, children, ...props }: PageSectionTitleProps) => {
88 return (
89 <h2 data-slot="page-section-title" className={cn('heading-section', className)} {...props}>
90 {children}
91 </h2>
92 )
93}
94PageSectionTitle.displayName = 'PageSectionTitle'
95
96// ============================================================================
97// Description
98// ============================================================================
99
100export type PageSectionDescriptionProps = React.ComponentProps<'div'>
101
102/**
103 * Description component for page section.
104 * Supporting text rendered below the title.
105 */
106const PageSectionDescription = ({ className, children, ...props }: PageSectionDescriptionProps) => {
107 return (
108 <div
109 data-slot="page-section-description"
110 className={cn('text-sm text-foreground-light', className)}
111 // Optically align with bottom of PageSectionAside
112 // trim-end is not available in Tailwind CSS
113 style={
114 {
115 textBoxTrim: 'trim-end',
116 } as React.CSSProperties
117 }
118 {...props}
119 >
120 {children}
121 </div>
122 )
123}
124PageSectionDescription.displayName = 'PageSectionDescription'
125
126// ============================================================================
127// Aside
128// ============================================================================
129
130export type PageSectionAsideProps = React.ComponentProps<'div'>
131
132/**
133 * Aside component for page section.
134 * Container for buttons and other action elements.
135 * Should be placed inside PageSectionMeta.
136 */
137const PageSectionAside = ({ className, ...props }: PageSectionAsideProps) => {
138 return (
139 <div
140 data-slot="page-section-aside"
141 className={cn(
142 'flex items-center gap-2',
143 // Align with bottom of PageSectionDescription
144 '@xl:self-end',
145 className
146 )}
147 {...props}
148 />
149 )
150}
151PageSectionAside.displayName = 'PageSectionAside'
152
153// ============================================================================
154// Meta
155// ============================================================================
156
157export type PageSectionMetaProps = React.ComponentProps<'div'>
158
159/**
160 * Meta wrapper for page section.
161 * Contains summary and aside components with proper layout.
162 * Should be placed as the first child of PageSectionRoot.
163 * Uses CSS to style children based on their data-slot attributes.
164 */
165const PageSectionMeta = ({ className, children, ...props }: PageSectionMetaProps) => {
166 return (
167 <div className="@container">
168 <div
169 data-slot="page-section-meta"
170 className={cn(
171 'flex flex-col @xl:flex-row @xl:justify-between @xl:items-center gap-4',
172 '*:data-[slot="page-section-summary"]:flex-1',
173 // Center alignment with PageSectionAside in case no PageSectionDescription present
174 '*:data-[slot="page-section-summary"]:@xl:self-center',
175 '*:data-[slot="page-section-aside"]:shrink-0',
176 className
177 )}
178 {...props}
179 >
180 {children}
181 </div>
182 </div>
183 )
184}
185PageSectionMeta.displayName = 'PageSectionMeta'
186
187// ============================================================================
188// Content
189// ============================================================================
190
191export type PageSectionContentProps = React.ComponentProps<'div'>
192
193/**
194 * Content component for page section.
195 * Container for the main section content.
196 */
197const PageSectionContent = ({ className, ...props }: PageSectionContentProps) => {
198 return <div data-slot="page-section-content" className={cn(className)} {...props} />
199}
200PageSectionContent.displayName = 'PageSectionContent'
201
202// ============================================================================
203// Exports
204// ============================================================================
205
206export type PageSectionProps = PageSectionRootProps
207
208/**
209 * Page section root component.
210 * Use PageSection, PageSectionMeta, PageSectionSummary, etc.
211 */
212export {
213 PageSectionRoot as PageSection,
214 PageSectionAside,
215 PageSectionContent,
216 PageSectionDescription,
217 PageSectionMeta,
218 PageSectionSummary,
219 PageSectionTitle,
220}