@voltro/client 0.51.0 → 0.53.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;
@@ -961,24 +1030,220 @@ export declare type FilterKind = 'text' | 'select' | 'multi-select' | 'number-ra
961
1030
  * input field → one FilterDescriptor. Pure. */
962
1031
  export declare const filtersFromSchema: (schema: Schema.Schema.Any) => ReadonlyArray<FilterDescriptor>;
963
1032
 
1033
+ /** DOM id of the JSON script the 422 re-render embeds (survives the
1034
+ * `interactive:'none'` strip — it is neither `type="module"` nor `src`). */
1035
+ export declare const FORM_FLASH_SCRIPT_ID = "__voltro_form_flash__";
1036
+
1037
+ /** Hidden field carrying the form's instance key (multi-form pages). */
1038
+ export declare const FORM_KEY_FIELD = "__voltro_form";
1039
+
1040
+ /** URL prefix the web listener mounts the no-JS endpoint under. */
1041
+ export declare const FORM_PATH_PREFIX = "/form/";
1042
+
1043
+ /** Hidden field carrying the declared success-redirect path. */
1044
+ export declare const FORM_REDIRECT_FIELD = "__voltro_redirect";
1045
+
1046
+ /** The path a native `<AutoForm>` POST goes to. */
1047
+ export declare const formActionPath: (mutationTag: string) => string;
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
+
964
1061
  export declare interface FormBinding<Input, Output> {
965
- /** 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`). */
966
1065
  readonly fields: ReadonlyArray<FieldDescriptor>;
967
1066
  readonly values: Partial<Input>;
968
- /** 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. */
969
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>;
970
1074
  /** True when the CURRENT values decode against the schema. */
971
1075
  readonly isValid: boolean;
972
1076
  readonly pending: boolean;
973
- /** 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. */
974
1079
  readonly submitError: unknown | undefined;
1080
+ /** A form-level (non-field) error carried in from a failed no-JS POST —
1081
+ * the RPC refused after valid input. Cleared by the next submit/reset. */
1082
+ readonly formError: string | undefined;
975
1083
  readonly data: Output | undefined;
976
1084
  readonly setValue: (name: string, value: unknown) => void;
977
1085
  readonly setValues: (patch: Partial<Input>) => void;
978
- readonly reset: () => void;
979
- /** Validate; if valid, run the mutation. Returns the output, or `undefined`
980
- * 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. */
981
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;
1102
+ }
1103
+
1104
+ /**
1105
+ * Convert posted form entries into the input object the mutation's schema
1106
+ * decodes — the shape `validateFields` (and the RPC payload) expects.
1107
+ *
1108
+ * Mapping rules (the documented contract):
1109
+ * - **checkbox/switch**: present → `true`, absent → `false`. A native POST
1110
+ * omits an unchecked checkbox entirely, so absence is data, not a gap.
1111
+ * - **number**: `Number(raw)`; `''` → the field is omitted (`undefined`), so
1112
+ * an optional number stays absent and a required one reports "missing"
1113
+ * instead of silently becoming `0`.
1114
+ * - **date/datetime**: the string passes through (the schema's encoded side);
1115
+ * `''` → omitted, same reasoning as number.
1116
+ * - **arrays** (multi-select): `getAll` semantics — every entry under the key,
1117
+ * items converted per the array's item type (number items → `Number`).
1118
+ * - **unknown keys are dropped**: only names the schema declares are mapped,
1119
+ * so protocol fields (`__voltro_form`, …) never reach the input object —
1120
+ * the server rejects undeclared fields, and a hidden bookkeeping field must
1121
+ * not turn every no-JS submit into a validation error.
1122
+ */
1123
+ export declare const formDataToInput: (schema: Schema.Schema.Any, entries: Iterable<FormEntry>) => Record<string, unknown>;
1124
+
1125
+ /** One posted entry. File entries must be filtered out by the caller —
1126
+ * uploads are a declared limit of the no-JS path. */
1127
+ export declare type FormEntry = readonly [name: string, value: string];
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
+
1180
+ /**
1181
+ * The error/values payload a failed no-JS form POST carries back into the
1182
+ * 422 re-render. Keyed by `formKey` so a page with several forms re-fills
1183
+ * only the one that was submitted. The SSR render passes it through the
1184
+ * server-request context; the client reads the same payload back off the
1185
+ * `#__voltro_form_flash__` JSON script, so a late-hydrating page shows the
1186
+ * identical state (no mismatch, no vanished errors).
1187
+ */
1188
+ export declare interface FormFlashPayload {
1189
+ /** The submitted form's key — `<AutoForm formKey>` or its mutation tag. */
1190
+ readonly formKey: string;
1191
+ /** The mapped input values as posted (pre-decode), to re-fill the fields. */
1192
+ readonly values: Readonly<Record<string, unknown>>;
1193
+ /** First error per field — the `validateFields` shape. */
1194
+ readonly errors: Readonly<Record<string, string>>;
1195
+ /** A non-field error (the RPC refused after valid input: guard, server). */
1196
+ readonly formError?: string;
1197
+ }
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;
982
1247
  }
