@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,100 @@
1
+ import { Meta } from "@storybook/addon-docs/blocks";
2
+
3
+ <Meta title="Foundation Lib/Browser/Client Fingerprint" />
4
+
5
+ # Client fingerprint через `@ryuzaki13/react-foundation-lib/fingerprint`
6
+
7
+ `getClientFingerprint` лениво загружает FingerprintJS, получает `visitorId` и кеширует Promise на всё время жизни JavaScript-модуля.
8
+
9
+ ## Установка и импорт
10
+
11
+ ```bash
12
+ npm install @ryuzaki13/react-foundation-lib @fingerprintjs/fingerprintjs
13
+ ```
14
+
15
+ ```ts
16
+ import { getClientFingerprint } from "@ryuzaki13/react-foundation-lib/fingerprint";
17
+ ```
18
+
19
+ Host должен разрешить peer dependency `@fingerprintjs/fingerprintjs >=5 <6`.
20
+
21
+ ## Использование
22
+
23
+ ```ts
24
+ const fingerprint = await getClientFingerprint();
25
+
26
+ if (fingerprint) {
27
+ attachAnonymousDiagnosticId(fingerprint);
28
+ }
29
+ ```
30
+
31
+ Результат `Promise<string | null>`:
32
+
33
+ - непустой trimmed `visitorId`, если библиотека успешно отработала;
34
+ - `null` вне browser;
35
+ - `null`, если load/get завершились ошибкой;
36
+ - `null`, если visitorId пустой после trim.
37
+
38
+ ## Кеширование
39
+
40
+ Первый browser-вызов создаёт одну Promise:
41
+
42
+ ```text
43
+ первый вызов ──► load FingerprintJS ──► fp.get() ──► string/null
44
+ │
45
+ следующие вызовы ─────────────────────► та же Promise
46
+ ```
47
+
48
+ Преимущества:
49
+
50
+ - параллельные consumers не запускают повторное вычисление;
51
+ - после успеха возвращается то же значение;
52
+ - после ошибки `null` также кешируется и автоматического retry нет.
53
+
54
+ Публичного reset/retry API нет. Reload страницы создаёт новый module runtime.
55
+
56
+ SSR-вызов возвращает `null` сразу и не заполняет browser promise cache. Поэтому последующий client runtime может выполнить настоящее вычисление.
57
+
58
+ ## Правильная React boundary
59
+
60
+ Не вызывайте async функцию во время render:
61
+
62
+ ```tsx
63
+ useEffect(() => {
64
+ let active = true;
65
+
66
+ void getClientFingerprint().then((value) => {
67
+ if (active) setFingerprint(value);
68
+ });
69
+
70
+ return () => {
71
+ active = false;
72
+ };
73
+ }, []);
74
+ ```
75
+
76
+ Если fingerprint нужен backend request, предпочтительнее интегрировать его в app/API boundary, а не дублировать state по компонентам.
77
+
78
+ ## Privacy и security
79
+
80
+ Fingerprint:
81
+
82
+ - не является authentication token;
83
+ - не доказывает личность пользователя;
84
+ - может меняться после обновления browser/device/settings;
85
+ - может совпасть у разных клиентов;
86
+ - не заменяет server session;
87
+ - требует правового/продуктового решения о consent и retention;
88
+ - не должен использоваться для скрытого tracking вопреки пользовательской политике.
89
+
90
+ Не помещайте fingerprint в публичный URL, DOM или logs без необходимости. При передаче на backend используйте HTTPS и установленную privacy policy.
91
+
92
+ ## Ошибки и тестирование
93
+
94
+ Функция намеренно поглощает ошибки FingerprintJS и возвращает `null`. Это graceful optional capability; если fingerprint обязателен для business flow, host должен явно обработать `null`.
95
+
96
+ Module-level cache усложняет повторные тесты. Используйте module reset (`vi.resetModules`) перед сценарием, которому нужен чистый первый вызов.
97
+
98
+ ```ts
99
+ function getClientFingerprint(): Promise<string | null>;
100
+ ```
@@ -0,0 +1,101 @@
1
+ import { Meta } from "@storybook/addon-docs/blocks";
2
+
3
+ <Meta title="Foundation Lib/Forms/Form Errors" />
4
+
5
+ # Чтение первой ошибки формы через `@ryuzaki13/react-foundation-lib/form`
6
+
7
+ Модуль содержит одну функцию `readFirstFormError`. Она получает неоднородный массив ошибок и возвращает первое найденное текстовое сообщение.
8
+
9
+ ## Установка и импорт
10
+
11
+ ```ts
12
+ import { readFirstFormError } from "@ryuzaki13/react-foundation-lib/form";
13
+ ```
14
+
15
+ Модуль ESM, не зависит от React и не привязан к конкретной form library.
16
+
17
+ ## Поддерживаемые формы ошибки
18
+
19
+ Функция понимает:
20
+
21
+ 1. Строку.
22
+ 2. Объект с непустым строковым полем `message`.
23
+ 3. Вложенный массив таких значений.
24
+
25
+ ```ts
26
+ readFirstFormError([undefined, { message: "Поле обязательно" }]);
27
+ // "Поле обязательно".
28
+
29
+ readFirstFormError([[null, "Неверный формат"], "Другая ошибка"]);
30
+ // "Неверный формат".
31
+ ```
32
+
33
+ Обычный `Error` также подходит, потому что имеет строковое поле `message`:
34
+
35
+ ```ts
36
+ readFirstFormError([new Error("Ошибка проверки")]);
37
+ // "Ошибка проверки".
38
+ ```
39
+
40
+ ## Алгоритм
41
+
42
+ ```text
43
+ errors отсутствует ──► undefined
44
+ │
45
+ ▼
46
+ обход слева направо
47
+ │
48
+ ├─ string ─────────► вернуть строку
49
+ ├─ array ──────────► рекурсивно найти первую строку
50
+ ├─ { message } ────► вернуть непустой string message
51
+ └─ другое ─────────► пропустить
52
+ ```
53
+
54
+ ```ts
55
+ const message = readFirstFormError(errors);
56
+
57
+ if (message) {
58
+ showFieldError(message);
59
+ }
60
+ ```
61
+
62
+ ## Точные правила
63
+
64
+ - `undefined` вместо массива возвращает `undefined`.
65
+ - Пустой массив возвращает `undefined`.
66
+ - Поиск идёт в исходном порядке.
67
+ - Пустая строка не является результатом и поиск продолжается.
68
+ - Строка из пробелов считается непустой и возвращается буквально.
69
+ - Числа, boolean, functions, `null` и объекты без string `message` пропускаются.
70
+ - Массивы обходятся рекурсивно без ограничения глубины.
71
+ - Входные массивы и objects не изменяются.
72
+
73
+ ## Пример в React
74
+
75
+ ```tsx
76
+ type FieldErrorProps = {
77
+ errors?: readonly unknown[];
78
+ };
79
+
80
+ export function FieldError({ errors }: FieldErrorProps) {
81
+ const message = readFirstFormError(errors);
82
+
83
+ return message ? <p role="alert">{message}</p> : null;
84
+ }
85
+ ```
86
+
87
+ Для нескольких ошибок, summary списка, локализации и сопоставления ошибки конкретному field нужен отдельный form-layer host-приложения.
88
+
89
+ ## Ограничения и ошибки
90
+
91
+ - Функция не выполняет trim или перевод сообщений.
92
+ - Она не проверяет schema form-library.
93
+ - Возвращается только первое сообщение; остальные теряются для этого вызова.
94
+ - Циклический массив приведёт к бесконечной рекурсии/ошибке call stack.
95
+ - Не передавайте backend payload напрямую без runtime-проверки и безопасного отображения.
96
+
97
+ ## API
98
+
99
+ ```ts
100
+ function readFirstFormError(errors: readonly unknown[] | undefined): string | undefined;
101
+ ```
@@ -0,0 +1,469 @@
1
+ import { Meta } from "@storybook/addon-docs/blocks";
2
+
3
+ <Meta title="Foundation Lib/API/Formatters/Обзор" />
4
+
5
+ # `@ryuzaki13/react-foundation-lib/formatters`
6
+
7
+ `formatters` — большой модуль подготовки значений для интерфейса и transport-контрактов. Он работает с датами, числами, строками, boolean/ABAP-флагами, ФИО, российскими телефонами, ведущими нулями и семантическими состояниями. Для таблиц модуль также предоставляет конфигурируемый formatter pipeline и row-based формулы.
8
+
9
+ Документация разделена по предметным областям, чтобы разработчику не приходилось читать один огромный файл. Этот README объясняет общие правила, помогает выбрать API и подробно описывает небольшие helpers верхнего уровня.
10
+
11
+ ## Установка
12
+
13
+ ```bash
14
+ npm install @ryuzaki13/react-foundation-lib
15
+ ```
16
+
17
+ Пакет является ESM. Корневого импорта пакета нет. Все formatter functions и их типы импортируются из точечного entrypoint:
18
+
19
+ ```ts
20
+ import { formatDateAsDate, formatNumberAsDecimal2, normalizeText } from "@ryuzaki13/react-foundation-lib/formatters";
21
+ ```
22
+
23
+ Некоторые общие типы принадлежат `/types`:
24
+
25
+ ```ts
26
+ import type { AbapBoolean, State } from "@ryuzaki13/react-foundation-lib/types";
27
+ ```
28
+
29
+ Вложенные каталоги исходников не являются package subpaths:
30
+
31
+ ```ts
32
+ // Неправильно: таких exports нет.
33
+ // import { formatDate } from "@ryuzaki13/react-foundation-lib/formatters/date";
34
+ // import { formatNumber } from "@ryuzaki13/react-foundation-lib/formatters/number";
35
+
36
+ // Правильно.
37
+ import { formatDate, formatNumber } from "@ryuzaki13/react-foundation-lib/formatters";
38
+ ```
39
+
40
+ ## Карта документации
41
+
42
+ | Область | README | Что внутри |
43
+ | -------------------- | ------------------------------------------------ | ----------------------------------------------------------------- |
44
+ | Boolean и ABAP-флаги | [boolean/README.mdx](./boolean/README.mdx) | `parseAbapBoolean`, `parseBoolean`, `toAbapBoolean`, truthy/falsy |
45
+ | Даты и диапазоны | [date/README.mdx](./date/README.mdx) | parsing, formatting, timezone, presets, calendar, education dates |
46
+ | Числа | [number/README.mdx](./number/README.mdx) | presets, parsing SAP-like строк, compact, zero semantics |
47
+ | Строки | [strings/README.mdx](./strings/README.mdx) | trim, spaces, leading zeros, truncate, русские plural forms |
48
+ | Value state | [valueState/README.mdx](./valueState/README.mdx) | fixed/threshold resolvers, registry, CSS presentation |
49
+ | Row-based формулы | [rowBased/README.mdx](./rowBased/README.mdx) | formula context, registry, dependencies и purity |
50
+ | Formatter pipeline | [pipeline/README.mdx](./pipeline/README.mdx) | plan/graph, validation, compilation, runtime ячеек |
51
+
52
+ Все эти области остаются частью одного публичного `/formatters` entrypoint. Деление относится к документации и организации исходников, а не к способу импорта.
53
+
54
+ ## Быстрый выбор функции
55
+
56
+ | Нужно | Использовать |
57
+ | ------------------------------------------- | --------------------------------------------------------- |
58
+ | Показать дату | `formatDateAsDate`, `formatDate` |
59
+ | Прочитать календарную дату | `parseDate`, `parseDateValue` |
60
+ | Прочитать абсолютный timestamp с timezone | `parseDateTZ`, `parseDateValueTZ` |
61
+ | Показать число | `formatNumberAs*`, `formatNumber` |
62
+ | Прочитать строку `"1 234,5"` как число | `toFiniteNumber` или `parseNumber` |
63
+ | Нормализовать необязательный текст | `normalizeText` |
64
+ | Схлопнуть пробелы в пользовательском тексте | `normalizeTextSpaces` |
65
+ | Выбрать `файл/файла/файлов` | `selectRussianPluralForm`, `formatRussianPlural` |
66
+ | Прочитать ABAP `X` | `parseAbapBoolean` |
67
+ | Сформировать ABAP boolean | `toAbapBoolean` |
68
+ | Нормализовать ФИО | `formatFullName`, `formatShortName` |
69
+ | Показать российский телефон | `formatPhone` |
70
+ | Получить условно очищенный телефон | `clearPhone` |
71
+ | Дополнить числовую строку нулями | `normalizeLeadingZeros` или `normalizeLeadingZerosStrict` |
72
+ | Вычислить visual state | `createFixedResolver`, `createThresholdResolver` |
73
+ | Форматировать ячейку по сериализуемой схеме | formatter pipeline |
74
+
75
+ ## Общие правила
76
+
77
+ ### Formatter обычно не является validator-ом
78
+
79
+ Функция может иметь fallback для невалидного значения, но это не делает данные валидными:
80
+
81
+ ```ts
82
+ formatNumber("abc", "decimal"); // "0"
83
+ formatDateAsDate("abc"); // ""
84
+ ```
85
+
86
+ Если нужно сообщить пользователю об ошибке, сначала валидируйте или парсите вход, а затем форматируйте успешный результат.
87
+
88
+ ### `unknown` требует понимания coercion
89
+
90
+ Некоторые функции принимают `unknown`, чтобы безопасно работать с внешними данными, но внутри используют разные правила:
91
+
92
+ - `formatNumber` применяет `Number(value)`;
93
+ - `toFiniteNumber` принимает только number или строку и понимает SAP-like separators;
94
+ - `parseBoolean` использует whitelist для строк и truthy/falsy для остальных значений;
95
+ - fixed value state сравнивает `String(value)`;
96
+ - `normalizeText` принимает только настоящий string.
97
+
98
+ Не переносите ожидания от одной функции на другую.
99
+
100
+ ### Нормализуйте на явной границе
101
+
102
+ Функции меняют значения: обрезают пробелы, меняют регистр, удаляют нули или форматируют числа. Для формы обычно разумно хранить пользовательский draft буквально, а нормализацию выполнять при сохранении/восстановлении или другом явно выбранном переходе.
103
+
104
+ ### Глобальные реестры настраиваются один раз
105
+
106
+ Date/number presets, value-state resolvers и row-based formulas хранятся на уровне загруженного модуля. Регистрируйте их в bootstrap-коде до построения formatter pipeline:
107
+
108
+ ```ts
109
+ export function configureApplicationFormatters(): void {
110
+ registerNumberPreset({ name: "quantity-4", decimals: 4 });
111
+ registerDatePreset({ name: "document-date", pattern: "dd.MM.yyyy" });
112
+
113
+ configureRowBasedFormatterRegistry(createRowBasedFormatterRegistry(rowBasedDefinitions));
114
+ }
115
+ ```
116
+
117
+ Функции `reset*` предназначены в основном для тестов. Они могут инвалидировать runtime, который уже хранит ссылки или ids.
118
+
119
+ ### Компилируйте горячий runtime заранее
120
+
121
+ Presets и formatter pipeline рассчитаны на множество ячеек. Не регистрируйте правила и не компилируйте pipeline в цикле рендера. Подготовьте стабильные объекты один раз, затем вызывайте готовый formatter/executor.
122
+
123
+ ## ФИО
124
+
125
+ ### `formatFullName(fullName)`
126
+
127
+ ```ts
128
+ function formatFullName(fullName: string): string;
129
+ ```
130
+
131
+ Функция пытается привести ФИО к виду `Фамилия Имя Отчество`:
132
+
133
+ ```ts
134
+ formatFullName(" иВАНОВ иВАН иВАНОВИЧ ");
135
+ // "Иванов Иван Иванович"
136
+
137
+ formatFullName("петров"); // "Петров"
138
+ ```
139
+
140
+ Обработка:
141
+
142
+ 1. falsy string возвращает `""`;
143
+ 2. внешние пробелы удаляются;
144
+ 3. последовательности whitespace схлопываются;
145
+ 4. строка приводится к нижнему регистру;
146
+ 5. у каждой из первых трёх частей первая code unit переводится в верхний регистр.
147
+
148
+ Функция предполагает порядок `фамилия имя отчество` и учитывает максимум три space-separated части. Всё после третьей части игнорируется.
149
+
150
+ ```ts
151
+ formatFullName("Иванов Иван Иванович младший");
152
+ // "Иванов Иван Иванович"
153
+ ```
154
+
155
+ Она не знает сложные правила имён, дефисы, частицы, locale и правильный регистр вроде `МакДональд`. `toLowerCase` + первая заглавная может испортить нестандартное написание. Используйте только для данных с ожидаемой простой русской структурой.
156
+
157
+ Инициалы обрабатываются минимально, без восстановления полного имени:
158
+
159
+ ```ts
160
+ formatFullName("Иванов И.И."); // "Иванов И И."
161
+ ```
162
+
163
+ Поэтому функция не является parser-ом формата `Фамилия И.О.` в полноценное ФИО.
164
+
165
+ ### `formatShortName(fullName)`
166
+
167
+ ```ts
168
+ function formatShortName(fullName: string): string;
169
+ ```
170
+
171
+ Создаёт вид `Фамилия И.О.` из первых трёх частей:
172
+
173
+ ```ts
174
+ formatShortName("иванов иван иванович"); // "Иванов И.И."
175
+ formatShortName("петров петр"); // "Петров П."
176
+ formatShortName("сидоров"); // "Сидоров"
177
+ ```
178
+
179
+ Требуется корректный порядок частей. Готовая строка с двумя инициалами в одном token не разбирается полноценно:
180
+
181
+ ```ts
182
+ formatShortName("Иванов И.И."); // "Иванов И."
183
+ ```
184
+
185
+ Пустой token после необычной пунктуации может привести к неожиданному результату; передавайте обычную строку ФИО, а не произвольный текст.
186
+
187
+ ### Когда не использовать helpers ФИО
188
+
189
+ - для международных имён с другим порядком;
190
+ - для юридически значимого написания;
191
+ - для восстановления ФИО из инициалов;
192
+ - когда четвёртая и последующие части значимы;
193
+ - когда регистр должен сохраняться буквально.
194
+
195
+ Для структурированных данных предпочтительнее хранить `surname`, `name`, `patronymic` отдельно и собирать нужное представление явно.
196
+
197
+ ## Российский телефон
198
+
199
+ ### `clearPhone(number)`
200
+
201
+ ```ts
202
+ function clearPhone(number: string): string;
203
+ ```
204
+
205
+ Функция:
206
+
207
+ 1. удаляет все нецифровые символы;
208
+ 2. оставляет первые 11 цифр;
209
+ 3. удаляет первый символ, если это `7` или `8`;
210
+ 4. добавляет префикс `+7`.
211
+
212
+ ```ts
213
+ clearPhone("8 (912) 345-67-89"); // "+79123456789"
214
+ clearPhone("+7 912 345-67-89 доб. 42"); // "+79123456789"
215
+ ```
216
+
217
+ Название `clearPhone` не означает строгую очистку до валидного E.164. Функция всегда добавляет `+7` и не проверяет количество цифр:
218
+
219
+ ```ts
220
+ clearPhone(""); // "+7"
221
+ clearPhone("123"); // "+7123"
222
+ ```
223
+
224
+ Если передать 11 цифр, которые начинаются не с `7` и не с `8`, все 11 останутся после добавленного `+7`, то есть результат не будет валидным российским номером.
225
+
226
+ ### `formatPhone(number)`
227
+
228
+ Форматирует только результат, который строго совпал с `+7` и десятью цифрами:
229
+
230
+ ```ts
231
+ formatPhone("8 (912) 345-67-89");
232
+ // "+7 (912) 345-67-89"
233
+ ```
234
+
235
+ Если после `clearPhone` не получился полный шаблон, возвращается исходная строка `number`, а не частично очищённое значение:
236
+
237
+ ```ts
238
+ formatPhone("123"); // "123"
239
+ ```
240
+
241
+ Это display helper, а не validator. Для формы телефона отдельно проверяйте контракт номера и храните явное transport-представление.
242
+
243
+ ## Ведущие нули
244
+
245
+ В модуле есть две функции с различной семантикой.
246
+
247
+ ### `normalizeLeadingZeros(value, fixed?)`
248
+
249
+ ```ts
250
+ function normalizeLeadingZeros(value: unknown, fixed?: number): unknown;
251
+ ```
252
+
253
+ Сначала функция приводит значение к числу через `Number(value)`. Если результат не finite, исходное значение возвращается без изменений. Затем число снова превращается в строку, поэтому исходное форматирование теряется.
254
+
255
+ При положительном `fixed` целая часть дополняется слева:
256
+
257
+ ```ts
258
+ normalizeLeadingZeros("42", 5); // "00042"
259
+ normalizeLeadingZeros(-12.3, 4); // "-0012.3"
260
+ normalizeLeadingZeros("00042", 5); // "00042"
261
+ ```
262
+
263
+ При отсутствии положительного `fixed` возвращается `String(Number(value))`:
264
+
265
+ ```ts
266
+ normalizeLeadingZeros("00042"); // "42"
267
+ normalizeLeadingZeros("00042", 0); // "42"
268
+ normalizeLeadingZeros("00042", -1); // "42"
269
+ ```
270
+
271
+ Это важное отличие от комментария «ничего не делает»: для parseable входа функция всё равно нормализует его через number/string.
272
+
273
+ JavaScript coercion также означает:
274
+
275
+ ```ts
276
+ normalizeLeadingZeros(null); // "0"
277
+ normalizeLeadingZeros(""); // "0"
278
+ normalizeLeadingZeros(false); // "0"
279
+ normalizeLeadingZeros(true); // "1"
280
+ normalizeLeadingZeros(undefined); // undefined
281
+ ```
282
+
283
+ Используйте положительное целое `fixed`. Дробное, бесконечное или экстремально большое значение не является поддержанным пользовательским контрактом.
284
+
285
+ ### `normalizeLeadingZerosStrict(value, fixed?)`
286
+
287
+ ```ts
288
+ function normalizeLeadingZerosStrict<T>(value: T, fixed?: number): T;
289
+ ```
290
+
291
+ Эта версия предназначена для строк с SAP-like grouping/decimal separators и старается сохранить тип входа.
292
+
293
+ Если `fixed` отсутствует, равен `0`, `NaN` или бесконечности, возвращается строго исходное значение:
294
+
295
+ ```ts
296
+ const input = " 001 234,50 ";
297
+ normalizeLeadingZerosStrict(input); // та же строка с пробелами
298
+ ```
299
+
300
+ Для строк:
301
+
302
+ - проверяется наличие хотя бы одной цифры;
303
+ - число должно успешно читаться через `toFiniteNumber`;
304
+ - внешние пробелы удаляются;
305
+ - grouping spaces из целой части удаляются;
306
+ - первый `.` или `,` и дробная часть сохраняются;
307
+ - `fixed < 0` удаляет ведущие нули;
308
+ - `fixed > 0` дополняет целую часть до длины.
309
+
310
+ ```ts
311
+ normalizeLeadingZerosStrict(" 00012,50 ", -1); // "12,50"
312
+ normalizeLeadingZerosStrict("12,50", 5); // "00012,50"
313
+ normalizeLeadingZerosStrict("1 234,50", 6); // "001234,50"
314
+ normalizeLeadingZerosStrict("not-a-number", 5); // исходная строка
315
+ ```
316
+
317
+ Для number функция возвращает number. Поэтому видимые ведущие нули исчезают при обратном преобразовании:
318
+
319
+ ```ts
320
+ normalizeLeadingZerosStrict(12, 5); // 12, не "00012"
321
+ ```
322
+
323
+ Остальные типы (`null`, object, boolean и т. п.) возвращаются без изменений.
324
+
325
+ Внутри `fixed` приводится к 32-bit integer через bitwise operation. Передавайте небольшое целое число; дробные и очень большие значения могут быть преобразованы неожиданно.
326
+
327
+ ### Как выбрать
328
+
329
+ | Ситуация | Выбор |
330
+ | ------------------------------------------------- | ------------------------------------------------------- |
331
+ | Простое значение, допустим `Number(value)` | `normalizeLeadingZeros` |
332
+ | Нужно сохранить строковый `,` и fraction | `normalizeLeadingZerosStrict` |
333
+ | Есть grouping spaces | `normalizeLeadingZerosStrict` |
334
+ | Number должен остаться number | `normalizeLeadingZerosStrict` |
335
+ | Нужен гарантированный display с нулями для number | сначала явно `String`, затем strict или обычная функция |
336
+
337
+ Не применяйте ведущие нули к бизнес-идентификаторам без уверенности: `00123` может быть отдельным значимым кодом, а не числом.
338
+
339
+ ## Boolean
340
+
341
+ Три функции имеют намеренно разные контракты:
342
+
343
+ | API | Краткое правило |
344
+ | ------------------ | ---------------------------------------------------------------------------- |
345
+ | `parseAbapBoolean` | `true` только для точного `"X"` |
346
+ | `parseBoolean` | строки `true`, `x`, `1` без учёта регистра; прочие values через truthy/falsy |
347
+ | `toAbapBoolean` | `"X"` или `" "`; среди строк только точное `"0"` считается false |
348
+
349
+ Последнее правило особенно неочевидно: `toAbapBoolean("")` и `toAbapBoolean("false")` возвращают `"X"`. Полная таблица и безопасные recipes приведены в [boolean/README.mdx](./boolean/README.mdx).
350
+
351
+ ## Строки
352
+
353
+ Строковое семейство включает:
354
+
355
+ - `normalizeText*` и `toSafeString`;
356
+ - `normalizeTextSpaces` и `stripInnerSpaces`;
357
+ - `stripLeadingZeros` и `startsWithIgnoringZeros`;
358
+ - `truncateText`;
359
+ - русские plural forms.
360
+
361
+ Точные различия пустой строки, fallback, whitespace и Unicode описаны в [strings/README.mdx](./strings/README.mdx).
362
+
363
+ ## Даты
364
+
365
+ Date API различает floating calendar и instant timezone parsing, поддерживает ABAP/OData, Intl/pattern presets, ranges, periods и учебный календарь. Начните с [date/README.mdx](./date/README.mdx), особенно с раздела о timezone.
366
+
367
+ ## Числа
368
+
369
+ Числовой API различает `formatNumber` с обычным `Number(value)` и SAP-like parser `toFiniteNumber`. Presets, compact formatting, `OrEmpty` и registry lifecycle подробно разобраны в [number/README.mdx](./number/README.mdx).
370
+
371
+ ## Value state
372
+
373
+ Fixed resolver сопоставляет точные строковые keys, threshold resolver делит число на сегменты. Registry ids, boundary semantics и presentation helpers описаны в [valueState/README.mdx](./valueState/README.mdx).
374
+
375
+ ## Formatter pipeline
376
+
377
+ Pipeline последовательно применяет `rowBasedOverride`, leading zeros, value state и typed formatting. Он поддерживает сериализуемые plan/graph configs, validation, dependency collection и construction-stage compilation.
378
+
379
+ - [pipeline/README.mdx](./pipeline/README.mdx) — основной runtime и config API;
380
+ - [rowBased/README.mdx](./rowBased/README.mdx) — host-defined formulas и их registry.
381
+
382
+ ## Пример единой конфигурации приложения
383
+
384
+ ```ts
385
+ import {
386
+ configureRowBasedFormatterRegistry,
387
+ createRowBasedFormatterRegistry,
388
+ registerDatePreset,
389
+ registerNumberPreset,
390
+ type RowBasedFormatterDefinition
391
+ } from "@ryuzaki13/react-foundation-lib/formatters";
392
+
393
+ const rowBasedDefinitions: RowBasedFormatterDefinition[] = [
394
+ {
395
+ id: "completion-percent",
396
+ name: "Процент выполнения",
397
+ description: "actual / planned * 100; dependencies: planned, actual",
398
+ fn: (ctx) => {
399
+ const planned = ctx.num(0);
400
+ return planned === 0 ? undefined : (ctx.num(1) / planned) * 100;
401
+ }
402
+ }
403
+ ];
404
+
405
+ export function configureApplicationFormatters(): void {
406
+ registerDatePreset({
407
+ name: "document-date",
408
+ pattern: "dd.MM.yyyy",
409
+ invalidFallback: "—"
410
+ });
411
+
412
+ registerNumberPreset({
413
+ name: "quantity-4",
414
+ decimals: 4
415
+ });
416
+
417
+ configureRowBasedFormatterRegistry(createRowBasedFormatterRegistry(rowBasedDefinitions));
418
+ }
419
+ ```
420
+
421
+ Вызовите bootstrap-функцию до чтения пользовательских formatter configs и компиляции runtime fields.
422
+
423
+ ## Тестирование formatter-кода
424
+
425
+ Проверяйте не только «обычный» пример:
426
+
427
+ - `null`, `undefined`, `""`, whitespace;
428
+ - неправильный тип;
429
+ - `NaN`, `Infinity`, отрицательный ноль;
430
+ - границы threshold и date ranges;
431
+ - timezone среды для date parsing;
432
+ - NBSP/NNBSP в числах и строках;
433
+ - неизвестный preset/formula id;
434
+ - reset глобального реестра между тестами;
435
+ - точный тип результата (`number`, `string`, `undefined`).
436
+
437
+ Formatter output часто содержит locale-specific неразрывный пробел. В тесте лучше сравнивать точный ожидаемый Unicode-контракт либо явно нормализовать только тогда, когда это разрешено задачей.
438
+
439
+ ## Частые архитектурные ошибки
440
+
441
+ ### Deep import из исходников
442
+
443
+ В consumer разрешён только опубликованный `/formatters`. Внутренние пути могут исчезнуть и не входят в npm `exports`.
444
+
445
+ ### Своя копия formatter-а в приложении
446
+
447
+ Если правило является общим техническим и уже покрыто модулем, используйте существующий API. Если действительно не хватает общего контракта, расширяйте foundation package и его публичный `index.ts`, а не копируйте helper локально.
448
+
449
+ ### Смешивание display и transport
450
+
451
+ `formatPhone` создаёт человекочитаемый телефон, `formatDateAsDateLong` — локализованную дату, `formatNumberAsDecimal2` — UI-число. Не отправляйте display strings в API без явного transport-контракта.
452
+
453
+ ### Невидимое подавление ошибок
454
+
455
+ Некоторые formatters возвращают `""` или `"0"` для ошибки. Если источник ненадёжен, используйте parser/validator до formatter-а и решайте fallback явно.
456
+
457
+ ### Глобальная настройка во время рендера
458
+
459
+ Preset и formula registries — application configuration, а не component state.
460
+
461
+ ## Публичная граница
462
+
463
+ Фактическим источником истины является [`index.ts`](./index.ts). Документируются только exports, достижимые через `@ryuzaki13/react-foundation-lib/formatters`.
464
+
465
+ Например, внутренний файл `formatTagAsHashtag.ts` сейчас не экспортирован из публичного index и поэтому не является поддерживаемым API. Наличие файла в repository не разрешает consumer делать private import.
466
+
467
+ ## Следующий шаг
468
+
469
+ Если нужна одна простая функция, выберите строку в таблице быстрого выбора. Если настраивается таблица или сохраняемый пользовательский config, начните с [formatter pipeline](./pipeline/README.mdx) и затем откройте документы зависимых областей.