@vtex/faststore 0.0.6 → 0.0.7

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 (132) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/dist/catalog.json +161 -20
  3. package/dist/cli/UnreachableCaseError-Crd168Vu.js +10 -0
  4. package/dist/cli/UnreachableCaseError-Crd168Vu.js.map +1 -0
  5. package/dist/cli/cacheControl-CEojH_TM.js +158 -0
  6. package/dist/cli/cacheControl-CEojH_TM.js.map +1 -0
  7. package/dist/cli/cachePolicy-m2vASMRW.js +26 -0
  8. package/dist/cli/cachePolicy-m2vASMRW.js.map +1 -0
  9. package/dist/cli/computeCoreSchemaHash-DPJZj-qJ.js +10 -0
  10. package/dist/cli/computeCoreSchemaHash-DPJZj-qJ.js.map +1 -0
  11. package/dist/cli/default-BCQHcwbL.js +253 -0
  12. package/dist/cli/default-BCQHcwbL.js.map +1 -0
  13. package/dist/cli/disableSuggestions-yO9UqiWn.js +21 -0
  14. package/dist/cli/disableSuggestions-yO9UqiWn.js.map +1 -0
  15. package/dist/cli/dist-C1JhdHfo.js +2001 -0
  16. package/dist/cli/dist-C1JhdHfo.js.map +1 -0
  17. package/dist/cli/graphql-DG9thCDG.js +14589 -0
  18. package/dist/cli/graphql-DG9thCDG.js.map +1 -0
  19. package/dist/cli/index.d.ts +350 -0
  20. package/dist/cli/index.js +35047 -0
  21. package/dist/cli/index.js.map +1 -0
  22. package/dist/cli/index.min-BsIHDfFK.js +613 -0
  23. package/dist/cli/index.min-BsIHDfFK.js.map +1 -0
  24. package/dist/cli/inlineTrace-uLgOJ3TH.js +52 -0
  25. package/dist/cli/inlineTrace-uLgOJ3TH.js.map +1 -0
  26. package/dist/cli/internalPlugin-DnuwN4_4.js +11 -0
  27. package/dist/cli/internalPlugin-DnuwN4_4.js.map +1 -0
  28. package/dist/cli/packageVersion-DQAnIEil.js +6 -0
  29. package/dist/cli/packageVersion-DQAnIEil.js.map +1 -0
  30. package/dist/cli/rolldown-runtime-BMI-E3GI.js +44 -0
  31. package/dist/cli/schemaIsSubgraph-DoGh3OYj.js +17 -0
  32. package/dist/cli/schemaIsSubgraph-DoGh3OYj.js.map +1 -0
  33. package/dist/cli/schemaReporting-K-me-3-v.js +199 -0
  34. package/dist/cli/schemaReporting-K-me-3-v.js.map +1 -0
  35. package/dist/cli/server-runtime.js +49859 -0
  36. package/dist/cli/server-runtime.js.map +1 -0
  37. package/dist/cli/traceTreeBuilder-BIcV2U7b.js +9118 -0
  38. package/dist/cli/traceTreeBuilder-BIcV2U7b.js.map +1 -0
  39. package/dist/cli/usageReporting-Bpl8vSZX.js +10201 -0
  40. package/dist/cli/usageReporting-Bpl8vSZX.js.map +1 -0
  41. package/dist/cli/v4-CBhFvC49.js +42 -0
  42. package/dist/cli/v4-CBhFvC49.js.map +1 -0
  43. package/dist/components.d.ts +5 -3
  44. package/dist/components.js +5 -3
  45. package/dist/content-types.json +93 -0
  46. package/dist/core/dist/define-component.d.ts +37 -0
  47. package/dist/core/dist/index.d.ts +2 -2
  48. package/dist/docs/account-entry-point.md +1 -1
  49. package/dist/docs/account-menu-identity.md +160 -0
  50. package/dist/docs/account-menu.md +106 -23
  51. package/dist/docs/avatar.md +103 -0
  52. package/dist/docs/my-account-profile.md +1 -1
  53. package/dist/docs/product-shelf.md +20 -13
  54. package/dist/index.d.ts +2 -2
  55. package/dist/internal/client.js +1 -1
  56. package/dist/internal/content.d.ts +2 -2
  57. package/dist/internal/server.d.ts +4 -1
  58. package/dist/internal/server.js +4 -1
  59. package/dist/matter/account-access-gate/account-access-gate.js.map +1 -1
  60. package/dist/matter/account-entry-point/account-entry-point.js +1 -1
  61. package/dist/matter/account-entry-point/account-entry-point.js.map +1 -1
  62. package/dist/matter/account-menu/account-menu.d.ts +70 -0
  63. package/dist/matter/account-menu/account-menu.js +201 -9
  64. package/dist/matter/account-menu/account-menu.js.map +1 -1
  65. package/dist/matter/account-menu/sign-out-failure.js +50 -0
  66. package/dist/matter/account-menu/sign-out-failure.js.map +1 -0
  67. package/dist/matter/account-menu-identity/account-menu-identity.d.ts +21 -0
  68. package/dist/matter/account-menu-identity/account-menu-identity.js +88 -0
  69. package/dist/matter/account-menu-identity/account-menu-identity.js.map +1 -0
  70. package/dist/matter/card-entry/card-entry.js +1 -0
  71. package/dist/matter/card-entry/card-entry.js.map +1 -1
  72. package/dist/matter/cart/cart.js +1 -1
  73. package/dist/matter/client-preferences/client-preferences-fieldset.js +2 -2
  74. package/dist/matter/client-profile-form/client-profile-fieldset.js +2 -2
  75. package/dist/matter/identity-summary/identity-summary.js +2 -1
  76. package/dist/matter/identity-summary/identity-summary.js.map +1 -1
  77. package/dist/matter/my-account-profile/my-account-profile.d.ts +3 -0
  78. package/dist/matter/my-account-profile/my-account-profile.js +2 -1
  79. package/dist/matter/my-account-profile/my-account-profile.js.map +1 -1
  80. package/dist/matter/payment/payment-controller.js +1 -0
  81. package/dist/matter/payment/payment-controller.js.map +1 -1
  82. package/dist/matter/payment/payment.js +1 -0
  83. package/dist/matter/payment/payment.js.map +1 -1
  84. package/dist/matter/product-shelf/product-shelf.d.ts +61 -31
  85. package/dist/matter/product-shelf/product-shelf.js +157 -57
  86. package/dist/matter/product-shelf/product-shelf.js.map +1 -1
  87. package/dist/matter/saved-cards/saved-cards.js +1 -0
  88. package/dist/matter/saved-cards/saved-cards.js.map +1 -1
  89. package/dist/matter/shipping-address/shipping-address.js +2 -2
  90. package/dist/particles/avatar/avatar.d.ts +17 -0
  91. package/dist/particles/avatar/avatar.js +65 -0
  92. package/dist/particles/avatar/avatar.js.map +1 -0
  93. package/dist/schema.graphql +65 -26
  94. package/dist/sdk/analytics/use-analytics-actions.js +2 -1
  95. package/dist/sdk/analytics/use-analytics-actions.js.map +1 -1
  96. package/dist/sdk/cart/use-cart-actions.js +9 -8
  97. package/dist/sdk/cart/use-cart-actions.js.map +1 -1
  98. package/dist/sdk/cart/use-cart-validate-on-open.js +2 -1
  99. package/dist/sdk/cart/use-cart-validate-on-open.js.map +1 -1
  100. package/dist/sdk/content/create-content-runtime.d.ts +22 -1
  101. package/dist/sdk/content/create-content-runtime.js +89 -1
  102. package/dist/sdk/content/create-content-runtime.js.map +1 -1
  103. package/dist/sdk/data/empty-content-value.js +16 -0
  104. package/dist/sdk/data/empty-content-value.js.map +1 -0
  105. package/dist/sdk/data/hot-data-provider.js +2 -1
  106. package/dist/sdk/data/hot-data-provider.js.map +1 -1
  107. package/dist/sdk/data/index.js +2 -2
  108. package/dist/sdk/data/reactive-list-item-data-provider.js +2 -2
  109. package/dist/sdk/data/variables-from-content.d.ts +43 -0
  110. package/dist/sdk/data/variables-from-content.js +57 -0
  111. package/dist/sdk/data/variables-from-content.js.map +1 -0
  112. package/dist/sdk/generated/gql.js +2 -1
  113. package/dist/sdk/generated/gql.js.map +1 -1
  114. package/dist/sdk/generated/graphql.js +85 -1
  115. package/dist/sdk/generated/graphql.js.map +1 -1
  116. package/dist/sdk/identity/use-identity-actions.js +5 -4
  117. package/dist/sdk/identity/use-identity-actions.js.map +1 -1
  118. package/dist/sdk/payment/use-payment-actions.js +1 -1
  119. package/dist/sdk/payment/use-place-order.js +1 -1
  120. package/dist/sdk/region/use-region-actions.js +2 -1
  121. package/dist/sdk/region/use-region-actions.js.map +1 -1
  122. package/dist/sdk/session/read-server-session.d.ts +21 -0
  123. package/dist/sdk/session/read-server-session.js +61 -0
  124. package/dist/sdk/session/read-server-session.js.map +1 -0
  125. package/dist/sdk/session/use-session-actions.js +5 -4
  126. package/dist/sdk/session/use-session-actions.js.map +1 -1
  127. package/dist/sdk/session/use-session-revalidation.js +2 -1
  128. package/dist/sdk/session/use-session-revalidation.js.map +1 -1
  129. package/dist/sdk/shipping/use-shipping-actions.js +3 -2
  130. package/dist/sdk/shipping/use-shipping-actions.js.map +1 -1
  131. package/dist/utils/safe-href.d.ts +11 -0
  132. package/package.json +13 -2
