From 0d7db567be1e7b9b9375580b0a8cf665ee69d1c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ramon=20do=20Ros=C3=A1rio?= Date: Wed, 15 Jan 2025 10:48:39 -0300 Subject: [PATCH] feat: adds SKUMatrix component (#2588) ## What's the purpose of this pull request? Implements the SKU Matrix feature and the respective controls on Product Details section of Headless CMS in order to facilitate the selection of product variations simultaneously (such as color and size) through an intuitive interface. ## How it works? Initially, we developed 3 components: `SKUMatrix`, `SKUMatrixTrigger` and `SKUMatrixSidebar`. - `SKUMatrix`: this component serves as a wrapper (component that will wrap `SKUMatrixTrigger` and `SKUMatrixSidebar`). It contains the `SKUMatrixProvider`. This provider will control all the internal state of SKU Matrix (open and close the slider, control the state of the SKUs: change the quantity of items to be added to the cart, return the list of product variations). For the SKU Matrix feature to work correctly, this wrapper must be present. To prevent `SKUMatrixSidebar` or `SKUMatrixTrigger` from being used in isolation, a treatment was made in the `useSKUMatrix` hook. - `SKUMatrixTrigger`: This component is responsible for triggering the slider opening/closing trigger. It is an abstraction of the `Button` component. Within this component, a context is consumed (`SKUMatrixProvider` contained in `SKUMatrix`), this hook returns the `setOpen` property, which is responsible for opening or closing `SKUMatrixSidebar`. - `SKUMatrixSidebar`: The feature main component. It is responsible for displaying the table containing all the variations of a product and allows the selection of multiple SKUs. For the component to work correctly, the context created and exposed by `SKUMatrix` must be consumed, as mentioned previously, all the logic and state management of `SKUMatrixSidebar` is contained in the context. This component is composed of: `SlideOver`, `Table`, `QuantitySelector`, `Price`, `Button`, `Skeleton`. As mentioned in the description of the created components, a context was created to manage states of SKU Matrix feature. There is a hook called `useSKUMatrix`, which is responsible for consuming the `SKUMatrixProvider` and returning all the properties so that the components in the chain can use it. A validation was implemented so that the components in the chain can only be used through the `SKUMatrix` wrapper. Continuing the implementation of the SKU Matrix resource, within `@faststore/core` in the ui folder, an abstraction of `SKUMatrixSidebar` was created. All the logic for capturing and formatting the data is done in this abstraction. To request the data, a new query was created. To use it, simply call the `useAllVariantProducts` hook. To consume this hook, it is necessary to pass some properties, of which we can highlight `callBack` and `enabled`. - `callBack`: is a function that will be executed only when the request is successful and will return all the data. This method will return the data fully formatted following the format pre-established in the `SKUMatrixSidebar` component. - `enabled`: is a `boolean` (true/false). It will inform the query whether the `SKUMatrixSidebar` is open or not. The request will only be made if the slider is open, otherwise it will not be, avoiding unnecessary requests. Within this `SKUMatrixSidebar` abstraction, the `SKUMatrix` context must be consumed. As mentioned previously, the context will control all states, such as informing the context of the list of product variations. Since this abstraction will be responsible for making the request and informing the context of the data, the `callBack` that is passed in `useAllVariantProducts` will be the method to add the items that will be displayed in the `SKUMatrixSidebar` table. Therefore, the `setAllVariantProducts` method present in the `SKUMatrix` context must be assigned. This way, the formatted data that will be returned will be informed to the context and later, the `SKUMatrixSidebar` will be able to consume this data and display it on the screen. Speaking a little about `useBuyButton`, this hook is responsible for adding the selected items to the cart. Previously, it did not accept a list of items, so it was necessary to modify it to meet the new functionality. Since the hook must consume some information to then inform the `buyButtonProps` (responsible for preparing the data to be inserted in the cart), it needs to observe the data of the products to be added to the cart. This data is returned from the `SKUMatrix` context, so whenever there is any type of modification in the context state (for example, a change in the quantity of a SKU within the `SKUMatrixSidebar` table), it will be reflected in the `useBuyButton` hook, and the properties will be generated correctly. Therefore, we performed: - Construction of 3 new components: `SKUMatrix`, `SKUMatrixTrigger`, `SKUMatrixSidebar` - Provider to manage the entire SKU Matrix context - Hook for consumption of the context: `useSKUMatrix` - Exclusive query to access only the important data for the construction of SKU Matrix: `useAllVariantProducts` - Change in the `useBuyButton` hook ## How to test it? Check the "Should display SKUMatrix?" checkbox field on Product Details section of Product Details Page at Headless CMS and publish changes. Then access a product details page at store. A "Select multiple" button must be appear below "Add to Cart" button, responsible to trigger the SKU Matrix Sidebar. ## References RFC: [B2B Faststore - SKU Matrix.pdf](https://github.com/user-attachments/files/17346054/B2B.Faststore.-.SKU.Matrix.pdf) ## Printscreens Headless CMS ![SKU Matrix - Headless CMS](https://github.com/user-attachments/assets/0e234c04-8e85-42d5-9acf-768f041b11ba) SKU Matrix trigger button: ![SKU Matrix - Trigger Button](https://github.com/user-attachments/assets/1843d695-7ad8-4eed-b51b-3201ece1b56c) SKU Matrix side bar: ![SKU Matrix - SKU Matrix Sidebar](https://github.com/user-attachments/assets/486276f3-862a-43c1-938b-fab095f120b6) --------- Co-authored-by: Hiago Moreira <158482127+HiagoMoreiraCubos@users.noreply.github.com> Co-authored-by: Fanny Chien Co-authored-by: Hiago Moreira --- packages/api/src/__generated__/schema.ts | 2 + .../platforms/vtex/resolvers/skuVariations.ts | 1 + packages/api/src/typeDefs/skuVariants.graphql | 5 + packages/components/src/hooks/index.ts | 3 +- packages/components/src/hooks/useSKUMatrix.ts | 15 + packages/components/src/index.ts | 11 + .../src/organisms/SKUMatrix/SKUMatrix.tsx | 22 + .../organisms/SKUMatrix/SKUMatrixSidebar.tsx | 297 +++++++++ .../organisms/SKUMatrix/SKUMatrixTrigger.tsx | 31 + .../src/organisms/SKUMatrix/index.ts | 8 + .../SKUMatrix/provider/SKUMatrixProvider.tsx | 104 +++ packages/core/@generated/gql.ts | 20 +- packages/core/@generated/graphql.ts | 157 +++++ packages/core/cms/faststore/sections.json | 599 ++++++------------ .../ProductDetails/DefaultComponents.ts | 10 +- .../ProductDetails/ProductDetails.tsx | 49 +- .../ProductDetails/section.module.scss | 42 +- .../ui/SKUMatrix/SKUMatrixSidebar.tsx | 132 ++++ packages/core/src/sdk/cart/useBuyButton.ts | 61 +- .../src/sdk/product/useAllVariantProducts.ts | 114 ++++ packages/core/src/typings/overrides.ts | 26 +- .../organisms/SKUMatrix/styles.scss | 163 +++++ packages/ui/src/styles/components.scss | 2 +- 23 files changed, 1431 insertions(+), 443 deletions(-) create mode 100644 packages/components/src/hooks/useSKUMatrix.ts create mode 100644 packages/components/src/organisms/SKUMatrix/SKUMatrix.tsx create mode 100644 packages/components/src/organisms/SKUMatrix/SKUMatrixSidebar.tsx create mode 100644 packages/components/src/organisms/SKUMatrix/SKUMatrixTrigger.tsx create mode 100644 packages/components/src/organisms/SKUMatrix/index.ts create mode 100644 packages/components/src/organisms/SKUMatrix/provider/SKUMatrixProvider.tsx create mode 100644 packages/core/src/components/ui/SKUMatrix/SKUMatrixSidebar.tsx create mode 100644 packages/core/src/sdk/product/useAllVariantProducts.ts create mode 100644 packages/ui/src/components/organisms/SKUMatrix/styles.scss diff --git a/packages/api/src/__generated__/schema.ts b/packages/api/src/__generated__/schema.ts index ad8de635fc..83f0480b3f 100644 --- a/packages/api/src/__generated__/schema.ts +++ b/packages/api/src/__generated__/schema.ts @@ -611,6 +611,8 @@ export type SkuVariants = { __typename?: 'SkuVariants'; /** SKU property values for the current SKU. */ activeVariations?: Maybe; + /** All possible variant combinations of the current product. It also includes the data for each variant. */ + allVariantProducts?: Maybe>; /** All available options for each SKU variant property, indexed by their name. */ allVariantsByName?: Maybe; /** diff --git a/packages/api/src/platforms/vtex/resolvers/skuVariations.ts b/packages/api/src/platforms/vtex/resolvers/skuVariations.ts index 81d725dac6..e59fc7794c 100644 --- a/packages/api/src/platforms/vtex/resolvers/skuVariations.ts +++ b/packages/api/src/platforms/vtex/resolvers/skuVariations.ts @@ -40,4 +40,5 @@ export const SkuVariants: Record> = { return filteredFormattedVariations }, + allVariantProducts: (root) => root.isVariantOf.items, } diff --git a/packages/api/src/typeDefs/skuVariants.graphql b/packages/api/src/typeDefs/skuVariants.graphql index 866b3492be..a56d69d0e0 100644 --- a/packages/api/src/typeDefs/skuVariants.graphql +++ b/packages/api/src/typeDefs/skuVariants.graphql @@ -24,6 +24,11 @@ type SkuVariants { considered the dominant one. """ availableVariations(dominantVariantName: String): FormattedVariants + + """ + All possible variant combinations of the current product. It also includes the data for each variant. + """ + allVariantProducts: [StoreProduct!] } """ diff --git a/packages/components/src/hooks/index.ts b/packages/components/src/hooks/index.ts index ae00f0330c..302d67bd2f 100644 --- a/packages/components/src/hooks/index.ts +++ b/packages/components/src/hooks/index.ts @@ -2,6 +2,7 @@ export { default as UIProvider, Toast as ToastProps, useUI } from './UIProvider' export { useFadeEffect } from './useFadeEffect' export { useTrapFocus } from './useTrapFocus' export { useSearch } from './useSearch' +export { useSKUMatrix } from './useSKUMatrix' export { useScrollDirection } from './useScrollDirection' export { useSlider } from './useSlider' export type { @@ -11,5 +12,3 @@ export type { SlideDirection, } from './useSlider' export { useSlideVisibility } from './useSlideVisibility' - - diff --git a/packages/components/src/hooks/useSKUMatrix.ts b/packages/components/src/hooks/useSKUMatrix.ts new file mode 100644 index 0000000000..3db5e45517 --- /dev/null +++ b/packages/components/src/hooks/useSKUMatrix.ts @@ -0,0 +1,15 @@ +import { useContext } from 'react' + +import { SKUMatrixContext } from '../organisms/SKUMatrix/provider/SKUMatrixProvider' + +export function useSKUMatrix() { + const context = useContext(SKUMatrixContext) + + if (!context) { + throw new Error( + 'Do not use SKUMatrix components outside the SKUMatrix context.' + ) + } + + return context +} diff --git a/packages/components/src/index.ts b/packages/components/src/index.ts index 3764572943..252250a912 100644 --- a/packages/components/src/index.ts +++ b/packages/components/src/index.ts @@ -360,3 +360,14 @@ export type { SlideOverProps, SlideOverHeaderProps, } from './organisms/SlideOver' + +export { + default as SKUMatrix, + SKUMatrixTrigger, + SKUMatrixSidebar, +} from './organisms/SKUMatrix' +export type { + SKUMatrixProps, + SKUMatrixTriggerProps, + SKUMatrixSidebarProps +} from './organisms/SKUMatrix' diff --git a/packages/components/src/organisms/SKUMatrix/SKUMatrix.tsx b/packages/components/src/organisms/SKUMatrix/SKUMatrix.tsx new file mode 100644 index 0000000000..7b1e404683 --- /dev/null +++ b/packages/components/src/organisms/SKUMatrix/SKUMatrix.tsx @@ -0,0 +1,22 @@ +import React, { forwardRef, HTMLAttributes } from 'react' +import SKUMatrixProvider from './provider/SKUMatrixProvider' + +export interface SKUMatrixProps extends HTMLAttributes { + /** + * ID to find this component in testing tools (e.g.: cypress, testing library, and jest). + */ + testId?: string +} + +const SKUMatrix = forwardRef(function SKUMatrix( + { testId = 'fs-sku-matrix', children, ...otherProps }, + ref +) { + return ( +
+ {children} +
+ ) +}) + +export default SKUMatrix diff --git a/packages/components/src/organisms/SKUMatrix/SKUMatrixSidebar.tsx b/packages/components/src/organisms/SKUMatrix/SKUMatrixSidebar.tsx new file mode 100644 index 0000000000..fee3a1acc9 --- /dev/null +++ b/packages/components/src/organisms/SKUMatrix/SKUMatrixSidebar.tsx @@ -0,0 +1,297 @@ +import Image from 'next/image' +import React, { useMemo } from 'react' +import { Badge, Button, QuantitySelector, Skeleton } from '../..' +import Price, { PriceFormatter } from '../../atoms/Price' +import Icon from '../../atoms/Icon' +import { useFadeEffect, useSKUMatrix, useUI } from '../../hooks' +import { + Table, + TableBody, + TableCell, + TableHead, + TableRow, +} from '../../molecules/Table' +import SlideOver, { SlideOverHeader, SlideOverProps } from '../SlideOver' + +interface VariationProductColumn { + name: string + additionalColumns: Array<{ label: string; value: string }> + availability: { + label: string + stockDisplaySettings: 'showStockQuantity' | 'showAvailability' + } + price: number + quantitySelector: number +} + +export interface SKUMatrixSidebarProps + extends Omit { + /** + * Title for the SKUMatrixSidebar component. + */ + title?: string + /** + * Represents the variations products to building the table. + */ + columns: VariationProductColumn + /** + * Properties related to the 'add to cart' button + */ + buyProps: { + 'data-testid': string + 'data-sku': string + 'data-seller': string + onClick(e: React.MouseEvent): void + } + /** + * Formatter function that transforms the raw price value and render the result. + */ + formatter?: PriceFormatter + /** + * Check if some result is still loading before render the result. + */ + loading?: boolean +} + +function SKUMatrixSidebar({ + direction = 'rightSide', + title, + overlayProps, + size = 'partial', + children, + columns, + buyProps: { onClick: buyButtonOnClick, ...buyProps }, + loading, + formatter, + ...otherProps +}: SKUMatrixSidebarProps) { + const { + isOpen, + setIsOpen, + setAllVariantProducts, + allVariantProducts, + onChangeQuantityItem, + } = useSKUMatrix() + const { pushToast } = useUI() + const { fade } = useFadeEffect() + + const cartDetails = useMemo(() => { + return allVariantProducts.reduce( + (acc, product) => ({ + amount: acc.amount + product.selectedCount, + subtotal: acc.subtotal + product.selectedCount * product.price, + }), + { amount: 0, subtotal: 0 } + ) + }, [allVariantProducts]) + + function resetQuantityItems() { + setAllVariantProducts((prev) => + prev.map((item) => ({ ...item, quantity: 0 })) + ) + } + + function onClose() { + resetQuantityItems() + setIsOpen(false) + } + + function handleAddToCart(e: React.MouseEvent) { + buyButtonOnClick(e) + onClose() + } + + const totalColumnsSkeletonLength = + Object.keys(columns).filter((v) => v !== 'additionalColumns').length + + (columns.additionalColumns?.length ?? 0) + + return ( + + +

{title}

+
+ + {children} + + + + + + {columns.name} + + + {columns.additionalColumns?.map(({ label, value }) => ( + + {label} + + ))} + + + {columns.availability.label} + + + + {columns.price} + + + + {columns.quantitySelector} + + + + + + {loading ? ( + <> + {Array.from({ length: 5 }).map((_, index) => { + return ( + + {Array.from({ + length: totalColumnsSkeletonLength, + }).map((_, index) => { + return ( + + + + + + ) + })} + + ) + })} + + ) : ( + <> + {allVariantProducts.map((variantProduct) => ( + + + + {variantProduct.name} + + + {columns.additionalColumns?.map(({ value }) => ( + + {variantProduct.specifications[value.toLowerCase()]} + + ))} + + + {columns.availability.stockDisplaySettings === + 'showAvailability' && ( + + {variantProduct.availability === 'outOfStock' + ? 'Out of stock' + : 'Available'} + + )} + + {columns.availability.stockDisplaySettings === + 'showStockQuantity' && variantProduct.inventory} + + + +
+ +
+
+ + +
+ + onChangeQuantityItem(variantProduct.id, value) + } + onValidateBlur={( + min: number, + maxValue: number, + quantity: number + ) => { + pushToast({ + title: 'Invalid quantity!', + message: `The quantity you entered is outside the range of ${min} to ${maxValue}. The quantity was set to ${quantity}.`, + status: 'INFO', + icon: ( + + ), + }) + }} + /> +
+
+
+ ))} + + )} +
+
+ +
+
+

+ {cartDetails.amount} {cartDetails.amount !== 1 ? 'Items' : 'Item'} +

+ +
+ + +
+
+ ) +} + +export default SKUMatrixSidebar diff --git a/packages/components/src/organisms/SKUMatrix/SKUMatrixTrigger.tsx b/packages/components/src/organisms/SKUMatrix/SKUMatrixTrigger.tsx new file mode 100644 index 0000000000..4332265738 --- /dev/null +++ b/packages/components/src/organisms/SKUMatrix/SKUMatrixTrigger.tsx @@ -0,0 +1,31 @@ +import React, { forwardRef } from 'react' +import Button from '../../atoms/Button' +import type { ButtonProps } from '../../atoms/Button' +import { useSKUMatrix } from '../../hooks' + +export type SKUMatrixTriggerProps = ButtonProps + +const SKUMatrixTrigger = forwardRef( + function SKUMatrixTrigger( + { children, variant = 'secondary', onClick, ...otherProps }, + ref + ) { + const { setIsOpen } = useSKUMatrix() + + return ( + + ) + } +) + +export default SKUMatrixTrigger diff --git a/packages/components/src/organisms/SKUMatrix/index.ts b/packages/components/src/organisms/SKUMatrix/index.ts new file mode 100644 index 0000000000..3be2f31a81 --- /dev/null +++ b/packages/components/src/organisms/SKUMatrix/index.ts @@ -0,0 +1,8 @@ +export { default } from './SKUMatrix' +export type { SKUMatrixProps } from './SKUMatrix' + +export { default as SKUMatrixTrigger } from './SKUMatrixTrigger' +export type { SKUMatrixTriggerProps } from './SKUMatrixTrigger' + +export { default as SKUMatrixSidebar } from './SKUMatrixSidebar' +export type { SKUMatrixSidebarProps } from './SKUMatrixSidebar' diff --git a/packages/components/src/organisms/SKUMatrix/provider/SKUMatrixProvider.tsx b/packages/components/src/organisms/SKUMatrix/provider/SKUMatrixProvider.tsx new file mode 100644 index 0000000000..263ca41a55 --- /dev/null +++ b/packages/components/src/organisms/SKUMatrix/provider/SKUMatrixProvider.tsx @@ -0,0 +1,104 @@ +import React, { createContext, useCallback, useState, SetStateAction } from 'react' +import type { ReactNode } from 'react' + +interface IAllVariantProducts { + id: string + name: string + image: { + url: string + alternateName: string + } + inventory: number + availability: string + price: number + listPrice: number + priceWithTaxes: number + listPriceWithTaxes: number + specifications: Record + selectedCount: number + offers: { + highPrice: number + lowPrice: number + lowPriceWithTaxes: number + offerCount: number + priceCurrency: string + offers: Array<{ + listPrice: number + listPriceWithTaxes: number + sellingPrice: number + priceCurrency: string + price: number + priceWithTaxes: number + priceValidUntil: string + itemCondition: string + availability: string + quantity: number + }> + } +} + +export interface SKUMatrixProviderContextValue { + /* + A boolean value that indicates if the modal is open. + */ + isOpen: boolean + /* + Array of all variant products. + */ + allVariantProducts: IAllVariantProducts[] + /* + Function to set the array of all variant products. + */ + setAllVariantProducts( + items: SetStateAction + ): void + /* + */ + onChangeQuantityItem(id: string, value: number): IAllVariantProducts[] + /* + function to set the modal is open + */ + setIsOpen(value: boolean): void +} + +export const SKUMatrixContext = + createContext(null) + +function SKUMatrixProvider({ children }: { children: ReactNode }) { + const [isOpen, setIsOpen] = useState(false) + const [allVariantProducts, setAllVariantProducts] = useState< + IAllVariantProducts[] + >([]) + + const onChangeQuantityItem = useCallback( + (id: string, value: number) => { + const data = [...allVariantProducts] + const matchedSKU = data.find((item) => item.id === id) + + if(matchedSKU) { + matchedSKU.selectedCount = value + } + + setAllVariantProducts(data) + + return data + }, + [allVariantProducts] + ) + + return ( + + {children} + + ) +} + +export default SKUMatrixProvider diff --git a/packages/core/@generated/gql.ts b/packages/core/@generated/gql.ts index 0176621770..8fcec3cc26 100644 --- a/packages/core/@generated/gql.ts +++ b/packages/core/@generated/gql.ts @@ -16,8 +16,10 @@ const documents = { types.ProductSummary_ProductFragmentDoc, '\n fragment Filter_facets on StoreFacet {\n ... on StoreFacetRange {\n key\n label\n\n min {\n selected\n absolute\n }\n\n max {\n selected\n absolute\n }\n\n __typename\n }\n ... on StoreFacetBoolean {\n key\n label\n values {\n label\n value\n selected\n quantity\n }\n\n __typename\n }\n }\n': types.Filter_FacetsFragmentDoc, - '\n fragment ProductDetailsFragment_product on StoreProduct {\n id: productID\n sku\n name\n gtin\n description\n unitMultiplier\n isVariantOf {\n name\n productGroupID\n skuVariants {\n activeVariations\n slugsMap\n availableVariations\n }\n }\n\n image {\n url\n alternateName\n }\n\n brand {\n name\n }\n\n offers {\n lowPrice\n lowPriceWithTaxes\n offers {\n availability\n price\n priceWithTaxes\n listPrice\n listPriceWithTaxes\n seller {\n identifier\n }\n }\n }\n\n additionalProperty {\n propertyID\n name\n value\n valueReference\n }\n\n # Contains necessary info to add this item to cart\n ...CartProductItem\n }\n': + '\n fragment ProductDetailsFragment_product on StoreProduct {\n id: productID\n sku\n name\n gtin\n description\n unitMultiplier\n isVariantOf {\n name\n productGroupID\n\t\t\tskuVariants {\n activeVariations\n slugsMap\n availableVariations\n }\n }\n\n image {\n url\n alternateName\n }\n\n brand {\n name\n }\n\n offers {\n lowPrice\n lowPriceWithTaxes\n offers {\n availability\n price\n priceWithTaxes\n listPrice\n listPriceWithTaxes\n seller {\n identifier\n }\n }\n }\n\n additionalProperty {\n propertyID\n name\n value\n valueReference\n }\n\n # Contains necessary info to add this item to cart\n ...CartProductItem\n }\n': types.ProductDetailsFragment_ProductFragmentDoc, + '\n fragment ProductSKUMatrixSidebarFragment_product on StoreProduct {\n id: productID\n isVariantOf {\n name\n productGroupID\n skuVariants {\n activeVariations\n slugsMap\n availableVariations\n allVariantProducts {\n\t\t\t\t\tsku\n name\n image {\n url\n alternateName\n }\n offers {\n highPrice\n lowPrice\n lowPriceWithTaxes\n offerCount\n priceCurrency\n offers {\n listPrice\n listPriceWithTaxes\n sellingPrice\n priceCurrency\n price\n priceWithTaxes\n priceValidUntil\n itemCondition\n availability\n quantity\n }\n }\n additionalProperty {\n propertyID\n value\n name\n valueReference\n }\n }\n }\n }\n }\n': + types.ProductSkuMatrixSidebarFragment_ProductFragmentDoc, '\n fragment ClientManyProducts on Query {\n search(\n first: $first\n after: $after\n sort: $sort\n term: $term\n selectedFacets: $selectedFacets\n sponsoredCount: $sponsoredCount\n\n ) {\n products {\n pageInfo {\n totalCount\n }\n }\n }\n }\n': types.ClientManyProductsFragmentDoc, '\n fragment ClientProduct on Query {\n product(locator: $locator) {\n id: productID\n }\n }\n': @@ -42,6 +44,8 @@ const documents = { types.ValidateCartMutationDocument, '\n mutation SubscribeToNewsletter($data: IPersonNewsletter!) {\n subscribeToNewsletter(data: $data) {\n id\n }\n }\n': types.SubscribeToNewsletterDocument, + '\n query ClientAllVariantProductsQuery($locator: [IStoreSelectedFacet!]!) {\n product(locator: $locator) {\n ...ProductSKUMatrixSidebarFragment_product\n }\n }\n': + types.ClientAllVariantProductsQueryDocument, '\n query ClientManyProductsQuery(\n $first: Int!\n $after: String\n $sort: StoreSort!\n $term: String!\n $selectedFacets: [IStoreSelectedFacet!]!\n $sponsoredCount: Int\n ) {\n ...ClientManyProducts\n search(\n first: $first\n after: $after\n sort: $sort\n term: $term\n selectedFacets: $selectedFacets\n sponsoredCount: $sponsoredCount\n ) {\n products {\n pageInfo {\n totalCount\n }\n edges {\n node {\n ...ProductSummary_product\n }\n }\n }\n }\n }\n': types.ClientManyProductsQueryDocument, '\n query ClientProductGalleryQuery(\n $first: Int!\n $after: String!\n $sort: StoreSort!\n $term: String!\n $selectedFacets: [IStoreSelectedFacet!]!\n ) {\n ...ClientProductGallery\n redirect(term: $term, selectedFacets: $selectedFacets) {\n url\n }\n search(\n first: $first\n after: $after\n sort: $sort\n term: $term\n selectedFacets: $selectedFacets\n ) {\n products {\n pageInfo {\n totalCount\n }\n }\n facets {\n ...Filter_facets\n }\n metadata {\n ...SearchEvent_metadata\n }\n }\n }\n\n fragment SearchEvent_metadata on SearchMetadata {\n isTermMisspelled\n logicalOperator\n fuzzy\n }\n': @@ -74,8 +78,14 @@ export function gql( * The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ export function gql( - source: '\n fragment ProductDetailsFragment_product on StoreProduct {\n id: productID\n sku\n name\n gtin\n description\n unitMultiplier\n isVariantOf {\n name\n productGroupID\n skuVariants {\n activeVariations\n slugsMap\n availableVariations\n }\n }\n\n image {\n url\n alternateName\n }\n\n brand {\n name\n }\n\n offers {\n lowPrice\n lowPriceWithTaxes\n offers {\n availability\n price\n priceWithTaxes\n listPrice\n listPriceWithTaxes\n seller {\n identifier\n }\n }\n }\n\n additionalProperty {\n propertyID\n name\n value\n valueReference\n }\n\n # Contains necessary info to add this item to cart\n ...CartProductItem\n }\n' + source: '\n fragment ProductDetailsFragment_product on StoreProduct {\n id: productID\n sku\n name\n gtin\n description\n unitMultiplier\n isVariantOf {\n name\n productGroupID\n\t\t\tskuVariants {\n activeVariations\n slugsMap\n availableVariations\n }\n }\n\n image {\n url\n alternateName\n }\n\n brand {\n name\n }\n\n offers {\n lowPrice\n lowPriceWithTaxes\n offers {\n availability\n price\n priceWithTaxes\n listPrice\n listPriceWithTaxes\n seller {\n identifier\n }\n }\n }\n\n additionalProperty {\n propertyID\n name\n value\n valueReference\n }\n\n # Contains necessary info to add this item to cart\n ...CartProductItem\n }\n' ): typeof import('./graphql').ProductDetailsFragment_ProductFragmentDoc +/** + * The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function gql( + source: '\n fragment ProductSKUMatrixSidebarFragment_product on StoreProduct {\n id: productID\n isVariantOf {\n name\n productGroupID\n skuVariants {\n activeVariations\n slugsMap\n availableVariations\n allVariantProducts {\n\t\t\t\t\tsku\n name\n image {\n url\n alternateName\n }\n offers {\n highPrice\n lowPrice\n lowPriceWithTaxes\n offerCount\n priceCurrency\n offers {\n listPrice\n listPriceWithTaxes\n sellingPrice\n priceCurrency\n price\n priceWithTaxes\n priceValidUntil\n itemCondition\n availability\n quantity\n }\n }\n additionalProperty {\n propertyID\n value\n name\n valueReference\n }\n }\n }\n }\n }\n' +): typeof import('./graphql').ProductSkuMatrixSidebarFragment_ProductFragmentDoc /** * The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ @@ -148,6 +158,12 @@ export function gql( export function gql( source: '\n mutation SubscribeToNewsletter($data: IPersonNewsletter!) {\n subscribeToNewsletter(data: $data) {\n id\n }\n }\n' ): typeof import('./graphql').SubscribeToNewsletterDocument +/** + * The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function gql( + source: '\n query ClientAllVariantProductsQuery($locator: [IStoreSelectedFacet!]!) {\n product(locator: $locator) {\n ...ProductSKUMatrixSidebarFragment_product\n }\n }\n' +): typeof import('./graphql').ClientAllVariantProductsQueryDocument /** * The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ diff --git a/packages/core/@generated/graphql.ts b/packages/core/@generated/graphql.ts index 730dc2d938..0f71d6e880 100644 --- a/packages/core/@generated/graphql.ts +++ b/packages/core/@generated/graphql.ts @@ -599,6 +599,8 @@ export type ShippingSla = { export type SkuVariants = { /** SKU property values for the current SKU. */ activeVariations: Maybe + /** All possible variant combinations of the current product. It also includes the data for each variant. */ + allVariantProducts: Maybe> /** All available options for each SKU variant property, indexed by their name. */ allVariantsByName: Maybe /** @@ -1221,6 +1223,49 @@ export type ProductDetailsFragment_ProductFragment = { }> } +export type ProductSkuMatrixSidebarFragment_ProductFragment = { + id: string + isVariantOf: { + name: string + productGroupID: string + skuVariants: { + activeVariations: any | null + slugsMap: any | null + availableVariations: any | null + allVariantProducts: Array<{ + sku: string + name: string + image: Array<{ url: string; alternateName: string }> + offers: { + highPrice: number + lowPrice: number + lowPriceWithTaxes: number + offerCount: number + priceCurrency: string + offers: Array<{ + listPrice: number + listPriceWithTaxes: number + sellingPrice: number + priceCurrency: string + price: number + priceWithTaxes: number + priceValidUntil: string + itemCondition: string + availability: string + quantity: number + }> + } + additionalProperty: Array<{ + propertyID: string + value: any + name: string + valueReference: any + }> + }> | null + } | null + } +} + export type ClientManyProductsFragment = { search: { products: { pageInfo: { totalCount: number } } } } @@ -1428,6 +1473,55 @@ export type SubscribeToNewsletterMutation = { subscribeToNewsletter: { id: string } | null } +export type ClientAllVariantProductsQueryQueryVariables = Exact<{ + locator: Array | IStoreSelectedFacet +}> + +export type ClientAllVariantProductsQueryQuery = { + product: { + id: string + isVariantOf: { + name: string + productGroupID: string + skuVariants: { + activeVariations: any | null + slugsMap: any | null + availableVariations: any | null + allVariantProducts: Array<{ + sku: string + name: string + image: Array<{ url: string; alternateName: string }> + offers: { + highPrice: number + lowPrice: number + lowPriceWithTaxes: number + offerCount: number + priceCurrency: string + offers: Array<{ + listPrice: number + listPriceWithTaxes: number + sellingPrice: number + priceCurrency: string + price: number + priceWithTaxes: number + priceValidUntil: string + itemCondition: string + availability: string + quantity: number + }> + } + additionalProperty: Array<{ + propertyID: string + value: any + name: string + valueReference: any + }> + }> | null + } | null + } + } +} + export type ClientManyProductsQueryQueryVariables = Exact<{ first: Scalars['Int']['input'] after: InputMaybe @@ -1891,6 +1985,60 @@ export const ProductDetailsFragment_ProductFragmentDoc = ProductDetailsFragment_ProductFragment, unknown > +export const ProductSkuMatrixSidebarFragment_ProductFragmentDoc = + new TypedDocumentString( + ` + fragment ProductSKUMatrixSidebarFragment_product on StoreProduct { + id: productID + isVariantOf { + name + productGroupID + skuVariants { + activeVariations + slugsMap + availableVariations + allVariantProducts { + sku + name + image { + url + alternateName + } + offers { + highPrice + lowPrice + lowPriceWithTaxes + offerCount + priceCurrency + offers { + listPrice + listPriceWithTaxes + sellingPrice + priceCurrency + price + priceWithTaxes + priceValidUntil + itemCondition + availability + quantity + } + } + additionalProperty { + propertyID + value + name + valueReference + } + } + } + } +} + `, + { fragmentName: 'ProductSKUMatrixSidebarFragment_product' } + ) as unknown as TypedDocumentString< + ProductSkuMatrixSidebarFragment_ProductFragment, + unknown + > export const ClientManyProductsFragmentDoc = new TypedDocumentString( ` fragment ClientManyProducts on Query { @@ -2102,6 +2250,15 @@ export const SubscribeToNewsletterDocument = { SubscribeToNewsletterMutation, SubscribeToNewsletterMutationVariables > +export const ClientAllVariantProductsQueryDocument = { + __meta__: { + operationName: 'ClientAllVariantProductsQuery', + operationHash: '4039e05f01a2fe449e20e8b82170d0ba94b1fbe9', + }, +} as unknown as TypedDocumentString< + ClientAllVariantProductsQueryQuery, + ClientAllVariantProductsQueryQueryVariables +> export const ClientManyProductsQueryDocument = { __meta__: { operationName: 'ClientManyProductsQuery', diff --git a/packages/core/cms/faststore/sections.json b/packages/core/cms/faststore/sections.json index c33bad1c1c..8c4f1cf1dd 100644 --- a/packages/core/cms/faststore/sections.json +++ b/packages/core/cms/faststore/sections.json @@ -29,12 +29,8 @@ "icon": { "title": "Icon", "type": "string", - "enumNames": [ - "Magnifying Glass" - ], - "enum": [ - "MagnifyingGlass" - ], + "enumNames": ["Magnifying Glass"], + "enum": ["MagnifyingGlass"], "default": "MagnifyingGlass" }, "alt": { @@ -67,12 +63,8 @@ "icon": { "title": "Icon", "type": "string", - "enumNames": [ - "Clock Clockwise" - ], - "enum": [ - "ClockClockwise" - ], + "enumNames": ["Clock Clockwise"], + "enum": ["ClockClockwise"], "default": "ClockClockwise" }, "alt": { @@ -116,12 +108,8 @@ "icon": { "title": "Icon", "type": "string", - "enumNames": [ - "Magnifying Glass" - ], - "enum": [ - "MagnifyingGlass" - ] + "enumNames": ["Magnifying Glass"], + "enum": ["MagnifyingGlass"] }, "alt": { "type": "string", @@ -163,16 +151,12 @@ "title": "Navbar", "type": "object", "description": "Navbar configuration", - "required": [ - "logo" - ], + "required": ["logo"], "properties": { "logo": { "title": "Logo", "type": "object", - "required": [ - "src" - ], + "required": ["src"], "properties": { "src": { "title": "Image", @@ -188,10 +172,7 @@ "link": { "title": "Logo Link", "type": "object", - "required": [ - "url", - "title" - ], + "required": ["url", "title"], "properties": { "url": { "title": "Link URL", @@ -209,9 +190,7 @@ "title": "Search Input", "description": "Search Input configurations", "type": "object", - "required": [ - "sort" - ], + "required": ["sort"], "properties": { "placeholder": { "title": "Placeholder for Search Bar", @@ -256,12 +235,8 @@ "icon": { "title": "Icon", "type": "string", - "enumNames": [ - "User" - ], - "enum": [ - "User" - ], + "enumNames": ["User"], + "enum": ["User"], "default": "User" }, "alt": { @@ -290,12 +265,8 @@ "icon": { "title": "Icon", "type": "string", - "enumNames": [ - "Shopping Cart" - ], - "enum": [ - "ShoppingCart" - ], + "enumNames": ["Shopping Cart"], + "enum": ["ShoppingCart"], "default": "ShoppingCart" }, "alt": { @@ -325,12 +296,8 @@ "icon": { "title": "Icon", "type": "string", - "enumNames": [ - "Map Pin" - ], - "enum": [ - "MapPin" - ], + "enumNames": ["Map Pin"], + "enum": ["MapPin"], "default": "MapPin" }, "alt": { @@ -354,10 +321,7 @@ "items": { "title": "Link", "type": "object", - "required": [ - "text", - "url" - ], + "required": ["text", "url"], "properties": { "text": { "title": "Link Text", @@ -381,12 +345,8 @@ "icon": { "title": "Icon", "type": "string", - "enumNames": [ - "List" - ], - "enum": [ - "List" - ], + "enumNames": ["List"], + "enum": ["List"], "default": "List" }, "alt": { @@ -421,11 +381,7 @@ "title": "Alert", "description": "Add an alert", "type": "object", - "required": [ - "icon", - "content", - "dismissible" - ], + "required": ["icon", "content", "dismissible"], "properties": { "icon": { "type": "string", @@ -438,14 +394,7 @@ "Truck", "User" ], - "enum": [ - "Bell", - "BellRinging", - "Checked", - "Info", - "Truck", - "User" - ] + "enum": ["Bell", "BellRinging", "Checked", "Info", "Truck", "User"] }, "content": { "type": "string", @@ -489,11 +438,7 @@ "items": { "title": "Incentive", "type": "object", - "required": [ - "title", - "firstLineText", - "icon" - ], + "required": ["title", "firstLineText", "icon"], "properties": { "title": { "type": "string", @@ -552,10 +497,7 @@ "items": { "title": "Link", "type": "object", - "required": [ - "text", - "url" - ], + "required": ["text", "url"], "properties": { "text": { "title": "Link Text", @@ -589,11 +531,7 @@ "items": { "title": "Link", "type": "object", - "required": [ - "alt", - "url", - "icon" - ], + "required": ["alt", "url", "icon"], "properties": { "icon": { "title": "Icon", @@ -648,10 +586,7 @@ "link": { "title": "Logo Link", "type": "object", - "required": [ - "url", - "title" - ], + "required": ["url", "title"], "properties": { "url": { "title": "Link URL", @@ -672,9 +607,7 @@ "acceptedPaymentMethods": { "title": "Payment Methods Sections", "type": "object", - "required": [ - "showPaymentMethods" - ], + "required": ["showPaymentMethods"], "properties": { "showPaymentMethods": { "title": "Display Payment Methods", @@ -692,10 +625,7 @@ "items": { "title": "Payment Method", "type": "object", - "required": [ - "icon", - "alt" - ], + "required": ["icon", "alt"], "properties": { "icon": { "type": "object", @@ -745,11 +675,7 @@ "title": "Banner Text", "description": "Add a quick promotion with a text/action pair", "type": "object", - "required": [ - "title", - "caption", - "link" - ], + "required": ["title", "caption", "link"], "properties": { "title": { "title": "Title", @@ -762,10 +688,7 @@ "link": { "title": "Call to Action", "type": "object", - "required": [ - "text", - "url" - ], + "required": ["text", "url"], "properties": { "text": { "title": "Text", @@ -785,28 +708,14 @@ "colorVariant": { "type": "string", "title": "Color variant", - "enumNames": [ - "Main", - "Light", - "Accent" - ], - "enum": [ - "main", - "light", - "accent" - ] + "enumNames": ["Main", "Light", "Accent"], + "enum": ["main", "light", "accent"] }, "variant": { "type": "string", "title": "Variant", - "enumNames": [ - "Primary", - "Secondary" - ], - "enum": [ - "primary", - "secondary" - ] + "enumNames": ["Primary", "Secondary"], + "enum": ["primary", "secondary"] } } } @@ -818,9 +727,7 @@ "title": "Hero", "description": "Add a quick promotion with an image/action pair", "type": "object", - "required": [ - "title" - ], + "required": ["title"], "properties": { "title": { "title": "Title", @@ -869,28 +776,14 @@ "colorVariant": { "type": "string", "title": "Color variant", - "enumNames": [ - "Main", - "Light", - "Accent" - ], - "enum": [ - "main", - "light", - "accent" - ] + "enumNames": ["Main", "Light", "Accent"], + "enum": ["main", "light", "accent"] }, "variant": { "type": "string", "title": "Variant", - "enumNames": [ - "Primary", - "Secondary" - ], - "enum": [ - "primary", - "secondary" - ] + "enumNames": ["Primary", "Secondary"], + "enum": ["primary", "secondary"] } } } @@ -911,11 +804,7 @@ "items": { "title": "Incentive", "type": "object", - "required": [ - "title", - "firstLineText", - "icon" - ], + "required": ["title", "firstLineText", "icon"], "properties": { "title": { "type": "string", @@ -964,12 +853,7 @@ "title": "Product Shelf", "description": "Add custom shelves to your store", "type": "object", - "required": [ - "title", - "numberOfItems", - "after", - "sort" - ], + "required": ["title", "numberOfItems", "after", "sort"], "properties": { "title": { "type": "string", @@ -1028,10 +912,7 @@ "items": { "title": "Facet", "type": "object", - "required": [ - "key", - "value" - ], + "required": ["key", "value"], "properties": { "key": { "title": "Key", @@ -1085,19 +966,12 @@ }, { "name": "CrossSellingShelf", - "requiredScopes": [ - "pdp", - "custom" - ], + "requiredScopes": ["pdp", "custom"], "schema": { "title": "Cross Selling Shelf", "description": "Add cross selling product data to your users", "type": "object", - "required": [ - "title", - "numberOfItems", - "kind" - ], + "required": ["title", "numberOfItems", "kind"], "properties": { "title": { "title": "Title", @@ -1119,14 +993,8 @@ "title": "Kind", "description": "Change cross selling types", "default": "buy", - "enum": [ - "buy", - "view" - ], - "enumNames": [ - "Who bought also bought", - "Who saw also saw" - ] + "enum": ["buy", "view"], + "enumNames": ["Who bought also bought", "Who saw also saw"] }, "taxesConfiguration": { "title": "Taxes Configuration", @@ -1154,12 +1022,7 @@ "title": "Product Tiles", "description": "Add custom highlights to your store", "type": "object", - "required": [ - "title", - "first", - "after", - "sort" - ], + "required": ["title", "first", "after", "sort"], "properties": { "title": { "title": "Title", @@ -1212,10 +1075,7 @@ "items": { "title": "Facet", "type": "object", - "required": [ - "key", - "value" - ], + "required": ["key", "value"], "properties": { "key": { "title": "Key", @@ -1258,9 +1118,7 @@ "title": "Newsletter", "description": "Allow users to subscribe to your updates", "type": "object", - "required": [ - "title" - ], + "required": ["title"], "properties": { "icon": { "title": "Icon", @@ -1269,12 +1127,8 @@ "icon": { "title": "Icon", "type": "string", - "enumNames": [ - "Envelope" - ], - "enum": [ - "Envelope" - ], + "enumNames": ["Envelope"], + "enum": ["Envelope"], "default": "Envelope" }, "alt": { @@ -1334,16 +1188,8 @@ "colorVariant": { "title": "Color variant", "type": "string", - "enumNames": [ - "Main", - "Light", - "Accent" - ], - "enum": [ - "main", - "light", - "accent" - ], + "enumNames": ["Main", "Light", "Accent"], + "enum": ["main", "light", "accent"], "default": "main" }, "toastSubscribe": { @@ -1365,12 +1211,8 @@ "icon": { "title": "Icon", "type": "string", - "enumNames": [ - "CircleWavyCheck" - ], - "enum": [ - "CircleWavyCheck" - ], + "enumNames": ["CircleWavyCheck"], + "enum": ["CircleWavyCheck"], "default": "CircleWavyCheck" } } @@ -1394,12 +1236,8 @@ "icon": { "title": "Icon", "type": "string", - "enumNames": [ - "CircleWavyWarning" - ], - "enum": [ - "CircleWavyWarning" - ], + "enumNames": ["CircleWavyWarning"], + "enum": ["CircleWavyWarning"], "default": "CircleWavyWarning" } } @@ -1422,18 +1260,12 @@ "title": "Banner Newsletter", "description": "Add newsletter with a banner", "type": "object", - "required": [ - "banner", - "newsletter" - ], + "required": ["banner", "newsletter"], "properties": { "banner": { "title": "Banner", "type": "object", - "required": [ - "title", - "link" - ], + "required": ["title", "link"], "properties": { "title": { "title": "Title", @@ -1448,10 +1280,7 @@ "link": { "title": "Call to Action", "type": "object", - "required": [ - "text", - "url" - ], + "required": ["text", "url"], "properties": { "text": { "title": "Text", @@ -1468,29 +1297,15 @@ "colorVariant": { "title": "Color variant", "type": "string", - "enumNames": [ - "Main", - "Light", - "Accent" - ], - "enum": [ - "main", - "light", - "accent" - ], + "enumNames": ["Main", "Light", "Accent"], + "enum": ["main", "light", "accent"], "default": "light" }, "variant": { "title": "Variant", "type": "string", - "enumNames": [ - "Primary", - "Secondary" - ], - "enum": [ - "primary", - "secondary" - ], + "enumNames": ["Primary", "Secondary"], + "enum": ["primary", "secondary"], "default": "secondary" } } @@ -1498,10 +1313,7 @@ "newsletter": { "title": "Newsletter", "type": "object", - "required": [ - "title", - "description" - ], + "required": ["title", "description"], "properties": { "icon": { "title": "Icon", @@ -1510,12 +1322,8 @@ "icon": { "title": "Icon", "type": "string", - "enumNames": [ - "Envelope" - ], - "enum": [ - "Envelope" - ], + "enumNames": ["Envelope"], + "enum": ["Envelope"], "default": "Envelope" }, "alt": { @@ -1570,16 +1378,8 @@ "colorVariant": { "title": "Color variant", "type": "string", - "enumNames": [ - "Main", - "Light", - "Accent" - ], - "enum": [ - "main", - "light", - "accent" - ], + "enumNames": ["Main", "Light", "Accent"], + "enum": ["main", "light", "accent"], "default": "main" }, "toastSubscribe": { @@ -1601,12 +1401,8 @@ "icon": { "title": "Icon", "type": "string", - "enumNames": [ - "CircleWavyCheck" - ], - "enum": [ - "CircleWavyCheck" - ], + "enumNames": ["CircleWavyCheck"], + "enum": ["CircleWavyCheck"], "default": "CircleWavyCheck" } } @@ -1630,12 +1426,8 @@ "icon": { "title": "Icon", "type": "string", - "enumNames": [ - "CircleWavyWarning" - ], - "enum": [ - "CircleWavyWarning" - ], + "enumNames": ["CircleWavyWarning"], + "enum": ["CircleWavyWarning"], "default": "CircleWavyWarning" } } @@ -1647,28 +1439,18 @@ }, { "name": "Breadcrumb", - "requiredScopes": [ - "pdp", - "plp" - ], + "requiredScopes": ["pdp", "plp"], "schema": { "title": "Breadcrumb", "description": "Configure the breadcrumb icon and depth", "type": "object", - "required": [ - "icon", - "alt" - ], + "required": ["icon", "alt"], "properties": { "icon": { "title": "Icon", "type": "string", - "enumNames": [ - "House" - ], - "enum": [ - "House" - ] + "enumNames": ["House"], + "enum": ["House"] }, "alt": { "title": "Alternative Label", @@ -1679,9 +1461,7 @@ }, { "name": "ProductDetails", - "requiredScopes": [ - "pdp" - ], + "requiredScopes": ["pdp"], "schema": { "title": "Product Details", "type": "object", @@ -1703,14 +1483,8 @@ "size": { "title": "Size", "type": "string", - "enumNames": [ - "Big", - "Small" - ], - "enum": [ - "big", - "small" - ] + "enumNames": ["Big", "Small"], + "enum": ["big", "small"] } } }, @@ -1737,12 +1511,8 @@ "icon": { "title": "Icon", "type": "string", - "enumNames": [ - "Shopping Cart" - ], - "enum": [ - "ShoppingCart" - ] + "enumNames": ["Shopping Cart"], + "enum": ["ShoppingCart"] }, "alt": { "type": "string", @@ -1807,16 +1577,8 @@ "initiallyExpanded": { "type": "string", "title": "Initially Expanded?", - "enumNames": [ - "First", - "All", - "None" - ], - "enum": [ - "first", - "all", - "none" - ] + "enumNames": ["First", "All", "None"], + "enum": ["first", "all", "none"] }, "displayDescription": { "title": "Should display description?", @@ -1856,23 +1618,102 @@ "default": "Tax included" } } + }, + "skuMatrix": { + "title": "SKUMatrix Configuration", + "type": "object", + "properties": { + "shouldDisplaySKUMatrix": { + "title": "Should display SKUMatrix?", + "type": "boolean", + "default": false + }, + "triggerButtonLabel": { + "title": "SKU Matrix Trigger label to be displayed", + "type": "string", + "default": "Select multiple" + }, + "separatorButtonsText": { + "title": "Separator text", + "description": "Text that separates the add to cart button from the SKU Matrix Trigger button.", + "type": "string", + "default": "Or" + }, + "columns": { + "title": "Columns", + "type": "object", + "properties": { + "name": { + "title": "SKU name column label", + "type": "string", + "default": "Name" + }, + "additionalColumns": { + "title": "Additional columns", + "type": "array", + "items": { + "title": "Column", + "type": "object", + "required": ["label", "value"], + "properties": { + "label": { + "title": "Label", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + } + } + } + }, + "availability": { + "title": "Availability column label", + "type": "object", + "properties": { + "label": { + "title": "Label", + "type": "string", + "default": "Availability" + }, + "stockDisplaySettings": { + "title": "Stock display settings", + "description": "Control how the stock status of your products is displayed to customers on your online store.", + "type": "string", + "enum": ["showAvailability", "showStockQuantity"], + "enumNames": [ + "Show availability (Available/Out of Stock)", + "Show stock quantity" + ], + "default": "showAvailability" + } + } + }, + "price": { + "title": "Price column label", + "type": "string", + "default": "Price" + }, + "quantitySelector": { + "title": "Quantity selector column label", + "type": "string", + "default": "Quantity" + } + } + } + } } } } }, { "name": "ProductGallery", - "requiredScopes": [ - "plp", - "search" - ], + "requiredScopes": ["plp", "search"], "schema": { "title": "Product Gallery", "type": "object", "description": "Product Gallery configuration", - "required": [ - "filter" - ], + "required": ["filter"], "properties": { "searchTermLabel": { "title": "Search page term label", @@ -1887,10 +1728,7 @@ "previousPageButton": { "title": "Previous page button", "type": "object", - "required": [ - "icon", - "label" - ], + "required": ["icon", "label"], "properties": { "icon": { "title": "Icon", @@ -1899,12 +1737,8 @@ "icon": { "title": "Icon", "type": "string", - "enumNames": [ - "ArrowLeft" - ], - "enum": [ - "ArrowLeft" - ], + "enumNames": ["ArrowLeft"], + "enum": ["ArrowLeft"], "default": "ArrowLeft" }, "alt": { @@ -1924,9 +1758,7 @@ "loadMorePageButton": { "title": "Load more products Button", "type": "object", - "required": [ - "label" - ], + "required": ["label"], "properties": { "label": { "title": "Load more products label", @@ -1938,10 +1770,7 @@ "filter": { "title": "Filter", "type": "object", - "required": [ - "title", - "mobileOnly" - ], + "required": ["title", "mobileOnly"], "properties": { "title": { "title": "Filter title", @@ -1960,10 +1789,7 @@ "filterButton": { "title": "Show filter button", "type": "object", - "required": [ - "label", - "icon" - ], + "required": ["label", "icon"], "properties": { "label": { "title": "Label", @@ -1973,20 +1799,13 @@ "icon": { "title": "Icon", "type": "object", - "required": [ - "icon", - "alt" - ], + "required": ["icon", "alt"], "properties": { "icon": { "title": "Icon", "type": "string", - "enumNames": [ - "FadersHorizontal" - ], - "enum": [ - "FadersHorizontal" - ], + "enumNames": ["FadersHorizontal"], + "enum": ["FadersHorizontal"], "default": "FadersHorizontal" }, "alt": { @@ -2125,10 +1944,7 @@ "icon": { "title": "Icon", "type": "object", - "required": [ - "icon", - "alt" - ], + "required": ["icon", "alt"], "properties": { "icon": { "title": "Icon", @@ -2168,11 +1984,7 @@ "checkoutButton": { "title": "Checkout button", "type": "object", - "required": [ - "label", - "loadingLabel", - "icon" - ], + "required": ["label", "loadingLabel", "icon"], "properties": { "label": { "title": "Label", @@ -2187,20 +1999,13 @@ "icon": { "title": "Icon", "type": "object", - "required": [ - "icon", - "alt" - ], + "required": ["icon", "alt"], "properties": { "icon": { "title": "Icon", "type": "string", - "enumNames": [ - "ArrowRight" - ], - "enum": [ - "ArrowRight" - ], + "enumNames": ["ArrowRight"], + "enum": ["ArrowRight"], "default": "ArrowRight" }, "alt": { @@ -2249,9 +2054,7 @@ "title": "Region Bar", "type": "object", "description": "Region Bar configuration", - "required": [ - "label" - ], + "required": ["label"], "properties": { "icon": { "title": "Location Icon", @@ -2260,12 +2063,8 @@ "icon": { "title": "Icon", "type": "string", - "enumNames": [ - "Map Pin" - ], - "enum": [ - "MapPin" - ], + "enumNames": ["Map Pin"], + "enum": ["MapPin"], "default": "MapPin" }, "alt": { @@ -2292,12 +2091,8 @@ "icon": { "title": "Icon", "type": "string", - "enumNames": [ - "Caret Right" - ], - "enum": [ - "CaretRight" - ], + "enumNames": ["Caret Right"], + "enum": ["CaretRight"], "default": "CaretRight" }, "alt": { @@ -2369,12 +2164,8 @@ "icon": { "title": "Icon", "type": "string", - "enumNames": [ - "Arrow Square Out" - ], - "enum": [ - "ArrowSquareOut" - ], + "enumNames": ["Arrow Square Out"], + "enum": ["ArrowSquareOut"], "default": "ArrowSquareOut" }, "alt": { @@ -2407,12 +2198,8 @@ "icon": { "title": "Icon", "type": "string", - "enumNames": [ - "CircleWavy Warning" - ], - "enum": [ - "CircleWavyWarning" - ] + "enumNames": ["CircleWavy Warning"], + "enum": ["CircleWavyWarning"] }, "alt": { "title": "Alternative Label", @@ -2466,4 +2253,4 @@ } } } -] \ No newline at end of file +] diff --git a/packages/core/src/components/sections/ProductDetails/DefaultComponents.ts b/packages/core/src/components/sections/ProductDetails/DefaultComponents.ts index 07b63e4f3e..7854fab8e2 100644 --- a/packages/core/src/components/sections/ProductDetails/DefaultComponents.ts +++ b/packages/core/src/components/sections/ProductDetails/DefaultComponents.ts @@ -9,13 +9,17 @@ import { QuantitySelector as UIQuantitySelector, ImageGalleryViewer as UIImageGalleryViewer, ImageGallery as UIImageGallery, + SKUMatrix as UISKUMatrix, + SKUMatrixSidebar as UISKUMatrixSidebar, + SKUMatrixTrigger as UISKUMatrixTrigger, } from '@faststore/ui' import LocalImageGallery from 'src/components/ui/ImageGallery' import LocalShippingSimulation from 'src/components/ui/ShippingSimulation/ShippingSimulation' import { Image } from 'src/components/ui/Image' import LocalNotAvailableButton from 'src/components/product/NotAvailableButton' -import LocalProductDescription from 'src/components/ui/ProductDescription' +import LocalSKUMatrixSidebar from 'src/components/ui/SKUMatrix/SKUMatrixSidebar' +import LocalProductDescription from 'src/components/ui/ProductDescription/ProductDescription' import { ProductDetailsSettings as LocalProductDetailsSettings } from 'src/components/ui/ProductDetails' export const ProductDetailsDefaultComponents = { @@ -29,9 +33,13 @@ export const ProductDetailsDefaultComponents = { ShippingSimulation: UIShippingSimulation, ImageGallery: UIImageGallery, ImageGalleryViewer: UIImageGalleryViewer, + SKUMatrix: UISKUMatrix, + SKUMatrixTrigger: UISKUMatrixTrigger, + SKUMatrixSidebar: UISKUMatrixSidebar, __experimentalImageGalleryImage: Image, __experimentalImageGallery: LocalImageGallery, __experimentalShippingSimulation: LocalShippingSimulation, + __experimentalSKUMatrixSidebar: LocalSKUMatrixSidebar, __experimentalNotAvailableButton: LocalNotAvailableButton, __experimentalProductDescription: LocalProductDescription, __experimentalProductDetailsSettings: LocalProductDetailsSettings, diff --git a/packages/core/src/components/sections/ProductDetails/ProductDetails.tsx b/packages/core/src/components/sections/ProductDetails/ProductDetails.tsx index cd77ef01ff..29df9bf941 100644 --- a/packages/core/src/components/sections/ProductDetails/ProductDetails.tsx +++ b/packages/core/src/components/sections/ProductDetails/ProductDetails.tsx @@ -55,6 +55,21 @@ export interface ProductDetailsProps { usePriceWithTaxes?: boolean taxesLabel?: string } + skuMatrix?: { + shouldDisplaySKUMatrix?: boolean + triggerButtonLabel: string + separatorButtonsText: string + columns: { + name: string + additionalColumns: Array<{ label: string; value: string }> + availability: { + label: string + stockDisplaySettings: 'showAvailability' | 'showStockQuantity' + } + price: number + quantitySelector: number + } + } } function ProductDetails({ @@ -74,6 +89,7 @@ function ProductDetails({ initiallyExpanded: productDescriptionInitiallyExpanded, displayDescription: shouldDisplayProductDescription, }, + skuMatrix, notAvailableButton: { title: notAvailableButtonTitle }, quantitySelector, taxesConfiguration, @@ -81,17 +97,19 @@ function ProductDetails({ const { DiscountBadge, ProductTitle, + SKUMatrix, + SKUMatrixTrigger, __experimentalImageGallery: ImageGallery, __experimentalShippingSimulation: ShippingSimulation, __experimentalNotAvailableButton: NotAvailableButton, __experimentalProductDescription: ProductDescription, __experimentalProductDetailsSettings: ProductDetailsSettings, + __experimentalSKUMatrixSidebar: SKUMatrixSidebar, } = useOverrideComponents<'ProductDetails'>() const { currency } = useSession() const context = usePDP() const { product, isValidating } = context?.data const [quantity, setQuantity] = useState(1) - if (!product) { throw new Error('NotFound') } @@ -104,7 +122,11 @@ function ProductDetails({ brand, isVariantOf, description, - isVariantOf: { name, productGroupID: productId }, + isVariantOf: { + name, + productGroupID: productId, + skuVariants: { slugsMap }, + }, image: productImages, offers: { offers: [{ availability, price, listPrice, listPriceWithTaxes, seller }], @@ -213,6 +235,27 @@ function ProductDetails({ isValidating={isValidating} taxesConfiguration={taxesConfiguration} /> + + {skuMatrix?.shouldDisplaySKUMatrix && + Object.keys(slugsMap).length > 1 && ( + <> +
+ {skuMatrix.separatorButtonsText} +
+ + + + {skuMatrix.triggerButtonLabel} + + + + + + )} {!outOfStock && ( @@ -277,7 +320,7 @@ export const fragment = gql(` isVariantOf { name productGroupID - skuVariants { + skuVariants { activeVariations slugsMap availableVariations diff --git a/packages/core/src/components/sections/ProductDetails/section.module.scss b/packages/core/src/components/sections/ProductDetails/section.module.scss index dbf85d3937..a74aa3886a 100644 --- a/packages/core/src/components/sections/ProductDetails/section.module.scss +++ b/packages/core/src/components/sections/ProductDetails/section.module.scss @@ -1,9 +1,13 @@ @layer components { .section { // Taxes label - --fs-product-details-taxes-label-color : var(--fs-color-info-text); - --fs-product-details-taxes-text-size : var(--fs-text-size-tiny); - --fs-product-details-taxes-text-weight : var(--fs-text-weight-regular); + --fs-product-details-taxes-label-color : var(--fs-color-info-text); + --fs-product-details-taxes-text-size : var(--fs-text-size-tiny); + --fs-product-details-taxes-text-weight : var(--fs-text-weight-regular); + + // Separator colors + --fs-product-details-separator-color : var(--fs-color-neutral-2); + --fs-product-details-separator-color-text : var(--fs-color-text-light); margin-top: 0; @@ -29,11 +33,43 @@ @import "@faststore/ui/src/components/organisms/ShippingSimulation/styles.scss"; @import "@faststore/ui/src/components/organisms/ImageGallery/styles.scss"; @import "@faststore/ui/src/components/organisms/ProductDetails/styles.scss"; + @import "@faststore/ui/src/components/organisms/SlideOver/styles.scss"; + @import "@faststore/ui/src/components/organisms/SKUMatrix/styles.scss"; [data-fs-product-details-taxes-label] { font-size: var(--fs-product-details-taxes-text-size); font-weight: var(--fs-product-details-taxes-text-weight); color: var(--fs-product-details-taxes-label-color); } + + [data-fs-product-details-settings-separator] { + position: relative; + display: flex; + align-items: center; + justify-content: space-between; + color: var(--fs-product-details-separator-color-text); + + &::after { + display: inline-block; + width: 45%; + height: 1px; + content: ""; + background-color: var(--fs-product-details-separator-color); + } + + &::before { + display: inline-block; + width: 45%; + height: 1px; + content: ""; + background-color: var(--fs-product-details-separator-color); + } + } + + [data-fs-sku-matrix] { + > [data-fs-button] { + width: 100%; + } + } } } diff --git a/packages/core/src/components/ui/SKUMatrix/SKUMatrixSidebar.tsx b/packages/core/src/components/ui/SKUMatrix/SKUMatrixSidebar.tsx new file mode 100644 index 0000000000..ff641090cd --- /dev/null +++ b/packages/core/src/components/ui/SKUMatrix/SKUMatrixSidebar.tsx @@ -0,0 +1,132 @@ +import type { SKUMatrixSidebarProps as UISKUMatrixSidebarProps } from '@faststore/ui' +import { + SKUMatrixSidebar as UISKUMatrixSidebar, + useSKUMatrix, +} from '@faststore/ui' +import { gql } from '@generated/gql' +import { useBuyButton } from 'src/sdk/cart/useBuyButton' +import { usePDP } from 'src/sdk/overrides/PageProvider' +import { useAllVariantProducts } from 'src/sdk/product/useAllVariantProducts' + +interface SKUMatrixProps extends UISKUMatrixSidebarProps {} + +function SKUMatrixSidebar(props: SKUMatrixProps) { + const { + data: { product }, + } = usePDP() + + const { allVariantProducts, isOpen, setAllVariantProducts } = useSKUMatrix() + const { isValidating } = useAllVariantProducts( + product.id, + isOpen, + setAllVariantProducts + ) + + const { + gtin, + unitMultiplier, + brand, + additionalProperty, + isVariantOf, + offers: { + offers: [{ seller }], + }, + } = product + + const buyButtonProps = allVariantProducts + .filter((item) => item.selectedCount) + .map((item) => { + const { + offers: { + offers: [{ price, priceWithTaxes, listPrice, listPriceWithTaxes }], + }, + } = item + + return { + id: item.id, + price, + priceWithTaxes, + listPrice, + listPriceWithTaxes, + seller, + quantity: item.selectedCount, + itemOffered: { + sku: item.id, + name: item.name, + gtin, + image: [item.image], + brand, + isVariantOf: { + ...isVariantOf, + skuVariants: { + ...isVariantOf.skuVariants, + activeVariations: item.specifications, + }, + }, + additionalProperty, + unitMultiplier, + }, + } + }) + + const buyProps = useBuyButton(buyButtonProps) + + return ( + + ) +} + +export const fragment = gql(` + fragment ProductSKUMatrixSidebarFragment_product on StoreProduct { + id: productID + isVariantOf { + name + productGroupID + skuVariants { + activeVariations + slugsMap + availableVariations + allVariantProducts { + sku + name + image { + url + alternateName + } + offers { + highPrice + lowPrice + lowPriceWithTaxes + offerCount + priceCurrency + offers { + listPrice + listPriceWithTaxes + sellingPrice + priceCurrency + price + priceWithTaxes + priceValidUntil + itemCondition + availability + quantity + } + } + additionalProperty { + propertyID + value + name + valueReference + } + } + } + } + } +`) + +export default SKUMatrixSidebar diff --git a/packages/core/src/sdk/cart/useBuyButton.ts b/packages/core/src/sdk/cart/useBuyButton.ts index 0964ae84e9..b68122531f 100644 --- a/packages/core/src/sdk/cart/useBuyButton.ts +++ b/packages/core/src/sdk/cart/useBuyButton.ts @@ -8,12 +8,14 @@ import { useUI } from '@faststore/ui' import { useSession } from '../session' import { cartStore } from './index' -export const useBuyButton = (item: CartItem | null) => { +export const useBuyButton = (item: CartItem | CartItem[] | null) => { const { openCart } = useUI() const { currency: { code }, } = useSession() + const itemIsArray = Array.isArray(item) + const onClick = useCallback( (e: React.MouseEvent) => { e.preventDefault() @@ -22,6 +24,33 @@ export const useBuyButton = (item: CartItem | null) => { return } + const value = itemIsArray + ? item.reduce((sum, item) => (sum += item.price * item.quantity), 0) + : item.price * item.quantity + + function generatedItem(item: CartItem) { + return { + item_id: item.itemOffered.isVariantOf.productGroupID, + item_name: item.itemOffered.isVariantOf.name, + item_brand: item.itemOffered.brand.name, + item_variant: item.itemOffered.sku, + quantity: item.quantity, + price: item.price, + discount: item.listPrice - item.price, + currency: code as CurrencyCode, + item_variant_name: item.itemOffered.name, + product_reference_id: item.itemOffered.gtin, + } + } + + function getItems() { + if (!itemIsArray) { + return [generatedItem(item)] + } + + return item.map(generatedItem) + } + import('@faststore/sdk').then(({ sendAnalyticsEvent }) => { sendAnalyticsEvent>({ name: 'add_to_cart', @@ -29,35 +58,27 @@ export const useBuyButton = (item: CartItem | null) => { currency: code as CurrencyCode, // TODO: In the future, we can explore more robust ways of // calculating the value (gift items, discounts, etc.). - value: item.price * item.quantity, - items: [ - { - item_id: item.itemOffered.isVariantOf.productGroupID, - item_name: item.itemOffered.isVariantOf.name, - item_brand: item.itemOffered.brand.name, - item_variant: item.itemOffered.sku, - quantity: item.quantity, - price: item.price, - discount: item.listPrice - item.price, - currency: code as CurrencyCode, - item_variant_name: item.itemOffered.name, - product_reference_id: item.itemOffered.gtin, - }, - ], + value, + items: getItems(), }, }) }) - cartStore.addItem(item) + itemIsArray + ? item.forEach((value) => cartStore.addItem(value)) + : cartStore.addItem(item) + openCart() }, - [code, item, openCart] + [code, item, openCart, itemIsArray] ) return { onClick, 'data-testid': 'buy-button', - 'data-sku': item?.itemOffered.sku, - 'data-seller': item?.seller.identifier, + 'data-sku': itemIsArray ? 'sku-matrix-sidebar' : item?.itemOffered.sku, + 'data-seller': itemIsArray + ? item[0]?.seller.identifier + : item?.seller.identifier, } } diff --git a/packages/core/src/sdk/product/useAllVariantProducts.ts b/packages/core/src/sdk/product/useAllVariantProducts.ts new file mode 100644 index 0000000000..bb30fe3e2c --- /dev/null +++ b/packages/core/src/sdk/product/useAllVariantProducts.ts @@ -0,0 +1,114 @@ +import { useMemo } from 'react' + +import { gql } from '@generated' +import type { + ClientAllVariantProductsQueryQuery, + ClientProductQueryQueryVariables, +} from '@generated/graphql' + +import { useQuery } from '../graphql/useQuery' +import { useSession } from '../session' + +const query = gql(` + query ClientAllVariantProductsQuery($locator: [IStoreSelectedFacet!]!) { + product(locator: $locator) { + ...ProductSKUMatrixSidebarFragment_product + } + } +`) + +type FormattedVariantProduct = { + id: string + name: string + image: { + url: string + alternateName: string + } + inventory: number + selectedCount: number + availability: string + offers: ClientAllVariantProductsQueryQuery['product']['isVariantOf']['skuVariants']['allVariantProducts'][0]['offers'] + price: number + listPrice: number + priceWithTaxes: number + listPriceWithTaxes: number + specifications: Record +} + +export const useAllVariantProducts = < + T extends ClientAllVariantProductsQueryQuery +>( + productID: string, + enabled: boolean, + processResponse: (data: FormattedVariantProduct[]) => void, + fallbackData?: T +) => { + const { channel, locale } = useSession() + const variables = useMemo(() => { + if (!channel) { + throw new Error( + `useAllVariantProducts: 'channel' from session is an empty string.` + ) + } + + return { + locator: [ + { key: 'id', value: productID }, + { key: 'channel', value: channel }, + { key: 'locale', value: locale }, + ], + } + }, [channel, locale, productID]) + + return useQuery( + query, + variables, + { + fallbackData, + revalidateOnMount: true, + doNotRun: !enabled, + onSuccess: (data: ClientAllVariantProductsQueryQuery) => { + const formattedData = + data.product.isVariantOf.skuVariants.allVariantProducts.map( + (item) => { + const specifications = item.additionalProperty.reduce<{ + [key: string]: any + }>( + (acc, prop) => ({ + ...acc, + [prop.name.toLowerCase()]: prop.value, + }), + {} + ) + + const outOfStock = + item.offers.offers[0].availability === + 'https://schema.org/OutOfStock' + + return { + id: item.sku, + name: item.name, + image: { + url: item.image[0].url, + alternateName: item.image[0].alternateName, + }, + inventory: item.offers.offers[0].quantity, + availability: outOfStock ? 'outOfStock' : 'available', + price: item.offers.offers[0].price, + listPrice: item.offers.offers[0].listPrice, + priceWithTaxes: item.offers.offers[0].priceWithTaxes, + listPriceWithTaxes: item.offers.offers[0].listPriceWithTaxes, + specifications, + offers: item.offers, + selectedCount: 0, + } + } + ) + + processResponse( + formattedData.sort((a, b) => a.name.localeCompare(b.name)) + ) + }, + } + ) +} diff --git a/packages/core/src/typings/overrides.ts b/packages/core/src/typings/overrides.ts index d6fc9e0481..df02eb198b 100644 --- a/packages/core/src/typings/overrides.ts +++ b/packages/core/src/typings/overrides.ts @@ -39,6 +39,9 @@ import type { ShippingSimulationProps, SkeletonProps, SkuSelectorProps, + SKUMatrixProps, + SKUMatrixTriggerProps, + SKUMatrixSidebarProps, } from '@faststore/ui' import type { @@ -81,10 +84,10 @@ export type SectionOverride = { export type OverrideComponentsForSection< Section extends SectionsOverrides[keyof SectionsOverrides]['Section'] > = { - // The first 'extends' condition is used to filter out sections that don't have overrides (typed 'never') - [K in keyof SectionsOverrides as SectionsOverrides[K] extends { - Section: never - } +// The first 'extends' condition is used to filter out sections that don't have overrides (typed 'never') +[K in keyof SectionsOverrides as SectionsOverrides[K] extends { + Section: never +} ? never : // In the second 'extends' condition, we check if the section matches the one we're looking for SectionsOverrides[K] extends { @@ -269,12 +272,25 @@ export type SectionsOverrides = { ImageGalleryViewerProps, ImageGalleryViewerProps > + SKUMatrix: ComponentOverrideDefinition + SKUMatrixTrigger: ComponentOverrideDefinition< + SKUMatrixTriggerProps, + SKUMatrixTriggerProps + > + SKUMatrixSidebar: ComponentOverrideDefinition< + SKUMatrixSidebarProps, + SKUMatrixSidebarProps + > __experimentalImageGalleryImage: ComponentOverrideDefinition __experimentalImageGallery: ComponentOverrideDefinition __experimentalShippingSimulation: ComponentOverrideDefinition __experimentalNotAvailableButton: ComponentOverrideDefinition __experimentalProductDescription: ComponentOverrideDefinition - __experimentalProductDetailsSettings: ComponentOverrideDefinition + __experimentalSKUMatrixSidebar: ComponentOverrideDefinition + __experimentalProductDetailsSettings: ComponentOverrideDefinition< + any, + any + > } } ProductGallery: { diff --git a/packages/ui/src/components/organisms/SKUMatrix/styles.scss b/packages/ui/src/components/organisms/SKUMatrix/styles.scss new file mode 100644 index 0000000000..e0bc712f48 --- /dev/null +++ b/packages/ui/src/components/organisms/SKUMatrix/styles.scss @@ -0,0 +1,163 @@ +[data-fs-sku-matrix-sidebar] { + // -------------------------------------------------------- + // Design Tokens for SKU Matrix Sidebar + // -------------------------------------------------------- + + // Default properties + + // Background + --fs-sku-matrix-sidebar-bkg-color : var(--fs-color-body-bkg); + + // Title + --fs-sku-matrix-sidebar-title-size : var(--fs-text-size-6); + --fs-sku-matrix-sidebar-title-text-weight : var(--fs-text-weight-semibold); + + // Cell + --fs-sku-matrix-sidebar-table-cell-font-size : var(--fs-text-size-tiny); + --fs-sku-matrix-sidebar-table-cell-text-weight : var(--fs-text-weight-medium); + + + // Partial + --fs-sku-matrix-slide-over-partial-gap : calc(2 * var(--fs-grid-padding)); + --fs-sku-matrix-slide-over-partial-width-mobile : calc(100vw - var(--fs-sku-matrix-slide-over-partial-gap)); + + // -------------------------------------------------------- + // Structural Styles + // -------------------------------------------------------- + + display: flex; + flex-direction: column; + height: 100vh; + overflow: auto; + + [data-fs-table] { + flex-shrink: 0; + padding: var(--fs-spacing-3) var(--fs-spacing-8); + padding-bottom: 0; + + @include media("=notebook") { + max-width: var(--fs-sku-matrix-slide-over-partial-width-mobile); + } + } + } + + [data-fs-sku-matrix-sidebar-title] { + font-size: var(--fs-sku-matrix-sidebar-title-size); + font-weight: var(--fs-sku-matrix-sidebar-title-text-weight); + line-height: 1.12; + } + + [data-fs-table] { + color: var(--fs-color-neutral-6); + + [data-fs-table-cell] { + &:first-child { + padding-left: 0; + } + + &:last-child { + padding-right: 0; + } + } + } + + [data-fs-table-head] { + [data-fs-table-cell] { + font-size: var(--fs-sku-matrix-sidebar-table-cell-font-size); + font-weight: var(--fs-sku-matrix-sidebar-table-cell-text-weight); + } + } + + [data-fs-table-cell], + [data-fs-price-variant="spot"] { + font-weight: var(--fs-text-weight-medium); + } + + [data-fs-table-cell]:first-child, + [data-fs-sku-matrix-sidebar-table-price] { + color: var(--fs-color-text); + } + + [data-fs-sku-matrix-sidebar-table-price] { + display: flex; + align-items: center; + justify-content: flex-end; + } + + [data-fs-sku-matrix-sidebar-table-cell-quantity-selector] { + width: 1%; + } + + [data-fs-sku-matrix-sidebar-cell-image] { + display: flex; + align-items: center; + gap: var(--fs-spacing-2); + + > div { + img { + object-fit: cover; + object-position: center; + } + } + } + + [data-fs-quantity-selector] { + [data-fs-icon] { + margin: 0; + } + } + + [data-fs-sku-matrix-sidebar-footer] { + display: flex; + justify-content: space-between; + + position: sticky; + bottom: 0; + left: 0; + right: 0; + margin-top: auto; + + background-color: var(--fs-sku-matrix-sidebar-bkg-color); + + padding: var(--fs-spacing-4) var(--fs-spacing-8); + border-top: var(--fs-border-width) solid var(--fs-border-color-light); + width: 100%; + + > div { + display: flex; + gap: var(--fs-spacing-3); + align-items: center; + + > p { + font-weight: var(--fs-text-weight-semibold); + color: var(--fs-color-neutral-5); + } + } + + @include media("