@numueg/theme-sdk 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs ADDED
@@ -0,0 +1,2669 @@
1
+ import { createContext, useContext, useMemo, useState, useEffect, useCallback, useRef, createElement, Component } from 'react';
2
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
3
+
4
+ // src/hooks/useShop.ts
5
+ var ShopContext = createContext(null);
6
+ var ProductContext = createContext(null);
7
+ var CollectionContext = createContext(null);
8
+ var CartContext = createContext(null);
9
+ var CustomerContext = createContext(null);
10
+ var ThemeSettingsContext = createContext(null);
11
+ var LocalizationContext = createContext(null);
12
+ var PageContext = createContext(null);
13
+ function useLocalization() {
14
+ const ctx = useContext(LocalizationContext);
15
+ if (!ctx) throw new Error("useLocalization must be used within NuMuProvider");
16
+ return ctx;
17
+ }
18
+ function useDirection() {
19
+ const { direction } = useLocalization();
20
+ return direction;
21
+ }
22
+ function useLocale() {
23
+ const { locale } = useLocalization();
24
+ return locale;
25
+ }
26
+ function useTranslation() {
27
+ const { translations, locale } = useLocalization();
28
+ return {
29
+ t: (key, fallback) => translations[key] || fallback || key,
30
+ locale
31
+ };
32
+ }
33
+ function useFieldTranslation(entity, field) {
34
+ const { locale } = useLocalization();
35
+ if (!entity) return void 0;
36
+ const translated = entity.attributes && typeof entity.attributes === "object" ? entity.attributes[`${field}_${locale}`] : void 0;
37
+ if (typeof translated === "string" && translated) return translated;
38
+ const base = entity[field];
39
+ if (typeof base === "string") return base;
40
+ if (base == null) return void 0;
41
+ return String(base);
42
+ }
43
+ function useNumberFormat() {
44
+ const { formatNumber } = useLocalization();
45
+ return formatNumber;
46
+ }
47
+
48
+ // src/hooks/useShop.ts
49
+ var DEFAULT_PROTOCOL = typeof window !== "undefined" ? window.location.protocol.replace(":", "") : "https";
50
+ function resolveDomain(store) {
51
+ if (store.domain) return store.domain;
52
+ if (store.subdomain) {
53
+ const platform = typeof window !== "undefined" && window.__NUMU_PLATFORM_DOMAIN || "numueg.app";
54
+ return `${store.subdomain}.${platform}`;
55
+ }
56
+ return store.slug;
57
+ }
58
+ function useShop() {
59
+ const ctx = useContext(ShopContext);
60
+ if (!ctx) throw new Error("useShop must be used within NuMuProvider");
61
+ const locale = useLocale();
62
+ return useMemo(() => {
63
+ const domain = resolveDomain(ctx);
64
+ const settings = ctx.settings;
65
+ const localePrefixEnabled = Boolean(
66
+ settings && settings.locale_url_prefix_enabled
67
+ );
68
+ const defaultLocale = ctx.default_language || "en";
69
+ const prefixedLocales = settings?.locale_url_prefix_locales || null;
70
+ const shouldPrefix = (l) => {
71
+ if (!localePrefixEnabled) return false;
72
+ if (prefixedLocales) return prefixedLocales.includes(l);
73
+ return l !== defaultLocale;
74
+ };
75
+ const formatUrl = (path) => {
76
+ if (!path) return `${DEFAULT_PROTOCOL}://${domain}/`;
77
+ if (/^https?:\/\//i.test(path)) return path;
78
+ if (path.startsWith("//")) return `${DEFAULT_PROTOCOL}:${path}`;
79
+ let normalized = path.startsWith("/") ? path : `/${path}`;
80
+ if (locale && shouldPrefix(locale)) {
81
+ const prefix = `/${locale}/`;
82
+ const root = `/${locale}`;
83
+ if (normalized !== root && !normalized.startsWith(prefix)) {
84
+ normalized = `${root}${normalized}`;
85
+ }
86
+ }
87
+ return `${DEFAULT_PROTOCOL}://${domain}${normalized}`;
88
+ };
89
+ return { ...ctx, domain, formatUrl };
90
+ }, [ctx, locale]);
91
+ }
92
+ function useProduct() {
93
+ const ctx = useContext(ProductContext);
94
+ if (!ctx) throw new Error("useProduct must be used within ProductProvider");
95
+ return ctx;
96
+ }
97
+ function useProductOptional() {
98
+ return useContext(ProductContext);
99
+ }
100
+ function useCollection() {
101
+ const ctx = useContext(CollectionContext);
102
+ if (!ctx) throw new Error("useCollection must be used within CollectionProvider");
103
+ return ctx;
104
+ }
105
+ function useCollectionOptional() {
106
+ return useContext(CollectionContext);
107
+ }
108
+ function useCart() {
109
+ const ctx = useContext(CartContext);
110
+ if (!ctx) throw new Error("useCart must be used within NuMuProvider");
111
+ return ctx;
112
+ }
113
+ function useCustomer() {
114
+ return useContext(CustomerContext);
115
+ }
116
+ function useThemeSettings() {
117
+ const ctx = useContext(ThemeSettingsContext);
118
+ if (!ctx) throw new Error("useThemeSettings must be used within NuMuProvider");
119
+ return ctx;
120
+ }
121
+ function usePage() {
122
+ return useContext(PageContext);
123
+ }
124
+ var SectionContext = createContext(null);
125
+ function useSection() {
126
+ const ctx = useContext(SectionContext);
127
+ if (!ctx) throw new Error("useSection must be used within a section component");
128
+ return ctx;
129
+ }
130
+ function useSectionOptional() {
131
+ return useContext(SectionContext);
132
+ }
133
+
134
+ // src/hooks/useMoney.ts
135
+ function useMoney(currencyOverride) {
136
+ const { formatMoney } = useLocalization();
137
+ const shop = useShop();
138
+ const ccy = currencyOverride || shop?.currency;
139
+ return (amount) => formatMoney(amount, ccy);
140
+ }
141
+ var DEFAULT_WIDTHS = [320, 480, 640, 768, 1024, 1280, 1600, 1920];
142
+ var DEFAULT_SIZES = "(min-width: 1024px) 25vw, (min-width: 640px) 50vw, 100vw";
143
+ function useImage(src, opts = {}) {
144
+ const { sizes = DEFAULT_SIZES, widths = DEFAULT_WIDTHS, alt = "" } = opts;
145
+ return useMemo(() => {
146
+ if (!src) return { src: null, srcSet: null, sizes, alt };
147
+ if (/[?&]w=\d+/.test(src)) return { src, srcSet: null, sizes, alt };
148
+ const sep = src.includes("?") ? "&" : "?";
149
+ const srcSet = widths.map((w) => `${src}${sep}w=${w} ${w}w`).join(", ");
150
+ return { src, srcSet, sizes, alt };
151
+ }, [src, sizes, widths, alt]);
152
+ }
153
+ function useProducts(opts = {}) {
154
+ const { limit, fetchIfMissing = false } = opts;
155
+ const page = usePage();
156
+ const shop = useShop();
157
+ const initial = page?.data?.products ?? null;
158
+ const [products, setProducts] = useState(initial ?? []);
159
+ const [loading, setLoading] = useState(
160
+ initial == null && fetchIfMissing
161
+ );
162
+ const [error, setError] = useState(null);
163
+ useEffect(() => {
164
+ if (initial != null) return;
165
+ if (!fetchIfMissing) return;
166
+ if (!shop?.id) return;
167
+ let cancelled = false;
168
+ (async () => {
169
+ try {
170
+ const params = new URLSearchParams({ store_id: shop.id });
171
+ if (limit) params.set("limit", String(limit));
172
+ const res = await fetch(`/api/products?${params.toString()}`);
173
+ if (!res.ok) throw new Error(`/api/products \u2192 ${res.status}`);
174
+ const data = await res.json();
175
+ if (cancelled) return;
176
+ setProducts(data.products ?? []);
177
+ setLoading(false);
178
+ } catch (err) {
179
+ if (cancelled) return;
180
+ setError(err instanceof Error ? err : new Error(String(err)));
181
+ setLoading(false);
182
+ }
183
+ })();
184
+ return () => {
185
+ cancelled = true;
186
+ };
187
+ }, [initial, fetchIfMissing, limit, shop?.id]);
188
+ const sliced = limit != null ? products.slice(0, limit) : products;
189
+ return { products: sliced, loading, error };
190
+ }
191
+ function useCollections(opts = {}) {
192
+ const { limit, fetchIfMissing = false } = opts;
193
+ const page = usePage();
194
+ const shop = useShop();
195
+ const initial = page?.data?.collections ?? null;
196
+ const [collections, setCollections] = useState(initial ?? []);
197
+ const [loading, setLoading] = useState(
198
+ initial == null && fetchIfMissing
199
+ );
200
+ const [error, setError] = useState(null);
201
+ useEffect(() => {
202
+ if (initial != null) return;
203
+ if (!fetchIfMissing) return;
204
+ if (!shop?.id) return;
205
+ let cancelled = false;
206
+ (async () => {
207
+ try {
208
+ const params = new URLSearchParams({ store_id: shop.id });
209
+ const res = await fetch(`/api/collections?${params.toString()}`);
210
+ if (!res.ok) throw new Error(`/api/collections \u2192 ${res.status}`);
211
+ const data = await res.json();
212
+ if (cancelled) return;
213
+ setCollections(data.collections ?? []);
214
+ setLoading(false);
215
+ } catch (err) {
216
+ if (cancelled) return;
217
+ setError(err instanceof Error ? err : new Error(String(err)));
218
+ setLoading(false);
219
+ }
220
+ })();
221
+ return () => {
222
+ cancelled = true;
223
+ };
224
+ }, [initial, fetchIfMissing, shop?.id]);
225
+ const sliced = limit != null ? collections.slice(0, limit) : collections;
226
+ return { collections: sliced, loading, error };
227
+ }
228
+ var NOOP_ACTIONS = {
229
+ login: async () => ({}),
230
+ register: async () => ({}),
231
+ logout: async () => ({}),
232
+ requestRecover: async () => ({}),
233
+ confirmReset: async () => ({}),
234
+ verifyEmail: async () => ({}),
235
+ resendVerification: async () => ({}),
236
+ updateProfile: async () => ({}),
237
+ changePassword: async () => ({}),
238
+ refresh: async () => void 0
239
+ };
240
+ var CustomerActionsContext = createContext(NOOP_ACTIONS);
241
+
242
+ // src/hooks/useCustomerActions.ts
243
+ function useCustomerActions() {
244
+ return useContext(CustomerActionsContext);
245
+ }
246
+ function unwrap(json) {
247
+ if (json && typeof json === "object" && "data" in json) {
248
+ return json.data;
249
+ }
250
+ return json;
251
+ }
252
+ function useOrders() {
253
+ const customer = useCustomer();
254
+ const [orders, setOrders] = useState([]);
255
+ const [loading, setLoading] = useState(false);
256
+ const [error, setError] = useState(null);
257
+ const [tick, setTick] = useState(0);
258
+ useEffect(() => {
259
+ if (typeof window === "undefined") return;
260
+ if (!customer) {
261
+ setOrders([]);
262
+ setError(null);
263
+ setLoading(false);
264
+ return;
265
+ }
266
+ let cancelled = false;
267
+ setLoading(true);
268
+ setError(null);
269
+ void (async () => {
270
+ try {
271
+ const res = await fetch("/api/customer/me/orders", {
272
+ method: "GET",
273
+ credentials: "include",
274
+ cache: "no-store"
275
+ });
276
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
277
+ const body = unwrap(await res.json());
278
+ if (cancelled) return;
279
+ const list = Array.isArray(body) ? body : Array.isArray(body?.items) ? body.items : [];
280
+ setOrders(list);
281
+ } catch (err) {
282
+ if (cancelled) return;
283
+ setError(err instanceof Error ? err : new Error(String(err)));
284
+ } finally {
285
+ if (!cancelled) setLoading(false);
286
+ }
287
+ })();
288
+ return () => {
289
+ cancelled = true;
290
+ };
291
+ }, [customer, tick]);
292
+ return {
293
+ orders,
294
+ loading,
295
+ error,
296
+ refresh: () => setTick((t) => t + 1)
297
+ };
298
+ }
299
+ function useOrder(id) {
300
+ const customer = useCustomer();
301
+ const [order, setOrder] = useState(null);
302
+ const [loading, setLoading] = useState(false);
303
+ const [error, setError] = useState(null);
304
+ const [tick, setTick] = useState(0);
305
+ useEffect(() => {
306
+ if (typeof window === "undefined") return;
307
+ if (!customer || !id) {
308
+ setOrder(null);
309
+ setError(null);
310
+ setLoading(false);
311
+ return;
312
+ }
313
+ let cancelled = false;
314
+ setLoading(true);
315
+ setError(null);
316
+ void (async () => {
317
+ try {
318
+ const res = await fetch(
319
+ `/api/customer/me/orders/${encodeURIComponent(id)}`,
320
+ {
321
+ method: "GET",
322
+ credentials: "include",
323
+ cache: "no-store"
324
+ }
325
+ );
326
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
327
+ const body = unwrap(await res.json());
328
+ if (cancelled) return;
329
+ setOrder(body);
330
+ } catch (err) {
331
+ if (cancelled) return;
332
+ setError(err instanceof Error ? err : new Error(String(err)));
333
+ } finally {
334
+ if (!cancelled) setLoading(false);
335
+ }
336
+ })();
337
+ return () => {
338
+ cancelled = true;
339
+ };
340
+ }, [customer, id, tick]);
341
+ return {
342
+ order,
343
+ loading,
344
+ error,
345
+ refresh: () => setTick((t) => t + 1)
346
+ };
347
+ }
348
+ function unwrap2(json) {
349
+ if (json && typeof json === "object" && "data" in json) {
350
+ return json.data;
351
+ }
352
+ return json;
353
+ }
354
+ async function readBody(res) {
355
+ try {
356
+ return unwrap2(await res.json());
357
+ } catch {
358
+ return null;
359
+ }
360
+ }
361
+ function useCustomerAddresses() {
362
+ const customer = useCustomer();
363
+ const [addresses, setAddresses] = useState([]);
364
+ const [loading, setLoading] = useState(false);
365
+ const [error, setError] = useState(null);
366
+ const [tick, setTick] = useState(0);
367
+ useEffect(() => {
368
+ if (typeof window === "undefined") return;
369
+ if (!customer) {
370
+ setAddresses([]);
371
+ setLoading(false);
372
+ setError(null);
373
+ return;
374
+ }
375
+ let cancelled = false;
376
+ setLoading(true);
377
+ setError(null);
378
+ void (async () => {
379
+ try {
380
+ const res = await fetch("/api/customer/me/addresses", {
381
+ method: "GET",
382
+ credentials: "include",
383
+ cache: "no-store"
384
+ });
385
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
386
+ const body = await readBody(res);
387
+ if (cancelled) return;
388
+ const list = Array.isArray(body) ? body : Array.isArray(body?.items) ? body.items : [];
389
+ setAddresses(list);
390
+ } catch (err) {
391
+ if (cancelled) return;
392
+ setError(err instanceof Error ? err : new Error(String(err)));
393
+ } finally {
394
+ if (!cancelled) setLoading(false);
395
+ }
396
+ })();
397
+ return () => {
398
+ cancelled = true;
399
+ };
400
+ }, [customer, tick]);
401
+ const refresh = useCallback(() => setTick((t) => t + 1), []);
402
+ function readCsrf() {
403
+ if (typeof document === "undefined") return null;
404
+ const m = document.cookie.match(/(?:^|; )numu_csrf=([^;]+)/);
405
+ return m ? decodeURIComponent(m[1]) : null;
406
+ }
407
+ function mutationHeaders() {
408
+ const h = { "Content-Type": "application/json" };
409
+ const csrf = readCsrf();
410
+ if (csrf) h["x-numu-csrf"] = csrf;
411
+ return h;
412
+ }
413
+ const addAddress = useCallback(
414
+ async (input) => {
415
+ const res = await fetch("/api/customer/me/addresses", {
416
+ method: "POST",
417
+ credentials: "include",
418
+ headers: mutationHeaders(),
419
+ body: JSON.stringify(input)
420
+ });
421
+ if (!res.ok) return null;
422
+ const created = await readBody(res);
423
+ refresh();
424
+ return created;
425
+ },
426
+ [refresh]
427
+ );
428
+ const updateAddress = useCallback(
429
+ async (id, input) => {
430
+ const res = await fetch(
431
+ `/api/customer/me/addresses/${encodeURIComponent(id)}`,
432
+ {
433
+ method: "PUT",
434
+ credentials: "include",
435
+ headers: mutationHeaders(),
436
+ body: JSON.stringify(input)
437
+ }
438
+ );
439
+ if (!res.ok) return null;
440
+ const updated = await readBody(res);
441
+ refresh();
442
+ return updated;
443
+ },
444
+ [refresh]
445
+ );
446
+ const deleteAddress = useCallback(
447
+ async (id) => {
448
+ const res = await fetch(
449
+ `/api/customer/me/addresses/${encodeURIComponent(id)}`,
450
+ {
451
+ method: "DELETE",
452
+ credentials: "include",
453
+ headers: mutationHeaders()
454
+ }
455
+ );
456
+ if (!res.ok) return false;
457
+ refresh();
458
+ return true;
459
+ },
460
+ [refresh]
461
+ );
462
+ const setDefaultAddress = useCallback(
463
+ async (id) => {
464
+ const res = await fetch(
465
+ `/api/customer/me/addresses/${encodeURIComponent(id)}/default`,
466
+ {
467
+ method: "PUT",
468
+ credentials: "include",
469
+ headers: mutationHeaders()
470
+ }
471
+ );
472
+ if (!res.ok) return false;
473
+ refresh();
474
+ return true;
475
+ },
476
+ [refresh]
477
+ );
478
+ return {
479
+ addresses,
480
+ loading,
481
+ error,
482
+ refresh,
483
+ addAddress,
484
+ updateAddress,
485
+ deleteAddress,
486
+ setDefaultAddress
487
+ };
488
+ }
489
+ var cache = /* @__PURE__ */ new Map();
490
+ function useNavigation(handle, options) {
491
+ const [items, setItems] = useState(
492
+ options?.initialItems ?? cache.get(handle) ?? []
493
+ );
494
+ const [loading, setLoading] = useState(
495
+ !options?.initialItems && !cache.has(handle)
496
+ );
497
+ const [error, setError] = useState(null);
498
+ useEffect(() => {
499
+ if (typeof window === "undefined") return;
500
+ if (!handle) {
501
+ setItems([]);
502
+ setLoading(false);
503
+ return;
504
+ }
505
+ if (cache.has(handle) && !options?.initialItems) {
506
+ setItems(cache.get(handle) || []);
507
+ setLoading(false);
508
+ return;
509
+ }
510
+ let cancelled = false;
511
+ setLoading(true);
512
+ setError(null);
513
+ void (async () => {
514
+ try {
515
+ const res = await fetch(
516
+ `/api/storefront/navigation/${encodeURIComponent(handle)}`,
517
+ { cache: "no-store" }
518
+ );
519
+ if (!res.ok) {
520
+ if (cancelled) return;
521
+ cache.set(handle, []);
522
+ setItems([]);
523
+ return;
524
+ }
525
+ const json = await res.json();
526
+ const list = Array.isArray(json) ? json : json.items || [];
527
+ if (cancelled) return;
528
+ cache.set(handle, list);
529
+ setItems(list);
530
+ } catch (err) {
531
+ if (cancelled) return;
532
+ setError(err instanceof Error ? err : new Error(String(err)));
533
+ setItems([]);
534
+ } finally {
535
+ if (!cancelled) setLoading(false);
536
+ }
537
+ })();
538
+ return () => {
539
+ cancelled = true;
540
+ };
541
+ }, [handle]);
542
+ return { items, loading, error };
543
+ }
544
+ var EMPTY = {
545
+ products: [],
546
+ collections: [],
547
+ pages: [],
548
+ articles: [],
549
+ total: 0
550
+ };
551
+ function useSearch(query, options = {}) {
552
+ const {
553
+ mode = "predictive",
554
+ types,
555
+ limit,
556
+ debounceMs = 200,
557
+ immediate = false
558
+ } = options;
559
+ const [results, setResults] = useState(EMPTY);
560
+ const [loading, setLoading] = useState(false);
561
+ const [error, setError] = useState(null);
562
+ const lastRequestId = useRef(0);
563
+ useEffect(() => {
564
+ if (typeof window === "undefined") return;
565
+ const trimmed = query.trim();
566
+ if (!trimmed) {
567
+ setResults(EMPTY);
568
+ setLoading(false);
569
+ setError(null);
570
+ return;
571
+ }
572
+ const requestId = ++lastRequestId.current;
573
+ setLoading(true);
574
+ setError(null);
575
+ const run = async () => {
576
+ try {
577
+ const params = new URLSearchParams({ q: trimmed, mode });
578
+ if (types?.length) params.set("types", types.join(","));
579
+ if (limit) params.set("limit", String(limit));
580
+ const res = await fetch(`/api/storefront/search?${params}`, {
581
+ cache: "no-store"
582
+ });
583
+ if (requestId !== lastRequestId.current) return;
584
+ if (!res.ok) {
585
+ setResults(EMPTY);
586
+ return;
587
+ }
588
+ const json = await res.json();
589
+ const body = json.data || json;
590
+ if (requestId !== lastRequestId.current) return;
591
+ setResults({
592
+ products: body.products || [],
593
+ collections: body.collections || [],
594
+ pages: body.pages || [],
595
+ articles: body.articles || [],
596
+ total: body.total || 0
597
+ });
598
+ } catch (err) {
599
+ if (requestId !== lastRequestId.current) return;
600
+ setError(err instanceof Error ? err : new Error(String(err)));
601
+ setResults(EMPTY);
602
+ } finally {
603
+ if (requestId === lastRequestId.current) setLoading(false);
604
+ }
605
+ };
606
+ if (immediate || mode === "full") {
607
+ void run();
608
+ return;
609
+ }
610
+ const t = setTimeout(run, debounceMs);
611
+ return () => clearTimeout(t);
612
+ }, [query, mode, debounceMs, immediate, limit, types?.join(",")]);
613
+ return { query, results, loading, error };
614
+ }
615
+ function useAnalytics() {
616
+ const track = useCallback(
617
+ (eventName, payload = {}) => {
618
+ if (typeof window === "undefined") return;
619
+ try {
620
+ window.dispatchEvent(
621
+ new CustomEvent("numu:analytics:event", {
622
+ detail: { event: eventName, payload, ts: Date.now() }
623
+ })
624
+ );
625
+ } catch {
626
+ }
627
+ void (async () => {
628
+ try {
629
+ await fetch("/api/storefront/track", {
630
+ method: "POST",
631
+ headers: { "Content-Type": "application/json" },
632
+ body: JSON.stringify({
633
+ event: eventName,
634
+ payload,
635
+ ts: Date.now()
636
+ }),
637
+ keepalive: true
638
+ // survive page-unload (e.g. begin_checkout → redirect)
639
+ });
640
+ } catch {
641
+ }
642
+ })();
643
+ },
644
+ []
645
+ );
646
+ return useMemo(() => ({ track }), [track]);
647
+ }
648
+ function useApp(slug) {
649
+ const shop = useShop();
650
+ const [state, setState] = useState({ data: null, loading: true, available: false, error: null });
651
+ const fetchApp = useCallback(async () => {
652
+ if (!slug) {
653
+ setState({ data: null, loading: false, available: false, error: null });
654
+ return;
655
+ }
656
+ setState((s) => ({ ...s, loading: true, error: null }));
657
+ try {
658
+ const url = `/api/storefront/apps/${encodeURIComponent(slug)}`;
659
+ const res = await fetch(url, {
660
+ credentials: "include",
661
+ headers: { Accept: "application/json" }
662
+ });
663
+ if (res.status === 404) {
664
+ setState({
665
+ data: null,
666
+ loading: false,
667
+ available: false,
668
+ error: null
669
+ });
670
+ return;
671
+ }
672
+ if (!res.ok) {
673
+ throw new Error(`useApp(${slug}) failed: HTTP ${res.status}`);
674
+ }
675
+ const body = await res.json();
676
+ const payload = body.data;
677
+ if (!payload) {
678
+ setState({
679
+ data: null,
680
+ loading: false,
681
+ available: false,
682
+ error: null
683
+ });
684
+ return;
685
+ }
686
+ setState({
687
+ data: payload,
688
+ loading: false,
689
+ available: payload.available !== false,
690
+ error: null
691
+ });
692
+ } catch (e) {
693
+ setState({
694
+ data: null,
695
+ loading: false,
696
+ available: false,
697
+ error: e instanceof Error ? e : new Error(String(e))
698
+ });
699
+ }
700
+ }, [slug, shop.id]);
701
+ useEffect(() => {
702
+ void fetchApp();
703
+ }, [fetchApp]);
704
+ return { ...state, refresh: fetchApp };
705
+ }
706
+ function storageKey(storeId) {
707
+ return `numu_wishlist_${storeId}`;
708
+ }
709
+ function readLocal(storeId) {
710
+ if (typeof window === "undefined") return [];
711
+ try {
712
+ const raw = window.localStorage.getItem(storageKey(storeId));
713
+ if (!raw) return [];
714
+ const parsed = JSON.parse(raw);
715
+ return Array.isArray(parsed) ? parsed : [];
716
+ } catch {
717
+ return [];
718
+ }
719
+ }
720
+ function writeLocal(storeId, items) {
721
+ if (typeof window === "undefined") return;
722
+ try {
723
+ window.localStorage.setItem(storageKey(storeId), JSON.stringify(items));
724
+ } catch {
725
+ }
726
+ }
727
+ function sameItem(a, productId, variantId) {
728
+ return a.product_id === productId && (a.variant_id ?? null) === (variantId ?? null);
729
+ }
730
+ function useWishlist(storeId) {
731
+ const customer = useCustomer();
732
+ const [items, setItems] = useState([]);
733
+ const [loading, setLoading] = useState(true);
734
+ useEffect(() => {
735
+ if (typeof window === "undefined") return;
736
+ setItems(readLocal(storeId));
737
+ setLoading(false);
738
+ }, [storeId, customer?.id]);
739
+ const has = useCallback(
740
+ (productId, variantId) => items.some((it) => sameItem(it, productId, variantId)),
741
+ [items]
742
+ );
743
+ const addToWishlist = useCallback(
744
+ (productId, variantId) => {
745
+ setItems((prev) => {
746
+ if (prev.some((it) => sameItem(it, productId, variantId))) {
747
+ return prev;
748
+ }
749
+ const next = [
750
+ ...prev,
751
+ {
752
+ product_id: productId,
753
+ variant_id: variantId ?? null,
754
+ added_at: Date.now()
755
+ }
756
+ ];
757
+ writeLocal(storeId, next);
758
+ return next;
759
+ });
760
+ },
761
+ [storeId]
762
+ );
763
+ const removeFromWishlist = useCallback(
764
+ (productId, variantId) => {
765
+ setItems((prev) => {
766
+ const next = prev.filter((it) => !sameItem(it, productId, variantId));
767
+ writeLocal(storeId, next);
768
+ return next;
769
+ });
770
+ },
771
+ [storeId]
772
+ );
773
+ const clear = useCallback(() => {
774
+ setItems([]);
775
+ writeLocal(storeId, []);
776
+ }, [storeId]);
777
+ return { items, loading, has, addToWishlist, removeFromWishlist, clear };
778
+ }
779
+ function useRelatedProducts(productId, options = {}) {
780
+ const limit = options.limit ?? 4;
781
+ const [items, setItems] = useState([]);
782
+ const [loading, setLoading] = useState(false);
783
+ const [error, setError] = useState(null);
784
+ useEffect(() => {
785
+ if (typeof window === "undefined") return;
786
+ if (!productId) {
787
+ setItems([]);
788
+ setLoading(false);
789
+ setError(null);
790
+ return;
791
+ }
792
+ let cancelled = false;
793
+ setLoading(true);
794
+ setError(null);
795
+ void (async () => {
796
+ try {
797
+ const res = await fetch(
798
+ `/api/storefront/products/${encodeURIComponent(productId)}/related?limit=${limit}`,
799
+ { cache: "no-store" }
800
+ );
801
+ if (!res.ok) {
802
+ if (cancelled) return;
803
+ setItems([]);
804
+ return;
805
+ }
806
+ const json = await res.json();
807
+ const list = Array.isArray(json) ? json : Array.isArray(json.data) ? json.data : Array.isArray(json.items) ? json.items : [];
808
+ if (cancelled) return;
809
+ setItems(list);
810
+ } catch (err) {
811
+ if (cancelled) return;
812
+ setError(err instanceof Error ? err : new Error(String(err)));
813
+ setItems([]);
814
+ } finally {
815
+ if (!cancelled) setLoading(false);
816
+ }
817
+ })();
818
+ return () => {
819
+ cancelled = true;
820
+ };
821
+ }, [productId, limit]);
822
+ return { items, loading, error };
823
+ }
824
+ var COOKIE_NAME = "numu_currency";
825
+ var COOKIE_MAX_AGE = 60 * 60 * 24 * 30;
826
+ function readCookie(name) {
827
+ if (typeof document === "undefined") return null;
828
+ const match = document.cookie.match(
829
+ new RegExp(`(?:^|;\\s*)${name}=([^;]+)`)
830
+ );
831
+ return match ? decodeURIComponent(match[1]) : null;
832
+ }
833
+ function writeCookie(name, value) {
834
+ if (typeof document === "undefined") return;
835
+ document.cookie = `${name}=${encodeURIComponent(value)}; path=/; max-age=${COOKIE_MAX_AGE}; SameSite=Lax`;
836
+ }
837
+ function useCurrency() {
838
+ const shop = useShop();
839
+ const [config, setConfig] = useState(null);
840
+ const [loading, setLoading] = useState(true);
841
+ const [selected, setSelectedState] = useState("");
842
+ useEffect(() => {
843
+ let cancelled = false;
844
+ (async () => {
845
+ try {
846
+ const res = await fetch(`/api/storefront/currencies`, {
847
+ credentials: "include",
848
+ headers: { Accept: "application/json" }
849
+ });
850
+ if (!res.ok) throw new Error(`currencies: HTTP ${res.status}`);
851
+ const body = await res.json();
852
+ if (cancelled) return;
853
+ setConfig(body.data);
854
+ const cookie = readCookie(COOKIE_NAME);
855
+ const valid = cookie && body.data.presentment.includes(cookie) ? cookie : null;
856
+ setSelectedState(valid || body.data.default_presentment || body.data.base);
857
+ } catch {
858
+ if (cancelled) return;
859
+ setConfig(null);
860
+ setSelectedState(shop.currency || "EGP");
861
+ } finally {
862
+ if (!cancelled) setLoading(false);
863
+ }
864
+ })();
865
+ return () => {
866
+ cancelled = true;
867
+ };
868
+ }, [shop.id, shop.currency]);
869
+ const rates = useMemo(() => {
870
+ const out = {};
871
+ if (!config) return out;
872
+ for (const [k, v] of Object.entries(config.rates)) {
873
+ const n = Number.parseFloat(v);
874
+ if (Number.isFinite(n)) out[k] = n;
875
+ }
876
+ return out;
877
+ }, [config]);
878
+ const convert = useCallback(
879
+ (cents, target) => {
880
+ if (!config) return cents;
881
+ const to = target || selected;
882
+ if (!to || to === config.base) return cents;
883
+ const rate = rates[to];
884
+ if (!rate || !Number.isFinite(rate)) return cents;
885
+ return Math.round(cents * rate);
886
+ },
887
+ [config, rates, selected]
888
+ );
889
+ const setSelected = useCallback((currency) => {
890
+ setSelectedState(currency);
891
+ writeCookie(COOKIE_NAME, currency);
892
+ }, []);
893
+ return {
894
+ base: config?.base || shop.currency || "EGP",
895
+ selected: selected || config?.base || shop.currency || "EGP",
896
+ presentment: config?.presentment || [shop.currency || "EGP"],
897
+ rates,
898
+ autoConvert: Boolean(config?.auto_convert),
899
+ loading,
900
+ setSelected,
901
+ convert
902
+ };
903
+ }
904
+
905
+ // src/utils/variants.ts
906
+ function findVariantByOptions(product, selection) {
907
+ const variants = product.variants || [];
908
+ if (variants.length === 0) return null;
909
+ if (variants.length === 1 && Object.keys(selection).length === 0) {
910
+ return variants[0];
911
+ }
912
+ for (const v of variants) {
913
+ const opts = v.option_values || v.options || {};
914
+ const matches = Object.entries(selection).every(
915
+ ([axis, value]) => opts[axis] === value
916
+ );
917
+ if (matches && Object.keys(selection).length === Object.keys(opts).length) {
918
+ return v;
919
+ }
920
+ }
921
+ return null;
922
+ }
923
+ function defaultVariant(product) {
924
+ const variants = product.variants || [];
925
+ if (variants.length === 0) return null;
926
+ const inStock = variants.find((v) => v.is_in_stock || v.in_stock);
927
+ return inStock || variants[0];
928
+ }
929
+ function availableValues(product, selection) {
930
+ const axes = product.options || [];
931
+ const variants = product.variants || [];
932
+ const out = {};
933
+ for (const axis of axes) {
934
+ out[axis.name] = /* @__PURE__ */ new Set();
935
+ for (const v of variants) {
936
+ const opts = v.option_values || v.options || {};
937
+ const compatible = Object.entries(selection).every(
938
+ ([k, val]) => k === axis.name || opts[k] === val
939
+ );
940
+ if (compatible && opts[axis.name]) {
941
+ out[axis.name].add(opts[axis.name]);
942
+ }
943
+ }
944
+ }
945
+ return out;
946
+ }
947
+ function useVariantSelection(product, opts = {}) {
948
+ const autoSelect = opts.autoSelect ?? true;
949
+ const initial = useMemo(() => {
950
+ if (!autoSelect) return {};
951
+ const dv = defaultVariant(product);
952
+ if (!dv) return {};
953
+ const opts2 = dv.option_values || dv.options || {};
954
+ return { ...opts2 };
955
+ }, [product, autoSelect]);
956
+ const [selection, setSelection] = useState(initial);
957
+ const select = useCallback((axis, value) => {
958
+ setSelection((prev) => ({ ...prev, [axis]: value }));
959
+ }, []);
960
+ const reset = useCallback(() => setSelection({}), []);
961
+ const variant = useMemo(
962
+ () => findVariantByOptions(product, selection),
963
+ [product, selection]
964
+ );
965
+ const availability = useMemo(
966
+ () => availableValues(product, selection),
967
+ [product, selection]
968
+ );
969
+ const isComplete = useMemo(() => {
970
+ const axes = product.options || [];
971
+ if (axes.length === 0) return true;
972
+ return axes.every((a) => Boolean(selection[a.name]));
973
+ }, [product, selection]);
974
+ return { selection, variant, select, reset, availability, isComplete };
975
+ }
976
+ function useGiftCardBalance() {
977
+ const [balance, setBalance] = useState(null);
978
+ const [loading, setLoading] = useState(false);
979
+ const [error, setError] = useState(null);
980
+ const check = useCallback(async (code) => {
981
+ if (typeof window === "undefined") return null;
982
+ const trimmed = (code || "").trim();
983
+ if (!trimmed) {
984
+ setError(new Error("Enter a gift card code."));
985
+ setBalance(null);
986
+ return null;
987
+ }
988
+ setLoading(true);
989
+ setError(null);
990
+ try {
991
+ const res = await fetch(
992
+ `/api/gift-cards/${encodeURIComponent(trimmed)}`,
993
+ { cache: "no-store" }
994
+ );
995
+ if (!res.ok) {
996
+ setBalance(null);
997
+ if (res.status === 404) {
998
+ setError(new Error("That gift card isn't valid or has been used up."));
999
+ return null;
1000
+ }
1001
+ setError(new Error(`Couldn't check gift card (HTTP ${res.status}).`));
1002
+ return null;
1003
+ }
1004
+ const json = await res.json();
1005
+ const data = json?.data;
1006
+ if (!data) {
1007
+ setBalance(null);
1008
+ setError(new Error("Unexpected response from server."));
1009
+ return null;
1010
+ }
1011
+ setBalance(data);
1012
+ return data;
1013
+ } catch (err) {
1014
+ setBalance(null);
1015
+ setError(err instanceof Error ? err : new Error(String(err)));
1016
+ return null;
1017
+ } finally {
1018
+ setLoading(false);
1019
+ }
1020
+ }, []);
1021
+ const reset = useCallback(() => {
1022
+ setBalance(null);
1023
+ setError(null);
1024
+ }, []);
1025
+ return { balance, loading, error, check, reset };
1026
+ }
1027
+ function useReorder() {
1028
+ const [result, setResult] = useState(null);
1029
+ const [loading, setLoading] = useState(false);
1030
+ const [error, setError] = useState(null);
1031
+ const reorder = useCallback(
1032
+ async (orderId) => {
1033
+ if (typeof window === "undefined") return null;
1034
+ if (!orderId) {
1035
+ setError(new Error("Order id is required."));
1036
+ return null;
1037
+ }
1038
+ setLoading(true);
1039
+ setError(null);
1040
+ try {
1041
+ const csrf = typeof document !== "undefined" ? document.cookie.match(/(?:^|; )numu_csrf=([^;]+)/)?.[1] ?? "" : "";
1042
+ const res = await fetch(
1043
+ `/api/customer/orders/${encodeURIComponent(orderId)}/reorder`,
1044
+ {
1045
+ method: "POST",
1046
+ headers: csrf ? { "x-numu-csrf": csrf } : {},
1047
+ credentials: "include"
1048
+ }
1049
+ );
1050
+ if (!res.ok) {
1051
+ if (res.status === 401) {
1052
+ setError(new Error("Please sign in to reorder."));
1053
+ } else if (res.status === 404) {
1054
+ setError(new Error("That order can't be found on your account."));
1055
+ } else {
1056
+ setError(new Error(`Couldn't reorder (HTTP ${res.status}).`));
1057
+ }
1058
+ return null;
1059
+ }
1060
+ const json = await res.json();
1061
+ const data = json?.data;
1062
+ if (!data) {
1063
+ setError(new Error("Unexpected response from server."));
1064
+ return null;
1065
+ }
1066
+ setResult(data);
1067
+ return data;
1068
+ } catch (err) {
1069
+ setError(err instanceof Error ? err : new Error(String(err)));
1070
+ return null;
1071
+ } finally {
1072
+ setLoading(false);
1073
+ }
1074
+ },
1075
+ []
1076
+ );
1077
+ const reset = useCallback(() => {
1078
+ setResult(null);
1079
+ setError(null);
1080
+ }, []);
1081
+ return { result, loading, error, reorder, reset };
1082
+ }
1083
+ var STORAGE_KEY = "numu_checkout_state";
1084
+ var EMPTY_STATE = {
1085
+ email: "",
1086
+ phone: "",
1087
+ shipping_address: {},
1088
+ selected_shipping_rate_id: null,
1089
+ shipping_method: null,
1090
+ payment_method: null,
1091
+ cod_requested: false,
1092
+ deposit_gateway: null,
1093
+ saved_payment_method_id: null,
1094
+ customer_notes: "",
1095
+ coupon_code: ""
1096
+ };
1097
+ function readState() {
1098
+ if (typeof window === "undefined") return { ...EMPTY_STATE };
1099
+ try {
1100
+ const raw = window.sessionStorage.getItem(STORAGE_KEY);
1101
+ if (!raw) return { ...EMPTY_STATE };
1102
+ return { ...EMPTY_STATE, ...JSON.parse(raw) };
1103
+ } catch {
1104
+ return { ...EMPTY_STATE };
1105
+ }
1106
+ }
1107
+ function writeState(state) {
1108
+ if (typeof window === "undefined") return;
1109
+ try {
1110
+ window.sessionStorage.setItem(STORAGE_KEY, JSON.stringify(state));
1111
+ window.dispatchEvent(new CustomEvent("numu:checkout:updated"));
1112
+ } catch {
1113
+ }
1114
+ }
1115
+ function hasContact(s) {
1116
+ return Boolean(
1117
+ s.email && s.shipping_address?.line1 && s.shipping_address?.city && s.shipping_address?.country
1118
+ );
1119
+ }
1120
+ function hasShipping(s) {
1121
+ return hasContact(s) && Boolean(s.selected_shipping_rate_id);
1122
+ }
1123
+ function hasPayment(s) {
1124
+ return hasShipping(s) && Boolean(s.payment_method);
1125
+ }
1126
+ function resolveStep(s) {
1127
+ if (!hasContact(s)) return "contact";
1128
+ if (!hasShipping(s)) return "shipping";
1129
+ if (!hasPayment(s)) return "payment";
1130
+ return "review";
1131
+ }
1132
+ function useCheckout() {
1133
+ const [state, setState] = useState(() => readState());
1134
+ const [rates, setRates] = useState(null);
1135
+ const [ratesLoading, setRatesLoading] = useState(false);
1136
+ useEffect(() => {
1137
+ function onChange() {
1138
+ setState(readState());
1139
+ }
1140
+ window.addEventListener("numu:checkout:updated", onChange);
1141
+ window.addEventListener("storage", (e) => {
1142
+ if (e.key === STORAGE_KEY) onChange();
1143
+ });
1144
+ return () => {
1145
+ window.removeEventListener("numu:checkout:updated", onChange);
1146
+ };
1147
+ }, []);
1148
+ const patch = useCallback((partial) => {
1149
+ setState((prev) => {
1150
+ const next = { ...prev, ...partial };
1151
+ writeState(next);
1152
+ return next;
1153
+ });
1154
+ }, []);
1155
+ const refreshShipping = useCallback(async () => {
1156
+ setRatesLoading(true);
1157
+ try {
1158
+ const res = await fetch("/api/shipping/options", {
1159
+ method: "POST",
1160
+ headers: { "Content-Type": "application/json" },
1161
+ body: JSON.stringify({ shipping_address: state.shipping_address })
1162
+ });
1163
+ if (!res.ok) {
1164
+ setRates([]);
1165
+ return [];
1166
+ }
1167
+ const body = await res.json();
1168
+ const list = body?.data?.options || body?.data || body?.options || [];
1169
+ setRates(list);
1170
+ return list;
1171
+ } catch {
1172
+ setRates([]);
1173
+ return [];
1174
+ } finally {
1175
+ setRatesLoading(false);
1176
+ }
1177
+ }, [state.shipping_address]);
1178
+ const placeOrder = useCallback(async () => {
1179
+ const payload = {
1180
+ line_items: [],
1181
+ shipping_address: state.shipping_address,
1182
+ payment_method: state.payment_method,
1183
+ selected_shipping_rate_id: state.selected_shipping_rate_id,
1184
+ shipping_method: state.shipping_method,
1185
+ guest_email: state.email,
1186
+ cod_requested: state.cod_requested,
1187
+ deposit_gateway: state.deposit_gateway,
1188
+ saved_payment_method_id: state.saved_payment_method_id,
1189
+ customer_notes: state.customer_notes || null,
1190
+ coupon_code: state.coupon_code || null
1191
+ };
1192
+ const res = await fetch("/api/checkout", {
1193
+ method: "POST",
1194
+ headers: {
1195
+ "Content-Type": "application/json",
1196
+ // Idempotency-Key prevents a double-click double-charge.
1197
+ "Idempotency-Key": typeof crypto !== "undefined" && "randomUUID" in crypto ? crypto.randomUUID() : `${Date.now()}-${Math.random()}`
1198
+ },
1199
+ body: JSON.stringify(payload)
1200
+ });
1201
+ const body = await res.json();
1202
+ if (!res.ok) {
1203
+ const detail = body?.detail || body?.error || `Checkout failed (${res.status})`;
1204
+ throw new Error(typeof detail === "string" ? detail : JSON.stringify(detail));
1205
+ }
1206
+ const data = body?.data || body;
1207
+ return data;
1208
+ }, [state]);
1209
+ const step = useMemo(() => resolveStep(state), [state]);
1210
+ const api = useMemo(
1211
+ () => ({
1212
+ state,
1213
+ step,
1214
+ contact: {
1215
+ set: (input) => patch({
1216
+ ...input.email !== void 0 && { email: input.email },
1217
+ ...input.phone !== void 0 && { phone: input.phone },
1218
+ ...input.shipping_address && {
1219
+ shipping_address: {
1220
+ ...state.shipping_address,
1221
+ ...input.shipping_address
1222
+ },
1223
+ // Invalidate downstream picks when address changes.
1224
+ selected_shipping_rate_id: null,
1225
+ shipping_method: null
1226
+ }
1227
+ }),
1228
+ isComplete: () => hasContact(state)
1229
+ },
1230
+ shipping: {
1231
+ rates,
1232
+ loading: ratesLoading,
1233
+ refresh: refreshShipping,
1234
+ select: (rateId) => {
1235
+ const rate = rates?.find((r) => r.id === rateId);
1236
+ patch({
1237
+ selected_shipping_rate_id: rateId,
1238
+ shipping_method: rate?.name || null
1239
+ });
1240
+ },
1241
+ isComplete: () => hasShipping(state)
1242
+ },
1243
+ payment: {
1244
+ select: (method, opts) => patch({
1245
+ payment_method: method,
1246
+ cod_requested: method === "cod",
1247
+ deposit_gateway: opts?.deposit_gateway || null,
1248
+ saved_payment_method_id: opts?.saved_payment_method_id || null
1249
+ }),
1250
+ isComplete: () => hasPayment(state)
1251
+ },
1252
+ setNotes: (notes) => patch({ customer_notes: notes }),
1253
+ setCoupon: (code) => patch({ coupon_code: code }),
1254
+ placeOrder,
1255
+ reset: () => {
1256
+ if (typeof window !== "undefined") {
1257
+ try {
1258
+ window.sessionStorage.removeItem(STORAGE_KEY);
1259
+ } catch {
1260
+ }
1261
+ }
1262
+ setState({ ...EMPTY_STATE });
1263
+ setRates(null);
1264
+ }
1265
+ }),
1266
+ [state, step, rates, ratesLoading, refreshShipping, placeOrder, patch]
1267
+ );
1268
+ return api;
1269
+ }
1270
+ function isAddressSufficient(a) {
1271
+ if (!a) return false;
1272
+ return Boolean(a.country);
1273
+ }
1274
+ function useShippingRates({
1275
+ address,
1276
+ location_id,
1277
+ enabled = true
1278
+ } = {}) {
1279
+ const [rates, setRates] = useState(null);
1280
+ const [loading, setLoading] = useState(false);
1281
+ const [error, setError] = useState(null);
1282
+ const refresh = useCallback(async () => {
1283
+ if (!isAddressSufficient(address)) {
1284
+ setRates(null);
1285
+ return;
1286
+ }
1287
+ setLoading(true);
1288
+ setError(null);
1289
+ try {
1290
+ const res = await fetch("/api/shipping/options", {
1291
+ method: "POST",
1292
+ headers: { "Content-Type": "application/json" },
1293
+ body: JSON.stringify({
1294
+ shipping_address: address,
1295
+ ...location_id ? { location_id } : {}
1296
+ })
1297
+ });
1298
+ if (!res.ok) {
1299
+ if (res.status === 404) {
1300
+ setRates([]);
1301
+ return;
1302
+ }
1303
+ throw new Error(`shipping rates: HTTP ${res.status}`);
1304
+ }
1305
+ const body = await res.json();
1306
+ const list = body?.data?.options || body?.data || body?.options || [];
1307
+ setRates(list);
1308
+ } catch (e) {
1309
+ setError(e instanceof Error ? e : new Error(String(e)));
1310
+ setRates([]);
1311
+ } finally {
1312
+ setLoading(false);
1313
+ }
1314
+ }, [address, location_id]);
1315
+ useEffect(() => {
1316
+ if (!enabled) return;
1317
+ void refresh();
1318
+ }, [enabled, refresh]);
1319
+ return { rates, loading, error, refresh };
1320
+ }
1321
+ var RTL_LOCALES = ["ar", "he", "fa", "ur"];
1322
+ var EMPTY_CART = {
1323
+ id: "",
1324
+ items: [],
1325
+ subtotal: 0,
1326
+ total: 0,
1327
+ currency: "EGP"
1328
+ };
1329
+ function readCsrfCookie() {
1330
+ if (typeof document === "undefined") return null;
1331
+ const match = document.cookie.match(/(?:^|;\s*)numu_csrf=([^;]+)/);
1332
+ return match ? decodeURIComponent(match[1]) : null;
1333
+ }
1334
+ async function postCartMutation(endpoint, body, applyCart, reserveToken) {
1335
+ const headers = {
1336
+ "Content-Type": "application/json"
1337
+ };
1338
+ const csrf = readCsrfCookie();
1339
+ if (csrf) headers["x-numu-csrf"] = csrf;
1340
+ const idempotencyKey = typeof crypto !== "undefined" && "randomUUID" in crypto ? crypto.randomUUID() : `${Date.now()}-${Math.random().toString(36).slice(2)}`;
1341
+ headers["x-numu-idempotency-key"] = idempotencyKey;
1342
+ const res = await fetch(endpoint, {
1343
+ method: body === void 0 ? "DELETE" : "POST",
1344
+ headers,
1345
+ body: body === void 0 ? void 0 : JSON.stringify(body)
1346
+ });
1347
+ if (!res.ok) return;
1348
+ const data = await res.json();
1349
+ applyCart(data);
1350
+ }
1351
+ function NuMuProvider({
1352
+ store,
1353
+ themeSettings,
1354
+ initialCart,
1355
+ customer,
1356
+ locale: initialLocale,
1357
+ translations: initialTranslations,
1358
+ children
1359
+ }) {
1360
+ const [cart, setCart] = useState(
1361
+ initialCart || { ...EMPTY_CART, currency: store.currency }
1362
+ );
1363
+ const [loading, setLoading] = useState(false);
1364
+ const [locale, setLocale] = useState(() => {
1365
+ if (initialLocale) return initialLocale;
1366
+ if (typeof document !== "undefined") {
1367
+ const m = document.cookie.match(/(?:^|; )numu_locale=([^;]+)/);
1368
+ if (m) return decodeURIComponent(m[1]);
1369
+ }
1370
+ return store.default_language || "en";
1371
+ });
1372
+ const [translations] = useState(initialTranslations || {});
1373
+ const [customerState, setCustomerState] = useState(
1374
+ customer ?? null
1375
+ );
1376
+ useEffect(() => {
1377
+ if (typeof window === "undefined") return;
1378
+ if (customer) return;
1379
+ let cancelled = false;
1380
+ void (async () => {
1381
+ try {
1382
+ const res = await fetch("/api/customer/me", {
1383
+ method: "GET",
1384
+ credentials: "include",
1385
+ cache: "no-store"
1386
+ });
1387
+ if (cancelled) return;
1388
+ if (!res.ok) return;
1389
+ const json = await res.json();
1390
+ const next = json && typeof json === "object" && "data" in json ? json.data : json;
1391
+ if (next && typeof next === "object") {
1392
+ setCustomerState(next);
1393
+ }
1394
+ } catch {
1395
+ }
1396
+ })();
1397
+ return () => {
1398
+ cancelled = true;
1399
+ };
1400
+ }, []);
1401
+ const refreshCustomer = useCallback(async () => {
1402
+ if (typeof window === "undefined") return;
1403
+ try {
1404
+ const res = await fetch("/api/customer/me", {
1405
+ method: "GET",
1406
+ credentials: "include",
1407
+ cache: "no-store"
1408
+ });
1409
+ if (res.status === 401) {
1410
+ setCustomerState(null);
1411
+ return;
1412
+ }
1413
+ if (!res.ok) return;
1414
+ const json = await res.json();
1415
+ const next = json && typeof json === "object" && "data" in json ? json.data : json;
1416
+ setCustomerState(next ?? null);
1417
+ } catch {
1418
+ }
1419
+ }, []);
1420
+ function readCsrf() {
1421
+ if (typeof document === "undefined") return null;
1422
+ const m = document.cookie.match(/(?:^|; )numu_csrf=([^;]+)/);
1423
+ return m ? decodeURIComponent(m[1]) : null;
1424
+ }
1425
+ async function postCustomer(path, body) {
1426
+ const headers = {
1427
+ "Content-Type": "application/json"
1428
+ };
1429
+ const csrf = readCsrf();
1430
+ if (csrf) headers["x-numu-csrf"] = csrf;
1431
+ const res = await fetch(path, {
1432
+ method: "POST",
1433
+ credentials: "include",
1434
+ cache: "no-store",
1435
+ headers,
1436
+ body: body !== void 0 ? JSON.stringify(body) : void 0
1437
+ });
1438
+ let json = null;
1439
+ try {
1440
+ json = await res.json();
1441
+ } catch {
1442
+ }
1443
+ return json;
1444
+ }
1445
+ async function putCustomer(path, body) {
1446
+ const headers = {
1447
+ "Content-Type": "application/json"
1448
+ };
1449
+ const csrf = readCsrf();
1450
+ if (csrf) headers["x-numu-csrf"] = csrf;
1451
+ const res = await fetch(path, {
1452
+ method: "PUT",
1453
+ credentials: "include",
1454
+ cache: "no-store",
1455
+ headers,
1456
+ body: JSON.stringify(body ?? {})
1457
+ });
1458
+ try {
1459
+ return await res.json();
1460
+ } catch {
1461
+ return null;
1462
+ }
1463
+ }
1464
+ const customerActions = useMemo(
1465
+ () => ({
1466
+ login: async (input) => {
1467
+ const r = await postCustomer("/api/customer/login", input);
1468
+ await refreshCustomer();
1469
+ return r;
1470
+ },
1471
+ register: async (input) => {
1472
+ const r = await postCustomer("/api/customer/register", input);
1473
+ await refreshCustomer();
1474
+ return r;
1475
+ },
1476
+ logout: async () => {
1477
+ const r = await postCustomer("/api/customer/logout", {});
1478
+ setCustomerState(null);
1479
+ return r;
1480
+ },
1481
+ requestRecover: (input) => postCustomer("/api/customer/recover", input),
1482
+ confirmReset: async (input) => {
1483
+ const r = await postCustomer("/api/customer/reset", input);
1484
+ setCustomerState(null);
1485
+ return r;
1486
+ },
1487
+ verifyEmail: async (input) => {
1488
+ const r = await postCustomer("/api/customer/verify-email", input);
1489
+ await refreshCustomer();
1490
+ return r;
1491
+ },
1492
+ resendVerification: (input) => postCustomer("/api/customer/resend-verification", input),
1493
+ updateProfile: async (input) => {
1494
+ const r = await putCustomer("/api/customer/me", input);
1495
+ await refreshCustomer();
1496
+ return r;
1497
+ },
1498
+ changePassword: (input) => putCustomer("/api/customer/me/password", input),
1499
+ refresh: refreshCustomer
1500
+ }),
1501
+ [refreshCustomer]
1502
+ );
1503
+ useEffect(() => {
1504
+ if (typeof window === "undefined") return;
1505
+ let cancelled = false;
1506
+ void (async () => {
1507
+ try {
1508
+ const res = await fetch("/api/cart", {
1509
+ method: "GET",
1510
+ credentials: "include",
1511
+ cache: "no-store"
1512
+ });
1513
+ if (!res.ok || cancelled) return;
1514
+ const data = await res.json();
1515
+ if (data && typeof data === "object") {
1516
+ setCart(data);
1517
+ }
1518
+ } catch {
1519
+ }
1520
+ })();
1521
+ return () => {
1522
+ cancelled = true;
1523
+ };
1524
+ }, []);
1525
+ const nextRequestId = useRef(0);
1526
+ const latestApplied = useRef(0);
1527
+ const reserveToken = useCallback(() => {
1528
+ nextRequestId.current += 1;
1529
+ return nextRequestId.current;
1530
+ }, []);
1531
+ const buildApplyCart = useCallback(
1532
+ (token) => (newCart) => {
1533
+ if (token < latestApplied.current) {
1534
+ return;
1535
+ }
1536
+ latestApplied.current = token;
1537
+ setCart(newCart);
1538
+ },
1539
+ []
1540
+ );
1541
+ const mutate = useCallback(
1542
+ async (endpoint, body) => {
1543
+ const token = reserveToken();
1544
+ setLoading(true);
1545
+ try {
1546
+ await postCartMutation(
1547
+ endpoint,
1548
+ body,
1549
+ buildApplyCart(token),
1550
+ () => token
1551
+ // already reserved; just return the same token
1552
+ );
1553
+ } finally {
1554
+ setLoading(false);
1555
+ }
1556
+ },
1557
+ [reserveToken, buildApplyCart]
1558
+ );
1559
+ const addItem = useCallback(
1560
+ async (productId, variantId, quantity) => {
1561
+ await mutate("/api/cart/add", {
1562
+ product_id: productId,
1563
+ variant_id: variantId,
1564
+ quantity: quantity || 1
1565
+ });
1566
+ },
1567
+ [mutate]
1568
+ );
1569
+ const removeItem = useCallback(
1570
+ async (itemId) => {
1571
+ await mutate("/api/cart/remove", { item_id: itemId });
1572
+ },
1573
+ [mutate]
1574
+ );
1575
+ const updateQuantity = useCallback(
1576
+ async (itemId, quantity) => {
1577
+ await mutate("/api/cart/update", { item_id: itemId, quantity });
1578
+ },
1579
+ [mutate]
1580
+ );
1581
+ const applyDiscount = useCallback(
1582
+ async (code) => {
1583
+ await mutate("/api/cart/discount", { code });
1584
+ },
1585
+ [mutate]
1586
+ );
1587
+ const removeDiscount = useCallback(async () => {
1588
+ await mutate("/api/cart/discount", void 0);
1589
+ }, [mutate]);
1590
+ const updateNote = useCallback(
1591
+ async (note) => {
1592
+ await mutate("/api/cart/update", { note });
1593
+ },
1594
+ [mutate]
1595
+ );
1596
+ const clearCart = useCallback(async () => {
1597
+ setCart({ ...EMPTY_CART, currency: store.currency });
1598
+ }, [store.currency]);
1599
+ const cartFirstRender = useRef(true);
1600
+ useEffect(() => {
1601
+ if (typeof window === "undefined") return;
1602
+ if (cartFirstRender.current) {
1603
+ cartFirstRender.current = false;
1604
+ return;
1605
+ }
1606
+ window.dispatchEvent(
1607
+ new CustomEvent("numu:cart:updated", { detail: cart })
1608
+ );
1609
+ }, [cart]);
1610
+ const cartValue = useMemo(
1611
+ () => ({
1612
+ cart,
1613
+ addItem,
1614
+ removeItem,
1615
+ updateQuantity,
1616
+ applyDiscount,
1617
+ removeDiscount,
1618
+ updateNote,
1619
+ clearCart,
1620
+ loading
1621
+ }),
1622
+ [
1623
+ cart,
1624
+ addItem,
1625
+ removeItem,
1626
+ updateQuantity,
1627
+ applyDiscount,
1628
+ removeDiscount,
1629
+ updateNote,
1630
+ clearCart,
1631
+ loading
1632
+ ]
1633
+ );
1634
+ const safeCurrency = (store.currency || "USD").toUpperCase();
1635
+ const numeralSystem = store.settings?.numerals === "arabic" ? "arab" : "latn";
1636
+ const intlLocale = `${locale}-u-nu-${numeralSystem}`;
1637
+ const moneyFmt = useMemo(() => {
1638
+ try {
1639
+ return new Intl.NumberFormat(intlLocale, {
1640
+ style: "currency",
1641
+ currency: safeCurrency
1642
+ });
1643
+ } catch {
1644
+ return new Intl.NumberFormat(locale, {
1645
+ style: "currency",
1646
+ currency: safeCurrency
1647
+ });
1648
+ }
1649
+ }, [intlLocale, locale, safeCurrency]);
1650
+ const dateFmt = useMemo(() => {
1651
+ try {
1652
+ return new Intl.DateTimeFormat(intlLocale, {
1653
+ year: "numeric",
1654
+ month: "long",
1655
+ day: "numeric"
1656
+ });
1657
+ } catch {
1658
+ return new Intl.DateTimeFormat(locale, {
1659
+ year: "numeric",
1660
+ month: "long",
1661
+ day: "numeric"
1662
+ });
1663
+ }
1664
+ }, [intlLocale, locale]);
1665
+ const availableLocales = useMemo(() => {
1666
+ const list = store.available_locales;
1667
+ if (Array.isArray(list) && list.length > 0) return list;
1668
+ return [locale];
1669
+ }, [store, locale]);
1670
+ const switchLocale = useCallback((next) => {
1671
+ if (typeof document === "undefined") return;
1672
+ if (!next) return;
1673
+ document.cookie = `numu_locale=${encodeURIComponent(next)}; Path=/; Max-Age=${60 * 60 * 24 * 365}; SameSite=Lax`;
1674
+ setLocale(next);
1675
+ if (typeof window !== "undefined") {
1676
+ window.location.reload();
1677
+ }
1678
+ }, []);
1679
+ const defaultNumberFmt = useMemo(() => {
1680
+ try {
1681
+ return new Intl.NumberFormat(intlLocale);
1682
+ } catch {
1683
+ return new Intl.NumberFormat(locale);
1684
+ }
1685
+ }, [intlLocale, locale]);
1686
+ const localization = useMemo(
1687
+ () => ({
1688
+ locale,
1689
+ direction: RTL_LOCALES.includes(locale) ? "rtl" : "ltr",
1690
+ translations,
1691
+ availableLocales,
1692
+ setLocale: switchLocale,
1693
+ formatMoney: (amount, currency) => {
1694
+ const ccy = (currency || safeCurrency).toUpperCase();
1695
+ if (ccy !== safeCurrency) {
1696
+ try {
1697
+ return new Intl.NumberFormat(intlLocale, {
1698
+ style: "currency",
1699
+ currency: ccy
1700
+ }).format(amount);
1701
+ } catch {
1702
+ return moneyFmt.format(amount);
1703
+ }
1704
+ }
1705
+ return moneyFmt.format(amount);
1706
+ },
1707
+ formatDate: (date) => dateFmt.format(typeof date === "string" ? new Date(date) : date),
1708
+ formatNumber: (n, options) => {
1709
+ if (!options) return defaultNumberFmt.format(n);
1710
+ try {
1711
+ return new Intl.NumberFormat(intlLocale, options).format(n);
1712
+ } catch {
1713
+ return String(n);
1714
+ }
1715
+ }
1716
+ }),
1717
+ [
1718
+ locale,
1719
+ translations,
1720
+ safeCurrency,
1721
+ moneyFmt,
1722
+ dateFmt,
1723
+ availableLocales,
1724
+ switchLocale,
1725
+ intlLocale,
1726
+ defaultNumberFmt
1727
+ ]
1728
+ );
1729
+ return /* @__PURE__ */ jsx(ShopContext.Provider, { value: store, children: /* @__PURE__ */ jsx(ThemeSettingsContext.Provider, { value: themeSettings, children: /* @__PURE__ */ jsx(LocalizationContext.Provider, { value: localization, children: /* @__PURE__ */ jsx(CartContext.Provider, { value: cartValue, children: /* @__PURE__ */ jsx(CustomerContext.Provider, { value: customerState, children: /* @__PURE__ */ jsx(CustomerActionsContext.Provider, { value: customerActions, children }) }) }) }) }) });
1730
+ }
1731
+ function ProductProvider({ product, children }) {
1732
+ return /* @__PURE__ */ jsx(ProductContext.Provider, { value: product, children });
1733
+ }
1734
+ function CollectionProvider({ collection, children }) {
1735
+ return /* @__PURE__ */ jsx(CollectionContext.Provider, { value: collection, children });
1736
+ }
1737
+ function Money({
1738
+ amount,
1739
+ currency,
1740
+ compareAt,
1741
+ className,
1742
+ as = "span"
1743
+ }) {
1744
+ const { formatMoney } = useLocalization();
1745
+ const shop = useShop();
1746
+ const ccy = currency || shop?.currency;
1747
+ const showCompare = compareAt != null && compareAt > amount;
1748
+ const children = [
1749
+ /* @__PURE__ */ jsx("span", { children: formatMoney(amount, ccy) }, "amt")
1750
+ ];
1751
+ if (showCompare) {
1752
+ children.push(" ");
1753
+ children.push(
1754
+ /* @__PURE__ */ jsx("s", { style: { opacity: 0.6 }, children: formatMoney(compareAt, ccy) }, "cmp")
1755
+ );
1756
+ }
1757
+ return createElement(
1758
+ as,
1759
+ { className, dir: "auto" },
1760
+ children
1761
+ );
1762
+ }
1763
+ var DEFAULT_WIDTHS2 = [320, 480, 640, 768, 1024, 1280, 1600, 1920];
1764
+ function buildSrcSet(src, widths = DEFAULT_WIDTHS2) {
1765
+ if (/[?&]w=\d+/.test(src)) return "";
1766
+ const sep = src.includes("?") ? "&" : "?";
1767
+ return widths.map((w) => `${src}${sep}w=${w} ${w}w`).join(", ");
1768
+ }
1769
+ function Image({
1770
+ src,
1771
+ alt,
1772
+ sizes = "(min-width: 1024px) 25vw, (min-width: 640px) 50vw, 100vw",
1773
+ responsive = true,
1774
+ loading = "lazy",
1775
+ className,
1776
+ style,
1777
+ ...rest
1778
+ }) {
1779
+ if (!src) {
1780
+ return /* @__PURE__ */ jsx(
1781
+ "div",
1782
+ {
1783
+ className,
1784
+ role: "img",
1785
+ "aria-label": alt,
1786
+ style: {
1787
+ backgroundColor: "rgba(0,0,0,0.05)",
1788
+ display: "block",
1789
+ ...style
1790
+ }
1791
+ }
1792
+ );
1793
+ }
1794
+ const srcSet = responsive ? buildSrcSet(src) : void 0;
1795
+ return /* @__PURE__ */ jsx(
1796
+ "img",
1797
+ {
1798
+ src,
1799
+ alt,
1800
+ srcSet: srcSet || void 0,
1801
+ sizes: srcSet ? sizes : void 0,
1802
+ loading,
1803
+ decoding: "async",
1804
+ className,
1805
+ style,
1806
+ ...rest
1807
+ }
1808
+ );
1809
+ }
1810
+ var ABSOLUTE_URL = /^[a-z]+:|^\/\//i;
1811
+ function Link({ to, children, ...rest }) {
1812
+ const shop = useShop();
1813
+ const isAbsolute = ABSOLUTE_URL.test(to);
1814
+ let href = to;
1815
+ if (!isAbsolute && shop && !to.startsWith("/")) {
1816
+ href = `/${to}`;
1817
+ }
1818
+ return /* @__PURE__ */ jsx("a", { href, ...rest, children });
1819
+ }
1820
+ function AddToCartButton({
1821
+ product,
1822
+ variant,
1823
+ quantity = 1,
1824
+ label = "Add to cart",
1825
+ loadingLabel = "Adding\u2026",
1826
+ soldOutLabel = "Sold out",
1827
+ errorLabel = "Couldn't add \u2014 try again",
1828
+ onAdded,
1829
+ ...rest
1830
+ }) {
1831
+ const { addItem } = useCart();
1832
+ const [state, setState] = useState("idle");
1833
+ const inStock = variant?.in_stock ?? product.in_stock;
1834
+ if (!inStock) {
1835
+ return /* @__PURE__ */ jsx(
1836
+ "button",
1837
+ {
1838
+ type: "button",
1839
+ disabled: true,
1840
+ "aria-disabled": "true",
1841
+ ...rest,
1842
+ children: soldOutLabel
1843
+ }
1844
+ );
1845
+ }
1846
+ async function handleClick() {
1847
+ if (state === "adding") return;
1848
+ setState("adding");
1849
+ try {
1850
+ await addItem(product.id, variant?.id, quantity);
1851
+ setState("idle");
1852
+ onAdded?.(product, variant);
1853
+ } catch {
1854
+ setState("error");
1855
+ setTimeout(() => setState("idle"), 2e3);
1856
+ }
1857
+ }
1858
+ return /* @__PURE__ */ jsx(
1859
+ "button",
1860
+ {
1861
+ type: "button",
1862
+ onClick: handleClick,
1863
+ disabled: state === "adding",
1864
+ "aria-busy": state === "adding",
1865
+ ...rest,
1866
+ children: state === "adding" ? loadingLabel : state === "error" ? errorLabel : label
1867
+ }
1868
+ );
1869
+ }
1870
+ var SectionErrorBoundary = class extends Component {
1871
+ constructor() {
1872
+ super(...arguments);
1873
+ this.state = { error: null };
1874
+ }
1875
+ static getDerivedStateFromError(error) {
1876
+ return { error };
1877
+ }
1878
+ componentDidCatch(error) {
1879
+ console.error(
1880
+ `[Section ${this.props.sectionType}#${this.props.sectionId}] threw:`,
1881
+ error
1882
+ );
1883
+ if (typeof window !== "undefined" && window.parent !== window) {
1884
+ try {
1885
+ window.parent.postMessage(
1886
+ {
1887
+ type: "numu:editor:section-error",
1888
+ payload: {
1889
+ sectionId: this.props.sectionId,
1890
+ sectionType: this.props.sectionType,
1891
+ message: error.message
1892
+ }
1893
+ },
1894
+ "*"
1895
+ );
1896
+ } catch {
1897
+ }
1898
+ }
1899
+ }
1900
+ render() {
1901
+ if (this.state.error) return this.props.fallback;
1902
+ return this.props.children;
1903
+ }
1904
+ };
1905
+ function defaultSectionFallback(sectionType, sectionId) {
1906
+ return /* @__PURE__ */ jsxs(
1907
+ "div",
1908
+ {
1909
+ role: "alert",
1910
+ "data-numu-section-error": "true",
1911
+ style: {
1912
+ padding: "1rem",
1913
+ border: "1px dashed #f87171",
1914
+ background: "#fef2f2",
1915
+ color: "#b91c1c",
1916
+ fontFamily: "system-ui, -apple-system, BlinkMacSystemFont, sans-serif",
1917
+ fontSize: "0.875rem",
1918
+ lineHeight: 1.5,
1919
+ borderRadius: "0.375rem",
1920
+ margin: "0.5rem 0"
1921
+ },
1922
+ children: [
1923
+ /* @__PURE__ */ jsx("div", { style: { fontWeight: 600, marginBottom: "0.25rem" }, children: "Section failed to render" }),
1924
+ /* @__PURE__ */ jsxs("div", { style: { opacity: 0.9 }, children: [
1925
+ /* @__PURE__ */ jsx("code", { style: { fontFamily: "monospace" }, children: sectionType }),
1926
+ " threw an error. Check the browser console for details."
1927
+ ] }),
1928
+ /* @__PURE__ */ jsxs(
1929
+ "div",
1930
+ {
1931
+ style: {
1932
+ opacity: 0.6,
1933
+ fontSize: "0.75rem",
1934
+ marginTop: "0.25rem",
1935
+ fontFamily: "monospace"
1936
+ },
1937
+ children: [
1938
+ "section #",
1939
+ sectionId
1940
+ ]
1941
+ }
1942
+ )
1943
+ ]
1944
+ }
1945
+ );
1946
+ }
1947
+ function Section({
1948
+ id,
1949
+ type,
1950
+ groupId,
1951
+ errorFallback,
1952
+ children,
1953
+ ...rest
1954
+ }) {
1955
+ return /* @__PURE__ */ jsx(
1956
+ "section",
1957
+ {
1958
+ "data-section-id": id,
1959
+ "data-section-type": type,
1960
+ "data-group-id": groupId,
1961
+ ...rest,
1962
+ children: /* @__PURE__ */ jsx(
1963
+ SectionErrorBoundary,
1964
+ {
1965
+ sectionId: id,
1966
+ sectionType: type,
1967
+ fallback: errorFallback ?? defaultSectionFallback(type, id),
1968
+ children
1969
+ }
1970
+ )
1971
+ }
1972
+ );
1973
+ }
1974
+ function Block({ id, type, errorFallback, children, ...rest }) {
1975
+ return /* @__PURE__ */ jsx("div", { "data-block-id": id, "data-block-type": type, ...rest, children: /* @__PURE__ */ jsx(
1976
+ SectionErrorBoundary,
1977
+ {
1978
+ sectionId: id,
1979
+ sectionType: type,
1980
+ fallback: errorFallback ?? defaultSectionFallback(type, id),
1981
+ children
1982
+ }
1983
+ ) });
1984
+ }
1985
+ var ABSOLUTE_URL2 = /^[a-z]+:|^\/\//i;
1986
+ function readCsrfCookie2() {
1987
+ if (typeof document === "undefined") return null;
1988
+ const m = document.cookie.match(/(?:^|;\s*)numu_csrf=([^;]+)/);
1989
+ return m ? decodeURIComponent(m[1]) : null;
1990
+ }
1991
+ function newIdempotencyKey() {
1992
+ if (typeof crypto !== "undefined" && "randomUUID" in crypto) {
1993
+ return crypto.randomUUID();
1994
+ }
1995
+ return `${Date.now()}-${Math.random().toString(36).slice(2)}`;
1996
+ }
1997
+ function Form({
1998
+ action,
1999
+ method = "POST",
2000
+ onSuccess,
2001
+ onError,
2002
+ children,
2003
+ ...rest
2004
+ }) {
2005
+ const [submitting, setSubmitting] = useState(false);
2006
+ const [error, setError] = useState(null);
2007
+ async function handleSubmit(e) {
2008
+ e.preventDefault();
2009
+ if (ABSOLUTE_URL2.test(action)) {
2010
+ const err = new Error(
2011
+ `<Form action> must be a same-origin path (got "${action}"). Use a /api/* proxy route or your route handler.`
2012
+ );
2013
+ setError(err);
2014
+ onError?.(err);
2015
+ return;
2016
+ }
2017
+ setSubmitting(true);
2018
+ setError(null);
2019
+ try {
2020
+ const formData = new FormData(e.currentTarget);
2021
+ const isMutation = method !== "GET";
2022
+ const isMultipart = e.currentTarget.dataset?.numuMultipart === "true";
2023
+ const headers = {};
2024
+ if (!isMultipart) headers["Content-Type"] = "application/json";
2025
+ if (isMutation) {
2026
+ const csrf = readCsrfCookie2();
2027
+ if (csrf) headers["x-numu-csrf"] = csrf;
2028
+ headers["x-numu-idempotency-key"] = newIdempotencyKey();
2029
+ }
2030
+ let url = action;
2031
+ let body;
2032
+ if (method === "GET") {
2033
+ const params = new URLSearchParams();
2034
+ for (const [k, v] of formData.entries()) {
2035
+ if (typeof v === "string") params.append(k, v);
2036
+ }
2037
+ const sep = action.includes("?") ? "&" : "?";
2038
+ url = `${action}${sep}${params.toString()}`;
2039
+ } else if (isMultipart) {
2040
+ body = formData;
2041
+ } else {
2042
+ const obj = {};
2043
+ for (const [k, v] of formData.entries()) obj[k] = v;
2044
+ body = JSON.stringify(obj);
2045
+ }
2046
+ const res = await fetch(url, { method, headers, body });
2047
+ if (!res.ok) {
2048
+ const text = await res.text();
2049
+ throw new Error(
2050
+ `${method} ${action} failed: ${res.status} ${res.statusText}${text ? ` \u2014 ${text.slice(0, 200)}` : ""}`
2051
+ );
2052
+ }
2053
+ const json = res.headers.get("content-type")?.includes("application/json") ? await res.json() : await res.text();
2054
+ onSuccess?.(json);
2055
+ } catch (err) {
2056
+ const e2 = err instanceof Error ? err : new Error(String(err));
2057
+ setError(e2);
2058
+ onError?.(e2);
2059
+ } finally {
2060
+ setSubmitting(false);
2061
+ }
2062
+ }
2063
+ return /* @__PURE__ */ jsx(
2064
+ "form",
2065
+ {
2066
+ onSubmit: handleSubmit,
2067
+ method,
2068
+ action,
2069
+ "aria-busy": submitting,
2070
+ ...rest,
2071
+ children: typeof children === "function" ? children({ submitting, error }) : children
2072
+ }
2073
+ );
2074
+ }
2075
+ function joinClass(...names) {
2076
+ return names.filter(Boolean).join(" ");
2077
+ }
2078
+ function ProductCard({
2079
+ product,
2080
+ href,
2081
+ className,
2082
+ slots,
2083
+ imageSizes
2084
+ }) {
2085
+ const target = href ?? `/products/${product.slug}`;
2086
+ const firstImage = product.images?.[0];
2087
+ const inStock = product.in_stock;
2088
+ const badge = slots?.badge !== void 0 ? slots.badge : inStock ? null : /* @__PURE__ */ jsx("span", { className: "numu-product-card__badge", children: "Sold out" });
2089
+ return /* @__PURE__ */ jsxs("article", { className: joinClass("numu-product-card", className), children: [
2090
+ /* @__PURE__ */ jsxs(Link, { to: target, className: "numu-product-card__link", children: [
2091
+ /* @__PURE__ */ jsxs("div", { className: "numu-product-card__media", children: [
2092
+ /* @__PURE__ */ jsx(
2093
+ Image,
2094
+ {
2095
+ src: firstImage?.url,
2096
+ alt: firstImage?.alt || product.name,
2097
+ sizes: imageSizes,
2098
+ className: "numu-product-card__image"
2099
+ }
2100
+ ),
2101
+ badge
2102
+ ] }),
2103
+ slots?.title ?? /* @__PURE__ */ jsx("h3", { className: "numu-product-card__title", children: product.name }),
2104
+ slots?.price ?? /* @__PURE__ */ jsx("div", { className: "numu-product-card__price", children: /* @__PURE__ */ jsx(
2105
+ Money,
2106
+ {
2107
+ amount: product.price,
2108
+ compareAt: product.compare_at_price,
2109
+ currency: product.currency
2110
+ }
2111
+ ) })
2112
+ ] }),
2113
+ slots?.cta === null ? null : /* @__PURE__ */ jsx("div", { className: "numu-product-card__cta", children: slots?.cta ?? /* @__PURE__ */ jsx(AddToCartButton, { product }) })
2114
+ ] });
2115
+ }
2116
+ function joinClass2(...names) {
2117
+ return names.filter(Boolean).join(" ");
2118
+ }
2119
+ function CollectionCard({
2120
+ collection,
2121
+ href,
2122
+ className,
2123
+ slots,
2124
+ imageSizes
2125
+ }) {
2126
+ const target = href ?? `/collections/${collection.slug}`;
2127
+ const productCount = typeof collection.product_count === "number" ? collection.product_count : void 0;
2128
+ return /* @__PURE__ */ jsx("article", { className: joinClass2("numu-collection-card", className), children: /* @__PURE__ */ jsxs(Link, { to: target, className: "numu-collection-card__link", children: [
2129
+ /* @__PURE__ */ jsx("div", { className: "numu-collection-card__media", children: /* @__PURE__ */ jsx(
2130
+ Image,
2131
+ {
2132
+ src: collection.image_url,
2133
+ alt: collection.name,
2134
+ sizes: imageSizes,
2135
+ className: "numu-collection-card__image"
2136
+ }
2137
+ ) }),
2138
+ slots?.title ?? /* @__PURE__ */ jsx("h3", { className: "numu-collection-card__title", children: collection.name }),
2139
+ slots?.count === null ? null : slots?.count ?? (productCount !== void 0 && /* @__PURE__ */ jsx("p", { className: "numu-collection-card__count", children: productCount === 1 ? "1 product" : `${productCount} products` }))
2140
+ ] }) });
2141
+ }
2142
+ var ALLOWED_TAGS = /* @__PURE__ */ new Set([
2143
+ "p",
2144
+ "br",
2145
+ "hr",
2146
+ "h1",
2147
+ "h2",
2148
+ "h3",
2149
+ "h4",
2150
+ "h5",
2151
+ "h6",
2152
+ "blockquote",
2153
+ "pre",
2154
+ "code",
2155
+ "strong",
2156
+ "b",
2157
+ "em",
2158
+ "i",
2159
+ "u",
2160
+ "s",
2161
+ "sub",
2162
+ "sup",
2163
+ "mark",
2164
+ "ul",
2165
+ "ol",
2166
+ "li",
2167
+ "dl",
2168
+ "dt",
2169
+ "dd",
2170
+ "a",
2171
+ "img",
2172
+ "table",
2173
+ "thead",
2174
+ "tbody",
2175
+ "tr",
2176
+ "th",
2177
+ "td",
2178
+ "span",
2179
+ "div"
2180
+ ]);
2181
+ var ALLOWED_ATTRS_BY_TAG = {
2182
+ a: /* @__PURE__ */ new Set(["href", "target", "rel", "class", "title"]),
2183
+ img: /* @__PURE__ */ new Set(["src", "alt", "width", "height", "loading", "class"]),
2184
+ span: /* @__PURE__ */ new Set(["class"]),
2185
+ div: /* @__PURE__ */ new Set(["class"]),
2186
+ p: /* @__PURE__ */ new Set(["class"]),
2187
+ h1: /* @__PURE__ */ new Set(["class"]),
2188
+ h2: /* @__PURE__ */ new Set(["class"]),
2189
+ h3: /* @__PURE__ */ new Set(["class"]),
2190
+ h4: /* @__PURE__ */ new Set(["class"]),
2191
+ h5: /* @__PURE__ */ new Set(["class"]),
2192
+ h6: /* @__PURE__ */ new Set(["class"]),
2193
+ blockquote: /* @__PURE__ */ new Set(["class"]),
2194
+ pre: /* @__PURE__ */ new Set(["class"]),
2195
+ code: /* @__PURE__ */ new Set(["class"]),
2196
+ ul: /* @__PURE__ */ new Set(["class"]),
2197
+ ol: /* @__PURE__ */ new Set(["class"]),
2198
+ li: /* @__PURE__ */ new Set(["class"]),
2199
+ table: /* @__PURE__ */ new Set(["class"]),
2200
+ thead: /* @__PURE__ */ new Set(["class"]),
2201
+ tbody: /* @__PURE__ */ new Set(["class"]),
2202
+ tr: /* @__PURE__ */ new Set(["class"]),
2203
+ th: /* @__PURE__ */ new Set(["class"]),
2204
+ td: /* @__PURE__ */ new Set(["class"])
2205
+ };
2206
+ var URL_SAFE_PROTOCOLS = /^(https?|mailto|tel):/i;
2207
+ function isSafeUrl(url) {
2208
+ const trimmed = url.trim();
2209
+ if (!trimmed) return false;
2210
+ if (trimmed.startsWith("/") || trimmed.startsWith("#") || trimmed.startsWith("?")) {
2211
+ return true;
2212
+ }
2213
+ return URL_SAFE_PROTOCOLS.test(trimmed);
2214
+ }
2215
+ function sanitizeHtml(input) {
2216
+ if (!input) return "";
2217
+ if (typeof window === "undefined" || typeof DOMParser === "undefined") {
2218
+ return sanitizeHtmlServer(input);
2219
+ }
2220
+ return sanitizeHtmlClient(input);
2221
+ }
2222
+ function sanitizeHtmlClient(input) {
2223
+ const doc = new DOMParser().parseFromString(
2224
+ `<div>${input}</div>`,
2225
+ "text/html"
2226
+ );
2227
+ const root = doc.body.firstElementChild;
2228
+ if (!root) return "";
2229
+ walk(root);
2230
+ return root.innerHTML;
2231
+ }
2232
+ function walk(node) {
2233
+ const children = Array.from(node.children);
2234
+ for (const child of children) {
2235
+ const tag = child.tagName.toLowerCase();
2236
+ if (!ALLOWED_TAGS.has(tag)) {
2237
+ if (tag === "script" || tag === "style" || tag === "iframe" || tag === "object" || tag === "embed") {
2238
+ child.remove();
2239
+ } else {
2240
+ const text = child.textContent || "";
2241
+ child.replaceWith(document.createTextNode(text));
2242
+ }
2243
+ continue;
2244
+ }
2245
+ const allowed = ALLOWED_ATTRS_BY_TAG[tag] || /* @__PURE__ */ new Set();
2246
+ for (const attr of Array.from(child.attributes)) {
2247
+ const name = attr.name.toLowerCase();
2248
+ if (name.startsWith("on")) {
2249
+ child.removeAttribute(attr.name);
2250
+ continue;
2251
+ }
2252
+ if (!allowed.has(name)) {
2253
+ child.removeAttribute(attr.name);
2254
+ continue;
2255
+ }
2256
+ if ((name === "href" || name === "src") && !isSafeUrl(attr.value)) {
2257
+ child.removeAttribute(attr.name);
2258
+ continue;
2259
+ }
2260
+ }
2261
+ if (tag === "a" && child.getAttribute("target") === "_blank") {
2262
+ child.setAttribute("rel", "noopener noreferrer");
2263
+ }
2264
+ walk(child);
2265
+ }
2266
+ }
2267
+ function sanitizeHtmlServer(input) {
2268
+ let s = input.replace(/<script[\s\S]*?<\/script>/gi, "").replace(/<style[\s\S]*?<\/style>/gi, "").replace(/<iframe[\s\S]*?<\/iframe>/gi, "").replace(/<object[\s\S]*?<\/object>/gi, "").replace(/<embed[\s\S]*?<\/embed>/gi, "").replace(/<link[\s\S]*?>/gi, "").replace(/<meta[\s\S]*?>/gi, "");
2269
+ s = s.replace(/\s+on\w+\s*=\s*("[^"]*"|'[^']*'|[^\s>]+)/gi, "");
2270
+ s = s.replace(
2271
+ /\s+(href|src)\s*=\s*("[^"]*"|'[^']*'|[^\s>]+)/gi,
2272
+ (full, attr, val) => {
2273
+ const cleaned = String(val).replace(/^['"]|['"]$/g, "");
2274
+ return isSafeUrl(cleaned) ? full : "";
2275
+ }
2276
+ );
2277
+ return s;
2278
+ }
2279
+ function RichText({ html, className, as = "div" }) {
2280
+ const safe = useMemo(() => sanitizeHtml(html || ""), [html]);
2281
+ if (!safe) return null;
2282
+ const Tag = as;
2283
+ return /* @__PURE__ */ jsx(
2284
+ Tag,
2285
+ {
2286
+ className,
2287
+ dangerouslySetInnerHTML: { __html: safe }
2288
+ }
2289
+ );
2290
+ }
2291
+ function CurrencySwitcher({
2292
+ className,
2293
+ onSelect,
2294
+ render
2295
+ }) {
2296
+ const { presentment, selected, setSelected, loading } = useCurrency();
2297
+ const handleChange = useCallback(
2298
+ (next) => {
2299
+ if (next === selected) return;
2300
+ setSelected(next);
2301
+ onSelect?.(next);
2302
+ },
2303
+ [selected, setSelected, onSelect]
2304
+ );
2305
+ if (loading) return null;
2306
+ if (presentment.length <= 1) return null;
2307
+ if (render) {
2308
+ return /* @__PURE__ */ jsx(Fragment, { children: render({
2309
+ currencies: presentment,
2310
+ current: selected,
2311
+ onChange: handleChange
2312
+ }) });
2313
+ }
2314
+ return /* @__PURE__ */ jsx(
2315
+ "select",
2316
+ {
2317
+ className: className ?? "numu-currency-switcher",
2318
+ value: selected,
2319
+ onChange: (e) => handleChange(e.target.value),
2320
+ "aria-label": "Currency",
2321
+ children: presentment.map((c) => /* @__PURE__ */ jsx("option", { value: c, children: c }, c))
2322
+ }
2323
+ );
2324
+ }
2325
+ var LANG_LABELS = {
2326
+ ar: "\u0627\u0644\u0639\u0631\u0628\u064A\u0629",
2327
+ en: "English",
2328
+ fr: "Fran\xE7ais",
2329
+ he: "\u05E2\u05D1\u05E8\u05D9\u05EA",
2330
+ fa: "\u0641\u0627\u0631\u0633\u06CC",
2331
+ ur: "\u0627\u0631\u062F\u0648",
2332
+ es: "Espa\xF1ol",
2333
+ de: "Deutsch"
2334
+ };
2335
+ function LocaleSwitcher({
2336
+ className,
2337
+ onSelect,
2338
+ render
2339
+ }) {
2340
+ const shop = useShop();
2341
+ const { locale } = useLocalization();
2342
+ const available = shop.available_locales || [];
2343
+ const list = available.length > 0 ? available : shop.default_language === "en" ? ["en"] : Array.from(/* @__PURE__ */ new Set([shop.default_language, "en"]));
2344
+ const labelFor = useCallback((code) => {
2345
+ return LANG_LABELS[code] || code.toUpperCase();
2346
+ }, []);
2347
+ const handleChange = useCallback(
2348
+ (next) => {
2349
+ if (next === locale) return;
2350
+ onSelect?.(next);
2351
+ if (typeof document !== "undefined") {
2352
+ const oneYear = 60 * 60 * 24 * 365;
2353
+ document.cookie = `numu_locale=${encodeURIComponent(next)}; path=/; max-age=${oneYear}; samesite=lax`;
2354
+ }
2355
+ if (typeof window !== "undefined") {
2356
+ window.location.reload();
2357
+ }
2358
+ },
2359
+ [locale, onSelect]
2360
+ );
2361
+ if (list.length <= 1) return null;
2362
+ if (render) {
2363
+ return /* @__PURE__ */ jsx(Fragment, { children: render({ locales: list, current: locale, labelFor, onChange: handleChange }) });
2364
+ }
2365
+ return /* @__PURE__ */ jsx(
2366
+ "select",
2367
+ {
2368
+ className: className ?? "numu-locale-switcher",
2369
+ value: locale,
2370
+ onChange: (e) => handleChange(e.target.value),
2371
+ "aria-label": "Language",
2372
+ children: list.map((l) => /* @__PURE__ */ jsx("option", { value: l, children: labelFor(l) }, l))
2373
+ }
2374
+ );
2375
+ }
2376
+
2377
+ // src/utils/normalize.ts
2378
+ var DEFAULT_THEME_ID = typeof process !== "undefined" && process.env?.NUMU_DEFAULT_THEME_ID || "modern";
2379
+ function resolveThemeSettings(raw) {
2380
+ const safeRaw = raw ?? {};
2381
+ if (safeRaw.schema_version === 3) {
2382
+ return safeRaw;
2383
+ }
2384
+ const themeBlock = safeRaw.theme || {};
2385
+ const themeId = typeof themeBlock.base_theme === "string" && themeBlock.base_theme || DEFAULT_THEME_ID;
2386
+ const globalSettings = {};
2387
+ for (const key of [
2388
+ "primary_color",
2389
+ "secondary_color",
2390
+ "font_family",
2391
+ "logo_url"
2392
+ ]) {
2393
+ if (themeBlock[key] !== void 0) globalSettings[key] = themeBlock[key];
2394
+ }
2395
+ if (safeRaw.identity) globalSettings.identity = safeRaw.identity;
2396
+ const sections = {};
2397
+ const order = [];
2398
+ const hero = safeRaw.hero;
2399
+ if (hero) {
2400
+ sections["hero_1"] = {
2401
+ type: "hero",
2402
+ settings: {
2403
+ // Mirrors backend `normalize_legacy_to_v3` exactly — includes
2404
+ // headline_ar so Arabic content survives the round-trip.
2405
+ headline: hero.headline ?? "",
2406
+ headline_ar: hero.headline_ar ?? "",
2407
+ subtitle: hero.subtitle ?? "",
2408
+ background_image: hero.hero_image_url ?? "",
2409
+ cta_text: hero.cta_text ?? "",
2410
+ cta_link: hero.cta_link ?? ""
2411
+ }
2412
+ };
2413
+ order.push("hero_1");
2414
+ }
2415
+ const products = safeRaw.products;
2416
+ if (products) {
2417
+ sections["featured_1"] = {
2418
+ type: "featured-products",
2419
+ settings: products
2420
+ };
2421
+ order.push("featured_1");
2422
+ }
2423
+ const templates = {};
2424
+ if (Object.keys(sections).length > 0) {
2425
+ templates["home"] = { name: "Home", sections, order };
2426
+ }
2427
+ const sectionGroups = {
2428
+ header: {
2429
+ name: "Header Group",
2430
+ sections: {
2431
+ header_1: {
2432
+ type: "header",
2433
+ settings: safeRaw.header || {}
2434
+ }
2435
+ },
2436
+ order: ["header_1"]
2437
+ },
2438
+ footer: {
2439
+ name: "Footer Group",
2440
+ sections: {
2441
+ footer_1: {
2442
+ type: "footer",
2443
+ settings: safeRaw.footer || {}
2444
+ }
2445
+ },
2446
+ order: ["footer_1"]
2447
+ }
2448
+ };
2449
+ let externalTheme = null;
2450
+ const ext = safeRaw.external_theme;
2451
+ if (ext && typeof ext === "object" && typeof ext.bundle_url === "string" && ext.bundle_url) {
2452
+ externalTheme = {
2453
+ bundle_url: ext.bundle_url,
2454
+ css_url: typeof ext.css_url === "string" ? ext.css_url : null,
2455
+ mode: typeof ext.mode === "string" ? ext.mode : "production"
2456
+ };
2457
+ }
2458
+ return {
2459
+ schema_version: 3,
2460
+ theme_id: themeId,
2461
+ global_settings: globalSettings,
2462
+ templates,
2463
+ section_groups: sectionGroups,
2464
+ external_theme: externalTheme
2465
+ };
2466
+ }
2467
+
2468
+ // src/utils/federation.ts
2469
+ var SDK_SYMBOL_KEY = "__NUMU_SDK_SLOT__";
2470
+ var REACT_SYMBOL_KEY = "__NUMU_REACT_SLOT__";
2471
+ function globalSlot(name, slotKey) {
2472
+ const g = globalThis;
2473
+ let sym = g[slotKey];
2474
+ if (!sym) {
2475
+ sym = Symbol.for(name);
2476
+ g[slotKey] = sym;
2477
+ }
2478
+ return sym;
2479
+ }
2480
+ function sdkSlot() {
2481
+ return globalSlot("@numueg/theme-sdk:singleton", SDK_SYMBOL_KEY);
2482
+ }
2483
+ function reactSlot() {
2484
+ return globalSlot("@numueg/theme-sdk:react", REACT_SYMBOL_KEY);
2485
+ }
2486
+ function registerSdkSingleton(sdk) {
2487
+ if (typeof globalThis === "undefined") return;
2488
+ globalThis[sdkSlot()] = sdk;
2489
+ }
2490
+ function getSdkSingleton() {
2491
+ if (typeof globalThis === "undefined") return null;
2492
+ return globalThis[sdkSlot()] ?? null;
2493
+ }
2494
+ function registerReactSingleton(react, reactDom) {
2495
+ if (typeof globalThis === "undefined") return;
2496
+ globalThis[reactSlot()] = {
2497
+ React: react,
2498
+ ReactDOM: reactDom
2499
+ };
2500
+ }
2501
+ function getReactSingleton() {
2502
+ if (typeof globalThis === "undefined") return null;
2503
+ return globalThis[reactSlot()] ?? null;
2504
+ }
2505
+ function isSdkAvailable() {
2506
+ return getSdkSingleton() !== null;
2507
+ }
2508
+
2509
+ // src/utils/defineSection.ts
2510
+ var SECTION_MARKER = /* @__PURE__ */ Symbol.for("numu.theme.section");
2511
+ var BLOCK_MARKER = /* @__PURE__ */ Symbol.for("numu.theme.block");
2512
+ function defineSection(input) {
2513
+ if (!input?.schema?.type) {
2514
+ throw new Error(
2515
+ "defineSection: `schema.type` is required and must be a stable string id."
2516
+ );
2517
+ }
2518
+ if (!/^[a-z][a-z0-9_-]*$/.test(input.schema.type)) {
2519
+ throw new Error(
2520
+ `defineSection: section type '${input.schema.type}' must be lowercase alphanumeric with dashes/underscores. The codegen step uses this as a TS key.`
2521
+ );
2522
+ }
2523
+ if (typeof input.render !== "function") {
2524
+ throw new Error(
2525
+ "defineSection: `render` must be a React component function."
2526
+ );
2527
+ }
2528
+ return Object.freeze({
2529
+ schema: input.schema,
2530
+ render: input.render,
2531
+ [SECTION_MARKER]: true
2532
+ });
2533
+ }
2534
+ function defineBlock(input) {
2535
+ if (!input?.schema?.type) {
2536
+ throw new Error("defineBlock: `schema.type` is required.");
2537
+ }
2538
+ if (!/^[a-z@][a-z0-9_/-]*$/i.test(input.schema.type)) {
2539
+ throw new Error(
2540
+ `defineBlock: block type '${input.schema.type}' has illegal chars. Use a-z, 0-9, dashes/underscores; @app/<slug>/<block> form is also accepted.`
2541
+ );
2542
+ }
2543
+ if (typeof input.render !== "function") {
2544
+ throw new Error(
2545
+ "defineBlock: `render` must be a React component function."
2546
+ );
2547
+ }
2548
+ return Object.freeze({
2549
+ schema: input.schema,
2550
+ render: input.render,
2551
+ [BLOCK_MARKER]: true
2552
+ });
2553
+ }
2554
+ function isDefinedSection(v) {
2555
+ return typeof v === "object" && v !== null && v[SECTION_MARKER] === true;
2556
+ }
2557
+ function isDefinedBlock(v) {
2558
+ return typeof v === "object" && v !== null && v[BLOCK_MARKER] === true;
2559
+ }
2560
+ function collectSections(modules) {
2561
+ const out = {};
2562
+ for (const [path, mod] of Object.entries(modules)) {
2563
+ const def = mod.default;
2564
+ if (!isDefinedSection(def)) {
2565
+ if (typeof console !== "undefined") {
2566
+ console.warn(
2567
+ `[numu] ${path}: default export is not a defineSection() result; skipped.`
2568
+ );
2569
+ }
2570
+ continue;
2571
+ }
2572
+ if (out[def.schema.type]) {
2573
+ throw new Error(
2574
+ `[numu] duplicate section type '${def.schema.type}': previously defined, now also defined in ${path}.`
2575
+ );
2576
+ }
2577
+ out[def.schema.type] = def;
2578
+ }
2579
+ return out;
2580
+ }
2581
+ function collectBlocks(modules) {
2582
+ const out = {};
2583
+ for (const [path, mod] of Object.entries(modules)) {
2584
+ const def = mod.default;
2585
+ if (!isDefinedBlock(def)) {
2586
+ if (typeof console !== "undefined") {
2587
+ console.warn(
2588
+ `[numu] ${path}: default export is not a defineBlock() result; skipped.`
2589
+ );
2590
+ }
2591
+ continue;
2592
+ }
2593
+ if (out[def.schema.type]) {
2594
+ throw new Error(
2595
+ `[numu] duplicate block type '${def.schema.type}': previously defined, now also defined in ${path}.`
2596
+ );
2597
+ }
2598
+ out[def.schema.type] = def;
2599
+ }
2600
+ return out;
2601
+ }
2602
+
2603
+ // src/utils/assetUrl.ts
2604
+ function getRuntime() {
2605
+ if (typeof window === "undefined") return {};
2606
+ return window;
2607
+ }
2608
+ function assetUrl(name) {
2609
+ if (!name) return "";
2610
+ if (/^https?:\/\//i.test(name) || name.startsWith("//")) return name;
2611
+ const runtime = getRuntime();
2612
+ const manifest = runtime.__NUMU_ASSET_MANIFEST;
2613
+ const base = runtime.__NUMU_ASSET_BASE_URL || "/assets/";
2614
+ let key = name.replace(/^\.\//, "");
2615
+ if (key.startsWith("/")) key = key.slice(1);
2616
+ if (key.startsWith("assets/")) key = key.slice("assets/".length);
2617
+ const hashed = manifest?.[key];
2618
+ const filename = hashed || key;
2619
+ const cleanBase = base.endsWith("/") ? base : `${base}/`;
2620
+ return `${cleanBase}${filename}`;
2621
+ }
2622
+
2623
+ // src/utils/locales.ts
2624
+ function flattenMessages(source, prefix = "") {
2625
+ const out = {};
2626
+ for (const [key, val] of Object.entries(source)) {
2627
+ const fullKey = prefix ? `${prefix}.${key}` : key;
2628
+ if (val === null || val === void 0) continue;
2629
+ if (typeof val === "string") {
2630
+ out[fullKey] = val;
2631
+ } else if (typeof val === "object" && !Array.isArray(val)) {
2632
+ Object.assign(
2633
+ out,
2634
+ flattenMessages(val, fullKey)
2635
+ );
2636
+ } else if (typeof console !== "undefined") {
2637
+ console.warn(
2638
+ `[numu] locale key '${fullKey}' has non-string value (${typeof val}); skipped.`
2639
+ );
2640
+ }
2641
+ }
2642
+ return out;
2643
+ }
2644
+ function pickTranslations(bundle, locale) {
2645
+ const fallback = bundle["en"] || bundle["en.default"] || {};
2646
+ const requested = bundle[locale];
2647
+ if (!requested) return fallback;
2648
+ return { ...fallback, ...requested };
2649
+ }
2650
+ function buildLocaleBundle(modules) {
2651
+ const bundle = {};
2652
+ for (const [path, raw] of Object.entries(modules)) {
2653
+ const m = /\/([^/]+)\.json$/.exec(path);
2654
+ if (!m) continue;
2655
+ let code = m[1];
2656
+ if (code !== "en.default") {
2657
+ code = code.replace(/\.default$/, "");
2658
+ }
2659
+ const value = raw;
2660
+ if (typeof value === "object" && value !== null) {
2661
+ bundle[code] = flattenMessages(value);
2662
+ }
2663
+ }
2664
+ return bundle;
2665
+ }
2666
+
2667
+ export { AddToCartButton, Block, CartContext, CollectionCard, CollectionContext, CollectionProvider, CurrencySwitcher, CustomerContext, Form, Image, Link, LocaleSwitcher, LocalizationContext, Money, NuMuProvider, PageContext, ProductCard, ProductContext, ProductProvider, RichText, Section, SectionContext, ShopContext, ThemeSettingsContext, assetUrl, availableValues, buildLocaleBundle, collectBlocks, collectSections, defaultVariant, defineBlock, defineSection, findVariantByOptions, flattenMessages, getReactSingleton, getSdkSingleton, isDefinedBlock, isDefinedSection, isSdkAvailable, pickTranslations, registerReactSingleton, registerSdkSingleton, resolveThemeSettings, sanitizeHtml, useAnalytics, useApp, useCart, useCheckout, useCollection, useCollectionOptional, useCollections, useCurrency, useCustomer, useCustomerActions, useCustomerAddresses, useDirection, useFieldTranslation, useGiftCardBalance, useImage, useLocale, useLocalization, useMoney, useNavigation, useNumberFormat, useOrder, useOrders, usePage, useProduct, useProductOptional, useProducts, useRelatedProducts, useReorder, useSearch, useSection, useSectionOptional, useShippingRates, useShop, useThemeSettings, useTranslation, useVariantSelection, useWishlist };
2668
+ //# sourceMappingURL=index.mjs.map
2669
+ //# sourceMappingURL=index.mjs.map