@sberbusiness/triplex-next-mcp-server 1.46.0-beta → 1.50.0-beta

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/README.md +7 -7
  2. package/bundle/components/Amount.md +2 -1
  3. package/bundle/components/AmountField.md +222 -323
  4. package/bundle/components/Avatar.md +138 -146
  5. package/bundle/components/Badge.md +2 -1
  6. package/bundle/components/BodyPage.md +2 -1
  7. package/bundle/components/Button.md +5 -1
  8. package/bundle/components/ButtonDropdownExtended.md +3 -1
  9. package/bundle/components/ButtonIcon.md +3 -1
  10. package/bundle/components/Calendar.md +3 -1
  11. package/bundle/components/CardAction.md +1 -0
  12. package/bundle/components/CardStatic.md +402 -0
  13. package/bundle/components/CheckboxTree.md +157 -215
  14. package/bundle/components/CheckboxTreeExtended.md +7 -7
  15. package/bundle/components/CheckboxXGroup.md +2 -1
  16. package/bundle/components/Chip.md +2 -1
  17. package/bundle/components/ChipGroup.md +1 -1
  18. package/bundle/components/ChipMonthYearPicker.md +6 -5
  19. package/bundle/components/ChipMultiselect.md +4 -1
  20. package/bundle/components/ChipOptions.md +2 -1
  21. package/bundle/components/ChipSort.md +4 -1
  22. package/bundle/components/ChipSuggest.md +7 -6
  23. package/bundle/components/CollapsibleTree.md +10 -5
  24. package/bundle/components/CollapsibleTreeExtended.md +8 -6
  25. package/bundle/components/ComposedKeyDownListener.md +4 -3
  26. package/bundle/components/Confirm.md +346 -281
  27. package/bundle/components/DateField.md +7 -1
  28. package/bundle/components/DateRange.md +351 -210
  29. package/bundle/components/Divider.md +85 -138
  30. package/bundle/components/DocumentNumberEdit.md +173 -214
  31. package/bundle/components/Dropdown.md +11 -1
  32. package/bundle/components/Ellipsis.md +131 -147
  33. package/bundle/components/FooterPage.md +4 -1
  34. package/bundle/components/FormField.md +13 -5
  35. package/bundle/components/FormGroup.md +3 -2
  36. package/bundle/components/Gap.md +1 -1
  37. package/bundle/components/Header.md +1 -0
  38. package/bundle/components/HeaderPage.md +3 -1
  39. package/bundle/components/HelpBox.md +2 -1
  40. package/bundle/components/IconWrapper.md +10 -6
  41. package/bundle/components/ImageGallery.md +2 -1
  42. package/bundle/components/ImageGalleryExtended.md +2 -1
  43. package/bundle/components/IslandAccordion.md +6 -1
  44. package/bundle/components/KeyDownListener.md +3 -2
  45. package/bundle/components/LightBox.md +15 -1
  46. package/bundle/components/List.md +2 -1
  47. package/bundle/components/ListActionItem.md +2 -1
  48. package/bundle/components/ListItem.md +3 -1
  49. package/bundle/components/ListItemControlsButton.md +2 -1
  50. package/bundle/components/ListItemSelectable.md +2 -1
  51. package/bundle/components/ListMaster.md +2 -2
  52. package/bundle/components/ListMasterFooter.md +2 -1
  53. package/bundle/components/ListMasterHeader.md +2 -1
  54. package/bundle/components/ListSortable.md +2 -1
  55. package/bundle/components/ListSortableItem.md +2 -1
  56. package/bundle/components/ListTableItem.md +10 -7
  57. package/bundle/components/LoaderMiddle.md +5 -1
  58. package/bundle/components/LoaderScreen.md +7 -1
  59. package/bundle/components/LoaderSmall.md +10 -4
  60. package/bundle/components/MaskedField.md +3 -1
  61. package/bundle/components/MediaWidth.md +2 -1
  62. package/bundle/components/MobileView.md +2 -1
  63. package/bundle/components/ModalWindow.md +4 -1
  64. package/bundle/components/MonthYearField.md +19 -5
  65. package/bundle/components/Notification.md +1 -0
  66. package/bundle/components/Overlay.md +7 -2
  67. package/bundle/components/Pagination.md +1 -7
  68. package/bundle/components/Radio.md +238 -115
  69. package/bundle/components/RadioXGroup.md +259 -0
  70. package/bundle/components/RadioYGroup.md +266 -0
  71. package/bundle/components/SegmentedControl.md +505 -158
  72. package/bundle/components/SelectExtendedField.md +2 -1
  73. package/bundle/components/SelectField.md +4 -1
  74. package/bundle/components/Skeleton.md +140 -69
  75. package/bundle/components/Slider.md +389 -97
  76. package/bundle/components/SliderRange.md +486 -105
  77. package/bundle/components/Spoiler.md +245 -63
  78. package/bundle/components/StatusTracker.md +386 -94
  79. package/bundle/components/Step.md +257 -68
  80. package/bundle/components/Stepper.md +355 -102
  81. package/bundle/components/StepperExtended.md +633 -0
  82. package/bundle/components/SuggestField.md +1061 -96
  83. package/bundle/components/SwipeableArea.md +7 -7
  84. package/bundle/components/Tabs.md +266 -92
  85. package/bundle/components/TabsExtended.md +18 -18
  86. package/bundle/components/TabsLine.md +302 -127
  87. package/bundle/components/TagColor.md +187 -66
  88. package/bundle/components/TagGroup.md +184 -48
  89. package/bundle/components/TextField.md +8 -4
  90. package/bundle/components/TextareaField.md +279 -70
  91. package/bundle/components/ThemeProvider.md +2 -1
  92. package/bundle/components/Tooltip.md +6 -3
  93. package/bundle/components/TopOverlay.md +17 -1
  94. package/bundle/components/TreeView.md +16 -7
  95. package/bundle/components/TriggerClickOnKeyDownEvent.md +277 -0
  96. package/bundle/components/UploadZone.md +318 -0
  97. package/bundle/guides/codestyle.md +16 -1
  98. package/bundle/guides/commits.md +47 -1
  99. package/bundle/guides/context.md +4 -1
  100. package/bundle/guides/tests.md +23 -2
  101. package/bundle/manifest.json +78 -29
  102. package/bundle/release-notes/1.47.0.mdx +115 -0
  103. package/bundle/release-notes/1.48.0.mdx +68 -0
  104. package/bundle/release-notes/1.49.0.mdx +85 -0
  105. package/bundle/release-notes/1.50.0.mdx +35 -0
  106. package/bundle/release-notes/1.51.0.mdx +7 -0
  107. package/bundle/tokens.json +163 -13
  108. package/dist/index.js +220 -6
  109. package/dist/index.js.map +1 -1
  110. package/package.json +1 -1
