Skip to content

feat(shop): product drawer, color swatches, and UI polish#891

Merged
tannerlinsley merged 3 commits intomainfrom
claude/naughty-satoshi-e456cb
May 5, 2026
Merged

feat(shop): product drawer, color swatches, and UI polish#891
tannerlinsley merged 3 commits intomainfrom
claude/naughty-satoshi-e456cb

Conversation

@Abeuty
Copy link
Copy Markdown
Contributor

@Abeuty Abeuty commented May 5, 2026

Summary

  • ProductDrawer — new slide-in quick-view drawer with resizable width (520px default, drag handle, double-click reset), blurred transparent hero image area, solid cream content area, vertical scrollable thumbnail strip showing all Shopify images, wrapping color/size pills, and a minimal close button at top-left
  • Color swatches on ProductCard — round swatch dots now use the same hex map and last-token-wins resolution as the drawer; all variant images are preloaded on mount via new Image() so hovering a swatch swaps the preview image instantly with no network wait
  • Shop token fix — moved @theme inline color mappings to app.css (processed by Tailwind) so bg-shop-bg, border-shop-line, etc. generate actual CSS instead of resolving to transparent when shop.css was loaded via ?url
  • Filter tabs & sort select — match Figma pill spec (rounded-xl, DM Mono, border-weight active state); counts hidden per spec; twMerge replaced with string concatenation where it was dropping text-shop-ui due to conflicting group keys
  • Chip.tsx / Size.tsx — tokenized hardcoded dark hex values to shop surface/line design tokens

Test plan

  • Open shop page, verify filter tabs and sort select render as matching pills
  • Click any product card → drawer slides in, image and thumbnails load, color/size pills wrap on narrow widths
  • Select a color in the drawer → hero image updates immediately before size is chosen
  • Drag the drawer's left edge to resize; double-click to reset to 520px
  • Close via the X button (top-left), Escape key, or clicking the scrim — drawer animates out as solid object
  • Hover color swatches on product cards → preview image swaps instantly (no loading flash)
  • Verify dark mode: drawer background, borders, and tokens all render correctly
  • Verify light mode: image area is blurred/transparent, content area is solid cream

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features

    • Added quick view drawer for browsing product details without leaving the shop
    • Color swatches on product cards with image previews on hover
    • Resizable product detail drawer with keyboard navigation and persistent width
    • "NEW" badge displays on recently published products
  • Refactor

    • Updated shop typography, colors, and layout styles
    • Simplified product grid responsive design
    • Currency now displays as whole numbers without decimal places

Introduces a full-featured product quick-view drawer and a suite of
shop UI improvements built toward the 2026 design system.

**ProductDrawer** (new component)
- Slide-in drawer with resizable width (default 520px, drag to resize,
  double-click to reset), solid exit animation via `displayHandle` latch
- Hero image with blurred transparent background (40% opacity,
  20px backdrop-blur); solid cream content area below
- Vertical scrollable thumbnail strip showing all Shopify images;
  selecting a color updates the hero immediately via wildcard variant match
- Wrapping color/size option pills (individual `flex-wrap` pills, not
  joined strips); standardized to `text-shop-sm` (12px) across all
  selector states so pills never shift width on selection
- Minimal close button (no box) at top-left of drawer

**ProductCard**
- Color swatches now use the same hex map and last-token-wins resolution
  as the drawer ("Vintage Black" → black, not vintage)
- Swatch circles preload all variant images on mount via `new Image()` so
  hover-to-preview is instant (in-place `src` swap on cached URLs)
- Hover over a swatch previews that color's variant image; mouse-leave
  the swatch row reverts to the featured image

**Shop UI tokens / CSS**
- Added `@theme inline` block to `app.css` so Tailwind generates shop
  color utilities (`bg-shop-bg`, `border-shop-line`, etc.) — fixes tokens
  silently resolving to transparent when shop.css was loaded via `?url`
- Filter tabs and sort select match Figma pill spec: rounded-xl, DM Mono,
  border-weight active state; tab counts hidden per spec
- Chip.tsx and Size.tsx tokenized from hardcoded dark hex values to
  shop surface/line tokens

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@netlify
Copy link
Copy Markdown

netlify Bot commented May 5, 2026

Deploy Preview for tanstack ready!

Name Link
🔨 Latest commit 93774b5
🔍 Latest deploy log https://app.netlify.com/projects/tanstack/deploys/69fa2eaa786661000817852a
😎 Deploy Preview https://deploy-preview-891--tanstack.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
Lighthouse
Lighthouse
1 paths audited
Performance: 45 (🔴 down 12 from production)
Accessibility: 90 (no change from production)
Best Practices: 83 (🔴 down 9 from production)
SEO: 97 (no change from production)
PWA: 70 (no change from production)
View the detailed breakdown and full score reports
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 5, 2026

