@sberbusiness/triplex-next-mcp-server 0.24.0-beta1

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 (140) hide show
  1. package/README.md +124 -0
  2. package/bundle/.gitkeep +0 -0
  3. package/bundle/components/AbstractTree.md +198 -0
  4. package/bundle/components/AccordionBase.md +227 -0
  5. package/bundle/components/Alert.md +317 -0
  6. package/bundle/components/Amount.md +225 -0
  7. package/bundle/components/AmountField.md +289 -0
  8. package/bundle/components/Avatar.md +209 -0
  9. package/bundle/components/Badge.md +277 -0
  10. package/bundle/components/Body.md +167 -0
  11. package/bundle/components/Button.md +230 -0
  12. package/bundle/components/ButtonDropdown.md +159 -0
  13. package/bundle/components/ButtonIcon.md +122 -0
  14. package/bundle/components/Calendar.md +445 -0
  15. package/bundle/components/Card.md +374 -0
  16. package/bundle/components/CarouselExtended.md +250 -0
  17. package/bundle/components/Checkbox.md +126 -0
  18. package/bundle/components/CheckboxTree.md +280 -0
  19. package/bundle/components/CheckboxTreeExtended.md +321 -0
  20. package/bundle/components/CheckboxXGroup.md +68 -0
  21. package/bundle/components/CheckboxYGroup.md +67 -0
  22. package/bundle/components/Chip.md +404 -0
  23. package/bundle/components/ChipGroup.md +179 -0
  24. package/bundle/components/Col.md +221 -0
  25. package/bundle/components/CollapsableTree.md +255 -0
  26. package/bundle/components/Confirm.md +343 -0
  27. package/bundle/components/DateField.md +371 -0
  28. package/bundle/components/DatePickerExtended.md +342 -0
  29. package/bundle/components/DateRange.md +275 -0
  30. package/bundle/components/DesignTokens.md +385 -0
  31. package/bundle/components/Divider.md +196 -0
  32. package/bundle/components/DocumentNumberEdit.md +280 -0
  33. package/bundle/components/Dropdown.md +433 -0
  34. package/bundle/components/Ellipsis.md +208 -0
  35. package/bundle/components/EmptyView.md +99 -0
  36. package/bundle/components/ExpandAnimation.md +281 -0
  37. package/bundle/components/Footer.md +339 -0
  38. package/bundle/components/FormField.md +326 -0
  39. package/bundle/components/FormGroup.md +99 -0
  40. package/bundle/components/Gap.md +113 -0
  41. package/bundle/components/Header.md +280 -0
  42. package/bundle/components/HelpBox.md +154 -0
  43. package/bundle/components/IconWrapper.md +145 -0
  44. package/bundle/components/Island.md +197 -0
  45. package/bundle/components/IslandAccordion.md +230 -0
  46. package/bundle/components/IslandWidget.md +233 -0
  47. package/bundle/components/KeyDownListener.md +157 -0
  48. package/bundle/components/LightBox.md +322 -0
  49. package/bundle/components/Link.md +161 -0
  50. package/bundle/components/List.md +247 -0
  51. package/bundle/components/ListMaster.md +204 -0
  52. package/bundle/components/Loader.md +161 -0
  53. package/bundle/components/LoaderScreen.md +134 -0
  54. package/bundle/components/Marker.md +127 -0
  55. package/bundle/components/MarkerStatus.md +148 -0
  56. package/bundle/components/MediaWidth.md +169 -0
  57. package/bundle/components/MobileView.md +120 -0
  58. package/bundle/components/ModalWindow.md +308 -0
  59. package/bundle/components/MonthYearField.md +168 -0
  60. package/bundle/components/MultiselectField.md +213 -0
  61. package/bundle/components/Notification.md +233 -0
  62. package/bundle/components/NumberField.md +178 -0
  63. package/bundle/components/OrderedList.md +169 -0
  64. package/bundle/components/Overlay.md +237 -0
  65. package/bundle/components/Page.md +246 -0
  66. package/bundle/components/Pagination.md +298 -0
  67. package/bundle/components/Portal.md +119 -0
  68. package/bundle/components/Radio.md +174 -0
  69. package/bundle/components/Row.md +125 -0
  70. package/bundle/components/SMSField.md +160 -0
  71. package/bundle/components/SegmentedControl.md +235 -0
  72. package/bundle/components/SelectExtendedField.md +322 -0
  73. package/bundle/components/SelectField.md +213 -0
  74. package/bundle/components/Skeleton.md +129 -0
  75. package/bundle/components/Slider.md +157 -0
  76. package/bundle/components/SliderExtended.md +210 -0
  77. package/bundle/components/SliderRange.md +160 -0
  78. package/bundle/components/SmallInput.md +101 -0
  79. package/bundle/components/Spoiler.md +121 -0
  80. package/bundle/components/StatusTracker.md +151 -0
  81. package/bundle/components/Step.md +121 -0
  82. package/bundle/components/Stepper.md +163 -0
  83. package/bundle/components/Suggest.md +160 -0
  84. package/bundle/components/SuggestField.md +157 -0
  85. package/bundle/components/SwipeableArea.md +144 -0
  86. package/bundle/components/Table.md +199 -0
  87. package/bundle/components/Tabs.md +152 -0
  88. package/bundle/components/TabsExtended.md +273 -0
  89. package/bundle/components/TabsLine.md +189 -0
  90. package/bundle/components/Tag.md +135 -0
  91. package/bundle/components/TagColor.md +121 -0
  92. package/bundle/components/TagGroup.md +100 -0
  93. package/bundle/components/TextField.md +152 -0
  94. package/bundle/components/TextareaField.md +124 -0
  95. package/bundle/components/ThemeProvider.md +181 -0
  96. package/bundle/components/Tooltip.md +254 -0
  97. package/bundle/components/TopOverlay.md +145 -0
  98. package/bundle/components/TreeView.md +224 -0
  99. package/bundle/components/Triggers.md +121 -0
  100. package/bundle/guides/codestyle.md +205 -0
  101. package/bundle/guides/commits.md +166 -0
  102. package/bundle/guides/context.md +277 -0
  103. package/bundle/guides/stories.md +618 -0
  104. package/bundle/guides/template.md +154 -0
  105. package/bundle/guides/tests.md +301 -0
  106. package/bundle/icons.json +4156 -0
  107. package/bundle/manifest.json +796 -0
  108. package/bundle/release-notes/1.1.0.mdx +26 -0
  109. package/bundle/release-notes/1.1.1.mdx +14 -0
  110. package/bundle/release-notes/1.10.0.mdx +43 -0
  111. package/bundle/release-notes/1.11.0.mdx +19 -0
  112. package/bundle/release-notes/1.12.0.mdx +21 -0
  113. package/bundle/release-notes/1.13.0.mdx +43 -0
  114. package/bundle/release-notes/1.14.0.mdx +29 -0
  115. package/bundle/release-notes/1.15.0.mdx +56 -0
  116. package/bundle/release-notes/1.16.0.mdx +55 -0
  117. package/bundle/release-notes/1.17.0.mdx +15 -0
  118. package/bundle/release-notes/1.18.0.mdx +24 -0
  119. package/bundle/release-notes/1.19.0.mdx +48 -0
  120. package/bundle/release-notes/1.19.1.mdx +9 -0
  121. package/bundle/release-notes/1.2.0.mdx +80 -0
  122. package/bundle/release-notes/1.20.0.mdx +48 -0
  123. package/bundle/release-notes/1.21.0.mdx +66 -0
  124. package/bundle/release-notes/1.22.0.mdx +75 -0
  125. package/bundle/release-notes/1.23.0.mdx +50 -0
  126. package/bundle/release-notes/1.24.0.mdx +59 -0
  127. package/bundle/release-notes/1.25.0.mdx +52 -0
  128. package/bundle/release-notes/1.26.0.mdx +11 -0
  129. package/bundle/release-notes/1.3.0.mdx +21 -0
  130. package/bundle/release-notes/1.4.0.mdx +37 -0
  131. package/bundle/release-notes/1.5.0.mdx +24 -0
  132. package/bundle/release-notes/1.6.0.mdx +37 -0
  133. package/bundle/release-notes/1.6.1.mdx +11 -0
  134. package/bundle/release-notes/1.7.0.mdx +29 -0
  135. package/bundle/release-notes/1.8.0.mdx +38 -0
  136. package/bundle/release-notes/1.9.0.mdx +28 -0
  137. package/dist/index.d.ts +2 -0
  138. package/dist/index.js +324 -0
  139. package/dist/index.js.map +1 -0
  140. package/package.json +51 -0
