Skip to content

Commit 21bff8e

Browse files
feat(shop): redesign /shop pages with editorial theme and Tailwind primitives (#843)
Scoped-only rewrite of the /shop landing, PDP, cart, drawer, and collection/search/policy pages to match a new editorial merch-store direction (cyan accent, DM Sans display, JetBrains Mono for prices/meta). Theme tokens live on a `.shop-scope` wrapper with light and dark variants, exposed to Tailwind via `@theme inline` so authoring stays in utility classes. New primitives under `src/components/shop/ui/` (Button, Badge, Chip, Size, Qty, Select, Input, Tab, Panel, Pulse, Mono, Crumb) and composed pieces (Hero, DropCard, Strip, Specs, Note) back the pages. Global site, navbar, and non-shop routes are unchanged; `shop.css` and the DM Sans / JetBrains Mono fonts only load on /shop. `ProductListItem` gains `productType` and `tags` to power the hover tag + New/Low-stock badges.
1 parent 2d1c52c commit 21bff8e

32 files changed

+1377
-564
lines changed

src/components/shop/Breadcrumbs.tsx

Lines changed: 0 additions & 52 deletions
This file was deleted.

src/components/shop/CartDrawer.tsx

Lines changed: 64 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
import * as React from 'react'
21
import * as Dialog from '@radix-ui/react-dialog'
32
import { Link } from '@tanstack/react-router'
43
import { Minus, Plus, ShoppingCart, Trash2, X } from 'lucide-react'
54
import { twMerge } from 'tailwind-merge'
65
import { useCart, useRemoveCartLine, useUpdateCartLine } from '~/hooks/useCart'
76
import { formatMoney, shopifyImageUrl } from '~/utils/shopify-format'
87
import type { CartLineDetail } from '~/utils/shopify-queries'
8+
import { ShopLabel, ShopMono } from './ui'
99

1010
type CartDrawerProps = {
1111
open: boolean
@@ -14,10 +14,8 @@ type CartDrawerProps = {
1414

1515
/**
1616
* Slide-in cart drawer. Shares state with /shop/cart through the same
17-
* useCart React Query key, so adds in the drawer mirror the full page
18-
* and vice-versa. Pinned to the right on desktop; full-width slide-up on
19-
* mobile would be nice later, but a right-anchored sheet is the standard
20-
* Shopify-theme pattern and works on phones too.
17+
* useCart React Query key. The root element wears `shop-scope` because the
18+
* drawer is portaled outside the ShopLayout tree.
2119
*/
2220
export function CartDrawer({ open, onOpenChange }: CartDrawerProps) {
2321
const { cart, totalQuantity } = useCart()
@@ -26,34 +24,36 @@ export function CartDrawer({ open, onOpenChange }: CartDrawerProps) {
2624
return (
2725
<Dialog.Root open={open} onOpenChange={onOpenChange}>
2826
<Dialog.Portal>
29-
<Dialog.Overlay className="cart-overlay fixed inset-0 z-[100] bg-black/20" />
27+
<Dialog.Overlay className="cart-overlay fixed inset-0 z-[100] bg-black/40" />
3028
<Dialog.Content
3129
className={twMerge(
32-
'cart-panel',
30+
'shop-scope cart-panel',
3331
'fixed right-4 top-[calc(var(--navbar-height,56px)+0.5rem)] z-[100]',
3432
'w-[calc(100vw-2rem)] sm:w-[24rem]',
3533
'max-h-[calc(100dvh-var(--navbar-height,56px)-1rem)]',
36-
'flex flex-col',
37-
'rounded-xl shadow-2xl',
38-
'bg-white dark:bg-gray-950 border border-gray-200 dark:border-gray-800',
34+
'flex flex-col rounded-xl',
35+
'bg-shop-bg-2 border border-shop-line text-shop-text',
36+
'shadow-[0_25px_50px_-12px_rgba(0,0,0,0.4)]',
3937
)}
4038
aria-describedby={undefined}
4139
>
42-
<header className="flex items-center justify-between px-5 py-3 border-b border-gray-200 dark:border-gray-800">
43-
<Dialog.Title className="font-semibold text-sm">
44-
Cart{totalQuantity > 0 ? ` (${totalQuantity})` : ''}
40+
<header className="flex items-center justify-between px-5 py-3 border-b border-shop-line">
41+
<Dialog.Title asChild>
42+
<ShopLabel as="h2">
43+
Cart{totalQuantity > 0 ? ` (${totalQuantity})` : ''}
44+
</ShopLabel>
4545
</Dialog.Title>
4646
<Dialog.Close
47-
className="p-1 rounded-md hover:bg-gray-100 dark:hover:bg-gray-900"
4847
aria-label="Close cart"
48+
className="p-1 rounded-md text-shop-text-2 hover:text-shop-text"
4949
>
5050
<X className="w-3.5 h-3.5" />
5151
</Dialog.Close>
5252
</header>
5353

5454
{hasLines ? (
5555
<>
56-
<ul className="overflow-y-auto px-5 divide-y divide-gray-200 dark:divide-gray-800">
56+
<ul className="overflow-y-auto px-5 flex-1 min-h-0">
5757
{cart.lines.nodes.map((line) => (
5858
<DrawerCartLine
5959
key={line.id}
@@ -75,15 +75,22 @@ export function CartDrawer({ open, onOpenChange }: CartDrawerProps) {
7575

7676
function DrawerEmpty({ onClose }: { onClose: () => void }) {
7777
return (
78-
<div className="flex-1 flex flex-col items-center justify-center gap-4 p-6 text-center">
79-
<ShoppingCart className="w-10 h-10 text-gray-400" />
80-
<p className="text-gray-600 dark:text-gray-400">Your cart is empty.</p>
78+
<div className="flex-1 flex flex-col items-center justify-center gap-4 p-8 text-center text-shop-text-2">
79+
<ShoppingCart className="w-10 h-10 text-shop-muted" />
80+
<p>Your cart is empty.</p>
8181
<Link
8282
to="/shop"
8383
onClick={onClose}
84-
className="inline-flex items-center px-4 py-2 rounded-lg bg-black text-white dark:bg-white dark:text-black font-semibold"
84+
className="
85+
inline-flex items-center gap-2 px-4 py-2.5 rounded-md
86+
bg-shop-accent text-shop-accent-ink font-semibold text-[13px]
87+
transition-[filter] hover:brightness-110 group
88+
"
8589
>
8690
Shop all products
91+
<span className="transition-transform group-hover:translate-x-[3px]">
92+
93+
</span>
8794
</Link>
8895
</div>
8996
)
@@ -98,26 +105,33 @@ function DrawerFooter({
98105
}) {
99106
const subtotal = cart.cost.subtotalAmount
100107
return (
101-
<footer className="border-t border-gray-200 dark:border-gray-800 px-6 py-4 flex flex-col gap-3">
108+
<footer className="px-5 py-4 flex flex-col gap-3 border-t border-shop-line">
102109
<div className="flex justify-between text-sm">
103-
<span className="text-gray-600 dark:text-gray-400">Subtotal</span>
104-
<span className="font-semibold">
110+
<span className="text-shop-text-2">Subtotal</span>
111+
<ShopMono className="font-medium text-shop-text">
105112
{formatMoney(subtotal.amount, subtotal.currencyCode)}
106-
</span>
113+
</ShopMono>
107114
</div>
108-
<p className="text-xs text-gray-500 dark:text-gray-500">
115+
<p className="font-shop-mono text-xs text-shop-muted tracking-[0.06em]">
109116
Shipping and taxes calculated at checkout.
110117
</p>
111118
<a
112119
href={cart.checkoutUrl}
113-
className="w-full text-center px-6 py-3 rounded-lg bg-black text-white dark:bg-white dark:text-black font-semibold"
120+
className="
121+
w-full h-10 rounded-md bg-shop-accent text-shop-accent-ink
122+
font-semibold text-[13px] flex items-center justify-center gap-2
123+
transition-[filter] hover:brightness-110 group
124+
"
114125
>
115126
Checkout
127+
<span className="transition-transform group-hover:translate-x-[3px]">
128+
129+
</span>
116130
</a>
117131
<Link
118132
to="/shop/cart"
119133
onClick={onClose}
120-
className="w-full text-center text-sm text-gray-600 dark:text-gray-400 hover:underline"
134+
className="text-center text-sm text-shop-text-2 hover:underline"
121135
>
122136
View cart
123137
</Link>
@@ -143,42 +157,40 @@ function DrawerCartLine({
143157
const isBusy = update.isPending || remove.isPending
144158

145159
return (
146-
<li className="flex gap-3 py-4">
160+
<li className="flex gap-3 py-4 border-b border-shop-line">
147161
<Link
148162
to="/shop/products/$handle"
149163
params={{ handle: merchandise.product.handle }}
150164
onClick={onClose}
151-
className="shrink-0"
165+
className="shrink-0 w-16 h-16 rounded-md border border-shop-line bg-shop-panel overflow-hidden"
152166
>
153-
<div className="w-16 h-16 rounded-lg overflow-hidden bg-gray-100 dark:bg-gray-900">
154-
{merchandise.image ? (
155-
<img
156-
src={shopifyImageUrl(merchandise.image.url, {
157-
width: 160,
158-
format: 'webp',
159-
})}
160-
alt={merchandise.image.altText ?? merchandise.product.title}
161-
className="h-full w-full object-cover"
162-
/>
163-
) : null}
164-
</div>
167+
{merchandise.image ? (
168+
<img
169+
src={shopifyImageUrl(merchandise.image.url, {
170+
width: 160,
171+
format: 'webp',
172+
})}
173+
alt={merchandise.image.altText ?? merchandise.product.title}
174+
className="w-full h-full object-cover"
175+
/>
176+
) : null}
165177
</Link>
166178
<div className="flex-1 min-w-0 flex flex-col gap-1">
167179
<Link
168180
to="/shop/products/$handle"
169181
params={{ handle: merchandise.product.handle }}
170182
onClick={onClose}
171-
className="text-sm font-semibold hover:underline truncate"
183+
className="text-sm font-semibold text-shop-text hover:underline truncate"
172184
>
173185
{merchandise.product.title}
174186
</Link>
175187
{options ? (
176-
<p className="text-xs text-gray-600 dark:text-gray-400 truncate">
188+
<ShopMono className="block text-xs text-shop-muted truncate">
177189
{options}
178-
</p>
190+
</ShopMono>
179191
) : null}
180192
<div className="flex items-center justify-between mt-1">
181-
<div className="inline-flex items-center rounded-md border border-gray-200 dark:border-gray-800 text-xs">
193+
<div className="inline-flex items-center border border-shop-line rounded-md text-xs">
182194
<button
183195
type="button"
184196
onClick={() => {
@@ -193,11 +205,13 @@ function DrawerCartLine({
193205
}}
194206
disabled={isBusy}
195207
aria-label="Decrease quantity"
196-
className="p-1.5 disabled:opacity-50"
208+
className="p-1.5 text-shop-text-2 hover:text-shop-text disabled:opacity-50"
197209
>
198210
<Minus className="w-3 h-3" />
199211
</button>
200-
<span className="min-w-[1.5rem] text-center">{line.quantity}</span>
212+
<span className="font-shop-mono text-shop-text min-w-[1.5rem] text-center">
213+
{line.quantity}
214+
</span>
201215
<button
202216
type="button"
203217
onClick={() =>
@@ -208,24 +222,24 @@ function DrawerCartLine({
208222
}
209223
disabled={isBusy}
210224
aria-label="Increase quantity"
211-
className="p-1.5 disabled:opacity-50"
225+
className="p-1.5 text-shop-text-2 hover:text-shop-text disabled:opacity-50"
212226
>
213227
<Plus className="w-3 h-3" />
214228
</button>
215229
</div>
216230
<div className="flex items-center gap-2">
217-
<span className="text-sm font-semibold">
231+
<ShopMono className="text-sm font-medium text-shop-text">
218232
{formatMoney(
219233
line.cost.totalAmount.amount,
220234
line.cost.totalAmount.currencyCode,
221235
)}
222-
</span>
236+
</ShopMono>
223237
<button
224238
type="button"
225239
onClick={() => remove.mutate({ lineId: line.id })}
226240
disabled={isBusy}
227241
aria-label="Remove from cart"
228-
className="p-1 rounded-md text-gray-500 hover:bg-gray-100 dark:hover:bg-gray-900 disabled:opacity-50"
242+
className="p-1 rounded-md text-shop-muted hover:text-shop-text disabled:opacity-50"
229243
>
230244
<Trash2 className="w-3.5 h-3.5" />
231245
</button>

0 commit comments

Comments
 (0)