@sberbusiness/triplex-next-mcp-server 1.42.0-beta → 1.43.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.
@@ -1,11 +1,19 @@
1
1
  ---
2
2
  component: TabsExtended
3
- category: Navigation (base)
4
- figma-node: ""
5
- figma-file: ""
6
- related: [Tabs, TabsLine]
7
- tokens: []
8
- stories: ""
3
+ category: Tabs
4
+ related: [Tabs, TabsLine, Badge, Text]
5
+ tokens:
6
+ - --triplex-next-Tabs-Type1_Background
7
+ - --triplex-next-Tabs-Type2_Background
8
+ - --triplex-next-Tabs-Type1_Tab_Background_Default
9
+ - --triplex-next-Tabs-Type2_Tab_Background_Default
10
+ - --triplex-next-Tabs-Type1_Tab_Background_Hover
11
+ - --triplex-next-Tabs-Type2_Tab_Background_Hover
12
+ - --triplex-next-Tabs-Type1_Tab_Background_Selected
13
+ - --triplex-next-Tabs-Type2_Tab_Background_Selected
14
+ - --triplex-next-Tabs-Tab_BorderColor_Default
15
+ - --triplex-next-Tabs-Tab_BorderColor_Focus
16
+ stories: stories/TabsExtended/TabsExtended.stories.tsx
9
17
  version: "1.0"
10
18
  ---
11
19
 
@@ -13,261 +21,439 @@ version: "1.0"
13
21
 
14
22
  ## Назначение
15
23
 
16
- Низкоуровневая основа табов: рендерит `<div role="tablist">` с контекстом
17
- для слотов (`Content`, `TabsWrapper`, `Tab`, `TabButton`, `DropdownWrapper`).
18
- Сам не задаёт компоновку — потребитель выбирает структуру вручную.
19
- `TabsWrapper` автоматически вычисляет, какие табы не помещаются в строку,
20
- и передаёт их id в `DropdownWrapper` для overflow-меню.
24
+ Базовый компонент табов-переключателей: сам он задаёт контекст, корневой контейнер с `role="tablist"` и логику «какие табы не поместились в строку», а разметку каждого таба собирает потребитель из субкомпонентов. Табы, которые не помещаются по ширине, автоматически уезжают в выпадающий список — его рендерит потребитель через render-prop.
21
25
 
22
- Используй когда: нужны табы с кастомной разметкой или своим dropdown'ом.
23
- Для готового решения с `ButtonDropdown` overflow — `Tabs`. Для линейных
24
- табов без overflow — `TabsLine`.
26
+ Используй когда: нужны табы с нестандартным содержимым или нестандартным триггером выпадающего списка — то, что нельзя выразить через готовый `Tabs`.
27
+ Не используй когда:
28
+
29
+ - достаточно списка табов «лейбл + значок уведомления» — возьми `Tabs`: он собран поверх `TabsExtended`, добавляет клавиатурную навигацию стрелками и готовую кнопку выпадающего списка;
30
+ - нужны табы-подчёркивания в шапке раздела — возьми `TabsLine`;
31
+ - нужен просто набор переключателей без семантики табов — это не задача компонента.
25
32
 
26
33
  ---
27
34
 
28
- ## Файловая структура
35
+ ## Состав (compound-компонент)
29
36
 
30
- ```text
31
- src/components/TabsExtended/
32
- ├── TabsExtended.tsx # FC + Composition; Provider
33
- ├── TabsExtendedContext.ts # state overflow-распределения
34
- ├── enums.ts # ETabsExtendedType
35
- ├── types.ts # TTabsExtendedOnSelectTab
36
- ├── utils.ts # SIZE→TEXT, TYPE→className maps
37
- ├── components/
38
- │ ├── TabsExtendedContent.tsx # FC + Composition
39
- │ ├── TabsExtendedTabsWrapper.tsx # measurement + overflow
40
- │ ├── TabsExtendedTab.tsx # forwardRef<HTMLSpanElement>
41
- │ ├── TabsExtendedTabButton.tsx # forwardRef<HTMLButtonElement>
42
- │ ├── TabsExtendedDropdownWrapper.tsx # render-prop slot
43
- │ └── TabsExtendedTabContext.ts # { isFakeTab }
44
- ├── styles/
45
- └── index.ts
46
- ```
37
+ Состав задаётся вложенностью субкомпонентов, порядок важен: `TabsWrapper` должен идти раньше `DropdownWrapper`.
47
38
 
48
- ---
39
+ | Субкомпонент | Обязателен | Что делает |
40
+ |---|---|---|
41
+ | `TabsExtended.Content` | Да | Контейнер с фоном, скруглением и внутренними отступами. Несёт размер (`size`) |
42
+ | `TabsExtended.Content.TabsWrapper` | Да | Рендерит табы и скрытую копию табов для замеров. По замерам решает, какие табы уезжают в Dropdown |
43
+ | `TabsExtended.Content.Tab` | Да | Контейнер (`<span>`) одного таба. Render-prop отдаёт `selected`, `isFirstInlineTab`, `isLastInlineTab`. Обрабатывает клик |
44
+ | `TabsExtended.Content.TabButton` | Практически всегда | Кнопка таба: `<button role="tab">` с текстом и опциональным значком уведомлений |
45
+ | `TabsExtended.Content.DropdownWrapper` | Нет | Контейнер выпадающего списка. Render-prop отдаёт id не поместившихся табов и обработчик выбора. Скрыт (`hidden`), пока все табы помещаются |
49
46
 
