@usequeek/theme-kit 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/README.md +37 -0
- package/api/collections.ts +93 -0
- package/api/orders.ts +23 -0
- package/apps/analytics-events.ts +32 -0
- package/apps/apps.css +613 -0
- package/apps/follow-card.tsx +192 -0
- package/apps/gtag.ts +19 -0
- package/apps/subscribe-form.tsx +124 -0
- package/client.ts +52 -0
- package/components/auth/auth-email-login-step.tsx +46 -0
- package/components/auth/auth-email-otp-step.tsx +54 -0
- package/components/auth/auth-email-otp-verify-step.tsx +82 -0
- package/components/auth/auth-email-register-step.tsx +59 -0
- package/components/auth/auth-flow.tsx +26 -0
- package/components/auth/auth-signup-step.tsx +53 -0
- package/components/blog/category-filter.tsx +92 -0
- package/components/blog/index.ts +12 -0
- package/components/blog/pagination.tsx +73 -0
- package/components/blog/post-categories.tsx +42 -0
- package/components/blog/post-meta.tsx +49 -0
- package/components/blog/related-posts.tsx +54 -0
- package/components/blog/share-buttons.tsx +89 -0
- package/components/cart/cart-panel-controller.tsx +67 -0
- package/components/cart/default-cart-shell.tsx +207 -0
- package/components/checkout/checkout-contact.tsx +84 -0
- package/components/checkout/default-checkout-shell.tsx +177 -0
- package/components/checkout/policy-links.tsx +98 -0
- package/components/image.tsx +149 -0
- package/components/markdown.tsx +54 -0
- package/components/product-media-frame.tsx +56 -0
- package/components/variant-picker.css +84 -0
- package/components/variant-picker.tsx +107 -0
- package/design-token-preview.tsx +81 -0
- package/edit-mode-active.tsx +324 -0
- package/edit-mode.tsx +80 -0
- package/hooks/use-address-autocomplete.ts +62 -0
- package/hooks/use-auth-flow.tsx +346 -0
- package/hooks/use-auth.ts +17 -0
- package/hooks/use-cart.ts +14 -0
- package/hooks/use-categories.ts +112 -0
- package/hooks/use-collection-params.ts +67 -0
- package/hooks/use-href.ts +15 -0
- package/hooks/use-posts.ts +75 -0
- package/hooks/use-product-detail.ts +69 -0
- package/hooks/use-product-questions.ts +77 -0
- package/hooks/use-product-sections.ts +165 -0
- package/hooks/use-products.ts +216 -0
- package/hooks/use-related-products.ts +56 -0
- package/hooks/use-resolve-coordinates.ts +48 -0
- package/hooks/use-reviews.ts +167 -0
- package/hooks/use-search.ts +55 -0
- package/hooks/use-shop-cart.ts +31 -0
- package/hooks/use-shop-params.ts +68 -0
- package/hooks/use-shop.ts +186 -0
- package/hooks/use-variant-selection.ts +98 -0
- package/index.ts +49 -0
- package/package.json +41 -0
- package/page-renderer.tsx +365 -0
- package/provider.tsx +91 -0
- package/sdk/client.ts +221 -0
- package/sdk/error-handler.ts +37 -0
- package/sdk/platform.ts +78 -0
- package/sdk/queek-client.ts +104 -0
- package/sdk/reauth-guard.ts +27 -0
- package/sdk/server-store-client.ts +165 -0
- package/shared-blocks/button.tsx +41 -0
- package/shared-blocks/callout.tsx +35 -0
- package/shared-blocks/content-default.tsx +23 -0
- package/shared-blocks/core-blocks.css +1792 -0
- package/shared-blocks/divider.tsx +17 -0
- package/shared-blocks/embed.tsx +40 -0
- package/shared-blocks/faq.tsx +44 -0
- package/shared-blocks/image.tsx +29 -0
- package/shared-blocks/index.ts +24 -0
- package/shared-blocks/manifest.ts +76 -0
- package/shared-blocks/product-qa.tsx +203 -0
- package/shared-blocks/quote.tsx +20 -0
- package/shared-blocks/reviews.tsx +436 -0
- package/shared-blocks/table.tsx +45 -0
- package/shared-blocks/video.tsx +60 -0
- package/stores/address-modal-store.ts +15 -0
- package/stores/auth-modal-store.ts +17 -0
- package/stores/cart-panel-store.ts +17 -0
- package/stores/cart-store.ts +172 -0
- package/stores/checkout-store.ts +167 -0
- package/stores/edit-preview-store.ts +61 -0
- package/stores/location-store.ts +41 -0
- package/stores/orders-sheet-store.ts +21 -0
- package/stores/policy-modal-store.ts +15 -0
- package/stores/product-modal-store.ts +18 -0
- package/stores/search-modal-store.ts +17 -0
- package/stores/toast-store.ts +46 -0
- package/stores/user-store.ts +88 -0
- package/theme-context.tsx +28 -0
- package/theme-variant-resolver.ts +44 -0
- package/types/block.ts +292 -0
- package/types/cart.ts +21 -0
- package/types/media.ts +55 -0
- package/types/menu.ts +27 -0
- package/types/order.ts +119 -0
- package/types/page.ts +86 -0
- package/types/product.ts +279 -0
- package/types/theme.ts +380 -0
- package/types/vendor.ts +243 -0
- package/utils/addon-selection.ts +31 -0
- package/utils/benefit-icon.tsx +145 -0
- package/utils/brand-palette.ts +64 -0
- package/utils/brand.ts +518 -0
- package/utils/contact-location.ts +44 -0
- package/utils/format.ts +64 -0
- package/utils/hero-content.ts +35 -0
- package/utils/image-srcset.ts +157 -0
- package/utils/markdown.tsx +237 -0
- package/utils/match-variant.ts +31 -0
- package/utils/menu-link.ts +41 -0
- package/utils/placeholder-resolver.ts +184 -0
- package/utils/product-media.ts +77 -0
- package/utils/product-normalizer.ts +323 -0
- package/utils/vendor-host.ts +173 -0
- package/utils/video-embed.ts +38 -0
package/types/product.ts
ADDED
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import type { ImageVariants } from './media';
|
|
2
|
+
|
|
3
|
+
export type ProductKind = 'physical' | 'service' | 'digital';
|
|
4
|
+
export type ProductStatus = 'active' | 'inactive';
|
|
5
|
+
|
|
6
|
+
export interface ProductPriceRange {
|
|
7
|
+
min_amount: number;
|
|
8
|
+
max_amount: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface ProductPricing {
|
|
12
|
+
base_amount: number;
|
|
13
|
+
sale_amount: number;
|
|
14
|
+
compare_at_amount: number;
|
|
15
|
+
discount_amount: number;
|
|
16
|
+
discount_percent: number;
|
|
17
|
+
is_price_from: boolean;
|
|
18
|
+
tax_inclusive: boolean;
|
|
19
|
+
price_range: ProductPriceRange;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ProductShop {
|
|
23
|
+
id: string;
|
|
24
|
+
name: string | null;
|
|
25
|
+
slug: string | null;
|
|
26
|
+
logo: string | null;
|
|
27
|
+
rating: number;
|
|
28
|
+
rating_count: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface ProductMedia {
|
|
32
|
+
thumbnail: string | null;
|
|
33
|
+
image: string | null;
|
|
34
|
+
original?: string | null;
|
|
35
|
+
primary_variant_image?: string | null;
|
|
36
|
+
video_url?: string | null;
|
|
37
|
+
/** Falls back to `image` server-side when the vendor hasn't set an explicit
|
|
38
|
+
* poster (ProductResource::resolveVideoPosterUrl) — always a usable value
|
|
39
|
+
* whenever video_url is set. */
|
|
40
|
+
video_poster_url?: string | null;
|
|
41
|
+
/** Srcset tier map for `image` (ProductResource::buildBaseMediaPayload /
|
|
42
|
+
* buildDetailMediaPayload — present on BOTH list and detail responses).
|
|
43
|
+
* null when the primary image isn't Media-backed. */
|
|
44
|
+
image_variants?: ImageVariants | null;
|
|
45
|
+
/** Sibling to `primary_variant_image`, same contract. */
|
|
46
|
+
primary_variant_image_variants?: ImageVariants | null;
|
|
47
|
+
gallery?: Array<{
|
|
48
|
+
id: string | number;
|
|
49
|
+
url: string;
|
|
50
|
+
alt?: string;
|
|
51
|
+
/** Per-gallery-item tier map (ProductResource::buildGalleryPayload).
|
|
52
|
+
* Detail responses only — the list payload has no `gallery` at all.
|
|
53
|
+
* Note `url` here is already the CARD url server-side, not the original. */
|
|
54
|
+
variants?: ImageVariants | null;
|
|
55
|
+
}>;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface ProductInventory {
|
|
59
|
+
in_stock: boolean;
|
|
60
|
+
tracking: boolean;
|
|
61
|
+
quantity: number;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface ProductReviewSummary {
|
|
65
|
+
rating: number;
|
|
66
|
+
review_count: number;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface ProductCategory {
|
|
70
|
+
id: string;
|
|
71
|
+
name: string;
|
|
72
|
+
slug: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface ProductOptionValue {
|
|
76
|
+
value: string;
|
|
77
|
+
label: string;
|
|
78
|
+
color_code: string | null;
|
|
79
|
+
/** Representative image for this option value (e.g. the "Denim" swatch's variant photo) —
|
|
80
|
+
* used when there's no color_code to render as a swatch (standard e-commerce pattern:
|
|
81
|
+
* color if color, else image if image, else text). */
|
|
82
|
+
image: string | null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface ProductOption {
|
|
86
|
+
name: string;
|
|
87
|
+
type: string | null;
|
|
88
|
+
values: ProductOptionValue[];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface ProductVariantOptionValue {
|
|
92
|
+
option_name: string;
|
|
93
|
+
value: string;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface ProductVariant {
|
|
97
|
+
id: string;
|
|
98
|
+
sku: string | null;
|
|
99
|
+
/** Printed barcode / GTIN (EAN-13/8, UPC-A/E) for in-store scan-to-cart. */
|
|
100
|
+
barcode: string | null;
|
|
101
|
+
title: string | null;
|
|
102
|
+
option_values: ProductVariantOptionValue[];
|
|
103
|
+
pricing: {
|
|
104
|
+
base_amount: number;
|
|
105
|
+
sale_amount: number;
|
|
106
|
+
compare_at_amount: number;
|
|
107
|
+
discount_amount: number;
|
|
108
|
+
discount_percent: number;
|
|
109
|
+
};
|
|
110
|
+
inventory: {
|
|
111
|
+
in_stock: boolean;
|
|
112
|
+
tracking: boolean;
|
|
113
|
+
quantity: number;
|
|
114
|
+
};
|
|
115
|
+
media: ProductMedia;
|
|
116
|
+
weight: string | null;
|
|
117
|
+
position: number | null;
|
|
118
|
+
is_active: boolean;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export interface ProductAddonItem {
|
|
122
|
+
id: string;
|
|
123
|
+
name: string;
|
|
124
|
+
display_name: string | null;
|
|
125
|
+
price: number;
|
|
126
|
+
in_stock: boolean;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export interface ProductAddon {
|
|
130
|
+
id: string;
|
|
131
|
+
title: string;
|
|
132
|
+
min: number;
|
|
133
|
+
max: number;
|
|
134
|
+
required: boolean;
|
|
135
|
+
multiple_selection: boolean;
|
|
136
|
+
/** Every item in the group is takeaway/delivery packaging rather than a real
|
|
137
|
+
* product choice. A required pack group is only genuinely required when the
|
|
138
|
+
* order leaves the premises — dine-in customers eat off a plate, so forcing
|
|
139
|
+
* one on them blocks checkout. Backend-derived; never re-derive it here. */
|
|
140
|
+
is_pack_group?: boolean;
|
|
141
|
+
items: ProductAddonItem[];
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export interface ProductReview {
|
|
145
|
+
id: string;
|
|
146
|
+
title: string | null;
|
|
147
|
+
description: string | null;
|
|
148
|
+
rating: number;
|
|
149
|
+
customer_name: string | null;
|
|
150
|
+
created_at: string | null;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export interface ProductFlags {
|
|
154
|
+
featured: boolean;
|
|
155
|
+
is_marketplace: boolean;
|
|
156
|
+
is_wholesale: boolean;
|
|
157
|
+
is_new: boolean;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export interface Product {
|
|
161
|
+
shop_id: string;
|
|
162
|
+
id: string;
|
|
163
|
+
p_id: number;
|
|
164
|
+
title: string;
|
|
165
|
+
slug: string;
|
|
166
|
+
excerpt: string | null;
|
|
167
|
+
description: string | null;
|
|
168
|
+
kind: ProductKind;
|
|
169
|
+
status: ProductStatus;
|
|
170
|
+
/** Printed barcode / GTIN for products without variants. Variant-level
|
|
171
|
+
* barcodes live on `variants[].barcode`. Powers QR in-store scan-to-cart. */
|
|
172
|
+
barcode: string | null;
|
|
173
|
+
currency: string;
|
|
174
|
+
pricing: ProductPricing;
|
|
175
|
+
shop: ProductShop;
|
|
176
|
+
media: ProductMedia;
|
|
177
|
+
inventory: ProductInventory;
|
|
178
|
+
variants_count: number;
|
|
179
|
+
review_summary: ProductReviewSummary;
|
|
180
|
+
flags: ProductFlags;
|
|
181
|
+
categories: ProductCategory[];
|
|
182
|
+
options: ProductOption[];
|
|
183
|
+
variants: ProductVariant[];
|
|
184
|
+
addons: ProductAddon[];
|
|
185
|
+
reviews: ProductReview[];
|
|
186
|
+
created_at: string | null;
|
|
187
|
+
updated_at: string | null;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export interface ProductListMeta {
|
|
191
|
+
next_cursor?: string | null;
|
|
192
|
+
has_more?: boolean;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export interface ApiResponse<T, M = Record<string, unknown>> {
|
|
196
|
+
data: T;
|
|
197
|
+
message?: string;
|
|
198
|
+
meta?: M;
|
|
199
|
+
/** Present on Laravel `simplePaginate()` resource responses (e.g. collection
|
|
200
|
+
* products) — that paginator never computes a total/last-page count (it's
|
|
201
|
+
* deliberately cheap on large catalogues), so `links.next`/`links.prev`
|
|
202
|
+
* (a URL or null) is the only reliable "is there another page" signal,
|
|
203
|
+
* not `meta.has_more` or a page count. */
|
|
204
|
+
links?: {
|
|
205
|
+
first?: string | null;
|
|
206
|
+
last?: string | null;
|
|
207
|
+
prev?: string | null;
|
|
208
|
+
next?: string | null;
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export interface LegacyProductLike {
|
|
213
|
+
id?: string;
|
|
214
|
+
p_id?: number;
|
|
215
|
+
title?: string;
|
|
216
|
+
slug?: string;
|
|
217
|
+
excerpt?: string | null;
|
|
218
|
+
description?: string | null;
|
|
219
|
+
compare_at_price?: number | null;
|
|
220
|
+
rating?: number;
|
|
221
|
+
review_count?: number;
|
|
222
|
+
kind?: string;
|
|
223
|
+
type?: string;
|
|
224
|
+
status?: string;
|
|
225
|
+
barcode?: string | null;
|
|
226
|
+
currency?: string;
|
|
227
|
+
price?: number;
|
|
228
|
+
discount_price?: number;
|
|
229
|
+
is_price_from?: boolean;
|
|
230
|
+
shop_id?: string;
|
|
231
|
+
vendor_id?: string;
|
|
232
|
+
thumbnail?: string | null;
|
|
233
|
+
image?: string | null;
|
|
234
|
+
// Sibling top-level fields on the real backend payload (ProductResource::
|
|
235
|
+
// buildBasePayload) — NOT nested under `media` server-side, unlike
|
|
236
|
+
// image/thumbnail which are sent in both places.
|
|
237
|
+
video_url?: string | null;
|
|
238
|
+
video_poster_url?: string | null;
|
|
239
|
+
stock?: number;
|
|
240
|
+
in_stock?: boolean;
|
|
241
|
+
inventory_tracking?: boolean;
|
|
242
|
+
pricing?: Partial<ProductPricing>;
|
|
243
|
+
inventory?: Partial<ProductInventory>;
|
|
244
|
+
flags?: Partial<ProductFlags>;
|
|
245
|
+
media?: Partial<ProductMedia>;
|
|
246
|
+
categories?: Array<Partial<ProductCategory>>;
|
|
247
|
+
options?: Array<{
|
|
248
|
+
name?: string;
|
|
249
|
+
type?: string | null;
|
|
250
|
+
values?: Array<string | Partial<ProductOptionValue>>;
|
|
251
|
+
values_detailed?: Array<Partial<ProductOptionValue>>;
|
|
252
|
+
}>;
|
|
253
|
+
variants?: Array<
|
|
254
|
+
Omit<Partial<ProductVariant>, 'option_values'> & {
|
|
255
|
+
// Partial<ProductVariant>'s own option_values is a plain array; a raw/legacy payload can
|
|
256
|
+
// also send it as a {optionName: value} map (normalizeVariantOptionValues handles both) —
|
|
257
|
+
// intersecting the two types directly collapses to an unsatisfiable type, hence the Omit.
|
|
258
|
+
option_values?: ProductVariantOptionValue[] | Record<string, string>;
|
|
259
|
+
// The backend sends a variant's own price/stock as FLAT fields (see
|
|
260
|
+
// ProductResource::resolveShopVariants), never nested under pricing/inventory —
|
|
261
|
+
// those nested keys only exist if some other caller pre-shapes the payload.
|
|
262
|
+
price?: number;
|
|
263
|
+
discount_price?: number;
|
|
264
|
+
compare_at_price?: number | null;
|
|
265
|
+
discount_amount?: number;
|
|
266
|
+
has_discount?: boolean;
|
|
267
|
+
in_stock?: boolean;
|
|
268
|
+
stock?: number;
|
|
269
|
+
track_inventory?: boolean;
|
|
270
|
+
}
|
|
271
|
+
>;
|
|
272
|
+
addons?: ProductAddon[];
|
|
273
|
+
reviews?: ProductReview[];
|
|
274
|
+
review_summary?: Partial<ProductReviewSummary>;
|
|
275
|
+
created_at?: string | null;
|
|
276
|
+
updated_at?: string | null;
|
|
277
|
+
shop?: Partial<ProductShop>;
|
|
278
|
+
vendor?: Partial<ProductShop>;
|
|
279
|
+
}
|
package/types/theme.ts
ADDED
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
import type { FC, ReactNode } from 'react';
|
|
2
|
+
import type { Block, BlockDataMap, BlockType, BlogBlockData, CategoriesBlockData, ProductsBlockData } from './block';
|
|
3
|
+
import type { CartItem as CartLineItem } from './cart';
|
|
4
|
+
import type { ImageVariants } from './media';
|
|
5
|
+
import type { MenuItem } from './menu';
|
|
6
|
+
import type { BlogCategory, Collection, Page, Pagination, Post } from './page';
|
|
7
|
+
import type { Product } from './product';
|
|
8
|
+
import type { DesignTokenAxisPath, DesignTokens, StorefrontConfig, VendorProfile } from './vendor';
|
|
9
|
+
|
|
10
|
+
export interface HomePageProps {
|
|
11
|
+
blocks: Block[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface GenericPageProps {
|
|
15
|
+
page: Page;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface BlogPageProps {
|
|
19
|
+
posts: Post[];
|
|
20
|
+
pagination?: Pagination;
|
|
21
|
+
/** Vendor blog categories with >=1 live post (for the filter bar). */
|
|
22
|
+
categories?: BlogCategory[];
|
|
23
|
+
/** Currently-filtered category slug, if any. */
|
|
24
|
+
activeCategory?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface PostPageProps {
|
|
28
|
+
post: Post;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface CollectionsPageProps {
|
|
32
|
+
collections: Collection[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface PoliciesPageProps {
|
|
36
|
+
/** Published policies (returns, shipping, privacy, terms, …). Each is a
|
|
37
|
+
* `Page` with `type: 'policy'`; its detail view resolves through the
|
|
38
|
+
* generic `/[vendor]/[slug]` route. */
|
|
39
|
+
policies: Page[];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface ShopCategory {
|
|
43
|
+
id: string;
|
|
44
|
+
slug: string;
|
|
45
|
+
name: string;
|
|
46
|
+
image?: string | null;
|
|
47
|
+
/** TaxonomyCategoryResource:22 — attached to every node of the tree
|
|
48
|
+
* (MediaVariantMap::attachUrlVariants walks `children` too). */
|
|
49
|
+
image_variants?: ImageVariants | null;
|
|
50
|
+
products_count?: number;
|
|
51
|
+
children?: ShopCategory[];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface ShopPageProps {
|
|
55
|
+
categories: ShopCategory[];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface CollectionPageProps {
|
|
59
|
+
collection: Collection;
|
|
60
|
+
products: Product[];
|
|
61
|
+
pagination?: Pagination;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface ProductPageProps {
|
|
65
|
+
product: Product;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface GalleryPageProps {
|
|
69
|
+
gallery: Page;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface LayoutProps {
|
|
73
|
+
children: ReactNode;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface SubscribeFormProps {
|
|
77
|
+
heading?: string;
|
|
78
|
+
tagline?: string;
|
|
79
|
+
cta?: string;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface HeaderProps {
|
|
83
|
+
menu: MenuItem[];
|
|
84
|
+
logo?: string | null;
|
|
85
|
+
announcement?: {
|
|
86
|
+
enabled: boolean;
|
|
87
|
+
text: string;
|
|
88
|
+
link?: string | null;
|
|
89
|
+
} | null;
|
|
90
|
+
/** Site-wide header icon visibility — all default true (see config.header.show_*). */
|
|
91
|
+
showSearch?: boolean;
|
|
92
|
+
showCart?: boolean;
|
|
93
|
+
showAccount?: boolean;
|
|
94
|
+
/** Theme-specific extra header icon (currently only glow/overlay's "Offers" shortcut). */
|
|
95
|
+
showOffers?: boolean;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface FooterProps {
|
|
99
|
+
/** Footer brand heading override; falls back to the vendor name. */
|
|
100
|
+
heading?: string | null;
|
|
101
|
+
/** Footer brand description/tagline override; falls back to vendor.tagline. */
|
|
102
|
+
tagline?: string | null;
|
|
103
|
+
columns?: Array<{ heading: string; content: string }>;
|
|
104
|
+
socials?: Record<string, string>;
|
|
105
|
+
copyright?: string | null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export interface CartShellProps {
|
|
109
|
+
items: CartLineItem[];
|
|
110
|
+
currency: string;
|
|
111
|
+
total: number;
|
|
112
|
+
empty: boolean;
|
|
113
|
+
onIncrease: (id: string, shopId: string) => void;
|
|
114
|
+
onDecrease: (id: string, shopId: string) => void;
|
|
115
|
+
onRemove: (id: string, shopId: string) => void;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export interface CheckoutShellProps {
|
|
119
|
+
cartItems: ReactNode;
|
|
120
|
+
deliveryModes: ReactNode;
|
|
121
|
+
addressPicker: ReactNode | null;
|
|
122
|
+
/** Rendered when two delivery options (queek/vendor) are available for customer selection */
|
|
123
|
+
deliverySourcePicker: ReactNode | null;
|
|
124
|
+
/** Rendered when delivery_mode is 'shipping' — lets customer pick a shipping zone */
|
|
125
|
+
shippingZonePicker: ReactNode | null;
|
|
126
|
+
schedulePicker: ReactNode | null;
|
|
127
|
+
paymentPicker: ReactNode;
|
|
128
|
+
promoInput: ReactNode;
|
|
129
|
+
noteInputs: ReactNode;
|
|
130
|
+
feesBreakdown: ReactNode;
|
|
131
|
+
deliveryMessage: { type: 'warning' | 'danger'; message: string } | null;
|
|
132
|
+
submitButton: ReactNode;
|
|
133
|
+
/** Whether the customer is signed in. Shells that skip the summary→checkout
|
|
134
|
+
* step (e.g. the QR single-modal sheet) use this to show a sign-in CTA that
|
|
135
|
+
* routes guests into the auth flow via `onTabChange('checkout')`. */
|
|
136
|
+
isAuthenticated?: boolean;
|
|
137
|
+
activeTab: 'summary' | 'checkout';
|
|
138
|
+
onTabChange: (tab: 'summary' | 'checkout') => void;
|
|
139
|
+
onBack?: () => void;
|
|
140
|
+
totalCount: number;
|
|
141
|
+
subtotal: number;
|
|
142
|
+
currency: string;
|
|
143
|
+
mode?: 'page' | 'panel';
|
|
144
|
+
/** Published policies (refund/shipping/privacy/terms) for the checkout footer links. */
|
|
145
|
+
policies: Page[];
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export interface LoginShellProps {
|
|
149
|
+
children: ReactNode;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export interface SignupShellProps {
|
|
153
|
+
children: ReactNode;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export interface AccountShellProps {
|
|
157
|
+
children: ReactNode;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export interface ProductCardProps {
|
|
161
|
+
product: Product;
|
|
162
|
+
href?: string;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export interface ThemeContextValue {
|
|
166
|
+
vendor: VendorProfile;
|
|
167
|
+
config: StorefrontConfig;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export interface ProductsBlockProps extends ProductsBlockData {
|
|
171
|
+
products?: Product[];
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export interface CategoriesBlockProps extends CategoriesBlockData {
|
|
175
|
+
categories?: Array<{ id: string; slug: string; name: string; image?: string | null }>;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export interface BlogBlockProps extends BlogBlockData {
|
|
179
|
+
posts?: Post[];
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export type ThemeManifestFieldType =
|
|
183
|
+
| 'string'
|
|
184
|
+
| 'text'
|
|
185
|
+
| 'markdown'
|
|
186
|
+
| 'image'
|
|
187
|
+
| 'url'
|
|
188
|
+
| 'int'
|
|
189
|
+
| 'number'
|
|
190
|
+
| 'boolean'
|
|
191
|
+
| 'color'
|
|
192
|
+
| 'string[]'
|
|
193
|
+
| 'object'
|
|
194
|
+
| 'object[]'
|
|
195
|
+
| 'enum';
|
|
196
|
+
|
|
197
|
+
export interface ThemeManifestField {
|
|
198
|
+
type: ThemeManifestFieldType;
|
|
199
|
+
required?: boolean;
|
|
200
|
+
note?: string;
|
|
201
|
+
default?: string | number | boolean | null;
|
|
202
|
+
options?: string[];
|
|
203
|
+
of?: Record<string, ThemeManifestField>;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export interface ThemeManifestVariant {
|
|
207
|
+
id: string;
|
|
208
|
+
label: string;
|
|
209
|
+
default?: boolean;
|
|
210
|
+
purpose?: string;
|
|
211
|
+
/**
|
|
212
|
+
* The data fields this variant's component actually renders. During the
|
|
213
|
+
* incremental migration, an existing prose string remains valid. Converted
|
|
214
|
+
* content fields use FieldDefinition so the merchant editor reads a closed
|
|
215
|
+
* control type while Qee receives the same explanatory note from the backend.
|
|
216
|
+
*/
|
|
217
|
+
fields?: Record<string, string | ThemeManifestField>;
|
|
218
|
+
/**
|
|
219
|
+
* Which design-token LEAVES (`DesignTokenAxisPath` — see its doc comment
|
|
220
|
+
* in vendor.ts) this variant's rendered markup actually consumes via CSS
|
|
221
|
+
* vars, i.e. which controls a per-section `block.tokens` override on THIS
|
|
222
|
+
* variant will visibly change. Authored next to the component/CSS that
|
|
223
|
+
* consumes the vars so it can't drift — flows verbatim through
|
|
224
|
+
* generate-theme-registry.ts → registry.json → Laravel's Theme.variants →
|
|
225
|
+
* ThemeManifestService (mirrors `fields`).
|
|
226
|
+
*
|
|
227
|
+
* Granular per sub-control, NOT per parent dimension — `'shape'` is not a
|
|
228
|
+
* valid entry; declare `'shape.radius'` and/or `'shape.border_weight'`
|
|
229
|
+
* independently, since one can be CSS-var-driven while the other is a
|
|
230
|
+
* hardcoded literal on the same or a sibling element (same for `image.*`
|
|
231
|
+
* and `color.*`). Declare `image.*` entries only once that variant's image
|
|
232
|
+
* element(s) are actually wired to the corresponding `--image-*` var (see
|
|
233
|
+
* each theme's own theme.css). If a variant renders no image, omit all
|
|
234
|
+
* `image.*` entries.
|
|
235
|
+
*
|
|
236
|
+
* No back-compat default: an omitted `editable` (or an omitted leaf) means
|
|
237
|
+
* NOT stylable for that control — every variant in every theme has an
|
|
238
|
+
* explicit, audited array as of the 2026-08-03 granular pass.
|
|
239
|
+
*/
|
|
240
|
+
editable?: DesignTokenAxisPath[];
|
|
241
|
+
/**
|
|
242
|
+
* Business buckets this variant is a strong fit for, as
|
|
243
|
+
* `config/category_packs.php` keys ONLY (queek_backend): food | supermarket |
|
|
244
|
+
* product | pharmacy | laundry | delivery | service | gas-refill |
|
|
245
|
+
* local_market. Omitted = universal. Authored next to the component (same
|
|
246
|
+
* anti-drift placement as `fields`/`editable`) and flows verbatim through
|
|
247
|
+
* generate-theme-registry.ts -> registry.json -> Laravel's Theme.variants ->
|
|
248
|
+
* ThemeManifestService, where BuildHomepageTool's scored resolver boosts a
|
|
249
|
+
* bucket match when auto-building a store. The backend matches buckets with
|
|
250
|
+
* `_`/`-` treated as equivalent.
|
|
251
|
+
*/
|
|
252
|
+
best_for?: string[];
|
|
253
|
+
/**
|
|
254
|
+
* Default true. false = the AI store builder must never auto-select this
|
|
255
|
+
* variant (special-purpose layouts: review-screenshot walls, decorative
|
|
256
|
+
* galleries, checkout/focused chrome). Manual/qee selection stays allowed.
|
|
257
|
+
*/
|
|
258
|
+
auto_pick?: boolean;
|
|
259
|
+
/**
|
|
260
|
+
* Gallery scope only: minimum slides this variant needs to render well
|
|
261
|
+
* (e.g. mosaic collages need 3). The builder gates on the vendor's actual
|
|
262
|
+
* hero slide count. Omitted = 1.
|
|
263
|
+
*/
|
|
264
|
+
min_images?: number;
|
|
265
|
+
/**
|
|
266
|
+
* Gallery hero variants only, in a theme that also declares an "overlay"
|
|
267
|
+
* header: does this hero present a dark, full-bleed area directly behind
|
|
268
|
+
* the nav, so a transparent header with white text reads clearly over it?
|
|
269
|
+
* The backend resolves the `header` scope per vendor from this — a hero
|
|
270
|
+
* declaring `true` gets the overlay header, everything else gets a solid
|
|
271
|
+
* one. Declared per variant (audited against the actual component/CSS),
|
|
272
|
+
* never inferred from the id — "does it have images" is not the same
|
|
273
|
+
* question, and "does it have a photo" isn't either: a variant can be
|
|
274
|
+
* full of photos and still have no dark band under the nav (a side-by-side
|
|
275
|
+
* split with a light copy panel, a gapped multi-panel diptych).
|
|
276
|
+
*/
|
|
277
|
+
supports_overlay_header?: boolean;
|
|
278
|
+
/**
|
|
279
|
+
* Header scope only: the inverse of `supports_overlay_header` — does THIS
|
|
280
|
+
* header variant need a hero that can carry a transparent bar to make
|
|
281
|
+
* sense at all? Omitted = false (a solid/always-visible header works
|
|
282
|
+
* regardless of what hero, if any, precedes it). The backend resolver
|
|
283
|
+
* reads both flags without ever checking a header's own id, so a theme is
|
|
284
|
+
* free to name its transparent header anything, or make a solid one the
|
|
285
|
+
* default, without breaking resolution.
|
|
286
|
+
*/
|
|
287
|
+
requires_overlay_hero?: boolean;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export interface ThemeImageSpec {
|
|
291
|
+
width: number;
|
|
292
|
+
height: number;
|
|
293
|
+
label: string;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export interface ThemeManifest {
|
|
297
|
+
name: string;
|
|
298
|
+
slug: string;
|
|
299
|
+
description: string;
|
|
300
|
+
version: string;
|
|
301
|
+
variants: Record<string, ThemeManifestVariant[]>;
|
|
302
|
+
/**
|
|
303
|
+
* A per-scope migration marker. A marked scope may only contain structured
|
|
304
|
+
* field metadata; an unmarked scope remains transitional. More scopes can
|
|
305
|
+
* be added without creating a parallel top-level marker for each one.
|
|
306
|
+
*/
|
|
307
|
+
fields_contract?: Partial<Record<string, 'structured'>>;
|
|
308
|
+
features?: string[];
|
|
309
|
+
images?: Record<string, ThemeImageSpec>;
|
|
310
|
+
/**
|
|
311
|
+
* Theme's default semantic design-token set (design-token-contract.md,
|
|
312
|
+
* queek_backend `.agent/.tmp/`) — the theme's current look expressed as
|
|
313
|
+
* tokens. Surfaced via `ThemeManifestService`/registry.json as the
|
|
314
|
+
* baseline the backend resolver layers vendor `tokens` overrides onto
|
|
315
|
+
* (theme defaults ← legacy fields ← vendor overrides). Optional during
|
|
316
|
+
* the pilot rollout — themes without it fall back to the storefront
|
|
317
|
+
* injector's own hardcoded defaults (`lib/core/utils/brand.ts`).
|
|
318
|
+
*/
|
|
319
|
+
tokens?: DesignTokens;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Block types that are framework-owned (React logic ships in core, themes
|
|
324
|
+
* only style via `.core-block-*` CSS). Themes do NOT implement these.
|
|
325
|
+
*/
|
|
326
|
+
export type FrameworkBlockType = 'divider' | 'embed' | 'video' | 'table' | 'button' | 'image' | 'reviews' | 'faq' | 'callout' | 'quote' | 'product_qa';
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Block types the theme is responsible for implementing. Framework-owned
|
|
330
|
+
* types are excluded — page-renderer routes those directly to core.
|
|
331
|
+
*
|
|
332
|
+
* `content` is theme-owned for structured variants (promo, brand-story,
|
|
333
|
+
* testimonials, marquee, etc.), but the default markdown variant is
|
|
334
|
+
* framework-owned at the renderer level.
|
|
335
|
+
*/
|
|
336
|
+
export type ThemeBlockType = Exclude<BlockType, FrameworkBlockType>;
|
|
337
|
+
|
|
338
|
+
export type ThemeBlocks = {
|
|
339
|
+
[K in ThemeBlockType]: FC<BlockDataMap[K]>;
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
export interface ThemeModule {
|
|
343
|
+
Layout: FC<LayoutProps>;
|
|
344
|
+
/** @deprecated Use getHeader(variant) instead */
|
|
345
|
+
Header: FC<HeaderProps>;
|
|
346
|
+
/** @deprecated Use getFooter(variant) instead */
|
|
347
|
+
Footer: FC<FooterProps>;
|
|
348
|
+
/** @deprecated Use getBlock(type, variant) instead */
|
|
349
|
+
blocks: ThemeBlocks;
|
|
350
|
+
getHeader: (variant?: string) => FC<HeaderProps>;
|
|
351
|
+
getFooter: (variant?: string) => FC<FooterProps>;
|
|
352
|
+
getBlock: <T extends ThemeBlockType>(type: T, variant?: string) => ThemeBlocks[T];
|
|
353
|
+
pages: {
|
|
354
|
+
Home: FC<HomePageProps>;
|
|
355
|
+
Page: FC<GenericPageProps>;
|
|
356
|
+
Blog: FC<BlogPageProps>;
|
|
357
|
+
Post: FC<PostPageProps>;
|
|
358
|
+
Shop?: FC<ShopPageProps>;
|
|
359
|
+
Collections?: FC<CollectionsPageProps>;
|
|
360
|
+
/** Optional override for the `/policies` listing. When absent, core renders
|
|
361
|
+
* `DefaultPoliciesPage` (themeable via `.core-policies-*` CSS). */
|
|
362
|
+
Policies?: FC<PoliciesPageProps>;
|
|
363
|
+
Collection: FC<CollectionPageProps>;
|
|
364
|
+
Product: FC<ProductPageProps>;
|
|
365
|
+
Gallery: FC<GalleryPageProps>;
|
|
366
|
+
};
|
|
367
|
+
shells?: {
|
|
368
|
+
CartShell?: FC<CartShellProps>;
|
|
369
|
+
CheckoutShell?: FC<CheckoutShellProps>;
|
|
370
|
+
LoginShell?: FC<LoginShellProps>;
|
|
371
|
+
SignupShell?: FC<SignupShellProps>;
|
|
372
|
+
AccountShell?: FC<AccountShellProps>;
|
|
373
|
+
};
|
|
374
|
+
productCard?: FC<ProductCardProps>;
|
|
375
|
+
ModalLayer?: FC;
|
|
376
|
+
/** Optional override for the default auth modal presentation. Theme can
|
|
377
|
+
* wrap <AuthFlow /> in any layout (sidebar, full-page split, inline card, etc). */
|
|
378
|
+
AuthRenderer?: FC;
|
|
379
|
+
manifest: ThemeManifest;
|
|
380
|
+
}
|