@godaddy/gd-commerce-storefront 0.9.0 → 0.11.0

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/README.md CHANGED
@@ -57,6 +57,7 @@ Use the providers your application already has; do not create another router or
57
57
  | `onItemAdded` | unset | Called with `{ name, quantity, isSample }` after each add, live or sample; use it to confirm adds on a site with a cart page instead of a drawer |
58
58
  | `onEvent` | unset | Called with a [`CommerceEvent`](#events) for every cart change and failure, such as for analytics |
59
59
  | `locale` | `en-US` | BCP 47 locale for formatting prices, such as `de-DE`; an invalid tag falls back to `en-US` with a development warning. It never selects UI text |
60
+ | `messages` | English | The package's text, such as `frFR`. Keys left out stay English. See [translations](#translations) |
60
61
  | `theme` | inherited text and transparent wrappers; neutral control palette | CSS custom properties applied to storefront surfaces and the portalled drawer |
61
62
  | `getSwatch` | unset | Swatches for attribute values, such as colors. Returns a CSS `background` for a value, or nothing. See [swatches](#swatches) |
62
63
 
@@ -81,6 +82,7 @@ A connection failure leaves the surrounding application and its state mounted. C
81
82
  '--commerce-radius': '0.75rem',
82
83
  '--commerce-sale': '#b42318',
83
84
  '--commerce-on-sale': '#ffffff',
85
+ '--commerce-card-image-aspect': '1 / 1',
84
86
  }}
85
87
  >
86
88
  {/* Register /collection and /item/:productId in your router. */}
@@ -92,7 +94,7 @@ The stylesheet includes all required utilities and scopes them to the package's
92
94
 
93
95
  By default, `.commerce-storefront` wrappers do not paint a background and inherit the host's text color. Set page and section backgrounds on the host application's layout elements. Muted text, hairlines, image placeholders and the quieter outlined buttons (cart button, product card actions, pagination) derive from the inherited text color, so they read on a light or dark page without configuration. `--commerce-surface` colors form inputs and the opaque cart drawer, paired with `--commerce-text`; it does not color component wrappers. Error and warning messages bring their own light background. The Sale badge fills with `--commerce-sale` behind `--commerce-on-sale` text, so it reads over an image and on any page; sale prices keep the surrounding text color, with the compare-at price struck through beside them. Images have square corners; `--commerce-radius` rounds buttons, inputs and option buttons. Primary buttons use `--commerce-accent` and are ringed in it on focus, so choose an accent that contrasts with the page. Set `--commerce-surface` and `--commerce-text` together to adapt controls and the drawer to a dark theme. Avoid setting wrapper backgrounds through CSS or the `theme` prop's `background`, `backgroundColor`, or `backgroundImage` properties, which can still override the transparent default. Descendants inherit the surface text and body font instead of global heading styles. Keep text, controls and focus indicators accessible when changing colors. Internal utility class names and markup are not a customization API; use the [product](#custom-product-layouts), [catalog](#custom-catalog-layouts) and [cart](#custom-cart-layouts) parts to change a layout, and the [styling hooks](#styling-hooks) to style inside a part.
94
96
 
95
- Prices follow the provider's `locale`, which defaults to `en-US`. UI text is English. The catalog title and description are configurable through `CatalogHeader`, and many parts take [replacement text](#replacing-text). Full localization is outside this initial API. When it arrives, translated text will be a separate prop that a site opts into; `locale` will keep formatting only, so a site that sets it for prices never gets new wording in a minor release. Replacement text will still take priority over translations.
97
+ Prices follow the provider's `locale`, which defaults to `en-US`. UI text is English unless the site sets [`messages`](#translations).
96
98
 
97
99
  ## Compatibility
98
100
 
@@ -107,6 +109,8 @@ The repository example uses React Router 7. Follow each router version's own Rea
107
109
  | `CommerceStorefront` | Recommended integration: provider and one drawer |
108
110
  | `Catalog` | Six products per cursor page, or `pageSize`. Its `title`, `description`, and `showHeader` props are deprecated; compose `CatalogRoot` instead |
