@sberbusiness/triplex-next-mcp-server 1.41.0-beta → 1.42.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.
@@ -0,0 +1,323 @@
1
+ ---
2
+ component: ComposedKeyDownListener
3
+ category: KeyDownListener
4
+ related: [KeyDownListener, TriggerClickOnKeyDownEvent, SelectExtendedField, SliderExtended]
5
+ tokens: []
6
+ stories: stories/ComposedKeyDownListener/ComposedKeyDownListener.stories.tsx
7
+ version: "1.0"
8
+ ---
9
+
10
+ # ComposedKeyDownListener
11
+
12
+ ## Назначение
13
+
14
+ Сервисный headless-компонент, композитор `KeyDownListener`. Принимает массив
15
+ конфигураций `keyDownListeners` и рендерит по одному `KeyDownListener` на элемент
16
+ массива — соседями к `children`. Каждый слушатель подписывается на `keydown` на
17
+ `window` независимо, поэтому компонент — это только способ описать несколько
18
+ горячих клавиш одним объявлением вместо ручного вложения нескольких
19
+ `KeyDownListener`.
20
+
21
+ Собственной разметки не рендерит: `children` выводятся как есть, без обёртки;
22
+ без `children` (и при любом наборе слушателей) DOM остаётся пустым — сами
23
+ `KeyDownListener` рендерят `null`.
24
+
25
+ Используй когда: у экрана или оверлея несколько глобальных горячих клавиш с
26
+ разными обработчиками — `Enter` подтверждает, `Esc` отменяет, стрелки листают.
27
+
28
+ Не используй когда:
29
+ - Клавиша всего одна — достаточно `KeyDownListener` напрямую.
30
+ - Клавиши разные, но обработчик один — у `KeyDownListener` `eventKeyCode`
31
+ принимает массив кодов; различить нажатую можно по `event.keyCode`.
32
+ - Клавиша должна обрабатываться только внутри конкретного элемента — навесь
33
+ `onKeyDown` на сам элемент: слушатели здесь глобальные и сработают при вводе в
34
+ любое поле на странице.
35
+ - Нужно кликнуть по конкретной кнопке по нажатию клавиши — есть готовый
36
+ `TriggerClickOnKeyDownEvent`.
37
+
38
+ ---
39
+
40
+ ## Варианты и props
41
+
42
+ ### Обязательные props
43
+
44
+ | Prop | Тип | Описание |
45
+ |---|---|---|
46
+ | `keyDownListeners` | `IKeyDownListenerProps[]` | Конфигурация слушателей. Из каждого элемента используются только `eventKeyCode` и `onMatch`; поле `children` элемента игнорируется — содержимое задаётся `children` самого композитора |
47
+
48
+ ### Опциональные props
49
+
50
+ | Prop | Тип | По умолчанию | Описание |
51
+ |---|---|---|---|
52
+ | `children` | `React.ReactNode` | — | Рендерится как есть, без обёртки. При пустом `keyDownListeners` компонент сводится к `children` |
53
+
54
+ ### Особенности поведения
55
+
56
+ - Слушатели рендерятся **соседями** к `children`
57
+ (`<>{listeners}{children}</>`), а не оборачивают их. Поэтому `children` не
58
+ зависят от длины и порядка массива: набор горячих клавиш можно менять между
59
+ рендерами, содержимое при этом не перемонтируется — DOM-узлы, значения
60
+ инпутов, фокус и анимации сохраняются.
61
+ - `onMatch` вызываются в порядке элементов `keyDownListeners`: слушатели
62
+ монтируются в порядке массива, а `window` вызывает обработчики в порядке
63
+ подписки. На нём завязан только порядок вызова — «перехватить» событие у
64
+ соседнего слушателя нельзя, все подписаны на `window` независимо.
65
+ - Несколько элементов массива могут слушать одну и ту же клавишу — сработают
66
+ все.
67
+ - Слушатели подписываются на `window` **до** `children`, потому что монтируются
68
+ раньше них. Если внутри `children` есть собственный глобальный
69
+ `keydown`-обработчик той же клавиши, `onMatch` сработает первым.
70
+ - Компонент ничего не мемоизирует: массив можно собирать инлайн в рендере.
71
+ Каждый `KeyDownListener` читает свежие `eventKeyCode` и `onMatch` из props в
72
+ момент события, переподписки на новые ссылки не происходит.
73
+ - `keyDownListeners` сопоставляются между рендерами по индексу (`key={index}`).
74
+ Для элементов конфигурации это безопасно: `KeyDownListener` не рендерит
75
+ разметку и не хранит внутренний state, поэтому переиспользование инстанса по
76
+ позиции наблюдаемого эффекта не даёт.
77
+
78
+ ---
79
+
80
+ ## Дизайн-токены
81
+
82
+ Собственных стилей и CSS-переменных нет — компонент не рендерит разметку.
83
+
84
+ ---
85
+
86
+ ## Инварианты
87
+
88
+ - **Имя prop `keyDownListeners`, `children` и интерфейс
89
+ `IComposedKeyDownListenerProps`** (экспортируется из barrel
90
+ `src/components/KeyDownListener/index.ts`) — публичное API, не менять.
91
+ - **Элементы `keyDownListeners` типизированы `IKeyDownListenerProps`** —
92
+ переиспользование типа `KeyDownListener` осознанное; не заводить отдельный тип
93
+ ради «более узкого» `Pick`, это сузит публичный контракт.
94
+ - **Плоский рендер слушателей** — не возвращаться к вложению
95
+ (`reduce` с оборачиванием `children`): вложенность делает глубину дерева
96
+ зависимой от длины массива и перемонтирует `children` при её изменении.
97
+ - **Собственной разметки нет** — не добавлять обёртку (`div`, `span`) вокруг
98
+ `children`: потребители рассчитывают, что компонент прозрачен для layout,
99
+ в том числе внутри flex/grid-контейнеров.
100
+ - **Без `forwardRef`** — осознанное исключение, как и у `KeyDownListener`:
101
+ компонент не рендерит собственный host-элемент, форвардить `ref` некуда.
102
+ - Реализация остаётся React 17-совместимой (функциональный компонент без
103
+ React 18-only API): код синхронизируется в `release-0`.
104
+
105
+ ---
106
+
107
+ ## Accessibility
108
+
109
+ - Собственной ARIA-семантики нет — компонент прозрачен для accessibility-дерева
110
+ и не рендерит фокусируемых элементов.
111
+ - Горячие клавиши глобальные: перехватываются независимо от позиции фокуса и не
112
+ анонсируются screen reader'ом. Действия, повешенные на клавиши, должны быть
113
+ доступны и обычным путём (кнопкой) — шорткат не единственный способ выполнить
114
+ действие.
115
+ - Компонент не проверяет, где находится фокус: `Enter` и `Esc` сработают и во
116
+ время ввода в поле. Если это нежелательно, фильтруй в `onMatch` по
117
+ `event.target`.
118
+
119
+ ---
120
+
121
+ ## Связанные компоненты
122
+
123
+ - `KeyDownListener` (`src/components/KeyDownListener/KeyDownListener.tsx`) —
124
+ элементарный слушатель, из которого собирается композитор; там же описано
125
+ сравнение по устаревшему `event.keyCode` и константы `EVENT_KEY_CODES`.
126
+ - `TriggerClickOnKeyDownEvent` (`src/components/Triggers/`) — по нажатию клавиши
127
+ вызывает `click` на элементе из `targetRef`.
128
+ - `SelectExtendedField`, `SliderExtendedDot`, `SliderExtendedTrack` — потребители
129
+ `KeyDownListener` внутри библиотеки.
130
+
131
+ ---
132
+
133
+ ## Stories
134
+
135
+ Основные истории: `stories/ComposedKeyDownListener/ComposedKeyDownListener.stories.tsx`
136
+ Файлы примеров: `stories/ComposedKeyDownListener/examples/`
137
+
138
+ | Story | Example file | Что демонстрирует |
139
+ |---|---|---|
140
+ | `Playground` | `Playground.tsx` | Набор клавиш выбирается чекбоксами: слушатели пересобираются, введённый текст сохраняется |
141
+ | `Default` | `Default.tsx` | Две клавиши с разными обработчиками: `Enter` подтверждает, `Esc` отменяет |
142
+ | `WithDynamicListeners` | `WithDynamicListeners.tsx` | Слушатель добавляется на лету — `children` не перемонтируются |
143
+ | `Example` | `Example.tsx` | Панель подтверждения: горячие клавиши смонтированы вместе с панелью, те же действия доступны кнопками |
144
+
145
+ Набор целиком исключён из скриншот-тестов (`testRunner: { skip: true }` в `meta`):
146
+ собственной разметки компонент не рендерит, на скриншот попадала бы только
147
+ обвязка примеров. Поведение слушателей покрыто unit-тестами.
148
+
149
+ ---
150
+
151
+ ## История изменений
152
+
153
+ | Дата | Изменение |
154
+ |---|---|
155
+ | 2026-08-11 | Создан документ (TRI-112). Слушатели рендерятся соседями к `children` вместо вложения — смена длины `keyDownListeners` больше не перемонтирует содержимое (покрыто unit-тестом). Добавлен собственный набор stories по modern pattern; демонстрационная story `Example` перенесена из набора `KeyDownListener`. Публичный API не изменён. |
156
+
157
+ ---
158
+
159
+ ## Примеры использования
160
+
161
+ ### Default
162
+
163
+ `stories/ComposedKeyDownListener/examples/Default.tsx`
164
+
165
+ ```tsx
166
+ import React, { useState } from "react";
167
+ import { ComposedKeyDownListener, EVENT_KEY_CODES } from "@sberbusiness/triplex-next";
168
+
169
+ export const Default = () => {
170
+ const [result, setResult] = useState("Ожидание нажатия клавиши");
171
+
172
+ const keyDownListeners = [
173
+ { eventKeyCode: EVENT_KEY_CODES.ENTER, onMatch: () => setResult("Подтверждено по Enter") },
174
+ { eventKeyCode: EVENT_KEY_CODES.ESCAPE, onMatch: () => setResult("Отменено по Esc") },
175
+ ];
176
+
177
+ return (
178
+ <ComposedKeyDownListener keyDownListeners={keyDownListeners}>
179
+ <div style={{ display: "flex", flexDirection: "column", gap: "16px", maxWidth: "400px" }}>
180
+ <div>Две горячие клавиши с разными обработчиками: Enter подтверждает, Esc отменяет.</div>
181
+ <div style={{ border: "1px dashed rgb(125, 131, 138)", borderRadius: "4px", padding: "16px" }}>
182
+ {result}
183
+ </div>
184
+ </div>
185
+ </ComposedKeyDownListener>
186
+ );
187
+ };
188
+ ```
189
+
190
+ ### WithDynamicListeners
191
+
192
+ `stories/ComposedKeyDownListener/examples/WithDynamicListeners.tsx`
193
+
194
+ ```tsx
195
+ import React, { useState } from "react";
196
+ import { ComposedKeyDownListener, EVENT_KEY_CODES } from "@sberbusiness/triplex-next";
197
+
198
+ export const WithDynamicListeners = () => {
199
+ const [arrowsEnabled, setArrowsEnabled] = useState(false);
200
+ const [result, setResult] = useState("Ожидание нажатия клавиши");
201
+
202
+ const keyDownListeners = [
203
+ { eventKeyCode: EVENT_KEY_CODES.ESCAPE, onMatch: () => setResult("Отменено по Esc") },
204
+ ...(arrowsEnabled
205
+ ? [
206
+ {
207
+ eventKeyCode: [EVENT_KEY_CODES.ARROW_LEFT, EVENT_KEY_CODES.ARROW_RIGHT],
208
+ onMatch: (event: KeyboardEvent) =>
209
+ setResult(event.keyCode === EVENT_KEY_CODES.ARROW_LEFT ? "Стрелка влево" : "Стрелка вправо"),
210
+ },
211
+ ]
212
+ : []),
213
+ ];
214
+
215
+ return (
216
+ <ComposedKeyDownListener keyDownListeners={keyDownListeners}>
217
+ <div style={{ display: "flex", flexDirection: "column", gap: "16px", maxWidth: "400px" }}>
218
+ <div>Набор слушателей меняется на лету — содержимое при этом не перемонтируется.</div>
219
+ <input type="text" placeholder="Введите текст" style={{ padding: "8px", width: "100%" }} />
220
+ <label style={{ display: "flex", alignItems: "center", gap: "8px" }}>
221
+ <input
222
+ type="checkbox"
223
+ checked={arrowsEnabled}
224
+ onChange={(event) => setArrowsEnabled(event.target.checked)}
225
+ />
226
+ Добавить слушатель стрелок — введённый текст и фокус сохранятся
227
+ </label>
228
+ <div style={{ border: "1px dashed rgb(125, 131, 138)", borderRadius: "4px", padding: "16px" }}>
229
+ {result}
230
+ </div>
231
+ </div>
232
+ </ComposedKeyDownListener>
233
+ );
234
+ };
235
+ ```
236
+
237
+ ### Example
238
+
239
+ `stories/ComposedKeyDownListener/examples/Example.tsx`
240
+
241
+ ```tsx
242
+ import React, { useRef, useState } from "react";
243
+ import {
244
+ Button,
245
+ ComposedKeyDownListener,
246
+ EButtonTheme,
247
+ EComponentSize,
248
+ EVENT_KEY_CODES,
249
+ } from "@sberbusiness/triplex-next";
250
+
251
+ export const Example = () => {
252
+ const [opened, setOpened] = useState(true);
253
+ const [status, setStatus] = useState("Черновик не сохранён");
254
+ const panelRef = useRef<HTMLDivElement>(null);
255
+
256
+ const handleSave = () => {
257
+ setStatus("Черновик сохранён");
258
+ setOpened(false);
259
+ };
260
+
261
+ const handleCancel = () => {
262
+ setStatus("Сохранение отменено");
263
+ setOpened(false);
264
+ };
265
+
266
+ // Слушатели глобальные, поэтому Enter на сфокусированной кнопке панели дошёл бы
267
+ // и до onMatch, и до её onClick. Отфильтровываем такие события по event.target —
268
+ // действие кнопки должно выигрывать у горячей клавиши.
269
+ const handleSaveOnEnter = (event: KeyboardEvent) => {
270
+ if (event.target instanceof HTMLButtonElement && panelRef.current?.contains(event.target)) {
271
+ return;
272
+ }
273
+
274
+ handleSave();
275
+ };
276
+
277
+ // Слушатели монтируются только вместе с панелью, поэтому горячие клавиши
278
+ // работают лишь пока она открыта.
279
+ const keyDownListeners = [
280
+ { eventKeyCode: EVENT_KEY_CODES.ENTER, onMatch: handleSaveOnEnter },
281
+ { eventKeyCode: EVENT_KEY_CODES.ESCAPE, onMatch: handleCancel },
282
+ ];
283
+
284
+ if (!opened) {
285
+ return (
286
+ <div style={{ display: "flex", flexDirection: "column", gap: "16px", maxWidth: "400px" }}>
287
+ <div>{status}</div>
288
+ <Button theme={EButtonTheme.SECONDARY} size={EComponentSize.MD} onClick={() => setOpened(true)}>
289
+ Открыть панель снова
290
+ </Button>
291
+ </div>
292
+ );
293
+ }
294
+
295
+ return (
296
+ <ComposedKeyDownListener keyDownListeners={keyDownListeners}>
297
+ <div
298
+ ref={panelRef}
299
+ style={{
300
+ display: "flex",
301
+ flexDirection: "column",
302
+ gap: "16px",
303
+ maxWidth: "400px",
304
+ border: "1px solid rgb(125, 131, 138)",
305
+ borderRadius: "8px",
306
+ padding: "16px",
307
+ }}
308
+ >
309
+ <div>Сохранить черновик? Enter — сохранить, Esc — отменить.</div>
310
+ <div style={{ display: "flex", gap: "8px" }}>
311
+ <Button theme={EButtonTheme.GENERAL} size={EComponentSize.MD} onClick={handleSave}>
312
+ Сохранить
313
+ </Button>
314
+ <Button theme={EButtonTheme.SECONDARY} size={EComponentSize.MD} onClick={handleCancel}>
315
+ Отменить
316
+ </Button>
317
+ </div>
318
+ </div>
319
+ </ComposedKeyDownListener>
320
+ );
321
+ };
322
+ ```
323
+
@@ -39,7 +39,7 @@ version: "1.0"
39
39
 
