@sberbusiness/triplex-next-mcp-server 1.43.0-beta → 1.44.0-beta

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,10 +1,9 @@
1
1
  ---
2
2
  component: TopOverlay
3
- category: Overlay
4
- figma-node: ""
5
- figma-file: ""
6
- related: [Overlay, LightBox]
7
- tokens: []
3
+ category: TopOverlay
4
+ related: [LightBox, Overlay]
5
+ tokens:
6
+ - --triplex-next-TopOverlay-Background
8
7
  stories: stories/TopOverlay/TopOverlay.stories.tsx
9
8
  version: "1.0"
10
9
  ---
@@ -13,128 +12,186 @@ version: "1.0"
13
12
 
14
13
  ## Назначение
15
14
 
16
- Оверлей, выезжающий из верха экрана: обёртка над `Overlay` с
17
- `direction=TOP`, `Mask` + `Panel`, FocusTrap, и автокоррекцией top-позиции
18
- при скролле родителя/`LightBox`-контейнера. Используется для системных
19
- сообщений, временных панелей.
15
+ Верхняя панель поверх контента `LightBox`: затемняющая маска на всю ширину экрана
16
+ лайтбокса и выезжающая сверху панель с контентом. Компонент — тонкая надстройка над
17
+ `Overlay` с зафиксированным `direction = EOverlayDirection.TOP`, ловушкой фокуса
18
+ (`FocusTrapExtended`) и пересчётом вертикального позиционирования относительно
19
+ экрана лайтбокса.
20
20
 
21
- Используй когда: нужна верхняя shading-панель внутри `LightBox` или
22
- прокручиваемого родителя, со встроенной фиксацией фокуса. Для общего
23
- оверлея — `Overlay` напрямую.
21
+ Реэкспортируется как `LightBox.TopOverlay` — это тот же самый компонент, а не обёртка.
24
22
 
25
- ---
26
-
27
- ## Файловая структура
28
-
29
- ```text
30
- src/components/TopOverlay/
31
- ├── TopOverlay.tsx # FC<ITopOverlayProps>; FocusTrap + Overlay(direction=TOP)
32
- ├── styles/TopOverlay.module.less
33
- ├── __tests__/
34
- └── index.ts
35
- ```
23
+ Используй когда: внутри `LightBox` нужно перекрыть контент подтверждением или
24
+ коротким сообщением, не закрывая сам лайтбокс (типовой сценарий — `Confirm` при
25
+ попытке выйти из формы с несохранёнными данными).
26
+ Не используй когда: панель нужна сбоку — `LightBox.SideOverlay`; нужен оверлей вне
27
+ `LightBox` с произвольным направлением — `Overlay`; нужно самостоятельное модальное
28
+ окно поверх страницы — `ModalWindow`.
36
29
 
37
30
  ---
38
31
 
39
- ## Публичный API
32
+ ## Варианты и props
40
33
 
41
- ### `ITopOverlayProps`
34
+ ### Обязательные props
42
35
 
43
- ```ts
44
- extends Pick<IOverlayProps, "opened" | "onOpen" | "onClose">
45
- ```
36
+ | Prop | Тип | Описание |
37
+ |---|---|---|
38
+ | `opened` | `boolean` | Управляемое состояние панели. Компонент не хранит его сам и не поднимает наружу: `setOpened` в нижележащий `Overlay` передаётся no-op'ом, закрытие инициирует только потребитель |
39
+
40
+ ### Опциональные props
46
41
 
47
- | Prop | Тип | Default | Описание |
42
+ | Prop | Тип | По умолчанию | Описание |
48
43
  |---|---|---|---|
