feat: Badge size prop (sm/md) and use Badge in ExperienceCard

This commit is contained in:
Ilia Mashkov
2026-05-18 13:02:07 +03:00
parent 48a08ec3fb
commit 37098be3c8
6 changed files with 39 additions and 14 deletions
@@ -64,10 +64,10 @@ describe('ExperienceCard', () => {
expect(screen.getByRole('heading', { level: 3 })).toHaveTextContent('Senior Developer'); expect(screen.getByRole('heading', { level: 3 })).toHaveTextContent('Senior Developer');
}); });
it('period badge has brutal-border, bg-blue, text-cream, text-sm', () => { it('period badge has brutal-border, bg-blue, text-cream, md size', () => {
render(<ExperienceCard {...DEFAULT_PROPS} />); render(<ExperienceCard {...DEFAULT_PROPS} />);
const badge = screen.getByText('2021 2024'); const badge = screen.getByText('2021 2024');
expect(badge).toHaveClass('brutal-border', 'bg-blue', 'text-cream', 'text-sm'); expect(badge).toHaveClass('brutal-border', 'bg-blue', 'text-cream', 'px-4', 'py-2', 'text-sm');
}); });
it('description renders via RichText with rich-text class', () => { it('description renders via RichText with rich-text class', () => {
@@ -1,4 +1,4 @@
import { Card, CardSidebar, CardTitle, RichText } from '$shared/ui'; import { Badge, Card, CardSidebar, CardTitle, RichText } from '$shared/ui';
type Props = { type Props = {
/** /**
@@ -10,7 +10,7 @@ type Props = {
*/ */
company: string; company: string;
/** /**
* Employment period (e.g. "2021 2024") * Employment period (e.g. "Jan 2021 Dec 2024")
*/ */
period: string; period: string;
/** /**
@@ -38,17 +38,14 @@ export function ExperienceCard({ title, company, period, description, stack, cla
<CardSidebar <CardSidebar
sidebar={ sidebar={
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<span className="brutal-border px-4 py-2 bg-blue text-cream text-sm self-start">{period}</span> <Badge size="md">{period}</Badge>
<p className="text-base font-medium">{company}</p> <p className="text-base font-medium">{company}</p>
{stack.length > 0 && ( {stack.length > 0 && (
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
{stack.map((tech) => ( {stack.map((tech) => (
<span <Badge key={tech} variant="outline">
key={tech}
className="brutal-border px-3 py-1 bg-cream text-blue text-sm uppercase tracking-wide"
>
{tech} {tech}
</span> </Badge>
))} ))}
</div> </div>
)} )}
+1 -1
View File
@@ -1,2 +1,2 @@
export type { BadgeVariant } from './ui/Badge'; export type { BadgeSize, BadgeVariant } from './ui/Badge';
export { Badge } from './ui/Badge'; export { Badge } from './ui/Badge';
+17
View File
@@ -42,6 +42,23 @@ describe('Badge', () => {
}); });
}); });
describe('sizes', () => {
it('defaults to sm size', () => {
render(<Badge>Tag</Badge>);
expect(screen.getByText('Tag')).toHaveClass('px-3', 'py-1', 'text-xs');
});
it('applies sm size classes', () => {
render(<Badge size="sm">Tag</Badge>);
expect(screen.getByText('Tag')).toHaveClass('px-3', 'py-1', 'text-xs');
});
it('applies md size classes', () => {
render(<Badge size="md">Tag</Badge>);
expect(screen.getByText('Tag')).toHaveClass('px-4', 'py-2', 'text-sm');
});
});
describe('className passthrough', () => { describe('className passthrough', () => {
it('merges custom className', () => { it('merges custom className', () => {
render(<Badge className="mt-4">Tag</Badge>); render(<Badge className="mt-4">Tag</Badge>);
+13 -2
View File
@@ -2,6 +2,7 @@ import type { ReactNode } from 'react';
import { cn } from '$shared/lib'; import { cn } from '$shared/lib';
export type BadgeVariant = 'default' | 'primary' | 'secondary' | 'outline'; export type BadgeVariant = 'default' | 'primary' | 'secondary' | 'outline';
export type BadgeSize = 'sm' | 'md';
interface Props { interface Props {
/** /**
@@ -13,6 +14,11 @@ interface Props {
* @default 'default' * @default 'default'
*/ */
variant?: BadgeVariant; variant?: BadgeVariant;
/**
* Size preset
* @default 'sm'
*/
size?: BadgeSize;
/** /**
* Additional CSS classes * Additional CSS classes
*/ */
@@ -26,12 +32,17 @@ const VARIANTS: Record<BadgeVariant, string> = {
outline: 'brutal-border bg-transparent text-blue', outline: 'brutal-border bg-transparent text-blue',
}; };
const SIZES: Record<BadgeSize, string> = {
sm: 'px-3 py-1 text-xs',
md: 'px-4 py-2 text-sm',
};
/** /**
* Small label for categorization or status. * Small label for categorization or status.
*/ */
export function Badge({ children, variant = 'default', className }: Props) { export function Badge({ children, variant = 'default', size = 'sm', className }: Props) {
return ( return (
<span className={cn('inline-block px-3 py-1 text-xs uppercase tracking-wider', VARIANTS[variant], className)}> <span className={cn('inline-block uppercase tracking-wider', SIZES[size], VARIANTS[variant], className)}>
{children} {children}
</span> </span>
); );
+1 -1
View File
@@ -1,4 +1,4 @@
export type { BadgeVariant } from './Badge'; export type { BadgeSize, BadgeVariant } from './Badge';
export { Badge } from './Badge'; export { Badge } from './Badge';
export type { ButtonSize, ButtonVariant } from './Button'; export type { ButtonSize, ButtonVariant } from './Button';
export { Button } from './Button'; export { Button } from './Button';