@asteby/storefront-react 0.15.0 → 0.16.1

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
@@ -624,6 +624,42 @@ useOpenCartDrawerWhenDone(fetchers.some(isAdd), { skip: silent })
624
624
  - Estilos: `cart.css` (CSS plano con los tokens de la tienda) y/o `classNames` por pieza (Tailwind). Cada pieza lleva `data-sf-cart-*`.
625
625
  - `renderLine`, `renderImage`, `icons`, `notice` y `isDefaultVariantTitle` cubren las variaciones de cada tienda.
626
626
 
627
+ ### Agregado optimista (`cartOptimistic`, `useOptimisticAdd`)
628
+
629
+ Al pulsar «Agregar» la fila aparece YA en el cajón, el contador sube y el cajón se abre (sin mover el foco al botón de cierre); cuando OPS responde, el carrito real la sustituye. Si la acción falla (409 sin existencias, `store_not_ready`, red, 5xx, tiempo agotado) la fila se quita, el contador regresa y aparece «No pudimos agregar X: motivo» (`role="alert"`, descartable); si el cajón queda vacío por el fallo, se cierra. **Solo es la fila**: precios, totales, envío e impuestos los dicta siempre OPS; el subtotal conserva el último valor de OPS con «calculando…» hasta confirmar. Sin JavaScript el formulario de la ruta sigue funcionando igual.
630
+
631
+ ```tsx
632
+ import { useFetcher } from 'react-router'
633
+ import { useOptimisticAdd, useOptimisticCartCount } from '@asteby/storefront-react/cart'
634
+
635
+ function AddToCart({ product, variant }) {
636
+ const fetcher = useFetcher()
637
+ const { add } = useOptimisticAdd(fetcher) // confirma o revierte cuando el fetcher termina
638
+ return (
639
+ <fetcher.Form method="post" action="/carrito"
640
+ onSubmit={(e) => {
641
+ e.preventDefault()
642
+ const form = e.currentTarget
643
+ add(
644
+ { variantId: variant.id, title: product.title, variantTitle: variant.title, image: product.featured_image, price: variant.price, quantity: 1 },
645
+ () => fetcher.submit(form),
646
+ )
647
+ }}>
648
+ <input type="hidden" name="intent" value="add" />
649
+ <input type="hidden" name="variant_id" value={variant.id} />
650
+ <button type="submit">Agregar</button>
651
+ </fetcher.Form>
652
+ )
653
+ }
654
+
655
+ const badge = useOptimisticCartCount(cart?.total_quantity ?? 0) // contador de la cabecera
656
+ ```
657
+
658
+ - La acción de ruta debe devolver el error en `fetcher.data` (`{ error: { code, status } }`, `{ ok: false }`, `{ success: false }` o `{ status >= 400 }`); para otro formato pasa `useOptimisticAdd(fetcher, { settle: (data) => ({ ok: true }) | ({ ok: false, reason }) })`. `describeAddFailure({ status, code })` da el motivo en es-MX.
659
+ - API sin React: `cartOptimistic.add(input, { open?, timeoutMs? })` devuelve el id de la acción; `confirm(id)`, `rollback(id, reason)`, `discard(id)` (sin aviso), `dismissNotice(id)`, `reset()`. Con `input.key` repetir la llamada no suma otra vez (doble clic); la misma variante comparte una fila y cada acción se confirma o revierte por separado, en cualquier orden.
660
+ - Un segundo `add` con el mismo fetcher antes de terminar da por entregado el anterior (el router lo reemplaza y manda el carrito real de la revalidación). Si el componente se desmonta, las filas en vuelo se descartan sin aviso.
661
+ - `optimisticLabels` en `CartDrawer` cambia los textos (`adding`, `added` «Agregado al carrito» en región `aria-live="polite"`, `calculating`, `failed(title, reason)`, `dismiss`). `cart.css` ya estiliza `data-sf-cart-line-pending`, `data-sf-cart-notice` y respeta `prefers-reduced-motion`.
662
+
627
663
  ## Compatibilidad de vehículos (`@asteby/storefront-react/fitment`)
628
664
 
