@usequeek/theme-kit 0.1.23 → 0.1.24

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 +37 -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 +7 -12
  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 +25 -4
  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 +17 -9
  44. package/sdk/queek-client.ts +11 -1
  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
package/CHANGELOG.md ADDED
@@ -0,0 +1,37 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@usequeek/theme-kit`. See the README for the versioning
4
+ policy. Versions before this file started are in the git tags.
5
+
6
+ ## 0.1.24 - 2026-10-12
7
+
8
+ ### Added
9
+
10
+ - `ApiRequestError` and `ApiTimeoutError` carry a stable `code` (`unauthenticated`,
11
+ `forbidden`, `not_found`, `rate_limited`, `client_error`, `server_error`,
12
+ `timeout`) and a `retryable` flag. `ApiRequestError` also carries `requestId`,
13
+ `retryAfterSeconds` and `detail`. Their messages say what happened and what to do
14
+ next, and never include the query string.
15
+ - The fetching hooks (`useProducts`, `useProductBySlug`, `useShop`, `usePosts`,
16
+ `useReviews`, `useProductQuestions`, `useCategories`, `useRelatedProducts`,
17
+ `useProductDetail`) return an `error` field (`Error | null`).
18
+ - One warning, in production builds only, when `QUEEK_API_URL` is not set.
19
+ - JSDoc with examples on the hooks, providers, clients, stores and the main
20
+ components.
21
+
22
+ ### Fixed
23
+
24
+ - `useProducts` and `useShop` no longer leave an unhandled promise rejection when
25
+ the request fails.
26
+ - Slugs and ids are URL-encoded when they are placed in a request path
27
+ (products, collections, related products, questions, orders).
28
+ - Google sign-in no longer logs the sign-in URL or the response body.
29
+ - Hook errors raised outside a provider name the missing provider and the fix.
30
+
31
+ ### Packaging
32
+
33
+ - The published package no longer contains `tests/` or `scripts/`.
34
+ - Peer dependency `@queekai/client-sdk` is `^0.3.1 || ^0.4.0` (was `*`); `engines.node` is
35
+ `>=22`; `bugs`, `homepage`, `author` and `keywords` are set; releases publish
36
+ with npm provenance.
37
+ - The licence names the legal entity, Queekx Technology LTD.
package/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2026 Queek
3
+ Copyright (c) 2026 Queekx Technology LTD
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -1,9 +1,21 @@
1
1
  # @usequeek/theme-kit
2
2
 
3
- The framework a Queek storefront theme is built against: hooks, stores, headless
4
- flows (auth, cart, checkout), types and the block renderer. See the
5
- [theme documentation](https://docs.usequeek.com/docs/themes) for how themes
6
- work end to end.
3
+ The runtime a Queek storefront theme is built on: hooks for products, cart, auth
4
+ and search, state stores, the page and block renderer, navigation, shared blocks,
5
+ and locale strings. A theme is React components and CSS; this kit supplies
6
+ everything that talks to Queek. See the
7
+ [theme documentation](https://docs.usequeek.com/docs/themes) for how themes work
8
+ end to end.
9
+
10
+ ## Requirements
11
+
12
+ - **Next.js 15 or later (App Router)**, `react` and `react-dom` 19 or later. The
13
+ kit imports `next` itself (links, navigation, server request headers), so it
14
+ runs inside a Next.js app, not in a bare React app.
15
+ - `@queekai/client-sdk` `^0.3.1 || ^0.4.0`, the Queek customer SDK the auth and checkout
16
+ hooks call.
17
+ - Node.js 22 or later (the version CI tests on).
18
+ - TypeScript with `moduleResolution: bundler` and `jsx: react-jsx`.
7
19
 
8
20
  ## Install
9
21
 
@@ -11,9 +23,6 @@ work end to end.
11
23
  npm install @usequeek/theme-kit
12
24
  ```
13
25
 
14
- Peer dependencies: `next` (>=15), `react` and `react-dom` (>=19) and
15
- `@queekai/client-sdk`.
16
-
17
26
  The package ships TypeScript source, not a build. Most modules carry a
18
27
  `'use client'` directive, and that directive is the whole contract with the
