@ryuzaki13/react-foundation-lib 1.4.1 → 1.4.3

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 (73) hide show
  1. package/README.md +154 -100
  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/bounded-copy-stack/README.mdx +257 -0
  30. package/src/context-menu/README.mdx +326 -0
  31. package/src/currency/README.mdx +155 -0
  32. package/src/date-segments/README.mdx +319 -0
  33. package/src/error/README.mdx +95 -0
  34. package/src/error-report/README.mdx +612 -0
  35. package/src/excel/README.mdx +449 -0
  36. package/src/fingerprint/README.mdx +100 -0
  37. package/src/form/README.mdx +101 -0
  38. package/src/foundationLib.mdx +70 -54
  39. package/src/hooks/README.mdx +988 -0
  40. package/src/http/README.mdx +126 -0
  41. package/src/media/README.mdx +1853 -0
  42. package/src/notifications/README.mdx +323 -0
  43. package/src/number-scale/README.mdx +318 -0
  44. package/src/odata-service/README.mdx +2916 -0
  45. package/src/presets/README.mdx +136 -0
  46. package/src/pwa/README.mdx +394 -0
  47. package/src/query-client/README.mdx +2500 -0
  48. package/src/range-output/README.mdx +161 -0
  49. package/src/seo/README.mdx +279 -0
  50. package/src/session-storage/README.mdx +125 -0
  51. package/src/string-comparison/README.mdx +96 -0
  52. package/src/table/README.mdx +3106 -0
  53. package/src/three-scene/README.mdx +248 -0
  54. package/src/types/README.mdx +172 -0
  55. package/src/utils/README.mdx +1724 -0
  56. package/src/validators/README.mdx +239 -0
  57. package/src/virtualizer/README.mdx +138 -0
  58. package/src/xml/README.mdx +149 -0
  59. package/dist/chunks/dateUtils-B5V5aXB-.js +0 -847
  60. package/dist/chunks/dateUtils-B5V5aXB-.js.map +0 -1
  61. package/dist/chunks/odata-service-B6TxBSLm.js +0 -1277
  62. package/dist/chunks/odata-service-B6TxBSLm.js.map +0 -1
  63. package/dist/odata/buildSeparatedArrays.d.ts.map +0 -1
  64. package/dist/odata/dateUtils.d.ts.map +0 -1
  65. package/dist/odata/findCollectionPairs.d.ts.map +0 -1
  66. package/dist/odata/index.d.ts +0 -6
  67. package/dist/odata/index.d.ts.map +0 -1
  68. package/dist/odata/index.js +0 -102
  69. package/dist/odata/index.js.map +0 -1
  70. package/dist/odata/store.d.ts.map +0 -1
  71. package/dist/odata/types.d.ts.map +0 -1
  72. package/src/pwa/README.md +0 -638
  73. package/src/query-client/README.md +0 -565