@@ -60,7 +60,7 @@ Content model (`account-entry-point`) fields, edited in the CMS:
60
60
 
61
61
  **One element, two links, chosen by the session.** `loggedIn` renders the signed-in link; `anonymous` and `identified` both render the signed-out one, because an identified but unauthenticated shopper cannot reach their account yet. There is no prop that selects between them — the merchant authors one element and the session decides.
62
62
 
63
- **The destinations are not authored.** Sign in goes to `/login`, carrying a `returnUrl` with the current path so the shopper returns where they started; the account link goes to the storefront's own `/account`. Both are fixed constants, so no schema edit and no CMS entry can misroute the auth flow. The sign-in destination comes from the shared `resolveSignInHref` (`utils/safe-href.ts`) rather than from this component, so any other surface that has to send a signed-out shopper to sign in reaches the same place by the same rule. The `returnUrl` value is constrained to a same-origin relative path, which keeps the sign-in route from being turned into an open redirect; a path the narrowing refuses drops the parameter, and so does a router reporting no path, leaving a bare `/login` rather than a broken link. That is the sender's half of the guarantee. Nothing stops a hand-crafted link, so the receiving half — narrowing the parameter through `resolveSiteRelativeHref` on arrival — belongs to the route that will serve `/login`. The signed-out link also opts out of Next.js prefetching: a header control is always in the viewport, and the destination is an auth route rather than a page, so it can set a cookie or redirect and the default would fire that request on every page view.
63
+ **The destinations are not authored.** Sign in goes to `/login`, carrying a `returnUrl` with the current path so the shopper returns where they started; the account link goes to the storefront's own `/pvt/account`. Both are fixed constants, so no schema edit and no CMS entry can misroute the auth flow. The sign-in destination comes from the shared `resolveSignInHref` (`utils/safe-href.ts`) rather than from this component, so any other surface that has to send a signed-out shopper to sign in reaches the same place by the same rule. The `returnUrl` value is constrained to a same-origin relative path, which keeps the sign-in route from being turned into an open redirect; a path the narrowing refuses drops the parameter, and so does a router reporting no path, leaving a bare `/login` rather than a broken link. That is the sender's half of the guarantee. Nothing stops a hand-crafted link, so the receiving half — narrowing the parameter through `resolveSiteRelativeHref` on arrival — belongs to the route that will serve `/login`. The signed-out link also opts out of Next.js prefetching: a header control is always in the viewport, and the destination is an auth route rather than a page, so it can set a cookie or redirect and the default would fire that request on every page view.
64
64
 
65
65
  **`returnUrl` carries the path only — not the query string, not the hash.** A shopper signing in from `/s?q=tenis` or a filtered listing returns to the bare path. This is a deliberate narrowing rather than a limitation of the router: recovering the query would mean reading it through `useSearchParams()` and wrapping the header in a Suspense boundary, and the hash never leaves the browser at all. Search and filter state is cheap to re-apply and the sign-in round trip is rare, so the entry point keeps the simpler shape. Revisit it if merchants report losing filters across sign-in.
66
66
 
