attaform 0.16.2 → 0.16.4

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 (67) hide show
  1. package/README.md +7 -7
  2. package/dist/chunks/devtools.cjs +1 -1
  3. package/dist/chunks/devtools.mjs +1 -1
  4. package/dist/index.cjs +3 -2
  5. package/dist/index.cjs.map +1 -1
  6. package/dist/index.d.cts +33 -4
  7. package/dist/index.d.mts +33 -4
  8. package/dist/index.d.ts +33 -4
  9. package/dist/index.mjs +3 -3
  10. package/dist/nuxt.d.cts +1 -1
  11. package/dist/nuxt.d.mts +1 -1
  12. package/dist/nuxt.d.ts +1 -1
  13. package/dist/shared/attaform.C8CyvYa_.cjs +36 -0
  14. package/dist/shared/attaform.C8CyvYa_.cjs.map +1 -0
  15. package/dist/shared/{attaform.CWCx2r0x.d.ts → attaform.CCQkY4Ta.d.ts} +1 -1
  16. package/dist/shared/{attaform.c_NzdRyc.cjs → attaform.CIwZtbGV.cjs} +6 -2
  17. package/dist/shared/attaform.CIwZtbGV.cjs.map +1 -0
  18. package/dist/shared/{attaform.CVv9Oh0a.d.mts → attaform.CMRmwGDt.d.cts} +1 -1
  19. package/dist/shared/{attaform.0Gxd_OOx.d.cts → attaform.CU3JperC.d.cts} +193 -27
  20. package/dist/shared/{attaform.0Gxd_OOx.d.mts → attaform.CU3JperC.d.mts} +193 -27
  21. package/dist/shared/{attaform.0Gxd_OOx.d.ts → attaform.CU3JperC.d.ts} +193 -27
  22. package/dist/shared/{attaform.Dq5BabH1.d.cts → attaform.CXMOheyZ.d.mts} +1 -1
  23. package/dist/shared/attaform.D13GMFgK.mjs +32 -0
  24. package/dist/shared/attaform.D13GMFgK.mjs.map +1 -0
  25. package/dist/shared/{attaform.jrxE_xZw.mjs → attaform.DZRj9s0s.mjs} +5 -3
  26. package/dist/shared/attaform.DZRj9s0s.mjs.map +1 -0
  27. package/dist/shared/{attaform.Bp1c-uGF.cjs → attaform.Dd_pWnmn.cjs} +17 -29
  28. package/dist/shared/attaform.Dd_pWnmn.cjs.map +1 -0
  29. package/dist/shared/{attaform.DILbdvfo.mjs → attaform.DyV1O4tI.mjs} +111 -22
  30. package/dist/shared/attaform.DyV1O4tI.mjs.map +1 -0
  31. package/dist/shared/{attaform.DdnithOf.mjs → attaform.UA19EF3J.mjs} +17 -29
  32. package/dist/shared/attaform.UA19EF3J.mjs.map +1 -0
  33. package/dist/shared/{attaform.C9Ph2SMx.cjs → attaform.fegmBJaq.cjs} +111 -21
  34. package/dist/shared/attaform.fegmBJaq.cjs.map +1 -0
  35. package/dist/shared/{attaform.CvOXSpCb.mjs → attaform.g7rfuXdz.mjs} +13 -16
  36. package/dist/shared/attaform.g7rfuXdz.mjs.map +1 -0
  37. package/dist/shared/{attaform.DfrYByDj.cjs → attaform.keLBaHB6.cjs} +13 -16
  38. package/dist/shared/attaform.keLBaHB6.cjs.map +1 -0
  39. package/dist/zod-v3.cjs +2 -2
  40. package/dist/zod-v3.d.cts +19 -14
  41. package/dist/zod-v3.d.mts +19 -14
  42. package/dist/zod-v3.d.ts +19 -14
  43. package/dist/zod-v3.mjs +2 -2
  44. package/dist/zod-v4.cjs +2 -2
  45. package/dist/zod-v4.d.cts +94 -5
  46. package/dist/zod-v4.d.mts +94 -5
  47. package/dist/zod-v4.d.ts +94 -5
  48. package/dist/zod-v4.mjs +2 -2
  49. package/dist/zod.cjs +23 -5
  50. package/dist/zod.cjs.map +1 -1
  51. package/dist/zod.d.cts +51 -7
  52. package/dist/zod.d.mts +51 -7
  53. package/dist/zod.d.ts +51 -7
  54. package/dist/zod.mjs +22 -5
  55. package/dist/zod.mjs.map +1 -1
  56. package/package.json +1 -1
  57. package/dist/shared/attaform.BAuJTWuT.d.mts +0 -84
  58. package/dist/shared/attaform.Bp1c-uGF.cjs.map +0 -1
  59. package/dist/shared/attaform.C9Ph2SMx.cjs.map +0 -1
  60. package/dist/shared/attaform.CvOXSpCb.mjs.map +0 -1
  61. package/dist/shared/attaform.DILbdvfo.mjs.map +0 -1
  62. package/dist/shared/attaform.DdnithOf.mjs.map +0 -1
  63. package/dist/shared/attaform.DfrYByDj.cjs.map +0 -1
  64. package/dist/shared/attaform.c_NzdRyc.cjs.map +0 -1
  65. package/dist/shared/attaform.jrxE_xZw.mjs.map +0 -1
  66. package/dist/shared/attaform.ls_7jBYc.d.ts +0 -84
  67. package/dist/shared/attaform.xIcmqscx.d.cts +0 -84
@@ -2,14 +2,22 @@ import { Ref, ObjectDirective, ComputedRef } from 'vue';
2
2
 
