@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.d.ts CHANGED
@@ -1,10 +1,10 @@
1
- import { i as Store, e as Product, b as Collection, C as Cart, c as Customer, P as Page, S as SizeChart, g as ProductVariant } from './entities-6MGANln7.js';
2
- export { A as Address, a as CartItem, O as Order, d as OrderItem, f as ProductImage, j as ProductOption, h as SizeChartMode } from './entities-6MGANln7.js';
3
- import { T as ThemeSettingsV3, c as SectionInstance, M as MountResult, B as BlockInstance, b as BlockSchema, a as BlockProps$1, f as SectionSchema, e as SectionProps$1 } from './theme-D8MOopvi.js';
4
- export { E as ExternalThemeMetadata, h as MAX_BLOCK_DEPTH, P as PageTemplate, i as PresetBlock, S as SectionGroup, d as SectionPreset, g as SettingDefinition } from './theme-D8MOopvi.js';
1
+ import { i as Store, e as Product, b as Collection, c as Customer, P as Page, S as SizeChart, g as ProductVariant, C as Cart } from './entities-B8378GKp.js';
2
+ export { A as Address, a as CartItem, O as Order, d as OrderItem, f as ProductImage, j as ProductOption, h as SizeChartMode } from './entities-B8378GKp.js';
3
+ import { T as ThemeSettingsV3, c as SectionInstance, M as MountResult, B as BlockInstance, b as BlockSchema, a as BlockProps$1, f as SectionSchema, e as SectionProps$1 } from './theme-CNTB4KnU.js';
4
+ export { E as ExternalThemeMetadata, h as MAX_BLOCK_DEPTH, P as PageTemplate, i as PresetBlock, S as SectionGroup, d as SectionPreset, g as SettingDefinition } from './theme-CNTB4KnU.js';
5
5
  export { KNOWN_SETTING_TYPES, KNOWN_TEMPLATES, REQUIRED_TEMPLATES, SDK_VERSION, THEME_CONTRACT_VERSION, ValidationIssue, ValidationResult, mergeResults, validateBuiltManifest, validateManifest, validateSectionSchema, validateSettingsAgainstSchema } from './validation.js';
6
- import { L as LocalizationState, T as ThemeMountContext, a as ThemeRenderArgs, M as MenuItemData } from './mount-Bt-4ken5.js';
7
- export { C as CartContext, b as CollectionContext, c as CustomerContext, d as LocalizationContext, N as NavigationContext, P as PageContext, e as ProductContext, S as ShopContext, f as ThemeMountPage, g as ThemeSettingsContext, h as buildThemeElement, m as mountTheme } from './mount-Bt-4ken5.js';
6
+ import { C as CartContextValue, L as LocalizationState, a as CurrencyState, T as ThemeMountContext, b as ThemeRenderArgs, M as MenuItemData } from './mount-HbnOAYZ-.js';
7
+ export { c as CartContext, d as CartMutationResult, e as CollectionContext, f as CurrencyConfig, g as CurrencyContext, h as CustomerContext, i as LocalizationContext, N as NavigationContext, P as PageContext, j as ProductContext, S as ShopContext, k as ThemeMountPage, l as ThemeSettingsContext, m as buildThemeElement, n as mountTheme } from './mount-HbnOAYZ-.js';
8
8
  import * as react from 'react';
9
9
  import { ReactElement, ReactNode, ElementType, CSSProperties, ImgHTMLAttributes, AnchorHTMLAttributes, ButtonHTMLAttributes, HTMLAttributes, FormHTMLAttributes, ComponentType } from 'react';
10
10
  export { resolveThemeSettings } from './normalize.js';
@@ -43,17 +43,7 @@ declare function useProductOptional(): Product | null;
43
43
  declare function useCollection(): Collection;
44
44
  declare function useCollectionOptional(): Collection | null;
45
45
 
46
- declare function useCart(): {
47
- cart: Cart;
48
- addItem: (productId: string, variantId?: string, quantity?: number) => Promise<void>;
49
- removeItem: (itemId: string) => Promise<void>;
50
- updateQuantity: (itemId: string, quantity: number) => Promise<void>;
51
- applyDiscount: (code: string) => Promise<void>;
52
- removeDiscount: () => Promise<void>;
53
- updateNote: (note: string) => Promise<void>;
54
- clearCart: () => Promise<void>;
55
- loading: boolean;
56
- };
46
+ declare function useCart(): CartContextValue;
57
47
 
58
48
  declare function useCustomer(): Customer | null;
59
49
 
@@ -136,14 +126,67 @@ declare function useSection(): SectionInstance;
136
126
  declare function useSectionOptional(): SectionInstance | null;
137
127
 
