@vtex/faststore 0.0.7 → 0.0.8
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 +43 -0
- package/dist/actions.json +152 -1
- package/dist/catalog.json +1264 -142
- package/dist/cli/index.d.ts +512 -63
- package/dist/cli/index.js +2098 -686
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/server-runtime.js +44 -24
- package/dist/cli/server-runtime.js.map +1 -1
- package/dist/client.d.ts +5 -1
- package/dist/client.js +5 -1
- package/dist/components.d.ts +34 -7
- package/dist/components.js +38 -11
- package/dist/content-types.json +523 -0
- package/dist/core/dist/define-component.d.ts +6 -1
- package/dist/docs/account-access-gate.md +4 -4
- package/dist/docs/account-address-book.md +519 -0
- package/dist/docs/account-entry-point.md +1 -1
- package/dist/docs/account-menu.md +5 -5
- package/dist/docs/alert-dialog.md +2 -2
- package/dist/docs/billing-address.md +17 -9
- package/dist/docs/card-entry.md +37 -6
- package/dist/docs/cardholder-document.md +16 -14
- package/dist/docs/cart-item.md +1 -1
- package/dist/docs/cart.md +8 -8
- package/dist/docs/client-profile-fieldset.md +7 -5
- package/dist/docs/copy-text.md +84 -0
- package/dist/docs/document-field.md +8 -8
- package/dist/docs/footer-link-group.md +2 -2
- package/dist/docs/header-root.md +1 -1
- package/dist/docs/incentives.md +2 -2
- package/dist/docs/logo.md +5 -5
- package/dist/docs/my-account-profile.md +4 -4
- package/dist/docs/navigation-link-list.md +2 -2
- package/dist/docs/navigation-link.md +3 -3
- package/dist/docs/order-cancel.md +52 -0
- package/dist/docs/order-delivery-address.md +63 -0
- package/dist/docs/order-delivery-group.md +83 -0
- package/dist/docs/order-details-back.md +41 -0
- package/dist/docs/order-details-header.md +48 -0
- package/dist/docs/order-list-row.md +46 -0
- package/dist/docs/order-list-search.md +101 -0
- package/dist/docs/order-list-views.md +97 -0
- package/dist/docs/order-list.md +94 -0
- package/dist/docs/order-payment.md +94 -0
- package/dist/docs/order-placed-date.md +49 -0
- package/dist/docs/order-placed-payment-summary.md +13 -15
- package/dist/docs/order-placed.md +225 -0
- package/dist/docs/order-progress.md +88 -0
- package/dist/docs/order-status-badge.md +91 -0
- package/dist/docs/order-summary.md +53 -0
- package/dist/docs/payment-method-badges.md +14 -3
- package/dist/docs/place-order.md +13 -9
- package/dist/docs/saved-cards.md +2 -2
- package/dist/docs/select.md +19 -6
- package/dist/docs/session.md +1 -1
- package/dist/docs/shipping-address.md +1 -1
- package/dist/docs/sign-in-redirect.md +6 -6
- package/dist/docs/toast.md +7 -3
- package/dist/index.d.ts +6 -2
- package/dist/index.js +2 -1
- package/dist/internal/client.d.ts +6 -1
- package/dist/internal/client.js +7 -2
- package/dist/internal/server.d.ts +6 -2
- package/dist/internal/server.js +6 -2
- package/dist/matter/account-access-gate/account-access-gate.d.ts +2 -1
- package/dist/matter/account-access-gate/account-access-gate.js +19 -16
- package/dist/matter/account-access-gate/account-access-gate.js.map +1 -1
- package/dist/matter/account-address-book/account-address-book-delete.d.ts +60 -0
- package/dist/matter/account-address-book/account-address-book-delete.js +184 -0
- package/dist/matter/account-address-book/account-address-book-delete.js.map +1 -0
- package/dist/matter/account-address-book/account-address-book-deliverability.d.ts +19 -0
- package/dist/matter/account-address-book/account-address-book-deliverability.js +50 -0
- package/dist/matter/account-address-book/account-address-book-deliverability.js.map +1 -0
- package/dist/matter/account-address-book/account-address-book-form-parts.d.ts +42 -0
- package/dist/matter/account-address-book/account-address-book-form-parts.js +146 -0
- package/dist/matter/account-address-book/account-address-book-form-parts.js.map +1 -0
- package/dist/matter/account-address-book/account-address-book-profile-notice.d.ts +25 -0
- package/dist/matter/account-address-book/account-address-book-profile-notice.js +72 -0
- package/dist/matter/account-address-book/account-address-book-profile-notice.js.map +1 -0
- package/dist/matter/account-address-book/account-address-book.d.ts +120 -0
- package/dist/matter/account-address-book/account-address-book.js +469 -0
- package/dist/matter/account-address-book/account-address-book.js.map +1 -0
- package/dist/matter/account-address-book/account-address-country-switch.js +59 -0
- package/dist/matter/account-address-book/account-address-country-switch.js.map +1 -0
- package/dist/matter/account-address-book/account-address-country.d.ts +36 -0
- package/dist/matter/account-address-book/account-address-country.js +151 -0
- package/dist/matter/account-address-book/account-address-country.js.map +1 -0
- package/dist/matter/account-address-book/account-address-first-line.js +11 -0
- package/dist/matter/account-address-book/account-address-first-line.js.map +1 -0
- package/dist/matter/account-address-book/account-address-form-context.d.ts +55 -0
- package/dist/matter/account-address-book/account-address-form-context.js +14 -0
- package/dist/matter/account-address-book/account-address-form-context.js.map +1 -0
- package/dist/matter/account-address-book/account-address-form.d.ts +85 -0
- package/dist/matter/account-address-book/account-address-form.js +946 -0
- package/dist/matter/account-address-book/account-address-form.js.map +1 -0
- package/dist/matter/account-address-book/account-address-postal-autofill.js +189 -0
- package/dist/matter/account-address-book/account-address-postal-autofill.js.map +1 -0
- package/dist/matter/account-address-book/account-address-record.d.ts +26 -0
- package/dist/matter/account-address-book/account-address-record.js +138 -0
- package/dist/matter/account-address-book/account-address-record.js.map +1 -0
- package/dist/matter/account-entry-point/account-entry-point.js +11 -11
- package/dist/matter/account-entry-point/account-entry-point.js.map +1 -1
- package/dist/matter/account-menu/account-menu.d.ts +7 -3
- package/dist/matter/account-menu/account-menu.js +69 -28
- package/dist/matter/account-menu/account-menu.js.map +1 -1
- package/dist/matter/billing-address/billing-address.d.ts +30 -17
- package/dist/matter/billing-address/billing-address.js +128 -33
- package/dist/matter/billing-address/billing-address.js.map +1 -1
- package/dist/matter/breadcrumb/breadcrumb.js +2 -2
- package/dist/matter/breadcrumb/breadcrumb.js.map +1 -1
- package/dist/matter/card-entry/card-capture.js +4 -3
- package/dist/matter/card-entry/card-capture.js.map +1 -1
- package/dist/matter/card-entry/card-entry.d.ts +5 -3
- package/dist/matter/card-entry/card-entry.js +77 -27
- package/dist/matter/card-entry/card-entry.js.map +1 -1
- package/dist/matter/cardholder-document/cardholder-document.d.ts +10 -9
- package/dist/matter/cardholder-document/cardholder-document.js +36 -16
- package/dist/matter/cardholder-document/cardholder-document.js.map +1 -1
- package/dist/matter/cart/cart.d.ts +7 -2
- package/dist/matter/cart/cart.js +29 -26
- package/dist/matter/cart/cart.js.map +1 -1
- package/dist/matter/cart-item/cart-item.js +2 -2
- package/dist/matter/cart-item/cart-item.js.map +1 -1
- package/dist/matter/client-preferences/client-preferences-fieldset.js +1 -1
- package/dist/matter/client-profile-form/client-profile-fieldset.js +8 -5
- package/dist/matter/client-profile-form/client-profile-fieldset.js.map +1 -1
- package/dist/matter/identity-edit/identity-edit.js +3 -3
- 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/my-account-profile/my-account-profile.d.ts +3 -3
- 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-cancel/order-cancel.d.ts +110 -0
- package/dist/matter/order-cancel/order-cancel.js +344 -0
- package/dist/matter/order-cancel/order-cancel.js.map +1 -0
- package/dist/matter/order-delivery-address/order-delivery-address.d.ts +73 -0
- package/dist/matter/order-delivery-address/order-delivery-address.js +172 -0
- package/dist/matter/order-delivery-address/order-delivery-address.js.map +1 -0
- package/dist/matter/order-delivery-group/order-delivery-group.d.ts +128 -0
- package/dist/matter/order-delivery-group/order-delivery-group.js +408 -0
- package/dist/matter/order-delivery-group/order-delivery-group.js.map +1 -0
- package/dist/matter/order-details-header/order-details-header.d.ts +104 -0
- package/dist/matter/order-details-header/order-details-header.js +190 -0
- package/dist/matter/order-details-header/order-details-header.js.map +1 -0
- package/dist/matter/order-list/customer-orders-filter.d.ts +12 -0
- package/dist/matter/order-list/customer-orders-filter.js +30 -0
- package/dist/matter/order-list/customer-orders-filter.js.map +1 -0
- package/dist/matter/order-list/order-list.d.ts +121 -0
- package/dist/matter/order-list/order-list.js +395 -0
- package/dist/matter/order-list/order-list.js.map +1 -0
- package/dist/matter/order-list-row/order-list-row.d.ts +44 -0
- package/dist/matter/order-list-row/order-list-row.js +127 -0
- package/dist/matter/order-list-row/order-list-row.js.map +1 -0
- package/dist/matter/order-list-search/order-list-search.d.ts +72 -0
- package/dist/matter/order-list-search/order-list-search.js +296 -0
- package/dist/matter/order-list-search/order-list-search.js.map +1 -0
- package/dist/matter/order-list-views/order-list-views.d.ts +65 -0
- package/dist/matter/order-list-views/order-list-views.js +154 -0
- package/dist/matter/order-list-views/order-list-views.js.map +1 -0
- package/dist/matter/order-payment/order-payment.d.ts +83 -0
- package/dist/matter/order-payment/order-payment.js +258 -0
- package/dist/matter/order-payment/order-payment.js.map +1 -0
- package/dist/matter/order-placed/order-placed-deliveries.d.ts +145 -0
- package/dist/matter/order-placed/order-placed-deliveries.js +380 -0
- package/dist/matter/order-placed/order-placed-deliveries.js.map +1 -0
- package/dist/matter/order-placed/order-placed.d.ts +122 -0
- package/dist/matter/order-placed/order-placed.js +452 -0
- package/dist/matter/order-placed/order-placed.js.map +1 -0
- package/dist/matter/order-placed-date/order-placed-date.d.ts +45 -0
- package/dist/matter/order-placed-date/order-placed-date.js +92 -0
- package/dist/matter/order-placed-date/order-placed-date.js.map +1 -0
- package/dist/matter/order-placed-payment-summary/order-placed-payment-summary.d.ts +5 -8
- package/dist/matter/order-placed-payment-summary/order-placed-payment-summary.js +29 -81
- package/dist/matter/order-placed-payment-summary/order-placed-payment-summary.js.map +1 -1
- package/dist/matter/order-progress/order-progress.d.ts +85 -0
- package/dist/matter/order-progress/order-progress.js +203 -0
- package/dist/matter/order-progress/order-progress.js.map +1 -0
- package/dist/matter/order-summary/order-summary.d.ts +81 -0
- package/dist/matter/order-summary/order-summary.js +192 -0
- package/dist/matter/order-summary/order-summary.js.map +1 -0
- package/dist/matter/payment/cardholder-document-rule.d.ts +35 -0
- package/dist/matter/payment/cardholder-document-rule.js +35 -0
- package/dist/matter/payment/cardholder-document-rule.js.map +1 -0
- package/dist/matter/payment/payment-card-capture.js +38 -23
- package/dist/matter/payment/payment-card-capture.js.map +1 -1
- package/dist/matter/payment/payment-controller.js +11 -0
- package/dist/matter/payment/payment-controller.js.map +1 -1
- package/dist/matter/payment/payment-method-badges.js +4 -2
- package/dist/matter/payment/payment-method-badges.js.map +1 -1
- package/dist/matter/payment/payment.js +11 -0
- package/dist/matter/payment/payment.js.map +1 -1
- package/dist/matter/payment/use-payment-method-selection.d.ts +39 -0
- package/dist/matter/payment/use-payment-method-selection.js +30 -2
- package/dist/matter/payment/use-payment-method-selection.js.map +1 -1
- package/dist/matter/payment/use-payment-submit-gate.d.ts +16 -2
- package/dist/matter/payment/use-payment-submit-gate.js +42 -14
- package/dist/matter/payment/use-payment-submit-gate.js.map +1 -1
- package/dist/matter/place-order/place-order.d.ts +12 -7
- package/dist/matter/place-order/place-order.js +126 -103
- package/dist/matter/place-order/place-order.js.map +1 -1
- package/dist/matter/pricing/pricing.js +5 -5
- package/dist/matter/pricing/pricing.js.map +1 -1
- package/dist/matter/region/region.js +1 -1
- package/dist/matter/saved-cards/saved-cards.js +26 -15
- package/dist/matter/saved-cards/saved-cards.js.map +1 -1
- package/dist/matter/session/session.js +2 -2
- package/dist/matter/session/session.js.map +1 -1
- package/dist/matter/shipping-address/country-descriptors.d.ts +1 -0
- package/dist/matter/shipping-address/country-descriptors.js +23 -1
- package/dist/matter/shipping-address/country-descriptors.js.map +1 -1
- package/dist/matter/shipping-address/shipping-address.js +62 -14
- package/dist/matter/shipping-address/shipping-address.js.map +1 -1
- package/dist/matter/shipping-simulator/shipping-simulator.d.ts +1 -1
- package/dist/matter/shipping-simulator/shipping-simulator.js.map +1 -1
- package/dist/matter/sign-in-redirect/sign-in-redirect.js +48 -10
- package/dist/matter/sign-in-redirect/sign-in-redirect.js.map +1 -1
- package/dist/matter/variant-selector/variant-selector.js +2 -2
- package/dist/matter/variant-selector/variant-selector.js.map +1 -1
- package/dist/particles/address-summary/address-summary.js +2 -2
- package/dist/particles/address-summary/address-summary.js.map +1 -1
- package/dist/particles/alert-dialog/alert-dialog.d.ts +5 -1
- package/dist/particles/alert-dialog/alert-dialog.js +5 -2
- package/dist/particles/alert-dialog/alert-dialog.js.map +1 -1
- package/dist/particles/copy-text/copy-text.d.ts +28 -0
- package/dist/particles/copy-text/copy-text.js +140 -0
- package/dist/particles/copy-text/copy-text.js.map +1 -0
- package/dist/particles/document-field/document-field.d.ts +34 -5
- package/dist/particles/document-field/document-field.js +115 -19
- package/dist/particles/document-field/document-field.js.map +1 -1
- package/dist/particles/field/field.js +1 -1
- package/dist/particles/field/field.js.map +1 -1
- package/dist/particles/footer-link-group/footer-link-group.d.ts +6 -2
- package/dist/particles/footer-link-group/footer-link-group.js +18 -7
- package/dist/particles/footer-link-group/footer-link-group.js.map +1 -1
- package/dist/particles/incentives/incentives.d.ts +6 -2
- package/dist/particles/incentives/incentives.js +7 -2
- package/dist/particles/incentives/incentives.js.map +1 -1
- package/dist/particles/logo/logo.d.ts +3 -2
- package/dist/particles/logo/logo.js +20 -8
- package/dist/particles/logo/logo.js.map +1 -1
- package/dist/particles/navigation-link/navigation-link.d.ts +2 -3
- package/dist/particles/navigation-link/navigation-link.js +4 -4
- package/dist/particles/navigation-link/navigation-link.js.map +1 -1
- package/dist/particles/navigation-link-list/navigation-link-list.d.ts +6 -2
- package/dist/particles/navigation-link-list/navigation-link-list.js +18 -7
- package/dist/particles/navigation-link-list/navigation-link-list.js.map +1 -1
- package/dist/particles/order-details-back/order-details-back.d.ts +28 -0
- package/dist/particles/order-details-back/order-details-back.js +52 -0
- package/dist/particles/order-details-back/order-details-back.js.map +1 -0
- package/dist/particles/order-status-badge/order-status-badge.d.ts +19 -0
- package/dist/particles/order-status-badge/order-status-badge.js +50 -0
- package/dist/particles/order-status-badge/order-status-badge.js.map +1 -0
- package/dist/particles/phone-field/phone-field.js +2 -2
- package/dist/particles/phone-field/phone-field.js.map +1 -1
- package/dist/particles/select/select.js +6 -6
- package/dist/particles/select/select.js.map +1 -1
- package/dist/particles/toast/toast.d.ts +1 -1
- package/dist/particles/toast/toast.js +32 -6
- package/dist/particles/toast/toast.js.map +1 -1
- package/dist/schema.graphql +1014 -28
- package/dist/sdk/actions/address-action-provider.d.ts +16 -0
- package/dist/sdk/actions/address-action-provider.js +20 -0
- package/dist/sdk/actions/address-action-provider.js.map +1 -0
- package/dist/sdk/actions/analytics-action-provider.js +1 -1
- package/dist/sdk/actions/index.d.ts +3 -1
- package/dist/sdk/actions/index.js +3 -1
- package/dist/sdk/actions/order-action-provider.d.ts +12 -0
- package/dist/sdk/actions/order-action-provider.js +16 -0
- package/dist/sdk/actions/order-action-provider.js.map +1 -0
- package/dist/sdk/actions/types.d.ts +6 -2
- package/dist/sdk/address/address-fields.js +38 -0
- package/dist/sdk/address/address-fields.js.map +1 -0
- package/dist/sdk/address/checkout-readiness.js +17 -0
- package/dist/sdk/address/checkout-readiness.js.map +1 -0
- package/dist/sdk/address/constants.js +60 -0
- package/dist/sdk/address/constants.js.map +1 -0
- package/dist/sdk/address/country-display.js +19 -0
- package/dist/sdk/address/country-display.js.map +1 -0
- package/dist/sdk/address/country-rules.js +30 -0
- package/dist/sdk/address/country-rules.js.map +1 -0
- package/dist/sdk/address/format-address.js +18 -0
- package/dist/sdk/address/format-address.js.map +1 -0
- package/dist/sdk/address/iso-countries.js +262 -0
- package/dist/sdk/address/iso-countries.js.map +1 -0
- package/dist/sdk/address/layouts.js +56 -0
- package/dist/sdk/address/layouts.js.map +1 -0
- package/dist/sdk/address/messages.js +97 -0
- package/dist/sdk/address/messages.js.map +1 -0
- package/dist/sdk/address/operations.d.ts +54 -0
- package/dist/sdk/address/operations.js +83 -0
- package/dist/sdk/address/operations.js.map +1 -0
- package/dist/sdk/address/postal-code-lookup-operations.js +23 -0
- package/dist/sdk/address/postal-code-lookup-operations.js.map +1 -0
- package/dist/sdk/address/postal-code.js +11 -0
- package/dist/sdk/address/postal-code.js.map +1 -0
- package/dist/sdk/address/schema.js +14 -0
- package/dist/sdk/address/schema.js.map +1 -0
- package/dist/sdk/address/server-fields.js +25 -0
- package/dist/sdk/address/server-fields.js.map +1 -0
- package/dist/sdk/address/switch-country.js +57 -0
- package/dist/sdk/address/switch-country.js.map +1 -0
- package/dist/sdk/address/types.d.ts +26 -0
- package/dist/sdk/address/use-address-actions.d.ts +29 -0
- package/dist/sdk/address/use-address-actions.js +247 -0
- package/dist/sdk/address/use-address-actions.js.map +1 -0
- package/dist/sdk/address/use-address-by-postal-code.js +57 -0
- package/dist/sdk/address/use-address-by-postal-code.js.map +1 -0
- package/dist/sdk/address/validate-address.js +62 -0
- package/dist/sdk/address/validate-address.js.map +1 -0
- package/dist/sdk/cache/storefront-cache.js +29 -4
- package/dist/sdk/cache/storefront-cache.js.map +1 -1
- package/dist/sdk/client/fetch-page-data.d.ts +11 -0
- package/dist/sdk/client/fetch-page-data.js +12 -1
- package/dist/sdk/client/fetch-page-data.js.map +1 -1
- package/dist/sdk/content/create-content-runtime.js +1 -8
- package/dist/sdk/content/create-content-runtime.js.map +1 -1
- package/dist/sdk/data/failed-fields.js +23 -0
- package/dist/sdk/data/failed-fields.js.map +1 -0
- package/dist/sdk/data/index.js +2 -2
- package/dist/sdk/data/page-reactive-data-root.d.ts +45 -7
- package/dist/sdk/data/page-reactive-data-root.js +88 -12
- package/dist/sdk/data/page-reactive-data-root.js.map +1 -1
- package/dist/sdk/data/reactive-list-item-data-provider.js +2 -2
- package/dist/sdk/generated/fragment-types.js +2 -0
- package/dist/sdk/generated/fragment-types.js.map +1 -1
- package/dist/sdk/generated/gql.js +3 -1
- package/dist/sdk/generated/gql.js.map +1 -1
- package/dist/sdk/generated/graphql.d.ts +8 -0
- package/dist/sdk/generated/graphql.js +212 -1
- package/dist/sdk/generated/graphql.js.map +1 -1
- package/dist/sdk/i18n/messages/en-US.js +138 -1
- package/dist/sdk/i18n/messages/en-US.json +138 -1
- package/dist/sdk/i18n/messages/en-US.json.d.ts +137 -0
- package/dist/sdk/i18n/messages/es-ES.js +138 -1
- package/dist/sdk/i18n/messages/es-ES.json +138 -1
- package/dist/sdk/i18n/messages/pt-BR.js +138 -1
- package/dist/sdk/i18n/messages/pt-BR.json +138 -1
- package/dist/sdk/navigation/current-page.js +30 -0
- package/dist/sdk/navigation/current-page.js.map +1 -0
- package/dist/sdk/navigation/not-found-log.d.ts +28 -0
- package/dist/sdk/navigation/not-found-log.js +133 -0
- package/dist/sdk/navigation/not-found-log.js.map +1 -0
- package/dist/sdk/navigation/path.js +25 -0
- package/dist/sdk/navigation/path.js.map +1 -0
- package/dist/sdk/navigation/redirect.d.ts +11 -0
- package/dist/sdk/navigation/redirect.js +22 -0
- package/dist/sdk/navigation/redirect.js.map +1 -0
- package/dist/sdk/navigation/report.js +70 -0
- package/dist/sdk/navigation/report.js.map +1 -0
- package/dist/sdk/navigation/resolve-storefront-href.d.ts +12 -0
- package/dist/sdk/navigation/resolve-storefront-href.js +214 -0
- package/dist/sdk/navigation/resolve-storefront-href.js.map +1 -0
- package/dist/sdk/navigation/route-identity-provider.d.ts +11 -0
- package/dist/sdk/navigation/route-identity-provider.js +20 -0
- package/dist/sdk/navigation/route-identity-provider.js.map +1 -0
- package/dist/sdk/navigation/route-map-provider.d.ts +19 -0
- package/dist/sdk/navigation/route-map-provider.js +29 -0
- package/dist/sdk/navigation/route-map-provider.js.map +1 -0
- package/dist/sdk/navigation/storefront-link.d.ts +16 -0
- package/dist/sdk/navigation/storefront-link.js +66 -0
- package/dist/sdk/navigation/storefront-link.js.map +1 -0
- package/dist/sdk/navigation/types.d.ts +45 -0
- package/dist/sdk/navigation/use-storefront-navigation.d.ts +26 -0
- package/dist/sdk/navigation/use-storefront-navigation.js +73 -0
- package/dist/sdk/navigation/use-storefront-navigation.js.map +1 -0
- package/dist/sdk/navigation/use-storefront-route.d.ts +26 -0
- package/dist/sdk/navigation/use-storefront-route.js +87 -0
- package/dist/sdk/navigation/use-storefront-route.js.map +1 -0
- package/dist/sdk/navigation/use-storefront-search-params.d.ts +11 -0
- package/dist/sdk/navigation/use-storefront-search-params.js +16 -0
- package/dist/sdk/navigation/use-storefront-search-params.js.map +1 -0
- package/dist/sdk/orders/completion-badge.d.ts +8 -0
- package/dist/sdk/orders/completion-badge.js +47 -0
- package/dist/sdk/orders/completion-badge.js.map +1 -0
- package/dist/sdk/orders/order-operations.d.ts +19 -0
- package/dist/sdk/orders/order-operations.js +21 -0
- package/dist/sdk/orders/order-operations.js.map +1 -0
- package/dist/sdk/orders/use-order-actions.d.ts +23 -0
- package/dist/sdk/orders/use-order-actions.js +62 -0
- package/dist/sdk/orders/use-order-actions.js.map +1 -0
- package/dist/sdk/payment/gateway-origin.js +26 -13
- package/dist/sdk/payment/gateway-origin.js.map +1 -1
- package/dist/sdk/payment/gateway.d.ts +14 -2
- package/dist/sdk/payment/gateway.js +32 -14
- package/dist/sdk/payment/gateway.js.map +1 -1
- package/dist/sdk/payment/operations.js +11 -0
- package/dist/sdk/payment/operations.js.map +1 -1
- package/dist/sdk/payment/submission-registry.d.ts +2 -1
- package/dist/sdk/payment/submission-registry.js +12 -8
- package/dist/sdk/payment/submission-registry.js.map +1 -1
- package/dist/sdk/payment/use-payment-actions.d.ts +5 -3
- package/dist/sdk/payment/use-payment-actions.js +5 -2
- package/dist/sdk/payment/use-payment-actions.js.map +1 -1
- package/dist/sdk/payment/use-place-order.d.ts +6 -3
- package/dist/sdk/payment/use-place-order.js +333 -143
- package/dist/sdk/payment/use-place-order.js.map +1 -1
- package/dist/sdk/seo/page-alternates.d.ts +55 -0
- package/dist/sdk/seo/page-alternates.js +132 -0
- package/dist/sdk/seo/page-alternates.js.map +1 -0
- package/dist/utils/own-value.js +19 -0
- package/dist/utils/own-value.js.map +1 -0
- package/dist/utils/safe-href.d.ts +4 -4
- package/dist/utils/safe-href.js +31 -22
- package/dist/utils/safe-href.js.map +1 -1
- package/package.json +5 -1
- package/skills/storefront-asset/SKILL.md +111 -0
- package/skills/storefront-asset/references/api-side.md +153 -0
- package/skills/storefront-asset/references/findings.md +40 -0
- package/skills/storefront-asset/references/ui-side.md +94 -0
- package/src/styles/tailwind.css +27 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"field.js","names":["BaseField","Field"],"sources":["../../../src/particles/field/field.tsx"],"sourcesContent":["'use client';\nimport type { FieldValidityState } from '@base-ui/react/field';\nimport { Field as BaseField } from '@base-ui/react/field';\nimport { defineComponent } from '@vtex/fs-core';\nimport type { ComponentPropsWithRef, ReactNode, RefCallback } from 'react';\nimport { createContext, useCallback, useContext, useEffect, useMemo, useRef } from 'react';\nimport type { Control, RegisterOptions } from 'react-hook-form';\nimport { useController, useFormContext, useFormState } from 'react-hook-form';\nimport { Checkbox } from '../checkbox/checkbox.js';\nimport { registerFieldClearer } from './bound-field-registry.js';\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport type FieldRootProps = ComponentPropsWithRef<'div'> & {\n disabled?: boolean;\n invalid?: boolean;\n name?: string;\n};\n\nexport type FieldLabelProps = ComponentPropsWithRef<'label'>;\n\nexport type FieldControlProps = Omit<ComponentPropsWithRef<'input'>, 'children'>;\n\nexport type FieldDescriptionProps = ComponentPropsWithRef<'p'>;\n\nexport type FieldErrorProps = ComponentPropsWithRef<'div'> & {\n /**\n * Controls when the error is shown. `true` always shows it (for externally/caller-driven\n * validity, e.g. presence-only checks with no native constraint validation); a\n * `ValidityState` key shows it only when that specific native validity flag is set. Omit to\n * fall back to Base UI's default: shown whenever the field's tracked validity is invalid.\n * Note `match={false}` does not suppress the error — it falls back to that default\n * invalid-driven behavior; callers needing a boolean should use `invalid ? true : 'customError'`.\n * Only consulted on the unbound path — a bound `Field.Error` renders from the field contract's\n * name and react-hook-form's error state instead.\n */\n match?: boolean | keyof ValidityState;\n};\n\nexport type FieldValidityProps = {\n children: (state: FieldValidityState) => ReactNode;\n};\n\n/** One choice of a field whose value is picked from a list. Platform-owned; an author reorders the part, not the list. */\nexport type FieldOption = Readonly<{ value: string; label: string }>;\n\n/** A rule member constrained to `value`, carrying its own message — react-hook-form's own shape for\n * a constrained validation rule, declared here as our own type (Decision 5). */\nexport type FieldRuleConstraint<T> = Readonly<{ value: T; message: string }>;\n\n/** A field-level validator: reads the live value, returns `true` (valid), a message, or `false`. */\nexport type FieldValidator = (value: unknown) => boolean | string;\n\n/**\n * The fieldset's validation rule for this field, handed to `useBoundField` and mapped internally to\n * react-hook-form's `RegisterOptions` (`toRegisterOptions`) — nothing that imports this type from the\n * barrel ever names a react-hook-form type (RFC `Dependencies`).\n *\n * These seven members are a starting point measured against every rule this repository authors\n * today, not a closed contract: `RegisterOptions` carries more (`setValueAs`, `deps`, `valueAsNumber`,\n * …), and a member is added here the day a field actually needs it — this is not a deliberately closed\n * set to work around.\n */\nexport type FieldRule = Readonly<{\n required?: boolean | string | FieldRuleConstraint<boolean>;\n min?: number | string | FieldRuleConstraint<number | string>;\n max?: number | string | FieldRuleConstraint<number | string>;\n minLength?: number | FieldRuleConstraint<number>;\n maxLength?: number | FieldRuleConstraint<number>;\n pattern?: RegExp | FieldRuleConstraint<RegExp>;\n validate?: FieldValidator | Readonly<Record<string, FieldValidator>>;\n}>;\n\n/**\n * The one place `RegisterOptions` is named as a value type. `FieldRule`'s members are declared\n * independently of react-hook-form's (Decision 5), but each already carries react-hook-form's own\n * runtime shape (a bare value, or `{ value, message }`), so this is an assertion, not a per-member\n * transform. `field-rule.type-test.tsx` pins the boundary from the other side: it fails to compile if\n * `FieldRule` ever widens back to a bare `RegisterOptions` alias.\n */\nfunction toRegisterOptions(rule: FieldRule | undefined): RegisterOptions | undefined {\n return rule as RegisterOptions | undefined;\n}\n\n/**\n * A control's own bid to make its field required. A control raises; it never lowers — so `false` and\n * an absent raise are the same thing, no opinion, and a baseline that already says required survives\n * either. `message` is the copy the raise brings with it (`PhoneField`'s `errorRequired`), used only\n * when the raise is what carries the field to required.\n */\nexport type FieldRequirementRaise = Readonly<{\n required?: boolean;\n message?: string;\n}>;\n\n/**\n * One field's requiredness, resolved once and read everywhere: the announcement, the blank check and\n * the error copy all come from here, so they cannot disagree.\n */\nexport type ResolvedRequiredness = Readonly<{\n /** `baselineRequired || authorRaise`. Spread into `aria-required` as `required || undefined`, so a\n * field never announces `aria-required=\"false\"`. */\n required: boolean;\n /** What the blank check reports. Absent when neither side supplied copy. */\n message?: string;\n /** The rule to register with: the baseline, carrying the blank check when the field is required. */\n rule: FieldRule | undefined;\n}>;\n\n/**\n * react-hook-form's own reading of `required`: `.value` for the object form\n * (`{ value: boolean; message: string }`), plain truthiness otherwise (a boolean or a message\n * string). A message string is itself the copy, which is why the two are read together.\n */\nfunction readBaseline(rule: FieldRule | undefined): { required: boolean; message?: string } {\n const required = rule?.required;\n if (typeof required === 'object' && required !== null) {\n return { required: Boolean(required.value), message: required.value ? required.message : undefined };\n }\n if (typeof required === 'string') return { required: true, message: required };\n return { required: Boolean(required) };\n}\n\n/** A `validate` member is either one function or a record of named ones; downstream only wants the record. */\nfunction normaliseValidators(validate: FieldRule['validate']): Record<string, FieldValidator> {\n if (!validate) return {};\n return typeof validate === 'function' ? { validate } : { ...validate };\n}\n\n/**\n * Composes `validator` into `rule` under `name`, alongside whatever the rule already validates —\n * never instead of it. The two composites reach for this to keep their own format check while a\n * fieldset-supplied validator stays in force; before it, both discarded their own validation whole\n * the moment the fieldset supplied any.\n */\nexport function mergeFieldValidator(rule: FieldRule | undefined, name: string, validator: FieldValidator): FieldRule {\n return { ...rule, validate: { ...normaliseValidators(rule?.validate), [name]: validator } };\n}\n\n/**\n * The one resolution of `required = baselineRequired || authorRaise`. The fieldset's `FieldRule` is\n * the baseline; a control may raise a field to required and never lower one, so `raise.required`\n * only ever adds. Read-only deliberately plays no part: a field that is read-only and required stays\n * required, and an unsatisfiable form is the declared baseline's fault, not this function's.\n *\n * Baseline copy wins over the raise's, matching react-hook-form's own order — it evaluates `required`\n * before `validate`, so the fieldset's message was already the one a shopper saw whenever both existed.\n *\n * `required` catches an empty string but not a space-filled one (`inputValue === ''` is\n * react-hook-form's only emptiness test for a string), so the blank check goes in beside it. It is\n * installed last, so it cannot be displaced by a same-named validator from the rule.\n */\nexport function resolveRequiredness(rule: FieldRule | undefined, raise?: FieldRequirementRaise): ResolvedRequiredness {\n const baseline = readBaseline(rule);\n const required = baseline.required || Boolean(raise?.required);\n\n if (!required) return { required: false, rule };\n\n const message = baseline.message ?? (baseline.required ? undefined : raise?.message);\n const notBlank: FieldValidator = (value) => String(value ?? '').trim().length > 0 || message || false;\n\n return { required, message, rule: { ...mergeFieldValidator(rule, 'notBlank', notBlank), required: message ?? true } };\n}\n\n/**\n * One field's identity and its static facts: the argument list `useController` needs, hoisted out of\n * a prop-drill that composition made impossible. Not a mirror of state react-hook-form also holds —\n * it carries NO live value, so it never changes while the shopper types, and the parts subscribe to\n * react-hook-form for value, error, dirty and touched.\n */\nexport type FieldContract = Readonly<{\n /** Full, namespaced react-hook-form field name, e.g. `shippingAddress.street`. */\n name: string;\n /** Resolved label text: the Control's `label` prop, else the platform's default. */\n label?: string;\n placeholder?: string;\n /** Seed value, registered as react-hook-form's field-level `defaultValue`. */\n defaultValue?: unknown;\n rule?: FieldRule;\n /** Only for a field whose value is picked from a list. Unread until `Field.Select` is added. */\n options?: readonly FieldOption[];\n disabled?: boolean;\n readOnly?: boolean;\n autoComplete?: string;\n inputMode?: 'none' | 'text' | 'decimal' | 'numeric' | 'tel' | 'search' | 'email' | 'url';\n}>;\n\n/** Supplies the field contract to everything below it. Renders no DOM. Never catalogued. */\nexport type FieldBindingProps = FieldContract & {\n children?: ReactNode;\n};\n\n/**\n * Provided by a fieldset. Declares the one namespace every name registered through it belongs to, and\n * registers a full field name, returning its unregister.\n */\nexport type FieldsetFieldRegistry = {\n /**\n * The namespace this fieldset owns — the prefix of every full name its own controls register. Declared\n * by the provider, never inferred from a caller.\n */\n namespace: string;\n /**\n * Registers a full, namespaced field name and returns a release function. Refcounted by the provider,\n * so two callers holding one name produce one entry, released when the last of them releases. The\n * returned function releases at most once.\n */\n registerFieldName: (name: string) => () => void;\n};\n\n/** What `useFieldsetFieldNames` hands a fieldset: the value to provide, and the list to register. */\nexport type FieldsetFieldNames = Readonly<{\n /** Referentially stable while `namespace` is unchanged. Goes straight into the context provider. */\n registry: FieldsetFieldRegistry;\n /** Every full field name currently held, in first-registration order. Stable identity. */\n getFieldNames: () => readonly string[];\n}>;\n\n/** New bound part. Reads the contract, binds through useBoundField, drives Checkbox.Root. */\nexport type FieldCheckboxProps = {\n readonly className?: string;\n readonly children?: ReactNode;\n};\n\n// ---------------------------------------------------------------------------\n// Field contract context\n// ---------------------------------------------------------------------------\n\nconst FieldContext = createContext<FieldContract | null>(null);\n\n/** Reads the field contract. Returns null when no binding is above the caller. */\nexport function useField(): FieldContract | null {\n return useContext(FieldContext);\n}\n\n/**\n * Per (control, field name), the seed the *fieldset supplied* for that name — `contract.defaultValue`\n * as handed down, not the value any one part narrowed it to before calling `resetField`. The\n * distinction only shows up where the RFC says two parts may bind one name (\"A fieldset registers\n * once, collecting the field names its own children mount, refcounted so that a part remounting, or\n * two parts binding one name, never unregisters a name still in use\"): a text part and a checkbox\n * part over the same name narrow one seed to two different types, so a ledger holding the narrowed\n * value disagrees with itself on every render — neither part's entry ever satisfies the other's\n * `seedsEqual` check, and the two reset each other forever. Keyed on what the fieldset supplied, both\n * parts read the same entry, the first to run adopts and the rest skip. The second part's own\n * `useController` registration still carries its own narrowing, so it reads dirty against a baseline\n * it did not set — the honest outcome, and better than a hang.\n *\n * Keyed by the `control` object itself rather than by any `FieldBinding` component instance. Adoption is a\n * property of the field name within this form, not of whichever component instance happens to be\n * mounted right now: a fieldset's own conditional control (the `documentType` gate on\n * `ClientProfileDocumentControl`, a country-descriptor swap in `ShippingAddressFieldset`) unmounts and\n * remounts its `FieldBinding` with a brand-new instance that remembers nothing, while the shopper's\n * edit survives the unmount in `control._formValues` — nothing in this codebase sets\n * `shouldUnregister`. A `useRef` latch scoped to the instance cannot see that history and re-adopts the\n * stale seed over the surviving edit; this ledger can, because it outlives the instance. A `WeakMap` so\n * a form that itself unmounts (a fresh `useForm()` call, a fresh `control`) starts over empty, exactly\n * as it should.\n *\n * A `Map` of seeds, not a `Set` of names: recording only that a name was adopted cannot distinguish a\n * seed already in force from a different one that supersedes it, so a later `defaultValue` could never\n * reach the form — that was the whole bug this ledger replaces. `has(name)` is read separately from\n * `get(name)` everywhere below, so \"never adopted\" and \"adopted `undefined`\" stay distinguishable\n * states.\n */\nconst adoptedSeeds = new WeakMap<Control, Map<string, unknown>>();\n\nfunction adoptedSeedsFor(control: Control): Map<string, unknown> {\n let seeds = adoptedSeeds.get(control);\n if (!seeds) {\n seeds = new Map();\n adoptedSeeds.set(control, seeds);\n }\n return seeds;\n}\n\n/**\n * Structural rather than referential equality between two seeds, mirroring `valuesEqual` in\n * `form.tsx:128-130` and its documented reasoning: a field's seed is a primitive or plain record a\n * bound Control's contract carries (a string, a boolean, a small object) — never a function or a\n * circular structure — so a `JSON.stringify` comparison is a safe stand-in for a real deep-equal.\n * Duplicated rather than imported: `field.tsx` imports nothing from `form.tsx`, and the particles are\n * deliberately independent modules.\n *\n * `Object.is` is rejected here even though neither shipped shape's `normalise` (`TEXT_FIELD_VALUE`,\n * `BOOLEAN_FIELD_VALUE`) can hand this function anything but a primitive today — this guard is\n * defensive against a shape that does not exist yet, not against a reachable path. The day a shape's\n * `normalise` returns a record or an array, a fieldset that hands out a freshly-allocated seed with\n * the same content on every render would fail `Object.is` against itself, and the\n * resetField → formState → render → resetField chain below would never terminate (measured: swapping\n * in `Object.is` hung the suite past 150 seconds, until a render ceiling was added that turns the same\n * mutation into a named failure in seconds). Do not remove this as dead code on the\n * grounds that no object-shaped seed reaches it today — the regression test that pins it is\n * `field.test.tsx`'s `'terminates when the seed is a fresh, structurally identical object literal on\n * every render'`, which exercises `useSeedAdoption` through a test-only shape precisely because no\n * shipped shape can.\n */\nfunction seedsEqual(a: unknown, b: unknown): boolean {\n return a === b || JSON.stringify(a) === JSON.stringify(b);\n}\n\n/**\n * react-hook-form copies a per-field `defaultValue` into control._defaultValues only when\n * shouldUnregister is set, which nothing here sets — so without this, the pristine comparison for a\n * seeded field runs against `undefined` and any touched field stays dirty for good. Adopted once per\n * (control, name) for a given seed, tracked in `adoptedSeeds`, which survives this component instance\n * unmounting and remounting — so a remount with the same stale seed prop never overwrites a shopper's\n * live edit, and a later, *different* seed reaches a field the shopper has not touched without the\n * shopper having to reload the page.\n *\n * Called from `useBoundField`, not from `FieldBinding` (relocated, T57): the caller is now the very\n * part whose own `useController` call registers `name`. `useController` calls `control.register(name,\n * …)` from a `useRef` initialiser during the first render, and `register` adds to `_names.mount`\n * synchronously — before that render returns, let alone before this effect's first run. So the mount\n * guard below is satisfied on this effect's very first run for the caller's own name; the ordering\n * `FieldBinding` could not rely on (a descendant control mounting on a *later* commit than the binding\n * around it) no longer arises, because binder and adopter are now one and the same component instance.\n * The T-5 test (`field.test.tsx`, a control that shows up on a later render than its enclosing\n * `FieldBinding`) still passes — not because that race is still possible, but because nothing here\n * special-cases it away: the guard simply reads true the one time it is asked.\n *\n * No dependency array all the same: this effect must still re-run on every render, because the guards\n * below can otherwise miss their only remaining chance — the dirty-then-clean case. A seed arriving\n * while the field is dirty is skipped by the third guard and never recorded; the only later signal that\n * it can now be adopted is the field itself turning clean, which changes neither `name` nor the\n * normalised seed this hook receives, so a dependency array keyed to its own arguments would never\n * re-invoke it. Re-running on every render costs nothing extra here: every guard below but the\n * adopting one is a no-op, and useFormContext() already forces a re-render on every formState emission\n * regardless (Invariant 4 is violated in letter, not by this effect), so this adds no new re-render of\n * its own.\n *\n * The first guard below is what does termination, not just correctness: react-hook-form rebuilds\n * formState on every emission and FormProvider memoises its context value with formState in its own\n * dependency list, so useFormContext()'s return value changes identity after every resetField call.\n * Without that guard, that identity change would drive another render, which would call resetField\n * again, which would change formState again — an unbounded loop. Its early return breaks it: once the\n * ledger's recorded seed for this name is equal (by `seedsEqual`) to the current `defaultValue`, every\n * further run for that name exits before reaching resetField — which is also why it must be the first\n * check after the ledger lookup, ahead of even the mount guard.\n *\n * Precondition this re-seed relies on and does not itself enforce: `defaultValue` regressing to a\n * reactive fragment's incomplete-read fallback once the field has already adopted real data.\n * Probing this against `useReactiveEntity`'s fallback expression (`use-reactive-entity.ts:57`), mounting\n * a clean field over real, complete data and then trying to make `fragmentResult.complete` flip back to\n * false: measured as **reachable**, not merely suspected. Two of the three probes reproduced it —\n * evicting a field the fragment selects (`cache.evict({ id, fieldName })`, then `cache.gc()`) and\n * moving the root query's entity id to one never written to the cache (a session or orderForm change)\n * both regress the read, and this effect cannot tell that apart from a genuine, saved change: the\n * second probe blanked a clean control from \"Ada\" to \"\", and an independent audit reproduced it.\n * `client.clearStore()` on a still-mounted, still-referenced entity did not reproduce it. The\n * fieldsets close that gap above this hook: they withhold their controls until `useEntityDataResolved`\n * reports their entity resolved, so a regressed read never reaches this effect through a fieldset.\n */\nfunction useSeedAdoption(name: string, suppliedSeed: unknown, narrowedSeed: unknown): void {\n const form = useFormContext();\n\n useEffect(() => {\n if (!form) return;\n const seeds = adoptedSeedsFor(form.control);\n if (seeds.has(name) && seedsEqual(seeds.get(name), suppliedSeed)) return;\n if (!form.control._names.mount.has(name)) return;\n if (seeds.has(name) && form.getFieldState(name).isDirty) return;\n form.resetField(name, { defaultValue: narrowedSeed });\n seeds.set(name, suppliedSeed);\n });\n}\n\nexport function FieldBinding({\n name,\n label,\n placeholder,\n defaultValue,\n rule,\n options,\n disabled,\n readOnly,\n autoComplete,\n inputMode,\n children,\n}: FieldBindingProps): ReactNode {\n // No live value in the contract (Decision 1), so it is stable across a whole editing session as\n // long as the caller's own props are stable — memoised here so an unrelated re-render of whatever\n // renders this binding never produces a new object identity for subscribers.\n const contract = useMemo<FieldContract>(\n () => ({ name, label, placeholder, defaultValue, rule, options, disabled, readOnly, autoComplete, inputMode }),\n [name, label, placeholder, defaultValue, rule, options, disabled, readOnly, autoComplete, inputMode],\n );\n\n return <FieldContext.Provider value={contract}>{children}</FieldContext.Provider>;\n}\n\n// ---------------------------------------------------------------------------\n// Bound part support — the empty value and seed narrowing a part owns (T57)\n// ---------------------------------------------------------------------------\n\n/**\n * What a bound part knows about its own value that `FieldBinding` does not (RFC `The rest of the\n * parts`): the value it holds when it holds nothing, and how to narrow an arbitrary seed to that\n * type. Each shape is the pair together, not two loose values, so a part cannot pair a boolean\n * `empty` with a string `normalise`.\n */\nexport type FieldValueShape<T> = Readonly<{\n /** The value `useBoundField`'s registered clearer writes — this field's own type, not `''` by default. */\n empty: T;\n /** Narrows an arbitrary seed (`contract.defaultValue`) to this part's own value type. */\n normalise: (seed: unknown) => T;\n}>;\n\n/** A text-shaped bound field: empty is `''`, and a non-string seed narrows to `''`. */\nexport const TEXT_FIELD_VALUE: FieldValueShape<string> = {\n empty: '',\n normalise: (seed) => (typeof seed === 'string' ? seed : ''),\n};\n\n/** A boolean-shaped bound field: empty is `false`, and any seed narrows through `Boolean(...)`. */\nexport const BOOLEAN_FIELD_VALUE: FieldValueShape<boolean> = {\n empty: false,\n normalise: (seed) => Boolean(seed),\n};\n\n/**\n * What `useBoundField` hands back: react-hook-form's own `field` object, reshaped into a type this\n * repository declares (Decision 6) — returning `useController`'s own `ControllerRenderProps` would put\n * a react-hook-form type back on the barrel the moment this hook is exported. `value: T`, not\n * `unknown`, because `BoundFieldControl` passes `field.value` straight into `BaseField.Control`, which\n * an `unknown` would reject. `ref` is a plain `RefCallback<HTMLElement>`, assignable to both\n * `Ref<HTMLInputElement>` and `Ref<HTMLButtonElement>`, so every existing `ref={field.ref}` site and\n * `useComposedRef` call keep compiling unchanged.\n */\nexport type BoundField<T> = Readonly<{\n name: string;\n value: T;\n onChange: (...event: unknown[]) => void;\n onBlur: () => void;\n ref: RefCallback<HTMLElement>;\n /** The resolved requiredness, so the control that renders the input announces the same value the\n * registered rule enforces. Spread as `required || undefined` — a field never announces `false`. */\n required: boolean;\n}>;\n\n/**\n * The only caller of `useController` in this package (I-1). Registers the field with react-hook-form,\n * adopts the same normalised seed as the dirty baseline (Decision 1), and registers a clearer that\n * writes the shape's own empty value for as long as the part stays mounted (FR-5) — three facts a\n * bound part previously scattered across its own call site, `FieldBinding` and `FormClear`.\n *\n * The effective rule is `rule ?? contract.rule`: a part with no rule of its own (`BoundFieldControl`,\n * `BoundFieldCheckbox`, `BoundFieldSelectRoot`) falls back to the contract's; `BoundPhoneField` and\n * `BoundDocumentField` pass their own `useMemo`-derived rule, which already merges the particle's\n * format check into `contract.rule` rather than replacing it.\n *\n * Requiredness resolves here rather than at each call site (FR: one chokepoint), so no bound control\n * can register a rule whose enforcement and announcement disagree, or forget the blank check. A\n * control that wants its field required passes a `raise`; only `FieldSelectTrigger`, which announces\n * from a different component than the one that binds, calls `resolveRequiredness` for itself.\n */\nexport function useBoundField<T>(\n contract: FieldContract,\n shape: FieldValueShape<T>,\n rule?: FieldRule,\n raise?: FieldRequirementRaise,\n): BoundField<T> {\n const { control, setValue } = useFormContext();\n // Memoised on the raise's two scalars, not on the raise object: callers build it inline, so a\n // fresh identity every render would hand `useController` a fresh `rules` object every render.\n const raiseRequired = raise?.required;\n const raiseMessage = raise?.message;\n const requiredness = useMemo(\n () => resolveRequiredness(rule ?? contract.rule, { required: raiseRequired, message: raiseMessage }),\n [rule, contract.rule, raiseRequired, raiseMessage],\n );\n // One named local, referenced by both useController's defaultValue and the adoption call below\n // (AC-3) — registration and the dirty baseline must never drift apart by reading two independent\n // normalise() calls.\n const seed = shape.normalise(contract.defaultValue);\n\n const { field } = useController({\n name: contract.name,\n rules: toRegisterOptions(requiredness.rule),\n defaultValue: seed,\n });\n\n useSeedAdoption(contract.name, contract.defaultValue, seed);\n\n useEffect(\n () =>\n registerFieldClearer(control, contract.name, () => setValue(contract.name, shape.empty, { shouldDirty: true })),\n [control, contract.name, setValue, shape.empty],\n );\n\n return { ...field, required: requiredness.required };\n}\n\n// ---------------------------------------------------------------------------\n// Fieldset registration seam (provider ships from a later fieldset task)\n// ---------------------------------------------------------------------------\n\n/** Exported so those tasks import it rather than redefine it. */\nexport const FieldsetFieldContext = createContext<FieldsetFieldRegistry | null>(null);\n\n/** Unchanged: the registry above the caller, or null. Now carrying a namespace. */\nexport function useFieldsetContext(): FieldsetFieldRegistry | null {\n return useContext(FieldsetFieldContext);\n}\n\n/**\n * Registers `${namespace}.${key}` with the enclosing fieldset while the caller is mounted, and returns\n * that full name. Throws during render when a registry is above the caller and declares a different\n * namespace. Registers nothing and throws nothing when no registry is above. Holds no state: the\n * refcount belongs to the provider.\n */\nexport function useFieldsetField(namespace: string, key: string): string {\n const registry = useFieldsetContext();\n const name = `${namespace}.${key}`;\n\n if (registry && registry.namespace !== namespace) {\n throw new Error(\n `A control asked to register \"${name}\", but the fieldset registry above it owns the namespace \"${registry.namespace}\". A registry serves exactly one namespace, declared by the fieldset that provides it.`,\n );\n }\n\n useEffect(() => {\n if (!registry) return;\n return registry.registerFieldName(name);\n }, [registry, name]);\n\n return name;\n}\n\n/**\n * The field-name bookkeeping a fieldset provides. Refcounted, so two callers of one name collapse to one\n * entry however they arrived — including a caller that reaches `registerFieldName` directly.\n */\nexport function useFieldsetFieldNames(namespace: string): FieldsetFieldNames {\n // Own state, not module state: this ref lives on the fieldset's own component instance, so it\n // survives every re-render regardless of whether the caller memoises the registry it hands out.\n const counts = useRef(new Map<string, number>());\n\n const registerFieldName = useCallback((name: string) => {\n counts.current.set(name, (counts.current.get(name) ?? 0) + 1);\n\n // One-shot: a counted Map, unlike the Set it replaces, is not idempotent on release. Without this\n // latch a double-invoked unregister (StrictMode, or a defensive caller) would decrement past a\n // second live holder and evict a name it still holds.\n let released = false;\n return () => {\n if (released) return;\n released = true;\n\n const remaining = (counts.current.get(name) ?? 0) - 1;\n if (remaining > 0) {\n counts.current.set(name, remaining);\n } else {\n counts.current.delete(name);\n }\n };\n }, []);\n\n const getFieldNames = useCallback((): readonly string[] => Array.from(counts.current.keys()), []);\n\n // Memoised on namespace: a fieldset that re-points at a different namespace produces a new registry\n // object, so every mounted control re-registers under the new namespace in the same commit.\n const registry = useMemo<FieldsetFieldRegistry>(\n () => ({ namespace, registerFieldName }),\n [namespace, registerFieldName],\n );\n\n return { registry, getFieldNames };\n}\n\n/**\n * Throws during render when an ancestor fieldset is already providing field registration for this\n * subtree. Every fieldset calls it in one line.\n */\nexport function useNoAncestorFieldset(componentName: string): void {\n const ancestor = useFieldsetContext();\n\n if (ancestor) {\n throw new Error(\n `${componentName} cannot be nested inside another fieldset. An ancestor fieldset owning the namespace \"${ancestor.namespace}\" already provides field registration for this subtree, so this fieldset's controls would register under that namespace instead of their own. Compose fieldsets as siblings under the same FormRoot instead of nesting them.`,\n );\n }\n}\n\n// ---------------------------------------------------------------------------\n// Definitions\n// ---------------------------------------------------------------------------\n\nexport const FieldRootDefinition = defineComponent<FieldRootProps>({\n name: 'FieldRoot',\n importPath: '@vtex/faststore/components',\n component: FieldRoot,\n providesContext: 'field',\n editor: { label: 'Field Root', category: 'layout' },\n schema: {\n disabled: { type: 'boolean', default: false, label: 'Disabled' },\n },\n guideline:\n 'Form field container that connects a label, input, and validation messages via ARIA. Use Field.Control as the input element, or place a compatible Base UI input (Input, Checkbox, Select) as a direct child. Set invalid=true to trigger Field.Error display. Wrapping a bound Control feeds invalid/dirty/touched from react-hook-form automatically.',\n});\n\nexport const FieldLabelDefinition = defineComponent<FieldLabelProps>({\n name: 'FieldLabel',\n importPath: '@vtex/faststore/components',\n component: FieldLabel,\n requiresContext: 'field',\n editor: { label: 'Field Label', category: 'layout' },\n schema: {},\n guideline:\n \"Accessible label inside Field.Root. Automatically associated with Field.Control's input via ARIA. Place before Field.Control in the DOM. Under a bound Control, a childless Field.Label renders the field contract's label.\",\n});\n\nexport const FieldControlDefinition = defineComponent<FieldControlProps>({\n name: 'FieldControl',\n importPath: '@vtex/faststore/components',\n component: FieldControl,\n requiresContext: 'field',\n editor: { label: 'Field Control', category: 'layout' },\n schema: {},\n guideline:\n \"Renders a native `<input>` element directly. It IS the input — do not place any child inside it. Pass input attributes (type, placeholder, required, etc.) directly as props. It is automatically linked to Field.Label and Field.Error via ARIA. Under a bound Control, it registers with react-hook-form using the field contract's name, seed value, and validation rule.\",\n});\n\nexport const FieldDescriptionDefinition = defineComponent<FieldDescriptionProps>({\n name: 'FieldDescription',\n importPath: '@vtex/faststore/components',\n component: FieldDescription,\n requiresContext: 'field',\n editor: { label: 'Field Description', category: 'layout' },\n schema: {},\n guideline:\n 'Helper text inside Field.Root. Automatically linked to the control via aria-describedby. Always visible regardless of validation state.',\n});\n\nexport const FieldErrorDefinition = defineComponent<FieldErrorProps>({\n name: 'FieldError',\n importPath: '@vtex/faststore/components',\n component: FieldError,\n requiresContext: 'field',\n editor: { label: 'Field Error', category: 'layout' },\n schema: {},\n guideline:\n \"Validation error message inside Field.Root. Shown when the field is in an invalid state. Linked to the control via aria-describedby. Under a bound Control, it renders react-hook-form's error message for the field contract's name.\",\n});\n\nexport const FieldValidityDefinition = defineComponent<FieldValidityProps>({\n name: 'FieldValidity',\n importPath: '@vtex/faststore/components',\n component: FieldValidity,\n requiresContext: 'field',\n editor: { label: 'Field Validity', category: 'layout' },\n schema: {},\n guideline:\n \"Validation feedback slot using a render prop for custom messages derived from the field's validity state. Use for dynamic messages when Field.Error text is not fixed.\",\n});\n\nexport const FieldCheckboxDefinition = defineComponent<FieldCheckboxProps>({\n name: 'FieldCheckbox',\n importPath: '@vtex/faststore/components',\n component: FieldCheckbox,\n requiresContext: 'field',\n editor: { label: 'Field Checkbox', category: 'action' },\n schema: {},\n guideline:\n 'Bound boolean control over the shipped Checkbox particle: reads the enclosing field contract and drives Checkbox.Root through react-hook-form. Compose Checkbox.Indicator as a child for the visual check mark. Place inside Field.Root, the only ancestor that provides the field context it requires today — a bound Control will satisfy it too, once one ships.',\n});\n\n// ---------------------------------------------------------------------------\n// Parts\n// ---------------------------------------------------------------------------\n\nexport function FieldRoot({ disabled, invalid, name, className, children, ...props }: FieldRootProps) {\n const contract = useField();\n const formContext = useFormContext();\n\n if (contract && formContext) {\n return (\n <BoundFieldRoot\n {...props}\n contract={contract}\n disabled={disabled}\n invalid={invalid}\n name={name}\n className={className}\n >\n {children}\n </BoundFieldRoot>\n );\n }\n\n return (\n <BaseField.Root\n {...props}\n disabled={disabled}\n invalid={invalid}\n name={name}\n className={className}\n data-fs-field-root=\"\"\n >\n {children}\n </BaseField.Root>\n );\n}\n\ntype BoundFieldRootProps = FieldRootProps & { contract: FieldContract };\n\nfunction BoundFieldRoot({ contract, disabled, invalid, name, className, children, ...props }: BoundFieldRootProps) {\n const { getFieldState, control } = useFormContext();\n const {\n invalid: rhfInvalid,\n isDirty,\n isTouched,\n } = getFieldState(contract.name, useFormState({ name: contract.name }));\n\n // Base UI's `FieldRoot` computes `dirty = dirtyProp ?? dirtyState`, and `setDirty` returns\n // immediately once `dirtyProp !== undefined` (`FieldRoot.js:50-67`) — so passing `false`\n // unconditionally disables Base UI's own fallback tracking exactly as hard as passing `true` did.\n // That fallback is still needed for any name react-hook-form never registers (a raw Base UI control\n // under a bound `Field.Root`), so the prop is three-state: `undefined` while the name is unmounted,\n // then react-hook-form's own value once it registers — which always wins from then on, in both\n // directions, closing the freeze-at-true latch the unconditional form reopened.\n const isRegistered = control._names.mount.has(contract.name);\n\n return (\n <BaseField.Root\n {...props}\n name={name ?? contract.name}\n disabled={disabled ?? contract.disabled}\n invalid={invalid ?? rhfInvalid}\n dirty={isRegistered ? isDirty : undefined}\n touched={isRegistered ? isTouched : undefined}\n className={className}\n data-fs-field-root=\"\"\n >\n {children}\n </BaseField.Root>\n );\n}\n\nexport function FieldLabel({ className, children, ...props }: FieldLabelProps) {\n const contract = useField();\n const formContext = useFormContext();\n\n if (contract && formContext) {\n return (\n <BoundFieldLabel {...props} contract={contract} className={className}>\n {children}\n </BoundFieldLabel>\n );\n }\n\n return (\n <BaseField.Label {...props} className={className} data-fs-field-label=\"\">\n {children}\n </BaseField.Label>\n );\n}\n\ntype BoundFieldLabelProps = FieldLabelProps & { contract: FieldContract };\n\nfunction BoundFieldLabel({ contract, className, children, ...props }: BoundFieldLabelProps) {\n return (\n <BaseField.Label {...props} className={className} data-fs-field-label=\"\">\n {children ?? contract.label}\n </BaseField.Label>\n );\n}\n\nexport function FieldControl({ className, ...props }: FieldControlProps) {\n const contract = useField();\n const formContext = useFormContext();\n\n if (contract && formContext) {\n return <BoundFieldControl {...props} contract={contract} className={className} />;\n }\n\n return <BaseField.Control {...props} className={className} data-fs-field-control=\"\" />;\n}\n\ntype BoundFieldControlProps = FieldControlProps & { contract: FieldContract };\n\nfunction BoundFieldControl({\n contract,\n placeholder,\n disabled,\n readOnly,\n autoComplete,\n inputMode,\n className,\n ...props\n}: BoundFieldControlProps) {\n const field = useBoundField(contract, TEXT_FIELD_VALUE);\n\n return (\n <BaseField.Control\n {...props}\n name={field.name}\n value={field.value}\n onChange={field.onChange}\n onBlur={field.onBlur}\n ref={field.ref}\n placeholder={placeholder ?? contract.placeholder}\n disabled={disabled ?? contract.disabled}\n readOnly={readOnly ?? contract.readOnly}\n autoComplete={autoComplete ?? contract.autoComplete}\n inputMode={inputMode ?? contract.inputMode}\n aria-required={field.required || undefined}\n className={className}\n data-fs-field-control=\"\"\n />\n );\n}\n\nexport function FieldDescription({ className, children, ...props }: FieldDescriptionProps) {\n return (\n <BaseField.Description {...props} className={className} data-fs-field-description=\"\">\n {children}\n </BaseField.Description>\n );\n}\n\nexport function FieldError({ className, children, ...props }: FieldErrorProps) {\n const contract = useField();\n const formContext = useFormContext();\n\n if (contract && formContext) {\n return (\n <BoundFieldError {...props} contract={contract} className={className}>\n {children}\n </BoundFieldError>\n );\n }\n\n return (\n <BaseField.Error {...props} className={className} data-fs-field-error=\"\">\n {children}\n </BaseField.Error>\n );\n}\n\ntype BoundFieldErrorProps = FieldErrorProps & { contract: FieldContract };\n\n// `match` is not destructured for use — its documented meaning applies to the unbound path only\n// (Decision: bound visibility is driven by react-hook-form's own error for the contract's name).\nfunction BoundFieldError({ contract, className, children, match: _match, ...props }: BoundFieldErrorProps) {\n const { getFieldState } = useFormContext();\n const { error } = getFieldState(contract.name, useFormState({ name: contract.name }));\n\n if (!error) return null;\n // A `validate` that reports failure with `false` rather than a string (e.g. DocumentField's\n // check-digit validator when no `errorMessage` was authored) leaves react-hook-form's own\n // `error.message` empty. Rendering in that case would put an empty `<div data-fs-field-error>` on\n // the page where the field's own default (unbound) anatomy renders nothing at all — a divergence\n // between the composed and default anatomies (finding F4). An author-supplied `children` still\n // always renders regardless, since that text does not depend on react-hook-form's message.\n if (!children && !error.message) return null;\n\n return (\n // `match` forces Base UI past its own native-constraint-validation gate: with no real HTML5\n // constraint on the input, Field.Error's default (no `match`) visibility check reads native\n // validity, not react-hook-form's. We have already decided to render from react-hook-form's\n // error above, so `match` here only lets that decision take effect — it does not affect the\n // message, which is our own `children` below and always wins over Base UI's own computed text.\n <BaseField.Error {...props} match className={className} data-fs-field-error=\"\">\n {children ?? error.message}\n </BaseField.Error>\n );\n}\n\nexport function FieldValidity({ children }: FieldValidityProps) {\n return <BaseField.Validity>{children}</BaseField.Validity>;\n}\n\nexport function FieldCheckbox({ className, children }: FieldCheckboxProps): ReactNode {\n const contract = useField();\n const formContext = useFormContext();\n\n if (contract && formContext) {\n return (\n <BoundFieldCheckbox contract={contract} className={className}>\n {children}\n </BoundFieldCheckbox>\n );\n }\n\n return <Checkbox.Root className={className}>{children}</Checkbox.Root>;\n}\n\ntype BoundFieldCheckboxProps = FieldCheckboxProps & { contract: FieldContract };\n\nfunction BoundFieldCheckbox({ contract, className, children }: BoundFieldCheckboxProps) {\n const field = useBoundField(contract, BOOLEAN_FIELD_VALUE);\n\n return (\n <Checkbox.Root\n checked={field.value}\n onCheckedChange={field.onChange}\n onBlur={field.onBlur}\n ref={field.ref}\n disabled={contract.disabled}\n className={className}\n data-fs-field-checkbox=\"\"\n >\n {children}\n </Checkbox.Root>\n );\n}\n\nexport const Field = {\n Root: FieldRoot,\n Label: FieldLabel,\n Control: FieldControl,\n Description: FieldDescription,\n Error: FieldError,\n Validity: FieldValidity,\n Checkbox: FieldCheckbox,\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"field.js","names":["BaseField","Field"],"sources":["../../../src/particles/field/field.tsx"],"sourcesContent":["'use client';\nimport type { FieldValidityState } from '@base-ui/react/field';\nimport { Field as BaseField } from '@base-ui/react/field';\nimport { defineComponent } from '@vtex/fs-core';\nimport type { ComponentPropsWithRef, ReactNode, RefCallback } from 'react';\nimport { createContext, useCallback, useContext, useEffect, useMemo, useRef } from 'react';\nimport type { Control, RegisterOptions } from 'react-hook-form';\nimport { useController, useFormContext, useFormState } from 'react-hook-form';\nimport { Checkbox } from '../checkbox/checkbox.js';\nimport { registerFieldClearer } from './bound-field-registry.js';\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport type FieldRootProps = ComponentPropsWithRef<'div'> & {\n disabled?: boolean;\n invalid?: boolean;\n name?: string;\n};\n\nexport type FieldLabelProps = ComponentPropsWithRef<'label'>;\n\nexport type FieldControlProps = Omit<ComponentPropsWithRef<'input'>, 'children'>;\n\nexport type FieldDescriptionProps = ComponentPropsWithRef<'p'>;\n\nexport type FieldErrorProps = ComponentPropsWithRef<'div'> & {\n /**\n * Controls when the error is shown. `true` always shows it (for externally/caller-driven\n * validity, e.g. presence-only checks with no native constraint validation); a\n * `ValidityState` key shows it only when that specific native validity flag is set. Omit to\n * fall back to Base UI's default: shown whenever the field's tracked validity is invalid.\n * Note `match={false}` does not suppress the error — it falls back to that default\n * invalid-driven behavior; callers needing a boolean should use `invalid ? true : 'customError'`.\n * Only consulted on the unbound path — a bound `Field.Error` renders from the field contract's\n * name and react-hook-form's error state instead.\n */\n match?: boolean | keyof ValidityState;\n};\n\nexport type FieldValidityProps = {\n children: (state: FieldValidityState) => ReactNode;\n};\n\n/** One choice of a field whose value is picked from a list. Platform-owned; an author reorders the part, not the list. */\nexport type FieldOption = Readonly<{ value: string; label: string }>;\n\n/** A rule member constrained to `value`, carrying its own message — react-hook-form's own shape for\n * a constrained validation rule, declared here as our own type (Decision 5). */\nexport type FieldRuleConstraint<T> = Readonly<{ value: T; message: string }>;\n\n/** A field-level validator: reads the live value, returns `true` (valid), a message, or `false`. */\nexport type FieldValidator = (value: unknown) => boolean | string;\n\n/**\n * The fieldset's validation rule for this field, handed to `useBoundField` and mapped internally to\n * react-hook-form's `RegisterOptions` (`toRegisterOptions`) — nothing that imports this type from the\n * barrel ever names a react-hook-form type (RFC `Dependencies`).\n *\n * These seven members are a starting point measured against every rule this repository authors\n * today, not a closed contract: `RegisterOptions` carries more (`setValueAs`, `deps`, `valueAsNumber`,\n * …), and a member is added here the day a field actually needs it — this is not a deliberately closed\n * set to work around.\n */\nexport type FieldRule = Readonly<{\n required?: boolean | string | FieldRuleConstraint<boolean>;\n min?: number | string | FieldRuleConstraint<number | string>;\n max?: number | string | FieldRuleConstraint<number | string>;\n minLength?: number | FieldRuleConstraint<number>;\n maxLength?: number | FieldRuleConstraint<number>;\n pattern?: RegExp | FieldRuleConstraint<RegExp>;\n validate?: FieldValidator | Readonly<Record<string, FieldValidator>>;\n}>;\n\n/**\n * The one place `RegisterOptions` is named as a value type. `FieldRule`'s members are declared\n * independently of react-hook-form's (Decision 5), but each already carries react-hook-form's own\n * runtime shape (a bare value, or `{ value, message }`), so this is an assertion, not a per-member\n * transform. `field-rule.type-test.tsx` pins the boundary from the other side: it fails to compile if\n * `FieldRule` ever widens back to a bare `RegisterOptions` alias.\n */\nfunction toRegisterOptions(rule: FieldRule | undefined): RegisterOptions | undefined {\n return rule as RegisterOptions | undefined;\n}\n\n/**\n * A control's own bid to make its field required. A control raises; it never lowers — so `false` and\n * an absent raise are the same thing, no opinion, and a baseline that already says required survives\n * either. `message` is the copy the raise brings with it (`PhoneField`'s `errorRequired`), used only\n * when the raise is what carries the field to required.\n */\nexport type FieldRequirementRaise = Readonly<{\n required?: boolean;\n message?: string;\n}>;\n\n/**\n * One field's requiredness, resolved once and read everywhere: the announcement, the blank check and\n * the error copy all come from here, so they cannot disagree.\n */\nexport type ResolvedRequiredness = Readonly<{\n /** `baselineRequired || authorRaise`. Spread into `aria-required` as `required || undefined`, so a\n * field never announces `aria-required=\"false\"`. */\n required: boolean;\n /** What the blank check reports. Absent when neither side supplied copy. */\n message?: string;\n /** The rule to register with: the baseline, carrying the blank check when the field is required. */\n rule: FieldRule | undefined;\n}>;\n\n/**\n * react-hook-form's own reading of `required`: `.value` for the object form\n * (`{ value: boolean; message: string }`), plain truthiness otherwise (a boolean or a message\n * string). A message string is itself the copy, which is why the two are read together.\n */\nexport function readBaseline(rule: FieldRule | undefined): { required: boolean; message?: string } {\n const required = rule?.required;\n if (typeof required === 'object' && required !== null) {\n return { required: Boolean(required.value), message: required.value ? required.message : undefined };\n }\n if (typeof required === 'string') return { required: true, message: required };\n return { required: Boolean(required) };\n}\n\n/** A `validate` member is either one function or a record of named ones; downstream only wants the record. */\nfunction normaliseValidators(validate: FieldRule['validate']): Record<string, FieldValidator> {\n if (!validate) return {};\n return typeof validate === 'function' ? { validate } : { ...validate };\n}\n\n/**\n * Composes `validator` into `rule` under `name`, alongside whatever the rule already validates —\n * never instead of it. The two composites reach for this to keep their own format check while a\n * fieldset-supplied validator stays in force; before it, both discarded their own validation whole\n * the moment the fieldset supplied any.\n */\nexport function mergeFieldValidator(rule: FieldRule | undefined, name: string, validator: FieldValidator): FieldRule {\n return { ...rule, validate: { ...normaliseValidators(rule?.validate), [name]: validator } };\n}\n\n/**\n * The one resolution of `required = baselineRequired || authorRaise`. The fieldset's `FieldRule` is\n * the baseline; a control may raise a field to required and never lower one, so `raise.required`\n * only ever adds. Read-only deliberately plays no part: a field that is read-only and required stays\n * required, and an unsatisfiable form is the declared baseline's fault, not this function's.\n *\n * Baseline copy wins over the raise's, matching react-hook-form's own order — it evaluates `required`\n * before `validate`, so the fieldset's message was already the one a shopper saw whenever both existed.\n *\n * `required` catches an empty string but not a space-filled one (`inputValue === ''` is\n * react-hook-form's only emptiness test for a string), so the blank check goes in beside it. It is\n * installed last, so it cannot be displaced by a same-named validator from the rule.\n */\nexport function resolveRequiredness(rule: FieldRule | undefined, raise?: FieldRequirementRaise): ResolvedRequiredness {\n const baseline = readBaseline(rule);\n const required = baseline.required || Boolean(raise?.required);\n\n if (!required) return { required: false, rule };\n\n const message = baseline.message ?? (baseline.required ? undefined : raise?.message);\n const notBlank: FieldValidator = (value) => String(value ?? '').trim().length > 0 || message || false;\n\n return { required, message, rule: { ...mergeFieldValidator(rule, 'notBlank', notBlank), required: message ?? true } };\n}\n\n/**\n * One field's identity and its static facts: the argument list `useController` needs, hoisted out of\n * a prop-drill that composition made impossible. Not a mirror of state react-hook-form also holds —\n * it carries NO live value, so it never changes while the shopper types, and the parts subscribe to\n * react-hook-form for value, error, dirty and touched.\n */\nexport type FieldContract = Readonly<{\n /** Full, namespaced react-hook-form field name, e.g. `shippingAddress.street`. */\n name: string;\n /** Resolved label text: the Control's `label` prop, else the platform's default. */\n label?: string;\n placeholder?: string;\n /** Seed value, registered as react-hook-form's field-level `defaultValue`. */\n defaultValue?: unknown;\n rule?: FieldRule;\n /** Only for a field whose value is picked from a list. Unread until `Field.Select` is added. */\n options?: readonly FieldOption[];\n disabled?: boolean;\n readOnly?: boolean;\n autoComplete?: string;\n inputMode?: 'none' | 'text' | 'decimal' | 'numeric' | 'tel' | 'search' | 'email' | 'url';\n}>;\n\n/** Supplies the field contract to everything below it. Renders no DOM. Never catalogued. */\nexport type FieldBindingProps = FieldContract & {\n children?: ReactNode;\n};\n\n/**\n * Provided by a fieldset. Declares the one namespace every name registered through it belongs to, and\n * registers a full field name, returning its unregister.\n */\nexport type FieldsetFieldRegistry = {\n /**\n * The namespace this fieldset owns — the prefix of every full name its own controls register. Declared\n * by the provider, never inferred from a caller.\n */\n namespace: string;\n /**\n * Registers a full, namespaced field name and returns a release function. Refcounted by the provider,\n * so two callers holding one name produce one entry, released when the last of them releases. The\n * returned function releases at most once.\n */\n registerFieldName: (name: string) => () => void;\n};\n\n/** What `useFieldsetFieldNames` hands a fieldset: the value to provide, and the list to register. */\nexport type FieldsetFieldNames = Readonly<{\n /** Referentially stable while `namespace` is unchanged. Goes straight into the context provider. */\n registry: FieldsetFieldRegistry;\n /** Every full field name currently held, in first-registration order. Stable identity. */\n getFieldNames: () => readonly string[];\n}>;\n\n/** New bound part. Reads the contract, binds through useBoundField, drives Checkbox.Root. */\nexport type FieldCheckboxProps = {\n readonly className?: string;\n readonly children?: ReactNode;\n};\n\n// ---------------------------------------------------------------------------\n// Field contract context\n// ---------------------------------------------------------------------------\n\nconst FieldContext = createContext<FieldContract | null>(null);\n\n/** Reads the field contract. Returns null when no binding is above the caller. */\nexport function useField(): FieldContract | null {\n return useContext(FieldContext);\n}\n\n/**\n * Per (control, field name), the seed the *fieldset supplied* for that name — `contract.defaultValue`\n * as handed down, not the value any one part narrowed it to before calling `resetField`. The\n * distinction only shows up where the RFC says two parts may bind one name (\"A fieldset registers\n * once, collecting the field names its own children mount, refcounted so that a part remounting, or\n * two parts binding one name, never unregisters a name still in use\"): a text part and a checkbox\n * part over the same name narrow one seed to two different types, so a ledger holding the narrowed\n * value disagrees with itself on every render — neither part's entry ever satisfies the other's\n * `seedsEqual` check, and the two reset each other forever. Keyed on what the fieldset supplied, both\n * parts read the same entry, the first to run adopts and the rest skip. The second part's own\n * `useController` registration still carries its own narrowing, so it reads dirty against a baseline\n * it did not set — the honest outcome, and better than a hang.\n *\n * Keyed by the `control` object itself rather than by any `FieldBinding` component instance. Adoption is a\n * property of the field name within this form, not of whichever component instance happens to be\n * mounted right now: a fieldset's own conditional control (the `documentType` gate on\n * `ClientProfileDocumentControl`, a country-descriptor swap in `ShippingAddressFieldset`) unmounts and\n * remounts its `FieldBinding` with a brand-new instance that remembers nothing, while the shopper's\n * edit survives the unmount in `control._formValues` — nothing in this codebase sets\n * `shouldUnregister`. A `useRef` latch scoped to the instance cannot see that history and re-adopts the\n * stale seed over the surviving edit; this ledger can, because it outlives the instance. A `WeakMap` so\n * a form that itself unmounts (a fresh `useForm()` call, a fresh `control`) starts over empty, exactly\n * as it should.\n *\n * A `Map` of seeds, not a `Set` of names: recording only that a name was adopted cannot distinguish a\n * seed already in force from a different one that supersedes it, so a later `defaultValue` could never\n * reach the form — that was the whole bug this ledger replaces. `has(name)` is read separately from\n * `get(name)` everywhere below, so \"never adopted\" and \"adopted `undefined`\" stay distinguishable\n * states.\n */\nconst adoptedSeeds = new WeakMap<Control, Map<string, unknown>>();\n\nfunction adoptedSeedsFor(control: Control): Map<string, unknown> {\n let seeds = adoptedSeeds.get(control);\n if (!seeds) {\n seeds = new Map();\n adoptedSeeds.set(control, seeds);\n }\n return seeds;\n}\n\n/**\n * Structural rather than referential equality between two seeds, mirroring `valuesEqual` in\n * `form.tsx:128-130` and its documented reasoning: a field's seed is a primitive or plain record a\n * bound Control's contract carries (a string, a boolean, a small object) — never a function or a\n * circular structure — so a `JSON.stringify` comparison is a safe stand-in for a real deep-equal.\n * Duplicated rather than imported: `field.tsx` imports nothing from `form.tsx`, and the particles are\n * deliberately independent modules.\n *\n * `Object.is` is rejected here even though neither shipped shape's `normalise` (`TEXT_FIELD_VALUE`,\n * `BOOLEAN_FIELD_VALUE`) can hand this function anything but a primitive today — this guard is\n * defensive against a shape that does not exist yet, not against a reachable path. The day a shape's\n * `normalise` returns a record or an array, a fieldset that hands out a freshly-allocated seed with\n * the same content on every render would fail `Object.is` against itself, and the\n * resetField → formState → render → resetField chain below would never terminate (measured: swapping\n * in `Object.is` hung the suite past 150 seconds, until a render ceiling was added that turns the same\n * mutation into a named failure in seconds). Do not remove this as dead code on the\n * grounds that no object-shaped seed reaches it today — the regression test that pins it is\n * `field.test.tsx`'s `'terminates when the seed is a fresh, structurally identical object literal on\n * every render'`, which exercises `useSeedAdoption` through a test-only shape precisely because no\n * shipped shape can.\n */\nfunction seedsEqual(a: unknown, b: unknown): boolean {\n return a === b || JSON.stringify(a) === JSON.stringify(b);\n}\n\n/**\n * react-hook-form copies a per-field `defaultValue` into control._defaultValues only when\n * shouldUnregister is set, which nothing here sets — so without this, the pristine comparison for a\n * seeded field runs against `undefined` and any touched field stays dirty for good. Adopted once per\n * (control, name) for a given seed, tracked in `adoptedSeeds`, which survives this component instance\n * unmounting and remounting — so a remount with the same stale seed prop never overwrites a shopper's\n * live edit, and a later, *different* seed reaches a field the shopper has not touched without the\n * shopper having to reload the page.\n *\n * Called from `useBoundField`, not from `FieldBinding` (relocated, T57): the caller is now the very\n * part whose own `useController` call registers `name`. `useController` calls `control.register(name,\n * …)` from a `useRef` initialiser during the first render, and `register` adds to `_names.mount`\n * synchronously — before that render returns, let alone before this effect's first run. So the mount\n * guard below is satisfied on this effect's very first run for the caller's own name; the ordering\n * `FieldBinding` could not rely on (a descendant control mounting on a *later* commit than the binding\n * around it) no longer arises, because binder and adopter are now one and the same component instance.\n * The T-5 test (`field.test.tsx`, a control that shows up on a later render than its enclosing\n * `FieldBinding`) still passes — not because that race is still possible, but because nothing here\n * special-cases it away: the guard simply reads true the one time it is asked.\n *\n * No dependency array all the same: this effect must still re-run on every render, because the guards\n * below can otherwise miss their only remaining chance — the dirty-then-clean case. A seed arriving\n * while the field is dirty is skipped by the third guard and never recorded; the only later signal that\n * it can now be adopted is the field itself turning clean, which changes neither `name` nor the\n * normalised seed this hook receives, so a dependency array keyed to its own arguments would never\n * re-invoke it. Re-running on every render costs nothing extra here: every guard below but the\n * adopting one is a no-op, and useFormContext() already forces a re-render on every formState emission\n * regardless (Invariant 4 is violated in letter, not by this effect), so this adds no new re-render of\n * its own.\n *\n * The first guard below is what does termination, not just correctness: react-hook-form rebuilds\n * formState on every emission and FormProvider memoises its context value with formState in its own\n * dependency list, so useFormContext()'s return value changes identity after every resetField call.\n * Without that guard, that identity change would drive another render, which would call resetField\n * again, which would change formState again — an unbounded loop. Its early return breaks it: once the\n * ledger's recorded seed for this name is equal (by `seedsEqual`) to the current `defaultValue`, every\n * further run for that name exits before reaching resetField — which is also why it must be the first\n * check after the ledger lookup, ahead of even the mount guard.\n *\n * Precondition this re-seed relies on and does not itself enforce: `defaultValue` regressing to a\n * reactive fragment's incomplete-read fallback once the field has already adopted real data.\n * Probing this against `useReactiveEntity`'s fallback expression (`use-reactive-entity.ts:57`), mounting\n * a clean field over real, complete data and then trying to make `fragmentResult.complete` flip back to\n * false: measured as **reachable**, not merely suspected. Two of the three probes reproduced it —\n * evicting a field the fragment selects (`cache.evict({ id, fieldName })`, then `cache.gc()`) and\n * moving the root query's entity id to one never written to the cache (a session or orderForm change)\n * both regress the read, and this effect cannot tell that apart from a genuine, saved change: the\n * second probe blanked a clean control from \"Ada\" to \"\", and an independent audit reproduced it.\n * `client.clearStore()` on a still-mounted, still-referenced entity did not reproduce it. The\n * fieldsets close that gap above this hook: they withhold their controls until `useEntityDataResolved`\n * reports their entity resolved, so a regressed read never reaches this effect through a fieldset.\n */\nfunction useSeedAdoption(name: string, suppliedSeed: unknown, narrowedSeed: unknown): void {\n const form = useFormContext();\n\n useEffect(() => {\n if (!form) return;\n const seeds = adoptedSeedsFor(form.control);\n if (seeds.has(name) && seedsEqual(seeds.get(name), suppliedSeed)) return;\n if (!form.control._names.mount.has(name)) return;\n if (seeds.has(name) && form.getFieldState(name).isDirty) return;\n form.resetField(name, { defaultValue: narrowedSeed });\n seeds.set(name, suppliedSeed);\n });\n}\n\nexport function FieldBinding({\n name,\n label,\n placeholder,\n defaultValue,\n rule,\n options,\n disabled,\n readOnly,\n autoComplete,\n inputMode,\n children,\n}: FieldBindingProps): ReactNode {\n // No live value in the contract (Decision 1), so it is stable across a whole editing session as\n // long as the caller's own props are stable — memoised here so an unrelated re-render of whatever\n // renders this binding never produces a new object identity for subscribers.\n const contract = useMemo<FieldContract>(\n () => ({ name, label, placeholder, defaultValue, rule, options, disabled, readOnly, autoComplete, inputMode }),\n [name, label, placeholder, defaultValue, rule, options, disabled, readOnly, autoComplete, inputMode],\n );\n\n return <FieldContext.Provider value={contract}>{children}</FieldContext.Provider>;\n}\n\n// ---------------------------------------------------------------------------\n// Bound part support — the empty value and seed narrowing a part owns (T57)\n// ---------------------------------------------------------------------------\n\n/**\n * What a bound part knows about its own value that `FieldBinding` does not (RFC `The rest of the\n * parts`): the value it holds when it holds nothing, and how to narrow an arbitrary seed to that\n * type. Each shape is the pair together, not two loose values, so a part cannot pair a boolean\n * `empty` with a string `normalise`.\n */\nexport type FieldValueShape<T> = Readonly<{\n /** The value `useBoundField`'s registered clearer writes — this field's own type, not `''` by default. */\n empty: T;\n /** Narrows an arbitrary seed (`contract.defaultValue`) to this part's own value type. */\n normalise: (seed: unknown) => T;\n}>;\n\n/** A text-shaped bound field: empty is `''`, and a non-string seed narrows to `''`. */\nexport const TEXT_FIELD_VALUE: FieldValueShape<string> = {\n empty: '',\n normalise: (seed) => (typeof seed === 'string' ? seed : ''),\n};\n\n/** A boolean-shaped bound field: empty is `false`, and any seed narrows through `Boolean(...)`. */\nexport const BOOLEAN_FIELD_VALUE: FieldValueShape<boolean> = {\n empty: false,\n normalise: (seed) => Boolean(seed),\n};\n\n/**\n * What `useBoundField` hands back: react-hook-form's own `field` object, reshaped into a type this\n * repository declares (Decision 6) — returning `useController`'s own `ControllerRenderProps` would put\n * a react-hook-form type back on the barrel the moment this hook is exported. `value: T`, not\n * `unknown`, because `BoundFieldControl` passes `field.value` straight into `BaseField.Control`, which\n * an `unknown` would reject. `ref` is a plain `RefCallback<HTMLElement>`, assignable to both\n * `Ref<HTMLInputElement>` and `Ref<HTMLButtonElement>`, so every existing `ref={field.ref}` site and\n * `useComposedRef` call keep compiling unchanged.\n */\nexport type BoundField<T> = Readonly<{\n name: string;\n value: T;\n onChange: (...event: unknown[]) => void;\n onBlur: () => void;\n ref: RefCallback<HTMLElement>;\n /** The resolved requiredness, so the control that renders the input announces the same value the\n * registered rule enforces. Spread as `required || undefined` — a field never announces `false`. */\n required: boolean;\n}>;\n\n/**\n * The only caller of `useController` in this package (I-1). Registers the field with react-hook-form,\n * adopts the same normalised seed as the dirty baseline (Decision 1), and registers a clearer that\n * writes the shape's own empty value for as long as the part stays mounted (FR-5) — three facts a\n * bound part previously scattered across its own call site, `FieldBinding` and `FormClear`.\n *\n * The effective rule is `rule ?? contract.rule`: a part with no rule of its own (`BoundFieldControl`,\n * `BoundFieldCheckbox`, `BoundFieldSelectRoot`) falls back to the contract's; `BoundPhoneField` and\n * `BoundDocumentField` pass their own `useMemo`-derived rule, which already merges the particle's\n * format check into `contract.rule` rather than replacing it.\n *\n * Requiredness resolves here rather than at each call site (FR: one chokepoint), so no bound control\n * can register a rule whose enforcement and announcement disagree, or forget the blank check. A\n * control that wants its field required passes a `raise`; only `FieldSelectTrigger`, which announces\n * from a different component than the one that binds, calls `resolveRequiredness` for itself.\n */\nexport function useBoundField<T>(\n contract: FieldContract,\n shape: FieldValueShape<T>,\n rule?: FieldRule,\n raise?: FieldRequirementRaise,\n): BoundField<T> {\n const { control, setValue } = useFormContext();\n // Memoised on the raise's two scalars, not on the raise object: callers build it inline, so a\n // fresh identity every render would hand `useController` a fresh `rules` object every render.\n const raiseRequired = raise?.required;\n const raiseMessage = raise?.message;\n const requiredness = useMemo(\n () => resolveRequiredness(rule ?? contract.rule, { required: raiseRequired, message: raiseMessage }),\n [rule, contract.rule, raiseRequired, raiseMessage],\n );\n // One named local, referenced by both useController's defaultValue and the adoption call below\n // (AC-3) — registration and the dirty baseline must never drift apart by reading two independent\n // normalise() calls.\n const seed = shape.normalise(contract.defaultValue);\n\n const { field } = useController({\n name: contract.name,\n rules: toRegisterOptions(requiredness.rule),\n defaultValue: seed,\n });\n\n useSeedAdoption(contract.name, contract.defaultValue, seed);\n\n useEffect(\n () =>\n registerFieldClearer(control, contract.name, () => setValue(contract.name, shape.empty, { shouldDirty: true })),\n [control, contract.name, setValue, shape.empty],\n );\n\n return { ...field, required: requiredness.required };\n}\n\n// ---------------------------------------------------------------------------\n// Fieldset registration seam (provider ships from a later fieldset task)\n// ---------------------------------------------------------------------------\n\n/** Exported so those tasks import it rather than redefine it. */\nexport const FieldsetFieldContext = createContext<FieldsetFieldRegistry | null>(null);\n\n/** Unchanged: the registry above the caller, or null. Now carrying a namespace. */\nexport function useFieldsetContext(): FieldsetFieldRegistry | null {\n return useContext(FieldsetFieldContext);\n}\n\n/**\n * Registers `${namespace}.${key}` with the enclosing fieldset while the caller is mounted, and returns\n * that full name. Throws during render when a registry is above the caller and declares a different\n * namespace. Registers nothing and throws nothing when no registry is above. Holds no state: the\n * refcount belongs to the provider.\n */\nexport function useFieldsetField(namespace: string, key: string): string {\n const registry = useFieldsetContext();\n const name = `${namespace}.${key}`;\n\n if (registry && registry.namespace !== namespace) {\n throw new Error(\n `A control asked to register \"${name}\", but the fieldset registry above it owns the namespace \"${registry.namespace}\". A registry serves exactly one namespace, declared by the fieldset that provides it.`,\n );\n }\n\n useEffect(() => {\n if (!registry) return;\n return registry.registerFieldName(name);\n }, [registry, name]);\n\n return name;\n}\n\n/**\n * The field-name bookkeeping a fieldset provides. Refcounted, so two callers of one name collapse to one\n * entry however they arrived — including a caller that reaches `registerFieldName` directly.\n */\nexport function useFieldsetFieldNames(namespace: string): FieldsetFieldNames {\n // Own state, not module state: this ref lives on the fieldset's own component instance, so it\n // survives every re-render regardless of whether the caller memoises the registry it hands out.\n const counts = useRef(new Map<string, number>());\n\n const registerFieldName = useCallback((name: string) => {\n counts.current.set(name, (counts.current.get(name) ?? 0) + 1);\n\n // One-shot: a counted Map, unlike the Set it replaces, is not idempotent on release. Without this\n // latch a double-invoked unregister (StrictMode, or a defensive caller) would decrement past a\n // second live holder and evict a name it still holds.\n let released = false;\n return () => {\n if (released) return;\n released = true;\n\n const remaining = (counts.current.get(name) ?? 0) - 1;\n if (remaining > 0) {\n counts.current.set(name, remaining);\n } else {\n counts.current.delete(name);\n }\n };\n }, []);\n\n const getFieldNames = useCallback((): readonly string[] => Array.from(counts.current.keys()), []);\n\n // Memoised on namespace: a fieldset that re-points at a different namespace produces a new registry\n // object, so every mounted control re-registers under the new namespace in the same commit.\n const registry = useMemo<FieldsetFieldRegistry>(\n () => ({ namespace, registerFieldName }),\n [namespace, registerFieldName],\n );\n\n return { registry, getFieldNames };\n}\n\n/**\n * Throws during render when an ancestor fieldset is already providing field registration for this\n * subtree. Every fieldset calls it in one line.\n */\nexport function useNoAncestorFieldset(componentName: string): void {\n const ancestor = useFieldsetContext();\n\n if (ancestor) {\n throw new Error(\n `${componentName} cannot be nested inside another fieldset. An ancestor fieldset owning the namespace \"${ancestor.namespace}\" already provides field registration for this subtree, so this fieldset's controls would register under that namespace instead of their own. Compose fieldsets as siblings under the same FormRoot instead of nesting them.`,\n );\n }\n}\n\n// ---------------------------------------------------------------------------\n// Definitions\n// ---------------------------------------------------------------------------\n\nexport const FieldRootDefinition = defineComponent<FieldRootProps>({\n name: 'FieldRoot',\n importPath: '@vtex/faststore/components',\n component: FieldRoot,\n providesContext: 'field',\n editor: { label: 'Field Root', category: 'layout' },\n schema: {\n disabled: { type: 'boolean', default: false, label: 'Disabled' },\n },\n guideline:\n 'Form field container that connects a label, input, and validation messages via ARIA. Use Field.Control as the input element, or place a compatible Base UI input (Input, Checkbox, Select) as a direct child. Set invalid=true to trigger Field.Error display. Wrapping a bound Control feeds invalid/dirty/touched from react-hook-form automatically.',\n});\n\nexport const FieldLabelDefinition = defineComponent<FieldLabelProps>({\n name: 'FieldLabel',\n importPath: '@vtex/faststore/components',\n component: FieldLabel,\n requiresContext: 'field',\n editor: { label: 'Field Label', category: 'layout' },\n schema: {},\n guideline:\n \"Accessible label inside Field.Root. Automatically associated with Field.Control's input via ARIA. Place before Field.Control in the DOM. Under a bound Control, a childless Field.Label renders the field contract's label.\",\n});\n\nexport const FieldControlDefinition = defineComponent<FieldControlProps>({\n name: 'FieldControl',\n importPath: '@vtex/faststore/components',\n component: FieldControl,\n requiresContext: 'field',\n editor: { label: 'Field Control', category: 'layout' },\n schema: {},\n guideline:\n \"Renders a native `<input>` element directly. It IS the input — do not place any child inside it. Pass input attributes (type, placeholder, required, etc.) directly as props. It is automatically linked to Field.Label and Field.Error via ARIA. Under a bound Control, it registers with react-hook-form using the field contract's name, seed value, and validation rule.\",\n});\n\nexport const FieldDescriptionDefinition = defineComponent<FieldDescriptionProps>({\n name: 'FieldDescription',\n importPath: '@vtex/faststore/components',\n component: FieldDescription,\n requiresContext: 'field',\n editor: { label: 'Field Description', category: 'layout' },\n schema: {},\n guideline:\n 'Helper text inside Field.Root. Automatically linked to the control via aria-describedby. Always visible regardless of validation state.',\n});\n\nexport const FieldErrorDefinition = defineComponent<FieldErrorProps>({\n name: 'FieldError',\n importPath: '@vtex/faststore/components',\n component: FieldError,\n requiresContext: 'field',\n editor: { label: 'Field Error', category: 'layout' },\n schema: {},\n guideline:\n \"Validation error message inside Field.Root. Shown when the field is in an invalid state. Linked to the control via aria-describedby. Under a bound Control, it renders react-hook-form's error message for the field contract's name.\",\n});\n\nexport const FieldValidityDefinition = defineComponent<FieldValidityProps>({\n name: 'FieldValidity',\n importPath: '@vtex/faststore/components',\n component: FieldValidity,\n requiresContext: 'field',\n editor: { label: 'Field Validity', category: 'layout' },\n schema: {},\n guideline:\n \"Validation feedback slot using a render prop for custom messages derived from the field's validity state. Use for dynamic messages when Field.Error text is not fixed.\",\n});\n\nexport const FieldCheckboxDefinition = defineComponent<FieldCheckboxProps>({\n name: 'FieldCheckbox',\n importPath: '@vtex/faststore/components',\n component: FieldCheckbox,\n requiresContext: 'field',\n editor: { label: 'Field Checkbox', category: 'action' },\n schema: {},\n guideline:\n 'Bound boolean control over the shipped Checkbox particle: reads the enclosing field contract and drives Checkbox.Root through react-hook-form. Compose Checkbox.Indicator as a child for the visual check mark. Place inside Field.Root, the only ancestor that provides the field context it requires today — a bound Control will satisfy it too, once one ships.',\n});\n\n// ---------------------------------------------------------------------------\n// Parts\n// ---------------------------------------------------------------------------\n\nexport function FieldRoot({ disabled, invalid, name, className, children, ...props }: FieldRootProps) {\n const contract = useField();\n const formContext = useFormContext();\n\n if (contract && formContext) {\n return (\n <BoundFieldRoot\n {...props}\n contract={contract}\n disabled={disabled}\n invalid={invalid}\n name={name}\n className={className}\n >\n {children}\n </BoundFieldRoot>\n );\n }\n\n return (\n <BaseField.Root\n {...props}\n disabled={disabled}\n invalid={invalid}\n name={name}\n className={className}\n data-fs-field-root=\"\"\n >\n {children}\n </BaseField.Root>\n );\n}\n\ntype BoundFieldRootProps = FieldRootProps & { contract: FieldContract };\n\nfunction BoundFieldRoot({ contract, disabled, invalid, name, className, children, ...props }: BoundFieldRootProps) {\n const { getFieldState, control } = useFormContext();\n const {\n invalid: rhfInvalid,\n isDirty,\n isTouched,\n } = getFieldState(contract.name, useFormState({ name: contract.name }));\n\n // Base UI's `FieldRoot` computes `dirty = dirtyProp ?? dirtyState`, and `setDirty` returns\n // immediately once `dirtyProp !== undefined` (`FieldRoot.js:50-67`) — so passing `false`\n // unconditionally disables Base UI's own fallback tracking exactly as hard as passing `true` did.\n // That fallback is still needed for any name react-hook-form never registers (a raw Base UI control\n // under a bound `Field.Root`), so the prop is three-state: `undefined` while the name is unmounted,\n // then react-hook-form's own value once it registers — which always wins from then on, in both\n // directions, closing the freeze-at-true latch the unconditional form reopened.\n const isRegistered = control._names.mount.has(contract.name);\n\n return (\n <BaseField.Root\n {...props}\n name={name ?? contract.name}\n disabled={disabled ?? contract.disabled}\n invalid={invalid ?? rhfInvalid}\n dirty={isRegistered ? isDirty : undefined}\n touched={isRegistered ? isTouched : undefined}\n className={className}\n data-fs-field-root=\"\"\n >\n {children}\n </BaseField.Root>\n );\n}\n\nexport function FieldLabel({ className, children, ...props }: FieldLabelProps) {\n const contract = useField();\n const formContext = useFormContext();\n\n if (contract && formContext) {\n return (\n <BoundFieldLabel {...props} contract={contract} className={className}>\n {children}\n </BoundFieldLabel>\n );\n }\n\n return (\n <BaseField.Label {...props} className={className} data-fs-field-label=\"\">\n {children}\n </BaseField.Label>\n );\n}\n\ntype BoundFieldLabelProps = FieldLabelProps & { contract: FieldContract };\n\nfunction BoundFieldLabel({ contract, className, children, ...props }: BoundFieldLabelProps) {\n return (\n <BaseField.Label {...props} className={className} data-fs-field-label=\"\">\n {children ?? contract.label}\n </BaseField.Label>\n );\n}\n\nexport function FieldControl({ className, ...props }: FieldControlProps) {\n const contract = useField();\n const formContext = useFormContext();\n\n if (contract && formContext) {\n return <BoundFieldControl {...props} contract={contract} className={className} />;\n }\n\n return <BaseField.Control {...props} className={className} data-fs-field-control=\"\" />;\n}\n\ntype BoundFieldControlProps = FieldControlProps & { contract: FieldContract };\n\nfunction BoundFieldControl({\n contract,\n placeholder,\n disabled,\n readOnly,\n autoComplete,\n inputMode,\n className,\n ...props\n}: BoundFieldControlProps) {\n const field = useBoundField(contract, TEXT_FIELD_VALUE);\n\n return (\n <BaseField.Control\n {...props}\n name={field.name}\n value={field.value}\n onChange={field.onChange}\n onBlur={field.onBlur}\n ref={field.ref}\n placeholder={placeholder ?? contract.placeholder}\n disabled={disabled ?? contract.disabled}\n readOnly={readOnly ?? contract.readOnly}\n autoComplete={autoComplete ?? contract.autoComplete}\n inputMode={inputMode ?? contract.inputMode}\n aria-required={field.required || undefined}\n className={className}\n data-fs-field-control=\"\"\n />\n );\n}\n\nexport function FieldDescription({ className, children, ...props }: FieldDescriptionProps) {\n return (\n <BaseField.Description {...props} className={className} data-fs-field-description=\"\">\n {children}\n </BaseField.Description>\n );\n}\n\nexport function FieldError({ className, children, ...props }: FieldErrorProps) {\n const contract = useField();\n const formContext = useFormContext();\n\n if (contract && formContext) {\n return (\n <BoundFieldError {...props} contract={contract} className={className}>\n {children}\n </BoundFieldError>\n );\n }\n\n return (\n <BaseField.Error {...props} className={className} data-fs-field-error=\"\">\n {children}\n </BaseField.Error>\n );\n}\n\ntype BoundFieldErrorProps = FieldErrorProps & { contract: FieldContract };\n\n// `match` is not destructured for use — its documented meaning applies to the unbound path only\n// (Decision: bound visibility is driven by react-hook-form's own error for the contract's name).\nfunction BoundFieldError({ contract, className, children, match: _match, ...props }: BoundFieldErrorProps) {\n const { getFieldState } = useFormContext();\n const { error } = getFieldState(contract.name, useFormState({ name: contract.name }));\n\n if (!error) return null;\n // A `validate` that reports failure with `false` rather than a string (e.g. DocumentField's\n // check-digit validator when no `errorMessage` was authored) leaves react-hook-form's own\n // `error.message` empty. Rendering in that case would put an empty `<div data-fs-field-error>` on\n // the page where the field's own default (unbound) anatomy renders nothing at all — a divergence\n // between the composed and default anatomies (finding F4). An author-supplied `children` still\n // always renders regardless, since that text does not depend on react-hook-form's message.\n if (!children && !error.message) return null;\n\n return (\n // `match` forces Base UI past its own native-constraint-validation gate: with no real HTML5\n // constraint on the input, Field.Error's default (no `match`) visibility check reads native\n // validity, not react-hook-form's. We have already decided to render from react-hook-form's\n // error above, so `match` here only lets that decision take effect — it does not affect the\n // message, which is our own `children` below and always wins over Base UI's own computed text.\n <BaseField.Error {...props} match className={className} data-fs-field-error=\"\">\n {children ?? error.message}\n </BaseField.Error>\n );\n}\n\nexport function FieldValidity({ children }: FieldValidityProps) {\n return <BaseField.Validity>{children}</BaseField.Validity>;\n}\n\nexport function FieldCheckbox({ className, children }: FieldCheckboxProps): ReactNode {\n const contract = useField();\n const formContext = useFormContext();\n\n if (contract && formContext) {\n return (\n <BoundFieldCheckbox contract={contract} className={className}>\n {children}\n </BoundFieldCheckbox>\n );\n }\n\n return <Checkbox.Root className={className}>{children}</Checkbox.Root>;\n}\n\ntype BoundFieldCheckboxProps = FieldCheckboxProps & { contract: FieldContract };\n\nfunction BoundFieldCheckbox({ contract, className, children }: BoundFieldCheckboxProps) {\n const field = useBoundField(contract, BOOLEAN_FIELD_VALUE);\n\n return (\n <Checkbox.Root\n checked={field.value}\n onCheckedChange={field.onChange}\n onBlur={field.onBlur}\n ref={field.ref}\n disabled={contract.disabled}\n className={className}\n data-fs-field-checkbox=\"\"\n >\n {children}\n </Checkbox.Root>\n );\n}\n\nexport const Field = {\n Root: FieldRoot,\n Label: FieldLabel,\n Control: FieldControl,\n Description: FieldDescription,\n Error: FieldError,\n Validity: FieldValidity,\n Checkbox: FieldCheckbox,\n};\n"],"mappings":"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"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { ContentValue } from "../../core/dist/define-content-type.js";
|
|
2
2
|
import { ComponentDefinition } from "../../core/dist/define-component.js";
|
|
3
3
|
import "../../core/dist/index.js";
|
|
4
|
+
import { PageRef } from "../../sdk/navigation/types.js";
|
|
4
5
|
import { ComponentPropsWithRef } from "react";
|
|
5
6
|
//#region src/particles/footer-link-group/footer-link-group.d.ts
|
|
6
7
|
export declare const FooterLinkGroupContent: {
|
|
@@ -50,8 +51,11 @@ export declare const FooterLinkGroupContent: {
|
|
|
50
51
|
};
|
|
51
52
|
};
|
|
52
53
|
type FooterLinkGroupContentValue = ContentValue<typeof FooterLinkGroupContent>;
|
|
53
|
-
|
|
54
|
-
|
|
54
|
+
type FooterLinkGroupContentLink = NonNullable<FooterLinkGroupContentValue['links']>[number];
|
|
55
|
+
/** One authored link. The prop accepts a page reference; a CMS href stays a string. */
|
|
56
|
+
export type FooterLinkGroupLink = Omit<FooterLinkGroupContentLink, 'href'> & {
|
|
57
|
+
href: PageRef | string;
|
|
58
|
+
};
|
|
55
59
|
export type FooterLinkGroupProps = ComponentPropsWithRef<'div'> & {
|
|
56
60
|
/** Authored fallback for the group heading, used when no CMS entry supplies one. */
|
|
57
61
|
heading?: string;
|
|
@@ -73,6 +73,22 @@ const FooterLinkGroupDefinition = defineComponent({
|
|
|
73
73
|
},
|
|
74
74
|
guideline: "One heading and its institutional links, both CMS-editable. Renders as a desktop column and a mobile accordion at the same time; the store's swatch pair decides which one a breakpoint shows. See footer-link-group.md."
|
|
75
75
|
});
|
|
76
|
+
function readLinks(content, authored) {
|
|
77
|
+
if (content !== void 0) return content;
|
|
78
|
+
return authored ?? [];
|
|
79
|
+
}
|
|
80
|
+
function resolveDestination(href, index) {
|
|
81
|
+
if (typeof href !== "string") return href;
|
|
82
|
+
const destination = resolveSafeHref(href);
|
|
83
|
+
if (!destination.safe) {
|
|
84
|
+
console.warn("[FooterLinkGroup] link destination rejected", {
|
|
85
|
+
index,
|
|
86
|
+
reason: destination.reason
|
|
87
|
+
});
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
return destination.href;
|
|
91
|
+
}
|
|
76
92
|
/** Unique within its own root, and every group renders its own root. */
|
|
77
93
|
const ACCORDION_ITEM_VALUE = "links";
|
|
78
94
|
/**
|
|
@@ -83,14 +99,9 @@ function resolveLinks(links) {
|
|
|
83
99
|
const resolved = [];
|
|
84
100
|
links.forEach((link, index) => {
|
|
85
101
|
if (link.label.trim().length === 0) return;
|
|
86
|
-
const destination = resolveSafeHref(link.href);
|
|
87
|
-
if (!destination.safe) console.warn("[FooterLinkGroup] link destination rejected", {
|
|
88
|
-
index,
|
|
89
|
-
reason: destination.reason
|
|
90
|
-
});
|
|
91
102
|
resolved.push({
|
|
92
103
|
label: link.label,
|
|
93
|
-
href:
|
|
104
|
+
href: resolveDestination(link.href, index),
|
|
94
105
|
target: link.target
|
|
95
106
|
});
|
|
96
107
|
});
|
|
@@ -98,7 +109,7 @@ function resolveLinks(links) {
|
|
|
98
109
|
}
|
|
99
110
|
function FooterLinkGroup({ heading, links, $content, className, ...props }) {
|
|
100
111
|
const groupHeading = $content?.heading ?? heading;
|
|
101
|
-
const resolved = resolveLinks($content?.links
|
|
112
|
+
const resolved = resolveLinks(readLinks($content?.links, links));
|
|
102
113
|
if (!groupHeading?.trim() || resolved.length === 0) return null;
|
|
103
114
|
const list = /* @__PURE__ */ jsx("ul", {
|
|
104
115
|
"data-fs-footer-link-group-list": "",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"footer-link-group.js","names":[],"sources":["../../../src/particles/footer-link-group/footer-link-group.tsx"],"sourcesContent":["import { type ContentValue, defineComponent, defineContentType } from '@vtex/fs-core';\nimport type { ComponentPropsWithRef } from 'react';\nimport { resolveSafeHref } from '../../utils/safe-href.js';\nimport {\n AccordionHeader,\n AccordionItem,\n AccordionPanel,\n AccordionRoot,\n AccordionTrigger,\n} from '../accordion/accordion.js';\nimport { NavigationLink } from '../navigation-link/navigation-link.js';\n\n// ---------------------------------------------------------------------------\n// Content model\n// ---------------------------------------------------------------------------\n\nexport const FooterLinkGroupContent = defineContentType({\n id: 'footer-link-group',\n title: 'Footer Link Group',\n description: 'A heading and its ordered list of institutional links.',\n schema: {\n type: 'object',\n additionalProperties: false,\n // Neither property is required. A missing element key already omits the component before any\n // required check runs, so requiring them would buy nothing and cost the prop fallback: an entry\n // authoring one field alone would be shape-invalid and take the whole element down with it. The\n // render gate holds the heading invariant instead, being the only point that sees both channels.\n properties: {\n heading: { type: 'string', minLength: 1, title: 'Group heading' },\n links: {\n type: 'array',\n title: 'Links',\n minItems: 1,\n items: {\n type: 'object',\n additionalProperties: false,\n required: ['label', 'href'],\n properties: {\n label: { type: 'string', minLength: 1, title: 'Label' },\n href: {\n type: 'string',\n minLength: 1,\n title: 'URL',\n format: 'uri-reference',\n // Author-time guidance only, and deliberately lookahead-free: nothing in this repo\n // compiles it, so its one consumer is the Content Platform Admin's form, whose regex\n // engine may be an RE2 family that rejects lookahead outright.\n pattern: '^(?:https?://.*|mailto:.+|tel:.+|/(?:[^/].*)?)$',\n },\n target: { type: 'string', enum: ['_self', '_blank'], default: '_self', title: 'Open in' },\n },\n },\n },\n },\n },\n});\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\ntype FooterLinkGroupContentValue = ContentValue<typeof FooterLinkGroupContent>;\n\n/** One authored link. Derived from the model so the prop and content channels cannot drift. */\nexport type FooterLinkGroupLink = NonNullable<FooterLinkGroupContentValue['links']>[number];\n\nexport type FooterLinkGroupProps = ComponentPropsWithRef<'div'> & {\n /** Authored fallback for the group heading, used when no CMS entry supplies one. */\n heading?: string;\n /** Authored fallback for the link list, used when no CMS entry supplies one. */\n links?: readonly FooterLinkGroupLink[];\n /** Injected by the compiler, sliced from the page's or layout's composite content entry. */\n $content?: FooterLinkGroupContentValue;\n};\n\n// ---------------------------------------------------------------------------\n// Definition\n// ---------------------------------------------------------------------------\n\nexport const FooterLinkGroupDefinition = defineComponent({\n name: 'FooterLinkGroup',\n importPath: '@vtex/faststore/components',\n component: FooterLinkGroup,\n content: FooterLinkGroupContent,\n editor: { label: 'Footer Link Group', category: 'navigation' },\n schema: {\n heading: { type: 'string', label: 'Group heading' },\n links: { type: 'array', label: 'Links' },\n },\n guideline:\n \"One heading and its institutional links, both CMS-editable. Renders as a desktop column and a mobile accordion at the same time; the store's swatch pair decides which one a breakpoint shows. See footer-link-group.md.\",\n});\n\n// ---------------------------------------------------------------------------\n// Implementation\n// ---------------------------------------------------------------------------\n\n/** A resolved link, ready for both forms. A null `href` is a destination the allowlist refused. */\ntype ResolvedLink = {\n label: string;\n href: string | null;\n target: FooterLinkGroupLink['target'];\n};\n\n/** Unique within its own root, and every group renders its own root. */\nconst ACCORDION_ITEM_VALUE = 'links';\n\n/**\n * Resolves the whole list once per render, so both forms show the same links and a refused\n * destination logs one line rather than two.\n */\nfunction resolveLinks(links: readonly FooterLinkGroupLink[]): ResolvedLink[] {\n const resolved: ResolvedLink[] = [];\n\n links.forEach((link, index) => {\n // `minLength` is enforced by neither the runtime slice nor the preflight, so a blank label\n // reaches the component. Drop the link: an anchor with no accessible name is a WCAG failure.\n if (link.label.trim().length === 0) {\n return;\n }\n const destination = resolveSafeHref(link.href);\n if (!destination.safe) {\n // A bounded reason and no authored value, the convention the content runtime's own log follows.\n console.warn('[FooterLinkGroup] link destination rejected', { index, reason: destination.reason });\n }\n resolved.push({\n label: link.label,\n href: destination.safe ? destination.href : null,\n target: link.target,\n });\n });\n\n return resolved;\n}\n\nexport function FooterLinkGroup({ heading, links, $content, className, ...props }: FooterLinkGroupProps) {\n const groupHeading = $content?.heading ?? heading;\n // An entry supplying `links: []` beats an authored prop: a merchant who cleared the list cleared it.\n const resolved = resolveLinks($content?.links ?? links ?? []);\n\n if (!groupHeading?.trim() || resolved.length === 0) {\n return null;\n }\n\n // One element rendered in both forms, so the two cannot drift.\n const list = (\n <ul data-fs-footer-link-group-list=\"\">\n {resolved.map((link, index) => (\n <li key={index} data-fs-footer-link-group-item=\"\">\n {link.href === null ? (\n <span data-fs-text=\"\" data-fs-footer-link-group-text=\"\">\n {link.label}\n </span>\n ) : (\n <NavigationLink href={link.href} target={link.target} data-fs-footer-link-group-link=\"\">\n {link.label}\n </NavigationLink>\n )}\n </li>\n ))}\n </ul>\n );\n\n return (\n <div {...props} className={className} data-fs-footer-link-group=\"\">\n <AccordionRoot data-fs-footer-link-group-accordion=\"\">\n <AccordionItem value={ACCORDION_ITEM_VALUE}>\n <AccordionHeader data-fs-footer-link-group-heading=\"\">\n <AccordionTrigger data-fs-text=\"\" data-fs-footer-link-group-trigger=\"\">\n {groupHeading}\n </AccordionTrigger>\n </AccordionHeader>\n {/* `hiddenUntilFound` requires the panel to stay mounted while closed, and the wrapper\n defaults `keepMounted` to false, so passing it keeps Base UI from warning about the pair. */}\n <AccordionPanel hiddenUntilFound keepMounted data-fs-footer-link-group-panel=\"\">\n {list}\n </AccordionPanel>\n </AccordionItem>\n </AccordionRoot>\n <div data-fs-footer-link-group-column=\"\">\n <h3 data-fs-text=\"\" data-fs-footer-link-group-heading=\"\">\n {groupHeading}\n </h3>\n {list}\n </div>\n </div>\n );\n}\n"],"mappings":";;;;;;;;AAgBA,MAAa,yBAAyB,kBAAkB;CACtD,IAAI;CACJ,OAAO;CACP,aAAa;CACb,QAAQ;EACN,MAAM;EACN,sBAAsB;EAKtB,YAAY;GACV,SAAS;IAAE,MAAM;IAAU,WAAW;IAAG,OAAO;GAAgB;GAChE,OAAO;IACL,MAAM;IACN,OAAO;IACP,UAAU;IACV,OAAO;KACL,MAAM;KACN,sBAAsB;KACtB,UAAU,CAAC,SAAS,MAAM;KAC1B,YAAY;MACV,OAAO;OAAE,MAAM;OAAU,WAAW;OAAG,OAAO;MAAQ;MACtD,MAAM;OACJ,MAAM;OACN,WAAW;OACX,OAAO;OACP,QAAQ;OAIR,SAAS;MACX;MACA,QAAQ;OAAE,MAAM;OAAU,MAAM,CAAC,SAAS,QAAQ;OAAG,SAAS;OAAS,OAAO;MAAU;KAC1F;IACF;GACF;EACF;CACF;AACF,CAAC;AAwBD,MAAa,4BAA4B,gBAAgB;CACvD,MAAM;CACN,YAAY;CACZ,WAAW;CACX,SAAS;CACT,QAAQ;EAAE,OAAO;EAAqB,UAAU;CAAa;CAC7D,QAAQ;EACN,SAAS;GAAE,MAAM;GAAU,OAAO;EAAgB;EAClD,OAAO;GAAE,MAAM;GAAS,OAAO;EAAQ;CACzC;CACA,WACE;AACJ,CAAC;;AAcD,MAAM,uBAAuB;;;;;AAM7B,SAAS,aAAa,OAAuD;CAC3E,MAAM,WAA2B,CAAC;CAElC,MAAM,SAAS,MAAM,UAAU;EAG7B,IAAI,KAAK,MAAM,KAAK,CAAC,CAAC,WAAW,GAC/B;EAEF,MAAM,cAAc,gBAAgB,KAAK,IAAI;EAC7C,IAAI,CAAC,YAAY,MAEf,QAAQ,KAAK,+CAA+C;GAAE;GAAO,QAAQ,YAAY;EAAO,CAAC;EAEnG,SAAS,KAAK;GACZ,OAAO,KAAK;GACZ,MAAM,YAAY,OAAO,YAAY,OAAO;GAC5C,QAAQ,KAAK;EACf,CAAC;CACH,CAAC;CAED,OAAO;AACT;AAEA,SAAgB,gBAAgB,EAAE,SAAS,OAAO,UAAU,WAAW,GAAG,SAA+B;CACvG,MAAM,eAAe,UAAU,WAAW;CAE1C,MAAM,WAAW,aAAa,UAAU,SAAS,SAAS,CAAC,CAAC;CAE5D,IAAI,CAAC,cAAc,KAAK,KAAK,SAAS,WAAW,GAC/C,OAAO;CAIT,MAAM,OACJ,oBAAC,MAAD;EAAI,kCAA+B;EAChC,UAAA,SAAS,KAAK,MAAM,UACnB,oBAAC,MAAD;GAAgB,kCAA+B;GAC5C,UAAA,KAAK,SAAS,OACb,oBAAC,QAAD;IAAM,gBAAa;IAAG,kCAA+B;IAClD,UAAA,KAAK;GACF,CAAA,IAEN,oBAAC,gBAAD;IAAgB,MAAM,KAAK;IAAM,QAAQ,KAAK;IAAQ,kCAA+B;IAClF,UAAA,KAAK;GACQ,CAAA;EAEhB,GAVK,KAUL,CACL;CACC,CAAA;CAGN,OACE,qBAAC,OAAD;EAAK,GAAI;EAAkB;EAAW,6BAA0B;EAAhE,UAAA,CACE,oBAAC,eAAD;GAAe,uCAAoC;GACjD,UAAA,qBAAC,eAAD;IAAe,OAAO;IAAtB,UAAA,CACE,oBAAC,iBAAD;KAAiB,qCAAkC;KACjD,UAAA,oBAAC,kBAAD;MAAkB,gBAAa;MAAG,qCAAkC;MACjE,UAAA;KACe,CAAA;IACH,CAAA,GAGjB,oBAAC,gBAAD;KAAgB,kBAAA;KAAiB,aAAA;KAAY,mCAAgC;KAC1E,UAAA;IACa,CAAA,CACH;;EACF,CAAA,GACf,qBAAC,OAAD;GAAK,oCAAiC;GAAtC,UAAA,CACE,oBAAC,MAAD;IAAI,gBAAa;IAAG,qCAAkC;IACnD,UAAA;GACC,CAAA,GACH,IACE;EACF,CAAA,CAAA;;AAET"}
|
|
1
|
+
{"version":3,"file":"footer-link-group.js","names":[],"sources":["../../../src/particles/footer-link-group/footer-link-group.tsx"],"sourcesContent":["import { type ContentValue, defineComponent, defineContentType } from '@vtex/fs-core';\nimport type { ComponentPropsWithRef } from 'react';\nimport type { PageRef } from '../../sdk/navigation/types.js';\nimport { resolveSafeHref } from '../../utils/safe-href.js';\nimport {\n AccordionHeader,\n AccordionItem,\n AccordionPanel,\n AccordionRoot,\n AccordionTrigger,\n} from '../accordion/accordion.js';\nimport { NavigationLink } from '../navigation-link/navigation-link.js';\n\n// ---------------------------------------------------------------------------\n// Content model\n// ---------------------------------------------------------------------------\n\nexport const FooterLinkGroupContent = defineContentType({\n id: 'footer-link-group',\n title: 'Footer Link Group',\n description: 'A heading and its ordered list of institutional links.',\n schema: {\n type: 'object',\n additionalProperties: false,\n // Neither property is required. A missing element key already omits the component before any\n // required check runs, so requiring them would buy nothing and cost the prop fallback: an entry\n // authoring one field alone would be shape-invalid and take the whole element down with it. The\n // render gate holds the heading invariant instead, being the only point that sees both channels.\n properties: {\n heading: { type: 'string', minLength: 1, title: 'Group heading' },\n links: {\n type: 'array',\n title: 'Links',\n minItems: 1,\n items: {\n type: 'object',\n additionalProperties: false,\n required: ['label', 'href'],\n properties: {\n label: { type: 'string', minLength: 1, title: 'Label' },\n href: {\n type: 'string',\n minLength: 1,\n title: 'URL',\n format: 'uri-reference',\n // Author-time guidance only, and deliberately lookahead-free: nothing in this repo\n // compiles it, so its one consumer is the Content Platform Admin's form, whose regex\n // engine may be an RE2 family that rejects lookahead outright.\n pattern: '^(?:https?://.*|mailto:.+|tel:.+|/(?:[^/].*)?)$',\n },\n target: { type: 'string', enum: ['_self', '_blank'], default: '_self', title: 'Open in' },\n },\n },\n },\n },\n },\n});\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\ntype FooterLinkGroupContentValue = ContentValue<typeof FooterLinkGroupContent>;\n\ntype FooterLinkGroupContentLink = NonNullable<FooterLinkGroupContentValue['links']>[number];\n\n/** One authored link. The prop accepts a page reference; a CMS href stays a string. */\nexport type FooterLinkGroupLink = Omit<FooterLinkGroupContentLink, 'href'> & {\n href: PageRef | string;\n};\n\nexport type FooterLinkGroupProps = ComponentPropsWithRef<'div'> & {\n /** Authored fallback for the group heading, used when no CMS entry supplies one. */\n heading?: string;\n /** Authored fallback for the link list, used when no CMS entry supplies one. */\n links?: readonly FooterLinkGroupLink[];\n /** Injected by the compiler, sliced from the page's or layout's composite content entry. */\n $content?: FooterLinkGroupContentValue;\n};\n\n// ---------------------------------------------------------------------------\n// Definition\n// ---------------------------------------------------------------------------\n\nexport const FooterLinkGroupDefinition = defineComponent({\n name: 'FooterLinkGroup',\n importPath: '@vtex/faststore/components',\n component: FooterLinkGroup,\n content: FooterLinkGroupContent,\n editor: { label: 'Footer Link Group', category: 'navigation' },\n schema: {\n heading: { type: 'string', label: 'Group heading' },\n links: { type: 'array', label: 'Links' },\n },\n guideline:\n \"One heading and its institutional links, both CMS-editable. Renders as a desktop column and a mobile accordion at the same time; the store's swatch pair decides which one a breakpoint shows. See footer-link-group.md.\",\n});\n\n// ---------------------------------------------------------------------------\n// Implementation\n// ---------------------------------------------------------------------------\n\n/** A resolved link, ready for both forms. A null `href` is a destination the allowlist refused. */\ntype ResolvedLink = {\n label: string;\n href: PageRef | string | null;\n target: FooterLinkGroupLink['target'];\n};\n\nfunction readLinks(\n content: readonly FooterLinkGroupContentLink[] | undefined,\n authored: readonly FooterLinkGroupLink[] | undefined,\n): readonly FooterLinkGroupLink[] {\n if (content !== undefined) return content;\n return authored ?? [];\n}\n\nfunction resolveDestination(href: PageRef | string, index: number): PageRef | string | null {\n if (typeof href !== 'string') return href;\n\n const destination = resolveSafeHref(href);\n if (!destination.safe) {\n // A bounded reason and no authored value, the convention the content runtime's own log follows.\n console.warn('[FooterLinkGroup] link destination rejected', { index, reason: destination.reason });\n return null;\n }\n return destination.href;\n}\n\n/** Unique within its own root, and every group renders its own root. */\nconst ACCORDION_ITEM_VALUE = 'links';\n\n/**\n * Resolves the whole list once per render, so both forms show the same links and a refused\n * destination logs one line rather than two.\n */\nfunction resolveLinks(links: readonly FooterLinkGroupLink[]): ResolvedLink[] {\n const resolved: ResolvedLink[] = [];\n\n links.forEach((link, index) => {\n // `minLength` is enforced by neither the runtime slice nor the preflight, so a blank label\n // reaches the component. Drop the link: an anchor with no accessible name is a WCAG failure.\n if (link.label.trim().length === 0) {\n return;\n }\n resolved.push({\n label: link.label,\n href: resolveDestination(link.href, index),\n target: link.target,\n });\n });\n\n return resolved;\n}\n\nexport function FooterLinkGroup({ heading, links, $content, className, ...props }: FooterLinkGroupProps) {\n const groupHeading = $content?.heading ?? heading;\n // An entry supplying `links: []` beats an authored prop: a merchant who cleared the list cleared it.\n const resolved = resolveLinks(readLinks($content?.links, links));\n\n if (!groupHeading?.trim() || resolved.length === 0) {\n return null;\n }\n\n // One element rendered in both forms, so the two cannot drift.\n const list = (\n <ul data-fs-footer-link-group-list=\"\">\n {resolved.map((link, index) => (\n <li key={index} data-fs-footer-link-group-item=\"\">\n {link.href === null ? (\n <span data-fs-text=\"\" data-fs-footer-link-group-text=\"\">\n {link.label}\n </span>\n ) : (\n <NavigationLink href={link.href} target={link.target} data-fs-footer-link-group-link=\"\">\n {link.label}\n </NavigationLink>\n )}\n </li>\n ))}\n </ul>\n );\n\n return (\n <div {...props} className={className} data-fs-footer-link-group=\"\">\n <AccordionRoot data-fs-footer-link-group-accordion=\"\">\n <AccordionItem value={ACCORDION_ITEM_VALUE}>\n <AccordionHeader data-fs-footer-link-group-heading=\"\">\n <AccordionTrigger data-fs-text=\"\" data-fs-footer-link-group-trigger=\"\">\n {groupHeading}\n </AccordionTrigger>\n </AccordionHeader>\n {/* `hiddenUntilFound` requires the panel to stay mounted while closed, and the wrapper\n defaults `keepMounted` to false, so passing it keeps Base UI from warning about the pair. */}\n <AccordionPanel hiddenUntilFound keepMounted data-fs-footer-link-group-panel=\"\">\n {list}\n </AccordionPanel>\n </AccordionItem>\n </AccordionRoot>\n <div data-fs-footer-link-group-column=\"\">\n <h3 data-fs-text=\"\" data-fs-footer-link-group-heading=\"\">\n {groupHeading}\n </h3>\n {list}\n </div>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { ContentValue } from "../../core/dist/define-content-type.js";
|
|
2
2
|
import { ComponentDefinition } from "../../core/dist/define-component.js";
|
|
3
3
|
import "../../core/dist/index.js";
|
|
4
|
+
import { PageRef } from "../../sdk/navigation/types.js";
|
|
4
5
|
import { ComponentPropsWithRef } from "react";
|
|
5
6
|
//#region src/particles/incentives/incentives.d.ts
|
|
6
7
|
export declare const IncentivesContent: {
|
|
@@ -71,8 +72,11 @@ export declare const IncentivesContent: {
|
|
|
71
72
|
*/
|
|
72
73
|
export declare const INCENTIVE_ICON_NAMES: readonly ["truck", "calendar", "gift", "store", "shield-check"];
|
|
73
74
|
type IncentivesContentValue = ContentValue<typeof IncentivesContent>;
|
|
74
|
-
|
|
75
|
-
|
|
75
|
+
type IncentiveContentItem = NonNullable<IncentivesContentValue['items']>[number];
|
|
76
|
+
/** One incentive item. The prop accepts a page reference; a CMS href stays a string. */
|
|
77
|
+
export type IncentiveItem = Omit<IncentiveContentItem, 'href'> & {
|
|
78
|
+
href?: PageRef | string;
|
|
79
|
+
};
|
|
76
80
|
export type IncentivesProps = ComponentPropsWithRef<'ul'> & {
|
|
77
81
|
/** Authored fallback list, rendered when no CMS entry supplies one. */
|
|
78
82
|
items?: readonly IncentiveItem[];
|
|
@@ -96,8 +96,13 @@ const IncentivesDefinition = defineComponent({
|
|
|
96
96
|
* link group and the social row, where a link is the whole point, a refusal here costs the anchor
|
|
97
97
|
* alone — an unlinked item is this component's ordinary shape, so the glyph and copy still render.
|
|
98
98
|
*/
|
|
99
|
+
function readItems(content, authored) {
|
|
100
|
+
if (content !== void 0) return content;
|
|
101
|
+
return authored ?? [];
|
|
102
|
+
}
|
|
99
103
|
function resolveHref(raw, index) {
|
|
100
|
-
if (
|
|
104
|
+
if (typeof raw !== "string") return raw ?? null;
|
|
105
|
+
if (!raw.trim()) return null;
|
|
101
106
|
const destination = resolveSafeHref(raw);
|
|
102
107
|
if (!destination.safe) {
|
|
103
108
|
console.warn("[Incentives] link destination rejected", {
|
|
@@ -177,7 +182,7 @@ function IncentiveBody({ item }) {
|
|
|
177
182
|
] });
|
|
178
183
|
}
|
|
179
184
|
function Incentives({ items, $content, className, ...props }) {
|
|
180
|
-
const resolved = resolveIncentives($content?.items
|
|
185
|
+
const resolved = resolveIncentives(readItems($content?.items, items));
|
|
181
186
|
if (resolved.length === 0) return null;
|
|
182
187
|
return /* @__PURE__ */ jsx("ul", {
|
|
183
188
|
role: "list",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"incentives.js","names":[],"sources":["../../../src/particles/incentives/incentives.tsx"],"sourcesContent":["import { type ContentValue, defineComponent, defineContentType } from '@vtex/fs-core';\nimport type { ComponentPropsWithRef } from 'react';\nimport { resolveSafeHref } from '../../utils/safe-href.js';\nimport { Icon } from '../icon/icon.js';\nimport { Image } from '../image/image.js';\nimport { NavigationLink } from '../navigation-link/navigation-link.js';\n\n// ---------------------------------------------------------------------------\n// Content model\n// ---------------------------------------------------------------------------\n\nexport const IncentivesContent = defineContentType({\n id: 'incentives',\n title: 'Incentives',\n description: 'Store benefit highlights rendered as a strip in the footer.',\n schema: {\n type: 'object',\n additionalProperties: false,\n // `items` is deliberately optional. A missing element key already omits the component before any\n // required check runs, so requiring the list would buy nothing and cost the prop fallback: an\n // entry without it would be shape-invalid and the authored `items` never reached.\n properties: {\n items: {\n type: 'array',\n title: 'Incentive items',\n // Author-time guidance only: no read-time gate enforces item bounds, so its one consumer is\n // the Content Platform Admin's form. An empty list still reaches the component, which\n // self-hides on it. No ceiling — the swatch has to lay out at any count regardless.\n minItems: 1,\n items: {\n type: 'object',\n additionalProperties: false,\n // `title` alone. A nested required property is validated at read time and a violation omits\n // the whole strip, so every entry here widens the blast radius of one under-filled item.\n required: ['title'],\n properties: {\n // `minLength` on the title alone: it is the required property, so a blank one is the only\n // blank that decides anything. Both lines render if present and drop if blank, which is\n // guidance the Admin form has no reason to state differently for one of them.\n title: { type: 'string', minLength: 1, title: 'Title' },\n firstLineText: { type: 'string', title: 'First line' },\n secondLineText: { type: 'string', title: 'Second line' },\n // Optional, because a benefit strip links nowhere and a shortcut row links everywhere,\n // and the two are the same shape. Author-time guidance only, deliberately identical to\n // FooterLinkGroup's so the Admin form cannot reject a destination `resolveSafeHref`\n // would have accepted; lookahead-free because that regex engine may be an RE2 family.\n href: {\n type: 'string',\n minLength: 1,\n title: 'Link',\n format: 'uri-reference',\n pattern: '^(?:https?://.*|mailto:.+|tel:.+|/(?:[^/].*)?)$',\n },\n // Inline rather than a named constant: the catalog scanner reads this file statically and\n // rejects an identifier it cannot evaluate.\n icon: {\n type: 'string',\n enum: ['truck', 'calendar', 'gift', 'store', 'shield-check'],\n title: 'Icon',\n },\n iconUrl: {\n type: 'string',\n format: 'uri',\n title: 'Custom icon',\n widget: { 'ui:widget': 'media-gallery' },\n },\n alt: { type: 'string', title: 'Custom icon alt text' },\n },\n },\n },\n },\n },\n});\n\n/**\n * The benefit glyphs the `icon` enum admits. Exported because a store's `global.json` has to list\n * them under `icons.include` for a CMS-chosen name to resolve at runtime — one import there instead\n * of five retyped strings, which is what keeps the layout task from silently missing one. Reading it\n * off the model rather than declaring it alongside makes the two impossible to drift apart.\n */\nexport const INCENTIVE_ICON_NAMES = IncentivesContent.schema.properties.items.items.properties.icon.enum;\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\ntype IncentivesContentValue = ContentValue<typeof IncentivesContent>;\n\n/** One incentive item, identical in the prop and the content channels. */\nexport type IncentiveItem = NonNullable<IncentivesContentValue['items']>[number];\n\ntype IncentiveIconName = NonNullable<IncentiveItem['icon']>;\n\nexport type IncentivesProps = ComponentPropsWithRef<'ul'> & {\n /** Authored fallback list, rendered when no CMS entry supplies one. */\n items?: readonly IncentiveItem[];\n /** Injected by the compiler, sliced from the layout's composite content entry. */\n $content?: IncentivesContentValue;\n};\n\n// ---------------------------------------------------------------------------\n// Definition\n// ---------------------------------------------------------------------------\n\nexport const IncentivesDefinition = defineComponent({\n name: 'Incentives',\n importPath: '@vtex/faststore/components',\n component: Incentives,\n content: IncentivesContent,\n editor: { label: 'Incentives', category: 'content' },\n schema: {\n items: { type: 'array', label: 'Incentive items' },\n },\n guideline:\n \"The store's value propositions as a strip of icon-and-copy items, the whole list CMS-editable: each item carries a title, up to two description lines, either a predefined icon or an uploaded one, and an optional href that turns the whole block into a link. Every predefined icon name must be listed in the store's icons.include. Renders nothing when unconfigured. See incentives.md.\",\n});\n\n// ---------------------------------------------------------------------------\n// Implementation\n// ---------------------------------------------------------------------------\n\n/**\n * The three glyph states an item can reach, kept as a union so \"at most one source\" is a property of\n * the type rather than of how the resolver happens to build it. `alt` belongs to the uploaded member\n * alone, since a sprite glyph has nothing to name.\n */\ntype ResolvedGlyph =\n | { icon: IncentiveIconName; src?: undefined; alt?: undefined }\n | { icon?: undefined; src: string; alt?: string }\n | { icon?: undefined; src?: undefined; alt?: undefined };\n\n/**\n * An item that can render: a non-blank title, its surviving description lines, a glyph state, and a\n * destination the allowlist cleared. The title is the item's identity, so it is the only part whose\n * absence drops the item.\n */\ntype ResolvedIncentive = { title: string; lines: string[]; href: string | null } & ResolvedGlyph;\n\n/**\n * The item's destination, or null for both \"none authored\" and \"the allowlist refused it\". Unlike the\n * link group and the social row, where a link is the whole point, a refusal here costs the anchor\n * alone — an unlinked item is this component's ordinary shape, so the glyph and copy still render.\n */\nfunction resolveHref(raw: string | undefined, index: number): string | null {\n // Absent and blank are \"no link\", not a rejection, so neither reaches the allowlist or the log.\n if (!raw?.trim()) return null;\n\n const destination = resolveSafeHref(raw);\n if (!destination.safe) {\n // A bounded reason and no authored value, the convention the content runtime's own log follows.\n console.warn('[Incentives] link destination rejected', { index, reason: destination.reason });\n return null;\n }\n\n return destination.href;\n}\n\n/**\n * Drops every item with no title to show, and every description line that would render empty.\n * Neither read-time gate enforces `minLength`, so both blanks reach the component. The asymmetry is\n * deliberate: an icon with no label is not an incentive, while a missing line is supplementary.\n */\nfunction resolveIncentives(items: readonly IncentiveItem[]): ResolvedIncentive[] {\n const resolved: ResolvedIncentive[] = [];\n\n items.forEach((item, index) => {\n if (!item.title.trim()) return;\n\n const lines = [item.firstLineText, item.secondLineText].filter((line): line is string => Boolean(line?.trim()));\n const href = resolveHref(item.href, index);\n\n if (item.iconUrl?.trim()) {\n resolved.push({ title: item.title, lines, href, src: item.iconUrl, alt: item.alt });\n } else if (item.icon) {\n resolved.push({ title: item.title, lines, href, icon: item.icon });\n } else {\n resolved.push({ title: item.title, lines, href });\n }\n });\n\n return resolved;\n}\n\n/**\n * The item's glyph, or nothing. Presence decides the channel — an upload first, then a predefined\n * sprite name — and an item carrying neither renders as copy alone rather than an empty slot.\n */\nfunction IncentiveGlyph({ item }: { item: ResolvedIncentive }) {\n if (item.src !== undefined) {\n return <Image src={item.src} alt={item.alt} data-fs-incentive-icon=\"\" />;\n }\n\n if (item.icon !== undefined) {\n return <Icon name={item.icon} data-fs-incentive-icon=\"\" />;\n }\n\n return null;\n}\n\n/**\n * The glyph and copy an item renders in both forms. Kept whole rather than split across the linked\n * and unlinked branches so the anchor wraps the entire block: on a shortcut row the pictogram is\n * half the click target, and a link around the label alone leaves it dead.\n */\nfunction IncentiveBody({ item }: { item: ResolvedIncentive }) {\n return (\n <>\n <IncentiveGlyph item={item} />\n <p data-fs-text=\"\" data-fs-incentive-title=\"\">\n {item.title}\n </p>\n {item.lines.map((line, lineIndex) => (\n <p key={lineIndex} data-fs-text=\"\" data-fs-incentive-description=\"\">\n {line}\n </p>\n ))}\n </>\n );\n}\n\nexport function Incentives({ items, $content, className, ...props }: IncentivesProps) {\n const resolved = resolveIncentives($content?.items ?? items ?? []);\n\n if (resolved.length === 0) return null;\n\n // `role` precedes the spread so a consumer can still override it.\n return (\n // biome-ignore lint/a11y/noRedundantRoles: WebKit drops the implicit list role from a ul whose computed `list-style` is `none` — exactly what this component's swatch sets — so the explicit role is what holds the semantics the list was chosen for.\n <ul role=\"list\" {...props} className={className} data-fs-incentives=\"\">\n {resolved.map((item, index) => (\n <li key={index} data-fs-incentive=\"\">\n {item.href === null ? (\n <IncentiveBody item={item} />\n ) : (\n // No `target`: these are the store's own shortcuts, not the outbound profiles the social\n // row forces a new tab for.\n <NavigationLink href={item.href} data-fs-incentive-link=\"\">\n <IncentiveBody item={item} />\n </NavigationLink>\n )}\n </li>\n ))}\n </ul>\n );\n}\n"],"mappings":";;;;;;;;;AAWA,MAAa,oBAAoB,kBAAkB;CACjD,IAAI;CACJ,OAAO;CACP,aAAa;CACb,QAAQ;EACN,MAAM;EACN,sBAAsB;EAItB,YAAY,EACV,OAAO;GACL,MAAM;GACN,OAAO;GAIP,UAAU;GACV,OAAO;IACL,MAAM;IACN,sBAAsB;IAGtB,UAAU,CAAC,OAAO;IAClB,YAAY;KAIV,OAAO;MAAE,MAAM;MAAU,WAAW;MAAG,OAAO;KAAQ;KACtD,eAAe;MAAE,MAAM;MAAU,OAAO;KAAa;KACrD,gBAAgB;MAAE,MAAM;MAAU,OAAO;KAAc;KAKvD,MAAM;MACJ,MAAM;MACN,WAAW;MACX,OAAO;MACP,QAAQ;MACR,SAAS;KACX;KAGA,MAAM;MACJ,MAAM;MACN,MAAM;OAAC;OAAS;OAAY;OAAQ;OAAS;MAAc;MAC3D,OAAO;KACT;KACA,SAAS;MACP,MAAM;MACN,QAAQ;MACR,OAAO;MACP,QAAQ,EAAE,aAAa,gBAAgB;KACzC;KACA,KAAK;MAAE,MAAM;MAAU,OAAO;KAAuB;IACvD;GACF;EACF,EACF;CACF;AACF,CAAC;;;;;;;AAQD,MAAa,uBAAuB,kBAAkB,OAAO,WAAW,MAAM,MAAM,WAAW,KAAK;AAwBpG,MAAa,uBAAuB,gBAAgB;CAClD,MAAM;CACN,YAAY;CACZ,WAAW;CACX,SAAS;CACT,QAAQ;EAAE,OAAO;EAAc,UAAU;CAAU;CACnD,QAAQ,EACN,OAAO;EAAE,MAAM;EAAS,OAAO;CAAkB,EACnD;CACA,WACE;AACJ,CAAC;;;;;;AA4BD,SAAS,YAAY,KAAyB,OAA8B;CAE1E,IAAI,CAAC,KAAK,KAAK,GAAG,OAAO;CAEzB,MAAM,cAAc,gBAAgB,GAAG;CACvC,IAAI,CAAC,YAAY,MAAM;EAErB,QAAQ,KAAK,0CAA0C;GAAE;GAAO,QAAQ,YAAY;EAAO,CAAC;EAC5F,OAAO;CACT;CAEA,OAAO,YAAY;AACrB;;;;;;AAOA,SAAS,kBAAkB,OAAsD;CAC/E,MAAM,WAAgC,CAAC;CAEvC,MAAM,SAAS,MAAM,UAAU;EAC7B,IAAI,CAAC,KAAK,MAAM,KAAK,GAAG;EAExB,MAAM,QAAQ,CAAC,KAAK,eAAe,KAAK,cAAc,CAAC,CAAC,QAAQ,SAAyB,QAAQ,MAAM,KAAK,CAAC,CAAC;EAC9G,MAAM,OAAO,YAAY,KAAK,MAAM,KAAK;EAEzC,IAAI,KAAK,SAAS,KAAK,GACrB,SAAS,KAAK;GAAE,OAAO,KAAK;GAAO;GAAO;GAAM,KAAK,KAAK;GAAS,KAAK,KAAK;EAAI,CAAC;OAC7E,IAAI,KAAK,MACd,SAAS,KAAK;GAAE,OAAO,KAAK;GAAO;GAAO;GAAM,MAAM,KAAK;EAAK,CAAC;OAEjE,SAAS,KAAK;GAAE,OAAO,KAAK;GAAO;GAAO;EAAK,CAAC;CAEpD,CAAC;CAED,OAAO;AACT;;;;;AAMA,SAAS,eAAe,EAAE,QAAqC;CAC7D,IAAI,KAAK,QAAQ,KAAA,GACf,OAAO,oBAAC,OAAD;EAAO,KAAK,KAAK;EAAK,KAAK,KAAK;EAAK,0BAAuB;CAAI,CAAA;CAGzE,IAAI,KAAK,SAAS,KAAA,GAChB,OAAO,oBAAC,MAAD;EAAM,MAAM,KAAK;EAAM,0BAAuB;CAAI,CAAA;CAG3D,OAAO;AACT;;;;;;AAOA,SAAS,cAAc,EAAE,QAAqC;CAC5D,OACE,qBAAA,UAAA,EAAA,UAAA;EACE,oBAAC,gBAAD,EAAsB,KAAO,CAAA;EAC7B,oBAAC,KAAD;GAAG,gBAAa;GAAG,2BAAwB;GACxC,UAAA,KAAK;EACL,CAAA;EACF,KAAK,MAAM,KAAK,MAAM,cACrB,oBAAC,KAAD;GAAmB,gBAAa;GAAG,iCAA8B;GAC9D,UAAA;EACA,GAFK,SAEL,CACJ;CACD,EAAA,CAAA;AAEN;AAEA,SAAgB,WAAW,EAAE,OAAO,UAAU,WAAW,GAAG,SAA0B;CACpF,MAAM,WAAW,kBAAkB,UAAU,SAAS,SAAS,CAAC,CAAC;CAEjE,IAAI,SAAS,WAAW,GAAG,OAAO;CAGlC,OAEE,oBAAC,MAAD;EAAI,MAAK;EAAO,GAAI;EAAkB;EAAW,sBAAmB;EACjE,UAAA,SAAS,KAAK,MAAM,UACnB,oBAAC,MAAD;GAAgB,qBAAkB;GAC/B,UAAA,KAAK,SAAS,OACb,oBAAC,eAAD,EAAqB,KAAO,CAAA,IAI5B,oBAAC,gBAAD;IAAgB,MAAM,KAAK;IAAM,0BAAuB;IACtD,UAAA,oBAAC,eAAD,EAAqB,KAAO,CAAA;GACd,CAAA;EAEhB,GAVK,KAUL,CACL;CACC,CAAA;AAER"}
|
|
1
|
+
{"version":3,"file":"incentives.js","names":[],"sources":["../../../src/particles/incentives/incentives.tsx"],"sourcesContent":["import { type ContentValue, defineComponent, defineContentType } from '@vtex/fs-core';\nimport type { ComponentPropsWithRef } from 'react';\nimport type { PageRef } from '../../sdk/navigation/types.js';\nimport { resolveSafeHref } from '../../utils/safe-href.js';\nimport { Icon } from '../icon/icon.js';\nimport { Image } from '../image/image.js';\nimport { NavigationLink } from '../navigation-link/navigation-link.js';\n\n// ---------------------------------------------------------------------------\n// Content model\n// ---------------------------------------------------------------------------\n\nexport const IncentivesContent = defineContentType({\n id: 'incentives',\n title: 'Incentives',\n description: 'Store benefit highlights rendered as a strip in the footer.',\n schema: {\n type: 'object',\n additionalProperties: false,\n // `items` is deliberately optional. A missing element key already omits the component before any\n // required check runs, so requiring the list would buy nothing and cost the prop fallback: an\n // entry without it would be shape-invalid and the authored `items` never reached.\n properties: {\n items: {\n type: 'array',\n title: 'Incentive items',\n // Author-time guidance only: no read-time gate enforces item bounds, so its one consumer is\n // the Content Platform Admin's form. An empty list still reaches the component, which\n // self-hides on it. No ceiling — the swatch has to lay out at any count regardless.\n minItems: 1,\n items: {\n type: 'object',\n additionalProperties: false,\n // `title` alone. A nested required property is validated at read time and a violation omits\n // the whole strip, so every entry here widens the blast radius of one under-filled item.\n required: ['title'],\n properties: {\n // `minLength` on the title alone: it is the required property, so a blank one is the only\n // blank that decides anything. Both lines render if present and drop if blank, which is\n // guidance the Admin form has no reason to state differently for one of them.\n title: { type: 'string', minLength: 1, title: 'Title' },\n firstLineText: { type: 'string', title: 'First line' },\n secondLineText: { type: 'string', title: 'Second line' },\n // Optional, because a benefit strip links nowhere and a shortcut row links everywhere,\n // and the two are the same shape. Author-time guidance only, deliberately identical to\n // FooterLinkGroup's so the Admin form cannot reject a destination `resolveSafeHref`\n // would have accepted; lookahead-free because that regex engine may be an RE2 family.\n href: {\n type: 'string',\n minLength: 1,\n title: 'Link',\n format: 'uri-reference',\n pattern: '^(?:https?://.*|mailto:.+|tel:.+|/(?:[^/].*)?)$',\n },\n // Inline rather than a named constant: the catalog scanner reads this file statically and\n // rejects an identifier it cannot evaluate.\n icon: {\n type: 'string',\n enum: ['truck', 'calendar', 'gift', 'store', 'shield-check'],\n title: 'Icon',\n },\n iconUrl: {\n type: 'string',\n format: 'uri',\n title: 'Custom icon',\n widget: { 'ui:widget': 'media-gallery' },\n },\n alt: { type: 'string', title: 'Custom icon alt text' },\n },\n },\n },\n },\n },\n});\n\n/**\n * The benefit glyphs the `icon` enum admits. Exported because a store's `global.json` has to list\n * them under `icons.include` for a CMS-chosen name to resolve at runtime — one import there instead\n * of five retyped strings, which is what keeps the layout task from silently missing one. Reading it\n * off the model rather than declaring it alongside makes the two impossible to drift apart.\n */\nexport const INCENTIVE_ICON_NAMES = IncentivesContent.schema.properties.items.items.properties.icon.enum;\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\ntype IncentivesContentValue = ContentValue<typeof IncentivesContent>;\n\ntype IncentiveContentItem = NonNullable<IncentivesContentValue['items']>[number];\n\n/** One incentive item. The prop accepts a page reference; a CMS href stays a string. */\nexport type IncentiveItem = Omit<IncentiveContentItem, 'href'> & {\n href?: PageRef | string;\n};\n\ntype IncentiveIconName = NonNullable<IncentiveItem['icon']>;\n\nexport type IncentivesProps = ComponentPropsWithRef<'ul'> & {\n /** Authored fallback list, rendered when no CMS entry supplies one. */\n items?: readonly IncentiveItem[];\n /** Injected by the compiler, sliced from the layout's composite content entry. */\n $content?: IncentivesContentValue;\n};\n\n// ---------------------------------------------------------------------------\n// Definition\n// ---------------------------------------------------------------------------\n\nexport const IncentivesDefinition = defineComponent({\n name: 'Incentives',\n importPath: '@vtex/faststore/components',\n component: Incentives,\n content: IncentivesContent,\n editor: { label: 'Incentives', category: 'content' },\n schema: {\n items: { type: 'array', label: 'Incentive items' },\n },\n guideline:\n \"The store's value propositions as a strip of icon-and-copy items, the whole list CMS-editable: each item carries a title, up to two description lines, either a predefined icon or an uploaded one, and an optional href that turns the whole block into a link. Every predefined icon name must be listed in the store's icons.include. Renders nothing when unconfigured. See incentives.md.\",\n});\n\n// ---------------------------------------------------------------------------\n// Implementation\n// ---------------------------------------------------------------------------\n\n/**\n * The three glyph states an item can reach, kept as a union so \"at most one source\" is a property of\n * the type rather than of how the resolver happens to build it. `alt` belongs to the uploaded member\n * alone, since a sprite glyph has nothing to name.\n */\ntype ResolvedGlyph =\n | { icon: IncentiveIconName; src?: undefined; alt?: undefined }\n | { icon?: undefined; src: string; alt?: string }\n | { icon?: undefined; src?: undefined; alt?: undefined };\n\n/**\n * An item that can render: a non-blank title, its surviving description lines, a glyph state, and a\n * destination the allowlist cleared. The title is the item's identity, so it is the only part whose\n * absence drops the item.\n */\ntype ResolvedIncentive = { title: string; lines: string[]; href: PageRef | string | null } & ResolvedGlyph;\n\n/**\n * The item's destination, or null for both \"none authored\" and \"the allowlist refused it\". Unlike the\n * link group and the social row, where a link is the whole point, a refusal here costs the anchor\n * alone — an unlinked item is this component's ordinary shape, so the glyph and copy still render.\n */\nfunction readItems(\n content: readonly IncentiveContentItem[] | undefined,\n authored: readonly IncentiveItem[] | undefined,\n): readonly IncentiveItem[] {\n if (content !== undefined) return content;\n return authored ?? [];\n}\n\nfunction resolveHref(raw: PageRef | string | undefined, index: number): PageRef | string | null {\n // A page reference skips the string allowlist. Absent is \"no link\", not a rejection.\n if (typeof raw !== 'string') return raw ?? null;\n // Blank is \"no link\", not a rejection, so it never reaches the allowlist or the log.\n if (!raw.trim()) return null;\n\n const destination = resolveSafeHref(raw);\n if (!destination.safe) {\n // A bounded reason and no authored value, the convention the content runtime's own log follows.\n console.warn('[Incentives] link destination rejected', { index, reason: destination.reason });\n return null;\n }\n\n return destination.href;\n}\n\n/**\n * Drops every item with no title to show, and every description line that would render empty.\n * Neither read-time gate enforces `minLength`, so both blanks reach the component. The asymmetry is\n * deliberate: an icon with no label is not an incentive, while a missing line is supplementary.\n */\nfunction resolveIncentives(items: readonly IncentiveItem[]): ResolvedIncentive[] {\n const resolved: ResolvedIncentive[] = [];\n\n items.forEach((item, index) => {\n if (!item.title.trim()) return;\n\n const lines = [item.firstLineText, item.secondLineText].filter((line): line is string => Boolean(line?.trim()));\n const href = resolveHref(item.href, index);\n\n if (item.iconUrl?.trim()) {\n resolved.push({ title: item.title, lines, href, src: item.iconUrl, alt: item.alt });\n } else if (item.icon) {\n resolved.push({ title: item.title, lines, href, icon: item.icon });\n } else {\n resolved.push({ title: item.title, lines, href });\n }\n });\n\n return resolved;\n}\n\n/**\n * The item's glyph, or nothing. Presence decides the channel — an upload first, then a predefined\n * sprite name — and an item carrying neither renders as copy alone rather than an empty slot.\n */\nfunction IncentiveGlyph({ item }: { item: ResolvedIncentive }) {\n if (item.src !== undefined) {\n return <Image src={item.src} alt={item.alt} data-fs-incentive-icon=\"\" />;\n }\n\n if (item.icon !== undefined) {\n return <Icon name={item.icon} data-fs-incentive-icon=\"\" />;\n }\n\n return null;\n}\n\n/**\n * The glyph and copy an item renders in both forms. Kept whole rather than split across the linked\n * and unlinked branches so the anchor wraps the entire block: on a shortcut row the pictogram is\n * half the click target, and a link around the label alone leaves it dead.\n */\nfunction IncentiveBody({ item }: { item: ResolvedIncentive }) {\n return (\n <>\n <IncentiveGlyph item={item} />\n <p data-fs-text=\"\" data-fs-incentive-title=\"\">\n {item.title}\n </p>\n {item.lines.map((line, lineIndex) => (\n <p key={lineIndex} data-fs-text=\"\" data-fs-incentive-description=\"\">\n {line}\n </p>\n ))}\n </>\n );\n}\n\nexport function Incentives({ items, $content, className, ...props }: IncentivesProps) {\n const resolved = resolveIncentives(readItems($content?.items, items));\n\n if (resolved.length === 0) return null;\n\n // `role` precedes the spread so a consumer can still override it.\n return (\n // biome-ignore lint/a11y/noRedundantRoles: WebKit drops the implicit list role from a ul whose computed `list-style` is `none` — exactly what this component's swatch sets — so the explicit role is what holds the semantics the list was chosen for.\n <ul role=\"list\" {...props} className={className} data-fs-incentives=\"\">\n {resolved.map((item, index) => (\n <li key={index} data-fs-incentive=\"\">\n {item.href === null ? (\n <IncentiveBody item={item} />\n ) : (\n // No `target`: these are the store's own shortcuts, not the outbound profiles the social\n // row forces a new tab for.\n <NavigationLink href={item.href} data-fs-incentive-link=\"\">\n <IncentiveBody item={item} />\n </NavigationLink>\n )}\n </li>\n ))}\n </ul>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { ContentValue } from "../../core/dist/define-content-type.js";
|
|
2
2
|
import { ComponentDefinition } from "../../core/dist/define-component.js";
|
|
3
3
|
import "../../core/dist/index.js";
|
|
4
|
+
import { PageRef } from "../../sdk/navigation/types.js";
|
|
4
5
|
import { ComponentPropsWithRef } from "react";
|
|
5
6
|
//#region src/particles/logo/logo.d.ts
|
|
6
7
|
export declare const LogoContent: {
|
|
@@ -41,8 +42,8 @@ export type LogoProps = Omit<ComponentPropsWithRef<'div'>, 'children'> & {
|
|
|
41
42
|
src?: string;
|
|
42
43
|
/** Accessible alt text. Optional: the render gate, not the type, requires a resolved value. */
|
|
43
44
|
alt?: string;
|
|
44
|
-
/** Navigation destination; defaults to the
|
|
45
|
-
href?: string;
|
|
45
|
+
/** Navigation destination; defaults to the home page. A string that fails the safety check is dropped. */
|
|
46
|
+
href?: PageRef | string;
|
|
46
47
|
/** Image loading strategy. `eager` suits the above-the-fold header; author `lazy` below it. */
|
|
47
48
|
loading?: 'lazy' | 'eager';
|
|
48
49
|
/** Merchant-authored logo, sliced from the page/layout content entry; wins over the props when
|
|
@@ -55,8 +55,8 @@ const LogoDefinition = defineComponent({
|
|
|
55
55
|
label: "Alt Text"
|
|
56
56
|
},
|
|
57
57
|
href: {
|
|
58
|
-
type: "
|
|
59
|
-
default: "
|
|
58
|
+
type: "link",
|
|
59
|
+
default: { standard: "home" },
|
|
60
60
|
label: "Link Destination"
|
|
61
61
|
},
|
|
62
62
|
loading: {
|
|
@@ -67,13 +67,12 @@ const LogoDefinition = defineComponent({
|
|
|
67
67
|
},
|
|
68
68
|
guideline: "Brand logo linking to the configured home; a CMS entry overrides its src, alt, and href at runtime. Every prop is optional: an element bound to a CMS entry should author none of them, so an unconfigured store renders no logo instead of a placeholder. Author src and alt only where no entry supplies them, never as a fallback beside one. Size it responsively with a swatch on data-fs-logo, and reach the mark itself through data-fs-logo-image. A destination that is not an http, https, mailto, tel, or site-relative URL is dropped and the image renders unlinked. Loads eagerly for the above-the-fold header; set loading to lazy below the fold. Composes the Image and NavigationLink particles."
|
|
69
69
|
});
|
|
70
|
-
function Logo({ src, alt, href = "
|
|
70
|
+
function Logo({ src, alt, href = { standard: "home" }, loading = "eager", $content, className, ...props }) {
|
|
71
71
|
const logoSrc = $content?.src?.trim() ? $content.src : src;
|
|
72
72
|
const logoAlt = $content?.alt ?? alt;
|
|
73
73
|
const logoHref = $content?.href?.trim() ? $content.href : href;
|
|
74
74
|
if (!logoSrc || !logoAlt?.trim()) return null;
|
|
75
|
-
const destination =
|
|
76
|
-
if (!destination.safe) console.warn("[Logo] link destination rejected", { reason: destination.reason });
|
|
75
|
+
const destination = resolveLogoHref(logoHref);
|
|
77
76
|
const image = /* @__PURE__ */ jsx(Image, {
|
|
78
77
|
src: logoSrc,
|
|
79
78
|
alt: logoAlt,
|
|
@@ -84,12 +83,25 @@ function Logo({ src, alt, href = "/", loading = "eager", $content, className, ..
|
|
|
84
83
|
...props,
|
|
85
84
|
className,
|
|
86
85
|
"data-fs-logo": "",
|
|
87
|
-
children: destination
|
|
88
|
-
href: destination
|
|
86
|
+
children: destination === null ? image : /* @__PURE__ */ jsx(NavigationLink, {
|
|
87
|
+
href: destination,
|
|
89
88
|
children: image
|
|
90
|
-
})
|
|
89
|
+
})
|
|
91
90
|
});
|
|
92
91
|
}
|
|
92
|
+
/**
|
|
93
|
+
* A page reference skips the string allowlist. A string is checked, and a refusal drops the link.
|
|
94
|
+
* No read-time validator enforces `format` or `pattern`, so this is the only gate on a CMS string.
|
|
95
|
+
*/
|
|
96
|
+
function resolveLogoHref(href) {
|
|
97
|
+
if (typeof href !== "string") return href;
|
|
98
|
+
const destination = resolveSafeHref(href);
|
|
99
|
+
if (!destination.safe) {
|
|
100
|
+
console.warn("[Logo] link destination rejected", { reason: destination.reason });
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
return destination.href;
|
|
104
|
+
}
|
|
93
105
|
//#endregion
|
|
94
106
|
export { Logo, LogoContent, LogoDefinition };
|
|
95
107
|
|