3
3
  /**
4
4
  * Schema-attached field metadata — the shared types used by both Zod
5
- * adapters (`attaform/zod` for v4 and `attaform/zod-v3` for v3) so a
6
- * consumer's data flow reads the same shape regardless of adapter.
7
- *
8
- * The Zod 4 adapter creates a typed `z.registry<FieldMetaPayload>()`
9
- * and writes through `schema.register(fieldMeta, payload)` (native) or
10
- * the `withMeta(schema, payload)` helper. The Zod 3 adapter has no
11
- * native registry it shims a `WeakMap<ZodTypeAny, FieldMetaPayload>`
12
- * with the same write API via `withMeta`.
5
+ * adapters and the unified `attaform/zod` entry so a consumer's data
6
+ * flow reads the same shape regardless of which path runs at lookup.
7
+ *
8
+ * Storage lives in the cross-adapter `field-meta-store` core: a pair
9
+ * of WeakMaps (single-payload for last-write-wins reads, list-of-
10
+ * payloads for shared-schema disambiguation). Every entry's
11
+ * `fieldMeta` re-exports the same registry-shaped object, so
12
+ * `withMeta`/`fieldMeta.add` writes from one entry surface at lookup
13
+ * through any other.
14
+ *
15
+ * `withMeta(schema, payload)` clones the schema before registering,
16
+ * so each call gets fresh identity (the WeakMap keys on reference).
17
+ * The cloning strategy depends on the major: Zod 4 schemas use the
18
+ * native `.clone()`, Zod 3 schemas reconstruct via constructor +
19
+ * `_def`. The unified entry's `withMeta` runtime-branches on which
20
+ * one is in play.
13
21
  *
14
22
  * Reads are unified through `AbstractSchema.getFieldMetaAtPath(path)`,
15
23
  * which returns a fully-resolved `ResolvedFieldMeta` (label /
@@ -90,10 +98,15 @@ type Path = readonly Segment[];
90
98
  * ```ts
91
99
  * parseDottedPath('user.address.line1') // ['user', 'address', 'line1']
92
100
  * parseDottedPath('items.0.name') // ['items', 0, 'name']
93
- * parseDottedPath('') // [] (root)
101
+ * parseDottedPath('') // [''] (the empty-string key)
94
102
  * ```
95
103
  *
96
- * Throws `InvalidPathError` for paths with empty segments
104
+ * The empty-string input `''` is the **literal empty-key path**, not
105
+ * the root. Use the array form `[]` for root. Form-level errors
106
+ * (root `.refine()`) live at the empty-string path bucket so
107
+ * `errors('')` returns them without sweeping every field error too.
108
+ *
109
+ * Throws `InvalidPathError` for paths with empty INTERNAL segments
97
110
  * (`'a..b'`, leading or trailing dots). For keys containing literal
98
111
  * dots, pass an array form (`['user.name']`) instead.
99
112
  */
@@ -457,9 +470,10 @@ type FormKey = string;
457
470
  /**
458
471
  * One validation failure. `path` points at the offending field as a
459
472
  * structured array — `['user', 'address', 0, 'line1']` for a nested
460
- * field, `[]` for a form-level error. `formKey` identifies which
461
- * form produced the error so a single error list can be routed to
462
- * multiple forms.
473
+ * field, `['']` (the empty-string path) for a form-level error
474
+ * (root `.refine()` messages, `setFormErrors()` entries, server-
475
+ * emitted form banners). `formKey` identifies which form produced
476
+ * the error so a single error list can be routed to multiple forms.
463
477
  *
464
478
  * Returned by `validate()` / `validateAsync()` / `handleSubmit`'s
465
479
  * `onError` callback, and by `parseApiErrors` for server responses.
@@ -467,7 +481,12 @@ type FormKey = string;
467
481
  type ValidationError = {
468
482
  /** Human-readable message describing the failure. */
469
483
  message: string;
470
- /** Structured path of the offending field. Empty array means a form-level error. */
484
+ /**
485
+ * Structured path of the offending field. The empty-string path
486
+ * `['']` is the form-level bucket — the dedicated home for errors
487
+ * that don't belong to any specific field, distinct from the
488
+ * whole-form subtree address `[]`.
489
+ */
471
490
  path: (string | number)[];
472
491
  /** Identifies which form produced this error. */
473
492
  formKey: FormKey;
@@ -844,8 +863,8 @@ type AbstractSchema<Form, GetValueFormType> = {
844
863
  * Return the resolved field metadata for the schema node at `path`
845
864
  * — label, description, placeholder, plus the full registered
846
865
  * payload as `meta` for consumer-augmented keys. Reads through the
847
- * adapter's metadata mechanism (Zod 4: `z.registry()`; Zod 3:
848
- * a WeakMap shim) and applies these one-way fallbacks:
866
+ * shared cross-adapter field-meta store and applies these one-way
867
+ * fallbacks:
849
868
  *
850
869
  * - `label`: registry payload → `humanize(lastSegment)`
851
870
  * - `description`: registry payload → `schema.description`
@@ -1386,6 +1405,17 @@ type UseFormConfiguration<Form extends GenericForm, GetValueFormType, Schema ext
1386
1405
  * coerced.
1387
1406
  */
1388
1407
  coerce?: boolean | CoercionRegistry;
1408
+ /**
1409
+ * Per-form override of the `shouldShowErrors` heuristic that drives
1410
+ * `field.showErrors` and `form.meta.showErrors`. Falls back to
1411
+ * `AttaformDefaults.shouldShowErrors`, then to the library default
1412
+ * (`defaultShouldShowErrors`). See `AttaformDefaults.shouldShowErrors`
1413
+ * for the resolution rules and predicate signature.
1414
+ *
1415
+ * Boolean shorthand: `true` → always show *when errors exist*;
1416
+ * `false` → never show.
1417
+ */
1418
+ shouldShowErrors?: ShouldShowErrorsConfig;
1389
1419
  };
1390
1420
  /**
1391
1421
  * App-level defaults applied to every `useForm` call. Set these once
@@ -1451,6 +1481,34 @@ type AttaformDefaults = {
1451
1481
  * authoritative writes whose strict typing is on the caller.
1452
1482
  */