40
40
  | Prop | Тип | По умолчанию | Описание |
41
41
  |---|---|---|---|
42
- | `sticky` | `boolean` | `false` | Футер прилипает к нижней границе при скролле. **Только для `type=FIRST`** (внутри `LightBox`); для `type=SECOND` типизирован как `never`. |
42
+ | `sticky` | `boolean` | `false` | Футер прилипает к нижней границе при скролле, если ширина viewport от 992px, а высота больше 800px. **Только для `type=FIRST`**; для `type=SECOND` типизирован как `never`. |
43
43
  | `size` | `EComponentSize` | — | Размер острова (Island). **Доступен только для `type=FIRST`**; для `type=SECOND` типизирован как `never`. |
44
44
 
45
45
  ### Ограничения по типам
@@ -111,6 +111,7 @@ version: "1.0"
111
111
 
112
112
  | Дата | Изменение |
113
113
  |---|---|
114
+ | 2026-08-06 | Sticky-позиционирование доступно вне `LightBox` при ширине viewport от 992px и отключено при высоте viewport не больше 800px. |
114
115
  | 2026-06-19 | Создан документ. AI-рефакторинг (clsx order, JSDoc на props, displayName), unit-тесты, миграция stories на modern pattern. |
115
116
 
116
117
  ---
