@vtex/faststore 0.0.5 → 0.0.6

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 (232) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/dist/actions.json +181 -0
  3. package/dist/catalog.json +1543 -495
  4. package/dist/client.d.ts +2 -1
  5. package/dist/client.js +2 -1
  6. package/dist/components.d.ts +28 -7
  7. package/dist/components.js +31 -12
  8. package/dist/docs/access-code-sign-in.md +1 -1
  9. package/dist/docs/address-summary.md +24 -3
  10. package/dist/docs/alert-dialog.md +204 -0
  11. package/dist/docs/billing-address.md +284 -0
  12. package/dist/docs/card-entry.md +250 -0
  13. package/dist/docs/cardholder-document.md +142 -0
  14. package/dist/docs/client-preferences-fieldset.md +148 -0
  15. package/dist/docs/client-preferences.md +2 -1
  16. package/dist/docs/client-profile-fieldset.md +184 -0
  17. package/dist/docs/client-profile.md +1 -1
  18. package/dist/docs/document-field.md +58 -4
  19. package/dist/docs/field.md +317 -0
  20. package/dist/docs/form.md +307 -0
  21. package/dist/docs/identity-edit.md +6 -6
  22. package/dist/docs/identity-summary.md +5 -5
  23. package/dist/docs/installments.md +248 -0
  24. package/dist/docs/my-account-profile.md +1 -0
  25. package/dist/docs/order-placed-payment-summary.md +138 -0
  26. package/dist/docs/payment-method-badges.md +232 -0
  27. package/dist/docs/payment-methods.md +1 -0
  28. package/dist/docs/payment.md +218 -0
  29. package/dist/docs/phone-field.md +58 -4
  30. package/dist/docs/place-order.md +157 -0
  31. package/dist/docs/save-card.md +186 -0
  32. package/dist/docs/saved-cards.md +218 -0
  33. package/dist/docs/select.md +465 -0
  34. package/dist/docs/shipping-address.md +110 -258
  35. package/dist/index.d.ts +4 -1
  36. package/dist/internal/client.d.ts +3 -1
  37. package/dist/internal/client.js +3 -1
  38. package/dist/matter/access-code-sign-in/access-code-sign-in.js +9 -8
  39. package/dist/matter/access-code-sign-in/access-code-sign-in.js.map +1 -1
  40. package/dist/matter/billing-address/billing-address.d.ts +165 -0
  41. package/dist/matter/billing-address/billing-address.js +408 -0
  42. package/dist/matter/billing-address/billing-address.js.map +1 -0
  43. package/dist/matter/card-entry/card-capture.d.ts +25 -0
  44. package/dist/matter/card-entry/card-capture.js +25 -0
  45. package/dist/matter/card-entry/card-capture.js.map +1 -0
  46. package/dist/matter/card-entry/card-entry.d.ts +120 -0
  47. package/dist/matter/card-entry/card-entry.js +832 -0
  48. package/dist/matter/card-entry/card-entry.js.map +1 -0
  49. package/dist/matter/cardholder-document/cardholder-document.d.ts +45 -0
  50. package/dist/matter/cardholder-document/cardholder-document.js +70 -0
  51. package/dist/matter/cardholder-document/cardholder-document.js.map +1 -0
  52. package/dist/matter/client-preferences/client-preferences-fieldset.d.ts +77 -0
  53. package/dist/matter/client-preferences/client-preferences-fieldset.js +299 -0
  54. package/dist/matter/client-preferences/client-preferences-fieldset.js.map +1 -0
  55. package/dist/matter/client-preferences/client-preferences.d.ts +3 -5
  56. package/dist/matter/client-preferences/client-preferences.js +6 -6
  57. package/dist/matter/client-preferences/client-preferences.js.map +1 -1
  58. package/dist/matter/client-profile/client-profile.d.ts +4 -6
  59. package/dist/matter/client-profile/client-profile.js +9 -6
  60. package/dist/matter/client-profile/client-profile.js.map +1 -1
  61. package/dist/matter/client-profile-form/client-profile-fieldset.d.ts +119 -0
  62. package/dist/matter/client-profile-form/client-profile-fieldset.js +488 -0
  63. package/dist/matter/client-profile-form/client-profile-fieldset.js.map +1 -0
  64. package/dist/matter/identity-edit/identity-edit.js +2 -2
  65. package/dist/matter/identity-edit/identity-edit.js.map +1 -1
  66. package/dist/matter/identity-summary/identity-summary.js +2 -2
  67. package/dist/matter/identity-summary/identity-summary.js.map +1 -1
  68. package/dist/matter/installments/installments-select.d.ts +42 -0
  69. package/dist/matter/installments/installments-select.js +167 -0
  70. package/dist/matter/installments/installments-select.js.map +1 -0
  71. package/dist/matter/installments/installments.d.ts +58 -0
  72. package/dist/matter/installments/installments.js +176 -0
  73. package/dist/matter/installments/installments.js.map +1 -0
  74. package/dist/matter/installments/use-installments-selection.d.ts +29 -0
  75. package/dist/matter/installments/use-installments-selection.js +95 -0
  76. package/dist/matter/installments/use-installments-selection.js.map +1 -0
  77. package/dist/matter/my-account-profile/my-account-profile.js +4 -4
  78. package/dist/matter/my-account-profile/my-account-profile.js.map +1 -1
  79. package/dist/matter/order-placed-payment-summary/order-placed-payment-summary.d.ts +63 -0
  80. package/dist/matter/order-placed-payment-summary/order-placed-payment-summary.js +305 -0
  81. package/dist/matter/order-placed-payment-summary/order-placed-payment-summary.js.map +1 -0
  82. package/dist/matter/payment/payment-card-capture.d.ts +19 -0
  83. package/dist/matter/payment/payment-card-capture.js +129 -0
  84. package/dist/matter/payment/payment-card-capture.js.map +1 -0
  85. package/dist/matter/payment/payment-controller.d.ts +24 -0
  86. package/dist/matter/payment/payment-controller.js +107 -0
  87. package/dist/matter/payment/payment-controller.js.map +1 -0
  88. package/dist/matter/payment/payment-method-badges.d.ts +57 -0
  89. package/dist/matter/payment/payment-method-badges.js +192 -0
  90. package/dist/matter/payment/payment-method-badges.js.map +1 -0
  91. package/dist/matter/payment/payment.d.ts +89 -0
  92. package/dist/matter/payment/payment.js +263 -0
  93. package/dist/matter/payment/payment.js.map +1 -0
  94. package/dist/matter/payment/use-payment-method-selection.d.ts +302 -0
  95. package/dist/matter/payment/use-payment-method-selection.js +458 -0
  96. package/dist/matter/payment/use-payment-method-selection.js.map +1 -0
  97. package/dist/matter/payment/use-payment-submit-gate.d.ts +111 -0
  98. package/dist/matter/payment/use-payment-submit-gate.js +144 -0
  99. package/dist/matter/payment/use-payment-submit-gate.js.map +1 -0
  100. package/dist/matter/place-order/place-order.d.ts +74 -0
  101. package/dist/matter/place-order/place-order.js +379 -0
  102. package/dist/matter/place-order/place-order.js.map +1 -0
  103. package/dist/matter/product-list/product-list.d.ts +1 -0
  104. package/dist/matter/product-list/product-list.js.map +1 -1
  105. package/dist/matter/product-shelf/product-shelf.d.ts +1 -0
  106. package/dist/matter/product-shelf/product-shelf.js +2 -0
  107. package/dist/matter/product-shelf/product-shelf.js.map +1 -1
  108. package/dist/matter/region/region.js +1 -1
  109. package/dist/matter/region/region.js.map +1 -1
  110. package/dist/matter/save-card/save-card.d.ts +49 -0
  111. package/dist/matter/save-card/save-card.js +188 -0
  112. package/dist/matter/save-card/save-card.js.map +1 -0
  113. package/dist/matter/saved-cards/saved-cards.d.ts +79 -0
  114. package/dist/matter/saved-cards/saved-cards.js +387 -0
  115. package/dist/matter/saved-cards/saved-cards.js.map +1 -0
  116. package/dist/matter/shipping-address/country-descriptors.js +20 -10
  117. package/dist/matter/shipping-address/country-descriptors.js.map +1 -1
  118. package/dist/matter/shipping-address/shipping-address.d.ts +113 -174
  119. package/dist/matter/shipping-address/shipping-address.js +480 -791
  120. package/dist/matter/shipping-address/shipping-address.js.map +1 -1
  121. package/dist/matter/variant-selector/variant-selector.js +1 -1
  122. package/dist/particles/address-summary/address-summary.d.ts +24 -0
  123. package/dist/particles/address-summary/address-summary.js +83 -2
  124. package/dist/particles/address-summary/address-summary.js.map +1 -1
  125. package/dist/particles/alert-dialog/alert-dialog.d.ts +68 -0
  126. package/dist/particles/alert-dialog/alert-dialog.js +187 -0
  127. package/dist/particles/alert-dialog/alert-dialog.js.map +1 -0
  128. package/dist/particles/checkbox/checkbox.d.ts +7 -3
  129. package/dist/particles/checkbox/checkbox.js +4 -1
  130. package/dist/particles/checkbox/checkbox.js.map +1 -1
  131. package/dist/particles/document-field/document-field.d.ts +48 -5
  132. package/dist/particles/document-field/document-field.js +189 -32
  133. package/dist/particles/document-field/document-field.js.map +1 -1
  134. package/dist/particles/field/bound-field-registry.js +52 -0
  135. package/dist/particles/field/bound-field-registry.js.map +1 -0
  136. package/dist/particles/field/field.d.ts +116 -0
  137. package/dist/particles/field/field.js +497 -6
  138. package/dist/particles/field/field.js.map +1 -1
  139. package/dist/particles/fieldset/fieldset.js +2 -2
  140. package/dist/particles/fieldset/fieldset.js.map +1 -1
  141. package/dist/particles/form/form.d.ts +104 -0
  142. package/dist/particles/form/form.js +485 -0
  143. package/dist/particles/form/form.js.map +1 -0
  144. package/dist/particles/phone-field/phone-field.d.ts +38 -10
  145. package/dist/particles/phone-field/phone-field.js +183 -39
  146. package/dist/particles/phone-field/phone-field.js.map +1 -1
  147. package/dist/particles/postal-code-field/postal-code-field.js +2 -2
  148. package/dist/particles/postal-code-field/postal-code-field.js.map +1 -1
  149. package/dist/particles/select/field-select.d.ts +43 -0
  150. package/dist/particles/select/field-select.js +150 -0
  151. package/dist/particles/select/field-select.js.map +1 -0
  152. package/dist/particles/select/select.d.ts +2 -0
  153. package/dist/particles/select/select.js.map +1 -1
  154. package/dist/particles/shared/compose-refs.js +21 -0
  155. package/dist/particles/shared/compose-refs.js.map +1 -0
  156. package/dist/schema.graphql +701 -2
  157. package/dist/sdk/actions/analytics-action-provider.d.ts +15 -0
  158. package/dist/sdk/actions/analytics-action-provider.js +19 -0
  159. package/dist/sdk/actions/analytics-action-provider.js.map +1 -0
  160. package/dist/sdk/actions/index.d.ts +3 -1
  161. package/dist/sdk/actions/index.js +3 -1
  162. package/dist/sdk/actions/payment-action-provider.d.ts +22 -0
  163. package/dist/sdk/actions/payment-action-provider.js +26 -0
  164. package/dist/sdk/actions/payment-action-provider.js.map +1 -0
  165. package/dist/sdk/actions/types.d.ts +6 -2
  166. package/dist/sdk/analytics/analytics-destination.d.ts +43 -0
  167. package/dist/sdk/analytics/analytics-destination.js +90 -0
  168. package/dist/sdk/analytics/analytics-destination.js.map +1 -0
  169. package/dist/sdk/analytics/use-analytics-actions.d.ts +57 -0
  170. package/dist/sdk/analytics/use-analytics-actions.js +108 -0
  171. package/dist/sdk/analytics/use-analytics-actions.js.map +1 -0
  172. package/dist/sdk/cart/use-cart-actions.js +1 -1
  173. package/dist/sdk/data/entity-resolution-context.js +31 -0
  174. package/dist/sdk/data/entity-resolution-context.js.map +1 -0
  175. package/dist/sdk/data/entity-subscriber.js +18 -8
  176. package/dist/sdk/data/entity-subscriber.js.map +1 -1
  177. package/dist/sdk/data/index.js +1 -0
  178. package/dist/sdk/data/page-reactive-data-root.d.ts +36 -0
  179. package/dist/sdk/data/page-reactive-data-root.js +4 -2
  180. package/dist/sdk/data/page-reactive-data-root.js.map +1 -1
  181. package/dist/sdk/data/reactive-data-provider.js +7 -3
  182. package/dist/sdk/data/reactive-data-provider.js.map +1 -1
  183. package/dist/sdk/data/reactive-list-item-data-provider.js +12 -4
  184. package/dist/sdk/data/reactive-list-item-data-provider.js.map +1 -1
  185. package/dist/sdk/data/use-reactive-data-settled.js +39 -0
  186. package/dist/sdk/data/use-reactive-data-settled.js.map +1 -0
  187. package/dist/sdk/data/use-reactive-entity.js +5 -1
  188. package/dist/sdk/data/use-reactive-entity.js.map +1 -1
  189. package/dist/sdk/generated/fragment-types.js +2 -0
  190. package/dist/sdk/generated/fragment-types.js.map +1 -1
  191. package/dist/sdk/generated/graphql.js.map +1 -1
  192. package/dist/sdk/i18n/messages/en-US.js +3 -0
  193. package/dist/sdk/i18n/messages/en-US.json +3 -0
  194. package/dist/sdk/i18n/messages/en-US.json.d.ts +3 -0
  195. package/dist/sdk/i18n/messages/es-ES.js +3 -0
  196. package/dist/sdk/i18n/messages/es-ES.json +3 -0
  197. package/dist/sdk/i18n/messages/pt-BR.js +3 -0
  198. package/dist/sdk/i18n/messages/pt-BR.json +3 -0
  199. package/dist/sdk/identity/use-identity-actions.js +3 -2
  200. package/dist/sdk/identity/use-identity-actions.js.map +1 -1
  201. package/dist/sdk/payment/gateway-origin.js +35 -0
  202. package/dist/sdk/payment/gateway-origin.js.map +1 -0
  203. package/dist/sdk/payment/gateway.d.ts +85 -0
  204. package/dist/sdk/payment/gateway.js +183 -0
  205. package/dist/sdk/payment/gateway.js.map +1 -0
  206. package/dist/sdk/payment/operations.d.ts +52 -0
  207. package/dist/sdk/payment/operations.js +223 -0
  208. package/dist/sdk/payment/operations.js.map +1 -0
  209. package/dist/sdk/payment/security-code-mask.js +71 -0
  210. package/dist/sdk/payment/security-code-mask.js.map +1 -0
  211. package/dist/sdk/payment/submission-registry.d.ts +33 -0
  212. package/dist/sdk/payment/submission-registry.js +58 -0
  213. package/dist/sdk/payment/submission-registry.js.map +1 -0
  214. package/dist/sdk/payment/use-payment-actions.d.ts +78 -0
  215. package/dist/sdk/payment/use-payment-actions.js +276 -0
  216. package/dist/sdk/payment/use-payment-actions.js.map +1 -0
  217. package/dist/sdk/payment/use-place-order.d.ts +65 -0
  218. package/dist/sdk/payment/use-place-order.js +640 -0
  219. package/dist/sdk/payment/use-place-order.js.map +1 -0
  220. package/dist/sdk/shipping/use-shipping-actions.js +1 -1
  221. package/dist/utils/loggable-error-name.js +49 -0
  222. package/dist/utils/loggable-error-name.js.map +1 -0
  223. package/dist/utils/resolve-authored-copy.js +18 -0
  224. package/dist/utils/resolve-authored-copy.js.map +1 -0
  225. package/dist/utils/substitute-template.js +33 -0
  226. package/dist/utils/substitute-template.js.map +1 -0
  227. package/package.json +2 -1
  228. package/src/styles/tailwind.css +34 -0
  229. package/dist/docs/client-profile-form.md +0 -192
  230. package/dist/matter/client-profile-form/client-profile-form.d.ts +0 -126
  231. package/dist/matter/client-profile-form/client-profile-form.js +0 -662
  232. package/dist/matter/client-profile-form/client-profile-form.js.map +0 -1