1453
1483
  coerce?: boolean | CoercionRegistry;
1484
+ /**
1485
+ * Default for `useForm({ shouldShowErrors })`. Centralised heuristic
1486
+ * that drives `field.showErrors` (and `form.meta.showErrors`) — a
1487
+ * boolean that gates whether a path's errors are *ready* to render.
1488
+ *
1489
+ * Resolution order (per-form wins):
1490
+ *
1491
+ * useForm({ shouldShowErrors }) > AttaformDefaults > library default
1492
+ *
1493
+ * The library default reads "show after the first submit attempt OR
1494
+ * after the field has been interacted with AND changed":
1495
+ *
1496
+ * ```ts
1497
+ * (field, formMeta) =>
1498
+ * formMeta.submitCount > 0 || (field.touched === true && field.dirty)
1499
+ * ```
1500
+ *
1501
+ * Compose with the library default via the public
1502
+ * `defaultShouldShowErrors` export. Boolean shorthand is supported:
1503
+ * `true` → always show *when errors exist*; `false` → never show. The
1504
+ * predicate is invoked only when `errors.length > 0`, so authors
1505
+ * don't re-check inside.
1506
+ *
1507
+ * The predicate's args are `Omit`'d of `showErrors` / `firstError`
1508
+ * to prevent recursive predicates — those are derived FROM this
1509
+ * predicate, so reading them inside would be a self-reference.
1510
+ */
1511
+ shouldShowErrors?: ShouldShowErrorsConfig;
1454
1512
  };
1455
1513
  /**
1456
1514
  * Callback invoked by `handleSubmit` after the form parses successfully.
@@ -1465,6 +1523,46 @@ type OnSubmit<Form extends GenericForm> = (form: Form) => void | Promise<void>;
1465
1523
  * automatic `onInvalidSubmit` UI nudge).
1466
1524
  */
1467
1525
  type OnError = (error: ValidationError[]) => void | Promise<void>;
1526
+ /**
1527
+ * Predicate that drives `field.showErrors` (and `form.meta.showErrors`).
1528
+ * Receives the field's reactive state plus the form's reactive meta;
1529
+ * returns `true` to render the field's errors, `false` to keep them
1530
+ * hidden. The framework gates the call on `errors.length > 0`, so
1531
+ * authors don't re-check error presence inside.
1532
+ *
1533
+ * Both arguments are `Omit`'d of `showErrors` / `firstError` — those
1534
+ * are derived FROM this predicate, so reading them inside would be a
1535
+ * self-reference. The omit is enforced at the type level AND at
1536
+ * runtime: the keys literally are not present on the objects passed
1537
+ * in, so `as` casting in TS or vanilla-JS bypass cannot create a
1538
+ * cycle.
1539
+ *
1540
+ * The library default — `defaultShouldShowErrors` — is publicly
1541
+ * exported so a layered predicate can compose with it:
1542
+ *
1543
+ * ```ts
1544
+ * import { defaultShouldShowErrors } from 'attaform'
1545
+ *
1546
+ * useForm({
1547
+ * schema,
1548
+ * shouldShowErrors: (field, formMeta) =>
1549
+ * field.path[0] === 'urgent' || defaultShouldShowErrors(field, formMeta),
1550
+ * })
1551
+ * ```
1552
+ */
1553
+ type ShouldShowErrors = (field: Omit<FieldState, 'showErrors' | 'firstError'>, formMeta: Omit<FormMeta, 'showErrors' | 'firstError'>) => boolean;
1554
+ /**
1555
+ * Configuration shape for `shouldShowErrors`. A predicate function or
1556
+ * a boolean shorthand:
1557
+ *
1558
+ * - `true` — always show errors (when any exist).
1559
+ * - `false` — never show errors.
1560
+ * - function — custom predicate, see `ShouldShowErrors`.
1561
+ *
1562
+ * Resolved through three tiers (per-form > plugin defaults > library
1563
+ * default).
1564
+ */
1565
+ type ShouldShowErrorsConfig = ShouldShowErrors | boolean;
1468
1566
  /**
1469
1567
  * Submit handler returned by `handleSubmit(onSubmit, onError)`. Bind
1470
1568
  * it to a `<form>`:
@@ -2067,13 +2165,58 @@ type FieldState<Value = unknown> = {
2067
2165
  * green-checkmark / `aria-invalid` UX.
2068
2166
  */
2069
2167
  readonly valid: boolean;
2168
+ /**
2169
+ * Centralised "should I render this field's errors right now?"
2170
+ * gate. Wraps `errors.length > 0 && shouldShowErrors(field, formMeta)`
2171
+ * so templates avoid re-spelling the heuristic at every error site:
2172
+ *
2173
+ * ```vue
2174
+ * <span v-if="form.fields.email.showErrors">
2175
+ * {{ form.fields.email.firstError?.message }}
2176
+ * </span>
2177
+ * ```
2178
+ *
2179
+ * The heuristic itself comes from `useForm({ shouldShowErrors })` →
2180
+ * `createAttaform({ defaults: { shouldShowErrors } })` → library
2181
+ * default (`defaultShouldShowErrors` — show after first submit OR
2182
+ * after touched-and-dirty). Override per form, app-wide, or
2183
+ * compose with `defaultShouldShowErrors` for a layered predicate.
2184
+ *
2185
+ * Falls back to `false` whenever there are no errors — the gate
2186
+ * skips the predicate entirely in that case.
2187
+ *
2188
+ * Available on container paths too: `form.fields.users[0].showErrors`
2189
+ * aggregates over the row's descendants (any descendant with a
2190
+ * qualifying error flips the container on).
2191
+ */
2192
+ readonly showErrors: boolean;
2193
+ /**
2194
+ * The first `ValidationError` at this path in the deterministic
2195
+ * schema-declaration order — equivalent to `errors[0]`, exposed as
2196
+ * a sugar accessor for the common case of "show the highest-priority
2197
+ * error message and ignore the rest":
2198
+ *
2199
+ * ```vue
2200
+ * <span v-if="form.fields.email.showErrors">
2201
+ * {{ form.fields.email.firstError?.message }}
2202
+ * </span>
2203
+ * ```
2204
+ *
2205
+ * `undefined` when no errors exist. Independent of `showErrors` —
2206
+ * the data primitive is always available; the heuristic only
2207
+ * decides when to render it.
2208
+ *
2209
+ * On container paths, the first error in the aggregated subtree
2210
+ * (descendants sorted by `pathOrdinal`).
2211
+ */
2212
+ readonly firstError: ValidationError | undefined;
2070
2213
  readonly path: ReadonlyArray<string | number>;