@@ -1,11 +1,11 @@
1
1
  ---
2
2
  component: AmountField
3
3
  category: TextFields
4
- figma-node: ""
5
- figma-file: ""
6
- related: [TextField, NumberField, Amount, FormField, FormFieldInput]
7
- tokens: []
8
- stories: stories/TextFields/AmountField.stories.tsx
4
+ related: [TextField, FormField, Amount, NumberField]
5
+ tokens:
6
+ - FormField.Input_Color_Default
7
+ - FormField.Input_Color_Disabled
8
+ stories: stories/AmountField/AmountField.stories.tsx
9
9
  version: "1.0"
10
10
  ---
11
11
 
@@ -13,272 +13,175 @@ version: "1.0"
13
13
 
14
14
  ## Назначение
15
15
 
16
- Поле ввода денежной суммы с автоматическим форматированием: разделители групп
17
- (пробел), фиксированная дробная часть, запятая как десятичный разделитель,
18
- отображение валюты справа. Управляет позицией каретки при вводе/удалении так,
19
- чтобы она не попадала на разделители, и блокирует ввод нецифровых символов
20
- (кроме запятой/точки).
16
+ Поле ввода денежной суммы. Пока пользователь печатает, значение форматируется на лету
17
+ (разряды разделяются пробелом, целая и дробная части — запятой: `1234.56` → `1 234,56`),
18
+ а наружу через `inputProps.onChange` уходит нормализованное значение с точкой и без
19
+ разделителей разрядов (`"1234.56"`) — его можно отправлять на бэкенд без дополнительной
20
+ обработки. Дробная часть всегда дописывается до `fractionDigits` знаков, а каретка
21
+ удерживается в осмысленной позиции при вводе, удалении и вставке.
21
22
 
22
- Построен поверх `TextFieldBase` и `FormFieldInput` (получает визуальное оформление
23
- и поддержку label/status/prefix/postfix от FormField-стека). Логика форматирования
24
- инкапсулирована в классе `AmountBaseInputCore`, который внутренне делит работу на
25
- парсер (`AmountBaseInputParser`) и форматер (`AmountBaseInputFormatter`).
23
+ Композиция над семейством `FormField`: `TextFieldBase` (→ `FormGroup` → `FormField`) +
24
+ `FormFieldInput`, плюс собственный слой со знаком валюты.
26
25
 
27
- Используй когда: пользователь вводит денежную сумму с валютой и фиксированной
28
- дробной частью.
29
- Не используй когда: нужен просто числовой ввод без валюты (используй `NumberField`);
30
- нужно только отобразить сумму без ввода (используй `Amount`).
26
+ Используй когда: пользователь вводит сумму — платёж, лимит, ставку, любой денежный
27
+ показатель, который нужно показывать с разрядами и хранить числом.
31
28
 
32
- ---
29
+ Не используй когда: нужно только показать готовую сумму без ввода (`Amount`), нужен
30
+ счётчик/произвольное число без денежного форматирования (`NumberField`), значение имеет
31
+ фиксированный формат-маску (`MaskedField`) или формат произвольный (`TextField`).
33
32
 
34
- ## Файловая структура
33
+ ---
35
34
 
36
- ```text
37
- src/components/AmountField/
38
- ├── AmountField.tsx # React.forwardRef, главный компонент
39
- ├── AmountBaseInputCore.ts # Класс-оркестратор: parse → format, хранит caret и cache
40
- ├── AmountBaseInputParser.ts # Класс-парсер: нормализует value, trimLeadingZeros, splits integer/fractional
41
- ├── AmountBaseInputFormatter.ts # Класс-форматер: добавляет пробелы между группами, запятую
42
- ├── utils.ts # createPlaceholder, setFallbackCaret
43
- ├── index.ts # Barrel (только AmountField)
44
- └── __tests__/ # Unit-тесты
45
- ```
35
+ ## Варианты и props
46
36
 
47
- Особенности:
48
- - **Нет LESS** — компонент визуально идентичен `TextField`, стили наследуются из
49
- `TextFieldBase`/`FormFieldInput`.
50
- - **Нет `stories/AmountField/`** — stories лежат в `stories/TextFields/AmountField.stories.tsx`
51
- (логически относится к группе TextFields).
52
- - Core-логика разделена на 3 класса намеренно — parser, formatter, core (оркестратор).
53
- Такое разделение нужно, чтобы можно было тестировать parse/format изолированно.
37
+ `IAmountFieldProps` расширяет `ITextFieldBaseProps` (кроме `children`), поэтому
38
+ `size`, `status`, `active`, `label`, `prefix`, `postfix`, `description`, `counter`,
39
+ `className` и div-атрибуты ведут себя так же, как у `TextField`.
54
40
 
55
- ---
41
+ ### Обязательные props
56
42
 
