@numueg/theme-sdk 0.6.1 → 0.10.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-4TMO5Y5W.mjs +183 -0
  3. package/dist/chunk-4TMO5Y5W.mjs.map +1 -0
  4. package/dist/chunk-EB2ESBLD.mjs +397 -0
  5. package/dist/chunk-EB2ESBLD.mjs.map +1 -0
  6. package/dist/chunk-IBNAZRUT.cjs +207 -0
  7. package/dist/chunk-IBNAZRUT.cjs.map +1 -0
  8. package/dist/chunk-KWODVI7F.cjs +408 -0
  9. package/dist/chunk-KWODVI7F.cjs.map +1 -0
  10. package/dist/chunk-TBSNHHFH.cjs +96 -0
  11. package/dist/chunk-TBSNHHFH.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 +693 -968
  17. package/dist/index.cjs.map +1 -1
  18. package/dist/index.d.mts +286 -94
  19. package/dist/index.d.ts +286 -94
  20. package/dist/index.mjs +531 -911
  21. package/dist/index.mjs.map +1 -1
  22. package/dist/{mount-BGumg1JM.d.mts → mount-HbnOAYZ-.d.ts} +90 -14
  23. package/dist/{mount-Bt-4ken5.d.ts → mount-eAhGgGtZ.d.mts} +90 -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.mjs CHANGED
@@ -1,494 +1,13 @@
1
- import { createContext, forwardRef, useState, useImperativeHandle, useEffect, useCallback, useMemo, useRef, useContext, StrictMode, createElement, Component } from 'react';
1
+ export { resolveThemeSettings } from './chunk-XF2FGIVS.mjs';
2
+ export { KNOWN_SETTING_TYPES, KNOWN_TEMPLATES, REQUIRED_TEMPLATES, SDK_VERSION, THEME_CONTRACT_VERSION, mergeResults, validateBuiltManifest, validateManifest, validateSectionSchema, validateSettingsAgainstSchema } from './chunk-EB2ESBLD.mjs';
3
+ import { ShopContext, ThemeSettingsContext, CurrentTemplateContext, PageContext, LocalizationContext, CurrencyContext, CartContext, CustomerContext, NavigationContext, CollectionContext, ProductContext, useShop, useLocalization, useCustomer, useThemeSettings } from './chunk-4TMO5Y5W.mjs';
4
+ export { CartContext, CollectionContext, CurrencyContext, CustomerContext, LocalizationContext, NavigationContext, PageContext, ProductContext, ShopContext, ThemeSettingsContext, useCollections, useCustomer, useDirection, useFieldTranslation, useLocale, useLocalization, useNumberFormat, usePage, useProducts, useShop, useThemeSettings, useTranslation } from './chunk-4TMO5Y5W.mjs';
5
+ import { createContext, forwardRef, useState, useImperativeHandle, useEffect, useCallback, useMemo, useRef, useContext, useSyncExternalStore, StrictMode, createElement, Component } from 'react';
2
6
  import { hydrateRoot, createRoot } from 'react-dom/client';
3
7
  import { jsx, Fragment, jsxs } from 'react/jsx-runtime';
4
8
 
5
9
  // src/types/theme.ts
6
10
  var MAX_BLOCK_DEPTH = 5;