@@ -40,7 +40,7 @@ version: "1.0"
40
40
 
41
41
  | Prop | Тип | По умолчанию | Описание |
42
42
  |---|---|---|---|
43
- | `sticky` | `boolean` | `false` | Заголовок прилипает к верхней границе при скролле. **Только для `type=FIRST`** (внутри `LightBox`); для `type=SECOND` типизирован как `never`. |
43
+ | `sticky` | `boolean` | `false` | Заголовок прилипает к верхней границе при скролле, если ширина viewport от 992px, а высота больше 800px. **Только для `type=FIRST`**; для `type=SECOND` типизирован как `never`. |
44
44
  | `size` | `EComponentSize` | — | Размер острова (Island). **Доступен только для `type=FIRST`**; для `type=SECOND` типизирован как `never`. |
45
45
 
46
46
  ### Ограничения по типам
@@ -114,6 +114,7 @@ version: "1.0"
114
114
 
115
115
  | Дата | Изменение |
116
116
  |---|---|
117
+ | 2026-08-06 | Sticky-позиционирование доступно вне `LightBox` при ширине viewport от 992px и отключено при высоте viewport не больше 800px. |
117
118
  | 2026-06-23 | Создан документ. AI-рефакторинг (clsx order, JSDoc на props, displayName), unit-тесты, миграция stories на modern pattern. |
