Add collection filters and sort, out-of-stock options, gallery drag

- Collections: filters and sort via the Storefront API — the products
  connection now takes `filters`/`after` and returns its facets, with
  cursor-based load more on the collection page
- Extract ProductFilters (renamed from SearchFilters) and a shared
  ProductToolbar so search and collections use the same chrome
- PDP: mark option values with no in-stock variant using a diagonal
  strike, computed against the other selected options
- PDP: pointer drag-scrolling for the mobile image carousel, since a
  scroll container doesn't drag with a mouse
- Header/cart: circular icon buttons, tighter spacing, and icon sizes
  set by class (Button's base CSS clamps un-classed svgs to size-4)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jWbNNJLksC1QG8z8845FX
This commit is contained in:
Rami Bitar
2026-08-01 12:27:05 -04:00
co-authored by Claude Opus 5
parent cc901b9ce8
commit 2ef5639a2e
13 changed files with 515 additions and 144 deletions
@@ -51,6 +51,8 @@ interface ProductDetailInfoProps {
handleAddToCart: () => void;
handleBuyNow?: () => void;
onOptionChange: (optionName: string, value: string) => void;
/** Whether an option value still has an in-stock variant behind it. */
isOptionValueAvailable?: (optionName: string, value: string) => boolean;
loading?: boolean;
buyingNow?: boolean;
addToCartLabel?: string;
@@ -81,6 +83,7 @@ const ProductDetailInfo: React.FC<ProductDetailInfoProps> = ({
handleAddToCart,
handleBuyNow,
onOptionChange,
isOptionValueAvailable,
loading = false,
buyingNow = false,
addToCartLabel = 'Add to Cart',
@@ -143,6 +146,8 @@ const ProductDetailInfo: React.FC<ProductDetailInfoProps> = ({
<div className="flex flex-wrap gap-2">
{optionValuesFor(option).map((value) => {
const isSelected = selected === value.name;
const isSoldOut =
isOptionValueAvailable?.(option.name, value.name) === false;
if (isSwatch) {
const { background, image } = swatchStyle(value);
@@ -153,14 +158,17 @@ const ProductDetailInfo: React.FC<ProductDetailInfoProps> = ({
onClick={() => onOptionChange(option.name, value.name)}
variant="ghost"
size="icon-sm"
title={value.name}
aria-label={value.name}
aria-pressed={isSelected}
title={
isSoldOut ? `${value.name} — out of stock` : value.name
}
data-available={!isSoldOut}
className={`rounded-full bg-cover bg-center hover:bg-transparent ${
isSelected
? 'ring-2 ring-foreground ring-offset-2'
: 'ring-1 ring-border hover:ring-foreground/40'
}`}
} ${isSoldOut ? 'option-unavailable text-foreground/60 opacity-60' : ''}`}
style={{
backgroundColor: background,
backgroundImage: image ? `url(${image})` : undefined,
@@ -179,11 +187,12 @@ const ProductDetailInfo: React.FC<ProductDetailInfoProps> = ({
onClick={() => onOptionChange(option.name, value.name)}
variant="outline"
aria-pressed={isSelected}
title={isSoldOut ? `${value.name} — out of stock` : undefined}
className={`min-w-14 px-5 font-normal shadow-none ${
isSelected
? 'border-foreground text-foreground'
: 'text-muted-foreground hover:border-foreground hover:text-foreground'
}`}
} ${isSoldOut ? 'option-unavailable text-muted-foreground/60' : ''}`}
>
{value.name}
</Button>