@sberbusiness/triplex-next-mcp-server 0.24.0-beta2 → 1.40.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 (73) hide show
  1. package/README.md +10 -0
  2. package/bundle/components/AlertContext.md +268 -0
  3. package/bundle/components/BodyPage.md +186 -0
  4. package/bundle/components/Button.md +93 -85
  5. package/bundle/components/ButtonDropdown.md +36 -70
  6. package/bundle/components/ButtonDropdownExtended.md +422 -0
  7. package/bundle/components/ButtonIcon.md +41 -48
  8. package/bundle/components/Caption.md +291 -0
  9. package/bundle/components/Checkbox.md +43 -122
  10. package/bundle/components/CheckboxXGroup.md +109 -46
  11. package/bundle/components/CheckboxYGroup.md +77 -49
  12. package/bundle/components/CodeText.md +197 -0
  13. package/bundle/components/Col.md +311 -162
  14. package/bundle/components/CollapsibleTree.md +584 -0
  15. package/bundle/components/CollapsibleTreeExtended.md +422 -0
  16. package/bundle/components/Dropdown.md +748 -346
  17. package/bundle/components/EmptyView.md +14 -15
  18. package/bundle/components/FooterPage.md +203 -0
  19. package/bundle/components/Gap.md +115 -57
  20. package/bundle/components/HeaderPage.md +226 -0
  21. package/bundle/components/HelpBox.md +45 -84
  22. package/bundle/components/ImageGallery.md +423 -0
  23. package/bundle/components/ImageGalleryExtended.md +606 -0
  24. package/bundle/components/LightBox.md +2252 -233
  25. package/bundle/components/List.md +216 -183
  26. package/bundle/components/ListActionItem.md +157 -0
  27. package/bundle/components/ListItem.md +276 -0
  28. package/bundle/components/ListItemControlsButton.md +103 -0
  29. package/bundle/components/ListItemControlsButtonDropdown.md +107 -0
  30. package/bundle/components/ListItemSelectable.md +99 -0
  31. package/bundle/components/ListItemTable.md +390 -0
  32. package/bundle/components/ListMaster.md +876 -156
  33. package/bundle/components/ListMasterFooter.md +80 -0
  34. package/bundle/components/ListMasterHeader.md +86 -0
  35. package/bundle/components/ListSortable.md +153 -0
  36. package/bundle/components/ListSortableItem.md +175 -0
  37. package/bundle/components/ListTableItem.md +391 -0
  38. package/bundle/components/ModalWindow.md +427 -237
  39. package/bundle/components/MonthYearField.md +145 -109
  40. package/bundle/components/Notification.md +625 -155
  41. package/bundle/components/Overlay.md +271 -170
  42. package/bundle/components/Page.md +284 -186
  43. package/bundle/components/Pagination.md +206 -229
  44. package/bundle/components/Portal.md +131 -67
  45. package/bundle/components/Row.md +171 -69
  46. package/bundle/components/Text.md +320 -0
  47. package/bundle/components/TextField.md +364 -97
  48. package/bundle/components/Title.md +296 -0
  49. package/bundle/guides/codestyle.md +38 -11
  50. package/bundle/guides/commits.md +119 -26
  51. package/bundle/guides/context.md +34 -30
  52. package/bundle/guides/stories.md +110 -40
  53. package/bundle/guides/template.md +3 -7
  54. package/bundle/guides/tests.md +48 -2
  55. package/bundle/manifest.json +275 -30
  56. package/bundle/release-notes/1.26.0.mdx +33 -2
  57. package/bundle/release-notes/1.27.0.mdx +43 -0
  58. package/bundle/release-notes/1.28.0.mdx +25 -0
  59. package/bundle/release-notes/1.29.0.mdx +47 -0
  60. package/bundle/release-notes/1.30.0.mdx +56 -0
  61. package/bundle/release-notes/1.31.0.mdx +52 -0
  62. package/bundle/release-notes/1.32.0.mdx +35 -0
  63. package/bundle/release-notes/1.33.0.mdx +61 -0
  64. package/bundle/release-notes/1.34.0.mdx +35 -0
  65. package/bundle/release-notes/1.35.0.mdx +21 -0
  66. package/bundle/release-notes/1.35.1.mdx +11 -0
  67. package/bundle/release-notes/1.36.0.mdx +21 -0
  68. package/bundle/release-notes/1.37.0.mdx +71 -0
  69. package/bundle/release-notes/1.38.0.mdx +36 -0
  70. package/bundle/release-notes/1.39.0.mdx +66 -0
  71. package/bundle/release-notes/1.40.0.mdx +27 -0
  72. package/bundle/release-notes/1.41.0.mdx +7 -0
  73. package/package.json +1 -1
