@sberbusiness/triplex-next-mcp-server 1.40.0-beta → 1.41.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,12 +1,12 @@
1
1
  ---
2
2
  component: Chip
3
3
  category: Chips
4
- figma-node: ""
5
- figma-file: ""
6
- related: [ChipGroup, Badge, IconWrapper, SelectField, MultiselectField]
4
+ related: [ChipGroup, ChipIcon, ChipOptions, ChipSelect, ChipMultiselect, ChipSort, ChipSuggest, ChipDatePicker, ChipClearButton, ChipDropdownArrow, Badge, IconWrapper]
7
5
  tokens:
8
- - --triplex-next-Chip-Background_Default
9
- - --triplex-next-Chip-Background_Hover
6
+ - --triplex-next-Chip-Background_Type1_Default
7
+ - --triplex-next-Chip-Background_Type1_Hover
8
+ - --triplex-next-Chip-Background_Type2_Default
9
+ - --triplex-next-Chip-Background_Type2_Hover
10
10
  - --triplex-next-Chip-Background_Active
11
11
  - --triplex-next-Chip-Background_Disabled
12
12
  - --triplex-next-Chip-Background_Selected_Default
@@ -22,7 +22,7 @@ tokens:
22
22
  - --triplex-next-Chip-Color_Selected_Active
23
23
  - --triplex-next-Chip-Color_Selected_Disabled
24
24
  - --triplex-next-Chip-Shadow_Focus
25
- stories: stories/Chips/Chip.stories.tsx (+ 8 других файлов)
25
+ stories: stories/Chips/Chip.stories.tsx
26
26
  version: "1.0"
27
27
  ---
28
28
 
@@ -30,375 +30,371 @@ version: "1.0"
30
30
 
31
31
  ## Назначение
32
32
 
