@asteby/storefront-react 0.14.0 → 0.16.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.
Files changed (46) hide show
  1. package/README.md +78 -0
  2. package/dist/account-server.cjs +41 -8
  3. package/dist/account-server.cjs.map +1 -1
  4. package/dist/account-server.d.cts +2 -2
  5. package/dist/account-server.d.ts +2 -2
  6. package/dist/account-server.js +42 -9
  7. package/dist/account-server.js.map +1 -1
  8. package/dist/account.cjs +3 -1
  9. package/dist/account.cjs.map +1 -1
  10. package/dist/account.d.cts +2 -2
  11. package/dist/account.d.ts +2 -2
  12. package/dist/account.js +3 -1
  13. package/dist/account.js.map +1 -1
  14. package/dist/cart.cjs +266 -10
  15. package/dist/cart.cjs.map +1 -1
  16. package/dist/cart.css +14 -0
  17. package/dist/cart.d.cts +125 -2
  18. package/dist/cart.d.ts +125 -2
  19. package/dist/cart.js +260 -11
  20. package/dist/cart.js.map +1 -1
  21. package/dist/checkout.cjs +24 -3
  22. package/dist/checkout.cjs.map +1 -1
  23. package/dist/checkout.d.cts +7 -5
  24. package/dist/checkout.d.ts +7 -5
  25. package/dist/checkout.js +24 -3
  26. package/dist/checkout.js.map +1 -1
  27. package/dist/chunk-Q7UTJIKY.js +122 -0
  28. package/dist/chunk-Q7UTJIKY.js.map +1 -0
  29. package/dist/fiscal-server.cjs +288 -0
  30. package/dist/fiscal-server.cjs.map +1 -0
  31. package/dist/fiscal-server.d.cts +97 -0
  32. package/dist/fiscal-server.d.ts +97 -0
  33. package/dist/fiscal-server.js +241 -0
  34. package/dist/fiscal-server.js.map +1 -0
  35. package/dist/fiscal.cjs +1345 -0
  36. package/dist/fiscal.cjs.map +1 -0
  37. package/dist/fiscal.css +343 -0
  38. package/dist/fiscal.d.cts +313 -0
  39. package/dist/fiscal.d.ts +313 -0
  40. package/dist/fiscal.js +1106 -0
  41. package/dist/fiscal.js.map +1 -0
  42. package/dist/types-BnvSbV6t.d.cts +68 -0
  43. package/dist/types-BnvSbV6t.d.ts +68 -0
  44. package/dist/{types-HcEUd9hW.d.cts → types-D7pVK0GK.d.cts} +5 -0
  45. package/dist/{types-HcEUd9hW.d.ts → types-D7pVK0GK.d.ts} +5 -0
  46. package/package.json +23 -2
package/dist/cart.d.cts CHANGED
@@ -2,6 +2,19 @@ import * as react from 'react';
2
2
  import { ElementType, ReactNode } from 'react';
3
3
  import { Cart, CartLine, Money } from '@asteby/storefront';
4
4
 