@@ -0,0 +1,317 @@
1
+ # Field
2
+
3
+ **Category:** layout
4
+ **Purpose:** Composable anatomy for a form field — label, input, description, error, and validity feedback — that behaves standalone or binds to a react-hook-form field through a context contract.
5
+
6
+ ---
7
+
8
+ ## When to use
9
+
10
+ - Any form field: a shipping-address line, a profile field, a checkout opt-in, a standalone search box.
11
+ - Inside a bound Control (a matter component that seeds `FieldBinding` from an entity's data) when the
12
+ anatomy needs to be reordered, restyled, or have a part omitted, without a new component.
13
+ - As the closed anatomy it already is today — `Field.Root` + `Field.Label` + `Field.Control` (+
14
+ `Field.Description` / `Field.Error`) with no binding above it — for any field a matter renders itself.
15
+
16
+ ## When NOT to use
17
+
18
+ - A boolean field's control — use `Field.Checkbox` (drives the shipped `Checkbox` particle), not
19
+ `Field.Control` (renders a native `<input>`).
20
+ - A field whose value is picked from a list — out of scope here; `FieldSelect` (in the select particle) is the bound
21
+ part for that, and the contract's `options` member is reserved for it.
22
+ - A submit action or a whole fieldset's write — those are `FormSubmit`/the fieldset tasks, not this
23
+ particle.
24
+
25
+ ---
26
+
27
+ ## Composition
28
+
29
+ `Field.Root` is the anatomy's entry point. The five other shipped parts, plus the new `Field.Checkbox`,
30
+ are children (directly or nested) of it — none of them require `Field.Root` at the type level, but every
31
+ shipped composition places them there for the label/control/error ARIA wiring Base UI owns.
32
+
33
+ ```
34
+ Field.Root (providesContext: 'field')
35
+ ├── Field.Label (requiresContext: 'field')
36
+ ├── Field.Control (requiresContext: 'field')
37
+ ├── Field.Description (requiresContext: 'field')
38
+ ├── Field.Error (requiresContext: 'field')
39
+ ├── Field.Validity (requiresContext: 'field'; render-prop, reads Base UI's validity state)
40
+ └── Field.Checkbox (requiresContext: 'field'; new — wraps Checkbox.Root)
41
+ ```
42
+
43
+ `providesContext`/`requiresContext` are the compiler's static check for a *schema-authored* composition —
44
+ they say "some ancestor must provide `field`", not "`FieldBinding` is that ancestor". At runtime,
45
+ `FieldBinding` is the only thing that ever supplies the context; `Field.Root` provides the *contract*
46
+ value for its compiler declaration but pushes no React context of its own.
47
+
48
+ Child order reflects authored order — an author reorders `Field.Label` and `Field.Control`, or places an
49
+ icon between them, and the DOM follows. No part receives the field name as a prop; whatever an author
50
+ composes inside a bound Control edits that Control's field, and there is no way to spell a different one.
51
+
52
+ ### The field contract (`FieldBinding` / `useField`)
53
+
54
+ `FieldBinding` is a **code-level export, never catalogued** — it carries no DOM of its own and never
55
+ appears in a schema. A bound Control (shipped per-entity by later tasks, e.g. a shipping-address street
56
+ field) renders it internally, seeded from the entity's data:
57
+
58
+ ```tsx
59
+ <FieldBinding name="shippingAddress.street" label="Street" defaultValue={address.street}>
60
+ <Field.Root>
61
+ <Field.Label />
62
+ <Field.Control />
63
+ <Field.Error />
64
+ </Field.Root>
65
+ </FieldBinding>
66
+ ```
67
+
68
+ Every part below a `FieldBinding` reads the contract via `useField()` and, when a react-hook-form
69
+ `FormProvider` is also present, renders its **bound** behaviour (reads/writes through react-hook-form).
70
+ With no contract, or a contract but no `FormProvider`, every part renders its plain, unbound behaviour —
71
+ identical to a `Field.Root` composed with no binding at all. `useField()` returns `null` rather than
72
+ throwing when no contract is present, because eight particles and four matters render `Field` parts with
73
+ no contract today.
74
+
75
+ The contract carries **no live value** — only `name` and static facts (`label`, `placeholder`,
76
+ `defaultValue`, `rule`, `options`, and the input semantics). It never changes while the shopper types, so
77
+ typing re-renders only the part subscribed to that value through react-hook-form, not every part under the
78
+ binding.
79
+
80
+ `defaultValue` is not adopted once and forgotten: the bound part re-seeds the field whenever it re-renders
81
+ with a `defaultValue` that differs from the one already applied, provided react-hook-form reports the field
82
+ clean. A field the shopper has typed into is left alone — a changed `defaultValue` reaching a dirty field is
83
+ skipped, not queued, so it is picked up on a later render once the field becomes clean again.
84
+
85
+ A bound part narrows the contract's `defaultValue` to its own value type before registering it —
86
+ `useBoundField`'s `shape.normalise` (`TEXT_FIELD_VALUE` coerces a non-string to `''`, `BOOLEAN_FIELD_VALUE`
87
+ runs it through `Boolean(...)`) — and that same narrowed value is also the dirty baseline react-hook-form
88
+ compares against, never the raw seed.
89
+
90
+ ### The fieldset seam (`useFieldsetField` / `useFieldsetFieldNames` / `useNoAncestorFieldset`)
91
+
92
+ `useFieldsetField(namespace, key)` always returns `${namespace}.${key}`, and registers that full name with
93
+ the enclosing fieldset registry for as long as the caller is mounted. With no registry above the caller,
94
+ that registration is a no-op — it returns the joined name and throws nothing. With a registry above that
95
+ declares a **different** namespace, the call **throws during render** instead of registering: the message
96
+ names both the registry's own namespace and the full name the call was refused under, so it is the control
97
+ that asked for the wrong namespace whose render fails first. That throw is uncaught — nothing in this
98
+ repository installs an `ErrorBoundary` — so it unwinds the whole render past the fieldset: a sibling
99
+ composed after the offending control never reaches its own render either, and the subtree comes back
100
+ blank. Naming the offender in the message is what tells a reader which control to fix; it does not confine
101
+ the failure to that control alone. A bound Control calls it to tell its fieldset which field name it
102
+ mounted, so the fieldset can later submit exactly the fields on screen.
103
+
104
+ A fieldset builds the registry it provides in one of two ways. `useFieldsetFieldNames(namespace)` is the
105
+ built-in path: it returns `{ registry, getFieldNames }`. `registry.registerFieldName` refcounts per name —
106
+ two callers of one name, however each arrived, collapse to one entry that is released only once the last
107
+ holder releases, and the release function it hands back is at-most-once (calling it again after the first
108
+ call does nothing). `getFieldNames()` lists every name currently held, in first-registration order. The
109
+ registry object is memoised on `namespace`, so a fieldset that re-points at a different namespace hands out
110
+ a new registry object, and every control still mounted re-registers under the new namespace in the same
111
+ commit.
112
+
113
+ The other way to build a registry is providing `FieldsetFieldContext` directly with a hand-written
114
+ `{ namespace, registerFieldName }` object — the context is exported for exactly that, and nothing requires
115
+ going through the hook. A control that calls `useFieldsetField` gets the namespace throw either way, since
116
+ that check reads `registry.namespace` and lives in `useFieldsetField` itself, never in `registerFieldName` —
117
+ so the namespace check is never lost for a normal control; only a caller that bypasses `useFieldsetField`
118
+ and reaches `registerFieldName` directly escapes it, on either kind of registry. Refcounting for that same
119
+ direct caller depends on which registry it reached: on one built by `useFieldsetFieldNames`, it is
120
+ refcounted exactly like a caller that arrived through `useFieldsetField` — two callers of one name collapse
121
+ to one entry either way (see above). On a hand-written registry, it gets whatever that particular
122
+ `registerFieldName` implementation wrote, which may not refcount at all. The structural type,
123
+ `{ namespace: string; registerFieldName: (name: string) => () => void }`, does not enforce either property
124
+ at the type level — but the contract's own documentation requires both, as provider obligations:
125
+ refcounting, and that the returned function releases at most once.
126
+
127
+ Every fieldset also calls `useNoAncestorFieldset(componentName)` once, a one-line guard that throws during
128
+ render when it finds an ancestor fieldset already providing field registration for the subtree — fieldsets
129
+ compose as siblings under one `FormRoot`, never nested. `ShippingAddressFieldset`, `ClientProfileFieldset`
130
+ and `ClientPreferencesFieldset` all provide their registry via `useFieldsetFieldNames` today.
131
+
132
+ ---
133
+
134
+ ## Components
135
+
136
+ ### Field.Root
137
+
138
+ Form field container that connects a label, input, and validation messages via ARIA. Declares
139
+ `providesContext: 'field'`.
140
+
141
+ | Prop | Type | Default | Notes |
142
+ |------|------|---------|-------|
143
+ | `disabled` | boolean | `false` | Explicit prop always wins over the contract's `disabled`. |
144
+ | `invalid` | boolean | — | Explicit prop always wins over the value react-hook-form derives when bound. |
145
+ | `name` | string | — | Explicit prop always wins over the contract's `name` when bound. |
146
+
147
+ Bound behaviour: passes the contract's `name` to Base UI's `name`; derives `invalid` from
148
+ react-hook-form's field state and passes it as a prop. `dirty`/`touched` are three-state: while
149
+ react-hook-form has not registered (mounted) the contract's name, both are `undefined`, so Base UI's own
150
+ internal tracking still drives `data-dirty`/`data-touched` for a control it never registers — a raw Base
151
+ UI control placed directly under the bound `Field.Root`, rather than through a `Field.*` adapter. Once
152
+ react-hook-form registers the name, its own dirty/touched state for that name always wins instead, in
153
+ both directions, so `data-dirty`/`data-touched` come off the rendered element as soon as react-hook-form
154
+ reports the field clean/untouched again — this applies identically to `dirty` and `touched`.
155
+
156
+ ### Field.Label
157
+
158
+ Accessible label, associated with `Field.Control`'s input via ARIA. No props beyond `className`/`children`.
159
+
160
+ Bound behaviour: a **childless** `Field.Label` renders the contract's `label`; with children, the children
161
+ win.
162
+
163
+ ### Field.Control
164
+
165
+ Renders a native `<input>` directly — it *is* the input, not a wrapper. No props beyond native input
166
+ attributes.
167
+
168
+ Bound behaviour: registers with react-hook-form using the contract's `name`, `rule` (as `rules`), and
169
+ `defaultValue`; wires `value`, `onChange`, `onBlur`, and `ref` from the resulting controller field. An
170
+ explicit `placeholder`, `disabled`, `readOnly`, `autoComplete`, or `inputMode` prop wins over the
171
+ contract's; `aria-required` is set when the field resolves to required.
172
+
173
+ Requiredness resolves in `useBoundField`, the one hook every bound control passes through, as
174
+ `required = baselineRequired || authorRaise` and nothing else. The fieldset's `rule.required` is the
175
+ baseline; a control may raise a field to required and never lower one. Read-only is deliberately not
176
+ a term: a field that is read-only and required stays required, and an unsatisfiable form is the
177
+ declared baseline's fault. The one resolved value drives the announcement, the validation and the
178
+ error copy together, so they cannot disagree — including a blank check beside `required`, because
179
+ `required` alone treats a space-filled string as present.
180
+
181
+ ### Field.Description
182
+
183
+ Helper text, linked to the control via `aria-describedby`. No props beyond `className`/`children`.
184
+ Unchanged whether bound or not — the contract carries no description text.
185
+
186
+ ### Field.Error
187
+
188
+ Validation error message, linked to the control via `aria-describedby`.
189
+
190
+ | Prop | Type | Default | Notes |
191
+ |------|------|---------|-------|
192
+ | `match` | `boolean \| keyof ValidityState` | — | Unbound path only (see below). |
193
+
194
+ Unbound behaviour: `match` keeps Base UI's documented meaning — `true` always shows the error; a
195
+ `ValidityState` key shows it only when that native flag is set; omitted, it falls back to Base UI's own
196
+ native-validity default.
197
+
198
+ Bound behaviour: renders `children ?? <the react-hook-form error's message>` for the contract's name, and
199
+ renders nothing when there is no error. `match` is not read on this path — visibility is driven by
200
+ react-hook-form's error, not by native constraint validation (which the shipped `Field.Control` never
201
+ opts into: there is no real `required` HTML attribute, only `aria-required`).
202
+
203
+ ### Field.Validity
204
+
205
+ Render-prop slot for custom messages derived from Base UI's own validity state.
206
+
207
+ **Bound behaviour is not transparent.** `Field.Root` only feeds Base UI's `invalid` flag from
208
+ react-hook-form; that flips `validity.valid` to `false`, so `data-invalid` and a bound `Field.Error` both
209
+ react correctly. It does **not** feed `.error`/`.errors` — those come from Base UI's own internal
210
+ `validityData`, which Base UI writes only from its own commit cycle (native constraint validation, or its
211
+ `validate` prop, run on change, on blur, or via an imperative `actionsRef.validate()`). A field with no
212
+ native HTML constraint and no `validate` prop — every shipped bound `Field.Control` — never triggers that
213
+ cycle. A required field left empty and submitted renders `valid: false` with `error: ''`:
214
+
215
+ ```tsx
216
+ <Field.Validity>{(validity) => (validity.validity.valid ? null : <span>{validity.error}</span>)}</Field.Validity>
217
+ // Invalid after submit, but `validity.error` is '' — renders an empty <span>, not the rule's message.
218
+ ```
219
+
220
+ Use `Field.Error` for a bound field's error text — it reads react-hook-form's error directly instead of
221
+ Base UI's own validity data. `Field.Validity` stays useful for `.validity.valid`/`.filled`/`.focused`, and
222
+ for a control that also carries a real native HTML constraint.
223
+
224
+ ### Field.Checkbox
225
+
226
+ New bound part: an adapter over the shipped `Checkbox` particle, not a change to it. Reads the field
227
+ contract, binds through `useBoundField`, and drives `Checkbox.Root`.
228
+
229
+ | Prop | Type | Notes |
230
+ |------|------|-------|
231
+ | `className` | string | Forwarded to `Checkbox.Root`. |
232
+ | `children` | ReactNode | Forwarded to `Checkbox.Root` — compose `Checkbox.Indicator` here for the visual check mark. `Field.Checkbox` renders no default indicator. |
233
+
234
+ Bound behaviour: renders `Checkbox.Root` with `checked={Boolean(field.value)}`,
235
+ `onCheckedChange={field.onChange}`, `onBlur={field.onBlur}` and `ref={field.ref}` — so react-hook-form
236
+ hears the checkbox's blur (a form running `mode: 'onBlur'`/`'onTouched'` treats it like every other
237
+ field) and can move focus to it after a failed submit — seeded from the contract's `defaultValue`/`rule`,
238
+ plus `disabled` from the contract. It also renders `data-fs-field-checkbox=""` on the same host element
239
+ as `Checkbox.Root`'s own `data-fs-checkbox-root=""`, so a brand can target the bound checkbox without
240
+ touching the generic one. Unbound behaviour: renders `Checkbox.Root` with `children` and `className`
241
+ only. `Checkbox.Root` itself gains no `requiresContext` from this pairing — every standalone checkbox
242
+ stays placeable anywhere — but its prop set opened to Base UI's own span props, so `ref`, `onBlur` and
243
+ any `data-*` attribute reach the rendered host element through a rest spread.
244
+
245
+ ---
246
+
247
+ ## Canonical schema example
248
+
249
+ `FieldBinding` and the two hooks are never catalogued, so a **bound** composition can only be authored in
250
+ code — a matter's Control renders `FieldBinding` internally. A schema can only author the **unbound**
251
+ anatomy, which is the same shape that ships on `main` today:
252
+
253
+ ```json
254
+ {
255
+ "email-field": {
256
+ "type": "FieldRoot",
257
+ "style": ["flex", "flex-col", "gap-1"],
258
+ "children": ["email-label", "email-control", "email-description"]
259
+ },
260
+ "email-label": { "type": "FieldLabel", "props": { "children": "Email address" } },
261
+ "email-control": { "type": "FieldControl", "props": { "type": "email", "placeholder": "you@example.com" } },
262
+ "email-description": { "type": "FieldDescription", "props": { "children": "We'll never share your email with anyone." } }
263
+ }
264
+ ```
265
+
266
+ ---
267
+
268
+ ## Common mistakes
269
+
270
+ - **Passing the field name as a prop to a part.** No part accepts one. The only way to name a field is a
271
+ `FieldBinding` above it — whatever an author composes inside a bound Control edits that Control's field.
272
+ - **Expecting the field contract to carry the live value.** It never does, by design (Decision 1 of the
273
+ spec that added binding) — read the value through react-hook-form (`getValues`, `useWatch`), not
274
+ through `useField()`. Do not call `useController` yourself to get at it: it is only ever called once
275
+ per bound part, inside `useBoundField`, and a second call site for the same name would register a
276
+ duplicate binding.
277
+ - **Nesting a bound Control inside another bound Control.** Unsupported; nothing enforces it today.
278
+ - **Using `Field.Control` for a boolean field.** It renders a native `<input>`; use `Field.Checkbox`, which
279
+ drives `Checkbox.Root`.
280
+ - **Passing an unstable `rule` or `options` object to a Control.** The contract memoises on these values —
281
+ a churning identity costs re-renders (every subscriber below the binding re-renders), never correctness.
282
+ - **Expecting `Field.Error`'s `match` prop to do anything on a bound composition.** It is read only on the
283
+ unbound path; a bound `Field.Error`'s visibility is driven by react-hook-form's error for the contract's
284
+ name.
285
+
286
+ ---
287
+
288
+ ## Styling
289
+
290
+ Every part renders its own `data-fs-*` hook and accepts `className`; none ships default styling.
291
+ `Field.Checkbox` additionally renders `data-fs-field-checkbox=""` on the same host element as
292
+ `Checkbox.Root`'s own `data-fs-checkbox-root=""`, so a brand can target the bound checkbox without touching
293
+ the generic one.
294
+
295
+ | Component | What you style | Plausible classes |
296
+ |---|---|---|
297
+ | `FieldRoot` | Stacks label/control/description/error | `flex flex-col gap-1` |
298
+ | `FieldLabel` | Label typography | `text-sm font-medium text-gray-700` |
299
+ | `FieldControl` | The input box; target `data-invalid:` for the error state | `border border-gray-300 rounded-md px-3 py-2 text-sm data-invalid:border-red-400` |
300
+ | `FieldDescription` | Helper text, quieter than the label | `text-xs text-gray-500` |
301
+ | `FieldError` | Error text, typically red | `text-xs text-red-500` |
302
+
303
+ ---
304
+
305
+ ## Related
306
+
307
+ - [[checkbox]] — the generic particle `Field.Checkbox` adapts. `Checkbox.Root` itself stays placeable
308
+ standalone, outside any form; it gains no form-specific requirement from this pairing.
309
+ - [[fieldset]] — the visual fieldset/legend particle. Unrelated to the write-boundary fieldset that
310
+ provides `useFieldsetField`'s registry — `ShippingAddressFieldset`, `ClientProfileFieldset` and
311
+ `ClientPreferencesFieldset`, documented above.
312
+ - [[document-field]], [[phone-field]] — field-shaped composites that read the field contract themselves
313
+ (through `useField()`/`useFormContext()`) rather than through a `Field.*` adapter, because they own
314
+ masking, progressive formatting and their own validators that an adapter has no way to reach past.
315
+ - [[email-field]], [[password-field]], [[number-field]], [[postal-code-field]], [[access-code-field]] —
316
+ particles that already compose `Field.Root` and its parts with their own closed anatomy; still
317
+ unaffected by the field contract until their own tasks convert them.
@@ -0,0 +1,307 @@
1
+ # Form
2
+
3
+ **Category:** action
4
+ **Purpose:** The composable form boundary — one `<form>`, one react-hook-form instance, and a registry of the fieldsets mounted inside it — so a single submit can validate and persist every entity a shopper changed in one press.
5
+
6
+ ---
7
+
8
+ ## When to use
9
+
10
+ - Any composition where a submit needs to persist more than one entity behind one button — a
11
+ checkout step with a name, a phone and a delivery address, for example.
12
+ - As the outermost boundary for a fieldset (a shipping-address fieldset, a profile fieldset —
13
+ shipped by later tasks) — a fieldset does nothing outside a `FormRoot`, because it has nowhere to
14
+ register.
15
+ - Wherever `FormSubmit`, `FormEnterSubmit`, `FormError`, `FormClear` or `FormReset` are needed — all
16
+ five require the context only `FormRoot` provides, and can be placed anywhere inside it, not only as
17
+ its direct children.
18
+
19
+ ## When NOT to use
20
+
21
+ - A single, self-contained write with its own explicit button and no reason to ever compose with
22
+ another entity — nothing in the catalog takes this shape today; every write-capable matter either
23
+ registers a fieldset with `FormRoot` or writes on change, with no component of its own `<form>`.
24
+ - A boolean or list field's own anatomy — that is `Field.Root`/`Field.Checkbox`/`Field.Select` (see
25
+ [[field]]), composed *inside* the fieldset a `FormRoot` wraps, not a `Form` part itself.
26
+ - A visual `<fieldset>`/`<legend>` grouping with no write behaviour — that is `Fieldset.Root`/
27
+ `Fieldset.Legend` (see [[fieldset]]), an unrelated, purely presentational particle sharing the word
28
+ "fieldset" by coincidence.
29
+
30
+ ---
31
+
32
+ ## Composition
33
+
34
+ ```
35
+ FormRoot (providesContext: 'form')
36
+ ├── FormSubmit (requiresContext: 'form')
37
+ ├── FormEnterSubmit (requiresContext: 'form')
38
+ ├── FormError (requiresContext: 'form')
39
+ ├── FormClear (requiresContext: 'form')
40
+ ├── FormReset (requiresContext: 'form')
41
+ └── a fieldset (a later task — registers itself with FormRoot)
42
+ └── a bound Control (shipped by the field contract — see [[field]])
43
+ ```
44
+
45
+ `FormSubmit`, `FormEnterSubmit`, `FormError`, `FormClear` and `FormReset` are siblings of each other and
46
+ of whatever fieldset(s) `FormRoot` wraps — none of them needs to be adjacent to the others, or to the
47
+ fieldset, in the tree. Child order under `FormRoot` reflects visual order: where an author places
48
+ `FormSubmit`/`FormEnterSubmit` relative to the fields is where the button renders.
49
+
50
+ `providesContext`/`requiresContext` are the compiler's static check for a schema-authored page — every
51
+ one of the five parts fails the build with a `context-constraint-violation` diagnostic if no `FormRoot`
52
+ is an ancestor. At runtime, each part's own hook (`useFormRegistry`) throws the same failure as a
53
+ defence for JSX composed outside the compiler (Storybook, tests, manual code).
54
+
55
+ `FormRoot` knows fieldsets, never fields: it reaches a field name only through a fieldset
56
+ registration's `getFieldNames()`, never by reading `Field.*` parts or the field contract directly. A
57
+ fieldset is a code-level concept (round 1's `FieldsetFieldContext`/`useFieldsetField` seam, plus the
58
+ `FieldsetRegistration` contract this task ships) — it is not itself catalogued or schema-authorable,
59
+ the same way `FieldBinding` is not.
60
+
61
+ ### The registry contract (`FieldsetRegistration` / `useFormRegistry`)
62
+
63
+ A fieldset registers itself once, for as long as it is mounted. Its `label` is the kebab-case rendering of
64
+ its own `namespace` (`shippingAddress` → `shipping-address`) — a machine identifier, never shopper copy;
65
+ see *FormError* below for the one place it surfaces:
66
+
67
+ ```tsx
68
+ const { registerFieldset } = useFormRegistry();
69
+
70
+ useEffect(
71
+ () =>
72
+ registerFieldset({
73
+ namespace: 'shippingAddress',
74
+ label: 'shipping-address',
75
+ getFieldNames: () => ['shippingAddress.street', 'shippingAddress.city'],
76
+ toPayload: (values) => ({ street: values.street, city: values.city }),
77
+ dispatch: (payload) => actions.shippingAddress.update(payload),
78
+ }),
79
+ [registerFieldset],
80
+ );
81
+ ```
82
+
83
+ `FormRoot` calls `dispatch` for a fieldset only when one of the fields it currently has mounted is dirty —
84
+ "dirty" means "the shopper touched a field here", not "differs from the seed" (see *Common
85
+ mistakes*). `FormRoot` destructures `dirtyFields` from `formState` during render, which is what
86
+ subscribes it, and walks each mounted field's dot-path through that object. Reading dirtiness inside the submit callback
87
+ instead would subscribe nothing, leaving the gate dependent on whichever composed part happened to
88
+ open a subscription of its own. Registrations dispatch in the order they were first inserted, one at a time; a
89
+ later registration under the same namespace replaces the earlier one in place rather than moving it —
90
+ this is how the same fieldset survives its own effect cleaning up and re-running.
91
+
92
+ `registerFieldset` throws when the namespace collides with one a *different*, still-mounted fieldset
93
+ already holds, and when it is a prefix or a descendant of one already registered (`profile` alongside
94
+ `profile.address`). Two fieldsets sharing a namespace would silently collapse into one, leaving the
95
+ displaced fieldset mounted, editable and permanently unwritable while submit still reports success; a
96
+ nested namespace would make the parent dispatch on the child's edits and marshal the child's values
97
+ into the parent's payload. Both are unusable by construction, so `FormRoot` fails fast at registration
98
+ rather than degrading silently.
99
+
100
+ ### Implicit submission
101
+
102
+ By default, `FormRoot` cancels HTML implicit submission for its own text-entry inputs: pressing Enter
103
+ in a field does nothing, because a stray Enter mid-address would commit a write the shopper has not
104
+ finished composing. Whether Enter submits is chosen by composition, not by a prop: mount `FormSubmit`
105
+ to keep that cancelling default, or `FormEnterSubmit` to allow it — there is no `submitOnEnter` prop
106
+ and no schema entry for the behaviour on either part.
107
+
108
+ The allowance is form-scoped, not fieldset-scoped: a `FormEnterSubmit` placed inside one fieldset of a
109
+ multi-fieldset `FormRoot` allows Enter to submit from *any* text-entry input of that `FormRoot`, not
110
+ only the fieldset it happens to sit in — `FormRoot` knows fieldsets, never fields, so there is no
111
+ narrower rule and no path to one. Two mounted `FormEnterSubmit`s compose (unmounting one leaves the
112
+ allowance the other still holds in place), and both `FormSubmit` and `FormEnterSubmit` can be mounted
113
+ together, in which case Enter still submits exactly once.
114
+
115
+ Enter that commits an IME candidate — the keystroke a Japanese, Chinese or Korean shopper uses to
116
+ choose a word, not to submit — is cancelled either way, allowance or not. Choosing a word must never
117
+ send a half-finished form.
118
+
119
+ An author-supplied `onKeyDown` on `FormRoot` runs first, unconditionally, before either behaviour: it
120
+ can cancel Enter on a form that otherwise allows it, but it cannot force Enter to submit on a form that
121
+ cancels it, because the built-in guard's own `preventDefault()` runs after the author's handler.
122
+
123
+ ---
124
+
125
+ ## Components
126
+
127
+ ### FormRoot
128
+
129
+ The only `<form>` in a composition. Renders `noValidate`, wires
130
+ `onSubmit={handleSubmit(onValid, onInvalid)}`, and provides both react-hook-form's context and the
131
+ fieldset registry to every descendant. Declares no `actions` — every write stays declared by the
132
+ fieldset that owns it.
133
+
134
+ No props beyond the native `<form>` attributes (`onSubmit` is owned internally and cannot be passed).
135
+
136
+ ### FormSubmit
137
+
138
+ | Prop | Type | Default | Notes |
139
+ |------|------|---------|-------|
140
+ | `label` | string | — | Rendered when there are no `children`. Falls through to the message catalog (`fs/form.submit`) when unset. |
141
+ | `children` | ReactNode | — | Wins over `label` when present. |
142
+
143
+ Text resolves `children ?? label ?? formatMessage({ id: 'fs/form.submit' })` — there is no hard-coded
144
+ English default. An author who wants copy other than the catalog's own must set `label` or `children`
145
+ explicitly.
146
+
147
+ A `type="submit"` `Button`, disabled while `formState.isSubmitting`. Pressing it validates every
148
+ registered field first; only once every field is valid does it skip untouched fieldsets, dispatch the
149
+ dirty ones in registration order, and collect what failed. Pressing Enter in a text input does *not*
150
+ submit — see *Implicit submission* above and `FormEnterSubmit` below for a form where it does.
151
+
152
+ ### FormEnterSubmit
153
+
154
+ | Prop | Type | Default | Notes |
155
+ |------|------|---------|-------|
156
+ | `label` | string | — | Rendered when there are no `children`. Falls through to the message catalog (`fs/form.submit`) when unset. |
157
+ | `children` | ReactNode | — | Wins over `label` when present. |
158
+
159
+ Renders the same button as `FormSubmit`, sharing its text resolution (the same `fs/form.submit` id —
160
+ there is no separate id for the Enter variant, since both parts render the same word) and its
161
+ `data-fs-form-submit`/`data-fs-form-submit-pending` hooks — there is no separate styling hook for this
162
+ part. While mounted, it additionally allows Enter to
163
+ submit the enclosing `FormRoot` from any of its text-entry inputs (see *Implicit submission* above for
164
+ the form-wide blast radius and the IME carve-out).
165
+
166
+ ### FormError
167
+
168
+ | Prop | Type | Default | Notes |
169
+ |------|------|---------|-------|
170
+ | `fallbackMessage` | string | `'Something went wrong. Please try again.'` | Shown for a fieldset whose write threw rather than resolving `userErrors`. |
171
+
172
+ Renders nothing when the last submit produced no failures. Otherwise, one group per failing fieldset,
173
+ carrying `data-fs-form-error-item={label}` — a styling/selector hook, never rendered as text. The visible
174
+ text inside each group is each failing `userError`'s own `message`, or `fallbackMessage` when the dispatch
175
+ rejected or never settled.
176
+
177
+ ### FormClear
178
+
179
+ | Prop | Type | Default | Notes |
180
+ |------|------|---------|-------|
181
+ | `label` | string | — | Rendered when there are no `children`. Falls through to the message catalog (`fs/form.clear`) when unset. |
182
+ | `children` | ReactNode | — | Wins over `label` when present. |
183
+
184
+ A `type="button"` `Button` that empties every field registered by every fieldset inside the enclosing
185
+ `FormRoot` — not only the fieldset nearest to it in the tree. Each bound part registers how its own
186
+ field is cleared, to its own empty value (`false` for a boolean field, `''` otherwise), and marks dirty
187
+ only the fields whose value actually changed: a field already empty stays clean. A registered name with
188
+ no bound part currently mounted is left alone. The following submit dispatches a write for every
189
+ fieldset that had something to clear, and nothing for one where clearing was a no-op. There is no prop
190
+ to scope it to one fieldset; place it only where clearing the whole form is the intended action.
191
+
192
+ ### FormReset
193
+
194
+ | Prop | Type | Default | Notes |
195
+ |------|------|---------|-------|
196
+ | `label` | string | — | Rendered when there are no `children`. Falls through to the message catalog (`fs/form.reset`) when unset. |
197
+ | `children` | ReactNode | — | Wins over `label` when present. |
198
+
199
+ A `type="button"` `Button` that returns every registered field name to react-hook-form's baseline for
200
+ that name, and clears its dirty/touched state. Before a fieldset's first successful write, that
201
+ baseline is whatever the field's own seed resolves to; it becomes "what was last written" only after a
202
+ successful submit.
203
+
204
+ ---
205
+
206
+ ## Canonical schema example
207
+
208
+ `FormSubmit`, `FormEnterSubmit`, `FormError`, `FormClear` and `FormReset` are schema-authorable; a
209
+ fieldset is not (it is a code-level composition today, the same way a bound `Field.Control` is). A
210
+ schema can author the form's boundary and its actions around wherever a fieldset renders itself:
211
+
212
+ ```json
213
+ {
214
+ "checkout-step": {
215
+ "type": "FormRoot",
216
+ "style": ["flex", "flex-col", "gap-4"],
217
+ "children": ["shipping-address-fieldset", "step-error", "step-submit"]
218
+ },
219
+ "step-error": { "type": "FormError" },
220
+ "step-submit": { "type": "FormSubmit", "props": { "label": "Save and continue" } }
221
+ }
222
+ ```
223
+
224
+ `shipping-address-fieldset` is a matter component (shipped by a later task) that renders its own
225
+ fields and calls `registerFieldset` internally — it is placed in the schema like any other
226
+ component, but nothing about its internals is schema-authorable.
227
+
228
+ ---
229
+
230
+ ## Common mistakes
231
+
232
+ - **Expecting a submit with nothing touched to dispatch anything.** It does not — `FormSubmit`'s skip
233
+ gate is per-fieldset `dirtyFields`, which the RFC and this doc both call "touched", not "changed".
234
+ Typing a value and then typing the original seed back still leaves that fieldset dirty and dispatches
235
+ a request that changes nothing.
236
+ - **Reading `formState.isDirty` to decide whether to submit.** Whole-form `isDirty` is `true` from the
237
+ moment any field is seeded and never reliably returns to `false` under this design — `FormRoot` never
238
+ reads it, and neither should a composition built on top of it.
239
+ - **Expecting `FormReset` to mean "clear the form".** That is `FormClear`. `FormReset` returns fields to
240
+ a baseline; before any successful write, that baseline is whatever the field's own seed resolves to, so
241
+ Reset reverts to the original value rather than emptying it — the two look the same only when a seed
242
+ happens to be empty. They diverge for good the moment something has been saved, when the baseline
243
+ becomes what was last written.
244
+ - **Placing `FormClear` expecting it to clear only the nearest fieldset.** It has no scoping prop by
245
+ design (Decision, engineer-approved): pressing it empties every field of every fieldset registered
246
+ anywhere in the enclosing `FormRoot`, and the following submit dispatches a write for each one. Place
247
+ it only where clearing the whole form is genuinely the intended action.
248
+ - **Nesting a second `FormRoot` inside the first.** Unsupported; nothing enforces it today, and two
249
+ `useForm()` instances would not share state. Compose fieldsets as siblings under one `FormRoot`
250
+ instead.
251
+ - **Confusing this with `Fieldset.Root`/`Fieldset.Legend`.** Those are a purely visual `<fieldset>`
252
+ wrapper (see [[fieldset]]) with no write behaviour and no relationship to the registry this particle
253
+ defines.
254
+ - **Giving `FormRoot` an `actions` selector.** It declares none by design (Decision 1) — a write always
255
+ belongs to the fieldset that dispatches it, so `catalog.json` can still say which component performs
256
+ it.
257
+ - **Reaching for a `submitOnEnter` prop.** There is none, on either part, by design: whether Enter
258
+ submits is chosen by composing `FormSubmit` or `FormEnterSubmit`, never by a flag.
259
+ - **Placing `FormEnterSubmit` inside one fieldset and expecting the allowance to stay scoped to it.**
260
+ It does not — the allowance is form-wide, because `FormRoot` knows fieldsets, never fields. Enter in
261
+ *any* text-entry input of that `FormRoot` submits, not only the ones in the fieldset the part sits in.
262
+
263
+ ---
264
+
265
+ ## Styling
266
+
267
+ | Component | What you style | Plausible classes |
268
+ |---|---|---|
269
+ | `FormRoot` | The `<form>` element — typically stacks its fieldsets and actions | `flex flex-col gap-6` |
270
+ | `FormSubmit` | The primary action button; target `data-fs-form-submit-pending` for the in-flight state | `bg-blue-600 text-white rounded-md px-4 py-2 data-fs-form-submit-pending:opacity-60` |
271
+ | `FormError` | The failure summary; carries `role="alert"` | `text-sm text-red-600 space-y-2` |
272
+ | `FormClear` | A secondary action, typically quieter than Submit | `text-sm text-gray-500 underline` |
273
+ | `FormReset` | A secondary action, typically quieter than Submit | `text-sm text-gray-500 underline` |
274
+
275
+ ```json
276
+ {
277
+ "checkout-step": {
278
+ "type": "FormRoot",
279
+ "style": ["flex", "flex-col", "gap-6"],
280
+ "children": ["shipping-address-fieldset", "step-error", "step-actions"]
281
+ },
282
+ "step-error": { "type": "FormError", "style": ["text-sm", "text-red-600", "space-y-2"] },
283
+ "step-actions": {
284
+ "type": "Container",
285
+ "style": ["flex", "gap-2"],
286
+ "children": ["step-reset", "step-submit"]
287
+ },
288
+ "step-reset": { "type": "FormReset", "style": ["text-sm", "text-gray-500", "underline"] },
289
+ "step-submit": {
290
+ "type": "FormSubmit",
291
+ "props": { "label": "Save and continue" },
292
+ "style": ["bg-blue-600", "text-white", "rounded-md", "px-4", "py-2"]
293
+ }
294
+ }
295
+ ```
296
+
297
+ ---
298
+
299
+ ## Related
300
+
301
+ - [[field]] — the field anatomy and the field contract a bound Control renders inside a fieldset; also
302
+ documents the fieldset *field-name* seam (`useFieldsetField`), the sibling scope to this particle's
303
+ *fieldset* registry.
304
+ - [[fieldset]] — the unrelated, purely visual `<fieldset>`/`<legend>` particle. Shares a name with the
305
+ write-boundary "fieldset" concept this doc uses, but has no code relationship to it.
306
+ - [[button]] — the particle every `Form` action wraps (`Button` with `type="submit"` or `type="button"`,
307
+ plus this particle's own `data-fs-form-*` hooks).
@@ -10,9 +10,9 @@ rather than a shared mechanism: the two differ in exactly the parts that are dom
10
10
  starts on the summary and needs an explicit cancel; that one derives its starting state from whether an
11
11
  address exists and closes itself with no cancel at all.
12
12
 
13
- The mode is owned here, not authored in a page (ADR-0006). `ClientProfileForm` announces nothing and never
14
- learns whether anything collapses around it, which is what lets the same form sit in a section, in a wizard
15
- step, or on a page of its own.
13
+ The mode is owned here, not authored in a page (ADR-0006). The composed `FormRoot`/`ClientProfileFieldset`
14
+ pair announces nothing and never learns whether anything collapses around it, which is what lets the same
15
+ form sit in a section, in a wizard step, or on a page of its own.
16
16
 
17
17
  ## Why no mode name reaches the schema
18
18
 
@@ -51,8 +51,8 @@ resolution error rather than a silently dead branch that blanks the section.
51
51
  - Starts on the summary. Unlike the delivery section, nothing about the profile data decides this: the
52
52
  summary is always the right first thing to show a shopper who has one.
53
53
  - The mode moves only on this section's own buttons, `EditButton` and `CancelButton`. Saving the form does
54
- not close the section, which is what keeps `ClientProfileForm`'s confirmation on screen long enough to
55
- read.
54
+ not close the section, so the form's own fields and controls stay on screen after a save rather than
55
+ being replaced by the summary.
56
56
  - **Self-contained.** It observes nothing outside itself and reports nothing: no other section opens or
57
57
  closes as a result of anything happening here. Whether finishing a form should collapse the section that
58
58
  holds it is a sequencing question, deliberately left to the flow-of-steps RFC (#611) rather than
@@ -68,7 +68,7 @@ IdentityEditController
68
68
  │ └─ IdentitySummaryView → IdentityEditButton
69
69
  └─ IdentitySummaryRoot
70
70
  ├─ IdentitySummaryView → IdentitySummarySelectedSummary
71
- └─ IdentityEditForm → ClientProfileFormRoot + IdentityEditCancelButton
71
+ └─ IdentityEditForm → FormRoot(ClientProfileFieldset) + IdentityEditCancelButton
72
72
  ```
73
73
 
74
74
  ## Accessibility