50
- ## Композиция
47
+ Без `DropdownWrapper` компонент работает, но не поместившиеся табы просто исчезают из строки — попасть в них будет нельзя.
51
48
 
52
- ```tsx
53
- <TabsExtended selectedId={id} onSelectTab={setId} type={ETabsExtendedType.TYPE_1}>
54
- <TabsExtended.Content size={EComponentSize.MD}>
55
- <TabsExtended.Content.TabsWrapper>
56
- {tabs.map(t => (
57
- <TabsExtended.Content.Tab key={t.id} id={t.id}>
58
- {({ selected, isFirstInlineTab, isLastInlineTab }) => (
59
- <TabsExtended.Content.TabButton selected={selected}>
60
- {t.label}
61
- </TabsExtended.Content.TabButton>
62
- )}
63
- </TabsExtended.Content.Tab>
64
- ))}
65
- </TabsExtended.Content.TabsWrapper>
66
-
67
- <TabsExtended.Content.DropdownWrapper>
68
- {({ dropdownItemsIds, onSelectTab }) => (/* свой dropdown */)}
69
- </TabsExtended.Content.DropdownWrapper>
70
- </TabsExtended.Content>
71
- </TabsExtended>
72
- ```
49
+ ---
73
50
 
74
- Все компоненты экспортируются также напрямую: `TabsExtendedContent`,
75
- `TabsExtendedTabsWrapper`, `TabsExtendedTab`, `TabsExtendedTabButton`,
76
- `TabsExtendedDropdownWrapper`.
51
+ ## Варианты и props
77
52
 
78
- ---
53
+ `ITabsExtendedProps` расширяет `React.HTMLAttributes<HTMLDivElement>`. Неизвестные компоненту атрибуты уходят на корневой `<div>`.
79
54
 
80
- ## Публичный API
55
+ ### Обязательные props
81
56
 
82
- ### `ITabsExtendedProps`
57
+ | Prop | Тип | Описание |
58
+ |---|---|---|
59
+ | `selectedId` | `string` | Id выбранного таба. Компонент полностью управляемый: своего состояния выбора у него нет |
60
+ | `onSelectTab` | `TTabsExtendedOnSelectTab` | Запрос на смену выбранного таба. Вызывается **только** если кликнули по табу, отличному от `selectedId` |
61
+
62
+ ### Опциональные props
83
63
 
84
- | Prop | Тип | Default | Описание |
64
+ | Prop | Тип | По умолчанию | Описание |
85
65
  |---|---|---|---|
86
- | `selectedId` | `string` | — | Id выбранного таба. |
87
- | `onSelectTab` | `(id: string) => void` | — | Колбэк выбора. Не вызывается, если `id === selectedId`. |
88
- | `type?` | `ETabsExtendedType` | `TYPE_1` | Стиль таба. |
89
- | `...HTMLAttributes<HTMLDivElement>` | — | — | На корневой div (role=tablist). |
66
+ | `type` | `ETabsExtendedType` | `TYPE_1` | Тип оформления, выбирается по фону **страницы**: `TYPE_1` — серый фон контейнера, для белой страницы; `TYPE_2` — белый фон контейнера, для затенённой страницы. Значение уходит в контекст и применяется к кнопкам таба |
90
67
 
91
- ### `ITabsExtendedContentProps`
68
+ ### Props субкомпонентов
92
69
 
93
- | Prop | Тип | Default | Описание |
70
+ | Субкомпонент | Prop | По умолчанию | Описание |
94
71
  |---|---|---|---|
95
- | `size?` | `EComponentSize` | `MD` | Размер; маппится на CSS-класс через `createSizeToClassNameMap`. |
96
- | `...HTMLAttributes<HTMLDivElement>` | — | — | На контейнер. |
72
+ | `Content` | `size` | `EComponentSize.MD` | Скругление и внутренние отступы контейнера |
73
+ | `Tab` | `id` | — | Id таба. Сопоставляется с `selectedId` и с id из `DropdownWrapper` |
74
+ | `TabButton` | `selected` | `false` | Выбранное состояние: класс `selected` и `aria-selected` |
75
+ | `TabButton` | `size` | `EComponentSize.MD` | Размер кнопки и размер текста внутри неё |
76
+ | `TabButton` | `showNotificationIcon` | `false` | Значок новых уведомлений (`Badge.Dot`) в правом верхнем углу кнопки |
97
77
 
98
- ### `ITabsExtendedTabProps`
78
+ ### Ограничения
99
79
 
