@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.
- package/CHANGELOG.md +19 -0
- package/LICENSE +21 -0
- package/README.md +64 -0
- package/dist/index-CGx6FNqb.d.ts +167 -0
- package/dist/index-zSb0FIyh.d.mts +167 -0
- package/dist/index.cjs +2751 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.mts +1645 -0
- package/dist/index.d.ts +1645 -0
- package/dist/index.mjs +2669 -0
- package/dist/index.mjs.map +1 -0
- package/dist/normalize.cjs +96 -0
- package/dist/normalize.cjs.map +1 -0
- package/dist/normalize.d.mts +15 -0
- package/dist/normalize.d.ts +15 -0
- package/dist/normalize.mjs +94 -0
- package/dist/normalize.mjs.map +1 -0
- package/dist/theme-NWJAU9jd.d.mts +128 -0
- package/dist/theme-NWJAU9jd.d.ts +128 -0
- package/dist/types.cjs +4 -0
- package/dist/types.cjs.map +1 -0
- package/dist/types.d.mts +2 -0
- package/dist/types.d.ts +2 -0
- package/dist/types.mjs +3 -0
- package/dist/types.mjs.map +1 -0
- package/package.json +81 -0
package/dist/index.d.ts
ADDED
|
@@ -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 };
|