diff --git a/pages/lesson-1/_meta.ru.json b/pages/lesson-1/_meta.ru.json
index 9976b05..e5adb63 100644
--- a/pages/lesson-1/_meta.ru.json
+++ b/pages/lesson-1/_meta.ru.json
@@ -1,4 +1,5 @@
{
"chapter-1": "1. Создание и настройка проекта",
- "chapter-2": "2. Storybook и css переменные"
+ "chapter-2": "2. Storybook и css переменные",
+ "chapter-3": "3. Полиморфные компоненты"
}
diff --git a/pages/lesson-1/chapter-3.en.mdx b/pages/lesson-1/chapter-3.en.mdx
new file mode 100644
index 0000000..922266a
--- /dev/null
+++ b/pages/lesson-1/chapter-3.en.mdx
@@ -0,0 +1 @@
+# Under construction
diff --git a/pages/lesson-1/chapter-3.ru.mdx b/pages/lesson-1/chapter-3.ru.mdx
new file mode 100644
index 0000000..8473de9
--- /dev/null
+++ b/pages/lesson-1/chapter-3.ru.mdx
@@ -0,0 +1,327 @@
+import { Callout } from 'nextra/components'
+
+# Компоненты, полиморфные компоненты
+
+## Button
+
+### Подготовка
+
+- Создайте папку _src/components/ui_
+
+- Создайте папку _button_ в _src/components/ui_ со следующей структурой:
+
+{/* prettier-ignore */}
+```markdown
+src
+└── components
+ └── ui
+ └── button
+ └── button.tsx
+ └── button.module.scss
+ └── button.stories.ts
+ └── index.ts
+```
+
+### Дизайн и варианты
+
+После рассмотрения дизайна стало понятно, что у нас будет несколько вариантов кнопок, а именно:
+
+
+
+- Стандартная, она же основная, она же `primary`
+- Второстепенная, она же `secondary`
+- Третьестепенная, она же `tertiary`, иногда ее называют `outlined`
+- Кнопка, занимающая всю ширину родителя, она же `fullWidth`
+- Кнопка, которая выглядит как ссылка, она же `link`
+- Ссылка, которая выглядит как кнопка
+
+### Варианты реализации
+
+Какие варианты реализации у нас есть?
+
+1. Создать один компонент, который будет принимать все возможные пропсы и в зависимости от них будет рендериться тот или иной вариант кнопки
+2. Создать отдельный компонент для каждого варианта кнопки
+
+Мы отдадим предпочтение **первому варианту**, так как он более гибкий и позволит нам легко добавлять новые варианты кнопок в будущем.
+
+### Props
+
+Опишем пропсы, которые будет принимать наш компонент:
+
+```tsx filename="button.tsx"
+import { ComponentPropsWithoutRef } from 'react'
+
+export type ButtonProps = {
+ variant?: 'primary' | 'secondary' | 'tertiary' | 'link'
+ fullWidth?: boolean
+} & ComponentPropsWithoutRef<'button'>
+```
+
+`ComponentPropsWithoutRef<'button'>` - это пропсы, которые принимает стандартный html-тег button, мы их расширяем своими пропсами.
+
+### Реализация
+
+Создадим сам компонент:
+
+```tsx
+import s from './button.module.scss'
+
+export const Button = ({ variant = 'primary', fullWidth, className, ...rest }: ButtonProps) => {
+ return (
+
+ )
+}
+```
+
+Для проверки добавим стили в _button.module.scss_:
+
+```scss filename="button.module.scss"
+.primary {
+ background-color: red;
+}
+
+.secondary {
+ background-color: green;
+}
+
+.tertiary {
+ background-color: blue;
+}
+
+.link {
+ background-color: yellow;
+}
+
+.fullWidth {
+ width: 100%;
+}
+```
+
+Стили согласно дизайну напишите сами
+
+- В файл _index.ts_ добавим экспорт компонента:
+
+```ts filename="src/components/ui/button/index.ts"
+export * from './button'
+```
+
+- Добавим stories для компонента:
+
+```tsx filename="button.stories.ts"
+import type { Meta, StoryObj } from '@storybook/react'
+
+import { Button } from './'
+
+const meta = {
+ title: 'Components/Button',
+ component: Button,
+ tags: ['autodocs'],
+ argTypes: {
+ variant: {
+ options: ['primary', 'secondary', 'tertiary', 'link'],
+ control: { type: 'radio' },
+ },
+ },
+} satisfies Meta
+
+export default meta
+type Story = StoryObj
+
+export const Primary: Story = {
+ args: {
+ variant: 'primary',
+ children: 'Primary Button',
+ disabled: false,
+ },
+}
+
+export const Secondary: Story = {
+ args: {
+ variant: 'secondary',
+ children: 'Secondary Button',
+ disabled: false,
+ },
+}
+export const Tertiary: Story = {
+ args: {
+ variant: 'tertiary',
+ children: 'Tertiary Button',
+ disabled: false,
+ },
+}
+export const Link: Story = {
+ args: {
+ variant: 'link',
+ children: 'Tertiary Button',
+ disabled: false,
+ },
+}
+
+export const FullWidth: Story = {
+ args: {
+ variant: 'primary',
+ children: 'Full Width Button',
+ disabled: false,
+ fullWidth: true,
+ },
+}
+```
+
+Вот что должно получиться:
+
+
+
+## Полиморфные компоненты
+
+### Теория и реализация
+
+Дизайнеры могут внезапно решить, что кнопка должна выглядеть как ссылка, а ссылка как кнопка,
+но при этом мы должны рендерить правильные тэги в DOM, для удобства пользователей. Как быть в таком случае?
+Для этого нам нужно будет передать нужный тег через пропс:
+
+```tsx filename="button.tsx" showLineNumbers {6,15,19} /Component/4
+import { ComponentPropsWithoutRef } from 'react'
+
+import s from './button.module.scss'
+
+export type ButtonProps = {
+ as: any
+ variant?: 'primary' | 'secondary' | 'tertiary' | 'link'
+ fullWidth?: boolean
+} & ComponentPropsWithoutRef<'button'>
+
+export const Button = ({
+ variant = 'primary',
+ fullWidth,
+ className,
+ as: Component = 'button',
+ ...rest
+}: ButtonProps) => {
+ return (
+
+ )
+}
+```
+
+Этот
+
+Добавим в stories пример использования:
+
+```tsx filename="button.stories.ts"
+export const AsLink: Story = {
+ args: {
+ variant: 'primary',
+ children: 'Link that looks like a button',
+ as: 'a',
+ },
+}
+```
+
+И проверим что всё работает как надо:
+
+
+
+Как видите, в DOM мы получили тег `a` с нужными пропсами.
+
+
+ В данном примере кнопка с тэгом 'a' не выглядит так, как обычная кнопка. Это связано со
+ стандартными стилями браузера и будет работать как надо когда мы застилизуем нашу кнопку правильно
+
+
+### Типизация
+
+В примере выше мы использовали `as: any`, но это не очень хорошо, так как мы теряем типизацию. Давайте попробуем это исправить.
+
+Добавим generic параметр в наш компонент и в пропсы:
+
+```tsx filename="button.tsx" showLineNumbers {1,5,6,7,11,13,14}
+import { ComponentPropsWithoutRef, ElementType, ReactNode } from 'react'
+
+import s from './button.module.scss'
+
+export type ButtonProps = {
+ as?: T
+ children: ReactNode
+ variant?: 'primary' | 'secondary' | 'tertiary' | 'link'
+ fullWidth?: boolean
+ className?: string
+} & ComponentPropsWithoutRef
+
+export const Button = (props: ButtonProps) => {
+ const { variant = 'primary', fullWidth, className, as: Component = 'button', ...rest } = props
+
+ return (
+
+ )
+}
+```
+
+Попробуем отрисовать две кнопки в App.tsx, где одна кнопка будет с тегом `button`, а другая с тегом `a`:
+
+
+
+Типизация работает правильно, потому что в первом случае мы отрисовываем тэг `a`, у которого есть проп `href`, а во втором случае мы отрисовываем тэг `button`, у которого его нет, поэтому мы и видим ошибку.
+
+
+ Обратите внимание, что типизацию мы тестировали в App.tsx, а не в stories, потому что storybook
+ все еще иногда ошибается с TypeScript.
+
+
+Все работает как надо, но нужно поправить еще один маленький нюанс:
+Из-за того, что мы заранее не знаем какие пропсы будут у компонента, мы указали className как параметр нашей кнопки (расширяя стандартные пропсы), чтобы не потерять его типизацию:
+
+```tsx filename="button.tsx" showLineNumbers {9}
+import { ComponentPropsWithoutRef, ElementType } from 'react'
+
+import s from './button.module.scss'
+
+export type ButtonProps = {
+ as?: T
+ variant?: 'primary' | 'secondary' | 'tertiary' | 'link'
+ fullWidth?: boolean
+ className?: string
+} & ComponentPropsWithoutRef
+
+export const Button = (props: ButtonProps) => {
+ const { variant = 'primary', fullWidth, className, as: Component = 'button', ...rest } = props
+
+ return (
+
+ )
+}
+```
+
+Это может привести к неожиданным коллизиям и странным ошибкам TypeScript'а, поэтому мы добавим небольшую проверку:
+
+```tsx filename="button.tsx" showLineNumbers {13}
+import { ComponentPropsWithoutRef, ElementType } from 'react'
+
+import s from './button.module.scss'
+
+export type ButtonProps = {
+ as?: T
+ variant?: 'primary' | 'secondary' | 'tertiary' | 'link'
+ fullWidth?: boolean
+ className?: string
+} & ComponentPropsWithoutRef
+
+export const Button = (
+ props: ButtonProps & Omit, keyof ButtonProps>
+) => {
+ const { variant = 'primary', fullWidth, className, as: Component = 'button', ...rest } = props
+
+ return (
+
+ )
+}
+```
+
+С помощью Omit мы убираем из пропсов переданного компонента все пропсы, которые уже есть в наших кастомных пропсах, тем самым избегая коллизий.
+
+Подробнее про типизацию полиморфных компонентов можно почитать [вот тут](https://itnext.io/react-polymorphic-components-with-typescript-f7ce72ea7af2).
+
+### Коммитим изменения
+
+```bash filename="Terminal"
+git add . && git commit -m "add polymorphic button"
+```
diff --git a/pages/lesson-1/images/Button.png b/pages/lesson-1/images/Button.png
new file mode 100644
index 0000000..8c9fb40
Binary files /dev/null and b/pages/lesson-1/images/Button.png differ
diff --git a/pages/lesson-1/images/button-as-link-error.png b/pages/lesson-1/images/button-as-link-error.png
new file mode 100644
index 0000000..710aaea
Binary files /dev/null and b/pages/lesson-1/images/button-as-link-error.png differ
diff --git a/pages/lesson-1/images/storybook-button-as-link.png b/pages/lesson-1/images/storybook-button-as-link.png
new file mode 100644
index 0000000..1b32ef5
Binary files /dev/null and b/pages/lesson-1/images/storybook-button-as-link.png differ
diff --git a/pages/lesson-1/images/storybook-button.png b/pages/lesson-1/images/storybook-button.png
new file mode 100644
index 0000000..ab2fb2a
Binary files /dev/null and b/pages/lesson-1/images/storybook-button.png differ