@sberbusiness/triplex-next-mcp-server 1.48.0-beta → 1.50.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 (77) hide show
  1. package/bundle/components/Amount.md +2 -1
  2. package/bundle/components/Badge.md +2 -1
  3. package/bundle/components/BodyPage.md +2 -1
  4. package/bundle/components/Button.md +5 -1
  5. package/bundle/components/ButtonDropdownExtended.md +3 -1
  6. package/bundle/components/ButtonIcon.md +3 -1
  7. package/bundle/components/Calendar.md +3 -1
  8. package/bundle/components/CardAction.md +1 -0
  9. package/bundle/components/CheckboxXGroup.md +2 -1
  10. package/bundle/components/Chip.md +2 -1
  11. package/bundle/components/ChipMonthYearPicker.md +2 -1
  12. package/bundle/components/ChipMultiselect.md +4 -1
  13. package/bundle/components/ChipOptions.md +2 -1
  14. package/bundle/components/ChipSort.md +4 -1
  15. package/bundle/components/CollapsibleTree.md +2 -1
  16. package/bundle/components/ComposedKeyDownListener.md +4 -3
  17. package/bundle/components/DateField.md +7 -1
  18. package/bundle/components/Divider.md +85 -138
  19. package/bundle/components/DocumentNumberEdit.md +173 -214
  20. package/bundle/components/Dropdown.md +11 -1
  21. package/bundle/components/Ellipsis.md +131 -147
  22. package/bundle/components/FooterPage.md +3 -1
  23. package/bundle/components/FormField.md +2 -1
  24. package/bundle/components/Gap.md +1 -1
  25. package/bundle/components/Header.md +1 -0
  26. package/bundle/components/HeaderPage.md +3 -1
  27. package/bundle/components/HelpBox.md +2 -1
  28. package/bundle/components/IconWrapper.md +10 -6
  29. package/bundle/components/ImageGallery.md +2 -1
  30. package/bundle/components/ImageGalleryExtended.md +2 -1
  31. package/bundle/components/IslandAccordion.md +6 -1
  32. package/bundle/components/KeyDownListener.md +3 -2
  33. package/bundle/components/LightBox.md +7 -1
  34. package/bundle/components/List.md +2 -1
  35. package/bundle/components/ListActionItem.md +2 -1
  36. package/bundle/components/ListItem.md +3 -1
  37. package/bundle/components/ListItemControlsButton.md +2 -1
  38. package/bundle/components/ListItemSelectable.md +2 -1
  39. package/bundle/components/ListMasterFooter.md +2 -1
  40. package/bundle/components/ListMasterHeader.md +2 -1
  41. package/bundle/components/ListSortable.md +2 -1
  42. package/bundle/components/ListSortableItem.md +2 -1
  43. package/bundle/components/ListTableItem.md +4 -1
  44. package/bundle/components/LoaderMiddle.md +5 -1
  45. package/bundle/components/LoaderScreen.md +7 -1
  46. package/bundle/components/LoaderSmall.md +10 -4
  47. package/bundle/components/MaskedField.md +3 -1
  48. package/bundle/components/MediaWidth.md +2 -1
  49. package/bundle/components/MobileView.md +2 -1
  50. package/bundle/components/ModalWindow.md +4 -1
  51. package/bundle/components/MonthYearField.md +4 -1
  52. package/bundle/components/Notification.md +1 -0
  53. package/bundle/components/Overlay.md +7 -2
  54. package/bundle/components/Pagination.md +1 -7
  55. package/bundle/components/Radio.md +238 -115
  56. package/bundle/components/RadioXGroup.md +259 -0
  57. package/bundle/components/RadioYGroup.md +266 -0
  58. package/bundle/components/SegmentedControl.md +505 -158
  59. package/bundle/components/SelectExtendedField.md +2 -1
  60. package/bundle/components/SelectField.md +4 -1
  61. package/bundle/components/Skeleton.md +140 -69
  62. package/bundle/components/Spoiler.md +245 -63
  63. package/bundle/components/Step.md +257 -68
  64. package/bundle/components/SuggestField.md +30 -18
  65. package/bundle/components/Tabs.md +2 -1
  66. package/bundle/components/TabsLine.md +2 -1
  67. package/bundle/components/TextField.md +8 -4
  68. package/bundle/components/TextareaField.md +2 -1
  69. package/bundle/components/ThemeProvider.md +2 -1
  70. package/bundle/components/Tooltip.md +6 -3
  71. package/bundle/guides/context.md +4 -1
  72. package/bundle/manifest.json +32 -10
  73. package/bundle/release-notes/1.49.0.mdx +78 -0
  74. package/bundle/release-notes/1.50.0.mdx +35 -0
  75. package/bundle/release-notes/1.51.0.mdx +7 -0
  76. package/bundle/tokens.json +161 -11
  77. package/package.json +1 -1
@@ -1,10 +1,58 @@
1
1
  ---
2
2
  component: SegmentedControl