138
128
  /**
139
- * useMoney convenience hook that returns a stable formatter bound to
140
- * the active store's currency.
129
+ * A section instance from a section group, carrying its own `id` (the key it
130
+ * had inside the group's `sections` map). The raw `SectionInstance` drops the
131
+ * id — but a theme needs it to key its React list, wire `<Section id={…}>`
132
+ * click-to-select, and look the type up in its own registry — so
133
+ * `useSectionGroup` re-attaches it here.
134
+ */
135
+ interface SectionGroupInstance extends SectionInstance {
136
+ /** The instance's key within the group's `sections` map. */
137
+ id: string;
138
+ }
139
+ /**
140
+ * useSectionGroup — read the ordered section instances for a named section
141
+ * group ("header", "footer", or any custom global group) out of
142
+ * `themeSettings.section_groups[group]`.
143
+ *
144
+ * This is the SDK side of "global sections shared across pages": the host
145
+ * ships one `section_groups` map on every page, and a theme renders a given
146
+ * group in its layout (header/footer) or wherever it wants a page-independent
147
+ * section list. The returned array is in the group's `order`, and each entry
148
+ * carries its `id` so the theme can key + select + dispatch on it:
149
+ *
150
+ * ```tsx
151
+ * const headerSections = useSectionGroup("header");
152
+ * return headerSections.map(({ id, type, settings }) => {
153
+ * const Comp = registry[type]?.render; // theme owns the registry
154
+ * return Comp ? (
155
+ * <Section key={id} id={id} type={type} groupId="header">
156
+ * <Comp settings={settings} />
157
+ * </Section>
158
+ * ) : null;
159
+ * });
160
+ * ```
161
+ *
162
+ * Pure data hook — it does NOT map types to components. The SDK has no section
163
+ * registry (a theme builds its own via `collectSections`), so there is no
164
+ * generic `<GlobalSections>` renderer to ship: the theme walks the returned
165
+ * instances through its own registry, exactly as it already does for template
166
+ * sections. Returns `[]` (a stable reference) when there is no provider, no
167
+ * `section_groups`, the named group is absent, or its `order` is empty.
168
+ *
169
+ * Disabled instances are INCLUDED (a `disabled` flag is preserved on each
170
+ * entry); filter with `.filter((s) => !s.disabled)` if the theme hides them.
171
+ * Ids listed in `order` but missing from `sections` are skipped defensively.
172
+ */
173
+ declare function useSectionGroup(group: string): SectionGroupInstance[];
174
+
175
+ /**
176
+ * useMoney — convenience hook that returns a formatter bound to the active
177
+ * store's currency.
141
178
  *
142
179
  * Useful when a section needs to format multiple amounts in a render:
143
180
  *
144
181
  * const money = useMoney();
145
182
  * return <td>{money(item.price * item.quantity)}</td>
146
183
  *
184
+ * Multi-currency: like <Money>, when the store has `auto_convert` on and no
185
+ * `currencyOverride` is passed, amounts are converted to and formatted in the
186
+ * visitor's SELECTED presentment currency (shared via `useCurrency()`), so a
187
+ * `<CurrencySwitcher>` change reflows these too. Pass a `currencyOverride` to
188
+ * pin a specific currency and skip conversion.
189
+ *
147
190
  * For one-off price displays prefer the <Money> component which handles
148
191
  * compare-at and inline rendering.
149
192
  */
@@ -579,10 +622,19 @@ declare function useAnalytics(): AnalyticsApi;
579
622
  * `{ available: false }` rather than as a network error — themes
580
623
  * branch on availability without try/catch.
581
624
  *
582
- * The hook revalidates whenever the slug changes, but does NOT refetch
583
- * on focus / interval app data is usually slow-changing (config +
584
- * manifest). Themes that need live data should layer their own
585
- * refresh on top of the returned `refresh()` callback.
625
+ * Phase 3 (client-data layer): the fetch now runs through the shared
626
+ * `useCachedResource` store keyed by `numu:app:<store_id>:<slug>`. Two upgrades
627
+ * over the old per-instance `useState` + fetch:
628
+ * - DEDUP + SYNC: N consumers of the same app slug share ONE request and one
629
+ * result, instead of each firing its own `/apps/{slug}` fetch.
630
+ * - CANCELLATION / ORDERING: the fetcher receives an AbortSignal and the
631
+ * cache seq-guards results, so a slow response for a superseded slug (or a
632
+ * superseded `refresh()`) can no longer apply stale state over a newer one
633
+ * — the audited "out-of-order responses apply stale state" bug.
634
+ *
635
+ * `loading` reflects the FIRST load only; a `refresh()` revalidates in the
636
+ * background (`isValidating`) while keeping the last-good data visible, rather
637
+ * than blanking to a skeleton on every manual refresh.
586
638
  */