118
119
 
119
120
  ---
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  component: Island
3
3
  category: Layout
4
- related: [IslandAccordion, IslandWidget, BodyPage, HeaderPage, FooterPage, Confirm, ModalWindowBody]
4
+ related: [IslandAccordion, IslandWidget, LoaderScreen, ModalWindow]
5
5
  tokens:
6
6
  - --triplex-next-Island-Type1_Background
7
7
  - --triplex-next-Island-Type1_Shadow
@@ -18,8 +18,9 @@ version: "1.0"
18
18
  ## Назначение
19
19
 
20
20
  Базовый контейнер-карточка библиотеки: `<div>` с фоном, тенью, скруглением и внутренними
21
- отступами. Собственной логики не имеет — задаёт поверхность, на которой лежит контент, и
22
- вертикальные отступы между своими частями `Island.Header`, `Island.Body`, `Island.Footer`.
21
+ отступами. Собственной логики не имеет — задаёт поверхность, на которой лежит контент,
22
+ вертикальные отступы между своими частями `Island.Header`, `Island.Body`, `Island.Footer` и
23
+ состояние загрузки (`loading`), в котором поверх контента показывается `LoaderScreen`.
23
24
 
24
25
  Используй когда: контент нужно визуально выделить карточкой на фоне страницы — блок страницы
