@usequeek/theme-kit 0.1.23 → 0.1.25

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 (100) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/LICENSE +1 -1
  3. package/README.md +164 -21
  4. package/api/collections.ts +14 -1
  5. package/api/orders.ts +15 -1
  6. package/apps/follow-card.tsx +5 -0
  7. package/apps/subscribe-form.tsx +2 -2
  8. package/components/auth/auth-flow.tsx +12 -0
  9. package/components/cart/cart-panel-controller.tsx +13 -0
  10. package/components/checkout/default-checkout-shell.tsx +5 -0
  11. package/components/checkout/policy-links.tsx +4 -0
  12. package/components/image.tsx +10 -0
  13. package/components/language-switcher.tsx +1 -0
  14. package/components/powered-by-queek.tsx +4 -3
  15. package/components/product-media-frame.tsx +5 -0
  16. package/components/product-metafields.tsx +9 -0
  17. package/components/variant-picker.css +4 -7
  18. package/edit-mode.tsx +4 -3
  19. package/hooks/use-address-autocomplete.ts +9 -0
  20. package/hooks/use-auth-flow.tsx +8 -13
  21. package/hooks/use-auth.ts +10 -0
  22. package/hooks/use-cart.ts +12 -0
  23. package/hooks/use-categories.ts +16 -2
  24. package/hooks/use-href.ts +9 -4
  25. package/hooks/use-posts.ts +16 -8
  26. package/hooks/use-product-detail.ts +25 -11
  27. package/hooks/use-product-questions.ts +16 -3
  28. package/hooks/use-products.ts +44 -22
  29. package/hooks/use-related-products.ts +17 -5
  30. package/hooks/use-resolve-coordinates.ts +9 -0
  31. package/hooks/use-reviews.ts +16 -2
  32. package/hooks/use-search.ts +10 -0
  33. package/hooks/use-shop-cart.ts +8 -0
  34. package/hooks/use-shop.ts +19 -6
  35. package/hooks/use-variant-selection.ts +23 -19
  36. package/index.ts +10 -11
  37. package/locales/en.default.json +4 -0
  38. package/navigation.tsx +2 -0
  39. package/package.json +29 -8
  40. package/page-renderer.tsx +11 -0
  41. package/provider.tsx +29 -3
  42. package/sdk/client.ts +183 -36
  43. package/sdk/error-handler.ts +18 -10
  44. package/sdk/queek-client.ts +12 -2
  45. package/sdk/server-store-client.ts +2 -2
  46. package/shared-blocks/embed.tsx +7 -3
  47. package/shared-blocks/manifest.ts +1 -1
  48. package/shared-blocks/product-qa.tsx +1 -1
  49. package/stores/address-modal-store.ts +1 -0
  50. package/stores/auth-modal-store.ts +1 -0
  51. package/stores/cart-panel-store.ts +1 -0
  52. package/stores/cart-store.ts +12 -7
  53. package/stores/checkout-store.ts +4 -4
  54. package/stores/location-store.ts +5 -0
  55. package/stores/orders-sheet-store.ts +1 -0
  56. package/stores/product-modal-store.ts +1 -0
  57. package/stores/search-modal-store.ts +1 -0
  58. package/stores/user-store.ts +1 -1
  59. package/strings/pseudo-locale.ts +1 -1
  60. package/theme-context.tsx +11 -1
  61. package/theme-mount.tsx +14 -0
  62. package/utils/format.ts +10 -1
  63. package/utils/menu-link.ts +8 -0
  64. package/utils/to-error.ts +4 -0
  65. package/utils/vendor-host.ts +1 -2
  66. package/scripts/verify-package.ts +0 -102
  67. package/tests/apps.test.tsx +0 -33
  68. package/tests/auth-email-errors.test.tsx +0 -55
  69. package/tests/currency-contract.test.ts +0 -85
  70. package/tests/default-metaobject-page.test.tsx +0 -56
  71. package/tests/display-format.test.ts +0 -403
  72. package/tests/fixtures/external-theme.tsx +0 -90
  73. package/tests/image-loading.test.tsx +0 -31
  74. package/tests/language-menu.test.tsx +0 -598
  75. package/tests/language-switcher.test.tsx +0 -350
  76. package/tests/list-pagination.test.tsx +0 -71
  77. package/tests/locale-switch.test.ts +0 -197
  78. package/tests/locale-transport.test.tsx +0 -321
  79. package/tests/manifest-parity.test.ts +0 -54
  80. package/tests/markdown-render.test.ts +0 -46
  81. package/tests/menu-placement.test.ts +0 -63
  82. package/tests/metaobjects-block.test.tsx +0 -70
  83. package/tests/navigation.test.tsx +0 -30
  84. package/tests/orders-walk.test.ts +0 -48
  85. package/tests/page-renderer-signature.test.tsx +0 -42
  86. package/tests/powered-by-queek.test.tsx +0 -65
  87. package/tests/powered-by-url.test.ts +0 -49
  88. package/tests/product-images.test.ts +0 -242
  89. package/tests/product-metafields.test.tsx +0 -154
  90. package/tests/public-surface.test.ts +0 -35
  91. package/tests/shop-query.test.ts +0 -64
  92. package/tests/shopper-chrome-strings.test.tsx +0 -271
  93. package/tests/storefront-client-scope.test.ts +0 -43
  94. package/tests/theme-hook-placement.test.tsx +0 -179
  95. package/tests/theme-strings-client.test.tsx +0 -310
  96. package/tests/theme-strings-pseudo.test.tsx +0 -171
  97. package/tests/theme-strings-typed.test.tsx +0 -97
  98. package/tests/theme-strings.test.tsx +0 -401
  99. package/tests/theme-typed-keys.fixture.ts +0 -76
  100. package/tests/use-shop-prefetch.test.tsx +0 -87
