@numueg/theme-sdk 0.10.1 → 0.13.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/dist/index.d.ts CHANGED
@@ -1,10 +1,10 @@
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';
1
+ import { i as Store, e as Product, b as Collection, c as Customer, P as Page, M as Metafield, j as ArticleDetail, k as ArticleSummary, B as BlogSummary, C as Cart, S as SizeChart, g as ProductVariant } from './entities-DRKn04q0.js';
2
+ export { A as Address, a as CartItem, O as Order, d as OrderItem, f as ProductImage, l as ProductOption, h as SizeChartMode } from './entities-DRKn04q0.js';
3
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
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 { C as CartContextValue, L as LocalizationState, a as CurrencyState, T as ThemeMountContext, b as ThemeRenderArgs, M as MenuItemData } from './mount-DQZu8aBB.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-DQZu8aBB.js';
6
+ import { C as CartContextValue, L as LocalizationState, a as CurrencyState, T as ThemeMountContext, b as ThemeRenderArgs, M as MenuItemData } from './mount-CLQniVfc.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-CLQniVfc.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,6 +43,54 @@ declare function useProductOptional(): Product | null;
43
43
  declare function useCollection(): Collection;
44
44
  declare function useCollectionOptional(): Collection | null;
45
45
 
46
+ interface ListingHeading {
47
+ /** The collection the shopper is inside, or `null` on the all-products listing. */
48
+ collection: Collection | null;
49
+ /** `true` when this listing is scoped to one collection. */
50
+ isCollection: boolean;
51
+ /** What to render as the page heading. */
52
+ title: string;
53
+ /** What to render under it. Empty string when there is nothing to say. */
54
+ description: string;
55
+ }
56
+ interface ListingHeadingOptions {
57
+ /** The merchant's own heading from this section's settings, if they set one. */
58
+ title?: string | null;
59
+ /** The merchant's own sub-heading from this section's settings. */
60
+ description?: string | null;
61
+ /**
62
+ * The theme's wording for the UNSCOPED listing — already localized by the
63
+ * theme, e.g. `localized(locale, "All products", "كل المنتجات")`.
64
+ */
65
+ defaultTitle?: string | null;
66
+ }
67
+ /**
68
+ * What a product-listing section should call itself.
69
+ *
70
+ * Every theme hardcoded "All products" as its listing heading, so a shopper who
71
+ * clicked into a category landed on a page titled **All products** showing a
72
+ * subset of the catalog. The products were correctly scoped — the page just
73
+ * never said which collection it was. That reads as a bug ("where did the rest
74
+ * go?") and it costs the collection name in the one place it matters most.
75
+ *
76
+ * Precedence, highest first:
77
+ * 1. the collection's own name — when the shopper is inside one, that IS the
78
+ * page, and it always beats a static setting;
79
+ * 2. the merchant's section setting — their wording for the all-products page;
80
+ * 3. the theme's default.
81
+ *
82
+ * Categories carry no separate Arabic column, so `collection.name` is rendered
83
+ * as the merchant typed it; only the fallback is localized, by the caller.
84
+ *
85
+ * @example
86
+ * const { title, description, isCollection } = useListingHeading({
87
+ * title: asString(s.title),
88
+ * description: asString(s.subtitle),
89
+ * defaultTitle: localized(locale, "All products", "كل المنتجات"),
90
+ * });
91
+ */
92
+ declare function useListingHeading(options?: ListingHeadingOptions): ListingHeading;
93
+
46
94
  declare function useCart(): CartContextValue;
47
95
 
48
96
  declare function useCustomer(): Customer | null;
@@ -121,6 +169,44 @@ declare function useNumberFormat(): (n: number, options?: Intl.NumberFormatOptio
121
169
 
122
170
  declare function usePage(): Page | null;
123
171
 
172
+ /**
173
+ * Owners a theme can read metafields from. Matches the platform's
174
+ * metafield owner types (product / collection / page). Only PUBLIC
175
+ * metafields ever reach the storefront — the privacy filter runs
176
+ * server-side at the payload layer, so a private field is simply absent
177
+ * here, never present-but-hidden.
178
+ */
179
+ type MetafieldOwner = "product" | "collection" | "page";
180
+ /**
181
+ * All public metafields of the given owner currently in context.
182
+ * Empty array when the owner isn't in context (e.g. `"product"` on a
183
+ * collection page) or has no fields — never throws.
184
+ */
185
+ declare function useMetafields(owner: MetafieldOwner): Metafield[];
186
+ /**
187
+ * Read one merchant-defined field directly:
188
+ *
189
+ * ```tsx
190
+ * const material = useMetafield("product", "specs", "material");
191
+ * if (material) <dd>{String(material.value)}</dd>;
192
+ * ```
193
+ *
194
+ * Returns the full {@link Metafield} (value pre-coerced to its declared
195
+ * type by the platform) or `null` when missing — same convention as the
196
+ * dynamic-source resolver (`{owner}.metafield:{namespace}.{key}`), so a
197
+ * theme's own fallback renders for stores that haven't set the field.
198
+ */
199
+ declare function useMetafield(owner: MetafieldOwner, namespace: string, key: string): Metafield | null;
200
+
201
+ /** Published blogs of the store (blogs-index template). */
202
+ declare function useBlogs(): BlogSummary[];
203
+ /** The current blog (blog template), or the article's blog on an article page. */
204
+ declare function useBlog(): BlogSummary | null;
205
+ /** Published articles of the current blog (blog template), newest first. */
206
+ declare function useArticles(): ArticleSummary[];
207
+ /** The current article (article template). */
208
+ declare function useArticle(): ArticleDetail | null;
209
+
124
210
  declare const SectionContext: react.Context<SectionInstance | null>;
125
211
  declare function useSection(): SectionInstance;
126
212
  declare function useSectionOptional(): SectionInstance | null;
@@ -840,6 +926,216 @@ interface UseCachedResourceOptions<T> {
840
926
  */
841
927
  declare function useCachedResource<T>(key: string | null | undefined, fetcher: CacheFetcher<T>, options?: UseCachedResourceOptions<T>): CachedResource<T>;
842
928
 
929
+ /**
930
+ * Storefront promotion types — the shape the platform serves to every theme.
931
+ *
932
+ * Source of truth is the backend's offers-v2 engine. The host proxies it at
933
+ * `GET /api/storefront/promotions`, which returns the visitor's active
934
+ * promotions grouped by surface. Themes only ever *render* these; the engine
935
+ * decides what a cart costs, and the same engine runs at checkout. A theme
936
+ * that computes its own discount is telling the shopper a number the server
937
+ * will overrule.
938
+ *
939
+ * ⚠️ Every `*_cents` field here is in INTEGER CENTS, straight off the API —
940
+ * unlike `Cart`, which the SDK normalizes to major units. Convert with the
941
+ * helpers in `lib/promotions` rather than dividing ad hoc; a missed
942
+ * conversion is a silent 100x error.
943
+ */
944
+ /** One step of a tiered "spend X, get Y%" rule. */
945
+ interface DiscountTier {
946
+ threshold_cents: number;
947
+ percent: number;
948
+ }
949
+ /**
950
+ * The math behind a promotion. `kind` is an open string union on purpose:
951
+ * the platform adds kinds over time and older themes must degrade quietly
952
+ * rather than crash on one they've never heard of.
953
+ */
954
+ interface DiscountRule {
955
+ kind: "percentage" | "fixed" | "free_shipping" | "bogo" | "tiered" | "multibuy" | (string & {});
956
+ value_cents?: number | null;
957
+ value_percent?: number | null;
958
+ min_subtotal_cents?: number | null;
959
+ max_discount_cents?: number | null;
960
+ buy_quantity?: number | null;
961
+ get_quantity?: number | null;
962
+ get_discount_percent?: number | null;
963
+ tiers?: DiscountTier[];
964
+ /** MULTIBUY: how many eligible items form one group (N). */
965
+ multibuy_quantity?: number | null;
966
+ /** MULTIBUY: the fixed total price for one complete group (P), in cents. */
967
+ multibuy_price_cents?: number | null;
968
+ }
969
+ /** A promotion resolved as active for the current visitor. */
970
+ interface ActivePromotion {
971
+ promotion_id: string;
972
+ surface?: string;
973
+ priority?: number;
974
+ content?: Record<string, unknown>;
975
+ translated_content?: {
976
+ headline?: Record<string, string>;
977
+ body?: Record<string, string>;
978
+ [key: string]: unknown;
979
+ };
980
+ discount_rule?: DiscountRule | null;
981
+ coupon_code?: string | null;
982
+ /**
983
+ * Which catalog entries can take part in the rule. BOTH EMPTY (or absent,
984
+ * on an older backend) means the whole store qualifies.
985
+ *
986
+ * Needed so a scoped offer doesn't lie: without it a "3 for EGP 650 on
987
+ * scarves" offer counts every cart unit, and a shopper holding one
988
+ * ineligible item is told "add 2 more" and then doesn't get the discount.
989
+ */
990
+ eligible_product_ids?: string[];
991
+ eligible_category_ids?: string[];
992
+ }
993
+ /** The `/api/storefront/promotions` payload, grouped by surface. */
994
+ interface ActivePromotionsPayload {
995
+ announcement_bars?: ActivePromotion[];
996
+ popups?: ActivePromotion[];
997
+ floating_widgets?: ActivePromotion[];
998
+ cookie_banner?: ActivePromotion | null;
999
+ /** Automatic (no code needed) discounts — where multibuy offers live. */
1000
+ auto_discounts?: ActivePromotion[];
1001
+ discount_codes_visible?: ActivePromotion[];
1002
+ }
1003
+ /**
1004
+ * A validated multibuy offer, normalized for display.
1005
+ *
1006
+ * `groupPriceMajor` is in MAJOR units (EGP, not piastres) so it can go
1007
+ * straight into `<Money>` / `formatMoney` — the conversion happened once,
1008
+ * here, rather than in each theme.
1009
+ */
1010
+ interface MultibuyOffer {
1011
+ promotionId: string;
1012
+ /** N — how many items make a group. */
1013
+ quantity: number;
1014
+ /** P in cents, as the engine stores it. */
1015
+ groupPriceCents: number;
1016
+ /** P in major units, ready to render. */
1017
+ groupPriceMajor: number;
1018
+ /** Merchant's bilingual headline, when they set one. */
1019
+ headline?: Record<string, string>;
1020
+ /** Products the offer is scoped to. Empty (with categories) = whole store. */
1021
+ eligibleProductIds: string[];
1022
+ /** Categories the offer is scoped to. Empty (with products) = whole store. */
1023
+ eligibleCategoryIds: string[];
1024
+ /** True when the offer applies to the entire catalogue. */
1025
+ isStoreWide: boolean;
1026
+ raw: ActivePromotion;
1027
+ }
1028
+ /** Where a shopper stands against one multibuy offer. */
1029
+ interface OfferProgress {
1030
+ /** Eligible units currently in the cart. */
1031
+ unitsInCart: number;
1032
+ /** How many more units to complete the next group. 0 = just unlocked. */
1033
+ unitsNeeded: number;
1034
+ /** Complete groups the cart has already earned. */
1035
+ groupsUnlocked: number;
1036
+ /**
1037
+ * What the engine says this offer actually saved, in MAJOR units. Read
1038
+ * from `cart.applied_promotions` — never recomputed, so the number the
1039
+ * shopper reads is the number they are charged.
1040
+ */
1041
+ savingMajor: number;
1042
+ }
1043
+
1044
+ /**
1045
+ * The store's active promotions for the current visitor.
1046
+ *
1047
+ * Reads the host proxy `GET /api/storefront/promotions` (never NUMU-api
1048
+ * directly — the host owns credentials, caching and the store resolution).
1049
+ * The backend groups by surface; the host already renders announcement bars,
1050
+ * popups, the cookie banner and floating widgets in the shell, so what a
1051
+ * theme normally wants is `auto_discounts` — the offers that price the cart
1052
+ * with no code to type.
1053
+ *
1054
+ * SSR-safe: `useCachedResource` doesn't fetch during server render, so this
1055
+ * resolves on hydrate. Sections must therefore tolerate a null first pass —
1056
+ * render nothing (or a skeleton) rather than assuming data on first paint.
1057
+ *
1058
+ * Returns `null` on any miss (offline, 404, flag off). That is deliberate:
1059
+ * a promotions outage should make an offer banner disappear, never break the
1060
+ * page it sits on.
1061
+ *
1062
+ * @param page page path used for page-targeted promotions (default "/")
1063
+ * @param locale "ar" | "en" — selects the translated content
1064
+ */
1065
+ declare function useActivePromotions(page?: string, locale?: string): ActivePromotionsPayload | null;
1066
+
1067
+ /**
1068
+ * Pure helpers for rendering platform promotions in a theme.
1069
+ *
1070
+ * These are headless by design — data and logic only, zero markup and zero
1071
+ * styling — so every theme keeps its own look while sharing one correct
1072
+ * implementation of the fiddly parts (unit counting, group math, the
1073
+ * cents→major boundary).
1074
+ *
1075
+ * The hard rule these encode: **a theme never computes a discount.** The
1076
+ * engine prices the cart and reports what it saved; helpers here only
1077
+ * describe progress toward an offer and format what the engine already
1078
+ * decided. Anything else eventually renders a number the server won't honour.
1079
+ */
1080
+
1081
+ /**
1082
+ * Extract the usable multibuy offers ("any N for a fixed total P").
1083
+ *
1084
+ * Accepts either the whole `/api/storefront/promotions` payload or just an
1085
+ * array of promotions, so callers don't have to reach into `auto_discounts`
1086
+ * themselves. Anything that isn't a well-formed multibuy — a kind this SDK
1087
+ * version doesn't know, a missing N or P — is skipped rather than thrown on:
1088
+ * a theme built before a rule kind existed must keep rendering when a
1089
+ * merchant starts using it.
1090
+ */
1091
+ declare function multibuyOffers(promotions: ActivePromotionsPayload | ActivePromotion[] | null | undefined): MultibuyOffer[];
1092
+ /**
1093
+ * Does this product take part in the offer?
1094
+ *
1095
+ * A store-wide offer includes everything. Otherwise the product must be named
1096
+ * directly or sit in a named category. A scoped offer over a product whose
1097
+ * `category_id` we don't know returns false — better to under-promise than to
1098
+ * advertise a discount the server won't apply.
1099
+ */
1100
+ declare function offerIncludesProduct(offer: MultibuyOffer | null | undefined, product: {
1101
+ id?: string;
1102
+ product_id?: string;
1103
+ category_id?: string | null;
1104
+ }): boolean;
1105
+ /**
1106
+ * How many cart UNITS actually qualify for the offer.
1107
+ *
1108
+ * Units, not lines — three of one product is a valid trio, the way the engine
1109
+ * scores it. For a scoped offer this is what the nudge must count; counting
1110
+ * the whole cart is how a shopper gets told "add 2 more" and then doesn't get
1111
+ * the discount.
1112
+ */
1113
+ declare function eligibleUnitsInCart(offer: MultibuyOffer | null | undefined, cart: Cart | null | undefined): number;
1114
+ /**
1115
+ * Where the cart stands against one multibuy offer.
1116
+ *
1117
+ * Counts UNITS, not lines — three of the same product is a valid trio, the
1118
+ * same way the engine scores it. `savingMajor` is read from the cart's
1119
+ * engine-supplied `applied_promotions`, so an unlocked offer shows the real
1120
+ * saving and a theme can never drift from the charged amount.
1121
+ *
1122
+ * Scoping is handled automatically: when the offer names products or
1123
+ * categories, only qualifying units are counted (via `eligibleUnitsInCart`).
1124
+ * Pass `eligibleUnits` explicitly to override that — e.g. when the theme has
1125
+ * a better source of truth than the cart lines.
1126
+ */
1127
+ declare function offerProgress(offer: MultibuyOffer | null | undefined, cart: Cart | null | undefined, eligibleUnits?: number): OfferProgress;
1128
+ /**
1129
+ * Should a product page advertise this offer?
1130
+ *
1131
+ * Only when N of this product actually costs more than the group price —
1132
+ * otherwise the "deal" is worse than just buying them, and §5 of the offer
1133
+ * spec forbids advertising it (the engine likewise refuses to apply it).
1134
+ *
1135
+ * @param unitPriceMajor the product's price in MAJOR units, as a theme has it
1136
+ */
1137
+ declare function offerBeatsRegularPrice(offer: MultibuyOffer | null | undefined, unitPriceMajor: number | null | undefined): boolean;
1138
+
843
1139
  /**
844
1140
  * useProductSizeChart — resolve the size chart to show for a product.
845
1141
  *
@@ -1340,9 +1636,16 @@ declare function asImageTransform(v: unknown): ImageTransform | undefined;
1340
1636
  * CSS reproducing the transform on an `<img>` that fills a fixed-aspect,
1341
1637
  * overflow-hidden frame. Default fit is `cover` (Shopify-style: the image
1342
1638
  * fills its frame, cropping to the focal point) — pass `"contain"` only for
1343
- * placements that must show the whole image (e.g. a logo). Empty object when
1344
- * there is no transform AND the caller wants the section's own className to
1345
- * decide; pass an explicit `fit` to force a default even without a transform.
1639
+ * placements that must show the whole image (e.g. a logo).
1640
+ *
1641
+ * **No transform empty object.** An inline style beats a className, so
1642
+ * returning `{ objectFit: fit }` here would silently override a placement's
1643
+ * own `object-contain` / `object-none` class on every untransformed image —
1644
+ * which is most merchant images. The section's className stays in charge; a
1645
+ * caller that needs a guaranteed fit supplies it itself, e.g.
1646
+ * `{ objectFit: fit, ...applyImageTransform(t, fit) }` (see `HeroMedia`).
1647
+ * This matches `Image.tsx`'s untransformed branch and the behaviour every
1648
+ * theme's local copy has shipped with.
1346
1649
  */
1347
1650
  declare function applyImageTransform(t: ImageTransform | undefined | null, fit?: "cover" | "contain"): CSSProperties;
1348
1651
  /**
@@ -2264,6 +2567,187 @@ declare function collectBlocks<T extends Record<string, unknown>>(modules: T): R
2264
2567
  */
2265
2568
  declare function assetUrl(name: string): string;
2266
2569
 
2570
+ /**
2571
+ * Storefront route builders.
2572
+ *
2573
+ * The storefront's URL shape is platform knowledge, not theme knowledge: a
2574
+ * theme that hardcodes `/products/${slug}` has quietly taken a dependency on
2575
+ * the host's routing, and every copy of that template is a place the fleet can
2576
+ * disagree with the host. Centralising the templates here means a route change
2577
+ * is one edit rather than a fleet-wide grep.
2578
+ *
2579
+ * Hoisted out of the themes' `sections/_shared.ts`, where `productHref` was
2580
+ * hand-copied into 4 themes and their 4 scaffold templates.
2581
+ */
2582
+ /**
2583
+ * Link to a product detail page.
2584
+ *
2585
+ * Falls back to the product index when there's no identifier, so a card built
2586
+ * from incomplete data links somewhere useful instead of `/products/undefined`.
2587
+ */
2588
+ declare function productHref(slugOrId: string | undefined | null): string;
2589
+ /**
2590
+ * Link to a collection page.
2591
+ *
2592
+ * Prefers the slug and falls back to the id, because a category with no slug
2593
+ * is common in practice and the bare template produces `/collections/undefined`
2594
+ * — a 404 the merchant sees as a broken menu item. Accepts either a category
2595
+ * object or a plain slug string so call sites don't have to unpack first.
2596
+ */
2597
+ declare function collectionHref(category: string | {
2598
+ slug?: string | null;
2599
+ id?: string | null;
2600
+ } | null | undefined): string;
2601
+
2602
+ /**
2603
+ * Money formatting for NUMU storefronts.
2604
+ *
2605
+ * Three implementations of this existed and disagreed:
2606
+ *
2607
+ * - the themes' own `fmt` (4 byte-identical copies): EGP fallback,
2608
+ * `maximumFractionDigits: 0`, locale `"ar"` mapped to `"ar-EG"`
2609
+ * - `@numueg/theme-kit`'s `formatMoney`: **USD** fallback, 2 fraction
2610
+ * digits, raw locale passed through
2611
+ * - `NuMuProvider`'s own `formatMoney`: a third variant again
2612
+ *
2613
+ * This adopts the themes' behaviour, because that is what production renders
2614
+ * today: adopting the kit's would silently turn every promo nudge from
2615
+ * `LE 500` into `£E 500.00` on live stores. NUMU is Egypt-first — EGP is the
2616
+ * right fallback, whole piastres are noise at Egyptian price points, and
2617
+ * `ar-EG` is what produces Egyptian Arabic numerals and currency placement.
2618
+ *
2619
+ * The unit is CENTS, matching the API end to end. `formatMoneyMajor` exists
2620
+ * for the one place that already divided (the SDK exposes cart money in major
2621
+ * units), so nobody has to remember which side of the boundary they're on.
2622
+ */
2623
+ interface FormatMoneyOptions {
2624
+ /** Currency code. Falls back to EGP — Egypt-first, not USD. */
2625
+ currency?: string | null;
2626
+ /** `"ar"` maps to `ar-EG`; anything else formats as `en-EG`. */
2627
+ locale?: string | null;
2628
+ /**
2629
+ * Fraction digits. Defaults to 0: Egyptian retail prices are whole pounds,
2630
+ * and `500.00 EGP` reads as noise next to `500 EGP`. Pass 2 where sub-unit
2631
+ * precision genuinely matters (a unit price, an FX-converted amount).
2632
+ */
2633
+ fractionDigits?: number;
2634
+ }
2635
+ /**
2636
+ * Format an integer amount of cents.
2637
+ *
2638
+ * @example formatMoney(50000) // "EGP 500"
2639
+ * @example formatMoney(50000, { locale: "ar" }) // "٥٠٠ ج.م.‏"
2640
+ */
2641
+ declare function formatMoney(cents: number, options?: FormatMoneyOptions): string;
2642
+ /**
2643
+ * Format an amount already expressed in major units.
2644
+ *
2645
+ * The SDK hands cart money to themes in major units while the API speaks
2646
+ * cents, so a shared helper that only accepted one of the two guaranteed a
2647
+ * factor-of-100 bug at the seam.
2648
+ */
2649
+ declare function formatMoneyMajor(amount: number, options?: FormatMoneyOptions): string;
2650
+ /** Cents to major units. Non-finite input yields 0, never NaN. */
2651
+ declare function centsToMajor(cents: number): number;
2652
+ /** Major units to integer cents, rounded — the inverse of `centsToMajor`. */
2653
+ declare function majorToCents(amount: number): number;
2654
+
2655
+ /**
2656
+ * Template + section-group resolution — the "no blank page" engine.
2657
+ *
2658
+ * This is engine policy, not theme code, and it was copy-pasted into every
2659
+ * theme: `src/sections/_template-utils.ts` is BYTE-IDENTICAL across 11 of the
2660
+ * 14 themes that carry it (md5 59608dd4f4a702136854d7aaf33ba5e3); the
2661
+ * remaining copies differ only in comments and brace style. The file's own
2662
+ * header instructed theme authors to copy it and said it could be deleted
2663
+ * "once the published SDK ships its own sanitizeTemplate". This is that.
2664
+ *
2665
+ * Why centralising it is safe — and why it matters:
2666
+ * - It contains ZERO design. It decides WHICH sections render, never how
2667
+ * they look, so it cannot homogenise themes.
2668
+ * - Getting it subtly wrong renders an EMPTY STORE. Empire re-implemented a
2669
+ * weaker version inline with no unknown-type filtering, so a stale
2670
+ * customisation renders "Unknown section" placeholders there while every
2671
+ * other theme degrades gracefully. One implementation removes that class.
2672
+ *
2673
+ * The two rules it encodes, both learned from real blank-page incidents:
2674
+ * 1. A template may arrive as an ARRAY or as a MAP + `order[]`. The
2675
+ * customizer writes one shape, theme presets use the other.
2676
+ * 2. Host customisation wins over the theme's bundled preset — UNLESS it
2677
+ * resolves to nothing this theme can render, in which case fall back to
2678
+ * the preset rather than showing the shopper an empty page.
2679
+ */
2680
+
2681
+ /** A template or section group, in either of the two shapes hosts send. */
2682
+ interface MaybeOrderedTemplate {
2683
+ name?: string;
2684
+ sections?: Record<string, SectionInstance> | SectionInstance[];
2685
+ order?: string[];
2686
+ }
2687
+ /** A section instance paired with the stable id the customizer selects by. */
2688
+ interface ResolvedSection {
2689
+ id: string;
2690
+ instance: SectionInstance;
2691
+ }
2692
+ /**
2693
+ * Normalise a template/group into an ordered list.
2694
+ *
2695
+ * Array form gets synthetic `${type}-${index}` ids (presets have no ids of
2696
+ * their own). Map form is walked in `order`, skipping ids the map doesn't
2697
+ * contain — a dangling id in `order` is a customisation that referenced a
2698
+ * deleted section and must not throw.
2699
+ */
2700
+ declare function resolveSections(group: MaybeOrderedTemplate | undefined): ResolvedSection[];
2701
+ /**
2702
+ * Choose between the host's customisation and the theme's bundled preset,
2703
+ * then drop section types this theme cannot render.
2704
+ *
2705
+ * `isKnown` is the theme's own registry lookup — the one thing that genuinely
2706
+ * belongs to the theme, so it stays a parameter.
2707
+ *
2708
+ * The order of the three checks is load-bearing:
2709
+ * - nothing from the host → preset (a fresh install renders its demo)
2710
+ * - host sections, none known → preset (a customisation built for a
2711
+ * DIFFERENT theme must not blank the page)
2712
+ * - otherwise → host sections, unknown types filtered out
2713
+ */
2714
+ declare function selectTemplateSections(hostTemplate: MaybeOrderedTemplate | undefined, builtinTemplate: MaybeOrderedTemplate | undefined, isKnown: (sectionType: string) => boolean): ResolvedSection[];
2715
+ /**
2716
+ * Find the theme's header/footer for a route it has no template for.
2717
+ *
2718
+ * Chrome reaches a theme from `section_groups` — except most themes never used
2719
+ * that channel: they put the header and footer INLINE in each template's own
2720
+ * section list. That works right up until the shopper hits a route the theme
2721
+ * ships no template for. Then there are no sections, so there is no chrome,
2722
+ * and the page renders with no navigation and no way back into the store.
2723
+ *
2724
+ * That is what `/blogs` looked like on every theme: correct, readable content
2725
+ * with nothing around it — no logo, no menu, no cart, no footer. A shopper
2726
+ * arriving from search had to press Back to escape.
2727
+ *
2728
+ * The template's OWN inline chrome still wins when it has some. This only
2729
+ * supplies chrome to routes that would otherwise have none, by borrowing the
2730
+ * chrome the theme already renders everywhere else — `home` first, since it is
2731
+ * the one template every theme ships and the one merchants customise.
2732
+ *
2733
+ * Returns `[]` when the theme genuinely has no chrome of this kind anywhere,
2734
+ * so a caller can still fall back to its own default.
2735
+ */
2736
+ declare function selectChromeSections(options: {
2737
+ /** `section_groups[kind]` from the host — the customizer's channel. */
2738
+ hostGroup?: MaybeOrderedTemplate;
2739
+ /** The theme's own preset `section_groups[kind]`. */
2740
+ presetGroup?: MaybeOrderedTemplate;
2741
+ /** Chrome found inline in the CURRENT template's section list. */
2742
+ inline?: ResolvedSection[];
2743
+ /** Every template the theme can draw on — host customisation and presets. */
2744
+ templates?: Array<MaybeOrderedTemplate | undefined>;
2745
+ /** True for the section types that count as this kind of chrome. */
2746
+ isChrome: (sectionType: string) => boolean;
2747
+ /** The theme's registry lookup. */
2748
+ isKnown: (sectionType: string) => boolean;
2749
+ }): ResolvedSection[];
2750
+
2267
2751
  /**
2268
2752
  * `applyGlobalStyleTokens(globalSettings, el)` — Phase 3.5.
2269
2753
  *
@@ -2459,4 +2943,4 @@ declare function pickTranslations(bundle: LocaleBundle, locale: string): LocaleM
2459
2943
  */
2460
2944
  declare function buildLocaleBundle<T extends Record<string, unknown>>(modules: T): LocaleBundle;
2461
2945
 
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 };
2946
+ export { type ActivePromotion, type ActivePromotionsPayload, AddToCartButton, type AddressInput, type AnalyticsApi, type AnalyticsPayload, type AppManifestBlock, type AppPayload, type AppState, ArticleDetail, ArticleSummary, Block, BlockInstance, BlockProps$1 as BlockProps, BlockSchema, BlogSummary, 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 DiscountRule, type DiscountTier, type DynamicResolveContext, type DynamicSourceRef, EditableImage, type EditableImageProps, EditableText, type EditableTextProps, type FocalSrcOptions, Form, type FormatMoneyOptions, type GiftCardBalance, HeroMedia, type HeroMediaProps, ICON_NAMES, Icon, IconMap, type IconProps, Image, type ImageTransform, LOGO_SHAPE_OPTIONS, LOGO_SIZE_OPTIONS, Link, type ListingHeading, type ListingHeadingOptions, type LocaleBundle, type LocaleMessages, LocaleSwitcher, type LocaleSwitcherProps, Logo, type LogoProps, type LogoShape, type LogoSize, type MaybeOrderedTemplate, MenuItemData, type MetafieldOwner, Money, MountResult, type MultibuyOffer, type MutateOptions, NAVIGATE_EVENT, type NavigateEventDetail, type NavigationItem, type NavigationState, NuMuProvider, type OfferProgress, 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, type ResolvedSection, 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, centsToMajor, clearSdkSingleton, collectBlocks, collectSections, collectionHref, computeGlobalStyleTokens, defaultVariant, defineBlock, defineSection, defineThemeEntry, dynamicSource, eligibleUnitsInCart, findVariantByOptions, flattenMessages, focalSrc, formatMoney, formatMoneyMajor, getReactSingleton, getSdkSingleton, isDefinedBlock, isDefinedSection, isDynamicSource, isSdkAvailable, logoImgStyle, logoStyleTokens, majorToCents, multibuyOffers, offerBeatsRegularPrice, offerIncludesProduct, offerProgress, pickTranslations, productHref, publishVariantSelection, readVariantSelection, registerReactSingleton, registerSdkSingleton, requestNavigate, resolveDynamicValue, resolveFontStack, resolveSections, resolveSettingsMap, resolveSizeChart, resolveSourcePath, sanitizeHtml, selectChromeSections, selectTemplateSections, useActivePromotions, useAnalytics, useApp, useArticle, useArticles, useBlog, useBlogs, useCachedResource, useCart, useCheckout, useCollection, useCollectionOptional, useCollections, useCurrency, useCurrentTemplate, useCustomer, useCustomerActions, useCustomerAddresses, useDirection, useFieldTranslation, useGiftCardBalance, useImage, useListingHeading, useLocale, useLocalization, useMetafield, useMetafields, useMoney, useNavigation, useNumberFormat, useOrder, useOrders, usePage, useProduct, useProductOptional, useProductSizeChart, useProducts, useRelatedProducts, useReorder, useResolvedSettings, useSearch, useSection, useSectionGroup, useSectionOptional, useShippingRates, useShop, useThemeSettings, useTranslation, useVariantSelection, useWishlist };