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}
+
+ ))}
+
+ )}
+
+ }
+ >
+
- {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';