@@ -0,0 +1,422 @@
1
+ ---
2
+ component: ButtonDropdownExtended
3
+ category: Buttons
4
+ related: [ButtonDropdown, Button, Dropdown, DropdownList]
5
+ tokens: []
6
+ stories: stories/Buttons/ButtonDropdownExtended.stories.tsx
7
+ version: "1.0"
8
+ ---
9
+
10
+ # ButtonDropdownExtended
11
+
12
+ ## Назначение
13
+
14
+ Низкоуровневый контейнер «кнопка + выпадающий блок». Сам ничего не рисует: кнопку и содержимое выпадающего блока задаёт потребитель через render-функции `renderButton` и `renderDropdown`. Компонент берёт на себя только состояние открытости и закрытие блока по Escape, по Tab (при `closeOnTab`) и по клику/тапу вне кнопки и вне блока.
15
+
16
+ Используй когда: нужен собственный триггер и/или произвольное содержимое выпадающего блока (форма, фильтр, кастомный список), а логику открытия/закрытия переиспользовать.
17
+ Не используй когда: нужен готовый список действий — используй `ButtonDropdown`, он построен поверх этого компонента.
18
+
19
+ ---
20
+
21
+ ## Варианты и props
22
+
23
+ ### Обязательные props
24
+
25
+ | Prop | Тип | Описание |
26
+ |---|---|---|
27
+ | `renderButton` | `(props: IButtonDropdownExtendedButtonProvideProps) => React.ReactNode` | Отрисовка кнопки-триггера. Получает `opened` и `setOpened` |
28
+ | `renderDropdown` | `(props: IButtonDropdownExtendedDropdownProvideProps) => React.ReactNode` | Отрисовка выпадающего блока. Дополнительно получает `className` внутреннего позиционирующего класса |
29
+ | `dropdownRef` | `React.RefObject<HTMLElement>` | Ссылка на DOM-элемент выпадающего блока. Нужна для click-outside: клик внутри этого элемента не закрывает блок |
30
+
31
+ ### Опциональные props
32
+
33
+ | Prop | Тип | По умолчанию | Описание |
34
+ |---|---|---|---|
35
+ | `opened` | `boolean` | — | Внешнее состояние открытости. Включает контролируемый режим |
36
+ | `setOpened` | `(opened: boolean) => void` | — | Запрос на смену состояния. Вызывается **только** в контролируемом режиме |
37
+ | `closeOnTab` | `boolean` | `false` | Закрывать блок по Tab (в дополнение к Escape и клику снаружи) |
38
+ | `className` | `string` | — | Дополнительный CSS-класс корневого `<div>` |
39
+ | `...rest` | `React.HTMLAttributes<HTMLDivElement>` | — | Атрибуты корневого `<div>` |
40
+
41
+ ### Контролируемый и неконтролируемый режим
42
+
43
+ - Режим **фиксируется на монтировании**: если `opened !== undefined` на первом рендере — компонент контролируемый на всё время жизни, иначе неконтролируемый. Появление или исчезновение `opened` после маунта режим не меняет.
44
+ - В контролируемом режиме внутреннее состояние не используется: `setOpened` вызывается, а фактическое значение приходит из prop `opened`. Без `setOpened` блок в этом режиме не сможет закрыться.
45
+ - В неконтролируемом режиме `setOpened` не вызывается — состояние живёт внутри компонента.
46
+ - `renderButton` и `renderDropdown` в обоих режимах получают одно и то же актуальное значение `opened` и один и тот же обработчик `setOpened`.
47
+
48
+ ### Статические субкомпоненты
49
+
50
+ `ButtonDropdownExtended.Dropdown` и `ButtonDropdownExtended.DropdownList` — реэкспорты `Dropdown` и `DropdownList`, чтобы собирать выпадающий блок без дополнительных импортов. Это те же самые компоненты, а не обёртки.
51
+
52
+ ---
53
+
54
+ ## Дизайн-токены
55
+
56
+ Собственных CSS-переменных нет. `ButtonDropdownExtended.module.less` задаёт только раскладку корневого контейнера: `display: inline-block`, `position: relative`, `vertical-align: middle` (общая опорная точка с `Button`), `line-height: 0`. Цвета и размеры приходят из вложенных `Button` и `Dropdown`.
57
+
58
+ Правила класса выпадающего блока (`padding: 4px`, `margin-top: 4px`, `right: 0`) объявлены **вложенным** селектором внутри `.buttonDropdownExtended`, то есть компилируются в потомковый. При штатном использовании они не применяются: `ButtonDropdownExtended.Dropdown` рендерится через `<Portal container={document.body}>`, и узел с этим классом не является потомком корневого `<div>`. Позиционирование блока в этом случае обеспечивает сам `Dropdown` по `targetRef`. Учитывай это, если правишь отступы: менять их нужно не здесь.
59
+
60
+ ---
61
+
62
+ ## Инварианты
63
+
64
+ - Компонент объявлен как `IButtonDropdownExtendedComponent extends React.FC<IButtonDropdownExtendedProps>`, `forwardRef` **не используется**: ref на корневой `<div>` не пробрасывается. Добавление `forwardRef` меняет базовый тип экспортируемого интерфейса — это breaking change, требует отдельного решения.
65
+ - Публичные имена `ButtonDropdownExtended`, `IButtonDropdownExtendedProps`, `IButtonDropdownExtendedButtonProvideProps`, `IButtonDropdownExtendedDropdownProvideProps`, `IButtonDropdownExtendedComponent` экспортируются из `src/components/Button/index.ts` — сохранять.
66
+ - Статические свойства `Dropdown` и `DropdownList` — часть публичного API.
67
+ - Фиксация режима управления на монтировании — наблюдаемое поведение, на него опираются потребители. Менять только осознанно.
68
+ - `renderDropdown` обязан применить переданный `className` к выпадающему блоку. Но не считай, что на нём держится раскладка: стили этого класса объявлены вложенным селектором внутри `.buttonDropdownExtended` и при рендере блока в портале (штатный путь через `ButtonDropdownExtended.Dropdown`) не применяются — см. раздел «Дизайн-токены».
69
+ - Корневой DOM-элемент — `<div>` с классом `buttonDropdownExtended`; на нём же живёт `containerRef`, по которому определяется «клик внутри триггера».
70
+ - Слушатели `keydown` / `mousedown` / `touchstart` вешаются на `document` только пока блок открыт.
71
+
72
+ ---
73
+
74
+ ## Accessibility
75
+
76
+ - Компонент не рендерит собственной разметки кроме контейнера `<div>` и **не выставляет ARIA-атрибутов**. Роли и связки — зона ответственности `renderButton` / `renderDropdown`.
77
+ - Потребитель обязан сам проставить на триггере `aria-haspopup`, `aria-expanded={opened}` и при необходимости `aria-controls` (см. `ButtonDropdown` как эталонную реализацию).
78
+ - Клавиатура, обрабатываемая самим компонентом: `Escape` закрывает блок всегда, `Tab` — только при `closeOnTab`. Обработчик висит на `document`, поэтому срабатывает независимо от того, где находится фокус.
79
+ - Фокус компонент не перемещает: при открытии фокус не уходит в блок, при закрытии не возвращается на триггер. Если продукту нужен focus trap или возврат фокуса — реализуй это в render-функциях.
80
+ - Текстовых строк компонент не содержит, хардкода языка нет.
81
+
82
+ ---
83
+
84
+ ## Связанные компоненты
85
+
86
+ - `ButtonDropdown` (`src/components/Button/ButtonDropdown.tsx`) — готовая кнопка со списком действий, построена поверх `ButtonDropdownExtended`.
87
+ - `Button` (`src/components/Button/Button.tsx`) — обычный триггер для `renderButton`.
88
+ - `Dropdown` (`src/components/Dropdown`) — выпадающий блок, доступен как `ButtonDropdownExtended.Dropdown`; рендерится в портале, позиционируется по `targetRef`.
89
+ - `DropdownList` (`src/components/Dropdown/desktop/DropdownList.tsx`) — список с клавиатурной навигацией, доступен как `ButtonDropdownExtended.DropdownList`.
90
+ - Внутренние потребители в библиотеке: `ListItemControlsButtonDropdown`, `TableBasicSettings` — при изменении поведения проверяй их.
91
+
92
+ ---
93
+
94
+ ## Stories
95
+
96
+ Основные истории: `stories/Buttons/ButtonDropdownExtended.stories.tsx`
97
+ Файлы примеров: `stories/Buttons/examples/ButtonDropdownExtended/`
98
+
99
+ | Story | Example file | Что демонстрирует |
100
+ |---|---|---|
101
+ | `Playground` | `Playground.tsx` | Интерактивный контроль `closeOnTab` |
102
+ | `Default` | `Default.tsx` | Неконтролируемый режим: кнопка `Button` + `Dropdown` со списком действий |
103
+ | `Controlled` | `Controlled.tsx` | Контролируемый режим через `opened` / `setOpened` |
104
+ | `CloseOnTab` | `CloseOnTab.tsx` | Закрытие блока по Tab |
105
+ | `WithCustomContent` | `WithCustomContent.tsx` | Произвольное содержимое блока вместо списка действий |
106
+ | `VisualTests` | `VisualTests.tsx` | Раскрытые блоки: список действий (`MIN_TARGET` + `RIGHT`, раскрывается `play`-функцией) и произвольный контент (`CONTENT` + `LEFT`, форсированно раскрыт в контролируемом режиме) |
107
+
108
+ Документационные стори намеренно рендерятся в закрытом состоянии: раскрытый `Dropdown` блокирует скролл страницы (`wheel`-обработчик в `DropdownDesktop`), что ломает навигацию по autodocs. Раскрытые состояния покрыты `VisualTests`.
109
+
110
+ Скриншот-тесты: `Playground` пропускается всегда; `Controlled` и `CloseOnTab` пропускаются как визуально идентичные `Default`.
111
+
112
+ ---
113
+
114
+ ## История изменений
115
+
116
+ | Дата | Изменение |
117
+ |---|---|
118
+ | 2026-07-29 | Создан документ AI-ready для `ButtonDropdownExtended`. AI-рефакторинг без изменения публичного API: режим управления переведён с ref на `useState` (устранены ошибки `react-hooks/refs`), убраны вырожденный тернарник в `useState` и мёртвые проверки `opened` внутри обработчиков, `setOpened && setOpened(...)` заменён на опциональный вызов, добавлены JSDoc. Добавлены unit-тесты и stories по modern pattern. |
119
+
120
+ ---
121
+
122
+ ## Примеры использования
123
+
124
+ ### Default
125
+
126
+ `stories/Buttons/examples/ButtonDropdownExtended/Default.tsx`
127
+
128
+ ```tsx
129
+ import React, { useRef } from "react";
130
+ import {
131
+ Button,
132
+ ButtonDropdownExtended,
133
+ EButtonTheme,
134
+ EComponentSize,
135
+ EDropdownAlignment,
136
+ EDropdownWidth,
137
+ IButtonDropdownExtendedButtonProvideProps,
138
+ IButtonDropdownExtendedDropdownProvideProps,
139
+ } from "@sberbusiness/triplex-next";
140
+
141
+ const OPTIONS = [
142
+ { id: "opt-1", label: "Действие 1" },
143
+ { id: "opt-2", label: "Действие 2" },
144
+ { id: "opt-3", label: "Действие 3" },
145
+ ];
146
+
147
+ export const Default = () => {
148
+ const buttonRef = useRef<HTMLButtonElement>(null);
149
+ const dropdownRef = useRef<HTMLDivElement>(null);
150
+
151
+ const renderButton = ({ opened, setOpened }: IButtonDropdownExtendedButtonProvideProps) => (
152
+ <Button
153
+ theme={EButtonTheme.GENERAL}
154
+ size={EComponentSize.MD}
155
+ aria-haspopup="menu"
156
+ aria-expanded={opened}
157
+ onClick={() => setOpened(!opened)}
158
+ ref={buttonRef}
159
+ >
160
+ Действия
161
+ </Button>
162
+ );
163
+
164
+ const renderDropdown = ({ opened, setOpened, className }: IButtonDropdownExtendedDropdownProvideProps) => (
165
+ <ButtonDropdownExtended.Dropdown
166
+ className={className}
167
+ size={EComponentSize.MD}
168
+ width={EDropdownWidth.MIN_TARGET}
169
+ alignment={EDropdownAlignment.LEFT}
170
+ opened={opened}
171
+ setOpened={setOpened}
172
+ targetRef={buttonRef}
173
+ ref={dropdownRef}
174
+ >
175
+ <ButtonDropdownExtended.DropdownList dropdownOpened={opened} size={EComponentSize.MD}>
176
+ {OPTIONS.map((option) => (
177
+ <ButtonDropdownExtended.DropdownList.Item
178
+ key={option.id}
179
+ id={option.id}
180
+ onSelect={() => setOpened(false)}
181
+ >
182
+ {option.label}
183
+ </ButtonDropdownExtended.DropdownList.Item>
184
+ ))}
185
+ </ButtonDropdownExtended.DropdownList>
186
+ </ButtonDropdownExtended.Dropdown>
187
+ );
188
+
189
+ return (
190
+ <ButtonDropdownExtended renderButton={renderButton} renderDropdown={renderDropdown} dropdownRef={dropdownRef} />
191
+ );
192
+ };
193
+ ```
194
+
195
+ ### Controlled
196
+
197
+ `stories/Buttons/examples/ButtonDropdownExtended/Controlled.tsx`
198
+
199
+ ```tsx
200
+ import React, { useRef, useState } from "react";
201
+ import {
202
+ Button,
203
+ ButtonDropdownExtended,
204
+ EButtonTheme,
205
+ EComponentSize,
206
+ EDropdownAlignment,
207
+ EDropdownWidth,
208
+ IButtonDropdownExtendedButtonProvideProps,
209
+ IButtonDropdownExtendedDropdownProvideProps,
210
+ } from "@sberbusiness/triplex-next";
211
+
212
+ const OPTIONS = [
213
+ { id: "opt-1", label: "Действие 1" },
214
+ { id: "opt-2", label: "Действие 2" },
215
+ { id: "opt-3", label: "Действие 3" },
216
+ ];
217
+
218
+ export const Controlled = () => {
219
+ const buttonRef = useRef<HTMLButtonElement>(null);
220
+ const dropdownRef = useRef<HTMLDivElement>(null);
221
+ // opened должен быть определён уже на первом рендере — режим управления фиксируется на монтировании.
222
+ const [opened, setOpened] = useState(false);
223
+
224
+ // В контролируемом режиме render-функции получают то же значение opened и тот же обработчик,
225
+ // что и в неконтролируемом, — контракт render-props одинаков в обоих режимах.
226
+ const renderButton = ({ opened, setOpened }: IButtonDropdownExtendedButtonProvideProps) => (
227
+ <Button
228
+ theme={EButtonTheme.GENERAL}
229
+ size={EComponentSize.MD}
230
+ aria-haspopup="menu"
231
+ aria-expanded={opened}
232
+ onClick={() => setOpened(!opened)}
233
+ ref={buttonRef}
234
+ >
235
+ Действия
236
+ </Button>
237
+ );
238
+
239
+ const renderDropdown = ({ className, opened, setOpened }: IButtonDropdownExtendedDropdownProvideProps) => (
240
+ <ButtonDropdownExtended.Dropdown
241
+ className={className}
242
+ size={EComponentSize.MD}
243
+ width={EDropdownWidth.MIN_TARGET}
244
+ alignment={EDropdownAlignment.LEFT}
245
+ opened={opened}
246
+ setOpened={setOpened}
247
+ targetRef={buttonRef}
248
+ ref={dropdownRef}
249
+ >
250
+ <ButtonDropdownExtended.DropdownList dropdownOpened={opened} size={EComponentSize.MD}>
251
+ {OPTIONS.map((option) => (
252
+ <ButtonDropdownExtended.DropdownList.Item
253
+ key={option.id}
254
+ id={option.id}
255
+ onSelect={() => setOpened(false)}
256
+ >
257
+ {option.label}
258
+ </ButtonDropdownExtended.DropdownList.Item>
259
+ ))}
260
+ </ButtonDropdownExtended.DropdownList>
261
+ </ButtonDropdownExtended.Dropdown>
262
+ );
263
+
264
+ return (
265
+ <ButtonDropdownExtended
266
+ opened={opened}
267
+ setOpened={setOpened}
268
+ renderButton={renderButton}
269
+ renderDropdown={renderDropdown}
270
+ dropdownRef={dropdownRef}
271
+ />
272
+ );
273
+ };
274
+ ```
275
+
276
+ ### CloseOnTab
277
+
278
+ `stories/Buttons/examples/ButtonDropdownExtended/CloseOnTab.tsx`
279
+
280
+ ```tsx
281
+ import React, { useRef } from "react";
282
+ import {
283
+ Button,
284
+ ButtonDropdownExtended,
285
+ EButtonTheme,
286
+ EComponentSize,
287
+ EDropdownAlignment,
288
+ EDropdownWidth,
289
+ IButtonDropdownExtendedButtonProvideProps,
290
+ IButtonDropdownExtendedDropdownProvideProps,
291
+ } from "@sberbusiness/triplex-next";
292
+
293
+ const OPTIONS = [
294
+ { id: "opt-1", label: "Действие 1" },
295
+ { id: "opt-2", label: "Действие 2" },
296
+ { id: "opt-3", label: "Действие 3" },
297
+ ];
298
+
299
+ export const CloseOnTab = () => {
300
+ const buttonRef = useRef<HTMLButtonElement>(null);
301
+ const dropdownRef = useRef<HTMLDivElement>(null);
302
+
303
+ const renderButton = ({ opened, setOpened }: IButtonDropdownExtendedButtonProvideProps) => (
304
+ <Button
305
+ theme={EButtonTheme.GENERAL}
306
+ size={EComponentSize.MD}
307
+ aria-haspopup="menu"
308
+ aria-expanded={opened}
309
+ onClick={() => setOpened(!opened)}
310
+ ref={buttonRef}
311
+ >
312
+ Действия
313
+ </Button>
314
+ );
315
+
316
+ const renderDropdown = ({ opened, setOpened, className }: IButtonDropdownExtendedDropdownProvideProps) => (
317
+ <ButtonDropdownExtended.Dropdown
318
+ className={className}
319
+ size={EComponentSize.MD}
320
+ width={EDropdownWidth.MIN_TARGET}
321
+ alignment={EDropdownAlignment.LEFT}
322
+ opened={opened}
323
+ setOpened={setOpened}
324
+ targetRef={buttonRef}
325
+ ref={dropdownRef}
326
+ >
327
+ <ButtonDropdownExtended.DropdownList dropdownOpened={opened} size={EComponentSize.MD}>
328
+ {OPTIONS.map((option) => (
329
+ <ButtonDropdownExtended.DropdownList.Item
330
+ key={option.id}
331
+ id={option.id}
332
+ onSelect={() => setOpened(false)}
333
+ >
334
+ {option.label}
335
+ </ButtonDropdownExtended.DropdownList.Item>
336
+ ))}
337
+ </ButtonDropdownExtended.DropdownList>
338
+ </ButtonDropdownExtended.Dropdown>
339
+ );
340
+
341
+ return (
342
+ <ButtonDropdownExtended
343
+ closeOnTab
344
+ renderButton={renderButton}
345
+ renderDropdown={renderDropdown}
346
+ dropdownRef={dropdownRef}
347
+ />
348
+ );
349
+ };
350
+ ```
351
+
352
+ ### WithCustomContent
353
+
354
+ `stories/Buttons/examples/ButtonDropdownExtended/WithCustomContent.tsx`
355
+
356
+ ```tsx
357
+ import React, { useRef, useState } from "react";
358
+ import {
359
+ Button,
360
+ ButtonDropdownExtended,
361
+ EButtonTheme,
362
+ EComponentSize,
363
+ EDropdownAlignment,
364
+ ETextSize,
365
+ EDropdownWidth,
366
+ IButtonDropdownExtendedButtonProvideProps,
367
+ IButtonDropdownExtendedDropdownProvideProps,
368
+ Text,
369
+ } from "@sberbusiness/triplex-next";
370
+
371
+ export const WithCustomContent = () => {
372
+ const buttonRef = useRef<HTMLButtonElement>(null);
373
+ const dropdownRef = useRef<HTMLDivElement>(null);
374
+ const [opened, setOpened] = useState(false);
375
+
376
+ const renderButton = ({ opened, setOpened }: IButtonDropdownExtendedButtonProvideProps) => (
377
+ <Button
378
+ theme={EButtonTheme.SECONDARY}
379
+ size={EComponentSize.MD}
380
+ aria-haspopup="dialog"
381
+ aria-expanded={opened}
382
+ onClick={() => setOpened(!opened)}
383
+ ref={buttonRef}
384
+ >
385
+ Фильтр
386
+ </Button>
387
+ );
388
+
389
+ const renderDropdown = ({ className, opened, setOpened }: IButtonDropdownExtendedDropdownProvideProps) => (
390
+ <ButtonDropdownExtended.Dropdown
391
+ className={className}
392
+ size={EComponentSize.MD}
393
+ width={EDropdownWidth.CONTENT}
394
+ alignment={EDropdownAlignment.LEFT}
395
+ opened={opened}
396
+ setOpened={setOpened}
397
+ targetRef={buttonRef}
398
+ ref={dropdownRef}
399
+ >
400
+ <div style={{ display: "flex", flexDirection: "column", gap: 12, padding: 8, width: 220 }}>
401
+ <Text tag="div" size={ETextSize.B3}>
402
+ Здесь может быть любой контент: форма, фильтр или подсказка.
403
+ </Text>
404
+ <Button theme={EButtonTheme.GENERAL} size={EComponentSize.SM} onClick={() => setOpened(false)}>
405
+ Применить
406
+ </Button>
407
+ </div>
408
+ </ButtonDropdownExtended.Dropdown>
409
+ );
410
+
411
+ return (
412
+ <ButtonDropdownExtended
413
+ opened={opened}
414
+ setOpened={setOpened}
415
+ renderButton={renderButton}
416
+ renderDropdown={renderDropdown}
417
+ dropdownRef={dropdownRef}
418
+ />
419
+ );
420
+ };
421
+ ```
422
+
@@ -1,8 +1,6 @@
1
1
  ---
