@sberbusiness/triplex-next-mcp-server 1.46.0-beta → 1.48.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 (48) hide show
  1. package/README.md +7 -7
  2. package/bundle/components/AmountField.md +222 -323
  3. package/bundle/components/Avatar.md +138 -146
  4. package/bundle/components/CardStatic.md +402 -0
  5. package/bundle/components/CheckboxTree.md +157 -215
  6. package/bundle/components/CheckboxTreeExtended.md +7 -7
  7. package/bundle/components/ChipGroup.md +1 -1
  8. package/bundle/components/ChipMonthYearPicker.md +4 -4
  9. package/bundle/components/ChipSuggest.md +7 -6
  10. package/bundle/components/CollapsibleTree.md +8 -4
  11. package/bundle/components/CollapsibleTreeExtended.md +8 -6
  12. package/bundle/components/Confirm.md +346 -281
  13. package/bundle/components/DateRange.md +351 -210
  14. package/bundle/components/FooterPage.md +1 -0
  15. package/bundle/components/FormField.md +11 -4
  16. package/bundle/components/FormGroup.md +3 -2
  17. package/bundle/components/LightBox.md +8 -0
  18. package/bundle/components/ListMaster.md +2 -2
  19. package/bundle/components/ListTableItem.md +6 -6
  20. package/bundle/components/MonthYearField.md +15 -4
  21. package/bundle/components/Slider.md +389 -97
  22. package/bundle/components/SliderRange.md +486 -105
  23. package/bundle/components/StatusTracker.md +386 -94
  24. package/bundle/components/Stepper.md +355 -102
  25. package/bundle/components/StepperExtended.md +633 -0
  26. package/bundle/components/SuggestField.md +1049 -96
  27. package/bundle/components/SwipeableArea.md +7 -7
  28. package/bundle/components/Tabs.md +265 -92
  29. package/bundle/components/TabsExtended.md +18 -18
  30. package/bundle/components/TabsLine.md +301 -127
  31. package/bundle/components/TagColor.md +187 -66
  32. package/bundle/components/TagGroup.md +184 -48
  33. package/bundle/components/TextareaField.md +278 -70
  34. package/bundle/components/TopOverlay.md +17 -1
  35. package/bundle/components/TreeView.md +16 -7
  36. package/bundle/components/TriggerClickOnKeyDownEvent.md +277 -0
  37. package/bundle/components/UploadZone.md +318 -0
  38. package/bundle/guides/codestyle.md +16 -1
  39. package/bundle/guides/commits.md +47 -1
  40. package/bundle/guides/tests.md +23 -2
  41. package/bundle/manifest.json +48 -21
  42. package/bundle/release-notes/1.47.0.mdx +115 -0
  43. package/bundle/release-notes/1.48.0.mdx +68 -0
  44. package/bundle/release-notes/1.49.0.mdx +7 -0
  45. package/bundle/tokens.json +3 -3
  46. package/dist/index.js +220 -6
  47. package/dist/index.js.map +1 -1
  48. package/package.json +1 -1
@@ -1,8 +1,6 @@
1
1
  ---
2
2
  component: SliderRange
3
- category: Form Controls
4
- figma-node: ""
5
- figma-file: ""
3
+ category: Slider
6
4
  related: [Slider, SliderExtended]
7
5
  tokens: []
8
6
  stories: stories/Slider/SliderRange.stories.tsx
@@ -13,148 +11,531 @@ version: "1.0"
13
11
 
14
12
  ## Назначение
15
13
 
16
- Готовый слайдер с **двумя** ползунками и закрашенной полосой между ними.
17
- Фиксированная компоновка: `Rail → Dot₁ → Track → Dot₂ → Marks`.
14
+ Слайдер с двумя ползунками — готовая сборка `SliderExtended` для выбора диапазона: полоса,
15
+ два ползунка, трек между ними и метки уже собраны, потребителю остаётся задать границы, шаг,
16
+ метки и владеть парой значений.
18
17
 
19
- Используй когда: нужно выбрать **диапазон** `[a, b]` из `[min, max]`.
20
- Для одного значения — `Slider`. Для нестандартной компоновки — `SliderExtended`.
18
+ Используй когда: пользователь выбирает диапазон из двух чисел и точность ввода не важна —
19
+ сумма «от и до», срок, процентный коридор.
21
20
 
22
- ---
23
-
24
- ## Файловая структура
21
+ Не используй когда:
25
22
 
