@sberbusiness/triplex-next-mcp-server 1.44.0-beta → 1.46.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 (89) hide show
  1. package/README.md +7 -6
  2. package/bundle/components/AlertContext.md +11 -8
  3. package/bundle/components/AlertProcess.md +13 -10
  4. package/bundle/components/Badge.md +6 -3
  5. package/bundle/components/Button.md +46 -43
  6. package/bundle/components/ButtonDropdownExtended.md +3 -2
  7. package/bundle/components/ButtonIcon.md +7 -4
  8. package/bundle/components/Calendar.md +62 -59
  9. package/bundle/components/Caption.md +26 -23
  10. package/bundle/components/CardAction.md +615 -0
  11. package/bundle/components/Checkbox.md +32 -29
  12. package/bundle/components/CheckboxTreeExtended.md +9 -5
  13. package/bundle/components/Chip.md +51 -47
  14. package/bundle/components/ChipMonthYearPicker.md +344 -0
  15. package/bundle/components/ChipMultiselect.md +1251 -0
  16. package/bundle/components/ChipOptions.md +329 -0
  17. package/bundle/components/ChipSort.md +333 -0
  18. package/bundle/components/ChipSuggest.md +587 -0
  19. package/bundle/components/CodeText.md +26 -23
  20. package/bundle/components/Col.md +1 -2
  21. package/bundle/components/CollapsibleTree.md +8 -5
  22. package/bundle/components/DateField.md +222 -303
  23. package/bundle/components/DatePickerExtended.md +3 -1
  24. package/bundle/components/Dropdown.md +33 -28
  25. package/bundle/components/EmptyView.md +12 -5
  26. package/bundle/components/FooterPage.md +6 -2
  27. package/bundle/components/FormField.md +47 -42
  28. package/bundle/components/FormGroup.md +192 -52
  29. package/bundle/components/Header.md +491 -211
  30. package/bundle/components/HeaderPage.md +6 -2
  31. package/bundle/components/HelpBox.md +7 -1
  32. package/bundle/components/ImageGallery.md +33 -34
  33. package/bundle/components/ImageGalleryExtended.md +33 -34
  34. package/bundle/components/Island.md +16 -13
  35. package/bundle/components/IslandAccordion.md +443 -152
  36. package/bundle/components/IslandWidget.md +622 -157
  37. package/bundle/components/LightBox.md +14 -12
  38. package/bundle/components/Link.md +211 -96
  39. package/bundle/components/ListActionItem.md +7 -4
  40. package/bundle/components/ListItem.md +11 -8
  41. package/bundle/components/ListItemControlsButton.md +16 -13
  42. package/bundle/components/ListItemControlsButtonDropdown.md +9 -6
  43. package/bundle/components/ListItemSelectable.md +7 -4
  44. package/bundle/components/ListMasterFooter.md +8 -5
  45. package/bundle/components/ListMasterHeader.md +8 -5
  46. package/bundle/components/ListSortableItem.md +11 -8
  47. package/bundle/components/ListTableItem.md +7 -4
  48. package/bundle/components/LoaderMiddle.md +7 -4
  49. package/bundle/components/LoaderScreen.md +11 -8
  50. package/bundle/components/LoaderSmall.md +7 -4
  51. package/bundle/components/Marker.md +184 -69
  52. package/bundle/components/MarkerStatus.md +206 -88
  53. package/bundle/components/MaskedField.md +4 -5
  54. package/bundle/components/MasterTable.md +602 -0
  55. package/bundle/components/ModalWindow.md +7 -4
  56. package/bundle/components/MonthYearField.md +3 -1
  57. package/bundle/components/MultiselectField.md +2183 -132
  58. package/bundle/components/Notification.md +12 -9
  59. package/bundle/components/Overlay.md +9 -4
  60. package/bundle/components/Page.md +2 -2
  61. package/bundle/components/Pagination.md +15 -8
  62. package/bundle/components/Row.md +1 -1
  63. package/bundle/components/SelectExtendedField.md +3 -3
  64. package/bundle/components/SelectField.md +466 -148
  65. package/bundle/components/SliderExtended.md +39 -36
  66. package/bundle/components/SmallInput.md +194 -51
  67. package/bundle/components/Suggest.md +535 -99
  68. package/bundle/components/SwipeableArea.md +428 -79
  69. package/bundle/components/TableBasic.md +19 -11
  70. package/bundle/components/TabsExtended.md +24 -21
  71. package/bundle/components/Tag.md +6 -2
  72. package/bundle/components/Text.md +28 -25
  73. package/bundle/components/TextField.md +3 -4
  74. package/bundle/components/ThemeProvider.md +50 -1
  75. package/bundle/components/Title.md +26 -23
  76. package/bundle/components/Tooltip.md +24 -21
  77. package/bundle/components/TopOverlay.md +7 -4
  78. package/bundle/guides/codestyle.md +3 -0
  79. package/bundle/guides/context.md +84 -12
  80. package/bundle/guides/guidelines.md +321 -0
  81. package/bundle/guides/template.md +22 -12
  82. package/bundle/manifest.json +75 -17
  83. package/bundle/release-notes/1.45.0.mdx +117 -0
  84. package/bundle/release-notes/1.46.0.mdx +66 -0
  85. package/bundle/release-notes/1.47.0.mdx +7 -0
  86. package/bundle/tokens.json +6362 -0
  87. package/dist/index.js +177 -28
  88. package/dist/index.js.map +1 -1
  89. package/package.json +1 -1
@@ -1,10 +1,12 @@
1
1
  ---
2
2
  component: IslandAccordion
3
3
  category: Layout
