@usequeek/theme-kit 0.1.0

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 (120) hide show
  1. package/README.md +37 -0
  2. package/api/collections.ts +93 -0
  3. package/api/orders.ts +23 -0
  4. package/apps/analytics-events.ts +32 -0
  5. package/apps/apps.css +613 -0
  6. package/apps/follow-card.tsx +192 -0
  7. package/apps/gtag.ts +19 -0
  8. package/apps/subscribe-form.tsx +124 -0
  9. package/client.ts +52 -0
  10. package/components/auth/auth-email-login-step.tsx +46 -0
  11. package/components/auth/auth-email-otp-step.tsx +54 -0
  12. package/components/auth/auth-email-otp-verify-step.tsx +82 -0
  13. package/components/auth/auth-email-register-step.tsx +59 -0
  14. package/components/auth/auth-flow.tsx +26 -0
  15. package/components/auth/auth-signup-step.tsx +53 -0
  16. package/components/blog/category-filter.tsx +92 -0
  17. package/components/blog/index.ts +12 -0
  18. package/components/blog/pagination.tsx +73 -0
  19. package/components/blog/post-categories.tsx +42 -0
  20. package/components/blog/post-meta.tsx +49 -0
  21. package/components/blog/related-posts.tsx +54 -0
  22. package/components/blog/share-buttons.tsx +89 -0
  23. package/components/cart/cart-panel-controller.tsx +67 -0
  24. package/components/cart/default-cart-shell.tsx +207 -0
  25. package/components/checkout/checkout-contact.tsx +84 -0
  26. package/components/checkout/default-checkout-shell.tsx +177 -0
  27. package/components/checkout/policy-links.tsx +98 -0
  28. package/components/image.tsx +149 -0
  29. package/components/markdown.tsx +54 -0
  30. package/components/product-media-frame.tsx +56 -0
  31. package/components/variant-picker.css +84 -0
  32. package/components/variant-picker.tsx +107 -0
  33. package/design-token-preview.tsx +81 -0
  34. package/edit-mode-active.tsx +324 -0
  35. package/edit-mode.tsx +80 -0
  36. package/hooks/use-address-autocomplete.ts +62 -0
  37. package/hooks/use-auth-flow.tsx +346 -0
  38. package/hooks/use-auth.ts +17 -0
  39. package/hooks/use-cart.ts +14 -0
  40. package/hooks/use-categories.ts +112 -0
  41. package/hooks/use-collection-params.ts +67 -0
  42. package/hooks/use-href.ts +15 -0
  43. package/hooks/use-posts.ts +75 -0
  44. package/hooks/use-product-detail.ts +69 -0
  45. package/hooks/use-product-questions.ts +77 -0
  46. package/hooks/use-product-sections.ts +165 -0
  47. package/hooks/use-products.ts +216 -0
  48. package/hooks/use-related-products.ts +56 -0
  49. package/hooks/use-resolve-coordinates.ts +48 -0
  50. package/hooks/use-reviews.ts +167 -0
  51. package/hooks/use-search.ts +55 -0
  52. package/hooks/use-shop-cart.ts +31 -0
  53. package/hooks/use-shop-params.ts +68 -0
  54. package/hooks/use-shop.ts +186 -0
  55. package/hooks/use-variant-selection.ts +98 -0
  56. package/index.ts +49 -0
  57. package/package.json +41 -0
  58. package/page-renderer.tsx +365 -0
  59. package/provider.tsx +91 -0
  60. package/sdk/client.ts +221 -0
  61. package/sdk/error-handler.ts +37 -0
  62. package/sdk/platform.ts +78 -0
  63. package/sdk/queek-client.ts +104 -0
  64. package/sdk/reauth-guard.ts +27 -0
  65. package/sdk/server-store-client.ts +165 -0
  66. package/shared-blocks/button.tsx +41 -0
  67. package/shared-blocks/callout.tsx +35 -0
  68. package/shared-blocks/content-default.tsx +23 -0
  69. package/shared-blocks/core-blocks.css +1792 -0
  70. package/shared-blocks/divider.tsx +17 -0
  71. package/shared-blocks/embed.tsx +40 -0
  72. package/shared-blocks/faq.tsx +44 -0
  73. package/shared-blocks/image.tsx +29 -0
  74. package/shared-blocks/index.ts +24 -0
  75. package/shared-blocks/manifest.ts +76 -0
  76. package/shared-blocks/product-qa.tsx +203 -0
  77. package/shared-blocks/quote.tsx +20 -0
  78. package/shared-blocks/reviews.tsx +436 -0
  79. package/shared-blocks/table.tsx +45 -0
  80. package/shared-blocks/video.tsx +60 -0
  81. package/stores/address-modal-store.ts +15 -0
  82. package/stores/auth-modal-store.ts +17 -0
  83. package/stores/cart-panel-store.ts +17 -0
  84. package/stores/cart-store.ts +172 -0
  85. package/stores/checkout-store.ts +167 -0
  86. package/stores/edit-preview-store.ts +61 -0
  87. package/stores/location-store.ts +41 -0
  88. package/stores/orders-sheet-store.ts +21 -0
  89. package/stores/policy-modal-store.ts +15 -0
  90. package/stores/product-modal-store.ts +18 -0
  91. package/stores/search-modal-store.ts +17 -0
  92. package/stores/toast-store.ts +46 -0
  93. package/stores/user-store.ts +88 -0
  94. package/theme-context.tsx +28 -0
  95. package/theme-variant-resolver.ts +44 -0
  96. package/types/block.ts +292 -0
  97. package/types/cart.ts +21 -0
  98. package/types/media.ts +55 -0
  99. package/types/menu.ts +27 -0
  100. package/types/order.ts +119 -0
  101. package/types/page.ts +86 -0
  102. package/types/product.ts +279 -0
  103. package/types/theme.ts +380 -0
  104. package/types/vendor.ts +243 -0
  105. package/utils/addon-selection.ts +31 -0
  106. package/utils/benefit-icon.tsx +145 -0
  107. package/utils/brand-palette.ts +64 -0
  108. package/utils/brand.ts +518 -0
  109. package/utils/contact-location.ts +44 -0
  110. package/utils/format.ts +64 -0
  111. package/utils/hero-content.ts +35 -0
  112. package/utils/image-srcset.ts +157 -0
  113. package/utils/markdown.tsx +237 -0
  114. package/utils/match-variant.ts +31 -0
  115. package/utils/menu-link.ts +41 -0
  116. package/utils/placeholder-resolver.ts +184 -0
  117. package/utils/product-media.ts +77 -0
  118. package/utils/product-normalizer.ts +323 -0
  119. package/utils/vendor-host.ts +173 -0
  120. package/utils/video-embed.ts +38 -0
