@valfuse-node/react 0.2.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.
@@ -0,0 +1,446 @@
1
+ import React$1, { ChangeEvent, PropsWithChildren } from 'react';
2
+ import { ValfuseSchema, ValfuseFieldErrors } from '@valfuse-node/form';
3
+ import * as react_jsx_runtime from 'react/jsx-runtime';
4
+ import * as _valfuse_node_localization from '@valfuse-node/localization';
5
+ import { RuntimeManifest } from '@valfuse-node/localization';
6
+
7
+ /**
8
+ * Controls when validation is triggered — mirrors react-hook-form's `mode` option:
9
+ *
10
+ * | Mode | Behaviour |
11
+ * |--------------|-------------------------------------------------------------------------|
12
+ * | `onSubmit` | Validate only when the form is submitted (default) |
13
+ * | `onBlur` | Validate when a field loses focus |
14
+ * | `onChange` | Validate on every keystroke / value change |
15
+ * | `onTouched` | Validate on the first blur; after that validate on every change |
16
+ * | `all` | Validate on both `onChange` and `onBlur` |
17
+ */
18
+ type ValfuseFormMode = "onSubmit" | "onBlur" | "onChange" | "onTouched" | "all";
19
+ /** A single field error — extends the core error with a required `type` field */
20
+ type ValfuseFieldError = {
21
+ message: string;
22
+ /** Error originator: "validation" | "server" | "manual" | "custom" */
23
+ type: string;
24
+ /** Optional semantic code (e.g. "email.required", "auth.not_found") */
25
+ code?: string;
26
+ metadata?: Record<string, unknown>;
27
+ };
28
+ type ValfuseFormErrors<TFieldValues extends Record<string, unknown>> = {
29
+ [K in keyof TFieldValues]?: ValfuseFieldError;
30
+ };
31
+ type ValfuseDirtyFields<TFieldValues extends Record<string, unknown>> = {
32
+ [K in keyof TFieldValues]?: boolean;
33
+ };
34
+ type ValfuseTouchedFields<TFieldValues extends Record<string, unknown>> = {
35
+ [K in keyof TFieldValues]?: boolean;
36
+ };
37
+ type ValfuseFormState<TFieldValues extends Record<string, unknown>> = {
38
+ /** All current field validation errors */
39
+ readonly errors: ValfuseFormErrors<TFieldValues>;
40
+ /** `true` while an async submit handler is running — never `undefined` */
41
+ readonly isSubmitting: boolean;
42
+ /** `true` after the form has been submitted at least once — never `undefined` */
43
+ readonly isSubmitted: boolean;
44
+ /**
45
+ * `true` if the most recent submission completed without validation errors
46
+ * and the `onValid` handler finished successfully — never `undefined`.
47
+ */
48
+ readonly isSubmitSuccessful: boolean;
49
+ /** Total number of submit attempts (resets on `reset()`) — never `undefined` */
50
+ readonly submitCount: number;
51
+ /** `true` if any field value differs from its default value — never `undefined` */
52
+ readonly isDirty: boolean;
53
+ /**
54
+ * `true` if current values pass schema validation and there are no active errors.
55
+ * Computed directly from the schema — accurate from the very first render.
56
+ */
57
+ readonly isValid: boolean;
58
+ /** Map of fields whose current value differs from the default (`{ email: true }`) */
59
+ readonly dirtyFields: ValfuseDirtyFields<TFieldValues>;
60
+ /** Map of fields the user has interacted with (focused + blurred) */
61
+ readonly touchedFields: ValfuseTouchedFields<TFieldValues>;
62
+ /** The `defaultValues` that were passed to `useValfuseForm` */
63
+ readonly defaultValues: Readonly<TFieldValues>;
64
+ };
65
+ /**
66
+ * Callback passed to `form.watch(callback)`.
67
+ * Called every time any field value changes.
68
+ */
69
+ type ValfuseWatchCallback<TFieldValues extends Record<string, unknown>> = (values: TFieldValues, info: {
70
+ name?: string;
71
+ type?: string;
72
+ }) => void;
73
+ /**
74
+ * Callable type for `form.watch` — mirrors react-hook-form overloads:
75
+ *
76
+ * ```ts
77
+ * form.watch() // → TFieldValues (all values)
78
+ * form.watch("email") // → TFieldValues["email"]
79
+ * form.watch(["email", "name"]) // → Array of values in the same order
80
+ * form.watch((values, info) => void) // → () => void (unsubscribe)
81
+ * ```
82
+ */
83
+ interface ValfuseWatchFunction<TFieldValues extends Record<string, unknown>> {
84
+ (): TFieldValues;
85
+ <TName extends keyof TFieldValues & string>(name: TName): TFieldValues[TName];
86
+ (names: Array<keyof TFieldValues & string>): Array<TFieldValues[keyof TFieldValues]>;
87
+ (callback: ValfuseWatchCallback<TFieldValues>): () => void;
88
+ }
89
+ /** Props returned by `form.register(name)` — spread directly onto `<input>` */
90
+ type ValfuseRegisterReturn = {
91
+ name: string;
92
+ /** Current field value — compatible with HTML input `value` prop */
93
+ value: string | number | readonly string[] | undefined;
94
+ onChange: (e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => void;
95
+ onBlur: () => void;
96
+ };
97
+ /**
98
+ * Opaque control object — passed to `<ValfuseController control={...} />`.
99
+ * Internal fields are prefixed with `_` and not part of the public API.
100
+ */
101
+ type ValfuseFormControl<TFieldValues extends Record<string, unknown>> = {
102
+ /** @internal Current field values */
103
+ _values: TFieldValues;
104
+ /** @internal Current validation errors */
105
+ _errors: ValfuseFormErrors<TFieldValues>;
106
+ /** @internal Update a single field value */
107
+ _updateField: <TName extends keyof TFieldValues & string>(name: TName, value: TFieldValues[TName]) => void;
108
+ /** @internal Mark a field as touched (triggers onBlur validation) */
109
+ _touchField: (name: string) => void;
110
+ /** @internal Set of names of fields the user has interacted with */
111
+ _touchedFields: ReadonlySet<string>;
112
+ };
113
+ type UseValfuseFormProps<TFieldValues extends Record<string, unknown>> = {
114
+ schema: ValfuseSchema;
115
+ defaultValues: TFieldValues;
116
+ /**
117
+ * When validation runs:
118
+ * - `"onSubmit"` (default) — only on form submission
119
+ * - `"onBlur"` — when a field loses focus
120
+ * - `"onChange"` — on every keystroke
121
+ * - `"onTouched"` — on first blur, then on every change after that
122
+ * - `"all"` — on both onChange and onBlur
123
+ */
124
+ mode?: ValfuseFormMode;
125
+ };
126
+ type UseValfuseFormReturn<TFieldValues extends Record<string, unknown>> = {
127
+ /** Registers an input field — spread the return value onto `<input>` */
128
+ register: <TName extends keyof TFieldValues & string>(name: TName) => ValfuseRegisterReturn;
129
+ /** Passed to `<ValfuseController control={...} />` */
130
+ control: ValfuseFormControl<TFieldValues>;
131
+ /** Wraps form submission: validates first, then calls `onValid` */
132
+ handleSubmit: (onValid: (values: TFieldValues) => void | Promise<void>) => (e?: React.FormEvent | {
133
+ preventDefault?: () => void;
134
+ }) => Promise<void>;
135
+ /**
136
+ * Reactive form state.
137
+ * Includes: errors, isSubmitting, isSubmitted, isSubmitSuccessful,
138
+ * submitCount, isDirty, isValid, dirtyFields, touchedFields, defaultValues.
139
+ */
140
+ formState: ValfuseFormState<TFieldValues>;
141
+ /** Inject external errors (e.g. from API responses) */
142
+ setErrors: (errors: ValfuseFieldErrors<Extract<keyof TFieldValues, string>>) => void;
143
+ /** Clear one, many, or all field errors */
144
+ clearErrors: (name?: keyof TFieldValues | Array<keyof TFieldValues>) => void;
145
+ /**
146
+ * Programmatically set a field value.
147
+ * Pass `{ shouldValidate: true }` to run validation immediately after setting.
148
+ */
149
+ setValue: <TName extends keyof TFieldValues>(name: TName, value: TFieldValues[TName], options?: {
150
+ shouldValidate?: boolean;
151
+ }) => void;
152
+ /**
153
+ * Manually trigger validation.
154
+ * - No argument → validate all fields
155
+ * - Single name → validate one field
156
+ * - Array of names → validate those fields
157
+ *
158
+ * Returns `true` if all triggered fields are valid, `false` otherwise.
159
+ */
160
+ trigger: (name?: keyof TFieldValues & string | Array<keyof TFieldValues & string>) => boolean;
161
+ /**
162
+ * Watch field values — mirrors react-hook-form's `watch`:
163
+ *
164
+ * ```ts
165
+ * form.watch() // all current values
166
+ * form.watch("email") // single field value
167
+ * form.watch(["email", "name"]) // array of values
168
+ * const unsub = form.watch((values, info) => { ... }); // subscribe
169
+ * unsub(); // unsubscribe
170
+ * ```
171
+ */
172
+ watch: ValfuseWatchFunction<TFieldValues>;
173
+ /** Reset the form to default values (or provided partial values) */
174
+ reset: (values?: Partial<TFieldValues>) => void;
175
+ };
176
+
177
+ declare function useValfuseForm<TFieldValues extends Record<string, unknown>>(props: UseValfuseFormProps<TFieldValues>): UseValfuseFormReturn<TFieldValues>;
178
+
179
+ type ValfuseControllerFieldState = {
180
+ error?: ValfuseFieldError;
181
+ isTouched: boolean;
182
+ };
183
+ type ValfuseControllerField<TValue> = {
184
+ name: string;
185
+ value: TValue;
186
+ /** Receives the raw value — not a DOM event */
187
+ onChange: (value: TValue) => void;
188
+ onBlur: () => void;
189
+ };
190
+ type ValfuseControllerRenderProps<TFieldValues extends Record<string, unknown>, TName extends keyof TFieldValues & string> = {
191
+ field: ValfuseControllerField<TFieldValues[TName]>;
192
+ fieldState: ValfuseControllerFieldState;
193
+ };
194
+ type ValfuseControllerProps<TFieldValues extends Record<string, unknown>, TName extends keyof TFieldValues & string = keyof TFieldValues & string> = {
195
+ control: ValfuseFormControl<TFieldValues>;
196
+ name: TName;
197
+ render: (props: ValfuseControllerRenderProps<TFieldValues, TName>) => React$1.ReactElement;
198
+ };
199
+ /**
200
+ * Controlled field wrapper for complex inputs (dropdowns, date-pickers, etc.)
201
+ * that cannot be registered with `form.register(name)`.
202
+ *
203
+ * `fieldState.error` is typed as `ValfuseFieldError | undefined`, so both
204
+ * `fieldState.error?.message` and `fieldState.error?.code` work out of the box.
205
+ *
206
+ * @example
207
+ * <ValfuseController
208
+ * control={form.control}
209
+ * name="roleId"
210
+ * render={({ field, fieldState }) => (
211
+ * <RoleDropdown
212
+ * value={field.value}
213
+ * onChange={field.onChange}
214
+ * onBlur={field.onBlur}
215
+ * error={fieldState.error?.code}
216
+ * />
217
+ * )}
218
+ * />
219
+ */
220
+ declare function ValfuseController<TFieldValues extends Record<string, unknown>, TName extends keyof TFieldValues & string = keyof TFieldValues & string>({ control, name, render, }: ValfuseControllerProps<TFieldValues, TName>): React$1.ReactElement;
221
+
222
+ /**
223
+ * Minimal runtime store contract used by React adapter hooks and provider.
224
+ */
225
+ interface LocalizationStore {
226
+ /** Returns the currently active locale. */
227
+ getLocale(): string;
228
+ /** Updates the active locale. */
229
+ setLocale(locale: string): void;
230
+ /** Looks up a key with fallback and optional interpolation. */
231
+ t(key: string, params?: Record<string, string | number>): string;
232
+ }
233
+ /**
234
+ * Creates a lightweight mutable localization store for React runtime usage.
235
+ *
236
+ * @param manifest Generated runtime manifest from `@valfuse-node/localization`.
237
+ * @param initialLocale Optional starting locale; defaults to `manifest.base_locale`.
238
+ */
239
+ declare function createLocalizationStore(manifest: RuntimeManifest, initialLocale?: string): LocalizationStore;
240
+
241
+ /**
242
+ * Generic contract for persisting / reading the active locale.
243
+ *
244
+ * Implement this interface to connect any storage backend
245
+ * (localStorage, sessionStorage, cookies, IndexedDB, remote API, …).
246
+ */
247
+ interface LocaleStorage {
248
+ /** Read the previously-saved locale. Returns `undefined` when nothing is stored. */
249
+ get(): string | undefined;
250
+ /** Persist the newly selected locale. */
251
+ set(locale: string): void;
252
+ /** Optional: remove the stored value (e.g. when locale is reset). */
253
+ remove?(): void;
254
+ }
255
+ interface LocalStorageStrategyOptions {
256
+ /** Storage key. Defaults to `"locale"`. */
257
+ key?: string;
258
+ }
259
+ interface SessionStorageStrategyOptions {
260
+ /** Storage key. Defaults to `"locale"`. */
261
+ key?: string;
262
+ }
263
+ interface CookieStrategyOptions {
264
+ /** Cookie name. Defaults to `"locale"`. */
265
+ key?: string;
266
+ /**
267
+ * Domain the cookie is scoped to.
268
+ * e.g. `".example.com"` shares across all sub-domains.
269
+ * Omit for the current host only.
270
+ */
271
+ domain?: string;
272
+ /** Cookie path. Defaults to `"/"`. */
273
+ path?: string;
274
+ /**
275
+ * Max age in seconds.
276
+ * Defaults to 1 year (`31_536_000`).
277
+ * Pass `0` to create a session cookie.
278
+ */
279
+ maxAge?: number;
280
+ /** Mark cookie as Secure. Defaults to `true` when on `https:`. */
281
+ secure?: boolean;
282
+ /** SameSite policy. Defaults to `"Lax"`. */
283
+ sameSite?: "Strict" | "Lax" | "None";
284
+ }
285
+ /**
286
+ * Persists the locale in `window.localStorage`.
287
+ *
288
+ * @example
289
+ * <LocalizationProvider storage={localStorageStrategy()} … />
290
+ */
291
+ declare function localStorageStrategy(options?: LocalStorageStrategyOptions): LocaleStorage;
292
+ /**
293
+ * Persists the locale in `window.sessionStorage` (cleared on tab close).
294
+ *
295
+ * @example
296
+ * <LocalizationProvider storage={sessionStorageStrategy()} … />
297
+ */
298
+ declare function sessionStorageStrategy(options?: SessionStorageStrategyOptions): LocaleStorage;
299
+ /**
300
+ * Persists the locale as an HTTP cookie.
301
+ *
302
+ * Supports `domain`, `path`, `maxAge`, `secure`, and `sameSite` options so the
303
+ * same cookie can be read server-side (e.g. for SSR / middleware redirects).
304
+ *
305
+ * @example
306
+ * <LocalizationProvider storage={cookieStrategy({ domain: ".example.com" })} … />
307
+ */
308
+ declare function cookieStrategy(options?: CookieStrategyOptions): LocaleStorage;
309
+ /**
310
+ * In-memory storage — locale is lost on page reload.
311
+ * Useful for testing or when no persistence is desired.
312
+ *
313
+ * @example
314
+ * <LocalizationProvider storage={memoryStrategy()} … />
315
+ */
316
+ declare function memoryStrategy(options?: {
317
+ initialLocale?: string;
318
+ }): LocaleStorage;
319
+ /**
320
+ * Combines multiple storages in priority order.
321
+ * Reads from the first storage that returns a value; writes to **all** of them.
322
+ *
323
+ * @example
324
+ * const storage = composeStorage(
325
+ * cookieStrategy({ domain: ".example.com" }),
326
+ * localStorageStrategy()
327
+ * );
328
+ */
329
+ declare function composeStorage(...storages: LocaleStorage[]): LocaleStorage;
330
+
331
+ interface LocalizationContextValue {
332
+ /** Active locale currently used for runtime resolution. */
333
+ locale: string;
334
+ /** Updates the active locale for all consumers under the provider. */
335
+ setLocale: (locale: string) => void;
336
+ /** Runtime store — `store.t(key, params)` looks up translations. */
337
+ store: LocalizationStore;
338
+ /** Generated runtime manifest — source of truth for keys and messages. */
339
+ manifest: RuntimeManifest;
340
+ }
341
+ interface LocalizationProviderProps {
342
+ /** Generated runtime manifest from `valfuse-localization generate`. */
343
+ manifest: RuntimeManifest;
344
+ /**
345
+ * Optional initial locale. When `storage` is also provided the stored value
346
+ * takes precedence over `initialLocale`.
347
+ */
348
+ initialLocale?: string;
349
+ /**
350
+ * Pluggable locale storage strategy.
351
+ *
352
+ * Built-in helpers (imported from `@valfuse-node/react`):
353
+ * - `localStorageStrategy()` — persists in `window.localStorage`
354
+ * - `sessionStorageStrategy()` — persists in `window.sessionStorage`
355
+ * - `cookieStrategy({ domain, maxAge, … })` — persists as a cookie
356
+ * - `memoryStrategy()` — in-memory only (no persistence)
357
+ * - `composeStorage(a, b)` — combines multiple strategies
358
+ */
359
+ storage?: LocaleStorage;
360
+ }
361
+ /**
362
+ * Provides localization state and runtime store to the React subtree.
363
+ *
364
+ * Pass a `storage` strategy to persist the selected locale across page reloads.
365
+ *
366
+ * @example
367
+ * import { LocalizationProvider, localStorageStrategy } from "@valfuse-node/react";
368
+ *
369
+ * <LocalizationProvider manifest={manifest} storage={localStorageStrategy()}>
370
+ * <App />
371
+ * </LocalizationProvider>
372
+ */
373
+ declare function LocalizationProvider({ manifest, initialLocale, storage, children, }: PropsWithChildren<LocalizationProviderProps>): react_jsx_runtime.JSX.Element;
374
+
375
+ type InterpolationParams = Record<string, string | number>;
376
+ type GenderVariant = "male" | "female" | "other";
377
+ interface NamespacedLocalizer {
378
+ translate(key: string, fallbackValue?: string | null): string;
379
+ /** Returns `null` when key does not exist or is `null`/`undefined`. */
380
+ translateOrNull(key: string | null | undefined): string | null;
381
+ format(key: string, params: InterpolationParams): string;
382
+ /** Returns `null` when key does not exist or is `null`/`undefined`. */
383
+ formatOrNull(key: string | null | undefined, params: InterpolationParams): string | null;
384
+ plural(key: string, count: number): string;
385
+ /** Returns `null` when key does not exist or is `null`/`undefined`. */
386
+ pluralOrNull(key: string | null | undefined, count: number): string | null;
387
+ gender(key: string, gender: GenderVariant, params: InterpolationParams): string;
388
+ context(key: string, context: string, params?: InterpolationParams): string;
389
+ }
390
+ type TranslationFallback = string | Record<string, string> | ((key: string) => string | undefined);
391
+ interface UseLocalizationOptions {
392
+ fallback?: TranslationFallback;
393
+ }
394
+ /**
395
+ * Returns the current localization context from `LocalizationProvider`.
396
+ *
397
+ * @throws Error when called outside of `LocalizationProvider`.
398
+ */
399
+ declare function useLocalization(options?: UseLocalizationOptions): {
400
+ translate: (key: string, fallbackValue?: string | null) => string;
401
+ translateOrNull: (key: string | null | undefined) => string | null;
402
+ format: (key: string, params: InterpolationParams) => string;
403
+ formatOrNull: (key: string | null | undefined, params: InterpolationParams) => string | null;
404
+ plural: (key: string, count: number) => string;
405
+ pluralOrNull: (key: string | null | undefined, count: number) => string | null;
406
+ gender: (key: string, value: GenderVariant, params: InterpolationParams) => string;
407
+ context: (key: string, value: string, params?: InterpolationParams) => string;
408
+ namespace: (scope: string) => NamespacedLocalizer;
409
+ entriesForLocale: [string, string][];
410
+ locale: string;
411
+ setLocale: (locale: string) => void;
412
+ store: LocalizationStore;
413
+ manifest: _valfuse_node_localization.RuntimeManifest;
414
+ };
415
+
416
+ /**
417
+ * Builds a nested object API from manifest entries for ergonomic app consumption.
418
+ *
419
+ * - Non-placeholder entries → string values.
420
+ * - Placeholder entries → functions accepting interpolation params.
421
+ */
422
+ declare function useLocalizationTree(): {
423
+ strings: Record<string, unknown>;
424
+ placeholders: Record<string, unknown>;
425
+ };
426
+
427
+ /**
428
+ * Creates a lazy locale loader that fetches messages from a pre-built manifest.
429
+ *
430
+ * Useful for code-splitting scenarios or SSR hydration flows where you want to
431
+ * load only the active locale's messages on demand.
432
+ */
433
+ declare function createLazyLocaleLoader(manifest: RuntimeManifest): (locale: string) => Promise<Record<string, string>>;
434
+
435
+ /**
436
+ * Creates an SSR-safe localization state snapshot.
437
+ *
438
+ * Use this on the server to pre-render localized content and hydrate
439
+ * the client with the correct locale without a flash of incorrect content.
440
+ */
441
+ declare function createSsrLocalizationState(manifest: RuntimeManifest, locale?: string): {
442
+ locale: string;
443
+ messages: Record<string, string>;
444
+ };
445
+
446
+ export { type GenderVariant, type InterpolationParams, type LocaleStorage, type LocalizationContextValue, LocalizationProvider, type LocalizationProviderProps, type LocalizationStore, type NamespacedLocalizer, type TranslationFallback, type UseLocalizationOptions, type UseValfuseFormProps, type UseValfuseFormReturn, ValfuseController, type ValfuseControllerField, type ValfuseControllerFieldState, type ValfuseControllerProps, type ValfuseControllerRenderProps, type ValfuseDirtyFields, type ValfuseFieldError, type ValfuseFormControl, type ValfuseFormErrors, type ValfuseFormMode, type ValfuseFormState, type ValfuseRegisterReturn, type ValfuseTouchedFields, type ValfuseWatchCallback, type ValfuseWatchFunction, composeStorage, cookieStrategy, createLazyLocaleLoader, createLocalizationStore, createSsrLocalizationState, localStorageStrategy, memoryStrategy, sessionStorageStrategy, useLocalization, useLocalizationTree, useValfuseForm };