@@ -4,6 +4,7 @@ import { useEffect, useState } from 'react';
4
4
  import { useStorefront, useStorefrontLocale } from '../provider';
5
5
  import { createBrowserClient } from '../sdk/client';
6
6
  import { withLocaleQuery } from '../utils/locale';
7
+ import { toError } from '../utils/to-error';
7
8
  import type { ReviewItem, ReviewsBlockData, ReviewsSummary } from '../types/block';
8
9
 
9
10
  interface ReviewsResponse {
@@ -62,10 +63,20 @@ function summarize(reviews: ReviewItem[]): ReviewsSummary {
62
63
  };
63
64
  }
64
65
 
66
+ /**
67
+ * Customer reviews for a `reviews` block: a list plus the rating summary
68
+ * (average and 1 to 5 breakdown). Scope, filters and sorting come from the
69
+ * block's own data. On failure the list is empty and `error` is set; the hook
70
+ * never throws.
71
+ *
72
+ * @example
73
+ * const { reviews, summary, isLoading } = useReviews({ scope: 'product', product_slug: 'oak-chair', limit: 6 });
74
+ */
65
75
  export function useReviews(options: ReviewsBlockData): {
66
76
  reviews: ReviewItem[];
67
77
  summary: ReviewsSummary;
68
78
  isLoading: boolean;
79
+ error: Error | null;
69
80
  } {
70
81
  const { vendor, previewData } = useStorefront();
71
82
  const locale = useStorefrontLocale();
@@ -73,6 +84,7 @@ export function useReviews(options: ReviewsBlockData): {
73
84
  const [reviews, setReviews] = useState<ReviewItem[]>(seeded);
74
85
  const [summary, setSummary] = useState<ReviewsSummary>(options._preview?.summary ?? summarize(seeded));
75
86
  const [isLoading, setIsLoading] = useState(!options._preview && !previewData);
87
+ const [error, setError] = useState<Error | null>(null);
76
88
 
77
89
  const {
78
90
  scope,
@@ -114,6 +126,7 @@ export function useReviews(options: ReviewsBlockData): {
114
126
 
115
127
  async function run(): Promise<void> {
116
128
  setIsLoading(true);
129
+ setError(null);
117
130
  try {
118
131
  let path = '/reviews';
119
132
  if (ids && ids.length > 0) {
@@ -136,10 +149,11 @@ export function useReviews(options: ReviewsBlockData): {
136
149
  setReviews(response.data.reviews ?? []);
137
150
  setSummary(response.data.summary ?? EMPTY_SUMMARY);
138
151
  }
139
- } catch {
152
+ } catch (caught) {
140
153
  if (!cancelled) {
141
154
  setReviews([]);
142
155
  setSummary(EMPTY_SUMMARY);
156
+ setError(toError(caught));
143
157
  }
144
158
  } finally {
145
159
  if (!cancelled) setIsLoading(false);
@@ -166,5 +180,5 @@ export function useReviews(options: ReviewsBlockData): {
166
180
  locale,
167
181
  ]);
168
182
 
169
- return { reviews, summary, isLoading };
183
+ return { reviews, summary, isLoading, error };
170
184
  }
@@ -4,12 +4,22 @@ import { useMemo } from 'react';
4
4
  import type { Product } from '../types/product';
5
5
  import type { Collection } from '../types/page';
6
6
 
7
+ /** What {@link useSearch} returns: the matches to show in a search panel. */
7
8
  export interface SearchResults {
8
9
  products: Product[];
9
10
  categories: Array<[slug: string, name: string]>;
10
11
  collections: Collection[];
11
12
  }
12
13
 
14
+ /**
15
+ * Instant search over products and collections you already hold (no request).
16
+ * An empty query returns the first products and categories as suggestions.
17
+ * Matches are case-insensitive on product title, category name and collection
18
+ * name, capped at 5 products, 4 categories and 2 collections.
19
+ *
20
+ * @example
21
+ * const { products, categories, collections } = useSearch(query, allProducts, allCollections);
22
+ */
13
23
  export function useSearch(
14
24
  query: string,
15
25
  products: Product[],
@@ -3,6 +3,14 @@
3
3
  import { useMemo } from 'react';
4
4
  import { useCartStore } from '../stores/cart-store';
5
5
 
6
+ /**
7
+ * The cart lines for one shop, with the unit count and the total (in the
8
+ * currency's major unit, before delivery and discounts). Pass the shop id from
9
+ * `product.shop_id`; a missing id gives an empty cart.
10
+ *
11
+ * @example
12
+ * const { items, count, total } = useShopCart(product.shop_id);
13
+ */
6
14
  export function useShopCart(shopId: string | null | undefined): {
7
15
  items: ReturnType<typeof useCartStore.getState>['items'];
8
16
  count: number;
package/hooks/use-shop.ts CHANGED
@@ -5,6 +5,7 @@ import { useStorefront, useStorefrontLocale } from '../provider';
5
5
  import type { Product, ApiResponse } from '../types/product';
6
6
  import { createBrowserClient } from '../sdk/client';
7
7
  import { localeCacheKeySegment, withLocaleQuery } from '../utils/locale';
8
+ import { toError } from '../utils/to-error';
8
9
  import {
9
10
  SHOP_DEFAULT_PER_PAGE,
10
11
  normalizeShopQuery,
@@ -78,11 +79,18 @@ function filterProducts(
78
79
  }
79
80
 
80
81
  /**
81
- * Shop catalogue data source. Hits the live products endpoint in production.
82
- * In preview (when the provider carries `previewData.products`) it filters,
83
- * sorts and paginates the demo catalogue in-memory so themes render real cards.
82
+ * The shop page's catalogue: one page of products for a category, keyword,
83
+ * sort and page number. Reads the live products endpoint in production. In
84
+ * preview (when the provider carries `previewData.products`) it filters, sorts
85
+ * and paginates the demo catalogue in memory, so themes render real cards.
84
86
  *
85
- * Page is controlled by the caller — pair with `useShopParams` for URL sync.
87
+ * The page is controlled by the caller; pair this with `useShopParams` to keep
88
+ * it in the URL. On failure the previous page of products stays on screen and
89
+ * `error` is set; the hook never throws.
90
+ *
91
+ * @example
92
+ * const { categorySlug, keyword, sort, page } = useShopParams();
93
+ * const { products, isLoading, pagination, error } = useShop({ categorySlug, keyword, sort, page });
86
94
  */
87
95
  export function useShop(options?: {
88
96
  categorySlug?: string | null;
@@ -94,6 +102,7 @@ export function useShop(options?: {
94
102
  products: Product[];
95
103
  isLoading: boolean;
96
104
  pagination: ShopPagination;
105
+ error: Error | null;
97
106
  } {
98
107
  const { vendor, previewData } = useStorefront();
99
108
  const locale = useStorefrontLocale();
@@ -123,6 +132,7 @@ export function useShop(options?: {
123
132
 
124
133
  const [products, setProducts] = useState<Product[]>(seed?.products ?? []);
125
134
  const [isLoading, setIsLoading] = useState(!isPreview && !seed);
135
+ const [error, setError] = useState<Error | null>(null);
126
136
  const [pagination, setPagination] = useState<ShopPagination>(seed?.pagination ?? { ...EMPTY_PAGINATION, perPage });
127
137
  // The key whose results are already in state. Seeded with the prefetch so
128
138
  // the first effect run does not refetch the page the server just rendered
@@ -166,6 +176,7 @@ export function useShop(options?: {
166
176
  // a newer one (filters B) had reset it, overwriting B's products.
167
177
  let cancelled = false;
168
178
  setIsLoading(true);
179
+ setError(null);
169
180
 
170
181
  const client = createBrowserClient(vendor.slug ?? undefined);
171
182
  const current = JSON.parse(queryKey) as typeof query;
@@ -182,6 +193,8 @@ export function useShop(options?: {
182
193
  setPagination(parsed.pagination);
183
194
  setProducts(parsed.products);
184
195
  loadedQueryRef.current = fetchKey;
196
+ } catch (caught) {
197
+ if (!cancelled) setError(toError(caught));
185
198
  } finally {
186
199
  if (!cancelled) setIsLoading(false);
187
200
  }
@@ -195,8 +208,8 @@ export function useShop(options?: {
195
208
  }, [isPreview, vendor.slug, queryKey, fetchKey, locale]);
196
209
 
197
210
  if (isPreview && previewResult) {
198
- return { products: previewResult.products, isLoading: false, pagination: previewResult.pagination };
211
+ return { products: previewResult.products, isLoading: false, pagination: previewResult.pagination, error: null };
199
212
  }
200
213
 
201
- return { products, isLoading, pagination };
214
+ return { products, isLoading, pagination, error };
202
215
  }
@@ -25,25 +25,6 @@ export interface VariantSelection {
25
25
  activeHasDiscount: boolean;
26
26
  }
27
27
 
28
- /**
29
- * The option-selection state machine every theme's product page used to
30
- * hand-roll — six near-identical copies, all of which gated their add button
31
- * on stock and addons but never on a resolved variant.
32
- *
33
- * Two things it fixes that the copies got wrong:
34
- *
35
- * 1. It seeds from a real variant (first in-stock, else first) rather than
36
- * from the first value of each option independently. On a sparse matrix —
37
- * Red/S is not stocked but Red/M and Blue/S are — first-value-of-each
38
- * produces a combination that matches nothing, so the page opened with an
39
- * unresolvable selection.
40
- * 2. It exposes `variantSatisfied`, so a selection that resolves to nothing
41
- * (mid-change, or that sparse seed) disables the add instead of sending a
42
- * line the backend rejects.
43
- *
44
- * Themes keep their own option markup and call `selectOption` — core owns the
45
- * state, the theme owns the design.
46
- */
47
28
  /**
48
29
  * The opening selection. Seeded from a variant that actually exists, so the
49
30
  * initial state is always resolvable — preferring one in stock, since opening
@@ -85,6 +66,29 @@ export function resolveVariantState(
85
66
  };
86
67
  }
87
68
 
69
+ /**
70
+ * The option-selection state machine every theme's product page used to
71
+ * hand-roll, and which gated the add button on stock and add-ons but never on a
72
+ * resolved variant.
73
+ *
74
+ * It does two things a hand-rolled version tends to get wrong:
75
+ *
76
+ * 1. It seeds from a real variant (first in-stock, else first) rather than
77
+ * from the first value of each option independently. On a sparse matrix —
78
+ * Red/S is not stocked but Red/M and Blue/S are — first-value-of-each
79
+ * produces a combination that matches nothing, so the page opened with an
80
+ * unresolvable selection.
81
+ * 2. It exposes `variantSatisfied`, so a selection that resolves to nothing
82
+ * (mid-change, or that sparse seed) disables the add instead of sending a
83
+ * line the backend rejects.
84
+ *
85
+ * Themes keep their own option markup and call `selectOption`: the kit owns the
86
+ * state, the theme owns the design.
87
+ *
88
+ * @example
89
+ * const { selectedOptions, selectOption, variantSatisfied, activePrice } = useVariantSelection(product);
90
+ * <button disabled={!variantSatisfied || !activeInStock}>Add to cart</button>
91
+ */
88
92
  export function useVariantSelection(product: Product): VariantSelection {
89
93
  const [selectedOptions, setSelectedOptions] = useState<Record<string, string>>(() => seedVariantOptions(product));
90
94
 
package/index.ts CHANGED
@@ -11,19 +11,18 @@
11
11
  * Two entries because the split is real, not cosmetic: this one is types and
12
12
  * pure functions, importable from anywhere including node tooling; `client.ts`
13
13
  * is React state and components and needs a client boundary at the consumer.
14
- * Core also holds one `next/headers` caller, which belongs in neither and is
15
- * deliberately unreachable from both.
14
+ * Modules that call `next/headers` (`sdk/server-store-client.ts` and the
15
+ * `api/collections.ts` and `api/shop.ts` fetchers built on it) are server-only:
16
+ * import them from server components, never from a client module.
16
17
  *
17
18
  * THESE ARE A DECLARATION, NOT AN IMPORT PATH. Do not migrate themes onto them.
18
- * Pointing a theme's files at `client.ts` fails the build outright: tooling that
19
- * imports theme components under tsx (a theme registry generator, for example)
20
- * would reach a gallery block through the barrel, which transitively pulls the
21
- * SDK, and the SDK does not resolve outside a bundler. It is the same reason a barrel would bloat every
22
- * theme chunk: the stores call `create()` at import time and package.json
23
- * declares no `sideEffects`, so webpack must treat every re-export as live and
24
- * cannot drop the unused ones. Themes keep importing the exact module they use;
25
- * these barrels stay the machine-checkable statement of what is in the public
26
- * surface.
19
+ * Pointing a theme's files at `client.ts` can fail the build outright: tooling
20
+ * that imports theme components under `tsx` (a theme registry generator, for
21
+ * example) would reach a gallery block through the barrel, which transitively
22
+ * pulls the SDK, and the SDK does not resolve outside a bundler. A barrel also
23
+ * makes every client module reachable from one entry, which bloats each theme
24
+ * chunk. Themes import the exact module they use; these barrels stay the
25
+ * machine-checkable statement of what is in the public surface.
27
26
  */
28
27
 
29
28
  export * from './types/theme';
@@ -176,5 +176,9 @@
176
176
  "language": {
177
177
  "label": "Language",
178
178
  "named": "Language: {name}"
179
+ },
180
+ "embed": {
181
+ "placeholder": "Paste the link or embed code of the content you want to show",
182
+ "title": "Embedded content"
179
183
  }
180
184
  }
package/navigation.tsx CHANGED
@@ -15,6 +15,7 @@ import NextLink from 'next/link';
15
15
  import { usePathname as useFrameworkPathname, useRouter as useFrameworkRouter } from 'next/navigation';
16
16
  import { useMemo, type AnchorHTMLAttributes, type ReactNode, type Ref } from 'react';
17
17
 
18
+ /** Props for {@link Link}: any anchor attribute except `href`, plus `prefetch` and `replace`. */
18
19
  export interface LinkProps extends Omit<AnchorHTMLAttributes<HTMLAnchorElement>, 'href'> {
19
20
  /** A path inside the store (`menuItemToHref()`, `useHref()`) or a full URL. */
20
21
  href: string;
@@ -31,6 +32,7 @@ export function Link({ href, prefetch, replace, ...anchor }: LinkProps) {
31
32
  return <NextLink href={href} prefetch={prefetch} replace={replace} {...anchor} />;
32
33
  }
33
34
 
35
+ /** What {@link useRouter} returns. */
34
36
  export interface Router {
35
37
  /** Go to `href`, adding a history entry. */
36
38
  push(href: string): void;
package/package.json CHANGED
@@ -1,9 +1,24 @@
1
1
  {
2
2
  "name": "@usequeek/theme-kit",
3
- "version": "0.1.23",
3
+ "version": "0.1.25",
4
4
  "private": false,
5
5
  "license": "MIT",
6
- "description": "The framework a Queek storefront theme is built against.",
6
+ "description": "The runtime a Queek storefront theme is built on: hooks, stores, page renderer, navigation, shared blocks and locale strings.",
7
+ "keywords": [
8
+ "queek",
9
+ "storefront",
10
+ "theme",
11
+ "themes",
12
+ "react",
13
+ "nextjs",
14
+ "commerce",
15
+ "ecommerce"
16
+ ],
17
+ "homepage": "https://docs.usequeek.com/docs/themes",
18
+ "bugs": {
19
+ "url": "https://github.com/usequeek/theme-kit/issues"
20
+ },
21
+ "author": "Queekx Technology LTD",
7
22
  "repository": {
8
23
  "type": "git",
9
24
  "url": "git+https://github.com/usequeek/theme-kit.git"
@@ -25,25 +40,31 @@
25
40
  "**/*.css",
26
41
  "locales/*.json",
27
42
  "README.md",
28
- "LICENSE"
43
+ "LICENSE",
44
+ "CHANGELOG.md",
45
+ "!tests",
46
+ "!scripts"
29
47
  ],
30
48
  "peerDependencies": {
49
+ "@usequeek/client-sdk": "^0.4.0",
31
50
  "next": ">=15",
32
51
  "react": ">=19",
33
- "react-dom": ">=19",
34
- "@queekai/client-sdk": "*"
52
+ "react-dom": ">=19"
53
+ },
54
+ "engines": {
55
+ "node": ">=22"
35
56
  },
36
57
  "dependencies": {
37
- "zustand": "^5.0.11",
58
+ "html-to-image": "^1.11.0",
38
59
  "react-markdown": "^10.0.0",
39
60
  "remark-gfm": "^4.0.0",
40
- "html-to-image": "^1.11.0"
61
+ "zustand": "^5.0.11"
41
62
  },
42
63
  "devDependencies": {
43
- "@queekai/client-sdk": "^0.3.0",
44
64
  "@types/node": "^20",
45
65
  "@types/react": "^19",
46
66
  "@types/react-dom": "^19",
67
+ "@usequeek/client-sdk": "^0.4.0",
47
68
  "jsdom": "^28.1.0",
48
69
  "next": "^16.0.0",
49
70
  "react": "^19.2.3",
package/page-renderer.tsx CHANGED
@@ -65,6 +65,17 @@ function signatureReplacer(_key: string, value: unknown): unknown {
65
65
  return value;
66
66
  }
67
67
 
68
+ /**
69
+ * Renders a page's blocks in order, choosing each block's theme variant (or
70
+ * the kit's shared block). Block data arrives already resolved: a theme never
71
+ * fetches it. While a merchant edits, a draft section that cannot render shows
72
+ * a short "can't be previewed yet" notice instead of blanking the page.
73
+ *
74
+ * @example
75
+ * export default function HomePage({ blocks }: HomePageProps) {
76
+ * return <PageRenderer blocks={blocks} />;
77
+ * }
78
+ */
68
79
  export function PageRenderer({ blocks }: { blocks: Block[] }): JSX.Element {
69
80
  const draftBlocks = useEditPreviewStore((state) => state.blocks);
70
81
  const reconcileDraftBlocks = useEditPreviewStore((state) => state.reconcile);
package/provider.tsx CHANGED
@@ -61,8 +61,23 @@ interface StorefrontContextValue {
61
61
  locales?: StoreLocale[];
62
62
  }
63
63
 
64
+ /** The context behind {@link StorefrontProvider}. Read it through the hooks; use it directly only to test for a provider without throwing. */
64
65
  export const StorefrontContext = createContext<StorefrontContextValue | null>(null);
65
66
 
67
+ /**
68
+ * Gives every kit hook and component the store they render for: its profile,
69
+ * configuration, menus, locale, languages and strings. The Queek storefront
70
+ * mounts it for every theme, so a theme never renders it. Render it yourself
71
+ * only in a test, a preview or a script.
72
+ *
73
+ * `basePath` defaults to `/<vendor.slug>`; pass `''` for a store served on its
74
+ * own domain. Brand design tokens become CSS variables on a wrapping element.
75
+ *
76
+ * @example
77
+ * <StorefrontProvider vendor={vendor} config={config} menus={[]}>
78
+ * <MySection />
79
+ * </StorefrontProvider>
80
+ */
66
81
  export function StorefrontProvider({
67
82
  vendor,
68
83
  config,
@@ -147,11 +162,22 @@ export function StorefrontProvider({
147
162
  );
148
163
  }
149
164
 
165
+ /**
166
+ * The current store: `vendor`, `config`, `menus`, `basePath`, `locale` and the
167
+ * rest of what the host passed to {@link StorefrontProvider}.
168
+ *
169
+ * @throws {Error} when called outside a `StorefrontProvider`.
170
+ *
171
+ * @example
172
+ * const { vendor, config } = useStorefront();
173
+ */
150
174
  export function useStorefront(): StorefrontContextValue {
151
175
  const context = useContext(StorefrontContext);
152
176
 
153
177
  if (!context) {
154
- throw new Error('useStorefront must be used within StorefrontProvider');
178
+ throw new Error(
179
+ 'useStorefront() was called outside <StorefrontProvider>. Render the component inside StorefrontProvider (the Queek storefront does this for every theme). In a test, wrap it yourself.',
180
+ );
155
181
  }
156
182
 
157
183
  return context;
@@ -167,7 +193,7 @@ export function useStorefrontLocale(): string | null {
167
193
  const context = useContext(StorefrontContext);
168
194
 
169
195
  if (!context) {
170
- throw new Error('useStorefrontLocale must be used within StorefrontProvider');
196
+ throw new Error('useStorefrontLocale() was called outside <StorefrontProvider>. Render the component inside StorefrontProvider.');
171
197
  }
172
198
 
173
199
  return context.locale ?? null;
@@ -188,7 +214,7 @@ export function useThemeStrings(): ThemeStringsFn {
188
214
  const context = useContext(StorefrontContext);
189
215
 
190
216
  if (!context) {
191
- throw new Error('useThemeStrings must be used within StorefrontProvider');
217
+ throw new Error('useThemeStrings() was called outside <StorefrontProvider>. Render the component inside StorefrontProvider.');
192
218
  }
193
219
 
194
220
  const theme = useOptionalTheme();