📝 Walkthrough

Walkthrough

This PR redesigns the shop interface by introducing a quick-view product drawer, updating the product card to display color swatches with hover image swapping, simplifying the shop layout and sidebar navigation, and establishing a new design system with tokenized typography and color utilities. Data shape changes include publishing date for "new" badges, product options, and variant images for color-swatch selection.

Changes

Product Card & Quick View System

Layer / File(s) Summary
Data Shape
src/utils/shopify-queries.ts
ProductListItem adds publishedAt, options, and variants.nodes (with selectedOptions and image); removes featuredImage.
Card Component
src/components/shop/ProductCard.tsx
ProductCard gains optional onQuickView callback; renders as interactive button when provided, otherwise as link. Replaces tag-derived badges with "NEW" badge based on publishedAt within 2-week window. Adds color swatch extraction from options, preloads variant images, and swaps hero image on color hover.
Route Wiring
src/routes/shop.index.tsx
Shop index now manages drawerHandle state, passes onQuickView={setDrawerHandle} to product cards, and restructures sort UI. Removes promotional drop card and footer strip. Updates grid to auto-fill minmax layout.

Product Detail Drawer

Layer / File(s) Summary
Drawer Component
src/components/shop/ProductDrawer.tsx
New ProductDrawer with right-side resizable drawer, persisted width in localStorage, keyboard navigation (Esc/arrows), and displayHandle to retain animation during close. DrawerBody fetches product via React Query; DrawerContent manages option selection, quantity, and image syncing.
Cart Integration
src/components/shop/ProductDrawer.tsx
Add-to-cart button gated by option completeness and variant availability; on click opens cart drawer and adds line item with variant id, quantity, and metadata (title/handle/price/image/options).
Route Loader
src/routes/shop.tsx
Simplified loader to only prefetch cart query; removed collections and policies fetching and passing. Inline route component now renders ShopLayout with Outlet only.

Layout & Navigation Simplification

Layer / File(s) Summary
Layout Component
src/components/shop/ShopLayout.tsx
ShopLayout now accepts only children prop, removing collections and policies. Renders .shop-scope wrapper, main with children, and ShopCartDrawer; prior sidebar rendering removed.
Route Structure
src/routes/shop.tsx
Removed ShopRoute wrapper and staticData.Title logic; collections/policies no longer passed to layout or used in route.

Design System & Styling

Layer / File(s) Summary
Theme Tokens
src/styles/app.css, src/styles/shop.css
app.css introduces --font-shop-display, --font-shop-mono, and shop text scale (--text-shop-display, --text-shop-heading, ..., --text-shop-2xs). Adds shop color tokens (--color-shop-*) mapped to CSS variables. shop.css updates light/dark scopes with new surface tokens, switches mono font to DM Mono, changes primary font to DM Sans, and adds shop-cta-gradient and shop-cta-rotate keyframes.
UI Component Styling
src/components/shop/ui/Badge.tsx, src/components/shop/ui/Button.tsx, src/components/shop/ui/Chip.tsx, src/components/shop/ui/Mono.tsx, src/components/shop/ui/Select.tsx, src/components/shop/ui/Size.tsx, src/components/shop/ui/Tab.tsx
Typography and border classes updated to use tokenized utilities (e.g., text-shop-sm, border-shop-line-2). Chip gains optional colorBg, selectedBg, selectedTextColor props for dynamic color styling with contrast-aware text. Tab switches from twMerge to template-string class building and stops rendering count. ShopHero updates heading to text-shop-display and lede to text-shop-sm.
Utility Update
src/utils/shopify-format.ts
formatMoney now enforces minimumFractionDigits: 0 and maximumFractionDigits: 0 for currency formatting.

Sequence Diagram