@@ -0,0 +1,160 @@
1
+ # AccountMenuIdentity
2
+
3
+ **Category:** identity
4
+ **Purpose:** The signed-in shopper's name and initials avatar, shown beside the account's section list so they can confirm whose account they are looking at.
5
+
6
+ ---
7
+
8
+ ## Visual cues (how to identify in an image)
9
+
10
+ - A small circle with one or two capital letters (`JD`) next to a person's full name, above or beside a column of account section links.
11
+ - It is shown only where the section list is a vertical column. On a narrow screen, where the same sections read as a horizontal row of pills, it is absent.
12
+ - A name only, never an email address, phone number, or edit control next to it (unless the account has no saved name, when the email stands in for it).
13
+
14
+ The image is the source of truth for layout.
15
+
16
+ ## When to use
17
+
18
+ - In the account's shared layout, beside the section list, to show whose account it is.
19
+
20
+ ## When NOT to use
21
+
22
+ - For a profile form or any editable field → use `MyAccountProfileRoot` and its controls.
23
+ - For a read-only identity card with e-mail, phone, and document → use `IdentitySummaryRoot`.
24
+ - For the header's sign-in or account control → use [[account-entry-point]].
25
+ - For a bare avatar with no account data → use [[avatar]].
26
+
27
+ ---
28
+
29
+ ## Composition
30
+
31
+ `AccountMenuIdentity` is a leaf that declares its own data read. It needs a `SessionRoot` ancestor and nothing else: it reads nothing from the menu, so it is valid anywhere under a session. In the `my-account` layout it is placed under `AccountAccessGate`, inside `AccountMenuRoot`, beside `AccountMenuList`.
32
+
33
+ ```
34
+ SessionRoot
35
+ └── AccountAccessGate
36
+ └── AccountMenuRoot
37
+ ├── AccountMenuIdentity ← beside the list, never inside it
38
+ ├── AccountMenuList
39
+ └── Slot ← the section's own page
40
+ ```
41
+
42
+ **Place it in the layout, never on a page.** It is `reactive`: its fragment joins the layout's query, which runs in the browser where the shopper's cookie is, and the profile is fetched once and reused as the shopper moves between sections.
43
+
44
+ **A missing profile costs only this block.** `customerProfile` is nullable. It answers `null` with no error for an anonymous visitor, and `null` with an error at its own path (`UNAUTHORIZED`, `UPSTREAM_ERROR`) when the read fails. The rest of the layout query, the session included, resolves as usual, so the gate and the menu are unaffected.
45
+
46
+ **Keep it outside `AccountMenuList`.** The list is the navigation landmark and surrounds its items alone; the identity block is not navigation.
47
+
48
+ The profile is optional to the menu, so it lives in its own component with its own fragment. A store that does not compose this block does not fetch the profile for the menu.
49
+
50
+ ---
51
+
52
+ ## Components
53
+
54
+ ### AccountMenuIdentity
55
+
56
+ No props in the schema. It accepts standard `<div>` attributes (`className`, `id`), and takes no children.
57
+
58
+ The block renders:
59
+
60
+ | State | What renders |
61
+ |---|---|
62
+ | Session not `loggedIn`, or still loading | Nothing |
63
+ | `loggedIn`, layout query pending | An `aria-hidden` skeleton (`data-fs-account-menu-identity-skeleton`) |
64
+ | `loggedIn`, `customerProfile` is `null` | Nothing |
65
+ | `loggedIn`, profile has neither a name nor an email | Nothing |
66
+ | `loggedIn`, profile resolved | The avatar and the name |
67
+
68
+ **The name** is the first and last names, each trimmed, joined with a space. With neither, the trimmed email stands in for it. **The avatar** shows initials derived from that same name: the first letters of the first and last words, or one letter for a single word. It shows initials only, because the profile has no photo field.
69
+
70
+ The avatar is hidden from assistive technology, so the name is announced once, as text.
71
+
72
+ ### AccountMenuIdentitySkeleton
73
+
74
+ The loading placeholder `AccountMenuIdentity` renders while the profile read is pending. No props. It needs no context.
75
+
76
+ ---
77
+
78
+ ## Canonical schema example
79
+
80
+ The my-account layout, with the identity block beside the section list:
81
+
82
+ ```json
83
+ {
84
+ "account-session": { "type": "SessionRoot", "children": ["account-gate"] },
85
+ "account-gate": { "type": "AccountAccessGate", "children": ["account-menu"] },
86
+ "account-menu": {
87
+ "type": "AccountMenuRoot",
88
+ "children": ["account-sidebar", "account-slot"]
89
+ },
90
+ "account-sidebar": {
91
+ "type": "Container",
92
+ "children": ["account-identity", "account-menu-list"]
93
+ },
94
+ "account-identity": {
95
+ "type": "AccountMenuIdentity",
96
+ "style": ["$account-menu-identity"]
97
+ },
98
+ "account-menu-list": { "type": "AccountMenuList", "props": { "label": "Account" } },
99
+ "account-slot": { "type": "Slot" }
100
+ }
101
+ ```
102
+
103
+ ## Common mistakes
104
+
105
+ - **Putting it inside `AccountMenuList`.** The list takes no children; the block sits beside it, outside the landmark.
106
+ - **Composing it on a page instead of the layout.** A page-scoped query runs on the server; the layout's runs in the browser with the shopper's cookie and is reused across sections.
107
+ - **Omitting `SessionRoot`.** The block reads the session and throws without one.
108
+ - **Expecting a photo.** It shows initials only. The profile has no photo field, so there is nothing to pass.
109
+ - **Reaching for `IdentitySummaryRoot`.** That is checkout's order-form identity card and needs a cart. The account sidebar reads the persistent profile.
110
+ - **Hiding it with a component prop.** There is none. Hide it below `md` through the swatch (`max-md:hidden`).
111
+
112
+ ---
113
+
114
+ ## Styling
115
+
116
+ | Component | What you style | Plausible classes |
117
+ |---|---|---|
118
+ | `AccountMenuIdentity` | `data-fs-account-menu-identity` is the block: lays the avatar beside the name and hides it on narrow screens | `max-md:hidden flex items-center gap-3` |
119
+ | Avatar | `data-fs-avatar`, with `data-fs-account-menu-identity-avatar`, is the circle | `size-10 rounded-full bg-gray-200 text-sm font-medium` |
120
+ | Name | `data-fs-account-menu-identity-name` is the name text | `m-0 truncate font-semibold` |
121
+ | Skeleton | `data-fs-account-menu-identity-skeleton`, with `-avatar` and `-line` parts, holds the block's place | `flex items-center gap-3` |
122
+
123
+ The avatar is composed inside the block, so it is not a schema element a swatch can name. Its look reaches it through descendant selectors on the block's hooks, as a store's swatch does:
124
+
125
+ ```json
126
+ {
127
+ "AccountMenuIdentity": {
128
+ "account-menu-identity": [
129
+ "max-md:hidden",
130
+ "flex",
131
+ "items-center",
132
+ "gap-3",
133
+ "[&_[data-fs-avatar]]:inline-flex",
134
+ "[&_[data-fs-avatar]]:size-10",
135
+ "[&_[data-fs-avatar]]:shrink-0",
136
+ "[&_[data-fs-avatar]]:items-center",
137
+ "[&_[data-fs-avatar]]:justify-center",
138
+ "[&_[data-fs-avatar]]:overflow-hidden",
139
+ "[&_[data-fs-avatar]]:rounded-full",
140
+ "[&_[data-fs-avatar]]:bg-gray-200",
141
+ "[&_[data-fs-avatar]]:text-sm",
142
+ "[&_[data-fs-avatar]]:font-medium",
143
+ "[&_[data-fs-account-menu-identity-name]]:m-0",
144
+ "[&_[data-fs-account-menu-identity-name]]:truncate",
145
+ "[&_[data-fs-account-menu-identity-name]]:font-semibold"
146
+ ]
147
+ }
148
+ }
149
+ ```
150
+
151
+ `max-md:hidden` is `display: none` below the `md` breakpoint, which removes the block from the layout and from the accessibility tree.
152
+
153
+ ---
154
+
155
+ ## Related
156
+
157
+ - [[avatar]] — the particle the block composes for its initials circle.
158
+ - [[account-menu]] — the section list the block sits beside.
159
+ - [[account-access-gate]] — the gate the layout places above both.
160
+ - `IdentitySummaryRoot` — checkout's read-only identity card, which reads the order form rather than the persistent profile.
@@ -28,12 +28,14 @@ The image is the source of truth for layout.
28
28
 
29
29
  ## Composition
30
30
 
31
- Two parts, and the split is the point. `AccountMenuRoot` is the provider and `AccountMenuList` is the landmark.
31
+ Two parts, and the split is the point. `AccountMenuRoot` is the provider and `AccountMenuList` is the landmark. A third, `AccountMenuIndexRedirect`, is the account root's only content and lives inside the slot. A fourth, `AccountMenuSignOut`, is the sidebar's way out and sits beside the list.
32
32
 
33
33
  ```text
34
34
  AccountMenuRoot
35
35
  ├── AccountMenuList
36
+ ├── AccountMenuSignOut ← beside the list, outside the landmark
36
37
  └── Slot ← the section's own page
38
+ └── AccountMenuIndexRedirect ← on /pvt/account only
37
39
  ```
38
40
 
39
41
  **The Root wraps the section's content; the landmark does not.** The Root's context has to reach consumers that live inside the slot — the account root's redirect, the sibling breadcrumb — and a context reaches only its descendants. A `<nav>` stretched that far would put every section's content inside a navigation region: a screen reader would announce the page as navigation and "skip to navigation" would skip nothing. So the Root wraps, and the `<nav>` stays around the items alone.
@@ -58,11 +60,11 @@ Content model (`account-menu`) fields, edited in the CMS:
58
60
  | `items[].href` | string | yes | Its destination; `http(s)`, `mailto:`, `tel:`, or a site-relative path |
59
61
  | `items[].external` | boolean | no | The entry leaves the account; it is never marked current |
60
62
 
61
- **The list comes from three channels, in order: a published entry, the authored `items` prop, then the library's five.** The default is Profile, Orders, Address, Payments, and Security, at `/account/profile` and its siblings — the set the PRD names. Presence decides, not emptiness: a channel supplying an empty list resolves to empty rather than falling through, because a merchant who cleared the list cleared it and the defaults must not override a deliberate publication.
63
+ **The list comes from three channels, in order: a published entry, the authored `items` prop, then the library's five.** The default is Profile, Orders, Address, Payments, and Security, at `/pvt/profile` and its siblings — the set the PRD names. Presence decides, not emptiness: a channel supplying an empty list resolves to empty rather than falling through, because a merchant who cleared the list cleared it and the defaults must not override a deliberate publication.
62
64
 
