@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,606 @@
1
+ ---
2
+ component: ImageGalleryExtended
3
+ category: ImageGalleryExtended
4
+ related: [ImageGallery, MobileView, LightBox]
5
+ tokens: [
6
+ "--triplex-next-ImageGallery-Accent_Color",
7
+ "--triplex-next-ImageGallery-Arrow_Background_Default",
8
+ "--triplex-next-ImageGallery-Arrow_Background_Hover",
9
+ "--triplex-next-ImageGallery-Arrow_Background_Active",
10
+ "--triplex-next-ImageGallery-Arrow_Background_Disabled",
11
+ "--triplex-next-ImageGallery-Arrow_BorderColor_Default",
12
+ "--triplex-next-ImageGallery-Arrow_BorderColor_Hover",
13
+ "--triplex-next-ImageGallery-Arrow_BorderColor_Active",
14
+ "--triplex-next-ImageGallery-Arrow_BorderColor_Disabled",
15
+ "--triplex-next-ImageGallery-Dot_Background_Default",
16
+ "--triplex-next-ImageGallery-Dot_Background_Hover",
17
+ "--triplex-next-ImageGallery-Dot_Background_Active",
18
+ "--triplex-next-ImageGallery-Thumb_Mask_Background"
19
+ ]
20
+ stories: stories/ImageGalleryExtended/ImageGalleryExtended.stories.tsx
21
+ version: "1.0"
22
+ ---
23
+
24
+ # ImageGalleryExtended
25
+
26
+ ## Назначение
27
+
28
+ Базовый (controlled) compound-компонент галереи изображений. Изображения задаются
29
+ массивом `items`, контейнер хранит активный индекс, обрабатывает стрелочную
30
+ навигацию с клавиатуры и раздаёт данные составным частям через React-контекст.
31
+ Раскладка задаётся декларативно — потребитель сам собирает нужные части
32
+ (`.Main`, `.Thumbnails`, `.Dots`, `.Nav`, `.Arrow`, `.Thumb`) в любом порядке.
33
+
34
+ Используй `ImageGalleryExtended` когда: нужен полный контроль над раскладкой и
35
+ поведением галереи (нестандартный порядок частей, кастомные стрелки или
36
+ миниатюры через render-функции, собственное управление активным изображением).
37
+
38
+ Не используй когда: достаточно стандартного пресета «крупная картинка + лента
39
+ миниатюр (десктоп) / тики (мобильный)» с uncontrolled-режимом — для этого есть
40
+ готовая обёртка `ImageGallery`.
41
+
42
+ ---
43
+
44
+ ## Варианты и props
45
+
46
+ Компонент — `Object.assign`-композиция: корневой контейнер + статические
47
+ субкомпоненты-части (`ImageGalleryExtended.Main` и др.).
48
+
49
+ ### Корневой контейнер `ImageGalleryExtended`
50
+
51
+ | Prop | Тип | Обязательный | Описание |
52
+ |---|---|---|---|
53
+ | `items` | `ReadonlyArray<IImageGalleryItemProps>` | да | Изображения: `{ id, src, alt?, thumbSrc? }`. `id` — ключ для controlled-режима, `thumbSrc` падает на `src`, если не задан |
54
+ | `selectedId` | `string` | да | Id активного изображения. Неизвестный/отсутствующий id резолвится в первый элемент |
55
+ | `onChange` | `(id: string) => void` | да | Вызывается только при реальной смене активного id (клик по уже активному не триггерит) |
56
+ | `children` | `React.ReactNode` | да | Составные части галереи |
57
+ | `...HTMLDivAttributes` | — | — | Кроме `onChange` (переопределён под смену изображения) |
58
+
59
+ Контейнер controlled-only: своего состояния активного изображения не держит.
60
+ Uncontrolled-режим (`defaultId`) добавляет обёртка `ImageGallery`.
61
+
62
+ ### `ImageGalleryExtended.Main`
63
+
64
+ Крупное изображение: вьюпорт + накладываемый поверх `children` (обычно `.Nav` со
65
+ стрелками). На десктопе — статичный слайд; на мобильном (ширина < SM) при
66
+ `items.length > 1` — лента со свайпом prev/next.
67
+
68
+ | Prop | Тип | По умолчанию | Описание |
69
+ |---|---|---|---|
70
+ | `height` | `'auto' \| number \| string` | `'auto'` | `'auto'` — фиксированные значения по breakpoint (504px / 264px); число → `px` |
71
+ | `withBlur` | `boolean` | `false` | Блюр-слой копии изображения по краям (для картинок уже вьюпорта) |
72
+ | `onImageClick` | `(index: number) => void` | — | Клик по картинке; получает индекс активного изображения. Точка интеграции с родительским LightBox |
73
+
74
+ ### `ImageGalleryExtended.Nav`
75
+
76
+ Провайдер состояния навигации через **render-функцию** (`children`). Своей
77
+ разметки не добавляет — рендерит только результат `children`. Размещается как
78
+ child `.Main`, чтобы стрелки позиционировались поверх картинки.
79
+
80
+ Состояние render-функции: `{ onPrev, onNext, isFirst, isLast, selectedIndex, itemsCount }`.
81
+
82
+ ### `ImageGalleryExtended.Arrow`
83
+
84
+ Презентационная кнопка-стрелка. Состояние (`disabled`/`hidden`/`onClick`)
85
+ передаётся через props, обычно из render-функции `.Nav`.
86
+
87
+ Видимость: стрелки скрыты по умолчанию и проявляются при ховере на `.Main`
88
+ (а также при фокусе с клавиатуры). Стрелка в состоянии `disabled` (например,
89
+ `PREV` на первом слайде или `NEXT` на последнем) не показывается вовсе.
90
+
91
+ | Prop | Тип | Обязательный | Описание |
92
+ |---|---|---|---|
93
+ | `direction` | `EImageGalleryArrowDirection` | да | `PREV` (`'prev'`) / `NEXT` (`'next'`) — иконка и позиция |
94
+ | `aria-label` | `string` | да | Доступное имя; передаёт потребитель (мультиязычность) |
95
+ | `...ButtonHTMLAttributes` | — | — | Кроме переопределённого `aria-label` |
96
+
97
+ ### `ImageGalleryExtended.Thumbnails`
98
+
99
+ Горизонтальная лента миниатюр с нативным скроллом и автоцентровкой активной.
100
+ `children` — опциональная **render-функция** миниатюры; по умолчанию рисует
101
+ `.Thumb`. Состояние render-функции: `{ item, index, isActive, ariaLabel, onSelect, ref }`.
102
+ Чтобы автоцентровка работала, проброс `ref` на корневой `<button>` обязателен.
103
+
104
+ ### `ImageGalleryExtended.Thumb`
105
+
106
+ Кнопка-миниатюра. `ref` пробрасывается на `<button>` (родитель собирает refs для
107
+ центровки). Поверх изображения лежит полупрозрачная маска
108
+ (`Thumb_Mask_Background`), которая скрывается на hover и для активной миниатюры
109
+ (`isActive`).
110
+
111
+ | Prop | Тип | Обязательный | Описание |
112
+ |---|---|---|---|
113
+ | `item` | `IImageGalleryItemProps` | да | Изображение миниатюры (использует `thumbSrc ?? src`) |
114
+ | `isActive` | `boolean` | да | Активна ли миниатюра (рамка + `aria-current`) |
115
+
116
+ ### `ImageGalleryExtended.Dots`
117
+
118
+ Ряд кликабельных тиков-индикаторов (мобильный preset). Изображения распределяются
119
+ по тикам бакетами равного размера (макс. 4 тика); при `items.length <= 1` ничего
120
+ не рендерит. Собственных публичных props нет (`className + ...HTMLDivAttributes`).
121
+
122
+ ---
123
+
124
+ ## Дизайн-токены
125
+
126
+ Токены используют общий с `ImageGallery` префикс `--triplex-next-ImageGallery-`
127
+ (семейства делят визуальный язык).
128
+
129
+ ```
130
+ --triplex-next-ImageGallery-Accent_Color
131
+
132
+ --triplex-next-ImageGallery-Arrow_Background_Default
133
+ --triplex-next-ImageGallery-Arrow_Background_Hover
134
+ --triplex-next-ImageGallery-Arrow_Background_Active
135
+ --triplex-next-ImageGallery-Arrow_Background_Disabled
136
+ --triplex-next-ImageGallery-Arrow_BorderColor_Default
137
+ --triplex-next-ImageGallery-Arrow_BorderColor_Hover
138
+ --triplex-next-ImageGallery-Arrow_BorderColor_Active
139
+ --triplex-next-ImageGallery-Arrow_BorderColor_Disabled
140
+
141
+ --triplex-next-ImageGallery-Dot_Background_Default
142
+ --triplex-next-ImageGallery-Dot_Background_Hover
143
+ --triplex-next-ImageGallery-Dot_Background_Active
144
+
145
+ --triplex-next-ImageGallery-Thumb_Mask_Background
146
+ ```
147
+
148
+ Runtime CSS-переменные (задаются компонентом через `style`, **не** дизайн-токены,
149
+ не попадают в `DesignTokens`):
150
+
151
+ ```
152
+ --triplex-next-runtime-ImageGalleryExtended-Main_Height
153
+ --triplex-next-runtime-ImageGalleryExtended-Track_Shift
154
+ --triplex-next-runtime-ImageGalleryExtended-Track_Drag
155
+ ```
156
+
157
+ ---
158
+
159
+ ## Инварианты
160
+
161
+ - **`forwardRef`** обязателен на всех публичных частях (`Root`, `.Main`, `.Arrow`,
162
+ `.Thumbnails`, `.Thumb`, `.Dots`). Target — корневой DOM-элемент части
163
+ (`<div>` для контейнера/Main/Thumbnails/Dots, `<button>` для Arrow/Thumb).
164
+ `.Thumbnails` пробрасывает ref через `useImperativeHandle` на внутренний
165
+ `carouselRef` — это нужно для автоцентровки; не заменять на прямой ref.
166
+ - **Контейнер controlled-only.** Не добавлять внутреннее состояние активного
167
+ изображения — uncontrolled живёт в обёртке `ImageGallery`.
168
+ - **`onChange` вызывается только при смене id** (клик по активному / неизменный
169
+ id игнорируются) — потребители на это полагаются.
170
+ - **Публичный API** (имена частей, props, `EImageGalleryArrowDirection` и его
171
+ значения `'prev'`/`'next'`, render-функции `.Nav`/`.Thumbnails`) — breaking
172
+ change при изменении.
173
+ - **Render-функции** `.Nav` и `.Thumbnails` — контракт; форма объекта состояния
174
+ фиксирована.
175
+ - Токены используют префикс `ImageGallery-`, а не `ImageGalleryExtended-` —
176
+ переименование сломает темизацию у потребителей обоих семейств.
177
+
178
+ ---
179
+
180
+ ## Accessibility
181
+
182
+ - **Клавиатура (контейнер):** `←` / `→` на корневом `<div>` (`tabIndex={0}` по
183
+ умолчанию) переключают активное изображение. Событие игнорируется, если фокус
184
+ на вложенном интерактивном элементе (`event.target !== event.currentTarget`),
185
+ чтобы не перехватывать навигацию у внутренних кнопок.
186
+ - **Клавиатура (лента миниатюр):** `←` / `→` на `.Thumbnails` переключают выбор.
187
+ При навигации стрелками фокус переносится с прежней миниатюры на активную —
188
+ иначе кольцо `:focus-visible` осталось бы на старой одновременно с рамкой
189
+ `.active` новой.
190
+ - **`aria-current`:** активная миниатюра (`.Thumb`), активный тик (`.Dots`) и
191
+ активная кнопка получают `aria-current="true"`.
192
+ - **`aria-label` — обязанность потребителя.** `.Arrow` требует `aria-label`
193
+ явным props; компонент не хардкодит язык. `.Thumb`/`.Dots` берут доступное имя
194
+ из `item.alt`.
195
+ - **Декоративные изображения** (блюр-слой) помечены `aria-hidden="true"` с пустым
196
+ `alt`.
197
+ - **Свайп vs скролл (мобильный):** горизонтальный жест перехватывается навигацией
198
+ (`touch-action: pan-y` + non-passive `touchmove` с `preventDefault`),
199
+ вертикальный отдаётся скроллу страницы. Направление фиксируется после порога
200
+ `DIRECTION_LOCK_PX`.
201
+
202
+ ---
203
+
204
+ ## Связанные компоненты
205
+
206
+ - `ImageGallery` — тонкая обёртка-пресет над `ImageGalleryExtended`: задаёт
207
+ стандартную раскладку (`.Main` + стрелки + миниатюры/тики через `MobileView`) и
208
+ добавляет uncontrolled-режим (`defaultId`). Используй её, если кастомная
209
+ раскладка не нужна.
210
+ - `MobileView` — переключает десктопную/мобильную ветку рендера внутри `.Main`
211
+ (лента свайпа) и в пресете `ImageGallery` (миниатюры ↔ тики).
212
+ - `LightBox` — типовой сценарий: `onImageClick` открывает изображение в лайтбоксе
213
+ (см. story `OpenFromAvatar`).
214
+
215
+ ### Внутренние части без отдельного AI.md
216
+
217
+ Описываются здесь, т.к. это приватные обёртки без самостоятельного публичного
218
+ API (экспортируются из barrel ради композиции, но используются внутри частей):
219
+
220
+ - `ImageGalleryExtendedSlide` — один слайд (опциональный блюр + изображение);
221
+ используется в `.Main` и в ленте свайпа.
222
+ - `ImageGalleryExtendedSwipeTrack` — лента крупного изображения со свайпом на
223
+ мобильном: окно из соседних слайдов (prev/current/next), доводка к соседу по
224
+ завершении жеста, эффект «резинки» на краях.
225
+
226
+ ---
227
+
228
+ ## Stories
229
+
230
+ Основные истории: `stories/ImageGalleryExtended/ImageGalleryExtended.stories.tsx`
231
+ Файлы примеров: `stories/ImageGalleryExtended/examples/`
232
+
233
+ | Story | Example file | Что демонстрирует |
234
+ |---|---|---|
235
+ | `Playground` | `Playground.tsx` | Интерактивный контроль `withBlur` / `height` |
236
+ | `Default` | `Default.tsx` | Полный десктопный состав: крупная картинка со стрелками + лента миниатюр |
237
+ | `MainOnly` | `MainOnly.tsx` | Только крупная картинка со стрелками (без миниатюр/тиков), с блюром |
238
+ | `WithDots` | `WithDots.tsx` | Мобильный preset: крупная картинка + тики-индикаторы |
239
+ | `CustomLayout` | `CustomLayout.tsx` | Произвольный порядок частей + кастомные стрелки и миниатюры через render-функции |
240
+ | `ManyThumbnails` | `ManyThumbnails.tsx` | Большой набор (20 изображений): горизонтальный скролл и автоцентровка ленты миниатюр |
241
+ | `OpenFromAvatar` | `OpenFromAvatar.tsx` | Открытие изображения в `LightBox` по клику (интеграция `onImageClick`) |
242
+ | `VisualTests` | `VisualTests.tsx` | Скриншот-регрессия: стрелки на границах диапазона (disabled) и тики-индикаторы |
243
+
244
+ ---
245
+
246
+ ## История изменений
247
+
248
+ | Дата | Изменение |
249
+ |---|---|
250
+ | 2026-05-28 | Создан документ |
251
+
252
+ ---
253
+
254
+ ## Примеры использования
255
+
256
+ ### Default
257
+
258
+ `stories/ImageGalleryExtended/examples/Default.tsx`
259
+
260
+ ```tsx
261
+ import React from "react";
262
+ import { ImageGalleryExtended, EImageGalleryArrowDirection } from "@sberbusiness/triplex-next";
263
+
264
+ const ITEMS = Array.from({ length: 9 }, (_, i) => ({
265
+ id: `photo-${i + 1}`,
266
+ src: `assets/images/imageGallery/0${i + 1}.jpg`,
267
+ alt: `Photo ${i + 1}`,
268
+ }));
269
+
270
+ /**
271
+ * Полный десктопный состав: крупная картинка со стрелками + лента миниатюр.
272
+ * Активный id хранится в состоянии (компонент controlled-only).
273
+ */
274
+ export const Default = () => {
275
+ const [selectedId, setSelectedId] = React.useState("photo-1");
276
+
277
+ return (
278
+ <ImageGalleryExtended items={ITEMS} selectedId={selectedId} onChange={setSelectedId}>
279
+ <ImageGalleryExtended.Main>
280
+ <ImageGalleryExtended.Nav>
281
+ {({ onPrev, onNext, isFirst, isLast, itemsCount }) => (
282
+ <>
283
+ <ImageGalleryExtended.Arrow
284
+ direction={EImageGalleryArrowDirection.PREV}
285
+ aria-label="Предыдущее изображение"
286
+ onClick={onPrev}
287
+ disabled={isFirst}
288
+ hidden={itemsCount <= 1}
289
+ />
290
+ <ImageGalleryExtended.Arrow
291
+ direction={EImageGalleryArrowDirection.NEXT}
292
+ aria-label="Следующее изображение"
293
+ onClick={onNext}
294
+ disabled={isLast}
295
+ hidden={itemsCount <= 1}
296
+ />
297
+ </>
298
+ )}
299
+ </ImageGalleryExtended.Nav>
300
+ </ImageGalleryExtended.Main>
301
+ <ImageGalleryExtended.Thumbnails />
302
+ </ImageGalleryExtended>
303
+ );
304
+ };
305
+ ```
306
+
307
+ ### MainOnly
308
+
309
+ `stories/ImageGalleryExtended/examples/MainOnly.tsx`
310
+
311
+ ```tsx
312
+ import React from "react";
313
+ import { ImageGalleryExtended, EImageGalleryArrowDirection } from "@sberbusiness/triplex-next";
314
+
315
+ const ITEMS = Array.from({ length: 9 }, (_, i) => ({
316
+ id: `photo-${i + 1}`,
317
+ src: `assets/images/imageGallery/0${i + 1}.jpg`,
318
+ alt: `Photo ${i + 1}`,
319
+ }));
320
+
321
+ /**
322
+ * Только крупная картинка со стрелками — без миниатюр и тиков.
323
+ * Навигация доступна стрелками и клавишами `←` / `→`.
324
+ */
325
+ export const MainOnly = () => {
326
+ const [selectedId, setSelectedId] = React.useState("photo-1");
327
+
328
+ return (
329
+ <ImageGalleryExtended items={ITEMS} selectedId={selectedId} onChange={setSelectedId}>
330
+ <ImageGalleryExtended.Main withBlur>
331
+ <ImageGalleryExtended.Nav>
332
+ {({ onPrev, onNext, isFirst, isLast, itemsCount }) => (
333
+ <>
334
+ <ImageGalleryExtended.Arrow
335
+ direction={EImageGalleryArrowDirection.PREV}
336
+ aria-label="Предыдущее изображение"
337
+ onClick={onPrev}
338
+ disabled={isFirst}
339
+ hidden={itemsCount <= 1}
340
+ />
341
+ <ImageGalleryExtended.Arrow
342
+ direction={EImageGalleryArrowDirection.NEXT}
343
+ aria-label="Следующее изображение"
344
+ onClick={onNext}
345
+ disabled={isLast}
346
+ hidden={itemsCount <= 1}
347
+ />
348
+ </>
349
+ )}
350
+ </ImageGalleryExtended.Nav>
351
+ </ImageGalleryExtended.Main>
352
+ </ImageGalleryExtended>
353
+ );
354
+ };
355
+ ```
356
+
357
+ ### WithDots
358
+
359
+ `stories/ImageGalleryExtended/examples/WithDots.tsx`
360
+
361
+ ```tsx
362
+ import React from "react";
363
+ import { ImageGalleryExtended } from "@sberbusiness/triplex-next";
364
+
365
+ const ITEMS = Array.from({ length: 9 }, (_, i) => ({
366
+ id: `photo-${i + 1}`,
367
+ src: `assets/images/imageGallery/0${i + 1}.jpg`,
368
+ alt: `Photo ${i + 1}`,
369
+ }));
370
+
371
+ /**
372
+ * Мобильный preset: крупная картинка + ряд тиков-индикаторов вместо ленты
373
+ * миниатюр. Состав частей собирается вручную из примитивов Extended.
374
+ */
375
+ export const WithDots = () => {
376
+ const [selectedId, setSelectedId] = React.useState("photo-1");
377
+
378
+ return (
379
+ <ImageGalleryExtended items={ITEMS} selectedId={selectedId} onChange={setSelectedId}>
380
+ <ImageGalleryExtended.Main withBlur />
381
+ <ImageGalleryExtended.Dots />
382
+ </ImageGalleryExtended>
383
+ );
384
+ };
385
+ ```
386
+
387
+ ### CustomLayout
388
+
389
+ `stories/ImageGalleryExtended/examples/CustomLayout.tsx`
390
+
391
+ ```tsx
392
+ import React from "react";
393
+ import { ImageGalleryExtended } from "@sberbusiness/triplex-next";
394
+
395
+ const ITEMS = Array.from({ length: 9 }, (_, i) => ({
396
+ id: `photo-${i + 1}`,
397
+ src: `assets/images/imageGallery/0${i + 1}.jpg`,
398
+ alt: `Photo ${i + 1}`,
399
+ }));
400
+
401
+ /**
402
+ * Порядок составных частей произволен: здесь лента миниатюр расположена над
403
+ * крупной картинкой. Демонстрирует обе render-функции: `Thumbnails` рисует
404
+ * миниатюры через `ImageGalleryExtended.Thumb` (можно подставить любую разметку,
405
+ * проброс `ref` включает автоцентровку), а вместо стандартных стрелок `Nav`
406
+ * отдаёт состояние навигации в кастомную панель «‹ N / Total ›».
407
+ */
408
+ export const CustomLayout = () => {
409
+ const [selectedId, setSelectedId] = React.useState("photo-1");
410
+
411
+ return (
412
+ <ImageGalleryExtended items={ITEMS} selectedId={selectedId} onChange={setSelectedId}>
413
+ <ImageGalleryExtended.Thumbnails>
414
+ {({ item, isActive, onSelect, ref }) => (
415
+ <ImageGalleryExtended.Thumb ref={ref} item={item} isActive={isActive} onClick={onSelect} />
416
+ )}
417
+ </ImageGalleryExtended.Thumbnails>
418
+ <ImageGalleryExtended.Main withBlur height={400}>
419
+ <ImageGalleryExtended.Nav>
420
+ {({ onPrev, onNext, isFirst, isLast, selectedIndex, itemsCount }) => (
421
+ <div
422
+ style={{
423
+ position: "absolute",
424
+ bottom: 12,
425
+ left: "50%",
426
+ zIndex: 2,
427
+ display: "flex",
428
+ alignItems: "center",
429
+ gap: 12,
430
+ padding: "6px 14px",
431
+ borderRadius: 20,
432
+ transform: "translateX(-50%)",
433
+ background: "rgba(0, 0, 0, 0.5)",
434
+ color: "#fff",
435
+ }}
436
+ >
437
+ <button
438
+ type="button"
439
+ aria-label="Предыдущее изображение"
440
+ disabled={isFirst}
441
+ onClick={onPrev}
442
+ style={{
443
+ border: "none",
444
+ background: "none",
445
+ color: "inherit",
446
+ fontSize: 18,
447
+ cursor: isFirst ? "default" : "pointer",
448
+ opacity: isFirst ? 0.4 : 1,
449
+ }}
450
+ >
451
+ ‹
452
+ </button>
453
+ <span>
454
+ {selectedIndex + 1} / {itemsCount}
455
+ </span>
456
+ <button
457
+ type="button"
458
+ aria-label="Следующее изображение"
459
+ disabled={isLast}
460
+ onClick={onNext}
461
+ style={{
462
+ border: "none",
463
+ background: "none",
464
+ color: "inherit",
465
+ fontSize: 18,
466
+ cursor: isLast ? "default" : "pointer",
467
+ opacity: isLast ? 0.4 : 1,
468
+ }}
469
+ >
470
+ ›
471
+ </button>
472
+ </div>
473
+ )}
474
+ </ImageGalleryExtended.Nav>
475
+ </ImageGalleryExtended.Main>
476
+ </ImageGalleryExtended>
477
+ );
478
+ };
479
+ ```
480
+
481
+ ### ManyThumbnails
482
+
483
+ `stories/ImageGalleryExtended/examples/ManyThumbnails.tsx`
484
+
485
+ ```tsx
486
+ import React from "react";
487
+ import { ImageGalleryExtended, EImageGalleryArrowDirection } from "@sberbusiness/triplex-next";
488
+
489
+ /** Доступных файлов-картинок всего 9, поэтому в большом наборе они повторяются по кругу. */
490
+ const IMAGE_COUNT = 9;
491
+
492
+ const ITEMS = Array.from({ length: 20 }, (_, i) => {
493
+ const imageIndex = (i % IMAGE_COUNT) + 1;
494
+
495
+ return {
496
+ id: `photo-${i + 1}`,
497
+ src: `assets/images/imageGallery/0${imageIndex}.jpg`,
498
+ alt: `Photo ${i + 1}`,
499
+ };
500
+ });
501
+
502
+ /**
503
+ * Большой набор (20 изображений) — проверяет горизонтальный скролл ленты миниатюр
504
+ * и автоцентровку активной миниатюры, когда они не помещаются в видимую область.
505
+ */
506
+ export const ManyThumbnails = () => {
507
+ const [selectedId, setSelectedId] = React.useState("photo-1");
508
+
509
+ return (
510
+ <ImageGalleryExtended items={ITEMS} selectedId={selectedId} onChange={setSelectedId}>
511
+ <ImageGalleryExtended.Main withBlur>
512
+ <ImageGalleryExtended.Nav>
513
+ {({ onPrev, onNext, isFirst, isLast, itemsCount }) => (
514
+ <>
515
+ <ImageGalleryExtended.Arrow
516
+ direction={EImageGalleryArrowDirection.PREV}
517
+ aria-label="Предыдущее изображение"
518
+ onClick={onPrev}
519
+ disabled={isFirst}
520
+ hidden={itemsCount <= 1}
521
+ />
522
+ <ImageGalleryExtended.Arrow
523
+ direction={EImageGalleryArrowDirection.NEXT}
524
+ aria-label="Следующее изображение"
525
+ onClick={onNext}
526
+ disabled={isLast}
527
+ hidden={itemsCount <= 1}
528
+ />
529
+ </>
530
+ )}
531
+ </ImageGalleryExtended.Nav>
532
+ </ImageGalleryExtended.Main>
533
+ <ImageGalleryExtended.Thumbnails />
534
+ </ImageGalleryExtended>
535
+ );
536
+ };
537
+ ```
538
+
539
+ ### OpenFromAvatar
540
+
541
+ `stories/ImageGalleryExtended/examples/OpenFromAvatar.tsx`
542
+
543
+ ```tsx
544
+ import React, { useState } from "react";
545
+ import { ImageGalleryExtended, LightBox, Page, EBodyPageType, Avatar, EAvatarSize } from "@sberbusiness/triplex-next";
546
+
547
+ const ITEMS = [{ id: "photo-1", src: "assets/images/imageGallery/01.jpg", alt: "Photo 1" }];
548
+
549
+ /**
550
+ * Сначала рендерится `Avatar` с миниатюрой изображения. Клик (или Enter/Space)
551
+ * открывает `LightBox`, внутри которого то же изображение показывается крупно
552
+ * через `ImageGalleryExtended.Main` в `Page.Body`.
553
+ */
554
+ export const OpenFromAvatar = () => {
555
+ const [selectedId, setSelectedId] = useState(ITEMS[0].id);
556
+ const [isOpen, setIsOpen] = useState(false);
557
+
558
+ const handleOpen = () => setIsOpen(true);
559
+ const handleClose = () => setIsOpen(false);
560
+
561
+ const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
562
+ if (event.key === "Enter" || event.key === " ") {
563
+ event.preventDefault();
564
+ handleOpen();
565
+ }
566
+ };
567
+
568
+ return (
569
+ <>
570
+ <Avatar
571
+ size={EAvatarSize.XXL}
572
+ borderRadius={12}
573
+ role="button"
574
+ tabIndex={0}
575
+ aria-label="Открыть изображение"
576
+ onClick={handleOpen}
577
+ onKeyDown={handleKeyDown}
578
+ style={{
579
+ backgroundImage: `url(${ITEMS[0].src})`,
580
+ backgroundSize: "cover",
581
+ cursor: "pointer",
582
+ }}
583
+ />
584
+
585
+ {isOpen ? (
586
+ <LightBox>
587
+ <LightBox.Content key="content">
588
+ <Page>
589
+ <Page.Body type={EBodyPageType.SECOND}>
590
+ <ImageGalleryExtended items={ITEMS} selectedId={selectedId} onChange={setSelectedId}>
591
+ <ImageGalleryExtended.Main withBlur height={592} />
592
+ </ImageGalleryExtended>
593
+ </Page.Body>
594
+ </Page>
595
+ </LightBox.Content>
596
+
597
+ <LightBox.Controls>
598
+ <LightBox.Controls.Close title="Закрыть" onClick={handleClose} />
599
+ </LightBox.Controls>
600
+ </LightBox>
601
+ ) : null}
602
+ </>
603
+ );
604
+ };
605
+ ```
606
+