4
- figma-node: ""
5
- figma-file: ""
6
- related: [Island, IslandWidget, ExpandAnimation, Step, Button]
7
- tokens: []
4
+ related: [Island, IslandWidget, ExpandAnimation, Step, ButtonIcon]
5
+ tokens:
6
+ - IslandAccordion.Type1_Header_Background_Hover
7
+ - IslandAccordion.Type2_Header_Background_Hover
8
+ - IslandAccordion.Type3_Header_Background_Hover
9
+ - IslandAccordion.Shadow_Focus
8
10
  stories: stories/IslandAccordion/IslandAccordion.stories.tsx
9
11
  version: "1.0"
10
12
  ---
@@ -13,213 +15,255 @@ version: "1.0"
13
15
 
14
16
  ## Назначение
15
17
 
16
- Аккордеон — список раскрывающихся `Island`-карточек. Каждая `Item` хранит
17
- собственное открытое состояние (либо управляется извне через `opened`),
18
- анимация раскрытия делегирована `ExpandAnimation`. Поддерживает step-индикатор
19
- (через `Step`), кнопку удаления (`onRemove`), статусы и размеры (из `EComponentSize`)
20
- и типы (из `EIslandType`) — оба прокидываются детям через контекст.
18
+ Список раскрывающихся карточек: `<ul>`, каждый элемент которого — `<li>` с карточкой `Island`,
19
+ кнопкой-заголовком и анимированно раскрываемым содержимым. Элемент дополнительно умеет
20
+ показывать номер шага со статусом (`Step`) и кнопку удаления.
21
21
 
22
- Используй когда: нужен последовательный список шагов/секций с раскрытием
23
- (wizard, FAQ-with-steps). Без step-индикатора — тоже валидный кейс.
24
- Не используй когда: нужен "плоский" аккордеон без визуала карточек —
25
- возьми кастомное решение или составь из `ExpandAnimation` напрямую.
22
+ Используй когда: набор логически сгруппированных блоков нужно уместить в ограниченном
23
+ пространстве и дать пользователю раскрывать их по одному — шаги оформления заявки, разделы
24
+ формы, FAQ.
26
25
 
27
- ---
28
-
29
- ## Файловая структура
30
-
31
- ```text
32
- src/components/IslandAccordion/
33
- ├── IslandAccordion.tsx # <ul>, Provider контекста (size/type), Object.assign → Item
34
- ├── IslandAccordionContext.ts # IIslandAccordionContext = { size, type }
35
- ├── index.ts # Barrel
36
- ├── components/
37
- │ ├── IslandAccordionItem.tsx # <li>, Island.Header с кнопкой + ExpandAnimation + Step/Remove
38
- │ ├── IslandAccordionContent.tsx # Тонкая обёртка над Island.Body
39
- │ ├── IslandAccordionFooter.tsx # Тонкая обёртка над Island.Footer
40
- │ └── index.ts
41
- └── styles/IslandAccordion.module.less
42
- ```
26
+ Не используй когда:
27
+ - Нужна одна нераскрывающаяся карточка — возьми `Island`.
28
+ - Нужен виджет, который сворачивается только в адаптиве, а не по клику пользователя — возьми
29
+ `IslandWidget`.
30
+ - Нужны вкладки (виден ровно один раздел, заголовки в ряд) — возьми `Tabs`.
31
+ - Нужна анимация раскрытия сама по себе, без карточки и заголовка — возьми `ExpandAnimation`.
32
+ - Нужно дерево с раскрывающимися узлами — возьми `CollapsibleTree`.
43
33
 
44
34
  ---
45
35
 
46
- ## Compound-структура
36
+ ## Состав
47
37
 
48
38
  ```tsx
49
- <IslandAccordion size={EComponentSize.MD} type={EIslandType.TYPE_1}>
50
- <IslandAccordion.Item
51
- id="step-1"
52
- title="Шаг первый"
53
- num={1}
54
- status={EStepStatus.COMPLETED}
55
- opened={isFirstOpen}
56
- onToggle={(next, id) => setFirstOpen(next)}
57
- onRemove={(id) => removeStep(id)}
58
- >
59
- <IslandAccordionContent>
60
- {/* ... произвольный контент шага ... */}
61
- </IslandAccordionContent>
62
- <IslandAccordionFooter>
63
- <Button>Следующий шаг</Button>
64
- </IslandAccordionFooter>
65
- </IslandAccordion.Item>
66
- </IslandAccordion>
39
+ <IslandAccordion size type> // <ul>, задаёт size и type через контекст
40
+ <IslandAccordion.Item id title> // <li> + Island + кнопка-заголовок + ExpandAnimation
41
+ <IslandAccordion.Item.Content> // Island.Body внутри ExpandAnimation
42
+ <IslandAccordion.Item.Footer> // Island.Footer внутри ExpandAnimation
67
43
  ```
68
44
 
45
+ Из barrel экспортируются и «плоские» имена: `IslandAccordionItem`, `IslandAccordionContent`,
46
+ `IslandAccordionFooter` — вместе с точечными псевдонимами это один и тот же компонент.
47
+
48
+ `Content` и `Footer` необязательны: содержимым `Item` может быть произвольный узел, он попадёт
49
+ внутрь `ExpandAnimation` как есть. Но тогда на него не подействуют внутренние отступы аккордеона
50
+ (они заданы на `.body` / `.footer`).
51
+
69
52
  ---
70
53
 
71
54
  ## Варианты и props
72
55
 
73
- ### IslandAccordion — `IIslandAccordionProps extends HTMLAttributes<HTMLUListElement>, Pick<IIslandProps, "type">`
74
-
75
- | Prop | Тип | Default | Описание |
76
- |---|---|---|---|
77
- | `size` | `EComponentSize` | `MD` | Передаётся детям через контекст. |
78
- | `type` | `EIslandType` | `TYPE_1` | Передаётся детям через контекст. |
79
- | `...ulAttributes` | — | — | Обычные `<ul>`-атрибуты. |
56
+ ### IslandAccordion
80
57
 