2071
2214
  readonly blank: boolean;
2072
2215
  /**
2073
2216
  * Presentational label for this field. Resolves through the
2074
- * adapter's metadata mechanismZod 4's `z.registry()` (typed
2075
- * payload via `schema.register(fieldMeta, {...})` or the
2076
- * `withMeta()` helper); Zod 3's WeakMap shim — and falls back to
2217
+ * shared cross-adapter field-meta storewritten via
2218
+ * `schema.register(fieldMeta, {...})` (Zod 4 native chain) or the
2219
+ * `withMeta()` helper (works on both majors) — and falls back to
2077
2220
  * a humanized form of the path's last segment when nothing has
2078
2221
  * been registered. Always a string.
2079
2222
  *
@@ -2782,12 +2925,15 @@ type UseFormReturnType<Form extends GenericForm, GetValueFormType extends Generi
2782
2925
  * if (result.ok) form.setFormErrors(result.errors)
2783
2926
  * ```
2784
2927
  *
2785
- * Form-level errors surface in `form.meta.errors` (alongside field
2786
- * errors) but are intentionally excluded from the path-keyed
2787
- * `form.errors` proxy (no key represents `[]` in a nested object) —
2788
- * read them via `meta.errors.filter(e => e.path.length === 0)` or
2789
- * `form.errors([])` (the call-form aggregates everywhere, including
2790
- * form-level errors at `path: []`).
2928
+ * Form-level errors land at the empty-string path bucket
2929
+ * (`path: ['']`). They surface in `form.meta.errors` (alongside
2930
+ * field errors), in `form.errors()` / `form.errors([])` (whole-form
2931
+ * subtree aggregates), and — uniquely — in `form.errors('')`,
2932
+ * which returns ONLY the form-level bucket. They're excluded from
2933
+ * the path-keyed `form.errors` drill proxy because no nested-object
2934
+ * key represents the empty-string path. Read them via
2935
+ * `meta.errors.filter(e => e.path.length === 1 && e.path[0] === '')`
2936
+ * if you need a programmatic split.
2791
2937
  */
