diff --git a/src/shared/ui/SectionAccordion/index.ts b/src/shared/ui/SectionAccordion/index.ts
new file mode 100644
index 0000000..5af53e2
--- /dev/null
+++ b/src/shared/ui/SectionAccordion/index.ts
@@ -0,0 +1 @@
+export { SectionAccordion } from './ui/SectionAccordion'
diff --git a/src/shared/ui/SectionAccordion/ui/SectionAccordion.test.tsx b/src/shared/ui/SectionAccordion/ui/SectionAccordion.test.tsx
new file mode 100644
index 0000000..e68d60b
--- /dev/null
+++ b/src/shared/ui/SectionAccordion/ui/SectionAccordion.test.tsx
@@ -0,0 +1,57 @@
+import { describe, it, expect, vi } from 'vitest'
+import { render, screen } from '@testing-library/react'
+import userEvent from '@testing-library/user-event'
+import { SectionAccordion } from './SectionAccordion'
+
+const defaultProps = {
+ number: '01',
+ title: 'About',
+ id: 'about',
+ isActive: false,
+ onClick: vi.fn(),
+ children:
Content here
,
+}
+
+describe('SectionAccordion', () => {
+ describe('collapsed state (isActive=false)', () => {
+ it('renders a section element with the given id', () => {
+ const { container } = render()
+ expect(container.querySelector('section#about')).toBeInTheDocument()
+ })
+ it('renders a button with number and title', () => {
+ render()
+ expect(screen.getByRole('button', { name: /01.*About/i })).toBeInTheDocument()
+ })
+ it('does not render children', () => {
+ render()
+ expect(screen.queryByText('Content here')).not.toBeInTheDocument()
+ })
+ it('calls onClick when button is clicked', async () => {
+ const onClick = vi.fn()
+ render()
+ await userEvent.click(screen.getByRole('button'))
+ expect(onClick).toHaveBeenCalledOnce()
+ })
+ })
+
+ describe('active state (isActive=true)', () => {
+ const activeProps = { ...defaultProps, isActive: true }
+
+ it('renders an h1 with number and title', () => {
+ render()
+ expect(screen.getByRole('heading', { level: 1, name: /01.*About/i })).toBeInTheDocument()
+ })
+ it('renders children', () => {
+ render()
+ expect(screen.getByText('Content here')).toBeInTheDocument()
+ })
+ it('does not render a button', () => {
+ render()
+ expect(screen.queryByRole('button')).not.toBeInTheDocument()
+ })
+ it('content wrapper has animate-fadeIn class', () => {
+ const { container } = render()
+ expect(container.querySelector('.animate-fadeIn')).toBeInTheDocument()
+ })
+ })
+})
diff --git a/src/shared/ui/SectionAccordion/ui/SectionAccordion.tsx b/src/shared/ui/SectionAccordion/ui/SectionAccordion.tsx
new file mode 100644
index 0000000..88fc0de
--- /dev/null
+++ b/src/shared/ui/SectionAccordion/ui/SectionAccordion.tsx
@@ -0,0 +1,65 @@
+import type { ReactNode } from 'react'
+
+interface SectionAccordionProps {
+ /**
+ * Display number prefix (e.g. "01")
+ */
+ number: string
+ /**
+ * Section title
+ */
+ title: string
+ /**
+ * HTML id for anchor navigation
+ */
+ id: string
+ /**
+ * Whether this section is expanded
+ */
+ isActive: boolean
+ /**
+ * Called when the collapsed header is clicked
+ */
+ onClick: () => void
+ /**
+ * Section content, shown when active
+ */
+ children: ReactNode
+}
+
+/**
+ * Accordion-style section that collapses to a heading button when inactive.
+ */
+export function SectionAccordion({ number, title, id, isActive, onClick, children }: SectionAccordionProps) {
+ return (
+
+ {isActive ? (
+
+
+
+ {number}. {title}
+
+
+
+ {children}
+
+
+ ) : (
+
+ )}
+
+ )
+}