26
- ```text
27
- src/components/SliderRange/
28
- ├── SliderRange.tsx # class component, склейка SliderExtended + два Dot + Track
29
- ├── index.ts # Barrel
30
- └── __tests__/
31
- ```
23
+ - Нужен один ползунок — возьми `Slider`.
24
+ - Нужен нестандартный состав частей (без меток, без трека, своё содержимое ползунка) или
25
+ собственная разметка подсказки — собери слайдер из `SliderExtended`.
26
+ - Нужно ввести точные границы — слайдер не заменяет пару `NumberField` / `AmountField`; при
27
+ крупных диапазонах их обычно ставят рядом со слайдером.
28
+ - Ползункам нужны текстовые подписи для скринридера — `SliderRange` их передать не
29
+ позволяет (см. Accessibility), собирай из `SliderExtended`.
32
30
 
33
31
  ---
34
32
 
35
33
  ## Варианты и props
36
34
 
37
- ### `ISliderRangeProps`
35
+ ### Обязательные props
36
+
37
+ | Prop | Тип | Описание |
38
+ |---|---|---|
39
+ | `min` | `number` | Минимальное значение диапазона |
40
+ | `max` | `number` | Максимальное значение диапазона |
41
+ | `size` | `EComponentSize.MD \| EComponentSize.LG` | Размер. `SM` не поддерживается |
42
+ | `values` | `TSliderRangeValues` (`[number, number]`) | Текущая пара значений |
43
+ | `onChange` | `(values: TSliderRangeValues) => void` | Вызывается с отсортированной парой значений |
44
+ | `marks` | `ISliderRangeMark[]` | Метки под полосой: `{ value: number; label: React.ReactNode }` |
38
45
 
39
- Extends `Omit<ISliderExtendedProps, "onChange" | "step">`.
46
+ ### Опциональные props
40
47
 
41
- | Prop | Тип | Default | Описание |
48
+ | Prop | Тип | По умолчанию | Описание |
42
49
  |---|---|---|---|
43
- | `values` | `TSliderRangeValues` (`[number, number]`) | — | Значения обоих ползунков. Оба ∈ `[min, max]`. |
44
- | `onChange` | `(values: [number, number]) => void` | — | Вызывается с **отсортированным** массивом: `[меньший, больший]`. |
45
- | `marks` | `ISliderRangeMark[]` | — | Метки `{value, label}` под rail. Обязателен, может быть `[]`. |
46
- | `step?` | `number \| number[]` | `1` | Шаг или массив возможных значений (первый = `min`, последний = `max`). |
47
- | `draggableTrack?` | `boolean` | `true` | Track можно перетаскивать (двигает оба Dot одновременно). |
48
- | `renderTooltipContent?` | `(value: number) => ReactNode` | — | Если передан — Tooltip рендерится над **каждым** Dot. |
49
- | `size` | `EComponentSize.MD \| LG` | — (из ISliderExtendedProps) | Только MD и LG. |
50
- | `min` / `max` | `number` | — | От SliderExtended. |
51
- | `disabled?` / `reverse?` | `boolean` | — | От SliderExtended. |
52
- | `children` | `never` | — | Запрещено. |
53
-
54
- ### `TSliderRangeValues`
55
-
56
- ```ts
57
- type TSliderRangeValues = [number, number];
58
- ```
50
+ | `step` | `number \| number[]` | `1` | Длина шага либо массив значений шагов. В массиве первое значение равно `min`, последнее — `max` |
51
+ | `draggableTrack` | `boolean` | `true` | Трек между ползунками перетаскивается целиком и сдвигает оба ползунка |
52
+ | `reverse` | `boolean` | `false` | Значения возрастают справа налево |
53
+ | `disabled` | `boolean` | `false` | Слайдер не активен: части не реагируют на курсор и выпадают из табуляции |
54
+ | `renderTooltipContent` | `(value: number) => React.ReactNode` | — | Содержимое подсказки над ползунком; вызывается для каждого ползунка со своим значением. Без этого prop подсказки не рендерятся вовсе |
55
+
56
+ Остальные атрибуты `React.HTMLAttributes<HTMLDivElement>` (включая `className`, `id`,
57
+ `aria-*`, `data-*`) уходят на **корневой `div`** слайдера. `children` запрещён
58
+ (`children?: never`): разметку собирает сам компонент.
59
+
60
+ ### Особенности поведения
61
+
62
+ - **Компонент двигает ползунки сам.** Он рисует их по внутренним значениям и сообщает новую
63
+ пару через `onChange`. `values` задаёт значения при монтировании и перезаписывает их, когда
64
+ потребитель присылает новую пару. Поэтому ползунки сдвинутся, даже если потребитель
65
+ проигнорирует `onChange`, — но тогда показанный диапазон разойдётся с его `values`.
66
+ Обновляй `values` в `onChange`, иначе состояние на экране и в потребителе рассинхронизируются.
67
+ - **Наружу пара всегда уходит отсортированной.** Если ползунки поменялись местами,
68
+ `onChange` получает `[меньшее, большее]`, а внутри компонент помнит, какой ползунок
69
+ какой (см. «Инварианты» про `innerValues`).
70
+ - **Неотсортированные `values` компонент исправляет сам.** Если `values[0] > values[1]`,
71
+ при монтировании и на каждом обновлении вызывается `onChange` с отсортированной парой.
72
+ Потребитель, который проигнорирует этот вызов и продолжит присылать перевёрнутую пару,
73
+ получит его снова на следующем обновлении; потребитель, который в ответ на него запишет
74
+ в state новую перевёрнутую пару, зациклит обновления.
75
+ - **Перемещение трека меняет оба ползунка и вызывает `onChange` дважды подряд** — по
76
+ вызову на ползунок. Первый вызов приходит с промежуточной парой, второй — с итоговой.
77
+ Это же верно для стрелок на сфокусированном треке.
78
+ - **`step` числом строит шаги как `[...range(min, max, step), max]`.** Последний шаг всегда
79
+ равен `max`, даже если он не кратен шагу: при `min: 0, max: 10, step: 3` шаги равны
80
+ `0, 3, 6, 9, 10`.
81
+ - **`step` массивом задаёт произвольную шкалу.** Шаги равны визуально, а не по значению —
82
+ так делается шкала сумм с укрупняющимся шагом (см. story `Amounts`). Массив, собранный
83
+ инлайн в JSX, пересчитывает шаги на каждом рендере потребителя; вынеси его в константу.
84
+ - **Ползунки могут стоять в одной точке**, на один шаг они не «раздвигаются»: минимальной
85
+ ширины диапазона у компонента нет.
86
+ - **Клик по полосе и по метке двигает ближайший ползунок** на ближайший шаг / на значение
87
+ метки.
88
+ - **`marks` не обязаны совпадать с шагами.** Метка со значением вне `[min, max]`
89
+ прижимается к краю полосы; `value` метки используется как React-ключ, поэтому дубли
90
+ значений дадут предупреждение о неуникальных ключах.
91
+ - **Подсказки видны только при `:hover` и `:focus` ползунка** — управляются стилями, не
92
+ props. Постоянно видимой подсказки нет, поэтому текущий диапазон обычно дублируют рядом
93
+ со слайдером (так сделано в большинстве stories).
94
+ - **Ширину и внешние отступы задаёт потребитель.** Своей ширины у слайдера нет; собственные
95
+ вертикальные отступы (под ползунки и под метки) он добавляет сам.
96
+
97
+ ---
59
98
 