2792
2938
  setFormErrors: (errors: ReadonlyArray<Partial<ValidationError> & {
2793
2939
  message: string;
@@ -2897,6 +3043,26 @@ type UseFormReturnType<Form extends GenericForm, GetValueFormType extends Generi
2897
3043
  * `options` is forwarded to `Element.scrollIntoView` unchanged.
2898
3044
  */
2899
3045
  scrollToFirstError: (options?: ScrollIntoViewOptions) => boolean;
3046
+ /**
3047
+ * Programmatically mark fields as touched — the sticky flag the
3048
+ * standard "show errors after interaction" pattern reads. Closes
3049
+ * the gap when fields are populated without a DOM gesture (post-
3050
+ * import, paste, autofill, server-seeded values you want to
3051
+ * validate immediately).
3052
+ *
3053
+ * ```ts
3054
+ * form.touch('email') // one leaf
3055
+ * form.touch('profile') // every leaf under profile
3056
+ * form.touch(['profile', 'name']) // segment-array form
3057
+ * form.touch() // every leaf in the form
3058
+ * ```
3059
+ *
3060
+ * Pure flag write — does not mutate value, focused, blurred, or
3061
+ * trigger validation. Idempotent: re-calling on an already-touched
3062
+ * field is a no-op. Touched is sticky-true; pair with
3063
+ * `form.reset()` / `form.resetField()` to clear.
3064
+ */
3065
+ touch: (path?: FlatPath<Form> | (string | number)[]) => void;
2900
3066
  /**
2901
3067
  * Append `value` to the array at `path`.
2902
3068
  *
@@ -2948,5 +3114,5 @@ type UseFormReturnType<Form extends GenericForm, GetValueFormType extends Generi
2948
3114
  blankPaths: ComputedRef<ReadonlySet<string>>;
2949
3115
  };
2950
3116
 
2951
- export { ROOT_PATH as Z, ROOT_PATH_KEY as _, canonicalizePath as ak, isPathPrefix as al, isUnset as am, parseDottedPath as an, unset as ao };
2952
- export type { ReactiveValidationStatus as $, AttaformDefaults as A, OnInvalidSubmitPolicy as B, CoercionRegistry as C, DeepPartial as D, OnSubmit 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, OnError as O, Path as P, PathKey as Q, RegisterValue as R, SlimPrimitiveKind as S, PendingValidationStatus as T, UseFormConfiguration as U, ValidationError as V, PersistConfig as W, PersistConfigOptions as X, PersistIncludeMode as Y, CoercionEntry as a, RegisterDirective as a0, RegisterFlatPath as a1, RegisterOptions as a2, RegisterSelectModifier as a3, RegisterTextModifier as a4, RegisterTransform as a5, Segment as a6, SetValueCallback as a7, SetValuePayload as a8, SettledValidationStatus as a9, SlimRuntimeOf as aa, SubmitHandler as ab, Unset as ac, ValidateOn as ad, ValidateOnConfig as ae, ValidationResponse as af, ValidationResponseWithoutValue as ag, ValueOfUnion as ah, WriteMeta as ai, WriteShape as aj, AbstractSchema as b, DefaultValuesShape as c, UseFormReturnType as d, RegisterModelDynamicCustomDirective as e, ApiErrorEnvelope as f, ApiErrorDetails as g, ApiErrorEntry as h, ArrayItem as i, ArrayPath as j, CoercionResult as k, CustomDirectiveRegisterAssignerFn as l, DefaultValuesResponse as m, FieldMetaPayload as n, FieldState as o, FieldStateMap as p, FieldStateMapEntry as q, FlatPath as r, FormErrorRecord as s, FormErrorsSurface as t, FormMeta as u, FormStorage as v, FormStorageKind as w, HistoryConfig as x, IsUnion as y, NestedType as z };
3117
+ export { ROOT_PATH_KEY as $, ROOT_PATH as _, canonicalizePath as am, isPathPrefix as an, isUnset as ao, parseDottedPath as ap, unset as aq };
3118
+ export type { AttaformDefaults as A, NestedType as B, CoercionRegistry as C, DeepPartial as D, OnInvalidSubmitPolicy 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, OnError as O, OnSubmit as P, Path as Q, RegisterValue as R, SlimPrimitiveKind as S, PathKey as T, UseFormConfiguration as U, ValidationError as V, PendingValidationStatus as W, PersistConfig as X, PersistConfigOptions as Y, PersistIncludeMode as Z, CoercionEntry as a, ReactiveValidationStatus as a0, RegisterDirective as a1, RegisterFlatPath as a2, RegisterOptions as a3, RegisterSelectModifier as a4, RegisterTextModifier as a5, RegisterTransform as a6, Segment as a7, SetValueCallback as a8, SetValuePayload as a9, SettledValidationStatus as aa, ShouldShowErrorsConfig as ab, SlimRuntimeOf as ac, SubmitHandler as ad, Unset as ae, ValidateOn as af, ValidateOnConfig as ag, ValidationResponse as ah, ValidationResponseWithoutValue as ai, ValueOfUnion as aj, WriteMeta as ak, WriteShape as al, AbstractSchema as b, DefaultValuesShape as c, UseFormReturnType as d, RegisterModelDynamicCustomDirective as e, ShouldShowErrors as f, ApiErrorEnvelope as g, ApiErrorDetails as h, ApiErrorEntry as i, ArrayItem as j, ArrayPath as k, CoercionResult as l, CustomDirectiveRegisterAssignerFn as m, DefaultValuesResponse as n, FieldMetaPayload as o, FieldState as p, FieldStateMap as q, FieldStateMapEntry as r, FlatPath as s, FormErrorRecord as t, FormErrorsSurface as u, FormMeta as v, FormStorage as w, FormStorageKind as x, HistoryConfig as y, IsUnion as z };
@@ -2,14 +2,22 @@ import { Ref, ObjectDirective, ComputedRef } from 'vue';
2
2
 
3
3
  /**
4
4
  * Schema-attached field metadata — the shared types used by both Zod
5
- * adapters (`attaform/zod` for v4 and `attaform/zod-v3` for v3) so a
6
- * consumer's data flow reads the same shape regardless of adapter.
7
- *
8
- * The Zod 4 adapter creates a typed `z.registry<FieldMetaPayload>()`
9
- * and writes through `schema.register(fieldMeta, payload)` (native) or
10
- * the `withMeta(schema, payload)` helper. The Zod 3 adapter has no
11
- * native registry it shims a `WeakMap<ZodTypeAny, FieldMetaPayload>`
12
- * with the same write API via `withMeta`.
5
+ * adapters and the unified `attaform/zod` entry so a consumer's data
6
+ * flow reads the same shape regardless of which path runs at lookup.
7
+ *
8
+ * Storage lives in the cross-adapter `field-meta-store` core: a pair
9
+ * of WeakMaps (single-payload for last-write-wins reads, list-of-
10
+ * payloads for shared-schema disambiguation). Every entry's
11
+ * `fieldMeta` re-exports the same registry-shaped object, so
12
+ * `withMeta`/`fieldMeta.add` writes from one entry surface at lookup
13
+ * through any other.
14
+ *
15
+ * `withMeta(schema, payload)` clones the schema before registering,
16
+ * so each call gets fresh identity (the WeakMap keys on reference).
17
+ * The cloning strategy depends on the major: Zod 4 schemas use the
18
+ * native `.clone()`, Zod 3 schemas reconstruct via constructor +
19
+ * `_def`. The unified entry's `withMeta` runtime-branches on which
20
+ * one is in play.
13
21
  *
14
22
  * Reads are unified through `AbstractSchema.getFieldMetaAtPath(path)`,
15
23
  * which returns a fully-resolved `ResolvedFieldMeta` (label /
@@ -90,10 +98,15 @@ type Path = readonly Segment[];
90
98
  * ```ts
91
99
  * parseDottedPath('user.address.line1') // ['user', 'address', 'line1']
92
100
  * parseDottedPath('items.0.name') // ['items', 0, 'name']
93
- * parseDottedPath('') // [] (root)
101
+ * parseDottedPath('') // [''] (the empty-string key)
94
102
  * ```
95
103
  *
96
- * Throws `InvalidPathError` for paths with empty segments
104
+ * The empty-string input `''` is the **literal empty-key path**, not
105
+ * the root. Use the array form `[]` for root. Form-level errors
106
+ * (root `.refine()`) live at the empty-string path bucket so
107
+ * `errors('')` returns them without sweeping every field error too.
108
+ *
109
+ * Throws `InvalidPathError` for paths with empty INTERNAL segments
97
110
  * (`'a..b'`, leading or trailing dots). For keys containing literal
98
111
  * dots, pass an array form (`['user.name']`) instead.
99
112
  */
@@ -457,9 +470,10 @@ type FormKey = string;
457
470
  /**
458
471
  * One validation failure. `path` points at the offending field as a
459
472
  * structured array — `['user', 'address', 0, 'line1']` for a nested
460
- * field, `[]` for a form-level error. `formKey` identifies which
461
- * form produced the error so a single error list can be routed to
462
- * multiple forms.
473
+ * field, `['']` (the empty-string path) for a form-level error
474
+ * (root `.refine()` messages, `setFormErrors()` entries, server-
475
+ * emitted form banners). `formKey` identifies which form produced
476
+ * the error so a single error list can be routed to multiple forms.
463
477
  *
464
478
  * Returned by `validate()` / `validateAsync()` / `handleSubmit`'s
465
479
  * `onError` callback, and by `parseApiErrors` for server responses.
@@ -467,7 +481,12 @@ type FormKey = string;
467
481
  type ValidationError = {
468
482
  /** Human-readable message describing the failure. */
469
483
  message: string;
470
- /** Structured path of the offending field. Empty array means a form-level error. */
484
+ /**
485
+ * Structured path of the offending field. The empty-string path
486
+ * `['']` is the form-level bucket — the dedicated home for errors
487
+ * that don't belong to any specific field, distinct from the
488
+ * whole-form subtree address `[]`.
489
+ */
471
490
  path: (string | number)[];
472
491
  /** Identifies which form produced this error. */
473
492
  formKey: FormKey;
@@ -844,8 +863,8 @@ type AbstractSchema<Form, GetValueFormType> = {
844
863
  * Return the resolved field metadata for the schema node at `path`
845
864
  * — label, description, placeholder, plus the full registered
846
865
  * payload as `meta` for consumer-augmented keys. Reads through the
847
- * adapter's metadata mechanism (Zod 4: `z.registry()`; Zod 3:
848
- * a WeakMap shim) and applies these one-way fallbacks:
866
+ * shared cross-adapter field-meta store and applies these one-way
867
+ * fallbacks:
849
868
  *
850
869
  * - `label`: registry payload → `humanize(lastSegment)`
851
870
  * - `description`: registry payload → `schema.description`
@@ -1386,6 +1405,17 @@ type UseFormConfiguration<Form extends GenericForm, GetValueFormType, Schema ext
1386
1405
  * coerced.
1387
1406
  */
1388
1407
  coerce?: boolean | CoercionRegistry;
1408
+ /**
1409
+ * Per-form override of the `shouldShowErrors` heuristic that drives
1410
+ * `field.showErrors` and `form.meta.showErrors`. Falls back to
1411
+ * `AttaformDefaults.shouldShowErrors`, then to the library default
1412
+ * (`defaultShouldShowErrors`). See `AttaformDefaults.shouldShowErrors`
1413
+ * for the resolution rules and predicate signature.
1414
+ *
1415
+ * Boolean shorthand: `true` → always show *when errors exist*;
1416
+ * `false` → never show.
1417
+ */
1418
+ shouldShowErrors?: ShouldShowErrorsConfig;
1389
1419
  };
1390
1420
  /**
1391
1421
  * App-level defaults applied to every `useForm` call. Set these once
@@ -1451,6 +1481,34 @@ type AttaformDefaults = {
1451
1481
  * authoritative writes whose strict typing is on the caller.
1452
1482
  */
1453
1483
  coerce?: boolean | CoercionRegistry;
1484
+ /**
1485
+ * Default for `useForm({ shouldShowErrors })`. Centralised heuristic
1486
+ * that drives `field.showErrors` (and `form.meta.showErrors`) — a
1487
+ * boolean that gates whether a path's errors are *ready* to render.
1488
+ *
1489
+ * Resolution order (per-form wins):
1490
+ *
1491
+ * useForm({ shouldShowErrors }) > AttaformDefaults > library default
1492
+ *
1493
+ * The library default reads "show after the first submit attempt OR
1494
+ * after the field has been interacted with AND changed":
1495
+ *
1496
+ * ```ts
1497
+ * (field, formMeta) =>
1498
+ * formMeta.submitCount > 0 || (field.touched === true && field.dirty)
1499
+ * ```
1500
+ *
1501
+ * Compose with the library default via the public
1502
+ * `defaultShouldShowErrors` export. Boolean shorthand is supported:
1503
+ * `true` → always show *when errors exist*; `false` → never show. The
1504
+ * predicate is invoked only when `errors.length > 0`, so authors
1505
+ * don't re-check inside.
1506
+ *
1507
+ * The predicate's args are `Omit`'d of `showErrors` / `firstError`
1508
+ * to prevent recursive predicates — those are derived FROM this
1509
+ * predicate, so reading them inside would be a self-reference.
1510
+ */
1511
+ shouldShowErrors?: ShouldShowErrorsConfig;
1454
1512
  };