57
- ## Варианты и props
43
+ | Prop | Тип | Описание |
44
+ |---|---|---|
45
+ | `inputProps` | `Omit<IFormFieldInputProps, "type" \| "maxLength" \| "onChange" \| "inputMode" \| "autoComplete"> & { value, onChange, ref? }` | Свойства поля ввода. `value` — нормализованное значение (`"1234.56"`, пустая строка для пустого поля), `onChange(value)` получает такое же нормализованное значение, `ref` — ссылка на `<input>`. Остальные атрибуты (`placeholder`, `id`, `onFocus`, `onBlur`, `onKeyDown`, `onSelect`, data-атрибуты) пробрасываются в `FormFieldInput`. |
58
46
 
59
- ### `IAmountFieldProps extends Omit<ITextFieldBaseProps, "children">`
47
+ ### Опциональные props
60
48
 
61
49
  | Prop | Тип | По умолчанию | Описание |
62
50
  |---|---|---|---|
63
- | `inputProps` | `Omit<IFormFieldInputProps, "type" \| "maxLength" \| "onChange" \| "inputMode" \| "autoComplete"> & { value: string; onChange: (value: string) => void }` | — (обязателен) | Всё для `<input>`, кроме полей, которые AmountField задаёт сам. **`onChange` — нестандартный**: получает `value: string` (ЧИСТОЕ значение без форматирования), не event. |
64
- | `currency` | `string` | — | Название валюты (например, "RUB"). Рендерится справа как постфикс через `Text` с типом `SECONDARY`/`DISABLED`. Не указано — постфикса нет. |
65
- | `maxIntegerDigits` | `number` | `16` | Максимум цифр до запятой. Ввод сверх лимита игнорируется. |
66
- | `fractionDigits` | `number` | `2` | Цифр после запятой. Используется для placeholder, parse, format. При `0` — компонент становится integer-only. |
67
- | `onClear` | `() => void` | — | Если указан — справа перед валютой появляется `FormFieldClear` (крестик). Коллбек при клике. |
68
- | `postfix` | `React.ReactNode` | — | Дополнительный постфикс (рядом с валютой и clear-кнопкой). Ограничение: ставится после валюты в визуальном порядке. |
69
- | `...TextFieldBaseProps` | — | — | `label`, `status`, `size`, `prefix`, `description`, `counter`, `data-test-id`, и т.д. |
70
-
71
- ### Что AmountField заставляет у `inputProps`
72
-
73
- - `type`: всегда `"text"` (задаётся TextFieldBase/FormFieldInput по умолчанию; в AmountField не передаётся).
74
- - `maxLength`: не используется — AmountField сам ограничивает через `maxIntegerDigits` + `fractionDigits`.
75
- - `onChange`: перехватывается. Снаружи получаешь чистое значение `string` без пробелов и с `.` как разделителем.
76
- - `inputMode`: всегда `"decimal"` — подсказывает мобильной клавиатуре показать цифры и запятую.
77
- - `autoComplete`: всегда `"off"` — браузерный автокомплит отключен.
51
+ | `size` | `EComponentSize` | `LG` | Размер поля: SM / MD / LG. Тот же размер получает слой со знаком валюты. |
52
+ | `status` | `EFormFieldStatus` | `DEFAULT` | Статус: default / error / warning / disabled. `DISABLED` блокирует input и приглушает знак валюты. |
53
+ | `currency` | `string` | — | Наименование валюты справа от значения. Не рендерится, пока отформатированное значение пустое. |
54
+ | `maxIntegerDigits` | `number` | `16` | Максимальное количество знаков перед запятой. Лишние цифры не попадают в значение. |
55
+ | `fractionDigits` | `number` | `2` | Количество знаков после запятой. `0` — поле без дробной части. |
56
+ | `onClear` | `() => void` | — | Обработчик очистки. Если передан, перед пользовательским `postfix` рендерится `FormFieldClear`. Компонент сам значение не сбрасывает — это делает потребитель. |
57
+ | `clearProps` | `Omit<IFormFieldClearProps, "onClick">` | — | Свойства кнопки очистки. Имеют смысл только вместе с `onClear`. Сюда передаётся `aria-label` — доступное имя кнопки. `onClick` задаётся через `onClear` и не переопределяется. |
58
+
59
+ ### Особенности поведения
60
+
61
+ - **Значение контролируемое и нормализованное.** В `value` хранится `"<целая>.<дробная>"`
62
+ без разделителей разрядов; в поле показывается форматированный вариант с запятой.
63
+ Значение, пришедшее снаружи, нормализуется только для отображения: `"1234,5"` покажется
64
+ как `1 234,50`, но `onChange` при этом не вызывается — в состоянии потребителя останется
65
+ `"1234,5"`, пока пользователь не отредактирует поле. Синхронизировать состояние с
66
+ нормализованным видом компонент не берётся: это задача потребителя.
67
+ - **Дробная часть дополняется нулями** до `fractionDigits`. Ввод только дробной части
68
+ (`,5`) восстанавливает целый ноль → `"0.50"`. Если целой части нет, а дробная нулевая
69
+ (`,00`) — значение считается пустым (`""`).
70
+ - **Ведущие нули отбрасываются** вместе со всем, что стоит перед первой значащей цифрой;
71
+ любые символы, кроме цифр и десятичного разделителя, игнорируются.
72
+ - **Разделители.** Ввод принимает и `,`, и `.` как десятичный разделитель. В отображаемом
73
+ значении разряды разделяются обычным пробелом (U+0020), десятичная часть — запятой.
74
+ - **Placeholder.** Если `inputProps.placeholder` не передан (или пустая строка), он
75
+ генерируется из `fractionDigits`: `0,00` при `2`, `0` при `0`.
76
+ - **Каретка.** После каждого ввода позиция каретки пересчитывается с учётом добавленных
77
+ разделителей и восстанавливается в layout-эффекте. Эффект зависит не только от
78
+ `inputProps.value`, но и от `maxIntegerDigits` и `fractionDigits`: смена формата
79
+ пересчитывает `core.caret` при неизменном значении, и без этих зависимостей каретка
80
+ осталась бы на индексе прежнего формата. Если значение снаружи не изменилось
81
+ (ввод невалиден или значение то же), каретка ставится хелпером `setFallbackCaret` —
82
+ он же учитывает `Backspace`/`Delete` рядом с разделителем и направление выделения.
83
+ - **Знак валюты** рисуется поверх поля отдельным слоем: невидимая (`color: transparent`)
84
+ копия значения задаёт отступ, за ней идёт сам `currency`. Копия помечена
85
+ `aria-hidden="true"`.
86
+ - **`data-test-id`** на компоненте порождает суффиксы: `{id}__input` на элементе ввода и
87
+ `{id}__unit` на знаке валюты.
78
88
 