2
2
  component: ButtonIcon
3
3
  category: Buttons
4
- figma-node: ""
5
- figma-file: ""
6
4
  related: [Button]
7
5
  tokens:
8
6
  - --triplex-next-Button-Icon_Shadow_Focus
@@ -17,19 +15,7 @@ version: "1.0"
17
15
  Кнопка только с иконкой без текстового контента. Используется как компактный trigger для локальных действий (например, открыть меню, удалить, закрыть).
18
16
 
19
17
  Используй когда: действие можно однозначно распознать по иконке и есть корректный `aria-label`.
20
- Не используй когда: нужен текстовый CTA или несколько визуальных тем с размерами как у `Button` - используй `Button`.
21
-
22
- ---
23
-
24
- ## Файловая структура
25
-
26
- ```text
27
- src/components/Button/
28
- ├── ButtonIcon.tsx # Компонент (forwardRef, native button)
29
- ├── enums.ts # EButtonIconShape (SQUIRCLE / CIRCLE)
30
- └── styles/
31
- └── ButtonIcon.module.less # Геометрия, focus-visible, базовые состояния
32
- ```
18
+ Не используй когда: нужен текстовый CTA или несколько визуальных тем с размерами как у `Button` — используй `Button`.
33
19
 
34
20
  ---
35
21
 