1455
1513
  /**
1456
1514
  * Callback invoked by `handleSubmit` after the form parses successfully.
@@ -1465,6 +1523,46 @@ type OnSubmit<Form extends GenericForm> = (form: Form) => void | Promise<void>;
1465
1523
  * automatic `onInvalidSubmit` UI nudge).
1466
1524
  */
1467
1525
  type OnError = (error: ValidationError[]) => void | Promise<void>;
1526
+ /**
1527
+ * Predicate that drives `field.showErrors` (and `form.meta.showErrors`).
1528
+ * Receives the field's reactive state plus the form's reactive meta;
1529
+ * returns `true` to render the field's errors, `false` to keep them
1530
+ * hidden. The framework gates the call on `errors.length > 0`, so
1531
+ * authors don't re-check error presence inside.
1532
+ *
1533
+ * Both arguments are `Omit`'d of `showErrors` / `firstError` — those
1534
+ * are derived FROM this predicate, so reading them inside would be a
1535
+ * self-reference. The omit is enforced at the type level AND at
1536
+ * runtime: the keys literally are not present on the objects passed
1537
+ * in, so `as` casting in TS or vanilla-JS bypass cannot create a
1538
+ * cycle.
1539
+ *
1540
+ * The library default — `defaultShouldShowErrors` — is publicly
1541
+ * exported so a layered predicate can compose with it:
1542
+ *
1543
+ * ```ts
1544
+ * import { defaultShouldShowErrors } from 'attaform'
1545
+ *
1546
+ * useForm({
1547
+ * schema,
1548
+ * shouldShowErrors: (field, formMeta) =>
1549
+ * field.path[0] === 'urgent' || defaultShouldShowErrors(field, formMeta),
1550
+ * })
1551
+ * ```
1552
+ */
1553
+ type ShouldShowErrors = (field: Omit<FieldState, 'showErrors' | 'firstError'>, formMeta: Omit<FormMeta, 'showErrors' | 'firstError'>) => boolean;
1554
+ /**
1555
+ * Configuration shape for `shouldShowErrors`. A predicate function or
1556
+ * a boolean shorthand:
1557
+ *
1558
+ * - `true` — always show errors (when any exist).
1559
+ * - `false` — never show errors.
1560
+ * - function — custom predicate, see `ShouldShowErrors`.
1561
+ *
1562
+ * Resolved through three tiers (per-form > plugin defaults > library
1563
+ * default).
1564
+ */
1565
+ type ShouldShowErrorsConfig = ShouldShowErrors | boolean;
1468
1566
  /**
1469
1567
  * Submit handler returned by `handleSubmit(onSubmit, onError)`. Bind
1470
1568
  * it to a `<form>`:
@@ -2067,13 +2165,58 @@ type FieldState<Value = unknown> = {
2067
2165
  * green-checkmark / `aria-invalid` UX.
2068
2166
  */
