@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.js CHANGED
@@ -1,13 +1,14 @@
1
- import { setShopProvider, getShopProvider } from '@fayz-ai/shop/runtime';
1
+ import { getShopProvider, setShopProvider } from '@fayz-ai/shop/runtime';
2
2
  import { createMockShopProvider } from '@fayz-ai/shop/mock';
3
3
  import { createAuthRuntime, createMockAuthAdapter } from '@fayz-ai/plugin-auth';
4
+ import { setShopAccessTokenResolver } from '@fayz-ai/sdk/shop';
4
5
  import { create } from 'zustand';
5
6
  import { persist } from 'zustand/middleware';
6
- import React6, { createContext, useState, useRef, useEffect, useMemo, useContext, useCallback } from 'react';
7
- import { hashRouterAdapter, registerScaffold, registerBlock, renderBlocks, defineApp } from '@fayz-ai/core';
7
+ import React7, { createContext, useState, useRef, useEffect, useMemo, useContext, useCallback } from 'react';
8
+ import { hashRouterAdapter, normalizePostalCode, lookupPostalCode, registerScaffold, registerBlock, formatPostalCode, renderBlocks, defineApp } from '@fayz-ai/core';
8
9
  export { getSupabaseClientOptional, renderApp } from '@fayz-ai/core';
9
10
  import * as LucideIcons from 'lucide-react';
10
- import { ChevronLeft, ChevronRight, ShoppingBag, X, Trash2, Phone, Mail, Search, User, UserCircle, Package, CreditCard, LogOut, Truck, RefreshCcw, ShieldCheck, Lock, Minus, Plus, Info, AlertCircle, Check, Send, MessageCircle, MapPin, PackageCheck, QrCode, XCircle, RotateCcw, Clock } from 'lucide-react';
11
+ import { ChevronLeft, ChevronRight, ShoppingBag, X, Trash2, Phone, Mail, Search, User, UserCircle, Package, CreditCard, LogOut, Truck, RefreshCcw, ShieldCheck, Lock, Minus, Plus, MapPin, Info, AlertCircle, Check, Send, MessageCircle, PackageCheck, QrCode, XCircle, RotateCcw, Clock } from 'lucide-react';
11
12
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
12
13
  import { validateDiscount } from '@fayz-ai/shop';
13
14
 
@@ -53,10 +54,23 @@ function resolveAuthAdapter(configured, fallback) {
53
54
  }
54
55
  }).adapter;
55
56
  }