100
- ```ts
101
- extends Omit<HTMLAttributes<HTMLSpanElement>, "children">
102
- {
103
- id: string;
104
- children: (props: ITabsExtendedItemProvideProps) => ReactNode;
105
- }
106
- ```
80
+ - `size` на `Content` и `size` на `TabButton` — **независимые props**. Контейнер не транслирует свой размер кнопкам; передавать нужно оба, иначе отступы контейнера и высота кнопок разойдутся.
81
+ - `Tab` и `DropdownWrapper` принимают `children` только как функцию (render-prop). Обычные узлы туда положить нельзя — тип это запрещает.
82
+ - Компонент не рендерит выпадающий список сам: и триггер, и список — целиком на потребителе. В библиотеке для этого обычно берут `ButtonDropdown`.
83
+ - **Высоту триггера потребитель обязан согласовать с высотой кнопки таба.** `DropdownWrapper` — обычный flex-элемент строки, а строка тянется по самому высокому: `ButtonDropdown` размера MD — 40px против 32px у таба того же размера, поэтому «сырой» триггер растягивает и контейнер (40px → 48px), и сами табы (32px → 40px, из-за `align-items: stretch`). Ориентир по высоте: SM — 20px, MD — 32px, LG — 40px; в библиотеке это зашито в `Tabs` классом `.tabButtonDropdown`, в примерах stories — через `buttonAttributes.style`.
84
+ - У `TabButton` нет собственного оформления для `disabled`: атрибут проставится, но визуально кнопка не изменится.
85
+ - Возможность переопределить роль зависит от порядка спреда и у каждого субкомпонента своя: на `TabsExtended` `role="tablist"` стоит до `...rest` — переопределяется; на `Tab` `role="presentation"` и `data-tab-item-id` стоят после спреда — не переопределяются (и `data-tab-item-id` трогать нельзя, по нему идут замеры); на `TabButton` спред идёт последним — переопределяется всё, включая `role="tab"`, `aria-selected` и `type`.
107
86
 
108
- `ITabsExtendedItemProvideProps`:
87
+ ---
109
88
 
110
- | Поле | Тип | Описание |
111
- |---|---|---|
112
- | `selected` | `boolean` | `selectedId === id`. |
113
- | `isFirstInlineTab` | `boolean` | Первый в `inlineItemsIds`. |
114
- | `isLastInlineTab` | `boolean` | Последний в `inlineItemsIds`. |
89
+ ## Дизайн-токены
115
90
 
116
- ### `ITabsExtendedButtonProps`
91
+ Компонент использует токены семейства `Tabs` — они общие с `Tabs`:
117
92
 