3
- category: Form Controls
4
- figma-node: ""
5
- figma-file: ""
6
- related: [Tabs, Radio, Checkbox, ButtonBase, IconWrapper]
7
- tokens: []
3
+ category: SegmentedControl
4
+ related: [Tabs, TabsLine, IconWrapper]
5
+ tokens:
6
+ - SegmentedControl.General_1_Background
7
+ - SegmentedControl.General_2_Background
8
+ - SegmentedControl.Secondary_1_Background
9
+ - SegmentedControl.Secondary_2_Background
10
+ - SegmentedControlSegment.BorderColor_Default
11
+ - SegmentedControlSegment.BorderColor_Focus
12
+ - SegmentedControlSegment.General_1_Background_Default
13
+ - SegmentedControlSegment.General_1_Background_Disabled
14
+ - SegmentedControlSegment.General_1_Background_Hover
15
+ - SegmentedControlSegment.General_1_Background_Selected_Default
16
+ - SegmentedControlSegment.General_1_Background_Selected_Disabled
17
+ - SegmentedControlSegment.General_1_Background_Selected_Hover
18
+ - SegmentedControlSegment.General_1_Color_Default
19
+ - SegmentedControlSegment.General_1_Color_Disabled
20
+ - SegmentedControlSegment.General_1_Color_Hover
21
+ - SegmentedControlSegment.General_1_Color_Selected_Default
22
+ - SegmentedControlSegment.General_1_Color_Selected_Disabled
23
+ - SegmentedControlSegment.General_1_Color_Selected_Hover
24
+ - SegmentedControlSegment.General_2_Background_Default
25
+ - SegmentedControlSegment.General_2_Background_Disabled
26
+ - SegmentedControlSegment.General_2_Background_Hover
27
+ - SegmentedControlSegment.General_2_Background_Selected_Default
28
+ - SegmentedControlSegment.General_2_Background_Selected_Disabled
29
+ - SegmentedControlSegment.General_2_Background_Selected_Hover
30
+ - SegmentedControlSegment.General_2_Color_Default
31
+ - SegmentedControlSegment.General_2_Color_Disabled
32
+ - SegmentedControlSegment.General_2_Color_Hover
33
+ - SegmentedControlSegment.General_2_Color_Selected_Default
34
+ - SegmentedControlSegment.General_2_Color_Selected_Disabled
35
+ - SegmentedControlSegment.General_2_Color_Selected_Hover
36
+ - SegmentedControlSegment.Secondary_1_Background_Default
37
+ - SegmentedControlSegment.Secondary_1_Background_Disabled
38
+ - SegmentedControlSegment.Secondary_1_Background_Hover
39
+ - SegmentedControlSegment.Secondary_1_Background_Selected_Default
40
+ - SegmentedControlSegment.Secondary_1_Background_Selected_Disabled
41
+ - SegmentedControlSegment.Secondary_1_Color_Default
42
+ - SegmentedControlSegment.Secondary_1_Color_Disabled
43
+ - SegmentedControlSegment.Secondary_1_Color_Hover
44
+ - SegmentedControlSegment.Secondary_1_Color_Selected_Default
45
+ - SegmentedControlSegment.Secondary_1_Color_Selected_Disabled
46
+ - SegmentedControlSegment.Secondary_2_Background_Default
47
+ - SegmentedControlSegment.Secondary_2_Background_Disabled
48
+ - SegmentedControlSegment.Secondary_2_Background_Hover
49
+ - SegmentedControlSegment.Secondary_2_Background_Selected_Default
50
+ - SegmentedControlSegment.Secondary_2_Background_Selected_Disabled
51
+ - SegmentedControlSegment.Secondary_2_Color_Default
52
+ - SegmentedControlSegment.Secondary_2_Color_Disabled
53
+ - SegmentedControlSegment.Secondary_2_Color_Hover
54
+ - SegmentedControlSegment.Secondary_2_Color_Selected_Default
55
+ - SegmentedControlSegment.Secondary_2_Color_Selected_Disabled
8
56
  stories: stories/SegmentedControl/SegmentedControl.stories.tsx
9
57
  version: "1.0"
10
58
  ---
@@ -13,223 +61,522 @@ version: "1.0"
13
61
 
14
62
  ## Назначение
15
63
 
16
- Группа «сегментов»-кнопок: выбор **одного** (`SINGLE`) или **нескольких**
17
- (`MULTIPLE`) вариантов из небольшого набора. Каждый сегмент —
18
- `<button aria-pressed>`. Похоже на iOS UISegmentedControl. Управление
19
- выбором — через `value` + `onSelect` снаружи; внутреннее состояние нет.
64
+ Ряд сегментов-кнопок в общей подложке, из которых пользователь выбирает один (`type: SINGLE`)
65
+ или несколько (`type: MULTIPLE`) вариантов. Компонент полностью управляемый: выбранное значение
66
+ приходит в `value`, запрос на смену — через `onSelect`. Состав сегментов задаёт потребитель
67
+ через `children` и составной субкомпонент `SegmentedControl.Segment`; сегменты делят ширину
68
+ контрола поровну (`flex: 1 1 0`).
20
69
 
21
- Используй когда: 2–6 коротких опций (фильтры, переключатели view).
22
- Для много опций / длинных лейблов — `Tabs` или `SelectField`. Для
23
- визуально не-кнопочного выбора — `Radio` / `Checkbox` группы.
70
+ Используй когда: вариантов немного (2–5), все они должны быть видны одновременно, и выбор
71
+ переключает режим отображения или фильтр — «день / неделя / месяц», «список / плитка».
24
72
 
25
- ---
73
+ Не используй когда:
26
74
 
27
- ## Файловая структура
28
-
29
- ```text
30
- src/components/SegmentedControl/
31
- ├── SegmentedControl.tsx # forwardRef + Object.assign({Segment})
32
- ├── SegmentedControlSegment.tsx # FC; ButtonBase + IconWrapper(active=selected)
33
- ├── SegmentedControlContext.ts # { type, value, disabled, onSegmentSelect }
34
- ├── enums.ts # ESegmentedControlType / Theme / Size
35
- ├── types.ts # discriminated union по type
36
- ├── styles/SegmentedControl.module.less + SegmentedControlSegment.module.less
37
- ├── index.ts # Barrel
38
- └── __tests__/
39
- ```
75
+ - Выбор переключает **страницы или панели контента** — нужна семантика табов
76
+ (`role="tablist"`, связка `aria-controls`, навигация стрелками). Возьми `Tabs`
77
+ или `TabsLine`: `SegmentedControl` этой семантики не даёт (см. Accessibility).
78
+ - Вариантов много или они не влезают в строку — сегменты не переносятся и не скроллятся,
79
+ а сжимаются, обрезая подписи по `text-overflow: ellipsis`.
80
+ - Нужен выбор из списка внутри формы с лейблом и статусом — возьми `SelectField`
81
+ или `RadioYGroup`.
82
+ - Нужны сбрасываемые фильтры с крестиком и счётчиком — это `Chip` / `ChipGroup`.
40
83
 
41
84
  ---
42
85
 
43
- ## Compound-структура
86
+ ## Варианты и props
44
87
 