63
- **The current section is derived, never authored.** The component reads the current path and matches each entry's destination on segment boundaries. At most one entry is current: among the matches, the one with the longest destination wins, which makes an exact match beat every ancestor without a second rule. `/account/order` is not a match for `/account/orders` — a prefix that does not land on a segment boundary is not a match at all.
65
+ **The current section is derived, never authored.** The component reads the current path and matches each entry's destination on segment boundaries. At most one entry is current: among the matches, the one with the longest destination wins, which makes an exact match beat every ancestor without a second rule. `/pvt/order` is not a match for `/pvt/orders` — a prefix that does not land on a segment boundary is not a match at all.
64
66
 
65
- **An exact match announces `aria-current="page"`; an ancestor match announces `aria-current="true"`.** Inside `/account/orders/123`, Orders is the current item of its set but not the current page. That claim belongs to the breadcrumb's deep entry, and two surfaces announcing "current page" for two different things is worse than the menu claiming only its set.
67
+ **An exact match announces `aria-current="page"`; an ancestor match announces `aria-current="true"`.** Inside `/pvt/orders/123`, Orders is the current item of its set but not the current page. That claim belongs to the breadcrumb's deep entry, and two surfaces announcing "current page" for two different things is worse than the menu claiming only its set.
66
68
 
67
69
  Two kinds of entry are excluded from the match entirely: one marked `external`, and one whose destination is not site-relative. A destination of `/` is excluded from ancestor matching alone — a root destination is an ancestor of every account path, so a merchant listing the storefront's home beside the sections would otherwise see it current on every page. It still matches the path `/` exactly.
68
70
 
@@ -80,6 +82,58 @@ It takes no children: one `<li>` per resolved section is what it renders. With n
80
82
 
81
83
  A refused destination keeps its label and loses its anchor, rendering as text under `data-fs-account-menu-text`. A blank label drops the entry outright: an anchor with no accessible name is a WCAG failure.
82
84
 
85
+ ### AccountMenuIndexRedirect
86
+
87
+ Exposed as `AccountMenu.IndexRedirect`. The account's root has no section of its own, and which section is first is the merchant's call, so the root is a page holding this part alone.
88
+
89
+ | Prop | Type | Default | Notes |
90
+ |------|------|---------|-------|
91
+ | `loadingLabel` | string | — | Announced while the first section opens. |
92
+ | `emptyMessage` | string | `'No account section is configured.'` | What the root says when there is no section to open. |
93
+ | `className` | string | — | Applied to whichever element renders. |
94
+
95
+ Content model (`account-menu-index-redirect`) fields, edited in the CMS, each winning over its prop when published; a blank field falls through:
96
+
97
+ | Field | Type | Required | Notes |
98
+ |-------|------|----------|-------|
99
+ | `loadingLabel` | string | no | The waiting announcement |
100
+ | `emptyMessage` | string | no | The no-section message |
101
+
102
+ **It replaces the current history entry with the first section that stays in the account.** It reads the Root's resolved list and skips, in order: an entry marked `external`, a destination the allowlist refused, a destination that is not site-relative even though the merchant did not flag it, and the root itself in any spelling that matches it. Replace, never push: back from the section leaves the account instead of bouncing through its root.
103
+
104
+ **While a destination exists it renders the access gate's loading look**, `AccountAccessGateLoading`, with `data-fs-account-menu-index-redirect` beside the part's own hook. The list itself never loads — the Root resolves it synchronously — so what the look covers is the navigation: the frame before the replace is issued and the time the section takes to land. In the `my-account` layout the gate's own wait for the session comes first, so the shopper sees one look throughout.
105
+
106
+ **With nothing to open, the root is terminal.** A published empty list, or one holding external entries only, navigates nowhere and renders the message instead, under `data-fs-account-menu-index-redirect-empty`. The shell and the sidebar stay. The library's five defaults do not come back: the Root's presence rule is what keeps a published empty list empty, and this part reads the Root's result rather than choosing a channel of its own.
107
+
108
+ It inherits the menu's V0 assumption that public paths carry no binding prefix. Where a store's public base path is its binding id, the root's own entry is not recognized as the current page and the replace targets an unprefixed destination.
109
+
110
+ ### AccountMenuSignOut
111
+
112
+ Exposed as `AccountMenu.SignOut`. A button that ends the session and leaves the account for the home page. It reads nothing from the Root, so it works anywhere under the `my-account` layout's gate; the sidebar is where shoppers look for it.
113
+
114
+ | Prop | Type | Default | Notes |
115
+ |------|------|---------|-------|
116
+ | `label` | string | `'Sign out'` | The button's text. |
117
+ | `errorMessage` | string | `"We couldn't sign you out. Please try again."` | Shown when the sign-out did not happen. |
118
+ | `className` | string | — | Applied to the wrapper. |
119
+
120
+ Content model (`account-menu-sign-out`) fields, edited in the CMS, each winning over its prop when published; a blank field falls through:
121
+
122
+ | Field | Type | Required | Notes |
123
+ |-------|------|----------|-------|
124
+ | `label` | string | no | The button's text |
125
+ | `errorMessage` | string | no | The failure message |
126
+
127
+ The component declares `actions: { session: ['logout'] }` and writes only through `$actions.session.logout()`.
128
+
129
+ **It declares the sign-out intent before it awaits the logout.** The logout's payload flips the session to signed out in place, before any navigation can start. A gate that saw that session with no intent would read it as an expired session and send the shopper to sign in for the account they just left. With the intent declared first, the gate stands down — it renders nothing — from the click until the document is gone. The shopper sees the page empty while the request runs; that is the gate's look, not this part's.
130
+
131
+ **Success is a document load of `/`.** It uses `location.assign`, not the router, so no client cache keeps the account behind the back control. What the browser's back-forward cache restores is the gate's to revalidate: it withdraws the intent on a restored page and re-reads the session. This part adds no `pageshow` handling of its own.
132
+
133
+ **Failure keeps the shopper where they are.** When the logout reports any error, rejects, or has no handler to run, the part withdraws the intent, the gate brings the account back, and the message renders under `role="alert"`. A logout that reports an error is a sign-out that did not happen, so the shopper is never sent home still signed in. The failure is held outside the component, because the gate unmounts the part for the length of the attempt and mounts it anew on withdrawal. The next click clears it.
134
+
135
+ **One activation, one logout.** While an attempt runs, a second click neither declares again nor dispatches a second mutation. The button is also disabled and `aria-busy` while any sign-out intent stands, so a second sign-out control on the page stops too.
136
+
83
137
  ### useAccountMenu()
84
138
 
85
139
  Returns `{ items, current }` — the resolved sections and the current one as `{ index, match }`. It throws outside an `AccountMenuRoot`. This is the contract the account root's redirect and the sibling breadcrumb read, so the two surfaces cannot disagree about which section is current.
@@ -92,26 +146,45 @@ Returns `{ items, current }` — the resolved sections and the current one as `{
92
146
  {
93
147
  "account-menu": {
94
148
  "type": "AccountMenuRoot",
95
- "children": ["account-menu-list", "account-slot"],
96
- "style": ["$account-menu"]
149
+ "children": ["account-menu-list", "account-menu-sign-out", "account-slot"]
97
150
  },
98
151
  "account-menu-list": {
99
152
  "type": "AccountMenuList",
100
- "props": { "label": "Minha conta" },
101
- "style": ["$account-menu-list"]
153
+ "props": { "label": "Minha conta" }
154
+ },
155
+ "account-menu-sign-out": {
156
+ "type": "AccountMenuSignOut",
157
+ "props": { "label": "Sair", "errorMessage": "Não conseguimos encerrar sua sessão. Tente novamente." }
102
158
  }
103
159
  }
104
160
  ```
105
161
 
106
162
  Bound to a CMS entry, the Root authors no `items` at all — the entry supplies the whole list.
107
163
 
