@asteby/storefront-react 0.7.0 → 0.9.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/README.md CHANGED
@@ -481,6 +481,43 @@ return Response.json(result) // PaymentAttemptResult; 409/422/429 se devuelve
481
481
  - **Tema**: `--sf-pay-text`, `-text-muted`, `-background`, `-input-background`, `-primary`, `-primary-contrast`, `-error`, `-success`, `-outline`, `-radius` se leen al montar y se pasan al Brick; `--sf-pay-min-height` reserva el alto (sin saltos).
482
482
  - **Otro proveedor**: implementa `PaymentFormAdapter` (`load`, `mount → { collect, unmount }`) y pásalo en `adapter`/`adapters={{ stripe_elements: … }}`.
483
483
 
484
+ ### «Método de pago» híbrido (`PaymentMethodSection` + `/payments/server`)
485
+
486
+ Lo que hace falta alrededor del Brick, listo para usar: filas por opción que publica OPS (tarjeta, efectivo, cuenta del proveedor, pagos sin tarjeta), tarjeta con el formulario seguro DENTRO de la página, efectivo con puntos de pago y ficha, y redirección para el resto. Sin JavaScript, o si el formulario no carga, todo cae al envío normal del formulario de la página (redirección). Textos, rutas, iconos y clases por props; el total es siempre `cart.cost.total` de OPS (nunca se suma ni se recalcula).
487
+
488
+ ```tsx
489
+ // loader: filas desde GET payment-methods (la tienda oculta lo que no ofrece)
490
+ import { buildPayChoices } from '@asteby/storefront-react/payments'
491
+ const choices = buildPayChoices(payment.methods, { hide: ['manual'] })
492
+
493
+ // componente, dentro del <form> de /pagar
494
+ import '@asteby/storefront-react/payments.css' // opcional: muestra el panel de la fila marcada sin JS
495
+ <PaymentMethodSection
496
+ choices={choices} amount={cart.cost.total} scope={cart.id} formRef={formRef}
497
+ payUrl="/pagar/cobrar" statusUrl="/pagar/estado" nonce={nonce}
498
+ delivery={delivery} deliveryReady={delivery !== '' && !delivery.startsWith('ship:')}
499
+ syncDelivery={saveDeliveryAndReloadCart} // guarda la entrega en el carrito de OPS y resuelve true al releer el total
500
+ onAmountChanged={revalidate} onDone={() => navigate('/pagar/gracias')}
501
+ rebuildHref="/carrito" messages={{ legend: 'Método de pago' }} icons={{ card: <CardIcon /> }}
502
+ classNames={{ radioLabel: '...', submit: 'btn-primary' }} />
503
+ ```
504
+
505
+ ```ts
506
+ // servidor: dos rutas de recurso (POST JSON) y el efectivo del formulario
507
+ import { createEmbeddedPayHandlers, paymentCspHeaders, paymentCspMode, paymentNonce } from '@asteby/storefront-react/payments/server'
508
+ export const pay = createEmbeddedPayHandlers({
509
+ client, site, returnPath: '/pagar/gracias', readCartId, readSession, serializeSession,
510
+ validateFields, prepareCart, createDraft, settle, hide: ['manual'],
511
+ })
512
+ // action de /pagar/cobrar -> pay.pay(request); action de /pagar/estado -> pay.status(request)
513
+ // efectivo (en la action de /pagar): const out = await pay.cash({ sf, request, cartId, fields, choice, before })
514
+ ```
515
+
516
+ - La tienda aporta lo suyo por callbacks: cookies (`readSession`/`serializeSession`), qué campos exige (`validateFields`), cómo guarda contacto, dirección y entrega (`prepareCart`), el comprobante (`createDraft`) y cuándo queda cerrado el pedido (`settle`: añade las cookies y devuelve `true` si fue aprobado, en revisión o ficha).
517
+ - El servidor decide qué se paga en la página: un `payment_choice` falsificado u oculto se rechaza (422), un punto de efectivo se arma de la lista del proveedor, el monto nunca viaja en `provider_data`, el reintento reutiliza el checkout y la llave de pago, hay límite por IP y validación de origen.
518
+ - CSP solo para `/pagar`: `paymentCspHeaders([client], { mode: paymentCspMode(env.PAY_CSP, isProd), nonce: paymentNonce(), scriptHashes, imgHttps })` y `paymentNonceFromHeaders` para `ServerRouter`.
519
+ - Accesibilidad: un `fieldset` con leyenda y radios nativos, avisos en `role="status"`/`alert`, botón de 44 px que explica su estado ocupado; pruebas con axe.
520
+
484
521
  ## Promociones y venta cruzada (`@asteby/storefront-react/promotions`)
485
522
 
486
523
  Mecanismos sin contenido: los textos y montos llegan como datos de OPS (`Cart.promotions`, `GET /storefront/promotions`, `carts.recommendations`).
@@ -503,6 +540,55 @@ import { CartProgressBars, CrossSellModal, OrderNote, PromoPopup } from '@asteby
503
540
  - `OrderNote`: `<textarea>` con etiqueta, 1000 caracteres, guardado con *debounce* (800 ms) y al salir del campo; sin JS es un `<form method="post">` con botón «Guardar nota».
504
541
  - Funciones puras testeables: `isExcludedPath`, `frequencyAllows`, `isCrawler`, `withinWindow`, `resolveTrigger`, `safeInternalPath`.
505
542
 
543
+ ## Cajón del carrito (`@asteby/storefront-react/cart`)
544
+
545
+ Cajón lateral derecho "Tu carrito (N)" que se abre al agregar. Subpath opcional, sin textos, rutas, moneda, iconos ni marca fijos: todo entra por props. Muestra `cart.cost` y los totales de línea de OPS; no recalcula nada. Muta el carrito con la acción de ruta que la tienda ya tiene (formularios `POST` nativos que funcionan sin JavaScript; con `useLineForm` se mejoran con un fetcher).
546
+
547
+ ```tsx
548
+ import { CartDrawer, openCartDrawer, handleCartLinkClick, useOpenCartDrawerWhenDone } from '@asteby/storefront-react/cart'
549
+ import '@asteby/storefront-react/cart.css' // opcional: usa --background, --card, --border, --primary...
550
+
551
+ // root.tsx, una sola vez
552
+ <CartDrawer
553
+ cart={cart}
554
+ labels={{ title: t.cart.yourCart, close: t.cart.close, empty: t.cart.empty, subtotal: t.cart.subtotal, note: t.cart.calcAtCheckout,
555
+ checkout: t.cart.checkout, viewCart: t.cart.viewCart, remove: t.cart.remove, increase: t.add.increase, decrease: t.add.decrease }}
556
+ paths={{ cart: paths.cart(), checkout: paths.checkout(), browse: paths.collections(), product: (h, v) => paths.product(h, v) }}
557
+ shipping={free && { message: '...', percent: free.progress, reached: free.reached, progressLabel: '...' }} // calculado con lo que manda OPS
558
+ upsell={<Suggestions />} upsellTitle={t.cart.suggestions}
559
+ renderMoney={(m) => <Money data={m} />}
560
+ renderLink={({ href, ...p }) => <Link to={href} {...p} />} // navegación del router
561
+ useLineForm={() => { const f = useFetcher(); return { Form: f.Form, formData: f.formData } }} // opcional, optimista
562
+ />
563
+
564
+ // Abrir tras agregar: busy = hay un agregado en vuelo
565
+ useOpenCartDrawerWhenDone(fetchers.some(isAdd), { skip: silent })
566
+ // Enlace de cabecera: href="/carrito" funciona sin JS; con JS abre el cajón
567
+ <a href={paths.cart()} onClick={handleCartLinkClick}>Mi carrito</a>
568
+ ```
569
+
570
+ - `openCartDrawer()`, `closeCartDrawer()`, `toggleCartDrawer()`, `isCartDrawerOpen()` y el hook `useCartDrawer()` (`open`, `openDrawer`, `closeDrawer`, `toggle`).
571
+ - `form` ajusta el contrato de la acción (por omisión `intent=update|remove`, `line_id`, `quantity`).
572
+ - Accesibilidad: `<dialog>` modal con `role="dialog"` y `aria-modal`, foco atrapado y devuelto al abridor, Esc, clic fuera y botón de cierre, el scroll del fondo se bloquea, `prefers-reduced-motion` respetado en `cart.css`.
573
+ - Estilos: `cart.css` (CSS plano con los tokens de la tienda) y/o `classNames` por pieza (Tailwind). Cada pieza lleva `data-sf-cart-*`.
574
+ - `renderLine`, `renderImage`, `icons`, `notice` y `isDefaultVariantTitle` cubren las variaciones de cada tienda.
575
+
576
+ ## Compatibilidad de vehículos (`@asteby/storefront-react/fitment`)
577
+
578
+ Módulo opcional y sin interfaz para tiendas de refacciones: catálogo marca → modelo → año → motor, clave de vehículo, opciones en cascada del selector y estado de compatibilidad de cada pieza. No trae textos ni marcas: los prefijos de etiqueta, el atributo de la tabla de compatibilidad y las grafías del backend se inyectan con `fitmentConfig` / `parseCatalog`.
579
+
580
+ ```ts
581
+ import { parseCatalog, resolveVehicle, fitmentConfig, fitFilterTags, withFit, safeRedirect } from '@asteby/storefront-react/fitment'
582
+
583
+ const config = fitmentConfig({ compatAttribute: 'compatibilidad' }) // por defecto: tags `fit:*`, atributo `compatibility`
584
+ const catalog = parseCatalog(section.props, { priorityAliases: { alta: 'high' } })
585
+ const vehicle = resolveVehicle(catalog, cookieKey) // null si la clave ya no existe
586
+ const tags = vehicle ? fitFilterTags(vehicle, config) : [] // filter.tag = alguno de
587
+ const product = withFit(rawProduct, vehicle, config) // cientos de tags fit:* → UNA etiqueta de estado
588
+ ```
589
+
590
+ La cookie, el formulario y el redirect son de la tienda; `safeRedirect` valida el destino. Un componente de selector sobre estas funciones llega en una versión posterior.
591
+
506
592
  ## API
507
593
 
508
594
  | Export | |
@@ -517,7 +603,7 @@ import { CartProgressBars, CrossSellModal, OrderNote, PromoPopup } from '@asteby
517
603
  | `defineSection`, `SectionRegistry`, `createSectionRegistry`, `Sections`, `resolveSectionProps`, `sectionJsonSchema` | Content sections |
518
604
  | `themeToCss`, `themeToCssVariables`, `ThemeStyle`, `themeFontsHref`, `tokenName` | SSR theme |
519
605
  | `ProductImage`, `ProductMockup`, `MockupProvider`, `createMockupRegistry`, `registerMockup`, `BUILTIN_MOCKUPS`, `MockupFrame`, `MockupText`, `TireMockup`, `PartMockup`, `SupplementMockup`, `GenericMockup` | Mockups de respaldo (subpath `/mockups`) |
520
- | `PaymentBrick`, `useEmbeddedPayment`, `VoucherCard`, `PaymentChallenge`, `createFetchTransport`, `mercadoPagoBricksAdapter`, `loadPaymentSdk`, `paymentCsp`, `readPaymentTheme` | Pago integrado (subpath `/payments`) |
606
+ | `PaymentMethodSection`, `buildPayChoices`, `createEmbeddedPayHandlers` (`/payments/server`), `PaymentBrick`, `useEmbeddedPayment`, `VoucherCard`, `PaymentChallenge`, `createFetchTransport`, `mercadoPagoBricksAdapter`, `loadPaymentSdk`, `paymentCsp`, `readPaymentTheme` | Pago integrado (subpath `/payments`) |
521
607
  | `PromoPopup`, `CartProgressBars`, `PromoProgressBar`, `CrossSellModal`, `OrderNote`, `isExcludedPath`, `frequencyAllows`, `isCrawler` | Promociones y venta cruzada (subpath `/promotions`) |
