@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.
- package/CHANGELOG.md +37 -0
- package/LICENSE +1 -1
- package/README.md +164 -21
- package/api/collections.ts +14 -1
- package/api/orders.ts +15 -1
- package/apps/follow-card.tsx +5 -0
- package/apps/subscribe-form.tsx +2 -2
- package/components/auth/auth-flow.tsx +12 -0
- package/components/cart/cart-panel-controller.tsx +13 -0
- package/components/checkout/default-checkout-shell.tsx +5 -0
- package/components/checkout/policy-links.tsx +4 -0
- package/components/image.tsx +10 -0
- package/components/language-switcher.tsx +1 -0
- package/components/powered-by-queek.tsx +4 -3
- package/components/product-media-frame.tsx +5 -0
- package/components/product-metafields.tsx +9 -0
- package/components/variant-picker.css +4 -7
- package/edit-mode.tsx +4 -3
- package/hooks/use-address-autocomplete.ts +9 -0
- package/hooks/use-auth-flow.tsx +7 -12
- package/hooks/use-auth.ts +10 -0
- package/hooks/use-cart.ts +12 -0
- package/hooks/use-categories.ts +16 -2
- package/hooks/use-href.ts +9 -4
- package/hooks/use-posts.ts +16 -8
- package/hooks/use-product-detail.ts +25 -11
- package/hooks/use-product-questions.ts +16 -3
- package/hooks/use-products.ts +44 -22
- package/hooks/use-related-products.ts +17 -5
- package/hooks/use-resolve-coordinates.ts +9 -0
- package/hooks/use-reviews.ts +16 -2
- package/hooks/use-search.ts +10 -0
- package/hooks/use-shop-cart.ts +8 -0
- package/hooks/use-shop.ts +19 -6
- package/hooks/use-variant-selection.ts +23 -19
- package/index.ts +10 -11
- package/locales/en.default.json +4 -0
- package/navigation.tsx +2 -0
- package/package.json +25 -4
- package/page-renderer.tsx +11 -0
- package/provider.tsx +29 -3
- package/sdk/client.ts +183 -36
- package/sdk/error-handler.ts +17 -9
- package/sdk/queek-client.ts +11 -1
- package/sdk/server-store-client.ts +2 -2
- package/shared-blocks/embed.tsx +7 -3
- package/shared-blocks/manifest.ts +1 -1
- package/shared-blocks/product-qa.tsx +1 -1
- package/stores/address-modal-store.ts +1 -0
- package/stores/auth-modal-store.ts +1 -0
- package/stores/cart-panel-store.ts +1 -0
- package/stores/cart-store.ts +12 -7
- package/stores/checkout-store.ts +4 -4
- package/stores/location-store.ts +5 -0
- package/stores/orders-sheet-store.ts +1 -0
- package/stores/product-modal-store.ts +1 -0
- package/stores/search-modal-store.ts +1 -0
- package/stores/user-store.ts +1 -1
- package/strings/pseudo-locale.ts +1 -1
- package/theme-context.tsx +11 -1
- package/theme-mount.tsx +14 -0
- package/utils/format.ts +10 -1
- package/utils/menu-link.ts +8 -0
- package/utils/to-error.ts +4 -0
- package/utils/vendor-host.ts +1 -2
- package/scripts/verify-package.ts +0 -102
- package/tests/apps.test.tsx +0 -33
- package/tests/auth-email-errors.test.tsx +0 -55
- package/tests/currency-contract.test.ts +0 -85
- package/tests/default-metaobject-page.test.tsx +0 -56
- package/tests/display-format.test.ts +0 -403
- package/tests/fixtures/external-theme.tsx +0 -90
- package/tests/image-loading.test.tsx +0 -31
- package/tests/language-menu.test.tsx +0 -598
- package/tests/language-switcher.test.tsx +0 -350
- package/tests/list-pagination.test.tsx +0 -71
- package/tests/locale-switch.test.ts +0 -197
- package/tests/locale-transport.test.tsx +0 -321
- package/tests/manifest-parity.test.ts +0 -54
- package/tests/markdown-render.test.ts +0 -46
- package/tests/menu-placement.test.ts +0 -63
- package/tests/metaobjects-block.test.tsx +0 -70
- package/tests/navigation.test.tsx +0 -30
- package/tests/orders-walk.test.ts +0 -48
- package/tests/page-renderer-signature.test.tsx +0 -42
- package/tests/powered-by-queek.test.tsx +0 -65
- package/tests/powered-by-url.test.ts +0 -49
- package/tests/product-images.test.ts +0 -242
- package/tests/product-metafields.test.tsx +0 -154
- package/tests/public-surface.test.ts +0 -35
- package/tests/shop-query.test.ts +0 -64
- package/tests/shopper-chrome-strings.test.tsx +0 -271
- package/tests/storefront-client-scope.test.ts +0 -43
- package/tests/theme-hook-placement.test.tsx +0 -179
- package/tests/theme-strings-client.test.tsx +0 -310
- package/tests/theme-strings-pseudo.test.tsx +0 -171
- package/tests/theme-strings-typed.test.tsx +0 -97
- package/tests/theme-strings.test.tsx +0 -401
- package/tests/theme-typed-keys.fixture.ts +0 -76
- 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
package/README.md
CHANGED
|
@@ -1,9 +1,21 @@
|
|
|
1
1
|
# @usequeek/theme-kit
|
|
2
2
|
|
|
3
|
-
The
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
|
|
30
|
-
`
|
|
31
|
-
|
|
32
|
-
|
|
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
|
|
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.
|
|
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
|
|
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`,
|
|
566
|
-
|
|
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
|
|
570
|
-
# 2. Tag and push the tag
|
|
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).
|
package/api/collections.ts
CHANGED
|
@@ -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
|
}
|
package/apps/follow-card.tsx
CHANGED
|
@@ -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;
|
package/apps/subscribe-form.tsx
CHANGED
|
@@ -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,
|
|
26
|
-
*
|
|
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);
|
package/components/image.tsx
CHANGED
|
@@ -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
|
-
*
|
|
19
|
-
*
|
|
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
|
|
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
|
|
3
|
-
|
|
4
|
-
Reads --brand-* directly
|
|
5
|
-
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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;
|