5
+ /** Textos del estado optimista. Por omisión es-MX; la tienda puede traducirlos o cambiarlos. */
6
+ type CartDrawerOptimisticLabels = {
7
+ /** Estado discreto en la fila provisional. */
8
+ adding: string;
9
+ /** Anuncio `aria-live="polite"` al mostrar/confirmar la fila. */
10
+ added: string;
11
+ /** Junto al subtotal mientras OPS no confirma. */
12
+ calculating: string;
13
+ /** Aviso de error: «No pudimos agregar X: motivo». */
14
+ failed: (title: string, reason: string) => string;
15
+ dismiss: string;
16
+ };
17
+ declare const DEFAULT_OPTIMISTIC_LABELS: CartDrawerOptimisticLabels;
5
18
  type CartDrawerLabels = {
6
19
  /** Título; el cajón agrega " (N)" con la cantidad de unidades. */
7
20
  title: string;
@@ -56,7 +69,7 @@ type CartDrawerLinkProps = {
56
69
  children: ReactNode;
57
70
  onClick?: () => void;
58
71
  };
59
- type CartDrawerSlot = 'dialog' | 'panel' | 'header' | 'title' | 'close' | 'empty' | 'body' | 'shipping' | 'bar' | 'barFill' | 'lines' | 'line' | 'image' | 'lineTitle' | 'variant' | 'price' | 'qty' | 'qtyButton' | 'qtyValue' | 'remove' | 'upsell' | 'footer' | 'subtotal' | 'note' | 'actions' | 'checkout' | 'viewCart' | 'browse' | 'emptyTitle' | 'emptyHint' | 'shippingMessage' | 'lineMain' | 'lineHead' | 'lineText' | 'controls' | 'upsellTitle' | 'subtotalLabel' | 'subtotalValue';
72
+ type CartDrawerSlot = 'dialog' | 'panel' | 'header' | 'title' | 'close' | 'empty' | 'body' | 'shipping' | 'bar' | 'barFill' | 'lines' | 'line' | 'image' | 'lineTitle' | 'variant' | 'price' | 'qty' | 'qtyButton' | 'qtyValue' | 'remove' | 'upsell' | 'footer' | 'subtotal' | 'note' | 'actions' | 'checkout' | 'viewCart' | 'browse' | 'emptyTitle' | 'emptyHint' | 'shippingMessage' | 'lineMain' | 'notices' | 'lineHead' | 'lineText' | 'controls' | 'upsellTitle' | 'subtotalLabel' | 'subtotalValue';
60
73
  type CartDrawerIcons = {
61
74
  close?: ReactNode;
62
75
  decrease?: ReactNode;
@@ -91,6 +104,8 @@ type CartDrawerProps = {
91
104
  icons?: CartDrawerIcons;
92
105
  /** Clases por pieza (además de los `data-sf-cart-*`), para tiendas con Tailwind. */
93
106
  classNames?: Partial<Record<CartDrawerSlot, string>>;
107
+ /** Textos de las filas provisionales y de los avisos (por omisión es-MX). */
108
+ optimisticLabels?: Partial<CartDrawerOptimisticLabels>;
94
109
  locale?: string;
95
110
  /** Se llama al abrir/cerrar por cualquier vía. */
96
111
  onOpenChange?: (open: boolean) => void;
@@ -105,6 +120,114 @@ type CartDrawerProps = {
105
120
  */
106
121
  declare function CartDrawer(props: CartDrawerProps): react.JSX.Element | null;
107
122
 
123
+ /** Lo que la tienda sabe del producto al pulsar «Agregar». `price` es el precio unitario ya mostrado en la ficha. */
124
+ type OptimisticLineInput = {
125
+ /** Variante agregada; dos acciones con la misma variante comparten una sola fila. */
126
+ variantId: string;
127
+ title: string;
128
+ variantTitle?: string | null | undefined;
129
+ productHandle?: string | undefined;
130
+ image?: {
131
+ url: string;
132
+ alt?: string | null | undefined;
133
+ } | null | undefined;
134
+ /** Precio unitario mostrado (solo se pinta, no se multiplica ni se suma). */
135
+ price?: Money | null | undefined;
136
+ /** Unidades de ESTA acción (por omisión 1). */
137
+ quantity?: number | undefined;
138
+ /** Clave de idempotencia: repetir `add` con una clave viva devuelve la misma acción sin sumar otra vez. */
139
+ key?: string | undefined;
140
+ };
141
+ type OptimisticAction = Required<Pick<OptimisticLineInput, 'variantId' | 'title'>> & Omit<OptimisticLineInput, 'quantity' | 'key'> & {
142
+ id: string;
143
+ quantity: number;
144
+ key?: string | undefined;
145
+ };
146
+ /** Fila provisional: todas las acciones en vuelo de una misma variante. */
147
+ type OptimisticRow = {
148
+ variantId: string;
149
+ title: string;
150
+ variantTitle?: string | null | undefined;
151
+ productHandle?: string | undefined;
152
+ image?: {
153
+ url: string;
154
+ alt?: string | null | undefined;
155
+ } | null | undefined;
156
+ price?: Money | null | undefined;
157
+ /** Unidades agregadas que OPS aún no confirma. */
158
+ quantity: number;
159
+ actionIds: string[];
160
+ };
161
+ type OptimisticNotice = {
162
+ id: string;
163
+ title: string;
164
+ reason: string;
165
+ };
166
+ type OptimisticSnapshot = {
167
+ rows: readonly OptimisticRow[];
168
+ /** Unidades provisionales en total (para el contador). */
169
+ quantity: number;
170
+ notices: readonly OptimisticNotice[];
171
+ /** Último evento a anunciar en la región `aria-live="polite"`; `n` cambia en cada evento. */
172
+ announcement: {
173
+ kind: 'added' | null;
174
+ n: number;
175
+ };
176
+ };
177
+ type AddOutcome = {
178
+ ok: true;
179
+ } | {
180
+ ok: false;
181
+ reason: string;
182
+ };
183
+ declare const cartOptimistic: {
184
+ /**
185
+ * Inserta YA la fila provisional, sube el contador y abre el cajón sin robar el foco. Devuelve el id de la acción
186
+ * para `confirm` / `rollback`. Una acción por clic: la misma variante suma cantidad en una sola fila.
187
+ */
188
+ add(input: OptimisticLineInput, options?: {
189
+ open?: boolean;
190
+ timeoutMs?: number;
191
+ }): string;
192
+ /** OPS confirmó: la fila provisional sale y el carrito real (ya en `cart`) ocupa su lugar. */
193
+ confirm(id: string): void;
194
+ /** Falló: se quita la acción (y su cantidad) y se avisa «No pudimos agregar X: motivo». */
195
+ rollback(id: string, reason?: string): void;
196
+ /** Quita la acción sin avisar (el componente que la lanzó se desmontó). */
197
+ discard(id: string): void;
198
+ dismissNotice(id: string): void;
199
+ /** Limpia todo (pruebas, cierre de sesión). */
200
+ reset(): void;
201
+ getSnapshot: () => OptimisticSnapshot;
202
+ };
203
+ /** Estado optimista (filas, contador, avisos). En el servidor siempre vacío. */
204
+ declare function useOptimisticCart(): OptimisticSnapshot;
205
+ /** Contador para la cabecera: unidades del carrito de OPS más las que están en vuelo. */
206
+ declare function useOptimisticCartCount(base: number): number;
207
+ /** Motivo corto en es-MX a partir de un error de la acción (HTTP, código de OPS o red). */
208
+ declare function describeAddFailure(error: {
209
+ status?: number | undefined;
210
+ code?: string | undefined;
211
+ message?: string | undefined;
212
+ } | null | undefined): string;
213
+ type FetcherLike = {
214
+ state: string;
215
+ data?: unknown;
216
+ };
217
+ /** Interpreta `fetcher.data` de la acción de ruta: `{ error }`, `{ success:false }`, `{ ok:false }` o `status >= 400` fallan. */
218
+ declare function defaultAddOutcome(data: unknown): AddOutcome;
219
+ /**
220
+ * Conecta un `useFetcher()` de React Router con el estado optimista. `add(input, submit)` inserta la fila, abre el
221
+ * cajón y llama a `submit()` (el `fetcher.submit(...)` de la tienda); al terminar el fetcher confirma o revierte.
222
+ * Un nuevo `add` con el mismo fetcher antes de terminar da por entregado el anterior (el router lo reemplaza y el
223
+ * carrito real que llega con la revalidación manda). Si el componente se desmonta, las filas se descartan sin aviso.
224
+ */
225
+ declare function useOptimisticAdd(fetcher: FetcherLike, options?: {
226
+ settle?: (data: unknown) => AddOutcome;
227
+ }): {
228
+ add(input: OptimisticLineInput, submit: () => void): string;
229
+ };
230
+
108
231
  /** Abre el cajón del carrito desde cualquier parte. */
109
232
  declare function openCartDrawer(): void;
110
233
  /** Cierra el cajón del carrito. */
@@ -141,4 +264,4 @@ type ClickLike = {
141
264
  */
142
265
  declare function handleCartLinkClick(e: ClickLike): void;
143
266
 
144
- export { CartDrawer, type CartDrawerFormContract, type CartDrawerIcons, type CartDrawerLabels, type CartDrawerLineForm, type CartDrawerLinkProps, type CartDrawerPaths, type CartDrawerProps, type CartDrawerShipping, type CartDrawerSlot, closeCartDrawer, handleCartLinkClick, isCartDrawerOpen, openCartDrawer, toggleCartDrawer, useCartDrawer, useOpenCartDrawerWhenDone };
267
+ export { type AddOutcome, CartDrawer, type CartDrawerFormContract, type CartDrawerIcons, type CartDrawerLabels, type CartDrawerLineForm, type CartDrawerLinkProps, type CartDrawerOptimisticLabels, type CartDrawerPaths, type CartDrawerProps, type CartDrawerShipping, type CartDrawerSlot, DEFAULT_OPTIMISTIC_LABELS, type OptimisticAction, type OptimisticLineInput, type OptimisticNotice, type OptimisticRow, type OptimisticSnapshot, cartOptimistic, closeCartDrawer, defaultAddOutcome, describeAddFailure, handleCartLinkClick, isCartDrawerOpen, openCartDrawer, toggleCartDrawer, useCartDrawer, useOpenCartDrawerWhenDone, useOptimisticAdd, useOptimisticCart, useOptimisticCartCount };
package/dist/cart.d.ts CHANGED
@@ -2,6 +2,19 @@ import * as react from 'react';
2
2
  import { ElementType, ReactNode } from 'react';
3
3
  import { Cart, CartLine, Money } from '@asteby/storefront';
4
4
 
5
+ /** Textos del estado optimista. Por omisión es-MX; la tienda puede traducirlos o cambiarlos. */
6
+ type CartDrawerOptimisticLabels = {
7
+ /** Estado discreto en la fila provisional. */
8
+ adding: string;
9
+ /** Anuncio `aria-live="polite"` al mostrar/confirmar la fila. */
10
+ added: string;
11
+ /** Junto al subtotal mientras OPS no confirma. */
12
+ calculating: string;
13
+ /** Aviso de error: «No pudimos agregar X: motivo». */
14
+ failed: (title: string, reason: string) => string;
15
+ dismiss: string;
16
+ };
17
+ declare const DEFAULT_OPTIMISTIC_LABELS: CartDrawerOptimisticLabels;
5
18
  type CartDrawerLabels = {
6
19
  /** Título; el cajón agrega " (N)" con la cantidad de unidades. */
7
20
  title: string;
@@ -56,7 +69,7 @@ type CartDrawerLinkProps = {
56
69
  children: ReactNode;
57
70
  onClick?: () => void;
58
71
  };
59
- type CartDrawerSlot = 'dialog' | 'panel' | 'header' | 'title' | 'close' | 'empty' | 'body' | 'shipping' | 'bar' | 'barFill' | 'lines' | 'line' | 'image' | 'lineTitle' | 'variant' | 'price' | 'qty' | 'qtyButton' | 'qtyValue' | 'remove' | 'upsell' | 'footer' | 'subtotal' | 'note' | 'actions' | 'checkout' | 'viewCart' | 'browse' | 'emptyTitle' | 'emptyHint' | 'shippingMessage' | 'lineMain' | 'lineHead' | 'lineText' | 'controls' | 'upsellTitle' | 'subtotalLabel' | 'subtotalValue';
72
+ type CartDrawerSlot = 'dialog' | 'panel' | 'header' | 'title' | 'close' | 'empty' | 'body' | 'shipping' | 'bar' | 'barFill' | 'lines' | 'line' | 'image' | 'lineTitle' | 'variant' | 'price' | 'qty' | 'qtyButton' | 'qtyValue' | 'remove' | 'upsell' | 'footer' | 'subtotal' | 'note' | 'actions' | 'checkout' | 'viewCart' | 'browse' | 'emptyTitle' | 'emptyHint' | 'shippingMessage' | 'lineMain' | 'notices' | 'lineHead' | 'lineText' | 'controls' | 'upsellTitle' | 'subtotalLabel' | 'subtotalValue';
60
73
  type CartDrawerIcons = {
61
74
  close?: ReactNode;
62
75
  decrease?: ReactNode;
@@ -91,6 +104,8 @@ type CartDrawerProps = {
91
104
  icons?: CartDrawerIcons;
92
105
  /** Clases por pieza (además de los `data-sf-cart-*`), para tiendas con Tailwind. */
93
106
  classNames?: Partial<Record<CartDrawerSlot, string>>;
107
+ /** Textos de las filas provisionales y de los avisos (por omisión es-MX). */
108
+ optimisticLabels?: Partial<CartDrawerOptimisticLabels>;
94
109
  locale?: string;
95
110
  /** Se llama al abrir/cerrar por cualquier vía. */
96
111
  onOpenChange?: (open: boolean) => void;
@@ -105,6 +120,114 @@ type CartDrawerProps = {
105
120
  */
106
121
  declare function CartDrawer(props: CartDrawerProps): react.JSX.Element | null;
107
122
 
123
+ /** Lo que la tienda sabe del producto al pulsar «Agregar». `price` es el precio unitario ya mostrado en la ficha. */
124
+ type OptimisticLineInput = {
125
+ /** Variante agregada; dos acciones con la misma variante comparten una sola fila. */
126
+ variantId: string;
127
+ title: string;
128
+ variantTitle?: string | null | undefined;
129
+ productHandle?: string | undefined;
130
+ image?: {
131
+ url: string;
132
+ alt?: string | null | undefined;
133
+ } | null | undefined;
134
+ /** Precio unitario mostrado (solo se pinta, no se multiplica ni se suma). */
135
+ price?: Money | null | undefined;
136
+ /** Unidades de ESTA acción (por omisión 1). */
137
+ quantity?: number | undefined;
138
+ /** Clave de idempotencia: repetir `add` con una clave viva devuelve la misma acción sin sumar otra vez. */
139
+ key?: string | undefined;
140
+ };
141
+ type OptimisticAction = Required<Pick<OptimisticLineInput, 'variantId' | 'title'>> & Omit<OptimisticLineInput, 'quantity' | 'key'> & {
142
+ id: string;
143
+ quantity: number;
144
+ key?: string | undefined;
145
+ };
146
+ /** Fila provisional: todas las acciones en vuelo de una misma variante. */
147
+ type OptimisticRow = {
148
+ variantId: string;
149
+ title: string;
150
+ variantTitle?: string | null | undefined;
151
+ productHandle?: string | undefined;
152
+ image?: {
153
+ url: string;
154
+ alt?: string | null | undefined;
155
+ } | null | undefined;
156
+ price?: Money | null | undefined;
157
+ /** Unidades agregadas que OPS aún no confirma. */
158
+ quantity: number;
159
+ actionIds: string[];
160
+ };
161
+ type OptimisticNotice = {
162
+ id: string;
163
+ title: string;
164
+ reason: string;
165
+ };
166
+ type OptimisticSnapshot = {
167
+ rows: readonly OptimisticRow[];
168
+ /** Unidades provisionales en total (para el contador). */
169
+ quantity: number;
170
+ notices: readonly OptimisticNotice[];
171
+ /** Último evento a anunciar en la región `aria-live="polite"`; `n` cambia en cada evento. */
172
+ announcement: {
173
+ kind: 'added' | null;
174
+ n: number;
175
+ };
176
+ };
177
+ type AddOutcome = {
178
+ ok: true;
179
+ } | {
180
+ ok: false;
181
+ reason: string;
182
+ };
183
+ declare const cartOptimistic: {
184
+ /**
185
+ * Inserta YA la fila provisional, sube el contador y abre el cajón sin robar el foco. Devuelve el id de la acción
186
+ * para `confirm` / `rollback`. Una acción por clic: la misma variante suma cantidad en una sola fila.
187
+ */
188
+ add(input: OptimisticLineInput, options?: {
189
+ open?: boolean;
190
+ timeoutMs?: number;
191
+ }): string;
192
+ /** OPS confirmó: la fila provisional sale y el carrito real (ya en `cart`) ocupa su lugar. */
193
+ confirm(id: string): void;
194
+ /** Falló: se quita la acción (y su cantidad) y se avisa «No pudimos agregar X: motivo». */
195
+ rollback(id: string, reason?: string): void;
196
+ /** Quita la acción sin avisar (el componente que la lanzó se desmontó). */
197
+ discard(id: string): void;
198
+ dismissNotice(id: string): void;
199
+ /** Limpia todo (pruebas, cierre de sesión). */
200
+ reset(): void;
201
+ getSnapshot: () => OptimisticSnapshot;
202
+ };
203
+ /** Estado optimista (filas, contador, avisos). En el servidor siempre vacío. */
204
+ declare function useOptimisticCart(): OptimisticSnapshot;
205
+ /** Contador para la cabecera: unidades del carrito de OPS más las que están en vuelo. */
206
+ declare function useOptimisticCartCount(base: number): number;
207
+ /** Motivo corto en es-MX a partir de un error de la acción (HTTP, código de OPS o red). */
208
+ declare function describeAddFailure(error: {
209
+ status?: number | undefined;
210
+ code?: string | undefined;
211
+ message?: string | undefined;
212
+ } | null | undefined): string;
213
+ type FetcherLike = {
214
+ state: string;
215
+ data?: unknown;
216
+ };
217
+ /** Interpreta `fetcher.data` de la acción de ruta: `{ error }`, `{ success:false }`, `{ ok:false }` o `status >= 400` fallan. */
218
+ declare function defaultAddOutcome(data: unknown): AddOutcome;
219
+ /**
220
+ * Conecta un `useFetcher()` de React Router con el estado optimista. `add(input, submit)` inserta la fila, abre el
221
+ * cajón y llama a `submit()` (el `fetcher.submit(...)` de la tienda); al terminar el fetcher confirma o revierte.
222
+ * Un nuevo `add` con el mismo fetcher antes de terminar da por entregado el anterior (el router lo reemplaza y el
223
+ * carrito real que llega con la revalidación manda). Si el componente se desmonta, las filas se descartan sin aviso.
224
+ */
225
+ declare function useOptimisticAdd(fetcher: FetcherLike, options?: {
226
+ settle?: (data: unknown) => AddOutcome;
227
+ }): {
228
+ add(input: OptimisticLineInput, submit: () => void): string;
229
+ };
230
+
108
231
  /** Abre el cajón del carrito desde cualquier parte. */
109
232
  declare function openCartDrawer(): void;
110
233
  /** Cierra el cajón del carrito. */
@@ -141,4 +264,4 @@ type ClickLike = {
141
264
  */
142
265
  declare function handleCartLinkClick(e: ClickLike): void;
143
266
 
144
- export { CartDrawer, type CartDrawerFormContract, type CartDrawerIcons, type CartDrawerLabels, type CartDrawerLineForm, type CartDrawerLinkProps, type CartDrawerPaths, type CartDrawerProps, type CartDrawerShipping, type CartDrawerSlot, closeCartDrawer, handleCartLinkClick, isCartDrawerOpen, openCartDrawer, toggleCartDrawer, useCartDrawer, useOpenCartDrawerWhenDone };
267
+ export { type AddOutcome, CartDrawer, type CartDrawerFormContract, type CartDrawerIcons, type CartDrawerLabels, type CartDrawerLineForm, type CartDrawerLinkProps, type CartDrawerOptimisticLabels, type CartDrawerPaths, type CartDrawerProps, type CartDrawerShipping, type CartDrawerSlot, DEFAULT_OPTIMISTIC_LABELS, type OptimisticAction, type OptimisticLineInput, type OptimisticNotice, type OptimisticRow, type OptimisticSnapshot, cartOptimistic, closeCartDrawer, defaultAddOutcome, describeAddFailure, handleCartLinkClick, isCartDrawerOpen, openCartDrawer, toggleCartDrawer, useCartDrawer, useOpenCartDrawerWhenDone, useOptimisticAdd, useOptimisticCart, useOptimisticCartCount };
package/dist/cart.js CHANGED
@@ -7,9 +7,17 @@ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
7
7
 
8
8
  var isOpen = false;
9
9
  var listeners = /* @__PURE__ */ new Set();
10
+ var quietOpen = false;
11
+ function setQuietOpen(next) {
12
+ quietOpen = next;
13
+ }
14
+ function isQuietOpen() {
15
+ return quietOpen;
16
+ }
10
17
  function set(next) {
11
18
  if (isOpen === next) return;
12
19
  isOpen = next;
20
+ if (!next) quietOpen = false;
13
21
  for (const l of [...listeners]) l();
14
22
  }
15
23
  var subscribe = (l) => {
@@ -49,6 +57,191 @@ function handleCartLinkClick(e) {
49
57
  e.preventDefault();
50
58
  openCartDrawer();
51
59
  }
60
+ var DEFAULT_TIMEOUT_MS = 3e4;
61
+ var actions = [];
62
+ var notices = [];
63
+ var announcement = { kind: null, n: 0 };
64
+ var seq = 0;
65
+ var timers = /* @__PURE__ */ new Map();
66
+ var listeners2 = /* @__PURE__ */ new Set();
67
+ var snapshot = compute();
68
+ function compute() {
69
+ const byVariant = /* @__PURE__ */ new Map();
70
+ for (const a of actions) {
71
+ const row = byVariant.get(a.variantId);
72
+ if (row) {
73
+ row.quantity += a.quantity;
74
+ row.actionIds.push(a.id);
75
+ } else {
76
+ byVariant.set(a.variantId, {
77
+ variantId: a.variantId,
78
+ title: a.title,
79
+ variantTitle: a.variantTitle,
80
+ productHandle: a.productHandle,
81
+ image: a.image,
82
+ price: a.price,
83
+ quantity: a.quantity,
84
+ actionIds: [a.id]
85
+ });
86
+ }
87
+ }
88
+ const rows = [...byVariant.values()];
89
+ return { rows, quantity: rows.reduce((n, r) => n + r.quantity, 0), notices, announcement };
90
+ }
91
+ function emit() {
92
+ snapshot = compute();
93
+ for (const l of [...listeners2]) l();
94
+ }
95
+ var subscribe2 = (l) => {
96
+ listeners2.add(l);
97
+ return () => {
98
+ listeners2.delete(l);
99
+ };
100
+ };
101
+ var EMPTY = { rows: [], quantity: 0, notices: [], announcement: { kind: null, n: 0 } };
102
+ function drop(id) {
103
+ const a = actions.find((x) => x.id === id);
104
+ if (!a) return void 0;
105
+ actions = actions.filter((x) => x.id !== id);
106
+ const t = timers.get(id);
107
+ if (t) clearTimeout(t);
108
+ timers.delete(id);
109
+ return a;
110
+ }
111
+ function announce() {
112
+ announcement = { kind: "added", n: announcement.n + 1 };
113
+ }
114
+ var cartOptimistic = {
115
+ /**
116
+ * Inserta YA la fila provisional, sube el contador y abre el cajón sin robar el foco. Devuelve el id de la acción
117
+ * para `confirm` / `rollback`. Una acción por clic: la misma variante suma cantidad en una sola fila.
118
+ */
119
+ add(input, options = {}) {
120
+ if (input.key) {
121
+ const live = actions.find((a) => a.key === input.key);
122
+ if (live) return live.id;
123
+ }
124
+ const id = `opt-${++seq}`;
125
+ const quantity = Math.max(1, Math.floor(input.quantity ?? 1));
126
+ actions = [...actions, { ...input, id, quantity }];
127
+ const ms = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
128
+ if (ms > 0 && typeof setTimeout === "function") {
129
+ timers.set(id, setTimeout(() => cartOptimistic.rollback(id, describeAddFailure({ code: "timeout" })), ms));
130
+ }
131
+ announce();
132
+ if (options.open !== false) {
133
+ setQuietOpen(true);
134
+ openCartDrawer();
135
+ }
136
+ emit();
137
+ return id;
138
+ },
139
+ /** OPS confirmó: la fila provisional sale y el carrito real (ya en `cart`) ocupa su lugar. */
140
+ confirm(id) {
141
+ if (!drop(id)) return;
142
+ announce();
143
+ emit();
144
+ },
145
+ /** Falló: se quita la acción (y su cantidad) y se avisa «No pudimos agregar X: motivo». */
146
+ rollback(id, reason = "unknown") {
147
+ const a = drop(id);
148
+ if (!a) return;
149
+ notices = [...notices, { id: `n-${++seq}`, title: a.title, reason }];
150
+ emit();
151
+ },
152
+ /** Quita la acción sin avisar (el componente que la lanzó se desmontó). */
153
+ discard(id) {
154
+ if (drop(id)) emit();
155
+ },
156
+ dismissNotice(id) {
157
+ notices = notices.filter((n) => n.id !== id);
158
+ emit();
159
+ },
160
+ /** Limpia todo (pruebas, cierre de sesión). */
161
+ reset() {
162
+ for (const t of timers.values()) clearTimeout(t);
163
+ timers.clear();
164
+ actions = [];
165
+ notices = [];
166
+ announcement = { kind: null, n: 0 };
167
+ setQuietOpen(false);
168
+ emit();
169
+ },
170
+ getSnapshot: () => snapshot
171
+ };
172
+ function useOptimisticCart() {
173
+ return useSyncExternalStore(subscribe2, () => snapshot, () => EMPTY);
174
+ }
175
+ function useOptimisticCartCount(base) {
176
+ return base + useOptimisticCart().quantity;
177
+ }
178
+ function describeAddFailure(error) {
179
+ const code = error?.code;
180
+ const status = error?.status;
181
+ if (code === "store_not_ready") return "la tienda est\xE1 cerrada por ahora";
182
+ if (code === "timeout") return "tard\xF3 demasiado, intenta de nuevo";
183
+ if (status === 409 || code === "out_of_stock" || code === "insufficient_stock") return "sin existencias suficientes";
184
+ if (status === 0 || code === "network" || !status && !code) return "revisa tu conexi\xF3n";
185
+ if (status && status >= 500) return "hubo un problema, intenta de nuevo";
186
+ if (status === 404) return "el producto ya no est\xE1 disponible";
187
+ return error?.message?.trim() || "intenta de nuevo";
188
+ }
189
+ function defaultAddOutcome(data) {
190
+ if (!data || typeof data !== "object") return { ok: true };
191
+ const d = data;
192
+ const status = typeof d.status === "number" ? d.status : void 0;
193
+ const err = d.error && typeof d.error === "object" ? d.error : d;
194
+ const code = typeof err.code === "string" ? err.code : void 0;
195
+ const failed = d.error != null || d.ok === false || d.success === false || status !== void 0 && status >= 400;
196
+ if (!failed) return { ok: true };
197
+ 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 }) };
198
+ }
199
+ function useOptimisticAdd(fetcher, options = {}) {
200
+ const current = useRef(null);
201
+ const was = useRef(false);
202
+ const settleRef = useRef(options.settle);
203
+ settleRef.current = options.settle;
204
+ useEffect(() => {
205
+ const busy = fetcher.state !== "idle";
206
+ const id = current.current;
207
+ if (was.current && !busy && id) {
208
+ current.current = null;
209
+ const out = (settleRef.current ?? defaultAddOutcome)(fetcher.data);
210
+ if (out.ok) cartOptimistic.confirm(id);
211
+ else cartOptimistic.rollback(id, out.reason);
212
+ }
213
+ was.current = busy;
214
+ }, [fetcher.state, fetcher.data]);
215
+ useEffect(
216
+ () => () => {
217
+ if (current.current) cartOptimistic.discard(current.current);
218
+ current.current = null;
219
+ },
220
+ []
221
+ );
222
+ return {
223
+ add(input, submit) {
224
+ const prev = current.current;
225
+ const id = cartOptimistic.add(input);
226
+ if (prev && prev !== id) cartOptimistic.confirm(prev);
227
+ current.current = id;
228
+ try {
229
+ submit();
230
+ } catch (e) {
231
+ current.current = null;
232
+ cartOptimistic.rollback(id, describeAddFailure({ message: e instanceof Error ? e.message : void 0 }));
233
+ }
234
+ return id;
235
+ }
236
+ };
237
+ }
238
+ var DEFAULT_OPTIMISTIC_LABELS = {
239
+ adding: "Agregando\u2026",
240
+ added: "Agregado al carrito",
241
+ calculating: "calculando\u2026",
242
+ failed: (title, reason) => `No pudimos agregar ${title}: ${reason}`,
243
+ dismiss: "Descartar aviso"
244
+ };
52
245
  var DEFAULT_FORM = {
53
246
  intentField: "intent",
54
247
  lineField: "line_id",
@@ -73,6 +266,9 @@ function CartDrawer(props) {
73
266
  const { cart, labels, paths, shipping, notice, upsell, upsellTitle, icons, classNames: cx = {}, onOpenChange } = props;
74
267
  const locale = useResolvedLocale(props.locale);
75
268
  const { open } = useCartDrawer();
269
+ const opt = useOptimisticCart();
270
+ const ol = { ...DEFAULT_OPTIMISTIC_LABELS, ...props.optimisticLabels };
271
+ const quiet = open && isQuietOpen();
76
272
  const titleId = useId();
77
273
  const upsellId = useId();
78
274
  const { ref, onBackdropClick } = useModalDialog({ open, onClose: () => closeCartDrawer() });
@@ -80,12 +276,20 @@ function CartDrawer(props) {
80
276
  useEffect(() => {
81
277
  onOpenChange?.(open);
82
278
  }, [open]);
83
- if (!open) return null;
279
+ const realLines = cart?.lines.length ?? 0;
280
+ const rollbackEmpty = open && quiet && realLines === 0 && opt.rows.length === 0 && opt.notices.length > 0;
281
+ useEffect(() => {
282
+ if (rollbackEmpty) closeCartDrawer();
283
+ }, [rollbackEmpty]);
284
+ if (!open) return opt.notices.length ? /* @__PURE__ */ jsx(OptimisticNotices, { notices: opt.notices, labels: ol, classNames: cx }) : null;
84
285
  const money = props.renderMoney ?? ((m) => formatMoney(m, locale));
85
286
  const link = props.renderLink ?? (({ href, className, children, onClick }) => /* @__PURE__ */ jsx("a", { href, className, onClick, children }));
86
- const count = cart?.total_quantity ?? 0;
287
+ const pendingByVariant = new Map(opt.rows.map((r) => [r.variantId, r]));
288
+ const count = (cart?.total_quantity ?? 0) + opt.quantity;
87
289
  const lines = cart?.lines ?? [];
88
- const empty = !cart || lines.length === 0;
290
+ const extraRows = opt.rows.filter((r) => !lines.some((l) => l.merchandise.id === r.variantId));
291
+ const pending = opt.rows.length > 0;
292
+ const empty = lines.length === 0 && extraRows.length === 0;
89
293
  const close = () => closeCartDrawer();
90
294
  return /* @__PURE__ */ jsx(
91
295
  "dialog",
@@ -104,8 +308,10 @@ function CartDrawer(props) {
104
308
  labels.title,
105
309
  count > 0 ? ` (${count})` : ""
106
310
  ] }),
107
- /* @__PURE__ */ jsx("button", { type: "button", onClick: close, "aria-label": labels.close, className: cx.close, "data-sf-cart-close": "", "data-sf-autofocus": "", children: icons?.close ?? /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: "\u2715" }) })
311
+ /* @__PURE__ */ 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__ */ jsx("span", { "aria-hidden": "true", children: "\u2715" }) })
108
312
  ] }),
313
+ /* @__PURE__ */ jsx(OptimisticNotices, { notices: opt.notices, labels: ol, classNames: cx }),
314
+ /* @__PURE__ */ jsx(OptimisticStatus, { kind: opt.announcement.kind, n: opt.announcement.n, text: ol.added }),
109
315
  empty ? /* @__PURE__ */ jsxs("div", { className: cx.empty, "data-sf-cart-empty": "", children: [
110
316
  /* @__PURE__ */ jsx("p", { className: cx.emptyTitle, "data-sf-cart-empty-title": "", children: labels.empty }),
111
317
  labels.emptyHint ? /* @__PURE__ */ jsx("p", { className: cx.emptyHint, "data-sf-cart-empty-hint": "", children: labels.emptyHint }) : null,
@@ -134,16 +340,25 @@ function CartDrawer(props) {
134
340
  icons?.shippingReached,
135
341
  notice
136
342
  ] }) : null,
137
- /* @__PURE__ */ jsx("ul", { className: cx.lines, "data-sf-cart-lines": "", children: lines.map((line) => /* @__PURE__ */ jsx(DrawerLine, { line, props, money, link, close }, line.id)) }),
343
+ /* @__PURE__ */ jsxs("ul", { className: cx.lines, "data-sf-cart-lines": "", children: [
344
+ lines.map((line) => /* @__PURE__ */ jsx(DrawerLine, { line, props, money, link, close, pending: pendingByVariant.get(line.merchandise.id), pendingLabel: ol.adding }, line.id)),
345
+ extraRows.map((row) => /* @__PURE__ */ jsx(PendingLine, { row, props, money, label: ol.adding }, `pending-${row.variantId}`))
346
+ ] }),
138
347
  upsell ? /* @__PURE__ */ jsxs("section", { className: cx.upsell, "data-sf-cart-upsell": "", "aria-labelledby": upsellTitle ? upsellId : void 0, children: [
139
348
  upsellTitle ? /* @__PURE__ */ jsx("h3", { id: upsellId, className: cx.upsellTitle, children: upsellTitle }) : null,
140
349
  upsell
141
350
  ] }) : null
142
351
  ] }),
143
352
  /* @__PURE__ */ jsxs("div", { className: cx.footer, "data-sf-cart-footer": "", children: [
144
- /* @__PURE__ */ jsxs("div", { className: cx.subtotal, "data-sf-cart-subtotal": "", children: [
353
+ /* @__PURE__ */ jsxs("div", { className: cx.subtotal, "data-sf-cart-subtotal": "", "aria-busy": pending || void 0, children: [
145
354
  /* @__PURE__ */ jsx("span", { className: cx.subtotalLabel, children: labels.subtotal }),
146
- /* @__PURE__ */ jsx("span", { className: cx.subtotalValue, "data-sf-cart-subtotal-value": "", children: cart ? money(cart.cost.subtotal) : null })
355
+ /* @__PURE__ */ jsxs("span", { className: cx.subtotalValue, "data-sf-cart-subtotal-value": "", children: [
356
+ cart ? money(cart.cost.subtotal) : null,
357
+ pending ? /* @__PURE__ */ jsxs("span", { "data-sf-cart-calculating": "", children: [
358
+ cart ? " " : "",
359
+ ol.calculating
360
+ ] }) : null
361
+ ] })
147
362
  ] }),
148
363
  labels.note ? /* @__PURE__ */ jsx("p", { className: cx.note, "data-sf-cart-note": "", children: labels.note }) : null,
149
364
  /* @__PURE__ */ jsxs("div", { className: cx.actions, "data-sf-cart-actions": "", children: [
@@ -161,12 +376,14 @@ function DrawerLine({
161
376
  props,
162
377
  money,
163
378
  link,
164
- close
379
+ close,
380
+ pending,
381
+ pendingLabel
165
382
  }) {
166
383
  const { labels, paths, icons, classNames: cx = {} } = props;
167
384
  const f = { ...DEFAULT_FORM, ...props.form };
168
385
  const { Form, formData } = (props.useLineForm ?? useNativeForm)();
169
- const quantity = formData ? Number(formData.get(f.quantityField) ?? 0) : line.quantity;
386
+ const quantity = formData ? Number(formData.get(f.quantityField) ?? 0) : line.quantity + (pending?.quantity ?? 0);
170
387
  if (formData && (formData.get(f.intentField) === f.removeIntent || quantity === 0)) return null;
171
388
  const merch = line.merchandise;
172
389
  const isDefault = props.isDefaultVariantTitle ?? defaultIsDefaultVariant;
@@ -201,11 +418,43 @@ function DrawerLine({
201
418
  ] }),
202
419
  price
203
420
  ] }),
204
- controls
421
+ controls,
422
+ pending ? /* @__PURE__ */ jsx("p", { className: cx.variant, "data-sf-cart-pending-label": "", children: pendingLabel }) : null
423
+ ] })
424
+ ] });
425
+ }
426
+ function PendingLine({ row, props, money, label }) {
427
+ const { classNames: cx = {} } = props;
428
+ const isDefault = props.isDefaultVariantTitle ?? defaultIsDefaultVariant;
429
+ return /* @__PURE__ */ jsxs("li", { className: cx.line, "data-sf-cart-line": "", "data-sf-cart-line-pending": "", "aria-busy": "true", children: [
430
+ /* @__PURE__ */ jsx("span", { className: cx.image, "data-sf-cart-image": "", children: row.image ? /* @__PURE__ */ jsx("img", { src: row.image.url, alt: row.image.alt ?? "", width: 80, height: 80, decoding: "async" }) : null }),
431
+ /* @__PURE__ */ jsxs("div", { className: cx.lineMain, "data-sf-cart-line-main": "", children: [
432
+ /* @__PURE__ */ jsxs("div", { className: cx.lineHead, "data-sf-cart-line-head": "", children: [
433
+ /* @__PURE__ */ jsxs("div", { className: cx.lineText, "data-sf-cart-line-text": "", children: [
434
+ /* @__PURE__ */ jsx("span", { className: cx.lineTitle, "data-sf-cart-line-title": "", children: row.title }),
435
+ row.variantTitle && !isDefault(row.variantTitle) ? /* @__PURE__ */ jsx("p", { className: cx.variant, "data-sf-cart-variant": "", children: row.variantTitle }) : null
436
+ ] }),
437
+ row.price ? /* @__PURE__ */ jsx("span", { className: cx.price, "data-sf-cart-price": "", children: money(row.price) }) : null
438
+ ] }),
439
+ /* @__PURE__ */ jsxs("p", { className: cx.variant, "data-sf-cart-pending-label": "", children: [
440
+ label,
441
+ " \xB7 \xD7",
442
+ row.quantity
443
+ ] })
205
444
  ] })
206
445
  ] });
207
446
  }