@@ -45,35 +31,16 @@ src/components/Button/
45
31
  |---|---|---|---|
46
32
  | `shape` | `EButtonIconShape` | `SQUIRCLE` | Форма кнопки: `SQUIRCLE` (радиус 4px) или `CIRCLE` (радиус 50%) |
47
33
  | `active` | `boolean` | `false` | Визуально активное состояние |
34
+ | `children` | `React.ReactNode` | — | Содержимое кнопки, обычно иконка из `@sberbusiness/icons-next` |
48
35
  | `className` | `string` | — | Дополнительный CSS-класс |
49
36
  | `...rest` | `React.ButtonHTMLAttributes<HTMLButtonElement> & DataAttributes` | — | Нативные атрибуты `<button>`, включая `disabled`, `aria-*`, `data-*` |
50
37
 
51
38
  ### Ограничения использования
52
39
 
53
40
  - Размер `ButtonIcon` задаётся размером переданной иконки; у компонента нет собственного prop `size`.
54
- - Корневой DOM-элемент всегда `button`, `type` по умолчанию принудительно установлен в `button`.
55
-
56
- ---
57
-
58
- ## Ключевые особенности реализации
59
-
60
- ### Маппинг формы через enum
61
-
62
- - `shape` маппится в CSS-класс через `Record<EButtonIconShape, string>`.
63
- - При расширении `EButtonIconShape` нужно добавить класс в маппинг и в `ButtonIcon.module.less`.
64
-
65
- ### Визуальные состояния
66
-
67
- - `active` и `disabled` добавляются как глобальные CSS-классы (`active`, `disabled`) для совместимости с иконками.
68
- - Дополнительно всегда добавляется класс `hoverable`.
69
-
70
- ---
71
-
72
- ## Accessibility
73
-
74
- - Компонент рендерит нативный `<button>`, поэтому базовая keyboard-навигация доступна из коробки.
75
- - Компонент **не хардкодит** `aria-label` (библиотека мультиязычная). Для иконки без текста потребитель обязан передать `aria-label` через `...rest`.
76
- - Фокусное состояние реализовано через `:focus-visible` и токен `--triplex-next-Button-Icon_Shadow_Focus`.
41
+ - Внешний DOM-узел — не `<button>`: компонент рендерит `IconWrapper` (`<span>` с классами `hoverable` и `displayContents`, то есть `display: contents`), а внутри него — `<button>`. Ref-target и получатель `className` / `...rest` — всегда `<button>`; классы состояний `active` и `disabled` выставляются на span-обёртке. Это важно для `>`-селекторов и логики на `parentElement`.
42
+ - `type="button"` выставляется компонентом (чтобы кнопка не триггерила submit формы), но может быть переопределён через `...rest`.
43
+ - При расширении `EButtonIconShape` необходимо добавить класс в маппинг `Record<EButtonIconShape, string>` и в `ButtonIcon.module.less`.
77
44
 
