@sberbusiness/triplex-next-mcp-server 1.50.0-beta → 1.53.1

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 (74) hide show
  1. package/README.md +1 -0
  2. package/bundle/components/AlertProcess.md +6 -3
  3. package/bundle/components/AlertProcessSpoiler.md +216 -0
  4. package/bundle/components/Badge.md +4 -3
  5. package/bundle/components/ButtonDropdown.md +5 -3
  6. package/bundle/components/Carousel.md +671 -0
  7. package/bundle/components/CarouselExtended.md +7 -5
  8. package/bundle/components/CarouselIndicators.md +666 -0
  9. package/bundle/components/CarouselItem.md +579 -0
  10. package/bundle/components/CarouselNextButton.md +633 -0
  11. package/bundle/components/CarouselPrevButton.md +633 -0
  12. package/bundle/components/CarouselViewport.md +631 -0
  13. package/bundle/components/CheckboxTreeExtended.md +21 -7
  14. package/bundle/components/CheckboxTreeExtendedCheckbox.md +591 -0
  15. package/bundle/components/CheckboxTreeExtendedNode.md +577 -0
  16. package/bundle/components/Chip.md +1 -0
  17. package/bundle/components/ChipDatePicker.md +332 -0
  18. package/bundle/components/ChipDatePickerTarget.md +316 -0
  19. package/bundle/components/ChipMonthYearPicker.md +4 -3
  20. package/bundle/components/ChipMonthYearPickerTarget.md +318 -0
  21. package/bundle/components/ChipMultiselect.md +63 -73
  22. package/bundle/components/ChipSelect.md +385 -0
  23. package/bundle/components/ChipSelectTarget.md +388 -0
  24. package/bundle/components/ChipSuggest.md +10 -16
  25. package/bundle/components/ChipSuggestTarget.md +506 -0
  26. package/bundle/components/CollapsibleTree.md +2 -1
  27. package/bundle/components/CollapsibleTreeExtended.md +2 -1
  28. package/bundle/components/DateField.md +4 -2
  29. package/bundle/components/DatePickerExtended.md +16 -14
  30. package/bundle/components/DateRange.md +17 -10
  31. package/bundle/components/Dropdown.md +36 -63
  32. package/bundle/components/EmptyView.md +9 -4
  33. package/bundle/components/ExpandAnimation.md +9 -5
  34. package/bundle/components/FormField.md +1 -1
  35. package/bundle/components/ImageGallery.md +19 -52
  36. package/bundle/components/ImageGalleryExtended.md +123 -139
  37. package/bundle/components/ListItemControlsButtonDropdown.md +3 -2
  38. package/bundle/components/LoaderSmall.md +5 -4
  39. package/bundle/components/MonthYearRange.md +334 -0
  40. package/bundle/components/MultiselectField.md +81 -143
  41. package/bundle/components/Notification.md +2 -1
  42. package/bundle/components/NotificationGrouped.md +174 -0
  43. package/bundle/components/NumberField.md +259 -116
  44. package/bundle/components/OrderedList.md +149 -112
  45. package/bundle/components/Overlay.md +3 -2
  46. package/bundle/components/PageIndicators.md +306 -0
  47. package/bundle/components/Pagination.md +81 -3
  48. package/bundle/components/PaginationExtended.md +160 -0
  49. package/bundle/components/Portal.md +5 -2
  50. package/bundle/components/Puller.md +375 -0
  51. package/bundle/components/SMSField.md +277 -102
  52. package/bundle/components/SelectExtendedField.md +11 -7
  53. package/bundle/components/SelectField.md +7 -4
  54. package/bundle/components/SmartSpot.md +259 -0
  55. package/bundle/components/Step.md +2 -1
  56. package/bundle/components/Stepper.md +7 -7
  57. package/bundle/components/Suggest.md +7 -6
  58. package/bundle/components/SuggestField.md +8 -6
  59. package/bundle/components/TableBasic.md +12 -14
  60. package/bundle/components/Tooltip.md +9 -8
  61. package/bundle/guides/commits.md +4 -1
  62. package/bundle/guides/context.md +5 -5
  63. package/bundle/guides/stories.md +29 -21
  64. package/bundle/manifest.json +145 -13
  65. package/bundle/release-notes/1.50.0.mdx +4 -0
  66. package/bundle/release-notes/1.51.0.mdx +57 -0
  67. package/bundle/release-notes/1.52.0.mdx +55 -0
  68. package/bundle/release-notes/1.53.0.mdx +60 -0
  69. package/bundle/release-notes/1.54.0.mdx +45 -0
  70. package/bundle/release-notes/1.55.0.mdx +7 -0
  71. package/bundle/tokens.json +377 -222
  72. package/dist/index.js +39 -1
  73. package/dist/index.js.map +1 -1
  74. package/package.json +1 -1