package/README.md ADDED
@@ -0,0 +1,37 @@
1
+ # @usequeek/theme-kit
2
+
3
+ The framework a Queek storefront theme is built against: hooks, stores, headless
4
+ flows (auth, cart, checkout), types and the block renderer.
5
+
6
+ **Ships TypeScript source, not a build.** 78 of its 115 modules carry a
7
+ `'use client'` directive, and that directive is the whole contract with the React
8
+ Server Components boundary — bundlers routinely strip or hoist it when a library
9
+ is pre-compiled, which turns a client module into a server one silently. The
10
+ consumer transpiles instead (`transpilePackages: ['@usequeek/theme-kit']`), so the
11
+ directives survive to the bundler that actually enforces them.
12
+
13
+ Deep subpaths resolve off the filesystem — there is deliberately NO `exports`
14
+ map. One was tried: with source-shipped `.ts`/`.tsx` an extensionless wildcard
15
+ target does not resolve, and spelling out extensions cannot cover both. Legacy
16
+ resolution under `moduleResolution: bundler` handles it. The cost is that the
17
+ package has no subpath encapsulation, so what a theme may import is enforced by
18
+ `tests/core-public-surface.test.ts` in the storefront repo instead — which is
19
+ where the real contract lives anyway. Revisit if this is ever published.
20
+
21
+ ## Use
22
+
23
+ ```ts
24
+ import { useCart } from '@usequeek/theme-kit/hooks/use-cart';
25
+ import type { ThemeModule } from '@usequeek/theme-kit/types/theme';
26
+ ```
27
+
28
+ Deep paths, mirroring the old `@/lib/core/*`. Barrels exist (`.` and `./client`)
29
+ to DECLARE the public surface — what a theme is allowed to build against — but
30
+ importing through them pulls every client module into one chunk and was measured
31
+ to break the app's build. Import the module you want.
32
+
33
+ ## Boundary
34
+
35
+ The kit must never import the storefront app, its themes, or `lib/storefront`.
36
+ That one-way rule is enforced by `tests/core-storefront-boundary.test.ts` in the
37
+ storefront repo, and it is why this package can be extracted at all.
@@ -0,0 +1,93 @@
1
+ import { createServerStoreClient, getRequestOrigin } from '../sdk/server-store-client';
2
+ import { swallowNotFound } from '../sdk/client';
3
+ import type { Collection, Pagination } from '../types/page';
4
+ import type { ApiResponse } from '../types/product';
5
+ import { extractProducts, normalizeProduct } from '../utils/product-normalizer';
6
+
7
+ /** Matches `VendorCollectionProductsRequest`'s validated `sort` values exactly
8
+ * (queek_backend) — a value outside this list gets a 422 from the backend. */
9
+ export type CollectionSort = 'newest' | 'popular' | 'price_low' | 'price_high';
10
+
11
+ export interface FetchCollectionProductsOptions {
12
+ page?: number;
13
+ perPage?: number;
14
+ sort?: CollectionSort;
15
+ keyword?: string;
16
+ }
17
+
18
+ // SSR collection reads go through `/client/store/*` so ResolveClientContext forces
19
+ // X-Platform=storefront (base prices, no marketplace commission) — never the
20
+ // legacy v1 routes shared with the commission-bearing marketplace.
21
+ export async function fetchCollections(vendorSlug: string, storefrontOnly = true): Promise<Collection[]> {
22
+ const client = createServerStoreClient(vendorSlug, await getRequestOrigin());
23
+
24
+ try {
25
+ const response = await client.get<ApiResponse<Collection[]>>(`/collections`, {
26
+ storefront: storefrontOnly ? 1 : 0,
27
+ });
28
+
29
+ return response.data ?? [];
30
+ } catch (error) {
31
+ // A timeout here cascades: the collection page reads null and calls
32
+ // notFound(), 404-ing a collection that exists.
33
+ return swallowNotFound(error) ?? [];
34
+ }
35
+ }
36
+
37
+ export async function fetchCollection(vendorSlug: string, slug: string): Promise<Collection | null> {
38
+ const collections = await fetchCollections(vendorSlug);
39
+
40
+ return collections.find((collection) => collection.slug === slug) ?? null;
41
+ }
42
+
43
+ export async function fetchCollectionProducts(
44
+ vendorSlug: string,
45
+ slug: string,
46
+ options?: FetchCollectionProductsOptions,
47
+ ): Promise<{
48
+ collection: Collection | null;
49
+ products: ReturnType<typeof normalizeProduct>[];
50
+ pagination?: Pagination;
51
+ }> {
52
+ const client = createServerStoreClient(vendorSlug, await getRequestOrigin());
53
+
54
+ const query: Record<string, string | number> = {};
55
+ if (options?.sort) query.sort = options.sort;
56
+ if (options?.keyword?.trim()) query.keyword = options.keyword.trim();
57
+ if (options?.perPage) query.per_page = options.perPage;
58
+ // Laravel's paginator reads the CURRENT page off the `page` query param
59
+ // implicitly (there is no `page` field in VendorCollectionProductsRequest's
60
+ // own validation rules) — it must be sent as a plain query param, not
61
+ // folded into the validated filters the backend actually reads.
62
+ if (options?.page && options.page > 1) query.page = options.page;
63
+
64
+ try {
65
+ const [collection, response] = await Promise.all([
66
+ fetchCollection(vendorSlug, slug),
67
+ client.get<ApiResponse<unknown, Record<string, unknown>>>(`/collections/${slug}/products`, query),
68
+ ]);
69
+
70
+ const meta = response.meta ?? {};
71
+
72
+ return {
73
+ collection,
74
+ products: extractProducts(response.data).map((product) => normalizeProduct(product)),
75
+ pagination: {
76
+ current_page: (meta.current_page as number) ?? options?.page ?? 1,
77
+ per_page: (meta.per_page as number) ?? options?.perPage,
78
+ // No `total`/`last_page` — this endpoint uses `simplePaginate()`
79
+ // (deliberately, to avoid a COUNT query on large catalogues), so
80
+ // `links.next` (a URL or null) is the only real "is there another
81
+ // page" signal. `CollectionPager` already treats total/last_page as
82
+ // optional and falls back to a plain "Page N" label without them.
83
+ has_more: Boolean(response.links?.next),
84
+ next_page_url: response.links?.next ?? null,
85
+ prev_page_url: response.links?.prev ?? null,
86
+ },
87
+ };
88
+ } catch (error) {
89
+ swallowNotFound(error);
90
+
91
+ return { collection: null, products: [] };
92
+ }
93
+ }
package/api/orders.ts ADDED
@@ -0,0 +1,23 @@
1
+ import { createBrowserClient } from '../sdk/client';
2
+ import type { OrderListResponse, OrderDetail } from '../types/order';
3
+
4
+ export async function fetchOrders(
5
+ vendorSlug: string,
6
+ page = 1,
7
+ status?: string,
8
+ ): Promise<OrderListResponse> {
9
+ const client = createBrowserClient(vendorSlug);
10
+ return client.get<OrderListResponse>('/orders', {
11
+ page,
12
+ ...(status && status !== 'all' ? { status } : {}),
13
+ });
14
+ }
15
+
16
+ export async function fetchOrderDetail(
17
+ vendorSlug: string,
18
+ id: string,
19
+ ): Promise<OrderDetail> {
20
+ const client = createBrowserClient(vendorSlug);
21
+ const res = await client.get<{ data: OrderDetail }>(`/orders/${id}`);
22
+ return res.data;
23
+ }
@@ -0,0 +1,32 @@
1
+ 'use client';
2
+
3
+ import { gtagPush } from './gtag';
4
+
5
+ /**
6
+ * Not gated on consent here — Consent Mode (see analytics.tsx) is what
7
+ * decides whether gtag.js actually transmits anything for a denied visitor.
8
+ * Callers push events unconditionally; Google's library filters them.
9
+ */
10
+ interface GaItem {
11
+ item_id: string;
12
+ item_name: string;
13
+ price: number;
14
+ quantity?: number;
15
+ }
16
+
17
+ export function pushViewItem(item: GaItem, currency: string): void {
18
+ gtagPush('event', 'view_item', { currency, value: item.price, items: [item] });
19
+ }
20
+
21
+ export function pushAddToCart(item: GaItem, currency: string): void {
22
+ gtagPush('event', 'add_to_cart', { currency, value: item.price * (item.quantity ?? 1), items: [item] });
23
+ }
24
+
25
+ export function pushPurchase(args: { transactionId: string; value: number; currency: string; items: GaItem[] }): void {
26
+ gtagPush('event', 'purchase', {
27
+ transaction_id: args.transactionId,
28
+ value: args.value,
29
+ currency: args.currency,
30
+ items: args.items,
31
+ });
32
+ }