60
- Публичный контракт: в `onChange` всегда приходит **отсортированный** массив
61
- `[min, max]`. Внутри (во время drag) значения могут «переставляться» — это
62
- состояние `innerValues` и наружу не видно.
99
+ ## Дизайн-токены
63
100
 
64
- ### `ISliderRangeMark`
101
+ Собственных токенов у компонента нет: своих стилей `SliderRange` не имеет и целиком состоит
102
+ из `SliderExtended`. Внешний вид настраивается токенами группы `Slider` — их список и
103
+ порядок переопределения см. в `SliderExtended-ai.md` → «Дизайн-токены». Значения по
104
+ умолчанию — `src/components/DesignTokens/components/Slider.ts`.
65
105
 
66
- ```ts
67
- { value: number; label: ReactNode }
68
- ```
106
+ ---
107
+
108
+ ## Инварианты
69
109
 
70
- `value` ∈ `[min, max]`.
110
+ - **`SliderRange` — class-компонент, и это часть публичного API.** `ref` на нём даёт
111
+ инстанс класса, а не корневой `div` (в отличие от `Slider`, который обёрнут в
112
+ `React.forwardRef`). Перевод в функциональный компонент или оборачивание в `forwardRef`
113
+ сменит ref-target и тип компонента — breaking change.
114
+ - **Внутреннее состояние `innerValues` хранит значения в порядке ползунков, а не по
115
+ возрастанию.** Так левый ползунок остаётся левым после того, как его перетащили за
116
+ правый: наружу пара уходит отсортированной, а внутри порядок сохраняется. Синхронизация с
117
+ `values` в `componentDidUpdate` учитывает это — когда ползунки перекрещены, новые `values`
118
+ присваиваются в обратном порядке. Замена `innerValues` на отсортированный массив заставит
119
+ ползунки перепрыгивать во время перетаскивания.
120
+ - **`onChange` вызывается внутри updater'а `setState` намеренно.** Перемещение трека меняет
121
+ оба ползунка двумя вызовами подряд в одном батче, и второй обязан видеть значение,
122
+ записанное первым, — иначе трек сдвинет только один ползунок. Побочный эффект: в
123
+ `StrictMode` двойной вызов updater'а приводит к двойному вызову `onChange` с теми же
124
+ значениями.
125
+ - **Пары значений сравниваются по склейке (`values.join("")`).** Из-за этого пары вроде
126
+ `[1, 23]` и `[12, 3]` считаются одинаковыми, и переход между ними не обновит ползунки.
127
+ Поведение историческое; чинить его нужно осознанно и вместе с release notes, потому что
128
+ оно наблюдаемо.
129
+ - **`step` по умолчанию задан через `defaultProps`** (`1`). Значение по умолчанию —
130
+ часть публичного контракта.
131
+ - **`children?: never`** — состав слайдера фиксирован, расширяется он только через
132
+ `SliderExtended`.
133
+ - **`ISliderRangeProps` наследуется от `ISliderExtendedProps`**
134
+ (`Omit<..., "onChange" | "step">`). Переименование props в `SliderExtended` ломает
135
+ публичный API `SliderRange` — менять только вместе.
136
+ - **Порядок частей в разметке (`Rail` → `Dot` → `Track` → `Dot` → `Marks`) и наличие `Rail`
137
+ обязательны.** Позиции клика и перетаскивания считаются от DOM-узла полосы; без `Rail`
138
+ работает только клавиатура. `Tooltip` обязан лежать внутри `Dot`.
139
+ - **`renderTooltipContent` вызывается на каждом рендере** для каждого ползунка — держи её
140
+ дешёвой и без side effects.
141
+ - Реализация остаётся React 17-совместимой (без `useId`, `useSyncExternalStore` и прочих
142
+ React 18-only API): код синхронизируется в `release-0`.
71
143
 
