attaform 0.18.1 → 0.19.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.
Files changed (71) hide show
  1. package/README.md +3 -0
  2. package/dist/chunks/devtools.cjs +1 -1
  3. package/dist/chunks/devtools.mjs +1 -1
  4. package/dist/chunks/indexeddb.cjs +1 -1
  5. package/dist/chunks/indexeddb.mjs +1 -1
  6. package/dist/chunks/local-storage.cjs +1 -1
  7. package/dist/chunks/local-storage.mjs +1 -1
  8. package/dist/chunks/session-storage.cjs +1 -1
  9. package/dist/chunks/session-storage.mjs +1 -1
  10. package/dist/index.cjs +4 -4
  11. package/dist/index.d.cts +68 -75
  12. package/dist/index.d.mts +68 -75
  13. package/dist/index.d.ts +68 -75
  14. package/dist/index.mjs +5 -5
  15. package/dist/nuxt.d.cts +1 -1
  16. package/dist/nuxt.d.mts +1 -1
  17. package/dist/nuxt.d.ts +1 -1
  18. package/dist/runtime/plugins/attaform.cjs +2 -2
  19. package/dist/runtime/plugins/attaform.mjs +2 -2
  20. package/dist/shared/{attaform.DsC3rZHG.mjs → attaform.BTi-PsHr.mjs} +544 -134
  21. package/dist/shared/attaform.BTi-PsHr.mjs.map +1 -0
  22. package/dist/shared/{attaform.iTqxvl-P.d.mts → attaform.BTpuvGec.d.ts} +46 -13
  23. package/dist/shared/{attaform.BqK_L4gK.cjs → attaform.BqEfHpVB.cjs} +119 -1
  24. package/dist/shared/attaform.BqEfHpVB.cjs.map +1 -0
  25. package/dist/shared/{attaform.DK9aj0N8.d.ts → attaform.BtBmfLQN.d.mts} +46 -13
  26. package/dist/shared/{attaform.Dj9mwbaV.d.mts → attaform.C0uGZQ4M.d.cts} +365 -86
  27. package/dist/shared/{attaform.Dj9mwbaV.d.ts → attaform.C0uGZQ4M.d.mts} +365 -86
  28. package/dist/shared/{attaform.Dj9mwbaV.d.cts → attaform.C0uGZQ4M.d.ts} +365 -86
  29. package/dist/shared/{attaform.II89Pcf4.cjs → attaform.C1msmO2v.cjs} +544 -134
  30. package/dist/shared/attaform.C1msmO2v.cjs.map +1 -0
  31. package/dist/shared/{attaform.tts_OM7j.d.cts → attaform.CBjmobqk.d.cts} +1 -1
  32. package/dist/shared/{attaform.2b7M2mww.d.mts → attaform.CJ-e9gYI.d.ts} +1 -1
  33. package/dist/shared/{attaform.tsNFcEW7.d.ts → attaform.CRNA0vrd.d.mts} +1 -1
  34. package/dist/shared/{attaform.BDdFdjeX.mjs → attaform.Cghpuav8.mjs} +3 -3
  35. package/dist/shared/{attaform.BDdFdjeX.mjs.map → attaform.Cghpuav8.mjs.map} +1 -1
  36. package/dist/shared/{attaform.CtNUB9nf.mjs → attaform.CiMqJHDm.mjs} +3 -3
  37. package/dist/shared/{attaform.CtNUB9nf.mjs.map → attaform.CiMqJHDm.mjs.map} +1 -1
  38. package/dist/shared/{attaform.5UhpSVFI.cjs → attaform.CoxJ8Qm8.cjs} +2 -2
  39. package/dist/shared/{attaform.5UhpSVFI.cjs.map → attaform.CoxJ8Qm8.cjs.map} +1 -1
  40. package/dist/shared/{attaform.Xhg0AYNa.mjs → attaform.CrpjyXdO.mjs} +120 -2
  41. package/dist/shared/attaform.CrpjyXdO.mjs.map +1 -0
  42. package/dist/shared/{attaform.DF8wo-ry.d.ts → attaform.D4I63aBV.d.ts} +1 -1
  43. package/dist/shared/{attaform.DVLB6CAn.d.mts → attaform.DXYHL99q.d.mts} +1 -1
  44. package/dist/shared/{attaform.Dlk1jMuv.cjs → attaform.JBx8cfMA.cjs} +3 -3
  45. package/dist/shared/{attaform.Dlk1jMuv.cjs.map → attaform.JBx8cfMA.cjs.map} +1 -1
  46. package/dist/shared/{attaform.DUHru0OF.cjs → attaform.OznWyOPy.cjs} +3 -3
  47. package/dist/shared/{attaform.DUHru0OF.cjs.map → attaform.OznWyOPy.cjs.map} +1 -1
  48. package/dist/shared/{attaform.M33WKVV4.d.cts → attaform.QvygsFGh.d.cts} +1 -1
  49. package/dist/shared/{attaform.Xt0A3QUd.mjs → attaform.a3uBo-gw.mjs} +3 -3
  50. package/dist/shared/{attaform.Xt0A3QUd.mjs.map → attaform.a3uBo-gw.mjs.map} +1 -1
  51. package/dist/shared/{attaform.DoSuaKMd.d.cts → attaform.ePUcKxId.d.cts} +46 -13
  52. package/dist/zod-v3.cjs +3 -3
  53. package/dist/zod-v3.d.cts +4 -4
  54. package/dist/zod-v3.d.mts +4 -4
  55. package/dist/zod-v3.d.ts +4 -4
  56. package/dist/zod-v3.mjs +3 -3
  57. package/dist/zod-v4.cjs +3 -3
  58. package/dist/zod-v4.d.cts +4 -4
  59. package/dist/zod-v4.d.mts +4 -4
  60. package/dist/zod-v4.d.ts +4 -4
  61. package/dist/zod-v4.mjs +3 -3
  62. package/dist/zod.cjs +4 -4
  63. package/dist/zod.d.cts +6 -6
  64. package/dist/zod.d.mts +6 -6
  65. package/dist/zod.d.ts +6 -6
  66. package/dist/zod.mjs +5 -5
  67. package/package.json +5 -5
  68. package/dist/shared/attaform.BqK_L4gK.cjs.map +0 -1
  69. package/dist/shared/attaform.DsC3rZHG.mjs.map +0 -1
  70. package/dist/shared/attaform.II89Pcf4.cjs.map +0 -1
  71. package/dist/shared/attaform.Xhg0AYNa.mjs.map +0 -1
