@letar/forms 1.0.0 → 1.0.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.
- package/CHANGELOG.md +34 -0
- package/README.md +79 -4
- package/{chunk-G3HYXHCZ.js → chunk-4V6WBJ76.js} +14 -14
- package/chunk-4V6WBJ76.js.map +1 -0
- package/{chunk-GIBNEYK3.js → chunk-7FEQFDJ7.js} +5 -4
- package/chunk-7FEQFDJ7.js.map +1 -0
- package/i18n.js +1 -1
- package/index.js +454 -290
- package/index.js.map +1 -1
- package/offline.js +1 -1
- package/package.json +7 -2
- package/chunk-G3HYXHCZ.js.map +0 -1
- package/chunk-GIBNEYK3.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,40 @@
|
|
|
4
4
|
|
|
5
5
|
Формат основан на [Keep a Changelog](https://keepachangelog.com/ru/1.0.0/).
|
|
6
6
|
|
|
7
|
+
## [0.58.0] - 2026-03-31
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- Pluggable `AddressProvider` interface for `Form.Field.Address` and `Form.Field.City`
|
|
12
|
+
- `createDaDataProvider()` — built-in DaData provider (Russia)
|
|
13
|
+
- `createForm({ addressProvider })` — set address provider once for all fields
|
|
14
|
+
- Provider resolution: field prop → createForm context → token fallback → env
|
|
15
|
+
- `addressProvider` prop on `Form` root component
|
|
16
|
+
- `CityFieldProps` exported from types
|
|
17
|
+
- `README.en.md`: Address Provider + createForm sections
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- All JSDoc, comments, runtime errors translated to English (118 files, ~3000 lines)
|
|
22
|
+
- Default UI strings: "Save", "Reset", "Unsaved changes", "Leave", "Stay", etc.
|
|
23
|
+
- `AddressValue.data` generalized to `Record<string, unknown>` (was DaData-specific)
|
|
24
|
+
- `AddressFieldProps.token` is now optional (use `provider` instead)
|
|
25
|
+
- `DaDataSuggestion` marked as deprecated
|
|
26
|
+
- `build:npm` copies `README.en.md` as `README.md` + `README.ru.md` for npm
|
|
27
|
+
|
|
28
|
+
## [0.56.0] - 2026-03-23
|
|
29
|
+
|
|
30
|
+
### Added
|
|
31
|
+
|
|
32
|
+
- `Form.DebugValues` — интерактивный JSON-инспектор значений формы (скрыт в production)
|
|
33
|
+
- `debug` prop на `Form` для автоматического отображения DebugValues
|
|
34
|
+
- Инфраструктура публикации `@letar/forms` на npm
|
|
35
|
+
|
|
36
|
+
### Fixed
|
|
37
|
+
|
|
38
|
+
- Совместимость с `@tanstack/store` 0.9+ (Subscription API)
|
|
39
|
+
- Исправлен баг `destroy` в `form-steps`
|
|
40
|
+
|
|
7
41
|
## [0.54.1] - 2026-01-05
|
|
8
42
|
|
|
9
43
|
### Fixed
|
package/README.md
CHANGED
|
@@ -5,6 +5,8 @@ Declarative form components for React with **40+ field types**, powered by [TanS
|
|
|
5
5
|
[](https://www.npmjs.com/package/@letar/forms)
|
|
6
6
|
[](./LICENSE)
|
|
7
7
|
|
|
8
|
+
[Документация на русском](./README.ru.md)
|
|
9
|
+
|
|
8
10
|
## Quick Start
|
|
9
11
|
|
|
10
12
|
```bash
|
|
@@ -16,8 +18,15 @@ import { Form } from '@letar/forms'
|
|
|
16
18
|
import { z } from 'zod/v4'
|
|
17
19
|
|
|
18
20
|
const Schema = z.object({
|
|
19
|
-
title: z
|
|
20
|
-
|
|
21
|
+
title: z
|
|
22
|
+
.string()
|
|
23
|
+
.min(2)
|
|
24
|
+
.meta({ ui: { title: 'Title', placeholder: 'Enter...' } }),
|
|
25
|
+
rating: z
|
|
26
|
+
.number()
|
|
27
|
+
.min(0)
|
|
28
|
+
.max(10)
|
|
29
|
+
.meta({ ui: { title: 'Rating' } }),
|
|
21
30
|
})
|
|
22
31
|
|
|
23
32
|
function MyForm() {
|
|
@@ -157,6 +166,67 @@ const Schema = z.object({
|
|
|
157
166
|
</Form>
|
|
158
167
|
```
|
|
159
168
|
|
|
169
|
+
### Address Provider
|
|
170
|
+
|
|
171
|
+
Address and city fields support pluggable geocoding providers. DaData (Russia) is built-in:
|
|
172
|
+
|
|
173
|
+
```tsx
|
|
174
|
+
import { createDaDataProvider, createForm } from '@letar/forms'
|
|
175
|
+
|
|
176
|
+
// Option 1: Set once via createForm (recommended)
|
|
177
|
+
const AppForm = createForm({
|
|
178
|
+
addressProvider: createDaDataProvider({ token: process.env.DADATA_TOKEN }),
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
<AppForm.Field.Address name="address" />
|
|
182
|
+
<AppForm.Field.City name="city" />
|
|
183
|
+
|
|
184
|
+
// Option 2: Per-field provider
|
|
185
|
+
<Form.Field.Address name="address" provider={myProvider} />
|
|
186
|
+
|
|
187
|
+
// Option 3: Backward compatible token prop
|
|
188
|
+
<Form.Field.Address name="address" token="dadata-token" />
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Custom provider — implement the `AddressProvider` interface:
|
|
192
|
+
|
|
193
|
+
```typescript
|
|
194
|
+
import type { AddressProvider } from '@letar/forms'
|
|
195
|
+
|
|
196
|
+
const googlePlaces: AddressProvider = {
|
|
197
|
+
async getSuggestions(query, options) {
|
|
198
|
+
const res = await fetch(`/api/places?q=${query}&limit=${options?.count ?? 10}`)
|
|
199
|
+
const data = await res.json()
|
|
200
|
+
return data.map((item) => ({
|
|
201
|
+
label: item.description,
|
|
202
|
+
value: item.description,
|
|
203
|
+
data: item.structured,
|
|
204
|
+
}))
|
|
205
|
+
},
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### createForm — App-Level Customization
|
|
210
|
+
|
|
211
|
+
Create an extended Form with app-specific fields, selects, and address provider:
|
|
212
|
+
|
|
213
|
+
```tsx
|
|
214
|
+
import { createForm, createDaDataProvider } from '@letar/forms'
|
|
215
|
+
import { SelectCategory } from './selects/select-category'
|
|
216
|
+
|
|
217
|
+
const AppForm = createForm({
|
|
218
|
+
addressProvider: createDaDataProvider({ token: '...' }),
|
|
219
|
+
extraSelects: { Category: SelectCategory },
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
// Usage — all customizations applied automatically
|
|
223
|
+
<AppForm initialValue={data} onSubmit={save}>
|
|
224
|
+
<AppForm.Field.Address name="address" />
|
|
225
|
+
<AppForm.Select.Category name="categoryId" />
|
|
226
|
+
<AppForm.Button.Submit />
|
|
227
|
+
</AppForm>
|
|
228
|
+
```
|
|
229
|
+
|
|
160
230
|
## Subpath Exports
|
|
161
231
|
|
|
162
232
|
```tsx
|
|
@@ -176,8 +246,9 @@ import { FormI18nProvider, useFormI18n } from '@letar/forms/i18n'
|
|
|
176
246
|
| `@chakra-ui/react` | >= 3.0.0 | Yes |
|
|
177
247
|
| `framer-motion` | >= 10.0.0 | Yes |
|
|
178
248
|
| `zod` | >= 3.24.0 | Yes |
|
|
179
|
-
| `@dnd-kit/*` | >= 6.0.0 | Optional |
|
|
180
|
-
| `use-mask-input` | >= 3.0.0 | Optional |
|
|
249
|
+
| `@dnd-kit/*` | >= 6.0.0 | Optional (drag & drop in arrays) |
|
|
250
|
+
| `use-mask-input` | >= 3.0.0 | Optional (Phone, MaskedInput) |
|
|
251
|
+
| `@uiw/react-json-view` | >= 2.0.0 | Optional (Form.DebugValues) |
|
|
181
252
|
|
|
182
253
|
## Documentation
|
|
183
254
|
|
|
@@ -186,3 +257,7 @@ Full documentation and live examples: **[forms.letar.best](https://forms.letar.b
|
|
|
186
257
|
## License
|
|
187
258
|
|
|
188
259
|
[MIT](./LICENSE)
|
|
260
|
+
|
|
261
|
+
---
|
|
262
|
+
|
|
263
|
+
**Version:** 0.58.0
|
|
@@ -51,7 +51,7 @@ async function getQueueFromStorage(storageKey) {
|
|
|
51
51
|
const stored = await idb.get(key);
|
|
52
52
|
return stored ?? [];
|
|
53
53
|
} catch (error) {
|
|
54
|
-
console.error("[OfflineService]
|
|
54
|
+
console.error("[OfflineService] Error loading queue from IndexedDB:", error);
|
|
55
55
|
return [];
|
|
56
56
|
}
|
|
57
57
|
}
|
|
@@ -64,7 +64,7 @@ async function saveQueueToStorage(queue, storageKey) {
|
|
|
64
64
|
const key = storageKey ?? DEFAULT_SYNC_QUEUE_STORAGE_KEY;
|
|
65
65
|
await idb.set(key, queue);
|
|
66
66
|
} catch (error) {
|
|
67
|
-
console.error("[OfflineService]
|
|
67
|
+
console.error("[OfflineService] Error saving queue to IndexedDB:", error);
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
70
|
async function addToQueue(action, storageKey) {
|
|
@@ -135,7 +135,7 @@ async function clearQueue(storageKey) {
|
|
|
135
135
|
const key = storageKey ?? DEFAULT_SYNC_QUEUE_STORAGE_KEY;
|
|
136
136
|
await idb.del(key);
|
|
137
137
|
} catch (error) {
|
|
138
|
-
console.error("[OfflineService]
|
|
138
|
+
console.error("[OfflineService] Error clearing queue from IndexedDB:", error);
|
|
139
139
|
}
|
|
140
140
|
}
|
|
141
141
|
function createSyncQueueStore(storageKey) {
|
|
@@ -217,7 +217,7 @@ function useOfflineStatus() {
|
|
|
217
217
|
},
|
|
218
218
|
() => isOffline,
|
|
219
219
|
() => false
|
|
220
|
-
// SSR fallback —
|
|
220
|
+
// SSR fallback — assume online
|
|
221
221
|
);
|
|
222
222
|
}
|
|
223
223
|
var defaultSyncQueueStore = createSyncQueueStore();
|
|
@@ -253,7 +253,7 @@ function useSyncQueue() {
|
|
|
253
253
|
const processQueue = useCallback(
|
|
254
254
|
async (handler) => {
|
|
255
255
|
if (isOffline2) {
|
|
256
|
-
console.warn("[SyncQueue]
|
|
256
|
+
console.warn("[SyncQueue] Cannot process queue in offline mode");
|
|
257
257
|
return [];
|
|
258
258
|
}
|
|
259
259
|
setIsProcessing(true);
|
|
@@ -304,7 +304,7 @@ function useOfflineForm({
|
|
|
304
304
|
queueItemId: queueItem.id
|
|
305
305
|
};
|
|
306
306
|
} catch (error) {
|
|
307
|
-
const errorMessage = error instanceof Error ? error.message : "
|
|
307
|
+
const errorMessage = error instanceof Error ? error.message : "Error saving to queue";
|
|
308
308
|
onError?.(errorMessage);
|
|
309
309
|
return {
|
|
310
310
|
success: false,
|
|
@@ -325,7 +325,7 @@ function useOfflineForm({
|
|
|
325
325
|
queued: false
|
|
326
326
|
};
|
|
327
327
|
} catch (error) {
|
|
328
|
-
const errorMessage = error instanceof Error ? error.message : "
|
|
328
|
+
const errorMessage = error instanceof Error ? error.message : "Submission error";
|
|
329
329
|
onError?.(errorMessage);
|
|
330
330
|
return {
|
|
331
331
|
success: false,
|
|
@@ -352,7 +352,7 @@ function useOfflineForm({
|
|
|
352
352
|
processQueue(handleQueuedAction).then((results) => {
|
|
353
353
|
const failed = results.filter((r) => !r.success);
|
|
354
354
|
if (failed.length > 0) {
|
|
355
|
-
console.warn(`[OfflineForm] ${failed.length}
|
|
355
|
+
console.warn(`[OfflineForm] ${failed.length} actions failed to sync`);
|
|
356
356
|
}
|
|
357
357
|
}).finally(() => {
|
|
358
358
|
processingRef.current = false;
|
|
@@ -369,7 +369,7 @@ function useOfflineForm({
|
|
|
369
369
|
};
|
|
370
370
|
}
|
|
371
371
|
function FormOfflineIndicator({
|
|
372
|
-
label = "
|
|
372
|
+
label = "Offline mode",
|
|
373
373
|
colorPalette = "orange",
|
|
374
374
|
variant = "subtle",
|
|
375
375
|
...rest
|
|
@@ -385,9 +385,9 @@ function FormOfflineIndicator({
|
|
|
385
385
|
}
|
|
386
386
|
function FormSyncStatus({
|
|
387
387
|
showWhenEmpty = false,
|
|
388
|
-
syncingLabel = "
|
|
389
|
-
pendingLabel = (count) =>
|
|
390
|
-
syncedLabel = "
|
|
388
|
+
syncingLabel = "Syncing...",
|
|
389
|
+
pendingLabel = (count) => `Pending: ${count}`,
|
|
390
|
+
syncedLabel = "Synced",
|
|
391
391
|
colorPalette = "blue",
|
|
392
392
|
...rest
|
|
393
393
|
}) {
|
|
@@ -431,5 +431,5 @@ function FormSyncStatus({
|
|
|
431
431
|
}
|
|
432
432
|
|
|
433
433
|
export { FormOfflineIndicator, FormSyncStatus, addToQueue, clearQueue, createSyncQueueStore, getOfflineStatus, getQueueFromStorage, processQueueItem, removeFromQueue, subscribeToStatusChanges, useOfflineForm, useOfflineStatus, useSyncQueue };
|
|
434
|
-
//# sourceMappingURL=chunk-
|
|
435
|
-
//# sourceMappingURL=chunk-
|
|
434
|
+
//# sourceMappingURL=chunk-4V6WBJ76.js.map
|
|
435
|
+
//# sourceMappingURL=chunk-4V6WBJ76.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/lib/offline/offline-service.ts","../src/lib/offline/use-offline-status.ts","../src/lib/offline/use-sync-queue.ts","../src/lib/offline/use-offline-form.ts","../src/lib/offline/form-offline-indicator.tsx","../src/lib/offline/form-sync-status.tsx"],"names":["listeners","notifyListeners","isOffline","useSyncExternalStore","useState","useCallback","useEffect","jsxs","HStack","jsx","Icon","Badge"],"mappings":";;;;;;AAiBA,SAAS,SAAA,GAAqB;AAC5B,EAAA,OAAO,OAAO,MAAA,KAAW,WAAA,IAAe,OAAO,SAAA,KAAc,WAAA;AAC/D;AAMA,IAAI,SAAA,GAAgD,IAAA;AACpD,eAAe,MAAA,GAAsD;AACnE,EAAA,IAAI,CAAC,WAAU,EAAG;AAChB,IAAA,OAAO,IAAA;AAAA,EACT;AACA,EAAA,IAAI,CAAC,SAAA,EAAW;AACd,IAAA,SAAA,GAAY,MAAM,OAAO,YAAY,CAAA;AAAA,EACvC;AACA,EAAA,OAAO,SAAA;AACT;AAMA,IAAM,8BAAA,GAAiC,sBAAA;AAUhC,SAAS,gBAAA,GAA4B;AAC1C,EAAA,IAAI,OAAO,cAAc,WAAA,EAAa;AACpC,IAAA,OAAO,KAAA;AAAA,EACT;AACA,EAAA,OAAO,CAAC,SAAA,CAAU,MAAA;AACpB;AAOO,SAAS,yBAAyB,QAAA,EAAoD;AAC3F,EAAA,IAAI,OAAO,WAAW,WAAA,EAAa;AAEjC,IAAA,OAAO,MAAM;AAAA,IAAC,CAAA;AAAA,EAChB;AAEA,EAAA,MAAM,YAAA,GAAe,MAAM,QAAA,CAAS,KAAK,CAAA;AACzC,EAAA,MAAM,aAAA,GAAgB,MAAM,QAAA,CAAS,IAAI,CAAA;AAEzC,EAAA,MAAA,CAAO,gBAAA,CAAiB,UAAU,YAAY,CAAA;AAC9C,EAAA,MAAA,CAAO,gBAAA,CAAiB,WAAW,aAAa,CAAA;AAEhD,EAAA,OAAO,MAAM;AACX,IAAA,MAAA,CAAO,mBAAA,CAAoB,UAAU,YAAY,CAAA;AACjD,IAAA,MAAA,CAAO,mBAAA,CAAoB,WAAW,aAAa,CAAA;AAAA,EACrD,CAAA;AACF;AASA,SAAS,UAAA,GAAqB;AAC5B,EAAA,OAAO,CAAA,EAAG,IAAA,CAAK,GAAA,EAAK,IAAI,IAAA,CAAK,MAAA,EAAO,CAAE,QAAA,CAAS,EAAE,CAAA,CAAE,KAAA,CAAM,CAAA,EAAG,CAAC,CAAC,CAAA,CAAA;AAChE;AAKA,eAAsB,oBAAoB,UAAA,EAA+C;AACvF,EAAA,IAAI;AACF,IAAA,MAAM,GAAA,GAAM,MAAM,MAAA,EAAO;AACzB,IAAA,IAAI,CAAC,GAAA,EAAK;AACR,MAAA,OAAO,EAAC;AAAA,IACV;AACA,IAAA,MAAM,MAAM,UAAA,IAAc,8BAAA;AAC1B,IAAA,MAAM,MAAA,GAAS,MAAM,GAAA,CAAI,GAAA,CAAqB,GAAG,CAAA;AACjD,IAAA,OAAO,UAAU,EAAC;AAAA,EACpB,SAAS,KAAA,EAAO;AACd,IAAA,OAAA,CAAQ,KAAA,CAAM,wDAAwD,KAAK,CAAA;AAC3E,IAAA,OAAO,EAAC;AAAA,EACV;AACF;AAKA,eAAe,kBAAA,CAAmB,OAAwB,UAAA,EAAoC;AAC5F,EAAA,IAAI;AACF,IAAA,MAAM,GAAA,GAAM,MAAM,MAAA,EAAO;AACzB,IAAA,IAAI,CAAC,GAAA,EAAK;AACR,MAAA;AAAA,IACF;AACA,IAAA,MAAM,MAAM,UAAA,IAAc,8BAAA;AAC1B,IAAA,MAAM,GAAA,CAAI,GAAA,CAAI,GAAA,EAAK,KAAK,CAAA;AAAA,EAC1B,SAAS,KAAA,EAAO;AACd,IAAA,OAAA,CAAQ,KAAA,CAAM,qDAAqD,KAAK,CAAA;AAAA,EAC1E;AACF;AAKA,eAAsB,UAAA,CAAW,QAAoB,UAAA,EAA6C;AAChG,EAAA,MAAM,KAAA,GAAQ,MAAM,mBAAA,CAAoB,UAAU,CAAA;AAElD,EAAA,MAAM,IAAA,GAAsB;AAAA,IAC1B,IAAI,UAAA,EAAW;AAAA,IACf,MAAA;AAAA,IACA,SAAA,EAAW,KAAK,GAAA,EAAI;AAAA,IACpB,QAAA,EAAU,CAAA;AAAA,IACV,WAAA,EAAa,CAAA;AAAA,IACb,MAAA,EAAQ;AAAA,GACV;AAEA,EAAA,KAAA,CAAM,KAAK,IAAI,CAAA;AACf,EAAA,MAAM,kBAAA,CAAmB,OAAO,UAAU,CAAA;AAE1C,EAAA,OAAO,IAAA;AACT;AAKA,eAAsB,eAAA,CAAgB,IAAY,UAAA,EAAuC;AACvF,EAAA,MAAM,KAAA,GAAQ,MAAM,mBAAA,CAAoB,UAAU,CAAA;AAClD,EAAA,MAAM,QAAQ,KAAA,CAAM,SAAA,CAAU,CAAC,IAAA,KAAS,IAAA,CAAK,OAAO,EAAE,CAAA;AAEtD,EAAA,IAAI,UAAU,EAAA,EAAI;AAChB,IAAA,OAAO,KAAA;AAAA,EACT;AAEA,EAAA,KAAA,CAAM,MAAA,CAAO,OAAO,CAAC,CAAA;AACrB,EAAA,MAAM,kBAAA,CAAmB,OAAO,UAAU,CAAA;AAE1C,EAAA,OAAO,IAAA;AACT;AAKA,eAAsB,gBAAA,CAAiB,MAAqB,OAAA,EAAyD;AACnH,EAAA,IAAI;AACF,IAAA,MAAM,MAAA,GAAS,MAAM,OAAA,CAAQ,IAAA,CAAK,MAAM,CAAA;AAExC,IAAA,IAAI,OAAO,OAAA,EAAS;AAClB,MAAA,OAAO;AAAA,QACL,OAAA,EAAS,IAAA;AAAA,QACT,IAAA,EAAM,EAAE,GAAG,IAAA,EAAM,QAAQ,QAAA;AAAkB,OAC7C;AAAA,IACF;AAGA,IAAA,MAAM,WAAA,GAA6B;AAAA,MACjC,GAAG,IAAA;AAAA,MACH,QAAA,EAAU,KAAK,QAAA,GAAW,CAAA;AAAA,MAC1B,QAAQ,IAAA,CAAK,QAAA,GAAW,CAAA,IAAK,IAAA,CAAK,cAAc,QAAA,GAAW,SAAA;AAAA,MAC3D,OAAO,MAAA,CAAO;AAAA,KAChB;AAEA,IAAA,OAAO;AAAA,MACL,OAAA,EAAS,KAAA;AAAA,MACT,IAAA,EAAM,WAAA;AAAA,MACN,OAAO,MAAA,CAAO;AAAA,KAChB;AAAA,EACF,SAAS,KAAA,EAAO;AAEd,IAAA,MAAM,YAAA,GAAe,KAAA,YAAiB,KAAA,GAAQ,KAAA,CAAM,OAAA,GAAU,eAAA;AAC9D,IAAA,MAAM,WAAA,GAA6B;AAAA,MACjC,GAAG,IAAA;AAAA,MACH,QAAA,EAAU,KAAK,QAAA,GAAW,CAAA;AAAA,MAC1B,QAAQ,IAAA,CAAK,QAAA,GAAW,CAAA,IAAK,IAAA,CAAK,cAAc,QAAA,GAAW,SAAA;AAAA,MAC3D,KAAA,EAAO;AAAA,KACT;AAEA,IAAA,OAAO;AAAA,MACL,OAAA,EAAS,KAAA;AAAA,MACT,IAAA,EAAM,WAAA;AAAA,MACN,KAAA,EAAO;AAAA,KACT;AAAA,EACF;AACF;AAKA,eAAsB,WAAW,UAAA,EAAoC;AACnE,EAAA,IAAI;AACF,IAAA,MAAM,GAAA,GAAM,MAAM,MAAA,EAAO;AACzB,IAAA,IAAI,CAAC,GAAA,EAAK;AACR,MAAA;AAAA,IACF;AACA,IAAA,MAAM,MAAM,UAAA,IAAc,8BAAA;AAC1B,IAAA,MAAM,GAAA,CAAI,IAAI,GAAG,CAAA;AAAA,EACnB,SAAS,KAAA,EAAO;AACd,IAAA,OAAA,CAAQ,KAAA,CAAM,yDAAyD,KAAK,CAAA;AAAA,EAC9E;AACF;AAKO,SAAS,qBAAqB,UAAA,EAAqC;AACxE,EAAA,IAAI,QAAyB,EAAC;AAC9B,EAAA,MAAMA,UAAAA,uBAAgB,GAAA,EAAgB;AACtC,EAAA,MAAM,MAAM,UAAA,IAAc,8BAAA;AAE1B,EAAA,MAAMC,mBAAkB,MAAM;AAC5B,IAAAD,UAAAA,CAAU,OAAA,CAAQ,CAAC,QAAA,KAAa,UAAU,CAAA;AAAA,EAC5C,CAAA;AAEA,EAAA,OAAO;AAAA,IACL,UAAU,MAAM,KAAA;AAAA,IAEhB,cAAA,EAAgB,MAAM,KAAA,CAAM,MAAA;AAAA,IAE5B,SAAA,EAAW,CAAC,QAAA,KAAyB;AACnC,MAAAA,UAAAA,CAAU,IAAI,QAAQ,CAAA;AACtB,MAAA,OAAO,MAAM;AACX,QAAAA,UAAAA,CAAU,OAAO,QAAQ,CAAA;AAAA,MAC3B,CAAA;AAAA,IACF,CAAA;AAAA,IAEA,YAAY,YAAY;AACtB,MAAA,KAAA,GAAQ,MAAM,oBAAoB,GAAG,CAAA;AACrC,MAAAC,gBAAAA,EAAgB;AAAA,IAClB,CAAA;AAAA,IAEA,GAAA,EAAK,OAAO,MAAA,KAAuB;AACjC,MAAA,MAAM,IAAA,GAAO,MAAM,UAAA,CAAW,MAAA,EAAQ,GAAG,CAAA;AACzC,MAAA,KAAA,CAAM,KAAK,IAAI,CAAA;AACf,MAAAA,gBAAAA,EAAgB;AAChB,MAAA,OAAO,IAAA;AAAA,IACT,CAAA;AAAA,IAEA,MAAA,EAAQ,OAAO,EAAA,KAAe;AAC5B,MAAA,MAAM,MAAA,GAAS,MAAM,eAAA,CAAgB,EAAA,EAAI,GAAG,CAAA;AAC5C,MAAA,IAAI,MAAA,EAAQ;AACV,QAAA,KAAA,GAAQ,MAAM,MAAA,CAAO,CAAC,IAAA,KAAS,IAAA,CAAK,OAAO,EAAE,CAAA;AAC7C,QAAAA,gBAAAA,EAAgB;AAAA,MAClB;AACA,MAAA,OAAO,MAAA;AAAA,IACT,CAAA;AAAA,IAEA,UAAA,EAAY,OAAO,OAAA,KAA+B;AAChD,MAAA,MAAM,UAAgC,EAAC;AAEvC,MAAA,KAAA,MAAW,QAAQ,KAAA,EAAO;AACxB,QAAA,IAAI,IAAA,CAAK,WAAW,SAAA,EAAW;AAC7B,UAAA,MAAM,MAAA,GAAS,MAAM,gBAAA,CAAiB,IAAA,EAAM,OAAO,CAAA;AACnD,UAAA,OAAA,CAAQ,KAAK,MAAM,CAAA;AAGnB,UAAA,IAAI,MAAA,CAAO,OAAA,IAAW,MAAA,CAAO,IAAA,EAAM;AACjC,YAAA,MAAM,eAAA,CAAgB,IAAA,CAAK,EAAA,EAAI,GAAG,CAAA;AAClC,YAAA,KAAA,GAAQ,MAAM,MAAA,CAAO,CAAC,MAAM,CAAA,CAAE,EAAA,KAAO,KAAK,EAAE,CAAA;AAAA,UAC9C,CAAA,MAAA,IAAW,OAAO,IAAA,EAAM;AAEtB,YAAA,MAAM,KAAA,GAAQ,MAAM,SAAA,CAAU,CAAC,MAAM,CAAA,CAAE,EAAA,KAAO,KAAK,EAAE,CAAA;AACrD,YAAA,IAAI,UAAU,EAAA,EAAI;AAChB,cAAA,KAAA,CAAM,KAAK,IAAI,MAAA,CAAO,IAAA;AAAA,YACxB;AACA,YAAA,MAAM,kBAAA,CAAmB,OAAO,GAAG,CAAA;AAAA,UACrC;AAAA,QACF;AAAA,MACF;AAEA,MAAAA,gBAAAA,EAAgB;AAChB,MAAA,OAAO,OAAA;AAAA,IACT;AAAA,GACF;AACF;ACjSA,IAAI,SAAA,GAAY,KAAA;AAEhB,IAAM,SAAA,uBAAgB,GAAA,EAAgB;AAEtC,IAAM,kBAAkB,MAAM;AAC5B,EAAA,SAAA,CAAU,OAAA,CAAQ,CAAC,QAAA,KAAa,QAAA,EAAU,CAAA;AAC5C,CAAA;AAGA,IAAI,OAAO,WAAW,WAAA,EAAa;AACjC,EAAA,SAAA,GAAY,gBAAA,EAAiB;AAE7B,EAAA,wBAAA,CAAyB,CAAC,OAAA,KAAY;AACpC,IAAA,SAAA,GAAY,OAAA;AACZ,IAAA,eAAA,EAAgB;AAAA,EAClB,CAAC,CAAA;AACH;AAsBO,SAAS,gBAAA,GAA4B;AAC1C,EAAA,OAAO,oBAAA;AAAA,IACL,CAAC,QAAA,KAAa;AACZ,MAAA,SAAA,CAAU,IAAI,QAAQ,CAAA;AACtB,MAAA,OAAO,MAAM;AACX,QAAA,SAAA,CAAU,OAAO,QAAQ,CAAA;AAAA,MAC3B,CAAA;AAAA,IACF,CAAA;AAAA,IACA,MAAM,SAAA;AAAA,IACN,MAAM;AAAA;AAAA,GACR;AACF;AC/CA,IAAM,wBAAwB,oBAAA,EAAqB;AACnD,IAAM,cAA+B,EAAC;AAGtC,IAAI,WAAA,GAAc,KAAA;AAElB,IAAM,aAAa,YAAY;AAC7B,EAAA,IAAI,CAAC,WAAA,IAAe,OAAO,MAAA,KAAW,WAAA,EAAa;AACjD,IAAA,WAAA,GAAc,IAAA;AACd,IAAA,MAAM,sBAAsB,UAAA,EAAW;AAAA,EACzC;AACF,CAAA;AAwCO,SAAS,YAAA,GAAmC;AACjD,EAAA,MAAM,CAAC,SAAA,EAAW,YAAY,CAAA,GAAI,SAAS,IAAI,CAAA;AAC/C,EAAA,MAAM,CAAC,YAAA,EAAc,eAAe,CAAA,GAAI,SAAS,KAAK,CAAA;AACtD,EAAA,MAAMC,aAAY,gBAAA,EAAiB;AAGnC,EAAA,MAAM,KAAA,GAAQC,oBAAAA;AAAA,IACZ,CAAC,QAAA,KAAa,qBAAA,CAAsB,SAAA,CAAU,QAAQ,CAAA;AAAA,IACtD,MAAM,sBAAsB,QAAA,EAAS;AAAA,IACrC,MAAM;AAAA;AAAA,GACR;AAGA,EAAA,SAAA,CAAU,MAAM;AACd,IAAA,UAAA,EAAW,CAAE,KAAK,MAAM;AACtB,MAAA,YAAA,CAAa,KAAK,CAAA;AAAA,IACpB,CAAC,CAAA;AAAA,EACH,CAAA,EAAG,EAAE,CAAA;AAGL,EAAA,MAAM,SAAA,GAAY,WAAA,CAAY,OAAO,MAAA,KAA+C;AAClF,IAAA,OAAO,qBAAA,CAAsB,IAAI,MAAM,CAAA;AAAA,EACzC,CAAA,EAAG,EAAE,CAAA;AAGL,EAAA,MAAM,YAAA,GAAe,WAAA,CAAY,OAAO,EAAA,KAAiC;AACvE,IAAA,OAAO,qBAAA,CAAsB,OAAO,EAAE,CAAA;AAAA,EACxC,CAAA,EAAG,EAAE,CAAA;AAGL,EAAA,MAAM,YAAA,GAAe,WAAA;AAAA,IACnB,OAAO,OAAA,KAA8D;AACnE,MAAA,IAAID,UAAAA,EAAW;AACb,QAAA,OAAA,CAAQ,KAAK,kDAAkD,CAAA;AAC/D,QAAA,OAAO,EAAC;AAAA,MACV;AAEA,MAAA,eAAA,CAAgB,IAAI,CAAA;AACpB,MAAA,IAAI;AACF,QAAA,OAAO,MAAM,qBAAA,CAAsB,UAAA,CAAW,OAAO,CAAA;AAAA,MACvD,CAAA,SAAE;AACA,QAAA,eAAA,CAAgB,KAAK,CAAA;AAAA,MACvB;AAAA,IACF,CAAA;AAAA,IACA,CAACA,UAAS;AAAA,GACZ;AAEA,EAAA,OAAO;AAAA,IACL,KAAA;AAAA,IACA,aAAa,KAAA,CAAM,MAAA;AAAA,IACnB,YAAA,EAAc,MAAM,MAAA,CAAO,CAAC,SAAS,IAAA,CAAK,MAAA,KAAW,SAAS,CAAA,CAAE,MAAA;AAAA,IAChE,SAAA;AAAA,IACA,YAAA;AAAA,IACA,SAAA;AAAA,IACA,YAAA;AAAA,IACA;AAAA,GACF;AACF;;;AChEO,SAAS,cAAA,CAAiC;AAAA,EAC/C,UAAA;AAAA,EACA,YAAA;AAAA,EACA,SAAA;AAAA,EACA,QAAA;AAAA,EACA;AACF,CAAA,EAAsD;AACpD,EAAA,MAAMA,aAAY,gBAAA,EAAiB;AACnC,EAAA,MAAM,EAAE,SAAA,EAAW,YAAA,EAAc,cAAc,YAAA,EAAc,WAAA,KAAgB,YAAA,EAAa;AAC1F,EAAA,MAAM,CAAC,eAAA,EAAiB,kBAAkB,CAAA,GAAIE,SAAwB,IAAI,CAAA;AAG1E,EAAA,MAAM,aAAA,GAAgB,OAAO,KAAK,CAAA;AAKlC,EAAA,MAAM,MAAA,GAASC,WAAAA;AAAA,IACb,OAAO,KAAA,KAA2C;AAEhD,MAAA,IAAIH,UAAAA,EAAW;AACb,QAAA,IAAI;AACF,UAAA,MAAM,SAAA,GAAY,MAAM,SAAA,CAAU;AAAA,YAChC,IAAA,EAAM,UAAA;AAAA,YACN,OAAA,EAAS;AAAA,WACV,CAAA;AAED,UAAA,QAAA,IAAW;AAEX,UAAA,OAAO;AAAA,YACL,OAAA,EAAS,IAAA;AAAA,YACT,MAAA,EAAQ,IAAA;AAAA,YACR,aAAa,SAAA,CAAU;AAAA,WACzB;AAAA,QACF,SAAS,KAAA,EAAO;AACd,UAAA,MAAM,YAAA,GAAe,KAAA,YAAiB,KAAA,GAAQ,KAAA,CAAM,OAAA,GAAU,uBAAA;AAC9D,UAAA,OAAA,GAAU,YAAY,CAAA;AACtB,UAAA,OAAO;AAAA,YACL,OAAA,EAAS,KAAA;AAAA,YACT,KAAA,EAAO;AAAA,WACT;AAAA,QACF;AAAA,MACF;AAGA,MAAA,IAAI;AACF,QAAA,MAAM,MAAA,GAAS,MAAM,YAAA,CAAa,KAAK,CAAA;AAEvC,QAAA,IAAI,OAAO,OAAA,EAAS;AAClB,UAAA,SAAA,IAAY;AAAA,QACd,CAAA,MAAA,IAAW,OAAO,KAAA,EAAO;AACvB,UAAA,OAAA,GAAU,OAAO,KAAK,CAAA;AAAA,QACxB;AAEA,QAAA,OAAO;AAAA,UACL,SAAS,MAAA,CAAO,OAAA;AAAA,UAChB,OAAO,MAAA,CAAO,KAAA;AAAA,UACd,MAAA,EAAQ;AAAA,SACV;AAAA,MACF,SAAS,KAAA,EAAO;AACd,QAAA,MAAM,YAAA,GAAe,KAAA,YAAiB,KAAA,GAAQ,KAAA,CAAM,OAAA,GAAU,kBAAA;AAC9D,QAAA,OAAA,GAAU,YAAY,CAAA;AACtB,QAAA,OAAO;AAAA,UACL,OAAA,EAAS,KAAA;AAAA,UACT,KAAA,EAAO,YAAA;AAAA,UACP,MAAA,EAAQ;AAAA,SACV;AAAA,MACF;AAAA,IACF,CAAA;AAAA,IACA,CAACA,UAAAA,EAAW,UAAA,EAAY,WAAW,YAAA,EAAc,SAAA,EAAW,UAAU,OAAO;AAAA,GAC/E;AAMA,EAAA,MAAM,kBAAA,GAAqBG,WAAAA;AAAA,IACzB,OAAO,MAAA,KAAsE;AAE3E,MAAA,IAAI,MAAA,CAAO,SAAS,UAAA,EAAY;AAC9B,QAAA,OAAO,EAAE,SAAS,IAAA,EAAK;AAAA,MACzB;AAEA,MAAA,OAAO,YAAA,CAAa,OAAO,OAAY,CAAA;AAAA,IACzC,CAAA;AAAA,IACA,CAAC,YAAY,YAAY;AAAA,GAC3B;AAGA,EAAAC,UAAU,MAAM;AAEd,IAAA,IAAI,CAACJ,UAAAA,IAAa,YAAA,GAAe,CAAA,IAAK,CAAC,cAAc,OAAA,EAAS;AAC5D,MAAA,aAAA,CAAc,OAAA,GAAU,IAAA;AACxB,MAAA,kBAAA,CAAmB,IAAA,CAAK,KAAK,CAAA;AAE7B,MAAA,YAAA,CAAa,kBAAkB,CAAA,CAC5B,IAAA,CAAK,CAAC,OAAA,KAAY;AACjB,QAAA,MAAM,SAAS,OAAA,CAAQ,MAAA,CAAO,CAAC,CAAA,KAAM,CAAC,EAAE,OAAO,CAAA;AAC/C,QAAA,IAAI,MAAA,CAAO,SAAS,CAAA,EAAG;AACrB,UAAA,OAAA,CAAQ,IAAA,CAAK,CAAA,cAAA,EAAiB,MAAA,CAAO,MAAM,CAAA,uBAAA,CAAyB,CAAA;AAAA,QACtE;AAAA,MACF,CAAC,CAAA,CACA,OAAA,CAAQ,MAAM;AACb,QAAA,aAAA,CAAc,OAAA,GAAU,KAAA;AAAA,MAC1B,CAAC,CAAA;AAAA,IACL;AAAA,EACF,GAAG,CAACA,UAAAA,EAAW,YAAA,EAAc,YAAA,EAAc,kBAAkB,CAAC,CAAA;AAE9D,EAAA,OAAO;AAAA,IACL,MAAA;AAAA,IACA,SAAA,EAAAA,UAAAA;AAAA,IACA,YAAA;AAAA,IACA,WAAA;AAAA,IACA,YAAA;AAAA,IACA;AAAA,GACF;AACF;ACvIO,SAAS,oBAAA,CAAqB;AAAA,EACnC,KAAA,GAAQ,cAAA;AAAA,EACR,YAAA,GAAe,QAAA;AAAA,EACf,OAAA,GAAU,QAAA;AAAA,EACV,GAAG;AACL,CAAA,EAAyD;AACvD,EAAA,MAAMA,aAAY,gBAAA,EAAiB;AAEnC,EAAA,IAAI,CAACA,UAAAA,EAAW;AACd,IAAA,OAAO,IAAA;AAAA,EACT;AAEA,EAAA,uBACE,GAAA,CAAC,KAAA,EAAA,EAAM,YAAA,EAA4B,OAAA,EAAkB,aAAA,EAAY,mBAAA,EAAqB,GAAG,IAAA,EACvF,QAAA,kBAAA,IAAA,CAAC,MAAA,EAAA,EAAO,GAAA,EAAK,CAAA,EACX,QAAA,EAAA;AAAA,oBAAA,GAAA,CAAC,QAAK,OAAA,EAAO,IAAA,EAAC,SAAS,CAAA,EACrB,QAAA,kBAAA,GAAA,CAAC,aAAU,CAAA,EACb,CAAA;AAAA,oBACA,GAAA,CAAC,UAAM,QAAA,EAAA,KAAA,EAAM;AAAA,GAAA,EACf,CAAA,EACF,CAAA;AAEJ;ACnBO,SAAS,cAAA,CAAe;AAAA,EAC7B,aAAA,GAAgB,KAAA;AAAA,EAChB,YAAA,GAAe,YAAA;AAAA,EACf,YAAA,GAAe,CAAC,KAAA,KAAkB,CAAA,SAAA,EAAY,KAAK,CAAA,CAAA;AAAA,EACnD,WAAA,GAAc,QAAA;AAAA,EACd,YAAA,GAAe,MAAA;AAAA,EACf,GAAG;AACL,CAAA,EAAmD;AACjD,EAAA,MAAMA,aAAY,gBAAA,EAAiB;AACnC,EAAA,MAAM,EAAE,YAAA,EAAc,YAAA,EAAa,GAAI,YAAA,EAAa;AAGpD,EAAA,IAAI,CAACA,UAAAA,IAAa,YAAA,KAAiB,KAAK,CAAC,YAAA,IAAgB,CAAC,aAAA,EAAe;AACvE,IAAA,OAAO,IAAA;AAAA,EACT;AAGA,EAAA,MAAM,gBAAgB,MAAM;AAE1B,IAAA,IAAI,YAAA,EAAc;AAChB,MAAA,uBACEK,IAAAA,CAACC,MAAAA,EAAA,EAAO,KAAK,CAAA,EACX,QAAA,EAAA;AAAA,wBAAAC,GAAAA,CAAC,OAAA,EAAA,EAAQ,IAAA,EAAK,IAAA,EAAK,CAAA;AAAA,wBACnBA,GAAAA,CAAC,MAAA,EAAA,EAAM,QAAA,EAAA,YAAA,EAAa;AAAA,OAAA,EACtB,CAAA;AAAA,IAEJ;AAGA,IAAA,IAAI,eAAe,CAAA,EAAG;AACpB,MAAA,MAAM,QAAQ,OAAO,YAAA,KAAiB,UAAA,GAAa,YAAA,CAAa,YAAY,CAAA,GAAI,YAAA;AAChF,MAAA,uBACEF,IAAAA,CAACC,MAAAA,EAAA,EAAO,KAAK,CAAA,EACX,QAAA,EAAA;AAAA,wBAAAC,GAAAA,CAACC,IAAAA,EAAA,EAAK,OAAA,EAAO,IAAA,EAAC,SAAS,CAAA,EACrB,QAAA,kBAAAD,GAAAA,CAAC,OAAA,EAAA,EAAQ,CAAA,EACX,CAAA;AAAA,wBACAA,GAAAA,CAAC,MAAA,EAAA,EAAM,QAAA,EAAA,KAAA,EAAM;AAAA,OAAA,EACf,CAAA;AAAA,IAEJ;AAGA,IAAA,uBACEF,IAAAA,CAACC,MAAAA,EAAA,EAAO,KAAK,CAAA,EACX,QAAA,EAAA;AAAA,sBAAAC,GAAAA,CAACC,IAAAA,EAAA,EAAK,OAAA,EAAO,IAAA,EAAC,SAAS,CAAA,EACrB,QAAA,kBAAAD,GAAAA,CAAC,OAAA,EAAA,EAAQ,CAAA,EACX,CAAA;AAAA,sBACAA,GAAAA,CAAC,MAAA,EAAA,EAAM,QAAA,EAAA,WAAA,EAAY;AAAA,KAAA,EACrB,CAAA;AAAA,EAEJ,CAAA;AAGA,EAAA,MAAM,qBAAA,GAAwB,YAAA,GAAe,CAAA,GAAI,QAAA,GAAW,eAAe,YAAA,GAAe,OAAA;AAE1F,EAAA,uBACEA,GAAAA;AAAA,IAACE,KAAAA;AAAA,IAAA;AAAA,MACC,YAAA,EAAc,qBAAA;AAAA,MACd,OAAA,EAAQ,QAAA;AAAA,MACR,aAAA,EAAY,aAAA;AAAA,MACZ,oBAAA,EAAoB,YAAA;AAAA,MACpB,iBAAA,EAAiB,YAAA;AAAA,MAChB,GAAG,IAAA;AAAA,MAEH,QAAA,EAAA,aAAA;AAAc;AAAA,GACjB;AAEJ","file":"chunk-4V6WBJ76.js","sourcesContent":["/**\n * Offline functionality service\n *\n * Business logic:\n * - Online/offline status detection\n * - Sync action queue in IndexedDB\n */\n\nimport type { ProcessQueueResult, SyncAction, SyncActionHandler, SyncQueueItem, SyncQueueStore } from './types'\n\n// ============================================\n// LAZY IMPORT IDB-KEYVAL\n// ============================================\n\n/**\n * Check that we are in a browser with IndexedDB support\n */\nfunction canUseIDB(): boolean {\n return typeof window !== 'undefined' && typeof indexedDB !== 'undefined'\n}\n\n/**\n * Lazy import of idb-keyval to avoid SSR issues\n * Cache the promise to prevent repeated imports\n */\nlet idbModule: typeof import('idb-keyval') | null = null\nasync function getIDB(): Promise<typeof import('idb-keyval') | null> {\n if (!canUseIDB()) {\n return null\n }\n if (!idbModule) {\n idbModule = await import('idb-keyval')\n }\n return idbModule\n}\n\n// ============================================\n// CONSTANTS\n// ============================================\n\nconst DEFAULT_SYNC_QUEUE_STORAGE_KEY = 'lena-form-sync-queue'\n\n// ============================================\n// CONNECTION STATUS\n// ============================================\n\n/**\n * Get current offline status\n * @returns true if offline, false if online\n */\nexport function getOfflineStatus(): boolean {\n if (typeof navigator === 'undefined') {\n return false\n }\n return !navigator.onLine\n}\n\n/**\n * Subscribe to connection status changes\n * @param callback - function called when status changes\n * @returns unsubscribe function\n */\nexport function subscribeToStatusChanges(callback: (isOffline: boolean) => void): () => void {\n if (typeof window === 'undefined') {\n // eslint-disable-next-line @typescript-eslint/no-empty-function\n return () => {}\n }\n\n const handleOnline = () => callback(false)\n const handleOffline = () => callback(true)\n\n window.addEventListener('online', handleOnline)\n window.addEventListener('offline', handleOffline)\n\n return () => {\n window.removeEventListener('online', handleOnline)\n window.removeEventListener('offline', handleOffline)\n }\n}\n\n// ============================================\n// SYNC QUEUE\n// ============================================\n\n/**\n * Generate unique ID\n */\nfunction generateId(): string {\n return `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`\n}\n\n/**\n * Get queue from IndexedDB\n */\nexport async function getQueueFromStorage(storageKey?: string): Promise<SyncQueueItem[]> {\n try {\n const idb = await getIDB()\n if (!idb) {\n return []\n }\n const key = storageKey ?? DEFAULT_SYNC_QUEUE_STORAGE_KEY\n const stored = await idb.get<SyncQueueItem[]>(key)\n return stored ?? []\n } catch (error) {\n console.error('[OfflineService] Error loading queue from IndexedDB:', error)\n return []\n }\n}\n\n/**\n * Save queue to IndexedDB\n */\nasync function saveQueueToStorage(queue: SyncQueueItem[], storageKey?: string): Promise<void> {\n try {\n const idb = await getIDB()\n if (!idb) {\n return\n }\n const key = storageKey ?? DEFAULT_SYNC_QUEUE_STORAGE_KEY\n await idb.set(key, queue)\n } catch (error) {\n console.error('[OfflineService] Error saving queue to IndexedDB:', error)\n }\n}\n\n/**\n * Add action to queue\n */\nexport async function addToQueue(action: SyncAction, storageKey?: string): Promise<SyncQueueItem> {\n const queue = await getQueueFromStorage(storageKey)\n\n const item: SyncQueueItem = {\n id: generateId(),\n action,\n createdAt: Date.now(),\n attempts: 0,\n maxAttempts: 3,\n status: 'PENDING',\n }\n\n queue.push(item)\n await saveQueueToStorage(queue, storageKey)\n\n return item\n}\n\n/**\n * Remove item from queue\n */\nexport async function removeFromQueue(id: string, storageKey?: string): Promise<boolean> {\n const queue = await getQueueFromStorage(storageKey)\n const index = queue.findIndex((item) => item.id === id)\n\n if (index === -1) {\n return false\n }\n\n queue.splice(index, 1)\n await saveQueueToStorage(queue, storageKey)\n\n return true\n}\n\n/**\n * Process one queue item\n */\nexport async function processQueueItem(item: SyncQueueItem, handler: SyncActionHandler): Promise<ProcessQueueResult> {\n try {\n const result = await handler(item.action)\n\n if (result.success) {\n return {\n success: true,\n item: { ...item, status: 'SYNCED' as const },\n }\n }\n\n // Unsuccessful result without exception\n const updatedItem: SyncQueueItem = {\n ...item,\n attempts: item.attempts + 1,\n status: item.attempts + 1 >= item.maxAttempts ? 'FAILED' : 'PENDING',\n error: result.error,\n }\n\n return {\n success: false,\n item: updatedItem,\n error: result.error,\n }\n } catch (error) {\n // Exception during execution\n const errorMessage = error instanceof Error ? error.message : 'Unknown error'\n const updatedItem: SyncQueueItem = {\n ...item,\n attempts: item.attempts + 1,\n status: item.attempts + 1 >= item.maxAttempts ? 'FAILED' : 'PENDING',\n error: errorMessage,\n }\n\n return {\n success: false,\n item: updatedItem,\n error: errorMessage,\n }\n }\n}\n\n/**\n * Clear queue\n */\nexport async function clearQueue(storageKey?: string): Promise<void> {\n try {\n const idb = await getIDB()\n if (!idb) {\n return\n }\n const key = storageKey ?? DEFAULT_SYNC_QUEUE_STORAGE_KEY\n await idb.del(key)\n } catch (error) {\n console.error('[OfflineService] Error clearing queue from IndexedDB:', error)\n }\n}\n\n/**\n * Create sync queue store\n */\nexport function createSyncQueueStore(storageKey?: string): SyncQueueStore {\n let queue: SyncQueueItem[] = []\n const listeners = new Set<() => void>()\n const key = storageKey ?? DEFAULT_SYNC_QUEUE_STORAGE_KEY\n\n const notifyListeners = () => {\n listeners.forEach((listener) => listener())\n }\n\n return {\n getQueue: () => queue,\n\n getQueueLength: () => queue.length,\n\n subscribe: (listener: () => void) => {\n listeners.add(listener)\n return () => {\n listeners.delete(listener)\n }\n },\n\n initialize: async () => {\n queue = await getQueueFromStorage(key)\n notifyListeners()\n },\n\n add: async (action: SyncAction) => {\n const item = await addToQueue(action, key)\n queue.push(item)\n notifyListeners()\n return item\n },\n\n remove: async (id: string) => {\n const result = await removeFromQueue(id, key)\n if (result) {\n queue = queue.filter((item) => item.id !== id)\n notifyListeners()\n }\n return result\n },\n\n processAll: async (handler: SyncActionHandler) => {\n const results: ProcessQueueResult[] = []\n\n for (const item of queue) {\n if (item.status === 'PENDING') {\n const result = await processQueueItem(item, handler)\n results.push(result)\n\n // Update queue after processing\n if (result.success && result.item) {\n await removeFromQueue(item.id, key)\n queue = queue.filter((q) => q.id !== item.id)\n } else if (result.item) {\n // Update item in queue\n const index = queue.findIndex((q) => q.id === item.id)\n if (index !== -1) {\n queue[index] = result.item\n }\n await saveQueueToStorage(queue, key)\n }\n }\n }\n\n notifyListeners()\n return results\n },\n }\n}\n","'use client'\n\nimport { useSyncExternalStore } from 'react'\n\nimport { getOfflineStatus, subscribeToStatusChanges } from './offline-service'\n\n// Global state for synchronization across tabs\nlet isOffline = false\n\nconst listeners = new Set<() => void>()\n\nconst notifyListeners = () => {\n listeners.forEach((listener) => listener())\n}\n\n// Initialize on first load\nif (typeof window !== 'undefined') {\n isOffline = getOfflineStatus()\n\n subscribeToStatusChanges((offline) => {\n isOffline = offline\n notifyListeners()\n })\n}\n\n/**\n * Hook for detecting offline status\n *\n * @returns true if the browser is offline\n *\n * @example\n * ```tsx\n * import { useOfflineStatus } from '@lena/form-components/offline'\n *\n * function MyComponent() {\n * const isOffline = useOfflineStatus()\n *\n * if (isOffline) {\n * return <OfflineBanner />\n * }\n *\n * return <OnlineContent />\n * }\n * ```\n */\nexport function useOfflineStatus(): boolean {\n return useSyncExternalStore(\n (callback) => {\n listeners.add(callback)\n return () => {\n listeners.delete(callback)\n }\n },\n () => isOffline,\n () => false, // SSR fallback — assume online\n )\n}\n","'use client'\n\nimport { useCallback, useEffect, useState, useSyncExternalStore } from 'react'\n\nimport { createSyncQueueStore } from './offline-service'\nimport type { ProcessQueueResult, SyncAction, SyncActionHandler, SyncQueueItem, UseSyncQueueResult } from './types'\nimport { useOfflineStatus } from './use-offline-status'\n\n// Global sync queue store (default)\nconst defaultSyncQueueStore = createSyncQueueStore()\nconst EMPTY_QUEUE: SyncQueueItem[] = []\n\n// Initialization flag\nlet initialized = false\n\nconst initialize = async () => {\n if (!initialized && typeof window !== 'undefined') {\n initialized = true\n await defaultSyncQueueStore.initialize()\n }\n}\n\n/**\n * Hook for working with the sync queue\n *\n * Allows adding actions to the queue in offline mode\n * and synchronizing them when connection is restored.\n *\n * @example\n * ```tsx\n * import { useSyncQueue } from '@lena/form-components/offline'\n *\n * function MyComponent() {\n * const { queue, queueLength, addAction, processQueue, isProcessing } = useSyncQueue()\n *\n * // Add action to queue (works offline too)\n * const handleBookLesson = async (slotId: string) => {\n * if (isOffline) {\n * await addAction({ type: 'BOOK_LESSON', payload: { slotId } })\n * toast({ title: 'Action added to sync queue' })\n * } else {\n * await api.bookLesson(slotId)\n * }\n * }\n *\n * // Process queue when connection is restored\n * useEffect(() => {\n * if (!isOffline && queueLength > 0) {\n * processQueue(async (action) => {\n * switch (action.type) {\n * case 'BOOK_LESSON':\n * return api.bookLesson(action.payload.slotId)\n * // ... other action types\n * }\n * })\n * }\n * }, [isOffline, queueLength, processQueue])\n * }\n * ```\n */\nexport function useSyncQueue(): UseSyncQueueResult {\n const [isLoading, setIsLoading] = useState(true)\n const [isProcessing, setIsProcessing] = useState(false)\n const isOffline = useOfflineStatus()\n\n // Subscribe to queue changes\n const queue = useSyncExternalStore(\n (callback) => defaultSyncQueueStore.subscribe(callback),\n () => defaultSyncQueueStore.getQueue(),\n () => EMPTY_QUEUE, // SSR fallback\n )\n\n // Initialize on mount\n useEffect(() => {\n initialize().then(() => {\n setIsLoading(false)\n })\n }, [])\n\n // Add action to queue\n const addAction = useCallback(async (action: SyncAction): Promise<SyncQueueItem> => {\n return defaultSyncQueueStore.add(action)\n }, [])\n\n // Remove action from queue\n const removeAction = useCallback(async (id: string): Promise<boolean> => {\n return defaultSyncQueueStore.remove(id)\n }, [])\n\n // Process entire queue\n const processQueue = useCallback(\n async (handler: SyncActionHandler): Promise<ProcessQueueResult[]> => {\n if (isOffline) {\n console.warn('[SyncQueue] Cannot process queue in offline mode')\n return []\n }\n\n setIsProcessing(true)\n try {\n return await defaultSyncQueueStore.processAll(handler)\n } finally {\n setIsProcessing(false)\n }\n },\n [isOffline],\n )\n\n return {\n queue,\n queueLength: queue.length,\n pendingCount: queue.filter((item) => item.status === 'PENDING').length,\n isLoading,\n isProcessing,\n addAction,\n removeAction,\n processQueue,\n }\n}\n","'use client'\n\nimport { useCallback, useEffect, useRef, useState } from 'react'\n\nimport type { OfflineSubmitResult, SyncAction, UseOfflineFormOptions, UseOfflineFormResult } from './types'\nimport { useOfflineStatus } from './use-offline-status'\nimport { useSyncQueue } from './use-sync-queue'\n\n/**\n * Hook for offline form support with TanStack Form\n *\n * Automatically detects connection status and:\n * - Online: sends data directly\n * - Offline: saves to IndexedDB queue for synchronization\n *\n * @example\n * ```tsx\n * import { useOfflineForm } from '@lena/form-components/offline'\n *\n * function ProfileForm({ initialData }) {\n * const { submit, isOffline, pendingCount, isProcessing } = useOfflineForm({\n * actionType: 'UPDATE_PROFILE',\n * onlineSubmit: async (value) => {\n * const result = await updateProfileAction(value)\n * return { success: result.success, error: result.error?.formErrors?.[0] }\n * },\n * onSuccess: () => toaster.success({ title: 'Saved' }),\n * onQueued: () => toaster.info({ title: 'Saved locally' }),\n * onError: (error) => toaster.error({ title: 'Error', description: error }),\n * })\n *\n * const form = useAppForm({\n * defaultValues: initialData,\n * onSubmit: async ({ value }) => {\n * await submit(value)\n * },\n * })\n *\n * return (\n * <form onSubmit={(e) => { e.preventDefault(); form.handleSubmit() }}>\n * {isOffline && <Badge colorPalette=\"orange\">Offline mode</Badge>}\n * {pendingCount > 0 && (\n * <Badge colorPalette=\"blue\">\n * {isProcessing ? 'Syncing...' : `Pending: ${pendingCount}`}\n * </Badge>\n * )}\n * <form.AppField name=\"name\" children={(field) => <field.TextField label=\"Name\" />} />\n * <Button type=\"submit\">{isOffline ? 'Save locally' : 'Save'}</Button>\n * </form>\n * )\n * }\n * ```\n */\nexport function useOfflineForm<T extends object>({\n actionType,\n onlineSubmit,\n onSuccess,\n onQueued,\n onError,\n}: UseOfflineFormOptions<T>): UseOfflineFormResult<T> {\n const isOffline = useOfflineStatus()\n const { addAction, processQueue, pendingCount, isProcessing, queueLength } = useSyncQueue()\n const [lastSyncAttempt, setLastSyncAttempt] = useState<number | null>(null)\n\n // Ref to prevent repeated queue processing\n const processingRef = useRef(false)\n\n /**\n * Form submission with offline support\n */\n const submit = useCallback(\n async (value: T): Promise<OfflineSubmitResult> => {\n // Offline — add to queue\n if (isOffline) {\n try {\n const queueItem = await addAction({\n type: actionType,\n payload: value as Record<string, unknown>,\n })\n\n onQueued?.()\n\n return {\n success: true,\n queued: true,\n queueItemId: queueItem.id,\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : 'Error saving to queue'\n onError?.(errorMessage)\n return {\n success: false,\n error: errorMessage,\n }\n }\n }\n\n // Online — send directly\n try {\n const result = await onlineSubmit(value)\n\n if (result.success) {\n onSuccess?.()\n } else if (result.error) {\n onError?.(result.error)\n }\n\n return {\n success: result.success,\n error: result.error,\n queued: false,\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : 'Submission error'\n onError?.(errorMessage)\n return {\n success: false,\n error: errorMessage,\n queued: false,\n }\n }\n },\n [isOffline, actionType, addAction, onlineSubmit, onSuccess, onQueued, onError],\n )\n\n /**\n * Handler for queued actions\n * Called when connection is restored\n */\n const handleQueuedAction = useCallback(\n async (action: SyncAction): Promise<{ success: boolean; error?: string }> => {\n // Process only our action type\n if (action.type !== actionType) {\n return { success: true } // Skip other action types\n }\n\n return onlineSubmit(action.payload as T)\n },\n [actionType, onlineSubmit],\n )\n\n // Automatic sync when connection is restored\n useEffect(() => {\n // If online and there are items in queue — try to sync\n if (!isOffline && pendingCount > 0 && !processingRef.current) {\n processingRef.current = true\n setLastSyncAttempt(Date.now())\n\n processQueue(handleQueuedAction)\n .then((results) => {\n const failed = results.filter((r) => !r.success)\n if (failed.length > 0) {\n console.warn(`[OfflineForm] ${failed.length} actions failed to sync`)\n }\n })\n .finally(() => {\n processingRef.current = false\n })\n }\n }, [isOffline, pendingCount, processQueue, handleQueuedAction])\n\n return {\n submit,\n isOffline,\n pendingCount,\n queueLength,\n isProcessing,\n lastSyncAttempt,\n }\n}\n","'use client'\n\nimport { Badge, type BadgeProps, HStack, Icon } from '@chakra-ui/react'\nimport { LuWifiOff } from 'react-icons/lu'\n\nimport type { OfflineIndicatorProps } from './types'\nimport { useOfflineStatus } from './use-offline-status'\n\n/**\n * Offline mode indicator\n *\n * Automatically shown when the browser is offline.\n * Hidden when connection is restored.\n *\n * @example\n * ```tsx\n * import { Form } from '@lena/form-components'\n *\n * <Form initialValue={data} onSubmit={handleSubmit}>\n * <Form.OfflineIndicator />\n * <Form.Field.String name=\"title\" />\n * <Form.Button.Submit />\n * </Form>\n * ```\n *\n * @example With custom settings\n * ```tsx\n * <Form.OfflineIndicator\n * label=\"No connection\"\n * colorPalette=\"red\"\n * variant=\"solid\"\n * />\n * ```\n */\nexport function FormOfflineIndicator({\n label = 'Offline mode',\n colorPalette = 'orange',\n variant = 'subtle',\n ...rest\n}: OfflineIndicatorProps & Omit<BadgeProps, 'children'>) {\n const isOffline = useOfflineStatus()\n\n if (!isOffline) {\n return null\n }\n\n return (\n <Badge colorPalette={colorPalette} variant={variant} data-testid=\"offline-indicator\" {...rest}>\n <HStack gap={1}>\n <Icon asChild boxSize={3}>\n <LuWifiOff />\n </Icon>\n <span>{label}</span>\n </HStack>\n </Badge>\n )\n}\n","'use client'\n\nimport { Badge, type BadgeProps, HStack, Icon, Spinner } from '@chakra-ui/react'\nimport { LuCheck, LuClock } from 'react-icons/lu'\n\nimport type { SyncStatusProps } from './types'\nimport { useOfflineStatus } from './use-offline-status'\nimport { useSyncQueue } from './use-sync-queue'\n\n/**\n * Sync queue status indicator\n *\n * Shows:\n * - Number of pending actions\n * - Spinner during synchronization\n * - \"Synced\" when queue is empty\n *\n * Works globally, does not require Form context.\n *\n * @example\n * ```tsx\n * import { FormSyncStatus } from '@lena/form-components/offline'\n *\n * // In layout or header\n * <FormSyncStatus />\n * ```\n *\n * @example With custom settings\n * ```tsx\n * <FormSyncStatus\n * showWhenEmpty={false}\n * syncingLabel=\"Syncing...\"\n * pendingLabel={(count) => `Pending: ${count}`}\n * syncedLabel=\"All synced\"\n * />\n * ```\n */\nexport function FormSyncStatus({\n showWhenEmpty = false,\n syncingLabel = 'Syncing...',\n pendingLabel = (count: number) => `Pending: ${count}`,\n syncedLabel = 'Synced',\n colorPalette = 'blue',\n ...rest\n}: SyncStatusProps & Omit<BadgeProps, 'children'>) {\n const isOffline = useOfflineStatus()\n const { pendingCount, isProcessing } = useSyncQueue()\n\n // Hide if online, queue empty and showWhenEmpty = false\n if (!isOffline && pendingCount === 0 && !isProcessing && !showWhenEmpty) {\n return null\n }\n\n // Determine what to display\n const renderContent = () => {\n // Synchronization in progress\n if (isProcessing) {\n return (\n <HStack gap={1}>\n <Spinner size=\"xs\" />\n <span>{syncingLabel}</span>\n </HStack>\n )\n }\n\n // There are pending items\n if (pendingCount > 0) {\n const label = typeof pendingLabel === 'function' ? pendingLabel(pendingCount) : pendingLabel\n return (\n <HStack gap={1}>\n <Icon asChild boxSize={3}>\n <LuClock />\n </Icon>\n <span>{label}</span>\n </HStack>\n )\n }\n\n // All synced\n return (\n <HStack gap={1}>\n <Icon asChild boxSize={3}>\n <LuCheck />\n </Icon>\n <span>{syncedLabel}</span>\n </HStack>\n )\n }\n\n // Color depends on state\n const effectiveColorPalette = pendingCount > 0 ? 'orange' : isProcessing ? colorPalette : 'green'\n\n return (\n <Badge\n colorPalette={effectiveColorPalette}\n variant=\"subtle\"\n data-testid=\"sync-status\"\n data-pending-count={pendingCount}\n data-processing={isProcessing}\n {...rest}\n >\n {renderContent()}\n </Badge>\n )\n}\n"]}
|
|
@@ -133,13 +133,14 @@ var STRING_FORMATS = [
|
|
|
133
133
|
];
|
|
134
134
|
var FormI18nContext = createContext(null);
|
|
135
135
|
function FormI18nProvider({ t, locale, children, setupZodErrorMap = false }) {
|
|
136
|
+
const resolvedT = t ?? ((key) => key);
|
|
136
137
|
useEffect(() => {
|
|
137
|
-
if (setupZodErrorMap) {
|
|
138
|
+
if (setupZodErrorMap && t) {
|
|
138
139
|
const errorMap = createFormErrorMap({ t });
|
|
139
140
|
z.config({ customError: errorMap });
|
|
140
141
|
}
|
|
141
142
|
}, [setupZodErrorMap, t]);
|
|
142
|
-
return /* @__PURE__ */ jsx(FormI18nContext.Provider, { value: { t, locale, enabled:
|
|
143
|
+
return /* @__PURE__ */ jsx(FormI18nContext.Provider, { value: { t: resolvedT, locale, enabled: !!t }, children });
|
|
143
144
|
}
|
|
144
145
|
function useFormI18n() {
|
|
145
146
|
return useContext(FormI18nContext);
|
|
@@ -187,5 +188,5 @@ function useLocalizedOptions(options) {
|
|
|
187
188
|
}
|
|
188
189
|
|
|
189
190
|
export { FormI18nProvider, SIZE_ORIGINS, STRING_FORMATS, ZOD_ERROR_CODES, createFormErrorMap, getLocalizedValue, useFormI18n, useLocalizedOptions };
|
|
190
|
-
//# sourceMappingURL=chunk-
|
|
191
|
-
//# sourceMappingURL=chunk-
|
|
191
|
+
//# sourceMappingURL=chunk-7FEQFDJ7.js.map
|
|
192
|
+
//# sourceMappingURL=chunk-7FEQFDJ7.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/lib/i18n/create-form-error-map.ts","../src/lib/i18n/form-i18n-provider.tsx","../src/lib/i18n/use-localized-options.ts"],"names":[],"mappings":"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","file":"chunk-7FEQFDJ7.js","sourcesContent":["import type { TranslateFunction, TranslateParams } from './form-i18n-provider'\n\n/**\n * Zod v4 issue type (упрощённый тип для работы с error map)\n * Полный тип: z.core.$ZodIssue (discriminated union)\n */\ninterface ZodIssue {\n readonly code: string\n readonly path: PropertyKey[]\n readonly message: string\n readonly input?: unknown\n readonly expected?: string\n readonly minimum?: number\n readonly maximum?: number\n readonly inclusive?: boolean\n readonly origin?: string\n readonly format?: string\n readonly options?: unknown[]\n readonly keys?: string[]\n readonly multipleOf?: number\n}\n\n/**\n * Конфигурация для создания error map\n */\nexport interface FormErrorMapConfig {\n /** Функция перевода (совместима с next-intl) */\n t: TranslateFunction\n /** Префикс для ключей валидации (по умолчанию 'validation') */\n prefix?: string\n}\n\n/**\n * Извлекает параметры из Zod issue для интерполяции в сообщение\n */\nfunction extractParams(issue: ZodIssue): TranslateParams {\n const params: TranslateParams = {}\n\n // Общие параметры\n if (issue.input !== undefined) {\n params.received = typeof issue.input === 'object' ? 'object' : String(issue.input)\n }\n\n // too_small / too_big\n if (issue.minimum !== undefined) {\n params.minimum = issue.minimum\n }\n if (issue.maximum !== undefined) {\n params.maximum = issue.maximum\n }\n if (issue.inclusive !== undefined) {\n params.inclusive = issue.inclusive\n }\n\n // invalid_type\n if (issue.expected !== undefined) {\n params.expected = issue.expected\n }\n\n // invalid_value (объединяет invalid_enum_value + invalid_literal в Zod v4)\n if (issue.options && Array.isArray(issue.options)) {\n params.options = issue.options.join(', ')\n }\n\n // unrecognized_keys\n if (issue.keys && Array.isArray(issue.keys)) {\n params.keys = issue.keys.join(', ')\n }\n\n // not_multiple_of\n if (issue.multipleOf !== undefined) {\n params.multipleOf = issue.multipleOf\n }\n\n // custom — сообщение из .refine()\n if (issue.message) {\n params.message = issue.message\n }\n\n return params\n}\n\n/**\n * Определяет origin (тип данных) для issue\n * Используется для построения ключа: validation.too_small.string\n */\nfunction getIssueOrigin(issue: ZodIssue): string | undefined {\n // Для too_small / too_big определяем origin по типу input или expected\n if (issue.code === 'too_small' || issue.code === 'too_big') {\n if (issue.origin) {\n return issue.origin\n }\n // Fallback по типу input\n const input = issue.input\n if (typeof input === 'string') {\n return 'string'\n }\n if (typeof input === 'number') {\n return 'number'\n }\n if (Array.isArray(input)) {\n return 'array'\n }\n if (input instanceof Date) {\n return 'date'\n }\n }\n\n // Для invalid_format (Zod v4, ранее invalid_string) определяем format\n if (issue.code === 'invalid_format') {\n if (issue.format) {\n return issue.format\n }\n }\n\n return undefined\n}\n\n/**\n * Пытается получить перевод по ключу\n * @returns переведённая строка или undefined если перевод не найден\n */\nfunction tryTranslate(t: TranslateFunction, key: string, params: TranslateParams): string | undefined {\n try {\n const result = t(key, params)\n // next-intl возвращает ключ при отсутствии перевода\n if (!result || result === key || result.startsWith(key)) {\n return undefined\n }\n return result\n } catch {\n return undefined\n }\n}\n\n/**\n * Создаёт Zod error map с поддержкой i18n\n *\n * Error map преобразует Zod ошибки в переведённые сообщения.\n * Ключи строятся по формату: `{prefix}.{code}.{origin?}`\n *\n * @example\n * ```tsx\n * import { createFormErrorMap } from '@lena/form-components'\n * import { z } from 'zod/v4'\n * import { useTranslations } from 'next-intl'\n *\n * // В провайдере приложения\n * const t = useTranslations('formSchemas')\n * z.config({ customError: createFormErrorMap({ t }) })\n *\n * // Или через FormI18nProvider\n * <FormI18nProvider t={t} locale={locale} setupZodErrorMap>\n * {children}\n * </FormI18nProvider>\n * ```\n *\n * @example Структура ключей в JSON\n * ```json\n * {\n * \"validation\": {\n * \"required\": \"Обязательное поле\",\n * \"too_small\": {\n * \"string\": \"Минимум {minimum} символов\",\n * \"number\": \"Минимум {minimum}\"\n * },\n * \"invalid_format\": {\n * \"email\": \"Некорректный email\"\n * }\n * }\n * }\n * ```\n */\nexport function createFormErrorMap(config: FormErrorMapConfig) {\n const { t, prefix = 'validation' } = config\n\n return (issue: ZodIssue): string | undefined => {\n const params = extractParams(issue)\n const origin = getIssueOrigin(issue)\n\n // Строим ключи для поиска перевода\n const baseKey = `${prefix}.${issue.code}`\n const originKey = origin ? `${baseKey}.${origin}` : null\n\n // Пробуем с origin, затем без\n let translated: string | undefined\n\n if (originKey) {\n translated = tryTranslate(t, originKey, params)\n }\n\n if (!translated) {\n translated = tryTranslate(t, baseKey, params)\n }\n\n // Возвращаем undefined чтобы Zod использовал дефолтное сообщение\n return translated\n }\n}\n\n/**\n * Список всех кодов ошибок Zod v4 для генерации ключей\n */\nexport const ZOD_ERROR_CODES = [\n 'invalid_type',\n 'too_small',\n 'too_big',\n 'invalid_format', // Zod v4: ранее invalid_string\n 'not_multiple_of',\n 'unrecognized_keys',\n 'invalid_value', // Zod v4: объединяет invalid_enum_value + invalid_literal\n 'invalid_union',\n 'invalid_key',\n 'invalid_element',\n 'custom',\n] as const\n\nexport type ZodErrorCode = (typeof ZOD_ERROR_CODES)[number]\n\n/**\n * Origins для ошибок too_small / too_big\n */\nexport const SIZE_ORIGINS = ['string', 'number', 'array', 'date', 'set', 'file'] as const\n\n/**\n * Formats для ошибок invalid_format (Zod v4, ранее invalid_string)\n */\nexport const STRING_FORMATS = [\n 'email',\n 'url',\n 'uuid',\n 'cuid',\n 'cuid2',\n 'ulid',\n 'regex',\n 'datetime',\n 'date',\n 'time',\n 'duration',\n 'ip',\n 'cidr',\n 'base64',\n 'base64url',\n 'json_string',\n 'e164',\n 'jwt',\n 'emoji',\n 'nanoid',\n 'guid',\n 'lowercase',\n 'uppercase',\n] as const\n","'use client'\n\nimport { createContext, type ReactNode, useContext, useEffect } from 'react'\nimport { z } from 'zod/v4'\nimport { createFormErrorMap } from './create-form-error-map'\n\n/**\n * Параметры для интерполяции в сообщениях об ошибках\n * @example { minimum: 5, maximum: 100 }\n */\nexport type TranslateParams = Record<string, string | number | boolean | undefined>\n\n/**\n * Функция перевода (совместима с next-intl useTranslations)\n * @param key - Ключ перевода в формате \"ModelName.fieldName.property\"\n * @param params - Опциональные параметры для интерполяции\n * @returns Переведённая строка или fallback\n */\nexport type TranslateFunction = (key: string, params?: TranslateParams) => string\n\n/**\n * Контекст для i18n в формах\n */\ninterface FormI18nContextValue {\n /** Функция перевода */\n t: TranslateFunction\n /** Текущая локаль */\n locale: string\n /** Включен ли i18n */\n enabled: boolean\n}\n\nconst FormI18nContext = createContext<FormI18nContextValue | null>(null)\n\n/**\n * Props для FormI18nProvider\n */\ninterface FormI18nProviderProps {\n /**\n * Функция перевода из next-intl или другой i18n библиотеки.\n * Если не задана, переводы полей (title, placeholder) не применяются,\n * но locale используется для constraint hints.\n * @example useTranslations('formSchemas')\n */\n t?: TranslateFunction\n /** Текущая локаль (default: 'en') */\n locale: string\n children: ReactNode\n /**\n * Автоматически настроить глобальный Zod error map для i18n\n *\n * При включении, Zod ошибки будут переводиться через функцию t()\n * с ключами вида `validation.{code}.{origin?}`\n *\n * @example\n * ```json\n * {\n * \"validation\": {\n * \"too_small\": { \"string\": \"Минимум {minimum} символов\" },\n * \"invalid_string\": { \"email\": \"Некорректный email\" }\n * }\n * }\n * ```\n *\n * @default false\n */\n setupZodErrorMap?: boolean\n}\n\n/**\n * Провайдер i18n для форм\n *\n * Оборачивает формы и предоставляет доступ к переводам через useFormI18n.\n * При наличии провайдера, компоненты форм будут использовать переводы\n * по ключам из i18nKey в схемах.\n *\n * @example Базовое использование\n * ```tsx\n * import { FormI18nProvider } from '@lena/form-components'\n * import { useTranslations, useLocale } from 'next-intl'\n *\n * function App({ children }) {\n * const t = useTranslations('formSchemas')\n * const locale = useLocale()\n *\n * return (\n * <FormI18nProvider t={t} locale={locale}>\n * {children}\n * </FormI18nProvider>\n * )\n * }\n * ```\n *\n * @example С автоматической настройкой Zod error map\n * ```tsx\n * <FormI18nProvider t={t} locale={locale} setupZodErrorMap>\n * {children}\n * </FormI18nProvider>\n * ```\n */\nexport function FormI18nProvider({ t, locale, children, setupZodErrorMap = false }: FormI18nProviderProps) {\n // Fallback t-функция: возвращает ключ как есть (без перевода)\n const resolvedT: TranslateFunction = t ?? ((key: string) => key)\n\n // Настраиваем глобальный Zod error map при включённом флаге\n useEffect(() => {\n if (setupZodErrorMap && t) {\n const errorMap = createFormErrorMap({ t })\n // Type assertion: наш error map совместим с Zod v4 API,\n // но TypeScript не может вывести это автоматически\n z.config({ customError: errorMap as z.core.$ZodErrorMap<z.core.$ZodIssue> })\n }\n }, [setupZodErrorMap, t])\n\n return <FormI18nContext.Provider value={{ t: resolvedT, locale, enabled: !!t }}>{children}</FormI18nContext.Provider>\n}\n\n/**\n * Хук для доступа к i18n в формах\n *\n * Возвращает функцию перевода и информацию о локали.\n * Если FormI18nProvider не найден, возвращает null.\n *\n * @example\n * ```tsx\n * function MyField() {\n * const i18n = useFormI18n()\n *\n * if (i18n) {\n * const label = i18n.t('Product.name.title')\n * }\n * }\n * ```\n */\nexport function useFormI18n(): FormI18nContextValue | null {\n return useContext(FormI18nContext)\n}\n\n/**\n * Получить переведённое значение с fallback\n *\n * Пытается получить перевод по ключу. Если перевод пустой или\n * i18n не настроен, возвращает fallback значение.\n *\n * @param i18n - Контекст i18n (может быть null)\n * @param i18nKey - Базовый ключ (например, \"Product.name\")\n * @param property - Свойство (например, \"title\", \"placeholder\")\n * @param fallback - Значение по умолчанию\n * @returns Переведённая строка или fallback\n */\nexport function getLocalizedValue(\n i18n: FormI18nContextValue | null,\n i18nKey: string | undefined,\n property: 'title' | 'placeholder' | 'description' | 'label',\n fallback: string | undefined,\n): string | undefined {\n if (!i18n || !i18nKey) {\n return fallback\n }\n\n try {\n const fullKey = `${i18nKey}.${property}`\n const translated = i18n.t(fullKey)\n\n // Если перевод пустой или равен ключу (next-intl возвращает ключ при отсутствии перевода)\n if (!translated || translated === fullKey) {\n return fallback\n }\n\n return translated\n } catch {\n // При ошибке возвращаем fallback\n return fallback\n }\n}\n","'use client'\n\nimport { useMemo } from 'react'\nimport { useFormI18n } from './form-i18n-provider'\n\n/**\n * Опция для select/radio/checkbox с поддержкой i18n\n */\nexport interface LocalizableOption {\n value: string | number\n label: string\n /** Опция отключена */\n disabled?: boolean\n /** Ключ i18n для перевода label (например, \"RecipeType.SWEET\") */\n i18nKey?: string\n}\n\n/**\n * Хук для локализации опций select/radio/checkbox\n *\n * Принимает массив опций с i18nKey и возвращает массив с переведёнными label.\n * Если i18n не настроен или перевод отсутствует, используется оригинальный label.\n *\n * @param options - Массив опций с возможными i18nKey\n * @returns Массив опций с переведёнными label\n *\n * @example\n * ```tsx\n * // Опции из Zod схемы\n * const rawOptions = [\n * { value: 'SWEET', label: 'Сладкое', i18nKey: 'RecipeType.SWEET' },\n * { value: 'SALTY', label: 'Солёное', i18nKey: 'RecipeType.SALTY' },\n * ]\n *\n * function MySelect() {\n * const options = useLocalizedOptions(rawOptions)\n * // options[0].label будет переведён если есть FormI18nProvider\n * }\n * ```\n */\nexport function useLocalizedOptions(\n options: LocalizableOption[] | undefined\n): { value: string | number; label: string; disabled?: boolean }[] {\n const i18n = useFormI18n()\n\n return useMemo(() => {\n if (!options) {\n return []\n }\n\n if (!i18n) {\n // i18n не настроен — возвращаем оригинальные label\n return options.map(({ value, label, disabled }) => ({ value, label, disabled }))\n }\n\n return options.map(({ value, label, disabled, i18nKey }) => {\n if (!i18nKey) {\n return { value, label, disabled }\n }\n\n try {\n // Ключ для enum option: \"EnumName.VALUE.label\"\n const fullKey = `${i18nKey}.label`\n const translated = i18n.t(fullKey)\n\n // Если перевод пустой или равен ключу — используем fallback\n if (!translated || translated === fullKey) {\n return { value, label, disabled }\n }\n\n return { value, label: translated, disabled }\n } catch {\n return { value, label, disabled }\n }\n })\n }, [options, i18n])\n}\n"]}
|
package/i18n.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { FormI18nProvider, SIZE_ORIGINS, STRING_FORMATS, ZOD_ERROR_CODES, createFormErrorMap, getLocalizedValue, useFormI18n, useLocalizedOptions } from './chunk-
|
|
1
|
+
export { FormI18nProvider, SIZE_ORIGINS, STRING_FORMATS, ZOD_ERROR_CODES, createFormErrorMap, getLocalizedValue, useFormI18n, useLocalizedOptions } from './chunk-7FEQFDJ7.js';
|
|
2
2
|
//# sourceMappingURL=i18n.js.map
|
|
3
3
|
//# sourceMappingURL=i18n.js.map
|