@kotao/storefront 0.2.8 → 0.2.10
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/dist/{chunk-RFM27BKZ.js → chunk-BV4OLXV4.js} +7 -3
- package/dist/chunk-BV4OLXV4.js.map +1 -0
- package/dist/{chunk-OGWYTYYS.js → chunk-GR4QJ7EC.js} +14 -22
- package/dist/chunk-GR4QJ7EC.js.map +1 -0
- package/dist/{chunk-3YQXJNR7.js → chunk-PWVXOMDE.js} +18 -9
- package/dist/chunk-PWVXOMDE.js.map +1 -0
- package/dist/{chunk-DTKLAOKR.js → chunk-XZVCUAZU.js} +14 -4
- package/dist/chunk-XZVCUAZU.js.map +1 -0
- package/dist/{client-CwOPPpFF.d.ts → client-D-M3Y1ul.d.ts} +2 -0
- package/dist/{createCartHandler-7DdKB-GF.d.ts → createCartHandler-DJEZW4o2.d.ts} +1 -1
- package/dist/editor.d.ts +1 -1
- package/dist/editor.js +1 -1
- package/dist/events-react.d.ts +1 -1
- package/dist/events-react.js +20 -9
- package/dist/events-react.js.map +1 -1
- package/dist/events.d.ts +3 -3
- package/dist/image.d.ts +1 -1
- package/dist/image.js +1 -1
- package/dist/index.d.ts +19 -12
- package/dist/index.js +29 -12
- package/dist/index.js.map +1 -1
- package/dist/react-router.d.ts +19 -7
- package/dist/react-router.js +43 -4
- package/dist/react-router.js.map +1 -1
- package/dist/react.d.ts +4 -5
- package/dist/react.js +144 -21
- package/dist/react.js.map +1 -1
- package/dist/{routing-Bz3Cg0BW.d.ts → routing-D2GFXAa0.d.ts} +8 -3
- package/dist/{shop-CnrnSAJw.d.ts → shop-4y-4z3oZ.d.ts} +5 -4
- package/dist/{types-DZX8ScsJ.d.ts → types-C4yXcret.d.ts} +2 -1
- package/package.json +2 -2
- package/dist/chunk-3YQXJNR7.js.map +0 -1
- package/dist/chunk-DTKLAOKR.js.map +0 -1
- package/dist/chunk-OGWYTYYS.js.map +0 -1
- package/dist/chunk-RFM27BKZ.js.map +0 -1
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { S as StoredThemeSettings } from './fetch-Bq_cc8_G.js';
|
|
2
2
|
import { C as CacheStrategy, K as KotaoCache, S as StorefrontClient, b as StorefrontCacheInput } from './client-D51WdvYR.js';
|
|
3
|
-
import { C as CartHandler } from './createCartHandler-
|
|
4
|
-
import { E as EventsClient } from './client-
|
|
5
|
-
import { H as HydrogenSession } from './types-
|
|
3
|
+
import { C as CartHandler } from './createCartHandler-DJEZW4o2.js';
|
|
4
|
+
import { E as EventsClient } from './client-D-M3Y1ul.js';
|
|
5
|
+
import { H as HydrogenSession } from './types-C4yXcret.js';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Typed customer-account shapes for the `@kotao/storefront` SDK surface (#91). The names/signatures
|
|
@@ -165,6 +165,9 @@ interface Geo {
|
|
|
165
165
|
colo: string | null;
|
|
166
166
|
botScore: number | null;
|
|
167
167
|
clientIp: string | null;
|
|
168
|
+
/** Approximate IP geolocation from the trusted gateway, never browser GPS. */
|
|
169
|
+
latitude?: number;
|
|
170
|
+
longitude?: number;
|
|
168
171
|
}
|
|
169
172
|
/** Gateway side (#81): produce the two header values for a geo payload. */
|
|
170
173
|
declare function signGeo(geo: Geo, key: string): Promise<{
|
|
@@ -179,6 +182,8 @@ type WithCache = <T>(key: string, strategy: CacheStrategy, fn: () => Promise<T>)
|
|
|
179
182
|
declare const createWithCache: (cache: KotaoCache) => WithCache;
|
|
180
183
|
|
|
181
184
|
interface KotaoEnv {
|
|
185
|
+
/** Content-addressed static asset tree supplied by the build service. */
|
|
186
|
+
KOTAO_ASSET_BASE?: string;
|
|
182
187
|
STOREFRONT_API_ENDPOINT: string;
|
|
183
188
|
STOREFRONT_API_TOKEN: string;
|
|
184
189
|
STOREFRONT_API_TOKEN_HEADER?: string;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { j as CartFormAction } from './types-
|
|
1
|
+
import { j as CartFormAction } from './types-C4yXcret.js';
|
|
2
2
|
import { ReactNode } from 'react';
|
|
3
3
|
|
|
4
4
|
interface CartFormProps {
|
|
@@ -33,8 +33,8 @@ declare const CartForm: ((_props: CartFormProps) => null) & {
|
|
|
33
33
|
};
|
|
34
34
|
|
|
35
35
|
/**
|
|
36
|
-
*
|
|
37
|
-
*
|
|
36
|
+
* Shared native-CMP consent state. Unknown choices deny optional categories; the versioned cookie
|
|
37
|
+
* is the same contract used by dispatch and checkout.
|
|
38
38
|
*/
|
|
39
39
|
type ConsentBucket = 'analytics' | 'marketing' | 'preferences';
|
|
40
40
|
type ConsentState = Readonly<Record<ConsentBucket, boolean>>;
|
|
@@ -166,6 +166,7 @@ interface PixelTransportOptions {
|
|
|
166
166
|
/** Same-origin carve-out endpoint (default '/api/storefront/pixel') — never cross-origin. */
|
|
167
167
|
readonly endpoint?: string;
|
|
168
168
|
readonly batchSize?: number;
|
|
169
|
+
readonly flushAfterMs?: number;
|
|
169
170
|
/**
|
|
170
171
|
* The actual network send. Required + injected: the browser sender (`sendBeacon`/keepalive `fetch`)
|
|
171
172
|
* is DOM-specific and lives in the React layer, keeping this batching core framework-agnostic and
|
|
@@ -179,7 +180,7 @@ interface PixelTransport {
|
|
|
179
180
|
}
|
|
180
181
|
/**
|
|
181
182
|
* Batches pixel events and POSTs them to the same-origin carve-out. Flushes on `batchSize` or an
|
|
182
|
-
* explicit `flush()` (the provider flushes on page-hide). The endpoint is same-origin so themes need
|
|
183
|
+
* explicit `flush()` (the provider flushes on page-hide), or after one second. The endpoint is same-origin so themes need
|
|
183
184
|
* no CORS and no token is exposed in the browser.
|
|
184
185
|
*/
|
|
185
186
|
declare function createPixelTransport(options: PixelTransportOptions): PixelTransport;
|
|
@@ -85,7 +85,8 @@ interface CartDiscountAllocation {
|
|
|
85
85
|
interface CartCost {
|
|
86
86
|
readonly subtotalAmount: Money;
|
|
87
87
|
readonly totalAmount: Money;
|
|
88
|
-
|
|
88
|
+
/** Null when destination taxes have not been configured. */
|
|
89
|
+
readonly totalTaxAmount: Money | null;
|
|
89
90
|
readonly discountTotal: Money;
|
|
90
91
|
readonly taxLines: readonly CartTaxLine[];
|
|
91
92
|
readonly discountAllocations: readonly CartDiscountAllocation[];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kotao/storefront",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.10",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"description": "Kotao Storefronts theme SDK — framework-agnostic server runtime, React Router 8 binding, and authenticated source-authoritative React/Hydrogen editor bridge (ADR-0003/ADR-0047). Leaf package (ADR-0006).",
|
|
@@ -72,7 +72,7 @@
|
|
|
72
72
|
"LICENSE"
|
|
73
73
|
],
|
|
74
74
|
"dependencies": {
|
|
75
|
-
"@kotao/storefront-contracts": "^0.2.
|
|
75
|
+
"@kotao/storefront-contracts": "^0.2.5"
|
|
76
76
|
},
|
|
77
77
|
"peerDependencies": {
|
|
78
78
|
"react": "^18.2.0 || ^19.0.0",
|
|
@@ -1 +0,0 @@
|
|
|
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"]}
|