@sberbusiness/triplex-next-mcp-server 1.40.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.
Files changed (36) hide show
  1. package/bundle/components/AlertProcess.md +460 -0
  2. package/bundle/components/Amount.md +145 -169
  3. package/bundle/components/Badge.md +155 -219
  4. package/bundle/components/Body.md +108 -116
  5. package/bundle/components/Calendar.md +183 -368
  6. package/bundle/components/CarouselExtended.md +355 -194
  7. package/bundle/components/Chip.md +310 -314
  8. package/bundle/components/ComposedKeyDownListener.md +323 -0
  9. package/bundle/components/FooterPage.md +2 -1
  10. package/bundle/components/FormField.md +468 -232
  11. package/bundle/components/HeaderPage.md +2 -1
  12. package/bundle/components/IconWrapper.md +142 -89
  13. package/bundle/components/Island.md +303 -123
  14. package/bundle/components/KeyDownListener.md +152 -100
  15. package/bundle/components/LoaderMiddle.md +265 -0
  16. package/bundle/components/LoaderScreen.md +178 -81
  17. package/bundle/components/LoaderSmall.md +230 -0
  18. package/bundle/components/MaskedField.md +481 -0
  19. package/bundle/components/MediaWidth.md +246 -108
  20. package/bundle/components/MobileView.md +192 -68
  21. package/bundle/components/ModalWindow.md +3 -1
  22. package/bundle/components/Page.md +3 -1
  23. package/bundle/components/Pagination.md +20 -0
  24. package/bundle/components/SelectExtendedField.md +691 -236
  25. package/bundle/components/ThemeProvider.md +305 -117
  26. package/bundle/components/Tooltip.md +421 -181
  27. package/bundle/guides/commits.md +54 -9
  28. package/bundle/guides/context.md +26 -0
  29. package/bundle/guides/stories.md +2 -0
  30. package/bundle/guides/template.md +1 -1
  31. package/bundle/manifest.json +63 -18
  32. package/bundle/release-notes/1.41.0.mdx +76 -0
  33. package/bundle/release-notes/1.41.1.mdx +18 -0
  34. package/bundle/release-notes/1.42.0.mdx +88 -0
  35. package/bundle/release-notes/1.43.0.mdx +7 -0
  36. package/package.json +1 -1
@@ -1,11 +1,9 @@
1
1
  ---
2
2
  component: KeyDownListener
3
- category: Utilities
4
- figma-node: ""
5
- figma-file: ""
6
- related: [ComposedKeyDownListener, WindowResizeListener]
3
+ category: KeyDownListener
4
+ related: [ComposedKeyDownListener, TriggerClickOnKeyDownEvent, SelectExtendedField, SliderExtended]
7
5
  tokens: []
8
- stories: ""
6
+ stories: stories/KeyDownListener/KeyDownListener.stories.tsx
9
7
  version: "1.0"
10
8
  ---
11
9
 
@@ -13,145 +11,199 @@ version: "1.0"
13
11
 
14
12
  ## Назначение
15
13
 
16
- Невизуальный компонент-слушатель `keydown` на `window`. При совпадении
17
- кода нажатой клавиши с `eventKeyCode` (одно значение или массив) вызывает
18
- `onMatch(event)`. Рендерит `children` как есть (или `null`) — не добавляет
19
- обёртку в DOM.
14
+ Сервисный headless-компонент. Пока смонтирован, слушает `keydown` на `window` и
15
+ вызывает `onMatch`, если числовой код нажатой клавиши (`event.keyCode`) совпал с
16
+ `eventKeyCode`. Собственной разметки не рендерит: `children` выводятся как есть,
17
+ без обёртки; без `children` не рендерит ничего.
20
18
 
21
- Используй когда: нужно отреагировать на глобальное нажатие клавиш
22
- (Escape для закрытия модалки, Tab/ArrowKey для навигации в кастомных
23
- виджетах), но не привязываться к конкретному focus-target'у.
19
+ Используй когда: нужна глобальная (документная) горячая клавиша, работающая
20
+ независимо от того, где находится фокус — закрытие оверлея по `Esc`, стрелки для
21
+ управления слайдером/ползунком, подтверждение по `Enter`.
24
22
 
25
- Для нескольких параллельных слушателей используй `ComposedKeyDownListener` —
26
- принимает массив конфигов и композитит их в иерархию.
23
+ Не используй когда:
24
+ - Клавиша должна обрабатываться только внутри конкретного элемента — навесь
25
+ `onKeyDown` на сам элемент, иначе обработчик сработает при вводе в другое поле
26
+ на странице.
27
+ - Нужно кликнуть по конкретной кнопке по нажатию клавиши — есть готовый
28
+ `TriggerClickOnKeyDownEvent`.
29
+ - Нужно несколько разных клавиш с разными обработчиками — используй
30
+ `ComposedKeyDownListener` (см. «Связанные компоненты»).
27
31
 