164
+ No part carries a `style` reference: each takes its type's `base` swatch from `global.json → styles` on its own. Reference a named swatch only when the store defines one, since a `$name` the store does not define expands to nothing, without an error.
165
+
166
+ The account root's page, whose only content is the redirect:
167
+
168
+ ```json
169
+ {
170
+ "account-index-redirect": {
171
+ "type": "AccountMenuIndexRedirect",
172
+ "props": { "loadingLabel": "Abrindo sua conta…", "emptyMessage": "Nenhuma seção configurada." }
173
+ }
174
+ }
175
+ ```
176
+
108
177
  ## Common mistakes
109
178
 
110
179
  - **Placing the Root beside the slot instead of around it.** The layout's own consumers would be satisfied and the page's would not: a context reaches only descendants. Context-constraint validation reports it as a build error rather than as an empty menu at runtime.
111
180
  - **Putting the `<nav>` around the section's content.** That is what the two-part split exists to prevent; do not wrap the slot in `AccountMenuList`.
112
181
  - **Leaving `label` unauthored.** The default is `Account`; a store with its own wording should say so, because a landmark's name is how a screen-reader user tells it from every other one.
113
182
  - **Styling the current item and nothing else.** The hook exists beside `aria-current`, never instead of it. Removing the programmatic signal leaves position conveyed by appearance alone.
114
- - **Publishing an empty list and expecting the defaults.** Empty is a configuration, not an absence. The account's root then has no section to open.
183
+ - **Publishing an empty list and expecting the defaults.** Empty is a configuration, not an absence. The account's root then has no section to open, and says so.
184
+ - **Placing `AccountMenuIndexRedirect` on a section's page.** It navigates away from whatever page hosts it; it belongs on `/pvt/account` alone.
185
+ - **Putting an external entry first and expecting the root to follow it.** It is skipped, as is an absolute destination left unflagged; the root opens the first section that stays in the account.
186
+ - **Placing `AccountMenuSignOut` outside the gate.** Nothing stands down while the logout runs, so the page shows the signed-out session for the moment before it leaves. Compose it under the `my-account` layout's gate.
187
+ - **Signing out with the router, or adding `pageshow` handling to the control.** The document load is what keeps the account off the back control, and the gate owns the back-forward cache.
115
188
  - **Authoring `$content` in the schema.** `$content` is compiler-injected from the page's or layout's content entry; naming it in `props` fails the build.
116
189
  - **Expecting the default five to resolve.** Their routes exist only in a store that authored the account's route family; until then the default is correct in the example store alone.
117
190
 
@@ -128,37 +201,47 @@ Bound to a CMS entry, the Root authors no `items` at all — the entry supplies
128
201
  | Current item | `data-fs-account-menu-item-current` marks the current row | `font-semibold` |
129
202
  | Link | `data-fs-account-menu-link` is the anchor | `block rounded-md px-3 py-2 text-sm` |
130
203
  | Refused text | `data-fs-account-menu-text` replaces the anchor for a refused destination | `block px-3 py-2 text-gray-400` |
204
+ | `AccountMenuIndexRedirect` | `data-fs-account-menu-index-redirect` is on both of its looks; the wait also carries the gate's `data-fs-account-access-gate-loading` | `text-sm text-gray-600` |
205
+ | No-section message | `data-fs-account-menu-index-redirect-empty` marks the terminal state | `flex-1 p-6` |
206
+ | `AccountMenuSignOut` | `data-fs-account-menu-sign-out` is the wrapper; `data-fs-account-menu-sign-out-pending` is on it while a sign-out runs | `flex flex-col gap-2 px-3` |
207
+ | Sign-out button | `data-fs-account-menu-sign-out-button` is the `<button>`; style `:disabled` for the wait | `text-left text-sm underline` |
208
+ | Sign-out failure | `data-fs-account-menu-sign-out-error` is the alert | `text-sm text-red-600` |
131
209
 
132
- The vertical-to-horizontal switch and the scroll affordance are one DOM styled two ways, authored as the `AccountMenu` swatch set in `global.json → styles`:
210
+ The vertical-to-horizontal switch and the scroll affordance are one DOM styled two ways, in CSS alone. A store authors them as `AccountMenuList.base` in `global.json → styles`, abridged here:
133
211
 
134
212
  ```json
135
213
  {
136
- "AccountMenuRoot": {
137
- "account-menu": [
138
- "flex",
139
- "flex-col",
140
- "gap-6",
141
- "md:flex-row",
142
- "md:gap-8",
214
+ "AccountMenuList": {
215
+ "base": [
143
216
  "[&_[data-fs-account-menu-items]]:flex",
144
- "[&_[data-fs-account-menu-items]]:gap-2",
145
- "[&_[data-fs-account-menu-items]]:overflow-x-auto",
146
217
  "md:[&_[data-fs-account-menu-items]]:flex-col",
147
- "md:[&_[data-fs-account-menu-items]]:overflow-visible",
148
- "[&_[data-fs-account-menu-item-current]_[data-fs-account-menu-link]]:bg-gray-100",
149
- "[&_[data-fs-account-menu-item-current]_[data-fs-account-menu-link]]:font-semibold"
218
+ "max-md:[&_[data-fs-account-menu-items]]:overflow-x-auto",
219
+ "max-md:[&_[data-fs-account-menu-items]]:scroll-pe-[32px]",
220
+ "max-md:[&_[data-fs-account-menu-items]]:mask-[linear-gradient(to_right,#000_calc(100%-32px),transparent)]",
221
+ "max-md:[&_[data-fs-account-menu-item]:last-child]:pe-[32px]",
222
+ "[&_[data-fs-account-menu-item]]:shrink-0",
223
+ "[&_[data-fs-account-menu-link]]:whitespace-nowrap",
224
+ "[&_[data-fs-account-menu-link]]:border-transparent",
225
+ "max-md:[&_[data-fs-account-menu-link]]:border-b-2",
226
+ "md:[&_[data-fs-account-menu-link]]:border-l-2",
227
+ "[&_[data-fs-account-menu-item-current]_[data-fs-account-menu-link]]:border-gray-900"
150
228
  ]
151
229
  }
152
230
  }
153
231
  ```
154
232
 
155
- The current-item look reaches the anchor through the row, since the hook is on the `<li>` and the anchor is its child.
233
+ - **The masked edge needs space as wide as its fade.** Give the last item a trailing padding and the `<ul>` a `scroll-padding-inline-end` equal to the fade width. Without the padding, a row scrolled to its end leaves the last label under the fade. Without the scroll padding, bringing a current last item into view parks it there.
234
+ - **Reserve the current rule on every link in `transparent`.** Becoming current then changes a color, never a width, so the row does not shift on a route change.
235
+ - **The current-item look reaches the anchor through the row**, since the hook is on the `<li>` and the anchor is its child.
236
+ - **Keep the list's rules off the Root.** The Root wraps the section's content, so a descendant rule there reaches into every page. The Root's own classes and the layout's wrappers place the sidebar beside the section.
156
237
 
157
238
  ---
158
239
 
159
240
  ## Related
160
241
 
161
242
  - [[account-entry-point]] — the header control that links into the account.
243
+ - [[account-access-gate]] — the loading look the root's redirect reuses, and the gate it runs under.
244
+ - [[account-menu-identity]] — the shopper's name and avatar, composed beside the list inside the Root.
162
245
  - [[navigation-link]] — the client-side link each item composes.
163
246
  - [[navigation-link-list]] — the general-purpose CMS-driven list this one is patterned on.
164
247
  - `BreadcrumbRoot` — the sibling trail that keeps the "current page" claim for the deep page.
