@usequeek/theme-kit 0.1.0 → 0.1.2

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 (46) hide show
  1. package/apps/follow-card.tsx +1 -0
  2. package/apps/subscribe-form.tsx +1 -0
  3. package/client.ts +4 -0
  4. package/components/auth/auth-email-login-step.tsx +1 -0
  5. package/components/auth/auth-email-otp-step.tsx +1 -0
  6. package/components/auth/auth-email-otp-verify-step.tsx +1 -0
  7. package/components/auth/auth-email-register-step.tsx +1 -0
  8. package/components/auth/auth-flow.tsx +1 -0
  9. package/components/auth/auth-signup-step.tsx +1 -0
  10. package/components/blog/category-filter.tsx +1 -0
  11. package/components/blog/pagination.tsx +1 -0
  12. package/components/blog/post-categories.tsx +1 -0
  13. package/components/blog/post-meta.tsx +1 -0
  14. package/components/blog/related-posts.tsx +1 -0
  15. package/components/blog/share-buttons.tsx +1 -0
  16. package/components/cart/cart-panel-controller.tsx +1 -0
  17. package/components/cart/default-cart-shell.tsx +1 -0
  18. package/components/checkout/checkout-contact.tsx +1 -0
  19. package/components/checkout/default-checkout-shell.tsx +1 -0
  20. package/components/checkout/policy-links.tsx +1 -0
  21. package/components/markdown.tsx +1 -0
  22. package/components/product-media-frame.tsx +1 -0
  23. package/components/variant-picker.tsx +1 -0
  24. package/demo.ts +201 -0
  25. package/edit-mode-active.tsx +1 -0
  26. package/edit-mode.tsx +1 -0
  27. package/hooks/use-auth-flow.tsx +1 -0
  28. package/index.ts +3 -0
  29. package/package.json +1 -1
  30. package/page-renderer.tsx +1 -0
  31. package/provider.tsx +1 -0
  32. package/shared-blocks/button.tsx +1 -0
  33. package/shared-blocks/callout.tsx +1 -0
  34. package/shared-blocks/content-default.tsx +1 -0
  35. package/shared-blocks/divider.tsx +1 -0
  36. package/shared-blocks/embed.tsx +1 -0
  37. package/shared-blocks/faq.tsx +1 -0
  38. package/shared-blocks/image.tsx +1 -0
  39. package/shared-blocks/product-qa.tsx +1 -0
  40. package/shared-blocks/quote.tsx +1 -0
  41. package/shared-blocks/reviews.tsx +1 -0
  42. package/shared-blocks/table.tsx +1 -0
  43. package/shared-blocks/video.tsx +1 -0
  44. package/theme-context.tsx +1 -0
  45. package/theme-mount.tsx +88 -0
  46. package/utils/benefit-icon.tsx +1 -0
@@ -1,5 +1,6 @@
1
1
  'use client';
2
2
 
3
+ import type { JSX } from 'react';
3
4
  import { useState, useCallback, useEffect } from 'react';
4
5
  import { useStorefront } from '../provider';
5
6
  import { useUserStore } from '../stores/user-store';
@@ -1,5 +1,6 @@
1
1
  'use client';
2
2
 
3
+ import type { JSX } from 'react';
3
4
  import { useCallback, useId, useState } from 'react';
4
5
  import { useStorefront } from '../provider';
5
6
  import { getQueekClient } from '../sdk/queek-client';
package/client.ts CHANGED
@@ -50,3 +50,7 @@ export * from './stores/location-store';
50
50
  export * from './stores/address-modal-store';
51
51
  export * from './stores/auth-modal-store';
52
52
  export * from './stores/cart-store';
53
+
54
+ // Mounting a theme — the piece a theme developer cannot build without.
55
+ export * from './theme-mount';
56
+ export * from './theme-context';
@@ -1,4 +1,5 @@
1
1
  'use client';
2
+ import type { JSX } from 'react';
2
3
  import { useAuthFlow } from '../../hooks/use-auth-flow';
3
4
 