@@ -319,6 +319,26 @@ type ArrayPath<Form, P extends FlatPath<Form> = FlatPath<Form>> = P extends stri
319
319
  * constrain `Path extends ArrayPath<Form>` so this is always well-defined.
320
320
  */
321
321
  type ArrayItem<Form, Path extends ArrayPath<Form>> = NestedType<Form, Path> extends ReadonlyArray<infer Item> ? Item : never;
322
+ /**
323
+ * Companion to `ArrayPath`: filter `FlatPath<Form>` down to the subset
324
+ * of paths whose resolved leaf is a record (an object with an open
325
+ * string-keyed index signature, e.g. `z.record(z.string(), V)`). A
326
+ * fixed-shape object (`z.object({ ... })`) is excluded — its keys are
327
+ * statically known, so it has no `string` index signature.
328
+ *
329
+ * `string extends keyof T` is the index-signature probe: it holds for
330
+ * `Record<string, V>` (`keyof` is `string`) and fails for a fixed object
331
+ * (`keyof` is the literal key union). The leading array guard keeps
332
+ * arrays (which also satisfy the object check) out of the record set.
333
+ */
334
+ type RecordPath<Form, P extends FlatPath<Form> = FlatPath<Form>> = P extends string ? NestedType<Form, P> extends readonly unknown[] ? never : NestedType<Form, P> extends Record<string, unknown> ? string extends keyof NestedType<Form, P> ? P : never : never : never;
335
+ /**
336
+ * Value type of the record addressed by `Path` — the `V` in a
337
+ * `Record<string, V>`. Callers constrain `Path extends RecordPath<Form>`,
338
+ * so the leaf is always an open string-keyed record and this is
339
+ * well-defined.
340
+ */
341
+ type RecordValue<Form, Path extends RecordPath<Form>> = NestedType<Form, Path> extends Record<string, infer Value> ? Value : never;
322
342
  /**
323
343
  * Widens primitive-literal leaves to their primitive supertype to
324
344
  * match the runtime "slim-primitive write contract."
@@ -1308,20 +1328,24 @@ type WriteMeta = {
1308
1328
  */
1309
1329
  readonly skipDiscriminatorReshape?: boolean;
1310
1330
  /**
1311
- * Hint about an array structural mutation, set by `field-arrays.ts`
1312
- * helpers so `setValueAtPath` can surgically clear variant memory
1313
- * for indices the operation invalidated. Without this hint, a raw
1314
- * whole-array `setValue(arrayPath, [...])` clears all memory under
1315
- * the array (the runtime can't tell which indices stayed put).
1316
- * Internal don't set from consumer code.
1331
+ * Records an array structural mutation precisely enough to replay the
1332
+ * exact index permutation it produced, set by `field-arrays.ts`
1333
+ * helpers. `setValueAtPath` uses it to surgically clear variant memory
1334
+ * for the indices the operation invalidated. Without this hint, a raw
1335
+ * whole-array `setValue(arrayPath, [...])` clears all memory under the
1336
+ * array (the runtime can't tell which indices stayed put). Internal —
1337
+ * don't set from consumer code.
1317
1338
  */