45
- ```tsx
46
- <SegmentedControl
47
- type={ESegmentedControlType.SINGLE}
48
- theme={ESegmentedControlTheme.GENERAL_1}
49
- size={ESegmentedControlSize.MD}
50
- value={selected} // string в SINGLE; string[] в MULTIPLE
51
- onSelect={setSelected}
52
- >
53
- <SegmentedControl.Segment value="day">День</SegmentedControl.Segment>
54
- <SegmentedControl.Segment value="week">Неделя</SegmentedControl.Segment>
55
- <SegmentedControl.Segment value="month">Месяц</SegmentedControl.Segment>
56
- </SegmentedControl>
57
- ```
88
+ `TSegmentedControlProps` — discriminated union по `type`: `ISegmentedControlSingleProps`
89
+ (`type: SINGLE`) и `ISegmentedControlMultipleProps` (`type: MULTIPLE`). Обе ветки расширяют
90
+ `ISegmentedControlCommonProps`, а он — `Omit<React.HTMLAttributes<HTMLDivElement>, "onSelect">`:
91
+ неизвестные компоненту атрибуты уходят на корневой `<div>` через `...rest`.
58
92
 
59
- ---
93
+ ### Обязательные props
60
94
 
61
- ## Варианты и props
62
-
63
- ### SegmentedControl — `TSegmentedControlProps = ISegmentedControlSingleProps | ISegmentedControlMultipleProps`
95
+ | Prop | Тип | Описание |
96
+ |---|---|---|
97
+ | `type` | `ESegmentedControlType` | Дискриминант союза. `SINGLE` — выбран ровно один сегмент, `MULTIPLE` — любое подмножество. Меняет типы `value` и `onSelect`, а не внешний вид |
98
+ | `value` | `string` при `SINGLE`, `string[]` при `MULTIPLE` | Выбранное значение. Сегмент считает себя выбранным, сравнивая свой `value` с этим (`===` при SINGLE, `includes` при MULTIPLE). Собственного состояния у компонента нет |
99
+ | `onSelect` | `(value: string) => void` при `SINGLE`, `(value: string[]) => void` при `MULTIPLE` | Запрос на смену значения. При `SINGLE` получает `value` нажатого сегмента, при `MULTIPLE` — **новый массив** выбранных значений (компонент сам добавляет или убирает значение) |
100
+ | `theme` | `ESegmentedControlTheme` | Визуальный стиль: пара цветов подложки и сегментов. Выбирается по фону страницы, см. ниже |
101
+ | `size` | `ESegmentedControlSize` | Размер: высота сегмента, размер шрифта, радиусы и паддинги |
64
102
 
65
- #### Общее (`ISegmentedControlCommonProps extends Omit<HTMLAttributes<HTMLDivElement>, "onSelect">`)
103
+ ### Опциональные props
66
104
 
67
- | Prop | Тип | Default | Описание |
105
+ | Prop | Тип | По умолчанию | Описание |
68
106
  |---|---|---|---|
69
- | `theme` | `ESegmentedControlTheme` | — (обязателен) | `GENERAL_1` / `GENERAL_2` / `SECONDARY_1` / `SECONDARY_2`. |
70
- | `size` | `ESegmentedControlSize` | — (обязателен) | `SM` / `MD` / `LG`. **Собственный enum**, не `EComponentSize`. |
71
- | `disabled?` | `boolean` | `false` | Дизейблит **все сегменты** (через context). |
72
- | `...HTMLAttributes` | — | — | На корневой `<div>`. |
107
+ | `disabled` | `boolean` | `false` | Выключает **все** сегменты сразу: значение уходит в контекст, и каждый сегмент ставит `disabled` на свой `<button>`. Выбранный сегмент остаётся визуально выбранным |
108
+ | `children` | `React.ReactNode` | — | Сегменты. Ожидается `SegmentedControl.Segment`; компонент не проверяет и не фильтрует состав `children` |
109
+
110
+ ### Props сегмента (`SegmentedControl.Segment`)
73
111
 
74
- #### SINGLE
112
+ `ISegmentedControlSegmentProps extends IButtonBaseProps` (то есть
113
+ `React.ButtonHTMLAttributes<HTMLButtonElement>`), поэтому сегмент принимает любые атрибуты
114
+ кнопки. Собственный prop один:
75
115
 
76
116
  | Prop | Тип | Описание |
77
117
  |---|---|---|
78
- | `type` | `ESegmentedControlType.SINGLE` | Discriminator. |
79
- | `value` | `string` | Текущий выбранный. |
80
- | `onSelect` | `(value: string) => void` | Передаётся **новый** value. |
118
+ | `value` | `string` | Значение сегмента. Сравнивается со `value` контрола, чтобы определить выбранное состояние, и уходит в `onSelect` при нажатии |
81
119
 
82
- #### MULTIPLE
120
+ Поведение остальных атрибутов кнопки:
83
121
 
84
- | Prop | Тип | Описание |
85
- |---|---|---|
86
- | `type` | `ESegmentedControlType.MULTIPLE` | Discriminator. |
87
- | `value` | `string[]` | Массив выбранных. |
88
- | `onSelect` | `(value: string[]) => void` | Передаётся **полный новый массив** (не дельта). |
122
+ - `disabled` — выключает **один** сегмент. Складывается с `disabled` контрола по `||`:
123
+ выключить сегмент можно, включить обратно вопреки контролу — нельзя.
124
+ - `onClick` — вызывается **после** запроса на смену значения, с исходным событием. Не
125
+ отменяет выбор: `preventDefault()` на выбор не влияет.
126
+ - `title` — если не передан, а `children` сегмента является строкой, эта строка
127
+ подставляется в `title` автоматически (подсказка для обрезанной подписи). Пустая строка
128
+ в `title` считается непереданной. Для нестрокового `children` (иконка) `title` не
129
+ выставляется — передавай его или `aria-label` сам.
130
+ - `className` — мерджится через `clsx` с собственными классами, не затирая их.
89
131
 
90
- ### SegmentedControl.Segment — `ISegmentedControlSegmentProps extends IButtonBaseProps`
132
+ ### Выбор темы
91
133
 
92
- | Prop | Тип | Описание |
134
+ | Тема | Подложка | Когда |
93
135
  |---|---|---|