@@ -0,0 +1,233 @@
1
+ ---
2
+ component: IslandWidget
3
+ category: Layout
4
+ figma-node: ""
5
+ figma-file: ""
6
+ related: [Island, IslandAccordion, ExpandAnimation, MediaWidth]
7
+ tokens: []
8
+ stories: stories/IslandWidget/IslandWidget.stories.tsx
9
+ version: "1.0"
10
+ ---
11
+
12
+ # IslandWidget
13
+
14
+ ## Назначение
15
+
16
+ Виджет-карточка: как `Island` (TYPE_1, MD), но:
17
+ - Принимает контент через **render-props** (`renderHeader`/`renderBody`/`renderFooter`) — не через children.
18
+ - На **мобиле (≤ SM_MAX) автоматически сворачивает Body+Footer**, заголовок становится кликабельным, рядом появляется chevron.
19
+ - Поддерживает необязательный **`ExtraFooter`** — дополнительный раздел, который открывается/закрывается управляемо (`open` prop), **снаружи** Island'а.
20
+ - Оптимизирован под работу в группе (`IslandWidgetWrapper` — provider для `hasExtraFooter`).
21
+
22
+ Используй когда: нужен адаптивный виджет на дашборде/пульте, который
23
+ схлопывается на мобиле. Если адаптивность не нужна — `Island` проще.
24
+
25
+ ---
26
+
27
+ ## Файловая структура
28
+
29
+ ```text
30
+ src/components/IslandWidget/
31
+ ├── IslandWidget.tsx # Object.assign с Header/Body/Footer/ExtraFooter
32
+ ├── IslandWidgetContext.tsx # { adaptive, disableAdaptiveCollapsing, open }
33
+ ├── IslandWidgetLayoutContext.tsx # { hasExtraFooter, setHasExtraFooter }
34
+ ├── index.ts # Barrel
35
+ ├── components/
36
+ │ ├── IslandWidgetHeader.tsx # FC + статические .Content/.Description, chevron при adaptive
37
+ │ ├── IslandWidgetHeaderContent.tsx
38
+ │ ├── IslandWidgetHeaderDescription.tsx
39
+ │ ├── IslandWidgetBody.tsx # FC
40
+ │ ├── IslandWidgetFooter.tsx # FC + статические .Content/.Controls
41
+ │ ├── IslandWidgetFooterContent.tsx
42
+ │ ├── IslandWidgetFooterControls.tsx
43
+ │ ├── IslandWidgetExtraFooter.tsx # FC, управляемое open (ExpandAnimation)
44
+ │ └── IslandWidgetWrapper.tsx # forwardRef, Provider IslandWidgetLayoutContext
45
+ └── styles/*.module.less
46
+ ```
47
+
48
+ ---
49
+
50
+ ## Compound-структура
51
+
52
+ ```tsx
53
+ <IslandWidgetWrapper>
54
+ <IslandWidget
55
+ renderHeader={(props) => (
56
+ <IslandWidget.Header {...props}>
57
+ <IslandWidget.Header.Content>Заголовок</IslandWidget.Header.Content>
58
+ <IslandWidget.Header.Description>Описание</IslandWidget.Header.Description>
59
+ </IslandWidget.Header>
60
+ )}
61
+ renderBody={(props) => <IslandWidget.Body {...props}>{...}</IslandWidget.Body>}
62
+ renderFooter={(props) => (
63
+ <IslandWidget.Footer {...props}>
64
+ <IslandWidget.Footer.Content>...</IslandWidget.Footer.Content>
65
+ <IslandWidget.Footer.Controls>
66
+ <Button>...</Button>
67
+ </IslandWidget.Footer.Controls>
68
+ </IslandWidget.Footer>
69
+ )}
70
+ />
71
+ <IslandWidget.ExtraFooter open={extraOpen}>
72
+ Дополнительный блок снизу
73
+ </IslandWidget.ExtraFooter>
74
+ </IslandWidgetWrapper>
75
+ ```
76
+
77
+ `IslandWidgetWrapper` опционален — нужен только если используешь `ExtraFooter`
78
+ (иначе `hasExtraFooter` всегда `false` и стили корня не подстраиваются).
79
+
80
+ ---
81
+
82
+ ## Варианты и props
83
+
84
+ ### IslandWidget — `IIslandWidgetProps extends HTMLAttributes<HTMLDivElement>`
85
+
86
+ | Prop | Тип | Обязателен | Описание |
87
+ |---|---|---|---|
88
+ | `renderHeader` | `(props: IIslandWidgetHeaderProps) => ReactNode` | Да | Рендер-функция заголовка. Получает пустой объект — можно использовать `IslandWidget.Header` с любыми child'ами. |
89
+ | `renderBody` | `(props: IIslandWidgetBodyProps) => ReactNode` | Да | Рендер-функция Body. |
90
+ | `renderFooter` | `(props: IIslandWidgetFooterProps) => ReactNode` | Нет | Необязательный Footer. |
91
+ | `disableAdaptiveCollapsing` | `boolean` | Нет (default `false`) | Отключает автоматическое сворачивание на мобиле. При `true` — заголовок некликабельный, chevron не рендерится. |
92
+
93
+ **Почему через render-props, а не children:** IslandWidget оборачивает контент в `ExpandAnimation` на adaptive, и ему нужно знать структуру слотов. Render-props позволяют вставлять слоты именно в нужные места (внутрь/снаружи ExpandAnimation).
94
+
95
+ ### IslandWidget.Header — `IIslandWidgetHeaderProps extends HTMLAttributes<HTMLDivElement>`
96
+
97
+ FC (не forwardRef). Не управляемый; читает `IslandWidgetContext` и рендерит chevron если `adaptive && !disableAdaptiveCollapsing`. Имеет статические `.Content` и `.Description`.
98
+
99
+ ### IslandWidget.Footer — `IIslandWidgetFooterProps extends HTMLAttributes<HTMLDivElement>`
100
+
101
+ FC. Имеет статические `.Content` и `.Controls` — типовой паттерн "описание + кнопки".
102
+
103
+ ### IslandWidget.Body — `IIslandWidgetBodyProps`
104
+
105
+ FC, без подслотов.
106
+
107
+ ### IslandWidget.ExtraFooter — `IIslandWidgetExtraFooterProps`
108
+
109
+ | Prop | Тип | Default | Описание |
110
+ |---|---|---|---|
111
+ | `open` | `boolean` | `false` | Управляемое состояние раскрытия. |
112
+
113
+ - Оборачивает содержимое в `ExpandAnimation expanded={open}`.
114
+ - В `useEffect` пишет `setHasExtraFooter(open)` в `IslandWidgetLayoutContext` — корневой `IslandWidget` использует этот флаг для корректировки стилей корня (добавляет `.islandWidgetWithExtraFooter`).
115
+
116
+ ### IslandWidgetWrapper
117
+
118
+ ForwardRef. Provider `IslandWidgetLayoutContext`. Нужен только для корректной работы `ExtraFooter`.
119
+
120
+ ---
121
+
122
+ ## Ключевые особенности реализации
123
+
124
+ ### Adaptive через `useMatchMedia`
125
+
126
+ ```ts
127
+ const adaptive = useMatchMedia(
128
+ `(max-width: ${EScreenWidth.SM_MAX})`,
129
+ window.innerWidth <= parseInt(EScreenWidth.SM_MAX),
130
+ );
131
+ ```
132
+
133
+ - Первым аргументом — media query, вторым — initial fallback для SSR-safe значения.
134
+ - `window.innerWidth` вычисляется на этапе рендера — **не SSR-safe** (упадёт на сервере). Потребители в SSR должны оборачивать виджет в `ClientOnly`-паттерн.
135
+
136
+ ### Управление открытием — внутреннее `useState`
137
+
138
+ ```ts
139
+ const [open, setOpen] = useState(disableAdaptiveCollapsing);
140
+ ```
141
+
142
+ - Если adaptive не включён — поведение не работает.
143
+ - `open` инициализируется значением `disableAdaptiveCollapsing`: без адаптивного сворачивания виджет открыт всегда (это защита, если `disableAdaptiveCollapsing` меняется рантайм).
144
+ - Нет prop'а `defaultOpen` / `opened` / `onToggle` — открытие полностью неуправляемое.
145
+ - Клик по заголовку → `handleHeaderClick` → `setOpen(!open)`. Срабатывает **только если adaptive**.
146
+
147
+ ### ExpandAnimation оборачивает Body + Footer (но не Header)
148
+
149
+ ```tsx
150
+ {expandableContent
151
+ ? <ExpandAnimation expanded={open}>{renderContent()}</ExpandAnimation>
152
+ : <>{renderContent()}</>}
153
+ ```
154
+
155
+ На desktop (не adaptive) или при `disableAdaptiveCollapsing=true` — без анимации, контент виден сразу.
156
+
157
+ ### `onClick` на wrapper'е Header'а, а не на Header'е
158
+
159
+ ```tsx
160
+ <div
161
+ className={clsx({ [styles.islandWidgetHeaderWrapperAdaptive]: adaptive })}
162
+ onClick={adaptive ? handleHeaderClick : undefined}
163
+ >
164
+ {renderHeader({})}
165
+ </div>
166
+ ```
167
+
168
+ - Click-target — обёртка, а не сам header. Это упрощает CSS (hover только на adaptive).
169
+ - **Нет `role="button"`/`tabIndex`/keyboard-навигации** на wrapper'е — клик только мышью. Это a11y-gap.
170
+
171
+ ### `hasExtraFooter` в LayoutContext
172
+
173
+ ```tsx
174
+ <div className={clsx(styles.islandWidget, className, {
175
+ [styles.islandWidgetWithExtraFooter]: hasExtraFooter,
176
+ })}>
177
+ ```
178
+
179
+ Корневой Island меняет стиль (например, нижний радиус), когда снизу есть ExtraFooter. Это делает Wrapper'ом эффективным лишь для пары "IslandWidget + ExtraFooter". Без Wrapper'а контекст дефолтный (`hasExtraFooter=false`).
180
+
181
+ ---
182
+
183
+ ## Инварианты
184
+
185
+ - **Render-props API (`renderHeader`/`renderBody`/`renderFooter`)** — публичный контракт. Не заменять на children без миграции потребителей.
186
+ - **Дочерние `Header`/`Body`/`Footer`** — `React.FC` (без forwardRef). Обёртки легковесные, но не прокидывают ref наружу.
187
+ - **`IslandWidgetHeader` / `IslandWidgetFooter`** — статические подкомпоненты через **не Object.assign**, а через `TIslandWidgetHeader = FC & { Content, Description }` + присваивание полей после определения. При изменении сохранить паттерн.
188
+ - **`IslandWidget.Body`** — не экспортируется в barrel прямо (`export * from "./IslandWidgetBody"` в components/index.ts есть, но проверить отдельно потребителей). Проверь экспорт перед изменением.
189
+ - **`useMatchMedia` + `window.innerWidth` на инициализации** — **не SSR-safe**. Не переносить в SSR-контекст без защиты.
190
+ - **Клик-target — wrapper Header'а**. Нет ARIA/keyboard для коллапса — известный a11y-debt.
191
+ - **`Island` хардкожен как `TYPE_1 / MD`** внутри — если нужен другой тип/размер, нужен рефакторинг с пропагацией через props.
192
+ - **`ExtraFooter` требует `IslandWidgetWrapper`** для корректного стиля нижнего радиуса корневого Island.
193
+ - **`ExpandAnimation`** оборачивает только Body+Footer; Header всегда виден.
194
+
195
+ ---
196
+
197
+ ## Accessibility
198
+
199
+ - Клик по header'у на мобиле **не доступен с клавиатуры** (нет `tabIndex`/`role="button"`/keyboard handler). Known debt. Для критичных сценариев используйте `disableAdaptiveCollapsing` и навигацию через iOS/Android native.
200
+ - Chevron-иконка — `aria-hidden="true"`, правильно.
201
+ - Нет `aria-expanded`/`aria-controls` на wrapper'е — скринридер не анонсирует состояние. Known debt.
202
+ - Остальное — прокидывается через `...htmlDivAttributes`.
203
+
204
+ ---
205
+
206
+ ## Дизайн-токены
207
+
208
+ Нет собственных (все от `Island.TYPE_1`: `--triplex-next-Island-Type1_Background` / `_Shadow`).
209
+
210
+ ---
211
+
212
+ ## Связанные компоненты
213
+
214
+ - `Island` — корневая карточка (hardcoded TYPE_1/MD внутри).
215
+ - `IslandAccordion` — альтернатива для списка раскрывающихся элементов.
216
+ - `ExpandAnimation` — используется для анимации сворачивания.
217
+ - `MediaWidth.useMatchMedia` — детектирует адаптив.
218
+ - `EScreenWidth` (`@sberbusiness/triplex-next/helpers/breakpoints`) — breakpoint SM_MAX.
219
+
220
+ ---
221
+
222
+ ## Stories
223
+
224
+ `stories/IslandWidget/IslandWidget.stories.tsx`
225
+ Примеры: `stories/IslandWidget/examples/`
226
+
227
+ ---
228
+
229
+ ## История изменений
230
+
231
+ | Дата | Изменение |
232
+ |---|---|
233
+ | 2026-04-18 | Создан документ |
@@ -0,0 +1,157 @@
1
+ ---
2
+ component: KeyDownListener
3
+ category: Utilities
4
+ figma-node: ""
5
+ figma-file: ""
6
+ related: [ComposedKeyDownListener, WindowResizeListener]
7
+ tokens: []
8
+ stories: ""
9
+ version: "1.0"
10
+ ---
11
+
12
+ # KeyDownListener
13
+
14
+ ## Назначение
15
+
16
+ Невизуальный компонент-слушатель `keydown` на `window`. При совпадении
17
+ кода нажатой клавиши с `eventKeyCode` (одно значение или массив) вызывает
18
+ `onMatch(event)`. Рендерит `children` как есть (или `null`) — не добавляет
19
+ обёртку в DOM.
20
+
21
+ Используй когда: нужно отреагировать на глобальное нажатие клавиш
22
+ (Escape для закрытия модалки, Tab/ArrowKey для навигации в кастомных
23
+ виджетах), но не привязываться к конкретному focus-target'у.
24
+
25
+ Для нескольких параллельных слушателей используй `ComposedKeyDownListener` —
26
+ принимает массив конфигов и композитит их в иерархию.
27
+
28
+ ---
29
+
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.
40
+
41
+ ---
42
+
43
+ ## Варианты и props
44
+
45
+ ### KeyDownListener — `IKeyDownListenerProps`
46
+
47
+ | Prop | Тип | Обязателен | Описание |
48
+ |---|---|---|---|
49
+ | `eventKeyCode` | `number \| number[]` | Да | Код клавиши (число — это `KeyboardEvent.keyCode`, не `key`). Один код или массив. |
50
+ | `onMatch` | `(event: KeyboardEvent) => void` | Да | Вызывается при совпадении кода. |
51
+ | `children` | `React.ReactNode` | Нет | Рендерятся "прозрачно" (компонент не оборачивает в DOM). |
52
+
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`, и т.д.
56
+
57
+ ### ComposedKeyDownListener — `IKeyDownListenerManagerProps`
58
+
59
+ | Prop | Тип | Обязателен | Описание |
60
+ |---|---|---|---|
61
+ | `keyDownListeners` | `IKeyDownListenerProps[]` | Да | Массив конфигов. Для каждого создаётся свой `KeyDownListener`. |
62
+ | `children` | `React.ReactNode` | Нет | Обёрнуты всеми listener'ами в иерархию (последний в массиве — самый внешний). |
63
+
64
+ ---
65
+
66
+ ## Ключевые особенности реализации
67
+
68
+ ### `KeyDownListener` — class component
69
+
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
+ ```
77
+
78
+ - **Сохранён как class**, хотя весь остальной код библиотеки — функциональные компоненты. Миграция на `useEffect` возможна, но class сохраняет идентичность listener'а через reference handler'а — аналог можно сделать через `useEffect` + стабильный handler (через ref на `onMatch`).
79
+ - Не `forwardRef` — компонент не имеет своего DOM.
80
+
81
+ ### Использование `keyCode`, а не `key` / `code`
82
+
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
+ ```
90
+
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.
94
+
95
+ ### `onMatch` не кешируется в class state
96
+
97
+ Каждый keydown вытаскивает `this.props.onMatch` актуальный — замыкание не устаревает. При миграции на hooks это поведение нужно воспроизвести через ref.
98
+
99
+ ### `ComposedKeyDownListener` — reduce-вариант через цикл
100
+
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
+ ```
107
+
108
+ - Последний listener в массиве → самый внешний wrapper в tree.
109
+ - Порядок не влияет на результат (все listener'ы слушают `window` независимо), но Reactу важен для reconciliation — не менять порядок без нужды.
110
+ - **Внимание:** ключи не проставляются. При рендере одного и того же конфига это ок, но при динамическом изменении массива React может ломать состояние child'ов — используй stable ordering.
111
+
112
+ ---
113
+
114
+ ## Инварианты
115
+
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`/контексты) без реальной нужды.
122
+
123
+ ---
124
+
125
+ ## Accessibility
126
+
127
+ - Компонент работает на уровне окна, перехватывает keydown до того, как фокусируемый элемент получит шанс обработать — при использовании **в составе интерактивных виджетов** учитывай, что нативный `onKeyDown` на inputе/кнопке сработает **после** глобального listener'а (из-за bubbling), но глобальный listener активен всегда, даже если фокус в другом месте.
128
+ - Не подменяй focus management'ом. Для Escape в модалках предпочитай `focus trap` + `onKeyDown` на overlay'е.
129
+ - Listener активен даже в скрытом state компонента-родителя, если сам KeyDownListener не размонтирован. Контролируй монтирование через условный рендеринг.
130
+
131
+ ---
132
+
133
+ ## Дизайн-токены
134
+
135
+ Нет.
136
+
137
+ ---
138
+
139
+ ## Связанные компоненты
140
+
141
+ - `ComposedKeyDownListener` — обёртка для нескольких listener'ов.
142
+ - `WindowResizeListener` — аналогичный паттерн для resize.
143
+ - `@sberbusiness/triplex-next/utils/keyboard` — `EVENT_KEY_CODES`, `EVENT_KEYS`, `isKey`.
144
+
145
+ ---
146
+
147
+ ## Stories
148
+
149
+ Нет.
150
+
151
+ ---
152
+
153
+ ## История изменений
154
+
155
+ | Дата | Изменение |
156
+ |---|---|
157
+ | 2026-04-18 | Создан документ |