25
26
  (`BodyPage`, `HeaderPage`, `FooterPage`), диалог подтверждения (`Confirm`), элемент аккордеона
@@ -46,10 +47,39 @@ version: "1.0"
46
47
  |---|---|---|---|
47
48
  | `type` | `EIslandType` | `EIslandType.TYPE_1` | Визуальный тип. Влияет только на фон и тень, на размеры — нет |
48
49
  | `size` | `EComponentSize` | `EComponentSize.MD` | Скругление, внутренние отступы карточки и отступы между Header / Body / Footer |
50
+ | `loading` | `boolean` | `false` | Показывает `LoaderScreen` поверх контента карточки |
51
+ | `loaderScreenProps` | `ILoaderScreenMiddleProps` | `undefined` | Props лоадера: `description`, `controls`, `className` и остальные атрибуты `<div>`. `type` задаёт Island (`middle`) |
49
52
 
50
53
  Компонент расширяет `React.HTMLAttributes<HTMLDivElement>` — все стандартные атрибуты `<div>`
51
54
  (`className`, `style`, `role`, `aria-*`, `data-*`, обработчики событий) попадают на корневой элемент.
52
55
 
56
+ ### Состояние загрузки
57
+
58
+ При `loading` последним потомком карточки рендерится `<LoaderScreen type="middle">` — абсолютно
59
+ спозиционированный по всей карточке (`inset: 0` от padding-box, то есть включая внутренние отступы)
60
+ блок с полупрозрачной подложкой и спиннером по центру.
61
+
62
+ - **Контент остаётся в DOM** — он не размонтируется, только перекрывается подложкой. Форма под
63
+ лоадером остаётся заполненной и фокусируемой: лоадер не блокирует клавиатуру и не ставит
64
+ `inert`/`aria-hidden`. Если ввод нужно запретить, потребитель сам дизейблит контролы.
65
+ - **Скругление подложки — `border-radius: inherit`**, поэтому лоадер повторяет скругление карточки
66
+ при любом `size` и в адаптиве. Дублировать значения по размерам не нужно.
67
+ - **`z-index` лоадера внутри Island — `@z-index-step` (100), а не `@z-index-loader-screen` (10100)**
68
+ из его собственных стилей: правило `.island .islandLoaderScreen` перебивает базовое по
69
+ специфичности. Подложка перекрывает контент карточки, но не глобальные оверлеи (`Dropdown`,
70
+ `ModalWindow`, `Tooltip`), лежащие над ней. Тот же приём — у `List` и `TableBasic`.
71
+ - **Лоадер идёт после `Island.Footer`**, поэтому селекторы отступов между частями
72
+ (`.islandBody + .islandFooter` и т.п.) он не ломает.
73
+ - **Карточка не должна скроллиться.** Лоадер позиционируется абсолютно и растягивается по padding-box
74
+ карточки. Если потребитель задал Island `overflow: auto`/`scroll`, подложка окажется в
75
+ scrollable-overflow: она перекроет только область, видимую в начале прокрутки, и уедет вверх
76
+ вместе с контентом. Для скроллируемой области состояние загрузки вешают на нескроллируемого
77
+ родителя.
78
+ - **В `ModalWindowBody` этих props нет** — `IModalWindowBodyProps` объявлен как
79
+ `Omit<IIslandProps, "loading" | "loaderScreenProps">` ровно по причине выше: тело модалки
80
+ скроллируется (`overflow: auto`). Состояние загрузки модального окна задаётся одноимёнными props
81
+ `ModalWindowContent` — он не скроллится и перекрывает окно целиком.
82
+
53
83
  ### Размеры