81
- ### IslandAccordion.Item — `IIslandAccordionItemProps extends Omit<HTMLAttributes<HTMLLIElement>, "title">`
58
+ Обязательных props нет.
82
59
 
83
- | Prop | Тип | Default | Описание |
60
+ | Prop | Тип | По умолчанию | Описание |
84
61
  |---|---|---|---|
85
- | `title` | `ReactNode` | — | Заголовок. Обязателен. |
86
- | `id` | `string` | — | Уникальный id (обязателен, используется как ключ и для ARIA). |
87
- | `num` | `number` | — | Цифра в кружке Step. Применяется только вместе со `status`. |
88
- | `status` | `EStepStatus` | — | Статус шага; рендерит `Step` при наличии `num`. |
89
- | `stepHint` | `string` | — | Подсказка рядом с цифрой. Игнорируется в `disabled`. |
90
- | `opened` | `boolean` | — (uncontrolled) | Управляемое состояние раскрытия. Если не передан — item uncontrolled, хранит своё состояние через `useState`. |
91
- | `disabled` | `boolean` | `false` | Блокирует клик по заголовку и удаление. |
92
- | `onToggle` | `(newOpened, id) => void` | — | Срабатывает даже в disabled? Нет — ранний return при `disabled`. |
93
- | `onRemove` | `(id) => void` | — | Рендерит крестик справа; при клике вызывает `onRemove(id)`. |
94
- | `transitionProps` | `IExpandAnimationProps["transitionProps"]` | — | Проброс в `ExpandAnimation`. |
95
-
96
- ### IslandAccordion.Item.Content / Footer
97
-
98
- Тонкие обёртки над `Island.Body` / `Island.Footer` с добавлением нужных
99
- отступов (`styles.body`, `styles.footer`).
62
+ | `size` | `EComponentSize` | `EComponentSize.MD` | Размер всех элементов: паддинги, скругление, отступ между элементами, размер заголовка |
63
+ | `type` | `EIslandType` | `EIslandType.TYPE_1` | Визуальный тип карточки `Island` у элементов; влияет и на цвет hover заголовка |
100
64
 
101
- ---
65
+ Расширяет `React.HTMLAttributes<HTMLUListElement>` — `className`, `style`, `aria-*`, `data-*`
66
+ и обработчики попадают на корневой `<ul>`.
102
67
 
103
- ## Ключевые особенности реализации
68
+ Обе props раздаются элементам **через контекст**, а не через клонирование детей, поэтому между
69
+ `IslandAccordion` и его `Item` можно вставлять произвольные обёртки. Обратная сторона: `Item`,
70
+ отрендеренный вне `IslandAccordion`, берёт значения по умолчанию из
71
+ `initialIslandAccordionContext` (`MD` + `TYPE_1`) и не ломается.
104
72
 
105
- ### Controlled / uncontrolled по `opened`
73
+ ### IslandAccordion.Item
106
74
 
107
- - Если `opened !== undefined` — controlled; `useEffect` синхронизирует
108
- внутренний `isOpen` с `opened` при изменении.
109
- - Если `opened === undefined` — uncontrolled; `handleHeaderClick` переключает
110
- локальный `isOpen` через `setIsOpen(newOpened)`.
111
- - В обоих случаях вызывается `onToggle(newOpened, id)`.
75
+ | Prop | Тип | По умолчанию | Описание |
76
+ |---|---|---|---|
77
+ | `title` | `React.ReactNode` | — | **Обязательный.** Содержимое заголовка. Оборачивается в `Title` с `tag="div"` |
78
+ | `id` | `string` | — | **Обязательный.** Проставляется на корневой `<li>` и передаётся в `onToggle` / `onRemove` |
79
+ | `num` | `number` | `undefined` | Номер в кружке слева от заголовка. Рендерится только вместе с `status` |
80
+ | `status` | `EStepStatus` | `undefined` | Статус шага (цвет кружка). Без него кружок не рендерится, даже если задан `num` |
81
+ | `stepHint` | `string` | `undefined` | Подсказка (`Tooltip` от `Step`) по наведению на кружок. Игнорируется при `disabled` |
82
+ | `opened` | `boolean` | `undefined` | Раскрыт ли элемент. Передан — элемент управляемый, не передан — раскрывается сам |
83
+ | `disabled` | `boolean` | `false` | Блокирует раскрытие и удаление, гасит подсказку шага, оставляет содержимое свёрнутым |
84
+ | `onToggle` | `(newOpened: boolean, id: string) => void` | `undefined` | Клик по заголовку. Первым аргументом — состояние, в которое элемент переходит |
85
+ | `onRemove` | `(id: string) => void` | `undefined` | Клик по кнопке удаления. Сама кнопка рендерится, **только** если колбэк передан |
86
+ | `transitionProps` | `IExpandAnimationProps["transitionProps"]` | `undefined` | Props `Transition` (react-transition-group), пробрасываются в `ExpandAnimation` |
112
87
 
113
- ### Размеры через маппинги
88
+ Расширяет `Omit<React.HTMLAttributes<HTMLLIElement>, "title">` — `title` переопределён на
89
+ `ReactNode`, поэтому HTML-атрибут `title` на `<li>` через него не задать.
114
90
 
115
- ```ts
116
- SIZE_TO_TITLE_SIZE_MAP = { SM: H3, MD: H3, LG: H2 } // Title внутри заголовка
117
- ```
91
+ ### Управляемый и неуправляемый режим
118
92
 
119
- Размер `size` также влияет на общий layout через CSS-классы (`.sm`, `.md`, `.lg`).
93
+ Один и тот же prop `opened` переключает режим:
120
94
 
