@numueg/theme-sdk 0.6.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/CHANGELOG.md +144 -0
  2. package/dist/chunk-3C37JX2X.cjs +207 -0
  3. package/dist/chunk-3C37JX2X.cjs.map +1 -0
  4. package/dist/chunk-JIAA7TGR.mjs +397 -0
  5. package/dist/chunk-JIAA7TGR.mjs.map +1 -0
  6. package/dist/chunk-PUR3FRGQ.mjs +183 -0
  7. package/dist/chunk-PUR3FRGQ.mjs.map +1 -0
  8. package/dist/chunk-TBSNHHFH.cjs +96 -0
  9. package/dist/chunk-TBSNHHFH.cjs.map +1 -0
  10. package/dist/chunk-X3J4GDI7.cjs +408 -0
  11. package/dist/chunk-X3J4GDI7.cjs.map +1 -0
  12. package/dist/chunk-XF2FGIVS.mjs +94 -0
  13. package/dist/chunk-XF2FGIVS.mjs.map +1 -0
  14. package/dist/{entities-6MGANln7.d.mts → entities-B8378GKp.d.mts} +12 -0
  15. package/dist/{entities-6MGANln7.d.ts → entities-B8378GKp.d.ts} +12 -0
  16. package/dist/index.cjs +643 -965
  17. package/dist/index.cjs.map +1 -1
  18. package/dist/index.d.mts +215 -82
  19. package/dist/index.d.ts +215 -82
  20. package/dist/index.mjs +485 -908
  21. package/dist/index.mjs.map +1 -1
  22. package/dist/{mount-BGumg1JM.d.mts → mount-CM4kGg9w.d.ts} +87 -14
  23. package/dist/{mount-Bt-4ken5.d.ts → mount-NLjFNyBv.d.mts} +87 -14
  24. package/dist/normalize.cjs +7 -91
  25. package/dist/normalize.cjs.map +1 -1
  26. package/dist/normalize.d.mts +1 -1
  27. package/dist/normalize.d.ts +1 -1
  28. package/dist/normalize.mjs +1 -92
  29. package/dist/normalize.mjs.map +1 -1
  30. package/dist/{theme-D8MOopvi.d.mts → theme-CNTB4KnU.d.mts} +29 -3
  31. package/dist/{theme-D8MOopvi.d.ts → theme-CNTB4KnU.d.ts} +29 -3
  32. package/dist/types.d.mts +2 -2
  33. package/dist/types.d.ts +2 -2
  34. package/dist/v2-compat.cjs +6 -163
  35. package/dist/v2-compat.cjs.map +1 -1
  36. package/dist/v2-compat.d.mts +1 -1
  37. package/dist/v2-compat.d.ts +1 -1
  38. package/dist/v2-compat.mjs +2 -159
  39. package/dist/v2-compat.mjs.map +1 -1
  40. package/dist/validation.cjs +43 -403
  41. package/dist/validation.cjs.map +1 -1
  42. package/dist/validation.d.mts +1 -1
  43. package/dist/validation.d.ts +1 -1
  44. package/dist/validation.mjs +1 -395
  45. package/dist/validation.mjs.map +1 -1
  46. package/dist/verify.cjs +6 -21
  47. package/dist/verify.cjs.map +1 -1
  48. package/dist/verify.d.mts +3 -3
  49. package/dist/verify.d.ts +3 -3
  50. package/dist/verify.mjs +3 -20
  51. package/dist/verify.mjs.map +1 -1
  52. package/package.json +1 -1
package/dist/index.cjs CHANGED
@@ -1,530 +1,37 @@
1
1
  'use strict';
2
2
 
3
+ var chunkTBSNHHFH_cjs = require('./chunk-TBSNHHFH.cjs');
4
+ var chunkX3J4GDI7_cjs = require('./chunk-X3J4GDI7.cjs');
5
+ var chunk3C37JX2X_cjs = require('./chunk-3C37JX2X.cjs');
3
6
  var react = require('react');
4
7
  var client = require('react-dom/client');
5
8
  var jsxRuntime = require('react/jsx-runtime');
6
9
 
7
10
  // src/types/theme.ts
8
11
  var MAX_BLOCK_DEPTH = 5;
