attaform 0.31.0 → 0.31.2

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 (52) hide show
  1. package/dist/abstract.d.mts +5 -5
  2. package/dist/abstract.mjs +5 -4
  3. package/dist/dev/abstract.mjs +5 -4
  4. package/dist/dev/chunks/devtools.mjs +1 -1
  5. package/dist/dev/directive.mjs +1 -1
  6. package/dist/dev/history.mjs +126 -6
  7. package/dist/dev/index.mjs +6 -5
  8. package/dist/dev/runtime/plugins/attaform.mjs +1 -1
  9. package/dist/dev/shared/{attaform-dev.zlvDr0WB.mjs → attaform-dev.04aupFBW.mjs} +3 -2
  10. package/dist/dev/shared/{attaform-dev.CtyqGia7.mjs → attaform-dev.BKWPjEB2.mjs} +5 -4
  11. package/dist/dev/shared/{attaform-dev.DlaT4xGd.mjs → attaform-dev.BnLLxX2l.mjs} +4 -4
  12. package/dist/dev/shared/{attaform-dev.B1q-x9R9.mjs → attaform-dev.Bu6wgrA5.mjs} +1 -1
  13. package/dist/dev/shared/{attaform-dev.k5X47GMD.mjs → attaform-dev.CKSqs_xQ.mjs} +3 -2
  14. package/dist/dev/shared/{attaform-dev.DJ3nsWw6.mjs → attaform-dev.Cf_hZfbT.mjs} +18 -9
  15. package/dist/dev/shared/{attaform-dev.y9x9Vp9F.mjs → attaform-dev.DLk51W7-.mjs} +565 -926
  16. package/dist/dev/shared/attaform-dev.DoG8mZTq.mjs +127 -0
  17. package/dist/dev/shared/attaform-dev.G-3XDl9F.mjs +1192 -0
  18. package/dist/dev/shared/{attaform-dev.Caibt7sb.mjs → attaform-dev.oguHqv1W.mjs} +11 -10
  19. package/dist/dev/zod-v3.mjs +7 -6
  20. package/dist/dev/zod-v4.mjs +7 -6
  21. package/dist/dev/zod.mjs +6 -5
  22. package/dist/directive.d.mts +2 -2
  23. package/dist/directive.mjs +1 -1
  24. package/dist/history.d.mts +1 -1
  25. package/dist/history.mjs +126 -6
  26. package/dist/index.d.mts +16 -55
  27. package/dist/index.mjs +6 -5
  28. package/dist/runtime/plugins/attaform.mjs +1 -1
  29. package/dist/shared/{attaform.DL2nl_QR.mjs → attaform.3kV6PqVJ.mjs} +11 -10
  30. package/dist/shared/{attaform.L011tHgJ.mjs → attaform.BJeiuJE2.mjs} +4 -4
  31. package/dist/shared/attaform.BNUhW4qC.mjs +103 -0
  32. package/dist/shared/{attaform.CdZIEf7_.mjs → attaform.BhtAZysa.mjs} +3 -2
  33. package/dist/shared/{attaform.BWUn8Kjy.mjs → attaform.BlAmZ-WU.mjs} +18 -9
  34. package/dist/shared/{attaform.LUPC-2Qg.mjs → attaform.CMzc4bPI.mjs} +5 -4
  35. package/dist/shared/{attaform.C4shmwyJ.mjs → attaform.CdA9bfRv.mjs} +1 -1
  36. package/dist/shared/attaform.CgyyJVuq.mjs +1187 -0
  37. package/dist/shared/{attaform.DAo0QBmS.d.mts → attaform.CibLYe9-.d.mts} +214 -173
  38. package/dist/shared/{attaform.DNPJPOO7.mjs → attaform.DCUJCKyG.mjs} +562 -925
  39. package/dist/shared/{attaform.D9RqPgGu.d.mts → attaform.DCyZT7u-.d.mts} +1 -1
  40. package/dist/shared/{attaform.CRVe8s82.d.mts → attaform.DD0Y8krq.d.mts} +43 -19
  41. package/dist/shared/{attaform.DxKQsqFZ.mjs → attaform.T0YjhjPU.mjs} +3 -2
  42. package/dist/shared/{attaform.DZnrDrNb.d.mts → attaform.bI_4nKZp.d.mts} +119 -8
  43. package/dist/zod-v3.d.mts +4 -4
  44. package/dist/zod-v3.mjs +7 -6
  45. package/dist/zod-v4.d.mts +15 -11
  46. package/dist/zod-v4.mjs +7 -6
  47. package/dist/zod.d.mts +4 -4
  48. package/dist/zod.mjs +6 -5
  49. package/package.json +2 -1
  50. package/skills/attaform/SKILL.md +1 -0
  51. package/dist/dev/shared/attaform-dev.CZz4GYZ6.mjs +0 -386
  52. package/dist/shared/attaform.8mfKZCwH.mjs +0 -358
