feat: add Section and Container components to shared/ui
This commit is contained in:
@@ -0,0 +1,2 @@
|
|||||||
|
export { Section, Container } from './ui/Section'
|
||||||
|
export type { SectionBackground, ContainerSize } from './ui/Section'
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
import { describe, it, expect } from 'vitest'
|
||||||
|
import { render, screen } from '@testing-library/react'
|
||||||
|
import { Section, Container } from './Section'
|
||||||
|
|
||||||
|
describe('Section', () => {
|
||||||
|
describe('rendering', () => {
|
||||||
|
it('renders a section element', () => {
|
||||||
|
const { container } = render(<Section>content</Section>)
|
||||||
|
expect(container.querySelector('section')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
it('renders children', () => {
|
||||||
|
render(<Section><span>hello</span></Section>)
|
||||||
|
expect(screen.getByText('hello')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('background variants', () => {
|
||||||
|
it('defaults to ochre background', () => {
|
||||||
|
const { container } = render(<Section>x</Section>)
|
||||||
|
expect(container.querySelector('section')).toHaveClass('bg-ochre-clay', 'text-carbon-black')
|
||||||
|
})
|
||||||
|
it('applies slate background', () => {
|
||||||
|
const { container } = render(<Section background="slate">x</Section>)
|
||||||
|
expect(container.querySelector('section')).toHaveClass('bg-slate-indigo', 'text-ochre-clay')
|
||||||
|
})
|
||||||
|
it('applies white background', () => {
|
||||||
|
const { container } = render(<Section background="white">x</Section>)
|
||||||
|
expect(container.querySelector('section')).toHaveClass('bg-white', 'text-carbon-black')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('bordered', () => {
|
||||||
|
it('no border classes by default', () => {
|
||||||
|
const { container } = render(<Section>x</Section>)
|
||||||
|
const el = container.querySelector('section')!
|
||||||
|
expect(el).not.toHaveClass('brutal-border-top')
|
||||||
|
expect(el).not.toHaveClass('brutal-border-bottom')
|
||||||
|
})
|
||||||
|
it('adds top and bottom borders when bordered=true', () => {
|
||||||
|
const { container } = render(<Section bordered>x</Section>)
|
||||||
|
const el = container.querySelector('section')!
|
||||||
|
expect(el).toHaveClass('brutal-border-top')
|
||||||
|
expect(el).toHaveClass('brutal-border-bottom')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('className', () => {
|
||||||
|
it('applies custom className', () => {
|
||||||
|
const { container } = render(<Section className="py-16">x</Section>)
|
||||||
|
expect(container.querySelector('section')).toHaveClass('py-16')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Container', () => {
|
||||||
|
describe('rendering', () => {
|
||||||
|
it('renders a div with children', () => {
|
||||||
|
render(<Container><span>inner</span></Container>)
|
||||||
|
expect(screen.getByText('inner')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('size variants', () => {
|
||||||
|
it('defaults to max-w-7xl', () => {
|
||||||
|
const { container } = render(<Container>x</Container>)
|
||||||
|
expect(container.firstChild).toHaveClass('max-w-7xl')
|
||||||
|
})
|
||||||
|
it('wide applies max-w-[1920px]', () => {
|
||||||
|
const { container } = render(<Container size="wide">x</Container>)
|
||||||
|
expect(container.firstChild).toHaveClass('max-w-[1920px]')
|
||||||
|
})
|
||||||
|
it('ultra-wide applies max-w-[2560px]', () => {
|
||||||
|
const { container } = render(<Container size="ultra-wide">x</Container>)
|
||||||
|
expect(container.firstChild).toHaveClass('max-w-[2560px]')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('layout', () => {
|
||||||
|
it('centers content horizontally', () => {
|
||||||
|
const { container } = render(<Container>x</Container>)
|
||||||
|
expect(container.firstChild).toHaveClass('mx-auto')
|
||||||
|
})
|
||||||
|
it('applies horizontal padding', () => {
|
||||||
|
const { container } = render(<Container>x</Container>)
|
||||||
|
expect(container.firstChild).toHaveClass('px-6')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('className', () => {
|
||||||
|
it('applies custom className', () => {
|
||||||
|
const { container } = render(<Container className="my-custom">x</Container>)
|
||||||
|
expect(container.firstChild).toHaveClass('my-custom')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
import type { ReactNode } from 'react'
|
||||||
|
import { cn } from '$shared/lib'
|
||||||
|
|
||||||
|
export type SectionBackground = 'ochre' | 'slate' | 'white'
|
||||||
|
export type ContainerSize = 'default' | 'wide' | 'ultra-wide'
|
||||||
|
|
||||||
|
interface SectionProps {
|
||||||
|
/**
|
||||||
|
* Section content
|
||||||
|
*/
|
||||||
|
children: ReactNode
|
||||||
|
/**
|
||||||
|
* Background color variant
|
||||||
|
* @default 'ochre'
|
||||||
|
*/
|
||||||
|
background?: SectionBackground
|
||||||
|
/**
|
||||||
|
* Adds top and bottom brutal borders
|
||||||
|
* @default false
|
||||||
|
*/
|
||||||
|
bordered?: boolean
|
||||||
|
/**
|
||||||
|
* CSS classes
|
||||||
|
*/
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const BACKGROUNDS: Record<SectionBackground, string> = {
|
||||||
|
ochre: 'bg-ochre-clay text-carbon-black',
|
||||||
|
slate: 'bg-slate-indigo text-ochre-clay',
|
||||||
|
white: 'bg-white text-carbon-black',
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Full-width page section with background and optional borders.
|
||||||
|
*/
|
||||||
|
export function Section({ children, background = 'ochre', bordered = false, className }: SectionProps) {
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
className={cn(
|
||||||
|
BACKGROUNDS[background],
|
||||||
|
bordered && 'brutal-border-top brutal-border-bottom',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ContainerProps {
|
||||||
|
/**
|
||||||
|
* Container content
|
||||||
|
*/
|
||||||
|
children: ReactNode
|
||||||
|
/**
|
||||||
|
* Max-width constraint
|
||||||
|
* @default 'default'
|
||||||
|
*/
|
||||||
|
size?: ContainerSize
|
||||||
|
/**
|
||||||
|
* CSS classes
|
||||||
|
*/
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const SIZES: Record<ContainerSize, string> = {
|
||||||
|
'default': 'max-w-7xl',
|
||||||
|
'wide': 'max-w-[1920px]',
|
||||||
|
'ultra-wide': 'max-w-[2560px]',
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Centered content container with responsive horizontal padding.
|
||||||
|
*/
|
||||||
|
export function Container({ children, size = 'default', className }: ContainerProps) {
|
||||||
|
return (
|
||||||
|
<div className={cn(SIZES[size], 'mx-auto px-6 md:px-12 lg:px-16', className)}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user