@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.
Files changed (120) hide show
  1. package/README.md +37 -0
  2. package/api/collections.ts +93 -0
  3. package/api/orders.ts +23 -0
  4. package/apps/analytics-events.ts +32 -0
  5. package/apps/apps.css +613 -0
  6. package/apps/follow-card.tsx +192 -0
  7. package/apps/gtag.ts +19 -0
  8. package/apps/subscribe-form.tsx +124 -0
  9. package/client.ts +52 -0
  10. package/components/auth/auth-email-login-step.tsx +46 -0
  11. package/components/auth/auth-email-otp-step.tsx +54 -0
  12. package/components/auth/auth-email-otp-verify-step.tsx +82 -0
  13. package/components/auth/auth-email-register-step.tsx +59 -0
  14. package/components/auth/auth-flow.tsx +26 -0
  15. package/components/auth/auth-signup-step.tsx +53 -0
  16. package/components/blog/category-filter.tsx +92 -0
  17. package/components/blog/index.ts +12 -0
  18. package/components/blog/pagination.tsx +73 -0
  19. package/components/blog/post-categories.tsx +42 -0
  20. package/components/blog/post-meta.tsx +49 -0
  21. package/components/blog/related-posts.tsx +54 -0
  22. package/components/blog/share-buttons.tsx +89 -0
  23. package/components/cart/cart-panel-controller.tsx +67 -0
  24. package/components/cart/default-cart-shell.tsx +207 -0
  25. package/components/checkout/checkout-contact.tsx +84 -0
  26. package/components/checkout/default-checkout-shell.tsx +177 -0
  27. package/components/checkout/policy-links.tsx +98 -0
  28. package/components/image.tsx +149 -0
  29. package/components/markdown.tsx +54 -0
  30. package/components/product-media-frame.tsx +56 -0
  31. package/components/variant-picker.css +84 -0
  32. package/components/variant-picker.tsx +107 -0
  33. package/design-token-preview.tsx +81 -0
  34. package/edit-mode-active.tsx +324 -0
  35. package/edit-mode.tsx +80 -0
  36. package/hooks/use-address-autocomplete.ts +62 -0
  37. package/hooks/use-auth-flow.tsx +346 -0
  38. package/hooks/use-auth.ts +17 -0
  39. package/hooks/use-cart.ts +14 -0
  40. package/hooks/use-categories.ts +112 -0
  41. package/hooks/use-collection-params.ts +67 -0
  42. package/hooks/use-href.ts +15 -0
  43. package/hooks/use-posts.ts +75 -0
  44. package/hooks/use-product-detail.ts +69 -0
  45. package/hooks/use-product-questions.ts +77 -0
  46. package/hooks/use-product-sections.ts +165 -0
  47. package/hooks/use-products.ts +216 -0
  48. package/hooks/use-related-products.ts +56 -0
  49. package/hooks/use-resolve-coordinates.ts +48 -0
  50. package/hooks/use-reviews.ts +167 -0
  51. package/hooks/use-search.ts +55 -0
  52. package/hooks/use-shop-cart.ts +31 -0
  53. package/hooks/use-shop-params.ts +68 -0
  54. package/hooks/use-shop.ts +186 -0
  55. package/hooks/use-variant-selection.ts +98 -0
  56. package/index.ts +49 -0
  57. package/package.json +41 -0
  58. package/page-renderer.tsx +365 -0
  59. package/provider.tsx +91 -0
  60. package/sdk/client.ts +221 -0
  61. package/sdk/error-handler.ts +37 -0
  62. package/sdk/platform.ts +78 -0
  63. package/sdk/queek-client.ts +104 -0
  64. package/sdk/reauth-guard.ts +27 -0
  65. package/sdk/server-store-client.ts +165 -0
  66. package/shared-blocks/button.tsx +41 -0
  67. package/shared-blocks/callout.tsx +35 -0
  68. package/shared-blocks/content-default.tsx +23 -0
  69. package/shared-blocks/core-blocks.css +1792 -0
  70. package/shared-blocks/divider.tsx +17 -0
  71. package/shared-blocks/embed.tsx +40 -0
  72. package/shared-blocks/faq.tsx +44 -0
  73. package/shared-blocks/image.tsx +29 -0
  74. package/shared-blocks/index.ts +24 -0
  75. package/shared-blocks/manifest.ts +76 -0
  76. package/shared-blocks/product-qa.tsx +203 -0
  77. package/shared-blocks/quote.tsx +20 -0
  78. package/shared-blocks/reviews.tsx +436 -0
  79. package/shared-blocks/table.tsx +45 -0
  80. package/shared-blocks/video.tsx +60 -0
  81. package/stores/address-modal-store.ts +15 -0
  82. package/stores/auth-modal-store.ts +17 -0
  83. package/stores/cart-panel-store.ts +17 -0
  84. package/stores/cart-store.ts +172 -0
  85. package/stores/checkout-store.ts +167 -0
  86. package/stores/edit-preview-store.ts +61 -0
  87. package/stores/location-store.ts +41 -0
  88. package/stores/orders-sheet-store.ts +21 -0
  89. package/stores/policy-modal-store.ts +15 -0
  90. package/stores/product-modal-store.ts +18 -0
  91. package/stores/search-modal-store.ts +17 -0
  92. package/stores/toast-store.ts +46 -0
  93. package/stores/user-store.ts +88 -0
  94. package/theme-context.tsx +28 -0
  95. package/theme-variant-resolver.ts +44 -0
  96. package/types/block.ts +292 -0
  97. package/types/cart.ts +21 -0
  98. package/types/media.ts +55 -0
  99. package/types/menu.ts +27 -0
  100. package/types/order.ts +119 -0
  101. package/types/page.ts +86 -0
  102. package/types/product.ts +279 -0
  103. package/types/theme.ts +380 -0
  104. package/types/vendor.ts +243 -0
  105. package/utils/addon-selection.ts +31 -0
  106. package/utils/benefit-icon.tsx +145 -0
  107. package/utils/brand-palette.ts +64 -0
  108. package/utils/brand.ts +518 -0
  109. package/utils/contact-location.ts +44 -0
  110. package/utils/format.ts +64 -0
  111. package/utils/hero-content.ts +35 -0
  112. package/utils/image-srcset.ts +157 -0
  113. package/utils/markdown.tsx +237 -0
  114. package/utils/match-variant.ts +31 -0
  115. package/utils/menu-link.ts +41 -0
  116. package/utils/placeholder-resolver.ts +184 -0
  117. package/utils/product-media.ts +77 -0
  118. package/utils/product-normalizer.ts +323 -0
  119. package/utils/vendor-host.ts +173 -0
  120. package/utils/video-embed.ts +38 -0
@@ -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
+ }