54
84
 
55
85
  | `size` | `border-radius` | `padding` | Отступ между частями | Адаптив (`max-width: @screen-sm-max`) |
@@ -142,6 +172,10 @@ Island — неинтерактивный контейнер: не выстав
142
172
  `role="dialog"` и `aria-modal="true"`, `IslandAccordion` кладёт внутрь `Island.Header` собственную
143
173
  `<button>` с `aria-expanded` / `aria-controls`.
144
174
 
175
+ Состояние `loading` тоже не объявляется автоматически: `LoaderScreen` приносит внутрь только
176
+ `role="status"` от `LoaderMiddle`. Если загрузку карточки должен озвучивать скринридер, потребитель
177
+ передаёт `aria-busy="true"` в `Island` — как и у `Button` с prop `loading`.
178
+
145
179
  Важно: `Island.Header` — это `<div>`, а не заголовок. Если нужен семантический заголовок карточки,
146
180
  положи внутрь `Title` с нужным тегом или собственный `<h*>`. Текстовые строки компонент не
147
181
  хардкодит (библиотека мультиязычная).
@@ -150,21 +184,50 @@ Island — неинтерактивный контейнер: не выстав
150
184
 
151
185
  ## Связанные компоненты
152
186
 
187
+ ### Альтернативы (в `related`)
188
+
189
+ - `IslandAccordion` — раскрывающиеся элементы. Строит на Island; `IslandAccordionFooter`
190
+ оборачивает `Island.Footer` и передаёт в него свой `className`. Опирается на хрупкую деталь:
191
+ гасит отступ Island через `margin-top: 0 !important`, поэтому правка селекторов отступов ломает
192
+ его молча.
193
+ - `IslandWidget` — виджет со сворачиванием в адаптиве; использует Island как контейнер, но своими
194
+ Header / Body / Footer, а не частями Island.
195
+
196
+ ### Контракты (в `related`)
197
+
198
+ - `LoaderScreen` — экран загрузки, который Island рендерит при `loading` (всегда `type="middle"`).
199
+ Настраивается через `loaderScreenProps` (`ILoaderScreenMiddleProps`).
200
+ - `ModalWindow` — `IModalWindowBodyProps` наследует `IIslandProps` через
201
+ `Omit<IIslandProps, "loading" | "loaderScreenProps">`, поэтому `type` и `size` в
202
+ `ModalWindowBody` доступны, а состояние загрузки — нет: тело модалки скроллируется, лоадер
203
+ задаётся на `ModalWindowContent`. Своего AI.md у `ModalWindowBody` нет — он описан в
204
+ `ModalWindow-ai.md`. Он же — направление из «Не используй когда», если нужен overlay, а не
205
+ карточка в потоке.
206
+
207
+ ### Другие направления из «Не используй когда» (в `related` не входят)
208
+
209
+ Отводы в другой класс задач — промах такого масштаба виден из «Назначения», ссылка не нужна.
210
+
211
+ - `LightBox`, `LightBoxSideOverlay` — если нужен overlay, а не карточка в потоке. У
212
+ `LightBoxSideOverlay` своего AI.md нет, смотри `LightBox`.
213
+ - `Gap`, `Row`, `Col` — если нужен только отступ или сетка без визуальной карточки.
214
+
215
+ ### Части Island (своего AI.md не имеют)
216
+
153
217
  - `IslandHeader` (`components/IslandHeader.tsx`, он же `Island.Header`) — шапка карточки.
