@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.
- package/CHANGELOG.md +45 -0
- package/dist/actions.json +181 -0
- package/dist/catalog.json +1543 -495
- package/dist/client.d.ts +2 -1
- package/dist/client.js +2 -1
- package/dist/components.d.ts +28 -7
- package/dist/components.js +31 -12
- package/dist/docs/access-code-sign-in.md +1 -1
- package/dist/docs/address-summary.md +24 -3
- package/dist/docs/alert-dialog.md +204 -0
- package/dist/docs/billing-address.md +284 -0
- package/dist/docs/card-entry.md +250 -0
- package/dist/docs/cardholder-document.md +142 -0
- package/dist/docs/client-preferences-fieldset.md +148 -0
- package/dist/docs/client-preferences.md +2 -1
- package/dist/docs/client-profile-fieldset.md +184 -0
- package/dist/docs/client-profile.md +1 -1
- package/dist/docs/document-field.md +58 -4
- package/dist/docs/field.md +317 -0
- package/dist/docs/form.md +307 -0
- package/dist/docs/identity-edit.md +6 -6
- package/dist/docs/identity-summary.md +5 -5
- package/dist/docs/installments.md +248 -0
- package/dist/docs/my-account-profile.md +1 -0
- package/dist/docs/order-placed-payment-summary.md +138 -0
- package/dist/docs/payment-method-badges.md +232 -0
- package/dist/docs/payment-methods.md +1 -0
- package/dist/docs/payment.md +218 -0
- package/dist/docs/phone-field.md +58 -4
- package/dist/docs/place-order.md +157 -0
- package/dist/docs/save-card.md +186 -0
- package/dist/docs/saved-cards.md +218 -0
- package/dist/docs/select.md +465 -0
- package/dist/docs/shipping-address.md +110 -258
- package/dist/index.d.ts +4 -1
- package/dist/internal/client.d.ts +3 -1
- package/dist/internal/client.js +3 -1
- package/dist/matter/access-code-sign-in/access-code-sign-in.js +9 -8
- package/dist/matter/access-code-sign-in/access-code-sign-in.js.map +1 -1
- package/dist/matter/billing-address/billing-address.d.ts +165 -0
- package/dist/matter/billing-address/billing-address.js +408 -0
- package/dist/matter/billing-address/billing-address.js.map +1 -0
- package/dist/matter/card-entry/card-capture.d.ts +25 -0
- package/dist/matter/card-entry/card-capture.js +25 -0
- package/dist/matter/card-entry/card-capture.js.map +1 -0
- package/dist/matter/card-entry/card-entry.d.ts +120 -0
- package/dist/matter/card-entry/card-entry.js +832 -0
- package/dist/matter/card-entry/card-entry.js.map +1 -0
- package/dist/matter/cardholder-document/cardholder-document.d.ts +45 -0
- package/dist/matter/cardholder-document/cardholder-document.js +70 -0
- package/dist/matter/cardholder-document/cardholder-document.js.map +1 -0
- package/dist/matter/client-preferences/client-preferences-fieldset.d.ts +77 -0
- package/dist/matter/client-preferences/client-preferences-fieldset.js +299 -0
- package/dist/matter/client-preferences/client-preferences-fieldset.js.map +1 -0
- package/dist/matter/client-preferences/client-preferences.d.ts +3 -5
- package/dist/matter/client-preferences/client-preferences.js +6 -6
- package/dist/matter/client-preferences/client-preferences.js.map +1 -1
- package/dist/matter/client-profile/client-profile.d.ts +4 -6
- package/dist/matter/client-profile/client-profile.js +9 -6
- package/dist/matter/client-profile/client-profile.js.map +1 -1
- package/dist/matter/client-profile-form/client-profile-fieldset.d.ts +119 -0
- package/dist/matter/client-profile-form/client-profile-fieldset.js +488 -0
- package/dist/matter/client-profile-form/client-profile-fieldset.js.map +1 -0
- package/dist/matter/identity-edit/identity-edit.js +2 -2
- package/dist/matter/identity-edit/identity-edit.js.map +1 -1
- package/dist/matter/identity-summary/identity-summary.js +2 -2
- package/dist/matter/identity-summary/identity-summary.js.map +1 -1
- package/dist/matter/installments/installments-select.d.ts +42 -0
- package/dist/matter/installments/installments-select.js +167 -0
- package/dist/matter/installments/installments-select.js.map +1 -0
- package/dist/matter/installments/installments.d.ts +58 -0
- package/dist/matter/installments/installments.js +176 -0
- package/dist/matter/installments/installments.js.map +1 -0
- package/dist/matter/installments/use-installments-selection.d.ts +29 -0
- package/dist/matter/installments/use-installments-selection.js +95 -0
- package/dist/matter/installments/use-installments-selection.js.map +1 -0
- package/dist/matter/my-account-profile/my-account-profile.js +4 -4
- package/dist/matter/my-account-profile/my-account-profile.js.map +1 -1
- package/dist/matter/order-placed-payment-summary/order-placed-payment-summary.d.ts +63 -0
- package/dist/matter/order-placed-payment-summary/order-placed-payment-summary.js +305 -0
- package/dist/matter/order-placed-payment-summary/order-placed-payment-summary.js.map +1 -0
- package/dist/matter/payment/payment-card-capture.d.ts +19 -0
- package/dist/matter/payment/payment-card-capture.js +129 -0
- package/dist/matter/payment/payment-card-capture.js.map +1 -0
- package/dist/matter/payment/payment-controller.d.ts +24 -0
- package/dist/matter/payment/payment-controller.js +107 -0
- package/dist/matter/payment/payment-controller.js.map +1 -0
- package/dist/matter/payment/payment-method-badges.d.ts +57 -0
- package/dist/matter/payment/payment-method-badges.js +192 -0
- package/dist/matter/payment/payment-method-badges.js.map +1 -0
- package/dist/matter/payment/payment.d.ts +89 -0
- package/dist/matter/payment/payment.js +263 -0
- package/dist/matter/payment/payment.js.map +1 -0
- package/dist/matter/payment/use-payment-method-selection.d.ts +302 -0
- package/dist/matter/payment/use-payment-method-selection.js +458 -0
- package/dist/matter/payment/use-payment-method-selection.js.map +1 -0
- package/dist/matter/payment/use-payment-submit-gate.d.ts +111 -0
- package/dist/matter/payment/use-payment-submit-gate.js +144 -0
- package/dist/matter/payment/use-payment-submit-gate.js.map +1 -0
- package/dist/matter/place-order/place-order.d.ts +74 -0
- package/dist/matter/place-order/place-order.js +379 -0
- package/dist/matter/place-order/place-order.js.map +1 -0
- package/dist/matter/product-list/product-list.d.ts +1 -0
- package/dist/matter/product-list/product-list.js.map +1 -1
- package/dist/matter/product-shelf/product-shelf.d.ts +1 -0
- package/dist/matter/product-shelf/product-shelf.js +2 -0
- package/dist/matter/product-shelf/product-shelf.js.map +1 -1
- package/dist/matter/region/region.js +1 -1
- package/dist/matter/region/region.js.map +1 -1
- package/dist/matter/save-card/save-card.d.ts +49 -0
- package/dist/matter/save-card/save-card.js +188 -0
- package/dist/matter/save-card/save-card.js.map +1 -0
- package/dist/matter/saved-cards/saved-cards.d.ts +79 -0
- package/dist/matter/saved-cards/saved-cards.js +387 -0
- package/dist/matter/saved-cards/saved-cards.js.map +1 -0
- package/dist/matter/shipping-address/country-descriptors.js +20 -10
- package/dist/matter/shipping-address/country-descriptors.js.map +1 -1
- package/dist/matter/shipping-address/shipping-address.d.ts +113 -174
- package/dist/matter/shipping-address/shipping-address.js +480 -791
- package/dist/matter/shipping-address/shipping-address.js.map +1 -1
- package/dist/matter/variant-selector/variant-selector.js +1 -1
- package/dist/particles/address-summary/address-summary.d.ts +24 -0
- package/dist/particles/address-summary/address-summary.js +83 -2
- package/dist/particles/address-summary/address-summary.js.map +1 -1
- package/dist/particles/alert-dialog/alert-dialog.d.ts +68 -0
- package/dist/particles/alert-dialog/alert-dialog.js +187 -0
- package/dist/particles/alert-dialog/alert-dialog.js.map +1 -0
- package/dist/particles/checkbox/checkbox.d.ts +7 -3
- package/dist/particles/checkbox/checkbox.js +4 -1
- package/dist/particles/checkbox/checkbox.js.map +1 -1
- package/dist/particles/document-field/document-field.d.ts +48 -5
- package/dist/particles/document-field/document-field.js +189 -32
- package/dist/particles/document-field/document-field.js.map +1 -1
- package/dist/particles/field/bound-field-registry.js +52 -0
- package/dist/particles/field/bound-field-registry.js.map +1 -0
- package/dist/particles/field/field.d.ts +116 -0
- package/dist/particles/field/field.js +497 -6
- package/dist/particles/field/field.js.map +1 -1
- package/dist/particles/fieldset/fieldset.js +2 -2
- package/dist/particles/fieldset/fieldset.js.map +1 -1
- package/dist/particles/form/form.d.ts +104 -0
- package/dist/particles/form/form.js +485 -0
- package/dist/particles/form/form.js.map +1 -0
- package/dist/particles/phone-field/phone-field.d.ts +38 -10
- package/dist/particles/phone-field/phone-field.js +183 -39
- package/dist/particles/phone-field/phone-field.js.map +1 -1
- package/dist/particles/postal-code-field/postal-code-field.js +2 -2
- package/dist/particles/postal-code-field/postal-code-field.js.map +1 -1
- package/dist/particles/select/field-select.d.ts +43 -0
- package/dist/particles/select/field-select.js +150 -0
- package/dist/particles/select/field-select.js.map +1 -0
- package/dist/particles/select/select.d.ts +2 -0
- package/dist/particles/select/select.js.map +1 -1
- package/dist/particles/shared/compose-refs.js +21 -0
- package/dist/particles/shared/compose-refs.js.map +1 -0
- package/dist/schema.graphql +701 -2
- package/dist/sdk/actions/analytics-action-provider.d.ts +15 -0
- package/dist/sdk/actions/analytics-action-provider.js +19 -0
- package/dist/sdk/actions/analytics-action-provider.js.map +1 -0
- package/dist/sdk/actions/index.d.ts +3 -1
- package/dist/sdk/actions/index.js +3 -1
- package/dist/sdk/actions/payment-action-provider.d.ts +22 -0
- package/dist/sdk/actions/payment-action-provider.js +26 -0
- package/dist/sdk/actions/payment-action-provider.js.map +1 -0
- package/dist/sdk/actions/types.d.ts +6 -2
- package/dist/sdk/analytics/analytics-destination.d.ts +43 -0
- package/dist/sdk/analytics/analytics-destination.js +90 -0
- package/dist/sdk/analytics/analytics-destination.js.map +1 -0
- package/dist/sdk/analytics/use-analytics-actions.d.ts +57 -0
- package/dist/sdk/analytics/use-analytics-actions.js +108 -0
- package/dist/sdk/analytics/use-analytics-actions.js.map +1 -0
- package/dist/sdk/cart/use-cart-actions.js +1 -1
- package/dist/sdk/data/entity-resolution-context.js +31 -0
- package/dist/sdk/data/entity-resolution-context.js.map +1 -0
- package/dist/sdk/data/entity-subscriber.js +18 -8
- package/dist/sdk/data/entity-subscriber.js.map +1 -1
- package/dist/sdk/data/index.js +1 -0
- package/dist/sdk/data/page-reactive-data-root.d.ts +36 -0
- package/dist/sdk/data/page-reactive-data-root.js +4 -2
- package/dist/sdk/data/page-reactive-data-root.js.map +1 -1
- package/dist/sdk/data/reactive-data-provider.js +7 -3
- package/dist/sdk/data/reactive-data-provider.js.map +1 -1
- package/dist/sdk/data/reactive-list-item-data-provider.js +12 -4
- package/dist/sdk/data/reactive-list-item-data-provider.js.map +1 -1
- package/dist/sdk/data/use-reactive-data-settled.js +39 -0
- package/dist/sdk/data/use-reactive-data-settled.js.map +1 -0
- package/dist/sdk/data/use-reactive-entity.js +5 -1
- package/dist/sdk/data/use-reactive-entity.js.map +1 -1
- package/dist/sdk/generated/fragment-types.js +2 -0
- package/dist/sdk/generated/fragment-types.js.map +1 -1
- package/dist/sdk/generated/graphql.js.map +1 -1
- package/dist/sdk/i18n/messages/en-US.js +3 -0
- package/dist/sdk/i18n/messages/en-US.json +3 -0
- package/dist/sdk/i18n/messages/en-US.json.d.ts +3 -0
- package/dist/sdk/i18n/messages/es-ES.js +3 -0
- package/dist/sdk/i18n/messages/es-ES.json +3 -0
- package/dist/sdk/i18n/messages/pt-BR.js +3 -0
- package/dist/sdk/i18n/messages/pt-BR.json +3 -0
- package/dist/sdk/identity/use-identity-actions.js +3 -2
- package/dist/sdk/identity/use-identity-actions.js.map +1 -1
- package/dist/sdk/payment/gateway-origin.js +35 -0
- package/dist/sdk/payment/gateway-origin.js.map +1 -0
- package/dist/sdk/payment/gateway.d.ts +85 -0
- package/dist/sdk/payment/gateway.js +183 -0
- package/dist/sdk/payment/gateway.js.map +1 -0
- package/dist/sdk/payment/operations.d.ts +52 -0
- package/dist/sdk/payment/operations.js +223 -0
- package/dist/sdk/payment/operations.js.map +1 -0
- package/dist/sdk/payment/security-code-mask.js +71 -0
- package/dist/sdk/payment/security-code-mask.js.map +1 -0
- package/dist/sdk/payment/submission-registry.d.ts +33 -0
- package/dist/sdk/payment/submission-registry.js +58 -0
- package/dist/sdk/payment/submission-registry.js.map +1 -0
- package/dist/sdk/payment/use-payment-actions.d.ts +78 -0
- package/dist/sdk/payment/use-payment-actions.js +276 -0
- package/dist/sdk/payment/use-payment-actions.js.map +1 -0
- package/dist/sdk/payment/use-place-order.d.ts +65 -0
- package/dist/sdk/payment/use-place-order.js +640 -0
- package/dist/sdk/payment/use-place-order.js.map +1 -0
- package/dist/sdk/shipping/use-shipping-actions.js +1 -1
- package/dist/utils/loggable-error-name.js +49 -0
- package/dist/utils/loggable-error-name.js.map +1 -0
- package/dist/utils/resolve-authored-copy.js +18 -0
- package/dist/utils/resolve-authored-copy.js.map +1 -0
- package/dist/utils/substitute-template.js +33 -0
- package/dist/utils/substitute-template.js.map +1 -0
- package/package.json +2 -1
- package/src/styles/tailwind.css +34 -0
- package/dist/docs/client-profile-form.md +0 -192
- package/dist/matter/client-profile-form/client-profile-form.d.ts +0 -126
- package/dist/matter/client-profile-form/client-profile-form.js +0 -662
- package/dist/matter/client-profile-form/client-profile-form.js.map +0 -1
|
@@ -0,0 +1,488 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { defineComponent } from "../../core/dist/define-component.js";
|
|
3
|
+
import "../../core/dist/index.js";
|
|
4
|
+
import { Field, FieldBinding, FieldsetFieldContext, useFieldsetField, useFieldsetFieldNames, useNoAncestorFieldset } from "../../particles/field/field.js";
|
|
5
|
+
import { useActionsOverride } from "../../sdk/actions/actions-override-context.js";
|
|
6
|
+
import { useDataOverride } from "../../sdk/data/data-override-context.js";
|
|
7
|
+
import { useSession } from "../session/session.js";
|
|
8
|
+
import { Fieldset } from "../../particles/fieldset/fieldset.js";
|
|
9
|
+
import { DOCUMENTS, DocumentField, isSupportedDocumentType } from "../../particles/document-field/document-field.js";
|
|
10
|
+
import { useFormRegistry } from "../../particles/form/form.js";
|
|
11
|
+
import { useEntityDataResolved } from "../../sdk/data/entity-resolution-context.js";
|
|
12
|
+
import { useReactiveDataSettled } from "../../sdk/data/use-reactive-data-settled.js";
|
|
13
|
+
import { PhoneField, toE164 } from "../../particles/phone-field/phone-field.js";
|
|
14
|
+
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
|
|
15
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
16
|
+
import { useFormContext } from "react-hook-form";
|
|
17
|
+
//#region src/matter/client-profile-form/client-profile-fieldset.tsx
|
|
18
|
+
/**
|
|
19
|
+
* `ClientProfileFieldset` — the composable, one-Save write of the `ClientProfile` sibling entity
|
|
20
|
+
* (RFC: Composable Entity Forms, `Proposal > Design > The fieldset`). It registers itself with the
|
|
21
|
+
* enclosing `FormRoot` under the `clientProfile` namespace and dispatches
|
|
22
|
+
* `$actions.identity.clientProfileUpdate` for its own submit slot; it renders no `<form>` of its own.
|
|
23
|
+
*
|
|
24
|
+
* The four controls below are the smallest placeable unit: each calls `useFieldsetField` for its full
|
|
25
|
+
* field name, renders a `FieldBinding` carrying the field's static facts, and — inside it — either the
|
|
26
|
+
* default anatomy or the author's own children. No control's field contract carries a blocking
|
|
27
|
+
* `RegisterOptions` key (no `required`, `min`, `max`, `minLength`, `maxLength`, `pattern`); requiredness
|
|
28
|
+
* on the phone control rides on its own `requirement` prop instead, which only ever gates required
|
|
29
|
+
* validation — a field the store does not want is omitted from the page, not switched off by a prop.
|
|
30
|
+
*
|
|
31
|
+
* The fieldset's context name is `client-profile-fieldset`, not `client-profile`: the shipped
|
|
32
|
+
* `ClientProfilePhoneControl` (`matter/client-profile/client-profile.tsx`) already requires
|
|
33
|
+
* `client-profile` and writes on blur, so reusing that name here would let an on-blur control compile
|
|
34
|
+
* inside this batched fieldset and write immediately. `ClientProfileFieldsetPhoneControl` carries the
|
|
35
|
+
* same container prefix as the context it requires, naming the collision rather than hiding it.
|
|
36
|
+
*/
|
|
37
|
+
const fragment = `
|
|
38
|
+
fragment ClientProfileFieldsetData on Query {
|
|
39
|
+
clientProfile {
|
|
40
|
+
__typename
|
|
41
|
+
id
|
|
42
|
+
firstName
|
|
43
|
+
lastName
|
|
44
|
+
phone
|
|
45
|
+
document
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
`;
|
|
49
|
+
/** No control publishes a rule today — a blocking `RegisterOptions` key would reopen the same submit
|
|
50
|
+
* block for a read-only session (Decision 6): `readOnly` disables a control's rendered `<input>` via a
|
|
51
|
+
* plain HTML attribute, never react-hook-form's own `disabled` registration option (`field.tsx`'s
|
|
52
|
+
* `useController` call takes no `disabled`), so a required field would stay fully registered and
|
|
53
|
+
* validated while the shopper has no way to focus or edit it. Kept as a stable empty object so the
|
|
54
|
+
* context value's identity does not churn on every render. */
|
|
55
|
+
const NO_RULES = {};
|
|
56
|
+
/**
|
|
57
|
+
* Rows `ClientProfileFieldsetSkeleton` draws when nobody says otherwise: one per control in the full
|
|
58
|
+
* anatomy — first name, last name, phone, document. It is a default and not a derivation, because
|
|
59
|
+
* the composed children are opaque here. A page that composes fewer sets `skeletonCount` to what it
|
|
60
|
+
* composed, or the form jumps by the difference when the controls take the skeleton's place.
|
|
61
|
+
*/
|
|
62
|
+
const CLIENT_PROFILE_SKELETON_ROWS = 4;
|
|
63
|
+
const CLIENT_PROFILE_FIELDSET_UNSET = Symbol("ClientProfileFieldsetContextUnset");
|
|
64
|
+
const ClientProfileFieldsetContext = createContext(CLIENT_PROFILE_FIELDSET_UNSET);
|
|
65
|
+
function useClientProfileFieldsetContext() {
|
|
66
|
+
const ctx = useContext(ClientProfileFieldsetContext);
|
|
67
|
+
if (ctx === CLIENT_PROFILE_FIELDSET_UNSET) throw new Error("ClientProfileFieldset controls must be used inside ClientProfileFieldset.");
|
|
68
|
+
return ctx;
|
|
69
|
+
}
|
|
70
|
+
const ClientProfileFieldsetDefinition = defineComponent({
|
|
71
|
+
name: "ClientProfileFieldset",
|
|
72
|
+
importPath: "@vtex/faststore/components",
|
|
73
|
+
component: ClientProfileFieldset,
|
|
74
|
+
fragment,
|
|
75
|
+
fetchStrategy: "reactive",
|
|
76
|
+
skeleton: ClientProfileFieldsetSkeleton,
|
|
77
|
+
requiresContext: ["form", "session"],
|
|
78
|
+
providesContext: "client-profile-fieldset",
|
|
79
|
+
actions: { identity: ["clientProfileUpdate"] },
|
|
80
|
+
editor: {
|
|
81
|
+
label: "Client Profile Fieldset",
|
|
82
|
+
category: "identity"
|
|
83
|
+
},
|
|
84
|
+
schema: {
|
|
85
|
+
country: {
|
|
86
|
+
type: "string",
|
|
87
|
+
default: "US",
|
|
88
|
+
label: "Phone country (US or BR)"
|
|
89
|
+
},
|
|
90
|
+
documentType: {
|
|
91
|
+
type: "string",
|
|
92
|
+
label: "Document type (e.g. CPF)"
|
|
93
|
+
},
|
|
94
|
+
skeletonCount: {
|
|
95
|
+
type: "number",
|
|
96
|
+
default: 4,
|
|
97
|
+
label: "Skeleton rows"
|
|
98
|
+
},
|
|
99
|
+
loadingLabel: {
|
|
100
|
+
type: "string",
|
|
101
|
+
default: "Loading your details",
|
|
102
|
+
label: "Loading announcement"
|
|
103
|
+
},
|
|
104
|
+
loadedLabel: {
|
|
105
|
+
type: "string",
|
|
106
|
+
default: "Your details are ready",
|
|
107
|
+
label: "Loaded announcement"
|
|
108
|
+
},
|
|
109
|
+
groupLabel: {
|
|
110
|
+
type: "string",
|
|
111
|
+
default: "Your details",
|
|
112
|
+
label: "Accessible group name"
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
guideline: "Registers a batched write of the ClientProfile entity with the enclosing FormRoot; renders one Fieldset.Root and no <form>. Must be inside a FormRoot with a SessionRoot ancestor. Compose ClientProfileFirstNameControl / ClientProfileLastNameControl / ClientProfileFieldsetPhoneControl / ClientProfileDocumentControl as children, in any order. The controls mount only once the profile has resolved, with skeletonCount placeholder rows in their place until then — set it to the number of controls composed, or the form jumps by the difference. Style the announcement sr-only. Dispatches only the fields the shopper changed. groupLabel names the group as its accessible name; blank emits no aria-label, and a composed FieldsetLegend wins over it."
|
|
116
|
+
});
|
|
117
|
+
const ClientProfileFirstNameControlDefinition = defineComponent({
|
|
118
|
+
name: "ClientProfileFirstNameControl",
|
|
119
|
+
importPath: "@vtex/faststore/components",
|
|
120
|
+
component: ClientProfileFirstNameControl,
|
|
121
|
+
requiresContext: "client-profile-fieldset",
|
|
122
|
+
providesContext: "field",
|
|
123
|
+
editor: {
|
|
124
|
+
label: "Client Profile First Name Control",
|
|
125
|
+
category: "identity"
|
|
126
|
+
},
|
|
127
|
+
schema: {
|
|
128
|
+
label: {
|
|
129
|
+
type: "string",
|
|
130
|
+
label: "Label"
|
|
131
|
+
},
|
|
132
|
+
placeholder: {
|
|
133
|
+
type: "string",
|
|
134
|
+
label: "Placeholder"
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
guideline: "The bound first-name control; must be a descendant of ClientProfileFieldset. Renders Field.Root > Field.Label + Field.Control + Field.Error by default, or the composed children in the author's own order."
|
|
138
|
+
});
|
|
139
|
+
const ClientProfileLastNameControlDefinition = defineComponent({
|
|
140
|
+
name: "ClientProfileLastNameControl",
|
|
141
|
+
importPath: "@vtex/faststore/components",
|
|
142
|
+
component: ClientProfileLastNameControl,
|
|
143
|
+
requiresContext: "client-profile-fieldset",
|
|
144
|
+
providesContext: "field",
|
|
145
|
+
editor: {
|
|
146
|
+
label: "Client Profile Last Name Control",
|
|
147
|
+
category: "identity"
|
|
148
|
+
},
|
|
149
|
+
schema: {
|
|
150
|
+
label: {
|
|
151
|
+
type: "string",
|
|
152
|
+
label: "Label"
|
|
153
|
+
},
|
|
154
|
+
placeholder: {
|
|
155
|
+
type: "string",
|
|
156
|
+
label: "Placeholder"
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
guideline: "The bound last-name control; must be a descendant of ClientProfileFieldset. Renders Field.Root > Field.Label + Field.Control + Field.Error by default, or the composed children in the author's own order."
|
|
160
|
+
});
|
|
161
|
+
const ClientProfileFieldsetPhoneControlDefinition = defineComponent({
|
|
162
|
+
name: "ClientProfileFieldsetPhoneControl",
|
|
163
|
+
importPath: "@vtex/faststore/components",
|
|
164
|
+
component: ClientProfileFieldsetPhoneControl,
|
|
165
|
+
requiresContext: "client-profile-fieldset",
|
|
166
|
+
providesContext: ["field", "phone-field"],
|
|
167
|
+
editor: {
|
|
168
|
+
label: "Client Profile Fieldset Phone Control",
|
|
169
|
+
category: "identity"
|
|
170
|
+
},
|
|
171
|
+
schema: {
|
|
172
|
+
label: {
|
|
173
|
+
type: "string",
|
|
174
|
+
label: "Label"
|
|
175
|
+
},
|
|
176
|
+
placeholder: {
|
|
177
|
+
type: "string",
|
|
178
|
+
label: "Placeholder"
|
|
179
|
+
},
|
|
180
|
+
requirement: {
|
|
181
|
+
type: ["mandatory", "optional"],
|
|
182
|
+
label: "Requirement"
|
|
183
|
+
},
|
|
184
|
+
errorInvalid: {
|
|
185
|
+
type: "string",
|
|
186
|
+
label: "Invalid error message"
|
|
187
|
+
},
|
|
188
|
+
errorRequired: {
|
|
189
|
+
type: "string",
|
|
190
|
+
label: "Required error message"
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
guideline: "The bound phone control; must be a descendant of ClientProfileFieldset. Distinct from ClientProfilePhoneControl (matter/client-profile), which writes on blur and requires the client-profile context — the two cannot compile into each other's container. Composes the PhoneField particle: recomposable (RFC:503) — given children, they replace PhoneField's default Field.Label + PhoneFieldInput + Field.Error anatomy, in any order; childless keeps today's order. Its className styles PhoneField's own root, the node that lays out label, input and error — not the wrapper, which carries only the data-fs-client-profile-fieldset-field=\"phone\" styling hook. A store that does not want a phone field omits this element from the page: requirement only governs required validation, never visibility."
|
|
194
|
+
});
|
|
195
|
+
const ClientProfileDocumentControlDefinition = defineComponent({
|
|
196
|
+
name: "ClientProfileDocumentControl",
|
|
197
|
+
importPath: "@vtex/faststore/components",
|
|
198
|
+
component: ClientProfileDocumentControl,
|
|
199
|
+
requiresContext: "client-profile-fieldset",
|
|
200
|
+
providesContext: ["field", "document-field"],
|
|
201
|
+
editor: {
|
|
202
|
+
label: "Client Profile Document Control",
|
|
203
|
+
category: "identity"
|
|
204
|
+
},
|
|
205
|
+
schema: {
|
|
206
|
+
label: {
|
|
207
|
+
type: "string",
|
|
208
|
+
label: "Label"
|
|
209
|
+
},
|
|
210
|
+
placeholder: {
|
|
211
|
+
type: "string",
|
|
212
|
+
label: "Placeholder"
|
|
213
|
+
},
|
|
214
|
+
errorMessage: {
|
|
215
|
+
type: "string",
|
|
216
|
+
label: "Error message"
|
|
217
|
+
}
|
|
218
|
+
},
|
|
219
|
+
guideline: "The bound tax-document control; must be a descendant of ClientProfileFieldset. Renders nothing, and registers no field name, when the fieldset's documentType is unsupported. Composes the DocumentField particle: recomposable (RFC:503) — given children, they replace DocumentField's default Field.Label + DocumentFieldInput + Field.Error anatomy, in any order; childless keeps today's order."
|
|
220
|
+
});
|
|
221
|
+
/**
|
|
222
|
+
* Registers a batched write of the `ClientProfile` entity with the enclosing `FormRoot`
|
|
223
|
+
* (`registerFieldset`, namespace `clientProfile`) on mount, unconditionally — so this fieldset keeps its
|
|
224
|
+
* original dispatch slot even while its own query is still resolving. Its children — the seed values and
|
|
225
|
+
* the field-name registry its controls need — render only once the entity has resolved; until then the
|
|
226
|
+
* `Fieldset.Root` carries `-pending` and renders `ClientProfileFieldsetSkeleton` (Decision 5).
|
|
227
|
+
* `dispatch` and `toPayload` are `useCallback([])`-stable and read `$actions`/`country`/`documentType`
|
|
228
|
+
* through a ref, so the registration effect runs exactly once per mount regardless of how often the
|
|
229
|
+
* parent re-renders.
|
|
230
|
+
*
|
|
231
|
+
* "Resolved" is two conditions, because `$data` alone cannot answer it. Under the reactive runtime the
|
|
232
|
+
* slice is defined from the first render — `useReactiveEntity` hands down `fallbackData` while the cache
|
|
233
|
+
* read is incomplete — so an empty server answer looks exactly like a resolved one, and the controls
|
|
234
|
+
* would mount and seed `''` before the shopper's real profile arrives. `useSeedAdoption` re-seeds a
|
|
235
|
+
* clean field with `defaultValue` on every render, but once the field turns dirty no later seed
|
|
236
|
+
* ever reaches it again: a shopper who starts typing before the real profile arrives locks that
|
|
237
|
+
* empty seed in for good, and even a shopper who does not type sees the field flash from `''` to
|
|
238
|
+
* the real data. `useReactiveDataSettled` supplies the missing half: the page query's own
|
|
239
|
+
* in-flight state, latched so it only ever goes pending → settled. The `$data === undefined` half still
|
|
240
|
+
* carries trees with no reactive root above them, where the slice arrives as a plain prop.
|
|
241
|
+
*
|
|
242
|
+
* Both ends of that wait are spoken: the fieldset carries `aria-busy` while it lasts and holds a polite
|
|
243
|
+
* live region that announces `loadingLabel` and then `loadedLabel`. Style the region `sr-only` — it is
|
|
244
|
+
* text meant for a screen reader, not for the page.
|
|
245
|
+
*/
|
|
246
|
+
function ClientProfileFieldset({ country = "US", documentType, skeletonCount = CLIENT_PROFILE_SKELETON_ROWS, loadingLabel = "Loading your details", loadedLabel = "Your details are ready", groupLabel = "Your details", className, children, $data: $dataProp, $actions: $actionsProp }) {
|
|
247
|
+
useNoAncestorFieldset("ClientProfileFieldset");
|
|
248
|
+
const $data = useDataOverride($dataProp);
|
|
249
|
+
const $actions = useActionsOverride($actionsProp);
|
|
250
|
+
const settled = useReactiveDataSettled();
|
|
251
|
+
const sliceResolved = useEntityDataResolved();
|
|
252
|
+
const resolved = settled && sliceResolved && $data !== void 0;
|
|
253
|
+
const { canEditData } = useSession();
|
|
254
|
+
const readOnly = !canEditData;
|
|
255
|
+
const { registerFieldset } = useFormRegistry();
|
|
256
|
+
const { getFieldState } = useFormContext();
|
|
257
|
+
const latest = useRef({
|
|
258
|
+
$actions,
|
|
259
|
+
country,
|
|
260
|
+
documentType,
|
|
261
|
+
readOnly,
|
|
262
|
+
getFieldState
|
|
263
|
+
});
|
|
264
|
+
useEffect(() => {
|
|
265
|
+
latest.current = {
|
|
266
|
+
$actions,
|
|
267
|
+
country,
|
|
268
|
+
documentType,
|
|
269
|
+
readOnly,
|
|
270
|
+
getFieldState
|
|
271
|
+
};
|
|
272
|
+
}, [
|
|
273
|
+
$actions,
|
|
274
|
+
country,
|
|
275
|
+
documentType,
|
|
276
|
+
readOnly,
|
|
277
|
+
getFieldState
|
|
278
|
+
]);
|
|
279
|
+
const { registry: fieldsetRegistry, getFieldNames } = useFieldsetFieldNames("clientProfile");
|
|
280
|
+
const dispatch = useCallback(async (payload) => {
|
|
281
|
+
if (latest.current.readOnly) throw new Error("ClientProfileFieldset cannot dispatch while the session is read-only.");
|
|
282
|
+
const update = latest.current.$actions?.identity?.clientProfileUpdate;
|
|
283
|
+
if (!update) throw new Error("ClientProfileFieldset requires $actions.identity.clientProfileUpdate.");
|
|
284
|
+
return update(payload);
|
|
285
|
+
}, []);
|
|
286
|
+
const toPayload = useCallback((values) => {
|
|
287
|
+
const { country: activeCountry, getFieldState } = latest.current;
|
|
288
|
+
const isDirty = (key) => getFieldState(`clientProfile.${key}`).isDirty;
|
|
289
|
+
const payload = {};
|
|
290
|
+
if ("firstName" in values && isDirty("firstName")) payload.firstName = String(values.firstName ?? "").trim();
|
|
291
|
+
if ("lastName" in values && isDirty("lastName")) payload.lastName = String(values.lastName ?? "").trim();
|
|
292
|
+
if ("phone" in values && isDirty("phone")) {
|
|
293
|
+
const raw = String(values.phone ?? "").trim();
|
|
294
|
+
payload.phone = raw ? toE164(raw, activeCountry) : "";
|
|
295
|
+
}
|
|
296
|
+
if ("document" in values && isDirty("document")) payload.document = String(values.document ?? "").replace(/\D/g, "");
|
|
297
|
+
return payload;
|
|
298
|
+
}, []);
|
|
299
|
+
useEffect(() => registerFieldset({
|
|
300
|
+
namespace: "clientProfile",
|
|
301
|
+
getFieldNames,
|
|
302
|
+
dispatch,
|
|
303
|
+
toPayload,
|
|
304
|
+
label: "client-profile"
|
|
305
|
+
}), [
|
|
306
|
+
registerFieldset,
|
|
307
|
+
getFieldNames,
|
|
308
|
+
dispatch,
|
|
309
|
+
toPayload
|
|
310
|
+
]);
|
|
311
|
+
const saved = useMemo(() => ({
|
|
312
|
+
firstName: $data?.firstName ?? "",
|
|
313
|
+
lastName: $data?.lastName ?? "",
|
|
314
|
+
phone: $data?.phone ?? "",
|
|
315
|
+
document: $data?.document ?? ""
|
|
316
|
+
}), [$data]);
|
|
317
|
+
const contextValue = useMemo(() => ({
|
|
318
|
+
namespace: "clientProfile",
|
|
319
|
+
saved,
|
|
320
|
+
readOnly,
|
|
321
|
+
country,
|
|
322
|
+
documentType,
|
|
323
|
+
rules: NO_RULES
|
|
324
|
+
}), [
|
|
325
|
+
saved,
|
|
326
|
+
readOnly,
|
|
327
|
+
country,
|
|
328
|
+
documentType
|
|
329
|
+
]);
|
|
330
|
+
const [announcement, setAnnouncement] = useState("");
|
|
331
|
+
const announced = resolved ? loadedLabel : loadingLabel;
|
|
332
|
+
useEffect(() => {
|
|
333
|
+
setAnnouncement(announced);
|
|
334
|
+
}, [announced]);
|
|
335
|
+
return /* @__PURE__ */ jsxs(Fieldset.Root, {
|
|
336
|
+
className,
|
|
337
|
+
disabled: readOnly,
|
|
338
|
+
"aria-busy": resolved ? void 0 : true,
|
|
339
|
+
...groupLabel?.trim() ? { "aria-label": groupLabel } : {},
|
|
340
|
+
"data-fs-client-profile-fieldset": "",
|
|
341
|
+
...resolved ? {} : { "data-fs-client-profile-fieldset-pending": "" },
|
|
342
|
+
...readOnly ? { "data-fs-client-profile-fieldset-readonly": "" } : {},
|
|
343
|
+
children: [resolved ? /* @__PURE__ */ jsx(FieldsetFieldContext.Provider, {
|
|
344
|
+
value: fieldsetRegistry,
|
|
345
|
+
children: /* @__PURE__ */ jsx(ClientProfileFieldsetContext.Provider, {
|
|
346
|
+
value: contextValue,
|
|
347
|
+
children
|
|
348
|
+
})
|
|
349
|
+
}) : /* @__PURE__ */ jsx(ClientProfileFieldsetSkeleton, { count: skeletonCount }), /* @__PURE__ */ jsx("output", {
|
|
350
|
+
"aria-live": "polite",
|
|
351
|
+
"data-fs-client-profile-fieldset-announcement": "",
|
|
352
|
+
children: announcement
|
|
353
|
+
})]
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* The loading state of the fieldset, rendered in place of the controls until the entity resolves and
|
|
358
|
+
* declared as the definition's `skeleton`. Purely visual: the wait is spoken by the fieldset's own live
|
|
359
|
+
* region, so this is hidden from assistive technology rather than announced twice.
|
|
360
|
+
*/
|
|
361
|
+
function ClientProfileFieldsetSkeleton({ count = CLIENT_PROFILE_SKELETON_ROWS, className, ...props }) {
|
|
362
|
+
return /* @__PURE__ */ jsx("div", {
|
|
363
|
+
...props,
|
|
364
|
+
className,
|
|
365
|
+
"data-fs-client-profile-fieldset-skeleton": "",
|
|
366
|
+
"aria-hidden": "true",
|
|
367
|
+
children: Array.from({ length: count }, (_, i) => /* @__PURE__ */ jsx("span", { "data-fs-client-profile-fieldset-skeleton-field": "" }, i))
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
ClientProfileFieldset.Skeleton = ClientProfileFieldsetSkeleton;
|
|
371
|
+
function ProfileTextControl({ fieldKey, attr, autoComplete, defaultLabel, label, placeholder, className, children }) {
|
|
372
|
+
const { namespace, saved, readOnly, rules } = useClientProfileFieldsetContext();
|
|
373
|
+
const name = useFieldsetField(namespace, fieldKey);
|
|
374
|
+
return /* @__PURE__ */ jsx(FieldBinding, {
|
|
375
|
+
name,
|
|
376
|
+
label: label ?? defaultLabel,
|
|
377
|
+
placeholder,
|
|
378
|
+
defaultValue: saved[fieldKey],
|
|
379
|
+
rule: rules[fieldKey],
|
|
380
|
+
disabled: readOnly,
|
|
381
|
+
autoComplete,
|
|
382
|
+
children: /* @__PURE__ */ jsx(Field.Root, {
|
|
383
|
+
className,
|
|
384
|
+
"data-fs-client-profile-fieldset-field": attr,
|
|
385
|
+
children: children ?? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
386
|
+
/* @__PURE__ */ jsx(Field.Label, {}),
|
|
387
|
+
/* @__PURE__ */ jsx(Field.Control, {}),
|
|
388
|
+
/* @__PURE__ */ jsx(Field.Error, {})
|
|
389
|
+
] })
|
|
390
|
+
})
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
function ClientProfileFirstNameControl({ label, placeholder, className, children }) {
|
|
394
|
+
return /* @__PURE__ */ jsx(ProfileTextControl, {
|
|
395
|
+
fieldKey: "firstName",
|
|
396
|
+
attr: "first-name",
|
|
397
|
+
autoComplete: "given-name",
|
|
398
|
+
defaultLabel: "First name",
|
|
399
|
+
label,
|
|
400
|
+
placeholder,
|
|
401
|
+
className,
|
|
402
|
+
children
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
function ClientProfileLastNameControl({ label, placeholder, className, children }) {
|
|
406
|
+
return /* @__PURE__ */ jsx(ProfileTextControl, {
|
|
407
|
+
fieldKey: "lastName",
|
|
408
|
+
attr: "last-name",
|
|
409
|
+
autoComplete: "family-name",
|
|
410
|
+
defaultLabel: "Last name",
|
|
411
|
+
label,
|
|
412
|
+
placeholder,
|
|
413
|
+
className,
|
|
414
|
+
children
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* Composes `PhoneField` inside a `FieldBinding`, wrapped in a `data-fs-client-profile-fieldset-field="phone"`
|
|
419
|
+
* node — the same styling hook attribute the text controls carry on their own `Field.Root`.
|
|
420
|
+
* `useFieldsetField` runs unconditionally, before `requirement` is even read — there is no suppressed
|
|
421
|
+
* state for this control to second-guess; `requirement` only ever raises the field to required,
|
|
422
|
+
* and left unset it raises nothing.
|
|
423
|
+
*/
|
|
424
|
+
function ClientProfileFieldsetPhoneControl({ label, placeholder, requirement, errorInvalid, errorRequired, className, children }) {
|
|
425
|
+
const { namespace, saved, readOnly, rules, country } = useClientProfileFieldsetContext();
|
|
426
|
+
const name = useFieldsetField(namespace, "phone");
|
|
427
|
+
const binding = /* @__PURE__ */ jsx(FieldBinding, {
|
|
428
|
+
name,
|
|
429
|
+
label,
|
|
430
|
+
placeholder,
|
|
431
|
+
defaultValue: saved.phone,
|
|
432
|
+
rule: rules.phone,
|
|
433
|
+
disabled: readOnly,
|
|
434
|
+
children: /* @__PURE__ */ jsx(PhoneField, {
|
|
435
|
+
country,
|
|
436
|
+
requirement,
|
|
437
|
+
errorInvalid,
|
|
438
|
+
errorRequired,
|
|
439
|
+
className,
|
|
440
|
+
children
|
|
441
|
+
})
|
|
442
|
+
});
|
|
443
|
+
return /* @__PURE__ */ jsx("div", {
|
|
444
|
+
"data-fs-client-profile-fieldset-field": "phone",
|
|
445
|
+
children: binding
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
/**
|
|
449
|
+
* `useFieldsetField` (through `useClientProfileFieldsetContext`'s namespace) lives only in this inner
|
|
450
|
+
* component, mounted only once `ClientProfileDocumentControl` has already decided the document type is
|
|
451
|
+
* supported. Splitting the decision from the registration this way (Decision 8) keeps a control that
|
|
452
|
+
* renders nothing from registering a field name `FormClear` would otherwise dirty and dispatch.
|
|
453
|
+
*/
|
|
454
|
+
function ClientProfileDocumentControlInner({ documentType, label, placeholder, errorMessage, className, children }) {
|
|
455
|
+
const { namespace, saved, readOnly, rules } = useClientProfileFieldsetContext();
|
|
456
|
+
const name = useFieldsetField(namespace, "document");
|
|
457
|
+
return /* @__PURE__ */ jsx(FieldBinding, {
|
|
458
|
+
name,
|
|
459
|
+
label: label ?? DOCUMENTS[documentType].name,
|
|
460
|
+
placeholder,
|
|
461
|
+
defaultValue: saved.document,
|
|
462
|
+
rule: rules.document,
|
|
463
|
+
disabled: readOnly,
|
|
464
|
+
children: /* @__PURE__ */ jsx(DocumentField, {
|
|
465
|
+
documentType,
|
|
466
|
+
errorMessage,
|
|
467
|
+
className,
|
|
468
|
+
"data-fs-client-profile-fieldset-field": "document",
|
|
469
|
+
children
|
|
470
|
+
})
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
function ClientProfileDocumentControl({ label, placeholder, errorMessage, className, children }) {
|
|
474
|
+
const { documentType } = useClientProfileFieldsetContext();
|
|
475
|
+
if (!isSupportedDocumentType(documentType)) return null;
|
|
476
|
+
return /* @__PURE__ */ jsx(ClientProfileDocumentControlInner, {
|
|
477
|
+
documentType,
|
|
478
|
+
label,
|
|
479
|
+
placeholder,
|
|
480
|
+
errorMessage,
|
|
481
|
+
className,
|
|
482
|
+
children
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
//#endregion
|
|
486
|
+
export { ClientProfileDocumentControl, ClientProfileDocumentControlDefinition, ClientProfileFieldset, ClientProfileFieldsetDefinition, ClientProfileFieldsetPhoneControl, ClientProfileFieldsetPhoneControlDefinition, ClientProfileFieldsetSkeleton, ClientProfileFirstNameControl, ClientProfileFirstNameControlDefinition, ClientProfileLastNameControl, ClientProfileLastNameControlDefinition };
|
|
487
|
+
|
|
488
|
+
//# sourceMappingURL=client-profile-fieldset.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client-profile-fieldset.js","names":[],"sources":["../../../src/matter/client-profile-form/client-profile-fieldset.tsx"],"sourcesContent":["'use client';\n\nimport { defineComponent } from '@vtex/fs-core';\nimport {\n type ComponentPropsWithRef,\n createContext,\n type ReactNode,\n useCallback,\n useContext,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from 'react';\nimport { useFormContext } from 'react-hook-form';\nimport type { DocumentType } from '../../particles/document-field/document-field.js';\nimport { DOCUMENTS, DocumentField, isSupportedDocumentType } from '../../particles/document-field/document-field.js';\nimport type { FieldRule } from '../../particles/field/field.js';\nimport {\n Field,\n FieldBinding,\n FieldsetFieldContext,\n useFieldsetField,\n useFieldsetFieldNames,\n useNoAncestorFieldset,\n} from '../../particles/field/field.js';\nimport { Fieldset } from '../../particles/fieldset/fieldset.js';\nimport { useFormRegistry } from '../../particles/form/form.js';\nimport type { PhoneCountry } from '../../particles/phone-field/phone-field.js';\nimport { PhoneField, toE164 } from '../../particles/phone-field/phone-field.js';\nimport { useActionsOverride } from '../../sdk/actions/actions-override-context.js';\nimport type { ComponentActions } from '../../sdk/actions/types.js';\nimport { useDataOverride } from '../../sdk/data/data-override-context.js';\nimport { useEntityDataResolved } from '../../sdk/data/entity-resolution-context.js';\nimport { useReactiveDataSettled } from '../../sdk/data/use-reactive-data-settled.js';\nimport type { ClientProfileUpdateInput } from '../../sdk/identity/operations.js';\nimport { useSession } from '../session/session.js';\n\n/**\n * `ClientProfileFieldset` — the composable, one-Save write of the `ClientProfile` sibling entity\n * (RFC: Composable Entity Forms, `Proposal > Design > The fieldset`). It registers itself with the\n * enclosing `FormRoot` under the `clientProfile` namespace and dispatches\n * `$actions.identity.clientProfileUpdate` for its own submit slot; it renders no `<form>` of its own.\n *\n * The four controls below are the smallest placeable unit: each calls `useFieldsetField` for its full\n * field name, renders a `FieldBinding` carrying the field's static facts, and — inside it — either the\n * default anatomy or the author's own children. No control's field contract carries a blocking\n * `RegisterOptions` key (no `required`, `min`, `max`, `minLength`, `maxLength`, `pattern`); requiredness\n * on the phone control rides on its own `requirement` prop instead, which only ever gates required\n * validation — a field the store does not want is omitted from the page, not switched off by a prop.\n *\n * The fieldset's context name is `client-profile-fieldset`, not `client-profile`: the shipped\n * `ClientProfilePhoneControl` (`matter/client-profile/client-profile.tsx`) already requires\n * `client-profile` and writes on blur, so reusing that name here would let an on-blur control compile\n * inside this batched fieldset and write immediately. `ClientProfileFieldsetPhoneControl` carries the\n * same container prefix as the context it requires, naming the collision rather than hiding it.\n */\n\n// ---------------------------------------------------------------------------\n// GraphQL Fragment — colocated on the fieldset, its own data root\n// ---------------------------------------------------------------------------\n\nconst fragment = /* GraphQL */ `\n fragment ClientProfileFieldsetData on Query {\n clientProfile {\n __typename\n id\n firstName\n lastName\n phone\n document\n }\n }\n`;\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/** The `$data` slice forwarded by the reactive runtime — the ClientProfile sibling entity, unwrapped. */\nexport type ClientProfileFieldsetData = {\n firstName?: string | null;\n lastName?: string | null;\n phone?: string | null;\n document?: string | null;\n} | null;\n\n/** The four keys this fieldset registers under its namespace. */\ntype ProfileFieldKey = 'firstName' | 'lastName' | 'phone' | 'document';\n\n/** What the fieldset publishes to its controls. Carries no live value — react-hook-form owns those. */\ntype ClientProfileFieldsetContextValue = Readonly<{\n namespace: 'clientProfile';\n /** Seeds, resolved from `$data`. Empty string, never null, so a control never seeds `undefined`. */\n saved: Readonly<Record<ProfileFieldKey, string>>;\n /** True when the session reports `canEditData: false`. */\n readOnly: boolean;\n /** Phone mask/validation country, from the fieldset's own prop. */\n country: PhoneCountry;\n /** Locale document type; the document control renders only when supported. */\n documentType: string | undefined;\n /** The fieldset's own validation rules, per key. Present and empty today (Decision 6). */\n rules: Readonly<Partial<Record<ProfileFieldKey, FieldRule>>>;\n}>;\n\n/** No control publishes a rule today — a blocking `RegisterOptions` key would reopen the same submit\n * block for a read-only session (Decision 6): `readOnly` disables a control's rendered `<input>` via a\n * plain HTML attribute, never react-hook-form's own `disabled` registration option (`field.tsx`'s\n * `useController` call takes no `disabled`), so a required field would stay fully registered and\n * validated while the shopper has no way to focus or edit it. Kept as a stable empty object so the\n * context value's identity does not churn on every render. */\nconst NO_RULES: Readonly<Partial<Record<ProfileFieldKey, FieldRule>>> = {};\n\n/**\n * Rows `ClientProfileFieldsetSkeleton` draws when nobody says otherwise: one per control in the full\n * anatomy — first name, last name, phone, document. It is a default and not a derivation, because\n * the composed children are opaque here. A page that composes fewer sets `skeletonCount` to what it\n * composed, or the form jumps by the difference when the controls take the skeleton's place.\n */\nconst CLIENT_PROFILE_SKELETON_ROWS = 4;\n\nconst CLIENT_PROFILE_FIELDSET_UNSET = Symbol('ClientProfileFieldsetContextUnset');\n\nconst ClientProfileFieldsetContext = createContext<\n ClientProfileFieldsetContextValue | typeof CLIENT_PROFILE_FIELDSET_UNSET\n>(CLIENT_PROFILE_FIELDSET_UNSET);\n\nfunction useClientProfileFieldsetContext(): ClientProfileFieldsetContextValue {\n const ctx = useContext(ClientProfileFieldsetContext);\n if (ctx === CLIENT_PROFILE_FIELDSET_UNSET) {\n throw new Error('ClientProfileFieldset controls must be used inside ClientProfileFieldset.');\n }\n return ctx;\n}\n\n// ---------------------------------------------------------------------------\n// Props\n// ---------------------------------------------------------------------------\n\nexport type ClientProfileFieldsetProps = {\n /** Phone mask/validation country, provided to the phone control via context. */\n country?: PhoneCountry;\n /** Locale document type; the document control renders only when supported. */\n documentType?: string;\n /** Placeholder rows the loading skeleton draws. Set it to the number of controls composed here. */\n skeletonCount?: number;\n /** Announced politely while the profile resolves. Blank means no announcement. */\n loadingLabel?: string;\n /** Announced politely once the controls mount. Blank means no announcement. */\n loadedLabel?: string;\n /** Accessible name of the `<fieldset>` group. Blank means no name is exposed. */\n groupLabel?: string;\n className?: string;\n children?: ReactNode;\n /** `ClientProfileFieldsetData` slice forwarded by the reactive runtime. */\n $data?: ClientProfileFieldsetData;\n /** Compiler-injected; carries `identity.clientProfileUpdate`. */\n $actions?: ComponentActions;\n};\n\ntype ProfileTextControlProps = { label?: string; placeholder?: string; className?: string; children?: ReactNode };\n\nexport type ClientProfileFirstNameControlProps = ProfileTextControlProps;\nexport type ClientProfileLastNameControlProps = ProfileTextControlProps;\n\nexport type ClientProfileFieldsetPhoneControlProps = {\n label?: string;\n placeholder?: string;\n /**\n * Raises the field to required. The composite owns it; the contract carries no rule (Decision 6),\n * so here the raise is the whole of the resolution. Unset is no opinion, which leaves the phone\n * optional.\n */\n requirement?: 'mandatory' | 'optional';\n errorInvalid?: string;\n errorRequired?: string;\n className?: string;\n /** Given, replaces PhoneField's default Field.Label + PhoneFieldInput + Field.Error anatomy. */\n children?: ReactNode;\n};\n\nexport type ClientProfileDocumentControlProps = {\n label?: string;\n placeholder?: string;\n errorMessage?: string;\n className?: string;\n /** Given, replaces DocumentField's default Field.Label + DocumentFieldInput + Field.Error anatomy. */\n children?: ReactNode;\n};\n\n// ---------------------------------------------------------------------------\n// Definitions\n// ---------------------------------------------------------------------------\n\nexport const ClientProfileFieldsetDefinition = defineComponent<ClientProfileFieldsetProps>({\n name: 'ClientProfileFieldset',\n importPath: '@vtex/faststore/components',\n component: ClientProfileFieldset,\n fragment,\n fetchStrategy: 'reactive',\n skeleton: ClientProfileFieldsetSkeleton,\n requiresContext: ['form', 'session'],\n providesContext: 'client-profile-fieldset',\n actions: { identity: ['clientProfileUpdate'] },\n editor: { label: 'Client Profile Fieldset', category: 'identity' },\n schema: {\n country: { type: 'string', default: 'US', label: 'Phone country (US or BR)' },\n documentType: { type: 'string', label: 'Document type (e.g. CPF)' },\n skeletonCount: { type: 'number', default: 4, label: 'Skeleton rows' },\n loadingLabel: { type: 'string', default: 'Loading your details', label: 'Loading announcement' },\n loadedLabel: { type: 'string', default: 'Your details are ready', label: 'Loaded announcement' },\n groupLabel: { type: 'string', default: 'Your details', label: 'Accessible group name' },\n },\n guideline:\n 'Registers a batched write of the ClientProfile entity with the enclosing FormRoot; renders one Fieldset.Root and no <form>. Must be inside a FormRoot with a SessionRoot ancestor. Compose ClientProfileFirstNameControl / ClientProfileLastNameControl / ClientProfileFieldsetPhoneControl / ClientProfileDocumentControl as children, in any order. The controls mount only once the profile has resolved, with skeletonCount placeholder rows in their place until then — set it to the number of controls composed, or the form jumps by the difference. Style the announcement sr-only. Dispatches only the fields the shopper changed. groupLabel names the group as its accessible name; blank emits no aria-label, and a composed FieldsetLegend wins over it.',\n});\n\nexport const ClientProfileFirstNameControlDefinition = defineComponent<ClientProfileFirstNameControlProps>({\n name: 'ClientProfileFirstNameControl',\n importPath: '@vtex/faststore/components',\n component: ClientProfileFirstNameControl,\n requiresContext: 'client-profile-fieldset',\n providesContext: 'field',\n editor: { label: 'Client Profile First Name Control', category: 'identity' },\n schema: {\n // No default (Decision 1): absence means the platform's value, applied at render time by\n // the control itself, not baked into the schema.\n label: { type: 'string', label: 'Label' },\n placeholder: { type: 'string', label: 'Placeholder' },\n },\n guideline:\n \"The bound first-name control; must be a descendant of ClientProfileFieldset. Renders Field.Root > Field.Label + Field.Control + Field.Error by default, or the composed children in the author's own order.\",\n});\n\nexport const ClientProfileLastNameControlDefinition = defineComponent<ClientProfileLastNameControlProps>({\n name: 'ClientProfileLastNameControl',\n importPath: '@vtex/faststore/components',\n component: ClientProfileLastNameControl,\n requiresContext: 'client-profile-fieldset',\n providesContext: 'field',\n editor: { label: 'Client Profile Last Name Control', category: 'identity' },\n schema: {\n // No default (Decision 1): absence means the platform's value, applied at render time by\n // the control itself, not baked into the schema.\n label: { type: 'string', label: 'Label' },\n placeholder: { type: 'string', label: 'Placeholder' },\n },\n guideline:\n \"The bound last-name control; must be a descendant of ClientProfileFieldset. Renders Field.Root > Field.Label + Field.Control + Field.Error by default, or the composed children in the author's own order.\",\n});\n\nexport const ClientProfileFieldsetPhoneControlDefinition = defineComponent<ClientProfileFieldsetPhoneControlProps>({\n name: 'ClientProfileFieldsetPhoneControl',\n importPath: '@vtex/faststore/components',\n component: ClientProfileFieldsetPhoneControl,\n requiresContext: 'client-profile-fieldset',\n // Provides both names (mirrors shipping-address.tsx:595's Decision 7): 'field' is what every\n // fieldset Control declares (RFC:503), but this control also composes PhoneField, whose\n // PhoneFieldInput part requires 'phone-field' — a page composing\n // ClientProfileFieldsetPhoneControl > PhoneFieldInput would otherwise fail the build with a\n // context-constraint violation, since PhoneField itself (the only 'phone-field' provider) never\n // appears in the page schema (finding F1).\n providesContext: ['field', 'phone-field'],\n editor: { label: 'Client Profile Fieldset Phone Control', category: 'identity' },\n schema: {\n // No default (Decision 1): absence means the platform's value, applied at render time by\n // the control itself, not baked into the schema.\n label: { type: 'string', label: 'Label' },\n placeholder: { type: 'string', label: 'Placeholder' },\n // No default: an unset requirement is no opinion, not a raise to mandatory.\n requirement: { type: ['mandatory', 'optional'] as const, label: 'Requirement' },\n // No default (Decision 1): absence means the platform's value, applied at render time by\n // the control itself, not baked into the schema.\n errorInvalid: { type: 'string', label: 'Invalid error message' },\n errorRequired: { type: 'string', label: 'Required error message' },\n },\n guideline:\n \"The bound phone control; must be a descendant of ClientProfileFieldset. Distinct from ClientProfilePhoneControl (matter/client-profile), which writes on blur and requires the client-profile context — the two cannot compile into each other's container. Composes the PhoneField particle: recomposable (RFC:503) — given children, they replace PhoneField's default Field.Label + PhoneFieldInput + Field.Error anatomy, in any order; childless keeps today's order. Its className styles PhoneField's own root, the node that lays out label, input and error — not the wrapper, which carries only the data-fs-client-profile-fieldset-field=\\\"phone\\\" styling hook. A store that does not want a phone field omits this element from the page: requirement only governs required validation, never visibility.\",\n});\n\nexport const ClientProfileDocumentControlDefinition = defineComponent<ClientProfileDocumentControlProps>({\n name: 'ClientProfileDocumentControl',\n importPath: '@vtex/faststore/components',\n component: ClientProfileDocumentControl,\n requiresContext: 'client-profile-fieldset',\n // Provides both names, same reasoning as ClientProfileFieldsetPhoneControlDefinition above: this\n // control composes DocumentField, whose DocumentFieldInput part requires 'document-field' — a page\n // composing ClientProfileDocumentControl > DocumentFieldInput would otherwise fail the build\n // (finding F1).\n providesContext: ['field', 'document-field'],\n editor: { label: 'Client Profile Document Control', category: 'identity' },\n schema: {\n // No default (Decision 1): absence means the platform's value, applied at render time by\n // the control itself, not baked into the schema.\n label: { type: 'string', label: 'Label' },\n placeholder: { type: 'string', label: 'Placeholder' },\n errorMessage: { type: 'string', label: 'Error message' },\n },\n guideline:\n \"The bound tax-document control; must be a descendant of ClientProfileFieldset. Renders nothing, and registers no field name, when the fieldset's documentType is unsupported. Composes the DocumentField particle: recomposable (RFC:503) — given children, they replace DocumentField's default Field.Label + DocumentFieldInput + Field.Error anatomy, in any order; childless keeps today's order.\",\n});\n\n// ---------------------------------------------------------------------------\n// Fieldset root\n// ---------------------------------------------------------------------------\n\n/**\n * Registers a batched write of the `ClientProfile` entity with the enclosing `FormRoot`\n * (`registerFieldset`, namespace `clientProfile`) on mount, unconditionally — so this fieldset keeps its\n * original dispatch slot even while its own query is still resolving. Its children — the seed values and\n * the field-name registry its controls need — render only once the entity has resolved; until then the\n * `Fieldset.Root` carries `-pending` and renders `ClientProfileFieldsetSkeleton` (Decision 5).\n * `dispatch` and `toPayload` are `useCallback([])`-stable and read `$actions`/`country`/`documentType`\n * through a ref, so the registration effect runs exactly once per mount regardless of how often the\n * parent re-renders.\n *\n * \"Resolved\" is two conditions, because `$data` alone cannot answer it. Under the reactive runtime the\n * slice is defined from the first render — `useReactiveEntity` hands down `fallbackData` while the cache\n * read is incomplete — so an empty server answer looks exactly like a resolved one, and the controls\n * would mount and seed `''` before the shopper's real profile arrives. `useSeedAdoption` re-seeds a\n * clean field with `defaultValue` on every render, but once the field turns dirty no later seed\n * ever reaches it again: a shopper who starts typing before the real profile arrives locks that\n * empty seed in for good, and even a shopper who does not type sees the field flash from `''` to\n * the real data. `useReactiveDataSettled` supplies the missing half: the page query's own\n * in-flight state, latched so it only ever goes pending → settled. The `$data === undefined` half still\n * carries trees with no reactive root above them, where the slice arrives as a plain prop.\n *\n * Both ends of that wait are spoken: the fieldset carries `aria-busy` while it lasts and holds a polite\n * live region that announces `loadingLabel` and then `loadedLabel`. Style the region `sr-only` — it is\n * text meant for a screen reader, not for the page.\n */\nexport function ClientProfileFieldset({\n country = 'US',\n documentType,\n skeletonCount = CLIENT_PROFILE_SKELETON_ROWS,\n loadingLabel = 'Loading your details',\n loadedLabel = 'Your details are ready',\n groupLabel = 'Your details',\n className,\n children,\n $data: $dataProp,\n $actions: $actionsProp,\n}: ClientProfileFieldsetProps) {\n // Nesting fieldsets has no legitimate use: useFieldsetField resolves the *nearest*\n // FieldsetFieldContext, not the one owning a control's actual namespace, so a control nested two\n // fieldsets deep would silently register under the wrong one and its write would be discarded on\n // submit. Runtime registry integrity, mirroring the namespace-collision throw FormRoot already ships\n // (form.tsx:280-288).\n useNoAncestorFieldset('ClientProfileFieldset');\n\n const $data = useDataOverride($dataProp);\n\n const $actions = useActionsOverride($actionsProp);\n // Three conditions, not two. `useReactiveDataSettled` latches one way and never re-arms, so on its\n // own it answers \"has this page ever resolved\", not \"is what I am holding right now the server's\n // answer\" — after the page query's entity ref moves to one this cache never stored, or its root is\n // skipped for want of a variable, the slice silently regresses to `fallbackData` while the latch\n // still reads settled. `useEntityDataResolved` is the per-entity half: unlatched, so it follows the\n // slice back down and returns this fieldset to its skeleton instead of leaving live controls seeded\n // from a placeholder.\n const settled = useReactiveDataSettled();\n const sliceResolved = useEntityDataResolved();\n const resolved = settled && sliceResolved && $data !== undefined;\n // Also enforces the SessionRoot ancestor: useSession() throws when rendered outside one.\n const { canEditData } = useSession();\n const readOnly = !canEditData;\n\n // Runs before useFormContext() below: useFormRegistry() throws the friendly \"Form parts must be used\n // inside FormRoot.\" error when no FormRoot is above, so a FormRoot-less render fails there rather than\n // reaching the getFieldState destructure, which throws an unhelpful TypeError instead (useFormContext()\n // resolves to null outside a FormRoot, and destructuring null throws).\n const { registerFieldset } = useFormRegistry();\n\n // Decision 3, as later narrowed: this fieldset still reads dirtiness imperatively, but the rule is no\n // longer the form's. `FormRoot` subscribes `dirtyFields` in render and decides which fieldsets to\n // dispatch from it, by the fields each one has mounted, on react-hook-form's documented contract.\n //\n // The imperative read survives here because it asks a different question in a different place: which\n // of *this* fieldset's own fields changed, resolved inside `toPayload` at submit time, by full\n // registered field name — the shape `getFieldState` documents. `useFormContext()` adds no\n // `formState` subscription of its own; one is created only when a caller reads a formState key off\n // the `useForm`/`useFormState` proxy (or hands that already-subscribed object to `getFieldState`'s\n // second argument). Calling `getFieldState(name)` with no second argument reads the current internal\n // state directly, so this fieldset root still does not re-render on a keystroke.\n const { getFieldState } = useFormContext();\n\n const latest = useRef({ $actions, country, documentType, readOnly, getFieldState });\n useEffect(() => {\n latest.current = { $actions, country, documentType, readOnly, getFieldState };\n }, [$actions, country, documentType, readOnly, getFieldState]);\n\n const { registry: fieldsetRegistry, getFieldNames } = useFieldsetFieldNames('clientProfile');\n\n const dispatch = useCallback(async (payload: Record<string, unknown>) => {\n // Defence-in-depth: the UI disables every input while read-only, but `FormClear` sets values (and\n // marks them dirty) on every registered field regardless of that disabled attribute — react-hook-\n // form's own `useController` calls never receive a `disabled` registration option, so it has no\n // notion of \"disabled\" of its own. Guarding here keeps the write from reaching the network even if\n // some other composition dirties this fieldset while canEditData is false (the server also refuses,\n // but a client-side guard should not depend on that).\n if (latest.current.readOnly) {\n throw new Error('ClientProfileFieldset cannot dispatch while the session is read-only.');\n }\n const update = latest.current.$actions?.identity?.clientProfileUpdate;\n // Throwing is the contract's failure path: FormRoot catches it and records this fieldset's failure\n // (form.tsx:264-266). Returning [] would report an unqualified success for a write that never\n // happened.\n if (!update) throw new Error('ClientProfileFieldset requires $actions.identity.clientProfileUpdate.');\n return update(payload as ClientProfileUpdateInput);\n }, []);\n\n const toPayload = useCallback((values: Record<string, unknown>): Record<string, unknown> => {\n const { country: activeCountry, getFieldState } = latest.current;\n // FR-5: a key is emitted only when the shopper actually changed it — presence in `values` alone\n // (every registered field is always present) is not enough. `dirtyFields` is not read directly;\n // `getFieldState` is called with no second argument, which reads react-hook-form's current\n // internal state without subscribing to it (Decision 3).\n const isDirty = (key: ProfileFieldKey) => getFieldState(`clientProfile.${key}`).isDirty;\n const payload: Record<string, string> = {};\n if ('firstName' in values && isDirty('firstName')) payload.firstName = String(values.firstName ?? '').trim();\n if ('lastName' in values && isDirty('lastName')) payload.lastName = String(values.lastName ?? '').trim();\n if ('phone' in values && isDirty('phone')) {\n const raw = String(values.phone ?? '').trim();\n // toE164 on a blank value returns the bare country code, which would write \"+55\" over the\n // shopper's number and fail every later save.\n payload.phone = raw ? toE164(raw, activeCountry) : '';\n }\n if ('document' in values && isDirty('document')) {\n payload.document = String(values.document ?? '').replace(/\\D/g, '');\n }\n return payload;\n }, []);\n\n useEffect(\n () =>\n registerFieldset({\n namespace: 'clientProfile',\n getFieldNames,\n dispatch,\n toPayload,\n // Decision 1: the register is a machine identifier, the kebab-case ASCII rendering of this\n // registration's own `namespace` (`clientProfile` → `client-profile`) — not authored per\n // page (Decision 2).\n label: 'client-profile',\n }),\n [registerFieldset, getFieldNames, dispatch, toPayload],\n );\n\n const saved = useMemo<Readonly<Record<ProfileFieldKey, string>>>(\n () => ({\n firstName: $data?.firstName ?? '',\n lastName: $data?.lastName ?? '',\n phone: $data?.phone ?? '',\n document: $data?.document ?? '',\n }),\n [$data],\n );\n\n const contextValue = useMemo<ClientProfileFieldsetContextValue>(\n () => ({ namespace: 'clientProfile', saved, readOnly, country, documentType, rules: NO_RULES }),\n [saved, readOnly, country, documentType],\n );\n\n // Assistive technology only reports a change inside a region that was already in the tree, so a\n // region mounted together with its text announces nothing. The region therefore sits in both\n // branches and starts empty, with the wait written on the pass after mount and replaced — not\n // cleared — once the controls are up: the skeleton is `aria-hidden`, so without this second\n // message the form would materialise in silence (WCAG 2.1 SC 4.1.3).\n const [announcement, setAnnouncement] = useState('');\n const announced = resolved ? loadedLabel : loadingLabel;\n useEffect(() => {\n setAnnouncement(announced);\n }, [announced]);\n\n return (\n <Fieldset.Root\n className={className}\n disabled={readOnly}\n aria-busy={resolved ? undefined : true}\n // FR-12: blank (or whitespace-only) means no name is exposed, mirroring the loadingLabel/\n // loadedLabel convention. On the root itself (FR-13), so it is identical whether the fieldset\n // is pending or resolved; a page-composed FieldsetLegend still wins via aria-labelledby.\n {...(groupLabel?.trim() ? { 'aria-label': groupLabel } : {})}\n data-fs-client-profile-fieldset=\"\"\n {...(resolved ? {} : { 'data-fs-client-profile-fieldset-pending': '' })}\n {...(readOnly ? { 'data-fs-client-profile-fieldset-readonly': '' } : {})}\n >\n {resolved ? (\n <FieldsetFieldContext.Provider value={fieldsetRegistry}>\n <ClientProfileFieldsetContext.Provider value={contextValue}>{children}</ClientProfileFieldsetContext.Provider>\n </FieldsetFieldContext.Provider>\n ) : (\n <ClientProfileFieldsetSkeleton count={skeletonCount} />\n )}\n <output aria-live=\"polite\" data-fs-client-profile-fieldset-announcement=\"\">\n {announcement}\n </output>\n </Fieldset.Root>\n );\n}\n\n/**\n * The loading state of the fieldset, rendered in place of the controls until the entity resolves and\n * declared as the definition's `skeleton`. Purely visual: the wait is spoken by the fieldset's own live\n * region, so this is hidden from assistive technology rather than announced twice.\n */\nexport function ClientProfileFieldsetSkeleton({\n count = CLIENT_PROFILE_SKELETON_ROWS,\n className,\n ...props\n}: ComponentPropsWithRef<'div'> & { count?: number }) {\n return (\n <div {...props} className={className} data-fs-client-profile-fieldset-skeleton=\"\" aria-hidden=\"true\">\n {Array.from({ length: count }, (_, i) => (\n <span key={i} data-fs-client-profile-fieldset-skeleton-field=\"\" />\n ))}\n </div>\n );\n}\n\nClientProfileFieldset.Skeleton = ClientProfileFieldsetSkeleton;\n\n// ---------------------------------------------------------------------------\n// Text controls — first name, last name\n// ---------------------------------------------------------------------------\n\nfunction ProfileTextControl({\n fieldKey,\n attr,\n autoComplete,\n defaultLabel,\n label,\n placeholder,\n className,\n children,\n}: {\n fieldKey: 'firstName' | 'lastName';\n attr: string;\n autoComplete: string;\n defaultLabel: string;\n label?: string;\n placeholder?: string;\n className?: string;\n children?: ReactNode;\n}) {\n const { namespace, saved, readOnly, rules } = useClientProfileFieldsetContext();\n const name = useFieldsetField(namespace, fieldKey);\n\n return (\n <FieldBinding\n name={name}\n label={label ?? defaultLabel}\n placeholder={placeholder}\n defaultValue={saved[fieldKey]}\n rule={rules[fieldKey]}\n disabled={readOnly}\n autoComplete={autoComplete}\n >\n <Field.Root className={className} data-fs-client-profile-fieldset-field={attr}>\n {children ?? (\n <>\n <Field.Label />\n <Field.Control />\n <Field.Error />\n </>\n )}\n </Field.Root>\n </FieldBinding>\n );\n}\n\nexport function ClientProfileFirstNameControl({\n label,\n placeholder,\n className,\n children,\n}: ClientProfileFirstNameControlProps) {\n return (\n <ProfileTextControl\n fieldKey=\"firstName\"\n attr=\"first-name\"\n autoComplete=\"given-name\"\n defaultLabel=\"First name\"\n label={label}\n placeholder={placeholder}\n className={className}\n >\n {children}\n </ProfileTextControl>\n );\n}\n\nexport function ClientProfileLastNameControl({\n label,\n placeholder,\n className,\n children,\n}: ClientProfileLastNameControlProps) {\n return (\n <ProfileTextControl\n fieldKey=\"lastName\"\n attr=\"last-name\"\n autoComplete=\"family-name\"\n defaultLabel=\"Last name\"\n label={label}\n placeholder={placeholder}\n className={className}\n >\n {children}\n </ProfileTextControl>\n );\n}\n\n// ---------------------------------------------------------------------------\n// Phone control — the fieldset-bound counterpart of ClientProfilePhoneControl\n// ---------------------------------------------------------------------------\n\n/**\n * Composes `PhoneField` inside a `FieldBinding`, wrapped in a `data-fs-client-profile-fieldset-field=\"phone\"`\n * node — the same styling hook attribute the text controls carry on their own `Field.Root`.\n * `useFieldsetField` runs unconditionally, before `requirement` is even read — there is no suppressed\n * state for this control to second-guess; `requirement` only ever raises the field to required,\n * and left unset it raises nothing.\n */\nexport function ClientProfileFieldsetPhoneControl({\n label,\n placeholder,\n requirement,\n errorInvalid,\n errorRequired,\n className,\n children,\n}: ClientProfileFieldsetPhoneControlProps) {\n const { namespace, saved, readOnly, rules, country } = useClientProfileFieldsetContext();\n const name = useFieldsetField(namespace, 'phone');\n\n const binding = (\n <FieldBinding\n name={name}\n label={label}\n placeholder={placeholder}\n defaultValue={saved.phone}\n rule={rules.phone}\n disabled={readOnly}\n >\n <PhoneField\n country={country}\n requirement={requirement}\n errorInvalid={errorInvalid}\n errorRequired={errorRequired}\n className={className}\n >\n {children}\n </PhoneField>\n </FieldBinding>\n );\n\n // The wrapper carries no className of its own (Decision 1): className lands on PhoneField's own\n // Field.Root, the node that actually stacks label, input and error, so it needs no flex slot here.\n return <div data-fs-client-profile-fieldset-field=\"phone\">{binding}</div>;\n}\n\n// ---------------------------------------------------------------------------\n// Document control — self-gates on documentType, through an inner component\n// ---------------------------------------------------------------------------\n\ntype ClientProfileDocumentControlInnerProps = ClientProfileDocumentControlProps & { documentType: DocumentType };\n\n/**\n * `useFieldsetField` (through `useClientProfileFieldsetContext`'s namespace) lives only in this inner\n * component, mounted only once `ClientProfileDocumentControl` has already decided the document type is\n * supported. Splitting the decision from the registration this way (Decision 8) keeps a control that\n * renders nothing from registering a field name `FormClear` would otherwise dirty and dispatch.\n */\nfunction ClientProfileDocumentControlInner({\n documentType,\n label,\n placeholder,\n errorMessage,\n className,\n children,\n}: ClientProfileDocumentControlInnerProps) {\n const { namespace, saved, readOnly, rules } = useClientProfileFieldsetContext();\n const name = useFieldsetField(namespace, 'document');\n\n return (\n <FieldBinding\n name={name}\n label={label ?? DOCUMENTS[documentType].name}\n placeholder={placeholder}\n defaultValue={saved.document}\n rule={rules.document}\n disabled={readOnly}\n >\n <DocumentField\n documentType={documentType}\n errorMessage={errorMessage}\n className={className}\n data-fs-client-profile-fieldset-field=\"document\"\n >\n {children}\n </DocumentField>\n </FieldBinding>\n );\n}\n\nexport function ClientProfileDocumentControl({\n label,\n placeholder,\n errorMessage,\n className,\n children,\n}: ClientProfileDocumentControlProps) {\n const { documentType } = useClientProfileFieldsetContext();\n if (!isSupportedDocumentType(documentType)) return null;\n\n return (\n <ClientProfileDocumentControlInner\n documentType={documentType}\n label={label}\n placeholder={placeholder}\n errorMessage={errorMessage}\n className={className}\n >\n {children}\n </ClientProfileDocumentControlInner>\n );\n}\n"],"mappings":"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"}
|
|
@@ -20,7 +20,7 @@ const IdentityEditControllerDefinition = defineComponent({
|
|
|
20
20
|
category: "commerce"
|
|
21
21
|
},
|
|
22
22
|
schema: {},
|
|
23
|
-
guideline: "Owns the identity section's summary/form toggle: starts on the summary, opens on IdentityEditButton and returns on IdentityEditCancelButton. Saving does not move it, so the form
|
|
23
|
+
guideline: "Owns the identity section's summary/form toggle: starts on the summary, opens on IdentityEditButton and returns on IdentityEditCancelButton. Saving does not move it, so the form stays open and no descendant needs to know about a section around it. Self-contained: collapsing tells nothing else, so no other section opens as a result. Compose an IdentitySummaryView holding the summary parts and an IdentityEditForm holding a FormRoot over ClientProfileFieldset, plus an IdentityEditButton in the heading and an IdentityEditCancelButton next to the form. No state is named anywhere: which state a part belongs to is which component it is. Renders no DOM of its own."
|
|
24
24
|
});
|
|
25
25
|
const IdentitySummaryViewDefinition = defineComponent({
|
|
26
26
|
name: "IdentitySummaryView",
|
|
@@ -44,7 +44,7 @@ const IdentityEditFormDefinition = defineComponent({
|
|
|
44
44
|
category: "commerce"
|
|
45
45
|
},
|
|
46
46
|
schema: {},
|
|
47
|
-
guideline: "Renders its children only while the identity section shows the form, and nothing while it shows the summary. Compose
|
|
47
|
+
guideline: "Renders its children only while the identity section shows the form, and nothing while it shows the summary. Compose a FormRoot holding ClientProfileFieldset and its controls inside, plus an IdentityEditCancelButton. Must be inside an IdentityEditController."
|
|
48
48
|
});
|
|
49
49
|
const IdentityEditButtonDefinition = defineComponent({
|
|
50
50
|
name: "IdentityEditButton",
|