@sberbusiness/triplex-next-mcp-server 1.43.0-beta → 1.44.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,10 +1,8 @@
1
1
  ---
2
2
  component: Tag
3
- category: Data Display
4
- figma-node: ""
5
- figma-file: ""
6
- related: [TagColor, TagGroup, ButtonIcon]
7
- tokens: []
3
+ category: Tag
4
+ related: [TagGroup, TagColor, ButtonIcon, Text]
5
+ tokens: [--triplex-next-Tag-Background]
8
6
  stories: stories/Tag/Tag.stories.tsx
9
7
  version: "1.0"
10
8
  ---
@@ -13,118 +11,150 @@ version: "1.0"
13
11
 
14
12
  ## Назначение
15
13
 
16
- Тег для отображения выбранного значения параметра (фильтра/категории).
17
- Структура: `<span>` с `Text` контентом + опциональная кнопка редактирования
18
- (`ButtonIcon` с edit-иконкой) + всегда кнопка удаления (`ButtonIcon` с
19
- cross-иконкой). Иконки масштабируются по `size` (16/20/24).
14
+ Тег — компактная плашка с выбранным значением параметра и кнопками управления им: удалением
15
+ и, опционально, редактированием. Типичное место — строка применённых фильтров или список выбранных
16
+ в форме значений, где пользователь должен видеть выбор и уметь его снять.
20
17
 
21
- Используй когда: нужно показать выбранное значение со встроенной возможностью
22
- удалить или редактировать. Для цветной маркировки (categorical labels) —
23
- `TagColor`. Для управления коллекцией тегов — `TagGroup`.
18
+ Используй когда: значение выбрано пользователем и его нужно показать вместе с возможностью убрать
19
+ или изменить.
20
+ Не используй когда: нужна неинтерактивная цветная метка статуса — это `TagColor`, у него нет кнопок,
21
+ но есть `status`. Нужен интерактивный элемент выбора фильтра (открывает выпадающий список,
22
+ хранит выбор) — это семейство `Chip`.
24
23
 
25
24
  ---
26
25
 
27
- ## Файловая структура
26
+ ## Варианты и props
28
27
 
29
- ```text
30
- src/components/Tag/
31
- ├── Tag.tsx # forwardRef<HTMLSpanElement, ITagProps>
32
- ├── types.ts # ITagProps
33
- ├── styles/Tag.module.less
34
- ├── __tests__/
35
- └── index.ts
36
- ```
28
+ ### Обязательные props
37
29
 
38
- ---
30
+ | Prop | Тип | Описание |
31
+ |---|---|---|
32
+ | `id` | `string` | Идентификатор тега. Рендерится атрибутом `id` на корневом `<span>` и уходит аргументом в `onEdit` и `onRemove` — по нему потребитель понимает, какой тег группы изменили. Значение должно быть уникальным в пределах документа |
33
+ | `size` | `EComponentSize` | Размер: SM (высота 20px) / MD (28px) / LG (32px). Задаёт высоту, внутренние отступы, размер текста (`ETextSize.B4` / `B3` / `B2`) и размер иконок (16 / 20 / 24) |
39
34
 
40
- ## Публичный API
35
+ ### Опциональные props
41
36
 
42
- ### `ITagProps`
37
+ | Prop | Тип | По умолчанию | Описание |
38
+ |---|---|---|---|
39
+ | `children` | `React.ReactNode` | — | Контент тега. Рендерится внутри `Text`, длинный текст обрезается многоточием |
40
+ | `disabled` | `boolean` | `false` | Текст переключается на `EFontType.DISABLED`, обе кнопки получают `disabled` — но своё значение в `editButtonProps` / `removeButtonProps` его перекрывает |
41
+ | `onEdit` | `(id: string) => void` | — | Колбэк редактирования. **Управляет наличием кнопки редактирования** — без него кнопка не рендерится |
42
+ | `onRemove` | `(id: string) => void` | — | Колбэк удаления. На наличие кнопки удаления НЕ влияет — она рендерится всегда |
43
+ | `editButtonProps` | `IButtonIconProps` | — | Props кнопки редактирования. Здесь потребитель задаёт `aria-label` |
44
+ | `removeButtonProps` | `IButtonIconProps` | — | Props кнопки удаления. Здесь потребитель задаёт `aria-label` |
43
45
 
44
- ```ts
45
- extends HTMLAttributes<HTMLSpanElement>
46
- ```
46
+ Остальные props — стандартные атрибуты `span` (`ITagProps extends React.HTMLAttributes<HTMLSpanElement>`),
47
+ уходят на корневой элемент через `...restProps`.
47
48
 
