@sberbusiness/triplex-next-mcp-server 0.24.0-beta1
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.
- package/README.md +124 -0
- package/bundle/.gitkeep +0 -0
- package/bundle/components/AbstractTree.md +198 -0
- package/bundle/components/AccordionBase.md +227 -0
- package/bundle/components/Alert.md +317 -0
- package/bundle/components/Amount.md +225 -0
- package/bundle/components/AmountField.md +289 -0
- package/bundle/components/Avatar.md +209 -0
- package/bundle/components/Badge.md +277 -0
- package/bundle/components/Body.md +167 -0
- package/bundle/components/Button.md +230 -0
- package/bundle/components/ButtonDropdown.md +159 -0
- package/bundle/components/ButtonIcon.md +122 -0
- package/bundle/components/Calendar.md +445 -0
- package/bundle/components/Card.md +374 -0
- package/bundle/components/CarouselExtended.md +250 -0
- package/bundle/components/Checkbox.md +126 -0
- package/bundle/components/CheckboxTree.md +280 -0
- package/bundle/components/CheckboxTreeExtended.md +321 -0
- package/bundle/components/CheckboxXGroup.md +68 -0
- package/bundle/components/CheckboxYGroup.md +67 -0
- package/bundle/components/Chip.md +404 -0
- package/bundle/components/ChipGroup.md +179 -0
- package/bundle/components/Col.md +221 -0
- package/bundle/components/CollapsableTree.md +255 -0
- package/bundle/components/Confirm.md +343 -0
- package/bundle/components/DateField.md +371 -0
- package/bundle/components/DatePickerExtended.md +342 -0
- package/bundle/components/DateRange.md +275 -0
- package/bundle/components/DesignTokens.md +385 -0
- package/bundle/components/Divider.md +196 -0
- package/bundle/components/DocumentNumberEdit.md +280 -0
- package/bundle/components/Dropdown.md +433 -0
- package/bundle/components/Ellipsis.md +208 -0
- package/bundle/components/EmptyView.md +99 -0
- package/bundle/components/ExpandAnimation.md +281 -0
- package/bundle/components/Footer.md +339 -0
- package/bundle/components/FormField.md +326 -0
- package/bundle/components/FormGroup.md +99 -0
- package/bundle/components/Gap.md +113 -0
- package/bundle/components/Header.md +280 -0
- package/bundle/components/HelpBox.md +154 -0
- package/bundle/components/IconWrapper.md +145 -0
- package/bundle/components/Island.md +197 -0
- package/bundle/components/IslandAccordion.md +230 -0
- package/bundle/components/IslandWidget.md +233 -0
- package/bundle/components/KeyDownListener.md +157 -0
- package/bundle/components/LightBox.md +322 -0
- package/bundle/components/Link.md +161 -0
- package/bundle/components/List.md +247 -0
- package/bundle/components/ListMaster.md +204 -0
- package/bundle/components/Loader.md +161 -0
- package/bundle/components/LoaderScreen.md +134 -0
- package/bundle/components/Marker.md +127 -0
- package/bundle/components/MarkerStatus.md +148 -0
- package/bundle/components/MediaWidth.md +169 -0
- package/bundle/components/MobileView.md +120 -0
- package/bundle/components/ModalWindow.md +308 -0
- package/bundle/components/MonthYearField.md +168 -0
- package/bundle/components/MultiselectField.md +213 -0
- package/bundle/components/Notification.md +233 -0
- package/bundle/components/NumberField.md +178 -0
- package/bundle/components/OrderedList.md +169 -0
- package/bundle/components/Overlay.md +237 -0
- package/bundle/components/Page.md +246 -0
- package/bundle/components/Pagination.md +298 -0
- package/bundle/components/Portal.md +119 -0
- package/bundle/components/Radio.md +174 -0
- package/bundle/components/Row.md +125 -0
- package/bundle/components/SMSField.md +160 -0
- package/bundle/components/SegmentedControl.md +235 -0
- package/bundle/components/SelectExtendedField.md +322 -0
- package/bundle/components/SelectField.md +213 -0
- package/bundle/components/Skeleton.md +129 -0
- package/bundle/components/Slider.md +157 -0
- package/bundle/components/SliderExtended.md +210 -0
- package/bundle/components/SliderRange.md +160 -0
- package/bundle/components/SmallInput.md +101 -0
- package/bundle/components/Spoiler.md +121 -0
- package/bundle/components/StatusTracker.md +151 -0
- package/bundle/components/Step.md +121 -0
- package/bundle/components/Stepper.md +163 -0
- package/bundle/components/Suggest.md +160 -0
- package/bundle/components/SuggestField.md +157 -0
- package/bundle/components/SwipeableArea.md +144 -0
- package/bundle/components/Table.md +199 -0
- package/bundle/components/Tabs.md +152 -0
- package/bundle/components/TabsExtended.md +273 -0
- package/bundle/components/TabsLine.md +189 -0
- package/bundle/components/Tag.md +135 -0
- package/bundle/components/TagColor.md +121 -0
- package/bundle/components/TagGroup.md +100 -0
- package/bundle/components/TextField.md +152 -0
- package/bundle/components/TextareaField.md +124 -0
- package/bundle/components/ThemeProvider.md +181 -0
- package/bundle/components/Tooltip.md +254 -0
- package/bundle/components/TopOverlay.md +145 -0
- package/bundle/components/TreeView.md +224 -0
- package/bundle/components/Triggers.md +121 -0
- package/bundle/guides/codestyle.md +205 -0
- package/bundle/guides/commits.md +166 -0
- package/bundle/guides/context.md +277 -0
- package/bundle/guides/stories.md +618 -0
- package/bundle/guides/template.md +154 -0
- package/bundle/guides/tests.md +301 -0
- package/bundle/icons.json +4156 -0
- package/bundle/manifest.json +796 -0
- package/bundle/release-notes/1.1.0.mdx +26 -0
- package/bundle/release-notes/1.1.1.mdx +14 -0
- package/bundle/release-notes/1.10.0.mdx +43 -0
- package/bundle/release-notes/1.11.0.mdx +19 -0
- package/bundle/release-notes/1.12.0.mdx +21 -0
- package/bundle/release-notes/1.13.0.mdx +43 -0
- package/bundle/release-notes/1.14.0.mdx +29 -0
- package/bundle/release-notes/1.15.0.mdx +56 -0
- package/bundle/release-notes/1.16.0.mdx +55 -0
- package/bundle/release-notes/1.17.0.mdx +15 -0
- package/bundle/release-notes/1.18.0.mdx +24 -0
- package/bundle/release-notes/1.19.0.mdx +48 -0
- package/bundle/release-notes/1.19.1.mdx +9 -0
- package/bundle/release-notes/1.2.0.mdx +80 -0
- package/bundle/release-notes/1.20.0.mdx +48 -0
- package/bundle/release-notes/1.21.0.mdx +66 -0
- package/bundle/release-notes/1.22.0.mdx +75 -0
- package/bundle/release-notes/1.23.0.mdx +50 -0
- package/bundle/release-notes/1.24.0.mdx +59 -0
- package/bundle/release-notes/1.25.0.mdx +52 -0
- package/bundle/release-notes/1.26.0.mdx +11 -0
- package/bundle/release-notes/1.3.0.mdx +21 -0
- package/bundle/release-notes/1.4.0.mdx +37 -0
- package/bundle/release-notes/1.5.0.mdx +24 -0
- package/bundle/release-notes/1.6.0.mdx +37 -0
- package/bundle/release-notes/1.6.1.mdx +11 -0
- package/bundle/release-notes/1.7.0.mdx +29 -0
- package/bundle/release-notes/1.8.0.mdx +38 -0
- package/bundle/release-notes/1.9.0.mdx +28 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +324 -0
- package/dist/index.js.map +1 -0
- package/package.json +51 -0
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
---
|
|
2
|
+
component: Card
|
|
3
|
+
category: Layout
|
|
4
|
+
figma-node: ""
|
|
5
|
+
figma-file: ""
|
|
6
|
+
related: [Island, IslandWidget, CardContent, CardMedia]
|
|
7
|
+
tokens:
|
|
8
|
+
- --triplex-next-Card-Static_General_Background
|
|
9
|
+
- --triplex-next-Card-Static_Secondary_Background
|
|
10
|
+
- --triplex-next-Card-Action_General_Background
|
|
11
|
+
- --triplex-next-Card-Action_General_Background_Hover
|
|
12
|
+
- --triplex-next-Card-Action_General_Background_Selected
|
|
13
|
+
- --triplex-next-Card-Action_General_Background_Selected_Hover
|
|
14
|
+
- --triplex-next-Card-Action_Secondary_Background
|
|
15
|
+
- --triplex-next-Card-Action_Secondary_Background_Hover
|
|
16
|
+
- --triplex-next-Card-Action_Secondary_Background_Selected
|
|
17
|
+
- --triplex-next-Card-Action_Secondary_Background_Selected_Hover
|
|
18
|
+
- --triplex-next-Card-Shadow_Default
|
|
19
|
+
- --triplex-next-Card-Shadow_Hover
|
|
20
|
+
- --triplex-next-Card-Shadow_Selected
|
|
21
|
+
- --triplex-next-Card-Shadow_Selected_Hover
|
|
22
|
+
- --triplex-next-Card-Shadow_Focus
|
|
23
|
+
stories: stories/Cards/CardStatic.stories.tsx, stories/Cards/CardAction.stories.tsx
|
|
24
|
+
version: "1.0"
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
# Card
|
|
28
|
+
|
|
29
|
+
## Назначение
|
|
30
|
+
|
|
31
|
+
Семейство карточек: прямоугольный контейнер со скруглением, фоном и
|
|
32
|
+
опциональной медиа-областью. Папка экспортирует **два** независимых
|
|
33
|
+
публичных компонента:
|
|
34
|
+
|
|
35
|
+
- **`CardStatic`** — статическая карточка. Используется как визуальный
|
|
36
|
+
блок-"плитка" контента.
|
|
37
|
+
- **`CardAction`** — интерактивная карточка: `tabIndex=0`, `role="button"`,
|
|
38
|
+
обработка `Space`/`Enter`, состояния hover/selected/focus, controlled/
|
|
39
|
+
uncontrolled выбор.
|
|
40
|
+
|
|
41
|
+
Оба содержат `Content` (с `Header`/`Body`/`Footer`) и `Media` как
|
|
42
|
+
compound-subcomponents.
|
|
43
|
+
|
|
44
|
+
Используй когда:
|
|
45
|
+
- **`CardStatic`** — нужна карточка-контейнер для блока информации (профиль,
|
|
46
|
+
продукт, сводка), на которую не нужно кликать.
|
|
47
|
+
- **`CardAction`** — карточка действует как большая кнопка/выбор: выбор
|
|
48
|
+
варианта, открытие деталей, чекбокс-карточка.
|
|
49
|
+
|
|
50
|
+
Не используй когда: нужен структурный блок страницы с сайдбаром (бери
|
|
51
|
+
`Island`); нужен виджет дашборда со слотами (бери `IslandWidget`).
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Файловая структура
|
|
56
|
+
|
|
57
|
+
```text
|
|
58
|
+
src/components/Card/
|
|
59
|
+
├── CardStatic.tsx # React.FC + compound (Content, Media)
|
|
60
|
+
├── CardAction.tsx # React.Component (class) + compound
|
|
61
|
+
├── enums.ts # ECardContentPaddingSize, ECardRoundingSize, ECardTheme
|
|
62
|
+
├── types.ts # ICardProps, ICardActionProps
|
|
63
|
+
├── utils.ts # mapCardRoundingSizeToCssClass, mapCardThemeToCssClass
|
|
64
|
+
├── index.ts # Barrel: CardStatic + CardAction + enums + types
|
|
65
|
+
├── styles/
|
|
66
|
+
│ ├── Card.module.less # Общие стили .card + rounding + theme + content
|
|
67
|
+
│ └── Action.module.less # .action specific (cursor, shadow/hover/focus)
|
|
68
|
+
├── components/
|
|
69
|
+
│ ├── CardMedia.tsx # Медиа-блок (обычно <div> с background-image)
|
|
70
|
+
│ └── CardContent/
|
|
71
|
+
│ ├── CardContent.tsx # + Body / Header / Footer compound
|
|
72
|
+
│ └── components/
|
|
73
|
+
│ ├── CardContentBody.tsx
|
|
74
|
+
│ ├── CardContentHeader.tsx
|
|
75
|
+
│ └── CardContentFooter.tsx
|
|
76
|
+
└── __tests__/
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Особенности:
|
|
80
|
+
- **Два разных паттерна реализации**: `CardStatic` — функциональный FC,
|
|
81
|
+
`CardAction` — class-component. Обусловлено состоянием focus-source и
|
|
82
|
+
controlled/uncontrolled логикой. Не унифицировать без рефакторинга.
|
|
83
|
+
- **`forwardRef` НЕТ ни на одном** (class + FC). Ref на class приходит
|
|
84
|
+
через внутренний `this.ref` (не форвардится наружу). **Это известный
|
|
85
|
+
debt** — в roadmap AI-Ready.
|
|
86
|
+
- **`CardStatic` и `CardAction`** делят LESS-файл `Card.module.less`
|
|
87
|
+
(общие `.card`, `.roundingMD`, `.general` etc.), а `Action.module.less`
|
|
88
|
+
добавляет только action-специфику.
|
|
89
|
+
- **`CardContent` с compound `Body`/`Header`/`Footer`** — не обязательны,
|
|
90
|
+
но дают стандартные отступы (header: margin-bottom 16px, footer: margin-top 16px).
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## Варианты и props
|
|
95
|
+
|
|
96
|
+
### `ICardProps extends React.HTMLAttributes<HTMLDivElement>` (CardStatic)
|
|
97
|
+
|
|
98
|
+
| Prop | Тип | Default | Описание |
|
|
99
|
+
|---|---|---|---|
|
|
100
|
+
| `roundingSize` | `ECardRoundingSize` (`MD` / `SM`) | `MD` | Скругление: MD=16px, SM=8px. |
|
|
101
|
+
| `theme` | `ECardTheme` (`GENERAL` / `SECONDARY`) | `GENERAL` | Тема фона (Static_General_Background / Static_Secondary_Background). |
|
|
102
|
+
| `children` | `React.ReactNode` | — | Обычно `CardMedia` и/или `CardContent`. |
|
|
103
|
+
| `className` | `string` | — | Доп. класс (`clsx` с базовыми). |
|
|
104
|
+
| `...HTMLDivAttributes` | — | — | Все стандартные. |
|
|
105
|
+
|
|
106
|
+
### `ICardActionProps extends ICardProps` (CardAction)
|
|
107
|
+
|
|
108
|
+
Наследует `roundingSize`, `theme` + добавляет:
|
|
109
|
+
|
|
110
|
+
| Prop | Тип | Default | Описание |
|
|
111
|
+
|---|---|---|---|
|
|
112
|
+
| `selected` | `boolean` | — | Controlled-состояние выбора. Если `undefined` → uncontrolled. |
|
|
113
|
+
| `toggle` | `(selected: boolean) => void` | — | Controlled-хендлер. Вызывается при клике/клавиатуре в controlled-режиме. |
|
|
114
|
+
| `onToggle` | `(selected: boolean) => void` | — | Uncontrolled-хендлер. Вызывается после изменения внутреннего state. |
|
|
115
|
+
|
|
116
|
+
**Controlled определяется один раз в конструкторе:** `isControlled: this.props.selected !== undefined`. Переключать
|
|
117
|
+
`selected` из `undefined` в `false` между рендерами — **не** превратит
|
|
118
|
+
компонент в controlled, флаг остался `false`. Если нужен controlled —
|
|
119
|
+
передай `selected` с самого начала.
|
|
120
|
+
|
|
121
|
+
### `CardContent` / `CardContent.Body/Header/Footer` / `CardMedia`
|
|
122
|
+
|
|
123
|
+
`CardContent`:
|
|
124
|
+
| Prop | Тип | Default | Описание |
|
|
125
|
+
|---|---|---|---|
|
|
126
|
+
| `paddingSize` | `ECardContentPaddingSize` (`MD` / `SM`) | `MD` | Внутренний padding: MD=24px, SM=16px. |
|
|
127
|
+
| `children` | — | — | Обычно `Header`/`Body`/`Footer` или свободный контент. |
|
|
128
|
+
|
|
129
|
+
`CardContent.Body/Header/Footer` и `CardMedia` — тонкие div-обёртки без
|
|
130
|
+
дополнительных props. У `CardContentHeader/Footer` фиксированные отступы
|
|
131
|
+
(`margin-bottom: 16px` / `margin-top: 16px`) относительно Body.
|
|
132
|
+
|
|
133
|
+
### Enums
|
|
134
|
+
|
|
135
|
+
| Enum | Значения | Что делает |
|
|
136
|
+
|---|---|---|
|
|
137
|
+
| `ECardRoundingSize` | `MD = "md"`, `SM = "sm"` | Border-radius: 16px / 8px. |
|
|
138
|
+
| `ECardTheme` | `GENERAL`, `SECONDARY` | Switch между Static_*/Action_* токенами. |
|
|
139
|
+
| `ECardContentPaddingSize` | `MD = "md"`, `SM = "sm"` | Padding CardContent: 24px / 16px. |
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## Ключевые особенности реализации
|
|
144
|
+
|
|
145
|
+
### `CardAction` — class component с EFocusSource
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
public state = {
|
|
149
|
+
focusSource: EFocusSource.NONE,
|
|
150
|
+
isControlled: this.props.selected !== undefined,
|
|
151
|
+
isSelected: !!this.props.selected,
|
|
152
|
+
};
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
`EFocusSource` из `src/enums/EFocusSource` различает KEYBOARD / MOUSE / NONE.
|
|
156
|
+
Только `KEYBOARD`-фокус рендерит `.focusVisible` → видимая focus-рамка.
|
|
157
|
+
Click мышью не показывает рамку (выглядит как "неподсвеченная" кнопка).
|
|
158
|
+
Это соответствует нативному `:focus-visible`, но реализовано вручную для
|
|
159
|
+
поддержки старых браузеров.
|
|
160
|
+
|
|
161
|
+
### `handleKeyDown` — Space и Enter
|
|
162
|
+
|
|
163
|
+
```ts
|
|
164
|
+
if (isKey(event.keyCode, "SPACE")) {
|
|
165
|
+
event.preventDefault(); // важно: без preventDefault страница прокрутится
|
|
166
|
+
this.handleToggle();
|
|
167
|
+
} else if (isKey(event.keyCode, "ENTER")) {
|
|
168
|
+
this.handleToggle(); // без preventDefault — Enter не прокручивает
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Не убирать `event.preventDefault()` для SPACE. Без него страница прокрутится
|
|
173
|
+
на 1 экран при выборе карточки.
|
|
174
|
+
|
|
175
|
+
### `componentDidUpdate` — синхронизация controlled-пропсы с onToggle
|
|
176
|
+
|
|
177
|
+
```ts
|
|
178
|
+
if (selected !== prevProps.selected) {
|
|
179
|
+
onToggle?.(!!selected);
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
**Странный паттерн: `onToggle` вызывается и в controlled-режиме при
|
|
184
|
+
внешнем изменении `selected`.** Это осознанный фичфикс — позволяет
|
|
185
|
+
слушать все изменения единым хендлером. Не удалять (сломает consumer'ов,
|
|
186
|
+
которые делают `onToggle={log}`).
|
|
187
|
+
|
|
188
|
+
### `handleToggle` — две ветки
|
|
189
|
+
|
|
190
|
+
```ts
|
|
191
|
+
if (isControlled) {
|
|
192
|
+
toggle?.(!selected);
|
|
193
|
+
} else {
|
|
194
|
+
this.setState(prev => ({isSelected: !prev.isSelected}), () => onToggle?.(!isSelected));
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Controlled → вызывает `toggle`. Uncontrolled → локальный setState + колбэк
|
|
199
|
+
`onToggle`. **Внимание**: в `onToggle?.(!isSelected)` используется
|
|
200
|
+
**захваченный** `isSelected` из destructure, не актуальный из setState
|
|
201
|
+
callback'а. Это намеренно: даёт значение после переключения.
|
|
202
|
+
|
|
203
|
+
### `isControlled: this.props.selected !== undefined` — фиксируется в initial state
|
|
204
|
+
|
|
205
|
+
Значит, переключение режима на лету не поддерживается. Если нужно —
|
|
206
|
+
передавай `selected={undefined}` с самого начала и потом управляй им.
|
|
207
|
+
В остальном — выбирай один режим.
|
|
208
|
+
|
|
209
|
+
### `CardStatic.Content / .Media` — присваивание свойствам FC
|
|
210
|
+
|
|
211
|
+
```ts
|
|
212
|
+
CardStatic.Content = CardContent;
|
|
213
|
+
CardStatic.Media = CardMedia;
|
|
214
|
+
CardStatic.displayName = "CardStatic";
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
Вместо `Object.assign` как в Badge — прямое присваивание. Эквивалентно.
|
|
218
|
+
Type augmentation через `React.FC<ICardProps> & ICardStaticComposition`.
|
|
219
|
+
TypeScript требует объявленного интерфейса композиции для FC — иначе
|
|
220
|
+
`.Content` не типизируется.
|
|
221
|
+
|
|
222
|
+
### `--radius` / `--padding` CSS-переменные
|
|
223
|
+
|
|
224
|
+
`roundingMD/SM` меняют CSS-переменную `--radius` на root `.card`, которую
|
|
225
|
+
подхватывают и `.card { border-radius: var(--radius) }`, и
|
|
226
|
+
`::before { border-radius: var(--radius) }` в Action. Та же техника для
|
|
227
|
+
`--padding` в `.cardContent`.
|
|
228
|
+
|
|
229
|
+
**`--card-inner-shadow`** в CardAction создаёт внутреннюю тень (для
|
|
230
|
+
selected / focus) через `::before` с `box-shadow` — чтобы не ломать
|
|
231
|
+
`border-radius` + `overflow: hidden` на родителе.
|
|
232
|
+
|
|
233
|
+
### `mapCardThemeToCssClass` / `mapCardRoundingSizeToCssClass`
|
|
234
|
+
|
|
235
|
+
Плоские объекты (не функция `createSizeToClassNameMap`). Простые маппинги
|
|
236
|
+
без TypeScript-валидации полноты. При добавлении нового enum-значения
|
|
237
|
+
нужно обновлять явно — без подсказки от компилятора.
|
|
238
|
+
|
|
239
|
+
### `role="button"` + `tabIndex={0}` на CardAction
|
|
240
|
+
|
|
241
|
+
CardAction — `<div>` с ARIA-ролью "button". Потребитель может переопределить
|
|
242
|
+
через `...attributes` (spread после role), но обычно не нужно. **Не
|
|
243
|
+
оборачивать в `<button>`** — в контенте часто есть другие кнопки/ссылки,
|
|
244
|
+
nested interactive — не валиден.
|
|
245
|
+
|
|
246
|
+
---
|
|
247
|
+
|
|
248
|
+
## Инварианты
|
|
249
|
+
|
|
250
|
+
- **CardStatic — FC, CardAction — class** — не унифицировать без согласования.
|
|
251
|
+
- **CardAction controlled/uncontrolled определяется один раз** — не менять
|
|
252
|
+
паттерн на динамическое определение.
|
|
253
|
+
- **`componentDidUpdate` в CardAction вызывает `onToggle` при изменении
|
|
254
|
+
`selected`** — не удалять, консюмеры полагаются.
|
|
255
|
+
- **`role="button"` + `tabIndex={0}` на CardAction** — не убирать. Это базовая
|
|
256
|
+
a11y.
|
|
257
|
+
- **`event.preventDefault()` для SPACE в `handleKeyDown`** — не удалять
|
|
258
|
+
(прокрутка страницы).
|
|
259
|
+
- **`displayName`** на каждом (CardStatic, CardAction, CardContent, CardMedia,
|
|
260
|
+
CardContentBody/Header/Footer) — для девтулзов.
|
|
261
|
+
- **`data-tx={process.env.npm_package_version}`** на обоих root-обёртках
|
|
262
|
+
(CardStatic и CardAction) — не удалять.
|
|
263
|
+
- **`overflow: hidden` на `.card`** — не убирать. Обеспечивает обрезку
|
|
264
|
+
media-изображения по скруглению.
|
|
265
|
+
- **`::before` с `--card-inner-shadow`** в Action — не схлопывать в
|
|
266
|
+
box-shadow самого блока, иначе `overflow: hidden` скроет тень.
|
|
267
|
+
- **`.cardContentHeader { margin-bottom: 16px }` / `.cardContentFooter {
|
|
268
|
+
margin-top: 16px }`** — фиксированные отступы, не токенизированы. При
|
|
269
|
+
дизайн-ревью могут быть пересмотрены.
|
|
270
|
+
- **Нет `forwardRef`** — известный debt. Не добавлять точечно без
|
|
271
|
+
согласования (поменяет семантику ref для CardAction, где сейчас
|
|
272
|
+
`this.ref` используется в focus-логике).
|
|
273
|
+
|
|
274
|
+
---
|
|
275
|
+
|
|
276
|
+
## Accessibility
|
|
277
|
+
|
|
278
|
+
- **`CardAction`**:
|
|
279
|
+
- `role="button"` + `tabIndex={0}` — валидная интерактивная роль.
|
|
280
|
+
- Клавиатура: `Enter` и `Space` triggering toggle. Space preventDefault'ится
|
|
281
|
+
от скролла.
|
|
282
|
+
- Focus-ring появляется **только** при клавиатурной навигации (EFocusSource)
|
|
283
|
+
— соответствует `:focus-visible`.
|
|
284
|
+
- `aria-pressed` / `aria-selected` — **компонент НЕ добавляет**. Если
|
|
285
|
+
CardAction работает как toggle-button, потребитель должен передать
|
|
286
|
+
`aria-pressed={selected}` (или `aria-selected` для списков). Без этого
|
|
287
|
+
screen reader не знает о состоянии выбора.
|
|
288
|
+
- **`CardStatic`** — семантически нейтральный div. Если используется как
|
|
289
|
+
ссылка, потребитель оборачивает в `<a>` или добавляет `role="link"` +
|
|
290
|
+
`href`-подобное поведение.
|
|
291
|
+
- **Nested interactive**: внутри CardAction могут быть ссылки/кнопки —
|
|
292
|
+
браузеры/screen reader могут повести себя неожиданно (клик по внутренней
|
|
293
|
+
кнопке триггерит и карту). В таких случаях используй `CardStatic` +
|
|
294
|
+
отдельную кнопку, либо `e.stopPropagation()` на вложенных элементах.
|
|
295
|
+
|
|
296
|
+
---
|
|
297
|
+
|
|
298
|
+
## Дизайн-токены
|
|
299
|
+
|
|
300
|
+
**CardStatic:**
|
|
301
|
+
```
|
|
302
|
+
--triplex-next-Card-Static_General_Background # theme=GENERAL
|
|
303
|
+
--triplex-next-Card-Static_Secondary_Background # theme=SECONDARY
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
**CardAction (theme=GENERAL):**
|
|
307
|
+
```
|
|
308
|
+
--triplex-next-Card-Action_General_Background
|
|
309
|
+
--triplex-next-Card-Action_General_Background_Hover
|
|
310
|
+
--triplex-next-Card-Action_General_Background_Selected
|
|
311
|
+
--triplex-next-Card-Action_General_Background_Selected_Hover
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
**CardAction (theme=SECONDARY):**
|
|
315
|
+
```
|
|
316
|
+
--triplex-next-Card-Action_Secondary_Background
|
|
317
|
+
--triplex-next-Card-Action_Secondary_Background_Hover
|
|
318
|
+
--triplex-next-Card-Action_Secondary_Background_Selected
|
|
319
|
+
--triplex-next-Card-Action_Secondary_Background_Selected_Hover
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
**Тени (только CardAction):**
|
|
323
|
+
```
|
|
324
|
+
--triplex-next-Card-Shadow_Default
|
|
325
|
+
--triplex-next-Card-Shadow_Hover
|
|
326
|
+
--triplex-next-Card-Shadow_Selected
|
|
327
|
+
--triplex-next-Card-Shadow_Selected_Hover
|
|
328
|
+
--triplex-next-Card-Shadow_Focus # через --card-inner-shadow
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
**Размеры не токенизированы:** radius (16/8), padding (24/16),
|
|
332
|
+
margin-header/footer (16) — жёстко в LESS. Кандидаты на размерные токены.
|
|
333
|
+
|
|
334
|
+
---
|
|
335
|
+
|
|
336
|
+
## Связанные компоненты
|
|
337
|
+
|
|
338
|
+
- `Island` (`src/components/Island`) — крупный блок с заголовком/футером/
|
|
339
|
+
сайдбаром. Выше Card по иерархии страницы.
|
|
340
|
+
- `IslandWidget` (`src/components/IslandWidget`) — виджет дашборда с
|
|
341
|
+
разметкой под дрегабл-сетку.
|
|
342
|
+
- `CardContent`, `CardMedia` — subcomponents, доступны и как `CardStatic.Content`,
|
|
343
|
+
`CardAction.Content`, `CardStatic.Media`, `CardAction.Media`.
|
|
344
|
+
- `EFocusSource` (`src/enums/EFocusSource`) — используется в Checkbox,
|
|
345
|
+
Radio, CardAction и др. для различения mouse/keyboard-фокуса.
|
|
346
|
+
- `isKey` (`src/utils/keyboard`) — утилита проверки keyCode.
|
|
347
|
+
|
|
348
|
+
---
|
|
349
|
+
|
|
350
|
+
## Stories
|
|
351
|
+
|
|
352
|
+
Две stories-файла (по одному на компонент):
|
|
353
|
+
|
|
354
|
+
`stories/Cards/CardStatic.stories.tsx`
|
|
355
|
+
Примеры: `stories/Cards/examples/CardStatic/`
|
|
356
|
+
|
|
357
|
+
| Story | Example file | Что демонстрирует |
|
|
358
|
+
|---|---|---|
|
|
359
|
+
| `Playground` | `PlaygroundExample.tsx` | Интерактивный контроль `roundingSize`, `theme`, `paddingSize` |
|
|
360
|
+
| `Default` | `DefaultExample.tsx` | Базовое использование с Content |
|
|
361
|
+
| `Themes` | `ThemesExample.tsx` | GENERAL vs SECONDARY |
|
|
362
|
+
| `RoundingSizes` | `RoundingSizesExample.tsx` | MD vs SM скругление |
|
|
363
|
+
| `PaddingSizes` | `PaddingSizesExample.tsx` | MD vs SM padding |
|
|
364
|
+
|
|
365
|
+
`stories/Cards/CardAction.stories.tsx`
|
|
366
|
+
Примеры: `stories/Cards/examples/CardAction/` (тот же набор + интерактив).
|
|
367
|
+
|
|
368
|
+
---
|
|
369
|
+
|
|
370
|
+
## История изменений
|
|
371
|
+
|
|
372
|
+
| Дата | Изменение |
|
|
373
|
+
|---|---|
|
|
374
|
+
| 2026-04-18 | Создан документ |
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
---
|
|
2
|
+
component: CarouselExtended
|
|
3
|
+
category: Navigation
|
|
4
|
+
figma-node: ""
|
|
5
|
+
figma-file: ""
|
|
6
|
+
related: [TabsExtended, SliderExtended]
|
|
7
|
+
tokens: []
|
|
8
|
+
stories: ""
|
|
9
|
+
version: "1.0"
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# CarouselExtended
|
|
13
|
+
|
|
14
|
+
## Назначение
|
|
15
|
+
|
|
16
|
+
Горизонтальная карусель с управляющими кнопками prev/next в виде render-функций.
|
|
17
|
+
Скроллит children по горизонтали через `scrollSmoothHorizontally`
|
|
18
|
+
(animated `scrollLeft` через requestAnimationFrame). Автоматически управляет
|
|
19
|
+
состояниями кнопок: `disabled` на краях, `hidden` если контент помещается
|
|
20
|
+
целиком.
|
|
21
|
+
|
|
22
|
+
"Extended" в имени — маркер того, что внешний вид кнопок не фиксирован:
|
|
23
|
+
потребитель через render-пропы `buttonPrev`/`buttonNext` сам решает, как
|
|
24
|
+
их рендерить, получая состояние (`disabled`, `hidden`, `onClick`).
|
|
25
|
+
|
|
26
|
+
Используй когда: нужен горизонтальный list контента, не помещающегося в
|
|
27
|
+
ширину (табы, карточки, чипы) с кастомными кнопками навигации.
|
|
28
|
+
Не используй когда: нужна пагинация страниц (бери `Pagination`); нужна
|
|
29
|
+
вертикальная прокрутка (компонент только горизонтальный); нужна карусель
|
|
30
|
+
с auto-play / слайд-переходами (компонент не делает слайдов, только скролл).
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Файловая структура
|
|
35
|
+
|
|
36
|
+
```text
|
|
37
|
+
src/components/CarouselExtended/
|
|
38
|
+
├── CarouselExtended.tsx # React.forwardRef, hooks (useState/Effect/Callback/Ref)
|
|
39
|
+
├── index.ts # Barrel
|
|
40
|
+
└── styles/CarouselExtended.module.less # .carouselExtended (flex + скрытый скроллбар)
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Особенности:
|
|
44
|
+
- **Нет stories** — компонент не демонстрируется в Storybook. Это известный
|
|
45
|
+
gap (в ROADMAP.md Storybook-колонка ⬜). Не создавать без общей задачи.
|
|
46
|
+
- **Нет subcomponents / compound'а** — кнопки рендерит потребитель через
|
|
47
|
+
render-пропы.
|
|
48
|
+
- **Нет дизайн-токенов** — компонент структурный, всё визуальное (цвет
|
|
49
|
+
кнопок, размеры) задаёт потребитель.
|
|
50
|
+
- **Использует `lodash` (не `lodash-es`)** — исключение из общего правила.
|
|
51
|
+
Не менять на lodash-es без общего рефакторинга импортов (вероятно
|
|
52
|
+
исторически, до перехода на ESM).
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## Варианты и props
|
|
57
|
+
|
|
58
|
+
### `ICarouselExtendedProps extends React.HTMLAttributes<HTMLDivElement>`
|
|
59
|
+
|
|
60
|
+
**Обязательные:**
|
|
61
|
+
|
|
62
|
+
| Prop | Тип | Описание |
|
|
63
|
+
|---|---|---|
|
|
64
|
+
| `buttonPrev` | `(props: ICarouselExtendedButtonProvideProps) => React.ReactNode` | Render-функция кнопки "Назад". Получает `disabled`, `hidden`, `onClick`. Оборачивать в `React.Fragment` не нужно — возвращается ReactNode. |
|
|
65
|
+
| `buttonNext` | `(props: ICarouselExtendedButtonProvideProps) => React.ReactNode` | Render-функция кнопки "Вперёд". |
|
|
66
|
+
| `stepPrev` | `number` | Сколько пикселей прокрутить влево при клике на prev. Округляется через `Math.floor(-stepPrev)`. |
|
|
67
|
+
| `stepNext` | `number` | Сколько пикселей прокрутить вправо при клике на next. Округляется через `Math.ceil(stepNext)`. |
|
|
68
|
+
|
|
69
|
+
**Опциональные:**
|
|
70
|
+
|
|
71
|
+
| Prop | Тип | Описание |
|
|
72
|
+
|---|---|---|
|
|
73
|
+
| `children` | `React.ReactNode` | Контент карусели (чипы, карточки, любые inline-элементы). |
|
|
74
|
+
| `className`, `style`, `id`, остальные HTML-атрибуты — передаются на **внешний** div-обёртку, НЕ на скроллируемый контейнер. |
|
|
75
|
+
|
|
76
|
+
### `ICarouselExtendedButtonProvideProps extends React.ButtonHTMLAttributes<HTMLButtonElement>`
|
|
77
|
+
|
|
78
|
+
Передаётся в render-функции кнопок:
|
|
79
|
+
|
|
80
|
+
| Поле | Тип | Описание |
|
|
81
|
+
|---|---|---|
|
|
82
|
+
| `disabled` | `boolean` | true, если скролл достиг края (prev → start, next → end). |
|
|
83
|
+
| `hidden` | `boolean` | true, если контент помещается целиком (`scrollWidth === clientWidth`). Обычно потребитель рендерит `null` или применяет `visibility: hidden`. |
|
|
84
|
+
| `onClick` | `() => void` | Обработчик клика — вызывает `scrollSmoothHorizontally`. |
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## Ключевые особенности реализации
|
|
89
|
+
|
|
90
|
+
### `forwardRef` с компонентным разделением refs
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
const carouselRef = useRef<HTMLDivElement | null>(null);
|
|
94
|
+
const setRef = (instance: HTMLDivElement | null) => {
|
|
95
|
+
carouselRef.current = instance;
|
|
96
|
+
if (typeof ref === "function") ref(instance);
|
|
97
|
+
else if (ref) ref.current = instance;
|
|
98
|
+
};
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
`ref`, прокинутый через `forwardRef`, указывает на **скроллируемый внутренний**
|
|
102
|
+
div, НЕ на внешнюю обёртку (на которую идут htmlDivAttributes). Это
|
|
103
|
+
осознанное решение: ref нужен для программного управления скроллом.
|
|
104
|
+
**Не путать с внешней div-обёрткой** — она без ref и, например, `className`
|
|
105
|
+
потребителя идёт на неё.
|
|
106
|
+
|
|
107
|
+
### `checkControls` вычисляет `hidden`, `nextDisabled`, `prevDisabled`
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
hidden: carousel.scrollWidth == carousel.clientWidth,
|
|
111
|
+
nextDisabled: Math.round(carousel.scrollLeft) + carousel.offsetWidth >= carousel.scrollWidth,
|
|
112
|
+
prevDisabled: Math.round(carousel.scrollLeft) <= 0,
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
`Math.round` на `scrollLeft` — защита от subpixel-значений (при zoom или
|
|
116
|
+
DPR ≠ 1 значение может быть дробным, что ломает strict-сравнение).
|
|
117
|
+
`offsetWidth` vs `clientWidth` — намеренно. `offsetWidth` включает границы
|
|
118
|
+
(у компонента их нет → эквивалентно), а `scrollWidth` и `clientWidth` могут
|
|
119
|
+
разойтись на 1px при dense rendering.
|
|
120
|
+
|
|
121
|
+
### `isEqual` предотвращает лишние ререндеры
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
if (isEqual(newState, controlsState) == false) {
|
|
125
|
+
setControlsState(newState);
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Без этой проверки каждый `scroll`-event триггерил бы `setState` → ререндер
|
|
130
|
+
children. Глубокое сравнение из lodash: объект маленький (3 булевых поля),
|
|
131
|
+
но на hot-path.
|
|
132
|
+
|
|
133
|
+
### Слушает и window.resize, и document.scroll
|
|
134
|
+
|
|
135
|
+
```ts
|
|
136
|
+
window.addEventListener("resize", checkControls);
|
|
137
|
+
document.addEventListener("scroll", checkControls);
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
**`document.scroll`** — не очевидное решение. Нужно, потому что при скролле
|
|
141
|
+
самой страницы могут измениться размеры (например, если карусель внутри
|
|
142
|
+
sticky-элемента, его реальная ширина меняется). Не убирать.
|
|
143
|
+
|
|
144
|
+
Плюс `onScroll` на самом carousel-div'е — для скролла внутри карусели.
|
|
145
|
+
|
|
146
|
+
### `scrollSmoothHorizontally` с `requestAnimationFrame`
|
|
147
|
+
|
|
148
|
+
`src/utils/scroll.ts` реализует плавный скролл через рекурсивный
|
|
149
|
+
`requestAnimationFrame` (24 кадра по умолчанию), уменьшая amount каждый
|
|
150
|
+
кадр. **Не использует** `scrollTo({behavior: "smooth"})` — вероятно для
|
|
151
|
+
поддержки старых браузеров (IE, старых Edge) или для единообразия скорости
|
|
152
|
+
анимации.
|
|
153
|
+
|
|
154
|
+
### `useEffect(checkControls, [children, checkControls])`
|
|
155
|
+
|
|
156
|
+
Перевычисляет состояние кнопок, когда меняются children. Без этого при
|
|
157
|
+
динамическом добавлении/удалении карточек кнопки могли бы остаться в
|
|
158
|
+
неправильном состоянии.
|
|
159
|
+
|
|
160
|
+
### `// eslint-disable-next-line react-hooks/refs`
|
|
161
|
+
|
|
162
|
+
Два места с disable — потому что `react-hooks/refs` правило не понимает
|
|
163
|
+
render-пропы (думает, что ref утекает). Не удалять, правило тут ложно
|
|
164
|
+
срабатывает.
|
|
165
|
+
|
|
166
|
+
### Скрытый скроллбар в LESS
|
|
167
|
+
|
|
168
|
+
```less
|
|
169
|
+
scrollbar-width: none; // Firefox
|
|
170
|
+
-ms-overflow-style: none; // IE + Edge
|
|
171
|
+
&::-webkit-scrollbar { display: none; } // Chrome/Safari
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Три вендор-префикса для trimming scrollbar. Не убирать ни один — каждый
|
|
175
|
+
нужен для своего браузера.
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
## Инварианты
|
|
180
|
+
|
|
181
|
+
- **`forwardRef` на скроллируемый внутренний div** (не внешний) — не менять.
|
|
182
|
+
Семантика ref = "скроллируемая область".
|
|
183
|
+
- **`buttonPrev` / `buttonNext` — render-пропы, не элементы** — сохранять
|
|
184
|
+
сигнатуру `(props) => ReactNode`.
|
|
185
|
+
- **`stepPrev` / `stepNext` — обязательные числа** — нет default'а,
|
|
186
|
+
потребитель должен осознанно выбрать величину.
|
|
187
|
+
- **`Math.floor(-stepPrev)` и `Math.ceil(stepNext)`** — асимметричное
|
|
188
|
+
округление для симметричного покрытия при не-целых step'ах. Не менять
|
|
189
|
+
без проверки на эдж-кейсах.
|
|
190
|
+
- **`window.resize` + `document.scroll` + `onScroll`** — три источника
|
|
191
|
+
триггера `checkControls`. Не удалять никакой, каждый покрывает свой
|
|
192
|
+
сценарий.
|
|
193
|
+
- **`isEqual` из lodash** — не удалять, предотвращает ререндеры детей.
|
|
194
|
+
- **Скрытие скроллбара** (три правила) — не удалять, дизайн-контракт.
|
|
195
|
+
- **`htmlDivAttributes` на внешней обёртке, не на скроллируемом div** —
|
|
196
|
+
если поменять, className и event handlers потребителя окажутся на
|
|
197
|
+
скроллируемом контейнере, где могут конфликтовать с onScroll.
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
## Accessibility
|
|
202
|
+
|
|
203
|
+
- **Нет ARIA-ролей по умолчанию** — карусель как контейнер семантически
|
|
204
|
+
нейтральна. Если содержимое — таблы/карточки, они сами декларируют
|
|
205
|
+
роли.
|
|
206
|
+
- **Клавиатурная навигация внутри** — задача содержимого (например, tab'ы
|
|
207
|
+
внутри имеют свою клавиатуру). CarouselExtended не перехватывает arrow
|
|
208
|
+
keys.
|
|
209
|
+
- **Кнопки `hidden`** — когда контент помещается, потребитель решает: скрыть
|
|
210
|
+
через `null` (лучше для a11y — кнопки не в tab-order) или через
|
|
211
|
+
`hidden`/`visibility: hidden`. Hidden HTML-атрибут приходит в render-prop,
|
|
212
|
+
и если потребитель просто спредит его — браузер сам уберёт из tab-order.
|
|
213
|
+
- **Screen reader и скролл** — при использовании в табах/навигации
|
|
214
|
+
потребитель может обернуть в `role="tablist"` и озвучивать позицию
|
|
215
|
+
через `aria-live`. Компонент сам этого не делает.
|
|
216
|
+
- **Focus trap** — компонент НЕ делает trap. Tab из карусели выходит нормально.
|
|
217
|
+
|
|
218
|
+
---
|
|
219
|
+
|
|
220
|
+
## Дизайн-токены
|
|
221
|
+
|
|
222
|
+
Нет — компонент полностью структурный. Цвета/размеры задаёт контент и
|
|
223
|
+
внешние кнопки.
|
|
224
|
+
|
|
225
|
+
---
|
|
226
|
+
|
|
227
|
+
## Связанные компоненты
|
|
228
|
+
|
|
229
|
+
- `TabsExtended` (`src/components/TabsExtended`) — часто использует
|
|
230
|
+
CarouselExtended внутри для скролла tab-row'а при переполнении.
|
|
231
|
+
- `SliderExtended` (`src/components/SliderExtended`) — семейно названный
|
|
232
|
+
компонент, но про другое (range-slider, не карусель).
|
|
233
|
+
- `scrollSmoothHorizontally` (`src/utils/scroll.ts`) — единственная
|
|
234
|
+
внутренняя зависимость, больше нигде не используется.
|
|
235
|
+
|
|
236
|
+
---
|
|
237
|
+
|
|
238
|
+
## Stories
|
|
239
|
+
|
|
240
|
+
**Нет stories.** Это известный пробел (ROADMAP.md: Storybook ⬜). Компонент
|
|
241
|
+
используется внутри TabsExtended и в потребительских приложениях без
|
|
242
|
+
отдельной демонстрации.
|
|
243
|
+
|
|
244
|
+
---
|
|
245
|
+
|
|
246
|
+
## История изменений
|
|
247
|
+
|
|
248
|
+
| Дата | Изменение |
|
|
249
|
+
|---|---|
|
|
250
|
+
| 2026-04-18 | Создан документ |
|