@@ -0,0 +1,103 @@
1
+ # Avatar
2
+
3
+ **Category:** content
4
+ **Purpose:** A person or account marker: an image with an initials fallback derived from a name. It carries no account data of its own.
5
+
6
+ ---
7
+
8
+ ## Visual cues (how to identify in an image)
9
+
10
+ - A small circle, usually 32–48px, holding one or two capital letters — `JD`, `A` — on a flat tinted background.
11
+ - Or the same circle holding a round photo of a person.
12
+ - It sits beside a person's name, in a comment, a profile header, or an account sidebar.
13
+
14
+ The image is the source of truth for layout.
15
+
16
+ ## When to use
17
+
18
+ - Wherever a person or account needs a compact visual marker beside their name.
19
+ - When there may be no photo: the initials are the fallback, not a separate component.
20
+
21
+ ## When NOT to use
22
+
23
+ - For a product or content image → use `Image`.
24
+ - For a status or count label → use `Badge`.
25
+ - For a logo → use `Logo`.
26
+
27
+ ---
28
+
29
+ ## Composition
30
+
31
+ `Avatar` is a leaf. It renders one `<span>` holding the image, when there is one, and the initials fallback. It has no children and requires no ancestor.
32
+
33
+ ```
34
+ Container
35
+ └── Avatar
36
+ ```
37
+
38
+ ---
39
+
40
+ ## Components
41
+
42
+ ### Avatar
43
+
44
+ | Prop | Type | Default | Notes |
45
+ |------|------|---------|-------|
46
+ | `name` | string | — | Whose avatar this is. The initials derive from it: the first letters of the first and last words, or one letter for a single word. |
47
+ | `src` | string | — | Image URL. Absent or empty, the initials show. |
48
+ | `alt` | string | — | Accessible name. Without it the avatar is decorative and hidden from assistive technology. |
49
+
50
+ The fallback shows whenever there is no image, the image is still loading, or it failed to load. The image replaces it only once it has loaded.
51
+
52
+ ---
53
+
54
+ ## Canonical schema example
55
+
56
+ ```json
57
+ {
58
+ "shopper-avatar": {
59
+ "type": "Avatar",
60
+ "props": { "name": "Jane Doe", "alt": "Jane Doe" },
61
+ "style": ["inline-flex", "size-10", "items-center", "justify-center", "rounded-full", "bg-gray-200", "text-sm", "font-medium"]
62
+ }
63
+ }
64
+ ```
65
+
66
+ ## Common mistakes
67
+
68
+ - **Leaving `alt` off a standalone avatar.** Without `alt` it is hidden from assistive technology, which is right beside text that already names the person and wrong anywhere else. Give it an `alt`.
69
+ - **Giving it an `alt` beside the person's name.** The name is then announced twice.
70
+ - **Expecting a photo from `name` alone.** `name` only derives initials; a photo needs `src`.
71
+ - **Inventing `ProfilePicture` or `UserIcon`.** The component is `Avatar`.
72
+ - **Styling without a size and shape.** The particle ships no look: give it a size, `rounded-full`, and overflow hidden, or the image and initials render unconstrained.
73
+
74
+ ---
75
+
76
+ ## Styling
77
+
78
+ | Component | What you style | Plausible classes |
79
+ |---|---|---|
80
+ | `Avatar` | `data-fs-avatar` is the circle: size, shape, background, and the initials' type | `inline-flex size-10 shrink-0 items-center justify-center overflow-hidden rounded-full bg-[#E5E7EB] text-[14px] font-medium` |
81
+ | Image | `data-fs-avatar-image` is the `<img>` once loaded | `size-full object-cover` |
82
+ | Initials | `data-fs-avatar-fallback` is the initials span | `uppercase` |
83
+
84
+ ```json
85
+ {
86
+ "shopper-avatar": {
87
+ "type": "Avatar",
88
+ "props": { "name": "Jane Doe", "alt": "Jane Doe" },
89
+ "style": [
90
+ "inline-flex", "size-10", "shrink-0", "items-center", "justify-center", "overflow-hidden",
91
+ "rounded-full", "bg-[#E5E7EB]", "text-[14px]", "font-medium", "[&_img]:size-full", "[&_img]:object-cover"
92
+ ]
93
+ }
94
+ }
95
+ ```
96
+
97
+ ---
98
+
99
+ ## Related
100
+
101
+ - [[badge]] — a status or count label; distinct from a person marker.
102
+ - `Image` — a content image with no fallback or initials.
103
+ - [[account-menu-identity]] — the account sidebar's name-and-avatar block, which composes `Avatar`.
@@ -61,7 +61,7 @@ A field Control is absent from the composition means absent from both states and
61
61
  - **Save order**: compute dirtiness first → nothing dirty raises the saved toast without dispatching → otherwise validate only the dirty registered fields' current draft values (Decision 9) → any error blocks the dispatch and focuses the first invalid field in rendered DOM order → otherwise normalize dirty values and dispatch one `$actions.identity.customerProfileUpdate` → a `userError` raises an error toast and keeps the edit state with the draft intact → success returns to the view state and raises a success toast (Decision 10). A field the shopper never touched is neither validated nor sent.
62
62
  - **Edit always opens from the latest saved values**: pressing Edit re-seeds the draft from `savedValues` before switching modes, so a value the server normalized on a previous save (not this task's concern, but a sibling Control's) is what the shopper sees, not a stale local draft.
63
63
  - **Background re-delivery**: when `$data` delivers new values while editing, a field re-seeds only if its draft still equals the previously saved value; a dirty field is never clobbered.
64
- - **Self-gate**: renders nothing outside `state === 'loggedIn'`, even if the topology's `SessionGuard` is missing. Renders `MyAccountProfileSkeleton` while `$data` is unresolved and the shopper is logged in — never an empty summary.
64
+ - **Self-gate**: renders nothing outside `state === 'loggedIn'`, even if the topology's `SessionGuard` is missing. Renders `MyAccountProfileSkeleton` while `$data` is unresolved and the shopper is logged in — never an empty summary. `customerProfile` is nullable, so `$data` is also `null` for an anonymous session (the self-gate renders nothing) and for a logged-in shopper whose read failed (the skeleton stays).
65
65
  - **PII**: no profile value appears in any log line. A write that throws (rather than resolving `userErrors`) logs only the error's name; the message shown to the shopper is the `saveFailedMessage` schema prop.
66
66
 
67
67
  ---
@@ -1,7 +1,7 @@
1
1
  # ProductShelf
2
2
 
3
3
  **Category:** commerce
4
- **Purpose:** A data-owning product shelf. `ProductShelfRoot` owns a facet-filtered `products(filter:)` query from the catalog module, receives its slice as `$data`, and publishes each product through a per-item context. `ProductShelfItems` repeats those products into a `Carousel` track, one card per slide. The card parts (`ProductCard*`) read each product from context — the shelf holds the data and config, the carousel arranges the rows, the cards render one product each.
4
+ **Purpose:** A data-owning product shelf. `ProductShelfRoot` owns a facet-filtered `products(first:, orderBy:, filter:)` query from the catalog module whose arguments bind to this element's configuration (the merchant's CMS entry, else the props), receives its slice as `$data`, and publishes each product through a per-item context. `ProductShelfItems` repeats those products into a `Carousel` track, one card per slide. The card parts (`ProductCard*`) read each product from context — the shelf holds the data and config, the carousel arranges the rows, the cards render one product each.
5
5
 
6
6
  ---
7
7
 
@@ -18,7 +18,7 @@ A ProductShelf block contains any combination of:
18
18
 
19
19
  If the image shows **any of the above**, use `ProductShelfRoot` composed with `CarouselRoot` and `ProductShelfItems`. When a section heading is visible, compose `ProductShelfHeading` inside `ProductShelfRoot` (above `CarouselRoot`) with the title as its `children` (like any `Text`). Pick only the card parts (`ProductCardImage`, `ProductCardName`, `ProductCardPrice`, `ProductCardListPrice`, `ProductCardDiscount`, …) that match what's visible in each card.
20
20
 