78
45
  ---
79
46
 
@@ -94,16 +61,24 @@ src/components/Button/
94
61
 
95
62
  ---
96
63
 
64
+ ## Accessibility
65
+
66
+ - Компонент рендерит нативный `<button>`, поэтому базовая keyboard-навигация доступна из коробки.
67
+ - Компонент **не хардкодит** `aria-label` (библиотека мультиязычная). Для иконки без текста потребитель **обязан** передать `aria-label` через `...rest`.
68
+ - Фокусное состояние реализовано через `:focus-visible` и токен `--triplex-next-Button-Icon_Shadow_Focus`.
69
+
70
+ ---
71
+
97
72
  ## Связанные компоненты
98
73
 
99
- - `Button` (`src/components/Button/Button.tsx`) - текстовая/универсальная кнопка, используемая вместо `ButtonIcon` когда нужен label.
100
- - `ButtonDropdown` (`src/components/Button/ButtonDropdown.tsx`) - использует иконочный паттерн для dots-варианта trigger.
74
+ - `Button` (`src/components/Button/Button.tsx`) — текстовая/универсальная кнопка, используемая вместо `ButtonIcon` когда нужен label.
75
+ - `ButtonDropdown` (`src/components/Button/ButtonDropdown.tsx`) — использует иконочный паттерн для dots-варианта trigger.
101
76
 