447
+ function OptimisticStatus({ kind, n, text }) {
448
+ return /* @__PURE__ */ jsx("div", { role: "status", "aria-live": "polite", "data-sf-cart-status": "", style: VISUALLY_HIDDEN, children: kind === "added" ? `${text}${n % 2 ? "" : "\u200B"}` : "" });
449
+ }
450
+ 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 };
451
+ function OptimisticNotices({ notices: notices2, labels, classNames: cx }) {
452
+ return /* @__PURE__ */ jsx("div", { role: "alert", "aria-live": "assertive", "data-sf-cart-notices": "", className: cx.notices, children: notices2.map((n) => /* @__PURE__ */ jsxs("p", { "data-sf-cart-notice": "", children: [
453
+ /* @__PURE__ */ jsx("span", { children: labels.failed(n.title, n.reason) }),
454
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: () => cartOptimistic.dismissNotice(n.id), "aria-label": labels.dismiss, "data-sf-cart-notice-dismiss": "", children: /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: "\u2715" }) })
455
+ ] }, n.id)) });
456
+ }
208
457
 
209
- export { CartDrawer, closeCartDrawer, handleCartLinkClick, isCartDrawerOpen, openCartDrawer, toggleCartDrawer, useCartDrawer, useOpenCartDrawerWhenDone };
458
+ export { CartDrawer, DEFAULT_OPTIMISTIC_LABELS, cartOptimistic, closeCartDrawer, defaultAddOutcome, describeAddFailure, handleCartLinkClick, isCartDrawerOpen, openCartDrawer, toggleCartDrawer, useCartDrawer, useOpenCartDrawerWhenDone, useOptimisticAdd, useOptimisticCart, useOptimisticCartCount };
210
459
  //# sourceMappingURL=cart.js.map
211
460
  //# sourceMappingURL=cart.js.map