@letar/forms 1.0.0 → 1.1.0

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 CHANGED
@@ -4,6 +4,63 @@
4
4
 
5
5
  Формат основан на [Keep a Changelog](https://keepachangelog.com/ru/1.0.0/).
6
6
 
7
+ ## [1.1.0] - 2026-04-01
8
+
9
+ ### Added
10
+
11
+ - **size-limit** CI: bundle size проверка перед каждым npm publish (20 KB brotli full)
12
+ - **Категорийные entry points**: `@letar/forms/fields/{text,number,datetime,selection,boolean,specialized}`
13
+ - **Бенчмарк ре-рендеров**: 10 полей, ввод в одно → 0 лишних рендеров у остальных
14
+ - **FieldErrorBoundary**: ErrorBoundary для каждого field-компонента (fallback при ошибке рендеринга)
15
+ - **Type-тесты**: DeepKeys, DeepValue, useTypedFormSubscribe (vitest expectTypeOf)
16
+ - `loadingText` prop в `Form.Button.Submit` для кастомного текста при загрузке
17
+ - `City` и `sortable` в FormFieldComponents/FormGroupListComponent типах
18
+
19
+ ### Fixed
20
+
21
+ - Race condition в Form.Steps — все шаги получали index=0
22
+ - Число полей "49" → "40" во всех 12 статьях и README
23
+
24
+ ### Changed
25
+
26
+ - tsup entry points расширены с 3 до 9 (code splitting для categories)
27
+ - Bundle Size секция в README с актуальными метриками
28
+ - `package.publish.json` exports map с 6 category entry points
29
+
30
+ ## [0.58.0] - 2026-03-31
31
+
32
+ ### Added
33
+
34
+ - Pluggable `AddressProvider` interface for `Form.Field.Address` and `Form.Field.City`
35
+ - `createDaDataProvider()` — built-in DaData provider (Russia)
36
+ - `createForm({ addressProvider })` — set address provider once for all fields
37
+ - Provider resolution: field prop → createForm context → token fallback → env
38
+ - `addressProvider` prop on `Form` root component
39
+ - `CityFieldProps` exported from types
40
+ - `README.en.md`: Address Provider + createForm sections
41
+
42
+ ### Changed
43
+
44
+ - All JSDoc, comments, runtime errors translated to English (118 files, ~3000 lines)
45
+ - Default UI strings: "Save", "Reset", "Unsaved changes", "Leave", "Stay", etc.
46
+ - `AddressValue.data` generalized to `Record<string, unknown>` (was DaData-specific)
47
+ - `AddressFieldProps.token` is now optional (use `provider` instead)
48
+ - `DaDataSuggestion` marked as deprecated
49
+ - `build:npm` copies `README.en.md` as `README.md` + `README.ru.md` for npm
50
+
51
+ ## [0.56.0] - 2026-03-23
52
+
53
+ ### Added
54
+
55
+ - `Form.DebugValues` — интерактивный JSON-инспектор значений формы (скрыт в production)
56
+ - `debug` prop на `Form` для автоматического отображения DebugValues
57
+ - Инфраструктура публикации `@letar/forms` на npm
58
+
59
+ ### Fixed
60
+
61
+ - Совместимость с `@tanstack/store` 0.9+ (Subscription API)
62
+ - Исправлен баг `destroy` в `form-steps`
63
+
7
64
  ## [0.54.1] - 2026-01-05
8
65
 
9
66
  ### Fixed
package/README.md CHANGED
@@ -5,6 +5,8 @@ Declarative form components for React with **40+ field types**, powered by [TanS
5
5
  [![npm version](https://img.shields.io/npm/v/@letar/forms)](https://www.npmjs.com/package/@letar/forms)
6
6
  [![license](https://img.shields.io/npm/l/@letar/forms)](./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.string().min(2).meta({ ui: { title: 'Title', placeholder: 'Enter...' } }),
20
- rating: z.number().min(0).max(10).meta({ ui: { title: 'Rating' } }),
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
package/README.ru.md ADDED
@@ -0,0 +1,304 @@
1
+ # @lena/form-components
2
+
3
+ Переиспользуемая UI-библиотека компонентов форм на базе TanStack Form для монорепозитория Lena.
4
+
5
+ [English documentation](./README.en.md)
6
+
7
+ ## Quick Start
8
+
9
+ ```tsx
10
+ import { Form } from '@lena/form-components'
11
+ import { z } from 'zod/v4'
12
+
13
+ const Schema = z.object({
14
+ title: z.string().min(2).meta({ ui: { title: 'Название', placeholder: 'Введите...' } }),
15
+ rating: z.number().min(0).max(10).meta({ ui: { title: 'Рейтинг' } }),
16
+ })
17
+
18
+ <Form schema={Schema} initialValue={{ title: '', rating: 5 }} onSubmit={save}>
19
+ <Form.Field.String name="title" />
20
+ <Form.Field.Number name="rating" />
21
+ <Form.Button.Submit>Сохранить</Form.Button.Submit>
22
+ </Form>
23
+ ```
24
+
25
+ **Или полная автогенерация:**
26
+
27
+ ```tsx
28
+ <Form.FromSchema schema={Schema} initialValue={data} onSubmit={handleSubmit} submitLabel="Создать" />
29
+ ```
30
+
31
+ ---
32
+
33
+ ## Философия: Отделение вёрстки от логики
34
+
35
+ | Аспект | Где определяется | Как используется в JSX |
36
+ | ----------------- | -------------------------- | ------------------------------- |
37
+ | **Валидация** | Zod схема | `schema={Schema}` |
38
+ | **UI метаданные** | Zod `.meta({ ui: {...} })` | Автоматически из схемы |
39
+ | **Структура** | TypeScript типы | `initialValue={data}` |
40
+ | **Вёрстка** | JSX | `<HStack>`, `<VStack>`, `<Box>` |
41
+
42
+ **Результат:** JSX содержит только вёрстку и имена полей. Вся логика живёт в схеме.
43
+
44
+ ---
45
+
46
+ ## Документация
47
+
48
+ | Категория | Документация | Описание |
49
+ | ---------------- | -------------------------------------------------------- | -------------------------------------------- |
50
+ | Field компоненты | [docs/fields.md](./docs/fields.md) | 40 типов полей (String, Number, Select, ...) |
51
+ | Form-level | [docs/form-level.md](./docs/form-level.md) | Steps, When, Errors, Middleware, Persistence |
52
+ | Schema генерация | [docs/schema-generation.md](./docs/schema-generation.md) | FromSchema, AutoFields, Builder |
53
+ | Offline | [docs/offline.md](./docs/offline.md) | Оффлайн режим, очередь синхронизации |
54
+ | ZenStack | [docs/zenstack.md](./docs/zenstack.md) | Плагин, @form.\* директивы, withUIMeta |
55
+ | i18n | [docs/i18n.md](./docs/i18n.md) | Мультиязычность, перевод ошибок валидации |
56
+ | API Reference | [docs/api-reference.md](./docs/api-reference.md) | Хуки, контексты, типы |
57
+
58
+ ---
59
+
60
+ ## Основные возможности
61
+
62
+ ### 40+ Field компонентов
63
+
64
+ ```tsx
65
+ // Текстовые
66
+ <Form.Field.String name="title" />
67
+ <Form.Field.Textarea name="description" />
68
+ <Form.Field.RichText name="content" />
69
+
70
+ // Числовые
71
+ <Form.Field.Number name="price" />
72
+ <Form.Field.Slider name="rating" />
73
+ <Form.Field.Currency name="amount" />
74
+
75
+ // Выбор
76
+ <Form.Field.Select name="category" />
77
+ <Form.Field.RadioGroup name="type" />
78
+ <Form.Field.Checkbox name="agree" />
79
+
80
+ // Специальные
81
+ <Form.Field.Date name="birthday" />
82
+ <Form.Field.Phone name="phone" />
83
+ <Form.Field.FileUpload name="avatar" />
84
+ ```
85
+
86
+ [Полный список → docs/fields.md](./docs/fields.md)
87
+
88
+ ### Form-level компоненты
89
+
90
+ ```tsx
91
+ <Form schema={Schema} initialValue={data} onSubmit={save}>
92
+ {/* Условный рендеринг */}
93
+ <Form.When field="type" is="company">
94
+ <Form.Field.String name="companyName" />
95
+ </Form.When>
96
+
97
+ {/* Мультистеп формы */}
98
+ <Form.Steps animated validateOnNext>
99
+ <Form.Steps.Step title="Шаг 1">...</Form.Steps.Step>
100
+ <Form.Steps.Step title="Шаг 2">...</Form.Steps.Step>
101
+ <Form.Steps.Navigation />
102
+ </Form.Steps>
103
+
104
+ {/* Сводка ошибок */}
105
+ <Form.Errors title="Исправьте ошибки:" />
106
+
107
+ {/* JSON-инспектор значений (скрыт в production) */}
108
+ <Form.DebugValues />
109
+
110
+ <Form.Button.Submit />
111
+ </Form>
112
+ ```
113
+
114
+ [Подробнее → docs/form-level.md](./docs/form-level.md)
115
+
116
+ ### Группы и массивы
117
+
118
+ ```tsx
119
+ // Вложенный объект
120
+ <Form.Group name="address">
121
+ <Form.Field.String name="city" /> {/* → address.city */}
122
+ <Form.Field.String name="street" /> {/* → address.street */}
123
+ </Form.Group>
124
+
125
+ // Массив
126
+ <Form.Group.List name="phones">
127
+ <Form.Field.Phone />
128
+ <Form.Group.List.Button.Add>Добавить телефон</Form.Group.List.Button.Add>
129
+ </Form.Group.List>
130
+ ```
131
+
132
+ ### Автоматические constraints из Zod
133
+
134
+ ```tsx
135
+ const Schema = z.object({
136
+ title: z.string().min(2).max(100), // → minLength={2} maxLength={100}
137
+ email: z.string().email(), // → type="email"
138
+ rating: z.number().min(1).max(10), // → min={1} max={10}
139
+ })
140
+
141
+ // DRY: валидация и UI constraints в одном месте
142
+ <Form.Field.String name="title" /> {/* maxLength={100} из схемы */}
143
+ ```
144
+
145
+ ### ZenStack интеграция
146
+
147
+ ```zmodel
148
+ model Product {
149
+ /// @form.title("Название продукта")
150
+ /// @form.placeholder("Введите название")
151
+ title String
152
+
153
+ /// @form.title("Цена")
154
+ /// @form.fieldType("currency")
155
+ /// @form.props({ min: 0, currency: "RUB" })
156
+ price Int
157
+ }
158
+ ```
159
+
160
+ ```tsx
161
+ import { ProductCreateFormSchema } from '@/generated/form-schemas'
162
+ ;<Form.FromSchema schema={ProductCreateFormSchema} initialValue={data} onSubmit={save} />
163
+ ```
164
+
165
+ [Подробнее → docs/zenstack.md](./docs/zenstack.md)
166
+
167
+ ### Offline Support
168
+
169
+ ```tsx
170
+ <Form
171
+ initialValue={data}
172
+ offline={{
173
+ actionType: 'UPDATE_PROFILE',
174
+ onQueued: () => toast.info('Сохранено локально'),
175
+ onSynced: () => toast.success('Синхронизировано'),
176
+ }}
177
+ onSubmit={handleSubmit}
178
+ >
179
+ <Form.OfflineIndicator />
180
+ <Form.Field.String name="name" />
181
+ <Form.Button.Submit />
182
+ </Form>
183
+ ```
184
+
185
+ [Подробнее → docs/offline.md](./docs/offline.md)
186
+
187
+ ---
188
+
189
+ ## Установка
190
+
191
+ ```bash
192
+ # Уже установлен в монорепозитории
193
+ import { Form } from '@lena/form-components'
194
+ ```
195
+
196
+ ### Опциональные зависимости (npm)
197
+
198
+ | Пакет | Для чего |
199
+ | ---------------------- | --------------------------------- |
200
+ | `@dnd-kit/*` | Drag & drop сортировка в массивах |
201
+ | `use-mask-input` | Phone, MaskedInput |
202
+ | `@tiptap/*` | RichText редактор |
203
+ | `@uiw/react-json-view` | Form.DebugValues (JSON инспектор) |
204
+ | `next-intl` | i18n интеграция |
205
+
206
+ ---
207
+
208
+ ## Команды
209
+
210
+ ```bash
211
+ nx build @lena/form-components # Сборка
212
+ nx lint @lena/form-components # Линтинг
213
+ nx test @lena/form-components # Тесты
214
+ ```
215
+
216
+ ---
217
+
218
+ ## Провайдер адресов
219
+
220
+ Поля Address и City поддерживают подключаемые провайдеры геокодинга. DaData (Россия) встроен:
221
+
222
+ ```typescript
223
+ import { createForm, createDaDataProvider } from '@lena/form-components'
224
+
225
+ // Вариант 1: через createForm (рекомендуемый)
226
+ const AppForm = createForm({
227
+ addressProvider: createDaDataProvider({ token: process.env.DADATA_TOKEN }),
228
+ })
229
+
230
+ <AppForm.Field.Address name="address" />
231
+ <AppForm.Field.City name="city" />
232
+
233
+ // Вариант 2: на конкретном поле
234
+ <Form.Field.Address name="address" provider={myProvider} />
235
+
236
+ // Вариант 3: обратная совместимость через token
237
+ <Form.Field.Address name="address" token="dadata-token" />
238
+ ```
239
+
240
+ Для других сервисов — реализуйте интерфейс `AddressProvider`:
241
+
242
+ ```typescript
243
+ const myProvider: AddressProvider = {
244
+ async getSuggestions(query, options) {
245
+ const res = await fetch(`/api/geocode?q=${query}`)
246
+ return res.json() // [{ label, value, data }]
247
+ },
248
+ }
249
+ ```
250
+
251
+ ---
252
+
253
+ ## AI Tooling (MCP)
254
+
255
+ MCP сервер [`@letar/form-mcp`](../form-mcp/README.md) предоставляет AI-ассистентам (Claude Code, Cursor, VS Code Copilot) полный контекст о библиотеке: 40+ полей, паттерны форм, @form.\* директивы.
256
+
257
+ ```json
258
+ { "form-mcp": { "command": "npx", "args": ["-y", "@letar/form-mcp"] } }
259
+ ```
260
+
261
+ ## Bundle Size
262
+
263
+ Библиотека поставляется как ESM с external dependencies. Все тяжёлые зависимости (Chakra, React, Tiptap, dnd-kit) — external и не включаются в bundle.
264
+
265
+ | Модуль | Размер (brotli) | Размер (raw) |
266
+ |--------|----------------|--------------|
267
+ | `@letar/forms` (все 40 полей) | **20 KB** | 109 KB |
268
+ | `@letar/forms/fields/text` | < 1 KB | re-export |
269
+ | `@letar/forms/fields/number` | < 1 KB | re-export |
270
+ | `@letar/forms/fields/datetime` | < 1 KB | re-export |
271
+ | `@letar/forms/fields/selection` | < 1 KB | re-export |
272
+ | `@letar/forms/fields/boolean` | < 1 KB | re-export |
273
+ | `@letar/forms/fields/specialized` | < 1 KB | re-export |
274
+ | `@letar/forms/offline` | < 1 KB | 5 KB |
275
+ | `@letar/forms/i18n` | < 1 KB | 13 KB |
276
+
277
+ Категорийные entry points (`fields/*`) позволяют импортировать только нужные поля:
278
+
279
+ ```typescript
280
+ // Полный импорт — все 40 полей
281
+ import { Form } from '@letar/forms'
282
+
283
+ // Категорийный импорт — только текстовые поля
284
+ import { FieldString, FieldTextarea } from '@letar/forms/fields/text'
285
+ ```
286
+
287
+ Метрики проверяются в CI через [size-limit](https://github.com/ai/size-limit).
288
+
289
+ ### Ре-рендеры
290
+
291
+ При вводе текста в одно поле формы из 10 полей — **остальные 9 полей НЕ ре-рендерятся** (0 лишних рендеров). TanStack Form обеспечивает field-level подписки — каждое поле изолировано.
292
+
293
+ ## Связанные документы
294
+
295
+ - [/.claude/docs/forms.md](../../.claude/docs/forms.md) — документация по формам
296
+ - [/.claude/docs/pwa-offline.md](../../.claude/docs/pwa-offline.md) — оффлайн-формы
297
+ - [/libs/form-mcp](../form-mcp/) — MCP сервер для AI-ассистентов
298
+ - [PLAN.md](./PLAN.md) — план развития библиотеки
299
+ - [TESTING_PLAN.md](./TESTING_PLAN.md) — план тестирования
300
+
301
+ ---
302
+
303
+ **Версия:** 0.59.0
304
+ **Последнее обновление:** 2026-04-01
@@ -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] \u041E\u0448\u0438\u0431\u043A\u0430 \u0437\u0430\u0433\u0440\u0443\u0437\u043A\u0438 \u043E\u0447\u0435\u0440\u0435\u0434\u0438 \u0438\u0437 IndexedDB:", error);
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] \u041E\u0448\u0438\u0431\u043A\u0430 \u0441\u043E\u0445\u0440\u0430\u043D\u0435\u043D\u0438\u044F \u043E\u0447\u0435\u0440\u0435\u0434\u0438 \u0432 IndexedDB:", error);
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] \u041E\u0448\u0438\u0431\u043A\u0430 \u043E\u0447\u0438\u0441\u0442\u043A\u0438 \u043E\u0447\u0435\u0440\u0435\u0434\u0438 \u0438\u0437 IndexedDB:", error);
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] \u041D\u0435\u0432\u043E\u0437\u043C\u043E\u0436\u043D\u043E \u043E\u0431\u0440\u0430\u0431\u043E\u0442\u0430\u0442\u044C \u043E\u0447\u0435\u0440\u0435\u0434\u044C \u0432 \u043E\u0444\u0444\u043B\u0430\u0439\u043D \u0440\u0435\u0436\u0438\u043C\u0435");
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 : "\u041E\u0448\u0438\u0431\u043A\u0430 \u0441\u043E\u0445\u0440\u0430\u043D\u0435\u043D\u0438\u044F \u0432 \u043E\u0447\u0435\u0440\u0435\u0434\u044C";
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 : "\u041E\u0448\u0438\u0431\u043A\u0430 \u043E\u0442\u043F\u0440\u0430\u0432\u043A\u0438";
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} \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0439 \u043D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0441\u0438\u043D\u0445\u0440\u043E\u043D\u0438\u0437\u0438\u0440\u043E\u0432\u0430\u0442\u044C`);
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 = "\u041E\u0444\u0444\u043B\u0430\u0439\u043D \u0440\u0435\u0436\u0438\u043C",
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 = "\u0421\u0438\u043D\u0445\u0440\u043E\u043D\u0438\u0437\u0430\u0446\u0438\u044F...",
389
- pendingLabel = (count) => `\u041E\u0436\u0438\u0434\u0430\u0435\u0442: ${count}`,
390
- syncedLabel = "\u0421\u0438\u043D\u0445\u0440\u043E\u043D\u0438\u0437\u0438\u0440\u043E\u0432\u0430\u043D\u043E",
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-G3HYXHCZ.js.map
435
- //# sourceMappingURL=chunk-G3HYXHCZ.js.map
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":"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","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"]}