2069
2167
  readonly valid: boolean;
2168
+ /**
2169
+ * Centralised "should I render this field's errors right now?"
2170
+ * gate. Wraps `errors.length > 0 && shouldShowErrors(field, formMeta)`
2171
+ * so templates avoid re-spelling the heuristic at every error site:
2172
+ *
2173
+ * ```vue
2174
+ * <span v-if="form.fields.email.showErrors">
2175
+ * {{ form.fields.email.firstError?.message }}
2176
+ * </span>
2177
+ * ```
2178
+ *
2179
+ * The heuristic itself comes from `useForm({ shouldShowErrors })` →
2180
+ * `createAttaform({ defaults: { shouldShowErrors } })` → library
2181
+ * default (`defaultShouldShowErrors` — show after first submit OR
2182
+ * after touched-and-dirty). Override per form, app-wide, or
2183
+ * compose with `defaultShouldShowErrors` for a layered predicate.
2184
+ *
2185
+ * Falls back to `false` whenever there are no errors — the gate
2186
+ * skips the predicate entirely in that case.
2187
+ *
2188
+ * Available on container paths too: `form.fields.users[0].showErrors`
2189
+ * aggregates over the row's descendants (any descendant with a
2190
+ * qualifying error flips the container on).
2191
+ */
2192
+ readonly showErrors: boolean;
2193
+ /**
2194
+ * The first `ValidationError` at this path in the deterministic
2195
+ * schema-declaration order — equivalent to `errors[0]`, exposed as
2196
+ * a sugar accessor for the common case of "show the highest-priority
2197
+ * error message and ignore the rest":
2198
+ *
2199
+ * ```vue
2200
+ * <span v-if="form.fields.email.showErrors">
2201
+ * {{ form.fields.email.firstError?.message }}
2202
+ * </span>
2203
+ * ```
2204
+ *
2205
+ * `undefined` when no errors exist. Independent of `showErrors` —
2206
+ * the data primitive is always available; the heuristic only
2207
+ * decides when to render it.
2208
+ *
2209
+ * On container paths, the first error in the aggregated subtree
2210
+ * (descendants sorted by `pathOrdinal`).
2211
+ */
2212
+ readonly firstError: ValidationError | undefined;
2070
2213
  readonly path: ReadonlyArray<string | number>;
2071
2214
  readonly blank: boolean;
2072
2215
  /**
2073
2216
  * Presentational label for this field. Resolves through the
2074
- * adapter's metadata mechanismZod 4's `z.registry()` (typed
2075
- * payload via `schema.register(fieldMeta, {...})` or the
2076
- * `withMeta()` helper); Zod 3's WeakMap shim — and falls back to
2217
+ * shared cross-adapter field-meta storewritten via
2218
+ * `schema.register(fieldMeta, {...})` (Zod 4 native chain) or the
2219
+ * `withMeta()` helper (works on both majors) — and falls back to
2077
2220
  * a humanized form of the path's last segment when nothing has
2078
2221
  * been registered. Always a string.
2079
2222
  *
@@ -2782,12 +2925,15 @@ type UseFormReturnType<Form extends GenericForm, GetValueFormType extends Generi
2782
2925
  * if (result.ok) form.setFormErrors(result.errors)
2783
2926
  * ```
2784
2927
  *
2785
- * Form-level errors surface in `form.meta.errors` (alongside field
2786
- * errors) but are intentionally excluded from the path-keyed
2787
- * `form.errors` proxy (no key represents `[]` in a nested object) —
2788
- * read them via `meta.errors.filter(e => e.path.length === 0)` or
2789
- * `form.errors([])` (the call-form aggregates everywhere, including
2790
- * form-level errors at `path: []`).
2928
+ * Form-level errors land at the empty-string path bucket
2929
+ * (`path: ['']`). They surface in `form.meta.errors` (alongside
2930
+ * field errors), in `form.errors()` / `form.errors([])` (whole-form
2931
+ * subtree aggregates), and — uniquely — in `form.errors('')`,
2932
+ * which returns ONLY the form-level bucket. They're excluded from
2933
+ * the path-keyed `form.errors` drill proxy because no nested-object
2934
+ * key represents the empty-string path. Read them via
2935
+ * `meta.errors.filter(e => e.path.length === 1 && e.path[0] === '')`
2936
+ * if you need a programmatic split.
2791
2937
  */