1318
1339
  readonly arrayOp?: {
1319
- readonly kind: 'shift-from';
1340
+ readonly kind: 'insert';
1341
+ readonly index: number;
1342
+ } | {
1343
+ readonly kind: 'remove';
1320
1344
  readonly index: number;
1321
1345
  } | {
1322
- readonly kind: 'shift-range';
1323
- readonly fromIndex: number;
1324
- readonly toIndex: number;
1346
+ readonly kind: 'move';
1347
+ readonly from: number;
1348
+ readonly to: number;
1325
1349
  } | {
1326
1350
  readonly kind: 'swap';
1327
1351
  readonly a: number;
@@ -1736,16 +1760,14 @@ type UseFormConfiguration<Form extends GenericForm, GetValueFormType, Schema ext
1736
1760
  */
1737
1761
  coerce?: boolean | CoercionRegistry;
1738
1762
  /**
1739
- * Per-form override of the `shouldShowErrors` heuristic that drives
1740
- * `field.showErrors` and `form.meta.showErrors`. Falls back to
1741
- * `AttaformDefaults.shouldShowErrors`, then to the library default
1742
- * (`defaultShouldShowErrors`). See `AttaformDefaults.shouldShowErrors`
1743
- * for the resolution rules and predicate signature.
1744
- *
1745
- * Boolean shorthand: `true` → always show *when errors exist*;
1746
- * `false` → never show.
1763
+ * Per-form override of the `getDisplayState` heuristic that drives
1764
+ * `field.displayState` and the `show*` booleans (and their `form.meta`
1765
+ * rollups). Falls back to `AttaformDefaults.getDisplayState`, then to
1766
+ * the library default (`defaultDisplayState`). See
1767
+ * `AttaformDefaults.getDisplayState` for the resolution rules and
1768
+ * predicate signature.
1747
1769
  */
1748
- shouldShowErrors?: ShouldShowErrorsConfig;
1770
+ getDisplayState?: GetDisplayState;
1749
1771
  /**
1750
1772
  * Recursion ceiling for schema walks that descend through recursive
1751
1773
  * schemas (Zod's `z.lazy(...)` today). Default `64`. Per-form value
@@ -1818,6 +1840,20 @@ type UseFormConfiguration<Form extends GenericForm, GetValueFormType, Schema ext
1818
1840
  * channel would be solo by construction.
1819
1841
  */
1820
1842
  multiTab?: boolean;
1843
+ /**
1844
+ * Whether `v-register` automatically manages aria attributes
1845
+ * (`aria-invalid`, `aria-busy`, `aria-required`, `aria-describedby`)
1846
+ * from the field's display state. **Defaults to `true`.**
1847
+ *
1848
+ * **Resolution order (per-register override > per-form > global > library):**
1849
+ *
1850
+ * register(path, { autoAria }) > useForm({ autoAria }) > AttaformDefaults.autoAria > library default (`true`)
1851
+ *
1852
+ * Set `false` to leave all aria wiring to your own markup form-wide.
1853
+ * Any aria attribute you author yourself is always left untouched,
1854
+ * independent of this flag.
1855
+ */
1856
+ autoAria?: boolean;
1821
1857
  /**
1822
1858
  * @internal
1823
1859
  * SSR prefetch mark — set by the `attaform/vite` compile-time
@@ -1899,33 +1935,41 @@ type AttaformDefaults = {
1899
1935
  */
1900
1936
  coerce?: boolean | CoercionRegistry;
1901
1937
  /**
1902
- * Default for `useForm({ shouldShowErrors })`. Centralised heuristic
1903
- * that drives `field.showErrors` (and `form.meta.showErrors`)a
1904
- * boolean that gates whether a path's errors are *ready* to render.
1938
+ * Default for `useForm({ getDisplayState })`. The centralised
1939
+ * heuristic that resolves every path's `field.displayState` — and thus
1940
+ * the `show*` booleans and their `form.meta` rollups to one of
1941
+ * `'idle' | 'pending' | 'error' | 'success'`.
1905
1942
  *
1906
1943
  * Resolution order (per-form wins):
1907
1944
  *
1908
- * useForm({ shouldShowErrors }) > AttaformDefaults > library default
1945
+ * useForm({ getDisplayState }) > AttaformDefaults > library default
1909
1946
  *
1910
- * The library default reads "show after the first submit attempt OR
1911
- * after the field has been interacted with AND changed":
1947
+ * The library default opens one timing gate, then resolves by
1948
+ * precedence: gate closed `'idle'`; a run in flight → `'pending'`;
1949
+ * an own-path error → `'error'`; otherwise `valid` → `'success'`, else
1950
+ * `'idle'`. The gate opens after the first submit attempt OR once the
1951
+ * field is touched and not currently focused:
1912
1952
  *
1913
1953
  * ```ts
1914
- * (field, formMeta) =>
1915
- * formMeta.submissionAttempts > 0 || (field.touched === true && field.dirty)
1954
+ * (field, formMeta) => {
1955
+ * const gateOpen =
1956
+ * formMeta.submissionAttempts > 0 ||
1957
+ * (field.touched === true && field.focused !== true)
1958
+ * if (!gateOpen) return 'idle'
1959
+ * if (field.validating === true) return 'pending'
1960
+ * // ...own-path error → 'error'; valid → 'success'; else 'idle'
1961
+ * }
1916
1962
  * ```
1917
1963
  *
1918
- * Compose with the library default via the public
1919
- * `defaultShouldShowErrors` export. Boolean shorthand is supported:
1920
- * `true` always show *when errors exist*; `false` → never show. The
1921
- * predicate is invoked only when `errors.length > 0`, so authors
1922
- * don't re-check inside.
1964
+ * Compose with the library default via the public `defaultDisplayState`
1965
+ * export. The predicate runs on every field-state read, so it owns the
1966
+ * idle / pending / error / success decision outright.
1923
1967
  *
1924
- * The predicate's args are `Omit`'d of `showErrors` / `firstError`
1925
- * to prevent recursive predicates those are derived FROM this
1926
- * predicate, so reading them inside would be a self-reference.
1968
+ * The predicate's args are `Omit`'d of the derived `displayState` /
1969
+ * `show*` / `firstError` keys (see `FieldStateDerivedKey`) to prevent
1970
+ * a self-referential predicate.
1927
1971
  */
1928
- shouldShowErrors?: ShouldShowErrorsConfig;
1972
+ getDisplayState?: GetDisplayState;
1929
1973
  /**
1930
1974
  * Default for `useForm({ maxRecursionDepth })`. Recursion ceiling
1931
1975
  * for schema walks that descend through recursive schemas (Zod's
@@ -2025,6 +2069,21 @@ type AttaformDefaults = {
2025
2069
  * the multi-tab-sync recipe's Security section for the threat model.
2026
2070
  */
2027
2071
  multiTab?: boolean;
2072
+ /**
2073
+ * App-wide default for `useForm({ autoAria })`. Library default is
2074
+ * `true`: `v-register` keeps `aria-invalid` / `aria-busy` /
2075
+ * `aria-required` / `aria-describedby` in sync with each field's
2076
+ * display state out of the box.
2077
+ *
2078
+ * **Resolution order (per-form wins):**
2079
+ *
2080
+ * useForm({ autoAria }) > AttaformDefaults.autoAria > library default (`true`)
2081
+ *
2082
+ * Set `false` once at the plugin level to make every form manage its
2083
+ * own aria markup. Authored aria attributes are always preserved
2084
+ * regardless of this setting.
2085
+ */
2086
+ autoAria?: boolean;
2028
2087
  };
