diff --git a/src/entities/experience/ui/ExperienceCard/ExperienceCard.test.tsx b/src/entities/experience/ui/ExperienceCard/ExperienceCard.test.tsx index 61bf825..701998a 100644 --- a/src/entities/experience/ui/ExperienceCard/ExperienceCard.test.tsx +++ b/src/entities/experience/ui/ExperienceCard/ExperienceCard.test.tsx @@ -32,6 +32,32 @@ describe('ExperienceCard', () => { }); }); + describe('layout', () => { + it('period badge is inside the sidebar column', () => { + render(); + const badge = screen.getByText('2021 – 2024'); + expect(badge.closest('.brutal-border-sidebar')).toBeInTheDocument(); + }); + + it('company name is inside the sidebar column', () => { + render(); + const company = screen.getByText('Acme Corp'); + expect(company.closest('.brutal-border-sidebar')).toBeInTheDocument(); + }); + + it('title is outside the sidebar column', () => { + render(); + const title = screen.getByText('Senior Developer'); + expect(title.closest('.brutal-border-sidebar')).toBeNull(); + }); + + it('description is outside the sidebar column', () => { + render(); + const desc = screen.getByText('Built scalable frontend systems.'); + expect(desc.closest('.brutal-border-sidebar')).toBeNull(); + }); + }); + describe('structure', () => { it('title is rendered as an h3', () => { render(); @@ -44,25 +70,33 @@ describe('ExperienceCard', () => { expect(badge).toHaveClass('brutal-border', 'bg-blue', 'text-cream', 'text-sm'); }); - it('company paragraph has opacity-60', () => { - render(); - const company = screen.getByText('Acme Corp'); - expect(company.tagName).toBe('P'); - expect(company).toHaveClass('opacity-60'); - }); - it('description renders via RichText with rich-text class', () => { render(); const desc = screen.getByText('Built scalable frontend systems.'); expect(desc.closest('.rich-text')).toBeInTheDocument(); }); - it('card has brutal-border class (from Card component)', () => { + it('card has brutal-border class', () => { const { container } = render(); expect(container.firstChild).toHaveClass('brutal-border'); }); }); + describe('stack tags', () => { + it('renders stack tags in the sidebar', () => { + render(); + const react = screen.getByText('React'); + const ts = screen.getByText('TypeScript'); + expect(react.closest('.brutal-border-sidebar')).toBeInTheDocument(); + expect(ts.closest('.brutal-border-sidebar')).toBeInTheDocument(); + }); + + it('renders nothing extra when stack is empty', () => { + render(); + expect(screen.queryByRole('list')).toBeNull(); + }); + }); + describe('className passthrough', () => { it('forwards className to the card', () => { const { container } = render(); diff --git a/src/entities/experience/ui/ExperienceCard/ExperienceCard.tsx b/src/entities/experience/ui/ExperienceCard/ExperienceCard.tsx index a60c9ee..265fdc9 100644 --- a/src/entities/experience/ui/ExperienceCard/ExperienceCard.tsx +++ b/src/entities/experience/ui/ExperienceCard/ExperienceCard.tsx @@ -1,4 +1,4 @@ -import { Card, CardContent, CardFooter, CardHeader, CardTitle, RichText } from '$shared/ui'; +import { Card, CardSidebar, CardTitle, RichText } from '$shared/ui'; type Props = { /** @@ -28,30 +28,38 @@ type Props = { }; /** - * Work experience card with title, company, period, description, and tech stack. + * Work experience card with sidebar layout. + * Sidebar: period badge, company, stack tags. + * Main: job title and rich-text description. */ export function ExperienceCard({ title, company, period, description, stack, className }: Props) { return ( - -
+ + {period} +

{company}

+ {stack.length > 0 && ( +
+ {stack.map((tech) => ( + + {tech} + + ))} +
+ )} +
+ } + > +
{title} -

{company}

+
- {period} -
- - - - {stack.length > 0 && ( - - {stack.map((tech) => ( - - {tech} - - ))} - - )} +
); } diff --git a/src/shared/styles/theme.css b/src/shared/styles/theme.css index 847c92d..d2e3f1c 100644 --- a/src/shared/styles/theme.css +++ b/src/shared/styles/theme.css @@ -269,6 +269,16 @@ .brutal-border-right { border-right: var(--border-width) solid var(--blue); } +/* Sidebar divider: bottom border on mobile, right border on desktop */ +.brutal-border-sidebar { + border-bottom: var(--border-width) solid var(--blue); +} +@media (min-width: 768px) { + .brutal-border-sidebar { + border-bottom: none; + border-right: var(--border-width) solid var(--blue); + } +} /* Editorial rich-text typography */ .rich-text { diff --git a/src/shared/ui/Card/index.ts b/src/shared/ui/Card/index.ts index e15b0d7..fe4a287 100644 --- a/src/shared/ui/Card/index.ts +++ b/src/shared/ui/Card/index.ts @@ -1,2 +1,2 @@ export type { CardBackground } from './ui/Card'; -export { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from './ui/Card'; +export { Card, CardContent, CardDescription, CardFooter, CardHeader, CardSidebar, CardTitle } from './ui/Card'; diff --git a/src/shared/ui/Card/ui/Card.test.tsx b/src/shared/ui/Card/ui/Card.test.tsx index 2193f8a..fa5c56d 100644 --- a/src/shared/ui/Card/ui/Card.test.tsx +++ b/src/shared/ui/Card/ui/Card.test.tsx @@ -1,5 +1,5 @@ import { render, screen } from '@testing-library/react'; -import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from './Card'; +import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardSidebar, CardTitle } from './Card'; describe('Card', () => { describe('rendering', () => { @@ -72,3 +72,51 @@ describe('CardFooter', () => { expect(el).toHaveClass('brutal-border-top', 'mt-6', 'pt-6', 'md:mt-8', 'md:pt-8'); }); }); +describe('CardSidebar', () => { + describe('rendering', () => { + it('renders sidebar content', () => { + render(Sidebar}>Main); + expect(screen.getByText('Sidebar')).toBeInTheDocument(); + }); + + it('renders main content', () => { + render(Sidebar}>Main); + expect(screen.getByText('Main')).toBeInTheDocument(); + }); + }); + + describe('structure', () => { + it('root wrapper is a flex container', () => { + const { container } = render(S}>M); + expect(container.firstChild).toHaveClass('flex'); + }); + + it('sidebar column has brutal-border-sidebar class', () => { + render(Sidebar}>Main); + const sidebar = screen.getByText('Sidebar').parentElement; + expect(sidebar).toHaveClass('brutal-border-sidebar'); + }); + + it('sidebar column has fixed width on md', () => { + render(Sidebar}>Main); + const sidebar = screen.getByText('Sidebar').parentElement; + expect(sidebar).toHaveClass('md:w-52'); + }); + + it('main column fills remaining space', () => { + render(Sidebar}>Main); + expect(screen.getByText('Main')).toHaveClass('flex-1'); + }); + }); + + describe('className passthrough', () => { + it('forwards className to the root wrapper', () => { + const { container } = render( + S} className="custom"> + M + , + ); + expect(container.firstChild).toHaveClass('custom'); + }); + }); +}); diff --git a/src/shared/ui/Card/ui/Card.tsx b/src/shared/ui/Card/ui/Card.tsx index 2f8a222..e9ab93c 100644 --- a/src/shared/ui/Card/ui/Card.tsx +++ b/src/shared/ui/Card/ui/Card.tsx @@ -85,3 +85,32 @@ export function CardContent({ children, className }: SlotProps) { export function CardFooter({ children, className }: SlotProps) { return
{children}
; } + +interface CardSidebarProps { + /** + * Left sidebar content — metadata such as period, company, stack + */ + sidebar: ReactNode; + /** + * Main content — primary info such as role title and description + */ + children: ReactNode; + /** + * Additional CSS classes for the root wrapper + */ + className?: string; +} + +/** + * Two-column card layout: narrow sidebar on the left, main content on the right. + * On mobile the columns stack vertically with a bottom border separator; + * on md+ they sit side-by-side with a right border separator. + */ +export function CardSidebar({ sidebar, children, className }: CardSidebarProps) { + return ( +
+
{sidebar}
+
{children}
+
+ ); +} diff --git a/src/shared/ui/index.ts b/src/shared/ui/index.ts index 6dc448b..70b4117 100644 --- a/src/shared/ui/index.ts +++ b/src/shared/ui/index.ts @@ -3,7 +3,7 @@ export { Badge } from './Badge'; export type { ButtonSize, ButtonVariant } from './Button'; export { Button } from './Button'; export type { CardBackground } from './Card'; -export { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from './Card'; +export { Card, CardContent, CardDescription, CardFooter, CardHeader, CardSidebar, CardTitle } from './Card'; export { Input, Textarea } from './Input'; export { RichText } from './RichText';