118
- ```ts
119
- extends ButtonHTMLAttributes<HTMLButtonElement>
93
+ ```text
94
+ --triplex-next-Tabs-Type1_Background
95
+ --triplex-next-Tabs-Type2_Background
96
+ --triplex-next-Tabs-Type1_Tab_Background_Default
97
+ --triplex-next-Tabs-Type2_Tab_Background_Default
98
+ --triplex-next-Tabs-Type1_Tab_Background_Hover
99
+ --triplex-next-Tabs-Type2_Tab_Background_Hover
100
+ --triplex-next-Tabs-Type1_Tab_Background_Selected
101
+ --triplex-next-Tabs-Type2_Tab_Background_Selected
102
+ --triplex-next-Tabs-Tab_BorderColor_Default
103
+ --triplex-next-Tabs-Tab_BorderColor_Focus
120
104
  ```
121
105
 
122
- | Prop | Тип | Default | Описание |
123
- |---|---|---|---|
124
- | `selected?` | `boolean` | — | Подсветка + `aria-selected`. |
125
- | `size?` | `EComponentSize` | `MD` | Размер. |
126
- | `showNotificationIcon?` | `boolean` | — | Рендерит `Badge.Dot`. |
106
+ Собственного префикса `--triplex-next-TabsExtended-*` у компонента нет: переименование токенов `Tabs` ломает оба компонента сразу.
127
107
 
128
- Контент рендерится дважды (`Text` default + hover-вариант) для CSS-эффектов.
108
+ ---
129
109
 
130
- ### `ITabsExtendedTabsWrapperProps`
110
+ ## Инварианты
131
111
 
132
- ```ts
133
- extends HTMLAttributes<HTMLDivElement>
134
- ```
112
+ - `forwardRef` есть у `TabsExtended` и всех пяти публичных субкомпонентов — не убирать. Ref указывает на тот же элемент, который получает `className` и `...rest`: у `TabsWrapper` это **отображаемый** контейнер (`tabsReal`), а не скрытый контейнер замеров.
113
+ - `TabsExtended` — `forwardRef`-компонент со статическим свойством `Content`, у `Content` — статические `TabsWrapper`, `Tab`, `TabButton`, `DropdownWrapper`. Набор статических свойств — часть публичного API.
114
+ - Интерфейс props кнопки называется `ITabsExtendedButtonProps` (без `Tab` в середине) — рассинхрон с именем компонента `TabsExtendedTabButton`. Переименование — breaking change, требует отдельного согласования.
115
+ - Атрибут `data-tab-item-id` на `Tab` — контракт замеров: по нему `TabsWrapper` находит табы среди детей скрытого контейнера. Не переименовывать и не снимать.
116
+ - Скрытый контейнер `tabsFake` обязан идти в разметке **раньше** отображаемого `tabsReal`: на этом построено сравнение позиций.
117
+ - `TabsWrapper` копирует `children` в скрытый контейнер, вырезая `data-`атрибуты потребителя (`stripDataAttributes`). Это нужно, чтобы селекторы по `data-`атрибутам не находили две копии одного таба. Копия создаётся через `React.createElement(child.type, props)` — компонент-обёртка вокруг `Tab` в копию не попадёт корректно, вкладывать `Tab` в собственные обёртки не стоит.
118
+ - У таба, уехавшего в Dropdown, и у любого таба из скрытой копии атрибут `id` снимается — иначе в DOM оказались бы два элемента с одинаковым `id`. `data-tab-item-id` при этом остаётся.
119
+ - Контекст `TabsExtendedContext` и `TabsExtendedTabContext` через barrel `index.ts` не экспортируются (`TabsExtendedTabContext` реэкспортируется из `components/index.ts` — это исторический факт, а не приглашение им пользоваться). Их форма — внутренняя деталь.
120
+ - `Tabs` построен поверх `TabsExtended` и опирается на `isFirstInlineTab` / `isLastInlineTab` в клавиатурной навигации. Любое изменение семантики этих флагов ломает `Tabs`.
121
+ - Значение контекста мемоизировано (`useMemo`), а `onSelectTab` обёрнут в `useCallback`. Возврат к пересозданию объекта на каждый рендер вернёт лишние перерисовки всех табов.
135
122
 
136
- Без публичных полей — только htmlDivAttributes.
123
+ ---
137
124
 
138
- ### `ITabsExtendedDropdownWrapperProps`
125
+ ## Accessibility
139
126
 
140
- ```ts
141
- extends Omit<HTMLAttributes<HTMLDivElement>, "children">
142
- {
143
- children: (props: ITabsExtendedDropdownWrapperProvideProps) => ReactNode;
144
- }
145
- ```
127
+ - Корневой элемент — `role="tablist"`, кнопка таба — `role="tab"` с `aria-selected`. Контейнер таба (`<span>`) помечен `role="presentation"`, чтобы не добавлять лишний уровень в дерево доступности.
128
+ - `aria-controls` и связку с панелью контента компонент **не выставляет** — если табы переключают панели, `id` панели и `aria-controls` проставляет потребитель.
129
+ - Клавиатурной навигации стрелками у `TabsExtended` нет: обработка `ArrowLeft` / `ArrowRight` и roving `tabIndex` живут в `Tabs`. Если строишь свой набор табов на `TabsExtended`, клавиатуру нужно реализовать самому — флаги `isFirstInlineTab` / `isLastInlineTab` из render-prop `Tab` для этого и предназначены.
130
+ - Выбор таба срабатывает по клику на контейнере `Tab`; нативная активация `<button>` по `Enter` / `Space` работает за счёт того, что `TabButton` рендерит настоящий `<button>`. Если подставляешь свой компонент кнопки, он тоже должен рендерить `<button>`, иначе выбор с клавиатуры сломается.
131
+ - Фокус подсвечивается через `:focus-visible` (токен `Tab_BorderColor_Focus`) — только при клавиатурной навигации.
132
+ - Таб, уехавший в Dropdown, скрыт через `display: none` и из порядка обхода клавиатурой выпадает; попасть в него можно только через выпадающий список.
133
+ - Компонент не хардкодит текст: `aria-label` кнопки выпадающего списка задаёт потребитель (библиотека мультиязычная).
146
134
 
147
- `ITabsExtendedDropdownWrapperProvideProps`:
135
+ ---
148
136
 
149
- | Поле | Тип | Описание |
150
- |---|---|---|
151
- | `dropdownItemsIds` | `string[]` | Id табов, не поместившихся inline. |
152
- | `onSelectTab` | `(id: string) => void` | Колбэк выбора (тот же, что в Context). |
137
+ ## Связанные компоненты
153
138
 
154
- ### Enums
139
+ Отдельного AI.md нет ни у одного субкомпонента семейства — все они описаны здесь. Это область задачи TRI-91 («Scope: только TabsExtended»): в таблице `docs/ai/ROADMAP.md` у `TabsExtended` одна строка.
155
140
 
156
- - `ETabsExtendedType`: `TYPE_1` | `TYPE_2`
141
+ **Части составного компонента (`components/`):**
157
142
 
158
- ---
143
+ - `TabsExtendedContent` — контейнер с фоном и размером; хост статических субкомпонентов.
144
+ - `TabsExtendedTabsWrapper` — замеры и распределение табов между строкой и Dropdown; единственное место, где живёт `useResizeDetector`.
145
+ - `TabsExtendedTab` — контейнер таба, обработка клика, render-prop с состоянием.
146
+ - `TabsExtendedTabButton` — кнопка таба: два слоя текста (обычный и hover-состояние) и значок уведомлений.
147
+ - `TabsExtendedDropdownWrapper` — контейнер выпадающего списка; отдаёт наружу id не поместившихся табов.
159
148
 
160
- ## Контекст
149
+ **Инфраструктура (через barrel не экспортируется):**
161
150
 
162
- ### `TabsExtendedContext`
151
+ - `TabsExtendedContext` — выбранный таб, распределение табов, ref на Dropdown, тип оформления.
152
+ - `TabsExtendedTabContext` — флаг `isFakeTab`, отличающий скрытую копию таба от отображаемой.
153
+ - `utils.ts` — маппинги «размер → размер текста» и «тип → CSS-класс».
163
154
 
164
- ```ts
165
- {
166
- dropdownItemsIds: string[]; // id табов в overflow
167
- inlineItemsIds: string[]; // id видимых inline
168
- setDropdownItemsIds(ids): void;
169
- setInlineItemsIds(ids): void;
170
- selectedId: string;
171
- onSelectTab(id): void; // wrapper, не вызывает onSelect повторно
172
- type: ETabsExtendedType;
173
- dropdownRef: RefObject<HTMLDivElement>; // на DropdownWrapper, для замера ширины
174
- }
175
- ```
155
+ **Соседние компоненты:**
176
156
 
177
- ### `TabsExtendedTabContext`
157
+ - `Tabs` — готовые табы поверх `TabsExtended`: принимает массив `tabs`, сам рендерит `ButtonDropdown` и добавляет навигацию стрелками. Первый выбор для типовой задачи.
158
+ - `TabsLine` — альтернативный визуальный паттерн табов (подчёркивание вместо залитой кнопки), собственной логики переполнения не имеет.
159
+ - `Badge` — `TabButton` рендерит `Badge.Dot` как значок новых уведомлений.
160
+ - `Text` — текст внутри `TabButton`; размер выбирается по `size` через `TABS_EXTENDED_SIZE_TO_TEXT_SIZE_MAP`.
161
+ - `ButtonDropdown` — компонент, которым в примерах и в `Tabs` заполняется `DropdownWrapper`. Зависимости от него у `TabsExtended` нет.
178
162
 
179
- ```ts
180
- { isFakeTab: boolean }
181
- ```
163
+ ---
164
+
165
+ ## Stories
166
+
167
+ Основные истории: `stories/TabsExtended/TabsExtended.stories.tsx`
168
+ Файлы примеров: `stories/TabsExtended/examples/`
182
169
 
183
- Включается только внутри fake-копии табов (см. ниже).
170
+ | Story | Example file | Что демонстрирует |
171
+ |---|---|---|
172
+ | `Playground` | `Playground.tsx` | Интерактивный контроль `type`, размера, количества табов и ширины контейнера |
173
+ | `Default` | `Default.tsx` | Минимальный состав: три таба без выпадающего списка |
174
+ | `Types` | `Types.tsx` | Типы оформления `TYPE_1` и `TYPE_2` |
175
+ | `Sizes` | `Sizes.tsx` | Размеры SM / MD / LG на `Content` и `TabButton` |
176
+ | `WithNotificationIcon` | `WithNotificationIcon.tsx` | Значок новых уведомлений на табах |
177
+ | `WithDropdown` | `WithDropdown.tsx` | Узкий контейнер: часть табов уезжает в `ButtonDropdown` |
178
+ | `VisualTests` | `VisualTests.tsx` | Типы × размеры, значок уведомлений, раскрытый выпадающий список, `:focus-visible` на табе (через `play`) |
184
179
 
185
180
  ---
186
181
 
187
- ## Overflow-механика (`TabsExtendedTabsWrapper`)
182
+ ## История изменений
188
183
 
189
- Рендерит **две копии** табов:
184
+ | Дата | Изменение |
185
+ |---|---|
186
+ | 2026-08-14 | Создан документ AI-ready для `TabsExtended` (TRI-91). |
187
+ | 2026-08-14 | Правки по ревью (документация и stories, кода компонента не касаются): в «Ограничения» добавлено требование согласовывать высоту триггера Dropdown с высотой кнопки таба — «сырой» `ButtonDropdown` растягивает контейнер и табы; в примерах stories это согласование сделано через `buttonAttributes.style` (проверено в браузере: контейнер 48px → 40px, таб 40px → 32px). Описание `ETabsExtendedType` в JSDoc и здесь приведено к одной формулировке — тип выбирается по фону страницы, а не контейнера. |
188
+ | 2026-08-14 | AI-рефакторинг: `TabsExtended`, `TabsExtendedContent`, `TabsExtendedTabsWrapper` и `TabsExtendedDropdownWrapper` переведены с `React.FC` на `forwardRef` (у `DropdownWrapper` ref потребителя объединён с внутренним ref замеров), у всех субкомпонентов появился `displayName`. Значение контекста мемоизировано. В `TabsWrapper` убраны non-null assertion'ы, чтение ref во время рендера заменено на состояние, зависимости `useLayoutEffect` приведены в порядок, константы и `stripDataAttributes` вынесены на module scope. JSDoc проставлен на всех публичных props, enum и типах. Публичный API (имена props, значения enum, barrel-экспорты) не изменён. Добавлены unit-тесты на все субкомпоненты и stories по modern pattern. |
190
189
 
191
- 1. **`tabsFake`** (скрытый) — внутри `TabsExtendedTabContext.Provider value={isFakeTab: true}`. Табы рендерятся без `id` и без `onClick` (см. `TabsExtendedTab`), `data-` атрибуты у детей удаляются (`stripDataAttributes`). Используется для замера фактической ширины каждого таба.
192
- 2. **`tabsReal`** (видимый) — настоящие табы.
190
+ ---
193
191
 
194
- `checkVisibleItems` (вызывается в `useLayoutEffect` + по `useResizeDetector` throttle 150ms):
195
- - Идёт по детям `tabsFake`, читает `getBoundingClientRect`.
196
- - Если `right > containerRight - 4 - dropdownWidth` → таб уходит в `dropdownIds`.
197
- - `containerRight - 4` — учёт padding (2px) + gap (2px).
198
- - Если **хотя бы один** таб реально вылез за `containerRight` (`overflow = true`),
199
- выставляется `setDropdownItemsIds(dropdownIds)`. Иначе — `[]` (всё помещается).
200
- - Inline-табы — те, у которых `data-tab-item-id` есть и right укладывается.
192
+ ## Примеры использования
201
193
 
202
- Размер dropdown отслеживается через `dropdownRef.getBoundingClientRect()` в
203
- `useLayoutEffect` каждый рендер; при изменении — повторный замер.
194
+ ### Default
204
195
 
205
- ---
196
+ `stories/TabsExtended/examples/Default.tsx`
206
197
 
207
- ## `TabsExtendedTab` поведение
198
+ ```tsx
199
+ import React, { useState } from "react";
200
+ import { TabsExtended } from "@sberbusiness/triplex-next";
201
+
202
+ const TABS = [
203
+ { id: "tab-1", label: "Обзор" },
204
+ { id: "tab-2", label: "Операции" },
205
+ { id: "tab-3", label: "Документы" },
206
+ ];
207
+
208
+ export const Default = () => {
209
+ const [selectedId, setSelectedId] = useState("tab-1");
210
+
211
+ return (
212
+ <TabsExtended selectedId={selectedId} onSelectTab={setSelectedId}>
213
+ <TabsExtended.Content>
214
+ <TabsExtended.Content.TabsWrapper>
215
+ {TABS.map(({ id, label }) => (
216
+ <TabsExtended.Content.Tab key={id} id={id}>
217
+ {({ selected }) => (
218
+ <TabsExtended.Content.TabButton selected={selected}>
219
+ {label}
220
+ </TabsExtended.Content.TabButton>
221
+ )}
222
+ </TabsExtended.Content.Tab>
223
+ ))}
224
+ </TabsExtended.Content.TabsWrapper>
225
+ </TabsExtended.Content>
226
+ </TabsExtended>
227
+ );
228
+ };
229
+ ```
208
230
 
209
- - `<span role="presentation" data-tab-item-id={id}>` — оборачивает render-prop children.
210
- - `id` атрибут НЕ ставится, если `isFakeTab` или таб в `dropdownItemsIds` (избегаем дублирования id в DOM).
211
- - `onClick` блокируется в fake-режиме.
212
- - Видимость: при попадании в overflow — добавляется `styles.hidden` (display:none), но таб остаётся в DOM (для пересчёта inline-кадра).
231
+ ### Types
213
232
 
214
- ---
233
+ `stories/TabsExtended/examples/Types.tsx`
215
234
 
216
- ## `TabsExtendedTabButton` поведение
235
+ ```tsx
236
+ import React, { useState } from "react";
237
+ import { ETabsExtendedType, TabsExtended } from "@sberbusiness/triplex-next";
217
238
 
