@ryuzaki13/react-foundation-lib 1.4.0 → 1.4.2

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 (101) hide show
  1. package/README.md +172 -114
  2. package/dist/chunks/{formatters-CBgrBeSA.js → formatters-D33Uh3si.js} +7 -8
  3. package/dist/chunks/{formatters-CBgrBeSA.js.map → formatters-D33Uh3si.js.map} +1 -1
  4. package/dist/chunks/odata-service-CWkHASdw.js +2211 -0
  5. package/dist/chunks/odata-service-CWkHASdw.js.map +1 -0
  6. package/dist/chunks/{validate-BW-7BZy_.js → validate-DyzojM0w.js} +2 -2
  7. package/dist/chunks/{validate-BW-7BZy_.js.map → validate-DyzojM0w.js.map} +1 -1
  8. package/dist/error-report/index.js +2 -2
  9. package/dist/excel/index.js +20 -21
  10. package/dist/excel/index.js.map +1 -1
  11. package/dist/formatters/index.js +4 -5
  12. package/dist/{odata → odata-service}/buildSeparatedArrays.d.ts +1 -1
  13. package/dist/odata-service/buildSeparatedArrays.d.ts.map +1 -0
  14. package/dist/{odata/types.d.ts → odata-service/collectionTypes.d.ts} +1 -1
  15. package/dist/odata-service/collectionTypes.d.ts.map +1 -0
  16. package/dist/{odata → odata-service}/dateUtils.d.ts +1 -1
  17. package/dist/odata-service/dateUtils.d.ts.map +1 -0
  18. package/dist/{odata → odata-service}/findCollectionPairs.d.ts +1 -1
  19. package/dist/odata-service/findCollectionPairs.d.ts.map +1 -0
  20. package/dist/odata-service/index.d.ts +5 -0
  21. package/dist/odata-service/index.d.ts.map +1 -1
  22. package/dist/odata-service/index.js +2 -2
  23. package/dist/{odata/store.d.ts → odata-service/odataCollectionStore.d.ts} +1 -1
  24. package/dist/odata-service/odataCollectionStore.d.ts.map +1 -0
  25. package/dist/query-client/index.js.map +1 -1
  26. package/dist/query-client/queryClient.d.ts +1 -3
  27. package/dist/query-client/queryClient.d.ts.map +1 -1
  28. package/package.json +1 -5
  29. package/src/array/README.mdx +1630 -0
  30. package/src/binary/README.mdx +1405 -0
  31. package/src/bounded-copy-stack/README.mdx +257 -0
  32. package/src/context-menu/README.mdx +326 -0
  33. package/src/copy/README.mdx +992 -0
  34. package/src/crypto/README.mdx +1052 -0
  35. package/src/currency/README.mdx +155 -0
  36. package/src/date-segments/README.mdx +319 -0
  37. package/src/dom/README.mdx +2179 -0
  38. package/src/error/README.mdx +95 -0
  39. package/src/error-report/README.mdx +612 -0
  40. package/src/excel/README.mdx +449 -0
  41. package/src/file/README.mdx +1957 -0
  42. package/src/fingerprint/README.mdx +100 -0
  43. package/src/form/README.mdx +101 -0
  44. package/src/formatters/README.mdx +469 -0
  45. package/src/formatters/boolean/README.mdx +494 -0
  46. package/src/formatters/date/README.mdx +732 -0
  47. package/src/formatters/number/README.mdx +542 -0
  48. package/src/formatters/pipeline/README.mdx +629 -0
  49. package/src/formatters/rowBased/README.mdx +486 -0
  50. package/src/formatters/strings/README.mdx +442 -0
  51. package/src/formatters/valueState/README.mdx +459 -0
  52. package/src/formulas/README.mdx +1283 -0
  53. package/src/foundationLib.mdx +70 -54
  54. package/src/hooks/README.mdx +988 -0
  55. package/src/http/README.mdx +126 -0
  56. package/src/media/README.mdx +1853 -0
  57. package/src/notifications/README.mdx +323 -0
  58. package/src/number-scale/README.mdx +318 -0
  59. package/src/odata-service/README.mdx +2916 -0
  60. package/src/presets/README.mdx +136 -0
  61. package/src/pwa/README.mdx +394 -0
  62. package/src/query-client/README.mdx +2500 -0
  63. package/src/range-output/README.mdx +161 -0
  64. package/src/seo/README.mdx +279 -0
  65. package/src/session-storage/README.mdx +125 -0
  66. package/src/string-comparison/README.mdx +96 -0
  67. package/src/table/README.mdx +3106 -0
  68. package/src/three-scene/README.mdx +248 -0
  69. package/src/types/README.mdx +172 -0
  70. package/src/utils/README.mdx +1724 -0
  71. package/src/validators/README.mdx +239 -0
  72. package/src/virtualizer/README.mdx +138 -0
  73. package/src/xml/README.mdx +149 -0
  74. package/dist/chunks/dateUtils-B5V5aXB-.js +0 -847
  75. package/dist/chunks/dateUtils-B5V5aXB-.js.map +0 -1
  76. package/dist/chunks/odata-service-B6TxBSLm.js +0 -1277
  77. package/dist/chunks/odata-service-B6TxBSLm.js.map +0 -1
  78. package/dist/odata/buildSeparatedArrays.d.ts.map +0 -1
  79. package/dist/odata/dateUtils.d.ts.map +0 -1
  80. package/dist/odata/findCollectionPairs.d.ts.map +0 -1
  81. package/dist/odata/index.d.ts +0 -6
  82. package/dist/odata/index.d.ts.map +0 -1
  83. package/dist/odata/index.js +0 -102
  84. package/dist/odata/index.js.map +0 -1
  85. package/dist/odata/store.d.ts.map +0 -1
  86. package/dist/odata/types.d.ts.map +0 -1
  87. package/src/binary/README.md +0 -110
  88. package/src/docs/browserAndReact.mdx +0 -150
  89. package/src/docs/filesAndExcel.mdx +0 -163
  90. package/src/docs/formatters-runtime.mdx +0 -455
  91. package/src/docs/formatters.mdx +0 -380
  92. package/src/docs/odataAndTables.mdx +0 -178
  93. package/src/docs/pureUtilities.mdx +0 -96
  94. package/src/docs/runtimeAndState.mdx +0 -155
  95. package/src/file/README.md +0 -289
  96. package/src/formatters/date/README.md +0 -520
  97. package/src/formatters/number/README.md +0 -123
  98. package/src/formatters/valueState/README.md +0 -275
  99. package/src/formulas/README.md +0 -42
  100. package/src/pwa/README.md +0 -638
  101. package/src/query-client/README.md +0 -565