9
-
10
- // src/validation/index.ts
11
- var THEME_CONTRACT_VERSION = 1;
12
- var SDK_VERSION = "0.6.0" ;
13
- var REQUIRED_TEMPLATES = [
14
- "home",
15
- "product",
16
- "collection",
17
- "cart",
18
- "page",
19
- "search",
20
- "404"
21
- ];
22
- var KNOWN_TEMPLATES = /* @__PURE__ */ new Set([
23
- ...REQUIRED_TEMPLATES,
24
- "blog",
25
- "article",
26
- "policies",
27
- "password",
28
- "account",
29
- "checkout"
30
- ]);
31
- var KNOWN_SETTING_TYPES = /* @__PURE__ */ new Set([
32
- "text",
33
- "textarea",
34
- "richtext",
35
- "number",
36
- "range",
37
- "color",
38
- "checkbox",
39
- "select",
40
- "radio",
41
- "font",
42
- "image_picker",
43
- "url",
44
- "product",
45
- "product_list",
46
- "collection",
47
- "collection_list",
48
- "header",
49
- "paragraph",
50
- "html",
51
- "date",
52
- "time",
53
- "video_picker",
54
- "color_scheme",
55
- "page_picker",
56
- "blog_picker",
57
- "link_list_picker",
58
- "variant_picker",
59
- "file_upload",
60
- "icon_picker",
61
- "icon"
62
- ]);
63
- var PRESENTATIONAL_SETTING_TYPES = /* @__PURE__ */ new Set([
64
- "header",
65
- "paragraph",
66
- "html"
67
- ]);
68
- var SECTION_TYPE_RE = /^[a-z][a-z0-9_-]*$/;
69
- var THEME_ID_RE = /^[a-z0-9][a-z0-9_-]*[a-z0-9]$/;
70
- var SEMVER_RE = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z-.]+)?(?:\+[0-9A-Za-z-.]+)?$/;
71
- function isObject(v) {
72
- return typeof v === "object" && v !== null && !Array.isArray(v);
73
- }
74
- function isNonEmptyString(v) {
75
- return typeof v === "string" && v.trim().length > 0;
76
- }
77
- var IssueBag = class {
78
- constructor() {
79
- this.issues = [];
80
- }
81
- err(code, message, path) {
82
- this.issues.push({ level: "error", code, message, path });
83
- }
84
- warn(code, message, path) {
85
- this.issues.push({ level: "warning", code, message, path });
86
- }
87
- result() {
88
- return {
89
- valid: !this.issues.some((i) => i.level === "error"),
90
- issues: this.issues
91
- };
92
- }
93
- };
94
- function collectPresetSectionTypes(manifest) {
95
- const types = /* @__PURE__ */ new Set();
96
- const presets = manifest.presets;
97
- if (!isObject(presets)) return types;
98
- const buckets = [presets.templates, presets.section_groups];
99
- for (const bucket of buckets) {
100
- if (!isObject(bucket)) continue;
101
- for (const entry of Object.values(bucket)) {
102
- if (!isObject(entry)) continue;
103
- const sections = entry.sections;
104
- const instances = Array.isArray(sections) ? sections : isObject(sections) ? Object.values(sections) : [];
105
- for (const inst of instances) {
106
- if (isObject(inst) && isNonEmptyString(inst.type)) types.add(inst.type);
107
- }
108
- }
109
- }
110
- return types;
111
- }
112
- function validateSettingInto(bag, setting, pathPrefix, seenIds) {
113
- if (!isObject(setting)) {
114
- bag.err("schema.setting.invalid", "Setting must be an object.", pathPrefix);
115
- return;
116
- }
117
- const type = setting.type;
118
- if (!isNonEmptyString(type)) {
119
- bag.err("schema.setting.type.missing", "Setting is missing a `type`.", pathPrefix);
120
- } else if (!KNOWN_SETTING_TYPES.has(type)) {
121
- bag.warn(
122
- "schema.setting.type.unknown",
123
- `Unknown setting type "${type}" \u2014 the customizer will fall back to a text input.`,
124
- pathPrefix
125
- );
126
- }
127
- const presentational = isNonEmptyString(type) && PRESENTATIONAL_SETTING_TYPES.has(type);
128
- if (!presentational) {
129
- if (!isNonEmptyString(setting.id)) {
130
- bag.err("schema.setting.id.missing", "Setting is missing an `id`.", pathPrefix);
131
- } else {
132
- if (seenIds.has(setting.id)) {
133
- bag.err(
134
- "schema.setting.id.duplicate",
135
- `Duplicate setting id "${setting.id}".`,
136
- pathPrefix
137
- );
138
- }
139
- seenIds.add(setting.id);
140
- }
141
- if (!isNonEmptyString(setting.label)) {
142
- bag.warn("schema.setting.label.missing", "Setting has no `label`.", pathPrefix);
143
- }
144
- }
145
- if (type === "select" || type === "radio") {
146
- const options = setting.options;
147
- if (!Array.isArray(options) || options.length === 0) {
148
- bag.err(
149
- "schema.setting.options.missing",
150
- `"${type}" setting "${String(setting.id)}" must declare a non-empty \`options\` array.`,
151
- pathPrefix
152
- );
153
- }
154
- }
155
- if (type === "range") {
156
- if (typeof setting.min !== "number" || typeof setting.max !== "number") {
157
- bag.err(
158
- "schema.setting.range.bounds",
159
- `"range" setting "${String(setting.id)}" must declare numeric \`min\` and \`max\`.`,
160
- pathPrefix
161
- );
162
- } else if (setting.min >= setting.max) {
163
- bag.err(
164
- "schema.setting.range.order",
165
- `"range" setting "${String(setting.id)}" has min >= max.`,
166
- pathPrefix
167
- );
168
- }
169
- }
170
- }
171
- function validateSectionSchema(schema, opts = {}) {
172
- const bag = new IssueBag();
173
- const where = opts.filenameType ? `${opts.filenameType}.json` : "section schema";
174
- if (!isObject(schema)) {
175
- bag.err("schema.invalid", "Section schema must be a JSON object.", where);
176
- return bag.result();
177
- }
178
- const type = schema.type;
179
- if (!isNonEmptyString(type)) {
180
- bag.err("schema.type.missing", "Section schema is missing a `type`.", where);
181
- } else {
182
- if (!SECTION_TYPE_RE.test(type)) {
183
- bag.err(
184
- "schema.type.format",
185
- `Section type "${type}" must match ${SECTION_TYPE_RE} (lowercase, start with a letter).`,
186
- where
187
- );
188
- }
189
- if (opts.filenameType && type !== opts.filenameType) {
190
- bag.err(
191
- "schema.type.filename_mismatch",
192
- `Schema type "${type}" must equal its filename "${opts.filenameType}" (component-filename = schema-type convention).`,
193
- where
194
- );
195
- }
196
- }
197
- if (!isNonEmptyString(schema.name)) {
198
- bag.err("schema.name.missing", "Section schema is missing a `name`.", where);
199
- }
200
- if (schema.settings === void 0) {
201
- bag.warn("schema.settings.missing", "Section schema has no `settings`.", where);
202
- } else if (!Array.isArray(schema.settings)) {
203
- bag.err("schema.settings.invalid", "`settings` must be an array.", where);
204
- } else {
205
- const seen = /* @__PURE__ */ new Set();
206
- schema.settings.forEach(
207
- (s, i) => validateSettingInto(bag, s, `${where}.settings[${i}]`, seen)
208
- );
209
- }
210
- if (schema.blocks !== void 0) {
211
- if (!Array.isArray(schema.blocks)) {
212
- bag.err("schema.blocks.invalid", "`blocks` must be an array.", where);
213
- } else {
214
- schema.blocks.forEach((b, i) => {
215
- if (!isObject(b) || !isNonEmptyString(b.type)) {
216
- bag.err("schema.block.type.missing", `Block[${i}] is missing a \`type\`.`, where);
217
- return;
218
- }
219
- if (!SECTION_TYPE_RE.test(b.type)) {
220
- bag.err(
221
- "schema.block.type.format",
222
- `Block type "${b.type}" must match ${SECTION_TYPE_RE}.`,
223
- where
224
- );
225
- }
226
- if (Array.isArray(b.settings)) {
227
- const seen = /* @__PURE__ */ new Set();
228
- b.settings.forEach(
229
- (s, j) => validateSettingInto(bag, s, `${where}.blocks[${i}].settings[${j}]`, seen)
230
- );
231
- }
232
- });
233
- }
234
- }
235
- return bag.result();
236
- }
237
- function validateSettingsAgainstSchema(settings, schema, pathPrefix = "settings") {
238
- const bag = new IssueBag();
239
- if (!isObject(settings)) {
240
- bag.err("instance.settings.invalid", "Section settings must be an object.", pathPrefix);
241
- return bag.result();
242
- }
243
- const defs = Array.isArray(schema.settings) ? schema.settings : [];
244
- const byId = /* @__PURE__ */ new Map();
245
- for (const d of defs) if (isNonEmptyString(d?.id)) byId.set(d.id, d);
246
- for (const [key, value] of Object.entries(settings)) {
247
- const def = byId.get(key);
248
- if (!def) {
249
- bag.warn(
250
- "instance.setting.unknown",
251
- `Setting "${key}" is not declared in the "${String(schema.type)}" schema.`,
252
- `${pathPrefix}.${key}`
253
- );
254
- continue;
255
- }
256
- if (value === null || value === void 0) continue;
257
- const p = `${pathPrefix}.${key}`;
258
- if ((def.type === "select" || def.type === "radio") && Array.isArray(def.options)) {
259
- const allowed = def.options.map((o) => o.value);
260
- if (typeof value === "string" && !allowed.includes(value)) {
261
- bag.err(
262
- "instance.setting.option.invalid",
263
- `"${key}" = "${value}" is not one of: ${allowed.join(", ")}.`,
264
- p
265
- );
266
- }
267
- }
268
- if (def.type === "range" && typeof value === "number") {
269
- if (typeof def.min === "number" && value < def.min) {
270
- bag.err("instance.setting.range.under", `"${key}" = ${value} is below min ${def.min}.`, p);
271
- }
272
- if (typeof def.max === "number" && value > def.max) {
273
- bag.err("instance.setting.range.over", `"${key}" = ${value} is above max ${def.max}.`, p);
274
- }
275
- }
276
- if (def.type === "checkbox" && typeof value !== "boolean") {
277
- bag.warn("instance.setting.type.mismatch", `"${key}" should be a boolean.`, p);
278
- }
279
- if ((def.type === "number" || def.type === "range") && typeof value !== "number") {
280
- bag.warn("instance.setting.type.mismatch", `"${key}" should be a number.`, p);
281
- }
282
- }
283
- return bag.result();
284
- }
285
- function validateManifestCore(bag, m) {
286
- if (!isNonEmptyString(m.id)) {
287
- bag.err("manifest.id.missing", "theme.json is missing `id`.", "id");
288
- } else if (!THEME_ID_RE.test(m.id)) {
289
- bag.err(
290
- "manifest.id.format",
291
- `Theme id "${m.id}" must be lowercase alphanumeric with dashes/underscores (no leading/trailing separator).`,
292
- "id"
293
- );
294
- }
295
- const name = m.name;
296
- const nameOk = isNonEmptyString(name) || isObject(name) && Object.values(name).some((v) => isNonEmptyString(v));
297
- if (!nameOk) {
298
- bag.err("manifest.name.missing", "theme.json is missing a non-empty `name`.", "name");
299
- }
300
- if (!isNonEmptyString(m.version)) {
301
- bag.err("manifest.version.missing", "theme.json is missing `version`.", "version");
302
- } else if (!SEMVER_RE.test(m.version)) {
303
- bag.err("manifest.version.invalid", `Version "${m.version}" is not valid semver.`, "version");
304
- }
305
- if (!isNonEmptyString(m.author)) {
306
- bag.err("manifest.author.missing", "theme.json is missing `author`.", "author");
307
- }
308
- if (m.min_sdk_version !== void 0 && !SEMVER_RE.test(String(m.min_sdk_version))) {
309
- bag.warn("manifest.min_sdk_version.invalid", "`min_sdk_version` is not valid semver.", "min_sdk_version");
310
- }
311
- }
312
- function validateManifest(manifest, ctx = {}) {
313
- const bag = new IssueBag();
314
- if (!isObject(manifest)) {
315
- bag.err("manifest.invalid", "theme.json must be a JSON object.", "theme.json");
316
- return bag.result();
317
- }
318
- validateManifestCore(bag, manifest);
319
- const presets = manifest.presets;
320
- if (!isObject(presets) || Object.keys(presets).length === 0) {
321
- bag.warn("manifest.presets.empty", "theme.json has no presets \u2014 merchants start with an empty page.", "presets");
322
- } else {
323
- if (ctx.sectionTypes) {
324
- for (const type of collectPresetSectionTypes(manifest)) {
325
- if (!ctx.sectionTypes.has(type)) {
326
- bag.err(
327
- "manifest.preset.unknown_section",
328
- `Preset references section type "${type}" with no schemas/sections/${type}.json.`,
329
- "presets"
330
- );
331
- }
332
- }
333
- }
334
- const templates = isObject(presets.templates) ? presets.templates : {};
335
- for (const t of REQUIRED_TEMPLATES) {
336
- if (!(t in templates)) {
337
- bag.warn(
338
- "manifest.template.missing",
339
- `No preset for required template "${t}" \u2014 the storefront will fall back to its built-in.`,
340
- `presets.templates.${t}`
341
- );
342
- }
343
- }
344
- }
345
- return bag.result();
346
- }
347
- function validateBuiltManifest(builtManifest, importMap, opts = {}) {
348
- const bag = new IssueBag();
349
- if (!isObject(builtManifest)) {
350
- bag.err("built.invalid", "manifest.json must be a JSON object.", "manifest.json");
351
- return bag.result();
352
- }
353
- validateManifestCore(bag, builtManifest);
354
- const schemas = builtManifest.section_schemas;
355
- const shipped = new Set(isObject(schemas) ? Object.keys(schemas) : []);
356
- if (!isObject(schemas)) {
357
- bag.warn("built.section_schemas.missing", "manifest.json has no `section_schemas`.", "section_schemas");
358
- }
359
- for (const type of collectPresetSectionTypes(builtManifest)) {
360
- if (!shipped.has(type)) {
361
- bag.err(
362
- "built.preset.unknown_section",
363
- `Preset references section type "${type}" not present in section_schemas.`,
364
- "presets"
365
- );
366
- }
367
- }
368
- if (isObject(schemas)) {
369
- for (const [type, schema] of Object.entries(schemas)) {
370
- const r = validateSectionSchema(schema, { filenameType: type });
371
- for (const issue of r.issues) bag.issues.push(issue);
372
- }
373
- }
374
- if (!isNonEmptyString(builtManifest.plugin_version)) {
375
- bag.warn("built.plugin_version.missing", "manifest.json has no `plugin_version`.", "plugin_version");
376
- }
377
- if (importMap !== void 0) {
378
- if (!isObject(importMap)) {
379
- bag.err("importmap.invalid", "import-map.json must be a JSON object.", "import-map.json");
380
- } else {
381
- const cv = importMap.contract_version;
382
- if (typeof cv !== "number") {
383
- bag.warn(
384
- "importmap.contract_version.missing",
385
- "import-map.json has no numeric `contract_version` \u2014 built by an older plugin.",
386
- "contract_version"
387
- );
388
- } else if (typeof opts.hostContractVersion === "number" && cv > opts.hostContractVersion) {
389
- bag.err(
390
- "importmap.contract_version.incompatible",
391
- `Theme built for contract v${cv}; this platform supports up to v${opts.hostContractVersion}.`,
392
- "contract_version"
393
- );
394
- }
395
- }
396
- }
397
- return bag.result();
398
- }
399
- function mergeResults(...results) {
400
- const issues = results.flatMap((r) => r.issues);
401
- return { valid: !issues.some((i) => i.level === "error"), issues };
402
- }
403
- var ShopContext = react.createContext(null);
404
- var ProductContext = react.createContext(null);
405
- var CollectionContext = react.createContext(null);
406
- var CartContext = react.createContext(null);
407
- var CustomerContext = react.createContext(null);
408
- var ThemeSettingsContext = react.createContext(null);
409
- var LocalizationContext = react.createContext(null);
410
- var PageContext = react.createContext(null);
411
- var CurrentTemplateContext = react.createContext("home");
412
- var NavigationContext = react.createContext(
413
- {}
414
- );
415
- function useLocalization() {
416
- const ctx = react.useContext(LocalizationContext);
417
- if (!ctx) throw new Error("useLocalization must be used within NuMuProvider");
418
- return ctx;
419
- }
420
- function useDirection() {
421
- const { direction } = useLocalization();
422
- return direction;
423
- }
424
- function useLocale() {
425
- const { locale } = useLocalization();
426
- return locale;
427
- }
428
- function useTranslation() {
429
- const { translations, locale } = useLocalization();
430
- return {
431
- t: (key, fallback) => translations[key] || fallback || key,
432
- locale
433
- };
434
- }
435
- function useFieldTranslation(entity, field) {
436
- const { locale } = useLocalization();
437
- if (!entity) return void 0;
438
- const translated = entity.attributes && typeof entity.attributes === "object" ? entity.attributes[`${field}_${locale}`] : void 0;
439
- if (typeof translated === "string" && translated) return translated;
440
- const base = entity[field];
441
- if (typeof base === "string") return base;
442
- if (base == null) return void 0;
443
- return String(base);
444
- }
445
- function useNumberFormat() {
446
- const { formatNumber } = useLocalization();
447
- return formatNumber;
448
- }
449
-
450
- // src/hooks/useShop.ts
451
- var DEFAULT_PROTOCOL = typeof window !== "undefined" ? window.location.protocol.replace(":", "") : "https";
452
- function resolveDomain(store) {
453
- if (store.domain) return store.domain;
454
- if (store.subdomain) {
455
- const platform = typeof window !== "undefined" && window.__NUMU_PLATFORM_DOMAIN || "numueg.app";
456
- return `${store.subdomain}.${platform}`;
457
- }
458
- return store.slug;
459
- }
460
- function useShop() {
461
- const ctx = react.useContext(ShopContext);
462
- if (!ctx) throw new Error("useShop must be used within NuMuProvider");
463
- const locale = useLocale();
464
- return react.useMemo(() => {
465
- const domain = resolveDomain(ctx);
466
- const settings = ctx.settings;
467
- const localePrefixEnabled = Boolean(
468
- settings && settings.locale_url_prefix_enabled
469
- );
470
- const defaultLocale = ctx.default_language || "en";
471
- const prefixedLocales = settings?.locale_url_prefix_locales || null;
472
- const shouldPrefix = (l) => {
473
- if (!localePrefixEnabled) return false;
474
- if (prefixedLocales) return prefixedLocales.includes(l);
475
- return l !== defaultLocale;
476
- };
477
- const formatUrl = (path) => {
478
- if (!path) return `${DEFAULT_PROTOCOL}://${domain}/`;
479
- if (/^https?:\/\//i.test(path)) return path;
480
- if (path.startsWith("//")) return `${DEFAULT_PROTOCOL}:${path}`;
481
- let normalized = path.startsWith("/") ? path : `/${path}`;
482
- if (locale && shouldPrefix(locale)) {
483
- const prefix = `/${locale}/`;
484
- const root = `/${locale}`;
485
- if (normalized !== root && !normalized.startsWith(prefix)) {
486
- normalized = `${root}${normalized}`;
487
- }
488
- }
489
- return `${DEFAULT_PROTOCOL}://${domain}${normalized}`;
490
- };
491
- return { ...ctx, domain, formatUrl };
492
- }, [ctx, locale]);
493
- }
494
12
  function useProduct() {
495
- const ctx = react.useContext(ProductContext);
13
+ const ctx = react.useContext(chunk3C37JX2X_cjs.ProductContext);
496
14
  if (!ctx) throw new Error("useProduct must be used within ProductProvider");
497
15
  return ctx;
498
16
  }
499
17
  function useProductOptional() {
500
- return react.useContext(ProductContext);
18
+ return react.useContext(chunk3C37JX2X_cjs.ProductContext);
501
19
  }
502
20
  function useCollection() {
503
- const ctx = react.useContext(CollectionContext);
21
+ const ctx = react.useContext(chunk3C37JX2X_cjs.CollectionContext);
504
22
  if (!ctx) throw new Error("useCollection must be used within CollectionProvider");
505
23
  return ctx;
506
24
  }
507
25
  function useCollectionOptional() {
508
- return react.useContext(CollectionContext);
26
+ return react.useContext(chunk3C37JX2X_cjs.CollectionContext);
509
27
  }
510
28
  function useCart() {
511
- const ctx = react.useContext(CartContext);
29
+ const ctx = react.useContext(chunk3C37JX2X_cjs.CartContext);
512
30
  if (!ctx) throw new Error("useCart must be used within NuMuProvider");
513
31
  return ctx;
514
32
  }
515
- function useCustomer() {
516
- return react.useContext(CustomerContext);
517
- }
518
- function useThemeSettings() {
519
- const ctx = react.useContext(ThemeSettingsContext);
520
- if (!ctx) throw new Error("useThemeSettings must be used within NuMuProvider");
521
- return ctx;
522
- }
523
33
  function useCurrentTemplate() {
524
- return react.useContext(CurrentTemplateContext);
525
- }
526
- function usePage() {
527
- return react.useContext(PageContext);
34
+ return react.useContext(chunk3C37JX2X_cjs.CurrentTemplateContext);
528
35
  }
529
36
  var SectionContext = react.createContext(null);
530
37
  function useSection() {
@@ -535,13 +42,57 @@ function useSection() {
535
42
  function useSectionOptional() {
536
43
  return react.useContext(SectionContext);
537
44
  }
45
+ var EMPTY = Object.freeze([]);
46
+ function useSectionGroup(group) {
47
+ const settings = react.useContext(chunk3C37JX2X_cjs.ThemeSettingsContext);
48
+ return react.useMemo(() => {
49
+ const grp = settings?.section_groups?.[group];
50
+ if (!grp || !Array.isArray(grp.order) || grp.order.length === 0) {
51
+ return EMPTY;
52
+ }
53
+ const sections = grp.sections ?? {};
54
+ const out = [];
55
+ for (const id of grp.order) {
56
+ const instance = sections[id];
57
+ if (!instance) continue;
58
+ out.push({ id, ...instance });
59
+ }
60
+ return out;
61
+ }, [settings, group]);
62
+ }
63
+ function useCurrency() {
64
+ const ctx = react.useContext(chunk3C37JX2X_cjs.CurrencyContext);
65
+ const shop = chunk3C37JX2X_cjs.useShop();
66
+ const fallback = react.useMemo(() => {
67
+ const ccy = shop?.currency || "EGP";
68
+ return {
69
+ base: ccy,
70
+ selected: ccy,
71
+ presentment: [ccy],
72
+ rates: {},
73
+ autoConvert: false,
74
+ loading: false,
75
+ setSelected: () => {
76
+ },
77
+ convert: (cents) => cents
78
+ };
79
+ }, [shop?.currency]);
80
+ return ctx ?? fallback;
81
+ }
538
82
 
539
83
  // src/hooks/useMoney.ts
540
84
  function useMoney(currencyOverride) {
541
- const { formatMoney } = useLocalization();
542
- const shop = useShop();
543
- const ccy = currencyOverride || shop?.currency;
544
- return (amount) => formatMoney(amount, ccy);
85
+ const { formatMoney } = chunk3C37JX2X_cjs.useLocalization();
86
+ const shop = chunk3C37JX2X_cjs.useShop();
87
+ const { selected, base, autoConvert, convert } = useCurrency();
88
+ return (amount) => {
89
+ const shouldConvert = autoConvert && !currencyOverride && !!selected && selected !== base;
90
+ if (shouldConvert) {
91
+ const converted = convert(Math.round(amount * 100), selected) / 100;
92
+ return formatMoney(converted, selected);
93
+ }
94
+ return formatMoney(amount, currencyOverride || shop?.currency);
95
+ };
545
96
  }
546
97
  var DEFAULT_WIDTHS = [320, 480, 640, 768, 1024, 1280, 1600, 1920];
547
98
  var DEFAULT_SIZES = "(min-width: 1024px) 25vw, (min-width: 640px) 50vw, 100vw";
@@ -555,81 +106,6 @@ function useImage(src, opts = {}) {
555
106
  return { src, srcSet, sizes, alt };
556
107
  }, [src, sizes, widths, alt]);
557
108
  }
558
- function useProducts(opts = {}) {
559
- const { limit, fetchIfMissing = false } = opts;
560
- const page = usePage();
561
- const shop = useShop();
562
- const initial = page?.data?.products ?? null;
563
- const [products, setProducts] = react.useState(initial ?? []);
564
- const [loading, setLoading] = react.useState(
565
- initial == null && fetchIfMissing
566
- );
567
- const [error, setError] = react.useState(null);
568
- react.useEffect(() => {
569
- if (initial != null) return;
570
- if (!fetchIfMissing) return;
571
- if (!shop?.id) return;
572
- let cancelled = false;
573
- (async () => {
574
- try {
575
- const params = new URLSearchParams({ store_id: shop.id });
576
- if (limit) params.set("limit", String(limit));
577
- const res = await fetch(`/api/products?${params.toString()}`);
578
- if (!res.ok) throw new Error(`/api/products \u2192 ${res.status}`);
579
- const data = await res.json();
580
- if (cancelled) return;
581
- setProducts(data.products ?? []);
582
- setLoading(false);
583
- } catch (err) {
584
- if (cancelled) return;
585
- setError(err instanceof Error ? err : new Error(String(err)));
586
- setLoading(false);
587
- }
588
- })();
589
- return () => {
590
- cancelled = true;
591
- };
592
- }, [initial, fetchIfMissing, limit, shop?.id]);
593
- const sliced = limit != null ? products.slice(0, limit) : products;
594
- return { products: sliced, loading, error };
595
- }
596
- function useCollections(opts = {}) {
597
- const { limit, fetchIfMissing = false } = opts;
598
- const page = usePage();
599
- const shop = useShop();
600
- const initial = page?.data?.collections ?? null;
601
- const [collections, setCollections] = react.useState(initial ?? []);
602
- const [loading, setLoading] = react.useState(
603
- initial == null && fetchIfMissing
604
- );
605
- const [error, setError] = react.useState(null);
606
- react.useEffect(() => {
607
- if (initial != null) return;
608
- if (!fetchIfMissing) return;
609
- if (!shop?.id) return;
610
- let cancelled = false;
611
- (async () => {
612
- try {
613
- const params = new URLSearchParams({ store_id: shop.id });
614
- const res = await fetch(`/api/collections?${params.toString()}`);
615
- if (!res.ok) throw new Error(`/api/collections \u2192 ${res.status}`);
616
- const data = await res.json();
617
- if (cancelled) return;
618
- setCollections(data.collections ?? []);
619
- setLoading(false);
620
- } catch (err) {
621
- if (cancelled) return;
622
- setError(err instanceof Error ? err : new Error(String(err)));
623
- setLoading(false);
624
- }
625
- })();
626
- return () => {
627
- cancelled = true;
628
- };
629
- }, [initial, fetchIfMissing, shop?.id]);
630
- const sliced = limit != null ? collections.slice(0, limit) : collections;
631
- return { collections: sliced, loading, error };
632
- }
633
109
  var NOOP_ACTIONS = {
634
110
  login: async () => ({}),
635
111
  register: async () => ({}),
@@ -655,7 +131,7 @@ function unwrap(json) {
655
131
  return json;
656
132
  }
657
133
  function useOrders() {
658
- const customer = useCustomer();
134
+ const customer = chunk3C37JX2X_cjs.useCustomer();
659
135
  const [orders, setOrders] = react.useState([]);
660
136
  const [loading, setLoading] = react.useState(false);
661
137
  const [error, setError] = react.useState(null);
@@ -702,7 +178,7 @@ function useOrders() {
702
178
  };
703
179
  }
704
180
  function useOrder(id) {
705
- const customer = useCustomer();
181
+ const customer = chunk3C37JX2X_cjs.useCustomer();
706
182
  const [order, setOrder] = react.useState(null);
707
183
  const [loading, setLoading] = react.useState(false);
708
184
  const [error, setError] = react.useState(null);
@@ -765,7 +241,7 @@ async function readBody(res) {
765
241
  }
766
242
  }
767
243
  function useCustomerAddresses() {
768
- const customer = useCustomer();
244
+ const customer = chunk3C37JX2X_cjs.useCustomer();
769
245
  const [addresses, setAddresses] = react.useState([]);
770
246
  const [loading, setLoading] = react.useState(false);
771
247
  const [error, setError] = react.useState(null);
@@ -928,8 +404,8 @@ function toNavigationItem(raw, locale) {
928
404
  };
929
405
  }
930
406
  function useNavigation(handle, options) {
931
- const navMap = react.useContext(NavigationContext);
932
- const localization = react.useContext(LocalizationContext);
407
+ const navMap = react.useContext(chunk3C37JX2X_cjs.NavigationContext);
408
+ const localization = react.useContext(chunk3C37JX2X_cjs.LocalizationContext);
933
409
  const locale = localization?.locale ?? "en";
934
410
  const hostProvidedAny = !!navMap && Object.keys(navMap).length > 0;
935
411
  const rawItems = handle ? navMap?.[handle] : void 0;
@@ -999,7 +475,7 @@ function useNavigation(handle, options) {
999
475
  }, [handle, hostItems === null]);
1000
476
  return { items, loading, error };
1001
477
  }
1002
- var EMPTY = {
478
+ var EMPTY2 = {
1003
479
  products: [],
1004
480
  collections: [],
1005
481
  pages: [],
@@ -1014,7 +490,7 @@ function useSearch(query, options = {}) {
1014
490
  debounceMs = 200,
1015
491
  immediate = false
1016
492
  } = options;
1017
- const [results, setResults] = react.useState(EMPTY);
493
+ const [results, setResults] = react.useState(EMPTY2);
1018
494
  const [loading, setLoading] = react.useState(false);
1019
495
  const [error, setError] = react.useState(null);
1020
496
  const lastRequestId = react.useRef(0);
@@ -1022,7 +498,7 @@ function useSearch(query, options = {}) {
1022
498
  if (typeof window === "undefined") return;
1023
499
  const trimmed = query.trim();
1024
500
  if (!trimmed) {
1025
- setResults(EMPTY);
501
+ setResults(EMPTY2);
1026
502
  setLoading(false);
1027
503
  setError(null);
1028
504
  return;
@@ -1040,7 +516,7 @@ function useSearch(query, options = {}) {
1040
516
  });
1041
517
  if (requestId !== lastRequestId.current) return;
1042
518
  if (!res.ok) {
1043
- setResults(EMPTY);
519
+ setResults(EMPTY2);
1044
520
  return;
1045
521
  }
1046
522
  const json = await res.json();
@@ -1056,7 +532,7 @@ function useSearch(query, options = {}) {
1056
532
  } catch (err) {
1057
533
  if (requestId !== lastRequestId.current) return;
1058
534
  setError(err instanceof Error ? err : new Error(String(err)));
1059
- setResults(EMPTY);
535
+ setResults(EMPTY2);
1060
536
  } finally {
1061
537
  if (requestId === lastRequestId.current) setLoading(false);
1062
538
  }
@@ -1161,63 +637,218 @@ function useAnalytics() {
1161
637
  );
1162
638
  return react.useMemo(() => ({ track }), [track]);
1163
639
  }
1164
- function useApp(slug) {
1165
- const shop = useShop();
1166
- const [state, setState] = react.useState({ data: null, loading: true, available: false, error: null });
1167
- const fetchApp = react.useCallback(async () => {
1168
- if (!slug) {
1169
- setState({ data: null, loading: false, available: false, error: null });
1170
- return;
1171
- }
1172
- setState((s) => ({ ...s, loading: true, error: null }));
640
+ var cache2 = /* @__PURE__ */ new Map();
641
+ function buildSnapshot(entry) {
642
+ return {
643
+ data: entry.data,
644
+ error: entry.error,
645
+ isLoading: !entry.settled,
646
+ isValidating: entry.inFlight
647
+ };
648
+ }
649
+ function commit(entry) {
650
+ entry.snapshot = buildSnapshot(entry);
651
+ entry.subscribers.forEach((cb) => cb());
652
+ }
653
+ function getEntry(key, initialData) {
654
+ let entry = cache2.get(key);
655
+ if (!entry) {
656
+ entry = {
657
+ data: initialData,
658
+ error: void 0,
659
+ settled: false,
660
+ inFlight: false,
661
+ seq: 0,
662
+ lastFetchAt: 0,
663
+ promise: void 0,
664
+ controller: void 0,
665
+ fetcher: void 0,
666
+ subscribers: /* @__PURE__ */ new Set(),
667
+ snapshot: {
668
+ data: initialData,
669
+ error: void 0,
670
+ isLoading: true,
671
+ isValidating: false
672
+ }
673
+ };
674
+ cache2.set(key, entry);
675
+ }
676
+ return entry;
677
+ }
678
+ function revalidateResource(key, fetcher, opts) {
679
+ const entry = getEntry(key);
680
+ entry.fetcher = fetcher;
681
+ if (!opts?.force && entry.promise) return entry.promise;
682
+ const mySeq = ++entry.seq;
683
+ if (entry.controller) entry.controller.abort();
684
+ const controller = new AbortController();
685
+ entry.controller = controller;
686
+ entry.inFlight = true;
687
+ commit(entry);
688
+ const run = (async () => {
1173
689
  try {
690
+ const data = await fetcher(controller.signal);
691
+ if (mySeq !== entry.seq) return;
692
+ entry.data = data;
693
+ entry.error = void 0;
694
+ entry.settled = true;
695
+ entry.lastFetchAt = Date.now();
696
+ } catch (err) {
697
+ if (controller.signal.aborted) return;
698
+ if (mySeq !== entry.seq) return;
699
+ const error = err instanceof Error ? err : new Error(String(err));
700
+ entry.error = error;
701
+ entry.settled = true;
702
+ entry.lastFetchAt = Date.now();
703
+ opts?.onError?.(error, key);
704
+ } finally {
705
+ if (mySeq === entry.seq) {
706
+ entry.inFlight = false;
707
+ entry.controller = void 0;
708
+ commit(entry);
709
+ }
710
+ }
711
+ })();
712
+ entry.promise = run;
713
+ void run.finally(() => {
714
+ if (entry.promise === run) entry.promise = void 0;
715
+ });
716
+ return run;
717
+ }
718
+ function mutateResource(key, next, opts) {
719
+ const entry = getEntry(key);
720
+ const value = typeof next === "function" ? next(entry.data) : next;
721
+ entry.data = value;
722
+ entry.error = void 0;
723
+ entry.settled = true;
724
+ commit(entry);
725
+ if (opts?.revalidate && entry.fetcher) {
726
+ void revalidateResource(key, entry.fetcher, { force: true });
727
+ }
728
+ }
729
+ function readResource(key) {
730
+ return cache2.get(key)?.data;
731
+ }
732
+ var NOOP = () => {
733
+ };
734
+ function useCachedResource(key, fetcher, options) {
735
+ const enabled = options?.enabled !== false && key != null && key !== "";
736
+ const revalidateOnMount = options?.revalidateOnMount !== false;
737
+ const dedupeIntervalMs = options?.dedupeIntervalMs ?? 2e3;
738
+ const cacheKey = enabled ? key : "";
739
+ const fetcherRef = react.useRef(fetcher);
740
+ fetcherRef.current = fetcher;
741
+ const onErrorRef = react.useRef(options?.onError);
742
+ onErrorRef.current = options?.onError;
743
+ const initialDataRef = react.useRef(options?.initialData);
744
+ const fallbackRef = react.useRef(null);
745
+ if (fallbackRef.current === null) {
746
+ fallbackRef.current = {
747
+ data: initialDataRef.current,
748
+ error: void 0,
749
+ isLoading: enabled && revalidateOnMount,
750
+ isValidating: false
751
+ };
752
+ }
753
+ const subscribe = react.useCallback(
754
+ (cb) => {
755
+ if (!enabled) return NOOP;
756
+ const entry = getEntry(cacheKey, initialDataRef.current);
757
+ entry.subscribers.add(cb);
758
+ return () => {
759
+ entry.subscribers.delete(cb);
760
+ };
761
+ },
762
+ [cacheKey, enabled]
763
+ );
764
+ const getSnapshot = react.useCallback(() => {
765
+ if (!enabled) return fallbackRef.current;
766
+ return getEntry(cacheKey, initialDataRef.current).snapshot;
767
+ }, [cacheKey, enabled]);
768
+ const getServerSnapshot = react.useCallback(
769
+ () => fallbackRef.current,
770
+ []
771
+ );
772
+ const state = react.useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
773
+ react.useEffect(() => {
774
+ if (!enabled) return;
775
+ const entry = getEntry(cacheKey, initialDataRef.current);
776
+ const fresh = entry.settled && Date.now() - entry.lastFetchAt < dedupeIntervalMs;
777
+ if (revalidateOnMount && !fresh) {
778
+ void revalidateResource(
779
+ cacheKey,
780
+ (signal) => fetcherRef.current(signal),
781
+ { onError: (e, k) => onErrorRef.current?.(e, k) }
782
+ );
783
+ }
784
+ }, [cacheKey, enabled, revalidateOnMount, dedupeIntervalMs]);
785
+ const mutate = react.useCallback(
786
+ (next, opts) => {
787
+ if (!enabled) return;
788
+ mutateResource(cacheKey, next, opts);
789
+ },
790
+ [cacheKey, enabled]
791
+ );
792
+ const revalidate = react.useCallback(() => {
793
+ if (!enabled) return Promise.resolve();
794
+ return revalidateResource(
795
+ cacheKey,
796
+ (signal) => fetcherRef.current(signal),
797
+ { force: true, onError: (e, k) => onErrorRef.current?.(e, k) }
798
+ );
799
+ }, [cacheKey, enabled]);
800
+ return {
801
+ data: state.data,
802
+ error: state.error,
803
+ isLoading: state.isLoading,
804
+ isValidating: state.isValidating,
805
+ mutate,
806
+ revalidate
807
+ };
808
+ }
809
+
810
+ // src/hooks/useApp.ts
811
+ function useApp(slug) {
812
+ const shop = chunk3C37JX2X_cjs.useShop();
813
+ const key = slug ? `numu:app:${shop.id}:${slug}` : "";
814
+ const fetcher = react.useCallback(
815
+ async (signal) => {
1174
816
  const url = `/api/storefront/apps/${encodeURIComponent(slug)}`;
1175
817
  const res = await fetch(url, {
1176
818
  credentials: "include",
1177
- headers: { Accept: "application/json" }
819
+ headers: { Accept: "application/json" },
820
+ signal
1178
821
  });
1179
822
  if (res.status === 404) {
1180
- setState({
1181
- data: null,
1182
- loading: false,
1183
- available: false,
1184
- error: null
1185
- });
1186
- return;
823
+ return { payload: null, available: false };
1187
824
  }
1188
825
  if (!res.ok) {
1189
826
  throw new Error(`useApp(${slug}) failed: HTTP ${res.status}`);
1190
827
  }
1191
828
  const body = await res.json();
1192
- const payload = body.data;
1193
- if (!payload) {
1194
- setState({
1195
- data: null,
1196
- loading: false,
1197
- available: false,
1198
- error: null
1199
- });
1200
- return;
1201
- }
1202
- setState({
1203
- data: payload,
1204
- loading: false,
1205
- available: payload.available !== false,
1206
- error: null
1207
- });
1208
- } catch (e) {
1209
- setState({
1210
- data: null,
1211
- loading: false,
1212
- available: false,
1213
- error: e instanceof Error ? e : new Error(String(e))
1214
- });
1215
- }
1216
- }, [slug, shop.id]);
1217
- react.useEffect(() => {
1218
- void fetchApp();
1219
- }, [fetchApp]);
1220
- return { ...state, refresh: fetchApp };
829
+ const payload = body.data ?? null;
830
+ if (!payload) return { payload: null, available: false };
831
+ return {
832
+ payload,
833
+ available: payload.available !== false
834
+ };
835
+ },
836
+ [slug]
837
+ );
838
+ const { data, isLoading, error, revalidate } = useCachedResource(key, fetcher, {
839
+ enabled: Boolean(slug),
840
+ initialData: { payload: null, available: false }
841
+ });
842
+ const refresh = react.useCallback(async () => {
843
+ await revalidate();
844
+ }, [revalidate]);
845
+ return {
846
+ data: error ? null : data?.payload ?? null,
847
+ loading: isLoading,
848
+ available: error ? false : data?.available ?? false,
849
+ error: error ?? null,
850
+ refresh
851
+ };
1221
852
  }
1222
853
  function storageKey(storeId) {
1223
854
  return `numu_wishlist_${storeId}`;
@@ -1233,114 +864,100 @@ function readLocal(storeId) {
1233
864
  return [];
1234
865
  }
1235
866
  }
1236
- function writeLocal(storeId, items) {
867
+ function persistWishlist(storeId, items) {
1237
868
  if (typeof window === "undefined") return;
1238
- try {
1239
- window.localStorage.setItem(storageKey(storeId), JSON.stringify(items));
1240
- } catch {
1241
- }
869
+ window.localStorage.setItem(storageKey(storeId), JSON.stringify(items));
1242
870
  }
1243
871
  function sameItem(a, productId, variantId) {
1244
872
  return a.product_id === productId && (a.variant_id ?? null) === (variantId ?? null);
1245
873
  }
874
+ var EMPTY3 = [];
1246
875
  function useWishlist(storeId) {
1247
- const customer = useCustomer();
1248
- const [items, setItems] = react.useState([]);
1249
- const [loading, setLoading] = react.useState(true);
1250
- react.useEffect(() => {
1251
- if (typeof window === "undefined") return;
1252
- setItems(readLocal(storeId));
1253
- setLoading(false);
1254
- }, [storeId, customer?.id]);
876
+ const key = storageKey(storeId);
877
+ const fetcher = react.useCallback(
878
+ () => Promise.resolve(readLocal(storeId)),
879
+ [storeId]
880
+ );
881
+ const { data, isLoading, mutate } = useCachedResource(
882
+ key,
883
+ fetcher,
884
+ { initialData: EMPTY3 }
885
+ );
886
+ const items = data ?? EMPTY3;
1255
887
  const has = react.useCallback(
1256
888
  (productId, variantId) => items.some((it) => sameItem(it, productId, variantId)),
1257
889
  [items]
1258
890
  );
891
+ const applyOptimistic = react.useCallback(
892
+ (compute) => {
893
+ const prev = readResource(key) ?? EMPTY3;
894
+ const next = compute(prev);
895
+ if (next === null) return;
896
+ mutate(next, { revalidate: false });
897
+ try {
898
+ persistWishlist(storeId, next);
899
+ } catch {
900
+ mutate(prev, { revalidate: false });
901
+ }
902
+ },
903
+ [key, storeId, mutate]
904
+ );
1259
905
  const addToWishlist = react.useCallback(
1260
- (productId, variantId) => {
1261
- setItems((prev) => {
1262
- if (prev.some((it) => sameItem(it, productId, variantId))) {
1263
- return prev;
906
+ (productId, variantId) => applyOptimistic(
907
+ (prev) => prev.some((it) => sameItem(it, productId, variantId)) ? null : [
908
+ ...prev,
909
+ {
910
+ product_id: productId,
911
+ variant_id: variantId ?? null,
912
+ added_at: Date.now()
1264
913
  }
1265
- const next = [
1266
- ...prev,
1267
- {
1268
- product_id: productId,
1269
- variant_id: variantId ?? null,
1270
- added_at: Date.now()
1271
- }
1272
- ];
1273
- writeLocal(storeId, next);
1274
- return next;
1275
- });
1276
- },
1277
- [storeId]
914
+ ]
915
+ ),
916
+ [applyOptimistic]
1278
917
  );
1279
918
  const removeFromWishlist = react.useCallback(
1280
- (productId, variantId) => {
1281
- setItems((prev) => {
1282
- const next = prev.filter((it) => !sameItem(it, productId, variantId));
1283
- writeLocal(storeId, next);
1284
- return next;
1285
- });
1286
- },
1287
- [storeId]
919
+ (productId, variantId) => applyOptimistic((prev) => {
920
+ const next = prev.filter((it) => !sameItem(it, productId, variantId));
921
+ return next.length === prev.length ? null : next;
922
+ }),
923
+ [applyOptimistic]
924
+ );
925
+ const clear = react.useCallback(
926
+ () => applyOptimistic((prev) => prev.length === 0 ? null : []),
927
+ [applyOptimistic]
1288
928
  );
1289
- const clear = react.useCallback(() => {
1290
- setItems([]);
1291
- writeLocal(storeId, []);
1292
- }, [storeId]);
1293
- return { items, loading, has, addToWishlist, removeFromWishlist, clear };
929
+ return { items, loading: isLoading, has, addToWishlist, removeFromWishlist, clear };
1294
930
  }
931
+ var EMPTY4 = [];
1295
932
  function useRelatedProducts(productId, options = {}) {
1296
933
  const limit = options.limit ?? 4;
1297
- const [items, setItems] = react.useState([]);
1298
- const [loading, setLoading] = react.useState(false);
1299
- const [error, setError] = react.useState(null);
1300
- react.useEffect(() => {
1301
- if (typeof window === "undefined") return;
1302
- if (!productId) {
1303
- setItems([]);
1304
- setLoading(false);
1305
- setError(null);
1306
- return;
1307
- }
1308
- let cancelled = false;
1309
- setLoading(true);
1310
- setError(null);
1311
- void (async () => {
1312
- try {
1313
- const res = await fetch(
1314
- `/api/storefront/products/${encodeURIComponent(productId)}/related?limit=${limit}`,
1315
- { cache: "no-store" }
1316
- );
1317
- if (!res.ok) {
1318
- if (cancelled) return;
1319
- setItems([]);
1320
- return;
1321
- }
1322
- const json = await res.json();
1323
- const list = Array.isArray(json) ? json : Array.isArray(json.data) ? json.data : Array.isArray(json.items) ? json.items : [];
1324
- if (cancelled) return;
1325
- setItems(list);
1326
- } catch (err) {
1327
- if (cancelled) return;
1328
- setError(err instanceof Error ? err : new Error(String(err)));
1329
- setItems([]);
1330
- } finally {
1331
- if (!cancelled) setLoading(false);
934
+ const key = productId ? `numu:related:${productId}:${limit}` : "";
935
+ const fetcher = react.useCallback(
936
+ async (signal) => {
937
+ const res = await fetch(
938
+ `/api/storefront/products/${encodeURIComponent(
939
+ productId
940
+ )}/related?limit=${limit}`,
941
+ { cache: "no-store", signal }
942
+ );
943
+ if (!res.ok) {
944
+ return EMPTY4;
1332
945
  }
1333
- })();
1334
- return () => {
1335
- cancelled = true;
1336
- };
1337
- }, [productId, limit]);
1338
- return { items, loading, error };
946
+ const json = await res.json();
947
+ return Array.isArray(json) ? json : Array.isArray(json.data) ? json.data : Array.isArray(json.items) ? json.items : EMPTY4;
948
+ },
949
+ [productId, limit]
950
+ );
951
+ const { data, isLoading, error } = useCachedResource(key, fetcher, {
952
+ enabled: Boolean(productId),
953
+ initialData: EMPTY4
954
+ });
955
+ return { items: data ?? EMPTY4, loading: isLoading, error: error ?? null };
1339
956
  }
1340
957
  function useProductSizeChart(productOverride) {
1341
958
  const ctxProduct = useProductOptional();
1342
959
  const product = productOverride ?? ctxProduct;
1343
- const shop = useShop();
960
+ const shop = chunk3C37JX2X_cjs.useShop();
1344
961
  const storeSettings = shop?.settings;
1345
962
  return react.useMemo(
1346
963
  () => resolveSizeChart(product?.attributes, storeSettings),
@@ -1360,86 +977,6 @@ function resolveSizeChart(productAttributes, storeSettings) {
1360
977
  if (hasRows(storeDefault)) return storeDefault;
1361
978
  return null;
1362
979
  }
1363
- var COOKIE_NAME = "numu_currency";
1364
- var COOKIE_MAX_AGE = 60 * 60 * 24 * 30;
1365
- function readCookie(name) {
1366
- if (typeof document === "undefined") return null;
1367
- const match = document.cookie.match(
1368
- new RegExp(`(?:^|;\\s*)${name}=([^;]+)`)
1369
- );
1370
- return match ? decodeURIComponent(match[1]) : null;
1371
- }
1372
- function writeCookie(name, value) {
1373
- if (typeof document === "undefined") return;
1374
- document.cookie = `${name}=${encodeURIComponent(value)}; path=/; max-age=${COOKIE_MAX_AGE}; SameSite=Lax`;
1375
- }
1376
- function useCurrency() {
1377
- const shop = useShop();
1378
- const [config, setConfig] = react.useState(null);
1379
- const [loading, setLoading] = react.useState(true);
1380
- const [selected, setSelectedState] = react.useState("");
1381
- react.useEffect(() => {
1382
- let cancelled = false;
1383
- (async () => {
1384
- try {
1385
- const res = await fetch(`/api/storefront/currencies`, {
1386
- credentials: "include",
1387
- headers: { Accept: "application/json" }
1388
- });
1389
- if (!res.ok) throw new Error(`currencies: HTTP ${res.status}`);
1390
- const body = await res.json();
1391
- if (cancelled) return;
1392
- setConfig(body.data);
1393
- const cookie = readCookie(COOKIE_NAME);
1394
- const valid = cookie && body.data.presentment.includes(cookie) ? cookie : null;
1395
- setSelectedState(valid || body.data.default_presentment || body.data.base);
1396
- } catch {
1397
- if (cancelled) return;
1398
- setConfig(null);
1399
- setSelectedState(shop.currency || "EGP");
1400
- } finally {
1401
- if (!cancelled) setLoading(false);
1402
- }
1403
- })();
1404
- return () => {
1405
- cancelled = true;
1406
- };
1407
- }, [shop.id, shop.currency]);
1408
- const rates = react.useMemo(() => {
1409
- const out = {};
1410
- if (!config) return out;
1411
- for (const [k, v] of Object.entries(config.rates)) {
1412
- const n = Number.parseFloat(v);
1413
- if (Number.isFinite(n)) out[k] = n;
1414
- }
1415
- return out;
1416
- }, [config]);
1417
- const convert = react.useCallback(
1418
- (cents, target) => {
1419
- if (!config) return cents;
1420
- const to = target || selected;
1421
- if (!to || to === config.base) return cents;
1422
- const rate = rates[to];
1423
- if (!rate || !Number.isFinite(rate)) return cents;
1424
- return Math.round(cents * rate);
1425
- },
1426
- [config, rates, selected]
1427
- );
1428
- const setSelected = react.useCallback((currency) => {
1429
- setSelectedState(currency);
1430
- writeCookie(COOKIE_NAME, currency);
1431
- }, []);
1432
- return {
1433
- base: config?.base || shop.currency || "EGP",
1434
- selected: selected || config?.base || shop.currency || "EGP",
1435
- presentment: config?.presentment || [shop.currency || "EGP"],
1436
- rates,
1437
- autoConvert: Boolean(config?.auto_convert),
1438
- loading,
1439
- setSelected,
1440
- convert
1441
- };
1442
- }
1443
980
 
1444
981
  // src/utils/variants.ts
1445
982
  function findVariantByOptions(product, selection) {
@@ -1895,6 +1432,19 @@ function readCsrfCookie() {
1895
1432
  const match = document.cookie.match(/(?:^|;\s*)numu_csrf=([^;]+)/);
1896
1433
  return match ? decodeURIComponent(match[1]) : null;
1897
1434
  }
1435
+ var CURRENCY_COOKIE_NAME = "numu_currency";
1436
+ var CURRENCY_COOKIE_MAX_AGE = 60 * 60 * 24 * 30;
1437
+ function readCurrencyCookie() {
1438
+ if (typeof document === "undefined") return null;
1439
+ const match = document.cookie.match(
1440
+ new RegExp(`(?:^|;\\s*)${CURRENCY_COOKIE_NAME}=([^;]+)`)
1441
+ );
1442
+ return match ? decodeURIComponent(match[1]) : null;
1443
+ }
1444
+ function writeCurrencyCookie(value) {
1445
+ if (typeof document === "undefined") return;
1446
+ document.cookie = `${CURRENCY_COOKIE_NAME}=${encodeURIComponent(value)}; path=/; max-age=${CURRENCY_COOKIE_MAX_AGE}; SameSite=Lax`;
1447
+ }
1898
1448
  async function postCartMutation(endpoint, body, applyCart, reserveToken) {
1899
1449
  const headers = {
1900
1450
  "Content-Type": "application/json"
@@ -1908,9 +1458,21 @@ async function postCartMutation(endpoint, body, applyCart, reserveToken) {
1908
1458
  headers,
1909
1459
  body: body === void 0 ? void 0 : JSON.stringify(body)
1910
1460
  });
1911
- if (!res.ok) return;
1461
+ if (!res.ok) {
1462
+ let message;
1463
+ try {
1464
+ const err = await res.json();
1465
+ if (err && typeof err === "object") {
1466
+ const m = err.message ?? err.error ?? err.detail;
1467
+ if (typeof m === "string") message = m;
1468
+ }
1469
+ } catch {
1470
+ }
1471
+ return { ok: false, status: res.status, message };
1472
+ }
1912
1473
  const json = await res.json();
1913
1474
  applyCart(unwrapCart(json));
1475
+ return { ok: true, status: res.status };
1914
1476
  }
1915
1477
  function NuMuProvider({
1916
1478
  store,
@@ -1920,6 +1482,7 @@ function NuMuProvider({
1920
1482
  locale: initialLocale,
1921
1483
  translations: initialTranslations,
1922
1484
  currentTemplate = "home",
1485
+ pageTemplate,
1923
1486
  initialProducts,
1924
1487
  initialCollections,
1925
1488
  navigation,
@@ -1929,17 +1492,21 @@ function NuMuProvider({
1929
1492
  () => ({
1930
1493
  type: currentTemplate,
1931
1494
  title: store?.name ?? "",
1495
+ // I3 — resolved alternate template key (e.g. "product.wholesale"),
1496
+ // surfaced via usePage()?.template. Omitted when undefined so a
1497
+ // page on its default template exposes no `template` field.
1498
+ ...pageTemplate ? { template: pageTemplate } : {},
1932
1499
  data: {
1933
1500
  products: initialProducts ?? [],
1934
1501
  collections: initialCollections ?? []
1935
1502
  }
1936
1503
  }),
1937
- [currentTemplate, store?.name, initialProducts, initialCollections]
1504
+ [currentTemplate, pageTemplate, store?.name, initialProducts, initialCollections]
1938
1505
  );
1939
1506
  const [cart, setCart] = react.useState(
1940
1507
  initialCart || { ...EMPTY_CART, currency: store.currency }
1941
1508
  );
1942
- const [loading, setLoading] = react.useState(false);
1509
+ const [loading, setLoading] = react.useState(!initialCart);
1943
1510
  const [locale, setLocale] = react.useState(() => {
1944
1511
  if (initialLocale) return initialLocale;
1945
1512
  if (typeof document !== "undefined") {
@@ -2096,6 +1663,8 @@ function NuMuProvider({
2096
1663
  setCart(normalizeCartFromServer(data));
2097
1664
  }
2098
1665
  } catch {
1666
+ } finally {
1667
+ if (!cancelled) setLoading(false);
2099
1668
  }
2100
1669
  })();
2101
1670
  return () => {
@@ -2123,7 +1692,7 @@ function NuMuProvider({
2123
1692
  const token = reserveToken();
2124
1693
  setLoading(true);
2125
1694
  try {
2126
- await postCartMutation(
1695
+ return await postCartMutation(
2127
1696
  endpoint,
2128
1697
  body,
2129
1698
  buildApplyCart(token),
@@ -2140,12 +1709,13 @@ function NuMuProvider({
2140
1709
  async (productId, variantId, quantity) => {
2141
1710
  const eventId = typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : `${Date.now()}`;
2142
1711
  const qty = quantity || 1;
2143
- await mutate("/api/cart/add", {
1712
+ const result = await mutate("/api/cart/add", {
2144
1713
  product_id: productId,
2145
1714
  variant_id: variantId,
2146
1715
  quantity: qty,
2147
1716
  _event_id: eventId
2148
1717
  });
1718
+ if (!result.ok) return result;
2149
1719
  try {
2150
1720
  if (typeof window !== "undefined") {
2151
1721
  window.dispatchEvent(
@@ -2164,34 +1734,29 @@ function NuMuProvider({
2164
1734
  }
2165
1735
  } catch {
2166
1736
  }
1737
+ return result;
2167
1738
  },
2168
1739
  [mutate]
2169
1740
  );
2170
1741
  const removeItem = react.useCallback(
2171
- async (itemId) => {
2172
- await mutate("/api/cart/remove", { item_id: itemId });
2173
- },
1742
+ (itemId) => mutate("/api/cart/remove", { item_id: itemId }),
2174
1743
  [mutate]
2175
1744
  );
2176
1745
  const updateQuantity = react.useCallback(
2177
- async (itemId, quantity) => {
2178
- await mutate("/api/cart/update", { item_id: itemId, quantity });
2179
- },
1746
+ (itemId, quantity) => mutate("/api/cart/update", { item_id: itemId, quantity }),
2180
1747
  [mutate]
2181
1748
  );
2182
1749
  const applyDiscount = react.useCallback(
2183
- async (code) => {
2184
- await mutate("/api/cart/discount", { code });
2185
- },
1750
+ (code) => mutate("/api/cart/discount", { code }),
1751
+ [mutate]
1752
+ );
1753
+ const removeDiscount = react.useCallback(
1754
+ // DELETE — postCartMutation interprets undefined body as DELETE.
1755
+ () => mutate("/api/cart/discount", void 0),
2186
1756
  [mutate]
2187
1757
  );
2188
- const removeDiscount = react.useCallback(async () => {
2189
- await mutate("/api/cart/discount", void 0);
2190
- }, [mutate]);
2191
1758
  const updateNote = react.useCallback(
2192
- async (note) => {
2193
- await mutate("/api/cart/update", { note });
2194
- },
1759
+ (note) => mutate("/api/cart/update", { note }),
2195
1760
  [mutate]
2196
1761
  );
2197
1762
  const clearCart = react.useCallback(async () => {
@@ -2327,13 +1892,93 @@ function NuMuProvider({
2327
1892
  defaultNumberFmt
2328
1893
  ]
2329
1894
  );
2330
- return /* @__PURE__ */ jsxRuntime.jsx(ShopContext.Provider, { value: store, children: /* @__PURE__ */ jsxRuntime.jsx(ThemeSettingsContext.Provider, { value: themeSettings, children: /* @__PURE__ */ jsxRuntime.jsx(CurrentTemplateContext.Provider, { value: currentTemplate, children: /* @__PURE__ */ jsxRuntime.jsx(PageContext.Provider, { value: pageValue, children: /* @__PURE__ */ jsxRuntime.jsx(LocalizationContext.Provider, { value: localization, children: /* @__PURE__ */ jsxRuntime.jsx(CartContext.Provider, { value: cartValue, children: /* @__PURE__ */ jsxRuntime.jsx(CustomerContext.Provider, { value: customerState, children: /* @__PURE__ */ jsxRuntime.jsx(CustomerActionsContext.Provider, { value: customerActions, children: /* @__PURE__ */ jsxRuntime.jsx(NavigationContext.Provider, { value: navigation ?? {}, children }) }) }) }) }) }) }) }) });
1895
+ const [currencyConfig, setCurrencyConfig] = react.useState(
1896
+ null
1897
+ );
1898
+ const [currencyLoading, setCurrencyLoading] = react.useState(true);
1899
+ const [currencySelected, setCurrencySelected] = react.useState("");
1900
+ react.useEffect(() => {
1901
+ if (typeof window === "undefined") return;
1902
+ let cancelled = false;
1903
+ void (async () => {
1904
+ try {
1905
+ const res = await fetch("/api/storefront/currencies", {
1906
+ credentials: "include",
1907
+ headers: { Accept: "application/json" }
1908
+ });
1909
+ if (!res.ok) throw new Error(`currencies: HTTP ${res.status}`);
1910
+ const body = await res.json();
1911
+ if (cancelled) return;
1912
+ setCurrencyConfig(body.data);
1913
+ const cookie = readCurrencyCookie();
1914
+ const valid = cookie && body.data.presentment?.includes(cookie) ? cookie : null;
1915
+ setCurrencySelected(
1916
+ valid || body.data.default_presentment || body.data.base
1917
+ );
1918
+ } catch {
1919
+ if (cancelled) return;
1920
+ setCurrencyConfig(null);
1921
+ setCurrencySelected(store.currency || "EGP");
1922
+ } finally {
1923
+ if (!cancelled) setCurrencyLoading(false);
1924
+ }
1925
+ })();
1926
+ return () => {
1927
+ cancelled = true;
1928
+ };
1929
+ }, [store.id, store.currency]);
1930
+ const currencyRates = react.useMemo(() => {
1931
+ const out = {};
1932
+ if (!currencyConfig) return out;
1933
+ for (const [k, v] of Object.entries(currencyConfig.rates)) {
1934
+ const n = Number.parseFloat(v);
1935
+ if (Number.isFinite(n)) out[k] = n;
1936
+ }
1937
+ return out;
1938
+ }, [currencyConfig]);
1939
+ const convertCurrency = react.useCallback(
1940
+ (cents, target) => {
1941
+ if (!currencyConfig) return cents;
1942
+ const to = target || currencySelected;
1943
+ if (!to || to === currencyConfig.base) return cents;
1944
+ const rate = currencyRates[to];
1945
+ if (!rate || !Number.isFinite(rate)) return cents;
1946
+ return Math.round(cents * rate);
1947
+ },
1948
+ [currencyConfig, currencyRates, currencySelected]
1949
+ );
1950
+ const setCurrency = react.useCallback((currency) => {
1951
+ setCurrencySelected(currency);
1952
+ writeCurrencyCookie(currency);
1953
+ }, []);
1954
+ const currencyValue = react.useMemo(
1955
+ () => ({
1956
+ base: currencyConfig?.base || store.currency || "EGP",
1957
+ selected: currencySelected || currencyConfig?.base || store.currency || "EGP",
1958
+ presentment: currencyConfig?.presentment || [store.currency || "EGP"],
1959
+ rates: currencyRates,
1960
+ autoConvert: Boolean(currencyConfig?.auto_convert),
1961
+ loading: currencyLoading,
1962
+ setSelected: setCurrency,
1963
+ convert: convertCurrency
1964
+ }),
1965
+ [
1966
+ currencyConfig,
1967
+ currencySelected,
1968
+ currencyRates,
1969
+ currencyLoading,
1970
+ store.currency,
1971
+ setCurrency,
1972
+ convertCurrency
1973
+ ]
1974
+ );
1975
+ return /* @__PURE__ */ jsxRuntime.jsx(chunk3C37JX2X_cjs.ShopContext.Provider, { value: store, children: /* @__PURE__ */ jsxRuntime.jsx(chunk3C37JX2X_cjs.ThemeSettingsContext.Provider, { value: themeSettings, children: /* @__PURE__ */ jsxRuntime.jsx(chunk3C37JX2X_cjs.CurrentTemplateContext.Provider, { value: currentTemplate, children: /* @__PURE__ */ jsxRuntime.jsx(chunk3C37JX2X_cjs.PageContext.Provider, { value: pageValue, children: /* @__PURE__ */ jsxRuntime.jsx(chunk3C37JX2X_cjs.LocalizationContext.Provider, { value: localization, children: /* @__PURE__ */ jsxRuntime.jsx(chunk3C37JX2X_cjs.CurrencyContext.Provider, { value: currencyValue, children: /* @__PURE__ */ jsxRuntime.jsx(chunk3C37JX2X_cjs.CartContext.Provider, { value: cartValue, children: /* @__PURE__ */ jsxRuntime.jsx(chunk3C37JX2X_cjs.CustomerContext.Provider, { value: customerState, children: /* @__PURE__ */ jsxRuntime.jsx(CustomerActionsContext.Provider, { value: customerActions, children: /* @__PURE__ */ jsxRuntime.jsx(chunk3C37JX2X_cjs.NavigationContext.Provider, { value: navigation ?? {}, children }) }) }) }) }) }) }) }) }) });
2331
1976
  }
2332
1977
  function ProductProvider({ product, children }) {
2333
- return /* @__PURE__ */ jsxRuntime.jsx(ProductContext.Provider, { value: product, children });
1978
+ return /* @__PURE__ */ jsxRuntime.jsx(chunk3C37JX2X_cjs.ProductContext.Provider, { value: product, children });
2334
1979
  }
2335
1980
  function CollectionProvider({ collection, children }) {
2336
- return /* @__PURE__ */ jsxRuntime.jsx(CollectionContext.Provider, { value: collection, children });
1981
+ return /* @__PURE__ */ jsxRuntime.jsx(chunk3C37JX2X_cjs.CollectionContext.Provider, { value: collection, children });
2337
1982
  }
2338
1983
 
2339
1984
  // src/utils/logoStyle.ts
@@ -2628,6 +2273,7 @@ var ThemeMountBridge = react.forwardRef(function ThemeMountBridge2({ ctx, mountE
2628
2273
  initialProducts: pageData.products,
2629
2274
  initialCollections: pageData.collections,
2630
2275
  currentTemplate: template,
2276
+ pageTemplate: ctx.page?.template,
2631
2277
  children: wrapEntityProviders(app, pageData)
2632
2278
  }
2633
2279
  );
@@ -2671,17 +2317,20 @@ function Money({
2671
2317
  className,
2672
2318
  as = "span"
2673
2319
  }) {
2674
- const { formatMoney } = useLocalization();
2675
- const shop = useShop();
2676
- const ccy = currency || shop?.currency;
2320
+ const { formatMoney } = chunk3C37JX2X_cjs.useLocalization();
2321
+ const shop = chunk3C37JX2X_cjs.useShop();
2322
+ const { selected, base, autoConvert, convert } = useCurrency();
2323
+ const shouldConvert = autoConvert && !currency && !!selected && selected !== base;
2324
+ const ccy = shouldConvert ? selected : currency || shop?.currency;
2325
+ const toDisplay = (major) => shouldConvert ? convert(Math.round(major * 100), selected) / 100 : major;
2677
2326
  const showCompare = compareAt != null && compareAt > amount;
2678
2327
  const children = [
2679
- /* @__PURE__ */ jsxRuntime.jsx("span", { children: formatMoney(amount, ccy) }, "amt")
2328
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: formatMoney(toDisplay(amount), ccy) }, "amt")
2680
2329
  ];
2681
2330
  if (showCompare) {
2682
2331
  children.push(" ");
2683
2332
  children.push(
2684
- /* @__PURE__ */ jsxRuntime.jsx("s", { style: { opacity: 0.6 }, children: formatMoney(compareAt, ccy) }, "cmp")
2333
+ /* @__PURE__ */ jsxRuntime.jsx("s", { style: { opacity: 0.6 }, children: formatMoney(toDisplay(compareAt), ccy) }, "cmp")
2685
2334
  );
2686
2335
  }
2687
2336
  return react.createElement(
@@ -2933,8 +2582,8 @@ function Logo({
2933
2582
  style,
2934
2583
  fallback
2935
2584
  }) {
2936
- const settings = useThemeSettings();
2937
- const shop = useShop();
2585
+ const settings = chunk3C37JX2X_cjs.useThemeSettings();
2586
+ const shop = chunk3C37JX2X_cjs.useShop();
2938
2587
  const g = settings?.global_settings ?? {};
2939
2588
  const url = str(src) || str(g.logo_url) || shop?.logo_url || "";
2940
2589
  const resolvedShape = shape || str(g.logo_shape) || "none";
@@ -2961,7 +2610,7 @@ function Logo({
2961
2610
  }
2962
2611
  var ABSOLUTE_URL = /^[a-z]+:|^\/\//i;
2963
2612
  function Link({ to, children, ...rest }) {
2964
- const shop = useShop();
2613
+ const shop = chunk3C37JX2X_cjs.useShop();
2965
2614
  const isAbsolute = ABSOLUTE_URL.test(to);
2966
2615
  let href = to;
2967
2616
  if (!isAbsolute && shop && !to.startsWith("/")) {
@@ -3417,11 +3066,29 @@ function walk(node) {
3417
3066
  }
3418
3067
  }
3419
3068
  function sanitizeHtmlServer(input) {
3420
- 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, "");
3421
- s = s.replace(/\s+on\w+\s*=\s*("[^"]*"|'[^']*'|[^\s>]+)/gi, "");
3069
+ if (!input) return "";
3070
+ let s = input;
3071
+ let prev;
3072
+ const DANGEROUS = [
3073
+ /<script\b[\s\S]*?<\/script\s*>/gi,
3074
+ /<style\b[\s\S]*?<\/style\s*>/gi,
3075
+ /<iframe\b[\s\S]*?<\/iframe\s*>/gi,
3076
+ /<object\b[\s\S]*?<\/object\s*>/gi,
3077
+ /<embed\b[\s\S]*?<\/embed\s*>/gi,
3078
+ /<\/?(?:script|style|iframe|object|embed|link|meta|form|input)\b[^>]*>/gi
3079
+ ];
3080
+ do {
3081
+ prev = s;
3082
+ for (const re of DANGEROUS) s = s.replace(re, "");
3083
+ } while (s !== prev);
3084
+ const HANDLER = /[\s/"'<]on\w+\s*=\s*("[^"]*"|'[^']*'|[^\s>]+)/gi;
3085
+ do {
3086
+ prev = s;
3087
+ s = s.replace(HANDLER, "");
3088
+ } while (s !== prev);
3422
3089
  s = s.replace(
3423
- /\s+(href|src)\s*=\s*("[^"]*"|'[^']*'|[^\s>]+)/gi,
3424
- (full, attr, val) => {
3090
+ /[\s/"'<](href|src)\s*=\s*("[^"]*"|'[^']*'|[^\s>]+)/gi,
3091
+ (full, _attr, val) => {
3425
3092
  const cleaned = String(val).replace(/^['"]|['"]$/g, "");
3426
3093
  return isSafeUrl(cleaned) ? full : "";
3427
3094
  }
@@ -3496,8 +3163,8 @@ function LocaleSwitcher({
3496
3163
  onSelect,
3497
3164
  render
3498
3165
  }) {
3499
- const shop = useShop();
3500
- const { locale } = useLocalization();
3166
+ const shop = chunk3C37JX2X_cjs.useShop();
3167
+ const { locale } = chunk3C37JX2X_cjs.useLocalization();
3501
3168
  const available = shop.available_locales || [];
3502
3169
  const list = available.length > 0 ? available : shop.default_language === "en" ? ["en"] : Array.from(/* @__PURE__ */ new Set([shop.default_language, "en"]));
3503
3170
  const labelFor = react.useCallback((code) => {
@@ -3766,97 +3433,6 @@ function Icon({
3766
3433
  );
3767
3434
  }
3768
3435
 
3769
- // src/utils/normalize.ts
3770
- var DEFAULT_THEME_ID = typeof process !== "undefined" && process.env?.NUMU_DEFAULT_THEME_ID || "modern";
3771
- function resolveThemeSettings(raw) {
3772
- const safeRaw = raw ?? {};
3773
- if (safeRaw.schema_version === 3) {
3774
- return safeRaw;
3775
- }
3776
- const themeBlock = safeRaw.theme || {};
3777
- const themeId = typeof themeBlock.base_theme === "string" && themeBlock.base_theme || DEFAULT_THEME_ID;
3778
- const globalSettings = {};
3779
- for (const key of [
3780
- "primary_color",
3781
- "secondary_color",
3782
- "font_family",
3783
- "logo_url"
3784
- ]) {
3785
- if (themeBlock[key] !== void 0) globalSettings[key] = themeBlock[key];
3786
- }
3787
- if (safeRaw.identity) globalSettings.identity = safeRaw.identity;
3788
- const sections = {};
3789
- const order = [];
3790
- const hero = safeRaw.hero;
3791
- if (hero) {
3792
- sections["hero_1"] = {
3793
- type: "hero",
3794
- settings: {
3795
- // Mirrors backend `normalize_legacy_to_v3` exactly — includes
3796
- // headline_ar so Arabic content survives the round-trip.
3797
- headline: hero.headline ?? "",
3798
- headline_ar: hero.headline_ar ?? "",
3799
- subtitle: hero.subtitle ?? "",
3800
- background_image: hero.hero_image_url ?? "",
3801
- cta_text: hero.cta_text ?? "",
3802
- cta_link: hero.cta_link ?? ""
3803
- }
3804
- };
3805
- order.push("hero_1");
3806
- }
3807
- const products = safeRaw.products;
3808
- if (products) {
3809
- sections["featured_1"] = {
3810
- type: "featured-products",
3811
- settings: products
3812
- };
3813
- order.push("featured_1");
3814
- }
3815
- const templates = {};
3816
- if (Object.keys(sections).length > 0) {
3817
- templates["home"] = { name: "Home", sections, order };
3818
- }
3819
- const sectionGroups = {
3820
- header: {
3821
- name: "Header Group",
3822
- sections: {
3823
- header_1: {
3824
- type: "header",
3825
- settings: safeRaw.header || {}
3826
- }
3827
- },
3828
- order: ["header_1"]
3829
- },
3830
- footer: {
3831
- name: "Footer Group",
3832
- sections: {
3833
- footer_1: {
3834
- type: "footer",
3835
- settings: safeRaw.footer || {}
3836
- }
3837
- },
3838
- order: ["footer_1"]
3839
- }
3840
- };
3841
- let externalTheme = null;
3842
- const ext = safeRaw.external_theme;
3843
- if (ext && typeof ext === "object" && typeof ext.bundle_url === "string" && ext.bundle_url) {
3844
- externalTheme = {
3845
- bundle_url: ext.bundle_url,
3846
- css_url: typeof ext.css_url === "string" ? ext.css_url : null,
3847
- mode: typeof ext.mode === "string" ? ext.mode : "production"
3848
- };
3849
- }
3850
- return {
3851
- schema_version: 3,
3852
- theme_id: themeId,
3853
- global_settings: globalSettings,
3854
- templates,
3855
- section_groups: sectionGroups,
3856
- external_theme: externalTheme
3857
- };
3858
- }
3859
-
3860
3436
  // src/utils/federation.ts
3861
3437
  function sdkSlot() {
3862
3438
  return /* @__PURE__ */ Symbol.for("@numueg/theme-sdk:singleton");
@@ -3993,9 +3569,9 @@ function resolveSettingsMap(settings, ctx) {
3993
3569
  return out;
3994
3570
  }
3995
3571
  function useResolvedSettings(instance) {
3996
- const product = react.useContext(ProductContext);
3997
- const collection = react.useContext(CollectionContext);
3998
- const store = react.useContext(ShopContext);
3572
+ const product = react.useContext(chunk3C37JX2X_cjs.ProductContext);
3573
+ const collection = react.useContext(chunk3C37JX2X_cjs.CollectionContext);
3574
+ const store = react.useContext(chunk3C37JX2X_cjs.ShopContext);
3999
3575
  const ctx = react.useMemo(
4000
3576
  () => ({ product, collection, store }),
4001
3577
  [product, collection, store]
@@ -4169,14 +3745,143 @@ function buildLocaleBundle(modules) {
4169
3745
  return bundle;
4170
3746
  }
4171
3747
 
3748
+ Object.defineProperty(exports, "resolveThemeSettings", {
3749
+ enumerable: true,
3750
+ get: function () { return chunkTBSNHHFH_cjs.resolveThemeSettings; }
3751
+ });
3752
+ Object.defineProperty(exports, "KNOWN_SETTING_TYPES", {
3753
+ enumerable: true,
3754
+ get: function () { return chunkX3J4GDI7_cjs.KNOWN_SETTING_TYPES; }
3755
+ });
3756
+ Object.defineProperty(exports, "KNOWN_TEMPLATES", {
3757
+ enumerable: true,
3758
+ get: function () { return chunkX3J4GDI7_cjs.KNOWN_TEMPLATES; }
3759
+ });
3760
+ Object.defineProperty(exports, "REQUIRED_TEMPLATES", {
3761
+ enumerable: true,
3762
+ get: function () { return chunkX3J4GDI7_cjs.REQUIRED_TEMPLATES; }
3763
+ });
3764
+ Object.defineProperty(exports, "SDK_VERSION", {
3765
+ enumerable: true,
3766
+ get: function () { return chunkX3J4GDI7_cjs.SDK_VERSION; }
3767
+ });
3768
+ Object.defineProperty(exports, "THEME_CONTRACT_VERSION", {
3769
+ enumerable: true,
3770
+ get: function () { return chunkX3J4GDI7_cjs.THEME_CONTRACT_VERSION; }
3771
+ });
3772
+ Object.defineProperty(exports, "mergeResults", {
3773
+ enumerable: true,
3774
+ get: function () { return chunkX3J4GDI7_cjs.mergeResults; }
3775
+ });
3776
+ Object.defineProperty(exports, "validateBuiltManifest", {
3777
+ enumerable: true,
3778
+ get: function () { return chunkX3J4GDI7_cjs.validateBuiltManifest; }
3779
+ });
3780
+ Object.defineProperty(exports, "validateManifest", {
3781
+ enumerable: true,
3782
+ get: function () { return chunkX3J4GDI7_cjs.validateManifest; }
3783
+ });
3784
+ Object.defineProperty(exports, "validateSectionSchema", {
3785
+ enumerable: true,
3786
+ get: function () { return chunkX3J4GDI7_cjs.validateSectionSchema; }
3787
+ });
3788
+ Object.defineProperty(exports, "validateSettingsAgainstSchema", {
3789
+ enumerable: true,
3790
+ get: function () { return chunkX3J4GDI7_cjs.validateSettingsAgainstSchema; }
3791
+ });
3792
+ Object.defineProperty(exports, "CartContext", {
3793
+ enumerable: true,
3794
+ get: function () { return chunk3C37JX2X_cjs.CartContext; }
3795
+ });
3796
+ Object.defineProperty(exports, "CollectionContext", {
3797
+ enumerable: true,
3798
+ get: function () { return chunk3C37JX2X_cjs.CollectionContext; }
3799
+ });
3800
+ Object.defineProperty(exports, "CurrencyContext", {
3801
+ enumerable: true,
3802
+ get: function () { return chunk3C37JX2X_cjs.CurrencyContext; }
3803
+ });
3804
+ Object.defineProperty(exports, "CustomerContext", {
3805
+ enumerable: true,
3806
+ get: function () { return chunk3C37JX2X_cjs.CustomerContext; }
3807
+ });
3808
+ Object.defineProperty(exports, "LocalizationContext", {
3809
+ enumerable: true,
3810
+ get: function () { return chunk3C37JX2X_cjs.LocalizationContext; }
3811
+ });
3812
+ Object.defineProperty(exports, "NavigationContext", {
3813
+ enumerable: true,
3814
+ get: function () { return chunk3C37JX2X_cjs.NavigationContext; }
3815
+ });
3816
+ Object.defineProperty(exports, "PageContext", {
3817
+ enumerable: true,
3818
+ get: function () { return chunk3C37JX2X_cjs.PageContext; }
3819
+ });
3820
+ Object.defineProperty(exports, "ProductContext", {
3821
+ enumerable: true,
3822
+ get: function () { return chunk3C37JX2X_cjs.ProductContext; }
3823
+ });
3824
+ Object.defineProperty(exports, "ShopContext", {
3825
+ enumerable: true,
3826
+ get: function () { return chunk3C37JX2X_cjs.ShopContext; }
3827
+ });
3828
+ Object.defineProperty(exports, "ThemeSettingsContext", {
3829
+ enumerable: true,
3830
+ get: function () { return chunk3C37JX2X_cjs.ThemeSettingsContext; }
3831
+ });
3832
+ Object.defineProperty(exports, "useCollections", {
3833
+ enumerable: true,
3834
+ get: function () { return chunk3C37JX2X_cjs.useCollections; }
3835
+ });
3836
+ Object.defineProperty(exports, "useCustomer", {
3837
+ enumerable: true,
3838
+ get: function () { return chunk3C37JX2X_cjs.useCustomer; }
3839
+ });
3840
+ Object.defineProperty(exports, "useDirection", {
3841
+ enumerable: true,
3842
+ get: function () { return chunk3C37JX2X_cjs.useDirection; }
3843
+ });
3844
+ Object.defineProperty(exports, "useFieldTranslation", {
3845
+ enumerable: true,
3846
+ get: function () { return chunk3C37JX2X_cjs.useFieldTranslation; }
3847
+ });
3848
+ Object.defineProperty(exports, "useLocale", {
3849
+ enumerable: true,
3850
+ get: function () { return chunk3C37JX2X_cjs.useLocale; }
3851
+ });
3852
+ Object.defineProperty(exports, "useLocalization", {
3853
+ enumerable: true,
3854
+ get: function () { return chunk3C37JX2X_cjs.useLocalization; }
3855
+ });
3856
+ Object.defineProperty(exports, "useNumberFormat", {
3857
+ enumerable: true,
3858
+ get: function () { return chunk3C37JX2X_cjs.useNumberFormat; }
3859
+ });
3860
+ Object.defineProperty(exports, "usePage", {
3861
+ enumerable: true,
3862
+ get: function () { return chunk3C37JX2X_cjs.usePage; }
3863
+ });
3864
+ Object.defineProperty(exports, "useProducts", {
3865
+ enumerable: true,
3866
+ get: function () { return chunk3C37JX2X_cjs.useProducts; }
3867
+ });
3868
+ Object.defineProperty(exports, "useShop", {
3869
+ enumerable: true,
3870
+ get: function () { return chunk3C37JX2X_cjs.useShop; }
3871
+ });
3872
+ Object.defineProperty(exports, "useThemeSettings", {
3873
+ enumerable: true,
3874
+ get: function () { return chunk3C37JX2X_cjs.useThemeSettings; }
3875
+ });
3876
+ Object.defineProperty(exports, "useTranslation", {
3877
+ enumerable: true,
3878
+ get: function () { return chunk3C37JX2X_cjs.useTranslation; }
3879
+ });
4172
3880
  exports.AddToCartButton = AddToCartButton;
4173
3881
  exports.Block = Block;
4174
- exports.CartContext = CartContext;
4175
3882
  exports.CollectionCard = CollectionCard;
4176
- exports.CollectionContext = CollectionContext;
4177
3883
  exports.CollectionProvider = CollectionProvider;
4178
3884
  exports.CurrencySwitcher = CurrencySwitcher;
4179
- exports.CustomerContext = CustomerContext;
4180
3885
  exports.EditableImage = EditableImage;
4181
3886
  exports.EditableText = EditableText;
4182
3887
  exports.Form = Form;
@@ -4185,30 +3890,19 @@ exports.ICON_NAMES = ICON_NAMES;
4185
3890
  exports.Icon = Icon;
4186
3891
  exports.IconMap = IconMap;
4187
3892
  exports.Image = Image;
4188
- exports.KNOWN_SETTING_TYPES = KNOWN_SETTING_TYPES;
4189
- exports.KNOWN_TEMPLATES = KNOWN_TEMPLATES;
4190
3893
  exports.LOGO_SHAPE_OPTIONS = LOGO_SHAPE_OPTIONS;
4191
3894
  exports.LOGO_SIZE_OPTIONS = LOGO_SIZE_OPTIONS;
4192
3895
  exports.Link = Link;
4193
3896
  exports.LocaleSwitcher = LocaleSwitcher;
4194
- exports.LocalizationContext = LocalizationContext;
4195
3897
  exports.Logo = Logo;
4196
3898
  exports.MAX_BLOCK_DEPTH = MAX_BLOCK_DEPTH;
4197
3899
  exports.Money = Money;
4198
- exports.NavigationContext = NavigationContext;
4199
3900
  exports.NuMuProvider = NuMuProvider;
4200
- exports.PageContext = PageContext;
4201
3901
  exports.ProductCard = ProductCard;
4202
- exports.ProductContext = ProductContext;
4203
3902
  exports.ProductProvider = ProductProvider;
4204
- exports.REQUIRED_TEMPLATES = REQUIRED_TEMPLATES;
4205
3903
  exports.RichText = RichText;
4206
- exports.SDK_VERSION = SDK_VERSION;
4207
3904
  exports.Section = Section;
4208
3905
  exports.SectionContext = SectionContext;
4209
- exports.ShopContext = ShopContext;
4210
- exports.THEME_CONTRACT_VERSION = THEME_CONTRACT_VERSION;
4211
- exports.ThemeSettingsContext = ThemeSettingsContext;
4212
3906
  exports.applyGlobalStyleTokens = applyGlobalStyleTokens;
4213
3907
  exports.applyImageTransform = applyImageTransform;
4214
3908
  exports.asImageTransform = asImageTransform;
@@ -4236,7 +3930,6 @@ exports.isDynamicSource = isDynamicSource;
4236
3930
  exports.isSdkAvailable = isSdkAvailable;
4237
3931
  exports.logoImgStyle = logoImgStyle;
4238
3932
  exports.logoStyleTokens = logoStyleTokens;
4239
- exports.mergeResults = mergeResults;
4240
3933
  exports.mountTheme = mountTheme;
4241
3934
  exports.pickTranslations = pickTranslations;
4242
3935
  exports.registerReactSingleton = registerReactSingleton;
@@ -4246,51 +3939,36 @@ exports.resolveFontStack = resolveFontStack;
4246
3939
  exports.resolveSettingsMap = resolveSettingsMap;
4247
3940
  exports.resolveSizeChart = resolveSizeChart;
4248
3941
  exports.resolveSourcePath = resolveSourcePath;
4249
- exports.resolveThemeSettings = resolveThemeSettings;
4250
3942
  exports.sanitizeHtml = sanitizeHtml;
4251
3943
  exports.useAnalytics = useAnalytics;
4252
3944
  exports.useApp = useApp;
3945
+ exports.useCachedResource = useCachedResource;
4253
3946
  exports.useCart = useCart;
4254
3947
  exports.useCheckout = useCheckout;
4255
3948
  exports.useCollection = useCollection;
4256
3949
  exports.useCollectionOptional = useCollectionOptional;
4257
- exports.useCollections = useCollections;
4258
3950
  exports.useCurrency = useCurrency;
4259
3951
  exports.useCurrentTemplate = useCurrentTemplate;
4260
- exports.useCustomer = useCustomer;
4261
3952
  exports.useCustomerActions = useCustomerActions;
4262
3953
  exports.useCustomerAddresses = useCustomerAddresses;
4263
- exports.useDirection = useDirection;
4264
- exports.useFieldTranslation = useFieldTranslation;
4265
3954
  exports.useGiftCardBalance = useGiftCardBalance;
4266
3955
  exports.useImage = useImage;
4267
- exports.useLocale = useLocale;
4268
- exports.useLocalization = useLocalization;
4269
3956
  exports.useMoney = useMoney;
4270
3957
  exports.useNavigation = useNavigation;
4271
- exports.useNumberFormat = useNumberFormat;
4272
3958
  exports.useOrder = useOrder;
4273
3959
  exports.useOrders = useOrders;
4274
- exports.usePage = usePage;
4275
3960
  exports.useProduct = useProduct;
4276
3961
  exports.useProductOptional = useProductOptional;
4277
3962
  exports.useProductSizeChart = useProductSizeChart;
4278
- exports.useProducts = useProducts;
4279
3963
  exports.useRelatedProducts = useRelatedProducts;
4280
3964
  exports.useReorder = useReorder;
4281
3965
  exports.useResolvedSettings = useResolvedSettings;
4282
3966
  exports.useSearch = useSearch;
4283
3967
  exports.useSection = useSection;
3968
+ exports.useSectionGroup = useSectionGroup;
4284
3969
  exports.useSectionOptional = useSectionOptional;
4285
3970
  exports.useShippingRates = useShippingRates;
4286
- exports.useShop = useShop;
4287
- exports.useThemeSettings = useThemeSettings;
4288
- exports.useTranslation = useTranslation;
4289
3971
  exports.useVariantSelection = useVariantSelection;
4290
3972
  exports.useWishlist = useWishlist;
4291
- exports.validateBuiltManifest = validateBuiltManifest;
4292
- exports.validateManifest = validateManifest;
4293
- exports.validateSectionSchema = validateSectionSchema;
4294
- exports.validateSettingsAgainstSchema = validateSettingsAgainstSchema;
4295
3973
  //# sourceMappingURL=index.cjs.map
4296
3974
  //# sourceMappingURL=index.cjs.map