sequenceDiagram
    participant User
    participant ProductList as Shop Index
    participant ProductCard
    participant ProductDrawer
    participant DrawerContent
    participant Shopify as Shopify API
    participant Cart as Cart Drawer

    User->>ProductList: Browse products
    ProductList->>ProductCard: Render cards with onQuickView
    ProductCard->>ProductCard: Extract color option & swatch
    
    User->>ProductCard: Hover color swatch
    ProductCard->>ProductCard: Swap hero image for variant
    
    User->>ProductCard: Click "Quick View"
    ProductCard->>ProductList: onQuickView(handle)
    ProductList->>ProductDrawer: Set drawerHandle state
    
    ProductDrawer->>Shopify: Fetch product details (React Query)
    Shopify-->>ProductDrawer: Product + variants + options
    ProductDrawer->>DrawerContent: Render with selected variant
    
    User->>DrawerContent: Select options & quantity
    DrawerContent->>DrawerContent: Compute exact variant match
    
    User->>DrawerContent: Click "Add to Cart"
    DrawerContent->>Cart: openCartDrawer()
    DrawerContent->>Shopify: addToCart.mutate({ variantId, quantity, metadata })
    Shopify-->>DrawerContent: ✓ Added
    DrawerContent->>DrawerContent: Reset after timeout
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 Hops through swatches bright and new,
Quick-view drawers with colors true,
Product cards that dance with flair,
Tokenized design floating in air!
DM Sans and surfaces smooth,
The shop now grooves!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 42.86% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the three main changes in this PR: the new ProductDrawer component, color swatches implementation in ProductCard, and comprehensive UI polish across shop components.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/naughty-satoshi-e456cb

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/components/shop/ui/Mono.tsx (1)

3-7: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Stale JSDoc — still references "JetBrains Mono, 10.5px".

The implementation now uses font-shop-mono (DM Mono) and text-shop-xs tokens, and you already updated the ShopMono comment on line 29 to say DM Mono. Update this one too so the two helpers don't diverge in their documentation.

📝 Suggested fix
 /**
  * Mono uppercase label used for section headings and metadata rows.
- * Consistent typography across the shop: JetBrains Mono, 10.5px,
- * 0.14em tracking, uppercase.
+ * Consistent typography across the shop: DM Mono, text-shop-xs,
+ * 0.14em tracking, uppercase.
  */
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/shop/ui/Mono.tsx` around lines 3 - 7, The top JSDoc comment in
src/components/shop/ui/Mono.tsx is stale — it still mentions "JetBrains Mono,
10.5px" while the implementation uses the font token font-shop-mono (DM Mono)
and text-shop-xs; update that JSDoc to match the actual implementation (use DM
Mono and text-shop-xs) so it aligns with the ShopMono comment and avoids
divergent documentation for the Mono/ShopMono helpers.
src/utils/shopify-format.ts (1)

6-13: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Forcing maximumFractionDigits: 0 rounds cart subtotals/totals, which misrepresents the actual charge.

This formatter is shared by product cards (ProductCard.tsx), the product drawer (ProductDrawer.tsx), the cart drawer (CartDrawer.tsx), and the cart route (shop.cart.tsx subtotal/total). Setting maximumFractionDigits: 0 rounds every value: e.g. a 99.49 total renders as "$99" and a 100.50 total as "$101". For cart subtotals/totals this is a real correctness issue — the user sees a price that does not match what Shopify will charge.

If the intent is just to drop trailing zeros on whole-amount product prices (e.g. $120.00$120), drop only maximumFractionDigits and let the locale's default fraction digits handle non-whole values. Better still, gate the zero-decimal behaviour behind a flag and use it only at the call sites where prices are known to be whole (product cards/hero), keeping cart math precise.

💸 Suggested fix — keep formatter precise by default, opt in for whole-amount displays
-export function formatMoney(amount: string | number, currencyCode: string) {
-  return new Intl.NumberFormat(undefined, {
-    style: 'currency',
-    currency: currencyCode,
-    minimumFractionDigits: 0,
-    maximumFractionDigits: 0,
-  }).format(typeof amount === 'string' ? Number(amount) : amount)
-}
+export function formatMoney(
+  amount: string | number,
+  currencyCode: string,
+  opts: { compact?: boolean } = {},
+) {
+  const value = typeof amount === 'string' ? Number(amount) : amount
+  return new Intl.NumberFormat(undefined, {
+    style: 'currency',
+    currency: currencyCode,
+    // Drop trailing zeros for whole values when the caller asks for compact display;
+    // otherwise honour the currency's natural precision so cart totals stay accurate.
+    ...(opts.compact && Number.isInteger(value)
+      ? { minimumFractionDigits: 0, maximumFractionDigits: 0 }
+      : {}),
+  }).format(value)
+}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/utils/shopify-format.ts` around lines 6 - 13, formatMoney currently
forces maximumFractionDigits: 0 which rounds non‑whole totals; change
formatMoney(amount, currencyCode) to remove the
minimumFractionDigits/maximumFractionDigits defaults and instead accept an
optional third param (e.g. forceZeroDecimals = false); when forceZeroDecimals is
true set both minimumFractionDigits: 0 and maximumFractionDigits: 0 to preserve
the old behavior for product displays, otherwise let Intl.NumberFormat use
locale defaults so cart subtotals/totals remain precise. Update call sites that
expect trimmed zeros (ProductCard.tsx, ProductDrawer.tsx) to pass true and leave
CartDrawer.tsx and shop.cart.tsx unchanged.
🧹 Nitpick comments (2)
src/styles/shop.css (1)