19
28
  React Server Components boundary; bundlers routinely strip or hoist it when a
@@ -26,10 +35,57 @@ bundler that enforces them:
26
35
  export default { transpilePackages: ['@usequeek/theme-kit'] };
27
36
  ```
28
37
 
29
- Compiling a theme against it needs `moduleResolution: bundler` and
30
- `jsx: react-jsx`. A vitest setup that renders kit components needs the package
31
- inlined (`server.deps.inline: ['@usequeek/theme-kit']`) so the shipped `.tsx`
32
- source is transformed like first-party code.
38
+ A vitest setup that renders kit components needs the package inlined
39
+ (`server.deps.inline: ['@usequeek/theme-kit']`) so the shipped `.tsx` source is
40
+ transformed like first-party code.
41
+
42
+ ## Quickstart
43
+
44
+ A product list that loads, handles a failed request, links to each product and
45
+ adds to the cart:
46
+
47
+ ```tsx
48
+ 'use client';
49
+
50
+ import { useCart } from '@usequeek/theme-kit/hooks/use-cart';
51
+ import { useHref } from '@usequeek/theme-kit/hooks/use-href';
52
+ import { useProducts } from '@usequeek/theme-kit/hooks/use-products';
53
+ import { Link } from '@usequeek/theme-kit/navigation';
54
+ import { formatMoney } from '@usequeek/theme-kit/utils/format';
55
+
56
+ export function PopularProducts() {
57
+ const { products, isLoading, error } = useProducts({ limit: 4, sort: 'popular' });
58
+ const { addProduct } = useCart();
59
+ const href = useHref();
60
+
61
+ if (isLoading) return <p>Loading…</p>;
62
+ if (error) return <p>We could not load products. Please try again.</p>;
63
+
64
+ return (
65
+ <ul>
66
+ {products.map((product) => (
67
+ <li key={product.id}>
68
+ <Link href={href(`/products/${product.slug}`)}>{product.title}</Link>
69
+ <span>{formatMoney(product.pricing.sale_amount, product.currency)}</span>
70
+ <button type="button" onClick={() => addProduct(product)}>
71
+ Add to cart
72
+ </button>
73
+ </li>
74
+ ))}
75
+ </ul>
76
+ );
77
+ }
78
+ ```
79
+
80
+ The storefront mounts `StorefrontProvider` for every theme, so a theme never
81
+ renders it. Hooks called outside it throw an error that says so. In a test or a
82
+ script, wrap the component yourself:
83
+
84
+ ```tsx
85
+ <StorefrontProvider vendor={vendor} config={config} menus={[]}>
86
+ <PopularProducts />
87
+ </StorefrontProvider>
88
+ ```
33
89
 
34
90
  ## Use
35
91
 
@@ -53,6 +109,71 @@ Theme CSS ships with the package: `@usequeek/theme-kit/shared-blocks/core-blocks
53
109
  (framework-owned blocks), plus per-component CSS next to its component
54
110
  (`components/variant-picker.css`, `apps/apps.css`).
55
111
 