218
- - `<button type="button" role="tab" aria-selected={selected}>`.
219
- - Внутри — два `<Text>` (default + hover) для CSS-перехода стилей.
220
- - `Badge.Dot` — при `showNotificationIcon`.
221
- - Размер текста: SIZE→`ETextSize` через `TABS_EXTENDED_SIZE_TO_TEXT_SIZE_MAP` (LG=B2, MD=B3, SM=B4).
239
+ const TABS = [
240
+ { id: "tab-1", label: "Обзор" },
241
+ { id: "tab-2", label: "Операции" },
242
+ { id: "tab-3", label: "Документы" },
243
+ ];
222
244
 
223
- ---
245
+ interface ITypeItemProps {
246
+ type: ETabsExtendedType;
247
+ }
224
248
 
225
- ## Инварианты
249
+ const TypeItem = ({ type }: ITypeItemProps) => {
250
+ const [selectedId, setSelectedId] = useState("tab-1");
251
+
252
+ return (
253
+ <div>
254
+ <div style={{ marginBottom: "8px", fontSize: "16px", fontWeight: "700" }}>{type}</div>
255
+ <TabsExtended type={type} selectedId={selectedId} onSelectTab={setSelectedId}>
256
+ <TabsExtended.Content>
257
+ <TabsExtended.Content.TabsWrapper>
258
+ {TABS.map(({ id, label }) => (
259
+ <TabsExtended.Content.Tab key={id} id={id}>
260
+ {({ selected }) => (
261
+ <TabsExtended.Content.TabButton selected={selected}>
262
+ {label}
263
+ </TabsExtended.Content.TabButton>
264
+ )}
265
+ </TabsExtended.Content.Tab>
266
+ ))}
267
+ </TabsExtended.Content.TabsWrapper>
268
+ </TabsExtended.Content>
269
+ </TabsExtended>
270
+ </div>
271
+ );
272
+ };
273
+
274
+ const TYPES = Object.values(ETabsExtendedType);
275
+
276
+ export const Types = () => (
277
+ <div style={{ display: "flex", flexDirection: "column", gap: "24px" }}>
278
+ {TYPES.map((type) => (
279
+ <TypeItem key={type} type={type} />
280
+ ))}
281
+ </div>
282
+ );
283
+ ```
226
284
 
227
- - **`TabsExtended` — `FC` + composition** (`TabsExtended.Content = ...`).
228
- - **`TabsExtended.Content` — `FC` + composition** (`TabsWrapper`/`Tab`/`TabButton`/`DropdownWrapper`).
229
- - **`TabsExtendedTab` и `TabsExtendedTabButton` — `forwardRef`** (Span/Button). Не убирать.
230
- - **`Tab.children` — render-prop**: `(state) => ReactNode`. Не accept'ить плоские дети.
231
- - **`DropdownWrapper.children` — render-prop**: получает `dropdownItemsIds` + `onSelectTab`.
232
- - **`TabsWrapper` рендерит две копии children** (fake + real). Любой таб должен корректно работать как fake (без `id`, без onClick).
233
- - **`stripDataAttributes` чистит только `data-*` пропсы у прямых children** (на 1 уровень). Глубже — потребитель ответственен.
234
- - **`onSelectTab` в Context — wrapper**, который НЕ вызывает props.onSelectTab при `id === selectedId`. Дедупликация выбора.
235
- - **`dropdownRef` — на `DropdownWrapper`** (не на самой кнопке dropdown'а потребителя). Используется для замера зарезервированной ширины.
236
- - **`role="tablist"` / `role="tab"` / `aria-selected`** — обязательная семантика; не переопределять без причины.
237
- - **`ETabsExtendedType` экспортируется** — часть публичного API.
285
+ ### Sizes
238
286
 
239
- ---
287
+ `stories/TabsExtended/examples/Sizes.tsx`
240
288
 
241
- ## Accessibility
289
+ ```tsx
290
+ import React, { useState } from "react";
291
+ import { EComponentSize, TabsExtended } from "@sberbusiness/triplex-next";
242
292
 