@@ -1,160 +1,6 @@
1
- import { G as GenericForm, F as FormKey, a as UseFormReturnType, _ as RegisterValue, v as Path, w as PathKey, aq as DisplayCtx, ar as GetDisplayState, as as DisplayMachine, at as FieldRecord, au as ErrorCell, ac as ValidationError, av as OriginalsRecord, A as AbstractSchema, ag as WriteMeta, d as DeepPartial, ah as WriteShape, aw as AttaformDomBinding, ax as TransformAbortHolder } from './attaform.DZnrDrNb.mjs';
1
+ import { v as Path, w as PathKey, aq as DisplayCtx, ar as GetDisplayState, as as DisplayMachine, G as GenericForm, F as FormKey, at as FieldRecord, au as ErrorCell, ac as ValidationError, av as OriginalsRecord, A as AbstractSchema, ag as WriteMeta, d as DeepPartial, ah as WriteShape, aw as StructureSnapshot, ax as IndexRemap, ay as AttaformDomBinding, az as TransformAbortHolder, a as UseFormReturnType, _ as RegisterValue } from './attaform.bI_4nKZp.mjs';
2
2
  import { Ref, ComputedRef, ShallowRef, App, InjectionKey, Plugin } from 'vue';
3
3
 
4
- /** Options accepted by `injectForm` in place of a bare key string. */
5
- type InjectFormInput = {
6
- readonly key?: FormKey;
7
- /**
8
- * Set by the Vite transform when this `injectForm` call site sits in
9
- * a component whose template / script reads the form's reactive
10
- * state. On the server, this enqueues the form for SSR prefetch and
11
- * wires `onServerPrefetch` so the descendant awaits the activation
12
- * promise before its render emits HTML.
13
- *
14
- * @internal Transform-emitted. Manual use is the documented escape
15
- * hatch when the transform can't reach the reference (dynamic
16
- * property access, untransformed bundlers).
17
- */
18
- readonly __ssrAccessed?: boolean;
19
- };
20
- /**
21
- * Reach an existing form from a descendant component without threading
22
- * it through props. The counterpart to `useForm`: `useForm` creates and
23
- * provides, `injectForm` looks up.
24
- *
25
- * ```ts
26
- * // The nearest ancestor's anonymous useForm() call.
27
- * const form = injectForm<SignupShape>()
28
- *
29
- * // A specific form by key, from anywhere in the app.
30
- * const cart = injectForm<CartShape>('cart')
31
- * ```
32
- *
33
- * A keyed call is a registry lookup, so it is independent of
34
- * component-tree position. A no-key call takes the closest ambient
35
- * ancestor and inherits its `formInstanceId`; only an anonymous
36
- * `useForm()` fills that ambient slot, so a keyed form is reachable
37
- * only by its key.
38
- *
39
- * Returns `null` when nothing matches, so narrow before use:
40
- *
41
- * ```ts
42
- * const form = injectForm<Shape>('signup')
43
- * if (!form) return
44
- * form.register('email')
45
- * ```
46
- *
47
- * A keyed miss warns in dev with the registered keys and the call site.
48
- * Pass the `Form` generic explicitly, since Vue's provide/inject erases
49
- * generics. The form stays alive for this component's lifetime and is
50
- * cleaned up once every consumer has unmounted.
51
- *
52
- * Components whose nearest `useForm`-calling ancestor is the same one get
53
- * the same form object for the same form: `injectForm('cart')` in two of
54
- * its descendants returns one form, with one set of field caches.
55
- */
56
- declare function injectForm<Form extends GenericForm, GetValueFormType extends GenericForm = Form>(input?: FormKey | InjectFormInput): UseFormReturnType<Form, GetValueFormType> | null;
57
-
58
- /**
59
- * Return type of `useRegister()`. Hybrid of `RegisterValue<V>` (so
60
- * `rv.path` / `rv.segments` / `rv.formKey` etc. work directly in
61
- * script setup) and `Ref<RegisterValue<V> | undefined>` (so Vue's
62
- * template auto-unwrap surfaces the underlying RV to `v-register`
63
- * and the directive's path-migration diff sees the real RV across
64
- * renders).
65
- *
66
- * The two surfaces don't clash at the type level: `RegisterValue`
67
- * doesn't carry a `value` field, and `Ref<T>`'s `value: T` becomes
68
- * the hybrid's only `.value`. Older code that read `rv.value?.path`
69
- * keeps working; new code can write `rv.path` directly.
70
- */
71
- type UseRegisterReturn<V = unknown> = RegisterValue<V> & Ref<RegisterValue<V> | undefined> & {
72
- /**
73
- * Whether a parent has actually bound this wrapper. `false` until a
74
- * `RegisterValue` lands, `true` once one has, and reactive in both
75
- * directions: a parent that binds on a later render flips it, and
76
- * so does one that stops binding.
77
- *
78
- * This is the test a dual-mode wrapper reaches for, and the reason
79
- * it exists as a field rather than a truthiness check on the
80
- * return: the composable cannot answer "is this bound" at setup
81
- * time. A parent is free to bind on a later render
82
- * (`v-register="ready ? form.register('x') : undefined"`), and
83
- * `useRegister` has to keep serving that, so it can never return a
84
- * plain `undefined` for "unbound right now". Script-setup code
85
- * reading `rv` directly sees the hybrid Proxy, which is an object
86
- * and therefore always truthy; a template sees the unwrapped ref,
87
- * which IS `undefined` when unbound. `rv?.isBound` reads the same
88
- * on both sides (#620).
89
- */
90
- readonly isBound: boolean;
91
- };
92
- /**
93
- * Options for `useRegister`.
94
- */
95
- type UseRegisterOptions = {
96
- /**
97
- * Declares that this wrapper is used both with and without a form, so
98
- * a render with no parent binding is intended rather than a mistake.
99
- * Silences the unbound diagnostic; nothing else changes.
100
- *
101
- * The diagnostic exists because a single-mode wrapper whose parent
102
- * forgot `v-register` renders a field that looks fine and stores
103
- * nothing, which is an expensive afternoon. Once a wrapper is
104
- * deliberately dual-mode, the same signal fires on correct code and
105
- * tells the author to do something wrong, so this is the one fact
106
- * that separates the two cases and only the author has it (#620).
107
- *
108
- * Pair it with `rv?.isBound` to branch on what actually happened.
109
- */
110
- readonly optional?: boolean;
111
- };
112
- /**
113
- * Re-bind a parent's `v-register` onto an inner native element. Use it
114
- * inside a component that wraps a single form field whose root is NOT
115
- * the input itself, such as a labelled row that renders `<label>`
116
- * around the input.
117
- *
118
- * ```vue
119
- * <!-- Parent -->
120
- * <MyInput v-register="form.register('email')" />
121
- *
122
- * <!-- MyInput.vue -->
123
- * <script setup lang="ts">
124
- * import { useRegister } from 'attaform'
125
- * const rv = useRegister()
126
- * </script>
127
- *
128
- * <template>
129
- * <label class="field">
130
- * <span>Email</span>
131
- * <input v-register="rv" />
132
- * </label>
133
- * </template>
134
- * ```
135
- *
136
- * The return is a hybrid: `v-register="rv"` hands the directive the
137
- * parent's own `RegisterValue`, and `rv.path` / `rv.segments` /
138
- * `rv.formKey` / `rv.innerRef` read directly in script setup with no
139
- * `.value` unwrap. Reads inside a `computed` or `watchEffect` re-run
140
- * when the parent rebinds to a different path.
141
- *
142
- * Modifiers on the parent's binding apply at the inner control, so
143
- * `<MyInput v-register.trim="form.register('email')" />` trims there,
144
- * together with any the wrapper writes on its own `v-register`.
145
- *
146
- * When no parent bound, every field reads `undefined`, so reach for
147
- * `rv?.path`. `v-register="rv"` is still safe. A wrapper that is meant
148
- * to render both with and without a form declares it with
149
- * `useRegister({ optional: true })` and branches on `rv?.isBound`.
150
- *
151
- * When the wrapper's root IS the input, Vue's attribute fallthrough
152
- * already binds it and `useRegister` is unnecessary. For a wrapper
153
- * that binds several fields, use `injectForm<Form>(key?)` and call
154
- * `ctx.register(...)` directly.
155
- */
156
- declare function useRegister<V = unknown>(options?: UseRegisterOptions): UseRegisterReturn<V> | undefined;
157
-
158
4
  type DynamicPathSweep = {
159
5
  /**
160
6
  * Note that a cache has just stored an entry for `segments` under
@@ -286,6 +132,13 @@ type FormStore<F extends GenericForm, G extends GenericForm = F> = {
286
132
  readonly pathSweep: DynamicPathSweep;
287
133
  readonly formKey: FormKey;
288
134
  readonly form: Ref<F>;
135
+ /**
136
+ * Per-path field records: one per leaf from construction, and one per
137
+ * container, the root (`[]`) included, once a directive binds to it or a
138
+ * write takes leaves away beneath it. A container's own record holds the
139
+ * stamp of that write and the interaction history of the descendants it
140
+ * took; see `stampContainers` and `foldInteractionHistory`.
141
+ */
289
142
  readonly fields: Map<PathKey, FieldRecord>;
290
143
  /**
291
144
  * The tagged error store: one cell per error-bearing path, each segregating
@@ -610,6 +463,13 @@ type FormStore<F extends GenericForm, G extends GenericForm = F> = {
610
463
  */
611
464
  setValueAtPath(path: Path, value: unknown, meta?: WriteMeta): boolean;
612
465
  getValueAtPath(path: Path): unknown;
466
+ /**
467
+ * The value `path` started as, for a container's `original`: the baseline
468
+ * at `path`, each array index followed to the slot its element held there.
469
+ * `undefined` for an element added since and for a path the baseline lacks.
470
+ * Frozen, since every read shares it.
471
+ */
472
+ getOriginalAtPath(path: Path): unknown;
613
473
  /**
614
474
  * Stable identity for the array element at `path`, the token the array
615
475
  * engine maintains across structural mutations. Empty for anything that is
@@ -641,6 +501,31 @@ type FormStore<F extends GenericForm, G extends GenericForm = F> = {
641
501
  * empty is skipped.
642
502
  */
643
503
  restoreErrorCells(entries: ReadonlyArray<readonly [PathKey, ErrorCell]>): void;
504
+ /**
505
+ * The per-element bookkeeping as it stands, for a history position to put
506
+ * back with its value (see `StructureSnapshot`). The entries are copied,
507
+ * and the records they hold are never mutated in place, so the snapshot
508
+ * stays detached from the live store.
509
+ */
510
+ captureStructure(): StructureSnapshot;
511
+ /**
512
+ * Put a `captureStructure()` back, before the value it was taken with is
513
+ * restored, so the write that restores it compares each row against its
514
+ * own baseline. Every reading it can change is woken.
515
+ *
516
+ * The field records follow their rows: each of `remaps` moves the records
517
+ * under its array's elements, in order, handing a removed element's
518
+ * history to the array and its ancestors, and each of `returning`, a
519
+ * record of a row the restore brings back, takes its place at its path,
520
+ * its element not yet on the page.
521
+ */
522
+ restoreStructure(snapshot: StructureSnapshot, remaps: ReadonlyArray<readonly [Path, IndexRemap]>, returning: readonly FieldRecord[]): void;
523
+ /**
524
+ * Hears the records a removal releases while a history module is attached,
525
+ * so an undo can bring a row back with its interaction history. Set by the
526
+ * module, so a form without one never carries it.
527
+ */
528
+ noteReleased?: ((records: readonly FieldRecord[]) => void) | undefined;
644
529
  /**
645
530
  * Merged read of the cell at `path`, in schema -> blank -> user order:
646
531
  * structural validation first, the synthesized blank entry between, user
@@ -695,18 +580,19 @@ type FormStore<F extends GenericForm, G extends GenericForm = F> = {
695
580
  */
696
581
  markInteracted(path: Path): void;
697
582
  /**
698
- * Walk every active-variant leaf under `segments` and flip `touched: true`.
699
- * Powers `form.touch(path?)`. Idempotent, and touches neither value nor
700
- * `focused` / `blurred`, nor does it trigger validation.
583
+ * Walk every active-variant leaf under `segments`, and every container there
584
+ * holding no leaf, and flip `touched: true`. Powers `form.touch(path?)`.
585
+ * Idempotent, and touches neither value nor `focused` / `blurred`, nor does
586
+ * it trigger validation.
701
587
  */
702
588
  touchAtPath(segments: Path): void;
703
589
  /**
704
- * Walk every active-variant leaf under `segments` and flip the whole
705
- * interaction ladder (`touched` / `interacted` / `blurredAfterInteraction`),
706
- * as though the user had focused, edited and left each one. Powers
707
- * `form.interact(path?)`. Idempotent, and leaves value / `focused` /
708
- * `blurred` alone. Returns whether any leaf resolved, so the caller can skip
709
- * validation and dev-warn on an empty path.
590
+ * Walk every active-variant leaf under `segments`, and every container there
591
+ * holding no leaf, and flip the whole interaction ladder (`touched` /
592
+ * `interacted` / `blurredAfterInteraction`), as though the user had focused,
593
+ * edited and left each one. Powers `form.interact(path?)`. Idempotent, and
594
+ * leaves value / `focused` / `blurred` alone. Returns whether anything
595
+ * resolved, so the caller can skip validation on an unresolved path.
710
596
  */
711
597
  interactAtPath(segments: Path): boolean;
712
598
  /**
@@ -729,18 +615,19 @@ type FormStore<F extends GenericForm, G extends GenericForm = F> = {
729
615
  * insert or removal) against its construction / reset baseline. The
730
616
  * structural half of `dirty`: per-element baselines travel with their
731
617
  * element across a mutation, so a positional value comparison alone cannot
732
- * see the shape change.
618
+ * see the shape change. `key` is the canonical key of `path`.
733
619
  */
734
- hasStructuralChangeUnder(path: Path): boolean;
620
+ hasStructuralChangeUnder(path: Path, key: PathKey): boolean;
735
621
  /**
736
- * Whether a baseline-present container under `path` was replaced wholesale
737
- * by a non-container (`setValue('profile', undefined)` and the like) and is
738
- * still absent. The other half of removal-driven `dirty`: every leaf in such
739
- * a subtree vanishes at once, so neither the present-leaf walk nor the array
740
- * tracker sees the loss. Self-filters by current liveness, so a refilled
741
- * path stops counting.
622
+ * Whether a path under `path` that held a real baseline value was removed
623
+ * and is still absent: a dropped key, an emptied container, a row a
624
+ * wholesale write took away. The other half of removal-driven `dirty`: the
625
+ * present-leaf walk never visits an absent leaf, and the array tracker
626
+ * follows only array lengths and orders. A path a later write brought back
627
+ * stops counting, and the present-leaf walk judges it instead. `key` is the
628
+ * canonical key of `path`.
742
629
  */
743
- hasRemovedSubtreeUnder(path: Path): boolean;
630
+ hasRemovedSubtreeUnder(path: Path, key: PathKey): boolean;
744
631
  getFieldRecord(path: Path): FieldRecord | undefined;
745
632
  /**
746
633
  * Cancel every in-flight field-level validation run: clear the timers of
@@ -849,6 +736,160 @@ type FormStore<F extends GenericForm, G extends GenericForm = F> = {
849
736
  dispose(): void;
850
737
  };
851
738
 
739
+ /** Options accepted by `injectForm` in place of a bare key string. */
740
+ type InjectFormInput = {
741
+ readonly key?: FormKey;
742
+ /**
743
+ * Set by the Vite transform when this `injectForm` call site sits in
744
+ * a component whose template / script reads the form's reactive
745
+ * state. On the server, this enqueues the form for SSR prefetch and
746
+ * wires `onServerPrefetch` so the descendant awaits the activation
747
+ * promise before its render emits HTML.
748
+ *
749
+ * @internal Transform-emitted. Manual use is the documented escape
750
+ * hatch when the transform can't reach the reference (dynamic
751
+ * property access, untransformed bundlers).
752
+ */
753
+ readonly __ssrAccessed?: boolean;
754
+ };
755
+ /**
756
+ * Reach an existing form from a descendant component without threading
757
+ * it through props. The counterpart to `useForm`: `useForm` creates and
758
+ * provides, `injectForm` looks up.
759
+ *
760
+ * ```ts
761
+ * // The nearest ancestor's anonymous useForm() call.
762
+ * const form = injectForm<SignupShape>()
763
+ *
764
+ * // A specific form by key, from anywhere in the app.
765
+ * const cart = injectForm<CartShape>('cart')
766
+ * ```
767
+ *
768
+ * A keyed call is a registry lookup, so it is independent of
769
+ * component-tree position. A no-key call takes the closest ambient
770
+ * ancestor and inherits its `formInstanceId`; only an anonymous
771
+ * `useForm()` fills that ambient slot, so a keyed form is reachable
772
+ * only by its key.
773
+ *
774
+ * Returns `null` when nothing matches, so narrow before use:
775
+ *
776
+ * ```ts
777
+ * const form = injectForm<Shape>('signup')
778
+ * if (!form) return
779
+ * form.register('email')
780
+ * ```
781
+ *
782
+ * A keyed miss warns in dev with the registered keys and the call site.
783
+ * Pass the `Form` generic explicitly, since Vue's provide/inject erases
784
+ * generics. The form stays alive for this component's lifetime and is
785
+ * cleaned up once every consumer has unmounted.
786
+ *
787
+ * Components whose nearest `useForm`-calling ancestor is the same one get
788
+ * the same form object for the same form: `injectForm('cart')` in two of
789
+ * its descendants returns one form, with one set of field caches.
790
+ */
791
+ declare function injectForm<Form extends GenericForm, GetValueFormType extends GenericForm = Form>(input?: FormKey | InjectFormInput): UseFormReturnType<Form, GetValueFormType> | null;
792
+
793
+ /**
794
+ * Return type of `useRegister()`. Hybrid of `RegisterValue<V>` (so
795
+ * `rv.path` / `rv.segments` / `rv.formKey` etc. work directly in
796
+ * script setup) and `Ref<RegisterValue<V> | undefined>` (so Vue's
797
+ * template auto-unwrap surfaces the underlying RV to `v-register`
798
+ * and the directive's path-migration diff sees the real RV across
799
+ * renders).
800
+ *
801
+ * The two surfaces don't clash at the type level: `RegisterValue`
802
+ * doesn't carry a `value` field, and `Ref<T>`'s `value: T` becomes
803
+ * the hybrid's only `.value`. Older code that read `rv.value?.path`
804
+ * keeps working; new code can write `rv.path` directly.
805
+ */
806
+ type UseRegisterReturn<V = unknown> = RegisterValue<V> & Ref<RegisterValue<V> | undefined> & {
807
+ /**
808
+ * Whether a parent has actually bound this wrapper. `false` until a
809
+ * `RegisterValue` lands, `true` once one has, and reactive in both
810
+ * directions: a parent that binds on a later render flips it, and
811
+ * so does one that stops binding.
812
+ *
813
+ * This is the test a dual-mode wrapper reaches for, and the reason
814
+ * it exists as a field rather than a truthiness check on the
815
+ * return: the composable cannot answer "is this bound" at setup
816
+ * time. A parent is free to bind on a later render
817
+ * (`v-register="ready ? form.register('x') : undefined"`), and
818
+ * `useRegister` has to keep serving that, so it can never return a
819
+ * plain `undefined` for "unbound right now". Script-setup code
820
+ * reading `rv` directly sees the hybrid Proxy, which is an object
821
+ * and therefore always truthy; a template sees the unwrapped ref,
822
+ * which IS `undefined` when unbound. `rv?.isBound` reads the same
823
+ * on both sides (#620).
824
+ */
825
+ readonly isBound: boolean;
826
+ };
827
+ /**
828
+ * Options for `useRegister`.
829
+ */
830
+ type UseRegisterOptions = {
831
+ /**
832
+ * Declares that this wrapper is used both with and without a form, so
833
+ * a render with no parent binding is intended rather than a mistake.
834
+ * Silences the unbound diagnostic; nothing else changes.
835
+ *
836
+ * The diagnostic exists because a single-mode wrapper whose parent
837
+ * forgot `v-register` renders a field that looks fine and stores
838
+ * nothing, which is an expensive afternoon. Once a wrapper is
839
+ * deliberately dual-mode, the same signal fires on correct code and
840
+ * tells the author to do something wrong, so this is the one fact
841
+ * that separates the two cases and only the author has it (#620).
842
+ *
843
+ * Pair it with `rv?.isBound` to branch on what actually happened.
844
+ */
845
+ readonly optional?: boolean;
846
+ };
847
+ /**
848
+ * Re-bind a parent's `v-register` onto an inner native element. Use it
849
+ * inside a component that wraps a single form field whose root is NOT
850
+ * the input itself, such as a labelled row that renders `<label>`
851
+ * around the input.
852
+ *
853
+ * ```vue
854
+ * <!-- Parent -->
855
+ * <MyInput v-register="form.register('email')" />
856
+ *
857
+ * <!-- MyInput.vue -->
858
+ * <script setup lang="ts">
859
+ * import { useRegister } from 'attaform'
860
+ * const rv = useRegister()
861
+ * </script>
862
+ *
863
+ * <template>
864
+ * <label class="field">
865
+ * <span>Email</span>
866
+ * <input v-register="rv" />
867
+ * </label>
868
+ * </template>
869
+ * ```
870
+ *
871
+ * The return is a hybrid: `v-register="rv"` hands the directive the
872
+ * parent's own `RegisterValue`, and `rv.path` / `rv.segments` /
873
+ * `rv.formKey` / `rv.innerRef` read directly in script setup with no
874
+ * `.value` unwrap. Reads inside a `computed` or `watchEffect` re-run
875
+ * when the parent rebinds to a different path.
876
+ *
877
+ * Modifiers on the parent's binding apply at the inner control, so
878
+ * `<MyInput v-register.trim="form.register('email')" />` trims there,
879
+ * together with any the wrapper writes on its own `v-register`.
880
+ *
881
+ * When no parent bound, every field reads `undefined`, so reach for
882
+ * `rv?.path`. `v-register="rv"` is still safe. A wrapper that is meant
883
+ * to render both with and without a form declares it with
884
+ * `useRegister({ optional: true })` and branches on `rv?.isBound`.
885
+ *
886
+ * When the wrapper's root IS the input, Vue's attribute fallthrough
887
+ * already binds it and `useRegister` is unnecessary. For a wrapper
888
+ * that binds several fields, use `injectForm<Form>(key?)` and call
889
+ * `ctx.register(...)` directly.
890
+ */
891
+ declare function useRegister<V = unknown>(options?: UseRegisterOptions): UseRegisterReturn<V> | undefined;
892
+
852
893
  /**
853
894
  * Public types for `useWizard`, the multistep-form orchestrator.
854
895
  *