2029
2088
  /**
2030
2089
  * Callback invoked by `handleSubmit` after the form parses successfully.
@@ -2040,45 +2099,59 @@ type OnSubmit<Form extends GenericForm> = (form: Form) => void | Promise<void>;
2040
2099
  */
2041
2100
  type OnError = (error: ValidationError[]) => void | Promise<void>;
2042
2101
  /**
2043
- * Predicate that drives `field.showErrors` (and `form.meta.showErrors`).
2044
- * Receives the field's reactive state plus the form's reactive meta;
2045
- * returns `true` to render the field's errors, `false` to keep them
2046
- * hidden. The framework gates the call on `errors.length > 0`, so
2047
- * authors don't re-check error presence inside.
2048
- *
2049
- * Both arguments are `Omit`'d of `showErrors` / `firstError` those
2050
- * are derived FROM this predicate, so reading them inside would be a
2051
- * self-reference. The omit is enforced at the type level AND at
2052
- * runtime: the keys literally are not present on the objects passed
2053
- * in, so `as` casting in TS or vanilla-JS bypass cannot create a
2054
- * cycle.
2055
- *
2056
- * The library default `defaultShouldShowErrors` is publicly
2057
- * exported so a layered predicate can compose with it:
2102
+ * The display-state verdict at a path: the single signal a UI needs to
2103
+ * decide what (if anything) to surface about validation right now.
2104
+ * Rolled up at containers and at the form root (`form.meta.displayState`).
2105
+ *
2106
+ * - `'idle'` nothing to surface. Either pre-interaction (the timing
2107
+ * gate hasn't opened) or gate-open with no verdict worth showing.
2108
+ * - `'pending'` a validation run is in flight at this path; the prior
2109
+ * verdict is stale. Drive a spinner / "Checking…" affordance.
2110
+ * - `'error'` a blocking error the timing gate has cleared for display.
2111
+ * - `'success'` validation passed and the gate has cleared a positive
2112
+ * confirmation (the green-check pattern).
2113
+ *
2114
+ * The four `show*` booleans on `FieldState` are sugar over this enum
2115
+ * (`showErrors === (displayState === 'error')`, and so on), so they can
2116
+ * never contradict it.
2117
+ */
2118
+ type DisplayState = 'idle' | 'pending' | 'error' | 'success';
2119
+ /**
2120
+ * Keys on `FieldState` layered on FROM the display-state predicate
2121
+ * (plus `firstError`, computed alongside them). `Omit`'d from the
2122
+ * predicate's arguments so a predicate cannot read its own output and
2123
+ * form a cycle — enforced at the type level AND at runtime: the base
2124
+ * objects passed in literally lack these keys, so an `as` cast in TS
2125
+ * or a vanilla-JS caller still can't reach them. `FieldStateBase` /
2126
+ * `FormMetaBase` (field-state-api.ts) omit the same set in lockstep.
2127
+ */
2128
+ type FieldStateDerivedKey = 'displayState' | 'showErrors' | 'showPending' | 'showSuccess' | 'showIdle' | 'firstError';
2129
+ /**
2130
+ * Predicate that resolves a path's `displayState`. Receives the field's
2131
+ * reactive state plus the form's reactive meta (both minus the derived
2132
+ * `displayState` / `show*` / `firstError` keys — see `FieldStateDerivedKey`)
2133
+ * and returns the single enum verdict; the `show*` booleans derive from
2134
+ * the result. Runs unconditionally on every field-state read, so the
2135
+ * idle / pending / error / success decision lives in exactly one place
2136
+ * and the whole app's validation-display behavior flows from it.
2137
+ *
2138
+ * The library default — `defaultDisplayState` — is publicly exported so
2139
+ * a layered predicate can compose with it:
2058
2140
  *
2059
2141
  * ```ts
2060
- * import { defaultShouldShowErrors } from 'attaform'
2142
+ * import { defaultDisplayState } from 'attaform'
2061
2143
  *
2062
2144
  * useForm({
2063
2145
  * schema,
2064
- * shouldShowErrors: (field, formMeta) =>
2065
- * field.path[0] === 'urgent' || defaultShouldShowErrors(field, formMeta),
2146
+ * // Defer to the default everywhere, but never show a success check on `username`.
2147
+ * getDisplayState: (field, formMeta) => {
2148
+ * const state = defaultDisplayState(field, formMeta)
2149
+ * return field.path[0] === 'username' && state === 'success' ? 'idle' : state
2150
+ * },
2066
2151
  * })
2067
2152
  * ```
2068
2153
  */