16-35: ⚡ Quick win

Remove the duplicate @theme inline block from shop.css — it's dead code when loaded via ?url.

shop.css is loaded as import shopCss from '~/styles/shop.css?url' in src/routes/shop.tsx, which means Tailwind never processes it and browsers ignore the @theme at-rule as an unknown construct. The identical @theme inline block already exists in app.css (lines 84–100), creating active drift risk — the --color-shop-surface / --color-shop-surface-hover tokens were already added to both files in this PR.

Keep only the runtime CSS variables under .shop-scope and html.dark .shop-scope, which shop.css actually contributes. The font declarations (--font-shop-display, --font-shop-mono) are already in app.css's @theme block.

🧹 Suggested cleanup
-@theme inline {
-  --color-shop-bg: var(--shop-bg);
-  --color-shop-bg-2: var(--shop-bg-2);
-  --color-shop-panel: var(--shop-panel);
-  --color-shop-panel-2: var(--shop-panel-2);
-  --color-shop-surface: var(--shop-surface);
-  --color-shop-surface-hover: var(--shop-surface-hover);
-  --color-shop-line: var(--shop-line);
-  --color-shop-line-2: var(--shop-line-2);
-  --color-shop-text: var(--shop-text);
-  --color-shop-text-2: var(--shop-text-2);
-  --color-shop-muted: var(--shop-muted);
-  --color-shop-accent: var(--shop-accent);
-  --color-shop-accent-ink: var(--shop-accent-ink);
-  --color-shop-green: `#22c993`;
-  --color-shop-orange: `#ff7a3a`;
-
-  --font-shop-display: 'DM Sans', ui-sans-serif, system-ui, sans-serif;
-  --font-shop-mono: 'DM Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
-}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/styles/shop.css` around lines 16 - 35, Remove the duplicate `@theme` inline
block in shop.css: delete the entire `@theme` inline { ... } that defines the CSS
custom properties (including --font-shop-display and --font-shop-mono and the
color tokens) and keep only the runtime rules under .shop-scope and html.dark
.shop-scope; ensure color tokens used at runtime (e.g., --color-shop-bg,
--color-shop-surface, --color-shop-surface-hover, --color-shop-accent,
--color-shop-green/orange, --color-shop-text/muted, and --color-shop-line
variants) remain in the .shop-scope rules, and do not re-add the font variables
(--font-shop-display, --font-shop-mono) since they are already declared in
app.css's `@theme` block.
src/components/shop/ProductDrawer.tsx (1)

68-75: ⚡ Quick win

Drop dead code flagged by lint

contrastColor (lines 68–75) is never referenced — isDarkColor is used instead. DrawerContent also declares allHandles, onNavigate, and onClose but never reads them; the parent DrawerBody passes them through unnecessarily. Removing both removes lint noise and makes the component contract honest.

♻️ Proposed cleanup
-function contrastColor(hex: string): string {
-  const h = hex.replace('#', '')
-  const r = parseInt(h.slice(0, 2), 16)
-  const g = parseInt(h.slice(2, 4), 16)
-  const b = parseInt(h.slice(4, 6), 16)
-  const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255
-  return luminance > 0.55 ? '#000000' : '#ffffff'
-}
-
 function isDarkColor(hex: string): boolean {
-function DrawerContent({
-  product,
-  allHandles,
-  onNavigate,
-  onClose,
-}: {
-  product: ProductDetail
-  allHandles: string[]
-  onNavigate: (handle: string) => void
-  onClose: () => void
-}) {
+function DrawerContent({ product }: { product: ProductDetail }) {

And update the <DrawerContent ... /> call site in DrawerBody to pass only product.

Also applies to: 368-378

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/shop/ProductDrawer.tsx` around lines 68 - 75, Remove the
unused contrastColor function and eliminate the dead props from DrawerContent
(remove allHandles, onNavigate, onClose from its parameter list and internal
usage), then update the DrawerBody -> <DrawerContent ... /> call site to only
pass the required product prop (ensure DrawerContent's type/signature and any
prop interfaces are updated accordingly); also delete any other duplicated
unused variants of contrastColor around lines ~368-378.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/components/shop/ProductCard.tsx`:
- Around line 10-77: ProductCard.tsx duplicates COLOR_MAP and a different
resolver (colorHex) from ProductDrawer.COLOR_HEX/resolveColorHex causing drift;
extract a single shared module (e.g. src/components/shop/colorTokens.ts) that
exports the canonical COLOR_MAP and a single resolver function (e.g.
resolveColorHex) that does a full-string lookup then tokenized reverse lookup,
include all tokens used by both card and drawer (add card-only keys like
mixed/holo/polished/blend and drawer-only keys like maroon) and update
ProductCard.tsx to import COLOR_MAP/resolveColorHex (replace colorHex) and
ProductDrawer.tsx to import the same exports so both components use the
identical map and lookup behavior.
- Line 8: TWO_WEEKS_MS is miscalculated as one year; change its value to
represent 14 days by using 14 * 24 * 60 * 60 * 1000 (or compute from a DAY_MS
constant) in ProductCard (symbol: TWO_WEEKS_MS) so the "NEW" badge logic uses a
two-week window rather than 365 days.

In `@src/components/shop/ProductDrawer.tsx`:
- Around line 611-631: The quantity stepper in ProductDrawer renders the
increment button on the left and decrement on the right; swap their DOM order so
the decrement (onClick uses setQuantity(q => Math.max(1, q - 1)) /
aria-label="Decrease quantity") appears before the <span>{quantity}</span> and
the increment (onClick uses setQuantity(q => q + 1) / aria-label="Increase
quantity") appears after it, preserving all classes and handlers on those
buttons to follow standard minus-left/plus-right convention.
- Around line 381-397: Seed the selected state so single-value options are
auto-picked: when initializing selected in ProductDrawer, set each option's
value to its sole value if option.values.length === 1, otherwise ''. Update the
initializer that currently uses Object.fromEntries(product.options.map((o) =>
[o.name, ''])) to inspect product.options (and each option.values[0]) so
findExactVariant can match every variant option; this will make selectedVariant
(computed via findExactVariant) resolve for single-value-option products and
restore Add to Cart behavior while leaving isComplete logic and heroOverride
unchanged.

---

Outside diff comments:
In `@src/components/shop/ui/Mono.tsx`:
- Around line 3-7: The top JSDoc comment in src/components/shop/ui/Mono.tsx is
stale — it still mentions "JetBrains Mono, 10.5px" while the implementation uses
the font token font-shop-mono (DM Mono) and text-shop-xs; update that JSDoc to
match the actual implementation (use DM Mono and text-shop-xs) so it aligns with
the ShopMono comment and avoids divergent documentation for the Mono/ShopMono
helpers.

In `@src/utils/shopify-format.ts`:
- Around line 6-13: formatMoney currently forces maximumFractionDigits: 0 which
rounds non‑whole totals; change formatMoney(amount, currencyCode) to remove the
minimumFractionDigits/maximumFractionDigits defaults and instead accept an
optional third param (e.g. forceZeroDecimals = false); when forceZeroDecimals is
true set both minimumFractionDigits: 0 and maximumFractionDigits: 0 to preserve
the old behavior for product displays, otherwise let Intl.NumberFormat use
locale defaults so cart subtotals/totals remain precise. Update call sites that
expect trimmed zeros (ProductCard.tsx, ProductDrawer.tsx) to pass true and leave
CartDrawer.tsx and shop.cart.tsx unchanged.

---

Nitpick comments:
In `@src/components/shop/ProductDrawer.tsx`:
- Around line 68-75: Remove the unused contrastColor function and eliminate the
dead props from DrawerContent (remove allHandles, onNavigate, onClose from its
parameter list and internal usage), then update the DrawerBody -> <DrawerContent
... /> call site to only pass the required product prop (ensure DrawerContent's
type/signature and any prop interfaces are updated accordingly); also delete any
other duplicated unused variants of contrastColor around lines ~368-378.

In `@src/styles/shop.css`:
- Around line 16-35: Remove the duplicate `@theme` inline block in shop.css:
delete the entire `@theme` inline { ... } that defines the CSS custom properties
(including --font-shop-display and --font-shop-mono and the color tokens) and
keep only the runtime rules under .shop-scope and html.dark .shop-scope; ensure
color tokens used at runtime (e.g., --color-shop-bg, --color-shop-surface,
--color-shop-surface-hover, --color-shop-accent, --color-shop-green/orange,
--color-shop-text/muted, and --color-shop-line variants) remain in the
.shop-scope rules, and do not re-add the font variables (--font-shop-display,
--font-shop-mono) since they are already declared in app.css's `@theme` block.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ac200810-1411-4f58-99d2-fcd807aa5f2d

📥 Commits

Reviewing files that changed from the base of the PR and between 63d81b8 and 8c50199.

📒 Files selected for processing (17)
  • src/components/shop/ProductCard.tsx
  • src/components/shop/ProductDrawer.tsx
  • src/components/shop/ShopHero.tsx
  • src/components/shop/ShopLayout.tsx
  • src/components/shop/ui/Badge.tsx
  • src/components/shop/ui/Button.tsx
  • src/components/shop/ui/Chip.tsx
  • src/components/shop/ui/Mono.tsx
  • src/components/shop/ui/Select.tsx
  • src/components/shop/ui/Size.tsx
  • src/components/shop/ui/Tab.tsx
  • src/routes/shop.index.tsx
  • src/routes/shop.tsx
  • src/styles/app.css
  • src/styles/shop.css
  • src/utils/shopify-format.ts
  • src/utils/shopify-queries.ts

import { formatMoney, shopifyImageUrl } from '~/utils/shopify-format'
import type { ProductListItem } from '~/utils/shopify-queries'

const TWO_WEEKS_MS = 365 * 24 * 60 * 60 * 1000
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Critical: TWO_WEEKS_MS is actually one year

365 * 24 * 60 * 60 * 1000 is 365 days — not 14 days. The "NEW" badge will be applied to every product published within the last year. The constant should multiply 14 days instead.

🐛 Proposed fix
-const TWO_WEEKS_MS = 365 * 24 * 60 * 60 * 1000
+const TWO_WEEKS_MS = 14 * 24 * 60 * 60 * 1000
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const TWO_WEEKS_MS = 365 * 24 * 60 * 60 * 1000
const TWO_WEEKS_MS = 14 * 24 * 60 * 60 * 1000
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/shop/ProductCard.tsx` at line 8, TWO_WEEKS_MS is miscalculated
as one year; change its value to represent 14 days by using 14 * 24 * 60 * 60 *
1000 (or compute from a DAY_MS constant) in ProductCard (symbol: TWO_WEEKS_MS)
so the "NEW" badge logic uses a two-week window rather than 365 days.

