@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,142 @@
1
+ # CardholderDocument
2
+
3
+ **Category:** commerce
4
+ **Purpose:** A single tax-document input (CPF) for the checkout payment step, shown or hidden and marked required by the currently selected payment method's server-driven configuration.
5
+
6
+ ---
7
+
8
+ ## Visual cues (how to identify in an image)
9
+
10
+ Use these signals to detect that a CardholderDocument block is present. Position on the page is irrelevant for detection — it typically sits near the card-entry fields in a payment/checkout step. The image is the source of truth for layout.
11
+
12
+ A CardholderDocument block is:
13
+
14
+ - A **single labeled text input**, formatted with dots and a dash like `123.456.789-01` (the CPF mask).
15
+ - Labeled **"CPF"** or a similar tax-document label — never "Card number", "Expiration", or "CVV".
16
+ - Not accompanied by a network icon, a "•••• 1234" masked number, or any card-brand logo — those belong to `CardEntry` or `SavedCards`, not this block.
17
+ - Sometimes marked with a required indicator (an asterisk or similar) next to its label; sometimes not — both are valid, since requiredness is server-driven per payment method.
18
+
19
+ If the image shows a **card-entry form with one extra plain text field formatted like a Brazilian tax ID**, use `CardholderDocument`. Do not use it for the shopper's account/profile document field — that is a different, identity-step component this block never composes with or reads from.
20
+
21
+ ---
22
+
23
+ ## When to use
24
+
25
+ - The checkout/payment step, as a sibling of the card-entry fields, for a store whose selected payment method requires (or offers) a cardholder tax document.
26
+
27
+ ## When NOT to use
28
+
29
+ - The shopper's saved profile/identity document (a client-identity or user-profile field) → use the identity-step document component instead. `CardholderDocument` is a payment-time value, deliberately separate — it never reads or writes the stored profile document, because the epic's RFC records that the two may differ.
30
+ - A generic, data-agnostic tax-document input with no server-driven presence/requiredness → use the `DocumentField` particle directly instead; `CardholderDocument` exists specifically to wire that particle to the selected payment method's validator.
31
+ - More than one document type or country → out of scope today. Only CPF renders; see this component's own spec for the recorded deferral.
32
+
33
+ ---
34
+
35
+ ## Composition
36
+
37
+ `CardholderDocument` is a leaf and must be a descendant of `PaymentSectionRoot` or `PaymentSectionController`. It takes no children and is placed as a **sibling** of `CardEntry`, `SavedCards`, `Installments`, and the other payment leaves — never nested inside any of them.
38
+
39
+ ```
40
+ PaymentSectionRoot (or PaymentSectionController)
41
+ ├── PaymentMethodList
42
+ ├── CardEntry (sibling — new-card fields)
43
+ ├── CardholderDocument (sibling — this block)
44
+ ├── Installments
45
+ └── SavedCards
46
+ ```
47
+
48
+ Child order reflects visual order. There is no parent/child relationship for this component to define — it is a single, independently placeable field.
49
+
50
+ **Composition rules:**
51
+
52
+ - Must be inside `PaymentSectionRoot` or `PaymentSectionController`. Outside either, it throws.
53
+ - Renders `null` (no DOM at all) when the selected payment method's validator does not mark a document displayable, or when no method is selected yet. This is normal, not an error — most methods most of the time render nothing here.
54
+ - Also renders `null` while the card payment is collapsed — the persisted selection is a method that is not a card, or one the section does not serve, and a composed `PaymentMethodList` can bring the card form back (see [[payment]]) — whatever that method's own validator says. When the card comes back, the field re-seeds from the value already typed.
55
+ - Takes no children; passing any is a no-op.
56
+
57
+ ---
58
+
59
+ ## Components
60
+
61
+ ### CardholderDocument
62
+
63
+ The field itself. Renders the `DocumentField` particle for CPF, with presence and requiredness resolved from `usePayment().selectedMethod.validator` (`documentDisplayable` / `documentRequired`) rather than from any prop.
64
+
65
+ | Prop | Type | Default | Notes |
66
+ |------|------|---------|-------|
67
+ | `label` | `string` | `'CPF'` | The field's visible label. |
68
+ | `placeholder` | `string` | `'000.000.000-00'` | Shown in the empty input as a format hint. |
69
+ | `errorMessage` | `string` | `'Invalid CPF'` | Shown inline when the typed value fails CPF check-digit validation on blur. |
70
+ | `disabled` | `boolean` | `false` | Disables the input without hiding it — independent of the data-driven presence gate. |
71
+
72
+ No `documentType` prop — this build supports CPF only. No `required` prop — requiredness is always read from the selected method's validator, never authored.
73
+
74
+ **Data:** declares no `fragment` of its own. It reads `selectedMethod`, `draft`, and `setDraftDocument` off the ambient `payment` context that `PaymentSectionRoot` / `PaymentSectionController` already fetches for.
75
+
76
+ ---
77
+
78
+ ## Canonical schema example
79
+
80
+ ```json
81
+ {
82
+ "payment-section": {
83
+ "type": "PaymentSectionRoot",
84
+ "children": ["payment-method-list", "card-entry", "cardholder-document", "installments"]
85
+ },
86
+ "payment-method-list": { "type": "PaymentMethodList" },
87
+ "card-entry": { "type": "CardEntry" },
88
+ "cardholder-document": {
89
+ "type": "CardholderDocument",
90
+ "props": { "label": "CPF", "placeholder": "000.000.000-00" }
91
+ },
92
+ "installments": { "type": "Installments" }
93
+ }
94
+ ```
95
+
96
+ ---
97
+
98
+ ## Common mistakes
99
+
100
+ - **Nesting `CardholderDocument` inside `CardEntry`.** It is a sibling under the same `PaymentSectionRoot`, never a child — `CardEntry` does not accept children.
101
+ - **Passing a `documentType`, `required`, `countryCode`, or `document` prop.** None exist. Document type is fixed to CPF; requiredness and presence are always read from the selected method's validator, never authored; the typed value is not a prop at all — it flows into the shared payment draft internally.
102
+ - **Using this component for the shopper's saved profile document.** That is a different, identity-step field this component never reads from or writes to — the two are intentionally separate, even for the same shopper.
103
+ - **Expecting it to render outside a `PaymentSectionRoot` / `PaymentSectionController`.** It throws — the same requirement every other payment leaf (`CardEntry`, `Installments`, `SavedCards`, …) has.
104
+ - **Treating a blank render as broken.** Most of the time, for most payment methods, this component correctly renders nothing — that is the "not displayable" state, not a bug.
105
+
106
+ ---
107
+
108
+ ## Known limitations
109
+
110
+ - **CPF only.** The underlying read model carries no document-type or country signal (see this component's own spec for the full, measured deferral) — a store outside Brazil cannot get a different document type from this component today.
111
+ - **Server-driven requiredness renders the field correctly and is also enforced at submit time — but not by this component or by `CardEntry`.** This component marks the field required when the validator says so; a required-but-blank or malformed value (whether or not required) blocks Place Order client-side, before any transaction opens, **while the selected method's validator also says the document is displayable**. That gate lives in `matter/payment/use-payment-submit-gate.ts` (T24) and is consumed by `PlaceOrder`, not by this component or by `CardEntry`'s own `validateDraft` — this component's only job stays publishing `draft.document` via `setDraftDocument`. When the *currently* selected method's validator says `documentDisplayable: false`, the gate treats `draft.document` as inert either way (fix pass, findings R1/S4/S5): there is no field on screen for the shopper to read a leftover value from or correct it in, whether that value is left over from a previously selected, displayable method or simply blank. See `specs/cardholder-document-submit-gate.md` for the gate's full contract.
112
+
113
+ ---
114
+
115
+ ## Styling
116
+
117
+ `CardholderDocument`'s single root is the composed `DocumentField` particle — style it exactly like any other text field in the payment section.
118
+
119
+ | Component | What you style | Plausible classes |
120
+ |---|---|---|
121
+ | `CardholderDocument` | The field's own label, input box, and inline error — same shape as a `Field.Root` | `flex flex-col gap-[4px] w-full` on the root; `text-[14px] font-medium text-[#1A1A1A]` for the label; `border border-[#CCCCCC] rounded-[6px] px-[12px] py-[8px] text-[14px]` for the input |
122
+ | Placing (`data-fs-cardholder-document-placing`) | Present while an order-placement sequence is in flight (T28); the field is also `inert` and `aria-disabled` at that point | `opacity-60 cursor-not-allowed` |
123
+
124
+ Styled example:
125
+
126
+ ```json
127
+ {
128
+ "cardholder-document": {
129
+ "type": "CardholderDocument",
130
+ "props": { "label": "CPF" },
131
+ "style": ["flex", "flex-col", "gap-[4px]", "w-full"]
132
+ }
133
+ }
134
+ ```
135
+
136
+ ---
137
+
138
+ ## Related
139
+
140
+ - [[payment]] — `PaymentSectionRoot` / `PaymentMethodList`: the required ancestor and the method picker. `CardholderDocument` cannot render outside a `PaymentSectionRoot` (or `PaymentSectionController`).
141
+ - `CardEntry` — the sibling new-card entry fields (number, holder name, expiration, CVV); a different task's deliverable, always a sibling, never a parent of this block.
142
+ - `DocumentField` — the underlying data-agnostic particle this component wraps. Use it directly only outside the payment context, where server-driven presence/requiredness do not apply.
@@ -0,0 +1,148 @@
1
+ # ClientPreferencesFieldset
2
+
3
+ **Category:** identity
4
+ **Purpose:** The composable, one-Save write of the `ClientPreferences` entity — the same two consent flags `ClientPreferencesRoot` already writes on toggle, given a second, batched write timing. Registers itself with the enclosing `FormRoot` under the `clientPreferences` namespace and dispatches at most one `identity.clientPreferencesUpdate` when the shopper presses that form's submit. Two bound controls — newsletter, save-personal-data — compose freely inside it.
5
+
6
+ ---
7
+
8
+ ## Two write timings for one entity — read this before composing a control
9
+
10
+ `ClientPreferencesRoot` and `ClientPreferencesFieldset` both read and write the same `ClientPreferences` entity (`optinNewsLetter`, `savePersonalData`). They differ only in *when* a toggle writes:
11
+
12
+ - **`ClientPreferencesRoot`** (`matter/client-preferences/client-preferences.tsx`) — self-persisting. Its Controls (`NewsletterOptInControl`, `SavePersonalDataOptInControl`) write the moment the shopper toggles them. Provides context `client-preferences-toggle`.
13
+ - **`ClientPreferencesFieldset`** (this matter) — batched. Its Controls (`ClientPreferencesNewsletterControl`, `ClientPreferencesSavePersonalDataControl`) write nothing themselves; they register with the enclosing `FormRoot` and wait for that form's submit. Provides context `client-preferences`.
14
+
15
+ The two context names are deliberately different, on purpose: reusing one name for both would let an on-toggle control compile inside the batched fieldset (or vice versa) and write at the wrong moment with no warning. Composing a Root's control under the other's provider fails the build instead — the compiler's `context-constraint-violation` diagnostic names the missing context.
16
+
17
+ ---
18
+
19
+ ## Visual cues (how to identify in an image)
20
+
21
+ - One or two consent checkboxes ("email me with news and offers", "save my information for next time") inside a page that also shows a **single submit button somewhere else on the page** — not one owned by this fieldset.
22
+ - If the checkbox instead writes the instant it is toggled (no surrounding Save button gates it), that is `ClientPreferencesRoot` + `NewsletterOptInControl` / `SavePersonalDataOptInControl` — a different matter with the same visible checkboxes. The image alone cannot always distinguish the two; the surrounding form structure is the signal.
23
+
24
+ ---
25
+
26
+ ## When to use
27
+
28
+ - A page that wants the two consent flags to submit together with other fieldsets (e.g. `ClientProfileFieldset`, `ShippingAddressFieldset`) behind one shared `FormRoot` submit.
29
+ - Anywhere the store wants the write timing (batched, on submit) legible from the schema, rather than immediate.
30
+
31
+ ## When NOT to use
32
+
33
+ - A standalone opt-in checkbox that should persist itself the instant it's toggled — use `ClientPreferencesRoot` + `NewsletterOptInControl` / `SavePersonalDataOptInControl` instead.
34
+ - Placing `NewsletterOptInControl` or `SavePersonalDataOptInControl` inside this fieldset, or `ClientPreferencesNewsletterControl` / `ClientPreferencesSavePersonalDataControl` inside `ClientPreferencesRoot` — neither compiles; see *Two write timings* above.
35
+
36
+ ---
37
+
38
+ ## Composition
39
+
40
+ ```
41
+ SessionRoot (required ancestor; session tri-state + canEditData)
42
+ └── FormRoot (the only <form> it dispatches from; owns useForm() and the registry)
43
+ ├── ClientPreferencesFieldset (data-fs-client-preferences-fieldset; registers namespace "clientPreferences")
44
+ │ ├── ClientPreferencesNewsletterControl
45
+ │ └── ClientPreferencesSavePersonalDataControl
46
+ ├── (any other fieldset — e.g. ClientProfileFieldset — as a sibling, same FormRoot)
47
+ ├── FormError
48
+ └── FormSubmit
49
+ ```
50
+
51
+ `ClientPreferencesFieldset` renders one `Fieldset.Root` and no `<form>` of its own. Every control `requiresContext: 'client-preferences'`; a control placed outside it (or under `ClientPreferencesRoot` instead) is rejected by the context-constraint walk at build time.
52
+
53
+ ---
54
+
55
+ ## Components
56
+
57
+ ### ClientPreferencesFieldset
58
+
59
+ | Prop | Type | Default | Notes |
60
+ |------|------|---------|-------|
61
+ | `skeletonCount` | number | `2` | Placeholder rows drawn while the preferences resolve. Set it to the number of controls composed. |
62
+ | `loadingLabel` | string | `'Loading your preferences'` | Announced politely while the preferences resolve. Blank means no announcement. |
63
+ | `loadedLabel` | string | `'Your preferences are ready'` | Announced politely once the controls mount. Blank means no announcement. |
64
+
65
+ Declares `requiresContext: ['form', 'session']` (needs both a `FormRoot` and a `SessionRoot` ancestor), `providesContext: 'client-preferences'`, `actions: { identity: ['clientPreferencesUpdate'] }`, and its own fragment on `Query.clientPreferences`. Renders `Fieldset.Root` with `disabled={readOnly}`; carries `-pending` until its entity resolves, and `-readonly` when the session reports `canEditData: false`.
66
+
67
+ **Loading:** the controls mount once, after the entity has resolved; until then the fieldset renders `ClientPreferencesFieldsetSkeleton` itself, in their place. Each checkbox hands its saved value to react-hook-form as a field-level `defaultValue`, re-applied on every render for as long as the checkbox stays clean — mounting before the answer arrives would seed unchecked, and a shopper who toggles it before the answer lands makes the checkbox dirty on that seed, permanently locking their saved preference out of it; even a shopper who does not toggle it sees the checkbox flash to the saved value. Resolution is the page query settling, latched: once the controls are up a later refetch never takes them down, so nothing the shopper toggled is lost.
68
+
69
+ The definition also declares that same component as its `skeleton`, which is the shape the repo's skeleton-with-fragment rule asks for, but nothing reads it today: the compiler wires `skeleton` into a Suspense fallback only on the `lazy` path, and this definition is `reactive`. The generator still emits the import, so a generated page carries an unused `ClientPreferencesFieldsetSkeleton` symbol.
70
+
71
+ `skeletonCount` sizes the wait. The skeleton draws two rows by default, one per consent flag in the full anatomy; a page that composes one sets `skeletonCount: 1`, or the form jumps by a row when the controls take the skeleton's place. The skeleton is `aria-hidden` — the wait is spoken instead by a polite live region the fieldset keeps mounted in both states, carrying `loadingLabel` and then `loadedLabel`, with `aria-busy` on the `<fieldset>` for as long as it lasts.
72
+
73
+ ### ClientPreferencesNewsletterControl / ClientPreferencesSavePersonalDataControl
74
+
75
+ | Prop | Type | Default | Notes |
76
+ |------|------|---------|-------|
77
+ | `label` | string | `'Email me with news and offers'` / `'Save my information for next time'` | |
78
+
79
+ Childless, renders `Field.Root > Field.Label > Field.Checkbox` (with the checkmark icon), the label text following. Composed children replace that default anatomy and still bind to the same field. Defaults to unchecked (opt-in, not opt-out) — a `null` saved value reads as `false`.
80
+
81
+ ---
82
+
83
+ ## Canonical schema example
84
+
85
+ ```json
86
+ {
87
+ "session": { "type": "SessionRoot", "children": ["form"] },
88
+ "form": {
89
+ "type": "FormRoot",
90
+ "children": ["preferences-fieldset", "form-error", "form-submit"]
91
+ },
92
+ "preferences-fieldset": {
93
+ "type": "ClientPreferencesFieldset",
94
+ "children": ["newsletter", "save-personal-data"]
95
+ },
96
+ "newsletter": {
97
+ "type": "ClientPreferencesNewsletterControl",
98
+ "props": { "label": "Sign me up for the newsletter" },
99
+ "children": []
100
+ },
101
+ "save-personal-data": {
102
+ "type": "ClientPreferencesSavePersonalDataControl",
103
+ "children": []
104
+ },
105
+ "form-error": { "type": "FormError", "children": [] },
106
+ "form-submit": { "type": "FormSubmit", "children": [] }
107
+ }
108
+ ```
109
+
110
+ (Omitting `save-personal-data` above drops that flag entirely — its default stays untouched server-side, with no CSS hack.)
111
+
112
+ ---
113
+
114
+ ## FormClear coerces booleans
115
+
116
+ `FormClear` empties every registered field to `''`. `ClientPreferencesFieldset`'s own `toPayload` runs `Boolean()` over each flag present in the namespace snapshot, so a cleared field dispatches `false` on the following submit — never the raw string `''`.
117
+
118
+ ---
119
+
120
+ ## Common mistakes
121
+
122
+ - **Confusing this fieldset's controls with `NewsletterOptInControl` / `SavePersonalDataOptInControl`.** See *Two write timings* above — they require different contexts and neither compiles under the other's provider.
123
+ - **Composing this fieldset without a `FormRoot` ancestor.** It requires both `form` and `session`; a `SessionRoot` alone is not enough.
124
+ - **Expecting a submit button from this fieldset.** It renders none — compose `FormSubmit` once, anywhere inside the enclosing `FormRoot`.
125
+ - **Composing `ClientPreferencesNewsletterControl` (or the other control) outside `ClientPreferencesFieldset`.** Every control `requiresContext: 'client-preferences'` and throws otherwise.
126
+
127
+ ---
128
+
129
+ ## Styling
130
+
131
+ | Component | What you style | Plausible classes |
132
+ |---|---|---|
133
+ | `ClientPreferencesFieldset` | The `data-fs-client-preferences-fieldset` root; carries `-pending`/`-readonly` presence attributes | `flex flex-col gap-2` |
134
+ | `ClientPreferencesFieldsetSkeleton` | `data-fs-client-preferences-fieldset-skeleton` and its `-skeleton-field` rows | `flex flex-col gap-2`, rows `h-5 rounded bg-gray-100 animate-pulse` |
135
+ | Announcement | `data-fs-client-preferences-fieldset-announcement` reaches the live region carrying `loadingLabel`/`loadedLabel`. Hide it with a clipping technique, never `display:none` — that drops it from the accessibility tree and defeats the announcement | `sr-only` |
136
+ | Control wrapper | `data-fs-client-preferences-fieldset-field="newsletter\|save-personal-data"` | `flex items-center gap-2` |
137
+
138
+ Checkbox-level visuals (the indicator, the checked state) are owned by the composed `checkbox`/`field` particles' own swatches and are not redefined here.
139
+
140
+ ---
141
+
142
+ ## Related
143
+
144
+ - [[client-preferences]] — the self-persisting Root and Controls this fieldset shares an entity with; see *Two write timings* above.
145
+ - [[client-profile-fieldset]] — the sibling fieldset for the other entity a checkout identity step commonly needs; composes into the same `FormRoot`.
146
+ - [[session]] — provides the identification tri-state and the required `SessionRoot` ancestor.
147
+ - [[form]] — `FormRoot`/`FormSubmit`/`FormError`/`FormClear`/`FormReset`, the enclosing form this fieldset registers with.
148
+ - [[field]], [[fieldset]], [[checkbox]] — the particles this matter composes directly.
@@ -50,7 +50,7 @@ SessionRoot (provides the session tri-state; required a
50
50
 
51
51
  ### ClientPreferencesRoot
52
52
 
53
- Binding provider for the section. Reads both opt-in values from `Query.clientPreferences`, seeds each Control, and tracks per-field pending/error. Renders a `<div data-fs-client-preferences-root>` wrapper. No merchant-facing props beyond `className`.
53
+ Binding provider for the section. Reads both opt-in values from `Query.clientPreferences`, seeds each Control, and tracks per-field pending/error. Renders a `<div data-fs-client-preferences-root>` wrapper. No merchant-facing props beyond `className`. Provides context `client-preferences-toggle` — a different name from the batched `ClientPreferencesFieldset` (context `client-preferences`), so the two write timings for the same entity cannot compile into each other's container. See [[client-preferences-fieldset]] for the one-Save alternative.
54
54
 
55
55
  **Data:** declares a colocated `fragment ClientPreferencesData on Query { clientPreferences { optinNewsLetter savePersonalData } }` with `fetchStrategy: 'reactive'`. The compiler merges it into the page query — no explicit `dataSources` entry needed.
56
56
 
@@ -179,6 +179,7 @@ Styled example:
179
179
  ## Related
180
180
 
181
181
  - [[email-identification]] — sibling identity control; reads/writes `clientProfile.email` and moves the session `anonymous → identified`. Same `SessionRoot` requirement.
182
+ - [[client-preferences-fieldset]] — the batched, one-Save alternative for the same two flags; requires a `FormRoot` and a different context (`client-preferences`), so it cannot compile alongside `NewsletterOptInControl` / `SavePersonalDataOptInControl` in the same container.
182
183
  - [[session]] — provides the identification tri-state and the required `SessionRoot` ancestor.
183
184
  - [[checkbox]] — the presentational particle each Control wraps.
184
185
  - [[field]] — provides the label/error wrapper each Control inlines.
@@ -0,0 +1,184 @@
1
+ # ClientProfileFieldset
2
+
3
+ **Category:** identity
4
+ **Purpose:** The composable, one-Save write of the `ClientProfile` entity. Registers itself with the enclosing `FormRoot` under the `clientProfile` namespace and dispatches at most one `identity.clientProfileUpdate` when the shopper presses that form's submit. Four bound controls — first name, last name, phone, document — compose freely inside it, in any order or subset.
5
+
6
+ ---
7
+
8
+ ## Naming collision — read this before composing the phone control
9
+
10
+ Two phone controls exist for the same entity, with different write timings, and their names are easy to swap by accident:
11
+
12
+ - **`ClientProfileFieldsetPhoneControl`** (this matter) — the batched control. It writes nothing on its own; it registers `clientProfile.phone` with the enclosing `FormRoot` and waits for that form's submit.
13
+ - **`ClientProfilePhoneControl`** (`matter/client-profile/client-profile.tsx`) — the on-blur control. It writes `clientProfileUpdate({ phone })` itself, the moment the field loses focus.
14
+
15
+ Neither compiles inside the other's container: `ClientProfileFieldsetPhoneControl` requires `client-profile-fieldset` context (provided only by `ClientProfileFieldset`), and `ClientProfilePhoneControl` requires `client-profile` context (provided only by `ClientProfileRoot`). Composing one under the other's Root fails the build — the compiler's `context-constraint-violation` diagnostic names the missing context.
16
+
17
+ ---
18
+
19
+ ## Visual cues (how to identify in an image)
20
+
21
+ - A group of shopper-profile inputs (first name, last name, a country-masked phone field, an optional tax-document field) inside a page that also shows a **single submit button somewhere else on the page**, not one owned by this fieldset.
22
+ - No visible Save/Continue button belongs to this block by itself — the nearest `FormSubmit` (composed by `FormRoot`) is what dispatches it.
23
+ - The image is the source of truth for layout.
24
+
25
+ Use `ClientProfileFieldset` (not the standalone `ClientProfilePhoneControl`) whenever the profile fields are meant to submit together with other fieldsets on the same page, behind one shared button.
26
+
27
+ ---
28
+
29
+ ## When to use
30
+
31
+ - A page that wants the shopper's profile fields (name, phone, document) to write behind a shared `FormRoot` submit, alongside other fieldsets (e.g. `ShippingAddressFieldset`, `ClientPreferencesFieldset`) — the RFC's "compose a profile write and an address write behind one submit" scenario.
32
+ - Anywhere the store wants one entity, one action, one namespace, with the write timing (batched, on submit) legible from the schema.
33
+
34
+ ## When NOT to use
35
+
36
+ - A single phone field that should persist itself on blur — use `ClientProfilePhoneControl` (`matter/client-profile`).
37
+ - Editing the email — that's `EmailIdentificationControl`'s job; no control in this fieldset touches it.
38
+ - Consent flags (newsletter, save-personal-data) — those belong to `ClientPreferencesFieldset`, a sibling namespace in the same `FormRoot`.
39
+
40
+ ---
41
+
42
+ ## Composition
43
+
44
+ ```
45
+ SessionRoot (required ancestor; session tri-state + canEditData)
46
+ └── FormRoot (the only <form> it dispatches from; owns useForm() and the registry)
47
+ ├── ClientProfileFieldset (data-fs-client-profile-fieldset; registers namespace "clientProfile")
48
+ │ ├── ClientProfileFirstNameControl
49
+ │ ├── ClientProfileLastNameControl
50
+ │ ├── ClientProfileFieldsetPhoneControl (country/requirement/errors as props)
51
+ │ └── ClientProfileDocumentControl (renders nothing when documentType is unsupported)
52
+ ├── (any other fieldset — e.g. ClientPreferencesFieldset — as a sibling, same FormRoot)
53
+ ├── FormError
54
+ └── FormSubmit
55
+ ```
56
+
57
+ `ClientProfileFieldset` renders one `Fieldset.Root` and no `<form>` of its own — `FormRoot` owns the only one. Every control `requiresContext: 'client-profile-fieldset'`; a control placed outside `ClientProfileFieldset` (or under `ClientProfileRoot` instead) is rejected by the context-constraint walk at build time.
58
+
59
+ ---
60
+
61
+ ## Components
62
+
63
+ ### ClientProfileFieldset
64
+
65
+ | Prop | Type | Default | Notes |
66
+ |------|------|---------|-------|
67
+ | `country` | `'US' \| 'BR'` | `'US'` | Phone mask/validation country, provided to the phone control via context. |
68
+ | `documentType` | string | — | e.g. `'CPF'`. The document control renders only when supported. |
69
+ | `skeletonCount` | number | `4` | Placeholder rows drawn while the profile resolves. Set it to the number of controls composed. |
70
+ | `loadingLabel` | string | `'Loading your details'` | Announced politely while the profile resolves. Blank means no announcement. |
71
+ | `loadedLabel` | string | `'Your details are ready'` | Announced politely once the controls mount. Blank means no announcement. |
72
+
73
+ Declares `requiresContext: ['form', 'session']` (needs both a `FormRoot` and a `SessionRoot` ancestor), `providesContext: 'client-profile-fieldset'`, `actions: { identity: ['clientProfileUpdate'] }`, and its own fragment on `Query.clientProfile`. Renders `Fieldset.Root` with `disabled={readOnly}`; carries `-pending` until its entity resolves, and `-readonly` when the session reports `canEditData: false`.
74
+
75
+ **Loading:** the controls mount once, after the entity has resolved; until then the fieldset renders `ClientProfileFieldsetSkeleton` itself, in their place. Each control hands its saved value to react-hook-form as a field-level `defaultValue`, re-applied on every render for as long as the field stays clean — mounting before the answer arrives would seed `''`, and a shopper who starts typing before it lands makes the field dirty on that empty seed, permanently locking their saved value out of it; even a shopper who does not type sees the field flash from `''` to the real data. Resolution is the page query settling, latched: once the controls are up a later refetch never takes them down, so nothing the shopper typed is lost.
76
+
77
+ The definition also declares that same component as its `skeleton`, which is the shape the repo's skeleton-with-fragment rule asks for, but nothing reads it today: the compiler wires `skeleton` into a Suspense fallback only on the `lazy` path, and this definition is `reactive`. The generator still emits the import, so a generated page carries an unused `ClientProfileFieldsetSkeleton` symbol.
78
+
79
+ `skeletonCount` sizes the wait. The skeleton draws four rows by default, one per control in the full anatomy; a page that composes three sets `skeletonCount: 3`, or the form jumps by a row when the controls take the skeleton's place. The skeleton is `aria-hidden` — the wait is spoken instead by a polite live region the fieldset keeps mounted in both states, carrying `loadingLabel` and then `loadedLabel`, with `aria-busy` on the `<fieldset>` for as long as it lasts.
80
+
81
+ ### ClientProfileFirstNameControl / ClientProfileLastNameControl
82
+
83
+ | Prop | Type | Default | Notes |
84
+ |------|------|---------|-------|
85
+ | `label` | string | `'First name'` / `'Last name'` | |
86
+ | `placeholder` | string | — | |
87
+
88
+ Childless, renders `Field.Root > Field.Label + Field.Control + Field.Error`. Composed children replace that default anatomy and still bind to the same field.
89
+
90
+ ### ClientProfileFieldsetPhoneControl
91
+
92
+ | Prop | Type | Default | Notes |
93
+ |------|------|---------|-------|
94
+ | `label` | string | `'Phone'` | |
95
+ | `requirement` | `'mandatory' \| 'optional'` | — | Requiredness only — the only source of it for this control, since the fieldset publishes no baseline. Unset raises nothing, so the phone is optional. A field the store does not want is omitted from the page, not switched off by a prop value. |
96
+ | `errorInvalid` | string | `'Invalid phone number'` | |
97
+ | `errorRequired` | string | `'Phone number is required'` | |
98
+
99
+ Composes the `PhoneField` particle. Recomposable (RFC:503): given children, they replace `PhoneField`'s
100
+ default `Field.Label` + `PhoneFieldInput` + `Field.Error` anatomy, in any order; childless keeps today's
101
+ order and text. To switch the phone field off, omit this element from the page — `requirement` never
102
+ governs visibility (see *Switching the phone field off* below).
103
+
104
+ ### ClientProfileDocumentControl
105
+
106
+ | Prop | Type | Default | Notes |
107
+ |------|------|---------|-------|
108
+ | `label` | string | `'CPF'` | |
109
+ | `placeholder` | string | — | |
110
+ | `errorMessage` | string | — | |
111
+
112
+ Renders nothing, and registers no field name, when the fieldset's `documentType` is unsupported. Composes
113
+ the `DocumentField` particle. Recomposable (RFC:503): given children, they replace `DocumentField`'s
114
+ default `Field.Label` + `DocumentFieldInput` + `Field.Error` anatomy, in any order; childless keeps
115
+ today's order and text.
116
+
117
+ ---
118
+
119
+ ## Canonical schema example
120
+
121
+ ```json
122
+ {
123
+ "session": { "type": "SessionRoot", "children": ["form"] },
124
+ "form": {
125
+ "type": "FormRoot",
126
+ "children": ["profile-fieldset", "form-error", "form-submit"]
127
+ },
128
+ "profile-fieldset": {
129
+ "type": "ClientProfileFieldset",
130
+ "props": { "country": "BR", "documentType": "CPF" },
131
+ "children": ["first-name", "last-name", "phone", "document"]
132
+ },
133
+ "first-name": { "type": "ClientProfileFirstNameControl", "children": [] },
134
+ "last-name": { "type": "ClientProfileLastNameControl", "children": [] },
135
+ "phone": { "type": "ClientProfileFieldsetPhoneControl", "children": [] },
136
+ "document": { "type": "ClientProfileDocumentControl", "children": [] },
137
+ "form-error": { "type": "FormError", "children": [] },
138
+ "form-submit": { "type": "FormSubmit", "children": [] }
139
+ }
140
+ ```
141
+
142
+ Adding a second fieldset (e.g. `ClientPreferencesFieldset`) as another child of `form`, alongside `profile-fieldset`, is what puts both entities behind the one `form-submit` button — the RFC's core composition.
143
+
144
+ ---
145
+
146
+ ## Switching the phone field off
147
+
148
+ `requirement` accepts only `'mandatory'` and `'optional'`, and left out it raises nothing — it governs required validation, never visibility. There is no prop value that hides the phone field: a store that does not want it omits `ClientProfileFieldsetPhoneControl` from the page entirely, which also means `clientProfile.phone` is never registered and can never block a submit. A stale `page.json` still carrying `"requirement": "hidden"` fails the generated store's own `next build` with a `TS2322` type error naming the attribute — it is not a schema-validation failure, and no diagnostic fires at `sukima build`.
149
+
150
+ No control in this fieldset ever puts `required` (or any other `RegisterOptions` key react-hook-form evaluates before `validate`) into its field contract, so the fieldset declares no baseline at all. Requiredness on the phone control rides only on its own `requirement` prop, which `PhoneField` passes to `useBoundField` as a raise: mandatory-and-empty means blocked, optional means clean. Because the baseline is empty here, the raise is the whole of it — where a fieldset does declare a baseline, `'optional'` could not lower it. A first-name or last-name control can never be required today — only the phone control has a `requirement` prop, because only `PhoneField` implements one.
151
+
152
+ ---
153
+
154
+ ## Common mistakes
155
+
156
+ - **Confusing `ClientProfileFieldsetPhoneControl` with `ClientProfilePhoneControl`.** See *Naming collision* above — they require different contexts and neither compiles under the other's Root.
157
+ - **Composing this fieldset without a `FormRoot` ancestor.** It requires both `form` and `session`; a `SessionRoot` alone is not enough.
158
+ - **Expecting a submit button from this fieldset.** It renders none — compose `FormSubmit` once, anywhere inside the enclosing `FormRoot`.
159
+ - **Adding a `required` prop to a text control.** There is none — the fieldset never blocks a submit on requiredness for a field it did not choose to gate. See *Switching the phone field off*.
160
+ - **Setting `requirement="hidden"` to hide the phone field.** The value does not exist; TypeScript rejects it and a generated store fails `next build`. Omit `ClientProfileFieldsetPhoneControl` from the page instead.
161
+ - **Composing `ClientProfileFirstNameControl` (or any control here) outside `ClientProfileFieldset`.** Every control `requiresContext: 'client-profile-fieldset'` and throws otherwise.
162
+
163
+ ---
164
+
165
+ ## Styling
166
+
167
+ | Component | What you style | Plausible classes |
168
+ |---|---|---|
169
+ | `ClientProfileFieldset` | The `data-fs-client-profile-fieldset` root; carries `-pending`/`-readonly` presence attributes | `flex flex-col gap-4` |
170
+ | Control wrapper | `data-fs-client-profile-fieldset-field="first-name\|last-name\|phone\|document"` | `flex flex-col gap-1` |
171
+ | `ClientProfileFieldsetSkeleton` | `data-fs-client-profile-fieldset-skeleton` and its `-skeleton-field` rows | `flex flex-col gap-4`, rows `h-11 rounded-lg bg-gray-100 animate-pulse` |
172
+ | Announcement | `data-fs-client-profile-fieldset-announcement` reaches the live region carrying `loadingLabel`/`loadedLabel`. Hide it with a clipping technique, never `display:none` — that drops it from the accessibility tree and defeats the announcement | `sr-only` |
173
+
174
+ Field-level visuals (borders, focus rings, error color) are owned by the composed particles' own swatches (`field`, `phone-field`, `document-field`) and are not redefined here.
175
+
176
+ ---
177
+
178
+ ## Related
179
+
180
+ - [[client-profile]] — the standalone, on-blur phone binding; see *Naming collision* above.
181
+ - [[client-preferences-fieldset]] — the sibling fieldset for the other entity a checkout identity step commonly needs; composes into the same `FormRoot`.
182
+ - [[session]] — provides the identification tri-state and the required `SessionRoot` ancestor.
183
+ - [[form]] — `FormRoot`/`FormSubmit`/`FormError`/`FormClear`/`FormReset`, the enclosing form this fieldset registers with.
184
+ - [[phone-field]], [[document-field]], [[field]], [[fieldset]] — the particles this matter composes directly.
@@ -58,7 +58,7 @@ Binds the reusable `PhoneField` particle to the profile. Reads the saved phone f
58
58
  |------|------|---------|-------|
59
59
  | `label` | string | `'Phone'` | Visible field label. |
60
60
  | `country` | `'US' \| 'BR'` | `'US'` | Selects the phone mask and validation rule. |
61
- | `requirement` | `'mandatory' \| 'optional' \| 'hidden'` | `'mandatory'` | Controls visibility and required validation. |
61
+ | `requirement` | `'mandatory' \| 'optional'` | `'mandatory'` | Controls required validation. A field the store does not want is omitted from the page rather than hidden by a prop. |
62
62
  | `errorInvalid` | string | `'Invalid phone number'` | Inline error for an invalid format. |
63
63
  | `errorRequired` | string | `'Phone number is required'` | Inline error when a mandatory field is empty. |
64
64
  | `$actions` | injected | — | Carries `identity.clientProfileUpdate` (compiler-injected). |
@@ -28,15 +28,24 @@ If the image shows a checkout identification step with a tax-document field, use
28
28
 
29
29
  ## Composition
30
30
 
31
- `DocumentField` is a single leaf particle. Internally it renders:
31
+ `DocumentField` provides `document-field` context and, childless, renders:
32
32
 
33
33
  ```
34
- DocumentField (data-fs-document-field + inherited data-fs-field-root)
34
+ DocumentField (data-fs-document-field + data-fs-field-root)
35
35
  ├── Field.Label (data-fs-field-label; the `label` prop)
36
- ├── Field.Control (data-fs-field-control; native <input inputmode="numeric">)
36
+ ├── DocumentFieldInput (data-fs-field-control; native <input inputmode="numeric">)
37
37
  └── Field.Error (data-fs-field-error; the `errorMessage`, shown when invalid)
38
38
  ```
39
39
 
40
+ **Recomposable (RFC:503):** given children, `DocumentField` renders them in place of the three parts
41
+ above, in any order. `DocumentFieldInput` is the one catalogued part that requires `document-field`
42
+ context — it reads the masked value, the composed ref and the particle's own `onChange`/`onBlur` from
43
+ `DocumentField` itself, so it must sit inside it; it carries the same `data-fs-field-control` hook the
44
+ default anatomy always used, so a recomposed page keeps its existing CSS. A composed childless
45
+ `Field.Label` / `Field.Error` (the generic parts from the `field` particle) works exactly as it does under
46
+ any other bound Control; a composed generic `Field.Control` instead re-registers the field with
47
+ react-hook-form and loses the mask — unsupported, the RFC's named double-binder hole.
48
+
40
49
  ---
41
50
 
42
51
  ## Props
@@ -52,9 +61,11 @@ DocumentField (data-fs-document-field + inherited data-fs-field-roo
52
61
  | `label` | string | — | Accessible label. |
53
62
  | `placeholder` | string | — | Input placeholder. |
54
63
  | `disabled` | boolean | `false` | Disables the input (e.g. while a write is in flight). |
55
- | `required` | boolean | `false` | CPF is Optional by Recommendation — default false (unlike email). |
64
+ | `requirement` | `'mandatory' \| 'optional'` | — | Raises the field to required, announced as `aria-required` (never the native attribute). Unset is no opinion, which is what keeps CPF Optional by Recommendation, unlike email. Bound to a fieldset whose rule already says required, `'optional'` does not lower it. |
56
65
  | `name` | string | `'document'` | Field/input name. |
57
66
  | `documentType` | `'CPF'` | `'CPF'` | Selects the mask + validator + digit-only input mode. |
67
+ | `ref` | `Ref<HTMLInputElement>` | — | Lands on the `<input>` `Field.Control` renders — **not on the wrapper `<div>`, as it did before** (a deliberate, source-breaking change). Composed with react-hook-form's own ref on the bound path — a caller ref and react-hook-form's ref are both honoured. |
68
+ | `children` | `ReactNode` | — | Given, replaces the default `Field.Label` + `DocumentFieldInput` + `Field.Error` anatomy, in any order (RFC:503). |
58
69
 
59
70
  **Masking:** applied progressively on every change; input is never blocked. Digit-only masks (CPF) strip non-digits and hint `inputMode="numeric"`.
60
71
 
@@ -62,6 +73,49 @@ DocumentField (data-fs-document-field + inherited data-fs-field-roo
62
73
 
63
74
  ---
64
75
 
76
+ ## Bound behaviour
77
+
78
+ `DocumentField` reads the field contract through `useField()` and binds through `useController` when both
79
+ a field contract (`FieldBinding`) and a react-hook-form `FormProvider` are present above it; otherwise it
80
+ runs the plain, unbound behaviour documented above, unaltered.
81
+
82
+ - **What the contract supplies**: `label` and `placeholder`, when the particle's own prop is omitted (an
83
+ explicit prop always wins); `defaultValue`, seeded into `useController`; and `rule`, merged with the
84
+ particle's own check-digit validator (below).
85
+ - **What stays the particle's**: the CPF mask, `documentType`, and the `errorMessage` copy.
86
+ - **Single binder, real contract for the read-only parts (RFC:503)**: the composite remains the sole
87
+ binder for its field — `useController` is called once, here. Its default (and a recomposed) anatomy's
88
+ input is `DocumentFieldInput`, which renders the raw Base UI `Field.Control` directly rather than this
89
+ package's own bound-aware wrapper, so it never independently re-registers the name and overwrites the
90
+ composite's masked value. `Field.Label` / `Field.Error` composed by an author, by contrast, do read the
91
+ real (non-empty) re-provided contract — carrying the *resolved* `name`/`label`/`placeholder` — so a
92
+ composed childless `Field.Label` renders real text; a composed childless `Field.Error` renders
93
+ react-hook-form's real message only when one exists — when `errorMessage` is unset the check-digit
94
+ validator reports failure with a bare `false`, react-hook-form's message is empty, and `Field.Error`
95
+ renders nothing, exactly like the default anatomy below. An earlier design (`FieldContractBoundary`)
96
+ pushed an empty contract instead, which is why a composed childless label used to render nothing; that
97
+ mechanism is gone.
98
+ - **Value shape stored in react-hook-form**: the same **masked** string the unbound particle emits
99
+ through `onValueChange` (e.g. `111.444.777-35`), matching the asymmetry with `PhoneField`'s unmasked
100
+ storage — pre-existing and preserved on purpose.
101
+ - **Validation**: when the contract's `rule` carries no `validate` of its own, the composite installs its
102
+ own check-digit validator as the rule's `validate`, returning `errorMessage` on failure (or a bare
103
+ `false` when `errorMessage` is unset) — reproducing today's behaviour exactly: the field goes invalid,
104
+ and inline text renders only when `errorMessage` is supplied. A contract-supplied `validate` always wins
105
+ over the particle's own. An empty value still passes (today's optional-field rule).
106
+ - **Focus**: the `ref` lands on the `<input>` `DocumentFieldInput` renders, so a failed submit moves focus
107
+ to it — in the default anatomy and in a recomposed one, since the composed ref threads through
108
+ regardless of where the author places the part.
109
+ - **Explicit `invalid`/`errorMessage` props still surface** on the default (childless) anatomy
110
+ unconditionally — it renders the raw Base UI `Field.Label`/`Field.Error` primitives directly, so it
111
+ never gates on react-hook-form's own error state the way a composed childless `Field.Error` intentionally
112
+ does (above).
113
+ - `DocumentFieldDefinition` gains no `requiresContext` — the particle stays placeable outside any form. It
114
+ does declare `providesContext: 'document-field'` — that is what makes `DocumentFieldInput` a legal
115
+ child.
116
+
117
+ ---
118
+
65
119
  ## Accessibility
66
120
 
67
121
  The label is associated with the input and the error is linked via `aria-describedby` automatically by the Base UI `Field` primitive (WCAG 2.1 AA). Standard text-input keyboard editing; masking/validation never blocks typing.