@usequeek/theme-kit 0.1.21 → 0.1.23
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/README.md +293 -117
- package/api/collections.ts +5 -5
- package/api/shop.ts +1 -1
- package/client.ts +2 -0
- package/components/checkout/checkout-contact.tsx +3 -3
- package/components/image.tsx +5 -5
- package/components/language-menu.css +349 -0
- package/components/language-menu.tsx +371 -0
- package/components/language-switcher.css +131 -0
- package/components/language-switcher.tsx +175 -0
- package/components/markdown.tsx +1 -1
- package/components/powered-by-queek.tsx +4 -4
- package/components/product-media-frame.tsx +1 -1
- package/components/product-metafields.tsx +2 -3
- package/components/variant-picker.tsx +6 -9
- package/demo.ts +10 -14
- package/design-token-preview.tsx +0 -2
- package/edit-mode-active.tsx +3 -4
- package/hooks/use-announcement.ts +1 -1
- package/hooks/use-auth-flow.tsx +1 -1
- package/hooks/use-locales.ts +107 -0
- package/hooks/use-posts.ts +1 -1
- package/hooks/use-product-detail.ts +1 -2
- package/hooks/use-product-sections.ts +4 -6
- package/hooks/use-products.ts +5 -8
- package/index.ts +13 -16
- package/locales/en.default.json +4 -0
- package/next-reference.d.ts +2 -4
- package/package.json +3 -2
- package/page-renderer.tsx +5 -6
- package/provider.tsx +22 -2
- package/scripts/verify-package.ts +6 -8
- package/sdk/client.ts +13 -12
- package/sdk/error-handler.ts +1 -1
- package/sdk/platform.ts +6 -8
- package/sdk/queek-client.ts +11 -12
- package/sdk/server-store-client.ts +13 -17
- package/shared-blocks/metaobjects.tsx +1 -1
- package/stores/cart-store.ts +2 -2
- package/stores/checkout-store.ts +1 -1
- package/stores/edit-preview-store.ts +5 -6
- package/stores/user-store.ts +4 -5
- package/strings/pseudo-locale.ts +3 -4
- package/strings/theme-strings.ts +9 -7
- package/tests/auth-email-errors.test.tsx +2 -2
- package/tests/currency-contract.test.ts +2 -3
- package/tests/display-format.test.ts +17 -25
- package/tests/fixtures/external-theme.tsx +23 -2
- package/tests/language-menu.test.tsx +598 -0
- package/tests/language-switcher.test.tsx +350 -0
- package/tests/list-pagination.test.tsx +8 -8
- package/tests/locale-switch.test.ts +197 -0
- package/tests/locale-transport.test.tsx +3 -3
- package/tests/markdown-render.test.ts +5 -6
- package/tests/menu-placement.test.ts +63 -0
- package/tests/orders-walk.test.ts +5 -5
- package/tests/page-renderer-signature.test.tsx +1 -1
- package/tests/powered-by-queek.test.tsx +3 -3
- package/tests/powered-by-url.test.ts +2 -2
- package/tests/public-surface.test.ts +1 -4
- package/tests/shopper-chrome-strings.test.tsx +5 -6
- package/tests/storefront-client-scope.test.ts +5 -5
- package/tests/theme-hook-placement.test.tsx +2 -2
- package/tests/theme-strings-client.test.tsx +5 -5
- package/tests/theme-strings-pseudo.test.tsx +1 -1
- package/tests/theme-strings-typed.test.tsx +1 -1
- package/tests/theme-strings.test.tsx +8 -11
- package/tests/use-shop-prefetch.test.tsx +1 -1
- package/theme-mount.tsx +6 -11
- package/types/block.ts +7 -7
- package/types/media.ts +8 -9
- package/types/product.ts +8 -12
- package/types/theme.ts +26 -25
- package/types/vendor.ts +10 -12
- package/utils/announcement.ts +3 -3
- package/utils/benefit-icon.tsx +9 -11
- package/utils/brand.ts +7 -10
- package/utils/format.ts +5 -7
- package/utils/image-srcset.ts +2 -2
- package/utils/locale-switch.ts +208 -0
- package/utils/locale.ts +10 -11
- package/utils/menu-link.ts +1 -1
- package/utils/menu-placement.ts +91 -0
- package/utils/product-media.ts +1 -1
- package/utils/product-normalizer.ts +2 -2
- package/utils/vendor-host.ts +7 -11
|
@@ -8,8 +8,8 @@ export interface PoweredByQueekProps {
|
|
|
8
8
|
/** Extra class so a footer can position the link — styling stays theme-owned. */
|
|
9
9
|
className?: string;
|
|
10
10
|
/**
|
|
11
|
-
*
|
|
12
|
-
*
|
|
11
|
+
* Lets a caller hide the link from a prop. Defaults to false; themes always
|
|
12
|
+
* render it visible — there is no merchant-facing toggle for it.
|
|
13
13
|
*/
|
|
14
14
|
hidden?: boolean;
|
|
15
15
|
}
|
|
@@ -18,8 +18,8 @@ export interface PoweredByQueekProps {
|
|
|
18
18
|
* Core-owned storefront attribution — the `ProductMetafields` pattern: themes
|
|
19
19
|
* import-and-place it, never re-implement it.
|
|
20
20
|
*
|
|
21
|
-
* Required on every footer variant of every theme (enforced by
|
|
22
|
-
*
|
|
21
|
+
* 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
23
|
* target (`https://usequeek.com/business`) and `rel` are the contract —
|
|
24
24
|
* do not change them. The href is tagged `utm_source=powered_by`,
|
|
25
25
|
* `utm_medium=storefront`, `utm_campaign=<store slug>` via `poweredByQueekUrl`
|
|
@@ -41,7 +41,7 @@ export function ProductMediaFrame({
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
// Thumbnail strips render at ~64-90px across every theme — the `thumb` tier
|
|
44
|
-
// (a fixed-src
|
|
44
|
+
// (a fixed-src tier that is never part of a srcset) is exactly right there.
|
|
45
45
|
const resolvedIntent: ImageIntent = intent ?? (variant === 'thumbnail' ? 'avatar' : 'hero');
|
|
46
46
|
|
|
47
47
|
return (
|
|
@@ -23,8 +23,7 @@ export interface ProductMetafieldsProps {
|
|
|
23
23
|
* Renders the product's presented metafields (all types), with linked
|
|
24
24
|
* metaobject entries as cards. Renders nothing when there are no visible
|
|
25
25
|
* values. Markup carries `core-metafields*` classes so each theme styles it
|
|
26
|
-
* in its own `theme.css`
|
|
27
|
-
* lives in core.
|
|
26
|
+
* in its own `theme.css` — no theme CSS ships in the kit.
|
|
28
27
|
*/
|
|
29
28
|
|
|
30
29
|
function isEntry(value: unknown): value is MetaobjectEntry {
|
|
@@ -95,7 +94,7 @@ function renderScalar(value: MetafieldScalar, key: string, label: string, type:
|
|
|
95
94
|
|
|
96
95
|
if (type === 'media_id') {
|
|
97
96
|
return /^https?:\/\//i.test(text) ? (
|
|
98
|
-
// Media values arrive as resolved URLs
|
|
97
|
+
// Media values arrive as resolved URLs — plain <img>:
|
|
99
98
|
// this is core, not a theme, so the themes' <Image> rule doesn't apply.
|
|
100
99
|
<img className="core-metafields__image" src={text} alt={label} loading="lazy" />
|
|
101
100
|
) : (
|
|
@@ -14,17 +14,14 @@ interface VariantPickerProps {
|
|
|
14
14
|
/**
|
|
15
15
|
* Renders a product's option groups as color swatch / image swatch / text
|
|
16
16
|
* buttons — standard e-commerce priority: color_code first, else a
|
|
17
|
-
* representative image, else plain text.
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* instead of a third reimplementation. Pair with `matchVariant()`
|
|
22
|
-
* (lib/core/utils/match-variant.ts) to resolve `selectedOptions` into a real
|
|
17
|
+
* representative image, else plain text. Shared by every surface that needs
|
|
18
|
+
* an option picker (quick-add, the QR ordering sheet, a theme's quick-view
|
|
19
|
+
* modal) instead of each re-implementing it. Pair with `matchVariant()`
|
|
20
|
+
* (utils/match-variant.ts) to resolve `selectedOptions` into a real
|
|
23
21
|
* `ProductVariant`, and gate the add action on that being non-null before
|
|
24
22
|
* calling `addProduct`/`addItemWithAddons` — a variant-enabled product added
|
|
25
|
-
* without one fails the cart save server-side
|
|
26
|
-
*
|
|
27
|
-
* is no valid no-selection state to fall back to.
|
|
23
|
+
* without one fails the cart save server-side rather than merely being "no
|
|
24
|
+
* variant"; there is no valid no-selection state to fall back to.
|
|
28
25
|
*/
|
|
29
26
|
export function VariantPicker({ product, selectedOptions, onSelect, className }: VariantPickerProps): JSX.Element | null {
|
|
30
27
|
if (product.options.length === 0) return null;
|
package/demo.ts
CHANGED
|
@@ -1,16 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Turns a theme's `demo.json` into the props its components expect.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* storefront's own preview routes, so they existed only here — a theme could be
|
|
9
|
-
* written outside this repo and never shown running against its own data.
|
|
4
|
+
* Shipped with the kit so a theme developer can render the demo data every
|
|
5
|
+
* theme carries in its `demo.json`: these are the functions that resolve its
|
|
6
|
+
* block references into real products, categories, posts and collections, so a
|
|
7
|
+
* theme can be previewed running against its own data.
|
|
10
8
|
*
|
|
11
|
-
* Pure and type-only by construction: no `fs`, no `next`, no app imports
|
|
12
|
-
*
|
|
13
|
-
* work identically in a developer's preview and in this repo's preview routes.
|
|
9
|
+
* Pure and type-only by construction: no `fs`, no `next`, no app imports, so
|
|
10
|
+
* these work identically in any preview.
|
|
14
11
|
*/
|
|
15
12
|
import type { Block, ReviewItem } from './types/block';
|
|
16
13
|
import type { Menu } from './types/menu';
|
|
@@ -81,11 +78,10 @@ export function getEnrichedPage<T extends Page>(page: T, demo: PreviewDemoData):
|
|
|
81
78
|
}
|
|
82
79
|
|
|
83
80
|
/**
|
|
84
|
-
* `demo.json`'s `posts` array
|
|
85
|
-
* `
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
* assuming every theme's data is current.
|
|
81
|
+
* `demo.json`'s `posts` array must mirror the shape of a store's blog posts
|
|
82
|
+
* (`content: Block[]`, `cover_image_url`). Some themes authored it before that
|
|
83
|
+
* convention settled, so this tolerates the older shapes instead of assuming
|
|
84
|
+
* every theme's data is current.
|
|
89
85
|
*/
|
|
90
86
|
function normalizePost(raw: Record<string, unknown>): Post {
|
|
91
87
|
const legacyMedia = raw.media as { image?: string; thumbnail?: string } | undefined;
|
package/design-token-preview.tsx
CHANGED
|
@@ -58,8 +58,6 @@ export function attachDesignTokenPreviewListener(target: StylableTarget): () =>
|
|
|
58
58
|
* onto the brand-root element's inline style, overlaying (not replacing)
|
|
59
59
|
* the SSR vars from `getBrandCssVariables`. Never persisted — a normal
|
|
60
60
|
* navigation/refresh reloads the real config.
|
|
61
|
-
*
|
|
62
|
-
* Frozen protocol: queek_backend `.agent/.tmp/live-preview-protocol.md`.
|
|
63
61
|
*/
|
|
64
62
|
export function DesignTokenPreviewListener({
|
|
65
63
|
targetRef,
|
package/edit-mode-active.tsx
CHANGED
|
@@ -11,9 +11,8 @@ const MESSAGE_SOURCE = 'queek-storefront';
|
|
|
11
11
|
const MESSAGE_SOURCE_PARENT = 'queek-merchant';
|
|
12
12
|
|
|
13
13
|
// Longest edge of the captured JPEG, in CSS px. Keeps the resulting
|
|
14
|
-
// data-URI small (~50-150KB) so it can ride
|
|
15
|
-
//
|
|
16
|
-
// instead of a data-URI once resume-persistence of attachments is needed.
|
|
14
|
+
// data-URI small (~50-150KB) so it can ride a Qee chat request body inline
|
|
15
|
+
// instead of being uploaded and referenced by URL.
|
|
17
16
|
const CAPTURE_MAX_EDGE = 1000;
|
|
18
17
|
|
|
19
18
|
async function captureBlock(el: HTMLElement): Promise<string> {
|
|
@@ -146,7 +145,7 @@ export function ActiveBlock({
|
|
|
146
145
|
|
|
147
146
|
// Screenshot THIS section locally (edit mode only — the whole component tree
|
|
148
147
|
// only renders inside the editor iframe) and hand the JPEG data-URI to the
|
|
149
|
-
// merchant, which
|
|
148
|
+
// merchant, which attaches it to a Qee chat message.
|
|
150
149
|
const captureToQee = async (): Promise<void> => {
|
|
151
150
|
const el = elRef.current;
|
|
152
151
|
if (!el || capturing) return;
|
|
@@ -8,7 +8,7 @@ const ROTATE_EVERY_MS = 6000;
|
|
|
8
8
|
* Turns the stored announcement into the rotating one every header receives:
|
|
9
9
|
* `text` is the message currently showing (so a header that only ever read
|
|
10
10
|
* `text` rotates with no change), `messages`/`index`/`next`/`prev` let a
|
|
11
|
-
* header draw its own pager
|
|
11
|
+
* header draw its own pager. Auto-advances while there is more than
|
|
12
12
|
* one message; a manual step restarts the clock so the message just chosen
|
|
13
13
|
* gets its full turn.
|
|
14
14
|
*/
|
package/hooks/use-auth-flow.tsx
CHANGED
|
@@ -255,7 +255,7 @@ export function AuthFlowProvider({ children }: { children: ReactNode }): JSX.Ele
|
|
|
255
255
|
console.warn('[google-auth] redirect endpoint returned non-2xx', res.status, text);
|
|
256
256
|
throw new Error(`Failed to initiate Google auth (HTTP ${res.status}).`);
|
|
257
257
|
}
|
|
258
|
-
//
|
|
258
|
+
// Response envelope: { status, message, data: { url } }
|
|
259
259
|
const body = (await res.json()) as {
|
|
260
260
|
data?: { url?: string; redirect_url?: string };
|
|
261
261
|
url?: string;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useCallback, useContext, useEffect, useMemo, useState } from 'react';
|
|
4
|
+
import { StorefrontContext } from '../provider';
|
|
5
|
+
import { usePathname, useRouter } from '../navigation';
|
|
6
|
+
import {
|
|
7
|
+
defaultStoreLocale,
|
|
8
|
+
localeHref,
|
|
9
|
+
resolveActiveLocale,
|
|
10
|
+
type StoreLocale,
|
|
11
|
+
} from '../utils/locale-switch';
|
|
12
|
+
|
|
13
|
+
export interface LocalesState {
|
|
14
|
+
/** The store's languages, the default first. One entry on a single-language store. */
|
|
15
|
+
locales: StoreLocale[];
|
|
16
|
+
/** The language the page renders in. */
|
|
17
|
+
active: StoreLocale;
|
|
18
|
+
/** The language served at the store root. */
|
|
19
|
+
defaultLocale: StoreLocale;
|
|
20
|
+
/** More than one language: whether a language choice makes sense at all. */
|
|
21
|
+
hasMultiple: boolean;
|
|
22
|
+
/**
|
|
23
|
+
* The current page in another language, e.g. `/fr/shop?sort=new`. Takes a
|
|
24
|
+
* language row or its code; an unknown code returns the current URL. The
|
|
25
|
+
* query string and hash are included once the page has mounted (the first
|
|
26
|
+
* render matches the server's markup), so use it for `href`s and call
|
|
27
|
+
* `switchTo` to navigate.
|
|
28
|
+
*/
|
|
29
|
+
hrefFor: (locale: StoreLocale | string) => string;
|
|
30
|
+
/** Go to the current page in another language, reading the live query string and hash. */
|
|
31
|
+
switchTo: (locale: StoreLocale | string) => void;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const EMPTY_SUFFIX = { search: '', hash: '' };
|
|
35
|
+
|
|
36
|
+
function readSuffix(): { search: string; hash: string } {
|
|
37
|
+
if (typeof window === 'undefined') return EMPTY_SUFFIX;
|
|
38
|
+
return { search: window.location.search, hash: window.location.hash };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The store's languages and the active one, for a theme that builds its own
|
|
43
|
+
* language control. Reads what the host passed to `StorefrontProvider`
|
|
44
|
+
* (`locales`) — no request. Absent or one language → a single-language state
|
|
45
|
+
* (`hasMultiple` false) with a no-op-safe `hrefFor`; outside a provider it
|
|
46
|
+
* does not throw.
|
|
47
|
+
*
|
|
48
|
+
* Most themes want the `LanguageSwitcher` component instead.
|
|
49
|
+
*/
|
|
50
|
+
export function useLocales(): LocalesState {
|
|
51
|
+
const context = useContext(StorefrontContext);
|
|
52
|
+
const pathname = usePathname();
|
|
53
|
+
const router = useRouter();
|
|
54
|
+
const basePath = context?.basePath ?? '';
|
|
55
|
+
const requestLocale = context?.locale ?? null;
|
|
56
|
+
const contextLocales = context?.locales;
|
|
57
|
+
|
|
58
|
+
const locales = useMemo(() => contextLocales ?? [], [contextLocales]);
|
|
59
|
+
const defaultLocale = useMemo(() => defaultStoreLocale(locales), [locales]);
|
|
60
|
+
const active = useMemo(() => resolveActiveLocale(locales, requestLocale), [locales, requestLocale]);
|
|
61
|
+
|
|
62
|
+
// Query and hash only exist in the browser: start empty so the first client
|
|
63
|
+
// render matches the server, then adopt them (and follow later changes).
|
|
64
|
+
const [suffix, setSuffix] = useState(EMPTY_SUFFIX);
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
const next = readSuffix();
|
|
67
|
+
setSuffix((prev) => (prev.search === next.search && prev.hash === next.hash ? prev : next));
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const resolve = useCallback(
|
|
71
|
+
(locale: StoreLocale | string): StoreLocale | null => {
|
|
72
|
+
if (typeof locale !== 'string') return locale;
|
|
73
|
+
const code = locale.toLowerCase();
|
|
74
|
+
return locales.find((row) => row.locale.toLowerCase() === code) ?? null;
|
|
75
|
+
},
|
|
76
|
+
[locales],
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
const build = useCallback(
|
|
80
|
+
(target: StoreLocale, search: string, hash: string) =>
|
|
81
|
+
localeHref({ pathname, search, hash, basePath, active, target }),
|
|
82
|
+
[pathname, basePath, active],
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
const hrefFor = useCallback(
|
|
86
|
+
(locale: StoreLocale | string): string => {
|
|
87
|
+
const target = resolve(locale);
|
|
88
|
+
return build(target ?? active, suffix.search, suffix.hash);
|
|
89
|
+
},
|
|
90
|
+
[resolve, build, active, suffix],
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
const switchTo = useCallback(
|
|
94
|
+
(locale: StoreLocale | string): void => {
|
|
95
|
+
const target = resolve(locale);
|
|
96
|
+
if (!target || target === active) return;
|
|
97
|
+
const live = readSuffix();
|
|
98
|
+
router.push(build(target, live.search, live.hash));
|
|
99
|
+
},
|
|
100
|
+
[resolve, build, active, router],
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
return useMemo(
|
|
104
|
+
() => ({ locales, active, defaultLocale, hasMultiple: locales.length > 1, hrefFor, switchTo }),
|
|
105
|
+
[locales, active, defaultLocale, hrefFor, switchTo],
|
|
106
|
+
);
|
|
107
|
+
}
|
package/hooks/use-posts.ts
CHANGED
|
@@ -11,7 +11,7 @@ import { withLocaleQuery } from '../utils/locale';
|
|
|
11
11
|
* Client-side blog-post listing, for blocks embedded on an arbitrary page
|
|
12
12
|
* (e.g. the 'blog' home-page block) — mirrors useProducts' shape/pattern.
|
|
13
13
|
* The blog LISTING page itself (pages/blog.tsx) gets its posts via SSR props
|
|
14
|
-
* instead (fetchPosts in
|
|
14
|
+
* instead (`fetchPosts` in the host's page loader); this hook is for anywhere
|
|
15
15
|
* else a theme needs a live post list without page-level SSR data.
|
|
16
16
|
*/
|
|
17
17
|
export function usePosts(options?: { category?: string | null; limit?: number }): { posts: Post[]; isLoading: boolean } {
|
|
@@ -15,8 +15,7 @@ interface UseProductDetailResult {
|
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
17
|
* The product LIST/collection endpoint never returns `addons`, `options`, or
|
|
18
|
-
* `variants` — those only exist on the single-product "view" response
|
|
19
|
-
* (`ProductResource::buildBasePayload` vs `buildViewPayload`, queek_backend).
|
|
18
|
+
* `variants` — those only exist on the single-product "view" response.
|
|
20
19
|
* Any quick-view/quick-add modal opened from a product card only has the
|
|
21
20
|
* list-shaped object, so it must re-fetch full detail before it can render
|
|
22
21
|
* or select an addon/option — rendering `listProduct.addons` directly always
|
|
@@ -31,8 +31,7 @@ export interface MenuGroupItem {
|
|
|
31
31
|
* smart collection whose rule DOES reference other collections — a browsing
|
|
32
32
|
* umbrella, not a product filter) offers those referenced collections as
|
|
33
33
|
* tiles first. Every id here is already present in this same response's
|
|
34
|
-
* flat product pool, so resolving a tap is a pure client-side lookup
|
|
35
|
-
* VendorProductController::resolveQrTabGroups (queek_backend). */
|
|
34
|
+
* flat product pool, so resolving a tap is a pure client-side lookup. */
|
|
36
35
|
export type MenuGroup =
|
|
37
36
|
| { id: string; name: string; type: 'products'; product_ids: string[] }
|
|
38
37
|
| { id: string; name: string; type: 'collections'; items: MenuGroupItem[] };
|
|
@@ -118,10 +117,9 @@ export function useProductSections(): {
|
|
|
118
117
|
.get<ApiResponse<unknown>>('/products', withLocaleQuery(undefined, locale))
|
|
119
118
|
.then((response) => {
|
|
120
119
|
const data = response.data;
|
|
121
|
-
// instore_qr only —
|
|
122
|
-
// inside `data
|
|
123
|
-
//
|
|
124
|
-
// land somewhere additive instead.
|
|
120
|
+
// instore_qr only — the API attaches this under meta.groups, never
|
|
121
|
+
// inside `data`: sectioned/menu-mode `data` is shared with the mobile
|
|
122
|
+
// app's response shape, so groups land somewhere additive instead.
|
|
125
123
|
const rawGroups = (response.meta as { groups?: MenuGroup[] } | undefined)?.groups;
|
|
126
124
|
const groups = Array.isArray(rawGroups) ? rawGroups : [];
|
|
127
125
|
|
package/hooks/use-products.ts
CHANGED
|
@@ -102,7 +102,7 @@ export function useProducts(options?: ProductQuery & {
|
|
|
102
102
|
if (options?.sort) query.sort = options.sort;
|
|
103
103
|
if (options?.limit) query.per_page = options.limit;
|
|
104
104
|
if (options?.hasVideo) query.has_video = 1;
|
|
105
|
-
// Ask the
|
|
105
|
+
// Ask the API for exactly these products (by id) instead of
|
|
106
106
|
// fetching an arbitrary `per_page`-sized page of the default listing
|
|
107
107
|
// and hoping the requested ids happen to land in it — with a small
|
|
108
108
|
// limit (bundle/shop-the-look/featured's 1-3) they almost never did,
|
|
@@ -149,13 +149,10 @@ export function useProducts(options?: ProductQuery & {
|
|
|
149
149
|
/**
|
|
150
150
|
* Full product detail (the same `/products/{slug}` request the PDP itself
|
|
151
151
|
* uses), including the full ordered detail `media.images` list — the
|
|
152
|
-
* list/collection endpoints `useProducts` calls only ever return
|
|
153
|
-
* `media.images` list
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
* in queek_backend). Single-product spotlight blocks (e.g. glow's `featured`
|
|
157
|
-
* variant) need this instead of substituting image/thumbnail as a fake
|
|
158
|
-
* 2-image list.
|
|
152
|
+
* list/collection endpoints `useProducts` calls only ever return a capped
|
|
153
|
+
* `media.images` list plus `media.{image,thumbnail}`. Single-product spotlight
|
|
154
|
+
* blocks (e.g. a `featured` variant) need this instead of substituting
|
|
155
|
+
* image/thumbnail as a fake 2-image list.
|
|
159
156
|
*/
|
|
160
157
|
/**
|
|
161
158
|
* Fetch one product by SLUG.
|
package/index.ts
CHANGED
|
@@ -1,15 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The public surface a theme is allowed to build against.
|
|
3
3
|
*
|
|
4
|
-
* The package is `@usequeek/theme-kit`: what a theme is built AGAINST
|
|
5
|
-
* the job rather than for our internals — "storefront core" describes where the
|
|
6
|
-
* code lives here, which is meaningless to someone who has never seen this repo.
|
|
4
|
+
* The package is `@usequeek/theme-kit`: what a theme is built AGAINST.
|
|
7
5
|
*
|
|
8
|
-
* Themes
|
|
9
|
-
* These two barrels name which of those paths are API
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* may change without notice.
|
|
6
|
+
* Themes import the kit through deep paths (`@usequeek/theme-kit/hooks/use-cart`).
|
|
7
|
+
* These two barrels name which of those paths are API, so the boundary is written
|
|
8
|
+
* down. Anything NOT re-exported here is the kit's own plumbing and may change
|
|
9
|
+
* without notice.
|
|
13
10
|
*
|
|
14
11
|
* Two entries because the split is real, not cosmetic: this one is types and
|
|
15
12
|
* pure functions, importable from anywhere including node tooling; `client.ts`
|
|
@@ -18,16 +15,15 @@
|
|
|
18
15
|
* deliberately unreachable from both.
|
|
19
16
|
*
|
|
20
17
|
* THESE ARE A DECLARATION, NOT AN IMPORT PATH. Do not migrate themes onto them.
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
* resolve outside a bundler. It is the same reason a barrel would bloat every
|
|
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
|
|
25
22
|
* theme chunk: the stores call `create()` at import time and package.json
|
|
26
23
|
* declares no `sideEffects`, so webpack must treat every re-export as live and
|
|
27
24
|
* cannot drop the unused ones. Themes keep importing the exact module they use;
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* machine-checkable statement of what is in the contract.
|
|
25
|
+
* these barrels stay the machine-checkable statement of what is in the public
|
|
26
|
+
* surface.
|
|
31
27
|
*/
|
|
32
28
|
|
|
33
29
|
export * from './types/theme';
|
|
@@ -49,11 +45,12 @@ export * from './utils/video-embed';
|
|
|
49
45
|
export * from './utils/match-variant';
|
|
50
46
|
export * from './utils/announcement';
|
|
51
47
|
export * from './utils/locale';
|
|
48
|
+
export * from './utils/locale-switch';
|
|
52
49
|
|
|
53
50
|
// Theme strings — the pure/server-safe i18n core (no next/*, no React).
|
|
54
51
|
export * from './strings/theme-strings';
|
|
55
52
|
// Pseudo-locale QA — dev/test-only dictionary transform + English-leak scan
|
|
56
|
-
// (pure/server-safe: no next/*, no React, so
|
|
53
|
+
// (pure/server-safe: no next/*, no React, so Node tooling can import it too).
|
|
57
54
|
export * from './strings/pseudo-locale';
|
|
58
55
|
|
|
59
56
|
// Rendering a theme's own demo.json — what makes local preview possible.
|
package/locales/en.default.json
CHANGED
package/next-reference.d.ts
CHANGED
|
@@ -4,9 +4,7 @@
|
|
|
4
4
|
* `/// <reference types="next" />` every Next app gets via next-env.d.ts.
|
|
5
5
|
*
|
|
6
6
|
* The kit runs inside Next apps, so depending on Next's own type package is
|
|
7
|
-
* correct. Depending on
|
|
8
|
-
*
|
|
9
|
-
* broken) is what must never happen again. This file is that dependency,
|
|
10
|
-
* written down.
|
|
7
|
+
* correct. Depending on a host app's own next-env.d.ts would break every
|
|
8
|
+
* consumer that does not have one. This file is that dependency, written down.
|
|
11
9
|
*/
|
|
12
10
|
/// <reference types="next" />
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@usequeek/theme-kit",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.23",
|
|
4
4
|
"private": false,
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"description": "The framework a Queek storefront theme is built against.",
|
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
"@types/node": "^20",
|
|
45
45
|
"@types/react": "^19",
|
|
46
46
|
"@types/react-dom": "^19",
|
|
47
|
+
"jsdom": "^28.1.0",
|
|
47
48
|
"next": "^16.0.0",
|
|
48
49
|
"react": "^19.2.3",
|
|
49
50
|
"react-dom": "^19.2.3",
|
|
@@ -52,7 +53,7 @@
|
|
|
52
53
|
"vitest": "^3.2.4"
|
|
53
54
|
},
|
|
54
55
|
"scripts": {
|
|
55
|
-
"test": "vitest run",
|
|
56
|
+
"test": "node scripts/check-public-text.mjs && node --test scripts/check-public-text.test.mjs && vitest run",
|
|
56
57
|
"typecheck": "tsc --noEmit",
|
|
57
58
|
"verify-package": "tsx scripts/verify-package.ts"
|
|
58
59
|
}
|
package/page-renderer.tsx
CHANGED
|
@@ -121,11 +121,10 @@ export function PageRenderer({ blocks }: { blocks: Block[] }): JSX.Element {
|
|
|
121
121
|
// help regardless of where the section's own text ends up. Forcing
|
|
122
122
|
// `color: #fff` alongside it is deliberate, not decorative — the same
|
|
123
123
|
// "on_accent independently defaulting to something unrelated" trap that
|
|
124
|
-
//
|
|
125
|
-
//
|
|
126
|
-
//
|
|
127
|
-
//
|
|
128
|
-
// default to. `!important`-equivalent isn't needed — this is the
|
|
124
|
+
// brand.ts's ensureReadableOnAccent guards against applies here too: a
|
|
125
|
+
// theme's own default text color was never chosen with an arbitrary vendor
|
|
126
|
+
// photo behind it, so legibility can't be left to inherit from wherever the
|
|
127
|
+
// theme happens to default to. `!important`-equivalent isn't needed — this is the
|
|
129
128
|
// closest ancestor rule, and CSS3 UI text is dark by default in every
|
|
130
129
|
// theme (see brand.ts's own comment on directly-matched rules winning
|
|
131
130
|
// over inherited ones for exactly the fix this exists to preempt).
|
|
@@ -167,7 +166,7 @@ export function PageRenderer({ blocks }: { blocks: Block[] }): JSX.Element {
|
|
|
167
166
|
// a sensible default for a plain content page's paragraphs/
|
|
168
167
|
// images, but for a colored band it leaves a seam of the page's
|
|
169
168
|
// own background between adjacent sections, breaking the "solid
|
|
170
|
-
// band" look (
|
|
169
|
+
// band" look (the seam measures 16px, exactly the default gap).
|
|
171
170
|
// Pull this section half a gap into the space above/below it;
|
|
172
171
|
// when the neighbor is ALSO a bg_color band doing the same, the
|
|
173
172
|
// two halves meet and the seam closes to zero. Derives from the
|
package/provider.tsx
CHANGED
|
@@ -9,6 +9,7 @@ import type { Product } from './types/product';
|
|
|
9
9
|
import type { StorefrontConfig, VendorProfile } from './types/vendor';
|
|
10
10
|
import { getBrandCssVariables } from './utils/brand';
|
|
11
11
|
import { parseLocaleCode } from './utils/locale';
|
|
12
|
+
import { normalizeStoreLocales, type StoreLocale, type StoreLocaleInput } from './utils/locale-switch';
|
|
12
13
|
import {
|
|
13
14
|
defaultThemeStrings,
|
|
14
15
|
t as translateThemeStrings,
|
|
@@ -43,7 +44,7 @@ interface StorefrontContextValue {
|
|
|
43
44
|
previewData?: StorefrontPreviewData;
|
|
44
45
|
basePath: string;
|
|
45
46
|
/** slug => 'full' | 'bare', for ShellInner to decide the CURRENT page's
|
|
46
|
-
* header/footer without a per-navigation fetch.
|
|
47
|
+
* header/footer without a per-navigation fetch. */
|
|
47
48
|
pagesChrome: Record<string, 'full' | 'bare'>;
|
|
48
49
|
/** Validated request locale (null = primary). Browser hooks read it via
|
|
49
50
|
* `useStorefrontLocale()` and send `?locale=` when it is set. */
|
|
@@ -53,6 +54,11 @@ interface StorefrontContextValue {
|
|
|
53
54
|
* Optional so hand-built context values (storefront scripts, tests) keep
|
|
54
55
|
* compiling without it. */
|
|
55
56
|
strings?: ThemeStringsDictionary[];
|
|
57
|
+
/** The store's languages (default first), normalised. Empty or omitted =
|
|
58
|
+
* a single-language store: `useLocales()` reports one language and the
|
|
59
|
+
* `LanguageSwitcher` renders nothing. Optional so hand-built context values
|
|
60
|
+
* keep compiling without it. */
|
|
61
|
+
locales?: StoreLocale[];
|
|
56
62
|
}
|
|
57
63
|
|
|
58
64
|
export const StorefrontContext = createContext<StorefrontContextValue | null>(null);
|
|
@@ -66,6 +72,7 @@ export function StorefrontProvider({
|
|
|
66
72
|
pagesChrome,
|
|
67
73
|
locale,
|
|
68
74
|
strings,
|
|
75
|
+
locales,
|
|
69
76
|
children,
|
|
70
77
|
}: {
|
|
71
78
|
vendor: VendorProfile;
|
|
@@ -89,6 +96,17 @@ export function StorefrontProvider({
|
|
|
89
96
|
* renders exactly as before. Existing props are untouched.
|
|
90
97
|
*/
|
|
91
98
|
strings?: ThemeStringsDictionary | ThemeStringsDictionary[] | null;
|
|
99
|
+
/**
|
|
100
|
+
* The store's languages, resolved by the host (no request from the kit).
|
|
101
|
+
* Pass the host's list as it is — rows follow the locales endpoint shape
|
|
102
|
+
* (`locale`, `name`, `native_name`, `is_primary`, `is_default`,
|
|
103
|
+
* `path_prefix`, …); extra fields are ignored and a missing `is_default`
|
|
104
|
+
* falls back to `is_primary`. Optional — absent or a single language
|
|
105
|
+
* means no language choice: `LanguageSwitcher` renders nothing and
|
|
106
|
+
* `useLocales()` reports one language. Themes never set this; the host
|
|
107
|
+
* owns it, like `basePath`.
|
|
108
|
+
*/
|
|
109
|
+
locales?: readonly StoreLocaleInput[] | null;
|
|
92
110
|
children: ReactNode;
|
|
93
111
|
}): JSX.Element {
|
|
94
112
|
const resolvedBasePath = basePath ?? `/${vendor.slug ?? ''}`;
|
|
@@ -102,6 +120,7 @@ export function StorefrontProvider({
|
|
|
102
120
|
),
|
|
103
121
|
[strings],
|
|
104
122
|
);
|
|
123
|
+
const resolvedLocales = useMemo(() => normalizeStoreLocales(locales), [locales]);
|
|
105
124
|
const value = useMemo(
|
|
106
125
|
() => ({
|
|
107
126
|
vendor,
|
|
@@ -112,8 +131,9 @@ export function StorefrontProvider({
|
|
|
112
131
|
pagesChrome: pagesChrome ?? {},
|
|
113
132
|
locale: resolvedLocale,
|
|
114
133
|
strings: resolvedStrings,
|
|
134
|
+
locales: resolvedLocales,
|
|
115
135
|
}),
|
|
116
|
-
[config, menus, previewData, vendor, resolvedBasePath, pagesChrome, resolvedLocale, resolvedStrings],
|
|
136
|
+
[config, menus, previewData, vendor, resolvedBasePath, pagesChrome, resolvedLocale, resolvedStrings, resolvedLocales],
|
|
117
137
|
);
|
|
118
138
|
const brandRootRef = useRef<HTMLDivElement>(null);
|
|
119
139
|
|
|
@@ -8,12 +8,12 @@
|
|
|
8
8
|
* error TS2503: Cannot find namespace 'JSX'.
|
|
9
9
|
*
|
|
10
10
|
* 42 modules annotated returns as `JSX.Element`, relying on a GLOBAL JSX
|
|
11
|
-
* namespace that came from
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
11
|
+
* namespace that came from a host app's `next-env.d.ts` — not from React.
|
|
12
|
+
* Inside a host app the kit free-rode on that file, so tsc, the tests, the
|
|
13
|
+
* build and the render smoke were ALL green. The workspace supplies exactly the
|
|
14
|
+
* file whose absence is the bug, which is why no in-repo check could ever have
|
|
15
|
+
* found it. (Every module now does `import type { JSX } from 'react'`, and this
|
|
16
|
+
* script proves it.)
|
|
17
17
|
*
|
|
18
18
|
* It packs the CURRENT SOURCE rather than installing the published version, so
|
|
19
19
|
* it fails before a broken version ships instead of after. Installing from the
|
|
@@ -26,8 +26,6 @@ import { dirname, join, resolve } from 'node:path';
|
|
|
26
26
|
import { fileURLToPath } from 'node:url';
|
|
27
27
|
|
|
28
28
|
// The kit package is `"type": "module"`, so `__dirname` does not exist here.
|
|
29
|
-
// (The storefront copy of this script could use it only because that package
|
|
30
|
-
// is CJS.)
|
|
31
29
|
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
32
30
|
const KIT = ROOT;
|
|
33
31
|
|