@numueg/theme-sdk 0.1.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.
@@ -0,0 +1,1645 @@
1
+ import { S as Store, P as Product, C as Collection, a as Cart, b as Customer, c as Page, d as ProductVariant } from './index-CGx6FNqb.js';
2
+ export { A as Address, e as CartItem, O as Order, f as OrderItem, g as ProductImage, h as ProductOption } from './index-CGx6FNqb.js';
3
+ import { T as ThemeSettingsV3, S as SectionInstance, B as BlockSchema, a as BlockProps$1, b as SectionSchema, c as SectionProps$1 } from './theme-NWJAU9jd.js';
4
+ export { d as BlockInstance, E as ExternalThemeMetadata, P as PageTemplate, e as SectionGroup, f as SectionPreset, g as SettingDefinition } from './theme-NWJAU9jd.js';
5
+ import * as react from 'react';
6
+ import { ReactNode, ElementType, ImgHTMLAttributes, AnchorHTMLAttributes, ButtonHTMLAttributes, HTMLAttributes, FormHTMLAttributes, ComponentType } from 'react';
7
+ import * as react_jsx_runtime from 'react/jsx-runtime';
8
+ export { resolveThemeSettings } from './normalize.js';
9
+
10
+ /**
11
+ * Augmented Store shape returned from useShop().
12
+ *
13
+ * Adds a couple of conveniences theme devs reach for constantly:
14
+ * - `domain` — the active hostname (subdomain or custom). Resolved
15
+ * from `store.domain ?? store.subdomain ?? store.slug`.
16
+ * - `formatUrl(path)` — emits a fully-qualified URL using `domain`.
17
+ * Theme code that needs to render absolute URLs (canonical tags,
18
+ * Open Graph, JSON-LD) goes through this so dev/prod / subdomain
19
+ * vs. custom-domain stays correct without each theme reinventing
20
+ * the resolver.
21
+ */
22
+ interface ShopWithHelpers extends Store {
23
+ /** Fully-qualified hostname this store currently serves on. */
24
+ domain: string;
25
+ /**
26
+ * Format a path (relative or absolute) as a fully-qualified URL on
27
+ * this store's domain. No-ops when given an already-absolute URL.
28
+ *
29
+ * Phase 6 — when the active locale is non-default and the store
30
+ * has opted into locale URL prefixes, the path is prefixed with
31
+ * `/{locale}/` (e.g. `/ar/products/foo`). Already-prefixed paths
32
+ * are left alone so calling `formatUrl(formatUrl(...))` is safe.
33
+ */
34
+ formatUrl(path: string): string;
35
+ }
36
+ declare function useShop(): ShopWithHelpers;
37
+
38
+ declare function useProduct(): Product;
39
+ declare function useProductOptional(): Product | null;
40
+
41
+ declare function useCollection(): Collection;
42
+ declare function useCollectionOptional(): Collection | null;
43
+
44
+ declare function useCart(): {
45
+ cart: Cart;
46
+ addItem: (productId: string, variantId?: string, quantity?: number) => Promise<void>;
47
+ removeItem: (itemId: string) => Promise<void>;
48
+ updateQuantity: (itemId: string, quantity: number) => Promise<void>;
49
+ applyDiscount: (code: string) => Promise<void>;
50
+ removeDiscount: () => Promise<void>;
51
+ updateNote: (note: string) => Promise<void>;
52
+ clearCart: () => Promise<void>;
53
+ loading: boolean;
54
+ };
55
+
56
+ declare function useCustomer(): Customer | null;
57
+
58
+ declare function useThemeSettings(): ThemeSettingsV3;
59
+
60
+ interface LocalizationState {
61
+ locale: string;
62
+ direction: "ltr" | "rtl";
63
+ translations: Record<string, string>;
64
+ formatMoney: (amount: number, currency?: string) => string;
65
+ formatDate: (date: string | Date) => string;
66
+ /**
67
+ * Phase 3.7 — locale-aware number formatter. Routes to either
68
+ * Western (1234) or Arab-Indic (١٢٣٤) digits depending on
69
+ * `store.settings.numerals`. Themes calling formatMoney get the
70
+ * same digit choice automatically; this is for raw counts ("12 items").
71
+ */
72
+ formatNumber: (n: number, options?: Intl.NumberFormatOptions) => string;
73
+ /**
74
+ * Phase 3.6 — switch the active locale.
75
+ *
76
+ * Sets the `numu_locale` cookie and triggers a full page reload so
77
+ * the server-rendered layout picks up the new locale (the storefront
78
+ * resolves locale at SSR time from cookie/query). Returns once the
79
+ * cookie is written; the page navigation cancels any pending React
80
+ * work so callers don't need to await.
81
+ */
82
+ setLocale: (next: string) => void;
83
+ /**
84
+ * Phase 3.6 — list of locales the store advertises. Empty when the
85
+ * store hasn't configured a multi-locale catalog. Themes use this
86
+ * to decide whether to render the LocaleSwitcher at all.
87
+ */
88
+ availableLocales: string[];
89
+ }
90
+ declare const ShopContext: react.Context<Store | null>;
91
+ declare const ProductContext: react.Context<Product | null>;
92
+ declare const CollectionContext: react.Context<Collection | null>;
93
+ declare const CartContext: react.Context<{
94
+ cart: Cart;
95
+ addItem: (productId: string, variantId?: string, quantity?: number) => Promise<void>;
96
+ removeItem: (itemId: string) => Promise<void>;
97
+ updateQuantity: (itemId: string, quantity: number) => Promise<void>;
98
+ applyDiscount: (code: string) => Promise<void>;
99
+ removeDiscount: () => Promise<void>;
100
+ updateNote: (note: string) => Promise<void>;
101
+ clearCart: () => Promise<void>;
102
+ loading: boolean;
103
+ } | null>;
104
+ declare const CustomerContext: react.Context<Customer | null>;
105
+ declare const ThemeSettingsContext: react.Context<ThemeSettingsV3 | null>;
106
+ declare const LocalizationContext: react.Context<LocalizationState | null>;
107
+ declare const PageContext: react.Context<Page | null>;
108
+
109
+ declare function useLocalization(): LocalizationState;
110
+ declare function useDirection(): "ltr" | "rtl";
111
+ declare function useLocale(): string;
112
+ declare function useTranslation(): {
113
+ t: (key: string, fallback?: string) => string;
114
+ locale: string;
115
+ };
116
+ /**
117
+ * Phase 3.6 — pull a translated field off a domain object.
118
+ *
119
+ * Convention: per-product translations live on `entity.attributes`
120
+ * keyed as `<field>_<locale>` — e.g. `name_ar`, `description_ar`.
121
+ * Backend writes these via the merchant hub when a merchant turns on
122
+ * "Translations" for a product. Themes call:
123
+ *
124
+ * const productName = useFieldTranslation(product, "name");
125
+ *
126
+ * and get the Arabic name when the active locale is "ar", falling
127
+ * back to the English `product.name` otherwise. Works for any object
128
+ * that pairs a base field on the entity with an `attributes` JSONB
129
+ * blob holding the translated variants.
130
+ *
131
+ * Why not put translations on the entity itself? Adding `name_ar`,
132
+ * `description_ar`, `name_he`, etc. as first-class columns means a
133
+ * schema migration every time a merchant enables a new locale. JSONB
134
+ * lets the merchant flip locales on/off in settings without touching
135
+ * the table.
136
+ */
137
+ declare function useFieldTranslation<T extends {
138
+ attributes?: Record<string, unknown>;
139
+ }>(entity: T | null | undefined, field: string): string | undefined;
140
+ /**
141
+ * Phase 3.7 — formatted-number hook.
142
+ *
143
+ * Themes that need to render counts ("12 items", "3 reviews") should
144
+ * route through this hook so Arab-Indic digits (٠١٢٣٤) vs Western
145
+ * (01234) stay consistent with money + date elsewhere on the page.
146
+ *
147
+ * const fmtNum = useNumberFormat();
148
+ * fmtNum(12) → "12" (or "١٢" for Arabic stores)
149
+ * fmtNum(12.5, { minimumFractionDigits: 2 }) → "12.50" / "١٢٫٥٠"
150
+ */
151
+ declare function useNumberFormat(): (n: number, options?: Intl.NumberFormatOptions) => string;
152
+
153
+ declare function usePage(): Page | null;
154
+
155
+ declare const SectionContext: react.Context<SectionInstance | null>;
156
+ declare function useSection(): SectionInstance;
157
+ declare function useSectionOptional(): SectionInstance | null;
158
+
159
+ /**
160
+ * useMoney — convenience hook that returns a stable formatter bound to
161
+ * the active store's currency.
162
+ *
163
+ * Useful when a section needs to format multiple amounts in a render:
164
+ *
165
+ * const money = useMoney();
166
+ * return <td>{money(item.price * item.quantity)}</td>
167
+ *
168
+ * For one-off price displays prefer the <Money> component which handles
169
+ * compare-at and inline rendering.
170
+ */
171
+ declare function useMoney(currencyOverride?: string): (amount: number) => string;
172
+
173
+ interface UseImageOptions {
174
+ /** Sizes attribute (CSS sizes form). */
175
+ sizes?: string;
176
+ /** Widths to generate in srcSet. Falsy → use defaults. */
177
+ widths?: number[];
178
+ /** Pass-through alt text — useImage doesn't render anything; the
179
+ * caller does. */
180
+ alt?: string;
181
+ }
182
+ interface ImageDescriptor {
183
+ src: string | null;
184
+ srcSet: string | null;
185
+ sizes: string;
186
+ alt: string;
187
+ }
188
+ /**
189
+ * useImage — returns srcSet/sizes for a CDN-served image so themes
190
+ * don't have to hand-roll responsive image markup. Pairs with the
191
+ * <Image> component, but is also useful when the theme wants to render
192
+ * something other than a plain `<img>` (e.g., `<picture>` with art
193
+ * direction or a CSS `background-image` via inline style).
194
+ *
195
+ * Returns `null` srcSet if the source URL already encodes a width
196
+ * directive (theme author opted out of auto-sizing).
197
+ */
198
+ declare function useImage(src: string | null | undefined, opts?: UseImageOptions): ImageDescriptor;
199
+
200
+ interface UseProductsOptions {
201
+ /** Limit the slice returned. Defaults to whatever the host gave us. */
202
+ limit?: number;
203
+ /**
204
+ * When true, fetch from `/api/products` if the host didn't provide a
205
+ * list via PageContext. Useful for sections rendered on routes that
206
+ * don't pre-fetch products (e.g. a custom CMS page that wants a
207
+ * "featured" rail). Default: false — themes typically prefer SSR data.
208
+ */
209
+ fetchIfMissing?: boolean;
210
+ }
211
+ interface UseProductsResult {
212
+ products: Product[];
213
+ loading: boolean;
214
+ error: Error | null;
215
+ }
216
+ /**
217
+ * useProducts — read the storefront-pre-fetched product list from the
218
+ * page context, optionally falling back to a client-side fetch.
219
+ *
220
+ * The SSR path passes `page.data.products: Product[]` from
221
+ * `numu-storefront/src/app/[domain]/page.tsx`; sections on the home
222
+ * route get them for free. For other routes that don't pre-fetch, set
223
+ * `fetchIfMissing: true` and we'll hit `/api/products`.
224
+ */
225
+ declare function useProducts(opts?: UseProductsOptions): UseProductsResult;
226
+
227
+ interface UseCollectionsOptions {
228
+ limit?: number;
229
+ fetchIfMissing?: boolean;
230
+ }
231
+ interface UseCollectionsResult {
232
+ collections: Collection[];
233
+ loading: boolean;
234
+ error: Error | null;
235
+ }
236
+ /**
237
+ * useCollections — analog to useProducts. Reads page.data.collections
238
+ * pre-fetched by the storefront SSR; falls back to /api/collections
239
+ * when fetchIfMissing is true.
240
+ */
241
+ declare function useCollections(opts?: UseCollectionsOptions): UseCollectionsResult;
242
+
243
+ /**
244
+ * The action surface a theme can call to drive customer auth +
245
+ * profile mutations. Each action returns the parsed response body
246
+ * (or throws on network failure); the *result* of the operation is
247
+ * conveyed by the response shape, not the throw boundary.
248
+ *
249
+ * The provider implements these by calling /api/customer/* proxy
250
+ * routes on the storefront, which in turn forward to FastAPI's
251
+ * /storefront/store/{store_id}/auth/* and /storefront/me/* endpoints.
252
+ *
253
+ * Why a separate context (vs bundling onto CustomerContext): the
254
+ * value `Customer | null` was already in CustomerContext for
255
+ * back-compat with themes that read `useCustomer()`. Adding a
256
+ * sibling context for actions keeps the existing read hook stable
257
+ * while letting themes opt into actions via `useCustomerActions()`.
258
+ */
259
+ interface CustomerActions {
260
+ /** Email + password → returns { success, data?, error? }. On
261
+ * success the SDK refreshes the customer context automatically. */
262
+ login: (input: {
263
+ email: string;
264
+ password: string;
265
+ }) => Promise<unknown>;
266
+ /** Create a new customer + log them in. Some stores require
267
+ * email verification before login is fully usable; this flow
268
+ * still issues the auth cookie so the customer can verify from
269
+ * the welcome email and land logged-in. */
270
+ register: (input: {
271
+ email: string;
272
+ password: string;
273
+ first_name?: string;
274
+ last_name?: string;
275
+ phone?: string;
276
+ accepts_marketing?: boolean;
277
+ }) => Promise<unknown>;
278
+ /** Clear the customer_access_token cookie + refresh the context
279
+ * to null. Idempotent: safe to call when not logged in. */
280
+ logout: () => Promise<unknown>;
281
+ /** Issue a password-reset email. Anti-enumeration: response
282
+ * never reveals whether the email exists. */
283
+ requestRecover: (input: {
284
+ email: string;
285
+ }) => Promise<unknown>;
286
+ /** Submit the new password with the token from the recovery email. */
287
+ confirmReset: (input: {
288
+ token: string;
289
+ password: string;
290
+ }) => Promise<unknown>;
291
+ /** Verify the email with the token from the welcome email. */
292
+ verifyEmail: (input: {
293
+ token: string;
294
+ }) => Promise<unknown>;
295
+ /** Resend the verification email. Server-side rate-limited to
296
+ * prevent email-bombing. */
297
+ resendVerification: (input: {
298
+ email: string;
299
+ }) => Promise<unknown>;
300
+ /** Update the customer's profile (name / phone / marketing). */
301
+ updateProfile: (input: {
302
+ first_name?: string;
303
+ last_name?: string;
304
+ phone?: string;
305
+ accepts_marketing?: boolean;
306
+ }) => Promise<unknown>;
307
+ /** Change the customer's password from inside the dashboard.
308
+ * Backend requires the current password as confirmation. */
309
+ changePassword: (input: {
310
+ current_password: string;
311
+ new_password: string;
312
+ }) => Promise<unknown>;
313
+ /** Re-fetch the customer profile from the backend and replace
314
+ * the context value. Themes call this after performing any
315
+ * out-of-band mutation that changed the customer's record. */
316
+ refresh: () => Promise<void>;
317
+ }
318
+
319
+ /**
320
+ * Returns the customer-mutation actions provided by NuMuProvider.
321
+ *
322
+ * Pair with `useCustomer()` for read access:
323
+ *
324
+ * const customer = useCustomer();
325
+ * const { login, logout, register } = useCustomerActions();
326
+ *
327
+ * The actions hit the storefront's `/api/customer/*` proxies, which
328
+ * own the cookie/CSRF/idempotency story. Themes don't talk to the
329
+ * backend directly — they get a stable client-side surface here.
330
+ */
331
+ declare function useCustomerActions(): CustomerActions;
332
+
333
+ /**
334
+ * Customer order history.
335
+ *
336
+ * `useOrders()` returns the logged-in customer's orders, paginated.
337
+ * The hook is gated on `useCustomer()` — when null (anonymous
338
+ * visitor), it returns an empty list without hitting the network.
339
+ *
340
+ * `useOrder(id)` fetches a single order. Backend rejects with 404
341
+ * when the id doesn't belong to the customer regardless of whether
342
+ * it exists for someone else (avoids enumeration).
343
+ */
344
+ interface OrderListEntry {
345
+ id: string;
346
+ order_number: string;
347
+ status: string;
348
+ payment_status: string;
349
+ fulfillment_status: string;
350
+ total: number;
351
+ currency: string;
352
+ created_at: string | null;
353
+ item_count?: number;
354
+ }
355
+ interface OrderListState {
356
+ orders: OrderListEntry[];
357
+ loading: boolean;
358
+ error: Error | null;
359
+ refresh: () => void;
360
+ }
361
+ interface OrderDetail extends OrderListEntry {
362
+ line_items: unknown[];
363
+ shipping_address?: Record<string, unknown> | null;
364
+ billing_address?: Record<string, unknown> | null;
365
+ subtotal: number;
366
+ shipping_cost: number;
367
+ tax_amount: number;
368
+ discount_amount: number;
369
+ }
370
+ interface OrderState {
371
+ order: OrderDetail | null;
372
+ loading: boolean;
373
+ error: Error | null;
374
+ refresh: () => void;
375
+ }
376
+ declare function useOrders(): OrderListState;
377
+ declare function useOrder(id: string | null | undefined): OrderState;
378
+
379
+ /**
380
+ * Customer address book. Used by:
381
+ * - The /account/addresses dashboard (list + CRUD).
382
+ * - Checkout autofill (default address selected by default).
383
+ *
384
+ * Mutations return the updated row (or void on delete) and bump the
385
+ * internal tick so the list reflects state changes immediately.
386
+ * Anonymous visitors get an empty list with no network calls.
387
+ */
388
+ interface CustomerAddress {
389
+ id: string;
390
+ first_name?: string | null;
391
+ last_name?: string | null;
392
+ address_line1?: string | null;
393
+ address_line2?: string | null;
394
+ city?: string | null;
395
+ state?: string | null;
396
+ postal_code?: string | null;
397
+ country?: string | null;
398
+ phone?: string | null;
399
+ label?: string | null;
400
+ is_default?: boolean;
401
+ latitude?: number | null;
402
+ longitude?: number | null;
403
+ }
404
+ type AddressInput = Omit<CustomerAddress, "id" | "is_default"> & {
405
+ is_default?: boolean;
406
+ };
407
+ interface CustomerAddressesState {
408
+ addresses: CustomerAddress[];
409
+ loading: boolean;
410
+ error: Error | null;
411
+ refresh: () => void;
412
+ addAddress: (input: AddressInput) => Promise<CustomerAddress | null>;
413
+ updateAddress: (id: string, input: Partial<AddressInput>) => Promise<CustomerAddress | null>;
414
+ deleteAddress: (id: string) => Promise<boolean>;
415
+ setDefaultAddress: (id: string) => Promise<boolean>;
416
+ }
417
+ declare function useCustomerAddresses(): CustomerAddressesState;
418
+
419
+ /**
420
+ * Menu item, modeled on Shopify's `linklists` entries.
421
+ *
422
+ * Themes typically render header/footer nav as a tree — we expose
423
+ * `children` so a single fetch hydrates an arbitrary depth. Backends
424
+ * that don't support nesting return a flat list (children=[]).
425
+ */
426
+ interface NavigationItem {
427
+ id: string;
428
+ title: string;
429
+ /** The URL the menu item points to — relative path or absolute URL. */
430
+ url: string;
431
+ /** Optional foreign keys when the item references a typed resource. */
432
+ resource_type?: "product" | "collection" | "page" | "blog" | "article" | "url" | null;
433
+ resource_handle?: string | null;
434
+ children: NavigationItem[];
435
+ }
436
+ interface NavigationState {
437
+ /** Items in display order. Empty array when the menu is missing or still loading. */
438
+ items: NavigationItem[];
439
+ loading: boolean;
440
+ /** Non-null when the fetch failed — the menu still resolves to []. */
441
+ error: Error | null;
442
+ }
443
+ /**
444
+ * Fetch a merchant-managed nav menu by handle.
445
+ *
446
+ * Backend contract: GET /api/storefront/navigation/{handle} →
447
+ * { items: NavigationItem[] }
448
+ * On 404 / network error / non-OK response the hook returns an empty
449
+ * list — the calling theme decides whether to render nothing or show
450
+ * a fallback. We deliberately don't throw because a missing menu is
451
+ * a soft failure (theme should still render).
452
+ *
453
+ * Why no SSR pre-fetch:
454
+ * The storefront's [domain]/layout doesn't currently inject menus
455
+ * into `page.data.navigation`. Once it does, themes can pass an
456
+ * `initialItems` prop (added below) and skip the round-trip; until
457
+ * then we fetch on mount with a process-local cache so the same
458
+ * handle doesn't fetch twice per session.
459
+ */
460
+ declare function useNavigation(handle: string, options?: {
461
+ initialItems?: NavigationItem[];
462
+ }): NavigationState;
463
+
464
+ /**
465
+ * Mixed-result search shape. The backend tsvector implementation lands in
466
+ * Phase 4; until then this hook talks to /api/storefront/search and
467
+ * gracefully degrades (empty results) when the endpoint isn't available.
468
+ *
469
+ * Two modes:
470
+ * - "predictive" (default): debounced, capped at 5 per type, returns
471
+ * fast for autocomplete dropdowns.
472
+ * - "full": full result set with paging, used by /search pages.
473
+ */
474
+ interface SearchResults {
475
+ products: Product[];
476
+ collections: Collection[];
477
+ pages: Page[];
478
+ /** Articles will populate once the blog backend lands; field shape is
479
+ * reserved here so themes can write code that doesn't break on
480
+ * upgrade. */
481
+ articles: Array<{
482
+ id: string;
483
+ title: string;
484
+ handle: string;
485
+ excerpt?: string;
486
+ }>;
487
+ total: number;
488
+ }
489
+ interface UseSearchOptions {
490
+ /** "predictive" debounces and caps; "full" returns paged results. */
491
+ mode?: "predictive" | "full";
492
+ /** Restrict to specific result types. Default: all. */
493
+ types?: Array<"products" | "collections" | "pages" | "articles">;
494
+ /** Per-type cap. Predictive mode defaults to 5; full mode defaults to 24. */
495
+ limit?: number;
496
+ /** Debounce window in ms for predictive mode. Default 200. */
497
+ debounceMs?: number;
498
+ /** Skip the debounce (e.g. when the user submits the form). */
499
+ immediate?: boolean;
500
+ }
501
+ interface SearchState {
502
+ query: string;
503
+ results: SearchResults;
504
+ loading: boolean;
505
+ error: Error | null;
506
+ }
507
+ declare function useSearch(query: string, options?: UseSearchOptions): SearchState;
508
+
509
+ /**
510
+ * Theme-facing analytics dispatcher.
511
+ *
512
+ * Two delivery channels, fired in parallel:
513
+ * 1. Server-side track: POST /api/storefront/track with the event
514
+ * payload. The backend fans out to merchant-configured pixels
515
+ * (GA4 Measurement Protocol, Meta CAPI, TikTok Events API). The
516
+ * server-side fanout lands in Phase 4 — until then the endpoint
517
+ * either no-ops or returns 404, which is fine; we don't await
518
+ * the response because failed pixels must never block the UI.
519
+ * 2. Window CustomEvent (`numu:analytics:event`): so theme devs can
520
+ * wire their own GTM container, third-party SDK, or in-house
521
+ * pixel without going through the server. Detail shape mirrors
522
+ * what gets POSTed.
523
+ *
524
+ * Standard event names (recommended for compatibility with GA4 / Meta):
525
+ * page_view, view_item, view_collection, search, add_to_cart,
526
+ * remove_from_cart, view_cart, begin_checkout, add_payment_info,
527
+ * add_shipping_info, purchase, refund, sign_up, login,
528
+ * add_to_wishlist, share. Custom names are also fine — the dispatcher
529
+ * does not validate, so themes can ship store-specific events.
530
+ */
531
+ interface AnalyticsPayload {
532
+ /** Standard or custom event name. Lowercase + underscores recommended. */
533
+ [key: string]: unknown;
534
+ }
535
+ interface AnalyticsApi {
536
+ /**
537
+ * Fire an event. Non-blocking — both the fetch and the CustomEvent
538
+ * dispatch happen in the next microtask; theme code shouldn't await.
539
+ */
540
+ track: (eventName: string, payload?: AnalyticsPayload) => void;
541
+ }
542
+ declare function useAnalytics(): AnalyticsApi;
543
+
544
+ /**
545
+ * Read app-provided data + manifest for a slug installed on the
546
+ * current store.
547
+ *
548
+ * Phase 6 wired this to the real backend at
549
+ * `/api/storefront/store/{store_id}/apps/{slug}`. Theme usage:
550
+ *
551
+ * const recommend = useApp<RecommendationData>("recommendation-engine");
552
+ * if (!recommend.available) return <Fallback />;
553
+ * if (recommend.loading) return <Skeleton />;
554
+ * return <RecommendList items={recommend.data?.products ?? []} />;
555
+ *
556
+ * `available` flips to true only when an enabled installation exists
557
+ * for the store. Apps the merchant hasn't installed surface as
558
+ * `{ available: false }` rather than as a network error — themes
559
+ * branch on availability without try/catch.
560
+ *
561
+ * The hook revalidates whenever the slug changes, but does NOT refetch
562
+ * on focus / interval — app data is usually slow-changing (config +
563
+ * manifest). Themes that need live data should layer their own
564
+ * refresh on top of the returned `refresh()` callback.
565
+ */
566
+ interface AppManifestBlock {
567
+ type: string;
568
+ name: string;
569
+ block_schema: Record<string, unknown>;
570
+ }
571
+ interface AppPayload<T = unknown> {
572
+ slug: string;
573
+ name: string;
574
+ description: string | null;
575
+ icon_url: string | null;
576
+ version: string;
577
+ manifest: Record<string, unknown>;
578
+ settings: Record<string, unknown>;
579
+ blocks: AppManifestBlock[];
580
+ /** App-provided data (shape defined by the app developer). Today
581
+ * the response always returns null for `data` — apps emit data via
582
+ * their `endpoints.data` URL, which the theme fetches separately.
583
+ * Surfaced here so the field is stable when v2 lands the proxy. */
584
+ data: T | null;
585
+ }
586
+ interface AppState<T = unknown> {
587
+ data: AppPayload<T> | null;
588
+ loading: boolean;
589
+ available: boolean;
590
+ error: Error | null;
591
+ /** Re-fetch the install. Returns a promise that resolves when the
592
+ * request settles (regardless of success). */
593
+ refresh: () => Promise<void>;
594
+ }
595
+ declare function useApp<T = unknown>(slug: string): AppState<T>;
596
+
597
+ /**
598
+ * Wishlist hook with localStorage fallback.
599
+ *
600
+ * Architecture (Phase 4 lands the server side):
601
+ * - Authenticated customer: persists to /api/customer/me/wishlist
602
+ * (server-backed, syncs across devices).
603
+ * - Anonymous visitor: persists to localStorage under
604
+ * `numu_wishlist_<store_id>`. On login, the wishlist merges into
605
+ * the server-side list (mirrors the cart's session→customer flow).
606
+ *
607
+ * v1 implementation: localStorage only. The /api/customer/me/wishlist
608
+ * endpoint isn't wired yet; an authed visitor still gets the local
609
+ * fallback so themes work end-to-end. When the endpoint lands, the
610
+ * mutation methods will short-circuit to the server fetch and the
611
+ * effect below will drop the localStorage path for authed users.
612
+ */
613
+ interface WishlistItem {
614
+ product_id: string;
615
+ /** Optional variant scoping — themes that show variant pickers can
616
+ * wishlist a specific size/color combo separately. */
617
+ variant_id?: string | null;
618
+ /** Server timestamp once persistence lands; ms-since-epoch in v1. */
619
+ added_at: number;
620
+ }
621
+ interface WishlistState {
622
+ items: WishlistItem[];
623
+ loading: boolean;
624
+ has: (productId: string, variantId?: string | null) => boolean;
625
+ addToWishlist: (productId: string, variantId?: string | null) => void;
626
+ removeFromWishlist: (productId: string, variantId?: string | null) => void;
627
+ clear: () => void;
628
+ }
629
+ /**
630
+ * @param storeId The store the wishlist is scoped to. Pass `useShop().id`.
631
+ * Without it, the hook can't keep one merchant's wishlist
632
+ * from leaking into another's localStorage on a shared
633
+ * domain (e.g. apex preview).
634
+ */
635
+ declare function useWishlist(storeId: string): WishlistState;
636
+
637
+ /**
638
+ * Fetch products related to the given product (same category, excluding
639
+ * self). Phase 3 lands collaborative-filtering / "frequently bought
640
+ * together"; v1 ships the simpler same-category-minus-self heuristic.
641
+ *
642
+ * Backend contract: GET /api/storefront/products/{id}/related?limit=N
643
+ * → { items: Product[] } | Product[]
644
+ *
645
+ * Returns an empty list (no error) when the endpoint is missing or the
646
+ * product has no siblings — themes should branch on `items.length` and
647
+ * either render the section or skip it entirely.
648
+ */
649
+ interface RelatedProductsState {
650
+ items: Product[];
651
+ loading: boolean;
652
+ error: Error | null;
653
+ }
654
+ declare function useRelatedProducts(productId: string | null | undefined, options?: {
655
+ limit?: number;
656
+ }): RelatedProductsState;
657
+
658
+ /**
659
+ * Multi-currency presentment — Phase 6.
660
+ *
661
+ * The store's *capture* currency (what Paymob/Stripe/etc. charges)
662
+ * never changes mid-session. This hook is purely about **display**:
663
+ * letting visitors browse prices in a currency they recognize.
664
+ *
665
+ * Usage:
666
+ *
667
+ * const { base, selected, presentment, convert, setSelected } = useCurrency();
668
+ * <p>{convert(product.price.amount_cents)} {selected}</p>
669
+ * {presentment.length > 1 && (
670
+ * <select value={selected} onChange={(e) => setSelected(e.target.value)}>
671
+ * {presentment.map(c => <option key={c}>{c}</option>)}
672
+ * </select>
673
+ * )}
674
+ *
675
+ * Behavior:
676
+ * - `selected` defaults to the persisted `numu_currency` cookie if
677
+ * valid, else `default_presentment`, else `base`.
678
+ * - `setSelected` writes the cookie (path=/, 30d) so navigation
679
+ * preserves the choice across pages.
680
+ * - `convert(cents)` returns the converted cents in `selected`,
681
+ * using the rates from the API. When no rate exists, returns
682
+ * the input unchanged (theme renders in base — better than a
683
+ * wrong number).
684
+ *
685
+ * Use `<CurrencySwitcher>` from the SDK for an opinionated UI, or
686
+ * read this hook directly for full control.
687
+ */
688
+ interface CurrencyConfig {
689
+ base: string;
690
+ default_presentment: string;
691
+ presentment: string[];
692
+ rates: Record<string, string>;
693
+ auto_convert: boolean;
694
+ }
695
+ interface CurrencyState {
696
+ base: string;
697
+ selected: string;
698
+ presentment: string[];
699
+ rates: Record<string, number>;
700
+ autoConvert: boolean;
701
+ loading: boolean;
702
+ setSelected: (currency: string) => void;
703
+ convert: (cents: number, target?: string) => number;
704
+ }
705
+ declare function useCurrency(): CurrencyState;
706
+
707
+ /**
708
+ * Variant resolution helpers — Phase 8.1.
709
+ *
710
+ * Themes render a variant picker (Size + Color radios), then ask the
711
+ * SDK "given my current selection, which variant is it?" The matching
712
+ * variant's id is what add-to-cart sends.
713
+ */
714
+
715
+ /**
716
+ * Pick the variant that exactly matches the given option_values map.
717
+ * Returns null when no variant matches — themes render that as
718
+ * "Combination unavailable" or a disabled buy button.
719
+ */
720
+ declare function findVariantByOptions(product: Pick<Product, "variants">, selection: Record<string, string>): ProductVariant | null;
721
+ /**
722
+ * Find the "default" variant — the one the PDP should auto-select on
723
+ * first render. Prefers the first in-stock variant; falls back to the
724
+ * first variant if all are out of stock (so the picker still shows
725
+ * something coherent).
726
+ */
727
+ declare function defaultVariant(product: Pick<Product, "variants">): ProductVariant | null;
728
+ /**
729
+ * Given a partial selection (e.g. just Size=M, no Color yet), return
730
+ * the set of values still available on each unselected axis. Themes
731
+ * use this to disable swatches whose paired variants are all out of
732
+ * stock for the current selection.
733
+ */
734
+ declare function availableValues(product: Pick<Product, "options" | "variants">, selection: Record<string, string>): Record<string, Set<string>>;
735
+
736
+ interface UseVariantSelection {
737
+ /** Currently-selected axis → value map. */
738
+ selection: Record<string, string>;
739
+ /** The variant matching the current selection, if any. */
740
+ variant: ProductVariant | null;
741
+ /** Pick a value on one axis. Other axes stay locked. */
742
+ select: (axis: string, value: string) => void;
743
+ /** Reset the selection (e.g. on "Choose another" button). */
744
+ reset: () => void;
745
+ /**
746
+ * For each unselected axis, the set of values that lead to at
747
+ * least one in-stock variant given the current locked axes.
748
+ * Themes use this to grey out swatches.
749
+ */
750
+ availability: Record<string, Set<string>>;
751
+ /**
752
+ * True when every option axis on the product has a chosen value
753
+ * and `variant` is resolved. Buy buttons should disable until
754
+ * this flips to true on products with options.
755
+ */
756
+ isComplete: boolean;
757
+ }
758
+ /**
759
+ * Hook that owns variant-axis selection state for a PDP.
760
+ *
761
+ * Initial state auto-selects the default variant's axis values so
762
+ * the PDP loads with a coherent price + image + buy state, matching
763
+ * Shopify's behavior. Themes that prefer "Choose your size" empty
764
+ * state can pass `autoSelect: false`.
765
+ */
766
+ declare function useVariantSelection(product: Pick<Product, "options" | "variants">, opts?: {
767
+ autoSelect?: boolean;
768
+ }): UseVariantSelection;
769
+
770
+ /**
771
+ * Gift card balance check — Phase 8.3.
772
+ *
773
+ * Themes call this from the checkout payment step to validate a
774
+ * customer-typed code before sending it as part of the checkout
775
+ * payload. The backend returns 404 for any non-redeemable card
776
+ * (expired, depleted, voided, or wrong store) — the response shape
777
+ * stays uniform so the hook can't be used to probe for valid codes.
778
+ *
779
+ * Backend contract (via storefront proxy):
780
+ * GET /api/gift-cards/{code}
781
+ * → 200 { data: { last_four, current_balance_cents, currency,
782
+ * expires_at? } }
783
+ * → 404 { error: { code, message } }
784
+ */
785
+ interface GiftCardBalance {
786
+ last_four: string;
787
+ current_balance_cents: number;
788
+ currency: string;
789
+ expires_at: string | null;
790
+ }
791
+ interface UseGiftCardBalance {
792
+ /** Last lookup result, or null when nothing checked yet / failed. */
793
+ balance: GiftCardBalance | null;
794
+ /** True while a lookup is in flight. */
795
+ loading: boolean;
796
+ /** Latest error from the lookup, if any. Cleared on next check. */
797
+ error: Error | null;
798
+ /** Trigger a balance check. Returns the result or null on failure. */
799
+ check: (code: string) => Promise<GiftCardBalance | null>;
800
+ /** Clear the last result + error (e.g. when the user clears the input). */
801
+ reset: () => void;
802
+ }
803
+ declare function useGiftCardBalance(): UseGiftCardBalance;
804
+
805
+ /**
806
+ * Reorder / "buy again" — Phase 8.5.
807
+ *
808
+ * Clones every line item from an existing order into the current cart.
809
+ * The backend skips lines whose product is deleted/archived, whose
810
+ * variant is gone, or that are out of stock — returning per-line
811
+ * reasons so themes can show a "couldn't add X items" banner.
812
+ *
813
+ * Backend contract:
814
+ * POST /storefront/me/orders/{order_id}/reorder
815
+ * → 200 { data: { added_count, skipped[], cart_total_items } }
816
+ * → 404 if order doesn't belong to the current customer
817
+ */
818
+ type ReorderSkipReason = "product_deleted" | "product_archived" | "out_of_stock" | "variant_unavailable";
819
+ interface ReorderSkippedItem {
820
+ product_id: string;
821
+ variant_id: string | null;
822
+ quantity: number;
823
+ reason: ReorderSkipReason | string;
824
+ }
825
+ interface ReorderResult {
826
+ added_count: number;
827
+ skipped: ReorderSkippedItem[];
828
+ cart_total_items: number;
829
+ }
830
+ interface UseReorder {
831
+ /** Last reorder result. */
832
+ result: ReorderResult | null;
833
+ /** True while a reorder is in flight. */
834
+ loading: boolean;
835
+ /** Latest error, cleared on next reorder call. */
836
+ error: Error | null;
837
+ /** Trigger the reorder. Returns the result or null on failure. */
838
+ reorder: (orderId: string) => Promise<ReorderResult | null>;
839
+ /** Clear the last result + error (e.g. after dismissing the banner). */
840
+ reset: () => void;
841
+ }
842
+ declare function useReorder(): UseReorder;
843
+
844
+ /**
845
+ * Programmatic checkout driver — Phase 7.6.
846
+ *
847
+ * The storefront's multi-step checkout pages (Phase 1.2) own the
848
+ * default flow: contact → shipping → payment → review → processing →
849
+ * thank-you. This hook gives themes a programmatic alternative so a
850
+ * BYOT theme can render the checkout exactly how it wants — single-
851
+ * page, accordion, full-bleed hero, whatever — without being forced
852
+ * through the platform's step pages.
853
+ *
854
+ * State lives in `numu_checkout_state` sessionStorage (same blob the
855
+ * platform's step pages use) so themes that drive checkout
856
+ * programmatically can hand off to the platform mid-flow (e.g. theme
857
+ * collects contact + shipping, then redirects to /checkout/payment
858
+ * for the gateway capture). The platform-side state machine guards
859
+ * deep-links via `hasContactStep / hasShippingStep / hasPaymentStep`
860
+ * regardless of how the state got populated.
861
+ *
862
+ * Usage:
863
+ *
864
+ * const checkout = useCheckout();
865
+ * checkout.contact.set({ email, phone, shipping_address });
866
+ * await checkout.shipping.refresh();
867
+ * checkout.shipping.select(rate.id);
868
+ * checkout.payment.select("paymob");
869
+ * const result = await checkout.placeOrder();
870
+ * if (result.payment_url) window.location.assign(result.payment_url);
871
+ */
872
+ type CheckoutStep = "contact" | "shipping" | "payment" | "review" | "processing";
873
+ interface CheckoutAddress {
874
+ first_name?: string;
875
+ last_name?: string;
876
+ line1?: string;
877
+ line2?: string | null;
878
+ city?: string;
879
+ state?: string | null;
880
+ postal_code?: string | null;
881
+ country?: string;
882
+ phone?: string | null;
883
+ }
884
+ interface ShippingRateOption {
885
+ id: string;
886
+ name: string;
887
+ amount_cents: number;
888
+ currency: string;
889
+ estimated_days_min?: number | null;
890
+ estimated_days_max?: number | null;
891
+ carrier?: string | null;
892
+ }
893
+ interface CheckoutSessionState {
894
+ email: string;
895
+ phone: string;
896
+ shipping_address: CheckoutAddress;
897
+ selected_shipping_rate_id: string | null;
898
+ shipping_method: string | null;
899
+ payment_method: string | null;
900
+ cod_requested: boolean;
901
+ deposit_gateway: string | null;
902
+ saved_payment_method_id: string | null;
903
+ customer_notes: string;
904
+ coupon_code: string;
905
+ }
906
+ interface PlaceOrderResult {
907
+ order_id: string;
908
+ order_number: string;
909
+ total: number;
910
+ currency: string;
911
+ payment_status: string;
912
+ payment_url?: string | null;
913
+ payment_data?: Record<string, unknown> | null;
914
+ }
915
+ interface CheckoutApi {
916
+ state: CheckoutSessionState;
917
+ step: CheckoutStep;
918
+ contact: {
919
+ set: (input: {
920
+ email?: string;
921
+ phone?: string;
922
+ shipping_address?: CheckoutAddress;
923
+ }) => void;
924
+ isComplete: () => boolean;
925
+ };
926
+ shipping: {
927
+ rates: ShippingRateOption[] | null;
928
+ loading: boolean;
929
+ refresh: () => Promise<ShippingRateOption[]>;
930
+ select: (rateId: string) => void;
931
+ isComplete: () => boolean;
932
+ };
933
+ payment: {
934
+ select: (method: string, opts?: {
935
+ saved_payment_method_id?: string;
936
+ deposit_gateway?: string;
937
+ }) => void;
938
+ isComplete: () => boolean;
939
+ };
940
+ setNotes: (notes: string) => void;
941
+ setCoupon: (code: string) => void;
942
+ placeOrder: () => Promise<PlaceOrderResult>;
943
+ reset: () => void;
944
+ }
945
+ declare function useCheckout(): CheckoutApi;
946
+
947
+ /**
948
+ * Fetch shipping rate options for an address — Phase 7.4.
949
+ *
950
+ * Themes that want to display rates on the cart page (Shopify
951
+ * pattern: "Shipping calculated at checkout — preview from cart")
952
+ * use this. It posts to /api/shipping/options the same way the
953
+ * platform's shipping step does, but as a self-contained hook so
954
+ * themes don't need to drive the full useCheckout() machinery for a
955
+ * simple display.
956
+ *
957
+ * Returns null when no address is provided (the typical pre-input
958
+ * state) — themes branch on `rates !== null` to decide whether to
959
+ * show the rates panel or a "Enter address" prompt.
960
+ *
961
+ * Optionally accepts a `location_id` for multi-location stores
962
+ * (Phase 8.2) — the resolver uses the named fulfilling location's
963
+ * origin address when calculating rates. Ignored if multi-location
964
+ * is off.
965
+ *
966
+ * Usage:
967
+ *
968
+ * const { rates, loading, refresh } = useShippingRates({
969
+ * address: { country: "EG", city: "Cairo" },
970
+ * });
971
+ */
972
+ interface UseShippingRatesOptions {
973
+ address?: CheckoutAddress | null;
974
+ location_id?: string | null;
975
+ /** Auto-fetch on mount + when the address changes. Default true. */
976
+ enabled?: boolean;
977
+ }
978
+ interface UseShippingRatesState {
979
+ rates: ShippingRateOption[] | null;
980
+ loading: boolean;
981
+ error: Error | null;
982
+ refresh: () => Promise<void>;
983
+ }
984
+ declare function useShippingRates({ address, location_id, enabled, }?: UseShippingRatesOptions): UseShippingRatesState;
985
+
986
+ interface NuMuProviderProps {
987
+ store: Store;
988
+ themeSettings: ThemeSettingsV3;
989
+ initialCart?: Cart;
990
+ customer?: Customer | null;
991
+ locale?: string;
992
+ translations?: Record<string, string>;
993
+ children: ReactNode;
994
+ }
995
+ declare function NuMuProvider({ store, themeSettings, initialCart, customer, locale: initialLocale, translations: initialTranslations, children, }: NuMuProviderProps): react_jsx_runtime.JSX.Element;
996
+
997
+ interface ProductProviderProps {
998
+ product: Product;
999
+ children: ReactNode;
1000
+ }
1001
+ declare function ProductProvider({ product, children }: ProductProviderProps): react_jsx_runtime.JSX.Element;
1002
+
1003
+ interface CollectionProviderProps {
1004
+ collection: Collection;
1005
+ children: ReactNode;
1006
+ }
1007
+ declare function CollectionProvider({ collection, children }: CollectionProviderProps): react_jsx_runtime.JSX.Element;
1008
+
1009
+ interface MoneyProps {
1010
+ /** Amount in major units (e.g. dollars, not cents). */
1011
+ amount: number;
1012
+ /** ISO-4217 currency code. Defaults to the active store's currency. */
1013
+ currency?: string;
1014
+ /**
1015
+ * When the same product has both a sale price and a compare-at price,
1016
+ * pass the higher (compare-at) value here and we'll render it with a
1017
+ * strike-through next to the active price. Skipped when undefined or
1018
+ * <= the main amount.
1019
+ */
1020
+ compareAt?: number;
1021
+ className?: string;
1022
+ /** Custom HTML element tag — defaults to `span`. */
1023
+ as?: ElementType;
1024
+ }
1025
+ /**
1026
+ * <Money amount={49.99} /> — formatted price, locale-aware.
1027
+ *
1028
+ * Wraps useLocalization().formatMoney so theme code stops re-implementing
1029
+ * Intl.NumberFormat. Renders inline with `dir="auto"` so the digits
1030
+ * flow naturally in RTL (Arabic) without flipping the currency symbol.
1031
+ *
1032
+ * Usage:
1033
+ * <Money amount={product.price} compareAt={product.compare_at_price} />
1034
+ */
1035
+ declare function Money({ amount, currency, compareAt, className, as, }: MoneyProps): react.ReactElement<any, string | react.JSXElementConstructor<any>>;
1036
+
1037
+ interface ImageProps extends Omit<ImgHTMLAttributes<HTMLImageElement>, "src" | "srcSet"> {
1038
+ src: string | undefined | null;
1039
+ alt: string;
1040
+ /**
1041
+ * Comma-separated breakpoints for the `sizes` attribute. Default
1042
+ * matches a typical responsive grid:
1043
+ * "(min-width: 1024px) 25vw, (min-width: 640px) 50vw, 100vw"
1044
+ */
1045
+ sizes?: string;
1046
+ /**
1047
+ * When true (default), generate a `srcSet` with several widths.
1048
+ * Disable for above-the-fold hero images where you want a single
1049
+ * source under direct theme control.
1050
+ */
1051
+ responsive?: boolean;
1052
+ /**
1053
+ * Loading strategy. Default "lazy" matches Shopify themes; pass
1054
+ * "eager" for above-the-fold imagery.
1055
+ */
1056
+ loading?: "eager" | "lazy";
1057
+ }
1058
+ /**
1059
+ * <Image> — drop-in replacement for `<img>` with srcSet + lazy loading
1060
+ * defaults. Themes should use this everywhere they'd otherwise write a
1061
+ * raw `<img>` so merchant-uploaded images get responsive variants and
1062
+ * lazy loading without per-section work.
1063
+ *
1064
+ * If `src` is empty/null, renders a placeholder div so the layout
1065
+ * doesn't shift while a merchant configures images in the customizer.
1066
+ */
1067
+ declare function Image({ src, alt, sizes, responsive, loading, className, style, ...rest }: ImageProps): react_jsx_runtime.JSX.Element;
1068
+
1069
+ interface LinkProps extends Omit<AnchorHTMLAttributes<HTMLAnchorElement>, "href"> {
1070
+ /**
1071
+ * Path relative to the storefront root, e.g. "/products/foo",
1072
+ * "/collections/all", "/cart", "/pages/about". External URLs (with
1073
+ * a protocol) are passed through unchanged.
1074
+ */
1075
+ to: string;
1076
+ children: ReactNode;
1077
+ }
1078
+ /**
1079
+ * Route-aware <Link>. Themes write paths as `/products/<slug>` (matches
1080
+ * the production subdomain root). The storefront proxy rewrites those
1081
+ * under `/<subdomain>/...` in dev path-segment routing; in production
1082
+ * the subdomain hostname does the same job at the edge.
1083
+ *
1084
+ * For plain anchor behavior — server-rendered HTML, full page nav — we
1085
+ * just emit a regular `<a>`. Themes that want client-side transitions
1086
+ * can wrap this in their own router-aware component; in practice
1087
+ * storefront pages are SSR'd so a full nav is fine and predictable.
1088
+ *
1089
+ * External URLs (have a protocol or start with `//`) pass through
1090
+ * unchanged so social-media links, CDN paths, etc. work without
1091
+ * special casing.
1092
+ */
1093
+ declare function Link({ to, children, ...rest }: LinkProps): react_jsx_runtime.JSX.Element;
1094
+
1095
+ interface AddToCartButtonProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, "onClick" | "disabled"> {
1096
+ product: Product;
1097
+ variant?: ProductVariant;
1098
+ quantity?: number;
1099
+ /** Custom labels — fallbacks are English defaults. */
1100
+ label?: ReactNode;
1101
+ loadingLabel?: ReactNode;
1102
+ soldOutLabel?: ReactNode;
1103
+ errorLabel?: ReactNode;
1104
+ /** Called after successful addition. Useful for analytics events. */
1105
+ onAdded?: (product: Product, variant?: ProductVariant) => void;
1106
+ }
1107
+ /**
1108
+ * Themed Add-to-Cart button with built-in loading/disabled/error states.
1109
+ *
1110
+ * Wraps useCart().addItem and tracks its own UX state machine:
1111
+ * idle → adding → idle (or → error briefly, then idle)
1112
+ *
1113
+ * Renders a regular `<button>` so themes style it with whatever class
1114
+ * names they want — we only own the disabled/aria-busy logic and label
1115
+ * swaps. If the variant (or product) is out of stock, button is
1116
+ * disabled and shows soldOutLabel.
1117
+ *
1118
+ * Doesn't trap navigation — for "buy now" flows that should redirect
1119
+ * to checkout, themes wrap this in their own `<a>` after onAdded.
1120
+ */
1121
+ declare function AddToCartButton({ product, variant, quantity, label, loadingLabel, soldOutLabel, errorLabel, onAdded, ...rest }: AddToCartButtonProps): react_jsx_runtime.JSX.Element;
1122
+
1123
+ interface SectionProps extends HTMLAttributes<HTMLElement> {
1124
+ /** Section instance id (the order key in templates). Required for the
1125
+ * customizer's click-to-select to work. */
1126
+ id: string;
1127
+ /** Section type as registered in the theme registry / schemas. */
1128
+ type: string;
1129
+ /** Section group id (header / footer / announcement-bar) when this
1130
+ * section is mounted inside a group rather than a page template. */
1131
+ groupId?: string;
1132
+ /** Override the fallback shown when this section throws. Defaults to
1133
+ * a small inline error card (visible to the merchant in the customizer,
1134
+ * invisible to live shoppers — see CSS comment below). */
1135
+ errorFallback?: ReactNode;
1136
+ children: ReactNode;
1137
+ }
1138
+ /**
1139
+ * <Section> — wrapper that emits the `data-section-id` / `data-section-type`
1140
+ * attributes the storefront's PreviewBridge needs to relay clicks back
1141
+ * to the V3 customizer for "select section X" navigation.
1142
+ *
1143
+ * Use this around the top-level element of every section component:
1144
+ *
1145
+ * export default function Hero({ settings, ...sectionMeta }) {
1146
+ * return (
1147
+ * <Section id={sectionMeta.id} type="hero">
1148
+ * <h1>{settings.headline}</h1>
1149
+ * </Section>
1150
+ * );
1151
+ * }
1152
+ *
1153
+ * Without `data-section-id` on a real DOM ancestor, clicking anywhere
1154
+ * in the section in the customizer iframe falls through to the page
1155
+ * background and the merchant has to use the section list panel to
1156
+ * select sections — slow and confusing.
1157
+ *
1158
+ * Errors thrown by `children` are caught by an internal ErrorBoundary
1159
+ * so one bad section doesn't unmount its siblings. Customize the
1160
+ * fallback via `errorFallback`.
1161
+ */
1162
+ declare function Section({ id, type, groupId, errorFallback, children, ...rest }: SectionProps): react_jsx_runtime.JSX.Element;
1163
+ interface BlockProps extends HTMLAttributes<HTMLDivElement> {
1164
+ /** Block instance id within its parent section. */
1165
+ id: string;
1166
+ type: string;
1167
+ /** Override the fallback shown when this block throws. */
1168
+ errorFallback?: ReactNode;
1169
+ children: ReactNode;
1170
+ }
1171
+ /**
1172
+ * <Block> — analog to <Section> for nested block selection. Same DOM
1173
+ * attribute contract; PreviewBridge reads `data-block-id` and reports
1174
+ * the block as selected when its child clicks bubble up.
1175
+ *
1176
+ * Wraps `children` in the same per-instance ErrorBoundary as <Section>
1177
+ * so a bad block within a section isolates its failure.
1178
+ */
1179
+ declare function Block({ id, type, errorFallback, children, ...rest }: BlockProps): react_jsx_runtime.JSX.Element;
1180
+
1181
+ interface FormProps extends Omit<FormHTMLAttributes<HTMLFormElement>, "onSubmit" | "method" | "action" | "children" | "onError"> {
1182
+ /**
1183
+ * Endpoint to POST/etc. the form values to. Should be one of the
1184
+ * storefront's `/api/*` proxy routes — they handle CSRF + cookie
1185
+ * forwarding + idempotency on the way to FastAPI.
1186
+ */
1187
+ action: string;
1188
+ /**
1189
+ * HTTP method. POST is the typical theme use-case (newsletter,
1190
+ * contact, customer login, address create). GET is supported for
1191
+ * search-style forms.
1192
+ */
1193
+ method?: "POST" | "GET" | "PUT" | "DELETE";
1194
+ /**
1195
+ * Called with the JSON-decoded response body on success. Use to
1196
+ * trigger a toast, redirect, etc. Theme owns the UX.
1197
+ */
1198
+ onSuccess?: (response: unknown) => void;
1199
+ /**
1200
+ * Called with the Error on failure. Falls through to the default
1201
+ * error label otherwise.
1202
+ */
1203
+ onError?: (error: Error) => void;
1204
+ /**
1205
+ * Render-prop access to the in-flight state if the theme wants to
1206
+ * show its own loading / disabled / error UI. Default `children`
1207
+ * behavior just lets the form's inputs and submit button render —
1208
+ * we don't impose styling.
1209
+ */
1210
+ children?: ReactNode | ((state: {
1211
+ submitting: boolean;
1212
+ error: Error | null;
1213
+ }) => ReactNode);
1214
+ }
1215
+ /**
1216
+ * <Form action="/api/cart/add" method="POST" onSuccess={...}>
1217
+ *
1218
+ * Theme-friendly wrapper around `<form>` that handles:
1219
+ * - CSRF: reads `numu_csrf` cookie, sends as `x-numu-csrf` header on
1220
+ * every mutating request. Same wire format as <NuMuProvider>'s
1221
+ * cart machinery — interoperates seamlessly.
1222
+ * - Idempotency: mints a UUID per submit, sends as
1223
+ * `x-numu-idempotency-key`. Backend dedupes if it supports the
1224
+ * header (cart endpoints today; more later).
1225
+ * - Submit lifecycle: tracks `submitting` + `error` state and
1226
+ * either passes them to a render-prop child or wires them to a
1227
+ * submit-button's `disabled` automatically.
1228
+ * - Throws away page navigation: `onSubmit` calls preventDefault and
1229
+ * fetches via JSON. Themes that want classic POST-and-navigate
1230
+ * can drop down to a plain `<form>`.
1231
+ *
1232
+ * Refuses to submit to absolute URLs (must point at the storefront
1233
+ * itself or an `/api/*` proxy) so a misconfigured theme can't leak
1234
+ * customer data to a third party origin.
1235
+ */
1236
+ declare function Form({ action, method, onSuccess, onError, children, ...rest }: FormProps): react_jsx_runtime.JSX.Element;
1237
+
1238
+ /**
1239
+ * Opinionated product tile.
1240
+ *
1241
+ * Themes drop this anywhere a product list is needed — collection
1242
+ * pages, search results, recommendations, recently-viewed grids.
1243
+ * Renders image + title + price (with compare-at strike-through when
1244
+ * present) + an optional "Sold out" badge + AddToCartButton.
1245
+ *
1246
+ * The component is **unstyled by default** — it ships only structural
1247
+ * markup with stable class names. Themes provide the CSS. Class names
1248
+ * use a `numu-product-card__*` BEM convention so themes can target
1249
+ * pieces without grep-and-replace if NUMU later renames anything.
1250
+ *
1251
+ * Class hooks:
1252
+ * .numu-product-card — root <article>
1253
+ * .numu-product-card__link — wrapping <a>
1254
+ * .numu-product-card__media — image container
1255
+ * .numu-product-card__image — the <img> itself
1256
+ * .numu-product-card__badge — "Sold out" overlay
1257
+ * .numu-product-card__title — title <h3>
1258
+ * .numu-product-card__price — price wrapper
1259
+ * .numu-product-card__cta — CTA wrapper (AddToCartButton)
1260
+ *
1261
+ * Themes can override any piece by passing `slots`:
1262
+ * <ProductCard product={p} slots={{ price: <CustomPrice /> }} />
1263
+ *
1264
+ * For deeper customization, themes should compose the primitives
1265
+ * (Image / Money / Link / AddToCartButton) directly instead.
1266
+ */
1267
+ interface ProductCardSlots {
1268
+ /** Replaces the badge area (default: "Sold out" when out of stock, else nothing). */
1269
+ badge?: ReactNode;
1270
+ /** Replaces the title `<h3>` block. */
1271
+ title?: ReactNode;
1272
+ /** Replaces the price `<Money>` block. */
1273
+ price?: ReactNode;
1274
+ /** Replaces the AddToCartButton. Pass `null` to hide it. */
1275
+ cta?: ReactNode | null;
1276
+ }
1277
+ interface ProductCardProps {
1278
+ product: Product;
1279
+ /** Override the link target. Default: `/products/{slug}`. */
1280
+ href?: string;
1281
+ className?: string;
1282
+ slots?: ProductCardSlots;
1283
+ /**
1284
+ * Image sizing hint for the responsive srcSet. Default: a 4-up grid
1285
+ * sizing (~25vw at desktop, ~50vw at tablet, ~100vw at mobile).
1286
+ * Override when rendering in a different layout density.
1287
+ */
1288
+ imageSizes?: string;
1289
+ }
1290
+ declare function ProductCard({ product, href, className, slots, imageSizes, }: ProductCardProps): react_jsx_runtime.JSX.Element;
1291
+
1292
+ /**
1293
+ * Opinionated collection tile.
1294
+ *
1295
+ * Renders a single collection's hero image + name + product count.
1296
+ * Used by collection-list sections, footer "Featured collections"
1297
+ * blocks, and search-result mixed views.
1298
+ *
1299
+ * Unstyled by default; themes ship the CSS. Class hooks:
1300
+ * .numu-collection-card — root <article>
1301
+ * .numu-collection-card__link — wrapping <a>
1302
+ * .numu-collection-card__media — image container
1303
+ * .numu-collection-card__image — the <img>
1304
+ * .numu-collection-card__title — title <h3>
1305
+ * .numu-collection-card__count — product count line
1306
+ */
1307
+ interface CollectionCardSlots {
1308
+ title?: ReactNode;
1309
+ /** Replaces the "N products" line. Pass null to hide it. */
1310
+ count?: ReactNode | null;
1311
+ }
1312
+ interface CollectionCardProps {
1313
+ collection: Collection;
1314
+ /** Override the link target. Default: `/collections/{slug}`. */
1315
+ href?: string;
1316
+ className?: string;
1317
+ slots?: CollectionCardSlots;
1318
+ imageSizes?: string;
1319
+ }
1320
+ declare function CollectionCard({ collection, href, className, slots, imageSizes, }: CollectionCardProps): react_jsx_runtime.JSX.Element;
1321
+
1322
+ /**
1323
+ * <RichText html=... /> — sanitized HTML renderer.
1324
+ *
1325
+ * Wraps `dangerouslySetInnerHTML` so theme code stops calling it
1326
+ * directly with merchant-supplied content. Themes that need to render
1327
+ * rich-text fields (product descriptions, page bodies, blog articles,
1328
+ * policy bodies) go through this so an XSS in a single field can't
1329
+ * cross-contaminate the whole storefront.
1330
+ *
1331
+ * **Why a built-in allowlist sanitizer instead of DOMPurify?**
1332
+ * The audit plan called for DOMPurify, but adding it as a hard dep
1333
+ * inflates every theme bundle by ~12KB gzipped — and the merchant-
1334
+ * editable surface is small enough (a fixed set of formatting tags +
1335
+ * links + images) that a curated allowlist is both smaller AND easier
1336
+ * to reason about. If a theme needs richer sanitization (embeds,
1337
+ * iframes, MathML), it can import DOMPurify directly and pre-sanitize
1338
+ * before passing the result here.
1339
+ *
1340
+ * Allowed tags:
1341
+ * p, br, hr, h1-h6, blockquote, pre, code,
1342
+ * strong, b, em, i, u, s, sub, sup, mark,
1343
+ * ul, ol, li, dl, dt, dd,
1344
+ * a (href, target, rel only),
1345
+ * img (src, alt, width, height, loading only — http(s) URLs only),
1346
+ * table, thead, tbody, tr, th, td,
1347
+ * span, div (with class attribute only).
1348
+ *
1349
+ * Stripped:
1350
+ * <script>, <style>, <iframe>, <object>, <embed>, <link>, <meta>,
1351
+ * form/input, on*= handlers, javascript:/data: URLs, srcset on
1352
+ * <img> (we let the storefront's image-transform handle that
1353
+ * uniformly).
1354
+ */
1355
+ interface RichTextProps {
1356
+ html: string | null | undefined;
1357
+ className?: string;
1358
+ /** Element to render — defaults to `<div>`. Use `<article>` for body content. */
1359
+ as?: "div" | "article" | "section" | "aside";
1360
+ }
1361
+ /**
1362
+ * Sanitize an HTML string against the allowlist above. Runs server-
1363
+ * AND client-side because rich-text fields are server-rendered for SEO.
1364
+ *
1365
+ * Implementation note: We use DOMParser when available (browser) for
1366
+ * structural correctness; on server we fall through a regex-based
1367
+ * pass that handles the common formatting tags + escapes everything
1368
+ * else. The server pass is intentionally conservative — themes that
1369
+ * need server-rendered rich content with edge-case structure should
1370
+ * sanitize on the API tier and pass the sanitized HTML through
1371
+ * `bypassSanitize` (escape hatch below).
1372
+ */
1373
+ declare function sanitizeHtml(input: string): string;
1374
+ declare function RichText({ html, className, as }: RichTextProps): react_jsx_runtime.JSX.Element | null;
1375
+
1376
+ /**
1377
+ * <CurrencySwitcher /> — wired in Phase 6.
1378
+ *
1379
+ * Reads presentment currencies + the persisted selection from
1380
+ * `useCurrency()`, which talks to
1381
+ * `/api/storefront/store/{id}/currencies`. Changing the dropdown
1382
+ * writes the `numu_currency` cookie via the hook, which then
1383
+ * propagates to `<Money>`'s display.
1384
+ *
1385
+ * Renders nothing when the store offers only one currency — themes
1386
+ * can drop the component into a layout without conditional render.
1387
+ *
1388
+ * Pass `render` to take over the markup completely (e.g. a custom
1389
+ * dropdown component or a flag-icon grid).
1390
+ */
1391
+ interface CurrencySwitcherProps {
1392
+ className?: string;
1393
+ onSelect?: (currency: string) => void;
1394
+ render?: (state: {
1395
+ currencies: string[];
1396
+ current: string;
1397
+ onChange: (next: string) => void;
1398
+ }) => React.ReactNode;
1399
+ }
1400
+ declare function CurrencySwitcher({ className, onSelect, render, }: CurrencySwitcherProps): react_jsx_runtime.JSX.Element | null;
1401
+
1402
+ interface LocaleSwitcherProps {
1403
+ className?: string;
1404
+ onSelect?: (locale: string) => void;
1405
+ /** Custom renderer. Receives the resolved locale list + current selection. */
1406
+ render?: (state: {
1407
+ locales: string[];
1408
+ current: string;
1409
+ labelFor: (code: string) => string;
1410
+ onChange: (next: string) => void;
1411
+ }) => React.ReactNode;
1412
+ }
1413
+ declare function LocaleSwitcher({ className, onSelect, render, }: LocaleSwitcherProps): react_jsx_runtime.JSX.Element | null;
1414
+
1415
+ /**
1416
+ * Module Federation singleton sharing for @numueg/theme-sdk.
1417
+ *
1418
+ * Why this exists:
1419
+ * BYOT bundles (loaded at runtime as cross-origin ESM modules) need to
1420
+ * share React + the SDK with the host storefront. Otherwise React's
1421
+ * internal "two copies" check trips and hooks crash. The host registers
1422
+ * its singletons here; the BYOT bundle reads them via the SDK shim.
1423
+ *
1424
+ * Threat model:
1425
+ * We do NOT want third-party scripts (analytics, chat widgets, BYOT
1426
+ * community themes) to read PII off the SDK. The earlier version stored
1427
+ * the SDK on `window.__NUMU_SDK__` — anything in the page could read it
1428
+ * and call `useCustomer()` etc.
1429
+ *
1430
+ * Mitigation:
1431
+ * The singleton is keyed by a Symbol that's only handed to consumers
1432
+ * we've verified (the storefront's BYOT entry function passes it to the
1433
+ * bundle's `setup()`). Symbols can't be enumerated through `window` or
1434
+ * `Object.getOwnPropertyNames`, so a leaked global reference doesn't
1435
+ * yield SDK access.
1436
+ *
1437
+ * For maximum safety we also expose `clearSdkSingleton()` so the host
1438
+ * can revoke after a BYOT theme is unmounted.
1439
+ */
1440
+ interface SdkSingleton {
1441
+ useShop: unknown;
1442
+ useProduct: unknown;
1443
+ useCollection: unknown;
1444
+ useCart: unknown;
1445
+ useCustomer: unknown;
1446
+ useThemeSettings: unknown;
1447
+ useLocalization: unknown;
1448
+ usePage: unknown;
1449
+ useSection: unknown;
1450
+ NuMuProvider: unknown;
1451
+ ProductProvider: unknown;
1452
+ CollectionProvider: unknown;
1453
+ }
1454
+ interface ReactSingleton {
1455
+ React: unknown;
1456
+ ReactDOM: unknown;
1457
+ }
1458
+ declare function registerSdkSingleton(sdk: SdkSingleton): void;
1459
+ declare function getSdkSingleton(): SdkSingleton | null;
1460
+ declare function registerReactSingleton(react: unknown, reactDom: unknown): void;
1461
+ declare function getReactSingleton(): ReactSingleton | null;
1462
+ declare function isSdkAvailable(): boolean;
1463
+
1464
+ /**
1465
+ * Section + block authoring helpers.
1466
+ *
1467
+ * `defineSection` and `defineBlock` bind a JSON schema to its render
1468
+ * component so theme authors don't keep two files in sync by hand.
1469
+ * The plugin ships the schema half (writes `schemas/sections/<type>.json`
1470
+ * for the customizer), the runtime imports the component half — both
1471
+ * sourced from the same factory call.
1472
+ *
1473
+ * Why factories instead of two separate files:
1474
+ * Pre-Phase-2 layout was:
1475
+ * theme/main.tsx → registers components
1476
+ * theme/schemas/sections/Hero.json → declares schema
1477
+ * Drift was the bug. Renaming a setting required edits in two places;
1478
+ * adding a new section meant remembering to update three lists.
1479
+ * Factories collapse that into a single export the plugin and runtime
1480
+ * both read.
1481
+ *
1482
+ * Usage:
1483
+ * // sections/Hero.tsx
1484
+ * import { defineSection } from "@numueg/theme-sdk";
1485
+ * export default defineSection({
1486
+ * schema: {
1487
+ * type: "hero",
1488
+ * name: "Hero",
1489
+ * settings: [{ type: "text", id: "heading", label: "Heading" }],
1490
+ * presets: [{ name: "Hero — Centered", settings: { heading: "Welcome" } }],
1491
+ * },
1492
+ * render: ({ settings }) => <h1>{settings.heading}</h1>,
1493
+ * });
1494
+ *
1495
+ * The plugin's section-discovery scan picks up files under
1496
+ * `src/sections/**\/*.{tsx,ts}` whose default export is a section
1497
+ * definition. No registration list needed.
1498
+ */
1499
+
1500
+ declare const SECTION_MARKER: unique symbol;
1501
+ declare const BLOCK_MARKER: unique symbol;
1502
+ /**
1503
+ * A defined section: paired schema + render component.
1504
+ *
1505
+ * The marker symbol lets the plugin's discovery scan identify section
1506
+ * definitions without depending on a structural shape that themes might
1507
+ * accidentally collide with.
1508
+ */
1509
+ interface DefinedSection {
1510
+ schema: SectionSchema;
1511
+ render: ComponentType<SectionProps$1>;
1512
+ readonly [SECTION_MARKER]: true;
1513
+ }
1514
+ interface DefinedBlock {
1515
+ schema: BlockSchema;
1516
+ render: ComponentType<BlockProps$1>;
1517
+ readonly [BLOCK_MARKER]: true;
1518
+ }
1519
+ interface DefineSectionInput {
1520
+ schema: SectionSchema;
1521
+ render: ComponentType<SectionProps$1>;
1522
+ }
1523
+ interface DefineBlockInput {
1524
+ schema: BlockSchema;
1525
+ render: ComponentType<BlockProps$1>;
1526
+ }
1527
+ /**
1528
+ * Bind a section schema to its renderer.
1529
+ *
1530
+ * Validates at runtime that `schema.type` is a stable identifier
1531
+ * (lowercase + dashes/underscores) — drift here breaks the plugin's
1532
+ * codegen step (which writes `__generated__/sections.d.ts` keyed off
1533
+ * `schema.type`).
1534
+ */
1535
+ declare function defineSection(input: DefineSectionInput): DefinedSection;
1536
+ declare function defineBlock(input: DefineBlockInput): DefinedBlock;
1537
+ declare function isDefinedSection(v: unknown): v is DefinedSection;
1538
+ declare function isDefinedBlock(v: unknown): v is DefinedBlock;
1539
+ /**
1540
+ * Build a section registry from a Vite glob import.
1541
+ *
1542
+ * Themes call this at the top of their `main.tsx`:
1543
+ *
1544
+ * const sectionModules = import.meta.glob<{ default: DefinedSection }>(
1545
+ * "./sections/*.tsx", { eager: true });
1546
+ * const sections = collectSections(sectionModules);
1547
+ *
1548
+ * The returned object is keyed by `schema.type` for direct lookup at
1549
+ * render time. Modules whose default export isn't a DefinedSection are
1550
+ * skipped with a console warning (drops a half-migrated section without
1551
+ * breaking the rest of the bundle).
1552
+ */
1553
+ declare function collectSections<T extends Record<string, unknown>>(modules: T): Record<string, DefinedSection>;
1554
+ declare function collectBlocks<T extends Record<string, unknown>>(modules: T): Record<string, DefinedBlock>;
1555
+
1556
+ /**
1557
+ * `assetUrl(name)` — resolve a theme-bundled asset's runtime URL.
1558
+ *
1559
+ * Themes ship static assets (images, fonts, JSON) under `assets/`.
1560
+ * The plugin copies each asset to `dist/assets/` with a content-hashed
1561
+ * filename and writes a manifest (`asset-manifest.json`) mapping each
1562
+ * source path to the hashed name.
1563
+ *
1564
+ * At runtime this function reads the manifest off the runtime window
1565
+ * object (`window.__NUMU_ASSET_MANIFEST`) — populated by the storefront's
1566
+ * `<RuntimeImportMap>` component when the theme bundle is loaded —
1567
+ * and returns the absolute URL to the hashed file.
1568
+ *
1569
+ * Why a runtime helper instead of bake-time string interpolation:
1570
+ * The same theme bundle is reused across hosted stores. The asset
1571
+ * base URL differs per environment (CDN vs local dev) and per store
1572
+ * (subdomain vs custom domain). Resolving at runtime keeps a single
1573
+ * bundle deployable across all of them.
1574
+ *
1575
+ * Usage:
1576
+ * <Image src={assetUrl("hero.jpg")} alt="..." />
1577
+ * <link rel="preload" as="font" href={assetUrl("fonts/Inter.woff2")} />
1578
+ *
1579
+ * Behavior when the manifest is missing or the asset isn't listed:
1580
+ * Returns the original `name` (prefixed with the conventional
1581
+ * `/assets/` path) as a fallback. This keeps dev workable when the
1582
+ * plugin's asset-pipeline step hasn't run yet, and surfaces obviously-
1583
+ * wrong asset names as 404s rather than silent failures.
1584
+ */
1585
+ declare function assetUrl(name: string): string;
1586
+
1587
+ /**
1588
+ * Theme-bundled locale files: load + merge.
1589
+ *
1590
+ * The plugin discovers `locales/<code>.json` (and the fallback
1591
+ * `locales/en.default.json`) and bakes them into the bundle as a map
1592
+ * keyed by locale code. The runtime calls `pickTranslations(map, locale)`
1593
+ * to resolve the right one, with English fallback for missing keys.
1594
+ *
1595
+ * Conventions:
1596
+ * - locales/en.default.json is the canonical English source. It's
1597
+ * the fallback for any other locale's missing keys.
1598
+ * - locales/ar.json (and friends) override or extend the default.
1599
+ * - Nested keys are flattened with dot notation: `{"hero": {"cta": "Buy"}}`
1600
+ * becomes `"hero.cta"` for `t("hero.cta")` to look up.
1601
+ *
1602
+ * Why flatten:
1603
+ * `useTranslation()` returns `t(key)` which expects a string lookup —
1604
+ * structured access would force every theme to write
1605
+ * `translations.hero?.cta ?? "Buy"`. Flattening keeps the call site
1606
+ * short and matches Shopify's `t` semantics.
1607
+ */
1608
+ interface LocaleMessages {
1609
+ [key: string]: string;
1610
+ }
1611
+ interface LocaleBundle {
1612
+ /** Locale code → flat key/value messages. */
1613
+ [locale: string]: LocaleMessages;
1614
+ }
1615
+ /**
1616
+ * Recursively flatten a nested locale object into dot-keyed strings.
1617
+ *
1618
+ * Non-string leaves are skipped with a console warning — translations
1619
+ * are user-facing strings; numbers and booleans don't belong in a
1620
+ * locale file (they should be in schema settings).
1621
+ */
1622
+ declare function flattenMessages(source: Record<string, unknown>, prefix?: string): LocaleMessages;
1623
+ /**
1624
+ * Resolve the messages map for the requested locale, falling back to
1625
+ * "en" then "en.default" then an empty map. Missing keys in the
1626
+ * requested locale are filled from "en" (so an Arabic translation
1627
+ * file with 90% coverage doesn't show empty strings for the missing
1628
+ * 10%; English shows through).
1629
+ */
1630
+ declare function pickTranslations(bundle: LocaleBundle, locale: string): LocaleMessages;
1631
+ /**
1632
+ * Build a LocaleBundle from raw imports. Themes call this in their
1633
+ * `main.tsx` after a Vite glob import:
1634
+ *
1635
+ * const localeModules = import.meta.glob<Record<string, unknown>>(
1636
+ * "./locales/*.json", { eager: true, import: "default" });
1637
+ * const translations = buildLocaleBundle(localeModules);
1638
+ *
1639
+ * `localeModules` keys are paths like `./locales/ar.json`; we strip
1640
+ * the directory + extension to get the locale code, and flatten each
1641
+ * value.
1642
+ */
1643
+ declare function buildLocaleBundle<T extends Record<string, unknown>>(modules: T): LocaleBundle;
1644
+
1645
+ export { AddToCartButton, type AddressInput, type AnalyticsApi, type AnalyticsPayload, type AppManifestBlock, type AppPayload, type AppState, Block, BlockProps$1 as BlockProps, BlockSchema, Cart, CartContext, type CheckoutAddress, type CheckoutApi, type CheckoutSessionState, type CheckoutStep, Collection, CollectionCard, type CollectionCardProps, type CollectionCardSlots, CollectionContext, CollectionProvider, type CurrencyConfig, type CurrencyState, CurrencySwitcher, type CurrencySwitcherProps, Customer, type CustomerAddress, type CustomerAddressesState, CustomerContext, type DefineBlockInput, type DefineSectionInput, type DefinedBlock, type DefinedSection, Form, type GiftCardBalance, Image, Link, type LocaleBundle, type LocaleMessages, LocaleSwitcher, type LocaleSwitcherProps, LocalizationContext, Money, type NavigationItem, type NavigationState, NuMuProvider, type OrderDetail, type OrderListEntry, type OrderListState, type OrderState, Page, PageContext, type PlaceOrderResult, Product, ProductCard, type ProductCardProps, type ProductCardSlots, ProductContext, 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, ShopContext, type ShopWithHelpers, Store, ThemeSettingsContext, ThemeSettingsV3, type UseGiftCardBalance, type UseReorder, type UseSearchOptions, type UseShippingRatesOptions, type UseShippingRatesState, type UseVariantSelection, type WishlistItem, type WishlistState, assetUrl, availableValues, buildLocaleBundle, collectBlocks, collectSections, defaultVariant, defineBlock, defineSection, findVariantByOptions, flattenMessages, getReactSingleton, getSdkSingleton, isDefinedBlock, isDefinedSection, isSdkAvailable, pickTranslations, registerReactSingleton, registerSdkSingleton, sanitizeHtml, useAnalytics, useApp, useCart, useCheckout, useCollection, useCollectionOptional, useCollections, useCurrency, useCustomer, useCustomerActions, useCustomerAddresses, useDirection, useFieldTranslation, useGiftCardBalance, useImage, useLocale, useLocalization, useMoney, useNavigation, useNumberFormat, useOrder, useOrders, usePage, useProduct, useProductOptional, useProducts, useRelatedProducts, useReorder, useSearch, useSection, useSectionOptional, useShippingRates, useShop, useThemeSettings, useTranslation, useVariantSelection, useWishlist };