@fayz-ai/storefront 0.7.0 → 0.8.2

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 (48) hide show
  1. package/README.md +3 -1
  2. package/dist/auth.d.ts.map +1 -1
  3. package/dist/components/CartDrawer.d.ts.map +1 -1
  4. package/dist/components/DeliveryEstimator.d.ts +17 -0
  5. package/dist/components/DeliveryEstimator.d.ts.map +1 -0
  6. package/dist/components/ProductGallery.d.ts +22 -0
  7. package/dist/components/ProductGallery.d.ts.map +1 -0
  8. package/dist/components/StorefrontHeader.d.ts.map +1 -1
  9. package/dist/config.d.ts +8 -0
  10. package/dist/config.d.ts.map +1 -1
  11. package/dist/createStorefrontApp.d.ts.map +1 -1
  12. package/dist/index.cjs +886 -370
  13. package/dist/index.cjs.map +1 -1
  14. package/dist/index.d.ts +1 -1
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +790 -275
  17. package/dist/index.js.map +1 -1
  18. package/dist/pages/CheckoutPage.d.ts.map +1 -1
  19. package/dist/pages/ProductDetailPage.d.ts.map +1 -1
  20. package/dist/presets.d.ts +5 -0
  21. package/dist/presets.d.ts.map +1 -1
  22. package/dist/stores/cart.store.d.ts +21 -0
  23. package/dist/stores/cart.store.d.ts.map +1 -1
  24. package/dist/stores/delivery.store.d.ts +64 -0
  25. package/dist/stores/delivery.store.d.ts.map +1 -0
  26. package/dist/testids.d.ts +12 -0
  27. package/dist/testids.d.ts.map +1 -1
  28. package/dist/theme.d.ts +18 -2
  29. package/dist/theme.d.ts.map +1 -1
  30. package/dist/workflows/checkout.d.ts +5 -2
  31. package/dist/workflows/checkout.d.ts.map +1 -1
  32. package/package.json +13 -7
  33. package/src/auth.ts +57 -1
  34. package/src/components/CartDrawer.tsx +23 -1
  35. package/src/components/DeliveryEstimator.tsx +157 -0
  36. package/src/components/ProductGallery.tsx +174 -0
  37. package/src/components/StorefrontHeader.tsx +35 -8
  38. package/src/config.ts +15 -3
  39. package/src/createStorefrontApp.tsx +18 -1
  40. package/src/index.ts +1 -0
  41. package/src/pages/CheckoutPage.tsx +240 -134
  42. package/src/pages/ProductDetailPage.tsx +17 -13
  43. package/src/presets.ts +66 -0
  44. package/src/stores/cart.store.ts +37 -1
  45. package/src/stores/delivery.store.ts +152 -0
  46. package/src/testids.ts +13 -0
  47. package/src/theme.ts +33 -3
  48. package/src/workflows/checkout.ts +34 -6
package/dist/index.cjs CHANGED
@@ -3,13 +3,14 @@
3
3
  var runtime = require('@fayz-ai/shop/runtime');
4
4
  var mock = require('@fayz-ai/shop/mock');
5
5
  var pluginAuth = require('@fayz-ai/plugin-auth');
6
+ var shop = require('@fayz-ai/sdk/shop');
6
7
  var zustand = require('zustand');
7
8
  var middleware = require('zustand/middleware');
8
- var React6 = require('react');
9
+ var React7 = require('react');
9
10
  var core = require('@fayz-ai/core');
10
11
  var LucideIcons = require('lucide-react');
11
12
  var jsxRuntime = require('react/jsx-runtime');
12
- var shop = require('@fayz-ai/shop');
13
+ var shop$1 = require('@fayz-ai/shop');
13
14
 
14
15
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
15
16
 
@@ -31,7 +32,7 @@ function _interopNamespace(e) {
31
32
  return Object.freeze(n);
32
33
  }
33
34
 
34
- var React6__default = /*#__PURE__*/_interopDefault(React6);
35
+ var React7__default = /*#__PURE__*/_interopDefault(React7);
35
36
  var LucideIcons__namespace = /*#__PURE__*/_interopNamespace(LucideIcons);
36
37
 
37
38
  // src/createStorefrontApp.tsx
@@ -76,10 +77,23 @@ function resolveAuthAdapter(configured, fallback) {
76
77
  }
77
78
  }).adapter;
78
79
  }
