feat: add TechStackBrick and TechStackGrid components to shared/ui

This commit is contained in:
Ilia Mashkov
2026-04-19 08:27:56 +03:00
parent faf6c574d1
commit ad188fcb21
3 changed files with 121 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
export { TechStackBrick, TechStackGrid } from './ui/TechStack'
@@ -0,0 +1,62 @@
import { describe, it, expect } from 'vitest'
import { render, screen } from '@testing-library/react'
import { TechStackBrick, TechStackGrid } from './TechStack'
describe('TechStackBrick', () => {
describe('rendering', () => {
it('renders the technology name', () => {
render(<TechStackBrick name="TypeScript" />)
expect(screen.getByText('TypeScript')).toBeInTheDocument()
})
})
describe('styling', () => {
it('has brutal-border class', () => {
const { container } = render(<TechStackBrick name="React" />)
expect(container.firstChild).toHaveClass('brutal-border')
})
it('has brutal-shadow class', () => {
const { container } = render(<TechStackBrick name="React" />)
expect(container.firstChild).toHaveClass('brutal-shadow')
})
it('name span has uppercase and tracking-wide', () => {
render(<TechStackBrick name="Go" />)
const span = screen.getByText('Go')
expect(span).toHaveClass('uppercase', 'tracking-wide')
})
it('applies custom className', () => {
const { container } = render(<TechStackBrick name="Go" className="w-full" />)
expect(container.firstChild).toHaveClass('w-full')
})
})
})
describe('TechStackGrid', () => {
describe('rendering', () => {
it('renders all skill names', () => {
render(<TechStackGrid skills={['React', 'TypeScript', 'Go']} />)
expect(screen.getByText('React')).toBeInTheDocument()
expect(screen.getByText('TypeScript')).toBeInTheDocument()
expect(screen.getByText('Go')).toBeInTheDocument()
})
it('renders correct number of bricks', () => {
const { container } = render(<TechStackGrid skills={['A', 'B', 'C']} />)
expect(container.firstChild!.childNodes).toHaveLength(3)
})
it('renders empty grid with no skills', () => {
const { container } = render(<TechStackGrid skills={[]} />)
expect(container.firstChild!.childNodes).toHaveLength(0)
})
})
describe('layout', () => {
it('has grid class', () => {
const { container } = render(<TechStackGrid skills={['A']} />)
expect(container.firstChild).toHaveClass('grid')
})
it('applies custom className', () => {
const { container } = render(<TechStackGrid skills={[]} className="my-custom" />)
expect(container.firstChild).toHaveClass('my-custom')
})
})
})
+58
View File
@@ -0,0 +1,58 @@
import { cn } from '$shared/lib'
interface TechStackBrickProps {
/**
* Technology name displayed in the brick
*/
name: string
/**
* CSS classes
*/
className?: string
}
/**
* Single technology label brick with brutalist border and hover effect.
*/
export function TechStackBrick({ name, className }: TechStackBrickProps) {
return (
<div
className={cn(
'brutal-border brutal-shadow bg-white px-4 py-3 text-center',
'transition-all duration-200 hover:shadow-none hover:translate-x-[2px] hover:translate-y-[2px]',
className,
)}
>
<span className="text-sm uppercase tracking-wide">{name}</span>
</div>
)
}
interface TechStackGridProps {
/**
* List of technology names to render as bricks
*/
skills: string[]
/**
* CSS classes
*/
className?: string
}
/**
* Responsive grid of TechStackBrick items.
*/
export function TechStackGrid({ skills, className }: TechStackGridProps) {
return (
<div
className={cn(
'grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 gap-4',
className,
)}
>
{skills.map((skill, index) => (
<TechStackBrick key={index} name={skill} />
))}
</div>
)
}