28
32
  ---
29
33
 
30
- ## Файловая структура
31
-
32
- ```text
33
- src/components/KeyDownListener/
34
- ├── KeyDownListener.tsx # React.Component (class) с window-keydown
35
- ├── ComposedKeyDownListener.tsx # FC, сворачивает массив listener'ов в tree
36
- └── index.ts # Barrel: оба компонента
37
- ```
38
-
39
- Нет styles, stories, tests.
34
+ ## Варианты и props
40
35
 
41
- ---
36
+ ### Обязательные props
42
37
 
43
- ## Варианты и props
38
+ | Prop | Тип | Описание |
39
+ |---|---|---|
40
+ | `eventKeyCode` | `number \| number[]` | Код клавиши из `EVENT_KEY_CODES` или массив кодов. Массив — «любая из перечисленных клавиш»; различить нажатую можно по `event.keyCode` внутри `onMatch` |
41
+ | `onMatch` | `(event: KeyboardEvent) => void` | Вызывается при совпадении. Получает нативный `KeyboardEvent` (не React SyntheticEvent) |
44
42
 
45
- ### KeyDownListener — `IKeyDownListenerProps`
43
+ ### Опциональные props
46
44
 
47
- | Prop | Тип | Обязателен | Описание |
45
+ | Prop | Тип | По умолчанию | Описание |
48
46
  |---|---|---|---|
49
- | `eventKeyCode` | `number \| number[]` | Да | Код клавиши (число — это `KeyboardEvent.keyCode`, не `key`). Один код или массив. |
50
- | `onMatch` | `(event: KeyboardEvent) => void` | Да | Вызывается при совпадении кода. |
51
- | `children` | `React.ReactNode` | Нет | Рендерятся "прозрачно" (компонент не оборачивает в DOM). |
47
+ | `children` | `React.ReactNode` | — | Рендерится как есть. Компонент не оборачивает содержимое и никак на него не влияет — вложение нужно только для читаемости кода |
48
+
49
+ ### Особенности поведения
50
+
51
+ - Слушатель навешивается на `window` в `componentDidMount` и снимается в
52
+ `componentWillUnmount`. Компонент не вызывает `preventDefault` и не
53
+ останавливает всплытие — это ответственность `onMatch`.
54
+ - Сравнение идёт по устаревшему `event.keyCode`, а не по `event.key`. Коды
55
+ берутся из `EVENT_KEY_CODES` (`@sberbusiness/triplex-next`); не хардкодь числа.
56
+ - `onMatch` и `eventKeyCode` читаются из `this.props` в момент события, поэтому
57
+ свежие значения после rerender применяются без переподписки.
58
+ - Несколько смонтированных `KeyDownListener` на одну клавишу срабатывают все —
59
+ порядок соответствует порядку монтирования.
60
+ - Компонент не проверяет, где находится фокус: `Esc` сработает и во время ввода
61
+ в поле. Если это нежелательно, фильтруй в `onMatch` по `event.target`.
52
62
 
53
- Тип кода: `keyCode = (typeof EVENT_KEY_CODES)[keyof typeof EVENT_KEY_CODES]`
54
- (числа из `@sberbusiness/triplex-next/utils/keyboard`): `ENTER=13`, `ESCAPE=27`,
55
- `TAB=9`, `ARROW_*=37-40`, и т.д.
63
+ ---
56
64
 
57
- ### ComposedKeyDownListener — `IKeyDownListenerManagerProps`
65
+ ## Дизайн-токены
58
66
 
59
- | Prop | Тип | Обязателен | Описание |
60
- |---|---|---|---|
61
- | `keyDownListeners` | `IKeyDownListenerProps[]` | Да | Массив конфигов. Для каждого создаётся свой `KeyDownListener`. |
62
- | `children` | `React.ReactNode` | Нет | Обёрнуты всеми listener'ами в иерархию (последний в массиве — самый внешний). |
67
+ Собственных стилей и CSS-переменных нет — компонент не рендерит разметку.
63
68
 
64
69
  ---
65
70
 
66
- ## Ключевые особенности реализации
67
-
68
- ### `KeyDownListener` — class component
71
+ ## Инварианты
69
72
 
