@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.
Files changed (151) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/dist/actions.json +42 -0
  3. package/dist/catalog.json +731 -16
  4. package/dist/cli/index.js +1 -1
  5. package/dist/cli/index.js.map +1 -1
  6. package/dist/components.d.ts +9 -4
  7. package/dist/components.js +11 -6
  8. package/dist/content-types.json +300 -0
  9. package/dist/core/dist/define-component.d.ts +6 -1
  10. package/dist/docs/account-entry-point.md +4 -4
  11. package/dist/docs/account-menu.md +2 -2
  12. package/dist/docs/account-saved-cards.md +301 -0
  13. package/dist/docs/binding-switcher.md +154 -0
  14. package/dist/docs/card-entry.md +3 -3
  15. package/dist/docs/card-summary.md +175 -0
  16. package/dist/docs/cart-destination.md +238 -0
  17. package/dist/docs/cart-item.md +21 -3
  18. package/dist/docs/cart-totalizer.md +1 -1
  19. package/dist/docs/cart.md +118 -11
  20. package/dist/docs/footer-social.md +1 -1
  21. package/dist/docs/free-shipping-progress.md +13 -9
  22. package/dist/docs/incentives.md +2 -2
  23. package/dist/docs/logo.md +1 -1
  24. package/dist/docs/payment-method-badges.md +2 -2
  25. package/dist/docs/payment-methods.md +1 -1
  26. package/dist/docs/product-information.md +2 -2
  27. package/dist/docs/product-shelf.md +7 -0
  28. package/dist/docs/select.md +3 -4
  29. package/dist/docs/sign-in-redirect.md +1 -1
  30. package/dist/docs/toast.md +30 -12
  31. package/dist/index.d.ts +2 -1
  32. package/dist/internal/client.d.ts +4 -1
  33. package/dist/internal/client.js +4 -1
  34. package/dist/internal/server.d.ts +4 -2
  35. package/dist/internal/server.js +4 -2
  36. package/dist/matter/account-saved-cards/account-saved-cards.d.ts +383 -0
  37. package/dist/matter/account-saved-cards/account-saved-cards.js +956 -0
  38. package/dist/matter/account-saved-cards/account-saved-cards.js.map +1 -0
  39. package/dist/matter/billing-address/billing-address.js.map +1 -1
  40. package/dist/matter/cart/cart-namespace.d.ts +5 -1
  41. package/dist/matter/cart/cart-namespace.js +5 -1
  42. package/dist/matter/cart/cart-namespace.js.map +1 -1
  43. package/dist/matter/cart/cart.d.ts +34 -4
  44. package/dist/matter/cart/cart.js +238 -28
  45. package/dist/matter/cart/cart.js.map +1 -1
  46. package/dist/matter/cart-destination/cart-destination.d.ts +222 -0
  47. package/dist/matter/cart-destination/cart-destination.js +613 -0
  48. package/dist/matter/cart-destination/cart-destination.js.map +1 -0
  49. package/dist/matter/cart-item/cart-item.d.ts +4 -2
  50. package/dist/matter/cart-item/cart-item.js +75 -16
  51. package/dist/matter/cart-item/cart-item.js.map +1 -1
  52. package/dist/matter/cart-totalizer/cart-totalizer.js +3 -2
  53. package/dist/matter/cart-totalizer/cart-totalizer.js.map +1 -1
  54. package/dist/matter/free-shipping-progress/free-shipping-progress.d.ts +6 -3
  55. package/dist/matter/free-shipping-progress/free-shipping-progress.js +76 -33
  56. package/dist/matter/free-shipping-progress/free-shipping-progress.js.map +1 -1
  57. package/dist/matter/pricing/pricing.js.map +1 -1
  58. package/dist/matter/product-shelf/product-shelf.d.ts +7 -0
  59. package/dist/matter/product-shelf/product-shelf.js +115 -1
  60. package/dist/matter/product-shelf/product-shelf.js.map +1 -1
  61. package/dist/matter/shipping-address/shipping-address.js.map +1 -1
  62. package/dist/matter/shipping-simulator/shipping-simulator.d.ts +1 -1
  63. package/dist/matter/shipping-simulator/shipping-simulator.js +6 -2
  64. package/dist/matter/shipping-simulator/shipping-simulator.js.map +1 -1
  65. package/dist/particles/binding-switcher/binding-switcher.d.ts +38 -0
  66. package/dist/particles/binding-switcher/binding-switcher.js +183 -0
  67. package/dist/particles/binding-switcher/binding-switcher.js.map +1 -0
  68. package/dist/particles/card-summary/card-network.d.ts +17 -0
  69. package/dist/particles/card-summary/card-network.js +25 -0
  70. package/dist/particles/card-summary/card-network.js.map +1 -0
  71. package/dist/particles/card-summary/card-summary.d.ts +83 -0
  72. package/dist/particles/card-summary/card-summary.js +188 -0
  73. package/dist/particles/card-summary/card-summary.js.map +1 -0
  74. package/dist/particles/footer-social/footer-social.d.ts +1 -1
  75. package/dist/particles/footer-social/footer-social.js +1 -1
  76. package/dist/particles/footer-social/footer-social.js.map +1 -1
  77. package/dist/particles/incentives/incentives.d.ts +1 -1
  78. package/dist/particles/incentives/incentives.js +1 -1
  79. package/dist/particles/incentives/incentives.js.map +1 -1
  80. package/dist/particles/payment-methods/payment-methods.d.ts +1 -1
  81. package/dist/particles/payment-methods/payment-methods.js +1 -1
  82. package/dist/particles/payment-methods/payment-methods.js.map +1 -1
  83. package/dist/particles/toast/toast.d.ts +20 -6
  84. package/dist/particles/toast/toast.js +49 -7
  85. package/dist/particles/toast/toast.js.map +1 -1
  86. package/dist/schema.graphql +298 -34
  87. package/dist/sdk/actions/index.d.ts +3 -1
  88. package/dist/sdk/actions/index.js +3 -1
  89. package/dist/sdk/actions/locale-action-provider.d.ts +16 -0
  90. package/dist/sdk/actions/locale-action-provider.js +20 -0
  91. package/dist/sdk/actions/locale-action-provider.js.map +1 -0
  92. package/dist/sdk/actions/saved-card-action-provider.d.ts +12 -0
  93. package/dist/sdk/actions/saved-card-action-provider.js +16 -0
  94. package/dist/sdk/actions/saved-card-action-provider.js.map +1 -0
  95. package/dist/sdk/actions/types.d.ts +6 -2
  96. package/dist/sdk/cache/storefront-cache.js +2 -1
  97. package/dist/sdk/cache/storefront-cache.js.map +1 -1
  98. package/dist/sdk/cart/operations.d.ts +2 -1
  99. package/dist/sdk/cart/operations.js +55 -1
  100. package/dist/sdk/cart/operations.js.map +1 -1
  101. package/dist/sdk/cart/optimistic.d.ts +30 -7
  102. package/dist/sdk/cart/optimistic.js +78 -41
  103. package/dist/sdk/cart/optimistic.js.map +1 -1
  104. package/dist/sdk/cart/use-cart-actions.d.ts +7 -0
  105. package/dist/sdk/cart/use-cart-actions.js +61 -15
  106. package/dist/sdk/cart/use-cart-actions.js.map +1 -1
  107. package/dist/sdk/cart/use-cart-sync-sales-channel.js +70 -0
  108. package/dist/sdk/cart/use-cart-sync-sales-channel.js.map +1 -0
  109. package/dist/sdk/generated/gql.js +15 -11
  110. package/dist/sdk/generated/gql.js.map +1 -1
  111. package/dist/sdk/generated/graphql.d.ts +177 -46
  112. package/dist/sdk/generated/graphql.js +2314 -93
  113. package/dist/sdk/generated/graphql.js.map +1 -1
  114. package/dist/sdk/i18n/messages/en-US.js +19 -0
  115. package/dist/sdk/i18n/messages/en-US.json +19 -0
  116. package/dist/sdk/i18n/messages/en-US.json.d.ts +19 -0
  117. package/dist/sdk/i18n/messages/es-ES.js +19 -0
  118. package/dist/sdk/i18n/messages/es-ES.json +19 -0
  119. package/dist/sdk/i18n/messages/pt-BR.js +19 -0
  120. package/dist/sdk/i18n/messages/pt-BR.json +19 -0
  121. package/dist/sdk/i18n/provider.js +1 -1
  122. package/dist/sdk/i18n/provider.js.map +1 -1
  123. package/dist/sdk/locale/binding-switch-provider.d.ts +13 -0
  124. package/dist/sdk/locale/binding-switch-provider.js +20 -0
  125. package/dist/sdk/locale/binding-switch-provider.js.map +1 -0
  126. package/dist/sdk/locale/use-locale-actions.d.ts +16 -0
  127. package/dist/sdk/locale/use-locale-actions.js +70 -0
  128. package/dist/sdk/locale/use-locale-actions.js.map +1 -0
  129. package/dist/sdk/navigation/binding-switch.d.ts +39 -0
  130. package/dist/sdk/navigation/binding-switch.js +129 -0
  131. package/dist/sdk/navigation/binding-switch.js.map +1 -0
  132. package/dist/sdk/navigation/decode-route-params.d.ts +11 -0
  133. package/dist/sdk/navigation/decode-route-params.js +32 -0
  134. package/dist/sdk/navigation/decode-route-params.js.map +1 -0
  135. package/dist/sdk/saved-cards/operations.d.ts +16 -0
  136. package/dist/sdk/saved-cards/operations.js +41 -0
  137. package/dist/sdk/saved-cards/operations.js.map +1 -0
  138. package/dist/sdk/saved-cards/use-saved-card-actions.d.ts +26 -0
  139. package/dist/sdk/saved-cards/use-saved-card-actions.js +86 -0
  140. package/dist/sdk/saved-cards/use-saved-card-actions.js.map +1 -0
  141. package/dist/sdk/seo/page-alternates.d.ts +5 -3
  142. package/dist/sdk/seo/page-alternates.js +7 -12
  143. package/dist/sdk/seo/page-alternates.js.map +1 -1
  144. package/dist/sdk/shipping/shipping-operations.d.ts +1 -1
  145. package/dist/sdk/shipping/shipping-operations.js +13 -0
  146. package/dist/sdk/shipping/shipping-operations.js.map +1 -1
  147. package/dist/sdk/state/cart.d.ts +9 -2
  148. package/dist/sdk/state/cart.js +9 -2
  149. package/dist/sdk/state/cart.js.map +1 -1
  150. package/package.json +2 -2
  151. 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.
