@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,301 @@
1
+ # Account Saved Cards
2
+
3
+ **Category:** identity
4
+ **Purpose:** The signed-in shopper's saved cards in their account: a list of card summaries, or a statement that there are none, with loading and failure states, and a confirmed removal for each card. Needs no cart.
5
+
6
+ ---
7
+
8
+ ## Visual cues (how to identify in an image)
9
+
10
+ - A section heading such as "Saved cards", "Payment methods", or "My cards" inside an account page.
11
+ - A vertical list of rows, each a card-network logo, a network name, and `•••• 1234`, with a line such as "Expires 05/28" under it.
12
+ - A dimmed row reading "Expired 05/24" or "This card can't be used".
13
+ - A "Remove" link or button in each row, opening a confirmation such as "Remove this card?" with Cancel and "Remove card".
14
+ - Or a single sentence such as "You have no saved cards yet." where the list would be.
15
+ - Usually beside the account's side navigation, never inside a checkout step.
16
+
17
+ The image is the source of truth for layout.
18
+
19
+ ## When to use
20
+
21
+ - Showing the shopper the cards saved to their account, outside any purchase: the account's Payments page, or a block inside another account page.
22
+
23
+ ## When NOT to use
24
+
25
+ - Picking a saved card to pay with during checkout → [[saved-cards]] (`SavedCards`, cart-scoped, with CVV entry).
26
+ - Describing one card on its own → [[card-summary]].
27
+ - The store's accepted payment methods as a strip of logos → [[payment-methods]].
28
+
29
+ ---
30
+
31
+ ## Composition
32
+
33
+ ```
34
+ AccountSavedCardsRoot ← requires 'session'; provides 'account-saved-cards'; declares savedCard.remove
35
+ ├── AccountSavedCardsHeading
36
+ ├── AccountSavedCardsRemoveFeedback
37
+ ├── AccountSavedCardsLoading
38
+ ├── AccountSavedCardsError
39
+ ├── AccountSavedCardsEmpty
40
+ └── AccountSavedCardsList ← provides 'account-saved-card'
41
+ └── AccountSavedCardItem ← one row per card; provides 'card-summary', 'account-saved-card-data'
42
+ ├── CardSummary (default when the row has no children)
43
+ ├── AccountSavedCardExpiry (default when the row has no children)
44
+ └── AccountSavedCardRemove (default when the row has no children) ← provides 'account-saved-card-remove'
45
+ └── AlertDialogRoot
46
+ ├── AccountSavedCardRemoveTrigger
47
+ └── AlertDialogPortal > AlertDialogBackdrop + AlertDialogViewport > AlertDialogPopup
48
+ ├── AccountSavedCardRemoveTitle
49
+ ├── AccountSavedCardRemoveDescription
50
+ ├── AccountSavedCardRemoveCancel
51
+ └── AccountSavedCardRemoveConfirm
52
+ ```
53
+
54
+ `AccountSavedCardRemove` with no children renders the prompt shown under it.
55
+
56
+ The Root renders nothing unless the session is `loggedIn`, and no chrome: no navigation, no page title, no landmark. It derives one state and exposes it on `data-fs-account-saved-cards-state`:
57
+
58
+ | State | When | Renders |
59
+ |---|---|---|
60
+ | `loading` | No cards yet and the read is in flight | `AccountSavedCardsLoading` |
61
+ | `error` | No cards and the read failed | `AccountSavedCardsError` |
62
+ | `empty` | The read resolved with no card | `AccountSavedCardsEmpty` |
63
+ | `ready` | The read resolved with cards | `AccountSavedCardsList` |
64
+ | `idle` | No cards and nothing in flight | Nothing but the heading |
65
+
66
+ The heading and the removal feedback render in every state. Every part except the Item must be inside the Root; the Item must be inside the List; the CardSummary parts, `AccountSavedCardExpiry`, and `AccountSavedCardRemove` inside the Item; and the five prompt parts inside `AccountSavedCardRemove`. A part outside its provider fails the build with a `ContextConstraintError`.
67
+
68
+ ---
69
+
70
+ ## Components
71
+
72
+ ### AccountSavedCardsRoot
73
+
74
+ Reads the card ids (`customerSavedCards`, reactive) and publishes the section state. No schema props.
75
+
76
+ It also holds the section's removal: which card is being removed (one at a time) and how the last removal ended. It declares `actions: { savedCard: ['remove'] }`, so every remove control inside reaches the action, including the one a bare Item renders.
77
+
78
+ ### AccountSavedCardsHeading
79
+
80
+ | Prop | Type | Default | Notes |
81
+ |------|------|---------|-------|
82
+ | `as` | `'h2' \| 'h3' \| 'h4' \| 'h5' \| 'h6'` | `h2` | No `h1`: the page title belongs to the page shell. |
83
+
84
+ ### AccountSavedCardsLoading
85
+
86
+ | Prop | Type | Default | Notes |
87
+ |------|------|---------|-------|
88
+ | `skeletonCount` | number | `2` | Placeholder rows (`data-fs-account-saved-cards-loading-row`), hidden from assistive technology. |
89
+
90
+ A polite live region (`data-fs-account-saved-cards-loading-announcement`) stays mounted and announces the wait.
91
+
92
+ ### AccountSavedCardsError
93
+
94
+ An alert. An expired session (`UNAUTHORIZED`) says so and links to sign-in (`data-fs-account-saved-cards-sign-in`), returning the shopper to the current page; any other failure says the cards could not be loaded. `data-fs-account-saved-cards-error-code` is `unauthorized` or `failed`. The server's message never renders. Children replace the message for every failure and drop the link.
95
+
96
+ ### AccountSavedCardsEmpty
97
+
98
+ The statement that no card is saved. Renders only once the read resolved with none.
99
+
100
+ ### AccountSavedCardsList
101
+
102
+ A `<ul>` repeating its child once per card.
103
+
104
+ ### AccountSavedCardItem
105
+
106
+ A `<li>` that provides its card to the CardSummary parts, `AccountSavedCardExpiry`, and `AccountSavedCardRemove`. With no children it renders `CardSummary`'s default (mark, network name, mask, last four digits), then `AccountSavedCardExpiry`, then `AccountSavedCardRemove`. It shows no label: the account's read does not return one.
107
+
108
+ An expired card's row carries `data-fs-account-saved-card-expired`, and an unusable one's `data-fs-account-saved-card-unusable`, so a swatch can dim it. A working card's row carries neither. While its card is being removed, the row carries `data-fs-account-saved-card-removing`. Compose the three parts as children to rearrange the row or to reword it from the CMS. Leave `AccountSavedCardRemove` out only for a read-only list.
109
+
110
+ ### AccountSavedCardExpiry
111
+
112
+ The line that says when the card expires, from the API's `status` and `expiry`. No schema props and no children.
113
+
114
+ | Card | Line | `data-fs-account-saved-card-expiry-status` |
115
+ |---|---|---|
116
+ | Working, with a date | "Expires 05/28" | `usable` |
117
+ | Expired, with a date | "Expired 05/24" | `expired` |
118
+ | Expired, no date | "Expired" | `expired` |
119
+ | Unusable (any other status) | "This card can't be used", never a date | `unusable` |
120
+ | Working, no date | Nothing | — |
121
+
122
+ The label is in `data-fs-account-saved-card-expiry-label`, the `MM/YY` date in `data-fs-account-saved-card-expiry-date` (a `<time>`). The short date is hidden from assistive technology; a visually hidden span reads the month and year in full ("May 2028") in the store's locale. A month outside 1–12 or a year outside four digits counts as no date.
123
+
124
+ ### AccountSavedCardRemove
125
+
126
+ The controller for one row's removal prompt. It owns the prompt's open state and passes it to its single `AlertDialogRoot` child; the schema never sets `open`. With no children it renders the default prompt. No schema props.
127
+
128
+ Opening the prompt dispatches nothing. Confirm dispatches `savedCard.remove` with the row's card id. The prompt then stays open until the server answers, and Escape and Cancel cannot close it meanwhile. On success the row leaves the list, and the prompt goes with it. On failure the prompt closes, focus returns to the trigger, and the row stays. An expired or unusable card is removed like any other.
129
+
130
+ ### AccountSavedCardRemoveTrigger
131
+
132
+ The row's control that opens the prompt. Its visible text is short ("Remove"); a visually hidden span adds the card ("Visa ending in 1234"), so each trigger has its own accessible name. Disabled while any removal in the section is pending.
133
+
134
+ ### AccountSavedCardRemoveTitle, AccountSavedCardRemoveDescription
135
+
136
+ The prompt's title and accessible description. With no children, the description renders the card's `CardSummary` before its text, so the prompt names the card it removes.
137
+
138
+ ### AccountSavedCardRemoveCancel
139
+
140
+ Closes the prompt and keeps the card. Opened with a mouse or the keyboard, the prompt focuses it first, so the safe action is the default; opened by touch, the popup takes focus itself. Disabled while the removal is pending.
141
+
142
+ ### AccountSavedCardRemoveConfirm
143
+
144
+ Removes the card. While the removal is pending it carries `aria-disabled="true"`, `aria-busy="true"`, and `data-fs-account-saved-card-remove-confirm-pending`, and shows its pending text ("Removing…"). It stays focusable, so focus does not leave the prompt.
145
+
146
+ ### AccountSavedCardsRemoveFeedback
147
+
148
+ Tells the shopper how the last removal ended. Place it once in the Root, outside the list, because a removed card's row unmounts. No schema props and no children.
149
+
150
+ | Outcome | Renders | `data-fs-account-saved-cards-remove-feedback-outcome` |
151
+ |---|---|---|
152
+ | Removed | "The card was removed." in a polite status region, which takes focus | `removed` |
153
+ | Session expired (`UNAUTHORIZED`) | An alert saying so, with a sign-in link (`data-fs-account-saved-cards-remove-feedback-sign-in`) that returns to the page | `unauthorized` |
154
+ | Any other failure | An alert: "We couldn't remove the card. Please try again." | `failed` |
155
+ | None yet, or a new removal started | Nothing | — |
156
+
157
+ The status region stays mounted so screen readers announce it. No message disappears on its own, and the server's message never renders.
158
+
159
+ ## Text
160
+
161
+ Every text resolves in this order: `children`, then the part's CMS entry (`$content`, a blank field counts as unset), then the catalog message in the store's locale.
162
+
163
+ | Part | CMS fields | Catalog ids |
164
+ |---|---|---|
165
+ | `AccountSavedCardsHeading` | `text` | `fs/saved-cards.heading` |
166
+ | `AccountSavedCardsEmpty` | `text` | `fs/saved-cards.empty` |
167
+ | `AccountSavedCardsLoading` | `text` | `fs/saved-cards.loading` |
168
+ | `AccountSavedCardsError` | `text`, `sessionExpiredText`, `signInLabel` | `fs/saved-cards.load-failed`, `fs/saved-cards.session-expired`, `fs/saved-cards.sign-in` |
169
+ | `AccountSavedCardExpiry` | `expiresLabel`, `expiredLabel`, `expiredText`, `unusableText` | `fs/saved-cards.expires`, `fs/saved-cards.expired`, `fs/saved-cards.expired-no-date`, `fs/saved-cards.unusable` |
170
+ | `AccountSavedCardRemoveTrigger` | `text` | `fs/saved-cards.remove.trigger` |
171
+ | `AccountSavedCardRemoveTitle` | `text` | `fs/saved-cards.remove.title` |
172
+ | `AccountSavedCardRemoveDescription` | `text` | `fs/saved-cards.remove.description` |
173
+ | `AccountSavedCardRemoveCancel` | `text` | `fs/saved-cards.remove.cancel` |
174
+ | `AccountSavedCardRemoveConfirm` | `text`, `pendingText` | `fs/saved-cards.remove.confirm`, `fs/saved-cards.remove.pending` |
175
+ | `AccountSavedCardsRemoveFeedback` | `successText`, `failureText`, `sessionExpiredText`, `signInLabel` | `fs/saved-cards.remove.success`, `fs/saved-cards.remove.failed`, `fs/saved-cards.session-expired`, `fs/saved-cards.sign-in` |
176
+
177
+ `AccountSavedCardExpiry` and `AccountSavedCardsRemoveFeedback` take no children. `expiresLabel` and `expiredLabel` precede the date, which the component always places; `expiredText` and `unusableText` stand alone. The trigger's hidden card name follows its text whatever the source. Children on the Confirm replace only its idle label. Only a part placed in the schema reads its CMS entry: the expiry line and the prompt an Item renders by default always show the catalog wording.
178
+
179
+ ## Canonical schema example
180
+
181
+ ```json
182
+ {
183
+ "saved-cards": {
184
+ "type": "AccountSavedCardsRoot",
185
+ "children": [
186
+ "saved-cards-heading",
187
+ "saved-cards-remove-feedback",
188
+ "saved-cards-loading",
189
+ "saved-cards-error",
190
+ "saved-cards-empty",
191
+ "saved-cards-list"
192
+ ]
193
+ },
194
+ "saved-cards-heading": { "type": "AccountSavedCardsHeading" },
195
+ "saved-cards-remove-feedback": { "type": "AccountSavedCardsRemoveFeedback" },
196
+ "saved-cards-loading": { "type": "AccountSavedCardsLoading" },
197
+ "saved-cards-error": { "type": "AccountSavedCardsError" },
198
+ "saved-cards-empty": { "type": "AccountSavedCardsEmpty" },
199
+ "saved-cards-list": { "type": "AccountSavedCardsList", "children": ["saved-card-item"] },
200
+ "saved-card-item": { "type": "AccountSavedCardItem" }
201
+ }
202
+ ```
203
+
204
+ Nested inside another account page, with a lower heading:
205
+
206
+ ```json
207
+ {
208
+ "saved-cards-heading": { "type": "AccountSavedCardsHeading", "props": { "as": "h3" } }
209
+ }
210
+ ```
211
+
212
+ With the row composed, so the expiry line and the removal prompt can be reworded from the CMS:
213
+
214
+ ```json
215
+ {
216
+ "saved-card-item": {
217
+ "type": "AccountSavedCardItem",
218
+ "children": ["saved-card-summary", "saved-card-expiry", "saved-card-remove"]
219
+ },
220
+ "saved-card-summary": { "type": "CardSummary" },
221
+ "saved-card-expiry": { "type": "AccountSavedCardExpiry" },
222
+ "saved-card-remove": { "type": "AccountSavedCardRemove", "children": ["saved-card-remove-dialog"] },
223
+ "saved-card-remove-dialog": {
224
+ "type": "AlertDialogRoot",
225
+ "children": ["saved-card-remove-trigger", "saved-card-remove-portal"]
226
+ },
227
+ "saved-card-remove-trigger": { "type": "AccountSavedCardRemoveTrigger" },
228
+ "saved-card-remove-portal": {
229
+ "type": "AlertDialogPortal",
230
+ "children": ["saved-card-remove-backdrop", "saved-card-remove-viewport"]
231
+ },
232
+ "saved-card-remove-backdrop": { "type": "AlertDialogBackdrop" },
233
+ "saved-card-remove-viewport": { "type": "AlertDialogViewport", "children": ["saved-card-remove-popup"] },
234
+ "saved-card-remove-popup": {
235
+ "type": "AlertDialogPopup",
236
+ "children": [
237
+ "saved-card-remove-title",
238
+ "saved-card-remove-description",
239
+ "saved-card-remove-cancel",
240
+ "saved-card-remove-confirm"
241
+ ]
242
+ },
243
+ "saved-card-remove-title": { "type": "AccountSavedCardRemoveTitle" },
244
+ "saved-card-remove-description": { "type": "AccountSavedCardRemoveDescription" },
245
+ "saved-card-remove-cancel": { "type": "AccountSavedCardRemoveCancel" },
246
+ "saved-card-remove-confirm": { "type": "AccountSavedCardRemoveConfirm" }
247
+ }
248
+ ```
249
+
250
+ ## Common mistakes
251
+
252
+ - **`AccountSavedCards`, `SavedCardsList`, `MyCards`.** The Root is `AccountSavedCardsRoot`; the list is `AccountSavedCardsList`.
253
+ - **Using `SavedCards` or `SavedCardOption` in the account.** Those are the checkout's cart-scoped picker and need a payment section.
254
+ - **Placing `AccountSavedCardItem` directly in the Root.** It must be inside `AccountSavedCardsList`.
255
+ - **Composing `AccountSavedCardsEmpty` as the only state part.** Compose Loading and Error too, or a slow or failed read shows nothing under the heading.
256
+ - **Setting `as: "h1"`.** Not accepted; the page title belongs to the page shell.
257
+ - **Passing `text` as a schema prop.** Text comes from children, the CMS entry, or the catalog.
258
+ - **Placing the Root outside the `my-account` layout.** It requires `session`; without a `SessionRoot` the build fails.
259
+ - **Giving the Item custom children without `AccountSavedCardExpiry`.** The row then hides when the card expires and whether it can be used.
260
+ - **Expecting CMS wording on an Item with no children.** Its default expiry line and removal prompt are not in the schema, so they read only the catalog; place `AccountSavedCardExpiry` and the `AccountSavedCardRemove` composition explicitly.
261
+ - **Giving the Item custom children without `AccountSavedCardRemove`.** The shopper can no longer remove the card. Do it only for a read-only list.
262
+ - **Leaving out `AccountSavedCardsRemoveFeedback`.** A removal then ends with no message: a success only drops the row, and a failure says nothing.
263
+ - **Placing the feedback inside the Item.** A removed card's row unmounts, taking the message with it.
264
+ - **Setting `open` on the `AlertDialogRoot` inside `AccountSavedCardRemove`.** The controller owns it.
265
+ - **Adding a plain button as the confirm control.** Only `AccountSavedCardRemoveConfirm` dispatches the removal.
266
+
267
+ ---
268
+
269
+ ## Styling
270
+
271
+ | Component | What you style | Plausible classes |
272
+ |---|---|---|
273
+ | `AccountSavedCardsRoot` | Section spacing | `flex flex-col gap-[16px]` |
274
+ | `AccountSavedCardsHeading` | Heading typography | `text-[20px] font-semibold text-[#1F1F1F]` |
275
+ | `AccountSavedCardsList` | Row stack | `flex flex-col gap-[8px]` |
276
+ | `AccountSavedCardItem` | Row box; dim expired and unusable rows | `rounded-[8px] border border-[#E5E5E5] p-[12px] data-[fs-account-saved-card-expired]:opacity-60 data-[fs-account-saved-card-unusable]:opacity-60` |
277
+ | `AccountSavedCardExpiry` | Line typography; color by state | `mt-[4px] text-[13px] text-[#6B6B6B] data-[fs-account-saved-card-expiry-status=expired]:text-[#B42318] data-[fs-account-saved-card-expiry-status=unusable]:text-[#B42318]` |
278
+ | `AccountSavedCardsEmpty` | Statement typography | `text-[14px] text-[#6B6B6B]` |
279
+ | `AccountSavedCardsLoading` | Placeholder stack; rows via `[&_[data-fs-account-saved-cards-loading-row]]` | `flex flex-col gap-[8px] [&_[data-fs-account-saved-cards-loading-row]]:h-[48px] [&_[data-fs-account-saved-cards-loading-row]]:rounded-[8px] [&_[data-fs-account-saved-cards-loading-row]]:bg-[#F0F0F0]` |
280
+ | `AccountSavedCardsError` | Alert box and link | `rounded-[8px] bg-[#FDECEC] p-[12px] text-[14px] text-[#B42318] [&_a]:underline` |
281
+ | `AccountSavedCardItem` (removing) | Fade the row while it is removed | `data-[fs-account-saved-card-removing]:opacity-60` |
282
+ | `AccountSavedCardRemoveTrigger` | Link-like control | `mt-[8px] text-[13px] text-[#B42318] underline disabled:opacity-50` |
283
+ | `AlertDialogPopup` | Prompt box (style the AlertDialog parts directly) | `w-full max-w-[420px] rounded-[12px] bg-white p-[24px] shadow-xl` |
284
+ | `AccountSavedCardRemoveConfirm` | Destructive button; dim while pending | `rounded-[6px] bg-[#B42318] px-[16px] py-[8px] text-white aria-disabled:opacity-70` |
285
+ | `AccountSavedCardsRemoveFeedback` | Message typography; alert color | `text-[14px] [&_[role=alert]]:text-[#B42318] [&_a]:underline` |
286
+
287
+ ```json
288
+ {
289
+ "saved-cards": { "type": "AccountSavedCardsRoot", "style": ["flex", "flex-col", "gap-[16px]"] },
290
+ "saved-card-item": { "type": "AccountSavedCardItem", "style": ["rounded-[8px]", "border", "border-[#E5E5E5]", "p-[12px]"] }
291
+ }
292
+ ```
293
+
294
+ ---
295
+
296
+ ## Related
297
+
298
+ - [[card-summary]]: the card description each row renders.
299
+ - [[alert-dialog]]: the overlay the removal prompt is built on.
300
+ - [[saved-cards]]: the checkout's saved-card picker; cart-scoped.
301
+ - [[my-account-profile]]: the account's other session-scoped matter.
@@ -0,0 +1,154 @@
1
+ # BindingSwitcher
2
+
3
+ **Category:** action
4
+ **Purpose:** Lists the store's bindings and starts a binding switch (locale, currency, and sales channel) through `locale.set`.
5
+
6
+ ---
7
+
8
+ ## Visual cues (how to identify in an image)
9
+
10
+ A closed dropdown whose visible value is a language name, a chevron, and sometimes one short sentence in a status line. The image is the source of truth for layout.
11
+
12
+ ---
13
+
14
+ ## When to use
15
+
16
+ A store that declares two or more bindings and wants the shopper to switch the whole binding. Pass `bindings` as `$data.Global.localization.bindings`. With fewer than two counted bindings the component renders nothing.
17
+
18
+ ## When NOT to use
19
+
20
+ - A country list, a currency picker, or a CMS language field.
21
+ - A store with one binding. It renders nothing and should not reserve an empty control.
22
+
23
+ ---
24
+
25
+ ## Composition
26
+
27
+ The schema tree is one node. `BindingSwitcher` has no children. The `Select` parts are internal and are not schema children.
28
+
29
+ ```
30
+ BindingSwitcher
31
+ ```
32
+
33
+ ---
34
+
35
+ ## Components
36
+
37
+ ### BindingSwitcher
38
+
39
+ Renders a host `div` (`data-fs-binding-switcher`). No fragment. Declares `actions: { locale: ['set'] }`. Choosing another binding calls `$actions.locale.set({ bindingId })`.
40
+
41
+ | Prop | Type | Default | Notes |
42
+ |------|------|---------|-------|
43
+ | `bindings` | object | — | `foundation.json` `localization.bindings`, keyed by binding id. No library default. The store passes `$data.Global.localization.bindings`. |
44
+ | `label` | string | `"Language"` | Accessible name, rendered as the select label. |
45
+ | `notice` | string | `"This page isn't available in the selected language."` | Shown when the page loaded with `fs_notice=no-equivalent`. Inline `<output>` (implicit `status` role). |
46
+
47
+ ---
48
+
49
+ ## Canonical schema example
50
+
51
+ ```json
52
+ {
53
+ "binding-switcher": {
54
+ "type": "BindingSwitcher",
55
+ "props": {
56
+ "bindings": "$data.Global.localization.bindings"
57
+ },
58
+ "style": [
59
+ "flex",
60
+ "flex-col",
61
+ "gap-2",
62
+ "text-[14px]",
63
+ "[&_[data-fs-binding-switcher-notice]]:text-[13px]",
64
+ "[&_[data-fs-select-trigger]]:inline-flex",
65
+ "[&_[data-fs-select-trigger]]:items-center",
66
+ "[&_[data-fs-select-trigger]]:gap-2",
67
+ "[&_[data-fs-select-trigger]]:rounded-md",
68
+ "[&_[data-fs-select-trigger]]:border",
69
+ "[&_[data-fs-select-trigger]]:border-neutral-300",
70
+ "[&_[data-fs-select-trigger]]:bg-white",
71
+ "[&_[data-fs-select-trigger]]:px-3",
72
+ "[&_[data-fs-select-trigger]]:py-1.5",
73
+ "[&_[data-fs-select-positioner]]:z-50",
74
+ "[&_[data-fs-select-popup]]:min-w-[var(--anchor-width)]",
75
+ "[&_[data-fs-select-popup]]:overflow-hidden",
76
+ "[&_[data-fs-select-popup]]:rounded-md",
77
+ "[&_[data-fs-select-popup]]:border",
78
+ "[&_[data-fs-select-popup]]:border-neutral-200",
79
+ "[&_[data-fs-select-popup]]:bg-white",
80
+ "[&_[data-fs-select-popup]]:shadow-lg",
81
+ "[&_[data-fs-select-list]]:py-1",
82
+ "[&_[data-fs-select-item]]:cursor-pointer",
83
+ "[&_[data-fs-select-item]]:px-3",
84
+ "[&_[data-fs-select-item]]:py-2",
85
+ "[&_[data-fs-select-item][data-highlighted]]:bg-neutral-100"
86
+ ]
87
+ }
88
+ }
89
+ ```
90
+
91
+ ---
92
+
93
+ ## Common mistakes
94
+
95
+ - **LocaleSelector** or **LanguagePicker.** The name is `BindingSwitcher`.
96
+ - **A toast for the notice.** The notice is an inline `<output>`.
97
+ - **A default bindings list.** The store passes `$data.Global.localization.bindings`.
98
+ - **Children that rebuild Select.** The schema element has no children.
99
+
100
+ ---
101
+
102
+ ## Styling
103
+
104
+ The host stacks the notice above the control. The popup stays in the host and opens below the trigger, 4px clear of it, so the element `style` reaches `[data-fs-select-popup]`. Give that popup an opaque surface. A parent locked to a fixed height lets the notice paint outside the bar; `min-h-16` on that parent keeps the notice inside it.
105
+
106
+ | Component | What you style | Plausible classes |
107
+ |---|---|---|
108
+ | `BindingSwitcher` | Stacks the notice and the control. The notice line is `text-[13px]`. The popup is `[data-fs-select-popup]`. | `flex flex-col gap-2 text-[14px] [&_[data-fs-binding-switcher-notice]]:text-[13px] [&_[data-fs-select-popup]]:bg-white [&_[data-fs-select-popup]]:border [&_[data-fs-select-popup]]:border-neutral-200 [&_[data-fs-select-popup]]:rounded-md [&_[data-fs-select-popup]]:shadow-lg` |
109
+
110
+ ```json
111
+ {
112
+ "binding-switcher": {
113
+ "type": "BindingSwitcher",
114
+ "props": {
115
+ "bindings": "$data.Global.localization.bindings"
116
+ },
117
+ "style": [
118
+ "flex",
119
+ "flex-col",
120
+ "gap-2",
121
+ "text-[14px]",
122
+ "[&_[data-fs-binding-switcher-notice]]:text-[13px]",
123
+ "[&_[data-fs-select-trigger]]:inline-flex",
124
+ "[&_[data-fs-select-trigger]]:items-center",
125
+ "[&_[data-fs-select-trigger]]:gap-2",
126
+ "[&_[data-fs-select-trigger]]:rounded-md",
127
+ "[&_[data-fs-select-trigger]]:border",
128
+ "[&_[data-fs-select-trigger]]:border-neutral-300",
129
+ "[&_[data-fs-select-trigger]]:bg-white",
130
+ "[&_[data-fs-select-trigger]]:px-3",
131
+ "[&_[data-fs-select-trigger]]:py-1.5",
132
+ "[&_[data-fs-select-positioner]]:z-50",
133
+ "[&_[data-fs-select-popup]]:min-w-[var(--anchor-width)]",
134
+ "[&_[data-fs-select-popup]]:overflow-hidden",
135
+ "[&_[data-fs-select-popup]]:rounded-md",
136
+ "[&_[data-fs-select-popup]]:border",
137
+ "[&_[data-fs-select-popup]]:border-neutral-200",
138
+ "[&_[data-fs-select-popup]]:bg-white",
139
+ "[&_[data-fs-select-popup]]:shadow-lg",
140
+ "[&_[data-fs-select-list]]:py-1",
141
+ "[&_[data-fs-select-item]]:cursor-pointer",
142
+ "[&_[data-fs-select-item]]:px-3",
143
+ "[&_[data-fs-select-item]]:py-2",
144
+ "[&_[data-fs-select-item][data-highlighted]]:bg-neutral-100"
145
+ ]
146
+ }
147
+ }
148
+ ```
149
+
150
+ ---
151
+
152
+ ## Related
153
+
154
+ - [[select]] — the empty dropdown this component composes. `Select` does not know about bindings.
@@ -90,7 +90,7 @@ In the checkout, the card number, holder name, expiration, and security code are
90
90
  }