33
- Семейство интерактивных "таблеток" (chips): прямоугольные элементы со
34
- скруглением для выбора, сортировки, фильтрации. Включает **базовый `Chip`**
35
- и **9 вариантов на его основе**. Чипы обычно кладутся внутрь `ChipGroup`
36
- (горизонтальный список с gap'ами и переносом).
37
-
38
- Базовый `Chip` — `<span role="button">`, реагирует на клавиатуру (Space
39
- блокирует скролл), поддерживает selected/disabled, prefix/postfix иконки
40
- и "точку уведомления" через `Badge.Dot`.
41
-
42
- Варианты:
43
- - **`ChipIcon`** — чип только с иконкой (иконка в prefix, пустой span в postfix для симметрии).
44
- - **`ChipMultiselect`** — target-чип для `MultiselectField` с ClearButton / DropdownArrow.
45
- - **`ChipSelect`** (в subfolder) — target-чип для `SelectExtendedField`.
46
- - **`ChipOptions`** — чип с иконкой опций + clear button.
47
- - **`ChipSort`** — чип-иконка для сортировки + selectExtendedField dropdown.
48
- - **`ChipSuggest`** (в subfolder) — чип-input с саджестами.
49
- - **`ChipDatePicker`** (в subfolder) — чип с календарным попапом.
50
- - **`ChipClearButton`** — кнопка "×" для очистки выбранного.
51
- - **`ChipDropdownArrow`** — стрелка dropdown (ротируется при opened).
52
-
53
- Используй когда: нужен фильтр/сортировка/мультивыбор в линейном наборе.
54
- Рекомендуется всегда внутри `ChipGroup`.
55
- Не используй когда: нужна "метка-значок" без интерактива (бери `Badge`/`Tag`);
56
- нужен input поля формы (бери `TextField`/`SelectField`).
33
+ Компактный интерактивный элемент-«чипс»: выполняет действие по нажатию и отображает
34
+ выбранное (`selected`) состояние. Одновременно служит базой для всего семейства Chip*
35
+ — поверх него построены `ChipIcon`, `ChipOptions`, а также target-элементы
36
+ `ChipSelect`, `ChipMultiselect`, `ChipSuggest`, `ChipDatePicker`.
57
37
 
58
- ---
59
-
60
- ## Файловая структура
38
+ Используй когда: нужен переключаемый фильтр, тег или компактная кнопка-действие
39
+ в ряду однотипных элементов.
40
+ Не используй когда: нужна обычная кнопка формы (возьми `Button` — у него нативная
41
+ клавиатурная активация и семантика `<button>`) или нужен выбор значения из списка
42
+ (возьми `ChipSelect` / `ChipMultiselect`).
61
43
 
62
- ```text
63
- src/components/Chip/
64
- ├── Chip.tsx # Базовый, React.forwardRef
65
- ├── ChipClearButton.tsx # ButtonIcon-обёртка с крестиком по size
66
- ├── ChipDropdownArrow.tsx # Стрелка, rotate при rotated
67
- ├── ChipIcon.tsx # Chip с prefix=children и postfix=<span />
68
- ├── ChipMultiselect.tsx # MultiselectField target
69
- ├── ChipOptions.tsx # Chip + OptionsIcon + ChipClearButton
70
- ├── ChipSort.tsx # SelectExtendedField target с SortIcon
71
- ├── ChipDatePicker/
72
- │ ├── ChipDatePicker.tsx
73
- │ ├── ChipDatePickerTarget.tsx
74
- │ └── index.ts
75
- ├── ChipSelect/
76
- │ ├── ChipSelect.tsx
77
- │ ├── ChipSelectTarget.tsx
78
- │ └── index.ts
79
- ├── ChipSuggest/
80
- │ ├── ChipSuggest.tsx
81
- │ ├── ChipSuggestTarget.tsx
82
- │ ├── ChipSuggestDropdown.tsx
83
- │ ├── desktop/
84
- │ ├── types.ts
85
- │ └── index.ts
86
- ├── index.ts # Barrel всех вариантов
87
- ├── styles/
88
- │ ├── Chip.module.less # Root чипа
89
- │ ├── ChipClearButton.module.less
90
- │ ├── ChipDropdownArrow.module.less
91
- │ ├── ChipOptions.module.less
92
- │ └── ChipSuggest.module.less
93
- └── __tests__/
94
- ```
95
-
96
- Особенности:
97
- - **Нет общего enum/типов файла** — каждый чип декларирует свой интерфейс
98
- через `extends IChipProps` / `Omit<IChipProps, ...>` / подобное.
99
- - **`ChipGroup` — отдельный компонент** (в `src/components/ChipGroup`). Большинство
100
- чипов получают `styles.chipGroupItem` класс, чтобы быть валидным flex-item
101
- внутри ChipGroup.
102
- - **`ChipIcon` делает `postfix={<span />}`** — трюк для симметричных
103
- паддингов, так как Chip меняет padding при наличии postfix.
44
+ Рекомендуется всегда располагать `Chip` внутри `ChipGroup` — он задаёт отступы,
45
+ перенос строк и однострочный режим со скроллом.
104
46
 
105
47
  ---
106
48
 
107
49
  ## Варианты и props
108
50
 
109
- ### Base `Chip` — `IChipProps extends Omit<HTMLAttributes<HTMLSpanElement>, "prefix">, DataAttributes`
110
-
111
- | Prop | Тип | Default | Описание |
112
- |---|---|---|---|
113
- | `size` | `EComponentSize` | `MD` | SM=28px, MD=40px, LG=56px. |
114
- | `disabled` | `boolean` | — | Визуальный disabled + `tabIndex=-1`. |
115
- | `selected` | `boolean` | — | "Активен" (галочка-выбор). |
116
- | `prefix` | `ReactNode` | — | Контент слева (обычно иконка). Оборачивается в `IconWrapper`. |
117
- | `postfix` | `ReactNode` | — | Контент справа. |
118
- | `showNotificationIcon` | `boolean` | — | Отрисовать `Badge.Dot` в правом-верхнем углу. |
119
- | `children` | `ReactNode` | — | Основной контент. |
120
- | `...HTMLSpanAttributes` | — | — | Все кроме `prefix` (HTML-атрибут переопределён). |
51
+ ### Типы (`EChipType`)
121
52
 
122
- ### `ChipIcon` — `IChipIconProps extends Omit<IChipProps, "prefix" | "postfix">`
53
+ | Значение | Описание |
54
+ |---|---|
55
+ | `TYPE_1` | Основной фон (по умолчанию) |
56
+ | `TYPE_2` | Альтернативный фон — для случаев, когда `TYPE_1` сливается с подложкой |
123
57
 
124
- Принимает `children` (иконку), кладёт в `prefix`, форсирует `postfix=<span/>`
125
- для симметричных паддингов.
58
+ Тип влияет **только** на фон в обычном состоянии и на hover. В состояниях `selected`,
59
+ `active` и `disabled` фон задаётся общими токенами и от типа не зависит.
126
60
 
127
- ### `ChipClearButton` — `IChipClearButtonProps extends Omit<IButtonIconProps, "children">`
61
+ ### Размеры (`EComponentSize`)
128
62
 
129
- Кнопка `<button>` с крестиком. Иконка выбирается по `size`:
130
- - SM → `CrossStrokeSrvIcon16`
131
- - MD → `CrossStrokeSrvIcon20`
132
- - LG → `CrossStrokeSrvIcon24`
63
+ | Значение | Высота | Padding | Border-radius | Font-size |
64
+ |---|---|---|---|---|
65
+ | `SM` | 28px | 0 12px | 6px | 12px |
66
+ | `MD` | 40px | 0 12px | 8px | 14px |
67
+ | `LG` | 56px | 0 16px | 10px | 16px |
133
68
 
134
- ### `ChipDropdownArrow`
69
+ `size` пробрасывается в `Badge.Dot` при `showNotificationIcon` — размер значка
70
+ уведомлений всегда согласован с размером чипса.
135
71
 
136
- | Prop | Тип | Описание |
137
- |---|---|---|
138
- | `rotated` | `boolean` | `true` → rotate(180deg). |
139
- | `size` | `EComponentSize` | Выбирает иконку Caret 16/20/24. |
72
+ ### Опциональные props
140
73
 
141
- ### `ChipMultiselect` — `extends Omit<IMultiselectFieldProps, "renderTarget">`
74
+ | Prop | Тип | По умолчанию | Описание |
75
+ |---|---|---|---|
76
+ | `type` | `EChipType` | `TYPE_1` | Визуальный тип |
77
+ | `size` | `EComponentSize` | `MD` | Размер |
78
+ | `selected` | `boolean` | `false` | Выбранное состояние (визуальное; компонент неуправляемый — состояние держит потребитель) |
79
+ | `disabled` | `boolean` | `false` | Визуально блокирует и убирает из tab-порядка (см. ограничения ниже) |
80
+ | `prefix` | `React.ReactNode` | — | Контент перед основным, обычно иконка слева; оборачивается в `IconWrapper` |
81
+ | `postfix` | `React.ReactNode` | — | Контент после основного, обычно иконка справа; оборачивается в `IconWrapper` |
82
+ | `showNotificationIcon` | `boolean` | `false` | Значок новых уведомлений (`Badge.Dot`) в правом верхнем углу |
83
+ | `children` | `React.ReactNode` | — | Основной контент; обрезается по `max-width` с многоточием |
84
+ | `...HTMLSpanAttributes` | — | — | Все атрибуты `<span>` кроме `prefix`, включая `onClick`, `aria-expanded`, `data-*` |
85
+
86
+ ### Ограничения
87
+
88
+ - **`prefix` / `postfix`: отсутствие значения передавай как `undefined`, не как `null` или `false`.**
89
+ Классы `.withPrefix` / `.withPostfix` (обнуляют боковой `padding`) выставляются по
90
+ `!== undefined`, а сама иконка рендерится по truthiness. Поэтому
91
+ `postfix={condition && <Icon/>}` при `condition === false` даст чипс с нулевым
92
+ правым паддингом и без иконки. Пиши `postfix={condition ? <Icon/> : undefined}`.
93
+ Внутри библиотеки все обёртки передают либо JSX, либо `<span />`-заглушку —
94
+ проблема проявляется только у внешнего потребителя.
95
+ - **`disabled` — не нативный атрибут.** Корневой элемент — `<span>`, поэтому блокировка
96
+ чисто визуальная: `pointer-events: none` в LESS плюс `tabIndex={-1}`. Реальные
97
+ пользовательские клики и клавиатура до обработчиков не доходят, но программно
98
+ диспатченные события (`fireEvent.click` в тестах, синтетический `dispatchEvent`)
99
+ вызовут `onClick` / `onKeyDown`. Не полагайся на `disabled` как на защиту в логике —
100
+ проверяй флаг в самом обработчике, если это критично.
101
+ - **`selected` и `disabled` совместимы** — есть отдельные токены
102
+ `--triplex-next-Chip-*_Selected_Disabled`.
103
+ - Ширина ограничена: `min-width: 40px`, `max-width: 264px`. Длинный `children`
104
+ обрезается многоточием, а не переносится.
142
105
 
143
- Обёртка над `MultiselectField`, где `renderTarget` рендерит `Chip` с
144
- `ClearButton` (если selected) или `DropdownArrow` (если нет).
106
+ ---
145
107
 
146
- | Доп. prop | Тип | Описание |
147
- |---|---|---|
148
- | `clearSelected` | `() => void` | Коллбек на крестик. |
149
- | `disabled` | `boolean` | Форвардится в Chip и MultiselectField. |
150
- | `selected` | `boolean` | Включает "выбранное" состояние + ClearButton. |
151
- | `label` | `ReactNode` | Текст при пустом выборе. |
152
- | `displayedValue` | `ReactNode` | Текст при выборе (fallback на label). |
108
+ ## Дизайн-токены
153
109
 
154
- ### `ChipOptions` — `extends Omit<IChipProps, "prefix" | "postfix">`
110
+ Паттерн: `--triplex-next-Chip-{Property}_{Modifier}_{State}`
155
111
 
156
- Иконка `OptionsStrokeSrvIcon24` слева. Справа ClearButton только если
157
- selected.
112
+ ```text
113
+ --triplex-next-Chip-Background_Type1_Default
114
+ --triplex-next-Chip-Background_Type1_Hover
115
+ --triplex-next-Chip-Background_Type2_Default
116
+ --triplex-next-Chip-Background_Type2_Hover
117
+ --triplex-next-Chip-Background_Active
118
+ --triplex-next-Chip-Background_Disabled
119
+ --triplex-next-Chip-Background_Selected_Default
120
+ --triplex-next-Chip-Background_Selected_Hover
121
+ --triplex-next-Chip-Background_Selected_Active
122
+ --triplex-next-Chip-Background_Selected_Disabled
123
+ --triplex-next-Chip-Color_Default
124
+ --triplex-next-Chip-Color_Hover
125
+ --triplex-next-Chip-Color_Active
126
+ --triplex-next-Chip-Color_Disabled
127
+ --triplex-next-Chip-Color_Selected_Default
128
+ --triplex-next-Chip-Color_Selected_Hover
129
+ --triplex-next-Chip-Color_Selected_Active
130
+ --triplex-next-Chip-Color_Selected_Disabled
131
+ --triplex-next-Chip-Shadow_Focus
132
+ ```
158
133
 
159
- | Доп. prop | Тип | Описание |
160
- |---|---|---|
161
- | `clearSelected` | `() => void` | Коллбек крестика. |
134
+ Токены `_Type1_` / `_Type2_` существуют только для `Background` в состояниях
135
+ Default и Hover. Цвет текста (`Color_*`) от типа не зависит.
162
136
 
163
- ### `ChipSort` — SelectExtendedField target с `SortStrokeSrvIcon24`
137
+ ---
164
138
 
165
- `selected = Boolean(value) && !isEqual(defaultValue, value)` — в отличие от
166
- простого `value`: если текущее значение равно дефолту, чип не подсвечивается
167
- как изменённый. **Это ключевое поведение Sort-чипа** — дефолтная сортировка
168
- не должна выглядеть как "активный фильтр".
139
+ ## Инварианты
169
140
 
170
- ### `ChipSelect`, `ChipSuggest`, `ChipDatePicker` — см. соответствующие
171
- файлы в subfolder (компонентно большие). Смотри их `<Name>Target.tsx` для
172
- того, как они рендерят базовый Chip.
141
+ - **`forwardRef`** — обязателен, `ref` указывает на корневой `<span>`. Не убирать.
142
+ - **Корневой элемент — `<span role="button">`, а не нативный `<button>`.** Это осознанное
143
+ решение: в `postfix` живут вложенные интерактивные элементы (`ChipClearButton`,
144
+ `ChipDropdownArrow`), а `<button>` внутри `<button>` — невалидный HTML. Смена корневого
145
+ тега сломает всё семейство Chip*.
146
+ - **`role` и `tabIndex` выставляются до `{...restProps}`**, то есть потребитель может
147
+ их переопределить через props. Это часть текущего контракта — менять порядок осознанно.
148
+ (`ref` при `forwardRef` в `restProps` не попадает, на него порядок не влияет.)
149
+ - **Класс `.chipGroupItem`** выставляется всегда, наравне с `.chip`. Объявлен
150
+ в `styles/Chip.module.less` и служит общим маркером «элемент верхнего уровня
151
+ в ряду чипсов»: его проставляют себе все члены семейства — `Chip`, `ChipSort`,
152
+ `ChipMultiselect`, `ChipSelect`, `ChipDatePicker`, `ChipSuggest`. Причём
153
+ `.chipGroupItem` не всегда совпадает с `.chip`: в `ChipSelect` / `ChipMultiselect`
154
+ `.chip` лежит внутри элемента с `.chipGroupItem`. Раскладка `ChipGroup` на этот
155
+ класс не опирается — в её стилях он не встречается. Не удалять и не переименовывать.
156
+ - **`EChipType` значения** (`type_1`, `type_2`) — публичное API, не переименовывать.
157
+ - **`EComponentSize`** — общий enum из `src/enums/EComponentSize`, локального аналога не заводить.
158
+ - **Селектор `[aria-expanded="true"]`** в LESS даёт active-стиль. На него опираются
159
+ chip-триггеры дропдаунов — не убирать вместе с рефакторингом стилей.
160
+ - **Barrel-экспорты** `src/components/Chip/index.ts` — `Chip`, `IChipProps`, `EChipType`
161
+ и всё семейство. Сохранять.
162
+ - **Story ids** `chips-chip--*` завязаны на baseline-скриншоты в `__screenshots__/` —
163
+ переименование story требует синхронного обновления baseline.
173
164
 
174
165
  ---
175
166
 
176
- ## Ключевые особенности реализации
167
+ ## Accessibility
177
168
 
178
- ### Space preventDefault в Chip
169
+ - Роль `role="button"` на `<span>`; в tab-порядке через `tabIndex={0}`, при `disabled` —
170
+ `tabIndex={-1}`.
171
+ - **Клавиатурная активация не реализована внутри компонента.** `Chip` перехватывает
172
+ `keydown` только чтобы вызвать `preventDefault()` на пробеле (код `Space`) — это гасит
173
+ прокрутку страницы, поскольку чипс фокусируем. Синтетический `click` по Enter/Space
174
+ компонент не генерирует (у `<span role="button">` нет нативного поведения кнопки),
175
+ внешний `onKeyDown` вызывается всегда и после `preventDefault`. Если чипс должен
176
+ срабатывать с клавиатуры, потребитель обрабатывает Enter/Space сам через `onKeyDown`.
177
+ Обёртки семейства (`ChipSelect`, `ChipSuggest`, `ChipDatePicker`) делают это на своём уровне.
178
+ - Фокус-стиль через `:focus-visible` (`--triplex-next-Chip-Shadow_Focus`) — виден только
179
+ при клавиатурной навигации. Нативная обводка на корневом элементе намеренно отключена
180
+ (`outline: none` в LESS) в пользу `box-shadow` — убирать правило нельзя, иначе фокус
181
+ отрисуется дважды.
182
+ - **Выбранное состояние не объявляется ассистивным технологиям автоматически.** `selected`
183
+ меняет только визуал. Для роли `button` передавай `aria-pressed` через `...rest`;
184
+ в списке-подобных сценариях используй `ChipSelect` / `ChipMultiselect`.
185
+ - **`aria-expanded`** потребитель передаёт сам, когда чипс работает триггером дропдауна —
186
+ атрибут дополнительно активирует active-стиль через CSS.
187
+ - Чипс без текстового `children` (только `prefix`/`postfix`, как в `ChipIcon`) требует
188
+ `aria-label` от потребителя — библиотека мультиязычная, текст не хардкодится.
179
189
 
180
- ```ts
181
- if (isKey(event.code, "SPACE")) {
182
- event.preventDefault();
183
- }
184
- onKeyDown?.(event);
185
- ```
190
+ ---
186
191
 
187
- Блокирует скролл страницы. Не добавляет toggle-логики — это задача
188
- потребителя. Потребители обычно через свой `onKeyDown` обрабатывают ENTER/SPACE
189
- для активации.
192
+ ## Связанные компоненты
190
193
 
191
- ### `role="button"` + `tabIndex={disabled ? -1 : 0}`
194
+ - `ChipGroup` (`src/components/ChipGroup`) — контейнер-раскладка для чипсов;
195
+ рекомендуемая обёртка, задаёт `size` и режим `oneLine`
196
+ - `ChipIcon` — тонкая обёртка: `children` уходит в `prefix`, `postfix` заполняется
197
+ пустым `<span />` ради симметричных отступов
198
+ - `ChipOptions` — чипс с иконкой опций и кнопкой сброса выбора (`clearSelected`)
199
+ - `ChipClearButton` — кнопка сброса для `postfix`; `forwardRef` на `HTMLButtonElement`,
200
+ props расширяют `Omit<IButtonIconProps, "children">` (то есть доступен весь API
201
+ `ButtonIcon`, включая `onClick` и `aria-label`) плюс собственный `size?: EComponentSize`,
202
+ выбирающий размер иконки-крестика
203
+ - `ChipDropdownArrow` — стрелка дропдауна для `postfix`; объявлена как `React.FC`
204
+ (не `forwardRef`), собственных обработчиков не имеет. Props: **обязательный**
205
+ `rotated: boolean` (даёт класс `.rotated`) и `size?: EComponentSize`
206
+ - `ChipSelect`, `ChipMultiselect`, `ChipSuggest`, `ChipDatePicker`, `ChipSort` —
207
+ составные компоненты, использующие `Chip` как target-элемент дропдауна
208
+ - `Badge` (`Badge.Dot`) — значок уведомлений при `showNotificationIcon`
209
+ - `IconWrapper` — обёртка `prefix` / `postfix`, добавляет классы `hoverable` / `disabled`,
210
+ управляющие цветом иконки при взаимодействии
192
211
 
193
- Chip — span с ARIA-ролью button. Disabled → вне tab-order. Переопределение
194
- `role` через `{...rest}` — возможно (например, ChipMultiselect передаёт
195
- `role="listbox"`).
212
+ ---
196
213
 
197
- ### Notification dot = Badge.Dot
214
+ ## Stories
198
215
 
199
- ```tsx
200
- {showNotificationIcon && <Badge.Dot size={size} className={styles.notificationIcon} />}
201
- ```
216
+ Основные истории: `stories/Chips/Chip.stories.tsx`
217
+ Файлы примеров: `stories/Chips/examples/Chip/`
202
218
 
203
- Тот же `Badge.Dot` из семейства Badge. Size — тот же. Позиция абсолютная,
204
- настраивается в LESS по размерам (SM=3px/3px, MD=4px/4px, LG=6px/6px).
219
+ | Story | Example file | Что демонстрирует |
220
+ |---|---|---|
221
+ | `Playground` | `PlaygroundExample.tsx` | Интерактивный контроль `type`, `size`, `disabled`, `showNotificationIcon` |
222
+ | `Default` | `DefaultExample.tsx` | Базовое использование: переключение `selected` по клику |
223
+ | `Types` | `TypesExample.tsx` | `TYPE_1` и `TYPE_2` |
224
+ | `Sizes` | `SizesExample.tsx` | Размеры SM / MD / LG |
225
+ | `States` | `StatesExample.tsx` | Состояния `selected` и `disabled` |
226
+ | `WithPrefixAndPostfix` | `WithPrefixAndPostfixExample.tsx` | Иконки слева и справа от контента |
227
+ | `WithNotificationIcon` | `WithNotificationIconExample.tsx` | Значок уведомлений во всех трёх размерах |
228
+ | `VisualTests` | `DefaultExample.tsx` | Скриншот-регрессия, `play` доводит чипс до состояния фокуса |
205
229
 
206
- ### `prefix`/`postfix` через `IconWrapper`
230
+ ---
207
231
 
208
- ```tsx
209
- <IconWrapper className={styles.prefix} disabled={Boolean(disabled)}>
210
- {prefix}
211
- </IconWrapper>
212
- ```
232
+ ## История изменений
213
233
 
214
- `IconWrapper` управляет цветом иконки через `disabled`. Если внутри prefix
215
- SVG с `paletteIndex` — подстраивается под текущую тему.
234
+ | Дата | Изменение |
235
+ |---|---|
236
+ | 2026-07-29 | Создан документ (TRI-26, AI-Ready Phase 1) |
237
+ | 2026-07-29 | AI-рефакторинг (TRI-26): codestyle-чистка `Chip.tsx` (константы-маппинги в UPPER_SNAKE_CASE, эквивалентные упрощения в `clsx`, JSDoc на `children` и `handleKeyDown`), unit-тесты расширены с 5 до 22 кейсов; публичный API, DOM и визуал не изменены |
238
+ | 2026-07-29 | Правки по ревью PR #487: уточнена атрибуция класса `.chipGroupItem` (его проставляет семейство Chip*, а не раскладка `ChipGroup`) и описание API `ChipClearButton` / `ChipDropdownArrow` |
216
239
 
217
- ### ChipIcon и `postfix={<span />}` — симметричные паддинги
240
+ ---
218
241
 
219
- ```tsx
220
- <Chip {...rest} prefix={children} postfix={<span />} ref={ref} />
221
- ```
242
+ ## Примеры использования
222
243
 
223
- Пустой span чтобы `styles.withPostfix` класс применился. Без этого padding-right
224
- был бы по обычному правилу, и чип выглядел бы асимметрично. **Hack, но
225
- намеренный.**
244
+ ### Default
226
245
 
227
- ### ChipMultiselect — role="listbox" на чипе
246
+ `stories/Chips/examples/Chip/DefaultExample.tsx`
228
247
 
229
248
  ```tsx
230
- <Chip role="listbox" aria-expanded={opened} ...>
249
+ import React, { useState } from "react";
250
+ import { Chip } from "@sberbusiness/triplex-next";
251
+
252
+ export const DefaultExample = () => {
253
+ const [selected, setSelected] = useState(false);
254
+ return (
255
+ <Chip selected={selected} onClick={() => setSelected((s) => !s)}>
256
+ Value
257
+ </Chip>
258
+ );
259
+ };
231
260
  ```
232
261
 
233
- Нестандартно — обычно listbox это UL. Это решение MultiselectField:
234
- chip-таргет действует как listbox button. Screen reader-ы разного качества
235
- по-разному интерпретируют. Если нужна более строгая семантика — потребителю
236
- надо делать свой renderTarget.
262
+ ### Types
237
263
 
238
- ### ChipSort — `isEqual` с defaultValue
264
+ `stories/Chips/examples/Chip/TypesExample.tsx`
239
265
 
240
- ```ts
241
- const selected = Boolean(value) && !isEqual(defaultValue, value);
266
+ ```tsx
267
+ import React, { useState } from "react";
268
+ import { Chip, EChipType } from "@sberbusiness/triplex-next";
269
+
270
+ type TypeItemProps = {
271
+ type: EChipType;
272
+ label: string;
273
+ };
274
+
275
+ const TypeItem = ({ type, label }: TypeItemProps) => {
276
+ const [selected, setSelected] = useState(false);
277
+
278
+ const handleClick = () => setSelected((prevSelected) => !prevSelected);
279
+
280
+ return (
281
+ <div>
282
+ <div style={{ marginBottom: 8, fontSize: 16, fontWeight: 700 }}>{label}</div>
283
+ <Chip type={type} selected={selected} onClick={handleClick}>
284
+ Value
285
+ </Chip>
286
+ </div>
287
+ );
288
+ };
289
+
290
+ const typeOptions = Object.values(EChipType).map((type) => ({
291
+ value: type,
292
+ label: type.toUpperCase(),
293
+ }));
294
+
295
+ export const TypesExample = () => (
296
+ <div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
297
+ {typeOptions.map(({ value, label }) => (
298
+ <TypeItem key={value} type={value} label={label} />
299
+ ))}
300
+ </div>
301
+ );
242
302
  ```
243
303
 
244
- Использует `lodash` (не `-es`). Не менять без общего рефакторинга импортов.
245
- Deep equal нужен, потому что options — объекты (value, label).
304
+ ### Sizes
246
305
 
247
- ### `aria-expanded` → `.chip[aria-expanded="true"]` стили
306
+ `stories/Chips/examples/Chip/SizesExample.tsx`
248
307
 
249
- ```less
250
- &:active,
251
- &[aria-expanded="true"] {
252
- background: var(--triplex-next-Chip-Background_Active);
253
- }
308
+ ```tsx
309
+ import React, { useState } from "react";
310
+ import { Chip, EComponentSize } from "@sberbusiness/triplex-next";
311
+
312
+ type SizeItemProps = {
313
+ size: EComponentSize;
314
+ label: string;
315
+ };
316
+
317
+ const SizeItem = ({ size, label }: SizeItemProps) => {
318
+ const [selected, setSelected] = useState(false);
319
+
320
+ const handleClick = () => setSelected((prevSelected) => !prevSelected);
321
+
322
+ return (
323
+ <div>
324
+ <div style={{ marginBottom: 8, fontSize: 16, fontWeight: 700 }}>{label}</div>
325
+ <Chip size={size} selected={selected} onClick={handleClick}>
326
+ Value
327
+ </Chip>
328
+ </div>
329
+ );
330
+ };
331
+
332
+ const sizeOptions = Object.values(EComponentSize).map((size) => ({
333
+ value: size,
334
+ label: size.toUpperCase(),
335
+ }));
336
+
337
+ export const SizesExample = () => (
338
+ <div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
339
+ {sizeOptions.map(({ value, label }) => (
340
+ <SizeItem key={value} size={value} label={label} />
341
+ ))}
342
+ </div>
343
+ );
254
344
  ```
255
345
 
256
- CSS-селектор на атрибут. Когда dropdown раскрыт, чип выглядит "нажатым" без
257
- дополнительного className. Умный приём.
346
+ ### States
258
347
 
259
- ### `styles.chipGroupItem` — маркер для ChipGroup
348
+ `stories/Chips/examples/Chip/StatesExample.tsx`
260
349
 
261
- Почти все варианты получают этот класс:
262
- ```less
263
- .chipGroupItem { display: inline-block; }
350
+ ```tsx
351
+ import React from "react";
352
+ import { Chip } from "@sberbusiness/triplex-next";
353
+
354
+ export const StatesExample = () => (
355
+ <div style={{ display: "flex", gap: 12 }}>
356
+ <Chip selected>Selected</Chip>
357
+ <Chip disabled>Disabled</Chip>
358
+ </div>
359
+ );
264
360
  ```
265
361
 
266
- ChipGroup рендерит flex-контейнер и ожидает inline-block детей. **Не
267
- удалять класс** — иначе чипы сломают layout в ChipGroup.
362
+ ### WithPrefixAndPostfix
268
363
 
269
- ### `uniqueId()` в ChipSort для `aria-controls`
364
+ `stories/Chips/examples/Chip/WithPrefixAndPostfixExample.tsx`
270
365
 
271
- ```ts
272
- const instanceId = useRef(uniqueId());
366
+ ```tsx
367
+ import React from "react";
368
+ import { SortStrokeSrvIcon20 } from "@sberbusiness/icons-next";
369
+ import { Chip } from "@sberbusiness/triplex-next";
370
+
371
+ export const WithPrefixAndPostfixExample = () => (
372
+ <Chip prefix={<SortStrokeSrvIcon20 paletteIndex={5} />} postfix={<SortStrokeSrvIcon20 paletteIndex={5} />}>
373
+ Value
374
+ </Chip>
375
+ );
273
376
  ```
274
377
 
275
- Связывает ChipIcon (role=combobox) и dropdown. Если через useRef(uniqueId())
276
- — значение стабильно между рендерами, но каждый инстанс получит свой id.
277
-
278
- ---
279
-
280
- ## Инварианты
281
-
282
- - **`Chip` — forwardRef**, не удалять.
283
- - **`role="button"` и `tabIndex` на Chip** — не менять семантику без
284
- согласования.
285
- - **`Space preventDefault`** в `handleKeyDown` — не удалять (скролл
286
- страницы).
287
- - **`Omit<HTMLAttributes, "prefix">`** — нужно, т.к. `prefix` — HTML
288
- устаревший атрибут (RDFa).
289
- - **`IconWrapper` для prefix/postfix** — не замена на div. `IconWrapper`
290
- управляет цветом иконок по disabled.
291
- - **`styles.chipGroupItem`** — на каждом root-элементе вариантов чипов.
292
- Ожидается ChipGroup'ом.
293
- - **`ChipIcon` → `postfix={<span />}`** — не удалять span. Нужен для
294
- симметричных паддингов.
295
- - **`ChipSort` — `!isEqual(defaultValue, value)` для selected** — не
296
- упрощать до `Boolean(value)`. Это UX-правило (дефолт не "активный фильтр").
297
- - **`Badge.Dot` для notification icon** — не заменять на произвольный
298
- элемент, размер синхронизирован с size чипа.
299
- - **Базовые токены `--triplex-next-Chip-*`** — не дублировать для вариантов.
300
- Варианты используют одни и те же токены (цвета).
301
- - **Варианты (ChipSelect/ChipSuggest/ChipDatePicker) обёртки над
302
- SelectExtendedField/SuggestField/подобными** — не инвертировать зависимость
303
- (не Chip оборачивает Field, а наоборот).
304
- - **`lodash` (не `-es`) в ChipSort** — исторически, не менять точечно.
305
-
306
- ---
307
-
308
- ## Accessibility
309
-
310
- - **`role="button"`** на Chip — работает с screen reader'ом как кнопка.
311
- - **`tabIndex` управляется через disabled** — disabled чип вне tab-order.
312
- - **`aria-expanded`** на Chip в ChipMultiselect / ChipSort — для dropdown
313
- состояния. Screen reader озвучит "раскрыто"/"свёрнуто".
314
- - **`aria-controls`** в ChipSort связывает чип с dropdown через
315
- `instanceId`.
316
- - **Клавиатура**:
317
- - Space — preventDefault (против скролла), потребитель обрабатывает
318
- активацию.
319
- - Enter — bubble up, потребитель вешает handler через `onKeyDown`.
320
- - ChipMultiselect/ChipSort — свои `handleKeyDown` обрабатывают Enter/Space
321
- для toggle opened.
322
- - **Notification icon `Badge.Dot`** — чисто визуальный, без ARIA. Если
323
- означает "есть новые" — потребитель должен добавить `aria-label` на
324
- Chip (например, "Фильтр, есть новые").
325
- - **Nested interactive**: ClearButton внутри Chip — кнопка в "кнопке".
326
- Решается `event.stopPropagation()` на ClearButton, чтобы клик на крестик
327
- не триггерил родительский toggle.
328
-
329
- ---
330
-
331
- ## Дизайн-токены
378
+ ### WithNotificationIcon
332
379
 
333
- **Базовые (разделяемые всеми вариантами):**
334
- ```
335
- --triplex-next-Chip-Background_Default
336
- --triplex-next-Chip-Background_Hover
337
- --triplex-next-Chip-Background_Active
338
- --triplex-next-Chip-Background_Disabled
339
- --triplex-next-Chip-Color_Default
340
- --triplex-next-Chip-Color_Hover
341
- --triplex-next-Chip-Color_Active
342
- --triplex-next-Chip-Color_Disabled
343
- --triplex-next-Chip-Shadow_Focus # focus-visible
344
- ```
380
+ `stories/Chips/examples/Chip/WithNotificationIconExample.tsx`
345
381
 
346
- **Selected (включённое состояние):**
347
- ```
348
- --triplex-next-Chip-Background_Selected_Default
349
- --triplex-next-Chip-Background_Selected_Hover
350
- --triplex-next-Chip-Background_Selected_Active
351
- --triplex-next-Chip-Background_Selected_Disabled
352
- --triplex-next-Chip-Color_Selected_Default
353
- --triplex-next-Chip-Color_Selected_Hover
354
- --triplex-next-Chip-Color_Selected_Active
355
- --triplex-next-Chip-Color_Selected_Disabled
382
+ ```tsx
383
+ import React from "react";
384
+ import { Chip, EComponentSize } from "@sberbusiness/triplex-next";
385
+
386
+ export const WithNotificationIconExample = () => (
387
+ <div style={{ display: "flex", gap: 12 }}>
388
+ <Chip size={EComponentSize.SM} showNotificationIcon>
389
+ SM
390
+ </Chip>
391
+ <Chip size={EComponentSize.MD} showNotificationIcon>
392
+ MD
393
+ </Chip>
394
+ <Chip size={EComponentSize.LG} showNotificationIcon>
395
+ LG
396
+ </Chip>
397
+ </div>
398
+ );
356
399
  ```
357
400
 
358
- Размеры (высоты, radius, padding, font-size) — hardcoded в LESS по size-
359
- классам, не токенизированы.
360
-
361
- ---
362
-
363
- ## Связанные компоненты
364
-
365
- - `ChipGroup` (`src/components/ChipGroup`) — контейнер для чипов с
366
- горизонтальным скроллом/переносом.
367
- - `Badge` (`src/components/Badge`) — notification dot (`Badge.Dot`) —
368
- та же реализация.
369
- - `IconWrapper` (`src/components/IconWrapper`) — обёртка для prefix/postfix
370
- иконок.
371
- - `ButtonIcon` (`src/components/Button`) — основа для `ChipClearButton`.
372
- - `MultiselectField`, `SelectExtendedField`, `SuggestField` — варианты
373
- чипов обёртки над этими field'ами.
374
- - `EComponentSize`, `createSizeToClassNameMap` — общий паттерн size'ов.
375
-
376
- ---
377
-
378
- ## Stories
379
-
380
- Отдельная story-файл для каждого варианта:
381
-
382
- | File | Variant |
383
- |---|---|
384
- | `stories/Chips/Chip.stories.tsx` | Base Chip |
385
- | `stories/Chips/ChipIcon.stories.tsx` | ChipIcon |
386
- | `stories/Chips/ChipGroup.stories.tsx` | ChipGroup (отдельный компонент) |
387
- | `stories/Chips/ChipMultiselect.stories.tsx` | ChipMultiselect |
388
- | `stories/Chips/ChipOptions.stories.tsx` | ChipOptions |
389
- | `stories/Chips/ChipSelect.stories.tsx` | ChipSelect |
390
- | `stories/Chips/ChipSort.stories.tsx` | ChipSort |
391
- | `stories/Chips/ChipSuggest.stories.tsx` | ChipSuggest |
392
- | `stories/Chips/ChipDatePicker.stories.tsx` | ChipDatePicker |
393
-
394
- Примеры в `stories/Chips/examples/<Name>/`. Typical: `Default`, `Playground`,
395
- `Sizes`, специфичные для варианта (`States`, `WithNotificationIcon`,
396
- `WithPrefixAndPostfix` для base Chip).
397
-
398
- ---
399
-
400
- ## История изменений
401
-
402
- | Дата | Изменение |
403
- |---|---|
404
- | 2026-04-18 | Создан документ |