94
- | `value` | `string` | Идентификатор сегмента (соответствует `value` SegmentedControl'а). |
95
- | `disabled?` | от ButtonBase | Локальный disabled (мержится с групповым). |
96
- | `title?` | `string` | HTML-атрибут tooltip; если пусто и `children` — string, берётся children. |
97
- | `onClick?` | от ButtonBase | Дополнительный handler (вызывается **после** `onSegmentSelect`). |
98
- | `children` | `ReactNode` | Контент кнопки. |
99
- | `...IButtonBaseProps` | — | — |
100
-
101
- ### Context — `ISegmentedControlContextType`
136
+ | `GENERAL_1` | серая (`ColorNeutral.90`) | базовый вариант на белой странице |
137
+ | `GENERAL_2` | белая (`ColorNeutral.100`) | на затенённой странице или внутри серого блока |
138
+ | `SECONDARY_1` | серая | вторичный, менее акцентный выбранный сегмент |
139
+ | `SECONDARY_2` | белая | вторичный вариант на затенённой странице |
102
140
 
103
- ```ts
104
- { type, value, disabled, onSegmentSelect: ({ selected, value }) => void }
105
- ```
141
+ Пара `GENERAL` / `SECONDARY` отличается только цветами выбранного сегмента, геометрия
142
+ одинаковая.
106
143
 
107
- ### Enums
144
+ ### Размер и тема независимы
108
145
 
109
- - `ESegmentedControlType.SINGLE | MULTIPLE`
110
- - `ESegmentedControlTheme.GENERAL_1 | GENERAL_2 | SECONDARY_1 | SECONDARY_2`
111
- - `ESegmentedControlSize.SM | MD | LG`
146
+ Оси не пересекаются: в `SegmentedControlSegment.module.less` классы размера (`.sm`, `.md`,
147
+ `.lg`) задают только геометрию (высота 20 / 32 / 40px, размер шрифта, радиус), классы темы —
148
+ только цвета. Любая комбинация допустима, поэтому stories показывают оси по отдельности,
149
+ а матрицу целиком не снимают.
112
150
 
113
151
  ---
114
152
 
115
- ## Ключевые особенности реализации
153
+ ## Дизайн-токены
154
+
155
+ Переопределяются через `ThemeProvider` (prop `tokens`) — см. `ThemeProvider-ai.md` →
156
+ «Как переопределять токены». Значения по умолчанию —
157
+ `src/components/DesignTokens/components/SegmentedControl.ts` и
158
+ `src/components/DesignTokens/components/SegmentedControlSegment.ts`.
116
159
 
117
- ### Discriminated union по `type`
160
+ Две группы: `SegmentedControl` — только фон подложки (по одному токену на тему),
161
+ `SegmentedControlSegment` — цвет и фон сегмента в разрезе «тема × выбранность × состояние»
162
+ плюс два токена рамки:
118
163
 
119
- ```ts
120
- type TSegmentedControlProps = ISegmentedControlSingleProps | ISegmentedControlMultipleProps;
164
+ ```text
165
+ SegmentedControl.{Theme}_Background
166
+ SegmentedControlSegment.{Theme}_Color_[Selected_]{Default|Hover|Disabled}
167
+ SegmentedControlSegment.{Theme}_Background_[Selected_]{Default|Hover|Disabled}
168
+ SegmentedControlSegment.BorderColor_Default
169
+ SegmentedControlSegment.BorderColor_Focus
121
170
  ```
122
171
 
123
- - TS гарантирует: `value: string` ↔ `type: SINGLE`; `value: string[]` ↔ `type: MULTIPLE`. `onSelect` сигнатура соответственно.
124
- - Discriminator обязателен на потребителе.
172
+ Рамка у сегмента есть всегда (`1px solid BorderColor_Default`) — она резервирует место,
173
+ чтобы фокусная рамка не сдвигала вёрстку. При `:focus-visible` меняется только её цвет
174
+ на `BorderColor_Focus`.
125
175
 
126
- ### `handleSegmentSelect` — превращение `(value, selected)` в новый список
176
+ **Известное расхождение (не исправлено осознанно).** В группе `SegmentedControlSegment`
177
+ объявлены, но не используются в стилях четыре токена: `Secondary_1_Color_Selected_Hover`,
178
+ `Secondary_1_Background_Selected_Hover`, `Secondary_2_Color_Selected_Hover`,
179
+ `Secondary_2_Background_Selected_Hover`. У тем `SECONDARY_*` в LESS нет правила
180
+ `&.selected:hover`, поэтому наведение на выбранный сегмент этих тем ничего не меняет,
181
+ хотя значения токенов отличаются от `Selected_Default`. Это pre-existing поведение;
182
+ добавление правила — визуальное изменение, его нужно согласовывать с дизайном и
183
+ перегенерировать baseline-скриншоты.
127
184
 
128
- ```ts
129
- case SINGLE: if (selected) onSelect(newValue); // selected всегда true в Segment.handleClick для SINGLE
130
- case MULTIPLE:
131
- if (selected) onSelect([...value, newValue])
132
- else onSelect(value.filter(v => v !== newValue));
133
- ```
185
+ ---
134
186
 
135
- - В SINGLE: повторный клик **не** снимает выбор (`selected: true` всегда отправляется).
136
- - В MULTIPLE: клик инвертирует — добавление или удаление из массива.
187
+ ## Инварианты
137
188
 
138
- ### `Segment.handleClick`
189
+ - `forwardRef<HTMLDivElement>` на `SegmentedControl` — не убирать. Ref указывает на корневую
190
+ подложку (`<div class="segmentedControl">`).
191
+ - `forwardRef<HTMLButtonElement>` на `SegmentedControlSegment` — не убирать. Ref указывает на
192
+ `<button>`, который рендерит `ButtonBase`, а не на обёртку `IconWrapper`.
193
+ - Составной API `SegmentedControl.Segment` собирается через `Object.assign(forwardRef(...), { Segment })`.
194
+ `SegmentedControlSegment` при этом остаётся и самостоятельным barrel-экспортом — оба пути
195
+ импорта публичные, сохранять нужно оба.
196
+ - Барел `src/components/SegmentedControl/index.ts` экспортирует `enums`, `types`,
197
+ `SegmentedControlSegment` и `SegmentedControl`. `SegmentedControlContext`,
198
+ `ISegmentedControlContextType` и `utils.ts` из барела **не** экспортируются — это
199
+ внутренний слой, менять его можно свободно.
200
+ - `displayName` — `"SegmentedControl"` и `"SegmentedControlSegment"`. Не менять.
201
+ - Значения `ESegmentedControlTheme` (`general_1` / `general_2` / `secondary_1` / `secondary_2`)
202
+ и `ESegmentedControlType` (`single` / `multiple`) — часть публичного API. Значения темы
203
+ дополнительно связаны с именами CSS-классов (`.general1` и т.д. — через
204
+ `themeToClassNameMap`) и с именами токенов `{Theme}_*`.
205
+ - **Значения `ESegmentedControlSize` обязаны совпадать с `EComponentSize`** (`sm` / `md` / `lg`).
206
+ Классы размера берутся из `createSizeToClassNameMap(styles)`, а он типизирован как
207
+ `Record<EComponentSize, string>` — индексация `sizeToClassNameMap[size]` работает только
208
+ потому, что строковые значения обоих enum совпадают. Добавление размера в
209
+ `ESegmentedControlSize`, которого нет в `EComponentSize`, даст `undefined` вместо класса
210
+ без ошибки типов.
211
+ - Оба файла стилей лежат в `src/components/SegmentedControl/styles/`, и это важно:
212
+ `generateScopedName` (`scripts/generate-scoped-name.ts`) хеширует класс по **имени папки
213
+ компонента** + имени класса + версии. Поэтому `.general1` и `.sm` из
214
+ `SegmentedControl.module.less` и селекторы `.general1 &` / `.sm &` из
215
+ `SegmentedControlSegment.module.less` получают одинаковый хеш и сцепляются. Перенос
216
+ одного из файлов в другую папку компонента молча сломает стилизацию сегментов.
217
+ - При `type: SINGLE` нажатие на уже выбранный сегмент **всё равно вызывает** `onSelect` с его
218
+ значением (сегмент сообщает `selected: true`). Это наблюдаемое поведение, зафиксировано
219
+ тестом; отмена выбора при SINGLE не поддерживается.
220
+ - При `type: MULTIPLE` компонент не хранит порядок выбора: новое значение добавляется в
221
+ конец массива (`[...value, newValue]`), удаление сохраняет исходный порядок остальных.
222
+ - `[...value]` в обработчике MULTIPLE — не избыточный спред, а сужение для TS: `value`
223
+ внутри деструктурированного союза имеет тип `string | string[]`, и `.filter` у строки нет.
224
+ - Класс `content` на внутреннем `<span>` сегмента отвечает за обрезку подписи
225
+ (`overflow: hidden`, `text-overflow: ellipsis`) — не удалять и не переименовывать.
139
226
 
140
- ```ts
141
- case SINGLE: onSegmentSelect({ value, selected: true });
142
- case MULTIPLE: onSegmentSelect({ value, selected: !selected });
143
- ```
227
+ ---
144
228
 
145
- ### `IconWrapper displayContents disabled active={selected}`
229
+ ## Accessibility
146
230
 
147
- ```tsx
148
- <IconWrapper displayContents disabled={...} active={selected}>
149
- <ButtonBase aria-pressed={selected} ...>
150
- <span className={styles.content}>{children}</span>
151
- </ButtonBase>
152
- </IconWrapper>
153
- ```
231
+ - Каждый сегмент — нативная `<button type="button">` (через `ButtonBase`), поэтому фокус,
232
+ `Enter` / `Space` и `disabled` работают нативно. Собственных обработчиков клавиатуры
233
+ компонент не добавляет.
234
+ - Выбранность кодируется `aria-pressed` на каждой кнопке: `true` у выбранных, `false`
235
+ у остальных. Это модель «группа кнопок-переключателей», а не табов и не радиогруппы.
236
+ - **Корневой `<div>` роли не имеет.** Группа для скринридера не объявлена: ни
237
+ `role="group"`, ни `role="tablist"`, ни `aria-label` компонент не выставляет. Если группу
238
+ нужно озвучить, передай атрибуты сам — они уйдут на корневой `<div>` через `...rest`:
239
+ `<SegmentedControl role="group" aria-label="Период" ...>`. Роль обязательна вместе
240
+ с именем: `<div>` без роли получает роль `generic`, которой
241
+ [HTML-AAM](https://www.w3.org/TR/html-aria/) запрещает доступное имя.
242
+ - Навигации стрелками между сегментами нет — обход идёт `Tab`'ом, каждый сегмент
243
+ отдельная остановка. Если нужна семантика табов с одной остановкой на группу,
244
+ это `Tabs` / `TabsLine`, а не `SegmentedControl`.
245
+ - Фокус виден только с клавиатуры: цвет рамки меняется по `:focus-visible`.
246
+ - Сегмент с одной иконкой доступного имени не получает: `title` подставляется из `children`
247
+ только для строкового содержимого. Для иконки передавай `aria-label` (или `title`) сам —
248
+ компонент не хардкодит текст, библиотека мультиязычная.
249
+ - `disabled` (и на контроле, и на сегменте) ставится нативным атрибутом, поэтому выключенные
250
+ сегменты выпадают из порядка фокуса и не озвучиваются как активируемые.
154
251
 
155
- - **`displayContents`** — IconWrapper не вводит свой DOM-узел.
156
- - **`active`/`disabled`** — публичные классы из `@sberbusiness/icons-next`, влияющие на цвет иконок внутри `children`.
157
- - `aria-pressed` — корректная семантика toggle-button (toggle для MULTIPLE; для SINGLE — тоже технически верно).
252
+ ---
158
253
 
159
- ### Default-значения context
254
+ ## Связанные компоненты
160
255
 
161
- ```ts
162
- { type: SINGLE, value: "", disabled: false, onSegmentSelect: noop }
163
- ```
256
+ - `Tabs` — альтернатива, когда выбор переключает панели контента: даёт `role="tablist"`,
257
+ навигацию стрелками и работу с непоместившимися табами. Их легко спутать, потому что
258
+ `SegmentedControl` часто используют как визуальный переключатель вида.
259
+ - `TabsLine` — альтернатива для табов-подчёркиваний в шапке раздела. Тот же критерий
260
+ выбора, что и у `Tabs`: нужна ли семантика табов.
261
+ - `IconWrapper` — рендерится внутри каждого сегмента (`displayContents`) и по классам
262
+ `active` / `disabled` управляет цветом иконки из `@sberbusiness/icons-next`. Поэтому
263
+ сегмент с иконкой перекрашивается вместе с выбранным состоянием; `paletteIndex` в примерах
264
+ задаёт базовый цвет.
265
+ - `ButtonBase` — внутренняя кнопка, на которой построен сегмент (в barrel библиотеки не
266
+ экспортируется). Даёт `type="button"`, `data-tx` и проброс ref.
164
267
 
165
- - При использовании `Segment` вне `SegmentedControl` — `value === ""`, ничего не выбрано, клик noop. Безопасно.
268
+ ---
166
269
 
167
- ### `getTitle`
270
+ ## Stories
168
271
 
169
- - Если `title` передан — использует его.
170
- - Иначе — если `children: string` — берёт children (для tooltip'а на overflow-text).
171
- - Иначе — undefined.
272
+ Основные истории: `stories/SegmentedControl/SegmentedControl.stories.tsx`
273
+ Файлы примеров: `stories/SegmentedControl/examples/`
172
274
 
173
- ### Темы — 4 варианта
275
+ | Story | Example file | Что демонстрирует |
276
+ |---|---|---|
277
+ | `Playground` | `Playground.tsx` | Интерактивный контроль `type`, `theme`, `size`, `disabled` |
278
+ | `Default` | `Default.tsx` | Минимальный контрол: `SINGLE`, `GENERAL_1`, `LG`, пять сегментов |
279
+ | `Types` | `Types.tsx` | `SINGLE` и `MULTIPLE` рядом; у MULTIPLE выбраны два сегмента |
280
+ | `Themes` | `Themes.tsx` | Все четыре значения `ESegmentedControlTheme` |
281
+ | `Sizes` | `Sizes.tsx` | Размеры SM / MD / LG |
282
+ | `Disabled` | `Disabled.tsx` | `disabled` на всём контроле, выбранный сегмент сохраняет вид |
283
+ | `Example` | `Example.tsx` | Production-like: три сегмента только с иконками в контейнере 144px |
284
+ | `VisualTests` | `VisualTests.tsx` | Краевые состояния для скриншотов: `:focus-visible` (открывается `play`), выключенные отдельные сегменты (включая выбранный), обрезка длинной подписи, сегменты-иконки, MULTIPLE без выбора и с полным выбором |
285
+
286
+ Из скриншот-тестов исключены (`testRunner: { skip: true }`) `Playground`, `Default` и
287
+ `Example`: `Default` визуально дублирует блок SINGLE в `Types`, а сегменты только с иконкой
288
+ покрыты отдельным блоком в `VisualTests` (там они сняты в теме `SECONDARY_1`, тогда как
289
+ `Example` использует `GENERAL_1`).
290
+
291
+ Story ID участвуют в именах baseline-скриншотов (`segmentedcontrol--themes--xs.png` и т.п.),
292
+ поэтому переименование story требует перегенерации baseline и удаления осиротевших файлов.
174
293
 
175
- - `GENERAL_1`, `GENERAL_2`, `SECONDARY_1`, `SECONDARY_2`. Проверять в LESS — отличия в фоне/border/цвете.
294
+ ---
176
295
 
177
- ### Sizes — собственный enum
296
+ ## История изменений
178
297
 
179
- - `ESegmentedControlSize.SM/MD/LG` — **не** `EComponentSize`. Расхождение с типичной практикой (исторически).
298
+ | Дата | Изменение |
299
+ |---|---|
300
+ | 2026-09-30 | Создан документ. AI-рефакторинг: `SegmentedControlSegment` переведён с `React.FC` на `forwardRef<HTMLButtonElement>`, чистые хелперы вынесены в `utils.ts` (`isSegmentSelected`, `getSegmentTitle`), JSDoc на props и полях контекста, внутренние импорты приведены к относительным. Добавлены unit-тесты (`__tests__/SegmentedControlSegment.test.tsx`, `__tests__/utils.test.tsx`, классы тем и размеров в `__tests__/SegmentedControl.test.tsx`). Stories перенесены в `stories/SegmentedControl/` по modern pattern, добавлена story `VisualTests`. |
180
301
 
181
302
  ---
182
303
 
183
- ## Инварианты
304
+ ## Примеры использования
184
305
 
185
- - **`type` обязателен**, discriminator union'а. Меняет тип `value`/`onSelect`.
186
- - **SINGLE: повторный клик не снимает выбор** (нет «снять всё»). Для optional-single — потребитель сам реализует.
187
- - **MULTIPLE: `onSelect` получает полный массив**, не дельту.
188
- - **`theme` и `size` обязательны** (нет default'ов).
189
- - **`size` — собственный enum** `ESegmentedControlSize`, не `EComponentSize`.
190
- - **`disabled` через context** — групповой; локальный `Segment.disabled` мержится.
191
- - **`aria-pressed={selected}`** — публичный a11y-контракт.
192
- - **`SegmentedControlContext` default — `{ SINGLE, "", false, noop }`** — Segment безопасен вне группы.
193
- - **ForwardRef** на корне SegmentedControl, **`FC`** на Segment.
194
- - **`IconWrapper displayContents`** — обязателен для не-DOM обёртки.
195
- - **`Segment.onClick` вызывается после `onSegmentSelect`** — порядок может быть важен.
306
+ ### Default
196
307
 
197
- ---
308
+ `stories/SegmentedControl/examples/Default.tsx`
198
309
 
199
- ## Accessibility
310
+ ```tsx
311
+ import React, { useState } from "react";
312
+ import {
313
+ SegmentedControl,
314
+ ESegmentedControlSize,
315
+ ESegmentedControlTheme,
316
+ ESegmentedControlType,
317
+ } from "@sberbusiness/triplex-next";
318
+
319
+ export const Default = () => {
320
+ const [value, setValue] = useState("segment_3");
321
+
322
+ return (
323
+ <SegmentedControl
324
+ type={ESegmentedControlType.SINGLE}
325
+ theme={ESegmentedControlTheme.GENERAL_1}
326
+ size={ESegmentedControlSize.LG}
327
+ value={value}
328
+ onSelect={setValue}
329
+ >
330
+ <SegmentedControl.Segment value="segment_1">Segment</SegmentedControl.Segment>
331
+ <SegmentedControl.Segment value="segment_2">Segment</SegmentedControl.Segment>
332
+ <SegmentedControl.Segment value="segment_3">Segment</SegmentedControl.Segment>
333
+ <SegmentedControl.Segment value="segment_4">Segment</SegmentedControl.Segment>
334
+ <SegmentedControl.Segment value="segment_5">Segment</SegmentedControl.Segment>
335
+ </SegmentedControl>
336
+ );
337
+ };
338
+ ```
200
339
 
201
- - `<button aria-pressed>` — корректная семантика toggle (особенно для MULTIPLE).
202
- - **Нет `role="group"` / `aria-label`** на контейнере — потребитель должен передать `aria-label`/`aria-labelledby` через `...rest`.
203
- - **Tab-навигация** — каждый Segment в фокусе по Tab. Для radio-like UX (стрелки) семантика отсутствует — это `<button>`, не `<input type="radio">`.
204
- - `disabled` через `<button disabled>` — корректно.
205
- - `title` атрибут — даёт tooltip; для длинных лейблов с overflow — UX полезно.
340
+ ### Types
206
341
 
207
- ---
342
+ `stories/SegmentedControl/examples/Types.tsx`
208
343
 
209
- ## Дизайн-токены
344
+ ```tsx
345
+ import React, { useState } from "react";
346
+ import {
347
+ SegmentedControl,
348
+ ESegmentedControlSize,
349
+ ESegmentedControlTheme,
350
+ ESegmentedControlType,
351
+ } from "@sberbusiness/triplex-next";
352
+
353
+ export const Types = () => {
354
+ const [singleValue, setSingleValue] = useState("segment_3");
355
+ const [multipleValue, setMultipleValue] = useState(["segment_2", "segment_4"]);
356
+
357
+ return (
358
+ <div style={{ display: "flex", flexDirection: "column", gap: 16 }}>
359
+ <div>
360
+ <div style={{ marginBottom: 8, fontSize: 16, fontWeight: 700 }}>SINGLE</div>
361
+ <SegmentedControl
362
+ type={ESegmentedControlType.SINGLE}
363
+ theme={ESegmentedControlTheme.GENERAL_1}
364
+ size={ESegmentedControlSize.LG}
365
+ value={singleValue}
366
+ onSelect={setSingleValue}
367
+ >
368
+ <SegmentedControl.Segment value="segment_1">Segment</SegmentedControl.Segment>
369
+ <SegmentedControl.Segment value="segment_2">Segment</SegmentedControl.Segment>
370
+ <SegmentedControl.Segment value="segment_3">Segment</SegmentedControl.Segment>
371
+ <SegmentedControl.Segment value="segment_4">Segment</SegmentedControl.Segment>
372
+ <SegmentedControl.Segment value="segment_5">Segment</SegmentedControl.Segment>
373
+ </SegmentedControl>
374
+ </div>
375
+ <div>
376
+ <div style={{ marginBottom: 8, fontSize: 16, fontWeight: 700 }}>MULTIPLE</div>
377
+ <SegmentedControl
378
+ type={ESegmentedControlType.MULTIPLE}
379
+ theme={ESegmentedControlTheme.GENERAL_1}
380
+ size={ESegmentedControlSize.LG}
381
+ value={multipleValue}
382
+ onSelect={setMultipleValue}
383
+ >
384
+ <SegmentedControl.Segment value="segment_1">Segment</SegmentedControl.Segment>
385
+ <SegmentedControl.Segment value="segment_2">Segment</SegmentedControl.Segment>
386
+ <SegmentedControl.Segment value="segment_3">Segment</SegmentedControl.Segment>
387
+ <SegmentedControl.Segment value="segment_4">Segment</SegmentedControl.Segment>
388
+ <SegmentedControl.Segment value="segment_5">Segment</SegmentedControl.Segment>
389
+ </SegmentedControl>
390
+ </div>
391
+ </div>
392
+ );
393
+ };
394
+ ```
210
395
 
211
- Нет собственных — наследуются от `Button`/тем (`general_1`, `secondary_1`, ...). Token-структура — в `styles/`.
396
+ ### Themes
212
397
 
213
- ---
398
+ `stories/SegmentedControl/examples/Themes.tsx`
214
399
 
215
- ## Связанные компоненты
400
+ ```tsx
401
+ import React, { useState } from "react";
402
+ import {
403
+ SegmentedControl,
404
+ ESegmentedControlSize,
405
+ ESegmentedControlTheme,
406
+ ESegmentedControlType,
407
+ } from "@sberbusiness/triplex-next";
408
+
409
+ interface IThemeItemProps {
410
+ theme: ESegmentedControlTheme;
411
+ }
412
+
413
+ const ThemeItem = ({ theme }: IThemeItemProps) => {
414
+ const [value, setValue] = useState("segment_3");
415
+
416
+ return (
417
+ <div>
418
+ <div style={{ marginBottom: 8, fontSize: 16, fontWeight: 700 }}>{theme.toUpperCase()}</div>
419
+ <SegmentedControl
420
+ type={ESegmentedControlType.SINGLE}
421
+ theme={theme}
422
+ size={ESegmentedControlSize.LG}
423
+ value={value}
424
+ onSelect={setValue}
425
+ >
426
+ <SegmentedControl.Segment value="segment_1">Segment</SegmentedControl.Segment>
427
+ <SegmentedControl.Segment value="segment_2">Segment</SegmentedControl.Segment>
428
+ <SegmentedControl.Segment value="segment_3">Segment</SegmentedControl.Segment>
429
+ <SegmentedControl.Segment value="segment_4">Segment</SegmentedControl.Segment>
430
+ <SegmentedControl.Segment value="segment_5">Segment</SegmentedControl.Segment>
431
+ </SegmentedControl>
432
+ </div>
433
+ );
434
+ };
435
+
436
+ const THEMES = Object.values(ESegmentedControlTheme);
437
+
438
+ export const Themes = () => (
439
+ <div style={{ display: "flex", flexDirection: "column", gap: 16 }}>
440
+ {THEMES.map((theme) => (
441
+ <ThemeItem key={theme} theme={theme} />
442
+ ))}
443
+ </div>
444
+ );
445
+ ```
216
446
 
217
- - `Tabs` — для большего количества опций / длинных лейблов.
218
- - `Radio` / `Checkbox` (+ `RadioYGroup` / аналог) — альтернатива для form-input семантики.
219
- - `ButtonBase` — нижележащий primitive.
220
- - `IconWrapper` — управление цветом иконок внутри (active/disabled).
447
+ ### Sizes
221
448
 
222
- ---
449
+ `stories/SegmentedControl/examples/Sizes.tsx`
223
450
 
224
- ## Stories
451
+ ```tsx
452
+ import React, { useState } from "react";
453
+ import {
454
+ SegmentedControl,
455
+ ESegmentedControlSize,
456
+ ESegmentedControlTheme,
457
+ ESegmentedControlType,
458
+ } from "@sberbusiness/triplex-next";
459
+
460
+ interface ISizeItemProps {
461
+ size: ESegmentedControlSize;
462
+ }
463
+
464
+ const SizeItem = ({ size }: ISizeItemProps) => {
465
+ const [value, setValue] = useState("segment_3");
466
+
467
+ return (
468
+ <div>
469
+ <div style={{ marginBottom: 8, fontSize: 16, fontWeight: 700 }}>{size.toUpperCase()}</div>
470
+ <SegmentedControl
471
+ type={ESegmentedControlType.SINGLE}
472
+ theme={ESegmentedControlTheme.GENERAL_1}
473
+ size={size}
474
+ value={value}
475
+ onSelect={setValue}
476
+ >
477
+ <SegmentedControl.Segment value="segment_1">Segment</SegmentedControl.Segment>
478
+ <SegmentedControl.Segment value="segment_2">Segment</SegmentedControl.Segment>
479
+ <SegmentedControl.Segment value="segment_3">Segment</SegmentedControl.Segment>
480
+ <SegmentedControl.Segment value="segment_4">Segment</SegmentedControl.Segment>
481
+ <SegmentedControl.Segment value="segment_5">Segment</SegmentedControl.Segment>
482
+ </SegmentedControl>
483
+ </div>
484
+ );
485
+ };
486
+
487
+ const SIZES = Object.values(ESegmentedControlSize);
488
+
489
+ export const Sizes = () => (
490
+ <div style={{ display: "flex", flexDirection: "column", gap: 16 }}>
491
+ {SIZES.map((size) => (
492
+ <SizeItem key={size} size={size} />
493
+ ))}
494
+ </div>
495
+ );
496
+ ```
225
497
 
226
- `stories/SegmentedControl/SegmentedControl.stories.tsx`
227
- Примеры: `stories/SegmentedControl/examples/`
498
+ ### Disabled
228
499
 
229
- ---
500
+ `stories/SegmentedControl/examples/Disabled.tsx`
230
501
 
231
- ## История изменений
502
+ ```tsx
503
+ import React, { useState } from "react";
504
+ import {
505
+ SegmentedControl,
506
+ ESegmentedControlSize,
507
+ ESegmentedControlTheme,
508
+ ESegmentedControlType,
509
+ } from "@sberbusiness/triplex-next";
510
+
511
+ export const Disabled = () => {
512
+ const [value, setValue] = useState("segment_3");
513
+
514
+ return (
515
+ <SegmentedControl
516
+ type={ESegmentedControlType.SINGLE}
517
+ theme={ESegmentedControlTheme.GENERAL_1}
518
+ size={ESegmentedControlSize.LG}
519
+ value={value}
520
+ onSelect={setValue}
521
+ disabled
522
+ >
523
+ <SegmentedControl.Segment value="segment_1">Segment</SegmentedControl.Segment>
524
+ <SegmentedControl.Segment value="segment_2">Segment</SegmentedControl.Segment>
525
+ <SegmentedControl.Segment value="segment_3">Segment</SegmentedControl.Segment>
526
+ <SegmentedControl.Segment value="segment_4">Segment</SegmentedControl.Segment>
527
+ <SegmentedControl.Segment value="segment_5">Segment</SegmentedControl.Segment>
528
+ </SegmentedControl>
529
+ );
530
+ };
531
+ ```
532
+
533
+ ### Example
534
+
535
+ `stories/SegmentedControl/examples/Example.tsx`
536
+
537
+ ```tsx
538
+ import React, { useState } from "react";
539
+ import { DefaulticonStrokePrdIcon20 } from "@sberbusiness/icons-next";
540
+ import {
541
+ SegmentedControl,
542
+ ESegmentedControlSize,
543
+ ESegmentedControlTheme,
544
+ ESegmentedControlType,
545
+ } from "@sberbusiness/triplex-next";
546
+
547
+ /** Индекс палитры иконки: выбранный сегмент рисует иконку на контрастном фоне. */
548
+ const SELECTED_ICON_PALETTE_INDEX = 10;
549
+ const ICON_PALETTE_INDEX = 5;
550
+
551
+ const SEGMENTS = [
552
+ { value: "segment_1", label: "segment 1" },
553
+ { value: "segment_2", label: "segment 2" },
554
+ { value: "segment_3", label: "segment 3" },
555
+ ];
556
+
557
+ export const Example = () => {
558
+ const [value, setValue] = useState("segment_2");
559
+
560
+ return (
561
+ <div style={{ width: 144 }}>
562
+ <SegmentedControl
563
+ type={ESegmentedControlType.SINGLE}
564
+ theme={ESegmentedControlTheme.GENERAL_1}
565
+ size={ESegmentedControlSize.LG}
566
+ value={value}
567
+ onSelect={setValue}
568
+ >
569
+ {SEGMENTS.map((segment) => (
570
+ <SegmentedControl.Segment key={segment.value} value={segment.value} aria-label={segment.label}>
571
+ <DefaulticonStrokePrdIcon20
572
+ paletteIndex={value === segment.value ? SELECTED_ICON_PALETTE_INDEX : ICON_PALETTE_INDEX}
573
+ style={{ display: "block" }}
574
+ />
575
+ </SegmentedControl.Segment>
576
+ ))}
577
+ </SegmentedControl>
578
+ </div>
579
+ );
580
+ };
581
+ ```
232
582
 
233
- | Дата | Изменение |
234
- |---|---|
235
- | 2026-04-18 | Создан документ |