@sberbusiness/triplex-next-mcp-server 0.24.0-beta2 → 0.36.0-beta0
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.
- package/README.md +10 -0
- package/bundle/components/BodyPage.md +186 -0
- package/bundle/components/Button.md +90 -85
- package/bundle/components/ButtonDropdown.md +28 -70
- package/bundle/components/ButtonIcon.md +35 -48
- package/bundle/components/Checkbox.md +43 -122
- package/bundle/components/CheckboxXGroup.md +109 -46
- package/bundle/components/CheckboxYGroup.md +77 -49
- package/bundle/components/CollapsibleTree.md +581 -0
- package/bundle/components/CollapsibleTreeExtended.md +418 -0
- package/bundle/components/EmptyView.md +14 -15
- package/bundle/components/FooterPage.md +203 -0
- package/bundle/components/Gap.md +115 -57
- package/bundle/components/HeaderPage.md +226 -0
- package/bundle/components/HelpBox.md +45 -84
- package/bundle/components/ImageGallery.md +423 -0
- package/bundle/components/ImageGalleryExtended.md +606 -0
- package/bundle/components/List.md +194 -186
- package/bundle/components/ListItem.md +251 -0
- package/bundle/components/ListItemControlsButton.md +103 -0
- package/bundle/components/ListItemControlsButtonDropdown.md +107 -0
- package/bundle/components/ListItemSelectable.md +99 -0
- package/bundle/components/ListItemTable.md +390 -0
- package/bundle/components/ListMaster.md +877 -156
- package/bundle/components/ListMasterFooter.md +80 -0
- package/bundle/components/ListMasterHeader.md +86 -0
- package/bundle/components/ListSortable.md +153 -0
- package/bundle/components/ListSortableItem.md +175 -0
- package/bundle/components/ModalWindow.md +427 -237
- package/bundle/components/MonthYearField.md +145 -109
- package/bundle/components/Notification.md +625 -155
- package/bundle/components/Overlay.md +271 -170
- package/bundle/components/Page.md +283 -186
- package/bundle/components/Pagination.md +206 -229
- package/bundle/guides/codestyle.md +38 -11
- package/bundle/guides/commits.md +1 -2
- package/bundle/guides/context.md +34 -30
- package/bundle/guides/stories.md +110 -40
- package/bundle/guides/template.md +3 -7
- package/bundle/guides/tests.md +6 -2
- package/bundle/manifest.json +194 -24
- package/bundle/release-notes/1.26.0.mdx +33 -2
- package/bundle/release-notes/1.27.0.mdx +43 -0
- package/bundle/release-notes/1.28.0.mdx +25 -0
- package/bundle/release-notes/1.29.0.mdx +47 -0
- package/bundle/release-notes/1.30.0.mdx +56 -0
- package/bundle/release-notes/1.31.0.mdx +52 -0
- package/bundle/release-notes/1.32.0.mdx +35 -0
- package/bundle/release-notes/1.33.0.mdx +61 -0
- package/bundle/release-notes/1.34.0.mdx +35 -0
- package/bundle/release-notes/1.35.0.mdx +21 -0
- package/bundle/release-notes/1.35.1.mdx +11 -0
- package/bundle/release-notes/1.36.0.mdx +21 -0
- package/bundle/release-notes/1.37.0.mdx +30 -0
- 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
|
+
|