From eec02e4fe44663938fe7a47143c1c6e34c3dcfb9 Mon Sep 17 00:00:00 2001 From: Abeuty Date: Thu, 7 May 2026 12:39:46 -0600 Subject: [PATCH 1/2] feat(shop): animation polish, sticky filter bar, and custom dropdowns - Fix ProductDrawer open artifact: use derived-state pattern so displayHandle updates synchronously (no empty-frame flash on first open) - Fix ProductDrawer close glitch: always set inline width so the panel doesn't snap to min-width while sliding out - Delay drawer open animation until product data is pre-fetched, so it never opens on a loading skeleton - Replace native with a carved-in chevron. */ -export const ShopSelect = React.forwardRef( - function ShopSelect({ className, children, ...rest }, ref) { - return ( - + * so all call-sites stay unchanged. */ +export function ShopSelect({ value, onChange, className, triggerClassName, children }: Props) { + const [open, setOpen] = React.useState(false) + const [focused, setFocused] = React.useState(null) + const triggerRef = React.useRef(null) + const menuRef = React.useRef(null) + + const options = extractOptions(children) + const selected = options.find((o) => o.value === value) + + // Close on outside click + React.useEffect(() => { + if (!open) return + const handler = (e: MouseEvent) => { + if ( + !triggerRef.current?.contains(e.target as Node) && + !menuRef.current?.contains(e.target as Node) + ) { + setOpen(false) + } + } + document.addEventListener('mousedown', handler) + return () => document.removeEventListener('mousedown', handler) + }, [open]) + + // Keyboard navigation + const onKeyDown = (e: React.KeyboardEvent) => { + if (!open) { + if (e.key === 'Enter' || e.key === ' ' || e.key === 'ArrowDown') { + e.preventDefault() + setOpen(true) + setFocused(value ?? options[0]?.value ?? null) + } + return + } + const idx = options.findIndex((o) => o.value === focused) + if (e.key === 'ArrowDown') { + e.preventDefault() + setFocused(options[Math.min(idx + 1, options.length - 1)]?.value ?? null) + } else if (e.key === 'ArrowUp') { + e.preventDefault() + setFocused(options[Math.max(idx - 1, 0)]?.value ?? null) + } else if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault() + if (focused) select(focused) + } else if (e.key === 'Escape') { + setOpen(false) + triggerRef.current?.focus() + } + } + + const select = (optValue: string) => { + onChange?.({ target: { value: optValue } }) + setOpen(false) + triggerRef.current?.focus() + } + + return ( +
+ + + {open ? ( +
+ {options.map((opt) => { + const isSelected = opt.value === value + const isFocused = opt.value === focused + return ( + + ) + })} +
+ ) : null} +
+ ) +} diff --git a/src/components/shop/ui/Tab.tsx b/src/components/shop/ui/Tab.tsx index fc54165c..bc0f6dd8 100644 --- a/src/components/shop/ui/Tab.tsx +++ b/src/components/shop/ui/Tab.tsx @@ -1,4 +1,5 @@ import * as React from 'react' +import { twMerge } from 'tailwind-merge' type Props = React.ButtonHTMLAttributes & { isActive?: boolean @@ -11,18 +12,18 @@ export const ShopTab = React.forwardRef( { isActive, count: _count, children, className, ...rest }, ref, ) { - const base = - 'inline-flex items-center px-4 py-2.5 rounded-xl font-shop-mono text-shop-ui transition-colors cursor-pointer' - const state = isActive - ? 'border-2 border-shop-muted font-medium text-shop-text' - : 'border border-shop-line-2 font-normal text-shop-text-2 hover:text-shop-text hover:border-shop-muted' - return ( diff --git a/src/routes/shop.index.tsx b/src/routes/shop.index.tsx index 30ed18dd..4e41e94a 100644 --- a/src/routes/shop.index.tsx +++ b/src/routes/shop.index.tsx @@ -124,69 +124,80 @@ function ShopIndex() { : allProducts.filter((p) => p.productType?.toLowerCase() === activeType) return ( -
-
- - Built in public,
- worn in production. - - } - lede="Official TanStack apparel, accessories, and stickers. Limited runs, ethically produced, shipped worldwide. Rep the libraries that ship your code every day." - /> +
+ {/* Hero */} +
+
+ + Built in public,
+ worn in production. + + } + lede="Official TanStack apparel, accessories, and stickers. Limited runs, ethically produced, shipped worldwide. Rep the libraries that ship your code every day." + /> +
-
- - navigate({ - to: '/shop', - search: (prev) => ({ ...prev, type: undefined }), - }) - } - > - All - - {typeOptions.map((opt) => ( + {/* Sticky filter + sort bar */} +
+
navigate({ to: '/shop', - search: (prev) => ({ ...prev, type: opt.key }), + search: (prev) => ({ ...prev, type: undefined }), }) } > - {opt.display} + All - ))} - { - const nextId = e.target.value as ValidSortId - navigate({ - to: '/shop', - search: (prev) => ({ - ...prev, - sort: nextId === 'BEST_SELLING' ? undefined : nextId, - }), - }) - }} - > - {SORT_OPTIONS.map((opt) => ( - + {typeOptions.map((opt) => ( + + navigate({ + to: '/shop', + search: (prev) => ({ ...prev, type: opt.key }), + }) + } + > + {opt.display} + ))} - + { + const nextId = e.target.value as ValidSortId + navigate({ + to: '/shop', + search: (prev) => ({ + ...prev, + sort: nextId === 'BEST_SELLING' ? undefined : nextId, + }), + }) + }} + > + {SORT_OPTIONS.map((opt) => ( + + ))} + +
+ {/* Product grid */} +
{products.length === 0 ? (
No products yet. Check back soon! @@ -216,6 +227,8 @@ function ShopIndex() { )} +
+ p.handle)} From c470d4ee736520bd8d5632b80df0e468e3757a9e Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Thu, 7 May 2026 18:44:29 +0000 Subject: [PATCH 2/2] ci: apply automated fixes --- src/components/shop/ProductDrawer.tsx | 4 +- src/components/shop/ui/Select.tsx | 8 +++- src/routes/shop.index.tsx | 57 +++++++++++++-------------- 3 files changed, 38 insertions(+), 31 deletions(-) diff --git a/src/components/shop/ProductDrawer.tsx b/src/components/shop/ProductDrawer.tsx index e62f2337..054c7b6d 100644 --- a/src/components/shop/ProductDrawer.tsx +++ b/src/components/shop/ProductDrawer.tsx @@ -148,7 +148,9 @@ export function ProductDrawer({ // slides out with content visible (not empty). Uses the derived-state pattern // so displayHandle is updated synchronously on open (no empty-frame flash). const [displayHandle, setDisplayHandle] = React.useState(null) - const [prevProductHandle, setPrevProductHandle] = React.useState(null) + const [prevProductHandle, setPrevProductHandle] = React.useState< + string | null + >(null) if (productHandle !== prevProductHandle) { setPrevProductHandle(productHandle) if (productHandle) setDisplayHandle(productHandle) diff --git a/src/components/shop/ui/Select.tsx b/src/components/shop/ui/Select.tsx index 8faed028..50b63d00 100644 --- a/src/components/shop/ui/Select.tsx +++ b/src/components/shop/ui/Select.tsx @@ -31,7 +31,13 @@ function extractOptions(children: React.ReactNode): Array { /** Custom themed dropdown. Keeps the same onChange API as a native