121
- ### Типы через локальный map
95
+ - **`opened` не передан** — элемент хранит раскрытие сам. Клик по заголовку меняет его и вызывает
96
+ `onToggle`.
97
+ - **`opened` передан** — раскрытие задаёт потребитель. Клик по заголовку **только** вызывает
98
+ `onToggle(!текущее, id)`; собственное состояние элемент не меняет и ждёт нового `opened`. Если
99
+ потребитель не обновит prop, элемент не раскроется.
122
100
 
123
- ```ts
124
- TYPE_TO_CLASS_NAME_MAP = { TYPE_1: styles.type1, TYPE_2: styles.type2, TYPE_3: styles.type3 }
125
- ```
101
+ Синхронизация `opened` → внутреннее состояние идёт **в фазе рендера** (паттерн React «adjusting
102
+ state when a prop changes»), а не в `useEffect`, поэтому промежуточный кадр со старым состоянием
103
+ не отрисовывается. Если `opened` из заданного становится `undefined`, элемент сохраняет последнее
104
+ синхронизированное значение и дальше управляет им сам.
126
105
 
127
- Дублирует `Island.utils.mapTypeToClassName`, но использует **локальные** стили
128
- аккордеона (в `IslandAccordion.module.less`), а не Island'овские.
106
+ «Открыт ровно один элемент» встроенным поведением **не является**: аккордеон не знает о состоянии
107
+ соседей. Потребитель держит id открытого элемента у себя и раздаёт `opened={id === openId}` —
108
+ см. story `OnlyOneOpenAtATime`.
129
109
 
130
- ### Чип `hoverable` на свёрнутом заголовке
110
+ ### Удаление
131
111
 
132
- ```tsx
133
- className={clsx(styles.header, { hoverable: !isOpen })}
134
- ```
112
+ `onRemove` только уведомляет — элемент себя не удаляет. Убрать его из списка должен потребитель
113
+ (см. story `Removable`).
135
114
 
136
- Когда item закрыт — иконка chevron реагирует на hover (меняет цвет через
137
- правила из `@sberbusiness/icons-next/styles/icons.css`). Когда открыт —
138
- класс не ставится, иконка статична.
115
+ ### Размеры
139
116
 
140
- ### ARIA-связка через `uniqueId`
117
+ | `size` | Паддинги заголовка / тела | `border-radius` | Отступ между элементами | Размер `Title` |
118
+ |---|---|---|---|---|
119
+ | `SM` | 16px | 16px | 16px | `H3` |
120
+ | `MD` | 24px | 24px | 16px | `H3` |
121
+ | `LG` | 32px | 32px | 24px | `H2` |
141
122
 
142
- - `instanceId` генерируется через `useRef(uniqueId()).current` — НЕ SSR-safe.
143
- Для SSR-сценариев потребителю нужно передавать стабильный `id`.
144
- - `aria-expanded`, `aria-controls` (кнопка → контент), `role="region"`,
145
- `aria-labelledby` (контент → кнопка) проставляются.
146
- - Контент рендерится внутри `ExpandAnimation` с этими ARIA-атрибутами;
147
- даже в свёрнутом состоянии `ExpandAnimation` может оставлять элемент в DOM
148
- (зависит от реализации) — не полагайся на "нет DOM = нет фокусируемых".
123
+ В раскрытом состоянии нижний паддинг заголовка уменьшается (12 / 16 / 24px), чтобы заголовок и
124
+ тело не расходились.
149
125
 
150
- ### Кнопка удаления — снаружи карточки
126
+ **Адаптив** (`max-width: @screen-sm-max`) устроен в двух местах и они должны сходиться:
127
+ - LESS схлопывает `MD` до метрик `SM`, а `LG` — до метрик `MD`;
128
+ - `useMobileView()` в TSX принудительно ставит `Title` размер `H3` при любом `size`.
151
129
 
152
- ```tsx
153
- {onRemove && (
154
- <span className={styles.remove}>
155
- <ButtonIcon onClick={handleRemoveClick} disabled={disabled} title="Удалить">
156
- <CrossStrokeSrvIcon24 paletteIndex={5} />
157
- </ButtonIcon>
158
- </span>
159
- )}
160
- ```
130
+ ---
161
131
 
162
- Это **хардкод русского текста `title="Удалить"`** — нарушение правил
163
- мультиязычности (`aria-label`/`title` не должны быть на конкретном языке).
164
- **Technical debt.** Кандидат на рефакторинг: вынести в prop `removeTitle`
165
- или полностью убрать, оставив потребителю.
132
+ ## Дизайн-токены
166
133
 
167
- ### `// eslint-disable-next-line react-hooks/refs`
134
+ Переопределяются через `ThemeProvider` (prop `tokens`) — см. `ThemeProvider-ai.md` →
135
+ «Как переопределять токены». Значения по умолчанию —
136
+ `src/components/DesignTokens/components/IslandAccordion.ts`.
168
137
 