80
+ var _accessToken = null;
81
+ var _hadAuthUser = false;
82
+ async function syncAccessToken(adapter2) {
83
+ try {
84
+ const existing = await adapter2.getSession();
85
+ _accessToken = existing?.session.accessToken ?? null;
86
+ } catch {
87
+ _accessToken = null;
88
+ }
89
+ }
79
90
  function initCustomerAuth(adapter2) {
80
91
  _adapter = adapter2;
92
+ shop.setShopAccessTokenResolver(() => _accessToken);
81
93
  adapter2.getSession().then((existing) => {
94
+ _accessToken = existing?.session.accessToken ?? null;
82
95
  if (!existing) return;
96
+ _hadAuthUser = true;
83
97
  const store = useSessionStore.getState();
84
98
  if (store.email !== existing.user.email) {
85
99
  void linkCustomer(existing.user);
@@ -87,7 +101,16 @@ function initCustomerAuth(adapter2) {
87
101
  }).catch(() => {
88
102
  });
89
103
  adapter2.onAuthStateChange((user) => {
90
- if (!user) useSessionStore.getState().signOut();
104
+ if (!user) {
105
+ _accessToken = null;
106
+ if (_hadAuthUser) {
107
+ _hadAuthUser = false;
108
+ useSessionStore.getState().signOut();
109
+ }
110
+ return;
111
+ }
112
+ _hadAuthUser = true;
113
+ void syncAccessToken(adapter2);
91
114
  });
92
115
  }
93
116
  function getCustomerAuthAdapter() {
@@ -123,6 +146,8 @@ async function establishCustomerSession(email, opts) {
123
146
  if (opts?.password) {
124
147
  const adapter2 = getCustomerAuthAdapter();
125
148
  const session = await adapter2.signIn(normalized, opts.password);
149
+ _accessToken = session.accessToken ?? null;
150
+ _hadAuthUser = true;
126
151
  return linkCustomer(session.user.email ? session.user : { ...session.user, email: normalized }, opts.name);
127
152
  }
128
153
  return linkCustomer({ email: normalized }, opts?.name);
@@ -134,12 +159,16 @@ async function signUpCustomer(email, password, name) {
134
159
  const adapter2 = getCustomerAuthAdapter();
135
160
  const normalized = email.trim().toLowerCase();
136
161
  const session = await adapter2.signUp(normalized, password, name);
162
+ _accessToken = session.accessToken ?? null;
163
+ _hadAuthUser = true;
137
164
  return linkCustomer(session.user.email ? session.user : { ...session.user, email: normalized }, name);
138
165
  }
139
166
  async function signOutCustomer() {
140
167
  try {
141
168
  await getCustomerAuthAdapter().signOut();
142
169
  } finally {
170
+ _accessToken = null;
171
+ _hadAuthUser = false;
143
172
  useSessionStore.getState().signOut();
144
173
  }
145
174
  }
@@ -152,7 +181,10 @@ function resolveConfig(config) {
152
181
  currency: config.currency ?? "BRL",
153
182
  locale: config.locale ?? "pt-BR",
154
183
  shipping: { flatRate: config.shipping?.flatRate ?? 0, freeAbove: config.shipping?.freeAbove },
155
- payments: { mode: config.payments?.mode ?? "mock" },
184
+ payments: {
185
+ mode: config.payments?.mode ?? "mock",
186
+ methods: config.payments?.methods?.length ? config.payments.methods : ["pix", "credit_card", "cash"]
187
+ },
156
188
  commerceMode,
157
189
  enquiry: {
158
190
  label: config.enquiry?.label ?? "Contact me",
@@ -184,15 +216,15 @@ function resolveConfig(config) {
184
216
  ] : [{ label: "Loja", to: "/" }])
185
217
  };
186
218
  }
187
- var StorefrontConfigContext = React6.createContext(null);
219
+ var StorefrontConfigContext = React7.createContext(null);
188
220
  var StorefrontConfigProvider = StorefrontConfigContext.Provider;
189
221
  function useStorefrontConfig() {
190
- const ctx = React6.useContext(StorefrontConfigContext);
222
+ const ctx = React7.useContext(StorefrontConfigContext);
191
223
  if (!ctx) throw new Error("useStorefrontConfig must be used inside createStorefrontApp");
192
224
  return ctx;
193
225
  }
194
226
  function useStorefrontConfigOptional() {
195
- return React6.useContext(StorefrontConfigContext);
227
+ return React7.useContext(StorefrontConfigContext);
196
228
  }
197
229
  function getStorefrontComponents(config) {
198
230
  return config.components ?? {};
@@ -202,8 +234,8 @@ function navigateTo(to) {
202
234
  adapter.navigate(to);
203
235
  }
204
236
  function useHashPath() {
205
- const [path, setPath] = React6.useState(() => adapter.getCurrentPath() || "/");
206
- React6.useEffect(() => {
237
+ const [path, setPath] = React7.useState(() => adapter.getCurrentPath() || "/");
238
+ React7.useEffect(() => {
207
239
  const unsub = adapter.onPathChange((p) => {
208
240
  setPath(p || "/");
209
241
  window.scrollTo(0, 0);
@@ -227,93 +259,7 @@ function matchPath(pattern, path) {
227
259
  return params;
228
260
  }
229
261
  function Link({ to, children, ...rest }) {
230
- return React6__default.default.createElement("a", { href: `#${to}`, ...rest }, children);
231
- }
232
- var RADIUS_MAP = {
233
- none: { button: "0px", card: "0px", input: "0px" },
234
- soft: { button: "6px", card: "10px", input: "6px" },
235
- round: { button: "10px", card: "16px", input: "10px" },
236
- pill: { button: "999px", card: "20px", input: "999px" }
237
- };
238
- var COLOR_VAR_MAP = {
239
- background: "--background",
240
- foreground: "--foreground",
241
- primary: "--primary",
242
- primaryForeground: "--primary-foreground",
243
- card: "--card",
244
- cardForeground: "--card-foreground",
245
- muted: "--muted",
246
- mutedForeground: "--muted-foreground",
247
- border: "--border",
248
- headerBackground: "--sf-header-bg",
249
- headerForeground: "--sf-header-fg",
250
- announcementBackground: "--sf-announcement-bg",
251
- announcementForeground: "--sf-announcement-fg"
252
- };
253
- function themeToCss(theme) {
254
- const lines = [];
255
- for (const [key, cssVar] of Object.entries(COLOR_VAR_MAP)) {
256
- const value = theme.colors?.[key];
257
- if (value) lines.push(`${cssVar}: ${value};`);
258
- }
259
- if (!theme.colors?.headerBackground && theme.colors?.background) {
260
- lines.push(`--sf-header-bg: ${theme.colors.background};`);
261
- }
262
- if (!theme.colors?.headerForeground && theme.colors?.foreground) {
263
- lines.push(`--sf-header-fg: ${theme.colors.foreground};`);
264
- }
265
- if (theme.font) {
266
- const fallback = theme.font.fallback ?? "sans-serif";
267
- lines.push(`--font-family: '${theme.font.body}', ${fallback};`);
268
- lines.push(`--sf-font-heading: '${theme.font.heading}', ${fallback};`);
269
- }
270
- const radius = RADIUS_MAP[theme.radius ?? "soft"];
271
- lines.push(`--button-radius: ${radius.button};`);
272
- lines.push(`--card-radius: ${radius.card};`);
273
- lines.push(`--input-radius: ${radius.input};`);
274
- lines.push(`--sf-radius-button: ${radius.button};`);
275
- lines.push(`--sf-radius-card: ${radius.card};`);
276
- lines.push(`--sf-radius-input: ${radius.input};`);
277
- let css = `:root {
278
- ${lines.join("\n ")}
279
- }
280
- `;
281
- css += `body { font-family: var(--font-family); }
282
- `;
283
- css += `h1, h2, h3, h4, .sf-heading { font-family: var(--sf-font-heading, var(--font-family)); }
284
- `;
285
- if (theme.uppercaseButtons) {
286
- css += `.sf-cta { text-transform: uppercase; letter-spacing: 0.08em; }
287
- `;
288
- }
289
- if (theme.header?.uppercaseNav) {
290
- css += `.sf-nav-link { text-transform: uppercase; letter-spacing: 0.06em; font-size: 0.8rem; }
291
- `;
292
- }
293
- return css;
294
- }
295
- function googleFontsHref(theme) {
296
- if (!theme.font) return null;
297
- const families = theme.font.googleFonts ?? [theme.font.heading, theme.font.body];
298
- const unique = [...new Set(families)];
299
- const params = unique.map((f) => `family=${encodeURIComponent(f).replace(/%20/g, "+")}:wght@300;400;500;600;700`).join("&");
300
- return `https://fonts.googleapis.com/css2?${params}&display=swap`;
301
- }
302
- function StorefrontThemeStyle({ theme }) {
303
- const fontsHref = googleFontsHref(theme);
304
- React6.useEffect(() => {
305
- if (!fontsHref) return;
306
- const id = "sf-theme-fonts";
307
- let link = document.getElementById(id);
308
- if (!link) {
309
- link = document.createElement("link");
310
- link.id = id;
311
- link.rel = "stylesheet";
312
- document.head.appendChild(link);
313
- }
314
- link.href = fontsHref;
315
- }, [fontsHref]);
316
- return React6__default.default.createElement("style", { "data-sf-theme": theme.name }, themeToCss(theme));
262
+ return React7__default.default.createElement("a", { href: `#${to}`, ...rest }, children);
317
263
  }
318
264
 
319
265
  // src/format.ts
@@ -323,6 +269,87 @@ function formatMoney(value, currency = "BRL", locale = "pt-BR") {
323
269
  function roundCents(value) {
324
270
  return Math.round(value * 100) / 100;
325
271
  }
272
+ var EMPTY = {
273
+ postalCode: "",
274
+ address: null,
275
+ options: [],
276
+ selectedZoneId: null,
277
+ status: "idle",
278
+ error: null
279
+ };
280
+ var useDeliveryStore = zustand.create()(
281
+ middleware.persist(
282
+ (set, get) => ({
283
+ ...EMPTY,
284
+ async resolve(postalCode, subtotal) {
285
+ const code = core.normalizePostalCode(postalCode);
286
+ if (code.length !== 8) {
287
+ set({ ...EMPTY, postalCode, status: "error", error: "Digite os 8 d\xEDgitos do CEP." });
288
+ return;
289
+ }
290
+ set({ postalCode: code, status: "loading", error: null });
291
+ let options = [];
292
+ try {
293
+ options = await runtime.getShopProvider().quoteShipping?.(code, subtotal) ?? [];
294
+ } catch {
295
+ set({
296
+ ...EMPTY,
297
+ postalCode: code,
298
+ status: "error",
299
+ error: "N\xE3o foi poss\xEDvel calcular o frete agora. Tente de novo."
300
+ });
301
+ return;
302
+ }
303
+ let address = null;
304
+ let notFound = false;
305
+ try {
306
+ address = await core.lookupPostalCode(code);
307
+ notFound = address === null;
308
+ } catch {
309
+ address = null;
310
+ }
311
+ set({
312
+ postalCode: code,
313
+ address,
314
+ options,
315
+ selectedZoneId: options[0]?.zoneId ?? null,
316
+ // A store with no zones configured quotes nothing, and that must NOT
317
+ // read as "we don't deliver here" — it falls back to the store-wide
318
+ // flat rate, which the cart already knows how to show.
319
+ status: options.length > 0 ? "served" : "unserved",
320
+ // Reported alongside the price, not instead of it: the CEP may be
321
+ // mistyped even though its range is covered.
322
+ error: notFound ? "CEP n\xE3o encontrado. Confira os n\xFAmeros." : null
323
+ });
324
+ },
325
+ selectZone(zoneId) {
326
+ if (get().options.some((option) => option.zoneId === zoneId)) set({ selectedZoneId: zoneId });
327
+ },
328
+ clear() {
329
+ set({ ...EMPTY });
330
+ }
331
+ }),
332
+ {
333
+ name: "fayz.storefront.delivery.v1",
334
+ // Deliberately NOT persisting `options`: a rehydrated
335
+ // price would be shown as current without anything having quoted it. The
336
+ // address survives, the money is asked for again.
337
+ partialize: (state) => ({ postalCode: state.postalCode, address: state.address }),
338
+ onRehydrateStorage: () => (state) => {
339
+ if (state?.address) state.status = "idle";
340
+ }
341
+ }
342
+ )
343
+ );
344
+ function resolveOptionRate(option, subtotal) {
345
+ if (option.freeAbove != null && subtotal >= option.freeAbove) return 0;
346
+ return option.baseRate;
347
+ }
348
+ var selectQuotedShipping = (state, subtotal) => {
349
+ if (state.status !== "served") return null;
350
+ const option = state.options.find((o) => o.zoneId === state.selectedZoneId) ?? state.options[0];
351
+ return option ? resolveOptionRate(option, subtotal) : null;
352
+ };
326
353
 
327
354
  // src/product-options.ts
328
355
  function cleanValues(value) {
@@ -367,7 +394,7 @@ function productOptionSelectionKey(selection) {
367
394
  // src/stores/cart.store.ts
368
395
  var useCartStore = zustand.create()(
369
396
  middleware.persist(
370
- (set) => ({
397
+ (set, get) => ({
371
398
  lines: [],
372
399
  discountCode: null,
373
400
  discountPercent: 0,
@@ -419,7 +446,16 @@ var useCartStore = zustand.create()(
419
446
  clearDiscount: () => set({ discountCode: null, discountPercent: 0 }),
420
447
  clear: () => set({ lines: [], discountCode: null, discountPercent: 0 }),
421
448
  openDrawer: () => set({ isOpen: true }),
422
- closeDrawer: () => set({ isOpen: false, justAddedLineId: null })
449
+ closeDrawer: () => set({ isOpen: false, justAddedLineId: null }),
450
+ reconcile: async (resolve) => {
451
+ const lines = get().lines;
452
+ if (lines.length === 0) return;
453
+ const keep = await Promise.all(
454
+ lines.map((line) => resolve(line).catch(() => true))
455
+ );
456
+ const kept = lines.filter((_, i) => keep[i]);
457
+ if (kept.length !== lines.length) set({ lines: kept });
458
+ }
423
459
  }),
424
460
  {
425
461
  name: "fayz.storefront.cart.v1",
@@ -437,10 +473,108 @@ var selectDiscountTotal = (s) => roundCents(selectSubtotal(s) * (s.discountPerce
437
473
  var selectShipping = (s, cfg) => {
438
474
  if (s.lines.length === 0) return 0;
439
475
  const subtotal = selectSubtotal(s);
476
+ const quoted = selectQuotedShipping(useDeliveryStore.getState(), subtotal);
477
+ if (quoted != null) return quoted;
440
478
  if (cfg.shipping.freeAbove != null && subtotal >= cfg.shipping.freeAbove) return 0;
441
479
  return cfg.shipping.flatRate;
442
480
  };
443
481
  var selectTotal = (s, cfg) => roundCents(selectSubtotal(s) - selectDiscountTotal(s) + selectShipping(s, cfg));
482
+ var RADIUS_MAP = {
483
+ none: { button: "0px", card: "0px", input: "0px" },
484
+ soft: { button: "6px", card: "10px", input: "6px" },
485
+ round: { button: "10px", card: "16px", input: "10px" },
486
+ pill: { button: "999px", card: "20px", input: "999px" }
487
+ };
488
+ var COLOR_VAR_MAP = {
489
+ background: "--background",
490
+ foreground: "--foreground",
491
+ primary: "--primary",
492
+ primaryForeground: "--primary-foreground",
493
+ card: "--card",
494
+ cardForeground: "--card-foreground",
495
+ muted: "--muted",
496
+ mutedForeground: "--muted-foreground",
497
+ border: "--border",
498
+ headerBackground: "--sf-header-bg",
499
+ headerForeground: "--sf-header-fg",
500
+ announcementBackground: "--sf-announcement-bg",
501
+ announcementForeground: "--sf-announcement-fg",
502
+ accent: "--sf-accent",
503
+ accentForeground: "--sf-accent-foreground"
504
+ };
505
+ function themeToCss(theme) {
506
+ const lines = [];
507
+ for (const [key, cssVar] of Object.entries(COLOR_VAR_MAP)) {
508
+ const value = theme.colors?.[key];
509
+ if (value) lines.push(`${cssVar}: ${value};`);
510
+ }
511
+ if (!theme.colors?.headerBackground && theme.colors?.background) {
512
+ lines.push(`--sf-header-bg: ${theme.colors.background};`);
513
+ }
514
+ if (!theme.colors?.headerForeground && theme.colors?.foreground) {
515
+ lines.push(`--sf-header-fg: ${theme.colors.foreground};`);
516
+ }
517
+ if (theme.font) {
518
+ const fallback = theme.font.fallback ?? "sans-serif";
519
+ lines.push(`--font-family: '${theme.font.body}', ${fallback};`);
520
+ lines.push(`--sf-font-heading: '${theme.font.heading}', ${fallback};`);
521
+ if (theme.font.display) lines.push(`--sf-font-display: '${theme.font.display}', ${fallback};`);
522
+ if (theme.font.serif) lines.push(`--sf-font-serif: '${theme.font.serif}', serif;`);
523
+ }
524
+ for (const [key, value] of Object.entries(theme.tokens ?? {})) {
525
+ const name = key.startsWith("--") ? key : `--${key}`;
526
+ lines.push(`${name}: ${value};`);
527
+ }
528
+ const radius = RADIUS_MAP[theme.radius ?? "soft"];
529
+ lines.push(`--button-radius: ${radius.button};`);
530
+ lines.push(`--card-radius: ${radius.card};`);
531
+ lines.push(`--input-radius: ${radius.input};`);
532
+ lines.push(`--sf-radius-button: ${radius.button};`);
533
+ lines.push(`--sf-radius-card: ${radius.card};`);
534
+ lines.push(`--sf-radius-input: ${radius.input};`);
535
+ let css = `:root {
536
+ ${lines.join("\n ")}
537
+ }
538
+ `;
539
+ css += `body { font-family: var(--font-family); }
540
+ `;
541
+ css += `h1, h2, h3, h4, .sf-heading { font-family: var(--sf-font-heading, var(--font-family)); }
542
+ `;
543
+ if (theme.uppercaseButtons) {
544
+ css += `.sf-cta { text-transform: uppercase; letter-spacing: 0.08em; }
545
+ `;
546
+ }
547
+ if (theme.header?.uppercaseNav) {
548
+ css += `.sf-nav-link { text-transform: uppercase; letter-spacing: 0.06em; font-size: 0.8rem; }
549
+ `;
550
+ }
551
+ return css;
552
+ }
553
+ function googleFontsHref(theme) {
554
+ if (!theme.font) return null;
555
+ const families = theme.font.googleFonts ?? [theme.font.heading, theme.font.body, theme.font.display, theme.font.serif].filter(
556
+ (f) => Boolean(f)
557
+ );
558
+ const unique = [...new Set(families)];
559
+ const params = unique.map((f) => `family=${encodeURIComponent(f).replace(/%20/g, "+")}:wght@300;400;500;600;700`).join("&");
560
+ return `https://fonts.googleapis.com/css2?${params}&display=swap`;
561
+ }
562
+ function StorefrontThemeStyle({ theme }) {
563
+ const fontsHref = googleFontsHref(theme);
564
+ React7.useEffect(() => {
565
+ if (!fontsHref) return;
566
+ const id = "sf-theme-fonts";
567
+ let link = document.getElementById(id);
568
+ if (!link) {
569
+ link = document.createElement("link");
570
+ link.id = id;
571
+ link.rel = "stylesheet";
572
+ document.head.appendChild(link);
573
+ }
574
+ link.href = fontsHref;
575
+ }, [fontsHref]);
576
+ return React7__default.default.createElement("style", { "data-sf-theme": theme.name }, themeToCss(theme));
577
+ }
444
578
  var initial = {
445
579
  search: "",
446
580
  categoryId: null,
@@ -460,9 +594,9 @@ var useCatalogStore = zustand.create()((set) => ({
460
594
  reset: () => set(initial)
461
595
  }));
462
596
  function useCategories() {
463
- const [categories, setCategories] = React6.useState([]);
464
- const [loading, setLoading] = React6.useState(true);
465
- React6.useEffect(() => {
597
+ const [categories, setCategories] = React7.useState([]);
598
+ const [loading, setLoading] = React7.useState(true);
599
+ React7.useEffect(() => {
466
600
  let cancelled = false;
467
601
  runtime.getShopProvider().listCategories().then((data) => {
468
602
  if (!cancelled) setCategories(data);
@@ -481,9 +615,9 @@ function prefersReducedMotion() {
481
615
  return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
482
616
  }
483
617
  function useInView(margin = "0px 0px -8% 0px") {
484
- const ref = React6.useRef(null);
485
- const [inView, setInView] = React6.useState(() => prefersReducedMotion());
486
- React6.useEffect(() => {
618
+ const ref = React7.useRef(null);
619
+ const [inView, setInView] = React7.useState(() => prefersReducedMotion());
620
+ React7.useEffect(() => {
487
621
  if (inView) return;
488
622
  const el = ref.current;
489
623
  if (!el || typeof IntersectionObserver === "undefined") {
@@ -518,8 +652,8 @@ function Reveal({ delay = 0, children, className, style, ...rest }) {
518
652
  );
519
653
  }
520
654
  function useScrolled(threshold = 8) {
521
- const [scrolled, setScrolled] = React6.useState(false);
522
- React6.useEffect(() => {
655
+ const [scrolled, setScrolled] = React7.useState(false);
656
+ React7.useEffect(() => {
523
657
  const onScroll = () => setScrolled(window.scrollY > threshold);
524
658
  onScroll();
525
659
  window.addEventListener("scroll", onScroll, { passive: true });
@@ -528,9 +662,9 @@ function useScrolled(threshold = 8) {
528
662
  return scrolled;
529
663
  }
530
664
  function usePopOnChange(value) {
531
- const [pop, setPop] = React6.useState(false);
532
- const prev = React6.useRef(value);
533
- React6.useEffect(() => {
665
+ const [pop, setPop] = React7.useState(false);
666
+ const prev = React7.useRef(value);
667
+ React7.useEffect(() => {
534
668
  if (prev.current !== value && value > 0) {
535
669
  setPop(true);
536
670
  const t = setTimeout(() => setPop(false), 400);
@@ -539,7 +673,7 @@ function usePopOnChange(value) {
539
673
  prev.current = value;
540
674
  return void 0;
541
675
  }, [value]);
542
- React6.useEffect(() => {
676
+ React7.useEffect(() => {
543
677
  prev.current = value;
544
678
  });
545
679
  return pop;
@@ -549,6 +683,7 @@ function usePopOnChange(value) {
549
683
  var TID = {
550
684
  // header
551
685
  headerSearch: "header-search",
686
+ headerSearchToggle: "header-search-toggle",
552
687
  cartButton: "cart-button",
553
688
  cartCount: "cart-count",
554
689
  accountLink: "account-link",
@@ -604,6 +739,9 @@ var TID = {
604
739
  pdpAddToCart: "pdp-add-to-cart",
605
740
  pdpRoot: "pdp-root",
606
741
  pdpGallery: "pdp-gallery",
742
+ pdpGalleryImage: "pdp-gallery-image",
743
+ pdpGalleryThumbs: "pdp-gallery-thumbs",
744
+ pdpGalleryThumb: "pdp-gallery-thumb",
607
745
  pdpActions: "pdp-actions",
608
746
  pdpEnquiryButton: "pdp-enquiry-button",
609
747
  enquiryForm: "enquiry-form",
@@ -632,6 +770,15 @@ var TID = {
632
770
  cartTotal: "cart-total",
633
771
  goCheckout: "go-checkout",
634
772
  cartEmpty: "cart-empty",
773
+ // delivery estimate (product page + cart)
774
+ deliveryEstimator: "delivery-estimator",
775
+ deliveryCepInput: "delivery-cep-input",
776
+ deliveryCepSubmit: "delivery-cep-submit",
777
+ deliveryOption: "delivery-option",
778
+ deliveryUnserved: "delivery-unserved",
779
+ deliveryError: "delivery-error",
780
+ deliveryAddress: "delivery-address",
781
+ deliveryClear: "delivery-clear",
635
782
  // checkout
636
783
  checkoutEmail: "checkout-email",
637
784
  checkoutName: "checkout-name",
@@ -678,7 +825,7 @@ var headerDivider = {
678
825
  var ANNOUNCEMENT_KEY = "fayz.storefront.announcement.dismissed";
679
826
  function AnnouncementBar() {
680
827
  const config = useStorefrontConfig();
681
- const [dismissed, setDismissed] = React6.useState(() => {
828
+ const [dismissed, setDismissed] = React7.useState(() => {
682
829
  try {
683
830
  return localStorage.getItem(ANNOUNCEMENT_KEY) === config.announcement;
684
831
  } catch {
@@ -737,12 +884,26 @@ function UtilityBar() {
737
884
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "ml-auto flex items-center gap-4", children: social?.map((s) => /* @__PURE__ */ jsxRuntime.jsx("a", { href: s.href, target: "_blank", rel: "noreferrer", className: "hover:underline", children: s.label }, s.href)) })
738
885
  ] }) });
739
886
  }
740
- function SearchInput({ className }) {
887
+ function SearchInput({ className, iconOnly = false }) {
741
888
  const config = useStorefrontConfig();
742
889
  const search = useCatalogStore((s) => s.search);
743
890
  const setSearch = useCatalogStore((s) => s.setSearch);
744
891
  const path = useHashPath();
745
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `relative ${className ?? ""}`, children: [
892
+ const [open, setOpen] = React7.useState(false);
893
+ if (iconOnly && !open) {
894
+ return /* @__PURE__ */ jsxRuntime.jsx(
895
+ "button",
896
+ {
897
+ type: "button",
898
+ "aria-label": "Buscar produtos",
899
+ "data-testid": TID.headerSearchToggle,
900
+ onClick: () => setOpen(true),
901
+ className: "rounded-full p-2.5 transition-opacity hover:opacity-70",
902
+ children: /* @__PURE__ */ jsxRuntime.jsx(LucideIcons.Search, { className: "h-5 w-5" })
903
+ }
904
+ );
905
+ }
906
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `relative ${iconOnly ? "w-full max-w-xs" : className ?? ""}`, children: [
746
907
  /* @__PURE__ */ jsxRuntime.jsx(LucideIcons.Search, { className: "pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 opacity-60" }),
747
908
  /* @__PURE__ */ jsxRuntime.jsx(
748
909
  "input",
@@ -751,10 +912,17 @@ function SearchInput({ className }) {
751
912
  type: "search",
752
913
  placeholder: "Buscar produtos...",
753
914
  value: search,
915
+ autoFocus: iconOnly,
754
916
  onChange: (e) => {
755
917
  setSearch(e.target.value);
756
918
  if (path !== config.catalogPath) navigateTo(config.catalogPath);
757
919
  },
920
+ onKeyDown: (e) => {
921
+ if (e.key === "Escape" && iconOnly) setOpen(false);
922
+ },
923
+ onBlur: () => {
924
+ if (iconOnly && !search) setOpen(false);
925
+ },
758
926
  className: "w-full border bg-white/10 py-2 pl-9 pr-4 text-sm outline-none transition-colors placeholder:opacity-60 focus:border-primary focus:bg-white focus:text-gray-900",
759
927
  style: {
760
928
  borderRadius: "var(--sf-radius-input)",
@@ -769,7 +937,7 @@ function HeaderActions() {
769
937
  const count = useCartStore(selectCount);
770
938
  const openDrawer = useCartStore((s) => s.openDrawer);
771
939
  const session = useSessionStore();
772
- const [accountOpen, setAccountOpen] = React6.useState(false);
940
+ const [accountOpen, setAccountOpen] = React7.useState(false);
773
941
  const pop = usePopOnChange(count);
774
942
  const firstName = session.name?.split(/\s+/)[0];
775
943
  const initial2 = (firstName ?? session.email ?? "C").slice(0, 1).toUpperCase();
@@ -907,6 +1075,7 @@ function StorefrontHeader() {
907
1075
  const variant = config.theme?.header?.variant ?? "classic";
908
1076
  const scrolled = useScrolled();
909
1077
  const showSearch = config.theme?.header?.showSearch !== false;
1078
+ const iconSearch = config.theme?.header?.searchStyle === "icon";
910
1079
  const logo = /* @__PURE__ */ jsxRuntime.jsx(Link, { to: "/", className: "sf-heading shrink-0 text-xl font-bold tracking-tight", children: config.logo ?? config.name });
911
1080
  return /* @__PURE__ */ jsxRuntime.jsxs(
912
1081
  "header",
@@ -920,7 +1089,7 @@ function StorefrontHeader() {
920
1089
  // Rio/Flex pattern: centered logo row, nav row below
921
1090
  /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
922
1091
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mx-auto grid h-16 max-w-7xl grid-cols-[1fr_auto_1fr] items-center px-4 sm:px-6", children: [
923
- showSearch ? /* @__PURE__ */ jsxRuntime.jsx(SearchInput, { className: "hidden w-full max-w-xs sm:block" }) : /* @__PURE__ */ jsxRuntime.jsx("div", {}),
1092
+ showSearch ? /* @__PURE__ */ jsxRuntime.jsx(SearchInput, { iconOnly: iconSearch, className: "hidden w-full max-w-xs sm:block" }) : /* @__PURE__ */ jsxRuntime.jsx("div", {}),
924
1093
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-center", children: logo }),
925
1094
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "justify-self-end", children: /* @__PURE__ */ jsxRuntime.jsx(HeaderActions, {}) })
926
1095
  ] }),
@@ -930,7 +1099,7 @@ function StorefrontHeader() {
930
1099
  // Brasília pattern: prominent search left, logo center, actions right; nav row below
931
1100
  /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
932
1101
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mx-auto grid h-16 max-w-7xl grid-cols-[1fr_auto_1fr] items-center gap-6 px-4 sm:px-6", children: [
933
- showSearch ? /* @__PURE__ */ jsxRuntime.jsx(SearchInput, { className: "hidden w-full max-w-sm sm:block" }) : /* @__PURE__ */ jsxRuntime.jsx("div", {}),
1102
+ showSearch ? /* @__PURE__ */ jsxRuntime.jsx(SearchInput, { iconOnly: iconSearch, className: "hidden w-full max-w-sm sm:block" }) : /* @__PURE__ */ jsxRuntime.jsx("div", {}),
934
1103
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-center", children: logo }),
935
1104
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "justify-self-end", children: /* @__PURE__ */ jsxRuntime.jsx(HeaderActions, {}) })
936
1105
  ] }),
@@ -942,17 +1111,19 @@ function StorefrontHeader() {
942
1111
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center gap-5", children: /* @__PURE__ */ jsxRuntime.jsx(NavLinks, { className: "hidden sm:flex" }) }),
943
1112
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-center", children: logo }),
944
1113
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-end gap-3", children: [
945
- showSearch && /* @__PURE__ */ jsxRuntime.jsx(SearchInput, { className: "hidden w-44 lg:block" }),
1114
+ showSearch && /* @__PURE__ */ jsxRuntime.jsx(SearchInput, { iconOnly: iconSearch, className: "hidden w-44 lg:block" }),
946
1115
  /* @__PURE__ */ jsxRuntime.jsx(HeaderActions, {})
947
1116
  ] })
948
1117
  ] })
949
1118
  ) : (
950
- // classic: logo left, search center-right, actions right (original layout)
1119
+ // classic: logo left, nav, then a right-aligned search + actions cluster
951
1120
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mx-auto flex h-16 max-w-7xl items-center gap-6 px-4 sm:px-6", children: [
952
1121
  logo,
953
1122
  /* @__PURE__ */ jsxRuntime.jsx(NavLinks, { className: "hidden md:flex" }),
954
- showSearch && /* @__PURE__ */ jsxRuntime.jsx(SearchInput, { className: "ml-auto hidden w-full max-w-sm sm:block" }),
955
- /* @__PURE__ */ jsxRuntime.jsx(HeaderActions, {})
1123
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "ml-auto flex items-center gap-2 sm:gap-3", children: [
1124
+ showSearch && /* @__PURE__ */ jsxRuntime.jsx(SearchInput, { iconOnly: iconSearch, className: iconSearch ? "" : "hidden w-full max-w-sm sm:block" }),
1125
+ /* @__PURE__ */ jsxRuntime.jsx(HeaderActions, {})
1126
+ ] })
956
1127
  ] })
957
1128
  )
958
1129
  ]
@@ -1045,12 +1216,12 @@ var REASON_MESSAGE = {
1045
1216
  };
1046
1217
  function useDiscountValidator() {
1047
1218
  const config = useStorefrontConfig();
1048
- return React6.useCallback(async (code) => {
1219
+ return React7.useCallback(async (code) => {
1049
1220
  const normalized = code.trim().toUpperCase();
1050
1221
  if (!normalized) return { valid: false, percent: 0, message: "Informe um cupom." };
1051
1222
  const configDiscount = config.discounts?.find((d) => d.code.trim().toUpperCase() === normalized);
1052
1223
  if (configDiscount) return { valid: true, percent: configDiscount.percent };
1053
- const validation = await shop.validateDiscount({ code: normalized });
1224
+ const validation = await shop$1.validateDiscount({ code: normalized });
1054
1225
  if (!validation.valid) {
1055
1226
  return { valid: false, percent: 0, message: REASON_MESSAGE[validation.reason ?? "not_found"] ?? "Cupom inv\xE1lido ou expirado." };
1056
1227
  }
@@ -1126,6 +1297,113 @@ function QuantityInput({ value, onChange, min = 1, max, testId, incTestId, decTe
1126
1297
  )
1127
1298
  ] });
1128
1299
  }
1300
+ function DeliveryEstimator({ compact = false }) {
1301
+ const config = useStorefrontConfig();
1302
+ const cart = useCartStore();
1303
+ const delivery = useDeliveryStore();
1304
+ const [draft, setDraft] = React7.useState(() => core.formatPostalCode(delivery.postalCode));
1305
+ const subtotal = selectSubtotal(cart);
1306
+ const money = (value) => formatMoney(value, config.currency, config.locale);
1307
+ const loading = delivery.status === "loading";
1308
+ async function submit(event) {
1309
+ event.preventDefault();
1310
+ await delivery.resolve(draft, subtotal);
1311
+ }
1312
+ function eta(option) {
1313
+ const { etaMinDays: min, etaMaxDays: max } = option;
1314
+ if (min == null && max == null) return null;
1315
+ if (min === 0 && (max === 0 || max === 1)) return "hoje ou amanh\xE3";
1316
+ if (min != null && max != null && min !== max) return `${min} a ${max} dias \xFAteis`;
1317
+ return `${max ?? min} dia(s) \xFAtil(eis)`;
1318
+ }
1319
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1320
+ "section",
1321
+ {
1322
+ "data-testid": TID.deliveryEstimator,
1323
+ className: compact ? "rounded-lg border p-3" : "mt-6 rounded-lg border p-4",
1324
+ children: [
1325
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 text-sm font-semibold", children: [
1326
+ /* @__PURE__ */ jsxRuntime.jsx(LucideIcons.Truck, { className: "h-4 w-4 text-primary" }),
1327
+ "Calcular entrega"
1328
+ ] }),
1329
+ /* @__PURE__ */ jsxRuntime.jsxs("form", { onSubmit: submit, className: "mt-3 flex gap-2", children: [
1330
+ /* @__PURE__ */ jsxRuntime.jsx(
1331
+ "input",
1332
+ {
1333
+ "data-testid": TID.deliveryCepInput,
1334
+ "aria-label": "CEP",
1335
+ inputMode: "numeric",
1336
+ autoComplete: "postal-code",
1337
+ placeholder: "00000-000",
1338
+ value: draft,
1339
+ onChange: (event) => setDraft(core.formatPostalCode(event.target.value)),
1340
+ className: "min-w-0 flex-1 rounded-lg border bg-background px-3 py-2 text-sm outline-none transition focus:border-primary focus:ring-2 focus:ring-primary/10"
1341
+ }
1342
+ ),
1343
+ /* @__PURE__ */ jsxRuntime.jsx(
1344
+ "button",
1345
+ {
1346
+ type: "submit",
1347
+ "data-testid": TID.deliveryCepSubmit,
1348
+ disabled: loading,
1349
+ className: "shrink-0 rounded-lg bg-primary px-4 py-2 text-sm font-semibold text-primary-foreground transition hover:opacity-90 disabled:opacity-50",
1350
+ children: loading ? "..." : "Calcular"
1351
+ }
1352
+ )
1353
+ ] }),
1354
+ delivery.error && /* @__PURE__ */ jsxRuntime.jsx("p", { "data-testid": TID.deliveryError, className: "mt-3 text-sm text-destructive", children: delivery.error }),
1355
+ delivery.address && /* @__PURE__ */ jsxRuntime.jsxs("p", { "data-testid": TID.deliveryAddress, className: "mt-3 flex items-start gap-1.5 text-xs text-muted-foreground", children: [
1356
+ /* @__PURE__ */ jsxRuntime.jsx(LucideIcons.MapPin, { className: "mt-0.5 h-3.5 w-3.5 shrink-0" }),
1357
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
1358
+ [delivery.address.street, delivery.address.district].filter(Boolean).join(", "),
1359
+ delivery.address.street || delivery.address.district ? " \u2014 " : "",
1360
+ delivery.address.city,
1361
+ "/",
1362
+ delivery.address.state
1363
+ ] })
1364
+ ] }),
1365
+ delivery.status === "served" && /* @__PURE__ */ jsxRuntime.jsx("ul", { className: "mt-3 space-y-2", children: delivery.options.map((option) => {
1366
+ const selected = delivery.selectedZoneId === option.zoneId;
1367
+ const label = eta(option);
1368
+ const rate = resolveOptionRate(option, subtotal);
1369
+ return /* @__PURE__ */ jsxRuntime.jsx("li", { children: /* @__PURE__ */ jsxRuntime.jsxs(
1370
+ "button",
1371
+ {
1372
+ type: "button",
1373
+ "data-testid": TID.deliveryOption,
1374
+ "data-zone": option.zoneId,
1375
+ "data-rate": rate.toFixed(2),
1376
+ "aria-pressed": selected,
1377
+ onClick: () => delivery.selectZone(option.zoneId),
1378
+ className: `flex w-full items-center justify-between gap-3 rounded-lg border px-3 py-2 text-left text-sm transition ${selected ? "border-primary bg-primary/5" : "border-border hover:bg-muted/40"}`,
1379
+ children: [
1380
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "min-w-0", children: [
1381
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "block truncate font-medium", children: option.carrier ? `${option.carrier} \xB7 ${option.name}` : option.name }),
1382
+ label && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "block text-xs text-muted-foreground", children: label })
1383
+ ] }),
1384
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "shrink-0 font-semibold", children: rate === 0 ? "Gr\xE1tis" : money(rate) })
1385
+ ]
1386
+ }
1387
+ ) }, option.zoneId);
1388
+ }) }),
1389
+ delivery.status === "unserved" && /* @__PURE__ */ jsxRuntime.jsx("p", { "data-testid": TID.deliveryUnserved, className: "mt-3 text-sm text-destructive", children: "Ainda n\xE3o entregamos nesse CEP." }),
1390
+ delivery.postalCode && /* @__PURE__ */ jsxRuntime.jsx(
1391
+ "button",
1392
+ {
1393
+ type: "button",
1394
+ "data-testid": TID.deliveryClear,
1395
+ onClick: () => {
1396
+ delivery.clear();
1397
+ setDraft("");
1398
+ },
1399
+ className: "mt-3 text-xs text-muted-foreground underline-offset-2 hover:underline",
1400
+ children: "Trocar CEP"
1401
+ }
1402
+ )
1403
+ ]
1404
+ }
1405
+ );
1406
+ }
1129
1407
  function SmoothImage({
1130
1408
  imageLoading,
1131
1409
  loading = "lazy",
@@ -1143,9 +1421,9 @@ function SmoothImage({
1143
1421
  const durationMs = resolved.durationMs ?? 420;
1144
1422
  const easing = resolved.easing ?? "cubic-bezier(0.22, 1, 0.36, 1)";
1145
1423
  const blur = resolved.blur ?? true;
1146
- const ref = React6__default.default.useRef(null);
1147
- const [loaded, setLoaded] = React6__default.default.useState(mode === "none");
1148
- React6__default.default.useEffect(() => {
1424
+ const ref = React7__default.default.useRef(null);
1425
+ const [loaded, setLoaded] = React7__default.default.useState(mode === "none");
1426
+ React7__default.default.useEffect(() => {
1149
1427
  if (mode === "none") {
1150
1428
  setLoaded(true);
1151
1429
  return;
@@ -1177,10 +1455,10 @@ function CartDrawer() {
1177
1455
  const config = useStorefrontConfig();
1178
1456
  const cart = useCartStore();
1179
1457
  const validate = useDiscountValidator();
1180
- const [code, setCode] = React6.useState("");
1181
- const [discountError, setDiscountError] = React6.useState(null);
1182
- const [applying, setApplying] = React6.useState(false);
1183
- React6.useEffect(() => {
1458
+ const [code, setCode] = React7.useState("");
1459
+ const [discountError, setDiscountError] = React7.useState(null);
1460
+ const [applying, setApplying] = React7.useState(false);
1461
+ React7.useEffect(() => {
1184
1462
  if (!cart.isOpen) return;
1185
1463
  const onKey = (e) => {
1186
1464
  if (e.key === "Escape") cart.closeDrawer();
@@ -1188,6 +1466,7 @@ function CartDrawer() {
1188
1466
  document.addEventListener("keydown", onKey);
1189
1467
  return () => document.removeEventListener("keydown", onKey);
1190
1468
  }, [cart.isOpen, cart.closeDrawer]);
1469
+ const delivery = useDeliveryStore();
1191
1470
  const subtotal = selectSubtotal(cart);
1192
1471
  const discountTotal = selectDiscountTotal(cart);
1193
1472
  const shipping = selectShipping(cart, config);
@@ -1344,6 +1623,7 @@ function CartDrawer() {
1344
1623
  ] }),
1345
1624
  discountError && /* @__PURE__ */ jsxRuntime.jsx("p", { "data-testid": TID.discountError, className: "text-xs text-destructive", children: discountError })
1346
1625
  ] }),
1626
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mb-3", children: /* @__PURE__ */ jsxRuntime.jsx(DeliveryEstimator, { compact: true }) }),
1347
1627
  /* @__PURE__ */ jsxRuntime.jsxs("dl", { className: "space-y-1.5 text-sm", children: [
1348
1628
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex justify-between", children: [
1349
1629
  /* @__PURE__ */ jsxRuntime.jsx("dt", { className: "text-muted-foreground", children: "Subtotal" }),
@@ -1373,7 +1653,13 @@ function CartDrawer() {
1373
1653
  ] })
1374
1654
  ] }),
1375
1655
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex justify-between", children: [
1376
- /* @__PURE__ */ jsxRuntime.jsx("dt", { className: "text-muted-foreground", children: "Frete" }),
1656
+ /* @__PURE__ */ jsxRuntime.jsxs("dt", { className: "text-muted-foreground", children: [
1657
+ "Frete",
1658
+ delivery.status === "served" && delivery.postalCode && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "ml-1 text-xs", children: [
1659
+ "\xB7 ",
1660
+ core.formatPostalCode(delivery.postalCode)
1661
+ ] })
1662
+ ] }),
1377
1663
  /* @__PURE__ */ jsxRuntime.jsx("dd", { "data-testid": TID.cartShipping, "data-price": shipping.toFixed(2), children: shipping === 0 ? "Gr\xE1tis" : money(shipping) })
1378
1664
  ] }),
1379
1665
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex justify-between border-t pt-2 text-base font-semibold", children: [
@@ -1403,12 +1689,12 @@ function CartDrawer() {
1403
1689
  ] });
1404
1690
  }
1405
1691
  function useProducts(opts) {
1406
- const [products, setProducts] = React6.useState([]);
1407
- const [loading, setLoading] = React6.useState(true);
1408
- const [error, setError] = React6.useState(null);
1409
- const [nonce, setNonce] = React6.useState(0);
1692
+ const [products, setProducts] = React7.useState([]);
1693
+ const [loading, setLoading] = React7.useState(true);
1694
+ const [error, setError] = React7.useState(null);
1695
+ const [nonce, setNonce] = React7.useState(0);
1410
1696
  const key = JSON.stringify(opts);
1411
- React6.useEffect(() => {
1697
+ React7.useEffect(() => {
1412
1698
  let cancelled = false;
1413
1699
  setLoading(true);
1414
1700
  runtime.getShopProvider().listProducts(JSON.parse(key)).then((data) => {
@@ -1726,10 +2012,10 @@ function FiltersPanel() {
1726
2012
  }
1727
2013
  function useStorefrontHead(input) {
1728
2014
  const { title, description } = input;
1729
- React6.useEffect(() => {
2015
+ React7.useEffect(() => {
1730
2016
  if (title) document.title = title;
1731
2017
  }, [title]);
1732
- React6.useEffect(() => {
2018
+ React7.useEffect(() => {
1733
2019
  if (!description) return;
1734
2020
  let tag = document.head.querySelector('meta[name="description"]');
1735
2021
  if (!tag) {
@@ -1748,9 +2034,9 @@ var SORT_MAP = {
1748
2034
  };
1749
2035
  var SORTS = ["newest", "price-asc", "price-desc", "name"];
1750
2036
  function useCatalogUrlSync() {
1751
- const hydrated = React6.useRef(false);
2037
+ const hydrated = React7.useRef(false);
1752
2038
  const catalog = useCatalogStore();
1753
- React6.useEffect(() => {
2039
+ React7.useEffect(() => {
1754
2040
  if (hydrated.current) return;
1755
2041
  hydrated.current = true;
1756
2042
  const q = window.location.hash.split("?")[1];
@@ -1765,7 +2051,7 @@ function useCatalogUrlSync() {
1765
2051
  if (p.has("min")) s.setPriceMin(Number(p.get("min")) || null);
1766
2052
  if (p.has("max")) s.setPriceMax(Number(p.get("max")) || null);
1767
2053
  }, []);
1768
- React6.useEffect(() => {
2054
+ React7.useEffect(() => {
1769
2055
  const p = new URLSearchParams();
1770
2056
  if (catalog.search) p.set("search", catalog.search);
1771
2057
  if (catalog.categoryId) p.set("category", catalog.categoryId);
@@ -1793,7 +2079,7 @@ function CatalogPage() {
1793
2079
  ...SORT_MAP[catalog.sort]
1794
2080
  };
1795
2081
  const { products, loading, error, reload } = useProducts(providerOpts);
1796
- const filtered = React6.useMemo(
2082
+ const filtered = React7.useMemo(
1797
2083
  () => products.filter((p) => {
1798
2084
  if (catalog.priceMin != null && p.price < catalog.priceMin) return false;
1799
2085
  if (catalog.priceMax != null && p.price > catalog.priceMax) return false;
@@ -1858,17 +2144,17 @@ function HeroSection({
1858
2144
  slides,
1859
2145
  height = "tall"
1860
2146
  }) {
1861
- const [index, setIndex] = React6.useState(0);
1862
- const [loadedSlides, setLoadedSlides] = React6.useState({});
2147
+ const [index, setIndex] = React7.useState(0);
2148
+ const [loadedSlides, setLoadedSlides] = React7.useState({});
1863
2149
  const config = useStorefrontConfig();
1864
- const interacted = React6.useRef(false);
2150
+ const interacted = React7.useRef(false);
1865
2151
  const hClass = height === "tall" ? "h-[72vh] min-h-[440px]" : "h-[48vh] min-h-[340px]";
1866
2152
  const revealImages = config.imageLoading.mode !== "none";
1867
2153
  const revealStyle = (loaded) => revealImages ? {
1868
2154
  filter: config.imageLoading.blur && !loaded ? "blur(10px)" : "blur(0px)",
1869
2155
  transition: `opacity 700ms ease, filter ${config.imageLoading.durationMs}ms ${config.imageLoading.easing}`
1870
2156
  } : void 0;
1871
- React6.useEffect(() => {
2157
+ React7.useEffect(() => {
1872
2158
  if (variant !== "slider" || slides.length < 2 || prefersReducedMotion()) return;
1873
2159
  const timer = setInterval(() => {
1874
2160
  if (!interacted.current) setIndex((i) => (i + 1) % slides.length);
@@ -2029,7 +2315,7 @@ function ProductRail({
2029
2315
  const { products, loading } = useProducts(
2030
2316
  filter === "new" ? { status: "active", orderBy: "created_at", order: "desc" } : { status: "active" }
2031
2317
  );
2032
- const visible = React6.useMemo(() => {
2318
+ const visible = React7.useMemo(() => {
2033
2319
  let list = products;
2034
2320
  if (filter === "sale") list = list.filter((p) => p.compareAtPrice != null && p.compareAtPrice > p.price);
2035
2321
  return list.slice(0, limit);
@@ -2078,16 +2364,16 @@ function MediaCarousel({
2078
2364
  overlay = "dark",
2079
2365
  className = ""
2080
2366
  }) {
2081
- const [index, setIndex] = React6.useState(0);
2082
- const [loadedItems, setLoadedItems] = React6.useState({});
2367
+ const [index, setIndex] = React7.useState(0);
2368
+ const [loadedItems, setLoadedItems] = React7.useState({});
2083
2369
  const config = useStorefrontConfig();
2084
- const interacted = React6.useRef(false);
2370
+ const interacted = React7.useRef(false);
2085
2371
  const revealImages = config.imageLoading.mode !== "none";
2086
2372
  const revealStyle = (loaded) => revealImages ? {
2087
2373
  filter: config.imageLoading.blur && !loaded ? "blur(10px)" : "blur(0px)",
2088
2374
  transition: `opacity 700ms ease, filter ${config.imageLoading.durationMs}ms ${config.imageLoading.easing}`
2089
2375
  } : void 0;
2090
- React6.useEffect(() => {
2376
+ React7.useEffect(() => {
2091
2377
  if (items.length < 2 || autoplayMs <= 0 || prefersReducedMotion()) return;
2092
2378
  const timer = setInterval(() => {
2093
2379
  if (!interacted.current) setIndex((i) => (i + 1) % items.length);
@@ -2180,13 +2466,13 @@ function ProductSlider({
2180
2466
  }) {
2181
2467
  const config = useStorefrontConfig();
2182
2468
  const actions = useStorefrontActions();
2183
- const viewportRef = React6.useRef(null);
2469
+ const viewportRef = React7.useRef(null);
2184
2470
  const components = getStorefrontComponents(config);
2185
2471
  const ProductCardComponent = components.ProductCard ?? ProductCard;
2186
2472
  const { products, loading } = useProducts(
2187
2473
  filter === "new" ? { status: "active", orderBy: "created_at", order: "desc" } : { status: "active" }
2188
2474
  );
2189
- const visible = React6.useMemo(() => {
2475
+ const visible = React7.useMemo(() => {
2190
2476
  let list = products;
2191
2477
  if (filter === "sale") list = list.filter((p) => p.compareAtPrice != null && p.compareAtPrice > p.price);
2192
2478
  if (categoryName) list = list.filter((p) => p.categoryName === categoryName);
@@ -2334,8 +2620,8 @@ function Testimonials({ title, items }) {
2334
2620
  ] }) });
2335
2621
  }
2336
2622
  function NewsletterBand({ title, subtitle }) {
2337
- const [email, setEmail] = React6.useState("");
2338
- const [done, setDone] = React6.useState(false);
2623
+ const [email, setEmail] = React7.useState("");
2624
+ const [done, setDone] = React7.useState(false);
2339
2625
  return /* @__PURE__ */ jsxRuntime.jsx("section", { "data-testid": TID.newsletter, className: "bg-primary py-16 text-primary-foreground", children: /* @__PURE__ */ jsxRuntime.jsxs(Reveal, { className: "mx-auto max-w-xl px-4 text-center sm:px-6", children: [
2340
2626
  /* @__PURE__ */ jsxRuntime.jsx("h2", { className: "sf-heading text-3xl font-bold", children: title ?? "Receba nossas novidades" }),
2341
2627
  subtitle && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-2 opacity-90", children: subtitle }),
@@ -2407,10 +2693,10 @@ function HomePage({ sections }) {
2407
2693
  return /* @__PURE__ */ jsxRuntime.jsx(StorefrontSections, { sections });
2408
2694
  }
2409
2695
  function useProduct(slug2) {
2410
- const [product, setProduct] = React6.useState(null);
2411
- const [loading, setLoading] = React6.useState(true);
2412
- const [error, setError] = React6.useState(null);
2413
- React6.useEffect(() => {
2696
+ const [product, setProduct] = React7.useState(null);
2697
+ const [loading, setLoading] = React7.useState(true);
2698
+ const [error, setError] = React7.useState(null);
2699
+ React7.useEffect(() => {
2414
2700
  let cancelled = false;
2415
2701
  setLoading(true);
2416
2702
  runtime.getShopProvider().listProducts({ slug: slug2, status: "active", limit: 1 }).then((data) => {
@@ -2548,13 +2834,13 @@ function buildWhatsAppHref(url, product) {
2548
2834
  }
2549
2835
  function ProductEnquiryForm({ product, onSuccess }) {
2550
2836
  const config = useStorefrontConfig();
2551
- const [customerName, setCustomerName] = React6.useState("");
2552
- const [customerEmail, setCustomerEmail] = React6.useState("");
2553
- const [customerPhone, setCustomerPhone] = React6.useState("");
2554
- const [subject, setSubject] = React6.useState(defaultSubject(config.enquiry.subjectPrefix, product));
2555
- const [message, setMessage] = React6.useState(`I would like more information about ${product.name}.`);
2556
- const [status, setStatus] = React6.useState("idle");
2557
- const [error, setError] = React6.useState(null);
2837
+ const [customerName, setCustomerName] = React7.useState("");
2838
+ const [customerEmail, setCustomerEmail] = React7.useState("");
2839
+ const [customerPhone, setCustomerPhone] = React7.useState("");
2840
+ const [subject, setSubject] = React7.useState(defaultSubject(config.enquiry.subjectPrefix, product));
2841
+ const [message, setMessage] = React7.useState(`I would like more information about ${product.name}.`);
2842
+ const [status, setStatus] = React7.useState("idle");
2843
+ const [error, setError] = React7.useState(null);
2558
2844
  const submit = async (event) => {
2559
2845
  event.preventDefault();
2560
2846
  setStatus("submitting");
@@ -2716,19 +3002,141 @@ function ProductEnquiryForm({ product, onSuccess }) {
2716
3002
  }
2717
3003
  );
2718
3004
  }
3005
+ function ProductGallery({ product, images, primaryImage }) {
3006
+ const ordered = orderImages(images, primaryImage);
3007
+ const [index, setIndex] = React7.useState(0);
3008
+ const [zoom, setZoom] = React7.useState(null);
3009
+ const frameRef = React7.useRef(null);
3010
+ const canHover = useCanHover();
3011
+ React7.useEffect(() => {
3012
+ setIndex(0);
3013
+ setZoom(null);
3014
+ }, [product.id]);
3015
+ const current = ordered[Math.min(index, ordered.length - 1)];
3016
+ function trackPointer(event) {
3017
+ if (!canHover) return;
3018
+ const frame = frameRef.current;
3019
+ if (!frame) return;
3020
+ const rect = frame.getBoundingClientRect();
3021
+ setZoom({
3022
+ x: (event.clientX - rect.left) / rect.width * 100,
3023
+ y: (event.clientY - rect.top) / rect.height * 100
3024
+ });
3025
+ }
3026
+ function step(delta) {
3027
+ if (ordered.length < 2) return;
3028
+ setIndex((current2) => (current2 + delta + ordered.length) % ordered.length);
3029
+ setZoom(null);
3030
+ }
3031
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-3 self-start", children: [
3032
+ /* @__PURE__ */ jsxRuntime.jsxs(
3033
+ "div",
3034
+ {
3035
+ ...storefrontComponentContracts.productDetail.gallery,
3036
+ ref: frameRef,
3037
+ className: "group relative aspect-square w-full overflow-hidden border bg-muted",
3038
+ style: { borderRadius: "var(--sf-radius-card)" },
3039
+ onMouseMove: trackPointer,
3040
+ onMouseLeave: () => setZoom(null),
3041
+ onKeyDown: (event) => {
3042
+ if (event.key === "ArrowRight") step(1);
3043
+ if (event.key === "ArrowLeft") step(-1);
3044
+ },
3045
+ tabIndex: ordered.length > 1 ? 0 : -1,
3046
+ role: ordered.length > 1 ? "group" : void 0,
3047
+ "aria-label": ordered.length > 1 ? `Imagens de ${product.name}` : void 0,
3048
+ children: [
3049
+ current && /* @__PURE__ */ jsxRuntime.jsx(
3050
+ "div",
3051
+ {
3052
+ className: "h-full w-full transition-transform duration-150 ease-out",
3053
+ style: zoom ? { transform: "scale(2)", transformOrigin: `${zoom.x}% ${zoom.y}%` } : { transform: "scale(1)" },
3054
+ children: /* @__PURE__ */ jsxRuntime.jsx(
3055
+ SmoothImage,
3056
+ {
3057
+ src: current.url,
3058
+ alt: current.altText ?? product.name,
3059
+ "data-testid": TID.pdpGalleryImage,
3060
+ className: "h-full w-full object-cover"
3061
+ },
3062
+ current.id
3063
+ )
3064
+ }
3065
+ ),
3066
+ ordered.length > 1 && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
3067
+ /* @__PURE__ */ jsxRuntime.jsx(GalleryArrow, { side: "left", onClick: () => step(-1) }),
3068
+ /* @__PURE__ */ jsxRuntime.jsx(GalleryArrow, { side: "right", onClick: () => step(1) }),
3069
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "pointer-events-none absolute bottom-3 right-3 rounded-full bg-black/60 px-2 py-0.5 text-[11px] font-medium text-white", children: [
3070
+ index + 1,
3071
+ "/",
3072
+ ordered.length
3073
+ ] })
3074
+ ] })
3075
+ ]
3076
+ }
3077
+ ),
3078
+ ordered.length > 1 && /* @__PURE__ */ jsxRuntime.jsx("ul", { "data-testid": TID.pdpGalleryThumbs, className: "flex gap-2 overflow-x-auto pb-1", children: ordered.map((image, position) => /* @__PURE__ */ jsxRuntime.jsx("li", { children: /* @__PURE__ */ jsxRuntime.jsx(
3079
+ "button",
3080
+ {
3081
+ type: "button",
3082
+ "data-testid": TID.pdpGalleryThumb,
3083
+ "data-index": position,
3084
+ "aria-label": `Imagem ${position + 1} de ${ordered.length}`,
3085
+ "aria-current": position === index,
3086
+ onClick: () => {
3087
+ setIndex(position);
3088
+ setZoom(null);
3089
+ },
3090
+ className: `h-16 w-16 shrink-0 overflow-hidden rounded-lg border transition ${position === index ? "border-primary ring-2 ring-primary/20" : "border-border opacity-70 hover:opacity-100"}`,
3091
+ children: /* @__PURE__ */ jsxRuntime.jsx("img", { src: image.url, alt: "", className: "h-full w-full object-cover", loading: "lazy" })
3092
+ }
3093
+ ) }, image.id)) })
3094
+ ] });
3095
+ }
3096
+ function orderImages(images, primary) {
3097
+ const all = images.length > 0 ? images : primary ? [primary] : [];
3098
+ return [...all].sort((a, b) => {
3099
+ if (a.isPrimary !== b.isPrimary) return a.isPrimary ? -1 : 1;
3100
+ return (a.sortOrder ?? 0) - (b.sortOrder ?? 0);
3101
+ });
3102
+ }
3103
+ function useCanHover() {
3104
+ const [canHover, setCanHover] = React7.useState(false);
3105
+ React7.useEffect(() => {
3106
+ const query = window.matchMedia?.("(hover: hover) and (pointer: fine)");
3107
+ if (!query) return;
3108
+ setCanHover(query.matches);
3109
+ const onChange = (event) => setCanHover(event.matches);
3110
+ query.addEventListener?.("change", onChange);
3111
+ return () => query.removeEventListener?.("change", onChange);
3112
+ }, []);
3113
+ return canHover;
3114
+ }
3115
+ function GalleryArrow({ side, onClick }) {
3116
+ return /* @__PURE__ */ jsxRuntime.jsx(
3117
+ "button",
3118
+ {
3119
+ type: "button",
3120
+ onClick,
3121
+ "aria-label": side === "left" ? "Imagem anterior" : "Pr\xF3xima imagem",
3122
+ className: `absolute top-1/2 z-10 flex h-9 w-9 -translate-y-1/2 items-center justify-center rounded-full bg-background/80 opacity-0 shadow-md backdrop-blur transition hover:bg-background focus:opacity-100 group-hover:opacity-100 ${side === "left" ? "left-3" : "right-3"}`,
3123
+ children: /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": true, className: "text-lg leading-none", children: side === "left" ? "\u2039" : "\u203A" })
3124
+ }
3125
+ );
3126
+ }
2719
3127
  function ProductDetailPage({ slug: slug2 }) {
2720
3128
  const config = useStorefrontConfig();
2721
3129
  const actions = useStorefrontActions();
2722
3130
  const { product, loading } = useProduct(slug2);
2723
3131
  const addItem = useCartStore((s) => s.addItem);
2724
- const [qty, setQty] = React6.useState(1);
2725
- const [selectedOptions, setSelectedOptions] = React6.useState({});
3132
+ const [qty, setQty] = React7.useState(1);
3133
+ const [selectedOptions, setSelectedOptions] = React7.useState({});
2726
3134
  const optionGroups = product ? getProductOptionGroups(product) : [];
2727
3135
  useStorefrontHead({
2728
3136
  title: product ? `${product.name} \u2014 ${config.name}` : config.name,
2729
3137
  description: product?.description
2730
3138
  });
2731
- React6.useEffect(() => {
3139
+ React7.useEffect(() => {
2732
3140
  if (!product) return;
2733
3141
  const defaults = Object.fromEntries(
2734
3142
  getProductOptionGroups(product).map((group) => [group.label, group.values[0] ?? ""])
@@ -2756,6 +3164,7 @@ function ProductDetailPage({ slug: slug2 }) {
2756
3164
  const image = product.images.find((i) => i.isPrimary) ?? product.images[0];
2757
3165
  const components = getStorefrontComponents(config);
2758
3166
  const ProductDetailComponent = components.ProductDetail;
3167
+ const GalleryComponent = components.ProductGallery ?? ProductGallery;
2759
3168
  const addToCart = () => addItem(product, qty, selectedOptions);
2760
3169
  const openEnquiry = () => {
2761
3170
  document.getElementById("storefront-product-enquiry")?.scrollIntoView({ behavior: "smooth", block: "start" });
@@ -2789,19 +3198,13 @@ function ProductDetailPage({ slug: slug2 }) {
2789
3198
  ] }),
2790
3199
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid animate-fade-up gap-10 md:grid-cols-2", children: [
2791
3200
  /* @__PURE__ */ jsxRuntime.jsx(
2792
- "div",
3201
+ GalleryComponent,
2793
3202
  {
2794
- ...storefrontComponentContracts.productDetail.gallery,
2795
- className: "group overflow-hidden border bg-muted",
2796
- style: { borderRadius: "var(--sf-radius-card)" },
2797
- children: image && /* @__PURE__ */ jsxRuntime.jsx(
2798
- SmoothImage,
2799
- {
2800
- src: image.url,
2801
- alt: image.altText ?? product.name,
2802
- className: "aspect-square w-full object-cover transition-transform duration-700 ease-out group-hover:scale-105"
2803
- }
2804
- )
3203
+ product,
3204
+ images: product.images,
3205
+ primaryImage: image,
3206
+ config,
3207
+ commerceMode: config.commerceMode
2805
3208
  }
2806
3209
  ),
2807
3210
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col py-2 lg:sticky lg:top-24 lg:self-start", children: [
@@ -2899,6 +3302,7 @@ function ProductDetailPage({ slug: slug2 }) {
2899
3302
  children: "Voltar ao cat\xE1logo"
2900
3303
  }
2901
3304
  ) }),
3305
+ config.commerceMode === "checkout" && /* @__PURE__ */ jsxRuntime.jsx(DeliveryEstimator, {}),
2902
3306
  config.commerceMode === "checkout" && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mt-6 grid grid-cols-3 gap-3 border-t pt-6 text-center", children: [
2903
3307
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col items-center gap-1.5 text-[11px] font-medium text-muted-foreground", children: [
2904
3308
  /* @__PURE__ */ jsxRuntime.jsx(LucideIcons.Truck, { className: "h-5 w-5 text-primary" }),
@@ -2924,9 +3328,10 @@ function ProductDetailPage({ slug: slug2 }) {
2924
3328
  /* @__PURE__ */ jsxRuntime.jsx(RelatedProducts, { product })
2925
3329
  ] });
2926
3330
  }
2927
- function formatDeliveryNotes(address) {
3331
+ function formatDeliveryNotes(address, shippingAddress) {
3332
+ if (address?.notes?.trim()) return address.notes.trim();
3333
+ if (shippingAddress) return void 0;
2928
3334
  if (!address) return void 0;
2929
- if (address.notes?.trim()) return address.notes.trim();
2930
3335
  const parts = [address.street, address.city, address.zip].map((part) => part?.trim()).filter(Boolean);
2931
3336
  return parts.length > 0 ? `Entrega: ${parts.join(", ")}` : void 0;
2932
3337
  }
@@ -2936,6 +3341,8 @@ async function placeStorefrontOrder({
2936
3341
  session,
2937
3342
  customer,
2938
3343
  address,
3344
+ shippingAddress,
3345
+ paymentMethod,
2939
3346
  markPaid = false
2940
3347
  }) {
2941
3348
  const email = customer.email.trim().toLowerCase();
@@ -2950,9 +3357,14 @@ async function placeStorefrontOrder({
2950
3357
  customerId: customerId ?? void 0,
2951
3358
  customer: { name, email },
2952
3359
  currency: config.currency,
2953
- notes: formatDeliveryNotes(address),
3360
+ notes: formatDeliveryNotes(address, shippingAddress),
2954
3361
  discountCode: cart.discountCode ?? void 0,
2955
3362
  shippingTotal: selectShipping(cart, config),
3363
+ // Structured address + method are what populate public.addresses and the
3364
+ // transactions ledger. `notes` stays for backwards compatibility with
3365
+ // checkouts that have not been updated yet.
3366
+ shippingAddress,
3367
+ paymentMethod,
2956
3368
  items: cart.lines.map((line) => ({
2957
3369
  productId: line.productId,
2958
3370
  quantity: line.quantity,
@@ -2960,16 +3372,17 @@ async function placeStorefrontOrder({
2960
3372
  }))
2961
3373
  });
2962
3374
  if (markPaid) {
2963
- await provider.updateOrder(order.id, { financialStatus: "paid" });
3375
+ if (provider.confirmPayment) await provider.confirmPayment(order.id);
3376
+ else await provider.updateOrder(order.id, { financialStatus: "paid" });
2964
3377
  }
2965
3378
  return { order, customerId: customerId ?? order.customerId ?? "" };
2966
3379
  }
2967
3380
  function SignInModal({ defaultEmail = "", onClose, onSignedIn }) {
2968
- const [email, setEmail] = React6.useState(defaultEmail);
2969
- const [password, setPassword] = React6.useState("");
2970
- const [busy, setBusy] = React6.useState(false);
2971
- const [error, setError] = React6.useState(null);
2972
- React6.useEffect(() => {
3381
+ const [email, setEmail] = React7.useState(defaultEmail);
3382
+ const [password, setPassword] = React7.useState("");
3383
+ const [busy, setBusy] = React7.useState(false);
3384
+ const [error, setError] = React7.useState(null);
3385
+ React7.useEffect(() => {
2973
3386
  const onKey = (event) => {
2974
3387
  if (event.key === "Escape") onClose();
2975
3388
  };
@@ -3074,19 +3487,18 @@ function SignInModal({ defaultEmail = "", onClose, onSignedIn }) {
3074
3487
  }
3075
3488
  );
3076
3489
  }
3077
- var SAVED_ADDRESSES = [
3078
- { id: "home", label: "Casa", street: "Rua das Flores, 123", city: "S\xE3o Paulo", zip: "01310-100" }
3079
- ];
3080
- var SAVED_PAYMENT_METHODS = [
3081
- { id: "visa", label: "Visa final 4242", card: "4242 4242 4242 4242", expiry: "12/29", cvc: "123" }
3082
- ];
3490
+ var PAYMENT_LABELS = {
3491
+ pix: { label: "Pix", hint: "Voc\xEA recebe a chave para pagar ap\xF3s confirmar o pedido" },
3492
+ credit_card: { label: "Cart\xE3o de cr\xE9dito", hint: "Maquininha na entrega" },
3493
+ debit_card: { label: "Cart\xE3o de d\xE9bito", hint: "Maquininha na entrega" },
3494
+ boleto: { label: "Boleto", hint: "Enviado por e-mail ap\xF3s a confirma\xE7\xE3o" },
3495
+ cash: { label: "Dinheiro", hint: "Pagamento na entrega" },
3496
+ other: { label: "Combinar com a loja", hint: "A loja entra em contato para acertar o pagamento" }
3497
+ };
3083
3498
  var PROCESSING_STEPS = [
3084
3499
  { icon: LucideIcons.Lock, label: "Validando seus dados..." },
3085
- { icon: LucideIcons.CreditCard, label: "Processando pagamento..." },
3086
- { icon: LucideIcons.PackageCheck, label: "Confirmando seu pedido..." }
3500
+ { icon: LucideIcons.PackageCheck, label: "Registrando seu pedido..." }
3087
3501
  ];
3088
- var DEFAULT_ADDRESS = SAVED_ADDRESSES[0];
3089
- var DEFAULT_PAYMENT = SAVED_PAYMENT_METHODS[0];
3090
3502
  var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
3091
3503
  function ProcessingOverlay({ step }) {
3092
3504
  const current = PROCESSING_STEPS[Math.min(step, PROCESSING_STEPS.length - 1)];
@@ -3129,41 +3541,69 @@ function CheckoutPage() {
3129
3541
  const config = useStorefrontConfig();
3130
3542
  const cart = useCartStore();
3131
3543
  const session = useSessionStore();
3544
+ const delivery = useDeliveryStore();
3132
3545
  const validateDiscount2 = useDiscountValidator();
3133
3546
  useStorefrontHead({ title: `Checkout \u2014 ${config.name}` });
3134
- const [selectedAddressId, setSelectedAddressId] = React6.useState(DEFAULT_ADDRESS.id);
3135
- const [selectedPaymentId, setSelectedPaymentId] = React6.useState(DEFAULT_PAYMENT.id);
3136
- const [savedAddresses, setSavedAddresses] = React6.useState(SAVED_ADDRESSES);
3137
- const [savedPaymentMethods, setSavedPaymentMethods] = React6.useState(SAVED_PAYMENT_METHODS);
3138
- const [addressMode, setAddressMode] = React6.useState("saved");
3139
- const [paymentMode, setPaymentMode] = React6.useState("saved");
3140
- const [form, setForm] = React6.useState({
3547
+ const paymentMethods = config.payments.methods;
3548
+ const [selectedAddressId, setSelectedAddressId] = React7.useState("new");
3549
+ const [savedAddresses, setSavedAddresses] = React7.useState([]);
3550
+ const [addressMode, setAddressMode] = React7.useState("new");
3551
+ const [paymentMethod, setPaymentMethod] = React7.useState(paymentMethods[0] ?? "pix");
3552
+ const [form, setForm] = React7.useState({
3141
3553
  email: session.email ?? "",
3142
3554
  name: session.name ?? "",
3143
- street: DEFAULT_ADDRESS.street,
3144
- city: DEFAULT_ADDRESS.city,
3145
- zip: DEFAULT_ADDRESS.zip,
3146
- card: DEFAULT_PAYMENT.card,
3147
- expiry: DEFAULT_PAYMENT.expiry,
3148
- cvc: DEFAULT_PAYMENT.cvc
3555
+ street: "",
3556
+ city: "",
3557
+ zip: "",
3558
+ number: "",
3559
+ complement: "",
3560
+ district: "",
3561
+ state: ""
3149
3562
  });
3150
- const [error, setError] = React6.useState(null);
3151
- const [discountCode, setDiscountCode] = React6.useState("");
3152
- const [discountError, setDiscountError] = React6.useState(null);
3153
- const [discountSuccess, setDiscountSuccess] = React6.useState(null);
3154
- const [applyingDiscount, setApplyingDiscount] = React6.useState(false);
3155
- const [placing, setPlacing] = React6.useState(false);
3156
- const [processingStep, setProcessingStep] = React6.useState(0);
3157
- const [showSignin, setShowSignin] = React6.useState(false);
3563
+ const [error, setError] = React7.useState(null);
3564
+ const [discountCode, setDiscountCode] = React7.useState("");
3565
+ const [discountError, setDiscountError] = React7.useState(null);
3566
+ const [discountSuccess, setDiscountSuccess] = React7.useState(null);
3567
+ const [applyingDiscount, setApplyingDiscount] = React7.useState(false);
3568
+ const [placing, setPlacing] = React7.useState(false);
3569
+ const [processingStep, setProcessingStep] = React7.useState(0);
3570
+ const [showSignin, setShowSignin] = React7.useState(false);
3571
+ const [zipLookup, setZipLookup] = React7.useState("idle");
3158
3572
  const subtotal = selectSubtotal(cart);
3159
3573
  const discountTotal = selectDiscountTotal(cart);
3160
3574
  const shipping = selectShipping(cart, config);
3161
3575
  const total = selectTotal(cart, config);
3162
3576
  const money = (value) => formatMoney(value, config.currency, config.locale);
3163
- React6.useEffect(() => {
3577
+ React7.useEffect(() => {
3164
3578
  if (cart.lines.length === 0 && !placing) navigateTo(config.catalogPath);
3165
3579
  }, [cart.lines.length, config.catalogPath, placing]);
3166
3580
  const set = (key) => (value) => setForm((current) => ({ ...current, [key]: value }));
3581
+ async function setZip(value) {
3582
+ const masked = core.formatPostalCode(value);
3583
+ setForm((current) => ({ ...current, zip: masked }));
3584
+ if (core.normalizePostalCode(masked).length !== 8) {
3585
+ setZipLookup("idle");
3586
+ return;
3587
+ }
3588
+ setZipLookup("loading");
3589
+ try {
3590
+ const found = await core.lookupPostalCode(masked);
3591
+ if (!found) {
3592
+ setZipLookup("not-found");
3593
+ return;
3594
+ }
3595
+ setZipLookup("found");
3596
+ setForm((current) => ({
3597
+ ...current,
3598
+ street: found.street || current.street,
3599
+ district: found.district || current.district,
3600
+ city: found.city,
3601
+ state: found.state
3602
+ }));
3603
+ } catch {
3604
+ setZipLookup("idle");
3605
+ }
3606
+ }
3167
3607
  async function applyDiscountCode() {
3168
3608
  setApplyingDiscount(true);
3169
3609
  setDiscountError(null);
@@ -3190,10 +3630,54 @@ function CheckoutPage() {
3190
3630
  setDiscountSuccess(null);
3191
3631
  setDiscountError(null);
3192
3632
  }
3633
+ React7.useEffect(() => {
3634
+ const customerId = session.customerId;
3635
+ if (!customerId) {
3636
+ setSavedAddresses([]);
3637
+ setAddressMode("new");
3638
+ return;
3639
+ }
3640
+ let cancelled = false;
3641
+ const provider = runtime.getShopProvider();
3642
+ void provider.listCustomerAddresses?.(customerId).then((addresses) => {
3643
+ if (cancelled || addresses.length === 0) return;
3644
+ setSavedAddresses(addresses);
3645
+ applySavedAddress(addresses[0]);
3646
+ }).catch(() => {
3647
+ });
3648
+ return () => {
3649
+ cancelled = true;
3650
+ };
3651
+ }, [session.customerId]);
3652
+ React7.useEffect(() => {
3653
+ if (addressMode !== "new") return;
3654
+ const looked = delivery.address;
3655
+ if (!looked) return;
3656
+ setForm((current) => {
3657
+ if (current.zip.trim() || current.street.trim()) return current;
3658
+ return {
3659
+ ...current,
3660
+ zip: core.formatPostalCode(looked.postalCode),
3661
+ street: looked.street,
3662
+ district: looked.district,
3663
+ city: looked.city,
3664
+ state: looked.state
3665
+ };
3666
+ });
3667
+ }, [delivery.address, addressMode]);
3193
3668
  function applySavedAddress(address) {
3194
3669
  setAddressMode("saved");
3195
3670
  setSelectedAddressId(address.id);
3196
- setForm((current) => ({ ...current, street: address.street, city: address.city, zip: address.zip }));
3671
+ setForm((current) => ({
3672
+ ...current,
3673
+ street: address.street,
3674
+ city: address.city,
3675
+ zip: address.postalCode,
3676
+ number: address.number ?? "",
3677
+ complement: address.complement ?? "",
3678
+ district: address.district ?? "",
3679
+ state: address.state
3680
+ }));
3197
3681
  }
3198
3682
  function selectAddress(addressId) {
3199
3683
  const address = savedAddresses.find((item) => item.id === addressId);
@@ -3202,37 +3686,7 @@ function CheckoutPage() {
3202
3686
  function addNewAddress() {
3203
3687
  setAddressMode("new");
3204
3688
  setSelectedAddressId("new");
3205
- setForm((current) => ({ ...current, street: "", city: "", zip: "" }));
3206
- }
3207
- function removeAddress(addressId) {
3208
- const nextAddresses = savedAddresses.filter((item) => item.id !== addressId);
3209
- setSavedAddresses(nextAddresses);
3210
- if (selectedAddressId !== addressId) return;
3211
- const fallback = nextAddresses[0];
3212
- if (fallback) applySavedAddress(fallback);
3213
- else addNewAddress();
3214
- }
3215
- function applySavedPayment(method) {
3216
- setPaymentMode("saved");
3217
- setSelectedPaymentId(method.id);
3218
- setForm((current) => ({ ...current, card: method.card, expiry: method.expiry, cvc: method.cvc }));
3219
- }
3220
- function selectPayment(paymentId) {
3221
- const method = savedPaymentMethods.find((item) => item.id === paymentId);
3222
- if (method) applySavedPayment(method);
3223
- }
3224
- function addNewPayment() {
3225
- setPaymentMode("new");
3226
- setSelectedPaymentId("new");
3227
- setForm((current) => ({ ...current, card: "", expiry: "", cvc: "" }));
3228
- }
3229
- function removePayment(paymentId) {
3230
- const nextMethods = savedPaymentMethods.filter((item) => item.id !== paymentId);
3231
- setSavedPaymentMethods(nextMethods);
3232
- if (selectedPaymentId !== paymentId) return;
3233
- const fallback = nextMethods[0];
3234
- if (fallback) applySavedPayment(fallback);
3235
- else addNewPayment();
3689
+ setForm((current) => ({ ...current, street: "", city: "", zip: "", number: "", complement: "", district: "", state: "" }));
3236
3690
  }
3237
3691
  function validate() {
3238
3692
  setError(null);
@@ -3244,12 +3698,20 @@ function CheckoutPage() {
3244
3698
  setError("Informe seu nome completo.");
3245
3699
  return false;
3246
3700
  }
3247
- if (!form.street.trim() || !form.city.trim() || !form.zip.trim()) {
3701
+ if (!form.street.trim() || !form.city.trim() || !form.zip.trim() || !form.district.trim()) {
3248
3702
  setError("Preencha ou selecione o endere\xE7o de entrega.");
3249
3703
  return false;
3250
3704
  }
3251
- if (!/^\d{16}$/.test(form.card.replace(/\s+/g, "")) || !form.expiry.trim() || !form.cvc.trim()) {
3252
- setError("Selecione ou preencha um m\xE9todo de pagamento v\xE1lido.");
3705
+ if (!form.number.trim()) {
3706
+ setError("Falta o n\xFAmero do endere\xE7o.");
3707
+ return false;
3708
+ }
3709
+ if (form.state.trim().length !== 2) {
3710
+ setError("Informe a UF com duas letras (ex.: RJ).");
3711
+ return false;
3712
+ }
3713
+ if (delivery.status === "unserved" && delivery.postalCode === core.normalizePostalCode(form.zip)) {
3714
+ setError("Ainda n\xE3o entregamos nesse CEP. Tente outro endere\xE7o.");
3253
3715
  return false;
3254
3716
  }
3255
3717
  return true;
@@ -3271,9 +3733,22 @@ function CheckoutPage() {
3271
3733
  session,
3272
3734
  customer: { email: form.email, name: form.name },
3273
3735
  address: { street: form.street, city: form.city, zip: form.zip },
3274
- // Demo only: mock mode instantly approves. Real Pix/Mercado Pago (M4)
3275
- // leaves the order pending until the payment webhook confirms.
3276
- markPaid: config.payments.mode === "mock"
3736
+ shippingAddress: {
3737
+ postalCode: form.zip.trim(),
3738
+ street: form.street.trim(),
3739
+ number: form.number.trim() || void 0,
3740
+ complement: form.complement.trim() || void 0,
3741
+ district: form.district.trim() || void 0,
3742
+ city: form.city.trim(),
3743
+ state: form.state.trim().toUpperCase()
3744
+ },
3745
+ // The method the buyer actually chose, which opens the ledger row in
3746
+ // public.transactions with the right kind.
3747
+ paymentMethod
3748
+ // Never marked paid from the browser. It used to be, through an RPC that
3749
+ // was granted to anon — so any buyer holding their own order id could
3750
+ // declare it settled. The order is now born `pending` and only the
3751
+ // merchant (or a PSP webhook) can confirm the money arrived.
3277
3752
  });
3278
3753
  await minDuration;
3279
3754
  cart.clear();
@@ -3346,35 +3821,25 @@ function CheckoutPage() {
3346
3821
  /* @__PURE__ */ jsxRuntime.jsxs("section", { children: [
3347
3822
  /* @__PURE__ */ jsxRuntime.jsx("h2", { className: "mb-3 text-xl font-semibold tracking-tight", children: "Endere\xE7o de entrega" }),
3348
3823
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mb-3 grid gap-3", children: [
3349
- savedAddresses.map((address) => /* @__PURE__ */ jsxRuntime.jsxs(
3824
+ savedAddresses.map((address) => /* @__PURE__ */ jsxRuntime.jsx(
3350
3825
  "div",
3351
3826
  {
3352
3827
  className: `group relative rounded-lg border p-3 pr-11 text-left text-sm transition hover:bg-muted/40 ${selectedAddressId === address.id ? "border-primary bg-primary/5" : "border-border bg-background"}`,
3353
- children: [
3354
- /* @__PURE__ */ jsxRuntime.jsxs("button", { type: "button", onClick: () => selectAddress(address.id), className: "block w-full text-left", children: [
3355
- /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex items-center justify-between gap-3 font-semibold", children: [
3356
- address.label,
3357
- addressMode === "saved" && selectedAddressId === address.id && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "rounded-full bg-primary/10 px-2 py-0.5 text-[11px] font-bold text-primary", children: "Selecionado" })
3358
- ] }),
3359
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "mt-1 block text-muted-foreground", children: address.street }),
3360
- /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "block text-muted-foreground", children: [
3361
- address.city,
3362
- " - ",
3363
- address.zip
3364
- ] })
3828
+ children: /* @__PURE__ */ jsxRuntime.jsxs("button", { type: "button", onClick: () => selectAddress(address.id), className: "block w-full text-left", children: [
3829
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex items-center justify-between gap-3 font-semibold", children: [
3830
+ address.label,
3831
+ addressMode === "saved" && selectedAddressId === address.id && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "rounded-full bg-primary/10 px-2 py-0.5 text-[11px] font-bold text-primary", children: "Selecionado" })
3365
3832
  ] }),
3366
- /* @__PURE__ */ jsxRuntime.jsx(
3367
- "button",
3368
- {
3369
- type: "button",
3370
- onClick: () => removeAddress(address.id),
3371
- className: "absolute right-3 top-3 inline-flex h-8 w-8 items-center justify-center rounded-full text-muted-foreground opacity-0 transition hover:bg-destructive/10 hover:text-destructive focus:opacity-100 group-hover:opacity-100",
3372
- "aria-label": `Remover endere\xE7o ${address.label}`,
3373
- title: "Remover endere\xE7o",
3374
- children: /* @__PURE__ */ jsxRuntime.jsx(LucideIcons.Trash2, { className: "h-4 w-4" })
3375
- }
3376
- )
3377
- ]
3833
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "mt-1 block text-muted-foreground", children: [
3834
+ [address.street, address.number].filter(Boolean).join(", "),
3835
+ address.complement ? ` \u2014 ${address.complement}` : ""
3836
+ ] }),
3837
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "block text-muted-foreground", children: [
3838
+ [address.district, address.city, address.state].filter(Boolean).join(" \xB7 "),
3839
+ " \u2014 ",
3840
+ address.postalCode
3841
+ ] })
3842
+ ] })
3378
3843
  },
3379
3844
  address.id
3380
3845
  )),
@@ -3385,83 +3850,62 @@ function CheckoutPage() {
3385
3850
  onClick: addNewAddress,
3386
3851
  className: `rounded-lg border border-dashed p-3 text-left text-sm transition hover:bg-muted/40 ${addressMode === "new" ? "border-primary bg-primary/5" : "border-border bg-muted/20"}`,
3387
3852
  children: [
3388
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "block font-semibold text-primary", children: "+ Adicionar novo endere\xE7o" }),
3853
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "block font-semibold text-primary", children: savedAddresses.length > 0 ? "+ Adicionar novo endere\xE7o" : "Informe o endere\xE7o de entrega" }),
3389
3854
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "mt-1 block text-muted-foreground", children: "Preencher outro endere\xE7o para esta compra" })
3390
3855
  ]
3391
3856
  }
3392
3857
  )
3393
3858
  ] }),
3394
3859
  addressMode === "new" && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid gap-3", children: [
3395
- field("Endere\xE7o", TID.checkoutStreet, form.street, set("street"), { placeholder: "Endere\xE7o" }),
3396
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid gap-3 sm:grid-cols-[1fr_160px]", children: [
3397
- field("Cidade", TID.checkoutCity, form.city, set("city"), { placeholder: "Cidade" }),
3398
- field("CEP", TID.checkoutZip, form.zip, set("zip"), { placeholder: "CEP" })
3399
- ] })
3860
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid gap-1 sm:grid-cols-[200px_1fr] sm:items-center", children: [
3861
+ field("CEP", TID.checkoutZip, form.zip, setZip, {
3862
+ placeholder: "CEP",
3863
+ inputMode: "numeric",
3864
+ autoComplete: "postal-code"
3865
+ }),
3866
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "px-1 text-xs text-muted-foreground", children: zipLookup === "loading" ? "Buscando endere\xE7o\u2026" : zipLookup === "not-found" ? "CEP n\xE3o encontrado \u2014 preencha o endere\xE7o \xE0 m\xE3o." : "Preenchemos o endere\xE7o para voc\xEA." })
3867
+ ] }),
3868
+ field("Endere\xE7o", TID.checkoutStreet, form.street, set("street"), { placeholder: "Rua, avenida\u2026" }),
3869
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-2 gap-3", children: [
3870
+ field("N\xFAmero", "checkout-number", form.number, set("number"), { placeholder: "N\xFAmero" }),
3871
+ field("Complemento", "checkout-complement", form.complement, set("complement"), { placeholder: "Apto, bloco (opcional)" })
3872
+ ] }),
3873
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-2 gap-3", children: [
3874
+ field("Bairro", "checkout-district", form.district, set("district"), { placeholder: "Bairro" }),
3875
+ field("UF", "checkout-state", form.state, set("state"), { placeholder: "UF" })
3876
+ ] }),
3877
+ field("Cidade", TID.checkoutCity, form.city, set("city"), { placeholder: "Cidade" })
3400
3878
  ] })
3401
3879
  ] }),
3402
3880
  /* @__PURE__ */ jsxRuntime.jsxs("section", { children: [
3403
3881
  /* @__PURE__ */ jsxRuntime.jsx("h2", { className: "mb-3 text-xl font-semibold tracking-tight", children: "Pagamento" }),
3404
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mb-3 grid gap-3", children: [
3405
- savedPaymentMethods.map((method) => /* @__PURE__ */ jsxRuntime.jsxs(
3406
- "div",
3407
- {
3408
- className: `group relative rounded-lg border p-3 pr-11 text-left text-sm transition hover:bg-muted/40 ${selectedPaymentId === method.id ? "border-primary bg-primary/5" : "border-border bg-background"}`,
3409
- children: [
3410
- /* @__PURE__ */ jsxRuntime.jsxs("button", { type: "button", onClick: () => selectPayment(method.id), className: "block w-full text-left", children: [
3411
- /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex items-center justify-between gap-3 font-semibold", children: [
3412
- /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex items-center gap-2", children: [
3413
- /* @__PURE__ */ jsxRuntime.jsx(LucideIcons.CreditCard, { className: "h-4 w-4" }),
3414
- method.label
3415
- ] }),
3416
- paymentMode === "saved" && selectedPaymentId === method.id && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "rounded-full bg-primary/10 px-2 py-0.5 text-[11px] font-bold text-primary", children: "Selecionado" })
3417
- ] }),
3418
- /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "mt-1 block text-muted-foreground", children: [
3419
- "Expira em ",
3420
- method.expiry
3421
- ] })
3422
- ] }),
3423
- /* @__PURE__ */ jsxRuntime.jsx(
3424
- "button",
3425
- {
3426
- type: "button",
3427
- onClick: () => removePayment(method.id),
3428
- className: "absolute right-3 top-3 inline-flex h-8 w-8 items-center justify-center rounded-full text-muted-foreground opacity-0 transition hover:bg-destructive/10 hover:text-destructive focus:opacity-100 group-hover:opacity-100",
3429
- "aria-label": `Remover cart\xE3o ${method.label}`,
3430
- title: "Remover cart\xE3o",
3431
- children: /* @__PURE__ */ jsxRuntime.jsx(LucideIcons.Trash2, { className: "h-4 w-4" })
3432
- }
3433
- )
3434
- ]
3435
- },
3436
- method.id
3437
- )),
3438
- /* @__PURE__ */ jsxRuntime.jsxs(
3882
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mb-3 grid gap-3", role: "radiogroup", "aria-label": "Forma de pagamento", children: paymentMethods.map((method) => {
3883
+ const copy = PAYMENT_LABELS[method];
3884
+ const selected = paymentMethod === method;
3885
+ return /* @__PURE__ */ jsxRuntime.jsxs(
3439
3886
  "button",
3440
3887
  {
3441
3888
  type: "button",
3442
- onClick: addNewPayment,
3443
- className: `rounded-lg border border-dashed p-3 text-left text-sm transition hover:bg-muted/40 ${paymentMode === "new" ? "border-primary bg-primary/5" : "border-border bg-muted/20"}`,
3889
+ role: "radio",
3890
+ "aria-checked": selected,
3891
+ "data-testid": `checkout-payment-${method}`,
3892
+ onClick: () => setPaymentMethod(method),
3893
+ className: `rounded-lg border p-3 text-left text-sm transition hover:bg-muted/40 ${selected ? "border-primary bg-primary/5" : "border-border bg-background"}`,
3444
3894
  children: [
3445
- /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex items-center gap-2 font-semibold text-primary", children: [
3446
- /* @__PURE__ */ jsxRuntime.jsx(LucideIcons.CreditCard, { className: "h-4 w-4" }),
3447
- "+ Adicionar novo cart\xE3o"
3895
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex items-center justify-between gap-3 font-semibold", children: [
3896
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex items-center gap-2", children: [
3897
+ /* @__PURE__ */ jsxRuntime.jsx(LucideIcons.CreditCard, { className: "h-4 w-4" }),
3898
+ copy.label
3899
+ ] }),
3900
+ selected && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "rounded-full bg-primary/10 px-2 py-0.5 text-[11px] font-bold text-primary", children: "Selecionado" })
3448
3901
  ] }),
3449
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "mt-1 block text-muted-foreground", children: "Usar outro cart\xE3o nesta compra" })
3902
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "mt-1 block text-muted-foreground", children: copy.hint })
3450
3903
  ]
3451
- }
3452
- )
3453
- ] }),
3454
- paymentMode === "new" && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid gap-3", children: [
3455
- field("N\xFAmero do cart\xE3o", TID.checkoutCard, form.card, set("card"), {
3456
- placeholder: "N\xFAmero do cart\xE3o",
3457
- inputMode: "numeric"
3458
- }),
3459
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid gap-3 sm:grid-cols-2", children: [
3460
- field("Validade", TID.checkoutExpiry, form.expiry, set("expiry"), { placeholder: "MM/AA" }),
3461
- field("CVC", TID.checkoutCvc, form.cvc, set("cvc"), { placeholder: "CVC", inputMode: "numeric" })
3462
- ] })
3463
- ] }),
3464
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-2 text-xs text-muted-foreground", children: "Pagamento de demonstra\xE7\xE3o - nenhuma cobran\xE7a \xE9 feita." })
3904
+ },
3905
+ method
3906
+ );
3907
+ }) }),
3908
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-2 text-xs text-muted-foreground", children: "Nenhuma cobran\xE7a \xE9 feita agora. Seu pedido \xE9 registrado e a loja confirma o pagamento." })
3465
3909
  ] }),
3466
3910
  error && /* @__PURE__ */ jsxRuntime.jsx("p", { "data-testid": TID.checkoutError, className: "rounded-lg border border-destructive/30 bg-destructive/5 px-3 py-2 text-sm text-destructive", children: error })
3467
3911
  ] }),
@@ -3815,11 +4259,11 @@ function SuccessMark() {
3815
4259
  }
3816
4260
  function OrderConfirmationPage({ orderId }) {
3817
4261
  const config = useStorefrontConfig();
3818
- const [order, setOrder] = React6.useState(null);
3819
- const [loading, setLoading] = React6.useState(true);
4262
+ const [order, setOrder] = React7.useState(null);
4263
+ const [loading, setLoading] = React7.useState(true);
3820
4264
  const money = (v) => formatMoney(v, config.currency, config.locale);
3821
4265
  useStorefrontHead({ title: order ? `Pedido #${order.orderNumber} \u2014 ${config.name}` : config.name });
3822
- React6.useEffect(() => {
4266
+ React7.useEffect(() => {
3823
4267
  let cancelled = false;
3824
4268
  runtime.getShopProvider().getOrder(orderId).then((o) => {
3825
4269
  if (!cancelled) setOrder(o);
@@ -3951,11 +4395,11 @@ function OrderConfirmationPage({ orderId }) {
3951
4395
  function useMyOrders() {
3952
4396
  const customerId = useSessionStore((s) => s.customerId);
3953
4397
  const email = useSessionStore((s) => s.email);
3954
- const [orders, setOrders] = React6.useState([]);
3955
- const [loading, setLoading] = React6.useState(true);
3956
- const [tick, setTick] = React6.useState(0);
3957
- const refresh = React6.useCallback(() => setTick((t) => t + 1), []);
3958
- React6.useEffect(() => {
4398
+ const [orders, setOrders] = React7.useState([]);
4399
+ const [loading, setLoading] = React7.useState(true);
4400
+ const [tick, setTick] = React7.useState(0);
4401
+ const refresh = React7.useCallback(() => setTick((t) => t + 1), []);
4402
+ React7.useEffect(() => {
3959
4403
  let cancelled = false;
3960
4404
  if (!customerId && !email) {
3961
4405
  setOrders([]);
@@ -3977,12 +4421,12 @@ function useMyOrders() {
3977
4421
  return { orders, loading, refresh };
3978
4422
  }
3979
4423
  function AuthForm() {
3980
- const [mode, setMode] = React6.useState("signin");
3981
- const [email, setEmail] = React6.useState("");
3982
- const [password, setPassword] = React6.useState("");
3983
- const [name, setName] = React6.useState("");
3984
- const [busy, setBusy] = React6.useState(false);
3985
- const [error, setError] = React6.useState(null);
4424
+ const [mode, setMode] = React7.useState("signin");
4425
+ const [email, setEmail] = React7.useState("");
4426
+ const [password, setPassword] = React7.useState("");
4427
+ const [name, setName] = React7.useState("");
4428
+ const [busy, setBusy] = React7.useState(false);
4429
+ const [error, setError] = React7.useState(null);
3986
4430
  async function submit(e) {
3987
4431
  e.preventDefault();
3988
4432
  setError(null);
@@ -4124,9 +4568,9 @@ function OrdersPanel() {
4124
4568
  }
4125
4569
  function ProfilePanel() {
4126
4570
  const session = useSessionStore();
4127
- const [name, setName] = React6.useState(session.name ?? "");
4128
- const [saving, setSaving] = React6.useState(false);
4129
- const [saved, setSaved] = React6.useState(false);
4571
+ const [name, setName] = React7.useState(session.name ?? "");
4572
+ const [saving, setSaving] = React7.useState(false);
4573
+ const [saved, setSaved] = React7.useState(false);
4130
4574
  async function save(e) {
4131
4575
  e.preventDefault();
4132
4576
  const trimmed = name.trim();
@@ -4222,7 +4666,7 @@ var SECTION_SUBTITLE = {
4222
4666
  function MyPurchasesPage() {
4223
4667
  const config = useStorefrontConfig();
4224
4668
  const session = useSessionStore();
4225
- const [section, setSection] = React6.useState("orders");
4669
+ const [section, setSection] = React7.useState("orders");
4226
4670
  useStorefrontHead({ title: `Minha conta \u2014 ${config.name}` });
4227
4671
  if (!session.email) {
4228
4672
  return /* @__PURE__ */ jsxRuntime.jsx("main", { className: "mx-auto max-w-3xl px-4 py-12 sm:px-6", children: /* @__PURE__ */ jsxRuntime.jsx(AuthForm, {}) });
@@ -4285,7 +4729,7 @@ function PolicyPage({ kind }) {
4285
4729
  ] }) })
4286
4730
  ] });
4287
4731
  }
4288
- var StorefrontErrorBoundary = class extends React6__default.default.Component {
4732
+ var StorefrontErrorBoundary = class extends React7__default.default.Component {
4289
4733
  constructor() {
4290
4734
  super(...arguments);
4291
4735
  this.state = { error: null };
@@ -4329,7 +4773,7 @@ var ACCENT = {
4329
4773
  function ToastCard({ toast: toast2 }) {
4330
4774
  const dismiss = useToastStore((s) => s.dismiss);
4331
4775
  const Icon2 = ICON[toast2.variant];
4332
- React6.useEffect(() => {
4776
+ React7.useEffect(() => {
4333
4777
  const timer = window.setTimeout(() => dismiss(toast2.id), toast2.durationMs);
4334
4778
  return () => window.clearTimeout(timer);
4335
4779
  }, [toast2.id, toast2.durationMs, dismiss]);
@@ -4440,6 +4884,15 @@ function initStorefrontRuntime(config) {
4440
4884
  } else {
4441
4885
  runtime.setShopProvider(mock.createMockShopProvider(buildMockSeed(config)));
4442
4886
  }
4887
+ const reconcileCart = () => {
4888
+ const provider = runtime.getShopProvider();
4889
+ void useCartStore.getState().reconcile(async (line) => {
4890
+ const product = await provider.getProduct(line.productId);
4891
+ return product != null;
4892
+ });
4893
+ };
4894
+ if (useCartStore.persist.hasHydrated()) reconcileCart();
4895
+ else useCartStore.persist.onFinishHydration(reconcileCart);
4443
4896
  if (config.supabaseUrl || config.supabaseAnonKey) {
4444
4897
  console.warn(
4445
4898
  "@fayz-ai/shop: supabaseUrl/supabaseAnonKey are legacy fields. Pass an explicit provider/adapter or use the Fayz SDK broker path."
@@ -4571,9 +5024,9 @@ function manifestToStorefrontConfig(manifest, surfaceName) {
4571
5024
  };
4572
5025
  }
4573
5026
  function StorefrontScaffold({ manifest, surface }) {
4574
- const config = React6__default.default.useMemo(() => manifestToStorefrontConfig(manifest, surface), [manifest, surface]);
4575
- const resolved = React6__default.default.useMemo(() => resolveConfig(config), [config]);
4576
- const inited = React6__default.default.useRef(false);
5027
+ const config = React7__default.default.useMemo(() => manifestToStorefrontConfig(manifest, surface), [manifest, surface]);
5028
+ const resolved = React7__default.default.useMemo(() => resolveConfig(config), [config]);
5029
+ const inited = React7__default.default.useRef(false);
4577
5030
  if (!inited.current) {
4578
5031
  initStorefrontRuntime(config);
4579
5032
  inited.current = true;
@@ -4799,11 +5252,73 @@ var atelierTemplate = {
4799
5252
  ]
4800
5253
  })
4801
5254
  };
5255
+ var foodTemplate = {
5256
+ id: "food",
5257
+ inspiration: "Delivery / restaurant storefront",
5258
+ announcement: "ENTREGA GR\xC1TIS ACIMA DE R$ 150 \xB7 PE\xC7A J\xC1",
5259
+ theme: {
5260
+ name: "food",
5261
+ colors: {
5262
+ background: "30 33% 97%",
5263
+ foreground: "20 14% 15%",
5264
+ primary: "14 84% 50%",
5265
+ primaryForeground: "0 0% 100%",
5266
+ card: "30 33% 99%",
5267
+ cardForeground: "20 14% 15%",
5268
+ muted: "28 20% 92%",
5269
+ mutedForeground: "22 10% 42%",
5270
+ border: "28 18% 85%",
5271
+ headerBackground: "20 14% 11%",
5272
+ headerForeground: "30 24% 96%",
5273
+ announcementBackground: "14 84% 50%",
5274
+ announcementForeground: "0 0% 100%"
5275
+ },
5276
+ font: { heading: "Poppins", body: "Inter" },
5277
+ radius: "round",
5278
+ header: { variant: "classic" },
5279
+ productCard: { style: "card", imageAspect: "square" }
5280
+ },
5281
+ home: (storeName, images) => ({
5282
+ sections: [
5283
+ {
5284
+ type: "hero",
5285
+ variant: "banner",
5286
+ height: "tall",
5287
+ slides: [
5288
+ { title: "Sabor de verdade, na sua porta", subtitle: `${storeName} \u2014 feito na hora, do jeito que voc\xEA ama.`, cta: "Ver card\xE1pio", href: "/catalog", hue: 20, image: images?.hero?.[0] }
5289
+ ]
5290
+ },
5291
+ {
5292
+ type: "benefits",
5293
+ items: [
5294
+ { icon: "Flame", title: "Feito na hora", text: "Preparado com capricho" },
5295
+ { icon: "Truck", title: "Entrega r\xE1pida", text: "Frete gr\xE1tis acima de R$ 150" },
5296
+ { icon: "CreditCard", title: "Pagamento f\xE1cil", text: "Pix, cart\xE3o e mais" }
5297
+ ]
5298
+ },
5299
+ { type: "categories", style: "tiles", title: "Nosso card\xE1pio" },
5300
+ { type: "products", title: "Mais pedidos", eyebrow: "favoritos da casa", filter: "all", limit: 8 },
5301
+ { type: "banner", title: "Combo do dia", eyebrow: "oferta", subtitle: "Sele\xE7\xE3o especial com desconto", cta: "Aproveitar", hue: 14, image: images?.banners?.[0] },
5302
+ { type: "products", title: "Novidades no card\xE1pio", filter: "new", limit: 4 },
5303
+ {
5304
+ type: "testimonials",
5305
+ title: "O que dizem por a\xED",
5306
+ items: [
5307
+ { quote: "Chegou quentinho e absurdo de bom. Virei cliente!", author: "Rafael M." },
5308
+ { quote: "Sabor de restaurante em casa, sem complica\xE7\xE3o.", author: "Beatriz L." },
5309
+ { quote: "Entrega r\xE1pida e por\xE7\xE3o generosa. Recomendo demais.", author: "Diego S." }
5310
+ ]
5311
+ },
5312
+ { type: "newsletter", title: "Receba nossas ofertas", subtitle: "Promo\xE7\xF5es e novidades do card\xE1pio, direto no seu e-mail." }
5313
+ ]
5314
+ })
5315
+ };
4802
5316
  var storefrontTemplates = {
4803
5317
  mare: mareTemplate,
4804
5318
  sertao: sertaoTemplate,
4805
5319
  volt: voltTemplate,
4806
- atelier: atelierTemplate
5320
+ atelier: atelierTemplate,
5321
+ food: foodTemplate
4807
5322
  };
4808
5323
 
4809
5324
  Object.defineProperty(exports, "getSupabaseClientOptional", {
@@ -4860,6 +5375,7 @@ exports.defineStorefrontConfig = defineStorefrontConfig;
4860
5375
  exports.defineStorefrontRoutes = defineStorefrontRoutes;
4861
5376
  exports.defineStorefrontSections = defineStorefrontSections;
4862
5377
  exports.establishCustomerSession = establishCustomerSession;
5378
+ exports.foodTemplate = foodTemplate;
4863
5379
  exports.formatMoney = formatMoney;
4864
5380
  exports.formatProductOptionSelection = formatProductOptionSelection;
4865
5381
  exports.getCustomerAuthAdapter = getCustomerAuthAdapter;