feat: add Section and Container components to shared/ui

This commit is contained in:
Ilia Mashkov
2026-04-19 08:26:26 +03:00
parent 9c52889b72
commit d17104b294
3 changed files with 179 additions and 0 deletions
+2
View File
@@ -0,0 +1,2 @@
export { Section, Container } from './ui/Section'
export type { SectionBackground, ContainerSize } from './ui/Section'
+95
View File
@@ -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')
})
})
})
+82
View File
@@ -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>
)
}