@vtex/faststore 0.0.8 → 0.0.10
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 +59 -0
- package/dist/actions.json +42 -0
- package/dist/catalog.json +731 -16
- package/dist/cli/index.js +1 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/components.d.ts +9 -4
- package/dist/components.js +11 -6
- package/dist/content-types.json +300 -0
- package/dist/core/dist/define-component.d.ts +6 -1
- package/dist/docs/account-entry-point.md +4 -4
- package/dist/docs/account-menu.md +2 -2
- package/dist/docs/account-saved-cards.md +301 -0
- package/dist/docs/binding-switcher.md +154 -0
- package/dist/docs/card-entry.md +3 -3
- package/dist/docs/card-summary.md +175 -0
- package/dist/docs/cart-destination.md +238 -0
- package/dist/docs/cart-item.md +21 -3
- package/dist/docs/cart-totalizer.md +1 -1
- package/dist/docs/cart.md +118 -11
- package/dist/docs/footer-social.md +1 -1
- package/dist/docs/free-shipping-progress.md +13 -9
- package/dist/docs/incentives.md +2 -2
- package/dist/docs/logo.md +1 -1
- package/dist/docs/payment-method-badges.md +2 -2
- package/dist/docs/payment-methods.md +1 -1
- package/dist/docs/product-information.md +2 -2
- package/dist/docs/product-shelf.md +7 -0
- package/dist/docs/select.md +3 -4
- package/dist/docs/sign-in-redirect.md +1 -1
- package/dist/docs/toast.md +30 -12
- package/dist/index.d.ts +2 -1
- package/dist/internal/client.d.ts +4 -1
- package/dist/internal/client.js +4 -1
- package/dist/internal/server.d.ts +4 -2
- package/dist/internal/server.js +4 -2
- package/dist/matter/account-saved-cards/account-saved-cards.d.ts +383 -0
- package/dist/matter/account-saved-cards/account-saved-cards.js +956 -0
- package/dist/matter/account-saved-cards/account-saved-cards.js.map +1 -0
- package/dist/matter/billing-address/billing-address.js.map +1 -1
- package/dist/matter/cart/cart-namespace.d.ts +5 -1
- package/dist/matter/cart/cart-namespace.js +5 -1
- package/dist/matter/cart/cart-namespace.js.map +1 -1
- package/dist/matter/cart/cart.d.ts +34 -4
- package/dist/matter/cart/cart.js +238 -28
- package/dist/matter/cart/cart.js.map +1 -1
- package/dist/matter/cart-destination/cart-destination.d.ts +222 -0
- package/dist/matter/cart-destination/cart-destination.js +613 -0
- package/dist/matter/cart-destination/cart-destination.js.map +1 -0
- package/dist/matter/cart-item/cart-item.d.ts +4 -2
- package/dist/matter/cart-item/cart-item.js +75 -16
- package/dist/matter/cart-item/cart-item.js.map +1 -1
- package/dist/matter/cart-totalizer/cart-totalizer.js +3 -2
- package/dist/matter/cart-totalizer/cart-totalizer.js.map +1 -1
- package/dist/matter/free-shipping-progress/free-shipping-progress.d.ts +6 -3
- package/dist/matter/free-shipping-progress/free-shipping-progress.js +76 -33
- package/dist/matter/free-shipping-progress/free-shipping-progress.js.map +1 -1
- package/dist/matter/pricing/pricing.js.map +1 -1
- package/dist/matter/product-shelf/product-shelf.d.ts +7 -0
- package/dist/matter/product-shelf/product-shelf.js +115 -1
- package/dist/matter/product-shelf/product-shelf.js.map +1 -1
- 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 +6 -2
- package/dist/matter/shipping-simulator/shipping-simulator.js.map +1 -1
- package/dist/particles/binding-switcher/binding-switcher.d.ts +38 -0
- package/dist/particles/binding-switcher/binding-switcher.js +183 -0
- package/dist/particles/binding-switcher/binding-switcher.js.map +1 -0
- package/dist/particles/card-summary/card-network.d.ts +17 -0
- package/dist/particles/card-summary/card-network.js +25 -0
- package/dist/particles/card-summary/card-network.js.map +1 -0
- package/dist/particles/card-summary/card-summary.d.ts +83 -0
- package/dist/particles/card-summary/card-summary.js +188 -0
- package/dist/particles/card-summary/card-summary.js.map +1 -0
- package/dist/particles/footer-social/footer-social.d.ts +1 -1
- package/dist/particles/footer-social/footer-social.js +1 -1
- package/dist/particles/footer-social/footer-social.js.map +1 -1
- package/dist/particles/incentives/incentives.d.ts +1 -1
- package/dist/particles/incentives/incentives.js +1 -1
- package/dist/particles/incentives/incentives.js.map +1 -1
- package/dist/particles/payment-methods/payment-methods.d.ts +1 -1
- package/dist/particles/payment-methods/payment-methods.js +1 -1
- package/dist/particles/payment-methods/payment-methods.js.map +1 -1
- package/dist/particles/toast/toast.d.ts +20 -6
- package/dist/particles/toast/toast.js +49 -7
- package/dist/particles/toast/toast.js.map +1 -1
- package/dist/schema.graphql +298 -34
- package/dist/sdk/actions/index.d.ts +3 -1
- package/dist/sdk/actions/index.js +3 -1
- package/dist/sdk/actions/locale-action-provider.d.ts +16 -0
- package/dist/sdk/actions/locale-action-provider.js +20 -0
- package/dist/sdk/actions/locale-action-provider.js.map +1 -0
- package/dist/sdk/actions/saved-card-action-provider.d.ts +12 -0
- package/dist/sdk/actions/saved-card-action-provider.js +16 -0
- package/dist/sdk/actions/saved-card-action-provider.js.map +1 -0
- package/dist/sdk/actions/types.d.ts +6 -2
- package/dist/sdk/cache/storefront-cache.js +2 -1
- package/dist/sdk/cache/storefront-cache.js.map +1 -1
- package/dist/sdk/cart/operations.d.ts +2 -1
- package/dist/sdk/cart/operations.js +55 -1
- package/dist/sdk/cart/operations.js.map +1 -1
- package/dist/sdk/cart/optimistic.d.ts +30 -7
- package/dist/sdk/cart/optimistic.js +78 -41
- package/dist/sdk/cart/optimistic.js.map +1 -1
- package/dist/sdk/cart/use-cart-actions.d.ts +7 -0
- package/dist/sdk/cart/use-cart-actions.js +61 -15
- package/dist/sdk/cart/use-cart-actions.js.map +1 -1
- package/dist/sdk/cart/use-cart-sync-sales-channel.js +70 -0
- package/dist/sdk/cart/use-cart-sync-sales-channel.js.map +1 -0
- package/dist/sdk/generated/gql.js +15 -11
- package/dist/sdk/generated/gql.js.map +1 -1
- package/dist/sdk/generated/graphql.d.ts +177 -46
- package/dist/sdk/generated/graphql.js +2314 -93
- package/dist/sdk/generated/graphql.js.map +1 -1
- package/dist/sdk/i18n/messages/en-US.js +19 -0
- package/dist/sdk/i18n/messages/en-US.json +19 -0
- package/dist/sdk/i18n/messages/en-US.json.d.ts +19 -0
- package/dist/sdk/i18n/messages/es-ES.js +19 -0
- package/dist/sdk/i18n/messages/es-ES.json +19 -0
- package/dist/sdk/i18n/messages/pt-BR.js +19 -0
- package/dist/sdk/i18n/messages/pt-BR.json +19 -0
- package/dist/sdk/i18n/provider.js +1 -1
- package/dist/sdk/i18n/provider.js.map +1 -1
- package/dist/sdk/locale/binding-switch-provider.d.ts +13 -0
- package/dist/sdk/locale/binding-switch-provider.js +20 -0
- package/dist/sdk/locale/binding-switch-provider.js.map +1 -0
- package/dist/sdk/locale/use-locale-actions.d.ts +16 -0
- package/dist/sdk/locale/use-locale-actions.js +70 -0
- package/dist/sdk/locale/use-locale-actions.js.map +1 -0
- package/dist/sdk/navigation/binding-switch.d.ts +39 -0
- package/dist/sdk/navigation/binding-switch.js +129 -0
- package/dist/sdk/navigation/binding-switch.js.map +1 -0
- package/dist/sdk/navigation/decode-route-params.d.ts +11 -0
- package/dist/sdk/navigation/decode-route-params.js +32 -0
- package/dist/sdk/navigation/decode-route-params.js.map +1 -0
- package/dist/sdk/saved-cards/operations.d.ts +16 -0
- package/dist/sdk/saved-cards/operations.js +41 -0
- package/dist/sdk/saved-cards/operations.js.map +1 -0
- package/dist/sdk/saved-cards/use-saved-card-actions.d.ts +26 -0
- package/dist/sdk/saved-cards/use-saved-card-actions.js +86 -0
- package/dist/sdk/saved-cards/use-saved-card-actions.js.map +1 -0
- package/dist/sdk/seo/page-alternates.d.ts +5 -3
- package/dist/sdk/seo/page-alternates.js +7 -12
- package/dist/sdk/seo/page-alternates.js.map +1 -1
- package/dist/sdk/shipping/shipping-operations.d.ts +1 -1
- package/dist/sdk/shipping/shipping-operations.js +13 -0
- package/dist/sdk/shipping/shipping-operations.js.map +1 -1
- package/dist/sdk/state/cart.d.ts +9 -2
- package/dist/sdk/state/cart.js +9 -2
- package/dist/sdk/state/cart.js.map +1 -1
- package/package.json +2 -2
- package/src/styles/tailwind.css +1 -2
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
# CartDestination
|
|
2
|
+
|
|
3
|
+
**Category:** content
|
|
4
|
+
**Purpose:** The Cart page's delivery location. `CartDestinationRoot` reads where the cart is delivered and writes a new destination from a postal code alone, so the free-shipping bar beside it follows. It has two modes: read ("Entregando em 01310-100 · São Paulo, SP" plus a change link) and edit (a postal code field with "Atualizar" and "Cancelar").
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Visual cues (how to identify in an image)
|
|
9
|
+
|
|
10
|
+
Look in the **top of the cart page's main column**, above the free-shipping bar and the line items:
|
|
11
|
+
|
|
12
|
+
- A single line reading **"Entregando em"** (or "Delivering to") followed by a **postal code and city, state**, beside a link such as **"Trocar localização?"**.
|
|
13
|
+
- With no destination yet, the line reads a prompt such as **"Informe seu CEP"** beside the same link.
|
|
14
|
+
- Pressing the link swaps the line for **one masked postal code field**, an **"Atualizar"** button, a **"Cancelar"** button and a short help line.
|
|
15
|
+
|
|
16
|
+
The image is the source of truth for layout.
|
|
17
|
+
|
|
18
|
+
A postal code control in the **site header** is not this component: that is `RegionRoot`.
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## When to use
|
|
23
|
+
|
|
24
|
+
- On the **Cart page**, once, where the design shows where the cart is delivered and lets the shopper change it.
|
|
25
|
+
|
|
26
|
+
## When NOT to use
|
|
27
|
+
|
|
28
|
+
- **In the header** — that is `Region`, which only sets a cookie and never touches the cart.
|
|
29
|
+
- **For the full delivery address on checkout** — that is `ShippingAddressFieldset`.
|
|
30
|
+
- **For a shipping-cost quote on a product page or in the cart drawer** — that is `ShippingSimulator`; it quotes and writes nothing.
|
|
31
|
+
- More than once per page: two roots mean two drafts over the same destination.
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Composition
|
|
36
|
+
|
|
37
|
+
`CartDestinationRoot` provides the `'cartDestination'` context; every other part declares `requiresContext: 'cartDestination'`.
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
CartDestinationRoot (provides 'cartDestination'; starts in read mode)
|
|
41
|
+
├── CartDestinationRead (read mode only)
|
|
42
|
+
│ ├── CartDestinationValue (the destination, once set)
|
|
43
|
+
│ ├── CartDestinationUnset (the store's invitation, while none is set)
|
|
44
|
+
│ │ └── Text ("Informe seu CEP")
|
|
45
|
+
│ └── CartDestinationChange (opens edit mode)
|
|
46
|
+
└── CartDestinationForm (edit mode only)
|
|
47
|
+
├── CartDestinationPostalCodeControl
|
|
48
|
+
├── CartDestinationSubmit
|
|
49
|
+
├── CartDestinationCancel
|
|
50
|
+
├── Text (the help line, placed by the store)
|
|
51
|
+
└── CartDestinationError
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Child order reflects visual order. The store cannot open edit mode from outside: the mode is local to the root, and the destination shown always comes from the cart, never from what was typed.
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## Components
|
|
59
|
+
|
|
60
|
+
### CartDestinationRoot
|
|
61
|
+
|
|
62
|
+
Reads the cart destination reactively and broadcasts it, the edit-mode flag, the draft and the write state. Starts in read mode, also with no destination.
|
|
63
|
+
|
|
64
|
+
**Data:** declares `fragment CartDestinationData on Query` (`shipping { id address { postalCode country city state } }`) with `fetchStrategy: 'reactive'`. It selects no name and no street.
|
|
65
|
+
|
|
66
|
+
**Write:** declares `actions: { shipping: ['setAddress'] }`. A submit sends `{ postalCode, country }` and nothing else; the cart keeps its recipient name. The response updates the destination and the free-shipping bar through the cache.
|
|
67
|
+
|
|
68
|
+
| Prop | Type | Default | Notes |
|
|
69
|
+
|------|------|---------|-------|
|
|
70
|
+
| `defaultCountry` | string | `"BRA"` | Used while the cart has no address; the address country wins |
|
|
71
|
+
|
|
72
|
+
### CartDestinationRead
|
|
73
|
+
|
|
74
|
+
Renders its children only in read mode. No props.
|
|
75
|
+
|
|
76
|
+
### CartDestinationValue
|
|
77
|
+
|
|
78
|
+
The destination as prefix, postal code, city and state, each in its own element. Renders nothing while loading or when none is set.
|
|
79
|
+
|
|
80
|
+
| Prop | Type | Default | Notes |
|
|
81
|
+
|------|------|---------|-------|
|
|
82
|
+
| `prefix` | string | — | For example `"Entregando em"`; authored content wins |
|
|
83
|
+
|
|
84
|
+
**Content model** (`cart-destination-value`): `prefix`.
|
|
85
|
+
|
|
86
|
+
### CartDestinationUnset
|
|
87
|
+
|
|
88
|
+
Renders its children only once the destination resolved and none is set. Place the invitation inside it, within `CartDestinationRead`. No props.
|
|
89
|
+
|
|
90
|
+
### CartDestinationChange
|
|
91
|
+
|
|
92
|
+
Button that opens edit mode, with `aria-expanded` tied to the mode. Styled as a link by the store.
|
|
93
|
+
|
|
94
|
+
| Prop | Type | Default | Notes |
|
|
95
|
+
|------|------|---------|-------|
|
|
96
|
+
| `label` | string | — | For example `"Trocar localização?"`; authored content wins |
|
|
97
|
+
|
|
98
|
+
**Content model** (`cart-destination-change`): `label`.
|
|
99
|
+
|
|
100
|
+
### CartDestinationForm
|
|
101
|
+
|
|
102
|
+
Renders only in edit mode. Enter in the field runs the submit, which flags an incomplete or unchanged draft instead of sending it. Opening it focuses the field; leaving it focuses `CartDestinationChange`.
|
|
103
|
+
|
|
104
|
+
### CartDestinationPostalCodeControl
|
|
105
|
+
|
|
106
|
+
A `PostalCodeField` bound to the draft, empty every time edit mode opens.
|
|
107
|
+
|
|
108
|
+
| Prop | Type | Default | Notes |
|
|
109
|
+
|------|------|---------|-------|
|
|
110
|
+
| `mask` | string | the country's mask | `99999-999` for BRA; also the mask Submit checks the draft against, and the typed value must still match the country's pattern, so a mask the pattern rejects (9 digits in BRA, for example) keeps Submit disabled |
|
|
111
|
+
| `label` | string | — | Field label |
|
|
112
|
+
| `placeholder` | string | — | Field placeholder |
|
|
113
|
+
| `invalidMessage` | string | — | Shown for a draft that is incomplete or fails the country's pattern |
|
|
114
|
+
|
|
115
|
+
### CartDestinationSubmit
|
|
116
|
+
|
|
117
|
+
Writes the typed postal code. Disabled until the draft is complete for the field's mask (the control's `mask`, else the country's), matches its pattern and differs from the current postal code, and while a write is pending (`aria-busy`). A pending write also waits for any cart line write in flight.
|
|
118
|
+
|
|
119
|
+
| Prop | Type | Default | Notes |
|
|
120
|
+
|------|------|---------|-------|
|
|
121
|
+
| `label` | string | — | For example `"Atualizar"` |
|
|
122
|
+
| `pendingLabel` | string | — | Shown while the write is in flight |
|
|
123
|
+
|
|
124
|
+
**Content model** (`cart-destination-submit`): `label`, `pendingLabel`.
|
|
125
|
+
|
|
126
|
+
### CartDestinationCancel
|
|
127
|
+
|
|
128
|
+
Closes edit mode, discards the draft and any error, and writes nothing. Disabled while a write is pending.
|
|
129
|
+
|
|
130
|
+
| Prop | Type | Default | Notes |
|
|
131
|
+
|------|------|---------|-------|
|
|
132
|
+
| `label` | string | — | For example `"Cancelar"` |
|
|
133
|
+
|
|
134
|
+
**Content model** (`cart-destination-cancel`): `label`.
|
|
135
|
+
|
|
136
|
+
### CartDestinationError
|
|
137
|
+
|
|
138
|
+
An alert announcing a failed write; renders nothing otherwise. A refused postal code and a network failure reach the client as the same error, so author one message that covers both.
|
|
139
|
+
|
|
140
|
+
| Prop | Type | Default | Notes |
|
|
141
|
+
|------|------|---------|-------|
|
|
142
|
+
| `genericMessage` | string | — | Authored message; the server's own is the fallback |
|
|
143
|
+
|
|
144
|
+
**Content model** (`cart-destination-error`): `genericMessage`.
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## Canonical schema example
|
|
149
|
+
|
|
150
|
+
```json
|
|
151
|
+
{
|
|
152
|
+
"cart-destination": {
|
|
153
|
+
"type": "CartDestinationRoot",
|
|
154
|
+
"style": ["flex", "flex-col", "gap-2", "text-sm"],
|
|
155
|
+
"children": ["cart-destination-read", "cart-destination-form"]
|
|
156
|
+
},
|
|
157
|
+
"cart-destination-read": {
|
|
158
|
+
"type": "CartDestinationRead",
|
|
159
|
+
"style": ["flex", "items-center", "gap-2"],
|
|
160
|
+
"children": ["cart-destination-value", "cart-destination-unset", "cart-destination-change"]
|
|
161
|
+
},
|
|
162
|
+
"cart-destination-value": { "type": "CartDestinationValue", "props": { "prefix": "Entregando em" }, "style": ["flex", "gap-1"] },
|
|
163
|
+
"cart-destination-unset": { "type": "CartDestinationUnset", "children": ["cart-destination-invitation"] },
|
|
164
|
+
"cart-destination-invitation": { "type": "Text", "props": { "as": "span", "children": "Informe seu CEP" } },
|
|
165
|
+
"cart-destination-change": {
|
|
166
|
+
"type": "CartDestinationChange",
|
|
167
|
+
"props": { "label": "Trocar localização?" },
|
|
168
|
+
"style": ["text-[#2563eb]", "font-medium", "cursor-pointer"]
|
|
169
|
+
},
|
|
170
|
+
"cart-destination-form": {
|
|
171
|
+
"type": "CartDestinationForm",
|
|
172
|
+
"style": ["flex", "flex-col", "gap-2"],
|
|
173
|
+
"children": [
|
|
174
|
+
"cart-destination-control",
|
|
175
|
+
"cart-destination-submit",
|
|
176
|
+
"cart-destination-cancel",
|
|
177
|
+
"cart-destination-help",
|
|
178
|
+
"cart-destination-error"
|
|
179
|
+
]
|
|
180
|
+
},
|
|
181
|
+
"cart-destination-control": { "type": "CartDestinationPostalCodeControl", "props": { "label": "CEP" } },
|
|
182
|
+
"cart-destination-submit": { "type": "CartDestinationSubmit", "props": { "label": "Atualizar", "pendingLabel": "Atualizando…" } },
|
|
183
|
+
"cart-destination-cancel": { "type": "CartDestinationCancel", "props": { "label": "Cancelar" } },
|
|
184
|
+
"cart-destination-help": {
|
|
185
|
+
"type": "Text",
|
|
186
|
+
"props": { "as": "span", "children": "Após confirmar, a barra volta ao modo leitura com o novo CEP e cidade." }
|
|
187
|
+
},
|
|
188
|
+
"cart-destination-error": {
|
|
189
|
+
"type": "CartDestinationError",
|
|
190
|
+
"props": { "genericMessage": "Não foi possível usar esse CEP. Confira o número e tente de novo." },
|
|
191
|
+
"style": ["text-sm", "text-red-700"]
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
---
|
|
197
|
+
|
|
198
|
+
## Styling hooks
|
|
199
|
+
|
|
200
|
+
| Attribute | On | Present when |
|
|
201
|
+
|---|---|---|
|
|
202
|
+
| `data-fs-cart-destination` | `CartDestinationRoot` | Always |
|
|
203
|
+
| `data-fs-cart-destination-editing` | `CartDestinationRoot` | Edit mode is open |
|
|
204
|
+
| `data-fs-cart-destination-pending` | `CartDestinationRoot` | A write is in flight |
|
|
205
|
+
| `data-fs-cart-destination-read` | `CartDestinationRead` | Read mode |
|
|
206
|
+
| `data-fs-cart-destination-value` | `CartDestinationValue` | A destination is set |
|
|
207
|
+
| `data-fs-cart-destination-value-prefix`, `-postal-code`, `-city`, `-state` | The piece's element | The piece exists |
|
|
208
|
+
| `data-fs-cart-destination-unset` | `CartDestinationUnset` | No destination is set |
|
|
209
|
+
| `data-fs-cart-destination-change` | `CartDestinationChange` | Always |
|
|
210
|
+
| `data-fs-cart-destination-form` | `CartDestinationForm` | Edit mode |
|
|
211
|
+
| `data-fs-cart-destination-postal-code-control` | `CartDestinationPostalCodeControl` | Always |
|
|
212
|
+
| `data-fs-cart-destination-submit`, `-submit-pending` | `CartDestinationSubmit` | Always, and while a write is in flight |
|
|
213
|
+
| `data-fs-cart-destination-cancel` | `CartDestinationCancel` | Always |
|
|
214
|
+
| `data-fs-cart-destination-error` | `CartDestinationError` | The latest write failed |
|
|
215
|
+
|
|
216
|
+
No part ships CSS of its own. Spacing, the link look of the change control and the two modes' boxes are swatch decisions; use `data-fs-cart-destination-editing` to style them apart.
|
|
217
|
+
|
|
218
|
+
---
|
|
219
|
+
|
|
220
|
+
## Common mistakes
|
|
221
|
+
|
|
222
|
+
- **Inventing `CartDestination` (without `Root`).** The provider is `CartDestinationRoot`.
|
|
223
|
+
- **Using `RegionRoot` on the cart page.** `Region` only sets the header cookie; it never changes the cart destination, so the free-shipping bar never moves.
|
|
224
|
+
- **Placing `CartDestinationValue` or `CartDestinationChange` outside `CartDestinationRead`.** They stay on screen while the form is open.
|
|
225
|
+
- **Placing the postal code control, `CartDestinationSubmit` or `CartDestinationCancel` outside `CartDestinationForm`.** Only the form is bound to edit mode: outside it they render in read mode too, and Enter in the field no longer reaches the submit.
|
|
226
|
+
- **Looking for an `open` prop or a controller.** The root owns edit mode; nothing outside it can open it.
|
|
227
|
+
- **Omitting `CartDestinationUnset`.** With no destination the page then shows only the change link and no invitation.
|
|
228
|
+
- **Expecting the Value to show what the shopper just typed.** It shows what the cart confirms.
|
|
229
|
+
- **Authoring a "CEP não encontrado" message.** A refused code and a network failure are the same error today: author one message in `CartDestinationError`.
|
|
230
|
+
|
|
231
|
+
---
|
|
232
|
+
|
|
233
|
+
## Related
|
|
234
|
+
|
|
235
|
+
- [[../free-shipping-progress/free-shipping-progress]] — the bar beside this component; it follows the destination write through the cache with no wiring.
|
|
236
|
+
- [[../region/region]] — the header's region control; same postal code field, but a cookie and no cart write.
|
|
237
|
+
- [[../shipping-address/shipping-address]] — the full delivery address on checkout, which this component's write shares with.
|
|
238
|
+
- [[../../particles/postal-code-field/postal-code-field]] — the masked input this component composes.
|
package/dist/docs/cart-item.md
CHANGED
|
@@ -39,6 +39,15 @@ CartList (iterator — renders its children once per cart line)
|
|
|
39
39
|
- **`CartItemRoot` is required.** Every `CartItem*` core particle reads its data from the nearest `CartItemRoot` (build-time `requiresContext: 'cart-item-data'`). Placing a core particle directly inside `CartList` with no `CartItemRoot` between them is a build error. When authoring a cart or mini-cart line template, always insert a `CartItemRoot` between the `CartList` and the row's particles — it carries no visible styling of its own, so put the row layout classes on it (or on a `Container` inside it).
|
|
40
40
|
- **`CartItemRoot` must be inside `CartList`.** It requires the `cart-item` iteration context that `CartList` provides.
|
|
41
41
|
|
|
42
|
+
## Image placeholder
|
|
43
|
+
|
|
44
|
+
`CartItemImage` renders the `<img>` while the line has a usable image. It renders a placeholder `span` in the same slot when:
|
|
45
|
+
|
|
46
|
+
- the line has no image, or its URL is blank; or
|
|
47
|
+
- the image fails to load, including a failure that happened before hydration (an image already settled with no pixels at mount is treated as failed).
|
|
48
|
+
|
|
49
|
+
The placeholder carries `data-fs-cart-item-image` and `data-fs-cart-item-image-broken`, and not `data-fs-image` because it is not an image element. It takes the image's `className` and `style`, so the slot keeps its size. It is hidden from assistive technology (`aria-hidden`, no role or name), so only a loaded image is announced. It has no built-in look: style it with a swatch keyed on the hook, for example `data-[fs-cart-item-image-broken]:bg-gray-100`. The placeholder keeps the image's display (`inline-block`, set inline) so the sizing classes keep working with no extra utility; a swatch overrides it only with an important utility, and the look stays the swatch's. A failure is remembered only for the URL that failed, so a line whose URL changes tries the new one. A consumer `onError` still runs when the browser delivers the `error` event; a failure found at mount is not reported through it. A `ref` reaches the `<img>` only.
|
|
50
|
+
|
|
42
51
|
## PDP link composition
|
|
43
52
|
|
|
44
53
|
`CartItemProductLink` links a line to its product page. It reads the `product.detailUrl` the core slice already carries, so it owns no fragment and placing it adds nothing to the page query. Wrap the image in it:
|
|
@@ -46,12 +55,12 @@ CartList (iterator — renders its children once per cart line)
|
|
|
46
55
|
```text
|
|
47
56
|
CartItemRoot
|
|
48
57
|
└─ CartItemProductLink (anchor to this line's product page)
|
|
49
|
-
└─ CartItemImage (
|
|
58
|
+
└─ CartItemImage (the link names itself with aria-label; see below)
|
|
50
59
|
```
|
|
51
60
|
|
|
52
|
-
**Placing it is the switch.** There is no prop to turn the link on or off: a mini-cart line composes it, a checkout line omits it. That is why `CartItemImage` on its own
|
|
61
|
+
**Placing it is the switch.** There is no prop to turn the link on or off: a mini-cart line composes it, a checkout line omits it. That is why `CartItemImage` on its own renders the `<img>` (or, with no usable image, the placeholder described below) and never an anchor.
|
|
53
62
|
|
|
54
|
-
**It names itself
|
|
63
|
+
**It names itself.** The link sets `aria-label` to the image's alt text (`image.altText ?? variant.name`, trimmed), so it keeps that name when the wrapped image is replaced by the hidden placeholder (a load error, or a blank URL). A loaded image gives the same name as before. An `aria-label` passed to the link wins over it. On a line whose `variant.image` is `null`, or whose name is blank, the leaf still renders its children unwrapped instead of wrapping nothing, because an anchor with no accessible name is a keyboard focus stop.
|
|
55
64
|
|
|
56
65
|
**No usable product URL, no link.** The leaf runs `detailUrl` through `resolveSafeHref`, the same allowlist the navigation particle applies to CMS-authored destinations, and renders its children unwrapped when the destination is blank or refused: `javascript:`, `data:`, protocol-relative, or malformed. A safe `detailUrl` is a logical path: `NavigationLink` prefixes it for the current binding. The particle gates only `$content.href`, so a platform URL arriving on the `href` prop is this leaf's to validate. Either way the image still shows: the leaf never self-hides, unlike the data-bearing leaves below, because hiding a wrapper would take its content with it. A refusal logs its reason and never the value and still renders the children unwrapped; a blank URL is ordinary and stays quiet.
|
|
57
66
|
|
|
@@ -76,6 +85,15 @@ CartItemRoot
|
|
|
76
85
|
|
|
77
86
|
For the cart-level counterparts (the "N items unavailable" summary and the bulk "remove all unavailable" button), see `cart.md` — Unavailable-item recovery.
|
|
78
87
|
|
|
88
|
+
## Removal feedback
|
|
89
|
+
|
|
90
|
+
`CartItemRemove` awaits the removal and tells the shopper when it went through:
|
|
91
|
+
|
|
92
|
+
- **Disabled while pending.** The button carries the native `disabled` attribute from the click until `removeItem` settles, so a second click sends no second request. A caller-passed `disabled` is kept.
|
|
93
|
+
- **Toast on a clean result only.** When `removeItem` resolves with an empty `userErrors` list, the button raises one `success` toast titled `removedMessage` (default `Item removed`, editable per store). A non-empty list, a missing `removeItem`, or a rejection raises no toast; failures keep surfacing through `CartError`.
|
|
94
|
+
- **`removedMessage`** is the only schema prop. The toast carries the title alone: no description, no action.
|
|
95
|
+
- **Needs a `ToastProvider`.** The toast shows through the layout's `ToastList`. Without a `ToastProvider` above the line, no toast shows and the removal still works.
|
|
96
|
+
|
|
79
97
|
## Gift lines
|
|
80
98
|
|
|
81
99
|
A promotion can add a line the shopper never chose. `CartItem.gift` marks it, and three things follow from that one flag — none of them requiring a different template, because `CartList` renders the same children for every row:
|
|
@@ -49,7 +49,7 @@ CartTotalizerRoot ← reactive data provider (totalizers + total)
|
|
|
49
49
|
| Component | Props | Notes |
|
|
50
50
|
|---|---|---|
|
|
51
51
|
| `CartTotalizerRoot` | `$data` | Provides `CartTotalizerContext` and `CartTotalValueContext`; lines format from the active storefront context |
|
|
52
|
-
| `CartTotalizerLine` | `totalizerId`, `label?`, `variant?`, `zeroLabel?` | `variant="discount"` auto-hides when
|
|
52
|
+
| `CartTotalizerLine` | `totalizerId`, `label?`, `variant?`, `zeroLabel?` | `variant="discount"` auto-hides when the amount is numerically zero, whatever its wire form (`0`, `"0.00"`, `"-0.00"`); in any variant the line renders nothing when the amount is not a finite number |
|
|
53
53
|
| `CartDiscountLine` | `otherLabel?` | Itemized discount breakdown: one line per promotion/coupon plus the residual. Self-hides when empty. Owns its own `Cart.discounts` slice |
|
|
54
54
|
| `CartMarkdownLine` | `label?` | Aggregate item-level catalog markdown (`Σ listPrice − price`): a single negative line, self-hides at zero. Distinct money from the `Discounts` totalizer / `CartDiscountLine` — may be shown alongside them. Owns its own `Cart.itemMarkdownTotal` slice |
|
|
55
55
|
| `CartTotal` | `label?` | The single root-agnostic grand total; reads `CartTotalValueContext` provided by either root. Set `label` to render a labeled total row (e.g. "Total") |
|
package/dist/docs/cart.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
Cart **writes** (add / remove / quantity) no longer flow through `CartRoot`. A component declares the cart methods it needs as an `actions: { cart: [...] }` selector on its definition and dispatches through the compiler-injected `$actions.cart.*` prop — the write-side mirror of `fragment` → `$data`. Cart writes are **context-free**: an add-to-cart button works on a PDP, a product card, a search result, or the header with **no `CartRoot` ancestor** (docs/storefront-core/reference-free-particle-model.md § Write side — actions, ADR-0010).
|
|
7
7
|
|
|
8
|
-
This doc covers `CartRoot` — hosted in the layout, never emitted in page schemas — plus the **mini-cart footer checkout CTA** ([below](#mini-cart-footer-checkout-cta)), the composition of `CartCheckoutGuard` → (`CartTotal` + `CartGoCheckout`). The remaining cart line-item and summary particles (`CartList`, `CartItem*`, `
|
|
8
|
+
This doc covers `CartRoot` — hosted in the layout, never emitted in page schemas — plus the **mini-cart footer checkout CTA** ([below](#mini-cart-footer-checkout-cta)), the composition of `CartCheckoutGuard` → (`CartTotal` + `CartGoCheckout`). The remaining cart line-item and summary particles (`CartList`, `CartItem*`, `CartError`) exist in the catalog and will be documented when the full cart page enters scope. `CartCount` is documented in [Item count](#item-count).
|
|
9
9
|
|
|
10
10
|
---
|
|
11
11
|
|
|
@@ -61,7 +61,7 @@ Cart **write** consumers (`BuyButtonButton`, `ProductCardAddToCart`, `CartItemQu
|
|
|
61
61
|
|
|
62
62
|
## Post-add behavior (open the mini-cart vs. redirect to checkout)
|
|
63
63
|
|
|
64
|
-
What happens after a successful add to cart is a **per-store choice made by composition**, not a config flag. The store composes exactly one of two behavior components — there is no
|
|
64
|
+
What happens after a successful add to cart is a **per-store choice made by composition**, not a config flag. The store composes exactly one of two behavior components — there is no foundation switch deciding it.
|
|
65
65
|
|
|
66
66
|
- **Mini-cart store → `CartOpenOnAdd`.** When the store has a mini-cart drawer, compose `CartOpenOnAdd` as the **parent of the mini-cart `DrawerRoot`** (its single child). It opens the drawer automatically on each successful add, with the new item visible and the count updated. It controls the drawer internally, so **do not set raw `open` / `onOpenChange` on `DrawerRoot`** — those are not authorable props, and the header `DrawerTrigger` plus manual close (Escape / backdrop / close button) keep working through the encapsulated wiring.
|
|
67
67
|
|
|
@@ -145,22 +145,28 @@ It is the drawer's own close control, so it **requires a `DrawerRoot` ancestor**
|
|
|
145
145
|
|
|
146
146
|
## Coupon
|
|
147
147
|
|
|
148
|
-
|
|
148
|
+
Six root-agnostic components wire up the cart coupon flow — apply a code, list the applied coupons, remove one, surface apply/remove errors, gate the entry on a single-coupon store, and tell the shopper when an applied coupon is not being applied. They read `useCart()` (for the applied list) and dispatch through `$actions.cart.applyCoupon` / `$actions.cart.removeCoupon`, so they compose anywhere inside a `CartRoot` subtree. In a mini-cart they sit in the `DrawerPopup` footer, above the totals.
|
|
149
149
|
|
|
150
150
|
The applied coupons are a **list of chips (1..N)**, not a single-coupon slot — forward-compatible with multi-coupon stores. The server recomputes totals on every apply/remove and returns the updated cart, so `CartTotal` and the totalizer rows update automatically; no optimistic state is used.
|
|
151
151
|
|
|
152
152
|
| Component | Element | Purpose | Notes |
|
|
153
153
|
|-----------|---------|---------|-------|
|
|
154
|
-
| `CartCouponForm` | `form` | Coupon code input + apply button | Dispatches `$actions.cart.applyCoupon`. Disables input + button and exposes `data-fs-cart-coupon-pending` while an apply is in flight; clears the input on success. |
|
|
155
|
-
| `CartAppliedCoupons` | `div` | Renders one chip per applied coupon | Reads `cart.appliedCoupons`; renders nothing when empty (no container). Each chip shows the `code` inline and a `CartCouponRemove`. An applied-but-no-discount coupon (`isUnmatchedCondition`) is exposed via the `data-fs-cart-coupon-unmatched` hook for the store to style/label (no baked copy). |
|
|
154
|
+
| `CartCouponForm` | `form` | Coupon code input + apply button | Dispatches `$actions.cart.applyCoupon`. Disables input + button and exposes `data-fs-cart-coupon-pending` while an apply is in flight; clears the input on success. Raises one success toast titled `appliedMessage` (default `Coupon applied`) only when the apply returns no errors and the submitted code comes back matched (compared ignoring case); an unmatched code, an error or a blank or `null` `appliedMessage` raises none. The toast still shows when `CartCouponEmpty` removes the form, and nothing shows without a `ToastProvider` (the apply still works). |
|
|
155
|
+
| `CartAppliedCoupons` | `div` | Renders one chip per applied coupon | Reads `cart.appliedCoupons`; renders nothing when empty (no container). Each chip shows the `code` inline and a `CartCouponRemove`. An applied-but-no-discount coupon (`isUnmatchedCondition`) is exposed via the `data-fs-cart-coupon-unmatched` hook for the store to style/label (no baked copy); compose `CartCouponUnmatched` in the chip template for a text notice. A chip template replaces the built-in remove control, so one that composes the notice must also compose `CartCouponRemove`. |
|
|
156
156
|
| `CartCouponRemove` | `button` | The per-chip remove control (the X) | Dispatches `$actions.cart.removeCoupon` for the chip's code. Use inside a `CartAppliedCoupons` chip, which supplies the code via context. |
|
|
157
157
|
| `CartCouponError` | `div` (`role="alert"`) | Inline coupon error banner | Reads a **dedicated** coupon error channel, separate from `CartError` (which carries item-write errors). Renders the first error, or nothing when there are none. |
|
|
158
|
+
| `CartCouponEmpty` | `div` | Coupon entry gate | Renders its children only while the resolved cart has no applied coupon; renders nothing while the cart loads or once any coupon (matched or not) is applied. Copy-free, no schema props. Wrap the coupon entry in it for a single-coupon store; omit it for a multi-coupon store. |
|
|
159
|
+
| `CartCouponUnmatched` | `output` | Per-chip notice for an unmatched coupon | Reads the chip's coupon from context; renders the `message` copy only while that coupon has `isUnmatchedCondition`, and nothing when it matches, outside a chip, or when the copy is blank. `children` override `message`. Never removes the coupon. Not part of the built-in chip: compose it next to `CartCouponRemove`. |
|
|
160
|
+
| `CartCouponSavings` | `span` | Per-chip amount a fixed-amount coupon took off | Reads the chip's coupon from context and sums the `COUPON` entries of `cart.discounts` named after its code; renders the amount with the currency's own minus sign (`-R$ 20,00`), and nothing for an unmatched, free-shipping or percentage coupon (the API attributes no amount to them), outside a chip, or when the sum is not above zero. `template` words the text (default `{amount}`): every `{amount}` is the formatted saving and every `{code}` the chip's code; other `{…}` text stays literal, and a blank template counts as unset. The chip already shows the code before its children, so `{code}` repeats it. Not part of the built-in chip: compose it next to `CartCouponRemove`. |
|
|
158
161
|
|
|
159
|
-
`CartAppliedCoupons` repeats its `children` once per applied coupon (like `CartList` repeats per item): place the chip template — an optional code element plus a `CartCouponRemove` — as its children, and it is cloned per coupon with the coupon supplied via context. Omit `children` to get a built-in chip (code inline + a remove X).
|
|
162
|
+
`CartAppliedCoupons` repeats its `children` once per applied coupon (like `CartList` repeats per item): place the chip template — an optional code element plus a `CartCouponRemove` — as its children, and it is cloned per coupon with the coupon supplied via context. Omit `children` to get a built-in chip (code inline + a remove X). A template that composes `CartCouponUnmatched` or `CartCouponSavings` must also compose `CartCouponRemove`, because composed children replace the built-in remove control.
|
|
160
163
|
|
|
161
|
-
**Errors:** coupon apply/remove `userErrors` (e.g. `COUPON_NOT_FOUND`, `COUPON_EXPIRED`) publish to a dedicated reactive var that only `CartCouponError` reads, so coupon feedback never collides with item-write errors shown by `CartError`.
|
|
164
|
+
**Errors:** coupon apply/remove `userErrors` (e.g. `COUPON_NOT_FOUND`, `COUPON_EXPIRED`) publish to a dedicated reactive var that only `CartCouponError` reads, so coupon feedback never collides with item-write errors shown by `CartError`. A code rejected while another coupon is applied returns `COUPON_NOT_COMBINED` (whatever the account allows) with a neutral message, and the applied coupons stay; a store can override the text with `CartCouponError` children. `cartCouponErrorsVar` is cleared when a coupon write starts (item writes and `validate` leave it alone) and when a CartRoot mounts, never on re-render, so a rejected code from a layout mini-cart form does not show under the Cart page's coupon form. Two cases stay:
|
|
162
165
|
|
|
163
|
-
|
|
166
|
+
- A coupon apply still in flight when a CartRoot mounts shows its failure after the clear, for example from a layout mini-cart form (known limitation).
|
|
167
|
+
- An error from the Cart page's form stays visible in a persistent `CartCouponError` after the shopper leaves (known limitation).
|
|
168
|
+
|
|
169
|
+
**Styling split:** these components own behaviour and the `data-fs-*` hooks (`data-fs-cart-coupon-form`, `-input`, `-apply`, `-pending`, `data-fs-cart-applied-coupons`, `data-fs-cart-coupon`, `-code`, `-unmatched`, `data-fs-cart-coupon-remove`, `data-fs-cart-coupon-unmatched-notice`, `data-fs-cart-coupon-savings`, `data-fs-cart-coupon-error`, `data-fs-cart-coupon-empty`). Chip and X **visuals (swatch values) are authored by the styling pipeline**, not these components.
|
|
164
170
|
|
|
165
171
|
Coupon block inside a mini-cart `DrawerPopup` footer, at the top of the footer above the totals and checkout CTA (the footer lives inside the `CartFilled` branch — coupons only matter for a non-empty cart):
|
|
166
172
|
|
|
@@ -168,13 +174,66 @@ Coupon block inside a mini-cart `DrawerPopup` footer, at the top of the footer a
|
|
|
168
174
|
{
|
|
169
175
|
"popup-footer": { "type": "Container", "children": ["coupon-block", "footer-summary", "checkout-cta", "footer-keep-shopping"] },
|
|
170
176
|
"coupon-block": { "type": "Container", "children": ["coupon-form", "applied-coupons", "coupon-error"] },
|
|
171
|
-
"coupon-form": { "type": "CartCouponForm", "props": { "placeholder": "Coupon code", "applyLabel": "Apply" } },
|
|
177
|
+
"coupon-form": { "type": "CartCouponForm", "props": { "placeholder": "Coupon code", "applyLabel": "Apply", "appliedMessage": "Coupon applied" } },
|
|
178
|
+
"applied-coupons": { "type": "CartAppliedCoupons", "children": ["coupon-remove"] },
|
|
179
|
+
"coupon-remove": { "type": "CartCouponRemove" },
|
|
180
|
+
"coupon-error": { "type": "CartCouponError" }
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### Single- and multi-coupon entry
|
|
185
|
+
|
|
186
|
+
Whether a store accepts one coupon or several is expressed by composition, with no flag. For a single-coupon store, wrap the collapsed entry (the accordion holding `CartCouponForm`) in `CartCouponEmpty`: the trigger and form disappear as soon as a code is applied and come back, collapsed, when the chip is removed. For a multi-coupon store, leave the gate out so the entry stays available for more codes, each shown as its own chip. `CartAppliedCoupons` and `CartCouponError` sit outside the gate in both compositions, so the chips and errors stay visible.
|
|
187
|
+
|
|
188
|
+
Single-coupon:
|
|
189
|
+
|
|
190
|
+
```json
|
|
191
|
+
{
|
|
192
|
+
"coupon-section": { "type": "Container", "children": ["coupon-entry-gate", "applied-coupons", "coupon-error"] },
|
|
193
|
+
"coupon-entry-gate": { "type": "CartCouponEmpty", "children": ["coupon-accordion"] },
|
|
194
|
+
"coupon-accordion": { "type": "AccordionRoot", "children": ["coupon-accordion-item"] },
|
|
195
|
+
"applied-coupons": { "type": "CartAppliedCoupons", "children": ["coupon-remove"] },
|
|
196
|
+
"coupon-remove": { "type": "CartCouponRemove" },
|
|
197
|
+
"coupon-error": { "type": "CartCouponError" }
|
|
198
|
+
}
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
Chip template with the saved amount and the unmatched-coupon notice (compose `CartCouponRemove` too, since composed children replace the built-in remove):
|
|
202
|
+
|
|
203
|
+
```json
|
|
204
|
+
{
|
|
205
|
+
"applied-coupons": { "type": "CartAppliedCoupons", "children": ["coupon-savings", "coupon-remove", "coupon-unmatched"] },
|
|
206
|
+
"coupon-savings": { "type": "CartCouponSavings", "props": { "template": "Economia de {amount}" } },
|
|
207
|
+
"coupon-remove": { "type": "CartCouponRemove", "props": { "children": "×" } },
|
|
208
|
+
"coupon-unmatched": { "type": "CartCouponUnmatched", "props": { "message": "Este cupom não está sendo aplicado" } }
|
|
209
|
+
}
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
Multi-coupon (no gate):
|
|
213
|
+
|
|
214
|
+
```json
|
|
215
|
+
{
|
|
216
|
+
"coupon-section": { "type": "Container", "children": ["coupon-accordion", "applied-coupons", "coupon-error"] },
|
|
217
|
+
"coupon-accordion": { "type": "AccordionRoot", "children": ["coupon-accordion-item"] },
|
|
172
218
|
"applied-coupons": { "type": "CartAppliedCoupons", "children": ["coupon-remove"] },
|
|
173
219
|
"coupon-remove": { "type": "CartCouponRemove" },
|
|
174
220
|
"coupon-error": { "type": "CartCouponError" }
|
|
175
221
|
}
|
|
176
222
|
```
|
|
177
223
|
|
|
224
|
+
`coupon-accordion-item` is the usual `AccordionItem > AccordionHeader > AccordionTrigger` plus `AccordionPanel > CartCouponForm`:
|
|
225
|
+
|
|
226
|
+
```json
|
|
227
|
+
{
|
|
228
|
+
"coupon-accordion-item": { "type": "AccordionItem", "props": { "value": "coupon" }, "children": ["coupon-accordion-header", "coupon-accordion-panel"] },
|
|
229
|
+
"coupon-accordion-header": { "type": "AccordionHeader", "children": ["coupon-accordion-trigger"] },
|
|
230
|
+
"coupon-accordion-trigger": { "type": "AccordionTrigger", "children": ["coupon-accordion-trigger-label"] },
|
|
231
|
+
"coupon-accordion-trigger-label": { "type": "Text", "props": { "as": "span", "children": "Have a coupon?" } },
|
|
232
|
+
"coupon-accordion-panel": { "type": "AccordionPanel", "children": ["coupon-form"] },
|
|
233
|
+
"coupon-form": { "type": "CartCouponForm" }
|
|
234
|
+
}
|
|
235
|
+
```
|
|
236
|
+
|
|
178
237
|
---
|
|
179
238
|
|
|
180
239
|
## Canonical schema example
|
|
@@ -342,11 +401,34 @@ Everything stays a layout composition: the guard, total, and link are independen
|
|
|
342
401
|
|
|
343
402
|
---
|
|
344
403
|
|
|
404
|
+
## Item count
|
|
405
|
+
|
|
406
|
+
`CartCount` renders `<span data-fs-cart-count>` with the number of units in the cart: the sum of `quantity` over the non-gift lines (two lines of 1 read 2; one line of 3 reads 3; a gift-only cart reads 0). Must be inside `CartRoot`.
|
|
407
|
+
|
|
408
|
+
- `template` — the output format, `{count}` replaced by the count; default `'{count}'`. Children override the text.
|
|
409
|
+
- `templateOne` — the output format used **only when the count is exactly 1**. It has no default: when unset, or blank, `template` is used at 1 as well. **0 uses `template`**, never `templateOne`, because the selector is the literal count and not a locale plural rule (in `pt-BR` the plural rule would call 0 singular).
|
|
410
|
+
- A count that is not a finite number renders **nothing**. A non-gift line whose `quantity` is missing, `null`, `NaN`, infinite or a string makes the count unreliable, so the span is not rendered, even with `children`; a partial sum would undercount. 0 is finite: an empty or gift-only cart still renders `0`.
|
|
411
|
+
|
|
412
|
+
`CartUnavailableCount` takes the same `template` and `templateOne` pair (see [below](#unavailable-item-recovery)), selected on the number of unavailable lines.
|
|
413
|
+
|
|
414
|
+
```json
|
|
415
|
+
{
|
|
416
|
+
"cart-title-count": {
|
|
417
|
+
"type": "CartCount",
|
|
418
|
+
"props": { "template": "{count} itens", "templateOne": "{count} item" }
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
Only the first `{count}` in a template is replaced.
|
|
424
|
+
|
|
425
|
+
---
|
|
426
|
+
|
|
345
427
|
## Unavailable-item recovery
|
|
346
428
|
|
|
347
429
|
`CartCheckoutGuard` blocks checkout while any line is not `AVAILABLE`; the recovery affordances let the shopper resolve the block from inside the cart. Three pieces, all reading the availability already on the cart:
|
|
348
430
|
|
|
349
|
-
- **`CartUnavailableCount`** — cart-level summary of how many lines are unavailable (e.g. "2 items unavailable"). Renders a `<span data-fs-cart-unavailable-count>` with a `template` prop (`{count}` is replaced by the integer count; default `'{count} items unavailable'`; children override the text). **Self-hides** (`null`) when every line is available — including while the cart is still loading — so it never shows a false "0 items unavailable". Must be inside `CartRoot`.
|
|
431
|
+
- **`CartUnavailableCount`** — cart-level summary of how many lines are unavailable (e.g. "2 items unavailable"). Renders a `<span data-fs-cart-unavailable-count>` with a `template` prop (`{count}` is replaced by the integer count; default `'{count} items unavailable'`; children override the text) and an optional `templateOne` used only when exactly one line is unavailable (it falls back to `template` when unset or blank). **Self-hides** (`null`) when every line is available — including while the cart is still loading — so it never shows a false "0 items unavailable". Must be inside `CartRoot`.
|
|
350
432
|
- **`CartRemoveUnavailable`** — the bulk "remove all unavailable" button (`<button data-fs-cart-remove-unavailable>`). On click it dispatches **one** `$actions.cart.removeItems({ ids })` call carrying every unavailable line id: one mutation, one upstream write, one cache update — no per-line flicker. **Self-hides** when there is nothing to remove. Children are the label (default "Remove unavailable items"). Must be inside `CartRoot` (it reads the cart for the ids; the write itself is context-free).
|
|
351
433
|
- **Per-item remove-on-alert** — a `CartItemRemove` nested inside `CartItemUnavailable` in the line-item template, so each unavailable alert carries its own remove control. Composition only, no new component — see `cart-item.md` (including the label-vs-children nuance: children replace the built-in label).
|
|
352
434
|
|
|
@@ -365,6 +447,31 @@ Schema shape (mini-cart footer, above the checkout CTA):
|
|
|
365
447
|
|
|
366
448
|
---
|
|
367
449
|
|
|
450
|
+
## Clearing the cart
|
|
451
|
+
|
|
452
|
+
`ClearCartButton` empties the cart in one click (`<button data-fs-clear-cart-button>`). It reads every line id from the cart and dispatches **one** `$actions.cart.removeItems({ ids })` call: every line goes, gift and unavailable lines included, in cart order. No confirmation dialog and no toast. Children are the label (default "Clear cart"). Must be inside `CartRoot`.
|
|
453
|
+
|
|
454
|
+
- **Self-hides** (`null`) when the cart has no lines or has not resolved, as `CartRemoveUnavailable` does.
|
|
455
|
+
- **Disabled while pending.** The button carries the native `disabled` attribute plus `data-fs-clear-cart-button-pending` until the call settles, and a second click in the same tick is swallowed, so one click is one mutation. Style the pending look with `disabled:` classes or the pending hook. A caller `disabled` prop also disables it.
|
|
456
|
+
- **Optimistic.** `removeItems` writes an optimistic cart, so `CartEmpty` replaces `CartFilled` as soon as the button is clicked, before the server answers. The button itself unmounts at that point when it sits inside `CartFilled`.
|
|
457
|
+
- **Failures go through `CartError`.** When the call fails, Apollo drops the optimistic layer, the lines come back, and the failure is published to the same `cartUserErrorsVar` that `CartError` shows. The button owns no rollback and raises no toast. `cartUserErrorsVar` is cleared when an item write starts (coupon writes and `validate` leave it alone) and when a CartRoot mounts, never on re-render, so an error from a write on another page does not show on the Cart page. Two cases stay, listed below. A rejection (no cart cached yet) is logged by error name and re-enables the button. Compose a `CartError` outside the `CartFilled` / `CartEmpty` branches so the message is visible after the lines return.
|
|
458
|
+
- A write still in flight when a CartRoot mounts shows its failure after the clear, for example an item write from a product page (known limitation).
|
|
459
|
+
- An error from the Cart page stays visible in a persistent `CartError` after the shopper leaves (known limitation).
|
|
460
|
+
|
|
461
|
+
Schema shape (Cart page):
|
|
462
|
+
|
|
463
|
+
```json
|
|
464
|
+
{
|
|
465
|
+
"page-shell": { "type": "Container", "children": ["page-heading", "cart-error", "page-body", "cart-loading", "cart-empty"] },
|
|
466
|
+
"cart-error": { "type": "CartError" },
|
|
467
|
+
"page-body": { "type": "CartFilled", "children": ["clear-cart-row"] },
|
|
468
|
+
"clear-cart-row": { "type": "Container", "children": ["clear-cart-btn"] },
|
|
469
|
+
"clear-cart-btn": { "type": "ClearCartButton", "props": { "children": "Limpar carrinho" } }
|
|
470
|
+
}
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
---
|
|
474
|
+
|
|
368
475
|
## Merchant alert banner
|
|
369
476
|
|
|
370
477
|
`CartAlertRoot` + `CartAlertIcon` + `CartAlertText` carry the **merchant's own** message inside the overlay — a minimum order, a running promotion, a policy note. The copy is CMS-editable, so changing it needs no schema edit and no deploy.
|
|
@@ -421,7 +528,7 @@ The message, as `<span data-fs-cart-alert-text data-fs-text>`. Copy comes from t
|
|
|
421
528
|
|
|
422
529
|
### The `icons.include` obligation
|
|
423
530
|
|
|
424
|
-
`Icon` resolves names against a **compile-time** sprite with no runtime fallback. An `icon` **prop** is a literal the compiler can see, so it is collected automatically. A **CMS-chosen** name does not exist at build time, so a store whose merchants pick the icon must register the admitted names in `
|
|
531
|
+
`Icon` resolves names against a **compile-time** sprite with no runtime fallback. An `icon` **prop** is a literal the compiler can see, so it is collected automatically. A **CMS-chosen** name does not exist at build time, so a store whose merchants pick the icon must register the admitted names in `foundation.json`:
|
|
425
532
|
|
|
426
533
|
```json
|
|
427
534
|
{ "icons": { "include": ["bell", "bell-ringing", "tag", "shopping-cart", "map-pin", "star"] } }
|
|
@@ -52,7 +52,7 @@ FooterRoot
|
|
|
52
52
|
|
|
53
53
|
**A destination the allowlist refuses renders as text.** Every `url` passes through the shared render-time check, which admits `http:`, `https:`, `mailto:`, `tel:`, and site-relative paths. A refused one — a `javascript:` value, a protocol-relative `//host`, or a URL not yet pasted — renders the profile's label as visible text with no link and no glyph, rather than a mark that looks clickable and goes nowhere. The rejection logs once with the entry index and a bounded reason.
|
|
54
54
|
|
|
55
|
-
**Every predefined name must be registered.** A `platform` resolves to a sprite symbol the store's build has to include; the store's `
|
|
55
|
+
**Every predefined name must be registered.** A `platform` resolves to a sprite symbol the store's build has to include; the store's `foundation.json` writes the names out under `icons.include` — it is JSON, so it cannot import `SOCIAL_PLATFORM_NAMES`, and a test that derives the expected set from the exported constant is what keeps the two in step. A name the store never registered renders an empty `<svg>` — no build error and no runtime error, just a missing mark. `linkedin` and `youtube` need provisioned icon data as well: unlike the other three they have no embedded fallback, so a store that registers them without running `sukima icon-data fetch` fails to build.
|
|
56
56
|
|
|
57
57
|
---
|
|
58
58
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# FreeShippingProgress
|
|
2
2
|
|
|
3
3
|
**Category:** commerce
|
|
4
|
-
**Purpose:** Reactive progress bar for the cart's nearest free-shipping goal. Reads `cart.freeShippingProgress` and the
|
|
4
|
+
**Purpose:** Reactive progress bar for the cart's nearest free-shipping goal. Reads `cart.freeShippingProgress` and the cart's `Items` totalizer (the amount the server measures, so shipping and discounts do not count), then renders one of three states: below-threshold ("faltam {value} para frete grátis" + a partially filled bar), earned ("você ganhou frete grátis!" + a full bar), or absent (renders nothing). Composed at the **top of a cart / mini-cart drawer** to nudge the shopper to add a bit more.
|
|
5
5
|
|
|
6
6
|
---
|
|
7
7
|
|
|
@@ -18,19 +18,23 @@ Look for a **strip at the top of an open cart or mini-cart** with a horizontal p
|
|
|
18
18
|
|
|
19
19
|
| State | Condition | Renders |
|
|
20
20
|
|---|---|---|
|
|
21
|
-
| Below threshold | `qualified === false`,
|
|
22
|
-
| Earned | field `qualified === true`, OR the reactive
|
|
21
|
+
| Below threshold | `qualified === false`, `Items` amount below `threshold` | Below-threshold message + a bar filled to `items / threshold` |
|
|
22
|
+
| Earned | field `qualified === true`, OR the reactive `Items` amount reaches `threshold` | Earned message + a full bar |
|
|
23
23
|
| Absent (self-hide) | `cart.freeShippingProgress === null` (no destination, no eligible promotion, empty cart, still loading, or degraded) | **Nothing** — no bar, no message, no wrapper |
|
|
24
24
|
|
|
25
25
|
## Reactive fill (no extra calls)
|
|
26
26
|
|
|
27
|
-
`freeShippingProgress` is
|
|
27
|
+
`freeShippingProgress` is kept out of `CartFragment`, so only some writes select it: the four line writes (remove, remove several, increase, decrease), the coupon removal and the shipping address update. Each returns the field and the bar takes the server's answer: below the goal it shows the amount still missing, and it comes back to qualified when a later write meets the goal again. The cart's `Items` totalizer is reactive too (the optimistic builders adjust it), so between a write's optimistic prediction and the server's answer the bar derives the **live** fill and remaining from the `Items` amount against the cached `threshold`. The server compares the same `Items` amount, so shipping charges and discounts never move the bar:
|
|
28
28
|
|
|
29
|
-
- `isQualified = qualified || (threshold != null &&
|
|
30
|
-
- `liveRemaining = isQualified ? 0 : max(threshold −
|
|
31
|
-
- `fill = isQualified ? 1 : clamp(
|
|
29
|
+
- `isQualified = qualified || (threshold != null && items >= threshold)` — the field flag covers the already-earned case (where `threshold` is `null`); the reactive comparison flips it at the boundary.
|
|
30
|
+
- `liveRemaining = isQualified ? 0 : max(threshold − items, 0)`
|
|
31
|
+
- `fill = isQualified ? 1 : clamp(items / threshold, 0..1)`
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
When the cart has no usable `Items` amount (no `Items` totalizer, or a blank or non-numeric amount), the bar shows the server's own answer instead: `isQualified = qualified`, `liveRemaining = max(remaining, 0)` from `freeShippingProgress.remaining`, and `fill = (threshold − liveRemaining) / threshold`.
|
|
34
|
+
|
|
35
|
+
While a line write that lowers the items total of a qualified cart is pending, the bar hides: a qualified value has no threshold, so the client cannot tell whether the drop crosses the goal. It returns with the server's answer, and also when the write fails. An emptied cart renders nothing.
|
|
36
|
+
|
|
37
|
+
`addItem`, coupon apply, validate, shipping option select, the region postal code and the cart shipping calculator do not select the field, so they do not refresh it: after one of those the bar keeps the value of the last write that returned it (or of the page load) and derives its state from the `Items` amount. A line write costs no extra read, because the server reuses the order form the write returned; a coupon removal costs one order-form read upstream when the cart has items and a postal code; the shipping address update costs none, because the server reuses the order form it just refreshed. There is no client refetch. `Money.amount` is a string at runtime and is always `Number()`-coerced before math/formatting.
|
|
34
38
|
|
|
35
39
|
---
|
|
36
40
|
|
|
@@ -58,7 +62,7 @@ FreeShippingProgressRoot ← reactive provider; self-hides on null/loadin
|
|
|
58
62
|
|
|
59
63
|
| Component | Props | Notes |
|
|
60
64
|
|---|---|---|
|
|
61
|
-
| `FreeShippingProgressRoot` | `$data` | Provides `FreeShippingProgressContext`; the copy formats from the active storefront context; renders nothing when the field is `null` |
|
|
65
|
+
| `FreeShippingProgressRoot` | `threshold?`, `remaining?`, `qualified?`, `itemsTotal?`, `$data` | Provides `FreeShippingProgressContext`; the copy formats from the active storefront context; renders nothing when the field is `null` |
|
|
62
66
|
| `FreeShippingProgressMessage` | `label?`, `qualifiedLabel?` | `label` is a `{value}`-token template (default `"faltam {value} para frete grátis"`); `qualifiedLabel` default `"você ganhou frete grátis!"`; `children` overrides the text |
|
|
63
67
|
| `FreeShippingProgressBar` | — | Renders a `Meter` bar; `aria-valuetext` mirrors the visible copy |
|
|
64
68
|
|
package/dist/docs/incentives.md
CHANGED
|
@@ -54,7 +54,7 @@ FooterRoot
|
|
|
54
54
|
|
|
55
55
|
**A refused destination costs the anchor and nothing else.** No read-time gate enforces the model's `pattern`, so an authored `javascript:` value reaches the component intact; the same render-time allowlist `FooterLinkGroup` and the social particle use — `http:`, `https:`, `mailto:`, `tel:`, and single-slash paths — is what stands between it and an `href`. Where those two drop the entry, this one renders the item unlinked with its glyph and copy intact, because an unlinked item is this component's ordinary shape and losing a benefit over a bad destination would be the larger failure. The refusal logs the item's index and the reason, never the authored value.
|
|
56
56
|
|
|
57
|
-
**Every predefined name must be registered.** An `icon` resolves to a sprite symbol the store's build has to include; the store's `
|
|
57
|
+
**Every predefined name must be registered.** An `icon` resolves to a sprite symbol the store's build has to include; the store's `foundation.json` writes the names out under `icons.include` — it is JSON, so it cannot import `INCENTIVE_ICON_NAMES`, and a test that derives the expected set from the exported constant is what keeps the two in step. A name the store never registered renders an empty `<svg>` — no build error and no runtime error, just a missing pictogram. All five names resolve in the default collection; a store on a different collection maps them with `icons.aliases`.
|
|
58
58
|
|
|
59
59
|
---
|
|
60
60
|
|
|
@@ -120,7 +120,7 @@ Size and box classes reach both paths, but **colour does not**. A predefined gly
|
|
|
120
120
|
|---|---|---|
|
|
121
121
|
| `Incentives` | The strip itself, wrapping on narrow screens | `flex flex-wrap gap-[32px] list-none p-0 m-0` |
|
|
122
122
|
| `data-fs-incentive` | The block one benefit occupies | `flex flex-col items-center text-center gap-[4px] flex-1` |
|
|
123
|
-
| `data-fs-incentive-link` | The anchor wrapping a linked block; absent on unlinked items, so style the layout on `data-fs-incentive` and only the link affordances here | `flex flex-col items-center gap-[4px] no-underline hover:text-
|
|
123
|
+
| `data-fs-incentive-link` | The anchor wrapping a linked block; absent on unlinked items, so style the layout on `data-fs-incentive` and only the link affordances here | `flex flex-col items-center gap-[4px] no-underline hover:text-primary` |
|
|
124
124
|
| `data-fs-incentive-icon` | Both glyph paths, sized to match | `w-[32px] h-[32px] object-contain` |
|
|
125
125
|
| `data-fs-incentive-title` | The bold benefit label | `text-[14px] font-bold text-[#1a1a1a]` |
|
|
126
126
|
| `data-fs-incentive-description` | Each supporting line | `text-[12px] text-[#616161]` |
|
package/dist/docs/logo.md
CHANGED
|
@@ -117,7 +117,7 @@ Bound to a CMS entry, the element authors no content props at all — the entry
|
|
|
117
117
|
```
|
|
118
118
|
|
|
119
119
|
The image carries no size of its own, so the width above only constrains the mark once the store tells it
|
|
120
|
-
to fill the wrapper. Author that once per store as the `Logo` base swatch in `
|
|
120
|
+
to fill the wrapper. Author that once per store as the `Logo` base swatch in `foundation.json → styles`:
|
|
121
121
|
|
|
122
122
|
```json
|
|
123
123
|
{
|