@numueg/theme-sdk 0.2.2 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -942,6 +942,29 @@ function useRelatedProducts(productId, options = {}) {
942
942
  }, [productId, limit]);
943
943
  return { items, loading, error };
944
944
  }
945
+ function useProductSizeChart(productOverride) {
946
+ const ctxProduct = useProductOptional();
947
+ const product = productOverride ?? ctxProduct;
948
+ const shop = useShop();
949
+ const storeSettings = shop?.settings;
950
+ return react.useMemo(
951
+ () => resolveSizeChart(product?.attributes, storeSettings),
952
+ [product?.attributes, storeSettings]
953
+ );
954
+ }
955
+ function hasRows(c) {
956
+ return !!c && typeof c === "object" && Array.isArray(c.rows) && c.rows.length > 0;
957
+ }
958
+ function resolveSizeChart(productAttributes, storeSettings) {
959
+ const product = productAttributes?.size_chart;
960
+ const storeDefault = storeSettings?.size_chart;
961
+ if (product?.mode === "off") return null;
962
+ if (product?.mode === "custom") return hasRows(product) ? product : null;
963
+ if (product?.mode === "default") return hasRows(storeDefault) ? storeDefault : null;
964
+ if (hasRows(product)) return product;
965
+ if (hasRows(storeDefault)) return storeDefault;
966
+ return null;
967
+ }
945
968
  var COOKIE_NAME = "numu_currency";
946
969
  var COOKIE_MAX_AGE = 60 * 60 * 24 * 30;
947
970
  function readCookie(name) {
@@ -1457,6 +1480,12 @@ function normalizeCartFromServer(cart) {
1457
1480
  items: Array.isArray(cart.items) ? cart.items.map((it) => ({ ...it, price: toMajor(it.price) })) : []
1458
1481
  };
1459
1482
  }
1483
+ function unwrapCart(json) {
1484
+ if (json && typeof json === "object" && "data" in json && json.data && typeof json.data === "object") {
1485
+ return json.data;
1486
+ }
1487
+ return json;
1488
+ }
1460
1489
  function readCsrfCookie() {
1461
1490
  if (typeof document === "undefined") return null;
1462
1491
  const match = document.cookie.match(/(?:^|;\s*)numu_csrf=([^;]+)/);
@@ -1476,8 +1505,8 @@ async function postCartMutation(endpoint, body, applyCart, reserveToken) {
1476
1505
  body: body === void 0 ? void 0 : JSON.stringify(body)
1477
1506
  });
1478
1507
  if (!res.ok) return;
1479
- const data = await res.json();
1480
- applyCart(data);
1508
+ const json = await res.json();
1509
+ applyCart(unwrapCart(json));
1481
1510
  }