79
89
  ---
80
90
 
81
- ## Ключевые особенности реализации
82
-
83
- ### Разделение value и formattedValue
84
-
85
- - **`value`** (то, что во внешнем state) — нормализованная строка: только цифры и `.`
86
- как разделитель (например, `"1234.56"`).
87
- - **`formattedValue`** (то, что показывается в `<input>`) — с пробелами и запятой
88
- (например, `"1 234,56"`).
89
-
90
- `AmountField` рендерит `formattedValue`, но во внешний `onChange` отдаёт `value`.
91
- Это значит: внешнее состояние всегда совместимо с `big.js` / `parseFloat` /
92
- backend API без дополнительной очистки.
93
-
94
- ### Ленивая инициализация core
95
-
96
- ```tsx
97
- const core = useRef<AmountBaseInputCore>();
98
- if (core.current === undefined) core.current = new AmountBaseInputCore(maxIntegerDigits, fractionDigits);
99
- ```
100
-
101
- Создаётся **один раз при первом рендере**. Изменение `maxIntegerDigits`/`fractionDigits`
102
- применяется через `core.current.maxIntegerDigits = ...` в `getFormattedValue`, а не
103
- через пересоздание инстанса.
104
-
105
- ### Синхронизация внешнего value с core
106
-
107
- ```tsx
108
- if (inputProps.value != core.current.value || ...) {
109
- core.current.apply(inputProps.value, inputProps.value.length);
110
- }
111
- ```
112
-
113
- На каждом рендере проверяется, изменилось ли значение извне (controlled re-render)
114
- или параметры (`max/fractionDigits`). Если да — `core.apply()` пересчитывает
115
- `formattedValue`. Это гарантирует, что компонент работает как controlled: внешний
116
- state — единственный источник истины.
117
-
118
- ### Управление кареткой: `useLayoutEffect` + `setCaretPosition`
119
-
120
- ```tsx
121
- useLayoutEffect(() => {
122
- if (core.current && refInput.current == document.activeElement)
123
- setCaretPosition(refInput.current, Math.max(core.current.caret, 0));
124
- }, [inputProps.value]);
125
- ```
126
-
127
- После каждого изменения value, если input в фокусе, восстанавливается позиция
128
- каретки в `core.caret`. Это компенсирует то, что React при controlled input
129
- сбрасывает каретку в конец после rerender.
130
-
131
- **`useLayoutEffect`, не `useEffect`** — каретка ставится синхронно до отрисовки,
132
- чтобы не было визуального "мигания".
133
-
134
- ### Fallback caret через `setFallbackCaret`
135
-
136
- Если `onChange` **не меняет значение** (невалидный ввод, идентичное значение) —
137
- React не перерендерится, и `useLayoutEffect` не сработает. Для этого случая
138
- `handleChange` вызывает `setFallbackCaret(event.target, core, fractionDigits)`,
139
- которая обрабатывает спецкейсы:
140
- - Каретка перед разделителем `,` → сдвиг вперёд.
141
- - Backspace/Delete возле разделителей → сдвиг.
142
- - Обратное направление выделения → каретка в конец.
143
-
144
- Эта утилита — единственное место, где компонент напрямую дёргает input DOM API
145
- (`setSelectionRange`).
146
-
147
- ### Кеш для восстановления контекста
148
-
149
- `core.cache` хранит: последнее `formattedValue`, `key` нажатой клавиши,
150
- `selectionStart`/`End`/`Direction` на момент последнего select-event. Это даёт
151
- парсеру/формате́ру/fallback-caret полную картину "что сделал пользователь", чтобы
152
- правильно обработать ввод.
153
-
154
- `handleSelect` обновляет `cache.selectionStart/End/Direction` на каждом
155
- событии выделения — значит, к моменту `handleChange` эти поля актуальны.
91
+ ## Дизайн-токены
156
92
 
157
- ### `setRef` вручную
93
+ Переопределяются через `ThemeProvider` (prop `tokens`) — см. `ThemeProvider-ai.md` →
94
+ «Как переопределять токены». Значения по умолчанию — `src/components/DesignTokens/components/FormField.ts`.
158
95
 