112
+ ### Hooks
113
+
114
+ The hooks that fetch (products, shop, categories, posts, reviews, questions,
115
+ related products, product detail) report failure through an `error` field (an
116
+ `Error`, or `null`). They never throw and never leave an unhandled rejection, and
117
+ they keep the data already on screen. They return `isLoading`, except
118
+ `useProductDetail`, which returns `loadingDetail`. In preview, hooks read
119
+ `previewData` from the provider and make no request.
120
+
121
+ | Hook | Module | Returns |
122
+ | --- | --- | --- |
123
+ | `useProducts(options?)` | `hooks/use-products` | A product list: `{ products, isLoading, error }` |
124
+ | `useProductBySlug(slug)` | `hooks/use-products` | One product by slug: `{ product, isLoading, error }` |
125
+ | `useProductDetail(listProduct, isOpen, vendorSlug?, locale?)` | `hooks/use-product-detail` | A list product completed with variants and add-ons |
126
+ | `useRelatedProducts(slug, limit?)` | `hooks/use-related-products` | Related products |
127
+ | `useShop(options?)` + `useShopParams()` | `hooks/use-shop`, `hooks/use-shop-params` | The shop catalogue page and its URL state |
128
+ | `useCollectionParams()` | `hooks/use-collection-params` | Search, sort and page state for a collection page |
129
+ | `useCategories(options?)` | `hooks/use-categories` | Categories, flattened |
130
+ | `usePosts(options?)` | `hooks/use-posts` | Blog posts |
131
+ | `useReviews(block)` | `hooks/use-reviews` | Reviews and the rating summary |
132
+ | `useProductQuestions(block)` | `hooks/use-product-questions` | Product questions and answers |
133
+ | `useCart()` / `useShopCart(shopId)` | `hooks/use-cart`, `hooks/use-shop-cart` | The cart and its actions; one shop's lines and totals |
134
+ | `useAuth()` | `hooks/use-auth` | The signed-in customer and sign-in modal controls |
135
+ | `useVariantSelection(product)` | `hooks/use-variant-selection` | Option selection state and the add-to-cart gate |
136
+ | `useSearch(query, products, collections)` | `hooks/use-search` | Instant search over data you hold |
137
+ | `useAddressAutocomplete(text)` / `useResolveCoordinates()` | `hooks/use-address-autocomplete`, `hooks/use-resolve-coordinates` | Delivery address lookup |
138
+ | `useHref()` | `hooks/use-href` | Builds a store-relative link |
139
+ | `useLocales()` | `hooks/use-locales` | The store's languages and the active one |
140
+ | `useStorefront()` / `useStorefrontLocale()` / `useThemeStrings()` | `provider` | The store, the request locale, the bound `t` |
141
+
142
+ ## Errors
143
+
144
+ Requests go through one place (`sdk/client.ts`). A non-2xx answer throws
145
+ `ApiRequestError`, a request that outlives its budget throws `ApiTimeoutError`.
146
+ Branch on `code`, never on the message:
147
+
148
+ | Field | Meaning |
149
+ | --- | --- |
150
+ | `code` | `unauthenticated` (401), `forbidden` (403), `not_found` (404), `rate_limited` (429), `client_error` (other 4xx), `server_error` (5xx), `timeout` |
151
+ | `status`, `url` | The HTTP status and the full request URL (`ApiRequestError` only) |
152
+ | `retryable` | `true` for 429, 5xx and timeouts. The kit never retries a write on its own: a timed-out POST may already have reached the API. |
153
+ | `requestId` | The API's `x-request-id`, for a support ticket |
154
+ | `retryAfterSeconds` | The API's `Retry-After`, when it sent one |
155
+ | `detail` | The API's own error message, when its body carried one |
156
+ | `cause` | The original `DOMException` (`ApiTimeoutError` only) |
157
+
158
+ Messages never include the query string, so search terms stay out of logs.
159
+ Server-side fetchers that must render "not found" for a missing resource use
160
+ `swallowNotFound`, which returns `null` for a 404 and rethrows everything else,
161
+ so an API outage never shows as a missing page.
162
+
163
+ ## Configuration (storefront host)
164
+
165
+ Themes set none of these; the storefront host does.
166
+
167
+ | Variable | Used for |
168
+ | --- | --- |
169
+ | `QUEEK_API_URL` (or `NEXT_PUBLIC_QUEEK_API_URL`) | Origin of the Queek API for server-side reads. Without it the kit falls back to `http://127.0.0.1:8000/api/v1` and warns once in production builds. |
170
+ | `NEXT_PUBLIC_QUEEK_AUTH_URL` | Origin of the sign-in service (default `https://auth.usequeek.com`). |
171
+ | `STOREFRONT_DOMAIN` | Domain whose subdomains are store slugs (default `queek.com.ng`). |
172
+ | `CUSTOM_DOMAIN_VENDOR_MAP` | JSON object mapping a custom domain to a store slug. |
173
+
174
+ Browser requests are always same-origin (`/api/v1/client/*`), so the host needs
175
+ a rewrite from that path to the API. The browser never learns the API origin.
176
+
56
177
  ## Theme strings
57
178
 
58
179
  The kit's UI-string mechanism (`strings/theme-strings.ts`; pure, with no
