@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.
Files changed (140) hide show
  1. package/README.md +124 -0
  2. package/bundle/.gitkeep +0 -0
  3. package/bundle/components/AbstractTree.md +198 -0
  4. package/bundle/components/AccordionBase.md +227 -0
  5. package/bundle/components/Alert.md +317 -0
  6. package/bundle/components/Amount.md +225 -0
  7. package/bundle/components/AmountField.md +289 -0
  8. package/bundle/components/Avatar.md +209 -0
  9. package/bundle/components/Badge.md +277 -0
  10. package/bundle/components/Body.md +167 -0
  11. package/bundle/components/Button.md +230 -0
  12. package/bundle/components/ButtonDropdown.md +159 -0
  13. package/bundle/components/ButtonIcon.md +122 -0
  14. package/bundle/components/Calendar.md +445 -0
  15. package/bundle/components/Card.md +374 -0
  16. package/bundle/components/CarouselExtended.md +250 -0
  17. package/bundle/components/Checkbox.md +126 -0
  18. package/bundle/components/CheckboxTree.md +280 -0
  19. package/bundle/components/CheckboxTreeExtended.md +321 -0
  20. package/bundle/components/CheckboxXGroup.md +68 -0
  21. package/bundle/components/CheckboxYGroup.md +67 -0
  22. package/bundle/components/Chip.md +404 -0
  23. package/bundle/components/ChipGroup.md +179 -0
  24. package/bundle/components/Col.md +221 -0
  25. package/bundle/components/CollapsableTree.md +255 -0
  26. package/bundle/components/Confirm.md +343 -0
  27. package/bundle/components/DateField.md +371 -0
  28. package/bundle/components/DatePickerExtended.md +342 -0
  29. package/bundle/components/DateRange.md +275 -0
  30. package/bundle/components/DesignTokens.md +385 -0
  31. package/bundle/components/Divider.md +196 -0
  32. package/bundle/components/DocumentNumberEdit.md +280 -0
  33. package/bundle/components/Dropdown.md +433 -0
  34. package/bundle/components/Ellipsis.md +208 -0
  35. package/bundle/components/EmptyView.md +99 -0
  36. package/bundle/components/ExpandAnimation.md +281 -0
  37. package/bundle/components/Footer.md +339 -0
  38. package/bundle/components/FormField.md +326 -0
  39. package/bundle/components/FormGroup.md +99 -0
  40. package/bundle/components/Gap.md +113 -0
  41. package/bundle/components/Header.md +280 -0
  42. package/bundle/components/HelpBox.md +154 -0
  43. package/bundle/components/IconWrapper.md +145 -0
  44. package/bundle/components/Island.md +197 -0
  45. package/bundle/components/IslandAccordion.md +230 -0
  46. package/bundle/components/IslandWidget.md +233 -0
  47. package/bundle/components/KeyDownListener.md +157 -0
  48. package/bundle/components/LightBox.md +322 -0
  49. package/bundle/components/Link.md +161 -0
  50. package/bundle/components/List.md +247 -0
  51. package/bundle/components/ListMaster.md +204 -0
  52. package/bundle/components/Loader.md +161 -0
  53. package/bundle/components/LoaderScreen.md +134 -0
  54. package/bundle/components/Marker.md +127 -0
  55. package/bundle/components/MarkerStatus.md +148 -0
  56. package/bundle/components/MediaWidth.md +169 -0
  57. package/bundle/components/MobileView.md +120 -0
  58. package/bundle/components/ModalWindow.md +308 -0
  59. package/bundle/components/MonthYearField.md +168 -0
  60. package/bundle/components/MultiselectField.md +213 -0
  61. package/bundle/components/Notification.md +233 -0
  62. package/bundle/components/NumberField.md +178 -0
  63. package/bundle/components/OrderedList.md +169 -0
  64. package/bundle/components/Overlay.md +237 -0
  65. package/bundle/components/Page.md +246 -0
  66. package/bundle/components/Pagination.md +298 -0
  67. package/bundle/components/Portal.md +119 -0
  68. package/bundle/components/Radio.md +174 -0
  69. package/bundle/components/Row.md +125 -0
  70. package/bundle/components/SMSField.md +160 -0
  71. package/bundle/components/SegmentedControl.md +235 -0
  72. package/bundle/components/SelectExtendedField.md +322 -0
  73. package/bundle/components/SelectField.md +213 -0
  74. package/bundle/components/Skeleton.md +129 -0
  75. package/bundle/components/Slider.md +157 -0
  76. package/bundle/components/SliderExtended.md +210 -0
  77. package/bundle/components/SliderRange.md +160 -0
  78. package/bundle/components/SmallInput.md +101 -0
  79. package/bundle/components/Spoiler.md +121 -0
  80. package/bundle/components/StatusTracker.md +151 -0
  81. package/bundle/components/Step.md +121 -0
  82. package/bundle/components/Stepper.md +163 -0
  83. package/bundle/components/Suggest.md +160 -0
  84. package/bundle/components/SuggestField.md +157 -0
  85. package/bundle/components/SwipeableArea.md +144 -0
  86. package/bundle/components/Table.md +199 -0
  87. package/bundle/components/Tabs.md +152 -0
  88. package/bundle/components/TabsExtended.md +273 -0
  89. package/bundle/components/TabsLine.md +189 -0
  90. package/bundle/components/Tag.md +135 -0
  91. package/bundle/components/TagColor.md +121 -0
  92. package/bundle/components/TagGroup.md +100 -0
  93. package/bundle/components/TextField.md +152 -0
  94. package/bundle/components/TextareaField.md +124 -0
  95. package/bundle/components/ThemeProvider.md +181 -0
  96. package/bundle/components/Tooltip.md +254 -0
  97. package/bundle/components/TopOverlay.md +145 -0
  98. package/bundle/components/TreeView.md +224 -0
  99. package/bundle/components/Triggers.md +121 -0
  100. package/bundle/guides/codestyle.md +205 -0
  101. package/bundle/guides/commits.md +166 -0
  102. package/bundle/guides/context.md +277 -0
  103. package/bundle/guides/stories.md +618 -0
  104. package/bundle/guides/template.md +154 -0
  105. package/bundle/guides/tests.md +301 -0
  106. package/bundle/icons.json +4156 -0
  107. package/bundle/manifest.json +796 -0
  108. package/bundle/release-notes/1.1.0.mdx +26 -0
  109. package/bundle/release-notes/1.1.1.mdx +14 -0
  110. package/bundle/release-notes/1.10.0.mdx +43 -0
  111. package/bundle/release-notes/1.11.0.mdx +19 -0
  112. package/bundle/release-notes/1.12.0.mdx +21 -0
  113. package/bundle/release-notes/1.13.0.mdx +43 -0
  114. package/bundle/release-notes/1.14.0.mdx +29 -0
  115. package/bundle/release-notes/1.15.0.mdx +56 -0
  116. package/bundle/release-notes/1.16.0.mdx +55 -0
  117. package/bundle/release-notes/1.17.0.mdx +15 -0
  118. package/bundle/release-notes/1.18.0.mdx +24 -0
  119. package/bundle/release-notes/1.19.0.mdx +48 -0
  120. package/bundle/release-notes/1.19.1.mdx +9 -0
  121. package/bundle/release-notes/1.2.0.mdx +80 -0
  122. package/bundle/release-notes/1.20.0.mdx +48 -0
  123. package/bundle/release-notes/1.21.0.mdx +66 -0
  124. package/bundle/release-notes/1.22.0.mdx +75 -0
  125. package/bundle/release-notes/1.23.0.mdx +50 -0
  126. package/bundle/release-notes/1.24.0.mdx +59 -0
  127. package/bundle/release-notes/1.25.0.mdx +52 -0
  128. package/bundle/release-notes/1.26.0.mdx +11 -0
  129. package/bundle/release-notes/1.3.0.mdx +21 -0
  130. package/bundle/release-notes/1.4.0.mdx +37 -0
  131. package/bundle/release-notes/1.5.0.mdx +24 -0
  132. package/bundle/release-notes/1.6.0.mdx +37 -0
  133. package/bundle/release-notes/1.6.1.mdx +11 -0
  134. package/bundle/release-notes/1.7.0.mdx +29 -0
  135. package/bundle/release-notes/1.8.0.mdx +38 -0
  136. package/bundle/release-notes/1.9.0.mdx +28 -0
  137. package/dist/index.d.ts +2 -0
  138. package/dist/index.js +324 -0
  139. package/dist/index.js.map +1 -0
  140. 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 | Создан документ |