154
- Тривиальная `<div>`-обёртка без собственных props; отдельного AI.md не имеет.
218
+ Тривиальная `<div>`-обёртка без собственных props.
155
219
  - `IslandBody` (`components/IslandBody.tsx`, он же `Island.Body`) — основное содержимое.
156
220
  Тривиальная `<div>`-обёртка без собственных props.
157
221
  - `IslandFooter` (`components/IslandFooter.tsx`, он же `Island.Footer`) — подвал карточки.
158
222
  Тривиальная `<div>`-обёртка без собственных props.
159
- - `IslandAccordion` — строит на Island раскрывающиеся элементы; `IslandAccordionFooter`
160
- оборачивает `Island.Footer` и передаёт в него свой `className`.
161
- - `IslandWidget` — виджет со сворачиванием в адаптиве; использует Island как контейнер, но своими
162
- Header / Body / Footer, а не частями Island.
223
+
224
+ ### Потребители (в `related` не входят — ссылка стоит с их стороны)
225
+
163
226
  - `BodyPage`, `HeaderPage`, `FooterPage` — оборачивают контент страницы в Island с `TYPE_1`, если
164
227
  их тип `FIRST`.
165
- - `Confirm` — диалог подтверждения на Island `TYPE_1` с `Island.Body` внутри.
166
- - `ModalWindowBody` — тело модального окна; `IModalWindowBodyProps` расширяет `IIslandProps`,
167
- поэтому `type` и `size` Island доступны и там.
228
+ - `Confirm` — диалог подтверждения на Island `TYPE_1` с `Island.Body` внутри. Наследует интерфейс:
229
+ `IConfirmProps extends IIslandProps` (`Confirm/Confirm.tsx:11`), поэтому добавление prop в Island
230
+ меняет публичный API `Confirm` без правок в его коде.
168
231
 
