@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.
- package/CHANGELOG.md +56 -0
- package/dist/catalog.json +161 -20
- package/dist/cli/UnreachableCaseError-Crd168Vu.js +10 -0
- package/dist/cli/UnreachableCaseError-Crd168Vu.js.map +1 -0
- package/dist/cli/cacheControl-CEojH_TM.js +158 -0
- package/dist/cli/cacheControl-CEojH_TM.js.map +1 -0
- package/dist/cli/cachePolicy-m2vASMRW.js +26 -0
- package/dist/cli/cachePolicy-m2vASMRW.js.map +1 -0
- package/dist/cli/computeCoreSchemaHash-DPJZj-qJ.js +10 -0
- package/dist/cli/computeCoreSchemaHash-DPJZj-qJ.js.map +1 -0
- package/dist/cli/default-BCQHcwbL.js +253 -0
- package/dist/cli/default-BCQHcwbL.js.map +1 -0
- package/dist/cli/disableSuggestions-yO9UqiWn.js +21 -0
- package/dist/cli/disableSuggestions-yO9UqiWn.js.map +1 -0
- package/dist/cli/dist-C1JhdHfo.js +2001 -0
- package/dist/cli/dist-C1JhdHfo.js.map +1 -0
- package/dist/cli/graphql-DG9thCDG.js +14589 -0
- package/dist/cli/graphql-DG9thCDG.js.map +1 -0
- package/dist/cli/index.d.ts +350 -0
- package/dist/cli/index.js +35047 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/index.min-BsIHDfFK.js +613 -0
- package/dist/cli/index.min-BsIHDfFK.js.map +1 -0
- package/dist/cli/inlineTrace-uLgOJ3TH.js +52 -0
- package/dist/cli/inlineTrace-uLgOJ3TH.js.map +1 -0
- package/dist/cli/internalPlugin-DnuwN4_4.js +11 -0
- package/dist/cli/internalPlugin-DnuwN4_4.js.map +1 -0
- package/dist/cli/packageVersion-DQAnIEil.js +6 -0
- package/dist/cli/packageVersion-DQAnIEil.js.map +1 -0
- package/dist/cli/rolldown-runtime-BMI-E3GI.js +44 -0
- package/dist/cli/schemaIsSubgraph-DoGh3OYj.js +17 -0
- package/dist/cli/schemaIsSubgraph-DoGh3OYj.js.map +1 -0
- package/dist/cli/schemaReporting-K-me-3-v.js +199 -0
- package/dist/cli/schemaReporting-K-me-3-v.js.map +1 -0
- package/dist/cli/server-runtime.js +49859 -0
- package/dist/cli/server-runtime.js.map +1 -0
- package/dist/cli/traceTreeBuilder-BIcV2U7b.js +9118 -0
- package/dist/cli/traceTreeBuilder-BIcV2U7b.js.map +1 -0
- package/dist/cli/usageReporting-Bpl8vSZX.js +10201 -0
- package/dist/cli/usageReporting-Bpl8vSZX.js.map +1 -0
- package/dist/cli/v4-CBhFvC49.js +42 -0
- package/dist/cli/v4-CBhFvC49.js.map +1 -0
- package/dist/components.d.ts +5 -3
- package/dist/components.js +5 -3
- package/dist/content-types.json +93 -0
- package/dist/core/dist/define-component.d.ts +37 -0
- package/dist/core/dist/index.d.ts +2 -2
- package/dist/docs/account-entry-point.md +1 -1
- package/dist/docs/account-menu-identity.md +160 -0
- package/dist/docs/account-menu.md +106 -23
- package/dist/docs/avatar.md +103 -0
- package/dist/docs/my-account-profile.md +1 -1
- package/dist/docs/product-shelf.md +20 -13
- package/dist/index.d.ts +2 -2
- package/dist/internal/client.js +1 -1
- package/dist/internal/content.d.ts +2 -2
- package/dist/internal/server.d.ts +4 -1
- package/dist/internal/server.js +4 -1
- package/dist/matter/account-access-gate/account-access-gate.js.map +1 -1
- package/dist/matter/account-entry-point/account-entry-point.js +1 -1
- package/dist/matter/account-entry-point/account-entry-point.js.map +1 -1
- package/dist/matter/account-menu/account-menu.d.ts +70 -0
- package/dist/matter/account-menu/account-menu.js +201 -9
- package/dist/matter/account-menu/account-menu.js.map +1 -1
- package/dist/matter/account-menu/sign-out-failure.js +50 -0
- package/dist/matter/account-menu/sign-out-failure.js.map +1 -0
- package/dist/matter/account-menu-identity/account-menu-identity.d.ts +21 -0
- package/dist/matter/account-menu-identity/account-menu-identity.js +88 -0
- package/dist/matter/account-menu-identity/account-menu-identity.js.map +1 -0
- package/dist/matter/card-entry/card-entry.js +1 -0
- package/dist/matter/card-entry/card-entry.js.map +1 -1
- package/dist/matter/cart/cart.js +1 -1
- package/dist/matter/client-preferences/client-preferences-fieldset.js +2 -2
- package/dist/matter/client-profile-form/client-profile-fieldset.js +2 -2
- package/dist/matter/identity-summary/identity-summary.js +2 -1
- package/dist/matter/identity-summary/identity-summary.js.map +1 -1
- package/dist/matter/my-account-profile/my-account-profile.d.ts +3 -0
- package/dist/matter/my-account-profile/my-account-profile.js +2 -1
- package/dist/matter/my-account-profile/my-account-profile.js.map +1 -1
- package/dist/matter/payment/payment-controller.js +1 -0
- package/dist/matter/payment/payment-controller.js.map +1 -1
- package/dist/matter/payment/payment.js +1 -0
- package/dist/matter/payment/payment.js.map +1 -1
- package/dist/matter/product-shelf/product-shelf.d.ts +61 -31
- package/dist/matter/product-shelf/product-shelf.js +157 -57
- package/dist/matter/product-shelf/product-shelf.js.map +1 -1
- package/dist/matter/saved-cards/saved-cards.js +1 -0
- package/dist/matter/saved-cards/saved-cards.js.map +1 -1
- package/dist/matter/shipping-address/shipping-address.js +2 -2
- package/dist/particles/avatar/avatar.d.ts +17 -0
- package/dist/particles/avatar/avatar.js +65 -0
- package/dist/particles/avatar/avatar.js.map +1 -0
- package/dist/schema.graphql +65 -26
- package/dist/sdk/analytics/use-analytics-actions.js +2 -1
- package/dist/sdk/analytics/use-analytics-actions.js.map +1 -1
- package/dist/sdk/cart/use-cart-actions.js +9 -8
- package/dist/sdk/cart/use-cart-actions.js.map +1 -1
- package/dist/sdk/cart/use-cart-validate-on-open.js +2 -1
- package/dist/sdk/cart/use-cart-validate-on-open.js.map +1 -1
- package/dist/sdk/content/create-content-runtime.d.ts +22 -1
- package/dist/sdk/content/create-content-runtime.js +89 -1
- package/dist/sdk/content/create-content-runtime.js.map +1 -1
- package/dist/sdk/data/empty-content-value.js +16 -0
- package/dist/sdk/data/empty-content-value.js.map +1 -0
- package/dist/sdk/data/hot-data-provider.js +2 -1
- package/dist/sdk/data/hot-data-provider.js.map +1 -1
- package/dist/sdk/data/index.js +2 -2
- package/dist/sdk/data/reactive-list-item-data-provider.js +2 -2
- package/dist/sdk/data/variables-from-content.d.ts +43 -0
- package/dist/sdk/data/variables-from-content.js +57 -0
- package/dist/sdk/data/variables-from-content.js.map +1 -0
- 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/use-identity-actions.js +5 -4
- package/dist/sdk/identity/use-identity-actions.js.map +1 -1
- package/dist/sdk/payment/use-payment-actions.js +1 -1
- package/dist/sdk/payment/use-place-order.js +1 -1
- package/dist/sdk/region/use-region-actions.js +2 -1
- package/dist/sdk/region/use-region-actions.js.map +1 -1
- package/dist/sdk/session/read-server-session.d.ts +21 -0
- package/dist/sdk/session/read-server-session.js +61 -0
- package/dist/sdk/session/read-server-session.js.map +1 -0
- package/dist/sdk/session/use-session-actions.js +5 -4
- package/dist/sdk/session/use-session-actions.js.map +1 -1
- package/dist/sdk/session/use-session-revalidation.js +2 -1
- package/dist/sdk/session/use-session-revalidation.js.map +1 -1
- package/dist/sdk/shipping/use-shipping-actions.js +3 -2
- package/dist/sdk/shipping/use-shipping-actions.js.map +1 -1
- package/dist/utils/safe-href.d.ts +11 -0
- package/package.json +13 -2
|
@@ -1 +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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"}
|
|
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, useRouter } 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 { useActionsOverride } from '../../sdk/actions/actions-override-context.js';\nimport type { ComponentActions } from '../../sdk/actions/types.js';\nimport type { SessionActions } from '../../sdk/session/use-session-actions.js';\nimport { resolveSafeHref, resolveSiteRelativeHref } from '../../utils/safe-href.js';\nimport { AccountAccessGateLoading } from '../account-access-gate/account-access-gate.js';\nimport {\n declareSignOutIntent,\n useSignOutIntent,\n withdrawSignOutIntent,\n} from '../account-access-gate/sign-out-intent.js';\nimport { clearSignOutFailure, reportSignOutFailure, useSignOutFailure } from './sign-out-failure.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// Nothing is required, following the gate's model: a slice missing a required property is shape-invalid,\n// which drops the whole element rather than the one field.\nexport const AccountMenuIndexRedirectContent = defineContentType({\n id: 'account-menu-index-redirect',\n title: 'Account Menu Index Redirect',\n description: \"What the account's root shows while it opens the first section, and when there is none to open.\",\n schema: {\n type: 'object',\n additionalProperties: false,\n properties: {\n loadingLabel: { type: 'string', minLength: 1, title: 'Loading announcement' },\n emptyMessage: { type: 'string', minLength: 1, title: 'No section message' },\n },\n },\n});\n\nexport const AccountMenuSignOutContent = defineContentType({\n id: 'account-menu-sign-out',\n title: 'Account Menu Sign Out',\n description: 'The sign-out control in the account sidebar, and what it says when signing out fails.',\n schema: {\n type: 'object',\n additionalProperties: false,\n properties: {\n label: { type: 'string', minLength: 1, title: 'Sign-out label' },\n errorMessage: { type: 'string', minLength: 1, title: 'Sign-out failed message' },\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/** It renders the gate's loading look or a message, so it forwards no element props beyond a class. */\nexport type AccountMenuIndexRedirectProps = {\n /** Text announced while the first section opens. */\n loadingLabel?: string;\n /** What the root says when the list holds no section to open. */\n emptyMessage?: string;\n /** Merchant-authored copy, sliced from the page content entry; wins over the props field by field. */\n $content?: ContentValue<typeof AccountMenuIndexRedirectContent>;\n className?: string;\n};\n\nexport type AccountMenuSignOutProps = {\n /** The control's text. */\n label?: string;\n /** What the shopper is told when the sign-out did not happen. */\n errorMessage?: string;\n /** Merchant-authored copy, sliced from the layout content entry; wins over the props field by field. */\n $content?: ContentValue<typeof AccountMenuSignOutContent>;\n /** Injected by the compiler from the definition's `actions`. */\n $actions?: ComponentActions;\n className?: 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\nexport const AccountMenuIndexRedirectDefinition = defineComponent<AccountMenuIndexRedirectProps>({\n name: 'AccountMenuIndexRedirect',\n importPath: '@vtex/faststore/components',\n component: AccountMenuIndexRedirect,\n content: AccountMenuIndexRedirectContent,\n requiresContext: 'account-menu',\n editor: { label: 'Account Menu Index Redirect', category: 'navigation' },\n schema: {\n // Mirrored from the content model on purpose, as every content-bearing component in the library\n // does: the entry wins when published, and this is what a store without a CMS authors.\n loadingLabel: { type: 'string', label: 'Loading Announcement' },\n emptyMessage: { type: 'string', default: 'No account section is configured.', label: 'No Section Message' },\n },\n guideline:\n \"The whole of the account's root page: it replaces the current history entry with the first section of the merchant's list that stays in the account, skipping one marked external, one whose destination was refused or is not site-relative, and the root itself. It shows the access gate's loading look until the section opens. A list with no such section leaves the root terminal — no navigation, and the no-section message in its place — rather than falling back to the library's defaults. Place it alone on the /pvt/account page, inside AccountMenuRoot and under AccountAccessGate, which the my-account layout provides; never on a section's own page, which it would navigate away from.\",\n});\n\nexport const AccountMenuSignOutDefinition = defineComponent<AccountMenuSignOutProps>({\n name: 'AccountMenuSignOut',\n importPath: '@vtex/faststore/components',\n component: AccountMenuSignOut,\n content: AccountMenuSignOutContent,\n actions: { session: ['logout'] },\n editor: { label: 'Account Menu Sign Out', category: 'navigation' },\n schema: {\n label: { type: 'string', default: 'Sign out', label: 'Label' },\n errorMessage: {\n type: 'string',\n default: \"We couldn't sign you out. Please try again.\",\n label: 'Sign-out Failed Message',\n },\n },\n guideline:\n \"The account sidebar's way out: it dispatches $actions.session.logout, tells AccountAccessGate the departure is deliberate so the gate stands down instead of sending the shopper to sign in, and then loads the storefront home as a new document, which is what keeps the account out of the client router's cache. A logout that reports an error keeps the shopper on the account page with the failure message rather than landing a still-signed-in shopper on the home page. Compose it inside AccountMenuRoot beside AccountMenuList, under the gate the my-account layout provides — the back-forward cache is the gate's to revalidate, not this control's.\",\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: '/pvt/profile' },\n { label: 'Orders', href: '/pvt/orders' },\n { label: 'Address', href: '/pvt/address' },\n { label: 'Payments', href: '/pvt/payments' },\n { label: 'Security', href: '/pvt/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 * Where the account's root opens: the first section that stays in the account. An external entry would\n * eject the shopper from the account they just opened, and so would an absolute one the merchant did not\n * flag — the same site-relative reading `findCurrent` applies. The page itself is skipped too, in every\n * spelling that matches it, since replacing it with itself would leave the loading look up for good.\n */\nfunction findIndexDestination(\n sections: readonly AccountMenuResolvedSection[],\n current: AccountMenuCurrent | null,\n): string | null {\n const here = current?.match === 'exact' ? sections[current.index]?.href : null;\n const herePath = here ? normalizePath(here) : null;\n\n for (const section of sections) {\n if (section.external || section.href === null || !resolveSiteRelativeHref(section.href).safe) {\n continue;\n }\n if (normalizePath(section.href) === herePath) {\n continue;\n }\n return section.href;\n }\n\n return null;\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\nexport function AccountMenuIndexRedirect({\n loadingLabel,\n emptyMessage = 'No account section is configured.',\n $content,\n className,\n}: Readonly<AccountMenuIndexRedirectProps>) {\n const { items, current } = useAccountMenu();\n const router = useRouter();\n // StrictMode runs an effect twice, and a router whose identity changes per render re-runs it on every\n // pass; one replace is the whole of this part's job.\n const replacedRef = useRef(false);\n\n const destination = findIndexDestination(items, current);\n\n useEffect(() => {\n if (destination === null || replacedRef.current) {\n return;\n }\n replacedRef.current = true;\n // Replace rather than push, so back from the section leaves the account instead of bouncing through\n // its root.\n // react-doctor-disable-next-line nextjs-no-client-side-redirect -- the order is the merchant's, published at runtime, so no static redirect can express it\n router.replace(destination);\n }, [destination, router]);\n\n // Blank entry fields fall through, so clearing one in the Admin reads as unauthored.\n const entryLoadingLabel = $content?.loadingLabel?.trim() ? $content.loadingLabel : loadingLabel;\n const entryEmptyMessage = $content?.emptyMessage?.trim() ? $content.emptyMessage : emptyMessage;\n\n if (destination === null) {\n return (\n <p className={className} data-fs-account-menu-index-redirect=\"\" data-fs-account-menu-index-redirect-empty=\"\">\n {entryEmptyMessage}\n </p>\n );\n }\n\n // The gate's own part rather than one of this part's: in the account layout the gate's wait for the\n // session comes first, and carrying the same look on until the section lands is what keeps a second\n // one from flashing between the two.\n return (\n <AccountAccessGateLoading className={className} data-fs-account-menu-index-redirect=\"\">\n {entryLoadingLabel}\n </AccountAccessGateLoading>\n );\n}\n\nconst DEFAULT_SIGN_OUT_LABEL = 'Sign out';\nconst DEFAULT_SIGN_OUT_ERROR = \"We couldn't sign you out. Please try again.\";\n\n/**\n * The first non-blank of the given values. The models' `minLength` is enforced by neither the runtime\n * slice nor the preflight, and a blank here is a nameless button or a silent failure.\n */\nfunction firstAuthored(...values: (string | undefined)[]): string | undefined {\n return values.find((value) => value?.trim());\n}\n\n/** An absent handler or a rejection is a sign-out that did not happen, never one that did. */\nasync function logOut(logout: SessionActions['logout'] | undefined): Promise<boolean> {\n if (!logout) {\n return false;\n }\n try {\n const errors = await logout();\n return errors.length === 0;\n } catch (error) {\n console.error('[AccountMenuSignOut] logout rejected', error);\n return false;\n }\n}\n\nexport function AccountMenuSignOut({\n label,\n errorMessage,\n $content,\n $actions: $actionsProp,\n className,\n}: Readonly<AccountMenuSignOutProps>) {\n const $actions = useActionsOverride($actionsProp);\n // Any standing intent, whichever control declared it, is a sign-out under way.\n const signingOut = useSignOutIntent();\n const failed = useSignOutFailure();\n // Two activations in the same task see the same render, so the disabled button alone cannot stop\n // the second one.\n const attemptingRef = useRef(false);\n\n async function handleClick() {\n if (attemptingRef.current) {\n return;\n }\n attemptingRef.current = true;\n clearSignOutFailure();\n // Before the await: the logout payload flips the session in place, and a gate that got its turn\n // between the response and the navigation would send the shopper to sign in for the account they\n // just left. From here on this control is unmounted under the gate; the handler still settles.\n declareSignOutIntent();\n\n if (await logOut($actions?.session?.logout)) {\n // A document load rather than the router, whose cache would keep the account behind the back\n // control. What the browser's back-forward cache restores is the gate's to revalidate.\n globalThis.location.assign('/');\n return;\n }\n\n attemptingRef.current = false;\n reportSignOutFailure();\n withdrawSignOutIntent();\n }\n\n const entryLabel = firstAuthored($content?.label, label) ?? DEFAULT_SIGN_OUT_LABEL;\n const entryErrorMessage = firstAuthored($content?.errorMessage, errorMessage) ?? DEFAULT_SIGN_OUT_ERROR;\n\n return (\n <div\n className={className}\n data-fs-account-menu-sign-out=\"\"\n data-fs-account-menu-sign-out-pending={signingOut ? '' : undefined}\n >\n <button\n type=\"button\"\n onClick={handleClick}\n disabled={signingOut}\n aria-busy={signingOut || undefined}\n data-fs-account-menu-sign-out-button=\"\"\n >\n {entryLabel}\n </button>\n {failed ? (\n <p role=\"alert\" data-fs-account-menu-sign-out-error=\"\">\n {entryErrorMessage}\n </p>\n ) : null}\n </div>\n );\n}\n\n// ---------------------------------------------------------------------------\n// Namespace export\n// ---------------------------------------------------------------------------\n\nexport const AccountMenu = {\n Root: AccountMenuRoot,\n List: AccountMenuList,\n IndexRedirect: AccountMenuIndexRedirect,\n SignOut: AccountMenuSignOut,\n};\n"],"mappings":"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"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useSyncExternalStore } from "react";
|
|
3
|
+
//#region src/matter/account-menu/sign-out-failure.ts
|
|
4
|
+
/**
|
|
5
|
+
* Whether the last sign-out from the account menu did not happen.
|
|
6
|
+
*
|
|
7
|
+
* Held outside the control that caught it because that control does not survive its own attempt:
|
|
8
|
+
* declaring the sign-out intent makes `AccountAccessGate` stand down and unmount the account, the menu
|
|
9
|
+
* and the control included, and withdrawing it on the failure mounts a fresh control. Local state would
|
|
10
|
+
* be gone in exactly the frame the failure has to be shown.
|
|
11
|
+
*
|
|
12
|
+
* Cleared by the next attempt, never on unmount: the gate unmounts the control mid-attempt, and
|
|
13
|
+
* StrictMode unmounts and remounts it in development. Like the intent, it dies with the document.
|
|
14
|
+
*/
|
|
15
|
+
let failed = false;
|
|
16
|
+
const subscribers = /* @__PURE__ */ new Set();
|
|
17
|
+
function notify() {
|
|
18
|
+
for (const subscriber of subscribers) subscriber();
|
|
19
|
+
}
|
|
20
|
+
/** Recorded before the intent is withdrawn, so the control the gate mounts on the withdraw reads it. */
|
|
21
|
+
function reportSignOutFailure() {
|
|
22
|
+
if (failed) return;
|
|
23
|
+
failed = true;
|
|
24
|
+
notify();
|
|
25
|
+
}
|
|
26
|
+
function clearSignOutFailure() {
|
|
27
|
+
if (!failed) return;
|
|
28
|
+
failed = false;
|
|
29
|
+
notify();
|
|
30
|
+
}
|
|
31
|
+
function subscribe(onStoreChange) {
|
|
32
|
+
subscribers.add(onStoreChange);
|
|
33
|
+
return () => {
|
|
34
|
+
subscribers.delete(onStoreChange);
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
function getSnapshot() {
|
|
38
|
+
return failed;
|
|
39
|
+
}
|
|
40
|
+
/** Server renders never carry a failure: nothing has been able to click sign out yet. */
|
|
41
|
+
function getServerSnapshot() {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
function useSignOutFailure() {
|
|
45
|
+
return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
|
|
46
|
+
}
|
|
47
|
+
//#endregion
|
|
48
|
+
export { clearSignOutFailure, reportSignOutFailure, useSignOutFailure };
|
|
49
|
+
|
|
50
|
+
//# sourceMappingURL=sign-out-failure.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sign-out-failure.js","names":[],"sources":["../../../src/matter/account-menu/sign-out-failure.ts"],"sourcesContent":["'use client';\n\nimport { useSyncExternalStore } from 'react';\n\n/**\n * Whether the last sign-out from the account menu did not happen.\n *\n * Held outside the control that caught it because that control does not survive its own attempt:\n * declaring the sign-out intent makes `AccountAccessGate` stand down and unmount the account, the menu\n * and the control included, and withdrawing it on the failure mounts a fresh control. Local state would\n * be gone in exactly the frame the failure has to be shown.\n *\n * Cleared by the next attempt, never on unmount: the gate unmounts the control mid-attempt, and\n * StrictMode unmounts and remounts it in development. Like the intent, it dies with the document.\n */\nlet failed = false;\n\nconst subscribers = new Set<() => void>();\n\nfunction notify(): void {\n for (const subscriber of subscribers) {\n subscriber();\n }\n}\n\n/** Recorded before the intent is withdrawn, so the control the gate mounts on the withdraw reads it. */\nexport function reportSignOutFailure(): void {\n if (failed) {\n return;\n }\n failed = true;\n notify();\n}\n\nexport function clearSignOutFailure(): void {\n if (!failed) {\n return;\n }\n failed = false;\n notify();\n}\n\nfunction subscribe(onStoreChange: () => void): () => void {\n subscribers.add(onStoreChange);\n return () => {\n subscribers.delete(onStoreChange);\n };\n}\n\nfunction getSnapshot(): boolean {\n return failed;\n}\n\n/** Server renders never carry a failure: nothing has been able to click sign out yet. */\nfunction getServerSnapshot(): boolean {\n return false;\n}\n\nexport function useSignOutFailure(): boolean {\n return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);\n}\n"],"mappings":";;;;;;;;;;;;;;AAeA,IAAI,SAAS;AAEb,MAAM,8BAAc,IAAI,IAAgB;AAExC,SAAS,SAAe;CACtB,KAAK,MAAM,cAAc,aACvB,WAAW;AAEf;;AAGA,SAAgB,uBAA6B;CAC3C,IAAI,QACF;CAEF,SAAS;CACT,OAAO;AACT;AAEA,SAAgB,sBAA4B;CAC1C,IAAI,CAAC,QACH;CAEF,SAAS;CACT,OAAO;AACT;AAEA,SAAS,UAAU,eAAuC;CACxD,YAAY,IAAI,aAAa;CAC7B,aAAa;EACX,YAAY,OAAO,aAAa;CAClC;AACF;AAEA,SAAS,cAAuB;CAC9B,OAAO;AACT;;AAGA,SAAS,oBAA6B;CACpC,OAAO;AACT;AAEA,SAAgB,oBAA6B;CAC3C,OAAO,qBAAqB,WAAW,aAAa,iBAAiB;AACvE"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { ComponentDefinition } from "../../core/dist/define-component.js";
|
|
2
|
+
import "../../core/dist/index.js";
|
|
3
|
+
import { ComponentPropsWithRef } from "react";
|
|
4
|
+
//#region src/matter/account-menu-identity/account-menu-identity.d.ts
|
|
5
|
+
/** The unwrapped `CustomerProfile` entity; `null` when the read returned no profile. */
|
|
6
|
+
export type AccountMenuIdentityData = {
|
|
7
|
+
id?: string | null;
|
|
8
|
+
firstName?: string | null;
|
|
9
|
+
lastName?: string | null;
|
|
10
|
+
email?: string | null;
|
|
11
|
+
} | null;
|
|
12
|
+
export type AccountMenuIdentityProps = Omit<ComponentPropsWithRef<'div'>, 'children'> & {
|
|
13
|
+
/** Injected by the reactive runtime: the unwrapped `customerProfile` entity. */
|
|
14
|
+
$data?: AccountMenuIdentityData;
|
|
15
|
+
};
|
|
16
|
+
export declare const AccountMenuIdentityDefinition: ComponentDefinition<AccountMenuIdentityProps>;
|
|
17
|
+
export declare const AccountMenuIdentitySkeletonDefinition: ComponentDefinition<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLDivElement>, HTMLDivElement>>;
|
|
18
|
+
export declare function AccountMenuIdentity({ $data: $dataProp, className, ...props }: AccountMenuIdentityProps): import("react/jsx-runtime").JSX.Element | null;
|
|
19
|
+
export declare function AccountMenuIdentitySkeleton({ className, ...props }: ComponentPropsWithRef<'div'>): import("react/jsx-runtime").JSX.Element;
|
|
20
|
+
//#endregion
|
|
21
|
+
//# sourceMappingURL=account-menu-identity.d.ts.map
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { defineComponent } from "../../core/dist/define-component.js";
|
|
3
|
+
import "../../core/dist/index.js";
|
|
4
|
+
import { useDataOverride } from "../../sdk/data/data-override-context.js";
|
|
5
|
+
import { useSession } from "../session/session.js";
|
|
6
|
+
import { Avatar } from "../../particles/avatar/avatar.js";
|
|
7
|
+
import { useEntityDataResolved } from "../../sdk/data/entity-resolution-context.js";
|
|
8
|
+
import { useReactiveDataSettled } from "../../sdk/data/use-reactive-data-settled.js";
|
|
9
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
10
|
+
const AccountMenuIdentityDefinition = defineComponent({
|
|
11
|
+
name: "AccountMenuIdentity",
|
|
12
|
+
importPath: "@vtex/faststore/components",
|
|
13
|
+
component: AccountMenuIdentity,
|
|
14
|
+
fragment: `
|
|
15
|
+
fragment AccountMenuIdentityData on Query {
|
|
16
|
+
customerProfile {
|
|
17
|
+
__typename
|
|
18
|
+
id
|
|
19
|
+
firstName
|
|
20
|
+
lastName
|
|
21
|
+
email
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
`,
|
|
25
|
+
fetchStrategy: "reactive",
|
|
26
|
+
requiresContext: "session",
|
|
27
|
+
skeleton: AccountMenuIdentitySkeleton,
|
|
28
|
+
editor: {
|
|
29
|
+
label: "Account Menu Identity",
|
|
30
|
+
category: "identity"
|
|
31
|
+
},
|
|
32
|
+
schema: {},
|
|
33
|
+
guideline: "The shopper's name and initials avatar, read reactively from the persistent profile. Compose it in the my-account layout, never on a page, so its fragment joins the layout's query, which runs in the browser where the shopper's cookie is; place it under AccountAccessGate, inside AccountMenuRoot beside AccountMenuList and never inside it, so it stays outside the navigation landmark. It reads the session, so a SessionRoot must be an ancestor. It renders a skeleton until the layout's query settles, and nothing for a session that is not loggedIn, for a read that returned no profile, or for a profile with neither a name nor an email. Style it through data-fs-account-menu-identity, and hide it below the md breakpoint with the store's swatch."
|
|
34
|
+
});
|
|
35
|
+
const AccountMenuIdentitySkeletonDefinition = defineComponent({
|
|
36
|
+
name: "AccountMenuIdentitySkeleton",
|
|
37
|
+
importPath: "@vtex/faststore/components",
|
|
38
|
+
component: AccountMenuIdentitySkeleton,
|
|
39
|
+
editor: {
|
|
40
|
+
label: "Account Menu Identity Skeleton",
|
|
41
|
+
category: "identity"
|
|
42
|
+
},
|
|
43
|
+
schema: {},
|
|
44
|
+
guideline: "Loading placeholder rendered by AccountMenuIdentity while the profile is unresolved. Declares no context requirement — it renders before the matter has anything to show."
|
|
45
|
+
});
|
|
46
|
+
/** The trimmed first and last names, else the trimmed email, else an empty string. */
|
|
47
|
+
function resolveDisplayName(profile) {
|
|
48
|
+
return [profile.firstName, profile.lastName].map((part) => part?.trim() ?? "").filter(Boolean).join(" ") || (profile.email?.trim() ?? "");
|
|
49
|
+
}
|
|
50
|
+
function AccountMenuIdentity({ $data: $dataProp, className, ...props }) {
|
|
51
|
+
const $data = useDataOverride($dataProp);
|
|
52
|
+
const settled = useReactiveDataSettled();
|
|
53
|
+
const resolved = useEntityDataResolved();
|
|
54
|
+
const { state } = useSession();
|
|
55
|
+
if (state !== "loggedIn") return null;
|
|
56
|
+
if (!settled || $data === void 0 && resolved) return /* @__PURE__ */ jsx(AccountMenuIdentitySkeleton, {
|
|
57
|
+
...props,
|
|
58
|
+
className
|
|
59
|
+
});
|
|
60
|
+
if ($data == null) return null;
|
|
61
|
+
const displayName = resolveDisplayName($data);
|
|
62
|
+
if (!displayName) return null;
|
|
63
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
64
|
+
...props,
|
|
65
|
+
className,
|
|
66
|
+
"data-fs-account-menu-identity": "",
|
|
67
|
+
children: [/* @__PURE__ */ jsx(Avatar, {
|
|
68
|
+
name: displayName,
|
|
69
|
+
"data-fs-account-menu-identity-avatar": ""
|
|
70
|
+
}), /* @__PURE__ */ jsx("p", {
|
|
71
|
+
"data-fs-account-menu-identity-name": "",
|
|
72
|
+
children: displayName
|
|
73
|
+
})]
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
function AccountMenuIdentitySkeleton({ className, ...props }) {
|
|
77
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
78
|
+
...props,
|
|
79
|
+
className,
|
|
80
|
+
"data-fs-account-menu-identity-skeleton": "",
|
|
81
|
+
"aria-hidden": "true",
|
|
82
|
+
children: [/* @__PURE__ */ jsx("span", { "data-fs-account-menu-identity-skeleton-avatar": "" }), /* @__PURE__ */ jsx("span", { "data-fs-account-menu-identity-skeleton-line": "" })]
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
//#endregion
|
|
86
|
+
export { AccountMenuIdentity, AccountMenuIdentityDefinition, AccountMenuIdentitySkeleton, AccountMenuIdentitySkeletonDefinition };
|
|
87
|
+
|
|
88
|
+
//# sourceMappingURL=account-menu-identity.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"account-menu-identity.js","names":[],"sources":["../../../src/matter/account-menu-identity/account-menu-identity.tsx"],"sourcesContent":["'use client';\n\nimport { defineComponent } from '@vtex/fs-core';\nimport type { ComponentPropsWithRef } from 'react';\nimport { Avatar } from '../../particles/avatar/avatar.js';\nimport { useDataOverride } from '../../sdk/data/data-override-context.js';\nimport { useEntityDataResolved } from '../../sdk/data/entity-resolution-context.js';\nimport { useReactiveDataSettled } from '../../sdk/data/use-reactive-data-settled.js';\nimport { useSession } from '../session/session.js';\n\n// ---------------------------------------------------------------------------\n// GraphQL fragment — the persistent, cart-independent profile, not checkout's order-form `clientProfile`.\n// Raw literal, as MyAccountProfileRoot does. A single top-level entity, so the compiler delivers `$data`\n// as the `CustomerProfile` itself rather than wrapped in `{ customerProfile }`.\n// ---------------------------------------------------------------------------\n\nconst fragment = /* GraphQL */ `\n fragment AccountMenuIdentityData on Query {\n customerProfile {\n __typename\n id\n firstName\n lastName\n email\n }\n }\n`;\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/** The unwrapped `CustomerProfile` entity; `null` when the read returned no profile. */\nexport type AccountMenuIdentityData = {\n id?: string | null;\n firstName?: string | null;\n lastName?: string | null;\n email?: string | null;\n} | null;\n\nexport type AccountMenuIdentityProps = Omit<ComponentPropsWithRef<'div'>, 'children'> & {\n /** Injected by the reactive runtime: the unwrapped `customerProfile` entity. */\n $data?: AccountMenuIdentityData;\n};\n\n// ---------------------------------------------------------------------------\n// Definitions\n// ---------------------------------------------------------------------------\n\nexport const AccountMenuIdentityDefinition = defineComponent<AccountMenuIdentityProps>({\n name: 'AccountMenuIdentity',\n importPath: '@vtex/faststore/components',\n component: AccountMenuIdentity,\n fragment,\n fetchStrategy: 'reactive',\n requiresContext: 'session',\n skeleton: AccountMenuIdentitySkeleton,\n editor: { label: 'Account Menu Identity', category: 'identity' },\n schema: {},\n guideline:\n \"The shopper's name and initials avatar, read reactively from the persistent profile. Compose it in the my-account layout, never on a page, so its fragment joins the layout's query, which runs in the browser where the shopper's cookie is; place it under AccountAccessGate, inside AccountMenuRoot beside AccountMenuList and never inside it, so it stays outside the navigation landmark. It reads the session, so a SessionRoot must be an ancestor. It renders a skeleton until the layout's query settles, and nothing for a session that is not loggedIn, for a read that returned no profile, or for a profile with neither a name nor an email. Style it through data-fs-account-menu-identity, and hide it below the md breakpoint with the store's swatch.\",\n});\n\nexport const AccountMenuIdentitySkeletonDefinition = defineComponent<ComponentPropsWithRef<'div'>>({\n name: 'AccountMenuIdentitySkeleton',\n importPath: '@vtex/faststore/components',\n component: AccountMenuIdentitySkeleton,\n editor: { label: 'Account Menu Identity Skeleton', category: 'identity' },\n schema: {},\n guideline:\n 'Loading placeholder rendered by AccountMenuIdentity while the profile is unresolved. Declares no context requirement — it renders before the matter has anything to show.',\n});\n\n// ---------------------------------------------------------------------------\n// Display name\n// ---------------------------------------------------------------------------\n\n/** The trimmed first and last names, else the trimmed email, else an empty string. */\nfunction resolveDisplayName(profile: NonNullable<AccountMenuIdentityData>): string {\n const fullName = [profile.firstName, profile.lastName]\n .map((part) => part?.trim() ?? '')\n .filter(Boolean)\n .join(' ');\n return fullName || (profile.email?.trim() ?? '');\n}\n\n// ---------------------------------------------------------------------------\n// Implementations\n// ---------------------------------------------------------------------------\n\nexport function AccountMenuIdentity({ $data: $dataProp, className, ...props }: AccountMenuIdentityProps) {\n const $data = useDataOverride($dataProp);\n // A `customerProfile: null` root leaves the slice on its `undefined` fallback, unresolved, so only the\n // layout query's settled state tells a read still in flight from one that returned no profile.\n const settled = useReactiveDataSettled();\n const resolved = useEntityDataResolved();\n // Enforces the SessionRoot ancestor: useSession() throws when rendered outside one.\n const { state } = useSession();\n\n // The gate comes before any read of the profile, so only an authenticated shopper's name is ever rendered.\n if (state !== 'loggedIn') {\n return null;\n }\n if (!settled || ($data === undefined && resolved)) {\n return <AccountMenuIdentitySkeleton {...props} className={className} />;\n }\n // A read with no profile costs only the greeting: the rest of the account does not depend on it.\n if ($data == null) {\n return null;\n }\n\n const displayName = resolveDisplayName($data);\n if (!displayName) {\n return null;\n }\n\n return (\n <div {...props} className={className} data-fs-account-menu-identity=\"\">\n <Avatar name={displayName} data-fs-account-menu-identity-avatar=\"\" />\n <p data-fs-account-menu-identity-name=\"\">{displayName}</p>\n </div>\n );\n}\n\n// No context: it is rendered directly by the matter before there is anything to provide or read.\nexport function AccountMenuIdentitySkeleton({ className, ...props }: ComponentPropsWithRef<'div'>) {\n return (\n <div {...props} className={className} data-fs-account-menu-identity-skeleton=\"\" aria-hidden=\"true\">\n <span data-fs-account-menu-identity-skeleton-avatar=\"\" />\n <span data-fs-account-menu-identity-skeleton-line=\"\" />\n </div>\n );\n}\n"],"mappings":";;;;;;;;;AAiDA,MAAa,gCAAgC,gBAA0C;CACrF,MAAM;CACN,YAAY;CACZ,WAAW;CACX;;;;;;;;;;;CACA,eAAe;CACf,iBAAiB;CACjB,UAAU;CACV,QAAQ;EAAE,OAAO;EAAyB,UAAU;CAAW;CAC/D,QAAQ,CAAC;CACT,WACE;AACJ,CAAC;AAED,MAAa,wCAAwC,gBAA8C;CACjG,MAAM;CACN,YAAY;CACZ,WAAW;CACX,QAAQ;EAAE,OAAO;EAAkC,UAAU;CAAW;CACxE,QAAQ,CAAC;CACT,WACE;AACJ,CAAC;;AAOD,SAAS,mBAAmB,SAAuD;CAKjF,OAJiB,CAAC,QAAQ,WAAW,QAAQ,QAAQ,CAAC,CACnD,KAAK,SAAS,MAAM,KAAK,KAAK,EAAE,CAAC,CACjC,OAAO,OAAO,CAAC,CACf,KAAK,GACM,MAAM,QAAQ,OAAO,KAAK,KAAK;AAC/C;AAMA,SAAgB,oBAAoB,EAAE,OAAO,WAAW,WAAW,GAAG,SAAmC;CACvG,MAAM,QAAQ,gBAAgB,SAAS;CAGvC,MAAM,UAAU,uBAAuB;CACvC,MAAM,WAAW,sBAAsB;CAEvC,MAAM,EAAE,UAAU,WAAW;CAG7B,IAAI,UAAU,YACZ,OAAO;CAET,IAAI,CAAC,WAAY,UAAU,KAAA,KAAa,UACtC,OAAO,oBAAC,6BAAD;EAA6B,GAAI;EAAkB;CAAY,CAAA;CAGxE,IAAI,SAAS,MACX,OAAO;CAGT,MAAM,cAAc,mBAAmB,KAAK;CAC5C,IAAI,CAAC,aACH,OAAO;CAGT,OACE,qBAAC,OAAD;EAAK,GAAI;EAAkB;EAAW,iCAA8B;EAApE,UAAA,CACE,oBAAC,QAAD;GAAQ,MAAM;GAAa,wCAAqC;EAAI,CAAA,GACpE,oBAAC,KAAD;GAAG,sCAAmC;GAAI,UAAA;EAAe,CAAA,CACtD;;AAET;AAGA,SAAgB,4BAA4B,EAAE,WAAW,GAAG,SAAuC;CACjG,OACE,qBAAC,OAAD;EAAK,GAAI;EAAkB;EAAW,0CAAuC;EAAG,eAAY;EAA5F,UAAA,CACE,oBAAC,QAAD,EAAM,iDAA8C,GAAI,CAAA,GACxD,oBAAC,QAAD,EAAM,+CAA4C,GAAI,CAAA,CACnD;;AAET"}
|