@@ -292,7 +413,7 @@ export default function Footer() {
292
413
  - MUST: place `<PoweredByQueek />` on every footer variant (`footers/*.tsx`,
293
414
  or `footer.tsx` where a theme has no `footers/` dir). The placement and
294
415
  spacing are the theme's — the component is the content, the footer owns
295
- where it sits. The theme check rule `theme/footer-shows-powered-by`
416
+ where it sits. The `theme/footer-shows-powered-by` rule of `npm run check`
296
417
  ([checks reference](https://docs.usequeek.com/docs/themes/checks)) enforces
297
418
  this, so a theme cannot ship without it.
298
419
  - Style only: the link emits `core-powered-by` with a
@@ -538,8 +659,8 @@ the kit is the one place that knows it, and a framework change is a kit
538
659
  release, not an update to every theme. `Link` is an `<a>` that moves inside the
539
660
  store without a full page load (`href`, `prefetch`, `replace`, plus any anchor
540
661
  attribute); `useRouter()` gives `push`, `replace`, `back` and `refresh`;
541
- `usePathname()` returns the current path. The theme check rejects a theme that
542
- imports `next`.
662
+ `usePathname()` returns the current path. `npm run check` in the theme starter rejects a
663
+ theme that imports `next`.
543
664
 
544
665
  ## Boundary
545
666
 
@@ -549,7 +670,7 @@ framework: the kit may import `next`, a theme may not.
549
670
  ## Develop
550
671
 
551
672
  ```bash
552
- npm install
673
+ npm ci
553
674
  npm run typecheck # tsc --noEmit
554
675
  npm test # public-text check, then vitest run
555
676
  npm run verify-package # pack current source, typecheck a real theme against it
@@ -560,19 +681,41 @@ it from a sandbox, so a break that only external consumers would see (for
560
681
  example a module relying on the global `JSX` namespace, which React 19 removed)
561
682
  fails here instead of after publishing.
562
683
 
684
+ ## Versioning
685
+
686
+ The kit is pre-1.0 (`0.x`). A change to the surface the barrels declare that can
687
+ break a theme ships in a minor version and is listed in
688
+ [CHANGELOG.md](./CHANGELOG.md); patch versions are fixes and additions. Pin a
689
+ minor version (`~0.1.23`) if a theme must not move on its own.
690
+
563
691
  ## Release
564
692
 
565
- Publishing runs on tags via `.github/workflows/publish.yml`, which needs the
566
- `NPM_TOKEN` repository secret:
693
+ Publishing runs on tags via `.github/workflows/publish.yml`, through npm
694
+ trusted publishing (no stored token), with provenance:
567
695
 
568
696
  ```bash
569
- # 1. Bump version in package.json and commit
570
- # 2. Tag and push the tag — the workflow typechecks, tests, runs verify-package,
571
- # then `npm publish --access public`:
697
+ # 1. Bump version in package.json, add the CHANGELOG entry, commit
698
+ # 2. Tag and push the tag. The workflow typechecks, tests, runs verify-package,
699
+ # then `npm publish --provenance --access public`:
572
700
  git tag vX.Y.Z
573
701
  git push origin main --tags
574
702
  ```
575
703
 
704
+ ## Contributing
705
+
706
+ Open an issue or a pull request at
707
+ [usequeek/theme-kit](https://github.com/usequeek/theme-kit). Run `npm run typecheck`
708
+ and `npm test` before you push; CI runs both plus `verify-package`. Keep a change
709
+ to the public surface small, documented with a JSDoc `@example`, and recorded in
710
+ the CHANGELOG.
711
+
712
+ ## Security
713
+
714
+ The kit holds no secrets and only calls the Queek API's `/client/*` routes (a
715
+ guard in `sdk/client.ts` refuses anything else). Customer tokens live in the
716
+ browser's `localStorage`. To report a vulnerability, use GitHub's private
717
+ vulnerability reporting on this repository rather than a public issue.
718
+
576
719
  ## License
577
720
 
578
- MIT
721
+ MIT, copyright Queekx Technology LTD. See [LICENSE](./LICENSE).
@@ -8,6 +8,7 @@ import { extractProducts, normalizeProduct } from '../utils/product-normalizer';
8
8
  * gets a 422 from the API. */
9
9
  export type CollectionSort = 'newest' | 'popular' | 'price_low' | 'price_high';
10
10
 
11
+ /** Options for `fetchCollectionProducts`. */
11
12
  export interface FetchCollectionProductsOptions {
12
13
  page?: number;
13
14
  perPage?: number;
@@ -20,6 +21,10 @@ export interface FetchCollectionProductsOptions {
20
21
  // SSR collection reads go through `/client/store/*`, which resolves the store and
21
22
  // forces X-Platform=storefront (base prices, no marketplace commission) — never
22
23
  // the legacy v1 routes shared with the marketplace.
24
+ /**
25
+ * The store's collections, for server components. Returns `[]` when the store
26
+ * has none. A 404 becomes `[]`; any other failure (timeout, 5xx) throws.
27
+ */
23
28
  export async function fetchCollections(vendorSlug: string, storefrontOnly = true, locale?: string): Promise<Collection[]> {
24
29
  const client = createServerStoreClient(vendorSlug, await getRequestOrigin());
25
30
 
@@ -36,12 +41,20 @@ export async function fetchCollections(vendorSlug: string, storefrontOnly = true
36
41
  }
37
42
  }
38
43
 
44
+ /** One collection by slug, or `null` when it is not in the store's list. */
39
45
  export async function fetchCollection(vendorSlug: string, slug: string, locale?: string): Promise<Collection | null> {
40
46
  const collections = await fetchCollections(vendorSlug, true, locale);
41
47
 
42
48
  return collections.find((collection) => collection.slug === slug) ?? null;
43
49
  }
44
50
 
51
+ /**
52
+ * A collection and one page of its products, for server components.
53
+ *
54
+ * A missing collection gives `{ collection: null, products: [] }`.
55
+ *
56
+ * @throws {ApiRequestError} on any other API failure (timeout, 5xx).
57
+ */
45
58
  export async function fetchCollectionProducts(
46
59
  vendorSlug: string,
47
60
  slug: string,
@@ -63,7 +76,7 @@ export async function fetchCollectionProducts(
63
76
  try {
64
77
  const [collection, response] = await Promise.all([
65
78
  fetchCollection(vendorSlug, slug, options?.locale),
66
- client.get<ApiResponse<unknown, Record<string, unknown>>>(`/collections/${slug}/products`, query, options?.locale),
79
+ client.get<ApiResponse<unknown, Record<string, unknown>>>(`/collections/${encodeURIComponent(slug)}/products`, query, options?.locale),
67
80
  ]);
68
81
 
69
82
  return {
package/api/orders.ts CHANGED
@@ -1,6 +1,15 @@
1
1
  import { createBrowserClient } from '../sdk/client';
2
2
  import type { OrderListResponse, OrderDetail } from '../types/order';
3
3
 
4
+ /**
5
+ * One page of the current customer's orders (`GET /client/store/orders`). Pass
6
+ * a `status` to filter; `'all'` or omitted means no filter.
7
+ *
8
+ * @throws {ApiRequestError} when the API refuses; `code: 'unauthenticated'` means the customer must sign in.
9
+ *
10
+ * @example
11
+ * const { data } = await fetchOrders(vendor.slug, 1, 'completed');
12
+ */
4
13
  export async function fetchOrders(
5
14
  vendorSlug: string,
6
15
  page = 1,
@@ -13,11 +22,16 @@ export async function fetchOrders(
13
22
  });
14
23
  }
15
24
 
25
+ /**
26
+ * One of the current customer's orders, by id.
27
+ *
28
+ * @throws {ApiRequestError} with `code: 'not_found'` when the order does not exist for this customer.
29
+ */
16
30
  export async function fetchOrderDetail(
17
31
  vendorSlug: string,
18
32
  id: string,
19
33
  ): Promise<OrderDetail> {
20
34
  const client = createBrowserClient(vendorSlug);
21
- const res = await client.get<{ data: OrderDetail }>(`/orders/${id}`);
35
+ const res = await client.get<{ data: OrderDetail }>(`/orders/${encodeURIComponent(id)}`);
22
36
  return res.data;
23
37
  }
@@ -41,6 +41,11 @@ const POSITION_CLASS: Record<string, string> = {
41
41
  'center-left': 'core-follow-card--center-left',
42
42
  };
43
43
 
44
+ /**
45
+ * The store's follow card, shown when the merchant has turned it on. It is
46
+ * floating by default; `display="inline"` places it in the page flow. Renders
47
+ * nothing when the app is off.
48
+ */
44
49
  export function FollowCard({ display }: { display?: 'floating' | 'inline' } = {}): JSX.Element | null {
45
50
  const { config, vendor } = useStorefront();
46
51
  const followConfig = config.apps?.follow_card;
@@ -22,8 +22,8 @@ type Status = 'idle' | 'saving' | 'done' | 'error';
22
22
  * The `subscribe` app's `inline` variant (the theme-declared default) — a
23
23
  * theme's own `getSubscribeForm` dispatch renders this directly, passing
24
24
  * heading/tagline/cta straight through with no gate of its own; gating on
25
- * `config.apps?.subscribe?.enabled` happens once, centrally, in whichever
26
- * variant dispatch mounts it (see vendor-shell.tsx / a theme's footer).
25
+ * `config.apps?.subscribe?.enabled` happens once, centrally, by whatever
26
+ * mounts it (the storefront host, or a theme's footer).
27
27
  */
28
28
  export function SubscribeForm({
29
29
  heading = 'Stay in the loop',
@@ -18,6 +18,18 @@ function AuthFlowInner(): JSX.Element | null {
18
18
  return null;
19
19
  }
20
20
 
21
+ /**
22
+ * The kit's customer sign-in and sign-up flow (email code, email and password,
23
+ * and Google), rendered step by step. A theme that wants its own sign-in
24
+ * layout provides `AuthRenderer` in its module and renders this inside it. Its
25
+ * markup carries `core-auth*` classes for your CSS, and every string goes
26
+ * through the theme strings (`auth.*`).
27
+ *
28
+ * @example
29
+ * export function AuthRenderer() {
30
+ * return <div className="my-auth-card"><AuthFlow /></div>;
31
+ * }
32
+ */
21
33
  export function AuthFlow(): JSX.Element {
22
34
  return (
23
35
  <AuthFlowProvider>
@@ -10,6 +10,7 @@ import { useShopCart } from '../../hooks/use-shop-cart';
10
10
  import { useCartPanelStore } from '../../stores/cart-panel-store';
11
11
  import { normalizeCurrencyCode } from '../../utils/format';
12
12
 
13
+ /** What {@link CartPanelController} hands to your render function: the panel state, this shop's lines, and the actions that change them. */
13
14
  export interface CartPanelControllerRenderProps {
14
15
  isOpen: boolean;
15
16
  items: CartItem[];
@@ -23,6 +24,18 @@ export interface CartPanelControllerRenderProps {
23
24
  onRemove: (id: string, shopId: string) => void;
24
25
  }
25
26
 
27
+ /**
28
+ * Headless controller for a cart panel (drawer). It owns open and close state,
29
+ * the Escape key, scroll locking and navigation to checkout; you own the markup.
30
+ * Open it from anywhere with `useCartPanelStore.getState().open()`.
31
+ *
32
+ * @example
33
+ * <CartPanelController>
34
+ * {({ isOpen, items, total, onClose, onCheckout }) =>
35
+ * isOpen ? <MyDrawer items={items} total={total} onClose={onClose} onCheckout={onCheckout} /> : <></>
36
+ * }
37
+ * </CartPanelController>
38
+ */
26
39
  export function CartPanelController({
27
40
  children,
28
41
  }: {
@@ -8,6 +8,11 @@ import { CheckoutContactSection } from './checkout-contact';
8
8
  import { useThemeStrings } from '../../provider';
9
9
  import { PolicyLinks } from './policy-links';
10
10
 
11
+ /**
12
+ * The kit's default checkout layout. The storefront fills its slots (address,
13
+ * payment, promo, fees, submit) and passes them in; a theme can replace the
14
+ * whole shell by providing its own checkout component.
15
+ */
11
16
  export function DefaultCheckoutShell({
12
17
  cartItems,
13
18
  deliveryModes,
@@ -23,6 +23,10 @@ function subscribeNoop(): () => void {
23
23
  return () => {};
24
24
  }
25
25
 
26
+ /**
27
+ * Links to the store's published policies (returns, shipping, privacy, terms).
28
+ * Each opens in the kit's policy modal. Renders nothing when `policies` is empty.
29
+ */
26
30
  export function PolicyLinks({ policies }: { policies: Page[] }): JSX.Element | null {
27
31
  const t = useThemeStrings();
28
32
  const activeSlug = usePolicyModalStore((s) => s.activeSlug);
@@ -23,6 +23,7 @@ const PLACEHOLDER_SVG = `data:image/svg+xml,${encodeURIComponent(
23
23
  </svg>`,
24
24
  )}`;
25
25
 
26
+ /** A labelled placeholder drawn in place of an image that is missing or failed to load. Size it to the space the image will fill. */
26
27
  export interface PlaceholderProp {
27
28
  label?: string;
28
29
  sublabel?: string;
@@ -59,6 +60,15 @@ type ImageProps = ImgHTMLAttributes<HTMLImageElement> & {
59
60
  */
60
61
  type LoadStage = 'primary' | 'original' | 'failed';
61
62
 
63
+ /**
64
+ * The kit's image element. Use it instead of a raw `<img>`: it picks a
65
+ * responsive `srcset` from the API's image variants, retries the original file
66
+ * when a resized one fails, and falls back to `fallback` or a branded
67
+ * placeholder. It accepts every `<img>` attribute.
68
+ *
69
+ * @example
70
+ * <Image src={product.media.image ?? ''} variants={product.media.image_variants} intent="card" alt={product.title} />
71
+ */
62
72
  export function Image({ fallback, placeholder, variants, intent = 'raw', onError, ...props }: ImageProps) {
63
73
  const [stage, setStage] = useState<LoadStage>('primary');
64
74
  // Read context directly — returns null outside a StorefrontProvider
@@ -9,6 +9,7 @@ import type { MenuAlign, MenuSide } from '../utils/menu-placement';
9
9
  import { LanguageMenu } from './language-menu';
10
10
  import './language-switcher.css';
11
11
 
12
+ /** Props for `LanguageSwitcher`. The README lists each prop with its default. */
12
13
  export interface LanguageSwitcherProps {
13
14
  /** Extra class on the root, to position or restyle the control. */
14
15
  className?: string;
@@ -4,6 +4,7 @@ import { useContext, type JSX } from 'react';
4
4
  import { StorefrontContext } from '../provider';
5
5
  import { poweredByQueekUrl } from '../utils/powered-by';
6
6
 
7
+ /** Props for {@link PoweredByQueek}. */
7
8
  export interface PoweredByQueekProps {
8
9
  /** Extra class so a footer can position the link — styling stays theme-owned. */
9
10
  className?: string;
@@ -15,11 +16,11 @@ export interface PoweredByQueekProps {
15
16
  }
16
17
 
17
18
  /**
18
- * Core-owned storefront attribution — the `ProductMetafields` pattern: themes
19
- * import-and-place it, never re-implement it.
19
+ * The storefront attribution link, owned by the kit: themes import and place
20
+ * it, and never re-implement it.
20
21
  *
21
22
  * Required on every footer variant of every theme (enforced by the
22
- * `theme/footer-shows-powered-by` rule of the theme check). The link
23
+ * `theme/footer-shows-powered-by` rule of `npm run check`). The link
23
24
  * target (`https://usequeek.com/business`) and `rel` are the contract —
24
25
  * do not change them. The href is tagged `utm_source=powered_by`,
25
26
  * `utm_medium=storefront`, `utm_campaign=<store slug>` via `poweredByQueekUrl`
@@ -19,6 +19,11 @@ interface ProductMediaFrameProps {
19
19
  intent?: ImageIntent;
20
20
  }
21
21
 
22
+ /**
23
+ * One frame of a product's media gallery: an image through `Image`, or a video.
24
+ * Pass a frame from `buildMediaFrames(product)`. `variant="thumbnail"` renders a
25
+ * muted video with no controls, so the browser shows its first frame.
26
+ */
22
27
  export function ProductMediaFrame({
23
28
  frame,
24
29
  className,
@@ -7,10 +7,12 @@ import type {
7
7
  } from '../types/product';
8
8
  import { formatDisplayDate } from '../utils/format';
9
9
 
10
+ /** Merchant-set labels and types for custom fields, keyed by field key. Passed as `metafieldDefinitions` on the product page. */
10
11
  export interface ProductMetafieldDefinitionLabels {
11
12
  [key: string]: { name: string; type: string };
12
13
  }
13
14
 
15
+ /** Props for {@link ProductMetafields}. */
14
16
  export interface ProductMetafieldsProps {
15
17
  product: Product;
16
18
  definitions?: ProductMetafieldDefinitionLabels;
@@ -205,6 +207,13 @@ export function renderValue(value: MetafieldValue, fieldKey: string, label: stri
205
207
  return renderScalar(value as MetafieldScalar, fieldKey, label, type);
206
208
  }
207
209
 
210
+ /**
211
+ * Place this on the product page to show the product's custom fields.
212
+ * Renders nothing when there are none to show.
213
+ *
214
+ * @example
215
+ * <ProductMetafields product={product} definitions={metafieldDefinitions} />
216
+ */
208
217
  export function ProductMetafields({ product, definitions }: ProductMetafieldsProps): JSX.Element | null {
209
218
  const metafields = product.metafields && typeof product.metafields === 'object' ? product.metafields : {};
210
219
  const visible = Object.entries(metafields).filter(([, value]) => isVisibleValue(value as MetafieldValue));
@@ -1,11 +1,8 @@
1
1
  /* ============================================================
2
- VariantPicker — shared option-selection UI (QR ordering sheet,
3
- default theme's quick-view modal). Prefix: qk-variant-.
4
- Reads --brand-* directly (not a context-scoped token set like
5
- --qr- or --qka-) so it renders correctly regardless of which
6
- surface mounts it. Mirrors quick-add-page.css's qka-option/qka-swatch
7
- rules — same visual language, kept as a separate small file since
8
- this component is reused outside that page's scope.
2
+ VariantPicker: shared option-selection UI (in-store ordering sheet,
3
+ quick-view modal). Class prefix: qk-variant-.
4
+ Reads --brand-* directly, not a surface-scoped token set, so it renders
5
+ correctly whichever surface mounts it.
9
6
  ============================================================ */
10
7
 
11
8
  .qk-variant-option {
package/edit-mode.tsx CHANGED
@@ -6,9 +6,10 @@ import dynamic from 'next/dynamic';
6
6
 
7
7
  const QK_EDIT_FLAG = 'edit';
8
8
 
9
- // Lightweight: reads `?edit=1` once on mount. Always shipped to the
10
- // storefront bundle, but cheap (one URLSearchParams parse) and adds no
11
- // runtime overhead for public visitors.
9
+ /**
10
+ * True when the page was opened by the merchant editor (`?edit=1`). Read once
11
+ * on mount. Cheap enough to ship to every visitor: one query-string parse.
12
+ */
12
13
  export function useEditMode(): boolean {
13
14
  const [isEdit, setIsEdit] = useState(false);
14
15
 
@@ -4,6 +4,7 @@ import { useEffect, useState } from 'react';
4
4
  import { useStorefront } from '../provider';
5
5
  import { getQueekClient } from '../sdk/queek-client';
6
6
 
7
+ /** One address suggestion. Pass `place_id` to `useResolveCoordinates` to get its position. */
7
8
  export interface AddressSuggestion {
8
9
  description: string;
9
10
  place_id: string;
@@ -11,6 +12,14 @@ export interface AddressSuggestion {
11
12
 
12
13
  const DEBOUNCE_MS = 300;
13
14
 
15
+ /**
16
+ * Address suggestions for what the shopper has typed, after a 300 ms pause.
17
+ * Nothing is requested for fewer than two characters. A failed request clears
18
+ * the suggestions rather than throwing, so the field stays usable.
19
+ *
20
+ * @example
21
+ * const { suggestions, loading } = useAddressAutocomplete(text);
22
+ */
14
23
  export function useAddressAutocomplete(query: string): {
15
24
  suggestions: AddressSuggestion[];
16
25
  loading: boolean;