@@ -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 (unchanged — its alt text names the link)
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 still renders a plain `<img>` with no anchor, exactly as before.
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 from what it wraps.** The link adds no `aria-label`, so its accessible name is the image's alt text (`image.altText ?? variant.name`). On a line whose `variant.image` is `null` the leaf renders its children unwrapped instead of wrapping nothing, because an anchor with no content is a keyboard focus stop with no accessible name.
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 `amount === 0` |
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*`, `CartCount`, `CartError`) exist in the catalog and will be documented when the full cart page enters scope.
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 `global.json` switch deciding it.
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
- Four root-agnostic components wire up the cart coupon flow — apply a code, list the applied coupons, remove one, and surface apply/remove errors. 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.
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`. On a single-coupon store, a rejected second code surfaces here as a normal apply error.
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
- **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-error`). Chip and X **visuals (swatch values) are authored by the styling pipeline**, not these components.
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 `global.json`:
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 `global.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.
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 reactive `cart.total`, 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.
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`, cart total below `threshold` | Below-threshold message + a bar filled to `cartTotal / threshold` |
22
- | Earned | field `qualified === true`, OR the reactive cart total reaches `threshold` | Earned message + a full bar |
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 a lazy field kept out of `CartFragment`, so its cached value (`threshold`, `qualified`) does not refresh on cart mutations. The bar therefore derives the **live** fill and remaining from the reactive `cart.total` — which *is* fresh via cache-write propagation — against the fetched `threshold`:
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 && cartTotal >= threshold)` — the field flag covers the already-earned case (where `threshold` is `null`); the reactive comparison flips it at the boundary between page loads.
30
- - `liveRemaining = isQualified ? 0 : max(threshold − cartTotal, 0)`
31
- - `fill = isQualified ? 1 : clamp(cartTotal / threshold, 0..1)`
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
- The bar updates instantly on every add/remove with **no** refetch of `freeShippingProgress`. `threshold` refreshes only on a full page re-sync. `Money.amount` is a string at runtime and is always `Number()`-coerced before math/formatting.
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
 
@@ -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 `global.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`.
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-brand` |
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 `global.json → styles`:
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
  {