@voltro/client 0.52.0 → 0.54.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/dist/index.d.ts CHANGED
@@ -6,6 +6,8 @@ import { Effect } from 'effect';
6
6
  import { EventDescriptor as EventDescriptor_2 } from '@voltro/protocol';
7
7
  import { Fiber } from 'effect';
8
8
  import { ManagedRuntime } from 'effect';
9
+ import { ParseResult } from 'effect';
10
+ import { ReactElement } from 'react';
9
11
  import { ReactNode } from 'react';
10
12
  import { Rpc } from '@effect/rpc';
11
13
  import { RpcClient } from '@effect/rpc';
@@ -215,6 +217,23 @@ export declare interface ApiHooks<P extends ProcedureTypeMap> {
215
217
  }): SubscriptionState<QueryData<P, Tag>> | SubscriptionIdle;
216
218
  };
217
219
  readonly useMutation: <Tag extends MutationTag<P>>(tag: Tag) => MutationBuilder<ProcedureInput<P[Tag]>, ProcedureOutput<P[Tag]>>;
220
+ /** Schema-driven form bound to one mutation — tag as a literal, `values` /
221
+ * `defaults` / the submit output all inferred from the descriptor. The
222
+ * input schema still resolves from the mounted client descriptor at
223
+ * runtime, exactly like the untyped hook.
224
+ *
225
+ * Two shapes: without `toInput` the form values ARE the mutation input.
226
+ * WITH `toInput` the form has its OWN `Values` shape (inferred from
227
+ * `defaults` / the mapper) and `toInput` must produce the mutation's
228
+ * input — checked by the compiler, not asserted. */
229
+ readonly useFormBinding: {
230
+ <Tag extends MutationTag<P>>(tag: Tag, options?: UseFormBindingOptions<FormInput<P, Tag>> & {
231
+ readonly toInput?: undefined;
232
+ }): FormBinding<FormInput<P, Tag>, ProcedureOutput<P[Tag]>>;
233
+ <Tag extends MutationTag<P>, Values extends Record<string, unknown>>(tag: Tag, options: UseFormBindingOptions<Values> & {
234
+ readonly toInput: (values: Partial<Values>) => ProcedureInput<P[Tag]>;
235
+ }): FormBinding<Values, ProcedureOutput<P[Tag]>>;
236
+ };
218
237
  readonly useAction: <Tag extends ActionTag<P>>(tag: Tag) => ActionState<ProcedureInput<P[Tag]>, ProcedureOutput<P[Tag]>>;
219
238
  }
220
239
 
