@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.
- package/CHANGELOG.md +144 -0
- package/dist/chunk-4TMO5Y5W.mjs +183 -0
- package/dist/chunk-4TMO5Y5W.mjs.map +1 -0
- package/dist/chunk-EB2ESBLD.mjs +397 -0
- package/dist/chunk-EB2ESBLD.mjs.map +1 -0
- package/dist/chunk-IBNAZRUT.cjs +207 -0
- package/dist/chunk-IBNAZRUT.cjs.map +1 -0
- package/dist/chunk-KWODVI7F.cjs +408 -0
- package/dist/chunk-KWODVI7F.cjs.map +1 -0
- package/dist/chunk-TBSNHHFH.cjs +96 -0
- package/dist/chunk-TBSNHHFH.cjs.map +1 -0
- package/dist/chunk-XF2FGIVS.mjs +94 -0
- package/dist/chunk-XF2FGIVS.mjs.map +1 -0
- package/dist/{entities-6MGANln7.d.mts → entities-B8378GKp.d.mts} +12 -0
- package/dist/{entities-6MGANln7.d.ts → entities-B8378GKp.d.ts} +12 -0
- package/dist/index.cjs +693 -968
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +286 -94
- package/dist/index.d.ts +286 -94
- package/dist/index.mjs +531 -911
- package/dist/index.mjs.map +1 -1
- package/dist/{mount-BGumg1JM.d.mts → mount-HbnOAYZ-.d.ts} +90 -14
- package/dist/{mount-Bt-4ken5.d.ts → mount-eAhGgGtZ.d.mts} +90 -14
- package/dist/normalize.cjs +7 -91
- package/dist/normalize.cjs.map +1 -1
- package/dist/normalize.d.mts +1 -1
- package/dist/normalize.d.ts +1 -1
- package/dist/normalize.mjs +1 -92
- package/dist/normalize.mjs.map +1 -1
- package/dist/{theme-D8MOopvi.d.mts → theme-CNTB4KnU.d.mts} +29 -3
- package/dist/{theme-D8MOopvi.d.ts → theme-CNTB4KnU.d.ts} +29 -3
- package/dist/types.d.mts +2 -2
- package/dist/types.d.ts +2 -2
- package/dist/v2-compat.cjs +6 -163
- package/dist/v2-compat.cjs.map +1 -1
- package/dist/v2-compat.d.mts +1 -1
- package/dist/v2-compat.d.ts +1 -1
- package/dist/v2-compat.mjs +2 -159
- package/dist/v2-compat.mjs.map +1 -1
- package/dist/validation.cjs +43 -403
- package/dist/validation.cjs.map +1 -1
- package/dist/validation.d.mts +1 -1
- package/dist/validation.d.ts +1 -1
- package/dist/validation.mjs +1 -395
- package/dist/validation.mjs.map +1 -1
- package/dist/verify.cjs +6 -21
- package/dist/verify.cjs.map +1 -1
- package/dist/verify.d.mts +3 -3
- package/dist/verify.d.ts +3 -3
- package/dist/verify.mjs +3 -20
- package/dist/verify.mjs.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,150 @@ All notable changes to `@numueg/theme-sdk` are documented here. The format is ba
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.9.0] - 2026-07-04
|
|
8
|
+
|
|
9
|
+
Template epic I3 — the SDK side of template overrides + global sections shared
|
|
10
|
+
across pages. Additive: three new/extended surfaces, no breaking changes. As
|
|
11
|
+
always, self-contained theme bundles freeze the SDK at build time, so themes
|
|
12
|
+
must be **rebuilt + redeployed** to pick these up.
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- **`useSectionGroup(group)`** — returns the ordered section instances for a
|
|
17
|
+
named section group ("header", "footer", or any custom global group) from
|
|
18
|
+
`themeSettings.section_groups[group]`. Each entry re-attaches its `id` (the
|
|
19
|
+
key it held in the group's `sections` map) so a theme can key its React list,
|
|
20
|
+
wire `<Section id={…}>` click-to-select, and dispatch the `type` through its
|
|
21
|
+
OWN registry. Returns a stable `[]` when there is no provider, no
|
|
22
|
+
`section_groups`, the group is absent, or its `order` is empty. Disabled
|
|
23
|
+
instances are included (the `disabled` flag is preserved); ids in `order` but
|
|
24
|
+
missing from `sections` are skipped. New export `useSectionGroup` + type
|
|
25
|
+
`SectionGroupInstance` (`SectionInstance & { id: string }`).
|
|
26
|
+
|
|
27
|
+
No `<GlobalSections>` component ships: the SDK has no section registry — a
|
|
28
|
+
theme builds its own via `collectSections` — so there is no generic renderer
|
|
29
|
+
to expose. Themes render the returned instances through their existing
|
|
30
|
+
registry, exactly as they already do for template sections.
|
|
31
|
+
|
|
32
|
+
- **`Page.template` (+ `ThemeMountPage.template`)** — an optional resolved
|
|
33
|
+
alternate template key surfaced via `usePage()?.template`, e.g.
|
|
34
|
+
`"product.wholesale"` for a product routed to the `wholesale` template
|
|
35
|
+
suffix. Distinct from `useCurrentTemplate()` / `page.type`, which stay the
|
|
36
|
+
base route type (`"product"`); `template` carries the FULL key a theme uses
|
|
37
|
+
to look up `themeSettings.templates[template]`. Hosts forward it in the mount
|
|
38
|
+
context's `page.template`; `mountTheme` threads it to `NuMuProvider`'s new
|
|
39
|
+
optional `pageTemplate` prop, which publishes it on the `PageContext` value.
|
|
40
|
+
Omitted for pages on their default template, so themes/hosts predating the
|
|
41
|
+
field are unaffected.
|
|
42
|
+
|
|
43
|
+
## [0.8.0] - 2026-07-04
|
|
44
|
+
|
|
45
|
+
Phase 3 (shared client-data layer). Additive — a new primitive plus internal
|
|
46
|
+
refactors of three hooks onto it. As always, self-contained theme bundles
|
|
47
|
+
freeze the SDK at build time, so themes must be **rebuilt + redeployed** to pick
|
|
48
|
+
these up.
|
|
49
|
+
|
|
50
|
+
### Added
|
|
51
|
+
|
|
52
|
+
- **`useCachedResource<T>(key, fetcher, opts?)`** — a tiny, dependency-free
|
|
53
|
+
SWR-style client cache (new `@numueg/theme-sdk` export, `src/lib/dataCache.ts`).
|
|
54
|
+
One module-level store keyed by string gives themes and SDK hooks:
|
|
55
|
+
- **Dedup** — the first consumer of a key starts the fetch and stashes the
|
|
56
|
+
in-flight promise; every other consumer that revalidates the same key while
|
|
57
|
+
it's pending joins that promise. N consumers → ONE network call.
|
|
58
|
+
- **Cross-instance sync** — every instance subscribes via
|
|
59
|
+
`useSyncExternalStore`; a fetch resolve or `mutate` rebuilds the entry's
|
|
60
|
+
snapshot and notifies ALL subscribers, so they re-render in lockstep.
|
|
61
|
+
- **Revalidate + cancellation** — each fetch reserves a monotonic sequence and
|
|
62
|
+
an `AbortController`; a forced fetch aborts the previous one and a result is
|
|
63
|
+
applied only if its sequence is still the latest, so an out-of-order
|
|
64
|
+
(superseded) response can never overwrite a newer one.
|
|
65
|
+
- **SSR-safe** — the fetch is effect-only (never runs under `renderToString`)
|
|
66
|
+
and the server snapshot returns `initialData` without touching the module
|
|
67
|
+
cache (no cross-request bleed).
|
|
68
|
+
|
|
69
|
+
New types: `CachedResource`, `CachedResourceState`, `CacheFetcher`,
|
|
70
|
+
`CacheMutator`, `MutateOptions`, `UseCachedResourceOptions`.
|
|
71
|
+
|
|
72
|
+
### Fixed
|
|
73
|
+
|
|
74
|
+
- **Wishlist hearts no longer desync.** `useWishlist` held the item list in
|
|
75
|
+
per-instance `useState`, so two `<Heart>`s for the same product each owned a
|
|
76
|
+
copy — adding via one never re-rendered the other. It now reads/writes ONE
|
|
77
|
+
shared `useCachedResource` entry (`numu_wishlist_<store_id>`), so an
|
|
78
|
+
add/remove anywhere reflows every heart. Writes are optimistic and roll back
|
|
79
|
+
if `localStorage` persistence throws (quota / private mode).
|
|
80
|
+
- **`useApp` no longer applies out-of-order responses.** The per-instance fetch
|
|
81
|
+
had no cancellation, so a slow reply for a superseded slug (or a superseded
|
|
82
|
+
`refresh()`) could apply stale state over a newer one. It now runs through
|
|
83
|
+
`useCachedResource` (keyed `numu:app:<store_id>:<slug>`) with an AbortSignal
|
|
84
|
+
and seq-guarded application; N consumers of one slug also share a single
|
|
85
|
+
request. `loading` reflects the first load only — `refresh()` now revalidates
|
|
86
|
+
in the background while keeping the last-good data (no skeleton flash).
|
|
87
|
+
- **`useRelatedProducts` dedupes + cancels.** Two related-products sections on
|
|
88
|
+
one PDP shared no state and each refetched; a rapid product switch could race.
|
|
89
|
+
Now keyed by `numu:related:<productId>:<limit>` through the shared cache, with
|
|
90
|
+
AbortSignal-backed cancellation of superseded fetches. Public shape unchanged.
|
|
91
|
+
|
|
92
|
+
## [0.7.0] - 2026-07-04
|
|
93
|
+
|
|
94
|
+
Phase 2 (correctness debt). Changes are additive or bug fixes; note that
|
|
95
|
+
self-contained theme bundles freeze the SDK at build time, so themes must be
|
|
96
|
+
**rebuilt + redeployed** to pick these up.
|
|
97
|
+
|
|
98
|
+
### Fixed
|
|
99
|
+
|
|
100
|
+
- **`/v2-compat` shared contexts.** `tsup` now builds with `splitting: true`, so
|
|
101
|
+
the context module (its ~12 `createContext()` calls) is hoisted into ONE shared
|
|
102
|
+
chunk imported by both `dist/index.*` and `dist/v2-compat.*`. Under the old
|
|
103
|
+
`splitting: false`, the `v2-compat` entry bundled its OWN copies, so
|
|
104
|
+
`useV2Products()` / `useV2Theme()` / … read null contexts that `NuMuProvider`
|
|
105
|
+
never populated and the documented V2→V3 migration path silently failed.
|
|
106
|
+
Verified for both esm + cjs; the pure `validation` / `verify` / `normalize`
|
|
107
|
+
entries stay React-free.
|
|
108
|
+
- **Multi-currency now propagates to `<Money>`.** The currency config, selection
|
|
109
|
+
and `convert()` are lifted into a new `CurrencyContext`, fetched ONCE by
|
|
110
|
+
`NuMuProvider` (previously `useCurrency()` fetched `/api/storefront/currencies`
|
|
111
|
+
and held the selection in per-component state, and `<Money>` ignored it
|
|
112
|
+
entirely). `<Money>` and `useMoney()` now present amounts in the visitor's
|
|
113
|
+
selected currency when the store has `auto_convert` on and no explicit
|
|
114
|
+
`currency` is pinned, so a `<CurrencySwitcher>` change reflows every price on
|
|
115
|
+
the page without a reload. SSR-safe: the selection starts empty on the server
|
|
116
|
+
and first client render, then converts post-hydration.
|
|
117
|
+
- **Cart mutation failures are no longer swallowed.** The cart methods
|
|
118
|
+
(`addItem` / `removeItem` / `updateQuantity` / `applyDiscount` /
|
|
119
|
+
`removeDiscount` / `updateNote`) now resolve a `CartMutationResult`
|
|
120
|
+
(`{ ok, status, message }`) instead of `void`. A non-2xx response
|
|
121
|
+
(out-of-stock, validation, 403 CSRF) resolves `{ ok: false, … }` and does NOT
|
|
122
|
+
apply a cart, and `addItem` gates its `add_to_cart` analytics event on `ok`
|
|
123
|
+
so a failed add no longer reports a phantom conversion. Back-compat: callers
|
|
124
|
+
that `await` these and ignore the return are unaffected.
|
|
125
|
+
- **RichText SSR sanitizer hardened.** The DOM-free server path (what ships in
|
|
126
|
+
the first paint before hydration) now (a) strips inline event handlers with a
|
|
127
|
+
broadened separator class `[\s/"'<]` so `<img/onerror=…>` is caught, (b)
|
|
128
|
+
strips dangerous element blocks to a fixpoint so nested/spliced `<script>`
|
|
129
|
+
can't reconstitute after one pass, and (c) drops non-allowlisted `href`/`src`
|
|
130
|
+
URLs (`javascript:` / `data:` / …). The client path remains the structural
|
|
131
|
+
DOMParser allowlist.
|
|
132
|
+
|
|
133
|
+
### Added
|
|
134
|
+
|
|
135
|
+
- `SectionProps` is now generic over its settings —
|
|
136
|
+
`SectionProps<S = Record<string, unknown>>` with `settings: S` — and carries
|
|
137
|
+
`id: string`, `type: string`, `groupId?: string`, so themes can drop their
|
|
138
|
+
hand-rolled per-theme section-props interfaces (`EmpSectionProps`).
|
|
139
|
+
- New exports (via `@numueg/theme-sdk`): `CurrencyContext`, and the types
|
|
140
|
+
`CartMutationResult` and `CartContextValue`.
|
|
141
|
+
|
|
142
|
+
### Changed
|
|
143
|
+
|
|
144
|
+
- **(Semi-breaking, TypeScript only)** `SectionProps` gains REQUIRED `id`/`type`
|
|
145
|
+
and its default `settings` type tightens from `Record<string, any>` to
|
|
146
|
+
`Record<string, unknown>`. Themes that referenced `SectionProps` directly
|
|
147
|
+
(most hand-rolled their own props, so real-world impact is small) may need to
|
|
148
|
+
narrow `settings` access or pass a settings type argument. Runtime behavior is
|
|
149
|
+
unchanged.
|
|
150
|
+
|
|
7
151
|
## [0.3.1] - 2026-06-17
|
|
8
152
|
|
|
9
153
|
### Added
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { createContext, useContext, useMemo, useState, useEffect } from 'react';
|
|
2
|
+
|
|
3
|
+
// src/contexts/index.ts
|
|
4
|
+
var ShopContext = createContext(null);
|
|
5
|
+
var ProductContext = createContext(null);
|
|
6
|
+
var CollectionContext = createContext(null);
|
|
7
|
+
var CartContext = createContext(null);
|
|
8
|
+
var CustomerContext = createContext(null);
|
|
9
|
+
var ThemeSettingsContext = createContext(null);
|
|
10
|
+
var LocalizationContext = createContext(null);
|
|
11
|
+
var PageContext = createContext(null);
|
|
12
|
+
var CurrentTemplateContext = createContext("home");
|
|
13
|
+
var NavigationContext = createContext(
|
|
14
|
+
{}
|
|
15
|
+
);
|
|
16
|
+
var CurrencyContext = createContext(null);
|
|
17
|
+
function useLocalization() {
|
|
18
|
+
const ctx = useContext(LocalizationContext);
|
|
19
|
+
if (!ctx) throw new Error("useLocalization must be used within NuMuProvider");
|
|
20
|
+
return ctx;
|
|
21
|
+
}
|
|
22
|
+
function useDirection() {
|
|
23
|
+
const { direction } = useLocalization();
|
|
24
|
+
return direction;
|
|
25
|
+
}
|
|
26
|
+
function useLocale() {
|
|
27
|
+
const { locale } = useLocalization();
|
|
28
|
+
return locale;
|
|
29
|
+
}
|
|
30
|
+
function useTranslation() {
|
|
31
|
+
const { translations, locale } = useLocalization();
|
|
32
|
+
return {
|
|
33
|
+
t: (key, fallback) => translations[key] || fallback || key,
|
|
34
|
+
locale
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
function useFieldTranslation(entity, field) {
|
|
38
|
+
const { locale } = useLocalization();
|
|
39
|
+
if (!entity) return void 0;
|
|
40
|
+
const translated = entity.attributes && typeof entity.attributes === "object" ? entity.attributes[`${field}_${locale}`] : void 0;
|
|
41
|
+
if (typeof translated === "string" && translated) return translated;
|
|
42
|
+
const base = entity[field];
|
|
43
|
+
if (typeof base === "string") return base;
|
|
44
|
+
if (base == null) return void 0;
|
|
45
|
+
return String(base);
|
|
46
|
+
}
|
|
47
|
+
function useNumberFormat() {
|
|
48
|
+
const { formatNumber } = useLocalization();
|
|
49
|
+
return formatNumber;
|
|
50
|
+
}
|
|
51
|
+
var DEFAULT_PROTOCOL = typeof window !== "undefined" ? window.location.protocol.replace(":", "") : "https";
|
|
52
|
+
function resolveDomain(store) {
|
|
53
|
+
if (store.domain) return store.domain;
|
|
54
|
+
if (store.subdomain) {
|
|
55
|
+
const platform = typeof window !== "undefined" && window.__NUMU_PLATFORM_DOMAIN || "numueg.app";
|
|
56
|
+
return `${store.subdomain}.${platform}`;
|
|
57
|
+
}
|
|
58
|
+
return store.slug;
|
|
59
|
+
}
|
|
60
|
+
function useShop() {
|
|
61
|
+
const ctx = useContext(ShopContext);
|
|
62
|
+
if (!ctx) throw new Error("useShop must be used within NuMuProvider");
|
|
63
|
+
const locale = useLocale();
|
|
64
|
+
return useMemo(() => {
|
|
65
|
+
const domain = resolveDomain(ctx);
|
|
66
|
+
const settings = ctx.settings;
|
|
67
|
+
const localePrefixEnabled = Boolean(
|
|
68
|
+
settings && settings.locale_url_prefix_enabled
|
|
69
|
+
);
|
|
70
|
+
const defaultLocale = ctx.default_language || "en";
|
|
71
|
+
const prefixedLocales = settings?.locale_url_prefix_locales || null;
|
|
72
|
+
const shouldPrefix = (l) => {
|
|
73
|
+
if (!localePrefixEnabled) return false;
|
|
74
|
+
if (prefixedLocales) return prefixedLocales.includes(l);
|
|
75
|
+
return l !== defaultLocale;
|
|
76
|
+
};
|
|
77
|
+
const formatUrl = (path) => {
|
|
78
|
+
if (!path) return `${DEFAULT_PROTOCOL}://${domain}/`;
|
|
79
|
+
if (/^https?:\/\//i.test(path)) return path;
|
|
80
|
+
if (path.startsWith("//")) return `${DEFAULT_PROTOCOL}:${path}`;
|
|
81
|
+
let normalized = path.startsWith("/") ? path : `/${path}`;
|
|
82
|
+
if (locale && shouldPrefix(locale)) {
|
|
83
|
+
const prefix = `/${locale}/`;
|
|
84
|
+
const root = `/${locale}`;
|
|
85
|
+
if (normalized !== root && !normalized.startsWith(prefix)) {
|
|
86
|
+
normalized = `${root}${normalized}`;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return `${DEFAULT_PROTOCOL}://${domain}${normalized}`;
|
|
90
|
+
};
|
|
91
|
+
return { ...ctx, domain, formatUrl };
|
|
92
|
+
}, [ctx, locale]);
|
|
93
|
+
}
|
|
94
|
+
function useCustomer() {
|
|
95
|
+
return useContext(CustomerContext);
|
|
96
|
+
}
|
|
97
|
+
function useThemeSettings() {
|
|
98
|
+
const ctx = useContext(ThemeSettingsContext);
|
|
99
|
+
if (!ctx) throw new Error("useThemeSettings must be used within NuMuProvider");
|
|
100
|
+
return ctx;
|
|
101
|
+
}
|
|
102
|
+
function usePage() {
|
|
103
|
+
return useContext(PageContext);
|
|
104
|
+
}
|
|
105
|
+
function useProducts(opts = {}) {
|
|
106
|
+
const { limit, fetchIfMissing = false } = opts;
|
|
107
|
+
const page = usePage();
|
|
108
|
+
const shop = useShop();
|
|
109
|
+
const initial = page?.data?.products ?? null;
|
|
110
|
+
const [products, setProducts] = useState(initial ?? []);
|
|
111
|
+
const [loading, setLoading] = useState(
|
|
112
|
+
initial == null && fetchIfMissing
|
|
113
|
+
);
|
|
114
|
+
const [error, setError] = useState(null);
|
|
115
|
+
useEffect(() => {
|
|
116
|
+
if (initial != null) return;
|
|
117
|
+
if (!fetchIfMissing) return;
|
|
118
|
+
if (!shop?.id) return;
|
|
119
|
+
let cancelled = false;
|
|
120
|
+
(async () => {
|
|
121
|
+
try {
|
|
122
|
+
const params = new URLSearchParams({ store_id: shop.id });
|
|
123
|
+
if (limit) params.set("limit", String(limit));
|
|
124
|
+
const res = await fetch(`/api/products?${params.toString()}`);
|
|
125
|
+
if (!res.ok) throw new Error(`/api/products \u2192 ${res.status}`);
|
|
126
|
+
const data = await res.json();
|
|
127
|
+
if (cancelled) return;
|
|
128
|
+
setProducts(data.products ?? []);
|
|
129
|
+
setLoading(false);
|
|
130
|
+
} catch (err) {
|
|
131
|
+
if (cancelled) return;
|
|
132
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
133
|
+
setLoading(false);
|
|
134
|
+
}
|
|
135
|
+
})();
|
|
136
|
+
return () => {
|
|
137
|
+
cancelled = true;
|
|
138
|
+
};
|
|
139
|
+
}, [initial, fetchIfMissing, limit, shop?.id]);
|
|
140
|
+
const sliced = limit != null ? products.slice(0, limit) : products;
|
|
141
|
+
return { products: sliced, loading, error };
|
|
142
|
+
}
|
|
143
|
+
function useCollections(opts = {}) {
|
|
144
|
+
const { limit, fetchIfMissing = false } = opts;
|
|
145
|
+
const page = usePage();
|
|
146
|
+
const shop = useShop();
|
|
147
|
+
const initial = page?.data?.collections ?? null;
|
|
148
|
+
const [collections, setCollections] = useState(initial ?? []);
|
|
149
|
+
const [loading, setLoading] = useState(
|
|
150
|
+
initial == null && fetchIfMissing
|
|
151
|
+
);
|
|
152
|
+
const [error, setError] = useState(null);
|
|
153
|
+
useEffect(() => {
|
|
154
|
+
if (initial != null) return;
|
|
155
|
+
if (!fetchIfMissing) return;
|
|
156
|
+
if (!shop?.id) return;
|
|
157
|
+
let cancelled = false;
|
|
158
|
+
(async () => {
|
|
159
|
+
try {
|
|
160
|
+
const params = new URLSearchParams({ store_id: shop.id });
|
|
161
|
+
const res = await fetch(`/api/collections?${params.toString()}`);
|
|
162
|
+
if (!res.ok) throw new Error(`/api/collections \u2192 ${res.status}`);
|
|
163
|
+
const data = await res.json();
|
|
164
|
+
if (cancelled) return;
|
|
165
|
+
setCollections(data.collections ?? []);
|
|
166
|
+
setLoading(false);
|
|
167
|
+
} catch (err) {
|
|
168
|
+
if (cancelled) return;
|
|
169
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
170
|
+
setLoading(false);
|
|
171
|
+
}
|
|
172
|
+
})();
|
|
173
|
+
return () => {
|
|
174
|
+
cancelled = true;
|
|
175
|
+
};
|
|
176
|
+
}, [initial, fetchIfMissing, shop?.id]);
|
|
177
|
+
const sliced = limit != null ? collections.slice(0, limit) : collections;
|
|
178
|
+
return { collections: sliced, loading, error };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export { CartContext, CollectionContext, CurrencyContext, CurrentTemplateContext, CustomerContext, LocalizationContext, NavigationContext, PageContext, ProductContext, ShopContext, ThemeSettingsContext, useCollections, useCustomer, useDirection, useFieldTranslation, useLocale, useLocalization, useNumberFormat, usePage, useProducts, useShop, useThemeSettings, useTranslation };
|
|
182
|
+
//# sourceMappingURL=chunk-4TMO5Y5W.mjs.map
|
|
183
|
+
//# sourceMappingURL=chunk-4TMO5Y5W.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/contexts/index.ts","../src/hooks/useLocalization.ts","../src/hooks/useShop.ts","../src/hooks/useCustomer.ts","../src/hooks/useThemeSettings.ts","../src/hooks/usePage.ts","../src/hooks/useProducts.ts","../src/hooks/useCollections.ts"],"names":["useContext","useState","useEffect"],"mappings":"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","file":"chunk-4TMO5Y5W.mjs","sourcesContent":["\"use client\";\nimport { createContext } from \"react\";\nimport type { Store, Product, Collection, Cart, Customer, Page } from \"../types/entities\";\nimport type { ThemeSettingsV3 } from \"../types/theme\";\n\nexport interface LocalizationState {\n locale: string;\n direction: \"ltr\" | \"rtl\";\n translations: Record<string, string>;\n formatMoney: (amount: number, currency?: string) => string;\n formatDate: (date: string | Date) => string;\n /**\n * Phase 3.7 — locale-aware number formatter. Routes to either\n * Western (1234) or Arab-Indic (١٢٣٤) digits depending on\n * `store.settings.numerals`. Themes calling formatMoney get the\n * same digit choice automatically; this is for raw counts (\"12 items\").\n */\n formatNumber: (n: number, options?: Intl.NumberFormatOptions) => string;\n /**\n * Phase 3.6 — switch the active locale.\n *\n * Sets the `numu_locale` cookie and triggers a full page reload so\n * the server-rendered layout picks up the new locale (the storefront\n * resolves locale at SSR time from cookie/query). Returns once the\n * cookie is written; the page navigation cancels any pending React\n * work so callers don't need to await.\n */\n setLocale: (next: string) => void;\n /**\n * Phase 3.6 — list of locales the store advertises. Empty when the\n * store hasn't configured a multi-locale catalog. Themes use this\n * to decide whether to render the LocaleSwitcher at all.\n */\n availableLocales: string[];\n}\n\n/**\n * Result of a cart mutation (add / remove / update / discount / note).\n *\n * `ok` mirrors the HTTP result: `false` for any non-2xx response — an\n * out-of-stock or validation rejection, a 403 CSRF failure, etc. — so a\n * theme can surface the failure (toast, inline error) instead of assuming\n * the write landed. `status` is the HTTP status (0 for a network throw that\n * the caller catches); `message` carries the backend's error text when the\n * error body parses.\n *\n * Back-compat: the cart methods previously resolved `Promise<void>`; callers\n * that `await` them and ignore the return keep working unchanged — the\n * resolved value is purely additive.\n */\nexport interface CartMutationResult {\n ok: boolean;\n status: number;\n message?: string;\n}\n\nexport interface CartContextValue {\n cart: Cart;\n addItem: (\n productId: string,\n variantId?: string,\n quantity?: number,\n /** Picker axes ({Color: \"Black\", Size: \"L\"}) — variant_name fallback for\n * products whose variant rows carry no option_values. */\n selectedOptions?: Record<string, string>,\n ) => Promise<CartMutationResult>;\n removeItem: (itemId: string) => Promise<CartMutationResult>;\n updateQuantity: (\n itemId: string,\n quantity: number,\n ) => Promise<CartMutationResult>;\n applyDiscount: (code: string) => Promise<CartMutationResult>;\n removeDiscount: () => Promise<CartMutationResult>;\n updateNote: (note: string) => Promise<CartMutationResult>;\n clearCart: () => Promise<void>;\n loading: boolean;\n}\n\nexport const ShopContext = createContext<Store | null>(null);\nexport const ProductContext = createContext<Product | null>(null);\nexport const CollectionContext = createContext<Collection | null>(null);\nexport const CartContext = createContext<CartContextValue | null>(null);\nexport const CustomerContext = createContext<Customer | null>(null);\nexport const ThemeSettingsContext = createContext<ThemeSettingsV3 | null>(null);\nexport const LocalizationContext = createContext<LocalizationState | null>(null);\nexport const PageContext = createContext<Page | null>(null);\n/**\n * Wave 5 — currently-active template identifier. Mirrors the key inside\n * `themeSettings.templates.<currentTemplate>` so themes can dispatch\n * which section list to render. Hosts set this via `NuMuProvider`'s\n * `currentTemplate` prop (which is passed in by the storefront page\n * component — `app/(store)/[subdomain]/product/[id]/page.tsx` passes\n * \"product\", `cart/page.tsx` passes \"cart\", etc.). Falls back to \"home\".\n */\nexport const CurrentTemplateContext = createContext<string>(\"home\");\n\n/**\n * A merchant-managed navigation menu item, exactly as the storefront\n * menus resolver returns it (`GET /storefront/store/{id}/menus`):\n * bilingual `label`, a pre-resolved `url`, and nested `children`.\n *\n * This is the RAW shape the host injects via `NuMuProvider`'s\n * `navigation` prop. `useNavigation(handle)` localizes it to the\n * display-ready `NavigationItem` (a single `title` string for the\n * active locale).\n */\nexport interface MenuItemData {\n id: string;\n label: Record<string, string>;\n url: string;\n type?: string | null;\n resource_id?: string | null;\n /**\n * §5 hide-page → hide-nav-link. `false` when the item targets a CMS page\n * (`/pages/<handle>`) that is currently unpublished or deleted. The backend\n * menus resolver annotates it; absent/`true` means visible (back-compat).\n */\n target_visible?: boolean;\n children?: MenuItemData[];\n}\n\n/**\n * Phase 2.4 — navigation menus keyed by handle (`main-menu`, `footer`,\n * plus custom), injected by the host from the storefront resolver so a\n * theme's `useNavigation(handle)` resolves without a client round-trip.\n *\n * Defaults to `{}` — an empty map signals \"host provided no menus\", at\n * which point `useNavigation` falls back to its own fetch / a theme's\n * `DEFAULT_NAV`. A present-but-handle-absent map means the menu simply\n * doesn't exist (render nothing / fallback), no fetch attempted.\n */\nexport const NavigationContext = createContext<Record<string, MenuItemData[]>>(\n {},\n);\n\n/**\n * Multi-currency presentment config, as returned by\n * `GET /api/storefront/currencies`. Display-only — the store's *capture*\n * currency never changes mid-session; this just lets visitors browse prices\n * in a currency they recognize.\n */\nexport interface CurrencyConfig {\n base: string;\n default_presentment: string;\n presentment: string[];\n rates: Record<string, string>; // Decimal-as-string\n auto_convert: boolean;\n}\n\n/**\n * The value `useCurrency()` returns and `CurrencyContext` carries.\n *\n * `convert(cents, target?)` returns the converted **cents** in `target`\n * (defaults to `selected`), using the API rates; returns the input unchanged\n * when no rate exists (theme renders base — better than a wrong number).\n */\nexport interface CurrencyState {\n base: string;\n selected: string;\n presentment: string[];\n rates: Record<string, number>;\n autoConvert: boolean;\n loading: boolean;\n setSelected: (currency: string) => void;\n convert: (cents: number, target?: string) => number;\n}\n\n/**\n * Phase 2 (correctness) — multi-currency lifted into a provider context.\n *\n * Previously `useCurrency()` fetched `/api/storefront/currencies` and held\n * the selected currency in per-instance `useState`, so each `<Money>`,\n * `useMoney()` and `<CurrencySwitcher>` owned an INDEPENDENT copy: switching\n * currency in the switcher never reached the price tags, and every consumer\n * re-fetched. `NuMuProvider` now fetches ONCE and publishes a single\n * `CurrencyState` here, so a `<CurrencySwitcher>` change re-renders every\n * `<Money>` on the page without a reload.\n *\n * Null when no provider is present (SSR before hydrate, or a theme mounted\n * outside `NuMuProvider`); `useCurrency()` then falls back to a base-only,\n * no-convert state derived from the store currency so `<Money>` still renders.\n */\nexport const CurrencyContext = createContext<CurrencyState | null>(null);\n","import { useContext } from \"react\";\nimport { LocalizationContext } from \"../contexts\";\nimport type { LocalizationState } from \"../contexts\";\n\nexport function useLocalization(): LocalizationState {\n const ctx = useContext(LocalizationContext);\n if (!ctx) throw new Error(\"useLocalization must be used within NuMuProvider\");\n return ctx;\n}\n\nexport function useDirection(): \"ltr\" | \"rtl\" {\n const { direction } = useLocalization();\n return direction;\n}\n\nexport function useLocale(): string {\n const { locale } = useLocalization();\n return locale;\n}\n\nexport function useTranslation() {\n const { translations, locale } = useLocalization();\n return {\n t: (key: string, fallback?: string) => translations[key] || fallback || key,\n locale,\n };\n}\n\n/**\n * Phase 3.6 — pull a translated field off a domain object.\n *\n * Convention: per-product translations live on `entity.attributes`\n * keyed as `<field>_<locale>` — e.g. `name_ar`, `description_ar`.\n * Backend writes these via the merchant hub when a merchant turns on\n * \"Translations\" for a product. Themes call:\n *\n * const productName = useFieldTranslation(product, \"name\");\n *\n * and get the Arabic name when the active locale is \"ar\", falling\n * back to the English `product.name` otherwise. Works for any object\n * that pairs a base field on the entity with an `attributes` JSONB\n * blob holding the translated variants.\n *\n * Why not put translations on the entity itself? Adding `name_ar`,\n * `description_ar`, `name_he`, etc. as first-class columns means a\n * schema migration every time a merchant enables a new locale. JSONB\n * lets the merchant flip locales on/off in settings without touching\n * the table.\n */\nexport function useFieldTranslation<\n T extends { attributes?: Record<string, unknown> },\n>(entity: T | null | undefined, field: string): string | undefined {\n const { locale } = useLocalization();\n if (!entity) return undefined;\n // English (or whichever default locale the merchant configured) is\n // stored as the base field; non-default locales live on attributes\n // with a `<field>_<locale>` key.\n const translated =\n entity.attributes && typeof entity.attributes === \"object\"\n ? (entity.attributes as Record<string, unknown>)[`${field}_${locale}`]\n : undefined;\n if (typeof translated === \"string\" && translated) return translated;\n // Base field — supports any value type that stringifies sensibly.\n const base = (entity as unknown as Record<string, unknown>)[field];\n if (typeof base === \"string\") return base;\n if (base == null) return undefined;\n return String(base);\n}\n\n/**\n * Phase 3.7 — formatted-number hook.\n *\n * Themes that need to render counts (\"12 items\", \"3 reviews\") should\n * route through this hook so Arab-Indic digits (٠١٢٣٤) vs Western\n * (01234) stay consistent with money + date elsewhere on the page.\n *\n * const fmtNum = useNumberFormat();\n * fmtNum(12) → \"12\" (or \"١٢\" for Arabic stores)\n * fmtNum(12.5, { minimumFractionDigits: 2 }) → \"12.50\" / \"١٢٫٥٠\"\n */\nexport function useNumberFormat() {\n const { formatNumber } = useLocalization();\n return formatNumber;\n}\n","\"use client\";\nimport { useContext, useMemo } from \"react\";\nimport { ShopContext } from \"../contexts\";\nimport { useLocale } from \"./useLocalization\";\nimport type { Store } from \"../types/entities\";\n\n/**\n * Augmented Store shape returned from useShop().\n *\n * Adds a couple of conveniences theme devs reach for constantly:\n * - `domain` — the active hostname (subdomain or custom). Resolved\n * from `store.domain ?? store.subdomain ?? store.slug`.\n * - `formatUrl(path)` — emits a fully-qualified URL using `domain`.\n * Theme code that needs to render absolute URLs (canonical tags,\n * Open Graph, JSON-LD) goes through this so dev/prod / subdomain\n * vs. custom-domain stays correct without each theme reinventing\n * the resolver.\n */\nexport interface ShopWithHelpers extends Store {\n /** Fully-qualified hostname this store currently serves on. */\n domain: string;\n /**\n * Format a path (relative or absolute) as a fully-qualified URL on\n * this store's domain. No-ops when given an already-absolute URL.\n *\n * Phase 6 — when the active locale is non-default and the store\n * has opted into locale URL prefixes, the path is prefixed with\n * `/{locale}/` (e.g. `/ar/products/foo`). Already-prefixed paths\n * are left alone so calling `formatUrl(formatUrl(...))` is safe.\n */\n formatUrl(path: string): string;\n}\n\nconst DEFAULT_PROTOCOL =\n typeof window !== \"undefined\"\n ? window.location.protocol.replace(\":\", \"\")\n : \"https\";\n\nfunction resolveDomain(store: Store): string {\n // Custom domain wins when set; otherwise fall back to the platform\n // subdomain. The slug is a last resort for stores that haven't been\n // assigned either yet (mostly demo stores in dev).\n if (store.domain) return store.domain;\n if (store.subdomain) {\n // The platform-domain default lives on the storefront; theme code\n // doesn't have access to env vars, so we read it off the runtime\n // window object when present and fall back to the well-known\n // production value. Themes can override by rendering with\n // `<meta name=\"numu:platform-domain\" content=\"...\">` if they're\n // hosted on a non-default platform.\n const platform =\n (typeof window !== \"undefined\" &&\n (window as unknown as { __NUMU_PLATFORM_DOMAIN?: string })\n .__NUMU_PLATFORM_DOMAIN) ||\n \"numueg.app\";\n return `${store.subdomain}.${platform}`;\n }\n return store.slug;\n}\n\nexport function useShop(): ShopWithHelpers {\n const ctx = useContext(ShopContext);\n if (!ctx) throw new Error(\"useShop must be used within NuMuProvider\");\n const locale = useLocale();\n\n return useMemo(() => {\n const domain = resolveDomain(ctx);\n const settings = (ctx as Store & { settings?: Record<string, unknown> })\n .settings;\n const localePrefixEnabled = Boolean(\n settings && (settings as { locale_url_prefix_enabled?: boolean }).locale_url_prefix_enabled,\n );\n const defaultLocale = ctx.default_language || \"en\";\n // List of locales that get a URL prefix. Defaults to \"everything\n // except the default\" — but a store can opt into `[\"ar\", \"en\"]`\n // (prefix even the default) if it wants a fully prefixed URL\n // scheme like Shopify does for markets.\n const prefixedLocales =\n (settings as { locale_url_prefix_locales?: string[] } | undefined)\n ?.locale_url_prefix_locales || null;\n const shouldPrefix = (l: string): boolean => {\n if (!localePrefixEnabled) return false;\n if (prefixedLocales) return prefixedLocales.includes(l);\n return l !== defaultLocale;\n };\n\n const formatUrl = (path: string): string => {\n if (!path) return `${DEFAULT_PROTOCOL}://${domain}/`;\n // Already absolute? Hand back as-is.\n if (/^https?:\\/\\//i.test(path)) return path;\n // Protocol-relative — assume same protocol as the host.\n if (path.startsWith(\"//\")) return `${DEFAULT_PROTOCOL}:${path}`;\n let normalized = path.startsWith(\"/\") ? path : `/${path}`;\n if (locale && shouldPrefix(locale)) {\n // Idempotent: don't double-prefix if the caller already\n // gave us a /{locale}/... path.\n const prefix = `/${locale}/`;\n const root = `/${locale}`;\n if (normalized !== root && !normalized.startsWith(prefix)) {\n normalized = `${root}${normalized}`;\n }\n }\n return `${DEFAULT_PROTOCOL}://${domain}${normalized}`;\n };\n return { ...ctx, domain, formatUrl };\n }, [ctx, locale]);\n}\n","import { useContext } from \"react\";\nimport { CustomerContext } from \"../contexts\";\nimport type { Customer } from \"../types/entities\";\n\nexport function useCustomer(): Customer | null {\n return useContext(CustomerContext);\n}\n","import { useContext } from \"react\";\nimport { ThemeSettingsContext } from \"../contexts\";\nimport type { ThemeSettingsV3 } from \"../types/theme\";\n\nexport function useThemeSettings(): ThemeSettingsV3 {\n const ctx = useContext(ThemeSettingsContext);\n if (!ctx) throw new Error(\"useThemeSettings must be used within NuMuProvider\");\n return ctx;\n}\n","import { useContext } from \"react\";\nimport { PageContext } from \"../contexts\";\nimport type { Page } from \"../types/entities\";\n\nexport function usePage(): Page | null {\n return useContext(PageContext);\n}\n","\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport { usePage } from \"./usePage\";\nimport { useShop } from \"./useShop\";\nimport type { Product } from \"../types/entities\";\n\ninterface UseProductsOptions {\n /** Limit the slice returned. Defaults to whatever the host gave us. */\n limit?: number;\n /**\n * When true, fetch from `/api/products` if the host didn't provide a\n * list via PageContext. Useful for sections rendered on routes that\n * don't pre-fetch products (e.g. a custom CMS page that wants a\n * \"featured\" rail). Default: false — themes typically prefer SSR data.\n */\n fetchIfMissing?: boolean;\n}\n\ninterface UseProductsResult {\n products: Product[];\n loading: boolean;\n error: Error | null;\n}\n\n/**\n * useProducts — read the storefront-pre-fetched product list from the\n * page context, optionally falling back to a client-side fetch.\n *\n * The SSR path passes `page.data.products: Product[]` from\n * `numu-storefront/src/app/[domain]/page.tsx`; sections on the home\n * route get them for free. For other routes that don't pre-fetch, set\n * `fetchIfMissing: true` and we'll hit `/api/products`.\n */\nexport function useProducts(\n opts: UseProductsOptions = {},\n): UseProductsResult {\n const { limit, fetchIfMissing = false } = opts;\n const page = usePage();\n const shop = useShop();\n const initial = (page?.data?.products as Product[] | undefined) ?? null;\n\n const [products, setProducts] = useState<Product[]>(initial ?? []);\n const [loading, setLoading] = useState<boolean>(\n initial == null && fetchIfMissing,\n );\n const [error, setError] = useState<Error | null>(null);\n\n useEffect(() => {\n if (initial != null) return;\n if (!fetchIfMissing) return;\n if (!shop?.id) return;\n let cancelled = false;\n (async () => {\n try {\n const params = new URLSearchParams({ store_id: shop.id });\n if (limit) params.set(\"limit\", String(limit));\n const res = await fetch(`/api/products?${params.toString()}`);\n if (!res.ok) throw new Error(`/api/products → ${res.status}`);\n const data = (await res.json()) as { products?: Product[] };\n if (cancelled) return;\n setProducts(data.products ?? []);\n setLoading(false);\n } catch (err) {\n if (cancelled) return;\n setError(err instanceof Error ? err : new Error(String(err)));\n setLoading(false);\n }\n })();\n return () => {\n cancelled = true;\n };\n }, [initial, fetchIfMissing, limit, shop?.id]);\n\n const sliced = limit != null ? products.slice(0, limit) : products;\n return { products: sliced, loading, error };\n}\n","\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport { usePage } from \"./usePage\";\nimport { useShop } from \"./useShop\";\nimport type { Collection } from \"../types/entities\";\n\ninterface UseCollectionsOptions {\n limit?: number;\n fetchIfMissing?: boolean;\n}\n\ninterface UseCollectionsResult {\n collections: Collection[];\n loading: boolean;\n error: Error | null;\n}\n\n/**\n * useCollections — analog to useProducts. Reads page.data.collections\n * pre-fetched by the storefront SSR; falls back to /api/collections\n * when fetchIfMissing is true.\n */\nexport function useCollections(\n opts: UseCollectionsOptions = {},\n): UseCollectionsResult {\n const { limit, fetchIfMissing = false } = opts;\n const page = usePage();\n const shop = useShop();\n const initial = (page?.data?.collections as Collection[] | undefined) ?? null;\n\n const [collections, setCollections] = useState<Collection[]>(initial ?? []);\n const [loading, setLoading] = useState<boolean>(\n initial == null && fetchIfMissing,\n );\n const [error, setError] = useState<Error | null>(null);\n\n useEffect(() => {\n if (initial != null) return;\n if (!fetchIfMissing) return;\n if (!shop?.id) return;\n let cancelled = false;\n (async () => {\n try {\n const params = new URLSearchParams({ store_id: shop.id });\n const res = await fetch(`/api/collections?${params.toString()}`);\n if (!res.ok) throw new Error(`/api/collections → ${res.status}`);\n const data = (await res.json()) as { collections?: Collection[] };\n if (cancelled) return;\n setCollections(data.collections ?? []);\n setLoading(false);\n } catch (err) {\n if (cancelled) return;\n setError(err instanceof Error ? err : new Error(String(err)));\n setLoading(false);\n }\n })();\n return () => {\n cancelled = true;\n };\n }, [initial, fetchIfMissing, shop?.id]);\n\n const sliced = limit != null ? collections.slice(0, limit) : collections;\n return { collections: sliced, loading, error };\n}\n"]}
|