@ryuzaki13/react-foundation-lib 1.4.1 → 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 (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,161 @@
1
+ import { Meta } from "@storybook/addon-docs/blocks";
2
+
3
+ <Meta title="Foundation Lib/Data/Range Output" />
4
+
5
+ # Подстановка открытых границ через `@ryuzaki13/react-foundation-lib/range-output`
6
+
7
+ Модуль хранит в UI открытые границы диапазона как `null`, но позволяет подставить конкретные значения при внешнем выводе или сохранении.
8
+
9
+ ## Содержание
10
+
11
+ - [Назначение](#назначение)
12
+ - [Установка и импорт](#установка-и-импорт)
13
+ - [Контракты](#контракты)
14
+ - [`readRangeOutputValueFallback`](#readrangeoutputvaluefallback)
15
+ - [`resolveRangeOutputValue`](#resolverangeoutputvalue)
16
+ - [Правильная boundary](#правильная-boundary)
17
+ - [API и ошибки](#api-и-ошибки)
18
+
19
+ ## Назначение
20
+
21
+ ```text
22
+ UI range [null, 6]
23
+ fallback { start: 0, end: 12 }
24
+ │
25
+ ▼
26
+ external output [0, 6]
27
+ ```
28
+
29
+ Исходное UI-значение не изменяется. `null` заменяется только при построении отдельного output tuple.
30
+
31
+ ## Установка и импорт
32
+
33
+ ```ts
34
+ import { readRangeOutputValueFallback, resolveRangeOutputValue } from "@ryuzaki13/react-foundation-lib/range-output";
35
+
36
+ import type { RangeOutputFallbackEndpointParser, RangeOutputValueFallback } from "@ryuzaki13/react-foundation-lib/range-output";
37
+ ```
38
+
39
+ Модуль pure, не требует React/browser APIs.
40
+
41
+ ## Контракты
42
+
43
+ ```ts
44
+ type RangeOutputValueFallback<TValue extends BaseType> = {
45
+ start?: TValue | null;
46
+ end?: TValue | null;
47
+ };
48
+
49
+ type RangeOutputFallbackEndpointParser<TValue extends BaseType> = (value: unknown) => TValue | null | undefined;
50
+ ```
51
+
52
+ Смысл результата parser:
53
+
54
+ | Результат parser | Что происходит |
55
+ | ---------------- | ----------------------------------------------- |
56
+ | конкретное value | сохраняется как fallback |
57
+ | `null` | явная пустая fallback-граница |
58
+ | `undefined` | поле отсутствует/невалидно и не входит в result |
59
+
60
+ ## `readRangeOutputValueFallback`
61
+
62
+ Функция читает только форму:
63
+
64
+ ```ts
65
+ {
66
+ outputValueFallback: {
67
+ start: unknown,
68
+ end: unknown
69
+ }
70
+ }
71
+ ```
72
+
73
+ Пример числового parser:
74
+
75
+ ```ts
76
+ function parseFiniteNumber(value: unknown): number | undefined {
77
+ const parsed = typeof value === "number" ? value : Number(value);
78
+ return Number.isFinite(parsed) ? parsed : undefined;
79
+ }
80
+
81
+ const fallback = readRangeOutputValueFallback({ outputValueFallback: { start: "1", end: "bad" } }, parseFiniteNumber);
82
+
83
+ console.log(fallback); // { start: 1 }.
84
+ ```
85
+
86
+ Функция возвращает `undefined`, если:
87
+
88
+ - верхний `props` не object-like record;
89
+ - `outputValueFallback` не object-like record;
90
+ - оба parser-result равны `undefined`.
91
+
92
+ Parser вызывается отдельно для `start` и `end`, в том числе когда raw field отсутствует. Он не должен иметь side effects.
93
+
94
+ `isRecord` здесь допускает не только plain object, но исключает arrays/null. Runtime schema конкретного значения полностью задаёт parser.
95
+
96
+ ## `resolveRangeOutputValue`
97
+
98
+ ```ts
99
+ resolveRangeOutputValue([null, 6], { start: 0, end: 12 });
100
+ // [0, 6].
101
+
102
+ resolveRangeOutputValue([3, null], { start: 0, end: 12 });
103
+ // [3, 12].
104
+ ```
105
+
106
+ Правила для каждой позиции:
107
+
108
+ 1. Если UI endpoint не `null`, он возвращается буквально.
109
+ 2. Если endpoint равен `null`, используется соответствующий fallback.
110
+ 3. Если fallback отсутствует или равен `null`, output остаётся `null`.
111
+
112
+ ```ts
113
+ resolveRangeOutputValue([null, null], { end: 12 });
114
+ // [null, 12].
115
+
116
+ resolveRangeOutputValue([null, null], undefined);
117
+ // [null, null].
118
+ ```
119
+
120
+ Всегда создаётся новый tuple. Вложенные ссылочные `Date` не клонируются.
121
+
122
+ ## Правильная boundary
123
+
124
+ Helper специально отделяет UI draft и внешний snapshot:
125
+
126
+ ```ts
127
+ const draftRange: readonly [number | null, number | null] = [null, 6];
128
+
129
+ function getConfig() {
130
+ return {
131
+ range: resolveRangeOutputValue(draftRange, outputFallback)
132
+ };
133
+ }
134
+ ```
135
+
136
+ Не записывайте output обратно в draft:
137
+
138
+ ```text
139
+ draft [null, 6] ───────────────► UI продолжает показывать открытую границу
140
+ │
141
+ └─ save boundary ───────► snapshot [0, 6]
142
+ ```
143
+
144
+ Это output substitution, а не общая нормализация диапазона: порядок start/end, min/max и domain validation не проверяются.
145
+
146
+ ## API и ошибки
147
+
148
+ | Export | Результат |
149
+ | ----------------------------------- | ------------------------------- |
150
+ | `RangeOutputValueFallback` | Optional start/end fallback |
151
+ | `RangeOutputFallbackEndpointParser` | Parser одного endpoint |
152
+ | `readRangeOutputValueFallback` | Parsed fallback или `undefined` |
153
+ | `resolveRangeOutputValue` | Новый output tuple |
154
+
155
+ Типичные ошибки:
156
+
157
+ - Считать `0`, `false` или `""` отсутствующими: используется nullish logic, эти значения сохраняются.
158
+ - Передавать `undefined` как UI endpoint при generic `TValue`, включающем undefined: заменяется только строго `null`.
159
+ - Мутировать draft результатом output resolver.
160
+ - Ожидать, что helper поменяет местами границы или проверит диапазон.
161
+ - Делать parser через unsafe cast внешнего `unknown`.
@@ -0,0 +1,279 @@
1
+ import { Meta } from "@storybook/addon-docs/blocks";
2
+
3
+ <Meta title="Foundation Lib/HTTP/SEO" />
4
+
5
+ # SEO
6
+
7
+ Модуль `@ryuzaki13/react-foundation-lib/seo` собирает единый набор SEO-описаний страницы:
8
+
9
+ - title и обычные meta tags;
10
+ - Open Graph;
11
+ - Twitter Card;
12
+ - canonical link;
13
+ - JSON-LD по Schema.org.
14
+
15
+ Он не меняет `document.head` напрямую. Результат передаётся роутеру, SSR-фреймворку или собственному renderer head-тегов.
16
+
17
+ ## Импорт
18
+
19
+ ```ts
20
+ import { createRouteHead, createSeoHead, type SeoContentType, type SeoHeadOptions } from "@ryuzaki13/react-foundation-lib/seo";
21
+ ```
22
+
23
+ Функции синхронные и не требуют DOM, поэтому подходят для server rendering.
24
+
25
+ ## Быстрый старт
26
+
27
+ ```ts
28
+ const head = createSeoHead({
29
+ pathname: "/articles/odata-guide",
30
+ baseUrl: "https://example.ru",
31
+ caption: "Практический гайд по OData",
32
+ description: "Разбираем фильтры, сортировку и пагинацию OData.",
33
+ type: "Article",
34
+ keywords: ["OData", "TypeScript", "SAP"],
35
+ image: {
36
+ url: "/images/odata-guide.webp",
37
+ alt: "Схема OData-запроса",
38
+ width: 1200,
39
+ height: 630
40
+ },
41
+ publishedAt: "2026-08-01T10:00:00+05:00",
42
+ modifiedAt: "2026-08-20T12:30:00+05:00"
43
+ });
44
+ ```
45
+
46
+ Результат имеет три массива:
47
+
48
+ ```ts
49
+ {
50
+ meta: (Record < string, string > []);
51
+ links: (Record < string, string > []);
52
+ scripts: Array<{
53
+ type: "application/ld+json";
54
+ children: string;
55
+ }>;
56
+ }
57
+ ```
58
+
59
+ Форма совместима с распространённым route-head подходом, но конкретное подключение зависит от фреймворка.
60
+
61
+ ## `createSeoHead(options)`
62
+
63
+ ### Параметры
64
+
65
+ ```ts
66
+ type SeoContentType = "WebSite" | "WebPage" | "Article" | "NewsArticle";
67
+
68
+ type SeoHeadOptions = {
69
+ pathname: string;
70
+ caption: string;
71
+ description: string;
72
+ type: SeoContentType;
73
+ baseUrl?: string;
74
+ keywords?: string[];
75
+ image?: {
76
+ url: string;
77
+ alt?: string | null;
78
+ width?: number | null;
79
+ height?: number | null;
80
+ };
81
+ publishedAt?: string;
82
+ modifiedAt?: string;
83
+ noIndex?: boolean;
84
+ };
85
+ ```
86
+
87
+ `caption` и `description` обязательны даже для пустой страницы. Библиотека не подставляет название сайта и не обрезает текст под лимиты поисковых систем.
88
+
89
+ ### Canonical URL
90
+
91
+ Если `pathname` уже абсолютный URL, он нормализуется через `new URL()`.
92
+
93
+ ```ts
94
+ createSeoHead({
95
+ pathname: "https://example.ru/catalog",
96
+ // baseUrl не нужен
97
+ caption: "Каталог",
98
+ description: "Каталог товаров",
99
+ type: "WebPage"
100
+ });
101
+ ```
102
+
103
+ Для относительного пути рекомендуется обязательно передавать абсолютный `baseUrl`:
104
+
105
+ ```ts
106
+ pathname: "/catalog",
107
+ baseUrl: "https://example.ru",
108
+ // canonical: https://example.ru/catalog
109
+ ```
110
+
111
+ Без `baseUrl` относительный путь сохраняется как есть. Это может быть приемлемо для некоторых head-renderer, но canonical URL по стандартной практике должен быть абсолютным.
112
+
113
+ Неверный `baseUrl` вместе с относительным `pathname` приводит к исключению `new URL`. Проверяйте конфигурацию origin при старте приложения.
114
+
115
+ ### Meta tags
116
+
117
+ Всегда создаются:
118
+
119
+ - `{ title: caption }`;
120
+ - description;
121
+ - robots;
122
+ - `og:type`, `og:title`, `og:description`, `og:url`, `og:locale`;
123
+ - `twitter:card`, `twitter:title`, `twitter:description`.
124
+
125
+ `Article` и `NewsArticle` получают Open Graph type `article`. `WebSite` и `WebPage` получают `website`.
126
+
127
+ Локаль фиксирована как `ru_RU` для Open Graph и `ru-RU` для JSON-LD.
128
+
129
+ Если есть изображение, Twitter Card становится `summary_large_image`; без изображения — `summary`.
130
+
131
+ ### Индексация
132
+
133
+ ```ts
134
+ noIndex: true;
135
+ // robots: "noindex, nofollow"
136
+ ```
137
+
138
+ По умолчанию используется `index, follow`. `noIndex` формирует meta-инструкцию, но не заменяет серверные заголовки, robots.txt, авторизацию и запрет выдачи приватных страниц.
139
+
140
+ ### Keywords
141
+
142
+ Непустой массив соединяется через `", "` и добавляется одновременно в обычный meta keywords и JSON-LD.
143
+
144
+ ```ts
145
+ keywords: ["React", "TypeScript"];
146
+ // "React, TypeScript"
147
+ ```
148
+
149
+ Пустые строки внутри массива не удаляются автоматически.
150
+
151
+ ### Изображение
152
+
153
+ Относительный `image.url` преобразуется с тем же `baseUrl`. Создаются `og:image` и `twitter:image`; при наличии `alt` дополнительно создаётся `og:image:alt`.
154
+
155
+ В JSON-LD формируется `ImageObject`. Неуказанные `width`, `height` и `alt` остаются `undefined` либо `null` согласно исходным данным и поведению `JSON.stringify`.
156
+
157
+ ### Даты
158
+
159
+ `publishedAt` и `modifiedAt` разбираются через стандартный `Date` и записываются в ISO UTC:
160
+
161
+ ```ts
162
+ publishedAt: "2026-08-01T10:00:00+05:00";
163
+ // datePublished: "2026-08-01T05:00:00.000Z"
164
+ ```
165
+
166
+ Пустая или неверная дата молча не добавляется. Для предсказуемости передавайте ISO 8601 со временной зоной.
167
+
168
+ ## JSON-LD
169
+
170
+ Каждый вызов создаёт один script descriptor:
171
+
172
+ ```ts
173
+ {
174
+ type: "application/ld+json",
175
+ children: JSON.stringify({
176
+ "@context": "https://schema.org",
177
+ "@type": type,
178
+ mainEntityOfPage: { "@type": "WebPage", "@id": canonicalUrl },
179
+ headline: caption,
180
+ description,
181
+ url: canonicalUrl,
182
+ inLanguage: "ru-RU",
183
+ }),
184
+ }
185
+ ```
186
+
187
+ Библиотека возвращает строку, а не готовый React-элемент. Следуйте API head-renderer. Если вручную вставляете строку в HTML script, учитывайте XSS-границу: SEO-текст должен быть доверенным или безопасно экранированным renderer. Простого `JSON.stringify` недостаточно для произвольного пользовательского текста, содержащего последовательности вроде закрывающего `script`-тега.
188
+
189
+ ## `createRouteHead(type, path, params)`
190
+
191
+ Упрощённый адаптер для данных route loader/CMS:
192
+
193
+ ```ts
194
+ type SeoHeadParams = {
195
+ caption: string;
196
+ description: string;
197
+ previewImage: string | null;
198
+ previewImageAlt: string | null;
199
+ created: string;
200
+ updated: string;
201
+ };
202
+ ```
203
+
204
+ ```ts
205
+ const head = createRouteHead("NewsArticle", "/news/42", loaderData?.seo);
206
+ ```
207
+
208
+ Адаптер:
209
+
210
+ - передаёт `path` как `pathname`;
211
+ - при `null`/`undefined` params подставляет пустые `caption` и `description`;
212
+ - превращает `previewImage` в `image`;
213
+ - сопоставляет `created` с `publishedAt`, `updated` с `modifiedAt`;
214
+ - не передаёт `baseUrl`, keywords или `noIndex`.
215
+
216
+ Поэтому URL результата останется относительным, если `path` относительный. Когда нужен абсолютный canonical, используйте полный `createSeoHead` либо передавайте абсолютный `path`.
217
+
218
+ ## Пример с TanStack Router
219
+
220
+ ```tsx
221
+ export const Route = createFileRoute("/articles/$slug")({
222
+ loader: loadArticle,
223
+ head: ({ loaderData, params }) =>
224
+ createSeoHead({
225
+ pathname: `/articles/${params.slug}`,
226
+ baseUrl: "https://example.ru",
227
+ caption: loaderData.title,
228
+ description: loaderData.description,
229
+ type: "Article",
230
+ image: loaderData.image ? { url: loaderData.image, alt: loaderData.title } : undefined
231
+ })
232
+ });
233
+ ```
234
+
235
+ Проверьте точную форму `head` вашей версии роутера: библиотека возвращает структурные данные, но не связывается с router напрямую.
236
+
237
+ ## Проверка результата
238
+
239
+ Для каждой важной страницы проверьте:
240
+
241
+ 1. canonical абсолютный и соответствует публичному URL;
242
+ 2. title и description не пусты;
243
+ 3. Open Graph image доступно без авторизации;
244
+ 4. JSON-LD проходит validator Schema.org/поисковой системы;
245
+ 5. preview SSR HTML содержит теги до выполнения JavaScript;
246
+ 6. приватные страницы имеют правильную политику индексации.
247
+
248
+ ## Частые ошибки
249
+
250
+ ### Передавать относительный путь без `baseUrl`
251
+
252
+ Функция не знает публичный origin приложения и оставит URL относительным.
253
+
254
+ ### Считать `createRouteHead` полным аналогом `createSeoHead`
255
+
256
+ Адаптер специально узкий и не принимает base URL, keywords, размеры image и `noIndex`.
257
+
258
+ ### Передавать локальную дату без timezone
259
+
260
+ Результат стандартного `Date` может зависеть от окружения. Используйте полный ISO 8601.
261
+
262
+ ### Генерировать SEO только после client fetch
263
+
264
+ Для надёжной индексации head должен появляться в SSR/pre-render HTML, если архитектура приложения это поддерживает.
265
+
266
+ ### Передавать недоверенный текст напрямую в JSON-LD script
267
+
268
+ Используйте безопасный renderer и контролируйте источник SEO-полей.
269
+
270
+ ## Краткая карта API
271
+
272
+ | Экспорт | Назначение |
273
+ | ------------------------------------- | ---------------------------------------------- |
274
+ | `createSeoHead(options)` | Собрать полный meta/link/JSON-LD набор |
275
+ | `createRouteHead(type, path, params)` | Адаптировать типичные route SEO-поля |
276
+ | `SeoContentType` | Допустимый Schema.org content type |
277
+ | `SeoHeadOptions` | Полная конфигурация страницы |
278
+ | `SeoHeadParams` | Входные данные упрощённого route adapter |
279
+ | `SeoImage` | URL и описательные размеры preview-изображения |
@@ -0,0 +1,125 @@
1
+ import { Meta } from "@storybook/addon-docs/blocks";
2
+
3
+ <Meta title="Foundation Lib/Browser/Session Storage ID" />
4
+
5
+ # Идентификатор вкладочной сессии через `@ryuzaki13/react-foundation-lib/session-storage`
6
+
7
+ `getSessionStorageId` возвращает стабильный id в пределах одной browser-вкладки. При первом вызове id создаётся и сохраняется в `sessionStorage`, при следующих — читается по тому же ключу.
8
+
9
+ ## Установка и импорт
10
+
11
+ ```ts
12
+ import { getSessionStorageId } from "@ryuzaki13/react-foundation-lib/session-storage";
13
+ ```
14
+
15
+ Default generator — `uuidv4` из `/crypto`.
16
+
17
+ ## Базовый пример
18
+
19
+ ```ts
20
+ const sessionId = getSessionStorageId("sales-app.error-report.session.v1");
21
+ ```
22
+
23
+ Повторный вызов в той же вкладке:
24
+
25
+ ```ts
26
+ const first = getSessionStorageId("sales-app.session.v1");
27
+ const second = getSessionStorageId("sales-app.session.v1");
28
+
29
+ console.log(first === second); // true, если storage доступен.
30
+ ```
31
+
32
+ Другая вкладка имеет отдельный `sessionStorage` и обычно получит другой id.
33
+
34
+ ## Алгоритм
35
+
36
+ ```text
37
+ sessionStorage недоступен ──► createId() без сохранения
38
+ │
39
+ ▼
40
+ getItem(key)
41
+ ├─ непустая строка ──────► вернуть её
42
+ └─ нет значения ─────────► createId() → setItem → вернуть id
43
+
44
+ ошибка getItem/setItem ─────► fallback createId()
45
+ ```
46
+
47
+ ```ts
48
+ const id = getSessionStorageId("demo.session", () => "known-id");
49
+ ```
50
+
51
+ Второй аргумент удобен для теста или другого генератора.
52
+
53
+ ## Граница стабильности
54
+
55
+ Идентификатор стабилен только при успешном storage:
56
+
57
+ - reload той же вкладки обычно сохраняет значение;
58
+ - новая вкладка получает своё storage;
59
+ - закрытие вкладки удаляет session storage по правилам браузера;
60
+ - privacy policy, sandbox iframe или переполнение могут запретить доступ;
61
+ - если storage недоступен, каждый вызов может создать новый fallback id.
62
+
63
+ Функция ловит ошибки чтения/записи и не ломает основной пользовательский сценарий.
64
+
65
+ Если `setItem` выбросил ошибку после первого вызова generator, fallback generator может быть вызван повторно. Не используйте `createId` с внешними необратимыми side effects.
66
+
67
+ ## Ключ storage
68
+
69
+ Ключ полностью задаёт host. Рекомендуемый формат:
70
+
71
+ ```text
72
+ <app-id>.<purpose>.v<schema-version>
73
+ ```
74
+
75
+ Примеры:
76
+
77
+ ```text
78
+ sales-app.error-report.session.v1
79
+ warehouse.telemetry.tab-session.v2
80
+ ```
81
+
82
+ Не используйте один ключ для несвязанных подсистем. При изменении семантики меняйте версию.
83
+
84
+ ## SSR и безопасность
85
+
86
+ Вне browser `sessionStorage` отсутствует, поэтому функция просто возвращает новый id generator-а. Она не пытается создать server session.
87
+
88
+ ```ts
89
+ const id = getSessionStorageId("demo", () => "server-fallback");
90
+ ```
91
+
92
+ Идентификатор:
93
+
94
+ - не является authentication token;
95
+ - не подтверждает пользователя;
96
+ - не должен давать доступ к данным;
97
+ - не является cross-tab device id;
98
+ - не должен включать персональные данные.
99
+
100
+ Для долговременного client fingerprint используйте отдельный `/fingerprint`, понимая его privacy-границы.
101
+
102
+ ## Тестирование
103
+
104
+ ```ts
105
+ sessionStorage.clear();
106
+
107
+ const createId = vi.fn(() => "session-1");
108
+
109
+ expect(getSessionStorageId("test.session", createId)).toBe("session-1");
110
+ expect(getSessionStorageId("test.session", () => "session-2")).toBe("session-1");
111
+ expect(createId).toHaveBeenCalledOnce();
112
+ ```
113
+
114
+ Очищайте `sessionStorage` между тестами.
115
+
116
+ ## API и ошибки
117
+
118
+ ```ts
119
+ function getSessionStorageId(key: string, createId?: () => string): string;
120
+ ```
121
+
122
+ - Пустой сохранённый string считается отсутствующим и заменяется.
123
+ - Ключ и результат generator не валидируются.
124
+ - Исключение самого fallback generator может быть проброшено.
125
+ - Функция ничего не удаляет из storage.
@@ -0,0 +1,96 @@
1
+ import { Meta } from "@storybook/addon-docs/blocks";
2
+
3
+ <Meta title="Foundation Lib/Data/String Comparison" />
4
+
5
+ # Сравнение строк через `@ryuzaki13/react-foundation-lib/string-comparison`
6
+
7
+ `compareStrings` задаёт единый порядок строк для русскоязычных UI-списков, смешанной кириллицы/латиницы и числовых суффиксов.
8
+
9
+ ## Установка и импорт
10
+
11
+ ```ts
12
+ import { compareStrings } from "@ryuzaki13/react-foundation-lib/string-comparison";
13
+ ```
14
+
15
+ Модуль использует стандартный `Intl.Collator` и не зависит от React.
16
+
17
+ ## Использование
18
+
19
+ ```ts
20
+ const names = ["item10", "item2", "item1"];
21
+ const sorted = [...names].sort(compareStrings);
22
+
23
+ console.log(sorted); // ["item1", "item2", "item10"].
24
+ console.log(names); // Исходный массив не изменён из-за [...names].
25
+ ```
26
+
27
+ Важно: сам `Array.prototype.sort` мутирует массив. `compareStrings` только comparator.
28
+
29
+ ```ts
30
+ names.sort(compareStrings); // names изменится.
31
+ ```
32
+
33
+ ## Правила сравнения
34
+
35
+ Основной collator настроен так:
36
+
37
+ ```ts
38
+ new Intl.Collator("ru-RU", {
39
+ usage: "sort",
40
+ sensitivity: "base",
41
+ numeric: true
42
+ });
43
+ ```
44
+
45
+ Это означает:
46
+
47
+ - locale фиксирован как `ru-RU`, а не зависит от системного языка;
48
+ - числовые части сравниваются как числа: `item2` раньше `item10`;
49
+ - основной проход не различает регистр и часть диакритики;
50
+ - если collator считает разные строки равными, применяется строгий UTF-16 fallback;
51
+ - результат нормализуется до `-1`, `0` или `1`.
52
+
53
+ ```ts
54
+ ["a", "A"].sort(compareStrings); // ["A", "a"].
55
+ ```
56
+
57
+ UTF-16 fallback нужен для детерминированного порядка, а не для лингвистической оценки регистра.
58
+
59
+ ## Контракт comparator
60
+
61
+ ```ts
62
+ function compareStrings(left: string, right: string): number;
63
+ ```
64
+
65
+ | Результат | Значение |
66
+ | --------: | ----------------------------------------------- |
67
+ | `-1` | `left` должен находиться раньше `right` |
68
+ | `0` | строки полностью равны для итогового comparator |
69
+ | `1` | `left` должен находиться позже `right` |
70
+
71
+ Подходящие места:
72
+
73
+ - `array.sort` / `toSorted`;
74
+ - сортировка labels справочника;
75
+ - предсказуемый порядок смешанных технических и русских названий;
76
+ - tie-breaker после бизнес-сортировки.
77
+
78
+ ## Ограничения
79
+
80
+ - Аргументы должны быть strings; функция не приводит `null`/число.
81
+ - Это UI-сортировка, а не database collation или backend `$orderby`.
82
+ - Не используйте comparator для equality/identity: разные строки могут быть близки по основной collation.
83
+ - Порядок не является Unicode normalization; при необходимости нормализуйте вход на явной boundary.
84
+ - Для server-side таблицы сортировку полного набора должен выполнять backend, а не одна загруженная страница.
85
+ - Runtime должен поддерживать `Intl.Collator` и locale data `ru-RU`.
86
+
87
+ ## Пример с объектами
88
+
89
+ ```ts
90
+ type Option = { id: string; label: string };
91
+
92
+ const sortedOptions = options.toSorted((left, right) => {
93
+ const byLabel = compareStrings(left.label, right.label);
94
+ return byLabel !== 0 ? byLabel : compareStrings(left.id, right.id);
95
+ });
96
+ ```