169
- ```ts
170
- const instanceId = useRef(uniqueId()).current;
138
+ ```text
139
+ IslandAccordion.Type1_Header_Background_Hover
140
+ IslandAccordion.Type2_Header_Background_Hover
141
+ IslandAccordion.Type3_Header_Background_Hover
142
+ IslandAccordion.Shadow_Focus
171
143
  ```
172
144
 
173
- Паттерн создаёт id один раз на монтировании. ESLint ругается, но это
174
- осознанный legacy-подход; в новом коде лучше `React.useId()`.
145
+ Hover-фон заголовка применяется только к **свёрнутому** элементу (`&.type1:not(.opened) .header:hover`).
146
+ `Shadow_Focus` — внутренняя рамка на `.header:focus-visible`. Фон и тень самой карточки берутся из
147
+ токенов `Island` (`Island.Type1_Background` и т.д.), собственных у аккордеона нет. Скругления и
148
+ отступы токенами не описаны — они заданы литералами в `styles/IslandAccordion.module.less`.
175
149
 
176
150
  ---
177
151
 
178
152
  ## Инварианты
179
153
 
180
- - **`IslandAccordionContext` shape** (`size`, `type`) — при изменении подхватят все items; передать извне только `type` и `size` — контекст public.
181
- - **`Object.assign` на `IslandAccordion` → `Item`**, на `Item` → `Content`/`Footer`. Двухуровневый compound.
182
- - **`<ul>` / `<li>`** — семантика списка, её нельзя заменить на `<div>` без согласования (ломает скринридеры и существующую стилизацию).
183
- - **`onRemove` title="Удалить"** — захардкоженный текст. Не дублировать паттерн в других компонентах; при переводе библиотеки — первая точка исправления.
184
- - **`forwardRef`** на корне и на `Item`; `Content`/`Footer` — `React.FC`.
185
- - **`opened` управляемое/неуправляемое поведение** — не нарушать (ломает интеграции).
186
- - **`id` обязателен** на Item — используется как React key и в ARIA.
187
- - **`uniqueId` из lodash** — не SSR-safe; потребители для SSR должны передавать `id` явно.
154
+ - **`forwardRef`** у `IslandAccordion` (→ `<ul>`) и `IslandAccordionItem` (→ `<li>`) — не убирать.
155
+ У `IslandAccordionContent` и `IslandAccordionFooter` его нет: они объявлены как `React.FC`.
156
+ Добавление `forwardRef` этим двум — изменение публичного контракта.
157
+ - **Корневые элементы `<ul>` и `<li>`** — на них держатся роли `list` / `listitem` и селекторы
158
+ отступов между элементами (`.item + .item.md`). Смена тега ломает и семантику, и вёрстку.
159
+ - **`id` у `Item` обязателен и уникален в пределах страницы** — он идёт прямо в DOM-атрибут `id`
160
+ корневого `<li>`, а не только в колбэки. Дубли дадут невалидный DOM.
161
+ - **Публичный API:** `IslandAccordion`, `IslandAccordionItem`, `IslandAccordionContent`,
162
+ `IslandAccordionFooter` и их интерфейсы props экспортируются из
163
+ `src/components/IslandAccordion/index.ts`. `IslandAccordionContext`,
164
+ `IIslandAccordionContext` и `initialIslandAccordionContext` в barrel **не** реэкспортируются —
165
+ это внутренний слой, менять его можно свободно.
166
+ - **`margin-top: 0 !important` у `.footer`** гасит правило отступов `Island`
167
+ (`.island.{size} .islandBody + .islandFooter`), у которого специфичность в четыре класса.
168
+ Убрать `!important` без переписывания селектора нельзя — подвал разъедется. Подробности —
169
+ в комментарии в LESS и в `Island-ai.md` → «Ограничения».
170
+ - **`.body` / `.footer` скрыты через `visibility: hidden`**, пока у элемента нет класса `.opened` —
171
+ «чтобы focus не попадал во внутренние элементы в закрытом состоянии» (комментарий в LESS).
172
+ `ExpandAnimation` независимо ставит `visibility: hidden` инлайном в фазе `exited`; классы
173
+ аккордеона переключаются синхронно с состоянием, инлайн-стиль — по завершении анимации.
174
+ Удаление любого из двух механизмов меняет момент, начиная с которого содержимое перестаёт
175
+ ловить фокус.
176
+ - **Глобальный класс `hoverable`** на кнопке-заголовке приходит из `@sberbusiness/icons-next` и
177
+ задаёт цвет каретки при наведении. Он не из `styles` — при чистке «мёртвых» классов не удалять.
178
+ - **Кружок шага рендерится только при `status` И `num`.** Условие намеренно записано тернарником
179
+ с явными проверками (`num !== undefined && num !== 0`), а не через `&&`: выражение `status && num`
180
+ при `num === 0` вернуло бы `0`, и React отрисовал бы текст «0». Намеренна здесь только форма записи.
181
+ Сам пропуск кружка при `num === 0` — **унаследованная особенность, а не желаемый контракт**:
182
+ ноль выглядит валидным номером шага, но исторически не рендерится. Поведение зафиксировано тестом
183
+ как есть; менять его — отдельная задача с записью в release notes, потому что это изменение
184
+ наблюдаемого поведения.
188
185
 
189
186
  ---
190
187
 
191
188
  ## Accessibility
192
189
 
193
- - **`<button>`-заголовок** — полная клавиатурная навигация (Tab, Enter, Space).
194
- - `aria-expanded` на заголовке, `aria-controls` → id контента.
195
- - `role="region"` + `aria-labelledby` на контенте — скринридер объявляет секцию при входе.
196
- - `disabled` — нативный HTML-атрибут на `<button>` + класс `.disabled` для CSS.
197
- - **Иконка chevron** — `aria-hidden="true"`, правильно (вся семантика на кнопке).
198
- - **Кнопка удаления** — title="Удалить" (см. debt выше). Для корректной мультиязычности потребитель должен переопределить через передаваемые пропы в будущем или не использовать `onRemove`.
190
+ - Заголовок — нативный `<button type="button">` внутри `Island.Header`. Клавиатура работает
191
+ штатно (`Enter` / `Space`), собственных обработчиков клавиш компонент не ставит.
192
+ - Связка заголовка и содержимого: у кнопки `id` и `aria-controls`, у раскрывающейся области
193
+ `role="region"` и `aria-labelledby`. Оба id генерируются автоматически
194
+ (`IslandAccordionItem-<n>-header` / `-body`) — потребителю их задавать не нужно и нельзя.
195
+ - `aria-expanded` на кнопке отражает **запрошенное** состояние. При `disabled` содержимое остаётся
196
+ свёрнутым (`expanded={isOpen && !disabled}`), а `aria-expanded` может показывать `true`, если
197
+ передан `opened` — это известное расхождение.
198
+ - Каретка помечена `aria-hidden="true"` — она декоративная.
199
+ - Навигации стрелками между элементами нет (в WAI-ARIA Accordion Pattern она опциональна). Между
200
+ заголовками ходят `Tab`.
201
+ - Список не получает `aria-label` автоматически. Если на странице несколько списков, передай его
202
+ сам в `IslandAccordion`.
203
+ - Фокус при раскрытии никуда не переносится — он остаётся на заголовке.
204
+ - **Кнопка удаления имеет захардкоженный `title="Удалить"`** — отступление от правила
205
+ мультиязычности `docs/ai/codestyle.md`. Задать его снаружи сейчас нельзя; починка требует нового
206
+ prop, то есть изменения публичного API.
199
207
 
200
208
  ---
201
209
 
202
- ## Дизайн-токены
210
+ ## Связанные компоненты
203
211
 
204
- Нет прямых — цвет/тень приходят от `Island` (`--triplex-next-Island-*`).
205
- Chevron-цвета — из `@sberbusiness/icons-next` (palette 5).
212
+ ### Части IslandAccordion (своего AI.md не имеют)
206
213
 
207
- ---
214
+ - `IslandAccordionItem` (он же `IslandAccordion.Item`) — раскрывающийся элемент. Вся логика
215
+ состояния, статуса и удаления живёт здесь.
216
+ - `IslandAccordionContent` (он же `IslandAccordion.Item.Content`) — обёртка над `Island.Body`,
217
+ добавляющая класс с отступами аккордеона. Собственных props, кроме `children`, нет:
218
+ `className` и `...rest` она не принимает.
219
+ - `IslandAccordionFooter` (он же `IslandAccordion.Item.Footer`) — обёртка над `Island.Footer`.
220
+ `IIslandAccordionFooterProps extends IIslandFooterProps`, `className` мерджится с базовым.
208
221
 
209
- ## Связанные компоненты
222
+ ### Альтернативы (в `related`)
223
+
224
+ - `Island` — та же карточка без раскрытия. `IslandAccordion` строится на нём и опирается на его
225
+ вёрстку: гасит отступ `Island.Footer` через `!important` и обнуляет паддинги `Island` в свою
226
+ пользу, поэтому правки селекторов отступов `Island` ломают аккордеон молча.
227
+ - `IslandWidget` — виджет на `Island`, который сворачивается в адаптиве, а не по клику. Свои
228
+ Header / Body / Footer, частями `Island` не пользуется.
229
+
230
+ ### Контракты по рендеру (в `related`)
210
231
 
211
- - `Island` — корневая карточка каждого Item.
212
- - `ExpandAnimation` — раскрытие контента.
213
- - `Step` — номер+статус в заголовке.
214
- - `Typography.Title` — заголовок.
215
- - `ButtonIcon` — кнопка удаления.
232
+ - `ExpandAnimation` — анимация раскрытия содержимого. Получает `role="region"`, `aria-labelledby`
233
+ и `transitionProps` от элемента. Именно он держит содержимое в DOM в свёрнутом состоянии
234
+ (`height: 0` + `visibility: hidden`), а не размонтирует его.
235
+ - `Step` — кружок с номером и статусом слева от заголовка. `position` всегда `XFirst`,
236
+ `size` берётся из контекста аккордеона, `stepHint` уходит в `children` и превращается в `Tooltip`.
237
+ - `ButtonIcon` — кнопка удаления справа от карточки (абсолютно спозиционирована **за** её
238
+ границами: `right: -36px` / `-40px`, поэтому у аккордеона с `onRemove` нужен внешний отступ справа).
239
+
240
+ ### Другие направления из «Не используй когда» (в `related` не входят)
241
+
242
+ - `Tabs` — если виден ровно один раздел, а заголовки стоят в ряд.
243
+ - `CollapsibleTree`, `CollapsibleTreeExtended` — если нужна древовидная структура.
216
244
 
217
245
  ---
218
246
 
219
247
  ## Stories
220
248
 
221
- `stories/IslandAccordion/IslandAccordion.stories.tsx`
222
- Примеры: `stories/IslandAccordion/examples/`
249
+ Основные истории: `stories/IslandAccordion/IslandAccordion.stories.tsx`
250
+ Файлы примеров: `stories/IslandAccordion/examples/`
251
+
252
+ | Story | Example file | Что демонстрирует |
253
+ |---|---|---|
254
+ | `Playground` | — | Интерактивный контроль `size`, `type`, `status`, `removable`, `disabled`, `title` |
255
+ | `Default` | `DefaultExample.tsx` | Минимальный элемент: заголовок, `Content`, `Footer` с кнопками |
256
+ | `Sizes` | `SizesExample.tsx` | Размеры SM / MD / LG: паддинги, скругление, размер заголовка |
257
+ | `Disabled` | `DisabledExample.tsx` | `disabled`: заголовок не кликается, содержимое свёрнуто |
258
+ | `Removable` | `RemovableExample.tsx` | `onRemove`: кнопка удаления и удаление элемента силами потребителя |
259
+ | `WithStatus` | `WithStatusExample.tsx` | Все значения `EStepStatus` в кружке шага |
260
+ | `WithStepHint` | `WithStepHintExample.tsx` | `stepHint`: подсказка `Tooltip` по наведению на кружок |
261
+ | `OnlyOneOpenAtATime` | `OnlyOneOpenAtATimeExample.tsx` | Управляемый режим: одновременно раскрыт не больше одного элемента |
262
+ | `VisualTests` | — | Скриншот-регрессия: раскрытый и свёрнутый элементы рядом |
263
+
264
+ Скриншот-регрессия снимается со stories `Default`, `Sizes`, `Disabled`, `Removable`,
265
+ `WithStatus`, `VisualTests`. `Playground`, `WithStepHint` и `OnlyOneOpenAtATime` помечены
266
+ `testRunner: { skip: true }` — они интерактивные и дают нестабильный кадр.
223
267
 
224
268
  ---
225
269
 
@@ -227,4 +271,251 @@ Chevron-цвета — из `@sberbusiness/icons-next` (palette 5).
227
271
 
228
272
  | Дата | Изменение |
229
273
  |---|---|
230
- | 2026-04-18 | Создан документ |
274
+ | 2026-09-09 | Создан документ. AI-рефакторинг: JSDoc на props, компонентах и контексте; синхронизация `opened` перенесена из `useEffect` в фазу рендера (промежуточный кадр со старым состоянием больше не отрисовывается); автогенерируемые id заголовка и содержимого получили вид `IslandAccordionItem-<n>-header` / `-body`; при `num={0}` со статусом больше не отрисовывается текст «0»; добавлены unit-тесты на `IslandAccordionItem`, контекст, части и публичный контракт `IslandAccordion`. Публичный API не изменён. |
275
+
276
+ ---
277
+
278
+ ## Примеры использования
279
+
280
+ ### Default
281
+
282
+ `stories/IslandAccordion/examples/DefaultExample.tsx`
283
+
284
+ ```tsx
285
+ import React from "react";
286
+ import { IslandAccordion, Button, EButtonTheme, EComponentSize } from "@sberbusiness/triplex-next";
287
+
288
+ export const DefaultExample = () => (
289
+ <IslandAccordion>
290
+ <IslandAccordion.Item id="island-accordion-default" num={1} title="Title">
291
+ <IslandAccordion.Item.Content>Content</IslandAccordion.Item.Content>
292
+ <IslandAccordion.Item.Footer>
293
+ <Button theme={EButtonTheme.LINK} size={EComponentSize.MD}>
294
+ Button link text
295
+ </Button>
296
+ <Button theme={EButtonTheme.SECONDARY} size={EComponentSize.MD}>
297
+ Button text
298
+ </Button>
299
+ <Button theme={EButtonTheme.GENERAL} size={EComponentSize.MD}>
300
+ Button text
301
+ </Button>
302
+ </IslandAccordion.Item.Footer>
303
+ </IslandAccordion.Item>
304
+ </IslandAccordion>
305
+ );
306
+ ```
307
+
308
+ ### Sizes
309
+
310
+ `stories/IslandAccordion/examples/SizesExample.tsx`
311
+
312
+ ```tsx
313
+ import React from "react";
314
+ import { IslandAccordion, Button, EButtonTheme, EComponentSize } from "@sberbusiness/triplex-next";
315
+
316
+ const SIZES = Object.values(EComponentSize);
317
+
318
+ export const SizesExample = () => (
319
+ <div style={{ display: "flex", flexDirection: "column", gap: 16 }}>
320
+ {SIZES.map((size, index) => (
321
+ <div key={size}>
322
+ <div style={{ marginBottom: "8px", fontSize: "16px", fontWeight: "700" }}>{size.toUpperCase()}</div>
323
+ <IslandAccordion size={size}>
324
+ <IslandAccordion.Item id={`island-accordion-sizes-${size}`} num={index + 1} title="Title">
325
+ <IslandAccordion.Item.Content>Content</IslandAccordion.Item.Content>
326
+ <IslandAccordion.Item.Footer>
327
+ <Button theme={EButtonTheme.LINK} size={size}>
328
+ Button link text
329
+ </Button>
330
+ <Button theme={EButtonTheme.SECONDARY} size={size}>
331
+ Button text
332
+ </Button>
333
+ <Button theme={EButtonTheme.GENERAL} size={size}>
334
+ Button text
335
+ </Button>
336
+ </IslandAccordion.Item.Footer>
337
+ </IslandAccordion.Item>
338
+ </IslandAccordion>
339
+ </div>
340
+ ))}
341
+ </div>
342
+ );
343
+ ```
344
+
345
+ ### Disabled
346
+
347
+ `stories/IslandAccordion/examples/DisabledExample.tsx`
348
+
349
+ ```tsx
350
+ import React from "react";
351
+ import { IslandAccordion, Button, EButtonTheme, EComponentSize } from "@sberbusiness/triplex-next";
352
+
353
+ export const DisabledExample = () => (
354
+ <IslandAccordion>
355
+ <IslandAccordion.Item id="island-accordion-disabled" num={1} title="Title" disabled>
356
+ <IslandAccordion.Item.Content>Контент аккордеона</IslandAccordion.Item.Content>
357
+ <IslandAccordion.Item.Footer>
358
+ <Button theme={EButtonTheme.GENERAL} size={EComponentSize.MD}>
359
+ Button Name
360
+ </Button>
361
+ </IslandAccordion.Item.Footer>
362
+ </IslandAccordion.Item>
363
+ </IslandAccordion>
364
+ );
365
+ ```
366
+
367
+ ### Removable
368
+
369
+ `stories/IslandAccordion/examples/RemovableExample.tsx`
370
+
371
+ ```tsx
372
+ import React from "react";
373
+ import { IslandAccordion, Button, EButtonTheme, EComponentSize } from "@sberbusiness/triplex-next";
374
+
375
+ export const RemovableExample = () => {
376
+ const handleRemove = (id: string) => document.getElementById(id)?.remove();
377
+
378
+ return (
379
+ <IslandAccordion>
380
+ <IslandAccordion.Item id="island-accordion-removable" num={1} title="Title" onRemove={handleRemove}>
381
+ <IslandAccordion.Item.Content>Контент аккордеона</IslandAccordion.Item.Content>
382
+ <IslandAccordion.Item.Footer>
383
+ <Button theme={EButtonTheme.LINK} size={EComponentSize.MD}>
384
+ Button link text
385
+ </Button>
386
+ <Button theme={EButtonTheme.SECONDARY} size={EComponentSize.MD}>
387
+ Button text
388
+ </Button>
389
+ <Button theme={EButtonTheme.GENERAL} size={EComponentSize.MD}>
390
+ Button text
391
+ </Button>
392
+ </IslandAccordion.Item.Footer>
393
+ </IslandAccordion.Item>
394
+ </IslandAccordion>
395
+ );
396
+ };
397
+ ```
398
+
399
+ ### WithStatus
400
+
401
+ `stories/IslandAccordion/examples/WithStatusExample.tsx`
402
+
403
+ ```tsx
404
+ import React from "react";
405
+ import { IslandAccordion, Button, EButtonTheme, EComponentSize, EStepStatus } from "@sberbusiness/triplex-next";
406
+
407
+ const ITEMS = [
408
+ { id: "accordion-status-0", status: EStepStatus.DEFAULT },
409
+ { id: "accordion-status-1", status: EStepStatus.DONE },
410
+ { id: "accordion-status-2", status: EStepStatus.ACTIVE },
411
+ { id: "accordion-status-3", status: EStepStatus.ERROR },
412
+ { id: "accordion-status-4", status: EStepStatus.DISABLED },
413
+ { id: "accordion-status-5", status: EStepStatus.WARNING },
414
+ ];
415
+
416
+ export const WithStatusExample = () => (
417
+ <IslandAccordion>
418
+ {ITEMS.map(({ id, status }, index) => (
419
+ <IslandAccordion.Item key={id} id={id} num={index + 1} status={status} title="Title">
420
+ <IslandAccordion.Item.Content>Content</IslandAccordion.Item.Content>
421
+ <IslandAccordion.Item.Footer>
422
+ <Button theme={EButtonTheme.LINK} size={EComponentSize.MD}>
423
+ Button link text
424
+ </Button>
425
+ <Button theme={EButtonTheme.SECONDARY} size={EComponentSize.MD}>
426
+ Button text
427
+ </Button>
428
+ <Button theme={EButtonTheme.GENERAL} size={EComponentSize.MD}>
429
+ Button text
430
+ </Button>
431
+ </IslandAccordion.Item.Footer>
432
+ </IslandAccordion.Item>
433
+ ))}
434
+ </IslandAccordion>
435
+ );
436
+ ```
437
+
438
+ ### WithStepHint
439
+
440
+ `stories/IslandAccordion/examples/WithStepHintExample.tsx`
441
+
442
+ ```tsx
443
+ import React from "react";
444
+ import { IslandAccordion, Button, EButtonTheme, EComponentSize, EStepStatus } from "@sberbusiness/triplex-next";
445
+
446
+ export const WithStepHintExample = () => (
447
+ <IslandAccordion>
448
+ <IslandAccordion.Item
449
+ id="island-accordion-step-hint"
450
+ num={1}
451
+ status={EStepStatus.DONE}
452
+ title="Title"
453
+ stepHint="Текст подсказки."
454
+ >
455
+ <IslandAccordion.Item.Content>Content</IslandAccordion.Item.Content>
456
+ <IslandAccordion.Item.Footer>
457
+ <Button theme={EButtonTheme.LINK} size={EComponentSize.MD}>
458
+ Button link text
459
+ </Button>
460
+ <Button theme={EButtonTheme.SECONDARY} size={EComponentSize.MD}>
461
+ Button text
462
+ </Button>
463
+ <Button theme={EButtonTheme.GENERAL} size={EComponentSize.MD}>
464
+ Button text
465
+ </Button>
466
+ </IslandAccordion.Item.Footer>
467
+ </IslandAccordion.Item>
468
+ </IslandAccordion>
469
+ );
470
+ ```
471
+
472
+ ### OnlyOneOpenAtATime
473
+
474
+ `stories/IslandAccordion/examples/OnlyOneOpenAtATimeExample.tsx`
475
+
476
+ ```tsx
477
+ import React, { useState } from "react";
478
+ import { IslandAccordion, Button, EButtonTheme, EComponentSize, EStepStatus } from "@sberbusiness/triplex-next";
479
+
480
+ const ITEMS = [
481
+ { id: "accordion-single-0", status: EStepStatus.DEFAULT },
482
+ { id: "accordion-single-1", status: EStepStatus.DONE },
483
+ { id: "accordion-single-2", status: EStepStatus.ACTIVE },
484
+ ];
485
+
486
+ export const OnlyOneOpenAtATimeExample = () => {
487
+ const [openItemId, setOpenItemId] = useState<string>();
488
+
489
+ const handleToggle = (open: boolean, id: string) => setOpenItemId(open ? id : undefined);
490
+
491
+ return (
492
+ <IslandAccordion>
493
+ {ITEMS.map(({ id, status }, index) => (
494
+ <IslandAccordion.Item
495
+ key={id}
496
+ id={id}
497
+ num={index + 1}
498
+ status={status}
499
+ title="Title"
500
+ opened={id === openItemId}
501
+ onToggle={handleToggle}
502
+ >
503
+ <IslandAccordion.Item.Content>Content</IslandAccordion.Item.Content>
504
+ <IslandAccordion.Item.Footer>
505
+ <Button theme={EButtonTheme.LINK} size={EComponentSize.MD}>
506
+ Button link text
507
+ </Button>
508
+ <Button theme={EButtonTheme.SECONDARY} size={EComponentSize.MD}>
509
+ Button text
510
+ </Button>
511
+ <Button theme={EButtonTheme.GENERAL} size={EComponentSize.MD}>
512
+ Button text
513
+ </Button>
514
+ </IslandAccordion.Item.Footer>
515
+ </IslandAccordion.Item>
516
+ ))}
517
+ </IslandAccordion>
518
+ );
519
+ };
520
+ ```
521
+