2069
- type ShouldShowErrors = (field: Omit<FieldState, 'showErrors' | 'firstError'>, formMeta: Omit<FormMeta, 'showErrors' | 'firstError'>) => boolean;
2070
- /**
2071
- * Configuration shape for `shouldShowErrors`. A predicate function or
2072
- * a boolean shorthand:
2073
- *
2074
- * - `true` — always show errors (when any exist).
2075
- * - `false` — never show errors.
2076
- * - function — custom predicate, see `ShouldShowErrors`.
2077
- *
2078
- * Resolved through three tiers (per-form > plugin defaults > library
2079
- * default).
2080
- */
2081
- type ShouldShowErrorsConfig = ShouldShowErrors | boolean;
2154
+ type GetDisplayState = (field: Omit<FieldState, FieldStateDerivedKey>, formMeta: Omit<FormMeta, FieldStateDerivedKey>) => DisplayState;
2082
2155
  /**
2083
2156
  * Submit handler returned by `handleSubmit(onSubmit, onError)`. Bind
2084
2157
  * it to a `<form>`:
@@ -2330,6 +2403,22 @@ type RegisterOptions = {
2330
2403
  * instead — see the "Custom assigners" section in the API docs.
2331
2404
  */
2332
2405
  transforms?: ReadonlyArray<RegisterTransform>;
2406
+ /**
2407
+ * Per-binding override for automatic aria management, the narrowest
2408
+ * tier of the `autoAria` cascade. By default the directive keeps
2409
+ * `aria-invalid` / `aria-busy` / `aria-required` / `aria-describedby`
2410
+ * in sync with the field's display state. Pass `autoAria: false` to
2411
+ * leave every aria attribute on this element to you (the directive
2412
+ * still manages value binding and registration), or `autoAria: true`
2413
+ * to re-enable management on one binding even when the form set
2414
+ * `useForm({ autoAria: false })`.
2415
+ *
2416
+ * Overrides `useForm({ autoAria })` and
2417
+ * `createAttaform({ defaults: { autoAria } })`. Writing an aria
2418
+ * attribute yourself also locks the directive out of that one
2419
+ * attribute, regardless of this flag.
2420
+ */
2421
+ autoAria?: boolean;
2333
2422
  };
2334
2423
  /**
2335
2424
  * The object returned by `form.register(path)`. Pass it to a native
@@ -2530,6 +2619,16 @@ type RegisterValue<Value = unknown> = Readonly<{
2530
2619
  * @internal
2531
2620
  */
2532
2621
  markBlank: () => boolean;
2622
+ /**
2623
+ * Flip this field's sticky `interacted` flag — the signal that the
2624
+ * user has issued at least one value edit here (an insert or a
2625
+ * delete). Called by the directive's input / change listeners on
2626
+ * every genuine user input; never by hydration or programmatic
2627
+ * writes. Idempotent (the store skips the write once set). Don't
2628
+ * call from consumer code.
2629
+ * @internal
2630
+ */
2631
+ markInteracted: () => void;
2533
2632
  /**
2534
2633
  * `true` when the schema's slim primitive set at this path includes
2535
2634
  * `'undefined'` — i.e. the leaf was declared `.optional()` (or as
@@ -2561,6 +2660,42 @@ type RegisterValue<Value = unknown> = Readonly<{
2561
2660
  * @internal
2562
2661
  */
2563
2662
  acceptsString: boolean;
2663
+ /**
2664
+ * The field's aria satellite ids, mirroring `FieldState.aria`. The
2665
+ * directive points `aria-describedby` at `errorId` while the field
2666
+ * is in its error state. Optional so hand-rolled `RegisterValue`
2667
+ * mocks don't have to declare it; the directive skips aria wiring
2668
+ * when absent.
2669
+ * @internal
2670
+ */
2671
+ aria?: {
2672
+ readonly errorId: string;
2673
+ readonly descriptionId: string;
2674
+ };
2675
+ /**
2676
+ * Whether the schema marks this path required, from
2677
+ * `schema.isRequiredAtPath(segments)`. Drives `aria-required`.
2678
+ * Optional for the same mock-tolerance reason as `aria`.
2679
+ * @internal
2680
+ */
2681
+ isRequired?: boolean;
2682
+ /**
2683
+ * Whether the directive should auto-manage aria attributes for this
2684
+ * binding. Resolves the per-register `autoAria` override against the
2685
+ * form-level value: `options.autoAria ?? formAutoAria`. The directive
2686
+ * treats an absent value as off.
2687
+ * @internal
2688
+ */
2689
+ ariaEnabled?: boolean;
2690
+ /**
2691
+ * The gated display-state verdict for this path, reusing the same
2692
+ * field-state identity as `form.fields`. The directive watches it to
2693
+ * keep `aria-invalid` / `aria-busy` / `aria-describedby` in lockstep
2694
+ * with the visible error state, even on async ticks with no parent
2695
+ * re-render. Optional; the directive skips aria wiring when absent.
2696
+ * @internal
2697
+ */
2698
+ ariaDisplayState?: Readonly<Ref<DisplayState>>;
2564
2699
  }>;