21
- The image is the source of truth for the heading text, `first`/`itemsPerPage`, and which card parts to include. The **source of the shelf's products** (`categoryId` / `brandId` / `collectionId` / `searchTerm` / `specifications`) is a merchant configuration decision and cannot be read off the screenshot — leave those unset when the image gives no signal, and let the shelf fall back to catalog-wide relevance.
21
+ The image is the source of truth for the heading text, `first`/`itemsPerPage`, and which card parts to include. The **source of the shelf's products** (`categoryId` / `brandId` / `collectionId` / `searchTerm`) is a merchant configuration decision and cannot be read off the screenshot — leave those unset when the image gives no signal, and let the shelf fall back to catalog-wide relevance.
22
22
 
23
23
  ---
24
24
 
@@ -36,8 +36,9 @@ The image is the source of truth for the heading text, `first`/`itemsPerPage`, a
36
36
 
37
37
  ## Behavior model
38
38
 
39
- - **The shelf owns the data.** `ProductShelfRoot` declares a `fragment ProductShelfData on Query` selecting `products(first: $first, orderBy: $orderBy, filter: $filter)` and receives the resolved slice as `$data`. `fetchStrategy: 'lazy'` defers the load (below-the-fold), and a skeleton shows while pending.
40
- - **Config feeds the query.** The serializable schema props map to the fragment variables: `first` → `$first`, `sort` → `$orderBy` (`ProductSortingInput`), and the facet props (`searchTerm`, `categoryId`, `brandId`, `collectionId`, `specifications`, `availableOnly`) → `$filter` (`ProductsFilterInput`). `buildProductShelfVariables(config)` is the pure mapping.
39
+ - **The shelf owns the data.** `ProductShelfRoot` declares a `fragment ProductShelfData on Query` selecting `products(first: $first, orderBy: $orderBy, filter: { categoryIds: $categoryId, brandIds: $brandId, collectionId: $collectionId, searchTerm: $searchTerm, availableOnly: $availableOnly })` and receives the resolved slice as `$data`. `fetchStrategy: 'lazy'` defers the load (below-the-fold), and a skeleton shows while pending.
40
+ - **The element's configuration feeds the query.** Every variable binds to this element: the compiler reads the field of the same name from the element's content slice (the `product-shelf` content model, in a store with a `cms` binding) and otherwise from the element's prop, then instances the fragment once per shelf on the page, so two shelves with different `first` or `sort` become two aliased `products` fields in one page query. `sort` reaches `$orderBy` (`ProductSortingInput`) through the `values` table in the definition's `variables` block, which equals `PRODUCT_SHELF_SORT_MAP`. A facet left empty is not sent, so the filter relaxes instead of sending `null`; `first` is honored by the API, never sliced on the client.
41
+ - **Props beside content.** Each content field is also a schema prop of the same name. The CMS value wins per field; the prop is the developer default and the only source in a store with no `cms` binding (Vivara, Storybook, tests). No field is `required`, so an unfilled entry never omits the shelf. `useProductShelf()` exposes the resolved `config` (`$content?.field ?? field`), `itemsPerPage` and `count`.
41
42
  - **Two context boundaries.** The shelf owns product data + config; each row owns one item. Card parts read the item through a guarded hook and never take list-shaped props.
42
43
  - **Presence is state-driven.** Empty result → the shelf renders nothing (no empty carousel). Unresolved lazy source → skeleton.
43
44
  - **The carousel is composed, not baked.** Place `CarouselRoot` with `ProductShelfItems` inside it, plus `CarouselPrevious` / `CarouselNext` / `CarouselBullets` where you want them. `ProductShelfItems` renders the `CarouselTrack`, so the carousel counts one slide per product.
@@ -51,17 +52,23 @@ The image is the source of truth for the heading text, `first`/`itemsPerPage`, a
51
52
 
52
53
  | Prop | Type | Default | Notes |
53
54
  |------|------|---------|-------|
54
- | `first` | number | `5` | Max products fetched (legacy `numberOfItems`) → `$first` |
55
55
  | `itemsPerPage` | number | `5` | Cards per page; bind to `CarouselRoot`'s `itemsPerPage` |
56
- | `sort` | `relevance \| price_asc \| price_desc \| name_asc \| name_desc \| release \| orders \| discount` | `relevance` | → `$orderBy` |
57
- | `searchTerm` | string | — | → `filter.searchTerm` |
58
- | `categoryId` | string | — | → `filter.categoryIds` |
59
- | `brandId` | string | — | → `filter.brandIds` |
60
- | `collectionId` | string | — | → `filter.collectionId` (legacy `productClusterIds`) |
61
- | `specifications` | `{ name, values }[]` | — | → `filter.specifications` (custom attributes) |
62
- | `availableOnly` | boolean | `false` | → `filter.availableOnly` (global "hide out-of-stock") |
56
+ | `usePriceWithTaxes` | boolean | `false` | Card prices render tax-inclusive |
57
+ | `taxesLabel` | string | `'Tax included'` | Text for the tax label part |
58
+ | `first` | number | `5` | Content field. Products fetched (legacy `numberOfItems`) → `$first` |
59
+ | `sort` | `relevance \| price_asc \| price_desc \| name_asc \| name_desc \| release \| orders \| discount` | `relevance` | Content field → `$orderBy` through the `values` table |
60
+ | `searchTerm` | string | — | Content field → `filter.searchTerm` |
61
+ | `categoryId` | string | — | Content field → `filter.categoryIds` |
62
+ | `brandId` | string | — | Content field → `filter.brandIds` |
63
+ | `collectionId` | string | — | Content field → `filter.collectionId` (legacy `productClusterIds`) |
64
+ | `availableOnly` | boolean | `false` | Content field → `filter.availableOnly` (global "hide out-of-stock") |
65
+ | `$content` | slice | — | Compiler-injected `product-shelf` entry; each field wins over the prop of the same name |
63
66
  | `$data` | slice | — | Compiler-injected fragment result |
64
- | `products` | `ProductShelfItem[]` | — | Pre-shaped items (stories/tests; bypasses `$data`) |
67
+ | `products` | `ProductShelfItem[]` | — | Pre-shaped items (stories/tests; bypasses `$data`). Rendered as given: `first` does not limit them |
68
+
69
+ ### Content model `product-shelf`
70
+
71
+ `ProductShelfContent` declares the merchant-facing fields: `first` (number, minimum 1), `sort` (the eight members above), `categoryId`, `brandId`, `collectionId`, `searchTerm` (string) and `availableOnly` (boolean). None is required. In a store with a `cms` binding the merchant fills them in the Content Platform entry keyed by the element id, and the shelf re-queries on the next request with no deploy; an empty field falls to the prop, and an empty facet relaxes the filter.
65
72
 
66
73
  ### ProductShelfItems
67
74
 
package/dist/index.d.ts CHANGED
@@ -2,7 +2,7 @@ import { ActionSelector } from "./core/dist/actions/registry.js";
2
2
  import { MockActions, mockAction } from "./core/dist/actions/mock-action.js";
3
3
  import { JsonValue, StorefrontContext } from "./core/dist/schema/generated/schema.js";
4
4
  import { ContentArraySchema, ContentNestedObjectShape, ContentObjectPropertySchema, ContentObjectSchema, ContentPropertySchema, ContentPropertyType, ContentScalarSchema, ContentScalarType, ContentTypeDefinition, ContentValue, defineContentType } from "./core/dist/define-content-type.js";
5
- import { ComponentDefinition, EditorMetadata, FetchStrategy, PropSchema, PropSchemaField, PropType, defineComponent } from "./core/dist/define-component.js";
5
+ import { ComponentDefinition, EditorMetadata, FetchStrategy, PropSchema, PropSchemaField, PropType, VariableBinding, VariableBindings, defineComponent } from "./core/dist/define-component.js";
6
6
  import "./core/dist/index.js";
7
7
  import { CartUserError } from "./sdk/cart/operations.js";
8
8
  import { AddItemPayload, CartActions, CartCouponPayload, CartLinePayload, CartLinesPayload, CartQuantityPayload } from "./sdk/cart/use-cart-actions.js";