Comment on lines +10 to +77
// Kept in sync with ProductDrawer COLOR_HEX — last token wins ("Vintage Black" → black)
const COLOR_MAP: Record<string, string> = {
black: '#0a0a0a',
white: '#f5f5f0',
cream: '#e4dcc4',
bone: '#e4dcc4',
natural: '#ddd3b8',
vintage: '#e8e0d0',
fog: '#c9c6ba',
sand: '#c8b97a',
ink: '#16130d',
navy: '#1a2e50',
slate: '#2e3339',
olive: '#5a5a3a',
rust: '#b84a27',
red: '#c41d1d',
blue: '#1d4ed8',
sea: '#3a5d66',
green: '#15803d',
gray: '#6b7280',
grey: '#6b7280',
charcoal: '#3a3a3c',
heather: '#8a8a9a',
denim: '#1a4569',
brown: '#6b3a2a',
pink: '#e8749a',
purple: '#7c3aed',
yellow: '#ca8a04',
orange: '#c2410c',
royal: '#4169e1',
kelly: '#4daa59',
aqua: '#00c4d4',
rose: '#c8818a',
dusty: '#c8818a',
coral: '#e8756a',
forest: '#228b22',
teal: '#0d9488',
lavender: '#967bb6',
lilac: '#967bb6',
tan: '#d2b48c',
ivory: '#fffff0',
gold: '#c9a227',
silver: '#a8a9ad',
ash: '#b2bec3',
stone: '#78716c',
moss: '#6b7c55',
sage: '#87a878',
sky: '#0ea5e9',
midnight: '#1e1b4b',
espresso: '#3c1f0f',
// card-specific
mixed: '#ef4c7a',
holo: '#d6e7ff',
polished: '#c5b07a',
blend: '#e8e0d0',
}