70
- ```tsx
71
- export class KeyDownListener extends React.Component<IKeyDownListenerProps> {
72
- public componentDidMount() { window.addEventListener("keydown", this.handleKeyDown); }
73
- public componentWillUnmount() { window.removeEventListener("keydown", this.handleKeyDown); }
74
- public render() { return this.props.children || null; }
75
- }
76
- ```
73
+ - **`KeyDownListener` — class-компонент**, и это часть публичного контракта:
74
+ `ref` указывает на инстанс класса, метод `handleKeyDown` публичный. Не
75
+ переписывать на функциональный компонент.
76
+ - **Без `forwardRef` — осознанное исключение** из общего правила библиотеки:
77
+ компонент не рендерит собственный host-элемент, форвардить ref некуда.
78
+ - **Имена props `children`, `eventKeyCode`, `onMatch`** и интерфейс
79
+ `IKeyDownListenerProps` (экспортируется из barrel) — публичное API, не менять.
80
+ `IKeyDownListenerProps` переиспользуется в `TriggerClickOnKeyDownEvent`
81
+ (`Pick<IKeyDownListenerProps, "eventKeyCode">`) и в props
82
+ `ComposedKeyDownListener`.
83
+ - **Сравнение по `event.keyCode`** — не мигрировать на `event.key` без отдельной
84
+ задачи: у `EVENT_KEYS.SPACE` значения `["Spacebar", "Space"]` не совпадают с
85
+ современным `event.key === " "`, и миграция изменит наблюдаемое поведение у
86
+ потребителей.
87
+ - **`render` возвращает `this.props.children || null`** — falsy-`children`
88
+ (`0`, `""`) осознанно дают `null`. Не заменять на `??`.
89
+ - Реализация остаётся React 17-совместимой (класс, без хуков и React 18-only
90
+ API): код синхронизируется в `release-0`.
77
91
 
78
- - **Сохранён как class**, хотя весь остальной код библиотеки — функциональные компоненты. Миграция на `useEffect` возможна, но class сохраняет идентичность listener'а через reference handler'а — аналог можно сделать через `useEffect` + стабильный handler (через ref на `onMatch`).
79
- - Не `forwardRef` — компонент не имеет своего DOM.
92
+ ---
80
93
 
81
- ### Использование `keyCode`, а не `key` / `code`
94
+ ## Accessibility
82
95
 
83
- ```ts
84
- if (typeof eventKeyCode === "number") {
85
- eventKeyCode === event.keyCode && onMatch(event);
86
- } else if (eventKeyCode.includes(event.keyCode)) {
87
- onMatch(event);
88
- }
89
- ```
96
+ - Собственной ARIA-семантики нет — компонент прозрачен для accessibility-дерева
97
+ и не рендерит фокусируемых элементов.
98
+ - Горячая клавиша глобальная: она перехватывается независимо от позиции фокуса и
99
+ не анонсируется screen reader'ом. Если клавиша дублирует видимое действие,
100
+ оставь пользователю и обычный путь (кнопку) — клавиатурный шорткат не должен
101
+ быть единственным способом выполнить действие.
102
+ - `Esc` для закрытия оверлеев — ожидаемое поведение; возврат фокуса на триггер
103
+ после закрытия компонент не делает, это ответственность потребителя.
90
104
 
91
- - `event.keyCode` — **deprecated** в современном вебе (замена: `event.key` или `event.code`). Компонент и набор `EVENT_KEY_CODES` сохранили legacy-подход.
92
- - Числовые константы (13, 27, 40, ...) должны использоваться через `EVENT_KEY_CODES.ENTER` / `.ESCAPE` и т.д., не литерально.
93
- - Для новых компонентов предпочитать `isKey(event.key, "ENTER")` из `utils/keyboard` — она поддерживает `key`-strings и legacy-fallback.
105
+ ---
94
106
 
95
- ### `onMatch` не кешируется в class state
107
+ ## Связанные компоненты
96
108
 
97
- Каждый keydown вытаскивает `this.props.onMatch` актуальный — замыкание не устаревает. При миграции на hooks это поведение нужно воспроизвести через ref.
109
+ ### `ComposedKeyDownListener` (`src/components/KeyDownListener/ComposedKeyDownListener.tsx`)
98
110
 
99
- ### `ComposedKeyDownListener` — reduce-вариант через цикл
111
+ Композитор: рендерит по одному `KeyDownListener` на элемент `keyDownListeners`.
112
+ Используй, когда нужно несколько разных клавиш с разными обработчиками.
113
+ Документация — `src/components/KeyDownListener/ComposedKeyDownListener-ai.md`.
100
114
 