159
- ```tsx
160
- const setRef = (instance) => {
161
- refInput.current = instance; // локальный ref
162
- if (typeof ref === "function") ref(instance);
163
- else if (ref) ref.current = instance;
164
- };
96
+ ```text
97
+ FormField.Input_Color_Default
98
+ FormField.Input_Color_Disabled
165
99
  ```
166
100
 
167
- Компоненту нужен **и** локальный ref (для caret-манипуляций), **и** внешний
168
- forwarded ref. `setRef` передаёт instance в оба. Не заменять на единственный
169
- внешний ref.
170
-
171
- ### `createPlaceholder(fractionDigits)`
172
-
173
- Если потребитель не передал `inputProps.placeholder`, генерируется автоматически:
174
- - `fractionDigits=0` → `"0"`.
175
- - `fractionDigits=2` → `"0,00"`.
176
- - `fractionDigits=4` → `"0,0000"`.
177
-
178
- ### `parseFractionalPart` восстанавливает целый ноль
179
-
180
- Если пользователь ввёл `.5` (без целой части) и дробная часть ненулевая, парсер
181
- восстанавливает `0.5`. Если дробная часть только из нулей (`"00"`) — целая часть
182
- остаётся пустой (парсинг останавливается), итоговое значение `""`.
183
-
184
- ### `trimLeadingZeros` — не агрессивен
185
-
186
- Удаляет ведущие нули **только если следующая цифра ненулевая**. Значение `"0"` или
187
- `"00"` остаётся целой частью `""` или `"0"` в зависимости от дробной. Это
188
- обеспечивает корректное поведение при вводе `0,5` (пользователь пишет с нуля).
101
+ Собственных токенов у `AmountField` нет: знак валюты красится теми же токенами, что и текст
102
+ поля, остальное оформление наследуется от семейства `FormField`.
189
103
 
190
104
  ---
191
105
 
192
106
  ## Инварианты
193
107
 
194
- - **`forwardRef`** на `AmountField` — обязателен, ref идёт на `<input>`. Не убирать.
195
- - **`inputMode="decimal"`** — не менять на `numeric` или `text`. `decimal` даёт
196
- правильную мобильную клавиатуру с запятой.
197
- - **`autoComplete="off"`** — не убирать. Браузерный автокомплит ломает
198
- форматирование.
199
- - **`onChange` отдаёт `string` (чистое значение), не event** — это **контракт**.
200
- Потребители полагаются на нормализованный формат. Не менять сигнатуру без
201
- breaking-change.
202
- - **Разделение parser/formatter/core** — не сливать в один класс. Тесты
203
- построены вокруг парсера/формате́ра в изоляции.
204
- - **Запятая как разделитель, точка тоже принимается** — парсер
205
- (`StringUtils.isDecimalSeparator`) принимает и `.`, и `,`. Formatter всегда
206
- выдаёт `,`. Не менять.
207
- - **Пробел как групповой разделитель** — не менять на NBSP или другой символ
208
- (parser полагается на `StringUtils.isDigit` для определения не-цифр).
209
- - **Core создаётся один раз** — не пересоздавать при каждом изменении props,
210
- потеряется кэш.
211
- - **`useLayoutEffect` (не `useEffect`)** для caret-позиции — не менять.
212
- - **`setFallbackCaret`** — обязателен в `handleChange`. Без него каретка
213
- теряется при идентичном/невалидном вводе.
214
- - **`data-test-id` суффиксы** (`__input`, `__unit`) — публичный контракт для
215
- тестов потребителей. Не переименовывать.
216
- - **`maxIntegerDigits=16` по умолчанию** — покрывает квадриллионы, но не больше
217
- (bigint). Если лимит мал — увеличивать через prop, не в коде.
108
+ - `forwardRef` на компоненте — не убирать. Внешний `ref` указывает на корневой `<div>`
109
+ `FormField`, ссылка на `<input>` передаётся через `inputProps.ref`.
110
+ - Публичный API (`IAmountFieldProps`: `inputProps`, `currency`, `maxIntegerDigits`,
111
+ `fractionDigits`, `onClear`, `clearProps` + унаследованные от `ITextFieldBaseProps`) —
112
+ изменение имён, типов и дефолтов является breaking change.
113
+ - `inputProps` не принимает `type`, `maxLength`, `inputMode`, `autoComplete` и собственный
114
+ `onChange` с сигнатурой события: их задаёт сам компонент (`inputMode="decimal"`,
115
+ `autoComplete="off"`, `onChange(value: string)`).
116
+ - Формат нормализованного значения (точка как десятичный разделитель, отсутствие
117
+ разделителей разрядов) — часть контракта: на нём построены потребители, отправляющие
118
+ значение на бэкенд.
119
+ - Разделитель разрядов в отображаемом значении — обычный пробел U+0020, тогда как
120
+ `Amount` (отображение суммы) использует `AmountConst.GroupSeparator` — неразрывный
121
+ пробел U+00A0. Различие существующее и наблюдаемое (копирование значения, e2e-селекторы),
122
+ менять его без отдельного решения нельзя.
123
+ - `AmountBaseInputCore`, `AmountBaseInputParser`, `AmountBaseInputFormatter` и `utils.ts` —
124
+ внутренняя кухня компонента, из barrel `index.ts` не экспортируется.
125
+ - Экземпляр `AmountBaseInputCore` живёт в `useRef` весь срок жизни компонента: он хранит
126
+ позицию каретки и кэш выделения между рендерами. Пересоздавать его на каждый рендер или
127
+ заменять на state нельзя — потеряется каретка.
128
+ - CSS-класс `currencyLayout` используется в unit-тестах как селектор слоя валюты —
129
+ переименование потребует правки теста.
218
130
 
219
131
  ---
220
132
 
221
133
  ## Accessibility
222
134
 
223
- - **Нативный `<input>`** — клавиатурная навигация работает из коробки.
224
- - **`inputMode="decimal"`** — на мобильных устройствах показывается числовая
225
- клавиатура с десятичным разделителем.
226
- - **`autoComplete="off"`** — отключает автозаполнение (важно для сумм —
227
- браузерный autofill не подходит под формат).
228
- - **Валюта рендерится как отдельный `<Text>`** — screen reader прочитает её как
229
- обычный текст (например, "RUB"). Для полного названия валюты — потребитель
230
- может передать `aria-label` на самом поле через `inputProps`.
231
- - **Для отрицательных чисел компонент НЕ делает замену `-` → `\u2212`** (в отличие
232
- от display-компонента `Amount`). Это корректно: в input'е нужно настоящее
233
- значение, которое можно скопировать и переиспользовать; символ `\u2212` ломает
234
- parsing извне.
235
- - **`onClear` + `FormFieldClear`** — кнопка с иконкой внутри поля. Потребитель
236
- обязан убедиться, что у кнопки есть `aria-label` (через внутреннюю реализацию
237
- FormFieldClear) — проверь при расширении.
238
-
239
- ---
240
-
241
- ## Дизайн-токены
242
-
243
- Нет собственных — все цвета и размеры наследуются из `TextFieldBase`/
244
- `FormFieldInput`/`Typography`. См. их документацию для списка токенов.
135
+ - Лейбл — настоящий `<label htmlFor>`: `FormFieldInput` генерирует `id` через `lodash uniqueId`
136
+ и связывает его с `FormFieldLabel`. Свой `id` можно передать в `inputProps`.
137
+ - Копия значения под знаком валюты помечена `aria-hidden="true"` — скринридер читает только
138
+ значение поля и сам знак валюты.
139
+ - `inputMode="decimal"` даёт числовую клавиатуру на мобильных устройствах.
140
+ - `status={EFormFieldStatus.DISABLED}` выставляет нативный `disabled` — поле штатно выпадает
141
+ из таб-обхода.
142
+ - Семантика ошибки (`aria-invalid`, `aria-describedby`) не хардкодится: при необходимости
143
+ потребитель передаёт атрибуты через `inputProps`.
144
+ - Кнопка очистки (`onClear`) — `FormFieldClear` с одной иконкой, без текста. Доступное имя
145
+ задаётся потребителем через `clearProps={{ "aria-label": "…" }}`: дефолтную строку зашить
146
+ нельзя, библиотека мультиязычная. Без `aria-label` кнопка остаётся безымянной — в примерах
147
+ и production-коде его передавать обязательно. Остальные поля семейства (`SuggestField`,
148
+ `DateField`, `ChipSuggest`, `MonthYearField`, `SelectExtendedField`) такого prop пока не
149
+ имеют и рендерят `FormFieldClear` только с `onClick`.
150
+ - Собственных клавиатурных сокращений нет; `onKeyDown` и `onSelect` из `inputProps`
151
+ вызываются после внутренних обработчиков, которые запоминают нажатую клавишу и выделение
152
+ для расчёта каретки.
245
153
 
246
154
  ---
247
155
 
248
156
  ## Связанные компоненты
249
157
 
250
- - `TextFieldBase` (`src/components/TextField/TextFieldBase.tsx`) — базовый
251
- контейнер поля, предоставляет label/status/size/prefix/postfix.
252
- - `FormField` / `FormFieldInput` (`src/components/FormField`) — нативный `<input>`
253
- с визуальным оформлением формы.
254
- - `FormFieldClear` (`src/components/FormField/components/FormFieldClear.tsx`) —
255
- кнопка очистки (крестик). Используется, если передан `onClear`.
256
- - `Typography` (`Text`, `ETextSize.B2`, `EFontType.SECONDARY/DISABLED`) —
257
- рендер валюты.
258
- - `Amount` (`src/components/Amount`) — display-аналог. Форматирование разное:
259
- AmountField использует свою реализацию (через parser/formatter классы),
260
- Amount — `formatAmount` из `utils/amountUtils.ts` на `big.js`. Результат
261
- визуально совпадает, но алгоритмы различны.
262
- - `NumberField` (`src/components/NumberField`) — числовой ввод без валюты.
263
- - `AmountConst` (`src/consts/AmountConst`) — общие константы разделителей.
264
- - Утилиты `utils/inputUtils.ts` (`setCaretPosition`), `utils/keyboard.ts` (`isKey`),
265
- `utils/stringUtils.ts` (`StringUtils.isDigit`, `isDecimalSeparator`).
158
+ - `TextFieldBase` — базовая раскладка поля (`FormGroup`/`FormField` + слоты `prefix`,
159
+ `label`, `postfix`, `description`, `counter`); `IAmountFieldProps` наследует её props.
160
+ Живёт в семействе `TextField`.
161
+ - `FormField` / `FormFieldInput` — контейнер поля и сам элемент ввода, куда уходит
162
+ `inputProps`.
163
+ - `FormFieldClear` — кнопка очистки, которую компонент добавляет в постфикс при `onClear`.
164
+ - `Amount` — отображение уже готовой суммы (без ввода), парная компонента для read-only
165
+ сценариев.
166
+ - `NumberField` — поле для произвольных чисел без денежного форматирования.
167
+ - `MaskedField`, `TextField` — соседние поля той же группы `TextFields` для форматов с
168
+ маской и произвольного текста.
266
169
 
267
170
  ---
268
171
 
269
172
  ## Stories
270
173
 
271
- `stories/TextFields/AmountField.stories.tsx`
272
- Файлы примеров: `stories/TextFields/examples/AmountField/`
174
+ Основные истории: `stories/AmountField/AmountField.stories.tsx`
175
+ Файлы примеров: `stories/AmountField/examples/`
273
176
 
274
177
  | Story | Example file | Что демонстрирует |
275
178
  |---|---|---|
276
- | `Playground` | `PlaygroundExample.tsx` | Интерактивный контроль `status`, `size`, `label`, `currency`, `prefix`, `postfix`, `description`, `counter`, `maxIntegerDigits`, `fractionDigits` |
277
- | `Basic` | `BasicExample.tsx` | Базовое controlled-использование |
278
- | `Sizes` | `SizesExample.tsx` | Все размеры `EComponentSize` |
279
- | `Statuses` | `StatusesExample.tsx` | Все `EFormFieldStatus` |
280
- | `WithClearButton` | `WithClearButtonExample.tsx` | С крестиком очистки |
281
- | `Example` | `ExampleExample.tsx` | Композиция с другими компонентами (формы) |
179
+ | `Playground` | `Playground.tsx` | Интерактивный контроль size / status / label / currency / maxIntegerDigits / fractionDigits и настроек: кнопка очистки, `HelpBox`, описание |
180
+ | `Default` | `Default.tsx` | Минимальное контролируемое поле со знаком валюты |
181
+ | `Sizes` | `Sizes.tsx` | Размеры SM / MD / LG |
182
+ | `Statuses` | `Statuses.tsx` | Статусы default / error / warning / disabled |
183
+ | `Production` | `Production.tsx` | Production-композиция (`Example: production`): кнопка очистки, `HelpBox` в постфиксе, description со ссылкой |
184
+ | `VisualTests` | `VisualTests.tsx` | Скриншот-регрессия: пустое поле в фокусе (через `play`), заполненные размеры со знаком валюты, заблокированное заполненное поле |
282
185
 
283
186
  ---
284
187
 
@@ -286,37 +189,36 @@ forwarded ref. `setRef` передаёт instance в оба. Не заменят
286
189
 
287
190
  | Дата | Изменение |
288
191
  |---|---|
289
- | 2026-04-18 | Создан документ |
192
+ | 2026-09-10 | Создан документ (TRI-9). AI-рефакторинг AmountField, unit-тесты на ядро, парсер, форматер и утилиты; добавлен `displayName` |
193
+ | 2026-09-14 | Правки по ревью (TRI-9): добавлен prop `clearProps` — доступное имя кнопки очистки задаётся через `aria-label`. Хелпер `setForwardedRef` переехал в общий внутренний модуль `src/helpers/setForwardedRef.ts` (пять копий по репозиторию свёрнуты в одну); публичный API это не затрагивает. |
194
+ | 2026-09-17 | Правки по ревью (TRI-9): `setForwardedRef` импортируется относительным путём вместо алиаса `@sberbusiness/triplex-next/helpers/...` — алиас в `src` зарезервирован за публичными subentry. |
195
+ | 2026-09-17 | Правки по ревью (TRI-9): в зависимости `useLayoutEffect`, восстанавливающего каретку, добавлены `maxIntegerDigits` и `fractionDigits`. Без них смена формата при неизменном `value` пересчитывала `core.caret`, но не переносила каретку в DOM. |
290
196
 
291
197
  ---
292
198
 
293
199
  ## Примеры использования
294
200
 
295
- ### Basic
201
+ ### Default
296
202
 
297
- `stories/TextFields/examples/AmountField/BasicExample.tsx`
203
+ `stories/AmountField/examples/Default.tsx`
298
204
 
299
205
  ```tsx