@@ -0,0 +1,449 @@
1
+ import { Meta } from "@storybook/addon-docs/blocks";
2
+
3
+ <Meta title="Foundation Lib/Data/Excel" />
4
+
5
+ # Excel
6
+
7
+ Модуль `@ryuzaki13/react-foundation-lib/excel` формирует и скачивает `.xlsx`-файлы в браузере. Он поддерживает:
8
+
9
+ - одну или несколько таблиц-листов;
10
+ - типизированные ячейки `String`, `Number`, `Date`, `Boolean`;
11
+ - Excel number/date formats;
12
+ - ширину колонок;
13
+ - стили отдельных ячеек;
14
+ - auto-filter;
15
+ - адаптацию OData/table-описаний к Excel.
16
+
17
+ Модуль строится поверх `write-excel-file/browser` и не предназначен для Node.js/SSR-скачивания.
18
+
19
+ ## Установка
20
+
21
+ ```bash
22
+ npm install @ryuzaki13/react-foundation-lib write-excel-file
23
+ ```
24
+
25
+ Версия `write-excel-file` должна соответствовать peer dependency пакета.
26
+
27
+ ```ts
28
+ import {
29
+ buildExcelSheetData,
30
+ downloadExcelFile,
31
+ downloadExcelWorkbook,
32
+ type ExcelColumn,
33
+ type ExcelRow
34
+ } from "@ryuzaki13/react-foundation-lib/excel";
35
+ ```
36
+
37
+ ## Самый простой экспорт
38
+
39
+ ```ts
40
+ await downloadExcelFile({
41
+ fileName: "orders.xlsx",
42
+ columns: [
43
+ { id: "ID", header: "Номер", type: String, width: 18 },
44
+ {
45
+ id: "AMOUNT",
46
+ header: "Сумма",
47
+ type: Number,
48
+ format: "#,##0.00",
49
+ width: 14
50
+ },
51
+ {
52
+ id: "CREATED_AT",
53
+ header: "Создан",
54
+ type: Date,
55
+ format: "dd.mm.yyyy hh:mm",
56
+ width: 20
57
+ }
58
+ ],
59
+ rows: [
60
+ {
61
+ ID: "A-100",
62
+ AMOUNT: 1250.5,
63
+ CREATED_AT: "2026-08-21T09:30:00+05:00"
64
+ }
65
+ ],
66
+ autoFilter: true
67
+ });
68
+ ```
69
+
70
+ Браузер скачает книгу с единственным листом `Sheet1`.
71
+
72
+ ## Базовая модель
73
+
74
+ ```ts
75
+ type ExcelColumn = {
76
+ id: string;
77
+ header: string;
78
+ width?: number;
79
+ type?: StringConstructor | NumberConstructor | DateConstructor | BooleanConstructor;
80
+ format?: string;
81
+ };
82
+
83
+ type ExcelRow = Record<string, unknown>;
84
+ ```
85
+
86
+ `column.id` — ключ, по которому значение читается из строки. Если в row такого ключа нет, ячейка будет пустой.
87
+
88
+ `header` попадает в первую строку. Заголовок автоматически получает жирный шрифт и тонкую серую нижнюю границу.
89
+
90
+ `width` передаётся writer как ширина Excel-колонки. Единица соответствует API `write-excel-file`, а не CSS-пикселям.
91
+
92
+ `format` — маска Excel, например `#,##0.00`, `dd.mm.yyyy`, `hh:mm:ss`. Это не `Intl`-формат и не строковый formatter: Excel применяет маску к настоящему числу или дате.
93
+
94
+ ## Почему тип ячейки важен
95
+
96
+ Неправильно передавать заранее отформатированную сумму:
97
+
98
+ ```ts
99
+ {
100
+ AMOUNT: "1 250,50 ₽";
101
+ }
102
+ ```
103
+
104
+ Excel увидит текст и не сможет корректно суммировать, сортировать и применять числовую маску.
105
+
106
+ Правильно:
107
+
108
+ ```ts
109
+ columns: [{ id: "AMOUNT", header: "Сумма", type: Number, format: "#,##0.00" }],
110
+ rows: [{ AMOUNT: 1250.5 }],
111
+ ```
112
+
113
+ ## Нормализация значений
114
+
115
+ `buildExcelSheetData` приводит данные согласно `column.type`.
116
+
117
+ ### Пустые значения
118
+
119
+ `null`, `undefined` и пустая строка `""` становятся пустой ячейкой. Строка из пробелов не считается пустой.
120
+
121
+ ### `Number`
122
+
123
+ Число сохраняется как есть. Другое значение проходит через `Number(value)` и принимается только при конечном результате.
124
+
125
+ ```ts
126
+ "125.5"; // 125.5
127
+ ""; // пустая ячейка
128
+ "not a number"; // пустая ячейка
129
+ Infinity; // пустая ячейка
130
+ ```
131
+
132
+ Учитывайте стандартные правила JavaScript: строки `"0x10"` и `"1e2"` преобразуются в `16` и `100`.
133
+
134
+ ### `String`
135
+
136
+ Используется `String(value)`. Объект без собственного осмысленного `toString` станет строкой `"[object Object]"`, поэтому сложные данные подготавливайте заранее.
137
+
138
+ ### `Boolean`
139
+
140
+ Используется `Boolean(value)`. Непустая строка `"false"` является `true`. Если backend присылает строковые флаги, сначала преобразуйте их в реальный boolean.
141
+
142
+ ### `Date`
143
+
144
+ Готовый `Date` используется напрямую. Остальное разбирается общим `parseDate` из formatters. Нераспознанное значение становится пустым.
145
+
146
+ Для предсказуемого времени используйте ISO/OData-формат с явной временной зоной. Excel хранит дату как значение, а отображение дополнительно зависит от format и настроек программы пользователя.
147
+
148
+ ### Без `type`
149
+
150
+ Значение передаётся writer почти без преобразования. Этот режим требует, чтобы вызывающий код уже подготовил тип, поддерживаемый `write-excel-file`.
151
+
152
+ ## `buildExcelSheetData(args)`
153
+
154
+ Создаёт `SheetData` без скачивания файла. Полезно для тестов и собственного вызова writer.
155
+
156
+ ```ts
157
+ const data = buildExcelSheetData({
158
+ columns,
159
+ rows,
160
+ getCellStyle: ({ row, column, rowIndex, columnIndex }) => {
161
+ if (column.id === "AMOUNT" && Number(row.AMOUNT) < 0) {
162
+ return { textColor: "#C50F1F", fontWeight: "bold" };
163
+ }
164
+
165
+ return undefined;
166
+ }
167
+ });
168
+ ```
169
+
170
+ Первая строка результата — header, затем строки данных. `rowIndex` и `columnIndex` в callback начинаются с нуля **для данных**, не включая header.
171
+
172
+ Стиль объединяется после `value`, `type` и `format`. Тип `ExcelCellStyle` специально исключает эти три поля, поэтому callback предназначен только для визуальных параметров writer.
173
+
174
+ ## Стили по value state
175
+
176
+ ### `resolveExcelCellStyleFromState(state)`
177
+
178
+ Преобразует общий `State` в цвет текста:
179
+
180
+ | State | Цвет |
181
+ | ------------- | --------- |
182
+ | `information` | `#2563EB` |
183
+ | `success` | `#107C10` |
184
+ | `warning` | `#B7791F` |
185
+ | `error` | `#C50F1F` |
186
+
187
+ Для `none`, пустой строки и `undefined` возвращается `undefined`.
188
+
189
+ ```ts
190
+ getCellStyle: ({ row }) =>
191
+ resolveExcelCellStyleFromState(row.STATE as State),
192
+ ```
193
+
194
+ Helper задаёт только `textColor`. При необходимости объедините результат со своим стилем.
195
+
196
+ ## Auto-filter
197
+
198
+ ```ts
199
+ autoFilter: true;
200
+ ```
201
+
202
+ Фильтр охватывает header и все строки данных. Он добавляется через низкоуровневую OpenXML transform-feature, потому что используемый writer не предоставляет отдельной высокоуровневой опции.
203
+
204
+ ### `buildExcelAutoFilterRef(columnCount, rowCount)`
205
+
206
+ ```ts
207
+ buildExcelAutoFilterRef(1, 2); // "A1:A2"
208
+ buildExcelAutoFilterRef(28, 10); // "A1:AB10"
209
+ buildExcelAutoFilterRef(0, 10); // undefined
210
+ ```
211
+
212
+ `rowCount` должен включать строку header. `downloadExcelFile` и workbook API вычисляют его автоматически.
213
+
214
+ ## Несколько листов
215
+
216
+ ### `downloadExcelWorkbook(args)`
217
+
218
+ ```ts
219
+ await downloadExcelWorkbook({
220
+ fileName: "report.xlsx",
221
+ sheets: [
222
+ {
223
+ name: "Заказы",
224
+ columns: orderColumns,
225
+ rows: orders,
226
+ autoFilter: true
227
+ },
228
+ {
229
+ name: "Справочник статусов",
230
+ columns: statusColumns,
231
+ rows: statuses,
232
+ autoFilter: false
233
+ }
234
+ ]
235
+ });
236
+ ```
237
+
238
+ Каждый лист имеет собственные колонки, ширины, строки, стили и filter. Порядок массива — порядок вкладок Excel.
239
+
240
+ Пустой массив `sheets` приводит к ошибке: «Для формирования Excel-книги нужен минимум один лист».
241
+
242
+ `fileName` передаётся browser writer. Рекомендуется явно добавлять расширение `.xlsx` и заранее очищать пользовательский ввод от нежелательных символов имени файла.
243
+
244
+ ## Имена листов
245
+
246
+ ### `resolveExcelSheetNames(names)`
247
+
248
+ Excel ограничивает имя листа 31 символом и запрещает управляющие символы, `[ ] / \ : * ?`. Helper:
249
+
250
+ - заменяет запрещённые группы пробелом;
251
+ - схлопывает пробелы;
252
+ - удаляет апострофы по краям;
253
+ - использует `Лист N` для пустого результата;
254
+ - обрезает до 31 символа;
255
+ - делает имена уникальными без учёта регистра через суффиксы ` (2)`, ` (3)`.
256
+
257
+ ```ts
258
+ resolveExcelSheetNames(["Продажи/Россия", "продажи россия", ""]);
259
+
260
+ // ["Продажи Россия", "продажи россия (2)", "Лист 3"]
261
+ ```
262
+
263
+ Функция возвращает новый массив и не меняет входные строки.
264
+
265
+ ## Table/OData-адаптер
266
+
267
+ Вторая часть модуля помогает экспортировать таблицу, описание колонок которой использует `ColumnRole`, `ODataMetaType` и formatter pipeline.
268
+
269
+ ### `TableExcelColumnDescriptor`
270
+
271
+ ```ts
272
+ type TableExcelColumnDescriptor = {
273
+ id: string;
274
+ header: string;
275
+ width?: number;
276
+ role?: ColumnRole;
277
+ odataType?: ODataMetaType;
278
+ clientOnly?: boolean;
279
+ formattersPipeline?: FormattersPipelineConfig;
280
+ };
281
+ ```
282
+
283
+ ### `resolveTableExcelColumnType(descriptor)`
284
+
285
+ Тип определяется в следующем порядке:
286
+
287
+ 1. `clientOnly: true` → `Number`;
288
+ 2. OData date/time type → `Date`;
289
+ 3. OData boolean type → `Boolean`;
290
+ 4. OData numeric type → `Number`;
291
+ 5. `role: "measure"` → `Number`;
292
+ 6. иначе → `String`.
293
+
294
+ `clientOnly` здесь предполагает вычисляемую числовую колонку. Если приложение создаёт client-only текст, generic descriptor не сможет вывести его автоматически — сформируйте `ExcelColumn` вручную.
295
+
296
+ ### `createTableExcelColumn(descriptor)`
297
+
298
+ Создаёт `ExcelColumn`, включая тип и format.
299
+
300
+ ```ts
301
+ const column = createTableExcelColumn({
302
+ id: "NET_AMOUNT",
303
+ header: "Сумма",
304
+ odataType: "decimal",
305
+ role: "measure",
306
+ width: 16,
307
+ formattersPipeline: {
308
+ steps: [
309
+ {
310
+ type: "typedValueFormat",
311
+ config: { numberPresetName: "currency" }
312
+ }
313
+ ]
314
+ }
315
+ });
316
+ ```
317
+
318
+ Точная структура pipeline описана в документации `/formatters`. Excel использует первый валидированный step `typedValueFormat`.
319
+
320
+ ### Числовые форматы из presets
321
+
322
+ Если pipeline содержит существующий `numberPresetName`, число получает столько десятичных нулей, сколько указано в preset:
323
+
324
+ ```text
325
+ decimals = 0 → #,##0
326
+ decimals = 2 → #,##0.00
327
+ ```
328
+
329
+ Для OData integer по умолчанию выбирается preset `integer`, для других чисел — `decimal`. Неизвестное имя preset игнорируется и заменяется подходящим default.
330
+
331
+ ### Форматы дат из presets
332
+
333
+ По умолчанию:
334
+
335
+ - OData `time` → time preset;
336
+ - `datetime`/`datetimeOffset` → datetime preset;
337
+ - остальные date types → date preset.
338
+
339
+ Встроенные presets переводятся в Excel-маски, включая варианты `dd.mm`, `d mmm`, `mmm yyyy`, `hh:mm` и форматы с секундами.
340
+
341
+ Для пользовательского date pattern токены нормализуются для Excel: `H` заменяется на `h`, `M` — на `m`. Если `datePresetName` не найден в registry, он исторически трактуется как ручной pattern.
342
+
343
+ Форматы даты в JavaScript formatter и Excel не полностью взаимозаменяемы. Всегда проверяйте пользовательские patterns в реальном Excel/LibreOffice.
344
+
345
+ ## Подготовленная таблица
346
+
347
+ ```ts
348
+ type ResolvedExcelTable = {
349
+ columns: ExcelColumn[];
350
+ rows: ExcelRow[];
351
+ cellStyles?: ReadonlyArray<Readonly<Record<string, ExcelCellStyle | undefined>>>;
352
+ };
353
+ ```
354
+
355
+ `cellStyles[rowIndex][columnId]` задаёт стиль конкретной ячейки. Индекс соответствует массиву `rows`.
356
+
357
+ ### `downloadResolvedExcelTable(args)`
358
+
359
+ ```ts
360
+ await downloadResolvedExcelTable({
361
+ fileName: "table.xlsx",
362
+ table: {
363
+ columns,
364
+ rows,
365
+ cellStyles: [{ AMOUNT: { textColor: "#107C10" } }, { AMOUNT: { textColor: "#C50F1F" } }]
366
+ }
367
+ });
368
+ ```
369
+
370
+ `autoFilter` по умолчанию `true`.
371
+
372
+ ### `downloadResolvedExcelWorkbook(args)`
373
+
374
+ ```ts
375
+ await downloadResolvedExcelWorkbook({
376
+ fileName: "analytics.xlsx",
377
+ sheets: [
378
+ { name: "План", table: planTable },
379
+ { name: "Факт", table: factTable, autoFilter: false }
380
+ ]
381
+ });
382
+ ```
383
+
384
+ Для каждого resolved sheet auto-filter тоже включён по умолчанию.
385
+
386
+ ## Browser boundary и обработка ошибок
387
+
388
+ `downloadExcelFile`, `downloadExcelWorkbook` и resolved download-функции используют browser bundle writer и инициируют сохранение файла. Вызывайте их из пользовательского действия в клиентском коде.
389
+
390
+ ```ts
391
+ async function onExportClick() {
392
+ setExporting(true);
393
+
394
+ try {
395
+ await downloadExcelFile(args);
396
+ } catch (error) {
397
+ showExportError(error);
398
+ } finally {
399
+ setExporting(false);
400
+ }
401
+ }
402
+ ```
403
+
404
+ Promise завершается после `toFile`. Ошибки writer, нехватка памяти и запрет скачивания браузером не подавляются.
405
+
406
+ Для больших таблиц построение всей книги происходит в памяти главного потока и может временно заблокировать UI. Модуль не реализует streaming, worker и серверную генерацию.
407
+
408
+ ## Частые ошибки
409
+
410
+ ### Передавать display-строки вместо typed values
411
+
412
+ Excel теряет возможность вычислять и сортировать данные. Передавайте настоящие `number`, `boolean` и `Date`.
413
+
414
+ ### Ожидать JavaScript-формат в поле `format`
415
+
416
+ Это маска Excel. `Intl.DateTimeFormat` options и formatter preset напрямую туда не подходят.
417
+
418
+ ### Экспортировать `"false"` как Boolean
419
+
420
+ Непустая строка истинна. Нормализуйте transport-значение до `false`.
421
+
422
+ ### Использовать API на сервере
423
+
424
+ Импорт идёт из `write-excel-file/browser`. Для server-side Excel нужен другой owner/адаптер.
425
+
426
+ ### Передавать пустой workbook
427
+
428
+ Многолистовая книга требует минимум один sheet. Пустой лист с header допустим, если есть columns.
429
+
430
+ ### Полагаться на исходные имена листов
431
+
432
+ Они нормализуются и могут получить суффикс. Если имя нужно показывать пользователю заранее, вызовите `resolveExcelSheetNames` до download.
433
+
434
+ ## Краткая карта API
435
+
436
+ | Экспорт | Назначение |
437
+ | ------------------------------------------- | ---------------------------------------------- |
438
+ | `buildExcelSheetData` | Собрать типизированные ячейки без скачивания |
439
+ | `downloadExcelFile` | Скачать книгу с одним листом `Sheet1` |
440
+ | `downloadExcelWorkbook` | Скачать книгу из нескольких независимых листов |
441
+ | `buildExcelAutoFilterRef` | Построить Excel-диапазон фильтра |
442
+ | `resolveExcelSheetNames` | Нормализовать и дедуплицировать имена листов |
443
+ | `resolveExcelCellStyleFromState` | Получить цвет текста из общего value state |
444
+ | `resolveTableExcelColumnType` | Вывести Excel type из OData/table descriptor |
445
+ | `createTableExcelColumn` | Создать колонку с type и format |
446
+ | `downloadResolvedExcelTable` | Скачать заранее подготовленную таблицу |
447
+ | `downloadResolvedExcelWorkbook` | Скачать несколько подготовленных таблиц |
448
+ | `ExcelColumn`, `ExcelRow`, `ExcelCellStyle` | Базовые generic-типы |
449
+ | `ResolvedExcelTable` | Таблица с optional map стилей |
@@ -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
+ ```