1482
1511
  function NuMuProvider({
1483
1512
  store,
@@ -1657,7 +1686,8 @@ function NuMuProvider({
1657
1686
  cache: "no-store"
1658
1687
  });
1659
1688
  if (!res.ok || cancelled) return;
1660
- const data = await res.json();
1689
+ const json = await res.json();
1690
+ const data = unwrapCart(json);
1661
1691
  if (data && typeof data === "object") {
1662
1692
  setCart(normalizeCartFromServer(data));
1663
1693
  }
@@ -1704,11 +1734,32 @@ function NuMuProvider({
1704
1734
  );
1705
1735
  const addItem = react.useCallback(
1706
1736
  async (productId, variantId, quantity) => {
1737
+ const eventId = typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : `${Date.now()}`;
1738
+ const qty = quantity || 1;
1707
1739
  await mutate("/api/cart/add", {
1708
1740
  product_id: productId,
1709
1741
  variant_id: variantId,
1710
- quantity: quantity || 1
1742
+ quantity: qty,
1743
+ _event_id: eventId
1711
1744
  });
1745
+ try {
1746
+ if (typeof window !== "undefined") {
1747
+ window.dispatchEvent(
1748
+ new CustomEvent("numu:analytics:event", {
1749
+ detail: {
1750
+ event: "add_to_cart",
1751
+ payload: {
1752
+ content_ids: [productId],
1753
+ content_type: "product",
1754
+ num_items: qty
1755
+ },
1756
+ event_id: eventId
1757
+ }
1758
+ })
1759
+ );
1760
+ }
1761
+ } catch {
1762
+ }
1712
1763
  },
1713
1764
  [mutate]
1714
1765
  );
@@ -1963,6 +2014,9 @@ function injectFontLink(href) {
1963
2014
  link.setAttribute("data-numu-font", "");
1964
2015
  document.head.appendChild(link);
1965
2016
  }
2017
+ function lookupFontStack(value) {
2018
+ return FONT_REGISTRY[value]?.stack ?? value;
2019
+ }
1966
2020
  function resolveFontStack(value) {
1967
2021
  const entry = FONT_REGISTRY[value];
1968
2022
  if (entry) {
@@ -1971,39 +2025,69 @@ function resolveFontStack(value) {
1971
2025
  }
1972
2026
  return value;
1973
2027
  }
1974
- function applyGlobalStyleTokens(globalSettings, el) {
1975
- if (!el || !globalSettings || typeof globalSettings !== "object") return;
1976
- const style = el.style;
2028
+ function computeGlobalStyleTokens(globalSettings) {
2029
+ const cssVars = {};
2030
+ const fontHrefs = [];
2031
+ if (!globalSettings || typeof globalSettings !== "object") {
2032
+ return { cssVars, fontHrefs };
2033
+ }
2034
+ const pushHref = (href) => {
2035
+ if (href && !fontHrefs.includes(href)) fontHrefs.push(href);
2036
+ };
1977
2037
  for (const [key, value] of Object.entries(globalSettings)) {
1978
2038
  if (!key || key.startsWith("__")) continue;
1979
2039
  if (value && typeof value === "object" && !Array.isArray(value)) {
1980
2040
  for (const [role, c] of Object.entries(value)) {
1981
- if (isColorValue(c)) style.setProperty(`--scheme-${key}-${role}`, c);
2041
+ if (isColorValue(c)) cssVars[`--scheme-${key}-${role}`] = c;
1982
2042
  }
1983
2043
  continue;
1984
2044
  }
1985
2045
  if (isColorValue(value)) {
1986
- style.setProperty(`--theme-${key}`, value.trim());
2046
+ cssVars[`--theme-${key}`] = value.trim();
1987
2047
  const role = COLOR_ROLE_ALIASES[key];
1988
- if (role) style.setProperty(`--theme-color-${role}`, value.trim());
2048
+ if (role) cssVars[`--theme-color-${role}`] = value.trim();
1989
2049
  continue;
1990
2050
  }
1991
2051
  if (isFontToken(value)) {
1992
- const stack = resolveFontStack(value);
1993
- style.setProperty(`--theme-${key}`, stack);
2052
+ const entry = FONT_REGISTRY[value];
2053
+ cssVars[`--theme-${key}`] = entry.stack;
1994
2054
  const role = FONT_ROLE_ALIASES[key];
1995
- if (role) style.setProperty(`--theme-font-${role}`, stack);
2055
+ if (role) cssVars[`--theme-font-${role}`] = entry.stack;
2056
+ pushHref(entry.href);
1996
2057
  continue;
1997
2058
  }
1998
2059
  if (typeof value === "string" || typeof value === "number") {
1999
2060
  const v = String(value).trim();
2000
- if (v) style.setProperty(`--theme-${key}`, v);
2061
+ if (v) cssVars[`--theme-${key}`] = v;
2062
+ }
2063
+ }
2064
+ for (const id of ["heading_font", "body_font"]) {
2065
+ const value = globalSettings[id];
2066
+ if (typeof value === "string" && value.trim()) {
2067
+ cssVars[`--theme-${id}`] = lookupFontStack(value);
2068
+ pushHref(FONT_REGISTRY[value]?.href);
2001
2069
  }
2002
2070
  }
2071
+ return { cssVars, fontHrefs };
2072
+ }
2073
+ function applyGlobalStyleTokens(globalSettings, el) {
2074
+ if (!el || !globalSettings || typeof globalSettings !== "object") return;
2075
+ const { cssVars, fontHrefs } = computeGlobalStyleTokens(globalSettings);
2076
+ const style = el.style;
2077
+ for (const [prop, value] of Object.entries(cssVars)) {
2078
+ style.setProperty(prop, value);
2079
+ }
2080
+ for (const href of fontHrefs) injectFontLink(href);
2003
2081
  }
2004
2082
  function pickStore(ctx) {
2005
2083
  const s = ctx.storeData ?? ctx.store;
2006
- if (s) return s;
2084
+ if (s) {
2085
+ const raw = s;
2086
+ if (!raw.currency && raw.default_currency) {
2087
+ return { ...raw, currency: raw.default_currency };
2088
+ }
2089
+ return s;
2090
+ }
2007
2091
  return {
2008
2092
  id: "unknown",
2009
2093
  name: "Store",
@@ -2038,19 +2122,9 @@ var ThemeMountBridge = react.forwardRef(function ThemeMountBridge2({ ctx, mountE
2038
2122
  []
2039
2123
  );
2040
2124
  react.useEffect(() => {
2125
+ if (!mountEl) return;
2041
2126
  const gs = themeSettings.global_settings ?? {};
2042
2127
  applyGlobalStyleTokens(gs, mountEl);
2043
- const headingFont = gs.heading_font;
2044
- if (typeof headingFont === "string" && headingFont.trim()) {
2045
- mountEl.style.setProperty(
2046
- "--theme-heading_font",
2047
- resolveFontStack(headingFont)
2048
- );
2049
- }
2050
- const bodyFont = gs.body_font;
2051
- if (typeof bodyFont === "string" && bodyFont.trim()) {
2052
- mountEl.style.setProperty("--theme-body_font", resolveFontStack(bodyFont));
2053
- }
2054
2128
  }, [themeSettings, mountEl]);
2055
2129
  const store = pickStore(ctx);
2056
2130
  const template = pickTemplate(ctx);
@@ -2080,22 +2154,22 @@ var ThemeMountBridge = react.forwardRef(function ThemeMountBridge2({ ctx, mountE
2080
2154
  }
2081
2155
  );
2082
2156
  });
2157
+ function buildThemeElement(ctx, mountEl, renderApp, ref) {
2158
+ return /* @__PURE__ */ jsxRuntime.jsx(react.StrictMode, { children: /* @__PURE__ */ jsxRuntime.jsx(ThemeMountBridge, { ctx, mountEl, renderApp, ref }) });
2159
+ }
2083
2160
  function mountTheme(el, ctx, renderApp) {
2084
- const root = client.createRoot(el);
2085
2161
  const handleRef = { current: null };
2086
- root.render(
2087
- /* @__PURE__ */ jsxRuntime.jsx(react.StrictMode, { children: /* @__PURE__ */ jsxRuntime.jsx(
2088
- ThemeMountBridge,
2089
- {
2090
- ctx,
2091
- mountEl: el,
2092
- renderApp,
2093
- ref: (h) => {
2094
- handleRef.current = h;
2095
- }
2096
- }
2097
- ) })
2098
- );
2162
+ const element = buildThemeElement(ctx, el, renderApp, (h) => {
2163
+ handleRef.current = h;
2164
+ });
2165
+ const shouldHydrate = ctx.hydrate === true && el.firstElementChild !== null;
2166
+ let root;
2167
+ if (shouldHydrate) {
2168
+ root = client.hydrateRoot(el, element);
2169
+ } else {
2170
+ root = client.createRoot(el);
2171
+ root.render(element);
2172
+ }
2099
2173
  return {
2100
2174
  applyDraft: (next) => handleRef.current?.applyDraft(next),
2101
2175
  cleanup: () => {
@@ -2104,6 +2178,14 @@ function mountTheme(el, ctx, renderApp) {
2104
2178
  }
2105
2179
  };
2106
2180
  }
2181
+
2182
+ // src/entry.tsx
2183
+ function defineThemeEntry(renderApp) {
2184
+ return {
2185
+ mount: (el, ctx) => mountTheme(el, ctx, renderApp),
2186
+ createApp: (ctx) => buildThemeElement(ctx, null, renderApp)
2187
+ };
2188
+ }
2107
2189
  function CollectionProvider({ collection, children }) {
2108
2190
  return /* @__PURE__ */ jsxRuntime.jsx(CollectionContext.Provider, { value: collection, children });
2109
2191
  }
@@ -2133,6 +2215,46 @@ function Money({
2133
2215
  children
2134
2216
  );
2135
2217
  }
2218
+
2219
+ // src/utils/imageTransform.ts
2220
+ var _clampT = (n, lo, hi) => Math.min(hi, Math.max(lo, Number.isFinite(n) ? n : lo));
2221
+ function asImageTransform(v) {
2222
+ if (v && typeof v === "object" && "transform" in v) {
2223
+ const t = v.transform;
2224
+ if (t && typeof t === "object") return t;
2225
+ }
2226
+ return void 0;
2227
+ }
2228
+ function applyImageTransform(t, fit = "cover") {
2229
+ if (!t) return { objectFit: fit };
2230
+ const fx = Math.round(_clampT(t.focal?.x ?? 0.5, 0, 1) * 1e4) / 100;
2231
+ const fy = Math.round(_clampT(t.focal?.y ?? 0.5, 0, 1) * 1e4) / 100;
2232
+ const zoom = _clampT(t.zoom ?? 1, 1, 4);
2233
+ const rot = ((t.rotation ?? 0) % 360 + 360) % 360;
2234
+ const effFit = t.fit ?? fit;
2235
+ const style = {
2236
+ transform: `scale(${zoom}) rotate(${rot}deg)`,
2237
+ transformOrigin: `${fx}% ${fy}%`,
2238
+ objectFit: effFit
2239
+ };
2240
+ if (effFit === "cover") style.objectPosition = `${fx}% ${fy}%`;
2241
+ return style;
2242
+ }
2243
+ var clamp01 = (n) => Math.min(1, Math.max(0, n));
2244
+ function focalSrc(url, options = {}) {
2245
+ if (!url) return "";
2246
+ if (url.startsWith("data:") || /[?&](fp-x|fp-y)=/.test(url)) return url;
2247
+ const p = new URLSearchParams();
2248
+ p.set("url", url);
2249
+ if (options.width) p.set("w", String(Math.round(options.width)));
2250
+ if (options.focal?.x != null) p.set("fp-x", String(clamp01(options.focal.x)));
2251
+ if (options.focal?.y != null) p.set("fp-y", String(clamp01(options.focal.y)));
2252
+ if (options.aspect) p.set("ar", options.aspect);
2253
+ if (options.fit) p.set("fit", options.fit);
2254
+ if (options.quality) p.set("q", String(Math.min(100, Math.max(1, Math.round(options.quality)))));
2255
+ if (options.format) p.set("f", options.format.toLowerCase());
2256
+ return `/api/image-transform?${p.toString()}`;
2257
+ }
2136
2258
  var DEFAULT_WIDTHS2 = [320, 480, 640, 768, 1024, 1280, 1600, 1920];
2137
2259
  function buildSrcSet(src, widths = DEFAULT_WIDTHS2) {
2138
2260
  if (/[?&]w=\d+/.test(src)) return "";
@@ -2145,27 +2267,48 @@ function Image({
2145
2267
  sizes = "(min-width: 1024px) 25vw, (min-width: 640px) 50vw, 100vw",
2146
2268
  responsive = true,
2147
2269
  loading = "lazy",
2270
+ aspectRatio,
2271
+ objectFit,
2272
+ objectPosition,
2273
+ transform,
2148
2274
  className,
2149
2275
  style,
2150
2276
  ...rest
2151
2277
  }) {
2278
+ const framed = Boolean(aspectRatio);
2152
2279
  if (!src) {
2153
- return /* @__PURE__ */ jsxRuntime.jsx(
2280
+ const placeholder = /* @__PURE__ */ jsxRuntime.jsx(
2154
2281
  "div",
2155
2282
  {
2156
- className,
2283
+ className: framed ? void 0 : className,
2157
2284
  role: "img",
2158
2285
  "aria-label": alt,
2159
2286
  style: {
2160
2287
  backgroundColor: "rgba(0,0,0,0.05)",
2161
2288
  display: "block",
2162
- ...style
2289
+ width: "100%",
2290
+ height: framed ? "100%" : void 0,
2291
+ ...framed ? {} : style
2163
2292
  }
2164
2293
  }
2165
2294
  );
2295
+ if (!framed) return placeholder;
2296
+ return /* @__PURE__ */ jsxRuntime.jsx(
2297
+ "span",
2298
+ {
2299
+ className,
2300
+ style: { display: "block", aspectRatio, overflow: "hidden", ...style },
2301
+ children: placeholder
2302
+ }
2303
+ );
2166
2304
  }
2167
2305
  const srcSet = responsive ? buildSrcSet(src) : void 0;
2168
- return /* @__PURE__ */ jsxRuntime.jsx(
2306
+ const effFit = objectFit ?? (framed ? "cover" : void 0);
2307
+ const fitStyle = transform ? applyImageTransform(transform, effFit === "contain" ? "contain" : "cover") : {
2308
+ ...effFit ? { objectFit: effFit } : {},
2309
+ ...objectPosition ? { objectPosition } : {}
2310
+ };
2311
+ const img = /* @__PURE__ */ jsxRuntime.jsx(
2169
2312
  "img",
2170
2313
  {
2171
2314
  src,
@@ -2174,11 +2317,27 @@ function Image({
2174
2317
  sizes: srcSet ? sizes : void 0,
2175
2318
  loading,
2176
2319
  decoding: "async",
2177
- className,
2178
- style,
2320
+ className: framed ? void 0 : className,
2321
+ style: framed ? { width: "100%", height: "100%", display: "block", ...fitStyle } : { ...fitStyle, ...style },
2179
2322
  ...rest
2180
2323
  }
2181
2324
  );
2325
+ if (!framed) return img;
2326
+ return /* @__PURE__ */ jsxRuntime.jsx(
2327
+ "span",
2328
+ {
2329
+ className,
2330
+ style: {
2331
+ display: "block",
2332
+ position: "relative",
2333
+ width: "100%",
2334
+ aspectRatio,
2335
+ overflow: "hidden",
2336
+ ...style
2337
+ },
2338
+ children: img
2339
+ }
2340
+ );
2182
2341
  }
2183
2342
  var ABSOLUTE_URL = /^[a-z]+:|^\/\//i;
2184
2343
  function Link({ to, children, ...rest }) {
@@ -2650,7 +2809,14 @@ function sanitizeHtmlServer(input) {
2650
2809
  return s;
2651
2810
  }
2652
2811
  function RichText({ html, className, as = "div" }) {
2653
- const safe = react.useMemo(() => sanitizeHtml(html || ""), [html]);
2812
+ const [domReady, setDomReady] = react.useState(false);
2813
+ react.useEffect(() => {
2814
+ setDomReady(true);
2815
+ }, []);
2816
+ const safe = react.useMemo(
2817
+ () => domReady ? sanitizeHtml(html || "") : sanitizeHtmlServer(html || ""),
2818
+ [html, domReady]
2819
+ );
2654
2820
  if (!safe) return null;
2655
2821
  const Tag = as;
2656
2822
  return /* @__PURE__ */ jsxRuntime.jsx(
@@ -3316,8 +3482,13 @@ function collectBlocks(modules) {
3316
3482
 
3317
3483
  // src/utils/assetUrl.ts
3318
3484
  function getRuntime() {
3319
- if (typeof window === "undefined") return {};
3320
- return window;
3485
+ if (typeof window !== "undefined") {
3486
+ return window;
3487
+ }
3488
+ if (typeof globalThis !== "undefined") {
3489
+ return globalThis;
3490
+ }
3491
+ return {};
3321
3492
  }
3322
3493
  function assetUrl(name) {
3323
3494
  if (!name) return "";
@@ -3334,23 +3505,6 @@ function assetUrl(name) {
3334
3505
  return `${cleanBase}${filename}`;
3335
3506
  }
3336
3507
 
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
3508
  // src/utils/locales.ts
3355
3509
  function flattenMessages(source, prefix = "") {
3356
3510
  const out = {};
@@ -3427,15 +3581,20 @@ exports.SectionContext = SectionContext;
3427
3581
  exports.ShopContext = ShopContext;
3428
3582
  exports.ThemeSettingsContext = ThemeSettingsContext;
3429
3583
  exports.applyGlobalStyleTokens = applyGlobalStyleTokens;
3584
+ exports.applyImageTransform = applyImageTransform;
3585
+ exports.asImageTransform = asImageTransform;
3430
3586
  exports.assetUrl = assetUrl;
3431
3587
  exports.availableValues = availableValues;
3432
3588
  exports.buildLocaleBundle = buildLocaleBundle;
3589
+ exports.buildThemeElement = buildThemeElement;
3433
3590
  exports.clearSdkSingleton = clearSdkSingleton;
3434
3591
  exports.collectBlocks = collectBlocks;
3435
3592
  exports.collectSections = collectSections;
3593
+ exports.computeGlobalStyleTokens = computeGlobalStyleTokens;
3436
3594
  exports.defaultVariant = defaultVariant;
3437
3595
  exports.defineBlock = defineBlock;
3438
3596
  exports.defineSection = defineSection;
3597
+ exports.defineThemeEntry = defineThemeEntry;
3439
3598
  exports.dynamicSource = dynamicSource;
3440
3599
  exports.findVariantByOptions = findVariantByOptions;
3441
3600
  exports.flattenMessages = flattenMessages;
@@ -3453,6 +3612,7 @@ exports.registerSdkSingleton = registerSdkSingleton;
3453
3612
  exports.resolveDynamicValue = resolveDynamicValue;
3454
3613
  exports.resolveFontStack = resolveFontStack;
3455
3614
  exports.resolveSettingsMap = resolveSettingsMap;
3615
+ exports.resolveSizeChart = resolveSizeChart;
3456
3616
  exports.resolveSourcePath = resolveSourcePath;
3457
3617
  exports.resolveThemeSettings = resolveThemeSettings;
3458
3618
  exports.sanitizeHtml = sanitizeHtml;
@@ -3482,6 +3642,7 @@ exports.useOrders = useOrders;
3482
3642
  exports.usePage = usePage;
3483
3643
  exports.useProduct = useProduct;
3484
3644
  exports.useProductOptional = useProductOptional;
3645
+ exports.useProductSizeChart = useProductSizeChart;
3485
3646
  exports.useProducts = useProducts;
3486
3647
  exports.useRelatedProducts = useRelatedProducts;
3487
3648
  exports.useReorder = useReorder;