72
144
  ---
73
145
 
74
- ## Ключевые особенности реализации
146
+ ## Accessibility
75
147
 
76
- ### Class component
148
+ - **Каждый ползунок — `role="slider"`** с `aria-valuemin`, `aria-valuenow`, `aria-valuemax`
149
+ из `min` / значения ползунка / `max` (рендерится `SliderExtended.Dot`).
150
+ - **Подписи ползункам `SliderRange` передать нельзя.** Все лишние props уходят на корневой
151
+ `div`, а не на ползунки, поэтому `aria-label` / `aria-labelledby` окажутся на контейнере
152
+ без роли. Если подписи «от» и «до» или `aria-valuetext` (нелинейная шкала, суммы, единицы
153
+ измерения) обязательны — собирай слайдер из `SliderExtended` и передавай атрибуты прямо в
154
+ `SliderExtended.Dot`.
155
+ - **Трек — `role="button"`**, потому что его можно перетаскивать клавиатурой. При
156
+ `draggableTrack={false}` он выпадает из табуляции.
157
+ - **Клавиатура:** `ArrowLeft` / `ArrowDown` — шаг назад, `ArrowRight` / `ArrowUp` — шаг
158
+ вперёд; на треке — шаг обоими ползунками сразу. Обработчики вызывают `preventDefault`,
159
+ чтобы страница не скроллилась. `Home` / `End` / `PageUp` / `PageDown` не поддерживаются.
160
+ - **Порядок табуляции динамический.** Пока слайдер не в фокусе, в табуляцию попадает только
161
+ ползунок с меньшим значением (`tabIndex` 0), второй исключён (`-1`). Как только фокус
162
+ попал внутрь, элементы получают `tabIndex` 1 (левый ползунок), 2 (трек), 3 (правый
163
+ ползунок). Побочный эффект положительных `tabIndex` — эти элементы встают в начало общего
164
+ порядка табуляции страницы, пока слайдер в фокусе.
165
+ - **Метки кликабельны, но не фокусируются** и не читаются как элементы управления: это
166
+ ускоритель для мыши, дублирующий клавиатурное перемещение по шагам.
167
+ - **Подсказки не связаны с ползунками через `aria`** — они показываются по `:hover`/`:focus`
168
+ и для скринридера бесполезны; значения доступны через `aria-valuenow`.
169
+ - **`disabled` только визуальный и мышиный.** Части получают `pointer-events: none` и
170
+ `tabIndex -1`, но нативного `disabled` у `span` нет: программно установленный фокус
171
+ оставляет клавиатурное перемещение работающим — стрелки двигают ползунки и вызывают
172
+ `onChange` так же, как у активного слайдера.
77
173
 
78
- В отличие от `Slider` (FC) и `SliderExtended` (FC), `SliderRange` — **`React.Component`**.
79
- Причина: хранение `innerValues` и логика `validateValues` / `componentDidUpdate`
80
- со сравнением предыдущих props по `values.join("")`.
174
+ ---
81
175
 