169
232
  ---
170
233
 
@@ -175,10 +238,14 @@ Island — неинтерактивный контейнер: не выстав
175
238
 
176
239
  | Story | Example file | Что демонстрирует |
177
240
  |---|---|---|
178
- | `Playground` | — | Интерактивный контроль `type`, `size` и состава (Header / Body / Footer) |
241
+ | `Playground` | — | Интерактивный контроль `type`, `size`, `loading` и состава (Header / Body / Footer) |
179
242
  | `Default` | `DefaultExample.tsx` | Минимальная карточка: Header + Body + Footer |
180
243
  | `Types` | `TypesExample.tsx` | Все значения `EIslandType` рядом |
181
244
  | `Sizes` | `SizesExample.tsx` | Размеры SM / MD / LG: скругление, паддинги, отступы между частями |
245
+ | `Loading` | `Loading.tsx` | `loading`: `LoaderScreen` поверх контента карточки |
246
+
247
+ Файлы примеров с постфиксом `Example` — локальный legacy-паттерн папки; новые примеры называются
248
+ по имени story (`Loading.tsx`).
182
249
 
183
250
  ---
184
251
 
@@ -186,6 +253,8 @@ Island — неинтерактивный контейнер: не выстав
186
253
 
187
254
  | Дата | Изменение |
188
255
  |---|---|
256
+ | 2026-08-11 | Prop состояния загрузки переименован `isLoading` → `loading` — по преобладающей в библиотеке конвенции boolean-props без префикса `is` (`loading` у `Button`, `List`, `Dropdown`, `Suggest`, `Table`). Prop ещё не выпускался, поэтому breaking change нет; `Omit` в `IModalWindowBodyProps` обновлён на `"loading"`. Поведение не изменилось. |
257
+ | 2026-08-06 | `related` приведён к правилу `docs/ai/CONTEXT.md` → «Как заполнять `related` в AI.md»: убраны потребители (`BodyPage`, `HeaderPage`, `FooterPage`, `Confirm`), `ModalWindowBody` заменён родителем `ModalWindow`, добавлен `LoaderScreen`. Раздел «Связанные компоненты» разбит по типам связи. Добавлено состояние загрузки: props `isLoading` и `loaderScreenProps` — при `isLoading` поверх контента карточки рендерится `LoaderScreen` типа `middle`. Подложка наследует скругление карточки, `z-index` понижен до локального. `ModalWindowBody` эти props не наследует (`Omit`) — его тело скроллируется, лоадер там задаётся на `ModalWindowContent`. Добавлены story `Loading` и unit-тесты. Остальной публичный API не изменён. |
189
258
  | 2026-08-04 | Создан документ. AI-рефакторинг: JSDoc на props, значениях `EIslandType` и субкомпонентах, `displayName` у `IslandHeader` / `IslandBody` / `IslandFooter`, unit-тесты на все типы и размеры, части острова и `mapTypeToClassName`. Исправлено: `className` в `IslandHeader` и `IslandFooter` больше не затирает базовый класс компонента; побочный отступ в `IslandAccordion` погашен в стилях аккордеона, вид библиотечных компонентов не изменился. Публичный API не изменён. |
190
259
 
191
260
  ---
@@ -287,3 +356,22 @@ export const SizesExample = () => (
287
356
  );
288
357
  ```
289
358
 
359
+ ### Loading
360
+
361
+ `stories/Island/examples/Loading.tsx`
362
+
363
+ ```tsx
364
+ import React from "react";
365
+ import { Island, EIslandType, EComponentSize } from "@sberbusiness/triplex-next";
366
+
367
+ export const Loading = () => (
368
+ <div style={{ maxWidth: 360 }}>
369
+ <Island type={EIslandType.TYPE_1} size={EComponentSize.MD} loading>
370
+ <Island.Header>Island Header</Island.Header>
371
+ <Island.Body>Island Body</Island.Body>
372
+ <Island.Footer>Island Footer</Island.Footer>
373
+ </Island>
374
+ </div>
375
+ );
376
+ ```
377
+