@@ -0,0 +1,671 @@
1
+ ---
2
+ component: Carousel
3
+ category: Carousel
4
+ related: [CarouselViewport, CarouselItem, CarouselIndicators, CarouselPrevButton, CarouselNextButton, CarouselExtended]
5
+ tokens: []
6
+ stories: stories/Carousel/Carousel.stories.tsx
7
+ version: "1.0"
8
+ ---
9
+
10
+ # Carousel
11
+
12
+ ## Назначение
13
+
14
+ Контейнер составной карусели: хранит положение ленты, измеряет слайды и область
15
+ просмотра, вычисляет границы прокрутки и передаёт состояние навигации через
16
+ контекст. Разметку слайдов и расположение навигации собирает потребитель.
17
+
18
+ Используй когда: нужны слайды переменного размера, горизонтальная или вертикальная
19
+ прокрутка, переключение по элементам либо по автоматически рассчитанным страницам.
20
+
21
+ Не используй когда: нужна свободно прокручиваемая горизонтальная лента с заданным
22
+ шагом в пикселях — для этого `CarouselExtended`. Для переключения разделов страницы
23
+ с подписями используй семейство `Tabs`.
24
+
25
+ Публичный импорт: `import { Carousel } from "@sberbusiness/triplex-next"`.
26
+ `ECarouselScrollMode` и `EOrientation` доступны из того же пакета. Части доступны как статические поля `Carousel`,
27
+ самостоятельных именованных экспортов `CarouselViewport` / `CarouselItem` /
28
+ `CarouselIndicators` / `CarouselPrevButton` / `CarouselNextButton` в barrel нет.
29
+
30
+ ---
31
+
32
+ ## Варианты и props
33
+
34
+ Обязательных props нет. `ICarouselProps` расширяет стандартные HTML-атрибуты
35
+ `<div>`; `children` задаёт композицию, а `className`, `style`, `aria-*`, `data-*`
36
+ и обработчики передаются на корневой элемент.
37
+
38
+ | Prop | По умолчанию | Назначение и ограничения |
39
+ |---|---|---|
40
+ | `orientation` | `EOrientation.HORIZONTAL` | `HORIZONTAL` (`"horizontal"`) измеряет ширины и двигает ленту по X; `VERTICAL` (`"vertical"`) — высоты и по Y |
41
+ | `scrollMode` | `ECarouselScrollMode.ITEM` | `ITEM` (`"item"`) раскрывает следующий или предыдущий скрытый элемент; `PAGE` (`"page"`) объединяет последовательные элементы в страницы |
42
+ | `gap` | `16` | Расстояние между слайдами в px. Участвует и в CSS-раскладке, и в расчётах геометрии; передавай конечное неотрицательное число |
43
+ | `viewportPadding` | `0` | Отступы области просмотра в px: число либо readonly-кортеж из 2, 3 или 4 чисел по правилам CSS shorthand. Отрицательные значения, `NaN` и `±Infinity` нормализуются в `0` |
44
+ | `children` | — | `Carousel.Viewport` с лентой слайдов и необязательная навигация |
45
+
46
+ ### Сборка композиции
47
+
48
+ ```tsx
49
+ import { Carousel, ECarouselScrollMode } from "@sberbusiness/triplex-next";
50
+
51
+ <Carousel scrollMode={ECarouselScrollMode.PAGE} aria-label="Предложения">
52
+ <Carousel.Viewport style={{ height: 240 }}>
53
+ <Carousel.Track>
54
+ <Carousel.Item index={0} style={{ width: 320 }}>Первый слайд</Carousel.Item>
55
+ <Carousel.Item index={1} style={{ width: 320 }}>Второй слайд</Carousel.Item>
56
+ </Carousel.Track>
57
+ <Carousel.PrevButton aria-label="Предыдущий слайд" />
58
+ <Carousel.NextButton aria-label="Следующий слайд" />
59
+ </Carousel.Viewport>
60
+ <Carousel.Indicators
61
+ aria-label="Страницы предложений"
62
+ indicatorProps={({ page }) => ({ "aria-label": `Страница ${page}` })}
63
+ />
64
+ </Carousel>
65
+ ```
66
+
67
+ `Carousel.Viewport` ограничивает видимую область, `Carousel.Track` располагает
68
+ слайды и применяет смещение. Задай размер области просмотра и слайдов так, чтобы
69
+ было что измерять; для вертикальной карусели особенно важна ограниченная высота.
70
+ `index` у `Carousel.Item` — последовательный индекс с нуля, соответствующий
71
+ DOM-порядку, а `key` — стабильный идентификатор данных. После удаления или
72
+ перестановки элементов индексы нужно пересчитать.
73
+
74
+ ### Навигация и пересчёт геометрии
75
+
76
+ - В режиме `ITEM` выбор следующего элемента может перескочить через уже видимые
77
+ элементы. Если слайд больше области просмотра, он сначала прокручивается
78
+ внутри себя на размер области, без смены текущего индекса. Это не фиксированный
79
+ шаг в px и не обязательная смена индекса на единицу.
80
+ - В режиме `PAGE` элементы группируются по размеру области просмотра за вычетом
81
+ `viewportPadding` по оси прокрутки и с учётом `gap`; группа центрируется в этой
82
+ области, итоговое смещение ограничивается краями контента.
83
+ Количество страниц зависит от размеров, а не равно количеству слайдов.
84
+ - Смена `gap`, `scrollMode` или `orientation` сбрасывает текущий индекс на `0`.
85
+ Изменение `viewportPadding` пересчитывает смещение без такого сброса.
86
+ - `viewportPadding` задаёт padding именно `Viewport`, участвует в расчёте границ
87
+ прокрутки и видимости слайдов. Списки чисел мемоизируются по значению: новый
88
+ массив с теми же числами не требует нового расчёта нормализованных отступов.
89
+ - `ResizeObserver` следит за `Viewport` и `Track`, очищает кэш размеров и
90
+ пересчитывает положение текущего элемента/страницы. В режиме `PAGE`, если
91
+ после пересчёта страниц стало меньше, индекс ограничивается в
92
+ `requestAnimationFrame`; в `ITEM` индекс не ограничивается.
93
+ - `offset`, текущий индекс и количество страниц — внутреннее состояние.
94
+ Управляемого индекса, callback смены страницы, зацикливания и автопрокрутки
95
+ в API корневого `Carousel` нет.
96
+
97
+ ---
98
+
99
+ ## Дизайн-токены
100
+
101
+ У корневого `.carousel` собственных дизайн-токенов нет: он задаёт flex-раскладку
102
+ и ориентацию. Поэтому `tokens` в frontmatter пуст.
103
+
104
+ В общем `styles/Carousel.module.less` токены `Carousel.Button_*` относятся к
105
+ `Carousel.PrevButton` / `Carousel.NextButton`; оформление индикаторов приходит
106
+ из `PageIndicators`. Эти контракты принадлежат соответствующим частям композиции.
107
+ Переопределение токенов описано в
108
+ [ThemeProvider-ai.md](../ThemeProvider/ThemeProvider-ai.md).
109
+
110
+ `--triplex-next-runtime-carousel-gap`, `--triplex-next-runtime-carousel-transform`
111
+ и `--triplex-next-runtime-carousel-viewport-padding` — внутренние runtime-переменные
112
+ `Track` / `Viewport`, вычисленные из props и состояния. Это не дизайн-токены,
113
+ не добавляй их в `ThemeProvider` или сгенерированные файлы тем.
114
+
115
+ ---
116
+
117
+ ## Инварианты
118
+
119
+ - `forwardRef` сохраняется. Ref корневого `Carousel` указывает на его внешний
120
+ `HTMLDivElement`, а не на `Viewport` или `Track`. `className`, `style` и
121
+ остальные HTML-атрибуты остаются на этом же узле.
122
+ - Публичные `Carousel`, `ICarouselProps`, `ECarouselScrollMode` и статические поля
123
+ `Viewport`, `Track`, `Item`, `PrevButton`, `NextButton`, `Indicators` сохраняют
124
+ имена и сигнатуры. Сохраняются все существующие экспорты `index.ts`.
125
+ - `displayName` корня — `"Carousel"`; CSS-классы `carousel`, `horizontal`,
126
+ `vertical` и существующие story IDs сохраняются.
127
+ - Части композиции используют контекст ближайшего `Carousel`. Для одной карусели
128
+ предусмотрены одна область просмотра и одна лента: refs не регистрируют
129
+ несколько `Viewport` / `Track`.
130
+ - Слайды регистрируются по `index` в `Map`. Геометрия обходит диапазон
131
+ `0..count-1`, поэтому индексы должны быть уникальными, непрерывными и
132
+ соответствовать порядку слайдов.
133
+ - `orientationRef`, `offsetRef` и `currentIndexRef` стабильны: они позволяют
134
+ обработчикам жестов и `ResizeObserver` читать актуальные значения. Кэш
135
+ геометрии инвалидируется при изменении конфигурации и ресайзе.
136
+ - `ResizeObserver` отключается при cleanup, отложенный пересчёт индекса через
137
+ `requestAnimationFrame` отменяется при размонтировании.
138
+ - Код остаётся совместимым с React 17 при синхронизации в `release-0`.
139
+
140
+ ---
141
+
142
+ ## Accessibility
143
+
144
+ - Корень — обычный `<div>` без встроенной роли или локализованных ARIA-текстов.
145
+ Название карусели и при необходимости `role="region"` задаёт потребитель.
146
+ - `Track` имеет `role="presentation"`, слайды — `role="group"`. Имена групп,
147
+ подписи содержимого и связи с соответствующими страницами задаёт потребитель.
148
+ - Корневой `Carousel` и `Viewport` не обрабатывают клавиши-стрелки. С клавиатуры
149
+ можно использовать обычные кнопки навигации; в режиме `PAGE` индикаторы
150
+ наследуют клавиатурную навигацию `PageIndicators`.
151
+ - `PrevButton` / `NextButton` показывают край через `aria-disabled`, а не через
152
+ нативный `disabled`: они остаются фокусируемыми, собственный переход на краю
153
+ блокируется, пользовательский `onClick` по-прежнему вызывается. Дай кнопкам
154
+ доступные имена через `aria-label`.
155
+ - На устройствах с `(pointer: coarse)` стрелки не рендерятся. `Viewport`
156
+ поддерживает touch-жесты вдоль своей оси и сохраняет прокрутку страницы по
157
+ поперечной оси. Переход происходит после жеста больше `50px`.
158
+ - В `ITEM` индикаторы отсутствуют; в `PAGE` они используют `role="tablist"` и
159
+ кнопки `role="tab"`. Подписи ряда и индикаторов задаёт потребитель.
160
+ - Слайды, вышедшие за видимую область, не получают `aria-hidden` или `inert`
161
+ автоматически. Фокус не переносится при прокрутке; интерактивность содержимого
162
+ и доступность скрытых элементов требуют решения на стороне продукта.
163
+ - При `prefers-reduced-motion: reduce` CSS-переход ленты отключается.
164
+
165
+ ---
166
+
167
+ ## Связанные компоненты
168
+
169
+ - `CarouselViewport` (`Carousel.Viewport`) — область просмотра с padding,
170
+ touch-жестами и ref на измеряемый контейнер; самостоятельный контракт семейства.
171
+ - `CarouselItem` (`Carousel.Item`) — слайд с обязательным `index`, регистрирует
172
+ DOM-узел для измерений и убирает его при размонтировании.
173
+ - `CarouselIndicators` (`Carousel.Indicators`) — связывает `PageIndicators` с
174
+ текущей страницей и количеством страниц контекста, работает только в `PAGE`.
175
+ - [CarouselPrevButton](CarouselPrevButton-ai.md) / [CarouselNextButton](CarouselNextButton-ai.md) (`Carousel.PrevButton` /
176
+ `Carousel.NextButton`) — стрелки навигации с граничным `aria-disabled` и
177
+ адаптацией к `(pointer: coarse)`.
178
+ - `Carousel.Track` — часть без отдельного AI.md: `<div role="presentation">`,
179
+ объединяет внешний ref с ref контекста, применяет `gap` и смещение через
180
+ runtime-переменные. Принимает стандартные HTML-атрибуты, объединяет `className`,
181
+ передаёт `children`; отдельного API управления прокруткой не имеет.
182
+ - [CarouselExtended](../CarouselExtended/CarouselExtended-ai.md) — альтернатива
183
+ для свободно прокручиваемой горизонтальной ленты со своим шагом в px и
184
+ render-функциями кнопок.
185
+
186
+ ---
187
+
188
+ ## Stories
189
+
190
+ Основные истории: `stories/Carousel/Carousel.stories.tsx`.
191
+ Файлы примеров: `stories/Carousel/examples/`.
192
+
193
+ | Story | Example file | Что демонстрирует |
194
+ |---|---|---|
195
+ | `Playground` | `Playground.tsx` | Выбор ориентации, режима прокрутки и зазора |
196
+ | `Default` | `Default.tsx` | Горизонтальная карусель со слайдами переменной ширины |
197
+ | `ScrollModes` | `ScrollModes.tsx` | Сравнение `ITEM` и `PAGE`, индикаторы в постраничном режиме |
198
+ | `Orientations` | `Orientations.tsx` | Горизонтальная и вертикальная раскладка, отдельный `gap` для каждой |
199
+ | `Production` | `Production.tsx` | Карточки действий со вложенными кнопками, `viewportPadding={8}` |
200
+ | `DynamicIndicators` | `DynamicIndicators.tsx` | Автоматический расчёт страниц и окно индикаторов для длинной ленты |
201
+
202
+ У `ScrollModes` и `Orientations` есть Linux/CI baseline-скриншоты на `xs` / `xl`
203
+ в `__screenshots__/`. Скрытая story `VisualTests` также снимается и проверяет
204
+ состояния кнопок навигации. Остальные текущие stories исключены из
205
+ скриншот-прогона через `testRunner.skip`; `Playground` также не попадает
206
+ в MCP-примеры.
207
+
208
+ ---
209
+
210
+ ## История изменений
211
+
212
+ | Дата | Изменение |
213
+ |---|---|
214
+ | 2026-10-29 | Добавлены ссылки на отдельную AI-документацию кнопок навигации. |
215
+ | 2026-10-21 | В режиме `PAGE` группировка и центрирование страниц учитывают `viewportPadding`: группа больше не выходит за видимую область. |
216
+ | 2026-10-20 | Создан документ. Завершён аудит Carousel в TRI-163: уточнены JSDoc и стабильные зависимости hooks, проверены существующие stories и дополнены тесты корневого контракта. |
217
+
218
+ ---
219
+
220
+ ## Примеры использования
221
+
222
+ ### Default
223
+
224
+ `stories/Carousel/examples/Default.tsx`
225
+
226
+ ```tsx
227
+ import React from "react";
228
+ import { Carousel } from "@sberbusiness/triplex-next";
229
+
230
+ const BASE_ITEM_STYLE: React.CSSProperties = {
231
+ display: "flex",
232
+ alignItems: "center",
233
+ justifyContent: "center",
234
+ boxSizing: "border-box",
235
+ borderRadius: "8px",
236
+ color: "#ffffff",
237
+ fontSize: "24px",
238
+ fontWeight: "bold",
239
+ userSelect: "none",
240
+ };
241
+
242
+ export const Default = () => {
243
+ const itemSizes = [92, 168, 220, 244, 252, 256, 252, 244, 220, 168, 92];
244
+ const items = itemSizes.map((size, index) => {
245
+ const totalItems = itemSizes.length;
246
+ const hue = (index / (totalItems - 1)) * 280;
247
+
248
+ return {
249
+ id: index + 1,
250
+ size,
251
+ backgroundColor: `hsl(${hue}, 50%, 50%)`,
252
+ };
253
+ });
254
+
255
+ const boxStyle: React.CSSProperties = {
256
+ width: "100%",
257
+ height: "256px",
258
+ maxWidth: "512px",
259
+ };
260
+
261
+ return (
262
+ <div style={boxStyle}>
263
+ <Carousel>
264
+ <Carousel.Viewport>
265
+ <Carousel.Track>
266
+ {items.map((item, index) => (
267
+ <Carousel.Item
268
+ key={item.id}
269
+ index={index}
270
+ style={{
271
+ ...BASE_ITEM_STYLE,
272
+ width: item.size,
273
+ height: "100%",
274
+ backgroundColor: item.backgroundColor,
275
+ }}
276
+ >
277
+ {item.id}
278
+ </Carousel.Item>
279
+ ))}
280
+ </Carousel.Track>
281
+ <Carousel.PrevButton aria-label="Previous slide" />
282
+ <Carousel.NextButton aria-label="Next slide" />
283
+ </Carousel.Viewport>
284
+ </Carousel>
285
+ </div>
286
+ );
287
+ };
288
+ ```
289
+
290
+ ### ScrollModes
291
+
292
+ `stories/Carousel/examples/ScrollModes.tsx`
293
+
294
+ ```tsx
295
+ import React from "react";
296
+ import { Carousel, ECarouselScrollMode } from "@sberbusiness/triplex-next";
297
+
298
+ interface ScrollModeItemProps {
299
+ scrollMode: ECarouselScrollMode;
300
+ }
301
+
302
+ const BASE_ITEM_STYLE: React.CSSProperties = {
303
+ display: "flex",
304
+ alignItems: "center",
305
+ justifyContent: "center",
306
+ boxSizing: "border-box",
307
+ height: "256px",
308
+ borderRadius: "8px",
309
+ color: "#ffffff",
310
+ fontSize: "24px",
311
+ fontWeight: "bold",
312
+ userSelect: "none",
313
+ };
314
+
315
+ const ScrollModeItem = ({ scrollMode }: ScrollModeItemProps) => {
316
+ const itemSizes = [92, 168, 220, 244, 252, 256, 252, 244, 220, 168, 92];
317
+ const items = itemSizes.map((size, index) => {
318
+ const totalItems = itemSizes.length;
319
+ const hue = (index / (totalItems - 1)) * 280;
320
+
321
+ return {
322
+ id: index + 1,
323
+ size,
324
+ backgroundColor: `hsl(${hue}, 50%, 50%)`,
325
+ };
326
+ });
327
+
328
+ const containerStyle: React.CSSProperties = {
329
+ maxWidth: "512px",
330
+ };
331
+
332
+ return (
333
+ <div style={containerStyle}>
334
+ <div style={{ marginBottom: 8, fontSize: 16, fontWeight: 700 }}>{scrollMode.toUpperCase()}</div>
335
+ <Carousel scrollMode={scrollMode}>
336
+ <Carousel.Viewport>
337
+ <Carousel.Track>
338
+ {items.map((item, index) => (
339
+ <Carousel.Item
340
+ key={item.id}
341
+ index={index}
342
+ style={{
343
+ ...BASE_ITEM_STYLE,
344
+ width: item.size,
345
+ backgroundColor: item.backgroundColor,
346
+ }}
347
+ >
348
+ {item.id}
349
+ </Carousel.Item>
350
+ ))}
351
+ </Carousel.Track>
352
+ <Carousel.PrevButton aria-label="Previous slide" />
353
+ <Carousel.NextButton aria-label="Next slide" />
354
+ </Carousel.Viewport>
355
+ <Carousel.Indicators />
356
+ </Carousel>
357
+ </div>
358
+ );
359
+ };
360
+
361
+ const SCROLL_MODES = Object.values(ECarouselScrollMode);
362
+
363
+ export const ScrollModes = () => (
364
+ <div style={{ display: "flex", flexDirection: "column", gap: 16 }}>
365
+ {SCROLL_MODES.map((scrollMode) => (
366
+ <ScrollModeItem key={scrollMode} scrollMode={scrollMode} />
367
+ ))}
368
+ </div>
369
+ );
370
+ ```
371
+
372
+ ### Orientations
373
+
374
+ `stories/Carousel/examples/Orientations.tsx`
375
+
376
+ ```tsx
377
+ import React from "react";
378
+ import { Carousel, EOrientation } from "@sberbusiness/triplex-next";
379
+
380
+ interface OrientationItemProps {
381
+ orientation: EOrientation;
382
+ }
383
+
384
+ const BASE_ITEM_STYLE: React.CSSProperties = {
385
+ display: "flex",
386
+ alignItems: "center",
387
+ justifyContent: "center",
388
+ boxSizing: "border-box",
389
+ borderRadius: "8px",
390
+ color: "#ffffff",
391
+ fontSize: "24px",
392
+ fontWeight: "bold",
393
+ userSelect: "none",
394
+ };
395
+
396
+ const ORIENTATION_TO_ITEM_DIMENSIONS_MAP: Record<EOrientation, (size: number) => React.CSSProperties> = {
397
+ [EOrientation.HORIZONTAL]: (size) => ({
398
+ width: size,
399
+ height: "100%",
400
+ }),
401
+ [EOrientation.VERTICAL]: (size) => ({
402
+ width: "100%",
403
+ height: size / 2,
404
+ }),
405
+ };
406
+
407
+ const ORIENTATION_TO_GAP_MAP: Record<EOrientation, number> = {
408
+ [EOrientation.HORIZONTAL]: 16,
409
+ [EOrientation.VERTICAL]: 8,
410
+ };
411
+
412
+ const OrientationItem = ({ orientation }: OrientationItemProps) => {
413
+ const itemSizes = [92, 168, 220, 244, 252, 256, 252, 244, 220, 168, 92];
414
+ const items = itemSizes.map((size, index) => {
415
+ const totalItems = itemSizes.length;
416
+ const hue = (index / (totalItems - 1)) * 280;
417
+
418
+ return {
419
+ id: index + 1,
420
+ size,
421
+ backgroundColor: `hsl(${hue}, 50%, 50%)`,
422
+ };
423
+ });
424
+
425
+ const containerStyle: React.CSSProperties = {
426
+ maxWidth: "512px",
427
+ };
428
+
429
+ const viewportStyle: React.CSSProperties = {
430
+ height: "256px",
431
+ };
432
+
433
+ return (
434
+ <div style={containerStyle}>
435
+ <div style={{ marginBottom: 8, fontSize: 16, fontWeight: 700 }}>{orientation.toUpperCase()}</div>
436
+ <Carousel orientation={orientation} gap={ORIENTATION_TO_GAP_MAP[orientation]}>
437
+ <Carousel.Viewport style={viewportStyle}>
438
+ <Carousel.Track>
439
+ {items.map((item, index) => {
440
+ const itemStyle: React.CSSProperties = {
441
+ ...BASE_ITEM_STYLE,
442
+ ...ORIENTATION_TO_ITEM_DIMENSIONS_MAP[orientation](item.size),
443
+ backgroundColor: item.backgroundColor,
444
+ };
445
+
446
+ return (
447
+ <Carousel.Item key={item.id} index={index} style={itemStyle}>
448
+ {item.id}
449
+ </Carousel.Item>
450
+ );
451
+ })}
452
+ </Carousel.Track>
453
+ <Carousel.PrevButton aria-label="Previous slide" />
454
+ <Carousel.NextButton aria-label="Next slide" />
455
+ </Carousel.Viewport>
456
+ <Carousel.Indicators />
457
+ </Carousel>
458
+ </div>
459
+ );
460
+ };
461
+
462
+ const ORIENTATIONS = Object.values(EOrientation);
463
+
464
+ export const Orientations = () => (
465
+ <div style={{ display: "flex", flexDirection: "column", gap: 16 }}>
466
+ {ORIENTATIONS.map((orientation) => (
467
+ <OrientationItem key={orientation} orientation={orientation} />
468
+ ))}
469
+ </div>
470
+ );
471
+ ```
472
+
473
+ ### Production
474
+
475
+ `stories/Carousel/examples/Production.tsx`
476
+
477
+ ```tsx
478
+ import React, { useState } from "react";
479
+ import {
480
+ Carousel,
481
+ CardAction,
482
+ ECardRoundingSize,
483
+ ECardTheme,
484
+ ECardContentPaddingSize,
485
+ Title,
486
+ ETitleSize,
487
+ EFontWeightTitle,
488
+ Text,
489
+ ETextSize,
490
+ EFontType,
491
+ Gap,
492
+ Link,
493
+ Button,
494
+ EButtonTheme,
495
+ EComponentSize,
496
+ } from "@sberbusiness/triplex-next";
497
+ import { DefaulticonStrokePrdIcon20 } from "@sberbusiness/icons-next";
498
+
499
+ const CARDS_IDS = Array.from({ length: 6 }, (_, i) => i);
500
+
501
+ const CardBody = () => (
502
+ <CardAction.Content.Body>
503
+ {[1, 2, 3].map((item) => (
504
+ <div key={item} style={{ display: "flex", alignItems: "center", marginTop: item > 1 ? "16px" : "0" }}>
505
+ <DefaulticonStrokePrdIcon20 paletteIndex={5} />
506
+ <Text size={ETextSize.B3} style={{ marginLeft: "8px" }}>
507
+ List item text
508
+ </Text>
509
+ </div>
510
+ ))}
511
+ <Gap size={8} />
512
+ <Text tag="div" type={EFontType.SECONDARY} size={ETextSize.B4}>
513
+ This message provides additional context or highlights important information to note.
514
+ </Text>
515
+ <Gap size={8} />
516
+ <Text tag="div" type={EFontType.PRIMARY} size={ETextSize.B3}>
517
+ <Link onKeyDown={(event) => event.stopPropagation()} onClick={(event) => event.stopPropagation()}>
518
+ Link text
519
+ </Link>
520
+ </Text>
521
+ </CardAction.Content.Body>
522
+ );
523
+
524
+ export const Production = () => {
525
+ const [selectedId, setSelectedId] = useState<number | null>(null);
526
+
527
+ return (
528
+ <div style={{ width: "100%", maxWidth: "752px" }}>
529
+ <Carousel viewportPadding={8}>
530
+ <Carousel.Viewport>
531
+ <Carousel.Track>
532
+ {CARDS_IDS.map((id, index) => {
533
+ const selected = selectedId === id;
534
+
535
+ return (
536
+ <Carousel.Item
537
+ key={id}
538
+ index={index}
539
+ style={{
540
+ width: "216px",
541
+ height: "100%",
542
+ boxSizing: "border-box",
543
+ }}
544
+ >
545
+ <CardAction
546
+ roundingSize={ECardRoundingSize.MD}
547
+ theme={ECardTheme.GENERAL}
548
+ selected={selected}
549
+ toggle={() => setSelectedId(selected ? null : id)}
550
+ >
551
+ <CardAction.Media
552
+ style={{
553
+ backgroundImage: "url(assets/images/evotor.png)",
554
+ height: "129px",
555
+ }}
556
+ />
557
+ <CardAction.Content paddingSize={ECardContentPaddingSize.SM}>
558
+ <CardAction.Content.Header>
559
+ <Title tag="div" size={ETitleSize.H3} weight={EFontWeightTitle.REGULAR}>
560
+ Title Text
561
+ </Title>
562
+ </CardAction.Content.Header>
563
+
564
+ <CardBody />
565
+
566
+ <CardAction.Content.Footer>
567
+ <Button
568
+ theme={
569
+ selected ? EButtonTheme.SECONDARY_LIGHT : EButtonTheme.SECONDARY
570
+ }
571
+ size={EComponentSize.SM}
572
+ onKeyDown={(event) => event.stopPropagation()}
573
+ onClick={(event) => event.stopPropagation()}
574
+ >
575
+ Button text
576
+ </Button>
577
+ </CardAction.Content.Footer>
578
+ </CardAction.Content>
579
+ </CardAction>
580
+ </Carousel.Item>
581
+ );
582
+ })}
583
+ </Carousel.Track>
584
+ <Carousel.PrevButton aria-label="Previous slide" />
585
+ <Carousel.NextButton aria-label="Next slide" />
586
+ </Carousel.Viewport>
587
+ </Carousel>
588
+ </div>
589
+ );
590
+ };
591
+ ```
592
+
593
+ ### DynamicIndicators
594
+
595
+ `stories/Carousel/examples/DynamicIndicators.tsx`
596
+
597
+ ```tsx
598
+ import React from "react";
599
+ import { Carousel, ECarouselScrollMode } from "@sberbusiness/triplex-next";
600
+
601
+ const BASE_ITEM_STYLE: React.CSSProperties = {
602
+ display: "flex",
603
+ alignItems: "center",
604
+ justifyContent: "center",
605
+ boxSizing: "border-box",
606
+ borderRadius: "8px",
607
+ color: "#ffffff",
608
+ fontSize: "24px",
609
+ fontWeight: "bold",
610
+ userSelect: "none",
611
+ };
612
+
613
+ export const DynamicIndicators = () => {
614
+ const itemSizes = Array.from({ length: 20 }, () => 128);
615
+
616
+ const items = itemSizes.map((size, index) => {
617
+ const totalItems = itemSizes.length;
618
+ const hue = (index / (totalItems - 1)) * 280;
619
+
620
+ return {
621
+ id: index + 1,
622
+ size,
623
+ backgroundColor: `hsl(${hue}, 50%, 50%)`,
624
+ };
625
+ });
626
+
627
+ const boxStyle: React.CSSProperties = {
628
+ display: "flex",
629
+ flexDirection: "column",
630
+ gap: "16px",
631
+ width: "100%",
632
+ maxWidth: "512px",
633
+ };
634
+
635
+ const containerStyle: React.CSSProperties = {
636
+ width: "100%",
637
+ height: "256px",
638
+ };
639
+
640
+ return (
641
+ <div style={boxStyle}>
642
+ <div style={containerStyle}>
643
+ <Carousel scrollMode={ECarouselScrollMode.PAGE}>
644
+ <Carousel.Viewport>
645
+ <Carousel.Track>
646
+ {items.map((item, index) => (
647
+ <Carousel.Item
648
+ key={item.id}
649
+ index={index}
650
+ style={{
651
+ ...BASE_ITEM_STYLE,
652
+ width: item.size,
653
+ height: "100%",
654
+ backgroundColor: item.backgroundColor,
655
+ }}
656
+ >
657
+ {item.id}
658
+ </Carousel.Item>
659
+ ))}
660
+ </Carousel.Track>
661
+ <Carousel.PrevButton aria-label="Previous slide" />
662
+ <Carousel.NextButton aria-label="Next slide" />
663
+ </Carousel.Viewport>
664
+ <Carousel.Indicators />
665
+ </Carousel>
666
+ </div>
667
+ </div>
668
+ );
669
+ };
670
+ ```
671
+