522
608
 
523
609
  ## `@asteby/storefront-react/ui`
package/dist/cart.cjs ADDED
@@ -0,0 +1,283 @@
1
+ 'use strict';
2
+
3
+ var react = require('react');
4
+ var storefront = require('@asteby/storefront');
5
+ var jsxRuntime = require('react/jsx-runtime');
6
+
7
+ // src/cart-drawer/cart-drawer.tsx
8
+ function useModalDialog({ open, onClose, initialFocus }) {
9
+ const ref = react.useRef(null);
10
+ const opener = react.useRef(null);
11
+ const onCloseRef = react.useRef(onClose);
12
+ onCloseRef.current = onClose;
13
+ react.useEffect(() => {
14
+ const el = ref.current;
15
+ if (!el || !open) return;
16
+ opener.current = typeof document !== "undefined" ? document.activeElement : null;
17
+ const hasModal = typeof el.showModal === "function";
18
+ if (!el.open) {
19
+ if (hasModal) {
20
+ try {
21
+ el.showModal();
22
+ } catch {
23
+ el.setAttribute("open", "");
24
+ }
25
+ } else el.setAttribute("open", "");
26
+ }
27
+ const target = initialFocus?.current ?? el.querySelector("[data-sf-autofocus]") ?? el;
28
+ if (!target.hasAttribute("tabindex") && target === el) el.setAttribute("tabindex", "-1");
29
+ target.focus({ preventScroll: true });
30
+ const onCancel = (e) => {
31
+ e.preventDefault();
32
+ onCloseRef.current("esc");
33
+ };
34
+ const onKey = (e) => {
35
+ if (e.key === "Escape") {
36
+ e.preventDefault();
37
+ onCloseRef.current("esc");
38
+ }
39
+ };
40
+ el.addEventListener("cancel", onCancel);
41
+ if (!hasModal) el.addEventListener("keydown", onKey);
42
+ return () => {
43
+ el.removeEventListener("cancel", onCancel);
44
+ el.removeEventListener("keydown", onKey);
45
+ if (el.open) {
46
+ if (typeof el.close === "function") {
47
+ try {
48
+ el.close();
49
+ } catch {
50
+ el.removeAttribute("open");
51
+ }
52
+ } else el.removeAttribute("open");
53
+ }
54
+ const back = opener.current;
55
+ if (back instanceof HTMLElement && back.isConnected) back.focus({ preventScroll: true });
56
+ };
57
+ }, [open]);
58
+ const onBackdropClick = react.useCallback((e) => {
59
+ if (e.target === e.currentTarget) onCloseRef.current("backdrop");
60
+ }, []);
61
+ return { ref, onBackdropClick };
62
+ }
63
+ var StorefrontContext = react.createContext(null);
64
+ function useOptionalStorefront() {
65
+ return react.useContext(StorefrontContext);
66
+ }
67
+
68
+ // src/locale.ts
69
+ function useResolvedLocale(locale) {
70
+ const sf = useOptionalStorefront();
71
+ return locale ?? sf?.locale ?? "es-MX";
72
+ }
73
+ var isOpen = false;
74
+ var listeners = /* @__PURE__ */ new Set();
75
+ function set(next) {
76
+ if (isOpen === next) return;
77
+ isOpen = next;
78
+ for (const l of [...listeners]) l();
79
+ }
80
+ var subscribe = (l) => {
81
+ listeners.add(l);
82
+ return () => {
83
+ listeners.delete(l);
84
+ };
85
+ };
86
+ function openCartDrawer() {
87
+ set(true);
88
+ }
89
+ function closeCartDrawer() {
90
+ set(false);
91
+ }
92
+ function toggleCartDrawer() {
93
+ set(!isOpen);
94
+ }
95
+ function isCartDrawerOpen() {
96
+ return isOpen;
97
+ }
98
+ function useCartDrawer() {
99
+ const open = react.useSyncExternalStore(subscribe, () => isOpen, () => false);
100
+ const toggle = react.useCallback(() => toggleCartDrawer(), []);
101
+ return { open, openDrawer: openCartDrawer, closeDrawer: closeCartDrawer, toggle };
102
+ }
103
+ function useOpenCartDrawerWhenDone(busy, options = {}) {
104
+ const was = react.useRef(false);
105
+ const skip = react.useRef(false);
106
+ skip.current = !!options.skip;
107
+ react.useEffect(() => {
108
+ if (was.current && !busy && !skip.current) openCartDrawer();
109
+ was.current = busy;
110
+ }, [busy]);
111
+ }
112
+ function handleCartLinkClick(e) {
113
+ if (e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
114
+ e.preventDefault();
115
+ openCartDrawer();
116
+ }
117
+ var DEFAULT_FORM = {
118
+ intentField: "intent",
119
+ lineField: "line_id",
120
+ quantityField: "quantity",
121
+ updateIntent: "update",
122
+ removeIntent: "remove"
123
+ };
124
+ var useNativeForm = () => ({ Form: "form" });
125
+ var defaultIsDefaultVariant = (t) => /^default title$/i.test(t.trim());
126
+ function useBodyScrollLock(locked) {
127
+ react.useEffect(() => {
128
+ if (!locked || typeof document === "undefined") return;
129
+ const el = document.documentElement;
130
+ const prev = el.style.overflow;
131
+ el.style.overflow = "hidden";
132
+ return () => {
133
+ el.style.overflow = prev;
134
+ };
135
+ }, [locked]);
136
+ }
137
+ function CartDrawer(props) {
138
+ const { cart, labels, paths, shipping, notice, upsell, upsellTitle, icons, classNames: cx = {}, onOpenChange } = props;
139
+ const locale = useResolvedLocale(props.locale);
140
+ const { open } = useCartDrawer();
141
+ const titleId = react.useId();
142
+ const upsellId = react.useId();
143
+ const { ref, onBackdropClick } = useModalDialog({ open, onClose: () => closeCartDrawer() });
144
+ useBodyScrollLock(open);
145
+ react.useEffect(() => {
146
+ onOpenChange?.(open);
147
+ }, [open]);
148
+ if (!open) return null;
149
+ const money = props.renderMoney ?? ((m) => storefront.formatMoney(m, locale));
150
+ const link = props.renderLink ?? (({ href, className, children, onClick }) => /* @__PURE__ */ jsxRuntime.jsx("a", { href, className, onClick, children }));
151
+ const count = cart?.total_quantity ?? 0;
152
+ const lines = cart?.lines ?? [];
153
+ const empty = !cart || lines.length === 0;
154
+ const close = () => closeCartDrawer();
155
+ return /* @__PURE__ */ jsxRuntime.jsx(
156
+ "dialog",
157
+ {
158
+ ref,
159
+ role: "dialog",
160
+ "aria-modal": "true",
161
+ "aria-labelledby": titleId,
162
+ onClick: onBackdropClick,
163
+ onClose: close,
164
+ className: cx.dialog,
165
+ "data-sf-cart-drawer": "",
166
+ children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cx.panel, "data-sf-cart-panel": "", children: [
167
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cx.header, "data-sf-cart-header": "", children: [
168
+ /* @__PURE__ */ jsxRuntime.jsxs("h2", { id: titleId, tabIndex: -1, className: cx.title, "data-sf-cart-title": "", children: [
169
+ labels.title,
170
+ count > 0 ? ` (${count})` : ""
171
+ ] }),
172
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", onClick: close, "aria-label": labels.close, className: cx.close, "data-sf-cart-close": "", "data-sf-autofocus": "", children: icons?.close ?? /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": "true", children: "\u2715" }) })
173
+ ] }),
174
+ empty ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cx.empty, "data-sf-cart-empty": "", children: [
175
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: cx.emptyTitle, "data-sf-cart-empty-title": "", children: labels.empty }),
176
+ labels.emptyHint ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: cx.emptyHint, "data-sf-cart-empty-hint": "", children: labels.emptyHint }) : null,
177
+ paths.browse && labels.browse ? link({ href: paths.browse, className: cx.browse, children: labels.browse, onClick: close }) : null
178
+ ] }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
179
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cx.body, "data-sf-cart-body": "", children: [
180
+ shipping ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cx.shipping, "data-sf-cart-shipping": "", "data-reached": shipping.reached || void 0, children: [
181
+ /* @__PURE__ */ jsxRuntime.jsxs("p", { className: cx.shippingMessage, children: [
182
+ shipping.reached ? icons?.shippingReached : icons?.shipping,
183
+ shipping.message
184
+ ] }),
185
+ /* @__PURE__ */ jsxRuntime.jsx(
186
+ "div",
187
+ {
188
+ role: "progressbar",
189
+ "aria-valuemin": 0,
190
+ "aria-valuemax": 100,
191
+ "aria-valuenow": Math.round(shipping.percent),
192
+ "aria-label": shipping.progressLabel,
193
+ className: cx.bar,
194
+ "data-sf-cart-bar": "",
195
+ children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: cx.barFill, "data-sf-cart-bar-fill": "", style: { width: `${Math.max(0, Math.min(100, shipping.percent))}%` } })
196
+ }
197
+ )
198
+ ] }) : notice ? /* @__PURE__ */ jsxRuntime.jsxs("p", { className: [cx.shipping, cx.shippingMessage].filter(Boolean).join(" ") || void 0, "data-sf-cart-shipping": "", "data-notice": "", children: [
199
+ icons?.shippingReached,
200
+ notice
201
+ ] }) : null,
202
+ /* @__PURE__ */ jsxRuntime.jsx("ul", { className: cx.lines, "data-sf-cart-lines": "", children: lines.map((line) => /* @__PURE__ */ jsxRuntime.jsx(DrawerLine, { line, props, money, link, close }, line.id)) }),
203
+ upsell ? /* @__PURE__ */ jsxRuntime.jsxs("section", { className: cx.upsell, "data-sf-cart-upsell": "", "aria-labelledby": upsellTitle ? upsellId : void 0, children: [
204
+ upsellTitle ? /* @__PURE__ */ jsxRuntime.jsx("h3", { id: upsellId, className: cx.upsellTitle, children: upsellTitle }) : null,
205
+ upsell
206
+ ] }) : null
207
+ ] }),
208
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cx.footer, "data-sf-cart-footer": "", children: [
209
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cx.subtotal, "data-sf-cart-subtotal": "", children: [
210
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: cx.subtotalLabel, children: labels.subtotal }),
211
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: cx.subtotalValue, "data-sf-cart-subtotal-value": "", children: cart ? money(cart.cost.subtotal) : null })
212
+ ] }),
213
+ labels.note ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: cx.note, "data-sf-cart-note": "", children: labels.note }) : null,
214
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cx.actions, "data-sf-cart-actions": "", children: [
215
+ link({ href: paths.checkout, className: cx.checkout, children: labels.checkout, onClick: close }),
216
+ link({ href: paths.cart, className: cx.viewCart, children: labels.viewCart, onClick: close })
217
+ ] })
218
+ ] })
219
+ ] })
220
+ ] })
221
+ }
222
+ );
223
+ }
224
+ function DrawerLine({
225
+ line,
226
+ props,
227
+ money,
228
+ link,
229
+ close
230
+ }) {
231
+ const { labels, paths, icons, classNames: cx = {} } = props;
232
+ const f = { ...DEFAULT_FORM, ...props.form };
233
+ const { Form, formData } = (props.useLineForm ?? useNativeForm)();
234
+ const quantity = formData ? Number(formData.get(f.quantityField) ?? 0) : line.quantity;
235
+ if (formData && (formData.get(f.intentField) === f.removeIntent || quantity === 0)) return null;
236
+ const merch = line.merchandise;
237
+ const isDefault = props.isDefaultVariantTitle ?? defaultIsDefaultVariant;
238
+ const title = merch.product.title;
239
+ const href = paths.product?.(merch.product.handle, merch.id);
240
+ const img = merch.image ?? merch.product.featured_image;
241
+ const image = props.renderImage ? props.renderImage(line) : img ? /* @__PURE__ */ jsxRuntime.jsx("img", { src: img.url, alt: img.alt ?? "", width: 80, height: 80, loading: "lazy", decoding: "async" }) : null;
242
+ const imageNode = /* @__PURE__ */ jsxRuntime.jsx("span", { className: cx.image, "data-sf-cart-image": "", children: image });
243
+ const price = /* @__PURE__ */ jsxRuntime.jsx("span", { className: cx.price, "data-sf-cart-price": "", children: money(line.cost.total) });
244
+ const controls = /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cx.controls, "data-sf-cart-controls": "", children: [
245
+ /* @__PURE__ */ jsxRuntime.jsxs(Form, { method: "post", action: paths.cart, className: cx.qty, "data-sf-cart-qty": "", children: [
246
+ /* @__PURE__ */ jsxRuntime.jsx("input", { type: "hidden", name: f.intentField, value: f.updateIntent }),
247
+ /* @__PURE__ */ jsxRuntime.jsx("input", { type: "hidden", name: f.lineField, value: line.id }),
248
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "submit", name: f.quantityField, value: quantity - 1, "aria-label": labels.decrease, className: cx.qtyButton, "data-sf-cart-qty-button": "", children: icons?.decrease ?? /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": "true", children: "\u2212" }) }),
249
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: cx.qtyValue, "aria-live": "polite", "data-sf-cart-qty-value": "", children: quantity }),
250
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "submit", name: f.quantityField, value: quantity + 1, "aria-label": labels.increase, className: cx.qtyButton, "data-sf-cart-qty-button": "", children: icons?.increase ?? /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": "true", children: "+" }) })
251
+ ] }),
252
+ /* @__PURE__ */ jsxRuntime.jsxs(Form, { method: "post", action: paths.cart, "data-sf-cart-remove-form": "", children: [
253
+ /* @__PURE__ */ jsxRuntime.jsx("input", { type: "hidden", name: f.intentField, value: f.removeIntent }),
254
+ /* @__PURE__ */ jsxRuntime.jsx("input", { type: "hidden", name: f.lineField, value: line.id }),
255
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "submit", className: cx.remove, "data-sf-cart-remove": "", children: labels.remove })
256
+ ] })
257
+ ] });
258
+ if (props.renderLine) return /* @__PURE__ */ jsxRuntime.jsx("li", { className: cx.line, "data-sf-cart-line": "", children: props.renderLine(line, { controls, price, image: imageNode }) }, line.id);
259
+ return /* @__PURE__ */ jsxRuntime.jsxs("li", { className: cx.line, "data-sf-cart-line": "", children: [
260
+ imageNode,
261
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cx.lineMain, "data-sf-cart-line-main": "", children: [
262
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cx.lineHead, "data-sf-cart-line-head": "", children: [
263
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cx.lineText, "data-sf-cart-line-text": "", children: [
264
+ href ? link({ href, className: cx.lineTitle, children: title, onClick: close }) : /* @__PURE__ */ jsxRuntime.jsx("span", { className: cx.lineTitle, "data-sf-cart-line-title": "", children: title }),
265
+ !isDefault(merch.title) ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: cx.variant, "data-sf-cart-variant": "", children: merch.title }) : null
266
+ ] }),
267
+ price
268
+ ] }),
269
+ controls
270
+ ] })
271
+ ] });
272
+ }
273
+
274
+ exports.CartDrawer = CartDrawer;
275
+ exports.closeCartDrawer = closeCartDrawer;
276
+ exports.handleCartLinkClick = handleCartLinkClick;
277
+ exports.isCartDrawerOpen = isCartDrawerOpen;
278
+ exports.openCartDrawer = openCartDrawer;
279
+ exports.toggleCartDrawer = toggleCartDrawer;
280
+ exports.useCartDrawer = useCartDrawer;
281
+ exports.useOpenCartDrawerWhenDone = useOpenCartDrawerWhenDone;
282
+ //# sourceMappingURL=cart.cjs.map
283
+ //# sourceMappingURL=cart.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/dialog.tsx","../src/context.tsx","../src/locale.ts","../src/cart-drawer/store.ts","../src/cart-drawer/cart-drawer.tsx"],"names":["useRef","useEffect","useCallback","createContext","useContext","useSyncExternalStore","useId","formatMoney","jsx","jsxs","Fragment"],"mappings":";;;;;;;AAkBO,SAAS,cAAA,CAAe,EAAE,IAAA,EAAM,OAAA,EAAS,cAAa,EAA0B;AACrF,EAAA,MAAM,GAAA,GAAMA,aAAiC,IAAI,CAAA;AACjD,EAAA,MAAM,MAAA,GAASA,aAAuB,IAAI,CAAA;AAC1C,EAAA,MAAM,UAAA,GAAaA,aAAO,OAAO,CAAA;AACjC,EAAA,UAAA,CAAW,OAAA,GAAU,OAAA;AAErB,EAAAC,eAAA,CAAU,MAAM;AACd,IAAA,MAAM,KAAK,GAAA,CAAI,OAAA;AACf,IAAA,IAAI,CAAC,EAAA,IAAM,CAAC,IAAA,EAAM;AAClB,IAAA,MAAA,CAAO,OAAA,GAAU,OAAO,QAAA,KAAa,WAAA,GAAc,SAAS,aAAA,GAAgB,IAAA;AAC5E,IAAA,MAAM,QAAA,GAAW,OAAO,EAAA,CAAG,SAAA,KAAc,UAAA;AACzC,IAAA,IAAI,CAAC,GAAG,IAAA,EAAM;AACZ,MAAA,IAAI,QAAA,EAAU;AACZ,QAAA,IAAI;AACF,UAAA,EAAA,CAAG,SAAA,EAAU;AAAA,QACf,CAAA,CAAA,MAAQ;AACN,UAAA,EAAA,CAAG,YAAA,CAAa,QAAQ,EAAE,CAAA;AAAA,QAC5B;AAAA,MACF,CAAA,MAAO,EAAA,CAAG,YAAA,CAAa,MAAA,EAAQ,EAAE,CAAA;AAAA,IACnC;AACA,IAAA,MAAM,SAAS,YAAA,EAAc,OAAA,IAAW,EAAA,CAAG,aAAA,CAA2B,qBAAqB,CAAA,IAAK,EAAA;AAChG,IAAA,IAAI,CAAC,MAAA,CAAO,YAAA,CAAa,UAAU,CAAA,IAAK,WAAW,EAAA,EAAI,EAAA,CAAG,YAAA,CAAa,UAAA,EAAY,IAAI,CAAA;AACvF,IAAA,MAAA,CAAO,KAAA,CAAM,EAAE,aAAA,EAAe,IAAA,EAAM,CAAA;AAEpC,IAAA,MAAM,QAAA,GAAW,CAAC,CAAA,KAAa;AAC7B,MAAA,CAAA,CAAE,cAAA,EAAe;AACjB,MAAA,UAAA,CAAW,QAAQ,KAAK,CAAA;AAAA,IAC1B,CAAA;AACA,IAAA,MAAM,KAAA,GAAQ,CAAC,CAAA,KAAqB;AAClC,MAAA,IAAI,CAAA,CAAE,QAAQ,QAAA,EAAU;AACtB,QAAA,CAAA,CAAE,cAAA,EAAe;AACjB,QAAA,UAAA,CAAW,QAAQ,KAAK,CAAA;AAAA,MAC1B;AAAA,IACF,CAAA;AACA,IAAA,EAAA,CAAG,gBAAA,CAAiB,UAAU,QAAQ,CAAA;AACtC,IAAA,IAAI,CAAC,QAAA,EAAU,EAAA,CAAG,gBAAA,CAAiB,WAAW,KAAK,CAAA;AACnD,IAAA,OAAO,MAAM;AACX,MAAA,EAAA,CAAG,mBAAA,CAAoB,UAAU,QAAQ,CAAA;AACzC,MAAA,EAAA,CAAG,mBAAA,CAAoB,WAAW,KAAK,CAAA;AACvC,MAAA,IAAI,GAAG,IAAA,EAAM;AACX,QAAA,IAAI,OAAO,EAAA,CAAG,KAAA,KAAU,UAAA,EAAY;AAClC,UAAA,IAAI;AACF,YAAA,EAAA,CAAG,KAAA,EAAM;AAAA,UACX,CAAA,CAAA,MAAQ;AACN,YAAA,EAAA,CAAG,gBAAgB,MAAM,CAAA;AAAA,UAC3B;AAAA,QACF,CAAA,MAAO,EAAA,CAAG,eAAA,CAAgB,MAAM,CAAA;AAAA,MAClC;AACA,MAAA,MAAM,OAAO,MAAA,CAAO,OAAA;AACpB,MAAA,IAAI,IAAA,YAAgB,eAAe,IAAA,CAAK,WAAA,OAAkB,KAAA,CAAM,EAAE,aAAA,EAAe,IAAA,EAAM,CAAA;AAAA,IACzF,CAAA;AAAA,EACF,CAAA,EAAG,CAAC,IAAI,CAAC,CAAA;AAGT,EAAA,MAAM,eAAA,GAAkBC,iBAAA,CAAY,CAAC,CAAA,KAA2D;AAC9F,IAAA,IAAI,EAAE,MAAA,KAAW,CAAA,CAAE,aAAA,EAAe,UAAA,CAAW,QAAQ,UAAU,CAAA;AAAA,EACjE,CAAA,EAAG,EAAE,CAAA;AAEL,EAAA,OAAO,EAAE,KAAK,eAAA,EAAgB;AAChC;AChEA,IAAM,iBAAA,GAAoBC,oBAA6C,IAAI,CAAA;AA4CpE,SAAS,qBAAA,GAAuD;AACrE,EAAA,OAAOC,iBAAW,iBAAiB,CAAA;AACrC;;;ACxDO,SAAS,kBAAkB,MAAA,EAAgC;AAChE,EAAA,MAAM,KAAK,qBAAA,EAAsB;AACjC,EAAA,OAAO,MAAA,IAAU,IAAI,MAAA,IAAU,OAAA;AACjC;ACDA,IAAI,MAAA,GAAS,KAAA;AACb,IAAM,SAAA,uBAAgB,GAAA,EAAgB;AAEtC,SAAS,IAAI,IAAA,EAAe;AAC1B,EAAA,IAAI,WAAW,IAAA,EAAM;AACrB,EAAA,MAAA,GAAS,IAAA;AACT,EAAA,KAAA,MAAW,CAAA,IAAK,CAAC,GAAG,SAAS,GAAG,CAAA,EAAE;AACpC;AAEA,IAAM,SAAA,GAAY,CAAC,CAAA,KAAkB;AACnC,EAAA,SAAA,CAAU,IAAI,CAAC,CAAA;AACf,EAAA,OAAO,MAAM;AACX,IAAA,SAAA,CAAU,OAAO,CAAC,CAAA;AAAA,EACpB,CAAA;AACF,CAAA;AAGO,SAAS,cAAA,GAAiB;AAC/B,EAAA,GAAA,CAAI,IAAI,CAAA;AACV;AAGO,SAAS,eAAA,GAAkB;AAChC,EAAA,GAAA,CAAI,KAAK,CAAA;AACX;AAEO,SAAS,gBAAA,GAAmB;AACjC,EAAA,GAAA,CAAI,CAAC,MAAM,CAAA;AACb;AAEO,SAAS,gBAAA,GAAmB;AACjC,EAAA,OAAO,MAAA;AACT;AAGO,SAAS,aAAA,GAAgB;AAC9B,EAAA,MAAM,OAAOC,0BAAA,CAAqB,SAAA,EAAW,MAAM,MAAA,EAAQ,MAAM,KAAK,CAAA;AACtE,EAAA,MAAM,SAASH,iBAAAA,CAAY,MAAM,gBAAA,EAAiB,EAAG,EAAE,CAAA;AACvD,EAAA,OAAO,EAAE,IAAA,EAAM,UAAA,EAAY,cAAA,EAAgB,WAAA,EAAa,iBAAiB,MAAA,EAAO;AAClF;AAOO,SAAS,yBAAA,CAA0B,IAAA,EAAe,OAAA,GAA8B,EAAC,EAAG;AACzF,EAAA,MAAM,GAAA,GAAMF,aAAO,KAAK,CAAA;AACxB,EAAA,MAAM,IAAA,GAAOA,aAAO,KAAK,CAAA;AACzB,EAAA,IAAA,CAAK,OAAA,GAAU,CAAC,CAAC,OAAA,CAAQ,IAAA;AACzB,EAAAC,gBAAU,MAAM;AACd,IAAA,IAAI,IAAI,OAAA,IAAW,CAAC,QAAQ,CAAC,IAAA,CAAK,SAAS,cAAA,EAAe;AAC1D,IAAA,GAAA,CAAI,OAAA,GAAU,IAAA;AAAA,EAChB,CAAA,EAAG,CAAC,IAAI,CAAC,CAAA;AACX;AAgBO,SAAS,oBAAoB,CAAA,EAAc;AAChD,EAAA,IAAI,CAAA,CAAE,gBAAA,IAAoB,CAAA,CAAE,MAAA,KAAW,CAAA,IAAK,CAAA,CAAE,OAAA,IAAW,CAAA,CAAE,OAAA,IAAW,CAAA,CAAE,QAAA,IAAY,CAAA,CAAE,MAAA,EAAQ;AAC9F,EAAA,CAAA,CAAE,cAAA,EAAe;AACjB,EAAA,cAAA,EAAe;AACjB;ACkBA,IAAM,YAAA,GAAiD;AAAA,EACrD,WAAA,EAAa,QAAA;AAAA,EACb,SAAA,EAAW,SAAA;AAAA,EACX,aAAA,EAAe,UAAA;AAAA,EACf,YAAA,EAAc,QAAA;AAAA,EACd,YAAA,EAAc;AAChB,CAAA;AAEA,IAAM,aAAA,GAAgB,OAA2B,EAAE,IAAA,EAAM,MAAA,EAAO,CAAA;AAEhE,IAAM,0BAA0B,CAAC,CAAA,KAAc,mBAAmB,IAAA,CAAK,CAAA,CAAE,MAAM,CAAA;AAE/E,SAAS,kBAAkB,MAAA,EAAiB;AAC1C,EAAAA,gBAAU,MAAM;AACd,IAAA,IAAI,CAAC,MAAA,IAAU,OAAO,QAAA,KAAa,WAAA,EAAa;AAChD,IAAA,MAAM,KAAK,QAAA,CAAS,eAAA;AACpB,IAAA,MAAM,IAAA,GAAO,GAAG,KAAA,CAAM,QAAA;AACtB,IAAA,EAAA,CAAG,MAAM,QAAA,GAAW,QAAA;AACpB,IAAA,OAAO,MAAM;AACX,MAAA,EAAA,CAAG,MAAM,QAAA,GAAW,IAAA;AAAA,IACtB,CAAA;AAAA,EACF,CAAA,EAAG,CAAC,MAAM,CAAC,CAAA;AACb;AAUO,SAAS,WAAW,KAAA,EAAwB;AACjD,EAAA,MAAM,EAAE,IAAA,EAAM,MAAA,EAAQ,KAAA,EAAO,UAAU,MAAA,EAAQ,MAAA,EAAQ,WAAA,EAAa,KAAA,EAAO,UAAA,EAAY,EAAA,GAAK,EAAC,EAAG,cAAa,GAAI,KAAA;AACjH,EAAA,MAAM,MAAA,GAAS,iBAAA,CAAkB,KAAA,CAAM,MAAM,CAAA;AAC7C,EAAA,MAAM,EAAE,IAAA,EAAK,GAAI,aAAA,EAAc;AAC/B,EAAA,MAAM,UAAUK,WAAA,EAAM;AACtB,EAAA,MAAM,WAAWA,WAAA,EAAM;AACvB,EAAA,MAAM,EAAE,GAAA,EAAK,eAAA,EAAgB,GAAI,cAAA,CAAe,EAAE,IAAA,EAAM,OAAA,EAAS,MAAM,eAAA,EAAgB,EAAG,CAAA;AAC1F,EAAA,iBAAA,CAAkB,IAAI,CAAA;AACtB,EAAAL,gBAAU,MAAM;AACd,IAAA,YAAA,GAAe,IAAI,CAAA;AAAA,EACrB,CAAA,EAAG,CAAC,IAAI,CAAC,CAAA;AAET,EAAA,IAAI,CAAC,MAAM,OAAO,IAAA;AAElB,EAAA,MAAM,QAAQ,KAAA,CAAM,WAAA,KAAgB,CAAC,CAAA,KAAaM,sBAAA,CAAY,GAAG,MAAM,CAAA,CAAA;AACvE,EAAA,MAAM,OAAO,KAAA,CAAM,UAAA,KAAe,CAAC,EAAE,MAAM,SAAA,EAAW,QAAA,EAAU,OAAA,EAAQ,qBACtEC,cAAAA,CAAC,GAAA,EAAA,EAAE,IAAA,EAAY,SAAA,EAAsB,SAAmB,QAAA,EAAS,CAAA,CAAA;AAEnE,EAAA,MAAM,KAAA,GAAQ,MAAM,cAAA,IAAkB,CAAA;AACtC,EAAA,MAAM,KAAA,GAAQ,IAAA,EAAM,KAAA,IAAS,EAAC;AAC9B,EAAA,MAAM,KAAA,GAAQ,CAAC,IAAA,IAAQ,KAAA,CAAM,MAAA,KAAW,CAAA;AACxC,EAAA,MAAM,KAAA,GAAQ,MAAM,eAAA,EAAgB;AAEpC,EAAA,uBACEA,cAAAA;AAAA,IAAC,QAAA;AAAA,IAAA;AAAA,MACC,GAAA;AAAA,MACA,IAAA,EAAK,QAAA;AAAA,MACL,YAAA,EAAW,MAAA;AAAA,MACX,iBAAA,EAAiB,OAAA;AAAA,MACjB,OAAA,EAAS,eAAA;AAAA,MACT,OAAA,EAAS,KAAA;AAAA,MACT,WAAW,EAAA,CAAG,MAAA;AAAA,MACd,qBAAA,EAAoB,EAAA;AAAA,MAEpB,0CAAC,KAAA,EAAA,EAAI,SAAA,EAAW,EAAA,CAAG,KAAA,EAAO,sBAAmB,EAAA,EAC3C,QAAA,EAAA;AAAA,wBAAAC,eAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAW,EAAA,CAAG,MAAA,EAAQ,uBAAoB,EAAA,EAC7C,QAAA,EAAA;AAAA,0BAAAA,eAAA,CAAC,IAAA,EAAA,EAAG,IAAI,OAAA,EAAS,QAAA,EAAU,IAAI,SAAA,EAAW,EAAA,CAAG,KAAA,EAAO,oBAAA,EAAmB,EAAA,EACpE,QAAA,EAAA;AAAA,YAAA,MAAA,CAAO,KAAA;AAAA,YAAO,KAAA,GAAQ,CAAA,GAAI,CAAA,EAAA,EAAK,KAAK,CAAA,CAAA,CAAA,GAAM;AAAA,WAAA,EAC7C,CAAA;AAAA,0BACAD,cAAAA,CAAC,QAAA,EAAA,EAAO,IAAA,EAAK,QAAA,EAAS,SAAS,KAAA,EAAO,YAAA,EAAY,MAAA,CAAO,KAAA,EAAO,SAAA,EAAW,EAAA,CAAG,OAAO,oBAAA,EAAmB,EAAA,EAAG,mBAAA,EAAkB,EAAA,EAC1H,QAAA,EAAA,KAAA,EAAO,KAAA,oBAASA,cAAAA,CAAC,MAAA,EAAA,EAAK,aAAA,EAAY,MAAA,EAAO,QAAA,EAAA,QAAA,EAAC,CAAA,EAC7C;AAAA,SAAA,EACF,CAAA;AAAA,QAEC,wBACCC,eAAA,CAAC,KAAA,EAAA,EAAI,WAAW,EAAA,CAAG,KAAA,EAAO,sBAAmB,EAAA,EAC3C,QAAA,EAAA;AAAA,0BAAAD,cAAAA,CAAC,OAAE,SAAA,EAAW,EAAA,CAAG,YAAY,0BAAA,EAAyB,EAAA,EAAI,iBAAO,KAAA,EAAM,CAAA;AAAA,UACtE,MAAA,CAAO,SAAA,mBAAYA,cAAAA,CAAC,GAAA,EAAA,EAAE,SAAA,EAAW,EAAA,CAAG,SAAA,EAAW,yBAAA,EAAwB,EAAA,EAAI,QAAA,EAAA,MAAA,CAAO,SAAA,EAAU,CAAA,GAAO,IAAA;AAAA,UACnG,MAAM,MAAA,IAAU,MAAA,CAAO,SAAS,IAAA,CAAK,EAAE,MAAM,KAAA,CAAM,MAAA,EAAQ,SAAA,EAAW,EAAA,CAAG,QAAQ,QAAA,EAAU,MAAA,CAAO,QAAQ,OAAA,EAAS,KAAA,EAAO,CAAA,GAAI;AAAA,SAAA,EACjI,oBAEAC,eAAA,CAAAC,mBAAA,EAAA,EACE,QAAA,EAAA;AAAA,0BAAAD,eAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAW,EAAA,CAAG,IAAA,EAAM,qBAAkB,EAAA,EACxC,QAAA,EAAA;AAAA,YAAA,QAAA,mBACCA,eAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAW,EAAA,CAAG,QAAA,EAAU,yBAAsB,EAAA,EAAG,cAAA,EAAc,QAAA,CAAS,OAAA,IAAW,MAAA,EACtF,QAAA,EAAA;AAAA,8BAAAA,eAAA,CAAC,GAAA,EAAA,EAAE,SAAA,EAAW,EAAA,CAAG,eAAA,EACd,QAAA,EAAA;AAAA,gBAAA,QAAA,CAAS,OAAA,GAAU,KAAA,EAAO,eAAA,GAAkB,KAAA,EAAO,QAAA;AAAA,gBACnD,QAAA,CAAS;AAAA,eAAA,EACZ,CAAA;AAAA,8BACAD,cAAAA;AAAA,gBAAC,KAAA;AAAA,gBAAA;AAAA,kBACC,IAAA,EAAK,aAAA;AAAA,kBACL,eAAA,EAAe,CAAA;AAAA,kBACf,eAAA,EAAe,GAAA;AAAA,kBACf,eAAA,EAAe,IAAA,CAAK,KAAA,CAAM,QAAA,CAAS,OAAO,CAAA;AAAA,kBAC1C,cAAY,QAAA,CAAS,aAAA;AAAA,kBACrB,WAAW,EAAA,CAAG,GAAA;AAAA,kBACd,kBAAA,EAAiB,EAAA;AAAA,kBAEjB,QAAA,kBAAAA,eAAC,KAAA,EAAA,EAAI,SAAA,EAAW,GAAG,OAAA,EAAS,uBAAA,EAAsB,EAAA,EAAG,KAAA,EAAO,EAAE,KAAA,EAAO,GAAG,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,IAAA,CAAK,GAAA,CAAI,GAAA,EAAK,SAAS,OAAO,CAAC,CAAC,CAAA,CAAA,CAAA,EAAI,EAAG;AAAA;AAAA;AAC7H,aAAA,EACF,CAAA,GACE,yBACFC,eAAA,CAAC,GAAA,EAAA,EAAE,WAAW,CAAC,EAAA,CAAG,UAAU,EAAA,CAAG,eAAe,EAAE,MAAA,CAAO,OAAO,EAAE,IAAA,CAAK,GAAG,KAAK,MAAA,EAAW,uBAAA,EAAsB,EAAA,EAAG,aAAA,EAAY,EAAA,EAAI,QAAA,EAAA;AAAA,cAAA,KAAA,EAAO,eAAA;AAAA,cAAiB;AAAA,aAAA,EAAO,CAAA,GAC9J,IAAA;AAAA,4BAEJD,eAAC,IAAA,EAAA,EAAG,SAAA,EAAW,GAAG,KAAA,EAAO,oBAAA,EAAmB,EAAA,EACzC,QAAA,EAAA,KAAA,CAAM,GAAA,CAAI,CAAC,yBACVA,cAAAA,CAAC,UAAA,EAAA,EAAyB,IAAA,EAAY,KAAA,EAAc,KAAA,EAAc,MAAY,KAAA,EAAA,EAA7D,IAAA,CAAK,EAAsE,CAC7F,CAAA,EACH,CAAA;AAAA,YAEC,MAAA,mBACCC,eAAA,CAAC,SAAA,EAAA,EAAQ,SAAA,EAAW,EAAA,CAAG,MAAA,EAAQ,qBAAA,EAAoB,EAAA,EAAG,iBAAA,EAAiB,WAAA,GAAc,QAAA,GAAW,MAAA,EAC7F,QAAA,EAAA;AAAA,cAAA,WAAA,mBAAcD,eAAC,IAAA,EAAA,EAAG,EAAA,EAAI,UAAU,SAAA,EAAW,EAAA,CAAG,WAAA,EAAc,QAAA,EAAA,WAAA,EAAY,CAAA,GAAQ,IAAA;AAAA,cAChF;AAAA,aAAA,EACH,CAAA,GACE;AAAA,WAAA,EACN,CAAA;AAAA,0CAEC,KAAA,EAAA,EAAI,SAAA,EAAW,EAAA,CAAG,MAAA,EAAQ,uBAAoB,EAAA,EAC7C,QAAA,EAAA;AAAA,4BAAAC,eAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAW,EAAA,CAAG,QAAA,EAAU,yBAAsB,EAAA,EACjD,QAAA,EAAA;AAAA,8BAAAD,eAAC,MAAA,EAAA,EAAK,SAAA,EAAW,EAAA,CAAG,aAAA,EAAgB,iBAAO,QAAA,EAAS,CAAA;AAAA,8BACpDA,cAAAA,CAAC,MAAA,EAAA,EAAK,SAAA,EAAW,GAAG,aAAA,EAAe,6BAAA,EAA4B,EAAA,EAAI,QAAA,EAAA,IAAA,GAAO,KAAA,CAAM,IAAA,CAAK,IAAA,CAAK,QAAQ,IAAI,IAAA,EAAK;AAAA,aAAA,EAC7G,CAAA;AAAA,YACC,MAAA,CAAO,IAAA,mBAAOA,cAAAA,CAAC,GAAA,EAAA,EAAE,SAAA,EAAW,EAAA,CAAG,IAAA,EAAM,mBAAA,EAAkB,EAAA,EAAI,QAAA,EAAA,MAAA,CAAO,IAAA,EAAK,CAAA,GAAO,IAAA;AAAA,4CAC9E,KAAA,EAAA,EAAI,SAAA,EAAW,EAAA,CAAG,OAAA,EAAS,wBAAqB,EAAA,EAC9C,QAAA,EAAA;AAAA,cAAA,IAAA,CAAK,EAAE,IAAA,EAAM,KAAA,CAAM,QAAA,EAAU,SAAA,EAAW,EAAA,CAAG,QAAA,EAAU,QAAA,EAAU,MAAA,CAAO,QAAA,EAAU,OAAA,EAAS,KAAA,EAAO,CAAA;AAAA,cAChG,IAAA,CAAK,EAAE,IAAA,EAAM,KAAA,CAAM,IAAA,EAAM,SAAA,EAAW,EAAA,CAAG,QAAA,EAAU,QAAA,EAAU,MAAA,CAAO,QAAA,EAAU,OAAA,EAAS,OAAO;AAAA,aAAA,EAC/F;AAAA,WAAA,EACF;AAAA,SAAA,EACF;AAAA,OAAA,EAEJ;AAAA;AAAA,GACF;AAEJ;AAEA,SAAS,UAAA,CAAW;AAAA,EAClB,IAAA;AAAA,EACA,KAAA;AAAA,EACA,KAAA;AAAA,EACA,IAAA;AAAA,EACA;AACF,CAAA,EAMG;AACD,EAAA,MAAM,EAAE,QAAQ,KAAA,EAAO,KAAA,EAAO,YAAY,EAAA,GAAK,IAAG,GAAI,KAAA;AACtD,EAAA,MAAM,IAAI,EAAE,GAAG,YAAA,EAAc,GAAG,MAAM,IAAA,EAAK;AAC3C,EAAA,MAAM,EAAE,IAAA,EAAM,QAAA,EAAS,GAAA,CAAK,KAAA,CAAM,eAAe,aAAA,GAAe;AAChE,EAAA,MAAM,QAAA,GAAW,QAAA,GAAW,MAAA,CAAO,QAAA,CAAS,GAAA,CAAI,EAAE,aAAa,CAAA,IAAK,CAAC,CAAA,GAAI,IAAA,CAAK,QAAA;AAC9E,EAAA,IAAI,QAAA,KAAa,QAAA,CAAS,GAAA,CAAI,CAAA,CAAE,WAAW,MAAM,CAAA,CAAE,YAAA,IAAgB,QAAA,KAAa,CAAA,CAAA,EAAI,OAAO,IAAA;AAE3F,EAAA,MAAM,QAAQ,IAAA,CAAK,WAAA;AACnB,EAAA,MAAM,SAAA,GAAY,MAAM,qBAAA,IAAyB,uBAAA;AACjD,EAAA,MAAM,KAAA,GAAQ,MAAM,OAAA,CAAQ,KAAA;AAC5B,EAAA,MAAM,OAAO,KAAA,CAAM,OAAA,GAAU,MAAM,OAAA,CAAQ,MAAA,EAAQ,MAAM,EAAE,CAAA;AAC3D,EAAA,MAAM,GAAA,GAAM,KAAA,CAAM,KAAA,IAAS,KAAA,CAAM,OAAA,CAAQ,cAAA;AACzC,EAAA,MAAM,KAAA,GAAQ,KAAA,CAAM,WAAA,GAAc,KAAA,CAAM,WAAA,CAAY,IAAI,CAAA,GAAI,GAAA,mBAC1DA,cAAAA,CAAC,KAAA,EAAA,EAAI,GAAA,EAAK,GAAA,CAAI,KAAK,GAAA,EAAK,GAAA,CAAI,GAAA,IAAO,EAAA,EAAI,KAAA,EAAO,EAAA,EAAI,MAAA,EAAQ,EAAA,EAAI,OAAA,EAAQ,MAAA,EAAO,QAAA,EAAS,OAAA,EAAQ,CAAA,GAC5F,IAAA;AAEJ,EAAA,MAAM,SAAA,mBAAYA,cAAAA,CAAC,MAAA,EAAA,EAAK,WAAW,EAAA,CAAG,KAAA,EAAO,oBAAA,EAAmB,EAAA,EAAI,QAAA,EAAA,KAAA,EAAM,CAAA;AAC1E,EAAA,MAAM,KAAA,mBAAQA,cAAAA,CAAC,MAAA,EAAA,EAAK,SAAA,EAAW,EAAA,CAAG,KAAA,EAAO,oBAAA,EAAmB,EAAA,EAAI,QAAA,EAAA,KAAA,CAAM,IAAA,CAAK,IAAA,CAAK,KAAK,CAAA,EAAE,CAAA;AACvF,EAAA,MAAM,2BACJC,eAAA,CAAC,KAAA,EAAA,EAAI,WAAW,EAAA,CAAG,QAAA,EAAU,yBAAsB,EAAA,EACjD,QAAA,EAAA;AAAA,oBAAAA,eAAA,CAAC,IAAA,EAAA,EAAK,MAAA,EAAO,MAAA,EAAO,MAAA,EAAQ,KAAA,CAAM,MAAM,SAAA,EAAW,EAAA,CAAG,GAAA,EAAK,kBAAA,EAAiB,EAAA,EAC1E,QAAA,EAAA;AAAA,sBAAAD,cAAAA,CAAC,WAAM,IAAA,EAAK,QAAA,EAAS,MAAM,CAAA,CAAE,WAAA,EAAa,KAAA,EAAO,CAAA,CAAE,YAAA,EAAc,CAAA;AAAA,sBACjEA,cAAAA,CAAC,OAAA,EAAA,EAAM,IAAA,EAAK,QAAA,EAAS,MAAM,CAAA,CAAE,SAAA,EAAW,KAAA,EAAO,IAAA,CAAK,EAAA,EAAI,CAAA;AAAA,sBACxDA,cAAAA,CAAC,QAAA,EAAA,EAAO,IAAA,EAAK,QAAA,EAAS,IAAA,EAAM,CAAA,CAAE,aAAA,EAAe,KAAA,EAAO,QAAA,GAAW,CAAA,EAAG,YAAA,EAAY,MAAA,CAAO,QAAA,EAAU,SAAA,EAAW,EAAA,CAAG,SAAA,EAAW,yBAAA,EAAwB,EAAA,EAC7I,QAAA,EAAA,KAAA,EAAO,QAAA,oBAAYA,cAAAA,CAAC,MAAA,EAAA,EAAK,aAAA,EAAY,MAAA,EAAO,QAAA,EAAA,QAAA,EAAC,CAAA,EAChD,CAAA;AAAA,sBACAA,cAAAA,CAAC,MAAA,EAAA,EAAK,SAAA,EAAW,EAAA,CAAG,UAAU,WAAA,EAAU,QAAA,EAAS,wBAAA,EAAuB,EAAA,EAAI,QAAA,EAAA,QAAA,EAAS,CAAA;AAAA,sBACrFA,cAAAA,CAAC,QAAA,EAAA,EAAO,IAAA,EAAK,QAAA,EAAS,IAAA,EAAM,CAAA,CAAE,aAAA,EAAe,KAAA,EAAO,QAAA,GAAW,CAAA,EAAG,YAAA,EAAY,MAAA,CAAO,QAAA,EAAU,SAAA,EAAW,EAAA,CAAG,SAAA,EAAW,yBAAA,EAAwB,EAAA,EAC7I,QAAA,EAAA,KAAA,EAAO,QAAA,oBAAYA,cAAAA,CAAC,MAAA,EAAA,EAAK,aAAA,EAAY,MAAA,EAAO,QAAA,EAAA,GAAA,EAAC,CAAA,EAChD;AAAA,KAAA,EACF,CAAA;AAAA,oBACAC,eAAA,CAAC,QAAK,MAAA,EAAO,MAAA,EAAO,QAAQ,KAAA,CAAM,IAAA,EAAM,4BAAyB,EAAA,EAC/D,QAAA,EAAA;AAAA,sBAAAD,cAAAA,CAAC,WAAM,IAAA,EAAK,QAAA,EAAS,MAAM,CAAA,CAAE,WAAA,EAAa,KAAA,EAAO,CAAA,CAAE,YAAA,EAAc,CAAA;AAAA,sBACjEA,cAAAA,CAAC,OAAA,EAAA,EAAM,IAAA,EAAK,QAAA,EAAS,MAAM,CAAA,CAAE,SAAA,EAAW,KAAA,EAAO,IAAA,CAAK,EAAA,EAAI,CAAA;AAAA,sBACxDA,cAAAA,CAAC,QAAA,EAAA,EAAO,IAAA,EAAK,QAAA,EAAS,SAAA,EAAW,EAAA,CAAG,MAAA,EAAQ,qBAAA,EAAoB,EAAA,EAAI,QAAA,EAAA,MAAA,CAAO,MAAA,EAAO;AAAA,KAAA,EACpF;AAAA,GAAA,EACF,CAAA;AAGF,EAAA,IAAI,KAAA,CAAM,YAAY,uBAAOA,eAAC,IAAA,EAAA,EAAiB,SAAA,EAAW,EAAA,CAAG,IAAA,EAAM,mBAAA,EAAkB,EAAA,EAAI,gBAAM,UAAA,CAAW,IAAA,EAAM,EAAE,QAAA,EAAU,KAAA,EAAO,OAAO,SAAA,EAAW,CAAA,EAAA,EAA/G,IAAA,CAAK,EAA4G,CAAA;AAEvJ,EAAA,uCACG,IAAA,EAAA,EAAG,SAAA,EAAW,EAAA,CAAG,IAAA,EAAM,qBAAkB,EAAA,EACvC,QAAA,EAAA;AAAA,IAAA,SAAA;AAAA,oCACA,KAAA,EAAA,EAAI,SAAA,EAAW,EAAA,CAAG,QAAA,EAAU,0BAAuB,EAAA,EAClD,QAAA,EAAA;AAAA,sBAAAC,eAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAW,EAAA,CAAG,QAAA,EAAU,0BAAuB,EAAA,EAClD,QAAA,EAAA;AAAA,wBAAAA,eAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAW,EAAA,CAAG,QAAA,EAAU,0BAAuB,EAAA,EACjD,QAAA,EAAA;AAAA,UAAA,IAAA,GAAO,IAAA,CAAK,EAAE,IAAA,EAAM,SAAA,EAAW,GAAG,SAAA,EAAW,QAAA,EAAU,OAAO,OAAA,EAAS,KAAA,EAAO,CAAA,mBAAID,eAAC,MAAA,EAAA,EAAK,SAAA,EAAW,GAAG,SAAA,EAAW,yBAAA,EAAwB,IAAI,QAAA,EAAA,KAAA,EAAM,CAAA;AAAA,UACnJ,CAAC,SAAA,CAAU,KAAA,CAAM,KAAK,oBAAIA,cAAAA,CAAC,GAAA,EAAA,EAAE,SAAA,EAAW,GAAG,OAAA,EAAS,sBAAA,EAAqB,EAAA,EAAI,QAAA,EAAA,KAAA,CAAM,OAAM,CAAA,GAAO;AAAA,SAAA,EACnG,CAAA;AAAA,QACC;AAAA,OAAA,EACH,CAAA;AAAA,MACC;AAAA,KAAA,EACH;AAAA,GAAA,EACF,CAAA;AAEJ","file":"cart.cjs","sourcesContent":["import { useCallback, useEffect, useRef, type RefObject } from 'react'\n\nexport type DialogCloseReason = 'esc' | 'x' | 'backdrop' | 'action'\n\ntype UseModalDialogOptions = {\n open: boolean\n /** Called for Esc and backdrop clicks. The owner decides whether to close. */\n onClose: (reason: DialogCloseReason) => void\n /** Element focused when the dialog opens. Default: the element marked `data-sf-autofocus`, else the dialog itself. */\n initialFocus?: RefObject<HTMLElement | null>\n}\n\n/**\n * Native `<dialog>` wiring: `showModal()` (focus trap and inert background come from the browser),\n * Esc and backdrop clicks routed to `onClose`, and focus restored to the opener on close.\n * Falls back to the `open` attribute where `showModal` does not exist (old browsers, some test DOMs)\n * with an Esc key handler.\n */\nexport function useModalDialog({ open, onClose, initialFocus }: UseModalDialogOptions) {\n const ref = useRef<HTMLDialogElement | null>(null)\n const opener = useRef<Element | null>(null)\n const onCloseRef = useRef(onClose)\n onCloseRef.current = onClose\n\n useEffect(() => {\n const el = ref.current\n if (!el || !open) return\n opener.current = typeof document !== 'undefined' ? document.activeElement : null\n const hasModal = typeof el.showModal === 'function'\n if (!el.open) {\n if (hasModal) {\n try {\n el.showModal()\n } catch {\n el.setAttribute('open', '')\n }\n } else el.setAttribute('open', '')\n }\n const target = initialFocus?.current ?? el.querySelector<HTMLElement>('[data-sf-autofocus]') ?? el\n if (!target.hasAttribute('tabindex') && target === el) el.setAttribute('tabindex', '-1')\n target.focus({ preventScroll: true })\n\n const onCancel = (e: Event) => {\n e.preventDefault() // we close through state so the owner can navigate/track first\n onCloseRef.current('esc')\n }\n const onKey = (e: KeyboardEvent) => {\n if (e.key === 'Escape') {\n e.preventDefault()\n onCloseRef.current('esc')\n }\n }\n el.addEventListener('cancel', onCancel)\n if (!hasModal) el.addEventListener('keydown', onKey)\n return () => {\n el.removeEventListener('cancel', onCancel)\n el.removeEventListener('keydown', onKey)\n if (el.open) {\n if (typeof el.close === 'function') {\n try {\n el.close()\n } catch {\n el.removeAttribute('open')\n }\n } else el.removeAttribute('open')\n }\n const back = opener.current\n if (back instanceof HTMLElement && back.isConnected) back.focus({ preventScroll: true })\n }\n }, [open])\n\n /** Click on the backdrop (the dialog element itself, outside its content box). */\n const onBackdropClick = useCallback((e: { target: EventTarget; currentTarget: EventTarget }) => {\n if (e.target === e.currentTarget) onCloseRef.current('backdrop')\n }, [])\n\n return { ref, onBackdropClick }\n}\n\n/** `prefers-reduced-motion: reduce`, read after hydration (false on the server). */\nexport function prefersReducedMotion(): boolean {\n try {\n return typeof window !== 'undefined' && !!window.matchMedia?.('(prefers-reduced-motion: reduce)').matches\n } catch {\n return false\n }\n}\n","import { createContext, useContext, useMemo, type ReactNode } from 'react'\nimport { createStorefrontClient, type Shop, type StorefrontClient, type StorefrontClientOptions } from '@asteby/storefront'\n\nexport type StorefrontContextValue = {\n /** Client for browser-side calls (use a public `pk_` token). */\n client: StorefrontClient | null\n shop: Shop | null\n /** BCP 47 locale used for formatting (defaults to `shop.locale`, then `es-MX`). */\n locale: string\n /** ISO 4217 currency (defaults to `shop.currency_code`, then `MXN`). */\n currency: string\n}\n\nconst StorefrontContext = createContext<StorefrontContextValue | null>(null)\n\nexport type StorefrontProviderProps = {\n /** A ready client. Takes precedence over `config`. */\n client?: StorefrontClient\n /** Options to create the browser client (use a `pk_` token). */\n config?: StorefrontClientOptions\n /** Shop info loaded on the server (root loader). */\n shop?: Shop | null\n locale?: string\n currency?: string\n children?: ReactNode\n}\n\n/**\n * Makes the storefront client, shop, locale and currency available to the\n * tree. SSR-safe: creating the client does not touch `window`.\n */\nexport function StorefrontProvider({ client, config, shop = null, locale, currency, children }: StorefrontProviderProps) {\n const configKey = config ? `${config.baseUrl}|${config.token}|${config.locale ?? ''}|${config.customerToken ?? ''}` : ''\n const resolvedClient = useMemo(\n () => client ?? (config ? createStorefrontClient(config) : null),\n [client, configKey],\n )\n const value = useMemo<StorefrontContextValue>(\n () => ({\n client: resolvedClient,\n shop,\n locale: locale ?? config?.locale ?? shop?.locale ?? 'es-MX',\n currency: currency ?? shop?.currency_code ?? 'MXN',\n }),\n [resolvedClient, shop, locale, currency, config?.locale],\n )\n return <StorefrontContext.Provider value={value}>{children}</StorefrontContext.Provider>\n}\n\n/** Storefront context. Throws outside `<StorefrontProvider>`. */\nexport function useStorefront(): StorefrontContextValue {\n const ctx = useContext(StorefrontContext)\n if (!ctx) throw new Error('useStorefront must be used inside <StorefrontProvider>')\n return ctx\n}\n\n/** Storefront context or `null` outside a provider (for components that work standalone). */\nexport function useOptionalStorefront(): StorefrontContextValue | null {\n return useContext(StorefrontContext)\n}\n\n/** The client from context; throws when the provider has none. */\nexport function useStorefrontClient(): StorefrontClient {\n const { client } = useStorefront()\n if (!client) throw new Error('<StorefrontProvider> needs a `client` or `config` for browser-side calls')\n return client\n}\n","import { useOptionalStorefront } from './context.js'\n\n/** Locale of a component: its own prop, else the `StorefrontProvider`'s, else `es-MX`. */\nexport function useResolvedLocale(locale?: string | null): string {\n const sf = useOptionalStorefront()\n return locale ?? sf?.locale ?? 'es-MX'\n}\n","import { useCallback, useEffect, useRef, useSyncExternalStore } from 'react'\n\n// Estado mínimo del cajón del carrito, fuera de React: cualquier parte de la tienda (cabecera, tarjeta, ficha)\n// lo abre sin pasar props. Es del navegador; en el servidor siempre está cerrado.\n\nlet isOpen = false\nconst listeners = new Set<() => void>()\n\nfunction set(next: boolean) {\n if (isOpen === next) return\n isOpen = next\n for (const l of [...listeners]) l()\n}\n\nconst subscribe = (l: () => void) => {\n listeners.add(l)\n return () => {\n listeners.delete(l)\n }\n}\n\n/** Abre el cajón del carrito desde cualquier parte. */\nexport function openCartDrawer() {\n set(true)\n}\n\n/** Cierra el cajón del carrito. */\nexport function closeCartDrawer() {\n set(false)\n}\n\nexport function toggleCartDrawer() {\n set(!isOpen)\n}\n\nexport function isCartDrawerOpen() {\n return isOpen\n}\n\n/** Estado y acciones del cajón. `open` es `false` en el servidor y en la primera pintura. */\nexport function useCartDrawer() {\n const open = useSyncExternalStore(subscribe, () => isOpen, () => false)\n const toggle = useCallback(() => toggleCartDrawer(), [])\n return { open, openDrawer: openCartDrawer, closeDrawer: closeCartDrawer, toggle }\n}\n\n/**\n * Abre el cajón cuando termina un \"agregar al carrito\". `busy` es verdadero mientras hay un agregado en vuelo\n * (en React Router: algún fetcher con la acción del carrito e intención de agregar). Con `skip` verdadero al\n * terminar (error, agregado silencioso) no se abre.\n */\nexport function useOpenCartDrawerWhenDone(busy: boolean, options: { skip?: boolean } = {}) {\n const was = useRef(false)\n const skip = useRef(false)\n skip.current = !!options.skip\n useEffect(() => {\n if (was.current && !busy && !skip.current) openCartDrawer()\n was.current = busy\n }, [busy])\n}\n\ntype ClickLike = {\n defaultPrevented: boolean\n button: number\n metaKey: boolean\n ctrlKey: boolean\n shiftKey: boolean\n altKey: boolean\n preventDefault: () => void\n}\n\n/**\n * `onClick` para el enlace \"Mi carrito\" de la cabecera: con un clic normal abre el cajón; con clic con\n * modificador, clic central o sin JavaScript el `href` (la página del carrito) sigue funcionando.\n */\nexport function handleCartLinkClick(e: ClickLike) {\n if (e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return\n e.preventDefault()\n openCartDrawer()\n}\n","import { useEffect, useId, type ElementType, type ReactNode } from 'react'\nimport { formatMoney, type Cart, type CartLine, type Money } from '@asteby/storefront'\nimport { useModalDialog } from '../dialog.js'\nimport { useResolvedLocale } from '../locale.js'\nimport { closeCartDrawer, useCartDrawer } from './store.js'\n\nexport type CartDrawerLabels = {\n /** Título; el cajón agrega \" (N)\" con la cantidad de unidades. */\n title: string\n /** Texto accesible del botón de cierre. */\n close: string\n empty: string\n emptyHint?: string\n /** Texto del enlace \"seguir comprando\" del estado vacío (se muestra con `paths.browse`). */\n browse?: string\n subtotal: string\n /** Nota bajo el subtotal. */\n note?: string\n checkout: string\n viewCart: string\n remove: string\n increase: string\n decrease: string\n}\n\nexport type CartDrawerPaths = {\n /** Página del carrito: acción de las mutaciones y destino de \"ver carrito\". */\n cart: string\n checkout: string\n browse?: string\n /** Enlace de la línea al producto; sin esto el título no es enlace. */\n product?: (handle: string, variantId: string) => string\n}\n\n/** Contrato de la acción de ruta del carrito de la tienda (nombres de campo e intenciones). */\nexport type CartDrawerFormContract = {\n intentField?: string\n lineField?: string\n quantityField?: string\n updateIntent?: string\n removeIntent?: string\n}\n\nexport type CartDrawerShipping = {\n /** Mensaje ya calculado por la tienda con lo que manda OPS. */\n message: ReactNode\n /** 0..100 */\n percent: number\n reached?: boolean\n progressLabel: string\n}\n\nexport type CartDrawerLineForm = {\n /** Componente de formulario (`'form'`, o `fetcher.Form` del router para mejorar sin recargar). */\n Form: ElementType\n /** Datos en vuelo del formulario, para pintar la cantidad de forma optimista. */\n formData?: FormData | null | undefined\n}\n\nexport type CartDrawerLinkProps = { href: string; className?: string | undefined; children: ReactNode; onClick?: () => void }\n\nexport type CartDrawerSlot =\n | 'dialog' | 'panel' | 'header' | 'title' | 'close' | 'empty' | 'body' | 'shipping' | 'bar' | 'barFill' | 'lines' | 'line'\n | 'image' | 'lineTitle' | 'variant' | 'price' | 'qty' | 'qtyButton' | 'qtyValue' | 'remove' | 'upsell' | 'footer' | 'subtotal'\n | 'note' | 'actions' | 'checkout' | 'viewCart' | 'browse' | 'emptyTitle' | 'emptyHint' | 'shippingMessage' | 'lineMain'\n | 'lineHead' | 'lineText' | 'controls' | 'upsellTitle' | 'subtotalLabel' | 'subtotalValue'\n\nexport type CartDrawerIcons = { close?: ReactNode; decrease?: ReactNode; increase?: ReactNode; shipping?: ReactNode; shippingReached?: ReactNode }\n\nexport type CartDrawerProps = {\n /** `cart.cost` y cada línea vienen de OPS; el cajón no calcula nada. */\n cart: Cart | null | undefined\n labels: CartDrawerLabels\n paths: CartDrawerPaths\n form?: CartDrawerFormContract\n /** Hook que da el formulario de cada línea. Por omisión `<form method=\"post\">` (funciona sin JavaScript). */\n useLineForm?: () => CartDrawerLineForm\n shipping?: CartDrawerShipping | null\n /** Aviso fijo cuando no hay barra de envío (p. ej. \"Envío gratis\"). */\n notice?: ReactNode\n /** Sección \"podría interesarte\" (la tienda decide qué mostrar). */\n upsell?: ReactNode\n upsellTitle?: string\n renderLine?: (line: CartLine, defaults: { controls: ReactNode; price: ReactNode; image: ReactNode }) => ReactNode\n renderImage?: (line: CartLine) => ReactNode\n renderMoney?: (money: Money) => ReactNode\n renderLink?: (props: CartDrawerLinkProps) => ReactNode\n /** Omite el variante cuando es el valor por defecto de OPS (\"Default Title\"). */\n isDefaultVariantTitle?: (title: string) => boolean\n icons?: CartDrawerIcons\n /** Clases por pieza (además de los `data-sf-cart-*`), para tiendas con Tailwind. */\n classNames?: Partial<Record<CartDrawerSlot, string>>\n locale?: string\n /** Se llama al abrir/cerrar por cualquier vía. */\n onOpenChange?: (open: boolean) => void\n}\n\nconst DEFAULT_FORM: Required<CartDrawerFormContract> = {\n intentField: 'intent',\n lineField: 'line_id',\n quantityField: 'quantity',\n updateIntent: 'update',\n removeIntent: 'remove',\n}\n\nconst useNativeForm = (): CartDrawerLineForm => ({ Form: 'form' })\n\nconst defaultIsDefaultVariant = (t: string) => /^default title$/i.test(t.trim())\n\nfunction useBodyScrollLock(locked: boolean) {\n useEffect(() => {\n if (!locked || typeof document === 'undefined') return\n const el = document.documentElement\n const prev = el.style.overflow\n el.style.overflow = 'hidden'\n return () => {\n el.style.overflow = prev\n }\n }, [locked])\n}\n\n/**\n * Cajón lateral del carrito (\"Tu carrito (N)\"). Sin textos ni marca: todo llega por props.\n * Se monta una vez (en la raíz) y se abre con `openCartDrawer()` / `useCartDrawer()`.\n * `<dialog>` modal: `role=\"dialog\"`, `aria-modal`, foco atrapado y devuelto al abridor, Esc y clic fuera cierran,\n * el fondo no hace scroll. Las mutaciones usan la acción de ruta de la tienda (formularios nativos que\n * funcionan sin JavaScript, o el `Form` de un fetcher con `useLineForm`). Estilos: `@asteby/storefront-react/cart.css`\n * (tokens de la tienda) y/o `classNames`.\n */\nexport function CartDrawer(props: CartDrawerProps) {\n const { cart, labels, paths, shipping, notice, upsell, upsellTitle, icons, classNames: cx = {}, onOpenChange } = props\n const locale = useResolvedLocale(props.locale)\n const { open } = useCartDrawer()\n const titleId = useId()\n const upsellId = useId()\n const { ref, onBackdropClick } = useModalDialog({ open, onClose: () => closeCartDrawer() })\n useBodyScrollLock(open)\n useEffect(() => {\n onOpenChange?.(open)\n }, [open]) // eslint-disable-line react-hooks/exhaustive-deps\n\n if (!open) return null\n\n const money = props.renderMoney ?? ((m: Money) => formatMoney(m, locale))\n const link = props.renderLink ?? (({ href, className, children, onClick }: CartDrawerLinkProps) => (\n <a href={href} className={className} onClick={onClick}>{children}</a>\n ))\n const count = cart?.total_quantity ?? 0\n const lines = cart?.lines ?? []\n const empty = !cart || lines.length === 0\n const close = () => closeCartDrawer()\n\n return (\n <dialog\n ref={ref}\n role=\"dialog\"\n aria-modal=\"true\"\n aria-labelledby={titleId}\n onClick={onBackdropClick}\n onClose={close}\n className={cx.dialog}\n data-sf-cart-drawer=\"\"\n >\n <div className={cx.panel} data-sf-cart-panel=\"\">\n <div className={cx.header} data-sf-cart-header=\"\">\n <h2 id={titleId} tabIndex={-1} className={cx.title} data-sf-cart-title=\"\">\n {labels.title}{count > 0 ? ` (${count})` : ''}\n </h2>\n <button type=\"button\" onClick={close} aria-label={labels.close} className={cx.close} data-sf-cart-close=\"\" data-sf-autofocus=\"\">\n {icons?.close ?? <span aria-hidden=\"true\">✕</span>}\n </button>\n </div>\n\n {empty ? (\n <div className={cx.empty} data-sf-cart-empty=\"\">\n <p className={cx.emptyTitle} data-sf-cart-empty-title=\"\">{labels.empty}</p>\n {labels.emptyHint ? <p className={cx.emptyHint} data-sf-cart-empty-hint=\"\">{labels.emptyHint}</p> : null}\n {paths.browse && labels.browse ? link({ href: paths.browse, className: cx.browse, children: labels.browse, onClick: close }) : null}\n </div>\n ) : (\n <>\n <div className={cx.body} data-sf-cart-body=\"\">\n {shipping ? (\n <div className={cx.shipping} data-sf-cart-shipping=\"\" data-reached={shipping.reached || undefined}>\n <p className={cx.shippingMessage}>\n {shipping.reached ? icons?.shippingReached : icons?.shipping}\n {shipping.message}\n </p>\n <div\n role=\"progressbar\"\n aria-valuemin={0}\n aria-valuemax={100}\n aria-valuenow={Math.round(shipping.percent)}\n aria-label={shipping.progressLabel}\n className={cx.bar}\n data-sf-cart-bar=\"\"\n >\n <div className={cx.barFill} data-sf-cart-bar-fill=\"\" style={{ width: `${Math.max(0, Math.min(100, shipping.percent))}%` }} />\n </div>\n </div>\n ) : notice ? (\n <p className={[cx.shipping, cx.shippingMessage].filter(Boolean).join(\" \") || undefined} data-sf-cart-shipping=\"\" data-notice=\"\">{icons?.shippingReached}{notice}</p>\n ) : null}\n\n <ul className={cx.lines} data-sf-cart-lines=\"\">\n {lines.map((line) => (\n <DrawerLine key={line.id} line={line} props={props} money={money} link={link} close={close} />\n ))}\n </ul>\n\n {upsell ? (\n <section className={cx.upsell} data-sf-cart-upsell=\"\" aria-labelledby={upsellTitle ? upsellId : undefined}>\n {upsellTitle ? <h3 id={upsellId} className={cx.upsellTitle}>{upsellTitle}</h3> : null}\n {upsell}\n </section>\n ) : null}\n </div>\n\n <div className={cx.footer} data-sf-cart-footer=\"\">\n <div className={cx.subtotal} data-sf-cart-subtotal=\"\">\n <span className={cx.subtotalLabel}>{labels.subtotal}</span>\n <span className={cx.subtotalValue} data-sf-cart-subtotal-value=\"\">{cart ? money(cart.cost.subtotal) : null}</span>\n </div>\n {labels.note ? <p className={cx.note} data-sf-cart-note=\"\">{labels.note}</p> : null}\n <div className={cx.actions} data-sf-cart-actions=\"\">\n {link({ href: paths.checkout, className: cx.checkout, children: labels.checkout, onClick: close })}\n {link({ href: paths.cart, className: cx.viewCart, children: labels.viewCart, onClick: close })}\n </div>\n </div>\n </>\n )}\n </div>\n </dialog>\n )\n}\n\nfunction DrawerLine({\n line,\n props,\n money,\n link,\n close,\n}: {\n line: CartLine\n props: CartDrawerProps\n money: (m: Money) => ReactNode\n link: (p: CartDrawerLinkProps) => ReactNode\n close: () => void\n}) {\n const { labels, paths, icons, classNames: cx = {} } = props\n const f = { ...DEFAULT_FORM, ...props.form }\n const { Form, formData } = (props.useLineForm ?? useNativeForm)()\n const quantity = formData ? Number(formData.get(f.quantityField) ?? 0) : line.quantity\n if (formData && (formData.get(f.intentField) === f.removeIntent || quantity === 0)) return null\n\n const merch = line.merchandise\n const isDefault = props.isDefaultVariantTitle ?? defaultIsDefaultVariant\n const title = merch.product.title\n const href = paths.product?.(merch.product.handle, merch.id)\n const img = merch.image ?? merch.product.featured_image\n const image = props.renderImage ? props.renderImage(line) : img ? (\n <img src={img.url} alt={img.alt ?? ''} width={80} height={80} loading=\"lazy\" decoding=\"async\" />\n ) : null\n\n const imageNode = <span className={cx.image} data-sf-cart-image=\"\">{image}</span>\n const price = <span className={cx.price} data-sf-cart-price=\"\">{money(line.cost.total)}</span>\n const controls = (\n <div className={cx.controls} data-sf-cart-controls=\"\">\n <Form method=\"post\" action={paths.cart} className={cx.qty} data-sf-cart-qty=\"\">\n <input type=\"hidden\" name={f.intentField} value={f.updateIntent} />\n <input type=\"hidden\" name={f.lineField} value={line.id} />\n <button type=\"submit\" name={f.quantityField} value={quantity - 1} aria-label={labels.decrease} className={cx.qtyButton} data-sf-cart-qty-button=\"\">\n {icons?.decrease ?? <span aria-hidden=\"true\">−</span>}\n </button>\n <span className={cx.qtyValue} aria-live=\"polite\" data-sf-cart-qty-value=\"\">{quantity}</span>\n <button type=\"submit\" name={f.quantityField} value={quantity + 1} aria-label={labels.increase} className={cx.qtyButton} data-sf-cart-qty-button=\"\">\n {icons?.increase ?? <span aria-hidden=\"true\">+</span>}\n </button>\n </Form>\n <Form method=\"post\" action={paths.cart} data-sf-cart-remove-form=\"\">\n <input type=\"hidden\" name={f.intentField} value={f.removeIntent} />\n <input type=\"hidden\" name={f.lineField} value={line.id} />\n <button type=\"submit\" className={cx.remove} data-sf-cart-remove=\"\">{labels.remove}</button>\n </Form>\n </div>\n )\n\n if (props.renderLine) return <li key={line.id} className={cx.line} data-sf-cart-line=\"\">{props.renderLine(line, { controls, price, image: imageNode })}</li>\n\n return (\n <li className={cx.line} data-sf-cart-line=\"\">\n {imageNode}\n <div className={cx.lineMain} data-sf-cart-line-main=\"\">\n <div className={cx.lineHead} data-sf-cart-line-head=\"\">\n <div className={cx.lineText} data-sf-cart-line-text=\"\">\n {href ? link({ href, className: cx.lineTitle, children: title, onClick: close }) : <span className={cx.lineTitle} data-sf-cart-line-title=\"\">{title}</span>}\n {!isDefault(merch.title) ? <p className={cx.variant} data-sf-cart-variant=\"\">{merch.title}</p> : null}\n </div>\n {price}\n </div>\n {controls}\n </div>\n </li>\n )\n}\n"]}
package/dist/cart.css ADDED
@@ -0,0 +1,70 @@
1
+ /*
2
+ * @asteby/storefront-react/cart.css: estilos por omisión del cajón del carrito (CartDrawer). CSS plano, sin Tailwind.
3
+ * Se pinta con los tokens de la tienda: --background, --foreground, --card, --border, --muted-foreground, --primary,
4
+ * --primary-foreground, --success, --ring, --radius. Cada tienda los define (y los cambia en tema oscuro).
5
+ * Sin colores ni marcas escritos aquí. Las tiendas con Tailwind pueden omitir esta hoja y usar `classNames`.
6
+ */
7
+ [data-sf-cart-drawer] {
8
+ position: fixed; inset: 0 0 0 auto; margin: 0; padding: 0; width: 100%; max-width: 28rem;
9
+ height: 100dvh; max-height: none; border: 0; color: var(--foreground); background: var(--card, var(--background));
10
+ box-shadow: -12px 0 40px rgb(0 0 0 / .25);
11
+ }
12
+ [data-sf-cart-drawer]::backdrop { background: rgb(0 0 0 / .55); backdrop-filter: blur(2px); }
13
+ [data-sf-cart-drawer]:focus { outline: none; }
14
+ [data-sf-cart-panel] { display: flex; flex-direction: column; height: 100%; }
15
+ [data-sf-cart-header] { display: flex; align-items: center; justify-content: space-between; gap: 1rem; padding: 1rem 1.25rem; border-bottom: 1px solid var(--border); }
16
+ [data-sf-cart-title] { margin: 0; font-size: 1.125rem; font-weight: 700; letter-spacing: .04em; text-transform: uppercase; outline: none; }
17
+ [data-sf-cart-close] { display: grid; place-items: center; width: 2.75rem; height: 2.75rem; padding: 0; font: inherit; font-size: 1.25rem; color: inherit; background: none; border: 0; border-radius: var(--radius, .375rem); cursor: pointer; }
18
+ [data-sf-cart-close]:hover, [data-sf-cart-qty-button]:hover { background: color-mix(in srgb, var(--foreground) 10%, transparent); }
19
+ [data-sf-cart-drawer] :is(button, a):focus-visible { outline: 2px solid var(--ring, var(--primary)); outline-offset: 2px; }
20
+
21
+ [data-sf-cart-empty] { display: grid; flex: 1; place-content: center; justify-items: center; gap: 1rem; padding: 0 2rem; text-align: center; }
22
+ [data-sf-cart-empty] p { margin: 0; }
23
+ [data-sf-cart-empty-title] { font-size: 1.125rem; font-weight: 700; }
24
+ [data-sf-cart-empty-hint] { font-size: .875rem; color: var(--muted-foreground); }
25
+
26
+ [data-sf-cart-body] { flex: 1; overflow-y: auto; overscroll-behavior: contain; }
27
+ [data-sf-cart-shipping] { margin: 0; padding: 1rem 1.25rem; border-bottom: 1px solid var(--border); background: color-mix(in srgb, var(--primary) 8%, var(--background)); }
28
+ [data-sf-cart-shipping] > p { display: flex; align-items: center; gap: .5rem; margin: 0; font-size: .875rem; font-weight: 600; }
29
+ [data-sf-cart-bar] { height: .5rem; margin-top: .625rem; overflow: hidden; border-radius: 999px; background: var(--background); }
30
+ [data-sf-cart-bar-fill] { height: 100%; border-radius: inherit; background: var(--success, var(--primary)); transition: width .5s; }
31
+
32
+ [data-sf-cart-lines] { margin: 0; padding: 0 1.25rem; list-style: none; }
33
+ [data-sf-cart-line] { display: flex; gap: .75rem; padding: 1rem 0; }
34
+ [data-sf-cart-line] + [data-sf-cart-line] { border-top: 1px solid var(--border); }
35
+ [data-sf-cart-image] { display: block; flex: none; width: 5rem; height: 5rem; overflow: hidden; border-radius: var(--radius, .375rem); background: color-mix(in srgb, var(--foreground) 6%, transparent); }
36
+ [data-sf-cart-image] img { display: block; width: 100%; height: 100%; object-fit: contain; padding: .25rem; box-sizing: border-box; }
37
+ [data-sf-cart-line-main] { display: flex; flex: 1; flex-direction: column; min-width: 0; }
38
+ [data-sf-cart-line-head] { display: flex; justify-content: space-between; gap: .75rem; }
39
+ [data-sf-cart-line-text] { min-width: 0; }
40
+ [data-sf-cart-line-text] > a, [data-sf-cart-line-title] { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; font-size: .875rem; font-weight: 600; color: inherit; text-decoration: none; }
41
+ [data-sf-cart-line-text] > a:hover { text-decoration: underline; }
42
+ [data-sf-cart-variant] { margin: 0; font-size: .75rem; color: var(--muted-foreground); }
43
+ [data-sf-cart-price] { flex: none; font-size: .875rem; font-weight: 700; font-variant-numeric: tabular-nums; }
44
+ [data-sf-cart-controls] { display: flex; align-items: center; justify-content: space-between; margin-top: auto; padding-top: .5rem; }
45
+ [data-sf-cart-qty] { display: flex; align-items: center; border: 1px solid color-mix(in srgb, var(--foreground) 25%, transparent); border-radius: var(--radius, .375rem); }
46
+ [data-sf-cart-qty-button] { display: grid; place-items: center; width: 2.75rem; height: 2.75rem; padding: 0; font: inherit; color: inherit; background: none; border: 0; cursor: pointer; }
47
+ [data-sf-cart-qty-value] { min-width: 1.75rem; text-align: center; font-size: .875rem; font-weight: 600; font-variant-numeric: tabular-nums; }
48
+ [data-sf-cart-remove] { min-height: 2.75rem; padding: 0 .25rem; font: inherit; font-size: .75rem; color: var(--muted-foreground); text-decoration: underline; background: none; border: 0; cursor: pointer; }
49
+ [data-sf-cart-remove]:hover { color: var(--foreground); }
50
+
51
+ [data-sf-cart-upsell] { padding: 1.25rem; border-top: 1px solid var(--border); }
52
+ [data-sf-cart-upsell] h3 { margin: 0 0 1rem; font-size: .75rem; letter-spacing: .08em; text-transform: uppercase; }
53
+
54
+ [data-sf-cart-footer] { padding: 1rem 1.25rem max(1.25rem, env(safe-area-inset-bottom)); border-top: 1px solid var(--border); background: var(--card, var(--background)); }
55
+ [data-sf-cart-subtotal] { display: flex; align-items: baseline; justify-content: space-between; }
56
+ [data-sf-cart-subtotal] > :first-child { font-size: .875rem; color: var(--muted-foreground); }
57
+ [data-sf-cart-subtotal-value] { font-size: 1.25rem; font-weight: 700; font-variant-numeric: tabular-nums; }
58
+ [data-sf-cart-note] { margin: .25rem 0 0; font-size: .75rem; color: var(--muted-foreground); }
59
+ [data-sf-cart-actions] { display: grid; gap: .625rem; margin-top: 1rem; }
60
+ [data-sf-cart-actions] > a, [data-sf-cart-empty] > a { display: inline-flex; align-items: center; justify-content: center; min-height: 3rem; padding: 0 1.25rem; font-weight: 700; letter-spacing: .04em; text-transform: uppercase; text-decoration: none; color: var(--foreground); border: 1px solid color-mix(in srgb, var(--foreground) 35%, transparent); border-radius: var(--radius, .375rem); }
61
+ [data-sf-cart-actions] > a:first-child { min-height: 3.5rem; color: var(--primary-foreground); background: var(--primary); border-color: var(--primary); }
62
+ [data-sf-cart-actions] > a:first-child:hover { filter: brightness(1.08); }
63
+
64
+ @media (prefers-reduced-motion: no-preference) {
65
+ [data-sf-cart-drawer][open] { animation: sf-cart-in .28s cubic-bezier(.2,.8,.2,1); }
66
+ [data-sf-cart-drawer][open]::backdrop { animation: sf-cart-fade .28s ease-out; }
67
+ }
68
+ @keyframes sf-cart-in { from { transform: translateX(100%); } }
69
+ @keyframes sf-cart-fade { from { opacity: 0; } }
70
+ @media (prefers-reduced-motion: reduce) { [data-sf-cart-bar-fill] { transition: none; } }