7
-
8
- // src/validation/index.ts
9
- var THEME_CONTRACT_VERSION = 1;
10
- var SDK_VERSION = "0.6.1" ;
11
- var REQUIRED_TEMPLATES = [
12
- "home",
13
- "product",
14
- "collection",
15
- "cart",
16
- "page",
17
- "search",
18
- "404"
19
- ];
20
- var KNOWN_TEMPLATES = /* @__PURE__ */ new Set([
21
- ...REQUIRED_TEMPLATES,
22
- "blog",
23
- "article",
24
- "policies",
25
- "password",
26
- "account",
27
- "checkout"
28
- ]);
29
- var KNOWN_SETTING_TYPES = /* @__PURE__ */ new Set([
30
- "text",
31
- "textarea",
32
- "richtext",
33
- "number",
34
- "range",
35
- "color",
36
- "checkbox",
37
- "select",
38
- "radio",
39
- "font",
40
- "image_picker",
41
- "url",
42
- "product",
43
- "product_list",
44
- "collection",
45
- "collection_list",
46
- "header",
47
- "paragraph",
48
- "html",
49
- "date",
50
- "time",
51
- "video_picker",
52
- "color_scheme",
53
- "page_picker",
54
- "blog_picker",
55
- "link_list_picker",
56
- "variant_picker",
57
- "file_upload",
58
- "icon_picker",
59
- "icon"
60
- ]);
61
- var PRESENTATIONAL_SETTING_TYPES = /* @__PURE__ */ new Set([
62
- "header",
63
- "paragraph",
64
- "html"
65
- ]);
66
- var SECTION_TYPE_RE = /^[a-z][a-z0-9_-]*$/;
67
- var THEME_ID_RE = /^[a-z0-9][a-z0-9_-]*[a-z0-9]$/;
68
- var SEMVER_RE = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z-.]+)?(?:\+[0-9A-Za-z-.]+)?$/;
69
- function isObject(v) {
70
- return typeof v === "object" && v !== null && !Array.isArray(v);
71
- }
72
- function isNonEmptyString(v) {
73
- return typeof v === "string" && v.trim().length > 0;
74
- }
75
- var IssueBag = class {
76
- constructor() {
77
- this.issues = [];
78
- }
79
- err(code, message, path) {
80
- this.issues.push({ level: "error", code, message, path });
81
- }
82
- warn(code, message, path) {
83
- this.issues.push({ level: "warning", code, message, path });
84
- }
85
- result() {
86
- return {
87
- valid: !this.issues.some((i) => i.level === "error"),
88
- issues: this.issues
89
- };
90
- }
91
- };
92
- function collectPresetSectionTypes(manifest) {
93
- const types = /* @__PURE__ */ new Set();
94
- const presets = manifest.presets;
95
- if (!isObject(presets)) return types;
96
- const buckets = [presets.templates, presets.section_groups];
97
- for (const bucket of buckets) {
98
- if (!isObject(bucket)) continue;
99
- for (const entry of Object.values(bucket)) {
100
- if (!isObject(entry)) continue;
101
- const sections = entry.sections;
102
- const instances = Array.isArray(sections) ? sections : isObject(sections) ? Object.values(sections) : [];
103
- for (const inst of instances) {
104
- if (isObject(inst) && isNonEmptyString(inst.type)) types.add(inst.type);
105
- }
106
- }
107
- }
108
- return types;
109
- }
110
- function validateSettingInto(bag, setting, pathPrefix, seenIds) {
111
- if (!isObject(setting)) {
112
- bag.err("schema.setting.invalid", "Setting must be an object.", pathPrefix);
113
- return;
114
- }
115
- const type = setting.type;
116
- if (!isNonEmptyString(type)) {
117
- bag.err("schema.setting.type.missing", "Setting is missing a `type`.", pathPrefix);
118
- } else if (!KNOWN_SETTING_TYPES.has(type)) {
119
- bag.warn(
120
- "schema.setting.type.unknown",
121
- `Unknown setting type "${type}" \u2014 the customizer will fall back to a text input.`,
122
- pathPrefix
123
- );
124
- }
125
- const presentational = isNonEmptyString(type) && PRESENTATIONAL_SETTING_TYPES.has(type);
126
- if (!presentational) {
127
- if (!isNonEmptyString(setting.id)) {
128
- bag.err("schema.setting.id.missing", "Setting is missing an `id`.", pathPrefix);
129
- } else {
130
- if (seenIds.has(setting.id)) {
131
- bag.err(
132
- "schema.setting.id.duplicate",
133
- `Duplicate setting id "${setting.id}".`,
134
- pathPrefix
135
- );
136
- }
137
- seenIds.add(setting.id);
138
- }
139
- if (!isNonEmptyString(setting.label)) {
140
- bag.warn("schema.setting.label.missing", "Setting has no `label`.", pathPrefix);
141
- }
142
- }
143
- if (type === "select" || type === "radio") {
144
- const options = setting.options;
145
- if (!Array.isArray(options) || options.length === 0) {
146
- bag.err(
147
- "schema.setting.options.missing",
148
- `"${type}" setting "${String(setting.id)}" must declare a non-empty \`options\` array.`,
149
- pathPrefix
150
- );
151
- }
152
- }
153
- if (type === "range") {
154
- if (typeof setting.min !== "number" || typeof setting.max !== "number") {
155
- bag.err(
156
- "schema.setting.range.bounds",
157
- `"range" setting "${String(setting.id)}" must declare numeric \`min\` and \`max\`.`,
158
- pathPrefix
159
- );
160
- } else if (setting.min >= setting.max) {
161
- bag.err(
162
- "schema.setting.range.order",
163
- `"range" setting "${String(setting.id)}" has min >= max.`,
164
- pathPrefix
165
- );
166
- }
167
- }
168
- }
169
- function validateSectionSchema(schema, opts = {}) {
170
- const bag = new IssueBag();
171
- const where = opts.filenameType ? `${opts.filenameType}.json` : "section schema";
172
- if (!isObject(schema)) {
173
- bag.err("schema.invalid", "Section schema must be a JSON object.", where);
174
- return bag.result();
175
- }
176
- const type = schema.type;
177
- if (!isNonEmptyString(type)) {
178
- bag.err("schema.type.missing", "Section schema is missing a `type`.", where);
179
- } else {
180
- if (!SECTION_TYPE_RE.test(type)) {
181
- bag.err(
182
- "schema.type.format",
183
- `Section type "${type}" must match ${SECTION_TYPE_RE} (lowercase, start with a letter).`,
184
- where
185
- );
186
- }
187
- if (opts.filenameType && type !== opts.filenameType) {
188
- bag.err(
189
- "schema.type.filename_mismatch",
190
- `Schema type "${type}" must equal its filename "${opts.filenameType}" (component-filename = schema-type convention).`,
191
- where
192
- );
193
- }
194
- }
195
- if (!isNonEmptyString(schema.name)) {
196
- bag.err("schema.name.missing", "Section schema is missing a `name`.", where);
197
- }
198
- if (schema.settings === void 0) {
199
- bag.warn("schema.settings.missing", "Section schema has no `settings`.", where);
200
- } else if (!Array.isArray(schema.settings)) {
201
- bag.err("schema.settings.invalid", "`settings` must be an array.", where);
202
- } else {
203
- const seen = /* @__PURE__ */ new Set();
204
- schema.settings.forEach(
205
- (s, i) => validateSettingInto(bag, s, `${where}.settings[${i}]`, seen)
206
- );
207
- }
208
- if (schema.blocks !== void 0) {
209
- if (!Array.isArray(schema.blocks)) {
210
- bag.err("schema.blocks.invalid", "`blocks` must be an array.", where);
211
- } else {
212
- schema.blocks.forEach((b, i) => {
213
- if (!isObject(b) || !isNonEmptyString(b.type)) {
214
- bag.err("schema.block.type.missing", `Block[${i}] is missing a \`type\`.`, where);
215
- return;
216
- }
217
- if (!SECTION_TYPE_RE.test(b.type)) {
218
- bag.err(
219
- "schema.block.type.format",
220
- `Block type "${b.type}" must match ${SECTION_TYPE_RE}.`,
221
- where
222
- );
223
- }
224
- if (Array.isArray(b.settings)) {
225
- const seen = /* @__PURE__ */ new Set();
226
- b.settings.forEach(
227
- (s, j) => validateSettingInto(bag, s, `${where}.blocks[${i}].settings[${j}]`, seen)
228
- );
229
- }
230
- });
231
- }
232
- }
233
- return bag.result();
234
- }
235
- function validateSettingsAgainstSchema(settings, schema, pathPrefix = "settings") {
236
- const bag = new IssueBag();
237
- if (!isObject(settings)) {
238
- bag.err("instance.settings.invalid", "Section settings must be an object.", pathPrefix);
239
- return bag.result();
240
- }
241
- const defs = Array.isArray(schema.settings) ? schema.settings : [];
242
- const byId = /* @__PURE__ */ new Map();
243
- for (const d of defs) if (isNonEmptyString(d?.id)) byId.set(d.id, d);
244
- for (const [key, value] of Object.entries(settings)) {
245
- const def = byId.get(key);
246
- if (!def) {
247
- bag.warn(
248
- "instance.setting.unknown",
249
- `Setting "${key}" is not declared in the "${String(schema.type)}" schema.`,
250
- `${pathPrefix}.${key}`
251
- );
252
- continue;
253
- }
254
- if (value === null || value === void 0) continue;
255
- const p = `${pathPrefix}.${key}`;
256
- if ((def.type === "select" || def.type === "radio") && Array.isArray(def.options)) {
257
- const allowed = def.options.map((o) => o.value);
258
- if (typeof value === "string" && !allowed.includes(value)) {
259
- bag.err(
260
- "instance.setting.option.invalid",
261
- `"${key}" = "${value}" is not one of: ${allowed.join(", ")}.`,
262
- p
263
- );
264
- }
265
- }
266
- if (def.type === "range" && typeof value === "number") {
267
- if (typeof def.min === "number" && value < def.min) {
268
- bag.err("instance.setting.range.under", `"${key}" = ${value} is below min ${def.min}.`, p);
269
- }
270
- if (typeof def.max === "number" && value > def.max) {
271
- bag.err("instance.setting.range.over", `"${key}" = ${value} is above max ${def.max}.`, p);
272
- }
273
- }
274
- if (def.type === "checkbox" && typeof value !== "boolean") {
275
- bag.warn("instance.setting.type.mismatch", `"${key}" should be a boolean.`, p);
276
- }
277
- if ((def.type === "number" || def.type === "range") && typeof value !== "number") {
278
- bag.warn("instance.setting.type.mismatch", `"${key}" should be a number.`, p);
279
- }
280
- }
281
- return bag.result();
282
- }
283
- function validateManifestCore(bag, m) {
284
- if (!isNonEmptyString(m.id)) {
285
- bag.err("manifest.id.missing", "theme.json is missing `id`.", "id");
286
- } else if (!THEME_ID_RE.test(m.id)) {
287
- bag.err(
288
- "manifest.id.format",
289
- `Theme id "${m.id}" must be lowercase alphanumeric with dashes/underscores (no leading/trailing separator).`,
290
- "id"
291
- );
292
- }
293
- const name = m.name;
294
- const nameOk = isNonEmptyString(name) || isObject(name) && Object.values(name).some((v) => isNonEmptyString(v));
295
- if (!nameOk) {
296
- bag.err("manifest.name.missing", "theme.json is missing a non-empty `name`.", "name");
297
- }
298
- if (!isNonEmptyString(m.version)) {
299
- bag.err("manifest.version.missing", "theme.json is missing `version`.", "version");
300
- } else if (!SEMVER_RE.test(m.version)) {
301
- bag.err("manifest.version.invalid", `Version "${m.version}" is not valid semver.`, "version");
302
- }
303
- if (!isNonEmptyString(m.author)) {
304
- bag.err("manifest.author.missing", "theme.json is missing `author`.", "author");
305
- }
306
- if (m.min_sdk_version !== void 0 && !SEMVER_RE.test(String(m.min_sdk_version))) {
307
- bag.warn("manifest.min_sdk_version.invalid", "`min_sdk_version` is not valid semver.", "min_sdk_version");
308
- }
309
- }
310
- function validateManifest(manifest, ctx = {}) {
311
- const bag = new IssueBag();
312
- if (!isObject(manifest)) {
313
- bag.err("manifest.invalid", "theme.json must be a JSON object.", "theme.json");
314
- return bag.result();
315
- }
316
- validateManifestCore(bag, manifest);
317
- const presets = manifest.presets;
318
- if (!isObject(presets) || Object.keys(presets).length === 0) {
319
- bag.warn("manifest.presets.empty", "theme.json has no presets \u2014 merchants start with an empty page.", "presets");
320
- } else {
321
- if (ctx.sectionTypes) {
322
- for (const type of collectPresetSectionTypes(manifest)) {
323
- if (!ctx.sectionTypes.has(type)) {
324
- bag.err(
325
- "manifest.preset.unknown_section",
326
- `Preset references section type "${type}" with no schemas/sections/${type}.json.`,
327
- "presets"
328
- );
329
- }
330
- }
331
- }
332
- const templates = isObject(presets.templates) ? presets.templates : {};
333
- for (const t of REQUIRED_TEMPLATES) {
334
- if (!(t in templates)) {
335
- bag.warn(
336
- "manifest.template.missing",
337
- `No preset for required template "${t}" \u2014 the storefront will fall back to its built-in.`,
338
- `presets.templates.${t}`
339
- );
340
- }
341
- }
342
- }
343
- return bag.result();
344
- }
345
- function validateBuiltManifest(builtManifest, importMap, opts = {}) {
346
- const bag = new IssueBag();
347
- if (!isObject(builtManifest)) {
348
- bag.err("built.invalid", "manifest.json must be a JSON object.", "manifest.json");
349
- return bag.result();
350
- }
351
- validateManifestCore(bag, builtManifest);
352
- const schemas = builtManifest.section_schemas;
353
- const shipped = new Set(isObject(schemas) ? Object.keys(schemas) : []);
354
- if (!isObject(schemas)) {
355
- bag.warn("built.section_schemas.missing", "manifest.json has no `section_schemas`.", "section_schemas");
356
- }
357
- for (const type of collectPresetSectionTypes(builtManifest)) {
358
- if (!shipped.has(type)) {
359
- bag.err(
360
- "built.preset.unknown_section",
361
- `Preset references section type "${type}" not present in section_schemas.`,
362
- "presets"
363
- );
364
- }
365
- }
366
- if (isObject(schemas)) {
367
- for (const [type, schema] of Object.entries(schemas)) {
368
- const r = validateSectionSchema(schema, { filenameType: type });
369
- for (const issue of r.issues) bag.issues.push(issue);
370
- }
371
- }
372
- if (!isNonEmptyString(builtManifest.plugin_version)) {
373
- bag.warn("built.plugin_version.missing", "manifest.json has no `plugin_version`.", "plugin_version");
374
- }
375
- if (importMap !== void 0) {
376
- if (!isObject(importMap)) {
377
- bag.err("importmap.invalid", "import-map.json must be a JSON object.", "import-map.json");
378
- } else {
379
- const cv = importMap.contract_version;
380
- if (typeof cv !== "number") {
381
- bag.warn(
382
- "importmap.contract_version.missing",
383
- "import-map.json has no numeric `contract_version` \u2014 built by an older plugin.",
384
- "contract_version"
385
- );
386
- } else if (typeof opts.hostContractVersion === "number" && cv > opts.hostContractVersion) {
387
- bag.err(
388
- "importmap.contract_version.incompatible",
389
- `Theme built for contract v${cv}; this platform supports up to v${opts.hostContractVersion}.`,
390
- "contract_version"
391
- );
392
- }
393
- }
394
- }
395
- return bag.result();
396
- }
397
- function mergeResults(...results) {
398
- const issues = results.flatMap((r) => r.issues);
399
- return { valid: !issues.some((i) => i.level === "error"), issues };
400
- }
401
- var ShopContext = createContext(null);
402
- var ProductContext = createContext(null);
403
- var CollectionContext = createContext(null);
404
- var CartContext = createContext(null);
405
- var CustomerContext = createContext(null);
406
- var ThemeSettingsContext = createContext(null);
407
- var LocalizationContext = createContext(null);
408
- var PageContext = createContext(null);
409
- var CurrentTemplateContext = createContext("home");
410
- var NavigationContext = createContext(
411
- {}
412
- );
413
- function useLocalization() {
414
- const ctx = useContext(LocalizationContext);
415
- if (!ctx) throw new Error("useLocalization must be used within NuMuProvider");
416
- return ctx;
417
- }
418
- function useDirection() {
419
- const { direction } = useLocalization();
420
- return direction;
421
- }
422
- function useLocale() {
423
- const { locale } = useLocalization();
424
- return locale;
425
- }
426
- function useTranslation() {
427
- const { translations, locale } = useLocalization();
428
- return {
429
- t: (key, fallback) => translations[key] || fallback || key,
430
- locale
431
- };
432
- }
433
- function useFieldTranslation(entity, field) {
434
- const { locale } = useLocalization();
435
- if (!entity) return void 0;
436
- const translated = entity.attributes && typeof entity.attributes === "object" ? entity.attributes[`${field}_${locale}`] : void 0;
437
- if (typeof translated === "string" && translated) return translated;
438
- const base = entity[field];
439
- if (typeof base === "string") return base;
440
- if (base == null) return void 0;
441
- return String(base);
442
- }
443
- function useNumberFormat() {
444
- const { formatNumber } = useLocalization();
445
- return formatNumber;
446
- }
447
-
448
- // src/hooks/useShop.ts
449
- var DEFAULT_PROTOCOL = typeof window !== "undefined" ? window.location.protocol.replace(":", "") : "https";
450
- function resolveDomain(store) {
451
- if (store.domain) return store.domain;
452
- if (store.subdomain) {
453
- const platform = typeof window !== "undefined" && window.__NUMU_PLATFORM_DOMAIN || "numueg.app";
454
- return `${store.subdomain}.${platform}`;
455
- }
456
- return store.slug;
457
- }
458
- function useShop() {
459
- const ctx = useContext(ShopContext);
460
- if (!ctx) throw new Error("useShop must be used within NuMuProvider");
461
- const locale = useLocale();
462
- return useMemo(() => {
463
- const domain = resolveDomain(ctx);
464
- const settings = ctx.settings;
465
- const localePrefixEnabled = Boolean(
466
- settings && settings.locale_url_prefix_enabled
467
- );
468
- const defaultLocale = ctx.default_language || "en";
469
- const prefixedLocales = settings?.locale_url_prefix_locales || null;
470
- const shouldPrefix = (l) => {
471
- if (!localePrefixEnabled) return false;
472
- if (prefixedLocales) return prefixedLocales.includes(l);
473
- return l !== defaultLocale;
474
- };
475
- const formatUrl = (path) => {
476
- if (!path) return `${DEFAULT_PROTOCOL}://${domain}/`;
477
- if (/^https?:\/\//i.test(path)) return path;
478
- if (path.startsWith("//")) return `${DEFAULT_PROTOCOL}:${path}`;
479
- let normalized = path.startsWith("/") ? path : `/${path}`;
480
- if (locale && shouldPrefix(locale)) {
481
- const prefix = `/${locale}/`;
482
- const root = `/${locale}`;
483
- if (normalized !== root && !normalized.startsWith(prefix)) {
484
- normalized = `${root}${normalized}`;
485
- }
486
- }
487
- return `${DEFAULT_PROTOCOL}://${domain}${normalized}`;
488
- };
489
- return { ...ctx, domain, formatUrl };
490
- }, [ctx, locale]);
491
- }
492
11
  function useProduct() {
493
12
  const ctx = useContext(ProductContext);
494
13
  if (!ctx) throw new Error("useProduct must be used within ProductProvider");
@@ -510,20 +29,9 @@ function useCart() {
510
29
  if (!ctx) throw new Error("useCart must be used within NuMuProvider");
511
30
  return ctx;
512
31
  }
513
- function useCustomer() {
514
- return useContext(CustomerContext);
515
- }
516
- function useThemeSettings() {
517
- const ctx = useContext(ThemeSettingsContext);
518
- if (!ctx) throw new Error("useThemeSettings must be used within NuMuProvider");
519
- return ctx;
520
- }
521
32
  function useCurrentTemplate() {
522
33
  return useContext(CurrentTemplateContext);
523
34
  }
524
- function usePage() {
525
- return useContext(PageContext);
526
- }
527
35
  var SectionContext = createContext(null);
528
36
  function useSection() {
529
37
  const ctx = useContext(SectionContext);
@@ -533,13 +41,57 @@ function useSection() {
533
41
  function useSectionOptional() {
534
42
  return useContext(SectionContext);
535
43
  }
44
+ var EMPTY = Object.freeze([]);
45
+ function useSectionGroup(group) {
46
+ const settings = useContext(ThemeSettingsContext);
47
+ return useMemo(() => {
48
+ const grp = settings?.section_groups?.[group];
49
+ if (!grp || !Array.isArray(grp.order) || grp.order.length === 0) {
50
+ return EMPTY;
51
+ }
52
+ const sections = grp.sections ?? {};
53
+ const out = [];
54
+ for (const id of grp.order) {
55
+ const instance = sections[id];
56
+ if (!instance) continue;
57
+ out.push({ id, ...instance });
58
+ }
59
+ return out;
60
+ }, [settings, group]);
61
+ }
62
+ function useCurrency() {
63
+ const ctx = useContext(CurrencyContext);
64
+ const shop = useShop();
65
+ const fallback = useMemo(() => {
66
+ const ccy = shop?.currency || "EGP";
67
+ return {
68
+ base: ccy,
69
+ selected: ccy,
70
+ presentment: [ccy],
71
+ rates: {},
72
+ autoConvert: false,
73
+ loading: false,
74
+ setSelected: () => {
75
+ },
76
+ convert: (cents) => cents
77
+ };
78
+ }, [shop?.currency]);
79
+ return ctx ?? fallback;
80
+ }
536
81
 
537
82
  // src/hooks/useMoney.ts
538
83
  function useMoney(currencyOverride) {
539
84
  const { formatMoney } = useLocalization();
540
85
  const shop = useShop();
541
- const ccy = currencyOverride || shop?.currency;
542
- return (amount) => formatMoney(amount, ccy);
86
+ const { selected, base, autoConvert, convert } = useCurrency();
87
+ return (amount) => {
88
+ const shouldConvert = autoConvert && !currencyOverride && !!selected && selected !== base;
89
+ if (shouldConvert) {
90
+ const converted = convert(Math.round(amount * 100), selected) / 100;
91
+ return formatMoney(converted, selected);
92
+ }
93
+ return formatMoney(amount, currencyOverride || shop?.currency);
94
+ };
543
95
  }
544
96
  var DEFAULT_WIDTHS = [320, 480, 640, 768, 1024, 1280, 1600, 1920];
545
97
  var DEFAULT_SIZES = "(min-width: 1024px) 25vw, (min-width: 640px) 50vw, 100vw";
@@ -553,81 +105,6 @@ function useImage(src, opts = {}) {
553
105
  return { src, srcSet, sizes, alt };
554
106
  }, [src, sizes, widths, alt]);
555
107
  }
556
- function useProducts(opts = {}) {
557
- const { limit, fetchIfMissing = false } = opts;
558
- const page = usePage();
559
- const shop = useShop();
560
- const initial = page?.data?.products ?? null;
561
- const [products, setProducts] = useState(initial ?? []);
562
- const [loading, setLoading] = useState(
563
- initial == null && fetchIfMissing
564
- );
565
- const [error, setError] = useState(null);
566
- useEffect(() => {
567
- if (initial != null) return;
568
- if (!fetchIfMissing) return;
569
- if (!shop?.id) return;
570
- let cancelled = false;
571
- (async () => {
572
- try {
573
- const params = new URLSearchParams({ store_id: shop.id });
574
- if (limit) params.set("limit", String(limit));
575
- const res = await fetch(`/api/products?${params.toString()}`);
576
- if (!res.ok) throw new Error(`/api/products \u2192 ${res.status}`);
577
- const data = await res.json();
578
- if (cancelled) return;
579
- setProducts(data.products ?? []);
580
- setLoading(false);
581
- } catch (err) {
582
- if (cancelled) return;
583
- setError(err instanceof Error ? err : new Error(String(err)));
584
- setLoading(false);
585
- }
586
- })();
587
- return () => {
588
- cancelled = true;
589
- };
590
- }, [initial, fetchIfMissing, limit, shop?.id]);
591
- const sliced = limit != null ? products.slice(0, limit) : products;
592
- return { products: sliced, loading, error };
593
- }
594
- function useCollections(opts = {}) {
595
- const { limit, fetchIfMissing = false } = opts;
596
- const page = usePage();
597
- const shop = useShop();
598
- const initial = page?.data?.collections ?? null;
599
- const [collections, setCollections] = useState(initial ?? []);
600
- const [loading, setLoading] = useState(
601
- initial == null && fetchIfMissing
602
- );
603
- const [error, setError] = useState(null);
604
- useEffect(() => {
605
- if (initial != null) return;
606
- if (!fetchIfMissing) return;
607
- if (!shop?.id) return;
608
- let cancelled = false;
609
- (async () => {
610
- try {
611
- const params = new URLSearchParams({ store_id: shop.id });
612
- const res = await fetch(`/api/collections?${params.toString()}`);
613
- if (!res.ok) throw new Error(`/api/collections \u2192 ${res.status}`);
614
- const data = await res.json();
615
- if (cancelled) return;
616
- setCollections(data.collections ?? []);
617
- setLoading(false);
618
- } catch (err) {
619
- if (cancelled) return;
620
- setError(err instanceof Error ? err : new Error(String(err)));
621
- setLoading(false);
622
- }
623
- })();
624
- return () => {
625
- cancelled = true;
626
- };
627
- }, [initial, fetchIfMissing, shop?.id]);
628
- const sliced = limit != null ? collections.slice(0, limit) : collections;
629
- return { collections: sliced, loading, error };
630
- }
631
108
  var NOOP_ACTIONS = {
632
109
  login: async () => ({}),
633
110
  register: async () => ({}),
@@ -997,7 +474,7 @@ function useNavigation(handle, options) {
997
474
  }, [handle, hostItems === null]);
998
475
  return { items, loading, error };
999
476
  }
1000
- var EMPTY = {
477
+ var EMPTY2 = {
1001
478
  products: [],
1002
479
  collections: [],
1003
480
  pages: [],
@@ -1012,7 +489,7 @@ function useSearch(query, options = {}) {
1012
489
  debounceMs = 200,
1013
490
  immediate = false
1014
491
  } = options;
1015
- const [results, setResults] = useState(EMPTY);
492
+ const [results, setResults] = useState(EMPTY2);
1016
493
  const [loading, setLoading] = useState(false);
1017
494
  const [error, setError] = useState(null);
1018
495
  const lastRequestId = useRef(0);
@@ -1020,7 +497,7 @@ function useSearch(query, options = {}) {
1020
497
  if (typeof window === "undefined") return;
1021
498
  const trimmed = query.trim();
1022
499
  if (!trimmed) {
1023
- setResults(EMPTY);
500
+ setResults(EMPTY2);
1024
501
  setLoading(false);
1025
502
  setError(null);
1026
503
  return;
@@ -1038,7 +515,7 @@ function useSearch(query, options = {}) {
1038
515
  });
1039
516
  if (requestId !== lastRequestId.current) return;
1040
517
  if (!res.ok) {
1041
- setResults(EMPTY);
518
+ setResults(EMPTY2);
1042
519
  return;
1043
520
  }
1044
521
  const json = await res.json();
@@ -1054,7 +531,7 @@ function useSearch(query, options = {}) {
1054
531
  } catch (err) {
1055
532
  if (requestId !== lastRequestId.current) return;
1056
533
  setError(err instanceof Error ? err : new Error(String(err)));
1057
- setResults(EMPTY);
534
+ setResults(EMPTY2);
1058
535
  } finally {
1059
536
  if (requestId === lastRequestId.current) setLoading(false);
1060
537
  }
@@ -1159,63 +636,218 @@ function useAnalytics() {
1159
636
  );
1160
637
  return useMemo(() => ({ track }), [track]);
1161
638
  }
639
+ var cache2 = /* @__PURE__ */ new Map();
640
+ function buildSnapshot(entry) {
641
+ return {
642
+ data: entry.data,
643
+ error: entry.error,
644
+ isLoading: !entry.settled,
645
+ isValidating: entry.inFlight
646
+ };
647
+ }
648
+ function commit(entry) {
649
+ entry.snapshot = buildSnapshot(entry);
650
+ entry.subscribers.forEach((cb) => cb());
651
+ }
652
+ function getEntry(key, initialData) {
653
+ let entry = cache2.get(key);
654
+ if (!entry) {
655
+ entry = {
656
+ data: initialData,
657
+ error: void 0,
658
+ settled: false,
659
+ inFlight: false,
660
+ seq: 0,
661
+ lastFetchAt: 0,
662
+ promise: void 0,
663
+ controller: void 0,
664
+ fetcher: void 0,
665
+ subscribers: /* @__PURE__ */ new Set(),
666
+ snapshot: {
667
+ data: initialData,
668
+ error: void 0,
669
+ isLoading: true,
670
+ isValidating: false
671
+ }
672
+ };
673
+ cache2.set(key, entry);
674
+ }
675
+ return entry;
676
+ }
677
+ function revalidateResource(key, fetcher, opts) {
678
+ const entry = getEntry(key);
679
+ entry.fetcher = fetcher;
680
+ if (!opts?.force && entry.promise) return entry.promise;
681
+ const mySeq = ++entry.seq;
682
+ if (entry.controller) entry.controller.abort();
683
+ const controller = new AbortController();
684
+ entry.controller = controller;
685
+ entry.inFlight = true;
686
+ commit(entry);
687
+ const run = (async () => {
688
+ try {
689
+ const data = await fetcher(controller.signal);
690
+ if (mySeq !== entry.seq) return;
691
+ entry.data = data;
692
+ entry.error = void 0;
693
+ entry.settled = true;
694
+ entry.lastFetchAt = Date.now();
695
+ } catch (err) {
696
+ if (controller.signal.aborted) return;
697
+ if (mySeq !== entry.seq) return;
698
+ const error = err instanceof Error ? err : new Error(String(err));
699
+ entry.error = error;
700
+ entry.settled = true;
701
+ entry.lastFetchAt = Date.now();
702
+ opts?.onError?.(error, key);
703
+ } finally {
704
+ if (mySeq === entry.seq) {
705
+ entry.inFlight = false;
706
+ entry.controller = void 0;
707
+ commit(entry);
708
+ }
709
+ }
710
+ })();
711
+ entry.promise = run;
712
+ void run.finally(() => {
713
+ if (entry.promise === run) entry.promise = void 0;
714
+ });
715
+ return run;
716
+ }
717
+ function mutateResource(key, next, opts) {
718
+ const entry = getEntry(key);
719
+ const value = typeof next === "function" ? next(entry.data) : next;
720
+ entry.data = value;
721
+ entry.error = void 0;
722
+ entry.settled = true;
723
+ commit(entry);
724
+ if (opts?.revalidate && entry.fetcher) {
725
+ void revalidateResource(key, entry.fetcher, { force: true });
726
+ }
727
+ }
728
+ function readResource(key) {
729
+ return cache2.get(key)?.data;
730
+ }
731
+ var NOOP = () => {
732
+ };
733
+ function useCachedResource(key, fetcher, options) {
734
+ const enabled = options?.enabled !== false && key != null && key !== "";
735
+ const revalidateOnMount = options?.revalidateOnMount !== false;
736
+ const dedupeIntervalMs = options?.dedupeIntervalMs ?? 2e3;
737
+ const cacheKey = enabled ? key : "";
738
+ const fetcherRef = useRef(fetcher);
739
+ fetcherRef.current = fetcher;
740
+ const onErrorRef = useRef(options?.onError);
741
+ onErrorRef.current = options?.onError;
742
+ const initialDataRef = useRef(options?.initialData);
743
+ const fallbackRef = useRef(null);
744
+ if (fallbackRef.current === null) {
745
+ fallbackRef.current = {
746
+ data: initialDataRef.current,
747
+ error: void 0,
748
+ isLoading: enabled && revalidateOnMount,
749
+ isValidating: false
750
+ };
751
+ }
752
+ const subscribe = useCallback(
753
+ (cb) => {
754
+ if (!enabled) return NOOP;
755
+ const entry = getEntry(cacheKey, initialDataRef.current);
756
+ entry.subscribers.add(cb);
757
+ return () => {
758
+ entry.subscribers.delete(cb);
759
+ };
760
+ },
761
+ [cacheKey, enabled]
762
+ );
763
+ const getSnapshot = useCallback(() => {
764
+ if (!enabled) return fallbackRef.current;
765
+ return getEntry(cacheKey, initialDataRef.current).snapshot;
766
+ }, [cacheKey, enabled]);
767
+ const getServerSnapshot = useCallback(
768
+ () => fallbackRef.current,
769
+ []
770
+ );
771
+ const state = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
772
+ useEffect(() => {
773
+ if (!enabled) return;
774
+ const entry = getEntry(cacheKey, initialDataRef.current);
775
+ const fresh = entry.settled && Date.now() - entry.lastFetchAt < dedupeIntervalMs;
776
+ if (revalidateOnMount && !fresh) {
777
+ void revalidateResource(
778
+ cacheKey,
779
+ (signal) => fetcherRef.current(signal),
780
+ { onError: (e, k) => onErrorRef.current?.(e, k) }
781
+ );
782
+ }
783
+ }, [cacheKey, enabled, revalidateOnMount, dedupeIntervalMs]);
784
+ const mutate = useCallback(
785
+ (next, opts) => {
786
+ if (!enabled) return;
787
+ mutateResource(cacheKey, next, opts);
788
+ },
789
+ [cacheKey, enabled]
790
+ );
791
+ const revalidate = useCallback(() => {
792
+ if (!enabled) return Promise.resolve();
793
+ return revalidateResource(
794
+ cacheKey,
795
+ (signal) => fetcherRef.current(signal),
796
+ { force: true, onError: (e, k) => onErrorRef.current?.(e, k) }
797
+ );
798
+ }, [cacheKey, enabled]);
799
+ return {
800
+ data: state.data,
801
+ error: state.error,
802
+ isLoading: state.isLoading,
803
+ isValidating: state.isValidating,
804
+ mutate,
805
+ revalidate
806
+ };
807
+ }
808
+
809
+ // src/hooks/useApp.ts
1162
810
  function useApp(slug) {
1163
811
  const shop = useShop();
1164
- const [state, setState] = useState({ data: null, loading: true, available: false, error: null });
1165
- const fetchApp = useCallback(async () => {
1166
- if (!slug) {
1167
- setState({ data: null, loading: false, available: false, error: null });
1168
- return;
1169
- }
1170
- setState((s) => ({ ...s, loading: true, error: null }));
1171
- try {
812
+ const key = slug ? `numu:app:${shop.id}:${slug}` : "";
813
+ const fetcher = useCallback(
814
+ async (signal) => {
1172
815
  const url = `/api/storefront/apps/${encodeURIComponent(slug)}`;
1173
816
  const res = await fetch(url, {
1174
817
  credentials: "include",
1175
- headers: { Accept: "application/json" }
818
+ headers: { Accept: "application/json" },
819
+ signal
1176
820
  });
1177
821
  if (res.status === 404) {
1178
- setState({
1179
- data: null,
1180
- loading: false,
1181
- available: false,
1182
- error: null
1183
- });
1184
- return;
822
+ return { payload: null, available: false };
1185
823
  }
1186
824
  if (!res.ok) {
1187
825
  throw new Error(`useApp(${slug}) failed: HTTP ${res.status}`);
1188
826
  }
1189
827
  const body = await res.json();
1190
- const payload = body.data;
1191
- if (!payload) {
1192
- setState({
1193
- data: null,
1194
- loading: false,
1195
- available: false,
1196
- error: null
1197
- });
1198
- return;
1199
- }
1200
- setState({
1201
- data: payload,
1202
- loading: false,
1203
- available: payload.available !== false,
1204
- error: null
1205
- });
1206
- } catch (e) {
1207
- setState({
1208
- data: null,
1209
- loading: false,
1210
- available: false,
1211
- error: e instanceof Error ? e : new Error(String(e))
1212
- });
1213
- }
1214
- }, [slug, shop.id]);
1215
- useEffect(() => {
1216
- void fetchApp();
1217
- }, [fetchApp]);
1218
- return { ...state, refresh: fetchApp };
828
+ const payload = body.data ?? null;
829
+ if (!payload) return { payload: null, available: false };
830
+ return {
831
+ payload,
832
+ available: payload.available !== false
833
+ };
834
+ },
835
+ [slug]
836
+ );
837
+ const { data, isLoading, error, revalidate } = useCachedResource(key, fetcher, {
838
+ enabled: Boolean(slug),
839
+ initialData: { payload: null, available: false }
840
+ });
841
+ const refresh = useCallback(async () => {
842
+ await revalidate();
843
+ }, [revalidate]);
844
+ return {
845
+ data: error ? null : data?.payload ?? null,
846
+ loading: isLoading,
847
+ available: error ? false : data?.available ?? false,
848
+ error: error ?? null,
849
+ refresh
850
+ };
1219
851
  }
1220
852
  function storageKey(storeId) {
1221
853
  return `numu_wishlist_${storeId}`;
@@ -1231,109 +863,95 @@ function readLocal(storeId) {
1231
863
  return [];
1232
864
  }
1233
865
  }
1234
- function writeLocal(storeId, items) {
866
+ function persistWishlist(storeId, items) {
1235
867
  if (typeof window === "undefined") return;
1236
- try {
1237
- window.localStorage.setItem(storageKey(storeId), JSON.stringify(items));
1238
- } catch {
1239
- }
868
+ window.localStorage.setItem(storageKey(storeId), JSON.stringify(items));
1240
869
  }
1241
870
  function sameItem(a, productId, variantId) {
1242
871
  return a.product_id === productId && (a.variant_id ?? null) === (variantId ?? null);
1243
872
  }
873
+ var EMPTY3 = [];
1244
874
  function useWishlist(storeId) {
1245
- const customer = useCustomer();
1246
- const [items, setItems] = useState([]);
1247
- const [loading, setLoading] = useState(true);
1248
- useEffect(() => {
1249
- if (typeof window === "undefined") return;
1250
- setItems(readLocal(storeId));
1251
- setLoading(false);
1252
- }, [storeId, customer?.id]);
875
+ const key = storageKey(storeId);
876
+ const fetcher = useCallback(
877
+ () => Promise.resolve(readLocal(storeId)),
878
+ [storeId]
879
+ );
880
+ const { data, isLoading, mutate } = useCachedResource(
881
+ key,
882
+ fetcher,
883
+ { initialData: EMPTY3 }
884
+ );
885
+ const items = data ?? EMPTY3;
1253
886
  const has = useCallback(
1254
887
  (productId, variantId) => items.some((it) => sameItem(it, productId, variantId)),
1255
888
  [items]
1256
889
  );
890
+ const applyOptimistic = useCallback(
891
+ (compute) => {
892
+ const prev = readResource(key) ?? EMPTY3;
893
+ const next = compute(prev);
894
+ if (next === null) return;
895
+ mutate(next, { revalidate: false });
896
+ try {
897
+ persistWishlist(storeId, next);
898
+ } catch {
899
+ mutate(prev, { revalidate: false });
900
+ }
901
+ },
902
+ [key, storeId, mutate]
903
+ );
1257
904
  const addToWishlist = useCallback(
1258
- (productId, variantId) => {
1259
- setItems((prev) => {
1260
- if (prev.some((it) => sameItem(it, productId, variantId))) {
1261
- return prev;
905
+ (productId, variantId) => applyOptimistic(
906
+ (prev) => prev.some((it) => sameItem(it, productId, variantId)) ? null : [
907
+ ...prev,
908
+ {
909
+ product_id: productId,
910
+ variant_id: variantId ?? null,
911
+ added_at: Date.now()
1262
912
  }
1263
- const next = [
1264
- ...prev,
1265
- {
1266
- product_id: productId,
1267
- variant_id: variantId ?? null,
1268
- added_at: Date.now()
1269
- }
1270
- ];
1271
- writeLocal(storeId, next);
1272
- return next;
1273
- });
1274
- },
1275
- [storeId]
913
+ ]
914
+ ),
915
+ [applyOptimistic]
1276
916
  );
1277
917
  const removeFromWishlist = useCallback(
1278
- (productId, variantId) => {
1279
- setItems((prev) => {
1280
- const next = prev.filter((it) => !sameItem(it, productId, variantId));
1281
- writeLocal(storeId, next);
1282
- return next;
1283
- });
1284
- },
1285
- [storeId]
918
+ (productId, variantId) => applyOptimistic((prev) => {
919
+ const next = prev.filter((it) => !sameItem(it, productId, variantId));
920
+ return next.length === prev.length ? null : next;
921
+ }),
922
+ [applyOptimistic]
1286
923
  );
1287
- const clear = useCallback(() => {
1288
- setItems([]);
1289
- writeLocal(storeId, []);
1290
- }, [storeId]);
1291
- return { items, loading, has, addToWishlist, removeFromWishlist, clear };
924
+ const clear = useCallback(
925
+ () => applyOptimistic((prev) => prev.length === 0 ? null : []),
926
+ [applyOptimistic]
927
+ );
928
+ return { items, loading: isLoading, has, addToWishlist, removeFromWishlist, clear };
1292
929
  }
930
+ var EMPTY4 = [];
1293
931
  function useRelatedProducts(productId, options = {}) {
1294
932
  const limit = options.limit ?? 4;
1295
- const [items, setItems] = useState([]);
1296
- const [loading, setLoading] = useState(false);
1297
- const [error, setError] = useState(null);
1298
- useEffect(() => {
1299
- if (typeof window === "undefined") return;
1300
- if (!productId) {
1301
- setItems([]);
1302
- setLoading(false);
1303
- setError(null);
1304
- return;
1305
- }
1306
- let cancelled = false;
1307
- setLoading(true);
1308
- setError(null);
1309
- void (async () => {
1310
- try {
1311
- const res = await fetch(
1312
- `/api/storefront/products/${encodeURIComponent(productId)}/related?limit=${limit}`,
1313
- { cache: "no-store" }
1314
- );
1315
- if (!res.ok) {
1316
- if (cancelled) return;
1317
- setItems([]);
1318
- return;
1319
- }
1320
- const json = await res.json();
1321
- const list = Array.isArray(json) ? json : Array.isArray(json.data) ? json.data : Array.isArray(json.items) ? json.items : [];
1322
- if (cancelled) return;
1323
- setItems(list);
1324
- } catch (err) {
1325
- if (cancelled) return;
1326
- setError(err instanceof Error ? err : new Error(String(err)));
1327
- setItems([]);
1328
- } finally {
1329
- if (!cancelled) setLoading(false);
933
+ const key = productId ? `numu:related:${productId}:${limit}` : "";
934
+ const fetcher = useCallback(
935
+ async (signal) => {
936
+ const res = await fetch(
937
+ `/api/storefront/products/${encodeURIComponent(
938
+ productId
939
+ )}/related?limit=${limit}`,
940
+ { cache: "no-store", signal }
941
+ );
942
+ if (!res.ok) {
943
+ return EMPTY4;
1330
944
  }
1331
- })();
1332
- return () => {
1333
- cancelled = true;
1334
- };
1335
- }, [productId, limit]);
1336
- return { items, loading, error };
945
+ const json = await res.json();
946
+ return Array.isArray(json) ? json : Array.isArray(json.data) ? json.data : Array.isArray(json.items) ? json.items : EMPTY4;
947
+ },
948
+ [productId, limit]
949
+ );
950
+ const { data, isLoading, error } = useCachedResource(key, fetcher, {
951
+ enabled: Boolean(productId),
952
+ initialData: EMPTY4
953
+ });
954
+ return { items: data ?? EMPTY4, loading: isLoading, error: error ?? null };
1337
955
  }
1338
956
  function useProductSizeChart(productOverride) {
1339
957
  const ctxProduct = useProductOptional();
@@ -1358,86 +976,6 @@ function resolveSizeChart(productAttributes, storeSettings) {
1358
976
  if (hasRows(storeDefault)) return storeDefault;
1359
977
  return null;
1360
978
  }
1361
- var COOKIE_NAME = "numu_currency";
1362
- var COOKIE_MAX_AGE = 60 * 60 * 24 * 30;
1363
- function readCookie(name) {
1364
- if (typeof document === "undefined") return null;
1365
- const match = document.cookie.match(
1366
- new RegExp(`(?:^|;\\s*)${name}=([^;]+)`)
1367
- );
1368
- return match ? decodeURIComponent(match[1]) : null;
1369
- }
1370
- function writeCookie(name, value) {
1371
- if (typeof document === "undefined") return;
1372
- document.cookie = `${name}=${encodeURIComponent(value)}; path=/; max-age=${COOKIE_MAX_AGE}; SameSite=Lax`;
1373
- }
1374
- function useCurrency() {
1375
- const shop = useShop();
1376
- const [config, setConfig] = useState(null);
1377
- const [loading, setLoading] = useState(true);
1378
- const [selected, setSelectedState] = useState("");
1379
- useEffect(() => {
1380
- let cancelled = false;
1381
- (async () => {
1382
- try {
1383
- const res = await fetch(`/api/storefront/currencies`, {
1384
- credentials: "include",
1385
- headers: { Accept: "application/json" }
1386
- });
1387
- if (!res.ok) throw new Error(`currencies: HTTP ${res.status}`);
1388
- const body = await res.json();
1389
- if (cancelled) return;
1390
- setConfig(body.data);
1391
- const cookie = readCookie(COOKIE_NAME);
1392
- const valid = cookie && body.data.presentment.includes(cookie) ? cookie : null;
1393
- setSelectedState(valid || body.data.default_presentment || body.data.base);
1394
- } catch {
1395
- if (cancelled) return;
1396
- setConfig(null);
1397
- setSelectedState(shop.currency || "EGP");
1398
- } finally {
1399
- if (!cancelled) setLoading(false);
1400
- }
1401
- })();
1402
- return () => {
1403
- cancelled = true;
1404
- };
1405
- }, [shop.id, shop.currency]);
1406
- const rates = useMemo(() => {
1407
- const out = {};
1408
- if (!config) return out;
1409
- for (const [k, v] of Object.entries(config.rates)) {
1410
- const n = Number.parseFloat(v);
1411
- if (Number.isFinite(n)) out[k] = n;
1412
- }
1413
- return out;
1414
- }, [config]);
1415
- const convert = useCallback(
1416
- (cents, target) => {
1417
- if (!config) return cents;
1418
- const to = target || selected;
1419
- if (!to || to === config.base) return cents;
1420
- const rate = rates[to];
1421
- if (!rate || !Number.isFinite(rate)) return cents;
1422
- return Math.round(cents * rate);
1423
- },
1424
- [config, rates, selected]
1425
- );
1426
- const setSelected = useCallback((currency) => {
1427
- setSelectedState(currency);
1428
- writeCookie(COOKIE_NAME, currency);
1429
- }, []);
1430
- return {
1431
- base: config?.base || shop.currency || "EGP",
1432
- selected: selected || config?.base || shop.currency || "EGP",
1433
- presentment: config?.presentment || [shop.currency || "EGP"],
1434
- rates,
1435
- autoConvert: Boolean(config?.auto_convert),
1436
- loading,
1437
- setSelected,
1438
- convert
1439
- };
1440
- }
1441
979
 
1442
980
  // src/utils/variants.ts
1443
981
  function findVariantByOptions(product, selection) {
@@ -1482,6 +1020,25 @@ function availableValues(product, selection) {
1482
1020
  }
1483
1021
  return out;
1484
1022
  }
1023
+
1024
+ // src/utils/selectionRegistry.ts
1025
+ var selections = /* @__PURE__ */ new Map();
1026
+ function publishVariantSelection(productId, selection) {
1027
+ if (!productId) return;
1028
+ selections.set(productId, selection);
1029
+ }
1030
+ function readVariantSelection(productId) {
1031
+ if (!productId) return null;
1032
+ const sel = selections.get(productId);
1033
+ if (!sel) return null;
1034
+ const filtered = {};
1035
+ for (const [k, v] of Object.entries(sel)) {
1036
+ if (v) filtered[k] = v;
1037
+ }
1038
+ return Object.keys(filtered).length > 0 ? filtered : null;
1039
+ }
1040
+
1041
+ // src/hooks/useVariantSelection.ts
1485
1042
  function useVariantSelection(product, opts = {}) {
1486
1043
  const autoSelect = opts.autoSelect ?? true;
1487
1044
  const initial = useMemo(() => {
@@ -1500,6 +1057,9 @@ function useVariantSelection(product, opts = {}) {
1500
1057
  () => findVariantByOptions(product, selection),
1501
1058
  [product, selection]
1502
1059
  );
1060
+ useEffect(() => {
1061
+ publishVariantSelection(product.id, selection);
1062
+ }, [product.id, selection]);
1503
1063
  const availability = useMemo(
1504
1064
  () => availableValues(product, selection),
1505
1065
  [product, selection]
@@ -1893,6 +1453,19 @@ function readCsrfCookie() {
1893
1453
  const match = document.cookie.match(/(?:^|;\s*)numu_csrf=([^;]+)/);
1894
1454
  return match ? decodeURIComponent(match[1]) : null;
1895
1455
  }
1456
+ var CURRENCY_COOKIE_NAME = "numu_currency";
1457
+ var CURRENCY_COOKIE_MAX_AGE = 60 * 60 * 24 * 30;
1458
+ function readCurrencyCookie() {
1459
+ if (typeof document === "undefined") return null;
1460
+ const match = document.cookie.match(
1461
+ new RegExp(`(?:^|;\\s*)${CURRENCY_COOKIE_NAME}=([^;]+)`)
1462
+ );
1463
+ return match ? decodeURIComponent(match[1]) : null;
1464
+ }
1465
+ function writeCurrencyCookie(value) {
1466
+ if (typeof document === "undefined") return;
1467
+ document.cookie = `${CURRENCY_COOKIE_NAME}=${encodeURIComponent(value)}; path=/; max-age=${CURRENCY_COOKIE_MAX_AGE}; SameSite=Lax`;
1468
+ }
1896
1469
  async function postCartMutation(endpoint, body, applyCart, reserveToken) {
1897
1470
  const headers = {
1898
1471
  "Content-Type": "application/json"
@@ -1906,9 +1479,21 @@ async function postCartMutation(endpoint, body, applyCart, reserveToken) {
1906
1479
  headers,
1907
1480
  body: body === void 0 ? void 0 : JSON.stringify(body)
1908
1481
  });
1909
- if (!res.ok) return;
1482
+ if (!res.ok) {
1483
+ let message;
1484
+ try {
1485
+ const err = await res.json();
1486
+ if (err && typeof err === "object") {
1487
+ const m = err.message ?? err.error ?? err.detail;
1488
+ if (typeof m === "string") message = m;
1489
+ }
1490
+ } catch {
1491
+ }
1492
+ return { ok: false, status: res.status, message };
1493
+ }
1910
1494
  const json = await res.json();
1911
1495
  applyCart(unwrapCart(json));
1496
+ return { ok: true, status: res.status };
1912
1497
  }
1913
1498
  function NuMuProvider({
1914
1499
  store,
@@ -1918,6 +1503,7 @@ function NuMuProvider({
1918
1503
  locale: initialLocale,
1919
1504
  translations: initialTranslations,
1920
1505
  currentTemplate = "home",
1506
+ pageTemplate,
1921
1507
  initialProducts,
1922
1508
  initialCollections,
1923
1509
  navigation,
@@ -1927,12 +1513,16 @@ function NuMuProvider({
1927
1513
  () => ({
1928
1514
  type: currentTemplate,
1929
1515
  title: store?.name ?? "",
1516
+ // I3 — resolved alternate template key (e.g. "product.wholesale"),
1517
+ // surfaced via usePage()?.template. Omitted when undefined so a
1518
+ // page on its default template exposes no `template` field.
1519
+ ...pageTemplate ? { template: pageTemplate } : {},
1930
1520
  data: {
1931
1521
  products: initialProducts ?? [],
1932
1522
  collections: initialCollections ?? []
1933
1523
  }
1934
1524
  }),
1935
- [currentTemplate, store?.name, initialProducts, initialCollections]
1525
+ [currentTemplate, pageTemplate, store?.name, initialProducts, initialCollections]
1936
1526
  );
1937
1527
  const [cart, setCart] = useState(
1938
1528
  initialCart || { ...EMPTY_CART, currency: store.currency }
@@ -2123,7 +1713,7 @@ function NuMuProvider({
2123
1713
  const token = reserveToken();
2124
1714
  setLoading(true);
2125
1715
  try {
2126
- await postCartMutation(
1716
+ return await postCartMutation(
2127
1717
  endpoint,
2128
1718
  body,
2129
1719
  buildApplyCart(token),
@@ -2137,15 +1727,18 @@ function NuMuProvider({
2137
1727
  [reserveToken, buildApplyCart]
2138
1728
  );
2139
1729
  const addItem = useCallback(
2140
- async (productId, variantId, quantity) => {
1730
+ async (productId, variantId, quantity, selectedOptions) => {
2141
1731
  const eventId = typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : `${Date.now()}`;
2142
1732
  const qty = quantity || 1;
2143
- await mutate("/api/cart/add", {
1733
+ const axes = selectedOptions && Object.keys(selectedOptions).length > 0 ? selectedOptions : readVariantSelection(productId) ?? void 0;
1734
+ const result = await mutate("/api/cart/add", {
2144
1735
  product_id: productId,
2145
1736
  variant_id: variantId,
2146
1737
  quantity: qty,
1738
+ selected_options: axes,
2147
1739
  _event_id: eventId
2148
1740
  });
1741
+ if (!result.ok) return result;
2149
1742
  try {
2150
1743
  if (typeof window !== "undefined") {
2151
1744
  window.dispatchEvent(
@@ -2164,34 +1757,29 @@ function NuMuProvider({
2164
1757
  }
2165
1758
  } catch {
2166
1759
  }
1760
+ return result;
2167
1761
  },
2168
1762
  [mutate]
2169
1763
  );
2170
1764
  const removeItem = useCallback(
2171
- async (itemId) => {
2172
- await mutate("/api/cart/remove", { item_id: itemId });
2173
- },
1765
+ (itemId) => mutate("/api/cart/remove", { item_id: itemId }),
2174
1766
  [mutate]
2175
1767
  );
2176
1768
  const updateQuantity = useCallback(
2177
- async (itemId, quantity) => {
2178
- await mutate("/api/cart/update", { item_id: itemId, quantity });
2179
- },
1769
+ (itemId, quantity) => mutate("/api/cart/update", { item_id: itemId, quantity }),
2180
1770
  [mutate]
2181
1771
  );
2182
1772
  const applyDiscount = useCallback(
2183
- async (code) => {
2184
- await mutate("/api/cart/discount", { code });
2185
- },
1773
+ (code) => mutate("/api/cart/discount", { code }),
1774
+ [mutate]
1775
+ );
1776
+ const removeDiscount = useCallback(
1777
+ // DELETE — postCartMutation interprets undefined body as DELETE.
1778
+ () => mutate("/api/cart/discount", void 0),
2186
1779
  [mutate]
2187
1780
  );
2188
- const removeDiscount = useCallback(async () => {
2189
- await mutate("/api/cart/discount", void 0);
2190
- }, [mutate]);
2191
1781
  const updateNote = useCallback(
2192
- async (note) => {
2193
- await mutate("/api/cart/update", { note });
2194
- },
1782
+ (note) => mutate("/api/cart/update", { note }),
2195
1783
  [mutate]
2196
1784
  );
2197
1785
  const clearCart = useCallback(async () => {
@@ -2327,7 +1915,87 @@ function NuMuProvider({
2327
1915
  defaultNumberFmt
2328
1916
  ]
2329
1917
  );
2330
- return /* @__PURE__ */ jsx(ShopContext.Provider, { value: store, children: /* @__PURE__ */ jsx(ThemeSettingsContext.Provider, { value: themeSettings, children: /* @__PURE__ */ jsx(CurrentTemplateContext.Provider, { value: currentTemplate, children: /* @__PURE__ */ jsx(PageContext.Provider, { value: pageValue, 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: /* @__PURE__ */ jsx(NavigationContext.Provider, { value: navigation ?? {}, children }) }) }) }) }) }) }) }) });
1918
+ const [currencyConfig, setCurrencyConfig] = useState(
1919
+ null
1920
+ );
1921
+ const [currencyLoading, setCurrencyLoading] = useState(true);
1922
+ const [currencySelected, setCurrencySelected] = useState("");
1923
+ useEffect(() => {
1924
+ if (typeof window === "undefined") return;
1925
+ let cancelled = false;
1926
+ void (async () => {
1927
+ try {
1928
+ const res = await fetch("/api/storefront/currencies", {
1929
+ credentials: "include",
1930
+ headers: { Accept: "application/json" }
1931
+ });
1932
+ if (!res.ok) throw new Error(`currencies: HTTP ${res.status}`);
1933
+ const body = await res.json();
1934
+ if (cancelled) return;
1935
+ setCurrencyConfig(body.data);
1936
+ const cookie = readCurrencyCookie();
1937
+ const valid = cookie && body.data.presentment?.includes(cookie) ? cookie : null;
1938
+ setCurrencySelected(
1939
+ valid || body.data.default_presentment || body.data.base
1940
+ );
1941
+ } catch {
1942
+ if (cancelled) return;
1943
+ setCurrencyConfig(null);
1944
+ setCurrencySelected(store.currency || "EGP");
1945
+ } finally {
1946
+ if (!cancelled) setCurrencyLoading(false);
1947
+ }
1948
+ })();
1949
+ return () => {
1950
+ cancelled = true;
1951
+ };
1952
+ }, [store.id, store.currency]);
1953
+ const currencyRates = useMemo(() => {
1954
+ const out = {};
1955
+ if (!currencyConfig) return out;
1956
+ for (const [k, v] of Object.entries(currencyConfig.rates)) {
1957
+ const n = Number.parseFloat(v);
1958
+ if (Number.isFinite(n)) out[k] = n;
1959
+ }
1960
+ return out;
1961
+ }, [currencyConfig]);
1962
+ const convertCurrency = useCallback(
1963
+ (cents, target) => {
1964
+ if (!currencyConfig) return cents;
1965
+ const to = target || currencySelected;
1966
+ if (!to || to === currencyConfig.base) return cents;
1967
+ const rate = currencyRates[to];
1968
+ if (!rate || !Number.isFinite(rate)) return cents;
1969
+ return Math.round(cents * rate);
1970
+ },
1971
+ [currencyConfig, currencyRates, currencySelected]
1972
+ );
1973
+ const setCurrency = useCallback((currency) => {
1974
+ setCurrencySelected(currency);
1975
+ writeCurrencyCookie(currency);
1976
+ }, []);
1977
+ const currencyValue = useMemo(
1978
+ () => ({
1979
+ base: currencyConfig?.base || store.currency || "EGP",
1980
+ selected: currencySelected || currencyConfig?.base || store.currency || "EGP",
1981
+ presentment: currencyConfig?.presentment || [store.currency || "EGP"],
1982
+ rates: currencyRates,
1983
+ autoConvert: Boolean(currencyConfig?.auto_convert),
1984
+ loading: currencyLoading,
1985
+ setSelected: setCurrency,
1986
+ convert: convertCurrency
1987
+ }),
1988
+ [
1989
+ currencyConfig,
1990
+ currencySelected,
1991
+ currencyRates,
1992
+ currencyLoading,
1993
+ store.currency,
1994
+ setCurrency,
1995
+ convertCurrency
1996
+ ]
1997
+ );
1998
+ return /* @__PURE__ */ jsx(ShopContext.Provider, { value: store, children: /* @__PURE__ */ jsx(ThemeSettingsContext.Provider, { value: themeSettings, children: /* @__PURE__ */ jsx(CurrentTemplateContext.Provider, { value: currentTemplate, children: /* @__PURE__ */ jsx(PageContext.Provider, { value: pageValue, children: /* @__PURE__ */ jsx(LocalizationContext.Provider, { value: localization, children: /* @__PURE__ */ jsx(CurrencyContext.Provider, { value: currencyValue, children: /* @__PURE__ */ jsx(CartContext.Provider, { value: cartValue, children: /* @__PURE__ */ jsx(CustomerContext.Provider, { value: customerState, children: /* @__PURE__ */ jsx(CustomerActionsContext.Provider, { value: customerActions, children: /* @__PURE__ */ jsx(NavigationContext.Provider, { value: navigation ?? {}, children }) }) }) }) }) }) }) }) }) });
2331
1999
  }
2332
2000
  function ProductProvider({ product, children }) {
2333
2001
  return /* @__PURE__ */ jsx(ProductContext.Provider, { value: product, children });
@@ -2628,6 +2296,7 @@ var ThemeMountBridge = forwardRef(function ThemeMountBridge2({ ctx, mountEl, ren
2628
2296
  initialProducts: pageData.products,
2629
2297
  initialCollections: pageData.collections,
2630
2298
  currentTemplate: template,
2299
+ pageTemplate: ctx.page?.template,
2631
2300
  children: wrapEntityProviders(app, pageData)
2632
2301
  }
2633
2302
  );
@@ -2673,15 +2342,18 @@ function Money({
2673
2342
  }) {
2674
2343
  const { formatMoney } = useLocalization();
2675
2344
  const shop = useShop();
2676
- const ccy = currency || shop?.currency;
2345
+ const { selected, base, autoConvert, convert } = useCurrency();
2346
+ const shouldConvert = autoConvert && !currency && !!selected && selected !== base;
2347
+ const ccy = shouldConvert ? selected : currency || shop?.currency;
2348
+ const toDisplay = (major) => shouldConvert ? convert(Math.round(major * 100), selected) / 100 : major;
2677
2349
  const showCompare = compareAt != null && compareAt > amount;
2678
2350
  const children = [
2679
- /* @__PURE__ */ jsx("span", { children: formatMoney(amount, ccy) }, "amt")
2351
+ /* @__PURE__ */ jsx("span", { children: formatMoney(toDisplay(amount), ccy) }, "amt")
2680
2352
  ];
2681
2353
  if (showCompare) {
2682
2354
  children.push(" ");
2683
2355
  children.push(
2684
- /* @__PURE__ */ jsx("s", { style: { opacity: 0.6 }, children: formatMoney(compareAt, ccy) }, "cmp")
2356
+ /* @__PURE__ */ jsx("s", { style: { opacity: 0.6 }, children: formatMoney(toDisplay(compareAt), ccy) }, "cmp")
2685
2357
  );
2686
2358
  }
2687
2359
  return createElement(
@@ -2960,19 +2632,40 @@ function Logo({
2960
2632
  );
2961
2633
  }
2962
2634
  var ABSOLUTE_URL = /^[a-z]+:|^\/\//i;
2963
- function Link({ to, children, ...rest }) {
2635
+ var NAVIGATE_EVENT = "numu:navigate";
2636
+ function requestNavigate(href) {
2637
+ if (typeof window === "undefined") return false;
2638
+ const event = new CustomEvent(NAVIGATE_EVENT, {
2639
+ detail: { href },
2640
+ cancelable: true
2641
+ });
2642
+ return !window.dispatchEvent(event);
2643
+ }
2644
+ function Link({ to, children, onClick, ...rest }) {
2964
2645
  const shop = useShop();
2965
2646
  const isAbsolute = ABSOLUTE_URL.test(to);
2966
2647
  let href = to;
2967
2648
  if (!isAbsolute && shop && !to.startsWith("/")) {
2968
2649
  href = `/${to}`;
2969
2650
  }
2970
- return /* @__PURE__ */ jsx("a", { href, ...rest, children });
2651
+ const handleClick = (e) => {
2652
+ onClick?.(e);
2653
+ if (e.defaultPrevented) return;
2654
+ if (e.button !== 0) return;
2655
+ if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
2656
+ if (rest.target && rest.target !== "_self") return;
2657
+ if (rest.download !== void 0) return;
2658
+ if (isAbsolute) return;
2659
+ if (to.startsWith("#") || href.startsWith("#")) return;
2660
+ if (requestNavigate(href)) e.preventDefault();
2661
+ };
2662
+ return /* @__PURE__ */ jsx("a", { href, onClick: handleClick, ...rest, children });
2971
2663
  }
2972
2664
  function AddToCartButton({
2973
2665
  product,
2974
2666
  variant,
2975
2667
  quantity = 1,
2668
+ selectedOptions,
2976
2669
  label = "Add to cart",
2977
2670
  loadingLabel = "Adding\u2026",
2978
2671
  soldOutLabel = "Sold out",
@@ -2999,7 +2692,7 @@ function AddToCartButton({
2999
2692
  if (state === "adding") return;
3000
2693
  setState("adding");
3001
2694
  try {
3002
- await addItem(product.id, variant?.id, quantity);
2695
+ await addItem(product.id, variant?.id, quantity, selectedOptions);
3003
2696
  setState("idle");
3004
2697
  onAdded?.(product, variant);
3005
2698
  } catch {
@@ -3417,11 +3110,29 @@ function walk(node) {
3417
3110
  }
3418
3111
  }
3419
3112
  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, "");
3113
+ if (!input) return "";
3114
+ let s = input;
3115
+ let prev;
3116
+ const DANGEROUS = [
3117
+ /<script\b[\s\S]*?<\/script\s*>/gi,
3118
+ /<style\b[\s\S]*?<\/style\s*>/gi,
3119
+ /<iframe\b[\s\S]*?<\/iframe\s*>/gi,
3120
+ /<object\b[\s\S]*?<\/object\s*>/gi,
3121
+ /<embed\b[\s\S]*?<\/embed\s*>/gi,
3122
+ /<\/?(?:script|style|iframe|object|embed|link|meta|form|input)\b[^>]*>/gi
3123
+ ];
3124
+ do {
3125
+ prev = s;
3126
+ for (const re of DANGEROUS) s = s.replace(re, "");
3127
+ } while (s !== prev);
3128
+ const HANDLER = /[\s/"'<]on\w+\s*=\s*("[^"]*"|'[^']*'|[^\s>]+)/gi;
3129
+ do {
3130
+ prev = s;
3131
+ s = s.replace(HANDLER, "");
3132
+ } while (s !== prev);
3422
3133
  s = s.replace(
3423
- /\s+(href|src)\s*=\s*("[^"]*"|'[^']*'|[^\s>]+)/gi,
3424
- (full, attr, val) => {
3134
+ /[\s/"'<](href|src)\s*=\s*("[^"]*"|'[^']*'|[^\s>]+)/gi,
3135
+ (full, _attr, val) => {
3425
3136
  const cleaned = String(val).replace(/^['"]|['"]$/g, "");
3426
3137
  return isSafeUrl(cleaned) ? full : "";
3427
3138
  }
@@ -3766,97 +3477,6 @@ function Icon({
3766
3477
  );
3767
3478
  }
3768
3479
 
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
3480
  // src/utils/federation.ts
3861
3481
  function sdkSlot() {
3862
3482
  return /* @__PURE__ */ Symbol.for("@numueg/theme-sdk:singleton");
@@ -4169,6 +3789,6 @@ function buildLocaleBundle(modules) {
4169
3789
  return bundle;
4170
3790
  }
4171
3791
 
4172
- export { AddToCartButton, Block, CartContext, CollectionCard, CollectionContext, CollectionProvider, CurrencySwitcher, CustomerContext, EditableImage, EditableText, Form, HeroMedia, ICON_NAMES, Icon, IconMap, Image, KNOWN_SETTING_TYPES, KNOWN_TEMPLATES, LOGO_SHAPE_OPTIONS, LOGO_SIZE_OPTIONS, Link, LocaleSwitcher, LocalizationContext, Logo, MAX_BLOCK_DEPTH, Money, NavigationContext, NuMuProvider, PageContext, ProductCard, ProductContext, ProductProvider, REQUIRED_TEMPLATES, RichText, SDK_VERSION, Section, SectionContext, ShopContext, THEME_CONTRACT_VERSION, ThemeSettingsContext, applyGlobalStyleTokens, applyImageTransform, asImageTransform, assetUrl, availableValues, buildLocaleBundle, buildThemeElement, clearSdkSingleton, collectBlocks, collectSections, computeGlobalStyleTokens, defaultVariant, defineBlock, defineSection, defineThemeEntry, dynamicSource, findVariantByOptions, flattenMessages, focalSrc, getReactSingleton, getSdkSingleton, isDefinedBlock, isDefinedSection, isDynamicSource, isSdkAvailable, logoImgStyle, logoStyleTokens, mergeResults, mountTheme, pickTranslations, registerReactSingleton, registerSdkSingleton, resolveDynamicValue, resolveFontStack, resolveSettingsMap, resolveSizeChart, resolveSourcePath, resolveThemeSettings, sanitizeHtml, useAnalytics, useApp, useCart, useCheckout, useCollection, useCollectionOptional, useCollections, useCurrency, useCurrentTemplate, useCustomer, useCustomerActions, useCustomerAddresses, useDirection, useFieldTranslation, useGiftCardBalance, useImage, useLocale, useLocalization, useMoney, useNavigation, useNumberFormat, useOrder, useOrders, usePage, useProduct, useProductOptional, useProductSizeChart, useProducts, useRelatedProducts, useReorder, useResolvedSettings, useSearch, useSection, useSectionOptional, useShippingRates, useShop, useThemeSettings, useTranslation, useVariantSelection, useWishlist, validateBuiltManifest, validateManifest, validateSectionSchema, validateSettingsAgainstSchema };
3792
+ export { AddToCartButton, Block, CollectionCard, CollectionProvider, CurrencySwitcher, EditableImage, EditableText, Form, HeroMedia, ICON_NAMES, Icon, IconMap, Image, LOGO_SHAPE_OPTIONS, LOGO_SIZE_OPTIONS, Link, LocaleSwitcher, Logo, MAX_BLOCK_DEPTH, Money, NAVIGATE_EVENT, NuMuProvider, ProductCard, ProductProvider, RichText, Section, SectionContext, applyGlobalStyleTokens, applyImageTransform, asImageTransform, assetUrl, availableValues, buildLocaleBundle, buildThemeElement, clearSdkSingleton, collectBlocks, collectSections, computeGlobalStyleTokens, defaultVariant, defineBlock, defineSection, defineThemeEntry, dynamicSource, findVariantByOptions, flattenMessages, focalSrc, getReactSingleton, getSdkSingleton, isDefinedBlock, isDefinedSection, isDynamicSource, isSdkAvailable, logoImgStyle, logoStyleTokens, mountTheme, pickTranslations, publishVariantSelection, readVariantSelection, registerReactSingleton, registerSdkSingleton, requestNavigate, resolveDynamicValue, resolveFontStack, resolveSettingsMap, resolveSizeChart, resolveSourcePath, sanitizeHtml, useAnalytics, useApp, useCachedResource, useCart, useCheckout, useCollection, useCollectionOptional, useCurrency, useCurrentTemplate, useCustomerActions, useCustomerAddresses, useGiftCardBalance, useImage, useMoney, useNavigation, useOrder, useOrders, useProduct, useProductOptional, useProductSizeChart, useRelatedProducts, useReorder, useResolvedSettings, useSearch, useSection, useSectionGroup, useSectionOptional, useShippingRates, useVariantSelection, useWishlist };
4173
3793
  //# sourceMappingURL=index.mjs.map
4174
3794
  //# sourceMappingURL=index.mjs.map