@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,342 @@
1
+ ---
2
+ component: DatePickerExtended
3
+ category: Internal
4
+ figma-node: ""
5
+ figma-file: ""
6
+ related: [DateField, DateRange, MonthYearField, Calendar, Dropdown, FocusTrap]
7
+ tokens: []
8
+ stories: ""
9
+ version: "1.0"
10
+ ---
11
+
12
+ # DatePickerExtended
13
+
14
+ ## Назначение
15
+
16
+ Внутренняя "база" для всех полей выбора даты: объединяет `Dropdown`
17
+ (с desktop + mobile веткой), `Calendar` (внутри dropdown'а) и
18
+ `FocusTrap` (клавиатурная навигация). Управляет открытием/закрытием
19
+ dropdown'а (controlled/uncontrolled внутри себя — state), ловит
20
+ `Esc` для закрытия, отличает "мышиное" взаимодействие от "клавиатурного"
21
+ (флаг `mouseUsedRef`) для настройки `FocusTrap`.
22
+
23
+ Публично импортируется потребителями вроде `DateField`, `DateRange`,
24
+ `MonthYearField`, но **render-props-интерфейс** (`renderTarget`,
25
+ `renderDropdownHeaderTarget`) делает его сложным для прямого
26
+ использования в приложениях — обычно его оборачивают.
27
+
28
+ Используй когда: строишь новый компонент выбора даты со своим target'ом
29
+ и своей валидацией — DatePickerExtended даёт dropdown + calendar +
30
+ focus management бесплатно.
31
+ Не используй как замену `DateField`/`DateRange`/`MonthYearField` —
32
+ бери эти готовые обёртки.
33
+
34
+ ---
35
+
36
+ ## Файловая структура
37
+
38
+ ```text
39
+ src/components/DatePickerExtended/
40
+ ├── DatePickerExtended.tsx # forwardRef, root
41
+ ├── DatePickerExtendedContext.tsx # Context { dropdownOpen, mouseUsedRef, setDropdownOpen }
42
+ ├── DatePickerExtendedDropdown.tsx # forwardRef, обёртка Dropdown + FocusTrap + mobile ветка
43
+ └── styles/DatePickerExtended.module.less # Только .datePickerExtendedMobileDropdown { max-height: 100vh }
44
+ ```
45
+
46
+ Особенности:
47
+ - **Нет `index.ts`** — импортируется как `import { DatePickerExtended } from ".../DatePickerExtended/DatePickerExtended"`.
48
+ Это намеренно — компонент считается "полупубличным" (используется
49
+ внутри библиотеки и потребителями при необходимости, но без красивого
50
+ barrel-a).
51
+ - **Нет `__tests__`** — покрывается через тесты `DateField`.
52
+ - **Нет собственных stories** — только через обёртки.
53
+ - **Почти нет LESS** — один класс для мобильного max-height.
54
+
55
+ ---
56
+
57
+ ## Варианты и props
58
+
59
+ ### `IDatePickerExtendedProps`
60
+
61
+ ```ts
62
+ extends Omit<React.HTMLAttributes<HTMLDivElement>, "onChange">,
63
+ Pick<IDropdownProps, "alignment">,
64
+ ICalendarProps
65
+ ```
66
+
67
+ **Рендер-пропсы (обязательные):**
68
+
69
+ | Prop | Тип | Описание |
70
+ |---|---|---|
71
+ | `renderTarget` | `() => React.ReactNode` | Target (поле ввода) на десктопе. |
72
+ | `renderDropdownHeaderTarget` | `() => React.ReactNode` | Target в заголовке dropdown'а в мобильном режиме (обычно тот же input, но в другом контейнере). |
73
+
74
+ **Dropdown:**
75
+
76
+ | Prop | Тип | Default | Описание |
77
+ |---|---|---|---|
78
+ | `alignment` | `EDropdownAlignment` | `LEFT` | Выравнивание dropdown'а относительно target'а. |
79
+ | `focusTrapProps` | `FocusTrapProps` | — | Прокидывается в `FocusTrap` (`focus-trap-react`). |
80
+ | `onDropdownOpen` | `() => void` | — | Callback при открытии. |
81
+ | `onDropdownClose` | `() => void` | — | Callback при закрытии. |
82
+
83
+ **Calendar (все пропсы `ICalendarProps` форвардятся):** `pickType`,
84
+ `format`, `defaultViewDate`, `pickedDate`, `limitRange`, `markedDays`,
85
+ `disabledDays`, `reversedPick`, `dayHtmlAttributes`, `monthHtmlAttributes`,
86
+ `yearHtmlAttributes`, `prevButtonProps`, `nextButtonProps`, `viewButtonProps`,
87
+ `todayButtonProps`, `onDateChange`, `onPageChange`, `onViewChange`.
88
+
89
+ **HTMLDivAttributes:** все, кроме `onChange` (конфликт с Calendar).
90
+
91
+ ### `DatePickerExtendedContext`
92
+
93
+ ```ts
94
+ interface IDatePickerExtendedContext {
95
+ dropdownOpen: boolean;
96
+ mouseUsedRef: React.MutableRefObject<boolean>;
97
+ setDropdownOpen: (open: boolean) => void;
98
+ }
99
+ ```
100
+
101
+ Передаётся в `renderTarget` через контекст (target живёт в другой ветке
102
+ дерева). `DateFieldTarget` и аналоги используют `useContext` для
103
+ управления dropdown'ом из input'а.
104
+
105
+ ### `DatePickerExtendedDropdown` — внутренний compound
106
+
107
+ `IDatePickerExtendedDropdownProps extends IDropdownProps` +
108
+ `renderCalendar`, `renderHeaderTarget`, `focusTrapProps`.
109
+ Не экспортируется публично.
110
+
111
+ ---
112
+
113
+ ## Ключевые особенности реализации
114
+
115
+ ### `mouseUsedRef` — отличение мышь vs клавиатура
116
+
117
+ ```ts
118
+ const mouseUsedRef = useRef(false);
119
+
120
+ const handleMouseDown = (event) => {
121
+ mouseUsedRef.current = true;
122
+ onMouseDown?.(event);
123
+ };
124
+ ```
125
+
126
+ Сбрасывается при закрытии dropdown'а (в `useEffect`, ветка else).
127
+ Используется в `FocusTrap`:
128
+
129
+ ```tsx
130
+ <FocusTrap
131
+ focusTrapOptions={{
132
+ initialFocus: !mouseUsedRef.current && undefined,
133
+ returnFocusOnDeactivate: !mouseUsedRef.current,
134
+ }}
135
+ >
136
+ ```
137
+
138
+ **Зачем:** если пользователь кликнул по полю мышью → не ставить
139
+ программный initialFocus внутрь dropdown'а (пользователь не хочет,
140
+ чтобы фокус "прыгал") и не возвращать фокус на target при закрытии.
141
+ Если клавиатурой (Enter/Space на input) → focus-trap работает, по Esc
142
+ фокус вернётся на input.
143
+
144
+ **Это UX-тонкость**, нельзя просто удалить `mouseUsedRef` — пострадает
145
+ клавиатурная доступность.
146
+
147
+ ### Outside mousedown → закрытие dropdown'а
148
+
149
+ ```ts
150
+ useEffect(() => {
151
+ const handleOutsideMouseDown = (event: MouseEvent) => {
152
+ if (
153
+ !containerRef.current?.contains(event.target as Node) &&
154
+ !dropdownRef.current?.contains(event.target as Node)
155
+ ) {
156
+ setDropdownOpen(false);
157
+ }
158
+ };
159
+ if (dropdownOpen) {
160
+ document.addEventListener("mousedown", handleOutsideMouseDown);
161
+ return () => document.removeEventListener("mousedown", handleOutsideMouseDown);
162
+ } else {
163
+ mouseUsedRef.current = false;
164
+ }
165
+ }, [dropdownOpen]);
166
+ ```
167
+
168
+ **Проверяет два контейнера** — корневой (`containerRef`, target) и
169
+ `dropdownRef` (сам dropdown). Если клик не в них обоих — закрыть.
170
+ `dropdownRef` нужен потому что Dropdown рендерится через Portal и
171
+ не является DOM-потомком `containerRef`.
172
+
173
+ **`mouseUsedRef` сбрасывается в else-ветке** — при каждом закрытии
174
+ флаг становится `false`, чтобы следующее открытие корректно решало,
175
+ как вести себя с focus trap'ом.
176
+
177
+ ### Esc → закрыть dropdown (локальный onKeyDown)
178
+
179
+ ```ts
180
+ const handleKeyDown = (event) => {
181
+ if (isKey(key, "ESCAPE")) {
182
+ if (dropdownOpen) setDropdownOpen(false);
183
+ }
184
+ onKeyDown?.(event);
185
+ };
186
+ ```
187
+
188
+ **Esc обрабатывается на root div**, не на document. Нужно, чтобы Esc
189
+ внутри input'а (фокус в target) → закрыть dropdown. Не фильтруется
190
+ `event.defaultPrevented` — если потребитель уже обработал Esc, он
191
+ всё равно закроет dropdown. Это компромисс.
192
+
193
+ ### `renderCalendar(adaptiveMode)` — две ветки
194
+
195
+ ```tsx
196
+ const renderMobileContent = () => (
197
+ <>
198
+ <DropdownMobileHeader controlButtons={<DropdownMobileClose onClick={...} />}>
199
+ {renderHeaderTarget()}
200
+ </DropdownMobileHeader>
201
+ <DropdownMobileBody>{renderCalendar(true)}</DropdownMobileBody>
202
+ </>
203
+ );
204
+ // desktop:
205
+ <FocusTrap ...>
206
+ <div role="presentation">{renderCalendar(false)}</div>
207
+ </FocusTrap>
208
+ ```
209
+
210
+ **`adaptiveMode=true`** — календарь на мобильном (полноэкранный,
211
+ другие размеры и поведение).
212
+ **`adaptiveMode=false`** — десктопный компактный календарь в dropdown'е.
213
+
214
+ Calendar сам смотрит на `adaptiveMode` и меняет поведение.
215
+
216
+ ### `onDateChange` → закрыть dropdown + вызвать внешний
217
+
218
+ ```ts
219
+ const handleChangeDate = (date: moment.Moment) => {
220
+ setDropdownOpen(false);
221
+ onDateChange(date);
222
+ };
223
+ ```
224
+
225
+ Выбор даты → dropdown закрывается автоматически. Это жёсткое
226
+ поведение, потребитель не может отключить. Если нужно "мультидневный
227
+ выбор" — Calendar не подходит, нужен другой компонент.
228
+
229
+ ### `FocusTrap` с `clickOutsideDeactivates`
230
+
231
+ ```ts
232
+ focusTrapOptions={{
233
+ clickOutsideDeactivates: true,
234
+ fallbackFocus: targetRef.current!,
235
+ ...
236
+ }}
237
+ ```
238
+
239
+ - `clickOutsideDeactivates` — клик вне → FocusTrap отпускает фокус,
240
+ но outside-handler всё равно закрывает dropdown. Это **защита**:
241
+ FocusTrap не борется с outside-handler'ом.
242
+ - `fallbackFocus: targetRef.current!` — если FocusTrap не может найти
243
+ focusable элемент внутри, поставить фокус на target (non-null
244
+ assertion обязателен потому что при первой активации targetRef
245
+ уже mounted).
246
+
247
+ ### `role="dialog" aria-modal="true"` на Dropdown
248
+
249
+ Семантика модального диалога для содержимого (календарь). Screen reader
250
+ объявит как модал. **Jарко** (не опционально), `...rest` идёт в
251
+ Dropdown после `role`/`aria-modal`, но `role` и `aria-modal` не
252
+ подлежат перебиванию снаружи потребителя (IDatePickerExtendedProps не
253
+ принимает role).
254
+
255
+ ### `data-tx={process.env.npm_package_version}` — есть
256
+
257
+ Стандарт библиотеки. Не удалять.
258
+
259
+ ### `setRef` — double-duty ref
260
+
261
+ ```ts
262
+ const setRef = (node: HTMLDivElement | null) => {
263
+ containerRef.current = node;
264
+ if (typeof ref === "function") ref(node);
265
+ else if (ref) ref.current = node;
266
+ };
267
+ ```
268
+
269
+ Один DOM-узел = target для Dropdown (`targetRef`) + forwarded ref.
270
+
271
+ ---
272
+
273
+ ## Инварианты
274
+
275
+ - **`forwardRef`** на root — обязателен (нужен для тулов).
276
+ - **`mouseUsedRef`** — не удалять. Управляет UX focus-trap'а.
277
+ - **Outside-handler проверяет оба ref'а** (containerRef + dropdownRef) —
278
+ не сводить к одному.
279
+ - **`mouseUsedRef.current = false` в else-ветке useEffect** — сбросить
280
+ флаг при каждом закрытии, иначе следующий цикл не получит корректный
281
+ initialFocus.
282
+ - **`role="dialog" aria-modal="true"`** на Dropdown — обязательно.
283
+ - **`clickOutsideDeactivates: true`** в FocusTrap — иначе outside-click
284
+ не закрывает dropdown (FocusTrap борется).
285
+ - **`setDropdownOpen(false)` в `handleChangeDate`** — выбор даты
286
+ закрывает dropdown. Неопциональное поведение.
287
+ - **Esc → закрытие независимо от defaultPrevented** — осознанное
288
+ решение. Не фильтровать.
289
+ - **Render-props `renderTarget` и `renderDropdownHeaderTarget`** —
290
+ единственный способ вставить target. Не принимать children.
291
+
292
+ ---
293
+
294
+ ## Accessibility
295
+
296
+ - **`FocusTrap`** (`focus-trap-react`) — клавиатурный фокус заперт
297
+ внутри dropdown'а, Tab ходит по кнопкам календаря.
298
+ - **`role="dialog" aria-modal="true"`** на Dropdown — семантика модала.
299
+ - **Esc** — закрывает dropdown.
300
+ - **`initialFocus`/`returnFocusOnDeactivate`** — условно, только для
301
+ клавиатурного открытия. При мышином открытии фокус не трогается
302
+ (UX-паттерн).
303
+ - **`<div role="presentation">`** вокруг календаря — убирает лишнюю
304
+ семантику. Сам Calendar имеет свою ARIA.
305
+ - **Mobile** — собственная разметка с `DropdownMobileHeader`/`Body`,
306
+ включая кнопку закрытия (`DropdownMobileClose`).
307
+
308
+ ---
309
+
310
+ ## Дизайн-токены
311
+
312
+ **Нет собственных.** Dropdown и Calendar — источники всех визуальных
313
+ токенов.
314
+
315
+ ---
316
+
317
+ ## Связанные компоненты
318
+
319
+ - `Dropdown` (`src/components/Dropdown`) — контейнер (Portal + positioning).
320
+ - `Calendar` (`src/components/Calendar`) — собственно выбор даты.
321
+ - `DropdownMobile*` (`src/components/Dropdown/mobile`) — мобильные
322
+ подкомпоненты (Header/Body/Close).
323
+ - `FocusTrap` (`focus-trap-react`) — внешний пакет для захвата фокуса.
324
+ - `DateField` (`src/components/DateField`) — основной потребитель.
325
+ - `DateRange` (`src/components/DateRange`) — потребитель для диапазона.
326
+ - `MonthYearField` (`src/components/MonthYearField`) — потребитель
327
+ для месяца/года.
328
+
329
+ ---
330
+
331
+ ## Stories
332
+
333
+ **Нет собственных stories.** Документируется косвенно через
334
+ `DateField`/`DateRange`/`MonthYearField`.
335
+
336
+ ---
337
+
338
+ ## История изменений
339
+
340
+ | Дата | Изменение |
341
+ |---|---|
342
+ | 2026-04-18 | Создан документ |
@@ -0,0 +1,275 @@
1
+ ---
2
+ component: DateRange
3
+ category: Form
4
+ figma-node: ""
5
+ figma-file: ""
6
+ related: [DateField, DatePickerExtended, Calendar, MonthYearField]
7
+ tokens: []
8
+ stories: stories/DateRange/DateRange.stories.tsx
9
+ version: "1.0"
10
+ ---
11
+
12
+ # DateRange
13
+
14
+ ## Назначение
15
+
16
+ Layout-контейнер для выбора **диапазона дат** (от … до) с опциональными
17
+ кнопками "сдвинуть диапазон назад/вперёд" на фиксированную величину
18
+ (день/неделя/месяц/квартал/год). Сам **не рендерит пикеры** — принимает
19
+ их через render-props (`renderPickerFrom`, `renderPickerTo`,
20
+ `renderButtonBack`, `renderButtonForward`), потребитель обычно
21
+ передаёт два `DateField`.
22
+
23
+ Автоматически **валидирует пересечение**: если пользователь поставил
24
+ `from > to`, компонент очищает `to` (или наоборот очищает `from`, если
25
+ пользователь ставит `to < from`).
26
+
27
+ Используй когда: нужен диапазон дат в форме (например, фильтр периода).
28
+ Не используй когда: нужна **одна** дата — бери `DateField`; нужен выбор
29
+ одного **периода** (месяц/квартал) — `MonthYearField`.
30
+
31
+ ---
32
+
33
+ ## Файловая структура
34
+
35
+ ```text
36
+ src/components/DateRange/
37
+ ├── DateRange.tsx # FC, НЕ forwardRef
38
+ ├── enums.ts # EDateRangeShiftUnit (day/week/month/quarter/year)
39
+ ├── index.ts # Barrel
40
+ ├── styles/DateRange.module.less # .dateRange flex + .separator 20x20 + .dateRangeButton z-index: 1
41
+ └── __tests__/DateRange.test.tsx
42
+ ```
43
+
44
+ Особенности:
45
+ - **НЕ `forwardRef`** — FC. Ref-форвардинг не реализован. Известный
46
+ AI-ready debt.
47
+ - **Render-props-first** — `renderPickerFrom`, `renderPickerTo`,
48
+ `renderButtonBack`, `renderButtonForward` — все 4 обязательны.
49
+ - **`moment.js`** для арифметики сдвига диапазона.
50
+ - **Тесты есть** — `__tests__/DateRange.test.tsx` покрывает валидацию
51
+ пересечения и сдвиги.
52
+
53
+ ---
54
+
55
+ ## Варианты и props
56
+
57
+ ### `IDateRangeProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "defaultValue" | "onChange">`
58
+
59
+ | Prop | Тип | Default | Описание |
60
+ |---|---|---|---|
61
+ | `value` | `TDateRangeValue = [string, string]` | — | Диапазон `[from, to]` в формате `YYYY-MM-DD`. |
62
+ | `onChange` | `(value: [string, string]) => void` | — | Вызывается при изменении любой даты или при сдвиге. |
63
+ | `shiftAmount` | `number` | `1` | Величина сдвига диапазона. |
64
+ | `shiftUnit` | `EDateRangeShiftUnit` | `MONTH` | Единица сдвига (`"day" \| "week" \| "month" \| "quarter" \| "year"` — значения moment-совместимы). |
65
+ | `hideNavigation` | `boolean` | `false` | Скрыть кнопки сдвига. |
66
+ | `renderPickerFrom` | `(props: IDateRangePickerProvideProps) => ReactNode` | — | Обязателен. Рендерит поле "от". |
67
+ | `renderPickerTo` | `(props: IDateRangePickerProvideProps) => ReactNode` | — | Обязателен. Рендерит поле "до". |
68
+ | `renderButtonBack` | `(props: IDateRangeButtonProvideProps) => ReactNode` | — | Обязателен. Рендерит кнопку "назад". |
69
+ | `renderButtonForward` | `(props: IDateRangeButtonProvideProps) => ReactNode` | — | Обязателен. Рендерит кнопку "вперёд". |
70
+ | `className`, `...HTMLDivAttributes` | — | — | Стандартные. |
71
+
72
+ ### Provide-props
73
+
74
+ **`IDateRangePickerProvideProps`:**
75
+ ```ts
76
+ { value: string; onChange: (value: string) => void; }
77
+ ```
78
+
79
+ **`IDateRangeButtonProvideProps`:**
80
+ ```ts
81
+ {
82
+ children: ReactNode; // стрелка-иконка Caretleft/rightStrokeSrvIcon20
83
+ className: string; // .dateRangeButton + .disabled при пустом диапазоне
84
+ onClick: () => void; // shiftRangeBack / shiftRangeForward
85
+ disabled: boolean; // true если start или end пустые
86
+ }
87
+ ```
88
+
89
+ ### `TDateRangeValue = [string, string]`
90
+
91
+ Кортеж из двух строк. Пустая строка = "дата не задана". Это важно: **не
92
+ `undefined`, а именно `""`**. Потребитель должен всегда передавать
93
+ массив длиной 2.
94
+
95
+ ### `EDateRangeShiftUnit`
96
+
97
+ ```ts
98
+ enum EDateRangeShiftUnit {
99
+ DAY = "day",
100
+ WEEK = "week",
101
+ MONTH = "month",
102
+ QUARTER = "quarter",
103
+ YEAR = "year",
104
+ }
105
+ ```
106
+
107
+ Значения — строки, совместимые с `moment().add(n, unit)`.
108
+
109
+ ---
110
+
111
+ ## Ключевые особенности реализации
112
+
113
+ ### Валидация пересечения: очистка при инверсии
114
+
115
+ ```ts
116
+ const handleChangePickerFrom = (date: string) => {
117
+ if (!date || !end || date <= end) {
118
+ onChange([date, end]);
119
+ } else {
120
+ onChange([date, ""]); // from > to → чистим to
121
+ }
122
+ };
123
+
124
+ const handleChangePickerTo = (date: string) => {
125
+ if (!date || !start || date >= start) {
126
+ onChange([start, date]);
127
+ } else {
128
+ onChange(["", date]); // to < from → чистим from
129
+ }
130
+ };
131
+ ```
132
+
133
+ **Важная деталь:** сравнение `date <= end` / `date >= start` — это
134
+ **лексикографическое** сравнение строк. Работает **только** для формата
135
+ `YYYY-MM-DD` (ISO). Для других форматов (`DD.MM.YYYY` и т. п.) сравнение
136
+ сломается.
137
+
138
+ **Инвариант:** `value` и всё, что DateRange возвращает через `onChange`,
139
+ строго в `YYYY-MM-DD`. Потребитель отвечает за форматирование в
140
+ собственных пикерах (`DateField` сам конвертирует через `format` проп,
141
+ но по умолчанию тоже `YYYY-MM-DD`).
142
+
143
+ ### Сдвиг диапазона — `moment` арифметика
144
+
145
+ ```ts
146
+ const shiftRangeBack = () => {
147
+ if (!start || !end) return;
148
+ const momentStart = moment(start, dateFormatYYYYMMDD);
149
+ const momentEnd = moment(end, dateFormatYYYYMMDD);
150
+ onChange([
151
+ momentStart.subtract(shiftAmount, shiftUnit).format(dateFormatYYYYMMDD),
152
+ momentEnd.subtract(shiftAmount, shiftUnit).format(dateFormatYYYYMMDD),
153
+ ]);
154
+ };
155
+ ```
156
+
157
+ **Защита:** если start или end пустые → no-op. Кнопка тоже
158
+ `disabled={!(start && end)}`.
159
+
160
+ **Сдвигаются обе границы на одно и то же значение** — не "растянуть
161
+ диапазон", а "сдвинуть целиком". Размер диапазона сохраняется.
162
+
163
+ **Граничный случай:** если `shiftUnit=MONTH` и `start=2026-01-31`, то
164
+ `moment.subtract(1, 'month') = 2025-12-31` (moment сам обрабатывает
165
+ конец месяца). Для шага `1 year + 29 feb` — может быть скачок на 1
166
+ день. Это поведение `moment` и осознанное допущение.
167
+
168
+ ### Separator — `MinusStrokeSrvIcon20`
169
+
170
+ Между двумя пикерами рендерится иконка-минус 20x20 (`flex-shrink: 0`,
171
+ чтобы не сжималась).
172
+
173
+ ### Кнопки сдвига — позиция z-index: 1
174
+
175
+ ```less
176
+ .dateRangeButton { z-index: 1; }
177
+ ```
178
+
179
+ **Зачем:** в макете кнопки могут визуально перекрываться с soft-edge
180
+ полями пикеров (Dropdown может показываться поверх). z-index: 1 даёт
181
+ кнопкам приоритет над содержимым пикера при совпадении контекста.
182
+ **Не удалять** — без этого кнопки могут "уйти под" пикер.
183
+
184
+ ### `disabled` = нет диапазона (пусто)
185
+
186
+ Кнопки сдвига становятся `disabled`, пока не выбраны **обе** даты.
187
+ Логично — не можем сдвинуть "не определённый" диапазон.
188
+
189
+ ### `hideNavigation` — скрывает кнопки, не сам сдвиг
190
+
191
+ Внешний `shiftRange*` не публичные методы — доступ к сдвигам есть
192
+ только через UI-кнопки. Если `hideNavigation=true`, сдвиги недоступны.
193
+
194
+ Если нужны **кастомные триггеры сдвига** (например, кнопки в другом
195
+ месте UI), это не поддерживается — потребитель должен сам арифметику
196
+ `moment` реализовать и пушить новое значение.
197
+
198
+ ### Нет forwardRef — известный debt
199
+
200
+ `React.FC` без ref-forwarding. Потребителю ref недоступен. Если нужен —
201
+ оборачивать в собственный div с ref (deviates от паттерна).
202
+
203
+ ### `data-tx={process.env.npm_package_version}` — **нет**
204
+
205
+ Этот компонент **не** ставит data-tx. Исключение из общего правила
206
+ библиотеки. Возможно debt — при ревизии может быть добавлен.
207
+
208
+ ---
209
+
210
+ ## Инварианты
211
+
212
+ - **`value` и `onChange` — в `YYYY-MM-DD`** (`dateFormatYYYYMMDD`).
213
+ Лексикографическое сравнение не будет работать в других форматах.
214
+ - **Все 4 render-props обязательны** — `renderPickerFrom`, `renderPickerTo`,
215
+ `renderButtonBack`, `renderButtonForward`.
216
+ - **Инверсия диапазона очищает противоположную границу** —
217
+ `from > to` → чистим `to`; `to < from` → чистим `from`. Не менять.
218
+ - **Сдвиг блокируется при пустом диапазоне** — `!start || !end` → no-op +
219
+ `disabled: true`.
220
+ - **`shiftUnit` значения совместимы с moment** — не добавлять значения,
221
+ не поддерживаемые `moment.add/subtract`.
222
+ - **Separator — иконка минуса 20x20** — не заменять на "–" текст
223
+ (не масштабируется).
224
+ - **`.dateRangeButton { z-index: 1 }`** — не удалять.
225
+ - **НЕТ forwardRef** — известный debt, не добавлять точечно.
226
+ - **НЕТ data-tx** — наследованный пропуск.
227
+
228
+ ---
229
+
230
+ ## Accessibility
231
+
232
+ - **Кнопки "назад"/"вперёд"** — потребитель отвечает за `aria-label`
233
+ (через render-props). DateRange не задаёт `aria-label`.
234
+ - **`disabled`** — передаётся в render-props, обычно попадает на
235
+ `<button disabled>`.
236
+ - **Separator** — декоративная иконка, screen reader её не озвучит
237
+ (SVG без `role`/`aria-label`).
238
+ - **Связь полей from-to** — ARIA связи не выставлены. Если нужна
239
+ явная связь "это пара полей" — обёртывать в `<fieldset><legend>`.
240
+
241
+ ---
242
+
243
+ ## Дизайн-токены
244
+
245
+ **Нет.** Layout-компонент, разметка flex + фиксированный separator.
246
+
247
+ ---
248
+
249
+ ## Связанные компоненты
250
+
251
+ - `DateField` (`src/components/DateField`) — типичный `renderPickerFrom`/`To`.
252
+ - `DatePickerExtended` — низкоуровневый пикер.
253
+ - `MonthYearField` — близнец, но для выбора отдельного периода.
254
+ - `Button`/`ButtonIcon` — типично для `renderButtonBack`/`Forward`.
255
+
256
+ ---
257
+
258
+ ## Stories
259
+
260
+ `stories/DateRange/DateRange.stories.tsx`
261
+ Примеры: `stories/DateRange/examples/`
262
+
263
+ Типовые stories:
264
+ - `Default` — два DateField + кнопки сдвига
265
+ - `HideNavigation` — без кнопок
266
+ - `ShiftUnits` — перебор DAY/WEEK/MONTH/QUARTER/YEAR
267
+ - `Playground` — интерактив
268
+
269
+ ---
270
+
271
+ ## История изменений
272
+
273
+ | Дата | Изменение |
274
+ |---|---|
275
+ | 2026-04-18 | Создан документ |