2565
2700
  /**
2566
2701
  * Custom assigner installed on an element via the directive's
@@ -2876,6 +3011,32 @@ type FieldState<Value = unknown> = {
2876
3011
  readonly focused: boolean | null;
2877
3012
  readonly blurred: boolean | null;
2878
3013
  readonly touched: boolean;
3014
+ /**
3015
+ * `true` once the user has issued at least one value edit on this
3016
+ * field through `v-register` (an insert or a delete), sticky
3017
+ * thereafter and preserved across disconnects. Distinct from
3018
+ * `dirty`: typing `"a"` then deleting it back to empty leaves the
3019
+ * field net-unchanged (`dirty: false`) yet `interacted: true`.
3020
+ * Distinct from `touched`: tabbing through a field without editing
3021
+ * flips `touched` but never `interacted`. Set only by user input,
3022
+ * never by hydration or programmatic `setValue`; cleared by
3023
+ * `form.reset()` / `form.resetField(path)`. Containers roll it up as
3024
+ * a disjunction (any descendant interacted).
3025
+ */
3026
+ readonly interacted: boolean;
3027
+ /**
3028
+ * `true` once the user has blurred this field after editing it: the
3029
+ * first time they edit a value and then leave. Sticky thereafter and
3030
+ * preserved across disconnects; a tab-through with no edit never sets
3031
+ * it (`interacted` is still false at that blur). It composes
3032
+ * `interacted` with the departure and drives the default display gate,
3033
+ * so errors reveal once the user finishes a pass and leaves, then stay
3034
+ * visible through a re-focus to be fixed live. Set only by user
3035
+ * input/blur, never by hydration or programmatic writes; cleared by
3036
+ * `form.reset()` / `form.resetField(path)`. Containers roll it up as a
3037
+ * disjunction.
3038
+ */
3039
+ readonly blurredAfterInteraction: boolean;
2879
3040
  readonly connected: boolean;
2880
3041
  /**
2881
3042
  * The first DOM element bound to this path via `v-register`, or
@@ -2932,9 +3093,29 @@ type FieldState<Value = unknown> = {
2932
3093
  */
2933
3094
  readonly valid: boolean;
2934
3095
  /**
2935
- * Centralised "should I render this field's errors right now?"
2936
- * gate. Wraps `errors.length > 0 && shouldShowErrors(field, formMeta)`
2937
- * so templates avoid re-spelling the heuristic at every error site:
3096
+ * The single display-state verdict at this path: `'idle'`,
3097
+ * `'pending'`, `'error'`, or `'success'`. The source of truth the
3098
+ * four `show*` booleans below derive from. Bind it directly when one
3099
+ * branch over the set reads cleaner than four flags:
3100
+ *
3101
+ * ```vue
3102
+ * <FieldStatusIcon :state="form.fields.email.displayState" />
3103
+ * ```
3104
+ *
3105
+ * Resolved by the `getDisplayState` heuristic:
3106
+ * `useForm({ getDisplayState })` →
3107
+ * `createAttaform({ defaults: { getDisplayState } })` → library
3108
+ * default (`defaultDisplayState`). Override per form, app-wide, or
3109
+ * compose with `defaultDisplayState` for a layered predicate.
3110
+ *
3111
+ * Available on container paths too: `form.fields.users[0].displayState`
3112
+ * rolls up over the row's descendants.
3113
+ */
3114
+ readonly displayState: DisplayState;
3115
+ /**
3116
+ * `displayState === 'error'`. The centralised "render this field's
3117
+ * errors right now?" gate, so templates avoid re-spelling the
3118
+ * heuristic at every error site:
2938
3119
  *
2939
3120
  * ```vue
2940
3121
  * <span v-if="form.fields.email.showErrors">
@@ -2942,20 +3123,29 @@ type FieldState<Value = unknown> = {
2942
3123
  * </span>
2943
3124
  * ```
2944
3125
  *
2945
- * The heuristic itself comes from `useForm({ shouldShowErrors })`
2946
- * `createAttaform({ defaults: { shouldShowErrors } })` library
2947
- * default (`defaultShouldShowErrors` show after first submit OR
2948
- * after touched-and-dirty). Override per form, app-wide, or
2949
- * compose with `defaultShouldShowErrors` for a layered predicate.
2950
- *
2951
- * Falls back to `false` whenever there are no errors — the gate
2952
- * skips the predicate entirely in that case.
2953
- *
2954
- * Available on container paths too: `form.fields.users[0].showErrors`
2955
- * aggregates over the row's descendants (any descendant with a
2956
- * qualifying error flips the container on).
3126
+ * Kept plural to match `errors` / `firstError`. On container paths it
3127
+ * rolls up over descendants (any descendant resolving to `'error'`
3128
+ * flips the container on).
2957
3129
  */
2958
3130
  readonly showErrors: boolean;
3131
+ /**
3132
+ * `displayState === 'pending'`. A per-field validation run is in
3133
+ * flight at this path and the prior verdict is stale; drive a spinner
3134
+ * or a "Checking…" affordance.
3135
+ */
3136
+ readonly showPending: boolean;
3137
+ /**
3138
+ * `displayState === 'success'`. Validation has passed and the timing
3139
+ * gate has cleared a positive confirmation; drive the green-check
3140
+ * pattern.
3141
+ */
3142
+ readonly showSuccess: boolean;
3143
+ /**
3144
+ * `displayState === 'idle'`. Nothing to surface yet — pre-interaction,
3145
+ * or gate-open with no verdict worth showing. Read it to suppress
3146
+ * helper text the moment any other signal takes over.
3147
+ */
3148
+ readonly showIdle: boolean;
2959
3149
  /**
2960
3150
  * The first `ValidationError` at this path in the deterministic
2961
3151
  * schema-declaration order — equivalent to `errors[0]`, exposed as
@@ -2977,6 +3167,53 @@ type FieldState<Value = unknown> = {
2977
3167
  */
2978
3168
  readonly firstError: ValidationError | undefined;
2979
3169
  readonly path: ReadonlyArray<string | number>;
