feat: add TechStackBrick and TechStackGrid components to shared/ui
This commit is contained in:
@@ -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')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user