@fayz-ai/storefront 0.8.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.
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,7 +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);
262
+ return React7__default.default.createElement("a", { href: `#${to}`, ...rest }, children);
231
263
  }
232
264
 
233
265
  // src/format.ts
@@ -237,6 +269,87 @@ function formatMoney(value, currency = "BRL", locale = "pt-BR") {
237
269
  function roundCents(value) {
238
270
  return Math.round(value * 100) / 100;
239
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
+ };
240
353
 
241
354
  // src/product-options.ts
242
355
  function cleanValues(value) {
@@ -360,6 +473,8 @@ var selectDiscountTotal = (s) => roundCents(selectSubtotal(s) * (s.discountPerce
360
473
  var selectShipping = (s, cfg) => {
361
474
  if (s.lines.length === 0) return 0;
362
475
  const subtotal = selectSubtotal(s);
476
+ const quoted = selectQuotedShipping(useDeliveryStore.getState(), subtotal);
477
+ if (quoted != null) return quoted;
363
478
  if (cfg.shipping.freeAbove != null && subtotal >= cfg.shipping.freeAbove) return 0;
364
479
  return cfg.shipping.flatRate;
365
480
  };
@@ -446,7 +561,7 @@ function googleFontsHref(theme) {
446
561
  }
447
562
  function StorefrontThemeStyle({ theme }) {
448
563
  const fontsHref = googleFontsHref(theme);
449
- React6.useEffect(() => {
564
+ React7.useEffect(() => {
450
565
  if (!fontsHref) return;
451
566
  const id = "sf-theme-fonts";
452
567
  let link = document.getElementById(id);
@@ -458,7 +573,7 @@ function StorefrontThemeStyle({ theme }) {
458
573
  }
459
574
  link.href = fontsHref;
460
575
  }, [fontsHref]);
461
- return React6__default.default.createElement("style", { "data-sf-theme": theme.name }, themeToCss(theme));
576
+ return React7__default.default.createElement("style", { "data-sf-theme": theme.name }, themeToCss(theme));
462
577
  }
463
578
  var initial = {
464
579
  search: "",
@@ -479,9 +594,9 @@ var useCatalogStore = zustand.create()((set) => ({
479
594
  reset: () => set(initial)
480
595
  }));
481
596
  function useCategories() {
482
- const [categories, setCategories] = React6.useState([]);
483
- const [loading, setLoading] = React6.useState(true);
484
- React6.useEffect(() => {
597
+ const [categories, setCategories] = React7.useState([]);
598
+ const [loading, setLoading] = React7.useState(true);
599
+ React7.useEffect(() => {
485
600
  let cancelled = false;
486
601
  runtime.getShopProvider().listCategories().then((data) => {
487
602
  if (!cancelled) setCategories(data);
@@ -500,9 +615,9 @@ function prefersReducedMotion() {
500
615
  return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
501
616
  }
502
617
  function useInView(margin = "0px 0px -8% 0px") {
503
- const ref = React6.useRef(null);
504
- const [inView, setInView] = React6.useState(() => prefersReducedMotion());
505
- React6.useEffect(() => {
618
+ const ref = React7.useRef(null);
619
+ const [inView, setInView] = React7.useState(() => prefersReducedMotion());
620
+ React7.useEffect(() => {
506
621
  if (inView) return;
507
622
  const el = ref.current;
508
623
  if (!el || typeof IntersectionObserver === "undefined") {
@@ -537,8 +652,8 @@ function Reveal({ delay = 0, children, className, style, ...rest }) {
537
652
  );
538
653
  }
539
654
  function useScrolled(threshold = 8) {
540
- const [scrolled, setScrolled] = React6.useState(false);
541
- React6.useEffect(() => {
655
+ const [scrolled, setScrolled] = React7.useState(false);
656
+ React7.useEffect(() => {
542
657
  const onScroll = () => setScrolled(window.scrollY > threshold);
543
658
  onScroll();
544
659
  window.addEventListener("scroll", onScroll, { passive: true });
@@ -547,9 +662,9 @@ function useScrolled(threshold = 8) {
547
662
  return scrolled;
548
663
  }
549
664
  function usePopOnChange(value) {
550
- const [pop, setPop] = React6.useState(false);
551
- const prev = React6.useRef(value);
552
- React6.useEffect(() => {
665
+ const [pop, setPop] = React7.useState(false);
666
+ const prev = React7.useRef(value);
667
+ React7.useEffect(() => {
553
668
  if (prev.current !== value && value > 0) {
554
669
  setPop(true);
555
670
  const t = setTimeout(() => setPop(false), 400);
@@ -558,7 +673,7 @@ function usePopOnChange(value) {
558
673
  prev.current = value;
559
674
  return void 0;
560
675
  }, [value]);
561
- React6.useEffect(() => {
676
+ React7.useEffect(() => {
562
677
  prev.current = value;
563
678
  });
564
679
  return pop;
@@ -624,6 +739,9 @@ var TID = {
624
739
  pdpAddToCart: "pdp-add-to-cart",
625
740
  pdpRoot: "pdp-root",
626
741
  pdpGallery: "pdp-gallery",
742
+ pdpGalleryImage: "pdp-gallery-image",
743
+ pdpGalleryThumbs: "pdp-gallery-thumbs",
744
+ pdpGalleryThumb: "pdp-gallery-thumb",
627
745
  pdpActions: "pdp-actions",
628
746
  pdpEnquiryButton: "pdp-enquiry-button",
629
747
  enquiryForm: "enquiry-form",
@@ -652,6 +770,15 @@ var TID = {
652
770
  cartTotal: "cart-total",
653
771
  goCheckout: "go-checkout",
654
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",
655
782
  // checkout
656
783
  checkoutEmail: "checkout-email",
657
784
  checkoutName: "checkout-name",
@@ -698,7 +825,7 @@ var headerDivider = {
698
825
  var ANNOUNCEMENT_KEY = "fayz.storefront.announcement.dismissed";
699
826
  function AnnouncementBar() {
700
827
  const config = useStorefrontConfig();
701
- const [dismissed, setDismissed] = React6.useState(() => {
828
+ const [dismissed, setDismissed] = React7.useState(() => {
702
829
  try {
703
830
  return localStorage.getItem(ANNOUNCEMENT_KEY) === config.announcement;
704
831
  } catch {
@@ -762,7 +889,7 @@ function SearchInput({ className, iconOnly = false }) {
762
889
  const search = useCatalogStore((s) => s.search);
763
890
  const setSearch = useCatalogStore((s) => s.setSearch);
764
891
  const path = useHashPath();
765
- const [open, setOpen] = React6.useState(false);
892
+ const [open, setOpen] = React7.useState(false);
766
893
  if (iconOnly && !open) {
767
894
  return /* @__PURE__ */ jsxRuntime.jsx(
768
895
  "button",
@@ -810,7 +937,7 @@ function HeaderActions() {
810
937
  const count = useCartStore(selectCount);
811
938
  const openDrawer = useCartStore((s) => s.openDrawer);
812
939
  const session = useSessionStore();
813
- const [accountOpen, setAccountOpen] = React6.useState(false);
940
+ const [accountOpen, setAccountOpen] = React7.useState(false);
814
941
  const pop = usePopOnChange(count);
815
942
  const firstName = session.name?.split(/\s+/)[0];
816
943
  const initial2 = (firstName ?? session.email ?? "C").slice(0, 1).toUpperCase();
@@ -1089,12 +1216,12 @@ var REASON_MESSAGE = {
1089
1216
  };
1090
1217
  function useDiscountValidator() {
1091
1218
  const config = useStorefrontConfig();
1092
- return React6.useCallback(async (code) => {
1219
+ return React7.useCallback(async (code) => {
1093
1220
  const normalized = code.trim().toUpperCase();
1094
1221
  if (!normalized) return { valid: false, percent: 0, message: "Informe um cupom." };
1095
1222
  const configDiscount = config.discounts?.find((d) => d.code.trim().toUpperCase() === normalized);
1096
1223
  if (configDiscount) return { valid: true, percent: configDiscount.percent };
1097
- const validation = await shop.validateDiscount({ code: normalized });
1224
+ const validation = await shop$1.validateDiscount({ code: normalized });
1098
1225
  if (!validation.valid) {
1099
1226
  return { valid: false, percent: 0, message: REASON_MESSAGE[validation.reason ?? "not_found"] ?? "Cupom inv\xE1lido ou expirado." };
1100
1227
  }
@@ -1170,6 +1297,113 @@ function QuantityInput({ value, onChange, min = 1, max, testId, incTestId, decTe
1170
1297
  )
1171
1298
  ] });
1172
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
+ }
1173
1407
  function SmoothImage({
1174
1408
  imageLoading,
1175
1409
  loading = "lazy",
@@ -1187,9 +1421,9 @@ function SmoothImage({
1187
1421
  const durationMs = resolved.durationMs ?? 420;
1188
1422
  const easing = resolved.easing ?? "cubic-bezier(0.22, 1, 0.36, 1)";
1189
1423
  const blur = resolved.blur ?? true;
1190
- const ref = React6__default.default.useRef(null);
1191
- const [loaded, setLoaded] = React6__default.default.useState(mode === "none");
1192
- 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(() => {
1193
1427
  if (mode === "none") {
1194
1428
  setLoaded(true);
1195
1429
  return;
@@ -1221,10 +1455,10 @@ function CartDrawer() {
1221
1455
  const config = useStorefrontConfig();
1222
1456
  const cart = useCartStore();
1223
1457
  const validate = useDiscountValidator();
1224
- const [code, setCode] = React6.useState("");
1225
- const [discountError, setDiscountError] = React6.useState(null);
1226
- const [applying, setApplying] = React6.useState(false);
1227
- 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(() => {
1228
1462
  if (!cart.isOpen) return;
1229
1463
  const onKey = (e) => {
1230
1464
  if (e.key === "Escape") cart.closeDrawer();
@@ -1232,6 +1466,7 @@ function CartDrawer() {
1232
1466
  document.addEventListener("keydown", onKey);
1233
1467
  return () => document.removeEventListener("keydown", onKey);
1234
1468
  }, [cart.isOpen, cart.closeDrawer]);
1469
+ const delivery = useDeliveryStore();
1235
1470
  const subtotal = selectSubtotal(cart);
1236
1471
  const discountTotal = selectDiscountTotal(cart);
1237
1472
  const shipping = selectShipping(cart, config);
@@ -1388,6 +1623,7 @@ function CartDrawer() {
1388
1623
  ] }),
1389
1624
  discountError && /* @__PURE__ */ jsxRuntime.jsx("p", { "data-testid": TID.discountError, className: "text-xs text-destructive", children: discountError })
1390
1625
  ] }),
1626
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mb-3", children: /* @__PURE__ */ jsxRuntime.jsx(DeliveryEstimator, { compact: true }) }),
1391
1627
  /* @__PURE__ */ jsxRuntime.jsxs("dl", { className: "space-y-1.5 text-sm", children: [
1392
1628
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex justify-between", children: [
1393
1629
  /* @__PURE__ */ jsxRuntime.jsx("dt", { className: "text-muted-foreground", children: "Subtotal" }),
@@ -1417,7 +1653,13 @@ function CartDrawer() {
1417
1653
  ] })
1418
1654
  ] }),
1419
1655
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex justify-between", children: [
1420
- /* @__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
+ ] }),
1421
1663
  /* @__PURE__ */ jsxRuntime.jsx("dd", { "data-testid": TID.cartShipping, "data-price": shipping.toFixed(2), children: shipping === 0 ? "Gr\xE1tis" : money(shipping) })
1422
1664
  ] }),
1423
1665
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex justify-between border-t pt-2 text-base font-semibold", children: [
@@ -1447,12 +1689,12 @@ function CartDrawer() {
1447
1689
  ] });
1448
1690
  }
1449
1691
  function useProducts(opts) {
1450
- const [products, setProducts] = React6.useState([]);
1451
- const [loading, setLoading] = React6.useState(true);
1452
- const [error, setError] = React6.useState(null);
1453
- 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);
1454
1696
  const key = JSON.stringify(opts);
1455
- React6.useEffect(() => {
1697
+ React7.useEffect(() => {
1456
1698
  let cancelled = false;
1457
1699
  setLoading(true);
1458
1700
  runtime.getShopProvider().listProducts(JSON.parse(key)).then((data) => {
@@ -1770,10 +2012,10 @@ function FiltersPanel() {
1770
2012
  }
1771
2013
  function useStorefrontHead(input) {
1772
2014
  const { title, description } = input;
1773
- React6.useEffect(() => {
2015
+ React7.useEffect(() => {
1774
2016
  if (title) document.title = title;
1775
2017
  }, [title]);
1776
- React6.useEffect(() => {
2018
+ React7.useEffect(() => {
1777
2019
  if (!description) return;
1778
2020
  let tag = document.head.querySelector('meta[name="description"]');
1779
2021
  if (!tag) {
@@ -1792,9 +2034,9 @@ var SORT_MAP = {
1792
2034
  };
1793
2035
  var SORTS = ["newest", "price-asc", "price-desc", "name"];
1794
2036
  function useCatalogUrlSync() {
1795
- const hydrated = React6.useRef(false);
2037
+ const hydrated = React7.useRef(false);
1796
2038
  const catalog = useCatalogStore();
1797
- React6.useEffect(() => {
2039
+ React7.useEffect(() => {
1798
2040
  if (hydrated.current) return;
1799
2041
  hydrated.current = true;
1800
2042
  const q = window.location.hash.split("?")[1];
@@ -1809,7 +2051,7 @@ function useCatalogUrlSync() {
1809
2051
  if (p.has("min")) s.setPriceMin(Number(p.get("min")) || null);
1810
2052
  if (p.has("max")) s.setPriceMax(Number(p.get("max")) || null);
1811
2053
  }, []);
1812
- React6.useEffect(() => {
2054
+ React7.useEffect(() => {
1813
2055
  const p = new URLSearchParams();
1814
2056
  if (catalog.search) p.set("search", catalog.search);
1815
2057
  if (catalog.categoryId) p.set("category", catalog.categoryId);
@@ -1837,7 +2079,7 @@ function CatalogPage() {
1837
2079
  ...SORT_MAP[catalog.sort]
1838
2080
  };
1839
2081
  const { products, loading, error, reload } = useProducts(providerOpts);
1840
- const filtered = React6.useMemo(
2082
+ const filtered = React7.useMemo(
1841
2083
  () => products.filter((p) => {
1842
2084
  if (catalog.priceMin != null && p.price < catalog.priceMin) return false;
1843
2085
  if (catalog.priceMax != null && p.price > catalog.priceMax) return false;
@@ -1902,17 +2144,17 @@ function HeroSection({
1902
2144
  slides,
1903
2145
  height = "tall"
1904
2146
  }) {
1905
- const [index, setIndex] = React6.useState(0);
1906
- const [loadedSlides, setLoadedSlides] = React6.useState({});
2147
+ const [index, setIndex] = React7.useState(0);
2148
+ const [loadedSlides, setLoadedSlides] = React7.useState({});
1907
2149
  const config = useStorefrontConfig();
1908
- const interacted = React6.useRef(false);
2150
+ const interacted = React7.useRef(false);
1909
2151
  const hClass = height === "tall" ? "h-[72vh] min-h-[440px]" : "h-[48vh] min-h-[340px]";
1910
2152
  const revealImages = config.imageLoading.mode !== "none";
1911
2153
  const revealStyle = (loaded) => revealImages ? {
1912
2154
  filter: config.imageLoading.blur && !loaded ? "blur(10px)" : "blur(0px)",
1913
2155
  transition: `opacity 700ms ease, filter ${config.imageLoading.durationMs}ms ${config.imageLoading.easing}`
1914
2156
  } : void 0;
1915
- React6.useEffect(() => {
2157
+ React7.useEffect(() => {
1916
2158
  if (variant !== "slider" || slides.length < 2 || prefersReducedMotion()) return;
1917
2159
  const timer = setInterval(() => {
1918
2160
  if (!interacted.current) setIndex((i) => (i + 1) % slides.length);
@@ -2073,7 +2315,7 @@ function ProductRail({
2073
2315
  const { products, loading } = useProducts(
2074
2316
  filter === "new" ? { status: "active", orderBy: "created_at", order: "desc" } : { status: "active" }
2075
2317
  );
2076
- const visible = React6.useMemo(() => {
2318
+ const visible = React7.useMemo(() => {
2077
2319
  let list = products;
2078
2320
  if (filter === "sale") list = list.filter((p) => p.compareAtPrice != null && p.compareAtPrice > p.price);
2079
2321
  return list.slice(0, limit);
@@ -2122,16 +2364,16 @@ function MediaCarousel({
2122
2364
  overlay = "dark",
2123
2365
  className = ""
2124
2366
  }) {
2125
- const [index, setIndex] = React6.useState(0);
2126
- const [loadedItems, setLoadedItems] = React6.useState({});
2367
+ const [index, setIndex] = React7.useState(0);
2368
+ const [loadedItems, setLoadedItems] = React7.useState({});
2127
2369
  const config = useStorefrontConfig();
2128
- const interacted = React6.useRef(false);
2370
+ const interacted = React7.useRef(false);
2129
2371
  const revealImages = config.imageLoading.mode !== "none";
2130
2372
  const revealStyle = (loaded) => revealImages ? {
2131
2373
  filter: config.imageLoading.blur && !loaded ? "blur(10px)" : "blur(0px)",
2132
2374
  transition: `opacity 700ms ease, filter ${config.imageLoading.durationMs}ms ${config.imageLoading.easing}`
2133
2375
  } : void 0;
2134
- React6.useEffect(() => {
2376
+ React7.useEffect(() => {
2135
2377
  if (items.length < 2 || autoplayMs <= 0 || prefersReducedMotion()) return;
2136
2378
  const timer = setInterval(() => {
2137
2379
  if (!interacted.current) setIndex((i) => (i + 1) % items.length);
@@ -2224,13 +2466,13 @@ function ProductSlider({
2224
2466
  }) {
2225
2467
  const config = useStorefrontConfig();
2226
2468
  const actions = useStorefrontActions();
2227
- const viewportRef = React6.useRef(null);
2469
+ const viewportRef = React7.useRef(null);
2228
2470
  const components = getStorefrontComponents(config);
2229
2471
  const ProductCardComponent = components.ProductCard ?? ProductCard;
2230
2472
  const { products, loading } = useProducts(
2231
2473
  filter === "new" ? { status: "active", orderBy: "created_at", order: "desc" } : { status: "active" }
2232
2474
  );
2233
- const visible = React6.useMemo(() => {
2475
+ const visible = React7.useMemo(() => {
2234
2476
  let list = products;
2235
2477
  if (filter === "sale") list = list.filter((p) => p.compareAtPrice != null && p.compareAtPrice > p.price);
2236
2478
  if (categoryName) list = list.filter((p) => p.categoryName === categoryName);
@@ -2378,8 +2620,8 @@ function Testimonials({ title, items }) {
2378
2620
  ] }) });
2379
2621
  }
2380
2622
  function NewsletterBand({ title, subtitle }) {
2381
- const [email, setEmail] = React6.useState("");
2382
- const [done, setDone] = React6.useState(false);
2623
+ const [email, setEmail] = React7.useState("");
2624
+ const [done, setDone] = React7.useState(false);
2383
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: [
2384
2626
  /* @__PURE__ */ jsxRuntime.jsx("h2", { className: "sf-heading text-3xl font-bold", children: title ?? "Receba nossas novidades" }),
2385
2627
  subtitle && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-2 opacity-90", children: subtitle }),
@@ -2451,10 +2693,10 @@ function HomePage({ sections }) {
2451
2693
  return /* @__PURE__ */ jsxRuntime.jsx(StorefrontSections, { sections });
2452
2694
  }
2453
2695
  function useProduct(slug2) {
2454
- const [product, setProduct] = React6.useState(null);
2455
- const [loading, setLoading] = React6.useState(true);
2456
- const [error, setError] = React6.useState(null);
2457
- 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(() => {
2458
2700
  let cancelled = false;
2459
2701
  setLoading(true);
2460
2702
  runtime.getShopProvider().listProducts({ slug: slug2, status: "active", limit: 1 }).then((data) => {
@@ -2592,13 +2834,13 @@ function buildWhatsAppHref(url, product) {
2592
2834
  }
2593
2835
  function ProductEnquiryForm({ product, onSuccess }) {
2594
2836
  const config = useStorefrontConfig();
2595
- const [customerName, setCustomerName] = React6.useState("");
2596
- const [customerEmail, setCustomerEmail] = React6.useState("");
2597
- const [customerPhone, setCustomerPhone] = React6.useState("");
2598
- const [subject, setSubject] = React6.useState(defaultSubject(config.enquiry.subjectPrefix, product));
2599
- const [message, setMessage] = React6.useState(`I would like more information about ${product.name}.`);
2600
- const [status, setStatus] = React6.useState("idle");
2601
- 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);
2602
2844
  const submit = async (event) => {
2603
2845
  event.preventDefault();
2604
2846
  setStatus("submitting");
@@ -2760,19 +3002,141 @@ function ProductEnquiryForm({ product, onSuccess }) {
2760
3002
  }
2761
3003
  );
2762
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
+ }
2763
3127
  function ProductDetailPage({ slug: slug2 }) {
2764
3128
  const config = useStorefrontConfig();
2765
3129
  const actions = useStorefrontActions();
2766
3130
  const { product, loading } = useProduct(slug2);
2767
3131
  const addItem = useCartStore((s) => s.addItem);
2768
- const [qty, setQty] = React6.useState(1);
2769
- const [selectedOptions, setSelectedOptions] = React6.useState({});
3132
+ const [qty, setQty] = React7.useState(1);
3133
+ const [selectedOptions, setSelectedOptions] = React7.useState({});
2770
3134
  const optionGroups = product ? getProductOptionGroups(product) : [];
2771
3135
  useStorefrontHead({
2772
3136
  title: product ? `${product.name} \u2014 ${config.name}` : config.name,
2773
3137
  description: product?.description
2774
3138
  });
2775
- React6.useEffect(() => {
3139
+ React7.useEffect(() => {
2776
3140
  if (!product) return;
2777
3141
  const defaults = Object.fromEntries(
2778
3142
  getProductOptionGroups(product).map((group) => [group.label, group.values[0] ?? ""])
@@ -2800,6 +3164,7 @@ function ProductDetailPage({ slug: slug2 }) {
2800
3164
  const image = product.images.find((i) => i.isPrimary) ?? product.images[0];
2801
3165
  const components = getStorefrontComponents(config);
2802
3166
  const ProductDetailComponent = components.ProductDetail;
3167
+ const GalleryComponent = components.ProductGallery ?? ProductGallery;
2803
3168
  const addToCart = () => addItem(product, qty, selectedOptions);
2804
3169
  const openEnquiry = () => {
2805
3170
  document.getElementById("storefront-product-enquiry")?.scrollIntoView({ behavior: "smooth", block: "start" });
@@ -2833,19 +3198,13 @@ function ProductDetailPage({ slug: slug2 }) {
2833
3198
  ] }),
2834
3199
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid animate-fade-up gap-10 md:grid-cols-2", children: [
2835
3200
  /* @__PURE__ */ jsxRuntime.jsx(
2836
- "div",
3201
+ GalleryComponent,
2837
3202
  {
2838
- ...storefrontComponentContracts.productDetail.gallery,
2839
- className: "group overflow-hidden border bg-muted",
2840
- style: { borderRadius: "var(--sf-radius-card)" },
2841
- children: image && /* @__PURE__ */ jsxRuntime.jsx(
2842
- SmoothImage,
2843
- {
2844
- src: image.url,
2845
- alt: image.altText ?? product.name,
2846
- className: "aspect-square w-full object-cover transition-transform duration-700 ease-out group-hover:scale-105"
2847
- }
2848
- )
3203
+ product,
3204
+ images: product.images,
3205
+ primaryImage: image,
3206
+ config,
3207
+ commerceMode: config.commerceMode
2849
3208
  }
2850
3209
  ),
2851
3210
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col py-2 lg:sticky lg:top-24 lg:self-start", children: [
@@ -2943,6 +3302,7 @@ function ProductDetailPage({ slug: slug2 }) {
2943
3302
  children: "Voltar ao cat\xE1logo"
2944
3303
  }
2945
3304
  ) }),
3305
+ config.commerceMode === "checkout" && /* @__PURE__ */ jsxRuntime.jsx(DeliveryEstimator, {}),
2946
3306
  config.commerceMode === "checkout" && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mt-6 grid grid-cols-3 gap-3 border-t pt-6 text-center", children: [
2947
3307
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col items-center gap-1.5 text-[11px] font-medium text-muted-foreground", children: [
2948
3308
  /* @__PURE__ */ jsxRuntime.jsx(LucideIcons.Truck, { className: "h-5 w-5 text-primary" }),
@@ -2968,9 +3328,10 @@ function ProductDetailPage({ slug: slug2 }) {
2968
3328
  /* @__PURE__ */ jsxRuntime.jsx(RelatedProducts, { product })
2969
3329
  ] });
2970
3330
  }
2971
- function formatDeliveryNotes(address) {
3331
+ function formatDeliveryNotes(address, shippingAddress) {
3332
+ if (address?.notes?.trim()) return address.notes.trim();
3333
+ if (shippingAddress) return void 0;
2972
3334
  if (!address) return void 0;
2973
- if (address.notes?.trim()) return address.notes.trim();
2974
3335
  const parts = [address.street, address.city, address.zip].map((part) => part?.trim()).filter(Boolean);
2975
3336
  return parts.length > 0 ? `Entrega: ${parts.join(", ")}` : void 0;
2976
3337
  }
@@ -2980,6 +3341,8 @@ async function placeStorefrontOrder({
2980
3341
  session,
2981
3342
  customer,
2982
3343
  address,
3344
+ shippingAddress,
3345
+ paymentMethod,
2983
3346
  markPaid = false
2984
3347
  }) {
2985
3348
  const email = customer.email.trim().toLowerCase();
@@ -2994,9 +3357,14 @@ async function placeStorefrontOrder({
2994
3357
  customerId: customerId ?? void 0,
2995
3358
  customer: { name, email },
2996
3359
  currency: config.currency,
2997
- notes: formatDeliveryNotes(address),
3360
+ notes: formatDeliveryNotes(address, shippingAddress),
2998
3361
  discountCode: cart.discountCode ?? void 0,
2999
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,
3000
3368
  items: cart.lines.map((line) => ({
3001
3369
  productId: line.productId,
3002
3370
  quantity: line.quantity,
@@ -3010,11 +3378,11 @@ async function placeStorefrontOrder({
3010
3378
  return { order, customerId: customerId ?? order.customerId ?? "" };
3011
3379
  }
3012
3380
  function SignInModal({ defaultEmail = "", onClose, onSignedIn }) {
3013
- const [email, setEmail] = React6.useState(defaultEmail);
3014
- const [password, setPassword] = React6.useState("");
3015
- const [busy, setBusy] = React6.useState(false);
3016
- const [error, setError] = React6.useState(null);
3017
- 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(() => {
3018
3386
  const onKey = (event) => {
3019
3387
  if (event.key === "Escape") onClose();
3020
3388
  };
@@ -3119,19 +3487,18 @@ function SignInModal({ defaultEmail = "", onClose, onSignedIn }) {
3119
3487
  }
3120
3488
  );
3121
3489
  }
3122
- var SAVED_ADDRESSES = [
3123
- { id: "home", label: "Casa", street: "Rua das Flores, 123", city: "S\xE3o Paulo", zip: "01310-100" }
3124
- ];
3125
- var SAVED_PAYMENT_METHODS = [
3126
- { id: "visa", label: "Visa final 4242", card: "4242 4242 4242 4242", expiry: "12/29", cvc: "123" }
3127
- ];
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
+ };
3128
3498
  var PROCESSING_STEPS = [
3129
3499
  { icon: LucideIcons.Lock, label: "Validando seus dados..." },
3130
- { icon: LucideIcons.CreditCard, label: "Processando pagamento..." },
3131
- { icon: LucideIcons.PackageCheck, label: "Confirmando seu pedido..." }
3500
+ { icon: LucideIcons.PackageCheck, label: "Registrando seu pedido..." }
3132
3501
  ];
3133
- var DEFAULT_ADDRESS = SAVED_ADDRESSES[0];
3134
- var DEFAULT_PAYMENT = SAVED_PAYMENT_METHODS[0];
3135
3502
  var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
3136
3503
  function ProcessingOverlay({ step }) {
3137
3504
  const current = PROCESSING_STEPS[Math.min(step, PROCESSING_STEPS.length - 1)];
@@ -3174,41 +3541,69 @@ function CheckoutPage() {
3174
3541
  const config = useStorefrontConfig();
3175
3542
  const cart = useCartStore();
3176
3543
  const session = useSessionStore();
3544
+ const delivery = useDeliveryStore();
3177
3545
  const validateDiscount2 = useDiscountValidator();
3178
3546
  useStorefrontHead({ title: `Checkout \u2014 ${config.name}` });
3179
- const [selectedAddressId, setSelectedAddressId] = React6.useState(DEFAULT_ADDRESS.id);
3180
- const [selectedPaymentId, setSelectedPaymentId] = React6.useState(DEFAULT_PAYMENT.id);
3181
- const [savedAddresses, setSavedAddresses] = React6.useState(SAVED_ADDRESSES);
3182
- const [savedPaymentMethods, setSavedPaymentMethods] = React6.useState(SAVED_PAYMENT_METHODS);
3183
- const [addressMode, setAddressMode] = React6.useState("saved");
3184
- const [paymentMode, setPaymentMode] = React6.useState("saved");
3185
- 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({
3186
3553
  email: session.email ?? "",
3187
3554
  name: session.name ?? "",
3188
- street: DEFAULT_ADDRESS.street,
3189
- city: DEFAULT_ADDRESS.city,
3190
- zip: DEFAULT_ADDRESS.zip,
3191
- card: DEFAULT_PAYMENT.card,
3192
- expiry: DEFAULT_PAYMENT.expiry,
3193
- cvc: DEFAULT_PAYMENT.cvc
3555
+ street: "",
3556
+ city: "",
3557
+ zip: "",
3558
+ number: "",
3559
+ complement: "",
3560
+ district: "",
3561
+ state: ""
3194
3562
  });
3195
- const [error, setError] = React6.useState(null);
3196
- const [discountCode, setDiscountCode] = React6.useState("");
3197
- const [discountError, setDiscountError] = React6.useState(null);
3198
- const [discountSuccess, setDiscountSuccess] = React6.useState(null);
3199
- const [applyingDiscount, setApplyingDiscount] = React6.useState(false);
3200
- const [placing, setPlacing] = React6.useState(false);
3201
- const [processingStep, setProcessingStep] = React6.useState(0);
3202
- 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");
3203
3572
  const subtotal = selectSubtotal(cart);
3204
3573
  const discountTotal = selectDiscountTotal(cart);
3205
3574
  const shipping = selectShipping(cart, config);
3206
3575
  const total = selectTotal(cart, config);
3207
3576
  const money = (value) => formatMoney(value, config.currency, config.locale);
3208
- React6.useEffect(() => {
3577
+ React7.useEffect(() => {
3209
3578
  if (cart.lines.length === 0 && !placing) navigateTo(config.catalogPath);
3210
3579
  }, [cart.lines.length, config.catalogPath, placing]);
3211
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
+ }
3212
3607
  async function applyDiscountCode() {
3213
3608
  setApplyingDiscount(true);
3214
3609
  setDiscountError(null);
@@ -3235,10 +3630,54 @@ function CheckoutPage() {
3235
3630
  setDiscountSuccess(null);
3236
3631
  setDiscountError(null);
3237
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]);
3238
3668
  function applySavedAddress(address) {
3239
3669
  setAddressMode("saved");
3240
3670
  setSelectedAddressId(address.id);
3241
- 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
+ }));
3242
3681
  }
3243
3682
  function selectAddress(addressId) {
3244
3683
  const address = savedAddresses.find((item) => item.id === addressId);
@@ -3247,37 +3686,7 @@ function CheckoutPage() {
3247
3686
  function addNewAddress() {
3248
3687
  setAddressMode("new");
3249
3688
  setSelectedAddressId("new");
3250
- setForm((current) => ({ ...current, street: "", city: "", zip: "" }));
3251
- }
3252
- function removeAddress(addressId) {
3253
- const nextAddresses = savedAddresses.filter((item) => item.id !== addressId);
3254
- setSavedAddresses(nextAddresses);
3255
- if (selectedAddressId !== addressId) return;
3256
- const fallback = nextAddresses[0];
3257
- if (fallback) applySavedAddress(fallback);
3258
- else addNewAddress();
3259
- }
3260
- function applySavedPayment(method) {
3261
- setPaymentMode("saved");
3262
- setSelectedPaymentId(method.id);
3263
- setForm((current) => ({ ...current, card: method.card, expiry: method.expiry, cvc: method.cvc }));
3264
- }
3265
- function selectPayment(paymentId) {
3266
- const method = savedPaymentMethods.find((item) => item.id === paymentId);
3267
- if (method) applySavedPayment(method);
3268
- }
3269
- function addNewPayment() {
3270
- setPaymentMode("new");
3271
- setSelectedPaymentId("new");
3272
- setForm((current) => ({ ...current, card: "", expiry: "", cvc: "" }));
3273
- }
3274
- function removePayment(paymentId) {
3275
- const nextMethods = savedPaymentMethods.filter((item) => item.id !== paymentId);
3276
- setSavedPaymentMethods(nextMethods);
3277
- if (selectedPaymentId !== paymentId) return;
3278
- const fallback = nextMethods[0];
3279
- if (fallback) applySavedPayment(fallback);
3280
- else addNewPayment();
3689
+ setForm((current) => ({ ...current, street: "", city: "", zip: "", number: "", complement: "", district: "", state: "" }));
3281
3690
  }
3282
3691
  function validate() {
3283
3692
  setError(null);
@@ -3289,12 +3698,20 @@ function CheckoutPage() {
3289
3698
  setError("Informe seu nome completo.");
3290
3699
  return false;
3291
3700
  }
3292
- if (!form.street.trim() || !form.city.trim() || !form.zip.trim()) {
3701
+ if (!form.street.trim() || !form.city.trim() || !form.zip.trim() || !form.district.trim()) {
3293
3702
  setError("Preencha ou selecione o endere\xE7o de entrega.");
3294
3703
  return false;
3295
3704
  }
3296
- if (!/^\d{16}$/.test(form.card.replace(/\s+/g, "")) || !form.expiry.trim() || !form.cvc.trim()) {
3297
- 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.");
3298
3715
  return false;
3299
3716
  }
3300
3717
  return true;
@@ -3316,9 +3733,22 @@ function CheckoutPage() {
3316
3733
  session,
3317
3734
  customer: { email: form.email, name: form.name },
3318
3735
  address: { street: form.street, city: form.city, zip: form.zip },
3319
- // Demo only: mock mode instantly approves. Real Pix/Mercado Pago (M4)
3320
- // leaves the order pending until the payment webhook confirms.
3321
- 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.
3322
3752
  });
3323
3753
  await minDuration;
3324
3754
  cart.clear();
@@ -3391,35 +3821,25 @@ function CheckoutPage() {
3391
3821
  /* @__PURE__ */ jsxRuntime.jsxs("section", { children: [
3392
3822
  /* @__PURE__ */ jsxRuntime.jsx("h2", { className: "mb-3 text-xl font-semibold tracking-tight", children: "Endere\xE7o de entrega" }),
3393
3823
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mb-3 grid gap-3", children: [
3394
- savedAddresses.map((address) => /* @__PURE__ */ jsxRuntime.jsxs(
3824
+ savedAddresses.map((address) => /* @__PURE__ */ jsxRuntime.jsx(
3395
3825
  "div",
3396
3826
  {
3397
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"}`,
3398
- children: [
3399
- /* @__PURE__ */ jsxRuntime.jsxs("button", { type: "button", onClick: () => selectAddress(address.id), className: "block w-full text-left", children: [
3400
- /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex items-center justify-between gap-3 font-semibold", children: [
3401
- address.label,
3402
- 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" })
3403
- ] }),
3404
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "mt-1 block text-muted-foreground", children: address.street }),
3405
- /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "block text-muted-foreground", children: [
3406
- address.city,
3407
- " - ",
3408
- address.zip
3409
- ] })
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" })
3410
3832
  ] }),
3411
- /* @__PURE__ */ jsxRuntime.jsx(
3412
- "button",
3413
- {
3414
- type: "button",
3415
- onClick: () => removeAddress(address.id),
3416
- 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",
3417
- "aria-label": `Remover endere\xE7o ${address.label}`,
3418
- title: "Remover endere\xE7o",
3419
- children: /* @__PURE__ */ jsxRuntime.jsx(LucideIcons.Trash2, { className: "h-4 w-4" })
3420
- }
3421
- )
3422
- ]
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
+ ] })
3423
3843
  },
3424
3844
  address.id
3425
3845
  )),
@@ -3430,83 +3850,62 @@ function CheckoutPage() {
3430
3850
  onClick: addNewAddress,
3431
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"}`,
3432
3852
  children: [
3433
- /* @__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" }),
3434
3854
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "mt-1 block text-muted-foreground", children: "Preencher outro endere\xE7o para esta compra" })
3435
3855
  ]
3436
3856
  }
3437
3857
  )
3438
3858
  ] }),
3439
3859
  addressMode === "new" && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid gap-3", children: [
3440
- field("Endere\xE7o", TID.checkoutStreet, form.street, set("street"), { placeholder: "Endere\xE7o" }),
3441
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid gap-3 sm:grid-cols-[1fr_160px]", children: [
3442
- field("Cidade", TID.checkoutCity, form.city, set("city"), { placeholder: "Cidade" }),
3443
- field("CEP", TID.checkoutZip, form.zip, set("zip"), { placeholder: "CEP" })
3444
- ] })
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" })
3445
3878
  ] })
3446
3879
  ] }),
3447
3880
  /* @__PURE__ */ jsxRuntime.jsxs("section", { children: [
3448
3881
  /* @__PURE__ */ jsxRuntime.jsx("h2", { className: "mb-3 text-xl font-semibold tracking-tight", children: "Pagamento" }),
3449
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mb-3 grid gap-3", children: [
3450
- savedPaymentMethods.map((method) => /* @__PURE__ */ jsxRuntime.jsxs(
3451
- "div",
3452
- {
3453
- 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"}`,
3454
- children: [
3455
- /* @__PURE__ */ jsxRuntime.jsxs("button", { type: "button", onClick: () => selectPayment(method.id), className: "block w-full text-left", children: [
3456
- /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex items-center justify-between gap-3 font-semibold", children: [
3457
- /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex items-center gap-2", children: [
3458
- /* @__PURE__ */ jsxRuntime.jsx(LucideIcons.CreditCard, { className: "h-4 w-4" }),
3459
- method.label
3460
- ] }),
3461
- 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" })
3462
- ] }),
3463
- /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "mt-1 block text-muted-foreground", children: [
3464
- "Expira em ",
3465
- method.expiry
3466
- ] })
3467
- ] }),
3468
- /* @__PURE__ */ jsxRuntime.jsx(
3469
- "button",
3470
- {
3471
- type: "button",
3472
- onClick: () => removePayment(method.id),
3473
- 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",
3474
- "aria-label": `Remover cart\xE3o ${method.label}`,
3475
- title: "Remover cart\xE3o",
3476
- children: /* @__PURE__ */ jsxRuntime.jsx(LucideIcons.Trash2, { className: "h-4 w-4" })
3477
- }
3478
- )
3479
- ]
3480
- },
3481
- method.id
3482
- )),
3483
- /* @__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(
3484
3886
  "button",
3485
3887
  {
3486
3888
  type: "button",
3487
- onClick: addNewPayment,
3488
- 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"}`,
3489
3894
  children: [
3490
- /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex items-center gap-2 font-semibold text-primary", children: [
3491
- /* @__PURE__ */ jsxRuntime.jsx(LucideIcons.CreditCard, { className: "h-4 w-4" }),
3492
- "+ 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" })
3493
3901
  ] }),
3494
- /* @__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 })
3495
3903
  ]
3496
- }
3497
- )
3498
- ] }),
3499
- paymentMode === "new" && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid gap-3", children: [
3500
- field("N\xFAmero do cart\xE3o", TID.checkoutCard, form.card, set("card"), {
3501
- placeholder: "N\xFAmero do cart\xE3o",
3502
- inputMode: "numeric"
3503
- }),
3504
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid gap-3 sm:grid-cols-2", children: [
3505
- field("Validade", TID.checkoutExpiry, form.expiry, set("expiry"), { placeholder: "MM/AA" }),
3506
- field("CVC", TID.checkoutCvc, form.cvc, set("cvc"), { placeholder: "CVC", inputMode: "numeric" })
3507
- ] })
3508
- ] }),
3509
- /* @__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." })
3510
3909
  ] }),
3511
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 })
3512
3911
  ] }),
@@ -3860,11 +4259,11 @@ function SuccessMark() {
3860
4259
  }
3861
4260
  function OrderConfirmationPage({ orderId }) {
3862
4261
  const config = useStorefrontConfig();
3863
- const [order, setOrder] = React6.useState(null);
3864
- const [loading, setLoading] = React6.useState(true);
4262
+ const [order, setOrder] = React7.useState(null);
4263
+ const [loading, setLoading] = React7.useState(true);
3865
4264
  const money = (v) => formatMoney(v, config.currency, config.locale);
3866
4265
  useStorefrontHead({ title: order ? `Pedido #${order.orderNumber} \u2014 ${config.name}` : config.name });
3867
- React6.useEffect(() => {
4266
+ React7.useEffect(() => {
3868
4267
  let cancelled = false;
3869
4268
  runtime.getShopProvider().getOrder(orderId).then((o) => {
3870
4269
  if (!cancelled) setOrder(o);
@@ -3996,11 +4395,11 @@ function OrderConfirmationPage({ orderId }) {
3996
4395
  function useMyOrders() {
3997
4396
  const customerId = useSessionStore((s) => s.customerId);
3998
4397
  const email = useSessionStore((s) => s.email);
3999
- const [orders, setOrders] = React6.useState([]);
4000
- const [loading, setLoading] = React6.useState(true);
4001
- const [tick, setTick] = React6.useState(0);
4002
- const refresh = React6.useCallback(() => setTick((t) => t + 1), []);
4003
- 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(() => {
4004
4403
  let cancelled = false;
4005
4404
  if (!customerId && !email) {
4006
4405
  setOrders([]);
@@ -4022,12 +4421,12 @@ function useMyOrders() {
4022
4421
  return { orders, loading, refresh };
4023
4422
  }
4024
4423
  function AuthForm() {
4025
- const [mode, setMode] = React6.useState("signin");
4026
- const [email, setEmail] = React6.useState("");
4027
- const [password, setPassword] = React6.useState("");
4028
- const [name, setName] = React6.useState("");
4029
- const [busy, setBusy] = React6.useState(false);
4030
- 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);
4031
4430
  async function submit(e) {
4032
4431
  e.preventDefault();
4033
4432
  setError(null);
@@ -4169,9 +4568,9 @@ function OrdersPanel() {
4169
4568
  }
4170
4569
  function ProfilePanel() {
4171
4570
  const session = useSessionStore();
4172
- const [name, setName] = React6.useState(session.name ?? "");
4173
- const [saving, setSaving] = React6.useState(false);
4174
- 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);
4175
4574
  async function save(e) {
4176
4575
  e.preventDefault();
4177
4576
  const trimmed = name.trim();
@@ -4267,7 +4666,7 @@ var SECTION_SUBTITLE = {
4267
4666
  function MyPurchasesPage() {
4268
4667
  const config = useStorefrontConfig();
4269
4668
  const session = useSessionStore();
4270
- const [section, setSection] = React6.useState("orders");
4669
+ const [section, setSection] = React7.useState("orders");
4271
4670
  useStorefrontHead({ title: `Minha conta \u2014 ${config.name}` });
4272
4671
  if (!session.email) {
4273
4672
  return /* @__PURE__ */ jsxRuntime.jsx("main", { className: "mx-auto max-w-3xl px-4 py-12 sm:px-6", children: /* @__PURE__ */ jsxRuntime.jsx(AuthForm, {}) });
@@ -4330,7 +4729,7 @@ function PolicyPage({ kind }) {
4330
4729
  ] }) })
4331
4730
  ] });
4332
4731
  }
4333
- var StorefrontErrorBoundary = class extends React6__default.default.Component {
4732
+ var StorefrontErrorBoundary = class extends React7__default.default.Component {
4334
4733
  constructor() {
4335
4734
  super(...arguments);
4336
4735
  this.state = { error: null };
@@ -4374,7 +4773,7 @@ var ACCENT = {
4374
4773
  function ToastCard({ toast: toast2 }) {
4375
4774
  const dismiss = useToastStore((s) => s.dismiss);
4376
4775
  const Icon2 = ICON[toast2.variant];
4377
- React6.useEffect(() => {
4776
+ React7.useEffect(() => {
4378
4777
  const timer = window.setTimeout(() => dismiss(toast2.id), toast2.durationMs);
4379
4778
  return () => window.clearTimeout(timer);
4380
4779
  }, [toast2.id, toast2.durationMs, dismiss]);
@@ -4625,9 +5024,9 @@ function manifestToStorefrontConfig(manifest, surfaceName) {
4625
5024
  };
4626
5025
  }
4627
5026
  function StorefrontScaffold({ manifest, surface }) {
4628
- const config = React6__default.default.useMemo(() => manifestToStorefrontConfig(manifest, surface), [manifest, surface]);
4629
- const resolved = React6__default.default.useMemo(() => resolveConfig(config), [config]);
4630
- 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);
4631
5030
  if (!inited.current) {
4632
5031
  initStorefrontRuntime(config);
4633
5032
  inited.current = true;