91
91
  ```
92
92
 
93
- **If the store's `global.json` already has an `icons` object, extend it — do not replace it.** A store that already sets `"collection"` (or `"aliases"` / `"collections"`) must keep that key alongside the new `include`:
93
+ **If the store's `foundation.json` already has an `icons` object, extend it — do not replace it.** A store that already sets `"collection"` (or `"aliases"` / `"collections"`) must keep that key alongside the new `include`:
94
94
 
95
95
  ```json
96
96
  {
@@ -107,7 +107,7 @@ Omitting `include` entirely is silent too. The network icon renders as an empty
107
107
 
108
108
  ## Store setup: response headers
109
109
 
110
- The card number and security code are typed into `CardEntry`'s raw fields and POSTed from the browser straight to the VTEX payment gateway, so the page that composes it needs a policy that names the vault host the card is POSTed to, forbids framing the page, and narrows what the page may load and call. The generator ships no default: the store declares it in `global.json` under `responseHeaders`, scoped by `routes` to the page that composes `CardEntry`.
110
+ The card number and security code are typed into `CardEntry`'s raw fields and POSTed from the browser straight to the VTEX payment gateway, so the page that composes it needs a policy that names the vault host the card is POSTed to, forbids framing the page, and narrows what the page may load and call. The generator ships no default: the store declares it in `foundation.json` under `responseHeaders`, scoped by `routes` to the page that composes `CardEntry`.
111
111
 
112
112
  ```json
113
113
  {
@@ -123,7 +123,7 @@ The card number and security code are typed into `CardEntry`'s raw fields and PO
123
123
  }
124
124
  ```
125
125
 
126
- - **The vault host in `connect-src` is `https://{account}.vtexvault.com`, for the account the store serves.** That is the `NEXT_PUBLIC_ACCOUNT` the store is built with, which is not always `account.name` in `global.json`. Write the served account, with no wildcard: `https://*.vtexvault.com` would let a compromised script send the card to any tenant's vault.
126
+ - **The vault host in `connect-src` is `https://{account}.vtexvault.com`, for the account the store serves.** That is the `NEXT_PUBLIC_ACCOUNT` the store is built with, which is not always `store.account` in `foundation.json`. Write the served account, with no wildcard: `https://*.vtexvault.com` would let a compromised script send the card to any tenant's vault.
127
127
  - **A policy that names the legacy payments host (`vtexpayments.com.br`) makes the browser block the card POST.** The order cannot be placed and nothing else on the page reports why. The generated app calls its own GraphQL at the relative `/api/graphql`, so `'self'` covers it; a store built with an absolute `NEXT_PUBLIC_STOREFRONT_API_URL` must add that origin.
128
128
  - **A rule's `routes` must name the page that composes `CardEntry`**, as the page's `route` spells it (`/checkout`, `/reserva/checkout/payment`). Renaming that page fails the compile instead of dropping the policy. A page added later that composes `CardEntry` carries nothing until its route is added to the rule.
129
129
  - **`script-src 'unsafe-inline'` is a known limit, and the policy does not stop injected script from sending the card off the page.** A static header cannot carry a nonce, so the inline scripts Next.js emits need it, and script injected into the page runs with the same rights. `connect-src` closes only `fetch`, XHR, beacons and WebSocket. The policy still leaves three channels open to any https host: an image request (`img-src https:`, which the page needs for catalog images), a form submission (no `form-action`) and a navigation (CSP cannot restrict one). What today's ruled policy does give is the vault origin pinned, no `'unsafe-eval'`, a page no other site can frame (`frame-ancestors 'none'`) and no `http:` or wildcard source. It does not replace keeping untrusted script off the payment page.
@@ -0,0 +1,175 @@
1
+ # Card Summary
2
+
3
+ **Category:** content
4
+ **Purpose:** A saved card's identity: its network's mark, its label or else its network name, a mask, and its last four digits, as parts the schema arranges.
5
+
6
+ ---
7
+
8
+ ## Visual cues (how to identify in an image)
9
+
10
+ - A card-network logo (Visa, Mastercard, American Express, Diners, Elo) beside a short text.
11
+ - A name such as `Visa` or a shopper's own label such as `Company card`.
12
+ - A masking symbol (`••••`, `****`) followed by four digits: `•••• 1234`.
13
+ - Usually one line per card, repeated in a list or inside a selectable row.
14
+
15
+ The image is the source of truth for layout.
16
+
17
+ ## When to use
18
+
19
+ - Describing one saved card wherever it appears: the account's saved-cards list, a removal confirmation, the checkout's saved-card row.
20
+
21
+ ## When NOT to use
22
+
23
+ - The store's accepted payment methods as a strip of logos → [[payment-methods]].
24
+ - Typing a new card → the checkout's card entry; this particle never takes a full number or a security code.
25
+ - A card's expiry or usability state → the row that owns the card renders it beside this particle.
26
+
27
+ ---
28
+
29
+ ## Composition
30
+
31
+ ```
32
+ <row that provides 'card-summary'> ← e.g. AccountSavedCardItem
33
+ └── CardSummary
34
+ ├── CardSummaryMark (default)
35
+ ├── CardSummaryName (default)
36
+ ├── CardSummaryMask (default)
37
+ └── CardSummaryDigits (default)
38
+ ```
39
+
40
+ `CardSummary` with no children renders the four parts in that order. Composing children replaces the default: any order, any subset. Child order is the visual order and the screen-reader order. The parts can also sit directly in the providing row, without the `CardSummary` wrapper.
41
+
42
+ Every part requires the `card-summary` context, and only a matter row that owns card data provides it in a schema. The row wraps its children in `CardSummaryRoot` in code and declares `'card-summary'` in its own `providesContext`, so the schema composes the parts directly in the row. Context matching is by contract, not by component name. Stories, tests, and hand-written JSX render `CardSummaryRoot` themselves.
43
+
44
+ A part outside any provider fails the build with a `ContextConstraintError`, and throws at runtime outside the compiler.
45
+
46
+ ---
47
+
48
+ ## Components
49
+
50
+ ### CardSummaryRoot
51
+
52
+ Code only: it has no catalog entry and is never placed in a schema, because a schema cannot supply the card. Renders no element. Publishes the card to the parts. Keeps only the trailing four digit characters of `lastDigits`; the raw value never reaches a part.
53
+
54
+ | Prop | Type | Default | Notes |
55
+ |------|------|---------|-------|
56
+ | `network` | string | — | The network's display name from the platform, e.g. `Visa`. Data, not a schema prop. |
57
+ | `lastDigits` | string | — | The card's last digits. Only four ever render. Data, not a schema prop. |
58
+ | `label` | string \| null | — | The shopper's own name for the card. Shown in place of the network name when non-blank. Data, not a schema prop. |
59
+
60
+ ### CardSummary
61
+
62
+ The wrapper, `<span data-fs-card-summary>`. Carries `data-fs-card-summary-network="<icon>"` when the network has a mark and `data-fs-card-summary-labeled` when a label is present. No props.
63
+
64
+ ### CardSummaryMark
65
+
66
+ The network's mark. No props. Decided by the data, never by which other parts are composed:
67
+
68
+ | Card | Renders |
69
+ |---|---|
70
+ | Mapped network, no label | The glyph, decorative (the name says the network). |
71
+ | Mapped network, labelled | The glyph plus the network name as screen-reader text. |
72
+ | Unmapped network, no label | Nothing. |
73
+ | Unmapped network, labelled | The network name as visible text (`data-fs-card-summary-network-name`), never another network's glyph. |
74
+
75
+ So a labelled card still tells a sighted shopper and a screen reader its network, even when the schema drops `CardSummaryName`. An unlabelled card does not: its glyph is decorative and `CardSummaryName` is what announces the network, so the network is announced exactly once in the default composition.
76
+
77
+ ### CardSummaryName
78
+
79
+ The label, or else the network name. Nothing when both are blank. Read-only. No props.
80
+
81
+ ### CardSummaryMask
82
+
83
+ Decoration for the hidden digits, always `aria-hidden`. Nothing when empty or when the card has no digits.
84
+
85
+ | Prop | Type | Default | Notes |
86
+ |------|------|---------|-------|
87
+ | `mask` | string | `••••` | The masking symbol. An empty string renders nothing. |
88
+
89
+ `children` replace `mask`, e.g. an `Icon`. Keep them inline: the particle sits inside labels and paragraphs.
90
+
91
+ ### CardSummaryDigits
92
+
93
+ The last four digits. Screen readers hear "ending in 1234" (message id `fs/card.ending-in`, in en-US, es-ES, and pt-BR); the visible digits are hidden from them. Nothing when the card has no digits. No props.
94
+
95
+ ---
96
+
97
+ ## Networks and the `icons.include` obligation
98
+
99
+ The mark comes from `CARD_NETWORK_ICON_BY_KEY` in `card-network.ts`, the one map every card surface shares. A network name matches after lowercasing and removing every character outside `a-z`: `Visa`, `Mastercard`, `Amex`, `American Express`, `Diners`, `Diners Club`, and `Elo` have marks. Matching is exact, so `Visa Electron` or `Elo Debit` render by name, without a mark. `cardNetworkIcon(name)` returns the icon name or `null`.
100
+
101
+ **Every mapped name must be registered.** The network comes from data, so the compiler cannot see which glyphs a route needs. A store that renders `CardSummaryMark` lists the names under `icons.include` in its `foundation.json`; it is JSON, so it cannot import `CARD_NETWORK_ICON_NAMES`, and the names are written out:
102
+
103
+ ```json
104
+ { "icons": { "include": ["visa", "mastercard", "amex", "diners", "elo-card"] } }
105
+ ```
106
+
107
+ A name the store never registered renders an empty `<svg>`: no build error and no runtime error, just a missing mark. All five are base-dialect names, so they resolve in any store whatever its icon collection.
108
+
109
+ ---
110
+
111
+ ## Canonical schema example
112
+
113
+ The account's row provides the context; the schema arranges the parts:
114
+
115
+ ```json
116
+ {
117
+ "saved-card-item": { "type": "AccountSavedCardItem", "children": ["card-summary"] },
118
+ "card-summary": { "type": "CardSummary", "style": ["inline-flex", "items-center", "gap-[8px]"] }
119
+ }
120
+ ```
121
+
122
+ Digits first, with another mask:
123
+
124
+ ```json
125
+ {
126
+ "saved-card-item": { "type": "AccountSavedCardItem", "children": ["card-digits", "card-mask", "card-mark", "card-name"] },
127
+ "card-digits": { "type": "CardSummaryDigits" },
128
+ "card-mask": { "type": "CardSummaryMask", "props": { "mask": "****" } },
129
+ "card-mark": { "type": "CardSummaryMark" },
130
+ "card-name": { "type": "CardSummaryName" }
131
+ }
132
+ ```
133
+
134
+ ## Common mistakes
135
+
136
+ - **`CardSummaryNumber`, `CardSummaryLastFour`.** The correct name is `CardSummaryDigits`.
137
+ - **`CardSummaryBrand`, `CardSummaryFlag`, `CardSummaryIcon`.** The correct name is `CardSummaryMark`.
138
+ - **`CardSummaryLabel`.** The correct name is `CardSummaryName`; it shows the label or else the network name.
139
+ - **Writing the bullets inside `CardSummaryDigits`.** The mask is its own part, `CardSummaryMask`.
140
+ - **Placing `CardSummaryRoot` in a schema, or passing `network`, `lastDigits`, or `label` as schema props.** The card is data from the row that provides `card-summary`; the Root is not in the catalog.
141
+ - **Composing a part outside a row that provides `card-summary`.** It fails the build.
142
+ - **Dropping `CardSummaryMark` or `CardSummaryDigits` from a custom composition.** The PRD requires both beside a card; only the default composition guarantees them.
143
+ - **Composing `CardSummaryMark` without `CardSummaryName`.** On an unlabelled card the glyph is decorative and the name is what announces the network, so a screen reader hears only "ending in 1234". Keep `CardSummaryName` whenever `CardSummaryMark` is composed.
144
+
145
+ ---
146
+
147
+ ## Styling
148
+
149
+ | Component | What you style | Plausible classes |
150
+ |---|---|---|
151
+ | `CardSummary` | Lines the parts up | `inline-flex items-center gap-[8px]` |
152
+ | `CardSummaryMark` | Mark box; size the glyph through it | `inline-flex items-center [&_svg]:h-[24px] [&_svg]:w-[32px]` |
153
+ | `CardSummaryName` | Name typography | `text-[14px] font-medium text-[#1F1F1F]` |
154
+ | `CardSummaryMask` | Mask color and spacing | `text-[14px] tracking-[2px] text-[#6B6B6B]` |
155
+ | `CardSummaryDigits` | Digit typography | `text-[14px] tabular-nums text-[#1F1F1F]` |
156
+
157
+ `CardSummaryRoot` renders no element and is not styled.
158
+
159
+ ```json
160
+ {
161
+ "card-summary": { "type": "CardSummary", "style": ["inline-flex", "items-center", "gap-[8px]"], "children": ["card-mark", "card-name", "card-mask", "card-digits"] },
162
+ "card-mark": { "type": "CardSummaryMark", "style": ["inline-flex", "items-center", "[&_svg]:h-[24px]", "[&_svg]:w-[32px]"] },
163
+ "card-name": { "type": "CardSummaryName", "style": ["text-[14px]", "font-medium", "text-[#1F1F1F]"] },
164
+ "card-mask": { "type": "CardSummaryMask", "style": ["text-[14px]", "tracking-[2px]", "text-[#6B6B6B]"] },
165
+ "card-digits": { "type": "CardSummaryDigits", "style": ["text-[14px]", "tabular-nums", "text-[#1F1F1F]"] }
166
+ }
167
+ ```
168
+
169
+ ---
170
+
171
+ ## Related
172
+
173
+ - [[payment-methods]]: the store's accepted payment methods as a strip of logos; not a saved card.
174
+ - [[address-summary]]: the same Root-plus-parts shape, for a postal address.
175
+ - [[icon]]: renders the mark from the base icon dialect.