@develemit/storefront 0.6.0 → 0.7.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/CHANGELOG.md +91 -0
- package/README.md +17 -95
- package/dist/components/checkout-success/checkout-success-view.d.ts +16 -1
- package/dist/components/checkout-success/labels.d.ts +3 -0
- package/dist/index.js +41 -19
- package/dist/index.js.map +1 -1
- package/dist/styles.css +19 -0
- package/docs/cart.md +110 -0
- package/package.json +12 -3
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
## 0.7.0 (2026-11-04)
|
|
2
|
+
|
|
3
|
+
### Features
|
|
4
|
+
|
|
5
|
+
- `CheckoutSuccessView` gains `slots` (`header`, `beforeLines`, `afterTotals`, `actions`), `showReference`, `showSummaryHeading`, `continueHref` and `renderLink`, plus the labels `summaryHeading`, `reference` (`{id}` placeholder) and `continue`. All additive; the view still has no hooks.
|
|
6
|
+
|
|
7
|
+
### Docs
|
|
8
|
+
|
|
9
|
+
- The package now ships `CHANGELOG.md` and `docs/cart.md` (cart, checkout, success page and discount codes, moved out of the README), and declares `description`, `repository` and `homepage`.
|
|
10
|
+
|
|
11
|
+
## 0.6.0 (2026-11-03)
|
|
12
|
+
|
|
13
|
+
No changes; released in lockstep with the other packages.
|
|
14
|
+
|
|
15
|
+
## 0.5.0 (2026-11-02)
|
|
16
|
+
|
|
17
|
+
### Features
|
|
18
|
+
|
|
19
|
+
- `CheckoutSuccessView({ order, locale?, labels?, className? })` renders the order summary for a checkout success page (lines, discount, shipping, total, ship-to address, tracking, per-status message), themed by `--sf-*` tokens with new `.sf-checkout-success*` styles. Exports `CheckoutSuccessLabels` and `defaultCheckoutSuccessLabels`.
|
|
20
|
+
- `ClearCartOnMount` empties the cart once after hydration, safe under React strict mode.
|
|
21
|
+
- `useCheckout({ startCheckout, onRedirect?, onDiscountInvalid?, labels? })` returns `{ checkout, busy, message, clearMessage }`: busy state, removal of out-of-stock lines, discount-reason messages and the gateway redirect. Exports `CheckoutLabels`, `defaultCheckoutLabels`, `StartCheckoutOptions`, `UseCheckoutOptions` and `UseCheckoutValue`.
|
|
22
|
+
- `@develemit/storefront/client` exports `toCheckoutResult(error)` and the `CheckoutResult` / `CheckoutFailure` types, so a server action can turn `CheckoutConflictError` and `DiscountInvalidError` into a serializable result without importing React.
|
|
23
|
+
- Exports `discountReasonMessage(reason, overrides?)`, the same reason text `CartSummary` shows for a rejected code, so hosts can show it when checkout rejects a code (`DiscountInvalidError`).
|
|
24
|
+
|
|
25
|
+
## 0.4.0 (2026-10-30)
|
|
26
|
+
|
|
27
|
+
### Features
|
|
28
|
+
|
|
29
|
+
- `client.previewDiscount({ code, items })` and `createCheckout(items, { discountCode })`, with a new `DiscountInvalidError` (422, carries the rejection `reason`). Exports `CreateCheckoutOptions`.
|
|
30
|
+
- `CartSummary` takes an optional `discount={{ onPreview, onApply, debounceMs }}` prop (also through `CartDrawer`'s `summary`): a collapsible "Have a code?" field, a discount row with Remove, reason messages, and a debounced re-preview that drops a code that stops applying. Labels live under `labels.discount`. Exports `CartSummaryDiscount` and `DiscountPreviewResult`. New `.sf-cart-summary__code*` styles. Needs a server with discounts.
|
|
31
|
+
|
|
32
|
+
## 0.3.0 (2026-10-30)
|
|
33
|
+
|
|
34
|
+
### Breaking
|
|
35
|
+
|
|
36
|
+
- `client.listProducts(query)` returns a page, `{ items: ProductCard[], nextCursor }`, instead of an array, and accepts `limit` and `cursor`. `ProductGrid`, `ProductCard`, `ShopView` and `FeaturedProducts` take the slim `ProductCard` shape (a full `Product` is still assignable).
|
|
37
|
+
|
|
38
|
+
### Features
|
|
39
|
+
|
|
40
|
+
- The default `renderImage` now renders `<img loading="lazy" decoding="async">`, or `loading="eager"` with `fetchpriority="high"` when `priority` is set (non-breaking behavior change).
|
|
41
|
+
|
|
42
|
+
- `ShopView` `nextHref` and `loadMoreLabel` render a server-rendered "Load more" link through `renderLink`; hosts build `nextHref` from `nextCursor`.
|
|
43
|
+
- The "Load more" link now has default `--sf-*` token styling (`.sf-shop__more`, centered in a new `.sf-shop__pager` wrapper).
|
|
44
|
+
- Exports the `ProductCardData` and `ProductCardPage` types (`ProductCardData` avoids clashing with the `ProductCard` component).
|
|
45
|
+
|
|
46
|
+
### Fixes
|
|
47
|
+
|
|
48
|
+
- Published tarballs no longer include declaration maps (`*.d.ts.map`), which pointed at `src/` files that were not shipped. JS sourcemaps now embed `sourcesContent`.
|
|
49
|
+
- Built `dist` type resolution: the `client` and `format` entries now emit to `dist/<name>/index.js` beside their declarations, so `allowJs` consumers (Next) get real types instead of `any`. No API change; the `exports` subpaths are unchanged.
|
|
50
|
+
|
|
51
|
+
## 0.2.0 (2026-10-29)
|
|
52
|
+
|
|
53
|
+
First published release. 0.1.0 was tagged but never published to npm, so installing 0.2.0 gets everything in both entries below.
|
|
54
|
+
|
|
55
|
+
### Features
|
|
56
|
+
|
|
57
|
+
- `CartDrawer` `slots` (`empty`, `summaryNote`) for a custom empty-cart state and a note under the subtotal.
|
|
58
|
+
- `CartButton` `icon`, `labelHidden` and `hideZeroCount` props.
|
|
59
|
+
- `--sf-motion-duration` theme variable (default `0ms`) for drawer slide-in/out and backdrop fade; forced to `0ms` under `prefers-reduced-motion: reduce`. The drawer carries `data-state="open" | "closed"`.
|
|
60
|
+
|
|
61
|
+
- Catalog presentation hooks: `--sf-card-hover-lift`, `--sf-card-hover-shadow`, `--sf-card-image-hover-scale`, `--sf-sold-opacity`, `--sf-sold-filter`, `--sf-grid-columns-mobile` and `--sf-color-focus` (all default to the current look).
|
|
62
|
+
- `CategoryFilter` `formatLabel` and `sortCategories`; `ShopView` `allLabel`, `formatCategoryLabel` and `sortCategories`.
|
|
63
|
+
- `ShopView`, `FeaturedProducts` and `ProductDetailView` forward `labels` to the availability badge.
|
|
64
|
+
|
|
65
|
+
### Fixes
|
|
66
|
+
|
|
67
|
+
- `AvailabilityBadge` is sized to its text (`inline-size: fit-content`) instead of stretching in grid/flex columns.
|
|
68
|
+
- Gallery slide and thumb frames are `position: relative`, so `next/image` `fill` renders correctly; the gallery and thumbs show a `--sf-color-focus` focus ring.
|
|
69
|
+
|
|
70
|
+
### Behavior changes
|
|
71
|
+
|
|
72
|
+
- An empty cart no longer renders the summary or a disabled Checkout button.
|
|
73
|
+
- Close is now an icon button (`×`) with `labels.close` as its accessible name, and Close and Remove have a 44×44px minimum target.
|
|
74
|
+
|
|
75
|
+
## 0.1.0 (2026-10-29)
|
|
76
|
+
|
|
77
|
+
First release. Brand-agnostic React UI, cart and typed API client. Peers: `react` and `react-dom` `^19`.
|
|
78
|
+
|
|
79
|
+
### Features
|
|
80
|
+
|
|
81
|
+
- Catalog components: `PriceTag`, `AvailabilityBadge`, `ProductCard`, `ProductGrid`, `CategoryFilter`, `ProductGallery`.
|
|
82
|
+
- Page views: `ShopView`, `ProductDetailView`, `FeaturedProducts`, `sortForDisplay`.
|
|
83
|
+
- Theme contract: `styles.css` ships the `--sf-*` custom properties; override them to brand the kit.
|
|
84
|
+
- Cart: `CartProvider`, `useCart`, a reducer with localStorage persistence, `AddToCartButton`, `CartDrawer`, `CartLineItem`, `CartSummary` and `CartButton`.
|
|
85
|
+
- `@develemit/storefront/client`: the typed API client (`createCommerceClient`, including `createCheckout` and `getOrderBySession`) and its error types.
|
|
86
|
+
- Image priority: `ImageProps.priority` lets `renderImage` adapters mark above-the-fold images (e.g. `next/image` `priority`). `ProductCard` accepts `priority`, `ProductGrid` accepts `priorityCount` (prioritizes the first N cards; `ShopView` and `FeaturedProducts` use 6), and `ProductGallery` prioritizes the active main image but not the thumbnails.
|
|
87
|
+
- `@develemit/storefront/format`: server-safe entry for `formatPrice` and `sortForDisplay`.
|
|
88
|
+
|
|
89
|
+
### Notes
|
|
90
|
+
|
|
91
|
+
- The main bundle is marked `"use client"`. Server components must import pure helpers from `@develemit/storefront/format`.
|
package/README.md
CHANGED
|
@@ -1,16 +1,23 @@
|
|
|
1
1
|
# @develemit/storefront
|
|
2
2
|
|
|
3
|
-
Brand-agnostic storefront UI
|
|
4
|
-
|
|
3
|
+
Brand-agnostic storefront UI, cart, checkout and a typed API client for shops
|
|
4
|
+
backed by `@develemit/commerce-server`. New here? Start with the
|
|
5
|
+
[getting-started guide](https://github.com/develemit/develemit-commerce/blob/main/docs/getting-started.md).
|
|
6
|
+
What changed in each version is in [CHANGELOG.md](CHANGELOG.md).
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
pnpm add @develemit/storefront @develemit/commerce-contracts react react-dom
|
|
10
|
+
```
|
|
5
11
|
|
|
6
12
|
## Boundary rules
|
|
7
13
|
|
|
8
14
|
- No client branding, copy, colors, or product data in this package.
|
|
9
15
|
The consuming app supplies those through props, theme tokens, and data adapters.
|
|
10
16
|
- No imports from `apps/*` or other app-specific packages.
|
|
11
|
-
- React
|
|
17
|
+
- React and React DOM are peer dependencies, never bundled. Next is optional:
|
|
18
|
+
it is wired in through the adapters below, never imported.
|
|
12
19
|
|
|
13
|
-
##
|
|
20
|
+
## Developing this package
|
|
14
21
|
|
|
15
22
|
- `pnpm exec nx build storefront` — library build to `packages/storefront/dist`
|
|
16
23
|
- `pnpm exec nx test storefront`
|
|
@@ -49,7 +56,8 @@ the variables in the host app (or pass a `StorefrontTheme` through
|
|
|
49
56
|
`PriceTag`, `AvailabilityBadge`, `ProductCard`, `ProductGrid`,
|
|
50
57
|
`CategoryFilter`, and `ProductGallery` are styled only through the `--sf-*`
|
|
51
58
|
variables. Wrap them in `StorefrontProvider` to supply framework adapters;
|
|
52
|
-
without it they fall back to plain `<a>` and `<img>`.
|
|
59
|
+
without it they fall back to plain `<a>` and `<img>`. `useStorefront()` reads
|
|
60
|
+
the provider's config (locale, adapters, hrefs) in your own components.
|
|
53
61
|
|
|
54
62
|
Example Next.js host setup (host code, not part of this package):
|
|
55
63
|
|
|
@@ -215,97 +223,11 @@ export default async function ProductPage({
|
|
|
215
223
|
}
|
|
216
224
|
```
|
|
217
225
|
|
|
218
|
-
## Cart
|
|
219
|
-
|
|
220
|
-
Wrap the app in `CartProvider` (inside `StorefrontProvider`). State is a
|
|
221
|
-
snapshot of `CartLine`s; prices and availability are re-verified server-side
|
|
222
|
-
at checkout. Persistence goes through a `CartStorage` (`load`/`save`); the
|
|
223
|
-
default is `localStorageCartStorage(key)`, which hydrates after mount and never
|
|
224
|
-
touches `window` during render.
|
|
225
|
-
|
|
226
|
-
```tsx
|
|
227
|
-
<CartProvider>
|
|
228
|
-
<CartButton />
|
|
229
|
-
<CartDrawer summary={{ onCheckout, checkoutLabel: 'Checkout' }} />
|
|
230
|
-
</CartProvider>
|
|
231
|
-
|
|
232
|
-
<ProductDetailView
|
|
233
|
-
product={product}
|
|
234
|
-
slots={{ actions: <AddToCartButton product={product} /> }}
|
|
235
|
-
/>
|
|
236
|
-
```
|
|
237
|
-
|
|
238
|
-
`useCart()` exposes `add` (returns `{ added, capped, reason? }`), `remove`,
|
|
239
|
-
`setQuantity`, `clear`, `open`/`close`, and the derived `itemCount` and
|
|
240
|
-
`subtotalCents`. A product's `quantity` is the line's `maxQuantity`, so
|
|
241
|
-
one-of-a-kind items can only be added once. Every label is overridable
|
|
242
|
-
through a `labels` prop.
|
|
243
|
-
|
|
244
|
-
### Checkout
|
|
245
|
-
|
|
246
|
-
`useCheckout({ startCheckout, onRedirect?, onDiscountInvalid?, labels? })`
|
|
247
|
-
wraps the host's checkout call (a server action or a fetch) and returns
|
|
248
|
-
`{ checkout, busy, message, clearMessage }`. `startCheckout(items, { discountCode })`
|
|
249
|
-
resolves to a `CheckoutResult`: `{ url }`, `{ error: 'out_of_stock', productIds }`
|
|
250
|
-
or `{ error: 'discount_invalid', reason }`. On the server, `toCheckoutResult(error)`
|
|
251
|
-
from `@develemit/storefront/client` maps `CheckoutConflictError` and
|
|
252
|
-
`DiscountInvalidError` to those results (other errors are rethrown). The hook
|
|
253
|
-
removes out-of-stock lines, sets `message`, and redirects with
|
|
254
|
-
`window.location.assign` unless `onRedirect` is given. Copy is overridable
|
|
255
|
-
through `labels` (`outOfStock`, `unavailable`, `discountReasons`).
|
|
256
|
-
|
|
257
|
-
### Success page
|
|
258
|
-
|
|
259
|
-
`<CheckoutSuccessView order={order} locale? labels? />` renders an order from
|
|
260
|
-
`getOrderBySession`: lines, subtotal, discount row (only when applied),
|
|
261
|
-
shipping, total, ship-to address (when the order includes `shippingAddress`),
|
|
262
|
-
tracking, and a message per `OrderStatus` (`pending` tells the buyer to expect
|
|
263
|
-
an email). It has no hooks, so a server component can render it. Labels live in
|
|
264
|
-
`labels` (including `labels.status`). `<ClearCartOnMount />` empties the cart
|
|
265
|
-
once, even under React strict mode. Styles are `.sf-checkout-success*`.
|
|
266
|
-
|
|
267
|
-
### Discount codes
|
|
268
|
-
|
|
269
|
-
`CartSummary` (and `CartDrawer` through `summary`) takes an optional `discount`
|
|
270
|
-
prop. The kit stays framework-agnostic, so the host performs the API call (a
|
|
271
|
-
server action in Next):
|
|
272
|
-
|
|
273
|
-
```tsx
|
|
274
|
-
<CartDrawer
|
|
275
|
-
summary={{
|
|
276
|
-
onCheckout: checkout,
|
|
277
|
-
discount: {
|
|
278
|
-
onPreview: async (code, items) => {
|
|
279
|
-
// call client.previewDiscount; map DiscountInvalidError to a reason
|
|
280
|
-
return { ok: true, code, discountCents: 480 };
|
|
281
|
-
// or { ok: false, reason: 'exhausted' }
|
|
282
|
-
},
|
|
283
|
-
onApply: (code) => setDiscountCode(code), // null when removed or dropped
|
|
284
|
-
},
|
|
285
|
-
}}
|
|
286
|
-
/>
|
|
287
|
-
```
|
|
226
|
+
## Cart and checkout
|
|
288
227
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
with its reason if it no longer applies. Pass the applied code to
|
|
293
|
-
`createCheckout`. Override copy with `labels.discount` (`toggle`, `field`,
|
|
294
|
-
`apply`, `remove`, `line`, `unavailable`, and `reasons` per rejection reason).
|
|
295
|
-
|
|
296
|
-
### Polish hooks
|
|
297
|
-
|
|
298
|
-
- `CartDrawer` `slots={{ empty, summaryNote }}`: `empty` replaces the
|
|
299
|
-
`labels.empty` text (for example with a link back to the shop); `summaryNote`
|
|
300
|
-
renders under the subtotal and Checkout button. An empty cart shows no
|
|
301
|
-
summary or Checkout button. Close and Remove are at least 44×44px.
|
|
302
|
-
- `CartButton` `icon` (rendered before the label), `labelHidden` (visually
|
|
303
|
-
hides the label, keeping an accessible name like "Cart, 2 items") and
|
|
304
|
-
`hideZeroCount` (omits the count at 0; the accessible name becomes
|
|
305
|
-
"Cart, empty").
|
|
306
|
-
- `--sf-motion-duration` (default `0ms`, no motion) drives the drawer's
|
|
307
|
-
slide-in and the backdrop's fade, in and out. It is forced to `0ms` under
|
|
308
|
-
`prefers-reduced-motion: reduce`. Example: `--sf-motion-duration: 200ms`.
|
|
228
|
+
`CartProvider`, `CartButton`, `CartDrawer`, `AddToCartButton`, `useCart`,
|
|
229
|
+
`useCheckout`, `CheckoutSuccessView`, `ClearCartOnMount` and discount codes are
|
|
230
|
+
documented in [docs/cart.md](docs/cart.md).
|
|
309
231
|
|
|
310
232
|
### Catalog presentation hooks
|
|
311
233
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ReactNode } from 'react';
|
|
1
2
|
import { OrderAddress, OrderSummary } from '@develemit/commerce-contracts';
|
|
2
3
|
import { CheckoutSuccessLabels } from './labels';
|
|
3
4
|
export interface CheckoutSuccessViewProps {
|
|
@@ -9,5 +10,19 @@ export interface CheckoutSuccessViewProps {
|
|
|
9
10
|
status?: Partial<CheckoutSuccessLabels['status']>;
|
|
10
11
|
};
|
|
11
12
|
className?: string;
|
|
13
|
+
slots?: {
|
|
14
|
+
header?: ReactNode;
|
|
15
|
+
beforeLines?: ReactNode;
|
|
16
|
+
afterTotals?: ReactNode;
|
|
17
|
+
actions?: ReactNode;
|
|
18
|
+
};
|
|
19
|
+
showReference?: boolean;
|
|
20
|
+
showSummaryHeading?: boolean;
|
|
21
|
+
continueHref?: string;
|
|
22
|
+
renderLink?: (props: {
|
|
23
|
+
href: string;
|
|
24
|
+
children: ReactNode;
|
|
25
|
+
className?: string;
|
|
26
|
+
}) => ReactNode;
|
|
12
27
|
}
|
|
13
|
-
export declare function CheckoutSuccessView({ order, locale, labels, className, }: CheckoutSuccessViewProps): import('react').JSX.Element;
|
|
28
|
+
export declare function CheckoutSuccessView({ order, locale, labels, className, slots, showReference, showSummaryHeading, continueHref, renderLink, }: CheckoutSuccessViewProps): import('react').JSX.Element;
|
package/dist/index.js
CHANGED
|
@@ -883,6 +883,9 @@ function ie({ label: e = "Cart", className: t, icon: n, labelHidden: r = !1, hid
|
|
|
883
883
|
// src/components/checkout-success/labels.ts
|
|
884
884
|
var $ = {
|
|
885
885
|
heading: "Order received",
|
|
886
|
+
summaryHeading: "Order summary",
|
|
887
|
+
reference: "Order {id}",
|
|
888
|
+
continue: "Continue shopping",
|
|
886
889
|
subtotal: "Subtotal",
|
|
887
890
|
discount: "Discount",
|
|
888
891
|
shipping: "Shipping",
|
|
@@ -914,27 +917,41 @@ function ae(e) {
|
|
|
914
917
|
e.country
|
|
915
918
|
].filter((e) => !!e);
|
|
916
919
|
}
|
|
917
|
-
function oe({ order: t, locale: n, labels: r, className: i }) {
|
|
918
|
-
let
|
|
920
|
+
function oe({ order: t, locale: n, labels: r, className: i, slots: a, showReference: o, showSummaryHeading: s, continueHref: c, renderLink: l }) {
|
|
921
|
+
let u = {
|
|
919
922
|
...$,
|
|
920
923
|
...r,
|
|
921
924
|
status: {
|
|
922
925
|
...$.status,
|
|
923
926
|
...r?.status
|
|
924
927
|
}
|
|
925
|
-
},
|
|
928
|
+
}, p = (r) => e(r, t.currency, n), m = t.shippingAddress ? ae(t.shippingAddress) : [], { fulfillment: h } = t, g = {
|
|
929
|
+
href: c ?? "",
|
|
930
|
+
className: "sf-checkout-success__continue",
|
|
931
|
+
children: u.continue
|
|
932
|
+
}, _ = c ? l ? l(g) : /* @__PURE__ */ d("a", { ...g }) : null, v = a?.actions ?? _;
|
|
926
933
|
return /* @__PURE__ */ f("section", {
|
|
927
934
|
className: ["sf-checkout-success", i].filter(Boolean).join(" "),
|
|
928
935
|
"data-status": t.status,
|
|
929
936
|
children: [
|
|
930
937
|
/* @__PURE__ */ d("h1", {
|
|
931
938
|
className: "sf-checkout-success__heading",
|
|
932
|
-
children:
|
|
939
|
+
children: u.heading
|
|
933
940
|
}),
|
|
941
|
+
o && /* @__PURE__ */ d("p", {
|
|
942
|
+
className: "sf-checkout-success__reference",
|
|
943
|
+
children: u.reference.replace("{id}", t.id)
|
|
944
|
+
}),
|
|
945
|
+
a?.header,
|
|
934
946
|
/* @__PURE__ */ d("p", {
|
|
935
947
|
className: "sf-checkout-success__status",
|
|
936
|
-
children:
|
|
948
|
+
children: u.status[t.status]
|
|
949
|
+
}),
|
|
950
|
+
s && /* @__PURE__ */ d("h2", {
|
|
951
|
+
className: "sf-checkout-success__summary-heading",
|
|
952
|
+
children: u.summaryHeading
|
|
937
953
|
}),
|
|
954
|
+
a?.beforeLines,
|
|
938
955
|
/* @__PURE__ */ d("ul", {
|
|
939
956
|
className: "sf-checkout-success__lines",
|
|
940
957
|
children: t.items.map((e) => /* @__PURE__ */ f("li", {
|
|
@@ -943,7 +960,7 @@ function oe({ order: t, locale: n, labels: r, className: i }) {
|
|
|
943
960
|
e.name,
|
|
944
961
|
" × ",
|
|
945
962
|
e.quantity
|
|
946
|
-
] }), /* @__PURE__ */ d("span", { children:
|
|
963
|
+
] }), /* @__PURE__ */ d("span", { children: p(e.priceCents * e.quantity) })]
|
|
947
964
|
}, e.productId))
|
|
948
965
|
}),
|
|
949
966
|
/* @__PURE__ */ f("dl", {
|
|
@@ -951,42 +968,47 @@ function oe({ order: t, locale: n, labels: r, className: i }) {
|
|
|
951
968
|
children: [
|
|
952
969
|
/* @__PURE__ */ f("div", {
|
|
953
970
|
className: "sf-checkout-success__row",
|
|
954
|
-
children: [/* @__PURE__ */ d("dt", { children:
|
|
971
|
+
children: [/* @__PURE__ */ d("dt", { children: u.subtotal }), /* @__PURE__ */ d("dd", { children: p(t.subtotalCents) })]
|
|
955
972
|
}),
|
|
956
973
|
t.discountCents > 0 && /* @__PURE__ */ f("div", {
|
|
957
974
|
className: "sf-checkout-success__row sf-checkout-success__row--discount",
|
|
958
975
|
"data-discount": !0,
|
|
959
|
-
children: [/* @__PURE__ */ f("dt", { children: [
|
|
976
|
+
children: [/* @__PURE__ */ f("dt", { children: [u.discount, t.discountCode && ` (${t.discountCode})`] }), /* @__PURE__ */ f("dd", { children: ["−", p(t.discountCents)] })]
|
|
960
977
|
}),
|
|
961
978
|
t.shippingLabel && /* @__PURE__ */ f("div", {
|
|
962
979
|
className: "sf-checkout-success__row",
|
|
963
980
|
children: [/* @__PURE__ */ f("dt", { children: [
|
|
964
|
-
|
|
981
|
+
u.shipping,
|
|
965
982
|
" (",
|
|
966
983
|
t.shippingLabel,
|
|
967
984
|
")"
|
|
968
|
-
] }), /* @__PURE__ */ d("dd", { children:
|
|
985
|
+
] }), /* @__PURE__ */ d("dd", { children: p(t.shippingCents) })]
|
|
969
986
|
}),
|
|
970
987
|
/* @__PURE__ */ f("div", {
|
|
971
988
|
className: "sf-checkout-success__row sf-checkout-success__row--total",
|
|
972
|
-
children: [/* @__PURE__ */ d("dt", { children:
|
|
989
|
+
children: [/* @__PURE__ */ d("dt", { children: u.total }), /* @__PURE__ */ d("dd", { children: p(t.totalCents) })]
|
|
973
990
|
})
|
|
974
991
|
]
|
|
975
992
|
}),
|
|
976
|
-
|
|
993
|
+
a?.afterTotals,
|
|
994
|
+
m.length > 0 && /* @__PURE__ */ f("address", {
|
|
977
995
|
className: "sf-checkout-success__address",
|
|
978
|
-
children: [/* @__PURE__ */ d("strong", { children:
|
|
996
|
+
children: [/* @__PURE__ */ d("strong", { children: u.shipTo }), m.map((e) => /* @__PURE__ */ d("span", { children: e }, e))]
|
|
979
997
|
}),
|
|
980
|
-
|
|
998
|
+
h?.trackingNumber && /* @__PURE__ */ f("p", {
|
|
981
999
|
className: "sf-checkout-success__tracking",
|
|
982
|
-
children: [
|
|
983
|
-
href:
|
|
1000
|
+
children: [h.carrier && `${h.carrier} · `, h.trackingUrl ? /* @__PURE__ */ f("a", {
|
|
1001
|
+
href: h.trackingUrl,
|
|
984
1002
|
children: [
|
|
985
|
-
|
|
1003
|
+
u.tracking,
|
|
986
1004
|
" ",
|
|
987
|
-
|
|
1005
|
+
h.trackingNumber
|
|
988
1006
|
]
|
|
989
|
-
}) :
|
|
1007
|
+
}) : h.trackingNumber]
|
|
1008
|
+
}),
|
|
1009
|
+
v && /* @__PURE__ */ d("div", {
|
|
1010
|
+
className: "sf-checkout-success__actions",
|
|
1011
|
+
children: v
|
|
990
1012
|
})
|
|
991
1013
|
]
|
|
992
1014
|
});
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":["_Fragment","_Fragment","_Fragment","formatPrice","VAR_NAMES","themeToCssVars","vars","key","value","theme","defaults","priority","slug","StorefrontContext","createContext","StorefrontProvider","overrides","hrefs","useStorefront","useContext","PriceTag","classes","sold","className","priceCents","currency","locale","defaultAvailabilityLabels","AvailabilityBadge","text","labels","variant","label","availability","quantity","ProductCard","product","image","renderLink","Fragment","renderImage","ProductGrid","products","emptyState","index","priorityCount","CategoryFilter","ordered","sortCategories","categories","controlled","onSelect","item","active","selected","allLabel","c","formatLabel","ProductGallery","setActive","useState","images","event","i","ShopView","slots","activeCategory","formatCategoryLabel","sortForDisplay","nextHref","loadMoreLabel","ProductDetailView","FeaturedProducts","heading","emptyCart","addLine","first","state","line","existing","l","cartReducer","action","subtotalCents","sum","itemCount","localStorageCartStorage","raw","parsed","result","cartLineSchema","CartContext","defaultStorage","CartProvider","dispatch","useReducer","hydrated","setHydrated","isOpen","setOpen","stateRef","useRef","useEffect","cancelled","storage","loaded","add","useCallback","useMemo","useCart","ctx","defaultDiscountLabels","discountReasonMessage","reason","defaultCheckoutLabels","assign","url","useCheckout","cart","busy","setBusy","message","setMessage","startCheckout","lines","options","onDiscountInvalid","remove","onRedirect","defaultAddToCartLabels","AddToCartButton","inCart","unavailable","atCap","disabled","handleClick","openOnAdd","defaultCartLineItemLabels","CartLineItem","editable","setQuantity","parseDurationMs","match","amount","useDrawerPresence","durationMs","setDurationMs","wasOpen","setWasOpen","lingering","setLingering","backdropRef","timer","DiscountField","open","setValue","submit","code","onApply","applied","onRemove","error","pending","useDiscount","items","signature","setApplied","setError","setPending","latest","request","previewedSignature","clear","run","current","id","onReject","apply","delay","discount","CartSummary","subtotal","discountLabels","checkoutDisabled","onCheckout","checkoutLabel","defaultCartDrawerLabels","FOCUSABLE","CartDrawer","panelRef","presence","trigger","handleKeyDown","last","summary","CartButton","count","showCount","hideZeroCount","ariaLabel","labelHidden","icon","defaultCheckoutSuccessLabels","addressLines","region","address","CheckoutSuccessView","money","cents","order","fulfillment","ClearCartOnMount","cleared"],"sources":["../src/lib/theme.ts","../src/lib/provider.tsx","../src/components/price-tag/price-tag.tsx","../src/components/availability-badge/availability-badge.tsx","../src/components/product-card/product-card.tsx","../src/components/product-grid/product-grid.tsx","../src/components/category-filter/category-filter.tsx","../src/components/product-gallery/product-gallery.tsx","../src/views/shop-view.tsx","../src/views/product-detail-view.tsx","../src/views/featured-products.tsx","../src/cart/cart-reducer.ts","../src/cart/cart-storage.ts","../src/cart/cart-provider.tsx","../src/components/cart-summary/discount-types.ts","../src/cart/use-checkout.ts","../src/components/add-to-cart-button/add-to-cart-button.tsx","../src/components/cart-line-item/cart-line-item.tsx","../src/lib/parse-duration.ts","../src/components/cart-drawer/use-drawer-presence.ts","../src/components/cart-summary/discount-field.tsx","../src/components/cart-summary/use-discount.ts","../src/components/cart-summary/cart-summary.tsx","../src/components/cart-drawer/cart-drawer.tsx","../src/components/cart-button/cart-button.tsx","../src/components/checkout-success/labels.ts","../src/components/checkout-success/checkout-success-view.tsx","../src/cart/clear-cart-on-mount.tsx"],"sourcesContent":["export interface StorefrontTheme {\n colorBg?: string;\n colorSurface?: string;\n colorFg?: string;\n colorMuted?: string;\n colorAccent?: string;\n colorAccentFg?: string;\n colorBorder?: string;\n colorSold?: string;\n radius?: string;\n fontDisplay?: string;\n fontBody?: string;\n spaceUnit?: string;\n}\n\nconst VAR_NAMES: Record<keyof StorefrontTheme, string> = {\n colorBg: '--sf-color-bg',\n colorSurface: '--sf-color-surface',\n colorFg: '--sf-color-fg',\n colorMuted: '--sf-color-muted',\n colorAccent: '--sf-color-accent',\n colorAccentFg: '--sf-color-accent-fg',\n colorBorder: '--sf-color-border',\n colorSold: '--sf-color-sold',\n radius: '--sf-radius',\n fontDisplay: '--sf-font-display',\n fontBody: '--sf-font-body',\n spaceUnit: '--sf-space-unit',\n};\n\nexport function themeToCssVars(theme: StorefrontTheme): Record<string, string> {\n const vars: Record<string, string> = {};\n for (const key of Object.keys(VAR_NAMES) as (keyof StorefrontTheme)[]) {\n const value = theme[key];\n if (value !== undefined) vars[VAR_NAMES[key]] = value;\n }\n return vars;\n}\n","import { createContext, useContext, type ReactNode } from 'react';\n\nexport interface LinkProps {\n href: string;\n children: ReactNode;\n className?: string;\n}\n\nexport interface ImageProps {\n src: string;\n alt: string;\n sizes?: string;\n className?: string;\n priority?: boolean;\n}\n\nexport interface StorefrontConfig {\n renderLink: (props: LinkProps) => ReactNode;\n renderImage: (props: ImageProps) => ReactNode;\n locale: string;\n hrefs: {\n product: (slug: string) => string;\n category: (slug: string) => string;\n };\n}\n\nconst defaults: StorefrontConfig = {\n renderLink: ({ href, children, className }) => (\n <a href={href} className={className}>\n {children}\n </a>\n ),\n renderImage: ({ src, alt, sizes, className, priority }) => (\n <img\n src={src}\n alt={alt}\n sizes={sizes}\n className={className}\n loading={priority ? 'eager' : 'lazy'}\n decoding=\"async\"\n fetchPriority={priority ? 'high' : undefined}\n />\n ),\n locale: 'en-US',\n hrefs: {\n product: (slug) => `/shop/${slug}`,\n category: (slug) => `/shop?category=${slug}`,\n },\n};\n\nconst StorefrontContext = createContext<StorefrontConfig>(defaults);\n\nexport interface StorefrontProviderProps extends Partial<\n Omit<StorefrontConfig, 'hrefs'>\n> {\n hrefs?: Partial<StorefrontConfig['hrefs']>;\n children: ReactNode;\n}\n\nexport function StorefrontProvider({\n children,\n hrefs,\n ...overrides\n}: StorefrontProviderProps) {\n const value: StorefrontConfig = {\n ...defaults,\n ...overrides,\n hrefs: { ...defaults.hrefs, ...hrefs },\n };\n return (\n <StorefrontContext.Provider value={value}>\n {children}\n </StorefrontContext.Provider>\n );\n}\n\nexport function useStorefront(): StorefrontConfig {\n return useContext(StorefrontContext);\n}\n","import { formatPrice } from '../../lib/format-price';\nimport { useStorefront } from '../../lib/provider';\n\nexport interface PriceTagProps {\n priceCents: number;\n currency: string;\n sold?: boolean;\n className?: string;\n}\n\nexport function PriceTag({\n priceCents,\n currency,\n sold = false,\n className,\n}: PriceTagProps) {\n const { locale } = useStorefront();\n const classes = ['sf-price', sold && 'sf-price--sold', className]\n .filter(Boolean)\n .join(' ');\n return (\n <span className={classes}>{formatPrice(priceCents, currency, locale)}</span>\n );\n}\n","import type { ProductAvailability } from '@develemit/commerce-contracts';\n\nexport interface AvailabilityLabels {\n oneOfAKind: string;\n sold: string;\n reserved: string;\n available: string;\n}\n\nexport const defaultAvailabilityLabels: AvailabilityLabels = {\n oneOfAKind: 'One of a kind',\n sold: 'Sold',\n reserved: 'On hold',\n available: 'Available',\n};\n\nexport interface AvailabilityBadgeProps {\n availability: ProductAvailability;\n quantity: number;\n labels?: Partial<AvailabilityLabels>;\n className?: string;\n}\n\nexport function AvailabilityBadge({\n availability,\n quantity,\n labels,\n className,\n}: AvailabilityBadgeProps) {\n const text = { ...defaultAvailabilityLabels, ...labels };\n let variant: string;\n let label: string;\n if (availability === 'sold') {\n variant = 'sold';\n label = text.sold;\n } else if (availability === 'reserved') {\n variant = 'reserved';\n label = text.reserved;\n } else if (quantity === 1) {\n variant = 'unique';\n label = text.oneOfAKind;\n } else {\n variant = 'available';\n label = text.available;\n }\n const classes = ['sf-badge', `sf-badge--${variant}`, className]\n .filter(Boolean)\n .join(' ');\n return (\n <span role=\"status\" className={classes}>\n {label}\n </span>\n );\n}\n","import type { ProductCard as ProductCardData } from '@develemit/commerce-contracts';\nimport { useStorefront } from '../../lib/provider';\nimport {\n AvailabilityBadge,\n type AvailabilityLabels,\n} from '../availability-badge/availability-badge';\nimport { PriceTag } from '../price-tag/price-tag';\n\nexport interface ProductCardProps {\n product: ProductCardData;\n labels?: Partial<AvailabilityLabels>;\n sizes?: string;\n priority?: boolean;\n}\n\nexport function ProductCard({\n product,\n labels,\n sizes,\n priority,\n}: ProductCardProps) {\n const { renderLink, renderImage, hrefs } = useStorefront();\n const sold = product.availability === 'sold';\n const image = product.images[0];\n const classes = ['sf-card', sold && 'sf-card--sold']\n .filter(Boolean)\n .join(' ');\n return (\n <article className={classes}>\n {renderLink({\n href: hrefs.product(product.slug),\n className: 'sf-card__link',\n children: (\n <>\n <div className=\"sf-card__media\">\n {image ? (\n renderImage({\n src: image.url,\n alt: image.alt,\n sizes,\n className: 'sf-card__image',\n priority,\n })\n ) : (\n <div className=\"sf-card__placeholder\" aria-hidden=\"true\" />\n )}\n </div>\n <div className=\"sf-card__body\">\n <h3 className=\"sf-card__title\">{product.name}</h3>\n <PriceTag\n priceCents={product.priceCents}\n currency={product.currency}\n sold={sold}\n />\n <AvailabilityBadge\n availability={product.availability}\n quantity={product.quantity}\n labels={labels}\n />\n </div>\n </>\n ),\n })}\n </article>\n );\n}\n","import type { ReactNode } from 'react';\nimport type { ProductCard as ProductCardData } from '@develemit/commerce-contracts';\nimport type { AvailabilityLabels } from '../availability-badge/availability-badge';\nimport { ProductCard } from '../product-card/product-card';\n\nexport interface ProductGridProps {\n products: ProductCardData[];\n emptyState?: ReactNode;\n labels?: Partial<AvailabilityLabels>;\n priorityCount?: number;\n}\n\nexport function ProductGrid({\n products,\n emptyState,\n labels,\n priorityCount = 0,\n}: ProductGridProps) {\n if (products.length === 0) {\n return (\n <div className=\"sf-grid-empty\">{emptyState ?? 'No products found.'}</div>\n );\n }\n return (\n <ul className=\"sf-grid\">\n {products.map((product, index) => (\n <li key={product.id} className=\"sf-grid__item\">\n <ProductCard\n product={product}\n labels={labels}\n priority={index < priorityCount}\n />\n </li>\n ))}\n </ul>\n );\n}\n","import type { Category } from '@develemit/commerce-contracts';\nimport { useStorefront } from '../../lib/provider';\n\nexport interface CategoryFilterProps {\n categories: Category[];\n selected?: string;\n onSelect?: (slug: string | undefined) => void;\n allLabel?: string;\n label?: string;\n formatLabel?: (category: Category) => string;\n sortCategories?: (a: Category, b: Category) => number;\n}\n\nexport function CategoryFilter({\n categories,\n selected,\n onSelect,\n allLabel = 'All',\n label = 'Categories',\n formatLabel,\n sortCategories,\n}: CategoryFilterProps) {\n const { renderLink, hrefs } = useStorefront();\n const ordered = sortCategories\n ? [...categories].sort(sortCategories)\n : categories;\n const controlled = onSelect !== undefined;\n\n const item = (slug: string | undefined, text: string, href: string) => {\n const active = selected === slug;\n const className = `sf-filter__item${active ? ' sf-filter__item--active' : ''}`;\n return (\n <li key={slug ?? '__all'}>\n {controlled ? (\n <button\n type=\"button\"\n className={className}\n aria-pressed={active}\n onClick={() => onSelect(slug)}\n >\n {text}\n </button>\n ) : (\n renderLink({\n href,\n className,\n children: text,\n })\n )}\n </li>\n );\n };\n\n return (\n <nav aria-label={label} className=\"sf-filter\">\n <ul className=\"sf-filter__list\">\n {item(undefined, allLabel, hrefs.category(''))}\n {ordered.map((c) =>\n item(\n c.slug,\n formatLabel ? formatLabel(c) : c.label,\n hrefs.category(c.slug),\n ),\n )}\n </ul>\n </nav>\n );\n}\n","import { useState, type KeyboardEvent } from 'react';\nimport type { ProductImage } from '@develemit/commerce-contracts';\nimport { useStorefront } from '../../lib/provider';\n\nexport interface ProductGalleryProps {\n images: ProductImage[];\n sizes?: string;\n}\n\nexport function ProductGallery({ images, sizes }: ProductGalleryProps) {\n const { renderImage } = useStorefront();\n const [active, setActive] = useState(0);\n\n if (images.length === 0) return null;\n\n const onKeyDown = (event: KeyboardEvent) => {\n if (event.key === 'ArrowRight') {\n setActive((i) => (i + 1) % images.length);\n } else if (event.key === 'ArrowLeft') {\n setActive((i) => (i - 1 + images.length) % images.length);\n } else if (event.key === 'Home') {\n setActive(0);\n } else if (event.key === 'End') {\n setActive(images.length - 1);\n } else {\n return;\n }\n event.preventDefault();\n };\n\n return (\n <div\n className=\"sf-gallery\"\n role=\"group\"\n aria-roledescription=\"carousel\"\n aria-label=\"Product images\"\n tabIndex={0}\n onKeyDown={onKeyDown}\n >\n <div className=\"sf-gallery__stage\">\n {images.map((image, index) => (\n <div\n key={image.url}\n className=\"sf-gallery__slide\"\n hidden={index !== active}\n >\n {renderImage({\n src: image.url,\n alt: image.alt,\n sizes,\n priority: index === active,\n className: 'sf-gallery__image',\n })}\n </div>\n ))}\n </div>\n {images.length > 1 && (\n <div className=\"sf-gallery__thumbs\">\n {images.map((image, index) => (\n <button\n key={image.url}\n type=\"button\"\n className={`sf-gallery__thumb${index === active ? ' sf-gallery__thumb--active' : ''}`}\n aria-label={`Show image ${index + 1} of ${images.length}`}\n aria-current={index === active}\n onClick={() => setActive(index)}\n >\n {renderImage({\n src: image.url,\n alt: '',\n className: 'sf-gallery__thumb-image',\n })}\n </button>\n ))}\n </div>\n )}\n </div>\n );\n}\n","import type { ReactNode } from 'react';\nimport type { Category, ProductCard } from '@develemit/commerce-contracts';\nimport { CategoryFilter } from '../components/category-filter/category-filter';\nimport { ProductGrid } from '../components/product-grid/product-grid';\nimport type { AvailabilityLabels } from '../components/availability-badge/availability-badge';\nimport { useStorefront } from '../lib/provider';\nimport { sortForDisplay } from './sort-for-display';\n\nexport interface ShopViewSlots {\n header?: ReactNode;\n intro?: ReactNode;\n emptyState?: ReactNode;\n}\n\nexport interface ShopViewProps {\n products: ProductCard[];\n categories: Category[];\n activeCategory?: string;\n slots?: ShopViewSlots;\n labels?: Partial<AvailabilityLabels>;\n allLabel?: string;\n formatCategoryLabel?: (category: Category) => string;\n sortCategories?: (a: Category, b: Category) => number;\n nextHref?: string;\n loadMoreLabel?: string;\n}\n\nexport function ShopView({\n products,\n categories,\n activeCategory,\n slots = {},\n labels,\n allLabel,\n formatCategoryLabel,\n sortCategories,\n nextHref,\n loadMoreLabel = 'Load more',\n}: ShopViewProps) {\n const { renderLink } = useStorefront();\n return (\n <div className=\"sf-shop\">\n {slots.header}\n {slots.intro}\n <CategoryFilter\n categories={categories}\n selected={activeCategory}\n allLabel={allLabel}\n formatLabel={formatCategoryLabel}\n sortCategories={sortCategories}\n />\n <ProductGrid\n products={sortForDisplay(products)}\n emptyState={slots.emptyState}\n labels={labels}\n priorityCount={6}\n />\n {nextHref && (\n <div className=\"sf-shop__pager\">\n {renderLink({\n href: nextHref,\n className: 'sf-shop__more',\n children: loadMoreLabel,\n })}\n </div>\n )}\n </div>\n );\n}\n","import type { ReactNode } from 'react';\nimport type { Product } from '@develemit/commerce-contracts';\nimport {\n AvailabilityBadge,\n type AvailabilityLabels,\n} from '../components/availability-badge/availability-badge';\nimport { PriceTag } from '../components/price-tag/price-tag';\nimport { ProductGallery } from '../components/product-gallery/product-gallery';\n\nexport interface ProductDetailViewSlots {\n actions?: ReactNode;\n afterDetails?: ReactNode;\n}\n\nexport interface ProductDetailViewProps {\n product: Product;\n slots?: ProductDetailViewSlots;\n labels?: Partial<AvailabilityLabels>;\n}\n\nexport function ProductDetailView({\n product,\n slots = {},\n labels,\n}: ProductDetailViewProps) {\n return (\n <article className=\"sf-detail\">\n <ProductGallery images={product.images} />\n <div className=\"sf-detail__body\">\n <h1 className=\"sf-detail__title\">{product.name}</h1>\n <PriceTag priceCents={product.priceCents} currency={product.currency} />\n <AvailabilityBadge\n availability={product.availability}\n quantity={product.quantity}\n labels={labels}\n />\n <p className=\"sf-detail__description\">{product.description}</p>\n {(product.materials || product.dimensions) && (\n <dl className=\"sf-detail__specs\">\n {product.materials && (\n <>\n <dt>Materials</dt>\n <dd>{product.materials}</dd>\n </>\n )}\n {product.dimensions && (\n <>\n <dt>Dimensions</dt>\n <dd>{product.dimensions}</dd>\n </>\n )}\n </dl>\n )}\n {slots.actions && (\n <div className=\"sf-detail__actions\">{slots.actions}</div>\n )}\n {slots.afterDetails}\n </div>\n </article>\n );\n}\n","import type { ProductCard } from '@develemit/commerce-contracts';\nimport type { AvailabilityLabels } from '../components/availability-badge/availability-badge';\nimport { ProductGrid } from '../components/product-grid/product-grid';\n\nexport interface FeaturedProductsProps {\n products: ProductCard[];\n heading?: string;\n labels?: Partial<AvailabilityLabels>;\n}\n\nexport function FeaturedProducts({\n products,\n heading = 'Featured',\n labels,\n}: FeaturedProductsProps) {\n if (products.length === 0) return null;\n return (\n <section className=\"sf-featured\">\n <h2 className=\"sf-featured__heading\">{heading}</h2>\n <ProductGrid products={products} labels={labels} priorityCount={6} />\n </section>\n );\n}\n","import type { CartLine } from '@develemit/commerce-contracts';\n\nexport interface CartState {\n lines: CartLine[];\n}\n\nexport type CartAction =\n | { type: 'add'; line: CartLine }\n | { type: 'remove'; productId: string }\n | { type: 'setQuantity'; productId: string; quantity: number }\n | { type: 'clear' }\n | { type: 'hydrate'; state: CartState };\n\nexport interface AddResult {\n state: CartState;\n added: boolean;\n capped: boolean;\n reason?: 'currency-mismatch';\n}\n\nexport const emptyCart: CartState = { lines: [] };\n\nexport function addLine(state: CartState, line: CartLine): AddResult {\n const first = state.lines[0];\n if (first && first.currency !== line.currency) {\n return { state, added: false, capped: false, reason: 'currency-mismatch' };\n }\n const existing = state.lines.find((l) => l.productId === line.productId);\n if (!existing) {\n const quantity = Math.min(line.quantity, line.maxQuantity);\n return {\n state: { lines: [...state.lines, { ...line, quantity }] },\n added: true,\n capped: quantity < line.quantity,\n };\n }\n if (existing.quantity >= existing.maxQuantity) {\n return { state, added: false, capped: true };\n }\n const quantity = Math.min(\n existing.quantity + line.quantity,\n existing.maxQuantity,\n );\n return {\n state: {\n lines: state.lines.map((l) =>\n l.productId === line.productId ? { ...l, quantity } : l,\n ),\n },\n added: true,\n capped: existing.quantity + line.quantity > existing.maxQuantity,\n };\n}\n\nexport function cartReducer(state: CartState, action: CartAction): CartState {\n switch (action.type) {\n case 'add':\n return addLine(state, action.line).state;\n case 'remove':\n return {\n lines: state.lines.filter((l) => l.productId !== action.productId),\n };\n case 'setQuantity': {\n if (action.quantity <= 0) {\n return cartReducer(state, {\n type: 'remove',\n productId: action.productId,\n });\n }\n return {\n lines: state.lines.map((l) =>\n l.productId === action.productId\n ? { ...l, quantity: Math.min(action.quantity, l.maxQuantity) }\n : l,\n ),\n };\n }\n case 'clear':\n return emptyCart;\n case 'hydrate':\n return action.state;\n }\n}\n\nexport function subtotalCents(state: CartState): number {\n return state.lines.reduce((sum, l) => sum + l.priceCents * l.quantity, 0);\n}\n\nexport function itemCount(state: CartState): number {\n return state.lines.reduce((sum, l) => sum + l.quantity, 0);\n}\n","import { cartLineSchema } from '@develemit/commerce-contracts';\nimport { emptyCart, type CartState } from './cart-reducer';\n\nexport interface CartStorage {\n load: () => CartState | Promise<CartState>;\n save: (state: CartState) => void | Promise<void>;\n}\n\nexport function localStorageCartStorage(key = 'sf-cart'): CartStorage {\n return {\n load() {\n if (typeof window === 'undefined') return emptyCart;\n try {\n const raw = window.localStorage.getItem(key);\n if (!raw) return emptyCart;\n const parsed: unknown = JSON.parse(raw);\n const lines = Array.isArray((parsed as CartState)?.lines)\n ? (parsed as CartState).lines\n : [];\n const valid = lines.flatMap((line) => {\n const result = cartLineSchema.safeParse(line);\n return result.success ? [result.data] : [];\n });\n return { lines: valid };\n } catch {\n return emptyCart;\n }\n },\n save(state) {\n if (typeof window === 'undefined') return;\n try {\n window.localStorage.setItem(key, JSON.stringify(state));\n } catch {\n // storage full or blocked: the cart simply stays in memory\n }\n },\n };\n}\n","import {\n createContext,\n useCallback,\n useContext,\n useEffect,\n useMemo,\n useReducer,\n useRef,\n useState,\n type ReactNode,\n} from 'react';\nimport type { CartLine } from '@develemit/commerce-contracts';\nimport {\n addLine,\n cartReducer,\n emptyCart,\n itemCount,\n subtotalCents,\n type AddResult,\n type CartState,\n} from './cart-reducer';\nimport { localStorageCartStorage, type CartStorage } from './cart-storage';\n\nexport interface CartContextValue {\n lines: CartLine[];\n itemCount: number;\n subtotalCents: number;\n currency: string | null;\n hydrated: boolean;\n isOpen: boolean;\n open: () => void;\n close: () => void;\n add: (line: CartLine) => AddResult;\n remove: (productId: string) => void;\n setQuantity: (productId: string, quantity: number) => void;\n clear: () => void;\n}\n\nconst CartContext = createContext<CartContextValue | null>(null);\n\nexport interface CartProviderProps {\n storage?: CartStorage;\n children: ReactNode;\n}\n\nconst defaultStorage = localStorageCartStorage();\n\nexport function CartProvider({\n storage = defaultStorage,\n children,\n}: CartProviderProps) {\n const [state, dispatch] = useReducer(cartReducer, emptyCart);\n const [hydrated, setHydrated] = useState(false);\n const [isOpen, setOpen] = useState(false);\n const stateRef = useRef<CartState>(state);\n useEffect(() => {\n stateRef.current = state;\n }, [state]);\n\n useEffect(() => {\n let cancelled = false;\n Promise.resolve(storage.load()).then((loaded) => {\n if (cancelled) return;\n dispatch({ type: 'hydrate', state: loaded });\n setHydrated(true);\n });\n return () => {\n cancelled = true;\n };\n }, [storage]);\n\n useEffect(() => {\n if (hydrated) void storage.save(state);\n }, [hydrated, state, storage]);\n\n const add = useCallback((line: CartLine) => {\n const result = addLine(stateRef.current, line);\n if (result.added) {\n stateRef.current = result.state;\n dispatch({ type: 'add', line });\n }\n return result;\n }, []);\n\n const value = useMemo<CartContextValue>(\n () => ({\n lines: state.lines,\n itemCount: itemCount(state),\n subtotalCents: subtotalCents(state),\n currency: state.lines[0]?.currency ?? null,\n hydrated,\n isOpen,\n open: () => setOpen(true),\n close: () => setOpen(false),\n add,\n remove: (productId) => dispatch({ type: 'remove', productId }),\n setQuantity: (productId, quantity) =>\n dispatch({ type: 'setQuantity', productId, quantity }),\n clear: () => dispatch({ type: 'clear' }),\n }),\n [state, hydrated, isOpen, add],\n );\n\n return <CartContext.Provider value={value}>{children}</CartContext.Provider>;\n}\n\nexport function useCart(): CartContextValue {\n const ctx = useContext(CartContext);\n if (!ctx) throw new Error('useCart must be used inside <CartProvider>');\n return ctx;\n}\n","import type { DiscountRejectionReason } from '@develemit/commerce-contracts';\n\nexport type DiscountPreviewResult =\n | { ok: true; code: string; discountCents: number }\n | { ok: false; reason: DiscountRejectionReason };\n\nexport interface CartSummaryDiscount {\n onPreview: (\n code: string,\n items: { productId: string; quantity: number }[],\n ) => Promise<DiscountPreviewResult>;\n onApply?: (code: string | null) => void;\n debounceMs?: number;\n}\n\nexport interface DiscountLabels {\n toggle: string;\n field: string;\n apply: string;\n remove: string;\n line: string;\n reasons: Record<DiscountRejectionReason, string>;\n unavailable: string;\n}\n\nexport const defaultDiscountLabels: DiscountLabels = {\n toggle: 'Have a code?',\n field: 'Discount code',\n apply: 'Apply',\n remove: 'Remove',\n line: 'Discount',\n unavailable: 'We could not check that code. Please try again.',\n reasons: {\n not_found: 'That code is not valid.',\n inactive: 'That code is not valid.',\n not_started: 'That code is not active yet.',\n expired: 'That code has expired.',\n min_subtotal: 'Your cart is below the minimum for that code.',\n exhausted: 'That code is no longer available.',\n currency: 'That code does not apply to this cart.',\n },\n};\n\nexport function discountReasonMessage(\n reason: DiscountRejectionReason,\n overrides?: Partial<DiscountLabels['reasons']>,\n): string {\n return overrides?.[reason] ?? defaultDiscountLabels.reasons[reason];\n}\n","import { useCallback, useState } from 'react';\nimport type { DiscountRejectionReason } from '@develemit/commerce-contracts';\nimport type { CheckoutResult } from '../client/checkout-result';\nimport { discountReasonMessage } from '../components/cart-summary/discount-types';\nimport { useCart } from './cart-provider';\n\nexport interface CheckoutLabels {\n outOfStock: string;\n unavailable: string;\n discountReasons?: Partial<Record<DiscountRejectionReason, string>>;\n}\n\nexport const defaultCheckoutLabels: CheckoutLabels = {\n outOfStock:\n 'Some items just sold or are on hold, so we removed them from your cart.',\n unavailable: 'Checkout is unavailable right now. Please try again.',\n};\n\nexport interface StartCheckoutOptions {\n discountCode?: string;\n}\n\nexport interface UseCheckoutOptions {\n startCheckout: (\n items: { productId: string; quantity: number }[],\n options: StartCheckoutOptions,\n ) => Promise<CheckoutResult>;\n onRedirect?: (url: string) => void;\n onDiscountInvalid?: (reason: DiscountRejectionReason) => void;\n labels?: Partial<CheckoutLabels>;\n}\n\nexport interface UseCheckoutValue {\n checkout: (options?: StartCheckoutOptions) => Promise<void>;\n busy: boolean;\n message: string | null;\n clearMessage: () => void;\n}\n\nconst assign = (url: string) => window.location.assign(url);\n\nexport function useCheckout({\n startCheckout,\n onRedirect = assign,\n onDiscountInvalid,\n labels,\n}: UseCheckoutOptions): UseCheckoutValue {\n const cart = useCart();\n const [busy, setBusy] = useState(false);\n const [message, setMessage] = useState<string | null>(null);\n const { lines, remove } = cart;\n const text = { ...defaultCheckoutLabels, ...labels };\n\n const checkout = useCallback(\n async (options: StartCheckoutOptions = {}) => {\n setBusy(true);\n setMessage(null);\n try {\n const result = await startCheckout(\n lines.map((l) => ({ productId: l.productId, quantity: l.quantity })),\n options,\n );\n if ('url' in result) {\n onRedirect(result.url);\n return;\n }\n if (result.error === 'discount_invalid') {\n setMessage(\n discountReasonMessage(result.reason, text.discountReasons),\n );\n onDiscountInvalid?.(result.reason);\n } else {\n result.productIds.forEach(remove);\n setMessage(text.outOfStock);\n }\n } catch {\n setMessage(text.unavailable);\n }\n setBusy(false);\n },\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [startCheckout, onRedirect, onDiscountInvalid, lines, remove, labels],\n );\n\n const clearMessage = useCallback(() => setMessage(null), []);\n\n return { checkout, busy, message, clearMessage };\n}\n","import type { Product } from '@develemit/commerce-contracts';\nimport { useCart } from '../../cart/cart-provider';\n\nexport interface AddToCartLabels {\n add: string;\n inCart: string;\n unavailable: string;\n}\n\nexport const defaultAddToCartLabels: AddToCartLabels = {\n add: 'Add to cart',\n inCart: 'In your cart',\n unavailable: 'Unavailable',\n};\n\nexport interface AddToCartButtonProps {\n product: Product;\n labels?: Partial<AddToCartLabels>;\n openOnAdd?: boolean;\n className?: string;\n}\n\nexport function AddToCartButton({\n product,\n labels,\n openOnAdd = true,\n className,\n}: AddToCartButtonProps) {\n const cart = useCart();\n const text = { ...defaultAddToCartLabels, ...labels };\n const inCart =\n cart.lines.find((l) => l.productId === product.id)?.quantity ?? 0;\n const unavailable = product.availability !== 'available';\n const atCap = inCart >= product.quantity;\n const disabled = !cart.hydrated || unavailable || atCap;\n const label = unavailable ? text.unavailable : atCap ? text.inCart : text.add;\n\n function handleClick() {\n const result = cart.add({\n productId: product.id,\n slug: product.slug,\n name: product.name,\n priceCents: product.priceCents,\n currency: product.currency,\n imageUrl: product.images[0]?.url,\n quantity: 1,\n maxQuantity: product.quantity,\n });\n if (result.added && openOnAdd) cart.open();\n }\n\n return (\n <button\n type=\"button\"\n className={['sf-button', className].filter(Boolean).join(' ')}\n disabled={disabled}\n onClick={handleClick}\n >\n {label}\n </button>\n );\n}\n","import type { CartLine } from '@develemit/commerce-contracts';\nimport { useCart } from '../../cart/cart-provider';\nimport { formatPrice } from '../../lib/format-price';\nimport { useStorefront } from '../../lib/provider';\n\nexport interface CartLineItemLabels {\n remove: string;\n quantity: string;\n}\n\nexport const defaultCartLineItemLabels: CartLineItemLabels = {\n remove: 'Remove',\n quantity: 'Quantity',\n};\n\nexport interface CartLineItemProps {\n line: CartLine;\n labels?: Partial<CartLineItemLabels>;\n}\n\nexport function CartLineItem({ line, labels }: CartLineItemProps) {\n const { remove, setQuantity } = useCart();\n const { locale, renderLink, renderImage, hrefs } = useStorefront();\n const text = { ...defaultCartLineItemLabels, ...labels };\n const editable = line.maxQuantity > 1;\n return (\n <li className=\"sf-cart-line\">\n {line.imageUrl &&\n renderImage({\n src: line.imageUrl,\n alt: '',\n sizes: '80px',\n className: 'sf-cart-line__image',\n })}\n <div className=\"sf-cart-line__body\">\n {renderLink({\n href: hrefs.product(line.slug),\n className: 'sf-cart-line__name',\n children: line.name,\n })}\n <span className=\"sf-cart-line__price\">\n {formatPrice(line.priceCents * line.quantity, line.currency, locale)}\n </span>\n {editable && (\n <label className=\"sf-cart-line__qty\">\n {text.quantity}\n <input\n type=\"number\"\n min={1}\n max={line.maxQuantity}\n value={line.quantity}\n onChange={(e) =>\n setQuantity(line.productId, Number(e.target.value))\n }\n />\n </label>\n )}\n <button\n type=\"button\"\n className=\"sf-link-button sf-cart-line__remove\"\n onClick={() => remove(line.productId)}\n aria-label={`${text.remove} ${line.name}`}\n >\n {text.remove}\n </button>\n </div>\n </li>\n );\n}\n","export function parseDurationMs(value: string): number {\n const match = /^\\s*(-?\\d*\\.?\\d+)\\s*(ms|s)\\s*$/.exec(value);\n if (!match) return 0;\n const amount = Number(match[1]);\n return Math.max(0, match[2] === 's' ? amount * 1000 : amount);\n}\n","import { useEffect, useState, type RefObject } from 'react';\nimport { parseDurationMs } from '../../lib/parse-duration';\n\nexport function useDrawerPresence(\n isOpen: boolean,\n backdropRef: RefObject<HTMLElement | null>,\n) {\n const [durationMs, setDurationMs] = useState(0);\n const [wasOpen, setWasOpen] = useState(isOpen);\n const [lingering, setLingering] = useState(false);\n\n if (wasOpen !== isOpen) {\n setWasOpen(isOpen);\n setLingering(!isOpen && durationMs > 0);\n }\n\n useEffect(() => {\n if (!isOpen || !backdropRef.current) return;\n setDurationMs(\n parseDurationMs(\n getComputedStyle(backdropRef.current).getPropertyValue(\n '--sf-motion-duration',\n ),\n ),\n );\n }, [isOpen, backdropRef]);\n\n useEffect(() => {\n if (!lingering) return;\n const timer = setTimeout(() => setLingering(false), durationMs);\n return () => clearTimeout(timer);\n }, [lingering, durationMs]);\n\n return { mounted: isOpen || lingering, state: isOpen ? 'open' : 'closed' };\n}\n","import { useState, type FormEvent } from 'react';\nimport { formatPrice } from '../../lib/format-price';\nimport type { DiscountLabels } from './discount-types';\nimport type { AppliedDiscount, DiscountError } from './use-discount';\n\nexport interface DiscountFieldProps {\n labels: DiscountLabels;\n applied: AppliedDiscount | null;\n error: DiscountError | null;\n pending: boolean;\n currency: string;\n locale: string;\n onApply: (code: string) => void;\n onRemove: () => void;\n}\n\nexport function DiscountField({\n labels,\n applied,\n error,\n pending,\n currency,\n locale,\n onApply,\n onRemove,\n}: DiscountFieldProps) {\n const [open, setOpen] = useState(false);\n const [value, setValue] = useState('');\n\n function submit(event: FormEvent) {\n event.preventDefault();\n const code = value.trim();\n if (code) onApply(code);\n }\n\n if (applied) {\n return (\n <p className=\"sf-cart-summary__row sf-cart-summary__discount\">\n <span>\n {labels.line} ({applied.code}){' '}\n <button type=\"button\" className=\"sf-link-button\" onClick={onRemove}>\n {labels.remove}\n </button>\n </span>\n <span className=\"sf-price\">\n −{formatPrice(applied.discountCents, currency, locale)}\n </span>\n </p>\n );\n }\n\n const message =\n error === 'unavailable'\n ? labels.unavailable\n : error\n ? labels.reasons[error]\n : null;\n return (\n <div className=\"sf-cart-summary__code\">\n {open ? (\n <form onSubmit={submit} className=\"sf-cart-summary__code-form\">\n <input\n aria-label={labels.field}\n value={value}\n onChange={(e) => setValue(e.target.value)}\n autoCapitalize=\"characters\"\n autoComplete=\"off\"\n />\n <button type=\"submit\" className=\"sf-button\" disabled={pending}>\n {labels.apply}\n </button>\n </form>\n ) : (\n <button\n type=\"button\"\n className=\"sf-link-button\"\n aria-expanded={false}\n onClick={() => setOpen(true)}\n >\n {labels.toggle}\n </button>\n )}\n {message && (\n <p role=\"alert\" className=\"sf-cart-summary__code-error\">\n {message}\n </p>\n )}\n </div>\n );\n}\n","import type { DiscountRejectionReason } from '@develemit/commerce-contracts';\nimport { useCallback, useEffect, useRef, useState } from 'react';\nimport { useCart } from '../../cart/cart-provider';\nimport type { CartSummaryDiscount } from './discount-types';\n\nexport interface AppliedDiscount {\n code: string;\n discountCents: number;\n}\n\nexport type DiscountError = DiscountRejectionReason | 'unavailable';\n\nexport function useDiscount(discount: CartSummaryDiscount | undefined) {\n const { lines } = useCart();\n const items = lines.map((l) => ({\n productId: l.productId,\n quantity: l.quantity,\n }));\n const signature = items.map((i) => `${i.productId}:${i.quantity}`).join(',');\n const [applied, setApplied] = useState<AppliedDiscount | null>(null);\n const [error, setError] = useState<DiscountError | null>(null);\n const [pending, setPending] = useState(false);\n const latest = useRef({ discount, items });\n useEffect(() => {\n latest.current = { discount, items };\n });\n const request = useRef(0);\n const previewedSignature = useRef(signature);\n\n const clear = useCallback(() => {\n request.current += 1;\n setApplied(null);\n latest.current.discount?.onApply?.(null);\n }, []);\n\n const run = useCallback(\n async (code: string, onReject: (reason: DiscountError) => void) => {\n const current = latest.current;\n if (!current.discount) return;\n const id = ++request.current;\n previewedSignature.current = current.items\n .map((i) => `${i.productId}:${i.quantity}`)\n .join(',');\n setPending(true);\n let result;\n try {\n result = await current.discount.onPreview(code, current.items);\n } catch {\n result = { ok: false as const, reason: 'unavailable' as const };\n }\n if (id !== request.current) return;\n setPending(false);\n if (result.ok) {\n setError(null);\n setApplied({ code: result.code, discountCents: result.discountCents });\n current.discount.onApply?.(result.code);\n } else {\n onReject(result.reason);\n }\n },\n [],\n );\n\n const apply = useCallback(\n (code: string) => run(code, (reason) => setError(reason)),\n [run],\n );\n\n const remove = useCallback(() => {\n setError(null);\n setPending(false);\n clear();\n }, [clear]);\n\n const code = applied?.code;\n const delay = discount?.debounceMs ?? 300;\n useEffect(() => {\n if (!code || signature === previewedSignature.current) return;\n const timer = setTimeout(() => {\n if (latest.current.items.length === 0) {\n clear();\n return;\n }\n void run(code, (reason) => {\n setError(reason);\n clear();\n });\n }, delay);\n return () => clearTimeout(timer);\n }, [code, signature, delay, run, clear]);\n\n return { applied, error, pending, apply, remove };\n}\n","import { useCart } from '../../cart/cart-provider';\nimport { formatPrice } from '../../lib/format-price';\nimport { useStorefront } from '../../lib/provider';\nimport { DiscountField } from './discount-field';\nimport {\n defaultDiscountLabels,\n type CartSummaryDiscount,\n type DiscountLabels,\n} from './discount-types';\nimport { useDiscount } from './use-discount';\n\nexport type {\n CartSummaryDiscount,\n DiscountPreviewResult,\n} from './discount-types';\nexport { discountReasonMessage } from './discount-types';\n\nexport interface CartSummaryLabels {\n subtotal: string;\n discount: Partial<Omit<DiscountLabels, 'reasons'>> & {\n reasons?: Partial<DiscountLabels['reasons']>;\n };\n}\n\nexport interface CartSummaryProps {\n onCheckout?: () => void;\n checkoutLabel?: string;\n checkoutDisabled?: boolean;\n labels?: Partial<CartSummaryLabels>;\n discount?: CartSummaryDiscount;\n}\n\nexport function CartSummary({\n onCheckout,\n checkoutLabel = 'Checkout',\n checkoutDisabled = false,\n labels,\n discount,\n}: CartSummaryProps) {\n const cart = useCart();\n const { locale } = useStorefront();\n const subtotal = labels?.subtotal ?? 'Subtotal';\n const state = useDiscount(discount);\n const discountLabels: DiscountLabels = {\n ...defaultDiscountLabels,\n ...labels?.discount,\n reasons: {\n ...defaultDiscountLabels.reasons,\n ...labels?.discount?.reasons,\n },\n };\n return (\n <div className=\"sf-cart-summary\">\n <p className=\"sf-cart-summary__row\">\n <span>{subtotal}</span>\n <span className=\"sf-price\">\n {formatPrice(cart.subtotalCents, cart.currency ?? 'USD', locale)}\n </span>\n </p>\n {discount && cart.lines.length > 0 && (\n <DiscountField\n labels={discountLabels}\n currency={cart.currency ?? 'USD'}\n locale={locale}\n onApply={(code) => void state.apply(code)}\n onRemove={state.remove}\n applied={state.applied}\n error={state.error}\n pending={state.pending}\n />\n )}\n <button\n type=\"button\"\n className=\"sf-button\"\n disabled={checkoutDisabled || !onCheckout || cart.lines.length === 0}\n onClick={onCheckout}\n >\n {checkoutLabel}\n </button>\n </div>\n );\n}\n","import { useEffect, useRef, type KeyboardEvent, type ReactNode } from 'react';\nimport { useCart } from '../../cart/cart-provider';\nimport { CartLineItem } from '../cart-line-item/cart-line-item';\nimport { useDrawerPresence } from './use-drawer-presence';\nimport {\n CartSummary,\n type CartSummaryProps,\n} from '../cart-summary/cart-summary';\n\nexport interface CartDrawerLabels {\n title: string;\n close: string;\n empty: string;\n}\n\nexport const defaultCartDrawerLabels: CartDrawerLabels = {\n title: 'Your cart',\n close: 'Close',\n empty: 'Your cart is empty.',\n};\n\nexport interface CartDrawerProps {\n labels?: Partial<CartDrawerLabels>;\n summary?: CartSummaryProps;\n slots?: CartDrawerSlots;\n}\n\nexport interface CartDrawerSlots {\n empty?: ReactNode;\n summaryNote?: ReactNode;\n}\n\nconst FOCUSABLE =\n 'a[href], button:not([disabled]), input:not([disabled]), [tabindex]:not([tabindex=\"-1\"])';\n\nexport function CartDrawer({ labels, summary, slots }: CartDrawerProps) {\n const cart = useCart();\n const text = { ...defaultCartDrawerLabels, ...labels };\n const panelRef = useRef<HTMLDivElement>(null);\n const backdropRef = useRef<HTMLDivElement>(null);\n const { isOpen } = cart;\n const presence = useDrawerPresence(isOpen, backdropRef);\n\n useEffect(() => {\n if (!isOpen) return;\n const trigger = document.activeElement as HTMLElement | null;\n panelRef.current?.querySelector<HTMLElement>(FOCUSABLE)?.focus();\n return () => trigger?.focus();\n }, [isOpen]);\n\n if (!presence.mounted) return null;\n\n function handleKeyDown(event: KeyboardEvent<HTMLDivElement>) {\n if (event.key === 'Escape') {\n event.stopPropagation();\n cart.close();\n return;\n }\n if (event.key !== 'Tab') return;\n const items = Array.from(\n panelRef.current?.querySelectorAll<HTMLElement>(FOCUSABLE) ?? [],\n );\n if (items.length === 0) return;\n const first = items[0];\n const last = items[items.length - 1];\n if (event.shiftKey && document.activeElement === first) {\n event.preventDefault();\n last.focus();\n } else if (!event.shiftKey && document.activeElement === last) {\n event.preventDefault();\n first.focus();\n }\n }\n\n return (\n <div\n ref={backdropRef}\n className=\"sf-drawer-backdrop\"\n data-state={presence.state}\n onClick={cart.close}\n >\n <div\n ref={panelRef}\n className=\"sf-drawer\"\n data-state={presence.state}\n role=\"dialog\"\n aria-modal=\"true\"\n aria-labelledby=\"sf-drawer-title\"\n onKeyDown={handleKeyDown}\n onClick={(e) => e.stopPropagation()}\n >\n <div className=\"sf-drawer__header\">\n <h2 id=\"sf-drawer-title\" className=\"sf-drawer__title\">\n {text.title}\n </h2>\n <button\n type=\"button\"\n className=\"sf-drawer__close\"\n aria-label={text.close}\n onClick={cart.close}\n >\n <span aria-hidden=\"true\">×</span>\n </button>\n </div>\n {cart.lines.length === 0 ? (\n <div className=\"sf-drawer__empty\">\n {slots?.empty ?? <p>{text.empty}</p>}\n </div>\n ) : (\n <>\n <ul className=\"sf-drawer__lines\">\n {cart.lines.map((line) => (\n <CartLineItem key={line.productId} line={line} />\n ))}\n </ul>\n <CartSummary {...summary} />\n {slots?.summaryNote && (\n <div className=\"sf-drawer__note\">{slots.summaryNote}</div>\n )}\n </>\n )}\n </div>\n </div>\n );\n}\n","import type { ReactNode } from 'react';\nimport { useCart } from '../../cart/cart-provider';\n\nexport interface CartButtonProps {\n label?: string;\n className?: string;\n icon?: ReactNode;\n labelHidden?: boolean;\n hideZeroCount?: boolean;\n}\n\nexport function CartButton({\n label = 'Cart',\n className,\n icon,\n labelHidden = false,\n hideZeroCount = false,\n}: CartButtonProps) {\n const cart = useCart();\n const count = cart.hydrated ? cart.itemCount : 0;\n const showCount = !(hideZeroCount && count === 0);\n const ariaLabel = showCount\n ? labelHidden\n ? `${label}, ${cart.itemCount} items`\n : undefined\n : `${label}, empty`;\n return (\n <button\n type=\"button\"\n className={['sf-cart-button', className].filter(Boolean).join(' ')}\n aria-haspopup=\"dialog\"\n aria-label={ariaLabel}\n onClick={cart.open}\n >\n {icon}\n <span className={labelHidden ? 'sf-visually-hidden' : undefined}>\n {label}\n </span>\n {showCount && (\n <span\n className=\"sf-cart-button__count\"\n aria-label={`${cart.itemCount} items`}\n >\n {count}\n </span>\n )}\n </button>\n );\n}\n","import type { OrderStatus } from '@develemit/commerce-contracts';\n\nexport interface CheckoutSuccessLabels {\n heading: string;\n subtotal: string;\n discount: string;\n shipping: string;\n total: string;\n shipTo: string;\n tracking: string;\n status: Record<OrderStatus, string>;\n}\n\nexport const defaultCheckoutSuccessLabels: CheckoutSuccessLabels = {\n heading: 'Order received',\n subtotal: 'Subtotal',\n discount: 'Discount',\n shipping: 'Shipping',\n total: 'Total',\n shipTo: 'Ship to',\n tracking: 'Track',\n status: {\n pending: \"We're confirming your payment and will email you once it's done.\",\n paid: 'Payment received. Thank you!',\n shipped: 'Your order is on its way.',\n cancelled: 'This checkout was cancelled.',\n expired: 'This checkout expired before payment.',\n refunded: 'This order has been refunded.',\n needs_attention:\n \"We couldn't confirm one of your items. We'll be in touch about a refund.\",\n },\n};\n","import type { OrderAddress, OrderSummary } from '@develemit/commerce-contracts';\nimport { formatPrice } from '../../lib/format-price';\nimport {\n defaultCheckoutSuccessLabels,\n type CheckoutSuccessLabels,\n} from './labels';\n\nexport interface CheckoutSuccessViewProps {\n order: OrderSummary & { shippingAddress?: OrderAddress | null };\n locale?: string;\n labels?: Partial<Omit<CheckoutSuccessLabels, 'status'>> & {\n status?: Partial<CheckoutSuccessLabels['status']>;\n };\n className?: string;\n}\n\nfunction addressLines(address: OrderAddress): string[] {\n const region = [address.city, address.state, address.postalCode]\n .filter(Boolean)\n .join(' ');\n return [\n address.name,\n address.line1,\n address.line2,\n region,\n address.country,\n ].filter((line): line is string => Boolean(line));\n}\n\nexport function CheckoutSuccessView({\n order,\n locale,\n labels,\n className,\n}: CheckoutSuccessViewProps) {\n const text = {\n ...defaultCheckoutSuccessLabels,\n ...labels,\n status: { ...defaultCheckoutSuccessLabels.status, ...labels?.status },\n };\n const money = (cents: number) => formatPrice(cents, order.currency, locale);\n const lines = order.shippingAddress\n ? addressLines(order.shippingAddress)\n : [];\n const { fulfillment } = order;\n return (\n <section\n className={['sf-checkout-success', className].filter(Boolean).join(' ')}\n data-status={order.status}\n >\n <h1 className=\"sf-checkout-success__heading\">{text.heading}</h1>\n <p className=\"sf-checkout-success__status\">{text.status[order.status]}</p>\n <ul className=\"sf-checkout-success__lines\">\n {order.items.map((item) => (\n <li key={item.productId} className=\"sf-checkout-success__line\">\n <span>\n {item.name} × {item.quantity}\n </span>\n <span>{money(item.priceCents * item.quantity)}</span>\n </li>\n ))}\n </ul>\n <dl className=\"sf-checkout-success__totals\">\n <div className=\"sf-checkout-success__row\">\n <dt>{text.subtotal}</dt>\n <dd>{money(order.subtotalCents)}</dd>\n </div>\n {order.discountCents > 0 && (\n <div\n className=\"sf-checkout-success__row sf-checkout-success__row--discount\"\n data-discount\n >\n <dt>\n {text.discount}\n {order.discountCode && ` (${order.discountCode})`}\n </dt>\n <dd>−{money(order.discountCents)}</dd>\n </div>\n )}\n {order.shippingLabel && (\n <div className=\"sf-checkout-success__row\">\n <dt>\n {text.shipping} ({order.shippingLabel})\n </dt>\n <dd>{money(order.shippingCents)}</dd>\n </div>\n )}\n <div className=\"sf-checkout-success__row sf-checkout-success__row--total\">\n <dt>{text.total}</dt>\n <dd>{money(order.totalCents)}</dd>\n </div>\n </dl>\n {lines.length > 0 && (\n <address className=\"sf-checkout-success__address\">\n <strong>{text.shipTo}</strong>\n {lines.map((line) => (\n <span key={line}>{line}</span>\n ))}\n </address>\n )}\n {fulfillment?.trackingNumber && (\n <p className=\"sf-checkout-success__tracking\">\n {fulfillment.carrier && `${fulfillment.carrier} · `}\n {fulfillment.trackingUrl ? (\n <a href={fulfillment.trackingUrl}>\n {text.tracking} {fulfillment.trackingNumber}\n </a>\n ) : (\n fulfillment.trackingNumber\n )}\n </p>\n )}\n </section>\n );\n}\n","import { useEffect, useRef } from 'react';\nimport { useCart } from './cart-provider';\n\nexport function ClearCartOnMount() {\n const { hydrated, clear } = useCart();\n const cleared = useRef(false);\n useEffect(() => {\n if (!hydrated || cleared.current) return;\n cleared.current = true;\n clear();\n }, [hydrated, clear]);\n return null;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":["_Fragment","_Fragment","_Fragment","formatPrice","VAR_NAMES","themeToCssVars","vars","key","value","theme","defaults","priority","slug","StorefrontContext","createContext","StorefrontProvider","overrides","hrefs","useStorefront","useContext","PriceTag","classes","sold","className","priceCents","currency","locale","defaultAvailabilityLabels","AvailabilityBadge","text","labels","variant","label","availability","quantity","ProductCard","product","image","renderLink","Fragment","renderImage","ProductGrid","products","emptyState","index","priorityCount","CategoryFilter","ordered","sortCategories","categories","controlled","onSelect","item","active","selected","allLabel","c","formatLabel","ProductGallery","setActive","useState","images","event","i","ShopView","slots","activeCategory","formatCategoryLabel","sortForDisplay","nextHref","loadMoreLabel","ProductDetailView","FeaturedProducts","heading","emptyCart","addLine","first","state","line","existing","l","cartReducer","action","subtotalCents","sum","itemCount","localStorageCartStorage","raw","parsed","result","cartLineSchema","CartContext","defaultStorage","CartProvider","dispatch","useReducer","hydrated","setHydrated","isOpen","setOpen","stateRef","useRef","useEffect","cancelled","storage","loaded","add","useCallback","useMemo","useCart","ctx","defaultDiscountLabels","discountReasonMessage","reason","defaultCheckoutLabels","assign","url","useCheckout","cart","busy","setBusy","message","setMessage","startCheckout","lines","options","onDiscountInvalid","remove","onRedirect","defaultAddToCartLabels","AddToCartButton","inCart","unavailable","atCap","disabled","handleClick","openOnAdd","defaultCartLineItemLabels","CartLineItem","editable","setQuantity","parseDurationMs","match","amount","useDrawerPresence","durationMs","setDurationMs","wasOpen","setWasOpen","lingering","setLingering","backdropRef","timer","DiscountField","open","setValue","submit","code","onApply","applied","onRemove","error","pending","useDiscount","items","signature","setApplied","setError","setPending","latest","request","previewedSignature","clear","run","current","id","onReject","apply","delay","discount","CartSummary","subtotal","discountLabels","checkoutDisabled","onCheckout","checkoutLabel","defaultCartDrawerLabels","FOCUSABLE","CartDrawer","panelRef","presence","trigger","handleKeyDown","last","summary","CartButton","count","showCount","hideZeroCount","ariaLabel","labelHidden","icon","defaultCheckoutSuccessLabels","addressLines","region","address","CheckoutSuccessView","money","cents","order","continueProps","continueHref","continueLink","actions","showReference","showSummaryHeading","fulfillment","ClearCartOnMount","cleared"],"sources":["../src/lib/theme.ts","../src/lib/provider.tsx","../src/components/price-tag/price-tag.tsx","../src/components/availability-badge/availability-badge.tsx","../src/components/product-card/product-card.tsx","../src/components/product-grid/product-grid.tsx","../src/components/category-filter/category-filter.tsx","../src/components/product-gallery/product-gallery.tsx","../src/views/shop-view.tsx","../src/views/product-detail-view.tsx","../src/views/featured-products.tsx","../src/cart/cart-reducer.ts","../src/cart/cart-storage.ts","../src/cart/cart-provider.tsx","../src/components/cart-summary/discount-types.ts","../src/cart/use-checkout.ts","../src/components/add-to-cart-button/add-to-cart-button.tsx","../src/components/cart-line-item/cart-line-item.tsx","../src/lib/parse-duration.ts","../src/components/cart-drawer/use-drawer-presence.ts","../src/components/cart-summary/discount-field.tsx","../src/components/cart-summary/use-discount.ts","../src/components/cart-summary/cart-summary.tsx","../src/components/cart-drawer/cart-drawer.tsx","../src/components/cart-button/cart-button.tsx","../src/components/checkout-success/labels.ts","../src/components/checkout-success/checkout-success-view.tsx","../src/cart/clear-cart-on-mount.tsx"],"sourcesContent":["export interface StorefrontTheme {\n colorBg?: string;\n colorSurface?: string;\n colorFg?: string;\n colorMuted?: string;\n colorAccent?: string;\n colorAccentFg?: string;\n colorBorder?: string;\n colorSold?: string;\n radius?: string;\n fontDisplay?: string;\n fontBody?: string;\n spaceUnit?: string;\n}\n\nconst VAR_NAMES: Record<keyof StorefrontTheme, string> = {\n colorBg: '--sf-color-bg',\n colorSurface: '--sf-color-surface',\n colorFg: '--sf-color-fg',\n colorMuted: '--sf-color-muted',\n colorAccent: '--sf-color-accent',\n colorAccentFg: '--sf-color-accent-fg',\n colorBorder: '--sf-color-border',\n colorSold: '--sf-color-sold',\n radius: '--sf-radius',\n fontDisplay: '--sf-font-display',\n fontBody: '--sf-font-body',\n spaceUnit: '--sf-space-unit',\n};\n\nexport function themeToCssVars(theme: StorefrontTheme): Record<string, string> {\n const vars: Record<string, string> = {};\n for (const key of Object.keys(VAR_NAMES) as (keyof StorefrontTheme)[]) {\n const value = theme[key];\n if (value !== undefined) vars[VAR_NAMES[key]] = value;\n }\n return vars;\n}\n","import { createContext, useContext, type ReactNode } from 'react';\n\nexport interface LinkProps {\n href: string;\n children: ReactNode;\n className?: string;\n}\n\nexport interface ImageProps {\n src: string;\n alt: string;\n sizes?: string;\n className?: string;\n priority?: boolean;\n}\n\nexport interface StorefrontConfig {\n renderLink: (props: LinkProps) => ReactNode;\n renderImage: (props: ImageProps) => ReactNode;\n locale: string;\n hrefs: {\n product: (slug: string) => string;\n category: (slug: string) => string;\n };\n}\n\nconst defaults: StorefrontConfig = {\n renderLink: ({ href, children, className }) => (\n <a href={href} className={className}>\n {children}\n </a>\n ),\n renderImage: ({ src, alt, sizes, className, priority }) => (\n <img\n src={src}\n alt={alt}\n sizes={sizes}\n className={className}\n loading={priority ? 'eager' : 'lazy'}\n decoding=\"async\"\n fetchPriority={priority ? 'high' : undefined}\n />\n ),\n locale: 'en-US',\n hrefs: {\n product: (slug) => `/shop/${slug}`,\n category: (slug) => `/shop?category=${slug}`,\n },\n};\n\nconst StorefrontContext = createContext<StorefrontConfig>(defaults);\n\nexport interface StorefrontProviderProps extends Partial<\n Omit<StorefrontConfig, 'hrefs'>\n> {\n hrefs?: Partial<StorefrontConfig['hrefs']>;\n children: ReactNode;\n}\n\nexport function StorefrontProvider({\n children,\n hrefs,\n ...overrides\n}: StorefrontProviderProps) {\n const value: StorefrontConfig = {\n ...defaults,\n ...overrides,\n hrefs: { ...defaults.hrefs, ...hrefs },\n };\n return (\n <StorefrontContext.Provider value={value}>\n {children}\n </StorefrontContext.Provider>\n );\n}\n\nexport function useStorefront(): StorefrontConfig {\n return useContext(StorefrontContext);\n}\n","import { formatPrice } from '../../lib/format-price';\nimport { useStorefront } from '../../lib/provider';\n\nexport interface PriceTagProps {\n priceCents: number;\n currency: string;\n sold?: boolean;\n className?: string;\n}\n\nexport function PriceTag({\n priceCents,\n currency,\n sold = false,\n className,\n}: PriceTagProps) {\n const { locale } = useStorefront();\n const classes = ['sf-price', sold && 'sf-price--sold', className]\n .filter(Boolean)\n .join(' ');\n return (\n <span className={classes}>{formatPrice(priceCents, currency, locale)}</span>\n );\n}\n","import type { ProductAvailability } from '@develemit/commerce-contracts';\n\nexport interface AvailabilityLabels {\n oneOfAKind: string;\n sold: string;\n reserved: string;\n available: string;\n}\n\nexport const defaultAvailabilityLabels: AvailabilityLabels = {\n oneOfAKind: 'One of a kind',\n sold: 'Sold',\n reserved: 'On hold',\n available: 'Available',\n};\n\nexport interface AvailabilityBadgeProps {\n availability: ProductAvailability;\n quantity: number;\n labels?: Partial<AvailabilityLabels>;\n className?: string;\n}\n\nexport function AvailabilityBadge({\n availability,\n quantity,\n labels,\n className,\n}: AvailabilityBadgeProps) {\n const text = { ...defaultAvailabilityLabels, ...labels };\n let variant: string;\n let label: string;\n if (availability === 'sold') {\n variant = 'sold';\n label = text.sold;\n } else if (availability === 'reserved') {\n variant = 'reserved';\n label = text.reserved;\n } else if (quantity === 1) {\n variant = 'unique';\n label = text.oneOfAKind;\n } else {\n variant = 'available';\n label = text.available;\n }\n const classes = ['sf-badge', `sf-badge--${variant}`, className]\n .filter(Boolean)\n .join(' ');\n return (\n <span role=\"status\" className={classes}>\n {label}\n </span>\n );\n}\n","import type { ProductCard as ProductCardData } from '@develemit/commerce-contracts';\nimport { useStorefront } from '../../lib/provider';\nimport {\n AvailabilityBadge,\n type AvailabilityLabels,\n} from '../availability-badge/availability-badge';\nimport { PriceTag } from '../price-tag/price-tag';\n\nexport interface ProductCardProps {\n product: ProductCardData;\n labels?: Partial<AvailabilityLabels>;\n sizes?: string;\n priority?: boolean;\n}\n\nexport function ProductCard({\n product,\n labels,\n sizes,\n priority,\n}: ProductCardProps) {\n const { renderLink, renderImage, hrefs } = useStorefront();\n const sold = product.availability === 'sold';\n const image = product.images[0];\n const classes = ['sf-card', sold && 'sf-card--sold']\n .filter(Boolean)\n .join(' ');\n return (\n <article className={classes}>\n {renderLink({\n href: hrefs.product(product.slug),\n className: 'sf-card__link',\n children: (\n <>\n <div className=\"sf-card__media\">\n {image ? (\n renderImage({\n src: image.url,\n alt: image.alt,\n sizes,\n className: 'sf-card__image',\n priority,\n })\n ) : (\n <div className=\"sf-card__placeholder\" aria-hidden=\"true\" />\n )}\n </div>\n <div className=\"sf-card__body\">\n <h3 className=\"sf-card__title\">{product.name}</h3>\n <PriceTag\n priceCents={product.priceCents}\n currency={product.currency}\n sold={sold}\n />\n <AvailabilityBadge\n availability={product.availability}\n quantity={product.quantity}\n labels={labels}\n />\n </div>\n </>\n ),\n })}\n </article>\n );\n}\n","import type { ReactNode } from 'react';\nimport type { ProductCard as ProductCardData } from '@develemit/commerce-contracts';\nimport type { AvailabilityLabels } from '../availability-badge/availability-badge';\nimport { ProductCard } from '../product-card/product-card';\n\nexport interface ProductGridProps {\n products: ProductCardData[];\n emptyState?: ReactNode;\n labels?: Partial<AvailabilityLabels>;\n priorityCount?: number;\n}\n\nexport function ProductGrid({\n products,\n emptyState,\n labels,\n priorityCount = 0,\n}: ProductGridProps) {\n if (products.length === 0) {\n return (\n <div className=\"sf-grid-empty\">{emptyState ?? 'No products found.'}</div>\n );\n }\n return (\n <ul className=\"sf-grid\">\n {products.map((product, index) => (\n <li key={product.id} className=\"sf-grid__item\">\n <ProductCard\n product={product}\n labels={labels}\n priority={index < priorityCount}\n />\n </li>\n ))}\n </ul>\n );\n}\n","import type { Category } from '@develemit/commerce-contracts';\nimport { useStorefront } from '../../lib/provider';\n\nexport interface CategoryFilterProps {\n categories: Category[];\n selected?: string;\n onSelect?: (slug: string | undefined) => void;\n allLabel?: string;\n label?: string;\n formatLabel?: (category: Category) => string;\n sortCategories?: (a: Category, b: Category) => number;\n}\n\nexport function CategoryFilter({\n categories,\n selected,\n onSelect,\n allLabel = 'All',\n label = 'Categories',\n formatLabel,\n sortCategories,\n}: CategoryFilterProps) {\n const { renderLink, hrefs } = useStorefront();\n const ordered = sortCategories\n ? [...categories].sort(sortCategories)\n : categories;\n const controlled = onSelect !== undefined;\n\n const item = (slug: string | undefined, text: string, href: string) => {\n const active = selected === slug;\n const className = `sf-filter__item${active ? ' sf-filter__item--active' : ''}`;\n return (\n <li key={slug ?? '__all'}>\n {controlled ? (\n <button\n type=\"button\"\n className={className}\n aria-pressed={active}\n onClick={() => onSelect(slug)}\n >\n {text}\n </button>\n ) : (\n renderLink({\n href,\n className,\n children: text,\n })\n )}\n </li>\n );\n };\n\n return (\n <nav aria-label={label} className=\"sf-filter\">\n <ul className=\"sf-filter__list\">\n {item(undefined, allLabel, hrefs.category(''))}\n {ordered.map((c) =>\n item(\n c.slug,\n formatLabel ? formatLabel(c) : c.label,\n hrefs.category(c.slug),\n ),\n )}\n </ul>\n </nav>\n );\n}\n","import { useState, type KeyboardEvent } from 'react';\nimport type { ProductImage } from '@develemit/commerce-contracts';\nimport { useStorefront } from '../../lib/provider';\n\nexport interface ProductGalleryProps {\n images: ProductImage[];\n sizes?: string;\n}\n\nexport function ProductGallery({ images, sizes }: ProductGalleryProps) {\n const { renderImage } = useStorefront();\n const [active, setActive] = useState(0);\n\n if (images.length === 0) return null;\n\n const onKeyDown = (event: KeyboardEvent) => {\n if (event.key === 'ArrowRight') {\n setActive((i) => (i + 1) % images.length);\n } else if (event.key === 'ArrowLeft') {\n setActive((i) => (i - 1 + images.length) % images.length);\n } else if (event.key === 'Home') {\n setActive(0);\n } else if (event.key === 'End') {\n setActive(images.length - 1);\n } else {\n return;\n }\n event.preventDefault();\n };\n\n return (\n <div\n className=\"sf-gallery\"\n role=\"group\"\n aria-roledescription=\"carousel\"\n aria-label=\"Product images\"\n tabIndex={0}\n onKeyDown={onKeyDown}\n >\n <div className=\"sf-gallery__stage\">\n {images.map((image, index) => (\n <div\n key={image.url}\n className=\"sf-gallery__slide\"\n hidden={index !== active}\n >\n {renderImage({\n src: image.url,\n alt: image.alt,\n sizes,\n priority: index === active,\n className: 'sf-gallery__image',\n })}\n </div>\n ))}\n </div>\n {images.length > 1 && (\n <div className=\"sf-gallery__thumbs\">\n {images.map((image, index) => (\n <button\n key={image.url}\n type=\"button\"\n className={`sf-gallery__thumb${index === active ? ' sf-gallery__thumb--active' : ''}`}\n aria-label={`Show image ${index + 1} of ${images.length}`}\n aria-current={index === active}\n onClick={() => setActive(index)}\n >\n {renderImage({\n src: image.url,\n alt: '',\n className: 'sf-gallery__thumb-image',\n })}\n </button>\n ))}\n </div>\n )}\n </div>\n );\n}\n","import type { ReactNode } from 'react';\nimport type { Category, ProductCard } from '@develemit/commerce-contracts';\nimport { CategoryFilter } from '../components/category-filter/category-filter';\nimport { ProductGrid } from '../components/product-grid/product-grid';\nimport type { AvailabilityLabels } from '../components/availability-badge/availability-badge';\nimport { useStorefront } from '../lib/provider';\nimport { sortForDisplay } from './sort-for-display';\n\nexport interface ShopViewSlots {\n header?: ReactNode;\n intro?: ReactNode;\n emptyState?: ReactNode;\n}\n\nexport interface ShopViewProps {\n products: ProductCard[];\n categories: Category[];\n activeCategory?: string;\n slots?: ShopViewSlots;\n labels?: Partial<AvailabilityLabels>;\n allLabel?: string;\n formatCategoryLabel?: (category: Category) => string;\n sortCategories?: (a: Category, b: Category) => number;\n nextHref?: string;\n loadMoreLabel?: string;\n}\n\nexport function ShopView({\n products,\n categories,\n activeCategory,\n slots = {},\n labels,\n allLabel,\n formatCategoryLabel,\n sortCategories,\n nextHref,\n loadMoreLabel = 'Load more',\n}: ShopViewProps) {\n const { renderLink } = useStorefront();\n return (\n <div className=\"sf-shop\">\n {slots.header}\n {slots.intro}\n <CategoryFilter\n categories={categories}\n selected={activeCategory}\n allLabel={allLabel}\n formatLabel={formatCategoryLabel}\n sortCategories={sortCategories}\n />\n <ProductGrid\n products={sortForDisplay(products)}\n emptyState={slots.emptyState}\n labels={labels}\n priorityCount={6}\n />\n {nextHref && (\n <div className=\"sf-shop__pager\">\n {renderLink({\n href: nextHref,\n className: 'sf-shop__more',\n children: loadMoreLabel,\n })}\n </div>\n )}\n </div>\n );\n}\n","import type { ReactNode } from 'react';\nimport type { Product } from '@develemit/commerce-contracts';\nimport {\n AvailabilityBadge,\n type AvailabilityLabels,\n} from '../components/availability-badge/availability-badge';\nimport { PriceTag } from '../components/price-tag/price-tag';\nimport { ProductGallery } from '../components/product-gallery/product-gallery';\n\nexport interface ProductDetailViewSlots {\n actions?: ReactNode;\n afterDetails?: ReactNode;\n}\n\nexport interface ProductDetailViewProps {\n product: Product;\n slots?: ProductDetailViewSlots;\n labels?: Partial<AvailabilityLabels>;\n}\n\nexport function ProductDetailView({\n product,\n slots = {},\n labels,\n}: ProductDetailViewProps) {\n return (\n <article className=\"sf-detail\">\n <ProductGallery images={product.images} />\n <div className=\"sf-detail__body\">\n <h1 className=\"sf-detail__title\">{product.name}</h1>\n <PriceTag priceCents={product.priceCents} currency={product.currency} />\n <AvailabilityBadge\n availability={product.availability}\n quantity={product.quantity}\n labels={labels}\n />\n <p className=\"sf-detail__description\">{product.description}</p>\n {(product.materials || product.dimensions) && (\n <dl className=\"sf-detail__specs\">\n {product.materials && (\n <>\n <dt>Materials</dt>\n <dd>{product.materials}</dd>\n </>\n )}\n {product.dimensions && (\n <>\n <dt>Dimensions</dt>\n <dd>{product.dimensions}</dd>\n </>\n )}\n </dl>\n )}\n {slots.actions && (\n <div className=\"sf-detail__actions\">{slots.actions}</div>\n )}\n {slots.afterDetails}\n </div>\n </article>\n );\n}\n","import type { ProductCard } from '@develemit/commerce-contracts';\nimport type { AvailabilityLabels } from '../components/availability-badge/availability-badge';\nimport { ProductGrid } from '../components/product-grid/product-grid';\n\nexport interface FeaturedProductsProps {\n products: ProductCard[];\n heading?: string;\n labels?: Partial<AvailabilityLabels>;\n}\n\nexport function FeaturedProducts({\n products,\n heading = 'Featured',\n labels,\n}: FeaturedProductsProps) {\n if (products.length === 0) return null;\n return (\n <section className=\"sf-featured\">\n <h2 className=\"sf-featured__heading\">{heading}</h2>\n <ProductGrid products={products} labels={labels} priorityCount={6} />\n </section>\n );\n}\n","import type { CartLine } from '@develemit/commerce-contracts';\n\nexport interface CartState {\n lines: CartLine[];\n}\n\nexport type CartAction =\n | { type: 'add'; line: CartLine }\n | { type: 'remove'; productId: string }\n | { type: 'setQuantity'; productId: string; quantity: number }\n | { type: 'clear' }\n | { type: 'hydrate'; state: CartState };\n\nexport interface AddResult {\n state: CartState;\n added: boolean;\n capped: boolean;\n reason?: 'currency-mismatch';\n}\n\nexport const emptyCart: CartState = { lines: [] };\n\nexport function addLine(state: CartState, line: CartLine): AddResult {\n const first = state.lines[0];\n if (first && first.currency !== line.currency) {\n return { state, added: false, capped: false, reason: 'currency-mismatch' };\n }\n const existing = state.lines.find((l) => l.productId === line.productId);\n if (!existing) {\n const quantity = Math.min(line.quantity, line.maxQuantity);\n return {\n state: { lines: [...state.lines, { ...line, quantity }] },\n added: true,\n capped: quantity < line.quantity,\n };\n }\n if (existing.quantity >= existing.maxQuantity) {\n return { state, added: false, capped: true };\n }\n const quantity = Math.min(\n existing.quantity + line.quantity,\n existing.maxQuantity,\n );\n return {\n state: {\n lines: state.lines.map((l) =>\n l.productId === line.productId ? { ...l, quantity } : l,\n ),\n },\n added: true,\n capped: existing.quantity + line.quantity > existing.maxQuantity,\n };\n}\n\nexport function cartReducer(state: CartState, action: CartAction): CartState {\n switch (action.type) {\n case 'add':\n return addLine(state, action.line).state;\n case 'remove':\n return {\n lines: state.lines.filter((l) => l.productId !== action.productId),\n };\n case 'setQuantity': {\n if (action.quantity <= 0) {\n return cartReducer(state, {\n type: 'remove',\n productId: action.productId,\n });\n }\n return {\n lines: state.lines.map((l) =>\n l.productId === action.productId\n ? { ...l, quantity: Math.min(action.quantity, l.maxQuantity) }\n : l,\n ),\n };\n }\n case 'clear':\n return emptyCart;\n case 'hydrate':\n return action.state;\n }\n}\n\nexport function subtotalCents(state: CartState): number {\n return state.lines.reduce((sum, l) => sum + l.priceCents * l.quantity, 0);\n}\n\nexport function itemCount(state: CartState): number {\n return state.lines.reduce((sum, l) => sum + l.quantity, 0);\n}\n","import { cartLineSchema } from '@develemit/commerce-contracts';\nimport { emptyCart, type CartState } from './cart-reducer';\n\nexport interface CartStorage {\n load: () => CartState | Promise<CartState>;\n save: (state: CartState) => void | Promise<void>;\n}\n\nexport function localStorageCartStorage(key = 'sf-cart'): CartStorage {\n return {\n load() {\n if (typeof window === 'undefined') return emptyCart;\n try {\n const raw = window.localStorage.getItem(key);\n if (!raw) return emptyCart;\n const parsed: unknown = JSON.parse(raw);\n const lines = Array.isArray((parsed as CartState)?.lines)\n ? (parsed as CartState).lines\n : [];\n const valid = lines.flatMap((line) => {\n const result = cartLineSchema.safeParse(line);\n return result.success ? [result.data] : [];\n });\n return { lines: valid };\n } catch {\n return emptyCart;\n }\n },\n save(state) {\n if (typeof window === 'undefined') return;\n try {\n window.localStorage.setItem(key, JSON.stringify(state));\n } catch {\n // storage full or blocked: the cart simply stays in memory\n }\n },\n };\n}\n","import {\n createContext,\n useCallback,\n useContext,\n useEffect,\n useMemo,\n useReducer,\n useRef,\n useState,\n type ReactNode,\n} from 'react';\nimport type { CartLine } from '@develemit/commerce-contracts';\nimport {\n addLine,\n cartReducer,\n emptyCart,\n itemCount,\n subtotalCents,\n type AddResult,\n type CartState,\n} from './cart-reducer';\nimport { localStorageCartStorage, type CartStorage } from './cart-storage';\n\nexport interface CartContextValue {\n lines: CartLine[];\n itemCount: number;\n subtotalCents: number;\n currency: string | null;\n hydrated: boolean;\n isOpen: boolean;\n open: () => void;\n close: () => void;\n add: (line: CartLine) => AddResult;\n remove: (productId: string) => void;\n setQuantity: (productId: string, quantity: number) => void;\n clear: () => void;\n}\n\nconst CartContext = createContext<CartContextValue | null>(null);\n\nexport interface CartProviderProps {\n storage?: CartStorage;\n children: ReactNode;\n}\n\nconst defaultStorage = localStorageCartStorage();\n\nexport function CartProvider({\n storage = defaultStorage,\n children,\n}: CartProviderProps) {\n const [state, dispatch] = useReducer(cartReducer, emptyCart);\n const [hydrated, setHydrated] = useState(false);\n const [isOpen, setOpen] = useState(false);\n const stateRef = useRef<CartState>(state);\n useEffect(() => {\n stateRef.current = state;\n }, [state]);\n\n useEffect(() => {\n let cancelled = false;\n Promise.resolve(storage.load()).then((loaded) => {\n if (cancelled) return;\n dispatch({ type: 'hydrate', state: loaded });\n setHydrated(true);\n });\n return () => {\n cancelled = true;\n };\n }, [storage]);\n\n useEffect(() => {\n if (hydrated) void storage.save(state);\n }, [hydrated, state, storage]);\n\n const add = useCallback((line: CartLine) => {\n const result = addLine(stateRef.current, line);\n if (result.added) {\n stateRef.current = result.state;\n dispatch({ type: 'add', line });\n }\n return result;\n }, []);\n\n const value = useMemo<CartContextValue>(\n () => ({\n lines: state.lines,\n itemCount: itemCount(state),\n subtotalCents: subtotalCents(state),\n currency: state.lines[0]?.currency ?? null,\n hydrated,\n isOpen,\n open: () => setOpen(true),\n close: () => setOpen(false),\n add,\n remove: (productId) => dispatch({ type: 'remove', productId }),\n setQuantity: (productId, quantity) =>\n dispatch({ type: 'setQuantity', productId, quantity }),\n clear: () => dispatch({ type: 'clear' }),\n }),\n [state, hydrated, isOpen, add],\n );\n\n return <CartContext.Provider value={value}>{children}</CartContext.Provider>;\n}\n\nexport function useCart(): CartContextValue {\n const ctx = useContext(CartContext);\n if (!ctx) throw new Error('useCart must be used inside <CartProvider>');\n return ctx;\n}\n","import type { DiscountRejectionReason } from '@develemit/commerce-contracts';\n\nexport type DiscountPreviewResult =\n | { ok: true; code: string; discountCents: number }\n | { ok: false; reason: DiscountRejectionReason };\n\nexport interface CartSummaryDiscount {\n onPreview: (\n code: string,\n items: { productId: string; quantity: number }[],\n ) => Promise<DiscountPreviewResult>;\n onApply?: (code: string | null) => void;\n debounceMs?: number;\n}\n\nexport interface DiscountLabels {\n toggle: string;\n field: string;\n apply: string;\n remove: string;\n line: string;\n reasons: Record<DiscountRejectionReason, string>;\n unavailable: string;\n}\n\nexport const defaultDiscountLabels: DiscountLabels = {\n toggle: 'Have a code?',\n field: 'Discount code',\n apply: 'Apply',\n remove: 'Remove',\n line: 'Discount',\n unavailable: 'We could not check that code. Please try again.',\n reasons: {\n not_found: 'That code is not valid.',\n inactive: 'That code is not valid.',\n not_started: 'That code is not active yet.',\n expired: 'That code has expired.',\n min_subtotal: 'Your cart is below the minimum for that code.',\n exhausted: 'That code is no longer available.',\n currency: 'That code does not apply to this cart.',\n },\n};\n\nexport function discountReasonMessage(\n reason: DiscountRejectionReason,\n overrides?: Partial<DiscountLabels['reasons']>,\n): string {\n return overrides?.[reason] ?? defaultDiscountLabels.reasons[reason];\n}\n","import { useCallback, useState } from 'react';\nimport type { DiscountRejectionReason } from '@develemit/commerce-contracts';\nimport type { CheckoutResult } from '../client/checkout-result';\nimport { discountReasonMessage } from '../components/cart-summary/discount-types';\nimport { useCart } from './cart-provider';\n\nexport interface CheckoutLabels {\n outOfStock: string;\n unavailable: string;\n discountReasons?: Partial<Record<DiscountRejectionReason, string>>;\n}\n\nexport const defaultCheckoutLabels: CheckoutLabels = {\n outOfStock:\n 'Some items just sold or are on hold, so we removed them from your cart.',\n unavailable: 'Checkout is unavailable right now. Please try again.',\n};\n\nexport interface StartCheckoutOptions {\n discountCode?: string;\n}\n\nexport interface UseCheckoutOptions {\n startCheckout: (\n items: { productId: string; quantity: number }[],\n options: StartCheckoutOptions,\n ) => Promise<CheckoutResult>;\n onRedirect?: (url: string) => void;\n onDiscountInvalid?: (reason: DiscountRejectionReason) => void;\n labels?: Partial<CheckoutLabels>;\n}\n\nexport interface UseCheckoutValue {\n checkout: (options?: StartCheckoutOptions) => Promise<void>;\n busy: boolean;\n message: string | null;\n clearMessage: () => void;\n}\n\nconst assign = (url: string) => window.location.assign(url);\n\nexport function useCheckout({\n startCheckout,\n onRedirect = assign,\n onDiscountInvalid,\n labels,\n}: UseCheckoutOptions): UseCheckoutValue {\n const cart = useCart();\n const [busy, setBusy] = useState(false);\n const [message, setMessage] = useState<string | null>(null);\n const { lines, remove } = cart;\n const text = { ...defaultCheckoutLabels, ...labels };\n\n const checkout = useCallback(\n async (options: StartCheckoutOptions = {}) => {\n setBusy(true);\n setMessage(null);\n try {\n const result = await startCheckout(\n lines.map((l) => ({ productId: l.productId, quantity: l.quantity })),\n options,\n );\n if ('url' in result) {\n onRedirect(result.url);\n return;\n }\n if (result.error === 'discount_invalid') {\n setMessage(\n discountReasonMessage(result.reason, text.discountReasons),\n );\n onDiscountInvalid?.(result.reason);\n } else {\n result.productIds.forEach(remove);\n setMessage(text.outOfStock);\n }\n } catch {\n setMessage(text.unavailable);\n }\n setBusy(false);\n },\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [startCheckout, onRedirect, onDiscountInvalid, lines, remove, labels],\n );\n\n const clearMessage = useCallback(() => setMessage(null), []);\n\n return { checkout, busy, message, clearMessage };\n}\n","import type { Product } from '@develemit/commerce-contracts';\nimport { useCart } from '../../cart/cart-provider';\n\nexport interface AddToCartLabels {\n add: string;\n inCart: string;\n unavailable: string;\n}\n\nexport const defaultAddToCartLabels: AddToCartLabels = {\n add: 'Add to cart',\n inCart: 'In your cart',\n unavailable: 'Unavailable',\n};\n\nexport interface AddToCartButtonProps {\n product: Product;\n labels?: Partial<AddToCartLabels>;\n openOnAdd?: boolean;\n className?: string;\n}\n\nexport function AddToCartButton({\n product,\n labels,\n openOnAdd = true,\n className,\n}: AddToCartButtonProps) {\n const cart = useCart();\n const text = { ...defaultAddToCartLabels, ...labels };\n const inCart =\n cart.lines.find((l) => l.productId === product.id)?.quantity ?? 0;\n const unavailable = product.availability !== 'available';\n const atCap = inCart >= product.quantity;\n const disabled = !cart.hydrated || unavailable || atCap;\n const label = unavailable ? text.unavailable : atCap ? text.inCart : text.add;\n\n function handleClick() {\n const result = cart.add({\n productId: product.id,\n slug: product.slug,\n name: product.name,\n priceCents: product.priceCents,\n currency: product.currency,\n imageUrl: product.images[0]?.url,\n quantity: 1,\n maxQuantity: product.quantity,\n });\n if (result.added && openOnAdd) cart.open();\n }\n\n return (\n <button\n type=\"button\"\n className={['sf-button', className].filter(Boolean).join(' ')}\n disabled={disabled}\n onClick={handleClick}\n >\n {label}\n </button>\n );\n}\n","import type { CartLine } from '@develemit/commerce-contracts';\nimport { useCart } from '../../cart/cart-provider';\nimport { formatPrice } from '../../lib/format-price';\nimport { useStorefront } from '../../lib/provider';\n\nexport interface CartLineItemLabels {\n remove: string;\n quantity: string;\n}\n\nexport const defaultCartLineItemLabels: CartLineItemLabels = {\n remove: 'Remove',\n quantity: 'Quantity',\n};\n\nexport interface CartLineItemProps {\n line: CartLine;\n labels?: Partial<CartLineItemLabels>;\n}\n\nexport function CartLineItem({ line, labels }: CartLineItemProps) {\n const { remove, setQuantity } = useCart();\n const { locale, renderLink, renderImage, hrefs } = useStorefront();\n const text = { ...defaultCartLineItemLabels, ...labels };\n const editable = line.maxQuantity > 1;\n return (\n <li className=\"sf-cart-line\">\n {line.imageUrl &&\n renderImage({\n src: line.imageUrl,\n alt: '',\n sizes: '80px',\n className: 'sf-cart-line__image',\n })}\n <div className=\"sf-cart-line__body\">\n {renderLink({\n href: hrefs.product(line.slug),\n className: 'sf-cart-line__name',\n children: line.name,\n })}\n <span className=\"sf-cart-line__price\">\n {formatPrice(line.priceCents * line.quantity, line.currency, locale)}\n </span>\n {editable && (\n <label className=\"sf-cart-line__qty\">\n {text.quantity}\n <input\n type=\"number\"\n min={1}\n max={line.maxQuantity}\n value={line.quantity}\n onChange={(e) =>\n setQuantity(line.productId, Number(e.target.value))\n }\n />\n </label>\n )}\n <button\n type=\"button\"\n className=\"sf-link-button sf-cart-line__remove\"\n onClick={() => remove(line.productId)}\n aria-label={`${text.remove} ${line.name}`}\n >\n {text.remove}\n </button>\n </div>\n </li>\n );\n}\n","export function parseDurationMs(value: string): number {\n const match = /^\\s*(-?\\d*\\.?\\d+)\\s*(ms|s)\\s*$/.exec(value);\n if (!match) return 0;\n const amount = Number(match[1]);\n return Math.max(0, match[2] === 's' ? amount * 1000 : amount);\n}\n","import { useEffect, useState, type RefObject } from 'react';\nimport { parseDurationMs } from '../../lib/parse-duration';\n\nexport function useDrawerPresence(\n isOpen: boolean,\n backdropRef: RefObject<HTMLElement | null>,\n) {\n const [durationMs, setDurationMs] = useState(0);\n const [wasOpen, setWasOpen] = useState(isOpen);\n const [lingering, setLingering] = useState(false);\n\n if (wasOpen !== isOpen) {\n setWasOpen(isOpen);\n setLingering(!isOpen && durationMs > 0);\n }\n\n useEffect(() => {\n if (!isOpen || !backdropRef.current) return;\n setDurationMs(\n parseDurationMs(\n getComputedStyle(backdropRef.current).getPropertyValue(\n '--sf-motion-duration',\n ),\n ),\n );\n }, [isOpen, backdropRef]);\n\n useEffect(() => {\n if (!lingering) return;\n const timer = setTimeout(() => setLingering(false), durationMs);\n return () => clearTimeout(timer);\n }, [lingering, durationMs]);\n\n return { mounted: isOpen || lingering, state: isOpen ? 'open' : 'closed' };\n}\n","import { useState, type FormEvent } from 'react';\nimport { formatPrice } from '../../lib/format-price';\nimport type { DiscountLabels } from './discount-types';\nimport type { AppliedDiscount, DiscountError } from './use-discount';\n\nexport interface DiscountFieldProps {\n labels: DiscountLabels;\n applied: AppliedDiscount | null;\n error: DiscountError | null;\n pending: boolean;\n currency: string;\n locale: string;\n onApply: (code: string) => void;\n onRemove: () => void;\n}\n\nexport function DiscountField({\n labels,\n applied,\n error,\n pending,\n currency,\n locale,\n onApply,\n onRemove,\n}: DiscountFieldProps) {\n const [open, setOpen] = useState(false);\n const [value, setValue] = useState('');\n\n function submit(event: FormEvent) {\n event.preventDefault();\n const code = value.trim();\n if (code) onApply(code);\n }\n\n if (applied) {\n return (\n <p className=\"sf-cart-summary__row sf-cart-summary__discount\">\n <span>\n {labels.line} ({applied.code}){' '}\n <button type=\"button\" className=\"sf-link-button\" onClick={onRemove}>\n {labels.remove}\n </button>\n </span>\n <span className=\"sf-price\">\n −{formatPrice(applied.discountCents, currency, locale)}\n </span>\n </p>\n );\n }\n\n const message =\n error === 'unavailable'\n ? labels.unavailable\n : error\n ? labels.reasons[error]\n : null;\n return (\n <div className=\"sf-cart-summary__code\">\n {open ? (\n <form onSubmit={submit} className=\"sf-cart-summary__code-form\">\n <input\n aria-label={labels.field}\n value={value}\n onChange={(e) => setValue(e.target.value)}\n autoCapitalize=\"characters\"\n autoComplete=\"off\"\n />\n <button type=\"submit\" className=\"sf-button\" disabled={pending}>\n {labels.apply}\n </button>\n </form>\n ) : (\n <button\n type=\"button\"\n className=\"sf-link-button\"\n aria-expanded={false}\n onClick={() => setOpen(true)}\n >\n {labels.toggle}\n </button>\n )}\n {message && (\n <p role=\"alert\" className=\"sf-cart-summary__code-error\">\n {message}\n </p>\n )}\n </div>\n );\n}\n","import type { DiscountRejectionReason } from '@develemit/commerce-contracts';\nimport { useCallback, useEffect, useRef, useState } from 'react';\nimport { useCart } from '../../cart/cart-provider';\nimport type { CartSummaryDiscount } from './discount-types';\n\nexport interface AppliedDiscount {\n code: string;\n discountCents: number;\n}\n\nexport type DiscountError = DiscountRejectionReason | 'unavailable';\n\nexport function useDiscount(discount: CartSummaryDiscount | undefined) {\n const { lines } = useCart();\n const items = lines.map((l) => ({\n productId: l.productId,\n quantity: l.quantity,\n }));\n const signature = items.map((i) => `${i.productId}:${i.quantity}`).join(',');\n const [applied, setApplied] = useState<AppliedDiscount | null>(null);\n const [error, setError] = useState<DiscountError | null>(null);\n const [pending, setPending] = useState(false);\n const latest = useRef({ discount, items });\n useEffect(() => {\n latest.current = { discount, items };\n });\n const request = useRef(0);\n const previewedSignature = useRef(signature);\n\n const clear = useCallback(() => {\n request.current += 1;\n setApplied(null);\n latest.current.discount?.onApply?.(null);\n }, []);\n\n const run = useCallback(\n async (code: string, onReject: (reason: DiscountError) => void) => {\n const current = latest.current;\n if (!current.discount) return;\n const id = ++request.current;\n previewedSignature.current = current.items\n .map((i) => `${i.productId}:${i.quantity}`)\n .join(',');\n setPending(true);\n let result;\n try {\n result = await current.discount.onPreview(code, current.items);\n } catch {\n result = { ok: false as const, reason: 'unavailable' as const };\n }\n if (id !== request.current) return;\n setPending(false);\n if (result.ok) {\n setError(null);\n setApplied({ code: result.code, discountCents: result.discountCents });\n current.discount.onApply?.(result.code);\n } else {\n onReject(result.reason);\n }\n },\n [],\n );\n\n const apply = useCallback(\n (code: string) => run(code, (reason) => setError(reason)),\n [run],\n );\n\n const remove = useCallback(() => {\n setError(null);\n setPending(false);\n clear();\n }, [clear]);\n\n const code = applied?.code;\n const delay = discount?.debounceMs ?? 300;\n useEffect(() => {\n if (!code || signature === previewedSignature.current) return;\n const timer = setTimeout(() => {\n if (latest.current.items.length === 0) {\n clear();\n return;\n }\n void run(code, (reason) => {\n setError(reason);\n clear();\n });\n }, delay);\n return () => clearTimeout(timer);\n }, [code, signature, delay, run, clear]);\n\n return { applied, error, pending, apply, remove };\n}\n","import { useCart } from '../../cart/cart-provider';\nimport { formatPrice } from '../../lib/format-price';\nimport { useStorefront } from '../../lib/provider';\nimport { DiscountField } from './discount-field';\nimport {\n defaultDiscountLabels,\n type CartSummaryDiscount,\n type DiscountLabels,\n} from './discount-types';\nimport { useDiscount } from './use-discount';\n\nexport type {\n CartSummaryDiscount,\n DiscountPreviewResult,\n} from './discount-types';\nexport { discountReasonMessage } from './discount-types';\n\nexport interface CartSummaryLabels {\n subtotal: string;\n discount: Partial<Omit<DiscountLabels, 'reasons'>> & {\n reasons?: Partial<DiscountLabels['reasons']>;\n };\n}\n\nexport interface CartSummaryProps {\n onCheckout?: () => void;\n checkoutLabel?: string;\n checkoutDisabled?: boolean;\n labels?: Partial<CartSummaryLabels>;\n discount?: CartSummaryDiscount;\n}\n\nexport function CartSummary({\n onCheckout,\n checkoutLabel = 'Checkout',\n checkoutDisabled = false,\n labels,\n discount,\n}: CartSummaryProps) {\n const cart = useCart();\n const { locale } = useStorefront();\n const subtotal = labels?.subtotal ?? 'Subtotal';\n const state = useDiscount(discount);\n const discountLabels: DiscountLabels = {\n ...defaultDiscountLabels,\n ...labels?.discount,\n reasons: {\n ...defaultDiscountLabels.reasons,\n ...labels?.discount?.reasons,\n },\n };\n return (\n <div className=\"sf-cart-summary\">\n <p className=\"sf-cart-summary__row\">\n <span>{subtotal}</span>\n <span className=\"sf-price\">\n {formatPrice(cart.subtotalCents, cart.currency ?? 'USD', locale)}\n </span>\n </p>\n {discount && cart.lines.length > 0 && (\n <DiscountField\n labels={discountLabels}\n currency={cart.currency ?? 'USD'}\n locale={locale}\n onApply={(code) => void state.apply(code)}\n onRemove={state.remove}\n applied={state.applied}\n error={state.error}\n pending={state.pending}\n />\n )}\n <button\n type=\"button\"\n className=\"sf-button\"\n disabled={checkoutDisabled || !onCheckout || cart.lines.length === 0}\n onClick={onCheckout}\n >\n {checkoutLabel}\n </button>\n </div>\n );\n}\n","import { useEffect, useRef, type KeyboardEvent, type ReactNode } from 'react';\nimport { useCart } from '../../cart/cart-provider';\nimport { CartLineItem } from '../cart-line-item/cart-line-item';\nimport { useDrawerPresence } from './use-drawer-presence';\nimport {\n CartSummary,\n type CartSummaryProps,\n} from '../cart-summary/cart-summary';\n\nexport interface CartDrawerLabels {\n title: string;\n close: string;\n empty: string;\n}\n\nexport const defaultCartDrawerLabels: CartDrawerLabels = {\n title: 'Your cart',\n close: 'Close',\n empty: 'Your cart is empty.',\n};\n\nexport interface CartDrawerProps {\n labels?: Partial<CartDrawerLabels>;\n summary?: CartSummaryProps;\n slots?: CartDrawerSlots;\n}\n\nexport interface CartDrawerSlots {\n empty?: ReactNode;\n summaryNote?: ReactNode;\n}\n\nconst FOCUSABLE =\n 'a[href], button:not([disabled]), input:not([disabled]), [tabindex]:not([tabindex=\"-1\"])';\n\nexport function CartDrawer({ labels, summary, slots }: CartDrawerProps) {\n const cart = useCart();\n const text = { ...defaultCartDrawerLabels, ...labels };\n const panelRef = useRef<HTMLDivElement>(null);\n const backdropRef = useRef<HTMLDivElement>(null);\n const { isOpen } = cart;\n const presence = useDrawerPresence(isOpen, backdropRef);\n\n useEffect(() => {\n if (!isOpen) return;\n const trigger = document.activeElement as HTMLElement | null;\n panelRef.current?.querySelector<HTMLElement>(FOCUSABLE)?.focus();\n return () => trigger?.focus();\n }, [isOpen]);\n\n if (!presence.mounted) return null;\n\n function handleKeyDown(event: KeyboardEvent<HTMLDivElement>) {\n if (event.key === 'Escape') {\n event.stopPropagation();\n cart.close();\n return;\n }\n if (event.key !== 'Tab') return;\n const items = Array.from(\n panelRef.current?.querySelectorAll<HTMLElement>(FOCUSABLE) ?? [],\n );\n if (items.length === 0) return;\n const first = items[0];\n const last = items[items.length - 1];\n if (event.shiftKey && document.activeElement === first) {\n event.preventDefault();\n last.focus();\n } else if (!event.shiftKey && document.activeElement === last) {\n event.preventDefault();\n first.focus();\n }\n }\n\n return (\n <div\n ref={backdropRef}\n className=\"sf-drawer-backdrop\"\n data-state={presence.state}\n onClick={cart.close}\n >\n <div\n ref={panelRef}\n className=\"sf-drawer\"\n data-state={presence.state}\n role=\"dialog\"\n aria-modal=\"true\"\n aria-labelledby=\"sf-drawer-title\"\n onKeyDown={handleKeyDown}\n onClick={(e) => e.stopPropagation()}\n >\n <div className=\"sf-drawer__header\">\n <h2 id=\"sf-drawer-title\" className=\"sf-drawer__title\">\n {text.title}\n </h2>\n <button\n type=\"button\"\n className=\"sf-drawer__close\"\n aria-label={text.close}\n onClick={cart.close}\n >\n <span aria-hidden=\"true\">×</span>\n </button>\n </div>\n {cart.lines.length === 0 ? (\n <div className=\"sf-drawer__empty\">\n {slots?.empty ?? <p>{text.empty}</p>}\n </div>\n ) : (\n <>\n <ul className=\"sf-drawer__lines\">\n {cart.lines.map((line) => (\n <CartLineItem key={line.productId} line={line} />\n ))}\n </ul>\n <CartSummary {...summary} />\n {slots?.summaryNote && (\n <div className=\"sf-drawer__note\">{slots.summaryNote}</div>\n )}\n </>\n )}\n </div>\n </div>\n );\n}\n","import type { ReactNode } from 'react';\nimport { useCart } from '../../cart/cart-provider';\n\nexport interface CartButtonProps {\n label?: string;\n className?: string;\n icon?: ReactNode;\n labelHidden?: boolean;\n hideZeroCount?: boolean;\n}\n\nexport function CartButton({\n label = 'Cart',\n className,\n icon,\n labelHidden = false,\n hideZeroCount = false,\n}: CartButtonProps) {\n const cart = useCart();\n const count = cart.hydrated ? cart.itemCount : 0;\n const showCount = !(hideZeroCount && count === 0);\n const ariaLabel = showCount\n ? labelHidden\n ? `${label}, ${cart.itemCount} items`\n : undefined\n : `${label}, empty`;\n return (\n <button\n type=\"button\"\n className={['sf-cart-button', className].filter(Boolean).join(' ')}\n aria-haspopup=\"dialog\"\n aria-label={ariaLabel}\n onClick={cart.open}\n >\n {icon}\n <span className={labelHidden ? 'sf-visually-hidden' : undefined}>\n {label}\n </span>\n {showCount && (\n <span\n className=\"sf-cart-button__count\"\n aria-label={`${cart.itemCount} items`}\n >\n {count}\n </span>\n )}\n </button>\n );\n}\n","import type { OrderStatus } from '@develemit/commerce-contracts';\n\nexport interface CheckoutSuccessLabels {\n heading: string;\n summaryHeading: string;\n reference: string;\n continue: string;\n subtotal: string;\n discount: string;\n shipping: string;\n total: string;\n shipTo: string;\n tracking: string;\n status: Record<OrderStatus, string>;\n}\n\nexport const defaultCheckoutSuccessLabels: CheckoutSuccessLabels = {\n heading: 'Order received',\n summaryHeading: 'Order summary',\n reference: 'Order {id}',\n continue: 'Continue shopping',\n subtotal: 'Subtotal',\n discount: 'Discount',\n shipping: 'Shipping',\n total: 'Total',\n shipTo: 'Ship to',\n tracking: 'Track',\n status: {\n pending: \"We're confirming your payment and will email you once it's done.\",\n paid: 'Payment received. Thank you!',\n shipped: 'Your order is on its way.',\n cancelled: 'This checkout was cancelled.',\n expired: 'This checkout expired before payment.',\n refunded: 'This order has been refunded.',\n needs_attention:\n \"We couldn't confirm one of your items. We'll be in touch about a refund.\",\n },\n};\n","import type { ReactNode } from 'react';\nimport type { OrderAddress, OrderSummary } from '@develemit/commerce-contracts';\nimport { formatPrice } from '../../lib/format-price';\nimport {\n defaultCheckoutSuccessLabels,\n type CheckoutSuccessLabels,\n} from './labels';\n\nexport interface CheckoutSuccessViewProps {\n order: OrderSummary & { shippingAddress?: OrderAddress | null };\n locale?: string;\n labels?: Partial<Omit<CheckoutSuccessLabels, 'status'>> & {\n status?: Partial<CheckoutSuccessLabels['status']>;\n };\n className?: string;\n slots?: {\n header?: ReactNode;\n beforeLines?: ReactNode;\n afterTotals?: ReactNode;\n actions?: ReactNode;\n };\n showReference?: boolean;\n showSummaryHeading?: boolean;\n continueHref?: string;\n renderLink?: (props: {\n href: string;\n children: ReactNode;\n className?: string;\n }) => ReactNode;\n}\n\nfunction addressLines(address: OrderAddress): string[] {\n const region = [address.city, address.state, address.postalCode]\n .filter(Boolean)\n .join(' ');\n return [\n address.name,\n address.line1,\n address.line2,\n region,\n address.country,\n ].filter((line): line is string => Boolean(line));\n}\n\nexport function CheckoutSuccessView({\n order,\n locale,\n labels,\n className,\n slots,\n showReference,\n showSummaryHeading,\n continueHref,\n renderLink,\n}: CheckoutSuccessViewProps) {\n const text = {\n ...defaultCheckoutSuccessLabels,\n ...labels,\n status: { ...defaultCheckoutSuccessLabels.status, ...labels?.status },\n };\n const money = (cents: number) => formatPrice(cents, order.currency, locale);\n const lines = order.shippingAddress\n ? addressLines(order.shippingAddress)\n : [];\n const { fulfillment } = order;\n const continueProps = {\n href: continueHref ?? '',\n className: 'sf-checkout-success__continue',\n children: text.continue,\n };\n const continueLink = continueHref ? (\n renderLink ? (\n renderLink(continueProps)\n ) : (\n <a {...continueProps} />\n )\n ) : null;\n const actions = slots?.actions ?? continueLink;\n return (\n <section\n className={['sf-checkout-success', className].filter(Boolean).join(' ')}\n data-status={order.status}\n >\n <h1 className=\"sf-checkout-success__heading\">{text.heading}</h1>\n {showReference && (\n <p className=\"sf-checkout-success__reference\">\n {text.reference.replace('{id}', order.id)}\n </p>\n )}\n {slots?.header}\n <p className=\"sf-checkout-success__status\">{text.status[order.status]}</p>\n {showSummaryHeading && (\n <h2 className=\"sf-checkout-success__summary-heading\">\n {text.summaryHeading}\n </h2>\n )}\n {slots?.beforeLines}\n <ul className=\"sf-checkout-success__lines\">\n {order.items.map((item) => (\n <li key={item.productId} className=\"sf-checkout-success__line\">\n <span>\n {item.name} × {item.quantity}\n </span>\n <span>{money(item.priceCents * item.quantity)}</span>\n </li>\n ))}\n </ul>\n <dl className=\"sf-checkout-success__totals\">\n <div className=\"sf-checkout-success__row\">\n <dt>{text.subtotal}</dt>\n <dd>{money(order.subtotalCents)}</dd>\n </div>\n {order.discountCents > 0 && (\n <div\n className=\"sf-checkout-success__row sf-checkout-success__row--discount\"\n data-discount\n >\n <dt>\n {text.discount}\n {order.discountCode && ` (${order.discountCode})`}\n </dt>\n <dd>−{money(order.discountCents)}</dd>\n </div>\n )}\n {order.shippingLabel && (\n <div className=\"sf-checkout-success__row\">\n <dt>\n {text.shipping} ({order.shippingLabel})\n </dt>\n <dd>{money(order.shippingCents)}</dd>\n </div>\n )}\n <div className=\"sf-checkout-success__row sf-checkout-success__row--total\">\n <dt>{text.total}</dt>\n <dd>{money(order.totalCents)}</dd>\n </div>\n </dl>\n {slots?.afterTotals}\n {lines.length > 0 && (\n <address className=\"sf-checkout-success__address\">\n <strong>{text.shipTo}</strong>\n {lines.map((line) => (\n <span key={line}>{line}</span>\n ))}\n </address>\n )}\n {fulfillment?.trackingNumber && (\n <p className=\"sf-checkout-success__tracking\">\n {fulfillment.carrier && `${fulfillment.carrier} · `}\n {fulfillment.trackingUrl ? (\n <a href={fulfillment.trackingUrl}>\n {text.tracking} {fulfillment.trackingNumber}\n </a>\n ) : (\n fulfillment.trackingNumber\n )}\n </p>\n )}\n {actions && <div className=\"sf-checkout-success__actions\">{actions}</div>}\n </section>\n );\n}\n","import { useEffect, useRef } from 'react';\nimport { useCart } from './cart-provider';\n\nexport function ClearCartOnMount() {\n const { hydrated, clear } = useCart();\n const cleared = useRef(false);\n useEffect(() => {\n if (!hydrated || cleared.current) return;\n cleared.current = true;\n clear();\n }, [hydrated, clear]);\n return null;\n}\n"],"mappings":"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"}
|
package/dist/styles.css
CHANGED
|
@@ -531,6 +531,25 @@
|
|
|
531
531
|
.sf-checkout-success__tracking {
|
|
532
532
|
margin: 0;
|
|
533
533
|
}
|
|
534
|
+
|
|
535
|
+
.sf-checkout-success__reference {
|
|
536
|
+
color: var(--sf-muted, inherit);
|
|
537
|
+
font-size: 0.875em;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
.sf-checkout-success__summary-heading {
|
|
541
|
+
font-size: 1.125em;
|
|
542
|
+
margin: 1rem 0 0.5rem;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
.sf-checkout-success__actions {
|
|
546
|
+
margin-top: 1.5rem;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
.sf-checkout-success__continue {
|
|
550
|
+
color: var(--sf-accent, inherit);
|
|
551
|
+
text-decoration: underline;
|
|
552
|
+
}
|
|
534
553
|
@media (prefers-reduced-motion: reduce) {
|
|
535
554
|
.sf-card,
|
|
536
555
|
.sf-card__image {
|
package/docs/cart.md
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# Cart and checkout
|
|
2
|
+
|
|
3
|
+
[← @develemit/storefront](../README.md)
|
|
4
|
+
|
|
5
|
+
## Cart
|
|
6
|
+
|
|
7
|
+
Wrap the app in `CartProvider` (inside `StorefrontProvider`). State is a
|
|
8
|
+
snapshot of `CartLine`s; prices and availability are re-verified server-side
|
|
9
|
+
at checkout. Persistence goes through a `CartStorage` (`load`/`save`); the
|
|
10
|
+
default is `localStorageCartStorage(key)`, which hydrates after mount and never
|
|
11
|
+
touches `window` during render.
|
|
12
|
+
|
|
13
|
+
```tsx
|
|
14
|
+
<CartProvider>
|
|
15
|
+
<CartButton />
|
|
16
|
+
<CartDrawer summary={{ onCheckout, checkoutLabel: 'Checkout' }} />
|
|
17
|
+
</CartProvider>
|
|
18
|
+
|
|
19
|
+
<ProductDetailView
|
|
20
|
+
product={product}
|
|
21
|
+
slots={{ actions: <AddToCartButton product={product} /> }}
|
|
22
|
+
/>
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
`useCart()` exposes `add` (returns `{ added, capped, reason? }`), `remove`,
|
|
26
|
+
`setQuantity`, `clear`, `open`/`close`, and the derived `itemCount` and
|
|
27
|
+
`subtotalCents`. A product's `quantity` is the line's `maxQuantity`, so
|
|
28
|
+
one-of-a-kind items can only be added once. Every label is overridable
|
|
29
|
+
through a `labels` prop; the defaults are exported (`defaultCartDrawerLabels`,
|
|
30
|
+
`defaultCartLineItemLabels`, `defaultAddToCartLabels`, `defaultCheckoutLabels`,
|
|
31
|
+
`defaultCheckoutSuccessLabels`, `defaultAvailabilityLabels`) so you can spread
|
|
32
|
+
and change a few. The pure cart logic (`cartReducer`, `addLine`, `emptyCart`)
|
|
33
|
+
is exported for hosts that keep the cart outside React.
|
|
34
|
+
|
|
35
|
+
## Checkout
|
|
36
|
+
|
|
37
|
+
`useCheckout({ startCheckout, onRedirect?, onDiscountInvalid?, labels? })`
|
|
38
|
+
wraps the host's checkout call (a server action or a fetch) and returns
|
|
39
|
+
`{ checkout, busy, message, clearMessage }`. `startCheckout(items, { discountCode })`
|
|
40
|
+
resolves to a `CheckoutResult`: `{ url }`, `{ error: 'out_of_stock', productIds }`
|
|
41
|
+
or `{ error: 'discount_invalid', reason }`. On the server, `toCheckoutResult(error)`
|
|
42
|
+
from `@develemit/storefront/client` maps `CheckoutConflictError` and
|
|
43
|
+
`DiscountInvalidError` to those results (other errors are rethrown). The hook
|
|
44
|
+
removes out-of-stock lines, sets `message`, and redirects with
|
|
45
|
+
`window.location.assign` unless `onRedirect` is given. Copy is overridable
|
|
46
|
+
through `labels` (`outOfStock`, `unavailable`, `discountReasons`).
|
|
47
|
+
|
|
48
|
+
## Success page
|
|
49
|
+
|
|
50
|
+
`<CheckoutSuccessView order={order} locale? labels? />` renders an order from
|
|
51
|
+
`getOrderBySession`: lines, subtotal, discount row (only when applied),
|
|
52
|
+
shipping, total, ship-to address (only when you pass an order that includes
|
|
53
|
+
`shippingAddress`; the public order-by-session route never returns it),
|
|
54
|
+
tracking, and a message per `OrderStatus` (`pending` tells the buyer to expect
|
|
55
|
+
an email). It has no hooks, so a server component can render it. Labels live in
|
|
56
|
+
`labels` (including `labels.status`). `<ClearCartOnMount />` empties the cart
|
|
57
|
+
once, even under React strict mode. Styles are `.sf-checkout-success*`.
|
|
58
|
+
|
|
59
|
+
Extension points, all optional: `slots={{ header, beforeLines, afterTotals,
|
|
60
|
+
actions }}` (ReactNode) put your own content under the heading, above the
|
|
61
|
+
lines, below the totals, and in the footer. `showReference` adds a
|
|
62
|
+
`labels.reference` row (default `Order {id}`), and `showSummaryHeading` adds an
|
|
63
|
+
`<h2>` from `labels.summaryHeading`. `continueHref` renders a link labelled
|
|
64
|
+
`labels.continue` unless `slots.actions` is given; pass `renderLink` to route it
|
|
65
|
+
through your framework's link (the view uses no hooks, so it can't read the
|
|
66
|
+
provider's).
|
|
67
|
+
|
|
68
|
+
## Discount codes
|
|
69
|
+
|
|
70
|
+
`CartSummary` (and `CartDrawer` through `summary`) takes an optional `discount`
|
|
71
|
+
prop. The kit stays framework-agnostic, so the host performs the API call (a
|
|
72
|
+
server action in Next):
|
|
73
|
+
|
|
74
|
+
```tsx
|
|
75
|
+
<CartDrawer
|
|
76
|
+
summary={{
|
|
77
|
+
onCheckout: checkout,
|
|
78
|
+
discount: {
|
|
79
|
+
onPreview: async (code, items) => {
|
|
80
|
+
// call client.previewDiscount; map DiscountInvalidError to a reason
|
|
81
|
+
return { ok: true, code, discountCents: 480 };
|
|
82
|
+
// or { ok: false, reason: 'exhausted' }
|
|
83
|
+
},
|
|
84
|
+
onApply: (code) => setDiscountCode(code), // null when removed or dropped
|
|
85
|
+
},
|
|
86
|
+
}}
|
|
87
|
+
/>
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
It renders a collapsible "Have a code?" field, a "Discount (CODE) −$4.80" row
|
|
91
|
+
with Remove, and a message for a rejected code. When the cart changes
|
|
92
|
+
(debounced, `debounceMs`, default 300) the code is previewed again and dropped
|
|
93
|
+
with its reason if it no longer applies. Pass the applied code to
|
|
94
|
+
`createCheckout`. `discountReasonMessage(reason, overrides?)` returns the same
|
|
95
|
+
text for a reason, for showing a code rejected at checkout. Override copy with `labels.discount` (`toggle`, `field`,
|
|
96
|
+
`apply`, `remove`, `line`, `unavailable`, and `reasons` per rejection reason).
|
|
97
|
+
|
|
98
|
+
## Polish hooks
|
|
99
|
+
|
|
100
|
+
- `CartDrawer` `slots={{ empty, summaryNote }}`: `empty` replaces the
|
|
101
|
+
`labels.empty` text (for example with a link back to the shop); `summaryNote`
|
|
102
|
+
renders under the subtotal and Checkout button. An empty cart shows no
|
|
103
|
+
summary or Checkout button. Close and Remove are at least 44×44px.
|
|
104
|
+
- `CartButton` `icon` (rendered before the label), `labelHidden` (visually
|
|
105
|
+
hides the label, keeping an accessible name like "Cart, 2 items") and
|
|
106
|
+
`hideZeroCount` (omits the count at 0; the accessible name becomes
|
|
107
|
+
"Cart, empty").
|
|
108
|
+
- `--sf-motion-duration` (default `0ms`, no motion) drives the drawer's
|
|
109
|
+
slide-in and the backdrop's fade, in and out. It is forced to `0ms` under
|
|
110
|
+
`prefers-reduced-motion: reduce`. Example: `--sf-motion-duration: 200ms`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@develemit/storefront",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
|
+
"description": "Brand-agnostic React storefront: components, page views, cart, checkout and a typed API client.",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/develemit/develemit-commerce.git",
|
|
8
|
+
"directory": "packages/storefront"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://github.com/develemit/develemit-commerce/blob/main/docs/getting-started.md",
|
|
4
11
|
"type": "module",
|
|
5
12
|
"main": "./dist/index.js",
|
|
6
13
|
"module": "./dist/index.js",
|
|
@@ -26,7 +33,9 @@
|
|
|
26
33
|
},
|
|
27
34
|
"files": [
|
|
28
35
|
"dist",
|
|
29
|
-
"
|
|
36
|
+
"docs",
|
|
37
|
+
"!**/*.tsbuildinfo",
|
|
38
|
+
"CHANGELOG.md"
|
|
30
39
|
],
|
|
31
40
|
"nx": {
|
|
32
41
|
"name": "storefront",
|
|
@@ -44,6 +53,6 @@
|
|
|
44
53
|
"access": "public"
|
|
45
54
|
},
|
|
46
55
|
"dependencies": {
|
|
47
|
-
"@develemit/commerce-contracts": "0.
|
|
56
|
+
"@develemit/commerce-contracts": "0.7.0"
|
|
48
57
|
}
|
|
49
58
|
}
|