983
1248
 
984
1249
  export declare interface FrameworkRuntimes {
@@ -1010,6 +1275,32 @@ export declare const getMutationNotifier: () => MutationNotifier | undefined;
1010
1275
  /** Read the current feed. Newest-first; up to MAX_MUTATIONS entries. */
1011
1276
  export declare const getMutations: () => ReadonlyArray<MutationEvent>;
1012
1277
 
1278
+ /**
1279
+ * A process-wide singleton React context, pinned on the global symbol registry.
1280
+ *
1281
+ * WHY this exists: the SSG prerender (`voltro build`) loads the framework
1282
+ * through TWO separate module instances — the renderer comes in via Vite's
1283
+ * `ssrLoadModule('@voltro/web/ssr')` (Vite-transformed) while a page's own
1284
+ * framework import is externalised to Node — so a plain `createContext()`
1285
+ * in a shared module is evaluated twice and yields two DISTINCT context
1286
+ * objects. The `<Router>` provider then holds one while the page's
1287
+ * `useLocation()` reads the other, which throws "Router hooks must be used
1288
+ * inside <Router>." The same duplicate-instance hazard shows up wherever a
1289
+ * deployment ends up with two copies of a package (the classic dual-package
1290
+ * hazard, monorepo hoisting quirks, separate SSR vs client bundles).
1291
+ *
1292
+ * Resolving every context through `Symbol.for(...)` on `globalThis` makes all
1293
+ * module copies share ONE instance, so provider and consumer can never diverge.
1294
+ * A duplicated copy of THIS helper is harmless — both copies hit the same
1295
+ * global registry entry.
1296
+ *
1297
+ * Lives in @voltro/client (the lowest React-carrying package) so both
1298
+ * @voltro/web and @voltro/ui reach it without a cycle; the key prefix keeps
1299
+ * its historical spelling on purpose — a mixed dist/src world must resolve
1300
+ * to the same registry entries.
1301
+ */
1302
+ export declare const globalContext: <T>(key: string, initial: T) => Context<T>;
1303
+
1013
1304
  /** Is this the framework's `Unauthenticated`? Matched on `_tag`, the wire
1014
1305
  * contract, rather than on an instance — the error crosses a package boundary
1015
1306
  * and may be re-created by the decoder. */
@@ -1263,6 +1554,9 @@ export declare interface OutboxControls {
1263
1554
  readonly online: boolean;
1264
1555
  readonly pending: number;
1265
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;
1266
1560
  }
1267
1561
 
1268
1562
  export declare interface OutboxEntry {
@@ -1274,6 +1568,22 @@ export declare interface OutboxEntry {
1274
1568
  readonly error?: unknown;
1275
1569
  }
1276
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
+
1277
1587
  export declare type OutboxStatus = 'pending' | 'sent' | 'failed' | 'conflict';
1278
1588
 
1279
1589
  /** Provide the current subject's scopes to `useCan`. Mount it once high in the
@@ -1575,6 +1885,14 @@ export declare type ResolvableHeaders = Readonly<Record<string, string>> | (() =
1575
1885
 
1576
1886
  export declare const resolveByTag: (client: unknown, tag: string) => unknown;
1577
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
+
1578
1896
  export declare interface ResolvedClient {
1579
1897
  readonly runtime: AnyRuntime;
1580
1898
  readonly cache: SubscriptionCache;
@@ -1587,6 +1905,13 @@ export declare const resolveStoreStorage: (area: StoreStorageArea) => StoreStora
1587
1905
  /** Resolve one map entry against the props. Pure. */
1588
1906
  export declare const resolveTrackingEvent: <P>(entry: TrackingEntry<P> | undefined, props: P) => TrackingEvent | null;
1589
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
+
1590
1915
  export declare interface ResourceCanInput {
1591
1916
  /** The action being gated (e.g. `'read'`, `'write'`, `'delete'`). */
1592
1917
  readonly action: string;
@@ -1832,6 +2157,29 @@ export declare type SequenceResult<Ctx> = {
1832
2157
  }>;
1833
2158
  };
1834
2159
 
2160
+ export declare const ServerRequestContext: Context<ServerRequestContextValue | null>;
2161
+
2162
+ export declare interface ServerRequestContextValue {
2163
+ readonly cookies: Readonly<Record<string, string>>;
2164
+ readonly headers: Readonly<Record<string, string>>;
2165
+ /** Raw request URL as it came off the wire (path + query). Useful
2166
+ * for SSR pages that need to read `?q=…` style search params
2167
+ * without touching anything client-only. Empty string for build-
2168
+ * time SSG renders where there is no incoming request. */
2169
+ readonly url: string;
2170
+ /** Present ONLY on the 422 re-render of a failed no-JS form POST:
2171
+ * the submitted values + field errors, keyed by formKey. Travels on
2172
+ * the EXISTING request context deliberately — a dedicated provider
2173
+ * would add a fiber fork the client boot does not have, and every
2174
+ * ancestor arity difference shifts every useId in the app. */
2175
+ readonly formFlash?: FormFlashPayload;
2176
+ }
2177
+
2178
+ export declare const ServerRequestProvider: ({ value, children, }: {
2179
+ readonly value: ServerRequestContextValue;
2180
+ readonly children: ReactNode;
2181
+ }) => ReactNode;
2182
+
1835
2183
  /** Register (or clear) the app-wide notifier that `notify:` routes to. Call once
1836
2184
  * at boot, next to your toast provider. */
1837
2185
  export declare const setMutationNotifier: (notifier: MutationNotifier | undefined) => void;
@@ -2168,6 +2516,8 @@ export declare class SubscriptionCache {
2168
2516
  private readonly inactiveTtlMs;
2169
2517
  private readonly onChangeHook;
2170
2518
  private readonly errorBus;
2519
+ private readonly mirror;
2520
+ private readonly mergeCell;
2171
2521
  constructor(options?: SubscriptionCacheOptions);
2172
2522
  /**
2173
2523
  * Register a subscriber. The cache forks the underlying fiber on the
@@ -2383,6 +2733,17 @@ export declare class SubscriptionCache {
2383
2733
  }
2384
2734
 
2385
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;
2386
2747
  /** Milliseconds to keep an entry alive after the last subscriber leaves.
2387
2748
  * Lets back-button / quick remount reuse the warm subscription. */
2388
2749
  readonly inactiveTtlMs?: number;
@@ -2476,6 +2837,29 @@ export declare interface SubscriptionMeta {
2476
2837
  readonly pendingPatches: number;
2477
2838
  }
2478
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
+
2479
2863
  /**
2480
2864
  * Subscribe to a streaming rpc by tag.
2481
2865
  *
@@ -3048,8 +3432,107 @@ export declare interface UseFormBindingOptions<Input> {
3048
3432
  readonly schema?: Schema.Schema.Any;
3049
3433
  /** Initial field values. */
3050
3434
  readonly defaults?: Partial<Input>;
3435
+ /** The failed no-JS POST's payload for THIS form (`useFormFlash` from
3436
+ * @voltro/web resolves it, SSR and client alike). When present it seeds
3437
+ * the initial values + field errors, so the 422 re-render shows the
3438
+ * submitted state server-side and hydrates to the identical state. */
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';
3051
3506
  }
3052
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
+
3522
+ /**
3523
+ * The failed-no-JS-POST payload for ONE form, or `undefined`.
3524
+ *
3525
+ * On the server (the 422 re-render) it comes off the request context; on the
3526
+ * client it is read back from the `#__voltro_form_flash__` JSON script the
3527
+ * same response embedded. Both carry the SAME payload, so a page that loads
3528
+ * its bundle after a native submit hydrates to exactly the server-rendered
3529
+ * state — errors visible, values filled, no mismatch.
3530
+ *
3531
+ * Keyed: only the form whose `formKey` was submitted receives the payload —
3532
+ * two `<AutoForm>`s on one page re-fill only the one that POSTed.
3533
+ */
3534
+ export declare const useFormFlash: (formKey: string) => FormFlashPayload | undefined;
3535
+
3053
3536
  /** The field shape a `<AutoForm mutation=…>` will render — from the mutation's
3054
3537
  * input Schema. Use it to render a matching skeleton while anything the form
3055
3538
  * depends on is still loading. */
@@ -3074,6 +3557,8 @@ export declare const useOutbox: (options: UseOutboxOptions) => OutboxControls;
3074
3557
  export declare interface UseOutboxOptions {
3075
3558
  /** Perform one queued write (the real mutation). Throw to fail. */
3076
3559
  readonly send: (entry: OutboxEntry) => Promise<void>;
3560
+ /** Durable backing (see {@link OutboxPersistence}). Omitted → in-memory only. */
3561
+ readonly persistence?: OutboxPersistence;
3077
3562
  /** Online override (defaults to navigator.onLine + online/offline events). */
3078
3563
  readonly online?: boolean;
3079
3564
  /** Classify a send error as a conflict (blocks replay) vs a transient
@@ -3198,6 +3683,8 @@ export declare interface UseSequenceResult {
3198
3683
  readonly failedStep: string | undefined;
3199
3684
  }
3200
3685
 
3686
+ export declare const useServerRequest: () => ServerRequestContextValue | null;
3687
+
3201
3688
  export declare function useSubscription<T = unknown>(apiName: string, rpcTag: string, input: Readonly<Record<string, unknown>>, options: SubscriptionOptions<T> & {
3202
3689
  readonly initialSnapshot: T;
3203
3690
  }): SubscriptionStateWithFallback<T>;
@@ -3245,6 +3732,10 @@ export declare interface UseUndoOptions {
3245
3732
  */
3246
3733
  export declare const useUpload: (apiName: string, hookOpts?: UploadOptions) => UploadHandle;
3247
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
+
3248
3739
  export declare const useWindowedSubscription: <Row = Record<string, unknown>>(apiName: string, queryTag: string, options: UseWindowedSubscriptionOptions) => WindowedSubscriptionState<Row>;
3249
3740
 
3250
3741
  export declare interface UseWindowedSubscriptionOptions {
@@ -3288,20 +3779,39 @@ export declare const useWorkflowUpdate: (apiName: string) => WorkflowUpdateState
3288
3779
  export declare const validateEventPayload: (descriptor: EventDescriptor<string, unknown>, payload: unknown) => EventValidation;
3289
3780
 
3290
3781
  /**
3291
- * Validate `values` against an input `schema`, returning the first error per
3292
- * top-level field (`{ [field]: message }`) the shape `<AutoForm>` and custom
3293
- * widgets render inline. Uses the same `decodeUnknownEither` + `ArrayFormatter`
3294
- * path the store's `.validate(...)` uses, so client and server speak ONE
3295
- * schema. `{ errors: 'all' }` collects every field's error in one pass.
3296
- *
3297
- * Client-side validation is non-authoritative UX — the server re-validates the
3298
- * 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.
3299
3787
  */
3300
- 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;
3301
3807
 
3302
3808
  export declare interface ValidationResult {
3303
3809
  readonly valid: boolean;
3810
+ /** Display text per field path — what `<AutoForm>` and custom widgets
3811
+ * render inline. First issue per field wins. */
3304
3812
  readonly errors: FieldErrors;
3813
+ /** Every issue, structured — for widget kits that translate themselves. */
3814
+ readonly issues: ReadonlyArray<FieldIssue>;
3305
3815
  }
3306
3816
 
3307
3817
  export declare type ValidationStatus = 'idle' | 'checking' | 'valid' | 'invalid';
@@ -3325,7 +3835,7 @@ export declare const validationStatus: (args: {
3325
3835
  /** The default widget kinds the framework can render from a schema. `'custom'`
3326
3836
  * means "no default widget" — the field needs a render-prop (a nested object,
3327
3837
  * an array of objects, or a multi-branch union). Mirrors innovation/01's set. */
3328
- 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';
3329
3839
 
3330
3840
  export declare interface WindowedSubscriptionState<Row> {
3331
3841
  /** The in-window rows (live). */