chore: format codebase and move SectionAccordion to entities/Section

This commit is contained in:
Ilia Mashkov
2026-04-23 20:52:43 +03:00
parent 8aff27f8ac
commit 1d333fd945
73 changed files with 1201 additions and 1153 deletions
+2 -2
View File
@@ -1,2 +1,2 @@
export { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from './ui/Card'
export type { CardBackground } from './ui/Card'
export { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from './ui/Card';
export type { CardBackground } from './ui/Card';
+9 -11
View File
@@ -1,14 +1,14 @@
import type { Meta, StoryObj } from '@storybook/nextjs-vite'
import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from './Card'
import type { Meta, StoryObj } from '@storybook/nextjs-vite';
import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from './Card';
const meta: Meta<typeof Card> = {
title: 'Shared/Card',
component: Card,
}
};
export default meta
export default meta;
type Story = StoryObj<typeof Card>
type Story = StoryObj<typeof Card>;
export const AllBackgrounds: Story = {
render: () => (
@@ -36,19 +36,17 @@ export const AllBackgrounds: Story = {
</Card>
</div>
),
}
};
export const NoPadding: Story = {
render: () => (
<div className="p-8 bg-ochre-clay">
<Card noPadding className="w-64 overflow-hidden">
<div className="h-40 bg-slate-indigo flex items-center justify-center text-ochre-clay">
Image placeholder
</div>
<div className="h-40 bg-slate-indigo flex items-center justify-center text-ochre-clay">Image placeholder</div>
</Card>
</div>
),
}
};
export const FullComposition: Story = {
render: () => (
@@ -67,4 +65,4 @@ export const FullComposition: Story = {
</Card>
</div>
),
}
};
+55 -55
View File
@@ -1,79 +1,79 @@
import { describe, it, expect } from 'vitest'
import { render, screen } from '@testing-library/react'
import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from './Card'
import { describe, it, expect } from 'vitest';
import { render, screen } from '@testing-library/react';
import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from './Card';
describe('Card', () => {
describe('rendering', () => {
it('renders children', () => {
render(<Card>Content</Card>)
expect(screen.getByText('Content')).toBeInTheDocument()
})
render(<Card>Content</Card>);
expect(screen.getByText('Content')).toBeInTheDocument();
});
it('has brutal-border and brutal-shadow classes', () => {
const { container } = render(<Card>Content</Card>)
expect(container.firstChild).toHaveClass('brutal-border', 'brutal-shadow')
})
})
const { container } = render(<Card>Content</Card>);
expect(container.firstChild).toHaveClass('brutal-border', 'brutal-shadow');
});
});
describe('background variants', () => {
it('defaults to ochre background', () => {
const { container } = render(<Card>Content</Card>)
expect(container.firstChild).toHaveClass('bg-ochre-clay')
})
const { container } = render(<Card>Content</Card>);
expect(container.firstChild).toHaveClass('bg-ochre-clay');
});
it('applies slate background', () => {
const { container } = render(<Card background="slate">Content</Card>)
expect(container.firstChild).toHaveClass('bg-slate-indigo')
})
const { container } = render(<Card background="slate">Content</Card>);
expect(container.firstChild).toHaveClass('bg-slate-indigo');
});
it('applies white background', () => {
const { container } = render(<Card background="white">Content</Card>)
expect(container.firstChild).toHaveClass('bg-white')
})
})
const { container } = render(<Card background="white">Content</Card>);
expect(container.firstChild).toHaveClass('bg-white');
});
});
describe('padding', () => {
it('has default padding', () => {
const { container } = render(<Card>Content</Card>)
expect(container.firstChild).toHaveClass('p-6')
})
const { container } = render(<Card>Content</Card>);
expect(container.firstChild).toHaveClass('p-6');
});
it('removes padding when noPadding is true', () => {
const { container } = render(<Card noPadding>Content</Card>)
expect(container.firstChild).not.toHaveClass('p-6')
})
})
const { container } = render(<Card noPadding>Content</Card>);
expect(container.firstChild).not.toHaveClass('p-6');
});
});
describe('className passthrough', () => {
it('merges custom className', () => {
const { container } = render(<Card className="group">Content</Card>)
expect(container.firstChild).toHaveClass('group')
})
})
})
const { container } = render(<Card className="group">Content</Card>);
expect(container.firstChild).toHaveClass('group');
});
});
});
describe('CardHeader', () => {
it('renders children with bottom margin', () => {
render(<CardHeader>Header</CardHeader>)
expect(screen.getByText('Header')).toHaveClass('mb-4')
})
})
render(<CardHeader>Header</CardHeader>);
expect(screen.getByText('Header')).toHaveClass('mb-4');
});
});
describe('CardTitle', () => {
it('renders children as h3', () => {
render(<CardTitle>Title</CardTitle>)
expect(screen.getByRole('heading', { level: 3 })).toHaveTextContent('Title')
})
})
render(<CardTitle>Title</CardTitle>);
expect(screen.getByRole('heading', { level: 3 })).toHaveTextContent('Title');
});
});
describe('CardDescription', () => {
it('renders children as paragraph with opacity', () => {
render(<CardDescription>Desc</CardDescription>)
const el = screen.getByText('Desc')
expect(el.tagName).toBe('P')
expect(el).toHaveClass('opacity-80')
})
})
render(<CardDescription>Desc</CardDescription>);
const el = screen.getByText('Desc');
expect(el.tagName).toBe('P');
expect(el).toHaveClass('opacity-80');
});
});
describe('CardContent', () => {
it('renders children in a div', () => {
render(<CardContent>Body</CardContent>)
expect(screen.getByText('Body')).toBeInTheDocument()
})
})
render(<CardContent>Body</CardContent>);
expect(screen.getByText('Body')).toBeInTheDocument();
});
});
describe('CardFooter', () => {
it('renders children with top border', () => {
render(<CardFooter>Footer</CardFooter>)
const el = screen.getByText('Footer')
expect(el).toHaveClass('brutal-border-top', 'mt-6', 'pt-6')
})
})
render(<CardFooter>Footer</CardFooter>);
const el = screen.getByText('Footer');
expect(el).toHaveClass('brutal-border-top', 'mt-6', 'pt-6');
});
});
+16 -16
View File
@@ -1,34 +1,34 @@
import type { ReactNode } from 'react'
import { cn } from '$shared/lib'
import type { ReactNode } from 'react';
import { cn } from '$shared/lib';
export type CardBackground = 'ochre' | 'slate' | 'white'
export type CardBackground = 'ochre' | 'slate' | 'white';
interface CardProps {
/**
* Card content
*/
children: ReactNode
children: ReactNode;
/**
* Additional CSS classes
*/
className?: string
className?: string;
/**
* Background color preset
* @default 'ochre'
*/
background?: CardBackground
background?: CardBackground;
/**
* Remove default padding
* @default false
*/
noPadding?: boolean
noPadding?: boolean;
}
const BG: Record<CardBackground, string> = {
ochre: 'bg-ochre-clay',
slate: 'bg-slate-indigo text-ochre-clay',
white: 'bg-white',
}
};
/**
* Brutalist card container with background and padding variants.
@@ -38,51 +38,51 @@ export function Card({ children, className, background = 'ochre', noPadding = fa
<div className={cn('brutal-border brutal-shadow', BG[background], !noPadding && 'p-6 md:p-8', className)}>
{children}
</div>
)
);
}
interface SlotProps {
/**
* Slot content
*/
children: ReactNode
children: ReactNode;
/**
* Additional CSS classes
*/
className?: string
className?: string;
}
/**
* Card header wrapper — adds bottom margin.
*/
export function CardHeader({ children, className }: SlotProps) {
return <div className={cn('mb-4', className)}>{children}</div>
return <div className={cn('mb-4', className)}>{children}</div>;
}
/**
* Card title — renders as h3.
*/
export function CardTitle({ children, className }: SlotProps) {
return <h3 className={className}>{children}</h3>
return <h3 className={className}>{children}</h3>;
}
/**
* Card description — muted paragraph below the title.
*/
export function CardDescription({ children, className }: SlotProps) {
return <p className={cn('mt-2 opacity-80', className)}>{children}</p>
return <p className={cn('mt-2 opacity-80', className)}>{children}</p>;
}
/**
* Card body content area.
*/
export function CardContent({ children, className }: SlotProps) {
return <div className={className}>{children}</div>
return <div className={className}>{children}</div>;
}
/**
* Card footer — separated by a brutal border-top.
*/
export function CardFooter({ children, className }: SlotProps) {
return <div className={cn('mt-6 pt-6 brutal-border-top', className)}>{children}</div>
return <div className={cn('mt-6 pt-6 brutal-border-top', className)}>{children}</div>;
}