300
206
  import React, { useState } from "react";
301
- import { AmountField, EComponentSize, EFormFieldStatus } from "@sberbusiness/triplex-next";
207
+ import { AmountField } from "@sberbusiness/triplex-next";
302
208
 
303
- export const BasicExample: React.FC = () => {
304
- const [value, setValue] = useState("");
209
+ export const Default = () => {
210
+ const [value, setValue] = useState<string>("");
305
211
 
306
212
  return (
307
- <div style={{ maxWidth: "300px" }}>
213
+ <div style={{ maxWidth: 300 }}>
308
214
  <AmountField
215
+ label="Label"
309
216
  inputProps={{
310
217
  value,
311
- placeholder: "0,00",
218
+ placeholder: "0,00 ₽",
312
219
  onChange: setValue,
313
220
  }}
314
- status={EFormFieldStatus.DEFAULT}
315
- size={EComponentSize.LG}
316
- label="Label"
317
- currency="RUB"
318
- maxIntegerDigits={18}
319
- fractionDigits={2}
221
+ currency="₽"
320
222
  />
321
223
  </div>
322
224
  );
@@ -325,136 +227,133 @@ export const BasicExample: React.FC = () => {
325
227
 
326
228
  ### Sizes
327
229
 
328
- `stories/TextFields/examples/AmountField/SizesExample.tsx`
230
+ `stories/AmountField/examples/Sizes.tsx`
329
231
 
330
232
  ```tsx
331
233
  import React, { useState } from "react";
332
234
  import { AmountField, EComponentSize } from "@sberbusiness/triplex-next";
333
235
 
334
- const AmountFieldItem: React.FC<{ size: EComponentSize }> = ({ size }) => {
335
- const [value, setValue] = useState("");
336
-
337
- return (
338
- <AmountField
339
- inputProps={{
340
- value,
341
- placeholder: "0,00",
342
- onChange: setValue,
343
- }}
344
- size={size}
345
- label="Label"
346
- />
347
- );
348
- };
236
+ interface ISizeItemProps {
237
+ size: EComponentSize;
238
+ }
349
239
 
350
- export const SizesExample: React.FC = () => {
351
- const sizes = Object.values(EComponentSize);
240
+ const SizeItem = ({ size }: ISizeItemProps) => {
241
+ const [value, setValue] = useState<string>("");
352
242
 
353
243
  return (
354
- <div style={{ maxWidth: "300px", display: "flex", flexDirection: "column", gap: "16px" }}>
355
- {sizes.map((size) => (
356
- <AmountFieldItem key={size} size={size} />
357
- ))}
244
+ <div style={{ maxWidth: 300 }}>
245
+ <div style={{ marginBottom: 8, fontSize: 16, fontWeight: 700 }}>{size.toUpperCase()}</div>
246
+ <AmountField
247
+ size={size}
248
+ label="Label"
249
+ inputProps={{
250
+ value,
251
+ placeholder: "0,00 ₽",
252
+ onChange: setValue,
253
+ }}
254
+ currency="₽"
255
+ />
358
256
  </div>
359
257
  );
360
258
  };
259
+
260
+ const SIZES = Object.values(EComponentSize);
261
+
262
+ export const Sizes = () => (
263
+ <div style={{ display: "flex", flexDirection: "column", gap: 16 }}>
264
+ {SIZES.map((size) => (
265
+ <SizeItem key={size} size={size} />
266
+ ))}
267
+ </div>
268
+ );
361
269
  ```
362
270
 
363
271
  ### Statuses
364
272
 
365
- `stories/TextFields/examples/AmountField/StatusesExample.tsx`
273
+ `stories/AmountField/examples/Statuses.tsx`
366
274
 
367
275
  ```tsx
368
276
  import React, { useState } from "react";
369
277
  import { AmountField, EFormFieldStatus } from "@sberbusiness/triplex-next";
370
278
 
371
- const AmountFieldItem: React.FC<{ status: EFormFieldStatus }> = ({ status }) => {
372
- const [value, setValue] = useState("");
373
-
374
- return (
375
- <AmountField
376
- inputProps={{
377
- value,
378
- placeholder: "0,00",
379
- onChange: setValue,
380
- }}
381
- status={status}
382
- label="Label"
383
- />
384
- );
385
- };
386
-
387
- export const StatusesExample: React.FC = () => {
388
- const statuses = Object.values(EFormFieldStatus);
389
-
390
- return (
391
- <div style={{ maxWidth: "300px", display: "flex", flexDirection: "column", gap: "16px" }}>
392
- {statuses.map((status) => (
393
- <AmountFieldItem key={status} status={status} />
394
- ))}
395
- </div>
396
- );
397
- };
398
- ```
399
-
400
- ### WithClearButton
401
-
402
- `stories/TextFields/examples/AmountField/WithClearButtonExample.tsx`
403
-
404
- ```tsx
405
- import React, { useState } from "react";
406
- import { AmountField } from "@sberbusiness/triplex-next";
279
+ interface IStatusItemProps {
280
+ status: EFormFieldStatus;
281
+ }
407
282
 
408
- export const WithClearButtonExample: React.FC = () => {
409
- const [value, setValue] = useState("8967452.31");
283
+ const StatusItem = ({ status }: IStatusItemProps) => {
284
+ const [value, setValue] = useState<string>("");
410
285
 
411
286
  return (
412
- <div style={{ maxWidth: "300px" }}>
287
+ <div style={{ maxWidth: 300 }}>
288
+ <div style={{ marginBottom: 8, fontSize: 16, fontWeight: 700 }}>{status.toUpperCase()}</div>
413
289
  <AmountField
290
+ status={status}
291
+ label="Label"
414
292
  inputProps={{
415
293
  value,
416
- placeholder: "0,00",
294
+ placeholder: "0,00 ₽",
417
295
  onChange: setValue,
418
296
  }}
419
- label="Label"
420
- onClear={() => setValue("")}
297
+ currency="₽"
421
298
  />
422
299
  </div>
423
300
  );
424
301
  };
302
+
303
+ const STATUSES = Object.values(EFormFieldStatus);
304
+
305
+ export const Statuses = () => (
306
+ <div style={{ display: "flex", flexDirection: "column", gap: 16 }}>
307
+ {STATUSES.map((status) => (
308
+ <StatusItem key={status} status={status} />
309
+ ))}
310
+ </div>
311
+ );
425
312
  ```
426
313
 
427
- ### Example
314
+ ### Production
428
315
 
429
- `stories/TextFields/examples/AmountField/ExampleExample.tsx`
316
+ `stories/AmountField/examples/Production.tsx`
430
317
 
431
318
  ```tsx
432
- import React, { useState } from "react";
433
- import { AmountField, HelpBox, ETooltipSize, Text, ETextSize, EFontType, Link } from "@sberbusiness/triplex-next";
319
+ import React, { useState, useRef } from "react";
320
+ import { AmountField, Text, HelpBox, Link, ETextSize, EFontType, ETooltipSize } from "@sberbusiness/triplex-next";
321
+
322
+ export const Production = () => {
323
+ const [value, setValue] = useState<string>("");
324
+ const inputRef = useRef<HTMLInputElement>(null);
434
325
 
435
- export const ExampleExample: React.FC = () => {
436
- const [value, setValue] = useState("");
326
+ const handleLinkClick: React.MouseEventHandler<HTMLAnchorElement> = (event) => {
327
+ event.preventDefault();
328
+ };
329
+
330
+ const handleClear = () => {
331
+ setValue("");
332
+ inputRef.current?.focus();
333
+ };
437
334
 
438
335
  return (
439
- <div style={{ maxWidth: "300px" }}>
336
+ <div style={{ maxWidth: 300 }}>
440
337
  <AmountField
441
- inputProps={{
442
- value,
443
- placeholder: "0,00",
444
- onChange: setValue,
445
- }}
446
338
  label="Label"
447
- currency="RUB"
448
339
  postfix={<HelpBox tooltipSize={ETooltipSize.SM}>Helpful details appear here</HelpBox>}
449
340
  description={
450
341
  <Text tag="div" size={ETextSize.B4} type={EFontType.SECONDARY}>
451
342
  (21) Description{" "}
452
- <Link href="#" onClick={(event) => event.preventDefault()}>
343
+ <Link href="#" onClick={handleLinkClick}>
453
344
  Link text
454
345
  </Link>
455
346
  </Text>
456
347
  }
457
- onClear={() => setValue("")}
348
+ inputProps={{
349
+ value,
350
+ placeholder: "0,00 ₽",
351
+ onChange: setValue,
352
+ ref: inputRef,
353
+ }}
354
+ currency="₽"
355
+ onClear={handleClear}
356
+ clearProps={{ "aria-label": "Очистить сумму" }}
458
357
  />
459
358
  </div>
460
359
  );