3170
+ /**
3171
+ * Stable, SSR-safe DOM id for this field, unique across every mount
3172
+ * on the page. Derived from the form's key and this path, folded with
3173
+ * the form's per-mount `instanceId` so two simultaneous mounts of the
3174
+ * same keyed form never collide. Bind it to wire a label and its
3175
+ * input without inventing your own id:
3176
+ *
3177
+ * ```vue
3178
+ * <label :for="form.fields.email.id">Email</label>
3179
+ * <input :id="form.fields.email.id" v-register="form.register('email')" />
3180
+ * ```
3181
+ *
3182
+ * Treat as identity, not state: stable for the path across the form's
3183
+ * lifetime, opaque, not meant to be parsed.
3184
+ */
3185
+ readonly id: string;
3186
+ /**
3187
+ * Satellite ids derived from {@link id} for the elements that
3188
+ * describe this field. Wire them to an error node and a description
3189
+ * node so assistive tech announces them with the input. The
3190
+ * `v-register` directive points `aria-describedby` at `errorId`
3191
+ * automatically while the field is in its error state; you render the
3192
+ * matching element and id it:
3193
+ *
3194
+ * ```vue
3195
+ * <input v-register="form.register('email')" />
3196
+ * <span :id="form.fields.email.aria.errorId" v-if="form.fields.email.showErrors">
3197
+ * {{ form.fields.email.firstError?.message }}
3198
+ * </span>
3199
+ * ```
3200
+ *
3201
+ * `descriptionId` is for opt-in help text; chain it into your own
3202
+ * `aria-describedby` when you render a persistent description element.
3203
+ */
3204
+ readonly aria: {
3205
+ readonly errorId: string;
3206
+ readonly descriptionId: string;
3207
+ };
3208
+ /**
3209
+ * Stable identity for this field as an element of its parent array,
3210
+ * suitable as a Vue `:key` when iterating array elements. An allocated
3211
+ * token (not derived from the element's value) that follows the
3212
+ * element across inserts, removals, moves, and swaps, so a row keeps
3213
+ * its component instance across a reorder. Empty for fields that are
3214
+ * not array elements. Treat as opaque identity, not state.
3215
+ */
3216
+ readonly key: string;
2980
3217
  readonly blank: boolean;
2981
3218
  /**
2982
3219
  * Presentational label for this field. Resolves through the
@@ -3422,8 +3659,8 @@ type FormMeta<F = unknown> = FieldState<F> & {
3422
3659
  *
3423
3660
  * Pure introspection counter — useful for "this form has been
3424
3661
  * visited and left" UX (analytics, prior-step badges, layered
3425
- * `shouldShowErrors` predicates) but does NOT drive the library's
3426
- * default `shouldShowErrors` heuristic. The reveal-on-submit story
3662
+ * `getDisplayState` predicates) but does NOT drive the library's
3663
+ * default `getDisplayState` heuristic. The reveal-on-submit story
3427
3664
  * runs entirely through `submissionAttempts`, which
3428
3665
  * `wizard.handleSubmit` bumps on the active form at intermediate
3429
3666
  * steps and on every form at the final step.
@@ -4217,6 +4454,48 @@ type UseFormReturnType<Form extends GenericForm, GetValueFormType extends Generi
4217
4454
  move: <Path extends ArrayPath<Form>>(path: Path, from: number, to: number) => void;
4218
4455
  /** Replace the element at `index` with `value`. No-op when out of range. */
4219
4456
  replace: <Path extends ArrayPath<Form>>(path: Path, index: number, value: ArrayItem<Form, Path>) => void;