57
+ var _accessToken = null;
58
+ var _hadAuthUser = false;
59
+ async function syncAccessToken(adapter2) {
60
+ try {
61
+ const existing = await adapter2.getSession();
62
+ _accessToken = existing?.session.accessToken ?? null;
63
+ } catch {
64
+ _accessToken = null;
65
+ }
66
+ }
56
67
  function initCustomerAuth(adapter2) {
57
68
  _adapter = adapter2;
69
+ setShopAccessTokenResolver(() => _accessToken);
58
70
  adapter2.getSession().then((existing) => {
71
+ _accessToken = existing?.session.accessToken ?? null;
59
72
  if (!existing) return;
73
+ _hadAuthUser = true;
60
74
  const store = useSessionStore.getState();
61
75
  if (store.email !== existing.user.email) {
62
76
  void linkCustomer(existing.user);
@@ -64,7 +78,16 @@ function initCustomerAuth(adapter2) {
64
78
  }).catch(() => {
65
79
  });
66
80
  adapter2.onAuthStateChange((user) => {
67
- if (!user) useSessionStore.getState().signOut();
81
+ if (!user) {
82
+ _accessToken = null;
83
+ if (_hadAuthUser) {
84
+ _hadAuthUser = false;
85
+ useSessionStore.getState().signOut();
86
+ }
87
+ return;
88
+ }
89
+ _hadAuthUser = true;
90
+ void syncAccessToken(adapter2);
68
91
  });
69
92
  }
70
93
  function getCustomerAuthAdapter() {
@@ -100,6 +123,8 @@ async function establishCustomerSession(email, opts) {
100
123
  if (opts?.password) {
101
124
  const adapter2 = getCustomerAuthAdapter();
102
125
  const session = await adapter2.signIn(normalized, opts.password);
126
+ _accessToken = session.accessToken ?? null;
127
+ _hadAuthUser = true;
103
128
  return linkCustomer(session.user.email ? session.user : { ...session.user, email: normalized }, opts.name);
104
129
  }
105
130
  return linkCustomer({ email: normalized }, opts?.name);
@@ -111,12 +136,16 @@ async function signUpCustomer(email, password, name) {
111
136
  const adapter2 = getCustomerAuthAdapter();
112
137
  const normalized = email.trim().toLowerCase();
113
138
  const session = await adapter2.signUp(normalized, password, name);
139
+ _accessToken = session.accessToken ?? null;
140
+ _hadAuthUser = true;
114
141
  return linkCustomer(session.user.email ? session.user : { ...session.user, email: normalized }, name);
115
142
  }
116
143
  async function signOutCustomer() {
117
144
  try {
118
145
  await getCustomerAuthAdapter().signOut();
119
146
  } finally {
147
+ _accessToken = null;
148
+ _hadAuthUser = false;
120
149
  useSessionStore.getState().signOut();
121
150
  }
122
151
  }
@@ -129,7 +158,10 @@ function resolveConfig(config) {
129
158
  currency: config.currency ?? "BRL",
130
159
  locale: config.locale ?? "pt-BR",
131
160
  shipping: { flatRate: config.shipping?.flatRate ?? 0, freeAbove: config.shipping?.freeAbove },
132
- payments: { mode: config.payments?.mode ?? "mock" },
161
+ payments: {
162
+ mode: config.payments?.mode ?? "mock",
163
+ methods: config.payments?.methods?.length ? config.payments.methods : ["pix", "credit_card", "cash"]
164
+ },
133
165
  commerceMode,
134
166
  enquiry: {
135
167
  label: config.enquiry?.label ?? "Contact me",
@@ -204,93 +236,7 @@ function matchPath(pattern, path) {
204
236
  return params;
205
237
  }
206
238
  function Link({ to, children, ...rest }) {
207
- return React6.createElement("a", { href: `#${to}`, ...rest }, children);
208
- }
209
- var RADIUS_MAP = {
210
- none: { button: "0px", card: "0px", input: "0px" },
211
- soft: { button: "6px", card: "10px", input: "6px" },
212
- round: { button: "10px", card: "16px", input: "10px" },
213
- pill: { button: "999px", card: "20px", input: "999px" }
214
- };
215
- var COLOR_VAR_MAP = {
216
- background: "--background",
217
- foreground: "--foreground",
218
- primary: "--primary",
219
- primaryForeground: "--primary-foreground",
220
- card: "--card",
221
- cardForeground: "--card-foreground",
222
- muted: "--muted",
223
- mutedForeground: "--muted-foreground",
224
- border: "--border",
225
- headerBackground: "--sf-header-bg",
226
- headerForeground: "--sf-header-fg",
227
- announcementBackground: "--sf-announcement-bg",
228
- announcementForeground: "--sf-announcement-fg"
229
- };
230
- function themeToCss(theme) {
231
- const lines = [];
232
- for (const [key, cssVar] of Object.entries(COLOR_VAR_MAP)) {
233
- const value = theme.colors?.[key];
234
- if (value) lines.push(`${cssVar}: ${value};`);
235
- }
236
- if (!theme.colors?.headerBackground && theme.colors?.background) {
237
- lines.push(`--sf-header-bg: ${theme.colors.background};`);
238
- }
239
- if (!theme.colors?.headerForeground && theme.colors?.foreground) {
240
- lines.push(`--sf-header-fg: ${theme.colors.foreground};`);
241
- }
242
- if (theme.font) {
243
- const fallback = theme.font.fallback ?? "sans-serif";
244
- lines.push(`--font-family: '${theme.font.body}', ${fallback};`);
245
- lines.push(`--sf-font-heading: '${theme.font.heading}', ${fallback};`);
246
- }
247
- const radius = RADIUS_MAP[theme.radius ?? "soft"];
248
- lines.push(`--button-radius: ${radius.button};`);
249
- lines.push(`--card-radius: ${radius.card};`);
250
- lines.push(`--input-radius: ${radius.input};`);
251
- lines.push(`--sf-radius-button: ${radius.button};`);
252
- lines.push(`--sf-radius-card: ${radius.card};`);
253
- lines.push(`--sf-radius-input: ${radius.input};`);
254
- let css = `:root {
255
- ${lines.join("\n ")}
256
- }
257
- `;
258
- css += `body { font-family: var(--font-family); }
259
- `;
260
- css += `h1, h2, h3, h4, .sf-heading { font-family: var(--sf-font-heading, var(--font-family)); }
261
- `;
262
- if (theme.uppercaseButtons) {
263
- css += `.sf-cta { text-transform: uppercase; letter-spacing: 0.08em; }
264
- `;
265
- }
266
- if (theme.header?.uppercaseNav) {
267
- css += `.sf-nav-link { text-transform: uppercase; letter-spacing: 0.06em; font-size: 0.8rem; }
268
- `;
269
- }
270
- return css;
271
- }
272
- function googleFontsHref(theme) {
273
- if (!theme.font) return null;
274
- const families = theme.font.googleFonts ?? [theme.font.heading, theme.font.body];
275
- const unique = [...new Set(families)];
276
- const params = unique.map((f) => `family=${encodeURIComponent(f).replace(/%20/g, "+")}:wght@300;400;500;600;700`).join("&");
277
- return `https://fonts.googleapis.com/css2?${params}&display=swap`;
278
- }
279
- function StorefrontThemeStyle({ theme }) {
280
- const fontsHref = googleFontsHref(theme);
281
- useEffect(() => {
282
- if (!fontsHref) return;
283
- const id = "sf-theme-fonts";
284
- let link = document.getElementById(id);
285
- if (!link) {
286
- link = document.createElement("link");
287
- link.id = id;
288
- link.rel = "stylesheet";
289
- document.head.appendChild(link);
290
- }
291
- link.href = fontsHref;
292
- }, [fontsHref]);
293
- return React6.createElement("style", { "data-sf-theme": theme.name }, themeToCss(theme));
239
+ return React7.createElement("a", { href: `#${to}`, ...rest }, children);
294
240
  }
295
241
 
296
242
  // src/format.ts
@@ -300,6 +246,87 @@ function formatMoney(value, currency = "BRL", locale = "pt-BR") {
300
246
  function roundCents(value) {
301
247
  return Math.round(value * 100) / 100;
302
248
  }
249
+ var EMPTY = {
250
+ postalCode: "",
251
+ address: null,
252
+ options: [],
253
+ selectedZoneId: null,
254
+ status: "idle",
255
+ error: null
256
+ };
257
+ var useDeliveryStore = create()(
258
+ persist(
259
+ (set, get) => ({
260
+ ...EMPTY,
261
+ async resolve(postalCode, subtotal) {
262
+ const code = normalizePostalCode(postalCode);
263
+ if (code.length !== 8) {
264
+ set({ ...EMPTY, postalCode, status: "error", error: "Digite os 8 d\xEDgitos do CEP." });
265
+ return;
266
+ }
267
+ set({ postalCode: code, status: "loading", error: null });
268
+ let options = [];
269
+ try {
270
+ options = await getShopProvider().quoteShipping?.(code, subtotal) ?? [];
271
+ } catch {
272
+ set({
273
+ ...EMPTY,
274
+ postalCode: code,
275
+ status: "error",
276
+ error: "N\xE3o foi poss\xEDvel calcular o frete agora. Tente de novo."
277
+ });
278
+ return;
279
+ }
280
+ let address = null;
281
+ let notFound = false;
282
+ try {
283
+ address = await lookupPostalCode(code);
284
+ notFound = address === null;
285
+ } catch {
286
+ address = null;
287
+ }
288
+ set({
289
+ postalCode: code,
290
+ address,
291
+ options,
292
+ selectedZoneId: options[0]?.zoneId ?? null,
293
+ // A store with no zones configured quotes nothing, and that must NOT
294
+ // read as "we don't deliver here" — it falls back to the store-wide
295
+ // flat rate, which the cart already knows how to show.
296
+ status: options.length > 0 ? "served" : "unserved",
297
+ // Reported alongside the price, not instead of it: the CEP may be
298
+ // mistyped even though its range is covered.
299
+ error: notFound ? "CEP n\xE3o encontrado. Confira os n\xFAmeros." : null
300
+ });
301
+ },
302
+ selectZone(zoneId) {
303
+ if (get().options.some((option) => option.zoneId === zoneId)) set({ selectedZoneId: zoneId });
304
+ },
305
+ clear() {
306
+ set({ ...EMPTY });
307
+ }
308
+ }),
309
+ {
310
+ name: "fayz.storefront.delivery.v1",
311
+ // Deliberately NOT persisting `options`: a rehydrated
312
+ // price would be shown as current without anything having quoted it. The
313
+ // address survives, the money is asked for again.
314
+ partialize: (state) => ({ postalCode: state.postalCode, address: state.address }),
315
+ onRehydrateStorage: () => (state) => {
316
+ if (state?.address) state.status = "idle";
317
+ }
318
+ }
319
+ )
320
+ );
321
+ function resolveOptionRate(option, subtotal) {
322
+ if (option.freeAbove != null && subtotal >= option.freeAbove) return 0;
323
+ return option.baseRate;
324
+ }
325
+ var selectQuotedShipping = (state, subtotal) => {
326
+ if (state.status !== "served") return null;
327
+ const option = state.options.find((o) => o.zoneId === state.selectedZoneId) ?? state.options[0];
328
+ return option ? resolveOptionRate(option, subtotal) : null;
329
+ };
303
330
 
304
331
  // src/product-options.ts
305
332
  function cleanValues(value) {
@@ -344,7 +371,7 @@ function productOptionSelectionKey(selection) {
344
371
  // src/stores/cart.store.ts
345
372
  var useCartStore = create()(
346
373
  persist(
347
- (set) => ({
374
+ (set, get) => ({
348
375
  lines: [],
349
376
  discountCode: null,
350
377
  discountPercent: 0,
@@ -396,7 +423,16 @@ var useCartStore = create()(
396
423
  clearDiscount: () => set({ discountCode: null, discountPercent: 0 }),
397
424
  clear: () => set({ lines: [], discountCode: null, discountPercent: 0 }),
398
425
  openDrawer: () => set({ isOpen: true }),
399
- closeDrawer: () => set({ isOpen: false, justAddedLineId: null })
426
+ closeDrawer: () => set({ isOpen: false, justAddedLineId: null }),
427
+ reconcile: async (resolve) => {
428
+ const lines = get().lines;
429
+ if (lines.length === 0) return;
430
+ const keep = await Promise.all(
431
+ lines.map((line) => resolve(line).catch(() => true))
432
+ );
433
+ const kept = lines.filter((_, i) => keep[i]);
434
+ if (kept.length !== lines.length) set({ lines: kept });
435
+ }
400
436
  }),
401
437
  {
402
438
  name: "fayz.storefront.cart.v1",
@@ -414,10 +450,108 @@ var selectDiscountTotal = (s) => roundCents(selectSubtotal(s) * (s.discountPerce
414
450
  var selectShipping = (s, cfg) => {
415
451
  if (s.lines.length === 0) return 0;
416
452
  const subtotal = selectSubtotal(s);
453
+ const quoted = selectQuotedShipping(useDeliveryStore.getState(), subtotal);
454
+ if (quoted != null) return quoted;
417
455
  if (cfg.shipping.freeAbove != null && subtotal >= cfg.shipping.freeAbove) return 0;
418
456
  return cfg.shipping.flatRate;
419
457
  };
420
458
  var selectTotal = (s, cfg) => roundCents(selectSubtotal(s) - selectDiscountTotal(s) + selectShipping(s, cfg));
459
+ var RADIUS_MAP = {
460
+ none: { button: "0px", card: "0px", input: "0px" },
461
+ soft: { button: "6px", card: "10px", input: "6px" },
462
+ round: { button: "10px", card: "16px", input: "10px" },
463
+ pill: { button: "999px", card: "20px", input: "999px" }
464
+ };
465
+ var COLOR_VAR_MAP = {
466
+ background: "--background",
467
+ foreground: "--foreground",
468
+ primary: "--primary",
469
+ primaryForeground: "--primary-foreground",
470
+ card: "--card",
471
+ cardForeground: "--card-foreground",
472
+ muted: "--muted",
473
+ mutedForeground: "--muted-foreground",
474
+ border: "--border",
475
+ headerBackground: "--sf-header-bg",
476
+ headerForeground: "--sf-header-fg",
477
+ announcementBackground: "--sf-announcement-bg",
478
+ announcementForeground: "--sf-announcement-fg",
479
+ accent: "--sf-accent",
480
+ accentForeground: "--sf-accent-foreground"
481
+ };
482
+ function themeToCss(theme) {
483
+ const lines = [];
484
+ for (const [key, cssVar] of Object.entries(COLOR_VAR_MAP)) {
485
+ const value = theme.colors?.[key];
486
+ if (value) lines.push(`${cssVar}: ${value};`);
487
+ }
488
+ if (!theme.colors?.headerBackground && theme.colors?.background) {
489
+ lines.push(`--sf-header-bg: ${theme.colors.background};`);
490
+ }
491
+ if (!theme.colors?.headerForeground && theme.colors?.foreground) {
492
+ lines.push(`--sf-header-fg: ${theme.colors.foreground};`);
493
+ }
494
+ if (theme.font) {
495
+ const fallback = theme.font.fallback ?? "sans-serif";
496
+ lines.push(`--font-family: '${theme.font.body}', ${fallback};`);
497
+ lines.push(`--sf-font-heading: '${theme.font.heading}', ${fallback};`);
498
+ if (theme.font.display) lines.push(`--sf-font-display: '${theme.font.display}', ${fallback};`);
499
+ if (theme.font.serif) lines.push(`--sf-font-serif: '${theme.font.serif}', serif;`);
500
+ }
501
+ for (const [key, value] of Object.entries(theme.tokens ?? {})) {
502
+ const name = key.startsWith("--") ? key : `--${key}`;
503
+ lines.push(`${name}: ${value};`);
504
+ }
505
+ const radius = RADIUS_MAP[theme.radius ?? "soft"];
506
+ lines.push(`--button-radius: ${radius.button};`);
507
+ lines.push(`--card-radius: ${radius.card};`);
508
+ lines.push(`--input-radius: ${radius.input};`);
509
+ lines.push(`--sf-radius-button: ${radius.button};`);
510
+ lines.push(`--sf-radius-card: ${radius.card};`);
511
+ lines.push(`--sf-radius-input: ${radius.input};`);
512
+ let css = `:root {
513
+ ${lines.join("\n ")}
514
+ }
515
+ `;
516
+ css += `body { font-family: var(--font-family); }
517
+ `;
518
+ css += `h1, h2, h3, h4, .sf-heading { font-family: var(--sf-font-heading, var(--font-family)); }
519
+ `;
520
+ if (theme.uppercaseButtons) {
521
+ css += `.sf-cta { text-transform: uppercase; letter-spacing: 0.08em; }
522
+ `;
523
+ }
524
+ if (theme.header?.uppercaseNav) {
525
+ css += `.sf-nav-link { text-transform: uppercase; letter-spacing: 0.06em; font-size: 0.8rem; }
526
+ `;
527
+ }
528
+ return css;
529
+ }
530
+ function googleFontsHref(theme) {
531
+ if (!theme.font) return null;
532
+ const families = theme.font.googleFonts ?? [theme.font.heading, theme.font.body, theme.font.display, theme.font.serif].filter(
533
+ (f) => Boolean(f)
534
+ );
535
+ const unique = [...new Set(families)];
536
+ const params = unique.map((f) => `family=${encodeURIComponent(f).replace(/%20/g, "+")}:wght@300;400;500;600;700`).join("&");
537
+ return `https://fonts.googleapis.com/css2?${params}&display=swap`;
538
+ }
539
+ function StorefrontThemeStyle({ theme }) {
540
+ const fontsHref = googleFontsHref(theme);
541
+ useEffect(() => {
542
+ if (!fontsHref) return;
543
+ const id = "sf-theme-fonts";
544
+ let link = document.getElementById(id);
545
+ if (!link) {
546
+ link = document.createElement("link");
547
+ link.id = id;
548
+ link.rel = "stylesheet";
549
+ document.head.appendChild(link);
550
+ }
551
+ link.href = fontsHref;
552
+ }, [fontsHref]);
553
+ return React7.createElement("style", { "data-sf-theme": theme.name }, themeToCss(theme));
554
+ }
421
555
  var initial = {
422
556
  search: "",
423
557
  categoryId: null,
@@ -526,6 +660,7 @@ function usePopOnChange(value) {
526
660
  var TID = {
527
661
  // header
528
662
  headerSearch: "header-search",
663
+ headerSearchToggle: "header-search-toggle",
529
664
  cartButton: "cart-button",
530
665
  cartCount: "cart-count",
531
666
  accountLink: "account-link",
@@ -581,6 +716,9 @@ var TID = {
581
716
  pdpAddToCart: "pdp-add-to-cart",
582
717
  pdpRoot: "pdp-root",
583
718
  pdpGallery: "pdp-gallery",
719
+ pdpGalleryImage: "pdp-gallery-image",
720
+ pdpGalleryThumbs: "pdp-gallery-thumbs",
721
+ pdpGalleryThumb: "pdp-gallery-thumb",
584
722
  pdpActions: "pdp-actions",
585
723
  pdpEnquiryButton: "pdp-enquiry-button",
586
724
  enquiryForm: "enquiry-form",
@@ -609,6 +747,15 @@ var TID = {
609
747
  cartTotal: "cart-total",
610
748
  goCheckout: "go-checkout",
611
749
  cartEmpty: "cart-empty",
750
+ // delivery estimate (product page + cart)
751
+ deliveryEstimator: "delivery-estimator",
752
+ deliveryCepInput: "delivery-cep-input",
753
+ deliveryCepSubmit: "delivery-cep-submit",
754
+ deliveryOption: "delivery-option",
755
+ deliveryUnserved: "delivery-unserved",
756
+ deliveryError: "delivery-error",
757
+ deliveryAddress: "delivery-address",
758
+ deliveryClear: "delivery-clear",
612
759
  // checkout
613
760
  checkoutEmail: "checkout-email",
614
761
  checkoutName: "checkout-name",
@@ -714,12 +861,26 @@ function UtilityBar() {
714
861
  /* @__PURE__ */ jsx("span", { className: "ml-auto flex items-center gap-4", children: social?.map((s) => /* @__PURE__ */ jsx("a", { href: s.href, target: "_blank", rel: "noreferrer", className: "hover:underline", children: s.label }, s.href)) })
715
862
  ] }) });
716
863
  }
717
- function SearchInput({ className }) {
864
+ function SearchInput({ className, iconOnly = false }) {
718
865
  const config = useStorefrontConfig();
719
866
  const search = useCatalogStore((s) => s.search);
720
867
  const setSearch = useCatalogStore((s) => s.setSearch);
721
868
  const path = useHashPath();
722
- return /* @__PURE__ */ jsxs("div", { className: `relative ${className ?? ""}`, children: [
869
+ const [open, setOpen] = useState(false);
870
+ if (iconOnly && !open) {
871
+ return /* @__PURE__ */ jsx(
872
+ "button",
873
+ {
874
+ type: "button",
875
+ "aria-label": "Buscar produtos",
876
+ "data-testid": TID.headerSearchToggle,
877
+ onClick: () => setOpen(true),
878
+ className: "rounded-full p-2.5 transition-opacity hover:opacity-70",
879
+ children: /* @__PURE__ */ jsx(Search, { className: "h-5 w-5" })
880
+ }
881
+ );
882
+ }
883
+ return /* @__PURE__ */ jsxs("div", { className: `relative ${iconOnly ? "w-full max-w-xs" : className ?? ""}`, children: [
723
884
  /* @__PURE__ */ jsx(Search, { className: "pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 opacity-60" }),
724
885
  /* @__PURE__ */ jsx(
725
886
  "input",
@@ -728,10 +889,17 @@ function SearchInput({ className }) {
728
889
  type: "search",
729
890
  placeholder: "Buscar produtos...",
730
891
  value: search,
892
+ autoFocus: iconOnly,
731
893
  onChange: (e) => {
732
894
  setSearch(e.target.value);
733
895
  if (path !== config.catalogPath) navigateTo(config.catalogPath);
734
896
  },
897
+ onKeyDown: (e) => {
898
+ if (e.key === "Escape" && iconOnly) setOpen(false);
899
+ },
900
+ onBlur: () => {
901
+ if (iconOnly && !search) setOpen(false);
902
+ },
735
903
  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",
736
904
  style: {
737
905
  borderRadius: "var(--sf-radius-input)",
@@ -884,6 +1052,7 @@ function StorefrontHeader() {
884
1052
  const variant = config.theme?.header?.variant ?? "classic";
885
1053
  const scrolled = useScrolled();
886
1054
  const showSearch = config.theme?.header?.showSearch !== false;
1055
+ const iconSearch = config.theme?.header?.searchStyle === "icon";
887
1056
  const logo = /* @__PURE__ */ jsx(Link, { to: "/", className: "sf-heading shrink-0 text-xl font-bold tracking-tight", children: config.logo ?? config.name });
888
1057
  return /* @__PURE__ */ jsxs(
889
1058
  "header",
@@ -897,7 +1066,7 @@ function StorefrontHeader() {
897
1066
  // Rio/Flex pattern: centered logo row, nav row below
898
1067
  /* @__PURE__ */ jsxs(Fragment, { children: [
899
1068
  /* @__PURE__ */ jsxs("div", { className: "mx-auto grid h-16 max-w-7xl grid-cols-[1fr_auto_1fr] items-center px-4 sm:px-6", children: [
900
- showSearch ? /* @__PURE__ */ jsx(SearchInput, { className: "hidden w-full max-w-xs sm:block" }) : /* @__PURE__ */ jsx("div", {}),
1069
+ showSearch ? /* @__PURE__ */ jsx(SearchInput, { iconOnly: iconSearch, className: "hidden w-full max-w-xs sm:block" }) : /* @__PURE__ */ jsx("div", {}),
901
1070
  /* @__PURE__ */ jsx("div", { className: "text-center", children: logo }),
902
1071
  /* @__PURE__ */ jsx("div", { className: "justify-self-end", children: /* @__PURE__ */ jsx(HeaderActions, {}) })
903
1072
  ] }),
@@ -907,7 +1076,7 @@ function StorefrontHeader() {
907
1076
  // Brasília pattern: prominent search left, logo center, actions right; nav row below
908
1077
  /* @__PURE__ */ jsxs(Fragment, { children: [
909
1078
  /* @__PURE__ */ 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: [
910
- showSearch ? /* @__PURE__ */ jsx(SearchInput, { className: "hidden w-full max-w-sm sm:block" }) : /* @__PURE__ */ jsx("div", {}),
1079
+ showSearch ? /* @__PURE__ */ jsx(SearchInput, { iconOnly: iconSearch, className: "hidden w-full max-w-sm sm:block" }) : /* @__PURE__ */ jsx("div", {}),
911
1080
  /* @__PURE__ */ jsx("div", { className: "text-center", children: logo }),
912
1081
  /* @__PURE__ */ jsx("div", { className: "justify-self-end", children: /* @__PURE__ */ jsx(HeaderActions, {}) })
913
1082
  ] }),
@@ -919,17 +1088,19 @@ function StorefrontHeader() {
919
1088
  /* @__PURE__ */ jsx("div", { className: "flex items-center gap-5", children: /* @__PURE__ */ jsx(NavLinks, { className: "hidden sm:flex" }) }),
920
1089
  /* @__PURE__ */ jsx("div", { className: "text-center", children: logo }),
921
1090
  /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-end gap-3", children: [
922
- showSearch && /* @__PURE__ */ jsx(SearchInput, { className: "hidden w-44 lg:block" }),
1091
+ showSearch && /* @__PURE__ */ jsx(SearchInput, { iconOnly: iconSearch, className: "hidden w-44 lg:block" }),
923
1092
  /* @__PURE__ */ jsx(HeaderActions, {})
924
1093
  ] })
925
1094
  ] })
926
1095
  ) : (
927
- // classic: logo left, search center-right, actions right (original layout)
1096
+ // classic: logo left, nav, then a right-aligned search + actions cluster
928
1097
  /* @__PURE__ */ jsxs("div", { className: "mx-auto flex h-16 max-w-7xl items-center gap-6 px-4 sm:px-6", children: [
929
1098
  logo,
930
1099
  /* @__PURE__ */ jsx(NavLinks, { className: "hidden md:flex" }),
931
- showSearch && /* @__PURE__ */ jsx(SearchInput, { className: "ml-auto hidden w-full max-w-sm sm:block" }),
932
- /* @__PURE__ */ jsx(HeaderActions, {})
1100
+ /* @__PURE__ */ jsxs("div", { className: "ml-auto flex items-center gap-2 sm:gap-3", children: [
1101
+ showSearch && /* @__PURE__ */ jsx(SearchInput, { iconOnly: iconSearch, className: iconSearch ? "" : "hidden w-full max-w-sm sm:block" }),
1102
+ /* @__PURE__ */ jsx(HeaderActions, {})
1103
+ ] })
933
1104
  ] })
934
1105
  )
935
1106
  ]
@@ -1103,6 +1274,113 @@ function QuantityInput({ value, onChange, min = 1, max, testId, incTestId, decTe
1103
1274
  )
1104
1275
  ] });
1105
1276
  }
1277
+ function DeliveryEstimator({ compact = false }) {
1278
+ const config = useStorefrontConfig();
1279
+ const cart = useCartStore();
1280
+ const delivery = useDeliveryStore();
1281
+ const [draft, setDraft] = useState(() => formatPostalCode(delivery.postalCode));
1282
+ const subtotal = selectSubtotal(cart);
1283
+ const money = (value) => formatMoney(value, config.currency, config.locale);
1284
+ const loading = delivery.status === "loading";
1285
+ async function submit(event) {
1286
+ event.preventDefault();
1287
+ await delivery.resolve(draft, subtotal);
1288
+ }
1289
+ function eta(option) {
1290
+ const { etaMinDays: min, etaMaxDays: max } = option;
1291
+ if (min == null && max == null) return null;
1292
+ if (min === 0 && (max === 0 || max === 1)) return "hoje ou amanh\xE3";
1293
+ if (min != null && max != null && min !== max) return `${min} a ${max} dias \xFAteis`;
1294
+ return `${max ?? min} dia(s) \xFAtil(eis)`;
1295
+ }
1296
+ return /* @__PURE__ */ jsxs(
1297
+ "section",
1298
+ {
1299
+ "data-testid": TID.deliveryEstimator,
1300
+ className: compact ? "rounded-lg border p-3" : "mt-6 rounded-lg border p-4",
1301
+ children: [
1302
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 text-sm font-semibold", children: [
1303
+ /* @__PURE__ */ jsx(Truck, { className: "h-4 w-4 text-primary" }),
1304
+ "Calcular entrega"
1305
+ ] }),
1306
+ /* @__PURE__ */ jsxs("form", { onSubmit: submit, className: "mt-3 flex gap-2", children: [
1307
+ /* @__PURE__ */ jsx(
1308
+ "input",
1309
+ {
1310
+ "data-testid": TID.deliveryCepInput,
1311
+ "aria-label": "CEP",
1312
+ inputMode: "numeric",
1313
+ autoComplete: "postal-code",
1314
+ placeholder: "00000-000",
1315
+ value: draft,
1316
+ onChange: (event) => setDraft(formatPostalCode(event.target.value)),
1317
+ 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"
1318
+ }
1319
+ ),
1320
+ /* @__PURE__ */ jsx(
1321
+ "button",
1322
+ {
1323
+ type: "submit",
1324
+ "data-testid": TID.deliveryCepSubmit,
1325
+ disabled: loading,
1326
+ 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",
1327
+ children: loading ? "..." : "Calcular"
1328
+ }
1329
+ )
1330
+ ] }),
1331
+ delivery.error && /* @__PURE__ */ jsx("p", { "data-testid": TID.deliveryError, className: "mt-3 text-sm text-destructive", children: delivery.error }),
1332
+ delivery.address && /* @__PURE__ */ jsxs("p", { "data-testid": TID.deliveryAddress, className: "mt-3 flex items-start gap-1.5 text-xs text-muted-foreground", children: [
1333
+ /* @__PURE__ */ jsx(MapPin, { className: "mt-0.5 h-3.5 w-3.5 shrink-0" }),
1334
+ /* @__PURE__ */ jsxs("span", { children: [
1335
+ [delivery.address.street, delivery.address.district].filter(Boolean).join(", "),
1336
+ delivery.address.street || delivery.address.district ? " \u2014 " : "",
1337
+ delivery.address.city,
1338
+ "/",
1339
+ delivery.address.state
1340
+ ] })
1341
+ ] }),
1342
+ delivery.status === "served" && /* @__PURE__ */ jsx("ul", { className: "mt-3 space-y-2", children: delivery.options.map((option) => {
1343
+ const selected = delivery.selectedZoneId === option.zoneId;
1344
+ const label = eta(option);
1345
+ const rate = resolveOptionRate(option, subtotal);
1346
+ return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs(
1347
+ "button",
1348
+ {
1349
+ type: "button",
1350
+ "data-testid": TID.deliveryOption,
1351
+ "data-zone": option.zoneId,
1352
+ "data-rate": rate.toFixed(2),
1353
+ "aria-pressed": selected,
1354
+ onClick: () => delivery.selectZone(option.zoneId),
1355
+ 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"}`,
1356
+ children: [
1357
+ /* @__PURE__ */ jsxs("span", { className: "min-w-0", children: [
1358
+ /* @__PURE__ */ jsx("span", { className: "block truncate font-medium", children: option.carrier ? `${option.carrier} \xB7 ${option.name}` : option.name }),
1359
+ label && /* @__PURE__ */ jsx("span", { className: "block text-xs text-muted-foreground", children: label })
1360
+ ] }),
1361
+ /* @__PURE__ */ jsx("span", { className: "shrink-0 font-semibold", children: rate === 0 ? "Gr\xE1tis" : money(rate) })
1362
+ ]
1363
+ }
1364
+ ) }, option.zoneId);
1365
+ }) }),
1366
+ delivery.status === "unserved" && /* @__PURE__ */ jsx("p", { "data-testid": TID.deliveryUnserved, className: "mt-3 text-sm text-destructive", children: "Ainda n\xE3o entregamos nesse CEP." }),
1367
+ delivery.postalCode && /* @__PURE__ */ jsx(
1368
+ "button",
1369
+ {
1370
+ type: "button",
1371
+ "data-testid": TID.deliveryClear,
1372
+ onClick: () => {
1373
+ delivery.clear();
1374
+ setDraft("");
1375
+ },
1376
+ className: "mt-3 text-xs text-muted-foreground underline-offset-2 hover:underline",
1377
+ children: "Trocar CEP"
1378
+ }
1379
+ )
1380
+ ]
1381
+ }
1382
+ );
1383
+ }
1106
1384
  function SmoothImage({
1107
1385
  imageLoading,
1108
1386
  loading = "lazy",
@@ -1120,9 +1398,9 @@ function SmoothImage({
1120
1398
  const durationMs = resolved.durationMs ?? 420;
1121
1399
  const easing = resolved.easing ?? "cubic-bezier(0.22, 1, 0.36, 1)";
1122
1400
  const blur = resolved.blur ?? true;
1123
- const ref = React6.useRef(null);
1124
- const [loaded, setLoaded] = React6.useState(mode === "none");
1125
- React6.useEffect(() => {
1401
+ const ref = React7.useRef(null);
1402
+ const [loaded, setLoaded] = React7.useState(mode === "none");
1403
+ React7.useEffect(() => {
1126
1404
  if (mode === "none") {
1127
1405
  setLoaded(true);
1128
1406
  return;
@@ -1165,6 +1443,7 @@ function CartDrawer() {
1165
1443
  document.addEventListener("keydown", onKey);
1166
1444
  return () => document.removeEventListener("keydown", onKey);
1167
1445
  }, [cart.isOpen, cart.closeDrawer]);
1446
+ const delivery = useDeliveryStore();
1168
1447
  const subtotal = selectSubtotal(cart);
1169
1448
  const discountTotal = selectDiscountTotal(cart);
1170
1449
  const shipping = selectShipping(cart, config);
@@ -1321,6 +1600,7 @@ function CartDrawer() {
1321
1600
  ] }),
1322
1601
  discountError && /* @__PURE__ */ jsx("p", { "data-testid": TID.discountError, className: "text-xs text-destructive", children: discountError })
1323
1602
  ] }),
1603
+ /* @__PURE__ */ jsx("div", { className: "mb-3", children: /* @__PURE__ */ jsx(DeliveryEstimator, { compact: true }) }),
1324
1604
  /* @__PURE__ */ jsxs("dl", { className: "space-y-1.5 text-sm", children: [
1325
1605
  /* @__PURE__ */ jsxs("div", { className: "flex justify-between", children: [
1326
1606
  /* @__PURE__ */ jsx("dt", { className: "text-muted-foreground", children: "Subtotal" }),
@@ -1350,7 +1630,13 @@ function CartDrawer() {
1350
1630
  ] })
1351
1631
  ] }),
1352
1632
  /* @__PURE__ */ jsxs("div", { className: "flex justify-between", children: [
1353
- /* @__PURE__ */ jsx("dt", { className: "text-muted-foreground", children: "Frete" }),
1633
+ /* @__PURE__ */ jsxs("dt", { className: "text-muted-foreground", children: [
1634
+ "Frete",
1635
+ delivery.status === "served" && delivery.postalCode && /* @__PURE__ */ jsxs("span", { className: "ml-1 text-xs", children: [
1636
+ "\xB7 ",
1637
+ formatPostalCode(delivery.postalCode)
1638
+ ] })
1639
+ ] }),
1354
1640
  /* @__PURE__ */ jsx("dd", { "data-testid": TID.cartShipping, "data-price": shipping.toFixed(2), children: shipping === 0 ? "Gr\xE1tis" : money(shipping) })
1355
1641
  ] }),
1356
1642
  /* @__PURE__ */ jsxs("div", { className: "flex justify-between border-t pt-2 text-base font-semibold", children: [
@@ -2693,6 +2979,128 @@ function ProductEnquiryForm({ product, onSuccess }) {
2693
2979
  }
2694
2980
  );
2695
2981
  }
2982
+ function ProductGallery({ product, images, primaryImage }) {
2983
+ const ordered = orderImages(images, primaryImage);
2984
+ const [index, setIndex] = useState(0);
2985
+ const [zoom, setZoom] = useState(null);
2986
+ const frameRef = useRef(null);
2987
+ const canHover = useCanHover();
2988
+ useEffect(() => {
2989
+ setIndex(0);
2990
+ setZoom(null);
2991
+ }, [product.id]);
2992
+ const current = ordered[Math.min(index, ordered.length - 1)];
2993
+ function trackPointer(event) {
2994
+ if (!canHover) return;
2995
+ const frame = frameRef.current;
2996
+ if (!frame) return;
2997
+ const rect = frame.getBoundingClientRect();
2998
+ setZoom({
2999
+ x: (event.clientX - rect.left) / rect.width * 100,
3000
+ y: (event.clientY - rect.top) / rect.height * 100
3001
+ });
3002
+ }
3003
+ function step(delta) {
3004
+ if (ordered.length < 2) return;
3005
+ setIndex((current2) => (current2 + delta + ordered.length) % ordered.length);
3006
+ setZoom(null);
3007
+ }
3008
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-3 self-start", children: [
3009
+ /* @__PURE__ */ jsxs(
3010
+ "div",
3011
+ {
3012
+ ...storefrontComponentContracts.productDetail.gallery,
3013
+ ref: frameRef,
3014
+ className: "group relative aspect-square w-full overflow-hidden border bg-muted",
3015
+ style: { borderRadius: "var(--sf-radius-card)" },
3016
+ onMouseMove: trackPointer,
3017
+ onMouseLeave: () => setZoom(null),
3018
+ onKeyDown: (event) => {
3019
+ if (event.key === "ArrowRight") step(1);
3020
+ if (event.key === "ArrowLeft") step(-1);
3021
+ },
3022
+ tabIndex: ordered.length > 1 ? 0 : -1,
3023
+ role: ordered.length > 1 ? "group" : void 0,
3024
+ "aria-label": ordered.length > 1 ? `Imagens de ${product.name}` : void 0,
3025
+ children: [
3026
+ current && /* @__PURE__ */ jsx(
3027
+ "div",
3028
+ {
3029
+ className: "h-full w-full transition-transform duration-150 ease-out",
3030
+ style: zoom ? { transform: "scale(2)", transformOrigin: `${zoom.x}% ${zoom.y}%` } : { transform: "scale(1)" },
3031
+ children: /* @__PURE__ */ jsx(
3032
+ SmoothImage,
3033
+ {
3034
+ src: current.url,
3035
+ alt: current.altText ?? product.name,
3036
+ "data-testid": TID.pdpGalleryImage,
3037
+ className: "h-full w-full object-cover"
3038
+ },
3039
+ current.id
3040
+ )
3041
+ }
3042
+ ),
3043
+ ordered.length > 1 && /* @__PURE__ */ jsxs(Fragment, { children: [
3044
+ /* @__PURE__ */ jsx(GalleryArrow, { side: "left", onClick: () => step(-1) }),
3045
+ /* @__PURE__ */ jsx(GalleryArrow, { side: "right", onClick: () => step(1) }),
3046
+ /* @__PURE__ */ 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: [
3047
+ index + 1,
3048
+ "/",
3049
+ ordered.length
3050
+ ] })
3051
+ ] })
3052
+ ]
3053
+ }
3054
+ ),
3055
+ ordered.length > 1 && /* @__PURE__ */ jsx("ul", { "data-testid": TID.pdpGalleryThumbs, className: "flex gap-2 overflow-x-auto pb-1", children: ordered.map((image, position) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(
3056
+ "button",
3057
+ {
3058
+ type: "button",
3059
+ "data-testid": TID.pdpGalleryThumb,
3060
+ "data-index": position,
3061
+ "aria-label": `Imagem ${position + 1} de ${ordered.length}`,
3062
+ "aria-current": position === index,
3063
+ onClick: () => {
3064
+ setIndex(position);
3065
+ setZoom(null);
3066
+ },
3067
+ 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"}`,
3068
+ children: /* @__PURE__ */ jsx("img", { src: image.url, alt: "", className: "h-full w-full object-cover", loading: "lazy" })
3069
+ }
3070
+ ) }, image.id)) })
3071
+ ] });
3072
+ }
3073
+ function orderImages(images, primary) {
3074
+ const all = images.length > 0 ? images : primary ? [primary] : [];
3075
+ return [...all].sort((a, b) => {
3076
+ if (a.isPrimary !== b.isPrimary) return a.isPrimary ? -1 : 1;
3077
+ return (a.sortOrder ?? 0) - (b.sortOrder ?? 0);
3078
+ });
3079
+ }
3080
+ function useCanHover() {
3081
+ const [canHover, setCanHover] = useState(false);
3082
+ useEffect(() => {
3083
+ const query = window.matchMedia?.("(hover: hover) and (pointer: fine)");
3084
+ if (!query) return;
3085
+ setCanHover(query.matches);
3086
+ const onChange = (event) => setCanHover(event.matches);
3087
+ query.addEventListener?.("change", onChange);
3088
+ return () => query.removeEventListener?.("change", onChange);
3089
+ }, []);
3090
+ return canHover;
3091
+ }
3092
+ function GalleryArrow({ side, onClick }) {
3093
+ return /* @__PURE__ */ jsx(
3094
+ "button",
3095
+ {
3096
+ type: "button",
3097
+ onClick,
3098
+ "aria-label": side === "left" ? "Imagem anterior" : "Pr\xF3xima imagem",
3099
+ 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"}`,
3100
+ children: /* @__PURE__ */ jsx("span", { "aria-hidden": true, className: "text-lg leading-none", children: side === "left" ? "\u2039" : "\u203A" })
3101
+ }
3102
+ );
3103
+ }
2696
3104
  function ProductDetailPage({ slug: slug2 }) {
2697
3105
  const config = useStorefrontConfig();
2698
3106
  const actions = useStorefrontActions();
@@ -2733,6 +3141,7 @@ function ProductDetailPage({ slug: slug2 }) {
2733
3141
  const image = product.images.find((i) => i.isPrimary) ?? product.images[0];
2734
3142
  const components = getStorefrontComponents(config);
2735
3143
  const ProductDetailComponent = components.ProductDetail;
3144
+ const GalleryComponent = components.ProductGallery ?? ProductGallery;
2736
3145
  const addToCart = () => addItem(product, qty, selectedOptions);
2737
3146
  const openEnquiry = () => {
2738
3147
  document.getElementById("storefront-product-enquiry")?.scrollIntoView({ behavior: "smooth", block: "start" });
@@ -2766,19 +3175,13 @@ function ProductDetailPage({ slug: slug2 }) {
2766
3175
  ] }),
2767
3176
  /* @__PURE__ */ jsxs("div", { className: "grid animate-fade-up gap-10 md:grid-cols-2", children: [
2768
3177
  /* @__PURE__ */ jsx(
2769
- "div",
3178
+ GalleryComponent,
2770
3179
  {
2771
- ...storefrontComponentContracts.productDetail.gallery,
2772
- className: "group overflow-hidden border bg-muted",
2773
- style: { borderRadius: "var(--sf-radius-card)" },
2774
- children: image && /* @__PURE__ */ jsx(
2775
- SmoothImage,
2776
- {
2777
- src: image.url,
2778
- alt: image.altText ?? product.name,
2779
- className: "aspect-square w-full object-cover transition-transform duration-700 ease-out group-hover:scale-105"
2780
- }
2781
- )
3180
+ product,
3181
+ images: product.images,
3182
+ primaryImage: image,
3183
+ config,
3184
+ commerceMode: config.commerceMode
2782
3185
  }
2783
3186
  ),
2784
3187
  /* @__PURE__ */ jsxs("div", { className: "flex flex-col py-2 lg:sticky lg:top-24 lg:self-start", children: [
@@ -2876,6 +3279,7 @@ function ProductDetailPage({ slug: slug2 }) {
2876
3279
  children: "Voltar ao cat\xE1logo"
2877
3280
  }
2878
3281
  ) }),
3282
+ config.commerceMode === "checkout" && /* @__PURE__ */ jsx(DeliveryEstimator, {}),
2879
3283
  config.commerceMode === "checkout" && /* @__PURE__ */ jsxs("div", { className: "mt-6 grid grid-cols-3 gap-3 border-t pt-6 text-center", children: [
2880
3284
  /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center gap-1.5 text-[11px] font-medium text-muted-foreground", children: [
2881
3285
  /* @__PURE__ */ jsx(Truck, { className: "h-5 w-5 text-primary" }),
@@ -2901,9 +3305,10 @@ function ProductDetailPage({ slug: slug2 }) {
2901
3305
  /* @__PURE__ */ jsx(RelatedProducts, { product })
2902
3306
  ] });
2903
3307
  }
2904
- function formatDeliveryNotes(address) {
3308
+ function formatDeliveryNotes(address, shippingAddress) {
3309
+ if (address?.notes?.trim()) return address.notes.trim();
3310
+ if (shippingAddress) return void 0;
2905
3311
  if (!address) return void 0;
2906
- if (address.notes?.trim()) return address.notes.trim();
2907
3312
  const parts = [address.street, address.city, address.zip].map((part) => part?.trim()).filter(Boolean);
2908
3313
  return parts.length > 0 ? `Entrega: ${parts.join(", ")}` : void 0;
2909
3314
  }
@@ -2913,6 +3318,8 @@ async function placeStorefrontOrder({
2913
3318
  session,
2914
3319
  customer,
2915
3320
  address,
3321
+ shippingAddress,
3322
+ paymentMethod,
2916
3323
  markPaid = false
2917
3324
  }) {
2918
3325
  const email = customer.email.trim().toLowerCase();
@@ -2927,9 +3334,14 @@ async function placeStorefrontOrder({
2927
3334
  customerId: customerId ?? void 0,
2928
3335
  customer: { name, email },
2929
3336
  currency: config.currency,
2930
- notes: formatDeliveryNotes(address),
3337
+ notes: formatDeliveryNotes(address, shippingAddress),
2931
3338
  discountCode: cart.discountCode ?? void 0,
2932
3339
  shippingTotal: selectShipping(cart, config),
3340
+ // Structured address + method are what populate public.addresses and the
3341
+ // transactions ledger. `notes` stays for backwards compatibility with
3342
+ // checkouts that have not been updated yet.
3343
+ shippingAddress,
3344
+ paymentMethod,
2933
3345
  items: cart.lines.map((line) => ({
2934
3346
  productId: line.productId,
2935
3347
  quantity: line.quantity,
@@ -2937,7 +3349,8 @@ async function placeStorefrontOrder({
2937
3349
  }))
2938
3350
  });
2939
3351
  if (markPaid) {
2940
- await provider.updateOrder(order.id, { financialStatus: "paid" });
3352
+ if (provider.confirmPayment) await provider.confirmPayment(order.id);
3353
+ else await provider.updateOrder(order.id, { financialStatus: "paid" });
2941
3354
  }
2942
3355
  return { order, customerId: customerId ?? order.customerId ?? "" };
2943
3356
  }
@@ -3051,19 +3464,18 @@ function SignInModal({ defaultEmail = "", onClose, onSignedIn }) {
3051
3464
  }
3052
3465
  );
3053
3466
  }
3054
- var SAVED_ADDRESSES = [
3055
- { id: "home", label: "Casa", street: "Rua das Flores, 123", city: "S\xE3o Paulo", zip: "01310-100" }
3056
- ];
3057
- var SAVED_PAYMENT_METHODS = [
3058
- { id: "visa", label: "Visa final 4242", card: "4242 4242 4242 4242", expiry: "12/29", cvc: "123" }
3059
- ];
3467
+ var PAYMENT_LABELS = {
3468
+ pix: { label: "Pix", hint: "Voc\xEA recebe a chave para pagar ap\xF3s confirmar o pedido" },
3469
+ credit_card: { label: "Cart\xE3o de cr\xE9dito", hint: "Maquininha na entrega" },
3470
+ debit_card: { label: "Cart\xE3o de d\xE9bito", hint: "Maquininha na entrega" },
3471
+ boleto: { label: "Boleto", hint: "Enviado por e-mail ap\xF3s a confirma\xE7\xE3o" },
3472
+ cash: { label: "Dinheiro", hint: "Pagamento na entrega" },
3473
+ other: { label: "Combinar com a loja", hint: "A loja entra em contato para acertar o pagamento" }
3474
+ };
3060
3475
  var PROCESSING_STEPS = [
3061
3476
  { icon: Lock, label: "Validando seus dados..." },
3062
- { icon: CreditCard, label: "Processando pagamento..." },
3063
- { icon: PackageCheck, label: "Confirmando seu pedido..." }
3477
+ { icon: PackageCheck, label: "Registrando seu pedido..." }
3064
3478
  ];
3065
- var DEFAULT_ADDRESS = SAVED_ADDRESSES[0];
3066
- var DEFAULT_PAYMENT = SAVED_PAYMENT_METHODS[0];
3067
3479
  var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
3068
3480
  function ProcessingOverlay({ step }) {
3069
3481
  const current = PROCESSING_STEPS[Math.min(step, PROCESSING_STEPS.length - 1)];
@@ -3106,23 +3518,24 @@ function CheckoutPage() {
3106
3518
  const config = useStorefrontConfig();
3107
3519
  const cart = useCartStore();
3108
3520
  const session = useSessionStore();
3521
+ const delivery = useDeliveryStore();
3109
3522
  const validateDiscount2 = useDiscountValidator();
3110
3523
  useStorefrontHead({ title: `Checkout \u2014 ${config.name}` });
3111
- const [selectedAddressId, setSelectedAddressId] = useState(DEFAULT_ADDRESS.id);
3112
- const [selectedPaymentId, setSelectedPaymentId] = useState(DEFAULT_PAYMENT.id);
3113
- const [savedAddresses, setSavedAddresses] = useState(SAVED_ADDRESSES);
3114
- const [savedPaymentMethods, setSavedPaymentMethods] = useState(SAVED_PAYMENT_METHODS);
3115
- const [addressMode, setAddressMode] = useState("saved");
3116
- const [paymentMode, setPaymentMode] = useState("saved");
3524
+ const paymentMethods = config.payments.methods;
3525
+ const [selectedAddressId, setSelectedAddressId] = useState("new");
3526
+ const [savedAddresses, setSavedAddresses] = useState([]);
3527
+ const [addressMode, setAddressMode] = useState("new");
3528
+ const [paymentMethod, setPaymentMethod] = useState(paymentMethods[0] ?? "pix");
3117
3529
  const [form, setForm] = useState({
3118
3530
  email: session.email ?? "",
3119
3531
  name: session.name ?? "",
3120
- street: DEFAULT_ADDRESS.street,
3121
- city: DEFAULT_ADDRESS.city,
3122
- zip: DEFAULT_ADDRESS.zip,
3123
- card: DEFAULT_PAYMENT.card,
3124
- expiry: DEFAULT_PAYMENT.expiry,
3125
- cvc: DEFAULT_PAYMENT.cvc
3532
+ street: "",
3533
+ city: "",
3534
+ zip: "",
3535
+ number: "",
3536
+ complement: "",
3537
+ district: "",
3538
+ state: ""
3126
3539
  });
3127
3540
  const [error, setError] = useState(null);
3128
3541
  const [discountCode, setDiscountCode] = useState("");
@@ -3132,6 +3545,7 @@ function CheckoutPage() {
3132
3545
  const [placing, setPlacing] = useState(false);
3133
3546
  const [processingStep, setProcessingStep] = useState(0);
3134
3547
  const [showSignin, setShowSignin] = useState(false);
3548
+ const [zipLookup, setZipLookup] = useState("idle");
3135
3549
  const subtotal = selectSubtotal(cart);
3136
3550
  const discountTotal = selectDiscountTotal(cart);
3137
3551
  const shipping = selectShipping(cart, config);
@@ -3141,6 +3555,32 @@ function CheckoutPage() {
3141
3555
  if (cart.lines.length === 0 && !placing) navigateTo(config.catalogPath);
3142
3556
  }, [cart.lines.length, config.catalogPath, placing]);
3143
3557
  const set = (key) => (value) => setForm((current) => ({ ...current, [key]: value }));
3558
+ async function setZip(value) {
3559
+ const masked = formatPostalCode(value);
3560
+ setForm((current) => ({ ...current, zip: masked }));
3561
+ if (normalizePostalCode(masked).length !== 8) {
3562
+ setZipLookup("idle");
3563
+ return;
3564
+ }
3565
+ setZipLookup("loading");
3566
+ try {
3567
+ const found = await lookupPostalCode(masked);
3568
+ if (!found) {
3569
+ setZipLookup("not-found");
3570
+ return;
3571
+ }
3572
+ setZipLookup("found");
3573
+ setForm((current) => ({
3574
+ ...current,
3575
+ street: found.street || current.street,
3576
+ district: found.district || current.district,
3577
+ city: found.city,
3578
+ state: found.state
3579
+ }));
3580
+ } catch {
3581
+ setZipLookup("idle");
3582
+ }
3583
+ }
3144
3584
  async function applyDiscountCode() {
3145
3585
  setApplyingDiscount(true);
3146
3586
  setDiscountError(null);
@@ -3167,10 +3607,54 @@ function CheckoutPage() {
3167
3607
  setDiscountSuccess(null);
3168
3608
  setDiscountError(null);
3169
3609
  }
3610
+ useEffect(() => {
3611
+ const customerId = session.customerId;
3612
+ if (!customerId) {
3613
+ setSavedAddresses([]);
3614
+ setAddressMode("new");
3615
+ return;
3616
+ }
3617
+ let cancelled = false;
3618
+ const provider = getShopProvider();
3619
+ void provider.listCustomerAddresses?.(customerId).then((addresses) => {
3620
+ if (cancelled || addresses.length === 0) return;
3621
+ setSavedAddresses(addresses);
3622
+ applySavedAddress(addresses[0]);
3623
+ }).catch(() => {
3624
+ });
3625
+ return () => {
3626
+ cancelled = true;
3627
+ };
3628
+ }, [session.customerId]);
3629
+ useEffect(() => {
3630
+ if (addressMode !== "new") return;
3631
+ const looked = delivery.address;
3632
+ if (!looked) return;
3633
+ setForm((current) => {
3634
+ if (current.zip.trim() || current.street.trim()) return current;
3635
+ return {
3636
+ ...current,
3637
+ zip: formatPostalCode(looked.postalCode),
3638
+ street: looked.street,
3639
+ district: looked.district,
3640
+ city: looked.city,
3641
+ state: looked.state
3642
+ };
3643
+ });
3644
+ }, [delivery.address, addressMode]);
3170
3645
  function applySavedAddress(address) {
3171
3646
  setAddressMode("saved");
3172
3647
  setSelectedAddressId(address.id);
3173
- setForm((current) => ({ ...current, street: address.street, city: address.city, zip: address.zip }));
3648
+ setForm((current) => ({
3649
+ ...current,
3650
+ street: address.street,
3651
+ city: address.city,
3652
+ zip: address.postalCode,
3653
+ number: address.number ?? "",
3654
+ complement: address.complement ?? "",
3655
+ district: address.district ?? "",
3656
+ state: address.state
3657
+ }));
3174
3658
  }
3175
3659
  function selectAddress(addressId) {
3176
3660
  const address = savedAddresses.find((item) => item.id === addressId);
@@ -3179,37 +3663,7 @@ function CheckoutPage() {
3179
3663
  function addNewAddress() {
3180
3664
  setAddressMode("new");
3181
3665
  setSelectedAddressId("new");
3182
- setForm((current) => ({ ...current, street: "", city: "", zip: "" }));
3183
- }
3184
- function removeAddress(addressId) {
3185
- const nextAddresses = savedAddresses.filter((item) => item.id !== addressId);
3186
- setSavedAddresses(nextAddresses);
3187
- if (selectedAddressId !== addressId) return;
3188
- const fallback = nextAddresses[0];
3189
- if (fallback) applySavedAddress(fallback);
3190
- else addNewAddress();
3191
- }
3192
- function applySavedPayment(method) {
3193
- setPaymentMode("saved");
3194
- setSelectedPaymentId(method.id);
3195
- setForm((current) => ({ ...current, card: method.card, expiry: method.expiry, cvc: method.cvc }));
3196
- }
3197
- function selectPayment(paymentId) {
3198
- const method = savedPaymentMethods.find((item) => item.id === paymentId);
3199
- if (method) applySavedPayment(method);
3200
- }
3201
- function addNewPayment() {
3202
- setPaymentMode("new");
3203
- setSelectedPaymentId("new");
3204
- setForm((current) => ({ ...current, card: "", expiry: "", cvc: "" }));
3205
- }
3206
- function removePayment(paymentId) {
3207
- const nextMethods = savedPaymentMethods.filter((item) => item.id !== paymentId);
3208
- setSavedPaymentMethods(nextMethods);
3209
- if (selectedPaymentId !== paymentId) return;
3210
- const fallback = nextMethods[0];
3211
- if (fallback) applySavedPayment(fallback);
3212
- else addNewPayment();
3666
+ setForm((current) => ({ ...current, street: "", city: "", zip: "", number: "", complement: "", district: "", state: "" }));
3213
3667
  }
3214
3668
  function validate() {
3215
3669
  setError(null);
@@ -3221,12 +3675,20 @@ function CheckoutPage() {
3221
3675
  setError("Informe seu nome completo.");
3222
3676
  return false;
3223
3677
  }
3224
- if (!form.street.trim() || !form.city.trim() || !form.zip.trim()) {
3678
+ if (!form.street.trim() || !form.city.trim() || !form.zip.trim() || !form.district.trim()) {
3225
3679
  setError("Preencha ou selecione o endere\xE7o de entrega.");
3226
3680
  return false;
3227
3681
  }
3228
- if (!/^\d{16}$/.test(form.card.replace(/\s+/g, "")) || !form.expiry.trim() || !form.cvc.trim()) {
3229
- setError("Selecione ou preencha um m\xE9todo de pagamento v\xE1lido.");
3682
+ if (!form.number.trim()) {
3683
+ setError("Falta o n\xFAmero do endere\xE7o.");
3684
+ return false;
3685
+ }
3686
+ if (form.state.trim().length !== 2) {
3687
+ setError("Informe a UF com duas letras (ex.: RJ).");
3688
+ return false;
3689
+ }
3690
+ if (delivery.status === "unserved" && delivery.postalCode === normalizePostalCode(form.zip)) {
3691
+ setError("Ainda n\xE3o entregamos nesse CEP. Tente outro endere\xE7o.");
3230
3692
  return false;
3231
3693
  }
3232
3694
  return true;
@@ -3248,9 +3710,22 @@ function CheckoutPage() {
3248
3710
  session,
3249
3711
  customer: { email: form.email, name: form.name },
3250
3712
  address: { street: form.street, city: form.city, zip: form.zip },
3251
- // Demo only: mock mode instantly approves. Real Pix/Mercado Pago (M4)
3252
- // leaves the order pending until the payment webhook confirms.
3253
- markPaid: config.payments.mode === "mock"
3713
+ shippingAddress: {
3714
+ postalCode: form.zip.trim(),
3715
+ street: form.street.trim(),
3716
+ number: form.number.trim() || void 0,
3717
+ complement: form.complement.trim() || void 0,
3718
+ district: form.district.trim() || void 0,
3719
+ city: form.city.trim(),
3720
+ state: form.state.trim().toUpperCase()
3721
+ },
3722
+ // The method the buyer actually chose, which opens the ledger row in
3723
+ // public.transactions with the right kind.
3724
+ paymentMethod
3725
+ // Never marked paid from the browser. It used to be, through an RPC that
3726
+ // was granted to anon — so any buyer holding their own order id could
3727
+ // declare it settled. The order is now born `pending` and only the
3728
+ // merchant (or a PSP webhook) can confirm the money arrived.
3254
3729
  });
3255
3730
  await minDuration;
3256
3731
  cart.clear();
@@ -3323,35 +3798,25 @@ function CheckoutPage() {
3323
3798
  /* @__PURE__ */ jsxs("section", { children: [
3324
3799
  /* @__PURE__ */ jsx("h2", { className: "mb-3 text-xl font-semibold tracking-tight", children: "Endere\xE7o de entrega" }),
3325
3800
  /* @__PURE__ */ jsxs("div", { className: "mb-3 grid gap-3", children: [
3326
- savedAddresses.map((address) => /* @__PURE__ */ jsxs(
3801
+ savedAddresses.map((address) => /* @__PURE__ */ jsx(
3327
3802
  "div",
3328
3803
  {
3329
3804
  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"}`,
3330
- children: [
3331
- /* @__PURE__ */ jsxs("button", { type: "button", onClick: () => selectAddress(address.id), className: "block w-full text-left", children: [
3332
- /* @__PURE__ */ jsxs("span", { className: "flex items-center justify-between gap-3 font-semibold", children: [
3333
- address.label,
3334
- addressMode === "saved" && selectedAddressId === address.id && /* @__PURE__ */ jsx("span", { className: "rounded-full bg-primary/10 px-2 py-0.5 text-[11px] font-bold text-primary", children: "Selecionado" })
3335
- ] }),
3336
- /* @__PURE__ */ jsx("span", { className: "mt-1 block text-muted-foreground", children: address.street }),
3337
- /* @__PURE__ */ jsxs("span", { className: "block text-muted-foreground", children: [
3338
- address.city,
3339
- " - ",
3340
- address.zip
3341
- ] })
3805
+ children: /* @__PURE__ */ jsxs("button", { type: "button", onClick: () => selectAddress(address.id), className: "block w-full text-left", children: [
3806
+ /* @__PURE__ */ jsxs("span", { className: "flex items-center justify-between gap-3 font-semibold", children: [
3807
+ address.label,
3808
+ addressMode === "saved" && selectedAddressId === address.id && /* @__PURE__ */ jsx("span", { className: "rounded-full bg-primary/10 px-2 py-0.5 text-[11px] font-bold text-primary", children: "Selecionado" })
3342
3809
  ] }),
3343
- /* @__PURE__ */ jsx(
3344
- "button",
3345
- {
3346
- type: "button",
3347
- onClick: () => removeAddress(address.id),
3348
- 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",
3349
- "aria-label": `Remover endere\xE7o ${address.label}`,
3350
- title: "Remover endere\xE7o",
3351
- children: /* @__PURE__ */ jsx(Trash2, { className: "h-4 w-4" })
3352
- }
3353
- )
3354
- ]
3810
+ /* @__PURE__ */ jsxs("span", { className: "mt-1 block text-muted-foreground", children: [
3811
+ [address.street, address.number].filter(Boolean).join(", "),
3812
+ address.complement ? ` \u2014 ${address.complement}` : ""
3813
+ ] }),
3814
+ /* @__PURE__ */ jsxs("span", { className: "block text-muted-foreground", children: [
3815
+ [address.district, address.city, address.state].filter(Boolean).join(" \xB7 "),
3816
+ " \u2014 ",
3817
+ address.postalCode
3818
+ ] })
3819
+ ] })
3355
3820
  },
3356
3821
  address.id
3357
3822
  )),
@@ -3362,83 +3827,62 @@ function CheckoutPage() {
3362
3827
  onClick: addNewAddress,
3363
3828
  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"}`,
3364
3829
  children: [
3365
- /* @__PURE__ */ jsx("span", { className: "block font-semibold text-primary", children: "+ Adicionar novo endere\xE7o" }),
3830
+ /* @__PURE__ */ jsx("span", { className: "block font-semibold text-primary", children: savedAddresses.length > 0 ? "+ Adicionar novo endere\xE7o" : "Informe o endere\xE7o de entrega" }),
3366
3831
  /* @__PURE__ */ jsx("span", { className: "mt-1 block text-muted-foreground", children: "Preencher outro endere\xE7o para esta compra" })
3367
3832
  ]
3368
3833
  }
3369
3834
  )
3370
3835
  ] }),
3371
3836
  addressMode === "new" && /* @__PURE__ */ jsxs("div", { className: "grid gap-3", children: [
3372
- field("Endere\xE7o", TID.checkoutStreet, form.street, set("street"), { placeholder: "Endere\xE7o" }),
3373
- /* @__PURE__ */ jsxs("div", { className: "grid gap-3 sm:grid-cols-[1fr_160px]", children: [
3374
- field("Cidade", TID.checkoutCity, form.city, set("city"), { placeholder: "Cidade" }),
3375
- field("CEP", TID.checkoutZip, form.zip, set("zip"), { placeholder: "CEP" })
3376
- ] })
3837
+ /* @__PURE__ */ jsxs("div", { className: "grid gap-1 sm:grid-cols-[200px_1fr] sm:items-center", children: [
3838
+ field("CEP", TID.checkoutZip, form.zip, setZip, {
3839
+ placeholder: "CEP",
3840
+ inputMode: "numeric",
3841
+ autoComplete: "postal-code"
3842
+ }),
3843
+ /* @__PURE__ */ 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." })
3844
+ ] }),
3845
+ field("Endere\xE7o", TID.checkoutStreet, form.street, set("street"), { placeholder: "Rua, avenida\u2026" }),
3846
+ /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-2 gap-3", children: [
3847
+ field("N\xFAmero", "checkout-number", form.number, set("number"), { placeholder: "N\xFAmero" }),
3848
+ field("Complemento", "checkout-complement", form.complement, set("complement"), { placeholder: "Apto, bloco (opcional)" })
3849
+ ] }),
3850
+ /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-2 gap-3", children: [
3851
+ field("Bairro", "checkout-district", form.district, set("district"), { placeholder: "Bairro" }),
3852
+ field("UF", "checkout-state", form.state, set("state"), { placeholder: "UF" })
3853
+ ] }),
3854
+ field("Cidade", TID.checkoutCity, form.city, set("city"), { placeholder: "Cidade" })
3377
3855
  ] })
3378
3856
  ] }),
3379
3857
  /* @__PURE__ */ jsxs("section", { children: [
3380
3858
  /* @__PURE__ */ jsx("h2", { className: "mb-3 text-xl font-semibold tracking-tight", children: "Pagamento" }),
3381
- /* @__PURE__ */ jsxs("div", { className: "mb-3 grid gap-3", children: [
3382
- savedPaymentMethods.map((method) => /* @__PURE__ */ jsxs(
3383
- "div",
3384
- {
3385
- 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"}`,
3386
- children: [
3387
- /* @__PURE__ */ jsxs("button", { type: "button", onClick: () => selectPayment(method.id), className: "block w-full text-left", children: [
3388
- /* @__PURE__ */ jsxs("span", { className: "flex items-center justify-between gap-3 font-semibold", children: [
3389
- /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-2", children: [
3390
- /* @__PURE__ */ jsx(CreditCard, { className: "h-4 w-4" }),
3391
- method.label
3392
- ] }),
3393
- paymentMode === "saved" && selectedPaymentId === method.id && /* @__PURE__ */ jsx("span", { className: "rounded-full bg-primary/10 px-2 py-0.5 text-[11px] font-bold text-primary", children: "Selecionado" })
3394
- ] }),
3395
- /* @__PURE__ */ jsxs("span", { className: "mt-1 block text-muted-foreground", children: [
3396
- "Expira em ",
3397
- method.expiry
3398
- ] })
3399
- ] }),
3400
- /* @__PURE__ */ jsx(
3401
- "button",
3402
- {
3403
- type: "button",
3404
- onClick: () => removePayment(method.id),
3405
- 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",
3406
- "aria-label": `Remover cart\xE3o ${method.label}`,
3407
- title: "Remover cart\xE3o",
3408
- children: /* @__PURE__ */ jsx(Trash2, { className: "h-4 w-4" })
3409
- }
3410
- )
3411
- ]
3412
- },
3413
- method.id
3414
- )),
3415
- /* @__PURE__ */ jsxs(
3859
+ /* @__PURE__ */ jsx("div", { className: "mb-3 grid gap-3", role: "radiogroup", "aria-label": "Forma de pagamento", children: paymentMethods.map((method) => {
3860
+ const copy = PAYMENT_LABELS[method];
3861
+ const selected = paymentMethod === method;
3862
+ return /* @__PURE__ */ jsxs(
3416
3863
  "button",
3417
3864
  {
3418
3865
  type: "button",
3419
- onClick: addNewPayment,
3420
- 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"}`,
3866
+ role: "radio",
3867
+ "aria-checked": selected,
3868
+ "data-testid": `checkout-payment-${method}`,
3869
+ onClick: () => setPaymentMethod(method),
3870
+ 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"}`,
3421
3871
  children: [
3422
- /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-2 font-semibold text-primary", children: [
3423
- /* @__PURE__ */ jsx(CreditCard, { className: "h-4 w-4" }),
3424
- "+ Adicionar novo cart\xE3o"
3872
+ /* @__PURE__ */ jsxs("span", { className: "flex items-center justify-between gap-3 font-semibold", children: [
3873
+ /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-2", children: [
3874
+ /* @__PURE__ */ jsx(CreditCard, { className: "h-4 w-4" }),
3875
+ copy.label
3876
+ ] }),
3877
+ selected && /* @__PURE__ */ jsx("span", { className: "rounded-full bg-primary/10 px-2 py-0.5 text-[11px] font-bold text-primary", children: "Selecionado" })
3425
3878
  ] }),
3426
- /* @__PURE__ */ jsx("span", { className: "mt-1 block text-muted-foreground", children: "Usar outro cart\xE3o nesta compra" })
3879
+ /* @__PURE__ */ jsx("span", { className: "mt-1 block text-muted-foreground", children: copy.hint })
3427
3880
  ]
3428
- }
3429
- )
3430
- ] }),
3431
- paymentMode === "new" && /* @__PURE__ */ jsxs("div", { className: "grid gap-3", children: [
3432
- field("N\xFAmero do cart\xE3o", TID.checkoutCard, form.card, set("card"), {
3433
- placeholder: "N\xFAmero do cart\xE3o",
3434
- inputMode: "numeric"
3435
- }),
3436
- /* @__PURE__ */ jsxs("div", { className: "grid gap-3 sm:grid-cols-2", children: [
3437
- field("Validade", TID.checkoutExpiry, form.expiry, set("expiry"), { placeholder: "MM/AA" }),
3438
- field("CVC", TID.checkoutCvc, form.cvc, set("cvc"), { placeholder: "CVC", inputMode: "numeric" })
3439
- ] })
3440
- ] }),
3441
- /* @__PURE__ */ jsx("p", { className: "mt-2 text-xs text-muted-foreground", children: "Pagamento de demonstra\xE7\xE3o - nenhuma cobran\xE7a \xE9 feita." })
3881
+ },
3882
+ method
3883
+ );
3884
+ }) }),
3885
+ /* @__PURE__ */ 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." })
3442
3886
  ] }),
3443
3887
  error && /* @__PURE__ */ 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 })
3444
3888
  ] }),
@@ -4262,7 +4706,7 @@ function PolicyPage({ kind }) {
4262
4706
  ] }) })
4263
4707
  ] });
4264
4708
  }
4265
- var StorefrontErrorBoundary = class extends React6.Component {
4709
+ var StorefrontErrorBoundary = class extends React7.Component {
4266
4710
  constructor() {
4267
4711
  super(...arguments);
4268
4712
  this.state = { error: null };
@@ -4417,6 +4861,15 @@ function initStorefrontRuntime(config) {
4417
4861
  } else {
4418
4862
  setShopProvider(createMockShopProvider(buildMockSeed(config)));
4419
4863
  }
4864
+ const reconcileCart = () => {
4865
+ const provider = getShopProvider();
4866
+ void useCartStore.getState().reconcile(async (line) => {
4867
+ const product = await provider.getProduct(line.productId);
4868
+ return product != null;
4869
+ });
4870
+ };
4871
+ if (useCartStore.persist.hasHydrated()) reconcileCart();
4872
+ else useCartStore.persist.onFinishHydration(reconcileCart);
4420
4873
  if (config.supabaseUrl || config.supabaseAnonKey) {
4421
4874
  console.warn(
4422
4875
  "@fayz-ai/shop: supabaseUrl/supabaseAnonKey are legacy fields. Pass an explicit provider/adapter or use the Fayz SDK broker path."
@@ -4548,9 +5001,9 @@ function manifestToStorefrontConfig(manifest, surfaceName) {
4548
5001
  };
4549
5002
  }
4550
5003
  function StorefrontScaffold({ manifest, surface }) {
4551
- const config = React6.useMemo(() => manifestToStorefrontConfig(manifest, surface), [manifest, surface]);
4552
- const resolved = React6.useMemo(() => resolveConfig(config), [config]);
4553
- const inited = React6.useRef(false);
5004
+ const config = React7.useMemo(() => manifestToStorefrontConfig(manifest, surface), [manifest, surface]);
5005
+ const resolved = React7.useMemo(() => resolveConfig(config), [config]);
5006
+ const inited = React7.useRef(false);
4554
5007
  if (!inited.current) {
4555
5008
  initStorefrontRuntime(config);
4556
5009
  inited.current = true;
@@ -4776,13 +5229,75 @@ var atelierTemplate = {
4776
5229
  ]
4777
5230
  })
4778
5231
  };
5232
+ var foodTemplate = {
5233
+ id: "food",
5234
+ inspiration: "Delivery / restaurant storefront",
5235
+ announcement: "ENTREGA GR\xC1TIS ACIMA DE R$ 150 \xB7 PE\xC7A J\xC1",
5236
+ theme: {
5237
+ name: "food",
5238
+ colors: {
5239
+ background: "30 33% 97%",
5240
+ foreground: "20 14% 15%",
5241
+ primary: "14 84% 50%",
5242
+ primaryForeground: "0 0% 100%",
5243
+ card: "30 33% 99%",
5244
+ cardForeground: "20 14% 15%",
5245
+ muted: "28 20% 92%",
5246
+ mutedForeground: "22 10% 42%",
5247
+ border: "28 18% 85%",
5248
+ headerBackground: "20 14% 11%",
5249
+ headerForeground: "30 24% 96%",
5250
+ announcementBackground: "14 84% 50%",
5251
+ announcementForeground: "0 0% 100%"
5252
+ },
5253
+ font: { heading: "Poppins", body: "Inter" },
5254
+ radius: "round",
5255
+ header: { variant: "classic" },
5256
+ productCard: { style: "card", imageAspect: "square" }
5257
+ },
5258
+ home: (storeName, images) => ({
5259
+ sections: [
5260
+ {
5261
+ type: "hero",
5262
+ variant: "banner",
5263
+ height: "tall",
5264
+ slides: [
5265
+ { 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] }
5266
+ ]
5267
+ },
5268
+ {
5269
+ type: "benefits",
5270
+ items: [
5271
+ { icon: "Flame", title: "Feito na hora", text: "Preparado com capricho" },
5272
+ { icon: "Truck", title: "Entrega r\xE1pida", text: "Frete gr\xE1tis acima de R$ 150" },
5273
+ { icon: "CreditCard", title: "Pagamento f\xE1cil", text: "Pix, cart\xE3o e mais" }
5274
+ ]
5275
+ },
5276
+ { type: "categories", style: "tiles", title: "Nosso card\xE1pio" },
5277
+ { type: "products", title: "Mais pedidos", eyebrow: "favoritos da casa", filter: "all", limit: 8 },
5278
+ { type: "banner", title: "Combo do dia", eyebrow: "oferta", subtitle: "Sele\xE7\xE3o especial com desconto", cta: "Aproveitar", hue: 14, image: images?.banners?.[0] },
5279
+ { type: "products", title: "Novidades no card\xE1pio", filter: "new", limit: 4 },
5280
+ {
5281
+ type: "testimonials",
5282
+ title: "O que dizem por a\xED",
5283
+ items: [
5284
+ { quote: "Chegou quentinho e absurdo de bom. Virei cliente!", author: "Rafael M." },
5285
+ { quote: "Sabor de restaurante em casa, sem complica\xE7\xE3o.", author: "Beatriz L." },
5286
+ { quote: "Entrega r\xE1pida e por\xE7\xE3o generosa. Recomendo demais.", author: "Diego S." }
5287
+ ]
5288
+ },
5289
+ { type: "newsletter", title: "Receba nossas ofertas", subtitle: "Promo\xE7\xF5es e novidades do card\xE1pio, direto no seu e-mail." }
5290
+ ]
5291
+ })
5292
+ };
4779
5293
  var storefrontTemplates = {
4780
5294
  mare: mareTemplate,
4781
5295
  sertao: sertaoTemplate,
4782
5296
  volt: voltTemplate,
4783
- atelier: atelierTemplate
5297
+ atelier: atelierTemplate,
5298
+ food: foodTemplate
4784
5299
  };
4785
5300
 
4786
- export { BenefitsRow, CartDrawer, CatalogPage, CategoryShowcase, CheckoutPage, FiltersPanel, HeroSection, HomePage, Link, ManifestoBlock, MediaCarousel, MyPurchasesPage, NewsletterBand, OrderConfirmationPage, OrderTrackingTimeline, Price, ProductCard, ProductDetailPage, ProductEnquiryForm, ProductGrid, ProductOptionSelector, ProductRail, ProductSlider, PromoBanner, QuantityInput, Reveal, SmoothImage, StorefrontFooter, StorefrontHeader, StorefrontScaffold, StorefrontSections, StorefrontShell, StorefrontThemeStyle, TID, Testimonials, atelierTemplate, bannerPlaceholder, createStorefront, createStorefrontApp, defineStorefront, defineStorefrontApp, defineStorefrontComponents, defineStorefrontConfig, defineStorefrontRoutes, defineStorefrontSections, establishCustomerSession, formatMoney, formatProductOptionSelection, getCustomerAuthAdapter, getProductOptionGroups, initStorefrontRuntime, mareTemplate, matchPath, navigateTo, normalizeProductOptionSelection, placeStorefrontOrder, prefersReducedMotion, productCardComponentContract, productOptionSelectionKey, registerStorefrontBlocks, resolveAuthAdapter, roundCents, sectionsToBlocks, selectCount, selectDiscountTotal, selectShipping, selectSubtotal, selectTotal, sertaoTemplate, signInByEmail, signOutCustomer, signUpCustomer, storefrontComponentContracts, storefrontTemplates, themeToCss, useCart, useCartStore, useCatalog, useCatalogStore, useCategories, useDiscountValidator, useEnquiry, useHashPath, useInView, useMyOrders, usePopOnChange, useProduct, useProducts, useScrolled, useSessionStore, useStorefront, useStorefrontActions, useStorefrontConfig, useStorefrontConfigOptional, voltTemplate };
5301
+ export { BenefitsRow, CartDrawer, CatalogPage, CategoryShowcase, CheckoutPage, FiltersPanel, HeroSection, HomePage, Link, ManifestoBlock, MediaCarousel, MyPurchasesPage, NewsletterBand, OrderConfirmationPage, OrderTrackingTimeline, Price, ProductCard, ProductDetailPage, ProductEnquiryForm, ProductGrid, ProductOptionSelector, ProductRail, ProductSlider, PromoBanner, QuantityInput, Reveal, SmoothImage, StorefrontFooter, StorefrontHeader, StorefrontScaffold, StorefrontSections, StorefrontShell, StorefrontThemeStyle, TID, Testimonials, atelierTemplate, bannerPlaceholder, createStorefront, createStorefrontApp, defineStorefront, defineStorefrontApp, defineStorefrontComponents, defineStorefrontConfig, defineStorefrontRoutes, defineStorefrontSections, establishCustomerSession, foodTemplate, formatMoney, formatProductOptionSelection, getCustomerAuthAdapter, getProductOptionGroups, initStorefrontRuntime, mareTemplate, matchPath, navigateTo, normalizeProductOptionSelection, placeStorefrontOrder, prefersReducedMotion, productCardComponentContract, productOptionSelectionKey, registerStorefrontBlocks, resolveAuthAdapter, roundCents, sectionsToBlocks, selectCount, selectDiscountTotal, selectShipping, selectSubtotal, selectTotal, sertaoTemplate, signInByEmail, signOutCustomer, signUpCustomer, storefrontComponentContracts, storefrontTemplates, themeToCss, useCart, useCartStore, useCatalog, useCatalogStore, useCategories, useDiscountValidator, useEnquiry, useHashPath, useInView, useMyOrders, usePopOnChange, useProduct, useProducts, useScrolled, useSessionStore, useStorefront, useStorefrontActions, useStorefrontConfig, useStorefrontConfigOptional, voltTemplate };
4787
5302
  //# sourceMappingURL=index.js.map
4788
5303
  //# sourceMappingURL=index.js.map