@vtex/faststore 0.0.4 → 0.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/dist/catalog.json +67 -1
- package/dist/components.d.ts +4 -1
- package/dist/components.js +5 -2
- package/dist/content-types.json +73 -0
- package/dist/docs/account-access-gate.md +153 -0
- package/dist/docs/account-entry-point.md +1 -2
- package/dist/docs/account-menu.md +164 -0
- package/dist/docs/buy-button.md +1 -1
- package/dist/docs/cart.md +1 -1
- package/dist/docs/session.md +1 -1
- package/dist/docs/sign-in-redirect.md +1 -1
- package/dist/matter/account-access-gate/account-access-gate.d.ts +86 -0
- package/dist/matter/account-access-gate/account-access-gate.js +283 -0
- package/dist/matter/account-access-gate/account-access-gate.js.map +1 -0
- package/dist/matter/account-access-gate/sign-out-intent.d.ts +17 -0
- package/dist/matter/account-access-gate/sign-out-intent.js +62 -0
- package/dist/matter/account-access-gate/sign-out-intent.js.map +1 -0
- package/dist/matter/account-entry-point/account-entry-point.js +2 -2
- package/dist/matter/account-entry-point/account-entry-point.js.map +1 -1
- package/dist/matter/account-menu/account-menu.d.ts +88 -0
- package/dist/matter/account-menu/account-menu.js +246 -0
- package/dist/matter/account-menu/account-menu.js.map +1 -0
- package/dist/matter/cart/cart.d.ts +1 -1
- package/dist/matter/cart/cart.js +2 -2
- package/dist/matter/cart/cart.js.map +1 -1
- package/dist/matter/delivery-options/delivery-options.js.map +1 -1
- package/dist/matter/my-account-profile/my-account-profile.js +1 -1
- package/dist/matter/my-account-profile/my-account-profile.js.map +1 -1
- package/dist/matter/shipping-address/shipping-address.js +1 -1
- package/dist/matter/shipping-address/shipping-address.js.map +1 -1
- package/dist/matter/shipping-simulator/shipping-simulator.js.map +1 -1
- package/dist/particles/footer-social/footer-social.js +1 -1
- package/dist/particles/incentives/incentives.js +1 -1
- package/dist/sdk/actions/cart-action-provider.d.ts +1 -1
- package/dist/sdk/actions/cart-action-provider.js +1 -1
- package/dist/sdk/actions/cart-action-provider.js.map +1 -1
- package/dist/sdk/actions/session-action-provider.d.ts +1 -1
- package/dist/sdk/actions/session-action-provider.js +1 -1
- package/dist/sdk/actions/session-action-provider.js.map +1 -1
- package/dist/sdk/actions/shipping-action-provider.d.ts +1 -1
- package/dist/sdk/actions/shipping-action-provider.js +1 -1
- package/dist/sdk/actions/shipping-action-provider.js.map +1 -1
- package/dist/sdk/cache/storefront-cache.js +1 -1
- package/dist/sdk/cache/storefront-cache.js.map +1 -1
- package/dist/sdk/cart/operations.d.ts +4 -4
- package/dist/sdk/cart/operations.js +1 -1
- package/dist/sdk/cart/operations.js.map +1 -1
- package/dist/sdk/cart/optimistic.js +1 -1
- package/dist/sdk/cart/optimistic.js.map +1 -1
- package/dist/sdk/cart/use-cart-actions.d.ts +1 -1
- package/dist/sdk/cart/use-cart-actions.js +3 -3
- package/dist/sdk/cart/use-cart-actions.js.map +1 -1
- package/dist/sdk/cart/use-cart-validate-on-open.js +1 -1
- package/dist/sdk/cart/use-cart-validate-on-open.js.map +1 -1
- package/dist/sdk/cart/write-gate.js +1 -1
- package/dist/sdk/cart/write-gate.js.map +1 -1
- package/dist/sdk/data/page-reactive-data-root.d.ts +2 -2
- package/dist/sdk/data/page-reactive-data-root.js.map +1 -1
- package/dist/sdk/data/reactive-data-provider.d.ts +1 -1
- package/dist/sdk/data/reactive-data-provider.js +1 -1
- package/dist/sdk/data/reactive-data-provider.js.map +1 -1
- package/dist/sdk/data/reactive-list-item-data-provider.d.ts +1 -1
- package/dist/sdk/data/reactive-list-item-data-provider.js +1 -1
- package/dist/sdk/data/reactive-list-item-data-provider.js.map +1 -1
- package/dist/sdk/data/use-reactive-entity.js +1 -1
- package/dist/sdk/data/use-reactive-entity.js.map +1 -1
- package/dist/sdk/generated/gql.js +2 -1
- package/dist/sdk/generated/gql.js.map +1 -1
- package/dist/sdk/generated/graphql.js +85 -1
- package/dist/sdk/generated/graphql.js.map +1 -1
- package/dist/sdk/identity/operations.js.map +1 -1
- package/dist/sdk/identity/use-identity-actions.d.ts +1 -1
- package/dist/sdk/identity/use-identity-actions.js.map +1 -1
- package/dist/sdk/session/auth-operations.d.ts +1 -1
- package/dist/sdk/session/auth-operations.js.map +1 -1
- package/dist/sdk/session/use-session-actions.d.ts +1 -1
- package/dist/sdk/session/use-session-actions.js.map +1 -1
- package/dist/sdk/session/use-session-revalidation.js +65 -0
- package/dist/sdk/session/use-session-revalidation.js.map +1 -0
- package/dist/sdk/shipping/option-select-operations.d.ts +1 -1
- package/dist/sdk/shipping/option-select-operations.js.map +1 -1
- package/dist/sdk/shipping/request-timeout.js +1 -1
- package/dist/sdk/shipping/request-timeout.js.map +1 -1
- package/dist/sdk/shipping/shipping-operations.d.ts +1 -1
- package/dist/sdk/shipping/shipping-operations.js.map +1 -1
- package/dist/sdk/shipping/use-shipping-actions.d.ts +2 -2
- package/dist/sdk/shipping/use-shipping-actions.js.map +1 -1
- package/dist/sdk/state/cart.d.ts +3 -4
- package/dist/sdk/state/cart.js +6 -7
- package/dist/sdk/state/cart.js.map +1 -1
- package/dist/utils/safe-href.js +3 -3
- package/dist/utils/safe-href.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { defineComponent } from "../../core/dist/define-component.js";
|
|
3
|
+
import { defineContentType } from "../../core/dist/define-content-type.js";
|
|
4
|
+
import "../../core/dist/index.js";
|
|
5
|
+
import { resolveSafeHref, resolveSiteRelativeHref } from "../../utils/safe-href.js";
|
|
6
|
+
import { NavigationLink } from "../../particles/navigation-link/navigation-link.js";
|
|
7
|
+
import { createContext, useContext, useEffect, useLayoutEffect, useMemo, useRef } from "react";
|
|
8
|
+
import { jsx } from "react/jsx-runtime";
|
|
9
|
+
import { usePathname } from "next/navigation";
|
|
10
|
+
//#region src/matter/account-menu/account-menu.tsx
|
|
11
|
+
const useIsomorphicLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
|
|
12
|
+
const AccountMenuContent = defineContentType({
|
|
13
|
+
id: "account-menu",
|
|
14
|
+
title: "Account Menu",
|
|
15
|
+
description: "The ordered list of the account's sections, editable without a deployment.",
|
|
16
|
+
schema: {
|
|
17
|
+
type: "object",
|
|
18
|
+
additionalProperties: false,
|
|
19
|
+
properties: { items: {
|
|
20
|
+
type: "array",
|
|
21
|
+
title: "Sections",
|
|
22
|
+
minItems: 1,
|
|
23
|
+
items: {
|
|
24
|
+
type: "object",
|
|
25
|
+
additionalProperties: false,
|
|
26
|
+
required: ["label", "href"],
|
|
27
|
+
properties: {
|
|
28
|
+
label: {
|
|
29
|
+
type: "string",
|
|
30
|
+
minLength: 1,
|
|
31
|
+
title: "Label"
|
|
32
|
+
},
|
|
33
|
+
href: {
|
|
34
|
+
type: "string",
|
|
35
|
+
minLength: 1,
|
|
36
|
+
title: "URL",
|
|
37
|
+
format: "uri-reference",
|
|
38
|
+
pattern: "^(?:https?://.*|mailto:.+|tel:.+|/(?:[^/].*)?)$"
|
|
39
|
+
},
|
|
40
|
+
external: {
|
|
41
|
+
type: "boolean",
|
|
42
|
+
title: "Leaves the account"
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
} }
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
const AccountMenuRootDefinition = defineComponent({
|
|
50
|
+
name: "AccountMenuRoot",
|
|
51
|
+
importPath: "@vtex/faststore/components",
|
|
52
|
+
component: AccountMenuRoot,
|
|
53
|
+
content: AccountMenuContent,
|
|
54
|
+
providesContext: "account-menu",
|
|
55
|
+
editor: {
|
|
56
|
+
label: "Account Menu Root",
|
|
57
|
+
category: "navigation"
|
|
58
|
+
},
|
|
59
|
+
schema: { items: {
|
|
60
|
+
type: "array",
|
|
61
|
+
label: "Sections"
|
|
62
|
+
} },
|
|
63
|
+
guideline: "The account menu's provider, and deliberately not its landmark: it resolves the section list, derives which section the shopper is reading, publishes both, and renders its children. Compose it in the my-account layout around both AccountMenuList and the Slot holding the section's own page — its context has to reach the consumers inside the slot, and a nav stretched that far would announce every section's content as navigation. The list comes from a published entry, else the authored items prop, else the library's five defaults; a channel supplying an empty list resolves to empty rather than falling through, because a merchant who cleared the list cleared it. Style it through data-fs-account-menu."
|
|
64
|
+
});
|
|
65
|
+
const AccountMenuListDefinition = defineComponent({
|
|
66
|
+
name: "AccountMenuList",
|
|
67
|
+
importPath: "@vtex/faststore/components",
|
|
68
|
+
component: AccountMenuList,
|
|
69
|
+
requiresContext: "account-menu",
|
|
70
|
+
editor: {
|
|
71
|
+
label: "Account Menu List",
|
|
72
|
+
category: "navigation"
|
|
73
|
+
},
|
|
74
|
+
schema: { label: {
|
|
75
|
+
type: "string",
|
|
76
|
+
default: "Account",
|
|
77
|
+
label: "Aria Label"
|
|
78
|
+
} },
|
|
79
|
+
guideline: "The account menu's navigation landmark and its items, and nothing else — it surrounds the items alone so the section's own content stays outside the navigation region. Author the label: a landmark with no accessible name is one a screen-reader user cannot tell from any other. The current section carries aria-current, page for the section itself and true for a page inside it, with data-fs-account-menu-item-current beside it as a style target and never as the only signal. When the list scrolls, the current item is brought into view on mount and on every route change. Must be a descendant of AccountMenuRoot."
|
|
80
|
+
});
|
|
81
|
+
/**
|
|
82
|
+
* The library's default set, the five sections the PRD names. Labels are English as an interim, until
|
|
83
|
+
* the Localization cluster's catalog resolves them per locale. Their destinations only resolve in a
|
|
84
|
+
* store that authored the account's route family.
|
|
85
|
+
*/
|
|
86
|
+
const DEFAULT_SECTIONS = [
|
|
87
|
+
{
|
|
88
|
+
label: "Profile",
|
|
89
|
+
href: "/account/profile"
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
label: "Orders",
|
|
93
|
+
href: "/account/orders"
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
label: "Address",
|
|
97
|
+
href: "/account/address"
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
label: "Payments",
|
|
101
|
+
href: "/account/payments"
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
label: "Security",
|
|
105
|
+
href: "/account/security"
|
|
106
|
+
}
|
|
107
|
+
];
|
|
108
|
+
function resolveSections(sections) {
|
|
109
|
+
const resolved = [];
|
|
110
|
+
sections.forEach((section, index) => {
|
|
111
|
+
if (section.label.trim().length === 0) return;
|
|
112
|
+
const destination = resolveSafeHref(section.href);
|
|
113
|
+
if (!destination.safe) console.warn("[AccountMenu] section destination rejected", {
|
|
114
|
+
index,
|
|
115
|
+
reason: destination.reason
|
|
116
|
+
});
|
|
117
|
+
resolved.push({
|
|
118
|
+
label: section.label,
|
|
119
|
+
href: destination.safe ? destination.href : null,
|
|
120
|
+
external: section.external ?? false
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
return resolved;
|
|
124
|
+
}
|
|
125
|
+
/** What a match runs against: no query, no hash, and no trailing slash except at the root. */
|
|
126
|
+
function normalizePath(value) {
|
|
127
|
+
const separator = value.search(/[?#]/);
|
|
128
|
+
const trimmed = (separator === -1 ? value : value.slice(0, separator)).replace(/\/+$/, "");
|
|
129
|
+
return trimmed.length === 0 ? "/" : trimmed;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* The current section, matched on segment boundaries. Legacy matched by prefix and marked every
|
|
133
|
+
* section whose path was a prefix of the current one; the longest match wins here, which keeps
|
|
134
|
+
* exactly one section current and makes an exact match beat every ancestor without a second rule.
|
|
135
|
+
*/
|
|
136
|
+
function findCurrent(sections, pathname) {
|
|
137
|
+
if (pathname === null) return null;
|
|
138
|
+
const path = normalizePath(pathname);
|
|
139
|
+
let current = null;
|
|
140
|
+
let matchedLength = -1;
|
|
141
|
+
for (const [index, section] of sections.entries()) {
|
|
142
|
+
if (section.external || section.href === null || !resolveSiteRelativeHref(section.href).safe) continue;
|
|
143
|
+
const href = normalizePath(section.href);
|
|
144
|
+
if (href.length <= matchedLength) continue;
|
|
145
|
+
const exact = path === href;
|
|
146
|
+
const ancestor = href !== "/" && path.startsWith(`${href}/`);
|
|
147
|
+
if (!exact && !ancestor) continue;
|
|
148
|
+
current = {
|
|
149
|
+
index,
|
|
150
|
+
match: exact ? "exact" : "ancestor"
|
|
151
|
+
};
|
|
152
|
+
matchedLength = href.length;
|
|
153
|
+
}
|
|
154
|
+
return current;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* `page` for the section itself, `true` for a page inside it. The distinction is kept rather than
|
|
158
|
+
* collapsed because the sibling breadcrumb names the deep page as the current one, and two surfaces
|
|
159
|
+
* announcing "current page" for two different things is worse than the menu claiming only its set.
|
|
160
|
+
*/
|
|
161
|
+
function ariaCurrentFor(match) {
|
|
162
|
+
return match === "exact" ? "page" : "true";
|
|
163
|
+
}
|
|
164
|
+
function overflows(element) {
|
|
165
|
+
if (!element) return false;
|
|
166
|
+
return element.scrollWidth > element.clientWidth || element.scrollHeight > element.clientHeight;
|
|
167
|
+
}
|
|
168
|
+
const AccountMenuContext = createContext(null);
|
|
169
|
+
/** The resolved section list and the current section. */
|
|
170
|
+
function useAccountMenu() {
|
|
171
|
+
const value = useContext(AccountMenuContext);
|
|
172
|
+
if (!value) throw new Error("AccountMenu parts must be used inside AccountMenuRoot.");
|
|
173
|
+
return value;
|
|
174
|
+
}
|
|
175
|
+
function AccountMenuRoot({ items, $content, className, children, ...props }) {
|
|
176
|
+
const pathname = usePathname();
|
|
177
|
+
const sections = $content?.items ?? items ?? DEFAULT_SECTIONS;
|
|
178
|
+
const value = useMemo(() => {
|
|
179
|
+
const resolved = resolveSections(sections);
|
|
180
|
+
return {
|
|
181
|
+
items: resolved,
|
|
182
|
+
current: findCurrent(resolved, pathname)
|
|
183
|
+
};
|
|
184
|
+
}, [sections, pathname]);
|
|
185
|
+
return /* @__PURE__ */ jsx(AccountMenuContext, {
|
|
186
|
+
value,
|
|
187
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
188
|
+
...props,
|
|
189
|
+
className,
|
|
190
|
+
"data-fs-account-menu": "",
|
|
191
|
+
children
|
|
192
|
+
})
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
function AccountMenuList({ label = "Account", className, ...props }) {
|
|
196
|
+
const { items, current } = useAccountMenu();
|
|
197
|
+
const currentRef = useRef(null);
|
|
198
|
+
const currentIndex = current?.index ?? null;
|
|
199
|
+
useIsomorphicLayoutEffect(() => {
|
|
200
|
+
if (currentIndex === null) return;
|
|
201
|
+
const item = currentRef.current;
|
|
202
|
+
if (!item || typeof item.scrollIntoView !== "function") return;
|
|
203
|
+
const list = item.parentElement;
|
|
204
|
+
if (!overflows(list) && !overflows(list?.parentElement)) return;
|
|
205
|
+
item.scrollIntoView({
|
|
206
|
+
block: "nearest",
|
|
207
|
+
inline: "nearest"
|
|
208
|
+
});
|
|
209
|
+
}, [currentIndex]);
|
|
210
|
+
if (items.length === 0) return null;
|
|
211
|
+
return /* @__PURE__ */ jsx("nav", {
|
|
212
|
+
...props,
|
|
213
|
+
"aria-label": label,
|
|
214
|
+
className,
|
|
215
|
+
"data-fs-account-menu-list": "",
|
|
216
|
+
children: /* @__PURE__ */ jsx("ul", {
|
|
217
|
+
"data-fs-account-menu-items": "",
|
|
218
|
+
children: items.map((item, index) => {
|
|
219
|
+
const isCurrent = index === currentIndex;
|
|
220
|
+
return /* @__PURE__ */ jsx("li", {
|
|
221
|
+
ref: isCurrent ? currentRef : null,
|
|
222
|
+
"data-fs-account-menu-item": "",
|
|
223
|
+
"data-fs-account-menu-item-current": isCurrent ? "" : void 0,
|
|
224
|
+
children: item.href === null ? /* @__PURE__ */ jsx("span", {
|
|
225
|
+
"data-fs-text": "",
|
|
226
|
+
"data-fs-account-menu-text": "",
|
|
227
|
+
children: item.label
|
|
228
|
+
}) : /* @__PURE__ */ jsx(NavigationLink, {
|
|
229
|
+
href: item.href,
|
|
230
|
+
"aria-current": current && isCurrent ? ariaCurrentFor(current.match) : void 0,
|
|
231
|
+
"data-fs-account-menu-link": "",
|
|
232
|
+
children: item.label
|
|
233
|
+
})
|
|
234
|
+
}, index);
|
|
235
|
+
})
|
|
236
|
+
})
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
const AccountMenu = {
|
|
240
|
+
Root: AccountMenuRoot,
|
|
241
|
+
List: AccountMenuList
|
|
242
|
+
};
|
|
243
|
+
//#endregion
|
|
244
|
+
export { AccountMenu, AccountMenuContent, AccountMenuList, AccountMenuListDefinition, AccountMenuRoot, AccountMenuRootDefinition, useAccountMenu };
|
|
245
|
+
|
|
246
|
+
//# sourceMappingURL=account-menu.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"account-menu.js","names":[],"sources":["../../../src/matter/account-menu/account-menu.tsx"],"sourcesContent":["'use client';\n\nimport { type ContentValue, defineComponent, defineContentType } from '@vtex/fs-core';\nimport { usePathname } from 'next/navigation';\nimport {\n type ComponentPropsWithRef,\n createContext,\n useContext,\n useEffect,\n useLayoutEffect,\n useMemo,\n useRef,\n} from 'react';\nimport { NavigationLink } from '../../particles/navigation-link/navigation-link.js';\nimport { resolveSafeHref, resolveSiteRelativeHref } from '../../utils/safe-href.js';\n\n// The scroll has to land before paint, or the list shows its top for a frame and then jumps.\n// `useEffect` on the server silences React 19's \"useLayoutEffect does nothing on the server\", as\n// `matter/variant-selector` does for its own pre-paint measurement.\nconst useIsomorphicLayoutEffect = typeof window === 'undefined' ? useEffect : useLayoutEffect;\n\n// ---------------------------------------------------------------------------\n// Content model\n// ---------------------------------------------------------------------------\n\nexport const AccountMenuContent = defineContentType({\n id: 'account-menu',\n title: 'Account Menu',\n description: \"The ordered list of the account's sections, editable without a deployment.\",\n schema: {\n type: 'object',\n additionalProperties: false,\n // `items` is not required, for the reason NavigationLinkList records: a missing element key\n // already omits the component, so requiring it would only cost the prop fallback its shape\n // validity.\n properties: {\n items: {\n type: 'array',\n title: 'Sections',\n minItems: 1,\n items: {\n type: 'object',\n additionalProperties: false,\n required: ['label', 'href'],\n properties: {\n label: { type: 'string', minLength: 1, title: 'Label' },\n href: {\n type: 'string',\n minLength: 1,\n title: 'URL',\n format: 'uri-reference',\n // Author-time guidance only, and lookahead-free so the Admin's RE2-family engine takes it.\n pattern: '^(?:https?://.*|mailto:.+|tel:.+|/(?:[^/].*)?)$',\n },\n external: { type: 'boolean', title: 'Leaves the account' },\n },\n },\n },\n },\n },\n});\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\ntype AccountMenuContentValue = ContentValue<typeof AccountMenuContent>;\n\n/** One authored section. Derived from the model so the prop and content channels cannot drift. */\nexport type AccountMenuSection = NonNullable<AccountMenuContentValue['items']>[number];\n\n/** A section resolved for render. A null `href` is a destination the allowlist refused. */\nexport type AccountMenuResolvedSection = {\n label: string;\n href: string | null;\n external: boolean;\n};\n\nexport type AccountMenuCurrent = {\n /** Index into the resolved list. */\n index: number;\n /** `exact` when the path is the section; `ancestor` when the shopper is inside it. */\n match: 'exact' | 'ancestor';\n};\n\nexport type AccountMenuContextValue = {\n /** Resolved, in the merchant's order, with blank-label sections dropped. */\n items: readonly AccountMenuResolvedSection[];\n /** The current section, or null when the path matches none. */\n current: AccountMenuCurrent | null;\n};\n\nexport type AccountMenuRootProps = ComponentPropsWithRef<'div'> & {\n /** Authored fallback for the section list, used when no CMS entry supplies one. */\n items?: readonly AccountMenuSection[];\n /** Injected by the compiler, sliced from the page's or layout's composite content entry. */\n $content?: AccountMenuContentValue;\n};\n\n/** The landmark renders one item per section, so it takes no children. */\nexport type AccountMenuListProps = Omit<ComponentPropsWithRef<'nav'>, 'children'> & {\n /** Accessible name of the navigation landmark. */\n label?: string;\n};\n\n// ---------------------------------------------------------------------------\n// Definitions\n// ---------------------------------------------------------------------------\n\nexport const AccountMenuRootDefinition = defineComponent<AccountMenuRootProps>({\n name: 'AccountMenuRoot',\n importPath: '@vtex/faststore/components',\n component: AccountMenuRoot,\n content: AccountMenuContent,\n providesContext: 'account-menu',\n editor: { label: 'Account Menu Root', category: 'navigation' },\n schema: {\n items: { type: 'array', label: 'Sections' },\n },\n guideline:\n \"The account menu's provider, and deliberately not its landmark: it resolves the section list, derives which section the shopper is reading, publishes both, and renders its children. Compose it in the my-account layout around both AccountMenuList and the Slot holding the section's own page — its context has to reach the consumers inside the slot, and a nav stretched that far would announce every section's content as navigation. The list comes from a published entry, else the authored items prop, else the library's five defaults; a channel supplying an empty list resolves to empty rather than falling through, because a merchant who cleared the list cleared it. Style it through data-fs-account-menu.\",\n});\n\nexport const AccountMenuListDefinition = defineComponent<AccountMenuListProps>({\n name: 'AccountMenuList',\n importPath: '@vtex/faststore/components',\n component: AccountMenuList,\n requiresContext: 'account-menu',\n editor: { label: 'Account Menu List', category: 'navigation' },\n schema: {\n label: { type: 'string', default: 'Account', label: 'Aria Label' },\n },\n guideline:\n \"The account menu's navigation landmark and its items, and nothing else — it surrounds the items alone so the section's own content stays outside the navigation region. Author the label: a landmark with no accessible name is one a screen-reader user cannot tell from any other. The current section carries aria-current, page for the section itself and true for a page inside it, with data-fs-account-menu-item-current beside it as a style target and never as the only signal. When the list scrolls, the current item is brought into view on mount and on every route change. Must be a descendant of AccountMenuRoot.\",\n});\n\n// ---------------------------------------------------------------------------\n// Resolution\n// ---------------------------------------------------------------------------\n\n/**\n * The library's default set, the five sections the PRD names. Labels are English as an interim, until\n * the Localization cluster's catalog resolves them per locale. Their destinations only resolve in a\n * store that authored the account's route family.\n */\nconst DEFAULT_SECTIONS: readonly AccountMenuSection[] = [\n { label: 'Profile', href: '/account/profile' },\n { label: 'Orders', href: '/account/orders' },\n { label: 'Address', href: '/account/address' },\n { label: 'Payments', href: '/account/payments' },\n { label: 'Security', href: '/account/security' },\n];\n\nfunction resolveSections(sections: readonly AccountMenuSection[]): AccountMenuResolvedSection[] {\n const resolved: AccountMenuResolvedSection[] = [];\n\n sections.forEach((section, index) => {\n // `minLength` is enforced by neither the runtime slice nor the preflight, so a blank label\n // reaches the component. Drop the section: an anchor with no accessible name is a WCAG failure.\n if (section.label.trim().length === 0) {\n return;\n }\n const destination = resolveSafeHref(section.href);\n if (!destination.safe) {\n console.warn('[AccountMenu] section destination rejected', { index, reason: destination.reason });\n }\n resolved.push({\n label: section.label,\n href: destination.safe ? destination.href : null,\n external: section.external ?? false,\n });\n });\n\n return resolved;\n}\n\n/** What a match runs against: no query, no hash, and no trailing slash except at the root. */\nfunction normalizePath(value: string): string {\n const separator = value.search(/[?#]/);\n const path = separator === -1 ? value : value.slice(0, separator);\n const trimmed = path.replace(/\\/+$/, '');\n return trimmed.length === 0 ? '/' : trimmed;\n}\n\n/**\n * The current section, matched on segment boundaries. Legacy matched by prefix and marked every\n * section whose path was a prefix of the current one; the longest match wins here, which keeps\n * exactly one section current and makes an exact match beat every ancestor without a second rule.\n */\nfunction findCurrent(\n sections: readonly AccountMenuResolvedSection[],\n pathname: string | null,\n): AccountMenuCurrent | null {\n if (pathname === null) {\n return null;\n }\n\n const path = normalizePath(pathname);\n let current: AccountMenuCurrent | null = null;\n let matchedLength = -1;\n\n for (const [index, section] of sections.entries()) {\n // An external section leaves the account and is outside the current-section contract, and an\n // absolute destination could never be a path in the first place.\n if (section.external || section.href === null || !resolveSiteRelativeHref(section.href).safe) {\n continue;\n }\n const href = normalizePath(section.href);\n if (href.length <= matchedLength) {\n continue;\n }\n // A root href is an ancestor of every account path, so a merchant listing the storefront's home\n // beside the sections would otherwise see it current on every page. It matches exactly or not at all.\n const exact = path === href;\n const ancestor = href !== '/' && path.startsWith(`${href}/`);\n if (!exact && !ancestor) {\n continue;\n }\n current = { index, match: exact ? 'exact' : 'ancestor' };\n matchedLength = href.length;\n }\n\n return current;\n}\n\n/**\n * `page` for the section itself, `true` for a page inside it. The distinction is kept rather than\n * collapsed because the sibling breadcrumb names the deep page as the current one, and two surfaces\n * announcing \"current page\" for two different things is worse than the menu claiming only its set.\n */\nfunction ariaCurrentFor(match: AccountMenuCurrent['match']): 'page' | 'true' {\n return match === 'exact' ? 'page' : 'true';\n}\n\nfunction overflows(element: Element | null | undefined): boolean {\n if (!element) {\n return false;\n }\n return element.scrollWidth > element.clientWidth || element.scrollHeight > element.clientHeight;\n}\n\n// ---------------------------------------------------------------------------\n// Context\n// ---------------------------------------------------------------------------\n\nconst AccountMenuContext = createContext<AccountMenuContextValue | null>(null);\n\n/** The resolved section list and the current section. */\nexport function useAccountMenu(): AccountMenuContextValue {\n const value = useContext(AccountMenuContext);\n if (!value) {\n throw new Error('AccountMenu parts must be used inside AccountMenuRoot.');\n }\n return value;\n}\n\n// ---------------------------------------------------------------------------\n// Implementations\n// ---------------------------------------------------------------------------\n\nexport function AccountMenuRoot({ items, $content, className, children, ...props }: AccountMenuRootProps) {\n const pathname = usePathname();\n // Presence decides, not truthiness: a channel supplying an empty list resolves to empty rather than\n // falling through, so the defaults never override a list the merchant published deliberately.\n // `minItems` guides the Admin form only, so that empty list still reaches this component.\n const sections = $content?.items ?? items ?? DEFAULT_SECTIONS;\n\n const value = useMemo<AccountMenuContextValue>(() => {\n const resolved = resolveSections(sections);\n return { items: resolved, current: findCurrent(resolved, pathname) };\n }, [sections, pathname]);\n\n return (\n <AccountMenuContext value={value}>\n <div {...props} className={className} data-fs-account-menu=\"\">\n {children}\n </div>\n </AccountMenuContext>\n );\n}\n\nexport function AccountMenuList({ label = 'Account', className, ...props }: AccountMenuListProps) {\n const { items, current } = useAccountMenu();\n const currentRef = useRef<HTMLLIElement>(null);\n const currentIndex = current?.index ?? null;\n\n useIsomorphicLayoutEffect(() => {\n if (currentIndex === null) {\n return;\n }\n const item = currentRef.current;\n if (!item || typeof item.scrollIntoView !== 'function') {\n return;\n }\n // Only when the row actually scrolls. An unconditional call scrolls whatever ancestor can move,\n // which on a menu that fits is the page. The containers are the item's `ul` and its `nav`.\n const list = item.parentElement;\n if (!overflows(list) && !overflows(list?.parentElement)) {\n return;\n }\n // `nearest` on both axes is a no-op once the item is visible, so a route change inside the\n // current section does not jolt the row.\n item.scrollIntoView({ block: 'nearest', inline: 'nearest' });\n }, [currentIndex]);\n\n if (items.length === 0) {\n return null;\n }\n\n return (\n <nav {...props} aria-label={label} className={className} data-fs-account-menu-list=\"\">\n <ul data-fs-account-menu-items=\"\">\n {items.map((item, index) => {\n const isCurrent = index === currentIndex;\n return (\n <li\n key={index}\n ref={isCurrent ? currentRef : null}\n data-fs-account-menu-item=\"\"\n data-fs-account-menu-item-current={isCurrent ? '' : undefined}\n >\n {item.href === null ? (\n <span data-fs-text=\"\" data-fs-account-menu-text=\"\">\n {item.label}\n </span>\n ) : (\n <NavigationLink\n href={item.href}\n aria-current={current && isCurrent ? ariaCurrentFor(current.match) : undefined}\n data-fs-account-menu-link=\"\"\n >\n {item.label}\n </NavigationLink>\n )}\n </li>\n );\n })}\n </ul>\n </nav>\n );\n}\n\n// ---------------------------------------------------------------------------\n// Namespace export\n// ---------------------------------------------------------------------------\n\nexport const AccountMenu = {\n Root: AccountMenuRoot,\n List: AccountMenuList,\n};\n"],"mappings":"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"}
|
|
@@ -149,7 +149,7 @@ export declare const CartDataContext: React.Context<CartDataContextValue>;
|
|
|
149
149
|
*
|
|
150
150
|
* Cart *writes* no longer flow through this context — they are dispatched via the compiler-injected
|
|
151
151
|
* `$actions.cart.*` prop (the `cart` action namespace), which reads/writes the cache directly and
|
|
152
|
-
* needs no `CartRoot` ancestor (
|
|
152
|
+
* needs no `CartRoot` ancestor (docs/storefront-core/reference-free-particle-model.md § Write side — actions, ADR-0010). This hook is reads only.
|
|
153
153
|
*/
|
|
154
154
|
export declare function useCart(): CartDataContextValue;
|
|
155
155
|
export declare const CartRootDefinition: ComponentDefinition<CartRootProps>;
|
package/dist/matter/cart/cart.js
CHANGED
|
@@ -6,8 +6,8 @@ import { useStorefrontContext } from "../../sdk/context/storefront-context.js";
|
|
|
6
6
|
import { graphql } from "../../sdk/generated/gql.js";
|
|
7
7
|
import { useActionsOverride } from "../../sdk/actions/actions-override-context.js";
|
|
8
8
|
import { useDataOverride } from "../../sdk/data/data-override-context.js";
|
|
9
|
-
import { Icon } from "../../particles/icon/icon.js";
|
|
10
9
|
import { NavigationLink } from "../../particles/navigation-link/navigation-link.js";
|
|
10
|
+
import { Icon } from "../../particles/icon/icon.js";
|
|
11
11
|
import { DrawerClose } from "../../particles/drawer/drawer.js";
|
|
12
12
|
import { Image } from "../../particles/image/image.js";
|
|
13
13
|
import { cartCouponErrorsVar, cartLastAddVar, cartUserErrorsVar, miniCartOpenVar, orderFormIdVar } from "../../sdk/state/cart.js";
|
|
@@ -107,7 +107,7 @@ const CartDataContext = CartDataContextInternal;
|
|
|
107
107
|
*
|
|
108
108
|
* Cart *writes* no longer flow through this context — they are dispatched via the compiler-injected
|
|
109
109
|
* `$actions.cart.*` prop (the `cart` action namespace), which reads/writes the cache directly and
|
|
110
|
-
* needs no `CartRoot` ancestor (
|
|
110
|
+
* needs no `CartRoot` ancestor (docs/storefront-core/reference-free-particle-model.md § Write side — actions, ADR-0010). This hook is reads only.
|
|
111
111
|
*/
|
|
112
112
|
function useCart() {
|
|
113
113
|
const ctx = useContext(CartDataContextInternal);
|