2792
2938
  setFormErrors: (errors: ReadonlyArray<Partial<ValidationError> & {
2793
2939
  message: string;
@@ -2897,6 +3043,26 @@ type UseFormReturnType<Form extends GenericForm, GetValueFormType extends Generi
2897
3043
  * `options` is forwarded to `Element.scrollIntoView` unchanged.
2898
3044
  */
2899
3045
  scrollToFirstError: (options?: ScrollIntoViewOptions) => boolean;
3046
+ /**
3047
+ * Programmatically mark fields as touched — the sticky flag the
3048
+ * standard "show errors after interaction" pattern reads. Closes
3049
+ * the gap when fields are populated without a DOM gesture (post-
3050
+ * import, paste, autofill, server-seeded values you want to
3051
+ * validate immediately).
3052
+ *
3053
+ * ```ts
3054
+ * form.touch('email') // one leaf
3055
+ * form.touch('profile') // every leaf under profile
3056
+ * form.touch(['profile', 'name']) // segment-array form
3057
+ * form.touch() // every leaf in the form
3058
+ * ```
3059
+ *
3060
+ * Pure flag write — does not mutate value, focused, blurred, or
3061
+ * trigger validation. Idempotent: re-calling on an already-touched
3062
+ * field is a no-op. Touched is sticky-true; pair with
3063
+ * `form.reset()` / `form.resetField()` to clear.
3064
+ */
3065
+ touch: (path?: FlatPath<Form> | (string | number)[]) => void;
2900
3066
  /**
2901
3067
  * Append `value` to the array at `path`.
2902
3068
  *
@@ -2948,5 +3114,5 @@ type UseFormReturnType<Form extends GenericForm, GetValueFormType extends Generi
2948
3114
  blankPaths: ComputedRef<ReadonlySet<string>>;
2949
3115
  };
2950
3116
 
2951
- export { ROOT_PATH as Z, ROOT_PATH_KEY as _, canonicalizePath as ak, isPathPrefix as al, isUnset as am, parseDottedPath as an, unset as ao };
2952
- export type { ReactiveValidationStatus as $, AttaformDefaults as A, OnInvalidSubmitPolicy as B, CoercionRegistry as C, DeepPartial as D, OnSubmit 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, OnError as O, Path as P, PathKey as Q, RegisterValue as R, SlimPrimitiveKind as S, PendingValidationStatus as T, UseFormConfiguration as U, ValidationError as V, PersistConfig as W, PersistConfigOptions as X, PersistIncludeMode as Y, CoercionEntry as a, RegisterDirective as a0, RegisterFlatPath as a1, RegisterOptions as a2, RegisterSelectModifier as a3, RegisterTextModifier as a4, RegisterTransform as a5, Segment as a6, SetValueCallback as a7, SetValuePayload as a8, SettledValidationStatus as a9, SlimRuntimeOf as aa, SubmitHandler as ab, Unset as ac, ValidateOn as ad, ValidateOnConfig as ae, ValidationResponse as af, ValidationResponseWithoutValue as ag, ValueOfUnion as ah, WriteMeta as ai, WriteShape as aj, AbstractSchema as b, DefaultValuesShape as c, UseFormReturnType as d, RegisterModelDynamicCustomDirective as e, ApiErrorEnvelope as f, ApiErrorDetails as g, ApiErrorEntry as h, ArrayItem as i, ArrayPath as j, CoercionResult as k, CustomDirectiveRegisterAssignerFn as l, DefaultValuesResponse as m, FieldMetaPayload as n, FieldState as o, FieldStateMap as p, FieldStateMapEntry as q, FlatPath as r, FormErrorRecord as s, FormErrorsSurface as t, FormMeta as u, FormStorage as v, FormStorageKind as w, HistoryConfig as x, IsUnion as y, NestedType as z };
3117
+ export { ROOT_PATH_KEY as $, ROOT_PATH as _, canonicalizePath as am, isPathPrefix as an, isUnset as ao, parseDottedPath as ap, unset as aq };
3118
+ export type { AttaformDefaults as A, NestedType as B, CoercionRegistry as C, DeepPartial as D, OnInvalidSubmitPolicy 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, OnError as O, OnSubmit as P, Path as Q, RegisterValue as R, SlimPrimitiveKind as S, PathKey as T, UseFormConfiguration as U, ValidationError as V, PendingValidationStatus as W, PersistConfig as X, PersistConfigOptions as Y, PersistIncludeMode as Z, CoercionEntry as a, ReactiveValidationStatus as a0, RegisterDirective as a1, RegisterFlatPath as a2, RegisterOptions as a3, RegisterSelectModifier as a4, RegisterTextModifier as a5, RegisterTransform as a6, Segment as a7, SetValueCallback as a8, SetValuePayload as a9, SettledValidationStatus as aa, ShouldShowErrorsConfig as ab, SlimRuntimeOf as ac, SubmitHandler as ad, Unset as ae, ValidateOn as af, ValidateOnConfig as ag, ValidationResponse as ah, ValidationResponseWithoutValue as ai, ValueOfUnion as aj, WriteMeta as ak, WriteShape as al, AbstractSchema as b, DefaultValuesShape as c, UseFormReturnType as d, RegisterModelDynamicCustomDirective as e, ShouldShowErrors as f, ApiErrorEnvelope as g, ApiErrorDetails as h, ApiErrorEntry as i, ArrayItem as j, ArrayPath as k, CoercionResult as l, CustomDirectiveRegisterAssignerFn as m, DefaultValuesResponse as n, FieldMetaPayload as o, FieldState as p, FieldStateMap as q, FieldStateMapEntry as r, FlatPath as s, FormErrorRecord as t, FormErrorsSurface as u, FormMeta as v, FormStorage as w, FormStorageKind as x, HistoryConfig as y, IsUnion as z };