49
- | `opened` | `boolean` | — | **Обязателен**. Контролируемое открытие. |
50
- | `onOpen?` | `() => void` | — | Колбэк при открытии (вызывается из `handleOpen`, после `updateTopPosition`). |
51
- | `onClose?` | `() => void` | — | Колбэк при закрытии (после анимации). |
52
- | `focusTrapProps?` | `FocusTrapProps` | — | Пропсы для `focus-trap-react`. По умолчанию `allowOutsideClick: true, preventScroll: true`. |
53
- | `children?` | `ReactNode` | — | Содержимое панели. |
44
+ | `children` | `React.ReactNode` | — | Содержимое панели. Обычный `ReactNode`, а не render-функция, как у `Overlay` |
45
+ | `onOpen` | `() => void` | — | Вызывается по завершении анимации открытия, после того как компонент запланировал активацию ловушки фокуса и пересчёт позиции. Это обновления состояния React — к моменту вызова они ещё не обязаны быть применены к DOM. При монтировании сразу с `opened={true}` анимации нет и колбэк приходит на маунте |
46
+ | `onClose` | `() => void` | — | Вызывается по завершении анимации закрытия, после того как компонент запланировал сброс внутреннего состояния и позиционирования. К DOM сброс так же применяется не раньше следующего рендера |
47
+ | `focusTrapProps` | `IFocusTrapExtendedProps` | — | Свойства `FocusTrapExtended`. Мержатся поверх собственных: `focusTrapOptions.preventScroll: true` задаётся по умолчанию и переопределяется из `focusTrapProps.focusTrapOptions`; переданный `active` перекрывает внутреннее управление ловушкой |
48
+ | `className` | `string` | — | Мержится с собственными классами обёртки через `clsx`, не затирает их |
49
+ | `style` | `React.CSSProperties` | — | Уходит на обёртку и мержится с runtime-переменной позиционирования. Собственный `top` в нём передавать не нужно: он перекроет рассчитанную позицию, потому что inline-свойство сильнее класса |
50
+ | `...HTMLDivAttributes` | — | — | Стандартные атрибуты корневого `<div>`-обёртки (кроме `children` и `onClose` — последний занят колбэком жизненного цикла) |
51
+
52
+ Props `opened`, `onOpen` и `onClose` берутся из `IOverlayProps` через `Pick<>` —
53
+ их типы и семантика синхронны с `Overlay`. Остальных props `Overlay`
54
+ (`direction`, `fixed`, `setOpened`, `onOpening`, `onClosing`) у `TopOverlay` нет:
55
+ направление зафиксировано, жизненный цикл управляется внутри.
56
+
57
+ `ref` и остальные DOM-атрибуты адресуют обёртку `.topOverlayWrapper` — элемент,
58
+ который держит `top`, классы `opened` / `closing` и внутри которого рендерится
59
+ `Overlay`. Во вложенный `Overlay` из props потребителя не уходит ничего.
60
+
61
+ ### Как работает позиционирование
62
+
63
+ Обёртка панели спозиционирована `absolute` внутри контента лайтбокса, поэтому её
64
+ фактическое положение во вьюпорте зависит от прокрутки родителя — при открытии на
65
+ прокрученном контенте панель уехала бы вверх или вниз от верхней границы экрана.
66
+ Компонент корректирует это инкрементально: читает CSS-переменную
67
+ `--lightBox-screen-top` у обёртки, сравнивает с `getBoundingClientRect().top` и
68
+ публикует разницу в runtime-переменной `--triplex-next-runtime-TopOverlay-Top`
69
+ (`utils.ts` → `getNextTopPosition`). Переменную читает `.topOverlayWrapper` в
70
+ LESS: `top: var(--triplex-next-runtime-TopOverlay-Top, 0)` — inline-стили в
71
+ компонентах запрещены (`docs/ai/codestyle.md`).
72
+
73
+ Пересчёт делается дважды: сразу при переходе `opened` в `true` и повторно в
74
+ `onOpen` — после завершения анимации, когда положение обёртки уже окончательное.
75
+ При закрытии (`onClose`) смещение сбрасывается в «не рассчитано», и переменная со
76
+ слоя снимается — позиция возвращается к дефолту `0` из LESS. Признак «не
77
+ рассчитано» — `undefined`, а не `0`: рассчитанный ноль такой же результат, как
78
+ любой другой, и он тоже публикуется (`0px`).
79
+
80
+ При монтировании сразу с `opened={true}` пересчёт **не делается**: в этот момент
81
+ лайтбокс ещё не на своём месте (у него собственная анимация появления), а
82
+ `getBoundingClientRect()` учитывает `transform` родителей — измерение дало бы
83
+ смещение в никуда (проверено: панель уезжала на `top: -832px`). Позиция остаётся
84
+ заданной стилями (`.topOverlayWrapper { top: 0 }` + `margin-top` панели), а
85
+ коррекция применяется при следующем открытии переходом `false → true`. Ловушка
86
+ фокуса и `onOpen` на маунте при этом отрабатывают штатно.
54
87
 
