@sberbusiness/triplex-next-mcp-server 1.41.0-beta → 1.42.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.
- package/bundle/components/AlertProcess.md +460 -0
- package/bundle/components/Amount.md +145 -169
- package/bundle/components/Badge.md +155 -219
- package/bundle/components/Body.md +108 -116
- package/bundle/components/CarouselExtended.md +355 -194
- package/bundle/components/ComposedKeyDownListener.md +323 -0
- package/bundle/components/FooterPage.md +2 -1
- package/bundle/components/HeaderPage.md +2 -1
- package/bundle/components/Island.md +100 -12
- package/bundle/components/KeyDownListener.md +11 -72
- package/bundle/components/LoaderSmall.md +230 -0
- package/bundle/components/MaskedField.md +481 -0
- package/bundle/components/MediaWidth.md +246 -108
- package/bundle/components/MobileView.md +192 -68
- package/bundle/components/ModalWindow.md +3 -1
- package/bundle/components/Page.md +3 -1
- package/bundle/components/Pagination.md +20 -0
- package/bundle/components/SelectExtendedField.md +691 -236
- package/bundle/guides/commits.md +40 -3
- package/bundle/guides/context.md +26 -0
- package/bundle/guides/stories.md +2 -0
- package/bundle/guides/template.md +1 -1
- package/bundle/manifest.json +43 -9
- package/bundle/release-notes/1.41.1.mdx +18 -0
- package/bundle/release-notes/1.42.0.mdx +81 -0
- package/bundle/release-notes/1.43.0.mdx +7 -0
- package/package.json +1 -1
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
---
|
|
2
2
|
component: CarouselExtended
|
|
3
|
-
category:
|
|
4
|
-
|
|
5
|
-
figma-file: ""
|
|
6
|
-
related: [TabsExtended, SliderExtended]
|
|
3
|
+
category: CarouselExtended
|
|
4
|
+
related: [Stepper]
|
|
7
5
|
tokens: []
|
|
8
|
-
stories:
|
|
6
|
+
stories: stories/CarouselExtended/CarouselExtended.stories.tsx
|
|
9
7
|
version: "1.0"
|
|
10
8
|
---
|
|
11
9
|
|
|
@@ -13,233 +11,198 @@ version: "1.0"
|
|
|
13
11
|
|
|
14
12
|
## Назначение
|
|
15
13
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
целиком.
|
|
14
|
+
Горизонтально прокручиваемая лента произвольного контента с кнопками прокрутки.
|
|
15
|
+
Компонент владеет только прокруткой и состоянием кнопок: разметку кнопок, их
|
|
16
|
+
внешний вид и позиционирование задаёт потребитель через рендер-функции
|
|
17
|
+
`buttonPrev` и `buttonNext`.
|
|
21
18
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
19
|
+
Используй когда: контент выкладывается в один горизонтальный ряд, его ширина
|
|
20
|
+
заранее неизвестна (шаги процесса, применённые фильтры, теги, набор периодов), и
|
|
21
|
+
нужны кнопки прокрутки в оформлении конкретного продукта.
|
|
25
22
|
|
|
26
|
-
|
|
27
|
-
ширину (табы, карточки, чипы) с кастомными кнопками навигации.
|
|
28
|
-
Не используй когда: нужна пагинация страниц (бери `Pagination`); нужна
|
|
29
|
-
вертикальная прокрутка (компонент только горизонтальный); нужна карусель
|
|
30
|
-
с auto-play / слайд-переходами (компонент не делает слайдов, только скролл).
|
|
23
|
+
Не используй когда:
|
|
31
24
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
└── styles/CarouselExtended.module.less # .carouselExtended (flex + скрытый скроллбар)
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
Особенности:
|
|
44
|
-
- **Нет stories** — компонент не демонстрируется в Storybook. Это известный
|
|
45
|
-
gap (в ROADMAP.md Storybook-колонка ⬜). Не создавать без общей задачи.
|
|
46
|
-
- **Нет subcomponents / compound'а** — кнопки рендерит потребитель через
|
|
47
|
-
render-пропы.
|
|
48
|
-
- **Нет дизайн-токенов** — компонент структурный, всё визуальное (цвет
|
|
49
|
-
кнопок, размеры) задаёт потребитель.
|
|
50
|
-
- **Использует `lodash` (не `lodash-es`)** — исключение из общего правила.
|
|
51
|
-
Не менять на lodash-es без общего рефакторинга импортов (вероятно
|
|
52
|
-
исторически, до перехода на ESM).
|
|
25
|
+
- Нужна слайдовая карусель с постраничной прокруткой, индикаторами, клавиатурой
|
|
26
|
+
и touch-жестами — для этого есть составной `Carousel`
|
|
27
|
+
(`src/components/Carousel/`) с `Viewport` / `Track` / `Item` / `Indicators`.
|
|
28
|
+
- Нужна лента шагов процесса — возьми `Stepper`, он уже собран на
|
|
29
|
+
`CarouselExtended` и добавляет выравнивание по выбранному шагу.
|
|
30
|
+
- Контент заведомо помещается в контейнер — обычного блока достаточно,
|
|
31
|
+
компонент не даст ничего, кроме скрытого скроллбара.
|
|
32
|
+
- Прокрутка нужна по вертикали — компонент прокручивает только по горизонтали.
|
|
53
33
|
|
|
54
34
|
---
|
|
55
35
|
|
|
56
36
|
## Варианты и props
|
|
57
37
|
|
|
58
|
-
###
|
|
59
|
-
|
|
60
|
-
**Обязательные:**
|
|
38
|
+
### Обязательные props
|
|
61
39
|
|
|
62
40
|
| Prop | Тип | Описание |
|
|
63
41
|
|---|---|---|
|
|
64
|
-
| `buttonPrev` | `(props: ICarouselExtendedButtonProvideProps) => React.ReactNode` |
|
|
65
|
-
| `buttonNext` | `(props: ICarouselExtendedButtonProvideProps) => React.ReactNode` |
|
|
66
|
-
| `stepPrev` | `number` |
|
|
67
|
-
| `stepNext` | `number` |
|
|
42
|
+
| `buttonPrev` | `(props: ICarouselExtendedButtonProvideProps) => React.ReactNode` | Рендер-функция кнопки «Назад». Вызывается на каждом рендере, результат ставится перед лентой |
|
|
43
|
+
| `buttonNext` | `(props: ICarouselExtendedButtonProvideProps) => React.ReactNode` | Рендер-функция кнопки «Вперёд». Результат ставится после ленты |
|
|
44
|
+
| `stepPrev` | `number` | Величина прокрутки в px за один клик по кнопке «Назад» |
|
|
45
|
+
| `stepNext` | `number` | Величина прокрутки в px за один клик по кнопке «Вперёд» |
|
|
68
46
|
|
|
69
|
-
|
|
47
|
+
`children` и остальные атрибуты `React.HTMLAttributes<HTMLDivElement>`
|
|
48
|
+
(`className`, `style`, `data-*`, обработчики) — опциональны и уходят на корневой
|
|
49
|
+
элемент.
|
|
70
50
|
|
|
71
|
-
|
|
72
|
-
|---|---|---|
|
|
73
|
-
| `children` | `React.ReactNode` | Контент карусели (чипы, карточки, любые inline-элементы). |
|
|
74
|
-
| `className`, `style`, `id`, остальные HTML-атрибуты — передаются на **внешний** div-обёртку, НЕ на скроллируемый контейнер. |
|
|
51
|
+
### Что получают рендер-функции кнопок
|
|
75
52
|
|
|
76
|
-
|
|
53
|
+
`ICarouselExtendedButtonProvideProps` — это `React.ButtonHTMLAttributes`, но
|
|
54
|
+
фактически компонент передаёт ровно три поля:
|
|
77
55
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
|
81
|
-
|
|
82
|
-
| `
|
|
83
|
-
|
|
84
|
-
|
|
56
|
+
| Поле | Когда выставляется |
|
|
57
|
+
|---|---|
|
|
58
|
+
| `disabled` | Достигнут соответствующий край ленты. Для `buttonPrev` — `scrollLeft <= 0`, для `buttonNext` — правый край |
|
|
59
|
+
| `hidden` | Прокрутка не нужна: `scrollWidth === clientWidth` |
|
|
60
|
+
| `onClick` | Всегда. Запускает анимированную прокрутку на `stepPrev` / `stepNext` |
|
|
61
|
+
|
|
62
|
+
### Особенности поведения
|
|
63
|
+
|
|
64
|
+
- **`hidden` — это подсказка, а не автоматическое скрытие.** Компонент всегда
|
|
65
|
+
вызывает обе рендер-функции; решение принимает потребитель. Обычно `hidden`
|
|
66
|
+
деструктурируют и возвращают `null` (так делают `Stepper` и все примеры).
|
|
67
|
+
Если прокинуть `hidden` дальше на нативную кнопку, сработает HTML-атрибут
|
|
68
|
+
`hidden` и кнопка скроется через `display: none` — в вёрстке на flex/grid это
|
|
69
|
+
даёт другой результат, чем отсутствие узла.
|
|
70
|
+
- **Начальное состояние до первого измерения:** `hidden: true`,
|
|
71
|
+
`prevDisabled: true`, `nextDisabled: false`. Render-функции вызываются уже на
|
|
72
|
+
первом рендере, поэтому наличие кнопок в DOM определяет потребитель: при
|
|
73
|
+
рекомендованном `hidden → null` на первом кадре их нет. Реальное состояние
|
|
74
|
+
приходит после первого `useEffect`.
|
|
75
|
+
- **Когда пересчитывается состояние кнопок:** монтирование, смена `children`,
|
|
76
|
+
`resize` окна, `scroll` документа и прокрутка самой ленты (`onScroll`).
|
|
77
|
+
`ResizeObserver` не используется — изменение ширины контейнера **без** ресайза
|
|
78
|
+
окна (раскрытие соседней панели, смена лейаута) само по себе пересчёта не
|
|
79
|
+
вызывает. Состояние также обновится при любой прокрутке ленты.
|
|
80
|
+
- **Прокрутка анимирована** утилитой `scrollSmoothHorizontally`: 24 кадра через
|
|
81
|
+
`requestAnimationFrame`. Клик во время анимации не отменяет предыдущую —
|
|
82
|
+
анимации складываются.
|
|
83
|
+
- **Дробный шаг округляется вверх по модулю:** `Math.floor(-stepPrev)` и
|
|
84
|
+
`Math.ceil(stepNext)`. Шаг `200.2` даёт прокрутку на `201` px в обе стороны.
|
|
85
|
+
- **Скроллбар скрыт стилями** (`scrollbar-width: none`, `-ms-overflow-style`,
|
|
86
|
+
`::-webkit-scrollbar`). Прокрутка колесом, тачпадом и тач-жестом работает
|
|
87
|
+
штатно, но визуального индикатора наличия скрытого контента нет — единственная
|
|
88
|
+
подсказка для пользователя — кнопки.
|
|
89
|
+
- **Внутренний контейнер ленты не настраивается снаружи:** `className` и `style`
|
|
90
|
+
уходят на корневой элемент, у прокручиваемого `div` только собственный класс.
|
|
91
|
+
Поэтому `gap` между элементами задать нельзя — отступы вешаются на сами
|
|
92
|
+
элементы (`marginRight` в примерах). Элементы ленты — flex-элементы: чтобы они
|
|
93
|
+
не сжимались и лента действительно прокручивалась, им нужен `flex: 0 0 auto`.
|
|
94
|
+
- **`hidden` считается по `clientWidth`, а `nextDisabled` — по `offsetWidth`.**
|
|
95
|
+
Значения расходятся, если у ленты появятся границы, padding-бокс с рамкой или
|
|
96
|
+
горизонтальный скроллбар занимает место. Сейчас стили этого не допускают.
|
|
85
97
|
|
|
86
98
|
---
|
|
87
99
|
|
|
88
|
-
##
|
|
89
|
-
|
|
90
|
-
### `forwardRef` с компонентным разделением refs
|
|
91
|
-
|
|
92
|
-
```ts
|
|
93
|
-
const carouselRef = useRef<HTMLDivElement | null>(null);
|
|
94
|
-
const setRef = (instance: HTMLDivElement | null) => {
|
|
95
|
-
carouselRef.current = instance;
|
|
96
|
-
if (typeof ref === "function") ref(instance);
|
|
97
|
-
else if (ref) ref.current = instance;
|
|
98
|
-
};
|
|
99
|
-
```
|
|
100
|
-
|
|
101
|
-
`ref`, прокинутый через `forwardRef`, указывает на **скроллируемый внутренний**
|
|
102
|
-
div, НЕ на внешнюю обёртку (на которую идут htmlDivAttributes). Это
|
|
103
|
-
осознанное решение: ref нужен для программного управления скроллом.
|
|
104
|
-
**Не путать с внешней div-обёрткой** — она без ref и, например, `className`
|
|
105
|
-
потребителя идёт на неё.
|
|
106
|
-
|
|
107
|
-
### `checkControls` вычисляет `hidden`, `nextDisabled`, `prevDisabled`
|
|
108
|
-
|
|
109
|
-
```ts
|
|
110
|
-
hidden: carousel.scrollWidth == carousel.clientWidth,
|
|
111
|
-
nextDisabled: Math.round(carousel.scrollLeft) + carousel.offsetWidth >= carousel.scrollWidth,
|
|
112
|
-
prevDisabled: Math.round(carousel.scrollLeft) <= 0,
|
|
113
|
-
```
|
|
114
|
-
|
|
115
|
-
`Math.round` на `scrollLeft` — защита от subpixel-значений (при zoom или
|
|
116
|
-
DPR ≠ 1 значение может быть дробным, что ломает strict-сравнение).
|
|
117
|
-
`offsetWidth` vs `clientWidth` — намеренно. `offsetWidth` включает границы
|
|
118
|
-
(у компонента их нет → эквивалентно), а `scrollWidth` и `clientWidth` могут
|
|
119
|
-
разойтись на 1px при dense rendering.
|
|
120
|
-
|
|
121
|
-
### `isEqual` предотвращает лишние ререндеры
|
|
122
|
-
|
|
123
|
-
```ts
|
|
124
|
-
if (isEqual(newState, controlsState) == false) {
|
|
125
|
-
setControlsState(newState);
|
|
126
|
-
}
|
|
127
|
-
```
|
|
128
|
-
|
|
129
|
-
Без этой проверки каждый `scroll`-event триггерил бы `setState` → ререндер
|
|
130
|
-
children. Глубокое сравнение из lodash: объект маленький (3 булевых поля),
|
|
131
|
-
но на hot-path.
|
|
132
|
-
|
|
133
|
-
### Слушает и window.resize, и document.scroll
|
|
134
|
-
|
|
135
|
-
```ts
|
|
136
|
-
window.addEventListener("resize", checkControls);
|
|
137
|
-
document.addEventListener("scroll", checkControls);
|
|
138
|
-
```
|
|
139
|
-
|
|
140
|
-
**`document.scroll`** — не очевидное решение. Нужно, потому что при скролле
|
|
141
|
-
самой страницы могут измениться размеры (например, если карусель внутри
|
|
142
|
-
sticky-элемента, его реальная ширина меняется). Не убирать.
|
|
143
|
-
|
|
144
|
-
Плюс `onScroll` на самом carousel-div'е — для скролла внутри карусели.
|
|
145
|
-
|
|
146
|
-
### `scrollSmoothHorizontally` с `requestAnimationFrame`
|
|
147
|
-
|
|
148
|
-
`src/utils/scroll.ts` реализует плавный скролл через рекурсивный
|
|
149
|
-
`requestAnimationFrame` (24 кадра по умолчанию), уменьшая amount каждый
|
|
150
|
-
кадр. **Не использует** `scrollTo({behavior: "smooth"})` — вероятно для
|
|
151
|
-
поддержки старых браузеров (IE, старых Edge) или для единообразия скорости
|
|
152
|
-
анимации.
|
|
153
|
-
|
|
154
|
-
### `useEffect(checkControls, [children, checkControls])`
|
|
155
|
-
|
|
156
|
-
Перевычисляет состояние кнопок, когда меняются children. Без этого при
|
|
157
|
-
динамическом добавлении/удалении карточек кнопки могли бы остаться в
|
|
158
|
-
неправильном состоянии.
|
|
159
|
-
|
|
160
|
-
### `// eslint-disable-next-line react-hooks/refs`
|
|
161
|
-
|
|
162
|
-
Два места с disable — потому что `react-hooks/refs` правило не понимает
|
|
163
|
-
render-пропы (думает, что ref утекает). Не удалять, правило тут ложно
|
|
164
|
-
срабатывает.
|
|
165
|
-
|
|
166
|
-
### Скрытый скроллбар в LESS
|
|
167
|
-
|
|
168
|
-
```less
|
|
169
|
-
scrollbar-width: none; // Firefox
|
|
170
|
-
-ms-overflow-style: none; // IE + Edge
|
|
171
|
-
&::-webkit-scrollbar { display: none; } // Chrome/Safari
|
|
172
|
-
```
|
|
100
|
+
## Дизайн-токены
|
|
173
101
|
|
|
174
|
-
|
|
175
|
-
|
|
102
|
+
Собственных CSS-переменных у компонента нет: `CarouselExtended.module.less`
|
|
103
|
+
задаёт только раскладку прокручиваемой ленты (`display: flex`,
|
|
104
|
+
`position: relative`, `overflow-y: hidden` — из-за чего `overflow-x` вычисляется
|
|
105
|
+
в `auto`) и скрывает скроллбар во всех браузерных движках. Цвета и размеры
|
|
106
|
+
приходят из контента и из кнопок, которые рендерит потребитель.
|
|
176
107
|
|
|
177
108
|
---
|
|
178
109
|
|
|
179
110
|
## Инварианты
|
|
180
111
|
|
|
181
|
-
- **`forwardRef` на
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
потребитель
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
-
|
|
196
|
-
|
|
197
|
-
|
|
112
|
+
- **`forwardRef` указывает на прокручиваемый контейнер, а не на корневой
|
|
113
|
+
элемент.** Это неочевидно и является публичным контрактом: `Stepper` читает по
|
|
114
|
+
этому ref `scrollLeft`, `getBoundingClientRect` и напрямую прокручивает ленту.
|
|
115
|
+
Переносить ref на корневой `div` нельзя.
|
|
116
|
+
Внутренний ref-колбэк мемоизирован по `ref`, поэтому от повторного отцепления и
|
|
117
|
+
прицепления узла избавлен только потребитель со стабильным ref (объектный или
|
|
118
|
+
мемоизированный колбэк). Инлайновый `ref={(node) => …}` пересоздаётся сам, и
|
|
119
|
+
React отцепляет узел на каждом рендере — это ограничение React, а не компонента.
|
|
120
|
+
- **`className`, `style` и `...rest` уходят на корневой элемент** — именно на нём
|
|
121
|
+
потребитель строит раскладку (`display: flex`, `position: relative` для
|
|
122
|
+
наложенных кнопок). Не переносить на ленту.
|
|
123
|
+
- **Порядок узлов в DOM:** `buttonPrev` → лента → `buttonNext`. Потребители
|
|
124
|
+
позиционируют кнопки относительно ленты (`Stepper` — абсолютно поверх краёв),
|
|
125
|
+
поэтому вставка дополнительных обёрток ломает их вёрстку.
|
|
126
|
+
- **Имена и типы публичных сущностей** — `CarouselExtended`,
|
|
127
|
+
`ICarouselExtendedProps`, `ICarouselExtendedButtonProvideProps`, а также имена
|
|
128
|
+
props `buttonPrev` / `buttonNext` / `stepPrev` / `stepNext` — часть публичного
|
|
129
|
+
API. `ICarouselExtendedButtonProvideProps` выглядит опечаткой («Provide» вместо
|
|
130
|
+
«Provided»), но переименование — breaking change и делается только отдельным
|
|
131
|
+
решением с записью в release notes.
|
|
132
|
+
- **`buttonPrev` и `buttonNext` — обязательные рендер-функции**, а не
|
|
133
|
+
`React.ReactNode`. Они вызываются во время рендера, поэтому не должны содержать
|
|
134
|
+
side-эффектов и собственных хуков.
|
|
135
|
+
- **`displayName`** равен `"CarouselExtended"`.
|
|
136
|
+
- **Класс `carouselExtended`** — единственный якорь прокручиваемого контейнера,
|
|
137
|
+
на него опираются unit-тесты.
|
|
138
|
+
- Реализация остаётся React 17-совместимой (без `useId`,
|
|
139
|
+
`useSyncExternalStore` и прочих React 18-only API): код синхронизируется
|
|
140
|
+
в `release-0`.
|
|
198
141
|
|
|
199
142
|
---
|
|
200
143
|
|
|
201
144
|
## Accessibility
|
|
202
145
|
|
|
203
|
-
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
Нет — компонент полностью структурный. Цвета/размеры задаёт контент и
|
|
223
|
-
внешние кнопки.
|
|
146
|
+
- Компонент не добавляет ролей и aria-атрибутов: ни корневой элемент, ни лента не
|
|
147
|
+
интерактивны и прозрачны для accessibility-дерева.
|
|
148
|
+
- **`aria-label` кнопок — обязанность потребителя.** Компонент не хардкодит текст,
|
|
149
|
+
библиотека мультиязычная. В примерах кнопки подписаны
|
|
150
|
+
`aria-label="Прокрутить назад"` / `"Прокрутить вперёд"`.
|
|
151
|
+
- `disabled` приходит в рендер-функцию: если прокинуть его на нативную `<button>`
|
|
152
|
+
(как делают `ButtonIcon` и примеры), кнопка на краю ленты корректно выпадает из
|
|
153
|
+
порядка табуляции и не читается как активная.
|
|
154
|
+
- Кнопки — вспомогательная навигация, а не единственный способ добраться до
|
|
155
|
+
контента: лента прокручивается колесом, тачпадом и тач-жестом. Для
|
|
156
|
+
клавиатурной прокрутки самой ленты контейнер должен попасть в фокус — своего
|
|
157
|
+
`tabIndex` у него нет. Если контент внутри некликабельный (текст, теги без
|
|
158
|
+
обработчиков), добавь фокусируемую обёртку на стороне потребителя.
|
|
159
|
+
- Прокрутка кнопкой не переносит фокус и не меняет DOM-порядок — контент,
|
|
160
|
+
уехавший за край, остаётся доступным скринридеру и табуляции.
|
|
161
|
+
- `Stepper` дополнительно ставит кнопкам `tabIndex={-1}`: там прокрутка
|
|
162
|
+
дублируется выбором шага, и кнопки намеренно исключены из табуляции. Это
|
|
163
|
+
решение потребителя, а не поведение `CarouselExtended`.
|
|
224
164
|
|
|
225
165
|
---
|
|
226
166
|
|
|
227
167
|
## Связанные компоненты
|
|
228
168
|
|
|
229
|
-
- `
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
169
|
+
- `Stepper` (`src/components/Stepper/`) — единственный потребитель внутри
|
|
170
|
+
библиотеки и единственное место, которое опирается на хрупкую деталь: ref
|
|
171
|
+
указывает на прокручиваемую ленту. `Stepper` передаёт объектный ref, считает
|
|
172
|
+
шаги прокрутки через собственный `ResizeObserver` и сам выравнивает выбранный
|
|
173
|
+
шаг, меняя `scrollLeft`. Любое изменение ref-таргета или порядка узлов сломает
|
|
174
|
+
его в первую очередь.
|
|
175
|
+
- `Carousel` (`src/components/Carousel/`) — современная слайдовая карусель со
|
|
176
|
+
своим составным API (`Carousel.Viewport`, `Carousel.Track`, `Carousel.Item`,
|
|
177
|
+
`Carousel.Indicators`, `Carousel.PrevButton` / `NextButton`), клавиатурой и
|
|
178
|
+
touch-жестами. Решает соседнюю задачу: там прокрутка постраничная или по
|
|
179
|
+
элементам, здесь — свободная лента произвольного контента. В таблице
|
|
180
|
+
`docs/ai/ROADMAP.md` пока нет, поэтому в `related` не указан.
|
|
181
|
+
- `ButtonIcon` (`src/components/Button/ButtonIcon.tsx`) — то, что обычно
|
|
182
|
+
возвращают рендер-функции кнопок: он принимает `disabled` и `onClick` без
|
|
183
|
+
дополнительной обвязки. Компонент его не импортирует, выбор кнопки за
|
|
184
|
+
потребителем.
|
|
185
|
+
- `scrollSmoothHorizontally` (`src/utils/scroll.ts`) — публичная утилита, на
|
|
186
|
+
которой построена анимация прокрутки. Тот же хелпер использует `Stepper`.
|
|
235
187
|
|
|
236
188
|
---
|
|
237
189
|
|
|
238
190
|
## Stories
|
|
239
191
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
192
|
+
Основные истории: `stories/CarouselExtended/CarouselExtended.stories.tsx`
|
|
193
|
+
Файлы примеров: `stories/CarouselExtended/examples/`
|
|
194
|
+
|
|
195
|
+
| Story | Example file | Что демонстрирует |
|
|
196
|
+
|---|---|---|
|
|
197
|
+
| `Playground` | `Playground.tsx` | Интерактивный подбор `stepPrev` / `stepNext`, количества элементов и ширины ленты |
|
|
198
|
+
| `Default` | `Default.tsx` | Лента шире видимой области: кнопки видны, «Назад» заблокирована в начальном положении |
|
|
199
|
+
| `WithShortContent` | `WithShortContent.tsx` | Контент помещается целиком: в рендер-функции приходит `hidden`, кнопки не рендерятся |
|
|
200
|
+
| `Example: лента применённых фильтров` | `Example.tsx` | Production-like композиция с `Tag`: удаление фильтра меняет `children` и пересчитывает состояние кнопок |
|
|
201
|
+
| `VisualTests` | `VisualTests.tsx` | Состояния, недостижимые без взаимодействия: лента, прокрученная в середину (активны обе кнопки) и до конца (кнопка «Вперёд» заблокирована) |
|
|
202
|
+
|
|
203
|
+
`Playground` исключён из скриншот-тестов. Остальные stories снимаются на
|
|
204
|
+
viewport'ах `xs` и `xl`; ширина ленты зафиксирована в px, поэтому кадры на обоих
|
|
205
|
+
viewport'ах отличаются только окружением.
|
|
243
206
|
|
|
244
207
|
---
|
|
245
208
|
|
|
@@ -247,4 +210,202 @@ scrollbar-width: none; // Firefox
|
|
|
247
210
|
|
|
248
211
|
| Дата | Изменение |
|
|
249
212
|
|---|---|
|
|
250
|
-
| 2026-
|
|
213
|
+
| 2026-08-13 | Создан документ. AI-рефакторинг (TRI-20): пересчёт состояния кнопок переведён на функциональный `setState` (слушатели `resize` / `scroll` больше не переподписываются на каждое изменение состояния), ref-колбэк стабилизирован, убраны non-null assertion'ы и устаревшие `eslint-disable`, импорт `isEqual` переведён с `lodash` на `lodash-es`, дополнен JSDoc props и компонента. Добавлены unit-тесты (20 кейсов) и stories в modern pattern. Публичный API не изменён: props, интерфейсы, barrel-экспорты, `forwardRef` на прокручиваемый контейнер и итоговая разметка — прежние. |
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
## Примеры использования
|
|
218
|
+
|
|
219
|
+
### Default
|
|
220
|
+
|
|
221
|
+
`stories/CarouselExtended/examples/Default.tsx`
|
|
222
|
+
|
|
223
|
+
```tsx
|
|
224
|
+
import React from "react";
|
|
225
|
+
import { CaretleftStrokeSrvIcon24, CaretrightStrokeSrvIcon24 } from "@sberbusiness/icons-next";
|
|
226
|
+
import { ButtonIcon, CarouselExtended, ICarouselExtendedButtonProvideProps } from "@sberbusiness/triplex-next";
|
|
227
|
+
|
|
228
|
+
const MONTHS = [
|
|
229
|
+
"Январь",
|
|
230
|
+
"Февраль",
|
|
231
|
+
"Март",
|
|
232
|
+
"Апрель",
|
|
233
|
+
"Май",
|
|
234
|
+
"Июнь",
|
|
235
|
+
"Июль",
|
|
236
|
+
"Август",
|
|
237
|
+
"Сентябрь",
|
|
238
|
+
"Октябрь",
|
|
239
|
+
"Ноябрь",
|
|
240
|
+
"Декабрь",
|
|
241
|
+
];
|
|
242
|
+
|
|
243
|
+
/** Величина прокрутки за один клик по кнопке. */
|
|
244
|
+
const SCROLL_STEP = 200;
|
|
245
|
+
|
|
246
|
+
/** Пока прокрутка не нужна, компонент передаёт hidden: кнопка не рендерится. */
|
|
247
|
+
const renderPrevButton = ({ hidden, ...buttonProps }: ICarouselExtendedButtonProvideProps) =>
|
|
248
|
+
hidden ? null : (
|
|
249
|
+
<ButtonIcon aria-label="Прокрутить назад" {...buttonProps}>
|
|
250
|
+
<CaretleftStrokeSrvIcon24 paletteIndex={5} />
|
|
251
|
+
</ButtonIcon>
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
const renderNextButton = ({ hidden, ...buttonProps }: ICarouselExtendedButtonProvideProps) =>
|
|
255
|
+
hidden ? null : (
|
|
256
|
+
<ButtonIcon aria-label="Прокрутить вперёд" {...buttonProps}>
|
|
257
|
+
<CaretrightStrokeSrvIcon24 paletteIndex={5} />
|
|
258
|
+
</ButtonIcon>
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
export const Default = () => (
|
|
262
|
+
<CarouselExtended
|
|
263
|
+
style={{ display: "flex", alignItems: "center", width: "480px" }}
|
|
264
|
+
buttonPrev={renderPrevButton}
|
|
265
|
+
buttonNext={renderNextButton}
|
|
266
|
+
stepPrev={SCROLL_STEP}
|
|
267
|
+
stepNext={SCROLL_STEP}
|
|
268
|
+
>
|
|
269
|
+
{MONTHS.map((month) => (
|
|
270
|
+
<div
|
|
271
|
+
key={month}
|
|
272
|
+
style={{
|
|
273
|
+
flex: "0 0 auto",
|
|
274
|
+
marginRight: "8px",
|
|
275
|
+
padding: "8px 16px",
|
|
276
|
+
border: "1px solid rgb(200, 202, 205)",
|
|
277
|
+
borderRadius: "8px",
|
|
278
|
+
}}
|
|
279
|
+
>
|
|
280
|
+
{month}
|
|
281
|
+
</div>
|
|
282
|
+
))}
|
|
283
|
+
</CarouselExtended>
|
|
284
|
+
);
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
### WithShortContent
|
|
288
|
+
|
|
289
|
+
`stories/CarouselExtended/examples/WithShortContent.tsx`
|
|
290
|
+
|
|
291
|
+
```tsx
|
|
292
|
+
import React from "react";
|
|
293
|
+
import { CaretleftStrokeSrvIcon24, CaretrightStrokeSrvIcon24 } from "@sberbusiness/icons-next";
|
|
294
|
+
import { ButtonIcon, CarouselExtended, ICarouselExtendedButtonProvideProps } from "@sberbusiness/triplex-next";
|
|
295
|
+
|
|
296
|
+
const MONTHS = ["Январь", "Февраль", "Март"];
|
|
297
|
+
|
|
298
|
+
/** Величина прокрутки за один клик по кнопке. */
|
|
299
|
+
const SCROLL_STEP = 200;
|
|
300
|
+
|
|
301
|
+
/** Контент помещается в ленту целиком, поэтому компонент передаёт hidden и кнопки не рендерятся. */
|
|
302
|
+
const renderPrevButton = ({ hidden, ...buttonProps }: ICarouselExtendedButtonProvideProps) =>
|
|
303
|
+
hidden ? null : (
|
|
304
|
+
<ButtonIcon aria-label="Прокрутить назад" {...buttonProps}>
|
|
305
|
+
<CaretleftStrokeSrvIcon24 paletteIndex={5} />
|
|
306
|
+
</ButtonIcon>
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
const renderNextButton = ({ hidden, ...buttonProps }: ICarouselExtendedButtonProvideProps) =>
|
|
310
|
+
hidden ? null : (
|
|
311
|
+
<ButtonIcon aria-label="Прокрутить вперёд" {...buttonProps}>
|
|
312
|
+
<CaretrightStrokeSrvIcon24 paletteIndex={5} />
|
|
313
|
+
</ButtonIcon>
|
|
314
|
+
);
|
|
315
|
+
|
|
316
|
+
export const WithShortContent = () => (
|
|
317
|
+
<CarouselExtended
|
|
318
|
+
style={{ display: "flex", alignItems: "center", width: "480px" }}
|
|
319
|
+
buttonPrev={renderPrevButton}
|
|
320
|
+
buttonNext={renderNextButton}
|
|
321
|
+
stepPrev={SCROLL_STEP}
|
|
322
|
+
stepNext={SCROLL_STEP}
|
|
323
|
+
>
|
|
324
|
+
{MONTHS.map((month) => (
|
|
325
|
+
<div
|
|
326
|
+
key={month}
|
|
327
|
+
style={{
|
|
328
|
+
flex: "0 0 auto",
|
|
329
|
+
marginRight: "8px",
|
|
330
|
+
padding: "8px 16px",
|
|
331
|
+
border: "1px solid rgb(200, 202, 205)",
|
|
332
|
+
borderRadius: "8px",
|
|
333
|
+
}}
|
|
334
|
+
>
|
|
335
|
+
{month}
|
|
336
|
+
</div>
|
|
337
|
+
))}
|
|
338
|
+
</CarouselExtended>
|
|
339
|
+
);
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
### Example: лента применённых фильтров
|
|
343
|
+
|
|
344
|
+
`stories/CarouselExtended/examples/Example.tsx`
|
|
345
|
+
|
|
346
|
+
```tsx
|
|
347
|
+
import React, { useState } from "react";
|
|
348
|
+
import { CaretleftStrokeSrvIcon24, CaretrightStrokeSrvIcon24 } from "@sberbusiness/icons-next";
|
|
349
|
+
import {
|
|
350
|
+
ButtonIcon,
|
|
351
|
+
CarouselExtended,
|
|
352
|
+
EComponentSize,
|
|
353
|
+
ICarouselExtendedButtonProvideProps,
|
|
354
|
+
Tag,
|
|
355
|
+
} from "@sberbusiness/triplex-next";
|
|
356
|
+
|
|
357
|
+
const INITIAL_FILTERS = [
|
|
358
|
+
{ id: "period", label: "Период: 01.01.2026 — 31.03.2026" },
|
|
359
|
+
{ id: "account", label: "Счёт: 40702810000000000001" },
|
|
360
|
+
{ id: "counterparty", label: "Контрагент: ООО «Ромашка»" },
|
|
361
|
+
{ id: "amount", label: "Сумма: от 10 000 ₽" },
|
|
362
|
+
{ id: "status", label: "Статус: исполнено" },
|
|
363
|
+
{ id: "currency", label: "Валюта: RUB" },
|
|
364
|
+
];
|
|
365
|
+
|
|
366
|
+
/** Величина прокрутки за один клик по кнопке. */
|
|
367
|
+
const SCROLL_STEP = 200;
|
|
368
|
+
|
|
369
|
+
const renderPrevButton = ({ hidden, ...buttonProps }: ICarouselExtendedButtonProvideProps) =>
|
|
370
|
+
hidden ? null : (
|
|
371
|
+
<ButtonIcon aria-label="Прокрутить назад" {...buttonProps}>
|
|
372
|
+
<CaretleftStrokeSrvIcon24 paletteIndex={5} />
|
|
373
|
+
</ButtonIcon>
|
|
374
|
+
);
|
|
375
|
+
|
|
376
|
+
const renderNextButton = ({ hidden, ...buttonProps }: ICarouselExtendedButtonProvideProps) =>
|
|
377
|
+
hidden ? null : (
|
|
378
|
+
<ButtonIcon aria-label="Прокрутить вперёд" {...buttonProps}>
|
|
379
|
+
<CaretrightStrokeSrvIcon24 paletteIndex={5} />
|
|
380
|
+
</ButtonIcon>
|
|
381
|
+
);
|
|
382
|
+
|
|
383
|
+
export const Example = () => {
|
|
384
|
+
const [filters, setFilters] = useState(INITIAL_FILTERS);
|
|
385
|
+
|
|
386
|
+
const handleRemove = (id: string) => setFilters((prevFilters) => prevFilters.filter((filter) => filter.id !== id));
|
|
387
|
+
|
|
388
|
+
return (
|
|
389
|
+
<CarouselExtended
|
|
390
|
+
style={{ display: "flex", alignItems: "center", width: "480px" }}
|
|
391
|
+
buttonPrev={renderPrevButton}
|
|
392
|
+
buttonNext={renderNextButton}
|
|
393
|
+
stepPrev={SCROLL_STEP}
|
|
394
|
+
stepNext={SCROLL_STEP}
|
|
395
|
+
>
|
|
396
|
+
{filters.map(({ id, label }) => (
|
|
397
|
+
<Tag
|
|
398
|
+
key={id}
|
|
399
|
+
id={id}
|
|
400
|
+
size={EComponentSize.MD}
|
|
401
|
+
style={{ flex: "0 0 auto", marginRight: "8px" }}
|
|
402
|
+
onRemove={handleRemove}
|
|
403
|
+
>
|
|
404
|
+
{label}
|
|
405
|
+
</Tag>
|
|
406
|
+
))}
|
|
407
|
+
</CarouselExtended>
|
|
408
|
+
);
|
|
409
|
+
};
|
|
410
|
+
```
|
|
411
|
+
|