@kotao/storefront 0.2.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 (66) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +34 -0
  3. package/dist/cart-documents-CwwRTvm8.d.ts +21 -0
  4. package/dist/cart-documents.d.ts +2 -0
  5. package/dist/cart-documents.js +3 -0
  6. package/dist/cart-documents.js.map +1 -0
  7. package/dist/chunk-2IWNUDSZ.js +104 -0
  8. package/dist/chunk-2IWNUDSZ.js.map +1 -0
  9. package/dist/chunk-3YQXJNR7.js +70 -0
  10. package/dist/chunk-3YQXJNR7.js.map +1 -0
  11. package/dist/chunk-5QOFKGKD.js +51 -0
  12. package/dist/chunk-5QOFKGKD.js.map +1 -0
  13. package/dist/chunk-GCQI6OKH.js +239 -0
  14. package/dist/chunk-GCQI6OKH.js.map +1 -0
  15. package/dist/chunk-ISAFEOR2.js +214 -0
  16. package/dist/chunk-ISAFEOR2.js.map +1 -0
  17. package/dist/chunk-MYDKIJRH.js +81 -0
  18. package/dist/chunk-MYDKIJRH.js.map +1 -0
  19. package/dist/chunk-OGWYTYYS.js +72 -0
  20. package/dist/chunk-OGWYTYYS.js.map +1 -0
  21. package/dist/chunk-PXZ737N3.js +439 -0
  22. package/dist/chunk-PXZ737N3.js.map +1 -0
  23. package/dist/chunk-QOEEUTHQ.js +110 -0
  24. package/dist/chunk-QOEEUTHQ.js.map +1 -0
  25. package/dist/chunk-U5677AEA.js +1690 -0
  26. package/dist/chunk-U5677AEA.js.map +1 -0
  27. package/dist/chunk-XMTWFJCZ.js +138 -0
  28. package/dist/chunk-XMTWFJCZ.js.map +1 -0
  29. package/dist/client-D9YYA2w6.d.ts +159 -0
  30. package/dist/customer-account-api-types.d.ts +144 -0
  31. package/dist/customer-account-api-types.js +3 -0
  32. package/dist/customer-account-api-types.js.map +1 -0
  33. package/dist/editor.d.ts +112 -0
  34. package/dist/editor.js +3 -0
  35. package/dist/editor.js.map +1 -0
  36. package/dist/image.d.ts +89 -0
  37. package/dist/image.js +3 -0
  38. package/dist/image.js.map +1 -0
  39. package/dist/index.d.ts +183 -0
  40. package/dist/index.js +208 -0
  41. package/dist/index.js.map +1 -0
  42. package/dist/pagination-CPgH3fAa.d.ts +77 -0
  43. package/dist/product.d.ts +105 -0
  44. package/dist/product.js +4 -0
  45. package/dist/product.js.map +1 -0
  46. package/dist/react-router.d.ts +29 -0
  47. package/dist/react-router.js +33 -0
  48. package/dist/react-router.js.map +1 -0
  49. package/dist/react.d.ts +299 -0
  50. package/dist/react.js +1813 -0
  51. package/dist/react.js.map +1 -0
  52. package/dist/routing-BfUp49o8.d.ts +302 -0
  53. package/dist/seo.d.ts +155 -0
  54. package/dist/seo.js +4 -0
  55. package/dist/seo.js.map +1 -0
  56. package/dist/settings.d.ts +87 -0
  57. package/dist/settings.js +3 -0
  58. package/dist/settings.js.map +1 -0
  59. package/dist/shop-DkQmro-Y.d.ts +202 -0
  60. package/dist/store-DQK1nhob.d.ts +67 -0
  61. package/dist/storefront-api-types.d.ts +312 -0
  62. package/dist/storefront-api-types.js +3 -0
  63. package/dist/storefront-api-types.js.map +1 -0
  64. package/dist/types-BDhHWZsP.d.ts +77 -0
  65. package/dist/types-Df3bGbKU.d.ts +167 -0
  66. package/package.json +82 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Kotao
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,34 @@
1
+ # @kotao/storefront
2
+
3
+ Framework-agnostic theme SDK runtime for Kotao Storefronts (ADR-0003). It mirrors Hydrogen's
4
+ API so a Hydrogen → Kotao migration is largely mechanical, and papers over the four Cloudflare
5
+ Workers-for-Platforms untrusted-mode realities (no `request.cf`, no `caches.default`, no `eval`,
6
+ frozen intra-request clock).
7
+
8
+ ## API mapping
9
+
10
+ | Hydrogen | Kotao | Notes |
11
+ | ---------------------------------------- | -------------------------------------------------------- | ----------------------------------------------------------------- |
12
+ | `createStorefrontClient` | `createStorefrontClient` | typed GraphQL; SWR-cached `query`, uncached `mutate` |
13
+ | `createWithCache` | `createWithCache` | SWR caching for third-party data |
14
+ | `CacheShort` / `CacheLong` / `CacheNone` | `CacheShort` / `CacheLong` / `CacheNone` / `CacheCustom` | plus `StorefrontDefaultCache` (fresh 1s / serve ~1 day) |
15
+ | `createHydrogenContext` | `createKotaoContext` | storefront + geo + cache + locale + session seam |
16
+ | Oxygen `request.cf` | `context.geo` | backed by signed `x-kotao-*` headers; `null` if unsigned/tampered |
17
+ | (Oxygen-managed) | `@kotao/storefront/react-router` | RR7 `getKotaoLoadContext` |
18
+
19
+ The one real migration diff is `request.cf` → `context.geo`.
20
+
21
+ ## Entry points
22
+
23
+ - `@kotao/storefront` — framework-agnostic core (no React Router import).
24
+ - `@kotao/storefront/react-router` — the React Router 7 binding (`getKotaoLoadContext`).
25
+
26
+ ## Signed geo contract
27
+
28
+ `signGeo` (used by the Gateway Worker, #81) and `verifyGeo` (used here) live together so the
29
+ `x-kotao-geo` / `x-kotao-geo-signature` HMAC-SHA256 contract can never drift. The signing key is
30
+ `env.STOREFRONT_GEO_SIGNING_KEY`. Any missing/invalid signature yields `null` — unsigned data is
31
+ never trusted.
32
+
33
+ Status: #79 ships the core runtime. The Storefront API GraphQL schema + typed codegen, cart,
34
+ SEO, analytics, and customer-account surfaces are tracked as separate issues.
@@ -0,0 +1,21 @@
1
+ import { CartAdapter } from '@kotao/smart-cart';
2
+
3
+ /**
4
+ * All fully-resolved cart GraphQL document strings from the react seam adapter.
5
+ * Exported for schema-validation tests (apps/storefront-api/test/cart-graphql-documents.test.ts)
6
+ * that submit each document to a real graphql-yoga instance and assert zero validation errors,
7
+ * catching invalid field selections (e.g. subfields on a scalar) at test time rather than runtime.
8
+ */
9
+ declare const REACT_SEAM_CART_DOCUMENTS: readonly string[];
10
+ /**
11
+ * A CartAdapter that drives the storefront-api cart over the same-origin /api/storefront GraphQL proxy
12
+ * (the proxy injects the read token + resolves the shop server-side). The theme supplies the cart id
13
+ * (from its loader's cart cookie/session) and renders <CartDrawerOverlay adapter={this} />.
14
+ */
15
+ declare function createStorefrontApiCartAdapter(opts: {
16
+ readonly cartId: string;
17
+ readonly apiPath?: string;
18
+ readonly fetchImpl?: typeof fetch;
19
+ }): CartAdapter;
20
+
21
+ export { REACT_SEAM_CART_DOCUMENTS as R, createStorefrontApiCartAdapter as c };
@@ -0,0 +1,2 @@
1
+ export { R as REACT_SEAM_CART_DOCUMENTS } from './cart-documents-CwwRTvm8.js';
2
+ import '@kotao/smart-cart';
@@ -0,0 +1,3 @@
1
+ export { REACT_SEAM_CART_DOCUMENTS } from './chunk-2IWNUDSZ.js';
2
+ //# sourceMappingURL=cart-documents.js.map
3
+ //# sourceMappingURL=cart-documents.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"cart-documents.js"}
@@ -0,0 +1,104 @@
1
+ // src/react/storefront-cart-adapter.ts
2
+ var toMinorUnits = (m) => Math.round(parseFloat(m.amount) * 100);
3
+ var CART_FIELDS = `
4
+ checkoutUrl
5
+ cost { subtotalAmount { amount currencyCode } discountTotal { amount currencyCode } }
6
+ discountCodes
7
+ lines(first: 250) { nodes {
8
+ id quantity
9
+ attributes { key value }
10
+ cost { totalAmount { amount currencyCode } }
11
+ merchandise { ... on ProductVariant { id title image { url } product { title } } }
12
+ } }`;
13
+ var CART_QUERY = `query KotaoSmartCart($cartId: ID!) { cart(id: $cartId) { ${CART_FIELDS} } }`;
14
+ var LINES_ADD = `mutation KotaoSmartCartAdd($cartId: ID!, $lines: [CartLineInput!]!) { cartLinesAdd(cartId: $cartId, lines: $lines) { cart { ${CART_FIELDS} } } }`;
15
+ var LINES_UPDATE = `mutation KotaoSmartCartUpdate($cartId: ID!, $lines: [CartLineUpdateInput!]!) { cartLinesUpdate(cartId: $cartId, lines: $lines) { cart { ${CART_FIELDS} } } }`;
16
+ var LINES_REMOVE = `mutation KotaoSmartCartRemove($cartId: ID!, $lineIds: [ID!]!) { cartLinesRemove(cartId: $cartId, lineIds: $lineIds) { cart { ${CART_FIELDS} } } }`;
17
+ var DISCOUNT_CODES_UPDATE = `mutation KotaoSmartCartDiscountCodes($cartId: ID!, $discountCodes: [String!]!) { cartDiscountCodesUpdate(cartId: $cartId, discountCodes: $discountCodes) { cart { ${CART_FIELDS} } } }`;
18
+ var REACT_SEAM_CART_DOCUMENTS = [
19
+ CART_QUERY,
20
+ LINES_ADD,
21
+ LINES_UPDATE,
22
+ LINES_REMOVE,
23
+ DISCOUNT_CODES_UPDATE
24
+ ];
25
+ function mapCart(c) {
26
+ const currency = c.cost.subtotalAmount.currencyCode;
27
+ const lines = c.lines.nodes.map((l) => {
28
+ const attributes = {};
29
+ for (const a of l.attributes ?? []) {
30
+ if (a.value !== null) attributes[a.key] = a.value;
31
+ }
32
+ const hasAttributes = Object.keys(attributes).length > 0;
33
+ return {
34
+ id: l.id,
35
+ title: l.merchandise.product.title,
36
+ variantTitle: l.merchandise.title,
37
+ variantId: l.merchandise.id ?? null,
38
+ quantity: l.quantity,
39
+ imageUrl: l.merchandise.image?.url ?? null,
40
+ linePrice: { amount: toMinorUnits(l.cost.totalAmount), currency },
41
+ ...hasAttributes && { attributes }
42
+ };
43
+ });
44
+ const discountMinorUnits = c.cost.discountTotal != null ? toMinorUnits(c.cost.discountTotal) : 0;
45
+ const appliedCodes = c.discountCodes?.filter((code) => code.length > 0);
46
+ return {
47
+ lines,
48
+ subtotal: { amount: toMinorUnits(c.cost.subtotalAmount), currency },
49
+ checkoutUrl: c.checkoutUrl,
50
+ currency,
51
+ ...discountMinorUnits > 0 && {
52
+ discountTotal: { amount: discountMinorUnits, currency }
53
+ },
54
+ ...appliedCodes != null && appliedCodes.length > 0 && { appliedCodes }
55
+ };
56
+ }
57
+ function createStorefrontApiCartAdapter(opts) {
58
+ const fetchImpl = opts.fetchImpl ?? fetch;
59
+ const apiPath = opts.apiPath ?? "/api/storefront";
60
+ const run = async (query, variables, key) => {
61
+ const res = await fetchImpl(apiPath, {
62
+ method: "POST",
63
+ headers: { "content-type": "application/json", accept: "application/json" },
64
+ body: JSON.stringify({ query, variables })
65
+ });
66
+ if (!res.ok) {
67
+ throw new Error(`storefront cart request failed: ${res.status} ${res.statusText}`);
68
+ }
69
+ const json = await res.json();
70
+ const payload = json.data?.[key];
71
+ const cart = payload && "cart" in payload ? payload.cart : payload;
72
+ return mapCart(cart);
73
+ };
74
+ return {
75
+ getCart: () => run(CART_QUERY, { cartId: opts.cartId }, "cart"),
76
+ addLine: (merchandiseId, quantity, attributes) => run(
77
+ LINES_ADD,
78
+ {
79
+ cartId: opts.cartId,
80
+ lines: [
81
+ {
82
+ merchandiseId,
83
+ quantity,
84
+ ...attributes !== void 0 && {
85
+ attributes: Object.entries(attributes).map(([key, value]) => ({ key, value }))
86
+ }
87
+ }
88
+ ]
89
+ },
90
+ "cartLinesAdd"
91
+ ),
92
+ updateLine: (id, quantity) => run(LINES_UPDATE, { cartId: opts.cartId, lines: [{ id, quantity }] }, "cartLinesUpdate"),
93
+ removeLine: (id) => run(LINES_REMOVE, { cartId: opts.cartId, lineIds: [id] }, "cartLinesRemove"),
94
+ updateDiscountCodes: (codes) => run(
95
+ DISCOUNT_CODES_UPDATE,
96
+ { cartId: opts.cartId, discountCodes: codes },
97
+ "cartDiscountCodesUpdate"
98
+ )
99
+ };
100
+ }
101
+
102
+ export { REACT_SEAM_CART_DOCUMENTS, createStorefrontApiCartAdapter };
103
+ //# sourceMappingURL=chunk-2IWNUDSZ.js.map
104
+ //# sourceMappingURL=chunk-2IWNUDSZ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/react/storefront-cart-adapter.ts"],"names":[],"mappings":"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","file":"chunk-2IWNUDSZ.js","sourcesContent":["import type { CartAdapter, SmartCartLine, SmartCartState } from '@kotao/smart-cart'\n\ninterface GqlMoney {\n amount: string\n currencyCode: string\n}\ninterface GqlCartLine {\n id: string\n quantity: number\n attributes: { key: string; value: string | null }[] | null\n cost: { totalAmount: GqlMoney }\n merchandise: {\n id: string\n title: string | null\n image: { url: string } | null\n product: { title: string }\n }\n}\ninterface GqlCart {\n checkoutUrl: string\n cost: { subtotalAmount: GqlMoney; discountTotal?: GqlMoney }\n discountCodes?: string[]\n lines: { nodes: GqlCartLine[] }\n}\n\n// GraphQL Money is a decimal string (\"216.00\"); SmartCartState money is integer minor units.\nconst toMinorUnits = (m: GqlMoney): number => Math.round(parseFloat(m.amount) * 100)\n\nconst CART_FIELDS = `\n checkoutUrl\n cost { subtotalAmount { amount currencyCode } discountTotal { amount currencyCode } }\n discountCodes\n lines(first: 250) { nodes {\n id quantity\n attributes { key value }\n cost { totalAmount { amount currencyCode } }\n merchandise { ... on ProductVariant { id title image { url } product { title } } }\n } }`\nconst CART_QUERY = `query KotaoSmartCart($cartId: ID!) { cart(id: $cartId) { ${CART_FIELDS} } }`\nconst LINES_ADD = `mutation KotaoSmartCartAdd($cartId: ID!, $lines: [CartLineInput!]!) { cartLinesAdd(cartId: $cartId, lines: $lines) { cart { ${CART_FIELDS} } } }`\nconst LINES_UPDATE = `mutation KotaoSmartCartUpdate($cartId: ID!, $lines: [CartLineUpdateInput!]!) { cartLinesUpdate(cartId: $cartId, lines: $lines) { cart { ${CART_FIELDS} } } }`\nconst LINES_REMOVE = `mutation KotaoSmartCartRemove($cartId: ID!, $lineIds: [ID!]!) { cartLinesRemove(cartId: $cartId, lineIds: $lineIds) { cart { ${CART_FIELDS} } } }`\nconst DISCOUNT_CODES_UPDATE = `mutation KotaoSmartCartDiscountCodes($cartId: ID!, $discountCodes: [String!]!) { cartDiscountCodesUpdate(cartId: $cartId, discountCodes: $discountCodes) { cart { ${CART_FIELDS} } } }`\n\n/**\n * All fully-resolved cart GraphQL document strings from the react seam adapter.\n * Exported for schema-validation tests (apps/storefront-api/test/cart-graphql-documents.test.ts)\n * that submit each document to a real graphql-yoga instance and assert zero validation errors,\n * catching invalid field selections (e.g. subfields on a scalar) at test time rather than runtime.\n */\nexport const REACT_SEAM_CART_DOCUMENTS: readonly string[] = [\n CART_QUERY,\n LINES_ADD,\n LINES_UPDATE,\n LINES_REMOVE,\n DISCOUNT_CODES_UPDATE,\n]\n\nfunction mapCart(c: GqlCart): SmartCartState {\n const currency = c.cost.subtotalAmount.currencyCode\n const lines: SmartCartLine[] = c.lines.nodes.map((l) => {\n const attributes: Record<string, string> = {}\n for (const a of l.attributes ?? []) {\n if (a.value !== null) attributes[a.key] = a.value\n }\n const hasAttributes = Object.keys(attributes).length > 0\n return {\n id: l.id,\n title: l.merchandise.product.title,\n variantTitle: l.merchandise.title,\n variantId: l.merchandise.id ?? null,\n quantity: l.quantity,\n imageUrl: l.merchandise.image?.url ?? null,\n linePrice: { amount: toMinorUnits(l.cost.totalAmount), currency },\n ...(hasAttributes && { attributes }),\n }\n })\n const discountMinorUnits = c.cost.discountTotal != null ? toMinorUnits(c.cost.discountTotal) : 0\n const appliedCodes = c.discountCodes?.filter((code) => code.length > 0)\n return {\n lines,\n subtotal: { amount: toMinorUnits(c.cost.subtotalAmount), currency },\n checkoutUrl: c.checkoutUrl,\n currency,\n ...(discountMinorUnits > 0 && {\n discountTotal: { amount: discountMinorUnits, currency },\n }),\n ...(appliedCodes != null && appliedCodes.length > 0 && { appliedCodes }),\n }\n}\n\n/**\n * A CartAdapter that drives the storefront-api cart over the same-origin /api/storefront GraphQL proxy\n * (the proxy injects the read token + resolves the shop server-side). The theme supplies the cart id\n * (from its loader's cart cookie/session) and renders <CartDrawerOverlay adapter={this} />.\n */\nexport function createStorefrontApiCartAdapter(opts: {\n readonly cartId: string\n readonly apiPath?: string\n readonly fetchImpl?: typeof fetch\n}): CartAdapter {\n const fetchImpl = opts.fetchImpl ?? fetch\n const apiPath = opts.apiPath ?? '/api/storefront'\n const run = async (\n query: string,\n variables: Record<string, unknown>,\n key: string,\n ): Promise<SmartCartState> => {\n const res = await fetchImpl(apiPath, {\n method: 'POST',\n headers: { 'content-type': 'application/json', accept: 'application/json' },\n body: JSON.stringify({ query, variables }),\n })\n if (!res.ok) {\n throw new Error(`storefront cart request failed: ${res.status} ${res.statusText}`)\n }\n const json = (await res.json()) as { data?: Record<string, unknown> }\n const payload = json.data?.[key] as GqlCart | { cart: GqlCart } | undefined\n const cart = payload && 'cart' in payload ? payload.cart : (payload as GqlCart)\n return mapCart(cart)\n }\n return {\n getCart: () => run(CART_QUERY, { cartId: opts.cartId }, 'cart'),\n addLine: (merchandiseId, quantity, attributes) =>\n run(\n LINES_ADD,\n {\n cartId: opts.cartId,\n lines: [\n {\n merchandiseId,\n quantity,\n ...(attributes !== undefined && {\n attributes: Object.entries(attributes).map(([key, value]) => ({ key, value })),\n }),\n },\n ],\n },\n 'cartLinesAdd',\n ),\n updateLine: (id, quantity) =>\n run(LINES_UPDATE, { cartId: opts.cartId, lines: [{ id, quantity }] }, 'cartLinesUpdate'),\n removeLine: (id) =>\n run(LINES_REMOVE, { cartId: opts.cartId, lineIds: [id] }, 'cartLinesRemove'),\n updateDiscountCodes: (codes) =>\n run(\n DISCOUNT_CODES_UPDATE,\n { cartId: opts.cartId, discountCodes: codes },\n 'cartDiscountCodesUpdate',\n ),\n }\n}\n"]}
@@ -0,0 +1,70 @@
1
+ // src/image/image-url.ts
2
+ var CDN_CGI = "/cdn-cgi/image/";
3
+ var isCdnUrl = (url) => /^https?:\/\/cdn\.kotao\./.test(url);
4
+ function splitCdnCgi(url) {
5
+ const u = new URL(url);
6
+ if (!u.pathname.startsWith(CDN_CGI)) return { bare: url, opts: new URLSearchParams() };
7
+ const rest = u.pathname.slice(CDN_CGI.length);
8
+ const slash = rest.indexOf("/");
9
+ const optStr = slash === -1 ? rest : rest.slice(0, slash);
10
+ const path = slash === -1 ? "" : rest.slice(slash);
11
+ const opts = new URLSearchParams();
12
+ for (const pair of optStr.split(",")) {
13
+ const [k, v] = pair.split("=");
14
+ if (k && v) opts.set(k, v);
15
+ }
16
+ return { bare: `${u.origin}${path}`, opts };
17
+ }
18
+ var OPT_ORDER = ["width", "height", "fit", "format", "quality"];
19
+ function imageUrl(url, options = {}) {
20
+ if (!isCdnUrl(url)) return url;
21
+ const { bare, opts } = splitCdnCgi(url);
22
+ if (options.width != null) opts.set("width", String(options.width));
23
+ if (options.height != null) opts.set("height", String(options.height));
24
+ if (options.fit != null) opts.set("fit", options.fit);
25
+ if (options.format != null) opts.set("format", options.format);
26
+ if (options.quality != null) opts.set("quality", String(options.quality));
27
+ const ordered = OPT_ORDER.filter((k) => opts.has(k)).map((k) => `${k}=${opts.get(k)}`);
28
+ if (ordered.length === 0) return bare;
29
+ const u = new URL(bare);
30
+ return `${u.origin}${CDN_CGI}${ordered.join(",")}${u.pathname}`;
31
+ }
32
+ function imageSrcSet(url, options) {
33
+ const { widths, ...rest } = options;
34
+ return widths.map((width) => `${imageUrl(url, { ...rest, width })} ${width}w`).join(", ");
35
+ }
36
+
37
+ // src/image/image-attrs.ts
38
+ var DEFAULT_WIDTHS = [320, 640, 768, 1024, 1280, 1536];
39
+ var FALLBACK_WIDTH = 1280;
40
+ function buildImageAttrs(options) {
41
+ const url = options.src ?? options.data.url;
42
+ const widths = options.srcSetWidths ?? (options.width ? [options.width, options.width * 2, options.width * 3] : DEFAULT_WIDTHS);
43
+ const aspectRatio = options.aspectRatio ?? (options.data.width != null && options.data.height != null ? `${options.data.width}/${options.data.height}` : void 0);
44
+ const transform = {};
45
+ if (options.crop) transform.fit = "cover";
46
+ if (options.format != null) transform.format = options.format;
47
+ if (options.quality != null) transform.quality = options.quality;
48
+ const heightPerWidth = options.width != null && options.height != null ? options.height / options.width : null;
49
+ const candidate = (w) => {
50
+ const opts = { ...transform, width: w };
51
+ if (heightPerWidth != null) opts.height = Math.round(w * heightPerWidth);
52
+ return opts;
53
+ };
54
+ const srcOptions = { ...transform, width: options.width ?? FALLBACK_WIDTH };
55
+ if (options.height != null) srcOptions.height = options.height;
56
+ return {
57
+ src: imageUrl(url, srcOptions),
58
+ srcSet: widths.map((w) => `${imageUrl(url, candidate(w))} ${w}w`).join(", "),
59
+ ...options.sizes != null ? { sizes: options.sizes } : {},
60
+ ...options.width != null ? { width: options.width } : {},
61
+ ...options.height != null ? { height: options.height } : {},
62
+ alt: options.data.altText ?? "",
63
+ loading: options.loading ?? "lazy",
64
+ ...aspectRatio != null ? { style: { aspectRatio } } : {}
65
+ };
66
+ }
67
+
68
+ export { buildImageAttrs, imageSrcSet, imageUrl };
69
+ //# sourceMappingURL=chunk-3YQXJNR7.js.map
70
+ //# sourceMappingURL=chunk-3YQXJNR7.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/image/image-url.ts","../src/image/image-attrs.ts"],"names":[],"mappings":"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","file":"chunk-3YQXJNR7.js","sourcesContent":["/**\n * Framework-agnostic image-URL builders for the storefront SDK (ADR-0003 §SDK image-URL contract).\n * They build Cloudflare `/cdn-cgi/image/<opts>/<path>` path-form transform URLs for `https://cdn.kotao.*`\n * bases, allowing the CDN to serve optimized renderings. Non-CDN URLs (external, data:, theme `/assets/...`)\n * pass through unchanged. Wire-native params — `width`/`height`/`fit`/`format`/`quality` — matching\n * Cloudflare's native image resizing; a Hydrogen-shaped React `<Image>` (with `crop`, `srcSet`/`sizes`\n * props) layers on top in the framework presets (#92). Pure string building, no framework imports, so it\n * stays in the SDK core.\n */\n\n/** Cloudflare-native resize mode (the Gateway's `fit` param). */\nexport type ImageFit = 'scale-down' | 'contain' | 'pad' | 'squeeze' | 'cover' | 'crop'\n/** Output format query value (the Gateway maps it to a content-type). */\nexport type ImageFormat = 'webp' | 'avif' | 'jpeg' | 'png'\n\nexport interface ImageUrlOptions {\n width?: number\n height?: number\n fit?: ImageFit\n format?: ImageFormat\n /** 1–100. */\n quality?: number\n}\n\nconst CDN_CGI = '/cdn-cgi/image/'\nconst isCdnUrl = (url: string): boolean => /^https?:\\/\\/cdn\\.kotao\\./.test(url)\n\n/** Strip a `/cdn-cgi/image/<opts>/` segment back to `{origin}{path}`, returning [bareUrl, existingOpts]. */\nfunction splitCdnCgi(url: string): { bare: string; opts: URLSearchParams } {\n const u = new URL(url)\n if (!u.pathname.startsWith(CDN_CGI)) return { bare: url, opts: new URLSearchParams() }\n const rest = u.pathname.slice(CDN_CGI.length)\n const slash = rest.indexOf('/')\n const optStr = slash === -1 ? rest : rest.slice(0, slash)\n const path = slash === -1 ? '' : rest.slice(slash) // includes leading '/'\n const opts = new URLSearchParams()\n for (const pair of optStr.split(',')) {\n const [k, v] = pair.split('=')\n if (k && v) opts.set(k, v)\n }\n return { bare: `${u.origin}${path}`, opts }\n}\n\nconst OPT_ORDER = ['width', 'height', 'fit', 'format', 'quality'] as const\n\n/**\n * Build a Cloudflare `/cdn-cgi/image/` path-form transform URL for a CDN image. For transformable\n * URLs (absolute `https://cdn.kotao.*`), emits `/cdn-cgi/image/{opts}/{path}` where `{opts}` is\n * the comma-joined key=value list (in stable order: width,height,fit,format,quality) and `{path}`\n * is the original pathname. Chaining replaces any existing `/cdn-cgi/image/` segment.\n * Non-CDN URLs pass through unchanged.\n */\nexport function imageUrl(url: string, options: ImageUrlOptions = {}): string {\n if (!isCdnUrl(url)) return url\n const { bare, opts } = splitCdnCgi(url)\n if (options.width != null) opts.set('width', String(options.width))\n if (options.height != null) opts.set('height', String(options.height))\n if (options.fit != null) opts.set('fit', options.fit)\n if (options.format != null) opts.set('format', options.format)\n if (options.quality != null) opts.set('quality', String(options.quality))\n const ordered = OPT_ORDER.filter((k) => opts.has(k)).map((k) => `${k}=${opts.get(k)}`)\n if (ordered.length === 0) return bare\n const u = new URL(bare)\n return `${u.origin}${CDN_CGI}${ordered.join(',')}${u.pathname}`\n}\n\nexport interface ImageSrcSetOptions extends Omit<ImageUrlOptions, 'width'> {\n /** Candidate widths (px) for the `srcset` descriptors. */\n widths: number[]\n}\n\n/**\n * Build a `srcset` string — one `url width` candidate per width — for responsive `<img srcset>` /\n * `<source srcset>`. Pair with a `sizes` attribute the theme controls.\n */\nexport function imageSrcSet(url: string, options: ImageSrcSetOptions): string {\n const { widths, ...rest } = options\n return widths.map((width) => `${imageUrl(url, { ...rest, width })} ${width}w`).join(', ')\n}\n","/**\n * Framework-agnostic `<img>` attribute builder (ADR-0003 §SDK image-URL contract). Turns the\n * Hydrogen-shaped `<Image>` surface (data/aspectRatio/crop/sizes/width/height) into concrete img\n * attributes (src, srcSet, sizes, width, height, style) over the `cdn.kotao.*` R2-native transform\n * contract, via the pure `imageUrl`/`imageSrcSet` builders. The React `<Image>` is a thin wrapper\n * that spreads these — so all logic lives here, in the core, unit-tested without a DOM.\n */\nimport { imageUrl, type ImageFormat, type ImageUrlOptions } from './image-url'\n\nexport interface ImageData {\n url: string\n altText?: string | null\n width?: number | null\n height?: number | null\n}\n\n/** Hydrogen crop region. v1 maps any crop to a fill+crop (Cloudflare `fit=cover`); positional gravity\n * (top/bottom/left/right) is not on the wire yet, so all values currently centre-crop. */\nexport type ImageCrop = 'center' | 'top' | 'bottom' | 'left' | 'right'\n\n// Optionals are `| undefined` on purpose: the React <Image> feeds them straight from optional props,\n// so explicit `undefined` must be assignable under exactOptionalPropertyTypes.\nexport interface ImageAttrsOptions {\n data: ImageData\n /** Override `data.url`. */\n src?: string | undefined\n /** \"width/height\" (e.g. \"4/5\") — sets the intrinsic box for CLS-free layout. */\n aspectRatio?: string | undefined\n crop?: ImageCrop | undefined\n /** The responsive `sizes` attribute; the theme owns it. */\n sizes?: string | undefined\n /** Fixed render width (px). With it, the srcSet becomes 1x/2x/3x instead of the responsive set. */\n width?: number | undefined\n height?: number | undefined\n loading?: ('eager' | 'lazy') | undefined\n /** srcSet candidate widths; defaults to a responsive set (or 1x/2x/3x when `width` is fixed). */\n srcSetWidths?: number[] | undefined\n format?: ImageFormat | undefined\n quality?: number | undefined\n}\n\nexport interface ImageAttrs {\n src: string\n srcSet: string\n sizes?: string\n width?: number\n height?: number\n alt: string\n loading: 'eager' | 'lazy'\n style?: { aspectRatio: string }\n}\n\n/** Responsive widths when no fixed `width` is given (mirrors Hydrogen's default interval set). */\nconst DEFAULT_WIDTHS = [320, 640, 768, 1024, 1280, 1536]\n/** The `src` fallback width for browsers that ignore srcSet. */\nconst FALLBACK_WIDTH = 1280\n\nexport function buildImageAttrs(options: ImageAttrsOptions): ImageAttrs {\n const url = options.src ?? options.data.url\n\n const widths =\n options.srcSetWidths ??\n (options.width ? [options.width, options.width * 2, options.width * 3] : DEFAULT_WIDTHS)\n\n const aspectRatio =\n options.aspectRatio ??\n (options.data.width != null && options.data.height != null\n ? `${options.data.width}/${options.data.height}`\n : undefined)\n\n // Shared transform params — set only when present (exactOptionalPropertyTypes forbids explicit\n // `undefined`). A crop maps to Cloudflare `fit=cover` (fill+crop).\n const transform: ImageUrlOptions = {}\n if (options.crop) transform.fit = 'cover'\n if (options.format != null) transform.format = options.format\n if (options.quality != null) transform.quality = options.quality\n\n // For a fixed box (width+height), the srcSet candidates must scale BOTH dimensions so every DPR\n // variant keeps the same crop aspect (a uniform height would distort the 2x/3x candidates).\n const heightPerWidth =\n options.width != null && options.height != null ? options.height / options.width : null\n const candidate = (w: number): ImageUrlOptions => {\n const opts: ImageUrlOptions = { ...transform, width: w }\n if (heightPerWidth != null) opts.height = Math.round(w * heightPerWidth)\n return opts\n }\n\n const srcOptions: ImageUrlOptions = { ...transform, width: options.width ?? FALLBACK_WIDTH }\n if (options.height != null) srcOptions.height = options.height\n\n return {\n src: imageUrl(url, srcOptions),\n srcSet: widths.map((w) => `${imageUrl(url, candidate(w))} ${w}w`).join(', '),\n ...(options.sizes != null ? { sizes: options.sizes } : {}),\n ...(options.width != null ? { width: options.width } : {}),\n ...(options.height != null ? { height: options.height } : {}),\n alt: options.data.altText ?? '',\n loading: options.loading ?? 'lazy',\n ...(aspectRatio != null ? { style: { aspectRatio } } : {}),\n }\n}\n"]}
@@ -0,0 +1,51 @@
1
+ // src/pagination/pagination.ts
2
+ function flattenConnection(connection) {
3
+ if (connection == null) return [];
4
+ if (connection.nodes != null) return [...connection.nodes];
5
+ return (connection.edges ?? []).flatMap((edge) => {
6
+ const node = edge?.node;
7
+ return node == null ? [] : [node];
8
+ });
9
+ }
10
+ var paramNames = (namespace) => {
11
+ const prefix = namespace ? `${namespace}_` : "";
12
+ return { direction: `${prefix}direction`, cursor: `${prefix}cursor` };
13
+ };
14
+ var toSearchParams = (input) => {
15
+ if (input instanceof URLSearchParams) return input;
16
+ if (input instanceof URL) return input.searchParams;
17
+ if (typeof input === "string") return new URL(input).searchParams;
18
+ return new URL(input.url).searchParams;
19
+ };
20
+ function getPaginationVariables(input, options) {
21
+ const pageSize = options.pageSize ?? options.pageBy ?? 20;
22
+ const params = toSearchParams(input);
23
+ const names = paramNames(options.namespace);
24
+ const direction = params.get(names.direction);
25
+ const cursor = params.get(names.cursor);
26
+ if (cursor && direction === "previous") return { last: pageSize, startCursor: cursor };
27
+ if (cursor && direction === "next") return { first: pageSize, endCursor: cursor };
28
+ return { first: pageSize };
29
+ }
30
+ var pageUrl = (current, names, direction, cursor) => {
31
+ const url = new URL(current.toString());
32
+ url.searchParams.set(names.direction, direction);
33
+ url.searchParams.set(names.cursor, cursor);
34
+ return url.toString();
35
+ };
36
+ function getPaginationInfo(connection, currentUrl, options = {}) {
37
+ const url = typeof currentUrl === "string" ? new URL(currentUrl) : currentUrl;
38
+ const names = paramNames(options.namespace);
39
+ const { pageInfo } = connection;
40
+ return {
41
+ nodes: connection.nodes,
42
+ hasNextPage: pageInfo.hasNextPage,
43
+ hasPreviousPage: pageInfo.hasPreviousPage,
44
+ ...pageInfo.hasNextPage && pageInfo.endCursor ? { nextPageUrl: pageUrl(url, names, "next", pageInfo.endCursor) } : {},
45
+ ...pageInfo.hasPreviousPage && pageInfo.startCursor ? { prevPageUrl: pageUrl(url, names, "previous", pageInfo.startCursor) } : {}
46
+ };
47
+ }
48
+
49
+ export { flattenConnection, getPaginationInfo, getPaginationVariables };
50
+ //# sourceMappingURL=chunk-5QOFKGKD.js.map
51
+ //# sourceMappingURL=chunk-5QOFKGKD.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/pagination/pagination.ts"],"names":[],"mappings":";AAuDO,SAAS,kBACd,UAAA,EACW;AACX,EAAA,IAAI,UAAA,IAAc,IAAA,EAAM,OAAO,EAAC;AAChC,EAAA,IAAI,WAAW,KAAA,IAAS,IAAA,SAAa,CAAC,GAAG,WAAW,KAAK,CAAA;AACzD,EAAA,OAAA,CAAQ,WAAW,KAAA,IAAS,EAAC,EAAG,OAAA,CAAQ,CAAC,IAAA,KAAS;AAChD,IAAA,MAAM,OAAO,IAAA,EAAM,IAAA;AACnB,IAAA,OAAO,IAAA,IAAQ,IAAA,GAAO,EAAC,GAAI,CAAC,IAAI,CAAA;AAAA,EAClC,CAAC,CAAA;AACH;AAiBA,IAAM,UAAA,GAAa,CAAC,SAAA,KAA8D;AAChF,EAAA,MAAM,MAAA,GAAS,SAAA,GAAY,CAAA,EAAG,SAAS,CAAA,CAAA,CAAA,GAAM,EAAA;AAC7C,EAAA,OAAO,EAAE,WAAW,CAAA,EAAG,MAAM,aAAa,MAAA,EAAQ,CAAA,EAAG,MAAM,CAAA,MAAA,CAAA,EAAS;AACtE,CAAA;AAEA,IAAM,cAAA,GAAiB,CAAC,KAAA,KAAqE;AAC3F,EAAA,IAAI,KAAA,YAAiB,iBAAiB,OAAO,KAAA;AAC7C,EAAA,IAAI,KAAA,YAAiB,GAAA,EAAK,OAAO,KAAA,CAAM,YAAA;AACvC,EAAA,IAAI,OAAO,KAAA,KAAU,QAAA,SAAiB,IAAI,GAAA,CAAI,KAAK,CAAA,CAAE,YAAA;AACrD,EAAA,OAAO,IAAI,GAAA,CAAI,KAAA,CAAM,GAAG,CAAA,CAAE,YAAA;AAC5B,CAAA;AAMO,SAAS,sBAAA,CACd,OACA,OAAA,EACqB;AACrB,EAAA,MAAM,QAAA,GAAW,OAAA,CAAQ,QAAA,IAAY,OAAA,CAAQ,MAAA,IAAU,EAAA;AACvD,EAAA,MAAM,MAAA,GAAS,eAAe,KAAK,CAAA;AACnC,EAAA,MAAM,KAAA,GAAQ,UAAA,CAAW,OAAA,CAAQ,SAAS,CAAA;AAC1C,EAAA,MAAM,SAAA,GAAY,MAAA,CAAO,GAAA,CAAI,KAAA,CAAM,SAAS,CAAA;AAC5C,EAAA,MAAM,MAAA,GAAS,MAAA,CAAO,GAAA,CAAI,KAAA,CAAM,MAAM,CAAA;AACtC,EAAA,IAAI,MAAA,IAAU,cAAc,UAAA,EAAY,OAAO,EAAE,IAAA,EAAM,QAAA,EAAU,aAAa,MAAA,EAAO;AACrF,EAAA,IAAI,MAAA,IAAU,cAAc,MAAA,EAAQ,OAAO,EAAE,KAAA,EAAO,QAAA,EAAU,WAAW,MAAA,EAAO;AAChF,EAAA,OAAO,EAAE,OAAO,QAAA,EAAS;AAC3B;AAYA,IAAM,OAAA,GAAU,CACd,OAAA,EACA,KAAA,EACA,WACA,MAAA,KACW;AACX,EAAA,MAAM,GAAA,GAAM,IAAI,GAAA,CAAI,OAAA,CAAQ,UAAU,CAAA;AACtC,EAAA,GAAA,CAAI,YAAA,CAAa,GAAA,CAAI,KAAA,CAAM,SAAA,EAAW,SAAS,CAAA;AAC/C,EAAA,GAAA,CAAI,YAAA,CAAa,GAAA,CAAI,KAAA,CAAM,MAAA,EAAQ,MAAM,CAAA;AACzC,EAAA,OAAO,IAAI,QAAA,EAAS;AACtB,CAAA;AAMO,SAAS,iBAAA,CACd,UAAA,EACA,UAAA,EACA,OAAA,GAAkC,EAAC,EAChB;AACnB,EAAA,MAAM,MAAM,OAAO,UAAA,KAAe,WAAW,IAAI,GAAA,CAAI,UAAU,CAAA,GAAI,UAAA;AACnE,EAAA,MAAM,KAAA,GAAQ,UAAA,CAAW,OAAA,CAAQ,SAAS,CAAA;AAC1C,EAAA,MAAM,EAAE,UAAS,GAAI,UAAA;AACrB,EAAA,OAAO;AAAA,IACL,OAAO,UAAA,CAAW,KAAA;AAAA,IAClB,aAAa,QAAA,CAAS,WAAA;AAAA,IACtB,iBAAiB,QAAA,CAAS,eAAA;AAAA,IAC1B,GAAI,QAAA,CAAS,WAAA,IAAe,QAAA,CAAS,YACjC,EAAE,WAAA,EAAa,OAAA,CAAQ,GAAA,EAAK,OAAO,MAAA,EAAQ,QAAA,CAAS,SAAS,CAAA,KAC7D,EAAC;AAAA,IACL,GAAI,QAAA,CAAS,eAAA,IAAmB,QAAA,CAAS,cACrC,EAAE,WAAA,EAAa,OAAA,CAAQ,GAAA,EAAK,OAAO,UAAA,EAAY,QAAA,CAAS,WAAW,CAAA,KACnE;AAAC,GACP;AACF","file":"chunk-5QOFKGKD.js","sourcesContent":["/* eslint-disable @typescript-eslint/no-explicit-any */\n\n/**\n * Framework-agnostic cursor pagination (ADR-0003 §SDK). Mirrors Shopify Hydrogen's\n * `getPaginationVariables` + `<Pagination>` over a relay-style connection\n * (`{ nodes, pageInfo { hasNextPage, hasPreviousPage, startCursor, endCursor } }`). Generic over the\n * connection's node type, so it works with any Storefront API connection (today: `Cart.lines`).\n *\n * Pure logic, no React — the React `<Pagination>` render-props component is a thin wrapper.\n */\n\n/** Relay PageInfo as the Storefront API emits it. */\nexport interface PageInfo {\n readonly hasNextPage: boolean\n readonly hasPreviousPage: boolean\n readonly startCursor?: string | null\n readonly endCursor?: string | null\n}\n\nexport interface Connection<T> {\n readonly nodes: readonly T[]\n readonly pageInfo: PageInfo\n}\n\nexport interface ConnectionEdge<T> {\n readonly node?: T | null\n}\n\nexport interface FlattenableConnection<T> {\n readonly nodes?: T[] | readonly T[] | null\n readonly edges?: readonly (ConnectionEdge<T> | null)[] | null\n}\n\ntype IsAny<T> = 0 extends 1 & T ? true : false\n\ntype NodeFromConnection<C> =\n IsAny<C> extends true\n ? any\n : C extends { readonly nodes?: readonly (infer Node)[] | null }\n ? Node\n : C extends { readonly edges?: readonly (infer Edge | null)[] | null }\n ? Edge extends { readonly node?: infer Node | null }\n ? NonNullable<Node>\n : unknown\n : unknown\n\n/**\n * Hydrogen-compatible connection flattener. Shopify code often accepts either Storefront API\n * `nodes` connections or Relay `edges[].node` connections; migrated routes can keep that shape.\n */\nexport function flattenConnection(connection: null | undefined): []\nexport function flattenConnection<T>(connection: FlattenableConnection<T>): T[]\nexport function flattenConnection<C extends FlattenableConnection<unknown>>(\n connection: C | null | undefined,\n): NodeFromConnection<C>[]\nexport function flattenConnection(\n connection: FlattenableConnection<unknown> | null | undefined,\n): unknown[] {\n if (connection == null) return []\n if (connection.nodes != null) return [...connection.nodes]\n return (connection.edges ?? []).flatMap((edge) => {\n const node = edge?.node\n return node == null ? [] : [node]\n })\n}\n\n/** The query variables a connection field takes for forward/backward paging. */\nexport interface PaginationVariables {\n first?: number\n last?: number\n startCursor?: string\n endCursor?: string\n}\n\nexport interface PaginationOptions {\n pageSize?: number\n pageBy?: number\n /** Isolates the URL params when multiple paginations share a page (`{ns}_direction`/`{ns}_cursor`). */\n namespace?: string\n}\n\nconst paramNames = (namespace?: string): { direction: string; cursor: string } => {\n const prefix = namespace ? `${namespace}_` : ''\n return { direction: `${prefix}direction`, cursor: `${prefix}cursor` }\n}\n\nconst toSearchParams = (input: URL | Request | URLSearchParams | string): URLSearchParams => {\n if (input instanceof URLSearchParams) return input\n if (input instanceof URL) return input.searchParams\n if (typeof input === 'string') return new URL(input).searchParams\n return new URL(input.url).searchParams\n}\n\n/**\n * Read `direction`/`cursor` from the request URL and return the connection variables for the page:\n * forward → `{ first, endCursor }`, backward → `{ last, startCursor }`, no params → `{ first }`.\n */\nexport function getPaginationVariables(\n input: URL | Request | URLSearchParams | string,\n options: PaginationOptions,\n): PaginationVariables {\n const pageSize = options.pageSize ?? options.pageBy ?? 20\n const params = toSearchParams(input)\n const names = paramNames(options.namespace)\n const direction = params.get(names.direction)\n const cursor = params.get(names.cursor)\n if (cursor && direction === 'previous') return { last: pageSize, startCursor: cursor }\n if (cursor && direction === 'next') return { first: pageSize, endCursor: cursor }\n return { first: pageSize }\n}\n\nexport interface PaginationInfo<T> {\n readonly nodes: readonly T[]\n readonly hasNextPage: boolean\n readonly hasPreviousPage: boolean\n /** Same-page URL with `direction=next` + the end cursor; absent when there is no next page. */\n readonly nextPageUrl: string\n /** Same-page URL with `direction=previous` + the start cursor; absent when there is no previous page. */\n readonly prevPageUrl: string\n}\n\nconst pageUrl = (\n current: URL,\n names: { direction: string; cursor: string },\n direction: 'next' | 'previous',\n cursor: string,\n): string => {\n const url = new URL(current.toString())\n url.searchParams.set(names.direction, direction)\n url.searchParams.set(names.cursor, cursor)\n return url.toString()\n}\n\n/**\n * Decorate a connection with the current page's nodes + the next/previous page URLs (built from the\n * current URL + the connection's cursors). A theme renders links to `nextPageUrl`/`prevPageUrl`.\n */\nexport function getPaginationInfo<T>(\n connection: Connection<T>,\n currentUrl: URL | string,\n options: { namespace?: string } = {},\n): PaginationInfo<T> {\n const url = typeof currentUrl === 'string' ? new URL(currentUrl) : currentUrl\n const names = paramNames(options.namespace)\n const { pageInfo } = connection\n return {\n nodes: connection.nodes,\n hasNextPage: pageInfo.hasNextPage,\n hasPreviousPage: pageInfo.hasPreviousPage,\n ...(pageInfo.hasNextPage && pageInfo.endCursor\n ? { nextPageUrl: pageUrl(url, names, 'next', pageInfo.endCursor) }\n : {}),\n ...(pageInfo.hasPreviousPage && pageInfo.startCursor\n ? { prevPageUrl: pageUrl(url, names, 'previous', pageInfo.startCursor) }\n : {}),\n } as PaginationInfo<T>\n}\n"]}
@@ -0,0 +1,239 @@
1
+ // src/settings/merge.ts
2
+ function mergeSettings(inputs, stored, preset) {
3
+ const out = {};
4
+ for (const input of inputs) {
5
+ const storedValue = stored?.[input.name];
6
+ const presetValue = preset?.[input.name];
7
+ out[input.name] = storedValue !== void 0 ? storedValue : presetValue !== void 0 ? presetValue : input.defaultValue;
8
+ }
9
+ return out;
10
+ }
11
+ function mergeSection(args) {
12
+ const settings = mergeSettings(
13
+ args.schema.settings,
14
+ args.instance?.settings,
15
+ args.preset?.settings
16
+ );
17
+ if (args.instance?.settings.disabled === true) settings.disabled = true;
18
+ const blockDefs = new Map((args.schema.blocks ?? []).map((b) => [b.type, b]));
19
+ const blocks = (args.instance?.blocks ?? []).map((b) => ({
20
+ ...b.id !== void 0 ? { id: b.id } : {},
21
+ type: b.type,
22
+ settings: mergeSettings(blockDefs.get(b.type)?.settings ?? [], b.settings, void 0)
23
+ }));
24
+ return { id: args.instance?.id ?? "", type: args.schema.type, settings, blocks };
25
+ }
26
+ function resolveValuesDocument(args, values) {
27
+ const sections = values.sections.map((instance) => {
28
+ const schema = args.schemas[instance.type];
29
+ if (schema === void 0) {
30
+ return {
31
+ id: instance.id,
32
+ type: instance.type,
33
+ settings: instance.settings,
34
+ blocks: instance.blocks.map((b) => ({
35
+ ...b.id !== void 0 ? { id: b.id } : {},
36
+ type: b.type,
37
+ settings: b.settings
38
+ }))
39
+ };
40
+ }
41
+ const preset = args.presets?.[instance.type];
42
+ return mergeSection({ schema, instance, ...preset !== void 0 ? { preset } : {} });
43
+ });
44
+ return { shopId: values.shopId, sections };
45
+ }
46
+ function resolveDocumentRecord(args, documents) {
47
+ if (documents === void 0) return void 0;
48
+ const resolved = {};
49
+ for (const [handle, document] of Object.entries(documents)) {
50
+ if (document !== void 0) resolved[handle] = resolveValuesDocument(args, document);
51
+ }
52
+ return resolved;
53
+ }
54
+ function resolveThemeSettings(args) {
55
+ const root = resolveValuesDocument(args, args.values);
56
+ const global = mergeSettings(args.globalInputs ?? [], args.globalStored, args.globalPreset);
57
+ const templates = resolveDocumentRecord(args, args.templates);
58
+ const sectionGroups = resolveDocumentRecord(args, args.sectionGroups);
59
+ return {
60
+ sections: root.sections,
61
+ global,
62
+ ...templates !== void 0 ? { templates } : {},
63
+ ...sectionGroups !== void 0 ? { sectionGroups } : {}
64
+ };
65
+ }
66
+
67
+ // src/settings/store.ts
68
+ function createThemeSettingsStore(initial) {
69
+ let snapshot = initial;
70
+ const listeners = /* @__PURE__ */ new Set();
71
+ const clampIndex = (index, length) => {
72
+ if (index === void 0 || Number.isNaN(index)) return length;
73
+ return Math.max(0, Math.min(Math.trunc(index), length));
74
+ };
75
+ const emit = () => {
76
+ for (const listener of listeners) listener();
77
+ };
78
+ const updateSection = (sectionId, update) => {
79
+ if (sectionId.startsWith("theme:")) return false;
80
+ let changed = false;
81
+ const sections = snapshot.sections.map((section) => {
82
+ if (section.id !== sectionId) return section;
83
+ const next = update(section);
84
+ if (next === null || next === section) return section;
85
+ changed = true;
86
+ return next;
87
+ });
88
+ if (!changed) return false;
89
+ snapshot = { ...snapshot, sections };
90
+ emit();
91
+ return true;
92
+ };
93
+ return {
94
+ getSnapshot: () => snapshot,
95
+ subscribe: (listener) => {
96
+ listeners.add(listener);
97
+ return () => {
98
+ listeners.delete(listener);
99
+ };
100
+ },
101
+ patchSection: (instanceId, settings) => {
102
+ if (instanceId.startsWith("theme:")) return;
103
+ let changed = false;
104
+ const sections = snapshot.sections.map((section) => {
105
+ if (section.id !== instanceId) return section;
106
+ changed = true;
107
+ return { ...section, settings };
108
+ });
109
+ if (!changed) return;
110
+ snapshot = { ...snapshot, sections };
111
+ emit();
112
+ },
113
+ insertSection: (section, index) => {
114
+ if (section.id.startsWith("theme:")) return;
115
+ if (snapshot.sections.some((existing) => existing.id === section.id)) return;
116
+ const next = [...snapshot.sections];
117
+ next.splice(clampIndex(index, next.length), 0, section);
118
+ snapshot = { ...snapshot, sections: next };
119
+ emit();
120
+ },
121
+ removeSection: (instanceId) => {
122
+ if (instanceId.startsWith("theme:")) return;
123
+ const sections = snapshot.sections.filter((section) => section.id !== instanceId);
124
+ if (sections.length === snapshot.sections.length) return;
125
+ snapshot = { ...snapshot, sections };
126
+ emit();
127
+ },
128
+ moveSection: (instanceId, index) => {
129
+ if (instanceId.startsWith("theme:")) return;
130
+ const fromIndex = snapshot.sections.findIndex((section2) => section2.id === instanceId);
131
+ if (fromIndex === -1) return;
132
+ const next = [...snapshot.sections];
133
+ const [section] = next.splice(fromIndex, 1);
134
+ if (section === void 0) return;
135
+ const toIndex = clampIndex(index, next.length);
136
+ if (fromIndex === toIndex) return;
137
+ next.splice(toIndex, 0, section);
138
+ snapshot = { ...snapshot, sections: next };
139
+ emit();
140
+ },
141
+ patchGlobal: (settings) => {
142
+ if (settings === snapshot.global) return;
143
+ snapshot = { ...snapshot, global: settings };
144
+ emit();
145
+ },
146
+ patchBlock: (sectionId, blockId, settings) => {
147
+ updateSection(sectionId, (section) => {
148
+ let changed = false;
149
+ const blocks = section.blocks.map((block) => {
150
+ if (block.id !== blockId) return block;
151
+ changed = true;
152
+ return { ...block, settings };
153
+ });
154
+ return changed ? { ...section, blocks } : null;
155
+ });
156
+ },
157
+ insertBlock: (sectionId, block, index) => {
158
+ updateSection(sectionId, (section) => {
159
+ if (block.id !== void 0 && section.blocks.some((existing) => existing.id === block.id)) {
160
+ return null;
161
+ }
162
+ const blocks = [...section.blocks];
163
+ blocks.splice(clampIndex(index, blocks.length), 0, block);
164
+ return { ...section, blocks };
165
+ });
166
+ },
167
+ removeBlock: (sectionId, blockId) => {
168
+ updateSection(sectionId, (section) => {
169
+ const blocks = section.blocks.filter((block) => block.id !== blockId);
170
+ return blocks.length === section.blocks.length ? null : { ...section, blocks };
171
+ });
172
+ },
173
+ moveBlock: (sectionId, blockId, index) => {
174
+ updateSection(sectionId, (section) => {
175
+ const fromIndex = section.blocks.findIndex((block2) => block2.id === blockId);
176
+ if (fromIndex === -1) return null;
177
+ const blocks = [...section.blocks];
178
+ const [block] = blocks.splice(fromIndex, 1);
179
+ if (block === void 0) return null;
180
+ const toIndex = clampIndex(index, blocks.length);
181
+ if (fromIndex === toIndex) return null;
182
+ blocks.splice(toIndex, 0, block);
183
+ return { ...section, blocks };
184
+ });
185
+ },
186
+ replaceSections: (sections) => {
187
+ const next = sections.filter((section) => !section.id.startsWith("theme:"));
188
+ if (next.length === snapshot.sections.length && next.every((section, index) => section === snapshot.sections[index])) {
189
+ return;
190
+ }
191
+ snapshot = { ...snapshot, sections: next };
192
+ emit();
193
+ },
194
+ replaceSectionGroup: (handle, document) => {
195
+ if (handle.trim() === "") return;
196
+ const sectionGroups = snapshot.sectionGroups ?? {};
197
+ const nextDocument = {
198
+ ...document,
199
+ sections: document.sections.filter((section) => !section.id.startsWith("theme:"))
200
+ };
201
+ if (sectionGroups[handle] === nextDocument) return;
202
+ snapshot = {
203
+ ...snapshot,
204
+ sectionGroups: {
205
+ ...sectionGroups,
206
+ [handle]: nextDocument
207
+ }
208
+ };
209
+ emit();
210
+ }
211
+ };
212
+ }
213
+
214
+ // src/settings/fetch.ts
215
+ var SAME_ORIGIN_PATH = /^\/(?![/\\])/;
216
+ var HTTPS_ENDPOINT = /^https:\/\//;
217
+ var DEFAULT_ENDPOINT = "/api/storefront/settings";
218
+ async function fetchThemeSettings(options = {}) {
219
+ const endpoint = options.endpoint ?? DEFAULT_ENDPOINT;
220
+ if (!SAME_ORIGIN_PATH.test(endpoint) && !(options.allowCrossOrigin === true && HTTPS_ENDPOINT.test(endpoint))) {
221
+ throw new Error(`theme-settings endpoint must be a same-origin path, got '${endpoint}'`);
222
+ }
223
+ const doFetch = options.fetch ?? fetch;
224
+ const headers = {};
225
+ if (options.auth !== void 0) {
226
+ headers[options.auth.tokenHeader ?? "x-kotao-storefront-token"] = options.auth.token;
227
+ if (options.auth.siteId !== void 0 && options.auth.siteId.trim().length > 0) {
228
+ headers["x-kotao-site-id"] = options.auth.siteId;
229
+ }
230
+ }
231
+ const init = Object.keys(headers).length > 0 ? { headers } : void 0;
232
+ const res = await doFetch(endpoint, init);
233
+ if (!res.ok) throw new Error(`theme settings fetch failed: HTTP ${res.status}`);
234
+ return await res.json();
235
+ }
236
+
237
+ export { createThemeSettingsStore, fetchThemeSettings, mergeSection, mergeSettings, resolveThemeSettings };
238
+ //# sourceMappingURL=chunk-GCQI6OKH.js.map
239
+ //# sourceMappingURL=chunk-GCQI6OKH.js.map