// Last token wins: "Vintage Black" → ["vintage","black"] reversed → "black" wins
function colorHex(name: string): string | undefined {
const tokens = name
.toLowerCase()
.split(/[\s_-]+/)
.reverse()
for (const token of tokens) {
if (COLOR_MAP[token]) return COLOR_MAP[token]
}
return undefined
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Duplicated COLOR_MAP with ProductDrawer.COLOR_HEX is already drifting

The comment on line 10 claims this is kept in sync with ProductDrawer.COLOR_HEX, but the two maps already disagree: this card map has mixed/holo/polished/blend (card-specific), and the drawer map adds maroon. The colorHex resolver here also lower-cases and tokenizes differently (no full-string lookup before token reverse) than resolveColorHex in the drawer. Future fixes will silently drift further.

Consider extracting both the map and the resolver into a small shared module (e.g. src/components/shop/colorTokens.ts) and importing it from both ProductCard.tsx and ProductDrawer.tsx.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/shop/ProductCard.tsx` around lines 10 - 77, ProductCard.tsx
duplicates COLOR_MAP and a different resolver (colorHex) from
ProductDrawer.COLOR_HEX/resolveColorHex causing drift; extract a single shared
module (e.g. src/components/shop/colorTokens.ts) that exports the canonical
COLOR_MAP and a single resolver function (e.g. resolveColorHex) that does a
full-string lookup then tokenized reverse lookup, include all tokens used by
both card and drawer (add card-only keys like mixed/holo/polished/blend and
drawer-only keys like maroon) and update ProductCard.tsx to import
COLOR_MAP/resolveColorHex (replace colorHex) and ProductDrawer.tsx to import the
same exports so both components use the identical map and lookup behavior.

Comment on lines +381 to +397
const [selected, setSelected] = React.useState<Record<string, string>>(() =>
Object.fromEntries(product.options.map((o) => [o.name, ''])),
)
const [quantity, setQuantity] = React.useState(1)
const [activeImageIndex, setActiveImageIndex] = React.useState(0)
const [showAdded, setShowAdded] = React.useState(false)
// heroOverride: set to variant image when user picks a color; cleared on thumbnail click
const [heroOverride, setHeroOverride] = React.useState<
(typeof product.images.nodes)[0] | null
>(null)

// Exact variant for add-to-cart; wildcard findMatchingVariant used only for chip availability
const selectedVariant = findExactVariant(variants, selected)
// True once the user has explicitly picked every option (color, size, etc.)
const isComplete = product.options
.filter((o) => o.values.length > 1)
.every((o) => !!selected[o.name])
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Critical: products with single-value options can't be added to cart

selected is seeded with '' for every option. UI rendering (line 498) and isComplete (line 396) both filter to values.length > 1, so a single-value option (e.g. Title: Default Title, very common on Shopify accessory products) is never user-selectable — selected[name] stays ''. But findExactVariant (line 113) requires selected[o.name] === o.value for every variant selectedOption, including the single-value one. Result: selectedVariant === undefined, the "Add to Cart" button stays disabled (!selectedVariant?.availableForSale), and onClick's if (!selectedVariant) return swallows the click silently.

Auto-select single-value options at initialization so they participate in exact matching:

🐛 Proposed fix
   const [selected, setSelected] = React.useState<Record<string, string>>(() =>
-    Object.fromEntries(product.options.map((o) => [o.name, ''])),
+    Object.fromEntries(
+      product.options.map((o) => [
+        o.name,
+        o.values.length === 1 ? o.values[0] : '',
+      ]),
+    ),
   )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const [selected, setSelected] = React.useState<Record<string, string>>(() =>
Object.fromEntries(product.options.map((o) => [o.name, ''])),
)
const [quantity, setQuantity] = React.useState(1)
const [activeImageIndex, setActiveImageIndex] = React.useState(0)
const [showAdded, setShowAdded] = React.useState(false)
// heroOverride: set to variant image when user picks a color; cleared on thumbnail click
const [heroOverride, setHeroOverride] = React.useState<
(typeof product.images.nodes)[0] | null
>(null)
// Exact variant for add-to-cart; wildcard findMatchingVariant used only for chip availability
const selectedVariant = findExactVariant(variants, selected)
// True once the user has explicitly picked every option (color, size, etc.)
const isComplete = product.options
.filter((o) => o.values.length > 1)
.every((o) => !!selected[o.name])
const [selected, setSelected] = React.useState<Record<string, string>>(() =>
Object.fromEntries(
product.options.map((o) => [
o.name,
o.values.length === 1 ? o.values[0] : '',
]),
),
)
const [quantity, setQuantity] = React.useState(1)
const [activeImageIndex, setActiveImageIndex] = React.useState(0)
const [showAdded, setShowAdded] = React.useState(false)
// heroOverride: set to variant image when user picks a color; cleared on thumbnail click
const [heroOverride, setHeroOverride] = React.useState<
(typeof product.images.nodes)[0] | null
>(null)
// Exact variant for add-to-cart; wildcard findMatchingVariant used only for chip availability
const selectedVariant = findExactVariant(variants, selected)
// True once the user has explicitly picked every option (color, size, etc.)
const isComplete = product.options
.filter((o) => o.values.length > 1)
.every((o) => !!selected[o.name])
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/shop/ProductDrawer.tsx` around lines 381 - 397, Seed the
selected state so single-value options are auto-picked: when initializing
selected in ProductDrawer, set each option's value to its sole value if
option.values.length === 1, otherwise ''. Update the initializer that currently
uses Object.fromEntries(product.options.map((o) => [o.name, ''])) to inspect
product.options (and each option.values[0]) so findExactVariant can match every
variant option; this will make selectedVariant (computed via findExactVariant)
resolve for single-value-option products and restore Add to Cart behavior while
leaving isComplete logic and heroOverride unchanged.

Comment on lines +611 to +631
<div className="bg-shop-surface flex h-[38px] items-center justify-center gap-4 px-4 rounded-full w-[100px] font-shop-mono select-none">
<button
type="button"
onClick={() => setQuantity((q) => q + 1)}
aria-label="Increase quantity"
className="text-shop-sm text-shop-text-2 leading-none hover:text-shop-text transition-colors"
>
+
</button>
<span className="text-shop-sm text-shop-text min-w-[1ch] text-center">
{quantity}
</span>
<button
type="button"
onClick={() => setQuantity((q) => Math.max(1, q - 1))}
aria-label="Decrease quantity"
className="text-shop-sm text-shop-text-2 leading-none hover:text-shop-text transition-colors"
>
</button>
</div>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Quantity stepper render order is reversed from convention

The buttons render [+] qty [−] left-to-right. Standard horizontal-stepper convention places minus on the left and plus on the right — a stepper "often consists of a minus button, a numeric value, and a plus button", and per Nielsen Norman Group, the plus segment is usually positioned to the right of (or above) the value and the minus segment is placed to the left (or below). Users will reach for the left button to decrement and accidentally increment instead.

♻️ Proposed reorder
-              <button
-                type="button"
-                onClick={() => setQuantity((q) => q + 1)}
-                aria-label="Increase quantity"
-                className="text-shop-sm text-shop-text-2 leading-none hover:text-shop-text transition-colors"
-              >
-                +
-              </button>
-              <span className="text-shop-sm text-shop-text min-w-[1ch] text-center">
-                {quantity}
-              </span>
               <button
                 type="button"
                 onClick={() => setQuantity((q) => Math.max(1, q - 1))}
                 aria-label="Decrease quantity"
                 className="text-shop-sm text-shop-text-2 leading-none hover:text-shop-text transition-colors"
               >
                 −
               </button>
+              <span className="text-shop-sm text-shop-text min-w-[1ch] text-center">
+                {quantity}
+              </span>
+              <button
+                type="button"
+                onClick={() => setQuantity((q) => q + 1)}
+                aria-label="Increase quantity"
+                className="text-shop-sm text-shop-text-2 leading-none hover:text-shop-text transition-colors"
+              >
+                +
+              </button>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<div className="bg-shop-surface flex h-[38px] items-center justify-center gap-4 px-4 rounded-full w-[100px] font-shop-mono select-none">
<button
type="button"
onClick={() => setQuantity((q) => q + 1)}
aria-label="Increase quantity"
className="text-shop-sm text-shop-text-2 leading-none hover:text-shop-text transition-colors"
>
+
</button>
<span className="text-shop-sm text-shop-text min-w-[1ch] text-center">
{quantity}
</span>
<button
type="button"
onClick={() => setQuantity((q) => Math.max(1, q - 1))}
aria-label="Decrease quantity"
className="text-shop-sm text-shop-text-2 leading-none hover:text-shop-text transition-colors"
>
</button>
</div>
<div className="bg-shop-surface flex h-[38px] items-center justify-center gap-4 px-4 rounded-full w-[100px] font-shop-mono select-none">
<button
type="button"
onClick={() => setQuantity((q) => Math.max(1, q - 1))}
aria-label="Decrease quantity"
className="text-shop-sm text-shop-text-2 leading-none hover:text-shop-text transition-colors"
>
</button>
<span className="text-shop-sm text-shop-text min-w-[1ch] text-center">
{quantity}
</span>
<button
type="button"
onClick={() => setQuantity((q) => q + 1)}
aria-label="Increase quantity"
className="text-shop-sm text-shop-text-2 leading-none hover:text-shop-text transition-colors"
>
</button>
</div>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/shop/ProductDrawer.tsx` around lines 611 - 631, The quantity
stepper in ProductDrawer renders the increment button on the left and decrement
on the right; swap their DOM order so the decrement (onClick uses setQuantity(q
=> Math.max(1, q - 1)) / aria-label="Decrease quantity") appears before the
<span>{quantity}</span> and the increment (onClick uses setQuantity(q => q + 1)
/ aria-label="Increase quantity") appears after it, preserving all classes and
handlers on those buttons to follow standard minus-left/plus-right convention.

@tannerlinsley tannerlinsley merged commit 3a4a24b into main May 5, 2026
8 checks passed
@tannerlinsley tannerlinsley deleted the claude/naughty-satoshi-e456cb branch May 5, 2026 20:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants