diff --git a/src/shared/ui/Section/index.ts b/src/shared/ui/Section/index.ts new file mode 100644 index 0000000..8f6b80e --- /dev/null +++ b/src/shared/ui/Section/index.ts @@ -0,0 +1,2 @@ +export { Section, Container } from './ui/Section' +export type { SectionBackground, ContainerSize } from './ui/Section' diff --git a/src/shared/ui/Section/ui/Section.test.tsx b/src/shared/ui/Section/ui/Section.test.tsx new file mode 100644 index 0000000..849cafe --- /dev/null +++ b/src/shared/ui/Section/ui/Section.test.tsx @@ -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(
content
) + expect(container.querySelector('section')).toBeInTheDocument() + }) + it('renders children', () => { + render(
hello
) + expect(screen.getByText('hello')).toBeInTheDocument() + }) + }) + + describe('background variants', () => { + it('defaults to ochre background', () => { + const { container } = render(
x
) + expect(container.querySelector('section')).toHaveClass('bg-ochre-clay', 'text-carbon-black') + }) + it('applies slate background', () => { + const { container } = render(
x
) + expect(container.querySelector('section')).toHaveClass('bg-slate-indigo', 'text-ochre-clay') + }) + it('applies white background', () => { + const { container } = render(
x
) + expect(container.querySelector('section')).toHaveClass('bg-white', 'text-carbon-black') + }) + }) + + describe('bordered', () => { + it('no border classes by default', () => { + const { container } = render(
x
) + 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(
x
) + 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(
x
) + expect(container.querySelector('section')).toHaveClass('py-16') + }) + }) +}) + +describe('Container', () => { + describe('rendering', () => { + it('renders a div with children', () => { + render(inner) + expect(screen.getByText('inner')).toBeInTheDocument() + }) + }) + + describe('size variants', () => { + it('defaults to max-w-7xl', () => { + const { container } = render(x) + expect(container.firstChild).toHaveClass('max-w-7xl') + }) + it('wide applies max-w-[1920px]', () => { + const { container } = render(x) + expect(container.firstChild).toHaveClass('max-w-[1920px]') + }) + it('ultra-wide applies max-w-[2560px]', () => { + const { container } = render(x) + expect(container.firstChild).toHaveClass('max-w-[2560px]') + }) + }) + + describe('layout', () => { + it('centers content horizontally', () => { + const { container } = render(x) + expect(container.firstChild).toHaveClass('mx-auto') + }) + it('applies horizontal padding', () => { + const { container } = render(x) + expect(container.firstChild).toHaveClass('px-6') + }) + }) + + describe('className', () => { + it('applies custom className', () => { + const { container } = render(x) + expect(container.firstChild).toHaveClass('my-custom') + }) + }) +}) diff --git a/src/shared/ui/Section/ui/Section.tsx b/src/shared/ui/Section/ui/Section.tsx new file mode 100644 index 0000000..ea2ffbb --- /dev/null +++ b/src/shared/ui/Section/ui/Section.tsx @@ -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 = { + 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 ( +
+ {children} +
+ ) +} + +interface ContainerProps { + /** + * Container content + */ + children: ReactNode + /** + * Max-width constraint + * @default 'default' + */ + size?: ContainerSize + /** + * CSS classes + */ + className?: string +} + +const SIZES: Record = { + '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 ( +
+ {children} +
+ ) +}