587
639
  interface AppManifestBlock {
588
640
  type: string;
@@ -628,8 +680,17 @@ declare function useApp<T = unknown>(slug: string): AppState<T>;
628
680
  * v1 implementation: localStorage only. The /api/customer/me/wishlist
629
681
  * endpoint isn't wired yet; an authed visitor still gets the local
630
682
  * fallback so themes work end-to-end. When the endpoint lands, the
631
- * mutation methods will short-circuit to the server fetch and the
632
- * effect below will drop the localStorage path for authed users.
683
+ * fetcher below swaps its localStorage read for the server fetch and the
684
+ * mutation methods gain a `{ revalidate: true }` write-through.
685
+ *
686
+ * Phase 3 (client-data layer): the items list now lives in the shared
687
+ * `useCachedResource` store keyed by `numu_wishlist_<store_id>`, NOT in
688
+ * per-instance `useState`. Previously two `<Heart>`s for the same product
689
+ * each held their own copy, so adding via one never re-rendered the other —
690
+ * the two hearts DESYNCED. Now every `useWishlist(storeId)` consumer reads
691
+ * and writes ONE shared entry, so an add/remove anywhere reflows every heart.
692
+ * Writes are optimistic (the shared store updates instantly) and roll back if
693
+ * persistence throws.
633
694
  */
634
695
  interface WishlistItem {
635
696
  product_id: string;
@@ -661,11 +722,17 @@ declare function useWishlist(storeId: string): WishlistState;
661
722
  * together"; v1 ships the simpler same-category-minus-self heuristic.
662
723
  *
663
724
  * Backend contract: GET /api/storefront/products/{id}/related?limit=N
664
- * → { items: Product[] } | Product[]
725
+ * → { items: Product[] } | { data: Product[] } | Product[]
665
726
  *
666
727
  * Returns an empty list (no error) when the endpoint is missing or the
667
728
  * product has no siblings — themes should branch on `items.length` and
668
729
  * either render the section or skip it entirely.
730
+ *
731
+ * Phase 3 (client-data layer): routed through the shared `useCachedResource`
732
+ * store keyed by `numu:related:<productId>:<limit>`. Two "related products"
733
+ * sections on the same PDP now share ONE request + one result instead of each
734
+ * refetching, and the AbortSignal + cache seq-guard drop a superseded response
735
+ * (e.g. after a rapid product switch) so it can't overwrite the newer list.
669
736
  */
670
737
  interface RelatedProductsState {
671
738
  items: Product[];
@@ -676,6 +743,103 @@ declare function useRelatedProducts(productId: string | null | undefined, option
676
743
  limit?: number;
677
744
  }): RelatedProductsState;
678
745
 
746
+ /**
747
+ * Shared client-data layer (Phase 3) — a tiny SWR-style cache with ZERO
748
+ * runtime dependencies.
749
+ *
750
+ * Why this exists (from the audit): several hooks fetched + held per-instance
751
+ * state, so N components rendering the same resource fired N identical requests
752
+ * AND drifted out of sync (e.g. two wishlist hearts for one product; `useApp`
753
+ * applying an out-of-order response over a newer one). `useCachedResource`
754
+ * centralizes fetching behind one module-level store keyed by a string:
755
+ *
756
+ * (a) DEDUP — the first consumer of a key starts the fetch and stashes the
757
+ * in-flight promise on the entry; every other consumer that revalidates
758
+ * the same key while it's pending joins that promise instead of firing a
759
+ * second request. N consumers → ONE network call.
760
+ * (b) CACHE — results live on the entry keyed by string, so a later mount
761
+ * reads the cached value immediately (and skips refetch within
762
+ * `dedupeIntervalMs`).
763
+ * (c) SUBSCRIBE / NOTIFY — every hook instance subscribes to its key via
764
+ * `useSyncExternalStore`. Any update (fetch resolve, `mutate`) rebuilds
765
+ * the entry's immutable snapshot and notifies ALL subscribers, so every
766
+ * instance re-renders with the same value — cross-instance state sync.
767
+ * (d) REVALIDATE + CANCELLATION — each fetch reserves a monotonic sequence
768
+ * number and an `AbortController`. Starting a new (forced) fetch aborts
769
+ * the previous one; when any fetch settles, its result is applied ONLY if
770
+ * its sequence is still the latest (`mySeq === entry.seq`). So a slow,
771
+ * superseded response can never overwrite a newer result regardless of
772
+ * the order the network resolves them in.
773
+ *
774
+ * SSR-safe: the fetch is triggered from `useEffect` (never runs under
775
+ * `renderToString`), and `useSyncExternalStore`'s server snapshot returns the
776
+ * `initialData` fallback without ever touching the module cache — so nothing is
777
+ * written to the shared map on the server and there is no cross-request bleed.
778
+ *
779
+ * This is intentionally NOT a full SWR clone (no focus/reconnect revalidation,
780
+ * no suspense, no infinite pagination). It's the smallest primitive that fixes
781
+ * the audited dedup + sync + ordering bugs and that themes can reuse.
782
+ */
783
+ /** The reactive slice a consumer reads. */
784
+ interface CachedResourceState<T> {
785
+ /** Latest cached value, or `initialData` before the first fetch settles. */
786
+ data: T | undefined;
787
+ /** Error from the most recent settled fetch (cleared on success / mutate). */
788
+ error: Error | undefined;
789
+ /** True until the first fetch (or `mutate`) settles a value for this key. */
790
+ isLoading: boolean;
791
+ /** True whenever a fetch is currently in flight (initial OR background). */
792
+ isValidating: boolean;
793
+ }
794
+ /** Fetcher receives an AbortSignal so a superseding fetch can cancel it. */
795
+ type CacheFetcher<T> = (signal: AbortSignal) => Promise<T>;
796
+ /** `mutate` accepts a value or an updater `(current) => next`. */
797
+ type CacheMutator<T> = T | undefined | ((current: T | undefined) => T | undefined);
798
+ interface MutateOptions {
799
+ /** After applying the local value, kick a fresh (forced) revalidation. */
800
+ revalidate?: boolean;
801
+ }
802
+ interface CachedResource<T> extends CachedResourceState<T> {
803
+ /**
804
+ * Optimistically write the cached value for this key (all subscribers
805
+ * re-render). Pass `{ revalidate: true }` to also refetch afterwards.
806
+ */
807
+ mutate: (next: CacheMutator<T>, opts?: MutateOptions) => void;
808
+ /** Force a fresh fetch, superseding + aborting any in-flight request. */
809
+ revalidate: () => Promise<void>;
810
+ }
811
+ interface UseCachedResourceOptions<T> {
812
+ /**
813
+ * Value shown on the server and on the first client paint before the fetch
814
+ * resolves. Used as the SSR snapshot (SSR-safe: no fetch on the server).
815
+ */
816
+ initialData?: T;
817
+ /** When `false`, the hook holds no subscription and never fetches. */
818
+ enabled?: boolean;
819
+ /** Auto-fetch on mount / key change (default `true`). */
820
+ revalidateOnMount?: boolean;
821
+ /**
822
+ * Skip the mount fetch if a value settled within this window (ms). Prevents
823
+ * a staggered second mount from refetching a still-fresh key. Default 2000.
824
+ */
825
+ dedupeIntervalMs?: number;
826
+ /** Called on a non-abort fetch error, with the key. */
827
+ onError?: (error: Error, key: string) => void;
828
+ }
829
+ /**
830
+ * Subscribe to a cached resource keyed by `key`, deduping the fetch across all
831
+ * instances and keeping them in sync. See the module docblock for the full
832
+ * contract.
833
+ *
834
+ * @param key Cache key. `null`/`undefined`/`""` (or `enabled: false`)
835
+ * disables the hook — no subscription, no fetch, `initialData`
836
+ * is returned.
837
+ * @param fetcher `(signal) => Promise<T>`. Recreated-per-render is fine; the
838
+ * latest is captured in a ref so it doesn't retrigger fetches.
839
+ * @param options See {@link UseCachedResourceOptions}.
840
+ */
841
+ declare function useCachedResource<T>(key: string | null | undefined, fetcher: CacheFetcher<T>, options?: UseCachedResourceOptions<T>): CachedResource<T>;
842
+
679
843
  /**
680
844
  * useProductSizeChart — resolve the size chart to show for a product.
681
845
  *
@@ -703,53 +867,6 @@ declare function useProductSizeChart(productOverride?: Product | null): SizeChar
703
867
  */
704
868
  declare function resolveSizeChart(productAttributes: Record<string, unknown> | undefined, storeSettings: Record<string, unknown> | undefined): SizeChart | null;
705
869
 
706
- /**
707
- * Multi-currency presentment — Phase 6.
708
- *
709
- * The store's *capture* currency (what Paymob/Stripe/etc. charges)
710
- * never changes mid-session. This hook is purely about **display**:
711
- * letting visitors browse prices in a currency they recognize.
712
- *
713
- * Usage:
714
- *
715
- * const { base, selected, presentment, convert, setSelected } = useCurrency();
716
- * <p>{convert(product.price.amount_cents)} {selected}</p>
717
- * {presentment.length > 1 && (
718
- * <select value={selected} onChange={(e) => setSelected(e.target.value)}>
719
- * {presentment.map(c => <option key={c}>{c}</option>)}
720
- * </select>
721
- * )}
722
- *
723
- * Behavior:
724
- * - `selected` defaults to the persisted `numu_currency` cookie if
725
- * valid, else `default_presentment`, else `base`.
726
- * - `setSelected` writes the cookie (path=/, 30d) so navigation
727
- * preserves the choice across pages.
728
- * - `convert(cents)` returns the converted cents in `selected`,
729
- * using the rates from the API. When no rate exists, returns
730
- * the input unchanged (theme renders in base — better than a
731
- * wrong number).
732
- *
733
- * Use `<CurrencySwitcher>` from the SDK for an opinionated UI, or
734
- * read this hook directly for full control.
735
- */
736
- interface CurrencyConfig {
737
- base: string;
738
- default_presentment: string;
739
- presentment: string[];
740
- rates: Record<string, string>;
741
- auto_convert: boolean;
742
- }
743
- interface CurrencyState {
744
- base: string;
745
- selected: string;
746
- presentment: string[];
747
- rates: Record<string, number>;
748
- autoConvert: boolean;
749
- loading: boolean;
750
- setSelected: (currency: string) => void;
751
- convert: (cents: number, target?: string) => number;
752
- }
753
870
  declare function useCurrency(): CurrencyState;
754
871
 
755
872
  /**
@@ -811,10 +928,30 @@ interface UseVariantSelection {
811
928
  * Shopify's behavior. Themes that prefer "Choose your size" empty
812
929
  * state can pass `autoSelect: false`.
813
930
  */
814
- declare function useVariantSelection(product: Pick<Product, "options" | "variants">, opts?: {
931
+ declare function useVariantSelection(product: Pick<Product, "options" | "variants"> & {
932
+ id?: string;
933
+ }, opts?: {
815
934
  autoSelect?: boolean;
816
935
  }): UseVariantSelection;
817
936
 
937
+ /**
938
+ * Live variant-picker selection registry.
939
+ *
940
+ * `useVariantSelection` publishes its current axis→value map here keyed by
941
+ * product id; `AddToCartButton` reads it back at click time. This is what
942
+ * lets the selection reach the cart even when no real variant row matches —
943
+ * legacy products keep their axes in attributes JSON with a single
944
+ * placeholder variant whose option_values is {}, so `findVariantByOptions`
945
+ * returns null and there'd otherwise be nothing to send.
946
+ *
947
+ * Module-scoped (not React context) on purpose: the hook instance lives in
948
+ * the theme's PDP section while the button may be a sibling — they share no
949
+ * provider of ours. Bounded: one entry per product id, last write wins.
950
+ */
951
+ declare function publishVariantSelection(productId: string | undefined | null, selection: Record<string, string>): void;
952
+ /** Latest non-empty selection for a product, or null. */
953
+ declare function readVariantSelection(productId: string | undefined | null): Record<string, string> | null;
954
+
818
955
  /**
819
956
  * Gift card balance check — Phase 8.3.
820
957
  *
@@ -1092,6 +1229,15 @@ interface NuMuProviderProps {
1092
1229
  * built before the prop existed keep rendering their home template.
1093
1230
  */
1094
1231
  currentTemplate?: string;
1232
+ /**
1233
+ * I3 — resolved alternate template key for the current page (e.g.
1234
+ * `"product.wholesale"`) when the storefront routed it to a template
1235
+ * suffix. Published on the synthesized `PageContext` value so themes read
1236
+ * it via `usePage()?.template`. Distinct from `currentTemplate`, which is
1237
+ * the base route type (`"product"`). Omit for pages on their default
1238
+ * template; additive, so themes/hosts predating it are unaffected.
1239
+ */
1240
+ pageTemplate?: string;
1095
1241
  /**
1096
1242
  * Pre-fetched product list for the current page. Themes that call
1097
1243
  * `useProducts()` will read these from PageContext without needing
@@ -1117,7 +1263,7 @@ interface NuMuProviderProps {
1117
1263
  navigation?: Record<string, MenuItemData[]>;
1118
1264
  children: ReactNode;
1119
1265
  }
1120
- declare function NuMuProvider({ store, themeSettings, initialCart, customer, locale: initialLocale, translations: initialTranslations, currentTemplate, initialProducts, initialCollections, navigation, children, }: NuMuProviderProps): react.JSX.Element;
1266
+ declare function NuMuProvider({ store, themeSettings, initialCart, customer, locale: initialLocale, translations: initialTranslations, currentTemplate, pageTemplate, initialProducts, initialCollections, navigation, children, }: NuMuProviderProps): react.JSX.Element;
1121
1267
 
1122
1268
  interface ProductProviderProps {
1123
1269
  product: Product;
@@ -1154,6 +1300,13 @@ interface MoneyProps {
1154
1300
  * Intl.NumberFormat. Renders inline with `dir="auto"` so the digits
1155
1301
  * flow naturally in RTL (Arabic) without flipping the currency symbol.
1156
1302
  *
1303
+ * Multi-currency: when the store has `auto_convert` on and the caller did NOT
1304
+ * pin an explicit `currency`, the amount is presented in the visitor's
1305
+ * SELECTED currency (converted via the shared rates from `useCurrency()`). A
1306
+ * `<CurrencySwitcher>` change therefore re-renders every `<Money>` on the page
1307
+ * without a reload. An explicit `currency` prop means "this amount is already
1308
+ * in that currency" — it is respected and never converted.
1309
+ *
1157
1310
  * Usage:
1158
1311
  * <Money amount={product.price} compareAt={product.compare_at_price} />
1159
1312
  */
@@ -1383,27 +1536,66 @@ interface LinkProps extends Omit<AnchorHTMLAttributes<HTMLAnchorElement>, "href"
1383
1536
  to: string;
1384
1537
  children: ReactNode;
1385
1538
  }
1539
+ /**
1540
+ * Event dispatched on `window` when a Link is clicked and eligible for
1541
+ * client-side (soft) navigation. `detail.href` is the target path.
1542
+ *
1543
+ * Contract: the event is CANCELABLE. A host that can perform soft
1544
+ * navigation (e.g. the Next.js storefront routing through its client
1545
+ * router) listens for it and calls `event.preventDefault()` to claim
1546
+ * the navigation. If no listener claims it, the Link falls back to
1547
+ * default anchor behavior — a normal full-page navigation — so themes
1548
+ * running under hosts without the bridge (older storefronts, the CLI
1549
+ * dev server, static previews) keep working unchanged.
1550
+ */
1551
+ declare const NAVIGATE_EVENT = "numu:navigate";
1552
+ interface NavigateEventDetail {
1553
+ href: string;
1554
+ }
1555
+ /**
1556
+ * Ask the host to soft-navigate to `href`. Returns true when a host
1557
+ * listener claimed the navigation (the caller should suppress its own
1558
+ * default behavior), false when no handler is present (the caller
1559
+ * should fall back to a full navigation). Exposed for themes that
1560
+ * navigate programmatically (e.g. after a search submit).
1561
+ */
1562
+ declare function requestNavigate(href: string): boolean;
1386
1563
  /**
1387
1564
  * Route-aware <Link>. Themes write paths as `/products/<slug>` (matches
1388
1565
  * the production subdomain root). The storefront proxy rewrites those
1389
1566
  * under `/<subdomain>/...` in dev path-segment routing; in production
1390
1567
  * the subdomain hostname does the same job at the edge.
1391
1568
  *
1392
- * For plain anchor behavior — server-rendered HTML, full page nav — we
1393
- * just emit a regular `<a>`. Themes that want client-side transitions
1394
- * can wrap this in their own router-aware component; in practice
1395
- * storefront pages are SSR'd so a full nav is fine and predictable.
1396
- *
1397
- * External URLs (have a protocol or start with `//`) pass through
1398
- * unchanged so social-media links, CDN paths, etc. work without
1399
- * special casing.
1400
- */
1401
- declare function Link({ to, children, ...rest }: LinkProps): react.JSX.Element;
1569
+ * Navigation is a two-tier contract:
1570
+ * 1. Soft (preferred): on an eligible plain left-click we dispatch a
1571
+ * cancelable NAVIGATE_EVENT. A router-aware host claims it with
1572
+ * preventDefault() and performs a client-side transition React,
1573
+ * the SDK runtime, and the evaluated theme bundle all stay warm,
1574
+ * so page-to-page moves skip the full document reload + remount.
1575
+ * 2. Hard (fallback): no listener claims the event default anchor
1576
+ * behavior, a normal full-page navigation. Identical to the
1577
+ * pre-0.10 behavior, so themes never break on hosts without the
1578
+ * bridge.
1579
+ *
1580
+ * Soft navigation is only attempted for storefront-internal paths and
1581
+ * unmodified left-clicks: external URLs (protocol or `//`), hash-only
1582
+ * anchors, modified clicks (ctrl/cmd/shift/alt — "open in new tab"),
1583
+ * non-left buttons, `target` other than `_self`, and `download` links
1584
+ * all keep default browser behavior.
1585
+ */
1586
+ declare function Link({ to, children, onClick, ...rest }: LinkProps): react.JSX.Element;
1402
1587
 
1403
1588
  interface AddToCartButtonProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, "onClick" | "disabled"> {
1404
1589
  product: Product;
1405
1590
  variant?: ProductVariant;
1406
1591
  quantity?: number;
1592
+ /**
1593
+ * Picker axes to persist on the cart line ({Color: "Black"}). Optional —
1594
+ * when omitted, the button reads the live useVariantSelection state for
1595
+ * this product from the SDK registry, so existing themes get the variant
1596
+ * label end-to-end (cart → checkout → order → email) with no changes.
1597
+ */
1598
+ selectedOptions?: Record<string, string>;
1407
1599
  /** Custom labels — fallbacks are English defaults. */
1408
1600
  label?: ReactNode;
1409
1601
  loadingLabel?: ReactNode;
@@ -1426,7 +1618,7 @@ interface AddToCartButtonProps extends Omit<ButtonHTMLAttributes<HTMLButtonEleme
1426
1618
  * Doesn't trap navigation — for "buy now" flows that should redirect
1427
1619
  * to checkout, themes wrap this in their own `<a>` after onAdded.
1428
1620
  */
1429
- declare function AddToCartButton({ product, variant, quantity, label, loadingLabel, soldOutLabel, errorLabel, onAdded, ...rest }: AddToCartButtonProps): react.JSX.Element;
1621
+ declare function AddToCartButton({ product, variant, quantity, selectedOptions, label, loadingLabel, soldOutLabel, errorLabel, onAdded, ...rest }: AddToCartButtonProps): react.JSX.Element;
1430
1622
 
1431
1623
  interface SectionProps extends HTMLAttributes<HTMLElement> {
1432
1624
  /** Section instance id (the order key in templates). Required for the
@@ -1670,13 +1862,13 @@ interface RichTextProps {
1670
1862
  * Sanitize an HTML string against the allowlist above. Runs server-
1671
1863
  * AND client-side because rich-text fields are server-rendered for SEO.
1672
1864
  *
1673
- * Implementation note: We use DOMParser when available (browser) for
1674
- * structural correctness; on server we fall through a regex-based
1675
- * pass that handles the common formatting tags + escapes everything
1676
- * else. The server pass is intentionally conservative themes that
1677
- * need server-rendered rich content with edge-case structure should
1678
- * sanitize on the API tier and pass the sanitized HTML through
1679
- * `bypassSanitize` (escape hatch below).
1865
+ * Implementation note: in the browser we use DOMParser (structural allowlist
1866
+ * see `sanitizeHtmlClient`/`walk`). On the server (no DOM) we fall through
1867
+ * to `sanitizeHtmlServer`, a hardened regex denylist that strips dangerous
1868
+ * element blocks to a fixpoint, removes inline event handlers (incl.
1869
+ * slash-separated ones), and drops non-allowlisted `href`/`src` URLs. Themes
1870
+ * needing server-rendered rich content with edge-case structure should
1871
+ * sanitize on the API tier and pass already-safe HTML through.
1680
1872
  */
1681
1873
  declare function sanitizeHtml(input: string): string;
1682
1874
  declare function RichText({ html, className, as }: RichTextProps): react.JSX.Element | null;
@@ -2267,4 +2459,4 @@ declare function pickTranslations(bundle: LocaleBundle, locale: string): LocaleM
2267
2459
  */
2268
2460
  declare function buildLocaleBundle<T extends Record<string, unknown>>(modules: T): LocaleBundle;
2269
2461
 
2270
- export { AddToCartButton, type AddressInput, type AnalyticsApi, type AnalyticsPayload, type AppManifestBlock, type AppPayload, type AppState, Block, BlockInstance, BlockProps$1 as BlockProps, BlockSchema, Cart, type CheckoutAddress, type CheckoutApi, type CheckoutSessionState, type CheckoutStep, Collection, CollectionCard, type CollectionCardProps, type CollectionCardSlots, CollectionProvider, type ComputedStyleTokens, type CurrencyConfig, type CurrencyState, CurrencySwitcher, type CurrencySwitcherProps, Customer, type CustomerAddress, type CustomerAddressesState, type DefineBlockInput, type DefineSectionInput, type DefinedBlock, type DefinedSection, type DynamicResolveContext, type DynamicSourceRef, EditableImage, type EditableImageProps, EditableText, type EditableTextProps, type FocalSrcOptions, Form, type GiftCardBalance, HeroMedia, type HeroMediaProps, ICON_NAMES, Icon, IconMap, type IconProps, Image, type ImageTransform, LOGO_SHAPE_OPTIONS, LOGO_SIZE_OPTIONS, Link, type LocaleBundle, type LocaleMessages, LocaleSwitcher, type LocaleSwitcherProps, Logo, type LogoProps, type LogoShape, type LogoSize, MenuItemData, Money, MountResult, type NavigationItem, type NavigationState, NuMuProvider, type OrderDetail, type OrderListEntry, type OrderListState, type OrderState, Page, type PlaceOrderResult, Product, ProductCard, type ProductCardProps, type ProductCardSlots, ProductProvider, ProductVariant, type RelatedProductsState, type ReorderResult, type ReorderSkipReason, type ReorderSkippedItem, RichText, type RichTextProps, type SearchResults, type SearchState, Section, SectionContext, SectionInstance, SectionProps$1 as SectionProps, SectionSchema, type ShippingRateOption, type ShopWithHelpers, SizeChart, Store, type ThemeEntry, ThemeMountContext, ThemeRenderArgs, ThemeSettingsV3, type UseGiftCardBalance, type UseReorder, type UseSearchOptions, type UseShippingRatesOptions, type UseShippingRatesState, type UseVariantSelection, type WishlistItem, type WishlistState, applyGlobalStyleTokens, applyImageTransform, asImageTransform, assetUrl, availableValues, buildLocaleBundle, clearSdkSingleton, collectBlocks, collectSections, computeGlobalStyleTokens, defaultVariant, defineBlock, defineSection, defineThemeEntry, dynamicSource, findVariantByOptions, flattenMessages, focalSrc, getReactSingleton, getSdkSingleton, isDefinedBlock, isDefinedSection, isDynamicSource, isSdkAvailable, logoImgStyle, logoStyleTokens, pickTranslations, registerReactSingleton, registerSdkSingleton, resolveDynamicValue, resolveFontStack, resolveSettingsMap, resolveSizeChart, resolveSourcePath, 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 };
2462
+ export { AddToCartButton, type AddressInput, type AnalyticsApi, type AnalyticsPayload, type AppManifestBlock, type AppPayload, type AppState, Block, BlockInstance, BlockProps$1 as BlockProps, BlockSchema, type CacheFetcher, type CacheMutator, type CachedResource, type CachedResourceState, Cart, CartContextValue, type CheckoutAddress, type CheckoutApi, type CheckoutSessionState, type CheckoutStep, Collection, CollectionCard, type CollectionCardProps, type CollectionCardSlots, CollectionProvider, type ComputedStyleTokens, CurrencyState, CurrencySwitcher, type CurrencySwitcherProps, Customer, type CustomerAddress, type CustomerAddressesState, type DefineBlockInput, type DefineSectionInput, type DefinedBlock, type DefinedSection, type DynamicResolveContext, type DynamicSourceRef, EditableImage, type EditableImageProps, EditableText, type EditableTextProps, type FocalSrcOptions, Form, type GiftCardBalance, HeroMedia, type HeroMediaProps, ICON_NAMES, Icon, IconMap, type IconProps, Image, type ImageTransform, LOGO_SHAPE_OPTIONS, LOGO_SIZE_OPTIONS, Link, type LocaleBundle, type LocaleMessages, LocaleSwitcher, type LocaleSwitcherProps, Logo, type LogoProps, type LogoShape, type LogoSize, MenuItemData, Money, MountResult, type MutateOptions, NAVIGATE_EVENT, type NavigateEventDetail, type NavigationItem, type NavigationState, NuMuProvider, type OrderDetail, type OrderListEntry, type OrderListState, type OrderState, Page, type PlaceOrderResult, Product, ProductCard, type ProductCardProps, type ProductCardSlots, ProductProvider, ProductVariant, type RelatedProductsState, type ReorderResult, type ReorderSkipReason, type ReorderSkippedItem, RichText, type RichTextProps, type SearchResults, type SearchState, Section, SectionContext, type SectionGroupInstance, SectionInstance, SectionProps$1 as SectionProps, SectionSchema, type ShippingRateOption, type ShopWithHelpers, SizeChart, Store, type ThemeEntry, ThemeMountContext, ThemeRenderArgs, ThemeSettingsV3, type UseCachedResourceOptions, type UseGiftCardBalance, type UseReorder, type UseSearchOptions, type UseShippingRatesOptions, type UseShippingRatesState, type UseVariantSelection, type WishlistItem, type WishlistState, applyGlobalStyleTokens, applyImageTransform, asImageTransform, assetUrl, availableValues, buildLocaleBundle, clearSdkSingleton, collectBlocks, collectSections, computeGlobalStyleTokens, defaultVariant, defineBlock, defineSection, defineThemeEntry, dynamicSource, findVariantByOptions, flattenMessages, focalSrc, getReactSingleton, getSdkSingleton, isDefinedBlock, isDefinedSection, isDynamicSource, isSdkAvailable, logoImgStyle, logoStyleTokens, pickTranslations, publishVariantSelection, readVariantSelection, registerReactSingleton, registerSdkSingleton, requestNavigate, resolveDynamicValue, resolveFontStack, resolveSettingsMap, resolveSizeChart, resolveSourcePath, sanitizeHtml, useAnalytics, useApp, useCachedResource, 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, useSectionGroup, useSectionOptional, useShippingRates, useShop, useThemeSettings, useTranslation, useVariantSelection, useWishlist };