@@ -0,0 +1,155 @@
1
+ import { Meta } from "@storybook/addon-docs/blocks";
2
+
3
+ <Meta title="Foundation Lib/Data/Currency" />
4
+
5
+ # Валютные подписи через `@ryuzaki13/react-foundation-lib/currency`
6
+
7
+ Модуль связывает общий OData-параметр `p_curr` с отображаемой подписью показателя. Он определяет один из двух режимов валюты и добавляет соответствующий суффикс к label.
8
+
9
+ ## Содержание
10
+
11
+ - [Назначение](#назначение)
12
+ - [Установка и импорт](#установка-и-импорт)
13
+ - [`CurrencyMode`](#currencymode)
14
+ - [`resolveCurrencyModeFromODataParameters`](#resolvecurrencymodefromodataparameters)
15
+ - [`resolveCurrencyAwareLabel`](#resolvecurrencyawarelabel)
16
+ - [Готовый pipeline](#готовый-pipeline)
17
+ - [Ограничения и ошибки](#ограничения-и-ошибки)
18
+ - [API-справочник](#api-справочник)
19
+
20
+ ## Назначение
21
+
22
+ ```text
23
+ wrapped OData params
24
+ { p_curr: { value: true } }
25
+ │
26
+ ▼
27
+ CurrencyMode "rub"
28
+ │
29
+ ▼
30
+ "Выручка" + ["за тн", "руб/тн"]
31
+ │
32
+ ▼
33
+ "Выручка руб/тн"
34
+ ```
35
+
36
+ Модуль не форматирует денежное число, не конвертирует валюту и не строит OData query. Для чисел используйте `/formatters`, для OData params — `/odata-service`.
37
+
38
+ ## Установка и импорт
39
+
40
+ ```ts
41
+ import { resolveCurrencyAwareLabel, resolveCurrencyModeFromODataParameters } from "@ryuzaki13/react-foundation-lib/currency";
42
+
43
+ import type { CurrencyMode } from "@ryuzaki13/react-foundation-lib/currency";
44
+ ```
45
+
46
+ Модуль ESM, не требует React или browser API. Параметры должны иметь публичную wrapped-форму из `/odata-service`.
47
+
48
+ ## `CurrencyMode`
49
+
50
+ ```ts
51
+ type CurrencyMode = "internal" | "rub";
52
+ ```
53
+
54
+ - `internal` — внутренний режим;
55
+ - `rub` — рублёвый режим.
56
+
57
+ `null` используется resolver-функцией, когда режим определить нельзя.
58
+
59
+ ## `resolveCurrencyModeFromODataParameters`
60
+
61
+ ```ts
62
+ const mode = resolveCurrencyModeFromODataParameters(params);
63
+ ```
64
+
65
+ Функция читает только собственное поле `p_curr.value`:
66
+
67
+ | Вход | Результат |
68
+ | ------------------------------- | ------------ |
69
+ | параметров нет | `null` |
70
+ | поля `p_curr` нет | `null` |
71
+ | `{ p_curr: { value: false } }` | `"internal"` |
72
+ | `{ p_curr: { value: true } }` | `"rub"` |
73
+ | `{ p_curr: { value: "true" } }` | `null` |
74
+ | `{ p_curr: { value: 1 } }` | `null` |
75
+
76
+ Строка `"true"` намеренно не приводится к boolean. Нормализуйте внешний payload на OData boundary, а не внутри UI label.
77
+
78
+ Пример с helper `/odata-service`:
79
+
80
+ ```ts
81
+ import { wrapODataParams } from "@ryuzaki13/react-foundation-lib/odata-service";
82
+
83
+ const params = wrapODataParams({ p_curr: true });
84
+ const mode = resolveCurrencyModeFromODataParameters(params);
85
+ // "rub".
86
+ ```
87
+
88
+ ## `resolveCurrencyAwareLabel`
89
+
90
+ ```ts
91
+ const label = resolveCurrencyAwareLabel("Выручка", ["за тн", "руб/тн"], "rub");
92
+ // "Выручка руб/тн".
93
+ ```
94
+
95
+ Пара `currencyLabels` имеет фиксированный смысл:
96
+
97
+ ```ts
98
+ readonly[(internalLabel, rubLabel)];
99
+ ```
100
+
101
+ | Mode | Индекс суффикса |
102
+ | ---------- | --------------: |
103
+ | `internal` | 0 |
104
+ | `rub` | 1 |
105
+
106
+ Точные правила:
107
+
108
+ - без `currencyLabels` возвращается исходный `label`;
109
+ - при `null`/`undefined` mode возвращается исходный `label`;
110
+ - суффикс обрезается через `trim()`;
111
+ - пустой суффикс не добавляется;
112
+ - обычно между label и суффиксом добавляется один пробел;
113
+ - если обрезанный суффикс начинается с запятой, дополнительный пробел не добавляется.
114
+
115
+ ```ts
116
+ resolveCurrencyAwareLabel("Выручка", [", за тн", ", руб/тн"], "rub");
117
+ // "Выручка, руб/тн".
118
+
119
+ resolveCurrencyAwareLabel("Выручка", ["", " "], "rub");
120
+ // "Выручка".
121
+ ```
122
+
123
+ Исходный `label` не обрезается и не нормализуется.
124
+
125
+ ## Готовый pipeline
126
+
127
+ ```ts
128
+ function buildMetricLabel(
129
+ baseLabel: string,
130
+ currencyLabels: readonly [string, string] | undefined,
131
+ params: WrappedODataParameters | undefined
132
+ ) {
133
+ const mode = resolveCurrencyModeFromODataParameters(params);
134
+ return resolveCurrencyAwareLabel(baseLabel, currencyLabels, mode);
135
+ }
136
+ ```
137
+
138
+ Resolver чистый: одинаковый вход даёт одинаковую строку и не меняет params/labels.
139
+
140
+ ## Ограничения и ошибки
141
+
142
+ - Поддерживается только параметр с точным id `p_curr`.
143
+ - Поддерживаются только два режима; код валюты `USD`, `EUR` и exchange rate не обрабатываются.
144
+ - Функция не проверяет грамматику и единицы измерения.
145
+ - Запятая должна входить в сам суффикс, если нужна форма `"Выручка, руб"`.
146
+ - Не передавайте плоский `{ p_curr: true }`: ожидается `{ p_curr: { value: true } }`.
147
+ - Не используйте label helper для финансовых вычислений.
148
+
149
+ ## API-справочник
150
+
151
+ | Export | Результат |
152
+ | ---------------------------------------- | ----------------------- |
153
+ | `CurrencyMode` | `"internal" \| "rub"` |
154
+ | `resolveCurrencyModeFromODataParameters` | `CurrencyMode \| null` |
155
+ | `resolveCurrencyAwareLabel` | Итоговая display-строка |
@@ -0,0 +1,319 @@
1
+ import { Meta } from "@storybook/addon-docs/blocks";
2
+
3
+ <Meta title="Foundation Lib/Date/Segments" />
4
+
5
+ # Сегменты даты и времени через `@ryuzaki13/react-foundation-lib/date-segments`
6
+
7
+ Модуль разбирает строковую маску на редактируемые сегменты и литералы, преобразует `Date` в значения полей и строго собирает `Date` обратно. Он предназначен для segment-based date/time inputs.
8
+
9
+ Документ объясняет маски, индексы, local time, двухзначный год, неполный ввод и все публичные типы.
10
+
11
+ ## Содержание
12
+
13
+ - [Назначение и границы](#назначение-и-границы)
14
+ - [Установка и импорт](#установка-и-импорт)
15
+ - [Базовые понятия](#базовые-понятия)
16
+ - [Поддерживаемые tokens](#поддерживаемые-tokens)
17
+ - [`parseDateSegmentMask`](#parsedatesegmentmask)
18
+ - [`isEditableDateSegment`](#iseditabledatesegment)
19
+ - [`dateToIndexedSegmentValues`](#datetoindexedsegmentvalues)
20
+ - [`areAllDateSegmentsEmpty`](#arealldatesegmentsempty)
21
+ - [`indexedSegmentsToDate`](#indexedsegmentstodate)
22
+ - [Полный input pipeline](#полный-input-pipeline)
23
+ - [Local time и календарная семантика](#local-time-и-календарная-семантика)
24
+ - [API и ошибки](#api-и-ошибки)
25
+
26
+ ## Назначение и границы
27
+
28
+ ```text
29
+ "dd.MM.yyyy HH:mm"
30
+ │ parseDateSegmentMask
31
+ ▼
32
+ [day, ".", month, ".", year, " ", hours, ":", minutes]
33
+ │ пользователь вводит значения по индексам
34
+ ▼
35
+ Map { 0:"03", 2:"03", 4:"2026", 6:"18", 8:"03" }
36
+ │ indexedSegmentsToDate
37
+ ▼
38
+ new Date(2026, 2, 3, 18, 3)
39
+ ```
40
+
41
+ Модуль не рендерит input, не управляет focus/caret, не форматирует итоговую дату для UI/backend и не парсит ISO/OData строку. Для общего парсинга/форматирования используйте `/formatters`.
42
+
43
+ ## Установка и импорт
44
+
45
+ ```ts
46
+ import {
47
+ areAllDateSegmentsEmpty,
48
+ dateToIndexedSegmentValues,
49
+ indexedSegmentsToDate,
50
+ isEditableDateSegment,
51
+ parseDateSegmentMask
52
+ } from "@ryuzaki13/react-foundation-lib/date-segments";
53
+
54
+ import type {
55
+ DateMaskSegment,
56
+ DateSegmentId,
57
+ EditableDateSegment,
58
+ LiteralDateSegment
59
+ } from "@ryuzaki13/react-foundation-lib/date-segments";
60
+ ```
61
+
62
+ Модуль не зависит от React/DOM и использует локальные getters/constructor стандартного `Date`.
63
+
64
+ ## Базовые понятия
65
+
66
+ Маска состоит из двух видов сегментов.
67
+
68
+ Editable segment:
69
+
70
+ ```ts
71
+ interface EditableDateSegment {
72
+ kind: "editable";
73
+ id: DateSegmentId;
74
+ token: string;
75
+ length: number;
76
+ min: number;
77
+ max: number;
78
+ placeholder: string;
79
+ ariaLabel: string;
80
+ }
81
+ ```
82
+
83
+ Literal segment:
84
+
85
+ ```ts
86
+ interface LiteralDateSegment {
87
+ kind: "literal";
88
+ text: string;
89
+ }
90
+ ```
91
+
92
+ Литералы — точки, двоеточия, пробелы, слова и неподдерживаемые символы маски. Они показываются пользователю, но не имеют редактируемого значения.
93
+
94
+ `DateMaskSegment` — discriminated union. Поле `kind` позволяет безопасно сузить тип.
95
+
96
+ ## Поддерживаемые tokens
97
+
98
+ | Token | `id` | Длина | Диапазон | Placeholder | ARIA label |
99
+ | ------ | --------- | ----: | -------- | ----------- | ---------- |
100
+ | `YYYY` | `year` | 4 | 1–9999 | `ГГГГ` | Год |
101
+ | `yyyy` | `year` | 4 | 1–9999 | `гггг` | Год |
102
+ | `yy` | `year` | 2 | 0–99 | `гг` | Год |
103
+ | `MM` | `month` | 2 | 1–12 | `мм` | Месяц |
104
+ | `DD` | `day` | 2 | 1–31 | `ДД` | День |
105
+ | `dd` | `day` | 2 | 1–31 | `дд` | День |
106
+ | `hh` | `hours` | 2 | 0–23 | `чч` | Часы |
107
+ | `HH` | `hours` | 2 | 0–23 | `чч` | Часы |
108
+ | `mm` | `minutes` | 2 | 0–59 | `мм` | Минуты |
109
+ | `ss` | `seconds` | 2 | 0–59 | `сс` | Секунды |
110
+
111
+ Обратите внимание:
112
+
113
+ - `MM` — месяц, `mm` — минуты;
114
+ - `dd`/`DD` — день;
115
+ - `hh` и `HH` оба означают 24-часовой диапазон `0..23`;
116
+ - одиночные `d`, `M`, `H`, `m`, `s` не являются tokens и станут literal text;
117
+ - tokens регистрозависимы.
118
+
119
+ ## `parseDateSegmentMask`
120
+
121
+ ```ts
122
+ const segments = parseDateSegmentMask("dd.MM.yyyy HH:mm");
123
+ ```
124
+
125
+ Результат:
126
+
127
+ ```ts
128
+ [
129
+ { kind: "editable", id: "day", token: "dd", length: 2 /* ... */ },
130
+ { kind: "literal", text: "." },
131
+ { kind: "editable", id: "month", token: "MM", length: 2 /* ... */ },
132
+ { kind: "literal", text: "." },
133
+ { kind: "editable", id: "year", token: "yyyy", length: 4 /* ... */ },
134
+ { kind: "literal", text: " " },
135
+ { kind: "editable", id: "hours", token: "HH", length: 2 /* ... */ },
136
+ { kind: "literal", text: ":" },
137
+ { kind: "editable", id: "minutes", token: "mm", length: 2 /* ... */ }
138
+ ];
139
+ ```
140
+
141
+ Соседние literal characters объединяются в один segment:
142
+
143
+ ```ts
144
+ parseDateSegmentMask("Дата: dd");
145
+ // literal "Дата: ", затем editable day.
146
+ ```
147
+
148
+ Пустая маска возвращает пустой массив. Parser не валидирует обязательность года/месяца/дня и допускает повтор одного `id`; корректный набор задаёт host.
149
+
150
+ ## `isEditableDateSegment`
151
+
152
+ Type guard:
153
+
154
+ ```ts
155
+ const editable = segments.filter(isEditableDateSegment);
156
+ // EditableDateSegment[].
157
+ ```
158
+
159
+ Он проверяет только `segment.kind === "editable"` и рассчитан на значения, полученные из parser или соответствующие публичному типу.
160
+
161
+ ## `dateToIndexedSegmentValues`
162
+
163
+ Функция создаёт `Map<number, string>`, где key — индекс segment в полном массиве, включая literals.
164
+
165
+ ```ts
166
+ const segments = parseDateSegmentMask("dd.MM.yyyy HH:mm");
167
+ const values = dateToIndexedSegmentValues(new Date(2026, 2, 3, 18, 3), segments);
168
+
169
+ console.log([...values.entries()]);
170
+ // [[0,"03"], [2,"03"], [4,"2026"], [6,"18"], [8,"03"]]
171
+ ```
172
+
173
+ Индексы `1`, `3`, `5`, `7` отсутствуют, потому что это literals.
174
+
175
+ Для `null`, `undefined` или invalid `Date` каждая editable позиция получает `""`:
176
+
177
+ ```ts
178
+ dateToIndexedSegmentValues(null, segments);
179
+ dateToIndexedSegmentValues(new Date("bad"), segments);
180
+ ```
181
+
182
+ Двухзначный year берёт последние две цифры полного года. Значения дополняются нулями слева до длины segment.
183
+
184
+ ## `areAllDateSegmentsEmpty`
185
+
186
+ ```ts
187
+ const empty = areAllDateSegmentsEmpty(segments, values);
188
+ ```
189
+
190
+ Возвращает `true`, если у каждого editable segment значение отсутствует или является пустой строкой.
191
+
192
+ Literal segments игнорируются. Строка из пробелов truthy и считается заполненной. Функция не проверяет длину/числовой формат.
193
+
194
+ ## `indexedSegmentsToDate`
195
+
196
+ ```ts
197
+ const date = indexedSegmentsToDate(segments, values);
198
+ ```
199
+
200
+ Возвращает строгий valid `Date` или `null`.
201
+
202
+ ### Полный ввод
203
+
204
+ Каждый editable segment должен иметь строку длиной не меньше `segment.length`:
205
+
206
+ ```ts
207
+ values.set(dayIndex, "3"); // Для token dd длины недостаточно.
208
+ indexedSegmentsToDate(segments, values); // null.
209
+ ```
210
+
211
+ Проверка длины не гарантирует numeric input. `"ab"` пройдёт length-check, но `parseInt` не даст valid component и итог будет `null`.
212
+
213
+ ### Календарная строгость
214
+
215
+ После сборки компоненты сверяются с полученным `Date`:
216
+
217
+ ```ts
218
+ // 31 февраля не превращается автоматически в март.
219
+ indexedSegmentsToDate(segments, february31Values); // null.
220
+
221
+ // 24:00 не принимается.
222
+ indexedSegmentsToDate(segments, hour24Values); // null.
223
+ ```
224
+
225
+ Проверяются month `1..12`, day `1..31`, hours `0..23`, minutes/seconds `0..59`, а затем реальная календарная дата.
226
+
227
+ ### Отсутствующие части и `defaultDate`
228
+
229
+ Если маска не содержит некоторый тип segment, компонент берётся из `defaultDate`:
230
+
231
+ ```ts
232
+ const timeSegments = parseDateSegmentMask("HH:mm");
233
+ const values = new Map([
234
+ [0, "09"],
235
+ [2, "15"]
236
+ ]);
237
+
238
+ const result = indexedSegmentsToDate(timeSegments, values, {
239
+ defaultDate: new Date(2026, 2, 3, 18, 3)
240
+ });
241
+ // 3 марта 2026, 09:15 local time.
242
+ ```
243
+
244
+ Default fallback без options — `new Date(1970, 0, 1, 0, 0, 0, 0)` в local timezone.
245
+
246
+ `defaultDate` не валидируется отдельно. Invalid fallback обычно приведёт к `null` после итоговой проверки.
247
+
248
+ ### Двухзначный год
249
+
250
+ Для token `yy`:
251
+
252
+ | Ввод | Полный год |
253
+ | --------- | ---------: |
254
+ | `00`–`69` | 2000–2069 |
255
+ | `70`–`99` | 1970–1999 |
256
+
257
+ Порог фиксирован и не зависит от текущего года.
258
+
259
+ ## Полный input pipeline
260
+
261
+ ```tsx
262
+ const segments = parseDateSegmentMask("dd.MM.yyyy");
263
+ const [values, setValues] = useState(() => dateToIndexedSegmentValues(initialDate, segments));
264
+
265
+ function updateSegment(index: number, value: string) {
266
+ setValues((current) => {
267
+ const next = new Map(current);
268
+ next.set(index, value);
269
+ return next;
270
+ });
271
+ }
272
+
273
+ function commit() {
274
+ if (areAllDateSegmentsEmpty(segments, values)) {
275
+ onChange(null);
276
+ return;
277
+ }
278
+
279
+ const nextDate = indexedSegmentsToDate(segments, values);
280
+ if (nextDate === null) {
281
+ showValidationError("Введите корректную дату");
282
+ return;
283
+ }
284
+
285
+ onChange(nextDate);
286
+ }
287
+ ```
288
+
289
+ UI самостоятельно отвечает за digits-only input, `aria-label`, focus movement, paste, validation message и controlled state.
290
+
291
+ ## Local time и календарная семантика
292
+
293
+ Функции используют `getFullYear`, `getMonth`, `getDate`, `getHours` и `new Date(year, ...)`. Это local calendar/time, а не UTC.
294
+
295
+ Не смешивайте segment input с автоматическим timezone conversion. Перед отправкой в OData/backend примените formatter, соответствующий контракту API.
296
+
297
+ ## API и ошибки
298
+
299
+ | Export | Результат |
300
+ | ---------------------------- | ------------------------------------ |
301
+ | `parseDateSegmentMask` | `DateMaskSegment[]` |
302
+ | `isEditableDateSegment` | Type guard |
303
+ | `dateToIndexedSegmentValues` | `Map<number, string>` |
304
+ | `areAllDateSegmentsEmpty` | `boolean` |
305
+ | `indexedSegmentsToDate` | `Date \| null` |
306
+ | `DateSegmentId` | year/month/day/hours/minutes/seconds |
307
+ | `EditableDateSegment` | Metadata редактируемого segment |
308
+ | `LiteralDateSegment` | `{ kind: "literal"; text: string }` |
309
+ | `DateMaskSegment` | Union editable/literal |
310
+
311
+ Типичные ошибки:
312
+
313
+ - Перепутать `MM` и `mm`.
314
+ - Хранить values по порядковому номеру editable fields вместо полного segment index.
315
+ - Передать однозначную строку `"3"` для token длины `2` и ожидать автодополнение при сборке.
316
+ - Ожидать UTC.
317
+ - Считать `min/max` metadata заменой UI input validation.
318
+ - Использовать произвольную маску с повторяющимися year/month segments.
319
+ - Нормализовать пользовательский partial draft до команды commit.