102
77
  ---
103
78
 
104
79
  ## Stories
105
80
 
106
- `stories/Buttons/ButtonIcon.stories.tsx`
81
+ Основные истории: `stories/Buttons/ButtonIcon.stories.tsx`
107
82
  Файлы примеров: `stories/Buttons/examples/ButtonIcon/`
108
83
 
109
84
  | Story | Example file | Что демонстрирует |
@@ -111,7 +86,9 @@ src/components/Button/
111
86
  | `Playground` | `PlaygroundExample.tsx` | Интерактивный контроль `shape`, `active`, `disabled` |
112
87
  | `Default` | `DefaultExample.tsx` | Базовый сценарий использования |
113
88
  | `Sizes` | `SizesExample.tsx` | Разные размеры через размер иконки (16/20/24/32) |
114
- | `Disabled` | `DisabledExample.tsx` | Неактивное состояние |
89
+ | `States` | `StatesExample.tsx` | Состояния `active` и `disabled` |
90
+ | `VisualTestsSquircle` | `VisualTestsExample.tsx` | Скриншот-регрессия: `:focus-visible` на squircle-кнопке |
91
+ | `VisualTestsCircle` | `VisualTestsExample.tsx` | Скриншот-регрессия: `:focus-visible` на circle-кнопке |
115
92
 