109
111
  | `CatalogRoot` and catalog parts | Build a [custom catalog or card layout](#custom-catalog-layouts) with the same rules as `Catalog` |
112
+ | `useProductCard`, `useCatalog` | Card and catalog state for your own card parts, such as a category line or a product count |
113
+ | `SampleProduct` | The type of [your own sample products](#your-own-sample-products), for a template shown before a store is connected |
110
114
  | `ProductDetails` | Reads `:productId`; validates URL option selections against catalog results. `ProductDetailsRoot` also takes a `productId` prop |
111
115
  | `ProductDetailsRoot` and product parts | Build a [custom product layout](#custom-product-layouts) with the same rules as `ProductDetails` |
112
116
  | `ProductCard` | Renders one `SKUGroup` with direct add or a product-details link; `ProductCardRoot` and card parts customize it |
@@ -117,7 +121,8 @@ The repository example uses React Router 7. Follow each router version's own Rea
117
121
  | `AddToCartButton` | Adds a verified `sku`, `name`, and optional integer `quantity`; while disabled, its accessible description says why. Its children replace the label |
118
122
  | `CommerceStatus` | Connection progress/error/retry for custom layouts |
119
123
  | `useCommerce` | Cart, connection, pending/error state and serialized cart actions |
120
- | `useMoney` | Formats minor units in the provider's `locale`, like the package's own prices, for custom parts |
124
+ | `useMoney` | Formats minor units in the provider's `locale`, like the package's own prices, for custom parts. `useMoneyRange` formats a range |
125
+ | `useMessage` | The provider's [text](#translations) by key, for custom parts |
121
126
  | `ProductImage` | A product image with the package's placeholder for a missing or broken image, for custom parts |
122
127
  | `CommerceProvider` | Lower-level composition when the recommended wrapper does not fit, such as a custom drawer; mount it once |
123
128
 
@@ -303,11 +308,25 @@ As with product layouts, the grid, spacing and border classes come from your own
303
308
  | `ProductCardDescription` | Up to two lines of description; nothing without one. The packaged card leaves it out |
304
309
  | `ProductCardAction` | Adds the product directly only when it has one SKU and no options; otherwise links to the product page to choose. Its children replace the label; as a function, they receive the `action`: `add`, `choose-options` or `view-product` |
305
310
 
306
- `className` on `CatalogProducts` goes on the element whose direct children are the cards, so a host grid places them, and `className` on `ProductCardRoot` goes on each card, so classes such as `col-span-2` work. While products load, that element holds three placeholder cards, so they follow the host's grid. The error with its retry button and, without a `CatalogEmpty`, the empty message render just before it. Card parts take no product, SKU or price props. Inside `CatalogProducts`, `ProductCardRoot` takes no `product` prop either and throws if it gets one, so a card always shows the product it is rendered for. Samples use the same card: they show a placeholder image, no badge, description or links, and add only to the sample cart. Card and catalog parts throw if they are rendered outside their root. Packaged cards fill their grid row, with the price and action at the bottom of each card.
311
+ `className` on `CatalogProducts` goes on the element whose direct children are the cards, so a host grid places them, and `className` on `ProductCardRoot` goes on each card, so classes such as `col-span-2` work. While products load, that element holds three placeholder cards, so they follow the host's grid. The error with its retry button and, without a `CatalogEmpty`, the empty message render just before it. Card parts take no product, SKU or price props. Inside `CatalogProducts`, `ProductCardRoot` takes no `product` prop either and throws if it gets one, so a card always shows the product it is rendered for. Samples use the same card and add only to the sample cart. They have no product page, so their title and image are not links. The packaged samples show a placeholder image and no badge or description; [your own samples](#your-own-sample-products) show their image, description and sale badge. Card images and their loading placeholders are square; set `--commerce-card-image-aspect`, such as `3 / 4`, in the `theme` or on any element around the cards to change that. Card and catalog parts throw if they are rendered outside their root. Packaged cards fill their grid row, with the price and action at the bottom of each card.
307
312
 
308
313
  The root keeps its page cursor in the page URL, as `after`. For a second catalog on the same page, such as a featured row above the main grid, set `urlState={false}` on it so the two do not share one cursor.
309
314
 
310
- For a product the host already has, such as a featured product, use `ProductCard` or `ProductCardRoot` with a `product` prop outside the catalog. `useProductCard()` returns read-only `product` (`null` for samples), `name`, `href` (`null` for samples) and `isSample` for building custom card parts. Every part accepts `className` on an outer wrapper that the package's CSS reset leaves alone, with the same limits as [product parts](#custom-product-layouts).
315
+ For a product the host already has, such as a featured product, use `ProductCard` or `ProductCardRoot` with a `product` prop outside the catalog. `useProductCard()` returns read-only `product` (`null` for samples), `sample` (the sample as you passed it, or `null` for a product), `name`, `href` (`null` for samples) and `isSample` for building custom card parts. `useCatalog()` returns the catalog's `count`, `isSample` and `status` (`loading`, `error` or `ready`) inside `CatalogRoot`, for a custom product count.
316
+
317
+ Elements that are only display, such as a "Bestseller" label, a category line or a "View" link, are your own components, written with these hooks. Write such a component once and read its data where you place it, so the same label works on a card and in the cart:
318
+
319
+ ```tsx
320
+ function CategoryLine() {
321
+ const { sample, product } = useProductCard<RoutineSample>();
322
+ // Samples carry your own fields; live products come from your catalog data.
323
+ // The type is not checked, so a sample from another catalog, such as the packaged ones, may lack your fields.
324
+ const category = sample ? sample.category : categoryFor(product);
325
+ return category ? <p className='eyebrow'>{category}</p> : null;
326
+ }
327
+ ```
328
+
329
+ Every part accepts `className` on an outer wrapper that the package's CSS reset leaves alone, with the same limits as [product parts](#custom-product-layouts).
311
330
 
312
331
  ## Custom cart layouts
313
332
 
@@ -406,7 +425,7 @@ For a cart page instead of, or as well as, the drawer, compose the same parts in
406
425
  Two hooks support custom cart markup:
407
426
 
408
427
  - `useCart()` works anywhere inside `CommerceProvider`. It returns read-only `items`, `itemCount`, `subtotal` (minor units, or `null` when the server has not priced the cart), `currency`, `isSample` and `loading`. Combine `itemCount` with `useCommerce().setOpen(true)` for a custom cart button.
409
- - `useCartItem()` returns the line inside `CartItemRoot`: `id`, `name`, `quantity`, `imageUrl`, `options`, `subtotal` and `currency`.
428
+ - `useCartItem()` returns the line inside `CartItemRoot`: `id`, `name`, `quantity`, `imageUrl`, `options`, `subtotal`, `currency` and `sample` (the sample as it was added, or `null` for a store line).
410
429
 
411
430
  ## Replacing text
412
431
 
@@ -433,7 +452,30 @@ Parts whose wording a site often changes take replacement content as children. T
433
452
  | `ProductCardAction` | `{ action, busy, soldOut }` |
434
453
  | `ProductDetailsBackLink` | Content only |
435
454
 
436
- A replaced label stays the start of the control's accessible name, so voice control users can say what they see. Where the package adds the product name for screen readers, it follows as visually hidden text: `<CartItemRemove>Delete</CartItemRemove>` is named "Delete Mug". Controls without replacement text, such as "Increase quantity of Mug", keep English names.
455
+ A replaced label stays the start of the control's accessible name, so voice control users can say what they see. Where the package adds the product name for screen readers, it follows as visually hidden text: `<CartItemRemove>Delete</CartItemRemove>` is named "Delete Mug". Controls without replacement text, such as "Increase quantity of Mug", take their names from [`messages`](#translations).
456
+
457
+ ## Translations
458
+
459
+ The package's text comes from the provider's `messages`. Set it with `locale` for a translated store; the two are separate, so a site that sets `locale` for prices never gets new wording.
460
+
461
+ ```tsx
462
+ import { CommerceStorefront } from '@godaddy/gd-commerce-storefront';
463
+ import { frFR } from '@godaddy/gd-commerce-storefront/messages/fr-FR';
464
+
465
+ <CommerceStorefront locale='fr-FR' messages={frFR}>
466
+ ```
467
+
468
+ French is its own import, so English-only sites do not load it. Pass only the keys to change, and the rest stay as they were, English by default: `messages={{ ...frFR, checkout: 'Commander' }}`. The `StorefrontMessages` type lists every key, and `enUS` has the English text. New keys may be added in minor releases and stay English until translated, so type a translation of your own as `Partial<StorefrontMessages>`.
469
+
470
+ Messages are plain data, so they can come from JSON, a translation service, or a server component. `{name}` placeholders are filled in, and numbers in them are formatted for the messages' `locale`. A message with a count can have plural forms, chosen by the language's rules; French counts 0 as singular:
471
+
472
+ ```ts
473
+ { productCount: { one: '{count} produit affiché', other: '{count} produits affichés' } }
474
+ ```
475
+
476
+ The server's failure messages are English. With English text the storefront shows them as they are; with any other language it shows the text in `errorCodes` for the response's `code`, such as `scope_mismatch`, or the generic `requestFailed`. Product names, descriptions and attribute names come from the catalog as the merchant entered them, and are not translated. Sample prices stay in USD.
477
+
478
+ [Replacement text](#replacing-text) and props such as `CatalogHeader`'s `title` take priority over `messages`. For custom parts, `useMessage()` returns the provider's text by key, `useMoney()` formats prices, and `useMoneyRange()` formats a range the way the `locale` writes it, such as `$20.00 – $25.00` or `20,00–25,00 €`.
437
479
 
438
480
  ## Styling hooks
439
481
 
@@ -458,7 +500,7 @@ Every part's outer element has a `data-commerce-part` attribute with the part's
458
500
  | `item-removed` | `itemId`, `isSample` |
459
501
  | `discount-applied` | `code` |
460
502
  | `checkout-started` | `cartId`; sent just before the browser leaves for checkout, so use `navigator.sendBeacon` or a keepalive request |
461
- | `error` | `operation` (`add`, `change-quantity`, `remove`, `discount`, `refresh` or `checkout`) and the `message` the cart shows |
503
+ | `error` | `operation` (`add`, `change-quantity`, `remove`, `discount`, `refresh` or `checkout`), the `message` the cart shows in the provider's language at the time of the failure, and the server's `code`, such as `scope_mismatch`, when it sent one. The cart's own notice follows a later change of `messages`; the event's text does not, so log `code` to group failures |
462
504
 
463
505
  New event types may be added in minor releases, so ignore types you do not handle. A callback that throws or returns a rejected promise, including `onItemAdded`, is logged to the console and does not fail the cart operation. Async callbacks are not awaited, so a slow analytics call never delays the cart.
464
506
 
@@ -506,8 +548,8 @@ A site can mount the same components before it has a connected store:
506
548
  </CommerceStorefront>
507
549
  ```
508
550
 
509
- `Catalog.sampleProducts` defaults to `false`. It allows six built-in sample products
510
- (Product 1–6, $10–$60 USD) **only** when `/api/commerce/config` reports `state: 'unbound'`.
551
+ `Catalog.sampleProducts` defaults to `false`. `true` allows six built-in sample products
552
+ (Product 1–6, $10–$60 USD, without images) **only** when `/api/commerce/config` reports `state: 'unbound'`.
511
553
  When a store is connected, the server reports a different state and the components show real
512
554
  products. You do not need to change props or the component tree. The prop has no effect while
513
555
  a store is connected. Real empty catalogs and errors never fall back to samples.
@@ -536,6 +578,55 @@ state for up to 60 seconds before showing the error. Once polling slows to every
536
578
  failure is already 60 seconds after the last success, so it shows the error at once. Polling
537
579
  stops when the state is `ready`; focus and explicit retry still refresh configuration.
538
580
 
581
+ #### Your own sample products
582
+
583
+ A template can show its own samples instead, with the same rule. Pass them to `sampleProducts` on
584
+ `Catalog` or `CatalogRoot`:
585
+
586
+ ```tsx
587
+ interface RoutineSample extends SampleProduct {
588
+ category: string;
589
+ }
590
+
591
+ const samples: RoutineSample[] = [
592
+ {
593
+ key: 'daily-cleanser',
594
+ name: 'Daily Cleanser',
595
+ price: 3200,
596
+ description: 'Gentle, pH-balanced foam that removes without stripping.',
597
+ imageUrl: '/images/cleanser.jpg',
598
+ category: 'Cleanse',
599
+ },
600
+ ];
601
+
602
+ <Catalog sampleProducts={samples} />
603
+ ```
604
+
605
+ Each sample needs a unique `key`, a `name` and a `price` in minor units of USD. `compareAtPrice`,
606
+ `description` and `imageUrl` are optional. `imageUrl` is any URL the browser can load, such as a
607
+ file in the site's own public folder; the package does not upload or check it. An image that
608
+ fails to load shows the image placeholder on the card and no image in the cart. `CatalogRoot`
609
+ throws, while no store is connected, for an empty list, an entry that is not an object, a
610
+ duplicate or empty key, a packaged sample's key (`sample-1` to `sample-6`), an empty name, a
611
+ `description` or `imageUrl` that is not a string, or a price or compare-at price that is not a
612
+ whole number of 0 or more. Once a store is connected the samples are not shown, so development
613
+ builds log the problem instead and the store's catalog still renders. A list written inline is
614
+ compared by its values, so it does not rebuild the cards when it is unchanged. In a card with
615
+ `ProductCardTitle`, a sample's image is hidden from screen readers, which read the name from the
616
+ title.
617
+
618
+ The card parts show a sample's image, description, price, and a Sale badge and struck-through
619
+ price when `compareAtPrice` is above `price`. Samples have no product page. Fields of your own,
620
+ such as `category`, come back unchanged from `useProductCard().sample` and, once added, from
621
+ `useCartItem().sample`. The type you pass, as in `useProductCard<RoutineSample>()`, is not
622
+ checked, so where other samples can appear, read your own fields as possibly missing. The sample
623
+ cart keeps each sample as it was added, with its image, so the cart still shows it on a page
624
+ without the catalog. While a catalog with a sample of the same key is mounted, the cart shows
625
+ that sample's current fields, so a price or name loaded later matches the card. The cart lists
626
+ packaged samples in catalog order and your own in the order they were added, after them. Once a
627
+ store is connected, the same card tree shows the store's products, so build the card for both:
628
+ your own parts read `sample` before a store is connected and `product` after.
629
+
539
630
  Use a server that reports connection state before you enable `sampleProducts`. Successful
540
631
  config responses without `state` still mean ready. A server that does not report `unbound`
541
632
  never shows samples. See the [server contract](docs/server-api.md) for the host's