@@ -751,6 +770,13 @@ export declare interface DataTableState<Row> {
751
770
  */
752
771
  export declare const decideAccess: (guards: ReadonlyArray<ManifestGuard> | undefined, subjectScopes: ReadonlyArray<string>) => AccessDecision;
753
772
 
773
+ /**
774
+ * Walk a ParseError's issue tree into structured, PATH-PRESERVING issues.
775
+ * Every leaf keeps its full path (`address.city`, `entries.0.startsAt`) —
776
+ * nothing collapses to the top-level segment.
777
+ */
778
+ export declare const decodeFieldIssues: (error: ParseResult.ParseError) => ReadonlyArray<FieldIssue>;
779
+
754
780
  /** All defined stores, in definition order. */
755
781
  export declare const definedStores: () => ReadonlyArray<StoreHandle<never>>;
756
782
 
@@ -792,6 +818,10 @@ export declare const defineTracking: <P = Record<string, unknown>>(name: string,
792
818
  */
793
819
  export declare const deriveEntityAdmins: (manifest: CapabilityManifest) => ReadonlyArray<EntityAdminSpec>;
794
820
 
821
+ /** The document's locale per the framework contract (`<html lang>`); `'en'`
822
+ * outside a browser. Primary subtag only (`de-AT` → `de`). */
823
+ export declare const documentLocale: () => string;
824
+
795
825
  export declare const enqueueEntry: (queue: Outbox, id: string, tag: string, input: unknown) => Outbox;
796
826
 
797
827
  /**
@@ -936,17 +966,56 @@ export declare interface FieldDescriptor {
936
966
  /** The resolved JSON-Schema node — carries `maxLength` / `pattern` /
937
967
  * `minimum` / `format` / `description` for widgets + validation hints. */
938
968
  readonly jsonSchema: Readonly<Record<string, unknown>>;
969
+ /** Dotted parent path for a nested-struct member (`'address'` for
970
+ * `address.city`) — the SECTION a renderer groups it under. Top-level
971
+ * fields carry none. Overridable via `formField({ section })`. */
972
+ readonly section?: string;
973
+ /** Human label for the section (humanised path tail). */
974
+ readonly sectionLabel?: string;
975
+ /** Help text — the Schema `description` annotation. */
976
+ readonly description?: string;
977
+ /** Declared ordering (`formField({ order })`); fields sort by it, stable
978
+ * against declaration order. */
979
+ readonly order?: number;
980
+ /** For `widget: 'array'` (array of structs): the ITEM's field descriptors,
981
+ * names relative to one item (`startsAt`, not `entries.0.startsAt`). */
982
+ readonly item?: ReadonlyArray<FieldDescriptor>;
983
+ /** For `widget: 'reference'`: the referenced TABLE — a renderer binds a
984
+ * query-backed picker to it; the VALUE stays the id (or id list). Comes
985
+ * from `formField({ reference: 'stores' })`. */
986
+ readonly reference?: string;
939
987
  }
940
988
 
941
989
  export declare interface FieldErrors {
942
990
  readonly [field: string]: string;
943
991
  }
944
992
 
993
+ /** One structured validation issue. `path` is the dotted field path
994
+ * (`'email'`, `'address.city'`, `'entries.0.startsAt'`); `id` is the stable
995
+ * message id (`'validation.minLength'`); `params` feed its template;
996
+ * `fallback` is a rendered English default so an unknown id still shows a
997
+ * sentence, never a bare key. */
998
+ export declare interface FieldIssue {
999
+ readonly path: string;
1000
+ readonly id: string;
1001
+ readonly params?: Readonly<Record<string, unknown>>;
1002
+ readonly fallback: string;
1003
+ }
1004
+
945
1005
  export declare interface FieldOption {
946
1006
  readonly value: string;
947
1007
  readonly label: string;
948
1008
  }
949
1009
 
1010
+ /** One display section of a form: `name` is the section path (`'address'`),
1011
+ * `undefined` for the top-level run of unsectioned fields. Contiguous in
1012
+ * display order, so a renderer can walk sections top to bottom. */
1013
+ export declare interface FieldSection {
1014
+ readonly name: string | undefined;
1015
+ readonly label: string | undefined;
1016
+ readonly fields: ReadonlyArray<FieldDescriptor>;
1017
+ }
1018
+
950
1019
  export declare interface FilterDescriptor {
951
1020
  readonly name: string;
952
1021
  readonly label: string;
@@ -977,16 +1046,36 @@ export declare const FORM_REDIRECT_FIELD = "__voltro_redirect";
977
1046
  /** The path a native `<AutoForm>` POST goes to. */
978
1047
  export declare const formActionPath: (mutationTag: string) => string;
979
1048
 
1049
+ /** A field ARRAY (`entries[]`): items + structural operations + per-item
1050
+ * field handles (`array('entries').field(0, 'startsAt')`). */
1051
+ export declare interface FormArrayHandle {
1052
+ readonly items: ReadonlyArray<unknown>;
1053
+ readonly push: (item: unknown) => void;
1054
+ readonly insert: (index: number, item: unknown) => void;
1055
+ readonly remove: (index: number) => void;
1056
+ readonly move: (from: number, to: number) => void;
1057
+ readonly swap: (a: number, b: number) => void;
1058
+ readonly field: (index: number, name: string) => FormFieldHandle;
1059
+ }
1060
+
980
1061
  export declare interface FormBinding<Input, Output> {
981
- /** Ordered, render-agnostic field list derived from the input schema. */
1062
+ /** Ordered, render-agnostic field list derived from the input schema
1063
+ * nested structs flattened into sections, arrays carrying item
1064
+ * descriptors (see `schemaToFields`). */
982
1065
  readonly fields: ReadonlyArray<FieldDescriptor>;
983
1066
  readonly values: Partial<Input>;
984
- /** First error per field populated on submit. */
1067
+ /** First VISIBLE error per field PATH (full dotted path — `'address.city'`,
1068
+ * not its top segment), display text in the resolved locale: gated schema
1069
+ * issues, blocked async checks, and routed server field errors. */
985
1070
  readonly errors: FieldErrors;
1071
+ /** Every current schema issue, structured `{ path, id, params, fallback }`
1072
+ * — ungated, for widget kits that translate themselves. */
1073
+ readonly issues: ReadonlyArray<FieldIssue>;
986
1074
  /** True when the CURRENT values decode against the schema. */
987
1075
  readonly isValid: boolean;
988
1076
  readonly pending: boolean;
989
- /** The mutation's typed failure, if the last submit threw. */
1077
+ /** The mutation's typed failure, if the last submit threw — MINUS anything
1078
+ * field-routable, which lands in `errors` instead. */
990
1079
  readonly submitError: unknown | undefined;
991
1080
  /** A form-level (non-field) error carried in from a failed no-JS POST —
992
1081
  * the RPC refused after valid input. Cleared by the next submit/reset. */
@@ -994,10 +1083,22 @@ export declare interface FormBinding<Input, Output> {
994
1083
  readonly data: Output | undefined;
995
1084
  readonly setValue: (name: string, value: unknown) => void;
996
1085
  readonly setValues: (patch: Partial<Input>) => void;
997
- readonly reset: () => void;
998
- /** Validate; if valid, run the mutation. Returns the output, or `undefined`
999
- * when validation blocked the submit. */
1086
+ /** Reset to the initial defaults — or to NEW defaults (switching the edited
1087
+ * record without a remount). */
1088
+ readonly reset: (nextDefaults?: Partial<Input>) => void;
1089
+ /** Validate (incl. async checks); if valid, map (`toInput`), run the
1090
+ * mutation (or `onSubmit`), route server field errors. Returns the output,
1091
+ * or `undefined` when anything blocked the submit. */
1000
1092
  readonly submit: () => Promise<Output | undefined>;
1093
+ /** One field, bound — see {@link FormFieldHandle}. */
1094
+ readonly field: (path: string) => FormFieldHandle;
1095
+ /** A field array — see {@link FormArrayHandle}. */
1096
+ readonly array: (path: string) => FormArrayHandle;
1097
+ /** The descriptors of one section (`section('address')`). */
1098
+ readonly section: (name: string) => ReadonlyArray<FieldDescriptor>;
1099
+ readonly state: FormStateSnapshot;
1100
+ /** Move focus to the first field with a visible error (display order). */
1101
+ readonly focusFirstInvalid: () => void;
1001
1102
  }
1002
1103
 
1003
1104
  /**
@@ -1025,6 +1126,57 @@ export declare const formDataToInput: (schema: Schema.Schema.Any, entries: Itera
1025
1126
  * uploads are a declared limit of the no-JS path. */
1026
1127
  export declare type FormEntry = readonly [name: string, value: string];
1027
1128
 
1129
+ /** UI metadata for one schema field, as an annotations object:
1130
+ *
1131
+ * Schema.String.annotations(formField({ section: 'contact', order: 2 }))
1132
+ *
1133
+ * Rides the JSON-Schema annotation (merged, not replaced — verified), so it
1134
+ * survives the same normalisation every other descriptor fact does. */
1135
+ export declare const formField: (meta: {
1136
+ readonly label?: string;
1137
+ readonly section?: string;
1138
+ readonly order?: number;
1139
+ readonly widget?: WidgetKind;
1140
+ /** Mark the field a REFERENCE to a table: `formField({ reference: 'stores' })`
1141
+ * → `widget: 'reference'` carrying the target, value = the id (or id
1142
+ * list, for a multi-reference feeding a target's `relations:`). */
1143
+ readonly reference?: string;
1144
+ }) => {
1145
+ readonly jsonSchema: {
1146
+ readonly "x-voltro": typeof meta;
1147
+ };
1148
+ };
1149
+
1150
+ /** One field, bound: value + change/blur wiring + gated error + descriptor
1151
+ * facts + a11y props a widget kit spreads onto its input. */
1152
+ export declare interface FormFieldHandle {
1153
+ readonly value: unknown;
1154
+ readonly setValue: (value: unknown) => void;
1155
+ readonly onBlur: () => void;
1156
+ /** The VISIBLE error (display-gated schema error, server error, or async
1157
+ * verdict) — `undefined` while hidden or absent. */
1158
+ readonly error: string | undefined;
1159
+ /** The routed server error for this field, if any (also part of `error`). */
1160
+ readonly serverError: string | undefined;
1161
+ readonly touched: boolean;
1162
+ readonly blurred: boolean;
1163
+ readonly dirty: boolean;
1164
+ readonly required: boolean;
1165
+ readonly label: string;
1166
+ readonly widget: string;
1167
+ readonly options: ReadonlyArray<FieldOption> | undefined;
1168
+ readonly description: string | undefined;
1169
+ /** `'idle' | 'checking' | 'valid' | 'invalid'` — mirrors the entry passed
1170
+ * under this path in `asyncFields`. */
1171
+ readonly asyncStatus: AsyncValidationResult['status'];
1172
+ readonly a11y: {
1173
+ readonly id: string;
1174
+ readonly 'aria-invalid': true | undefined;
1175
+ readonly 'aria-required': true | undefined;
1176
+ readonly 'aria-describedby': string | undefined;
1177
+ };
1178
+ }
1179
+
1028
1180
  /**
1029
1181
  * The error/values payload a failed no-JS form POST carries back into the
1030
1182
  * 422 re-render. Keyed by `formKey` so a page with several forms re-fills
@@ -1044,6 +1196,56 @@ export declare interface FormFlashPayload {
1044
1196
  readonly formError?: string;
1045
1197
  }
1046
1198
 
1199
+ /** A mutation's input as the form binding's value shape. A struct input is its
1200
+ * own shape; the (theoretical) non-object input degrades to the untyped map
1201
+ * rather than failing the whole binding. */
1202
+ declare type FormInput<P extends ProcedureTypeMap, Tag extends keyof P> = ProcedureInput<P[Tag]> extends Record<string, unknown> ? ProcedureInput<P[Tag]> : Record<string, unknown>;
1203
+
1204
+ /**
1205
+ * Group an ordered field list into contiguous sections — the structure a
1206
+ * form (or its skeleton — the placeholder must have the SHAPE of the real
1207
+ * thing, title rows included) renders. Pure; feeds `<FormSkeleton>`,
1208
+ * `<AutoForm>` layouts and `form.section(...)` alike.
1209
+ */
1210
+ export declare const formSections: (fields: ReadonlyArray<FieldDescriptor>) => ReadonlyArray<FieldSection>;
1211
+
1212
+ /** The full form state, engine + mutation, in ONE place — a save button reads
1213
+ * `canSubmit`/`pending`, a dialog closes on `isSubmitSuccessful`, a banner
1214
+ * listens to `submitError`. */
1215
+ export declare interface FormStateSnapshot {
1216
+ readonly isDirty: boolean;
1217
+ readonly isPristine: boolean;
1218
+ readonly isTouched: boolean;
1219
+ /** The CURRENT values decode (display gating does not apply here). */
1220
+ readonly isValid: boolean;
1221
+ /** An async field check is in flight. */
1222
+ readonly isValidating: boolean;
1223
+ readonly canSubmit: boolean;
1224
+ readonly isSubmitting: boolean;
1225
+ readonly isSubmitted: boolean;
1226
+ readonly isSubmitSuccessful: boolean;
1227
+ readonly submissionAttempts: number;
1228
+ /** VISIBLE errors right now (post-gating). */
1229
+ readonly errorCount: number;
1230
+ readonly firstInvalidPath: string | undefined;
1231
+ /** The mutation is on the wire — can outlast `isSubmitting` under
1232
+ * auto-optimistic. */
1233
+ readonly pending: boolean;
1234
+ readonly isLoading: boolean;
1235
+ readonly submitError: unknown | undefined;
1236
+ readonly data: unknown;
1237
+ }
1238
+
1239
+ export declare interface FormValidateOptions {
1240
+ /** When a field's error becomes VISIBLE while editing. `'afterTouched'`
1241
+ * (default): after that field blurred once, or after the first submit
1242
+ * attempt — a form never opens with errors. `'always'`: live from the
1243
+ * first keystroke. `'never'`: only after a submit attempt. */
1244
+ readonly onChange?: 'always' | 'afterTouched' | 'never';
1245
+ /** Reveal a field's error when it blurs (default true). */
1246
+ readonly onBlur?: boolean;
1247
+ }
1248
+
1047
1249
  export declare interface FrameworkRuntimes {
1048
1250
  /** Look up an api handle (runtime + cache) by name. Throws if no such api was mounted. */
1049
1251
  readonly get: (name: string) => ApiHandle;
@@ -1352,6 +1554,9 @@ export declare interface OutboxControls {
1352
1554
  readonly online: boolean;
1353
1555
  readonly pending: number;
1354
1556
  readonly conflicts: ReadonlyArray<OutboxEntry>;
1557
+ /** Resolve a conflicted entry with a new input (see {@link resolveConflictEntry})
1558
+ * and replay immediately when online. */
1559
+ readonly resolveConflict: (id: string, input: unknown) => void;
1355
1560
  }
1356
1561
 
1357
1562
  export declare interface OutboxEntry {
@@ -1363,6 +1568,22 @@ export declare interface OutboxEntry {
1363
1568
  readonly error?: unknown;
1364
1569
  }
1365
1570
 
1571
+ /**
1572
+ * Durability seam for the outbox queue. `load` runs once on mount and returns
1573
+ * the surviving entries (oldest first); `save` is called after every queue
1574
+ * transition with the entries worth keeping (`sent` entries are compacted away
1575
+ * before the call — a delivered write needs no durability).
1576
+ *
1577
+ * The one real implementation is `outboxPersistence()` in
1578
+ * `@voltro/local-first`, which stores the entries through the same
1579
+ * `PersistenceAdapter` the sync engine uses — ONE durable queue for a device,
1580
+ * whichever surface enqueued the write.
1581
+ */
1582
+ export declare interface OutboxPersistence {
1583
+ readonly load: () => Promise<Outbox>;
1584
+ readonly save: (queue: Outbox) => Promise<void>;
1585
+ }
1586
+
1366
1587
  export declare type OutboxStatus = 'pending' | 'sent' | 'failed' | 'conflict';
1367
1588
 
1368
1589
  /** Provide the current subject's scopes to `useCan`. Mount it once high in the
@@ -1664,6 +1885,14 @@ export declare type ResolvableHeaders = Readonly<Record<string, string>> | (() =
1664
1885
 
1665
1886
  export declare const resolveByTag: (client: unknown, tag: string) => unknown;
1666
1887
 
1888
+ /** Resolve a conflicted entry: replace its input with the RESOLVED value and
1889
+ * return it to `pending`, so the next replay sends the resolution and the
1890
+ * entries it was blocking become replayable again. The resolved input
1891
+ * typically comes from a conflict policy (`@voltro/local-first`'s
1892
+ * `resolveWithPolicy` — CRDT fields merge, scalars resolve deterministically);
1893
+ * a caller may equally pass a hand-picked value. */
1894
+ export declare const resolveConflictEntry: (queue: Outbox, id: string, input: unknown) => Outbox;
1895
+
1667
1896
  export declare interface ResolvedClient {
1668
1897
  readonly runtime: AnyRuntime;
1669
1898
  readonly cache: SubscriptionCache;
@@ -1676,6 +1905,13 @@ export declare const resolveStoreStorage: (area: StoreStorageArea) => StoreStora
1676
1905
  /** Resolve one map entry against the props. Pure. */
1677
1906
  export declare const resolveTrackingEvent: <P>(entry: TrackingEntry<P> | undefined, props: P) => TrackingEvent | null;
1678
1907
 
1908
+ /**
1909
+ * Render one issue id to display text: app resolver first (its `undefined`
1910
+ * falls through), then the built-in catalog for the locale's primary subtag,
1911
+ * then English, then the issue's own fallback sentence.
1912
+ */
1913
+ export declare const resolveValidationMessage: (issue: Pick<FieldIssue, "id" | "params" | "fallback">, options?: ValidateOptions) => string;
1914
+
1679
1915
  export declare interface ResourceCanInput {
1680
1916
  /** The action being gated (e.g. `'read'`, `'write'`, `'delete'`). */
1681
1917
  readonly action: string;
@@ -2280,6 +2516,8 @@ export declare class SubscriptionCache {
2280
2516
  private readonly inactiveTtlMs;
2281
2517
  private readonly onChangeHook;
2282
2518
  private readonly errorBus;
2519
+ private readonly mirror;
2520
+ private readonly mergeCell;
2283
2521
  constructor(options?: SubscriptionCacheOptions);
2284
2522
  /**
2285
2523
  * Register a subscriber. The cache forks the underlying fiber on the
@@ -2495,6 +2733,17 @@ export declare class SubscriptionCache {
2495
2733
  }
2496
2734
 
2497
2735
  export declare interface SubscriptionCacheOptions {
2736
+ /** Durable local mirror (see {@link SubscriptionMirror}). */
2737
+ readonly mirror?: SubscriptionMirror;
2738
+ /**
2739
+ * CRDT cell merger for the downstream lane (plan 18): folds a
2740
+ * `mergeCells` delta op's incremental update into the held cell. The one
2741
+ * real implementation is `crdtMergeCell` from `@voltro/local-first` (this
2742
+ * package stays CRDT-library-free). Absent → a mergeCells op replaces the
2743
+ * cell (the producer only emits them when the app declares CRDT tables,
2744
+ * so an app without the binding never sees one).
2745
+ */
2746
+ readonly mergeCell?: (column: string, prevValue: unknown, update: unknown) => unknown;
2498
2747
  /** Milliseconds to keep an entry alive after the last subscriber leaves.
2499
2748
  * Lets back-button / quick remount reuse the warm subscription. */
2500
2749
  readonly inactiveTtlMs?: number;
@@ -2588,6 +2837,29 @@ export declare interface SubscriptionMeta {
2588
2837
  readonly pendingPatches: number;
2589
2838
  }
2590
2839
 
2840
+ /**
2841
+ * Durable local mirror seam (plan 02 — local-first). `save` fires after every
2842
+ * server event that moved an entry's base (snapshot or delta) with the rows
2843
+ * AND the revision; `load` runs once for a COLD entry (no server data yet)
2844
+ * and, when it resolves first, seeds the entry so the UI renders mirrored
2845
+ * rows offline — and the NEXT (re)subscribe presents the mirrored revision as
2846
+ * `voltro-resume-from`, composing with delta-resume. The implementation
2847
+ * (`@voltro/local-first`'s query mirror) owns partitioning, encrypted-column
2848
+ * stripping and which tags mirror at all — the cache calls it for every
2849
+ * entry and a foreign tag is the mirror's no-op.
2850
+ */
2851
+ export declare interface SubscriptionMirror {
2852
+ readonly load: (key: string) => Promise<{
2853
+ readonly data: unknown;
2854
+ readonly revision: number;
2855
+ } | undefined>;
2856
+ readonly save: (key: string, snap: {
2857
+ readonly tag: string | undefined;
2858
+ readonly data: unknown;
2859
+ readonly revision: number;
2860
+ }) => Promise<void>;
2861
+ }
2862
+
2591
2863
  /**
2592
2864
  * Subscribe to a streaming rpc by tag.
2593
2865
  *
@@ -3165,8 +3437,88 @@ export declare interface UseFormBindingOptions<Input> {
3165
3437
  * the initial values + field errors, so the 422 re-render shows the
3166
3438
  * submitted state server-side and hydrates to the identical state. */
3167
3439
  readonly flash?: FormFlashPayload | undefined;
3440
+ /** Locale for the built-in validation messages (en/de shipped). Default:
3441
+ * the document's `<html lang>` — the framework's locale contract. */
3442
+ readonly locale?: string;
3443
+ /** App override for message-id resolution — wire your i18n catalog in one
3444
+ * line: `messages: (id, params) => t(id, params)`. Return `undefined` to
3445
+ * fall through to the built-in defaults. Resolves BOTH schema-validation
3446
+ * ids (`validation.minLength`) and server ids (`validation.emailTaken`
3447
+ * from `ctx.validation.fail`). */
3448
+ readonly messages?: ValidationMessageResolver;
3449
+ /** Error-visibility timing — see {@link FormValidateOptions}. */
3450
+ readonly validate?: FormValidateOptions;
3451
+ /**
3452
+ * Form values ≠ mutation input — the mapping seam. Runs BEFORE validation:
3453
+ * the INPUT is validated against the schema, and its error paths map back
3454
+ * to form fields via {@link UseFormBindingOptions.errorPath} (same-name
3455
+ * fields map automatically).
3456
+ *
3457
+ * toInput: (values) => ({ id: employee.id, ...employeePatch(values) })
3458
+ */
3459
+ readonly toInput?: (values: Partial<Input>) => unknown;
3460
+ /** Input-path → form-path mapping for `toInput` error routing, e.g.
3461
+ * `{ firstName: 'name' }`. Same-name paths need no entry. */
3462
+ readonly errorPath?: Readonly<Record<string, string>>;
3463
+ /**
3464
+ * Own the save: runs INSTEAD of the plain `mutate(input)`, with the mutation
3465
+ * handle in hand — for composed writes (mutation + links + follow-ups).
3466
+ * Server field errors thrown here still route to their fields.
3467
+ *
3468
+ * onSubmit: async ({ input, mutate, values }) => {
3469
+ * const row = await mutate(input)
3470
+ * await setLinks(values.assignedStores)
3471
+ * return row
3472
+ * }
3473
+ */
3474
+ readonly onSubmit?: (args: {
3475
+ readonly input: unknown;
3476
+ readonly values: Partial<Input>;
3477
+ readonly mutate: (input: unknown) => Promise<unknown>;
3478
+ }) => Promise<unknown>;
3479
+ /** Ran after a successful submit with the mutation's output. */
3480
+ readonly onSuccess?: (output: unknown) => void;
3481
+ /**
3482
+ * Async field checks `submit` must wait for. Pass each field's
3483
+ * `useAsyncValidation` result under its field path:
3484
+ *
3485
+ * const email = useAsyncValidation('app', 'users.emailAvailable', values.email ?? '', { … })
3486
+ * const form = useFormBinding('app', 'users.create', { asyncFields: { email } })
3487
+ *
3488
+ * On submit, in-flight checks are awaited (bounded by `asyncTimeoutMs`),
3489
+ * an `'invalid'` verdict blocks the submit with the error on ITS field.
3490
+ */
3491
+ readonly asyncFields?: Readonly<Record<string, AsyncValidationResult>>;
3492
+ /** Upper bound for waiting on `asyncFields` (ms, default 5000). On timeout
3493
+ * the still-checking field blocks the submit with `validation.checking`. */
3494
+ readonly asyncTimeoutMs?: number;
3495
+ /** The defaults hang on a subscription that has not delivered yet. While
3496
+ * true the form suppresses `dirty` and every error — render the skeleton,
3497
+ * not an empty form scolding the user. */
3498
+ readonly isLoading?: boolean;
3499
+ /** Re-render scope of THIS hook. `'all'` (default): re-render on every
3500
+ * engine change — simplest, correct everywhere. `'fields'`: the hook only
3501
+ * re-renders on submission-level changes; field components subscribe
3502
+ * narrowly via `useFormField(form, path)` so a keystroke re-renders one
3503
+ * field, not the page. In that mode `values`/`errors` on the binding are
3504
+ * the state as of the LAST binding render. */
3505
+ readonly subscribe?: 'all' | 'fields';
3168
3506
  }
3169
3507
 
3508
+ /**
3509
+ * Narrow per-field subscription — the render-performance half of the engine.
3510
+ *
3511
+ * `useFormBinding` with `subscribe: 'fields'` re-renders only on
3512
+ * submission-level changes; each field COMPONENT calls this with the binding
3513
+ * and its path, and a keystroke re-renders that field alone:
3514
+ *
3515
+ * const Field = ({ form, path }: { form: FormBinding<I, O>; path: string }) => {
3516
+ * const f = useFormField(form, path)
3517
+ * return <input id={f.a11y.id} value={String(f.value ?? '')} onChange={(e) => f.setValue(e.target.value)} onBlur={f.onBlur} />
3518
+ * }
3519
+ */
3520
+ export declare const useFormField: (binding: FormBinding<never, never> | FormBinding<Record<string, unknown>, unknown>, path: string) => FormFieldHandle;
3521
+
3170
3522
  /**
3171
3523
  * The failed-no-JS-POST payload for ONE form, or `undefined`.
3172
3524
  *
@@ -3205,6 +3557,8 @@ export declare const useOutbox: (options: UseOutboxOptions) => OutboxControls;
3205
3557
  export declare interface UseOutboxOptions {
3206
3558
  /** Perform one queued write (the real mutation). Throw to fail. */
3207
3559
  readonly send: (entry: OutboxEntry) => Promise<void>;
3560
+ /** Durable backing (see {@link OutboxPersistence}). Omitted → in-memory only. */
3561
+ readonly persistence?: OutboxPersistence;
3208
3562
  /** Online override (defaults to navigator.onLine + online/offline events). */
3209
3563
  readonly online?: boolean;
3210
3564
  /** Classify a send error as a conflict (blocks replay) vs a transient
@@ -3378,6 +3732,10 @@ export declare interface UseUndoOptions {
3378
3732
  */
3379
3733
  export declare const useUpload: (apiName: string, hookOpts?: UploadOptions) => UploadHandle;
3380
3734
 
3735
+ /** The app-wide resolver, if a provider is mounted. Consumed by the form
3736
+ * binding; exported for widget kits that resolve ids themselves. */
3737
+ export declare const useValidationMessages: () => ValidationMessageResolver | undefined;
3738
+
3381
3739
  export declare const useWindowedSubscription: <Row = Record<string, unknown>>(apiName: string, queryTag: string, options: UseWindowedSubscriptionOptions) => WindowedSubscriptionState<Row>;
3382
3740
 
3383
3741
  export declare interface UseWindowedSubscriptionOptions {
@@ -3421,20 +3779,39 @@ export declare const useWorkflowUpdate: (apiName: string) => WorkflowUpdateState
3421
3779
  export declare const validateEventPayload: (descriptor: EventDescriptor<string, unknown>, payload: unknown) => EventValidation;
3422
3780
 
3423
3781
  /**
3424
- * Validate `values` against an input `schema`, returning the first error per
3425
- * top-level field (`{ [field]: message }`) the shape `<AutoForm>` and custom
3426
- * widgets render inline. Uses the same `decodeUnknownEither` + `ArrayFormatter`
3427
- * path the store's `.validate(...)` uses, so client and server speak ONE
3428
- * schema. `{ errors: 'all' }` collects every field's error in one pass.
3429
- *
3430
- * Client-side validation is non-authoritative UX — the server re-validates the
3431
- * same schema on the mutation; this just gives instant inline feedback.
3782
+ * Validate `values` against an input `schema`. Uses the same
3783
+ * `decodeUnknownEither` path the store's `.validate(...)` uses, so client and
3784
+ * server speak ONE schema; `{ errors: 'all' }` collects every field in one
3785
+ * pass. Client-side validation is non-authoritative UX the server
3786
+ * re-validates the same schema on the mutation.
3432
3787
  */
3433
- export declare const validateFields: (schema: Schema.Schema.Any, values: unknown) => ValidationResult;
3788
+ export declare const validateFields: (schema: Schema.Schema.Any, values: unknown, options?: ValidateOptions) => ValidationResult;
3789
+
3790
+ export declare interface ValidateOptions {
3791
+ /** BCP-47-ish tag; only the primary subtag is read. Default: the document's
3792
+ * `<html lang>`, else `'en'`. */
3793
+ readonly locale?: string;
3794
+ readonly messages?: ValidationMessageResolver;
3795
+ }
3796
+
3797
+ /** Resolve one message id to display text. Return `undefined` to fall through
3798
+ * to the built-in en/de defaults — so a resolver only has to know the ids it
3799
+ * overrides. Wire it to `@voltro/i18n` in one line:
3800
+ * `messages: (id, params) => t(id, params)`. */
3801
+ export declare type ValidationMessageResolver = (id: string, params: Readonly<Record<string, unknown>> | undefined, fallback: string) => string | undefined;
3802
+
3803
+ export declare const ValidationMessagesProvider: (props: {
3804
+ readonly messages: ValidationMessageResolver;
3805
+ readonly children?: ReactNode;
3806
+ }) => ReactElement;
3434
3807
 
3435
3808
  export declare interface ValidationResult {
3436
3809
  readonly valid: boolean;
3810
+ /** Display text per field path — what `<AutoForm>` and custom widgets
3811
+ * render inline. First issue per field wins. */
3437
3812
  readonly errors: FieldErrors;
3813
+ /** Every issue, structured — for widget kits that translate themselves. */
3814
+ readonly issues: ReadonlyArray<FieldIssue>;
3438
3815
  }
3439
3816
 
3440
3817
  export declare type ValidationStatus = 'idle' | 'checking' | 'valid' | 'invalid';
@@ -3458,7 +3835,7 @@ export declare const validationStatus: (args: {
3458
3835
  /** The default widget kinds the framework can render from a schema. `'custom'`
3459
3836
  * means "no default widget" — the field needs a render-prop (a nested object,
3460
3837
  * an array of objects, or a multi-branch union). Mirrors innovation/01's set. */
3461
- export declare type WidgetKind = 'text' | 'textarea' | 'number' | 'checkbox' | 'switch' | 'select' | 'radio' | 'async-select' | 'multi-select' | 'date' | 'datetime' | 'daterange' | 'file' | 'hidden' | 'custom';
3838
+ export declare type WidgetKind = 'text' | 'textarea' | 'number' | 'checkbox' | 'switch' | 'select' | 'radio' | 'async-select' | 'multi-select' | 'date' | 'datetime' | 'daterange' | 'file' | 'hidden' | 'array' | 'reference' | 'custom';
3462
3839
 
3463
3840
  export declare interface WindowedSubscriptionState<Row> {
3464
3841
  /** The in-window rows (live). */