4
5
  export function AuthEmailLoginStep(): JSX.Element {
@@ -1,5 +1,6 @@
1
1
  'use client';
2
2
 
3
+ import type { JSX } from 'react';
3
4
  import { useAuthFlow } from '../../hooks/use-auth-flow';
4
5
 
5
6
  export function AuthEmailOtpStep(): JSX.Element {
@@ -1,5 +1,6 @@
1
1
  'use client';
2
2
 
3
+ import type { JSX } from 'react';
3
4
  import { useRef } from 'react';
4
5
  import { useAuthFlow } from '../../hooks/use-auth-flow';
5
6
 
@@ -1,4 +1,5 @@
1
1
  'use client';
2
+ import type { JSX } from 'react';
2
3
  import { useAuthFlow } from '../../hooks/use-auth-flow';
3
4
 
4
5
  export function AuthEmailRegisterStep(): JSX.Element {
@@ -1,5 +1,6 @@
1
1
  'use client';
2
2
 
3
+ import type { JSX } from 'react';
3
4
  import { AuthFlowProvider, useAuthFlow } from '../../hooks/use-auth-flow';
4
5
  import { AuthEmailOtpStep } from './auth-email-otp-step';
5
6
  import { AuthEmailOtpVerifyStep } from './auth-email-otp-verify-step';
@@ -1,5 +1,6 @@
1
1
  'use client';
2
2
 
3
+ import type { JSX } from 'react';
3
4
  import { useAuthFlow } from '../../hooks/use-auth-flow';
4
5
 
5
6
  export function AuthSignupStep(): JSX.Element {
@@ -1,5 +1,6 @@
1
1
  'use client';
2
2
 
3
+ import type { JSX } from 'react';
3
4
  import Link from 'next/link';
4
5
  import type { BlogCategory } from '../../types/page';
5
6
  import { useStorefront } from '../../provider';
@@ -1,5 +1,6 @@
1
1
  'use client';
2
2
 
3
+ import type { JSX } from 'react';
3
4
  import Link from 'next/link';
4
5
  import type { Pagination } from '../../types/page';
5
6
  import { useStorefront } from '../../provider';
@@ -1,5 +1,6 @@
1
1
  'use client';
2
2
 
3
+ import type { JSX } from 'react';
3
4
  import Link from 'next/link';
4
5
  import type { BlogCategory } from '../../types/page';
5
6
  import { useStorefront } from '../../provider';
@@ -1,5 +1,6 @@
1
1
  'use client';
2
2
 
3
+ import type { JSX } from 'react';
3
4
  import type { Post } from '../../types/page';
4
5
  import { useStorefront } from '../../provider';
5
6
  import { Image } from '../image';
@@ -1,5 +1,6 @@
1
1
  'use client';
2
2
 
3
+ import type { JSX } from 'react';
3
4
  import Link from 'next/link';
4
5
  import type { Post } from '../../types/page';
5
6
  import { useStorefront } from '../../provider';
@@ -1,5 +1,6 @@
1
1
  'use client';
2
2
 
3
+ import type { JSX } from 'react';
3
4
  import { useEffect, useState } from 'react';
4
5
 
5
6
  interface ShareButtonsProps {
@@ -1,5 +1,6 @@
1
1
  'use client';
2
2
 
3
+ import type { JSX } from 'react';
3
4
  import { useEffect, type ReactNode } from 'react';
4
5
  import { useRouter } from 'next/navigation';
5
6
  import type { CartItem } from '../../types/cart';
@@ -1,5 +1,6 @@
1
1
  'use client';
2
2
 
3
+ import type { JSX } from 'react';
3
4
  import { useRef, useState } from 'react';
4
5
  import Link from 'next/link';
5
6
  import type { CartShellProps } from '../../types/theme';
@@ -1,5 +1,6 @@
1
1
  'use client';
2
2
 
3
+ import type { JSX } from 'react';
3
4
  import { useEffect, useRef, useState } from 'react';
4
5
  import { useAuth } from '../../hooks/use-auth';
5
6
  import { Image } from '../image';
@@ -1,4 +1,5 @@
1
1
  'use client';
2
+ import type { JSX } from 'react';
2
3
  import Link from 'next/link';
3
4
  import type { CheckoutShellProps } from '../../types/theme';
4
5
  import { formatMoney } from '../../utils/format';
@@ -1,5 +1,6 @@
1
1
  'use client';
2
2
 
3
+ import type { JSX } from 'react';
3
4
  import { useEffect, useSyncExternalStore } from 'react';
4
5
  import { createPortal } from 'react-dom';
5
6
  import Link from 'next/link';
@@ -1,5 +1,6 @@
1
1
  'use client';
2
2
 
3
+ import type { JSX } from 'react';
3
4
  import type { ComponentPropsWithoutRef } from 'react';
4
5
  import ReactMarkdown from 'react-markdown';
5
6
  import remarkGfm from 'remark-gfm';
@@ -1,5 +1,6 @@
1
1
  'use client';
2
2
 
3
+ import type { JSX } from 'react';
3
4
  import { Image, type PlaceholderProp } from './image';
4
5
  import type { ImageIntent } from '../utils/image-srcset';
5
6
  import type { MediaFrame } from '../utils/product-media';
@@ -1,5 +1,6 @@
1
1
  'use client';
2
2
 
3
+ import type { JSX } from 'react';
3
4
  import type { Product, ProductVariant } from '../types/product';
4
5
  import './variant-picker.css';
5
6
 
package/demo.ts ADDED
@@ -0,0 +1,201 @@
1
+ /**
2
+ * Turns a theme's `demo.json` into the props its components expect.
3
+ *
4
+ * SHIPPED WITH THE KIT because without it a theme developer has demo data and
5
+ * no way to render it. THEME.md already requires every theme to carry a
6
+ * demo.json; these are the functions that resolve its block references into
7
+ * real products, categories, posts and collections. They lived in the
8
+ * storefront's own preview routes, so they existed only here — a theme could be
9
+ * written outside this repo and never shown running against its own data.
10
+ *
11
+ * Pure and type-only by construction: no `fs`, no `next`, no app imports. That
12
+ * is what made moving it a rename rather than a rewrite, and it is why these
13
+ * work identically in a developer's preview and in this repo's preview routes.
14
+ */
15
+ import type { Block, ReviewItem } from './types/block';
16
+ import type { Menu } from './types/menu';
17
+ import type { Collection, Page, Post } from './types/page';
18
+ import type { Product } from './types/product';
19
+ import type { StorefrontConfig, VendorProfile } from './types/vendor';
20
+
21
+ /**
22
+ * Pure demo-data transforms — no filesystem access, so this file is safe to
23
+ * import from a 'use client' component (unlike `demo.ts`, which pulls in
24
+ * Node's `fs`/`path` for `loadDemoData` and will break the browser bundle if
25
+ * a client component imports a value from it). `demo.ts` re-exports
26
+ * everything here for existing server-component call sites; new client-side
27
+ * code should import from here directly.
28
+ */
29
+
30
+ export type DemoCategory = { id: string; slug: string; name: string; image?: string | null };
31
+
32
+ export interface PreviewDemoData {
33
+ profile: VendorProfile;
34
+ config: StorefrontConfig;
35
+ menus: Menu[];
36
+ pages: Record<string, Page>;
37
+ products?: Product[];
38
+ categories?: DemoCategory[];
39
+ // Raw shape varies across themes that predate the `content: Block[]` /
40
+ // `cover_image_url` convention (see `normalizePost`) — kept loose here and
41
+ // normalized on read via `getPosts`/`getPost`.
42
+ posts?: Record<string, unknown>[];
43
+ }
44
+
45
+ export function enrichBlocks(blocks: Block[], demo: PreviewDemoData): Block[] {
46
+ return blocks.map((block) => {
47
+ if (block.type === 'products' && demo.products) {
48
+ const data = (block.data ?? {}) as Record<string, unknown>;
49
+ const title = (data.title as string) ?? '';
50
+ const limit = (data.limit as number) ?? demo.products.length;
51
+ const ids = data.ids as string[] | undefined;
52
+ let items = demo.products;
53
+
54
+ if (Array.isArray(ids) && ids.length > 0) {
55
+ // Explicit ids field — preserve order from ids array for section variety
56
+ const byId = new Map(demo.products.map((p) => [p.id, p]));
57
+ items = ids.map((id) => byId.get(id)).filter((p): p is NonNullable<typeof p> => Boolean(p));
58
+ } else if (/new|arrival/i.test(title)) {
59
+ items = demo.products.filter((product) => product.flags?.is_new);
60
+ } else if (/featured|popular|best|focus|lookbook/i.test(title)) {
61
+ items = demo.products.filter((product) => product.flags?.featured);
62
+ }
63
+
64
+ if (items.length === 0) items = demo.products;
65
+ return { ...block, data: { ...block.data, products: items.slice(0, limit) } };
66
+ }
67
+
68
+ if (block.type === 'categories' && demo.categories) {
69
+ return { ...block, data: { ...block.data, categories: demo.categories } };
70
+ }
71
+
72
+ return block;
73
+ });
74
+ }
75
+
76
+ export function getEnrichedPage<T extends Page>(page: T, demo: PreviewDemoData): T {
77
+ return {
78
+ ...page,
79
+ content: enrichBlocks(page.content ?? [], demo),
80
+ };
81
+ }
82
+
83
+ /**
84
+ * `demo.json`'s `posts` array is the documented, spec-correct source (see
85
+ * `themes/THEME.md` — "must mirror the vendor bootstrap shape") — a few
86
+ * themes authored it before the `content: Block[]` / `cover_image_url`
87
+ * convention settled, so this tolerates the older shapes instead of
88
+ * assuming every theme's data is current.
89
+ */
90
+ function normalizePost(raw: Record<string, unknown>): Post {
91
+ const legacyMedia = raw.media as { image?: string; thumbnail?: string } | undefined;
92
+ const coverImageUrl =
93
+ (raw.cover_image_url as string | undefined) ?? legacyMedia?.image ?? legacyMedia?.thumbnail ?? null;
94
+
95
+ let content = raw.content;
96
+ if (typeof content === 'string') {
97
+ // Legacy raw-HTML content string — convert the handful of tags these
98
+ // themes actually used into one markdown content block rather than
99
+ // rendering literal "<p>"/"<h2>" text.
100
+ const markdown = content
101
+ .replace(/<h2>(.*?)<\/h2>/gi, '\n\n## $1\n\n')
102
+ .replace(/<h3>(.*?)<\/h3>/gi, '\n\n### $1\n\n')
103
+ .replace(/<p>(.*?)<\/p>/gi, '$1\n\n')
104
+ .replace(/\n{3,}/g, '\n\n')
105
+ .trim();
106
+ content = [{ type: 'content', data: { markdown } }];
107
+ }
108
+
109
+ return {
110
+ ...raw,
111
+ type: 'post',
112
+ content: Array.isArray(content) ? (content as Block[]) : [],
113
+ cover_image_url: coverImageUrl,
114
+ } as Post;
115
+ }
116
+
117
+ export function getPosts(demo: PreviewDemoData): Post[] {
118
+ return (demo.posts ?? [])
119
+ .map(normalizePost)
120
+ .sort((left, right) => {
121
+ const leftDate = left.published_at ? new Date(left.published_at).getTime() : 0;
122
+ const rightDate = right.published_at ? new Date(right.published_at).getTime() : 0;
123
+ return rightDate - leftDate;
124
+ });
125
+ }
126
+
127
+ /**
128
+ * Vendor-scope reviews for preview surfaces, flattened from the demo's own
129
+ * product reviews.
130
+ *
131
+ * Review-backed sections (beaurify's `content/testimonials`) render real vendor
132
+ * reviews rather than merchant-authored copy — deliberately, see its manifest.
133
+ * But nothing seeded them in preview, so the section returned null everywhere a
134
+ * vendor evaluates it: theme preview, style guide, and the section-library
135
+ * thumbnails. Themes already ship product reviews in demo.json; this exposes
136
+ * them at vendor scope instead of asking every theme to duplicate the data.
137
+ *
138
+ * Demo review records predate `ReviewItem` (`user` is a bare name, the body is
139
+ * `comment`), so both spellings are accepted.
140
+ */
141
+ export function getVendorReviews(demo: PreviewDemoData): ReviewItem[] {
142
+ return (demo.products ?? []).flatMap((product) => {
143
+ const raw = (product as unknown as { reviews?: Record<string, unknown>[] }).reviews ?? [];
144
+
145
+ return raw.map((review, index): ReviewItem => {
146
+ const author = typeof review.user === 'string'
147
+ ? review.user
148
+ : (review.customer_name as string | null) ?? 'Verified buyer';
149
+
150
+ return {
151
+ id: String(review.id ?? `${product.id}-review-${index}`),
152
+ rating: Number(review.rating ?? 5),
153
+ title: (review.title as string | null) ?? null,
154
+ description: (review.description as string | null) ?? (review.comment as string | null) ?? null,
155
+ media: [],
156
+ is_verified_purchase: review.is_verified_purchase !== false,
157
+ created_at: String(review.created_at ?? ''),
158
+ user: { id: `${product.id}-reviewer-${index}`, name: author, avatar: null },
159
+ product: { id: product.id, slug: product.slug, title: product.title, thumbnail: product.media?.thumbnail ?? null },
160
+ };
161
+ });
162
+ });
163
+ }
164
+
165
+ export function getPost(demo: PreviewDemoData, slug: string): Post | null {
166
+ return getPosts(demo).find((post) => post.slug === slug) ?? null;
167
+ }
168
+
169
+ export function getPolicies(demo: PreviewDemoData): Page[] {
170
+ return Object.values(demo.pages).filter((page) => page.type === 'policy');
171
+ }
172
+
173
+ export function getCollections(demo: PreviewDemoData): Collection[] {
174
+ const categories = demo.categories ?? [];
175
+ const products = demo.products ?? [];
176
+
177
+ return categories.map((category) => ({
178
+ id: category.id,
179
+ slug: category.slug,
180
+ name: category.name,
181
+ description: `${category.name} — curated picks from this collection.`,
182
+ image: category.image ?? null,
183
+ mode: 'manual',
184
+ products_count: products.filter((product) => product.categories.some((item) => item.slug === category.slug)).length,
185
+ }));
186
+ }
187
+
188
+ export function getCollection(demo: PreviewDemoData, slug: string): { collection: Collection; products: Product[] } | null {
189
+ const collection = getCollections(demo).find((item) => item.slug === slug);
190
+ if (!collection) return null;
191
+
192
+ const products = (demo.products ?? []).filter((product) => product.categories.some((category) => category.slug === slug));
193
+
194
+ return {
195
+ collection: {
196
+ ...collection,
197
+ products,
198
+ },
199
+ products,
200
+ };
201
+ }
@@ -1,5 +1,6 @@
1
1
  'use client';
2
2
 
3
+ import type { JSX } from 'react';
3
4
  import type { Block } from './types/block';
4
5
  import { useEditPreviewStore } from './stores/edit-preview-store';
5
6
  import { useEffect, useRef, useState, type ReactNode } from 'react';
package/edit-mode.tsx CHANGED
@@ -1,5 +1,6 @@
1
1
  'use client';
2
2
 
3
+ import type { JSX } from 'react';
3
4
  import { useEffect, useState, type ReactNode } from 'react';
4
5
  import dynamic from 'next/dynamic';
5
6
 
@@ -1,5 +1,6 @@
1
1
  'use client';
2
2
 
3
+ import type { JSX } from 'react';
3
4
  import { createContext, useContext, useState, type ReactNode } from 'react';
4
5
  import { QueekSdkError, type EmailVerifyOtpResponse } from '@queekai/client-sdk';
5
6
  import { useStorefront } from '../provider';
package/index.ts CHANGED
@@ -47,3 +47,6 @@ export * from './utils/contact-location';
47
47
  export * from './utils/benefit-icon';
48
48
  export * from './utils/video-embed';
49
49
  export * from './utils/match-variant';
50
+
51
+ // Rendering a theme's own demo.json — what makes local preview possible.
52
+ export * from './demo';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usequeek/theme-kit",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "private": false,
5
5
  "license": "UNLICENSED",
6
6
  "description": "The framework a Queek storefront theme is built against.",
package/page-renderer.tsx CHANGED
@@ -1,5 +1,6 @@
1
1
  'use client';
2
2
 
3
+ import type { JSX } from 'react';
3
4
  import { Component, Suspense, use, useEffect, useMemo, type ComponentType, type ReactNode } from 'react';
4
5
  import type { Block, BlockDataMap } from './types/block';
5
6
  import type { Product } from './types/product';
package/provider.tsx CHANGED
@@ -1,5 +1,6 @@
1
1
  'use client';
2
2
 
3
+ import type { JSX } from 'react';
3
4
  import { createContext, useContext, useMemo, useRef, type ReactNode } from 'react';
4
5
  import type { Menu } from './types/menu';
5
6
  import type { ReviewItem } from './types/block';
@@ -1,5 +1,6 @@
1
1
  'use client';
2
2
 
3
+ import type { JSX } from 'react';
3
4
  import Link from 'next/link';
4
5
  import type { ButtonBlockData } from '../types/block';
5
6
 
@@ -1,5 +1,6 @@
1
1
  'use client';
2
2
 
3
+ import type { JSX } from 'react';
3
4
  import type { CalloutBlockData, CalloutTone } from '../types/block';
4
5
  import { Markdown } from '../components/markdown';
5
6
 
@@ -1,5 +1,6 @@
1
1
  'use client';
2
2
 
3
+ import type { JSX } from 'react';
3
4
  import type { ContentBlockData } from '../types/block';
4
5
  import { Markdown } from '../components/markdown';
5
6
 
@@ -1,5 +1,6 @@
1
1
  'use client';
2
2
 
3
+ import type { JSX } from 'react';
3
4
  import type { DividerBlockData } from '../types/block';
4
5
 
5
6
  /**
@@ -1,5 +1,6 @@
1
1
  'use client';
2
2
 
3
+ import type { JSX } from 'react';
3
4
  import type { EmbedBlockData } from '../types/block';
4
5
 
5
6
  /**
@@ -1,5 +1,6 @@
1
1
  'use client';
2
2
 
3
+ import type { JSX } from 'react';
3
4
  import type { FaqBlockData } from '../types/block';
4
5
 
5
6
  /**
@@ -1,5 +1,6 @@
1
1
  'use client';
2
2
 
3
+ import type { JSX } from 'react';
3
4
  import type { ImageBlockData } from '../types/block';
4
5
  import { Image } from '../components/image';
5
6
 
@@ -1,5 +1,6 @@
1
1
  'use client';
2
2
 
3
+ import type { JSX } from 'react';
3
4
  import { useState } from 'react';
4
5
  import type { ProductQaBlockData, ProductQuestionItem } from '../types/block';
5
6
  import { Image } from '../components/image';
@@ -1,5 +1,6 @@
1
1
  'use client';
2
2
 
3
+ import type { JSX } from 'react';
3
4
  import type { QuoteBlockData } from '../types/block';
4
5
 
5
6
  /**
@@ -1,5 +1,6 @@
1
1
  'use client';
2
2
 
3
+ import type { JSX } from 'react';
3
4
  import { useEffect, useRef, useState } from 'react';
4
5
  import type { ReviewItem, ReviewsBlockData, ReviewsSummary } from '../types/block';
5
6
  import { Image } from '../components/image';
@@ -1,5 +1,6 @@
1
1
  'use client';
2
2
 
3
+ import type { JSX } from 'react';
3
4
  import type { TableBlockData } from '../types/block';
4
5
 
5
6
  /**
@@ -1,5 +1,6 @@
1
1
  'use client';
2
2
 
3
+ import type { JSX } from 'react';
3
4
  import type { VideoBlockData } from '../types/block';
4
5
  import { resolveVideoEmbedUrl } from '../utils/video-embed';
5
6
 
package/theme-context.tsx CHANGED
@@ -1,5 +1,6 @@
1
1
  'use client';
2
2
 
3
+ import type { JSX } from 'react';
3
4
  import { createContext, useContext, type ReactNode } from 'react';
4
5
  import type { ThemeModule } from './types/theme';
5
6
 
@@ -0,0 +1,88 @@
1
+ 'use client';
2
+
3
+ import { Component, createElement, type ErrorInfo, type JSX, type ReactNode } from 'react';
4
+ import type { FooterProps, HeaderProps, ThemeModule } from './types/theme';
5
+ import { ThemeProvider } from './theme-context';
6
+
7
+ /**
8
+ * Renders a theme. THE MINIMUM A THEME NEEDS TO BE SEEN.
9
+ *
10
+ * Without this, installing `@usequeek/theme-kit` gave a developer every hook,
11
+ * store, type and shared block — and no way to actually mount what they built.
12
+ * The component that does it, `VendorShell`, lives in the storefront app, so it
13
+ * was never part of what shipped. A theme could be written and typechecked from
14
+ * outside this repo but never rendered.
15
+ *
16
+ * DELIBERATELY SMALLER THAN VendorShell. That composes the same three pieces and
17
+ * then layers on the Queek storefront's own chrome — checkout header, cart and
18
+ * auth modals, toasts, WhatsApp chat, analytics, the merchant edit-mode bridge.
19
+ * None of that is a theme's concern, and pulling it in would drag the whole app
20
+ * into the package. What is left is the actual contract: a Layout wrapping a
21
+ * header, the page, and a footer.
22
+ *
23
+ * Themes render inside a boundary because a theme's header and footer are
24
+ * SIBLINGS of the page, so a crash in either escapes any boundary the page sets
25
+ * up and blanks the whole screen. A theme developer should see which part broke,
26
+ * not an empty document.
27
+ */
28
+
29
+ class ThemeBoundary extends Component<
30
+ { part: string; children: ReactNode },
31
+ { error: Error | null }
32
+ > {
33
+ state: { error: Error | null } = { error: null };
34
+
35
+ static getDerivedStateFromError(error: Error): { error: Error } {
36
+ return { error };
37
+ }
38
+
39
+ componentDidCatch(error: Error, info: ErrorInfo): void {
40
+ console.error(`[theme-kit] the theme's ${this.props.part} threw`, error, info.componentStack);
41
+ }
42
+
43
+ render(): ReactNode {
44
+ if (!this.state.error) return this.props.children;
45
+
46
+ // Visible on purpose. A silent fallback during theme development means
47
+ // staring at a missing header wondering whether it is CSS.
48
+ return (
49
+ <div role="alert" style={{ padding: '1rem', border: '2px solid #e0492f', background: '#fff5f3', color: '#211711', font: '14px/1.5 ui-monospace, monospace' }}>
50
+ <strong>Theme {this.props.part} crashed.</strong>
51
+ <pre style={{ margin: '0.5rem 0 0', whiteSpace: 'pre-wrap' }}>{this.state.error.message}</pre>
52
+ </div>
53
+ );
54
+ }
55
+ }
56
+
57
+ export interface ThemeMountProps {
58
+ theme: ThemeModule;
59
+ headerProps: HeaderProps;
60
+ footerProps: FooterProps;
61
+ /** Variant ids from the store's config; omit to render the theme's default. */
62
+ headerVariant?: string;
63
+ footerVariant?: string;
64
+ children: ReactNode;
65
+ }
66
+
67
+ export function ThemeMount({
68
+ theme,
69
+ headerProps,
70
+ footerProps,
71
+ headerVariant,
72
+ footerVariant,
73
+ children,
74
+ }: ThemeMountProps): JSX.Element {
75
+ return (
76
+ <ThemeProvider theme={theme}>
77
+ <theme.Layout>
78
+ <ThemeBoundary part="header">
79
+ {createElement(theme.getHeader(headerVariant), headerProps)}
80
+ </ThemeBoundary>
81
+ <main className="storefront-main">{children}</main>
82
+ <ThemeBoundary part="footer">
83
+ {createElement(theme.getFooter(footerVariant), footerProps)}
84
+ </ThemeBoundary>
85
+ </theme.Layout>
86
+ </ThemeProvider>
87
+ );
88
+ }
@@ -1,3 +1,4 @@
1
+ import type { JSX } from 'react';
1
2
  /**
2
3
  * Shared icon-keyword vocabulary for "benefit row" content blocks (glow's
3
4
  * `trust-badges`, any theme's `features-columns`, and similar icon+title+text