116
93
  ---
117
94
 
@@ -120,6 +97,10 @@ src/components/Button/
120
97
  | Дата | Изменение |
121
98
  |---|---|
122
99
  | 2026-04-15 | Создан документ AI-ready для `ButtonIcon`. |
100
+ | 2026-04-27 | Приведён в соответствие с `docs/ai/template-ai.md`: убрана секция «Файловая структура», содержимое «Ключевых особенностей реализации» перенесено в `Ограничения использования`, добавлена колонка `Example file`. |
101
+ | 2026-07-08 | Актуализирована таблица Stories: story `Disabled` заменена на `States` (`StatesExample.tsx`). |
102
+ | 2026-07-15 | `vertical-align: top` заменён на `middle` (синхронно с `Button`) — общая опорная точка для кнопок разной высоты и центрирование относительно строки инлайн-текста. |
103
+ | 2026-07-30 | AI-рефакторинг: внутренние импорты переведены на относительные, `children` объявлен и задокументирован явно, JSDoc уточнён дефолтами; добавлены unit-тесты `__tests__/ButtonIcon.test.tsx`; в таблицу Stories добавлены отсутствовавшие `VisualTestsSquircle` и `VisualTestsCircle`. Публичный API не изменён. |
123
104
 
124
105
  ---
125
106
 