4457
+ /**
4458
+ * Read-only, reactive view of the array at `path` as one `FieldState`
4459
+ * per element, in index order. Each entry carries its element `key`,
4460
+ * an allocated identity token, so a `v-for` keyed by it keeps a row's
4461
+ * component instance across an insert, removal, move, or swap:
4462
+ *
4463
+ * ```vue
4464
+ * <div v-for="(row, i) in form.list('contacts')" :key="row.key">
4465
+ * <input v-register="form.register(`contacts.${i}.name`)" />
4466
+ * <p v-if="row.showErrors">{{ row.firstError?.message }}</p>
4467
+ * </div>
4468
+ * ```
4469
+ *
4470
+ * Entries are the same field states `form.fields` exposes, so reads
4471
+ * stay live. `form.fields(path)` remains the single aggregated
4472
+ * container for the whole array; `list` is the per-element view.
4473
+ * For a record, reach for `record`, which keys each entry by its own
4474
+ * key.
4475
+ */
4476
+ list: <Path extends ArrayPath<Form>>(path: Path) => readonly FieldState<ArrayItem<Form, Path>>[];
4477
+ /**
4478
+ * Read-only, reactive view of the record at `path` as one `FieldState`
4479
+ * per entry, keyed by the entry's own key. Where `list` hands back an
4480
+ * ordered array for an array path, `record` hands back a keyed object
4481
+ * for a record path, so you iterate it by key:
4482
+ *
4483
+ * ```vue
4484
+ * <div v-for="(field, key) in form.record('scoresByTeam')" :key="key">
4485
+ * <label>{{ key }}</label>
4486
+ * <input v-register="form.register(`scoresByTeam.${key}`)" />
4487
+ * <p v-if="field.showErrors">{{ field.firstError?.message }}</p>
4488
+ * </div>
4489
+ * ```
4490
+ *
4491
+ * Entries are the same field states `form.fields` exposes, so reads
4492
+ * stay live, and the keyed shape mirrors the record's own keys: an
4493
+ * entry appears once you write its key (`form.setValue`) and drops
4494
+ * when the key leaves. `form.fields(path)` remains the single
4495
+ * aggregated container for the whole record; `record` is the
4496
+ * per-entry view.
4497
+ */
4498
+ record: <Path extends RecordPath<Form>>(path: Path) => Readonly<Record<string, FieldState<RecordValue<Form, Path>>>>;
4220
4499
  /**
4221
4500
  * Read-only view of the form's blank path set. Reactive — Vue 3.5
4222
4501
  * tracks `.has()` / `for..of` / size accesses, so consumers can drive
@@ -4244,5 +4523,5 @@ type UseFormReturnType<Form extends GenericForm, GetValueFormType extends Generi
4244
4523
  blankPaths: ComputedRef<BlankPathsView>;
4245
4524
  };
4246
4525
 
4247
- export { ROOT_PATH as a2, ROOT_PATH_KEY as a3, canonicalizePath as aq, isPathPrefix as ar, isUnset as as, parseDottedPath as at, unset as au };
4248
- export type { PersistConfigOptions as $, AttaformDefaults as A, IsUnion as B, CoercionEntry as C, DefaultValuesInput as D, ErrorsProxyShape as E, FormKey as F, GenericForm as G, HandleSubmit as H, IsTuple as I, JoinSegments as J, KeyofUnion as K, LiftedValueShape as L, MetaTrackerValue as M, NestedReadType as N, NestedType as O, OnError as P, OnInvalidSubmitPolicy as Q, RegisterValue as R, Segment as S, OnSubmit as T, UseFormConfiguration as U, ValidationError as V, PartialFlatPath as W, Path as X, PathKey as Y, PendingValidationStatus as Z, PersistConfig as _, AbstractSchema as a, PersistIncludeMode as a0, Primitive as a1, ReactiveValidationStatus as a4, RegisterDirective as a5, RegisterFlatPath as a6, RegisterOptions as a7, RegisterSelectModifier as a8, RegisterTextModifier as a9, RegisterTransform as aa, SetValueCallback as ab, SetValuePayload as ac, SettledValidationStatus as ad, ShouldShowErrorsConfig as ae, SlimPrimitiveKind as af, SlimRuntimeOf as ag, SubmitHandler as ah, Unset as ai, ValidateOn as aj, ValidateOnConfig as ak, ValidationResponse as al, ValidationResponseWithoutValue as am, ValueOfUnion as an, WriteMeta as ao, WriteShape as ap, SchemaFactoryOptions as av, PersistOptInRegistry as aw, UseFormReturnType as b, RegisterModelDynamicCustomDirective as c, ShouldShowErrors as d, ApiErrorEnvelope as e, ApiErrorDetails as f, ApiErrorEntry as g, ArrayItem as h, ArrayPath as i, CoercionRegistry as j, CoercionResult as k, CustomDirectiveRegisterAssignerFn as l, DeepPartial as m, DefaultValuesResponse as n, DefaultValuesShape as o, FieldMetaPayload as p, FieldState as q, FieldStateMap as r, FieldStateMapEntry as s, FlatPath as t, FormErrorRecord as u, FormErrorsSurface as v, FormMeta as w, FormStorage as x, FormStorageKind as y, HistoryConfig as z };
4526
+ export { ROOT_PATH_KEY as $, ROOT_PATH as _, canonicalizePath as as, isPathPrefix as at, isUnset as au, parseDottedPath as av, unset as aw };
4527
+ export type { AbstractSchema as A, NestedType as B, CoercionEntry as C, DeepPartial as D, ErrorsProxyShape as E, FieldMetaPayload as F, GenericForm as G, HandleSubmit as H, IsTuple as I, JoinSegments as J, KeyofUnion as K, LiftedValueShape as L, MetaTrackerValue as M, NestedReadType as N, OnError as O, OnInvalidSubmitPolicy as P, OnSubmit as Q, PartialFlatPath as R, Path as S, PathKey as T, PendingValidationStatus as U, PersistConfig as V, PersistConfigOptions as W, PersistIncludeMode as X, PersistOptInRegistry as Y, Primitive as Z, ApiErrorDetails as a, ReactiveValidationStatus as a0, RegisterDirective as a1, RegisterFlatPath as a2, RegisterModelDynamicCustomDirective as a3, RegisterOptions as a4, RegisterSelectModifier as a5, RegisterTextModifier as a6, RegisterTransform as a7, RegisterValue as a8, SchemaFactoryOptions as a9, Segment as aa, SetValueCallback as ab, SetValuePayload as ac, SettledValidationStatus as ad, SlimPrimitiveKind as ae, SlimRuntimeOf as af, SubmitHandler as ag, Unset as ah, UseFormConfiguration as ai, UseFormReturnType as aj, ValidateOn as ak, ValidateOnConfig as al, ValidationError as am, ValidationResponse as an, ValidationResponseWithoutValue as ao, ValueOfUnion as ap, WriteMeta as aq, WriteShape as ar, ApiErrorEntry as b, ApiErrorEnvelope as c, ArrayItem as d, ArrayPath as e, AttaformDefaults as f, CoercionRegistry as g, CoercionResult as h, CustomDirectiveRegisterAssignerFn as i, DefaultValuesInput as j, DefaultValuesResponse as k, DefaultValuesShape as l, DisplayState as m, FieldState as n, FieldStateMap as o, FieldStateMapEntry as p, FlatPath as q, FormErrorRecord as r, FormErrorsSurface as s, FormKey as t, FormMeta as u, FormStorage as v, FormStorageKind as w, GetDisplayState as x, HistoryConfig as y, IsUnion as z };