629
665
  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`.
@@ -703,3 +739,45 @@ export default function Login() {
703
739
  - `createAuthAction({ storefront, customerCookie, safeNext?, defaultNext?, clientIp?, capabilities? })` -> `({ request }) => Promise<Response | AuthActionData>`. Un solo `POST` con campo `intent`: `login`, `register`, `verify`, `otp-request`, `otp-login`, `forgot`, `reset`. Éxito: `303` a `next` con `Set-Cookie` de la sesión. Si no, `AuthActionData` (`{ ok, step?, email?, error?: { code, field? } }`, sin textos ni contraseñas). `forgot` siempre avanza a `reset` (no revela qué correos tienen cuenta).
704
740
  - `<AccountAuth>`: `classNames` por pieza, atributos `data-sf-account-*`, `locale` (`es` por omisión, `en`), `messages` para cambiar cualquier texto, `form` para el `Form` del router. Con el mock (`createMockStorefront`) funciona entera: código `000000`.
705
741
  - Sin JavaScript funciona el inicio de sesión con contraseña y los pasos que devuelve la action; el cambio entre pantallas (enlaces) necesita JavaScript.
742
+
743
+ ## Facturación y constancia fiscal (`/fiscal`)
744
+
745
+ Subruta `@asteby/storefront-react/fiscal` (+ `/fiscal/server`, + `fiscal.css` opcional). Todo sale de `Region.fiscal_id` (etiquetas, formato, regímenes, usos, documentos); el núcleo no sabe de ningún país. Textos es-MX por omisión, `locale="en"` o `messages` para otros.
746
+
747
+ - `FiscalIdentityFields`: ID fiscal (mayúsculas, sin espacios, validado con el patrón de la región), razón social, CP fiscal, régimen (filtrado por tipo de persona si pasas `personTypeOf`, p. ej. `personTypeByLength({ individual: 13, company: 12 })`) y uso (filtrado por `allowed_regimes`). Controles `invoice.<campo>`.
748
+ - `IdentityDocumentReader`: «Sube tu constancia». Lee el PDF en el dispositivo (pdf.js se carga bajo demanda con `import()`, nunca en el bundle principal) y requiere `workerSrc`, un `pdf.worker.mjs` que sirve TU sitio (sin CDN). Muestra los campos prellenados con la marca «De tu constancia», avisos (QR, vigencia, régimen) y errores (no PDF, tamaño, escaneado, contraseña) con salida a captura manual. Nunca muestra ni conserva lo que la región lista en `discard_fields` (CURP, calle...). Entrega valores + evidencia (`buildDocumentEvidence`).
749
+ - `InvoiceToggle`: «Necesito factura» colapsado; perfil guardado (RFC enmascarado, Cambiar / Usar otros datos), lector o captura manual. `onChange` da `{ requested, invoice, ready, source, payLabel }` (`Pagar` / `Pagar y facturar`).
750
+ - `InvoiceStatusCard`: los estados de una `InvoiceSummary` (sin pedir, esperando pago, en proceso con `onRefresh` cada 10 s máx. 1 min, emitida, faltan datos, revisión, fuera de plazo, no disponible, cancelada).
751
+ - `/fiscal/server`: `loadFiscalContext(sf)` (región + `capabilities.invoicing`, caché corta, degrada a «sin facturación» sin lanzar) y `createInvoiceAction({ storefront, customerCookie })` con intents `set-invoice`, `save-profile`, `request-invoice`, `clear-profile` y `extract-document` (subida sin JS: se lee en memoria con el mismo motor, 5 MiB / 6 páginas / 3 s, sin disco ni logs; el archivo se descarta).
752
+
753
+ ```tsx
754
+ // app/routes/checkout.tsx (React Router)
755
+ import { InvoiceToggle } from '@asteby/storefront-react/fiscal'
756
+ import { createInvoiceAction, loadFiscalContext } from '@asteby/storefront-react/fiscal/server'
757
+ import '@asteby/storefront-react/fiscal.css'
758
+
759
+ export const loader = async () => ({ fiscal: await loadFiscalContext(storefront) })
760
+ export const action = createInvoiceAction({ storefront, customerCookie })
761
+
762
+ export default function Checkout() {
763
+ const { fiscal } = useLoaderData<typeof loader>()
764
+ const data = useActionData<typeof action>()
765
+ const [pay, setPay] = useState('Pagar')
766
+ return (
767
+ <Form method="post" encType="multipart/form-data">
768
+ {fiscal.region && (
769
+ <InvoiceToggle region={fiscal.region} capabilities={fiscal.invoicing} profile={customer?.fiscal} data={data}
770
+ canSave={!!customer} workerSrc="/assets/pdf.worker.mjs" serverUpload noJsHref="?factura=1"
771
+ personTypeOf={personTypeByLength({ individual: 13, company: 12 })}
772
+ onChange={(s) => setPay(s.payLabel)} />
773
+ )}
774
+ <button type="submit" name="intent" value="set-invoice">{pay}</button>
775
+ </Form>
776
+ )
777
+ }
778
+
779
+ // Cuenta: <IdentityDocumentReader region={region} workerSrc=... onConfirm={...} /> con intent save-profile (+ campo consent).
780
+ // Pedido: <InvoiceStatusCard invoice={order.invoice} available={fiscal.available} onRefresh={revalidate} />
781
+ ```
782
+
783
+ CSP: el lector necesita `worker-src 'self'` (o el origen que sirva el worker). Sin `workerSrc` el navegador no puede leer el PDF y el comprador pasa a captura manual.
package/dist/cart.cjs CHANGED
@@ -72,9 +72,17 @@ function useResolvedLocale(locale) {
72
72
  }
73
73
  var isOpen = false;
74
74
  var listeners = /* @__PURE__ */ new Set();
75
+ var quietOpen = false;
76
+ function setQuietOpen(next) {
77
+ quietOpen = next;
78
+ }
79
+ function isQuietOpen() {
80
+ return quietOpen;
81
+ }
75
82
  function set(next) {
76
83
  if (isOpen === next) return;
77
84
  isOpen = next;
85
+ if (!next) quietOpen = false;
78
86
  for (const l of [...listeners]) l();
79
87
  }
80
88
  var subscribe = (l) => {
@@ -114,6 +122,191 @@ function handleCartLinkClick(e) {
114
122
  e.preventDefault();
115
123
  openCartDrawer();
116
124
  }
125
+ var DEFAULT_TIMEOUT_MS = 3e4;
126
+ var actions = [];
127
+ var notices = [];
128
+ var announcement = { kind: null, n: 0 };
129
+ var seq = 0;
130
+ var timers = /* @__PURE__ */ new Map();
131
+ var listeners2 = /* @__PURE__ */ new Set();
132
+ var snapshot = compute();
133
+ function compute() {
134
+ const byVariant = /* @__PURE__ */ new Map();
135
+ for (const a of actions) {
136
+ const row = byVariant.get(a.variantId);
137
+ if (row) {
138
+ row.quantity += a.quantity;
139
+ row.actionIds.push(a.id);
140
+ } else {
141
+ byVariant.set(a.variantId, {
142
+ variantId: a.variantId,
143
+ title: a.title,
144
+ variantTitle: a.variantTitle,
145
+ productHandle: a.productHandle,
146
+ image: a.image,
147
+ price: a.price,
148
+ quantity: a.quantity,
149
+ actionIds: [a.id]
150
+ });
151
+ }
152
+ }
153
+ const rows = [...byVariant.values()];
154
+ return { rows, quantity: rows.reduce((n, r) => n + r.quantity, 0), notices, announcement };
155
+ }
156
+ function emit() {
157
+ snapshot = compute();
158
+ for (const l of [...listeners2]) l();
159
+ }
160
+ var subscribe2 = (l) => {
161
+ listeners2.add(l);
162
+ return () => {
163
+ listeners2.delete(l);
164
+ };
165
+ };
166
+ var EMPTY = { rows: [], quantity: 0, notices: [], announcement: { kind: null, n: 0 } };
167
+ function drop(id) {
168
+ const a = actions.find((x) => x.id === id);
169
+ if (!a) return void 0;
170
+ actions = actions.filter((x) => x.id !== id);
171
+ const t = timers.get(id);
172
+ if (t) clearTimeout(t);
173
+ timers.delete(id);
174
+ return a;
175
+ }
176
+ function announce() {
177
+ announcement = { kind: "added", n: announcement.n + 1 };
178
+ }
179
+ var cartOptimistic = {
180
+ /**
181
+ * Inserta YA la fila provisional, sube el contador y abre el cajón sin robar el foco. Devuelve el id de la acción
182
+ * para `confirm` / `rollback`. Una acción por clic: la misma variante suma cantidad en una sola fila.
183
+ */
184
+ add(input, options = {}) {
185
+ if (input.key) {
186
+ const live = actions.find((a) => a.key === input.key);
187
+ if (live) return live.id;
188
+ }
189
+ const id = `opt-${++seq}`;
190
+ const quantity = Math.max(1, Math.floor(input.quantity ?? 1));
191
+ actions = [...actions, { ...input, id, quantity }];
192
+ const ms = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
193
+ if (ms > 0 && typeof setTimeout === "function") {
194
+ timers.set(id, setTimeout(() => cartOptimistic.rollback(id, describeAddFailure({ code: "timeout" })), ms));
195
+ }
196
+ announce();
197
+ if (options.open !== false) {
198
+ setQuietOpen(true);
199
+ openCartDrawer();
200
+ }
201
+ emit();
202
+ return id;
203
+ },
204
+ /** OPS confirmó: la fila provisional sale y el carrito real (ya en `cart`) ocupa su lugar. */
205
+ confirm(id) {
206
+ if (!drop(id)) return;
207
+ announce();
208
+ emit();
209
+ },
210
+ /** Falló: se quita la acción (y su cantidad) y se avisa «No pudimos agregar X: motivo». */
211
+ rollback(id, reason = "unknown") {
212
+ const a = drop(id);
213
+ if (!a) return;
214
+ notices = [...notices, { id: `n-${++seq}`, title: a.title, reason }];
215
+ emit();
216
+ },
217
+ /** Quita la acción sin avisar (el componente que la lanzó se desmontó). */
218
+ discard(id) {
219
+ if (drop(id)) emit();
220
+ },
221
+ dismissNotice(id) {
222
+ notices = notices.filter((n) => n.id !== id);
223
+ emit();
224
+ },
225
+ /** Limpia todo (pruebas, cierre de sesión). */
226
+ reset() {
227
+ for (const t of timers.values()) clearTimeout(t);
228
+ timers.clear();
229
+ actions = [];
230
+ notices = [];
231
+ announcement = { kind: null, n: 0 };
232
+ setQuietOpen(false);
233
+ emit();
234
+ },
235
+ getSnapshot: () => snapshot
236
+ };
237
+ function useOptimisticCart() {
238
+ return react.useSyncExternalStore(subscribe2, () => snapshot, () => EMPTY);
239
+ }
240
+ function useOptimisticCartCount(base) {
241
+ return base + useOptimisticCart().quantity;
242
+ }
243
+ function describeAddFailure(error) {
244
+ const code = error?.code;
245
+ const status = error?.status;
246
+ if (code === "store_not_ready") return "la tienda est\xE1 cerrada por ahora";
247
+ if (code === "timeout") return "tard\xF3 demasiado, intenta de nuevo";
248
+ if (status === 409 || code === "out_of_stock" || code === "insufficient_stock") return "sin existencias suficientes";
249
+ if (status === 0 || code === "network" || !status && !code) return "revisa tu conexi\xF3n";
250
+ if (status && status >= 500) return "hubo un problema, intenta de nuevo";
251
+ if (status === 404) return "el producto ya no est\xE1 disponible";
252
+ return error?.message?.trim() || "intenta de nuevo";
253
+ }
254
+ function defaultAddOutcome(data) {
255
+ if (!data || typeof data !== "object") return { ok: true };
256
+ const d = data;
257
+ const status = typeof d.status === "number" ? d.status : void 0;
258
+ const err = d.error && typeof d.error === "object" ? d.error : d;
259
+ const code = typeof err.code === "string" ? err.code : void 0;
260
+ const failed = d.error != null || d.ok === false || d.success === false || status !== void 0 && status >= 400;
261
+ if (!failed) return { ok: true };
262
+ return { ok: false, reason: describeAddFailure({ status: status ?? (typeof err.status === "number" ? err.status : void 0), code, message: typeof err.message === "string" ? err.message : void 0 }) };
263
+ }
264
+ function useOptimisticAdd(fetcher, options = {}) {
265
+ const current = react.useRef(null);
266
+ const was = react.useRef(false);
267
+ const settleRef = react.useRef(options.settle);
268
+ settleRef.current = options.settle;
269
+ react.useEffect(() => {
270
+ const busy = fetcher.state !== "idle";
271
+ const id = current.current;
272
+ if (was.current && !busy && id) {
273
+ current.current = null;
274
+ const out = (settleRef.current ?? defaultAddOutcome)(fetcher.data);
275
+ if (out.ok) cartOptimistic.confirm(id);
276
+ else cartOptimistic.rollback(id, out.reason);
277
+ }
278
+ was.current = busy;
279
+ }, [fetcher.state, fetcher.data]);
280
+ react.useEffect(
281
+ () => () => {
282
+ if (current.current) cartOptimistic.discard(current.current);
283
+ current.current = null;
284
+ },
285
+ []
286
+ );
287
+ return {
288
+ add(input, submit) {
289
+ const prev = current.current;
290
+ const id = cartOptimistic.add(input);
291
+ if (prev && prev !== id) cartOptimistic.confirm(prev);
292
+ current.current = id;
293
+ try {
294
+ submit();
295
+ } catch (e) {
296
+ current.current = null;
297
+ cartOptimistic.rollback(id, describeAddFailure({ message: e instanceof Error ? e.message : void 0 }));
298
+ }
299
+ return id;
300
+ }
301
+ };
302
+ }
303
+ var DEFAULT_OPTIMISTIC_LABELS = {
304
+ adding: "Agregando\u2026",
305
+ added: "Agregado al carrito",
306
+ calculating: "calculando\u2026",
307
+ failed: (title, reason) => `No pudimos agregar ${title}: ${reason}`,
308
+ dismiss: "Descartar aviso"
309
+ };
117
310
  var DEFAULT_FORM = {
118
311
  intentField: "intent",
119
312
  lineField: "line_id",
@@ -138,6 +331,9 @@ function CartDrawer(props) {
138
331
  const { cart, labels, paths, shipping, notice, upsell, upsellTitle, icons, classNames: cx = {}, onOpenChange } = props;
139
332
  const locale = useResolvedLocale(props.locale);
140
333
  const { open } = useCartDrawer();
334
+ const opt = useOptimisticCart();
335
+ const ol = { ...DEFAULT_OPTIMISTIC_LABELS, ...props.optimisticLabels };
336
+ const quiet = open && isQuietOpen();
141
337
  const titleId = react.useId();
142
338
  const upsellId = react.useId();
143
339
  const { ref, onBackdropClick } = useModalDialog({ open, onClose: () => closeCartDrawer() });
@@ -145,12 +341,20 @@ function CartDrawer(props) {
145
341
  react.useEffect(() => {
146
342
  onOpenChange?.(open);
147
343
  }, [open]);
148
- if (!open) return null;
344
+ const realLines = cart?.lines.length ?? 0;
345
+ const rollbackEmpty = open && quiet && realLines === 0 && opt.rows.length === 0 && opt.notices.length > 0;
346
+ react.useEffect(() => {
347
+ if (rollbackEmpty) closeCartDrawer();
348
+ }, [rollbackEmpty]);
349
+ if (!open) return opt.notices.length ? /* @__PURE__ */ jsxRuntime.jsx(OptimisticNotices, { notices: opt.notices, labels: ol, classNames: cx }) : null;
149
350
  const money = props.renderMoney ?? ((m) => storefront.formatMoney(m, locale));
150
351
  const link = props.renderLink ?? (({ href, className, children, onClick }) => /* @__PURE__ */ jsxRuntime.jsx("a", { href, className, onClick, children }));
151
- const count = cart?.total_quantity ?? 0;
352
+ const pendingByVariant = new Map(opt.rows.map((r) => [r.variantId, r]));
353
+ const count = (cart?.total_quantity ?? 0) + opt.quantity;
152
354
  const lines = cart?.lines ?? [];
153
- const empty = !cart || lines.length === 0;
355
+ const extraRows = opt.rows.filter((r) => !lines.some((l) => l.merchandise.id === r.variantId));
356
+ const pending = opt.rows.length > 0;
357
+ const empty = lines.length === 0 && extraRows.length === 0;
154
358
  const close = () => closeCartDrawer();
155
359
  return /* @__PURE__ */ jsxRuntime.jsx(
156
360
  "dialog",
@@ -169,8 +373,10 @@ function CartDrawer(props) {
169
373
  labels.title,
170
374
  count > 0 ? ` (${count})` : ""
171
375
  ] }),
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" }) })
376
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", onClick: close, "aria-label": labels.close, className: cx.close, "data-sf-cart-close": "", "data-sf-autofocus": quiet ? void 0 : "", children: icons?.close ?? /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": "true", children: "\u2715" }) })
173
377
  ] }),
378
+ /* @__PURE__ */ jsxRuntime.jsx(OptimisticNotices, { notices: opt.notices, labels: ol, classNames: cx }),
379
+ /* @__PURE__ */ jsxRuntime.jsx(OptimisticStatus, { kind: opt.announcement.kind, n: opt.announcement.n, text: ol.added }),
174
380
  empty ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cx.empty, "data-sf-cart-empty": "", children: [
175
381
  /* @__PURE__ */ jsxRuntime.jsx("p", { className: cx.emptyTitle, "data-sf-cart-empty-title": "", children: labels.empty }),
176
382
  labels.emptyHint ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: cx.emptyHint, "data-sf-cart-empty-hint": "", children: labels.emptyHint }) : null,
@@ -199,16 +405,25 @@ function CartDrawer(props) {
199
405
  icons?.shippingReached,
200
406
  notice
201
407
  ] }) : 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)) }),
408
+ /* @__PURE__ */ jsxRuntime.jsxs("ul", { className: cx.lines, "data-sf-cart-lines": "", children: [
409
+ lines.map((line) => /* @__PURE__ */ jsxRuntime.jsx(DrawerLine, { line, props, money, link, close, pending: pendingByVariant.get(line.merchandise.id), pendingLabel: ol.adding }, line.id)),
410
+ extraRows.map((row) => /* @__PURE__ */ jsxRuntime.jsx(PendingLine, { row, props, money, label: ol.adding }, `pending-${row.variantId}`))
411
+ ] }),
203
412
  upsell ? /* @__PURE__ */ jsxRuntime.jsxs("section", { className: cx.upsell, "data-sf-cart-upsell": "", "aria-labelledby": upsellTitle ? upsellId : void 0, children: [
204
413
  upsellTitle ? /* @__PURE__ */ jsxRuntime.jsx("h3", { id: upsellId, className: cx.upsellTitle, children: upsellTitle }) : null,
205
414
  upsell
206
415
  ] }) : null
207
416
  ] }),
208
417
  /* @__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: [
418
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cx.subtotal, "data-sf-cart-subtotal": "", "aria-busy": pending || void 0, children: [
210
419
  /* @__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 })
420
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: cx.subtotalValue, "data-sf-cart-subtotal-value": "", children: [
421
+ cart ? money(cart.cost.subtotal) : null,
422
+ pending ? /* @__PURE__ */ jsxRuntime.jsxs("span", { "data-sf-cart-calculating": "", children: [
423
+ cart ? " " : "",
424
+ ol.calculating
425
+ ] }) : null
426
+ ] })
212
427
  ] }),
213
428
  labels.note ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: cx.note, "data-sf-cart-note": "", children: labels.note }) : null,
214
429
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cx.actions, "data-sf-cart-actions": "", children: [
@@ -226,12 +441,14 @@ function DrawerLine({
226
441
  props,
227
442
  money,
228
443
  link,
229
- close
444
+ close,
445
+ pending,
446
+ pendingLabel
230
447
  }) {
231
448
  const { labels, paths, icons, classNames: cx = {} } = props;
232
449
  const f = { ...DEFAULT_FORM, ...props.form };
233
450
  const { Form, formData } = (props.useLineForm ?? useNativeForm)();
234
- const quantity = formData ? Number(formData.get(f.quantityField) ?? 0) : line.quantity;
451
+ const quantity = formData ? Number(formData.get(f.quantityField) ?? 0) : line.quantity + (pending?.quantity ?? 0);
235
452
  if (formData && (formData.get(f.intentField) === f.removeIntent || quantity === 0)) return null;
236
453
  const merch = line.merchandise;
237
454
  const isDefault = props.isDefaultVariantTitle ?? defaultIsDefaultVariant;
@@ -266,18 +483,57 @@ function DrawerLine({
266
483
  ] }),
267
484
  price
268
485
  ] }),
269
- controls
486
+ controls,
487
+ pending ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: cx.variant, "data-sf-cart-pending-label": "", children: pendingLabel }) : null
270
488
  ] })
271
489
  ] });
272
490
  }
491
+ function PendingLine({ row, props, money, label }) {
492
+ const { classNames: cx = {} } = props;
493
+ const isDefault = props.isDefaultVariantTitle ?? defaultIsDefaultVariant;
494
+ return /* @__PURE__ */ jsxRuntime.jsxs("li", { className: cx.line, "data-sf-cart-line": "", "data-sf-cart-line-pending": "", "aria-busy": "true", children: [
495
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: cx.image, "data-sf-cart-image": "", children: row.image ? /* @__PURE__ */ jsxRuntime.jsx("img", { src: row.image.url, alt: row.image.alt ?? "", width: 80, height: 80, decoding: "async" }) : null }),
496
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cx.lineMain, "data-sf-cart-line-main": "", children: [
497
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cx.lineHead, "data-sf-cart-line-head": "", children: [
498
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cx.lineText, "data-sf-cart-line-text": "", children: [
499
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: cx.lineTitle, "data-sf-cart-line-title": "", children: row.title }),
500
+ row.variantTitle && !isDefault(row.variantTitle) ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: cx.variant, "data-sf-cart-variant": "", children: row.variantTitle }) : null
501
+ ] }),
502
+ row.price ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: cx.price, "data-sf-cart-price": "", children: money(row.price) }) : null
503
+ ] }),
504
+ /* @__PURE__ */ jsxRuntime.jsxs("p", { className: cx.variant, "data-sf-cart-pending-label": "", children: [
505
+ label,
506
+ " \xB7 \xD7",
507
+ row.quantity
508
+ ] })
509
+ ] })
510
+ ] });
511
+ }
512
+ function OptimisticStatus({ kind, n, text }) {
513
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { role: "status", "aria-live": "polite", "data-sf-cart-status": "", style: VISUALLY_HIDDEN, children: kind === "added" ? `${text}${n % 2 ? "" : "\u200B"}` : "" });
514
+ }
515
+ var VISUALLY_HIDDEN = { position: "absolute", width: 1, height: 1, margin: -1, padding: 0, overflow: "hidden", clip: "rect(0 0 0 0)", whiteSpace: "nowrap", border: 0 };
516
+ function OptimisticNotices({ notices: notices2, labels, classNames: cx }) {
517
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { role: "alert", "aria-live": "assertive", "data-sf-cart-notices": "", className: cx.notices, children: notices2.map((n) => /* @__PURE__ */ jsxRuntime.jsxs("p", { "data-sf-cart-notice": "", children: [
518
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: labels.failed(n.title, n.reason) }),
519
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", onClick: () => cartOptimistic.dismissNotice(n.id), "aria-label": labels.dismiss, "data-sf-cart-notice-dismiss": "", children: /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": "true", children: "\u2715" }) })
520
+ ] }, n.id)) });
521
+ }
273
522
 
274
523
  exports.CartDrawer = CartDrawer;
524
+ exports.DEFAULT_OPTIMISTIC_LABELS = DEFAULT_OPTIMISTIC_LABELS;
525
+ exports.cartOptimistic = cartOptimistic;
275
526
  exports.closeCartDrawer = closeCartDrawer;
527
+ exports.defaultAddOutcome = defaultAddOutcome;
528
+ exports.describeAddFailure = describeAddFailure;
276
529
  exports.handleCartLinkClick = handleCartLinkClick;
277
530
  exports.isCartDrawerOpen = isCartDrawerOpen;
278
531
  exports.openCartDrawer = openCartDrawer;
279
532
  exports.toggleCartDrawer = toggleCartDrawer;
280
533
  exports.useCartDrawer = useCartDrawer;
281
534
  exports.useOpenCartDrawerWhenDone = useOpenCartDrawerWhenDone;
535
+ exports.useOptimisticAdd = useOptimisticAdd;
536
+ exports.useOptimisticCart = useOptimisticCart;
537
+ exports.useOptimisticCartCount = useOptimisticCartCount;
282
538
  //# sourceMappingURL=cart.cjs.map
283
539
  //# sourceMappingURL=cart.cjs.map