243
- - `<div role="tablist">` на корне.
244
- - `<button role="tab" aria-selected>` на каждой кнопке.
245
- - Управление фокусом (roving tabindex, ArrowLeft/Right) — НЕ встроено;
246
- потребитель реализует сам (см. `Tabs`).
247
- - `<TabsExtendedTab>` — `role="presentation"` (это обёртка, не tab сам по себе).
248
- - Таб в overflow не имеет id и убран из таб-ордера через `display:none` —
249
- попадает в кастомный dropdown потребителя (там ARIA — на потребителе).
293
+ const TABS = [
294
+ { id: "tab-1", label: "Обзор" },
295
+ { id: "tab-2", label: "Операции" },
296
+ { id: "tab-3", label: "Документы" },
297
+ ];
250
298
 
251
- ---
299
+ interface ISizeItemProps {
300
+ size: EComponentSize;
301
+ }
252
302
 
253
- ## Связанные компоненты
303
+ const SizeItem = ({ size }: ISizeItemProps) => {
304
+ const [selectedId, setSelectedId] = useState("tab-1");
305
+
306
+ return (
307
+ <div>
308
+ <div style={{ marginBottom: "8px", fontSize: "16px", fontWeight: "700" }}>{size.toUpperCase()}</div>
309
+ <TabsExtended selectedId={selectedId} onSelectTab={setSelectedId}>
310
+ {/* size задаётся отдельно: на Content — отступы и скругление контейнера, на TabButton — высота кнопки. */}
311
+ <TabsExtended.Content size={size}>
312
+ <TabsExtended.Content.TabsWrapper>
313
+ {TABS.map(({ id, label }) => (
314
+ <TabsExtended.Content.Tab key={id} id={id}>
315
+ {({ selected }) => (
316
+ <TabsExtended.Content.TabButton selected={selected} size={size}>
317
+ {label}
318
+ </TabsExtended.Content.TabButton>
319
+ )}
320
+ </TabsExtended.Content.Tab>
321
+ ))}
322
+ </TabsExtended.Content.TabsWrapper>
323
+ </TabsExtended.Content>
324
+ </TabsExtended>
325
+ </div>
326
+ );
327
+ };
328
+
329
+ const SIZES = Object.values(EComponentSize);
330
+
331
+ export const Sizes = () => (
332
+ <div style={{ display: "flex", flexDirection: "column", gap: "24px" }}>
333
+ {SIZES.map((size) => (
334
+ <SizeItem key={size} size={size} />
335
+ ))}
336
+ </div>
337
+ );
338
+ ```
254
339
 
255
- - `Tabs` — готовый таб-бар поверх TabsExtended с `ButtonDropdown` overflow.
256
- - `TabsLine` — горизонтальные линейные табы (без overflow, своя реализация).
257
- - `Badge.Dot` — индикатор в `TabButton`.
258
- - `Typography.Text` — текст лейбла.
340
+ ### WithNotificationIcon
259
341
 
260
- ---
342
+ `stories/TabsExtended/examples/WithNotificationIcon.tsx`
261
343
 
262
- ## Stories
344
+ ```tsx
345
+ import React, { useState } from "react";
346
+ import { TabsExtended } from "@sberbusiness/triplex-next";
347
+
348
+ const TABS = [
349
+ { id: "tab-1", label: "Обзор", showNotificationIcon: false },
350
+ { id: "tab-2", label: "Операции", showNotificationIcon: true },
351
+ { id: "tab-3", label: "Документы", showNotificationIcon: true },
352
+ ];
353
+
354
+ export const WithNotificationIcon = () => {
355
+ const [selectedId, setSelectedId] = useState("tab-1");
356
+
357
+ return (
358
+ <TabsExtended selectedId={selectedId} onSelectTab={setSelectedId}>
359
+ <TabsExtended.Content>
360
+ <TabsExtended.Content.TabsWrapper>
361
+ {TABS.map(({ id, label, showNotificationIcon }) => (
362
+ <TabsExtended.Content.Tab key={id} id={id}>
363
+ {({ selected }) => (
364
+ <TabsExtended.Content.TabButton
365
+ selected={selected}
366
+ showNotificationIcon={showNotificationIcon}
367
+ >
368
+ {label}
369
+ </TabsExtended.Content.TabButton>
370
+ )}
371
+ </TabsExtended.Content.Tab>
372
+ ))}
373
+ </TabsExtended.Content.TabsWrapper>
374
+ </TabsExtended.Content>
375
+ </TabsExtended>
376
+ );
377
+ };
378
+ ```
263
379
 
264
- Отдельных stories нет — демонстрируется через `Tabs` и собственные примеры
265
- в `src/components/TabsExtended/components/index.ts` (`Type1Example`, `Type2Example`, `PlaygroundExample`).
380
+ ### WithDropdown
266
381
 
267
- ---
382
+ `stories/TabsExtended/examples/WithDropdown.tsx`
268
383
 
269
- ## История изменений
384
+ ```tsx
385
+ import React, { useState } from "react";
386
+ import {
387
+ ButtonDropdown,
388
+ EButtonDotsTheme,
389
+ EComponentSize,
390
+ ITabsExtendedDropdownWrapperProvideProps,
391
+ TabsExtended,
392
+ } from "@sberbusiness/triplex-next";
393
+
394
+ const TABS = [
395
+ { id: "tab-1", label: "Обзор" },
396
+ { id: "tab-2", label: "Операции" },
397
+ { id: "tab-3", label: "Документы" },
398
+ { id: "tab-4", label: "Реквизиты" },
399
+ { id: "tab-5", label: "Выписка" },
400
+ { id: "tab-6", label: "Настройки" },
401
+ ];
402
+
403
+ /**
404
+ * Высоту триггера нужно согласовать с высотой кнопки таба: по умолчанию кнопка размера MD
405
+ * выше таба (40px против 32px) и растягивает по себе и контейнер, и сами табы.
406
+ */
407
+ const DROPDOWN_TRIGGER_STYLE: React.CSSProperties = {
408
+ minWidth: "32px",
409
+ minHeight: "32px",
410
+ height: "32px",
411
+ padding: 0,
412
+ justifyContent: "center",
413
+ };
414
+
415
+ export const WithDropdown = () => {
416
+ const [selectedId, setSelectedId] = useState("tab-1");
417
+
418
+ /** Табы, не поместившиеся в строку, превращаются в опции выпадающего списка. */
419
+ const getDropdownOptions = ({ dropdownItemsIds, onSelectTab }: ITabsExtendedDropdownWrapperProvideProps) =>
420
+ TABS.filter(({ id }) => dropdownItemsIds.includes(id)).map((tab) => ({
421
+ ...tab,
422
+ onSelect: () => onSelectTab(tab.id),
423
+ }));
424
+
425
+ return (
426
+ // Ширина контейнера ограничена, чтобы часть табов не поместилась в строку.
427
+ <div style={{ maxWidth: "420px" }}>
428
+ <TabsExtended selectedId={selectedId} onSelectTab={setSelectedId}>
429
+ <TabsExtended.Content>
430
+ <TabsExtended.Content.TabsWrapper>
431
+ {TABS.map(({ id, label }) => (
432
+ <TabsExtended.Content.Tab key={id} id={id}>
433
+ {({ selected }) => (
434
+ <TabsExtended.Content.TabButton selected={selected}>
435
+ {label}
436
+ </TabsExtended.Content.TabButton>
437
+ )}
438
+ </TabsExtended.Content.Tab>
439
+ ))}
440
+ </TabsExtended.Content.TabsWrapper>
441
+
442
+ <TabsExtended.Content.DropdownWrapper>
443
+ {(provideProps) => (
444
+ <ButtonDropdown
445
+ theme={EButtonDotsTheme.DOTS_SECONDARY}
446
+ size={EComponentSize.MD}
447
+ options={getDropdownOptions(provideProps)}
448
+ selected={TABS.find(({ id }) => id === selectedId)}
449
+ buttonAttributes={{ "aria-label": "Ещё табы", style: DROPDOWN_TRIGGER_STYLE }}
450
+ />
451
+ )}
452
+ </TabsExtended.Content.DropdownWrapper>
453
+ </TabsExtended.Content>
454
+ </TabsExtended>
455
+ </div>
456
+ );
457
+ };
458
+ ```
270
459
 
271
- | Дата | Изменение |
272
- |---|---|
273
- | 2026-04-18 | Создан документ |