82
- ### `innerValues` — внутреннее состояние
176
+ ## Связанные компоненты
83
177
 
84
- - При drag левый Dot может «перепрыгнуть» правый → во внутреннем состоянии
85
- `innerValues[0] > innerValues[1]`, но в `onChange` наружу летит уже отсортированный массив.
86
- - При получении новых `values` извне логика восстанавливает соответствие
87
- (см. `componentDidUpdate`): если Dot'ы «поменялись», innerValues сохраняются
88
- в правильном порядке для их Dot-экземпляров.
178
+ - `SliderExtended` (`src/components/SliderExtended/`) — составной слайдер, из которого
179
+ собран `SliderRange`, и источник его props. Нужен, когда состава `SliderRange` не
180
+ хватает: свой набор частей, своя подсказка, `aria`-атрибуты на ползунках.
181
+ - `Slider` (`src/components/Slider/`) — тот же слайдер на один ползунок: принимает `value`
182
+ числом и не следит за порядком значений.
89
183
 
90
- ### `validateValues`
184
+ ---
91
185
 
92
- Если `values[0] > values[1]` — компонент **автоматически** зовёт `onChange`
93
- с отсортированным массивом. Конструктор и `componentDidUpdate` вызывают
94
- валидацию на каждом rerender.
186
+ ## Stories
95
187
 