55
88
  ---
56
89
 
57
- ## Поведение
90
+ ## Дизайн-токены
58
91
 
59
- ### Состояния
60
-
61
- | Имя | Тип | Назначение |
62
- |---|---|---|
63
- | `closing` | `boolean` | Анимация закрытия в процессе (`opened` стал `false`, ещё не отработал). |
64
- | `activeFocusTrap` | `boolean` | Включён ли FocusTrap (включается на open, отключается перед closing). |
65
- | `overlayWrapperTopPosition` | `number \| string` | Top-смещение wrapper'а (0 по умолчанию). |
66
- | `prevOpened` | `Ref<boolean>` | Для определения перехода `true → false` / `false → true`. |
67
- | `overlayWrapperRef` | `Ref<HTMLDivElement>` | Для расчёта `getBoundingClientRect`. |
68
-
69
- ### `updateTopPosition`
70
-
71
- Вызывается в `handleOpen` и при `false → true`:
72
- 1. Читает `--lightBox-screen-top` CSS-переменную из computed-style wrapper'а.
73
- 2. Если `position.top < 0` (overlay выше viewport'а) — добавляет `Math.abs(position.top) + lightBoxTopPosition`.
74
- 3. Иначе — вычитает `position.top - lightBoxTopPosition`.
75
-
76
- Цель: компенсировать прокрутку родителя/`LightBox`'а так, чтобы overlay
77
- встал у видимого верха.
78
-
79
- ### Жизненный цикл
92
+ ```css
93
+ --triplex-next-TopOverlay-Background
94
+ ```
80
95
 
81
- - `opened: false → true`: `useEffect` зовёт `updateTopPosition`. `Overlay.onOpen` → `handleOpen` (внутри: `updateTopPosition` ещё раз + `setActiveFocusTrap(true)` + `onOpen?.()`).
82
- - `opened: true → false`: `setClosing(true)`. `Overlay.onClosing` (анимация началась) → `handleClosing` → `setActiveFocusTrap(false)`. `Overlay.onClose` (анимация завершилась) → `handleClose` → `setClosing(false)` + сброс `overlayWrapperTopPosition` + `onClose?.()`.
96
+ Токен задаёт цвет маски. Панель намеренно рисуется без фона и тени
97
+ (`background: none`, `box-shadow: none`) — фон даёт контент, который в неё кладут
98
+ (обычно `Confirm`), поэтому токен `--triplex-next-Overlay-Panel_Background`
99
+ здесь не действует.
83
100
 
84
- ### Структура
101
+ Runtime CSS-переменная (задаётся компонентом через `style`, **не** дизайн-токен,
102
+ в `DesignTokens` не попадает):
85
103
 
86
- ```tsx
87
- <FocusTrap active={activeFocusTrap} focusTrapOptions={{allowOutsideClick: true, preventScroll: true, ...}}>
88
- <div className={topOverlayWrapper {opened, closing}} ref={...} style={{top: ...}}>
89
- <Overlay direction={TOP} opened={opened} onOpen={handleOpen} onClose={handleClose} onClosing={handleClosing}>
90
- {(overlayProps) => (
91
- <>
92
- <Overlay.Mask opened={overlayProps.opened} />
93
- <Overlay.Panel {...overlayProps}>{children}</Overlay.Panel>
94
- </>
95
- )}
96
- </Overlay>
97
- </div>
98
- </FocusTrap>
99
104
  ```
105
+ --triplex-next-runtime-TopOverlay-Top
106
+ ```
107
+
108
+ Кроме токена компонент читает служебные CSS-переменные `LightBox`:
109
+ `--lightBox-screen-top` (в TS, для позиционирования), `--lightBox-screen-left` и
110
+ `--lightBox-screen-width` (в LESS, для растяжки маски по экрану лайтбокса).
100
111
 
101
112
  ---
102
113
 
103
114
  ## Инварианты
104
115
 
105
- - **`FC, не forwardRef`** — ref не пробрасывается.
106
- - **`opened` контролируемый** — внутреннего state открытости нет. `setOpened` передаётся в `Overlay`, но это пустой no-op (`() => {}`); закрытие должно идти через внешний `opened={false}`.
107
- - **`direction = EOverlayDirection.TOP`** — зашито; для других направлений использовать `Overlay` напрямую.
108
- - **`FocusTrap` использует `focus-trap-react`** — `allowOutsideClick: true` и `preventScroll: true` зашиты как defaults; потребитель может переопределить через `focusTrapProps.focusTrapOptions`.
109
- - **`activeFocusTrap` отключается ДО `onClose`** (на `onClosing`) — чтобы анимация уехала с уже разблокированным фокусом.
110
- - **`updateTopPosition` читает `--lightBox-screen-top`** — связь с `LightBox`. Без `LightBox` переменная отсутствует → `0`.
111
- - **`Overlay.Mask` и `Overlay.Panel` рендерятся внутри одного render-prop'а** — порядок Mask → Panel зашит.
112
- - **Стили `.topOverlay`/`.topOverlayMask`/`.topOverlayPanel`/`.topOverlayWrapper` + `.opened`/`.closing`** — публичные CSS-классы (используются для управления visual transitions).
113
- - **Без явной валидации `children`** — потребитель отвечает за содержимое.
116
+ - **`forwardRef` обязателен** — target ref'а — обёртка `.topOverlayWrapper`, а не
117
+ вложенный `Overlay`: именно на обёртке живут позиционирование и классы состояния.
118
+ - Публичный API — `opened`, `onOpen`, `onClose`, `focusTrapProps`, `children`,
119
+ DOM-атрибуты обёртки и интерфейс `ITopOverlayProps`. Переименование или изменение
120
+ типов — breaking change.
121
+ - `direction` жёстко задан как `EOverlayDirection.TOP` и не выносится в props —
122
+ на нём держатся стили панели и маски.
123
+ - `setOpened` уходит в `Overlay` no-op'ом: компонент полностью управляемый,
124
+ состояние живёт у потребителя.
125
+ - Монтирование сразу с `opened={true}` — рабочий сценарий: `OverlayBase` в этом
126
+ случае вызывает `onOpen` на маунте, ловушка фокуса активируется. Пересчёт
127
+ позиции на маунте намеренно пропускается (см. «Как работает позиционирование») —
128
+ не возвращать его, иначе панель уезжает за экран.
129
+ - Позиция задаётся runtime-переменной `--triplex-next-runtime-TopOverlay-Top`, а
130
+ не inline-свойством `top`: inline-стили в компонентах запрещены. Не возвращать
131
+ `style={{ top }}` — правило проверяется ревью по `.coderabbit.yaml`.
132
+ - Props потребителя во вложенный `Overlay` не уходят: `className` и `direction`
133
+ задаются ему явно, а `ref`, `className`, `style` и DOM-атрибуты остаются на
134
+ обёртке. Спреда `restProps` в `Overlay` больше нет — не возвращать.
135
+ - Родительскому `LightBox` потребитель обязан передать `isTopOverlayOpened` —
136
+ иначе контролы лайтбокса не приглушатся под открытой панелью.
137
+ - Классы `.topOverlayMask`, `.topOverlayPanel`, `.topOverlayWrapper` и `.topOverlay`
138
+ из `styles/TopOverlay.module.less` проверяются unit-тестами — не переименовывать
139
+ без синхронного обновления тестов.
140
+ - `z-index` панели (`@z-index-step * 5`, 500) выбран так, чтобы панель была выше
141
+ `.lightBoxControls` и лоадера контента лайтбокса. Понижение вернёт баг, при
142
+ котором панель перекрывалась `LoaderScreen`.
114
143
 
115
144
  ---
116
145
 
117
146
  ## Accessibility
118
147
 
119
- - `FocusTrap` — фокус заперт внутри overlay при `activeFocusTrap = true`.
120
- - `allowOutsideClick: true` — клик вне overlay не блокируется (закрытие — на потребителе через external `opened`).
121
- - `preventScroll: true` — focus-trap не вызывает scroll при initial focus.
122
- - ARIA-роли (dialog/alertdialog) на overlay не выставляются — потребитель отвечает за разметку children (например, `<div role="dialog" aria-modal="true" aria-label>`).
148
+ - Пока панель открыта, фокус удерживается внутри неё: `FocusTrapExtended`
149
+ активируется в `onOpen` (после анимации открытия, а при монтировании сразу
150
+ открытым — на маунте) и деактивируется в `onClosing` (в начале закрытия),
151
+ поэтому возврат фокуса на триггер происходит до того, как панель исчезнет.
152
+ - `focusTrapOptions.preventScroll: true` задаётся по умолчанию — установка фокуса
153
+ внутри панели не прокручивает контент лайтбокса.
154
+ - Роль диалога компонент на себя не берёт: `aria-modal="true"` и `role="dialog"`
155
+ уже стоят на корневом элементе `LightBox`, а панель рендерится внутри него.
156
+ - Маска панели некликабельна — обработчик закрытия по клику вне контента не
157
+ вешается. Закрытие целиком на стороне потребителя: кнопки внутри панели или
158
+ `Confirm.Close` с `clickByEsc`.
159
+ - Компонент не хардкодит тексты и `aria-label` — доступные имена задаёт потребитель
160
+ через содержимое панели.
123
161
 
124
162
  ---
125
163
 
126
164
  ## Связанные компоненты
127
165
 
128
- - `Overlay` (`Overlay.Mask`, `Overlay.Panel`) — основа.
129
- - `EOverlayDirection.TOP` — направление.
130
- - `LightBox` — типичный родитель (через CSS-переменную `--lightBox-screen-top`).
131
- - `focus-trap-react` (`FocusTrap`, `FocusTrapProps`) — npm-пакет для трапа фокуса.
166
+ - `LightBox` — родитель. Реэкспортирует компонент как `LightBox.TopOverlay`,
167
+ задаёт CSS-переменные экрана, по `isTopOverlayOpened` приглушает свои контролы.
168
+ - `Overlay` — база. `TopOverlay` рендерит `Overlay`, `Overlay.Mask` и
169
+ `Overlay.Panel`, а `ITopOverlayProps` берёт часть полей из `IOverlayProps`
170
+ через `Pick<>`.
171
+ - `FocusTrapExtended` — обёртка над `focus-trap-react`, удерживающая фокус внутри
172
+ открытой панели; настраивается через `focusTrapProps`.
173
+ - `Confirm` — типовое содержимое панели: подтверждение выхода из формы.
132
174
 
133
175
  ---
134
176
 
135
177
  ## Stories
136
178
 
137
179
  Основные истории: `stories/TopOverlay/TopOverlay.stories.tsx`
180
+ Файлы примеров: `stories/TopOverlay/examples/`
181
+
182
+ | Story | Example file | Что демонстрирует |
183
+ |---|---|---|
184
+ | `Playground` | `Playground.tsx` | Интерактивное управление `opened` в открытом лайтбоксе |
185
+ | `Default` | `Default.tsx` | Минимальный сценарий: панель открывается кнопкой из шапки лайтбокса |
186
+ | `WithConfirm` | `WithConfirm.tsx` | Production-like сценарий: подтверждение выхода из формы через `Confirm` |
187
+ | `VisualTests` | `VisualTests.tsx` | Открытая панель поверх контента лайтбокса — состояние для скриншот-тестов |
188
+
189
+ Стори `Default` и `WithConfirm` исключены из скриншот-тестов (`testRunner.skip`):
190
+ лайтбокс в них открывается по клику, статичный снимок показал бы только кнопку.
191
+ Визуальное покрытие даёт `VisualTests`, где панель отрисована открытой сразу —
192
+ без `play`-функции, чтобы снимок не зависел от анимации выезда. Заодно эта стори
193
+ сторожит сценарий «смонтирован открытым»: если вернуть пересчёт позиции на маунт,
194
+ её baseline сразу разъедется.
138
195
 
139
196
  ---
140
197
 
@@ -142,4 +199,223 @@ extends Pick<IOverlayProps, "opened" | "onOpen" | "onClose">
142
199
 
143
200
  | Дата | Изменение |
144
201
  |---|---|
145
- | 2026-04-18 | Создан документ |
202
+ | 2026-08-27 | `TopOverlay` переведён на `forwardRef`, `ITopOverlayProps` расширен атрибутами `<div>`: `ref`, `className`, `style` и DOM-атрибуты адресуют обёртку. Монтирование сразу с `opened={true}` запускает цикл открытия (ловушка фокуса, `onOpen`) — исправлено в `OverlayBase`, см. `Overlay-ai.md`; пересчёт позиции на маунте намеренно не делается. Рассчитанная позиция переехала из inline-`top` в runtime-переменную `--triplex-next-runtime-TopOverlay-Top`. |
203
+ | 2026-08-25 | Создан документ. AI-рефакторинг: позиционирование вынесено в `utils.ts`, добавлен `displayName`, unit-тесты на расчёт позиции и ловушку фокуса; добавлены stories `stories/TopOverlay/` |
204
+
205
+ ---
206
+
207
+ ## Примеры использования
208
+
209
+ ### Default
210
+
211
+ `stories/TopOverlay/examples/Default.tsx`
212
+
213
+ ```tsx
214
+ import React, { useState } from "react";
215
+ import {
216
+ Button,
217
+ EBodyPageType,
218
+ EButtonTheme,
219
+ EComponentSize,
220
+ EFontType,
221
+ EHeaderPageType,
222
+ ETextSize,
223
+ ETitleSize,
224
+ FocusTrapUtils,
225
+ Gap,
226
+ LightBox,
227
+ Page,
228
+ Text,
229
+ Title,
230
+ TopOverlay,
231
+ } from "@sberbusiness/triplex-next";
232
+
233
+ export const Default = () => {
234
+ const [lightBoxOpened, setLightBoxOpened] = useState(false);
235
+ const [topOverlayOpened, setTopOverlayOpened] = useState(false);
236
+
237
+ return (
238
+ <div>
239
+ <Button theme={EButtonTheme.SECONDARY} size={EComponentSize.MD} onClick={() => setLightBoxOpened(true)}>
240
+ Открыть LightBox
241
+ </Button>
242
+
243
+ {lightBoxOpened ? (
244
+ <LightBox isLoading={false} isSideOverlayOpened={false} isTopOverlayOpened={topOverlayOpened}>
245
+ <LightBox.Content isLoading={false}>
246
+ <Page>
247
+ <Page.Header type={EHeaderPageType.FIRST} sticky>
248
+ <Page.Header.Title>
249
+ <Page.Header.Title.Content>
250
+ <Title
251
+ tag="h1"
252
+ size={ETitleSize.H1}
253
+ tabIndex={-1}
254
+ // Устанавливает фокус на первый элемент при открытии LightBox.
255
+ {...{ [FocusTrapUtils.firstInteractionElementDataAttr]: true }}
256
+ >
257
+ Заявка на кредит
258
+ </Title>
259
+ </Page.Header.Title.Content>
260
+ <Page.Header.Title.Controls>
261
+ <Button
262
+ theme={EButtonTheme.GENERAL}
263
+ size={EComponentSize.MD}
264
+ onClick={() => setTopOverlayOpened(true)}
265
+ >
266
+ Открыть верхнюю панель
267
+ </Button>
268
+ </Page.Header.Title.Controls>
269
+ </Page.Header.Title>
270
+ </Page.Header>
271
+
272
+ <Page.Body type={EBodyPageType.FIRST}>
273
+ <Text tag="div" size={ETextSize.B3} type={EFontType.SECONDARY}>
274
+ Верхняя панель выезжает поверх контента лайтбокса и затемняет его маской.
275
+ </Text>
276
+ </Page.Body>
277
+ </Page>
278
+
279
+ <TopOverlay opened={topOverlayOpened} onOpen={() => {}} onClose={() => {}}>
280
+ <div style={{ padding: 24, textAlign: "center" }}>
281
+ <Title tag="h2" size={ETitleSize.H2}>
282
+ Верхняя панель
283
+ </Title>
284
+ <Gap size={16} />
285
+ <Button
286
+ theme={EButtonTheme.SECONDARY}
287
+ size={EComponentSize.MD}
288
+ onClick={() => setTopOverlayOpened(false)}
289
+ >
290
+ Закрыть панель
291
+ </Button>
292
+ </div>
293
+ </TopOverlay>
294
+ </LightBox.Content>
295
+
296
+ <LightBox.Controls>
297
+ <LightBox.Controls.Close title="Закрыть" onClick={() => setLightBoxOpened(false)} />
298
+ </LightBox.Controls>
299
+ </LightBox>
300
+ ) : null}
301
+ </div>
302
+ );
303
+ };
304
+ ```
305
+
306
+ ### WithConfirm
307
+
308
+ `stories/TopOverlay/examples/WithConfirm.tsx`
309
+
310
+ ```tsx
311
+ import React, { useState } from "react";
312
+ import {
313
+ Button,
314
+ Confirm,
315
+ EBodyPageType,
316
+ EButtonTheme,
317
+ EComponentSize,
318
+ EFontType,
319
+ EHeaderPageType,
320
+ ETextSize,
321
+ ETitleSize,
322
+ FocusTrapUtils,
323
+ LightBox,
324
+ Page,
325
+ Text,
326
+ Title,
327
+ TopOverlay,
328
+ } from "@sberbusiness/triplex-next";
329
+
330
+ /**
331
+ * Типовой сценарий: закрытие LightBox с несохранёнными данными подтверждается в верхней панели.
332
+ */
333
+ export const WithConfirm = () => {
334
+ const [lightBoxOpened, setLightBoxOpened] = useState(false);
335
+ const [topOverlayOpened, setTopOverlayOpened] = useState(false);
336
+
337
+ const handleCloseLightBox = () => setTopOverlayOpened(true);
338
+ const handleCancel = () => setTopOverlayOpened(false);
339
+ const handleConfirm = () => {
340
+ setTopOverlayOpened(false);
341
+ setLightBoxOpened(false);
342
+ };
343
+
344
+ return (
345
+ <div>
346
+ <Button theme={EButtonTheme.SECONDARY} size={EComponentSize.MD} onClick={() => setLightBoxOpened(true)}>
347
+ Открыть LightBox
348
+ </Button>
349
+
350
+ {lightBoxOpened ? (
351
+ <LightBox isLoading={false} isSideOverlayOpened={false} isTopOverlayOpened={topOverlayOpened}>
352
+ <LightBox.Content isLoading={false}>
353
+ <Page>
354
+ <Page.Header type={EHeaderPageType.FIRST} sticky>
355
+ <Page.Header.Title>
356
+ <Page.Header.Title.Content>
357
+ <Title
358
+ tag="h1"
359
+ size={ETitleSize.H1}
360
+ tabIndex={-1}
361
+ // Устанавливает фокус на первый элемент при открытии LightBox.
362
+ {...{ [FocusTrapUtils.firstInteractionElementDataAttr]: true }}
363
+ >
364
+ Форма редактирования
365
+ </Title>
366
+ </Page.Header.Title.Content>
367
+ </Page.Header.Title>
368
+ </Page.Header>
369
+
370
+ <Page.Body type={EBodyPageType.FIRST}>
371
+ <Text tag="div" size={ETextSize.B3} type={EFontType.SECONDARY}>
372
+ Закройте лайтбокс крестиком — верхняя панель попросит подтвердить выход.
373
+ </Text>
374
+ </Page.Body>
375
+ </Page>
376
+
377
+ <TopOverlay opened={topOverlayOpened} onOpen={() => {}} onClose={() => {}}>
378
+ <Confirm>
379
+ <Confirm.Content>
380
+ <Confirm.Content.Title>Внимание</Confirm.Content.Title>
381
+ <Confirm.Content.SubTitle>
382
+ Несохранённые данные будут утеряны. Вы уверены, что хотите покинуть форму
383
+ редактирования?
384
+ </Confirm.Content.SubTitle>
385
+ </Confirm.Content>
386
+ <Confirm.Controls>
387
+ <Button
388
+ theme={EButtonTheme.SECONDARY}
389
+ size={EComponentSize.MD}
390
+ onClick={handleCancel}
391
+ >
392
+ Отмена
393
+ </Button>
394
+ <Button
395
+ theme={EButtonTheme.DANGER}
396
+ size={EComponentSize.MD}
397
+ onClick={handleConfirm}
398
+ >
399
+ Покинуть форму
400
+ </Button>
401
+ </Confirm.Controls>
402
+ <Confirm.Close
403
+ title="Закрыть"
404
+ // Закрыть по Esc, только если верхняя панель открыта.
405
+ clickByEsc={topOverlayOpened}
406
+ onClick={handleCancel}
407
+ />
408
+ </Confirm>
409
+ </TopOverlay>
410
+ </LightBox.Content>
411
+
412
+ <LightBox.Controls>
413
+ <LightBox.Controls.Close title="Закрыть" onClick={handleCloseLightBox} />
414
+ </LightBox.Controls>
415
+ </LightBox>
416
+ ) : null}
417
+ </div>
418
+ );
419
+ };
420
+ ```
421
+