@@ -173,19 +154,31 @@ export const SizesExample = () => (
173
154
  );
174
155
  ```
175
156
 
176
- ### Disabled
157
+ ### States
177
158
 
178
- `stories/Buttons/examples/ButtonIcon/DisabledExample.tsx`
159
+ `stories/Buttons/examples/ButtonIcon/StatesExample.tsx`
179
160
 
180
161
  ```tsx
181
162
  import React from "react";
182
163
  import { DefaulticonStrokePrdIcon32 } from "@sberbusiness/icons-next";
183
164
  import { ButtonIcon, EButtonIconShape } from "@sberbusiness/triplex-next";
184
165
 
185
- export const DisabledExample = () => (
186
- <ButtonIcon shape={EButtonIconShape.SQUIRCLE} disabled>
187
- <DefaulticonStrokePrdIcon32 paletteIndex={5} />
188
- </ButtonIcon>
166
+ export const StatesExample = () => (
167
+ <div style={{ display: "flex", flexDirection: "column", gap: 16 }}>
168
+ <div style={{ display: "flex", flexDirection: "column", alignItems: "start", gap: "8px" }}>
169
+ <div style={{ marginBottom: "8px", fontSize: "16px", fontWeight: "700" }}>Active</div>
170
+ <ButtonIcon shape={EButtonIconShape.SQUIRCLE} active>
171
+ <DefaulticonStrokePrdIcon32 paletteIndex={5} />
172
+ </ButtonIcon>
173
+ </div>
174
+
175
+ <div style={{ display: "flex", flexDirection: "column", alignItems: "start", gap: "8px" }}>
176
+ <div style={{ marginBottom: "8px", fontSize: "16px", fontWeight: "700" }}>Disabled</div>
177
+ <ButtonIcon shape={EButtonIconShape.SQUIRCLE} disabled>
178
+ <DefaulticonStrokePrdIcon32 paletteIndex={5} />
179
+ </ButtonIcon>
180
+ </div>
181
+ </div>
189
182
  );
190
183
  ```
191
184