@numueg/theme-sdk 0.2.2 → 0.2.3
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/README.md +8 -8
- package/dist/{entities-iiuRSPpk.d.mts → entities-C8B2U-V0.d.mts} +19 -0
- package/dist/{entities-iiuRSPpk.d.ts → entities-C8B2U-V0.d.ts} +19 -0
- package/dist/index.cjs +117 -27
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +100 -44
- package/dist/index.d.ts +100 -44
- package/dist/index.mjs +116 -28
- package/dist/index.mjs.map +1 -1
- package/dist/types.d.mts +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/v2-compat.d.mts +1 -1
- package/dist/v2-compat.d.ts +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
[](https://www.npmjs.com/package/@numueg/theme-sdk)
|
|
6
6
|
[](./LICENSE)
|
|
7
7
|
|
|
8
|
-
The SDK every NUMU theme consumes.
|
|
8
|
+
The SDK every NUMU theme consumes. The `mountTheme()` runtime helper, 27+ hooks (`useCart`, `useProduct`, `useCheckout`, `useVariantSelection`, `useCurrentTemplate`, `useResolvedSettings`, …), 15+ components (`AddToCartButton`, `ProductCard`, `Money`, `Section`, `EditableText`/`EditableImage`, `NuMuProvider`, …), and helpers for variant resolution, focal image crops (`focalSrc`), global style tokens, asset URLs, and federation singletons.
|
|
9
9
|
|
|
10
10
|
Themes import via the bare specifier `@numueg/theme-sdk` — at runtime the storefront's import map resolves it to the host-loaded singleton so every theme on the platform shares one React identity.
|
|
11
11
|
|
|
@@ -31,23 +31,23 @@ The `@numueg/theme-plugin` Vite plugin does this for you automatically.
|
|
|
31
31
|
|
|
32
32
|
## Usage
|
|
33
33
|
|
|
34
|
+
The host storefront calls your bundle's `mount(el, ctx)` where `ctx = { storeData, page, themeSettings, locale, … }`. Use `mountTheme()` — it wires catalog data, global style tokens, navigation, and live-edit updates for you and returns the `{ unmount, update }` contract the host expects:
|
|
35
|
+
|
|
34
36
|
```tsx
|
|
35
|
-
import {
|
|
36
|
-
import { NuMuProvider, useCart, AddToCartButton } from "@numueg/theme-sdk";
|
|
37
|
-
import type { MountContext } from "@numueg/theme-sdk";
|
|
37
|
+
import { mountTheme, useCart } from "@numueg/theme-sdk";
|
|
38
38
|
|
|
39
39
|
function App() {
|
|
40
40
|
const { cart } = useCart();
|
|
41
41
|
return <p>Items in cart: {cart.item_count}</p>;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
export function mount(ctx:
|
|
45
|
-
|
|
46
|
-
root.render(<NuMuProvider {...ctx}><App /></NuMuProvider>);
|
|
47
|
-
return () => root.unmount();
|
|
44
|
+
export function mount(el: HTMLElement, ctx: unknown) {
|
|
45
|
+
return mountTheme(el, ctx, () => <App />);
|
|
48
46
|
}
|
|
49
47
|
```
|
|
50
48
|
|
|
49
|
+
> The authoritative `ctx` shape is defined by the host's `ByotThemeBoundary` (numu-storefront), not by SDK types — accept it as opaque and let `mountTheme`/`NuMuProvider` normalize it.
|
|
50
|
+
|
|
51
51
|
## Docs
|
|
52
52
|
|
|
53
53
|
- [SDK Overview](https://numueg.app/docs/sdk/overview)
|
|
@@ -138,6 +138,25 @@ interface Order {
|
|
|
138
138
|
items: OrderItem[];
|
|
139
139
|
created_at: string;
|
|
140
140
|
shipping_address?: Address;
|
|
141
|
+
/**
|
|
142
|
+
* Discount code the customer applied at checkout, if any. Sourced from
|
|
143
|
+
* the backend order-detail (`GET /storefront/me/orders/{id}`). Themes
|
|
144
|
+
* render this on the order-confirmation / order-detail page.
|
|
145
|
+
*/
|
|
146
|
+
coupon_code?: string | null;
|
|
147
|
+
/**
|
|
148
|
+
* Automatic offers (offers-v2 promotions) applied to the order. Each
|
|
149
|
+
* entry carries the localized title and the discount it contributed.
|
|
150
|
+
* `amount` is in integer cents — divide by 100 (or pass through
|
|
151
|
+
* `<Money>`) before display, consistent with the rest of the order's
|
|
152
|
+
* money fields.
|
|
153
|
+
*/
|
|
154
|
+
applied_promotions?: {
|
|
155
|
+
id: string;
|
|
156
|
+
title: string;
|
|
157
|
+
title_ar?: string;
|
|
158
|
+
amount: number;
|
|
159
|
+
}[];
|
|
141
160
|
}
|
|
142
161
|
interface OrderItem {
|
|
143
162
|
product_id: string;
|
|
@@ -138,6 +138,25 @@ interface Order {
|
|
|
138
138
|
items: OrderItem[];
|
|
139
139
|
created_at: string;
|
|
140
140
|
shipping_address?: Address;
|
|
141
|
+
/**
|
|
142
|
+
* Discount code the customer applied at checkout, if any. Sourced from
|
|
143
|
+
* the backend order-detail (`GET /storefront/me/orders/{id}`). Themes
|
|
144
|
+
* render this on the order-confirmation / order-detail page.
|
|
145
|
+
*/
|
|
146
|
+
coupon_code?: string | null;
|
|
147
|
+
/**
|
|
148
|
+
* Automatic offers (offers-v2 promotions) applied to the order. Each
|
|
149
|
+
* entry carries the localized title and the discount it contributed.
|
|
150
|
+
* `amount` is in integer cents — divide by 100 (or pass through
|
|
151
|
+
* `<Money>`) before display, consistent with the rest of the order's
|
|
152
|
+
* money fields.
|
|
153
|
+
*/
|
|
154
|
+
applied_promotions?: {
|
|
155
|
+
id: string;
|
|
156
|
+
title: string;
|
|
157
|
+
title_ar?: string;
|
|
158
|
+
amount: number;
|
|
159
|
+
}[];
|
|
141
160
|
}
|
|
142
161
|
interface OrderItem {
|
|
143
162
|
product_id: string;
|
package/dist/index.cjs
CHANGED
|
@@ -1457,6 +1457,12 @@ function normalizeCartFromServer(cart) {
|
|
|
1457
1457
|
items: Array.isArray(cart.items) ? cart.items.map((it) => ({ ...it, price: toMajor(it.price) })) : []
|
|
1458
1458
|
};
|
|
1459
1459
|
}
|
|
1460
|
+
function unwrapCart(json) {
|
|
1461
|
+
if (json && typeof json === "object" && "data" in json && json.data && typeof json.data === "object") {
|
|
1462
|
+
return json.data;
|
|
1463
|
+
}
|
|
1464
|
+
return json;
|
|
1465
|
+
}
|
|
1460
1466
|
function readCsrfCookie() {
|
|
1461
1467
|
if (typeof document === "undefined") return null;
|
|
1462
1468
|
const match = document.cookie.match(/(?:^|;\s*)numu_csrf=([^;]+)/);
|
|
@@ -1476,8 +1482,8 @@ async function postCartMutation(endpoint, body, applyCart, reserveToken) {
|
|
|
1476
1482
|
body: body === void 0 ? void 0 : JSON.stringify(body)
|
|
1477
1483
|
});
|
|
1478
1484
|
if (!res.ok) return;
|
|
1479
|
-
const
|
|
1480
|
-
applyCart(
|
|
1485
|
+
const json = await res.json();
|
|
1486
|
+
applyCart(unwrapCart(json));
|
|
1481
1487
|
}
|
|
1482
1488
|
function NuMuProvider({
|
|
1483
1489
|
store,
|
|
@@ -1657,7 +1663,8 @@ function NuMuProvider({
|
|
|
1657
1663
|
cache: "no-store"
|
|
1658
1664
|
});
|
|
1659
1665
|
if (!res.ok || cancelled) return;
|
|
1660
|
-
const
|
|
1666
|
+
const json = await res.json();
|
|
1667
|
+
const data = unwrapCart(json);
|
|
1661
1668
|
if (data && typeof data === "object") {
|
|
1662
1669
|
setCart(normalizeCartFromServer(data));
|
|
1663
1670
|
}
|
|
@@ -1704,11 +1711,32 @@ function NuMuProvider({
|
|
|
1704
1711
|
);
|
|
1705
1712
|
const addItem = react.useCallback(
|
|
1706
1713
|
async (productId, variantId, quantity) => {
|
|
1714
|
+
const eventId = typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : `${Date.now()}`;
|
|
1715
|
+
const qty = quantity || 1;
|
|
1707
1716
|
await mutate("/api/cart/add", {
|
|
1708
1717
|
product_id: productId,
|
|
1709
1718
|
variant_id: variantId,
|
|
1710
|
-
quantity:
|
|
1719
|
+
quantity: qty,
|
|
1720
|
+
_event_id: eventId
|
|
1711
1721
|
});
|
|
1722
|
+
try {
|
|
1723
|
+
if (typeof window !== "undefined") {
|
|
1724
|
+
window.dispatchEvent(
|
|
1725
|
+
new CustomEvent("numu:analytics:event", {
|
|
1726
|
+
detail: {
|
|
1727
|
+
event: "add_to_cart",
|
|
1728
|
+
payload: {
|
|
1729
|
+
content_ids: [productId],
|
|
1730
|
+
content_type: "product",
|
|
1731
|
+
num_items: qty
|
|
1732
|
+
},
|
|
1733
|
+
event_id: eventId
|
|
1734
|
+
}
|
|
1735
|
+
})
|
|
1736
|
+
);
|
|
1737
|
+
}
|
|
1738
|
+
} catch {
|
|
1739
|
+
}
|
|
1712
1740
|
},
|
|
1713
1741
|
[mutate]
|
|
1714
1742
|
);
|
|
@@ -2133,6 +2161,46 @@ function Money({
|
|
|
2133
2161
|
children
|
|
2134
2162
|
);
|
|
2135
2163
|
}
|
|
2164
|
+
|
|
2165
|
+
// src/utils/imageTransform.ts
|
|
2166
|
+
var _clampT = (n, lo, hi) => Math.min(hi, Math.max(lo, Number.isFinite(n) ? n : lo));
|
|
2167
|
+
function asImageTransform(v) {
|
|
2168
|
+
if (v && typeof v === "object" && "transform" in v) {
|
|
2169
|
+
const t = v.transform;
|
|
2170
|
+
if (t && typeof t === "object") return t;
|
|
2171
|
+
}
|
|
2172
|
+
return void 0;
|
|
2173
|
+
}
|
|
2174
|
+
function applyImageTransform(t, fit = "cover") {
|
|
2175
|
+
if (!t) return { objectFit: fit };
|
|
2176
|
+
const fx = Math.round(_clampT(t.focal?.x ?? 0.5, 0, 1) * 1e4) / 100;
|
|
2177
|
+
const fy = Math.round(_clampT(t.focal?.y ?? 0.5, 0, 1) * 1e4) / 100;
|
|
2178
|
+
const zoom = _clampT(t.zoom ?? 1, 1, 4);
|
|
2179
|
+
const rot = ((t.rotation ?? 0) % 360 + 360) % 360;
|
|
2180
|
+
const effFit = t.fit ?? fit;
|
|
2181
|
+
const style = {
|
|
2182
|
+
transform: `scale(${zoom}) rotate(${rot}deg)`,
|
|
2183
|
+
transformOrigin: `${fx}% ${fy}%`,
|
|
2184
|
+
objectFit: effFit
|
|
2185
|
+
};
|
|
2186
|
+
if (effFit === "cover") style.objectPosition = `${fx}% ${fy}%`;
|
|
2187
|
+
return style;
|
|
2188
|
+
}
|
|
2189
|
+
var clamp01 = (n) => Math.min(1, Math.max(0, n));
|
|
2190
|
+
function focalSrc(url, options = {}) {
|
|
2191
|
+
if (!url) return "";
|
|
2192
|
+
if (url.startsWith("data:") || /[?&](fp-x|fp-y)=/.test(url)) return url;
|
|
2193
|
+
const p = new URLSearchParams();
|
|
2194
|
+
p.set("url", url);
|
|
2195
|
+
if (options.width) p.set("w", String(Math.round(options.width)));
|
|
2196
|
+
if (options.focal?.x != null) p.set("fp-x", String(clamp01(options.focal.x)));
|
|
2197
|
+
if (options.focal?.y != null) p.set("fp-y", String(clamp01(options.focal.y)));
|
|
2198
|
+
if (options.aspect) p.set("ar", options.aspect);
|
|
2199
|
+
if (options.fit) p.set("fit", options.fit);
|
|
2200
|
+
if (options.quality) p.set("q", String(Math.min(100, Math.max(1, Math.round(options.quality)))));
|
|
2201
|
+
if (options.format) p.set("f", options.format.toLowerCase());
|
|
2202
|
+
return `/api/image-transform?${p.toString()}`;
|
|
2203
|
+
}
|
|
2136
2204
|
var DEFAULT_WIDTHS2 = [320, 480, 640, 768, 1024, 1280, 1600, 1920];
|
|
2137
2205
|
function buildSrcSet(src, widths = DEFAULT_WIDTHS2) {
|
|
2138
2206
|
if (/[?&]w=\d+/.test(src)) return "";
|
|
@@ -2145,27 +2213,48 @@ function Image({
|
|
|
2145
2213
|
sizes = "(min-width: 1024px) 25vw, (min-width: 640px) 50vw, 100vw",
|
|
2146
2214
|
responsive = true,
|
|
2147
2215
|
loading = "lazy",
|
|
2216
|
+
aspectRatio,
|
|
2217
|
+
objectFit,
|
|
2218
|
+
objectPosition,
|
|
2219
|
+
transform,
|
|
2148
2220
|
className,
|
|
2149
2221
|
style,
|
|
2150
2222
|
...rest
|
|
2151
2223
|
}) {
|
|
2224
|
+
const framed = Boolean(aspectRatio);
|
|
2152
2225
|
if (!src) {
|
|
2153
|
-
|
|
2226
|
+
const placeholder = /* @__PURE__ */ jsxRuntime.jsx(
|
|
2154
2227
|
"div",
|
|
2155
2228
|
{
|
|
2156
|
-
className,
|
|
2229
|
+
className: framed ? void 0 : className,
|
|
2157
2230
|
role: "img",
|
|
2158
2231
|
"aria-label": alt,
|
|
2159
2232
|
style: {
|
|
2160
2233
|
backgroundColor: "rgba(0,0,0,0.05)",
|
|
2161
2234
|
display: "block",
|
|
2162
|
-
|
|
2235
|
+
width: "100%",
|
|
2236
|
+
height: framed ? "100%" : void 0,
|
|
2237
|
+
...framed ? {} : style
|
|
2163
2238
|
}
|
|
2164
2239
|
}
|
|
2165
2240
|
);
|
|
2241
|
+
if (!framed) return placeholder;
|
|
2242
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
2243
|
+
"span",
|
|
2244
|
+
{
|
|
2245
|
+
className,
|
|
2246
|
+
style: { display: "block", aspectRatio, overflow: "hidden", ...style },
|
|
2247
|
+
children: placeholder
|
|
2248
|
+
}
|
|
2249
|
+
);
|
|
2166
2250
|
}
|
|
2167
2251
|
const srcSet = responsive ? buildSrcSet(src) : void 0;
|
|
2168
|
-
|
|
2252
|
+
const effFit = objectFit ?? (framed ? "cover" : void 0);
|
|
2253
|
+
const fitStyle = transform ? applyImageTransform(transform, effFit === "contain" ? "contain" : "cover") : {
|
|
2254
|
+
...effFit ? { objectFit: effFit } : {},
|
|
2255
|
+
...objectPosition ? { objectPosition } : {}
|
|
2256
|
+
};
|
|
2257
|
+
const img = /* @__PURE__ */ jsxRuntime.jsx(
|
|
2169
2258
|
"img",
|
|
2170
2259
|
{
|
|
2171
2260
|
src,
|
|
@@ -2174,11 +2263,27 @@ function Image({
|
|
|
2174
2263
|
sizes: srcSet ? sizes : void 0,
|
|
2175
2264
|
loading,
|
|
2176
2265
|
decoding: "async",
|
|
2177
|
-
className,
|
|
2178
|
-
style,
|
|
2266
|
+
className: framed ? void 0 : className,
|
|
2267
|
+
style: framed ? { width: "100%", height: "100%", display: "block", ...fitStyle } : { ...fitStyle, ...style },
|
|
2179
2268
|
...rest
|
|
2180
2269
|
}
|
|
2181
2270
|
);
|
|
2271
|
+
if (!framed) return img;
|
|
2272
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
2273
|
+
"span",
|
|
2274
|
+
{
|
|
2275
|
+
className,
|
|
2276
|
+
style: {
|
|
2277
|
+
display: "block",
|
|
2278
|
+
position: "relative",
|
|
2279
|
+
width: "100%",
|
|
2280
|
+
aspectRatio,
|
|
2281
|
+
overflow: "hidden",
|
|
2282
|
+
...style
|
|
2283
|
+
},
|
|
2284
|
+
children: img
|
|
2285
|
+
}
|
|
2286
|
+
);
|
|
2182
2287
|
}
|
|
2183
2288
|
var ABSOLUTE_URL = /^[a-z]+:|^\/\//i;
|
|
2184
2289
|
function Link({ to, children, ...rest }) {
|
|
@@ -3334,23 +3439,6 @@ function assetUrl(name) {
|
|
|
3334
3439
|
return `${cleanBase}${filename}`;
|
|
3335
3440
|
}
|
|
3336
3441
|
|
|
3337
|
-
// src/utils/imageTransform.ts
|
|
3338
|
-
var clamp01 = (n) => Math.min(1, Math.max(0, n));
|
|
3339
|
-
function focalSrc(url, options = {}) {
|
|
3340
|
-
if (!url) return "";
|
|
3341
|
-
if (url.startsWith("data:") || /[?&](fp-x|fp-y)=/.test(url)) return url;
|
|
3342
|
-
const p = new URLSearchParams();
|
|
3343
|
-
p.set("url", url);
|
|
3344
|
-
if (options.width) p.set("w", String(Math.round(options.width)));
|
|
3345
|
-
if (options.focal?.x != null) p.set("fp-x", String(clamp01(options.focal.x)));
|
|
3346
|
-
if (options.focal?.y != null) p.set("fp-y", String(clamp01(options.focal.y)));
|
|
3347
|
-
if (options.aspect) p.set("ar", options.aspect);
|
|
3348
|
-
if (options.fit) p.set("fit", options.fit);
|
|
3349
|
-
if (options.quality) p.set("q", String(Math.min(100, Math.max(1, Math.round(options.quality)))));
|
|
3350
|
-
if (options.format) p.set("f", options.format.toLowerCase());
|
|
3351
|
-
return `/api/image-transform?${p.toString()}`;
|
|
3352
|
-
}
|
|
3353
|
-
|
|
3354
3442
|
// src/utils/locales.ts
|
|
3355
3443
|
function flattenMessages(source, prefix = "") {
|
|
3356
3444
|
const out = {};
|
|
@@ -3427,6 +3515,8 @@ exports.SectionContext = SectionContext;
|
|
|
3427
3515
|
exports.ShopContext = ShopContext;
|
|
3428
3516
|
exports.ThemeSettingsContext = ThemeSettingsContext;
|
|
3429
3517
|
exports.applyGlobalStyleTokens = applyGlobalStyleTokens;
|
|
3518
|
+
exports.applyImageTransform = applyImageTransform;
|
|
3519
|
+
exports.asImageTransform = asImageTransform;
|
|
3430
3520
|
exports.assetUrl = assetUrl;
|
|
3431
3521
|
exports.availableValues = availableValues;
|
|
3432
3522
|
exports.buildLocaleBundle = buildLocaleBundle;
|