96
- ### Жёсткая структура
188
+ Основные истории: `stories/Slider/SliderRange.stories.tsx`
189
+ Файлы примеров: `stories/Slider/examples/SliderRange/`
190
+
191
+ | Story | Example file | Что демонстрирует |
192
+ |---|---|---|
193
+ | `Playground` | `Playground.tsx` | Интерактивный подбор `min`, `max`, `step`, `marks`, `size`, `draggableTrack`, `disabled`, `reverse` |
194
+ | `Default` | `Default.tsx` | Диапазон с метками и шагом по умолчанию |
195
+ | `Sizes` | `Sizes.tsx` | Размеры MD и LG |
196
+ | `WithTooltip` | `WithTooltip.tsx` | Подсказки над обоими ползунками через `renderTooltipContent` |
197
+ | `WithCustomSteps` | `WithCustomSteps.tsx` | Шаги заданы массивом значений |
198
+ | `NonDraggableTrack` | `NonDraggableTrack.tsx` | `draggableTrack={false}` — трек не перетаскивается |
199
+ | `Disabled` | `Disabled.tsx` | Неактивный слайдер |
200
+ | `Reverse` | `Reverse.tsx` | Значения возрастают справа налево |
201
+ | `Amounts` | `Amounts.tsx` | Production-like шкала сумм с неравномерным шагом: слайдер работает с индексами шкалы |
202
+ | `VisualTests` | `VisualTests.tsx` | Размеры, шаги массивом, ползунки в одной точке, реверс и неактивное состояние; `play` ставит фокус на левый ползунок, чтобы в кадр попали обводка фокуса и подсказка |
203
+
204
+ `Playground` исключён из скриншот-тестов. `WithTooltip` исключён, потому что подсказки
205
+ скрыты, пока ползунок не в фокусе (состояние с подсказкой снимает `VisualTests`), а
206
+ `NonDraggableTrack` — потому что `draggableTrack` в статике неотличим от `Default`.
207
+
208
+ ---
209
+
210
+ ## История изменений
211
+
212
+ | Дата | Изменение |
213
+ |---|---|
214
+ | 2026-09-15 | Создан документ. AI-рефакторинг (TRI-79): props задокументированы JSDoc, сортировка и сравнение значений вынесены в чистые функции модуля, синхронизация `values` → `innerValues` упрощена, из разметки убран избыточный проброс `size` (значение приходит тем же spread'ом). Публичный API props, интерфейсов и barrel-экспортов не изменён, компонент остался классом. Добавлены unit-тесты, stories переведены на modern pattern. |
215
+
216
+ ---
217
+
218
+ ## Примеры использования
219
+
220
+ ### Default
221
+
222
+ `stories/Slider/examples/SliderRange/Default.tsx`
97
223
 
98
224
  ```tsx
99
- <SliderExtended step={step} size={size} {...rest}>
100
- <SliderExtended.Rail />
101
- <SliderExtended.Dot key="1" value={innerValues[0]} onChange={...}>
102
- {renderTooltipContent && <SliderExtended.Tooltip value={innerValues[0]}>...</SliderExtended.Tooltip>}
103
- </SliderExtended.Dot>
104
- <SliderExtended.Track draggable={draggableTrack} />
105
- <SliderExtended.Dot key="2" value={innerValues[1]} onChange={...}>
106
- {renderTooltipContent && <SliderExtended.Tooltip value={innerValues[1]}>...</SliderExtended.Tooltip>}
107
- </SliderExtended.Dot>
108
- <SliderExtended.Marks>
109
- {marks.map(m => <SliderExtended.Mark key={m.value} value={m.value}>{m.label}</SliderExtended.Mark>)}
110
- </SliderExtended.Marks>
111
- </SliderExtended>
225
+ import React, { useState } from "react";
226
+ import { EComponentSize, SliderRange, TSliderRangeValues } from "@sberbusiness/triplex-next";
227
+
228
+ const MARKS = [
229
+ { value: 0, label: "0" },
230
+ { value: 30, label: "30" },
231
+ { value: 70, label: "70" },
232
+ { value: 100, label: "100" },
233
+ ];
234
+
235
+ export const Default = () => {
236
+ const [values, setValues] = useState<TSliderRangeValues>([30, 70]);
237
+
238
+ return (
239
+ <div style={{ maxWidth: "750px", padding: "30px" }}>
240
+ <div>values = [{values.join(", ")}]</div>
241
+ <br />
242
+ <SliderRange
243
+ min={0}
244
+ max={100}
245
+ size={EComponentSize.MD}
246
+ marks={MARKS}
247
+ values={values}
248
+ onChange={setValues}
249
+ />
250
+ </div>
251
+ );
252
+ };
112
253
  ```
113
254
 
114
- ---
255
+ ### Sizes
115
256
 
116
- ## Инварианты
257
+ `stories/Slider/examples/SliderRange/Sizes.tsx`
117
258
 
118
- - **Два Dot, жёсткая структура** — менять только через SliderExtended.
119
- - **`onChange` получает отсортированный `[a, b]`** — публичный контракт.
120
- - **`marks: ISliderRangeMark[]`** — обязателен (может быть `[]`).
121
- - **`step` default `1`**.
122
- - **`size: MD | LG`** — `SM` не поддерживается.
123
- - **`children: never`**.
124
- - **`values`: оба ∈ `[min, max]`** — потребитель отвечает.
125
- - **Если `step` — массив**, первое = `min`, последнее = `max`.
126
- - **`SliderRange` — class component** (не FC, не forwardRef). Ref не пробрасывается.
127
- - **`renderTooltipContent`** вызывается ДВАЖДЫ за рендер (по разу на Dot) — функция должна быть чистой.
259
+ ```tsx
260
+ import React, { useState } from "react";
261
+ import { EComponentSize, SliderRange, TSliderRangeValues } from "@sberbusiness/triplex-next";
262
+
263
+ const MARKS = [
264
+ { value: 0, label: "0" },
265
+ { value: 50, label: "50" },
266
+ { value: 100, label: "100" },
267
+ ];
268
+
269
+ interface ISizeItemProps {
270
+ size: EComponentSize.MD | EComponentSize.LG;
271
+ }
272
+
273
+ const SIZES: ISizeItemProps["size"][] = [EComponentSize.MD, EComponentSize.LG];
274
+
275
+ const SizeItem = ({ size }: ISizeItemProps) => {
276
+ const [values, setValues] = useState<TSliderRangeValues>([30, 70]);
277
+
278
+ return (
279
+ <div>
280
+ <div style={{ marginBottom: "8px", fontSize: "16px", fontWeight: "700" }}>{size.toUpperCase()}</div>
281
+ <SliderRange min={0} max={100} size={size} marks={MARKS} values={values} onChange={setValues} />
282
+ </div>
283
+ );
284
+ };
285
+
286
+ export const Sizes = () => (
287
+ <div style={{ maxWidth: "750px", padding: "30px", display: "flex", flexDirection: "column", gap: "24px" }}>
288
+ {SIZES.map((size) => (
289
+ <SizeItem key={size} size={size} />
290
+ ))}
291
+ </div>
292
+ );
293
+ ```
128
294
 
129
- ---
295
+ ### WithTooltip
130
296
 
131
- ## Accessibility
297
+ `stories/Slider/examples/SliderRange/WithTooltip.tsx`
132
298
 
133
- - Наследуется от `SliderExtended.Dot` — оба Dot имеют `role="slider"`,
134
- `aria-valuemin/now/max`.
135
- - `Track` (между Dot'ами) — `role="button"`, `tabIndex={2}` когда слайдер в фокусе:
136
- можно двигать весь диапазон с клавиатуры.
137
- - Keyboard: `ArrowLeft/Right/Up/Down` на focused Dot перемещают его на step;
138
- на focused Track — двигают оба Dot'а.
299
+ ```tsx
300
+ import React, { useState } from "react";
301
+ import { EComponentSize, SliderRange, TSliderRangeValues } from "@sberbusiness/triplex-next";
302
+
303
+ const MARKS = [
304
+ { value: 0, label: "0" },
305
+ { value: 35, label: "35" },
306
+ { value: 66, label: "66" },
307
+ { value: 100, label: "100" },
308
+ ];
309
+
310
+ export const WithTooltip = () => {
311
+ const [values, setValues] = useState<TSliderRangeValues>([35, 66]);
312
+
313
+ return (
314
+ <div style={{ maxWidth: "750px", padding: "30px" }}>
315
+ <div>values = [{values.join(", ")}]</div>
316
+ <br />
317
+ <SliderRange
318
+ min={0}
319
+ max={100}
320
+ size={EComponentSize.MD}
321
+ marks={MARKS}
322
+ values={values}
323
+ onChange={setValues}
324
+ renderTooltipContent={(value) => `${value} %`}
325
+ />
326
+ </div>
327
+ );
328
+ };
329
+ ```
139
330
 
140
- ---
331
+ ### WithCustomSteps
141
332
 
142
- ## Связанные компоненты
333
+ `stories/Slider/examples/SliderRange/WithCustomSteps.tsx`
143
334
 
144
- - `Slider` — один Dot.
145
- - `SliderExtended` — низкоуровневая основа.
335
+ ```tsx
336
+ import React, { useState } from "react";
337
+ import { EComponentSize, SliderRange, TSliderRangeValues } from "@sberbusiness/triplex-next";
338
+
339
+ // Массив шагов вынесен из JSX: инлайновый массив пересчитывает шаги на каждом рендере.
340
+ const STEPS = [0, 25, 50, 75, 100];
341
+
342
+ const MARKS = STEPS.map((step) => ({ value: step, label: String(step) }));
343
+
344
+ export const WithCustomSteps = () => {
345
+ const [values, setValues] = useState<TSliderRangeValues>([25, 75]);
346
+
347
+ return (
348
+ <div style={{ maxWidth: "750px", padding: "30px" }}>
349
+ <div>values = [{values.join(", ")}]</div>
350
+ <br />
351
+ <SliderRange
352
+ min={0}
353
+ max={100}
354
+ step={STEPS}
355
+ size={EComponentSize.MD}
356
+ marks={MARKS}
357
+ values={values}
358
+ onChange={setValues}
359
+ />
360
+ </div>
361
+ );
362
+ };
363
+ ```
146
364
 
147
- ---
365
+ ### NonDraggableTrack
148
366
 
149
- ## Stories
367
+ `stories/Slider/examples/SliderRange/NonDraggableTrack.tsx`
150
368
 
151
- `stories/Slider/SliderRange.stories.tsx`
152
- Примеры: `stories/Slider/examples/SliderRange/`
369
+ ```tsx
370
+ import React, { useState } from "react";
371
+ import { EComponentSize, SliderRange, TSliderRangeValues } from "@sberbusiness/triplex-next";
372
+
373
+ const MARKS = [
374
+ { value: 0, label: "0" },
375
+ { value: 35, label: "35" },
376
+ { value: 66, label: "66" },
377
+ { value: 100, label: "100" },
378
+ ];
379
+
380
+ export const NonDraggableTrack = () => {
381
+ const [values, setValues] = useState<TSliderRangeValues>([35, 66]);
382
+
383
+ return (
384
+ <div style={{ maxWidth: "750px", padding: "30px" }}>
385
+ <div>values = [{values.join(", ")}]</div>
386
+ <br />
387
+ <SliderRange
388
+ min={0}
389
+ max={100}
390
+ size={EComponentSize.MD}
391
+ marks={MARKS}
392
+ values={values}
393
+ onChange={setValues}
394
+ draggableTrack={false}
395
+ />
396
+ </div>
397
+ );
398
+ };
399
+ ```
153
400
 
154
- ---
401
+ ### Disabled
155
402
 
156
- ## История изменений
403
+ `stories/Slider/examples/SliderRange/Disabled.tsx`
404
+
405
+ ```tsx
406
+ import React from "react";
407
+ import { EComponentSize, SliderRange } from "@sberbusiness/triplex-next";
408
+
409
+ const MARKS = [
410
+ { value: 0, label: "0" },
411
+ { value: 35, label: "35" },
412
+ { value: 66, label: "66" },
413
+ { value: 100, label: "100" },
414
+ ];
415
+
416
+ export const Disabled = () => (
417
+ <div style={{ maxWidth: "750px", padding: "30px" }}>
418
+ <SliderRange
419
+ min={0}
420
+ max={100}
421
+ size={EComponentSize.MD}
422
+ marks={MARKS}
423
+ values={[35, 66]}
424
+ onChange={() => {}}
425
+ disabled
426
+ />
427
+ </div>
428
+ );
429
+ ```
430
+
431
+ ### Reverse
432
+
433
+ `stories/Slider/examples/SliderRange/Reverse.tsx`
434
+
435
+ ```tsx
436
+ import React, { useState } from "react";
437
+ import { EComponentSize, SliderRange, TSliderRangeValues } from "@sberbusiness/triplex-next";
438
+
439
+ const MARKS = [
440
+ { value: 0, label: "0" },
441
+ { value: 35, label: "35" },
442
+ { value: 66, label: "66" },
443
+ { value: 100, label: "100" },
444
+ ];
445
+
446
+ export const Reverse = () => {
447
+ const [values, setValues] = useState<TSliderRangeValues>([35, 66]);
448
+
449
+ return (
450
+ <div style={{ maxWidth: "750px", padding: "30px" }}>
451
+ <div>values = [{values.join(", ")}]</div>
452
+ <br />
453
+ <SliderRange
454
+ min={0}
455
+ max={100}
456
+ size={EComponentSize.MD}
457
+ marks={MARKS}
458
+ values={values}
459
+ onChange={setValues}
460
+ reverse
461
+ />
462
+ </div>
463
+ );
464
+ };
465
+ ```
466
+
467
+ ### Amounts
468
+
469
+ `stories/Slider/examples/SliderRange/Amounts.tsx`
470
+
471
+ ```tsx
472
+ import React, { useState } from "react";
473
+ import { EComponentSize, SliderRange, TSliderRangeValues } from "@sberbusiness/triplex-next";
474
+
475
+ /** Возвращает значения от from до to (не включая), с шагом step. */
476
+ const range = (from: number, to: number, step: number): number[] => {
477
+ const values: number[] = [];
478
+
479
+ for (let value = from; value < to; value += step) {
480
+ values.push(value);
481
+ }
482
+
483
+ return values;
484
+ };
485
+
486
+ /**
487
+ * Шкала сумм с неравномерным шагом: чем больше сумма, тем крупнее шаг.
488
+ * Слайдер работает с индексами шкалы, поэтому шаги равны визуально.
489
+ */
490
+ const AMOUNTS = [
491
+ 0,
492
+ 10000,
493
+ ...range(20000, 240000, 20000),
494
+ ...range(240000, 1000000, 40000),
495
+ ...range(1000000, 2150000, 50000),
496
+ ...range(2150000, 2600000, 75000),
497
+ ...range(2600000, 3000000, 100000),
498
+ ...range(3000000, 3800000, 200000),
499
+ ...range(3800000, 9000000, 400000),
500
+ ...range(9000000, 10000000, 500000),
501
+ ...range(10000000, 25000000, 2500000),
502
+ ...range(25000000, 40000000, 5000000),
503
+ ...range(40000000, 90000001, 10000000),
504
+ 100000000,
505
+ ];
506
+
507
+ const amountFormat = new Intl.NumberFormat("ru-RU");
508
+
509
+ const MARKS = [
510
+ { value: 0, label: "0 млн" },
511
+ { value: AMOUNTS.indexOf(1000000), label: "1 млн" },
512
+ { value: AMOUNTS.indexOf(3000000), label: "3 млн" },
513
+ { value: AMOUNTS.length - 1, label: "100 млн" },
514
+ ];
515
+
516
+ export const Amounts = () => {
517
+ const [amountIndexes, setAmountIndexes] = useState<TSliderRangeValues>([
518
+ AMOUNTS.indexOf(1000000),
519
+ AMOUNTS.indexOf(3000000),
520
+ ]);
521
+
522
+ return (
523
+ <div style={{ maxWidth: "750px", padding: "30px" }}>
524
+ <div>
525
+ от {amountFormat.format(AMOUNTS[amountIndexes[0]])} до {amountFormat.format(AMOUNTS[amountIndexes[1]])}
526
+ </div>
527
+ <br />
528
+ <SliderRange
529
+ min={0}
530
+ max={AMOUNTS.length - 1}
531
+ size={EComponentSize.MD}
532
+ marks={MARKS}
533
+ values={amountIndexes}
534
+ onChange={setAmountIndexes}
535
+ renderTooltipContent={(value) => amountFormat.format(AMOUNTS[value])}
536
+ />
537
+ </div>
538
+ );
539
+ };
540
+ ```
157
541
 
158
- | Дата | Изменение |
159
- |---|---|
160
- | 2026-04-18 | Создан документ |