101
- ```tsx
102
- let accumulatedElement = children as JSX.Element;
103
- for (let i = 0; i < keyDownListeners.length; i++) {
104
- accumulatedElement = <KeyDownListener {...}>{accumulatedElement}</KeyDownListener>;
105
- }
106
- ```
115
+ ### Потребители внутри библиотеки
107
116
 
108
- - Последний listener в массиве → самый внешний wrapper в tree.
109
- - Порядок не влияет на результат (все listener'ы слушают `window` независимо), но Reactу важен для reconciliation — не менять порядок без нужды.
110
- - **Внимание:** ключи не проставляются. При рендере одного и того же конфига это ок, но при динамическом изменении массива React может ломать состояние child'ов — используй stable ordering.
117
+ - `TriggerClickOnKeyDownEvent` (`src/components/Triggers/`) — по нажатию клавиши
118
+ вызывает `click` на элементе из `targetRef`.
119
+ - `SelectExtendedField` — закрывает выпадающий список по `Esc`.
120
+ - `SliderExtendedDot`, `SliderExtendedTrack` — стрелки для перемещения ползунка,
121
+ слушатели монтируются только когда элемент в фокусе.
111
122
 
112
123
  ---
113
124
 
114
- ## Инварианты
125
+ ## Stories
115
126
 
116
- - **`eventKeyCode` — `number | number[]`** (не `string`/`KeyboardEvent["key"]`). Публичный контракт. При смене на `key`-строки — breaking change.
117
- - **`onMatch(event: KeyboardEvent)`** — получает нативное событие, не синтетическое React-событие. Не менять тип на `React.KeyboardEvent`.
118
- - **Class component** — сохранить при рефакторинге; миграция на FC допустима, но должна воспроизвести `componentWillUnmount`-очистку и актуальный `onMatch`.
119
- - **`window.addEventListener`** — listener на окне, не на document. Не менять без согласования (влияет на порядок обработки событий с портальными элементами).
120
- - **`children` рендерятся без обёртки** — компонент "прозрачный" в DOM. Не добавлять `<div>`/`<Fragment>` без нужды.
121
- - **`ComposedKeyDownListener`** — простой композитор. Не усложнять (например, не добавлять `useMemo`/контексты) без реальной нужды.
127
+ Основные истории: `stories/KeyDownListener/KeyDownListener.stories.tsx`
128
+ Файлы примеров: `stories/KeyDownListener/examples/`
122
129
 
123
- ---
130
+ | Story | Example file | Что демонстрирует |
131
+ |---|---|---|
132
+ | `Playground` | `Playground.tsx` | Интерактивный выбор `eventKeyCode` и счётчик срабатываний `onMatch` |
133
+ | `Default` | `Default.tsx` | Слушатель одной клавиши: панель скрывается по `Esc` |
134
+ | `WithMultipleKeys` | `WithMultipleKeys.tsx` | Массив кодов в `eventKeyCode`; нажатая клавиша различается по `event.keyCode` |
124
135
 
125
- ## Accessibility
136
+ Набор целиком исключён из скриншот-тестов (`testRunner: { skip: true }` в `meta`):
137
+ собственной разметки компонент не рендерит, на скриншот попадала бы только
138
+ обвязка примеров. Поведение слушателя покрыто unit-тестами.
126
139
 
127
- - Компонент работает на уровне окна, перехватывает keydown до того, как фокусируемый элемент получит шанс обработать — при использовании **в составе интерактивных виджетов** учитывай, что нативный `onKeyDown` на inputе/кнопке сработает **после** глобального listener'а (из-за bubbling), но глобальный listener активен всегда, даже если фокус в другом месте.
128
- - Не подменяй focus management'ом. Для Escape в модалках предпочитай `focus trap` + `onKeyDown` на overlay'е.
129
- - Listener активен даже в скрытом state компонента-родителя, если сам KeyDownListener не размонтирован. Контролируй монтирование через условный рендеринг.
140
+ Примеры `ComposedKeyDownListener` живут в собственном наборе stories —
141
+ `stories/ComposedKeyDownListener/`.
130
142
 
131
143
  ---
132
144
 
133
- ## Дизайн-токены
145
+ ## История изменений
134
146
 
135
- Нет.
147
+ | Дата | Изменение |
148
+ |---|---|
149
+ | 2026-08-11 | TRI-112: у `ComposedKeyDownListener` появились собственный `ComposedKeyDownListener-ai.md` и набор stories `stories/ComposedKeyDownListener/`; раздел «Связанные компоненты» сокращён до ссылки, story `Example` (демонстрация композитора) перенесена в новый набор. Ограничение на стабильную длину `keyDownListeners` снято — композитор рендерит слушателей соседями к `children`. Набор stories `KeyDownListener` исключён из скриншот-тестов, story `VisualTests` и baseline удалены — визуального интерфейса у компонента нет. Реализация самого `KeyDownListener` не менялась. |
150
+ | 2026-08-04 | По ревью PR #502: интерфейс props `IComposedKeyDownListenerProps` стал экспортируемым — публичная поверхность пакета расширена аддитивно. `ComposedKeyDownListener` добавлен отдельной строкой в `docs/ai/ROADMAP.md`, собственный AI.md вынесен в отдельную задачу. |
151
+ | 2026-08-04 | Создан документ. AI-рефакторинг (TRI-48): JSDoc на props `IKeyDownListenerProps` и `ComposedKeyDownListener`, ветвление в `handleKeyDown` сведено к одной проверке (снят `eslint-disable`), в `ComposedKeyDownListener` убран `as JSX.Element` и добавлен `displayName`, unit-тесты (16 кейсов на оба компонента), stories по modern pattern. Публичный API не менялся; компонент остаётся class-компонентом, `forwardRef` осознанно отсутствует. |
136
152
 
137
153
  ---
138
154
 
139
- ## Связанные компоненты
155
+ ## Примеры использования
140
156
 
141
- - `ComposedKeyDownListener` — обёртка для нескольких listener'ов.
142
- - `WindowResizeListener` — аналогичный паттерн для resize.
143
- - `@sberbusiness/triplex-next/utils/keyboard` — `EVENT_KEY_CODES`, `EVENT_KEYS`, `isKey`.
157
+ ### Default
144
158
 
145
- ---
159
+ `stories/KeyDownListener/examples/Default.tsx`
146
160
 
147
- ## Stories
161
+ ```tsx
162
+ import React, { useState } from "react";
163
+ import { KeyDownListener, EVENT_KEY_CODES } from "@sberbusiness/triplex-next";
164
+
165
+ export const Default = () => {
166
+ const [visible, setVisible] = useState(true);
167
+
168
+ return (
169
+ <div style={{ display: "flex", flexDirection: "column", gap: "16px", maxWidth: "400px" }}>
170
+ <KeyDownListener eventKeyCode={EVENT_KEY_CODES.ESCAPE} onMatch={() => setVisible(false)} />
171
+ <div>Нажмите Esc, чтобы скрыть панель.</div>
172
+ {visible && (
173
+ <div style={{ border: "1px dashed rgb(125, 131, 138)", borderRadius: "4px", padding: "16px" }}>
174
+ Панель скрывается по нажатию Esc в любом месте страницы.
175
+ </div>
176
+ )}
177
+ </div>
178
+ );
179
+ };
180
+ ```
148
181
 
149
- Нет.
182
+ ### WithMultipleKeys
150
183
 
151
- ---
184
+ `stories/KeyDownListener/examples/WithMultipleKeys.tsx`
152
185
 
153
- ## История изменений
186
+ ```tsx
187
+ import React, { useState } from "react";
188
+ import { KeyDownListener, EVENT_KEY_CODES } from "@sberbusiness/triplex-next";
189
+
190
+ export const WithMultipleKeys = () => {
191
+ const [value, setValue] = useState(0);
192
+
193
+ const handleMatch = (event: KeyboardEvent) => {
194
+ setValue((prevValue) => (event.keyCode === EVENT_KEY_CODES.ARROW_LEFT ? prevValue - 1 : prevValue + 1));
195
+ };
196
+
197
+ return (
198
+ <KeyDownListener eventKeyCode={[EVENT_KEY_CODES.ARROW_LEFT, EVENT_KEY_CODES.ARROW_RIGHT]} onMatch={handleMatch}>
199
+ <div style={{ display: "flex", flexDirection: "column", gap: "16px", maxWidth: "400px" }}>
200
+ <div>Один слушатель реагирует на несколько клавиш: стрелка влево и стрелка вправо.</div>
201
+ <div style={{ border: "1px dashed rgb(125, 131, 138)", borderRadius: "4px", padding: "16px" }}>
202
+ Значение: {value}
203
+ </div>
204
+ </div>
205
+ </KeyDownListener>
206
+ );
207
+ };
208
+ ```
154
209
 
155
- | Дата | Изменение |
156
- |---|---|
157
- | 2026-04-18 | Создан документ |
@@ -0,0 +1,265 @@
1
+ ---
2
+ component: LoaderMiddle
3
+ category: Loaders
4
+ related: [LoaderSmall, LoaderScreen, TableBasic]
5
+ tokens:
6
+ - --triplex-next-Loader-Middle_Background_Default
7
+ - --triplex-next-Loader-Element_Background_Brand
8
+ stories: stories/Loaders/LoaderMiddle/LoaderMiddle.stories.tsx
9
+ version: "1.0"
10
+ ---
11
+
12
+ # LoaderMiddle
13
+
14
+ ## Назначение
15
+
16
+ Квадратный индикатор загрузки фиксированного размера 64×64 px: четыре точки по углам
17
+ внутренней области и линия, обегающая их по периметру. Все состояния анимации задаются CSS —
18
+ у компонента нет ни props, ни внутреннего состояния.
19
+
20
+ Используй когда: нужно показать, что грузится целая область или страница, а место под
21
+ индикатор заведомо есть (плитка, пустая таблица, оверлей поверх контента).
22
+
23
+ Не используй когда:
24
+ - Индикатор должен встраиваться в строку текста, кнопку или узкий элемент управления —
25
+ возьми `LoaderSmall` (горизонтальный, с темами и размерами `SM` / `MD` / `LG`).
26
+ - Нужен индикатор с подложкой, подписью и кнопками отмены — возьми `LoaderScreen`
27
+ (`type="middle"` рендерит внутри себя именно `LoaderMiddle`).
28
+ - Нужно управлять размером, цветом или темой — LoaderMiddle не настраивается, см. «Инварианты».
29
+
30
+ ---
31
+
32
+ ## Варианты и props
33
+
34
+ У компонента **нет props**. Публичный тип — `React.FC` без параметров: `<LoaderMiddle />` —
35
+ единственная допустимая форма вызова.
36
+
37
+ Следствия, важные при интеграции:
38
+
39
+ - `className` и произвольные HTML-атрибуты **не принимаются** — обернуть в свой контейнер и
40
+ стилизовать его. Так поступает `WithAnimationExample` в stories: сбрасывает фон через
41
+ внешний класс-обёртку, а не через prop.
42
+ - `ref` на корневой `<div>` **не пробрасывается** — компонент не обёрнут в `forwardRef`.
43
+ Если нужен доступ к DOM, измеряй элемент-обёртку.
44
+ - Размер (64×64), геометрия точек (12×12) и тайминги анимаций (цикл 3 s) заданы в
45
+ `styles/LoaderMiddle.module.less` и снаружи не настраиваются.
46
+
47
+ ---
48
+
49
+ ## Дизайн-токены
50
+
51
+ ```text
52
+ --triplex-next-Loader-Middle_Background_Default
53
+ --triplex-next-Loader-Element_Background_Brand
54
+ ```
55
+
56
+ - `Middle_Background_Default` — фон квадратной подложки (`ColorNeutral.80` в светлой теме,
57
+ `ColorDarkNeutral.50` в тёмной).
58
+ - `Element_Background_Brand` — цвет точек и обегающей линии.
59
+
60
+ Оба токена объявлены в `src/components/DesignTokens/components/Loader.ts` и общие для
61
+ семейства Loader (`Element_Background_Brand` используется также в `LoaderSmall`).
62
+
63
+ ---
64
+
65
+ ## Инварианты
66
+
67
+ - **Публичный контракт — компонент без props.** Добавление `className`, `...rest` или обёртка
68
+ в `forwardRef` меняет публичный тип компонента и требует отдельной задачи и записи в release
69
+ notes. По той же схеме объявлен сосед `LoaderSmall` (`React.FC`, без `forwardRef`).
70
+ - **Корневой элемент — `<div role="status">`.** На эту связку опираются unit-тесты компонента и
71
+ тесты потребителей (`LoaderScreen.test.tsx`, `TableBasic.test.tsx`), которые ищут лоадер через
72
+ `getByRole("status", { name: "loading" })`. Имена CSS-классов снаружи не используются —
73
+ тесты потребителей намеренно опираются только на роль и доступное имя.
74
+ - **Разметка внутренностей фиксирована:** контейнер `.loaderMiddleDots`, один `.line` и ровно
75
+ четыре точки `.dot` с модификаторами `.dot1`–`.dot4`. Порядок и количество завязаны на
76
+ keyframes (`lineCycle`, `dotCycleMiddle`) и на задержки анимации; удаление или перестановка
77
+ ломает визуальную последовательность.
78
+ - **`displayName = "LoaderMiddle"`** — используется в React DevTools.
79
+ - **Токены `Loader-*` общие для семейства** — переименование ломает темизацию у потребителей и
80
+ затрагивает `LoaderSmall`.
81
+
82
+ ---
83
+
84
+ ## Accessibility
85
+
86
+ - Корневой элемент имеет `role="status"` — это неявная `aria-live="polite"` область: скринридер
87
+ сообщит о появлении лоадера, не прерывая текущее чтение.
88
+ - `aria-label="loading"` захардкожен в компоненте. Это отступление от общего правила библиотеки
89
+ «не хардкодить язык в aria-атрибутах», унаследованное от `LoaderSmall`; менять его нельзя без
90
+ согласования — строка входит в доступное имя, по которому лоадер находят тесты потребителей.
91
+ Локализация доступного имени — предмет отдельной задачи по всему семейству Loader.
92
+ - Компонент неинтерактивен: не получает фокус, не обрабатывает клавиатуру и не содержит текста.
93
+ Если загрузку нужно подписать для пользователя — используй `LoaderScreen` с `description`.
94
+ - Анимация непрерывная и не отключается по `prefers-reduced-motion` — известное ограничение
95
+ семейства Loader.
96
+
97
+ ---
98
+
99
+ ## Связанные компоненты
100
+
101
+ - `LoaderSmall` (`src/components/Loader/LoaderSmall/LoaderSmall.tsx`) — горизонтальный вариант из
102
+ трёх точек с props `theme` (`ELoaderSmallTheme`) и `size` (`EComponentSize`). Экспортируется из
103
+ того же barrel `src/components/Loader/index.ts`.
104
+ - `LoaderScreen` (`src/components/LoaderScreen/LoaderScreen.tsx`) — оверлей-подложка с лоадером по
105
+ центру, подписью и кнопками; при `type="middle"` рендерит `LoaderMiddle` без обёрток.
106
+ - `TableBasic` (`src/components/Table/TableBasic/TableBasic.tsx`) — показывает `LoaderMiddle` в
107
+ футере таблицы при догрузке данных на пустом состоянии.
108
+
109
+ ---
110
+
111
+ ## Stories
112
+
113
+ Основные истории: `stories/Loaders/LoaderMiddle/LoaderMiddle.stories.tsx`
114
+ Файлы примеров: `stories/Loaders/LoaderMiddle/examples/`
115
+
116
+ | Story | Example file | Что демонстрирует |
117
+ |---|---|---|
118
+ | `Default` | `DefaultExample.tsx` | Единственная форма вызова — `<LoaderMiddle />` без props |
119
+ | `WithAnimation` | `WithAnimationExample.tsx` | Замена лоадера на Lottie-анимацию результата (success / fail / warning / waiting) с подгонкой размера точек |
120
+ | `VisualTests` | — | Скриншот-регрессия, анимации заглушены декоратором |
121
+
122
+ Стори `Default` и `WithAnimation` исключены из скриншот-тестов (`testRunner: { skip: true }`):
123
+ первая визуально идентична `VisualTests`, вторая завязана на проигрывание Lottie и давала бы
124
+ нестабильный снимок. Playground не создаётся — у компонента нет настраиваемых props
125
+ (`docs/ai/stories-guide.md` → «Когда не создавать Playground»).
126
+
127
+ ---
128
+
129
+ ## История изменений
130
+
131
+ | Дата | Изменение |
132
+ |---|---|
133
+ | 2026-08-05 | Создан документ. AI-рефакторинг: JSDoc на компоненте, порядок импортов приведён к codestyle, в LESS объединены дублирующиеся базовые стили `.dot` и `.line`, unit-тесты расширены с 1 до 2 кейсов (роль корневого элемента, количество точек и линии). В stories `controls: { disable: true }` перенесён на верхний уровень `parameters` (внутри `docs` не применялся). Публичный API, DOM и визуальное поведение не изменены. |
134
+
135
+ ---
136
+
137
+ ## Примеры использования
138
+
139
+ ### Default
140
+
141
+ `stories/Loaders/LoaderMiddle/examples/DefaultExample.tsx`
142
+
143
+ ```tsx
144
+ import React from "react";
145
+ import { LoaderMiddle } from "@sberbusiness/triplex-next";
146
+
147
+ export const DefaultExample = () => <LoaderMiddle />;
148
+ ```
149
+
150
+ ### WithAnimation
151
+
152
+ `stories/Loaders/LoaderMiddle/examples/WithAnimationExample.tsx`
153
+
154
+ ```tsx
155
+ import React, { useState } from "react";
156
+ import { Player } from "@lottiefiles/react-lottie-player";
157
+ import { Button, EButtonTheme, EComponentSize, LoaderMiddle } from "@sberbusiness/triplex-next";
158
+ import "./WithAnimationExample.less";
159
+
160
+ const successSignAnimation = new URL("../lottie_animations/successSign.json", import.meta.url).href;
161
+ const failSignAnimation = new URL("../lottie_animations/failSign.json", import.meta.url).href;
162
+ const warningSignAnimation = new URL("../lottie_animations/warningSign.json", import.meta.url).href;
163
+ const waitingSignAnimation = new URL("../lottie_animations/waitingSign.json", import.meta.url).href;
164
+
165
+ /** Размер стороны LoaderMiddle */
166
+ const loaderMiddleSize = 64;
167
+ /** Диаметр dots в LoaderMiddle */
168
+ const loaderMiddleDotSize = 12;
169
+
170
+ /** Размер стороны Lottie */
171
+ const lottieSize = 85;
172
+ /** Диаметр dots в Lottie - изначальный диаметр 12×12, под transform scale [80,80] */
173
+ const lottieDotSize = 12 * 0.8;
174
+
175
+ /** Диаметр dots в Lottie после вписывания Lottie в LoaderMiddle */
176
+ const lottieDotSizeWhenFitted = (lottieDotSize / lottieSize) * loaderMiddleSize;
177
+ /** Масштаб dots в Lottie после вписывания Lottie в LoaderMiddle */
178
+ const lottieDotSizeScale = loaderMiddleDotSize / lottieDotSizeWhenFitted;
179
+
180
+ /** Целевой размер стороны Lottie, при котором dots в Lottie совпадают с dots в LoaderMiddle. */
181
+ const lottieViewportSize = loaderMiddleSize * lottieDotSizeScale;
182
+
183
+ type TStatus = "success" | "fail" | "warning" | "waiting";
184
+
185
+ const STATUS_TO_ANIMATION_MAP: Record<TStatus, string> = {
186
+ success: successSignAnimation,
187
+ fail: failSignAnimation,
188
+ warning: warningSignAnimation,
189
+ waiting: waitingSignAnimation,
190
+ };
191
+
192
+ export const WithAnimationExample = () => {
193
+ const [lottieStarted, setLottieStarted] = useState(false);
194
+ const [isLottiePlay, setIsLottiePlay] = useState(false);
195
+
196
+ const handleChange = () => {
197
+ if (lottieStarted) {
198
+ setLottieStarted(false);
199
+ setIsLottiePlay(false);
200
+ return;
201
+ }
202
+
203
+ setLottieStarted(true);
204
+ setIsLottiePlay(true);
205
+ };
206
+
207
+ const renderContent = (status: TStatus) => {
208
+ return (
209
+ <div
210
+ style={{
211
+ width: loaderMiddleSize,
212
+ height: loaderMiddleSize,
213
+ position: "relative",
214
+ margin: "32px 0 0 32px",
215
+ }}
216
+ >
217
+ {!lottieStarted ? (
218
+ <div className="withAnimationExample">
219
+ <LoaderMiddle />
220
+ </div>
221
+ ) : (
222
+ <div
223
+ style={{
224
+ position: "absolute",
225
+ top: "50%",
226
+ left: "50%",
227
+ width: lottieViewportSize,
228
+ height: lottieViewportSize,
229
+ transform: "translate(-50%, -50%)",
230
+ }}
231
+ >
232
+ <Player
233
+ autoplay={isLottiePlay}
234
+ loop={false}
235
+ keepLastFrame
236
+ src={STATUS_TO_ANIMATION_MAP[status]}
237
+ onEvent={(event) => {
238
+ if (event === "complete") {
239
+ setIsLottiePlay(false);
240
+ }
241
+ }}
242
+ />
243
+ </div>
244
+ )}
245
+ </div>
246
+ );
247
+ };
248
+
249
+ return (
250
+ <div style={{ display: "flex", flexDirection: "column", gap: 16, alignItems: "flex-start" }}>
251
+ <Button theme={EButtonTheme.GENERAL} size={EComponentSize.SM} onClick={handleChange}>
252
+ {lottieStarted ? "Start Loading" : "Stop Loading"}
253
+ </Button>
254
+
255
+ <div style={{ display: "flex", gap: 24 }}>
256
+ {renderContent("success")}
257
+ {renderContent("fail")}
258
+ {renderContent("warning")}
259
+ {renderContent("waiting")}
260
+ </div>
261
+ </div>
262
+ );
263
+ };
264
+ ```
265
+