48
- | Prop | Тип | Default | Описание |
49
- |---|---|---|---|
50
- | `id` | `string` | — | Уникальный id; передаётся в `onEdit(id)` / `onRemove(id)`. |
51
- | `size` | `EComponentSize` | — | **Обязателен** (нет default'а). Маппится на размер иконки и текста. |
52
- | `disabled?` | `boolean` | — | Disabled-состояние; влияет на цвет текста (`EFontType.DISABLED`) и `disabled` обеих кнопок. |
53
- | `onEdit?` | `(id: string) => void` | — | Если передан — рендерится edit-кнопка. |
54
- | `onRemove?` | `(id: string) => void` | — | Колбэк удаления; **remove-кнопка рендерится всегда**. |
55
- | `editButtonProps?` | `IButtonIconProps` | — | HTML/aria-атрибуты для edit-кнопки (объединяется с `onClick`). |
56
- | `removeButtonProps?` | `IButtonIconProps` | — | HTML/aria-атрибуты для remove-кнопки. |
57
- | `children` | `ReactNode` | — | Содержимое (текст тега). |
58
- | `...HTMLAttributes<HTMLSpanElement>` | — | — | На корневой `<span>`. |
59
-
60
- ### Размер → иконка/текст
61
-
62
- | `size` | Edit/Remove иконка | Text size |
63
- |---|---|---|
64
- | `SM` | `*StrokeSrvIcon16` | `B4` |
65
- | `MD` | `*StrokeSrvIcon20` | `B3` |
66
- | `LG` | `*StrokeSrvIcon24` | `B2` |
49
+ ### Асимметрия кнопок
67
50
 
68
- Иконки — `paletteIndex={5}` (приглушённая палитра).
51
+ Две кнопки живут по разным правилам, и это легко перепутать:
69
52
 
70
- ---
53
+ - кнопка **редактирования** рендерится только при переданном `onEdit`;
54
+ - кнопка **удаления** рендерится всегда, даже без `onRemove` — тогда клик по ней просто ничего
55
+ не делает.
71
56
 
72
- ## Поведение
57
+ ### Порядок обработчиков клика
73
58
 
74
- ### Edit-кнопка
59
+ `onClick` внутри `editButtonProps` / `removeButtonProps` не заменяет `onEdit` / `onRemove`,
60
+ а вызывается **после** него: компонент вынимает `onClick` из этих props и вешает на кнопку
61
+ собственный обработчик, который сначала зовёт колбэк тега, потом пользовательский `onClick`.
75
62
 
76
- - Рендерится **только** если передан `onEdit`.
77
- - `onClick` объединяет `onEdit(id)` + `editButtonProps.onClick(event)`.
63
+ ---
78
64
 
79
- ### Remove-кнопка
65
+ ## Дизайн-токены
80
66
 
81
- - Рендерится **всегда** (даже без `onRemove`).
82
- - `onClick` вызывает `onRemove?.(id)` (опционально) + `removeButtonProps.onClick(event)`.
83
- - Если `onRemove` не передан — кнопка остаётся видимой, но клик ничего не делает (кроме `removeButtonProps.onClick`).
67
+ ```text
68
+ --triplex-next-Tag-Background
69
+ ```
84
70
 
85
- ### Disabled
71
+ Единственный собственный токен — фон плашки. Цвет текста задаёт `Text` через токены типографики
72
+ (`Typography-Primary_Color` / `Typography-Disabled_Color`), цвет иконок — палитра
73
+ `@sberbusiness/icons-next` (`paletteIndex={5}`), кольцо фокуса кнопок — `ButtonIcon`
74
+ (`Button-Icon_Shadow_Focus`).
86
75
 
87
- - `Text type = DISABLED`.
88
- - `ButtonIcon disabled` — обе кнопки.
76
+ Размерные значения (высоты 20/28/32px, отступы, `border-radius: 4px`, `gap: 2px`) заданы литералами
77
+ в `styles/Tag.module.less` — они специфичны для компонента.
89
78
 
90
79
  ---
91
80
 
92
81
  ## Инварианты
93
82
 
94
- - **`forwardRef<HTMLSpanElement>`** — не убирать.
95
- - **`size` обязателен** — нет default'а; передавать всегда.
96
- - **Remove-кнопка всегда в DOM** — даже без `onRemove`. Если нужно скрыть — `display:none` через `removeButtonProps.style` (или собственная обёртка); поведение `Tag` это не поддерживает напрямую.
97
- - **`onEdit` определяет видимость edit-кнопки** — единственный механизм. `editButtonProps` без `onEdit` не отрендерит кнопку.
98
- - **`id` прокидывается ТОЛЬКО в колбэки** (`onEdit`/`onRemove`), не в DOM. Если нужен HTML id — через `restProps`.
99
- - **Иконки `paletteIndex={5}`** — намеренно. Менять только координированно с дизайн-системой.
100
- - **`Text` size маппится через `sizeToTextSizeMap`**, иконки — через `sizeTo{Edit,Remove}IconMap`. Все три должны обновляться синхронно при добавлении нового размера.
101
- - **Корень — `<span>`**, не `<div>` — чтобы тег был inline-friendly.
83
+ - `forwardRef<HTMLSpanElement>` — не убирать. Ref всегда указывает на корневой `<span>`.
84
+ - Корневой элемент — `<span>` (тег инлайновый, встаёт в строку текста и в `TagGroup`).
85
+ Менять на `div` нельзя.
86
+ - `displayName` — `"Tag"`. Не менять.
87
+ - `id` рендерится атрибутом на корневом `<span>` и одновременно служит бизнес-идентификатором
88
+ в `onEdit` / `onRemove`. Prop обязательный, поэтому идентификатор попадает в DOM у всех
89
+ потребителей: значение должно быть уникальным в пределах документа. Если бизнес-идентификатор
90
+ уже используется другим элементом страницы (типовой случай — тег выбранного значения и опция
91
+ списка в `MultiselectField`), тегу нужен свой префикс.
92
+ - Кнопка удаления рендерится безусловно, вне зависимости от `onRemove` — менять на условный
93
+ рендер нельзя без согласования, это изменит вёрстку у потребителей, не передающих `onRemove`.
94
+ - Порядок в DOM: текст → кнопка редактирования → кнопка удаления. Он же задаёт порядок фокуса,
95
+ на него опираются скриншот-тесты.
96
+ - Собственный `onClick` тега на кнопках всегда вызывается раньше пользовательского
97
+ `onClick` из `*ButtonProps`.
98
+ - Компонент не хардкодит текстовые строки: `aria-label` / `title` кнопок задаёт потребитель.
99
+ Библиотека мультиязычная.
100
+ - Экспорты `Tag` и `ITagProps` идут в barrel `src/components/Tag/index.ts` — сохранять.
101
+ - Ширина ограничена контейнером (`max-width: 100%`), текст обрезается многоточием
102
+ (`.content` с `overflow: hidden`, `text-overflow: ellipsis`, `min-width: 0`) — часть
103
+ визуального контракта, покрыта stories `WithOverflow` и `VisualTests`.
102
104
 
103
105
  ---
104
106
 
105
107
  ## Accessibility
106
108
 
107
- - Корневой `<span>` без явной ARIA-роли — потребитель должен задать смысл через `aria-label` или родительский контекст (`TagGroup`).
108
- - Edit/Remove кнопки — `<button>` через `ButtonIcon`; добавлять `aria-label` через `editButtonProps`/`removeButtonProps` (например, `aria-label="Удалить тег Москва"`).
109
- - `disabled` отключает кнопки (`aria-disabled` + `disabled` атрибут от `ButtonIcon`).
110
- - Иконки декоративные — содержание описывается через `aria-label` кнопки.
109
+ - Собственной ARIA-роли у тега нет: корневой `<span>` — контейнер. Роль группы задаёт `TagGroup`
110
+ (`role="group"`).
111
+ - Кнопки — `ButtonIcon` без текстового контента, поэтому **доступное имя обязан задать
112
+ потребитель**: `editButtonProps={{ "aria-label": "..." }}` и
113
+ `removeButtonProps={{ "aria-label": "..." }}`. Без них кнопки остаются безымянными
114
+ для скринридера. Компонент не подставляет умолчание намеренно — строки на конкретном языке
115
+ внутри библиотеки запрещены.
116
+ - Клавиатура: собственных обработчиков у тега нет, работает нативная семантика `<button>`
117
+ (Tab / Enter / Space). Порядок фокуса совпадает с порядком в DOM: сначала редактирование,
118
+ затем удаление.
119
+ - Кольцо фокуса рисует `ButtonIcon` через `:focus-visible`, то есть только при переходе
120
+ с клавиатуры.
121
+ - При `disabled` обе кнопки получают нативный `disabled` и выпадают из порядка фокуса,
122
+ колбэки не вызываются. `disabled` спредится до `editButtonProps` / `removeButtonProps`,
123
+ поэтому собственное значение в них перекрывает состояние тега — в отличие от `onClick`,
124
+ который тег вынимает из props принудительно.
111
125
 
112
126
  ---
113
127
 
114
128
  ## Связанные компоненты
115
129
 
116
- - `TagColor` — категориальные цветные теги (без edit/remove).
117
- - `TagGroup` — контейнер для коллекции тегов (с overflow).
118
- - `ButtonIcon` — кнопки edit/remove.
119
- - `Typography.Text` — текст внутри.
120
- - `@sberbusiness/icons-next` — `EditStrokeSrvIcon{16,20,24}`, `CrossStrokeSrvIcon{16,20,24}`.
130
+ - `TagGroup` — контейнер группы тегов (`role="group"`), задаёт раскладку и переносы;
131
+ собственный `size` передаётся ему отдельно, теги внутри свой `size` получают сами.
132
+ - `TagColor` — визуально похожая плашка, но неинтерактивная: без кнопок, зато со `status`
133
+ (`DEFAULT` / `SUCCESS` / `INFO` / `WARNING` / `ERROR`). Берётся, когда нужна метка состояния,
134
+ а не снимаемое значение.
135
+ - `ButtonIcon` — рендерится внутри как кнопка редактирования и удаления; от него тег получает
136
+ форму, состояние `disabled` и стили фокуса. Все props из `editButtonProps` / `removeButtonProps`
137
+ уходят именно на него.
138
+ - `Text` — рендерит контент тега; тег выбирает `size` (`ETextSize`) и `type` (`EFontType`)
139
+ по своим `size` и `disabled`.
121
140
 
122
141
  ---
123
142
 
124
143
  ## Stories
125
144
 
126
145
  Основные истории: `stories/Tag/Tag.stories.tsx`
127
- Примеры: `stories/Tag/examples/`
146
+ Файлы примеров: `stories/Tag/examples/`
147
+
148
+ | Story | Example file | Что демонстрирует |
149
+ |---|---|---|
150
+ | `Playground` | `Playground.tsx` | Интерактивный контроль `children`, `size`, `disabled` и наличия кнопки редактирования |
151
+ | `Default` | `Default.tsx` | Минимальный тег: контент и кнопка удаления |
152
+ | `Editable` | `Editable.tsx` | Тег с кнопкой редактирования — появляется вместе с `onEdit` |
153
+ | `Sizes` | `Sizes.tsx` | Размеры SM / MD / LG |
154
+ | `Disabled` | `Disabled.tsx` | Отключенное состояние: приглушённый текст и заблокированные кнопки |
155
+ | `WithCustomButtonProps` | `WithCustomButtonProps.tsx` | `editButtonProps` / `removeButtonProps`: `title`, `aria-label` и собственный `onClick` |
156
+ | `WithOverflow` | `WithOverflow.tsx` | Обрезка длинного контента многоточием по ширине контейнера |
157
+ | `VisualTests` | `VisualTests.tsx` | Матрица размеров и состояний плюс кольцо фокуса кнопки — для скриншот-тестов |
128
158
 
129
159
  ---
130
160
 
@@ -132,4 +162,155 @@ extends HTMLAttributes<HTMLSpanElement>
132
162
 
133
163
  | Дата | Изменение |
134
164
  |---|---|
135
- | 2026-04-18 | Создан документ |
165
+ | 2026-08-27 | `id` пробрасывается атрибутом на корневой `<span>` — раньше деструктурировался из props и в DOM не попадал (ломающее изменение). |
166
+ | 2026-08-26 | Создан документ. AI-рефакторинг (`displayName`, снят лишний `useCallback` с render-функций, константы приведены к UPPER_SNAKE_CASE, JSDoc на props), расширены unit-тесты, stories переведены на modern pattern. |
167
+
168
+ ---
169
+
170
+ ## Примеры использования
171
+
172
+ ### Default
173
+
174
+ `stories/Tag/examples/Default.tsx`
175
+
176
+ ```tsx
177
+ import React from "react";
178
+ import { Tag, EComponentSize } from "@sberbusiness/triplex-next";
179
+
180
+ export const Default = () => (
181
+ <Tag id="default-tag" size={EComponentSize.LG} onRemove={() => {}} removeButtonProps={{ "aria-label": "Удалить" }}>
182
+ Selected value
183
+ </Tag>
184
+ );
185
+ ```
186
+
187
+ ### Editable
188
+
189
+ `stories/Tag/examples/Editable.tsx`
190
+
191
+ ```tsx
192
+ import React from "react";
193
+ import { Tag, EComponentSize } from "@sberbusiness/triplex-next";
194
+
195
+ export const Editable = () => (
196
+ <Tag
197
+ id="editable-tag"
198
+ size={EComponentSize.LG}
199
+ onEdit={() => {}}
200
+ onRemove={() => {}}
201
+ editButtonProps={{ "aria-label": "Редактировать" }}
202
+ removeButtonProps={{ "aria-label": "Удалить" }}
203
+ >
204
+ Selected value
205
+ </Tag>
206
+ );
207
+ ```
208
+
209
+ ### Sizes
210
+
211
+ `stories/Tag/examples/Sizes.tsx`
212
+
213
+ ```tsx
214
+ import React from "react";
215
+ import { Tag, EComponentSize } from "@sberbusiness/triplex-next";
216
+
217
+ const SIZES = Object.values(EComponentSize);
218
+
219
+ export const Sizes = () => (
220
+ <div style={{ display: "flex", flexDirection: "column", gap: 16 }}>
221
+ {SIZES.map((size) => (
222
+ <div key={size}>
223
+ <div style={{ marginBottom: 8, fontSize: 16, fontWeight: 700 }}>{size.toUpperCase()}</div>
224
+ <Tag
225
+ id={`${size}-tag`}
226
+ size={size}
227
+ onEdit={() => {}}
228
+ onRemove={() => {}}
229
+ editButtonProps={{ "aria-label": "Редактировать" }}
230
+ removeButtonProps={{ "aria-label": "Удалить" }}
231
+ >
232
+ Selected value
233
+ </Tag>
234
+ </div>
235
+ ))}
236
+ </div>
237
+ );
238
+ ```
239
+
240
+ ### Disabled
241
+
242
+ `stories/Tag/examples/Disabled.tsx`
243
+
244
+ ```tsx
245
+ import React from "react";
246
+ import { Tag, EComponentSize } from "@sberbusiness/triplex-next";
247
+
248
+ export const Disabled = () => (
249
+ <Tag
250
+ id="disabled-tag"
251
+ size={EComponentSize.LG}
252
+ disabled
253
+ onEdit={() => {}}
254
+ onRemove={() => {}}
255
+ editButtonProps={{ "aria-label": "Редактировать" }}
256
+ removeButtonProps={{ "aria-label": "Удалить" }}
257
+ >
258
+ Selected value
259
+ </Tag>
260
+ );
261
+ ```
262
+
263
+ ### WithCustomButtonProps
264
+
265
+ `stories/Tag/examples/WithCustomButtonProps.tsx`
266
+
267
+ ```tsx
268
+ import React from "react";
269
+ import { Tag, EComponentSize } from "@sberbusiness/triplex-next";
270
+
271
+ export const WithCustomButtonProps = () => (
272
+ <Tag
273
+ id="custom-props-tag"
274
+ size={EComponentSize.LG}
275
+ onEdit={() => {}}
276
+ onRemove={() => {}}
277
+ editButtonProps={{
278
+ title: "Редактировать тег",
279
+ "aria-label": "Редактировать тег",
280
+ onClick: () => {},
281
+ }}
282
+ removeButtonProps={{
283
+ title: "Удалить тег",
284
+ "aria-label": "Удалить тег",
285
+ onClick: () => {},
286
+ }}
287
+ >
288
+ Selected value
289
+ </Tag>
290
+ );
291
+ ```
292
+
293
+ ### WithOverflow
294
+
295
+ `stories/Tag/examples/WithOverflow.tsx`
296
+
297
+ ```tsx
298
+ import React from "react";
299
+ import { Tag, EComponentSize } from "@sberbusiness/triplex-next";
300
+
301
+ export const WithOverflow = () => (
302
+ <div style={{ maxWidth: 440 }}>
303
+ <Tag
304
+ id="long-tag"
305
+ size={EComponentSize.LG}
306
+ onEdit={() => {}}
307
+ onRemove={() => {}}
308
+ editButtonProps={{ "aria-label": "Редактировать" }}
309
+ removeButtonProps={{ "aria-label": "Удалить" }}
310
+ >
311
+ Very long tag text that should be truncated with ellipsis
312
+ </Tag>
313
+ </div>
314
+ );
315
+ ```
316
+