@@ -19,4 +19,4 @@ import { ShippingOptionSelectUserError } from "./sdk/shipping/option-select-oper
19
19
  import { ShippingAddressInput, ShippingAddressUpdateErrorEntity } from "./sdk/shipping/shipping-operations.js";
20
20
  import { ShippingActions } from "./sdk/shipping/use-shipping-actions.js";
21
21
  import { ComponentActions } from "./sdk/actions/types.js";
22
- export { type ActionSelector, type AddItemPayload, type CartActions, type CartCouponPayload, type CartLinePayload, type CartLinesPayload, type CartQuantityPayload, type CartUserError, type ClientPreferenceKey, type ClientPreferencesUpdatePayload, type ClientProfileUpdatePayload, type ClientProfileUserError, type ComponentActions, type ComponentDefinition, type ContentArraySchema, type ContentNestedObjectShape, type ContentObjectPropertySchema, type ContentObjectSchema, type ContentPropertySchema, type ContentPropertyType, type ContentScalarSchema, type ContentScalarType, type ContentTypeDefinition, type ContentValue, type CustomerProfileUpdatePayload, type CustomerProfileUserError, type EditorMetadata, type FetchStrategy, type IdentityActions, type JsonValue, type MockActions, type PaymentActions, type PaymentBillingAddressUpdateInput, type PaymentGatewayCallbackResult, type PaymentGatewayCard, type PaymentGatewayNewCard, type PaymentGatewaySavedCard, type PaymentGatewayTransaction, type PaymentMethodSelectInput, type PaymentMoney, type PaymentStartTransactionResult, type PaymentUserError, type PropSchema, type PropSchemaField, type PropType, type RegionActions, type RegionUserError, type SendPaymentsRequest, type SessionAccessCodeRequestResult, type SessionActions, type SessionPasswordLoginPayload, type SessionRequestAccessCodePayload, type SessionUserError, type SessionValidateAccessCodePayload, type ShippingActions, type ShippingAddressInput, type ShippingAddressUpdateErrorEntity, type ShippingOptionSelectUserError, type StorefrontContext, defineComponent, defineContentType, mockAction };
22
+ export { type ActionSelector, type AddItemPayload, type CartActions, type CartCouponPayload, type CartLinePayload, type CartLinesPayload, type CartQuantityPayload, type CartUserError, type ClientPreferenceKey, type ClientPreferencesUpdatePayload, type ClientProfileUpdatePayload, type ClientProfileUserError, type ComponentActions, type ComponentDefinition, type ContentArraySchema, type ContentNestedObjectShape, type ContentObjectPropertySchema, type ContentObjectSchema, type ContentPropertySchema, type ContentPropertyType, type ContentScalarSchema, type ContentScalarType, type ContentTypeDefinition, type ContentValue, type CustomerProfileUpdatePayload, type CustomerProfileUserError, type EditorMetadata, type FetchStrategy, type IdentityActions, type JsonValue, type MockActions, type PaymentActions, type PaymentBillingAddressUpdateInput, type PaymentGatewayCallbackResult, type PaymentGatewayCard, type PaymentGatewayNewCard, type PaymentGatewaySavedCard, type PaymentGatewayTransaction, type PaymentMethodSelectInput, type PaymentMoney, type PaymentStartTransactionResult, type PaymentUserError, type PropSchema, type PropSchemaField, type PropType, type RegionActions, type RegionUserError, type SendPaymentsRequest, type SessionAccessCodeRequestResult, type SessionActions, type SessionPasswordLoginPayload, type SessionRequestAccessCodePayload, type SessionUserError, type SessionValidateAccessCodePayload, type ShippingActions, type ShippingAddressInput, type ShippingAddressUpdateErrorEntity, type ShippingOptionSelectUserError, type StorefrontContext, type VariableBinding, type VariableBindings, defineComponent, defineContentType, mockAction };
@@ -1,11 +1,11 @@
1
1
  import { gql } from "../sdk/graphql-tag.js";
2
2
  import { DataOverrideContext, useDataOverride } from "../sdk/data/data-override-context.js";
3
+ import { PageReactiveDataRoot, ReactiveRootContext } from "../sdk/data/page-reactive-data-root.js";
3
4
  import { HotDataProvider } from "../sdk/data/hot-data-provider.js";
4
5
  import { publishedVariantIdFor, selectedVariantVar } from "../sdk/state/variant-selector.js";
5
6
  import { cartCouponErrorsVar, cartLastAddVar, cartUserErrorsVar, miniCartOpenVar, orderFormIdVar } from "../sdk/state/cart.js";
6
7
  import { buildOptimisticDecrease, buildOptimisticIncrease, buildOptimisticRemove } from "../sdk/cart/optimistic.js";
7
8
  import { ListItemRefContext, useListItemRef } from "../sdk/data/list-item-ref-context.js";
8
- import { PageReactiveDataRoot, ReactiveRootContext } from "../sdk/data/page-reactive-data-root.js";
9
9
  import { AnalyticsActionProvider } from "../sdk/actions/analytics-action-provider.js";
10
10
  import { CartActionProvider } from "../sdk/actions/cart-action-provider.js";
11
11
  import { IdentityActionProvider } from "../sdk/actions/identity-action-provider.js";
@@ -1,2 +1,2 @@
1
- import { ContentEntryBoundaryProps, ContentEntryResult, ContentRuntime, ContentRuntimeConfig, ContentSlices, ReadContentSlicesArgs, createContentRuntime } from "../sdk/content/create-content-runtime.js";
2
- export { type ContentEntryBoundaryProps, type ContentEntryResult, type ContentRuntime, type ContentRuntimeConfig, type ContentSlices, type ReadContentSlicesArgs, createContentRuntime };
1
+ import { ContentEntryBoundaryProps, ContentEntryResult, ContentItemResult, ContentRuntime, ContentRuntimeConfig, ContentSlices, ReadContentItemArgs, ReadContentSlicesArgs, createContentRuntime } from "../sdk/content/create-content-runtime.js";
2
+ export { type ContentEntryBoundaryProps, type ContentEntryResult, type ContentItemResult, type ContentRuntime, type ContentRuntimeConfig, type ContentSlices, type ReadContentItemArgs, type ReadContentSlicesArgs, createContentRuntime };
@@ -2,6 +2,9 @@ import { buildStorefrontContextHeaders } from "../sdk/context/headers.js";
2
2
  import { CreateClientOptions, createClient } from "../sdk/client/create-client.js";
3
3
  import { FetchPageDataOptions, FetchPageDataResult, fetchPageData } from "../sdk/client/fetch-page-data.js";
4
4
  import { PreloadQueryRegistry, RegisterPreloadQueryOptions, registerPreloadQuery } from "../sdk/client/register-preload-query.js";
5
+ import { ContentVariableField, ContentVariablesSpec, VariablesFromContentOptions, variablesFromContent } from "../sdk/data/variables-from-content.js";
5
6
  import { getIntl, loadMessages } from "../sdk/i18n/server.js";
7
+ import { ServerSessionRead, readServerSession } from "../sdk/session/read-server-session.js";
8
+ import { resolveSignInHref } from "../utils/safe-href.js";
6
9
  import { PreloadQueryProps } from "@apollo/client-integration-nextjs";
7
- export { type CreateClientOptions, type FetchPageDataOptions, type FetchPageDataResult, type PreloadQueryProps, type PreloadQueryRegistry, type RegisterPreloadQueryOptions, buildStorefrontContextHeaders, createClient, fetchPageData, getIntl, loadMessages, registerPreloadQuery };
10
+ export { type ContentVariableField, type ContentVariablesSpec, type CreateClientOptions, type FetchPageDataOptions, type FetchPageDataResult, type PreloadQueryProps, type PreloadQueryRegistry, type RegisterPreloadQueryOptions, type ServerSessionRead, type VariablesFromContentOptions, buildStorefrontContextHeaders, createClient, fetchPageData, getIntl, loadMessages, readServerSession, registerPreloadQuery, resolveSignInHref, variablesFromContent };